Add configuration app and token assignment workflow
This commit is contained in:
parent
a243776070
commit
584bb724b8
161
scripts/apps/assignment-manager.js
Normal file
161
scripts/apps/assignment-manager.js
Normal file
@ -0,0 +1,161 @@
|
||||
import { MODULE_ID, SETTINGS } from "../constants.js";
|
||||
|
||||
export async function assignReactionToSelectedTokens(reactionId) {
|
||||
if (!game.user.isGM) {
|
||||
ui.notifications.warn(game.i18n.localize("CONFIGURABLE_REACTIONS.Errors.GmOnly"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!reactionId) {
|
||||
ui.notifications.warn(game.i18n.localize("CONFIGURABLE_REACTIONS.Errors.NoReactionSelected"));
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedTokens = canvas?.tokens?.controlled ?? [];
|
||||
if (!selectedTokens.length) {
|
||||
ui.notifications.warn(game.i18n.localize("CONFIGURABLE_REACTIONS.Errors.NoTokensSelected"));
|
||||
return;
|
||||
}
|
||||
|
||||
const reactions = game.settings.get(MODULE_ID, SETTINGS.REACTIONS) ?? [];
|
||||
const reaction = reactions.find(r => r.id === reactionId);
|
||||
if (!reaction) {
|
||||
ui.notifications.error(game.i18n.localize("CONFIGURABLE_REACTIONS.Errors.ReactionNotFound"));
|
||||
return;
|
||||
}
|
||||
|
||||
const assignments = foundry.utils.deepClone(game.settings.get(MODULE_ID, SETTINGS.ASSIGNMENTS) ?? []);
|
||||
let assignedCount = 0;
|
||||
let skippedCount = 0;
|
||||
|
||||
for (const token of selectedTokens) {
|
||||
const tokenDocument = token.document;
|
||||
const actor = token.actor;
|
||||
|
||||
if (!actor) {
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const assignment = buildAssignmentForToken(tokenDocument, actor, reactionId);
|
||||
const alreadyAssigned = assignments.some(existing =>
|
||||
existing.reactionId === assignment.reactionId &&
|
||||
existing.mode === assignment.mode &&
|
||||
existing.actorUuid === assignment.actorUuid &&
|
||||
existing.tokenUuid === assignment.tokenUuid
|
||||
);
|
||||
|
||||
if (alreadyAssigned) {
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
assignments.push(assignment);
|
||||
|
||||
if (assignment.mode === "actor") {
|
||||
await addReactionFlag(actor, reactionId);
|
||||
await ensureManagedReactionEffectOnActor(actor, reaction);
|
||||
} else {
|
||||
await addReactionFlag(tokenDocument, reactionId);
|
||||
}
|
||||
|
||||
assignedCount++;
|
||||
}
|
||||
|
||||
await game.settings.set(MODULE_ID, SETTINGS.ASSIGNMENTS, assignments);
|
||||
ui.notifications.info(game.i18n.format("CONFIGURABLE_REACTIONS.Assignments.AssignedResult", { assigned: assignedCount, skipped: skippedCount }));
|
||||
}
|
||||
|
||||
export async function removeAssignment(assignmentId) {
|
||||
if (!game.user.isGM) return;
|
||||
|
||||
const assignments = foundry.utils.deepClone(game.settings.get(MODULE_ID, SETTINGS.ASSIGNMENTS) ?? []);
|
||||
const assignment = assignments.find(a => a.id === assignmentId);
|
||||
if (!assignment) return;
|
||||
|
||||
await game.settings.set(MODULE_ID, SETTINGS.ASSIGNMENTS, assignments.filter(a => a.id !== assignmentId));
|
||||
|
||||
if (assignment.mode === "actor" && assignment.actorUuid) {
|
||||
const actor = await fromUuid(assignment.actorUuid);
|
||||
if (actor) await removeReactionFromDocument(actor, assignment.reactionId, true);
|
||||
}
|
||||
|
||||
if (assignment.mode === "token" && assignment.tokenUuid) {
|
||||
const tokenDocument = await fromUuid(assignment.tokenUuid);
|
||||
if (tokenDocument) await removeReactionFromDocument(tokenDocument, assignment.reactionId, false);
|
||||
}
|
||||
}
|
||||
|
||||
function buildAssignmentForToken(tokenDocument, actor, reactionId) {
|
||||
if (tokenDocument.actorLink === true) {
|
||||
return {
|
||||
id: foundry.utils.randomID(),
|
||||
reactionId,
|
||||
mode: "actor",
|
||||
actorUuid: actor.uuid,
|
||||
tokenUuid: null,
|
||||
sceneUuid: null,
|
||||
name: actor.name
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: foundry.utils.randomID(),
|
||||
reactionId,
|
||||
mode: "token",
|
||||
actorUuid: null,
|
||||
tokenUuid: tokenDocument.uuid,
|
||||
sceneUuid: tokenDocument.parent?.uuid ?? canvas.scene?.uuid ?? null,
|
||||
name: tokenDocument.name
|
||||
};
|
||||
}
|
||||
|
||||
async function addReactionFlag(document, reactionId) {
|
||||
const assigned = foundry.utils.deepClone(document.getFlag(MODULE_ID, "assignedReactions") ?? []);
|
||||
if (!assigned.some(entry => entry.reactionId === reactionId)) {
|
||||
assigned.push({ reactionId, assignedAt: Date.now(), assignedBy: game.user.id });
|
||||
}
|
||||
await document.setFlag(MODULE_ID, "assignedReactions", assigned);
|
||||
}
|
||||
|
||||
async function ensureManagedReactionEffectOnActor(actor, reaction) {
|
||||
if (!game.settings.get(MODULE_ID, SETTINGS.CREATE_MANAGED_EFFECTS_ON_ASSIGNMENT)) return;
|
||||
|
||||
const existing = actor.effects.find(effect =>
|
||||
effect.getFlag(MODULE_ID, "reactionId") === reaction.id &&
|
||||
effect.getFlag(MODULE_ID, "managed") === true
|
||||
);
|
||||
|
||||
if (existing) return;
|
||||
|
||||
await actor.createEmbeddedDocuments("ActiveEffect", [{
|
||||
name: game.i18n.format("CONFIGURABLE_REACTIONS.Effects.ManagedReaction", { name: reaction.name }),
|
||||
icon: reaction.icon ?? "icons/svg/aura.svg",
|
||||
origin: `Module.${MODULE_ID}`,
|
||||
disabled: false,
|
||||
transfer: false,
|
||||
changes: [],
|
||||
flags: {
|
||||
[MODULE_ID]: {
|
||||
reactionId: reaction.id,
|
||||
managed: true
|
||||
}
|
||||
}
|
||||
}]);
|
||||
}
|
||||
|
||||
async function removeReactionFromDocument(document, reactionId, removeManagedEffects) {
|
||||
const assigned = foundry.utils.deepClone(document.getFlag(MODULE_ID, "assignedReactions") ?? [])
|
||||
.filter(entry => entry.reactionId !== reactionId);
|
||||
|
||||
if (assigned.length) await document.setFlag(MODULE_ID, "assignedReactions", assigned);
|
||||
else await document.unsetFlag(MODULE_ID, "assignedReactions");
|
||||
|
||||
if (!removeManagedEffects || !document.effects) return;
|
||||
|
||||
const effectIds = document.effects
|
||||
.filter(effect => effect.getFlag(MODULE_ID, "reactionId") === reactionId && effect.getFlag(MODULE_ID, "managed") === true)
|
||||
.map(effect => effect.id);
|
||||
|
||||
if (effectIds.length) await document.deleteEmbeddedDocuments("ActiveEffect", effectIds);
|
||||
}
|
||||
177
scripts/apps/reaction-config-app.js
Normal file
177
scripts/apps/reaction-config-app.js
Normal file
@ -0,0 +1,177 @@
|
||||
import { MODULE_ID, SETTINGS, DEFAULT_REACTION, ACTION_TYPES, TRIGGER_TYPES } from "../constants.js";
|
||||
import { assignReactionToSelectedTokens, removeAssignment } from "./assignment-manager.js";
|
||||
|
||||
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
|
||||
|
||||
export class ConfigurableReactionsConfigApp extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||
static DEFAULT_OPTIONS = {
|
||||
id: "configurable-reactions-config",
|
||||
tag: "section",
|
||||
classes: ["configurable-reactions", "standard-form"],
|
||||
window: {
|
||||
title: "CONFIGURABLE_REACTIONS.App.Title",
|
||||
icon: "fa-solid fa-bolt"
|
||||
},
|
||||
position: {
|
||||
width: 900,
|
||||
height: 760
|
||||
},
|
||||
actions: {
|
||||
createReaction: ConfigurableReactionsConfigApp.#onCreateReaction,
|
||||
saveReactionJson: ConfigurableReactionsConfigApp.#onSaveReactionJson,
|
||||
deleteReaction: ConfigurableReactionsConfigApp.#onDeleteReaction,
|
||||
assignSelectedTokens: ConfigurableReactionsConfigApp.#onAssignSelectedTokens,
|
||||
removeAssignment: ConfigurableReactionsConfigApp.#onRemoveAssignment,
|
||||
addTeleportAction: ConfigurableReactionsConfigApp.#onAddTeleportAction,
|
||||
addStatusAction: ConfigurableReactionsConfigApp.#onAddStatusAction
|
||||
}
|
||||
};
|
||||
|
||||
static PARTS = {
|
||||
main: {
|
||||
template: `modules/${MODULE_ID}/templates/reaction-config.hbs`
|
||||
}
|
||||
};
|
||||
|
||||
async _prepareContext(options) {
|
||||
const reactions = foundry.utils.deepClone(game.settings.get(MODULE_ID, SETTINGS.REACTIONS) ?? []);
|
||||
const assignments = foundry.utils.deepClone(game.settings.get(MODULE_ID, SETTINGS.ASSIGNMENTS) ?? []);
|
||||
|
||||
const selectedReactionId = this._selectedReactionId ?? reactions[0]?.id ?? null;
|
||||
const selectedReaction = reactions.find(r => r.id === selectedReactionId) ?? reactions[0] ?? null;
|
||||
this._selectedReactionId = selectedReaction?.id ?? null;
|
||||
|
||||
return {
|
||||
reactions,
|
||||
assignments: assignments.map(a => ({
|
||||
...a,
|
||||
reactionName: reactions.find(r => r.id === a.reactionId)?.name ?? a.reactionId
|
||||
})),
|
||||
selectedReaction,
|
||||
selectedReactionJson: selectedReaction ? JSON.stringify(selectedReaction, null, 2) : "",
|
||||
triggerTypes: TRIGGER_TYPES,
|
||||
actionTypes: ACTION_TYPES,
|
||||
statusEffects: CONFIG.statusEffects.map(s => ({ id: s.id, name: game.i18n.localize(s.name ?? s.id) }))
|
||||
};
|
||||
}
|
||||
|
||||
_onRender(context, options) {
|
||||
super._onRender(context, options);
|
||||
|
||||
this.element.querySelector("[name='selectedReactionId']")?.addEventListener("change", event => {
|
||||
this._selectedReactionId = event.currentTarget.value;
|
||||
this.render({ force: true });
|
||||
});
|
||||
}
|
||||
|
||||
static async #onCreateReaction(event, target) {
|
||||
const reactions = foundry.utils.deepClone(game.settings.get(MODULE_ID, SETTINGS.REACTIONS) ?? []);
|
||||
const reaction = foundry.utils.deepClone(DEFAULT_REACTION);
|
||||
reaction.id = foundry.utils.randomID();
|
||||
reaction.name = game.i18n.localize("CONFIGURABLE_REACTIONS.Reactions.NewReaction");
|
||||
|
||||
reactions.push(reaction);
|
||||
await game.settings.set(MODULE_ID, SETTINGS.REACTIONS, reactions);
|
||||
|
||||
this._selectedReactionId = reaction.id;
|
||||
this.render({ force: true });
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
static async #onDeleteReaction(event, target) {
|
||||
const reactionId = this.element.querySelector("[name='selectedReactionId']")?.value;
|
||||
if (!reactionId) return;
|
||||
|
||||
const confirmed = await foundry.applications.api.DialogV2.confirm({
|
||||
window: { title: game.i18n.localize("CONFIGURABLE_REACTIONS.Reactions.Delete") },
|
||||
content: `<p>${game.i18n.localize("CONFIGURABLE_REACTIONS.Reactions.DeleteConfirm")}</p>`
|
||||
});
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
const reactions = foundry.utils.deepClone(game.settings.get(MODULE_ID, SETTINGS.REACTIONS) ?? [])
|
||||
.filter(r => r.id !== reactionId);
|
||||
const assignments = foundry.utils.deepClone(game.settings.get(MODULE_ID, SETTINGS.ASSIGNMENTS) ?? [])
|
||||
.filter(a => a.reactionId !== reactionId);
|
||||
|
||||
await game.settings.set(MODULE_ID, SETTINGS.REACTIONS, reactions);
|
||||
await game.settings.set(MODULE_ID, SETTINGS.ASSIGNMENTS, assignments);
|
||||
|
||||
this._selectedReactionId = reactions[0]?.id ?? null;
|
||||
this.render({ force: true });
|
||||
}
|
||||
|
||||
static async #onAssignSelectedTokens(event, target) {
|
||||
const reactionId = this.element.querySelector("[name='selectedReactionId']")?.value;
|
||||
await assignReactionToSelectedTokens(reactionId);
|
||||
this.render({ force: true });
|
||||
}
|
||||
|
||||
static async #onRemoveAssignment(event, target) {
|
||||
const assignmentId = target.dataset.assignmentId;
|
||||
await removeAssignment(assignmentId);
|
||||
this.render({ force: true });
|
||||
}
|
||||
|
||||
static async #onAddTeleportAction(event, target) {
|
||||
await this.#addAction({
|
||||
id: foundry.utils.randomID(),
|
||||
type: ACTION_TYPES.TELEPORT,
|
||||
enabled: true,
|
||||
radius: 30,
|
||||
askOwner: false,
|
||||
snapToGrid: true,
|
||||
maxAttempts: 80,
|
||||
consumeOnFailure: false
|
||||
});
|
||||
}
|
||||
|
||||
static async #onAddStatusAction(event, target) {
|
||||
await this.#addAction({
|
||||
id: foundry.utils.randomID(),
|
||||
type: ACTION_TYPES.APPLY_STATUS,
|
||||
enabled: true,
|
||||
statuses: [],
|
||||
duration: {
|
||||
type: "rounds",
|
||||
value: 1
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async #addAction(action) {
|
||||
const reactions = foundry.utils.deepClone(game.settings.get(MODULE_ID, SETTINGS.REACTIONS) ?? []);
|
||||
const reaction = reactions.find(r => r.id === this._selectedReactionId);
|
||||
if (!reaction) return;
|
||||
|
||||
reaction.actions ??= [];
|
||||
reaction.actions.push(action);
|
||||
|
||||
await game.settings.set(MODULE_ID, SETTINGS.REACTIONS, reactions);
|
||||
this.render({ force: true });
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user