Files

402 lines
15 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// W&G Voidships Builder — Voidship Actor Sheet (vehicle+flag emulation)
// v1.1.0 — Foundry V11V13 safe
/* eslint-env es2021 */
const VB_MOD = "wg-voidships-builder";
const VB_DEBUG = true; // Debug on
function dbg(...args) { if (VB_DEBUG) console.debug("[VB]", ...args); }
// --- SLOT HELPERS: normalizacja i przydział "pierwszy wolny pasujący" ---
const VB_WEAPON_SLOTS = ["prow", "dorsal", "port", "starboard", "keel"];
function normalizeSlotName(s) {
const v = String(s || "").trim().toLowerCase();
return VB_WEAPON_SLOTS.includes(v) ? v : null;
}
function normalizeAllowedSlots(allowed) {
if (!allowed) return [];
if (Array.isArray(allowed)) return allowed.map(normalizeSlotName).filter(Boolean);
return String(allowed).split(/[|,; ]+/g).map(normalizeSlotName).filter(Boolean);
}
function getVSH(it) {
// canonical: flags; fallback: legacy system.voidship (skopiujemy do flags przy dropie)
return it.getFlag(VB_MOD, "voidship") || it.system?.voidship || null;
}
function countUsedWeaponSlots(actor) {
const used = { prow: 0, dorsal: 0, port: 0, starboard: 0, keel: 0 };
for (const it of actor.items ?? []) {
const v = getVSH(it);
const eq = (it.system?.equipped ?? true);
const slot = normalizeSlotName(v?.slot);
if (eq && slot && (slot in used)) used[slot] += 1;
}
return used;
}
function pickFirstFreeSlot(allowed, hullSlots, used) {
const order = ["prow", "dorsal", "port", "starboard", "keel"];
for (const s of order) {
if (!allowed.includes(s)) continue;
const cap = Number(hullSlots?.[s] ?? 0);
const u = Number(used?.[s] ?? 0);
if (u < cap) return s;
}
return null;
}
// ---------------- Handlebars 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);
});
}
if (!Handlebars.helpers.eq) {
Handlebars.registerHelper("eq", (a, b) => a === b);
}
if (!Handlebars.helpers.json) {
Handlebars.registerHelper("json", (ctx) => {
try { return JSON.stringify(ctx, null, 2); }
catch { return String(ctx); }
});
}
});
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" }],
dragDrop: [{ dragSelector: ".voidship-sheet", dropSelector: ".voidship-sheet" }]
});
}
/** Allow viewing for everyone (adjust if you need perms) */
_canUserView(user) { return !!user; }
get isVoidship() { return this.actor?.getFlag(VB_MOD, "isVoidship") === true; }
// ---- math helpers ----
_sumModsFromItems(items) {
const mods = { armour: 0, integrity: 0, manoeuvre: 0, detection: 0, speed: 0, space: 0, power: 0 };
let useSpace = 0, usePower = 0;
const slotsUsed = { prow: 0, dorsal: 0, port: 0, starboard: 0, keel: 0 };
for (const it of items) {
const vs = it.system?.voidship;
if (!vs) continue;
const equipped = (it.system?.equipped ?? true);
const slotKey = String(vs.slot || "").toLowerCase();
if (equipped && ["prow", "dorsal", "port", "starboard", "keel"].includes(slotKey)) {
slotsUsed[slotKey] = (slotsUsed[slotKey] || 0) + 1;
}
// capacity usage (always counts, bo to realne zużycie jeśli wolisz tylko equipped, to obejmij warunkiem)
const use = vs.use || {};
if (Number.isFinite(Number(use.space))) useSpace += Number(use.space);
if (Number.isFinite(Number(use.power))) usePower += Number(use.power);
// mods liczą się tylko jeśli equipped
if (!equipped) continue;
const m = vs.mods || {};
for (const k of Object.keys(mods)) {
const val = Number(m[k]);
if (Number.isFinite(val)) mods[k] += val;
}
}
return { mods, use: { space: useSpace, power: usePower }, slotsUsed };
}
// ---- data aggregation for template ----
async getData(options) {
const data = await super.getData(options);
const actor = this.actor;
const VB_MOD = "wg-voidships-builder";
// Flagowe źródła (przeżywają migracje W&G)
// czytaj flagi wprost z actor.flags[namespace] (albo pojedyncze klucze)
const flagsNs = actor.flags?.[VB_MOD] || {};
const defaultHull = { class: actor.name, size: "Cruiser", slots: { prow: 0, dorsal: 0, port: 0, starboard: 0, keel: 0 } };
const defaultBase = { armour: 0, integrity: 0, manoeuvre: 0, detection: 0, speed: 0, space: 0, power: 0 };
const defaultCrew = { command: 0, pilot: 0, sensors: 0, engineering: 0, gunnery: 0, security: 0, flightDeck: 0, passengers: 0 };
const crew = actor.getFlag(VB_MOD, "crew") ?? flagsNs.crew ?? defaultCrew;
const rtRaw = actor.getFlag(VB_MOD, "rtRaw") ?? flagsNs.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 ?? rtRaw.source ?? ""
};
// --- komponenty z itemów (flags canonical) ---
const items = actor.items.contents || [];
const components = [];
const slotsUsed = { prow: 0, dorsal: 0, port: 0, starboard: 0, keel: 0 };
const mods = { armour: 0, integrity: 0, manoeuvre: 0, detection: 0, speed: 0, space: 0, power: 0 };
const useTotal = { space: 0, power: 0 };
for (const it of items) {
const v = getVSH(it);
if (!v) continue;
const c = foundry.utils.duplicate(v);
c.name = it.name;
c.itemId = it.id;
c.equipped = (it.system?.equipped ?? true);
c.slot = normalizeSlotName(c.slot) || "Internal";
c.allowedSlots = normalizeAllowedSlots(c.allowedSlots || foundry.utils.getProperty(it.flags, `${VB_MOD}.component.allowedSlots`));
// defaults
c.use = { space: 0, power: 0, ...(c.use || {}) };
c.mods = { armour: 0, integrity: 0, manoeuvre: 0, detection: 0, speed: 0, space: 0, power: 0, ...(c.mods || {}) };
components.push(c);
if (c.equipped) {
// sumuj mody
for (const k of Object.keys(mods)) {
const val = Number(c.mods[k] || 0);
if (Number.isFinite(val)) mods[k] += val;
}
// sumuj zużycie/produkcję
const us = Number(c.use.space || 0);
const up = Number(c.use.power || 0);
if (Number.isFinite(us)) useTotal.space += us;
if (Number.isFinite(up)) useTotal.power += up;
// slot usage (tylko jeśli to broń z legalnym slotem)
if (VB_WEAPON_SLOTS.includes(c.slot)) {
slotsUsed[c.slot] = (slotsUsed[c.slot] || 0) + 1;
}
}
}
const base = actor.getFlag(VB_MOD, "base") || { armour:0, integrity:0, manoeuvre:0, detection:0, speed:0, space:0, power:0 };
// total = base + mods
const totals = {
armour: (Number(base.armour)||0) + mods.armour,
integrity: (Number(base.integrity)||0) + mods.integrity,
manoeuvre: (Number(base.manoeuvre)||0) + mods.manoeuvre,
detection: (Number(base.detection)||0) + mods.detection,
speed: (Number(base.speed)||0) + mods.speed,
space: (Number(base.space)||0) + mods.space,
power: (Number(base.power)||0) + mods.power
};
totals.spaceFree = totals.space - useTotal.space;
totals.powerFree = totals.power - useTotal.power;
// warnings
const hull = actor.getFlag(VB_MOD, "hull") || { slots: {} };
const warnings = {};
for (const s of VB_WEAPON_SLOTS) {
if ((slotsUsed[s]||0) > (hull.slots?.[s]||0)) { warnings.slots = "One or more weapon slot groups exceed hull limits."; break; }
}
if (totals.spaceFree < 0) warnings.capacity = "Not enough Space for installed components.";
if (totals.powerFree < 0) warnings.capacity = "Not enough Power for installed components.";
// zaszyj
data.hull = hull;
data.base = base;
data.components = components;
data.slotsUsed = slotsUsed;
data.totals = totals;
data.mods = mods;
data.useTotal = useTotal;
// extra debug
data.debug = {
usedWeaponSlots: slotsUsed,
itemsCount: items.length,
componentsCount: components.length,
itemsNoVSH: items.filter(i => !getVSH(i)).map(i => ({id:i.id, name:i.name}))
};
return data;
}
// ---- listeners & UI ----
activateListeners(html) {
super.activateListeners(html);
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.actor.getFlag(VB_MOD, "base") || {};
const content = `
<form>
<div class="form-group"><label>Armour</label><input type="number" name="armour" value="${Number(cur.armour || 0)}"/></div>
<div class="form-group"><label>Integrity</label><input type="number" name="integrity" value="${Number(cur.integrity || 0)}"/></div>
<div class="form-group"><label>Manoeuvre</label><input type="number" name="manoeuvre" value="${Number(cur.manoeuvre || 0)}"/></div>
<div class="form-group"><label>Detection</label><input type="number" name="detection" value="${Number(cur.detection || 0)}"/></div>
<div class="form-group"><label>Speed</label><input type="number" name="speed" value="${Number(cur.speed || 0)}"/></div>
<div class="form-group"><label>Space</label><input type="number" name="space" value="${Number(cur.space || 0)}"/></div>
<div class="form-group"><label>Power</label><input type="number" name="power" value="${Number(cur.power || 0)}"/></div>
</form>`;
new Dialog({
title: "Voidship — Base Stats",
content, buttons: {
ok: {
label: "Save",
callback: async (dlg) => {
const form = dlg[0].querySelector("form");
const num = (n) => Number(form.querySelector(`[name="${n}"]`)?.value || 0);
await this.actor.setFlag(VB_MOD, "base", {
armour: num("armour"), integrity: num("integrity"),
manoeuvre: num("manoeuvre"), detection: num("detection"),
speed: num("speed"), space: num("space"), power: num("power")
});
this.render(true);
}
},
cancel: { label: "Cancel" }
}, default: "ok"
}, { jQuery: true }).render(true);
});
// Components controls
html.find("[data-action='comp-toggle']").on("click", async (ev) => {
const row = ev.currentTarget.closest("tr");
const id = row?.dataset?.itemId;
const item = this.actor.items.get(id);
if (!item) return;
const equipped = !(item.system?.equipped ?? true);
await item.update({ "system.equipped": equipped });
this.render(true);
});
html.find("[data-action='comp-edit']").on("click", async (ev) => {
const row = ev.currentTarget.closest("tr");
const id = row?.dataset?.itemId;
const item = this.actor.items.get(id);
if (item) item.sheet?.render(true);
});
html.find("[data-action='comp-remove']").on("click", async (ev) => {
const row = ev.currentTarget.closest("tr");
const id = row?.dataset?.itemId;
if (!id) return;
await this.actor.deleteEmbeddedDocuments("Item", [id]);
this.render(true);
});
}
async _onDropItem(event, data) {
event.preventDefault();
// 1) Rozwiąż źródło (uuid/pack/data)
let doc = null;
try {
if (data?.uuid) doc = await fromUuid(data.uuid);
else if (data?.pack && data?._id) {
const pack = game.packs.get(data.pack);
if (pack) doc = await pack.getDocument(data._id);
} else if (data) {
doc = await Item.implementation.fromDropData(data);
}
} catch (e) { /* ignore */ }
const raw = doc?.toObject?.(false) ?? data?.data ?? data;
if (!raw?.type) { ui.notifications.error("Drop failed: cannot resolve item data (no UUID/DATA)."); return false; }
// 2) Czy to komponent?
const flagsIn = foundry.utils.getProperty(raw, `flags.${VB_MOD}`) || {};
const legacySys = foundry.utils.getProperty(raw, "system.voidship") || null;
const hasCompTag = !!flagsIn.component;
const hasLegacy = !!legacySys;
if (!hasCompTag && !hasLegacy) {
ui.notifications.warn("This item is not a voidship component.");
return false;
}
// 3) Skonstruuj flags.${VB_MOD}.voidship (kanoniczne)
const vshIn = flagsIn.voidship || legacySys || {};
const flagsOut = foundry.utils.duplicate(flagsIn);
flagsOut.voidship = foundry.utils.mergeObject({
slot: vshIn.slot || null,
use: { space: 0, power: 0, ...(vshIn.use || {}) },
mods: { armour: 0, integrity: 0, manoeuvre: 0, detection: 0, speed: 0, space: 0, power: 0, ...(vshIn.mods || {}) },
allowedSlots: normalizeAllowedSlots(vshIn.allowedSlots || flagsIn?.component?.allowedSlots || [])
}, {}, { inplace: false });
// 4) Auto-przydział slotu dla broni (jeśli allowedSlots != pusty)
const allowed = flagsOut.voidship.allowedSlots || [];
if (allowed.length > 0) {
const hull = this.actor.getFlag(VB_MOD, "hull") || {};
const hullSlots = hull.slots || {};
const used = countUsedWeaponSlots(this.actor);
const chosen = pickFirstFreeSlot(allowed, hullSlots, used);
if (!chosen) {
ui.notifications.warn("No free matching weapon slot for this component.");
return false;
}
flagsOut.voidship.slot = chosen;
} else {
// nie-broń
flagsOut.voidship.slot = flagsOut.voidship.slot || "Internal";
}
// 5) wpisz flags i domyślne "equipped"
raw.flags = raw.flags || {};
raw.flags[VB_MOD] = flagsOut;
if (raw.system?.equipped === undefined) {
raw.system = raw.system || {};
raw.system.equipped = true;
}
// 6) Utwórz na aktorze
await this.actor.createEmbeddedDocuments("Item", [raw]);
this.render(true);
return true;
}
}
// Register the sheet
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`);
});