Implement up smash, dacus and further improve input system. (#88)
Reviewed-on: #88 Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
This commit was merged in pull request #88.
This commit is contained in:
@@ -101,4 +101,5 @@ pub enum AnimKind {
|
|||||||
AttackNeutralAir,
|
AttackNeutralAir,
|
||||||
AttackForwardAir,
|
AttackForwardAir,
|
||||||
AttackDash,
|
AttackDash,
|
||||||
|
AttackUpSmash,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ impl AttackKind {
|
|||||||
AttackKind::UpAir => AnimKind::AttackUpTilt,
|
AttackKind::UpAir => AnimKind::AttackUpTilt,
|
||||||
AttackKind::DownTilt => AnimKind::AttackDownTilt,
|
AttackKind::DownTilt => AnimKind::AttackDownTilt,
|
||||||
AttackKind::ForwardTilt => AnimKind::AttackForwardTilt,
|
AttackKind::ForwardTilt => AnimKind::AttackForwardTilt,
|
||||||
AttackKind::UpSmash => AnimKind::AttackUpTilt,
|
AttackKind::UpSmash => AnimKind::AttackUpSmash,
|
||||||
AttackKind::DownSmash => AnimKind::AttackUpTilt,
|
AttackKind::DownSmash => AnimKind::AttackUpTilt,
|
||||||
AttackKind::ForwardSmash => AnimKind::AttackUpTilt,
|
AttackKind::ForwardSmash => AnimKind::AttackUpTilt,
|
||||||
AttackKind::DashAttack => AnimKind::AttackDash,
|
AttackKind::DashAttack => AnimKind::AttackDash,
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ pub struct FighterAttributes {
|
|||||||
|
|
||||||
pub air_dodge_intangible_duration: u32,
|
pub air_dodge_intangible_duration: u32,
|
||||||
pub air_dodge_intangible_start: u32,
|
pub air_dodge_intangible_start: u32,
|
||||||
|
|
||||||
|
pub dash_attack_cancel_window: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FighterAttributes {
|
impl FighterAttributes {
|
||||||
|
|||||||
@@ -149,6 +149,8 @@ pub fn fighter_debug(
|
|||||||
last_frame.movement.x, last_frame.movement.y
|
last_frame.movement.x, last_frame.movement.y
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ui.label(format!("Flick age Y {}", input.peek_movement_flick_dbg()));
|
||||||
}
|
}
|
||||||
FighterDebugRowKind::Position => {
|
FighterDebugRowKind::Position => {
|
||||||
ui.label(format!("Position: {:?}", translation.0));
|
ui.label(format!("Position: {:?}", translation.0));
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ mod tests {
|
|||||||
air_dodge_decay: FGi32::ZERO,
|
air_dodge_decay: FGi32::ZERO,
|
||||||
air_dodge_intangible_duration: 29,
|
air_dodge_intangible_duration: 29,
|
||||||
air_dodge_intangible_start: 4,
|
air_dodge_intangible_start: 4,
|
||||||
|
dash_attack_cancel_window: 3,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ impl FighterStateImpl for DashState {
|
|||||||
fn check_interrupt(&self, ctx: &mut InterruptContext) -> Option<FighterState> {
|
fn check_interrupt(&self, ctx: &mut InterruptContext) -> Option<FighterState> {
|
||||||
let attributes = ctx.fighter_attributes;
|
let attributes = ctx.fighter_attributes;
|
||||||
|
|
||||||
if ctx.get_current_animation_frame() <= 2 {
|
if ctx.get_current_animation_frame() <= 2 || ctx.input.get_last_frame().movement.x.signum() != self.direction.to_sign() {
|
||||||
if let Some(state) = ground_attack::check_interrupt(ctx) {
|
if let Some(state) = ground_attack::check_interrupt(ctx) {
|
||||||
return Some(state);
|
return Some(state);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,19 @@ impl FighterStateImpl for GroundAttackState {
|
|||||||
const NAME: &'static str = "GroundAttack";
|
const NAME: &'static str = "GroundAttack";
|
||||||
|
|
||||||
fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option<FighterState> {
|
fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option<FighterState> {
|
||||||
|
// Allow interrupting dash attack only into an up/down smash
|
||||||
|
if let AttackKind::DashAttack = self.attack_kind {
|
||||||
|
if state_context.get_current_animation_frame() < state_context.fighter_attributes.dash_attack_cancel_window {
|
||||||
|
if let Some(state) = check_interrupt(state_context) {
|
||||||
|
if let FighterState::GroundAttack(state) = state {
|
||||||
|
if matches!(state.attack_kind, AttackKind::UpSmash) || matches!(state.attack_kind, AttackKind::DownSmash) {
|
||||||
|
return Some(FighterState::GroundAttack(state));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if state_context.is_current_animation_finished() {
|
if state_context.is_current_animation_finished() {
|
||||||
Some(FighterState::Wait(WaitState))
|
Some(FighterState::Wait(WaitState))
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,17 +1,10 @@
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
fighter::{
|
fighter::{
|
||||||
animation::AnimKind,
|
animation::AnimKind, state::{
|
||||||
state::{
|
CollisionContext, EnterContext, FighterState, FighterStateImpl, InterruptContext, UpdateContext, air::{self, AirborneStateCommon}, air_dodge::AirDodgeState, ground, ground_attack,
|
||||||
CollisionContext, EnterContext, FighterState, FighterStateImpl, InterruptContext,
|
|
||||||
UpdateContext,
|
|
||||||
air::{self, AirborneStateCommon},
|
|
||||||
air_dodge::AirDodgeState,
|
|
||||||
ground,
|
|
||||||
},
|
},
|
||||||
},
|
}, input::{DigitalAction, FighterCommands}, math::{int::FGi32, vec::FGVec2},
|
||||||
input::{DigitalAction, FighterCommands},
|
|
||||||
math::{int::FGi32, vec::FGVec2},
|
|
||||||
};
|
};
|
||||||
#[derive(Debug, Hash, Clone, Copy)]
|
#[derive(Debug, Hash, Clone, Copy)]
|
||||||
pub enum JumpType {
|
pub enum JumpType {
|
||||||
@@ -52,6 +45,9 @@ impl FighterStateImpl for JumpSquatState {
|
|||||||
const NAME: &'static str = "JumpSquat";
|
const NAME: &'static str = "JumpSquat";
|
||||||
|
|
||||||
fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option<FighterState> {
|
fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option<FighterState> {
|
||||||
|
if let Some(state) = ground_attack::check_interrupt(state_context) {
|
||||||
|
return Some(state);
|
||||||
|
}
|
||||||
// Special handling for wavedash from jump squat
|
// Special handling for wavedash from jump squat
|
||||||
if state_context.input.has_command(FighterCommands::Shield) {
|
if state_context.input.has_command(FighterCommands::Shield) {
|
||||||
// If aiming sideways and in the deadzone, do a perfect wavedash
|
// If aiming sideways and in the deadzone, do a perfect wavedash
|
||||||
|
|||||||
+344
-67
@@ -9,6 +9,8 @@ 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::{DigitalAction, FighterInputFrame};
|
||||||
|
use crate::math::int::FGi32;
|
||||||
|
use crate::math::vec::FGVec2;
|
||||||
use crate::netcode::GGRSCfg;
|
use crate::netcode::GGRSCfg;
|
||||||
use crate::player::Player;
|
use crate::player::Player;
|
||||||
|
|
||||||
@@ -64,6 +66,34 @@ pub enum FighterAttackCommandType {
|
|||||||
TiltAttackRight,
|
TiltAttackRight,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||||
|
pub enum FlickDirection {
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FlickDirection {
|
||||||
|
const fn smash_move_command(self) -> FighterCommands {
|
||||||
|
match self {
|
||||||
|
Self::Up => FighterCommands::SmashMoveUp,
|
||||||
|
Self::Down => FighterCommands::SmashMoveDown,
|
||||||
|
Self::Left => FighterCommands::SmashMoveLeft,
|
||||||
|
Self::Right => FighterCommands::SmashMoveRight,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn smash_attack_command(self) -> FighterAttackCommandType {
|
||||||
|
match self {
|
||||||
|
Self::Up => FighterAttackCommandType::SmashAttackUp,
|
||||||
|
Self::Down => FighterAttackCommandType::SmashAttackDown,
|
||||||
|
Self::Left => FighterAttackCommandType::SmashAttackLeft,
|
||||||
|
Self::Right => FighterAttackCommandType::SmashAttackRight,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl FighterAttackCommandType {
|
impl FighterAttackCommandType {
|
||||||
pub fn to_attack_kind_grounded(&self) -> AttackKind {
|
pub fn to_attack_kind_grounded(&self) -> AttackKind {
|
||||||
match self {
|
match self {
|
||||||
@@ -193,6 +223,149 @@ impl ButtonHistory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Hash)]
|
||||||
|
struct FlickAxisState {
|
||||||
|
age: u8,
|
||||||
|
eligible: bool,
|
||||||
|
consumed: bool,
|
||||||
|
command_emitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FlickAxisState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
age: u8::MAX,
|
||||||
|
eligible: false,
|
||||||
|
consumed: false,
|
||||||
|
command_emitted: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FlickAxisState {
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
current: FGi32,
|
||||||
|
previous: FGi32,
|
||||||
|
reset_threshold: FGi32,
|
||||||
|
axis_threshold: FGi32,
|
||||||
|
frame_threshold: u8,
|
||||||
|
) {
|
||||||
|
if current.abs() < reset_threshold {
|
||||||
|
*self = Self::default();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if previous.abs() < reset_threshold || previous.signum() != current.signum() {
|
||||||
|
self.age = 1;
|
||||||
|
self.consumed = false;
|
||||||
|
self.command_emitted = false;
|
||||||
|
} else {
|
||||||
|
self.age = self.age.saturating_add(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.eligible =
|
||||||
|
!self.consumed && current.abs() >= axis_threshold && self.age <= frame_threshold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Hash)]
|
||||||
|
struct StickFlickState {
|
||||||
|
x: FlickAxisState,
|
||||||
|
y: FlickAxisState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StickFlickState {
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
current: FGVec2,
|
||||||
|
previous: FGVec2,
|
||||||
|
reset_threshold: FGi32,
|
||||||
|
axis_threshold: FGi32,
|
||||||
|
frame_threshold: u8,
|
||||||
|
) {
|
||||||
|
self.x.update(
|
||||||
|
current.x,
|
||||||
|
previous.x,
|
||||||
|
reset_threshold,
|
||||||
|
axis_threshold,
|
||||||
|
frame_threshold,
|
||||||
|
);
|
||||||
|
self.y.update(
|
||||||
|
current.y,
|
||||||
|
previous.y,
|
||||||
|
reset_threshold,
|
||||||
|
axis_threshold,
|
||||||
|
frame_threshold,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn peek(&self, value: FGVec2) -> Option<FlickDirection> {
|
||||||
|
self.resolve(value, self.x.eligible, self.y.eligible)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns each axis that has not produced a passive command during this
|
||||||
|
/// gesture. This does not consume the flick, so an attack can still use
|
||||||
|
/// the remainder of its smash-input window.
|
||||||
|
fn next_commands(&mut self, value: FGVec2) -> [Option<FlickDirection>; 2] {
|
||||||
|
let use_x = self.x.eligible && !self.x.command_emitted;
|
||||||
|
let use_y = self.y.eligible && !self.y.command_emitted;
|
||||||
|
|
||||||
|
if use_x {
|
||||||
|
self.x.command_emitted = true;
|
||||||
|
}
|
||||||
|
if use_y {
|
||||||
|
self.y.command_emitted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[
|
||||||
|
use_x.then_some(if value.x.is_positive() {
|
||||||
|
FlickDirection::Right
|
||||||
|
} else {
|
||||||
|
FlickDirection::Left
|
||||||
|
}),
|
||||||
|
use_y.then_some(if value.y.is_positive() {
|
||||||
|
FlickDirection::Up
|
||||||
|
} else {
|
||||||
|
FlickDirection::Down
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn consume(&mut self, value: FGVec2) -> Option<FlickDirection> {
|
||||||
|
let direction = self.peek(value)?;
|
||||||
|
// Simultaneous eligible axes are one diagonal gesture. Consuming only
|
||||||
|
// the selected axis would expose the other axis on the next frame.
|
||||||
|
if self.x.eligible {
|
||||||
|
self.x.consumed = true;
|
||||||
|
self.x.eligible = false;
|
||||||
|
}
|
||||||
|
if self.y.eligible {
|
||||||
|
self.y.consumed = true;
|
||||||
|
self.y.eligible = false;
|
||||||
|
}
|
||||||
|
Some(direction)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve(&self, value: FGVec2, use_x: bool, use_y: bool) -> Option<FlickDirection> {
|
||||||
|
if use_x {
|
||||||
|
Some(if value.x.is_positive() {
|
||||||
|
FlickDirection::Right
|
||||||
|
} else {
|
||||||
|
FlickDirection::Left
|
||||||
|
})
|
||||||
|
} else if use_y {
|
||||||
|
Some(if value.y.is_positive() {
|
||||||
|
FlickDirection::Up
|
||||||
|
} else {
|
||||||
|
FlickDirection::Down
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Per-fighter input state. Every field is deterministic rollback state.
|
/// Per-fighter input state. Every field is deterministic rollback state.
|
||||||
#[derive(Component, Clone, Copy, Hash)]
|
#[derive(Component, Clone, Copy, Hash)]
|
||||||
#[require(Rollback)]
|
#[require(Rollback)]
|
||||||
@@ -203,8 +376,8 @@ pub struct FighterInput {
|
|||||||
button_history: [ButtonHistory; 3],
|
button_history: [ButtonHistory; 3],
|
||||||
prev_frame: FighterInputFrame,
|
prev_frame: FighterInputFrame,
|
||||||
current_frame: FighterInputFrame,
|
current_frame: FighterInputFrame,
|
||||||
frames_in_x_flick_deadzone: u8,
|
movement_flick: StickFlickState,
|
||||||
frames_in_y_flick_deadzone: u8,
|
directional_attack_flick: StickFlickState,
|
||||||
attack_command: FighterAttackCommandType,
|
attack_command: FighterAttackCommandType,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,8 +390,8 @@ impl Default for FighterInput {
|
|||||||
button_history: [ButtonHistory::default(); 3],
|
button_history: [ButtonHistory::default(); 3],
|
||||||
prev_frame: FighterInputFrame::default(),
|
prev_frame: FighterInputFrame::default(),
|
||||||
current_frame: FighterInputFrame::default(),
|
current_frame: FighterInputFrame::default(),
|
||||||
frames_in_x_flick_deadzone: 0xFE,
|
movement_flick: StickFlickState::default(),
|
||||||
frames_in_y_flick_deadzone: 0xFE,
|
directional_attack_flick: StickFlickState::default(),
|
||||||
attack_command: FighterAttackCommandType::Neutral,
|
attack_command: FighterAttackCommandType::Neutral,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -368,8 +541,26 @@ impl FighterInput {
|
|||||||
self.current_frame.bindings_for(action).count()
|
self.current_frame.bindings_for(action).count()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_frames_in_smash_move_deadzone(&self) -> u8 {
|
pub fn peek_movement_flick(&self) -> Option<FlickDirection> {
|
||||||
self.frames_in_x_flick_deadzone
|
self.movement_flick.peek(self.current_frame.movement)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn peek_movement_flick_dbg(&self) -> u8 {
|
||||||
|
self.movement_flick.y.age
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn consume_movement_flick(&mut self) -> Option<FlickDirection> {
|
||||||
|
self.movement_flick.consume(self.current_frame.movement)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn peek_directional_attack_flick(&self) -> Option<FlickDirection> {
|
||||||
|
self.directional_attack_flick
|
||||||
|
.peek(self.current_frame.directional_attack)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn consume_directional_attack_flick(&mut self) -> Option<FlickDirection> {
|
||||||
|
self.directional_attack_flick
|
||||||
|
.consume(self.current_frame.directional_attack)
|
||||||
}
|
}
|
||||||
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;
|
||||||
@@ -441,31 +632,30 @@ pub fn postprocess_input(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let x_abs = input.current_frame.movement.x.abs();
|
let movement = input.current_frame.movement;
|
||||||
let y_abs = input.current_frame.movement.y.abs();
|
let previous_movement = input.prev_frame.movement;
|
||||||
|
let directional_attack = input.current_frame.directional_attack;
|
||||||
|
let previous_directional_attack = input.prev_frame.directional_attack;
|
||||||
|
let x_abs = movement.x.abs();
|
||||||
|
let y_abs = 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 {
|
input.movement_flick.update(
|
||||||
input.frames_in_x_flick_deadzone = 0xFE;
|
movement,
|
||||||
} else if input.prev_frame.movement.x.abs() < reset_deadzone
|
previous_movement,
|
||||||
|| input.prev_frame.movement.x.signum() != input.current_frame.movement.x.signum()
|
reset_deadzone,
|
||||||
{
|
axis_threshold,
|
||||||
input.frames_in_x_flick_deadzone = 1;
|
frame_threshold,
|
||||||
} else {
|
);
|
||||||
input.frames_in_x_flick_deadzone = input.frames_in_x_flick_deadzone.saturating_add(1);
|
input.directional_attack_flick.update(
|
||||||
}
|
directional_attack,
|
||||||
|
previous_directional_attack,
|
||||||
if y_abs < reset_deadzone {
|
reset_deadzone,
|
||||||
input.frames_in_y_flick_deadzone = 0xFE;
|
axis_threshold,
|
||||||
} else if input.prev_frame.movement.y.abs() < reset_deadzone
|
frame_threshold,
|
||||||
|| 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
let tap = game_settings.input_common.input_buffer_size;
|
let tap = game_settings.input_common.input_buffer_size;
|
||||||
let jump_pressed = input.is_pressed(DigitalAction::Jump);
|
let jump_pressed = input.is_pressed(DigitalAction::Jump);
|
||||||
@@ -485,62 +675,40 @@ pub fn postprocess_input(
|
|||||||
input.set_lifetime(FighterCommands::GuardHold, tap);
|
input.set_lifetime(FighterCommands::GuardHold, tap);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut y_flick_detected =
|
for flick in input
|
||||||
y_abs >= axis_threshold && input.frames_in_y_flick_deadzone <= frame_threshold;
|
.movement_flick
|
||||||
let mut x_flick_detected =
|
.next_commands(movement)
|
||||||
x_abs >= axis_threshold && input.frames_in_x_flick_deadzone <= frame_threshold;
|
.into_iter()
|
||||||
if y_flick_detected {
|
.flatten()
|
||||||
|
{
|
||||||
|
let command = flick.smash_move_command();
|
||||||
|
match flick {
|
||||||
|
FlickDirection::Up | FlickDirection::Down => {
|
||||||
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;
|
|
||||||
let command = if input.current_frame.movement.y > 0.0 {
|
|
||||||
FighterCommands::SmashMoveUp
|
|
||||||
} else {
|
|
||||||
FighterCommands::SmashMoveDown
|
|
||||||
};
|
|
||||||
input.set_lifetime(command, tap);
|
|
||||||
}
|
}
|
||||||
if x_flick_detected {
|
FlickDirection::Left | FlickDirection::Right => {
|
||||||
input.clear_command(FighterCommands::SmashMoveLeft);
|
input.clear_command(FighterCommands::SmashMoveLeft);
|
||||||
input.clear_command(FighterCommands::SmashMoveRight);
|
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);
|
input.set_lifetime(command, tap);
|
||||||
}
|
if flick == FlickDirection::Up {
|
||||||
if x_flick_detected && y_flick_detected {
|
|
||||||
x_flick_detected = x_abs > y_abs;
|
|
||||||
y_flick_detected = !x_flick_detected;
|
|
||||||
}
|
|
||||||
|
|
||||||
if y_flick_detected && input.current_frame.movement.y > 0.0 {
|
|
||||||
input.set_lifetime(FighterCommands::TapJump, tap);
|
input.set_lifetime(FighterCommands::TapJump, tap);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if attack_pressed {
|
if attack_pressed {
|
||||||
let attack_type = if y_flick_detected {
|
let attack_type = if let Some(flick) = input.consume_movement_flick() {
|
||||||
if input.current_frame.movement.y > 0.0 {
|
flick.smash_attack_command()
|
||||||
FighterAttackCommandType::SmashAttackUp
|
|
||||||
} else {
|
|
||||||
FighterAttackCommandType::SmashAttackDown
|
|
||||||
}
|
|
||||||
} else if x_flick_detected {
|
|
||||||
if input.current_frame.movement.x > 0.0 {
|
|
||||||
FighterAttackCommandType::SmashAttackRight
|
|
||||||
} else {
|
|
||||||
FighterAttackCommandType::SmashAttackLeft
|
|
||||||
}
|
|
||||||
} else if x_abs > y_abs {
|
} else if x_abs > y_abs {
|
||||||
if input.current_frame.movement.x.is_positive() {
|
if movement.x.is_positive() {
|
||||||
FighterAttackCommandType::TiltAttackRight
|
FighterAttackCommandType::TiltAttackRight
|
||||||
} else {
|
} else {
|
||||||
FighterAttackCommandType::TiltAttackLeft
|
FighterAttackCommandType::TiltAttackLeft
|
||||||
}
|
}
|
||||||
} else if y_abs > x_abs {
|
} else if y_abs > x_abs {
|
||||||
if input.current_frame.movement.y.is_positive() {
|
if movement.y.is_positive() {
|
||||||
FighterAttackCommandType::TiltAttackUp
|
FighterAttackCommandType::TiltAttackUp
|
||||||
} else {
|
} else {
|
||||||
FighterAttackCommandType::TiltAttackDown
|
FighterAttackCommandType::TiltAttackDown
|
||||||
@@ -548,10 +716,17 @@ pub fn postprocess_input(
|
|||||||
} else {
|
} else {
|
||||||
FighterAttackCommandType::Neutral
|
FighterAttackCommandType::Neutral
|
||||||
};
|
};
|
||||||
|
|
||||||
|
dbg!(input.movement_flick.y, attack_type);
|
||||||
input.set_lifetime(FighterCommands::Attack, tap);
|
input.set_lifetime(FighterCommands::Attack, tap);
|
||||||
input.set_attack_command_type(attack_type);
|
input.set_attack_command_type(attack_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(flick) = input.consume_directional_attack_flick() {
|
||||||
|
input.set_lifetime(FighterCommands::Attack, tap);
|
||||||
|
input.set_attack_command_type(flick.smash_attack_command());
|
||||||
|
}
|
||||||
|
|
||||||
for command in FighterCommands::iter() {
|
for command in FighterCommands::iter() {
|
||||||
input.update_hold(command);
|
input.update_hold(command);
|
||||||
}
|
}
|
||||||
@@ -563,6 +738,20 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::input::ActionBindingMask;
|
use crate::input::ActionBindingMask;
|
||||||
|
|
||||||
|
const RESET_THRESHOLD: FGi32 = FGi32::lit("0.2");
|
||||||
|
const AXIS_THRESHOLD: FGi32 = FGi32::lit("0.8");
|
||||||
|
const FRAME_THRESHOLD: u8 = 3;
|
||||||
|
|
||||||
|
fn update_flick(state: &mut StickFlickState, current: FGVec2, previous: FGVec2) {
|
||||||
|
state.update(
|
||||||
|
current,
|
||||||
|
previous,
|
||||||
|
RESET_THRESHOLD,
|
||||||
|
AXIS_THRESHOLD,
|
||||||
|
FRAME_THRESHOLD,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn advance_command(input: &mut FighterInput, command: FighterCommands) {
|
fn advance_command(input: &mut FighterInput, command: FighterCommands) {
|
||||||
input.reduce_command_lifetime(command);
|
input.reduce_command_lifetime(command);
|
||||||
input.update_hold(command);
|
input.update_hold(command);
|
||||||
@@ -583,6 +772,94 @@ mod tests {
|
|||||||
assert!(second_binding_pressed(current, previous));
|
assert!(second_binding_pressed(current, previous));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn flick_remains_available_for_the_configured_window() {
|
||||||
|
let mut flick = StickFlickState::default();
|
||||||
|
let right = FGVec2::lit("1", "0");
|
||||||
|
|
||||||
|
update_flick(&mut flick, right, FGVec2::ZERO);
|
||||||
|
assert_eq!(flick.peek(right), Some(FlickDirection::Right));
|
||||||
|
|
||||||
|
for _ in 2..=FRAME_THRESHOLD {
|
||||||
|
update_flick(&mut flick, right, right);
|
||||||
|
assert_eq!(flick.peek(right), Some(FlickDirection::Right));
|
||||||
|
}
|
||||||
|
|
||||||
|
update_flick(&mut flick, right, right);
|
||||||
|
assert_eq!(flick.peek(right), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn passive_command_does_not_consume_flick() {
|
||||||
|
let mut flick = StickFlickState::default();
|
||||||
|
let right = FGVec2::lit("1", "0");
|
||||||
|
|
||||||
|
update_flick(&mut flick, right, FGVec2::ZERO);
|
||||||
|
assert_eq!(
|
||||||
|
flick.next_commands(right),
|
||||||
|
[Some(FlickDirection::Right), None]
|
||||||
|
);
|
||||||
|
assert_eq!(flick.next_commands(right), [None, None]);
|
||||||
|
assert_eq!(flick.peek(right), Some(FlickDirection::Right));
|
||||||
|
|
||||||
|
update_flick(&mut flick, right, right);
|
||||||
|
assert_eq!(flick.consume(right), Some(FlickDirection::Right));
|
||||||
|
assert_eq!(flick.peek(right), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn consumed_flick_rearms_after_returning_below_reset_threshold() {
|
||||||
|
let mut flick = StickFlickState::default();
|
||||||
|
let right = FGVec2::lit("1", "0");
|
||||||
|
|
||||||
|
update_flick(&mut flick, right, FGVec2::ZERO);
|
||||||
|
assert_eq!(flick.consume(right), Some(FlickDirection::Right));
|
||||||
|
update_flick(&mut flick, right, right);
|
||||||
|
assert_eq!(flick.peek(right), None);
|
||||||
|
|
||||||
|
update_flick(&mut flick, FGVec2::ZERO, right);
|
||||||
|
update_flick(&mut flick, right, FGVec2::ZERO);
|
||||||
|
assert_eq!(flick.peek(right), Some(FlickDirection::Right));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn flick_can_reach_activation_threshold_after_entering_reset_deadzone() {
|
||||||
|
let mut flick = StickFlickState::default();
|
||||||
|
let partial = FGVec2::lit("0.4", "0");
|
||||||
|
let right = FGVec2::lit("1", "0");
|
||||||
|
|
||||||
|
update_flick(&mut flick, partial, FGVec2::ZERO);
|
||||||
|
assert_eq!(flick.peek(partial), None);
|
||||||
|
update_flick(&mut flick, right, partial);
|
||||||
|
assert_eq!(flick.peek(right), Some(FlickDirection::Right));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn diagonal_flick_prioritizes_horizontal_and_consumes_the_gesture() {
|
||||||
|
let mut flick = StickFlickState::default();
|
||||||
|
let diagonal = FGVec2::lit("0.9", "1");
|
||||||
|
|
||||||
|
update_flick(&mut flick, diagonal, FGVec2::ZERO);
|
||||||
|
assert_eq!(
|
||||||
|
flick.next_commands(diagonal),
|
||||||
|
[Some(FlickDirection::Right), Some(FlickDirection::Up)]
|
||||||
|
);
|
||||||
|
assert_eq!(flick.consume(diagonal), Some(FlickDirection::Right));
|
||||||
|
assert_eq!(flick.peek(diagonal), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn direction_reversal_starts_a_new_flick() {
|
||||||
|
let mut flick = StickFlickState::default();
|
||||||
|
let right = FGVec2::lit("1", "0");
|
||||||
|
let left = FGVec2::lit("-1", "0");
|
||||||
|
|
||||||
|
update_flick(&mut flick, right, FGVec2::ZERO);
|
||||||
|
assert_eq!(flick.consume(right), Some(FlickDirection::Right));
|
||||||
|
update_flick(&mut flick, left, right);
|
||||||
|
assert_eq!(flick.peek(left), Some(FlickDirection::Left));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn command_can_be_consumed_without_erasing_raw_input() {
|
fn command_can_be_consumed_without_erasing_raw_input() {
|
||||||
let mut input = FighterInput::default();
|
let mut input = FighterInput::default();
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use crate::math::vec::FGVec2;
|
|||||||
pub fn sample_gamepad(pad: &Gamepad, input_map: &BaseInputMap) -> FighterInputFrame {
|
pub fn sample_gamepad(pad: &Gamepad, input_map: &BaseInputMap) -> FighterInputFrame {
|
||||||
let mut input_frame = FighterInputFrame::default();
|
let mut input_frame = FighterInputFrame::default();
|
||||||
let mut movement = Vec2::ZERO;
|
let mut movement = Vec2::ZERO;
|
||||||
|
let mut directional_attack = Vec2::ZERO;
|
||||||
|
|
||||||
for gamepad_element in &input_map.gamepad {
|
for gamepad_element in &input_map.gamepad {
|
||||||
match &gamepad_element.binding {
|
match &gamepad_element.binding {
|
||||||
@@ -34,12 +35,24 @@ pub fn sample_gamepad(pad: &Gamepad, input_map: &BaseInputMap) -> FighterInputFr
|
|||||||
movement.y += sign as f32
|
movement.y += sign as f32
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
InputMapAction::AttackXDir(sign) => {
|
||||||
|
if pressed {
|
||||||
|
directional_attack.x += sign as f32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InputMapAction::AttackYDir(sign) => {
|
||||||
|
if pressed {
|
||||||
|
directional_attack.y += sign as f32;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// gamepad does not have a separate axis for left and right, so sign here should be 1
|
// gamepad does not have a separate axis for left and right, so sign here should be 1
|
||||||
GamepadBinding::Axis(axis, sign) => {
|
GamepadBinding::Axis(axis, sign) => {
|
||||||
let value = pad.get(*axis).unwrap_or(0.0) * (*sign as f32);
|
let value = pad.get(*axis).unwrap_or(0.0) * (*sign as f32);
|
||||||
match gamepad_element.action {
|
match gamepad_element.action {
|
||||||
|
InputMapAction::AttackXDir(_) => directional_attack.x += value,
|
||||||
|
InputMapAction::AttackYDir(_) => directional_attack.y += value,
|
||||||
InputMapAction::MovementXDir(_) => movement.x += value,
|
InputMapAction::MovementXDir(_) => movement.x += value,
|
||||||
InputMapAction::MovementYDir(_) => movement.y += value,
|
InputMapAction::MovementYDir(_) => movement.y += value,
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -49,6 +62,10 @@ pub fn sample_gamepad(pad: &Gamepad, input_map: &BaseInputMap) -> FighterInputFr
|
|||||||
}
|
}
|
||||||
|
|
||||||
input_frame.movement = FGVec2::new(FGi32::from_num(movement.x), FGi32::from_num(movement.y));
|
input_frame.movement = FGVec2::new(FGi32::from_num(movement.x), FGi32::from_num(movement.y));
|
||||||
|
input_frame.directional_attack = FGVec2::new(
|
||||||
|
FGi32::from_num(directional_attack.x),
|
||||||
|
FGi32::from_num(directional_attack.y),
|
||||||
|
);
|
||||||
|
|
||||||
input_frame
|
input_frame
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,12 @@ pub fn sample_keyboard(key: &ButtonInput<KeyCode>, input_map: &BaseInputMap) ->
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
InputMapAction::AttackXDir(sign) => {
|
||||||
|
input_frame.directional_attack.x += (sign) * action_strength;
|
||||||
|
}
|
||||||
|
InputMapAction::AttackYDir(sign) => {
|
||||||
|
input_frame.directional_attack.y += (sign) * action_strength;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ pub enum InputActionState {
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum InputMapAction {
|
pub enum InputMapAction {
|
||||||
|
AttackXDir(i32),
|
||||||
|
AttackYDir(i32),
|
||||||
MovementXDir(i32),
|
MovementXDir(i32),
|
||||||
MovementYDir(i32),
|
MovementYDir(i32),
|
||||||
Jump,
|
Jump,
|
||||||
@@ -26,6 +28,7 @@ impl InputMapAction {
|
|||||||
Self::Shield => Some(crate::input::DigitalAction::Shield),
|
Self::Shield => Some(crate::input::DigitalAction::Shield),
|
||||||
Self::Attack => Some(crate::input::DigitalAction::Attack),
|
Self::Attack => Some(crate::input::DigitalAction::Attack),
|
||||||
Self::MovementXDir(_) | Self::MovementYDir(_) => None,
|
Self::MovementXDir(_) | Self::MovementYDir(_) => None,
|
||||||
|
Self::AttackYDir(_) | Self::AttackXDir(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-1
@@ -14,7 +14,7 @@ pub mod gamepad;
|
|||||||
pub mod keyboard;
|
pub mod keyboard;
|
||||||
pub mod map;
|
pub mod map;
|
||||||
|
|
||||||
pub use buffer::{FighterCommands, FighterInput};
|
pub use buffer::{FighterCommands, FighterInput, FlickDirection};
|
||||||
pub use frame::{ActionBindingMask, DigitalAction, DigitalActionBindings, FighterInputFrame};
|
pub use frame::{ActionBindingMask, DigitalAction, DigitalActionBindings, FighterInputFrame};
|
||||||
pub use map::{
|
pub use map::{
|
||||||
BaseInputMap, GamepadBinding, GamepadInputMapElement, InputActionState, InputMapAction,
|
BaseInputMap, GamepadBinding, GamepadInputMapElement, InputActionState, InputMapAction,
|
||||||
@@ -136,6 +136,11 @@ impl Plugin for FighterInputPlugin {
|
|||||||
action: InputMapAction::Jump,
|
action: InputMapAction::Jump,
|
||||||
binding_slot: 0,
|
binding_slot: 0,
|
||||||
},
|
},
|
||||||
|
GamepadInputMapElement {
|
||||||
|
binding: GamepadBinding::Button(GamepadButton::West),
|
||||||
|
action: InputMapAction::Jump,
|
||||||
|
binding_slot: 1,
|
||||||
|
},
|
||||||
GamepadInputMapElement {
|
GamepadInputMapElement {
|
||||||
binding: GamepadBinding::Button(GamepadButton::LeftTrigger2),
|
binding: GamepadBinding::Button(GamepadButton::LeftTrigger2),
|
||||||
action: InputMapAction::Shield,
|
action: InputMapAction::Shield,
|
||||||
@@ -152,6 +157,17 @@ impl Plugin for FighterInputPlugin {
|
|||||||
action: InputMapAction::Attack,
|
action: InputMapAction::Attack,
|
||||||
binding_slot: 0,
|
binding_slot: 0,
|
||||||
},
|
},
|
||||||
|
//C-stick
|
||||||
|
GamepadInputMapElement {
|
||||||
|
binding: GamepadBinding::Axis(GamepadAxis::RightStickX, 1),
|
||||||
|
action: InputMapAction::AttackXDir(1),
|
||||||
|
binding_slot: 0,
|
||||||
|
},
|
||||||
|
GamepadInputMapElement {
|
||||||
|
binding: GamepadBinding::Axis(GamepadAxis::RightStickY, 1),
|
||||||
|
action: InputMapAction::AttackYDir(1),
|
||||||
|
binding_slot: 0,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
.insert_resource(MenuInputMap {
|
.insert_resource(MenuInputMap {
|
||||||
@@ -259,6 +275,13 @@ fn preprocess_input_frame(frame: &mut FighterInputFrame, game_settings: &GameSet
|
|||||||
if frame.movement.y.abs() < game_settings.input_common.stick_deadzone {
|
if frame.movement.y.abs() < game_settings.input_common.stick_deadzone {
|
||||||
frame.movement.y = FGi32::ZERO;
|
frame.movement.y = FGi32::ZERO;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if frame.directional_attack.x.abs() < game_settings.input_common.stick_deadzone {
|
||||||
|
frame.directional_attack.x = FGi32::ZERO;
|
||||||
|
}
|
||||||
|
if frame.directional_attack.y.abs() < game_settings.input_common.stick_deadzone {
|
||||||
|
frame.directional_attack.y = FGi32::ZERO;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_local_inputs(
|
fn read_local_inputs(
|
||||||
|
|||||||
@@ -153,8 +153,8 @@ pub fn navigate(
|
|||||||
Ok(_entity) => {
|
Ok(_entity) => {
|
||||||
// Successfully navigated
|
// Successfully navigated
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(_e) => {
|
||||||
error!("Navigation error: {}", e)
|
//error!("Navigation error: {}", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user