foitin/pipeline/pack_character.py
m3ultra b067bf7f0b Scaffold: deterministic 60Hz combat core, drop-in character format, MODELBEAST pipeline
- Godot 4.7 project; fixed-tick FSM fighter, facing-relative input buffer
  with numpad motion parsing, box collision, training-mode hitbox overlay
- characters/ = self-describing drop-in folders (spec in _spec/); two
  generated placeholder fighters (alpha, beta)
- pipeline/: pack_character.py + autohitbox.py (pose/silhouette hurtboxes)
- STUDY.md: reverse-engineering study of the BKB reference games

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 18:45:44 +10:00

68 lines
2.5 KiB
Python

#!/usr/bin/env python3
"""Pack rendered frames into a character folder (CHARACTER_SPEC.md).
Takes a renders directory laid out as renders/<char>/<move>/*.png and
assembles characters/<char>/ with a data.json template per move (timing
fields to be tuned by hand, hurtboxes to be filled by autohitbox.py).
Usage:
python3 pipeline/pack_character.py renders/hero --name HERO --color '#cc4488'
"""
import argparse
import json
import shutil
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
DATA_TEMPLATE = dict(total=30, loop=False, startup=10, active=[11, 14],
damage=50, hitstun=16, blockstun=10, pushback=10,
hurtboxes={"default": [[-25, -300, 50, 300]]}, hitboxes={})
LOOPED = {"idle", "walk_f", "walk_b"}
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("renders_dir", help="renders/<char> containing <move>/*.png")
ap.add_argument("--name", default=None)
ap.add_argument("--color", default="#cccccc")
ap.add_argument("--health", type=int, default=1000)
args = ap.parse_args()
src = Path(args.renders_dir)
cid = src.name
dst = REPO / "characters" / cid
moves = []
for move_dir in sorted(p for p in src.iterdir() if p.is_dir()):
frames = sorted(move_dir.glob("*.png")) + sorted(move_dir.glob("*.webp"))
if not frames:
continue
out = dst / "moves" / move_dir.name / "frames"
out.mkdir(parents=True, exist_ok=True)
for i, f in enumerate(frames):
shutil.copy(f, out / f"{i:04d}{f.suffix}")
data_path = out.parent / "data.json"
if not data_path.exists(): # never clobber hand-tuned frame data
data = dict(name=move_dir.name, **DATA_TEMPLATE)
data["loop"] = move_dir.name in LOOPED
data_path.write_text(json.dumps(data, indent=2))
moves.append(move_dir.name)
manifest_path = dst / "manifest.json"
if not manifest_path.exists():
manifest_path.write_text(json.dumps({
"id": cid, "name": (args.name or cid).upper(),
"health": args.health, "walk_speed": 3.2, "back_speed": 2.6,
"jump_velocity": -13.5, "color": args.color,
"normals": {"P": "jab", "K": "kick"},
"command_moves": [],
}, indent=2))
print(f"packed characters/{cid}: {len(moves)} moves -> {moves}")
print("next: pipeline/autohitbox.py for hurtboxes, then tune data.json timings")
if __name__ == "__main__":
main()