- batch_roster kit_glb mode: pre-retargeted MIRPAMO kits render via --self-anim; unmapped actions dropped - both nude bases fully playable: 16 moves, auto finisher timing, base-only wardrobe until WARDROBEGOD garments arrive - select grid scales to 18+ (9 per row tier) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
167 lines
8.5 KiB
Python
167 lines
8.5 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"}),
|
|
# -- MeshGod wave (TRELLIS fused meshes: torn="*" rips the whole texture) --
|
|
"decks": dict(model="decks.fbx", torn="*",
|
|
kit={"idle": "Bouncing Fight Idle", "jab": "Elbow Punching",
|
|
"straight": "Cross Punch", "kick": "Kicking (2)",
|
|
"special": "Stylish Flip", "finisher": "Headbutt"}),
|
|
"birdie": dict(model="birdie.fbx", torn="*",
|
|
kit={"idle": "Ready Idle", "jab": "Punching",
|
|
"straight": "Hook Punch", "kick": "Knee Kick Lead",
|
|
"special": "Flip Kick (1)", "finisher": "Bayonet Stab"}),
|
|
"widow": dict(model="widow.fbx", torn="*",
|
|
kit={"idle": "Ready Idle", "jab": "Boxing (2)",
|
|
"straight": "Big Body Blow", "kick": "Kicking",
|
|
"special": "Inverted Double Kick To Kip Up", "finisher": "Strangling"}),
|
|
"violet": dict(model="violet.fbx", torn="*",
|
|
kit={"idle": "Bouncing Fight Idle", "jab": "Boxing (1)",
|
|
"straight": "Mutant Punch", "kick": "Leg Sweep",
|
|
"special": "Flip Kick", "finisher": "Standing Torch Melee Attack 01"}),
|
|
"nappo": dict(model="nappo.fbx", torn="*",
|
|
kit={"idle": "Bouncing Fight Idle", "jab": "Elbow Punch",
|
|
"straight": "Cross Punch", "kick": "Kicking (2)",
|
|
"special": "Front Twist Flip", "finisher": "Grab And Slam"}),
|
|
"dread": dict(model="dread.fbx", torn="*",
|
|
kit={"idle": "Ready Idle", "jab": "Punching",
|
|
"straight": "Hook Punch", "kick": "Kicking (6)",
|
|
"special": "Front Flip", "finisher": "Fireball"}),
|
|
"jima": dict(model="jima.fbx", torn="*",
|
|
kit={"idle": "Ginga Variation 3", "jab": "Boxing (2)",
|
|
"straight": "Big Body Blow", "kick": "Knee Kick Lead",
|
|
"special": "Macaco Side", "finisher": "Stable Sword Outward Slash"}),
|
|
"roca": dict(model="roca.fbx", torn="*",
|
|
kit={"idle": "Ready Idle", "jab": "Elbow Punching",
|
|
"straight": "Cross Punch", "kick": "Standing Melee Attack Kick Ver. 1",
|
|
"special": "Ginga Sideways To Au", "finisher": "Hell Slammer B"}),
|
|
# -- MIRPAMO wave: autorigged nude bases; kits pre-retargeted into the
|
|
# GLB (mirpamo anim), rendered via --self-anim. Garment states come
|
|
# later through WARDROBEGOD FIT.
|
|
"siren": dict(kit_glb="phrtt2_kit.glb",
|
|
kit={"idle": "Bouncing Fight Idle", "jab": "Cross Punch",
|
|
"straight": "Hook Punch", "kick": "Kicking (6)",
|
|
"special": "Flip Kick", "finisher": "Standing Melee Attack Kick Ver. 1"}),
|
|
"daphne": dict(kit_glb="daphne_kit.glb",
|
|
kit={"idle": "Ready Idle", "jab": "Cross Punch",
|
|
"straight": "Hook Punch", "kick": "Kicking (6)",
|
|
"special": "Flip Kick", "finisher": "Standing Melee Attack Kick Ver. 1"}),
|
|
}
|
|
|
|
|
|
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"])
|
|
out = REPO / "renders" / cid
|
|
if out.exists():
|
|
shutil.rmtree(out)
|
|
|
|
if "kit_glb" in cfg:
|
|
# pre-retargeted MIRPAMO kit: render the GLB's own baked actions
|
|
hb(f"{cid}: render kit glb (self-anim)")
|
|
subprocess.run([BLENDER, "-b", "--python",
|
|
str(REPO / "pipeline/render_moves.py"), "--",
|
|
"--char", str(R / cfg["kit_glb"]), "--self-anim",
|
|
"--out", str(out), "--step", "3", "--rotz", "155",
|
|
"--max-frames", "160"],
|
|
capture_output=True, text=True, check=False)
|
|
else:
|
|
clips = ",".join(str(M / f"{c}.fbx") for c in moves.values())
|
|
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); drop unmapped extras
|
|
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)
|
|
elif "kit_glb" in cfg and (out / d).is_dir():
|
|
shutil.rmtree(out / d)
|
|
|
|
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")
|