Compare commits

...
2 Commits
Author SHA1 Message Date
EIREXE e80a961021 Make triggers be bound to airdodge on non-gamecube controllers
CI / production-build (pull_request) Successful in 10m30s
CI / build-and-test (pull_request) Successful in 1m45s
2026-09-26 13:28:36 +02:00
EIREXE 615872f29c Implement being able to drop through soft platforms (#64)
CI / build-and-test (push) Successful in 1m47s
CI / production-build (push) Successful in 10m53s
Reviewed-on: #64
Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
2026-09-26 10:38:51 +00:00
9 changed files with 160 additions and 36 deletions
+2
View File
@@ -88,6 +88,8 @@ pub enum AnimKind {
CrouchIdle, CrouchIdle,
CrouchExit, CrouchExit,
PlatformPass,
FlinchGround, FlinchGround,
FlinchAir, FlinchAir,
+64 -30
View File
@@ -1,25 +1,30 @@
use super::{ use super::{
CollisionContext, EnterContext, FighterStateImpl, InterruptContext, UpdateContext, ground, CollisionContext, EnterContext, FighterStateImpl, InterruptContext, UpdateContext, ground,
}; };
use crate::fighter::FighterSurfaceState;
use crate::fighter::animation::AnimKind; use crate::fighter::animation::AnimKind;
use crate::fighter::state::air::AirborneStateCommon;
use crate::fighter::state::ground::{
grounded_movement_common_interrupts, grounded_movement_standstill_common_interrupts,
};
use crate::fighter::state::platform_pass::PlatformPassState;
use crate::fighter::state::{FighterState, dash, ground_attack, turn, walk}; use crate::fighter::state::{FighterState, dash, ground_attack, turn, walk};
use crate::fighter::state::ground::{grounded_movement_common_interrupts, grounded_movement_standstill_common_interrupts}; use crate::input::FighterCommands;
use crate::stage::line::StagePolyType;
#[derive(Debug, Clone, Hash)] #[derive(Debug, Clone, Hash)]
pub enum CrouchSubState { pub enum CrouchSubState {
Entering, Entering { pass_queued: bool },
Idle, Idle,
Exiting Exiting,
} }
#[derive(Debug, Clone, Hash)] #[derive(Debug, Clone, Hash)]
pub struct CrouchState { pub struct CrouchState {
sub_state: CrouchSubState sub_state: CrouchSubState,
} }
pub fn crouch_idle_interrupts( pub fn crouch_idle_interrupts(state_context: &InterruptContext) -> Option<FighterState> {
state_context: &InterruptContext,
) -> Option<FighterState> {
if let Some(state) = ground_attack::check_interrupt(state_context) { if let Some(state) = ground_attack::check_interrupt(state_context) {
Some(state) Some(state)
} else if let Some(state) = dash::check_input(state_context) { } else if let Some(state) = dash::check_input(state_context) {
@@ -33,9 +38,7 @@ pub fn crouch_idle_interrupts(
} }
} }
pub fn crouch_entering_interrupts( pub fn crouch_entering_interrupts(state_context: &InterruptContext) -> Option<FighterState> {
state_context: &InterruptContext,
) -> Option<FighterState> {
if let Some(state) = ground_attack::check_interrupt(state_context) { if let Some(state) = ground_attack::check_interrupt(state_context) {
Some(state) Some(state)
} else { } else {
@@ -51,54 +54,85 @@ impl FighterStateImpl for CrouchState {
} }
match self.sub_state { match self.sub_state {
CrouchSubState::Entering => { CrouchSubState::Entering { pass_queued } => {
// pass
if let FighterSurfaceState::Grounded { line_id } = state_context.surface {
if let Some(stage_poly) = state_context
.stage_collision
.stage_polys
.get(line_id.polygon)
{
let is_inputting_crouch = -state_context.game_settings.input_common.run_stick_threshold > state_context.input.get_last_frame().movement.y;
let is_on_a_soft_platform = matches!(stage_poly.poly_type, StagePolyType::Platform);
if pass_queued && is_inputting_crouch && is_on_a_soft_platform
{
return Some(FighterState::PlatformPass(PlatformPassState::new(
AirborneStateCommon::default(),
line_id.polygon,
)));
}
}
}
// While entering the crouch animation we do not allow movement inputs, but we do allow attacks and jumps // While entering the crouch animation we do not allow movement inputs, but we do allow attacks and jumps
if state_context.is_current_animation_finished() { if state_context.is_current_animation_finished() {
return Some(FighterState::Crouch( return Some(FighterState::Crouch(CrouchState {
CrouchState { sub_state: CrouchSubState::Idle,
sub_state: CrouchSubState::Idle, }));
}
));
} else { } else {
return crouch_entering_interrupts(state_context); return crouch_entering_interrupts(state_context);
} }
}, }
CrouchSubState::Idle => { CrouchSubState::Idle => {
if state_context.input.get_last_frame().movement.y > -state_context.game_settings.input_common.run_stick_threshold { if state_context.input.get_last_frame().movement.y
return Some(FighterState::Crouch ( > -state_context.game_settings.input_common.run_stick_threshold
CrouchState { {
sub_state: CrouchSubState::Exiting, return Some(FighterState::Crouch(CrouchState {
} sub_state: CrouchSubState::Exiting,
)); }));
} else { } else {
return crouch_idle_interrupts(state_context); return crouch_idle_interrupts(state_context);
} }
}, }
CrouchSubState::Exiting => { CrouchSubState::Exiting => {
if state_context.is_current_animation_finished() { if state_context.is_current_animation_finished() {
return grounded_movement_standstill_common_interrupts(state_context); return grounded_movement_standstill_common_interrupts(state_context);
} else { } else {
return crouch_idle_interrupts(state_context); return crouch_idle_interrupts(state_context);
} }
}, }
} }
} }
fn on_enter(&mut self, state_context: &mut EnterContext) { fn on_enter(&mut self, state_context: &mut EnterContext) {
match self.sub_state { match self.sub_state {
CrouchSubState::Entering => { CrouchSubState::Entering { .. } => {
state_context.play_animation(AnimKind::CrouchEnter, false); state_context.play_animation(AnimKind::CrouchEnter, false);
}, }
CrouchSubState::Idle => { CrouchSubState::Idle => {
state_context.play_animation(AnimKind::CrouchIdle, true); state_context.play_animation(AnimKind::CrouchIdle, true);
}, }
CrouchSubState::Exiting => { CrouchSubState::Exiting => {
state_context.play_animation(AnimKind::CrouchExit, false); state_context.play_animation(AnimKind::CrouchExit, false);
}, }
} }
} }
fn update(&mut self, state_context: &mut UpdateContext) { fn update(&mut self, state_context: &mut UpdateContext) {
if let CrouchSubState::Entering {
pass_queued,
} = &mut self.sub_state
{
if state_context
.input
.has_command(FighterCommands::SmashMoveDown)
{
state_context
.input
.clear_command(FighterCommands::SmashMoveDown);
*pass_queued = true;
}
}
ground::update_friction_motion(state_context); ground::update_friction_motion(state_context);
} }
@@ -119,7 +153,7 @@ pub fn check_input(state_context: &InterruptContext) -> Option<FighterState> {
if input_frame.movement.y <= -state_context.game_settings.input_common.run_stick_threshold { if input_frame.movement.y <= -state_context.game_settings.input_common.run_stick_threshold {
return Some(FighterState::Crouch(CrouchState { return Some(FighterState::Crouch(CrouchState {
sub_state: CrouchSubState::Entering sub_state: CrouchSubState::Entering { pass_queued: false },
})); }));
} }
None None
+17 -1
View File
@@ -42,6 +42,7 @@ pub mod turn;
pub mod wait; pub mod wait;
pub mod walk; pub mod walk;
pub mod crouch; pub mod crouch;
pub mod platform_pass;
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum SurfacePolicy { pub enum SurfacePolicy {
@@ -127,7 +128,9 @@ pub struct InterruptContext<'a> {
pub facing_direction: &'a mut FighterFacingDirection, pub facing_direction: &'a mut FighterFacingDirection,
pub game_settings: &'a GameSettings, pub game_settings: &'a GameSettings,
pub fighter_attributes: &'a FighterAttributes, pub fighter_attributes: &'a FighterAttributes,
pub surface: &'a FighterSurfaceState,
pub animation: AnimationStatus, pub animation: AnimationStatus,
pub stage_collision: &'a StageCollision,
} }
impl InterruptContext<'_> { impl InterruptContext<'_> {
@@ -148,8 +151,10 @@ pub struct EnterContext<'a> {
pub stale_move_queue: &'a mut FighterStaleMoveQueue, pub stale_move_queue: &'a mut FighterStaleMoveQueue,
pub facing_direction: &'a mut FighterFacingDirection, pub facing_direction: &'a mut FighterFacingDirection,
pub animation_frame: &'a mut FighterAnimationFrame, pub animation_frame: &'a mut FighterAnimationFrame,
pub surface: &'a mut FighterSurfaceState,
pub fighter_attributes: &'a FighterAttributes, pub fighter_attributes: &'a FighterAttributes,
pub attack_script_assets: &'a FighterAttackScriptAssets, pub attack_script_assets: &'a FighterAttackScriptAssets,
pub game_settings: &'a GameSettings,
} }
impl EnterContext<'_> { impl EnterContext<'_> {
@@ -160,6 +165,7 @@ impl EnterContext<'_> {
pub struct ExitContext<'a> { pub struct ExitContext<'a> {
pub hitboxes: &'a mut FighterHitboxes, pub hitboxes: &'a mut FighterHitboxes,
pub surface: &'a mut FighterSurfaceState,
} }
pub struct UpdateContext<'a> { pub struct UpdateContext<'a> {
@@ -214,6 +220,7 @@ impl CollisionContext<'_> {
fighter_states! { fighter_states! {
Wait => wait::WaitState, Wait => wait::WaitState,
Walk => walk::WalkState, Walk => walk::WalkState,
PlatformPass => platform_pass::PlatformPassState,
Crouch => crouch::CrouchState, Crouch => crouch::CrouchState,
Dash => dash::DashState, Dash => dash::DashState,
Run => run::RunState, Run => run::RunState,
@@ -268,6 +275,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
animations: &'a FighterAnimations, animations: &'a FighterAnimations,
baked_animations: &'a Assets<BakedFighterAnimations>, baked_animations: &'a Assets<BakedFighterAnimations>,
attributes: &'a FighterAttributes, attributes: &'a FighterAttributes,
stage_collision: &'a StageCollision,
) -> InterruptContext<'a> { ) -> InterruptContext<'a> {
let animation = self.animation_status(animations, baked_animations); let animation = self.animation_status(animations, baked_animations);
InterruptContext { InterruptContext {
@@ -276,6 +284,8 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
game_settings, game_settings,
fighter_attributes: attributes, fighter_attributes: attributes,
animation, animation,
surface: &self.surface,
stage_collision,
} }
} }
@@ -283,6 +293,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
&'a mut self, &'a mut self,
attributes: &'a FighterAttributes, attributes: &'a FighterAttributes,
attack_script_assets: &'a FighterAttackScriptAssets, attack_script_assets: &'a FighterAttackScriptAssets,
game_settings: &'a GameSettings,
) -> EnterContext<'a> { ) -> EnterContext<'a> {
EnterContext { EnterContext {
input: &mut self.input, input: &mut self.input,
@@ -292,14 +303,17 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
stale_move_queue: &mut self.stale_move_queue, stale_move_queue: &mut self.stale_move_queue,
facing_direction: &mut self.facing_direction, facing_direction: &mut self.facing_direction,
animation_frame: &mut self.animation_frame, animation_frame: &mut self.animation_frame,
surface: &mut self.surface,
fighter_attributes: attributes, fighter_attributes: attributes,
attack_script_assets, attack_script_assets,
game_settings
} }
} }
fn exit_context(&mut self) -> ExitContext<'_> { fn exit_context(&mut self) -> ExitContext<'_> {
ExitContext { ExitContext {
hitboxes: &mut self.hitboxes, hitboxes: &mut self.hitboxes,
surface: &mut self.surface,
} }
} }
@@ -407,7 +421,7 @@ fn transition_state(
game_settings.fighter_common.leave_ground_ecb_frame_lock_duration, game_settings.fighter_common.leave_ground_ecb_frame_lock_duration,
); );
} }
next.on_enter(&mut frame.enter_context(attributes, attack_script_assets)); next.on_enter(&mut frame.enter_context(attributes, attack_script_assets, game_settings));
*current = next; *current = next;
info!("{} -> {}", old_name, new_name); info!("{} -> {}", old_name, new_name);
} }
@@ -417,6 +431,7 @@ pub fn state_interrupt_system(
game_settings: Res<GameSettings>, game_settings: Res<GameSettings>,
baked_animations: Res<Assets<BakedFighterAnimations>>, baked_animations: Res<Assets<BakedFighterAnimations>>,
fighter_manifests: Res<Assets<FighterManifest>>, fighter_manifests: Res<Assets<FighterManifest>>,
stage_collision: Res<StageCollision>,
) { ) {
for fighter in &mut query { for fighter in &mut query {
let StatefulFighterQueryItem { let StatefulFighterQueryItem {
@@ -490,6 +505,7 @@ pub fn state_interrupt_system(
animations, animations,
&baked_animations, &baked_animations,
&manifest.attributes, &manifest.attributes,
&stage_collision
)) ))
}; };
+61
View File
@@ -0,0 +1,61 @@
use super::{CollisionContext, EnterContext, FighterStateImpl, InterruptContext, UpdateContext};
use crate::fighter::animation::AnimKind;
use crate::fighter::state::air::AirborneStateCommon;
use crate::fighter::state::{ExitContext, FighterState, air, jump};
#[derive(Debug, Clone, Copy, Hash, Default)]
pub struct PlatformPassState {
airborne_state_common: AirborneStateCommon,
ignored_polygon: usize,
}
impl PlatformPassState {
pub fn new(airborne_state_common: AirborneStateCommon, ignored_polygon: usize) -> Self {
Self {
airborne_state_common,
ignored_polygon
}
}
}
impl FighterStateImpl for PlatformPassState {
const NAME: &'static str = "PlatformPass";
fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option<FighterState> {
jump::check_input_double_jump(state_context)
}
fn on_enter(&mut self, state_context: &mut EnterContext) {
state_context.surface.ignore_polygon(self.ignored_polygon, state_context.game_settings.fighter_common.platform_pass_platform_ignore_duration_frames);
state_context.velocity.y = state_context.game_settings.fighter_common.platform_pass_initial_downwards_velocity;
state_context.play_animation(AnimKind::PlatformPass, false);
}
fn on_exit(&mut self, state_context: &mut ExitContext) {
state_context.surface.clear_polygon_ignore(self.ignored_polygon);
}
fn update(&mut self, state_context: &mut UpdateContext) {
air::update_standard_motion(state_context, &mut self.airborne_state_common);
}
fn check_collision_interrupt(
&mut self,
state_context: &mut CollisionContext,
) -> Option<FighterState> {
air::collision_interrupt(
state_context,
crate::fighter::state::land::LandingType::Normal,
)
}
fn is_visually_grounded<'a>(
&'a self,
) -> bool {
false
}
fn surface_policy(&self) -> super::SurfacePolicy {
super::SurfacePolicy::Airborne(crate::fighter::surface::EcbLockPolicy::StartOnDetach)
}
}
+8
View File
@@ -57,6 +57,14 @@ impl FighterSurfaceState {
*self = Self::Grounded { line_id }; *self = Self::Grounded { line_id };
} }
pub fn clear_polygon_ignore(&mut self, polygon: usize) {
let Self::Airborne { ignored_polys, .. } = self else {
return;
};
ignored_polys.retain(|element| element.polygon != polygon);
}
pub fn ignore_polygon(&mut self, polygon: usize, duration_frames: u32) { pub fn ignore_polygon(&mut self, polygon: usize, duration_frames: u32) {
let Self::Airborne { ignored_polys, .. } = self else { let Self::Airborne { ignored_polys, .. } = self else {
return; return;
+3 -1
View File
@@ -39,7 +39,9 @@ pub struct FighterSettingsCommon {
/// Maximum angle changed caused by directional influence /// Maximum angle changed caused by directional influence
pub di_max_angle: FGi32, pub di_max_angle: FGi32,
pub leave_ground_ecb_frame_lock_duration: u32 pub leave_ground_ecb_frame_lock_duration: u32,
pub platform_pass_platform_ignore_duration_frames: u32,
pub platform_pass_initial_downwards_velocity: FGi32,
} }
#[derive(Reflect, Serialize, Deserialize, Clone)] #[derive(Reflect, Serialize, Deserialize, Clone)]
#[reflect(opaque)] #[reflect(opaque)]
+2
View File
@@ -233,6 +233,8 @@ pub fn postprocess_input(
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_to_set = 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 {
+2 -3
View File
@@ -116,13 +116,12 @@ impl Plugin for FighterInputPlugin {
binding: GamepadBinding::Button(GamepadButton::North), binding: GamepadBinding::Button(GamepadButton::North),
action: InputMapAction::Jump, action: InputMapAction::Jump,
}, },
// shield/airdodge -- both shoulder buttons, Melee-style
GamepadInputMapElement { GamepadInputMapElement {
binding: GamepadBinding::Button(GamepadButton::LeftTrigger), binding: GamepadBinding::Button(GamepadButton::LeftTrigger2),
action: InputMapAction::Shield, action: InputMapAction::Shield,
}, },
GamepadInputMapElement { GamepadInputMapElement {
binding: GamepadBinding::Button(GamepadButton::RightTrigger), binding: GamepadBinding::Button(GamepadButton::RightTrigger2),
action: InputMapAction::Shield, action: InputMapAction::Shield,
}, },
// attack // attack
+1 -1
View File
@@ -34,7 +34,7 @@ pub struct StageLineID {
#[derive(Reflect, Clone, Default)] #[derive(Reflect, Clone, Default)]
pub struct StagePoly { pub struct StagePoly {
poly_type: StagePolyType, pub poly_type: StagePolyType,
pub segments: Vec<StagePolyLineSegment>, pub segments: Vec<StagePolyLineSegment>,
} }