saumwacht-godot/tools/luneraria/build_luneraria_demo.py
Martin Stickelbroeck 9f62e30377 G2: Ancients, Türme, Bossgrotte und Superboss; macOS- und Windows-ARM64-Pakete
Mondherz-Ancients (Saumkern-Kristall als Herz, Sichelklingen, Ringe, Krone,
Teamwappen, Energieadern) mit den Stufen stabil/instabil/kritisch, kurzem
Zusammenbruch und bleibender Ruine; der Sieg wartet nie auf die Grafik.
Turmfamilie aus einem Blender-Generator: Grenzwacht (Tower 1), Bastion
(Tower 2), Kernwacht (Basisturm in der Formensprache des Ancients), mit
Schadensstufen, Einsturz, Ruine und sichtbarem Angriff (Zielring, Aufladen,
Bolzen, Einschlag) - der Schaden bleibt vollständig in der Simulation.
Bossgrotte Mondschlund mit Tor, Treppe in die Tiefe, Altar, Säulen, Felsen
und fünf Zuständen; der Generator hält Abstand zur Top-Lane, die Höhle
braucht eine kleine Geländesenke (nur Darstellung). Superboss Schlundkrone
mit 24 Knochen und 11 Clips (inkl. Brüllen und Sieg), Adern in Teamfarbe,
dasselbe Modell für Grotte und Lane. Gemeinsame LN-Materialfamilie (Stein,
Metall, Kristall, Rune, Stoff, Mondwasser, Kreaturenhaut), Occlusion-Fade
ohne dithernde Schatten. Keine Regeländerung.

Geprüft: verify_g2 245/245, verify_g1 443/443, Regeln 91/91 und 73/73,
Stress 17/17, Balance 40 Partien identisch mit G1, verify_m1 38/38,
verify_m2 14/14, Netz 43/43 und Loopback 12/12, Legacy 209 und 463.
21 Ansichten vorher/nachher (1920x1080), Videos Turmkampf und Bossbegegnung,
Leistung G1/G2 auf demselben Mac (+0,5 ms in vollen Kampfansichten).
Pakete 0.17.0-g2 für macOS (Universal, ad-hoc signiert), Windows x64/ARM64
und Linux; macOS nativ und unter Rosetta, beide Windows-Pakete in Windows 11
ARM (Parallels, OpenGL-Fallback) mit --lune-smoke bestanden.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 14:12:49 +02:00

156 lines
6.9 KiB
Python

#!/usr/bin/env python3
"""Build unpackable Champions of Luneraria demos for Linux, Windows (x64 and ARM64) and macOS.
Run: python3 tools/luneraria/build_luneraria_demo.py [--check-only] [--only macos,windows,...]
(on macOS without an accepted Xcode licence: /opt/homebrew/bin/python3.12 ...)
Godot: `godot` from PATH or the GODOT environment variable (4.7.2 with its export templates).
Output: ../dist/luneraria/ChampionsOfLuneraria-Linux-x64-<ver>.zip
../dist/luneraria/ChampionsOfLuneraria-Windows-x64-<ver>.zip
../dist/luneraria/ChampionsOfLuneraria-Windows-ARM64-<ver>.zip
../dist/luneraria/ChampionsOfLuneraria-macOS-universal-<ver>.zip (app bundle, ad-hoc signed)
Old Saumwacht releases live in ../_archiv/saumwacht-dist and stay untouched.
"""
from pathlib import Path
import argparse, hashlib, json, os, re, zipfile, subprocess, sys
PROJECT = Path(__file__).resolve().parents[2]
DIST = PROJECT.parent / "dist" / "luneraria"
# One source of truth: [luneraria] config/version in project.godot (also shown in the menu).
_match = re.search(r'^\[luneraria\][^\[]*?^config/version="([^"]+)"', (PROJECT / "project.godot").read_text(encoding="utf-8"), re.M | re.S)
VERSION = _match.group(1) if _match else "0.0.0-unbekannt"
GODOT = os.environ.get("GODOT", "godot")
INTRO = f"""CHAMPIONS OF LUNERARIA · PROTOTYP {VERSION}
START
macOS: Das ZIP entpacken und "Champions of Luneraria" starten. Die App ist nur ad-hoc
signiert (nicht notarisiert): beim ersten Start Rechtsklick > Öffnen, oder im
Terminal: xattr -dr com.apple.quarantine "Champions of Luneraria.app"
Universal: Apple Silicon und Intel, macOS 11 oder neuer.
Windows: ChampionsOfLuneraria.exe (x64; auf Windows 11 ARM das ARM64-Paket nutzen)
Linux: ./ChampionsOfLuneraria.x86_64
Grafikprobleme: --rendering-driver opengl3 (Windows/Linux)
Godot und Blender werden zum Spielen nicht benötigt. Das ganze ZIP zuerst entpacken.
INHALT
Drei Lanes, zwei Teams à drei Rollen, Partieende nur durch einen zerstörten Ancient.
Planung → automatischer Kampf → Abrechnung. Helden, Items, Angriffschampions und
die Bossgrotte „Mondschlund“ mit der Schlundkrone, die als Verstärkung entsandt wird.
G2: monumentale Ancients mit Zustandsstufen, Türme mit Angriff, Schaden und Ruinen.
NETZWERK (Direktverbindung, experimentell, pausiert)
Im Setup: Hosten oder Beitreten mit Adresse und Port (Standard 24571).
Der Host ist autoritativ. Freie Plätze werden von Bots gespielt.
PLATTFORMEN
Entwickelt und geprüft auf Fedora 44 (bis G1) und macOS 27 / Apple M5 (G2).
Windows 11 (ARM, Parallels) siehe docs/luneraria/G2-STRATEGISCHE-BAUWERKE.md.
QUELLEN
Doku im Quellrepo unter docs/luneraria/
"""
# preset, folder, binary (None = direct zip export), zip stem
PRESETS = [
("Luneraria-Linux", "linux", "ChampionsOfLuneraria.x86_64", "ChampionsOfLuneraria-Linux-x64"),
("Luneraria-Windows", "windows", "ChampionsOfLuneraria.exe", "ChampionsOfLuneraria-Windows-x64"),
("Luneraria-Windows-ARM64", "windows-arm64", "ChampionsOfLuneraria.exe", "ChampionsOfLuneraria-Windows-ARM64"),
("Luneraria-macOS", "macos", None, "ChampionsOfLuneraria-macOS-universal"),
]
def run_export(preset: str, target: Path) -> bool:
cmd = [GODOT, "--headless", "--path", str(PROJECT), "--export-release", preset, str(target)]
print("+", " ".join(cmd), flush=True)
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0 or not target.exists():
print(result.stdout[-2000:])
print(result.stderr[-2000:], file=sys.stderr)
return False
return True
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1 << 20), b""):
digest.update(block)
return digest.hexdigest()
def build_folder(preset: str, folder: str, binary: str, stem: str) -> Path | None:
out_dir = DIST / folder
out_dir.mkdir(parents=True, exist_ok=True)
if not run_export(preset, out_dir / binary):
return None
(out_dir / "LIESMICH.txt").write_text(INTRO, encoding="utf-8")
zip_path = DIST / f"{stem}-{VERSION}.zip"
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
for path in sorted(out_dir.iterdir()):
if path.suffix == ".zip":
continue
info = zipfile.ZipInfo.from_file(path, path.name)
if path.name == binary:
info.external_attr = 0o755 << 16
info.compress_type = zipfile.ZIP_DEFLATED
zf.writestr(info, path.read_bytes())
return zip_path
def build_macos(preset: str, stem: str) -> Path | None:
# Godot writes the app bundle straight into the zip (permissions and ad-hoc signature intact).
zip_path = DIST / f"{stem}-{VERSION}.zip"
if zip_path.exists():
zip_path.unlink()
if not run_export(preset, zip_path):
return None
with zipfile.ZipFile(zip_path, "a", zipfile.ZIP_DEFLATED) as zf:
zf.writestr("LIESMICH.txt", INTRO)
return zip_path
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--check-only", action="store_true")
parser.add_argument("--only", default="", help="comma separated folders: linux,windows,windows-arm64,macos")
args = parser.parse_args()
DIST.mkdir(parents=True, exist_ok=True)
# Fail early on missing templates / manifests instead of shipping a broken zip.
required = [
PROJECT / "assets-source/phase6/export-templates/linux_release.x86_64",
PROJECT / "assets-source/phase6/export-templates/windows_release_x86_64.exe",
PROJECT / "assets/luneraria/peoples_manifest.json",
PROJECT / "assets/luneraria/map_props_manifest.json",
PROJECT / "assets/luneraria/g2/g2_manifest.json",
PROJECT / "scenes/luneraria.tscn",
]
missing = [str(p) for p in required if not p.exists()]
if missing:
print("MISSING", missing)
return 1
if args.check_only:
print("CHECK_OK", VERSION)
return 0
only = [x for x in args.only.split(",") if x]
ok = True
built = {}
for preset, folder, binary, stem in PRESETS:
if only and folder not in only:
continue
artifact = build_macos(preset, stem) if binary is None else build_folder(preset, folder, binary, stem)
if artifact is None:
print("EXPORT_FAIL", preset)
ok = False
continue
built[artifact.name] = {"preset": preset, "bytes": artifact.stat().st_size, "sha256": sha256(artifact)}
print("PACK", artifact, artifact.stat().st_size, "bytes", flush=True)
manifest = DIST / "build-manifest.json"
data = json.loads(manifest.read_text()) if manifest.exists() else {}
data.setdefault("builds", {}).update(built)
data["version"] = VERSION
manifest.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n")
return 0 if ok else 1
if __name__ == "__main__":
raise SystemExit(main())