chore: initial deploy — SLA Industries Foundry VTT

Co-authored-by: slaguru666 <timevans666@gmail.com>
This commit is contained in:
2026-03-31 17:00:36 +01:00
commit 2305cdf438
7 changed files with 366 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/
+9
View File
@@ -0,0 +1,9 @@
{
"missionTypes": ["Retrieval", "Termination", "Escort", "Containment", "Public Order", "Counter-Ebb Sweep", "Evidence Recovery", "Black Bag"],
"sectors": ["Mort", "Cannibal Sector", "Downtown", "Industrial Spine", "The Docks", "Lower Downtown", "Sub-City Arcology", "Transit Hub"],
"targets": ["renegade operative", "corp data courier", "infected slop gang", "missing asset", "cult logistics cell", "rogue media crew", "biotech sample", "witness under protection"],
"contacts": ["Mr. Slayer liaison", "Department-M minder", "sector overseer", "anonymous tip line", "embedded media fixer", "field scientist", "security captain", "ops auditor"],
"complications": ["the target is already moving", "media drones are on scene", "Cannibal activity spikes nearby", "an internal rival team is on the same brief", "local law has sealed the zone", "the client withheld one critical fact", "the package is alive", "there is a clock on public exposure"],
"payoffs": ["8,000 credits plus expenses", "10,000 credits and media bonuses", "6,500 credits with expedited status review", "12,000 credits on silent completion", "9,000 credits and requisition priority", "7,500 credits plus salvage rights"],
"twists": ["the employer wants witnesses removed", "the job is bait for another target", "the package is compromised by Ebb bleed", "the evidence points back to SLA payroll", "the target is an innocent lookalike", "the extraction route is burned", "the media angle is the true objective", "an enemy operative is already wearing a company badge"]
}
+3
View File
@@ -0,0 +1,3 @@
{
"SLABriefingDesk.Title": "SLA Briefing Desk"
}
+32
View File
@@ -0,0 +1,32 @@
{
"id": "sla-briefing-desk",
"title": "SLA Briefing Desk",
"description": "Creates SLA Industries mission briefings, objectives, complications, and handouts for the table.",
"version": "0.1.0",
"compatibility": {
"minimum": "13",
"verified": "13.350",
"maximum": "13"
},
"authors": [
{
"name": "Tim Evans"
}
],
"esmodules": [
"module/sla-briefing-desk.mjs"
],
"styles": [
"styles/sla-briefing-desk.css"
],
"languages": [
{
"lang": "en",
"name": "English",
"path": "lang/en.json"
}
],
"manifest": "https://raw.githubusercontent.com/slaguru666/sla-briefing-desk/main/module.json",
"download": "https://github.com/slaguru666/sla-briefing-desk/releases/latest/download/sla-briefing-desk.zip",
"url": "https://github.com/slaguru666/sla-briefing-desk"
}
+173
View File
@@ -0,0 +1,173 @@
const MODULE_ID = "sla-briefing-desk";
const DRAFT_KEY = "briefingDraft";
let TABLES = null;
function getHtmlRoot(html) {
return html instanceof HTMLElement ? html : html?.[0] ?? null;
}
async function loadTables() {
if (TABLES) return TABLES;
const response = await fetch("modules/sla-briefing-desk/data/briefing-tables.json");
TABLES = await response.json();
return TABLES;
}
function pick(array) {
return Array.isArray(array) && array.length ? array[Math.floor(Math.random() * array.length)] : "";
}
function defaultBriefing() {
return {
jobName: "SLA Brief",
missionType: "",
sector: "",
target: "",
contact: "",
payoff: "",
complication: "",
twist: "",
notes: ""
};
}
function getDraft() {
return foundry.utils.mergeObject(defaultBriefing(), game.settings.get(MODULE_ID, DRAFT_KEY) ?? {});
}
async function setDraft(draft) {
await game.settings.set(MODULE_ID, DRAFT_KEY, foundry.utils.mergeObject(defaultBriefing(), draft ?? {}));
}
function renderBriefingHtml(briefing) {
return `
<section class="sla-briefing-output">
<h2>${foundry.utils.escapeHTML(briefing.jobName || "SLA Brief")}</h2>
<p><strong>Mission Type:</strong> ${foundry.utils.escapeHTML(briefing.missionType)}</p>
<p><strong>Sector:</strong> ${foundry.utils.escapeHTML(briefing.sector)}</p>
<p><strong>Target:</strong> ${foundry.utils.escapeHTML(briefing.target)}</p>
<p><strong>Contact:</strong> ${foundry.utils.escapeHTML(briefing.contact)}</p>
<p><strong>Payoff:</strong> ${foundry.utils.escapeHTML(briefing.payoff)}</p>
<p><strong>Complication:</strong> ${foundry.utils.escapeHTML(briefing.complication)}</p>
<p><strong>Twist:</strong> ${foundry.utils.escapeHTML(briefing.twist)}</p>
${briefing.notes ? `<p><strong>Operational Notes:</strong> ${foundry.utils.escapeHTML(briefing.notes)}</p>` : ""}
</section>
`;
}
async function randomBriefing() {
const tables = await loadTables();
const missionType = pick(tables.missionTypes);
const sector = pick(tables.sectors);
const target = pick(tables.targets);
return {
jobName: `${missionType} - ${sector}`,
missionType,
sector,
target,
contact: pick(tables.contacts),
payoff: pick(tables.payoffs),
complication: pick(tables.complications),
twist: pick(tables.twists),
notes: ""
};
}
class SlaBriefingDeskApp extends FormApplication {
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
id: "sla-briefing-desk-app",
title: "SLA Briefing Desk",
template: "modules/sla-briefing-desk/templates/briefing-desk.hbs",
classes: ["sla-briefing-desk"],
width: 640,
height: "auto",
closeOnSubmit: false,
submitOnChange: false,
resizable: true
});
}
async getData() {
return { briefing: getDraft() };
}
activateListeners(html) {
super.activateListeners(html);
html.find("[data-action='randomize']").on("click", async () => {
await setDraft(await randomBriefing());
this.render(false);
});
html.find("[data-action='chat']").on("click", async () => {
const draft = await this._collectDraft();
await setDraft(draft);
await ChatMessage.create({
user: game.user.id,
speaker: { alias: "SLA Briefing Desk" },
content: renderBriefingHtml(draft)
});
});
html.find("[data-action='journal']").on("click", async () => {
const draft = await this._collectDraft();
await setDraft(draft);
const journal = await JournalEntry.create({
name: draft.jobName || "SLA Brief",
pages: [
{
name: "Briefing",
type: "text",
text: { content: renderBriefingHtml(draft) }
}
]
});
ui.notifications.info(`Created briefing journal: ${journal.name}`);
});
}
async _collectDraft() {
const fd = new foundry.applications.ux.FormDataExtended(this.form);
return foundry.utils.mergeObject(getDraft(), fd.object);
}
async _updateObject(_event, formData) {
await setDraft(formData);
ui.notifications.info("SLA briefing draft saved.");
}
static open() {
return new SlaBriefingDeskApp().render(true);
}
}
function installJournalButton(app, html) {
const root = getHtmlRoot(html);
const header = root?.querySelector(".directory-header .header-actions, .directory-header .action-buttons");
if (!header || header.querySelector(".sla-briefing-desk-open")) return;
const button = document.createElement("button");
button.type = "button";
button.className = "sla-briefing-desk-open";
button.innerHTML = `<i class="fas fa-file-lines"></i> Briefing Desk`;
button.addEventListener("click", () => SlaBriefingDeskApp.open());
header.appendChild(button);
}
Hooks.once("init", () => {
game.settings.register(MODULE_ID, DRAFT_KEY, {
scope: "world",
config: false,
type: Object,
default: defaultBriefing()
});
game.settings.registerMenu(MODULE_ID, "briefingDeskMenu", {
name: "SLA Briefing Desk",
label: "Open Briefing Desk",
hint: "Draft or randomize an SLA mission briefing.",
icon: "fas fa-file-lines",
type: SlaBriefingDeskApp,
restricted: true
});
});
Hooks.on("renderJournalDirectory", installJournalButton);
+83
View File
@@ -0,0 +1,83 @@
.sla-briefing-desk .window-content {
background: linear-gradient(180deg, #16171c 0%, #1d2026 100%);
color: #f4efe5 !important;
}
.sla-briefing-form {
display: grid;
gap: 12px;
}
.sla-briefing-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.sla-briefing-field,
.sla-briefing-card {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(213, 180, 88, 0.3);
border-radius: 12px;
padding: 10px 12px;
}
.sla-briefing-field label,
.sla-briefing-card h3 {
display: block;
color: #f4efe5 !important;
margin-bottom: 6px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.sla-briefing-field input,
.sla-briefing-field textarea {
width: 100%;
background: rgba(0, 0, 0, 0.35);
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 10px;
color: #f4efe5 !important;
padding: 8px 10px;
}
.sla-briefing-desk .window-title,
.sla-briefing-desk p,
.sla-briefing-desk span,
.sla-briefing-desk strong,
.sla-briefing-desk input,
.sla-briefing-desk textarea,
.sla-briefing-desk option {
color: #f4efe5 !important;
}
.sla-briefing-desk input::placeholder,
.sla-briefing-desk textarea::placeholder {
color: rgba(244, 239, 229, 0.58) !important;
}
.sla-briefing-field textarea {
min-height: 88px;
}
.sla-briefing-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.sla-briefing-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-briefing-grid {
grid-template-columns: 1fr;
}
}
+51
View File
@@ -0,0 +1,51 @@
<form class="sla-briefing-form">
<div class="sla-briefing-grid">
<div class="sla-briefing-field">
<label>Job Name</label>
<input type="text" name="jobName" value="{{briefing.jobName}}">
</div>
<div class="sla-briefing-field">
<label>Mission Type</label>
<input type="text" name="missionType" value="{{briefing.missionType}}">
</div>
<div class="sla-briefing-field">
<label>Sector</label>
<input type="text" name="sector" value="{{briefing.sector}}">
</div>
<div class="sla-briefing-field">
<label>Target</label>
<input type="text" name="target" value="{{briefing.target}}">
</div>
<div class="sla-briefing-field">
<label>Primary Contact</label>
<input type="text" name="contact" value="{{briefing.contact}}">
</div>
<div class="sla-briefing-field">
<label>Payoff</label>
<input type="text" name="payoff" value="{{briefing.payoff}}">
</div>
</div>
<div class="sla-briefing-grid">
<div class="sla-briefing-field">
<label>Complication</label>
<textarea name="complication">{{briefing.complication}}</textarea>
</div>
<div class="sla-briefing-field">
<label>Twist</label>
<textarea name="twist">{{briefing.twist}}</textarea>
</div>
</div>
<div class="sla-briefing-field">
<label>Operational Notes</label>
<textarea name="notes">{{briefing.notes}}</textarea>
</div>
<div class="sla-briefing-actions">
<button type="button" data-action="randomize">Randomize</button>
<button type="button" data-action="chat">Post To Chat</button>
<button type="button" data-action="journal">Create Journal</button>
<button type="submit">Save Draft</button>
</div>
</form>