From 84b28bd9c90308ff357a94730ae6cebeefbd29d9 Mon Sep 17 00:00:00 2001 From: Nikhil Nagarajan Date: Sat, 27 Dec 2025 10:21:17 -0500 Subject: [PATCH 01/18] Schema definition for DHSummonAction --- lang/en.json | 3 ++- module/data/action/summonAction.mjs | 11 +++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lang/en.json b/lang/en.json index 828b2303..4b786683 100755 --- a/lang/en.json +++ b/lang/en.json @@ -69,7 +69,8 @@ }, "summon": { "name": "Summon", - "tooltip": "Create tokens in the scene." + "tooltip": "Create tokens in the scene.", + "error": "You do not have permission to summon tokens or there is no active scene." } }, "Config": { diff --git a/module/data/action/summonAction.mjs b/module/data/action/summonAction.mjs index b06f1d38..7f272c69 100644 --- a/module/data/action/summonAction.mjs +++ b/module/data/action/summonAction.mjs @@ -1,3 +1,4 @@ +import { ui } from '../../applications/_module.mjs'; import DHBaseAction from './baseAction.mjs'; export default class DHSummonAction extends DHBaseAction { @@ -5,12 +6,18 @@ export default class DHSummonAction extends DHBaseAction { const fields = foundry.data.fields; return { ...super.defineSchema(), - documentUUID: new fields.DocumentUUIDField({ type: 'Actor' }) + tokenArray: new fields.ArrayField(new fields.SchemaField({ + actorUUID: new fields.DocumentUUIDField({ type: 'Actor', required: true }), + count: new fields.NumberField({ required: true, default: 1, min:1 }) + }), { required: false }) }; } async trigger(event, ...args) { - if (!this.canSummon || !canvas.scene) return; + if (!this.canSummon || !canvas.scene){ + ui.notifications.error(game.i18n.localize("DAGGERHEART.ACTIONS.TYPES.summon.error")); + return; + } } get canSummon() { From b8fcd440c627ad2580a9b8127f14bd38ab245532 Mon Sep 17 00:00:00 2001 From: Nikhil Nagarajan Date: Sat, 27 Dec 2025 10:43:45 -0500 Subject: [PATCH 02/18] Will reimplement --- lang/en.json | 5 +++++ module/data/action/summonAction.mjs | 10 ++++++++++ templates/actionTypes/summon.hbs | 14 ++++++++++++++ 3 files changed, 29 insertions(+) create mode 100644 templates/actionTypes/summon.hbs diff --git a/lang/en.json b/lang/en.json index 4b786683..a000beca 100755 --- a/lang/en.json +++ b/lang/en.json @@ -121,6 +121,11 @@ }, "cost": { "stepTooltip": "+{step} per step" + }, + "summon":{ + "addSummonEntry": "Add Summon Entry", + "actorUUID": "Actor to Summon", + "count": "Count" } } }, diff --git a/module/data/action/summonAction.mjs b/module/data/action/summonAction.mjs index 7f272c69..246b043d 100644 --- a/module/data/action/summonAction.mjs +++ b/module/data/action/summonAction.mjs @@ -23,4 +23,14 @@ export default class DHSummonAction extends DHBaseAction { get canSummon() { return game.user.can('TOKEN_CREATE'); } + + //Accessor for summon manager for performing the summon action + get summonManager() { + return game.dh.summon; //incomplete implementation + } + + //Logic to perform the summon action - incomplete implementation + async _performAction(event, ...args) { + return this.summonManager.summonTokens(this.tokenArray); + } } diff --git a/templates/actionTypes/summon.hbs b/templates/actionTypes/summon.hbs new file mode 100644 index 00000000..3a079db5 --- /dev/null +++ b/templates/actionTypes/summon.hbs @@ -0,0 +1,14 @@ +
+ {{localize "DAGGERHEART.ACTIONS.TYPES.summon.name"}} +

{{localize "DAGGERHEART.ACTIONS.Settings.summonHint"}}

+
+ {{#each source.tokenArray}} +
+ {{formField ../fields.documentUUID label=DAGGERHEART.ACTIONS.Settings.summon.actorUUID name="tokenArray.{{@index}}.actorUUID" value=this.actorUUID documentType="Actor"}} + {{formField ../fields.number label=DAGGERHEART.ACTIONS.Settings.summon.count name="tokenArray.{{@index}}.count" value=this.count min=1}} + +
+ {{/each}} + +
+
\ No newline at end of file From 13753295411339e1de76b4aea2f5a73a9d271173 Mon Sep 17 00:00:00 2001 From: Nikhil Nagarajan Date: Mon, 5 Jan 2026 14:34:24 -0500 Subject: [PATCH 03/18] HBS idea formed. Need to recheck drag drop implementation --- lang/en.json | 3 +- .../sheets-configs/action-base-config.mjs | 2 +- .../sheets-configs/action-settings-config.mjs | 61 +++++++++++++++++++ module/data/action/summonAction.mjs | 31 ++++++++-- module/systemRegistration/handlebars.mjs | 1 + templates/actionTypes/summon.hbs | 53 ++++++++++++---- .../action-settings/configuration.hbs | 3 +- 7 files changed, 133 insertions(+), 21 deletions(-) diff --git a/lang/en.json b/lang/en.json index a000beca..fa0efe8b 100755 --- a/lang/en.json +++ b/lang/en.json @@ -125,7 +125,8 @@ "summon":{ "addSummonEntry": "Add Summon Entry", "actorUUID": "Actor to Summon", - "count": "Count" + "count": "Count", + "hint": "Add Actor(s) and the quantity to summon under this action." } } }, diff --git a/module/applications/sheets-configs/action-base-config.mjs b/module/applications/sheets-configs/action-base-config.mjs index 96790a5b..6a23f538 100644 --- a/module/applications/sheets-configs/action-base-config.mjs +++ b/module/applications/sheets-configs/action-base-config.mjs @@ -85,7 +85,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) } }; - static CLEAN_ARRAYS = ['damage.parts', 'cost', 'effects']; + static CLEAN_ARRAYS = ['damage.parts', 'cost', 'effects','summon']; _getTabs(tabs) { for (const v of Object.values(tabs)) { diff --git a/module/applications/sheets-configs/action-settings-config.mjs b/module/applications/sheets-configs/action-settings-config.mjs index 91b85802..47168f93 100644 --- a/module/applications/sheets-configs/action-settings-config.mjs +++ b/module/applications/sheets-configs/action-settings-config.mjs @@ -6,10 +6,13 @@ export default class DHActionSettingsConfig extends DHActionBaseConfig { this.effects = effects; this.sheetUpdate = sheetUpdate; + + this._dragDrop = this._createDragDropHandlers(); } static DEFAULT_OPTIONS = { ...DHActionBaseConfig.DEFAULT_OPTIONS, + dragDrop: [{ dragSelector: null, dropSelector: '.summon-actor-drop' }], actions: { ...DHActionBaseConfig.DEFAULT_OPTIONS.actions, addEffect: this.addEffect, @@ -18,8 +21,23 @@ export default class DHActionSettingsConfig extends DHActionBaseConfig { } }; + _createDragDropHandlers() { + return this.options.dragDrop.map(d => { + d.callbacks = { + drop: this._onDrop.bind(this) + }; + return new foundry.applications.ux.DragDrop.implementation(d); + }); + } + async _prepareContext(options) { const context = await super._prepareContext(options); + const summonData = this.action.summon || []; + context.summonActors = await Promise.all(summonData.map(async (entry) => { + if (!entry.actorUUID) return null; + const actor = await fromUuid(entry.actorUUID); + return actor ? { name: actor.name, img: actor.img } : { name: "Unknown", img: "icons/svg/mystery-man.svg" }; + })); context.effects = this.effects; context.getEffectDetails = this.getEffectDetails.bind(this); @@ -63,4 +81,47 @@ export default class DHActionSettingsConfig extends DHActionBaseConfig { this.effects = await this.sheetUpdate(this.action.toObject(), { ...updatedEffect, id }); this.render(); } + + //For drag drop implementation for summon actor selection + _onRender(context, options) { + super._onRender(context, options); + this._dragDrop.forEach(d => d.bind(this.element)); + } + + async _onDrop(event) { + const data = TextEditor.getDragEventData(event); + console.log("Daggerheart | Summon Drop Data:", data); + + if (!data || !data.uuid) return; + + const doc = await fromUuid(data.uuid); + if (!doc) return; + + + let actorUuid = null; + + if (doc.documentName === "Actor") { + actorUuid = doc.uuid; + } else if (doc.documentName === "Token" && doc.actor) { + actorUuid = doc.actor.uuid; + } else { + console.warn("Daggerheart | Dropped document is not an Actor:", doc); + return; + } + + const dropZone = event.target.closest('.summon-actor-drop'); + if (!dropZone) return; + + const index = Number(dropZone.dataset.index); + + const actionData = this.action.toObject(); + if (!actionData.summon) actionData.summon = []; + + if (actionData.summon[index]) { + actionData.summon[index].actorUUID = actorUuid; + + // Trigger update + this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(actionData) }); + } + } } diff --git a/module/data/action/summonAction.mjs b/module/data/action/summonAction.mjs index 246b043d..c90c3030 100644 --- a/module/data/action/summonAction.mjs +++ b/module/data/action/summonAction.mjs @@ -6,18 +6,25 @@ export default class DHSummonAction extends DHBaseAction { const fields = foundry.data.fields; return { ...super.defineSchema(), - tokenArray: new fields.ArrayField(new fields.SchemaField({ + summon: new fields.ArrayField(new fields.SchemaField({ actorUUID: new fields.DocumentUUIDField({ type: 'Actor', required: true }), - count: new fields.NumberField({ required: true, default: 1, min:1 }) - }), { required: false }) + count: new fields.NumberField({ required: true, default: 1, min: 1, integer: true }) + }), { required: false, initial: [] }) + }; + } + + get defaultValues() { + return { + summon: { actorUUID: "", count: 1 } }; } async trigger(event, ...args) { if (!this.canSummon || !canvas.scene){ - ui.notifications.error(game.i18n.localize("DAGGERHEART.ACTIONS.TYPES.summon.error")); + ui.notifications.warn(game.i18n.localize("DAGGERHEART.ACTIONS.TYPES.summon.error")); return; } + await this._performAction(); } get canSummon() { @@ -31,6 +38,18 @@ export default class DHSummonAction extends DHBaseAction { //Logic to perform the summon action - incomplete implementation async _performAction(event, ...args) { - return this.summonManager.summonTokens(this.tokenArray); - } + const validSummons = this.summon.filter(entry => entry.actorUUID); + if (validSummons.length === 0) { + ui.notifications.warn("No actors configured for this Summon action."); + return; + } + // FOR NOW: Just log the data to prove the schema working or not + console.group("Summon Action Triggered"); + + for (const entry of validSummons) { + const actor = await fromUuid(entry.actorUUID); + console.log(`- Ready to summon ${entry.count}x [${actor?.name || "Unknown Actor"}]`); + } + console.groupEnd(); + } } diff --git a/module/systemRegistration/handlebars.mjs b/module/systemRegistration/handlebars.mjs index 32e047fd..97769181 100644 --- a/module/systemRegistration/handlebars.mjs +++ b/module/systemRegistration/handlebars.mjs @@ -32,6 +32,7 @@ export const preloadHandlebarsTemplates = async function () { 'systems/daggerheart/templates/actionTypes/effect.hbs', 'systems/daggerheart/templates/actionTypes/beastform.hbs', 'systems/daggerheart/templates/actionTypes/countdown.hbs', + 'systems/daggerheart/templates/actionTypes/summon.hbs', 'systems/daggerheart/templates/settings/components/settings-item-line.hbs', 'systems/daggerheart/templates/ui/tooltip/parts/tooltipChips.hbs', 'systems/daggerheart/templates/ui/tooltip/parts/tooltipTags.hbs', diff --git a/templates/actionTypes/summon.hbs b/templates/actionTypes/summon.hbs index 3a079db5..aabe094d 100644 --- a/templates/actionTypes/summon.hbs +++ b/templates/actionTypes/summon.hbs @@ -1,14 +1,43 @@ -
- {{localize "DAGGERHEART.ACTIONS.TYPES.summon.name"}} -

{{localize "DAGGERHEART.ACTIONS.Settings.summonHint"}}

-
- {{#each source.tokenArray}} -
- {{formField ../fields.documentUUID label=DAGGERHEART.ACTIONS.Settings.summon.actorUUID name="tokenArray.{{@index}}.actorUUID" value=this.actorUUID documentType="Actor"}} - {{formField ../fields.number label=DAGGERHEART.ACTIONS.Settings.summon.count name="tokenArray.{{@index}}.count" value=this.count min=1}} - +
+ + {{localize "DAGGERHEART.ACTIONS.TYPES.summon.name"}} + + +

{{localize "DAGGERHEART.ACTIONS.Settings.summon.hint"}}

+ {{#each source as |entry index|}} +
+ + {{!-- Actor --}} +
+ {{#if entry.actorUUID}} + {{!-- Filled State --}} + {{#with (lookup @root.summonActors index) as |actor|}} +
+ + {{actor.name}} + {{!-- Hidden input to store the actual value --}} + +
+ {{/with}} + {{else}} + {{!-- Empty State --}} +
+ {{localize "DAGGERHEART.GENERAL.missingDragDropThing" thing=(localize "Actor")}} + +
+ {{/if}}
- {{/each}} - -
+ + {{!-- Count --}} + {{formField ../fields.count + label="DAGGERHEART.ACTIONS.Settings.summon.count" + name=(concat "summon." index ".count") + value=entry.count + min=1 + localize=true}} + {{!-- Obtained idea from cost.hbs --}} + + +
+ {{/each}}
\ No newline at end of file diff --git a/templates/sheets-settings/action-settings/configuration.hbs b/templates/sheets-settings/action-settings/configuration.hbs index 51b2a72b..48b08ded 100644 --- a/templates/sheets-settings/action-settings/configuration.hbs +++ b/templates/sheets-settings/action-settings/configuration.hbs @@ -2,7 +2,8 @@ class="tab {{this.tabs.config.cssClass}}" data-group="primary" data-tab="config" -> +> + {{#if fields.summon}}{{> 'systems/daggerheart/templates/actionTypes/summon.hbs' fields=fields.summon.element.fields source=source.summon}}{{/if}} {{> 'systems/daggerheart/templates/actionTypes/uses.hbs' fields=fields.uses.fields source=source.uses}} {{> 'systems/daggerheart/templates/actionTypes/cost.hbs' fields=fields.cost.element.fields source=source.cost costOptions=costOptions}} {{> 'systems/daggerheart/templates/actionTypes/range-target.hbs' fields=(object range=fields.range target=fields.target.fields) source=(object target=source.target range=source.range)}} From ad1dee313fdd5981df95340e6decbb05c4640a33 Mon Sep 17 00:00:00 2001 From: Nikhil Nagarajan Date: Tue, 6 Jan 2026 11:46:38 -0500 Subject: [PATCH 04/18] Tried to refine drag drop --- lang/en.json | 4 +- .../sheets-configs/action-base-config.mjs | 14 ++++- .../sheets-configs/action-settings-config.mjs | 61 ------------------- module/data/action/summonAction.mjs | 1 - templates/actionTypes/summon.hbs | 34 +---------- 5 files changed, 18 insertions(+), 96 deletions(-) diff --git a/lang/en.json b/lang/en.json index fa0efe8b..7f359f09 100755 --- a/lang/en.json +++ b/lang/en.json @@ -70,7 +70,8 @@ "summon": { "name": "Summon", "tooltip": "Create tokens in the scene.", - "error": "You do not have permission to summon tokens or there is no active scene." + "error": "You do not have permission to summon tokens or there is no active scene.", + "invalidDrop": "You can only drop Actor entities to summon." } }, "Config": { @@ -125,6 +126,7 @@ "summon":{ "addSummonEntry": "Add Summon Entry", "actorUUID": "Actor to Summon", + "actor": "Actor", "count": "Count", "hint": "Add Actor(s) and the quantity to summon under this action." } diff --git a/module/applications/sheets-configs/action-base-config.mjs b/module/applications/sheets-configs/action-base-config.mjs index 6a23f538..a2a08c39 100644 --- a/module/applications/sheets-configs/action-base-config.mjs +++ b/module/applications/sheets-configs/action-base-config.mjs @@ -35,7 +35,8 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) handler: this.updateForm, submitOnChange: true, closeOnSubmit: false - } + }, + dragDrop: [{ dragSelector: null, dropSelector: '.summon-actor-drop'}] }; static PARTS = { @@ -233,4 +234,15 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) this.tabGroups.primary = 'base'; await super.close(options); } + + /** Implementation for dragdrop for summon actor selection **/ + async _onDrop(event) { + const data = foundry.applications.ux.TextEditor.getDragEventData(event); + const item=await foundry.utils.fromUuid(data.uuid); + if (!(item instanceof game.system.api.documents.DhpActor)) { + ui.notifications.warn(game.i18n.localize("DAGGERHEART.ACTIONS.TYPES.summon.invalidDrop")); + return; + } + } + } diff --git a/module/applications/sheets-configs/action-settings-config.mjs b/module/applications/sheets-configs/action-settings-config.mjs index 47168f93..91b85802 100644 --- a/module/applications/sheets-configs/action-settings-config.mjs +++ b/module/applications/sheets-configs/action-settings-config.mjs @@ -6,13 +6,10 @@ export default class DHActionSettingsConfig extends DHActionBaseConfig { this.effects = effects; this.sheetUpdate = sheetUpdate; - - this._dragDrop = this._createDragDropHandlers(); } static DEFAULT_OPTIONS = { ...DHActionBaseConfig.DEFAULT_OPTIONS, - dragDrop: [{ dragSelector: null, dropSelector: '.summon-actor-drop' }], actions: { ...DHActionBaseConfig.DEFAULT_OPTIONS.actions, addEffect: this.addEffect, @@ -21,23 +18,8 @@ export default class DHActionSettingsConfig extends DHActionBaseConfig { } }; - _createDragDropHandlers() { - return this.options.dragDrop.map(d => { - d.callbacks = { - drop: this._onDrop.bind(this) - }; - return new foundry.applications.ux.DragDrop.implementation(d); - }); - } - async _prepareContext(options) { const context = await super._prepareContext(options); - const summonData = this.action.summon || []; - context.summonActors = await Promise.all(summonData.map(async (entry) => { - if (!entry.actorUUID) return null; - const actor = await fromUuid(entry.actorUUID); - return actor ? { name: actor.name, img: actor.img } : { name: "Unknown", img: "icons/svg/mystery-man.svg" }; - })); context.effects = this.effects; context.getEffectDetails = this.getEffectDetails.bind(this); @@ -81,47 +63,4 @@ export default class DHActionSettingsConfig extends DHActionBaseConfig { this.effects = await this.sheetUpdate(this.action.toObject(), { ...updatedEffect, id }); this.render(); } - - //For drag drop implementation for summon actor selection - _onRender(context, options) { - super._onRender(context, options); - this._dragDrop.forEach(d => d.bind(this.element)); - } - - async _onDrop(event) { - const data = TextEditor.getDragEventData(event); - console.log("Daggerheart | Summon Drop Data:", data); - - if (!data || !data.uuid) return; - - const doc = await fromUuid(data.uuid); - if (!doc) return; - - - let actorUuid = null; - - if (doc.documentName === "Actor") { - actorUuid = doc.uuid; - } else if (doc.documentName === "Token" && doc.actor) { - actorUuid = doc.actor.uuid; - } else { - console.warn("Daggerheart | Dropped document is not an Actor:", doc); - return; - } - - const dropZone = event.target.closest('.summon-actor-drop'); - if (!dropZone) return; - - const index = Number(dropZone.dataset.index); - - const actionData = this.action.toObject(); - if (!actionData.summon) actionData.summon = []; - - if (actionData.summon[index]) { - actionData.summon[index].actorUUID = actorUuid; - - // Trigger update - this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(actionData) }); - } - } } diff --git a/module/data/action/summonAction.mjs b/module/data/action/summonAction.mjs index c90c3030..56e0446c 100644 --- a/module/data/action/summonAction.mjs +++ b/module/data/action/summonAction.mjs @@ -1,4 +1,3 @@ -import { ui } from '../../applications/_module.mjs'; import DHBaseAction from './baseAction.mjs'; export default class DHSummonAction extends DHBaseAction { diff --git a/templates/actionTypes/summon.hbs b/templates/actionTypes/summon.hbs index aabe094d..f32486be 100644 --- a/templates/actionTypes/summon.hbs +++ b/templates/actionTypes/summon.hbs @@ -6,38 +6,8 @@

{{localize "DAGGERHEART.ACTIONS.Settings.summon.hint"}}

{{#each source as |entry index|}}
- - {{!-- Actor --}} -
- {{#if entry.actorUUID}} - {{!-- Filled State --}} - {{#with (lookup @root.summonActors index) as |actor|}} -
- - {{actor.name}} - {{!-- Hidden input to store the actual value --}} - -
- {{/with}} - {{else}} - {{!-- Empty State --}} -
- {{localize "DAGGERHEART.GENERAL.missingDragDropThing" thing=(localize "Actor")}} - -
- {{/if}} -
- - {{!-- Count --}} - {{formField ../fields.count - label="DAGGERHEART.ACTIONS.Settings.summon.count" - name=(concat "summon." index ".count") - value=entry.count - min=1 - localize=true}} - {{!-- Obtained idea from cost.hbs --}} - - + {{formField ../fields.actorUUID label="DAGGERHEART.ACTIONS.Settings.summon.actor" value=entry.actorUUID name=(concat "summon." index ".actorUUID") localize=true classes="summon-actor-drop"}} + {{formField ../fields.count label="DAGGERHEART.ACTIONS.Settings.summon.count" value=entry.count name=(concat "summon." index ".count") localize=true}}
{{/each}} \ No newline at end of file From e07d0204b232a8bf7911deb629dea1ede2bfbce2 Mon Sep 17 00:00:00 2001 From: Nikhil Nagarajan Date: Tue, 6 Jan 2026 14:04:48 -0500 Subject: [PATCH 05/18] drag drop implemented (css tbd) --- .../sheets-configs/action-base-config.mjs | 20 ++++++++- styles/less/global/global.less | 20 +++++++++ templates/actionTypes/summon.hbs | 44 +++++++++++++++++-- 3 files changed, 80 insertions(+), 4 deletions(-) diff --git a/module/applications/sheets-configs/action-base-config.mjs b/module/applications/sheets-configs/action-base-config.mjs index a2a08c39..c75bc585 100644 --- a/module/applications/sheets-configs/action-base-config.mjs +++ b/module/applications/sheets-configs/action-base-config.mjs @@ -36,7 +36,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) submitOnChange: true, closeOnSubmit: false }, - dragDrop: [{ dragSelector: null, dropSelector: '.summon-actor-drop'}] + dragDrop: [{ dragSelector: null, dropSelector: '.summon-entry', handlers: ['_onDrop'] }] }; static PARTS = { @@ -100,6 +100,20 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) async _prepareContext(_options) { const context = await super._prepareContext(_options, 'action'); context.source = this.action.toObject(true); + // Resolving summon entries so actions can read entry.name / entry.img / entry.uuid + if (Array.isArray(context.source.summon)) { + context.source.summon = await Promise.all(context.source.summon.map(async entry => { + if (!entry) return entry; + const uuid = entry.actorUUID ?? entry.uuid; + entry.uuid = uuid; + try { + const doc = await foundry.utils.fromUuid(uuid); + entry.name = entry.name ?? doc?.name; + entry.img = entry.img ?? (doc?.img ?? doc?.prototypeToken?.texture?.src ?? null); + } catch (_) {} + return entry; + })); + } context.openSection = this.openSection; context.tabs = this._getTabs(this.constructor.TABS); context.config = CONFIG.DH; @@ -243,6 +257,10 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) ui.notifications.warn(game.i18n.localize("DAGGERHEART.ACTIONS.TYPES.summon.invalidDrop")); return; } + //Add to summon array + const actionData = this.action.toObject(); // Get current action data + actionData.summon.push({ actorUUID: data.uuid, count: 1 });// Add new summon entry + await this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(actionData) }); // Update the form with new data } } diff --git a/styles/less/global/global.less b/styles/less/global/global.less index 6cc63c2a..8abaddee 100644 --- a/styles/less/global/global.less +++ b/styles/less/global/global.less @@ -9,6 +9,26 @@ border-radius: 3px; color: light-dark(@dark-blue-50, @beige-50); font-family: @font-body; + &.summon-actor-drop { + height: fit-content; + min-height: inherit; + display: flex; + justify-content: center; + .actors-list.summon-entry .actor-summon-item .actor-summon-line { + display: flex; + justify-content: flex-end; + align-items: center; + .image{ + max-width: 10%; + } + .h4{ + width: --webkit-fill-available; + } + .controls.effect-control{ + padding:3px; + } + } + } } .daggerheart.dh-style { diff --git a/templates/actionTypes/summon.hbs b/templates/actionTypes/summon.hbs index f32486be..278037fb 100644 --- a/templates/actionTypes/summon.hbs +++ b/templates/actionTypes/summon.hbs @@ -1,13 +1,51 @@
{{localize "DAGGERHEART.ACTIONS.TYPES.summon.name"}} -

{{localize "DAGGERHEART.ACTIONS.Settings.summon.hint"}}

- {{#each source as |entry index|}} + {{!-- {{#each source as |entry index|}}
{{formField ../fields.actorUUID label="DAGGERHEART.ACTIONS.Settings.summon.actor" value=entry.actorUUID name=(concat "summon." index ".actorUUID") localize=true classes="summon-actor-drop"}} {{formField ../fields.count label="DAGGERHEART.ACTIONS.Settings.summon.count" value=entry.count name=(concat "summon." index ".count") localize=true}}
- {{/each}} + {{/each}} --}} + +
+
+ + {{#each source as |entry index|}} +
    +
    + +

    + {{entry.name}} +

    + {{formField ../fields.count label="DAGGERHEART.ACTIONS.Settings.summon.count" value=entry.count name=(concat "summon." index ".count") localize=true}} + +
    +
+ {{/each}} + {{#unless source.length}} + {{localize "DAGGERHEART.GENERAL.dropActorsHere"}} + {{/unless}} +
+
\ No newline at end of file From 6cbe75355295671c15382e7da9eedb4b82611547 Mon Sep 17 00:00:00 2001 From: Nikhil Nagarajan Date: Tue, 6 Jan 2026 14:49:23 -0500 Subject: [PATCH 06/18] phase 1 complete --- .../sheets-configs/action-base-config.mjs | 40 +++++++++++++++++-- templates/actionTypes/summon.hbs | 1 + .../action-settings/configuration.hbs | 1 - .../action-settings/effect.hbs | 1 + 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/module/applications/sheets-configs/action-base-config.mjs b/module/applications/sheets-configs/action-base-config.mjs index c75bc585..76965275 100644 --- a/module/applications/sheets-configs/action-base-config.mjs +++ b/module/applications/sheets-configs/action-base-config.mjs @@ -29,7 +29,8 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) removeElement: this.removeElement, editEffect: this.editEffect, addDamage: this.addDamage, - removeDamage: this.removeDamage + removeDamage: this.removeDamage, + editDoc: this.editDoc }, form: { handler: this.updateForm, @@ -216,12 +217,33 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) static removeElement(event, button) { event.stopPropagation(); const data = this.action.toObject(), - key = event.target.closest('[data-key]').dataset.key, - index = button.dataset.index; + key = event.target.closest('[data-key]').dataset.key; + + // Prefer explicit index, otherwise find by uuid + let index = button?.dataset.index; + if (index === undefined || index === null || index === '') { + const uuid = button?.dataset.uuid ?? button?.dataset.itemUuid; + index = data[key].findIndex(e => (e?.actorUUID ?? e?.uuid) === uuid); + if (index === -1) return; + } else index = Number(index); + data[key].splice(index, 1); this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); } + static async editDoc(event, button) { + event.stopPropagation(); + const uuid = button?.dataset.itemUuid ?? button?.dataset.uuid; // Obtain uuid from dataset + if (!uuid) return; + //Try catching errors + try { + const doc = await foundry.utils.fromUuid(uuid); + if (doc?.sheet) return doc.sheet.render({ force: true }); + } catch (err) { + console.warn("editDoc action failed for", uuid, err); + } + } + static addDamage(_event) { if (!this.action.damage.parts) return; const data = this.action.toObject(), @@ -259,7 +281,17 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) } //Add to summon array const actionData = this.action.toObject(); // Get current action data - actionData.summon.push({ actorUUID: data.uuid, count: 1 });// Add new summon entry + //checking to see if actor is already in summon list add 1 to count instead of adding new entry + let countvalue = 1; + for (const entry of actionData.summon) { + if (entry.actorUUID === data.uuid) { + entry.count += 1; + countvalue = entry.count; + await this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(actionData) }); + return; + } + } + actionData.summon.push({ actorUUID: data.uuid, count: countvalue });// Add new summon entry await this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(actionData) }); // Update the form with new data } diff --git a/templates/actionTypes/summon.hbs b/templates/actionTypes/summon.hbs index 278037fb..d313fb4c 100644 --- a/templates/actionTypes/summon.hbs +++ b/templates/actionTypes/summon.hbs @@ -35,6 +35,7 @@ data-action='removeElement' data-target="summon" data-uuid="{{entry.uuid}}" + data-index="{{index}}" data-tooltip='{{localize "CONTROLS.CommonDelete"}}' > diff --git a/templates/sheets-settings/action-settings/configuration.hbs b/templates/sheets-settings/action-settings/configuration.hbs index 48b08ded..5bd29e39 100644 --- a/templates/sheets-settings/action-settings/configuration.hbs +++ b/templates/sheets-settings/action-settings/configuration.hbs @@ -3,7 +3,6 @@ data-group="primary" data-tab="config" > - {{#if fields.summon}}{{> 'systems/daggerheart/templates/actionTypes/summon.hbs' fields=fields.summon.element.fields source=source.summon}}{{/if}} {{> 'systems/daggerheart/templates/actionTypes/uses.hbs' fields=fields.uses.fields source=source.uses}} {{> 'systems/daggerheart/templates/actionTypes/cost.hbs' fields=fields.cost.element.fields source=source.cost costOptions=costOptions}} {{> 'systems/daggerheart/templates/actionTypes/range-target.hbs' fields=(object range=fields.range target=fields.target.fields) source=(object target=source.target range=source.range)}} diff --git a/templates/sheets-settings/action-settings/effect.hbs b/templates/sheets-settings/action-settings/effect.hbs index bf2f3aa1..e94f4328 100644 --- a/templates/sheets-settings/action-settings/effect.hbs +++ b/templates/sheets-settings/action-settings/effect.hbs @@ -9,5 +9,6 @@ {{#if fields.macro}}{{> 'systems/daggerheart/templates/actionTypes/macro.hbs' fields=fields.macro source=source.macro}}{{/if}} {{#if fields.effects}}{{> 'systems/daggerheart/templates/actionTypes/effect.hbs' fields=fields.effects.element.fields source=source.effects}}{{/if}} {{#if fields.beastform}}{{> 'systems/daggerheart/templates/actionTypes/beastform.hbs' fields=fields.beastform.fields source=source.beastform}}{{/if}} + {{#if fields.summon}}{{> 'systems/daggerheart/templates/actionTypes/summon.hbs' fields=fields.summon.element.fields source=source.summon}}{{/if}} {{#if fields.countdown}}{{> 'systems/daggerheart/templates/actionTypes/countdown.hbs' fields=fields.countdown.element.fields source=source.countdown}}{{/if}} \ No newline at end of file From e83202681ecddec76a6dbd33bf098fb9169f176e Mon Sep 17 00:00:00 2001 From: Nikhil Nagarajan Date: Tue, 6 Jan 2026 18:15:27 -0500 Subject: [PATCH 07/18] tbd work on summon action type --- module/applications/dialogs/summonDialog.mjs | 48 +++++++++++++++ module/data/fields/action/_module.mjs | 1 + module/data/fields/action/summonField.mjs | 61 ++++++++++++++++++++ templates/dialogs/summon/summonDialog.hbs | 24 ++++++++ 4 files changed, 134 insertions(+) create mode 100644 module/applications/dialogs/summonDialog.mjs create mode 100644 module/data/fields/action/summonField.mjs create mode 100644 templates/dialogs/summon/summonDialog.hbs diff --git a/module/applications/dialogs/summonDialog.mjs b/module/applications/dialogs/summonDialog.mjs new file mode 100644 index 00000000..c5ab7a3d --- /dev/null +++ b/module/applications/dialogs/summonDialog.mjs @@ -0,0 +1,48 @@ +const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; + +export default class DHSummonDialog extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(summonData) { + super(summonData); + // Initialize summons and index + this.summons = summonData.summons || []; + this.index = 0; + } + + get_title() { + return game.i18n.localize("DAGGERHEART.DIALOGS.SUMMON.title"); + } + + static DEFAULT_OPTIONS= { + ...super.DEFAULT_OPTIONS, + template: 'systems/daggerheart/module/applications/dialogs/summon/summonDialog.hbs', + width: 400, + height: 'auto', + classes: ['daggerheart', 'dialog', 'summon-dialog'], + dragDrop: [{ dragSelector: '.summon-token', dropSelector: null, handler:'onDrop'}] + }; + + async _prepareContext() { + const context = await super._prepareContext(); + context.summons=this.summons; + context.index=this.index; + return context; + } + + getData(options={}) { + const data = super.getData(options); + data.summons=this.summons; + data.index=this.index; + return data; + } + async prepareContext() { + const context = await super.prepareContext(); + return context; + } + + async onDrop(event) {//add to canvas + event.preventDefault(); + const tokenData = JSON.parse(event.dataTransfer.getData('text/plain')); + const position = { x: event.clientX, y: event.clientY }; + await canvas.scene.createEmbeddedDocuments("Token", [tokenData], { temporary: false, x: position.x, y: position.y }); + } +} \ No newline at end of file diff --git a/module/data/fields/action/_module.mjs b/module/data/fields/action/_module.mjs index ef69394a..0bdffca2 100644 --- a/module/data/fields/action/_module.mjs +++ b/module/data/fields/action/_module.mjs @@ -9,3 +9,4 @@ export { default as BeastformField } from './beastformField.mjs'; export { default as DamageField } from './damageField.mjs'; export { default as RollField } from './rollField.mjs'; export { default as MacroField } from './macroField.mjs'; +export { default as SummonField } from './summonField.mjs'; diff --git a/module/data/fields/action/summonField.mjs b/module/data/fields/action/summonField.mjs new file mode 100644 index 00000000..7bcd1145 --- /dev/null +++ b/module/data/fields/action/summonField.mjs @@ -0,0 +1,61 @@ +import DHSummonDialog from '../../../applications/dialogs/summonDialog.mjs'; + +const fields = foundry.data.fields; + +export default class DHSummonField extends fields.SchemaField { + /** + * Action Workflow order + */ + static order = 120; + + constructor(options = {}, context = {}) { + const summonFields = { + summon: new fields.ArrayField(new fields.SchemaField({ + actorUUID: new fields.DocumentUUIDField({ + type: 'Actor', + required: true }), + count: new fields.NumberField({ + required: true, + default: 1, + min: 1, + integer: true }) + }), { required: false, initial: [] }) + }; + super(summonFields, options, context); + } + + /** + * Summon Action Workflow part. + * Must be called within Action context or similar. + * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. + */ + static async execute(config) { + const selected = await DHSummonDialog.configure(config, this.item); + if (!selected) return false; + return await DHSummonField.summon.call(this, selected); + } + /** + * Update Action Workflow config object. + * Must be called within Action context. + * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. + */ + prepareConfig(config) { + if (!canvas.scene){ + ui.notifications.warn(game.i18n.localize("DAGGERHEART.ACTIONS.TYPES.summon.error")); + return false; + } + return true; + } + /** + * Logic to perform the summon action - incomplete implementation + */ + + get defaultValues() { + return { + summon: { actorUUID: "", count: 1 } + }; + } + get canSummon() { + return game.user.can('TOKEN_CREATE'); + } +} \ No newline at end of file diff --git a/templates/dialogs/summon/summonDialog.hbs b/templates/dialogs/summon/summonDialog.hbs new file mode 100644 index 00000000..9e03e129 --- /dev/null +++ b/templates/dialogs/summon/summonDialog.hbs @@ -0,0 +1,24 @@ +
+ + {{localize "DAGGERHEART.DIALOGS.Summon.title"}} + +

{{localize "DAGGERHEART.DIALOGS.Summon.hint"}}

+ +
+ {{#each summons as |entry index|}} +
    +
    + + {{#if (gte entry.count 1)}} +

    + {{entry.name}} (x{{entry.count}}) +

    + {{else}} +

    + {{entry.name}} +

    + {{/if}} +
    +
+ {{/each}} +
\ No newline at end of file From e22928b59ec6b7e3408858c10f0b8eab351e4c0f Mon Sep 17 00:00:00 2001 From: Nikhil Nagarajan Date: Wed, 7 Jan 2026 13:03:22 -0500 Subject: [PATCH 08/18] Improved Schema and now it works --- module/data/action/summonAction.mjs | 24 +++----- module/data/fields/action/summonField.mjs | 75 ++++++++--------------- 2 files changed, 36 insertions(+), 63 deletions(-) diff --git a/module/data/action/summonAction.mjs b/module/data/action/summonAction.mjs index 56e0446c..68f93510 100644 --- a/module/data/action/summonAction.mjs +++ b/module/data/action/summonAction.mjs @@ -1,20 +1,20 @@ +import DHSummonField from '../fields/action/summonField.mjs'; import DHBaseAction from './baseAction.mjs'; +import DHSummonDialog from '../../applications/dialogs/summonDialog.mjs'; export default class DHSummonAction extends DHBaseAction { static defineSchema() { - const fields = foundry.data.fields; return { ...super.defineSchema(), - summon: new fields.ArrayField(new fields.SchemaField({ - actorUUID: new fields.DocumentUUIDField({ type: 'Actor', required: true }), - count: new fields.NumberField({ required: true, default: 1, min: 1, integer: true }) - }), { required: false, initial: [] }) + summon: new DHSummonField({ required: false, initial: [] }) }; } + static extraSchemas=[...super.extraSchemas,'summon']; + get defaultValues() { return { - summon: { actorUUID: "", count: 1 } + summon: [] }; } @@ -30,12 +30,6 @@ export default class DHSummonAction extends DHBaseAction { return game.user.can('TOKEN_CREATE'); } - //Accessor for summon manager for performing the summon action - get summonManager() { - return game.dh.summon; //incomplete implementation - } - - //Logic to perform the summon action - incomplete implementation async _performAction(event, ...args) { const validSummons = this.summon.filter(entry => entry.actorUUID); if (validSummons.length === 0) { @@ -50,5 +44,7 @@ export default class DHSummonAction extends DHBaseAction { console.log(`- Ready to summon ${entry.count}x [${actor?.name || "Unknown Actor"}]`); } console.groupEnd(); - } -} + + //Todo: Open Summon Dialog + } +} \ No newline at end of file diff --git a/module/data/fields/action/summonField.mjs b/module/data/fields/action/summonField.mjs index 7bcd1145..1abb31f4 100644 --- a/module/data/fields/action/summonField.mjs +++ b/module/data/fields/action/summonField.mjs @@ -1,61 +1,38 @@ -import DHSummonDialog from '../../../applications/dialogs/summonDialog.mjs'; - const fields = foundry.data.fields; -export default class DHSummonField extends fields.SchemaField { +export default class DHSummonField extends fields.ArrayField { /** * Action Workflow order */ static order = 120; constructor(options = {}, context = {}) { - const summonFields = { - summon: new fields.ArrayField(new fields.SchemaField({ - actorUUID: new fields.DocumentUUIDField({ - type: 'Actor', - required: true }), - count: new fields.NumberField({ - required: true, - default: 1, - min: 1, - integer: true }) - }), { required: false, initial: [] }) - }; + const summonFields = new fields.SchemaField({ + actorUUID: new fields.DocumentUUIDField({ + type: 'Actor', + required: true + }), + count: new fields.NumberField({ + required: true, + default: 1, + min: 1, + integer: true + }) + }) super(summonFields, options, context); + + // summon: new fields.ArrayField(new fields.SchemaField({ + // actorUUID: new fields.DocumentUUIDField({ + // type: 'Actor', + // required: true }), + // count: new fields.NumberField({ + // required: true, + // default: 1, + // min: 1, + // integer: true }) + // }), { required: false, initial: [] }) + // }; + // super(summonFields, options, context); } - /** - * Summon Action Workflow part. - * Must be called within Action context or similar. - * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. - */ - static async execute(config) { - const selected = await DHSummonDialog.configure(config, this.item); - if (!selected) return false; - return await DHSummonField.summon.call(this, selected); - } - /** - * Update Action Workflow config object. - * Must be called within Action context. - * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. - */ - prepareConfig(config) { - if (!canvas.scene){ - ui.notifications.warn(game.i18n.localize("DAGGERHEART.ACTIONS.TYPES.summon.error")); - return false; - } - return true; - } - /** - * Logic to perform the summon action - incomplete implementation - */ - - get defaultValues() { - return { - summon: { actorUUID: "", count: 1 } - }; - } - get canSummon() { - return game.user.can('TOKEN_CREATE'); - } } \ No newline at end of file From f5e00dedb2265a18df756c95ecc9b0c7a4eed540 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Wed, 7 Jan 2026 19:14:17 +0100 Subject: [PATCH 09/18] . --- module/data/action/summonAction.mjs | 73 ++++++++++------------- module/data/fields/action/summonField.mjs | 40 +++++-------- 2 files changed, 47 insertions(+), 66 deletions(-) diff --git a/module/data/action/summonAction.mjs b/module/data/action/summonAction.mjs index 68f93510..d03c766b 100644 --- a/module/data/action/summonAction.mjs +++ b/module/data/action/summonAction.mjs @@ -1,50 +1,41 @@ -import DHSummonField from '../fields/action/summonField.mjs'; import DHBaseAction from './baseAction.mjs'; -import DHSummonDialog from '../../applications/dialogs/summonDialog.mjs'; export default class DHSummonAction extends DHBaseAction { - static defineSchema() { - return { - ...super.defineSchema(), - summon: new DHSummonField({ required: false, initial: [] }) - }; - } + static extraSchemas = [...super.extraSchemas, 'summon']; - static extraSchemas=[...super.extraSchemas,'summon']; + // get defaultValues() { + // return { + // summon: [] + // }; + // } - get defaultValues() { - return { - summon: [] - }; - } + // async trigger(event, ...args) { + // if (!this.canSummon || !canvas.scene){ + // ui.notifications.warn(game.i18n.localize("DAGGERHEART.ACTIONS.TYPES.summon.error")); + // return; + // } + // await this._performAction(); + // } - async trigger(event, ...args) { - if (!this.canSummon || !canvas.scene){ - ui.notifications.warn(game.i18n.localize("DAGGERHEART.ACTIONS.TYPES.summon.error")); - return; - } - await this._performAction(); - } + // get canSummon() { + // return game.user.can('TOKEN_CREATE'); + // } - get canSummon() { - return game.user.can('TOKEN_CREATE'); - } + // async _performAction(event, ...args) { + // const validSummons = this.summon.filter(entry => entry.actorUUID); + // if (validSummons.length === 0) { + // ui.notifications.warn("No actors configured for this Summon action."); + // return; + // } + // // FOR NOW: Just log the data to prove the schema working or not + // console.group("Summon Action Triggered"); - async _performAction(event, ...args) { - const validSummons = this.summon.filter(entry => entry.actorUUID); - if (validSummons.length === 0) { - ui.notifications.warn("No actors configured for this Summon action."); - return; - } - // FOR NOW: Just log the data to prove the schema working or not - console.group("Summon Action Triggered"); + // for (const entry of validSummons) { + // const actor = await fromUuid(entry.actorUUID); + // console.log(`- Ready to summon ${entry.count}x [${actor?.name || "Unknown Actor"}]`); + // } + // console.groupEnd(); - for (const entry of validSummons) { - const actor = await fromUuid(entry.actorUUID); - console.log(`- Ready to summon ${entry.count}x [${actor?.name || "Unknown Actor"}]`); - } - console.groupEnd(); - - //Todo: Open Summon Dialog - } -} \ No newline at end of file + // //Todo: Open Summon Dialog + // } +} diff --git a/module/data/fields/action/summonField.mjs b/module/data/fields/action/summonField.mjs index 1abb31f4..038cae09 100644 --- a/module/data/fields/action/summonField.mjs +++ b/module/data/fields/action/summonField.mjs @@ -7,32 +7,22 @@ export default class DHSummonField extends fields.ArrayField { static order = 120; constructor(options = {}, context = {}) { - const summonFields = new fields.SchemaField({ - actorUUID: new fields.DocumentUUIDField({ - type: 'Actor', - required: true - }), - count: new fields.NumberField({ - required: true, - default: 1, - min: 1, - integer: true - }) + const summonFields = new fields.SchemaField({ + actorUUID: new fields.DocumentUUIDField({ + type: 'Actor', + required: true + }), + count: new fields.NumberField({ + required: true, + default: 1, + min: 1, + integer: true }) + }); super(summonFields, options, context); - - // summon: new fields.ArrayField(new fields.SchemaField({ - // actorUUID: new fields.DocumentUUIDField({ - // type: 'Actor', - // required: true }), - // count: new fields.NumberField({ - // required: true, - // default: 1, - // min: 1, - // integer: true }) - // }), { required: false, initial: [] }) - // }; - // super(summonFields, options, context); } -} \ No newline at end of file + static async execute(config) { + console.log('something'); + } +} From 506a17bd031ffb2bc894febba2c9be804b5cc193 Mon Sep 17 00:00:00 2001 From: Nikhil Nagarajan Date: Wed, 7 Jan 2026 15:53:25 -0500 Subject: [PATCH 10/18] Dialog created. Tokens not dragged(tbd). --- lang/en.json | 4 ++ module/applications/dialogs/summonDialog.mjs | 54 ++++++++++---------- module/data/action/summonAction.mjs | 36 ------------- module/data/fields/action/summonField.mjs | 25 ++++++++- templates/actionTypes/summon.hbs | 6 --- templates/dialogs/summon/summonDialog.hbs | 18 ++++--- 6 files changed, 66 insertions(+), 77 deletions(-) diff --git a/lang/en.json b/lang/en.json index 7f359f09..80091d1e 100755 --- a/lang/en.json +++ b/lang/en.json @@ -610,6 +610,10 @@ "title": "{name} Resource", "rerollDice": "Reroll Dice" }, + "Summon": { + "title": "Summon Tokens", + "hint": "Drag tokens from the list below into the scene to summon them." + }, "TagTeamSelect": { "title": "Tag Team Roll", "leaderTitle": "Initiating Character", diff --git a/module/applications/dialogs/summonDialog.mjs b/module/applications/dialogs/summonDialog.mjs index c5ab7a3d..9dc189fd 100644 --- a/module/applications/dialogs/summonDialog.mjs +++ b/module/applications/dialogs/summonDialog.mjs @@ -5,44 +5,46 @@ export default class DHSummonDialog extends HandlebarsApplicationMixin(Applicati super(summonData); // Initialize summons and index this.summons = summonData.summons || []; - this.index = 0; } - get_title() { - return game.i18n.localize("DAGGERHEART.DIALOGS.SUMMON.title"); - } + static PARTS = { + main: { template: 'systems/daggerheart/templates/dialogs/summon/summonDialog.hbs' } + }; + static DEFAULT_OPTIONS= { - ...super.DEFAULT_OPTIONS, - template: 'systems/daggerheart/module/applications/dialogs/summon/summonDialog.hbs', - width: 400, - height: 'auto', + tag: 'form', + window: { + title: "DAGGERHEART.APPLICATIONS.Summon.title", + resizable: false + }, + position: { + width: 400, + height: 'auto' + }, classes: ['daggerheart', 'dialog', 'summon-dialog'], - dragDrop: [{ dragSelector: '.summon-token', dropSelector: null, handler:'onDrop'}] + dragDrop: [{dragSelector: '.summon-token'}], }; async _prepareContext() { const context = await super._prepareContext(); - context.summons=this.summons; - context.index=this.index; + context.summons=await Promise.all(this.summons.map(async(entry)=>{ + const actor = await fromUuid(entry.actorUUID); + return { + ...entry, + name: actor?.name || game.i18n.localize("DAGGERHEART.GENERAL.Unknown"), + img: actor?.img || 'icons/svg/mystery-man.svg', + }; + })); return context; } - getData(options={}) { - const data = super.getData(options); - data.summons=this.summons; - data.index=this.index; - return data; - } - async prepareContext() { - const context = await super.prepareContext(); - return context; + _onDragStart(event) { + const uuid = event.currentTarget.dataset.uuid; + if(!uuid) return; + const dragData = { type: 'Actor', uuid: uuid }; + event.dataTransfer.effectAllowed = 'all'; + event.dataTransfer.setData('text/plain', JSON.stringify(dragData)); } - async onDrop(event) {//add to canvas - event.preventDefault(); - const tokenData = JSON.parse(event.dataTransfer.getData('text/plain')); - const position = { x: event.clientX, y: event.clientY }; - await canvas.scene.createEmbeddedDocuments("Token", [tokenData], { temporary: false, x: position.x, y: position.y }); - } } \ No newline at end of file diff --git a/module/data/action/summonAction.mjs b/module/data/action/summonAction.mjs index d03c766b..1505ce2d 100644 --- a/module/data/action/summonAction.mjs +++ b/module/data/action/summonAction.mjs @@ -2,40 +2,4 @@ import DHBaseAction from './baseAction.mjs'; export default class DHSummonAction extends DHBaseAction { static extraSchemas = [...super.extraSchemas, 'summon']; - - // get defaultValues() { - // return { - // summon: [] - // }; - // } - - // async trigger(event, ...args) { - // if (!this.canSummon || !canvas.scene){ - // ui.notifications.warn(game.i18n.localize("DAGGERHEART.ACTIONS.TYPES.summon.error")); - // return; - // } - // await this._performAction(); - // } - - // get canSummon() { - // return game.user.can('TOKEN_CREATE'); - // } - - // async _performAction(event, ...args) { - // const validSummons = this.summon.filter(entry => entry.actorUUID); - // if (validSummons.length === 0) { - // ui.notifications.warn("No actors configured for this Summon action."); - // return; - // } - // // FOR NOW: Just log the data to prove the schema working or not - // console.group("Summon Action Triggered"); - - // for (const entry of validSummons) { - // const actor = await fromUuid(entry.actorUUID); - // console.log(`- Ready to summon ${entry.count}x [${actor?.name || "Unknown Actor"}]`); - // } - // console.groupEnd(); - - // //Todo: Open Summon Dialog - // } } diff --git a/module/data/fields/action/summonField.mjs b/module/data/fields/action/summonField.mjs index 038cae09..49ff1798 100644 --- a/module/data/fields/action/summonField.mjs +++ b/module/data/fields/action/summonField.mjs @@ -1,4 +1,5 @@ const fields = foundry.data.fields; +import DHSummonDialog from '../../../applications/dialogs/summonDialog.mjs'; export default class DHSummonField extends fields.ArrayField { /** @@ -22,7 +23,27 @@ export default class DHSummonField extends fields.ArrayField { super(summonFields, options, context); } - static async execute(config) { - console.log('something'); + static async execute() { + if(!canvas.scene){ + ui.notifications.warn(game.i18n.localize("DAGGERHEART.ACTIONS.TYPES.summon.error")); + return; + } + const validSummons = this.summon.filter(entry => entry.actorUUID); + if (validSummons.length === 0) { + console.log("No actors configured for this Summon action."); + return; + } + // FOR NOW: Just log the data to prove the schema working or not + console.group("Summon Action Triggered"); + for (const entry of validSummons) { + const actor = await fromUuid(entry.actorUUID); + console.log(`- Ready to summon ${entry.count}x [${actor?.name || "Unknown Actor"}]`); + } + console.groupEnd(); + //Open Summon Dialog + const summonData = { summons: validSummons }; + console.log(summonData); + const dialog = new DHSummonDialog(summonData); + dialog.render(true); } } diff --git a/templates/actionTypes/summon.hbs b/templates/actionTypes/summon.hbs index d313fb4c..276045a8 100644 --- a/templates/actionTypes/summon.hbs +++ b/templates/actionTypes/summon.hbs @@ -3,12 +3,6 @@ {{localize "DAGGERHEART.ACTIONS.TYPES.summon.name"}}

{{localize "DAGGERHEART.ACTIONS.Settings.summon.hint"}}

- {{!-- {{#each source as |entry index|}} -
- {{formField ../fields.actorUUID label="DAGGERHEART.ACTIONS.Settings.summon.actor" value=entry.actorUUID name=(concat "summon." index ".actorUUID") localize=true classes="summon-actor-drop"}} - {{formField ../fields.count label="DAGGERHEART.ACTIONS.Settings.summon.count" value=entry.count name=(concat "summon." index ".count") localize=true}} -
- {{/each}} --}}
diff --git a/templates/dialogs/summon/summonDialog.hbs b/templates/dialogs/summon/summonDialog.hbs index 9e03e129..61817b93 100644 --- a/templates/dialogs/summon/summonDialog.hbs +++ b/templates/dialogs/summon/summonDialog.hbs @@ -1,24 +1,28 @@
- {{localize "DAGGERHEART.DIALOGS.Summon.title"}} + {{localize "DAGGERHEART.APPLICATIONS.Summon.title"}} -

{{localize "DAGGERHEART.DIALOGS.Summon.hint"}}

+

{{localize "DAGGERHEART.APPLICATIONS.Summon.hint"}}

- {{#each summons as |entry index|}} -
    + +
      + {{#each summons as |entry index|}} +
    • {{#if (gte entry.count 1)}}

      - {{entry.name}} (x{{entry.count}}) + {{entry.name}}

      + Count: {{entry.count}} {{else}}

      {{entry.name}}

      {{/if}}
      -
    - {{/each}} + + {{/each}} +
\ No newline at end of file From f47a869af35461022a9bb7b27f3cb05f888b4bbc Mon Sep 17 00:00:00 2001 From: Nikhil Nagarajan Date: Wed, 7 Jan 2026 18:02:49 -0500 Subject: [PATCH 11/18] Bare minimum implementation --- module/data/fields/action/summonField.mjs | 63 +++++++++++++++++++---- 1 file changed, 54 insertions(+), 9 deletions(-) diff --git a/module/data/fields/action/summonField.mjs b/module/data/fields/action/summonField.mjs index 49ff1798..6ba1f8a2 100644 --- a/module/data/fields/action/summonField.mjs +++ b/module/data/fields/action/summonField.mjs @@ -33,17 +33,62 @@ export default class DHSummonField extends fields.ArrayField { console.log("No actors configured for this Summon action."); return; } - // FOR NOW: Just log the data to prove the schema working or not - console.group("Summon Action Triggered"); + for (const entry of validSummons) { const actor = await fromUuid(entry.actorUUID); - console.log(`- Ready to summon ${entry.count}x [${actor?.name || "Unknown Actor"}]`); } - console.groupEnd(); - //Open Summon Dialog - const summonData = { summons: validSummons }; - console.log(summonData); - const dialog = new DHSummonDialog(summonData); - dialog.render(true); + + // //Open Summon Dialog + // const summonData = { summons: validSummons }; + // console.log(summonData); + // const dialog = new DHSummonDialog(summonData); + // dialog.render(true); + + // Create folder and add tokens to actor folder + const rootFolderName = game.i18n.localize("DAGGERHEART.APPLICATIONS.Summon.title"); + let rootFolder = game.folders.find(f => f.name === rootFolderName && f.type === 'Actor'); + if (!rootFolder) { + rootFolder = await Folder.create({ + name: rootFolderName, + type: 'Actor', + }); + } + const parentName = this.item.name ?? "Unkown Feature"; + const actionName = this.name ?? "Unkown Action"; + const subFolderName = `${parentName} - ${actionName}`; + + let subFolder = game.folders.find(f => f.name === subFolderName && f.type === 'Actor' && f.folder?.id === rootFolder.id); + if (!subFolder) { + subFolder = await Folder.create({ + name: subFolderName, + type: 'Actor', + folder: rootFolder.id + }); + const actorsToSummon = []; + for (const entry of validSummons) { + const sourceActor = await fromUuid(entry.actorUUID); + if (!sourceActor) { + console.warn('DHSummonField: Could not find actor for UUID', entry.actorUUID); + continue; + } + const actorData = sourceActor.toObject(); + delete actorData._id; // Remove _id to create a new Actor + actorData.folder = subFolder.id; + + for (let i = 0; i < entry.count; i++) { + const newActor = foundry.utils.deepClone(actorData); + if (entry.count > 1) { + newActor.name = `${actorData.name} ${i + 1}`; + } + actorsToSummon.push(newActor); + } + } + if (actorsToSummon.length > 0) { + await Actor.createDocuments(actorsToSummon); + ui.notifications.info(`Summoned ${actorsToSummon.length} actors successfully in folder ${subFolder.name}.`); + } + } else { + ui.notifications.info(`Summon actors already exist in folder ${subFolder.name}.`); + } } } From 27d49d35fa2d29246a34cb3000e1de363fa1be33 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Thu, 8 Jan 2026 20:25:11 +0100 Subject: [PATCH 12/18] Finalized functionality --- daggerheart.mjs | 5 + lang/en.json | 3 +- .../sheets-configs/action-base-config.mjs | 68 +++++++----- module/canvas/_module.mjs | 1 + module/canvas/tokens.mjs | 16 +++ module/data/fields/action/summonField.mjs | 93 ++++++---------- module/documents/_module.mjs | 1 + module/documents/tokenManager.mjs | 103 ++++++++++++++++++ styles/less/sheets/actions/actions.less | 50 +++++++++ styles/less/sheets/index.less | 2 + templates/actionTypes/summon.hbs | 82 +++++++------- 11 files changed, 296 insertions(+), 128 deletions(-) create mode 100644 module/canvas/tokens.mjs create mode 100644 module/documents/tokenManager.mjs create mode 100644 styles/less/sheets/actions/actions.less diff --git a/daggerheart.mjs b/daggerheart.mjs index f1d8c67a..f10d4fe7 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -2,6 +2,7 @@ import { SYSTEM } from './module/config/system.mjs'; import * as applications from './module/applications/_module.mjs'; import * as data from './module/data/_module.mjs'; import * as models from './module/data/_module.mjs'; +import * as canvas from './module/canvas/_module.mjs'; import * as documents from './module/documents/_module.mjs'; import * as dice from './module/dice/_module.mjs'; import * as fields from './module/data/fields/_module.mjs'; @@ -19,6 +20,7 @@ import { import { placeables } from './module/canvas/_module.mjs'; import './node_modules/@yaireo/tagify/dist/tagify.css'; import TemplateManager from './module/documents/templateManager.mjs'; +import TokenManager from './module/documents/tokenManager.mjs'; CONFIG.DH = SYSTEM; CONFIG.TextEditor.enrichers.push(...enricherConfig); @@ -51,6 +53,8 @@ CONFIG.ChatMessage.template = 'systems/daggerheart/templates/ui/chat/chat-messag CONFIG.Canvas.rulerClass = placeables.DhRuler; CONFIG.Canvas.layers.templates.layerClass = placeables.DhTemplateLayer; +CONFIG.Canvas.layers.tokens.layerClass = canvas.DhTokenLayer; + CONFIG.MeasuredTemplate.objectClass = placeables.DhMeasuredTemplate; CONFIG.Scene.documentClass = documents.DhScene; @@ -73,6 +77,7 @@ CONFIG.ui.countdowns = applications.ui.DhCountdowns; CONFIG.ux.ContextMenu = applications.ux.DHContextMenu; CONFIG.ux.TooltipManager = documents.DhTooltipManager; CONFIG.ux.TemplateManager = new TemplateManager(); +CONFIG.ux.TokenManager = new TokenManager(); Hooks.once('init', () => { game.system.api = { diff --git a/lang/en.json b/lang/en.json index 80091d1e..11d7700c 100755 --- a/lang/en.json +++ b/lang/en.json @@ -123,11 +123,12 @@ "cost": { "stepTooltip": "+{step} per step" }, - "summon":{ + "summon": { "addSummonEntry": "Add Summon Entry", "actorUUID": "Actor to Summon", "actor": "Actor", "count": "Count", + "dropSummonsHere": "Drop Summons Here", "hint": "Add Actor(s) and the quantity to summon under this action." } } diff --git a/module/applications/sheets-configs/action-base-config.mjs b/module/applications/sheets-configs/action-base-config.mjs index 76965275..91972d8a 100644 --- a/module/applications/sheets-configs/action-base-config.mjs +++ b/module/applications/sheets-configs/action-base-config.mjs @@ -15,7 +15,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) static DEFAULT_OPTIONS = { tag: 'form', - classes: ['daggerheart', 'dh-style', 'dialog', 'max-800'], + classes: ['daggerheart', 'dh-style', 'dialog', 'action-config', 'max-800'], window: { icon: 'fa-solid fa-wrench', resizable: false @@ -37,7 +37,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) submitOnChange: true, closeOnSubmit: false }, - dragDrop: [{ dragSelector: null, dropSelector: '.summon-entry', handlers: ['_onDrop'] }] + dragDrop: [{ dragSelector: null, dropSelector: '#summon-drop-zone', handlers: ['_onDrop'] }] }; static PARTS = { @@ -87,7 +87,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) } }; - static CLEAN_ARRAYS = ['damage.parts', 'cost', 'effects','summon']; + static CLEAN_ARRAYS = ['damage.parts', 'cost', 'effects', 'summon']; _getTabs(tabs) { for (const v of Object.values(tabs)) { @@ -98,23 +98,24 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) return tabs; } + _attachPartListeners(partId, htmlElement, options) { + super._attachPartListeners(partId, htmlElement, options); + + htmlElement.querySelectorAll('.summon-count-wrapper input').forEach(element => { + element.addEventListener('change', this.updateSummonCount.bind(this)); + }); + } + async _prepareContext(_options) { const context = await super._prepareContext(_options, 'action'); context.source = this.action.toObject(true); // Resolving summon entries so actions can read entry.name / entry.img / entry.uuid - if (Array.isArray(context.source.summon)) { - context.source.summon = await Promise.all(context.source.summon.map(async entry => { - if (!entry) return entry; - const uuid = entry.actorUUID ?? entry.uuid; - entry.uuid = uuid; - try { - const doc = await foundry.utils.fromUuid(uuid); - entry.name = entry.name ?? doc?.name; - entry.img = entry.img ?? (doc?.img ?? doc?.prototypeToken?.texture?.src ?? null); - } catch (_) {} - return entry; - })); + context.summons = []; + for (const summon of context.source.summon) { + const actor = await foundry.utils.fromUuid(summon.actorUUID); + context.summons.push({ actor, count: summon.count }); } + context.openSection = this.openSection; context.tabs = this._getTabs(this.constructor.TABS); context.config = CONFIG.DH; @@ -197,8 +198,9 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) } static async updateForm(event, _, formData) { - const submitData = this._prepareSubmitData(event, formData), - data = foundry.utils.mergeObject(this.action.toObject(), submitData); + const submitData = this._prepareSubmitData(event, formData); + + const data = foundry.utils.mergeObject(this.action.toObject(), submitData); this.action = await this.action.update(data); this.sheetUpdate?.(this.action); @@ -234,13 +236,13 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) static async editDoc(event, button) { event.stopPropagation(); const uuid = button?.dataset.itemUuid ?? button?.dataset.uuid; // Obtain uuid from dataset - if (!uuid) return; + if (!uuid) return; //Try catching errors try { const doc = await foundry.utils.fromUuid(uuid); if (doc?.sheet) return doc.sheet.render({ force: true }); } catch (err) { - console.warn("editDoc action failed for", uuid, err); + console.warn('editDoc action failed for', uuid, err); } } @@ -261,6 +263,14 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); } + updateSummonCount(event) { + const wrapper = event.target.closest('.summon-count-wrapper'); + const index = wrapper.dataset.index; + const data = this.action.toObject(); + data.summon[index].count = Number.parseInt(event.target.value); + this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); + } + /** Specific implementation in extending classes **/ static async addEffect(_event) {} static removeEffect(_event, _button) {} @@ -271,28 +281,28 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) await super.close(options); } - /** Implementation for dragdrop for summon actor selection **/ async _onDrop(event) { const data = foundry.applications.ux.TextEditor.getDragEventData(event); - const item=await foundry.utils.fromUuid(data.uuid); + const item = await foundry.utils.fromUuid(data.uuid); if (!(item instanceof game.system.api.documents.DhpActor)) { - ui.notifications.warn(game.i18n.localize("DAGGERHEART.ACTIONS.TYPES.summon.invalidDrop")); + ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.summon.invalidDrop')); return; } - //Add to summon array - const actionData = this.action.toObject(); // Get current action data - //checking to see if actor is already in summon list add 1 to count instead of adding new entry + + const actionData = this.action.toObject(); let countvalue = 1; for (const entry of actionData.summon) { if (entry.actorUUID === data.uuid) { entry.count += 1; countvalue = entry.count; - await this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(actionData) }); + await this.constructor.updateForm.bind(this)(null, null, { + object: foundry.utils.flattenObject(actionData) + }); return; } } - actionData.summon.push({ actorUUID: data.uuid, count: countvalue });// Add new summon entry - await this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(actionData) }); // Update the form with new data + + actionData.summon.push({ actorUUID: data.uuid, count: countvalue }); + await this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(actionData) }); } - } diff --git a/module/canvas/_module.mjs b/module/canvas/_module.mjs index 6b8885f4..c211b549 100644 --- a/module/canvas/_module.mjs +++ b/module/canvas/_module.mjs @@ -1 +1,2 @@ export * as placeables from './placeables/_module.mjs'; +export { default as DhTokenLayer } from './tokens.mjs'; diff --git a/module/canvas/tokens.mjs b/module/canvas/tokens.mjs new file mode 100644 index 00000000..9813cd48 --- /dev/null +++ b/module/canvas/tokens.mjs @@ -0,0 +1,16 @@ +export default class DhTokenLayer extends foundry.canvas.layers.TokenLayer { + async _createPreview(createData, options) { + if (options.actor) { + const tokenSizes = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).tokenSizes; + if (options.actor?.system.metadata.usesSize) { + const tokenSize = tokenSizes[options.actor.system.size]; + if (tokenSize && options.actor.system.size !== CONFIG.DH.ACTOR.tokenSize.custom.id) { + createData.width = tokenSize; + createData.height = tokenSize; + } + } + } + + return super._createPreview(createData, options); + } +} diff --git a/module/data/fields/action/summonField.mjs b/module/data/fields/action/summonField.mjs index 6ba1f8a2..a4df927f 100644 --- a/module/data/fields/action/summonField.mjs +++ b/module/data/fields/action/summonField.mjs @@ -1,5 +1,4 @@ const fields = foundry.data.fields; -import DHSummonDialog from '../../../applications/dialogs/summonDialog.mjs'; export default class DHSummonField extends fields.ArrayField { /** @@ -24,71 +23,47 @@ export default class DHSummonField extends fields.ArrayField { } static async execute() { - if(!canvas.scene){ - ui.notifications.warn(game.i18n.localize("DAGGERHEART.ACTIONS.TYPES.summon.error")); + if (!canvas.scene) { + ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.summon.error')); return; } - const validSummons = this.summon.filter(entry => entry.actorUUID); - if (validSummons.length === 0) { - console.log("No actors configured for this Summon action."); + + if (this.summon.length === 0) { + ui.notifications.warn('No actors configured for this Summon action.'); return; } - - for (const entry of validSummons) { - const actor = await fromUuid(entry.actorUUID); - } - // //Open Summon Dialog - // const summonData = { summons: validSummons }; - // console.log(summonData); - // const dialog = new DHSummonDialog(summonData); - // dialog.render(true); + const summonData = []; + for (const summon of this.summon) { + /* Possibly check for any available instances in the world with prepared images */ + let actor = await foundry.utils.fromUuid(summon.actorUUID); - // Create folder and add tokens to actor folder - const rootFolderName = game.i18n.localize("DAGGERHEART.APPLICATIONS.Summon.title"); - let rootFolder = game.folders.find(f => f.name === rootFolderName && f.type === 'Actor'); - if (!rootFolder) { - rootFolder = await Folder.create({ - name: rootFolderName, - type: 'Actor', - }); - } - const parentName = this.item.name ?? "Unkown Feature"; - const actionName = this.name ?? "Unkown Action"; - const subFolderName = `${parentName} - ${actionName}`; - - let subFolder = game.folders.find(f => f.name === subFolderName && f.type === 'Actor' && f.folder?.id === rootFolder.id); - if (!subFolder) { - subFolder = await Folder.create({ - name: subFolderName, - type: 'Actor', - folder: rootFolder.id - }); - const actorsToSummon = []; - for (const entry of validSummons) { - const sourceActor = await fromUuid(entry.actorUUID); - if (!sourceActor) { - console.warn('DHSummonField: Could not find actor for UUID', entry.actorUUID); - continue; - } - const actorData = sourceActor.toObject(); - delete actorData._id; // Remove _id to create a new Actor - actorData.folder = subFolder.id; - - for (let i = 0; i < entry.count; i++) { - const newActor = foundry.utils.deepClone(actorData); - if (entry.count > 1) { - newActor.name = `${actorData.name} ${i + 1}`; - } - actorsToSummon.push(newActor); - } + /* Check for any available instances of the actor present in the world if we're missing artwork in the compendium */ + const dataType = game.system.api.data.actors[`Dh${actor.type.capitalize()}`]; + if (actor.inCompendium && dataType && actor.img === dataType.DEFAULT_ICON) { + const worldActorCopy = game.actors.find(x => x.name === actor.name); + actor = worldActorCopy ?? actor; } - if (actorsToSummon.length > 0) { - await Actor.createDocuments(actorsToSummon); - ui.notifications.info(`Summoned ${actorsToSummon.length} actors successfully in folder ${subFolder.name}.`); - } - } else { - ui.notifications.info(`Summon actors already exist in folder ${subFolder.name}.`); + + summonData.push({ actor, count: summon.count }); } + + const handleSummon = async summonIndex => { + const summon = summonData[summonIndex]; + const result = await CONFIG.ux.TokenManager.createPreviewAsync(summon.actor, { + name: `${summon.actor.prototypeToken.name}${summon.count > 1 ? ` (${summon.count}x)` : ''}` + }); + if (!result) return; + + summon.count--; + if (summon.count === 0) { + summonIndex++; + if (summonIndex === summonData.length) return; + } + + handleSummon(summonIndex); + }; + + handleSummon(0); } } diff --git a/module/documents/_module.mjs b/module/documents/_module.mjs index 22718bea..8073cfe1 100644 --- a/module/documents/_module.mjs +++ b/module/documents/_module.mjs @@ -8,3 +8,4 @@ export { default as DhScene } from './scene.mjs'; export { default as DhToken } from './token.mjs'; export { default as DhTooltipManager } from './tooltipManager.mjs'; export { default as DhTemplateManager } from './templateManager.mjs'; +export { default as DhTokenManager } from './tokenManager.mjs'; diff --git a/module/documents/tokenManager.mjs b/module/documents/tokenManager.mjs new file mode 100644 index 00000000..06d78273 --- /dev/null +++ b/module/documents/tokenManager.mjs @@ -0,0 +1,103 @@ +/** + * A singleton class that handles preview tokens. + */ + +export default class DhTokenManager { + #activePreview; + #actor; + #resolve; + + /** + * Create a template preview, deactivating any existing ones. + * @param {object} data + */ + async createPreview(actor, tokenData) { + this.#actor = actor; + const token = await canvas.tokens._createPreview( + { + ...actor.prototypeToken, + displayName: 50, + ...tokenData + }, + { renderSheet: false, actor } + ); + + this.#activePreview = { + document: token.document, + object: token, + origin: { x: token.document.x, y: token.document.y } + }; + + this.#activePreview.events = { + contextmenu: this.#cancelTemplate.bind(this), + mousedown: this.#confirmTemplate.bind(this), + mousemove: this.#onDragMouseMove.bind(this) + }; + + canvas.stage.on('mousemove', this.#activePreview.events.mousemove); + canvas.stage.on('mousedown', this.#activePreview.events.mousedown); + canvas.app.view.addEventListener('contextmenu', this.#activePreview.events.contextmenu); + } + + async createPreviewAsync(actor, tokenData) { + return new Promise(resolve => { + this.#resolve = resolve; + this.createPreview(actor, tokenData); + }); + } + + /** + * Handles the movement of the token preview on mousedrag. + * @param {mousemove Event} event + */ + #onDragMouseMove(event) { + event.stopPropagation(); + const { moveTime, object } = this.#activePreview; + const update = {}; + + const now = Date.now(); + if (now - (moveTime || 0) <= 16) return; + this.#activePreview.moveTime = now; + + let cursor = event.getLocalPosition(canvas.templates); + + Object.assign(update, canvas.grid.getTopLeftPoint(cursor)); + + object.document.updateSource(update); + object.renderFlags.set({ refresh: true }); + } + + /** + * Cancels the preview token on right-click. + * @param {contextmenu Event} event + */ + #cancelTemplate(_event, resolved) { + const { mousemove, mousedown, contextmenu } = this.#activePreview.events; + this.#activePreview.object.destroy(); + + canvas.stage.off('mousemove', mousemove); + canvas.stage.off('mousedown', mousedown); + canvas.app.view.removeEventListener('contextmenu', contextmenu); + if (this.#resolve && !resolved) this.#resolve(false); + } + + /** + * Creates a real Actor and token at the preview location and cancels the preview. + * @param {click Event} event + */ + async #confirmTemplate(event) { + event.stopPropagation(); + this.#cancelTemplate(event, true); + + const actor = this.#actor.inCompendium + ? await game.system.api.documents.DhpActor.create(this.#actor.toObject()) + : this.#actor; + const tokenData = await actor.getTokenDocument(); + const token = await canvas.scene.createEmbeddedDocuments('Token', [ + { ...tokenData, x: this.#activePreview.document.x, y: this.#activePreview.document.y } + ]); + + this.#activePreview = undefined; + if (this.#resolve) this.#resolve(token); + } +} diff --git a/styles/less/sheets/actions/actions.less b/styles/less/sheets/actions/actions.less new file mode 100644 index 00000000..813e4416 --- /dev/null +++ b/styles/less/sheets/actions/actions.less @@ -0,0 +1,50 @@ +.application.daggerheart.dh-style.action-config { + .actor-summon-items { + width: 100%; + display: flex; + flex-direction: column; + gap: 10px; + + .actor-summon-line { + display: flex; + align-items: center; + gap: 5px; + border-radius: 3px; + + .actor-summon-name { + flex: 2; + display: flex; + align-items: center; + gap: 5px; + + img { + height: 40px; + } + } + + .actor-summon-controls { + flex: 1; + display: flex; + align-items: center; + gap: 5px; + + .controls { + display: flex; + gap: 5px; + } + } + } + + .summon-dragger { + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + height: 40px; + margin-top: 10px; + border: 1px dashed light-dark(@dark-blue-50, @beige-50); + border-radius: 3px; + color: light-dark(@dark-blue-50, @beige-50); + } + } +} diff --git a/styles/less/sheets/index.less b/styles/less/sheets/index.less index 1de1b055..c339e7be 100644 --- a/styles/less/sheets/index.less +++ b/styles/less/sheets/index.less @@ -37,3 +37,5 @@ @import './items/feature.less'; @import './items/heritage.less'; @import './items/item-sheet-shared.less'; + +@import './actions/actions.less'; diff --git a/templates/actionTypes/summon.hbs b/templates/actionTypes/summon.hbs index 276045a8..6ac7a90b 100644 --- a/templates/actionTypes/summon.hbs +++ b/templates/actionTypes/summon.hbs @@ -1,46 +1,50 @@ -
+
{{localize "DAGGERHEART.ACTIONS.TYPES.summon.name"}} -

{{localize "DAGGERHEART.ACTIONS.Settings.summon.hint"}}

-
-
- - {{#each source as |entry index|}} -
    -
    - -

    - {{entry.name}} -

    - {{formField ../fields.count label="DAGGERHEART.ACTIONS.Settings.summon.count" value=entry.count name=(concat "summon." index ".count") localize=true}} -
    - - - - - - +
      + {{#each @root.summons as |summon index|}} +
    • +
      + +

      + {{summon.actor.name}} +

      +
      + +
      +
      +
      +
      -
    - {{/each}} - {{#unless source.length}} - {{localize "DAGGERHEART.GENERAL.dropActorsHere"}} - {{/unless}} -
    -
    + + +
+ + {{/each}} +
+ {{localize "DAGGERHEART.ACTIONS.Settings.summon.dropSummonsHere"}} +
+
\ No newline at end of file From ae211cda3012085a766d35d8959059b98c320a13 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Thu, 8 Jan 2026 20:49:49 +0100 Subject: [PATCH 13/18] Cleanup --- lang/en.json | 11 +--- module/applications/dialogs/summonDialog.mjs | 50 ------------------- .../sheets-configs/action-base-config.mjs | 18 +++---- styles/less/global/global.less | 20 -------- templates/actionTypes/summon.hbs | 2 +- templates/dialogs/summon/summonDialog.hbs | 28 ----------- 6 files changed, 8 insertions(+), 121 deletions(-) delete mode 100644 module/applications/dialogs/summonDialog.mjs delete mode 100644 templates/dialogs/summon/summonDialog.hbs diff --git a/lang/en.json b/lang/en.json index 11d7700c..af3afc22 100755 --- a/lang/en.json +++ b/lang/en.json @@ -124,12 +124,7 @@ "stepTooltip": "+{step} per step" }, "summon": { - "addSummonEntry": "Add Summon Entry", - "actorUUID": "Actor to Summon", - "actor": "Actor", - "count": "Count", - "dropSummonsHere": "Drop Summons Here", - "hint": "Add Actor(s) and the quantity to summon under this action." + "dropSummonsHere": "Drop Summons Here" } } }, @@ -611,10 +606,6 @@ "title": "{name} Resource", "rerollDice": "Reroll Dice" }, - "Summon": { - "title": "Summon Tokens", - "hint": "Drag tokens from the list below into the scene to summon them." - }, "TagTeamSelect": { "title": "Tag Team Roll", "leaderTitle": "Initiating Character", diff --git a/module/applications/dialogs/summonDialog.mjs b/module/applications/dialogs/summonDialog.mjs deleted file mode 100644 index 9dc189fd..00000000 --- a/module/applications/dialogs/summonDialog.mjs +++ /dev/null @@ -1,50 +0,0 @@ -const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; - -export default class DHSummonDialog extends HandlebarsApplicationMixin(ApplicationV2) { - constructor(summonData) { - super(summonData); - // Initialize summons and index - this.summons = summonData.summons || []; - } - - static PARTS = { - main: { template: 'systems/daggerheart/templates/dialogs/summon/summonDialog.hbs' } - }; - - - static DEFAULT_OPTIONS= { - tag: 'form', - window: { - title: "DAGGERHEART.APPLICATIONS.Summon.title", - resizable: false - }, - position: { - width: 400, - height: 'auto' - }, - classes: ['daggerheart', 'dialog', 'summon-dialog'], - dragDrop: [{dragSelector: '.summon-token'}], - }; - - async _prepareContext() { - const context = await super._prepareContext(); - context.summons=await Promise.all(this.summons.map(async(entry)=>{ - const actor = await fromUuid(entry.actorUUID); - return { - ...entry, - name: actor?.name || game.i18n.localize("DAGGERHEART.GENERAL.Unknown"), - img: actor?.img || 'icons/svg/mystery-man.svg', - }; - })); - return context; - } - - _onDragStart(event) { - const uuid = event.currentTarget.dataset.uuid; - if(!uuid) return; - const dragData = { type: 'Actor', uuid: uuid }; - event.dataTransfer.effectAllowed = 'all'; - event.dataTransfer.setData('text/plain', JSON.stringify(dragData)); - } - -} \ No newline at end of file diff --git a/module/applications/sheets-configs/action-base-config.mjs b/module/applications/sheets-configs/action-base-config.mjs index 91972d8a..d30d9c08 100644 --- a/module/applications/sheets-configs/action-base-config.mjs +++ b/module/applications/sheets-configs/action-base-config.mjs @@ -109,7 +109,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) async _prepareContext(_options) { const context = await super._prepareContext(_options, 'action'); context.source = this.action.toObject(true); - // Resolving summon entries so actions can read entry.name / entry.img / entry.uuid + context.summons = []; for (const summon of context.source.summon) { const actor = await foundry.utils.fromUuid(summon.actorUUID); @@ -233,17 +233,10 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); } - static async editDoc(event, button) { - event.stopPropagation(); - const uuid = button?.dataset.itemUuid ?? button?.dataset.uuid; // Obtain uuid from dataset - if (!uuid) return; - //Try catching errors - try { - const doc = await foundry.utils.fromUuid(uuid); - if (doc?.sheet) return doc.sheet.render({ force: true }); - } catch (err) { - console.warn('editDoc action failed for', uuid, err); - } + static async editDoc(_event, target) { + const element = target.closest('[data-item-uuid]'); + const doc = (await foundry.utils.fromUuid(element.dataset.itemUuid)) ?? null; + if (doc) return doc.sheet.render({ force: true }); } static addDamage(_event) { @@ -264,6 +257,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) } updateSummonCount(event) { + event.stopPropagation(); const wrapper = event.target.closest('.summon-count-wrapper'); const index = wrapper.dataset.index; const data = this.action.toObject(); diff --git a/styles/less/global/global.less b/styles/less/global/global.less index 8abaddee..6cc63c2a 100644 --- a/styles/less/global/global.less +++ b/styles/less/global/global.less @@ -9,26 +9,6 @@ border-radius: 3px; color: light-dark(@dark-blue-50, @beige-50); font-family: @font-body; - &.summon-actor-drop { - height: fit-content; - min-height: inherit; - display: flex; - justify-content: center; - .actors-list.summon-entry .actor-summon-item .actor-summon-line { - display: flex; - justify-content: flex-end; - align-items: center; - .image{ - max-width: 10%; - } - .h4{ - width: --webkit-fill-available; - } - .controls.effect-control{ - padding:3px; - } - } - } } .daggerheart.dh-style { diff --git a/templates/actionTypes/summon.hbs b/templates/actionTypes/summon.hbs index 6ac7a90b..429977d9 100644 --- a/templates/actionTypes/summon.hbs +++ b/templates/actionTypes/summon.hbs @@ -16,7 +16,7 @@
- +
diff --git a/templates/dialogs/summon/summonDialog.hbs b/templates/dialogs/summon/summonDialog.hbs deleted file mode 100644 index 61817b93..00000000 --- a/templates/dialogs/summon/summonDialog.hbs +++ /dev/null @@ -1,28 +0,0 @@ -
- - {{localize "DAGGERHEART.APPLICATIONS.Summon.title"}} - -

{{localize "DAGGERHEART.APPLICATIONS.Summon.hint"}}

- -
- -
    - {{#each summons as |entry index|}} -
  • -
    - - {{#if (gte entry.count 1)}} -

    - {{entry.name}} -

    - Count: {{entry.count}} - {{else}} -

    - {{entry.name}} -

    - {{/if}} -
    -
  • - {{/each}} -
-
\ No newline at end of file From 47b9be0f0115095d383d31af61c42e8c49777071 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Thu, 8 Jan 2026 22:19:50 +0100 Subject: [PATCH 14/18] . --- lang/en.json | 3 ++- module/canvas/placeables/token.mjs | 29 +++++++++++++++++++++++ module/data/fields/action/summonField.mjs | 5 ++-- module/documents/tokenManager.mjs | 5 ++-- 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/lang/en.json b/lang/en.json index af3afc22..ffa54cff 100755 --- a/lang/en.json +++ b/lang/en.json @@ -2827,7 +2827,8 @@ "deleteItem": "Delete Item", "immune": "Immune", "middleClick": "[Middle Click] Keep tooltip view", - "tokenSize": "The token size used on the canvas" + "tokenSize": "The token size used on the canvas", + "previewTokenHelp": "Left-click to place, right-click to cancel" } } } diff --git a/module/canvas/placeables/token.mjs b/module/canvas/placeables/token.mjs index 64ec3fa9..39ccac44 100644 --- a/module/canvas/placeables/token.mjs +++ b/module/canvas/placeables/token.mjs @@ -1,4 +1,12 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { + /** @inheritdoc */ + async _draw(options) { + await super._draw(options); + + if (this.document.flags.daggerheart?.createPlacement) + this.previewHelp ||= this.addChild(this.#drawPreviewHelp()); + } + /** @inheritDoc */ async _drawEffects() { this.effects.renderable = false; @@ -69,4 +77,25 @@ export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { bar.position.set(0, posY); return true; } + + /** + * Draw a helptext for previews as a text object + * @returns {PreciseText} The Text object for the preview helper + */ + #drawPreviewHelp() { + const { uiScale } = canvas.dimensions; + + const textStyle = CONFIG.canvasTextStyle.clone(); + textStyle.fontSize = 18; + textStyle.wordWrapWidth = this.w * 2.5; + textStyle.fontStyle = 'italic'; + + const helpText = new PreciseText( + `(${game.i18n.localize('DAGGERHEART.UI.Tooltip.previewTokenHelp')})`, + textStyle + ); + helpText.anchor.set(helpText.width / 900, 1); + helpText.scale.set(uiScale, uiScale); + return helpText; + } } diff --git a/module/data/fields/action/summonField.mjs b/module/data/fields/action/summonField.mjs index a4df927f..da884c42 100644 --- a/module/data/fields/action/summonField.mjs +++ b/module/data/fields/action/summonField.mjs @@ -33,6 +33,7 @@ export default class DHSummonField extends fields.ArrayField { return; } + this.actor.sheet?.minimize(); const summonData = []; for (const summon of this.summon) { /* Possibly check for any available instances in the world with prepared images */ @@ -53,12 +54,12 @@ export default class DHSummonField extends fields.ArrayField { const result = await CONFIG.ux.TokenManager.createPreviewAsync(summon.actor, { name: `${summon.actor.prototypeToken.name}${summon.count > 1 ? ` (${summon.count}x)` : ''}` }); - if (!result) return; + if (!result) return this.actor.sheet?.maximize(); summon.count--; if (summon.count === 0) { summonIndex++; - if (summonIndex === summonData.length) return; + if (summonIndex === summonData.length) return this.actor.sheet?.maximize(); } handleSummon(summonIndex); diff --git a/module/documents/tokenManager.mjs b/module/documents/tokenManager.mjs index 06d78273..0a878887 100644 --- a/module/documents/tokenManager.mjs +++ b/module/documents/tokenManager.mjs @@ -39,10 +39,11 @@ export default class DhTokenManager { canvas.app.view.addEventListener('contextmenu', this.#activePreview.events.contextmenu); } - async createPreviewAsync(actor, tokenData) { + /* Currently intended for using as a preview of where to create a token. (note the flag) */ + async createPreviewAsync(actor, tokenData = {}) { return new Promise(resolve => { this.#resolve = resolve; - this.createPreview(actor, tokenData); + this.createPreview(actor, { ...tokenData, flags: { daggerheart: { createPlacement: true } } }); }); } From 69da660bfa7c0fd96f69217c866a82920f266d0c Mon Sep 17 00:00:00 2001 From: WBHarry Date: Fri, 9 Jan 2026 15:39:13 +0100 Subject: [PATCH 15/18] Added optional summon render to chat message --- lang/en.json | 8 +- .../sheets-configs/action-base-config.mjs | 4 +- module/data/action/baseAction.mjs | 14 ++-- module/data/fields/action/summonField.mjs | 75 ++++++++++++------- module/data/fields/actionField.mjs | 3 +- module/documents/tokenManager.mjs | 4 +- styles/less/ui/chat/action.less | 45 +++++++++++ templates/ui/chat/action.hbs | 18 ++++- 8 files changed, 128 insertions(+), 43 deletions(-) diff --git a/lang/en.json b/lang/en.json index ffa54cff..f40e82a9 100755 --- a/lang/en.json +++ b/lang/en.json @@ -71,7 +71,9 @@ "name": "Summon", "tooltip": "Create tokens in the scene.", "error": "You do not have permission to summon tokens or there is no active scene.", - "invalidDrop": "You can only drop Actor entities to summon." + "invalidDrop": "You can only drop Actor entities to summon.", + "chatMessageTitle": "Test2", + "chatMessageHeaderTitle": "Summoning" } }, "Config": { @@ -2180,6 +2182,10 @@ "stress": "Stress", "subclasses": "Subclasses", "success": "Success", + "summon": { + "single": "Summon", + "plural": "Summons" + }, "take": "Take", "Target": { "single": "Target", diff --git a/module/applications/sheets-configs/action-base-config.mjs b/module/applications/sheets-configs/action-base-config.mjs index d30d9c08..7224bd72 100644 --- a/module/applications/sheets-configs/action-base-config.mjs +++ b/module/applications/sheets-configs/action-base-config.mjs @@ -111,7 +111,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) context.source = this.action.toObject(true); context.summons = []; - for (const summon of context.source.summon) { + for (const summon of context.source.summon ?? []) { const actor = await foundry.utils.fromUuid(summon.actorUUID); context.summons.push({ actor, count: summon.count }); } @@ -261,7 +261,7 @@ export default class DHActionBaseConfig extends DaggerheartSheet(ApplicationV2) const wrapper = event.target.closest('.summon-count-wrapper'); const index = wrapper.dataset.index; const data = this.action.toObject(); - data.summon[index].count = Number.parseInt(event.target.value); + data.summon[index].count = event.target.value; this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); } diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index a9521b4a..ec4e6422 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -163,15 +163,13 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel */ getRollData(data = {}) { const actorData = this.actor ? this.actor.getRollData(false) : {}; + actorData.result = data.roll?.total ?? 1; + actorData.scale = data.costs?.length // Right now only return the first scalable cost. + ? (data.costs.find(c => c.scalable)?.total ?? 1) + : 1; + actorData.roll = {}; - return { - ...actorData, - result: data.roll?.total ?? 1, - scale: data.costs?.length // Right now only return the first scalable cost. - ? (data.costs.find(c => c.scalable)?.total ?? 1) - : 1, - roll: {} - }; + return actorData; } /** diff --git a/module/data/fields/action/summonField.mjs b/module/data/fields/action/summonField.mjs index da884c42..6ecb7296 100644 --- a/module/data/fields/action/summonField.mjs +++ b/module/data/fields/action/summonField.mjs @@ -1,3 +1,5 @@ +import FormulaField from '../formulaField.mjs'; + const fields = foundry.data.fields; export default class DHSummonField extends fields.ArrayField { @@ -12,11 +14,9 @@ export default class DHSummonField extends fields.ArrayField { type: 'Actor', required: true }), - count: new fields.NumberField({ + count: new FormulaField({ required: true, - default: 1, - min: 1, - integer: true + default: '1' }) }); super(summonFields, options, context); @@ -33,38 +33,57 @@ export default class DHSummonField extends fields.ArrayField { return; } - this.actor.sheet?.minimize(); + const rolls = []; const summonData = []; for (const summon of this.summon) { - /* Possibly check for any available instances in the world with prepared images */ - let actor = await foundry.utils.fromUuid(summon.actorUUID); - - /* Check for any available instances of the actor present in the world if we're missing artwork in the compendium */ - const dataType = game.system.api.data.actors[`Dh${actor.type.capitalize()}`]; - if (actor.inCompendium && dataType && actor.img === dataType.DEFAULT_ICON) { - const worldActorCopy = game.actors.find(x => x.name === actor.name); - actor = worldActorCopy ?? actor; + let count = summon.count; + const roll = new Roll(summon.count); + if (!roll.isDeterministic) { + await roll.evaluate(); + if (game.modules.get('dice-so-nice')?.active) rolls.push(roll); + count = roll.total; } - summonData.push({ actor, count: summon.count }); + const actor = DHSummonField.getWorldActor(await foundry.utils.fromUuid(summon.actorUUID)); + /* Extending summon data in memory so it's available in actionField.toChat. Think it's harmless, but ugly. Could maybe find a better way. */ + summon.rolledCount = count; + summon.actor = actor.toObject(); + + summonData.push({ actor, count: count }); } - const handleSummon = async summonIndex => { - const summon = summonData[summonIndex]; - const result = await CONFIG.ux.TokenManager.createPreviewAsync(summon.actor, { - name: `${summon.actor.prototypeToken.name}${summon.count > 1 ? ` (${summon.count}x)` : ''}` - }); - if (!result) return this.actor.sheet?.maximize(); + if (rolls.length) await Promise.all(rolls.map(roll => game.dice3d.showForRoll(roll, game.user, true))); - summon.count--; - if (summon.count === 0) { - summonIndex++; - if (summonIndex === summonData.length) return this.actor.sheet?.maximize(); - } + this.actor.sheet?.minimize(); + DHSummonField.handleSummon(summonData, this.actor); + } - handleSummon(summonIndex); - }; + /* Check for any available instances of the actor present in the world if we're missing artwork in the compendium */ + static getWorldActor(baseActor) { + const dataType = game.system.api.data.actors[`Dh${baseActor.type.capitalize()}`]; + if (baseActor.inCompendium && dataType && baseActor.img === dataType.DEFAULT_ICON) { + const worldActorCopy = game.actors.find(x => x.name === baseActor.name); + return worldActorCopy ?? baseActor; + } - handleSummon(0); + return baseActor; + } + + static async handleSummon(summonData, actionActor, summonIndex = 0) { + const summon = summonData[summonIndex]; + const result = await CONFIG.ux.TokenManager.createPreviewAsync(summon.actor, { + name: `${summon.actor.prototypeToken.name}${summon.count > 1 ? ` (${summon.count}x)` : ''}` + }); + + if (!result) return actionActor.sheet?.maximize(); + summon.actor = result.actor; + + summon.count--; + if (summon.count === 0) { + summonIndex++; + if (summonIndex === summonData.length) return actionActor.sheet?.maximize(); + } + + DHSummonField.handleSummon(summonData, actionActor, summonIndex); } } diff --git a/module/data/fields/actionField.mjs b/module/data/fields/actionField.mjs index 6257da38..3578c760 100644 --- a/module/data/fields/actionField.mjs +++ b/module/data/fields/actionField.mjs @@ -267,7 +267,8 @@ export function ActionMixin(Base) { action: { name: this.name, img: this.baseAction ? this.parent.parent.img : this.img, - tags: this.tags ? this.tags : ['Spell', 'Arcana', 'Lv 10'] + tags: this.tags ? this.tags : ['Spell', 'Arcana', 'Lv 10'], + summon: this.summon }, itemOrigin: this.item, description: this.description || (this.item instanceof Item ? this.item.system.description : '') diff --git a/module/documents/tokenManager.mjs b/module/documents/tokenManager.mjs index 0a878887..be5467da 100644 --- a/module/documents/tokenManager.mjs +++ b/module/documents/tokenManager.mjs @@ -94,11 +94,11 @@ export default class DhTokenManager { ? await game.system.api.documents.DhpActor.create(this.#actor.toObject()) : this.#actor; const tokenData = await actor.getTokenDocument(); - const token = await canvas.scene.createEmbeddedDocuments('Token', [ + const result = await canvas.scene.createEmbeddedDocuments('Token', [ { ...tokenData, x: this.#activePreview.document.x, y: this.#activePreview.document.y } ]); this.#activePreview = undefined; - if (this.#resolve) this.#resolve(token); + if (this.#resolve && result.length) this.#resolve(result[0]); } } diff --git a/styles/less/ui/chat/action.less b/styles/less/ui/chat/action.less index 817b0acd..bdc6a0ed 100644 --- a/styles/less/ui/chat/action.less +++ b/styles/less/ui/chat/action.less @@ -98,6 +98,51 @@ .description { padding: 8px; + + .summons-header { + font-size: var(--font-size-14); + text-align: center; + + span::before, + span::after { + color: @dark-blue; + } + &:before { + background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, @dark-blue 100%); + } + + &:after { + background: linear-gradient(90deg, @dark-blue 0%, rgba(0, 0, 0, 0) 100%); + } + } + + .summons-container { + display: flex; + flex-direction: column; + gap: 4px; + + .summon-container { + display: flex; + align-items: center; + justify-content: space-between; + + .summon-label-container { + flex: 1; + display: flex; + align-items: center; + gap: 4px; + + img { + height: 32px; + } + + label { + display: flex; + flex-wrap: wrap; + } + } + } + } } .ability-card-footer { diff --git a/templates/ui/chat/action.hbs b/templates/ui/chat/action.hbs index 6b505164..65bb0762 100644 --- a/templates/ui/chat/action.hbs +++ b/templates/ui/chat/action.hbs @@ -8,6 +8,22 @@
-
{{{description}}}
+
+ {{{description}}} + {{#if action.summon}} +
{{localize "DAGGERHEART.GENERAL.summon.plural"}}
+
+ {{#each action.summon}} +
+
+ + +
+ # {{this.rolledCount}} +
+ {{/each}} +
+ {{/if}} +
\ No newline at end of file From 3553dc0cf3fcefbcdcd6f1db7ff500538e62bff5 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Fri, 9 Jan 2026 21:14:30 +0100 Subject: [PATCH 16/18] Updated SRD --- ...ary_Arch_Necromancer_WPEOIGfclNJxWb87.json | 34 ++++---- ...rsary_Demon_of_Wrath_5lphJAgzoqZI3VoG.json | 21 ++--- .../adversary_Dryad_wR7cFKrHvRzbzhBT.json | 34 ++++---- ...adversary_Green_Ooze_SHXedd9zZPVfUgUa.json | 31 ++++--- ...versary_Head_Vampire_i2UNbRvgyoSs07M6.json | 36 ++++---- ...sary_Huge_Green_Ooze_6hbqmxDXFOzZJDk4.json | 34 ++++---- ...ged_Knife_Lieutenant_aTljstqteGoLpCBq.json | 30 ++++++- ...dversary_Petty_Noble_wycLpvebWdUqRhpP.json | 57 +++++-------- ...rsary_Pirate_Captain_OROJbjsqagVh7ECV.json | 31 ++++--- .../adversary_Red_Ooze_9rVlbJVrDNn1x7PS.json | 29 ++++--- ...ersary_Secret_Keeper_sLAccjvCWfeedbpI.json | 85 ++++++++++--------- ...rsary_Tangle_Bramble_XcAGOSmtCFLT1unN.json | 30 ++++++- 12 files changed, 254 insertions(+), 198 deletions(-) diff --git a/src/packs/adversaries/adversary_Arch_Necromancer_WPEOIGfclNJxWb87.json b/src/packs/adversaries/adversary_Arch_Necromancer_WPEOIGfclNJxWb87.json index 4fc58990..d4e506cb 100644 --- a/src/packs/adversaries/adversary_Arch_Necromancer_WPEOIGfclNJxWb87.json +++ b/src/packs/adversaries/adversary_Arch_Necromancer_WPEOIGfclNJxWb87.json @@ -533,33 +533,31 @@ "description": "

Spend a Fear to summon a @UUID[Compendium.daggerheart.adversaries.Actor.YhJrP7rTBiRdX5Fp]{Zombie Legion}, which appears at Close range and immediately takes the spotlight.

", "resource": null, "actions": { - "gZg3AkzCYUTExjE6": { - "type": "effect", - "_id": "gZg3AkzCYUTExjE6", + "qSuWxC8xQOhnbBx9": { + "type": "summon", + "_id": "qSuWxC8xQOhnbBx9", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", - "cost": [ - { - "scalable": false, - "key": "fear", - "value": 1, - "step": null - } - ], + "cost": [], "uses": { "value": null, "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "any", - "amount": null + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.YhJrP7rTBiRdX5Fp", + "count": "1" + } + ], "name": "Spend Fear", - "img": "icons/magic/death/undead-zombie-grave-green.webp", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Demon_of_Wrath_5lphJAgzoqZI3VoG.json b/src/packs/adversaries/adversary_Demon_of_Wrath_5lphJAgzoqZI3VoG.json index 9e838d6d..800e7305 100644 --- a/src/packs/adversaries/adversary_Demon_of_Wrath_5lphJAgzoqZI3VoG.json +++ b/src/packs/adversaries/adversary_Demon_of_Wrath_5lphJAgzoqZI3VoG.json @@ -457,11 +457,12 @@ "img": "icons/creatures/unholy/demon-fire-horned-clawed.webp", "range": "" }, - "7G6uWlFEeOLsJIWY": { - "type": "effect", - "_id": "7G6uWlFEeOLsJIWY", + "FlE6i0tbKEguF9wz": { + "type": "summon", + "_id": "FlE6i0tbKEguF9wz", "systemPath": "actions", - "description": "

Summon [[/r 1d4]]@UUID[Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb]{Minor Demons}, who appear at Close range.

", + "baseAction": false, + "description": "", "chatDisplay": true, "originItem": { "type": "itemCollection" @@ -474,13 +475,13 @@ "recovery": null, "consumeOnSuccess": false }, - "effects": [], - "target": { - "type": "any", - "amount": null - }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb", + "count": "1d4" + } + ], "name": "Summon", - "img": "icons/creatures/unholy/demon-fire-horned-clawed.webp", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Dryad_wR7cFKrHvRzbzhBT.json b/src/packs/adversaries/adversary_Dryad_wR7cFKrHvRzbzhBT.json index f0a5d81c..ca9ce647 100644 --- a/src/packs/adversaries/adversary_Dryad_wR7cFKrHvRzbzhBT.json +++ b/src/packs/adversaries/adversary_Dryad_wR7cFKrHvRzbzhBT.json @@ -363,33 +363,31 @@ "description": "

Spend a Fear to grow three @UUID[Compendium.daggerheart.adversaries.Actor.o63nS0k3wHu6EgKP]{Treant Sapling Minions}, who appear at Close range and immediately take the spotlight.

", "resource": null, "actions": { - "84Q2b0zIY9c7Yhho": { - "type": "effect", - "_id": "84Q2b0zIY9c7Yhho", + "R84DdS0OIx2cUt1w": { + "type": "summon", + "_id": "R84DdS0OIx2cUt1w", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", - "cost": [ - { - "scalable": false, - "key": "fear", - "value": 1, - "step": null - } - ], + "cost": [], "uses": { "value": null, "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "self", - "amount": null + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.o63nS0k3wHu6EgKP", + "count": "3" + } + ], "name": "Spend Fear", - "img": "icons/magic/unholy/orb-hands-pink.webp", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Green_Ooze_SHXedd9zZPVfUgUa.json b/src/packs/adversaries/adversary_Green_Ooze_SHXedd9zZPVfUgUa.json index c7446a11..b03b5495 100644 --- a/src/packs/adversaries/adversary_Green_Ooze_SHXedd9zZPVfUgUa.json +++ b/src/packs/adversaries/adversary_Green_Ooze_SHXedd9zZPVfUgUa.json @@ -510,34 +510,41 @@ "description": "

When the @Lookup[@name] has 3 or more HP marked, you can spend a Fear to split them into two @UUID[Compendium.daggerheart.adversaries.Actor.aLkLFuVoKz2NLoBK]{Tiny Green Oozes} (with no marked HP or Stress). Immediately spotlight both of them.

", "resource": null, "actions": { - "s5mLw6DRGd76MLcC": { - "type": "effect", - "_id": "s5mLw6DRGd76MLcC", + "J8U7dw3cDSsEirr5": { + "type": "summon", + "_id": "J8U7dw3cDSsEirr5", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", "cost": [ { "scalable": false, "key": "fear", "value": 1, - "step": null + "itemId": null, + "step": null, + "consumeOnSuccess": false } ], "uses": { "value": null, "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "self", - "amount": null + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.aLkLFuVoKz2NLoBK", + "count": "2" + } + ], "name": "Spend Fear", - "img": "icons/creatures/slimes/slime-movement-pseudopods-green.webp", - "range": "" + "range": "self" } }, "originItemType": null, diff --git a/src/packs/adversaries/adversary_Head_Vampire_i2UNbRvgyoSs07M6.json b/src/packs/adversaries/adversary_Head_Vampire_i2UNbRvgyoSs07M6.json index 9e948594..d5891359 100644 --- a/src/packs/adversaries/adversary_Head_Vampire_i2UNbRvgyoSs07M6.json +++ b/src/packs/adversaries/adversary_Head_Vampire_i2UNbRvgyoSs07M6.json @@ -474,33 +474,31 @@ "description": "

Spend 2 Fear to summon [[/r 1d4]] @UUID[Compendium.daggerheart.adversaries.Actor.WWyUp6Mxl1S3KYUG]{Vampires}, who appear at Far range and immediately take the spotlight.

", "resource": null, "actions": { - "5Q6RMUTiauKw0tDj": { - "type": "effect", - "_id": "5Q6RMUTiauKw0tDj", + "jGFOnU6PNdWU6iF4": { + "type": "summon", + "_id": "jGFOnU6PNdWU6iF4", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", - "cost": [ - { - "scalable": false, - "key": "fear", - "value": 2, - "step": null - } - ], + "cost": [], "uses": { "value": null, "max": "", - "recovery": null + "recovery": null, + "consumeOnSuccess": false }, - "effects": [], - "target": { - "type": "any", - "amount": null - }, - "name": "Summon Vampires", - "img": "icons/creatures/mammals/bat-giant-tattered-purple.webp", + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.WWyUp6Mxl1S3KYUG", + "count": "1d4" + } + ], + "name": "Spend Fear", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Huge_Green_Ooze_6hbqmxDXFOzZJDk4.json b/src/packs/adversaries/adversary_Huge_Green_Ooze_6hbqmxDXFOzZJDk4.json index 6f64f883..3bb8ae96 100644 --- a/src/packs/adversaries/adversary_Huge_Green_Ooze_6hbqmxDXFOzZJDk4.json +++ b/src/packs/adversaries/adversary_Huge_Green_Ooze_6hbqmxDXFOzZJDk4.json @@ -479,33 +479,31 @@ "description": "

When the @Lookup[@name] has 4 or more HP marked, you can spend a Fear to split them into two @UUID[Compendium.daggerheart.adversaries.Actor.SHXedd9zZPVfUgUa]{Green Oozes}(with no marked HP or Stress). Immediately spotlight both of them.

", "resource": null, "actions": { - "iQsYAqpUFvJslRDr": { - "type": "effect", - "_id": "iQsYAqpUFvJslRDr", + "aeRdkiRsDNagTKhp": { + "type": "summon", + "_id": "aeRdkiRsDNagTKhp", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", - "cost": [ - { - "scalable": false, - "key": "fear", - "value": 1, - "step": null - } - ], + "cost": [], "uses": { "value": null, "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "any", - "amount": null + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.SHXedd9zZPVfUgUa", + "count": "2" + } + ], "name": "Spend Fear", - "img": "icons/creatures/slimes/slime-movement-pseudopods-green.webp", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Jagged_Knife_Lieutenant_aTljstqteGoLpCBq.json b/src/packs/adversaries/adversary_Jagged_Knife_Lieutenant_aTljstqteGoLpCBq.json index 165bb160..c139d76f 100644 --- a/src/packs/adversaries/adversary_Jagged_Knife_Lieutenant_aTljstqteGoLpCBq.json +++ b/src/packs/adversaries/adversary_Jagged_Knife_Lieutenant_aTljstqteGoLpCBq.json @@ -287,7 +287,35 @@ "system": { "description": "

Summon three @Compendium[daggerheart.adversaries.Actor.C0OMQqV7pN6t7ouR], who appear at Far range.

", "resource": null, - "actions": {}, + "actions": { + "MCTBsw9lusUdubj0": { + "type": "summon", + "_id": "MCTBsw9lusUdubj0", + "systemPath": "actions", + "baseAction": false, + "description": "", + "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, + "actionType": "action", + "cost": [], + "uses": { + "value": null, + "max": "", + "recovery": null, + "consumeOnSuccess": false + }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.C0OMQqV7pN6t7ouR", + "count": "3" + } + ], + "name": "Summon", + "range": "" + } + }, "originItemType": null, "subType": null, "originId": null, diff --git a/src/packs/adversaries/adversary_Petty_Noble_wycLpvebWdUqRhpP.json b/src/packs/adversaries/adversary_Petty_Noble_wycLpvebWdUqRhpP.json index 4ac7e746..db284f40 100644 --- a/src/packs/adversaries/adversary_Petty_Noble_wycLpvebWdUqRhpP.json +++ b/src/packs/adversaries/adversary_Petty_Noble_wycLpvebWdUqRhpP.json @@ -258,57 +258,40 @@ "description": "

Once per scene, mark a Stress to summon 1d4 @UUID[Compendium.daggerheart.adversaries.Actor.B4LZcGuBAHzyVdzy]{Bladed Guards}, who appear at Far range to enforce the @Lookup[@name]’s will.

", "resource": null, "actions": { - "cUKwhq1imsTVru8D": { - "type": "attack", - "_id": "cUKwhq1imsTVru8D", + "tioTtYfIGFIXRITN": { + "type": "summon", + "_id": "tioTtYfIGFIXRITN", "systemPath": "actions", - "description": "

Once per scene, mark a Stress to summon 1d4 @UUID[Compendium.daggerheart.adversaries.Actor.B4LZcGuBAHzyVdzy]{Bladed Guards}, who appear at Far range to enforce the Noble’s will.

", + "baseAction": false, + "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", "cost": [ { "scalable": false, "key": "stress", "value": 1, - "step": null + "itemId": null, + "step": null, + "consumeOnSuccess": false } ], "uses": { "value": null, - "max": "", - "recovery": null - }, - "damage": { - "parts": [], - "includeBase": false - }, - "target": { - "type": "any", - "amount": null - }, - "effects": [], - "roll": { - "type": "diceSet", - "trait": null, - "difficulty": null, - "bonus": null, - "advState": "neutral", - "diceRolling": { - "multiplier": "prof", - "flatMultiplier": 1, - "dice": "d4", - "compare": null, - "treshold": null - }, - "useDefault": false - }, - "save": { - "trait": null, - "difficulty": null, - "damageMod": "none" + "max": "1", + "recovery": "scene", + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.B4LZcGuBAHzyVdzy", + "count": "1d4" + } + ], "name": "Summon Guards", - "img": "icons/environment/people/infantry-armored.webp", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Pirate_Captain_OROJbjsqagVh7ECV.json b/src/packs/adversaries/adversary_Pirate_Captain_OROJbjsqagVh7ECV.json index 409d7698..5b00ec60 100644 --- a/src/packs/adversaries/adversary_Pirate_Captain_OROJbjsqagVh7ECV.json +++ b/src/packs/adversaries/adversary_Pirate_Captain_OROJbjsqagVh7ECV.json @@ -313,36 +313,43 @@ "_id": "WGEGO0DSOs5cF0EL", "img": "icons/environment/people/charge.webp", "system": { - "description": "

Once per scene, mark a Stress to summon a Pirate Raiders Horde, which appears at Far range.

", + "description": "

Once per scene, mark a Stress to summon a @UUID[Compendium.daggerheart.adversaries.Actor.5YgEajn0wa4i85kC]{Pirate Raider Horde}, which appears at Far range.

", "resource": null, "actions": { - "NlgIp0KrmZoS27Xy": { - "type": "effect", - "_id": "NlgIp0KrmZoS27Xy", + "nuYk5WeLLpIKa69q": { + "type": "summon", + "_id": "nuYk5WeLLpIKa69q", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", "cost": [ { "scalable": false, "key": "stress", "value": 1, - "step": null + "itemId": null, + "step": null, + "consumeOnSuccess": false } ], "uses": { "value": null, "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "any", - "amount": null + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.5YgEajn0wa4i85kC", + "count": "1" + } + ], "name": "Mark Stress", - "img": "icons/environment/people/charge.webp", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Red_Ooze_9rVlbJVrDNn1x7PS.json b/src/packs/adversaries/adversary_Red_Ooze_9rVlbJVrDNn1x7PS.json index 320b71af..2c10ae3f 100644 --- a/src/packs/adversaries/adversary_Red_Ooze_9rVlbJVrDNn1x7PS.json +++ b/src/packs/adversaries/adversary_Red_Ooze_9rVlbJVrDNn1x7PS.json @@ -454,33 +454,40 @@ "description": "

When the @Lookup[@name] has 3 or more HP marked, you can spend a Fear to split them into two @UUID[Compendium.daggerheart.adversaries.Actor.1fkLQXVtmILqfJ44]{Tiny Red Oozes} (with no marked HP or Stress). Immediately spotlight both of them.

", "resource": null, "actions": { - "dw6Juw8mriH7sg0e": { - "type": "effect", - "_id": "dw6Juw8mriH7sg0e", + "BMEr77hDxaQyYBna": { + "type": "summon", + "_id": "BMEr77hDxaQyYBna", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", "cost": [ { "scalable": false, "key": "fear", "value": 1, - "step": null + "itemId": null, + "step": null, + "consumeOnSuccess": false } ], "uses": { "value": null, "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "any", - "amount": null + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.1fkLQXVtmILqfJ44", + "count": "2" + } + ], "name": "Spend Fear", - "img": "icons/creatures/slimes/slime-movement-splashing-red.webp", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Secret_Keeper_sLAccjvCWfeedbpI.json b/src/packs/adversaries/adversary_Secret_Keeper_sLAccjvCWfeedbpI.json index 0c8757c5..d17c3f86 100644 --- a/src/packs/adversaries/adversary_Secret_Keeper_sLAccjvCWfeedbpI.json +++ b/src/packs/adversaries/adversary_Secret_Keeper_sLAccjvCWfeedbpI.json @@ -416,28 +416,6 @@ "description": "

Countdown (6). When the @Lookup[@name] is in the spotlight for the first time, activate the countdown. When they mark HP, tick down this countdown by the number of HP marked. When it triggers, summon a @UUID[Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb]{Minor Demon} who appears at Close range.

", "resource": null, "actions": { - "0rixG6jLRynAYNqA": { - "type": "effect", - "_id": "0rixG6jLRynAYNqA", - "systemPath": "actions", - "description": "

Summon a @UUID[Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb]{Minor Demon} who appears at Close range.

", - "chatDisplay": true, - "actionType": "action", - "cost": [], - "uses": { - "value": null, - "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "any", - "amount": null - }, - "name": "Summon", - "img": "icons/magic/unholy/silhouette-light-fire-blue.webp", - "range": "close" - }, "ZVXHY2fpomoKV7jG": { "type": "countdown", "_id": "ZVXHY2fpomoKV7jG", @@ -474,6 +452,33 @@ "name": "Start Countdown", "img": "icons/magic/unholy/silhouette-light-fire-blue.webp", "range": "" + }, + "YReYG6DrWp4QGSij": { + "type": "summon", + "_id": "YReYG6DrWp4QGSij", + "systemPath": "actions", + "baseAction": false, + "description": "", + "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, + "actionType": "action", + "cost": [], + "uses": { + "value": null, + "max": "", + "recovery": null, + "consumeOnSuccess": false + }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb", + "count": "1" + } + ], + "name": "Summon", + "range": "" } }, "originItemType": null, @@ -502,33 +507,31 @@ "description": "

Once per scene, when the @Lookup[@name] marks 2 or more HP, you can mark a Stress to summon a @UUID[Compendium.daggerheart.adversaries.Actor.NoRZ1PqB8N5wcIw0]{Demonic Hound Pack}, which appears at Close range and is immediately spotlighted.

", "resource": null, "actions": { - "JBuQUJhif2A7IlJd": { - "type": "effect", - "_id": "JBuQUJhif2A7IlJd", + "tfmY6HYkkY27NBaF": { + "type": "summon", + "_id": "tfmY6HYkkY27NBaF", "systemPath": "actions", + "baseAction": false, "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", - "cost": [ - { - "scalable": false, - "key": "stress", - "value": 1, - "step": null - } - ], + "cost": [], "uses": { "value": null, - "max": "1", - "recovery": "scene" - }, - "effects": [], - "target": { - "type": "self", - "amount": null + "max": "", + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.NoRZ1PqB8N5wcIw0", + "count": "1" + } + ], "name": "Mark Stress", - "img": "icons/creatures/unholy/demon-fire-horned-clawed.webp", "range": "" } }, diff --git a/src/packs/adversaries/adversary_Tangle_Bramble_XcAGOSmtCFLT1unN.json b/src/packs/adversaries/adversary_Tangle_Bramble_XcAGOSmtCFLT1unN.json index a6e5ca17..0f1ba28f 100644 --- a/src/packs/adversaries/adversary_Tangle_Bramble_XcAGOSmtCFLT1unN.json +++ b/src/packs/adversaries/adversary_Tangle_Bramble_XcAGOSmtCFLT1unN.json @@ -340,7 +340,35 @@ "system": { "description": "

When an attack from the @Lookup[@name] causes a target to mark HP and there are three or more @Lookup[@name] Minions within Close range, you can combine the Minions into a @UUID[Compendium.daggerheart.adversaries.Actor.PKSXFuaIHUCoH63A]{Tangle Bramble Swarm Horde}. The Horde’s HP is equal to the number of Minions combined.

", "resource": null, - "actions": {}, + "actions": { + "g1OQ5xlMHFWsoktd": { + "type": "summon", + "_id": "g1OQ5xlMHFWsoktd", + "systemPath": "actions", + "baseAction": false, + "description": "", + "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, + "actionType": "action", + "cost": [], + "uses": { + "value": null, + "max": "", + "recovery": null, + "consumeOnSuccess": false + }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.PKSXFuaIHUCoH63A", + "count": "1" + } + ], + "name": "Summon", + "range": "" + } + }, "originItemType": null, "subType": null, "originId": null, From b1e56e1cd600ce2c0e2be8b10cb71372778351c5 Mon Sep 17 00:00:00 2001 From: Murilo Brito Date: Fri, 9 Jan 2026 19:14:44 -0300 Subject: [PATCH 17/18] bugfix: fix title lines not rendering in chat messages --- styles/less/ui/chat/action.less | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/styles/less/ui/chat/action.less b/styles/less/ui/chat/action.less index bdc6a0ed..8d309cfe 100644 --- a/styles/less/ui/chat/action.less +++ b/styles/less/ui/chat/action.less @@ -102,17 +102,27 @@ .summons-header { font-size: var(--font-size-14); text-align: center; + display: flex; + align-items: center; + justify-content: center; - span::before, - span::after { - color: @dark-blue; + span { + width: 100%; } + + &:before, + &:after { + content: ' '; + height: 1px; + width: 100%; + } + &:before { - background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, @dark-blue 100%); + background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, light-dark(@dark-blue, @golden) 100%); } &:after { - background: linear-gradient(90deg, @dark-blue 0%, rgba(0, 0, 0, 0) 100%); + background: linear-gradient(90deg, light-dark(@dark-blue, @golden) 0%, rgba(0, 0, 0, 0) 100%); } } From f47cbb41e82071fdbfbecf9a6e5a471d23aca43a Mon Sep 17 00:00:00 2001 From: WBHarry Date: Sat, 10 Jan 2026 01:35:12 +0100 Subject: [PATCH 18/18] Added summon actions to the easily doable environments in the SRD --- ...g_Heart_of_the_Woods_oY69NN4rYxoRE4hl.json | 2 +- ...ironment_Chaos_Realm_2Z1mKc65LxNk2PqR.json | 39 +++++++++++++------ ...ironment_Cult_Ritual_QAXXiOKBDmCTauHD.json | 21 +++++----- ...nt_Divine_Usurpation_4DLYez7VbMCFDAuZ.json | 36 ++++++++--------- ...onment_Mountain_Pass_acMu9wJrMZZzLSTJ.json | 30 +++++++++++++- ...ronment_Raging_River_t4cdqTfzcqP3H1vJ.json | 31 +++++++++------ 6 files changed, 104 insertions(+), 55 deletions(-) diff --git a/src/packs/environments/environment_Burning_Heart_of_the_Woods_oY69NN4rYxoRE4hl.json b/src/packs/environments/environment_Burning_Heart_of_the_Woods_oY69NN4rYxoRE4hl.json index dc42fb07..ea4f1951 100644 --- a/src/packs/environments/environment_Burning_Heart_of_the_Woods_oY69NN4rYxoRE4hl.json +++ b/src/packs/environments/environment_Burning_Heart_of_the_Woods_oY69NN4rYxoRE4hl.json @@ -314,7 +314,7 @@ "name": "Charcoal Constructs", "type": "feature", "system": { - "description": "

Warped animals wreathed in indigo f l ame trample through a point of your choice. All targets within Close range of that point must make an Agility Reaction Roll. Targets who fail take 3d12+3 physical damage. Targets who succeed take half damage instead.

@Template[type:emanation|range:c]

Are these real animals consumed by the fl ame or merely constructs of the corrupting magic?

", + "description": "

Warped animals wreathed in indigo flame trample through a point of your choice. All targets within Close range of that point must make an Agility Reaction Roll. Targets who fail take 3d12+3 physical damage. Targets who succeed take half damage instead.

@Template[type:emanation|range:c]

Are these real animals consumed by the fl ame or merely constructs of the corrupting magic?

", "resource": null, "actions": { "gbXIaKr8em134IZC": { diff --git a/src/packs/environments/environment_Chaos_Realm_2Z1mKc65LxNk2PqR.json b/src/packs/environments/environment_Chaos_Realm_2Z1mKc65LxNk2PqR.json index 77781de0..361b15bc 100644 --- a/src/packs/environments/environment_Chaos_Realm_2Z1mKc65LxNk2PqR.json +++ b/src/packs/environments/environment_Chaos_Realm_2Z1mKc65LxNk2PqR.json @@ -467,33 +467,48 @@ "description": "

Spend a Fear to summon an @UUID[Compendium.daggerheart.adversaries.Actor.A0SeeDzwjvqOsyof]{Outer Realms Abomination}, an@UUID[Compendium.daggerheart.adversaries.Actor.ms6nuOl3NFkhPj1k]{Outer Realms Corrupter}, and [[/r 2d6]] @UUID[Compendium.daggerheart.adversaries.Actor.moJhHgKqTKPS2WYS]{Outer Realms Thrall}, who appear at Close range of a chosen PC in defiance of logic and causality. Immediately spotlight one of these adversaries, and you can spend an additional Fear to automatically succeed on that adversary’s standard attack.

What halfconsumed remnants of the shattered world do these monstrosities cast aside in pursuit of living flesh? What jagged refl ections of former personhood do you catch between moments of unquestioning malice?

", "resource": null, "actions": { - "5a8ESNroEQHAm7rO": { - "type": "effect", - "_id": "5a8ESNroEQHAm7rO", + "KCzdCu2KhAx9KyhT": { + "type": "summon", + "_id": "KCzdCu2KhAx9KyhT", "systemPath": "actions", - "description": "

Spend a Fear to summon an @UUID[Compendium.daggerheart.adversaries.Actor.A0SeeDzwjvqOsyof]{Outer Realms Abomination}, an@UUID[Compendium.daggerheart.adversaries.Actor.ms6nuOl3NFkhPj1k]{Outer Realms Corrupter}, and [[/r 2d6]] @UUID[Compendium.daggerheart.adversaries.Actor.moJhHgKqTKPS2WYS]{Outer Realms Thrall}, who appear at Close range of a chosen PC in defiance of logic and causality. Immediately spotlight one of these adversaries, and you can spend an additional Fear to automatically succeed on that adversary’s standard attack.

What halfconsumed remnants of the shattered world do these monstrosities cast aside in pursuit of living flesh? What jagged refl ections of former personhood do you catch between moments of unquestioning malice?

", + "baseAction": false, + "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", "cost": [ { "scalable": false, "key": "fear", "value": 1, - "step": null + "itemId": null, + "step": null, + "consumeOnSuccess": false } ], "uses": { "value": null, "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "any", - "amount": null + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.A0SeeDzwjvqOsyof", + "count": "1" + }, + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.ms6nuOl3NFkhPj1k", + "count": "1" + }, + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.moJhHgKqTKPS2WYS", + "count": "2d6" + } + ], "name": "Spend Fear", - "img": "icons/creatures/unholy/demons-horned-glowing-pink.webp", "range": "" } }, diff --git a/src/packs/environments/environment_Cult_Ritual_QAXXiOKBDmCTauHD.json b/src/packs/environments/environment_Cult_Ritual_QAXXiOKBDmCTauHD.json index 705c9585..66931f0a 100644 --- a/src/packs/environments/environment_Cult_Ritual_QAXXiOKBDmCTauHD.json +++ b/src/packs/environments/environment_Cult_Ritual_QAXXiOKBDmCTauHD.json @@ -343,11 +343,12 @@ "img": "icons/magic/unholy/barrier-fire-pink.webp", "range": "" }, - "suFEnfpOfeVRvnJF": { - "type": "effect", - "_id": "suFEnfpOfeVRvnJF", + "HG7tbEdlYl3yLQnR": { + "type": "summon", + "_id": "HG7tbEdlYl3yLQnR", "systemPath": "actions", - "description": "

Summon a @UUID[Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb]{Minor Demon} within Very Close range of the ritual’s leader.

", + "baseAction": false, + "description": "", "chatDisplay": true, "originItem": { "type": "itemCollection" @@ -360,13 +361,13 @@ "recovery": null, "consumeOnSuccess": false }, - "effects": [], - "target": { - "type": "any", - "amount": null - }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.3tqCjDwJAQ7JKqMb", + "count": "1" + } + ], "name": "Summon Demon", - "img": "icons/magic/unholy/barrier-fire-pink.webp", "range": "" } }, diff --git a/src/packs/environments/environment_Divine_Usurpation_4DLYez7VbMCFDAuZ.json b/src/packs/environments/environment_Divine_Usurpation_4DLYez7VbMCFDAuZ.json index aacf87e9..d8e9cded 100644 --- a/src/packs/environments/environment_Divine_Usurpation_4DLYez7VbMCFDAuZ.json +++ b/src/packs/environments/environment_Divine_Usurpation_4DLYez7VbMCFDAuZ.json @@ -248,33 +248,31 @@ "description": "

Spend 2 Fear to summon [[/r 1d4+2]] @UUID[Compendium.daggerheart.adversaries.Actor.OsLG2BjaEdTZUJU9]{Fallen Shock Troop} that appear within Close range of the Usurper to assist their divine siege. Immediately spotlight the Shock Troops to use a “Group Attack” action.

Which High Fallen do these troops serve? Which god’s fl esh do they wish to feast upon?

", "resource": null, "actions": { - "qIQTEO5t72xFtKYI": { - "type": "effect", - "_id": "qIQTEO5t72xFtKYI", + "okcqGrI4rdghugUi": { + "type": "summon", + "_id": "okcqGrI4rdghugUi", "systemPath": "actions", - "description": "

Spend 2 Fear to summon [[/r 1d4+2]] @UUID[Compendium.daggerheart.adversaries.Actor.OsLG2BjaEdTZUJU9]{Fallen Shock Troop} that appear within Close range of the Usurper to assist their divine siege. Immediately spotlight the Shock Troops to use a “Group Attack” action.

Which High Fallen do these troops serve? Which god’s fl esh do they wish to feast upon?

", + "baseAction": false, + "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", - "cost": [ - { - "scalable": false, - "key": "fear", - "value": 2, - "step": null - } - ], + "cost": [], "uses": { "value": null, "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "self", - "amount": null + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.OsLG2BjaEdTZUJU9", + "count": "1d4+2" + } + ], "name": "Spend Fear", - "img": "icons/magic/unholy/orb-hands-pink.webp", "range": "" } }, diff --git a/src/packs/environments/environment_Mountain_Pass_acMu9wJrMZZzLSTJ.json b/src/packs/environments/environment_Mountain_Pass_acMu9wJrMZZzLSTJ.json index 8e7cf1c8..9ba6a918 100644 --- a/src/packs/environments/environment_Mountain_Pass_acMu9wJrMZZzLSTJ.json +++ b/src/packs/environments/environment_Mountain_Pass_acMu9wJrMZZzLSTJ.json @@ -246,7 +246,35 @@ "system": { "description": "

When the PCs enter the raptors’ hunting grounds, two @UUID[Compendium.daggerheart.adversaries.Actor.OMQ0v6PE8s1mSU0K]{Giant Eagles} appear at Very Far range of a chosen PC, identifying the PCs as likely prey.

How long has it been since the eagles last found prey? Do they have eggs in their nest or unfl edged young?

", "resource": null, - "actions": {}, + "actions": { + "88MyOC3IRcct6VLk": { + "type": "summon", + "_id": "88MyOC3IRcct6VLk", + "systemPath": "actions", + "baseAction": false, + "description": "", + "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, + "actionType": "action", + "cost": [], + "uses": { + "value": null, + "max": "", + "recovery": null, + "consumeOnSuccess": false + }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.OMQ0v6PE8s1mSU0K", + "count": "2" + } + ], + "name": "Summon", + "range": "" + } + }, "originItemType": null, "originId": null, "featureForm": "reaction" diff --git a/src/packs/environments/environment_Raging_River_t4cdqTfzcqP3H1vJ.json b/src/packs/environments/environment_Raging_River_t4cdqTfzcqP3H1vJ.json index 5c973fa6..6c34c296 100644 --- a/src/packs/environments/environment_Raging_River_t4cdqTfzcqP3H1vJ.json +++ b/src/packs/environments/environment_Raging_River_t4cdqTfzcqP3H1vJ.json @@ -360,33 +360,40 @@ "description": "

Spend a Fear to summon a @UUID[Compendium.daggerheart.adversaries.Actor.8KWVLWXFhlY2kYx0]{Glass Snake} within Close range of a chosen PC. The Snake appears in or near the river and immediately takes the spotlight to use their “Spinning Serpent” action.

What treasures does the beast have in their burrow? What travelers have already fallen victim to this predator?

", "resource": null, "actions": { - "Mnp0Yzc7EPVXm8So": { - "type": "effect", - "_id": "Mnp0Yzc7EPVXm8So", + "uY9HMKE4Q5g7bRKg": { + "type": "summon", + "_id": "uY9HMKE4Q5g7bRKg", "systemPath": "actions", - "description": "

Spend a Fear to summon a @UUID[Compendium.daggerheart.adversaries.Actor.8KWVLWXFhlY2kYx0]{Glass Snake} within Close range of a chosen PC. The Snake appears in or near the river and immediately takes the spotlight to use their “Spinning Serpent” action.

What treasures does the beast have in their burrow? What travelers have already fallen victim to this predator?

", + "baseAction": false, + "description": "", "chatDisplay": true, + "originItem": { + "type": "itemCollection" + }, "actionType": "action", "cost": [ { "scalable": false, "key": "fear", "value": 1, - "step": null + "itemId": null, + "step": null, + "consumeOnSuccess": false } ], "uses": { "value": null, "max": "", - "recovery": null - }, - "effects": [], - "target": { - "type": "self", - "amount": null + "recovery": null, + "consumeOnSuccess": false }, + "summon": [ + { + "actorUUID": "Compendium.daggerheart.adversaries.Actor.8KWVLWXFhlY2kYx0", + "count": "1" + } + ], "name": "Spend Fear", - "img": "icons/creatures/reptiles/snake-fangs-bite-green-yellow.webp", "range": "" } },