Files
2026-06-17 11:45:47 +10:00

163 lines
5.9 KiB
Python

#!/usr/bin/env python3
"""
Extract canonical ghost/ability/set data from the source Ghost_Data.xlsx
into JSON seed files consumed by scripts/seed.js.
Pip strings (e.g. "◉◉◉◯◯") are converted to integers.
Rarity comes from the "✪..." section headers (1-4 stars).
Type comes from the "Red/Yellow/Blue Ghosts" section headers.
Boss ghosts are detected by abilities that appear in the Boss Ability table.
Set linkage (boss -> physical set) is a static reference map (factual data).
"""
import json
import sys
import openpyxl
FILLED = ""
def pips(s):
if not isinstance(s, str):
return 0
return s.count(FILLED)
def num(v):
if v in (None, "-", ""):
return None
try:
return int(v)
except (TypeError, ValueError):
return None
# Boss-ghost -> physical LEGO set reference (factual mapping, used as "set references").
# Format: ability/ghost name : {set_number, set_name}
BOSS_SETS = {
"Dr. Drewell": ("70418", "J.B.'s Ghost Lab"),
"Captain Archibald": ("70419", "Wrecked Shrimp Boat"),
"Mamali": ("70420", "Graveyard Mystery"),
"Samuel Mason": ("70421", "El Fuego's Stunt Truck"),
"Anomolo": ("70422", "Shrimp Shack Attack"),
"Spewer": ("70423", "Paranormal Intercept Bus 3000"),
"The Bawa": ("70424", "Ghost Train Express"),
"Mr. Nibs": ("70425", "Newbury Haunted High School"),
"Lady E": ("70427", "Welcome to the Hidden Side"),
"Trucker Dale": ("70428", "Jack's Beach Buggy"),
"Harry Cane": ("70429", "El Fuego's Stunt Plane"),
"Joe Ishmael": ("70431", "The Lighthouse of Darkness"),
"Tragico": ("70432", "Haunted Fairground"),
"Maxine Turbo": ("70434", "Supernatural Race Car"),
"Bart Chaney": ("70435", "Newbury Abandoned Prison"),
"Blaze M. Barr": ("70436", "Phantom Fire Truck 3000"),
"The Maw": ("70437", "Mystery Castle"),
}
def main(xlsx_path, out_dir):
wb = openpyxl.load_workbook(xlsx_path, data_only=True)
ws = wb.active
rows = [[c for c in r] for r in ws.iter_rows(values_only=True)]
ghosts = []
common_abilities = []
boss_abilities = []
cur_type = None
cur_rarity = None
mode = "ghosts" # ghosts -> common_ability -> boss_ability
for r in rows:
first = r[0] if r else None
if not isinstance(first, str):
continue
s = first.strip()
if s == "Common Ghost Abilities":
mode = "common_ability"; continue
if s == "Boss Ghost Abilities":
mode = "boss_ability"; continue
if s == "Ghost Abilities":
continue
if mode == "ghosts":
if s.endswith("Red Ghosts") and not s.startswith(""):
cur_type = "red"; continue
if s.endswith("Yellow Ghosts") and not s.startswith(""):
cur_type = "yellow"; continue
if s.endswith("Blue Ghosts") and not s.startswith(""):
cur_type = "blue"; continue
if s.startswith(""):
cur_rarity = s.count(""); continue
if s in ("Name",) or s.startswith("Ghost Team") or s.startswith("Newbury is haunted"):
continue
# data row
ghosts.append({
"name": first,
"type": cur_type,
"rarity": cur_rarity,
"speed": pips(r[1]),
"range": pips(r[2]),
"chargeShot": pips(r[3]),
"health": num(r[4]),
"damage": num(r[5]),
"ability": (r[6].strip() if isinstance(r[6], str) else None),
})
elif mode in ("common_ability", "boss_ability"):
if s == "Name":
continue
if s.startswith("Last revised"):
continue
entry = {
"name": first,
"charges": num(r[1]),
"cooldown": (r[2].strip() if isinstance(r[2], str) else r[2]),
"effect": (r[3].strip() if isinstance(r[3], str) else None),
}
(common_abilities if mode == "common_ability" else boss_abilities).append(entry)
boss_ability_names = {a["name"] for a in boss_abilities}
# mark bosses + attach set reference
for g in ghosts:
is_boss = g["ability"] in boss_ability_names
g["isBoss"] = is_boss
ref = BOSS_SETS.get(g["name"])
g["setNumber"] = ref[0] if ref else None
g["setName"] = ref[1] if ref else None
abilities = []
for a in common_abilities:
a2 = dict(a); a2["kind"] = "common"; abilities.append(a2)
for a in boss_abilities:
a2 = dict(a); a2["kind"] = "boss"; abilities.append(a2)
# build set roster: each boss-linked set gets its boss + a sampling is done at runtime,
# but we record the canonical set list here for the admin UI starter.
sets = []
seen = set()
for num_, name in sorted(set(BOSS_SETS.values())):
if num_ in seen:
continue
seen.add(num_)
boss = next((g["name"] for g in ghosts if g["setNumber"] == num_), None)
sets.append({"setNumber": num_, "setName": name, "boss": boss})
with open(f"{out_dir}/ghosts.json", "w") as f:
json.dump(ghosts, f, indent=2, ensure_ascii=False)
with open(f"{out_dir}/abilities.json", "w") as f:
json.dump(abilities, f, indent=2, ensure_ascii=False)
with open(f"{out_dir}/sets.json", "w") as f:
json.dump(sets, f, indent=2, ensure_ascii=False)
bosses = [g["name"] for g in ghosts if g["isBoss"]]
print(f"ghosts: {len(ghosts)} | abilities: {len(abilities)} "
f"(common {len(common_abilities)}, boss {len(boss_abilities)}) "
f"| sets: {len(sets)} | bosses: {len(bosses)}")
by_type = {}
for g in ghosts:
by_type[g["type"]] = by_type.get(g["type"], 0) + 1
print("by type:", by_type)
if __name__ == "__main__":
xlsx = sys.argv[1] if len(sys.argv) > 1 else "Ghost_Data.xlsx"
out = sys.argv[2] if len(sys.argv) > 2 else "data"
main(xlsx, out)