foitin/pipeline/batch_roster.py
m3ultra 0f83637fcc Roster batch tooling: 6 new fighters from the asset library
- pipeline/batch_roster.py: model + clip-kit per fighter, renders
  base+torn, packs, hitboxes, tunes, portraits — sequential batch
- --strip flag (mesh removal) added; Mixamo pack models turn out hollow
  under clothes, so this batch ships base+torn; true clothes-off waits
  on rigging the wardrobegod nude bases (phrtt2, daphne)
- tune_fighters: manifests + default finisher timing for raver/chef/
  exec/jinx/elva/umbra; getup move tuned
- select screen wraps to a 4-per-row grid

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 23:38:16 +10:00

111 lines
4.8 KiB
Python

#!/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")