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|}}
-
- {{uses.value}}/{{uses.max}}
+ {{log @root}}
+ {{uses.value}}/{{formulaValue uses.max @root.rollConfig.data}}
{{/if}}
{{#each costs as | cost index |}}
@@ -20,10 +20,10 @@
{{label}}
- {{#if scalable}}
-
+ {{#if (and scalable (gt maxStep 1))}}
+
{{/if}}
- {{total}}/10
+ {{total}}/{{max}}
{{/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 @@
{{{description}}}
-
{{#if item.uses.max}}
@@ -12,7 +11,7 @@
{{localize "DAGGERHEART.GENERAL.recovery"}}