Files
sla-gear-cache/module/sla-gear-cache.mjs
T

197 lines
5.9 KiB
JavaScript

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) {
if (!game.user?.isGM) return;
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);