diff --git a/lang/de.json b/lang/de.json index 1c02182..a20e0d9 100644 --- a/lang/de.json +++ b/lang/de.json @@ -68,6 +68,9 @@ "CONFIGURABLE_REACTIONS.Actions.ApplyStatus.Label": "Status setzen", "CONFIGURABLE_REACTIONS.Actions.ApplyStatus.Hint": "Setzt einen oder mehrere Foundry-Status auf dem betroffenen Actor.", "CONFIGURABLE_REACTIONS.Actions.ApplyStatus.NoStatuses": "Keine Status ausgewählt", + "CONFIGURABLE_REACTIONS.Actions.ApplyStatus.SelectedStatuses": "Gewählte Statuseffekte", + "CONFIGURABLE_REACTIONS.Actions.ApplyStatus.SelectPlaceholder": "Status auswählen", + "CONFIGURABLE_REACTIONS.Actions.ApplyStatus.RemoveStatus": "Status entfernen: {status}", "CONFIGURABLE_REACTIONS.Actions.Teleport.Label": "Teleport", "CONFIGURABLE_REACTIONS.Actions.Teleport.Hint": "Teleportiert den Token in einen gültigen freien Bereich innerhalb des Radius.", "CONFIGURABLE_REACTIONS.Actions.Teleport.Summary": "{radius} ft · {mode}", diff --git a/lang/en.json b/lang/en.json index 16c740c..d4bd718 100644 --- a/lang/en.json +++ b/lang/en.json @@ -68,6 +68,9 @@ "CONFIGURABLE_REACTIONS.Actions.ApplyStatus.Label": "Apply status", "CONFIGURABLE_REACTIONS.Actions.ApplyStatus.Hint": "Applies one or more Foundry statuses to the affected actor.", "CONFIGURABLE_REACTIONS.Actions.ApplyStatus.NoStatuses": "No statuses selected", + "CONFIGURABLE_REACTIONS.Actions.ApplyStatus.SelectedStatuses": "Selected status effects", + "CONFIGURABLE_REACTIONS.Actions.ApplyStatus.SelectPlaceholder": "Select status", + "CONFIGURABLE_REACTIONS.Actions.ApplyStatus.RemoveStatus": "Remove status: {status}", "CONFIGURABLE_REACTIONS.Actions.Teleport.Label": "Teleport", "CONFIGURABLE_REACTIONS.Actions.Teleport.Hint": "Teleports the token to a valid free space within the radius.", "CONFIGURABLE_REACTIONS.Actions.Teleport.Summary": "{radius} ft · {mode}", diff --git a/scripts/apps/reaction-config-app.js b/scripts/apps/reaction-config-app.js index c5b1545..0c02381 100644 --- a/scripts/apps/reaction-config-app.js +++ b/scripts/apps/reaction-config-app.js @@ -33,7 +33,8 @@ export class ConfigurableReactionsConfigApp extends HandlebarsApplicationMixin(A removeAction: ConfigurableReactionsConfigApp.#onRemoveAction, saveReactionBasics: ConfigurableReactionsConfigApp.#onSaveReactionBasics, clearTriggerSpell: ConfigurableReactionsConfigApp.#onClearTriggerSpell, - clearActionSpell: ConfigurableReactionsConfigApp.#onClearActionSpell + clearActionSpell: ConfigurableReactionsConfigApp.#onClearActionSpell, + removeActionStatus: ConfigurableReactionsConfigApp.#onRemoveActionStatus } }; @@ -66,16 +67,23 @@ export class ConfigurableReactionsConfigApp extends HandlebarsApplicationMixin(A const activeTrigger = triggerPalette.find(t => t.type === selectedReaction?.trigger?.type) ?? null; const triggerSpellName = selectedReaction?.trigger?.spell?.itemName || selectedReaction?.trigger?.spell?.itemUuid || ""; const activeTriggerAcceptsSpell = isSpellTrigger(selectedReaction?.trigger?.type); + const statusEffects = getStatusEffectOptions(); const visualActions = (selectedReaction?.actions ?? []).map((action, index) => { const paletteEntry = actionPalette.find(p => p.type === action.type); + const selectedStatusIds = Array.from(new Set(action.statuses ?? [])); + const selectedStatuses = selectedStatusIds.map(statusId => resolveStatusEffect(statusId, statusEffects)); + return { ...action, index, label: paletteEntry?.label ?? action.type, icon: paletteEntry?.icon ?? "fa-solid fa-puzzle-piece", - summary: summarizeAction(action), + summary: summarizeAction(action, statusEffects), isSpellAction: action.type === ACTION_TYPES.CAST_SPELL_FROM_TOKEN, - spellName: action.spellSelection?.itemName || action.spellSelection?.itemUuid || "" + spellName: action.spellSelection?.itemName || action.spellSelection?.itemUuid || "", + isStatusAction: action.type === ACTION_TYPES.APPLY_STATUS, + selectedStatuses, + availableStatuses: statusEffects.filter(status => !selectedStatusIds.includes(status.id)) }; }); @@ -95,7 +103,7 @@ export class ConfigurableReactionsConfigApp extends HandlebarsApplicationMixin(A activeTriggerAcceptsSpell, triggerSpellName, visualActions, - statusEffects: CONFIG.statusEffects.map(s => ({ id: s.id, name: game.i18n.localize(s.name ?? s.id) })) + statusEffects }; } @@ -107,9 +115,22 @@ export class ConfigurableReactionsConfigApp extends HandlebarsApplicationMixin(A this.render({ force: true }); }); + this.#activateStatusSelectors(); this.#activateDragAndDrop(); } + #activateStatusSelectors() { + for (const select of this.element.querySelectorAll("[data-cr-status-select]")) { + select.addEventListener("change", async event => { + const actionIndex = Number(event.currentTarget.dataset.actionIndex); + const statusId = event.currentTarget.value; + if (!Number.isInteger(actionIndex) || !statusId) return; + + await this.#addActionStatus(actionIndex, statusId); + }); + } + } + #activateDragAndDrop() { for (const draggable of this.element.querySelectorAll("[data-cr-drag]")) { draggable.setAttribute("draggable", "true"); @@ -275,6 +296,18 @@ export class ConfigurableReactionsConfigApp extends HandlebarsApplicationMixin(A }); } + static async #onRemoveActionStatus(event, target) { + const actionIndex = Number(target.dataset.actionIndex); + const statusId = target.dataset.statusId; + if (!Number.isInteger(actionIndex) || !statusId) return; + + await this.#mutateSelectedReaction(reaction => { + const action = reaction.actions?.[actionIndex]; + if (!action || action.type !== ACTION_TYPES.APPLY_STATUS) return; + action.statuses = (action.statuses ?? []).filter(id => id !== statusId); + }); + } + static async #onSaveReactionBasics(event, target) { const name = this.element.querySelector("[name='reactionName']")?.value?.trim(); const enabled = this.element.querySelector("[name='reactionEnabled']")?.checked === true; @@ -318,6 +351,16 @@ export class ConfigurableReactionsConfigApp extends HandlebarsApplicationMixin(A }); } + async #addActionStatus(actionIndex, statusId) { + await this.#mutateSelectedReaction(reaction => { + const action = reaction.actions?.[actionIndex]; + if (!action || action.type !== ACTION_TYPES.APPLY_STATUS) return; + + action.statuses ??= []; + if (!action.statuses.includes(statusId)) action.statuses.push(statusId); + }); + } + async #mutateSelectedReaction(mutator) { const reactions = foundry.utils.deepClone(game.settings.get(MODULE_ID, SETTINGS.REACTIONS) ?? []); const reaction = reactions.find(r => r.id === this._selectedReactionId); @@ -436,7 +479,23 @@ function createDefaultAction(type, item = null) { } } -function summarizeAction(action) { +function getStatusEffectOptions() { + return (CONFIG.statusEffects ?? []).map(status => ({ + id: status.id, + label: game.i18n.localize(status.name ?? status.label ?? status.id), + icon: status.img ?? status.icon ?? "icons/svg/aura.svg" + })).sort((a, b) => a.label.localeCompare(b.label, game.i18n.lang)); +} + +function resolveStatusEffect(statusId, statusEffects = getStatusEffectOptions()) { + return statusEffects.find(status => status.id === statusId) ?? { + id: statusId, + label: statusId, + icon: "icons/svg/aura.svg" + }; +} + +function summarizeAction(action, statusEffects = getStatusEffectOptions()) { switch (action.type) { case ACTION_TYPES.TELEPORT: return game.i18n.format("CONFIGURABLE_REACTIONS.Actions.Teleport.Summary", { @@ -445,7 +504,9 @@ function summarizeAction(action) { }); case ACTION_TYPES.APPLY_STATUS: { - const statuses = action.statuses?.length ? action.statuses.join(", ") : game.i18n.localize("CONFIGURABLE_REACTIONS.Actions.ApplyStatus.NoStatuses"); + const statuses = action.statuses?.length + ? action.statuses.map(statusId => resolveStatusEffect(statusId, statusEffects).label).join(", ") + : game.i18n.localize("CONFIGURABLE_REACTIONS.Actions.ApplyStatus.NoStatuses"); const duration = action.duration?.type === "unlimited" ? game.i18n.localize("CONFIGURABLE_REACTIONS.Duration.Unlimited") : `${action.duration?.value ?? 1} ${action.duration?.type ?? "rounds"}`; return `${statuses} · ${duration}`; } diff --git a/styles/configurable-reactions.css b/styles/configurable-reactions.css index b4e7bf6..0d27929 100644 --- a/styles/configurable-reactions.css +++ b/styles/configurable-reactions.css @@ -197,3 +197,62 @@ gap: 0.25rem; justify-content: end; } + +.configurable-reactions .cr-status-builder { + display: flex; + flex-direction: column; + gap: 0.35rem; + margin-top: 0.5rem; +} + +.configurable-reactions .cr-status-label { + opacity: 0.85; + font-weight: 600; +} + +.configurable-reactions .cr-status-chip-list { + display: flex; + flex-wrap: wrap; + gap: 0.25rem; + min-height: 1.6rem; + padding: 0.25rem; + border: 1px solid var(--color-border-light-tertiary, #888); + border-radius: 5px; + background: rgb(0 0 0 / 0.08); +} + +.configurable-reactions .cr-status-chip { + display: inline-flex; + align-items: center; + gap: 0.3rem; + max-width: 100%; + padding: 0.15rem 0.25rem 0.15rem 0.35rem; + border: 1px solid var(--color-border-light-secondary, #777); + border-radius: 999px; + background: rgb(0 0 0 / 0.16); + font-size: 0.8rem; +} + +.configurable-reactions .cr-status-chip img { + width: 16px; + height: 16px; + border: none; + object-fit: contain; +} + +.configurable-reactions .cr-status-chip span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.configurable-reactions .cr-status-chip button { + width: 1.35rem; + min-height: 1.35rem; + padding: 0; + line-height: 1; +} + +.configurable-reactions .cr-status-builder select { + width: 100%; +} diff --git a/templates/reaction-config.hbs b/templates/reaction-config.hbs index a19c302..d58f2a1 100644 --- a/templates/reaction-config.hbs +++ b/templates/reaction-config.hbs @@ -130,6 +130,32 @@ {{localize "CONFIGURABLE_REACTIONS.Builder.DropSpellOnAction"}} {{/if}} {{/if}} + {{#if this.isStatusAction}} +
+ {{localize "CONFIGURABLE_REACTIONS.Actions.ApplyStatus.SelectedStatuses"}} +
+ {{#if this.selectedStatuses.length}} + {{#each this.selectedStatuses}} + + + {{this.label}} + + + {{/each}} + {{else}} + {{localize "CONFIGURABLE_REACTIONS.Actions.ApplyStatus.NoStatuses"}} + {{/if}} +
+ +
+ {{/if}}
{{#if this.spellName}}