Combat Combos
The key words MUST, MUST NOT, SHOULD, and MAY are used as in RFC 2119.
Melee attacks chain in a 3-hit combo (per-weapon combo_steps). Chaining is governed by two-tier timing (#461) measured as a fraction of the current swing's animation — not a wall-clock timer. A single boundary, just_start, splits the swing: a press before it fumbles, a press at or after it queues the next step, which fires when the current swing completes. This preserves action commitment (#377 — a swing always fully executes, see Player State Machine).
There is no just-attack tier. An earlier draft (#155/PR #459) added a third, "just-attack" window granting a damage bonus for precise timing; Rozalin's #459 playtest found this both inaccurate to PSZ and counterintuitive (#461). Crit and damage bonuses come from character stats and equipment, not combo timing. Players use the accept window to decide when to advance the combo (reposition / stagger), not to farm a per-hit bonus.
The two tiers
frame 0 animation end
|------------|-------------|
miss-early chain-accept too-late
(fumble) (queue chain) (swing ended un-queued →
| combo broken, back to IDLE)
just_start
| Tier | Window (fraction of swing) | On attack press (MUST) |
|---|---|---|
| miss-early | [0, just_start) | Fumble — the press is NOT buffered, does not advance the combo, and locks out chaining for the remainder of this swing: every later press in the same swing MUST be ignored, so the swing ends un-queued and the combo breaks. Muted-red flash on the fumbling press. |
| chain-accept | [just_start, 1.0] | Queue the next step at standard damage (green flash; yellow for a special/strong chain). No timing-based damage bonus. |
| too-late | after animation end | Unreachable as a press — the swing ending un-queued has already broken the combo and returned to IDLE. |
- The
just_startboundary MUST come from data (WeaponComboConfigresources,data/combo_configs/), indexed by the step being chained from — per-motion tuning, not code constants. Defaultjust_start=0.55; Saber (fast) is tuned to0.45per #155. (The serialized field keeps the namejust_startfor.tresstability — it is now the single chain-accept threshold, not the start of a just window.) - Why miss-early fumbles instead of being merely ignored: the accept window spans roughly half the swing, so with an ignore-only miss-early, mashing always lands a press inside the window and chains anyway (Rozalin's #459 playtest — "can still be advanced by spamming"). The fumble makes mashing break its own combo. The lockout is per swing: the fumble flag MUST reset when the next step starts (each step is a clean chain attempt) and on any exit from ATTACKING.
- A queued step MUST fire at swing end (via
animation_finishedor, for looping animations that never emit it, the elapsed-length safety net — the mechgun-root fix generalizes to every step). Exactly one of the two paths fires per step (_attack_step_endedguard). - The swing ending with nothing queued MUST break the combo: reset to step 0, deactivate the hitbox, return to IDLE (red flash when mid-chain). There is no post-animation grace window.
- Only one queue slot: a second press while queued is a no-op — it MUST NOT re-roll or replace the queued chain.
- The final step (finisher) has no accept window — a press during it is a no-op; the finisher ends to IDLE.
- Damage-initiated exits from ATTACKING MUST clear the queue (centralized in
transition_to, with the #428 hitbox deactivation) — an interrupted swing never fires its queued follow-up. - Strong (special) attacks share the same boundary: a strong-attack press queues a special step (
_queued_combo_special), which keeps its wind-up delay and element on fire.
PSO:BB reference frames
The boundary is runtime-computed from the animation fraction — PSO:BB's motion files carry no timing events (see psobb-re, combo-timing investigation). Motion frame counts from plymotion-profile.tsv inform per-weapon tuning (e.g. Saber A1 = 19 frames → earlier accept; finishers A3 = 59–80 frames → no window). The in-repo starting values are feel-tuning defaults, expected to move via playtest.
Feedback
Queue-time flash by outcome (green = normal chain, yellow = special chain, muted red = fumbled press, red = chain broken), drawn by the combo ring and gated on the Combo Timing debug toggle exactly as before. The PSO:BB chain sounds (SE_PL_CHAIN1/2/HIT) are not in the pack yet — audio cues land as a follow-up with the next pack republish.
Combo-timing debug overlay
The Combo Timing debug toggle (DebugConfig.show_combo_timing, default OFF, toggled from the field start menu) draws the at-feet timing ring, visualizing the chain-accept window: visible in green from just_start to animation end, shrinking toward the swing's end. When the toggle is OFF it MUST hide every timing ring — both the combo ring (_combo_ring) and the element-tinted heavy/special-attack charge ring (_charge_ring); #415 fixed the charge ring leak. The heavy attack's rising particles and model glow are permanent attack VFX, not timing rings, and are unaffected by the toggle.
Implemented by
scripts/resources/weapon_combo_config.gd(WeaponComboConfig) — per-stepjust_startchain-accept fraction; instances indata/combo_configs/.scripts/autoloads/combat_manager.gd—get_combo_timing(weapon_type, from_step)resolves the resource (default config for untuned types).scripts/3d/player/player.gd(Player) —_try_queue_combo(fumble-vs-accept check on press),_attack_step_finished(single fire point at swing end),_handle_attack_state(elapsed tracking + safety net)._start_attack/_start_strong_attackroute ATTACKING-state presses into the queue.
Status: implemented (#461) — test_combo_two_tier pins the accept boundary, queue semantics, and finisher; test_combo_miss_early_fumble pins the fumble lockout; test_mechgun_final_step_no_root pins the safety net; the PSZ_AUTOPILOT_COMBO=1 probe drives a live fumble + normal-chain end-to-end.
These attacks are the ATTACKING state of the Player State Machine.