This commit is contained in:
2025-09-18 09:12:46 +02:00
parent e8ad547844
commit 5892e6148d
3 changed files with 266 additions and 1 deletions
+8 -1
View File
@@ -5,7 +5,14 @@
"version": "1.4.8", "version": "1.4.8",
"compatibility": { "minimum": "12", "verified": "13" }, "compatibility": { "minimum": "12", "verified": "13" },
"authors": [{ "name": "Gizmo & GPT-5 Thinking" }], "authors": [{ "name": "Gizmo & GPT-5 Thinking" }],
"esmodules": ["scripts/voidships.js"], "scripts": [
"scripts/voidships.js",
"scripts/voidship-actor.js"
],
"templates": [
"templates/voidship-sheet.hbs"
],
"packs": [ "packs": [
{ "label": "W&G Voidship Hulls (Empty)", "name": "wg-voidship-hulls", "path": "packs/wg-voidship-hulls.db", "type": "Actor", "system": "wrath-and-glory" }, { "label": "W&G Voidship Hulls (Empty)", "name": "wg-voidship-hulls", "path": "packs/wg-voidship-hulls.db", "type": "Actor", "system": "wrath-and-glory" },
{ "label": "W&G Voidship Items", "name": "wg-voidship-items", "path": "packs/wg-voidship-items.db", "type": "Item", "system": "wrath-and-glory" }, { "label": "W&G Voidship Items", "name": "wg-voidship-items", "path": "packs/wg-voidship-items.db", "type": "Item", "system": "wrath-and-glory" },
@@ -0,0 +1,165 @@
// W&G Voidships Builder — Voidship Actor Sheet (vehicle+flag emulation)
// v1.0.0 (safe MVP) — works on Foundry v11v13
/* eslint-env es2021 */
const VB_MOD = "wg-voidships-builder";
class VoidshipSheet extends ActorSheet {
static get defaultOptions() {
const opts = super.defaultOptions;
return foundry.utils.mergeObject(opts, {
classes: ["wg-voidships", "sheet", "actor", "voidship"],
template: `modules/${VB_MOD}/templates/voidship-sheet.hbs`,
width: 720,
height: 620,
tabs: [{ navSelector: ".tabs", contentSelector: ".sheet-body", initial: "stats" }]
});
}
/** Prefer to use this sheet only for vehicles marked flagiem voidship */
_canUserView(user) {
return !!user;
}
get isVoidship() {
return this.actor?.getFlag(VB_MOD, "isVoidship") === true;
}
/** Aggregate base + mods from items into derived summary (safe, optional) */
_computeDerived() {
const a = this.actor;
const base = {
armour: Number(a.getFlag(VB_MOD, "base.armour")) || 0,
integrity: Number(a.getFlag(VB_MOD, "base.integrity")) || 0,
manoeuvre: Number(a.getFlag(VB_MOD, "base.manoeuvre")) || 0,
detection: Number(a.getFlag(VB_MOD, "base.detection")) || 0,
speed: Number(a.getFlag(VB_MOD, "base.speed")) || 0,
space: Number(a.getFlag(VB_MOD, "base.space")) || 0,
power: Number(a.getFlag(VB_MOD, "base.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); // treat as mounted unless explicitly off
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 && slots.hasOwnProperty(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
};
// Headroom (positive = free)
total.spaceFree = total.space;
total.powerFree = total.power;
// If components encode their usage (voidship.use.{space|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);
}
return { base, mods, total, slots };
}
async getData(options={}) {
const data = await super.getData(options);
data.isVoidship = this.isVoidship;
data.voidship = this._computeDerived();
// expose a minimal config (labels)
data.cfg = {
labels: {
armour: "Armour",
integrity: "Integrity",
manoeuvre: "Manoeuvre",
detection: "Detection",
speed: "Speed",
space: "Space",
power: "Power",
spaceFree: "Space (free)",
powerFree: "Power (free)"
}
};
return data;
}
activateListeners(html) {
super.activateListeners(html);
// Guard: allow sheet UI even for non-voidship, but prompt to mark
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._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 (html) => {
const form = html[0].querySelector("form") || html[0];
const get = (k) => Number(form.querySelector(`[name="${k}"]`)?.value || 0);
const patch = {
"base.armour": get("armour"),
"base.integrity": get("integrity"),
"base.manoeuvre": get("manoeuvre"),
"base.detection": get("detection"),
"base.speed": get("speed"),
"base.space": get("space"),
"base.power": get("power")
};
for (const [k,v] of Object.entries(patch)) await this.actor.setFlag(VB_MOD, k, v);
this.render(true);
}
},
cancel: { label: "Cancel" }
},
default: "ok"
}, { jQuery: true }).render(true);
});
}
}
Hooks.once("init", () => {
// Register the sheet for the 'vehicle' type (W&G Vehicles via Church of Steel)
Actors.registerSheet(VB_MOD, VoidshipSheet, {
types: ["vehicle"],
makeDefault: false,
label: "Voidship (W&G)"
});
// Optional: a convenient factory on game namespace
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`);
});
@@ -0,0 +1,93 @@
<form class="voidship-sheet">
<header class="sheet-header flexrow" style="align-items:center; gap:.5rem;">
<img class="profile" src="{{actor.img}}" width="48" height="48"/>
<h1 class="charname"><input name="name" type="text" value="{{actor.name}}"/></h1>
<div style="margin-left:auto; display:flex; gap:.5rem;">
{{#if isVoidship}}
<button type="button" data-action="configure-base"><i class="fas fa-cog"></i> Base</button>
<button type="button" data-action="unmark-voidship"><i class="fas fa-times-circle"></i> Unmark</button>
{{else}}
<button type="button" data-action="mark-voidship"><i class="fas fa-star"></i> Mark as Voidship</button>
{{/if}}
</div>
</header>
{{#unless isVoidship}}
<section class="warning" style="margin:.5rem 0; padding:.5rem; background:#531a1a; color:#fff; border:1px solid #aa4444;">
This vehicle is not marked as a <b>Voidship</b>. Click <b>Mark as Voidship</b> to enable derived voidship stats.
</section>
{{/unless}}
<nav class="tabs" data-group="primary">
<a class="item" data-tab="stats">Stats</a>
<a class="item" data-tab="components">Components</a>
</nav>
<section class="sheet-body">
<div class="tab" data-group="primary" data-tab="stats">
<div class="grid grid-2col" style="gap:.5rem;">
<div class="card">
<h3>Totals</h3>
<ol class="properties">
<li>{{cfg.labels.armour}}: <b>{{voidship.total.armour}}</b></li>
<li>{{cfg.labels.integrity}}: <b>{{voidship.total.integrity}}</b></li>
<li>{{cfg.labels.manoeuvre}}: <b>{{voidship.total.manoeuvre}}</b></li>
<li>{{cfg.labels.detection}}: <b>{{voidship.total.detection}}</b></li>
<li>{{cfg.labels.speed}}: <b>{{voidship.total.speed}}</b></li>
<li>{{cfg.labels.spaceFree}}: <b>{{voidship.total.spaceFree}}</b></li>
<li>{{cfg.labels.powerFree}}: <b>{{voidship.total.powerFree}}</b></li>
</ol>
</div>
<div class="card">
<h3>Slots</h3>
<ol class="properties">
<li>Prow: <b>{{voidship.slots.prow}}</b></li>
<li>Dorsal: <b>{{voidship.slots.dorsal}}</b></li>
<li>Port: <b>{{voidship.slots.port}}</b></li>
<li>Starboard: <b>{{voidship.slots.starboard}}</b></li>
<li>Keel: <b>{{voidship.slots.keel}}</b></li>
<li>Other: <b>{{voidship.slots.other}}</b></li>
</ol>
</div>
</div>
<hr/>
<div class="grid grid-2col" style="gap:.5rem;">
<div class="card">
<h3>Base (flags)</h3>
<ol class="properties">
<li>{{cfg.labels.armour}}: <b>{{voidship.base.armour}}</b></li>
<li>{{cfg.labels.integrity}}: <b>{{voidship.base.integrity}}</b></li>
<li>{{cfg.labels.manoeuvre}}: <b>{{voidship.base.manoeuvre}}</b></li>
<li>{{cfg.labels.detection}}: <b>{{voidship.base.detection}}</b></li>
<li>{{cfg.labels.speed}}: <b>{{voidship.base.speed}}</b></li>
<li>{{cfg.labels.space}}: <b>{{voidship.base.space}}</b></li>
<li>{{cfg.labels.power}}: <b>{{voidship.base.power}}</b></li>
</ol>
</div>
<div class="card">
<h3>Mods (items)</h3>
<ol class="properties">
<li>{{cfg.labels.armour}}: <b>{{voidship.mods.armour}}</b></li>
<li>{{cfg.labels.integrity}}: <b>{{voidship.mods.integrity}}</b></li>
<li>{{cfg.labels.manoeuvre}}: <b>{{voidship.mods.manoeuvre}}</b></li>
<li>{{cfg.labels.detection}}: <b>{{voidship.mods.detection}}</b></li>
<li>{{cfg.labels.speed}}: <b>{{voidship.mods.speed}}</b></li>
<li>{{cfg.labels.space}}: <b>{{voidship.mods.space}}</b></li>
<li>{{cfg.labels.power}}: <b>{{voidship.mods.power}}</b></li>
</ol>
</div>
</div>
</div>
<div class="tab" data-group="primary" data-tab="components">
<section>
<p>Mounted items are read from the Actor's inventory. For best results, put per-component data in <code>item.system.voidship</code>:</p>
<pre>{
"slot": "Prow|Dorsal|Port|Starboard|Keel",
"mods": { "armour":0, "integrity":0, "manoeuvre":0, "detection":0, "speed":0, "space":0, "power":0 },
"use": { "space":0, "power":0 }
}</pre>
</section>
</div>
</section>
</form>