Summary
BLOCK_STARTUP_MS = 0 and PARRY_WINDOW_MS = 140, so the first 140ms of any
block is a parry and a guard is effective the instant the button goes down.
A defender who presses block on reaction therefore gets a parry essentially every
time, and the plain blocked outcome — which the frame data treats as the normal
case — almost never happens.
That matters because a parry is by far the strongest defensive result in the
game: the attacker is stunned for GUARD_BREAK_STUN_MS (420ms) and the defender
is handed an instant Massive Strike. It is meant to be the reward for reading a
swing, and it is currently the reward for pressing RMB late.
Evidence
meleeSummary across six separate measured runs this session — a 16-fighter
deathmatch, several canonical duels, and a 16-bot match:
| run |
blocks raised |
blockedHits |
parries |
| deathmatch (16) |
14 |
0 |
24 |
| deathmatch (16) |
8 |
1 |
19 |
| deathmatch (16) |
11 |
0 |
21 |
| duel |
3 / 10 / 4 |
0 |
0 / 0 / 1 |
| duel |
11 / 7 / 8 / 6 |
0 |
2 / 1 / 3 / 1 |
| duel |
1 / 3 / 1 |
0 |
0 |
blockedHits is 0 in every run but one, where it is 1. outcomeByMove.slash
shows the same split: blocked: 0, parried: 24.
Why it happens
// resolveMelee
if (defender.blocking && def.blockable && !behind) {
const outcome = defender.blockTimer <= PARRY_WINDOW_MS ? "parried" : "blocked";
}
blocking becomes true at blockTimer >= BLOCK_STARTUP_MS, and that constant is
0. So the reachable window for a plain block is blockTimer > 140ms — you
only get one by holding a guard up before the swing was thrown and keeping it
up. Reactive blocking, which is what both the bots and a human under pressure
actually do, lands inside the parry window every time.
Open question, deliberately not answered here
This may be tuning rather than a defect — but the numbers say the two outcomes are
not currently a choice, and the spec presents them as one. Worth deciding
explicitly:
- Give the block real startup (
BLOCK_STARTUP_MS > 0), so a guard thrown up late
is a block and only an early one parries. This is the fighting-game answer and
it makes the parry a read again.
- Or narrow
PARRY_WINDOW_MS well below the reaction time the AI achieves.
- Or accept it, and change
specs/melee.md to say the parry is the default
outcome of a successful guard — because right now the spec and the data
disagree.
Definition of done
- A decision recorded in
specs/melee.md with the number that implements it.
meleeSummary.blockedHits non-zero across a handful of canonical runs — the
counter exists precisely so "this outcome never happens" is visible, and it is
currently telling us something nobody has acted on.
- The training probe gains a scenario that produces a plain block and asserts the
outcome, alongside the existing parry one.
Notes
Found while reading meleeSummary for unrelated netcode work, which is the
counter doing its job — see docs/diagnostics.md on reading both halves of the
melee summary.
Summary
BLOCK_STARTUP_MS = 0andPARRY_WINDOW_MS = 140, so the first 140ms of anyblock is a parry and a guard is effective the instant the button goes down.
A defender who presses block on reaction therefore gets a parry essentially every
time, and the plain
blockedoutcome — which the frame data treats as the normalcase — almost never happens.
That matters because a parry is by far the strongest defensive result in the
game: the attacker is stunned for
GUARD_BREAK_STUN_MS(420ms) and the defenderis handed an instant Massive Strike. It is meant to be the reward for reading a
swing, and it is currently the reward for pressing RMB late.
Evidence
meleeSummaryacross six separate measured runs this session — a 16-fighterdeathmatch, several canonical duels, and a 16-bot match:
blockedHitsis 0 in every run but one, where it is 1.outcomeByMove.slashshows the same split:
blocked: 0, parried: 24.Why it happens
blockingbecomes true atblockTimer >= BLOCK_STARTUP_MS, and that constant is0. So the reachable window for a plain block is
blockTimer > 140ms— youonly get one by holding a guard up before the swing was thrown and keeping it
up. Reactive blocking, which is what both the bots and a human under pressure
actually do, lands inside the parry window every time.
Open question, deliberately not answered here
This may be tuning rather than a defect — but the numbers say the two outcomes are
not currently a choice, and the spec presents them as one. Worth deciding
explicitly:
BLOCK_STARTUP_MS > 0), so a guard thrown up lateis a block and only an early one parries. This is the fighting-game answer and
it makes the parry a read again.
PARRY_WINDOW_MSwell below the reaction time the AI achieves.specs/melee.mdto say the parry is the defaultoutcome of a successful guard — because right now the spec and the data
disagree.
Definition of done
specs/melee.mdwith the number that implements it.meleeSummary.blockedHitsnon-zero across a handful of canonical runs — thecounter exists precisely so "this outcome never happens" is visible, and it is
currently telling us something nobody has acted on.
outcome, alongside the existing parry one.
Notes
Found while reading
meleeSummaryfor unrelated netcode work, which is thecounter doing its job — see
docs/diagnostics.mdon reading both halves of themelee summary.