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

319 lines
12 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 (mapping weapons + core reqs + safer drops) — Foundry v11v13
/* eslint-env es2021 */
const VB_MOD = "wg-voidships-builder";
const VB_DEBUG = true; // debug on; switch later via setting
function dbg(...args) { if (VB_DEBUG) console.debug("[VB]", ...args); }
// Canonical slot keys
const SLOT_KEYS = ["prow","dorsal","port","starboard","keel"];
// Default RT-like core requirements (if hull.requirements is empty)
const DEFAULT_CORE_REQS = [
{ tag: "bridge", min: 1, label: "Bridge" },
{ tag: "life-support", min: 1, label: "Life Support" },
{ tag: "gellar", min: 1, label: "Gellar Field" },
{ tag: "warp-drive", min: 1, label: "Warp Drive" },
{ tag: "reactor", min: 1, label: "Plasma/Power Reactor" },
{ tag: "augur", min: 1, label: "Augur Array" },
{ tag: "void-shields", min: 1, label: "Void Shields" }
];
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; }
/** Sum flags.base + items mods + usage */
_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 usage = { space:0, power:0 };
const slotsUsed = { prow:0, dorsal:0, port:0, starboard:0, keel:0 };
for (const it of a.items.contents) {
const sys = it.system ?? {};
const v = sys.voidship ?? {};
const equipped = (sys.equipped ?? true) === true; // treat true/undefined as mounted
const comp = it.getFlag(VB_MOD, "component") || v.component || {}; // our kind/tags live in flags
if (equipped) {
// accumulate mods
const m = v.mods || {};
for (const k of Object.keys(mods)) {
const n = Number(m[k]);
if (Number.isFinite(n)) mods[k] += n;
}
// accumulate usage
const u = v.use || {};
if (Number.isFinite(Number(u.space))) usage.space += Number(u.space);
if (Number.isFinite(Number(u.power))) usage.power += Number(u.power);
// weapon slots count ONLY for kind === "weapon"
const kind = String(comp?.kind || "").toLowerCase();
const slot = String(v.slot || "").toLowerCase();
if (kind === "weapon" && SLOT_KEYS.includes(slot)) slotsUsed[slot] += 1;
}
}
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,
spaceFree: base.space + mods.space - usage.space,
powerFree: base.power + mods.power - usage.power
};
dbg("computeDerived:", { base, mods, usage, total, slotsUsed });
return { base, mods, usage, total, slotsUsed };
}
}
/** 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);
});
}
// pretty-json (for debug blocks)
if (!Handlebars.helpers.tojson) {
Handlebars.registerHelper("tojson", v => JSON.stringify(v, null, 2));
}
});
VoidshipSheet.prototype.getData = async function(options) {
const data = await super.getData(options);
const actor = this.actor;
const flags = actor.getFlag(VB_MOD, "") || {};
const hull = flags.hull || { class: actor.name, size: "Cruiser", slots: { prow:0,dorsal:0,port:0,starboard:0,keel:0 } };
const base = flags.base || { armour:0, integrity:0, manoeuvre:0, detection:0, speed:0, space:0, power:0 };
const crew = flags.crew || { command:1,pilot:1,sensors:1,engineering:2,gunnery:2,security:2,flightDeck:0,passengers:0 };
const rtRaw = flags["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 ?? ""
};
// components (only items having system.voidship)
const components = actor.items.contents
.filter(i => i.system?.voidship)
.map(i => {
const v = foundry.utils.deepClone(i.system.voidship || {});
const comp = i.getFlag(VB_MOD, "component") || v.component || {};
return {
itemId: i.id,
name: i.name,
equipped: (i.system?.equipped ?? true) === true,
slot: v.slot || "Internal",
use: Object.assign({ space:0, power:0 }, v.use),
mods: Object.assign({ armour:0, integrity:0, manoeuvre:0, detection:0, speed:0, space:0, power:0 }, v.mods),
component: { kind: String(comp.kind||"").toLowerCase(), tags: comp.tags || [], allowedSlots: comp.allowedSlots || [] }
};
});
// derived totals and slotsUsed (weapons only)
const { base:baseCalc, mods, usage, total, slotsUsed } = this._computeDerived();
// Warnings: slots over-capacity (compare ONLY weapons vs hull slot caps)
const warnings = {};
if (components.length) {
const over =
(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 (over) warnings.slots = "One or more weapon slot groups exceed hull limits.";
}
if (total.spaceFree < 0 || total.powerFree < 0) {
warnings.capacity = "Not enough Space/Power for installed components.";
}
// Validate allowedSlots for weapons (if provided)
const badMounts = components.filter(c =>
c.equipped &&
c.component?.kind === "weapon" &&
Array.isArray(c.component.allowedSlots) &&
c.component.allowedSlots.length &&
!c.component.allowedSlots.map(s => String(s).toLowerCase()).includes(String(c.slot).toLowerCase())
);
if (badMounts.length) {
warnings.mounts = `Invalid weapon placement: ${badMounts.map(b=>`${b.name}${b.slot}`).join(", ")}.`;
}
// Requirements — gather tags from equipped components
const tagCounts = {};
for (const c of components) {
if (!c.equipped) continue;
for (const t of (c.component?.tags || [])) {
const key = String(t).toLowerCase();
tagCounts[key] = (tagCounts[key] || 0) + 1;
}
}
const reqs = (Array.isArray(hull.requirements) && hull.requirements.length) ? hull.requirements : DEFAULT_CORE_REQS;
const missing = [];
for (const r of reqs) {
const need = Number(r.min||0);
if (!need) continue;
const have = tagCounts[String(r.tag||"").toLowerCase()] || 0;
if (have < need) missing.push(`${r.label||r.tag}: ${have}/${need}`);
}
if (missing.length) warnings.requirements = `Missing required systems — ${missing.join("; ")}`;
// expose to hbs
data.hull = hull;
data.base = base;
data.crew = crew;
data.mods = mods;
data.usage = usage;
data.totals = total;
data.slotsUsed = slotsUsed;
data.rtCard = rtCard;
data.components = components;
data.warnings = warnings;
data.debug = { flags, tagCounts, reqs }; // handy raw blocks
return data;
};
VoidshipSheet.prototype.activateListeners = function(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);
});
// configure base
html.find("[data-action='configure-base']").on("click", async () => {
const cur = this._computeDerived().base;
const content = `
<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>
`;
new Dialog({
title: "Voidship — Base Stats",
content, buttons: {
ok: { label: "Save", callback: async (dlg) => {
const form = dlg[0].querySelector("form") || dlg[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 raw debug blocks
html.find("[data-action='toggle-raw']").on("click", (ev) => {
const el = html.find(".vb-raw")[0];
if (el) el.style.display = (el.style.display === "none") ? "block" : "none";
});
};
// ACCEPT only items having system.voidship (and optionally flags[VB_MOD].component)
VoidshipSheet.prototype._onDropItem = async function(event, data) {
try {
const resolved = await Item.fromDropData(data);
if (!resolved) return ui.notifications.error("Drop failed: cannot resolve item (no UUID/DATA).");
const sys = resolved.system ?? {};
if (!sys.voidship) return ui.notifications.warn("This item is not a voidship component.");
// optional hard guard: if not our 'voidOnly' flag, still allow — but warn
const vFlags = resolved.getFlag(VB_MOD, "component");
if (!vFlags) dbg("Component missing flags; proceeding with system.voidship only.");
// pass to Foundry to actually create the embedded item
const created = await this.actor.createEmbeddedDocuments("Item", [resolved.toObject()]);
if (created?.length) {
ui.notifications.info(`Installed component: ${created[0].name}`);
this.render(true);
}
} catch (err) {
console.error(err);
ui.notifications.error("Drop failed: cannot resolve item data (no UUID/DATA).");
}
};
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`);
});