chore: initial deploy — SLA Industries Foundry VTT
Co-authored-by: slaguru666 <timevans666@gmail.com>
This commit is contained in:
+15
@@ -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/
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"SLAOpsClock.Title": "SLA Ops Clock"
|
||||||
|
}
|
||||||
+32
@@ -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"
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user