chore: initial deploy — SLA Industries Foundry VTT

Co-authored-by: slaguru666 <timevans666@gmail.com>
This commit is contained in:
2026-03-31 17:00:39 +01:00
commit 5134f245b7
6 changed files with 362 additions and 0 deletions
+15
View File
@@ -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/
+3
View File
@@ -0,0 +1,3 @@
{
"SLAOpsClock.Title": "SLA Ops Clock"
}
+32
View File
@@ -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"
}
+138
View File
@@ -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 `
<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);
+115
View File
@@ -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;
}
}
+59
View File
@@ -0,0 +1,59 @@
<form class="sla-ops-clock-form">
<div class="sla-ops-clock-grid">
<div class="sla-ops-clock-field">
<label>Operation Name</label>
<input type="text" name="opName" value="{{state.opName}}">
</div>
<div class="sla-ops-clock-field">
<label>Current Stage</label>
<input type="text" name="stage" value="{{state.stage}}">
</div>
</div>
<div class="sla-ops-clock-panel">
<h3>Mission Clock</h3>
<div class="sla-ops-clock-track">
{{#each segments}}
<div class="sla-ops-clock-segment {{#if this.filled}}is-filled{{/if}}"></div>
{{/each}}
</div>
</div>
<div class="sla-ops-clock-meter-row">
<div class="sla-ops-clock-field sla-ops-clock-meter">
<label>Clock Filled</label>
<input type="number" name="clock" min="0" max="8" value="{{state.clock}}">
</div>
<div class="sla-ops-clock-field sla-ops-clock-meter">
<label>Heat</label>
<input type="number" name="heat" min="0" max="9" value="{{state.heat}}">
</div>
<div class="sla-ops-clock-field sla-ops-clock-meter">
<label>Collateral</label>
<input type="number" name="collateral" min="0" max="9" value="{{state.collateral}}">
</div>
</div>
<div class="sla-ops-clock-grid">
<div class="sla-ops-clock-field">
<label>Pressure</label>
<input type="number" name="pressure" min="0" max="9" value="{{state.pressure}}">
</div>
<div class="sla-ops-clock-field">
<label>Escalation Trigger</label>
<input type="text" name="trigger" value="{{state.trigger}}">
</div>
</div>
<div class="sla-ops-clock-panel">
<h3>GM Notes</h3>
<textarea name="notes">{{state.notes}}</textarea>
</div>
<div class="sla-ops-clock-actions">
<button type="button" data-action="advance">Advance Clock</button>
<button type="button" data-action="post">Post Update To Chat</button>
<button type="button" data-action="reset">Reset</button>
<button type="submit">Save</button>
</div>
</form>