168 lines
5.9 KiB
JavaScript
168 lines
5.9 KiB
JavaScript
const MODULE_ID = "sla-npc-director";
|
|
const FLAG_KEY = "directorState";
|
|
const STANCE_OPTIONS = ["Aggressive", "Holding", "Defensive", "Broken"];
|
|
const MORALE_OPTIONS = ["Steady", "Shaken", "Fragile", "Collapsed"];
|
|
const SUPPRESSION_OPTIONS = ["0", "1", "2", "3"];
|
|
const INTENT_OPTIONS = ["Rush", "Flank", "Hold", "Suppress", "Fallback", "Flee"];
|
|
|
|
function getHtmlRoot(html) {
|
|
return html instanceof HTMLElement ? html : html?.[0] ?? null;
|
|
}
|
|
|
|
function getTargets() {
|
|
const selectedTokens = canvas?.tokens?.controlled?.map((token) => token.document) ?? [];
|
|
if (selectedTokens.length) return selectedTokens;
|
|
|
|
const combatants = game.combat?.combatants?.map((combatant) => combatant.token ?? combatant.actor)?.filter(Boolean) ?? [];
|
|
return combatants;
|
|
}
|
|
|
|
function readState(document) {
|
|
return foundry.utils.mergeObject(
|
|
{
|
|
stance: "Holding",
|
|
morale: "Steady",
|
|
suppression: "0",
|
|
intent: "Hold",
|
|
note: ""
|
|
},
|
|
document?.getFlag?.(MODULE_ID, FLAG_KEY) ?? {}
|
|
);
|
|
}
|
|
|
|
async function writeState(document, state) {
|
|
if (!document?.setFlag) return;
|
|
await document.setFlag(MODULE_ID, FLAG_KEY, state);
|
|
}
|
|
|
|
function buildSummary(entries) {
|
|
const lines = entries.map((entry) => {
|
|
const note = entry.note ? ` | ${foundry.utils.escapeHTML(entry.note)}` : "";
|
|
return `<li><strong>${foundry.utils.escapeHTML(entry.name)}</strong>: ${entry.stance}, ${entry.morale}, suppression ${entry.suppression}, intent ${entry.intent}${note}</li>`;
|
|
}).join("");
|
|
return `<section class="sla-npc-director-summary"><h2>SLA NPC Director</h2><ul>${lines}</ul></section>`;
|
|
}
|
|
|
|
class SlaNpcDirectorApp extends FormApplication {
|
|
static get defaultOptions() {
|
|
return foundry.utils.mergeObject(super.defaultOptions, {
|
|
id: "sla-npc-director-app",
|
|
title: "SLA NPC Director",
|
|
template: "modules/sla-npc-director/templates/npc-director.hbs",
|
|
classes: ["sla-npc-director"],
|
|
width: 980,
|
|
height: "auto",
|
|
closeOnSubmit: false,
|
|
submitOnChange: false,
|
|
resizable: true
|
|
});
|
|
}
|
|
|
|
async getData() {
|
|
const entries = getTargets().map((document) => ({
|
|
name: document.name ?? document.actor?.name ?? "Unknown",
|
|
docId: document.id,
|
|
docType: document.documentName,
|
|
...readState(document)
|
|
}));
|
|
|
|
return {
|
|
entries,
|
|
stanceOptions: STANCE_OPTIONS,
|
|
moraleOptions: MORALE_OPTIONS,
|
|
suppressionOptions: SUPPRESSION_OPTIONS,
|
|
intentOptions: INTENT_OPTIONS
|
|
};
|
|
}
|
|
|
|
activateListeners(html) {
|
|
super.activateListeners(html);
|
|
html.find("[data-action='refresh']").on("click", () => this.render(false));
|
|
html.find("[data-action='save']").on("click", async () => {
|
|
await this._saveRows();
|
|
ui.notifications.info("SLA NPC Director states saved.");
|
|
});
|
|
html.find("[data-action='chat']").on("click", async () => {
|
|
const entries = await this._saveRows();
|
|
await ChatMessage.create({
|
|
user: game.user.id,
|
|
speaker: { alias: "SLA NPC Director" },
|
|
whisper: ChatMessage.getWhisperRecipients("GM"),
|
|
content: buildSummary(entries)
|
|
});
|
|
});
|
|
}
|
|
|
|
async _saveRows() {
|
|
const rows = Array.from(this.form?.querySelectorAll(".sla-npc-director-row") ?? []);
|
|
const summaries = [];
|
|
for (const row of rows) {
|
|
const docId = row.dataset.docId;
|
|
const document = canvas?.scene?.tokens?.get?.(docId) ?? game.combat?.combatants?.find((combatant) => combatant.token?.id === docId)?.token;
|
|
if (!document) continue;
|
|
const state = {
|
|
stance: row.querySelector("[data-field='stance']")?.value ?? "Holding",
|
|
morale: row.querySelector("[data-field='morale']")?.value ?? "Steady",
|
|
suppression: row.querySelector("[data-field='suppression']")?.value ?? "0",
|
|
intent: row.querySelector("[data-field='intent']")?.value ?? "Hold",
|
|
note: row.querySelector("[data-field='note']")?.value ?? ""
|
|
};
|
|
await writeState(document, state);
|
|
summaries.push({ name: document.name ?? document.actor?.name ?? "Unknown", ...state });
|
|
}
|
|
return summaries;
|
|
}
|
|
|
|
async _updateObject() {
|
|
await this._saveRows();
|
|
}
|
|
|
|
static open() {
|
|
return new SlaNpcDirectorApp().render(true);
|
|
}
|
|
}
|
|
|
|
function appendOpenButton(root, selector, className, label) {
|
|
if (!game.user?.isGM) return false;
|
|
const header = root?.querySelector(selector);
|
|
if (!header || header.querySelector(`.${className}`)) return false;
|
|
const button = document.createElement("button");
|
|
button.type = "button";
|
|
button.className = className;
|
|
button.innerHTML = `<i class="fas fa-user-shield"></i> ${label}`;
|
|
button.addEventListener("click", () => SlaNpcDirectorApp.open());
|
|
header.appendChild(button);
|
|
return true;
|
|
}
|
|
|
|
function installCombatButton(app, html) {
|
|
const root = getHtmlRoot(html);
|
|
appendOpenButton(root, ".directory-header .header-actions, .directory-header .action-buttons, .combat-sidebar .encounter-controls", "sla-npc-director-open", "NPC Director");
|
|
}
|
|
|
|
function installActorDirectoryButton(app, html) {
|
|
const root = getHtmlRoot(html);
|
|
appendOpenButton(root, ".directory-header .header-actions, .directory-header .action-buttons", "sla-npc-director-actor-open", "NPC Director");
|
|
}
|
|
|
|
function installSettingsButton(app, html) {
|
|
const root = getHtmlRoot(html);
|
|
appendOpenButton(root, ".settings-sidebar #game-details, .settings-sidebar .directory-header .header-actions, .settings-sidebar .action-buttons", "sla-npc-director-settings-open", "NPC Director");
|
|
}
|
|
|
|
Hooks.once("init", () => {
|
|
Handlebars.registerHelper("eq", (left, right) => left === right);
|
|
game.settings.registerMenu(MODULE_ID, "npcDirectorMenu", {
|
|
name: "SLA NPC Director",
|
|
label: "Open NPC Director",
|
|
hint: "Track combat stance, morale, suppression, and intent for current targets.",
|
|
icon: "fas fa-user-shield",
|
|
type: SlaNpcDirectorApp,
|
|
restricted: true
|
|
});
|
|
});
|
|
|
|
Hooks.on("renderCombatTracker", installCombatButton);
|
|
Hooks.on("renderActorDirectory", installActorDirectoryButton);
|
|
Hooks.on("renderSettings", installSettingsButton);
|