diff --git a/assets/fighters/test/test.fighter.ron b/assets/fighters/test/test.fighter.ron index c23f653..6ec9de7 100644 --- a/assets/fighters/test/test.fighter.ron +++ b/assets/fighters/test/test.fighter.ron @@ -1,37 +1,44 @@ ( id: TestFighter, - model_path: "fighters/test/test-fighter-normal.glb", + model_path: "fighters/test/test-fighter-2.glb", + model_scale: "10.0", baked_animation_path: "fighters/test/test.fighterbake", + icon: ( + path: "fighters/test/icon.png", + ), attributes: ( base_walk_accel: "0.1", stick_walk_accel: "0.16", - max_walk_vel: "0.7", - ground_friction: "0.09", + max_walk_vel: "1.0", + ground_friction: "0.1", dash_acceleration_duration: 13, dash_duration: 24, - base_dash_accel: "0.02", - stick_dash_accel: "0.065", - max_dash_vel: "1.1", - short_hop_vertical_velocity: "1.05", - full_jump_vertical_velocity: "1.6", + base_dash_accel: "0.1", + stick_dash_accel: "0.05", + max_dash_vel: "2.3", + short_hop_vertical_velocity: "1.35", + full_jump_vertical_velocity: "2.1", jump_horizontal_velocity: "0.7", - air_jump_horizontal_velocity: "0.5", + air_jump_horizontal_velocity: "0.96", air_jump_multiplier: "1.0", dash_initial_velocity: "1.4", - terminal_velocity: "1.3", - gravity: "0.064", + terminal_velocity: "1.6", + gravity: "0.084", + weight: "100.0", jumpsquat_duration: 5, landing_iasa: 4, landing_duration: 30, - max_air_horizontal_velocity: "1.35", - air_acceleration_base: "0.19", - air_acceleration_stick: "0.09", - air_friction: "0.05", + max_air_horizontal_velocity: "2.5", + air_drift_max: "1.0", + air_acceleration_base: "0.03", + air_acceleration_stick: "0.01", + air_friction: "0.01", air_dodge_duration: 49, air_dodge_velocity: "2.790", - air_dodge_decay: "0.9" + air_dodge_decay: "0.9", + ecb_vertical_midpoint_offset: "0.0" ), camera: ( vertical_origin_offset: "8.0", @@ -42,20 +49,166 @@ visibility_radius: "5.0", ), animations: { - Wait: "PlyPurin5K_Share_ACTION_Wait", - Walk: "PlyPurin5K_Share_ACTION_WalkFast", - Run: "PlyPurin5K_Share_ACTION_Run", - Dash: "PlyPurin5K_Share_ACTION_Dash", - JumpSquat: "PlyPurin5K_Share_ACTION_Landing", - JumpForward: "PlyPurin5K_Share_ACTION_JumpF", - JumpBack: "PlyPurin5K_Share_ACTION_JumpB", - Landing: "PlyPurin5K_Share_ACTION_Landing", - AirDodge: "PlyPurin5K_Share_ACTION_EscapeAir", - DoubleJump: "PlyPurin5K_Share_ACTION_JumpAerialF1", - Fall: "PlyPurin5K_Share_ACTION_Fall", - Turn: "PlyPurin5K_Share_ACTION_Turn", + Wait: "Idle_Loop_Game", + Walk: "Walk_Loop_IK_Game", + Run: "Run_Game", + Dash: "Dash_Game", + JumpSquat: "Jump_Squat_Game", + JumpForward: "Jump_Start_IK_Game", + JumpBack: "JumpB_Game", + LandingSpecial: "SpecialLand_Game", + Landing: "Jump_Land_Game", + AirDodge: "Idle_Loop_Game", + DoubleJumpForward: "Jump_Start_IK_Game", + DoubleJumpBack: "JumpB_Game", + Fall: "Fall_Loop_Game", + Turn: "Turn_Game", + AttackJab1: "Punch_Jab_Game", + AttackUpTilt: "Idle_Loop_Game", + AttackForwardTilt: "Idle_Loop_Game", + AttackDownTilt: "Idle_Loop_Game" }, attack_scripts: { - NAir: "fighters/test/scripts/fair.attack.rhai" - } + NeutralAir: "fighters/test/scripts/fair.attack.rhai", + Jab: "fighters/test/scripts/fair.attack.rhai", + UpTilt: "fighters/test/scripts/uptilt.attack.rhai", + ForwardTilt: "fighters/test/scripts/uptilt.attack.rhai", + DownTilt: "fighters/test/scripts/uptilt.attack.rhai", + }, + hurtboxes: [ + // center one + ( + bone: "spine_01", + offset: (x: "0.0", y: "0.0", z: "0.0"), + rotation: FGVec3(x: "0.0", y: "0.0", z: "0.0"), + half_length: "0.05", + radius: "0.15" + ), + ( + bone: "spine_03", + offset: (x: "0.0", y: "0.0", z: "0.0"), + rotation: FGVec3(x: "0.0", y: "0.0", z: "0.0"), + half_length: "0.05", + radius: "0.15" + ), + ( + bone: "Head", + offset: (x: "0.0", y: "0.125", z: "0.0"), + rotation: FGVec3(x: "0.0", y: "0.0", z: "0.0"), + half_length: "0.05", + radius: "0.1" + ), + ( + bone: "thigh_r", + offset: (x: "0.0", y: "0.15", z: "-0.01"), + rotation: FGVec3(x: "0.0", y: "0.0", z: "0.0"), + half_length: "0.2", + radius: "0.07" + ), + ( + bone: "thigh_l", + offset: (x: "0.0", y: "0.15", z: "-0.01"), + rotation: FGVec3(x: "0.0", y: "0.0", z: "0.0"), + half_length: "0.2", + radius: "0.07" + ), + ( + bone: "calf_l", + offset: (x: "0.0", y: "0.15", z: "-0.0"), + rotation: FGVec3(x: "0.0", y: "0.0", z: "0.0"), + half_length: "0.2", + radius: "0.05" + ), + ( + bone: "calf_r", + offset: (x: "0.0", y: "0.15", z: "-0.0"), + rotation: FGVec3(x: "0.0", y: "0.0", z: "0.0"), + half_length: "0.2", + radius: "0.05" + ), + ( + bone: "foot_r", + offset: (x: "0.0", y: "0.1", z: "0.01"), + rotation: FGVec3(x: "-25.0", y: "0.0", z: "0.0"), + half_length: "0.1", + radius: "0.04" + ), + ( + bone: "foot_l", + offset: (x: "0.0", y: "0.1", z: "0.01"), + rotation: FGVec3(x: "-25.0", y: "0.0", z: "0.0"), + half_length: "0.1", + radius: "0.04" + ), + ( + bone: "upperarm_l", + offset: (x: "0.0", y: "0.09", z: "-0.0"), + rotation: FGVec3(x: "0.0", y: "0.0", z: "0.0"), + half_length: "0.125", + radius: "0.05" + ), + ( + bone: "lowerarm_l", + offset: (x: "0.0", y: "0.12", z: "-0.0"), + rotation: FGVec3(x: "0.0", y: "0.0", z: "0.0"), + half_length: "0.115", + radius: "0.04" + ), + ( + bone: "hand_l", + offset: (x: "0.0", y: "0.05", z: "-0.0"), + rotation: FGVec3(x: "0.0", y: "0.0", z: "0.0"), + half_length: "0.01", + radius: "0.04" + ), + ( + bone: "upperarm_r", + offset: (x: "0.0", y: "0.09", z: "-0.0"), + rotation: FGVec3(x: "0.0", y: "0.0", z: "0.0"), + half_length: "0.125", + radius: "0.05" + ), + ( + bone: "lowerarm_r", + offset: (x: "0.0", y: "0.12", z: "-0.0"), + rotation: FGVec3(x: "0.0", y: "0.0", z: "0.0"), + half_length: "0.115", + radius: "0.04" + ), + ( + bone: "hand_r", + offset: (x: "0.0", y: "0.05", z: "-0.0"), + rotation: FGVec3(x: "0.0", y: "0.0", z: "0.0"), + half_length: "0.01", + radius: "0.04" + ), + /*( + bone: "RFootJA", + offset: (x: "0.0", y: "0.5", z: "0.0"), + rotation: FGVec3(x: "0.0", y: "0.0", z: "90.0"), + half_length: "0.5", + radius: "1.0" + ), + ( + bone: "JOBJ_47", + offset: (x: "0.0", y: "0.0", z: "0.0"), + rotation: FGVec3(x: "0.0", y: "0.0", z: "90.0"), + half_length: "0.5", + radius: "1.0" + ), + ( + bone: "LFootJA", + offset: (x: "0.0", y: "0.5", z: "0.0"), + rotation: FGVec3(x: "0.0", y: "0.0", z: "90.0"), + half_length: "0.5", + radius: "1.0" + ), + ( + bone: "JOBJ_41", + offset: (x: "0.0", y: "0.0", z: "0.0"), + rotation: FGVec3(x: "0.0", y: "0.0", z: "90.0"), + half_length: "0.5", + radius: "1.0" + ),*/ + ] ) diff --git a/assets/game.stylesheet.ron b/assets/game.stylesheet.ron index 21cdbad..24fa103 100644 --- a/assets/game.stylesheet.ron +++ b/assets/game.stylesheet.ron @@ -1,4 +1,19 @@ ( + cursor: ( + path: "ui/textures/cursor.png", + sampling: Nearest + ), + css_token: ( + path: "ui/textures/fighter_select_token.png", + sampling: Linear + ), + player_colors: [ + Srgba((red: 0.780, green: 0.420, blue: 0.933, alpha: 1.000)), + Srgba((red: 0.843, green: 0.227, blue: 0.204, alpha: 1.000)), + Srgba((red: 0.333, green: 0.804, blue: 0.078, alpha: 1.000)), + Srgba((red: 0.075, green: 0.376, blue: 0.416, alpha: 1.000)), + Srgba((red: 0.682, green: 0.878, blue: 0.914, alpha: 1.000)) + ], button_styles: { MainMenu: ( focus_outline_width_px: 4, @@ -24,8 +39,29 @@ ) ), CharacterSelectCharacter: ( - focus_outline_width_px: 4, - focus_outline_offset_px: 2, + focus_outline_width_px: 10, + focus_outline_offset_px: 5, + focus_outline_color: Srgba((red: 1.0, green: 1.0, blue: 0.0, alpha: 1.0)), + normal: ( + background_color: Srgba((red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)), + text_color: Srgba((red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)), + box_shadow: (), + border_size: 10, + ), + press: ( + background_color: Srgba((red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)), + text_color: Srgba((red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)), + box_shadow: (), + ), + hover: ( + background_color: Srgba((red: 0.0, green: 1.0, blue: 0.0, alpha: 1.0)), + text_color: Srgba((red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)), + box_shadow: (), + ) + ), + CharacterSelectStartBanner: ( + focus_outline_width_px: 10, + focus_outline_offset_px: 5, focus_outline_color: Srgba((red: 1.0, green: 1.0, blue: 0.0, alpha: 1.0)), normal: ( background_color: Srgba((red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)), @@ -42,6 +78,51 @@ text_color: Srgba((red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)), box_shadow: (), ) - ) + ), + StageSelectIcon: ( + focus_outline_width_px: 4, + focus_outline_offset_px: 2, + focus_outline_color: Srgba((red: 1.0, green: 1.0, blue: 0.0, alpha: 1.0)), + normal: ( + border_color: Srgba((red: 1.0, green: 1.0, blue: 0.0, alpha: 1.0)), + background_color: Srgba((red: 1.0, green: 0.0, blue: 0.0, alpha: 0.0)), + text_color: Srgba((red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)), + border_size: 10, + border_radius: 40.0, + box_shadow: ( + spread_radius: 5.0, + blur_radius: 5.0, + offset_px_y: 10, + offset_px_x: 10, + color: Srgba((red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0)) + ), + ), + press: ( + background_color: Srgba((red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)), + border_color: Srgba((red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)), + text_color: Srgba((red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)), + border_radius: 40.0, + border_size: 10, + box_shadow: ( + spread_radius: 5.0, + offset_px_y: 10, + offset_px_x: 10, + color: Srgba((red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0)) + ), + ), + hover: ( + background_color: Srgba((red: 0.0, green: 1.0, blue: 0.0, alpha: 1.0)), + border_color: Srgba((red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)), + border_radius: 40.0, + border_size: 10, + text_color: Srgba((red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)), + box_shadow: ( + spread_radius: 5.0, + offset_px_y: 10, + offset_px_x: 10, + color: Srgba((red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0)) + ), + ) + ), } ) \ No newline at end of file diff --git a/assets/game_settings.ron b/assets/game_settings.ron index 81090eb..3974fb0 100644 --- a/assets/game_settings.ron +++ b/assets/game_settings.ron @@ -5,8 +5,9 @@ smash_input_reset_axis_threshold: "0.25", smash_input_axis_threshold: "0.8", run_stick_threshold: "0.625", - smash_input_frame_threshold: 2, - input_buffer_size: 5 + smash_input_frame_threshold: 3, + input_buffer_size: 3, + turn_into_dashback_window: 2, ), fighter_common: ( @@ -15,7 +16,7 @@ ground_friction_over_walk_speed_multiplier: "2.0" ), stage_manifest_paths: [ - "stages/test.stage.ron" + "stages/battlefield/test.stage.ron" ], fighter_manifest_paths: [ "fighters/test/test.fighter.ron" diff --git a/assets/stages/battlefield/test.stage.ron b/assets/stages/battlefield/test.stage.ron new file mode 100644 index 0000000..29f4d6b --- /dev/null +++ b/assets/stages/battlefield/test.stage.ron @@ -0,0 +1,139 @@ +( + id: TestStage, + model_path: "stages/battlefield/battlefield.glb#Scene0", + icon: ( + path: "stages/battlefield/icon.png" + ), + + // Battlefield + /*camera: ( + left: -200.0, + right: 200.0, + bottom: -59.0, + top: 170.0, + origin: (0.0, 44.0), + vertical_fov_degrees: 30.0, + min_depth: 83.0, + max_depth: 1000.0, + subject_scale: 1.5, + fighter_forward_extent_scale: 1.5, + tracking_smoothness: 1.8, + vertical_pan_degrees: -10.0, + horizontal_pan_degrees_per_unit: 0.1, + vertical_pan_degrees_per_unit: 0.1, + max_horizontal_pan_degrees: 17.5, + max_upward_pan_degrees: 5.0, + max_downward_pan_degrees: 7.0, + ),*/ + // Yoshi's story + + /*camera: ( + left: -180.0, + right: 179.0, + bottom: -71.0, + top: 169.0, + origin: (0.0, 44.0), + vertical_fov_degrees: 20.0, + min_depth: 100.0, + max_depth: 1000.0, + subject_scale: 1.0, + fighter_forward_extent_scale: 1.0, + tracking_smoothness: 1.0, + vertical_pan_degrees: -10.0, + horizontal_pan_degrees_per_unit: 0.05, + vertical_pan_degrees_per_unit: 0.15, + max_horizontal_pan_degrees: 17.5, + max_upward_pan_degrees: 5.0, + max_downward_pan_degrees: 7.0, + ),*/ + + camera: ( + left: -180.0, + right: 179.0, + bottom: -71.0, + top: 169.0, + origin: (0.0, 44.0), + vertical_fov_degrees: 20.0, + min_depth: 100.0, + max_depth: 1000.0, + subject_scale: 1.5, + fighter_forward_extent_scale: 1.0, + tracking_smoothness: 1.0, + vertical_pan_degrees: -10.0, + horizontal_pan_degrees_per_unit: 0.05, + vertical_pan_degrees_per_unit: 0.15, + max_horizontal_pan_degrees: 17.5, + max_upward_pan_degrees: 5.0, + max_downward_pan_degrees: 7.0, + ), + polygons: [ + ( + poly_type: Platform, + lines: [ + ( + (x: "-20.0", y: "47.0"), + Floor + ), + ( + (x: "20.0", y: "47.0"), + Floor + ) + ] + ), + ( + poly_type: Platform, + lines: [ + ( + (x: "-65.0", y: "24.0"), + Floor + ), + ( + (x: "-25.0", y: "24.0"), + Floor + ) + ] + ), + ( + poly_type: Platform, + lines: [ + ( + (x: "25.0", y: "24.0"), + Floor + ), + ( + (x: "65.0", y: "24.0"), + Floor + ) + ] + ), + ( + poly_type: Closed, + lines: [ + ( + (x: "-68.0", y: "-3.0"), + Wall + ), + ( + (x: "-68.0", y: "0.0"), + Floor, + ), + ( + (x: "0.0", y: "0.0"), + Floor + ), + ( + (x: "68", y: "0.0"), + Wall + ), + ( + (x: "68.0", y: "-3.0"), + Ceiling + ), + ( + (x: "0.0", y: "-15.0"), + Ceiling + ), + ] + ) + ] +) diff --git a/src/fighter/animation.rs b/src/fighter/animation.rs index 6ca69f4..c11a159 100644 --- a/src/fighter/animation.rs +++ b/src/fighter/animation.rs @@ -73,8 +73,10 @@ pub enum AnimKind { JumpSquat, JumpForward, JumpBack, - DoubleJump, + DoubleJumpForward, + DoubleJumpBack, Landing, + LandingSpecial, AirDodge, Fall, Turn, diff --git a/src/fighter/attributes.rs b/src/fighter/attributes.rs index 6985fde..7c7caeb 100644 --- a/src/fighter/attributes.rs +++ b/src/fighter/attributes.rs @@ -32,6 +32,7 @@ pub struct FighterAttributes { pub air_acceleration_base: FGi32, pub air_acceleration_stick: FGi32, pub max_air_horizontal_velocity: FGi32, + pub air_drift_max: FGi32, pub ground_friction: FGi32, pub terminal_velocity: FGi32, diff --git a/src/fighter/state/air.rs b/src/fighter/state/air.rs index 74c49aa..9e6ac76 100644 --- a/src/fighter/state/air.rs +++ b/src/fighter/state/air.rs @@ -19,31 +19,23 @@ pub fn apply_air_drift( } } else { let target_velocity = - input.get_last_frame().movement.x.signum() * attribs.max_air_horizontal_velocity; - let acceleration = - input.get_last_frame().movement.x.signum() * attribs.air_acceleration_base; + input.get_last_frame().movement.x * attribs.air_drift_max; let mut acceleration = - acceleration + input.get_last_frame().movement.x * attribs.air_acceleration_stick; - let velocity_x = velocity.x; - if !(velocity_x * acceleration < FGi32::ZERO) { - if acceleration > FGi32::ZERO { - if velocity_x + acceleration > target_velocity { - acceleration = -attribs.air_friction; - if velocity_x + acceleration < target_velocity { - acceleration = target_velocity - velocity_x; - } - if velocity_x + acceleration > attribs.max_air_horizontal_velocity { - acceleration = attribs.max_air_horizontal_velocity - velocity_x; - } - } - } else if velocity_x + acceleration < target_velocity { - acceleration = attribs.air_friction; - if velocity_x + acceleration > target_velocity { - acceleration = target_velocity - velocity_x; - } - if velocity_x + acceleration < -attribs.max_air_horizontal_velocity { - acceleration = -attribs.max_air_horizontal_velocity - velocity_x; - } + 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)); } } @@ -60,3 +52,69 @@ pub fn apply_air_motion( prev_translation.0 = translation.0; translation.0 += velocity.0; } + +#[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, + 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); + } +} diff --git a/src/fighter/state/air_dodge.rs b/src/fighter/state/air_dodge.rs index a6b2178..379c1e3 100644 --- a/src/fighter/state/air_dodge.rs +++ b/src/fighter/state/air_dodge.rs @@ -1,14 +1,9 @@ use crate::{ fighter::{ - animation::AnimKind, - collision, - state::{ - FighterState, FighterStateContext, FighterStateImpl, air, fall::FallState, - ground::GroundedStateCommon, land::LandingState, + animation::AnimKind, collision, state::{ + FighterState, FighterStateContext, FighterStateImpl, air, fall::FallState, ground::GroundedStateCommon, land::{LandingState, LandingType}, }, - }, - input, - math::vec::FGVec2, + }, input, math::vec::FGVec2, }; #[derive(Debug, Clone, Hash)] @@ -47,7 +42,7 @@ impl FighterStateImpl for AirDodgeState { let grounded_common = GroundedStateCommon { current_line_id: res.line_id, }; - Some(FighterState::Land(LandingState { grounded_common })) + Some(FighterState::Land(LandingState { grounded_common, landing_type: LandingType::Special })) } else { None } diff --git a/src/fighter/state/fall.rs b/src/fighter/state/fall.rs index 60d71b7..117ee4a 100644 --- a/src/fighter/state/fall.rs +++ b/src/fighter/state/fall.rs @@ -2,7 +2,7 @@ use super::{FighterStateContext, FighterStateImpl}; use crate::fighter::animation::AnimKind; use crate::fighter::collision; use crate::fighter::state::ground::GroundedStateCommon; -use crate::fighter::state::land::LandingState; +use crate::fighter::state::land::{LandingState, LandingType}; use crate::fighter::state::{FighterState, air, jump}; #[derive(Debug, Clone, Copy, Hash)] @@ -45,7 +45,7 @@ impl FighterStateImpl for FallState { let grounded_common = GroundedStateCommon { current_line_id: res.line_id, }; - Some(FighterState::Land(LandingState { grounded_common })) + Some(FighterState::Land(LandingState::create(grounded_common, LandingType::Normal))) } else { None } diff --git a/src/fighter/state/ground.rs b/src/fighter/state/ground.rs index b38eadb..38ff64f 100644 --- a/src/fighter/state/ground.rs +++ b/src/fighter/state/ground.rs @@ -9,9 +9,9 @@ use crate::fighter::{ FighterAttributes, FighterPreviousTranslation, FighterTranslation, FighterVelocity, }; use crate::game_settings::GameSettings; -use crate::math::int::FGi32; +use crate::math::int::{FGWide, FGi32}; use crate::math::vec::FGVec2; -use crate::stage::line::{StageLineID, StagePolyLineSegmentType}; +use crate::stage::line::{StageLineID, StagePoly, StagePolyLineSegment, StagePolyLineSegmentType}; use bevy::prelude::*; @@ -74,6 +74,85 @@ pub enum GroundedMotionResult { InAir, } +enum GroundedPositionResult { + InGround { + point: FGVec2, + segment_idx: usize, + blocked: bool, + }, + InAir, +} + +fn is_walkable_floor(segment: &StagePolyLineSegment) -> bool { + segment.segment_type == StagePolyLineSegmentType::Floor && segment.normal.y > FGi32::ZERO +} + +fn resolve_grounded_position( + stage_poly: &StagePoly, + starting_segment_idx: usize, + point: FGVec2, + can_walk_off: bool, +) -> GroundedPositionResult { + let mut segment_idx = starting_segment_idx; + + // A single frame can cross several short connected segments. The bound also + // prevents malformed closed polygons from cycling forever. + for _ in 0..stage_poly.segments.len() { + let Some(segment) = stage_poly.get_segment(segment_idx) else { + return GroundedPositionResult::InAir; + }; + if !is_walkable_floor(segment) { + return GroundedPositionResult::InAir; + } + + let segment_vector = segment.segment.point2() - segment.segment.point1(); + let projection = segment_vector.dot(point - segment.segment.point1()); + let length_squared = segment_vector.length_squared(); + + let (next_segment_idx, edge_point) = if projection < FGWide::ZERO { + ( + stage_poly.get_prev_segment_index(segment_idx), + segment.segment.point1(), + ) + } else if projection > length_squared { + ( + stage_poly.get_next_segment_index(segment_idx), + segment.segment.point2(), + ) + } else { + let projected_point = segment.segment.point1() + + (point - segment.segment.point1()).project_onto(segment.segment.direction()); + return GroundedPositionResult::InGround { + point: projected_point, + segment_idx, + blocked: false, + }; + }; + + if let Some(next_segment_idx) = next_segment_idx + && next_segment_idx != segment_idx + && stage_poly + .get_segment(next_segment_idx) + .is_some_and(is_walkable_floor) + { + segment_idx = next_segment_idx; + continue; + } + + return if can_walk_off { + GroundedPositionResult::InAir + } else { + GroundedPositionResult::InGround { + point: edge_point, + segment_idx, + blocked: true, + } + }; + } + + GroundedPositionResult::InAir +} + pub fn apply_grounded_motion( velocity: &FighterVelocity, translation: &mut FighterTranslation, @@ -87,7 +166,7 @@ pub fn apply_grounded_motion( pub fn collide_with_stage_grounded( state_context: &mut FighterStateContext, ground_common: &mut GroundedStateCommon, - _can_walk_off: bool, + can_walk_off: bool, ) -> GroundedMotionResult { let translation = &mut state_context.translation; let ecb = &state_context.ecb; @@ -99,49 +178,149 @@ pub fn collide_with_stage_grounded( if let Some(stage_poly) = stage_poly { let ecb_bottom_pos = translation.0 + ecb.get_bottom_point(); - let current_segment_idx = ground_common.current_line_id.segment; - - // First, we check the current segment, if that fails, we check the surrounding ones - let grounded_ecb_bottom_pos = [ - Some(current_segment_idx), - stage_poly.get_prev_segment_index(current_segment_idx), - stage_poly.get_next_segment_index(current_segment_idx), - ] - .into_iter() - .flatten() - .find_map(|segment_idx| { - let segment = stage_poly.get_segment(segment_idx)?; - if segment.segment_type != StagePolyLineSegmentType::Floor { - return None; + match resolve_grounded_position( + stage_poly, + ground_common.current_line_id.segment, + ecb_bottom_pos, + can_walk_off, + ) { + GroundedPositionResult::InGround { + point, + segment_idx, + blocked, + } => { + let current_segment = stage_poly.get_segment(segment_idx).unwrap(); + translation.0 = point - ecb.get_bottom_point(); + state_context.velocity.0 = if blocked { + FGVec2::ZERO + } else { + state_context.velocity.0 + - state_context + .velocity + .0 + .project_onto_normalized(current_segment.normal) + }; + ground_common.current_line_id.segment = segment_idx; + GroundedMotionResult::InGround } - - let line_dir = segment.segment.direction(); - let projected_point = segment.segment.point1() - + (ecb_bottom_pos - segment.segment.point1()).project_onto(line_dir); - let closest_point = segment.segment.closest_point(ecb_bottom_pos); - let distance = closest_point.distance(projected_point); - - if distance < 0.01 { - return Some((projected_point, segment_idx, segment.normal)); - } - - None - }); - - if let Some((bp, new_segment_idx, normal)) = grounded_ecb_bottom_pos { - translation.0 = bp - ecb.get_bottom_point(); - state_context.velocity.0 = - state_context.velocity.0 - state_context.velocity.0.project_onto_normalized(normal); - ground_common.current_line_id.segment = new_segment_idx; - GroundedMotionResult::InGround - } else { - GroundedMotionResult::InAir + GroundedPositionResult::InAir => GroundedMotionResult::InAir, } } else { GroundedMotionResult::InAir } } +#[cfg(test)] +mod tests { + use super::*; + use crate::stage::line::StagePolyType; + + fn two_segment_floor() -> StagePoly { + StagePoly::build( + StagePolyType::Platform, + &[ + (FGVec2::lit("0", "0"), StagePolyLineSegmentType::Floor), + (FGVec2::lit("10", "0"), StagePolyLineSegmentType::Floor), + (FGVec2::lit("20", "0"), StagePolyLineSegmentType::Floor), + ], + ) + } + + #[test] + fn tiny_endpoint_crossing_moves_to_adjacent_floor() { + let stage = two_segment_floor(); + let point = FGVec2::new(FGi32::from_bits(10 * 65_536 + 1), FGi32::ZERO); + + let GroundedPositionResult::InGround { + point: resolved, + segment_idx, + blocked, + } = resolve_grounded_position(&stage, 0, point, false) + else { + panic!("connected floor should remain grounded"); + }; + + assert_eq!(segment_idx, 1); + assert_eq!(resolved, point); + assert!(!blocked); + } + + #[test] + fn blocked_edge_keeps_floor_and_can_move_inward() { + let stage = StagePoly::build( + StagePolyType::Platform, + &[ + (FGVec2::lit("0", "0"), StagePolyLineSegmentType::Floor), + (FGVec2::lit("10", "0"), StagePolyLineSegmentType::Wall), + (FGVec2::lit("10", "-2"), StagePolyLineSegmentType::Ceiling), + ], + ); + let just_past_edge = FGVec2::new(FGi32::from_bits(10 * 65_536 + 1), FGi32::ZERO); + + let GroundedPositionResult::InGround { + point, + segment_idx, + blocked, + } = resolve_grounded_position(&stage, 0, just_past_edge, false) + else { + panic!("non-walk-off movement should stop at the edge"); + }; + assert_eq!(point, FGVec2::lit("10", "0")); + assert_eq!(segment_idx, 0); + assert!(blocked); + + let GroundedPositionResult::InGround { + point, + segment_idx, + blocked, + } = resolve_grounded_position( + &stage, + segment_idx, + FGVec2::new(FGi32::from_bits(10 * 65_536 - 1), FGi32::ZERO), + false, + ) + else { + panic!("fighter should be able to move back from the edge"); + }; + assert!(point.x < FGi32::lit("10")); + assert_eq!(segment_idx, 0); + assert!(!blocked); + } + + #[test] + fn walk_off_edge_enters_air() { + let stage = StagePoly::build( + StagePolyType::Platform, + &[ + (FGVec2::lit("0", "0"), StagePolyLineSegmentType::Floor), + (FGVec2::lit("10", "0"), StagePolyLineSegmentType::Floor), + ], + ); + let just_past_edge = FGVec2::new(FGi32::from_bits(10 * 65_536 + 1), FGi32::ZERO); + + assert!(matches!( + resolve_grounded_position(&stage, 0, just_past_edge, true), + GroundedPositionResult::InAir + )); + } + + #[test] + fn vertical_segment_cannot_hold_grounded_fighter() { + let stage = StagePoly::build( + StagePolyType::Platform, + &[ + (FGVec2::lit("10", "0"), StagePolyLineSegmentType::Floor), + (FGVec2::lit("10", "-2"), StagePolyLineSegmentType::Floor), + ], + ); + + assert!(matches!( + resolve_grounded_position(&stage, 0, FGVec2::lit("10", "0"), false), + GroundedPositionResult::InAir + )); + } +} + pub fn grounded_movement_common_interrupts( state_context: &FighterStateContext, ground_common: &GroundedStateCommon, diff --git a/src/fighter/state/jump.rs b/src/fighter/state/jump.rs index 44f7187..53ecc87 100644 --- a/src/fighter/state/jump.rs +++ b/src/fighter/state/jump.rs @@ -2,18 +2,10 @@ use bevy::prelude::*; use crate::{ fighter::{ - animation::AnimKind, - collision, - state::{ - FighterState, FighterStateContext, FighterStateImpl, air, - air_dodge::{self, AirDodgeState}, - fall::FallState, - ground::{self, GroundedMotionResult, GroundedStateCommon}, - land::LandingState, + animation::AnimKind, collision, state::{ + FighterState, FighterStateContext, FighterStateImpl, air, air_dodge::{self, AirDodgeState}, fall::FallState, ground::{self, GroundedMotionResult, GroundedStateCommon}, land::{LandingState, LandingType}, }, - }, - input::FighterCommands, - math::{int::FGi32, vec::FGVec2}, + }, input::FighterCommands, math::{int::FGi32, vec::FGVec2}, }; #[derive(Debug, Hash, Clone, Copy)] pub enum JumpType { @@ -174,7 +166,13 @@ impl FighterStateImpl for JumpState { } } JumpType::DoubleJump => { - state_context.play_animation(AnimKind::DoubleJump, false); + if movement_stick_x.is_zero() + || movement_stick_x.signum() == state_context.facing_direction.to_sign() + { + state_context.play_animation(AnimKind::DoubleJumpForward, false); + } else { + state_context.play_animation(AnimKind::DoubleJumpBack, false); + } } } @@ -233,7 +231,7 @@ impl FighterStateImpl for JumpState { let grounded_common = GroundedStateCommon { current_line_id: res.line_id, }; - Some(FighterState::Land(LandingState { grounded_common })) + Some(FighterState::Land(LandingState::create(grounded_common, LandingType::Normal))) } else { None } diff --git a/src/fighter/state/land.rs b/src/fighter/state/land.rs index 3ad74eb..0b37788 100644 --- a/src/fighter/state/land.rs +++ b/src/fighter/state/land.rs @@ -11,14 +11,21 @@ use crate::fighter::{ }, }; +#[derive(Debug, Clone, Hash)] +pub enum LandingType { + Normal, + Special +} + #[derive(Debug, Clone, Hash)] pub struct LandingState { pub grounded_common: GroundedStateCommon, + pub landing_type: LandingType, } impl LandingState { - pub fn create(grounded_common: GroundedStateCommon) -> Self { - Self { grounded_common } + pub fn create(grounded_common: GroundedStateCommon, landing_type: LandingType) -> Self { + Self { grounded_common, landing_type } } } @@ -29,7 +36,7 @@ impl FighterStateImpl for LandingState { if state_context.is_current_animation_finished() { grounded_movement_standstill_common_interrupts(state_context, &self.grounded_common) } else if state_context.get_current_animation_frame() - >= state_context.fighter_manifest.attributes.landing_iasa + >= state_context.fighter_manifest.attributes.landing_iasa && let LandingType::Normal = self.landing_type { dash::check_input(state_context, &self.grounded_common).or_else(|| { ground::grounded_movement_common_interrupts(state_context, &self.grounded_common) @@ -69,7 +76,7 @@ impl FighterStateImpl for LandingState { state_context: &mut FighterStateContext, ) -> Option { if let GroundedMotionResult::InAir = - ground::collide_with_stage_grounded(state_context, &mut self.grounded_common, false) + ground::collide_with_stage_grounded(state_context, &mut self.grounded_common, true) { Some(FighterState::Fall(FallState)) } else { @@ -78,6 +85,13 @@ impl FighterStateImpl for LandingState { } fn on_enter(&mut self, state_context: &mut FighterStateContext) { - state_context.play_animation(AnimKind::Landing, false); + match self.landing_type { + LandingType::Normal => { + state_context.play_animation(AnimKind::Landing, false); + }, + LandingType::Special => { + state_context.play_animation(AnimKind::LandingSpecial, false); + }, + }; } } diff --git a/src/fighter/state/turn.rs b/src/fighter/state/turn.rs index 6a94b29..1d53462 100644 --- a/src/fighter/state/turn.rs +++ b/src/fighter/state/turn.rs @@ -3,6 +3,7 @@ use crate::fighter::animation::AnimKind; use crate::fighter::state::fall::FallState; use crate::fighter::state::ground::{self, GroundedMotionResult, GroundedStateCommon}; use crate::fighter::state::{FighterState, dash, jump, wait, walk}; +use crate::input::FighterCommands; #[derive(Debug, Clone, Hash)] pub struct TurnState { @@ -13,7 +14,13 @@ impl FighterStateImpl for TurnState { const NAME: &'static str = "Turn"; fn check_interrupt(&self, state_context: &mut FighterStateContext) -> Option { // State chain that results in the turn completing + if state_context.get_current_animation_frame() <= state_context.game_settings.input_common.turn_into_dashback_window as u32 { + if let Some(state) = dash::check_input(state_context, &self.grounded_common) { + return Some(state); + } + } if let Some(state) = jump::check_input(state_context, &self.grounded_common) { + *state_context.facing_direction = state_context.facing_direction.reverse(); Some(state) } else if state_context.is_current_animation_finished() { if let Some(state) = dash::check_input(state_context, &self.grounded_common) { @@ -78,14 +85,14 @@ pub fn check_input( grounded_common: &GroundedStateCommon, ) -> Option { let input_frame = state_context.input.get_last_frame(); - let smash_input_frame_threshold = state_context - .game_settings - .input_common - .smash_input_frame_threshold; + + let has_smash_input = state_context.input.has_command(FighterCommands::SmashMoveLeft) || state_context.input.has_command(FighterCommands::SmashMoveRight); + if !input_frame.movement.x.is_zero() && input_frame.movement.x.signum() != state_context.facing_direction.to_sign() - && state_context.input.get_frames_in_smash_move_deadzone() > smash_input_frame_threshold + && !has_smash_input { + dbg!(input_frame.movement.x); return Some(FighterState::Turn(TurnState { grounded_common: grounded_common.clone(), })); diff --git a/src/fighter/state/walk.rs b/src/fighter/state/walk.rs index db6fdbd..7f2def9 100644 --- a/src/fighter/state/walk.rs +++ b/src/fighter/state/walk.rs @@ -13,9 +13,7 @@ pub struct WalkState { impl FighterStateImpl for WalkState { const NAME: &'static str = "Walk"; fn check_interrupt(&self, state_context: &mut FighterStateContext) -> Option { - if let Some(state) = turn::check_input(state_context, &self.grounded_common) { - return Some(state); - } else if let Some(state) = + if let Some(state) = ground::grounded_movement_common_interrupts(state_context, &self.grounded_common) { return Some(state); diff --git a/src/game_settings.rs b/src/game_settings.rs index ce1a838..7457ee2 100644 --- a/src/game_settings.rs +++ b/src/game_settings.rs @@ -22,6 +22,7 @@ pub struct InputSettingsCommon { pub run_stick_threshold: FGi32, pub smash_input_frame_threshold: u8, pub input_buffer_size: u8, + pub turn_into_dashback_window: u8, } #[derive(Reflect, Serialize, Deserialize, Resource, Asset, Clone, RonAsset)] diff --git a/src/input/buffer.rs b/src/input/buffer.rs index 30b76b5..67d6b9e 100644 --- a/src/input/buffer.rs +++ b/src/input/buffer.rs @@ -216,19 +216,6 @@ pub fn postprocess_input( ); } - if x_abs >= axis_threshold && input.frames_in_x_flick_deadzone <= frame_threshold { - input.clear_command(FighterCommands::SmashMoveLeft); - 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); - } - let mut y_flick_detected = y_abs >= axis_threshold && input.frames_in_y_flick_deadzone <= frame_threshold; let mut x_flick_detected = diff --git a/src/input/mod.rs b/src/input/mod.rs index 7944b0f..d4ba78a 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -272,10 +272,7 @@ fn read_local_inputs( Some((_, LocalInputSource::Gamepad(_))) => gamepad .map(|pad| gamepad::sample_gamepad(pad, &input_map)) .unwrap_or_default(), - None => match gamepad { - Some(pad) => gamepad::sample_gamepad(pad, &input_map), - None => keyboard::sample_keyboard(&key, &input_map), - }, + None => FighterInputFrame::default() }; // preprocess frame diff --git a/src/math/segment.rs b/src/math/segment.rs index d66cfb1..f7b390a 100644 --- a/src/math/segment.rs +++ b/src/math/segment.rs @@ -141,6 +141,15 @@ impl FGSegment2d { None } } + + /// Returns a value representing the normalized position along the segment. + pub fn project_point_normalized(&self, point: FGVec2) -> FGi32 { + let segment_vector = self.point2 - self.point1; + let offset = point - self.point1; + // The signed projection of `offset` onto `segment_vector`, scaled by the length of the segment. + let projection_scaled = segment_vector.dot(offset); + FGi32::from_num(FGWide::from_num(projection_scaled) / segment_vector.length_squared()) + } } #[cfg(test)]