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>
156 lines
6.9 KiB
Python
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())
|