// W&G Voidships Builder — Voidship Actor Sheet (vehicle+flag emulation) // v1.1.0 (mapping weapons + core reqs + safer drops) — Foundry v11–v13 /* eslint-env es2021 */ const VB_MOD = "wg-voidships-builder"; const VB_DEBUG = true; // debug on; switch later via setting function dbg(...args) { if (VB_DEBUG) console.debug("[VB]", ...args); } // Canonical slot keys const SLOT_KEYS = ["prow","dorsal","port","starboard","keel"]; // Default RT-like core requirements (if hull.requirements is empty) const DEFAULT_CORE_REQS = [ { tag: "bridge", min: 1, label: "Bridge" }, { tag: "life-support", min: 1, label: "Life Support" }, { tag: "gellar", min: 1, label: "Gellar Field" }, { tag: "warp-drive", min: 1, label: "Warp Drive" }, { tag: "reactor", min: 1, label: "Plasma/Power Reactor" }, { tag: "augur", min: 1, label: "Augur Array" }, { tag: "void-shields", min: 1, label: "Void Shields" } ]; class VoidshipSheet extends ActorSheet { static get defaultOptions() { const opts = super.defaultOptions; return foundry.utils.mergeObject(opts, { classes: ["wg-voidships", "sheet", "actor", "voidship", "themed", "theme-light"], template: `modules/${VB_MOD}/templates/voidship-sheet.hbs`, width: 720, height: 680, tabs: [{ navSelector: ".tabs", contentSelector: ".sheet-body", initial: "wg" }] }); } _canUserView(user) { return !!user; } get isVoidship() { return this.actor?.getFlag(VB_MOD, "isVoidship") === true; } /** Sum flags.base + items mods + usage */ _computeDerived() { const a = this.actor; const b = a.getFlag(VB_MOD, "base") || {}; const base = { armour: Number(b.armour)||0, integrity: Number(b.integrity)||0, manoeuvre: Number(b.manoeuvre)||0, detection: Number(b.detection)||0, speed: Number(b.speed)||0, space: Number(b.space)||0, power: Number(b.power)||0 }; const mods = { armour:0, integrity:0, manoeuvre:0, detection:0, speed:0, space:0, power:0 }; const usage = { space:0, power:0 }; const slotsUsed = { prow:0, dorsal:0, port:0, starboard:0, keel:0 }; for (const it of a.items.contents) { const sys = it.system ?? {}; const v = sys.voidship ?? {}; const equipped = (sys.equipped ?? true) === true; // treat true/undefined as mounted const comp = it.getFlag(VB_MOD, "component") || v.component || {}; // our kind/tags live in flags if (equipped) { // accumulate mods const m = v.mods || {}; for (const k of Object.keys(mods)) { const n = Number(m[k]); if (Number.isFinite(n)) mods[k] += n; } // accumulate usage const u = v.use || {}; if (Number.isFinite(Number(u.space))) usage.space += Number(u.space); if (Number.isFinite(Number(u.power))) usage.power += Number(u.power); // weapon slots count ONLY for kind === "weapon" const kind = String(comp?.kind || "").toLowerCase(); const slot = String(v.slot || "").toLowerCase(); if (kind === "weapon" && SLOT_KEYS.includes(slot)) slotsUsed[slot] += 1; } } const total = { armour: base.armour + mods.armour, integrity: base.integrity + mods.integrity, manoeuvre: base.manoeuvre + mods.manoeuvre, detection: base.detection + mods.detection, speed: base.speed + mods.speed, space: base.space + mods.space, power: base.power + mods.power, spaceFree: base.space + mods.space - usage.space, powerFree: base.power + mods.power - usage.power }; dbg("computeDerived:", { base, mods, usage, total, slotsUsed }); return { base, mods, usage, total, slotsUsed }; } } /** helpers */ Hooks.once("init", () => { if (!Handlebars.helpers.signed) { Handlebars.registerHelper("signed", v => { const n = Number(v||0); if (!Number.isFinite(n) || n===0) return "±0"; return (n>0?"+":"−")+Math.abs(n); }); } // pretty-json (for debug blocks) if (!Handlebars.helpers.tojson) { Handlebars.registerHelper("tojson", v => JSON.stringify(v, null, 2)); } }); VoidshipSheet.prototype.getData = async function(options) { const data = await super.getData(options); const actor = this.actor; const flags = actor.getFlag(VB_MOD, "") || {}; const hull = flags.hull || { class: actor.name, size: "Cruiser", slots: { prow:0,dorsal:0,port:0,starboard:0,keel:0 } }; const base = flags.base || { armour:0, integrity:0, manoeuvre:0, detection:0, speed:0, space:0, power:0 }; const crew = flags.crew || { command:1,pilot:1,sensors:1,engineering:2,gunnery:2,security:2,flightDeck:0,passengers:0 }; const rtRaw = flags["rtRaw"] || {}; const rt = rtRaw.rt || {}; const rtCard = { speed: rt.speed ?? rtRaw.combat?.speed ?? 0, manoeuvrability: rt.manoeuvrability ?? rtRaw.mnvr ?? 0, detection: rt.detection ?? 0, hullIntegrity: rt.hullIntegrity ?? rtRaw.combat?.wounds?.max ?? 0, armour: rt.armour ?? 0, space: rt.space ?? 0, power: rt.power ?? 0, weapons: { prow: rt.weapons?.prow ?? 0, dorsal: rt.weapons?.dorsal ?? 0, port: rt.weapons?.port ?? 0, starboard: rt.weapons?.starboard ?? 0, keel: rt.weapons?.keel ?? 0 }, turret: rt.turret ?? 0, notes: rtRaw.notes ?? "", source: rt.source ?? "" }; // components (only items having system.voidship) const components = actor.items.contents .filter(i => i.system?.voidship) .map(i => { const v = foundry.utils.deepClone(i.system.voidship || {}); const comp = i.getFlag(VB_MOD, "component") || v.component || {}; return { itemId: i.id, name: i.name, equipped: (i.system?.equipped ?? true) === true, slot: v.slot || "Internal", use: Object.assign({ space:0, power:0 }, v.use), mods: Object.assign({ armour:0, integrity:0, manoeuvre:0, detection:0, speed:0, space:0, power:0 }, v.mods), component: { kind: String(comp.kind||"").toLowerCase(), tags: comp.tags || [], allowedSlots: comp.allowedSlots || [] } }; }); // derived totals and slotsUsed (weapons only) const { base:baseCalc, mods, usage, total, slotsUsed } = this._computeDerived(); // Warnings: slots over-capacity (compare ONLY weapons vs hull slot caps) const warnings = {}; if (components.length) { const over = (slotsUsed.prow > (hull.slots?.prow || 0)) || (slotsUsed.dorsal > (hull.slots?.dorsal || 0)) || (slotsUsed.port > (hull.slots?.port || 0)) || (slotsUsed.starboard > (hull.slots?.starboard || 0)) || (slotsUsed.keel > (hull.slots?.keel || 0)); if (over) warnings.slots = "One or more weapon slot groups exceed hull limits."; } if (total.spaceFree < 0 || total.powerFree < 0) { warnings.capacity = "Not enough Space/Power for installed components."; } // Validate allowedSlots for weapons (if provided) const badMounts = components.filter(c => c.equipped && c.component?.kind === "weapon" && Array.isArray(c.component.allowedSlots) && c.component.allowedSlots.length && !c.component.allowedSlots.map(s => String(s).toLowerCase()).includes(String(c.slot).toLowerCase()) ); if (badMounts.length) { warnings.mounts = `Invalid weapon placement: ${badMounts.map(b=>`${b.name}→${b.slot}`).join(", ")}.`; } // Requirements — gather tags from equipped components const tagCounts = {}; for (const c of components) { if (!c.equipped) continue; for (const t of (c.component?.tags || [])) { const key = String(t).toLowerCase(); tagCounts[key] = (tagCounts[key] || 0) + 1; } } const reqs = (Array.isArray(hull.requirements) && hull.requirements.length) ? hull.requirements : DEFAULT_CORE_REQS; const missing = []; for (const r of reqs) { const need = Number(r.min||0); if (!need) continue; const have = tagCounts[String(r.tag||"").toLowerCase()] || 0; if (have < need) missing.push(`${r.label||r.tag}: ${have}/${need}`); } if (missing.length) warnings.requirements = `Missing required systems — ${missing.join("; ")}`; // expose to hbs data.hull = hull; data.base = base; data.crew = crew; data.mods = mods; data.usage = usage; data.totals = total; data.slotsUsed = slotsUsed; data.rtCard = rtCard; data.components = components; data.warnings = warnings; data.debug = { flags, tagCounts, reqs }; // handy raw blocks return data; }; VoidshipSheet.prototype.activateListeners = function(html) { super.activateListeners(html); // mark/unmark html.find("[data-action='mark-voidship']").on("click", async () => { await this.actor.setFlag(VB_MOD, "isVoidship", true); ui.notifications.info("Marked as Voidship."); this.render(true); }); html.find("[data-action='unmark-voidship']").on("click", async () => { await this.actor.unsetFlag(VB_MOD, "isVoidship"); ui.notifications.info("Unmarked Voidship."); this.render(true); }); // configure base html.find("[data-action='configure-base']").on("click", async () => { const cur = this._computeDerived().base; const content = `
`; new Dialog({ title: "Voidship — Base Stats", content, buttons: { ok: { label: "Save", callback: async (dlg) => { const form = dlg[0].querySelector("form") || dlg[0]; const get = (k) => Number(form.querySelector(`[name="${k}"]`)?.value || 0); const patchBase = { armour: get("armour"), integrity: get("integrity"), manoeuvre: get("manoeuvre"), detection: get("detection"), speed: get("speed"), space: get("space"), power: get("power") }; await this.actor.setFlag(VB_MOD, "base", patchBase); this.render(true); }}, cancel: { label: "Cancel" } }, default: "ok" }, { jQuery: true }).render(true); }); // toggle raw debug blocks html.find("[data-action='toggle-raw']").on("click", (ev) => { const el = html.find(".vb-raw")[0]; if (el) el.style.display = (el.style.display === "none") ? "block" : "none"; }); }; // ACCEPT only items having system.voidship (and optionally flags[VB_MOD].component) VoidshipSheet.prototype._onDropItem = async function(event, data) { try { const resolved = await Item.fromDropData(data); if (!resolved) return ui.notifications.error("Drop failed: cannot resolve item (no UUID/DATA)."); const sys = resolved.system ?? {}; if (!sys.voidship) return ui.notifications.warn("This item is not a voidship component."); // optional hard guard: if not our 'voidOnly' flag, still allow — but warn const vFlags = resolved.getFlag(VB_MOD, "component"); if (!vFlags) dbg("Component missing flags; proceeding with system.voidship only."); // pass to Foundry to actually create the embedded item const created = await this.actor.createEmbeddedDocuments("Item", [resolved.toObject()]); if (created?.length) { ui.notifications.info(`Installed component: ${created[0].name}`); this.render(true); } } catch (err) { console.error(err); ui.notifications.error("Drop failed: cannot resolve item data (no UUID/DATA)."); } }; Hooks.once("init", () => { Actors.registerSheet(VB_MOD, VoidshipSheet, { types: ["vehicle"], makeDefault: false, label: "Voidship (W&G)" }); game.wgVoidships = game.wgVoidships || {}; game.wgVoidships.createVoidship = async function (name = "New Voidship") { const actor = await Actor.create({ name, type: "vehicle", folder: null }); await actor.setFlag(VB_MOD, "isVoidship", true); ui.notifications.info(`Voidship created: ${name}`); return actor; }; console.log(`${VB_MOD}: VoidshipSheet registered`); });