Rework input buffer to match HDR closer (#79)
## Summary - Preserve distinct same-action bindings in rollback input frames. - Add independent command lifetimes with tap and state-scoped hold buffering. - Detect second shield bindings and consume shield commands for air dodge. - Fix duplicate keyboard mappings being overwritten. Reviewed-on: #79 Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
This commit was merged in pull request #79.
This commit is contained in:
+497
-141
@@ -1,8 +1,4 @@
|
||||
//! Per-fighter input state: the current and previous frame, plus the buffered
|
||||
//! commands (smash inputs and friends) that states query when deciding whether
|
||||
//! to interrupt.
|
||||
|
||||
use std::ops::{Index, IndexMut};
|
||||
//! Rollback-safe input history and command buffering.
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy_ggrs::{PlayerInputs, Rollback};
|
||||
@@ -12,14 +8,15 @@ use strum_macros::EnumIter;
|
||||
use crate::fighter::FighterFacingDirection;
|
||||
use crate::fighter::attack::AttackKind;
|
||||
use crate::game_settings::GameSettings;
|
||||
use crate::input::FighterInputFrame;
|
||||
use crate::input::{DigitalAction, FighterInputFrame};
|
||||
use crate::netcode::GGRSCfg;
|
||||
use crate::player::Player;
|
||||
|
||||
#[derive(Clone, Copy, EnumIter, Debug)]
|
||||
#[derive(Clone, Copy, EnumIter, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum FighterCommands {
|
||||
Jump,
|
||||
Shield,
|
||||
GuardHold,
|
||||
Attack,
|
||||
SmashMoveUp,
|
||||
SmashMoveDown,
|
||||
@@ -27,7 +24,31 @@ pub enum FighterCommands {
|
||||
SmashMoveRight,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Hash, Copy)]
|
||||
pub(crate) fn second_binding_pressed(
|
||||
current: crate::input::ActionBindingMask,
|
||||
previous: crate::input::ActionBindingMask,
|
||||
) -> bool {
|
||||
current.count() >= 2 && current.pressed_since(previous).0 != 0
|
||||
}
|
||||
|
||||
impl FighterCommands {
|
||||
pub const COUNT: usize = 8;
|
||||
|
||||
pub const fn index(self) -> usize {
|
||||
match self {
|
||||
Self::Jump => 0,
|
||||
Self::Shield => 1,
|
||||
Self::GuardHold => 2,
|
||||
Self::Attack => 3,
|
||||
Self::SmashMoveUp => 4,
|
||||
Self::SmashMoveDown => 5,
|
||||
Self::SmashMoveLeft => 6,
|
||||
Self::SmashMoveRight => 7,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Hash, Copy, Debug)]
|
||||
pub enum FighterAttackCommandType {
|
||||
#[default]
|
||||
Neutral,
|
||||
@@ -44,86 +65,140 @@ pub enum FighterAttackCommandType {
|
||||
impl FighterAttackCommandType {
|
||||
pub fn to_attack_kind_grounded(&self) -> AttackKind {
|
||||
match self {
|
||||
FighterAttackCommandType::Neutral => AttackKind::Jab,
|
||||
FighterAttackCommandType::SmashAttackUp => AttackKind::UpSmash,
|
||||
FighterAttackCommandType::SmashAttackDown => AttackKind::DownSmash,
|
||||
FighterAttackCommandType::SmashAttackLeft => AttackKind::ForwardSmash,
|
||||
FighterAttackCommandType::SmashAttackRight => AttackKind::ForwardSmash,
|
||||
FighterAttackCommandType::TiltAttackUp => AttackKind::UpTilt,
|
||||
FighterAttackCommandType::TiltAttackDown => AttackKind::DownTilt,
|
||||
FighterAttackCommandType::TiltAttackLeft => AttackKind::ForwardTilt,
|
||||
FighterAttackCommandType::TiltAttackRight => AttackKind::ForwardTilt,
|
||||
Self::Neutral => AttackKind::Jab,
|
||||
Self::SmashAttackUp => AttackKind::UpSmash,
|
||||
Self::SmashAttackDown => AttackKind::DownSmash,
|
||||
Self::SmashAttackLeft | Self::SmashAttackRight => AttackKind::ForwardSmash,
|
||||
Self::TiltAttackUp => AttackKind::UpTilt,
|
||||
Self::TiltAttackDown => AttackKind::DownTilt,
|
||||
Self::TiltAttackLeft | Self::TiltAttackRight => AttackKind::ForwardTilt,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_attack_kind_airborne(&self, facing_direction: &FighterFacingDirection) -> AttackKind {
|
||||
match self {
|
||||
FighterAttackCommandType::Neutral => AttackKind::NeutralAir,
|
||||
FighterAttackCommandType::SmashAttackUp | FighterAttackCommandType::TiltAttackUp => AttackKind::UpAir,
|
||||
FighterAttackCommandType::SmashAttackDown | FighterAttackCommandType::TiltAttackDown => AttackKind::DownAir,
|
||||
|
||||
FighterAttackCommandType::SmashAttackLeft | FighterAttackCommandType::TiltAttackLeft => {
|
||||
match facing_direction {
|
||||
FighterFacingDirection::Left => AttackKind::ForwardAir,
|
||||
FighterFacingDirection::Right => AttackKind::BackAir,
|
||||
}
|
||||
Self::Neutral => AttackKind::NeutralAir,
|
||||
Self::SmashAttackUp | Self::TiltAttackUp => AttackKind::UpAir,
|
||||
Self::SmashAttackDown | Self::TiltAttackDown => AttackKind::DownAir,
|
||||
Self::SmashAttackLeft | Self::TiltAttackLeft => match facing_direction {
|
||||
FighterFacingDirection::Left => AttackKind::ForwardAir,
|
||||
FighterFacingDirection::Right => AttackKind::BackAir,
|
||||
},
|
||||
FighterAttackCommandType::SmashAttackRight | FighterAttackCommandType::TiltAttackRight => {
|
||||
match facing_direction {
|
||||
FighterFacingDirection::Left => AttackKind::BackAir,
|
||||
FighterFacingDirection::Right => AttackKind::ForwardAir,
|
||||
}
|
||||
Self::SmashAttackRight | Self::TiltAttackRight => match facing_direction {
|
||||
FighterFacingDirection::Left => AttackKind::BackAir,
|
||||
FighterFacingDirection::Right => AttackKind::ForwardAir,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Frames of life remaining for each buffered command. Indexed by
|
||||
/// [`FighterCommands`] so adding a command is a two-line change here.
|
||||
#[derive(Default, Clone, Copy, Hash)]
|
||||
pub struct FighterCommandLifetimes {
|
||||
smash_move_left: u8,
|
||||
smash_move_right: u8,
|
||||
smash_move_down: u8,
|
||||
smash_move_up: u8,
|
||||
jump: u8,
|
||||
shield: u8,
|
||||
attack: u8,
|
||||
#[derive(Clone, Copy, Debug, Hash)]
|
||||
struct CommandSlot {
|
||||
lifetime: u8,
|
||||
age: u16,
|
||||
hold_remaining: HoldLimit,
|
||||
consumed: bool,
|
||||
}
|
||||
|
||||
impl Index<FighterCommands> for FighterCommandLifetimes {
|
||||
type Output = u8;
|
||||
|
||||
fn index(&self, command: FighterCommands) -> &Self::Output {
|
||||
match command {
|
||||
FighterCommands::SmashMoveLeft => &self.smash_move_left,
|
||||
FighterCommands::SmashMoveRight => &self.smash_move_right,
|
||||
FighterCommands::SmashMoveDown => &self.smash_move_down,
|
||||
FighterCommands::SmashMoveUp => &self.smash_move_up,
|
||||
FighterCommands::Jump => &self.jump,
|
||||
FighterCommands::Shield => &self.shield,
|
||||
FighterCommands::Attack => &self.attack,
|
||||
impl Default for CommandSlot {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
lifetime: 0,
|
||||
age: 0,
|
||||
hold_remaining: HoldLimit::Disabled,
|
||||
consumed: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexMut<FighterCommands> for FighterCommandLifetimes {
|
||||
fn index_mut(&mut self, command: FighterCommands) -> &mut Self::Output {
|
||||
match command {
|
||||
FighterCommands::SmashMoveLeft => &mut self.smash_move_left,
|
||||
FighterCommands::SmashMoveRight => &mut self.smash_move_right,
|
||||
FighterCommands::SmashMoveDown => &mut self.smash_move_down,
|
||||
FighterCommands::SmashMoveUp => &mut self.smash_move_up,
|
||||
FighterCommands::Jump => &mut self.jump,
|
||||
FighterCommands::Shield => &mut self.shield,
|
||||
FighterCommands::Attack => &mut self.attack,
|
||||
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
|
||||
enum HoldLimit {
|
||||
#[default]
|
||||
Disabled,
|
||||
Frames(u16),
|
||||
Infinite,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
|
||||
enum HoldOverride {
|
||||
#[default]
|
||||
Inherit,
|
||||
Disabled,
|
||||
Frames(u16),
|
||||
Infinite,
|
||||
}
|
||||
|
||||
impl HoldOverride {
|
||||
fn from_max_frames(max_frames: u16) -> Self {
|
||||
if max_frames == 0 {
|
||||
Self::Infinite
|
||||
} else {
|
||||
Self::Frames(max_frames)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Default, Clone, Copy, Hash)]
|
||||
impl HoldLimit {
|
||||
fn from_max_frames(max_frames: u16) -> Self {
|
||||
if max_frames == 0 {
|
||||
Self::Infinite
|
||||
} else {
|
||||
Self::Frames(max_frames)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Hash)]
|
||||
struct ButtonHistory {
|
||||
trigger_age: u16,
|
||||
release_age: u16,
|
||||
held_frames: u16,
|
||||
secondary_trigger_age: u16,
|
||||
}
|
||||
|
||||
impl Default for ButtonHistory {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
trigger_age: u16::MAX,
|
||||
release_age: u16::MAX,
|
||||
held_frames: 0,
|
||||
secondary_trigger_age: u16::MAX,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ButtonHistory {
|
||||
fn update(&mut self, current: u16, previous: u16) {
|
||||
let pressed = current & !previous;
|
||||
let released = previous & !current;
|
||||
self.trigger_age = self.trigger_age.saturating_add(1);
|
||||
self.release_age = self.release_age.saturating_add(1);
|
||||
self.secondary_trigger_age = self.secondary_trigger_age.saturating_add(1);
|
||||
if pressed != 0 {
|
||||
self.trigger_age = 0;
|
||||
}
|
||||
if released != 0 {
|
||||
self.release_age = 0;
|
||||
}
|
||||
if current != 0 {
|
||||
self.held_frames = self.held_frames.saturating_add(1);
|
||||
} else {
|
||||
self.held_frames = 0;
|
||||
}
|
||||
if current.count_ones() >= 2 && (pressed != 0 || previous.count_ones() < 2) {
|
||||
self.secondary_trigger_age = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-fighter input state. Every field is deterministic rollback state.
|
||||
#[derive(Component, Clone, Copy, Hash)]
|
||||
#[require(Rollback)]
|
||||
pub struct FighterInput {
|
||||
command_lifetimes: FighterCommandLifetimes,
|
||||
command_slots: [CommandSlot; FighterCommands::COUNT],
|
||||
hold_limits: [HoldOverride; FighterCommands::COUNT],
|
||||
hold_all: HoldLimit,
|
||||
button_history: [ButtonHistory; 3],
|
||||
prev_frame: FighterInputFrame,
|
||||
current_frame: FighterInputFrame,
|
||||
frames_in_x_flick_deadzone: u8,
|
||||
@@ -131,6 +206,22 @@ pub struct FighterInput {
|
||||
attack_command: FighterAttackCommandType,
|
||||
}
|
||||
|
||||
impl Default for FighterInput {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
command_slots: [CommandSlot::default(); FighterCommands::COUNT],
|
||||
hold_limits: [HoldOverride::default(); FighterCommands::COUNT],
|
||||
hold_all: HoldLimit::default(),
|
||||
button_history: [ButtonHistory::default(); 3],
|
||||
prev_frame: FighterInputFrame::default(),
|
||||
current_frame: FighterInputFrame::default(),
|
||||
frames_in_x_flick_deadzone: 0xFE,
|
||||
frames_in_y_flick_deadzone: 0xFE,
|
||||
attack_command: FighterAttackCommandType::Neutral,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FighterInput {
|
||||
pub fn get_last_frame(&self) -> FighterInputFrame {
|
||||
self.current_frame
|
||||
@@ -142,40 +233,188 @@ impl FighterInput {
|
||||
}
|
||||
|
||||
pub fn clear_buffer(&mut self) {
|
||||
self.command_lifetimes = FighterCommandLifetimes::default()
|
||||
self.command_slots = [CommandSlot::default(); FighterCommands::COUNT];
|
||||
}
|
||||
|
||||
pub fn clear_command(&mut self, command: FighterCommands) {
|
||||
self.command_lifetimes[command] = 0;
|
||||
let slot = &mut self.command_slots[command.index()];
|
||||
slot.lifetime = 0;
|
||||
slot.hold_remaining = HoldLimit::Disabled;
|
||||
slot.consumed = true;
|
||||
}
|
||||
|
||||
pub fn consume_command(&mut self, command: FighterCommands) -> bool {
|
||||
if self.has_command(command) {
|
||||
self.clear_command(command);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_lifetime(&mut self, command: FighterCommands, lifetime: u8) {
|
||||
self.command_lifetimes[command] = lifetime;
|
||||
let slot = &mut self.command_slots[command.index()];
|
||||
slot.lifetime = lifetime;
|
||||
slot.age = 0;
|
||||
slot.hold_remaining = HoldLimit::Disabled;
|
||||
slot.consumed = false;
|
||||
self.latch_if_hold_enabled(command);
|
||||
}
|
||||
|
||||
pub fn has_command(&self, command: FighterCommands) -> bool {
|
||||
self.command_lifetimes[command] > 0
|
||||
self.command_slots[command.index()].lifetime > 0
|
||||
}
|
||||
|
||||
pub fn reduce_command_lifetime(&mut self, command: FighterCommands) {
|
||||
self.command_lifetimes[command] = self.command_lifetimes[command].saturating_sub(1);
|
||||
self.command_slots[command.index()].lifetime = self.command_slots[command.index()]
|
||||
.lifetime
|
||||
.saturating_sub(1);
|
||||
}
|
||||
|
||||
/// Holds all inputs for n frames
|
||||
pub fn enable_hold_all(&mut self, max_frames: u16) {
|
||||
self.hold_all = HoldLimit::from_max_frames(max_frames);
|
||||
self.latch_active_commands();
|
||||
}
|
||||
|
||||
pub fn disable_hold_all(&mut self) {
|
||||
self.hold_all = HoldLimit::Disabled;
|
||||
}
|
||||
|
||||
/// Stops global enrollment and immediately cancels every latched command.
|
||||
pub fn cancel_hold_all(&mut self) {
|
||||
self.disable_hold_all();
|
||||
for slot in &mut self.command_slots {
|
||||
slot.hold_remaining = HoldLimit::Disabled;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enable_hold(&mut self, command: FighterCommands, max_frames: u16) {
|
||||
self.hold_limits[command.index()] = HoldOverride::from_max_frames(max_frames);
|
||||
if self.has_command(command) {
|
||||
self.latch_if_hold_enabled(command);
|
||||
}
|
||||
}
|
||||
|
||||
/// Explicitly prevents this command from using either command-specific or
|
||||
/// global hold buffering, while preserving an existing latch.
|
||||
pub fn disable_hold(&mut self, command: FighterCommands) {
|
||||
self.hold_limits[command.index()] = HoldOverride::Disabled;
|
||||
}
|
||||
|
||||
/// Removes the command-specific policy so this command inherits the
|
||||
/// global policy again.
|
||||
pub fn reset_hold_override(&mut self, command: FighterCommands) {
|
||||
self.hold_limits[command.index()] = HoldOverride::Inherit;
|
||||
}
|
||||
|
||||
/// Stops enrollment and immediately cancels this command's hold lifetime.
|
||||
pub fn cancel_hold(&mut self, command: FighterCommands) {
|
||||
self.disable_hold(command);
|
||||
self.command_slots[command.index()].hold_remaining = HoldLimit::Disabled;
|
||||
}
|
||||
|
||||
fn latch_if_hold_enabled(&mut self, command: FighterCommands) {
|
||||
let limit = self.hold_limit(command);
|
||||
let slot = &mut self.command_slots[command.index()];
|
||||
slot.hold_remaining = match limit {
|
||||
HoldLimit::Disabled => return,
|
||||
HoldLimit::Infinite => HoldLimit::Infinite,
|
||||
// The limit includes tap-buffer frames already elapsed.
|
||||
HoldLimit::Frames(max_frames) => {
|
||||
let remaining = max_frames.saturating_sub(slot.age);
|
||||
if remaining == 0 {
|
||||
return;
|
||||
}
|
||||
HoldLimit::Frames(remaining)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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)
|
||||
.pressed_since(self.prev_frame.bindings_for(action))
|
||||
.0
|
||||
!= 0
|
||||
}
|
||||
|
||||
pub fn is_released(&self, action: DigitalAction) -> bool {
|
||||
self.current_frame
|
||||
.bindings_for(action)
|
||||
.released_since(self.prev_frame.bindings_for(action))
|
||||
.0
|
||||
!= 0
|
||||
}
|
||||
|
||||
pub fn pressed_bindings(&self, action: DigitalAction) -> u16 {
|
||||
self.current_frame
|
||||
.bindings_for(action)
|
||||
.pressed_since(self.prev_frame.bindings_for(action))
|
||||
.0
|
||||
}
|
||||
|
||||
pub fn binding_count(&self, action: DigitalAction) -> u32 {
|
||||
self.current_frame.bindings_for(action).count()
|
||||
}
|
||||
|
||||
pub fn get_frames_in_smash_move_deadzone(&self) -> u8 {
|
||||
self.frames_in_x_flick_deadzone
|
||||
}
|
||||
|
||||
pub fn set_attack_command_type(&mut self, command: FighterAttackCommandType) {
|
||||
self.attack_command = command;
|
||||
}
|
||||
|
||||
pub fn get_attack_command_type(&self) -> FighterAttackCommandType {
|
||||
self.attack_command
|
||||
}
|
||||
|
||||
fn hold_limit(&self, command: FighterCommands) -> HoldLimit {
|
||||
let specific = self.hold_limits[command.index()];
|
||||
match specific {
|
||||
HoldOverride::Inherit => self.hold_all,
|
||||
HoldOverride::Disabled => HoldLimit::Disabled,
|
||||
HoldOverride::Frames(frames) => HoldLimit::Frames(frames),
|
||||
HoldOverride::Infinite => HoldLimit::Infinite,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_hold(&mut self, command: FighterCommands) {
|
||||
let index = command.index();
|
||||
let slot = &mut self.command_slots[index];
|
||||
slot.age = slot.age.saturating_add(1);
|
||||
match slot.hold_remaining {
|
||||
HoldLimit::Disabled => {}
|
||||
HoldLimit::Infinite => {
|
||||
if slot.lifetime == 0 {
|
||||
slot.lifetime = 1;
|
||||
}
|
||||
}
|
||||
HoldLimit::Frames(remaining) => {
|
||||
// This tick consumes one of the total persistent frames, but
|
||||
// the command remains active for the tick that consumes the
|
||||
// final frame.
|
||||
let remaining = remaining.saturating_sub(1);
|
||||
slot.hold_remaining = if remaining == 0 {
|
||||
HoldLimit::Disabled
|
||||
} else {
|
||||
HoldLimit::Frames(remaining)
|
||||
};
|
||||
if slot.lifetime == 0 {
|
||||
slot.lifetime = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Turns this frame's rolled-back inputs into per-fighter [`FighterInput`]
|
||||
/// state. Runs in `GameplaySet::Input`, before any state looks at it.
|
||||
pub fn postprocess_input(
|
||||
query: Query<(&mut FighterInput, &Player)>,
|
||||
inputs: Res<PlayerInputs<GGRSCfg>>,
|
||||
@@ -187,95 +426,95 @@ pub fn postprocess_input(
|
||||
}
|
||||
|
||||
input.push_input_frame(inputs[player.handle].0);
|
||||
let current_bindings = input.current_frame.bindings;
|
||||
let previous_bindings = input.prev_frame.bindings;
|
||||
for (history, action) in input.button_history.iter_mut().zip([
|
||||
DigitalAction::Jump,
|
||||
DigitalAction::Shield,
|
||||
DigitalAction::Attack,
|
||||
]) {
|
||||
history.update(
|
||||
current_bindings.get(action).0,
|
||||
previous_bindings.get(action).0,
|
||||
);
|
||||
}
|
||||
|
||||
// Directional smash detection
|
||||
let x_abs = input.current_frame.movement.x.abs();
|
||||
let y_abs = input.current_frame.movement.y.abs();
|
||||
|
||||
let reset_deadzone = game_settings.input_common.smash_input_reset_axis_threshold;
|
||||
let axis_threshold = game_settings.input_common.smash_input_axis_threshold;
|
||||
let frame_threshold = game_settings.input_common.smash_input_frame_threshold;
|
||||
|
||||
if x_abs < reset_deadzone {
|
||||
input.frames_in_x_flick_deadzone = 0xFE;
|
||||
} else if input.prev_frame.movement.x.abs() < reset_deadzone
|
||||
|| input.prev_frame.movement.x.signum() != input.current_frame.movement.x.signum()
|
||||
{
|
||||
input.frames_in_x_flick_deadzone = 1;
|
||||
} else {
|
||||
let prev_x_abs = input.prev_frame.movement.x.abs();
|
||||
if prev_x_abs < reset_deadzone
|
||||
|| input.prev_frame.movement.x.signum() != input.current_frame.movement.x.signum()
|
||||
{
|
||||
input.frames_in_x_flick_deadzone = 1;
|
||||
} else {
|
||||
input.frames_in_x_flick_deadzone =
|
||||
input.frames_in_x_flick_deadzone.saturating_add(1);
|
||||
}
|
||||
input.frames_in_x_flick_deadzone = input.frames_in_x_flick_deadzone.saturating_add(1);
|
||||
}
|
||||
|
||||
if y_abs < reset_deadzone {
|
||||
input.frames_in_y_flick_deadzone = 0xFE;
|
||||
} else if input.prev_frame.movement.y.abs() < reset_deadzone
|
||||
|| input.prev_frame.movement.y.signum() != input.current_frame.movement.y.signum()
|
||||
{
|
||||
input.frames_in_y_flick_deadzone = 1;
|
||||
} else {
|
||||
let prev_y_abs = input.prev_frame.movement.y.abs();
|
||||
if prev_y_abs < reset_deadzone
|
||||
|| input.prev_frame.movement.y.signum() != input.current_frame.movement.y.signum()
|
||||
{
|
||||
input.frames_in_y_flick_deadzone = 1;
|
||||
} else {
|
||||
input.frames_in_y_flick_deadzone =
|
||||
input.frames_in_y_flick_deadzone.saturating_add(1);
|
||||
}
|
||||
input.frames_in_y_flick_deadzone = input.frames_in_y_flick_deadzone.saturating_add(1);
|
||||
}
|
||||
|
||||
if !input.prev_frame.jump && input.current_frame.jump {
|
||||
input.set_lifetime(
|
||||
FighterCommands::Jump,
|
||||
game_settings.input_common.input_buffer_size,
|
||||
);
|
||||
let tap = game_settings.input_common.input_buffer_size;
|
||||
let jump_pressed = input.is_pressed(DigitalAction::Jump);
|
||||
let shield_pressed = input.is_pressed(DigitalAction::Shield);
|
||||
let attack_pressed = input.is_pressed(DigitalAction::Attack);
|
||||
if jump_pressed {
|
||||
input.set_lifetime(FighterCommands::Jump, tap);
|
||||
}
|
||||
if shield_pressed {
|
||||
input.set_lifetime(FighterCommands::Shield, tap);
|
||||
}
|
||||
|
||||
if !input.prev_frame.shield && input.current_frame.shield {
|
||||
input.set_lifetime(
|
||||
FighterCommands::Shield,
|
||||
game_settings.input_common.input_buffer_size,
|
||||
);
|
||||
}
|
||||
|
||||
if !input.prev_frame.attack && input.current_frame.attack {
|
||||
input.set_lifetime(
|
||||
FighterCommands::Attack,
|
||||
game_settings.input_common.input_buffer_size,
|
||||
);
|
||||
let shield_now = input.current_frame.bindings_for(DigitalAction::Shield);
|
||||
let shield_previous = input.prev_frame.bindings_for(DigitalAction::Shield);
|
||||
let second_shield_pressed = second_binding_pressed(shield_now, shield_previous);
|
||||
if second_shield_pressed {
|
||||
input.set_lifetime(FighterCommands::GuardHold, tap);
|
||||
}
|
||||
|
||||
let mut y_flick_detected =
|
||||
y_abs >= axis_threshold && input.frames_in_y_flick_deadzone <= frame_threshold;
|
||||
let mut x_flick_detected =
|
||||
x_abs >= axis_threshold && input.frames_in_x_flick_deadzone <= frame_threshold;
|
||||
|
||||
if y_flick_detected {
|
||||
input.clear_command(FighterCommands::SmashMoveUp);
|
||||
input.clear_command(FighterCommands::SmashMoveDown);
|
||||
|
||||
input.frames_in_y_flick_deadzone = 0xFF;
|
||||
|
||||
let command_to_set = if input.current_frame.movement.y > 0.0 {
|
||||
let command = if input.current_frame.movement.y > 0.0 {
|
||||
FighterCommands::SmashMoveUp
|
||||
} else {
|
||||
FighterCommands::SmashMoveDown
|
||||
};
|
||||
|
||||
input.set_lifetime(command_to_set, game_settings.input_common.input_buffer_size);
|
||||
input.set_lifetime(command, tap);
|
||||
}
|
||||
if x_flick_detected {
|
||||
input.clear_command(FighterCommands::SmashMoveLeft);
|
||||
input.clear_command(FighterCommands::SmashMoveRight);
|
||||
input.frames_in_x_flick_deadzone = 0xFF;
|
||||
let command = if input.current_frame.movement.x > 0.0 {
|
||||
FighterCommands::SmashMoveRight
|
||||
} else {
|
||||
FighterCommands::SmashMoveLeft
|
||||
};
|
||||
input.set_lifetime(command, tap);
|
||||
}
|
||||
|
||||
// if both flicks are detected, only one wins
|
||||
|
||||
if x_flick_detected && y_flick_detected {
|
||||
x_flick_detected = x_abs > y_abs;
|
||||
y_flick_detected = !x_flick_detected;
|
||||
}
|
||||
|
||||
let attack_detected = input.current_frame.attack && !input.prev_frame.attack;
|
||||
|
||||
// Tilt/smash up/down
|
||||
if attack_detected {
|
||||
if attack_pressed {
|
||||
let attack_type = if y_flick_detected {
|
||||
if input.current_frame.movement.y > 0.0 {
|
||||
FighterAttackCommandType::SmashAttackUp
|
||||
@@ -303,25 +542,142 @@ pub fn postprocess_input(
|
||||
} else {
|
||||
FighterAttackCommandType::Neutral
|
||||
};
|
||||
|
||||
input.set_lifetime(
|
||||
FighterCommands::Attack,
|
||||
game_settings.input_common.input_buffer_size,
|
||||
);
|
||||
input.set_lifetime(FighterCommands::Attack, tap);
|
||||
input.set_attack_command_type(attack_type);
|
||||
}
|
||||
|
||||
if x_flick_detected {
|
||||
input.clear_command(FighterCommands::SmashMoveLeft);
|
||||
input.clear_command(FighterCommands::SmashMoveRight);
|
||||
|
||||
let command_to_set = if input.current_frame.movement.x > 0.0 {
|
||||
FighterCommands::SmashMoveRight
|
||||
} else {
|
||||
FighterCommands::SmashMoveLeft
|
||||
};
|
||||
|
||||
input.set_lifetime(command_to_set, game_settings.input_common.input_buffer_size);
|
||||
for command in FighterCommands::iter() {
|
||||
input.update_hold(command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::input::ActionBindingMask;
|
||||
|
||||
fn advance_command(input: &mut FighterInput, command: FighterCommands) {
|
||||
input.reduce_command_lifetime(command);
|
||||
input.update_hold(command);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn second_binding_is_an_edge_not_a_level() {
|
||||
let first = ActionBindingMask(0b0001);
|
||||
let both = ActionBindingMask(0b0011);
|
||||
assert!(second_binding_pressed(both, first));
|
||||
assert!(!second_binding_pressed(both, both));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replacing_a_binding_still_triggers_the_combination() {
|
||||
let previous = ActionBindingMask(0b0011);
|
||||
let current = ActionBindingMask(0b0101);
|
||||
assert!(second_binding_pressed(current, previous));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_can_be_consumed_without_erasing_raw_input() {
|
||||
let mut input = FighterInput::default();
|
||||
input.set_lifetime(FighterCommands::GuardHold, 3);
|
||||
assert!(input.consume_command(FighterCommands::GuardHold));
|
||||
assert!(!input.has_command(FighterCommands::GuardHold));
|
||||
assert_eq!(input.get_last_frame(), FighterInputFrame::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hold_extension_does_not_resurrect_consumed_commands() {
|
||||
let mut input = FighterInput::default();
|
||||
input.enable_hold(FighterCommands::Shield, 10);
|
||||
input.set_lifetime(FighterCommands::Shield, 1);
|
||||
input.update_hold(FighterCommands::Shield);
|
||||
input.reduce_command_lifetime(FighterCommands::Shield);
|
||||
input.update_hold(FighterCommands::Shield);
|
||||
assert!(input.has_command(FighterCommands::Shield));
|
||||
input.consume_command(FighterCommands::Shield);
|
||||
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);
|
||||
// Recognition happened before jump squat enabled persistence.
|
||||
input.update_hold(FighterCommands::Attack);
|
||||
input.enable_hold_all(10);
|
||||
|
||||
// Five simulation ticks cover the test fighter's jump squat. The
|
||||
// physical attack binding is released, but persistence belongs to the
|
||||
// recognized command rather than the binding's held state.
|
||||
for _ in 0..5 {
|
||||
advance_command(&mut input, FighterCommands::Attack);
|
||||
}
|
||||
|
||||
assert!(input.has_command(FighterCommands::Attack));
|
||||
// Jump-squat exit stops enrollment. The latch crosses into Jump and
|
||||
// remains visible on its first airborne interrupt frame.
|
||||
input.disable_hold_all();
|
||||
advance_command(&mut input, FighterCommands::Attack);
|
||||
assert!(input.has_command(FighterCommands::Attack));
|
||||
assert!(input.consume_command(FighterCommands::Attack));
|
||||
assert!(!input.has_command(FighterCommands::Attack));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finite_hold_limit_includes_elapsed_tap_frames() {
|
||||
let mut input = FighterInput::default();
|
||||
input.set_lifetime(FighterCommands::Attack, 3);
|
||||
// The recognition frame has already elapsed when jump squat starts.
|
||||
input.update_hold(FighterCommands::Attack);
|
||||
input.enable_hold_all(10);
|
||||
|
||||
for _ in 0..9 {
|
||||
advance_command(&mut input, FighterCommands::Attack);
|
||||
assert!(input.has_command(FighterCommands::Attack));
|
||||
}
|
||||
advance_command(&mut input, FighterCommands::Attack);
|
||||
assert!(!input.has_command(FighterCommands::Attack));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stopping_enrollment_and_cancelling_latches_are_distinct() {
|
||||
let mut input = FighterInput::default();
|
||||
input.enable_hold_all(10);
|
||||
input.set_lifetime(FighterCommands::Shield, 1);
|
||||
input.disable_hold_all();
|
||||
advance_command(&mut input, FighterCommands::Shield);
|
||||
assert!(input.has_command(FighterCommands::Shield));
|
||||
|
||||
input.cancel_hold_all();
|
||||
advance_command(&mut input, FighterCommands::Shield);
|
||||
assert!(!input.has_command(FighterCommands::Shield));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_override_can_disable_global_hold() {
|
||||
let mut input = FighterInput::default();
|
||||
input.enable_hold_all(10);
|
||||
input.disable_hold(FighterCommands::Attack);
|
||||
input.set_lifetime(FighterCommands::Attack, 1);
|
||||
advance_command(&mut input, FighterCommands::Attack);
|
||||
assert!(!input.has_command(FighterCommands::Attack));
|
||||
|
||||
input.reset_hold_override(FighterCommands::Attack);
|
||||
input.set_lifetime(FighterCommands::Attack, 1);
|
||||
advance_command(&mut input, FighterCommands::Attack);
|
||||
assert!(input.has_command(FighterCommands::Attack));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn infinite_hold_uses_an_explicit_state() {
|
||||
let mut input = FighterInput::default();
|
||||
input.enable_hold(FighterCommands::Shield, 0);
|
||||
input.set_lifetime(FighterCommands::Shield, 1);
|
||||
for _ in 0..u16::MAX as usize + 1 {
|
||||
advance_command(&mut input, FighterCommands::Shield);
|
||||
}
|
||||
assert!(input.has_command(FighterCommands::Shield));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user