Immersive-Vibe-Development-Studio / scripts /extract_characters.py
BolyosCsaba
feat: Immersive Vibe Development Studio โ€” initial release
94c4245
#!/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/<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)
# 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/<game>/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)")