Files
sla-ops-clock/module/sla-ops-clock.mjs
T
2026-03-31 17:00:39 +01:00

139 lines
4.2 KiB
JavaScript

const MODULE_ID = "sla-ops-clock";
const SETTING_KEY = "opsState";
function defaultState() {
return {
opName: "New SLA Operation",
stage: "Insertion",
clock: 0,
heat: 0,
collateral: 0,
pressure: 0,
trigger: "Media attention spikes",
notes: ""
};
}
function getState() {
return foundry.utils.mergeObject(defaultState(), game.settings.get(MODULE_ID, SETTING_KEY) ?? {});
}
async function setState(nextState) {
await game.settings.set(MODULE_ID, SETTING_KEY, foundry.utils.mergeObject(defaultState(), nextState ?? {}));
}
function renderChatSummary(state) {
return `
<section class="sla-ops-clock-chat">
<h2>${foundry.utils.escapeHTML(state.opName || "SLA Operation")}</h2>
<p><strong>Stage:</strong> ${foundry.utils.escapeHTML(state.stage || "Unstated")}</p>
<p><strong>Clock:</strong> ${state.clock}/8 | <strong>Heat:</strong> ${state.heat} | <strong>Collateral:</strong> ${state.collateral} | <strong>Pressure:</strong> ${state.pressure}</p>
<p><strong>Escalation Trigger:</strong> ${foundry.utils.escapeHTML(state.trigger || "None")}</p>
${state.notes ? `<p><strong>GM Notes:</strong> ${foundry.utils.escapeHTML(state.notes)}</p>` : ""}
</section>
`;
}
function getHtmlRoot(html) {
return html instanceof HTMLElement ? html : html?.[0] ?? null;
}
class SlaOpsClockApp extends FormApplication {
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
id: "sla-ops-clock-app",
title: "SLA Ops Clock",
template: "modules/sla-ops-clock/templates/ops-clock.hbs",
classes: ["sla-ops-clock"],
width: 560,
height: "auto",
submitOnChange: false,
closeOnSubmit: false,
resizable: true
});
}
async getData() {
const state = getState();
return {
state,
segments: Array.from({ length: 8 }, (_value, index) => ({
index,
filled: index < Number(state.clock || 0)
}))
};
}
activateListeners(html) {
super.activateListeners(html);
html.find("[data-action='advance']").on("click", async () => {
const current = await this._collectState();
current.clock = Math.min(8, Number(current.clock || 0) + 1);
current.pressure = Math.min(9, Number(current.pressure || 0) + 1);
await setState(current);
this.render(false);
});
html.find("[data-action='post']").on("click", async () => {
const current = await this._collectState();
await setState(current);
await ChatMessage.create({
user: game.user.id,
speaker: { alias: "SLA Ops Clock" },
content: renderChatSummary(current)
});
});
html.find("[data-action='reset']").on("click", async () => {
await setState(defaultState());
this.render(false);
});
}
async _collectState() {
const form = this.form;
if (!form) return getState();
const fd = new foundry.applications.ux.FormDataExtended(form);
return foundry.utils.mergeObject(getState(), fd.object);
}
async _updateObject(_event, formData) {
await setState(formData);
ui.notifications.info("SLA Ops Clock saved.");
}
static open() {
return new SlaOpsClockApp().render(true);
}
}
function installSceneButton(app, html) {
const root = getHtmlRoot(html);
const header = root?.querySelector(".directory-header .header-actions, .directory-header .action-buttons");
if (!header || header.querySelector(".sla-ops-clock-open")) return;
const button = document.createElement("button");
button.type = "button";
button.className = "sla-ops-clock-open";
button.innerHTML = `<i class="fas fa-gauge-high"></i> Ops Clock`;
button.addEventListener("click", () => SlaOpsClockApp.open());
header.appendChild(button);
}
Hooks.once("init", () => {
game.settings.register(MODULE_ID, SETTING_KEY, {
scope: "world",
config: false,
type: Object,
default: defaultState()
});
game.settings.registerMenu(MODULE_ID, "opsClockMenu", {
name: "SLA Ops Clock",
label: "Open Ops Clock",
hint: "Track mission heat, collateral, and pressure for the current operation.",
icon: "fas fa-gauge-high",
type: SlaOpsClockApp,
restricted: true
});
});
Hooks.on("renderSceneDirectory", installSceneButton);