diff --git a/daggerheart.mjs b/daggerheart.mjs index b5e1dcaf..dcbe7740 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -18,6 +18,7 @@ import { } from './module/systemRegistration/_module.mjs'; import { placeables } from './module/canvas/_module.mjs'; import { registerRollDiceHooks } from './module/dice/dhRoll.mjs'; +import { registerDHActorHooks } from './module/documents/actor.mjs'; Hooks.once('init', () => { CONFIG.DH = SYSTEM; @@ -156,6 +157,7 @@ Hooks.on('ready', () => { socketRegistration.registerSocketHooks(); registerCountdownApplicationHooks(); registerRollDiceHooks(); + registerDHActorHooks(); }); Hooks.once('dicesoniceready', () => {}); diff --git a/lang/en.json b/lang/en.json index 7acaf35a..a666992e 100755 --- a/lang/en.json +++ b/lang/en.json @@ -1132,6 +1132,7 @@ "examples": { "label": "Examples" }, "advantageOn": { "label": "Gain Advantage On" }, "tokenImg": { "label": "Token Image" }, + "tokenRingImg": { "label": "Subject Texture" }, "tokenSize": { "placeholder": "Using character dimensions", "height": { "label": "Height" }, diff --git a/module/applications/dialogs/damageReductionDialog.mjs b/module/applications/dialogs/damageReductionDialog.mjs index 8a67ef98..3d33f2a3 100644 --- a/module/applications/dialogs/damageReductionDialog.mjs +++ b/module/applications/dialogs/damageReductionDialog.mjs @@ -1,6 +1,6 @@ import { damageKeyToNumber, getDamageLabel } from '../../helpers/utils.mjs'; -const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; +const { DialogV2, ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; export default class DamageReductionDialog extends HandlebarsApplicationMixin(ApplicationV2) { constructor(resolve, reject, actor, damage) { @@ -122,7 +122,7 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap getDamageInfo = () => { const selectedArmorMarks = Object.values(this.marks.armor).filter(x => x.selected); const selectedStressMarks = Object.values(this.marks.stress).filter(x => x.selected); - const stressReductions = Object.values(this.availableStressReductions).filter(red => red.selected); + const stressReductions = Object.values(this.availableStressReductions ?? {}).filter(red => red.selected); const currentMarks = this.actor.system.armor.system.marks.value + selectedArmorMarks.length + selectedStressMarks.length; @@ -210,9 +210,17 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap async close(fromSave) { if (!fromSave) { - this.reject(); + this.resolve(); } await super.close({}); } + + static async armorStackQuery({actorId, damage}) { + return new Promise(async (resolve, reject) => { + const actor = await fromUuid(actorId); + if(!actor || !actor?.isOwner) reject(); + new DamageReductionDialog(resolve, reject, actor, damage).render({ force: true }); + }) + } } diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index a77fe71f..c24bcfec 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -188,7 +188,17 @@ export default class CharacterSheet extends DHBaseActorSheet { * @param {HTMLElement} el * @returns {foundry.documents.Item?} */ - const getItem = el => this.actor.items.get(el.closest('[data-item-id]')?.dataset.itemId); + const getItem = element => { + const listElement = (element.target ?? element).closest('[data-item-id]'); + const itemId = listElement.dataset.itemId; + + switch (listElement.dataset.type) { + case 'effect': + return this.document.effects.get(itemId); + default: + return this.document.items.get(itemId); + } + }; return [ { diff --git a/module/applications/sheets/items/beastform.mjs b/module/applications/sheets/items/beastform.mjs index e3b72d01..194f3ab1 100644 --- a/module/applications/sheets/items/beastform.mjs +++ b/module/applications/sheets/items/beastform.mjs @@ -30,8 +30,17 @@ export default class BeastformSheet extends DHBaseItemSheet { }; /**@inheritdoc */ - async _preparePartContext(partId, context) { - await super._preparePartContext(partId, context); + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + + context.document = context.document.toObject(); + context.document.effects = this.document.effects.map(effect => { + const data = effect.toObject(); + data.id = effect.id; + if (effect.type === 'beastform') data.mandatory = true; + + return data; + }); return context; } diff --git a/module/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs index d17c26dc..0e2242d7 100644 --- a/module/applications/ui/chatLog.mjs +++ b/module/applications/ui/chatLog.mjs @@ -215,7 +215,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo if (message.system.onSave && message.system.targets.find(t => t.id === target.id)?.saved?.success === true) damage = Math.ceil(damage * (CONFIG.DH.ACTIONS.damageOnSave[message.system.onSave]?.mod ?? 1)); - await target.actor.takeDamage(damage, message.system.roll.type); + target.actor.takeDamage(damage, message.system.roll.type); } }; @@ -227,7 +227,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelected')); for (var target of targets) { - await target.actor.takeHealing([{ value: message.system.roll.total, type: message.system.roll.type }]); + target.actor.takeHealing([{ value: message.system.roll.total, type: message.system.roll.type }]); } }; diff --git a/module/data/activeEffect/beastformEffect.mjs b/module/data/activeEffect/beastformEffect.mjs index 3aa25bef..6445f65d 100644 --- a/module/data/activeEffect/beastformEffect.mjs +++ b/module/data/activeEffect/beastformEffect.mjs @@ -10,6 +10,11 @@ export default class BeastformEffect extends foundry.abstract.TypeDataModel { base64: false, nullable: true }), + tokenRingImg: new fields.FilePathField({ + initial: 'icons/svg/mystery-man.svg', + categories: ['IMAGE'], + base64: false + }), tokenSize: new fields.SchemaField({ height: new fields.NumberField({ integer: true, nullable: true }), width: new fields.NumberField({ integer: true, nullable: true }) @@ -28,6 +33,11 @@ export default class BeastformEffect extends foundry.abstract.TypeDataModel { width: this.characterTokenData.tokenSize.width, texture: { src: this.characterTokenData.tokenImg + }, + ring: { + subject: { + texture: this.characterTokenData.tokenRingImg + } } }; diff --git a/module/data/item/beastform.mjs b/module/data/item/beastform.mjs index 2eb871ec..b7ea5cb9 100644 --- a/module/data/item/beastform.mjs +++ b/module/data/item/beastform.mjs @@ -3,7 +3,7 @@ import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayFie import BaseDataItem from './base.mjs'; export default class DHBeastform extends BaseDataItem { - static LOCALIZATION_PREFIXES = ['DAGGERHEART.Sheets.Beastform']; + static LOCALIZATION_PREFIXES = ['DAGGERHEART.ITEMS.Beastform']; /** @inheritDoc */ static get metadata() { @@ -29,12 +29,17 @@ export default class DHBeastform extends BaseDataItem { categories: ['IMAGE'], base64: false }), + tokenRingImg: new fields.FilePathField({ + initial: 'icons/svg/mystery-man.svg', + categories: ['IMAGE'], + base64: false + }), tokenSize: new fields.SchemaField({ height: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true }), width: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true }) }), examples: new fields.StringField(), - advantageOn: new fields.ArrayField(new fields.StringField()), + advantageOn: new fields.StringField(), features: new ForeignDocumentUUIDArrayField({ type: 'Item' }) }; } @@ -56,40 +61,54 @@ export default class DHBeastform extends BaseDataItem { 'Item', this.features.map(x => x.toObject()) ); - const effects = await this.parent.parent.createEmbeddedDocuments( + + const extraEffects = await this.parent.parent.createEmbeddedDocuments( 'ActiveEffect', - this.parent.effects.map(x => x.toObject()) + this.parent.effects.filter(x => x.type !== 'beastform').map(x => x.toObject()) ); - await this.parent.parent.createEmbeddedDocuments('ActiveEffect', [ - { - type: 'beastform', - name: game.i18n.localize('DAGGERHEART.ITEMS.Beastform.beastformEffect'), - img: 'icons/creatures/abilities/paw-print-pair-purple.webp', - system: { - isBeastform: true, - characterTokenData: { - tokenImg: this.parent.parent.prototypeToken.texture.src, - tokenSize: { - height: this.parent.parent.prototypeToken.height, - width: this.parent.parent.prototypeToken.width - } - }, - advantageOn: this.advantageOn, - featureIds: features.map(x => x.id), - effectIds: effects.map(x => x.id) - } + const beastformEffect = this.parent.effects.find(x => x.type === 'beastform'); + await beastformEffect.updateSource({ + system: { + characterTokenData: { + tokenImg: this.parent.parent.prototypeToken.texture.src, + tokenRingImg: this.parent.parent.prototypeToken.ring.subject.texture, + tokenSize: { + height: this.parent.parent.prototypeToken.height, + width: this.parent.parent.prototypeToken.width + } + }, + advantageOn: this.advantageOn, + featureIds: features.map(x => x.id), + effectIds: extraEffects.map(x => x.id) } - ]); + }); + + await this.parent.parent.createEmbeddedDocuments('ActiveEffect', [beastformEffect.toObject()]); await updateActorTokens(this.parent.parent, { height: this.tokenSize.height, width: this.tokenSize.width, texture: { src: this.tokenImg + }, + ring: { + subject: { + texture: this.tokenRingImg + } } }); return false; } + + _onCreate() { + this.parent.createEmbeddedDocuments('ActiveEffect', [ + { + type: 'beastform', + name: game.i18n.localize('DAGGERHEART.ITEMS.Beastform.beastformEffect'), + img: 'icons/creatures/abilities/paw-print-pair-purple.webp' + } + ]); + } } diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index b48d8c26..1fab0f71 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -1,10 +1,19 @@ import DamageSelectionDialog from '../applications/dialogs/damageSelectionDialog.mjs'; -import { GMUpdateEvent, socketEvent } from '../systemRegistration/socket.mjs'; +import { emitAsGM, emitAsOwner, GMUpdateEvent, socketEvent } from '../systemRegistration/socket.mjs'; import DamageReductionDialog from '../applications/dialogs/damageReductionDialog.mjs'; import { LevelOptionType } from '../data/levelTier.mjs'; import DHFeature from '../data/item/feature.mjs'; -export default class DhpActor extends foundry.documents.Actor { +export default class DhpActor extends Actor { + + /** + * Return the first Actor active owner. + */ + get owner() { + const user = this.hasPlayerOwner && game.users.players.find(u => this.testUserPermission(u, "OWNER") && u.active);; + if(!user) return game.user.isGM ? game.user : null; + return user; + } /** * Whether this actor is an NPC. @@ -440,49 +449,40 @@ export default class DhpActor extends foundry.documents.Actor { } async takeDamage(damage, type) { + if (Hooks.call(`${CONFIG.DH.id}.preTakeDamage`, this, damage, type) === false) return null; + if (this.type === 'companion') { await this.modifyResource([{ value: 1, type: 'stress' }]); return; } - const hpDamage = - damage >= this.system.damageThresholds.severe - ? 3 - : damage >= this.system.damageThresholds.major - ? 2 - : damage >= this.system.damageThresholds.minor - ? 1 - : 0; + const hpDamage = this.convertDamageToThreshold(damage); + + if (Hooks.call(`${CONFIG.DH.id}.postDamageTreshold`, this, hpDamage, damage, type) === false) return null; + + if(!hpDamage) return; + + const updates = [{ value: hpDamage, type: 'hitPoints' }]; if ( this.type === 'character' && this.system.armor && this.system.armor.system.marks.value < this.system.armorScore ) { - new Promise((resolve, reject) => { - new DamageReductionDialog(resolve, reject, this, hpDamage).render(true); - }) - .then(async ({ modifiedDamage, armorSpent, stressSpent }) => { - const resources = [ - { value: modifiedDamage, type: 'hitPoints' }, - ...(armorSpent ? [{ value: armorSpent, type: 'armorStack' }] : []), - ...(stressSpent ? [{ value: stressSpent, type: 'stress' }] : []) - ]; - await this.modifyResource(resources); - }) - .catch(() => { - const cls = getDocumentClass('ChatMessage'); - const msg = new cls({ - user: game.user.id, - content: game.i18n.format('DAGGERHEART.UI.Notifications.damageIgnore', { - character: this.name - }) - }); - cls.create(msg.toObject()); - }); - } else { - await this.modifyResource([{ value: hpDamage, type: 'hitPoints' }]); + const armorStackResult = await this.owner.query('armorStack', {actorId: this.uuid, damage: hpDamage}); + if(armorStackResult) { + const { modifiedDamage, armorSpent, stressSpent } = armorStackResult; + updates.find(u => u.type === 'hitPoints').value = modifiedDamage; + updates.push( + ...(armorSpent ? [{ value: armorSpent, type: 'armorStack' }] : []), + ...(stressSpent ? [{ value: stressSpent, type: 'stress' }] : []) + ); + } } + + await this.modifyResource(updates); + + if (Hooks.call(`${CONFIG.DH.id}.postTakeDamage`, this, damage, type) === false) return null; } async takeHealing(resources) { @@ -492,6 +492,8 @@ export default class DhpActor extends foundry.documents.Actor { async modifyResource(resources) { if (!resources.length) return; + + if(resources.find(r => r.type === 'stress')) this.convertStressDamageToHP(resources); let updates = { actor: { target: this, resources: {} }, armor: { target: this.system.armor, resources: {} } }; resources.forEach(r => { switch (r.type) { @@ -519,7 +521,8 @@ export default class DhpActor extends foundry.documents.Actor { }); Object.values(updates).forEach(async u => { if (Object.keys(u.resources).length > 0) { - if (game.user.isGM) { + await emitAsGM(GMUpdateEvent.UpdateDocument, u.target.update.bind(u.target), u.resources, u.target.uuid); + /* if (game.user.isGM) { await u.target.update(u.resources); } else { await game.socket.emit(`system.${CONFIG.DH.id}`, { @@ -530,8 +533,34 @@ export default class DhpActor extends foundry.documents.Actor { update: u.resources } }); - } + } */ } }); } + + convertDamageToThreshold(damage) { + return damage >= this.system.damageThresholds.severe + ? 3 + : damage >= this.system.damageThresholds.major + ? 2 + : damage >= this.system.damageThresholds.minor + ? 1 + : 0; + } + + convertStressDamageToHP(resources) { + const stressDamage = resources.find(r => r.type === 'stress'), + newValue = this.system.resources.stress.value + stressDamage.value; + if(newValue <= this.system.resources.stress.maxTotal) return; + const hpDamage = resources.find(r => r.type === 'hitPoints'); + if(hpDamage) hpDamage.value++; + else resources.push({ + type: 'hitPoints', + value: 1 + }) + } } + +export const registerDHActorHooks = () => { + CONFIG.queries.armorStack = DamageReductionDialog.armorStackQuery; +} \ No newline at end of file diff --git a/module/systemRegistration/socket.mjs b/module/systemRegistration/socket.mjs index d7f79df9..b336a012 100644 --- a/module/systemRegistration/socket.mjs +++ b/module/systemRegistration/socket.mjs @@ -63,21 +63,28 @@ export const registerSocketHooks = () => { }); }; -export const emitAsGM = async (eventName, callback, args) => { +export const emitAsGM = async (eventName, callback, update, uuid = null) => { if(!game.user.isGM) { - return new Promise(async (resolve, reject) => { - try { - const response = await game.socket.emit(`system.${CONFIG.DH.id}`, { - action: socketEvent.GMUpdate, - data: { - action: eventName, - update: args - } - }); - resolve(response); - } catch (error) { - reject(error); + return await game.socket.emit(`system.${CONFIG.DH.id}`, { + action: socketEvent.GMUpdate, + data: { + action: eventName, + uuid, + update } - }) - } else return callback(args); + }); + } else return callback(update); } + +export const emitAsOwner = (eventName, userId, args) => { + if(userId === game.user.id) return; + if(!eventName || !userId) return false; + game.socket.emit(`system.${CONFIG.DH.id}`, { + action: eventName, + data: { + userId, + ...args + } + }); + return false; +} \ No newline at end of file diff --git a/templates/sheets/global/tabs/tab-effects.hbs b/templates/sheets/global/tabs/tab-effects.hbs index 744ef8e1..a75f1b0b 100644 --- a/templates/sheets/global/tabs/tab-effects.hbs +++ b/templates/sheets/global/tabs/tab-effects.hbs @@ -17,7 +17,7 @@ {{effect.name}}
{{/each}} diff --git a/templates/sheets/items/beastform/settings.hbs b/templates/sheets/items/beastform/settings.hbs index 78af825f..dec7d134 100644 --- a/templates/sheets/items/beastform/settings.hbs +++ b/templates/sheets/items/beastform/settings.hbs @@ -8,11 +8,7 @@ {{formGroup systemFields.examples value=source.system.examples localize=true}} - + {{formGroup systemFields.advantageOn value=source.system.advantageOn localize=true}} diff --git a/templates/ui/chat/adversary-roll.hbs b/templates/ui/chat/adversary-roll.hbs index 1f97fefe..a8918062 100644 --- a/templates/ui/chat/adversary-roll.hbs +++ b/templates/ui/chat/adversary-roll.hbs @@ -49,7 +49,7 @@