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 `

${foundry.utils.escapeHTML(state.opName || "SLA Operation")}

Stage: ${foundry.utils.escapeHTML(state.stage || "Unstated")}

Clock: ${state.clock}/8 | Heat: ${state.heat} | Collateral: ${state.collateral} | Pressure: ${state.pressure}

Escalation Trigger: ${foundry.utils.escapeHTML(state.trigger || "None")}

${state.notes ? `

GM Notes: ${foundry.utils.escapeHTML(state.notes)}

` : ""}
`; } 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) { if (!game.user?.isGM) return; 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 = ` 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);