#!/usr/bin/env python3 """ Reclassify unit_type and assign faction for all units in public/resources/units/*.json. Schema target after this pass: - unit_type ∈ {melee, ranged, siege, support, civilian, summoned} (drops naval/air; those live in `domain`) - faction ∈ {player, wild, freepeople, summoned} (new orthogonal axis) Rules (deterministic, applied top-down): 1. unit_type=='support' → keep support 2. unit_type in {'wild','npc'} → see faction logic; classify combat role from kw/range 3. has 'siege' keyword → siege 4. range >= 1 OR 'ranged' kw → ranged 5. otherwise → melee Faction (in-game allegiance, orthogonal to race_required): - existing 'faction' field wins (e.g. dwarf_wanderer already has freepeople) - 'wild' keyword OR was unit_type=='wild' → wild - was unit_type=='npc' → freepeople (sole current case is dwarf_wanderer) - else → dwarf (Game 1: only player-controlled race) """ from __future__ import annotations import argparse import json import sys from collections import Counter from pathlib import Path REPO = Path(__file__).resolve().parents[1] UNITS_DIR = REPO / "public" / "resources" / "units" def classify(unit: dict) -> tuple[str, str]: kw = unit.get("keywords") or [] rng = unit.get("range") or 0 old_type = unit.get("unit_type") or "" # faction faction = unit.get("faction") if not faction: if "wild" in kw or old_type == "wild": faction = "wild" elif old_type == "npc": faction = "freepeople" else: faction = "dwarf" # unit_type — combat role if old_type == "support": new_type = "support" elif old_type == "siege" or "siege" in kw: # Preserve any unit already authored as siege (some lack the keyword, # e.g. dwarf_steam_bomber / dwarf_war_zeppelin / dwarf_sky_fortress). new_type = "siege" elif rng >= 1 or "ranged" in kw: new_type = "ranged" elif (unit.get("attack") or 0) == 0 and rng == 0 and old_type != "wild": # Non-combatants without an explicit support type → classify as support new_type = "support" else: new_type = "melee" return new_type, faction def process(path: Path, *, dry_run: bool) -> list[tuple[str, str, str, str, str]]: """Returns list of (id, old_type, new_type, old_faction, new_faction) for changed entries.""" data = json.loads(path.read_text()) items = data if isinstance(data, list) else [data] if not items or not isinstance(items[0], dict) or not items[0].get("id"): return [] changed = [] for it in items: if not it.get("id"): continue old_type = it.get("unit_type") or "" old_faction = it.get("faction") or "" new_type, new_faction = classify(it) if new_type != old_type or new_faction != old_faction: changed.append((it["id"], old_type, new_type, old_faction, new_faction)) it["unit_type"] = new_type it["faction"] = new_faction if changed and not dry_run: path.write_text(json.dumps(items if isinstance(data, list) else items[0], indent=2) + "\n") return changed def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--apply", action="store_true", help="write changes (default: dry run)") args = ap.parse_args() if not UNITS_DIR.is_dir(): print(f"missing dir: {UNITS_DIR}", file=sys.stderr) return 2 type_changes: Counter[tuple[str, str]] = Counter() faction_changes: Counter[tuple[str, str]] = Counter() all_changes = [] for fp in sorted(UNITS_DIR.glob("*.json")): if fp.name.endswith(".schema.json"): continue for ch in process(fp, dry_run=not args.apply): all_changes.append((fp.name, *ch)) type_changes[(ch[1], ch[2])] += 1 faction_changes[(ch[3], ch[4])] += 1 print(f"{'APPLIED' if args.apply else 'DRY RUN'}: {len(all_changes)} unit(s) changed\n") print("=== unit_type transitions ===") for (a, b), n in sorted(type_changes.items(), key=lambda x: -x[1]): print(f" {a or '(none)':10s} → {b:10s} {n}") print("\n=== faction transitions ===") for (a, b), n in sorted(faction_changes.items(), key=lambda x: -x[1]): print(f" {a or '(none)':10s} → {b:10s} {n}") print("\n=== first 20 row-level changes ===") for row in all_changes[:20]: fn, uid, ot, nt, of, nf = row print(f" {uid:30s} type {ot or '-':8s}->{nt:8s} faction {of or '-':10s}->{nf}") if len(all_changes) > 20: print(f" ... and {len(all_changes) - 20} more") return 0 if __name__ == "__main__": sys.exit(main())