Files
vtt_work/wg-voidships-builder/scripts/voidship-actor.js
T
2025-09-18 18:32:48 +02:00

442 lines
19 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.0.1 — works on Foundry v11v13
/* 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 = `
<form>
<div class="form-group"><label>Armour</label><input type="number" name="armour" value="${cur.armour}"/></div>
<div class="form-group"><label>Integrity</label><input type="number" name="integrity" value="${cur.integrity}"/></div>
<div class="form-group"><label>Manoeuvre</label><input type="number" name="manoeuvre" value="${cur.manoeuvre}"/></div>
<div class="form-group"><label>Detection</label><input type="number" name="detection" value="${cur.detection}"/></div>
<div class="form-group"><label>Speed</label><input type="number" name="speed" value="${cur.speed}"/></div>
<div class="form-group"><label>Space</label><input type="number" name="space" value="${cur.space}"/></div>
<div class="form-group"><label>Power</label><input type="number" name="power" value="${cur.power}"/></div>
</form>`;
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 => `<option value="${s}" ${v.slot === s ? "selected" : ""}>${s}</option>`).join("");
const content = `
<form>
<div class="form-group"><label>Equipped</label><input type="checkbox" name="equipped" ${eq ? "checked" : ""}/></div>
<div class="form-group"><label>Slot</label><select name="slot">${slotOptions}</select></div>
<fieldset style="margin-top:.5rem;"><legend>Use</legend>
<div class="form-group"><label>Space</label><input type="number" step="1" name="use.space" value="${v.use.space}"/></div>
<div class="form-group"><label>Power</label><input type="number" step="1" name="use.power" value="${v.use.power}"/></div>
</fieldset>
<fieldset style="margin-top:.5rem;"><legend>Mods</legend>
${["armour", "integrity", "manoeuvre", "detection", "speed", "space", "power"].map(k => `
<div class="form-group"><label>${k[0].toUpperCase() + k.slice(1)}</label>
<input type="number" step="1" name="mods.${k}" value="${v.mods[k]}"/>
</div>`).join("")}
</fieldset>
</form>`;
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: `<p>Remove <b>${foundry.utils.escapeHTML(item.name)}</b> from this voidship?</p>`,
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`);
});