Skip to content

Commit

Permalink
Add support for custom expressions in active effects (#193)
Browse files Browse the repository at this point in the history
- Provides support for evaluating expressions using our custom DSL within active effect values. This allows us to easily automate skills like defensive mastery.
- Standardizes adding the damage and accuracy bonuses with the `CheckConfigurer` API across the various data models (mostly)
- Add a button to revert damage on damage message
  • Loading branch information
Azurelol authored Jan 9, 2025
1 parent 948f362 commit d3166b5
Show file tree
Hide file tree
Showing 22 changed files with 786 additions and 605 deletions.
4 changes: 3 additions & 1 deletion lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@
"Normal": "Normal",
"Hard": "Hard",
"VeryHard": "Very Hard",
"InlineRollCheck": "Roll an attribute check on the selected actors",
"InlineRollCheck": "Roll an attribute check on the selected actors. Hold SHIFT to configure the check beforehand.",
"InlineDamage": "Apply damage to selected token",
"InlineRecovery": "Apply resource recovery to selected token",
"InlineLoss": "Apply resource loss to selected tokens",
Expand Down Expand Up @@ -851,6 +851,7 @@
"ChatApplyDamageImmune": "<strong>{actor}</strong> was <strong>immune</strong> to <strong>{type}</strong> and took <strong>{damage}</strong> damage from <strong>{from}</strong>",
"ChatApplyDamageImmuneIgnored": "<strong>{actor}</strong> took <strong>{damage} {type}</strong> damage from <strong>{from} (Immunity ignored)</strong>",
"ChatApplyDamageAbsorb": "<strong>{actor} absorbed {type}</strong> damage from <strong>{from}</strong> and was healed by <strong>{damage}</strong> HP",
"ChatRevertDamage": "Revert applied damage",
"ChatEvaluateAmountNoActor": "No reference to an actor provided",
"ChatEvaluateAmountNoItem": "No reference to an item provided",
"ChatEvaluateNoSkill": "The referenced skill was not found in the actor",
Expand Down Expand Up @@ -1175,6 +1176,7 @@
"CheckPushModifier": "Bond Strength Bonus",
"MagicCheckBaseAccuracy": "Base Magic Accuracy",
"MagicCheckBonusGeneric": "Generic Magic Check Bonus",
"DamageBonusAll": "Global Damage Bonus",
"DamageBonusTypeSpell": "Spell Damage Bonus",
"DamageBonusTypeMelee": "Melee Damage Bonus",
"DamageBonusTypeRanged": "Ranged Damage Bonus",
Expand Down
2 changes: 2 additions & 0 deletions module/checks/accuracy-check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ function onRenderCheck(data, checkResult, actor, item, flags) {
modifiers: damage.modifiers,
};
}
const applyDamage = damageData != null;

// Push combined data for accuracy and damage
data.push({
Expand All @@ -166,6 +167,7 @@ function onRenderCheck(data, checkResult, actor, item, flags) {
partial: isTargeted ? 'systems/projectfu/templates/chat/partials/chat-check-targets.hbs' : 'systems/projectfu/templates/chat/partials/chat-check-notargets.hbs',
data: {
targets: targets,
applyDamage: applyDamage,
},
});
}
Expand Down
101 changes: 101 additions & 0 deletions module/checks/check-configuration.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ class CheckConfigurer {
this.#check = check;
}

/**
* @param {Attribute} primary
* @param {Attribute} secondary
*/
setAttributes(primary, secondary) {
this.#check.primary = primary;
this.#check.secondary = secondary;
}

/**
* @param {DamageType} type
* @param {number} baseDamage
Expand All @@ -76,6 +85,98 @@ class CheckConfigurer {
return this;
}

/**
* @param {FUItem} item
* @param {FUActor} actor
* @return {CheckConfigurer}
*/
addItemAccuracyBonuses(item, actor) {
return this.addModelAccuracyBonuses(item.system, actor);
}

/**
* @param {DataModel} model
* @param {FUActor} actor
* @return {CheckConfigurer}
*/
addModelAccuracyBonuses(model, actor) {
// Wewapon Category
const category = model.category?.value;
if (category && actor.system.bonuses.accuracy[category]) {
this.#check.modifiers.push({
label: `FU.AccuracyCheckBonus${category.capitalize()}`,
value: actor.system.bonuses.accuracy[category],
});
}
// Attack Type
const attackType = model.type?.value;
if (attackType === 'melee' && actor.system.bonuses.accuracy.accuracyMelee) {
this.#check.modifiers.push({
label: 'FU.AccuracyCheckBonusMelee',
value: actor.system.bonuses.accuracy.accuracyMelee,
});
} else if (attackType === 'ranged' && actor.system.bonuses.accuracy.accuracyRanged) {
this.#check.modifiers.push({
label: 'FU.AccuracyCheckBonusRanged',
value: actor.system.bonuses.accuracy.accuracyRanged,
});
}
return this;
}

/**
* @param {String} label
* @param {Number} value
*/
addModifier(label, value) {
this.#check.modifiers.push({
label: label,
value: value,
});
}

/**
* @param {FUActor} actor
* @param {FUItem} item
* @return {CheckConfigurer}
*/
addItemDamageBonuses(item, actor) {
return this.addModelDamageBonuses(item.system, actor);
}

/**
* @param {DataModel} model
* @param {FUActor} actor
* @return {CheckConfigurer}
*/
addModelDamageBonuses(model, actor) {
// All Damage
const globalBonus = actor.system.bonuses.damage.all;
if (globalBonus) {
this.addDamageBonus(`FU.DamageBonusAll`, globalBonus);
}
// Damage Type
if (model.damageType) {
const damageTypeBonus = actor.system.bonuses.damage[model.damageType.value];
if (damageTypeBonus) {
this.addDamageBonus(`FU.DamageBonus${model.damageType.value}`, damageTypeBonus);
}
}
// Attack Type
const attackTypeBonus = actor.system.bonuses.damage[model.type.value] ?? 0;
if (attackTypeBonus) {
this.addDamageBonus(`FU.DamageBonusType${model.type.value.capitalize()}`, attackTypeBonus);
}
// Weapon Category
if (model.category) {
const weaponCategoryBonus = actor.system.bonuses.damage[model.category.value] ?? 0;
if (weaponCategoryBonus) {
this.addDamageBonus(`FU.DamageBonusCategory${model.category.value.capitalize()}`, weaponCategoryBonus);
}
}
return this;
}

/**
* @param {(damage: DamageData | null) => DamageData | null} callback
* @return {CheckConfigurer}
Expand Down
57 changes: 57 additions & 0 deletions module/checks/checks-v2.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ import { SupportCheck } from './support-check.mjs';
* @property {Attribute} secondary
*/

/**
* @typedef {Object} CheckConfigurationPromptData
* @property {Attribute} primary
* @property {Attribute} secondary
* @property {Number} modifier
* @property {Number} difficulty
*/

/**
* @param {FUActor} actor
* @param {FUItem} item
Expand Down Expand Up @@ -511,6 +519,54 @@ const isCheck = (message, type = allExceptDisplay) => {
return false;
};

/**
* @param {FUActor} actor
* @param {CheckConfigurationPromptData} check
* @param {String} title
* @returns {Promise<CheckConfigurationPromptData>}
*/
async function promptConfiguration(actor, check, title) {
title = title || 'FU.DialogCheckTitle';
const attributes = actor.system.attributes;
return await Dialog.wait(
{
title: game.i18n.localize(title),
content: await renderTemplate('systems/projectfu/templates/dialog/dialog-check.hbs', {
attributes: FU.attributes,
attributeAbbr: FU.attributeAbbreviations,
attributeValues: Object.entries(attributes).reduce(
(previousValue, [attribute, { current }]) => ({
...previousValue,
[attribute]: current,
}),
{},
),
primary: check.primary || 'mig',
secondary: check.secondary || 'mig',
modifier: check.modifier || 0,
difficulty: check.difficulty || 0,
}),
buttons: [
{
icon: '<i class="fas fa-dice"></i>',
label: game.i18n.localize('FU.DialogCheckRoll'),
callback: (jQuery) => {
return {
primary: jQuery.find('*[name=primary]:checked').val(),
secondary: jQuery.find('*[name=secondary]:checked').val(),
modifier: +jQuery.find('*[name=modifier]').val(),
difficulty: +jQuery.find('*[name=difficulty]').val(),
};
},
},
],
},
{
classes: ['projectfu', 'unique-dialog', 'backgroundstyle'],
},
);
}

document.addEventListener('click', (event) => {
const toggleLink = event.target.closest('.universal-toggle');
if (!toggleLink) return;
Expand Down Expand Up @@ -560,6 +616,7 @@ export const ChecksV2 = Object.freeze({
supportCheck,
modifyCheck,
isCheck,
promptConfiguration,
});

CheckRetarget.initialize();
Expand Down
9 changes: 7 additions & 2 deletions module/documents/effects/active-effect.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { FUActor } from '../actors/actor.mjs';
import { FUItem } from '../items/item.mjs';
import { SYSTEM } from '../../helpers/config.mjs';
import { ExpressionContext, Expressions } from '../../expressions/expressions.mjs';

const CRISIS_INTERACTION = 'CrisisInteraction';
const EFFECT_TYPE = 'type';
Expand Down Expand Up @@ -144,11 +145,15 @@ export class FUActiveEffect extends ActiveEffect {
}

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);
const value = Roll.validate(expression) ? Roll.safeEval(expression) : change.value;
console.debug('Substituting change variable:', change.value, value);
// Second, evaluate with our custom expressions
const context = ExpressionContext.resolveTarget(target);
const value = Expressions.evaluate(expression, context);
console.debug('Substituting active effect change variable:', change.value, value);
change.value = String(value ?? 0);
} catch (e) {
console.error(e);
Expand Down
5 changes: 3 additions & 2 deletions module/documents/items/basic/basic-item-data-model.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ const prepareCheck = (check, actor, item, registerCallback) => {
label: 'FU.AccuracyCheckBaseAccuracy',
value: item.system.accuracy.value,
});
const attackTypeBonus = actor.system.bonuses.damage[item.system.type.value] ?? 0;
AccuracyCheck.configure(check)
.setDamage(item.system.damageType.value, item.system.damage.value + attackTypeBonus)
.setDamage(item.system.damageType.value, item.system.damage.value)
.setTargetedDefense(item.system.defense)
.addItemAccuracyBonuses(item, actor)
.addItemDamageBonuses(item, actor)
.modifyHrZero((hrZero) => hrZero || item.system.rollInfo.useWeapon.hrZero.value);
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,40 +24,22 @@ const weaponModuleTypes = {
const prepareCheck = (check, actor, item, registerCallback) => {
if (check.type === 'accuracy' && item.system instanceof ClassFeatureTypeDataModel && item.system.data instanceof WeaponModuleDataModel) {
/** @type WeaponModuleDataModel */
const data = item.system.data;
check.primary = data.accuracy.attr1;
check.secondary = data.accuracy.attr2;
const baseAccuracy = data.accuracy.modifier;
const module = item.system.data;
check.primary = module.accuracy.attr1;
check.secondary = module.accuracy.attr2;
const baseAccuracy = module.accuracy.modifier;
if (baseAccuracy) {
check.modifiers.push({
label: 'FU.AccuracyCheckBaseAccuracy',
value: baseAccuracy,
});
}
const category = data.category;
if (category && actor.system.bonuses.accuracy[category]) {
check.modifiers.push({
label: `FU.AccuracyCheckBonus${category.capitalize()}`,
value: actor.system.bonuses.accuracy[category],
});
}

const attackType = data.type;
if (attackType === 'melee' && actor.system.bonuses.accuracy.accuracyMelee) {
check.modifiers.push({
label: 'FU.AccuracyCheckBonusMelee',
value: actor.system.bonuses.accuracy.accuracyMelee,
});
} else if (attackType === 'ranged' && actor.system.bonuses.accuracy.accuracyRanged) {
check.modifiers.push({
label: 'FU.AccuracyCheckBonusRanged',
value: actor.system.bonuses.accuracy.accuracyRanged,
});
}
const configurer = AccuracyCheck.configure(check).setDamage(module.damage.type, module.damage.bonus).addModelAccuracyBonuses(module, actor);

const configurer = AccuracyCheck.configure(check).setDamage(data.damage.type, data.damage.bonus);
const category = module.category;

const attackTypeBonus = actor.system.bonuses.damage[data.type] ?? 0;
const attackTypeBonus = actor.system.bonuses.damage[module.type] ?? 0;
if (attackTypeBonus) {
configurer.addDamageBonus(`FU.DamageBonusType${item.system.type.value.capitalize()}`, attackTypeBonus);
}
Expand Down
13 changes: 3 additions & 10 deletions module/documents/items/shield/shield-data-model.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,12 @@ const prepareCheck = (check, actor, item, registerCallback) => {
value: item.system.accuracy.value,
});

const configurer = AccuracyCheck.configure(check)
AccuracyCheck.configure(check)
.setDamage(item.system.damageType.value, item.system.damage.value)
.setTargetedDefense(item.system.defense)
.addItemDamageBonuses(item, actor)
.addItemAccuracyBonuses(item, actor)
.modifyHrZero((hrZero) => hrZero || item.system.rollInfo.useWeapon.hrZero.value);

const attackTypeBonus = actor.system.bonuses.damage[item.system.type.value] ?? 0;
if (attackTypeBonus) {
configurer.addDamageBonus(`FU.DamageBonusType${item.system.type.value.capitalize()}`, attackTypeBonus);
}
const weaponCategoryBonus = actor.system.bonuses.damage[item.system.category.value] ?? 0;
if (weaponCategoryBonus) {
configurer.addDamageBonus(`FU.DamageBonusCategory${item.system.category.value.capitalize()}`, weaponCategoryBonus);
}
}
};

Expand Down
22 changes: 4 additions & 18 deletions module/documents/items/skill/skill-data-model.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -44,31 +44,17 @@ const onPrepareAccuracyCheck = (check, actor, item, registerCallback) => {
if (skillData.rollInfo.useWeapon.damage.value) {
configurer
.setDamage(weaponData.damageType.value, weaponData.damage.value)
.addItemAccuracyBonuses(weapon, actor)
.addItemDamageBonuses(weapon, actor)
.modifyHrZero((hrZero) => hrZero || skillData.rollInfo.useWeapon.hrZero.value)
.setTargetedDefense(weaponData.defense);

const { [weaponData.type.value]: weaponTypeDamageBonus, [weaponData.category.value]: weaponCategoryDamageBonus } = actor.system.bonuses.damage;

if (weaponTypeDamageBonus) {
configurer.addDamageBonus(`FU.DamageBonusType${weaponData.type.value.capitalize()}`, weaponTypeDamageBonus);
}
if (weaponCategoryDamageBonus) {
configurer.addDamageBonus(`FU.DamageBonusCategory${weaponData.category.value.capitalize()}`, weaponCategoryDamageBonus);
}
} else {
configurer
.setDamage(skillData.rollInfo.damage.type.value, skillData.rollInfo.damage.value)
.addItemDamageBonuses(weapon, actor)
.addItemAccuracyBonuses(weapon, actor)
.modifyHrZero((hrZero) => hrZero || skillData.rollInfo.useWeapon.hrZero.value)
.setTargetedDefense(weaponData.defense);

const { [weaponData.type.value]: weaponTypeDamageBonus, [weaponData.category.value]: weaponCategoryDamageBonus } = actor.system.bonuses.damage;

if (weaponTypeDamageBonus) {
configurer.addDamageBonus(`FU.DamageBonusType${weaponData.type.value.capitalize()}`, weaponTypeDamageBonus);
}
if (weaponCategoryDamageBonus) {
configurer.addDamageBonus(`FU.DamageBonusCategory${weaponData.category.value.capitalize()}`, weaponCategoryDamageBonus);
}
}
}
} else {
Expand Down
Loading

0 comments on commit d3166b5

Please sign in to comment.