// W&G Voidships Builder — Voidship Actor Sheet (vehicle+flag emulation) // v1.0.0 (safe MVP) — works on Foundry v11–v13 /* eslint-env es2021 */ const VB_MOD = "wg-voidships-builder"; class VoidshipSheet extends ActorSheet { static get defaultOptions() { const opts = super.defaultOptions; return foundry.utils.mergeObject(opts, { classes: ["wg-voidships", "sheet", "actor", "voidship"], template: `modules/${VB_MOD}/templates/voidship-sheet.hbs`, width: 720, height: 680, tabs: [{ navSelector: ".tabs", contentSelector: ".sheet-body", initial: "wg" }] }); } /** Prefer to use this sheet only for vehicles marked flagiem voidship */ _canUserView(user) { return !!user; } get isVoidship() { return this.actor?.getFlag(VB_MOD, "isVoidship") === true; } /** Aggregate base + mods from items into derived summary (safe, optional) */ _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 slots = { prow: 0, dorsal: 0, port: 0, starboard: 0, keel: 0, other: 0 }; const items = a?.items ?? []; for (const it of items) { const eq = (it.system?.equipped ?? true); // treat as mounted unless explicitly off const v = it.system?.voidship ?? {}; const m = v.mods ?? {}; if (eq) { for (const k of Object.keys(mods)) { const val = Number(m[k]); if (Number.isFinite(val)) mods[k] += val; } } const slot = String((v.slot || it.getFlag(VB_MOD, "slot") || "")).toLowerCase(); if (slot && slots.hasOwnProperty(slot)) slots[slot]++; else slots.other++; } 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 }; // Headroom (positive = free) total.spaceFree = total.space; total.powerFree = total.power; // If components encode their usage (voidship.use.{space|power}) for (const it of items) { const use = it.system?.voidship?.use ?? {}; if (Number.isFinite(Number(use.space))) total.spaceFree -= Number(use.space); if (Number.isFinite(Number(use.power))) total.powerFree -= Number(use.power); } return { base, mods, total, slots }; } async getData(options = {}) { const data = await super.getData(options); data.isVoidship = this.isVoidship; data.voidship = this._computeDerived(); const hull = this.actor.getFlag(VB_MOD, "hull") || {}; data.hull = { class: hull.class || this.actor.name, size: hull.size || "Cruiser", slotsAllowed: hull.slots || { prow: 0, dorsal: 0, port: 0, starboard: 0, keel: 0 } }; data.crew = this.actor.getFlag(VB_MOD, "crew") || { command: 1, pilot: 1, sensors: 1, engineering: 2, gunnery: 2, security: 2, flightDeck: 0, passengers: 0 }; data.rtRaw = this.actor.getFlag(VB_MOD, "rtRaw") || {}; try { data.rtRawString = JSON.stringify(data.rtRaw, null, 2); } catch (e) { data.rtRawString = "{}"; } // expose a minimal config (labels) data.cfg = { labels: { armour: "Armour", integrity: "Integrity", manoeuvre: "Manoeuvre", detection: "Detection", speed: "Speed", space: "Space", power: "Power", spaceFree: "Space (free)", powerFree: "Power (free)" } }; return data; } activateListeners(html) { super.activateListeners(html); // Guard: allow sheet UI even for non-voidship, but prompt to mark 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); }); 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 (html) => { const form = html[0].querySelector("form") || html[0]; const get = (k) => Number(form.querySelector(`[name="${k}"]`)?.value || 0); const patch = { "base.armour": get("armour"), "base.integrity": get("integrity"), "base.manoeuvre": get("manoeuvre"), "base.detection": get("detection"), "base.speed": get("speed"), "base.space": get("space"), "base.power": get("power") }; for (const [k, v] of Object.entries(patch)) await this.actor.setFlag(VB_MOD, k, v); this.render(true); } }, cancel: { label: "Cancel" } }, default: "ok" }, { jQuery: true }).render(true); }); } } Hooks.once("init", () => { // Register the sheet for the 'vehicle' type (W&G Vehicles via Church of Steel) Actors.registerSheet(VB_MOD, VoidshipSheet, { types: ["vehicle"], makeDefault: false, label: "Voidship (W&G)" }); // Optional: a convenient factory on game namespace 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`); });