diff --git a/daggerheart.mjs b/daggerheart.mjs index a6676f19..153c2f84 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; @@ -154,6 +155,7 @@ Hooks.on('ready', () => { socketRegistration.registerSocketHooks(); registerCountdownApplicationHooks(); registerRollDiceHooks(); + registerDHActorHooks(); }); Hooks.once('dicesoniceready', () => {}); 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/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/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/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 @@