Merged with development

This commit is contained in:
WBHarry 2026-01-12 14:55:53 +01:00
commit 379398f2c7
46 changed files with 968 additions and 73 deletions

View file

@ -66,6 +66,7 @@ CONFIG.Token.rulerClass = placeables.DhTokenRuler;
CONFIG.Token.hudClass = applications.hud.DHTokenHUD; CONFIG.Token.hudClass = applications.hud.DHTokenHUD;
CONFIG.ui.combat = applications.ui.DhCombatTracker; CONFIG.ui.combat = applications.ui.DhCombatTracker;
CONFIG.ui.nav = applications.ui.DhSceneNavigation;
CONFIG.ui.chat = applications.ui.DhChatLog; CONFIG.ui.chat = applications.ui.DhChatLog;
CONFIG.ui.effectsDisplay = applications.ui.DhEffectsDisplay; CONFIG.ui.effectsDisplay = applications.ui.DhEffectsDisplay;
CONFIG.ui.hotbar = applications.ui.DhHotbar; CONFIG.ui.hotbar = applications.ui.DhHotbar;
@ -89,6 +90,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, {
@ -321,7 +324,7 @@ const updateActorsRangeDependentEffects = async token => {
CONFIG.DH.SETTINGS.gameSettings.variantRules CONFIG.DH.SETTINGS.gameSettings.variantRules
).rangeMeasurement; ).rangeMeasurement;
for (let effect of token.actor.allApplicableEffects()) { for (let effect of token.actor?.allApplicableEffects() ?? []) {
if (!effect.system.rangeDependence?.enabled) continue; if (!effect.system.rangeDependence?.enabled) continue;
const { target, range, type } = effect.system.rangeDependence; const { target, range, type } = effect.system.rangeDependence;
@ -384,3 +387,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;
}
}

View file

@ -94,7 +94,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": {
@ -1151,7 +1153,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",
@ -1225,6 +1228,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",
@ -2062,10 +2088,10 @@
"partyMembers": "Party Members", "partyMembers": "Party Members",
"projects": "Projects", "projects": "Projects",
"types": "Types", "types": "Types",
"itemFeatures": "Item Features",
"questions": "Questions", "questions": "Questions",
"configuration": "Configuration", "configuration": "Configuration",
"base": "Base" "base": "Base",
"triggers": "Triggers"
}, },
"Tiers": { "Tiers": {
"singular": "Tier", "singular": "Tier",
@ -2443,6 +2469,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"
} }
@ -2452,6 +2484,9 @@
}, },
"roll": { "roll": {
"title": "Actions" "title": "Actions"
},
"trigger": {
"title": "Triggers"
} }
}, },
"Homebrew": { "Homebrew": {
@ -2579,7 +2614,9 @@
} }
}, },
"disabledText": "Daggerheart Measurements are disabled in System Settings - Variant Rules", "disabledText": "Daggerheart Measurements are disabled in System Settings - Variant Rules",
"rangeMeasurement": "Range Measurement" "rangeMeasurement": "Range Measurement",
"sceneEnvironments": "Scene Environments",
"dragEnvironmentHere": "Drag environments here"
} }
}, },
"UI": { "UI": {
@ -2792,7 +2829,9 @@
"gmRequired": "This action requires an online GM", "gmRequired": "This action requires an online GM",
"gmOnly": "This can only be accessed by the GM", "gmOnly": "This can only be accessed by the GM",
"noActorOwnership": "You do not have permissions for this character", "noActorOwnership": "You do not have permissions for this character",
"documentIsMissing": "The {documentType} is missing from the world." "documentIsMissing": "The {documentType} is missing from the world.",
"tokenActorMissing": "{name} is missing an Actor",
"tokenActorsMissing": "[{names}] missing Actors"
}, },
"Sidebar": { "Sidebar": {
"actorDirectory": { "actorDirectory": {

View file

@ -21,6 +21,8 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
async _prepareContext(options) { async _prepareContext(options) {
const context = await super._prepareContext(options); const context = await super._prepareContext(options);
if (!this.actor) return context;
context.partyOnCanvas = context.partyOnCanvas =
this.actor.type === 'party' && this.actor.type === 'party' &&
this.actor.system.partyMembers.some(member => member.getActiveTokens().length > 0); this.actor.system.partyMembers.some(member => member.getActiveTokens().length > 0);
@ -58,14 +60,33 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
} }
static async #onToggleCombat() { static async #onToggleCombat() {
const tokensWithoutActors = canvas.tokens.controlled.filter(t => !t.actor);
const warning =
tokensWithoutActors.length === 1
? game.i18n.format('DAGGERHEART.UI.Notifications.tokenActorMissing', {
name: tokensWithoutActors[0].name
})
: game.i18n.format('DAGGERHEART.UI.Notifications.tokenActorsMissing', {
names: tokensWithoutActors.map(x => x.name).join(', ')
});
const tokens = canvas.tokens.controlled const tokens = canvas.tokens.controlled
.filter(t => !t.actor || !DHTokenHUD.#nonCombatTypes.includes(t.actor.type)) .filter(t => t.actor && !DHTokenHUD.#nonCombatTypes.includes(t.actor.type))
.map(t => t.document); .map(t => t.document);
if (!this.object.controlled) tokens.push(this.document); if (!this.object.controlled && this.document.actor) tokens.push(this.document);
try { try {
if (this.document.inCombat) await TokenDocument.implementation.deleteCombatants(tokens); if (this.document.inCombat) {
else await TokenDocument.implementation.createCombatants(tokens); const tokensInCombat = tokens.filter(t => t.inCombat);
await TokenDocument.implementation.deleteCombatants([...tokensInCombat, ...tokensWithoutActors]);
} else {
if (tokensWithoutActors.length) {
ui.notifications.warn(warning);
}
const tokensOutOfCombat = tokens.filter(t => !t.inCombat);
await TokenDocument.implementation.createCombatants(tokensOutOfCombat);
}
} catch (err) { } catch (err) {
ui.notifications.warn(err.message); ui.notifications.warn(err.message);
} }

View file

@ -1,16 +1,28 @@
import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
export default class DhSceneConfigSettings extends foundry.applications.sheets.SceneConfig { export default class DhSceneConfigSettings extends foundry.applications.sheets.SceneConfig {
// static DEFAULT_OPTIONS = { constructor(options) {
// ...super.DEFAULT_OPTIONS, super(options);
// form: {
// handler: this.updateData, Hooks.on(socketEvent.Refresh, ({ refreshType }) => {
// closeOnSubmit: true if (refreshType === RefreshType.Scene) {
// } this.daggerheartFlag = new game.system.api.data.scenes.DHScene(this.document.flags.daggerheart);
// }; this.render();
}
});
}
static DEFAULT_OPTIONS = {
...super.DEFAULT_OPTIONS,
actions: {
...super.DEFAULT_OPTIONS.actions,
removeSceneEnvironment: DhSceneConfigSettings.#removeSceneEnvironment
}
};
static buildParts() { static buildParts() {
const { footer, tabs, ...parts } = super.PARTS; const { footer, tabs, ...parts } = super.PARTS;
const tmpParts = { const tmpParts = {
// tabs,
tabs: { template: 'systems/daggerheart/templates/scene/tabs.hbs' }, tabs: { template: 'systems/daggerheart/templates/scene/tabs.hbs' },
...parts, ...parts,
dh: { template: 'systems/daggerheart/templates/scene/dh-config.hbs' }, dh: { template: 'systems/daggerheart/templates/scene/dh-config.hbs' },
@ -28,27 +40,45 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S
static TABS = DhSceneConfigSettings.buildTabs(); static TABS = DhSceneConfigSettings.buildTabs();
async _preRender(context, options) {
await super._preFirstRender(context, options);
this.daggerheartFlag = new game.system.api.data.scenes.DHScene(this.document.flags.daggerheart);
}
_attachPartListeners(partId, htmlElement, options) { _attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options); super._attachPartListeners(partId, htmlElement, options);
switch (partId) { switch (partId) {
case 'dh': case 'dh':
htmlElement.querySelector('#rangeMeasurementSetting')?.addEventListener('change', async event => { htmlElement.querySelector('#rangeMeasurementSetting')?.addEventListener('change', async event => {
const flagData = foundry.utils.mergeObject(this.document.flags.daggerheart, { this.daggerheartFlag.updateSource({ rangeMeasurement: { setting: event.target.value } });
rangeMeasurement: { setting: event.target.value }
});
this.document.flags.daggerheart = flagData;
this.render(); this.render();
}); });
const dragArea = htmlElement.querySelector('.scene-environments');
if (dragArea) dragArea.ondrop = this._onDrop.bind(this);
break; break;
} }
} }
async _onDrop(event) {
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
const item = await foundry.utils.fromUuid(data.uuid);
if (item instanceof game.system.api.documents.DhpActor && item.type === 'environment') {
await this.daggerheartFlag.updateSource({
sceneEnvironments: [...this.daggerheartFlag.sceneEnvironments, data.uuid]
});
this.render();
}
}
/** @inheritDoc */ /** @inheritDoc */
async _preparePartContext(partId, context, options) { async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options); context = await super._preparePartContext(partId, context, options);
switch (partId) { switch (partId) {
case 'dh': case 'dh':
context.data = new game.system.api.data.scenes.DHScene(canvas.scene.flags.daggerheart); context.data = this.daggerheartFlag;
context.variantRules = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules); context.variantRules = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules);
break; break;
} }
@ -56,8 +86,24 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S
return context; return context;
} }
// static async updateData(event, _, formData) { static async #removeSceneEnvironment(_event, button) {
// const data = foundry.utils.expandObject(formData.object); await this.daggerheartFlag.updateSource({
// this.close(data); sceneEnvironments: this.daggerheartFlag.sceneEnvironments.filter(
// } (_, index) => index !== Number.parseInt(button.dataset.index)
)
});
this.render();
}
/** @override */
async _processSubmitData(event, form, submitData, options) {
submitData.flags.daggerheart = this.daggerheartFlag.toObject();
for (const key of Object.keys(this.document._source.flags.daggerheart?.sceneEnvironments ?? {})) {
if (!submitData.flags.daggerheart.sceneEnvironments[key]) {
submitData.flags.daggerheart.sceneEnvironments[`-=${key}`] = null;
}
}
super._processSubmitData(event, form, submitData, options);
}
} }

View file

@ -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', 'action-config', '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
@ -30,7 +31,10 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2)
editEffect: this.editEffect, editEffect: this.editEffect,
addDamage: this.addDamage, addDamage: this.addDamage,
removeDamage: this.removeDamage, removeDamage: this.removeDamage,
editDoc: this.editDoc editDoc: this.editDoc,
addTrigger: this.addTrigger,
removeTrigger: this.removeTrigger,
expandTrigger: this.expandTrigger
}, },
form: { form: {
handler: this.updateForm, handler: this.updateForm,
@ -57,6 +61,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'
} }
}; };
@ -84,6 +92,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'
} }
}; };
@ -128,6 +144,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 = [
@ -256,6 +282,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;
}
}
updateSummonCount(event) { updateSummonCount(event) {
event.stopPropagation(); event.stopPropagation();
const wrapper = event.target.closest('.summon-count-wrapper'); const wrapper = event.target.closest('.summon-count-wrapper');

View file

@ -211,7 +211,7 @@ export default function DHApplicationMixin(Base) {
const step = event.key === 'ArrowUp' ? 1 : event.key === 'ArrowDown' ? -1 : 0; const step = event.key === 'ArrowUp' ? 1 : event.key === 'ArrowDown' ? -1 : 0;
if (step !== 0) { if (step !== 0) {
handleUpdate(step); handleUpdate(step);
deltaInput.dispatchEvent(new Event("change", { bubbles: true })); deltaInput.dispatchEvent(new Event('change', { bubbles: true }));
} }
}); });
@ -222,7 +222,7 @@ export default function DHApplicationMixin(Base) {
if (deltaInput === document.activeElement) { if (deltaInput === document.activeElement) {
event.preventDefault(); event.preventDefault();
handleUpdate(Math.sign(-1 * event.deltaY)); handleUpdate(Math.sign(-1 * event.deltaY));
deltaInput.dispatchEvent(new Event("change", { bubbles: true })); deltaInput.dispatchEvent(new Event('change', { bubbles: true }));
} }
}, },
{ passive: false } { passive: false }
@ -236,7 +236,7 @@ export default function DHApplicationMixin(Base) {
// Handle contenteditable // Handle contenteditable
for (const input of htmlElement.querySelectorAll('[contenteditable][data-property]')) { for (const input of htmlElement.querySelectorAll('[contenteditable][data-property]')) {
const property = input.dataset.property; const property = input.dataset.property;
input.addEventListener("blur", () => { input.addEventListener('blur', () => {
const selection = document.getSelection(); const selection = document.getSelection();
if (input.contains(selection.anchorNode)) { if (input.contains(selection.anchorNode)) {
selection.empty(); selection.empty();
@ -244,12 +244,12 @@ export default function DHApplicationMixin(Base) {
this.document.update({ [property]: input.textContent }); this.document.update({ [property]: input.textContent });
}); });
input.addEventListener("keydown", event => { input.addEventListener('keydown', event => {
if (event.key === "Enter") input.blur(); if (event.key === 'Enter') input.blur();
}); });
// Chrome sometimes add <br>, which aren't a problem for the value but are for the placeholder // Chrome sometimes add <br>, which aren't a problem for the value but are for the placeholder
input.addEventListener("input", () => input.querySelectorAll("br").forEach((i) => i.remove())); input.addEventListener('input', () => input.querySelectorAll('br').forEach(i => i.remove()));
} }
} }
@ -585,7 +585,9 @@ export default function DHApplicationMixin(Base) {
if (!doc || !descriptionElement) continue; if (!doc || !descriptionElement) continue;
// localize the description (idk if it's still necessary) // localize the description (idk if it's still necessary)
const description = game.i18n.localize(doc.system?.description ?? doc.description); const description = doc.system?.getEnrichedDescription
? await doc.system.getEnrichedDescription()
: game.i18n.localize(doc.system?.description ?? doc.description);
// Enrich the description and attach it; // Enrich the description and attach it;
const isAction = doc.documentName === 'Action'; const isAction = doc.documentName === 'Action';
@ -736,7 +738,7 @@ export default function DHApplicationMixin(Base) {
}; };
if (inVault) data['system.inVault'] = true; if (inVault) data['system.inVault'] = true;
if (disabled) data.disabled = true; if (disabled) data.disabled = true;
if (type === "domainCard" && parent?.system.domains?.length) { if (type === 'domainCard' && parent?.system.domains?.length) {
data.system.domain = parent.system.domains[0]; data.system.domain = parent.system.domains[0];
} }

View file

@ -76,16 +76,10 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
/**@inheritdoc */ /**@inheritdoc */
async _preparePartContext(partId, context, options) { async _preparePartContext(partId, context, options) {
await super._preparePartContext(partId, context, options); await super._preparePartContext(partId, context, options);
const { TextEditor } = foundry.applications.ux;
switch (partId) { switch (partId) {
case 'description': case 'description':
const value = foundry.utils.getProperty(this.document, 'system.description') ?? ''; context.enrichedDescription = await this.document.system.getEnrichedDescription();
context.enrichedDescription = await TextEditor.enrichHTML(value, {
relativeTo: this.item,
rollData: this.item.getRollData(),
secrets: this.item.isOwner
});
break; break;
case 'effects': case 'effects':
await this._prepareEffectsContext(context, options); await this._prepareEffectsContext(context, options);

View file

@ -5,4 +5,5 @@ export { default as DhCombatTracker } from './combatTracker.mjs';
export { default as DhEffectsDisplay } from './effectsDisplay.mjs'; export { default as DhEffectsDisplay } from './effectsDisplay.mjs';
export { default as DhFearTracker } from './fearTracker.mjs'; export { default as DhFearTracker } from './fearTracker.mjs';
export { default as DhHotbar } from './hotbar.mjs'; export { default as DhHotbar } from './hotbar.mjs';
export { default as DhSceneNavigation } from './sceneNavigation.mjs';
export { ItemBrowser } from './itemBrowser.mjs'; export { ItemBrowser } from './itemBrowser.mjs';

View file

@ -127,7 +127,7 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
resource, resource,
active: index === combat.turn, active: index === combat.turn,
canPing: combatant.sceneId === canvas.scene?.id && game.user.hasPermission('PING_CANVAS'), canPing: combatant.sceneId === canvas.scene?.id && game.user.hasPermission('PING_CANVAS'),
type: combatant.actor.system.type, type: combatant.actor?.system?.type,
img: await this._getCombatantThumbnail(combatant) img: await this._getCombatantThumbnail(combatant)
}; };
@ -165,7 +165,7 @@ export default class DhCombatTracker extends foundry.applications.sidebar.tabs.C
if (this.viewed.turn !== toggleTurn) { if (this.viewed.turn !== toggleTurn) {
const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns; const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns;
if (combatant.actor.type === 'character') { if (combatant.actor?.type === 'character') {
await updateCountdowns( await updateCountdowns(
CONFIG.DH.GENERAL.countdownProgressionTypes.spotlight.id, CONFIG.DH.GENERAL.countdownProgressionTypes.spotlight.id,
CONFIG.DH.GENERAL.countdownProgressionTypes.characterSpotlight.id CONFIG.DH.GENERAL.countdownProgressionTypes.characterSpotlight.id

View file

@ -0,0 +1,89 @@
import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs';
export default class DhSceneNavigation extends foundry.applications.ui.SceneNavigation {
/** @inheritdoc */
static DEFAULT_OPTIONS = {
...super.DEFAULT_OPTIONS,
classes: ['faded-ui', 'flexcol', 'scene-navigation'],
actions: {
openSceneEnvironment: DhSceneNavigation.#openSceneEnvironment
}
};
/** @inheritdoc */
static PARTS = {
scenes: {
root: true,
template: 'systems/daggerheart/templates/ui/sceneNavigation/scene-navigation.hbs'
}
};
/** @inheritdoc */
async _prepareContext(options) {
const context = await super._prepareContext(options);
const extendScenes = scenes =>
scenes.map(x => {
const scene = game.scenes.get(x.id);
if (!scene.flags.daggerheart) return x;
const daggerheartInfo = new game.system.api.data.scenes.DHScene(scene.flags.daggerheart);
const environments = daggerheartInfo.sceneEnvironments.filter(
x => x && x.testUserPermission(game.user, 'LIMITED')
);
const hasEnvironments = environments.length > 0;
return {
...x,
hasEnvironments,
environmentImage: hasEnvironments ? environments[0].img : null,
environments: environments
};
});
context.scenes.active = extendScenes(context.scenes.active);
context.scenes.inactive = extendScenes(context.scenes.inactive);
return context;
}
static async #openSceneEnvironment(event, button) {
const scene = game.scenes.get(button.dataset.sceneId);
const sceneEnvironments = new game.system.api.data.scenes.DHScene(
scene.flags.daggerheart
).sceneEnvironments.filter(x => x.testUserPermission(game.user, 'LIMITED'));
if (sceneEnvironments.length === 1 || event.shiftKey) {
sceneEnvironments[0].sheet.render(true);
} else {
new foundry.applications.ux.ContextMenu.implementation(
button,
'.scene-environment',
sceneEnvironments.map(environment => ({
name: environment.name,
callback: () => {
if (scene.flags.daggerheart.sceneEnvironments[0] !== environment.uuid) {
const newEnvironments = scene.flags.daggerheart.sceneEnvironments;
const newFirst = newEnvironments.splice(
newEnvironments.findIndex(x => x === environment.uuid)
)[0];
newEnvironments.unshift(newFirst);
emitAsGM(
GMUpdateEvent.UpdateDocument,
scene.update.bind(scene),
{ 'flags.daggerheart.sceneEnvironments': newEnvironments },
scene.uuid
);
}
environment.sheet.render({ force: true });
}
})),
{
jQuery: false,
fixed: true
}
);
CONFIG.ux.ContextMenu.triggerContextMenu(event, '.scene-environment');
}
}
}

View file

@ -96,11 +96,11 @@ export default class DHContextMenu extends foundry.applications.ux.ContextMenu {
* Trigger a context menu event in response to a normal click on a additional options button. * Trigger a context menu event in response to a normal click on a additional options button.
* @param {PointerEvent} event * @param {PointerEvent} event
*/ */
static triggerContextMenu(event) { static triggerContextMenu(event, altSelector) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
const { clientX, clientY } = event; const { clientX, clientY } = event;
const selector = '[data-item-uuid]'; const selector = altSelector ?? '[data-item-uuid]';
const target = event.target.closest(selector) ?? event.currentTarget.closest(selector); const target = event.target.closest(selector) ?? event.currentTarget.closest(selector);
target?.dispatchEvent( target?.dispatchEvent(
new PointerEvent('contextmenu', { new PointerEvent('contextmenu', {

View file

@ -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';

View file

@ -9,7 +9,7 @@ export const AdversaryBPPerEncounter = (adversaries, characters) => {
); );
if (existingEntry) { if (existingEntry) {
existingEntry.nr += 1; existingEntry.nr += 1;
} else { } else if (adversary.type) {
acc.push({ adversary, nr: 1 }); acc.push({ adversary, nr: 1 });
} }
return acc; return acc;

View file

@ -1,5 +1,3 @@
const hooksConfig = { export const hooksConfig = {
effectDisplayToggle: 'DHEffectDisplayToggle' effectDisplayToggle: 'DHEffectDisplayToggle'
}; };
export default hooksConfig;

View file

@ -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
}; };

View 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'
}
};

View file

@ -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 => {
@ -342,6 +344,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;

View file

@ -1,8 +1,11 @@
import BaseDataActor from './base.mjs'; import BaseDataActor from './base.mjs';
import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs'; import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
import DHEnvironmentSettings from '../../applications/sheets-configs/environment-settings.mjs'; import DHEnvironmentSettings from '../../applications/sheets-configs/environment-settings.mjs';
import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
export default class DhEnvironment extends BaseDataActor { export default class DhEnvironment extends BaseDataActor {
scenes = new Set();
/**@override */ /**@override */
static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Environment']; static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Environment'];
@ -53,6 +56,31 @@ export default class DhEnvironment extends BaseDataActor {
} }
isItemValid(source) { isItemValid(source) {
return source.type === "feature"; return source.type === 'feature';
}
_onUpdate(changes, options, userId) {
super._onUpdate(changes, options, userId);
for (const scene of this.scenes) {
scene.render();
}
}
_onDelete(options, userId) {
super._onDelete(options, userId);
for (const scene of this.scenes) {
if (game.user.isActiveGM) {
const newSceneEnvironments = scene.flags.daggerheart.sceneEnvironments.filter(
x => x !== this.parent.uuid
);
scene.update({ 'flags.daggerheart.sceneEnvironments': newSceneEnvironments }).then(() => {
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Scene });
game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: { refreshType: RefreshType.TagTeamRoll }
});
});
}
}
} }
} }

View file

@ -15,8 +15,9 @@ export default class DhCombat extends foundry.abstract.TypeDataModel {
get extendedBattleToggles() { get extendedBattleToggles() {
const modifiers = CONFIG.DH.ENCOUNTER.BPModifiers; const modifiers = CONFIG.DH.ENCOUNTER.BPModifiers;
const adversaries = const adversaries =
this.parent.turns?.filter(x => x.isNPC)?.map(x => ({ ...x.actor, type: x.actor.system.type })) ?? []; this.parent.turns?.filter(x => x.actor && x.isNPC)?.map(x => ({ ...x.actor, type: x.actor.system.type })) ??
const characters = this.parent.turns?.filter(x => !x.isNPC) ?? []; [];
const characters = this.parent.turns?.filter(x => x.actor && !x.isNPC) ?? [];
const activeAutomatic = Object.keys(modifiers).reduce((acc, categoryKey) => { const activeAutomatic = Object.keys(modifiers).reduce((acc, categoryKey) => {
const category = modifiers[categoryKey]; const category = modifiers[categoryKey];

View file

@ -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';

View 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
);
}
}

View file

@ -54,6 +54,21 @@ export default class DHArmor extends AttachableItem {
); );
} }
/**@inheritdoc */
async getDescriptionData() {
const baseDescription = this.description;
const allFeatures = CONFIG.DH.ITEM.allArmorFeatures();
const features = this.armorFeatures.map(x => allFeatures[x.value]);
if (!features.length) return { prefix: null, value: baseDescription, suffix: null };
const prefix = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/sheets/items/armor/description.hbs',
{ features }
);
return { prefix, value: baseDescription, suffix: null };
}
/**@inheritdoc */ /**@inheritdoc */
async _preUpdate(changes, options, user) { async _preUpdate(changes, options, user) {
const allowed = await super._preUpdate(changes, options, user); const allowed = await super._preUpdate(changes, options, user);

View file

@ -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';
@ -124,6 +124,33 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
return [source, page ? `pg ${page}.` : null].filter(x => x).join('. '); return [source, page ? `pg ${page}.` : null].filter(x => x).join('. ');
} }
/**
* Augments the description for the item with type specific info to display. Implemented in applicable item subtypes.
* @param {object} [options] - Options that modify the styling of the rendered template. { headerStyle: undefined|'none'|'large' }
* @returns {string}
*/
async getDescriptionData(_options) {
return { prefix: null, value: this.description, suffix: null };
}
/**
* Gets the enriched and augmented description for the item.
* @param {object} [options] - Options that modify the styling of the rendered template. { headerStyle: undefined|'none'|'large' }
* @returns {string}
*/
async getEnrichedDescription() {
if (!this.metadata.hasDescription) return '';
const { prefix, value, suffix } = await this.getDescriptionData();
const fullDescription = [prefix, value, suffix].filter(p => !!p).join('\n<hr>\n');
return await foundry.applications.ux.TextEditor.implementation.enrichHTML(fullDescription, {
relativeTo: this,
rollData: this.getRollData(),
secrets: this.isOwner
});
}
/** /**
* Obtain a data object used to evaluate any dice rolls associated with this Item Type * Obtain a data object used to evaluate any dice rolls associated with this Item Type
* @param {object} [options] - Options which modify the getRollData method. * @param {object} [options] - Options which modify the getRollData method.
@ -135,6 +162,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)) {

View file

@ -110,6 +110,21 @@ export default class DHWeapon extends AttachableItem {
); );
} }
/**@inheritdoc */
async getDescriptionData() {
const baseDescription = this.description;
const allFeatures = CONFIG.DH.ITEM.allWeaponFeatures();
const features = this.weaponFeatures.map(x => allFeatures[x.value]);
if (!features.length) return { prefix: null, value: baseDescription, suffix: null };
const prefix = await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/sheets/items/weapon/description.hbs',
{ features }
);
return { prefix, value: baseDescription, suffix: null };
}
prepareDerivedData() { prepareDerivedData() {
this.attack.roll.trait = this.rules.attack.roll.trait ?? this.attack.roll.trait; this.attack.roll.trait = this.rules.attack.roll.trait ?? this.attack.roll.trait;
} }

View file

@ -1,3 +1,8 @@
import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
/* Foundry does not add any system data for subtyped Scenes. The data model is therefore used by instantiating a new instance of it for sceneConfigSettings.mjs.
Needed dataprep and lifetime hooks are handled in documents/scene.
*/
export default class DHScene extends foundry.abstract.DataModel { export default class DHScene extends foundry.abstract.DataModel {
static defineSchema() { static defineSchema() {
const fields = foundry.data.fields; const fields = foundry.data.fields;
@ -13,7 +18,8 @@ export default class DHScene extends foundry.abstract.DataModel {
veryClose: new fields.NumberField({ integer: true, label: 'DAGGERHEART.CONFIG.Range.veryClose.name' }), veryClose: new fields.NumberField({ integer: true, label: 'DAGGERHEART.CONFIG.Range.veryClose.name' }),
close: new fields.NumberField({ integer: true, label: 'DAGGERHEART.CONFIG.Range.close.name' }), close: new fields.NumberField({ integer: true, label: 'DAGGERHEART.CONFIG.Range.close.name' }),
far: new fields.NumberField({ integer: true, label: 'DAGGERHEART.CONFIG.Range.far.name' }) far: new fields.NumberField({ integer: true, label: 'DAGGERHEART.CONFIG.Range.far.name' })
}) }),
sceneEnvironments: new ForeignDocumentUUIDArrayField({ type: 'Actor', prune: true })
}; };
} }
} }

View file

@ -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'
})
}) })
}; };
} }

View file

@ -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) {

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

@ -37,4 +37,30 @@ export default class DhScene extends Scene {
this.#sizeSyncBatch.clear(); this.#sizeSyncBatch.clear();
this.updateEmbeddedDocuments('Token', entries, { animation: { movementSpeed: 1.5 } }); this.updateEmbeddedDocuments('Token', entries, { animation: { movementSpeed: 1.5 } });
}, 0); }, 0);
prepareBaseData() {
super.prepareBaseData();
if (this instanceof game.system.api.documents.DhScene) {
const system = new game.system.api.data.scenes.DHScene(this.flags.daggerheart);
// Register this scene to all environements
for (const environment of system.sceneEnvironments) {
environment.system.scenes?.add(this);
}
}
}
_onDelete(options, userId) {
super._onDelete(options, userId);
if (this instanceof game.system.api.documents.DhScene) {
const system = new game.system.api.data.scenes.DHScene(this.flags.daggerheart);
// Clear this scene from all environments that aren't deleted
for (const environment of system.sceneEnvironments) {
environment?.system?.scenes?.delete(this);
}
}
}
} }

View file

@ -83,7 +83,7 @@ export default class DHToken extends CONFIG.Token.documentClass {
if (combat?.system?.battleToggles?.length) { if (combat?.system?.battleToggles?.length) {
await combat.toggleModifierEffects( await combat.toggleModifierEffects(
true, true,
tokens.map(x => x.actor) tokens.filter(x => x.actor).map(x => x.actor)
); );
} }
super.createCombatants(tokens, combat ?? {}); super.createCombatants(tokens, combat ?? {});
@ -95,7 +95,7 @@ export default class DHToken extends CONFIG.Token.documentClass {
if (combat?.system?.battleToggles?.length) { if (combat?.system?.battleToggles?.length) {
await combat.toggleModifierEffects( await combat.toggleModifierEffects(
false, false,
tokens.map(x => x.actor) tokens.filter(x => x.actor).map(x => x.actor)
); );
} }
super.deleteCombatants(tokens, combat ?? {}); super.deleteCombatants(tokens, combat ?? {});

View file

@ -220,12 +220,15 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
for (const [index, itemValue] of pathValue.entries()) { for (const [index, itemValue] of pathValue.entries()) {
const itemIsAction = itemValue instanceof game.system.api.models.actions.actionsTypes.base; const itemIsAction = itemValue instanceof game.system.api.models.actions.actionsTypes.base;
const value = itemIsAction || !itemValue?.item ? itemValue : itemValue.item; const value = itemIsAction || !itemValue?.item ? itemValue : itemValue.item;
const enrichedValue = await TextEditor.enrichHTML(value.system?.description ?? value.description); const enrichedValue =
(await value.system?.getEnrichedDescription?.()) ??
(await TextEditor.enrichHTML(value.system?.description ?? value.description));
if (itemIsAction) value.enrichedDescription = enrichedValue; if (itemIsAction) value.enrichedDescription = enrichedValue;
else foundry.utils.setProperty(item, `${basePath}.${index}.enrichedDescription`, enrichedValue); else foundry.utils.setProperty(item, `${basePath}.${index}.enrichedDescription`, enrichedValue);
} }
} else { } else {
const enrichedValue = await TextEditor.enrichHTML(pathValue); const enrichedValue =
(await item.system?.getEnrichedDescription?.()) ?? (await TextEditor.enrichHTML(pathValue));
foundry.utils.setProperty( foundry.utils.setProperty(
item, item,
`${data.path ? `${data.path}.` : ''}enriched${data.name.capitalize()}`, `${data.path ? `${data.path}.` : ''}enriched${data.name.capitalize()}`,
@ -262,7 +265,7 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
const combat = game.combats.get(combatId); const combat = game.combats.get(combatId);
const adversaries = const adversaries =
combat.turns?.filter(x => x.actor?.isNPC)?.map(x => ({ ...x.actor, type: x.actor.system.type })) ?? []; combat.turns?.filter(x => x.actor?.isNPC)?.map(x => ({ ...x.actor, type: x.actor.system.type })) ?? [];
const characters = combat.turns?.filter(x => !x.isNPC) ?? []; const characters = combat.turns?.filter(x => !x.isNPC && x.actor) ?? [];
const nrCharacters = characters.length; const nrCharacters = characters.length;
const currentBP = AdversaryBPPerEncounter(adversaries, characters); const currentBP = AdversaryBPPerEncounter(adversaries, characters);
@ -272,7 +275,7 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
); );
const categories = combat.combatants.reduce((acc, combatant) => { const categories = combat.combatants.reduce((acc, combatant) => {
if (combatant.actor.type === 'adversary') { if (combatant.actor?.type === 'adversary') {
const keyData = Object.keys(acc).reduce((identifiers, categoryKey) => { const keyData = Object.keys(acc).reduce((identifiers, categoryKey) => {
if (identifiers) return identifiers; if (identifiers) return identifiers;
const category = acc[categoryKey]; const category = acc[categoryKey];
@ -352,7 +355,7 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
await combat.toggleModifierEffects( await combat.toggleModifierEffects(
event.target.checked, event.target.checked,
combat.combatants.filter(x => x.actor.type === 'adversary').map(x => x.actor), combat.combatants.filter(x => x.actor?.type === 'adversary').map(x => x.actor),
category, category,
grouping grouping
); );

View file

@ -37,7 +37,8 @@ export const GMUpdateEvent = {
export const RefreshType = { export const RefreshType = {
Countdown: 'DhCoundownRefresh', Countdown: 'DhCoundownRefresh',
TagTeamRoll: 'DhTagTeamRollRefresh', TagTeamRoll: 'DhTagTeamRollRefresh',
EffectsDisplay: 'DhEffectsDisplayRefresh' EffectsDisplay: 'DhEffectsDisplayRefresh',
Scene: 'DhSceneRefresh'
}; };
export const registerSocketHooks = () => { export const registerSocketHooks = () => {
@ -92,6 +93,10 @@ export const registerSocketHooks = () => {
} }
} }
}); });
Hooks.on(socketEvent.RefreshDocument, async data => {
const document = await foundry.utils.fromUuid(data.uuid);
document.sheet.render();
});
}; };
export const registerUserQueries = () => { export const registerUserQueries = () => {

View file

@ -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,

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

@ -47,4 +47,58 @@
color: light-dark(@dark-blue-50, @beige-50); color: light-dark(@dark-blue-50, @beige-50);
} }
} }
.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;
}
}
} }

View file

@ -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';

View file

@ -33,3 +33,5 @@
@import './scene-config/scene-config.less'; @import './scene-config/scene-config.less';
@import './effects-display/sheet.less'; @import './effects-display/sheet.less';
@import './scene-navigation/scene-navigation.less';

View file

@ -37,4 +37,63 @@
.helper-text { .helper-text {
font-style: italic; font-style: italic;
} }
.scene-environments {
display: flex;
flex-direction: column;
gap: 8px;
.scene-environment {
display: flex;
align-items: center;
gap: 8px;
.scene-environment-inner {
display: flex;
align-items: center;
gap: 16px;
flex: 1;
img {
height: 36px;
}
h5 {
margin: 0;
}
.tags {
display: flex;
gap: 4px;
padding-bottom: 0;
.tag {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 3px 5px;
font-size: var(--font-size-12);
font: @font-body;
background: light-dark(@dark-15, @beige-15);
border: 1px solid light-dark(@dark, @beige);
border-radius: 3px;
}
.label {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
font-size: var(--font-size-12);
}
}
}
.remove-icon {
font-size: 16px;
}
}
}
} }

View file

@ -0,0 +1,36 @@
#ui-left #ui-left-column-2 {
flex: 0 0 230px;
.scene-navigation {
.scene-wrapper {
display: flex;
gap: 2px;
height: var(--control-size);
width: 100%;
.scene-environment {
padding: 0;
img {
border-radius: 4px;
}
}
}
.scene {
justify-content: center;
align-content: center;
background: var(--control-bg-color);
border: 1px solid var(--control-border-color);
border-radius: 4px;
color: var(--control-icon-color);
pointer-events: all;
transition:
border 0.25s,
color 0.25s;
text-shadow: none;
width: 200px;
max-width: 200px;
}
}
}

View file

@ -2,7 +2,7 @@
"id": "daggerheart", "id": "daggerheart",
"title": "Daggerheart", "title": "Daggerheart",
"description": "An unofficial implementation of the Daggerheart system", "description": "An unofficial implementation of the Daggerheart system",
"version": "1.4.5", "version": "1.4.6",
"compatibility": { "compatibility": {
"minimum": "13.346", "minimum": "13.346",
"verified": "13.351", "verified": "13.351",

View file

@ -21,4 +21,39 @@
<span class="helper-text">{{localize "DAGGERHEART.SETTINGS.Scene.disabledText"}}</span> <span class="helper-text">{{localize "DAGGERHEART.SETTINGS.Scene.disabledText"}}</span>
{{/if}} {{/if}}
</fieldset> </fieldset>
<fieldset>
<legend>
<span>{{localize "DAGGERHEART.SETTINGS.Scene.sceneEnvironments"}}</span>
</legend>
<div class="scene-environments">
{{#each data.sceneEnvironments as |environment index|}}
<div class="scene-environment" data-index="{{index}}">
{{#if environment}}
<div class="scene-environment-inner">
<img src="{{environment.img}}" />
<h5>{{environment.name}}</h5>
<div class="tags">
<div class="tag">
<span>
{{localize (concat 'DAGGERHEART.GENERAL.Tiers.' environment.system.tier)}}
</span>
</div>
{{#if environment.system.type}}
<div class="tag">
<span>
{{localize (concat 'DAGGERHEART.CONFIG.EnvironmentType.' environment.system.type '.label')}}
</span>
</div>
{{/if}}
</div>
</div>
{{/if}}
<a data-action="removeSceneEnvironment" data-index="{{index}}"><i class="fa-solid fa-trash remove-icon"></i></a>
</div>
{{/each}}
<span class="drag-area">{{localize "DAGGERHEART.SETTINGS.Scene.dragEnvironmentHere"}}</span>
</div>
</fieldset>
</div> </div>

View file

@ -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>

View 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>

View file

@ -0,0 +1,5 @@
<div>
{{#each features as | feature |}}
<div><strong>{{localize feature.label}}</strong>: {{{localize feature.description}}}</div>
{{/each}}
</div>

View file

@ -0,0 +1,5 @@
<div>
{{#each features as | feature |}}
<div><strong>{{localize feature.label}}</strong>: {{{localize feature.description}}}</div>
{{/each}}
</div>

View file

@ -0,0 +1,36 @@
<nav id="scene-navigation" aria-roledescription="{{localize "SCENE_NAVIGATION.LABEL"}}" data-tooltip-direction="RIGHT">
{{#if canExpand}}
<a id="scene-navigation-expand" class="ui-control" data-action="toggleExpand">
<i class="fa-solid fa-caret-down" inert></i>
</a>
{{/if}}
<menu id="scene-navigation-active" class="scene-navigation-menu flexcol">
{{#each scenes.active as |scene|}}
<li class="scene-wrapper">
<div class="ui-control scene {{scene.cssClass}}" data-scene-id="{{scene.id}}" data-action="viewScene" {{#if scene.tooltip}}data-tooltip-text="{{scene.tooltip}}"{{/if}}>
<span class="scene-name ellipsis">{{scene.name}}</span>
{{#if scene.users}}
<ul class="scene-players">
{{#each scene.users as |user|}}
<li class="scene-player" style="--color-bg:{{user.color}}; --color-border:{{user.border}}"
data-tooltip aria-label="{{user.name}}">{{user.letter}}</li>
{{/each}}
</ul>
{{/if}}
</div>
{{#if scene.hasEnvironments}}
<button class="ui-control scene-environment {{#if (gt scene.environments.length 1)}}many-environments{{/if}}" data-action="openSceneEnvironment" data-scene-id="{{scene.id}}"><img src="{{scene.environmentImage}}" /> </button>
{{/if}}
</li>
{{/each}}
</menu>
<menu id="scene-navigation-inactive" class="scene-navigation-menu flexcol">
{{#each scenes.inactive as |scene|}}
<li class="scene-wrapper">
<div class="ui-control scene {{scene.cssClass}}" data-scene-id="{{scene.id}}" data-action="viewScene" {{#if scene.tooltip}}data-tooltip-text="{{scene.tooltip}}"{{/if}}>
<span class="scene-name ellipsis">{{scene.name}}</span>
</div>
</li>
{{/each}}
</menu>
</nav>