Skip to content

Commit cc7d577

Browse files
committed
ADD - Damage formula for weapons
1 parent 5c6f39b commit cc7d577

10 files changed

Lines changed: 173 additions & 11 deletions

File tree

src/animabf.mjs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import './scss/animabf.scss';
2828
import { System, registerSystemOnGame } from './utils/systemMeta';
2929

3030
import { resolveTokenName } from './utils/tokenName.js';
31+
import { FormulaEvaluator } from './utils/formulaEvaluator.js';
3132

3233
/* ------------------------------------ */
3334
/* Initialize system */
@@ -282,6 +283,42 @@ Hooks.on('getChatMessageContextOptions', (_app, menu) => {
282283
}
283284
});
284285

286+
Hooks.on('chatMessage', (chatLog, message, chatData) => {
287+
if (!message || !message.includes('@formula{')) return;
288+
if (chatData._animabfFormulaDone) return;
289+
290+
const speaker = chatData.speaker || {};
291+
let actor = null;
292+
293+
if (speaker.actor) {
294+
actor = game.actors.get(speaker.actor) ?? actor;
295+
}
296+
297+
if (!actor && speaker.token) {
298+
try {
299+
const tokenDoc = fromUuidSync(speaker.token);
300+
actor = tokenDoc?.actor ?? actor;
301+
} catch (e) {
302+
console.error('Formula @ speaker.token resolve error', e);
303+
}
304+
}
305+
306+
if (!actor && canvas?.tokens?.controlled?.length) {
307+
actor = canvas.tokens.controlled[0]?.actor ?? actor;
308+
}
309+
310+
const replaced = message.replace(/@formula\{([^}]+)\}/g, (match, inner) => {
311+
const value = FormulaEvaluator.evaluate(inner, actor);
312+
return value ?? match;
313+
});
314+
315+
if (replaced === message) return;
316+
317+
chatData._animabfFormulaDone = true;
318+
chatLog.processMessage(replaced, chatData);
319+
return false;
320+
});
321+
285322
// // Auto-number unlinked tokens as "{name} (n)" when dropped
286323
// Hooks.on('createToken', async doc => {
287324
// // Ignore linked tokens

src/lang/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@
113113
"anima.ui.combat.weapons.reducedArmor.final.title": "Reduced armor",
114114
"anima.ui.combat.weapons.equipped.title": "Equipped?",
115115
"anima.ui.combat.weapons.hasOwnStr.title": "Own STR?",
116+
"anima.ui.combat.weapons.damage.formula.title": "Damage Formula",
117+
"anima.ui.combat.weapons.damage.applyQualityInFormula.title": "Add quality damage",
118+
"anima.ui.combat.weapons.useFormula.title": "Use custom formula",
116119
"anima.ui.combat.weapons.initiative.base.title": "Initiative base",
117120
"anima.ui.combat.weapons.initiative.final.title": "Initiative",
118121
"anima.ui.combat.weapons.integrity.base.title": "Base integrity",

src/lang/es.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@
113113
"anima.ui.combat.weapons.reducedArmor.final.title": "Reducción de armadura",
114114
"anima.ui.combat.weapons.equipped.title": "¿Equipada?",
115115
"anima.ui.combat.weapons.hasOwnStr.title": "¿FUE propio?",
116+
"anima.ui.combat.weapons.damage.formula.title": "Fórmula Daño",
117+
"anima.ui.combat.weapons.damage.applyQualityInFormula.title": "Sumar daño de calidad",
118+
"anima.ui.combat.weapons.useFormula.title": "Usar fórmula personalizada",
116119
"anima.ui.combat.weapons.initiative.base.title": "Turno base",
117120
"anima.ui.combat.weapons.initiative.final.title": "Turno",
118121
"anima.ui.combat.weapons.integrity.base.title": "Entereza base",

src/lang/fr.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@
115115
"anima.ui.combat.weapons.defaultAttackButton.title": "Attaque par défaut",
116116
"anima.ui.combat.weapons.equipped.title": "Equipé?",
117117
"anima.ui.combat.weapons.hasOwnStr.title": "Assez de FOR?",
118+
"anima.ui.combat.weapons.damage.formula.title": "Damage Formula",
119+
"anima.ui.combat.weapons.damage.applyQualityInFormula.title": "Add quality damage",
120+
"anima.ui.combat.weapons.useFormula.title": "Use custom formula",
118121
"anima.ui.combat.weapons.initiative.base.title": "Base Initiative",
119122
"anima.ui.combat.weapons.initiative.final.title": "Initiative",
120123
"anima.ui.combat.weapons.integrity.base.title": "Base Solidité",

src/module/actor/utils/prepareActor/calculations/items/weapon/calculations/calculateWeaponDamage.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
WeaponSizeProportion
44
} from '../../../../../../../types/combat/WeaponItemConfig';
55
import { calculateWeaponStrengthModifier } from '../util/calculateWeaponStrengthModifier';
6+
import { FormulaEvaluator } from '../../../../../../../../utils/formulaEvaluator.js';
67

78
/**
89
* @param {import('../../../../../../../types/Items').WeaponDataSource} weapon
@@ -28,8 +29,27 @@ const addSizeModifier = (weapon, damage) => {
2829
*/
2930
export const calculateWeaponDamage = (weapon, data) => {
3031
const getDamage = () => {
32+
const formula = weapon.system?.damage?.formula?.value?.trim();
33+
const useFormula = weapon.system?.useCustomFormula.value;
34+
35+
if (useFormula && formula) {
36+
const fakeActor = { system: data };
37+
const value = FormulaEvaluator.evaluate(formula, fakeActor);
38+
39+
if (value !== null && !Number.isNaN(value)) {
40+
const specialBonus = weapon.system.damage.special?.value ?? 0;
41+
const extraDamage = data.general.modifiers.extraDamage?.value ?? 0;
42+
43+
const addQuality = weapon.system.damage.applyQualityInFormula?.value === true;
44+
const qualityBonus = addQuality ? (weapon.system.quality?.value ?? 0) * 2 : 0;
45+
46+
return value + specialBonus + extraDamage + qualityBonus;
47+
}
48+
}
49+
3150
const weaponStrengthModifier = calculateWeaponStrengthModifier(weapon, data);
32-
const extraDamage = data.general.modifiers.extraDamage.value + weapon.system.damage.special.value;
51+
const extraDamage =
52+
data.general.modifiers.extraDamage.value + weapon.system.damage.special.value;
3353

3454
if (
3555
weapon.system.isRanged.value &&

src/module/actor/utils/prepareActor/calculations/items/weapon/mutateWeaponsData.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ export const mutateWeaponsData = data => {
4141
weapon.system.damage = {
4242
base: weapon.system.damage.base,
4343
special: weapon.system.damage.special,
44-
final: { value: calculateWeaponDamage(weapon, data) }
44+
final: { value: calculateWeaponDamage(weapon, data) },
45+
formula: weapon.system.damage.formula ?? { value: '' },
46+
applyQualityInFormula: weapon.system.damage.applyQualityInFormula
4547
};
4648

4749
weapon.system.reducedArmor.base.value = calculateArmorReductionFromQuality(weapon);

src/module/types/combat/WeaponItemConfig.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,9 @@ export const INITIAL_WEAPON_DATA = {
128128
damage: {
129129
base: { value: 0 },
130130
special: { value: 0 },
131-
final: { value: 0 }
131+
final: { value: 0 },
132+
formula: { value: '' },
133+
applyQualityInFormula: { value: false }
132134
},
133135
initiative: {
134136
base: { value: 0 },
@@ -173,7 +175,8 @@ export const INITIAL_WEAPON_DATA = {
173175
critic: {
174176
primary: { value: WeaponCritic.CUT },
175177
secondary: { value: NoneWeaponCritic.NONE }
176-
}
178+
},
179+
useCustomFormula: { value: false }
177180
};
178181

179182
/** @type {import("../Items").WeaponItemConfig} */

src/template.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -748,7 +748,9 @@
748748
"damage": {
749749
"base": { "value": 0 },
750750
"final": { "value": 0 },
751-
"special": { "value": 0 }
751+
"special": { "value": 0 },
752+
"formula": { "value": "" },
753+
"applyQualityInFormula": { "value": false }
752754
},
753755
"initiative": {
754756
"base": { "value": 0 },
@@ -777,7 +779,8 @@
777779
"sizeProportion": { "value": "" },
778780
"hasOwnStr": { "value": false },
779781
"weaponStrength": { "base": { "value": 0 }, "final": { "value": 0 } },
780-
"critic": { "primary": { "value": "-" }, "secondary": { "value": "-" } }
782+
"critic": { "primary": { "value": "-" }, "secondary": { "value": "-" } },
783+
"useCustomFormula": {"value": false }
781784
},
782785
"supernaturalShield": {
783786
"type": { "value": "none" },

src/templates/items/weapon/weapon.hbs

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,41 @@
8585
inputValue=system.breaking.base.value
8686
}}
8787
{{>
88-
"systems/animabf/templates/common/ui/vertical-titled-input.hbs"
89-
class="damage"
90-
title=(localize "anima.ui.combat.weapons.damage.base.title")
91-
inputName="system.damage.base.value"
92-
inputValue=system.damage.base.value
88+
"systems/animabf/templates/common/ui/vertical-titled-input.hbs"
89+
class="use-formula"
90+
title=(localize "anima.ui.combat.weapons.useFormula.title")
91+
inputType="checkbox"
92+
inputName="system.useCustomFormula.value"
93+
inputValue=system.useCustomFormula.value
9394
}}
95+
{{#if system.useCustomFormula.value}}
96+
{{>
97+
"systems/animabf/templates/common/ui/vertical-titled-input.hbs"
98+
class="special"
99+
title=(localize "anima.ui.combat.weapons.damage.formula.title")
100+
inputType="text"
101+
inputName="system.damage.formula.value"
102+
inputValue=system.damage.formula.value
103+
}}
104+
{{>
105+
"systems/animabf/templates/common/ui/vertical-titled-input.hbs"
106+
class="add-quality-damage"
107+
title=(localize "anima.ui.combat.weapons.damage.applyQualityInFormula.title")
108+
inputType="checkbox"
109+
inputName="system.damage.applyQualityInFormula.value"
110+
inputValue=system.damage.applyQualityInFormula.value
111+
}}
112+
{{else}}
113+
{{>
114+
"systems/animabf/templates/common/ui/vertical-titled-input.hbs"
115+
class="damage"
116+
title=(localize "anima.ui.combat.weapons.damage.base.title")
117+
inputName="system.damage.base.value"
118+
inputValue=system.damage.base.value
119+
}}
120+
{{/if}}
121+
122+
94123
{{>
95124
"systems/animabf/templates/common/ui/vertical-titled-input.hbs"
96125
class="initiative-base"

src/utils/formulaEvaluator.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// utils/formulaEvaluator.js
2+
3+
export class FormulaEvaluator {
4+
/**
5+
* Eval numeric formula using actor data paths.
6+
* Supports @paths like:
7+
* - @characteristics.primaries.power.mod
8+
* - @system.characteristics.primaries.power.mod
9+
* No dice allowed here.
10+
*
11+
* @param {string} formula
12+
* @param {Actor|null} actor
13+
* @returns {number|null}
14+
*/
15+
static evaluate(formula, actor = null) {
16+
const clean = (formula ?? '').trim();
17+
if (!clean) return null;
18+
19+
// No dice inside @formula for now
20+
if (/[dD]\d+/.test(clean)) {
21+
console.warn('FormulaEvaluator: dice are not allowed inside @formula:', clean);
22+
return null;
23+
}
24+
25+
// Local context from actor.system
26+
/** @type {any} */
27+
const ctx = actor?.system ? foundry.utils.duplicate(actor.system) : {};
28+
29+
// Allow both @foo.bar and @system.foo.bar
30+
ctx.system = ctx;
31+
32+
try {
33+
// Replace @path with numeric values from ctx
34+
const replaced = clean.replace(/@([a-zA-Z0-9_.]+)/g, (match, path) => {
35+
const value = foundry.utils.getProperty(ctx, path);
36+
const num = Number(value);
37+
return Number.isFinite(num) ? String(num) : '0';
38+
});
39+
40+
const compact = replaced.replace(/\s+/g, '');
41+
if (!/^[0-9+\-*/().]*$/.test(compact)) {
42+
console.error('FormulaEvaluator: invalid chars after replace', {
43+
original: clean,
44+
replaced
45+
});
46+
return null;
47+
}
48+
49+
const total = Roll.safeEval(replaced);
50+
return Number.isFinite(total) ? total : null;
51+
} catch (err) {
52+
console.error('FormulaEvaluator error evaluating formula:', {
53+
formula: clean,
54+
err
55+
});
56+
return null;
57+
}
58+
}
59+
}

0 commit comments

Comments
 (0)