Files
vtt_work/wg-voidships-builder/scripts/voidship-actor.js
T
2025-09-19 12:59:10 +02:00

417 lines
16 KiB
JavaScript
Raw 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); }
// ---------------- 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 hull = actor.getFlag(VB_MOD, "hull") ?? flagsNs.hull ?? defaultHull;
const base = actor.getFlag(VB_MOD, "base") ?? flagsNs.base ?? defaultBase;
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: czytamy Z FLAG =====
const items = actor.items.contents;
const inferSlotFromTags = (tags=[]) => {
const low = (Array.isArray(tags)?tags:[]).map(t => String(t).toLowerCase());
const pref = ["prow","dorsal","port","starboard","keel","aetherics","internal"];
const hit = pref.find(p => low.includes(p));
return hit ? (hit[0].toUpperCase()+hit.slice(1)) : "Internal";
};
const components = [];
for (const i of items) {
const fmod = i.flags?.[VB_MOD] || {};
const vflag = fmod.voidship || null; // <-- TU trzymamy liczby
const meta = fmod.component || {}; // kind/subtype/tags
// Dopuszczamy też stare itemy z system.voidship (jeśli coś przetrwa)
const vsys = i.system?.voidship || null;
// komponent, jeśli ma vflag LUB meta.tags LUB vsys
const isComponent = !!(vflag || vsys || (Array.isArray(meta.tags) && meta.tags.length));
if (!isComponent) continue;
const vf = vflag || vsys || {};
const use = {
space: Number(vf?.use?.space ?? 0),
power: Number(vf?.use?.power ?? 0)
};
const mods = {
armour: Number(vf?.mods?.armour ?? 0),
integrity: Number(vf?.mods?.integrity ?? 0),
manoeuvre: Number(vf?.mods?.manoeuvre ?? 0),
detection: Number(vf?.mods?.detection ?? 0),
speed: Number(vf?.mods?.speed ?? 0),
space: Number(vf?.mods?.space ?? 0),
power: Number(vf?.mods?.power ?? 0)
};
let slot = vf.slot || meta.slot || inferSlotFromTags(meta.tags);
const equipped = (i.system?.equipped ?? true) && (vf.disabled !== true);
components.push({
itemId: i.id,
name: i.name,
slot,
equipped,
use, mods,
tags: meta.tags || []
});
}
// Zliczanie slotów (tylko equipped)
const slotsUsed = components.reduce((acc, c) => {
if (!c.equipped) return acc;
const key = String(c.slot || "Internal").toLowerCase();
if (["prow","dorsal","port","starboard","keel"].includes(key)) acc[key] = (acc[key] || 0) + 1;
return acc;
}, { prow: 0, dorsal: 0, port: 0, starboard: 0, keel: 0 });
// Capacity: bazowe + mody z komponentów (equipped) vs użycie (equipped)
const modsSum = components.reduce((acc, c) => {
if (!c.equipped) return acc;
for (const k of Object.keys(acc)) acc[k] += Number(c.mods[k] || 0);
return acc;
}, { armour:0, integrity:0, manoeuvre:0, detection:0, speed:0, space:0, power:0 });
const useSum = 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 });
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 - useSum.space;
totals.powerFree = totals.power - useSum.power;
// Ostrzeżenia
const warnings = {};
const overSlots =
(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 (overSlots) warnings.slots = "One or more weapon slot groups exceed hull limits.";
if (totals.spaceFree < 0 || totals.powerFree < 0)
warnings.capacity = "Not enough Space/Power for installed components.";
// wstrzyknij do template
data.hull = hull;
data.base = base;
data.crew = crew;
data.rtCard = rtCard;
data.components = components;
data.slotsUsed = slotsUsed;
data.totals = totals;
data.mods = modsSum; // ← DODANE: żeby HBS miał liczby modów
data.warnings = warnings;
dbg("sheet.getData", { base, hull, totals, slotsUsed, componentsCount: components.length });
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);
});
}
// --- robust drop handlers: flag-only storage (survives W&G) ---
async _onDrop(event) {
event.preventDefault();
let raw = {};
try { raw = JSON.parse(event.dataTransfer?.getData("text/plain") || "{}"); }
catch { ui.notifications.error("Drop failed: cannot parse drop payload."); return; }
if (raw?.type === "Item") return this._onDropItem(event, raw);
dbg("drop.ignored", raw);
}
async _onDropItem(event, data) {
const VB_MOD = "wg-voidships-builder";
dbg("drop.item.raw", foundry.utils.deepClone(data));
// 1) spróbuj fromUuid
let itemDoc = null;
if (data?.uuid) {
try { itemDoc = await fromUuid(data.uuid); } catch (e) { dbg("fromUuid.error", e); }
}
// 2) inline data
let itemData = itemDoc?.toObject?.() ?? data?.data ?? null;
// 3) ostatecznie spróbuj Foundry helpera
if (!itemData) {
try {
const i = await Item.implementation.fromDropData(data);
itemData = i?.toObject?.() ?? null;
} catch (e) {
ui.notifications.error("Drop failed: cannot resolve item data (no UUID/DATA).");
dbg("fromDropData.error", e, data); return;
}
}
if (!itemData) { ui.notifications.error("Drop failed: no item data."); return; }
// 4) Czy to komponent? akceptujemy po flagach lub po system.voidship
const fmod = itemData.flags?.[VB_MOD] || {};
const meta = fmod.component || {};
const vflag = fmod.voidship || null;
const vsys = itemData.system?.voidship || null;
const tags = Array.isArray(meta.tags) ? meta.tags : [];
const isComponent = !!(vflag || vsys || tags.length);
if (!isComponent) {
ui.notifications.warn("This item is not a voidship component.");
dbg("notComponent", { name: itemData.name, flags: itemData.flags });
return;
}
// 5) wyznacz slot + liczby (preferuj to co już jest w flagach; jeśli było tylko w system.voidship przenieś do flag)
const inferSlotFromTags = (tags=[]) => {
const low = (Array.isArray(tags)?tags:[]).map(t => String(t).toLowerCase());
const pref = ["prow","dorsal","port","starboard","keel","aetherics","internal"];
const hit = pref.find(p => low.includes(p));
return hit ? (hit[0].toUpperCase()+hit.slice(1)) : "Internal";
};
const vf = vflag || vsys || {};
const slot = vf.slot || meta.slot || inferSlotFromTags(meta.tags);
const vfixed = {
slot,
use: {
space: Number(vf?.use?.space ?? 0),
power: Number(vf?.use?.power ?? 0)
},
mods: {
armour: Number(vf?.mods?.armour ?? 0),
integrity: Number(vf?.mods?.integrity ?? 0),
manoeuvre: Number(vf?.mods?.manoeuvre ?? 0),
detection: Number(vf?.mods?.detection ?? 0),
speed: Number(vf?.mods?.speed ?? 0),
space: Number(vf?.mods?.space ?? 0),
power: Number(vf?.mods?.power ?? 0)
}
};
// 6) Nie dotykamy schema W&G zapisujemy w FLAGS
itemData.flags = foundry.utils.mergeObject(itemData.flags || {}, {
[VB_MOD]: foundry.utils.mergeObject(fmod, { voidship: vfixed }, { inplace: false })
}, { inplace: false });
// 7) dopnij equipped (żeby mody się naliczały od razu)
itemData.system = foundry.utils.mergeObject(itemData.system || {}, { equipped: true }, { inplace: false });
dbg("drop.finalized", { name: itemData.name, vfixed });
try {
const created = await this.actor.createEmbeddedDocuments("Item", [itemData]);
dbg("drop.created", created?.map(c => ({ id: c.id, name: c.name })));
this.render(true);
} catch (e) {
ui.notifications.error("Failed to add component to this voidship (see console).");
dbg("drop.create.error", e);
}
}
}
// 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`);
});