#!/usr/bin/env python3 """Per-fighter finisher timing from the actual frames. The impact moment of a strike is the frame of maximum forward extent (rightmost opaque pixel, fighters render facing right). Sets: total = frames * 6 ticks (batch renders sample at 10fps -> real-time) active = impact frame .. +3 frames, startup just before Writes tools/finisher_timings.json (merged by tune_fighters.py) and patches each characters//moves/finisher/data.json in place. """ import json import sys from pathlib import Path import numpy as np from PIL import Image REPO = Path(__file__).resolve().parent.parent OUT = REPO / "tools/finisher_timings.json" def impact_frame(frames_dir: Path): frames = sorted(frames_dir.glob("*.png")) ext = [] for f in frames: a = np.array(Image.open(f).split()[-1]) cols = np.where(a.max(axis=0) > 32)[0] ext.append(cols[-1] if len(cols) else 0) if not ext: return None lo = max(int(len(ext) * 0.2), 1) # skip windup extension i = lo + int(np.argmax(ext[lo:])) return i, len(frames) def main() -> None: chars = sys.argv[1:] or [d.name for d in (REPO / "characters").iterdir() if not d.name.startswith("_")] timings = json.loads(OUT.read_text()) if OUT.exists() else {} for cid in sorted(chars): fdir = REPO / "characters" / cid / "moves/finisher/frames" if not fdir.exists(): continue res = impact_frame(fdir) if res is None: continue i, n = res tpf = 6 # ticks per art frame total = n * tpf a0, a1 = i * tpf, min((i + 4) * tpf - 1, total - 2) t = dict(total=total, startup=max(a0 - 1, 1), active=[a0, a1], damage=0, finisher=True, hitstun=0, blockstun=0, pushback=0, hitboxes={f"{a0}-{a1}": [[0, -320, 180, 300]]}) timings[cid] = t data_path = fdir.parent / "data.json" data = json.loads(data_path.read_text()) hurt = data.get("hurtboxes") data.update(t) if hurt: data["hurtboxes"] = hurt data_path.write_text(json.dumps(data, indent=2)) print(f"{cid}: impact frame {i}/{n} -> active ticks {a0}-{a1}, total {total}") OUT.write_text(json.dumps(timings, indent=2)) if __name__ == "__main__": main()