Compare commits
4
Commits
856f65b5b5
...
021f466ac3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
021f466ac3 | ||
|
|
97788b7df8 | ||
|
|
32428982ad | ||
|
|
47f7ae1e69 |
+5
-2
@@ -7,9 +7,10 @@ authors = [""]
|
|||||||
[workspace]
|
[workspace]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["development", "debug_features"]
|
default = ["development", "debug_features", "gamecube-adapter"]
|
||||||
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"] }
|
||||||
@@ -34,7 +35,6 @@ 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,3 +49,6 @@ 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 }
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ 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 {
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
fighter::{
|
fighter::{
|
||||||
FighterInvulnerabilityState, animation::AnimKind, state::{
|
FighterInvulnerabilityState,
|
||||||
|
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)]
|
||||||
@@ -29,8 +33,13 @@ 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 + state_context.fighter_attributes.air_dodge_intangible_duration;
|
let airdodge_intangible_end = state_context.fighter_attributes.air_dodge_intangible_start
|
||||||
if state_context.animation_frame == state_context.fighter_attributes.air_dodge_intangible_start {
|
+ state_context
|
||||||
|
.fighter_attributes
|
||||||
|
.air_dodge_intangible_duration;
|
||||||
|
if state_context.animation_frame
|
||||||
|
== state_context.fighter_attributes.air_dodge_intangible_start
|
||||||
|
{
|
||||||
*state_context.vulnerability = FighterInvulnerabilityState::Intangible;
|
*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;
|
||||||
@@ -54,6 +63,13 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -79,7 +95,14 @@ 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(|| {
|
.then_some(())
|
||||||
|
.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
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ 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 {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use crate::{
|
|||||||
ground,
|
ground,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
input::FighterCommands,
|
input::{DigitalAction, FighterCommands},
|
||||||
math::{int::FGi32, vec::FGVec2},
|
math::{int::FGi32, vec::FGVec2},
|
||||||
};
|
};
|
||||||
#[derive(Debug, Hash, Clone, Copy)]
|
#[derive(Debug, Hash, Clone, Copy)]
|
||||||
@@ -68,8 +68,18 @@ 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!("Jump {}", state_context.input.get_last_frame().jump);
|
info!(
|
||||||
let jump_type = if state_context.input.get_last_frame().jump {
|
"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
|
||||||
@@ -97,9 +107,14 @@ 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ use crate::fighter::state::hitstun::HitstunState;
|
|||||||
use crate::fighter::surface::EcbLockPolicy;
|
use crate::fighter::surface::EcbLockPolicy;
|
||||||
use crate::fighter::visual::FighterAnimations;
|
use crate::fighter::visual::FighterAnimations;
|
||||||
use crate::fighter::{
|
use crate::fighter::{
|
||||||
Fighter, FighterDamage, FighterECB, FighterFacingDirection, FighterHitboxes, FighterHitstop, FighterHitstopShake, FighterHitstopShakeKind, FighterHitstopShakeRole, FighterInvulnerabilityState, FighterPendingRespawn, FighterPresentationSnap, FighterPreviousTranslation, FighterSurfaceState, FighterTranslation, FighterVelocity,
|
Fighter, FighterDamage, FighterECB, FighterFacingDirection, FighterHitboxes, FighterHitstop,
|
||||||
|
FighterHitstopShake, FighterHitstopShakeKind, FighterHitstopShakeRole,
|
||||||
|
FighterInvulnerabilityState, FighterPendingRespawn, FighterPresentationSnap,
|
||||||
|
FighterPreviousTranslation, FighterSurfaceState, FighterTranslation, FighterVelocity,
|
||||||
};
|
};
|
||||||
use crate::game_settings::GameSettings;
|
use crate::game_settings::GameSettings;
|
||||||
use crate::input::FighterInput;
|
use crate::input::FighterInput;
|
||||||
@@ -32,6 +35,7 @@ use crate::stage::line::StageCollision;
|
|||||||
use crate::stage::manifest::{StageManifest, StageManifestRegistry};
|
use crate::stage::manifest::{StageManifest, StageManifestRegistry};
|
||||||
|
|
||||||
pub mod air;
|
pub mod air;
|
||||||
|
pub mod air_attack;
|
||||||
pub mod air_dodge;
|
pub mod air_dodge;
|
||||||
pub mod angel;
|
pub mod angel;
|
||||||
pub mod crouch;
|
pub mod crouch;
|
||||||
@@ -47,7 +51,6 @@ pub mod run;
|
|||||||
pub mod turn;
|
pub mod turn;
|
||||||
pub mod wait;
|
pub mod wait;
|
||||||
pub mod walk;
|
pub mod walk;
|
||||||
pub mod air_attack;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||||
pub enum SurfacePolicy {
|
pub enum SurfacePolicy {
|
||||||
@@ -170,6 +173,7 @@ 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,
|
||||||
@@ -306,7 +310,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
|
|||||||
animation,
|
animation,
|
||||||
surface: &self.surface,
|
surface: &self.surface,
|
||||||
stage_collision,
|
stage_collision,
|
||||||
attack_script_assets: attack_script_assets
|
attack_script_assets: attack_script_assets,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,6 +337,7 @@ 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,
|
||||||
@@ -540,7 +545,7 @@ pub fn state_interrupt_system(
|
|||||||
&baked_animations,
|
&baked_animations,
|
||||||
&manifest.attributes,
|
&manifest.attributes,
|
||||||
&stage_collision,
|
&stage_collision,
|
||||||
attack_script_assets
|
attack_script_assets,
|
||||||
))
|
))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+497
-141
@@ -1,8 +1,4 @@
|
|||||||
//! Per-fighter input state: the current and previous frame, plus the buffered
|
//! Rollback-safe input history and command buffering.
|
||||||
//! 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};
|
||||||
@@ -12,14 +8,15 @@ 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::FighterInputFrame;
|
use crate::input::{DigitalAction, FighterInputFrame};
|
||||||
use crate::netcode::GGRSCfg;
|
use crate::netcode::GGRSCfg;
|
||||||
use crate::player::Player;
|
use crate::player::Player;
|
||||||
|
|
||||||
#[derive(Clone, Copy, EnumIter, Debug)]
|
#[derive(Clone, Copy, EnumIter, Debug, PartialEq, Eq, Hash)]
|
||||||
pub enum FighterCommands {
|
pub enum FighterCommands {
|
||||||
Jump,
|
Jump,
|
||||||
Shield,
|
Shield,
|
||||||
|
GuardHold,
|
||||||
Attack,
|
Attack,
|
||||||
SmashMoveUp,
|
SmashMoveUp,
|
||||||
SmashMoveDown,
|
SmashMoveDown,
|
||||||
@@ -27,7 +24,31 @@ pub enum FighterCommands {
|
|||||||
SmashMoveRight,
|
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 {
|
pub enum FighterAttackCommandType {
|
||||||
#[default]
|
#[default]
|
||||||
Neutral,
|
Neutral,
|
||||||
@@ -44,86 +65,140 @@ 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 {
|
||||||
FighterAttackCommandType::Neutral => AttackKind::Jab,
|
Self::Neutral => AttackKind::Jab,
|
||||||
FighterAttackCommandType::SmashAttackUp => AttackKind::UpSmash,
|
Self::SmashAttackUp => AttackKind::UpSmash,
|
||||||
FighterAttackCommandType::SmashAttackDown => AttackKind::DownSmash,
|
Self::SmashAttackDown => AttackKind::DownSmash,
|
||||||
FighterAttackCommandType::SmashAttackLeft => AttackKind::ForwardSmash,
|
Self::SmashAttackLeft | Self::SmashAttackRight => AttackKind::ForwardSmash,
|
||||||
FighterAttackCommandType::SmashAttackRight => AttackKind::ForwardSmash,
|
Self::TiltAttackUp => AttackKind::UpTilt,
|
||||||
FighterAttackCommandType::TiltAttackUp => AttackKind::UpTilt,
|
Self::TiltAttackDown => AttackKind::DownTilt,
|
||||||
FighterAttackCommandType::TiltAttackDown => AttackKind::DownTilt,
|
Self::TiltAttackLeft | Self::TiltAttackRight => AttackKind::ForwardTilt,
|
||||||
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 {
|
||||||
FighterAttackCommandType::Neutral => AttackKind::NeutralAir,
|
Self::Neutral => AttackKind::NeutralAir,
|
||||||
FighterAttackCommandType::SmashAttackUp | FighterAttackCommandType::TiltAttackUp => AttackKind::UpAir,
|
Self::SmashAttackUp | Self::TiltAttackUp => AttackKind::UpAir,
|
||||||
FighterAttackCommandType::SmashAttackDown | FighterAttackCommandType::TiltAttackDown => AttackKind::DownAir,
|
Self::SmashAttackDown | Self::TiltAttackDown => AttackKind::DownAir,
|
||||||
|
Self::SmashAttackLeft | Self::TiltAttackLeft => match facing_direction {
|
||||||
FighterAttackCommandType::SmashAttackLeft | FighterAttackCommandType::TiltAttackLeft => {
|
FighterFacingDirection::Left => AttackKind::ForwardAir,
|
||||||
match facing_direction {
|
FighterFacingDirection::Right => AttackKind::BackAir,
|
||||||
FighterFacingDirection::Left => AttackKind::ForwardAir,
|
|
||||||
FighterFacingDirection::Right => AttackKind::BackAir,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
FighterAttackCommandType::SmashAttackRight | FighterAttackCommandType::TiltAttackRight => {
|
Self::SmashAttackRight | Self::TiltAttackRight => match facing_direction {
|
||||||
match facing_direction {
|
FighterFacingDirection::Left => AttackKind::BackAir,
|
||||||
FighterFacingDirection::Left => AttackKind::BackAir,
|
FighterFacingDirection::Right => AttackKind::ForwardAir,
|
||||||
FighterFacingDirection::Right => AttackKind::ForwardAir,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Frames of life remaining for each buffered command. Indexed by
|
#[derive(Clone, Copy, Debug, Hash)]
|
||||||
/// [`FighterCommands`] so adding a command is a two-line change here.
|
struct CommandSlot {
|
||||||
#[derive(Default, Clone, Copy, Hash)]
|
lifetime: u8,
|
||||||
pub struct FighterCommandLifetimes {
|
age: u16,
|
||||||
smash_move_left: u8,
|
hold_remaining: HoldLimit,
|
||||||
smash_move_right: u8,
|
consumed: bool,
|
||||||
smash_move_down: u8,
|
|
||||||
smash_move_up: u8,
|
|
||||||
jump: u8,
|
|
||||||
shield: u8,
|
|
||||||
attack: u8,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Index<FighterCommands> for FighterCommandLifetimes {
|
impl Default for CommandSlot {
|
||||||
type Output = u8;
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
fn index(&self, command: FighterCommands) -> &Self::Output {
|
lifetime: 0,
|
||||||
match command {
|
age: 0,
|
||||||
FighterCommands::SmashMoveLeft => &self.smash_move_left,
|
hold_remaining: HoldLimit::Disabled,
|
||||||
FighterCommands::SmashMoveRight => &self.smash_move_right,
|
consumed: false,
|
||||||
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 IndexMut<FighterCommands> for FighterCommandLifetimes {
|
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
|
||||||
fn index_mut(&mut self, command: FighterCommands) -> &mut Self::Output {
|
enum HoldLimit {
|
||||||
match command {
|
#[default]
|
||||||
FighterCommands::SmashMoveLeft => &mut self.smash_move_left,
|
Disabled,
|
||||||
FighterCommands::SmashMoveRight => &mut self.smash_move_right,
|
Frames(u16),
|
||||||
FighterCommands::SmashMoveDown => &mut self.smash_move_down,
|
Infinite,
|
||||||
FighterCommands::SmashMoveUp => &mut self.smash_move_up,
|
}
|
||||||
FighterCommands::Jump => &mut self.jump,
|
|
||||||
FighterCommands::Shield => &mut self.shield,
|
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
|
||||||
FighterCommands::Attack => &mut self.attack,
|
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)]
|
#[require(Rollback)]
|
||||||
pub struct FighterInput {
|
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,
|
prev_frame: FighterInputFrame,
|
||||||
current_frame: FighterInputFrame,
|
current_frame: FighterInputFrame,
|
||||||
frames_in_x_flick_deadzone: u8,
|
frames_in_x_flick_deadzone: u8,
|
||||||
@@ -131,6 +206,22 @@ 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
|
||||||
@@ -142,40 +233,188 @@ impl FighterInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_buffer(&mut self) {
|
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) {
|
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) {
|
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 {
|
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) {
|
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 {
|
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>>,
|
||||||
@@ -187,95 +426,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
|
||||||
|
|| input.prev_frame.movement.x.signum() != input.current_frame.movement.x.signum()
|
||||||
|
{
|
||||||
|
input.frames_in_x_flick_deadzone = 1;
|
||||||
} else {
|
} else {
|
||||||
let prev_x_abs = input.prev_frame.movement.x.abs();
|
input.frames_in_x_flick_deadzone = input.frames_in_x_flick_deadzone.saturating_add(1);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|| input.prev_frame.movement.y.signum() != input.current_frame.movement.y.signum()
|
||||||
|
{
|
||||||
|
input.frames_in_y_flick_deadzone = 1;
|
||||||
} else {
|
} else {
|
||||||
let prev_y_abs = input.prev_frame.movement.y.abs();
|
input.frames_in_y_flick_deadzone = input.frames_in_y_flick_deadzone.saturating_add(1);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !input.prev_frame.jump && input.current_frame.jump {
|
let tap = game_settings.input_common.input_buffer_size;
|
||||||
input.set_lifetime(
|
let jump_pressed = input.is_pressed(DigitalAction::Jump);
|
||||||
FighterCommands::Jump,
|
let shield_pressed = input.is_pressed(DigitalAction::Shield);
|
||||||
game_settings.input_common.input_buffer_size,
|
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 {
|
let shield_now = input.current_frame.bindings_for(DigitalAction::Shield);
|
||||||
input.set_lifetime(
|
let shield_previous = input.prev_frame.bindings_for(DigitalAction::Shield);
|
||||||
FighterCommands::Shield,
|
let second_shield_pressed = second_binding_pressed(shield_now, shield_previous);
|
||||||
game_settings.input_common.input_buffer_size,
|
if second_shield_pressed {
|
||||||
);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
let attack_detected = input.current_frame.attack && !input.prev_frame.attack;
|
if attack_pressed {
|
||||||
|
|
||||||
// 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
|
||||||
@@ -303,25 +542,142 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
if x_flick_detected {
|
for command in FighterCommands::iter() {
|
||||||
input.clear_command(FighterCommands::SmashMoveLeft);
|
input.update_hold(command);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+109
-3
@@ -2,13 +2,119 @@ 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 jump: bool,
|
pub bindings: DigitalActionBindings,
|
||||||
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ use bevy::input::gamepad::{
|
|||||||
};
|
};
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
use super::poll::{GcAdapterSnapshot, GcPortState};
|
use super::GcPorts;
|
||||||
|
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;
|
||||||
@@ -23,15 +24,6 @@ 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() {
|
||||||
|
|||||||
+32
-16
@@ -1,10 +1,23 @@
|
|||||||
|
#[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;
|
||||||
|
|
||||||
pub use bridge::{GC_PORT_COUNT, GcPort, GcPorts};
|
#[cfg(all(feature = "gamecube-adapter", not(target_os = "windows")))]
|
||||||
|
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::*;
|
||||||
|
|
||||||
@@ -15,21 +28,24 @@ impl Plugin for GamecubeAdapterPlugin {
|
|||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.init_resource::<GcPorts>();
|
app.init_resource::<GcPorts>();
|
||||||
|
|
||||||
let Ok(snapshot) = poll::spawn_poll_thread() else {
|
#[cfg(all(feature = "gamecube-adapter", not(target_os = "windows")))]
|
||||||
// No adapter connected; leave GcPorts empty and skip wiring the
|
{
|
||||||
// polling systems.
|
let Ok(snapshot) = poll::spawn_poll_thread() else {
|
||||||
return;
|
// No adapter connected; leave GcPorts empty and skip wiring the
|
||||||
};
|
// polling systems.
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
app.insert_resource(snapshot).add_systems(
|
app.insert_resource(snapshot).add_systems(
|
||||||
PreUpdate,
|
PreUpdate,
|
||||||
(
|
(
|
||||||
bridge::gc_connection_system.before(gamepad_connection_system),
|
bridge::gc_connection_system.before(gamepad_connection_system),
|
||||||
bridge::gc_event_system
|
bridge::gc_event_system
|
||||||
.after(gamepad_connection_system)
|
.after(gamepad_connection_system)
|
||||||
.before(gamepad_event_processing_system),
|
.before(gamepad_event_processing_system),
|
||||||
)
|
)
|
||||||
.in_set(InputSystems),
|
.in_set(InputSystems),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,13 @@ 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 => input_frame.jump |= pressed,
|
InputMapAction::Jump | InputMapAction::Shield | InputMapAction::Attack => {}
|
||||||
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.
|
||||||
|
|||||||
@@ -30,9 +30,15 @@ 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 => input_frame.jump = action_strength != FGi32::ZERO,
|
InputMapAction::Jump | InputMapAction::Shield | InputMapAction::Attack => {
|
||||||
InputMapAction::Shield => input_frame.shield = action_strength != FGi32::ZERO,
|
if let Some(action) = keyboard_elment.action.digital_action() {
|
||||||
InputMapAction::Attack => input_frame.attack = action_strength != FGi32::ZERO,
|
input_frame.bindings.set(
|
||||||
|
action,
|
||||||
|
keyboard_elment.binding_slot,
|
||||||
|
action_strength != FGi32::ZERO,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,17 @@ 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),
|
||||||
@@ -32,6 +43,7 @@ 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)]
|
||||||
@@ -44,6 +56,7 @@ 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)]
|
||||||
|
|||||||
+14
-1
@@ -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::FighterInputFrame;
|
pub use frame::{ActionBindingMask, DigitalAction, DigitalActionBindings, 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,36 +78,43 @@ 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
|
||||||
],
|
],
|
||||||
@@ -116,28 +123,34 @@ 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,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user