use crate::fighter::state::jump::{self}; use crate::fighter::state::land::{LandingState, LandingType}; use crate::fighter::state::ledge_grab::LedgeGrabState; use crate::fighter::state::{ CollisionContext, FighterState, InterruptContext, air_attack, air_dodge, }; use crate::fighter::*; use crate::input::FighterCommands; #[derive(Default, Debug, Clone, Copy, Hash)] pub struct AirborneStateCommon { pub is_fast_falling: bool, } pub fn process_fast_fall( airborne_state_common: &mut AirborneStateCommon, input: &mut FighterInput, velocity: &mut FighterVelocity, attributes: &FighterAttributes, ) { if velocity.y.is_negative() { if input.has_command(FighterCommands::SmashMoveDown) { airborne_state_common.is_fast_falling = true; } input.clear_command(FighterCommands::SmashMoveDown); } if airborne_state_common.is_fast_falling { velocity.y = -attributes.fast_fall_speed; } } pub fn integrate_gravity( velocity: &mut FighterVelocity, attributes: &FighterAttributes, fast_fall: bool, ) { velocity.y -= attributes.gravity; let terminal_vel = if fast_fall { -attributes.fast_fall_speed } else { -attributes.terminal_velocity }; velocity.y = velocity.y.max(terminal_vel); } pub fn apply_air_drift( velocity: &mut FighterVelocity, attribs: &FighterAttributes, input: &FighterInput, ) { if input.get_last_frame().movement.x.is_zero() { if velocity.x.abs() < attribs.air_friction { velocity.x = FGi32::ZERO; } else { let velocity_sign = velocity.x.signum(); velocity.x += (-velocity_sign) * attribs.air_friction; } } else { let target_velocity = input.get_last_frame().movement.x * attribs.air_drift_max; let mut acceleration = input.get_last_frame().movement.x.signum() * attribs.air_acceleration_base + input.get_last_frame().movement.x * attribs.air_acceleration_stick; // Acceleration opposes velocity if acceleration * velocity.x < 0 { // do nothing } else { let predicted_vel = velocity.x + acceleration; let will_go_past = predicted_vel.abs() > target_velocity.abs(); if velocity.x.abs() < target_velocity.abs() && will_go_past { // Prevent overshoot acceleration = target_velocity - velocity.x } else if velocity.x.abs() > target_velocity.abs() { let diff = (velocity.x - target_velocity).abs(); acceleration = velocity.x.signum() * (-(attribs.air_friction).min(diff)); } } velocity.x += acceleration; } } pub fn apply_air_motion( velocity: &FighterVelocity, translation: &mut FighterTranslation, prev_translation: &mut FighterPreviousTranslation, ) { // Vertical velocity should be 0 on the ground prev_translation.0 = translation.0; translation.0 += velocity.0; } pub fn update_standard_motion( ctx: &mut crate::fighter::state::UpdateContext, airborne_common: &mut AirborneStateCommon, ) { process_fast_fall( airborne_common, ctx.input, &mut ctx.velocity, ctx.fighter_attributes, ); integrate_gravity( ctx.velocity, ctx.fighter_attributes, airborne_common.is_fast_falling, ); apply_air_drift(ctx.velocity, ctx.fighter_attributes, ctx.input); apply_air_motion(ctx.velocity, ctx.translation, ctx.prev_translation); } pub fn air_interrupts_common( ctx: &mut InterruptContext, airborne_state_common: &AirborneStateCommon, ) -> Option { jump::check_input_double_jump(ctx).or_else(|| { air_dodge::check_input(ctx) .or_else(|| air_attack::check_interrupt(ctx, airborne_state_common)) }) } pub fn collision_interrupt( ctx: &mut CollisionContext, landing_type: LandingType, can_ignore_soft_platforms: bool, ) -> Option { if ctx.velocity.y <= FGi32::ZERO { if let Some(ledge_result) = ctx.air_collide_with_ledges() { return Some(FighterState::LedgeGrab(LedgeGrabState::new(ledge_result.ledge))); } } let res = ctx.air_collide_with_stage(can_ignore_soft_platforms)?; ctx.translation.0 = res.resolved_translation; let bottom_offset = ctx.ecb.rebase_onto_bottom(); ctx.translation.0 += bottom_offset; *ctx.desired_ecb = crate::fighter::ecb::FighterDesiredECB::from(*ctx.ecb); ctx.presentation_snap.0 = true; ctx.surface.land(res.line_id, ctx.ledge_info); Some(FighterState::Land(LandingState::create(landing_type))) } #[cfg(test)] mod tests { use super::*; use crate::{ input::{FighterInput, FighterInputFrame}, math::vec::FGVec2, }; fn test_attributes() -> FighterAttributes { FighterAttributes { pushbox_offset: FGi32::ZERO, pushbox_radius: FGi32::ONE, base_walk_accel: FGi32::ZERO, stick_walk_accel: FGi32::ZERO, max_walk_vel: FGi32::ZERO, dash_acceleration_duration: 0, dash_duration: 0, dash_initial_velocity: FGi32::ZERO, base_dash_accel: FGi32::ZERO, stick_dash_accel: FGi32::ZERO, max_dash_vel: FGi32::ZERO, jumpsquat_duration: 0, short_hop_vertical_velocity: FGi32::ZERO, full_jump_vertical_velocity: FGi32::ZERO, jump_horizontal_velocity: FGi32::ZERO, air_jump_multiplier: FGi32::ZERO, air_jump_horizontal_velocity: FGi32::ZERO, landing_iasa: 0, landing_duration: 0, air_acceleration_base: FGi32::ZERO, air_acceleration_stick: FGi32::ZERO, max_air_horizontal_velocity: FGi32::lit("1.2"), air_drift_max: FGi32::lit("1.2"), ground_friction: FGi32::ZERO, terminal_velocity: FGi32::ZERO, fast_fall_speed: FGi32::ZERO, gravity: FGi32::ZERO, weight: FGi32::lit("1"), air_friction: FGi32::lit("0.001"), air_dodge_duration: 0, air_dodge_velocity: FGi32::ZERO, air_dodge_decay: FGi32::ZERO, air_dodge_intangible_duration: 29, air_dodge_intangible_start: 4, dash_attack_cancel_window: 3, } } #[test] fn zero_air_acceleration_is_symmetric() { let attributes = test_attributes(); let mut right_input = FighterInput::default(); right_input.push_input_frame(FighterInputFrame { movement: FGVec2::new(FGi32::ONE, FGi32::ZERO), ..default() }); let mut left_input = FighterInput::default(); left_input.push_input_frame(FighterInputFrame { movement: FGVec2::new(-FGi32::ONE, FGi32::ZERO), ..default() }); let mut right_velocity = FighterVelocity(FGVec2::ZERO); let mut left_velocity = FighterVelocity(FGVec2::ZERO); apply_air_drift(&mut right_velocity, &attributes, &right_input); apply_air_drift(&mut left_velocity, &attributes, &left_input); assert_eq!(right_velocity.x, FGi32::ZERO); assert_eq!(left_velocity.x, FGi32::ZERO); } }