Compare commits

..
1 Commits
Author SHA1 Message Date
EIREXE 895a8b8756 Implement invulnerability/intangibility and sakurai angle
CI / build-and-test (pull_request) Successful in 2m8s
CI / production-build (pull_request) Successful in 11m7s
2026-09-29 19:36:48 +02:00
14 changed files with 190 additions and 744 deletions
+2 -5
View File
@@ -7,10 +7,9 @@ authors = [""]
[workspace] [workspace]
[features] [features]
default = ["development", "debug_features", "gamecube-adapter"] default = ["development", "debug_features"]
development = ["bevy/dynamic_linking", "bevy/file_watcher", "bevy/embedded_watcher"] development = ["bevy/dynamic_linking", "bevy/file_watcher", "bevy/embedded_watcher"]
debug_features = ["bevy/free_camera"] debug_features = ["bevy/free_camera"]
gamecube-adapter = ["dep:gc-adapter-neo"]
[dependencies] [dependencies]
bevy = { version = "0.19", features = ["asset_processor", "basis-universal", "compressed_image_saver", "bevy_settings"] } bevy = { version = "0.19", features = ["asset_processor", "basis-universal", "compressed_image_saver", "bevy_settings"] }
@@ -35,6 +34,7 @@ strum = "0.28.0"
strum_macros = "0.28.0" strum_macros = "0.28.0"
bevy_asset_loader = "0.27.0" bevy_asset_loader = "0.27.0"
ron_asset_manager = "0.9.0" ron_asset_manager = "0.9.0"
gc-adapter-neo = { version = "1.1.1", features = ["libusb"] }
rhai = "1.26.0" rhai = "1.26.0"
enum_cycling = "0.2.1" enum_cycling = "0.2.1"
enum_cycling_derive = "0.2.1" enum_cycling_derive = "0.2.1"
@@ -49,6 +49,3 @@ inherits = "release"
lto = "thin" lto = "thin"
codegen-units = 1 codegen-units = 1
strip = "symbols" strip = "symbols"
[target.'cfg(not(target_os = "windows"))'.dependencies]
gc-adapter-neo = { version = "1.1.1", features = ["libusb"], optional = true }
-3
View File
@@ -45,9 +45,6 @@ impl FighterStateImpl for AirAttackState {
} }
fn on_enter(&mut self, state_context: &mut EnterContext) { 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); state_context.play_animation(self.attack_kind.get_animation(), false);
let script = state_context.attack_script_assets.scripts[&self.attack_kind].clone(); let script = state_context.attack_script_assets.scripts[&self.attack_kind].clone();
state_context.hitboxes.0 = Some(crate::fighter::FighterActiveAttack { state_context.hitboxes.0 = Some(crate::fighter::FighterActiveAttack {
+6 -29
View File
@@ -1,17 +1,13 @@
use crate::{ use crate::{
fighter::{ fighter::{
FighterInvulnerabilityState, FighterInvulnerabilityState, animation::AnimKind, state::{
animation::AnimKind,
state::{
CollisionContext, EnterContext, FighterState, FighterStateImpl, InterruptContext, CollisionContext, EnterContext, FighterState, FighterStateImpl, InterruptContext,
UpdateContext, UpdateContext,
air::{self, AirborneStateCommon}, air::{self, AirborneStateCommon},
fall::FallState, fall::FallState,
land::LandingType, land::LandingType,
}, },
}, }, input, math::vec::FGVec2,
input,
math::vec::FGVec2,
}; };
#[derive(Debug, Clone, Hash)] #[derive(Debug, Clone, Hash)]
@@ -33,14 +29,9 @@ impl FighterStateImpl for AirDodgeState {
} }
fn update(&mut self, state_context: &mut UpdateContext) { fn update(&mut self, state_context: &mut UpdateContext) {
let airdodge_intangible_end = state_context.fighter_attributes.air_dodge_intangible_start let airdodge_intangible_end = state_context.fighter_attributes.air_dodge_intangible_start + state_context.fighter_attributes.air_dodge_intangible_duration;
+ state_context if state_context.animation_frame == state_context.fighter_attributes.air_dodge_intangible_start {
.fighter_attributes *state_context.vulnerability = FighterInvulnerabilityState::Intangible { frames_remaining: Some(state_context.fighter_attributes.air_dodge_intangible_duration) };
.air_dodge_intangible_duration;
if state_context.animation_frame
== state_context.fighter_attributes.air_dodge_intangible_start
{
*state_context.vulnerability = FighterInvulnerabilityState::Intangible;
} else if state_context.animation_frame >= airdodge_intangible_end { } else if state_context.animation_frame >= airdodge_intangible_end {
*state_context.vulnerability = FighterInvulnerabilityState::Vulnerable; *state_context.vulnerability = FighterInvulnerabilityState::Vulnerable;
} }
@@ -63,13 +54,6 @@ impl FighterStateImpl for AirDodgeState {
fn on_enter(&mut self, state_context: &mut EnterContext) { fn on_enter(&mut self, state_context: &mut EnterContext) {
state_context.play_animation(AnimKind::AirDodge, false); state_context.play_animation(AnimKind::AirDodge, false);
state_context
.input
.clear_command(input::FighterCommands::Shield);
state_context
.input
.clear_command(input::FighterCommands::GuardHold);
state_context.velocity.0 = state_context.velocity.0 =
self.direction * state_context.fighter_attributes.air_dodge_velocity; self.direction * state_context.fighter_attributes.air_dodge_velocity;
} }
@@ -95,14 +79,7 @@ pub fn check_input(state_context: &InterruptContext) -> Option<FighterState> {
state_context state_context
.input .input
.has_command(input::FighterCommands::Shield) .has_command(input::FighterCommands::Shield)
.then_some(()) .then(|| {
.or_else(|| {
state_context
.input
.has_command(input::FighterCommands::GuardHold)
.then_some(())
})
.map(|_| {
FighterState::AirDodge(AirDodgeState { FighterState::AirDodge(AirDodgeState {
direction: state_context direction: state_context
.input .input
-3
View File
@@ -42,9 +42,6 @@ impl FighterStateImpl for GroundAttackState {
} }
fn on_enter(&mut self, state_context: &mut EnterContext) { 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); state_context.play_animation(self.attack_kind.get_animation(), false);
let script = state_context.attack_script_assets.scripts[&self.attack_kind].clone(); let script = state_context.attack_script_assets.scripts[&self.attack_kind].clone();
state_context.hitboxes.0 = Some(crate::fighter::FighterActiveAttack { state_context.hitboxes.0 = Some(crate::fighter::FighterActiveAttack {
+3 -18
View File
@@ -11,7 +11,7 @@ use crate::{
ground, ground,
}, },
}, },
input::{DigitalAction, FighterCommands}, input::FighterCommands,
math::{int::FGi32, vec::FGVec2}, math::{int::FGi32, vec::FGVec2},
}; };
#[derive(Debug, Hash, Clone, Copy)] #[derive(Debug, Hash, Clone, Copy)]
@@ -68,18 +68,8 @@ impl FighterStateImpl for JumpSquatState {
direction: wavedash_dir.normalize_or_zero(), direction: wavedash_dir.normalize_or_zero(),
})) }))
} else if self.duration_counter >= state_context.fighter_attributes.jumpsquat_duration { } else if self.duration_counter >= state_context.fighter_attributes.jumpsquat_duration {
info!( info!("Jump {}", state_context.input.get_last_frame().jump);
"Jump {}", let jump_type = if state_context.input.get_last_frame().jump {
state_context
.input
.get_last_frame()
.is_held(DigitalAction::Jump)
);
let jump_type = if state_context
.input
.get_last_frame()
.is_held(DigitalAction::Jump)
{
JumpType::LongJump JumpType::LongJump
} else { } else {
JumpType::ShortHop JumpType::ShortHop
@@ -107,14 +97,9 @@ impl FighterStateImpl for JumpSquatState {
fn on_enter(&mut self, state_context: &mut EnterContext) { fn on_enter(&mut self, state_context: &mut EnterContext) {
state_context.play_animation(AnimKind::JumpSquat, false); state_context.play_animation(AnimKind::JumpSquat, false);
state_context.input.enable_hold_all(10);
state_context.input.clear_command(FighterCommands::Jump); state_context.input.clear_command(FighterCommands::Jump);
} }
fn on_exit(&mut self, state_context: &mut crate::fighter::state::ExitContext) {
state_context.input.disable_hold_all();
}
fn is_visually_grounded<'a>(&'a self) -> bool { fn is_visually_grounded<'a>(&'a self) -> bool {
true true
} }
-2
View File
@@ -174,7 +174,6 @@ impl EnterContext<'_> {
} }
pub struct ExitContext<'a> { pub struct ExitContext<'a> {
pub input: &'a mut FighterInput,
pub hitboxes: &'a mut FighterHitboxes, pub hitboxes: &'a mut FighterHitboxes,
pub surface: &'a mut FighterSurfaceState, pub surface: &'a mut FighterSurfaceState,
pub vulnerability: &'a mut FighterInvulnerabilityState, pub vulnerability: &'a mut FighterInvulnerabilityState,
@@ -339,7 +338,6 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
fn exit_context(&mut self) -> ExitContext<'_> { fn exit_context(&mut self) -> ExitContext<'_> {
ExitContext { ExitContext {
input: &mut self.input,
hitboxes: &mut self.hitboxes, hitboxes: &mut self.hitboxes,
surface: &mut self.surface, surface: &mut self.surface,
vulnerability: &mut self.vulnerability, vulnerability: &mut self.vulnerability,
+129 -485
View File
@@ -1,4 +1,8 @@
//! Rollback-safe input history and command buffering. //! 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};
use bevy::prelude::*; use bevy::prelude::*;
use bevy_ggrs::{PlayerInputs, Rollback}; use bevy_ggrs::{PlayerInputs, Rollback};
@@ -8,15 +12,14 @@ use strum_macros::EnumIter;
use crate::fighter::FighterFacingDirection; use crate::fighter::FighterFacingDirection;
use crate::fighter::attack::AttackKind; use crate::fighter::attack::AttackKind;
use crate::game_settings::GameSettings; use crate::game_settings::GameSettings;
use crate::input::{DigitalAction, FighterInputFrame}; use crate::input::FighterInputFrame;
use crate::netcode::GGRSCfg; use crate::netcode::GGRSCfg;
use crate::player::Player; use crate::player::Player;
#[derive(Clone, Copy, EnumIter, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Copy, EnumIter, Debug)]
pub enum FighterCommands { pub enum FighterCommands {
Jump, Jump,
Shield, Shield,
GuardHold,
Attack, Attack,
SmashMoveUp, SmashMoveUp,
SmashMoveDown, SmashMoveDown,
@@ -24,31 +27,7 @@ pub enum FighterCommands {
SmashMoveRight, SmashMoveRight,
} }
pub(crate) fn second_binding_pressed( #[derive(Default, Clone, Hash, Copy)]
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 { pub enum FighterAttackCommandType {
#[default] #[default]
Neutral, Neutral,
@@ -65,140 +44,86 @@ pub enum FighterAttackCommandType {
impl FighterAttackCommandType { impl FighterAttackCommandType {
pub fn to_attack_kind_grounded(&self) -> AttackKind { pub fn to_attack_kind_grounded(&self) -> AttackKind {
match self { match self {
Self::Neutral => AttackKind::Jab, FighterAttackCommandType::Neutral => AttackKind::Jab,
Self::SmashAttackUp => AttackKind::UpSmash, FighterAttackCommandType::SmashAttackUp => AttackKind::UpSmash,
Self::SmashAttackDown => AttackKind::DownSmash, FighterAttackCommandType::SmashAttackDown => AttackKind::DownSmash,
Self::SmashAttackLeft | Self::SmashAttackRight => AttackKind::ForwardSmash, FighterAttackCommandType::SmashAttackLeft => AttackKind::ForwardSmash,
Self::TiltAttackUp => AttackKind::UpTilt, FighterAttackCommandType::SmashAttackRight => AttackKind::ForwardSmash,
Self::TiltAttackDown => AttackKind::DownTilt, FighterAttackCommandType::TiltAttackUp => AttackKind::UpTilt,
Self::TiltAttackLeft | Self::TiltAttackRight => AttackKind::ForwardTilt, FighterAttackCommandType::TiltAttackDown => AttackKind::DownTilt,
FighterAttackCommandType::TiltAttackLeft => AttackKind::ForwardTilt,
FighterAttackCommandType::TiltAttackRight => AttackKind::ForwardTilt,
} }
} }
pub fn to_attack_kind_airborne(&self, facing_direction: &FighterFacingDirection) -> AttackKind { pub fn to_attack_kind_airborne(&self, facing_direction: &FighterFacingDirection) -> AttackKind {
match self { match self {
Self::Neutral => AttackKind::NeutralAir, FighterAttackCommandType::Neutral => AttackKind::NeutralAir,
Self::SmashAttackUp | Self::TiltAttackUp => AttackKind::UpAir, FighterAttackCommandType::SmashAttackUp | FighterAttackCommandType::TiltAttackUp => AttackKind::UpAir,
Self::SmashAttackDown | Self::TiltAttackDown => AttackKind::DownAir, FighterAttackCommandType::SmashAttackDown | FighterAttackCommandType::TiltAttackDown => AttackKind::DownAir,
Self::SmashAttackLeft | Self::TiltAttackLeft => match facing_direction {
FighterAttackCommandType::SmashAttackLeft | FighterAttackCommandType::TiltAttackLeft => {
match facing_direction {
FighterFacingDirection::Left => AttackKind::ForwardAir, FighterFacingDirection::Left => AttackKind::ForwardAir,
FighterFacingDirection::Right => AttackKind::BackAir, FighterFacingDirection::Right => AttackKind::BackAir,
}
}, },
Self::SmashAttackRight | Self::TiltAttackRight => match facing_direction { FighterAttackCommandType::SmashAttackRight | FighterAttackCommandType::TiltAttackRight => {
match facing_direction {
FighterFacingDirection::Left => AttackKind::BackAir, FighterFacingDirection::Left => AttackKind::BackAir,
FighterFacingDirection::Right => AttackKind::ForwardAir, FighterFacingDirection::Right => AttackKind::ForwardAir,
}
}, },
} }
} }
} }
#[derive(Clone, Copy, Debug, Hash)] /// Frames of life remaining for each buffered command. Indexed by
struct CommandSlot { /// [`FighterCommands`] so adding a command is a two-line change here.
lifetime: u8, #[derive(Default, Clone, Copy, Hash)]
age: u16, pub struct FighterCommandLifetimes {
hold_remaining: HoldLimit, smash_move_left: u8,
consumed: bool, smash_move_right: u8,
smash_move_down: u8,
smash_move_up: u8,
jump: u8,
shield: u8,
attack: u8,
} }
impl Default for CommandSlot { impl Index<FighterCommands> for FighterCommandLifetimes {
fn default() -> Self { type Output = u8;
Self {
lifetime: 0, fn index(&self, command: FighterCommands) -> &Self::Output {
age: 0, match command {
hold_remaining: HoldLimit::Disabled, FighterCommands::SmashMoveLeft => &self.smash_move_left,
consumed: false, 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,
} }
} }
} }
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)] impl IndexMut<FighterCommands> for FighterCommandLifetimes {
enum HoldLimit { fn index_mut(&mut self, command: FighterCommands) -> &mut Self::Output {
#[default] match command {
Disabled, FighterCommands::SmashMoveLeft => &mut self.smash_move_left,
Frames(u16), FighterCommands::SmashMoveRight => &mut self.smash_move_right,
Infinite, FighterCommands::SmashMoveDown => &mut self.smash_move_down,
} FighterCommands::SmashMoveUp => &mut self.smash_move_up,
FighterCommands::Jump => &mut self.jump,
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)] FighterCommands::Shield => &mut self.shield,
enum HoldOverride { FighterCommands::Attack => &mut self.attack,
#[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)
} }
} }
} }
impl HoldLimit { #[derive(Component, Default, Clone, Copy, Hash)]
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)] #[require(Rollback)]
pub struct FighterInput { pub struct FighterInput {
command_slots: [CommandSlot; FighterCommands::COUNT], command_lifetimes: FighterCommandLifetimes,
hold_limits: [HoldOverride; FighterCommands::COUNT],
hold_all: HoldLimit,
button_history: [ButtonHistory; 3],
prev_frame: FighterInputFrame, prev_frame: FighterInputFrame,
current_frame: FighterInputFrame, current_frame: FighterInputFrame,
frames_in_x_flick_deadzone: u8, frames_in_x_flick_deadzone: u8,
@@ -206,22 +131,6 @@ pub struct FighterInput {
attack_command: FighterAttackCommandType, 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 { impl FighterInput {
pub fn get_last_frame(&self) -> FighterInputFrame { pub fn get_last_frame(&self) -> FighterInputFrame {
self.current_frame self.current_frame
@@ -233,188 +142,40 @@ impl FighterInput {
} }
pub fn clear_buffer(&mut self) { pub fn clear_buffer(&mut self) {
self.command_slots = [CommandSlot::default(); FighterCommands::COUNT]; self.command_lifetimes = FighterCommandLifetimes::default()
} }
pub fn clear_command(&mut self, command: FighterCommands) { pub fn clear_command(&mut self, command: FighterCommands) {
let slot = &mut self.command_slots[command.index()]; self.command_lifetimes[command] = 0;
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) { pub fn set_lifetime(&mut self, command: FighterCommands, lifetime: u8) {
let slot = &mut self.command_slots[command.index()]; self.command_lifetimes[command] = lifetime;
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 { pub fn has_command(&self, command: FighterCommands) -> bool {
self.command_slots[command.index()].lifetime > 0 self.command_lifetimes[command] > 0
} }
pub fn reduce_command_lifetime(&mut self, command: FighterCommands) { pub fn reduce_command_lifetime(&mut self, command: FighterCommands) {
self.command_slots[command.index()].lifetime = self.command_slots[command.index()] self.command_lifetimes[command] = self.command_lifetimes[command].saturating_sub(1);
.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 { pub fn get_frames_in_smash_move_deadzone(&self) -> u8 {
self.frames_in_x_flick_deadzone self.frames_in_x_flick_deadzone
} }
pub fn set_attack_command_type(&mut self, command: FighterAttackCommandType) { pub fn set_attack_command_type(&mut self, command: FighterAttackCommandType) {
self.attack_command = command; self.attack_command = command;
} }
pub fn get_attack_command_type(&self) -> FighterAttackCommandType { pub fn get_attack_command_type(&self) -> FighterAttackCommandType {
self.attack_command 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( pub fn postprocess_input(
query: Query<(&mut FighterInput, &Player)>, query: Query<(&mut FighterInput, &Player)>,
inputs: Res<PlayerInputs<GGRSCfg>>, inputs: Res<PlayerInputs<GGRSCfg>>,
@@ -426,95 +187,95 @@ pub fn postprocess_input(
} }
input.push_input_frame(inputs[player.handle].0); 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 x_abs = input.current_frame.movement.x.abs();
let y_abs = input.current_frame.movement.y.abs(); let y_abs = input.current_frame.movement.y.abs();
let reset_deadzone = game_settings.input_common.smash_input_reset_axis_threshold; let reset_deadzone = game_settings.input_common.smash_input_reset_axis_threshold;
let axis_threshold = game_settings.input_common.smash_input_axis_threshold; let axis_threshold = game_settings.input_common.smash_input_axis_threshold;
let frame_threshold = game_settings.input_common.smash_input_frame_threshold; let frame_threshold = game_settings.input_common.smash_input_frame_threshold;
if x_abs < reset_deadzone { if x_abs < reset_deadzone {
input.frames_in_x_flick_deadzone = 0xFE; input.frames_in_x_flick_deadzone = 0xFE;
} else if input.prev_frame.movement.x.abs() < reset_deadzone } 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.prev_frame.movement.x.signum() != input.current_frame.movement.x.signum()
{ {
input.frames_in_x_flick_deadzone = 1; input.frames_in_x_flick_deadzone = 1;
} else { } 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 { if y_abs < reset_deadzone {
input.frames_in_y_flick_deadzone = 0xFE; input.frames_in_y_flick_deadzone = 0xFE;
} else if input.prev_frame.movement.y.abs() < reset_deadzone } 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.prev_frame.movement.y.signum() != input.current_frame.movement.y.signum()
{ {
input.frames_in_y_flick_deadzone = 1; input.frames_in_y_flick_deadzone = 1;
} else { } 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);
}
} }
let tap = game_settings.input_common.input_buffer_size; if !input.prev_frame.jump && input.current_frame.jump {
let jump_pressed = input.is_pressed(DigitalAction::Jump); input.set_lifetime(
let shield_pressed = input.is_pressed(DigitalAction::Shield); FighterCommands::Jump,
let attack_pressed = input.is_pressed(DigitalAction::Attack); game_settings.input_common.input_buffer_size,
if jump_pressed { );
input.set_lifetime(FighterCommands::Jump, tap);
}
if shield_pressed {
input.set_lifetime(FighterCommands::Shield, tap);
} }
let shield_now = input.current_frame.bindings_for(DigitalAction::Shield); if !input.prev_frame.shield && input.current_frame.shield {
let shield_previous = input.prev_frame.bindings_for(DigitalAction::Shield); input.set_lifetime(
let second_shield_pressed = second_binding_pressed(shield_now, shield_previous); FighterCommands::Shield,
if second_shield_pressed { game_settings.input_common.input_buffer_size,
input.set_lifetime(FighterCommands::GuardHold, tap); );
}
if !input.prev_frame.attack && input.current_frame.attack {
input.set_lifetime(
FighterCommands::Attack,
game_settings.input_common.input_buffer_size,
);
} }
let mut y_flick_detected = let mut y_flick_detected =
y_abs >= axis_threshold && input.frames_in_y_flick_deadzone <= frame_threshold; y_abs >= axis_threshold && input.frames_in_y_flick_deadzone <= frame_threshold;
let mut x_flick_detected = let mut x_flick_detected =
x_abs >= axis_threshold && input.frames_in_x_flick_deadzone <= frame_threshold; x_abs >= axis_threshold && input.frames_in_x_flick_deadzone <= frame_threshold;
if y_flick_detected { if y_flick_detected {
input.clear_command(FighterCommands::SmashMoveUp); input.clear_command(FighterCommands::SmashMoveUp);
input.clear_command(FighterCommands::SmashMoveDown); input.clear_command(FighterCommands::SmashMoveDown);
input.frames_in_y_flick_deadzone = 0xFF; input.frames_in_y_flick_deadzone = 0xFF;
let command = if input.current_frame.movement.y > 0.0 {
let command_to_set = if input.current_frame.movement.y > 0.0 {
FighterCommands::SmashMoveUp FighterCommands::SmashMoveUp
} else { } else {
FighterCommands::SmashMoveDown FighterCommands::SmashMoveDown
}; };
input.set_lifetime(command, tap);
} input.set_lifetime(command_to_set, game_settings.input_common.input_buffer_size);
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 { if x_flick_detected && y_flick_detected {
x_flick_detected = x_abs > y_abs; x_flick_detected = x_abs > y_abs;
y_flick_detected = !x_flick_detected; y_flick_detected = !x_flick_detected;
} }
if attack_pressed { let attack_detected = input.current_frame.attack && !input.prev_frame.attack;
// Tilt/smash up/down
if attack_detected {
let attack_type = if y_flick_detected { let attack_type = if y_flick_detected {
if input.current_frame.movement.y > 0.0 { if input.current_frame.movement.y > 0.0 {
FighterAttackCommandType::SmashAttackUp FighterAttackCommandType::SmashAttackUp
@@ -542,142 +303,25 @@ pub fn postprocess_input(
} else { } else {
FighterAttackCommandType::Neutral FighterAttackCommandType::Neutral
}; };
input.set_lifetime(FighterCommands::Attack, tap);
input.set_lifetime(
FighterCommands::Attack,
game_settings.input_common.input_buffer_size,
);
input.set_attack_command_type(attack_type); input.set_attack_command_type(attack_type);
} }
for command in FighterCommands::iter() { if x_flick_detected {
input.update_hold(command); input.clear_command(FighterCommands::SmashMoveLeft);
} input.clear_command(FighterCommands::SmashMoveRight);
}
}
#[cfg(test)] let command_to_set = if input.current_frame.movement.x > 0.0 {
mod tests { FighterCommands::SmashMoveRight
use super::*; } else {
use crate::input::ActionBindingMask; FighterCommands::SmashMoveLeft
};
fn advance_command(input: &mut FighterInput, command: FighterCommands) { input.set_lifetime(command_to_set, game_settings.input_common.input_buffer_size);
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));
} }
} }
+3 -109
View File
@@ -2,119 +2,13 @@ use serde::{Deserialize, Serialize};
use crate::math::vec::FGVec2; use crate::math::vec::FGVec2;
/// A fixed-width set of distinct bindings assigned to one logical action.
/// Keeping binding identity in the rollback frame allows combinations such as
/// pressing a second shield binding while the first one is still held.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub struct ActionBindingMask(pub u16);
impl ActionBindingMask {
pub const fn empty() -> Self {
Self(0)
}
pub const fn from_slot(slot: u8) -> Self {
Self(if slot < 16 { 1u16 << slot } else { 0 })
}
pub const fn contains(self, other: Self) -> bool {
self.0 & other.0 == other.0
}
pub const fn intersects(self, other: Self) -> bool {
self.0 & other.0 != 0
}
pub const fn count(self) -> u32 {
self.0.count_ones()
}
pub const fn pressed_since(self, previous: Self) -> Self {
Self(self.0 & !previous.0)
}
pub const fn released_since(self, previous: Self) -> Self {
Self(previous.0 & !self.0)
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub struct DigitalActionBindings {
pub jump: ActionBindingMask,
pub shield: ActionBindingMask,
pub attack: ActionBindingMask,
}
impl DigitalActionBindings {
pub fn set(&mut self, action: DigitalAction, slot: u8, pressed: bool) {
let mask = match action {
DigitalAction::Jump => &mut self.jump,
DigitalAction::Shield => &mut self.shield,
DigitalAction::Attack => &mut self.attack,
};
let bit = ActionBindingMask::from_slot(slot).0;
if pressed {
mask.0 |= bit;
} else {
mask.0 &= !bit;
}
}
pub const fn get(self, action: DigitalAction) -> ActionBindingMask {
match action {
DigitalAction::Jump => self.jump,
DigitalAction::Shield => self.shield,
DigitalAction::Attack => self.attack,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum DigitalAction {
Jump,
Shield,
Attack,
}
/// One frame of a player's raw input. This is the type GGRS serializes and /// One frame of a player's raw input. This is the type GGRS serializes and
/// sends over the wire, so keep it small and keep it `PartialEq`. /// sends over the wire, so keep it small and keep it `PartialEq`.
#[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize, Hash)] #[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize, Hash)]
pub struct FighterInputFrame { pub struct FighterInputFrame {
pub movement: FGVec2, pub movement: FGVec2,
pub directional_attack: FGVec2, pub directional_attack: FGVec2,
pub bindings: DigitalActionBindings, pub jump: bool,
} pub shield: bool,
pub attack: bool,
impl FighterInputFrame {
pub const fn bindings_for(self, action: DigitalAction) -> ActionBindingMask {
self.bindings.get(action)
}
pub const fn is_held(self, action: DigitalAction) -> bool {
self.bindings_for(action).0 != 0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn binding_masks_preserve_distinct_same_action_bindings() {
let mut bindings = DigitalActionBindings::default();
bindings.set(DigitalAction::Shield, 0, true);
bindings.set(DigitalAction::Shield, 1, true);
assert_eq!(bindings.shield.0, 0b11);
assert_eq!(bindings.shield.count(), 2);
assert_eq!(bindings.shield.pressed_since(ActionBindingMask(1)).0, 0b10);
}
#[test]
fn clearing_one_binding_does_not_clear_the_other() {
let mut bindings = DigitalActionBindings::default();
bindings.set(DigitalAction::Shield, 0, true);
bindings.set(DigitalAction::Shield, 1, true);
bindings.set(DigitalAction::Shield, 0, false);
assert_eq!(bindings.shield.0, 0b10);
}
} }
+10 -2
View File
@@ -7,8 +7,7 @@ use bevy::input::gamepad::{
}; };
use bevy::prelude::*; use bevy::prelude::*;
use super::GcPorts; use super::poll::{GcAdapterSnapshot, GcPortState};
use super::poll::GcAdapterSnapshot;
/// GameCube adapters report 4 ports. /// GameCube adapters report 4 ports.
pub const GC_PORT_COUNT: usize = 4; pub const GC_PORT_COUNT: usize = 4;
@@ -24,6 +23,15 @@ const GC_ADAPTER_PID: u16 = 0x0337;
#[derive(Component, Debug, Clone, Copy)] #[derive(Component, Debug, Clone, Copy)]
pub struct GcPort(pub u8); pub struct GcPort(pub u8);
/// Tracks which entity (if any) represents each of the 4 adapter ports,
/// and the last snapshot seen, so both systems below can diff instead of
/// re-deriving state every frame.
#[derive(Resource, Default)]
pub struct GcPorts {
pub entities: [Option<Entity>; GC_PORT_COUNT],
last: [GcPortState; GC_PORT_COUNT],
}
fn passthrough_gamepad_settings() -> GamepadSettings { fn passthrough_gamepad_settings() -> GamepadSettings {
let mut settings = GamepadSettings::default(); let mut settings = GamepadSettings::default();
for axis in GamepadAxis::all() { for axis in GamepadAxis::all() {
+1 -17
View File
@@ -1,23 +1,10 @@
#[cfg(all(feature = "gamecube-adapter", not(target_os = "windows")))]
pub mod bridge; pub mod bridge;
#[cfg(all(feature = "gamecube-adapter", not(target_os = "windows")))]
pub mod poll; pub mod poll;
#[cfg(all(feature = "gamecube-adapter", not(target_os = "windows")))] pub use bridge::{GC_PORT_COUNT, GcPort, GcPorts};
pub use bridge::{GC_PORT_COUNT, GcPort};
#[cfg(all(feature = "gamecube-adapter", not(target_os = "windows")))]
pub use poll::{GcAdapterSnapshot, GcPortState}; pub use poll::{GcAdapterSnapshot, GcPortState};
#[derive(Resource, Default)]
pub struct GcPorts {
pub entities: [Option<Entity>; 4],
#[cfg(all(feature = "gamecube-adapter", not(target_os = "windows")))]
pub(super) last: [poll::GcPortState; 4],
}
#[cfg(all(feature = "gamecube-adapter", not(target_os = "windows")))]
use bevy::input::InputSystems; use bevy::input::InputSystems;
#[cfg(all(feature = "gamecube-adapter", not(target_os = "windows")))]
use bevy::input::gamepad::{gamepad_connection_system, gamepad_event_processing_system}; use bevy::input::gamepad::{gamepad_connection_system, gamepad_event_processing_system};
use bevy::prelude::*; use bevy::prelude::*;
@@ -28,8 +15,6 @@ impl Plugin for GamecubeAdapterPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.init_resource::<GcPorts>(); app.init_resource::<GcPorts>();
#[cfg(all(feature = "gamecube-adapter", not(target_os = "windows")))]
{
let Ok(snapshot) = poll::spawn_poll_thread() else { let Ok(snapshot) = poll::spawn_poll_thread() else {
// No adapter connected; leave GcPorts empty and skip wiring the // No adapter connected; leave GcPorts empty and skip wiring the
// polling systems. // polling systems.
@@ -48,4 +33,3 @@ impl Plugin for GamecubeAdapterPlugin {
); );
} }
} }
}
+3 -6
View File
@@ -14,13 +14,10 @@ pub fn sample_gamepad(pad: &Gamepad, input_map: &BaseInputMap) -> FighterInputFr
match &gamepad_element.binding { match &gamepad_element.binding {
GamepadBinding::Button(button) => { GamepadBinding::Button(button) => {
let pressed = pad.pressed(*button); let pressed = pad.pressed(*button);
if let Some(action) = gamepad_element.action.digital_action() {
input_frame
.bindings
.set(action, gamepad_element.binding_slot, pressed);
}
match gamepad_element.action { match gamepad_element.action {
InputMapAction::Jump | InputMapAction::Shield | InputMapAction::Attack => {} InputMapAction::Jump => input_frame.jump |= pressed,
InputMapAction::Shield => input_frame.shield |= pressed,
InputMapAction::Attack => input_frame.attack |= pressed,
// Digital buttons driving movement isn't used by the // Digital buttons driving movement isn't used by the
// default bindings (the stick covers that), but stay // default bindings (the stick covers that), but stay
// consistent with the keyboard backend if one is added. // consistent with the keyboard backend if one is added.
+3 -9
View File
@@ -30,15 +30,9 @@ pub fn sample_keyboard(key: &ButtonInput<KeyCode>, input_map: &BaseInputMap) ->
InputMapAction::MovementYDir(sign) => { InputMapAction::MovementYDir(sign) => {
input_frame.movement.y += (sign) * action_strength input_frame.movement.y += (sign) * action_strength
} }
InputMapAction::Jump | InputMapAction::Shield | InputMapAction::Attack => { InputMapAction::Jump => input_frame.jump = action_strength != FGi32::ZERO,
if let Some(action) = keyboard_elment.action.digital_action() { InputMapAction::Shield => input_frame.shield = action_strength != FGi32::ZERO,
input_frame.bindings.set( InputMapAction::Attack => input_frame.attack = action_strength != FGi32::ZERO,
action,
keyboard_elment.binding_slot,
action_strength != FGi32::ZERO,
);
}
}
} }
} }
-13
View File
@@ -19,17 +19,6 @@ pub enum InputMapAction {
Attack, Attack,
} }
impl InputMapAction {
pub const fn digital_action(&self) -> Option<crate::input::DigitalAction> {
match self {
Self::Jump => Some(crate::input::DigitalAction::Jump),
Self::Shield => Some(crate::input::DigitalAction::Shield),
Self::Attack => Some(crate::input::DigitalAction::Attack),
Self::MovementXDir(_) | Self::MovementYDir(_) => None,
}
}
}
#[derive(Debug)] #[derive(Debug)]
pub enum MenuInputMapAction { pub enum MenuInputMapAction {
MovementXDir(i32), MovementXDir(i32),
@@ -43,7 +32,6 @@ pub enum MenuInputMapAction {
pub struct KeyboardInputMapElement { pub struct KeyboardInputMapElement {
pub key: KeyCode, pub key: KeyCode,
pub action: InputMapAction, pub action: InputMapAction,
pub binding_slot: u8,
} }
#[derive(Debug)] #[derive(Debug)]
@@ -56,7 +44,6 @@ pub enum GamepadBinding {
pub struct GamepadInputMapElement { pub struct GamepadInputMapElement {
pub binding: GamepadBinding, pub binding: GamepadBinding,
pub action: InputMapAction, pub action: InputMapAction,
pub binding_slot: u8,
} }
#[derive(Debug)] #[derive(Debug)]
+1 -14
View File
@@ -15,7 +15,7 @@ pub mod keyboard;
pub mod map; pub mod map;
pub use buffer::{FighterCommands, FighterInput}; pub use buffer::{FighterCommands, FighterInput};
pub use frame::{ActionBindingMask, DigitalAction, DigitalActionBindings, FighterInputFrame}; pub use frame::FighterInputFrame;
pub use map::{ pub use map::{
BaseInputMap, GamepadBinding, GamepadInputMapElement, InputActionState, InputMapAction, BaseInputMap, GamepadBinding, GamepadInputMapElement, InputActionState, InputMapAction,
KeyboardInputMapElement, MenuGamepadInputMapElement, MenuInputMap, MenuInputMapAction, KeyboardInputMapElement, MenuGamepadInputMapElement, MenuInputMap, MenuInputMapAction,
@@ -78,43 +78,36 @@ impl Plugin for FighterInputPlugin {
KeyboardInputMapElement { KeyboardInputMapElement {
key: KeyCode::KeyD, key: KeyCode::KeyD,
action: InputMapAction::MovementXDir(1), action: InputMapAction::MovementXDir(1),
binding_slot: 0,
}, },
// left // left
KeyboardInputMapElement { KeyboardInputMapElement {
key: KeyCode::KeyA, key: KeyCode::KeyA,
action: InputMapAction::MovementXDir(-1), action: InputMapAction::MovementXDir(-1),
binding_slot: 0,
}, },
// up // up
KeyboardInputMapElement { KeyboardInputMapElement {
key: KeyCode::KeyW, key: KeyCode::KeyW,
action: InputMapAction::MovementYDir(1), action: InputMapAction::MovementYDir(1),
binding_slot: 0,
}, },
// down // down
KeyboardInputMapElement { KeyboardInputMapElement {
key: KeyCode::KeyS, key: KeyCode::KeyS,
action: InputMapAction::MovementYDir(-1), action: InputMapAction::MovementYDir(-1),
binding_slot: 0,
}, },
// jump // jump
KeyboardInputMapElement { KeyboardInputMapElement {
key: KeyCode::Space, key: KeyCode::Space,
action: InputMapAction::Jump, action: InputMapAction::Jump,
binding_slot: 0,
}, },
// shield/airdodge // shield/airdodge
KeyboardInputMapElement { KeyboardInputMapElement {
key: KeyCode::ShiftLeft, key: KeyCode::ShiftLeft,
action: InputMapAction::Shield, action: InputMapAction::Shield,
binding_slot: 0,
}, },
// attack // attack
KeyboardInputMapElement { KeyboardInputMapElement {
key: KeyCode::KeyJ, key: KeyCode::KeyJ,
action: InputMapAction::Attack, action: InputMapAction::Attack,
binding_slot: 0,
}, },
// Menu // Menu
], ],
@@ -123,34 +116,28 @@ impl Plugin for FighterInputPlugin {
GamepadInputMapElement { GamepadInputMapElement {
binding: GamepadBinding::Axis(GamepadAxis::LeftStickX, 1), binding: GamepadBinding::Axis(GamepadAxis::LeftStickX, 1),
action: InputMapAction::MovementXDir(1), action: InputMapAction::MovementXDir(1),
binding_slot: 0,
}, },
GamepadInputMapElement { GamepadInputMapElement {
binding: GamepadBinding::Axis(GamepadAxis::LeftStickY, 1), binding: GamepadBinding::Axis(GamepadAxis::LeftStickY, 1),
action: InputMapAction::MovementYDir(1), action: InputMapAction::MovementYDir(1),
binding_slot: 0,
}, },
// jump // jump
GamepadInputMapElement { GamepadInputMapElement {
binding: GamepadBinding::Button(GamepadButton::North), binding: GamepadBinding::Button(GamepadButton::North),
action: InputMapAction::Jump, action: InputMapAction::Jump,
binding_slot: 0,
}, },
GamepadInputMapElement { GamepadInputMapElement {
binding: GamepadBinding::Button(GamepadButton::LeftTrigger2), binding: GamepadBinding::Button(GamepadButton::LeftTrigger2),
action: InputMapAction::Shield, action: InputMapAction::Shield,
binding_slot: 0,
}, },
GamepadInputMapElement { GamepadInputMapElement {
binding: GamepadBinding::Button(GamepadButton::RightTrigger2), binding: GamepadBinding::Button(GamepadButton::RightTrigger2),
action: InputMapAction::Shield, action: InputMapAction::Shield,
binding_slot: 1,
}, },
// attack // attack
GamepadInputMapElement { GamepadInputMapElement {
binding: GamepadBinding::Button(GamepadButton::East), binding: GamepadBinding::Button(GamepadButton::East),
action: InputMapAction::Attack, action: InputMapAction::Attack,
binding_slot: 0,
}, },
], ],
}) })