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,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 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"SLAGearCache.Title": "SLA Gear Cache"
|
||||
}
|
||||
+32
@@ -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"
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user