Added ferocity trigger

This commit is contained in:
WBHarry 2026-01-05 02:17:29 +01:00
parent ce2e5871bc
commit 4b86bb229a
11 changed files with 165 additions and 42 deletions

View file

@ -386,11 +386,11 @@ class RegisteredTriggers extends Map {
super(); super();
} }
async registerTriggers(trigger, actor, uuid, commands) { async registerTriggers(trigger, actor, triggeringActorType, uuid, commands) {
const existingTrigger = this.get(trigger); const existingTrigger = this.get(trigger);
if (!existingTrigger) this.set(trigger, new Map()); if (!existingTrigger) this.set(trigger, new Map());
this.get(trigger).set(uuid, { actor, commands }); this.get(trigger).set(uuid, { actor, triggeringActorType, commands });
} }
async runTrigger(trigger, currentActor, ...args) { async runTrigger(trigger, currentActor, ...args) {
@ -400,15 +400,19 @@ class RegisteredTriggers extends Map {
const dualityTrigger = this.get(trigger); const dualityTrigger = this.get(trigger);
if (dualityTrigger) { if (dualityTrigger) {
for (let { actor, commands } of dualityTrigger.values()) { for (let { actor, triggeringActorType, commands } of dualityTrigger.values()) {
if (currentActor?.uuid !== actor) continue; const triggerData = CONFIG.DH.TRIGGER.triggers[trigger];
if (triggerData.usesActor && triggeringActorType !== 'any') {
if (triggeringActorType === 'self' && currentActor?.uuid !== actor) continue;
else if (triggeringActorType === 'other' && currentActor?.uuid === actor) continue;
}
for (let command of commands) { for (let command of commands) {
try { try {
const result = await command(...args); const result = await command(...args);
if (result?.updates?.length) updates.push(...result.updates); if (result?.updates?.length) updates.push(...result.updates);
} catch (_) { } catch (_) {
const triggerName = game.i18n.localize(CONFIG.DH.TRIGGER.triggers[trigger].label); const triggerName = game.i18n.localize(triggerData.label);
ui.notifications.error( ui.notifications.error(
game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerError', { game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerError', {
trigger: triggerName, trigger: triggerName,

View file

@ -1145,7 +1145,8 @@
"any": "Any", "any": "Any",
"friendly": "Friendly", "friendly": "Friendly",
"hostile": "Hostile", "hostile": "Hostile",
"self": "Self" "self": "Self",
"other": "Other"
}, },
"TemplateTypes": { "TemplateTypes": {
"circle": "Circle", "circle": "Circle",
@ -1220,17 +1221,25 @@
} }
}, },
"Triggers": { "Triggers": {
"postDamageReduction": {
"label": "After Damage Reduction"
},
"preDamageReduction": {
"label": "Before Damage Reduction"
},
"dualityRoll": { "dualityRoll": {
"label": "Duality Roll", "label": "Duality Roll"
"hint": "this: Action, roll: DhRoll, actor: DhActor"
}, },
"fearRoll": { "fearRoll": {
"label": "Fear Roll", "label": "Fear Roll"
"hint": "this: Action, roll: DhRoll, actor: DhActor"
}, },
"triggerTexts": { "triggerTexts": {
"strangePatternsContent": "Increase Hope or clear stress?" "strangePatternsContent": "Increase Hope or clear stress?",
"ferocityContent": "Spend 2 Hope to gain {bonus} bonus Evasion until after the next attack against you?",
"ferocityEffectDescription": "Your evasion is increased by {bonus}. This bonus lasts until after the next attack made against you."
}, },
"triggerType": "Trigger Type",
"triggeringActorType": "Triggering Actor Type",
"triggerError": "{trigger} trigger failed for {actor}. It's probably configured wrong." "triggerError": "{trigger} trigger failed for {actor}. It's probably configured wrong."
}, },
"WeaponFeature": { "WeaponFeature": {

View file

@ -127,12 +127,16 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
context.baseSaveDifficulty = this.action.actor?.baseSaveDifficulty; context.baseSaveDifficulty = this.action.actor?.baseSaveDifficulty;
context.baseAttackBonus = this.action.actor?.system.attack?.roll.bonus; context.baseAttackBonus = this.action.actor?.system.attack?.roll.bonus;
context.hasRoll = this.action.hasRoll; context.hasRoll = this.action.hasRoll;
context.triggerOptions = CONFIG.DH.TRIGGER.triggers; context.triggers = context.source.triggers.map((trigger, index) => {
context.triggers = context.source.triggers.map((trigger, index) => ({ const { hint, returns, usesActor } = CONFIG.DH.TRIGGER.triggers[trigger.trigger];
return {
...trigger, ...trigger,
hint: CONFIG.DH.TRIGGER.triggers[trigger.trigger].hint, hint,
returns,
usesActor,
revealed: this.openTrigger === index revealed: this.openTrigger === index
})); };
});
const settingsTiers = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers; const settingsTiers = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers;
context.tierOptions = [ context.tierOptions = [
@ -248,7 +252,10 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
static addTrigger() { static addTrigger() {
const data = this.action.toObject(); const data = this.action.toObject();
data.triggers.push({ trigger: CONFIG.DH.TRIGGER.triggers.dualityRoll.id }); data.triggers.push({
trigger: CONFIG.DH.TRIGGER.triggers.dualityRoll.id,
triggeringActor: CONFIG.DH.TRIGGER.triggerActorTargetType.any.id
});
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
} }

View file

@ -1,12 +1,42 @@
/* hints and returns are intentionally not translated. They are programatical terms and best understood in english */
export const triggers = { export const triggers = {
dualityRoll: { dualityRoll: {
id: 'dualityRoll', id: 'dualityRoll',
usesActor: true,
args: ['roll', 'actor'],
label: 'DAGGERHEART.CONFIG.Triggers.dualityRoll.label', label: 'DAGGERHEART.CONFIG.Triggers.dualityRoll.label',
hint: 'DAGGERHEART.CONFIG.Triggers.dualityRoll.hint' hint: 'this: Action, roll: DhRoll, actor: DhActor',
returns: '{ updates: [{ key, value, total }] }'
}, },
fearRoll: { fearRoll: {
id: 'fearRoll', id: 'fearRoll',
usesActor: true,
args: ['roll', 'actor'],
label: 'DAGGERHEART.CONFIG.Triggers.fearRoll.label', label: 'DAGGERHEART.CONFIG.Triggers.fearRoll.label',
hint: 'DAGGERHEART.CONFIG.Triggers.fearRoll.hint' hint: 'this: Action, roll: DhRoll, actor: DhActor',
returns: '{ updates: [{ key, value, total }] }'
},
postDamageReduction: {
id: 'postDamageReduction',
usesActor: true,
args: ['damageUpdates', 'actor'],
label: 'DAGGERHEART.CONFIG.Triggers.postDamageReduction.label',
hint: 'damageUpdates: ResourceUpdates, actor: DhActor',
returns: '{ updates: [{ originActor: this.actor, updates: [{ key, value, total }] }] }'
}
};
export const triggerActorTargetType = {
any: {
id: 'any',
label: 'DAGGERHEART.CONFIG.TargetTypes.any'
},
self: {
id: 'self',
label: 'DAGGERHEART.CONFIG.TargetTypes.self'
},
other: {
id: 'other',
label: 'DAGGERHEART.CONFIG.TargetTypes.other'
} }
}; };

View file

@ -6,7 +6,15 @@ export default class TriggerField extends foundry.data.fields.SchemaField {
nullable: false, nullable: false,
blank: false, blank: false,
initial: CONFIG.DH.TRIGGER.triggers.dualityRoll.id, initial: CONFIG.DH.TRIGGER.triggers.dualityRoll.id,
choices: CONFIG.DH.TRIGGER.triggers choices: CONFIG.DH.TRIGGER.triggers,
label: 'DAGGERHEART.CONFIG.Triggers.triggerType'
}),
triggeringActorType: new foundry.data.fields.StringField({
nullable: false,
blank: false,
initial: CONFIG.DH.TRIGGER.triggerActorTargetType.any.id,
choices: CONFIG.DH.TRIGGER.triggerActorTargetType,
label: 'DAGGERHEART.CONFIG.Triggers.triggeringActorType'
}), }),
command: new foundry.data.fields.JavaScriptField({ async: true }) command: new foundry.data.fields.JavaScriptField({ async: true })
}, },

View file

@ -139,15 +139,19 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
super.prepareBaseData(); super.prepareBaseData();
for (const action of this.actions ?? []) { for (const action of this.actions ?? []) {
if (!action.actor) continue;
const actionsToRegister = []; const actionsToRegister = [];
for (let i = 0; i < action.triggers.length; i++) { for (let i = 0; i < action.triggers.length; i++) {
const trigger = action.triggers[i]; const trigger = action.triggers[i];
const fn = new foundry.utils.AsyncFunction('roll', 'actor', `{${trigger.command}\n}`); const { args } = CONFIG.DH.TRIGGER.triggers[trigger.trigger];
const fn = new foundry.utils.AsyncFunction(...args, `{${trigger.command}\n}`);
actionsToRegister.push(fn.bind(action)); actionsToRegister.push(fn.bind(action));
if (i === action.triggers.length - 1) if (i === action.triggers.length - 1)
game.system.registeredTriggers.registerTriggers( game.system.registeredTriggers.registerTriggers(
trigger.trigger, trigger.trigger,
action.actor?.uuid, action.actor?.uuid,
trigger.triggeringActorType,
this.parent.uuid, this.parent.uuid,
actionsToRegister actionsToRegister
); );

View file

@ -646,6 +646,19 @@ export default class DhpActor extends Actor {
} }
} }
const results = await game.system.registeredTriggers.runTrigger(
CONFIG.DH.TRIGGER.triggers.postDamageReduction.id,
this,
updates,
this
);
if (results?.length) {
const resourceMap = new ResourceUpdateMap(results[0].originActor);
for (var result of results) resourceMap.addResources(result.updates);
resourceMap.updateResources();
}
updates.forEach( updates.forEach(
u => u =>
(u.value = (u.value =

View file

@ -84,6 +84,7 @@
"triggers": [ "triggers": [
{ {
"trigger": "dualityRoll", "trigger": "dualityRoll",
"triggeringActorType": "self",
"command": "/* Check if there's a Strange Pattern match */\nconst dice = [roll.dFear.total, roll.dHope.total];\nconst resource = this.parent.resource?.diceStates ? Object.values(this.parent.resource.diceStates).map(x => x.value)[0] : null;\nconst diceMatch = dice.includes(resource);\n\nif (!diceMatch) return;\n\n/* Create a dialog to choose Hope or Stress - or to cancel*/\nconst choice = await foundry.applications.api.DialogV2.wait({\n classes: ['dh-style', 'two-big-buttons'],\n window: { title: this.item.name },\n content: game.i18n.localize('DAGGERHEART.CONFIG.Triggers.triggerTexts.strangePatternsContent'),\n buttons: [\n {\n action: 'hope',\n label: game.i18n.localize('DAGGERHEART.GENERAL.hope'),\n icon: 'fa-solid fa-hands-holding'\n },\n {\n action: 'stress',\n label: game.i18n.localize('DAGGERHEART.GENERAL.stress'),\n icon: 'fa-solid fa-bolt-lightning'\n }\n ]\n});\n \nif (!choice) return;\n\n/* Return resource update according to choice */\nconst isHope = choice === 'hope';\nconst updates = [{ key: choice, value: isHope ? 1 : -1, total: isHope ? -1 : 1, enabled: true }];\nreturn { updates };" "command": "/* Check if there's a Strange Pattern match */\nconst dice = [roll.dFear.total, roll.dHope.total];\nconst resource = this.parent.resource?.diceStates ? Object.values(this.parent.resource.diceStates).map(x => x.value)[0] : null;\nconst diceMatch = dice.includes(resource);\n\nif (!diceMatch) return;\n\n/* Create a dialog to choose Hope or Stress - or to cancel*/\nconst choice = await foundry.applications.api.DialogV2.wait({\n classes: ['dh-style', 'two-big-buttons'],\n window: { title: this.item.name },\n content: game.i18n.localize('DAGGERHEART.CONFIG.Triggers.triggerTexts.strangePatternsContent'),\n buttons: [\n {\n action: 'hope',\n label: game.i18n.localize('DAGGERHEART.GENERAL.hope'),\n icon: 'fa-solid fa-hands-holding'\n },\n {\n action: 'stress',\n label: game.i18n.localize('DAGGERHEART.GENERAL.stress'),\n icon: 'fa-solid fa-bolt-lightning'\n }\n ]\n});\n \nif (!choice) return;\n\n/* Return resource update according to choice */\nconst isHope = choice === 'hope';\nconst updates = [{ key: choice, value: isHope ? 1 : -1, total: isHope ? -1 : 1, enabled: true }];\nreturn { updates };"
} }
] ]

View file

@ -17,7 +17,16 @@
"description": "<p class=\"Body-Foundation\">When you cause an adversary to mark 1 or more Hit Points, you can <strong>spend 2 Hope</strong> to increase your Evasion by the number of Hit Points they marked. This bonus lasts until after the next attack made against you.</p>", "description": "<p class=\"Body-Foundation\">When you cause an adversary to mark 1 or more Hit Points, you can <strong>spend 2 Hope</strong> to increase your Evasion by the number of Hit Points they marked. This bonus lasts until after the next attack made against you.</p>",
"chatDisplay": true, "chatDisplay": true,
"actionType": "action", "actionType": "action",
"cost": [], "cost": [
{
"scalable": false,
"key": "hope",
"value": 2,
"itemId": null,
"step": null,
"consumeOnSuccess": false
}
],
"uses": { "uses": {
"value": null, "value": null,
"max": "", "max": "",
@ -30,8 +39,15 @@
"amount": null "amount": null
}, },
"name": "Spend Hope", "name": "Spend Hope",
"img": "icons/skills/melee/maneuver-daggers-paired-orange.webp", "img": "icons/skills/melee/maneuver-sword-katana-yellow.webp",
"range": "" "range": "",
"triggers": [
{
"trigger": "postDamageReduction",
"triggeringActorType": "other",
"command": "/* Check if sufficient hope */\nif (this.actor.system.resources.hope.value < 2) return;\n\n/* Check if hit point damage was dealt */\nconst hpDamage = damageUpdates.find(u => u.key === CONFIG.DH.GENERAL.healingTypes.hitPoints.id);\nif (hpDamage.value < 0) return;\n\n/* Dialog to give player choice */\nconst confirmed = await foundry.applications.api.DialogV2.confirm({\n window: { title: this.item?.name ?? '' },\n content: game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerTexts.ferocityContent', { bonus: hpDamage.value }),\n});\n\nif (!confirmed) return;\n\n/* Create the effect */\nthis.actor.createEmbeddedDocuments('ActiveEffect', [{\n name: this.item.name,\n img: 'icons/skills/melee/maneuver-sword-katana-yellow.webp',\n description: game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerTexts.ferocityEffectDescription', { bonus: hpDamage.value }),\n changes: [{ key: 'system.evasion', mode: 2, value: hpDamage.value }]\n}]);\n\n/* Update hope */\nreturn { updates: [{ \n originActor: this.actor, \n updates: [{\n key: CONFIG.DH.GENERAL.healingTypes.hope.id,\n value: -2,\n total: 2\n }] \n}]}"
}
]
} }
}, },
"attribution": { "attribution": {

View file

@ -2,17 +2,31 @@
.trigger-data { .trigger-data {
width: 100%; width: 100%;
display: flex; display: flex;
align-items: center; justify-content: space-between;
gap: 8px; gap: 8px;
.trigger-data-inner {
flex: 1;
display: flex;
flex-direction: column;
select { select {
flex: 1; flex: 1;
} }
.hint-section { .select-section {
flex: 1;
display: flex;
gap: 8px;
}
.programmer-section {
flex: 3; flex: 3;
display: flex; display: flex;
align-items: center; flex-direction: column;
.hint-section {
display: flex;
gap: 4px; gap: 4px;
.hint { .hint {
@ -21,6 +35,12 @@
} }
} }
} }
}
.expand-trigger {
font-size: 18px;
}
}
.code-mirror-wrapper { .code-mirror-wrapper {
width: 100%; width: 100%;

View file

@ -10,12 +10,23 @@
<legend><a data-action="removeTrigger" data-index="{{index}}"><i class="fa-solid fa-trash"></i></a></legend> <legend><a data-action="removeTrigger" data-index="{{index}}"><i class="fa-solid fa-trash"></i></a></legend>
<div class="trigger-data"> <div class="trigger-data">
{{formInput @root.fields.triggers.element.fields.trigger value=trigger.trigger name=(concat "triggers." index ".trigger") blank=false localize=true}} <div class="trigger-data-inner">
<div class="select-section">
{{formGroup @root.fields.triggers.element.fields.trigger value=trigger.trigger name=(concat "triggers." index ".trigger") blank=false localize=true}}
{{#if trigger.usesActor}}{{formGroup @root.fields.triggers.element.fields.triggeringActorType value=trigger.triggeringActorType name=(concat "triggers." index ".triggeringActorType") blank=false localize=true}}{{/if}}
</div>
<div class="programmer-section">
<div class="hint-section"> <div class="hint-section">
<strong>{{localize "Context: "}}</strong> <strong>{{localize "Context: "}}</strong>
<span class="hint">{{localize trigger.hint}}</span> <span class="hint">{{localize trigger.hint}}</span>
</div> </div>
<a data-action="expandTrigger" data-index="{{index}}"><i class="fa-solid fa-angle-down"></i></a> <div class="hint-section">
<strong>{{localize "Return: "}}</strong>
<span class="hint">{{localize trigger.returns}}</span>
</div>
</div>
</div>
<a class="expand-trigger" data-action="expandTrigger" data-index="{{index}}"><i class="fa-solid fa-angle-down"></i></a>
</div> </div>
<div class="code-mirror-wrapper {{#if trigger.revealed}}revealed{{/if}}"> <div class="code-mirror-wrapper {{#if trigger.revealed}}revealed{{/if}}">