From 5134f245b707171028796ab545a5e87882fa7d41 Mon Sep 17 00:00:00 2001 From: "Tim Evans (slaguru666)" Date: Tue, 31 Mar 2026 17:00:39 +0100 Subject: [PATCH] =?UTF-8?q?chore:=20initial=20deploy=20=E2=80=94=20SLA=20I?= =?UTF-8?q?ndustries=20Foundry=20VTT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: slaguru666 --- .gitignore | 15 +++++ lang/en.json | 3 + module.json | 32 +++++++++ module/sla-ops-clock.mjs | 138 +++++++++++++++++++++++++++++++++++++++ styles/sla-ops-clock.css | 115 ++++++++++++++++++++++++++++++++ templates/ops-clock.hbs | 59 +++++++++++++++++ 6 files changed, 362 insertions(+) create mode 100644 .gitignore create mode 100644 lang/en.json create mode 100644 module.json create mode 100644 module/sla-ops-clock.mjs create mode 100644 styles/sla-ops-clock.css create mode 100644 templates/ops-clock.hbs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b24d620 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +node_modules/ +npm-debug.log* +.npm/ +dist/ +*.min.js +*.min.css +.DS_Store +.DS_Store? +._* +Thumbs.db +.vscode/ +.idea/ +*.swp +*.swo +worlds/ diff --git a/lang/en.json b/lang/en.json new file mode 100644 index 0000000..665c5c2 --- /dev/null +++ b/lang/en.json @@ -0,0 +1,3 @@ +{ + "SLAOpsClock.Title": "SLA Ops Clock" +} diff --git a/module.json b/module.json new file mode 100644 index 0000000..86ec64d --- /dev/null +++ b/module.json @@ -0,0 +1,32 @@ +{ + "id": "sla-ops-clock", + "title": "SLA Ops Clock", + "description": "Tracks SLA mission tempo with operation clock, heat, collateral, and pressure controls plus player-facing updates.", + "version": "0.1.0", + "compatibility": { + "minimum": "13", + "verified": "13.350", + "maximum": "13" + }, + "authors": [ + { + "name": "Tim Evans" + } + ], + "esmodules": [ + "module/sla-ops-clock.mjs" + ], + "styles": [ + "styles/sla-ops-clock.css" + ], + "languages": [ + { + "lang": "en", + "name": "English", + "path": "lang/en.json" + } + ], + "manifest": "https://raw.githubusercontent.com/slaguru666/sla-ops-clock/main/module.json", + "download": "https://github.com/slaguru666/sla-ops-clock/releases/latest/download/sla-ops-clock.zip", + "url": "https://github.com/slaguru666/sla-ops-clock" +} \ No newline at end of file diff --git a/module/sla-ops-clock.mjs b/module/sla-ops-clock.mjs new file mode 100644 index 0000000..d89b15a --- /dev/null +++ b/module/sla-ops-clock.mjs @@ -0,0 +1,138 @@ +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) { + 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); diff --git a/styles/sla-ops-clock.css b/styles/sla-ops-clock.css new file mode 100644 index 0000000..76d855e --- /dev/null +++ b/styles/sla-ops-clock.css @@ -0,0 +1,115 @@ +.sla-ops-clock .window-content { + background: linear-gradient(180deg, #11161d 0%, #171d25 100%); + color: #f2efe7 !important; +} + +.sla-ops-clock-form { + display: grid; + gap: 12px; +} + +.sla-ops-clock-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px 14px; +} + +.sla-ops-clock-field, +.sla-ops-clock-panel { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(215, 181, 82, 0.28); + border-radius: 12px; + padding: 10px 12px; +} + +.sla-ops-clock-field label, +.sla-ops-clock-panel h3 { + display: block; + color: #f5efe3 !important; + font-weight: 700; + letter-spacing: 0.05em; + margin-bottom: 6px; + text-transform: uppercase; +} + +.sla-ops-clock-field input, +.sla-ops-clock-field select, +.sla-ops-clock-field textarea { + width: 100%; + background: rgba(0, 0, 0, 0.35); + border: 1px solid rgba(255, 255, 255, 0.14); + border-radius: 10px; + color: #f2efe7 !important; + padding: 8px 10px; +} + +.sla-ops-clock .window-title, +.sla-ops-clock p, +.sla-ops-clock span, +.sla-ops-clock strong, +.sla-ops-clock input, +.sla-ops-clock select, +.sla-ops-clock textarea, +.sla-ops-clock option { + color: #f2efe7 !important; +} + +.sla-ops-clock input::placeholder, +.sla-ops-clock textarea::placeholder { + color: rgba(242, 239, 231, 0.55) !important; +} + +.sla-ops-clock-panel textarea { + min-height: 86px; +} + +.sla-ops-clock-track { + display: grid; + grid-template-columns: repeat(8, 1fr); + gap: 6px; +} + +.sla-ops-clock-segment { + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.15); + background: rgba(255, 255, 255, 0.08); + height: 18px; +} + +.sla-ops-clock-segment.is-filled { + background: linear-gradient(90deg, #d7b552 0%, #f1d27a 100%); + border-color: rgba(255, 242, 199, 0.65); +} + +.sla-ops-clock-meter-row { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; +} + +.sla-ops-clock-meter strong { + display: block; + font-size: 1.6rem; +} + +.sla-ops-clock-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.sla-ops-clock-actions button { + background: linear-gradient(180deg, #d6b24e 0%, #b88a24 100%) !important; + border: 1px solid rgba(255, 232, 180, 0.55) !important; + border-radius: 999px; + color: #17140c !important; + padding: 7px 14px; + font-weight: 700; +} + +@media (max-width: 720px) { + .sla-ops-clock-grid, + .sla-ops-clock-meter-row { + grid-template-columns: 1fr; + } +} diff --git a/templates/ops-clock.hbs b/templates/ops-clock.hbs new file mode 100644 index 0000000..0189d2e --- /dev/null +++ b/templates/ops-clock.hbs @@ -0,0 +1,59 @@ +
+
+
+ + +
+
+ + +
+
+ +
+

Mission Clock

+
+ {{#each segments}} +
+ {{/each}} +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+

GM Notes

+ +
+ +
+ + + + +
+