mirror of
https://github.com/migatu/vtt_work.git
synced 2026-07-14 13:34:42 +00:00
sync: wg-voidships-builder v1.4.9
This commit is contained in:
@@ -2,9 +2,16 @@
|
||||
"id": "wg-voidships-builder",
|
||||
"title": "Wrath & Glory: Voidships — Builder",
|
||||
"description": "Składanie okrętów jako Actorów przez dodawanie wyposażenia (gear) z kompendium.",
|
||||
"version": "1.4.8",
|
||||
"compatibility": { "minimum": "12", "verified": "13" },
|
||||
"authors": [{ "name": "Gizmo & GPT-5 Thinking" }],
|
||||
"version": "1.4.9",
|
||||
"compatibility": {
|
||||
"minimum": "12",
|
||||
"verified": "13"
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "Gizmo & GPT-5 Thinking"
|
||||
}
|
||||
],
|
||||
"scripts": [
|
||||
"scripts/voidships.js",
|
||||
"scripts/voidship-actor.js"
|
||||
@@ -12,15 +19,47 @@
|
||||
"templates": [
|
||||
"templates/voidship-sheet.hbs"
|
||||
],
|
||||
|
||||
"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 Items", "name": "wg-voidship-items", "path": "packs/wg-voidship-items.db", "type": "Item", "system": "wrath-and-glory" },
|
||||
{ "label": "W&G Voidships — Tables", "name": "wg-voidships-tables", "path": "packs/wg-voidships-tables.db", "type": "RollTable" },
|
||||
{ "label": "W&G Voidship Macros", "name": "wg-voidship-macros", "path": "packs/wg-voidship-macros.db", "type": "Macro" },
|
||||
|
||||
{ "label": "W&G Voidship Archeotech", "name": "wg-voidship-archeotech", "path": "packs/wg-voidship-archeotech.db", "type": "Item", "system": "wrath-and-glory" },
|
||||
{ "label": "W&G Voidship Xenos", "name": "wg-voidship-xenos", "path": "packs/wg-voidship-xenos.db", "type": "Item", "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 Voidships — Tables",
|
||||
"name": "wg-voidships-tables",
|
||||
"path": "packs/wg-voidships-tables.db",
|
||||
"type": "RollTable"
|
||||
},
|
||||
{
|
||||
"label": "W&G Voidship Macros",
|
||||
"name": "wg-voidship-macros",
|
||||
"path": "packs/wg-voidship-macros.db",
|
||||
"type": "Macro"
|
||||
},
|
||||
{
|
||||
"label": "W&G Voidship Archeotech",
|
||||
"name": "wg-voidship-archeotech",
|
||||
"path": "packs/wg-voidship-archeotech.db",
|
||||
"type": "Item",
|
||||
"system": "wrath-and-glory"
|
||||
},
|
||||
{
|
||||
"label": "W&G Voidship Xenos",
|
||||
"name": "wg-voidship-xenos",
|
||||
"path": "packs/wg-voidship-xenos.db",
|
||||
"type": "Item",
|
||||
"system": "wrath-and-glory"
|
||||
}
|
||||
],
|
||||
"packFolders": [
|
||||
{
|
||||
@@ -37,7 +76,13 @@
|
||||
"folders": []
|
||||
}
|
||||
],
|
||||
"relationships": { "systems": [{ "id": "wrath-and-glory" }] },
|
||||
"relationships": {
|
||||
"systems": [
|
||||
{
|
||||
"id": "wrath-and-glory"
|
||||
}
|
||||
]
|
||||
},
|
||||
"readme": "journal/README.md",
|
||||
"license": "LICENSE"
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
{"_id":"HULLPLATE0000001","name":"Reinforced Hull Plating","type":"gear","img":"icons/svg/armor.svg","flags":{"wg-voidships-builder":{"component":{"kind":"armour","subtype":"plating","tags":["hull","armour","internal"]},"voidOnly":true,"voidship":{"slot":"Internal","use":{"space":1,"power":0},"mods":{"armour":2,"integrity":4,"manoeuvre":-1,"detection":0,"speed":0,"space":0,"power":0}}}},"system":{"quantity":1,"rarity":"Common","description":"Additional armour plates and bracing.","value":0,"keywords":[],"effect":"","test":"","voidship":{"use":{},"mods":{}}},"effects":[]}
|
||||
{"_id":"CARGOHOLD0000001","name":"Cargo Hold & Lighter Bay","type":"gear","img":"icons/svg/crate.svg","flags":{"wg-voidships-builder":{"component":{"kind":"cargo-bay","subtype":"standard","tags":["cargo-bay","internal"]},"voidOnly":true,"voidship":{"slot":"Internal","use":{"space":-5,"power":1},"mods":{"armour":0,"integrity":0,"manoeuvre":0,"detection":0,"speed":0,"space":-5,"power":0}}}},"system":{"quantity":1,"rarity":"Common","description":"General-purpose cargo hold with lighter handling.","value":0,"keywords":[],"effect":"","test":"","voidship":{"use":{},"mods":{}}},"effects":[]}
|
||||
{"_id":"AUXPLASMA0000001","name":"Auxiliary Plasma Generator","type":"gear","img":"icons/svg/energy.svg","flags":{"wg-voidships-builder":{"component":{"kind":"plasma","subtype":"aux-generator","tags":["aux-plasma","internal"]},"voidOnly":true,"voidship":{"slot":"Internal","use":{"space":2,"power":-6},"mods":{"armour":0,"integrity":0,"manoeuvre":0,"detection":0,"speed":0,"space":0,"power":6}}}},"system":{"quantity":1,"rarity":"Common","description":"Auxiliary plasma reactor increasing available power.","value":0,"keywords":[],"effect":"","test":"","voidship":{"use":{"power":0},"mods":{}}},"effects":[]}
|
||||
{"_id":"MACROPROW0000001","name":"Macrobattery (Prow)","type":"gear","img":"icons/svg/sword.svg","flags":{"wg-voidships-builder":{"component":{"kind":"weapon","subtype":"macrobattery","tags":["weapon","prow"]},"voidOnly":true,"voidship":{"slot":"Prow","use":{"space":2,"power":2},"mods":{"armour":0,"integrity":0,"manoeuvre":0,"detection":0,"speed":0,"space":0,"power":0}}}},"system":{"quantity":1,"rarity":"Common","description":"Prow macrobattery.","value":0,"keywords":[],"effect":"","test":"","voidship":{"use":{},"mods":{}}},"effects":[]}
|
||||
{"_id":"MACRODORS0000001","name":"Macrobattery (Dorsal)","type":"gear","img":"icons/svg/sword.svg","flags":{"wg-voidships-builder":{"component":{"kind":"weapon","subtype":"macrobattery","tags":["weapon","dorsal"]},"voidOnly":true,"voidship":{"slot":"Dorsal","use":{"space":2,"power":2},"mods":{"armour":0,"integrity":0,"manoeuvre":0,"detection":0,"speed":0,"space":0,"power":0}}}},"system":{"quantity":1,"rarity":"Common","description":"Dorsal macrobattery.","value":0,"keywords":[],"effect":"","test":"","voidship":{"use":{},"mods":{}}},"effects":[]}
|
||||
{"_id":"ITEMREACTCRU0001","name":"Plasma Reactor (Cruiser) — Mars-Pattern","type":"gear","img":"icons/svg/energy.svg","flags":{"wg-voidships-builder":{"voidOnly":true,"component":{"kind":"reactor","subtype":"cruiser","tags":["reactor","plasma"]}}},"system":{"description":"Standardowy reaktor plazmowy klasy krążownik (Mars-Pattern). Generuje moc dla wszystkich systemów okrętu.","voidship":{"slot":"Internal","use":{"space":20,"power":0},"mods":{"armour":0,"integrity":0,"manoeuvre":0,"detection":0,"speed":0,"space":0,"power":60}}},"effects":[]}
|
||||
{"_id":"ITEMREACTLCR0001","name":"Plasma Reactor (Light Cruiser) — Mars-Pattern","type":"gear","img":"icons/svg/energy.svg","flags":{"wg-voidships-builder":{"voidOnly":true,"component":{"kind":"reactor","subtype":"light-cruiser","tags":["reactor","plasma"]}}},"system":{"description":"Reaktor plazmowy dla lekkich krążowników (Mars-Pattern).","voidship":{"slot":"Internal","use":{"space":18,"power":0},"mods":{"armour":0,"integrity":0,"manoeuvre":0,"detection":0,"speed":0,"space":0,"power":55}}},"effects":[]}
|
||||
{"_id":"ITEMREACTFRI0001","name":"Plasma Reactor (Frigate) — Jovian-Pattern","type":"gear","img":"icons/svg/energy.svg","flags":{"wg-voidships-builder":{"voidOnly":true,"component":{"kind":"reactor","subtype":"frigate","tags":["reactor","plasma"]}}},"system":{"description":"Reaktor plazmowy okrętów klasy fregata (Jovian-Pattern).","voidship":{"slot":"Internal","use":{"space":15,"power":0},"mods":{"armour":0,"integrity":0,"manoeuvre":0,"detection":0,"speed":0,"space":0,"power":45}}},"effects":[]}
|
||||
{"_id":"ITEMREACTESC0001","name":"Plasma Reactor (Escort) — Jovian-Pattern","type":"gear","img":"icons/svg/energy.svg","flags":{"wg-voidships-builder":{"voidOnly":true,"component":{"kind":"reactor","subtype":"escort","tags":["reactor","plasma"]}}},"system":{"description":"Lżejszy reaktor plazmowy dla eskort.","voidship":{"slot":"Internal","use":{"space":12,"power":0},"mods":{"armour":0,"integrity":0,"manoeuvre":0,"detection":0,"speed":0,"space":0,"power":40}}},"effects":[]}
|
||||
{"_id":"ITEMMACROUNI0001","name":"Macrobattery (Universal) — Mars-Pattern","type":"gear","img":"icons/svg/dice-target.svg","flags":{"wg-voidships-builder":{"voidOnly":true,"component":{"kind":"weapon","subtype":"macro","tags":["weapon","macro"]}}},"system":{"description":"Uniwersalna makrobateria. Może być montowana na dowolnym stanowisku broni.","voidship":{"allowedSlots":["prow","dorsal","port","starboard"],"use":{"space":8,"power":4},"mods":{"armour":0,"integrity":0,"manoeuvre":0,"detection":0,"speed":0,"space":0,"power":0}}},"effects":[]}
|
||||
{"_id":"ITEMWPMACBRD0001","name":"Macrobattery (Broadside) — Mars-Pattern","type":"gear","img":"icons/svg/dice-target.svg","flags":{"wg-voidships-builder":{"voidOnly":true,"component":{"kind":"weapon","subtype":"macro-broadside","tags":["weapon","macro"]}}},"system":{"description":"Makrobateria burtowa. Montaż wyłącznie na burtach.","voidship":{"allowedSlots":["port","starboard"],"use":{"space":9,"power":4},"mods":{"armour":0,"integrity":0,"manoeuvre":0,"detection":0,"speed":0,"space":0,"power":0}}},"effects":[]}
|
||||
{"_id":"ITEMTORPPROW0001","name":"Torpedo Tubes — Jovian-Pattern","type":"gear","img":"icons/svg/anchor.svg","flags":{"wg-voidships-builder":{"voidOnly":true,"component":{"kind":"weapon","subtype":"torpedo","tags":["weapon","torpedo"]}}},"system":{"description":"Wyrzutnie torpedowe montowane w dziobie.","voidship":{"allowedSlots":["prow"],"use":{"space":10,"power":5},"mods":{"armour":0,"integrity":0,"manoeuvre":0,"detection":0,"speed":0,"space":0,"power":0}}},"effects":[]}
|
||||
{"_id":"ITEMLANCEDSL0001","name":"Lance Battery — Starflare-Pattern","type":"gear","img":"icons/svg/sun.svg","flags":{"wg-voidships-builder":{"voidOnly":true,"component":{"kind":"weapon","subtype":"lance","tags":["weapon","lance"]}}},"system":{"description":"Bateria lanc. Preferowane stanowiska: dorsal/port/starboard.","voidship":{"allowedSlots":["dorsal","port","starboard"],"use":{"space":10,"power":6},"mods":{"armour":0,"integrity":0,"manoeuvre":0,"detection":0,"speed":0,"space":0,"power":0}}},"effects":[]}
|
||||
|
||||
@@ -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) {
|
||||
// 1) Rozwiąż źródło dropu (UUID/pack/data) możliwie bezpiecznie
|
||||
let doc = null;
|
||||
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;
|
||||
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;
|
||||
}
|
||||
vsh.slot = chosen; // przypisz ostateczny slot montażu
|
||||
} else {
|
||||
// Nie-broń: zostaw istniejący slot albo nadaj "Internal"
|
||||
vsh.slot = vsh.slot || "Internal";
|
||||
}
|
||||
};
|
||||
|
||||
// 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) Domyślnie traktuj jako "equipped" (zamontowane), jeśli schema na to pozwala
|
||||
if (raw.system.equipped === undefined) raw.system.equipped = true;
|
||||
|
||||
// 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 })));
|
||||
// 8) Utwórz item na aktorze
|
||||
await this.actor.createEmbeddedDocuments("Item", [raw]);
|
||||
// 9) Przelicz i odśwież
|
||||
this.render(true);
|
||||
} catch (e) {
|
||||
ui.notifications.error("Failed to add component to this voidship (see console).");
|
||||
dbg("drop.create.error", e);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
sync_foundry_module.py (wersja z dry-run + argumentami CLI)
|
||||
sync_foundry_module.py (z dry-run, argumentami CLI i preflightem git/module.json)
|
||||
|
||||
Funkcje:
|
||||
1) Znajduje ...\AppData\(Local|LocalLow|Roaming)\FoundryVTT\Data\modules dla podanego usera
|
||||
(lub używa pełnego override).
|
||||
2) Czyści zawartość wskazanego modułu (z potwierdzeniem lub bez; w dry-run tylko listuje).
|
||||
3) Kopiuje zawartość z SOURCE_BASE_DIR\<MODULE_SUBDIR_NAME> (w dry-run tylko plan).
|
||||
Kroki:
|
||||
[PRE] W katalogu źródłowym:
|
||||
- Wczytaj module.json, pobierz 'id' i 'version'
|
||||
- Zwiększ ostatnią liczbę w 'version' o 1, zapisz module.json
|
||||
- git add -A; git commit -m "sync: <id> v<version>"; git push
|
||||
(w dry-run wyłącznie plan, bez modyfikacji i bez gita)
|
||||
|
||||
Argumenty CLI nadpisują stałe z sekcji KONFIGURACJA.
|
||||
[2] Wyczyść zawartość docelowego modułu (z potwierdzeniem lub bez; w dry-run tylko lista)
|
||||
[3] Skopiuj zawartość źródła do celu (w dry-run: plan)
|
||||
|
||||
Wymagania: Python 3.8+, git w PATH, uprawnienia do zapisu.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -17,6 +21,8 @@ import os
|
||||
import sys
|
||||
import shutil
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
@@ -27,11 +33,11 @@ from typing import List, Optional, Tuple
|
||||
TARGET_USER: str = "mtuszowski" # nazwa folderu w C:\Users\...
|
||||
APPDATA_KIND: Optional[str] = None # None | "Local" | "LocalLow" | "Roaming"
|
||||
TARGET_MODULES_DIR_OVERRIDE: Optional[str] = None # pełna ścieżka do ...\FoundryVTT\Data\modules
|
||||
MODULE_SUBDIR_NAME: str = "wg-voidships"
|
||||
MODULE_SUBDIR_NAME: str = "wg-voidships-builder"
|
||||
CONFIRM_DELETE: bool = True
|
||||
SOURCE_BASE_DIR: str = r"C:\vtt_work"
|
||||
RELATIVE_PATTERN: str = r"FoundryVTT\Data\modules"
|
||||
DRY_RUN: bool = False # <- nowa stała
|
||||
DRY_RUN: bool = False # globalny tryb symulacji (nic nie zmienia)
|
||||
|
||||
# ==========================
|
||||
# KONIEC KONFIGURACJI
|
||||
@@ -104,14 +110,12 @@ def ensure_dir(path: Path) -> None:
|
||||
|
||||
|
||||
def iter_dir_contents(dir_path: Path) -> List[Path]:
|
||||
"""Zwraca listę *bez* samego katalogu: wszystkie pliki/katalogi pierwszego poziomu."""
|
||||
if not dir_path.exists():
|
||||
return []
|
||||
return list(dir_path.iterdir())
|
||||
|
||||
|
||||
def list_recursive(dir_path: Path) -> List[Path]:
|
||||
"""Rekurencyjna lista wszystkich elementów wewnątrz katalogu (głębiej niż 1)."""
|
||||
items: List[Path] = []
|
||||
if not dir_path.exists():
|
||||
return items
|
||||
@@ -125,15 +129,9 @@ def list_recursive(dir_path: Path) -> List[Path]:
|
||||
|
||||
|
||||
def clear_dir_contents(dir_path: Path, dry_run: bool) -> Tuple[int, int]:
|
||||
"""
|
||||
Usuwa zawartość katalogu (pozostawia go). W dry_run tylko listuje.
|
||||
Zwraca (liczba_plików, liczba_katalogów) do usunięcia/usuniętych.
|
||||
"""
|
||||
ensure_dir(dir_path)
|
||||
files = 0
|
||||
dirs = 0
|
||||
|
||||
# Zbierz listę do raportu
|
||||
to_delete = iter_dir_contents(dir_path)
|
||||
|
||||
if dry_run:
|
||||
@@ -143,20 +141,13 @@ def clear_dir_contents(dir_path: Path, dry_run: bool) -> Tuple[int, int]:
|
||||
for child in to_delete:
|
||||
tag = "[plik]" if child.is_file() else "[katalog]" if child.is_dir() else "[inne]"
|
||||
print(f" {tag} {norm(child)}")
|
||||
# policz rekurencyjnie dla katalogów
|
||||
for child in to_delete:
|
||||
if child.is_file() or child.is_symlink():
|
||||
files += 1
|
||||
elif child.is_dir():
|
||||
# policz wszystkie elementy w środku informacyjnie
|
||||
nested = list_recursive(child)
|
||||
dirs += 1
|
||||
for n in nested:
|
||||
# nie drukujemy każdego pliku zagnieżdżonego — wystarczy powyższa lista top-level
|
||||
pass
|
||||
return files, dirs
|
||||
|
||||
# tryb wykonawczy
|
||||
for child in to_delete:
|
||||
if child.is_file() or child.is_symlink():
|
||||
files += 1
|
||||
@@ -168,10 +159,6 @@ def clear_dir_contents(dir_path: Path, dry_run: bool) -> Tuple[int, int]:
|
||||
|
||||
|
||||
def plan_copy(src: Path, dst: Path) -> List[Tuple[Path, Path, str]]:
|
||||
"""
|
||||
Tworzy plan (źródło, cel, status) dla wszystkich elementów w src.
|
||||
Status: "overwrite" jeśli cel istnieje (plik/katalog), inaczej "create".
|
||||
"""
|
||||
plan: List[Tuple[Path, Path, str]] = []
|
||||
if not src.is_dir():
|
||||
raise FileNotFoundError(f"Katalog źródłowy nie istnieje: {norm(src)}")
|
||||
@@ -179,13 +166,11 @@ def plan_copy(src: Path, dst: Path) -> List[Tuple[Path, Path, str]]:
|
||||
for root, dirs, files in os.walk(src):
|
||||
root_p = Path(root)
|
||||
rel_root = root_p.relative_to(src)
|
||||
# katalogi
|
||||
for d in dirs:
|
||||
s = root_p / d
|
||||
t = dst / rel_root / d
|
||||
status = "overwrite" if t.exists() else "create"
|
||||
plan.append((s, t, status))
|
||||
# pliki
|
||||
for f in files:
|
||||
s = root_p / f
|
||||
t = dst / rel_root / f
|
||||
@@ -195,10 +180,6 @@ def plan_copy(src: Path, dst: Path) -> List[Tuple[Path, Path, str]]:
|
||||
|
||||
|
||||
def copy_dir_contents(src: Path, dst: Path, dry_run: bool) -> Tuple[int, int]:
|
||||
"""
|
||||
Kopiuje zawartość katalogu src do dst. W dry_run tylko listuje plan.
|
||||
Zwraca (liczba_plików, liczba_katalogów) skopiowanych/planowanych.
|
||||
"""
|
||||
ensure_dir(dst)
|
||||
plan = plan_copy(src, dst)
|
||||
|
||||
@@ -213,7 +194,6 @@ def copy_dir_contents(src: Path, dst: Path, dry_run: bool) -> Tuple[int, int]:
|
||||
dirs = sum(1 for s, _, _ in plan if s.is_dir())
|
||||
return files, dirs
|
||||
|
||||
# wykonanie
|
||||
files = 0
|
||||
dirs = 0
|
||||
for s, t, _ in plan:
|
||||
@@ -238,8 +218,109 @@ def copy_dir_contents(src: Path, dst: Path, dry_run: bool) -> Tuple[int, int]:
|
||||
return files, dirs
|
||||
|
||||
|
||||
# ============ PRE-FLIGHT: module.json + git ============
|
||||
|
||||
def read_manifest(manifest_path: Path) -> dict:
|
||||
if not manifest_path.is_file():
|
||||
raise FileNotFoundError(f"Brak pliku manifestu: {norm(manifest_path)}")
|
||||
try:
|
||||
with manifest_path.open("r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except json.JSONDecodeError as e:
|
||||
raise ValueError(f"Niepoprawny JSON w {norm(manifest_path)}: {e}") from e
|
||||
|
||||
|
||||
def bump_version(last_version: str) -> str:
|
||||
"""
|
||||
Zwiększa o 1 ostatnią *liczbę* w wersji rozdzielanej kropkami.
|
||||
Przykłady: 1 -> 2, 1.2 -> 1.3, 1.2.9 -> 1.2.10
|
||||
"""
|
||||
parts = last_version.strip().split(".")
|
||||
if not parts:
|
||||
raise ValueError(f"Niepoprawna wersja: '{last_version}'")
|
||||
try:
|
||||
n = int(parts[-1])
|
||||
except ValueError:
|
||||
raise ValueError(f"Ostatni segment wersji nie jest liczbą: '{last_version}'")
|
||||
parts[-1] = str(n + 1)
|
||||
return ".".join(parts)
|
||||
|
||||
|
||||
def write_manifest(manifest_path: Path, data: dict) -> None:
|
||||
tmp = manifest_path.with_suffix(".json.tmp")
|
||||
with tmp.open("w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
f.write("\n")
|
||||
tmp.replace(manifest_path) # atomowa podmiana na tym samym FS
|
||||
|
||||
|
||||
def run_git_commands(repo_dir: Path, commit_msg: str, dry_run: bool) -> None:
|
||||
"""
|
||||
Wykonuje: git add -A; git commit -m "<msg>"; git push
|
||||
W dry_run — tylko wyświetla plan.
|
||||
"""
|
||||
if dry_run:
|
||||
print("[DRY-RUN] git add -A")
|
||||
print(f"[DRY-RUN] git commit -m {commit_msg!r}")
|
||||
print("[DRY-RUN] git push")
|
||||
return
|
||||
|
||||
def run(cmd: List[str]) -> None:
|
||||
r = subprocess.run(cmd, cwd=str(repo_dir), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError(f"Błąd podczas uruchamiania: {' '.join(cmd)}\nWyjście:\n{r.stdout}")
|
||||
else:
|
||||
if r.stdout.strip():
|
||||
print(r.stdout)
|
||||
|
||||
# add, commit (może nie utworzyć commita jeśli brak zmian), push
|
||||
run(["git", "add", "-A"])
|
||||
# commit powinien się powieść — jeśli nie ma zmian, chcemy FAIL
|
||||
r = subprocess.run(["git", "commit", "-m", commit_msg], cwd=str(repo_dir),
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||
if r.returncode != 0:
|
||||
# typowy komunikat: "nothing to commit, working tree clean" -> traktujemy jako błąd wg wymagań
|
||||
raise RuntimeError(f"Błąd podczas git commit:\n{r.stdout}")
|
||||
else:
|
||||
if r.stdout.strip():
|
||||
print(r.stdout)
|
||||
run(["git", "push"])
|
||||
|
||||
|
||||
def preflight_source(src_dir: Path, dry_run: bool) -> Tuple[str, str]:
|
||||
"""
|
||||
Wczytuje module.json, bumpuje wersję, zapisuje (jeśli nie dry_run),
|
||||
i wykonuje git add/commit/push. Zwraca (module_id, new_version).
|
||||
"""
|
||||
manifest_path = src_dir / "module.json"
|
||||
manifest = read_manifest(manifest_path)
|
||||
|
||||
if "id" not in manifest or "version" not in manifest:
|
||||
raise KeyError(f"W {norm(manifest_path)} brakuje pola 'id' lub 'version'.")
|
||||
|
||||
module_id = str(manifest["id"])
|
||||
old_version = str(manifest["version"])
|
||||
new_version = bump_version(old_version)
|
||||
|
||||
print(f"[PRE] module.json: id={module_id}, version={old_version} -> {new_version}")
|
||||
|
||||
if dry_run:
|
||||
print("[DRY-RUN] Nie zapisuję module.json i nie wykonuję git.")
|
||||
else:
|
||||
manifest["version"] = new_version
|
||||
write_manifest(manifest_path, manifest)
|
||||
print(f"[PRE] Zapisano zaktualizowany {norm(manifest_path)}")
|
||||
|
||||
commit_msg = f"sync: {module_id} v{new_version}"
|
||||
run_git_commands(src_dir, commit_msg, dry_run=False)
|
||||
|
||||
return module_id, new_version
|
||||
|
||||
|
||||
# ================= CLI / MAIN =================
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
p = argparse.ArgumentParser(description="Synchronizacja modułu FoundryVTT (z dry-run).")
|
||||
p = argparse.ArgumentParser(description="Synchronizacja modułu FoundryVTT (preflight: module.json + git).")
|
||||
p.add_argument("--user", dest="user", default=TARGET_USER,
|
||||
help=f"Nazwa użytkownika Windows (domyślnie: {TARGET_USER})")
|
||||
p.add_argument("--appdata-kind", dest="appdata_kind", choices=APPDATA_VARIANTS + (None,),
|
||||
@@ -251,7 +332,7 @@ def parse_args() -> argparse.Namespace:
|
||||
p.add_argument("--module-subdir-name", dest="module_subdir_name",
|
||||
default=MODULE_SUBDIR_NAME,
|
||||
help=f"Nazwa podkatalogu modułu (domyślnie: {MODULE_SUBDIR_NAME})")
|
||||
# confirm-delete jako przełącznik dwu-stanowy
|
||||
|
||||
cd_group = p.add_mutually_exclusive_group()
|
||||
cd_group.add_argument("--confirm-delete", dest="confirm_delete", action="store_true",
|
||||
help="Włącz potwierdzenie kasowania (jeśli było wyłączone).")
|
||||
@@ -266,12 +347,11 @@ def parse_args() -> argparse.Namespace:
|
||||
default=RELATIVE_PATTERN,
|
||||
help=f"Wzorzec relatywny od AppData (domyślnie: {RELATIVE_PATTERN})")
|
||||
|
||||
# dry-run jako przełącznik dwu-stanowy
|
||||
dr_group = p.add_mutually_exclusive_group()
|
||||
dr_group.add_argument("--dry-run", dest="dry_run", action="store_true",
|
||||
help="Tryb symulacji: tylko listuje co by zrobił.")
|
||||
help="Tryb symulacji: tylko plan (bez modyfikacji i bez gita).")
|
||||
dr_group.add_argument("--no-dry-run", dest="dry_run", action="store_false",
|
||||
help="Tryb wykonawczy: wprowadza zmiany na dysku.")
|
||||
help="Tryb wykonawczy: wprowadza zmiany i robi git.")
|
||||
p.set_defaults(dry_run=DRY_RUN)
|
||||
|
||||
return p.parse_args()
|
||||
@@ -289,7 +369,14 @@ def main() -> int:
|
||||
relative_pattern = args.relative_pattern
|
||||
dry_run = args.dry_run
|
||||
|
||||
# 1) Ustal katalog modules
|
||||
# Ścieżki
|
||||
src_dir = Path(source_base) / module_subdir
|
||||
|
||||
# [PRE] module.json + git w katalogu źródłowym
|
||||
print(f"[PRE] Katalog źródłowy: {norm(src_dir)}")
|
||||
preflight_source(src_dir, dry_run=dry_run)
|
||||
|
||||
# 1) Ustal katalog modules (cel)
|
||||
if modules_override:
|
||||
modules_dir = Path(modules_override)
|
||||
if not modules_dir.is_dir():
|
||||
@@ -302,7 +389,7 @@ def main() -> int:
|
||||
target_module_dir = modules_dir / module_subdir
|
||||
print(f"[INFO] Docelowy katalog modułu: {norm(target_module_dir)}")
|
||||
|
||||
# 2) Kasowanie zawartości (albo plan kasowania w dry-run)
|
||||
# 2) Kasowanie zawartości (albo plan w dry-run)
|
||||
if confirm_delete and not dry_run:
|
||||
ans = input(f"Czy na pewno wyczyścić zawartość '{norm(target_module_dir)}'? [y/N]: ").strip().lower()
|
||||
if ans not in ("y", "yes", "t", "tak"):
|
||||
@@ -315,9 +402,7 @@ def main() -> int:
|
||||
del_files, del_dirs = clear_dir_contents(target_module_dir, dry_run=dry_run)
|
||||
print(f"[INFO] {'Do usunięcia' if dry_run else 'Usunięto'}: plików={del_files}, katalogów={del_dirs}")
|
||||
|
||||
# 3) Kopiowanie (albo plan kopiowania w dry-run)
|
||||
src_dir = Path(source_base) / module_subdir
|
||||
print(f"[INFO] Katalog źródłowy: {norm(src_dir)}")
|
||||
# 3) Kopiowanie (albo plan kopiowania)
|
||||
print("[AKCJA] Kopiowanie zawartości..." if not dry_run else "[SYMULACJA] Plan kopiowania...")
|
||||
cp_files, cp_dirs = copy_dir_contents(src_dir, target_module_dir, dry_run=dry_run)
|
||||
print(f"[INFO] {'Do skopiowania' if dry_run else 'Skopiowano'}: plików={cp_files}, katalogów={cp_dirs}")
|
||||
|
||||
Reference in New Issue
Block a user