131 lines
4.7 KiB
Python
131 lines
4.7 KiB
Python
#!/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())
|