Vollständiger, engineunabhängiger Regelkern (Luneraria.Core, ohne UnityEngine): Partie mit sechs Plätzen und zwei Teams, private Planung mit atomarer Übernahme, Gold, Käufe, Aufwertungen, Formation, drei Lanes mit zwei Laneturmen und zwei Basistürmen je Team, Ancients, unbegrenzte Wellen, Helden mit Stufen, Items und Talenten, Angriffschampions, Bossgrotte mit Anspruch, Entsendung, Kopfgeld und dreirundiger Sperre, Bots und Siegbedingung (nur Ancient-Fall). Serverautoritative Befehlsschnittstelle. - Deterministisch: fester Takt, geordnete Maps, fortlaufende IDs, kein Zufall, keine Uhr, keine Threads - Tabellen einmal aus Godot erzeugt (tools/port/gen_content.py) - Golden-Referenzen aus Godot (tools/godot-golden): 19 Szenarien, 120 Snapshots, 51.221 Werte, Abweichung 0 (Toleranzen dokumentiert) - Tests: content 75, core 91, m2 73, golden 157, determinism 6 = 402/402 - Leistung (M5): Simulationsschritt 0,259 ms statt 1,587 ms in GDScript - Referenzfehler R1 im Bot-Gedächtnis bewusst übernommen und dokumentiert - Geplante Helden (Lucy, Ghaliga, Siegfried, Feena) im Datenmodell, inaktiv - Doku unter docs/port (U1-RULES, PORT-STATUS, PORT-QA, U2 bis U5) Unity-Editor-Lauf offen: Die Unity-Lizenz ist noch nicht aktiviert; gebaut und getestet mit dem .NET 8 SDK auf Unitys Sprachstand (C# 9, .NET Standard 2.1). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
158 lines
9.3 KiB
Python
158 lines
9.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Generates Assets/Luneraria/Core/Content/ContentData.cs from the Godot content export
|
|
(tools/godot-golden/content_export.gd). One-time bootstrap of the port: after generation the
|
|
C# file is the maintained source; a test compares it with the exported reference
|
|
(Assets/Luneraria/Tests/Core/Golden/content.json).
|
|
Usage: python3 tools/port/gen_content.py /tmp/lune_content.json
|
|
"""
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
OUT = ROOT / "Assets/Luneraria/Core/Content/ContentData.cs"
|
|
|
|
|
|
def s(value):
|
|
return '"' + str(value).replace("\\", "\\\\").replace('"', '\\"') + '"'
|
|
|
|
|
|
def d(value):
|
|
text = repr(float(value))
|
|
return text if ("." in text or "e" in text or "inf" in text) else text + ".0"
|
|
|
|
|
|
UNIT_FIELDS = [("id", "Id", s), ("display_name", "DisplayName", s), ("cost", "Cost", int), ("upgrade_id", "UpgradeId", s),
|
|
("max_hp", "MaxHp", d), ("damage", "Damage", d), ("attack_type", "AttackType", int), ("armor_type", "ArmorType", int),
|
|
("attack_range", "AttackRange", d), ("speed", "Speed", d), ("ability", "Ability", s), ("leak_value", "LeakValue", int),
|
|
("bounty", "Bounty", int), ("ability_description", "AbilityDescription", s), ("is_boss", "IsBoss", lambda v: "true" if v else "false"),
|
|
("income_gain", "IncomeGain", int)]
|
|
|
|
|
|
def unit_def(entry, indent):
|
|
parts = []
|
|
for key, name, fmt in UNIT_FIELDS:
|
|
if key in entry:
|
|
parts.append(f"{name} = {fmt(entry[key])}")
|
|
unknown = set(entry) - {k for k, _, _ in UNIT_FIELDS}
|
|
if unknown:
|
|
raise SystemExit(f"unknown unit keys {unknown}")
|
|
return "new UnitDef { " + ", ".join(parts) + " }"
|
|
|
|
|
|
def table(name, entries):
|
|
lines = [f" public static readonly OrderedMap<string, UnitDef> {name} = Map("]
|
|
rows = [f" {unit_def(entry, 12)}" for entry in entries.values()]
|
|
lines.append(",\n".join(rows))
|
|
lines.append(" );")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main():
|
|
data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
|
out = []
|
|
out.append("// <auto-generated once> by tools/port/gen_content.py from the Godot reference (luneraria/g3, 1b540f2).")
|
|
out.append("// Since the port this file is the maintained source of the rule tables; ContentTests compares it")
|
|
out.append("// with the exported reference in Assets/Luneraria/Tests/Core/Golden/content.json.")
|
|
out.append("using System.Collections.Generic;")
|
|
out.append("")
|
|
out.append("namespace Luneraria.Core")
|
|
out.append("{")
|
|
out.append(" /// <summary>Rule tables of Champions of Luneraria: troops, monsters, sends, heroes, items, champions, bots.</summary>")
|
|
out.append(" public static partial class ContentData")
|
|
out.append(" {")
|
|
out.append(table("Units", data["units"]) + "\n")
|
|
out.append(table("Enemies", data["enemies"]) + "\n")
|
|
out.append(table("Sends", data["sends"]) + "\n")
|
|
# Heroes
|
|
out.append(" public static readonly OrderedMap<string, HeroSpec> Heroes = Ordered(")
|
|
rows = []
|
|
for hero_id, hero in data["heroes"].items():
|
|
base = hero["def"]
|
|
rows.append(" new HeroSpec { " + ", ".join([f"Id = {s(hero_id)}", f"Name = {s(hero['name'])}", f"Title = {s(hero['title'])}",
|
|
f"Model = {s(hero['model'])}", f"Cost = {int(hero['cost'])}", f"RoleText = {s(hero['role'])}",
|
|
f"Provisional = {'true' if hero.get('provisional') else 'false'}", f"Summary = {s(hero['summary'])}",
|
|
"Base = " + unit_def(base, 16)]) + " }")
|
|
out.append(",\n".join(rows))
|
|
out.append(" );\n")
|
|
out.append(" public static readonly int[] LevelXp = { " + ", ".join(str(v) for v in data["level_xp"]) + " };")
|
|
out.append(" public static readonly int[] TalentLevels = { " + ", ".join(str(v) for v in data["talent_levels"]) + " };\n")
|
|
out.append(" /// <summary>Per hero: tiers of two mutually exclusive choices.</summary>")
|
|
out.append(" public static readonly Dictionary<string, TalentOption[][]> Talents = new Dictionary<string, TalentOption[][]>")
|
|
out.append(" {")
|
|
rows = []
|
|
for hero_id, tiers in data["talents"].items():
|
|
tier_rows = []
|
|
for tier in tiers:
|
|
options = ", ".join(f"new TalentOption({s(o['id'])}, {s(o['name'])}, {s(o['text'])})" for o in tier)
|
|
tier_rows.append(" new[] { " + options + " }")
|
|
rows.append(" { " + s(hero_id) + ", new[]\n {\n" + ",\n".join(tier_rows) + "\n } }")
|
|
out.append(",\n".join(rows))
|
|
out.append(" };\n")
|
|
# Items
|
|
out.append(" public static readonly OrderedMap<string, ItemSpec> Items = Ordered(")
|
|
rows = []
|
|
known = {"name", "cost", "text", "hp_flat", "damage", "reduction", "attack_speed", "regen", "splash", "min_level"}
|
|
for item_id, item in data["items"].items():
|
|
if set(item) - known:
|
|
raise SystemExit(f"unknown item keys {set(item) - known}")
|
|
parts = [f"Id = {s(item_id)}", f"Name = {s(item['name'])}", f"Cost = {int(item['cost'])}", f"Text = {s(item['text'])}"]
|
|
for key, name in [("hp_flat", "HpFlat"), ("damage", "Damage"), ("reduction", "Reduction"), ("attack_speed", "AttackSpeed"), ("regen", "Regen"), ("splash", "Splash")]:
|
|
if key in item:
|
|
parts.append(f"{name} = {d(item[key])}")
|
|
if "min_level" in item:
|
|
parts.append(f"MinLevel = {int(item['min_level'])}")
|
|
rows.append(" new ItemSpec { " + ", ".join(parts) + " }")
|
|
out.append(",\n".join(rows))
|
|
out.append(" );\n")
|
|
# Champions
|
|
out.append(" public static readonly OrderedMap<string, ChampionSpec> Champions = Ordered(")
|
|
rows = []
|
|
known = {"name", "role", "model", "cost", "base", "text", "priority", "siege_factor", "view_scale"}
|
|
for champion_id, spec in data["champions"].items():
|
|
if set(spec) - known:
|
|
raise SystemExit(f"unknown champion keys {set(spec) - known}")
|
|
parts = [f"Id = {s(champion_id)}", f"Name = {s(spec['name'])}", f"RoleText = {s(spec['role'])}", f"Model = {s(spec['model'])}",
|
|
f"Cost = {int(spec['cost'])}", f"Base = {s(spec['base'])}", f"Text = {s(spec['text'])}", f"ViewScale = {d(spec['view_scale'])}"]
|
|
if "priority" in spec:
|
|
parts.append(f"Priority = {s(spec['priority'])}")
|
|
if "siege_factor" in spec:
|
|
parts.append(f"SiegeFactor = {d(spec['siege_factor'])}")
|
|
rows.append(" new ChampionSpec { " + ", ".join(parts) + " }")
|
|
out.append(",\n".join(rows))
|
|
out.append(" );\n")
|
|
# Roles, rows, names
|
|
out.append(" /// <summary>Base troop id to role; iteration order matters for RoleOf (first match).</summary>")
|
|
out.append(" public static readonly OrderedMap<string, string> MatchRoles = Pairs(" + ", ".join(f"{s(k)}, {s(v)}" for k, v in data["match_roles"].items()) + ");")
|
|
out.append(" public static readonly OrderedMap<string, string> HeroRoles = Pairs(" + ", ".join(f"{s(k)}, {s(v)}" for k, v in data["hero_roles"].items()) + ");")
|
|
out.append(" public static readonly string[] RowOrder = { " + ", ".join(s(v) for v in data["row_order"]) + " };")
|
|
out.append(" public static readonly string[] BotNames = { " + ", ".join(s(v) for v in data["bot_names"]) + " };")
|
|
out.append(" public static readonly string[] WavesMelee = { " + ", ".join(s(v) for v in data["waves_melee"]) + " };")
|
|
out.append(" public static readonly string[] ArmorNames = { " + ", ".join(s(v) for v in data["armor_names"]) + " };")
|
|
out.append(" public static readonly string[] DamageNames = { " + ", ".join(s(v) for v in data["damage_names"]) + " };")
|
|
out.append(" public static readonly OrderedMap<string, string> FactionNames = Pairs(" + ", ".join(f"{s(k)}, {s(v)}" for k, v in data["faction_names"].items()) + ");\n")
|
|
# Bot profiles
|
|
out.append(" /// <summary>Bot profiles in the reference order (the seed rotation depends on it).</summary>")
|
|
out.append(" public static readonly OrderedMap<string, BotProfile> BotProfiles = Ordered(")
|
|
rows = []
|
|
for name, p in data["bot_profiles"].items():
|
|
parts = [f"Name = {s(name)}", f"Front = {d(p['front'])}", f"Melee = {d(p['melee'])}", f"Ranged = {d(p['ranged'])}",
|
|
f"Splash = {d(p['splash'])}", f"Support = {d(p['support'])}", f"UpgradeFrom = {int(p['upgrade_from'])}",
|
|
f"SupportLane = {d(p['support_lane'])}", f"Hero = {s(p['hero'])}", f"HeroRound = {int(p['hero_round'])}",
|
|
"Items = new[] { " + ", ".join(s(i) for i in p["items"]) + " }",
|
|
"TalentChoices = new[] { " + ", ".join(str(int(t)) for t in p["talents"]) + " }", f"Champion = {s(p['champion'])}"]
|
|
if "boss_from" in p:
|
|
parts.append(f"BossFrom = {int(p['boss_from'])}")
|
|
parts.append(f"BossTroops = {int(p['boss_troops'])}")
|
|
rows.append(" new BotProfile { " + ", ".join(parts) + " }")
|
|
out.append(",\n".join(rows))
|
|
out.append(" );")
|
|
out.append(" }")
|
|
out.append("}")
|
|
OUT.parent.mkdir(parents=True, exist_ok=True)
|
|
OUT.write_text("\n".join(out) + "\n", encoding="utf-8")
|
|
print("written", OUT, sum(1 for _ in OUT.open(encoding="utf-8")), "lines")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|