sync: wg-voidships-builder v1.4.9

This commit is contained in:
2025-09-19 15:59:30 +02:00
parent c11e029b92
commit 20345f1605
4 changed files with 288 additions and 130 deletions
+97 -71
View File
@@ -4,6 +4,47 @@
const VB_MOD = "wg-voidships-builder";
const VB_DEBUG = true; // Debug on
function dbg(...args) { if (VB_DEBUG) console.debug("[VB]", ...args); }
// --- SLOT HELPERS: normalizacja i przydział "pierwszy wolny pasujący" ---
const VB_WEAPON_SLOTS = ["prow", "dorsal", "port", "starboard", "keel"];
function normalizeSlotName(s) {
const v = String(s || "").trim().toLowerCase();
return VB_WEAPON_SLOTS.includes(v) ? v : null;
}
function normalizeAllowedSlots(allowed) {
if (!allowed) return [];
if (Array.isArray(allowed)) return allowed.map(normalizeSlotName).filter(Boolean);
// string "prow|dorsal|port" albo "prow,dorsal"
return String(allowed)
.split(/[|,; ]+/g)
.map(normalizeSlotName)
.filter(Boolean);
}
function countUsedWeaponSlots(actor) {
const used = { prow: 0, dorsal: 0, port: 0, starboard: 0, keel: 0 };
for (const it of actor.items ?? []) {
const vs = it.system?.voidship;
if (!vs) continue;
const slot = normalizeSlotName(vs.slot);
const eq = (it.system?.equipped ?? true);
if (eq && slot && (slot in used)) used[slot] += 1;
}
return used;
}
function pickFirstFreeSlot(allowed, hullSlots, used) {
// Kolejność zgodnie z wymaganiem: "pierwszy wolny pasujący"
const order = ["prow", "dorsal", "port", "starboard", "keel"];
for (const s of order) {
if (!allowed.includes(s)) continue;
const cap = Number(hullSlots?.[s] ?? 0);
const u = Number(used?.[s] ?? 0);
if (u < cap) return s;
}
return null;
}
// ---------------- Handlebars helpers ----------------
Hooks.once("init", () => {
@@ -311,88 +352,73 @@ async _onDrop(event) {
if (raw?.type === "Item") return this._onDropItem(event, raw);
dbg("drop.ignored", raw);
}
/** Drop itemów na arkusz: tylko komponenty voidship.
* - Jeżeli item ma allowedSlots (broń): przydziel "pierwszy wolny pasujący" i zapisz do system.voidship.slot
* - Jeżeli nie ma allowedSlots: zachowaj istniejący system.voidship.slot (albo slot "Internal" dla nie-broni)
*/
async _onDropItem(event, data) {
const VB_MOD = "wg-voidships-builder";
dbg("drop.item.raw", foundry.utils.deepClone(data));
event.preventDefault();
// 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;
// 1) Rozwiąż źródło dropu (UUID/pack/data) możliwie bezpiecznie
let doc = null;
try {
if (data?.uuid) doc = await fromUuid(data.uuid);
else if (data?.pack && data?._id) {
const pack = game.packs.get(data.pack);
if (pack) doc = await pack.getDocument(data._id);
} else if (data?.data || data?.type) {
// Foundry potrafi zrzucać raw "data"
doc = await Item.implementation.fromDropData(data);
}
}
if (!itemData) { ui.notifications.error("Drop failed: no item data."); return; }
} catch (e) { /* fall-through */ }
// 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);
// 2) Wyprowadź "raw" do stworzenia osadzonego itemu
const raw = doc?.toObject?.(false) ?? data?.data ?? data;
if (!raw?.system) {
ui.notifications.error("Drop failed: cannot resolve item data (no UUID/DATA).");
return false;
}
// 3) Sprawdź, czy to komponent voidship (wymagamy system.voidship)
const isComponent = !!raw.system?.voidship || !!raw.flags?.["wg-voidships-builder"]?.component;
if (!isComponent) {
ui.notifications.warn("This item is not a voidship component.");
dbg("notComponent", { name: itemData.name, flags: itemData.flags });
return;
return false;
}
// 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";
};
// 4) Upewnij się, że istnieje sekcja voidship
raw.system.voidship = raw.system.voidship || {};
const vsh = raw.system.voidship;
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)
// 5) Wyciągnij allowedSlots (preferuj z system.voidship.allowedSlots; ewentualnie z flags.component.allowedSlots)
const fromFlags = foundry.utils.getProperty(raw, "flags.wg-voidships-builder.component.allowedSlots");
const allowed = normalizeAllowedSlots(vsh.allowedSlots ?? fromFlags);
// 6) Jeżeli to broń (ma ograniczenia slotów), wybierz pierwszy wolny pasujący i wpisz do vsh.slot.
if (allowed.length > 0) {
const hull = this.actor.getFlag("wg-voidships-builder", "hull") || {};
const hullSlots = hull.slots || {};
const used = countUsedWeaponSlots(this.actor);
const chosen = pickFirstFreeSlot(allowed, hullSlots, used);
if (!chosen) {
ui.notifications.warn("No free matching weapon slot for this component.");
return false;
}
};
// 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);
vsh.slot = chosen; // przypisz ostateczny slot montażu
} else {
// Nie-broń: zostaw istniejący slot albo nadaj "Internal"
vsh.slot = vsh.slot || "Internal";
}
// 7) Domyślnie traktuj jako "equipped" (zamontowane), jeśli schema na to pozwala
if (raw.system.equipped === undefined) raw.system.equipped = true;
// 8) Utwórz item na aktorze
await this.actor.createEmbeddedDocuments("Item", [raw]);
// 9) Przelicz i odśwież
this.render(true);
return true;
}