#!/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 {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("// 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(" /// Rule tables of Champions of Luneraria: troops, monsters, sends, heroes, items, champions, bots.") 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 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(" /// Per hero: tiers of two mutually exclusive choices.") out.append(" public static readonly Dictionary Talents = new Dictionary") 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 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 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(" /// Base troop id to role; iteration order matters for RoleOf (first match).") out.append(" public static readonly OrderedMap MatchRoles = Pairs(" + ", ".join(f"{s(k)}, {s(v)}" for k, v in data["match_roles"].items()) + ");") out.append(" public static readonly OrderedMap 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 FactionNames = Pairs(" + ", ".join(f"{s(k)}, {s(v)}" for k, v in data["faction_names"].items()) + ");\n") # Bot profiles out.append(" /// Bot profiles in the reference order (the seed rotation depends on it).") out.append(" public static readonly OrderedMap 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()