Skip to content

Commit

Permalink
Merge pull request #4 from draconas1/dev
Browse files Browse the repository at this point in the history
Auto set actor to status bloodied, dying or dead based on HP.
  • Loading branch information
draconas1 authored Oct 24, 2021
2 parents 7d4bcc0 + 346e08c commit f006de9
Show file tree
Hide file tree
Showing 9 changed files with 240 additions and 66 deletions.
34 changes: 22 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ This requires the D&D 4E setting: https://github.com/EndlesNights/dnd4eBeta
* **Foundry VTT Compatibility**: 0.8.9
* **DnD4E system compatibility**: 0.2.43

## Masterplan Importer
An importer for Monster Data from Masterplan for D&D 4E.
You will need https://github.com/draconas1/masterplan-json-export to export the data, this module provides the importer.

### Installation
## Installation
* Open the Foundry application and click **"Install Module"** in the **"Addon Modules"** tab.
* Paste the following link: https://raw.githubusercontent.com/draconas1/foundry-4e-tools/main/module.json
* Click "Install"
* Enable it on your game.
*

## Masterplan Importer
An importer for Monster Data from Masterplan for D&D 4E.
You will need https://github.com/draconas1/masterplan-json-export to export the data, this module provides the importer.

### Use
* The GM will get button in the actors tab for "import monsters"
Expand All @@ -32,18 +33,27 @@ The folder import and duplicate checking have configuration options that let you
### Addons & Compatibility
* Prototype tokens will automatically get square auras compatible with [Token Auras](https://foundryvtt.com/packages/token-auras)

### Notes
There are probably a lot of bugs, here are some I know about:
1. It still has legacy code and UI for the "todo list" tutorial all over it.
2. Empty folders can be created for empty encounters.

## Tools
TBD: Auto-bloodied token
### Auto-Bloodied & Dead

#### Use
* Tokens will automatically gain a big bloodied status icon when reduced to bloodied value
* NPC's will automatically gain a dead status icon and be set as defeated in the combat tracker when reduced to 0 HP.
* PC's will automatically gain a dying status icon when reduced to 0 HP
* PC's will automatically gain a dead status icon and be set as defeated in the combat tracker when reduced to -bloodied HP.

Gaining HP will set them back to the relevant status and undefeat them in combat tracker.

#### Configuration
There are configuration values for automatically setting bloodied and automatically setting the various dead states.

### Replace Dead Status Icon
I found the dead status icon hard to see in some tokens. There is now a configuration option that replaces it with a big red skull and crossbones.

## Data
Some compendiums of [4th Edition System Reference Document](http://weirdzine.com/wp-content/uploads/2015/07/4E_SRD-1.pdf) compliant data.
* Weapons
* TBD: Armour
* Armour

### Addons & Compatibility
* Compendiums are arranged using [Compendium Folders](https://foundryvtt.com/packages/compendium-folders)
Expand Down
3 changes: 3 additions & 0 deletions icons/dead.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions languages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,22 @@
"do-not-import-duplicates-in-folder": {
"Name": "Default: Only check duplicates in same folder?",
"Hint": "Default setting for only checking within the encounter folder to see if a monster already exists."
},
"bloodied-icon": {
"Name": "Auto Bloodied",
"Hint": "Apply the Bloodied status icon (large) automatically when a creature is bloodied."
},
"dead-icon": {
"Name": "Auto Defeated",
"Hint": "When a creature hits 0 HP it is given the dead status and marked as defeated in combat if an NPC, or dying if a PC. PC's are marked dead on -bloodied HP"
},
"bloodied-tint": {
"Name": "Auto Bloodied: Red Tint",
"Hint": "Tint the token red automatically when a creature is bloodied."
},
"change-dead-icon": {
"Name": "Change Dead Status Icon",
"Hint": "Changes the Dead Status icon to be a red skull and crossbones which I found more obvious than the default"
}
},
"confirms": {
Expand Down
4 changes: 2 additions & 2 deletions module.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
"title": "Dracs 4E tools for Foundry VTT",
"description": "Imports Creatures from Masterplan into Foundry",
"author": "Drac",
"version": "0.0.4",
"version": "0.0.5",
"minimumCoreVersion": "0.8.0",
"compatibleCoreVersion": "0.8.9",
"url": "https://github.com/draconas1/foundry-4e-tools",
"manifest": "https://raw.githubusercontent.com/draconas1/foundry-4e-tools/main/module.json",
"download": "https://github.com/draconas1/foundry-4e-tools/releases/download/0.0.4/foundry-4e-tools.zip",
"download": "https://github.com/draconas1/foundry-4e-tools/releases/download/0.0.5/foundry-4e-tools.zip",
"esmodules": [
"module/4e-tools.js"
],
Expand Down
73 changes: 24 additions & 49 deletions module/4e-tools.js
Original file line number Diff line number Diff line change
@@ -1,46 +1,48 @@
import {renderSidebarTab} from "./hooks.js";
import {setBloodiedDeadOnHPChange} from "./hooks/auto-bloodied-dead.js";
import {addImportMonsterButton} from "./hooks/import-button.js";
import {setDeadIcon} from "./hooks/set-dead-icon.js";
import {registerConfigs} from "./config.js";

Hooks.once('devModeReady', ({ registerPackageDebugFlag }) => {
registerPackageDebugFlag(DnD4eTools.ID);
});

Hooks.once('init', () => {
DnD4eTools.initialize();
registerConfigs();
});

Hooks.on('renderSidebarTab', renderSidebarTab);
Hooks.on('renderSidebarTab', addImportMonsterButton);
Hooks.on('updateActor', setBloodiedDeadOnHPChange);
Hooks.once("setup", setDeadIcon);

/**
* A single ToDo in our list of Todos.
* @typedef {Object} ToDo
* @property {string} id - A unique ID to identify this todo.
* @property {string} label - The text of the todo.
* @property {boolean} isDone - Marks whether the todo is done.
* @property {string} userId - The user who owns this todo.
*/
export default class DnD4eTools {
static ID = 'foundry-4e-tools';
static NAME = '4e-tools';

static FLAGS = {
TODOS: 'todos'
ORIGINAL_DEAD_STATUS_ICON: '',
MODDED_DEAD_STATUS_ICON: "modules/foundry-4e-tools/icons/dead.svg"
}

static SETTINGS = {
INJECT_BUTTON: 'inject-button',
CREATE_IN_ENCOUNTER_FOLDERS: 'create-in-encounter-folders',
DO_NOT_DUPLICATE: 'do-not-import-duplicates',
DO_NOT_DUPLICATE_IN_FOLDER: 'do-not-import-duplicates-in-folder',
BLOODIED_ICON: 'bloodied-icon',
DEAD_ICON: 'dead-icon',
CHANGE_DEAD_ICON: 'change-dead-icon'
}

static TEMPLATES = {
TODOLIST: `modules/${this.ID}/templates/todo-list.hbs`,
IMPORTER_INPUT: `modules/${this.ID}/templates/importer-input.hbs`,
}

static devMode() {
return game.modules.get('_dev-mode')?.api?.getPackageDebugValue(this.ID);
}

static log(force, ...args) {
const shouldLog = force || this.devMode();

Expand All @@ -49,45 +51,18 @@ export default class DnD4eTools {
}
}

static setDeadIcon() {
const deadStatus = CONFIG.statusEffects.find(x => x.id === "dead");
if (game.settings.get(DnD4eTools.ID, DnD4eTools.SETTINGS.CHANGE_DEAD_ICON)) {
deadStatus.icon = DnD4eTools.FLAGS.MODDED_DEAD_STATUS_ICON
}
else {
deadStatus.icon = DnD4eTools.FLAGS.ORIGINAL_DEAD_STATUS_ICON
}
}

static initialize() {
//this.toDoListConfig = new ToDoListConfig();
console.log(this.NAME + " | Initialising 4E Tools and Masterplan Importer")
// game.settings.register(this.ID, this.SETTINGS.INJECT_BUTTON, {
// name: `TOOLS4E.settings.${this.SETTINGS.INJECT_BUTTON}.Name`,
// default: true,
// type: Boolean,
// scope: 'client',
// config: true,
// hint: `TOOLS4E.settings.${this.SETTINGS.INJECT_BUTTON}.Hint`,
// onChange: () => ui.players.render()
// });

game.settings.register(this.ID, this.SETTINGS.CREATE_IN_ENCOUNTER_FOLDERS, {
name: `TOOLS4E.settings.${this.SETTINGS.CREATE_IN_ENCOUNTER_FOLDERS}.Name`,
default: true,
type: Boolean,
scope: 'client',
config: true,
hint: `TOOLS4E.settings.${this.SETTINGS.CREATE_IN_ENCOUNTER_FOLDERS}.Hint`
});

game.settings.register(this.ID, this.SETTINGS.DO_NOT_DUPLICATE, {
name: `TOOLS4E.settings.${this.SETTINGS.DO_NOT_DUPLICATE}.Name`,
default: true,
type: Boolean,
scope: 'client',
config: true,
hint: `TOOLS4E.settings.${this.SETTINGS.DO_NOT_DUPLICATE}.Hint`
});

game.settings.register(this.ID, this.SETTINGS.DO_NOT_DUPLICATE_IN_FOLDER, {
name: `TOOLS4E.settings.${this.SETTINGS.DO_NOT_DUPLICATE_IN_FOLDER}.Name`,
default: true,
type: Boolean,
scope: 'client',
config: true,
hint: `TOOLS4E.settings.${this.SETTINGS.DO_NOT_DUPLICATE_IN_FOLDER}.Hint`
});
}
}

Expand Down
56 changes: 56 additions & 0 deletions module/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import DnD4eTools from "./4e-tools.js";

export function registerConfigs() {
game.settings.register(DnD4eTools.ID, DnD4eTools.SETTINGS.CREATE_IN_ENCOUNTER_FOLDERS, {
name: `TOOLS4E.settings.${DnD4eTools.SETTINGS.CREATE_IN_ENCOUNTER_FOLDERS}.Name`,
default: true,
type: Boolean,
scope: 'client',
config: true,
hint: `TOOLS4E.settings.${DnD4eTools.SETTINGS.CREATE_IN_ENCOUNTER_FOLDERS}.Hint`
});

game.settings.register(DnD4eTools.ID, DnD4eTools.SETTINGS.DO_NOT_DUPLICATE, {
name: `TOOLS4E.settings.${DnD4eTools.SETTINGS.DO_NOT_DUPLICATE}.Name`,
default: true,
type: Boolean,
scope: 'client',
config: true,
hint: `TOOLS4E.settings.${DnD4eTools.SETTINGS.DO_NOT_DUPLICATE}.Hint`
});

game.settings.register(DnD4eTools.ID, DnD4eTools.SETTINGS.DO_NOT_DUPLICATE_IN_FOLDER, {
name: `TOOLS4E.settings.${DnD4eTools.SETTINGS.DO_NOT_DUPLICATE_IN_FOLDER}.Name`,
default: true,
type: Boolean,
scope: 'client',
config: true,
hint: `TOOLS4E.settings.${DnD4eTools.SETTINGS.DO_NOT_DUPLICATE_IN_FOLDER}.Hint`
});

game.settings.register(DnD4eTools.ID, DnD4eTools.SETTINGS.BLOODIED_ICON, {
name: `TOOLS4E.settings.${DnD4eTools.SETTINGS.BLOODIED_ICON}.Name`,
default: true,
type: Boolean,
scope: 'client',
config: true,
hint: `TOOLS4E.settings.${DnD4eTools.SETTINGS.BLOODIED_ICON}.Hint`
});
game.settings.register(DnD4eTools.ID, DnD4eTools.SETTINGS.DEAD_ICON, {
name: `TOOLS4E.settings.${DnD4eTools.SETTINGS.DEAD_ICON}.Name`,
default: true,
type: Boolean,
scope: 'client',
config: true,
hint: `TOOLS4E.settings.${DnD4eTools.SETTINGS.DEAD_ICON}.Hint`
});
game.settings.register(DnD4eTools.ID, DnD4eTools.SETTINGS.CHANGE_DEAD_ICON, {
name: `TOOLS4E.settings.${DnD4eTools.SETTINGS.CHANGE_DEAD_ICON}.Name`,
default: false,
type: Boolean,
scope: 'client',
config: true,
hint: `TOOLS4E.settings.${DnD4eTools.SETTINGS.CHANGE_DEAD_ICON}.Hint`,
onChange: () => DnD4eTools.setDeadIcon()
});
}
107 changes: 107 additions & 0 deletions module/hooks/auto-bloodied-dead.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import DnD4eTools from "../4e-tools.js";

const dead = "dead"
const dying = "dying"
const bloodied = "bloodied"

export async function setBloodiedDeadOnHPChange(actor, change, options, userId) {
// only fire this for the user that changed the hp in the first place
if (userId !== game.user.id) { return;}
// only fire if the HP changed
if(change.data?.attributes?.hp?.hasOwnProperty('value')) {
DnD4eTools.log(false, "Firing bloodied update for " + game.users.get(userId).data.name)
const newHP = change.data.attributes.hp.value
const maxHP = actor.data.data.attributes.hp.max
const bloodiedHP = maxHP / 2
if (!maxHP) {
DnD4eTools.log(false, "Could not get actor max HP")
return;
}
DnD4eTools.log(false, newHP + "/" + maxHP)

// remove any of bloodied, dying or dead as a batch or else we get weird concurrent update issues
const statusIds = findEffectIds(bloodied, actor).concat(findEffectIds(dying, actor)).concat(findEffectIds(dead, actor))
await actor.deleteEmbeddedDocuments("ActiveEffect", statusIds)

if (newHP <= bloodiedHP && newHP > 0 && game.settings.get(DnD4eTools.ID, DnD4eTools.SETTINGS.BLOODIED_ICON)) {
applyEffectIfNotPresent(bloodied, actor)
}
if (newHP <= 0 && game.settings.get(DnD4eTools.ID, DnD4eTools.SETTINGS.DEAD_ICON)) {
if (actor.data.type === 'NPC') {
DnD4eTools.log(false, "NPC Dead!")
applyEffectIfNotPresent(dead, actor)
defeatInCombat(actor)
}
else {
if (newHP <= 0 - bloodiedHP) {
DnD4eTools.log(false, "PC Dead!")
applyEffectIfNotPresent(dead, actor)
defeatInCombat(actor)
}
else {
DnD4eTools.log(false, "PC Dying!")
applyEffectIfNotPresent(dying, actor)
defeatInCombat(actor, false)
}
}
}
if (newHP > 0 && game.settings.get(DnD4eTools.ID, DnD4eTools.SETTINGS.DEAD_ICON)) {
defeatInCombat(actor, false)
}
}

function defeatInCombat(actor, defeated = true) {
const activeCombat = game.combat
// if we are not runnign a combat, stop now
if (!activeCombat) { return }
const updates = []
for (const token of actor.getActiveTokens()) {
const matchingCombatants = activeCombat.combatants?.contents?.filter(c => c.data.tokenId === token.id)
if (!matchingCombatants || matchingCombatants.length === 0) {
continue
}
const tokenUpdates = matchingCombatants.map(combatant => {
return {
_id: combatant.id,
defeated: defeated
}
})

tokenUpdates.forEach(update => updates.push(update))
}

if (!updates.length) { return }
DnD4eTools.log(false, updates)
activeCombat.updateEmbeddedDocuments("Combatant", updates)
}

function applyEffectIfNotPresent(statusToCheck, actor) {
const bloodiedEffect = actor.effects.find(x => x.data.flags.core?.statusId === statusToCheck)
if (bloodiedEffect) {
DnD4eTools.log(false, `Actor already has ${statusToCheck}, not reapplying`)
return
}
else {
DnD4eTools.log(false, `Actor does not have status ${statusToCheck}. Applying it`)
}

const status = CONFIG.statusEffects.find(x => x.id === statusToCheck)
const effect = {
...status,
"label" : game.i18n.localize(status.label),
"flags": {
"core": {
"statusId": statusToCheck,
"overlay": true
}
}
}
delete effect.id

ActiveEffect.create(effect, { parent : actor })
}

function findEffectIds(statusToCheck, actor) {
return actor.effects.filter(effect => effect.data.flags.core?.statusId === statusToCheck).map(effect => effect.id)
}
}
6 changes: 3 additions & 3 deletions module/hooks.js → module/hooks/import-button.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import CreatureImporterScreen from "./screens/creature-import.js";
import DnD4eTools from "./4e-tools.js";
import CreatureImporterScreen from "../screens/creature-import.js";
import DnD4eTools from "../4e-tools.js";

export function renderSidebarTab(activeTab, html) {
export function addImportMonsterButton(activeTab, html) {
// fire only fr GM on actors tab
if (activeTab.options.id === "actors" && game.user.isGM) {
DnD4eTools.log(false, "Adding import button to Actors tab")
Expand Down
Loading

0 comments on commit f006de9

Please sign in to comment.