mirror of
https://github.com/Foundryborne/daggerheart.git
synced 2026-01-12 03:31:07 +01:00
[Feature] Trigger System (#1500)
* Initial * . * Added StrangePattern trigger * Set command codeblock to expandable * Added automation setting * Added ferocity trigger * Improved StrangePatterns trigger to handle multiple matches
This commit is contained in:
parent
0b343c9f52
commit
454507ba7b
20 changed files with 450 additions and 15 deletions
|
|
@ -84,6 +84,8 @@ Hooks.once('init', () => {
|
||||||
fields
|
fields
|
||||||
};
|
};
|
||||||
|
|
||||||
|
game.system.registeredTriggers = new RegisteredTriggers();
|
||||||
|
|
||||||
const { DocumentSheetConfig } = foundry.applications.apps;
|
const { DocumentSheetConfig } = foundry.applications.apps;
|
||||||
DocumentSheetConfig.unregisterSheet(TokenDocument, 'core', foundry.applications.sheets.TokenConfig);
|
DocumentSheetConfig.unregisterSheet(TokenDocument, 'core', foundry.applications.sheets.TokenConfig);
|
||||||
DocumentSheetConfig.registerSheet(TokenDocument, SYSTEM.id, applications.sheetConfigs.DhTokenConfig, {
|
DocumentSheetConfig.registerSheet(TokenDocument, SYSTEM.id, applications.sheetConfigs.DhTokenConfig, {
|
||||||
|
|
@ -379,3 +381,50 @@ Hooks.on('refreshToken', (_, options) => {
|
||||||
|
|
||||||
Hooks.on('renderCompendiumDirectory', (app, html) => applications.ui.ItemBrowser.injectSidebarButton(html));
|
Hooks.on('renderCompendiumDirectory', (app, html) => applications.ui.ItemBrowser.injectSidebarButton(html));
|
||||||
Hooks.on('renderDocumentDirectory', (app, html) => applications.ui.ItemBrowser.injectSidebarButton(html));
|
Hooks.on('renderDocumentDirectory', (app, html) => applications.ui.ItemBrowser.injectSidebarButton(html));
|
||||||
|
|
||||||
|
class RegisteredTriggers extends Map {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async registerTriggers(trigger, actor, triggeringActorType, uuid, commands) {
|
||||||
|
const existingTrigger = this.get(trigger);
|
||||||
|
if (!existingTrigger) this.set(trigger, new Map());
|
||||||
|
|
||||||
|
this.get(trigger).set(uuid, { actor, triggeringActorType, commands });
|
||||||
|
}
|
||||||
|
|
||||||
|
async runTrigger(trigger, currentActor, ...args) {
|
||||||
|
const updates = [];
|
||||||
|
const triggerSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).triggers;
|
||||||
|
if (!triggerSettings.enabled) return updates;
|
||||||
|
|
||||||
|
const dualityTrigger = this.get(trigger);
|
||||||
|
if (dualityTrigger) {
|
||||||
|
for (let { actor, triggeringActorType, commands } of dualityTrigger.values()) {
|
||||||
|
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) {
|
||||||
|
try {
|
||||||
|
const result = await command(...args);
|
||||||
|
if (result?.updates?.length) updates.push(...result.updates);
|
||||||
|
} catch (_) {
|
||||||
|
const triggerName = game.i18n.localize(triggerData.label);
|
||||||
|
ui.notifications.error(
|
||||||
|
game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerError', {
|
||||||
|
trigger: triggerName,
|
||||||
|
actor: currentActor?.name
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updates;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
42
lang/en.json
42
lang/en.json
|
|
@ -90,7 +90,9 @@
|
||||||
"customFormula": "Custom Formula",
|
"customFormula": "Custom Formula",
|
||||||
"formula": "Formula"
|
"formula": "Formula"
|
||||||
},
|
},
|
||||||
"displayInChat": "Display in chat"
|
"displayInChat": "Display in chat",
|
||||||
|
"deleteTriggerTitle": "Delete Trigger",
|
||||||
|
"deleteTriggerContent": "Are you sure you want to delete the {trigger} trigger?"
|
||||||
},
|
},
|
||||||
"RollField": {
|
"RollField": {
|
||||||
"diceRolling": {
|
"diceRolling": {
|
||||||
|
|
@ -1144,7 +1146,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",
|
||||||
|
|
@ -1218,6 +1221,29 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Triggers": {
|
||||||
|
"postDamageReduction": {
|
||||||
|
"label": "After Damage Reduction"
|
||||||
|
},
|
||||||
|
"preDamageReduction": {
|
||||||
|
"label": "Before Damage Reduction"
|
||||||
|
},
|
||||||
|
"dualityRoll": {
|
||||||
|
"label": "Duality Roll"
|
||||||
|
},
|
||||||
|
"fearRoll": {
|
||||||
|
"label": "Fear Roll"
|
||||||
|
},
|
||||||
|
"triggerTexts": {
|
||||||
|
"strangePatternsContentTitle": "Matched {nr} times.",
|
||||||
|
"strangePatternsContentSubTitle": "Increase hope and stress to a total of {nr}.",
|
||||||
|
"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."
|
||||||
|
},
|
||||||
"WeaponFeature": {
|
"WeaponFeature": {
|
||||||
"barrier": {
|
"barrier": {
|
||||||
"name": "Barrier",
|
"name": "Barrier",
|
||||||
|
|
@ -2058,7 +2084,8 @@
|
||||||
"itemFeatures": "Item Features",
|
"itemFeatures": "Item Features",
|
||||||
"questions": "Questions",
|
"questions": "Questions",
|
||||||
"configuration": "Configuration",
|
"configuration": "Configuration",
|
||||||
"base": "Base"
|
"base": "Base",
|
||||||
|
"triggers": "Triggers"
|
||||||
},
|
},
|
||||||
"Tiers": {
|
"Tiers": {
|
||||||
"singular": "Tier",
|
"singular": "Tier",
|
||||||
|
|
@ -2432,6 +2459,12 @@
|
||||||
"hint": "Automatically apply effects. Targets must be selected before the action is made and Reaction Roll Automation must be different than Never. Bypass users permissions."
|
"hint": "Automatically apply effects. Targets must be selected before the action is made and Reaction Roll Automation must be different than Never. Bypass users permissions."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"triggers": {
|
||||||
|
"enabled": {
|
||||||
|
"label": "Enabled",
|
||||||
|
"hint": "Advanced automation such as triggering a popup for a wizard's Strange Patterns"
|
||||||
|
}
|
||||||
|
},
|
||||||
"summaryMessages": {
|
"summaryMessages": {
|
||||||
"label": "Summary Messages"
|
"label": "Summary Messages"
|
||||||
}
|
}
|
||||||
|
|
@ -2441,6 +2474,9 @@
|
||||||
},
|
},
|
||||||
"roll": {
|
"roll": {
|
||||||
"title": "Actions"
|
"title": "Actions"
|
||||||
|
},
|
||||||
|
"trigger": {
|
||||||
|
"title": "Triggers"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Homebrew": {
|
"Homebrew": {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
|
||||||
|
|
||||||
this.action = action;
|
this.action = action;
|
||||||
this.openSection = null;
|
this.openSection = null;
|
||||||
|
this.openTrigger = this.action.triggers.length > 0 ? 0 : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get title() {
|
get title() {
|
||||||
|
|
@ -15,7 +16,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
|
||||||
|
|
||||||
static DEFAULT_OPTIONS = {
|
static DEFAULT_OPTIONS = {
|
||||||
tag: 'form',
|
tag: 'form',
|
||||||
classes: ['daggerheart', 'dh-style', 'dialog', 'max-800'],
|
classes: ['daggerheart', 'dh-style', 'action-config', 'dialog', 'max-800'],
|
||||||
window: {
|
window: {
|
||||||
icon: 'fa-solid fa-wrench',
|
icon: 'fa-solid fa-wrench',
|
||||||
resizable: false
|
resizable: false
|
||||||
|
|
@ -29,7 +30,10 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
|
||||||
removeElement: this.removeElement,
|
removeElement: this.removeElement,
|
||||||
editEffect: this.editEffect,
|
editEffect: this.editEffect,
|
||||||
addDamage: this.addDamage,
|
addDamage: this.addDamage,
|
||||||
removeDamage: this.removeDamage
|
removeDamage: this.removeDamage,
|
||||||
|
addTrigger: this.addTrigger,
|
||||||
|
removeTrigger: this.removeTrigger,
|
||||||
|
expandTrigger: this.expandTrigger
|
||||||
},
|
},
|
||||||
form: {
|
form: {
|
||||||
handler: this.updateForm,
|
handler: this.updateForm,
|
||||||
|
|
@ -55,6 +59,10 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
|
||||||
effect: {
|
effect: {
|
||||||
id: 'effect',
|
id: 'effect',
|
||||||
template: 'systems/daggerheart/templates/sheets-settings/action-settings/effect.hbs'
|
template: 'systems/daggerheart/templates/sheets-settings/action-settings/effect.hbs'
|
||||||
|
},
|
||||||
|
trigger: {
|
||||||
|
id: 'trigger',
|
||||||
|
template: 'systems/daggerheart/templates/sheets-settings/action-settings/trigger.hbs'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -82,6 +90,14 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
|
||||||
id: 'effect',
|
id: 'effect',
|
||||||
icon: null,
|
icon: null,
|
||||||
label: 'DAGGERHEART.GENERAL.Tabs.effects'
|
label: 'DAGGERHEART.GENERAL.Tabs.effects'
|
||||||
|
},
|
||||||
|
trigger: {
|
||||||
|
active: false,
|
||||||
|
cssClass: '',
|
||||||
|
group: 'primary',
|
||||||
|
id: 'trigger',
|
||||||
|
icon: null,
|
||||||
|
label: 'DAGGERHEART.GENERAL.Tabs.triggers'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -111,6 +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.triggers = context.source.triggers.map((trigger, index) => {
|
||||||
|
const { hint, returns, usesActor } = CONFIG.DH.TRIGGER.triggers[trigger.trigger];
|
||||||
|
return {
|
||||||
|
...trigger,
|
||||||
|
hint,
|
||||||
|
returns,
|
||||||
|
usesActor,
|
||||||
|
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 = [
|
||||||
|
|
@ -224,6 +250,60 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
|
||||||
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
|
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static addTrigger() {
|
||||||
|
const data = this.action.toObject();
|
||||||
|
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) });
|
||||||
|
}
|
||||||
|
|
||||||
|
static async removeTrigger(_event, button) {
|
||||||
|
const trigger = CONFIG.DH.TRIGGER.triggers[this.action.triggers[button.dataset.index].trigger];
|
||||||
|
const confirmed = await foundry.applications.api.DialogV2.confirm({
|
||||||
|
window: {
|
||||||
|
title: game.i18n.localize('DAGGERHEART.ACTIONS.Config.deleteTriggerTitle')
|
||||||
|
},
|
||||||
|
content: game.i18n.format('DAGGERHEART.ACTIONS.Config.deleteTriggerContent', {
|
||||||
|
trigger: game.i18n.localize(trigger.label)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
const data = this.action.toObject();
|
||||||
|
data.triggers = data.triggers.filter((_, index) => index !== Number.parseInt(button.dataset.index));
|
||||||
|
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
|
||||||
|
}
|
||||||
|
|
||||||
|
static async expandTrigger(_event, button) {
|
||||||
|
const index = Number.parseInt(button.dataset.index);
|
||||||
|
const toggle = (element, codeMirror) => {
|
||||||
|
codeMirror.classList.toggle('revealed');
|
||||||
|
const button = element.querySelector('a > i');
|
||||||
|
button.classList.toggle('fa-angle-up');
|
||||||
|
button.classList.toggle('fa-angle-down');
|
||||||
|
};
|
||||||
|
|
||||||
|
const fieldset = button.closest('fieldset');
|
||||||
|
const codeMirror = fieldset.querySelector('.code-mirror-wrapper');
|
||||||
|
toggle(fieldset, codeMirror);
|
||||||
|
|
||||||
|
if (this.openTrigger !== null && this.openTrigger !== index) {
|
||||||
|
const previouslyExpanded = fieldset
|
||||||
|
.closest(`section`)
|
||||||
|
.querySelector(`fieldset[data-index="${this.openTrigger}"]`);
|
||||||
|
const codeMirror = previouslyExpanded.querySelector('.code-mirror-wrapper');
|
||||||
|
toggle(previouslyExpanded, codeMirror);
|
||||||
|
this.openTrigger = index;
|
||||||
|
} else if (this.openTrigger === index) {
|
||||||
|
this.openTrigger = null;
|
||||||
|
} else {
|
||||||
|
this.openTrigger = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Specific implementation in extending classes **/
|
/** Specific implementation in extending classes **/
|
||||||
static async addEffect(_event) {}
|
static async addEffect(_event) {}
|
||||||
static removeEffect(_event, _button) {}
|
static removeEffect(_event, _button) {}
|
||||||
|
|
|
||||||
|
|
@ -10,3 +10,4 @@ export * as itemConfig from './itemConfig.mjs';
|
||||||
export * as settingsConfig from './settingsConfig.mjs';
|
export * as settingsConfig from './settingsConfig.mjs';
|
||||||
export * as systemConfig from './system.mjs';
|
export * as systemConfig from './system.mjs';
|
||||||
export * as itemBrowserConfig from './itemBrowserConfig.mjs';
|
export * as itemBrowserConfig from './itemBrowserConfig.mjs';
|
||||||
|
export * as triggerConfig from './triggerConfig.mjs';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
const hooksConfig = {
|
export const hooksConfig = {
|
||||||
effectDisplayToggle: 'DHEffectDisplayToggle'
|
effectDisplayToggle: 'DHEffectDisplayToggle'
|
||||||
};
|
};
|
||||||
|
|
||||||
export default hooksConfig;
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@ import * as SETTINGS from './settingsConfig.mjs';
|
||||||
import * as EFFECTS from './effectConfig.mjs';
|
import * as EFFECTS from './effectConfig.mjs';
|
||||||
import * as ACTIONS from './actionConfig.mjs';
|
import * as ACTIONS from './actionConfig.mjs';
|
||||||
import * as FLAGS from './flagsConfig.mjs';
|
import * as FLAGS from './flagsConfig.mjs';
|
||||||
import HOOKS from './hooksConfig.mjs';
|
import * as HOOKS from './hooksConfig.mjs';
|
||||||
|
import * as TRIGGER from './triggerConfig.mjs';
|
||||||
import * as ITEMBROWSER from './itemBrowserConfig.mjs';
|
import * as ITEMBROWSER from './itemBrowserConfig.mjs';
|
||||||
|
|
||||||
export const SYSTEM_ID = 'daggerheart';
|
export const SYSTEM_ID = 'daggerheart';
|
||||||
|
|
@ -24,5 +25,6 @@ export const SYSTEM = {
|
||||||
ACTIONS,
|
ACTIONS,
|
||||||
FLAGS,
|
FLAGS,
|
||||||
HOOKS,
|
HOOKS,
|
||||||
|
TRIGGER,
|
||||||
ITEMBROWSER
|
ITEMBROWSER
|
||||||
};
|
};
|
||||||
|
|
|
||||||
42
module/config/triggerConfig.mjs
Normal file
42
module/config/triggerConfig.mjs
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
/* hints and returns are intentionally not translated. They are programatical terms and best understood in english */
|
||||||
|
export const triggers = {
|
||||||
|
dualityRoll: {
|
||||||
|
id: 'dualityRoll',
|
||||||
|
usesActor: true,
|
||||||
|
args: ['roll', 'actor'],
|
||||||
|
label: 'DAGGERHEART.CONFIG.Triggers.dualityRoll.label',
|
||||||
|
hint: 'this: Action, roll: DhRoll, actor: DhActor',
|
||||||
|
returns: '{ updates: [{ key, value, total }] }'
|
||||||
|
},
|
||||||
|
fearRoll: {
|
||||||
|
id: 'fearRoll',
|
||||||
|
usesActor: true,
|
||||||
|
args: ['roll', 'actor'],
|
||||||
|
label: 'DAGGERHEART.CONFIG.Triggers.fearRoll.label',
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -2,6 +2,7 @@ import DhpActor from '../../documents/actor.mjs';
|
||||||
import D20RollDialog from '../../applications/dialogs/d20RollDialog.mjs';
|
import D20RollDialog from '../../applications/dialogs/d20RollDialog.mjs';
|
||||||
import { ActionMixin } from '../fields/actionField.mjs';
|
import { ActionMixin } from '../fields/actionField.mjs';
|
||||||
import { originItemField } from '../chat-message/actorRoll.mjs';
|
import { originItemField } from '../chat-message/actorRoll.mjs';
|
||||||
|
import TriggerField from '../fields/triggerField.mjs';
|
||||||
|
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
|
|
@ -34,7 +35,8 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
||||||
nullable: false,
|
nullable: false,
|
||||||
required: true
|
required: true
|
||||||
}),
|
}),
|
||||||
targetUuid: new fields.StringField({ initial: undefined })
|
targetUuid: new fields.StringField({ initial: undefined }),
|
||||||
|
triggers: new fields.ArrayField(new TriggerField())
|
||||||
};
|
};
|
||||||
|
|
||||||
this.extraSchemas.forEach(s => {
|
this.extraSchemas.forEach(s => {
|
||||||
|
|
@ -343,6 +345,10 @@ export class ResourceUpdateMap extends Map {
|
||||||
}
|
}
|
||||||
|
|
||||||
addResources(resources) {
|
addResources(resources) {
|
||||||
|
if (!resources?.length) return;
|
||||||
|
const invalidResources = resources.some(resource => !resource.key);
|
||||||
|
if (invalidResources) return;
|
||||||
|
|
||||||
for (const resource of resources) {
|
for (const resource of resources) {
|
||||||
if (!resource.key) continue;
|
if (!resource.key) continue;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,5 +2,6 @@ export { ActionCollection } from './actionField.mjs';
|
||||||
export { default as FormulaField } from './formulaField.mjs';
|
export { default as FormulaField } from './formulaField.mjs';
|
||||||
export { default as ForeignDocumentUUIDField } from './foreignDocumentUUIDField.mjs';
|
export { default as ForeignDocumentUUIDField } from './foreignDocumentUUIDField.mjs';
|
||||||
export { default as ForeignDocumentUUIDArrayField } from './foreignDocumentUUIDArrayField.mjs';
|
export { default as ForeignDocumentUUIDArrayField } from './foreignDocumentUUIDArrayField.mjs';
|
||||||
|
export { default as TriggerField } from './triggerField.mjs';
|
||||||
export { default as MappingField } from './mappingField.mjs';
|
export { default as MappingField } from './mappingField.mjs';
|
||||||
export * as ActionFields from './action/_module.mjs';
|
export * as ActionFields from './action/_module.mjs';
|
||||||
|
|
|
||||||
24
module/data/fields/triggerField.mjs
Normal file
24
module/data/fields/triggerField.mjs
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
export default class TriggerField extends foundry.data.fields.SchemaField {
|
||||||
|
constructor(context) {
|
||||||
|
super(
|
||||||
|
{
|
||||||
|
trigger: new foundry.data.fields.StringField({
|
||||||
|
nullable: false,
|
||||||
|
blank: false,
|
||||||
|
initial: CONFIG.DH.TRIGGER.triggers.dualityRoll.id,
|
||||||
|
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 })
|
||||||
|
},
|
||||||
|
context
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
* @property {boolean} isInventoryItem- Indicates whether items of this type is a Inventory Item
|
* @property {boolean} isInventoryItem- Indicates whether items of this type is a Inventory Item
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { addLinkedItemsDiff, createScrollText, getScrollTextData, updateLinkedItemApps } from '../../helpers/utils.mjs';
|
import { addLinkedItemsDiff, getScrollTextData, updateLinkedItemApps } from '../../helpers/utils.mjs';
|
||||||
import { ActionsField } from '../fields/actionField.mjs';
|
import { ActionsField } from '../fields/actionField.mjs';
|
||||||
import FormulaField from '../fields/formulaField.mjs';
|
import FormulaField from '../fields/formulaField.mjs';
|
||||||
|
|
||||||
|
|
@ -135,6 +135,30 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prepareBaseData() {
|
||||||
|
super.prepareBaseData();
|
||||||
|
|
||||||
|
for (const action of this.actions ?? []) {
|
||||||
|
if (!action.actor) continue;
|
||||||
|
|
||||||
|
const actionsToRegister = [];
|
||||||
|
for (let i = 0; i < action.triggers.length; i++) {
|
||||||
|
const trigger = action.triggers[i];
|
||||||
|
const { args } = CONFIG.DH.TRIGGER.triggers[trigger.trigger];
|
||||||
|
const fn = new foundry.utils.AsyncFunction(...args, `{${trigger.command}\n}`);
|
||||||
|
actionsToRegister.push(fn.bind(action));
|
||||||
|
if (i === action.triggers.length - 1)
|
||||||
|
game.system.registeredTriggers.registerTriggers(
|
||||||
|
trigger.trigger,
|
||||||
|
action.actor?.uuid,
|
||||||
|
trigger.triggeringActorType,
|
||||||
|
this.parent.uuid,
|
||||||
|
actionsToRegister
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async _preCreate(data, options, user) {
|
async _preCreate(data, options, user) {
|
||||||
// Skip if no initial action is required or actions already exist
|
// Skip if no initial action is required or actions already exist
|
||||||
if (this.metadata.hasInitialAction && foundry.utils.isEmpty(this.actions)) {
|
if (this.metadata.hasInitialAction && foundry.utils.isEmpty(this.actions)) {
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,13 @@ export default class DhAutomation extends foundry.abstract.DataModel {
|
||||||
label: 'DAGGERHEART.GENERAL.player.plurial'
|
label: 'DAGGERHEART.GENERAL.player.plurial'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
}),
|
||||||
|
triggers: new fields.SchemaField({
|
||||||
|
enabled: new fields.BooleanField({
|
||||||
|
nullable: false,
|
||||||
|
initial: true,
|
||||||
|
label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.triggers.enabled.label'
|
||||||
|
})
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -224,6 +224,30 @@ export default class DualityRoll extends D20Roll {
|
||||||
await super.buildPost(roll, config, message);
|
await super.buildPost(roll, config, message);
|
||||||
|
|
||||||
await DualityRoll.dualityUpdate(config);
|
await DualityRoll.dualityUpdate(config);
|
||||||
|
await DualityRoll.handleTriggers(roll, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async handleTriggers(roll, config) {
|
||||||
|
const updates = [];
|
||||||
|
const dualityUpdates = await game.system.registeredTriggers.runTrigger(
|
||||||
|
CONFIG.DH.TRIGGER.triggers.dualityRoll.id,
|
||||||
|
roll.data?.parent,
|
||||||
|
roll,
|
||||||
|
roll.data?.parent
|
||||||
|
);
|
||||||
|
if (dualityUpdates?.length) updates.push(...dualityUpdates);
|
||||||
|
|
||||||
|
if (config.roll.result.duality === -1) {
|
||||||
|
const fearUpdates = await game.system.registeredTriggers.runTrigger(
|
||||||
|
CONFIG.DH.TRIGGER.triggers.fearRoll.id,
|
||||||
|
roll.data?.parent,
|
||||||
|
roll,
|
||||||
|
roll.data?.parent
|
||||||
|
);
|
||||||
|
if (fearUpdates?.length) updates.push(...fearUpdates);
|
||||||
|
}
|
||||||
|
|
||||||
|
config.resourceUpdates.addResources(updates);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async addDualityResourceUpdates(config) {
|
static async addDualityResourceUpdates(config) {
|
||||||
|
|
|
||||||
|
|
@ -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 =
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,14 @@
|
||||||
},
|
},
|
||||||
"name": "Clear Stress",
|
"name": "Clear Stress",
|
||||||
"img": "icons/magic/symbols/rune-sigil-black-pink.webp",
|
"img": "icons/magic/symbols/rune-sigil-black-pink.webp",
|
||||||
"range": ""
|
"range": "",
|
||||||
|
"triggers": [
|
||||||
|
{
|
||||||
|
"trigger": "dualityRoll",
|
||||||
|
"triggeringActorType": "self",
|
||||||
|
"command": "/* Ignore if it's a TagTeam roll */\nconst tagTeam = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);\nif (tagTeam.members[actor.id]) return;\n\n/* 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 nrMatches = dice.filter(x => x === resource).length;\n\nif (!nrMatches) return;\n\n/* Create a dialog to choose Hope or Stress - or to cancel*/\nconst content = `\n <div><div>${game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerTexts.strangePatternsContentTitle', { nr: nrMatches })}</div>\n <div>${game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerTexts.strangePatternsContentSubTitle', { nr: nrMatches })}</div>\n <div class=\"flexrow\" style=\"gap: 8px;\">\n <button type=\"button\" id=\"hopeButton\">\n <i class=\"fa-solid fa-hands-holding\"></i>\n <label>0</label>\n </button>\n <button type=\"button\" id=\"stressButton\">\n <i class=\"fa-solid fa-bolt-lightning\"></i>\n <label>0</label>\n </button>\n </div>\n</div>`;\n\nconst result = await foundry.applications.api.DialogV2.input({\n classes: ['dh-style', 'two-big-buttons'],\n window: { title: this.item.name },\n content: content,\n render: (_, dialog) => {\n const hopeButton = dialog.element.querySelector('#hopeButton');\n const stressButton = dialog.element.querySelector('#stressButton');\ndialog.element.querySelector('button[type=\"submit\"]').disabled = true;\n \n const updateFunc = (event, selector, adding, clamp) => {\n const button = event.target.closest(`#${selector}Button`);\n const parent = event.target.closest('.flexrow');\n const hope = Number.parseInt(parent.querySelector('#hopeButton label').innerHTML);\n const stress = Number.parseInt(parent.querySelector('#stressButton label').innerHTML);\n const currentTotal = (Number.isNumeric(hope) ? hope : 0) + (Number.isNumeric(stress) ? stress : 0);\n if (adding && currentTotal === nrMatches) return;\n \n const current = Number.parseInt(button.querySelector('label').innerHTML);\n if (!adding && current === 0) return;\n \n const value = Number.isNumeric(current) ? adding ? current+1 : current-1 : 1;\n if (!dialog.data) dialog.data = {};\n dialog.data[selector] = clamp(value);\n button.querySelector('label').innerHTML = dialog.data[selector];\n\n event.target.closest('.dialog-form').querySelector('button[type=\"submit\"]').disabled = !adding || currentTotal < (nrMatches-1);\n \n };\n hopeButton.addEventListener('click', event => updateFunc(event, 'hope', true, x => Math.min(x, nrMatches)));\n hopeButton.addEventListener('contextmenu', event => updateFunc(event, 'hope', false, x => Math.max(x, 0)));\n stressButton.addEventListener('click', event => updateFunc(event, 'stress', true, x => Math.min(x, nrMatches)));\n stressButton.addEventListener('contextmenu', event => updateFunc(event, 'stress', false, x => Math.max(x, 0)));\n },\n ok: { callback: (_event, _result, dialog) => {\n const hope = dialog.data.hope ?? 0;\n const stress = dialog.data.stress ?? 0;\n if (!hope && !stress) return;\n\n /* Return resource update according to choices */\n const hopeUpdate = hope ? { key: 'hope', value: hope, total: -hope, enabled: true } : null;\n const stressUpdate = stress ? { key: 'stress', value: -stress, total: stress, enabled: true } : null;\n return { updates: [hopeUpdate, stressUpdate].filter(x => x) };\n }}\n});\n\nreturn result;"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"originItemType": null,
|
"originItemType": null,
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
55
styles/less/sheets/actions/actions.less
Normal file
55
styles/less/sheets/actions/actions.less
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
.application.daggerheart.dh-style.action-config {
|
||||||
|
.trigger-data {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.trigger-data-inner {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
select {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-section {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.programmer-section {
|
||||||
|
flex: 3;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.hint-section {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
flex: 1;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-trigger {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-mirror-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 0;
|
||||||
|
min-height: 0;
|
||||||
|
transition: height 0.1s ease-in-out;
|
||||||
|
|
||||||
|
&.revealed {
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
@import './actions/actions.less';
|
||||||
|
|
||||||
@import './actors/actor-sheet-shared.less';
|
@import './actors/actor-sheet-shared.less';
|
||||||
|
|
||||||
@import './actors/adversary/actions.less';
|
@import './actors/adversary/actions.less';
|
||||||
|
|
|
||||||
|
|
@ -19,4 +19,15 @@
|
||||||
</div>
|
</div>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>
|
||||||
|
{{localize "DAGGERHEART.SETTINGS.Automation.trigger.title"}}
|
||||||
|
</legend>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
{{formGroup settingFields.schema.fields.triggers.fields.enabled value=settingFields.triggers.enabled localize=true}}
|
||||||
|
<p class="hint">{{localize "DAGGERHEART.SETTINGS.Automation.FIELDS.triggers.enabled.hint"}}</p>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
</section>
|
</section>
|
||||||
37
templates/sheets-settings/action-settings/trigger.hbs
Normal file
37
templates/sheets-settings/action-settings/trigger.hbs
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
<section
|
||||||
|
class="tab {{this.tabs.trigger.cssClass}}"
|
||||||
|
data-group="primary"
|
||||||
|
data-tab="trigger"
|
||||||
|
>
|
||||||
|
<button type="button" data-action="addTrigger">{{localize "Add Trigger"}} <i class="fa-solid fa-plus icon-button"></i></button>
|
||||||
|
|
||||||
|
{{#each @root.triggers as |trigger index|}}
|
||||||
|
<fieldset class="one-column" data-index="{{index}}">
|
||||||
|
<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-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">
|
||||||
|
<strong>{{localize "Context: "}}</strong>
|
||||||
|
<span class="hint">{{localize trigger.hint}}</span>
|
||||||
|
</div>
|
||||||
|
<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 class="code-mirror-wrapper {{#if trigger.revealed}}revealed{{/if}}">
|
||||||
|
{{formInput @root.fields.triggers.element.fields.command value=trigger.command elementType="code-mirror" name=(concat "triggers." index ".command") aria=(object label=(localize "Test")) }}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
{{/each}}
|
||||||
|
</section>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue