Add xlsx -> JSON extractor
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user