diff --git a/module/actor.js b/module/actor.js index 8288404..034b26a 100644 --- a/module/actor.js +++ b/module/actor.js @@ -11,6 +11,7 @@ export class SimpleActor extends Actor { super.prepareDerivedData(); this.data.data.groups = this.data.data.groups || {}; this.data.data.attributes = this.data.data.attributes || {}; + EntitySheetHelper.clampResourceValues(this.data.data.attributes); } /* -------------------------------------------- */ @@ -20,6 +21,16 @@ export class SimpleActor extends Actor { return EntitySheetHelper.createDialog.call(this, data, options); } + /* -------------------------------------------- */ + + /** + * Is this Actor used as a template for other Actors? + * @type {boolean} + */ + get isTemplate() { + return !!this.getFlag("worldbuilding", "isTemplate"); + } + /* -------------------------------------------- */ /* Roll Data Preparation */ /* -------------------------------------------- */ @@ -236,4 +247,17 @@ export class SimpleActor extends Actor { } } } + + /* -------------------------------------------- */ + + /** @inheritdoc */ + async modifyTokenAttribute(attribute, value, isDelta = false, isBar = true) { + const current = foundry.utils.getProperty(this.data.data, attribute); + if ( !isBar || !isDelta || (current?.dtype !== "Resource") ) { + return super.modifyTokenAttribute(attribute, value, isDelta, isBar); + } + const updates = {[`data.${attribute}.value`]: Math.clamped(current.value + value, current.min, current.max)}; + const allowed = Hooks.call("modifyTokenAttribute", {attribute, value, isDelta, isBar}, updates); + return allowed !== false ? this.update(updates) : this; + } } diff --git a/module/helper.js b/module/helper.js index 4997798..dd04769 100644 --- a/module/helper.js +++ b/module/helper.js @@ -587,4 +587,21 @@ export class EntitySheetHelper { options: options }); } + + /* -------------------------------------------- */ + + /** + * Ensure the resource values are within the specified min and max. + * @param {object} attrs The Document's attributes. + */ + static clampResourceValues(attrs) { + const flat = foundry.utils.flattenObject(attrs); + for ( const [attr, value] of Object.entries(flat) ) { + const parts = attr.split("."); + if ( parts.pop() !== "value" ) continue; + const current = foundry.utils.getProperty(attrs, parts.join(".")); + if ( current?.dtype !== "Resource" ) continue; + foundry.utils.setProperty(attrs, attr, Math.clamped(value, current.min || 0, current.max || 0)); + } + } } diff --git a/module/item.js b/module/item.js index b4e0cce..99fb5a0 100644 --- a/module/item.js +++ b/module/item.js @@ -11,6 +11,7 @@ export class SimpleItem extends Item { super.prepareDerivedData(); this.data.data.groups = this.data.data.groups || {}; this.data.data.attributes = this.data.data.attributes || {}; + EntitySheetHelper.clampResourceValues(this.data.data.attributes); } /* -------------------------------------------- */ @@ -19,4 +20,14 @@ export class SimpleItem extends Item { static async createDialog(data={}, options={}) { return EntitySheetHelper.createDialog.call(this, data, options); } + + /* -------------------------------------------- */ + + /** + * Is this Item used as a template for other Items? + * @type {boolean} + */ + get isTemplate() { + return !!this.getFlag("worldbuilding", "isTemplate"); + } } diff --git a/module/macro.js b/module/macro.js index 4f07cbf..067b580 100644 --- a/module/macro.js +++ b/module/macro.js @@ -6,6 +6,7 @@ * @returns {Promise} */ export async function createWorldbuildingMacro(data, slot) { + if ( !data.roll || !data.label ) return false; const command = `const roll = new Roll("${data.roll}", actor ? actor.getRollData() : {}); roll.toMessage({speaker, flavor: "${data.label}"});`; let macro = game.macros.find(m => (m.name === data.label) && (m.command === command)); diff --git a/module/simple.js b/module/simple.js index 059bea6..6ffff4d 100644 --- a/module/simple.js +++ b/module/simple.js @@ -10,7 +10,7 @@ import { SimpleItemSheet } from "./item-sheet.js"; import { SimpleActorSheet } from "./actor-sheet.js"; import { preloadHandlebarsTemplates } from "./templates.js"; import { createWorldbuildingMacro } from "./macro.js"; -import { SimpleTokenDocument } from "./simpletokendocument.js"; +import { SimpleToken, SimpleTokenDocument } from "./token.js"; /* -------------------------------------------- */ /* Foundry VTT Initialization */ @@ -40,9 +40,8 @@ Hooks.once("init", async function() { // Define custom Document classes CONFIG.Actor.documentClass = SimpleActor; CONFIG.Item.documentClass = SimpleItem; - - // Update TokenDocument with overrided getBarAttribute method CONFIG.Token.documentClass = SimpleTokenDocument; + CONFIG.Token.objectClass = SimpleToken; // Register sheet application classes Actors.unregisterSheet("core", ActorSheet); @@ -116,7 +115,7 @@ Hooks.on("getActorDirectoryEntryContext", (html, options) => { icon: '', condition: li => { const actor = game.actors.get(li.data(idAttr)); - return !actor.getFlag("worldbuilding", "isTemplate"); + return !actor.isTemplate; }, callback: li => { const actor = game.actors.get(li.data(idAttr)); @@ -130,7 +129,7 @@ Hooks.on("getActorDirectoryEntryContext", (html, options) => { icon: '', condition: li => { const actor = game.actors.get(li.data(idAttr)); - return actor.getFlag("worldbuilding", "isTemplate"); + return actor.isTemplate; }, callback: li => { const actor = game.actors.get(li.data(idAttr)); @@ -150,7 +149,7 @@ Hooks.on("getItemDirectoryEntryContext", (html, options) => { icon: '', condition: li => { const item = game.items.get(li.data(idAttr)); - return !item.getFlag("worldbuilding", "isTemplate"); + return !item.isTemplate; }, callback: li => { const item = game.items.get(li.data(idAttr)); @@ -164,7 +163,7 @@ Hooks.on("getItemDirectoryEntryContext", (html, options) => { icon: '', condition: li => { const item = game.items.get(li.data(idAttr)); - return item.getFlag("worldbuilding", "isTemplate"); + return item.isTemplate; }, callback: li => { const item = game.items.get(li.data(idAttr)); diff --git a/module/simpletokendocument.js b/module/simpletokendocument.js deleted file mode 100644 index dd63f65..0000000 --- a/module/simpletokendocument.js +++ /dev/null @@ -1,11 +0,0 @@ -export class SimpleTokenDocument extends TokenDocument { - - /** @inheritdoc */ - getBarAttribute(barName, {alternative}={}) { - const attr = super.getBarAttribute(barName, {alternative}); - if ( attr === null ) return null; - attr.editable = true; // Attribute always editable, super requires attr to exist in actor template - return attr; - } - -} \ No newline at end of file diff --git a/module/token.js b/module/token.js new file mode 100644 index 0000000..aca893a --- /dev/null +++ b/module/token.js @@ -0,0 +1,52 @@ +/** + * Extend the base TokenDocument to support resource type attributes. + * @extends {TokenDocument} + */ +export class SimpleTokenDocument extends TokenDocument { + /** @inheritdoc */ + getBarAttribute(barName, {alternative}={}) { + const data = super.getBarAttribute(barName, {alternative}); + const attr = alternative || this.data[barName]?.attribute; + if ( !data || !attr || !this.actor ) return data; + const current = foundry.utils.getProperty(this.actor.data.data, attr); + if ( "min" in current ) data.min = parseInt(current.min || 0); + data.editable = true; + return data; + } + + /* -------------------------------------------- */ + + static getTrackedAttributes(data, _path=[]) { + if ( data || _path.length ) return super.getTrackedAttributes(data, _path); + data = {}; + for ( const model of Object.values(game.system.model.Actor) ) { + foundry.utils.mergeObject(data, model); + } + for ( const actor of game.actors ) { + if ( actor.isTemplate ) foundry.utils.mergeObject(data, actor.toObject().data); + } + return super.getTrackedAttributes(data); + } +} + + +/* -------------------------------------------- */ + + +/** + * Extend the base Token class to implement additional system-specific logic. + * @extends {Token} + */ +export class SimpleToken extends Token { + _drawBar(number, bar, data) { + if ( "min" in data ) { + // Copy the data to avoid mutating what the caller gave us. + data = {...data}; + // Shift the value and max by the min to ensure that the bar's percentage is drawn accurately if this resource has + // a non-zero min. + data.value -= data.min; + data.max -= data.min; + } + return super._drawBar(number, bar, data); + } +}