#!/usr/bin/env python3 """ Dev-time script. Extracts buildXxx() functions from playground sandbox JS files. Writes sandbox_cache/characters_registry.json and sandbox_cache/characters.js. Usage: python3 scripts/extract_characters.py Source: /Users/bolyos/Desktop/playground//sandbox/ """ import json import re from pathlib import Path PLAYGROUND_BASE = Path("/Users/bolyos/Desktop/playground") OUT_DIR = Path(__file__).parent.parent / "sandbox_cache" OUT_DIR.mkdir(exist_ok=True) # Pure Three.js geometry primitives — not characters, exclude them GEOMETRY_PRIMITIVES = { "buildBox", "buildCylinder", "buildSphere", "buildCone", "buildIcosahedron", "buildOctahedron", "buildDodecahedron", "buildTetrahedron", "buildLathe", } # Scan patterns in priority order SCAN_PATTERNS = [ "*/sandbox/src/characters.js", # jungle modular structure — try first "*/sandbox/main.js", "*/sandbox/bundle.js", ] def extract_functions(source: str) -> dict[str, str]: """Extract top-level function buildXxx() {...} blocks from JS source.""" registry: dict[str, str] = {} pattern = re.compile(r'^(function (build\w+)\([^)]*\)\s*\{)', re.MULTILINE) for m in pattern.finditer(source): fn_name = m.group(2) if fn_name in GEOMETRY_PRIMITIVES: continue start = m.start() depth = 0 for j in range(m.start(1), len(source)): if source[j] == "{": depth += 1 elif source[j] == "}": depth -= 1 if depth == 0: registry[fn_name] = source[start : j + 1] break return registry def game_slug(js_path: Path) -> str: """Derive a clean slug from the game directory name.""" # parent chain: .../playground//sandbox/[src/]file.js parts = js_path.parts try: pg_idx = next(i for i, p in enumerate(parts) if p == "playground") game_name = parts[pg_idx + 1] except (StopIteration, IndexError): game_name = js_path.parent.parent.name return re.sub(r"[^a-zA-Z0-9]+", "_", game_name).strip("_").lower() # ── Collect all JS files to scan ───────────────────────────────────────────── js_files: list[Path] = [] for pattern in SCAN_PATTERNS: for f in sorted(PLAYGROUND_BASE.glob(pattern)): if f not in js_files: js_files.append(f) print(f"Files to scan: {len(js_files)}") for f in js_files: print(f" {f}") # ── Extract, resolving buildHero collisions by namespacing ──────────────────── registry: dict[str, str] = {} hero_sources: dict[str, str] = {} # fn_name → game_slug for collision tracking for js_file in js_files: slug = game_slug(js_file) try: text = js_file.read_text(errors="ignore") except OSError as e: print(f" WARNING: could not read {js_file}: {e}") continue found = extract_functions(text) print(f" {js_file.name} ({slug}): {len(found)} functions found") for fn_name, fn_body in found.items(): if fn_name == "buildHero": namespaced = f"buildHero_{slug}" # Rename the function declaration to match the namespaced key fn_body_renamed = fn_body.replace( f"function buildHero(", f"function {namespaced}(", 1 ) registry[namespaced] = fn_body_renamed print(f" ↳ buildHero → {namespaced}") else: if fn_name in registry: print(f" ↳ {fn_name} already seen, skipping duplicate") else: registry[fn_name] = fn_body print(f"\nTotal unique build functions: {len(registry)}") # ── Character functions (names containing 'Character', 'Songoku', 'Hero', or Pokemon names) ── character_fns = sorted([ k for k in registry if any(tag in k for tag in ["Character", "Hero", "Songoku", "Raticate", "Persian", "Tauros", "Snorlax", "Graveler", "Onix", "Zubat", "Golbat", "Pidgey", "Fearow", "Beedrill", "Butterfree", "Voltorb", "Electrode", "Jigglypuff", "Abra", "Alakazam", "Gengar", "Doduo", "Rapidash"]) ]) print(f"\nCharacter functions ({len(character_fns)}): {character_fns}") # ── Write outputs ───────────────────────────────────────────────────────────── json_path = OUT_DIR / "characters_registry.json" json_path.write_text(json.dumps(registry, indent=2)) print(f"\nWritten: {json_path} ({json_path.stat().st_size} bytes)") js_path = OUT_DIR / "characters.js" js_path.write_text("\n\n".join(registry.values()) + "\n") print(f"Written: {js_path} ({js_path.stat().st_size} bytes)")