foitin/tools/tune_fighters.py
m3ultra 54a54f2467 Torn-clothing wardrobe mechanic (Phase A)
- engine: wardrobe states per character (manifest wardrobe.states),
  frames@<state> variant frame-sets per move with base fallback,
  tears:true moves advance state, hp threshold forces final state
- pipeline: --torn punches jagged alpha holes into clothing textures
  (alpha-clip materials) and renders @torn variants; pack maps them
- both fighters: full torn frame-sets for all 12 moves
- special gets designed forward lunge (clip travel was backward)
- verified: demo QCF+P connects, knockdown shows torn frames

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 21:09:06 +10:00

91 lines
4.1 KiB
Python

#!/usr/bin/env python3
"""Apply hand-tuned frame data + manifests to the real fighters.
Run after pack_character.py. Timing values are modern fighting-game pacing
(60Hz ticks — see STUDY.md §2), NOT the source clip lengths: `total` compresses
or stretches the sprite sequence to gameplay speed. Values are per-move design
data — tweak freely and re-run.
"""
import json
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
# move -> data.json overrides. Boxes in 512-canvas local px (origin ground-centre).
# root_motion_frames=[] strips clip travel where gameplay owns movement
# (walking speed comes from input, not animation).
TUNE = {
"idle": dict(total=96, loop=True, root_motion_frames=[]),
"walk_f": dict(total=48, loop=True, root_motion_frames=[]),
"walk_b": dict(total=56, loop=True, root_motion_frames=[]),
"jump": dict(total=48, root_motion_frames=[]),
"crouch": dict(total=20, root_motion_frames=[]),
"win": dict(total=100, root_motion_frames=[]),
"lose": dict(total=110, root_motion_frames=[]),
"block": dict(total=24, root_motion_frames=[]),
"hit": dict(total=24, root_motion_frames=[]),
"knockdown": dict(total=60, hurtboxes={"default": [[-70, -70, 150, 70]]}),
"jab": dict(total=26, startup=5, active=[6, 9], damage=30, hitstun=14,
blockstun=8, pushback=8,
hitboxes={"6-9": [[40, -300, 85, 40]]}),
"straight": dict(total=34, startup=9, active=[10, 14], damage=55, hitstun=18,
blockstun=11, pushback=12,
hitboxes={"10-14": [[45, -305, 100, 45]]}),
"kick": dict(total=40, startup=12, active=[13, 18], damage=70, hitstun=22,
blockstun=13, pushback=14,
hitboxes={"13-18": [[40, -320, 120, 60]]}),
# designed lunge (per-tick root_motion beats clip-derived frames data —
# the chapa/hurricane clips travel the wrong way for gameplay)
"special": dict(total=52, startup=15, active=[16, 24], damage=95, hitstun=26,
blockstun=16, pushback=18, knockdown=True, tears=True,
root_motion=[0, 0, 0, 0, 2, 5, 8, 11, 13, 14, 14, 13, 12, 11,
10, 9, 8, 7, 6, 5, 4, 3, 2, 2, 1, 1],
hitboxes={"16-24": [[35, -330, 115, 80]]}),
}
MANIFESTS = {
"kachujin": {
"id": "kachujin", "name": "KACHUJIN", "health": 1000,
"walk_speed": 3.4, "back_speed": 2.7, "jump_velocity": -13.5,
"color": "#c03a2e", "sprite_scale": 1.25,
"wardrobe": {"states": ["base", "torn"], "tear_below_hp": 0.35},
"normals": {"P": "jab", "6P": "straight", "K": "kick"},
"command_moves": [{"motion": [2, 3, 6], "button": "P", "move": "special"}],
},
"vesper": {
"id": "vesper", "name": "VESPER", "health": 950,
"walk_speed": 3.7, "back_speed": 2.9, "jump_velocity": -14.0,
"color": "#b48fd0", "sprite_scale": 1.25,
"wardrobe": {"states": ["base", "torn"], "tear_below_hp": 0.35},
"normals": {"P": "jab", "6P": "straight", "K": "kick"},
"command_moves": [{"motion": [2, 3, 6], "button": "P", "move": "special"}],
},
}
def main() -> None:
chars = sys.argv[1:] or list(MANIFESTS)
for cid in chars:
root = REPO / "characters" / cid
if not root.exists():
print(f"skip {cid}: not packed yet")
continue
for move, overrides in TUNE.items():
data_path = root / "moves" / move / "data.json"
if not data_path.exists():
continue
data = json.loads(data_path.read_text())
hurt = data.get("hurtboxes") # keep autohitbox output unless overridden
data.update(overrides)
if "hurtboxes" not in overrides and hurt:
data["hurtboxes"] = hurt
data_path.write_text(json.dumps(data, indent=2))
if cid in MANIFESTS:
(root / "manifest.json").write_text(json.dumps(MANIFESTS[cid], indent=2))
print(f"tuned {cid}")
if __name__ == "__main__":
main()