From 490a4885a44139b42b02ce8ad1a2792e2e35303e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Rom=C3=A1n=20N=C3=BA=C3=B1ez?= Date: Tue, 29 Sep 2026 21:55:16 +0200 Subject: [PATCH] Fix command persistence through jump squat --- src/fighter/state/air_attack.rs | 3 + src/fighter/state/ground_attack.rs | 3 + src/input/buffer.rs | 92 +++++++++++++++++++++++------- 3 files changed, 78 insertions(+), 20 deletions(-) diff --git a/src/fighter/state/air_attack.rs b/src/fighter/state/air_attack.rs index e523c0e..dcd0c71 100644 --- a/src/fighter/state/air_attack.rs +++ b/src/fighter/state/air_attack.rs @@ -45,6 +45,9 @@ impl FighterStateImpl for AirAttackState { } fn on_enter(&mut self, state_context: &mut EnterContext) { + state_context + .input + .consume_command(crate::input::FighterCommands::Attack); state_context.play_animation(self.attack_kind.get_animation(), false); let script = state_context.attack_script_assets.scripts[&self.attack_kind].clone(); state_context.hitboxes.0 = Some(crate::fighter::FighterActiveAttack { diff --git a/src/fighter/state/ground_attack.rs b/src/fighter/state/ground_attack.rs index 87d1241..ee40146 100644 --- a/src/fighter/state/ground_attack.rs +++ b/src/fighter/state/ground_attack.rs @@ -42,6 +42,9 @@ impl FighterStateImpl for GroundAttackState { } fn on_enter(&mut self, state_context: &mut EnterContext) { + state_context + .input + .consume_command(crate::input::FighterCommands::Attack); state_context.play_animation(self.attack_kind.get_animation(), false); let script = state_context.attack_script_assets.scripts[&self.attack_kind].clone(); state_context.hitboxes.0 = Some(crate::fighter::FighterActiveAttack { diff --git a/src/input/buffer.rs b/src/input/buffer.rs index 8c79ac4..41ee2f2 100644 --- a/src/input/buffer.rs +++ b/src/input/buffer.rs @@ -101,6 +101,8 @@ impl FighterAttackCommandType { struct CommandSlot { lifetime: u8, age: u16, + hold_remaining: u16, + hold_latched: bool, consumed: bool, } @@ -109,6 +111,8 @@ impl Default for CommandSlot { Self { lifetime: 0, age: 0, + hold_remaining: 0, + hold_latched: false, consumed: false, } } @@ -220,6 +224,8 @@ impl FighterInput { pub fn clear_command(&mut self, command: FighterCommands) { let slot = &mut self.command_slots[command.index()]; slot.lifetime = 0; + slot.hold_remaining = 0; + slot.hold_latched = false; slot.consumed = true; } @@ -236,7 +242,10 @@ impl FighterInput { let slot = &mut self.command_slots[command.index()]; slot.lifetime = lifetime; slot.age = 0; + slot.hold_remaining = 0; + slot.hold_latched = false; slot.consumed = false; + self.latch_if_hold_enabled(command); } pub fn has_command(&self, command: FighterCommands) -> bool { @@ -252,8 +261,9 @@ impl FighterInput { pub fn enable_hold_all(&mut self, max_frames: u16) { self.hold_all = HoldLimit { enabled: true, - max_frames, + max_frames: if max_frames == 0 { u16::MAX } else { max_frames }, }; + self.latch_active_commands(); } pub fn disable_hold_all(&mut self) { @@ -263,14 +273,35 @@ impl FighterInput { pub fn enable_hold(&mut self, command: FighterCommands, max_frames: u16) { self.hold_limits[command.index()] = HoldLimit { enabled: true, - max_frames, + max_frames: if max_frames == 0 { u16::MAX } else { max_frames }, }; + if self.has_command(command) { + self.latch_if_hold_enabled(command); + } } pub fn disable_hold(&mut self, command: FighterCommands) { self.hold_limits[command.index()] = HoldLimit::default(); } + fn latch_if_hold_enabled(&mut self, command: FighterCommands) { + let limit = self.hold_limit(command); + if !limit.enabled { + return; + } + let slot = &mut self.command_slots[command.index()]; + slot.hold_latched = true; + slot.hold_remaining = limit.max_frames; + } + + fn latch_active_commands(&mut self) { + for command in FighterCommands::iter() { + if self.has_command(command) { + self.latch_if_hold_enabled(command); + } + } + } + pub fn is_pressed(&self, action: DigitalAction) -> bool { self.current_frame .bindings_for(action) @@ -317,15 +348,19 @@ impl FighterInput { } } - fn update_hold(&mut self, command: FighterCommands, source_held: bool) { + fn update_hold(&mut self, command: FighterCommands) { let index = command.index(); - let limit = self.hold_limit(command); let slot = &mut self.command_slots[index]; slot.age = slot.age.saturating_add(1); - if slot.lifetime == 0 && source_held && !slot.consumed { - if limit.enabled && (limit.max_frames == 0 || slot.age < limit.max_frames) { + if slot.hold_latched && slot.hold_remaining != 0 { + if slot.hold_remaining != u16::MAX { + slot.hold_remaining = slot.hold_remaining.saturating_sub(1); + } + if slot.lifetime == 0 { slot.lifetime = 1; } + } else if slot.hold_latched { + slot.hold_latched = false; } } } @@ -463,17 +498,14 @@ pub fn postprocess_input( // Extend only commands whose source is still held. Recognition above // has already reset age and cleared the consumed marker for new edges. - let jump_held = input.current_frame.is_held(DigitalAction::Jump); - let shield_held = input.current_frame.is_held(DigitalAction::Shield); - let attack_held = input.current_frame.is_held(DigitalAction::Attack); - input.update_hold(FighterCommands::Jump, jump_held); - input.update_hold(FighterCommands::Shield, shield_held); - input.update_hold(FighterCommands::GuardHold, shield_now.count() >= 2); - input.update_hold(FighterCommands::Attack, attack_held); - input.update_hold(FighterCommands::SmashMoveUp, false); - input.update_hold(FighterCommands::SmashMoveDown, false); - input.update_hold(FighterCommands::SmashMoveLeft, false); - input.update_hold(FighterCommands::SmashMoveRight, false); + input.update_hold(FighterCommands::Jump); + input.update_hold(FighterCommands::Shield); + input.update_hold(FighterCommands::GuardHold); + input.update_hold(FighterCommands::Attack); + input.update_hold(FighterCommands::SmashMoveUp); + input.update_hold(FighterCommands::SmashMoveDown); + input.update_hold(FighterCommands::SmashMoveLeft); + input.update_hold(FighterCommands::SmashMoveRight); } } @@ -511,12 +543,32 @@ mod tests { let mut input = FighterInput::default(); input.enable_hold(FighterCommands::Shield, 10); input.set_lifetime(FighterCommands::Shield, 1); - input.update_hold(FighterCommands::Shield, true); + input.update_hold(FighterCommands::Shield); input.reduce_command_lifetime(FighterCommands::Shield); - input.update_hold(FighterCommands::Shield, true); + input.update_hold(FighterCommands::Shield); assert!(input.has_command(FighterCommands::Shield)); input.consume_command(FighterCommands::Shield); - input.update_hold(FighterCommands::Shield, true); + input.update_hold(FighterCommands::Shield); assert!(!input.has_command(FighterCommands::Shield)); } + + #[test] + fn hold_buffer_keeps_released_attack_through_jump_squat() { + let mut input = FighterInput::default(); + input.set_lifetime(FighterCommands::Attack, 3); + input.enable_hold_all(10); + + // The command remains latched even though its physical source is no + // longer held. Five simulation ticks cover the test fighter's squat. + for _ in 0..5 { + for command in FighterCommands::iter() { + input.reduce_command_lifetime(command); + } + input.update_hold(FighterCommands::Attack); + } + + assert!(input.has_command(FighterCommands::Attack)); + input.disable_hold_all(); + assert!(input.has_command(FighterCommands::Attack)); + } }