From c3d682b7dd4cc9bba642b8a8a28f45aa84cc52b0 Mon Sep 17 00:00:00 2001 From: m3ultra Date: Wed, 29 Jul 2026 14:31:13 +1000 Subject: [PATCH] Finisher timing pass: measured impact windows for all batch fighters tools/tune_finishers.py detects each finisher's impact frame (max forward extent of the sprite) and sets real-time totals + active windows; values persist in finisher_timings.json and win over DEFAULT_FINISHER in tune. kachujin/vesper keep hand-tuned values. Co-Authored-By: Claude Opus 5 --- characters/birdie/moves/finisher/data.json | 12 +- characters/chef/moves/finisher/data.json | 12 +- characters/decks/moves/finisher/data.json | 12 +- characters/dread/moves/finisher/data.json | 12 +- characters/elva/moves/finisher/data.json | 12 +- characters/exec/moves/finisher/data.json | 12 +- characters/jima/moves/finisher/data.json | 12 +- characters/jinx/moves/finisher/data.json | 12 +- characters/nappo/moves/finisher/data.json | 12 +- characters/raver/moves/finisher/data.json | 12 +- characters/roca/moves/finisher/data.json | 12 +- characters/umbra/moves/finisher/data.json | 12 +- characters/violet/moves/finisher/data.json | 12 +- characters/widow/moves/finisher/data.json | 12 +- tools/finisher_timings.json | 324 +++++++++++++++++++++ tools/tune_fighters.py | 5 + tools/tune_finishers.py | 67 +++++ 17 files changed, 480 insertions(+), 84 deletions(-) create mode 100644 tools/finisher_timings.json create mode 100644 tools/tune_finishers.py diff --git a/characters/birdie/moves/finisher/data.json b/characters/birdie/moves/finisher/data.json index b1d3cb44..38ba9a20 100644 --- a/characters/birdie/moves/finisher/data.json +++ b/characters/birdie/moves/finisher/data.json @@ -1,11 +1,11 @@ { "name": "finisher", - "total": 220, + "total": 198, "loop": false, - "startup": 78, + "startup": 95, "active": [ - 82, - 96 + 96, + 119 ], "damage": 0, "hitstun": 0, @@ -676,11 +676,11 @@ } }, "hitboxes": { - "82-96": [ + "96-119": [ [ 0, -320, - 170, + 180, 300 ] ] diff --git a/characters/chef/moves/finisher/data.json b/characters/chef/moves/finisher/data.json index a222a701..387f338a 100644 --- a/characters/chef/moves/finisher/data.json +++ b/characters/chef/moves/finisher/data.json @@ -1,11 +1,11 @@ { "name": "finisher", - "total": 220, + "total": 138, "loop": false, - "startup": 78, + "startup": 23, "active": [ - 82, - 96 + 24, + 47 ], "damage": 0, "hitstun": 0, @@ -476,11 +476,11 @@ } }, "hitboxes": { - "82-96": [ + "24-47": [ [ 0, -320, - 170, + 180, 300 ] ] diff --git a/characters/decks/moves/finisher/data.json b/characters/decks/moves/finisher/data.json index f8a2e520..0b658f62 100644 --- a/characters/decks/moves/finisher/data.json +++ b/characters/decks/moves/finisher/data.json @@ -1,11 +1,11 @@ { "name": "finisher", - "total": 220, + "total": 132, "loop": false, - "startup": 78, + "startup": 65, "active": [ - 82, - 96 + 66, + 89 ], "damage": 0, "hitstun": 0, @@ -456,11 +456,11 @@ } }, "hitboxes": { - "82-96": [ + "66-89": [ [ 0, -320, - 170, + 180, 300 ] ] diff --git a/characters/dread/moves/finisher/data.json b/characters/dread/moves/finisher/data.json index 7586a9fc..e3e8373f 100644 --- a/characters/dread/moves/finisher/data.json +++ b/characters/dread/moves/finisher/data.json @@ -1,11 +1,11 @@ { "name": "finisher", - "total": 220, + "total": 204, "loop": false, - "startup": 78, + "startup": 131, "active": [ - 82, - 96 + 132, + 155 ], "damage": 0, "hitstun": 0, @@ -696,11 +696,11 @@ } }, "hitboxes": { - "82-96": [ + "132-155": [ [ 0, -320, - 170, + 180, 300 ] ] diff --git a/characters/elva/moves/finisher/data.json b/characters/elva/moves/finisher/data.json index d4c0a1e8..55e0c511 100644 --- a/characters/elva/moves/finisher/data.json +++ b/characters/elva/moves/finisher/data.json @@ -1,11 +1,11 @@ { "name": "finisher", - "total": 220, + "total": 300, "loop": false, - "startup": 78, + "startup": 77, "active": [ - 82, - 96 + 78, + 101 ], "damage": 0, "hitstun": 0, @@ -636,11 +636,11 @@ } }, "hitboxes": { - "82-96": [ + "78-101": [ [ 0, -320, - 170, + 180, 300 ] ] diff --git a/characters/exec/moves/finisher/data.json b/characters/exec/moves/finisher/data.json index 74bdeb75..6792f3bc 100644 --- a/characters/exec/moves/finisher/data.json +++ b/characters/exec/moves/finisher/data.json @@ -1,11 +1,11 @@ { "name": "finisher", - "total": 220, + "total": 144, "loop": false, - "startup": 78, + "startup": 65, "active": [ - 82, - 96 + 66, + 89 ], "damage": 0, "hitstun": 0, @@ -496,11 +496,11 @@ } }, "hitboxes": { - "82-96": [ + "66-89": [ [ 0, -320, - 170, + 180, 300 ] ] diff --git a/characters/jima/moves/finisher/data.json b/characters/jima/moves/finisher/data.json index aabd15c4..9c84afe5 100644 --- a/characters/jima/moves/finisher/data.json +++ b/characters/jima/moves/finisher/data.json @@ -1,11 +1,11 @@ { "name": "finisher", - "total": 220, + "total": 126, "loop": false, - "startup": 78, + "startup": 89, "active": [ - 82, - 96 + 90, + 113 ], "damage": 0, "hitstun": 0, @@ -436,11 +436,11 @@ } }, "hitboxes": { - "82-96": [ + "90-113": [ [ 0, -320, - 170, + 180, 300 ] ] diff --git a/characters/jinx/moves/finisher/data.json b/characters/jinx/moves/finisher/data.json index 9bb3d3ac..5f4f27dc 100644 --- a/characters/jinx/moves/finisher/data.json +++ b/characters/jinx/moves/finisher/data.json @@ -1,11 +1,11 @@ { "name": "finisher", - "total": 220, + "total": 186, "loop": false, - "startup": 78, + "startup": 77, "active": [ - 82, - 96 + 78, + 101 ], "damage": 0, "hitstun": 0, @@ -616,11 +616,11 @@ } }, "hitboxes": { - "82-96": [ + "78-101": [ [ 0, -320, - 170, + 180, 300 ] ] diff --git a/characters/nappo/moves/finisher/data.json b/characters/nappo/moves/finisher/data.json index 71474df8..125501e1 100644 --- a/characters/nappo/moves/finisher/data.json +++ b/characters/nappo/moves/finisher/data.json @@ -1,11 +1,11 @@ { "name": "finisher", - "total": 220, + "total": 258, "loop": false, - "startup": 78, + "startup": 197, "active": [ - 82, - 96 + 198, + 221 ], "damage": 0, "hitstun": 0, @@ -876,11 +876,11 @@ } }, "hitboxes": { - "82-96": [ + "198-221": [ [ 0, -320, - 170, + 180, 300 ] ] diff --git a/characters/raver/moves/finisher/data.json b/characters/raver/moves/finisher/data.json index 1d674bf7..cb3e6e68 100644 --- a/characters/raver/moves/finisher/data.json +++ b/characters/raver/moves/finisher/data.json @@ -1,11 +1,11 @@ { "name": "finisher", - "total": 220, + "total": 258, "loop": false, - "startup": 78, + "startup": 197, "active": [ - 82, - 96 + 198, + 221 ], "damage": 0, "hitstun": 0, @@ -616,11 +616,11 @@ } }, "hitboxes": { - "82-96": [ + "198-221": [ [ 0, -320, - 170, + 180, 300 ] ] diff --git a/characters/roca/moves/finisher/data.json b/characters/roca/moves/finisher/data.json index 9d9df57d..8c820368 100644 --- a/characters/roca/moves/finisher/data.json +++ b/characters/roca/moves/finisher/data.json @@ -1,11 +1,11 @@ { "name": "finisher", - "total": 220, + "total": 324, "loop": false, - "startup": 78, + "startup": 287, "active": [ - 82, - 96 + 288, + 311 ], "damage": 0, "hitstun": 0, @@ -1096,11 +1096,11 @@ } }, "hitboxes": { - "82-96": [ + "288-311": [ [ 0, -320, - 170, + 180, 300 ] ] diff --git a/characters/umbra/moves/finisher/data.json b/characters/umbra/moves/finisher/data.json index f53b31f4..e7023c11 100644 --- a/characters/umbra/moves/finisher/data.json +++ b/characters/umbra/moves/finisher/data.json @@ -1,11 +1,11 @@ { "name": "finisher", - "total": 220, + "total": 150, "loop": false, - "startup": 78, + "startup": 35, "active": [ - 82, - 96 + 36, + 59 ], "damage": 0, "hitstun": 0, @@ -516,11 +516,11 @@ } }, "hitboxes": { - "82-96": [ + "36-59": [ [ 0, -320, - 170, + 180, 300 ] ] diff --git a/characters/violet/moves/finisher/data.json b/characters/violet/moves/finisher/data.json index 138d0538..3c38674a 100644 --- a/characters/violet/moves/finisher/data.json +++ b/characters/violet/moves/finisher/data.json @@ -1,11 +1,11 @@ { "name": "finisher", - "total": 220, + "total": 126, "loop": false, - "startup": 78, + "startup": 53, "active": [ - 82, - 96 + 54, + 77 ], "damage": 0, "hitstun": 0, @@ -436,11 +436,11 @@ } }, "hitboxes": { - "82-96": [ + "54-77": [ [ 0, -320, - 170, + 180, 300 ] ] diff --git a/characters/widow/moves/finisher/data.json b/characters/widow/moves/finisher/data.json index 4694e71e..f883c3e7 100644 --- a/characters/widow/moves/finisher/data.json +++ b/characters/widow/moves/finisher/data.json @@ -1,11 +1,11 @@ { "name": "finisher", - "total": 220, + "total": 324, "loop": false, - "startup": 78, + "startup": 179, "active": [ - 82, - 96 + 180, + 203 ], "damage": 0, "hitstun": 0, @@ -1096,11 +1096,11 @@ } }, "hitboxes": { - "82-96": [ + "180-203": [ [ 0, -320, - 170, + 180, 300 ] ] diff --git a/tools/finisher_timings.json b/tools/finisher_timings.json new file mode 100644 index 00000000..36babd71 --- /dev/null +++ b/tools/finisher_timings.json @@ -0,0 +1,324 @@ +{ + "birdie": { + "total": 198, + "startup": 95, + "active": [ + 96, + 119 + ], + "damage": 0, + "finisher": true, + "hitstun": 0, + "blockstun": 0, + "pushback": 0, + "hitboxes": { + "96-119": [ + [ + 0, + -320, + 180, + 300 + ] + ] + } + }, + "chef": { + "total": 138, + "startup": 23, + "active": [ + 24, + 47 + ], + "damage": 0, + "finisher": true, + "hitstun": 0, + "blockstun": 0, + "pushback": 0, + "hitboxes": { + "24-47": [ + [ + 0, + -320, + 180, + 300 + ] + ] + } + }, + "decks": { + "total": 132, + "startup": 65, + "active": [ + 66, + 89 + ], + "damage": 0, + "finisher": true, + "hitstun": 0, + "blockstun": 0, + "pushback": 0, + "hitboxes": { + "66-89": [ + [ + 0, + -320, + 180, + 300 + ] + ] + } + }, + "dread": { + "total": 204, + "startup": 131, + "active": [ + 132, + 155 + ], + "damage": 0, + "finisher": true, + "hitstun": 0, + "blockstun": 0, + "pushback": 0, + "hitboxes": { + "132-155": [ + [ + 0, + -320, + 180, + 300 + ] + ] + } + }, + "elva": { + "total": 300, + "startup": 77, + "active": [ + 78, + 101 + ], + "damage": 0, + "finisher": true, + "hitstun": 0, + "blockstun": 0, + "pushback": 0, + "hitboxes": { + "78-101": [ + [ + 0, + -320, + 180, + 300 + ] + ] + } + }, + "exec": { + "total": 144, + "startup": 65, + "active": [ + 66, + 89 + ], + "damage": 0, + "finisher": true, + "hitstun": 0, + "blockstun": 0, + "pushback": 0, + "hitboxes": { + "66-89": [ + [ + 0, + -320, + 180, + 300 + ] + ] + } + }, + "jima": { + "total": 126, + "startup": 89, + "active": [ + 90, + 113 + ], + "damage": 0, + "finisher": true, + "hitstun": 0, + "blockstun": 0, + "pushback": 0, + "hitboxes": { + "90-113": [ + [ + 0, + -320, + 180, + 300 + ] + ] + } + }, + "jinx": { + "total": 186, + "startup": 77, + "active": [ + 78, + 101 + ], + "damage": 0, + "finisher": true, + "hitstun": 0, + "blockstun": 0, + "pushback": 0, + "hitboxes": { + "78-101": [ + [ + 0, + -320, + 180, + 300 + ] + ] + } + }, + "nappo": { + "total": 258, + "startup": 197, + "active": [ + 198, + 221 + ], + "damage": 0, + "finisher": true, + "hitstun": 0, + "blockstun": 0, + "pushback": 0, + "hitboxes": { + "198-221": [ + [ + 0, + -320, + 180, + 300 + ] + ] + } + }, + "raver": { + "total": 258, + "startup": 197, + "active": [ + 198, + 221 + ], + "damage": 0, + "finisher": true, + "hitstun": 0, + "blockstun": 0, + "pushback": 0, + "hitboxes": { + "198-221": [ + [ + 0, + -320, + 180, + 300 + ] + ] + } + }, + "roca": { + "total": 324, + "startup": 287, + "active": [ + 288, + 311 + ], + "damage": 0, + "finisher": true, + "hitstun": 0, + "blockstun": 0, + "pushback": 0, + "hitboxes": { + "288-311": [ + [ + 0, + -320, + 180, + 300 + ] + ] + } + }, + "umbra": { + "total": 150, + "startup": 35, + "active": [ + 36, + 59 + ], + "damage": 0, + "finisher": true, + "hitstun": 0, + "blockstun": 0, + "pushback": 0, + "hitboxes": { + "36-59": [ + [ + 0, + -320, + 180, + 300 + ] + ] + } + }, + "violet": { + "total": 126, + "startup": 53, + "active": [ + 54, + 77 + ], + "damage": 0, + "finisher": true, + "hitstun": 0, + "blockstun": 0, + "pushback": 0, + "hitboxes": { + "54-77": [ + [ + 0, + -320, + 180, + 300 + ] + ] + } + }, + "widow": { + "total": 324, + "startup": 179, + "active": [ + 180, + 203 + ], + "damage": 0, + "finisher": true, + "hitstun": 0, + "blockstun": 0, + "pushback": 0, + "hitboxes": { + "180-203": [ + [ + 0, + -320, + 180, + 300 + ] + ] + } + } +} \ No newline at end of file diff --git a/tools/tune_fighters.py b/tools/tune_fighters.py index bba2e1b7..568cf98e 100644 --- a/tools/tune_fighters.py +++ b/tools/tune_fighters.py @@ -130,6 +130,9 @@ MANIFESTS = { def main() -> None: chars = sys.argv[1:] or list(MANIFESTS) + # measured impact windows (tools/tune_finishers.py) beat DEFAULT_FINISHER + timings_path = REPO / "tools/finisher_timings.json" + timings = json.loads(timings_path.read_text()) if timings_path.exists() else {} for cid in chars: root = REPO / "characters" / cid if not root.exists(): @@ -137,6 +140,8 @@ def main() -> None: continue merged = dict(TUNE) merged.update(CHAR_TUNE.get(cid, {})) + if cid in timings: + merged["finisher"] = timings[cid] for move, overrides in merged.items(): data_path = root / "moves" / move / "data.json" if not data_path.exists(): diff --git a/tools/tune_finishers.py b/tools/tune_finishers.py new file mode 100644 index 00000000..339d8d05 --- /dev/null +++ b/tools/tune_finishers.py @@ -0,0 +1,67 @@ +#!/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()