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 7577b503c5
7 changed files with 388 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/
+26
View File
@@ -0,0 +1,26 @@
{
"ammo": [
{ "name": "Ammo: 3mm [STD]", "quantity": 8, "weight": 0, "cost": 3 },
{ "name": "Ammo: 3mm SMG [HE]", "quantity": 4, "weight": 0, "cost": 3 },
{ "name": "Ammo: 3mm [AP]", "quantity": 4, "weight": 0, "cost": 3 },
{ "name": "Ammo: Shotgun Shells", "quantity": 10, "weight": 0.2, "cost": 4 }
],
"medical": [
{ "name": "BOOPA Medical Kit (Standard)", "quantity": 1, "weight": 1.5, "cost": 50 },
{ "name": "Stim Patch", "quantity": 2, "weight": 0.1, "cost": 15 },
{ "name": "Rush", "quantity": 1, "weight": 0, "cost": 50, "type": "drug" },
{ "name": "Bandage Pack", "quantity": 3, "weight": 0.1, "cost": 4 }
],
"breach": [
{ "name": "Flashbang Grenade", "quantity": 2, "weight": 0.3, "cost": 25 },
{ "name": "Entrenching Tool", "quantity": 1, "weight": 1, "cost": 12 },
{ "name": "Duct Tape (Roll)", "quantity": 2, "weight": 0.2, "cost": 2 },
{ "name": "Cutting Charge", "quantity": 1, "weight": 0.6, "cost": 75 }
],
"surveillance": [
{ "name": "UV / Multi-Spectrum Torch", "quantity": 1, "weight": 0.3, "cost": 15 },
{ "name": "Klippo Multi-Band Communicator", "quantity": 1, "weight": 0.3, "cost": 150 },
{ "name": "Disposable Camera Drone", "quantity": 1, "weight": 0.4, "cost": 80 },
{ "name": "Oyster Card (ITB Card)", "quantity": 1, "weight": 0, "cost": 5 }
]
}
+3
View File
@@ -0,0 +1,3 @@
{
"SLAGearCache.Title": "SLA Gear Cache"
}
+32
View File
@@ -0,0 +1,32 @@
{
"id": "sla-gear-cache",
"title": "SLA Gear Cache",
"description": "Generates ammo caches, med packs, breach kits, and requisition bundles for SLA operations.",
"version": "0.1.0",
"compatibility": {
"minimum": "13",
"verified": "13.350",
"maximum": "13"
},
"authors": [
{
"name": "Tim Evans"
}
],
"esmodules": [
"module/sla-gear-cache.mjs"
],
"styles": [
"styles/sla-gear-cache.css"
],
"languages": [
{
"lang": "en",
"name": "English",
"path": "lang/en.json"
}
],
"manifest": "https://raw.githubusercontent.com/slaguru666/sla-gear-cache/main/module.json",
"download": "https://github.com/slaguru666/sla-gear-cache/releases/latest/download/sla-gear-cache.zip",
"url": "https://github.com/slaguru666/sla-gear-cache"
}
+195
View File
@@ -0,0 +1,195 @@
const MODULE_ID = "sla-gear-cache";
const DRAFT_KEY = "cacheDraft";
let CACHE_TABLES = null;
function getHtmlRoot(html) {
return html instanceof HTMLElement ? html : html?.[0] ?? null;
}
async function loadTables() {
if (CACHE_TABLES) return CACHE_TABLES;
const response = await fetch("modules/sla-gear-cache/data/cache-tables.json");
CACHE_TABLES = await response.json();
return CACHE_TABLES;
}
function defaultState() {
return {
cacheType: "ammo",
itemCount: 3,
items: []
};
}
function getState() {
return foundry.utils.mergeObject(defaultState(), game.settings.get(MODULE_ID, DRAFT_KEY) ?? {});
}
async function setState(state) {
await game.settings.set(MODULE_ID, DRAFT_KEY, foundry.utils.mergeObject(defaultState(), state ?? {}));
}
function sumItems(items) {
return items.reduce((totals, item) => {
totals.weight += Number(item.weight || 0) * Number(item.quantity || 0);
totals.cost += Number(item.cost || 0) * Number(item.quantity || 0);
return totals;
}, { weight: 0, cost: 0 });
}
function renderCacheHtml(state) {
const items = state.items ?? [];
const rows = items.map((item) => `<li>${foundry.utils.escapeHTML(item.name)} x${item.quantity} | ${item.weight} kg | ${item.cost} cR</li>`).join("");
const totals = sumItems(items);
return `
<section class="sla-gear-cache-output">
<h2>SLA Gear Cache - ${foundry.utils.escapeHTML(state.cacheType)}</h2>
<ul>${rows}</ul>
<p><strong>Total Weight:</strong> ${totals.weight.toFixed(1)} kg | <strong>Total Value:</strong> ${totals.cost.toFixed(0)} cR</p>
</section>
`;
}
async function randomCache(cacheType, itemCount) {
const tables = await loadTables();
const pool = tables[cacheType] ?? [];
const items = [];
for (let index = 0; index < Math.max(1, Number(itemCount || 1)); index += 1) {
const picked = foundry.utils.deepClone(pool[Math.floor(Math.random() * pool.length)]);
if (!picked) continue;
items.push(picked);
}
return {
cacheType,
itemCount: Number(itemCount || 3),
items
};
}
function selectedActor() {
if (canvas?.tokens?.controlled?.length) return canvas.tokens.controlled[0].actor;
return game.user.character ?? null;
}
async function addItemsToActor(actor, items) {
if (!actor) {
ui.notifications.warn("Select a token or character first.");
return;
}
const docs = items.map((item) => ({
name: item.name,
type: item.type ?? "item",
system: {
quantity: item.quantity,
weight: item.weight,
cost: item.cost,
description: { value: "Generated by SLA Gear Cache." }
}
}));
await actor.createEmbeddedDocuments("Item", docs);
ui.notifications.info(`Added ${docs.length} generated cache item(s) to ${actor.name}.`);
}
class SlaGearCacheApp extends FormApplication {
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
id: "sla-gear-cache-app",
title: "SLA Gear Cache",
template: "modules/sla-gear-cache/templates/gear-cache.hbs",
classes: ["sla-gear-cache"],
width: 560,
height: "auto",
closeOnSubmit: false,
submitOnChange: false,
resizable: true
});
}
async getData() {
const state = getState();
return {
state,
items: state.items ?? [],
totals: sumItems(state.items ?? []),
cacheTypes: [
{ value: "ammo", label: "Ammo Cache", selected: state.cacheType === "ammo" },
{ value: "medical", label: "Medical Cache", selected: state.cacheType === "medical" },
{ value: "breach", label: "Breach Kit", selected: state.cacheType === "breach" },
{ value: "surveillance", label: "Surveillance Pack", selected: state.cacheType === "surveillance" }
]
};
}
activateListeners(html) {
super.activateListeners(html);
html.find("[data-action='randomize']").on("click", async () => {
const state = await this._collectState();
const nextState = await randomCache(state.cacheType, state.itemCount);
await setState(nextState);
this.render(false);
});
html.find("[data-action='chat']").on("click", async () => {
const state = await this._collectState();
await setState(state);
await ChatMessage.create({
user: game.user.id,
speaker: { alias: "SLA Gear Cache" },
content: renderCacheHtml(state)
});
});
html.find("[data-action='actor']").on("click", async () => {
const state = await this._collectState();
await setState(state);
await addItemsToActor(selectedActor(), state.items ?? []);
});
}
async _collectState() {
const fd = new foundry.applications.ux.FormDataExtended(this.form);
return foundry.utils.mergeObject(getState(), fd.object);
}
async _updateObject(_event, formData) {
await setState(formData);
ui.notifications.info("SLA Gear Cache saved.");
}
static open() {
return new SlaGearCacheApp().render(true);
}
}
function installActorButton(app, html) {
const root = getHtmlRoot(html);
const header = root?.querySelector(".directory-header .header-actions, .directory-header .action-buttons");
if (!header || header.querySelector(".sla-gear-cache-open")) return;
const button = document.createElement("button");
button.type = "button";
button.className = "sla-gear-cache-open";
button.innerHTML = `<i class="fas fa-box-open"></i> Gear Cache`;
button.addEventListener("click", () => SlaGearCacheApp.open());
header.appendChild(button);
}
Hooks.once("init", () => {
game.settings.register(MODULE_ID, DRAFT_KEY, {
scope: "world",
config: false,
type: Object,
default: defaultState()
});
game.settings.registerMenu(MODULE_ID, "gearCacheMenu", {
name: "SLA Gear Cache",
label: "Open Gear Cache",
hint: "Generate SLA ammo, medical, breach, and surveillance stash bundles.",
icon: "fas fa-box-open",
type: SlaGearCacheApp,
restricted: true
});
});
Hooks.on("renderActorDirectory", installActorButton);
+80
View File
@@ -0,0 +1,80 @@
.sla-gear-cache .window-content {
background: linear-gradient(180deg, #0f141a 0%, #181d25 100%);
color: #f5efe4 !important;
}
.sla-gear-cache-form {
display: grid;
gap: 12px;
}
.sla-gear-cache-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.sla-gear-cache-field,
.sla-gear-cache-list {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(215, 181, 82, 0.25);
border-radius: 12px;
padding: 10px 12px;
}
.sla-gear-cache-field label,
.sla-gear-cache-list h3 {
display: block;
color: #f5efe4 !important;
margin-bottom: 6px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.sla-gear-cache-field select,
.sla-gear-cache-field input {
width: 100%;
background: rgba(0, 0, 0, 0.35);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 10px;
color: #f5efe4 !important;
padding: 8px 10px;
}
.sla-gear-cache .window-title,
.sla-gear-cache p,
.sla-gear-cache span,
.sla-gear-cache strong,
.sla-gear-cache li,
.sla-gear-cache input,
.sla-gear-cache select,
.sla-gear-cache option {
color: #f5efe4 !important;
}
.sla-gear-cache-list ul {
margin: 0;
padding-left: 18px;
}
.sla-gear-cache-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.sla-gear-cache-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-gear-cache-grid {
grid-template-columns: 1fr;
}
}
+37
View File
@@ -0,0 +1,37 @@
<form class="sla-gear-cache-form">
<div class="sla-gear-cache-grid">
<div class="sla-gear-cache-field">
<label>Cache Type</label>
<select name="cacheType">
{{#each cacheTypes}}
<option value="{{this.value}}" {{#if this.selected}}selected{{/if}}>{{this.label}}</option>
{{/each}}
</select>
</div>
<div class="sla-gear-cache-field">
<label>Items To Roll</label>
<input type="number" name="itemCount" min="1" max="6" value="{{state.itemCount}}">
</div>
</div>
<div class="sla-gear-cache-list">
<h3>Generated Cache</h3>
{{#if items.length}}
<ul>
{{#each items}}
<li>{{name}} x{{quantity}} | {{weight}} kg | {{cost}} cR</li>
{{/each}}
</ul>
<p><strong>Total Weight:</strong> {{totals.weight}} kg | <strong>Total Value:</strong> {{totals.cost}} cR</p>
{{else}}
<p>No cache generated yet.</p>
{{/if}}
</div>
<div class="sla-gear-cache-actions">
<button type="button" data-action="randomize">Randomize Cache</button>
<button type="button" data-action="chat">Post To Chat</button>
<button type="button" data-action="actor">Add To Selected Actor</button>
<button type="submit">Save Cache</button>
</div>
</form>