#!/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=[]), "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]]}), "special": dict(total=52, startup=15, active=[16, 24], damage=95, hitstun=26, blockstun=16, pushback=18, knockdown=True, hitboxes={"16-24": [[35, -330, 115, 80]]}), # travel: clip root motion } 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, "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, "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()