#!/usr/bin/env python3 """Batch-build roster fighters: render (base + torn) -> pack -> hitbox -> tune. Each fighter = model + clip kit. Runs sequentially; a fighter appears on the select screen as soon as its pack+tune completes. Heartbeat: ~/.jobs/foitin-roster.hb """ import json import os import shutil import subprocess import sys import time from pathlib import Path REPO = Path(__file__).resolve().parent.parent BLENDER = "/Applications/Blender.app/Contents/MacOS/Blender" M = Path.home() / "Documents/mixamo-animations" R = Path.home() / "Documents/foitin_assets/roster" HB = Path.home() / ".jobs/foitin-roster.hb" HB.parent.mkdir(exist_ok=True) SHARED = { "walk_f": "Walk Forward Left", "walk_b": "Stepping Backward", "jump": "Jump Up", "block": "Standing Block", "crouch": "Standing To Crouch", "hit": "Taking Punch", "knockdown": "Death From Right", "dizzy": "Stunned", "death": "Standing Death Backward 01", "getup": "Crouch To Stand", } FIGHTERS = { "raver": dict(model="woman_raver_01.fbx", torn="Ch03", kit={"idle": "Ready Idle", "jab": "Elbow Punch", "straight": "Cross Punch", "kick": "Kicking (1)", "special": "Spin Flip Kick", "finisher": "Grab And Slam"}), "chef": dict(model="woman_chef_01.fbx", torn="Shirt,Pants", kit={"idle": "Bouncing Fight Idle", "jab": "Punching (1)", "straight": "Hook Punch", "kick": "Standing Melee Kick", "special": "Drop Kick", "finisher": "Standing Melee Attack Downward"}), "exec": dict(model="woman_business_01.fbx", torn="Suit,Pants", kit={"idle": "Ready Idle", "jab": "Punching", "straight": "Hook Punch", "kick": "Roundhouse Kick", "special": "Flying Kick", "finisher": "Stabbing"}), "jinx": dict(model="woman_casual_02.fbx", torn="Shirt,Pants", kit={"idle": "Bouncing Fight Idle", "jab": "Boxing (1)", "straight": "Boxing (2)", "kick": "Kicking (3)", "special": "Flying Knee Punch Combo", "finisher": "Double Dagger Stab"}), "elva": dict(model="monster_elf_01.fbx", torn="Ch34", kit={"idle": "Ready Idle", "jab": "Elbow Punching", "straight": "Cross Punch", "kick": "Kicking (4)", "special": "Butterfly Twirl", "finisher": "Brutal Assassination (1)"}), "umbra": dict(model="monster_shadow_f_01.fbx", torn="Ch47", kit={"idle": "Bouncing Fight Idle", "jab": "Punching (1)", "straight": "Hook Punch", "kick": "Kicking (5)", "special": "Armada", "finisher": "Standing Melee Attack Horizontal"}), } def hb(msg: str) -> None: HB.write_text(f"{time.strftime('%H:%M:%S')} {msg}\n") print(msg, flush=True) def render(char: Path, clips: str, out: Path, torn: str | None) -> None: cmd = [BLENDER, "-b", "--python", str(REPO / "pipeline/render_moves.py"), "--", "--char", str(char), "--clips", clips, "--out", str(out), "--step", "3", "--rotz", "155", "--max-frames", "160"] if torn: cmd += ["--torn", torn] subprocess.run(cmd, capture_output=True, text=True, check=False) def build(cid: str, cfg: dict) -> None: moves = dict(SHARED) moves.update(cfg["kit"]) clips = ",".join(str(M / f"{c}.fbx") for c in moves.values()) out = REPO / "renders" / cid if out.exists(): shutil.rmtree(out) hb(f"{cid}: render base ({len(moves)} clips)") render(R / cfg["model"], clips, out, None) hb(f"{cid}: render torn") render(R / cfg["model"], clips, out, cfg["torn"]) # clip-name dirs -> move-name dirs (both states) stem_to_move = {c.lower(): m for m, c in moves.items()} for d in sorted(os.listdir(out)): base, _, state = d.partition("@") if base in stem_to_move: new = stem_to_move[base] + (("@" + state) if state else "") os.rename(out / d, out / new) hb(f"{cid}: pack + hitbox + tune") subprocess.run([str(REPO / ".venv/bin/python"), str(REPO / "pipeline/pack_character.py"), str(out)], capture_output=True, check=False) subprocess.run([str(REPO / ".venv/bin/python"), str(REPO / "pipeline/autohitbox.py"), str(REPO / "characters" / cid)], capture_output=True, check=False) subprocess.run([str(REPO / ".venv/bin/python"), str(REPO / "tools/tune_fighters.py"), cid], capture_output=True, check=False) # portrait from idle frame 0 (alpha-bbox head crop) subprocess.run([str(REPO / ".venv/bin/python"), str(REPO / "tools/make_portrait.py"), cid], capture_output=True, check=False) hb(f"{cid}: DONE") if __name__ == "__main__": targets = sys.argv[1:] or list(FIGHTERS) for cid in targets: build(cid, FIGHTERS[cid]) hb("ALL DONE")