diff --git a/daggerheart.mjs b/daggerheart.mjs index f4dba5a3..ae1502b0 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -80,6 +80,7 @@ CONFIG.ux.ContextMenu = applications.ux.DHContextMenu; CONFIG.ux.TooltipManager = documents.DhTooltipManager; CONFIG.ux.TemplateManager = new TemplateManager(); CONFIG.ux.TokenManager = new TokenManager(); +CONFIG.debug.triggers = false; Hooks.once('init', () => { game.system.api = { @@ -91,7 +92,7 @@ Hooks.once('init', () => { fields }; - game.system.registeredTriggers = new RegisteredTriggers(); + game.system.registeredTriggers = new game.system.api.data.RegisteredTriggers(); const { DocumentSheetConfig } = foundry.applications.apps; DocumentSheetConfig.unregisterSheet(TokenDocument, 'core', foundry.applications.sheets.TokenConfig); @@ -422,49 +423,12 @@ Hooks.on('refreshToken', (_, options) => { Hooks.on('renderCompendiumDirectory', (app, html) => applications.ui.ItemBrowser.injectSidebarButton(html)); Hooks.on('renderDocumentDirectory', (app, html) => applications.ui.ItemBrowser.injectSidebarButton(html)); -class RegisteredTriggers extends Map { - constructor() { - super(); - } +/* Non actor-linked Actors should unregister the triggers of their tokens if a scene's token layer is torn down */ +Hooks.on('canvasTearDown', canvas => { + game.system.registeredTriggers.unregisterSceneTriggers(canvas.scene); +}); - 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; - } -} +/* Non actor-linked Actors should register the triggers of their tokens on a readied scene */ +Hooks.on('canvasReady', canas => { + game.system.registeredTriggers.registerSceneTriggers(canvas.scene); +}); diff --git a/lang/en.json b/lang/en.json index c9160405..6b055be0 100755 --- a/lang/en.json +++ b/lang/en.json @@ -2090,6 +2090,7 @@ "tier4": "tier 4", "domains": "Domains", "downtime": "Downtime", + "itemFeatures": "Item Features", "roll": "Roll", "rules": "Rules", "partyMembers": "Party Members", @@ -2729,6 +2730,9 @@ "rerollDamage": "Reroll Damage", "assignTagRoll": "Assign as Tag Roll" }, + "ConsoleLogs": { + "triggerRun": "DH TRIGGER | Item '{item}' on actor '{actor}' ran a '{trigger}' trigger." + }, "Countdowns": { "title": "Countdowns", "toggleIconMode": "Toggle Icon Only", diff --git a/module/applications/dialogs/d20RollDialog.mjs b/module/applications/dialogs/d20RollDialog.mjs index d8306923..441842dc 100644 --- a/module/applications/dialogs/d20RollDialog.mjs +++ b/module/applications/dialogs/d20RollDialog.mjs @@ -123,7 +123,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio context.formula = this.roll.constructFormula(this.config); if (this.actor?.system?.traits) context.abilities = this.getTraitModifiers(); - context.showReaction = !this.config.roll?.type && context.rollType === 'DualityRoll'; + context.showReaction = !this.config.roll?.type || context.rollType === 'DualityRoll'; context.reactionOverride = this.reactionOverride; } diff --git a/module/applications/sidebar/tabs/daggerheartMenu.mjs b/module/applications/sidebar/tabs/daggerheartMenu.mjs index 6c7a9df1..b29437bf 100644 --- a/module/applications/sidebar/tabs/daggerheartMenu.mjs +++ b/module/applications/sidebar/tabs/daggerheartMenu.mjs @@ -25,7 +25,7 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract /** @override */ static DEFAULT_OPTIONS = { - classes: ['dh-style'], + classes: ['dh-style', 'directory'], window: { title: 'SIDEBAR.TabSettings' }, diff --git a/module/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs index febb6155..b4aa246a 100644 --- a/module/applications/ui/chatLog.mjs +++ b/module/applications/ui/chatLog.mjs @@ -96,6 +96,19 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo super.close(options); } + /** Ensure the chat theme inherits the interface theme */ + _replaceHTML(result, content, options) { + const themedElement = result.log?.querySelector(".chat-log"); + themedElement?.classList.remove("themed", "theme-light", "theme-dark"); + super._replaceHTML(result, content, options); + } + + /** Remove chat log theme from notifications area */ + async _onFirstRender(result, content) { + await super._onFirstRender(result, content); + document.querySelector("#chat-notifications .chat-log")?.classList.remove("themed", "theme-light", "theme-dark") + } + async onRollSimple(event, message) { const buttonType = event.target.dataset.type ?? 'damage', total = message.rolls.reduce((a, c) => a + Roll.fromJSON(c).total, 0), diff --git a/module/data/_module.mjs b/module/data/_module.mjs index 0a476ee9..7ad20808 100644 --- a/module/data/_module.mjs +++ b/module/data/_module.mjs @@ -1,6 +1,7 @@ export { default as DhCombat } from './combat.mjs'; export { default as DhCombatant } from './combatant.mjs'; export { default as DhTagTeamRoll } from './tagTeamRoll.mjs'; +export { default as RegisteredTriggers } from './registeredTriggers.mjs'; export * as countdowns from './countdowns.mjs'; export * as actions from './action/_module.mjs'; diff --git a/module/data/item/base.mjs b/module/data/item/base.mjs index 415fc8d4..0c9fdabe 100644 --- a/module/data/item/base.mjs +++ b/module/data/item/base.mjs @@ -164,26 +164,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { 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 - ); - } - } + game.system.registeredTriggers.registerItemTriggers(this.parent); } async _preCreate(data, options, user) { @@ -246,6 +227,28 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { const armorData = getScrollTextData(this.parent.parent.system.resources, changed.system.marks, 'armor'); options.scrollingTextData = [armorData]; } + + if (changed.system?.actions) { + const triggersToRemove = Object.keys(changed.system.actions).reduce((acc, key) => { + if (!changed.system.actions[key]) { + const strippedKey = key.replace('-=', ''); + acc.push(...this.actions.get(strippedKey).triggers.map(x => x.trigger)); + } + + return acc; + }, []); + + game.system.registeredTriggers.unregisterTriggers(triggersToRemove, this.parent.uuid); + + if (!(this.parent.parent.token instanceof game.system.api.documents.DhToken)) { + for (const token of this.parent.parent.getActiveTokens()) { + game.system.registeredTriggers.unregisterTriggers( + triggersToRemove, + `${token.document.uuid}.${this.parent.uuid}` + ); + } + } + } } _onUpdate(changed, options, userId) { diff --git a/module/data/registeredTriggers.mjs b/module/data/registeredTriggers.mjs new file mode 100644 index 00000000..fe962c5e --- /dev/null +++ b/module/data/registeredTriggers.mjs @@ -0,0 +1,154 @@ +export default class RegisteredTriggers extends Map { + constructor() { + super(); + } + + registerTriggers(triggers, actor, uuid) { + for (const triggerKey of Object.keys(CONFIG.DH.TRIGGER.triggers)) { + const match = triggers[triggerKey]; + const existingTrigger = this.get(triggerKey); + + if (!match) { + if (existingTrigger?.get(uuid)) this.get(triggerKey).delete(uuid); + } else { + const { trigger, triggeringActorType, commands } = match; + + if (!existingTrigger) this.set(trigger, new Map()); + this.get(trigger).set(uuid, { actor, triggeringActorType, commands }); + } + } + } + + registerItemTriggers(item, registerOverride) { + for (const action of item.system.actions ?? []) { + if (!action.actor) continue; + + /* Non actor-linked should only prep synthetic actors so they're not registering triggers unless they're on the canvas */ + if ( + !registerOverride && + !action.actor.prototypeToken.actorLink && + (!(action.actor.parent instanceof game.system.api.documents.DhToken) || !action.actor.parent?.uuid) + ) + continue; + + const triggers = {}; + for (const trigger of action.triggers) { + const { args } = CONFIG.DH.TRIGGER.triggers[trigger.trigger]; + const fn = new foundry.utils.AsyncFunction(...args, `{${trigger.command}\n}`); + + if (!triggers[trigger.trigger]) + triggers[trigger.trigger] = { + trigger: trigger.trigger, + triggeringActorType: trigger.triggeringActorType, + commands: [] + }; + triggers[trigger.trigger].commands.push(fn.bind(action)); + } + + this.registerTriggers(triggers, action.actor?.uuid, item.uuid); + } + } + + unregisterTriggers(triggerKeys, uuid) { + for (const triggerKey of triggerKeys) { + const existingTrigger = this.get(triggerKey); + if (!existingTrigger) return; + + existingTrigger.delete(uuid); + } + } + + unregisterItemTriggers(items) { + for (const item of items) { + if (!item.system.actions.size) continue; + + const triggers = (item.system.actions ?? []).reduce((acc, action) => { + acc.push(...action.triggers.map(x => x.trigger)); + return acc; + }, []); + + this.unregisterTriggers(triggers, item.uuid); + } + } + + unregisterSceneTriggers(scene) { + for (const triggerKey of Object.keys(CONFIG.DH.TRIGGER.triggers)) { + const existingTrigger = this.get(triggerKey); + if (!existingTrigger) continue; + const filtered = new Map(); + for (const [uuid, data] of existingTrigger.entries()) { + if (!uuid.startsWith(scene.uuid)) filtered.set(uuid, data); + } + this.set(triggerKey, filtered); + } + } + + registerSceneTriggers(scene) { + /* TODO: Finish sceneEnvironment registration and unreg */ + // const systemData = new game.system.api.data.scenes.DHScene(scene.flags.daggerheart); + // for (const environment of systemData.sceneEnvironments) { + // for (const feature of environment.system.features) { + // if(feature) this.registerItemTriggers(feature, true); + // } + // } + + for (const actor of scene.tokens.filter(x => x.actor).map(x => x.actor)) { + if (actor.prototypeToken.actorLink) continue; + + for (const item of actor.items) { + this.registerItemTriggers(item); + } + } + } + + 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) { + const tokenBoundActors = ['adversary', 'environment']; + const triggerActors = ['character', ...tokenBoundActors]; + for (let [itemUuid, { actor: actorUuid, triggeringActorType, commands }] of dualityTrigger.entries()) { + const actor = await foundry.utils.fromUuid(actorUuid); + if (!actor || !triggerActors.includes(actor.type)) continue; + if (tokenBoundActors.includes(actor.type) && !actor.getActiveTokens().length) continue; + + const triggerData = CONFIG.DH.TRIGGER.triggers[trigger]; + if (triggerData.usesActor && triggeringActorType !== 'any') { + if (triggeringActorType === 'self' && currentActor?.uuid !== actorUuid) continue; + else if (triggeringActorType === 'other' && currentActor?.uuid === actorUuid) continue; + } + + for (const command of commands) { + try { + if (CONFIG.debug.triggers) { + const item = await foundry.utils.fromUuid(itemUuid); + console.log( + game.i18n.format('DAGGERHEART.UI.ConsoleLogs.triggerRun', { + actor: actor.name ?? '', + item: item?.name ?? '', + trigger: game.i18n.localize(triggerData.label) + }) + ); + } + + 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; + } +} diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 300bd14c..cec1a24d 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -104,6 +104,16 @@ export default class DhpActor extends Actor { } } + async _preDelete() { + if (this.prototypeToken.actorLink) { + game.system.registeredTriggers.unregisterItemTriggers(this.items); + } else { + for (const token of this.getActiveTokens()) { + game.system.registeredTriggers.unregisterItemTriggers(token.actor.items); + } + } + } + _onDelete(options, userId) { super._onDelete(options, userId); for (const party of this.parties) { diff --git a/module/documents/item.mjs b/module/documents/item.mjs index 7607658c..0a163dab 100644 --- a/module/documents/item.mjs +++ b/module/documents/item.mjs @@ -208,4 +208,23 @@ export default class DHItem extends foundry.documents.Item { cls.create(msg); } + + deleteTriggers() { + const actions = Array.from(this.system.actions ?? []); + if (!actions.length) return; + + const triggerKeys = actions.flatMap(action => action.triggers.map(x => x.trigger)); + + game.system.registeredTriggers.unregisterTriggers(triggerKeys, this.uuid); + + if (!(this.actor.parent instanceof game.system.api.documents.DhToken)) { + for (const token of this.actor.getActiveTokens()) { + game.system.registeredTriggers.unregisterTriggers(triggerKeys, `${token.document.uuid}.${this.uuid}`); + } + } + } + + async _preDelete() { + this.deleteTriggers(); + } } diff --git a/module/documents/token.mjs b/module/documents/token.mjs index 4ac29264..317f3acf 100644 --- a/module/documents/token.mjs +++ b/module/documents/token.mjs @@ -536,4 +536,10 @@ export default class DHToken extends CONFIG.Token.documentClass { }; } //#endregion + + async _preDelete() { + if (this.actor && !this.actor.prototypeToken?.actorLink) { + game.system.registeredTriggers.unregisterItemTriggers(this.actor.items); + } + } } diff --git a/styles/less/global/chat.less b/styles/less/global/chat.less index 69ee369a..dc671e44 100644 --- a/styles/less/global/chat.less +++ b/styles/less/global/chat.less @@ -2,20 +2,18 @@ @import '../utils/fonts.less'; @import '../utils/mixin.less'; -.theme-light { - .daggerheart.chat-sidebar .chat-log, - #chat-notifications .chat-log { - .chat-message { - background-image: url('../assets/parchments/dh-parchment-light.png'); +.daggerheart.chat-sidebar.theme-light, +#interface.theme-light { + .chat-log .chat-message { + background-image: url('../assets/parchments/dh-parchment-light.png'); - .message-header .message-header-metadata .message-metadata, - .message-header .message-header-main .message-sub-header-container { - color: @dark; - } + .message-header .message-header-metadata .message-metadata, + .message-header .message-header-main .message-sub-header-container { + color: @dark; + } - .message-header .message-header-main .message-sub-header-container h4 { - color: @dark-blue; - } + .message-header .message-header-main .message-sub-header-container h4 { + color: @dark-blue; } } } diff --git a/styles/less/ui/chat/ability-use.less b/styles/less/ui/chat/ability-use.less index 88302d0d..b590911d 100644 --- a/styles/less/ui/chat/ability-use.less +++ b/styles/less/ui/chat/ability-use.less @@ -2,6 +2,7 @@ @import '../../utils/fonts.less'; @import '../../utils/spacing.less'; +.daggerheart.chat-sidebar.theme-light, #interface.theme-light { .daggerheart.chat.domain-card { .domain-card-move .domain-card-header { diff --git a/styles/less/ui/chat/action.less b/styles/less/ui/chat/action.less index 8d309cfe..a3d2f3cc 100644 --- a/styles/less/ui/chat/action.less +++ b/styles/less/ui/chat/action.less @@ -2,6 +2,7 @@ @import '../../utils/fonts.less'; @import '../../utils/spacing.less'; +.daggerheart.chat-sidebar.theme-light, #interface.theme-light { .daggerheart.chat.action { .action-move .action-section { diff --git a/styles/less/ui/chat/chat.less b/styles/less/ui/chat/chat.less index 714add13..1b1e3c1c 100644 --- a/styles/less/ui/chat/chat.less +++ b/styles/less/ui/chat/chat.less @@ -2,9 +2,9 @@ @import '../../utils/fonts.less'; @import '../../utils/spacing.less'; +.daggerheart.chat-sidebar.theme-light, #interface.theme-light { - .daggerheart.chat-sidebar .chat-log, - #chat-notifications .chat-log { + .chat-log { --text-color: @dark-blue; --bg-color: @dark-blue-40; diff --git a/styles/less/ui/chat/damage-summary.less b/styles/less/ui/chat/damage-summary.less index 3fea45e5..b47cd41f 100644 --- a/styles/less/ui/chat/damage-summary.less +++ b/styles/less/ui/chat/damage-summary.less @@ -1,5 +1,6 @@ @import '../../utils/colors.less'; +.daggerheart.chat-sidebar.theme-light, #interface.theme-light { .daggerheart.chat.damage-summary .token-target-container { &:hover { diff --git a/styles/less/ui/chat/downtime.less b/styles/less/ui/chat/downtime.less index 2875ea10..ca0cd090 100644 --- a/styles/less/ui/chat/downtime.less +++ b/styles/less/ui/chat/downtime.less @@ -2,6 +2,7 @@ @import '../../utils/fonts.less'; @import '../../utils/spacing.less'; +.daggerheart.chat-sidebar.theme-light, #interface.theme-light { .daggerheart.chat.downtime { .downtime-moves-list .downtime-move { diff --git a/styles/less/ui/chat/effect-summary.less b/styles/less/ui/chat/effect-summary.less index 3d72571d..87d53eeb 100644 --- a/styles/less/ui/chat/effect-summary.less +++ b/styles/less/ui/chat/effect-summary.less @@ -1,5 +1,6 @@ @import '../../utils/colors.less'; +.daggerheart.chat-sidebar.theme-light, #interface.theme-light { .daggerheart.chat.effect-summary { .effect-header, diff --git a/styles/less/ui/chat/group-roll.less b/styles/less/ui/chat/group-roll.less index 02b8e312..9ed87220 100644 --- a/styles/less/ui/chat/group-roll.less +++ b/styles/less/ui/chat/group-roll.less @@ -125,9 +125,9 @@ .group-roll-trait { padding: 2px 8px; - border: 1px solid light-dark(white, white); + border: 1px solid light-dark(@dark-blue, white); border-radius: 6px; - color: light-dark(white, white); + color: light-dark(@dark-blue, white); background: light-dark(@beige-80, @beige-80); } } diff --git a/styles/less/ui/chat/sheet.less b/styles/less/ui/chat/sheet.less index 3d47a9b5..b632db35 100644 --- a/styles/less/ui/chat/sheet.less +++ b/styles/less/ui/chat/sheet.less @@ -1,6 +1,7 @@ @import '../../utils/colors.less'; @import '../../utils/fonts.less'; +.daggerheart.chat-sidebar.theme-light, #interface.theme-light { .chat-message:not(.duality) .message-content { color: @dark; diff --git a/styles/less/ui/sidebar/daggerheartMenu.less b/styles/less/ui/sidebar/daggerheartMenu.less index 80eda9a1..677214d7 100644 --- a/styles/less/ui/sidebar/daggerheartMenu.less +++ b/styles/less/ui/sidebar/daggerheartMenu.less @@ -5,6 +5,8 @@ display: flex; flex-direction: column; gap: 8px; + overflow: auto; + height: 100%; } h2 { diff --git a/system.json b/system.json index ae0396f1..2e216dc1 100644 --- a/system.json +++ b/system.json @@ -2,7 +2,7 @@ "id": "daggerheart", "title": "Daggerheart", "description": "An unofficial implementation of the Daggerheart system", - "version": "1.5.0", + "version": "1.5.1", "compatibility": { "minimum": "13.346", "verified": "13.351",