163 lines
5.9 KiB
Python
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)
|