From 2be4ee88578cd9e62dede6d7b3a0eaa7075d338e Mon Sep 17 00:00:00 2001 From: Dapoulp <74197441+Dapoulp@users.noreply.github.com> Date: Mon, 28 Jul 2025 17:44:11 +0200 Subject: [PATCH 1/6] Feature/416 reaction roll query (#445) * Create files * before fixing damage roll on main * g * Player query for Roll All Save * Exec Save message as GM for players * Fix DsN bug --- daggerheart.mjs | 3 +- lang/en.json | 3 ++ .../dialogs/damageReductionDialog.mjs | 6 +--- module/applications/ui/chatLog.mjs | 34 +++++++++++++++--- module/data/action/baseAction.mjs | 36 ++++++++++++------- module/data/actor/base.mjs | 6 ++-- module/dice/damageRoll.mjs | 2 +- module/documents/actor.mjs | 17 +++++---- module/documents/token.mjs | 1 + module/systemRegistration/socket.mjs | 18 ++++++++-- templates/dialogs/reactionRoll.hbs | 3 ++ 11 files changed, 91 insertions(+), 38 deletions(-) create mode 100644 templates/dialogs/reactionRoll.hbs diff --git a/daggerheart.mjs b/daggerheart.mjs index bf8a9f63..56ad3e3d 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -19,7 +19,6 @@ 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'; import './node_modules/@yaireo/tagify/dist/tagify.css'; Hooks.once('init', () => { @@ -169,7 +168,7 @@ Hooks.on('ready', () => { registerCountdownHooks(); socketRegistration.registerSocketHooks(); registerRollDiceHooks(); - registerDHActorHooks(); + socketRegistration.registerUserQueries(); }); Hooks.once('dicesoniceready', () => {}); diff --git a/lang/en.json b/lang/en.json index c02898f0..40317f8c 100755 --- a/lang/en.json +++ b/lang/en.json @@ -453,6 +453,9 @@ "title": "Ownership Selection - {name}", "default": "Default Ownership" }, + "ReactionRoll": { + "title": "Reaction Roll: {trait}" + }, "ResourceDice": { "title": "{name} Resource", "rerollDice": "Reroll Dice" diff --git a/module/applications/dialogs/damageReductionDialog.mjs b/module/applications/dialogs/damageReductionDialog.mjs index 3e3bde44..9049522d 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 { DialogV2, ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; +const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; export default class DamageReductionDialog extends HandlebarsApplicationMixin(ApplicationV2) { constructor(resolve, reject, actor, damage, damageType) { @@ -53,10 +53,6 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap ); } - get title() { - return game.i18n.localize('DAGGERHEART.APPLICATIONS.DamageReduction.title'); - } - static DEFAULT_OPTIONS = { tag: 'form', classes: ['daggerheart', 'views', 'damage-reduction'], diff --git a/module/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs index e0f990ba..5e507a3b 100644 --- a/module/applications/ui/chatLog.mjs +++ b/module/applications/ui/chatLog.mjs @@ -1,3 +1,5 @@ +import { emitAsGM, GMUpdateEvent } from "../../systemRegistration/socket.mjs"; + export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog { constructor(options) { super(options); @@ -98,17 +100,41 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo 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?.hasSave) return; - action.rollSave(token, event, message); + action.rollSave(token.actor, event, message).then(result => emitAsGM( + GMUpdateEvent.UpdateSaveMessage, + action.updateSaveMessage.bind(action, result, message, token.id), + { + action: action.uuid, + message: message._id, + token: token.id, + result + } + )); } } - onRollAllSave(event, _message) { + async onRollAllSave(event, message) { event.stopPropagation(); + if(!game.user.isGM) return; const targets = event.target.parentElement.querySelectorAll( '.target-section > [data-token] .target-save-container' ); - targets.forEach(el => { - el.dispatchEvent(new PointerEvent('click', { shiftKey: true })); + const actor = await this.getActor(message.system.source.actor), + action = this.getAction(actor, message.system.source.item, message.system.source.action); + targets.forEach(async el => { + const tokenId = el.closest('[data-token]')?.dataset.token, + token = game.canvas.tokens.get(tokenId); + if(!token.actor) return; + if(game.user === token.actor.owner) + el.dispatchEvent(new PointerEvent('click', { shiftKey: true })); + else { + token.actor.owner.query('reactionRoll', { + actionId: action.uuid, + actorId: token.actor.uuid, + event, + message + }).then(result => action.updateSaveMessage(result, message, token.id)); + } }); } diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index f3fdb3d6..8f0e0682 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -299,9 +299,9 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel /* EFFECTS */ /* SAVE */ - async rollSave(target, event, message) { - if (!target?.actor) return; - return target.actor + async rollSave(actor, event, message) { + if (!actor) return; + return actor .diceRoll({ event, title: 'Roll Save', @@ -310,16 +310,28 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel difficulty: this.save.difficulty ?? this.actor?.baseSaveDifficulty, type: 'reaction' }, - data: target.actor.getRollData() - }) - .then(async result => { - if (result) - this.updateChatMessage(message, target.id, { - result: result.roll.total, - success: result.roll.success - }); + data: actor.getRollData() }); } + + updateSaveMessage(result, message, targetId) { + const updateMsg = this.updateChatMessage.bind(this, message, targetId, { + result: result.roll.total, + success: result.roll.success + }); + if (game.modules.get('dice-so-nice')?.active) + game.dice3d.waitFor3DAnimationByMessageID(result.message.id ?? result.message._id).then(() => updateMsg()); + else updateMsg(); + } + + static rollSaveQuery({ actionId, actorId, event, message }) { + return new Promise(async (resolve, reject) => { + const actor = await fromUuid(actorId), + action = await fromUuid(actionId); + if (!actor || !actor?.isOwner) reject(); + action.rollSave(actor, event, message).then(result => resolve(result)); + }); + } /* SAVE */ async updateChatMessage(message, targetId, changes, chain = true) { @@ -333,7 +345,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel if (chain) { if (message.system.source.message) this.updateChatMessage(ui.chat.collection.get(message.system.source.message), targetId, changes, false); - const relatedChatMessages = ui.chat.collection.filter(c => c.system.source.message === message._id); + const relatedChatMessages = ui.chat.collection.filter(c => c.system.source?.message === message._id); relatedChatMessages.forEach(c => { this.updateChatMessage(c, targetId, changes, false); }); diff --git a/module/data/actor/base.mjs b/module/data/actor/base.mjs index 1f4060b0..f0db33e9 100644 --- a/module/data/actor/base.mjs +++ b/module/data/actor/base.mjs @@ -5,12 +5,14 @@ const resistanceField = (resistanceLabel, immunityLabel, reductionLabel) => resistance: new foundry.data.fields.BooleanField({ initial: false, label: `${resistanceLabel}.label`, - hint: `${resistanceLabel}.hint` + hint: `${resistanceLabel}.hint`, + isAttributeChoice: true }), immunity: new foundry.data.fields.BooleanField({ initial: false, label: `${immunityLabel}.label`, - hint: `${immunityLabel}.hint` + hint: `${immunityLabel}.hint`, + isAttributeChoice: true }), reduction: new foundry.data.fields.NumberField({ integer: true, diff --git a/module/dice/damageRoll.mjs b/module/dice/damageRoll.mjs index 43e275d4..d74ae410 100644 --- a/module/dice/damageRoll.mjs +++ b/module/dice/damageRoll.mjs @@ -12,7 +12,7 @@ export default class DamageRoll extends DHRoll { static async buildEvaluate(roll, config = {}, message = {}) { if (config.evaluate !== false) { - if (config.dialog.configure === false) roll.constructFormula(config); + // if (config.dialog.configure === false) roll.constructFormula(config); for (const roll of config.roll) await roll.roll.evaluate(); } roll._evaluated = true; diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index c60a0b90..ec464ddb 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -1,5 +1,4 @@ import { emitAsGM, GMUpdateEvent } from '../systemRegistration/socket.mjs'; -import DamageReductionDialog from '../applications/dialogs/damageReductionDialog.mjs'; import { LevelOptionType } from '../data/levelTier.mjs'; import DHFeature from '../data/item/feature.mjs'; import { damageKeyToNumber } from '../helpers/utils.mjs'; @@ -483,10 +482,14 @@ export default class DhpActor extends Actor { this.#canReduceDamage(hpDamage.value, hpDamage.damageTypes) ) { const armorStackResult = await this.owner.query('armorStack', { - actorId: this.uuid, - damage: hpDamage.value, - type: [...hpDamage.damageTypes] - }); + actorId: this.uuid, + damage: hpDamage.value, + type: [...hpDamage.damageTypes] + }, + { + timeout: 30000 + } + ); if (armorStackResult) { const { modifiedDamage, armorSpent, stressSpent } = armorStackResult; updates.find(u => u.key === 'hitPoints').value = modifiedDamage; @@ -638,7 +641,3 @@ export default class DhpActor extends Actor { }); } } - -export const registerDHActorHooks = () => { - CONFIG.queries.armorStack = DamageReductionDialog.armorStackQuery; -}; diff --git a/module/documents/token.mjs b/module/documents/token.mjs index a8105eb2..b6c47450 100644 --- a/module/documents/token.mjs +++ b/module/documents/token.mjs @@ -52,6 +52,7 @@ export default class DHToken extends TokenDocument { for (const [name, field] of Object.entries(schema.fields)) { const p = _path.concat([name]); if (field instanceof foundry.data.fields.NumberField) attributes.value.push(p); + if (field instanceof foundry.data.fields.BooleanField && field.options.isAttributeChoice) attributes.value.push(p); if (field instanceof foundry.data.fields.StringField) attributes.value.push(p); if (field instanceof foundry.data.fields.ArrayField) attributes.value.push(p); const isSchema = field instanceof foundry.data.fields.SchemaField; diff --git a/module/systemRegistration/socket.mjs b/module/systemRegistration/socket.mjs index 0037d99d..e97fe5b0 100644 --- a/module/systemRegistration/socket.mjs +++ b/module/systemRegistration/socket.mjs @@ -1,3 +1,5 @@ +import DamageReductionDialog from '../applications/dialogs/damageReductionDialog.mjs'; + export function handleSocketEvent({ action = null, data = {} } = {}) { switch (action) { case socketEvent.GMUpdate: @@ -21,7 +23,8 @@ export const socketEvent = { export const GMUpdateEvent = { UpdateDocument: 'DhGMUpdateDocument', UpdateSetting: 'DhGMUpdateSetting', - UpdateFear: 'DhGMUpdateFear' + UpdateFear: 'DhGMUpdateFear', + UpdateSaveMessage: 'DhGMUpdateSaveMessage' }; export const RefreshType = { @@ -53,8 +56,12 @@ export const registerSocketHooks = () => { ) ) ); - /* Hooks.callAll(socketEvent.DhpFearUpdate); - await game.socket.emit(`system.${CONFIG.DH.id}`, { action: socketEvent.DhpFearUpdate }); */ + break; + case GMUpdateEvent.UpdateSaveMessage: + const action = await fromUuid(data.update.action), + message = game.messages.get(data.update.message); + if(!action || !message) return; + action.updateSaveMessage(data.update.result, message, data.update.token); break; } @@ -69,6 +76,11 @@ export const registerSocketHooks = () => { }); }; +export const registerUserQueries = () => { + CONFIG.queries.armorStack = DamageReductionDialog.armorStackQuery; + CONFIG.queries.reactionRoll = game.system.api.models.actions.actionsTypes.base.rollSaveQuery; +} + export const emitAsGM = async (eventName, callback, update, uuid = null) => { if (!game.user.isGM) { return await game.socket.emit(`system.${CONFIG.DH.id}`, { diff --git a/templates/dialogs/reactionRoll.hbs b/templates/dialogs/reactionRoll.hbs new file mode 100644 index 00000000..98c94f16 --- /dev/null +++ b/templates/dialogs/reactionRoll.hbs @@ -0,0 +1,3 @@ +
+ Reaction Roll +
\ No newline at end of file From 330e15cc46f131c6dbc3ec6ac7116cdf212071e5 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Mon, 28 Jul 2025 21:17:34 +0200 Subject: [PATCH 2/6] Fixed so that extra selected proficiciency is added correctly (#448) --- module/data/actor/character.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 989e4519..ec6e5c7c 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -532,7 +532,7 @@ export default class DhCharacter extends BaseDataActor { this.evasion += selection.value; break; case 'proficiency': - this.proficiency = selection.value; + this.proficiency += selection.value; break; case 'experience': Object.keys(this.experiences).forEach(key => { From 4defe69c2148a8dd65b746c3caf6ed75a9d59d84 Mon Sep 17 00:00:00 2001 From: Cyril ALFARO Date: Mon, 28 Jul 2025 23:32:25 +0200 Subject: [PATCH 3/6] Update dhRoll.mjs (#449) --- module/dice/dhRoll.mjs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs index 4bc1a5bd..aa1ebb68 100644 --- a/module/dice/dhRoll.mjs +++ b/module/dice/dhRoll.mjs @@ -193,11 +193,12 @@ export const registerRollDiceHooks = () => { if (config.roll.isCritical) updates.push({ key: 'stress', value: -1 }); if (config.roll.result.duality === -1) updates.push({ key: 'fear', value: 1 }); - if (config.rerolledRoll.isCritical || config.rerolledRoll.result.duality === 1) - updates.push({ key: 'hope', value: -1 }); - if (config.rerolledRoll.isCritical) updates.push({ key: 'stress', value: 1 }); - if (config.rerolledRoll.result.duality === -1) updates.push({ key: 'fear', value: -1 }); - + if (config.rerolledRoll) { + if (config.rerolledRoll.isCritical || config.rerolledRoll.result.duality === 1) updates.push({ key: 'hope', value: -1 }); + if (config.rerolledRoll.isCritical) updates.push({ key: 'stress', value: 1 }); + if (config.rerolledRoll.result.duality === -1) updates.push({ key: 'fear', value: -1 }); + } + if (updates.length) { const target = actor.system.partner ?? actor; if (!['dead', 'unconcious'].some(x => actor.statuses.has(x))) { From 253faabbcfec0fe84f82d2bf130d3a6cae346f42 Mon Sep 17 00:00:00 2001 From: Cyril ALFARO Date: Tue, 29 Jul 2025 13:00:48 +0200 Subject: [PATCH 4/6] fix: statuses are Set not Array, change include() to has() in placeables/token.mjs (#451) It crash if I have a status on a token and I refresh Foundry. It's because it calls a include() function on a Set. --- module/canvas/placeables/token.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/canvas/placeables/token.mjs b/module/canvas/placeables/token.mjs index dd6f089e..4c2ae4ed 100644 --- a/module/canvas/placeables/token.mjs +++ b/module/canvas/placeables/token.mjs @@ -18,7 +18,7 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { x => x.statuses.size === 1 && x.name === game.i18n.localize(statusMap.get(x.statuses.first()).name) ); for (var status of effect.statuses) { - if (!currentStatusActiveEffects.find(x => x.statuses.includes(status))) { + if (!currentStatusActiveEffects.find(x => x.statuses.has(status))) { const statusData = statusMap.get(status); acc.push({ name: game.i18n.localize(statusData.name), From 2608c4a5ae2540cdb71e60c0ae67ee3ab6bf90fa Mon Sep 17 00:00:00 2001 From: Cyril ALFARO Date: Tue, 29 Jul 2025 16:52:29 +0200 Subject: [PATCH 5/6] Update character.mjs (#455) --- module/applications/sheets/actors/character.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index d9098f9b..6bbef5b0 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -260,7 +260,7 @@ export default class CharacterSheet extends DHBaseActorSheet { icon: 'fa-solid fa-arrow-up', condition: target => { const doc = getDocFromElementSync(target); - return doc && system.inVault; + return doc && doc.system.inVault; }, callback: async target => { const doc = await getDocFromElement(target); From 8e516df7cb553c9120b826af947cb0a2af34facd Mon Sep 17 00:00:00 2001 From: Dapoulp <74197441+Dapoulp@users.noreply.github.com> Date: Tue, 29 Jul 2025 22:34:09 +0200 Subject: [PATCH 6/6] Feature/443 adversary action roll type (#456) * Some tests * Filter types choices * Resource/Uses max as FormulaField * Removed isReversed on item resources * Stuffs --------- Co-authored-by: WBHarry --- lang/en.json | 3 +- .../dialogs/damageReductionDialog.mjs | 2 +- .../sheets-configs/action-config.mjs | 15 ++++++- module/config/generalConfig.mjs | 22 +++++----- module/config/itemConfig.mjs | 2 +- module/data/action/baseAction.mjs | 39 ++++++++---------- module/data/actor/character.mjs | 6 +++ module/data/fields/action/costField.mjs | 32 ++++++++++++--- module/data/fields/action/rollField.mjs | 37 +++++++++++++++++ module/data/fields/action/usesField.mjs | 11 ++++- module/data/item/base.mjs | 3 +- module/dice/d20Roll.mjs | 8 +--- module/documents/actor.mjs | 11 ++--- module/helpers/handlebarsHelper.mjs | 10 +++++ module/systemRegistration/socket.mjs | 2 +- templates/actionTypes/cost.hbs | 2 +- templates/actionTypes/roll.hbs | 40 ++++++++++--------- templates/dialogs/dice-roll/costSelection.hbs | 10 ++--- templates/ui/tooltip/action.hbs | 3 +- 19 files changed, 172 insertions(+), 86 deletions(-) diff --git a/lang/en.json b/lang/en.json index 40317f8c..9afa7a62 100755 --- a/lang/en.json +++ b/lang/en.json @@ -716,7 +716,7 @@ "name": "Hope", "abbreviation": "HO" }, - "armorStack": { + "armorSlot": { "name": "Armor Slot", "abbreviation": "AS" }, @@ -1381,6 +1381,7 @@ "imagePath": "Image Path", "inactiveEffects": "Inactive Effects", "inventory": "Inventory", + "itemResource": "Item Resource", "level": "Level", "levelUp": "Level Up", "loadout": "Loadout", diff --git a/module/applications/dialogs/damageReductionDialog.mjs b/module/applications/dialogs/damageReductionDialog.mjs index 9049522d..e0841324 100644 --- a/module/applications/dialogs/damageReductionDialog.mjs +++ b/module/applications/dialogs/damageReductionDialog.mjs @@ -225,7 +225,7 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap await super.close({}); } - static async armorStackQuery({ actorId, damage, type }) { + static async armorSlotQuery({ actorId, damage, type }) { return new Promise(async (resolve, reject) => { const actor = await fromUuid(actorId); if (!actor || !actor?.isOwner) reject(); diff --git a/module/applications/sheets-configs/action-config.mjs b/module/applications/sheets-configs/action-config.mjs index 7d50c0c6..20ed4993 100644 --- a/module/applications/sheets-configs/action-config.mjs +++ b/module/applications/sheets-configs/action-config.mjs @@ -108,9 +108,11 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) { context.hasBaseDamage = !!this.action.parent.attack; context.getEffectDetails = this.getEffectDetails.bind(this); context.costOptions = this.getCostOptions(); + context.getRollTypeOptions = this.getRollTypeOptions(); context.disableOption = this.disableOption.bind(this); context.isNPC = this.action.actor?.isNPC; context.baseSaveDifficulty = this.action.actor?.baseSaveDifficulty; + context.baseAttackBonus = this.action.actor?.system.attack?.roll.bonus; context.hasRoll = this.action.hasRoll; const settingsTiers = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers; @@ -131,14 +133,23 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) { const resource = this.action.parent.resource; if (resource) { options[this.action.parent.parent.id] = { - label: this.action.parent.parent.name, - group: 'TYPES.Actor.character' + label: "DAGGERHEART.GENERAL.itemResource", + group: 'Global' }; } return options; } + getRollTypeOptions() { + const types = foundry.utils.deepClone(CONFIG.DH.GENERAL.rollTypes); + if(!this.action.actor) return types; + Object.values(types).forEach(t => { + if(this.action.actor.type !== 'character' && t.playerOnly) delete types[t.id]; + }) + return types; + } + disableOption(index, costOptions, choices) { const filtered = foundry.utils.deepClone(costOptions); Object.keys(filtered).forEach(o => { diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index 7e44cad7..81d4309f 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -85,10 +85,10 @@ export const healingTypes = { label: 'DAGGERHEART.CONFIG.HealingType.hope.name', abbreviation: 'DAGGERHEART.CONFIG.HealingType.hope.abbreviation' }, - armorStack: { - id: 'armorStack', - label: 'DAGGERHEART.CONFIG.HealingType.armorStack.name', - abbreviation: 'DAGGERHEART.CONFIG.HealingType.armorStack.abbreviation' + armorSlot: { + id: 'armorSlot', + label: 'DAGGERHEART.CONFIG.HealingType.armorSlot.name', + abbreviation: 'DAGGERHEART.CONFIG.HealingType.armorSlot.abbreviation' }, fear: { id: 'fear', @@ -199,7 +199,7 @@ export const defaultRestOptions = { actionType: 'action', chatDisplay: false, healing: { - applyTo: healingTypes.armorStack.id, + applyTo: healingTypes.armorSlot.id, value: { custom: { enabled: true, @@ -287,7 +287,7 @@ export const defaultRestOptions = { actionType: 'action', chatDisplay: false, healing: { - applyTo: healingTypes.armorStack.id, + applyTo: healingTypes.armorSlot.id, value: { custom: { enabled: true, @@ -425,8 +425,8 @@ export const refreshTypes = { }; export const abilityCosts = { - hp: { - id: 'hp', + hitPoints: { + id: 'hitPoints', label: 'DAGGERHEART.CONFIG.HealingType.hitPoints.name', group: 'Global' }, @@ -473,11 +473,13 @@ export const rollTypes = { }, spellcast: { id: 'spellcast', - label: 'DAGGERHEART.CONFIG.RollTypes.spellcast.name' + label: 'DAGGERHEART.CONFIG.RollTypes.spellcast.name', + playerOnly: true }, trait: { id: 'trait', - label: 'DAGGERHEART.CONFIG.RollTypes.trait.name' + label: 'DAGGERHEART.CONFIG.RollTypes.trait.name', + playerOnly: true }, diceSet: { id: 'diceSet', diff --git a/module/config/itemConfig.mjs b/module/config/itemConfig.mjs index 851ddc32..4b2e3144 100644 --- a/module/config/itemConfig.mjs +++ b/module/config/itemConfig.mjs @@ -604,7 +604,7 @@ export const weaponFeatures = { img: 'icons/skills/melee/hand-grip-sword-strike-orange.webp', cost: [ { - type: 'armorStack', + type: 'armorSlot', value: 1 } ], diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index 8f0e0682..26c86d87 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -185,13 +185,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel prepareRoll() { const roll = { - modifiers: this.modifiers, - trait: this.roll?.trait, + baseModifiers: this.roll.getModifier(), label: 'Attack', type: this.actionType, difficulty: this.roll?.difficulty, formula: this.roll.getFormula(), - bonus: this.roll.bonus, advantage: CONFIG.DH.ACTIONS.advantageState[this.roll.advState].value }; if (this.roll?.type === 'diceSet') roll.lite = true; @@ -205,6 +203,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel async consume(config) { const usefulResources = foundry.utils.deepClone(this.actor.system.resources); + for (var cost of config.costs) { if (cost.keyIsID) { usefulResources[cost.key] = { @@ -225,19 +224,16 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel keyIsID: resource.keyIsID }; }); - + console.log(resources) await this.actor.modifyResource(resources); - if (config.uses?.enabled) { - const newActions = foundry.utils.getProperty(this.item.system, this.systemPath).map(x => x.toObject()); - newActions[this.index].uses.value++; - await this.item.update({ [`system.${this.systemPath}`]: newActions }); - } + if (config.uses?.enabled) + this.update({ 'uses.value': this.uses.value + 1 }); } /* */ /* ROLL */ get hasRoll() { - return !!this.roll?.type || !!this.roll?.bonus; + return !!this.roll?.type; } get modifiers() { @@ -301,17 +297,16 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel /* SAVE */ async rollSave(actor, event, message) { if (!actor) return; - return actor - .diceRoll({ - event, - title: 'Roll Save', - roll: { - trait: this.save.trait, - difficulty: this.save.difficulty ?? this.actor?.baseSaveDifficulty, - type: 'reaction' - }, - data: actor.getRollData() - }); + return actor.diceRoll({ + event, + title: 'Roll Save', + roll: { + trait: this.save.trait, + difficulty: this.save.difficulty ?? this.actor?.baseSaveDifficulty, + type: 'reaction' + }, + data: actor.getRollData() + }); } updateSaveMessage(result, message, targetId) { @@ -324,7 +319,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel else updateMsg(); } - static rollSaveQuery({ actionId, actorId, event, message }) { + static rollSaveQuery({ actionId, actorId, event, message }) { return new Promise(async (resolve, reject) => { const actor = await fromUuid(actorId), action = await fromUuid(actionId); diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index ec6e5c7c..41a62091 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -563,6 +563,12 @@ export default class DhCharacter extends BaseDataActor { this.resources.hope.value = Math.min(baseHope, this.resources.hope.max); this.attack.roll.trait = this.rules.attack.roll.trait ?? this.attack.roll.trait; + this.resources.armor = { + value: this.armor.system.marks.value, + max: this.armorScore, + isReversed: true + }; + this.attack.damage.parts[0].value.custom.formula = `@prof${this.basicAttackDamageDice}${this.rules.attack.damage.bonus ? ` + ${this.rules.attack.damage.bonus}` : ''}`; } diff --git a/module/data/fields/action/costField.mjs b/module/data/fields/action/costField.mjs index f5e1999c..4ddfb8e9 100644 --- a/module/data/fields/action/costField.mjs +++ b/module/data/fields/action/costField.mjs @@ -26,11 +26,19 @@ 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; c.step = c.step ?? 1; - c.total = c.value * c.scale * c.step; + c.total = c.value + ((c.scale - 1) * c.step); c.enabled = c.hasOwnProperty('enabled') ? c.enabled : true; + c.max = c.key === 'fear' + ? game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear) + : resources[c.key].isReversed + ? resources[c.key].max + : resources[c.key].value + if(c.scalable) c.maxStep = Math.floor(c.max / c.step); return c; }); } @@ -51,9 +59,11 @@ export default class CostField extends fields.ArrayField { const resources = CostField.getResources.call(this, realCosts); return realCosts.reduce( (a, c) => - a && resources[c.key].isReversed - ? resources[c.key].value + (c.total ?? c.value) <= resources[c.key].max - : resources[c.key]?.value >= (c.total ?? c.value), + !resources[c.key] + ? a + : a && resources[c.key].isReversed + ? resources[c.key].value + (c.total ?? c.value) <= resources[c.key].max + : resources[c.key]?.value >= (c.total ?? c.value), true ); } @@ -61,10 +71,11 @@ export default class CostField extends fields.ArrayField { static getResources(costs) { const actorResources = this.actor.system.resources; const itemResources = {}; - for (var itemResource of costs) { + for (let itemResource of costs) { if (itemResource.keyIsID) { itemResources[itemResource.key] = { - value: this.parent.resource.value ?? 0 + value: this.parent.resource.value ?? 0, + max: CostField.formatMax.call(this, this.parent?.resource?.max) }; } } @@ -79,4 +90,13 @@ export default class CostField extends fields.ArrayField { const realCosts = costs?.length ? costs.filter(c => c.enabled) : []; return realCosts; } + + static formatMax(max) { + max ??= 0; + if (isNaN(max)) { + const roll = Roll.replaceFormulaData(max, this.getRollData()); + max = roll.total; + } + return Number(max); + } } diff --git a/module/data/fields/action/rollField.mjs b/module/data/fields/action/rollField.mjs index 511e0660..f27938dc 100644 --- a/module/data/fields/action/rollField.mjs +++ b/module/data/fields/action/rollField.mjs @@ -66,6 +66,43 @@ export class DHActionRollData extends foundry.abstract.DataModel { } return formula; } + + getModifier() { + const modifiers = []; + if(!this.parent?.actor) return modifiers; + switch (this.parent.actor.type) { + case 'character': + const trait = this.useDefault || !this.trait ? (this.parent.item.system.attack.roll.trait ?? 'agility') : this.trait; + if(this.type === CONFIG.DH.GENERAL.rollTypes.attack.id || this.type === CONFIG.DH.GENERAL.rollTypes.trait.id) + modifiers.push( + { + label: `DAGGERHEART.CONFIG.Traits.${trait}.name`, + value: this.parent.actor.system.traits[trait].value + } + ) + else if(this.type === CONFIG.DH.GENERAL.rollTypes.spellcast.id) + modifiers.push( + { + label: `DAGGERHEART.CONFIG.RollTypes.spellcast.name`, + value: this.parent.actor.system.spellcastModifier + } + ) + break; + case 'companion': + case 'adversary': + if(this.type === CONFIG.DH.GENERAL.rollTypes.attack.id) + modifiers.push( + { + label: 'Bonus to Hit', + value: this.bonus ?? this.parent.actor.system.attack.roll.bonus + } + ) + break; + default: + break; + } + return modifiers; + } } export default class RollField extends fields.EmbeddedDataField { diff --git a/module/data/fields/action/usesField.mjs b/module/data/fields/action/usesField.mjs index df6c5d0c..177904a1 100644 --- a/module/data/fields/action/usesField.mjs +++ b/module/data/fields/action/usesField.mjs @@ -1,10 +1,12 @@ +import FormulaField from "../formulaField.mjs"; + const fields = foundry.data.fields; export default class UsesField extends fields.SchemaField { constructor(options = {}, context = {}) { const usesFields = { value: new fields.NumberField({ nullable: true, initial: null }), - max: new fields.NumberField({ nullable: true, initial: null }), + max: new FormulaField({ nullable: true, initial: null, deterministic: true }), recovery: new fields.StringField({ choices: CONFIG.DH.GENERAL.refreshTypes, initial: null, @@ -33,6 +35,11 @@ export default class UsesField extends fields.SchemaField { static hasUses(uses) { if (!uses) return true; - return (uses.hasOwnProperty('enabled') && !uses.enabled) || uses.value + 1 <= uses.max; + let max = uses.max ?? 0; + if(isNaN(max)) { + const roll = new Roll(Roll.replaceFormulaData(uses.max, this.getRollData())).evaluateSync(); + max = roll.total; + } + return (uses.hasOwnProperty('enabled') && !uses.enabled) || uses.value + 1 <= max; } } diff --git a/module/data/item/base.mjs b/module/data/item/base.mjs index 5b95a810..87267c39 100644 --- a/module/data/item/base.mjs +++ b/module/data/item/base.mjs @@ -10,6 +10,7 @@ import { addLinkedItemsDiff, updateLinkedItemApps } from '../../helpers/utils.mjs'; import { ActionsField } from '../fields/actionField.mjs'; +import FormulaField from '../fields/formulaField.mjs'; const fields = foundry.data.fields; @@ -48,7 +49,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { initial: CONFIG.DH.ITEM.itemResourceTypes.simple }), value: new fields.NumberField({ integer: true, min: 0, initial: 0 }), - max: new fields.StringField({ nullable: true, initial: null }), + max: new FormulaField({ nullable: true, initial: null, deterministic: true }), icon: new fields.StringField(), recovery: new fields.StringField({ choices: CONFIG.DH.GENERAL.refreshTypes, diff --git a/module/dice/d20Roll.mjs b/module/dice/d20Roll.mjs index ce400110..701616f1 100644 --- a/module/dice/d20Roll.mjs +++ b/module/dice/d20Roll.mjs @@ -124,13 +124,7 @@ export default class D20Roll extends DHRoll { } applyBaseBonus() { - const modifiers = []; - - if (this.options.roll.bonus) - modifiers.push({ - label: 'Bonus to Hit', - value: this.options.roll.bonus - }); + const modifiers = foundry.utils.deepClone(this.options.roll.baseModifiers) ?? []; modifiers.push(...this.getBonus(`roll.${this.options.type}`, `${this.options.type?.capitalize()} Bonus`)); modifiers.push( diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index ec464ddb..03dabd34 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -481,7 +481,7 @@ export default class DhpActor extends Actor { this.system.armor && this.#canReduceDamage(hpDamage.value, hpDamage.damageTypes) ) { - const armorStackResult = await this.owner.query('armorStack', { + const armorSlotResult = await this.owner.query('armorSlot', { actorId: this.uuid, damage: hpDamage.value, type: [...hpDamage.damageTypes] @@ -490,11 +490,11 @@ export default class DhpActor extends Actor { timeout: 30000 } ); - if (armorStackResult) { - const { modifiedDamage, armorSpent, stressSpent } = armorStackResult; + if (armorSlotResult) { + const { modifiedDamage, armorSpent, stressSpent } = armorSlotResult; updates.find(u => u.key === 'hitPoints').value = modifiedDamage; updates.push( - ...(armorSpent ? [{ value: armorSpent, key: 'armorStack' }] : []), + ...(armorSpent ? [{ value: armorSpent, key: 'armor' }] : []), ...(stressSpent ? [{ value: stressSpent, key: 'stress' }] : []) ); } @@ -569,6 +569,7 @@ export default class DhpActor extends Actor { armor: { target: this.system.armor, resources: {} }, items: {} }; + resources.forEach(r => { if (r.keyIsID) { updates.items[r.key] = { @@ -584,7 +585,7 @@ export default class DhpActor extends Actor { game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear) + r.value ); break; - case 'armorStack': + case 'armor': updates.armor.resources['system.marks.value'] = Math.max( Math.min(this.system.armor.system.marks.value + r.value, this.system.armorScore), 0 diff --git a/module/helpers/handlebarsHelper.mjs b/module/helpers/handlebarsHelper.mjs index 751b2c38..790d291c 100644 --- a/module/helpers/handlebarsHelper.mjs +++ b/module/helpers/handlebarsHelper.mjs @@ -7,6 +7,7 @@ export default class RegisterHandlebarsHelpers { includes: this.includes, times: this.times, damageFormula: this.damageFormula, + formulaValue: this.formulaValue, damageSymbols: this.damageSymbols, rollParsed: this.rollParsed, hasProperty: foundry.utils.hasProperty, @@ -39,6 +40,15 @@ export default class RegisterHandlebarsHelpers { return instances.join(traitTotal > 0 ? ' + ' : ' - '); } + static formulaValue(formula, item) { + if(isNaN(formula)) { + const data = item.getRollData.bind(item)(), + roll = new Roll(Roll.replaceFormulaData(formula, data)).evaluateSync(); + formula = roll.total; + } + return formula; + } + static damageSymbols(damageParts) { const symbols = [...new Set(damageParts.reduce((a, c) => a.concat([...c.type]), []))].map( p => CONFIG.DH.GENERAL.damageTypes[p].icon diff --git a/module/systemRegistration/socket.mjs b/module/systemRegistration/socket.mjs index e97fe5b0..e3885450 100644 --- a/module/systemRegistration/socket.mjs +++ b/module/systemRegistration/socket.mjs @@ -77,7 +77,7 @@ export const registerSocketHooks = () => { }; export const registerUserQueries = () => { - CONFIG.queries.armorStack = DamageReductionDialog.armorStackQuery; + CONFIG.queries.armorSlot = DamageReductionDialog.armorSlotQuery; CONFIG.queries.reactionRoll = game.system.api.models.actions.actionsTypes.base.rollSaveQuery; } diff --git a/templates/actionTypes/cost.hbs b/templates/actionTypes/cost.hbs index 116fc631..e956b284 100644 --- a/templates/actionTypes/cost.hbs +++ b/templates/actionTypes/cost.hbs @@ -6,7 +6,7 @@ {{#each source as |cost index|}}
{{formField ../fields.scalable label="Scalable" value=cost.scalable name=(concat "cost." index ".scalable") classes="checkbox"}} - {{formField ../fields.key choices=(@root.disableOption index @root.costOptions ../source) label="Resource" value=cost.key name=(concat "cost." index ".key") localize=true}} + {{formField ../fields.key choices=(@root.disableOption index @root.costOptions ../source) label="Resource" value=cost.key name=(concat "cost." index ".key") localize=true blank=false}} {{formField ../fields.value label="Amount" value=cost.value name=(concat "cost." index ".value")}} {{formField ../fields.step label="Step" value=cost.step name=(concat "cost." index ".step") disabled=(not cost.scalable)}} diff --git a/templates/actionTypes/roll.hbs b/templates/actionTypes/roll.hbs index 2d2dab3c..3e25b9c8 100644 --- a/templates/actionTypes/roll.hbs +++ b/templates/actionTypes/roll.hbs @@ -3,25 +3,27 @@ Roll {{#if @root.hasBaseDamage}}{{formInput fields.useDefault name="roll.useDefault" value=source.useDefault dataset=(object tooltip="Use default Item values" tooltipDirection="UP")}}{{/if}} - {{#if @root.isNPC}} - {{formField fields.bonus label="Bonus" name="roll.bonus" value=source.bonus}} - {{formField fields.advState label= "Advantage State" name="roll.advState" value=source.advState localize=true}} + + {{formField fields.type label="Type" name="roll.type" value=source.type localize=true choices=@root.getRollTypeOptions}} + {{#if (eq source.type "diceSet")}} +
+ {{formField fields.diceRolling.fields.multiplier name="roll.diceRolling.multiplier" value=source.diceRolling.multiplier localize=true}} + {{#if (eq source.diceRolling.multiplier 'flat')}}{{formField fields.diceRolling.fields.flatMultiplier value=source.diceRolling.flatMultiplier name="roll.diceRolling.flatMultiplier" localize=true }}{{/if}} + {{formField fields.diceRolling.fields.dice name="roll.diceRolling.dice" value=source.diceRolling.dice localize=true}} + {{formField fields.diceRolling.fields.compare name="roll.diceRolling.compare" value=source.diceRolling.compare localize=true blank=""}} + {{formField fields.diceRolling.fields.treshold name="roll.diceRolling.treshold" value=source.diceRolling.treshold localize=true}} +
{{else}} - {{formField fields.type label="Type" name="roll.type" value=source.type localize=true}} - {{#if (eq source.type "diceSet")}} -
- {{formField fields.diceRolling.fields.multiplier name="roll.diceRolling.multiplier" value=source.diceRolling.multiplier localize=true}} - {{#if (eq source.diceRolling.multiplier 'flat')}}{{formField fields.diceRolling.fields.flatMultiplier value=source.diceRolling.flatMultiplier name="roll.diceRolling.flatMultiplier" localize=true }}{{/if}} - {{formField fields.diceRolling.fields.dice name="roll.diceRolling.dice" value=source.diceRolling.dice localize=true}} - {{formField fields.diceRolling.fields.compare name="roll.diceRolling.compare" value=source.diceRolling.compare localize=true blank=""}} - {{formField fields.diceRolling.fields.treshold name="roll.diceRolling.treshold" value=source.diceRolling.treshold localize=true}} -
- {{else}} -
- {{formField fields.trait label="Trait" name="roll.trait" value=source.trait localize=true disabled=(not source.type)}} - {{formField fields.difficulty label="Difficulty" name="roll.difficulty" value=source.difficulty disabled=(not source.type)}} - {{formField fields.advState label= "Advantage State" name="roll.advState" value=source.advState localize=true}} -
- {{/if}} +
+ {{#unless (eq source.type 'spellcast')}} + {{#if @root.isNPC}} + {{formField fields.bonus label="Bonus" name="roll.bonus" value=source.bonus placeholder=@root.baseAttackBonus disabled=(not source.type)}} + {{else}} + {{formField fields.trait label="Trait" name="roll.trait" value=source.trait localize=true disabled=(not source.type)}} + {{/if}} + {{/unless}} + {{formField fields.difficulty label="Difficulty" name="roll.difficulty" value=source.difficulty disabled=(not source.type)}} + {{formField fields.advState label= "Advantage State" name="roll.advState" value=source.advState localize=true disabled=(not source.type)}} +
{{/if}} \ No newline at end of file diff --git a/templates/dialogs/dice-roll/costSelection.hbs b/templates/dialogs/dice-roll/costSelection.hbs index d376c749..3ece4cbf 100644 --- a/templates/dialogs/dice-roll/costSelection.hbs +++ b/templates/dialogs/dice-roll/costSelection.hbs @@ -8,8 +8,8 @@
- - + {{log @root}} + {{/if}} {{#each costs as | cost index |}} @@ -20,10 +20,10 @@ - {{#if scalable}} - + {{#if (and scalable (gt maxStep 1))}} + {{/if}} - + {{/each}} diff --git a/templates/ui/tooltip/action.hbs b/templates/ui/tooltip/action.hbs index 59e0be70..20929bf3 100644 --- a/templates/ui/tooltip/action.hbs +++ b/templates/ui/tooltip/action.hbs @@ -2,7 +2,6 @@

{{item.name}}

{{{description}}}
- {{#if item.uses.max}}

{{localize "DAGGERHEART.GENERAL.uses"}}

@@ -12,7 +11,7 @@
-
{{item.uses.max}}
+
{{formulaValue item.uses.max item}}