commit 7577b503c5531d588ce5ad036c49a9c6d43cd395 Author: Tim Evans (slaguru666) Date: Tue Mar 31 17:00:36 2026 +0100 chore: initial deploy — SLA Industries Foundry VTT Co-authored-by: slaguru666 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/data/cache-tables.json b/data/cache-tables.json new file mode 100644 index 0000000..84e0e13 --- /dev/null +++ b/data/cache-tables.json @@ -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 } + ] +} diff --git a/lang/en.json b/lang/en.json new file mode 100644 index 0000000..f28f3c5 --- /dev/null +++ b/lang/en.json @@ -0,0 +1,3 @@ +{ + "SLAGearCache.Title": "SLA Gear Cache" +} diff --git a/module.json b/module.json new file mode 100644 index 0000000..c951082 --- /dev/null +++ b/module.json @@ -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" +} \ No newline at end of file diff --git a/module/sla-gear-cache.mjs b/module/sla-gear-cache.mjs new file mode 100644 index 0000000..87b3b61 --- /dev/null +++ b/module/sla-gear-cache.mjs @@ -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) => `
  • ${foundry.utils.escapeHTML(item.name)} x${item.quantity} | ${item.weight} kg | ${item.cost} cR
  • `).join(""); + const totals = sumItems(items); + return ` +
    +

    SLA Gear Cache - ${foundry.utils.escapeHTML(state.cacheType)}

    +
      ${rows}
    +

    Total Weight: ${totals.weight.toFixed(1)} kg | Total Value: ${totals.cost.toFixed(0)} cR

    +
    + `; +} + +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 = ` 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); diff --git a/styles/sla-gear-cache.css b/styles/sla-gear-cache.css new file mode 100644 index 0000000..f0152d4 --- /dev/null +++ b/styles/sla-gear-cache.css @@ -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; + } +} diff --git a/templates/gear-cache.hbs b/templates/gear-cache.hbs new file mode 100644 index 0000000..b16ed08 --- /dev/null +++ b/templates/gear-cache.hbs @@ -0,0 +1,37 @@ +
    +
    +
    + + +
    +
    + + +
    +
    + +
    +

    Generated Cache

    + {{#if items.length}} +
      + {{#each items}} +
    • {{name}} x{{quantity}} | {{weight}} kg | {{cost}} cR
    • + {{/each}} +
    +

    Total Weight: {{totals.weight}} kg | Total Value: {{totals.cost}} cR

    + {{else}} +

    No cache generated yet.

    + {{/if}} +
    + +
    + + + + +
    +