Add status effect selector
This commit is contained in:
parent
c2ffa47ff2
commit
db75a89401
@ -68,6 +68,9 @@
|
|||||||
"CONFIGURABLE_REACTIONS.Actions.ApplyStatus.Label": "Status setzen",
|
"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.Hint": "Setzt einen oder mehrere Foundry-Status auf dem betroffenen Actor.",
|
||||||
"CONFIGURABLE_REACTIONS.Actions.ApplyStatus.NoStatuses": "Keine Status ausgewählt",
|
"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.Label": "Teleport",
|
||||||
"CONFIGURABLE_REACTIONS.Actions.Teleport.Hint": "Teleportiert den Token in einen gültigen freien Bereich innerhalb des Radius.",
|
"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}",
|
"CONFIGURABLE_REACTIONS.Actions.Teleport.Summary": "{radius} ft · {mode}",
|
||||||
|
|||||||
@ -68,6 +68,9 @@
|
|||||||
"CONFIGURABLE_REACTIONS.Actions.ApplyStatus.Label": "Apply status",
|
"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.Hint": "Applies one or more Foundry statuses to the affected actor.",
|
||||||
"CONFIGURABLE_REACTIONS.Actions.ApplyStatus.NoStatuses": "No statuses selected",
|
"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.Label": "Teleport",
|
||||||
"CONFIGURABLE_REACTIONS.Actions.Teleport.Hint": "Teleports the token to a valid free space within the radius.",
|
"CONFIGURABLE_REACTIONS.Actions.Teleport.Hint": "Teleports the token to a valid free space within the radius.",
|
||||||
"CONFIGURABLE_REACTIONS.Actions.Teleport.Summary": "{radius} ft · {mode}",
|
"CONFIGURABLE_REACTIONS.Actions.Teleport.Summary": "{radius} ft · {mode}",
|
||||||
|
|||||||
@ -33,7 +33,8 @@ export class ConfigurableReactionsConfigApp extends HandlebarsApplicationMixin(A
|
|||||||
removeAction: ConfigurableReactionsConfigApp.#onRemoveAction,
|
removeAction: ConfigurableReactionsConfigApp.#onRemoveAction,
|
||||||
saveReactionBasics: ConfigurableReactionsConfigApp.#onSaveReactionBasics,
|
saveReactionBasics: ConfigurableReactionsConfigApp.#onSaveReactionBasics,
|
||||||
clearTriggerSpell: ConfigurableReactionsConfigApp.#onClearTriggerSpell,
|
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 activeTrigger = triggerPalette.find(t => t.type === selectedReaction?.trigger?.type) ?? null;
|
||||||
const triggerSpellName = selectedReaction?.trigger?.spell?.itemName || selectedReaction?.trigger?.spell?.itemUuid || "";
|
const triggerSpellName = selectedReaction?.trigger?.spell?.itemName || selectedReaction?.trigger?.spell?.itemUuid || "";
|
||||||
const activeTriggerAcceptsSpell = isSpellTrigger(selectedReaction?.trigger?.type);
|
const activeTriggerAcceptsSpell = isSpellTrigger(selectedReaction?.trigger?.type);
|
||||||
|
const statusEffects = getStatusEffectOptions();
|
||||||
const visualActions = (selectedReaction?.actions ?? []).map((action, index) => {
|
const visualActions = (selectedReaction?.actions ?? []).map((action, index) => {
|
||||||
const paletteEntry = actionPalette.find(p => p.type === action.type);
|
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 {
|
return {
|
||||||
...action,
|
...action,
|
||||||
index,
|
index,
|
||||||
label: paletteEntry?.label ?? action.type,
|
label: paletteEntry?.label ?? action.type,
|
||||||
icon: paletteEntry?.icon ?? "fa-solid fa-puzzle-piece",
|
icon: paletteEntry?.icon ?? "fa-solid fa-puzzle-piece",
|
||||||
summary: summarizeAction(action),
|
summary: summarizeAction(action, statusEffects),
|
||||||
isSpellAction: action.type === ACTION_TYPES.CAST_SPELL_FROM_TOKEN,
|
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,
|
activeTriggerAcceptsSpell,
|
||||||
triggerSpellName,
|
triggerSpellName,
|
||||||
visualActions,
|
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.render({ force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.#activateStatusSelectors();
|
||||||
this.#activateDragAndDrop();
|
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() {
|
#activateDragAndDrop() {
|
||||||
for (const draggable of this.element.querySelectorAll("[data-cr-drag]")) {
|
for (const draggable of this.element.querySelectorAll("[data-cr-drag]")) {
|
||||||
draggable.setAttribute("draggable", "true");
|
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) {
|
static async #onSaveReactionBasics(event, target) {
|
||||||
const name = this.element.querySelector("[name='reactionName']")?.value?.trim();
|
const name = this.element.querySelector("[name='reactionName']")?.value?.trim();
|
||||||
const enabled = this.element.querySelector("[name='reactionEnabled']")?.checked === true;
|
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) {
|
async #mutateSelectedReaction(mutator) {
|
||||||
const reactions = foundry.utils.deepClone(game.settings.get(MODULE_ID, SETTINGS.REACTIONS) ?? []);
|
const reactions = foundry.utils.deepClone(game.settings.get(MODULE_ID, SETTINGS.REACTIONS) ?? []);
|
||||||
const reaction = reactions.find(r => r.id === this._selectedReactionId);
|
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) {
|
switch (action.type) {
|
||||||
case ACTION_TYPES.TELEPORT:
|
case ACTION_TYPES.TELEPORT:
|
||||||
return game.i18n.format("CONFIGURABLE_REACTIONS.Actions.Teleport.Summary", {
|
return game.i18n.format("CONFIGURABLE_REACTIONS.Actions.Teleport.Summary", {
|
||||||
@ -445,7 +504,9 @@ function summarizeAction(action) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
case ACTION_TYPES.APPLY_STATUS: {
|
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"}`;
|
const duration = action.duration?.type === "unlimited" ? game.i18n.localize("CONFIGURABLE_REACTIONS.Duration.Unlimited") : `${action.duration?.value ?? 1} ${action.duration?.type ?? "rounds"}`;
|
||||||
return `${statuses} · ${duration}`;
|
return `${statuses} · ${duration}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -197,3 +197,62 @@
|
|||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
justify-content: end;
|
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%;
|
||||||
|
}
|
||||||
|
|||||||
@ -130,6 +130,32 @@
|
|||||||
<small class="cr-inline-drop">{{localize "CONFIGURABLE_REACTIONS.Builder.DropSpellOnAction"}}</small>
|
<small class="cr-inline-drop">{{localize "CONFIGURABLE_REACTIONS.Builder.DropSpellOnAction"}}</small>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{#if this.isStatusAction}}
|
||||||
|
<div class="cr-status-builder">
|
||||||
|
<small class="cr-status-label">{{localize "CONFIGURABLE_REACTIONS.Actions.ApplyStatus.SelectedStatuses"}}</small>
|
||||||
|
<div class="cr-status-chip-list">
|
||||||
|
{{#if this.selectedStatuses.length}}
|
||||||
|
{{#each this.selectedStatuses}}
|
||||||
|
<span class="cr-status-chip" title="{{this.label}}">
|
||||||
|
<img src="{{this.icon}}" alt="">
|
||||||
|
<span>{{this.label}}</span>
|
||||||
|
<button type="button" data-action="removeActionStatus" data-action-index="{{../index}}" data-status-id="{{this.id}}" title="{{localize 'CONFIGURABLE_REACTIONS.Actions.ApplyStatus.RemoveStatus' status=this.label}}">
|
||||||
|
<i class="fa-solid fa-xmark"></i>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
{{/each}}
|
||||||
|
{{else}}
|
||||||
|
<small class="cr-inline-drop">{{localize "CONFIGURABLE_REACTIONS.Actions.ApplyStatus.NoStatuses"}}</small>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
<select data-cr-status-select data-action-index="{{this.index}}">
|
||||||
|
<option value="">{{localize "CONFIGURABLE_REACTIONS.Actions.ApplyStatus.SelectPlaceholder"}}</option>
|
||||||
|
{{#each this.availableStatuses}}
|
||||||
|
<option value="{{this.id}}">{{this.label}}</option>
|
||||||
|
{{/each}}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
<div class="cr-action-buttons">
|
<div class="cr-action-buttons">
|
||||||
{{#if this.spellName}}
|
{{#if this.spellName}}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user