| |
| """ |
| 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/<game>/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) |
|
|
| |
| GEOMETRY_PRIMITIVES = { |
| "buildBox", "buildCylinder", "buildSphere", "buildCone", |
| "buildIcosahedron", "buildOctahedron", "buildDodecahedron", |
| "buildTetrahedron", "buildLathe", |
| } |
|
|
| |
| SCAN_PATTERNS = [ |
| "*/sandbox/src/characters.js", |
| "*/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.""" |
| |
| 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() |
|
|
|
|
| |
| 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}") |
|
|
| |
| registry: dict[str, str] = {} |
| hero_sources: dict[str, str] = {} |
|
|
| 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}" |
| |
| 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_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}") |
|
|
| |
| 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)") |
|
|