Skip to content

Commit

Permalink
Add damage model bonus for expression support (#220)
Browse files Browse the repository at this point in the history
* Add a damage bonus field which supports expressions
- Migrates the skill data model to use the V2 models
* Introduce async evaluation
* Fix skill checks with weapons

---------

Co-authored-by: Sören Jahns <[email protected]>
  • Loading branch information
Azurelol and Shourn authored Feb 19, 2025
1 parent 4c71de3 commit 6c62792
Show file tree
Hide file tree
Showing 34 changed files with 794 additions and 381 deletions.
3 changes: 2 additions & 1 deletion lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1284,6 +1284,7 @@
"DamagePipelineStepIncomingDamagePoison": "Incoming Damage: Poison",
"EndOfTurn": "End of Turn",
"EndOfRound": "End of Round",
"EndOfCombat": "End of Combat"
"EndOfCombat": "End of Combat",
"Extra": "Extra"
}
}
42 changes: 40 additions & 2 deletions module/checks/check-configuration.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,27 @@ const initHrZero = (hrZero) => (check) => {
* @typedef DamageData
* @property {DamageType} type
* @property {BonusDamage[]} modifiers
* @property {number} [modifierTotal]
* @property {number} modifierTotal
* @property {number} [total]
* @property {String} extra An expression to evaluate to add extra damage
*/

/**
* @typedef TemplateDamageData
* @property {Object} result - The result attributes from the check.
* @property {number} result.attr1 - The primary check result.
* @property {number} result.attr2 - The secondary check result.
* @property {Object} damage - The damage details.
* @property {number} damage.hrZero - The HR zero value.
* @property {number} damage.bonus - The total damage bonus.
* @property {number} damage.total - The total calculated damage.
* @property {string} damage.type - The type of damage.
* @property {String} damage.extra - Additional damage information.
* @property {Object} translation - Translation details for damage types and icons.
* @property {Object} translation.damageTypes - The available damage types.
* @property {Object} translation.damageIcon - The icon representation of damage types.
* @property {Array} modifiers - Modifiers applied to the damage.
*
*/

/**
Expand Down Expand Up @@ -76,6 +95,13 @@ class CheckConfigurer {
return this;
}

/**
* @returns {DamageData}
*/
get damage() {
return this.#check.additionalData[DAMAGE];
}

/**
* @param {FUItem} item
* @param {FUActor} actor
Expand All @@ -91,7 +117,7 @@ class CheckConfigurer {
* @return {CheckConfigurer}
*/
addModelAccuracyBonuses(model, actor) {
// Wewapon Category
// Weapon Category
const category = model.category?.value;
if (category && actor.system.bonuses.accuracy[category]) {
this.#check.modifiers.push({
Expand All @@ -116,6 +142,7 @@ class CheckConfigurer {
}

/**
* @description A modifier to the check (accuracy)
* @param {String} label
* @param {Number} value
*/
Expand Down Expand Up @@ -188,6 +215,15 @@ class CheckConfigurer {
return this;
}

/**
* @param {String} extra
* @return {CheckConfigurer}
*/
setExtraDamage(extra) {
this.damage.extra = extra;
return this;
}

/**
* @param {boolean} hrZero
* @return {CheckConfigurer}
Expand Down Expand Up @@ -382,6 +418,7 @@ class CheckInspector {
}

/**
* @returns {TemplateDamageData}
* @remarks Used for templating
*/
getDamageData() {
Expand All @@ -400,6 +437,7 @@ class CheckInspector {
bonus: damage.modifierTotal,
total: damage.total,
type: damage.type,
extra: damage.extra,
},
translation: {
damageTypes: FU.damageTypes,
Expand Down
6 changes: 3 additions & 3 deletions module/checks/checks-v2.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,9 @@ async function prepareCheck(check, actor, item, initialConfigCallback) {
check.modifiers ??= [];
check.additionalData ??= {};
Object.seal(check);
// Set initial targets (actions without rolls can have targeting)
CheckConfiguration.configure(check).setDefaultTargets();
// Initial callback
await (initialConfigCallback ? initialConfigCallback(check, actor, item) : undefined);
Object.defineProperty(check, 'type', {
value: check.type,
Expand All @@ -208,9 +211,6 @@ async function prepareCheck(check, actor, item, initialConfigCallback) {
throw new Error('check id missing');
}

// Set initial targets (actions without rolls can have targeting)
CheckConfiguration.configure(check).setDefaultTargets();

/**
* @type {{callback: Promise | (() => Promise | void), priority: number}[]}
*/
Expand Down
7 changes: 5 additions & 2 deletions module/checks/common-sections.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@ import { ConsumableDataModel } from '../documents/items/consumable/consumable-da
* @param {string} description
* @param {string} summary
* @param {number} [order]
* @param {Boolean} open Defaults to true
*/
const description = (sections, description, summary, order) => {
const description = (sections, description, summary, order, open = true) => {
if (summary || description) {
sections.push(async () => ({
partial: 'systems/projectfu/templates/chat/partials/chat-item-description.hbs',
data: {
summary,
//The open attribute needs to be present without a value to be considered true.
open: open ? 'open' : '',
description: await TextEditor.enrichHTML(description),
},
order,
Expand Down Expand Up @@ -146,7 +149,7 @@ const opportunity = (sections, opportunity, order) => {
* @param {TargetData[]} targets
* @param {Object} flags
* @param accuracyData
* @param damageData
* @param {TemplateDamageData} damageData
*/
const damage = (sections, actor, item, targets, flags, accuracyData, damageData) => {
const isTargeted = targets?.length > 0 || !Targeting.STRICT_TARGETING;
Expand Down
21 changes: 21 additions & 0 deletions module/documents/actors/common/damage-bonuses-data-model.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,27 @@
* @property {number} ice
* @property {number} light
* @property {number} poison
* @property {number} beast
* @property {number} construct
* @property {number} demon
* @property {number} elemental
* @property {number} humanoid
* @property {number} monster
* @property {number} plant
* @property {number} undead
*/
export class DamageBonusesDataModel extends foundry.abstract.DataModel {
static defineSchema() {
const { NumberField } = foundry.data.fields;
return {
all: new NumberField({ initial: 0, integer: true, nullable: false }),

// Category
melee: new NumberField({ initial: 0, integer: true, nullable: false }),
ranged: new NumberField({ initial: 0, integer: true, nullable: false }),
spell: new NumberField({ initial: 0, integer: true, nullable: false }),

// Weapon
arcane: new NumberField({ initial: 0, integer: true, nullable: false }),
bow: new NumberField({ initial: 0, integer: true, nullable: false }),
brawling: new NumberField({ initial: 0, integer: true, nullable: false }),
Expand All @@ -44,6 +54,7 @@ export class DamageBonusesDataModel extends foundry.abstract.DataModel {
sword: new NumberField({ initial: 0, integer: true, nullable: false }),
thrown: new NumberField({ initial: 0, integer: true, nullable: false }),

// Affinity
physical: new NumberField({ initial: 0, integer: true, nullable: false }),
air: new NumberField({ initial: 0, integer: true, nullable: false }),
bolt: new NumberField({ initial: 0, integer: true, nullable: false }),
Expand All @@ -53,6 +64,16 @@ export class DamageBonusesDataModel extends foundry.abstract.DataModel {
ice: new NumberField({ initial: 0, integer: true, nullable: false }),
light: new NumberField({ initial: 0, integer: true, nullable: false }),
poison: new NumberField({ initial: 0, integer: true, nullable: false }),

// Species
beast: new NumberField({ initial: 0, integer: true, nullable: false }),
construct: new NumberField({ initial: 0, integer: true, nullable: false }),
demon: new NumberField({ initial: 0, integer: true, nullable: false }),
elemental: new NumberField({ initial: 0, integer: true, nullable: false }),
humanoid: new NumberField({ initial: 0, integer: true, nullable: false }),
monster: new NumberField({ initial: 0, integer: true, nullable: false }),
plant: new NumberField({ initial: 0, integer: true, nullable: false }),
undead: new NumberField({ initial: 0, integer: true, nullable: false }),
};
}
}
6 changes: 3 additions & 3 deletions module/documents/effects/active-effect.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -177,15 +177,15 @@ export class FUActiveEffect extends ActiveEffect {
* @param {EffectChangeData} change
* @returns {{}|*}
*/
async apply(target, change) {
apply(target, change) {
// Support expressions
if (change.value && typeof change.value === 'string') {
try {
// First, evaluate using built-in support
const expression = Roll.replaceFormulaData(change.value, this.parent);
// Second, evaluate with our custom expressions
const context = ExpressionContext.resolveTarget(target);
const value = await Expressions.evaluate(expression, context);
const context = ExpressionContext.resolveTarget(target, this.parent);
const value = Expressions.evaluate(expression, context);
change.value = String(value ?? 0);
console.debug(`Assigning ${change.key} = ${change.value}`);
} catch (e) {
Expand Down
6 changes: 4 additions & 2 deletions module/documents/items/common/damage-data-model-v2.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { FU } from '../../../helpers/config.mjs';

/**
* @property {boolean} hasDamage
* @property {boolean} hrZero
* @property {number} value
* @property {boolean} hrZero Whether to treat the high roll as zero
* @property {number} value The base value which is generally added to the high roll
* @property {String} extra An expression which is evaluated during damage application
* @property {DamageType} type
*/
export class DamageDataModelV2 extends foundry.abstract.DataModel {
Expand All @@ -13,6 +14,7 @@ export class DamageDataModelV2 extends foundry.abstract.DataModel {
hasDamage: new BooleanField(),
hrZero: new BooleanField(),
value: new NumberField({ initial: 0, integer: true, nullable: false }),
extra: new StringField({ nullable: true }),
type: new StringField({ initial: 'physical', choices: Object.keys(FU.damageTypes), blank: true, nullable: false }),
};
}
Expand Down
9 changes: 8 additions & 1 deletion module/documents/items/item.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,15 @@ const capitalizeFirst = (string) => (typeof string === 'string' ? string.charAt(
*/

/**
* Extend the basic Item with some very simple modifications.
* @typedef Item
* @property {Actor} actor
* @proeprty {String} uuid
*/

/**
* @description Extend the basic Item document with some very simple modifications.
* @extends {Item}
* @inheritDoc
*/
export class FUItem extends Item {
overrides = this.overrides ?? {};
Expand Down
4 changes: 2 additions & 2 deletions module/documents/items/misc/misc-ability-data-model.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ export class MiscAbilityDataModel extends foundry.abstract.TypeDataModel {
label: 'FU.CheckBonus',
value: this.accuracy,
});
check.modifiers.push(...weaponCheck.modifiers);
check.modifiers.push(...weaponCheck.modifiers.filter(({ label }) => label !== 'FU.AccuracyCheckBonusGeneric'));
}

if (this.damage.hasDamage) {
Expand All @@ -218,7 +218,7 @@ export class MiscAbilityDataModel extends foundry.abstract.TypeDataModel {
});
}
if (item.system.useWeapon.damage) {
damageMods.push(...damage.modifiers.filter(({ label }) => label !== 'FU.AccuracyCheckBonusGeneric'));
damageMods.push(...damage.modifiers);
}
return {
type: this.damage.type || damage.type,
Expand Down
Loading

0 comments on commit 6c62792

Please sign in to comment.