Files
shinespark/src/fighter/state/air.rs
T
EIREXE 94e292026e
CI / build-and-test (push) Successful in 1m47s
CI / production-build (push) Successful in 11m8s
Fix fighter visuals after landing snap (#67)
## Summary

- Mark landing position corrections as presentation discontinuities.
- Render and frame fighters from the current position for the snap frame.
- Keep previous translation history unchanged for gameplay collision and attack logic.

Reviewed-on: #67
Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
2026-09-26 15:54:25 +00:00

197 lines
6.2 KiB
Rust

use crate::fighter::state::land::{LandingState, LandingType};
use crate::fighter::state::{CollisionContext, FighterState};
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 collision_interrupt(
ctx: &mut CollisionContext,
landing_type: LandingType,
) -> Option<FighterState> {
let res = ctx.air_collide_with_stage()?;
let diff = res.hit_position - ctx.translation.0;
ctx.ecb.bottom -= diff;
ctx.ecb.top -= diff;
ctx.ecb.left -= diff;
ctx.ecb.right -= diff;
ctx.translation.0 = res.hit_position;
ctx.presentation_snap.0 = true;
ctx.surface.land(res.line_id);
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 {
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,
}
}
#[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);
}
}