Fix Layout

This commit is contained in:
ChatGPT
2026-06-04 22:52:08 +02:00
parent db75a89401
commit 4a03a992d0
5 changed files with 228 additions and 76 deletions
+145 -30
View File
@@ -18,7 +18,8 @@ export class ConfigurableReactionsConfigApp extends HandlebarsApplicationMixin(A
classes: ["configurable-reactions", "standard-form"],
window: {
title: "CONFIGURABLE_REACTIONS.App.Title",
icon: "fa-solid fa-bolt"
icon: "fa-solid fa-bolt",
resizable: true
},
position: {
width: 1040,
@@ -33,6 +34,7 @@ export class ConfigurableReactionsConfigApp extends HandlebarsApplicationMixin(A
removeAction: ConfigurableReactionsConfigApp.#onRemoveAction,
saveReactionBasics: ConfigurableReactionsConfigApp.#onSaveReactionBasics,
clearTriggerSpell: ConfigurableReactionsConfigApp.#onClearTriggerSpell,
removeDamageType: ConfigurableReactionsConfigApp.#onRemoveDamageType,
clearActionSpell: ConfigurableReactionsConfigApp.#onClearActionSpell,
removeActionStatus: ConfigurableReactionsConfigApp.#onRemoveActionStatus
}
@@ -67,7 +69,12 @@ 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 activeTriggerIsDamage = selectedReaction?.trigger?.type === TRIGGER_TYPES.DAMAGE_RECEIVED;
const statusEffects = getStatusEffectOptions();
const damageTypes = getDamageTypeOptions();
const selectedDamageTypeIds = Array.from(new Set(selectedReaction?.conditions?.damage?.types ?? []));
const selectedDamageTypes = selectedDamageTypeIds.map(typeId => resolveDamageType(typeId, damageTypes));
const availableDamageTypes = damageTypes.filter(type => !selectedDamageTypeIds.includes(type.id));
const visualActions = (selectedReaction?.actions ?? []).map((action, index) => {
const paletteEntry = actionPalette.find(p => p.type === action.type);
const selectedStatusIds = Array.from(new Set(action.statuses ?? []));
@@ -101,9 +108,12 @@ export class ConfigurableReactionsConfigApp extends HandlebarsApplicationMixin(A
actionPalette,
activeTrigger,
activeTriggerAcceptsSpell,
activeTriggerIsDamage,
triggerSpellName,
visualActions,
statusEffects
statusEffects,
selectedDamageTypes,
availableDamageTypes
};
}
@@ -115,7 +125,10 @@ export class ConfigurableReactionsConfigApp extends HandlebarsApplicationMixin(A
this.render({ force: true });
});
this.#activateJsonEditor();
this.#activateBasicsSync();
this.#activateStatusSelectors();
this.#activateDamageTypeSelector();
this.#activateDragAndDrop();
}
@@ -130,6 +143,41 @@ export class ConfigurableReactionsConfigApp extends HandlebarsApplicationMixin(A
});
}
}
#activateJsonEditor() {
const textarea = this.element.querySelector("[name='reactionJson']");
if (!textarea) return;
textarea.addEventListener("blur", async () => {
if (document.activeElement === textarea) return;
await this.#saveReactionJsonFromTextarea({ render: true });
});
}
#activateBasicsSync() {
const nameInput = this.element.querySelector("[name='reactionName']");
const enabledInput = this.element.querySelector("[name='reactionEnabled']");
nameInput?.addEventListener("input", () => this.#syncJsonFromDesignerForm());
nameInput?.addEventListener("change", async () => this.#saveBasicsFromDesignerForm());
nameInput?.addEventListener("blur", async () => this.#saveBasicsFromDesignerForm());
enabledInput?.addEventListener("change", async () => {
this.#syncJsonFromDesignerForm();
await this.#saveBasicsFromDesignerForm();
});
}
#activateDamageTypeSelector() {
const select = this.element.querySelector("[data-cr-damage-type-select]");
if (!select) return;
select.addEventListener("change", async event => {
const damageType = event.currentTarget.value;
if (!damageType) return;
await this.#addDamageType(damageType);
});
}
#activateDragAndDrop() {
for (const draggable of this.element.querySelectorAll("[data-cr-drag]")) {
@@ -208,29 +256,7 @@ export class ConfigurableReactionsConfigApp extends HandlebarsApplicationMixin(A
}
static async #onSaveReactionJson(event, target) {
const textarea = this.element.querySelector("[name='reactionJson']");
if (!textarea) return;
let reaction;
try {
reaction = JSON.parse(textarea.value);
} catch (error) {
ui.notifications.error(game.i18n.localize("CONFIGURABLE_REACTIONS.Errors.InvalidJson"));
return;
}
if (!reaction.id) reaction.id = foundry.utils.randomID();
if (!reaction.name) reaction.name = game.i18n.localize("CONFIGURABLE_REACTIONS.Reactions.NewReaction");
reaction.actions ??= [];
const reactions = foundry.utils.deepClone(game.settings.get(MODULE_ID, SETTINGS.REACTIONS) ?? []);
const index = reactions.findIndex(r => r.id === reaction.id);
if (index >= 0) reactions[index] = reaction;
else reactions.push(reaction);
await game.settings.set(MODULE_ID, SETTINGS.REACTIONS, reactions);
this._selectedReactionId = reaction.id;
this.render({ force: true });
await this.#saveReactionJsonFromTextarea({ render: true });
}
static async #onDeleteReaction(event, target) {
@@ -308,16 +334,21 @@ export class ConfigurableReactionsConfigApp extends HandlebarsApplicationMixin(A
});
}
static async #onSaveReactionBasics(event, target) {
const name = this.element.querySelector("[name='reactionName']")?.value?.trim();
const enabled = this.element.querySelector("[name='reactionEnabled']")?.checked === true;
static async #onRemoveDamageType(event, target) {
const damageType = target.dataset.damageType;
if (!damageType) return;
await this.#mutateSelectedReaction(reaction => {
if (name) reaction.name = name;
reaction.enabled = enabled;
reaction.conditions ??= {};
reaction.conditions.damage ??= {};
reaction.conditions.damage.types = (reaction.conditions.damage.types ?? []).filter(type => type !== damageType);
});
}
static async #onSaveReactionBasics(event, target) {
await this.#saveBasicsFromDesignerForm();
}
async #setTrigger(triggerType) {
await this.#mutateSelectedReaction(reaction => {
reaction.trigger ??= {};
@@ -361,12 +392,77 @@ export class ConfigurableReactionsConfigApp extends HandlebarsApplicationMixin(A
});
}
async #addDamageType(damageType) {
await this.#mutateSelectedReaction(reaction => {
reaction.conditions ??= {};
reaction.conditions.damage ??= { enabled: true, amountMode: "damageOnly", types: [], typeMode: "any", minAmount: 1 };
reaction.conditions.damage.types ??= [];
if (!reaction.conditions.damage.types.includes(damageType)) reaction.conditions.damage.types.push(damageType);
});
}
async #saveReactionJsonFromTextarea({ render = true } = {}) {
const textarea = this.element.querySelector("[name='reactionJson']");
if (!textarea) return false;
let reaction;
try {
reaction = JSON.parse(textarea.value);
} catch (error) {
ui.notifications.error(game.i18n.localize("CONFIGURABLE_REACTIONS.Errors.InvalidJson"));
return false;
}
if (!reaction.id) reaction.id = foundry.utils.randomID();
if (!reaction.name) reaction.name = game.i18n.localize("CONFIGURABLE_REACTIONS.Reactions.NewReaction");
reaction.actions ??= [];
const reactions = foundry.utils.deepClone(game.settings.get(MODULE_ID, SETTINGS.REACTIONS) ?? []);
const index = reactions.findIndex(r => r.id === reaction.id);
if (index >= 0) reactions[index] = reaction;
else reactions.push(reaction);
await game.settings.set(MODULE_ID, SETTINGS.REACTIONS, reactions);
this._selectedReactionId = reaction.id;
if (render) this.render({ force: true });
return true;
}
async #saveBasicsFromDesignerForm() {
const name = this.element.querySelector("[name='reactionName']")?.value?.trim();
const enabled = this.element.querySelector("[name='reactionEnabled']")?.checked === true;
await this.#mutateSelectedReaction(reaction => {
if (name) reaction.name = name;
reaction.enabled = enabled;
});
}
#syncJsonFromDesignerForm() {
const reactions = foundry.utils.deepClone(game.settings.get(MODULE_ID, SETTINGS.REACTIONS) ?? []);
const reaction = reactions.find(r => r.id === this._selectedReactionId);
if (!reaction) return;
const name = this.element.querySelector("[name='reactionName']")?.value?.trim();
const enabled = this.element.querySelector("[name='reactionEnabled']")?.checked === true;
if (name) reaction.name = name;
reaction.enabled = enabled;
this.#updateJsonTextarea(reaction);
}
#updateJsonTextarea(reaction) {
const textarea = this.element.querySelector("[name='reactionJson']");
if (!textarea || document.activeElement === textarea) return;
textarea.value = JSON.stringify(reaction, null, 2);
}
async #mutateSelectedReaction(mutator) {
const reactions = foundry.utils.deepClone(game.settings.get(MODULE_ID, SETTINGS.REACTIONS) ?? []);
const reaction = reactions.find(r => r.id === this._selectedReactionId);
if (!reaction) return;
mutator(reaction);
this.#updateJsonTextarea(reaction);
await game.settings.set(MODULE_ID, SETTINGS.REACTIONS, reactions);
this.render({ force: true });
}
@@ -495,6 +591,25 @@ function resolveStatusEffect(statusId, statusEffects = getStatusEffectOptions())
};
}
function getDamageTypeOptions() {
const damageTypes = CONFIG.DND5E?.damageTypes ?? {};
const entries = damageTypes instanceof Map
? Array.from(damageTypes.entries())
: Object.entries(damageTypes);
return entries.map(([id, value]) => ({
id,
label: game.i18n.localize(value?.label ?? value ?? id)
})).sort((a, b) => a.label.localeCompare(b.label, game.i18n.lang));
}
function resolveDamageType(typeId, damageTypes = getDamageTypeOptions()) {
return damageTypes.find(type => type.id === typeId) ?? {
id: typeId,
label: typeId
};
}
function summarizeAction(action, statusEffects = getStatusEffectOptions()) {
switch (action.type) {
case ACTION_TYPES.TELEPORT: