// W&G Voidships Builder — Voidship Actor Sheet (vehicle+flag emulation) // v1.0.1 — works on Foundry v11–v13 /* eslint-env es2021 */ const VB_MOD = "wg-voidships-builder"; const VB_DEBUG = true; // debug; później zrobimy game.settings function dbg(...args) { if (VB_DEBUG) console.debug("[VB]", ...args); } 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; } /** Prostszą agregację zostawiamy do walidacji ręcznej etc. */ _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); 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 && Object.prototype.hasOwnProperty.call(slots, 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 }; total.spaceFree = total.space; total.powerFree = total.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); } dbg("computeDerived: base=", base, "mods=", mods, "total=", total, "slots=", slots); return { base, mods, total, slots }; } async getData(options) { const data = await super.getData(options); const actor = this.actor; // ——— FLAGS (cały namespace) const ns = actor.flags?.[VB_MOD] || {}; const hull = ns.hull || { class: actor.name, size: "Cruiser", slots: { prow: 0, dorsal: 0, port: 0, starboard: 0, keel: 0 } }; const base = ns.base || { armour: 0, integrity: 0, manoeuvre: 0, detection: 0, speed: 0, space: 0, power: 0 }; const crew = ns.crew || { command: 1, pilot: 1, sensors: 1, engineering: 2, gunnery: 2, security: 2, flightDeck: 0, passengers: 0 }; const rtRaw = ns.rtRaw || {}; const rt = rtRaw.rt || {}; // ——— RT snapshot do karty 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 ?? "" }; // ——— KOMPONENTY (tylko nasze) const components = actor.items.contents .filter(i => i.flags?.[VB_MOD]?.component?.kind) .map(i => { const vs = i.system?.voidship ?? i.flags?.[VB_MOD]?.voidship ?? {}; const v = foundry.utils.deepClone(vs); v.itemId = i.id; v.name = i.name; v.equipped = (i.system?.equipped ?? true); v.slot = (v.slot || "Internal"); v.use = Object.assign({ space: 0, power: 0 }, v.use); v.mods = Object.assign({ armour: 0, integrity: 0, manoeuvre: 0, detection: 0, speed: 0, space: 0, power: 0 }, v.mods); v.kind = i.flags?.[VB_MOD]?.component?.kind; v.tags = i.flags?.[VB_MOD]?.component?.tags ?? []; return v; }); // ——— SUMY MODYFIKATORÓW (equipped) const modsSum = components.reduce((acc, c) => { if (!c.equipped) return acc; acc.armour += Number(c.mods.armour || 0); acc.integrity += Number(c.mods.integrity || 0); acc.manoeuvre += Number(c.mods.manoeuvre || 0); acc.detection += Number(c.mods.detection || 0); acc.speed += Number(c.mods.speed || 0); acc.space += Number(c.mods.space || 0); acc.power += Number(c.mods.power || 0); return acc; }, { armour:0, integrity:0, manoeuvre:0, detection:0, speed:0, space:0, power:0 }); // ——— CAPACITY (use) dla equipped const useTotal = components.reduce((acc, c) => { if (!c.equipped) return acc; acc.space += Number(c.use.space || 0); acc.power += Number(c.use.power || 0); return acc; }, { space: 0, power: 0 }); // ——— TOTALS = BASE + MODS const totals = { armour: (Number(base.armour) || 0) + modsSum.armour, integrity: (Number(base.integrity) || 0) + modsSum.integrity, manoeuvre: (Number(base.manoeuvre) || 0) + modsSum.manoeuvre, detection: (Number(base.detection) || 0) + modsSum.detection, speed: (Number(base.speed) || 0) + modsSum.speed, space: (Number(base.space) || 0) + modsSum.space, power: (Number(base.power) || 0) + modsSum.power }; totals.spaceFree = totals.space - useTotal.space; totals.powerFree = totals.power - useTotal.power; // ——— SLOTY (użyte vs limit) const slotsUsed = components.reduce((acc, c) => { if (!c.equipped) return acc; const k = (c.slot || "Internal").toLowerCase(); if (["prow","dorsal","port","starboard","keel"].includes(k)) acc[k] = (acc[k]||0)+1; return acc; }, { prow:0, dorsal:0, port:0, starboard:0, keel:0 }); // ——— Ostrzeżenia + wymagania const warnings = {}; const anySlotOver = (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 (anySlotOver) warnings.slots = "One or more weapon slot groups exceed hull limits."; if (totals.spaceFree < 0) warnings.capacity = "Not enough Space for installed components."; if (totals.powerFree < 0) warnings.capacity = (warnings.capacity ? warnings.capacity + " " : "") + "Not enough Power for installed components."; const req = (hull.requirements ?? []).map(r => ({ tag: r.tag, min: Number(r.min||0), label: r.label || r.tag })); const haveByTag = components.reduce((acc, c) => { if (!c.equipped) return acc; const tags = Array.isArray(c.tags) ? c.tags : []; for (const t of tags) acc[t] = (acc[t]||0) + 1; return acc; }, {}); const missing = req.filter(r => (haveByTag[r.tag]||0) < r.min); if (missing.length) { const list = missing.map(m => `${m.label}: ${haveByTag[m.tag]||0}/${m.min}`).join("; "); warnings.requirements = `Missing required components → ${list}`; } // ——— Dane do template data.hull = hull; data.base = base; data.crew = crew; data.rtCard = rtCard; data.components = components; data.modsSum = modsSum; data.totals = totals; data.slotsUsed = slotsUsed; data.warnings = warnings; data.flags = actor.flags; return data; } activateListeners(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); }); // Base config dialog 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 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 equipped html.on("click", "[data-action='comp-toggle']", async (ev) => { const tr = ev.currentTarget.closest("tr[data-item-id]"); const itemId = tr?.dataset.itemId; if (!itemId) return; const item = this.actor.items.get(itemId); const cur = !!(item.system?.equipped ?? true); await item.update({ "system.equipped": !cur }); this.render(true); }); // Edit component html.on("click", "[data-action='comp-edit']", async (ev) => { const tr = ev.currentTarget.closest("tr[data-item-id]"); const itemId = tr?.dataset.itemId; if (!itemId) return; const item = this.actor.items.get(itemId); const v = foundry.utils.deepClone(item.system?.voidship || {}); v.slot = v.slot || "Internal"; v.use = Object.assign({ space: 0, power: 0 }, v.use); v.mods = Object.assign({ armour: 0, integrity: 0, manoeuvre: 0, detection: 0, speed: 0, space: 0, power: 0 }, v.mods); const eq = (item.system?.equipped ?? true); const slotOptions = ["Prow", "Dorsal", "Port", "Starboard", "Keel", "Internal", "Aetherics"] .map(s => ``).join(""); const content = ` `; new Dialog({ title: `Edit Component — ${item.name}`, content, buttons: { ok: { label: "Save", callback: async (dlg) => { const f = dlg[0].querySelector("form"); const read = (sel, num = false) => { const el = f.querySelector(`[name="${sel}"]`); if (!el) return num ? 0 : ""; if (el.type === "checkbox") return el.checked; const val = el.value ?? (el.textContent ?? ""); return num ? Number(val || 0) : val; }; const patch = { "system.voidship.slot": read("slot"), "system.voidship.use.space": read("use.space", true), "system.voidship.use.power": read("use.power", true), "system.voidship.mods.armour": read("mods.armour", true), "system.voidship.mods.integrity": read("mods.integrity", true), "system.voidship.mods.manoeuvre": read("mods.manoeuvre", true), "system.voidship.mods.detection": read("mods.detection", true), "system.voidship.mods.speed": read("mods.speed", true), "system.voidship.mods.space": read("mods.space", true), "system.voidship.mods.power": read("mods.power", true), "system.equipped": !!read("equipped") }; await item.update(patch); // miękka walidacja const { total, slots } = this._computeDerived(); const flags = this.actor.getFlag(VB_MOD, "") || {}; const hull = flags.hull || { slots: { prow: 0, dorsal: 0, port: 0, starboard: 0, keel: 0 } }; const overSlots = ["prow", "dorsal", "port", "starboard", "keel"].filter(k => slots[k] > (hull.slots?.[k] || 0)); if (overSlots.length) ui.notifications.warn(`Slot limit exceeded: ${overSlots.join(", ")}`); if (total.spaceFree < 0) ui.notifications.warn(`Not enough Space (over by ${Math.abs(total.spaceFree)})`); if (total.powerFree < 0) ui.notifications.warn(`Not enough Power (over by ${Math.abs(total.powerFree)})`); this.render(true); } }, cancel: { label: "Cancel" } }, default: "ok" }, { jQuery: true }).render(true); }); // Remove html.on("click", "[data-action='comp-remove']", async (ev) => { const tr = ev.currentTarget.closest("tr[data-item-id]"); const itemId = tr?.dataset.itemId; if (!itemId) return; const item = this.actor.items.get(itemId); new Dialog({ title: "Remove Component", content: `Remove ${foundry.utils.escapeHTML(item.name)} from this voidship?
`, buttons: { ok: { label: "Remove", callback: () => this.actor.deleteEmbeddedDocuments("Item", [itemId]) }, cancel: { label: "Cancel" } }, default: "ok" }).render(true); }); // Auto-refresh po zmianach itemów tego aktora const refreshIfMine = (doc) => { try { if (doc?.parent?.id === this.actor?.id) this.render(false); } catch(_) {} }; Hooks.on('createItem', refreshIfMine); Hooks.on('updateItem', refreshIfMine); Hooks.on('deleteItem', refreshIfMine); this.once("close", () => { Hooks.off('createItem', refreshIfMine); Hooks.off('updateItem', refreshIfMine); Hooks.off('deleteItem', refreshIfMine); }); } } // ———————————————————————————————————————————————————————————————— // INIT: helper + rejestracja sheetu + domyślki/sanitizacja Hooks.once("init", () => { if (!Handlebars.helpers.signed) { Handlebars.registerHelper("signed", function (value) { const n = Number(value || 0); if (Number.isNaN(n) || n === 0) return "±0"; return (n > 0 ? "+" : "−") + Math.abs(n); }); } 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; }; // Auto-domyślki dla itemów dodawanych do voidshipów Hooks.on("preCreateItem", (item) => { const parent = item?.parent; if (!(parent instanceof Actor)) return; if (parent.getFlag(VB_MOD, "isVoidship") !== true) return; const vs = item.system?.voidship; if (!vs) { item.updateSource({ "system.voidship": { slot: "Internal", use: { space: 0, power: 0 }, mods: { armour: 0, integrity: 0, manoeuvre: 0, detection: 0, speed: 0, space: 0, power: 0 } } }); } if (item.system?.equipped === undefined) item.updateSource({ "system.equipped": true }); }); // Sanitizacja liczb przy update (żeby nie trafiały NaN) Hooks.on("preUpdateItem", (item, change) => { const parent = item?.parent; if (!(parent instanceof Actor)) return; if (parent.getFlag(VB_MOD, "isVoidship") !== true) return; const fixNum = (path) => { const v = foundry.utils.getProperty(change, path); if (v === undefined) return; const n = Number(v); foundry.utils.setProperty(change, path, Number.isFinite(n) ? n : 0); }; [ "system.voidship.use.space", "system.voidship.use.power", "system.voidship.mods.armour", "system.voidship.mods.integrity", "system.voidship.mods.manoeuvre", "system.voidship.mods.detection", "system.voidship.mods.speed", "system.voidship.mods.space", "system.voidship.mods.power" ].forEach(fixNum); }); console.log(`${VB_MOD}: VoidshipSheet registered`); });