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 <noreply@anthropic.com>
This commit is contained in:
parent
e624bec379
commit
c3d682b7dd
@ -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
|
||||
]
|
||||
]
|
||||
|
||||
@ -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
|
||||
]
|
||||
]
|
||||
|
||||
@ -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
|
||||
]
|
||||
]
|
||||
|
||||
@ -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
|
||||
]
|
||||
]
|
||||
|
||||
@ -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
|
||||
]
|
||||
]
|
||||
|
||||
@ -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
|
||||
]
|
||||
]
|
||||
|
||||
@ -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
|
||||
]
|
||||
]
|
||||
|
||||
@ -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
|
||||
]
|
||||
]
|
||||
|
||||
@ -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
|
||||
]
|
||||
]
|
||||
|
||||
@ -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
|
||||
]
|
||||
]
|
||||
|
||||
@ -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
|
||||
]
|
||||
]
|
||||
|
||||
@ -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
|
||||
]
|
||||
]
|
||||
|
||||
@ -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
|
||||
]
|
||||
]
|
||||
|
||||
@ -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
|
||||
]
|
||||
]
|
||||
|
||||
324
tools/finisher_timings.json
Normal file
324
tools/finisher_timings.json
Normal file
@ -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
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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():
|
||||
|
||||
67
tools/tune_finishers.py
Normal file
67
tools/tune_finishers.py
Normal file
@ -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/<id>/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()
|
||||
Loading…
Reference in New Issue
Block a user