diff --git a/module/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs index 9e07c050..2e9e711e 100644 --- a/module/applications/ui/chatLog.mjs +++ b/module/applications/ui/chatLog.mjs @@ -30,9 +30,6 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo html.querySelectorAll('.roll-all-save-button').forEach(element => element.addEventListener('click', event => this.onRollAllSave(event, data.message)) ); - html.querySelectorAll('.duality-action-effect').forEach(element => - element.addEventListener('click', event => this.onApplyEffect(event, data.message)) - ); html.querySelectorAll('.simple-roll-button').forEach(element => element.addEventListener('click', event => this.onRollSimple(event, data.message)) ); @@ -44,15 +41,9 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo html.querySelectorAll('.button-target-selection').forEach(element => { element.addEventListener('click', event => this.onTargetSelection(event, data.message)); }); - html.querySelectorAll('.damage-button').forEach(element => - element.addEventListener('click', event => this.onDamage(event, data.message)) - ); html.querySelectorAll('.healing-button').forEach(element => element.addEventListener('click', event => this.onHealing(event, data.message)) ); - html.querySelectorAll('.target-indicator').forEach(element => - element.addEventListener('click', this.onToggleTargets) - ); html.querySelectorAll('.ability-use-button').forEach(element => element.addEventListener('click', event => this.abilityUseButton(event, data.message)) ); @@ -149,45 +140,10 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo }); } - async onApplyEffect(event, message) { - event.stopPropagation(); - const actor = await this.getActor(message.system.source.actor); - if (!actor || !game.user.isGM) return true; - if (message.system.source.item && message.system.source.action) { - const action = this.getAction(actor, message.system.source.item, message.system.source.action); - if (!action || !action?.applyEffects) return; - const { isHit, targets } = this.getTargetList(event, message); - if (targets.length === 0) - ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelected')); - await action.applyEffects(event, message, targets); - } - } - onTargetSelection(event, message) { event.stopPropagation(); - const targetSelection = Boolean(event.target.dataset.targetHit), - msg = ui.chat.collection.get(message._id); - if (msg.system.targetSelection === targetSelection) return; - // if (targetSelection !== true && !Array.from(game.user.targets).length) - // return ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelected')); - msg.system.targetSelection = targetSelection; - msg.system.prepareDerivedData(); - ui.chat.updateMessage(msg); - } - - getTargetList(event, message) { - const targetSelection = event.target - .closest('.message-content') - .querySelector('.button-target-selection.target-selected'), - isHit = Boolean(targetSelection?.dataset?.targetHit) ?? false; - return { - isHit, - targets: isHit - ? message.system.targets - .filter(t => t.hit === true) - .map(target => game.canvas.tokens.documentCollection.find(t => t.actor.uuid === target.actorId)) - : Array.from(game.user.targets) - }; + const msg = ui.chat.collection.get(message._id); + msg.system.targetMode = Boolean(event.target.dataset.targetHit); } hoverTarget(event) { @@ -211,48 +167,6 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo game.canvas.pan(token); } - async onDamage(event, message) { - event.stopPropagation(); - const { isHit, targets } = this.getTargetList(event, message); - - if (message.system.onSave && isHit) { - const pendingingSaves = message.system.targets.filter( - target => target.hit && target.saved.success === null - ); - if (pendingingSaves.length) { - const confirm = await foundry.applications.api.DialogV2.confirm({ - window: { title: 'Pending Reaction Rolls found' }, - content: `
Some Tokens still need to roll their Reaction Roll.
Are you sure you want to continue ?
Undone reaction rolls will be considered as failed
` - }); - if (!confirm) return; - } - } - - if (targets.length === 0) - return ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelected')); - - for (let target of targets) { - let damages = foundry.utils.deepClone(message.system.damage); - if ( - !message.system.hasHealing && - message.system.onSave && - message.system.targets.find(t => t.id === target.id)?.saved?.success === true - ) { - const mod = CONFIG.DH.ACTIONS.damageOnSave[message.system.onSave]?.mod ?? 1; - Object.entries(damages).forEach(([k, v]) => { - v.total = 0; - v.parts.forEach(part => { - part.total = Math.ceil(part.total * mod); - v.total += part.total; - }); - }); - } - - if (message.system.hasHealing) target.actor.takeHealing(damages); - else target.actor.takeDamage(damages); - } - } - async onRollSimple(event, message) { const buttonType = event.target.dataset.type ?? 'damage', total = message.rolls.reduce((a,c) => a + Roll.fromJSON(c).total, 0), @@ -280,17 +194,6 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo }) } - /** - * Toggle visibility of target containers. - * @param {MouseEvent} event - */ - onToggleTargets(event) { - event.stopPropagation(); - event.currentTarget.parentElement - ?.querySelectorAll('.target-container') - .forEach(el => el.classList.toggle('hidden')); - } - async abilityUseButton(event, message) { event.stopPropagation(); diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index a7523b29..337cdb2a 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -114,7 +114,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel async use(event, ...args) { if (!this.actor) throw new Error("An Action can't be used outside of an Actor context."); - if (this.chatDisplay) this.toChat(); + if (this.chatDisplay) await this.toChat(); let config = this.prepareConfig(event); for (let i = 0; i < this.constructor.extraSchemas.length; i++) { @@ -139,11 +139,16 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel config = await this.actor.diceRoll(config); if (!config) return; } - + if (this.doFollowUp()) { - if (this.rollDamage) await this.rollDamage(event, config); - if (this.rollHealing) await this.rollHealing(event, config); - if (this.trigger) await this.trigger(event, config); + if (this.rollDamage && this.damage.parts.length) await this.rollDamage(event, config); + else if (this.trigger) await this.trigger(event, config); + else if(this.hasSave || this.hasEffect) { + const roll = new Roll(''); + roll._evaluated = true; + if(this.hasTarget) config.targetSelection = config.targets.length > 0; + await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config); + } } // Consume resources @@ -254,6 +259,10 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel /* SAVE */ /* EFFECTS */ + get hasEffect() { + return this.effects?.length > 0; + } + async applyEffects(event, data, targets) { targets ??= data.system.targets; const force = true; /* Where should this come from? */ @@ -343,10 +352,16 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel async updateChatMessage(message, targetId, changes, chain = true) { setTimeout(async () => { const chatMessage = ui.chat.collection.get(message._id), - msgTargets = chatMessage.system.targets, - msgTarget = msgTargets.find(mt => mt.id === targetId); + msgTarget = chatMessage.system.targets.find(mt => mt.id === targetId) ?? chatMessage.system.oldTargets.find(mt => mt.id === targetId); msgTarget.saved = changes; - await chatMessage.update({ 'system.targets': msgTargets }); + await chatMessage.update( + { + system: { + targets: chatMessage.system.targets, + oldTargets: chatMessage.system.oldTargets + } + } + ); }, 100); if (chain) { if (message.system.source.message) diff --git a/module/data/action/damageAction.mjs b/module/data/action/damageAction.mjs index 3cfa7a5a..40fa2a12 100644 --- a/module/data/action/damageAction.mjs +++ b/module/data/action/damageAction.mjs @@ -45,12 +45,12 @@ export default class DHDamageAction extends DHBaseAction { formulas = this.formatFormulas(formulas, systemData); delete systemData.evaluate; - systemData.targets.forEach(t => t.hit = true); const config = { ...systemData, roll: formulas, dialog: {}, - data: this.getRollData() + data: this.getRollData(), + targetSelection: systemData.targets.length > 0 } if (this.hasSave) config.onSave = this.save.damageMod; if (data.system) { diff --git a/module/data/action/effectAction.mjs b/module/data/action/effectAction.mjs index 505bc924..e2bea4a2 100644 --- a/module/data/action/effectAction.mjs +++ b/module/data/action/effectAction.mjs @@ -2,17 +2,4 @@ import DHBaseAction from './baseAction.mjs'; export default class DHEffectAction extends DHBaseAction { static extraSchemas = [...super.extraSchemas, 'effects', 'target']; - - async trigger(event, data) { - if (this.effects.length) { - const cls = getDocumentClass('ChatMessage'), - msg = { - type: 'applyEffect', - user: game.user.id, - system: data - }; - - return await cls.create(msg); - } else this.toChat(this.id); - } } diff --git a/module/data/chat-message/_modules.mjs b/module/data/chat-message/_modules.mjs index 2612622c..a4e2c1fd 100644 --- a/module/data/chat-message/_modules.mjs +++ b/module/data/chat-message/_modules.mjs @@ -1,11 +1,9 @@ import DHAbilityUse from "./abilityUse.mjs"; import DHActorRoll from "./adversaryRoll.mjs"; -import DHApplyEffect from './applyEffects.mjs' export const config = { abilityUse: DHAbilityUse, adversaryRoll: DHActorRoll, damageRoll: DHActorRoll, - dualityRoll: DHActorRoll, - applyEffect: DHApplyEffect + dualityRoll: DHActorRoll }; diff --git a/module/data/chat-message/adversaryRoll.mjs b/module/data/chat-message/adversaryRoll.mjs index 24cac3d7..db3cab52 100644 --- a/module/data/chat-message/adversaryRoll.mjs +++ b/module/data/chat-message/adversaryRoll.mjs @@ -1,25 +1,30 @@ const fields = foundry.data.fields; +const targetsField = () => new fields.ArrayField( + new fields.SchemaField({ + id: new fields.StringField({}), + actorId: new fields.StringField({}), + name: new fields.StringField({}), + img: new fields.StringField({}), + difficulty: new fields.NumberField({ integer: true, nullable: true }), + evasion: new fields.NumberField({ integer: true }), + hit: new fields.BooleanField({ initial: false }), + saved: new fields.SchemaField({ + result: new fields.NumberField(), + success: new fields.BooleanField({ nullable: true, initial: null }) + }) + }) +) + export default class DHActorRoll extends foundry.abstract.TypeDataModel { + targetHook = null; + static defineSchema() { return { title: new fields.StringField(), roll: new fields.ObjectField(), - targets: new fields.ArrayField( - new fields.SchemaField({ - id: new fields.StringField({}), - actorId: new fields.StringField({}), - name: new fields.StringField({}), - img: new fields.StringField({}), - difficulty: new fields.NumberField({ integer: true, nullable: true }), - evasion: new fields.NumberField({ integer: true }), - hit: new fields.BooleanField({ initial: false }), - saved: new fields.SchemaField({ - result: new fields.NumberField(), - success: new fields.BooleanField({ nullable: true, initial: null }) - }) - }) - ), + targets: targetsField(), + oldTargets: targetsField(), targetSelection: new fields.BooleanField({ initial: false }), hasRoll: new fields.BooleanField({ initial: false }), hasDamage: new fields.BooleanField({ initial: false }), @@ -45,23 +50,96 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel { return 'systems/daggerheart/templates/ui/chat/roll.hbs'; } - prepareDerivedData() { - this.hasHitTarget = this.targets.filter(t => t.hit === true).length > 0; - this.currentTargets = - this.targetSelection !== true - ? Array.from(game.user.targets).map(t => - game.system.api.fields.ActionFields.TargetField.formatTarget(t) - ) - : this.targets; - if(this.targetSelection === true) { - this.targetShort = this.targets.reduce((a,c) => { - if(c.hit) a.hit += 1; - else c.miss += 1; - return a; - }, {hit: 0, miss: 0}) + get targetMode() { + return this.targetSelection; + } + + set targetMode(mode) { + this.targetSelection = mode; + this.updateTargets(); + this.registerTargetHook(); + this.parent.update( + { + system: { + targetSelection: this.targetSelection, + oldTargets: this.oldTargets + } + } + ); + } + + get hitTargets() { + return this.currentTargets.filter(t => (t.hit || !this.targetSelection)); + } + + async updateTargets() { + this.currentTargets = this.getTargetList(); + if(!this.targetSelection && this.hasSave) { + this.currentTargets.forEach(ct => { + if(this.targets.find(t => t.actorId === ct.actorId)) return; + const indexTarget = this.oldTargets.findIndex(ot => ot.actorId === ct.actorId); + if(indexTarget === -1) + this.oldTargets.push(ct); + }); + this.setPendingSaves(); + if(this.currentTargets.length) { + if(!this.parent._id) return; + const updates = await this.parent.update( + { + system: { + oldTargets: this.oldTargets + } + } + ); + if(!updates) + ui.chat.updateMessage(this.parent); + } } - this.pendingSaves = this.targets.filter( - target => target.hit && target.saved.success === null - ).length > 0; + } + + registerTargetHook() { + if(this.targetSelection && this.targetHook !== null) { + Hooks.off("targetToken", this.targetHook); + this.targetHook = null; + } else if(!this.targetSelection && this.targetHook === null) { + this.targetHook = Hooks.on("targetToken", foundry.utils.debounce(this.updateTargets.bind(this), 50)); + } + } + + prepareDerivedData() { + if(this.hasTarget) { + this.hasHitTarget = this.targets.filter(t => t.hit === true).length > 0; + this.updateTargets(); + this.registerTargetHook(); + if(this.targetSelection === true) { + this.targetShort = this.targets.reduce((a,c) => { + if(c.hit) a.hit += 1; + else c.miss += 1; + return a; + }, {hit: 0, miss: 0}) + } + if(this.hasSave) this.setPendingSaves(); + } + } + + getTargetList() { + return this.targetSelection !== true + ? Array.from(game.user.targets).map(t =>{ + const target = game.system.api.fields.ActionFields.TargetField.formatTarget(t), + oldTarget = this.targets.find(ot => ot.actorId === target.actorId) ?? this.oldTargets.find(ot => ot.actorId === target.actorId); + if(oldTarget) return oldTarget; + return target; + }) + : this.targets; + } + + setPendingSaves() { + this.pendingSaves = this.targetSelection + ? this.targets.filter( + target => target.hit && target.saved.success === null + ).length > 0 + : this.currentTargets.filter( + target => target.saved.success === null + ).length > 0; } } diff --git a/module/data/chat-message/applyEffects.mjs b/module/data/chat-message/applyEffects.mjs deleted file mode 100644 index 41b0aaf5..00000000 --- a/module/data/chat-message/applyEffects.mjs +++ /dev/null @@ -1,37 +0,0 @@ -export default class DHApplyEffect extends foundry.abstract.TypeDataModel { - static defineSchema() { - const fields = foundry.data.fields; - - return { - title: new fields.StringField(), - targets: new fields.ArrayField( - new fields.SchemaField({ - id: new fields.StringField({ required: true }), - name: new fields.StringField(), - img: new fields.StringField(), - hit: new fields.BooleanField({ initial: false }) - }) - ), - targetSelection: new fields.BooleanField({ initial: true }), - source: new fields.SchemaField({ - actor: new fields.StringField(), - item: new fields.StringField(), - action: new fields.StringField() - }) - }; - } - - prepareDerivedData() { - this.hasHitTarget = this.targets.filter(t => t.hit === true).length > 0; - this.currentTargets = - this.targetSelection !== true - ? Array.from(game.user.targets).map(t => - game.system.api.fields.ActionFields.TargetField.formatTarget(t) - ) - : this.targets; - } - - get messageTemplate() { - return 'systems/daggerheart/templates/ui/chat/apply-effects.hbs'; - } -} diff --git a/module/data/fields/action/costField.mjs b/module/data/fields/action/costField.mjs index abfc2e63..6d4b084a 100644 --- a/module/data/fields/action/costField.mjs +++ b/module/data/fields/action/costField.mjs @@ -26,7 +26,6 @@ export default class CostField extends fields.ArrayField { } static calcCosts(costs) { - // console.log(costs, CostField.getResources.call(this, costs)); const resources = CostField.getResources.call(this, costs); return costs.map(c => { c.scale = c.scale ?? 1; diff --git a/module/data/fields/action/targetField.mjs b/module/data/fields/action/targetField.mjs index f54cd6fe..681f8353 100644 --- a/module/data/fields/action/targetField.mjs +++ b/module/data/fields/action/targetField.mjs @@ -57,7 +57,11 @@ export default class TargetField extends fields.SchemaField { name: actor.actor.name, img: actor.actor.img, difficulty: actor.actor.system.difficulty, - evasion: actor.actor.system.evasion + evasion: actor.actor.system.evasion, + saved: { + value: null, + success: null + } }; } } diff --git a/module/dice/damageRoll.mjs b/module/dice/damageRoll.mjs index bab685b4..be49d685 100644 --- a/module/dice/damageRoll.mjs +++ b/module/dice/damageRoll.mjs @@ -16,7 +16,7 @@ export default class DamageRoll extends DHRoll { const parts = config.roll.map(r => this.postEvaluate(r)); config.damage = this.unifyDamageRoll(parts); - config.targetSelection = config.targets?.length + // config.targetSelection = config.targets?.length } static postEvaluate(roll, config = {}) { diff --git a/module/documents/chatMessage.mjs b/module/documents/chatMessage.mjs index 51b34ab0..0fdc2a5f 100644 --- a/module/documents/chatMessage.mjs +++ b/module/documents/chatMessage.mjs @@ -30,6 +30,8 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { } } + this.enrichChatMessage(html); + return html; } @@ -53,4 +55,74 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { return super._preCreate(data, options, rollActorOwner ?? user); } + + enrichChatMessage(html) { + html.querySelectorAll('.damage-button').forEach(element => + element.addEventListener('click', this.onDamage.bind(this)) + ); + + html.querySelectorAll('.duality-action-effect').forEach(element => + element.addEventListener('click', this.onApplyEffect.bind(this)) + ); + } + + getTargetList() { + const targets = this.system.hitTargets; + return targets.map(target => game.canvas.tokens.documentCollection.find(t => t.actor.uuid === target.actorId)); + + } + + async onDamage(event) { + event.stopPropagation(); + const targets = this.getTargetList(); + + if (this.system.onSave) { + const pendingingSaves = this.system.hitTargets.filter(t => t.saved.success === null); + if (pendingingSaves.length) { + const confirm = await foundry.applications.api.DialogV2.confirm({ + window: { title: 'Pending Reaction Rolls found' }, + content: `Some Tokens still need to roll their Reaction Roll.
Are you sure you want to continue ?
Undone reaction rolls will be considered as failed
` + }); + if (!confirm) return; + } + } + + if (targets.length === 0) + return ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelected')); + + for (let target of targets) { + let damages = foundry.utils.deepClone(this.system.damage); + if ( + !this.system.hasHealing && + this.system.onSave && + this.system.hitTargets.find(t => t.id === target.id)?.saved?.success === true + ) { + const mod = CONFIG.DH.ACTIONS.damageOnSave[this.system.onSave]?.mod ?? 1; + Object.entries(damages).forEach(([k, v]) => { + v.total = 0; + v.parts.forEach(part => { + part.total = Math.ceil(part.total * mod); + v.total += part.total; + }); + }); + } + + if (this.system.hasHealing) target.actor.takeHealing(damages); + else target.actor.takeDamage(damages); + } + } + + async onApplyEffect(event) { + event.stopPropagation(); + const actor = await foundry.utils.fromUuid(this.system.source.actor); + if (!actor || !game.user.isGM) return true; + if (this.system.source.item && this.system.source.action) { + const action = this.getAction(actor, this.system.source.item, this.system.source.action); + if (!action || !action?.applyEffects) return; + const targets = this.getTargetList(); + if (targets.length === 0) + ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelected')); + await action.applyEffects(event, this, targets); + } + } } diff --git a/styles/less/ui/chat/chat.less b/styles/less/ui/chat/chat.less index 69bdd6f5..99cb981b 100644 --- a/styles/less/ui/chat/chat.less +++ b/styles/less/ui/chat/chat.less @@ -549,11 +549,11 @@ display: flex; font-size: var(--font-size-14); color: var(--text-color); - padding: 5px 0; .button-target-selection { flex: 1; text-align: center; + padding: 5px 0; } .button-target-selection:hover, @@ -652,6 +652,7 @@ overflow: hidden; grid-template-rows: 1fr; transition: grid-template-rows 250ms ease; + margin: 0; .wrapper { display: flex; @@ -692,7 +693,7 @@ justify-content: center; height: 25px; width: 25px; - &.is-absolute { + &:not(:first-child) { position: absolute; bottom: 0; right: 0; diff --git a/system.json b/system.json index 995f63f3..eee2711f 100644 --- a/system.json +++ b/system.json @@ -247,8 +247,7 @@ "dualityRoll": {}, "adversaryRoll": {}, "damageRoll": {}, - "abilityUse": {}, - "applyEffect": {} + "abilityUse": {} } }, "primaryTokenAttribute": "resources.hitPoints", diff --git a/templates/ui/chat/parts/target-part.hbs b/templates/ui/chat/parts/target-part.hbs index 2bad8f64..94810a03 100644 --- a/templates/ui/chat/parts/target-part.hbs +++ b/templates/ui/chat/parts/target-part.hbs @@ -1,13 +1,13 @@