mirror of
https://github.com/migatu/vtt_work.git
synced 2026-07-14 21:38:39 +00:00
aaa
This commit is contained in:
@@ -1,10 +1,24 @@
|
||||
// W&G Voidships Builder — Voidship Actor Sheet (vehicle+flag emulation)
|
||||
// v1.0.1 — works on Foundry v11–v13
|
||||
// v1.1.0 (mapping weapons + core reqs + safer drops) — Foundry v11–v13
|
||||
/* eslint-env es2021 */
|
||||
const VB_MOD = "wg-voidships-builder";
|
||||
const VB_DEBUG = true; // debug; później zrobimy game.settings
|
||||
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;
|
||||
@@ -20,35 +34,49 @@ class VoidshipSheet extends ActorSheet {
|
||||
_canUserView(user) { return !!user; }
|
||||
get isVoidship() { return this.actor?.getFlag(VB_MOD, "isVoidship") === true; }
|
||||
|
||||
/** Prostszą agregację zostawiamy do walidacji ręcznej etc. */
|
||||
/** 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
|
||||
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) {
|
||||
|
||||
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 val = Number(m[k]);
|
||||
if (Number.isFinite(val)) mods[k] += val;
|
||||
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 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,
|
||||
@@ -56,341 +84,229 @@ class VoidshipSheet extends ActorSheet {
|
||||
detection: base.detection + mods.detection,
|
||||
speed: base.speed + mods.speed,
|
||||
space: base.space + mods.space,
|
||||
power: base.power + mods.power
|
||||
power: base.power + mods.power,
|
||||
spaceFree: base.space + mods.space - usage.space,
|
||||
powerFree: base.power + mods.power - usage.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 };
|
||||
dbg("computeDerived:", { base, mods, usage, total, slotsUsed });
|
||||
return { base, mods, usage, total, slotsUsed };
|
||||
}
|
||||
}
|
||||
|
||||
async getData(options) {
|
||||
const data = await super.getData(options);
|
||||
const actor = this.actor;
|
||||
/** 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));
|
||||
}
|
||||
});
|
||||
|
||||
// ——— 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 || {};
|
||||
VoidshipSheet.prototype.getData = async function(options) {
|
||||
const data = await super.getData(options);
|
||||
const actor = this.actor;
|
||||
|
||||
// ——— 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 ?? ""
|
||||
};
|
||||
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 || {};
|
||||
|
||||
// ——— 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;
|
||||
});
|
||||
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 ?? ""
|
||||
};
|
||||
|
||||
// ——— 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 });
|
||||
// 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 || [] }
|
||||
};
|
||||
});
|
||||
|
||||
// ——— 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 });
|
||||
// derived totals and slotsUsed (weapons only)
|
||||
const { base:baseCalc, mods, usage, total, slotsUsed } = this._computeDerived();
|
||||
|
||||
// ——— 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 =
|
||||
// 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 (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.";
|
||||
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.";
|
||||
}
|
||||
|
||||
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}`;
|
||||
// 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;
|
||||
}
|
||||
|
||||
// ——— 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);
|
||||
});
|
||||
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).");
|
||||
}
|
||||
};
|
||||
|
||||
// ————————————————————————————————————————————————————————————————
|
||||
// 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 });
|
||||
@@ -398,44 +314,5 @@ Hooks.once("init", () => {
|
||||
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`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user