diff --git a/Cargo.lock b/Cargo.lock index 56ab845..70e187b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3234,6 +3234,7 @@ dependencies = [ "az", "bytemuck", "half", + "serde", "typenum", ] diff --git a/Cargo.toml b/Cargo.toml index 8c7d84a..98bf2a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ serde = "1.0.229" bitfields = "3.0.0" clap = { version = "4.6.6", features = ["derive"] } bevy_wind_waker_shader = { git = "https://github.com/lavaeater/bevy_wind_waker_shader.git" } -fixed = "1.31.0" +fixed = { version = "1.31.0", features = ["serde"] } # Enable a small amount of optimization in the dev profile. [profile.dev] diff --git a/src/fighter/animation.rs b/src/fighter/animation.rs index 9912e40..f74cf9a 100644 --- a/src/fighter/animation.rs +++ b/src/fighter/animation.rs @@ -13,7 +13,7 @@ pub struct FighterAnimationPlayerLink(Entity); pub fn apply_fighter_translation_to_visuals(query: Query<(&FighterTranslation, &FighterECB, &mut Transform)>) { for (translation, ecb, mut transform) in query { let translation = translation.0 + ecb.get_bottom_point(); - transform.translation = Vec3::new(translation.x, translation.y, 0.0); + transform.translation = Vec3::new(translation.x.to_num(), translation.y.to_num(), 0.0); } } diff --git a/src/fighter/attributes.rs b/src/fighter/attributes.rs index 56bb11c..bc69e44 100644 --- a/src/fighter/attributes.rs +++ b/src/fighter/attributes.rs @@ -1,31 +1,32 @@ use bevy::prelude::*; -use crate::input::FighterInputFrame; +use crate::{input::FighterInputFrame, math::int::FGi32}; /// Per-fighter tuning values. These are character data, not global rules — /// anything that applies to every fighter belongs in /// [`crate::game_settings::FighterSettingsCommon`] instead. #[derive(Component, Clone, Copy)] pub struct FighterAttributes { - pub base_walk_accel: f32, - pub stick_walk_accel: f32, - pub max_walk_vel: f32, + pub base_walk_accel: FGi32, + pub stick_walk_accel: FGi32, + pub max_walk_vel: FGi32, pub dash_duration: u32, - pub dash_initial_velocity: f32, - pub base_dash_accel: f32, - pub stick_dash_accel: f32, - pub max_dash_vel: f32, + pub dash_initial_velocity: FGi32, + pub base_dash_accel: FGi32, + pub stick_dash_accel: FGi32, + pub max_dash_vel: FGi32, - pub jump_vertical_velocity: f32, + pub jumpsquat_duration: u32, + pub jump_vertical_velocity: FGi32, - pub ground_friction: f32, - pub terminal_velocity: f32, - pub gravity: f32, + pub ground_friction: FGi32, + pub terminal_velocity: FGi32, + pub gravity: FGi32, } impl FighterAttributes { - pub fn get_accel_and_target_dashrun(&self, input: &FighterInputFrame) -> (f32, f32) { + pub fn get_accel_and_target_dashrun(&self, input: &FighterInputFrame) -> (FGi32, FGi32) { let accel = input.movement.x * self.stick_dash_accel; let accel = accel + input.movement.x.signum() * self.base_dash_accel; let target_vel = input.movement.x * self.max_dash_vel; @@ -33,7 +34,7 @@ impl FighterAttributes { (accel, target_vel) } - pub fn get_accel_and_target_walk(&self, input: &FighterInputFrame) -> (f32, f32) { + pub fn get_accel_and_target_walk(&self, input: &FighterInputFrame) -> (FGi32, FGi32) { let accel = input.movement.x * self.stick_walk_accel; let accel = accel + input.movement.x.signum() * self.base_walk_accel; let target_vel = input.movement.x * self.max_walk_vel; diff --git a/src/fighter/collision.rs b/src/fighter/collision.rs index fb10e2b..6e62c69 100644 --- a/src/fighter/collision.rs +++ b/src/fighter/collision.rs @@ -5,6 +5,8 @@ use bevy::{color::palettes::css::HOT_PINK, prelude::*}; use crate::fighter::ecb::FighterPreviousECB; use crate::fighter::state::FighterStateContext; use crate::fighter::{FighterECB, FighterPreviousTranslation, FighterTranslation, FighterVelocity}; +use crate::math::segment::FGSegment2d; +use crate::math::vec::FGVec2; use crate::stage::StagePoly; use crate::stage::line::{StageCollision, StageLineID}; /* @@ -36,14 +38,14 @@ pub fn collide_fighter_with_scene( pub struct AirCollisionWithStageResult { pub line_id: StageLineID, - pub hit_position: Vec2, - pub hit_normal: Vec2, + pub hit_position: FGVec2, + pub hit_normal: FGVec2, } pub fn air_collide_with_stage( state_context: &FighterStateContext, ) -> Option { - let ray_segment = Segment2d::new( + let ray_segment = FGSegment2d::new( state_context.prev_translation.0 + state_context.prev_ecb.get_bottom_point(), state_context.translation.0 + state_context.ecb.get_bottom_point(), ); diff --git a/src/fighter/debug.rs b/src/fighter/debug.rs index 04541a0..c5b4f41 100644 --- a/src/fighter/debug.rs +++ b/src/fighter/debug.rs @@ -1,16 +1,20 @@ //! Fighter debug views: gizmos and the egui inspector window. +use bevy::color::palettes::css::ORANGE; use bevy::{color::palettes::css::YELLOW, prelude::*}; use bevy_egui::{EguiContexts, egui}; +use crate::fighter::ecb::FighterPreviousECB; use crate::fighter::state::StateNameDebug; -use crate::fighter::{FighterECB, FighterTranslation, FighterVelocity}; +use crate::fighter::{FighterECB, FighterPreviousTranslation, FighterTranslation, FighterVelocity}; use crate::input::FighterInput; -pub fn debug_draw_ecb(ecbs: Query<(&FighterECB, &FighterTranslation)>, mut gizmos: Gizmos) { - for (ecb, trf) in ecbs { - let transformed = ecb.to_3d_lineloop(Transform::from_xyz(trf.x, trf.y, 0.0)); +pub fn debug_draw_ecb(ecbs: Query<(&FighterECB, &FighterTranslation, &FighterPreviousECB, &FighterPreviousTranslation)>, mut gizmos: Gizmos) { + for (ecb, trf, ecb_prev, trf_prev) in ecbs { + let transformed = ecb.to_3d_lineloop(Transform::from_xyz(trf.x.to_num(), trf.y.to_num(), 0.0)); gizmos.lineloop(transformed, YELLOW); + let prev_transformed = ecb_prev.to_3d_lineloop(Transform::from_xyz(trf_prev.x.to_num(), trf_prev.y.to_num(), -0.1)); + gizmos.lineloop(prev_transformed, ORANGE); } } diff --git a/src/fighter/ecb.rs b/src/fighter/ecb.rs index 4349a5e..b4ba3b5 100644 --- a/src/fighter/ecb.rs +++ b/src/fighter/ecb.rs @@ -1,4 +1,7 @@ use bevy::prelude::*; +use fixed::traits::Fixed; + +use crate::math::{int::FGi32, vec::FGVec2}; /// Environment collision box: the diamond used to resolve a fighter against /// the stage. Purely geometry — the collision response lives in @@ -6,40 +9,74 @@ use bevy::prelude::*; #[derive(Component, Debug, Clone, Copy)] #[require(FighterPreviousECB)] pub struct FighterECB { - pub vertical_half: f32, - pub horizontal_half: f32, + pub vertical_half: FGi32, + pub horizontal_half: FGi32, } #[derive(Component, Debug, Clone, Copy, Default)] pub struct FighterPreviousECB { - pub vertical_half: f32, - pub horizontal_half: f32, + pub vertical_half: FGi32, + pub horizontal_half: FGi32, +} + +impl From for FighterPreviousECB { + fn from(ecb: FighterECB) -> Self { + Self { + vertical_half: ecb.vertical_half, + horizontal_half: ecb.horizontal_half, + } + } +} + +pub fn snapshot_fighter_ecb( + fighters: Query<(&FighterECB, &mut FighterPreviousECB)>, +) { + for (ecb, mut previous_ecb) in fighters { + *previous_ecb = (*ecb).into(); + } } impl FighterECB { pub fn to_3d_lineloop(&self, trf: Transform) -> [Vec3; 4] { - let rel_up = trf.transform_point(Vec3::new(0.0, self.vertical_half, 0.0)); - let rel_down = trf.transform_point(Vec3::new(0.0, -self.vertical_half, 0.0)); - let rel_left = trf.transform_point(Vec3::new(-self.horizontal_half, 0.0, 0.0)); - let rel_right = trf.transform_point(Vec3::new(self.horizontal_half, 0.0, 0.0)); + let rel_up = trf.transform_point(Vec3::new(0.0, self.vertical_half.to_num(), 0.0)); + let rel_down = trf.transform_point(Vec3::new(0.0, (-self.vertical_half).to_num(), 0.0)); + let rel_left = trf.transform_point(Vec3::new((-self.horizontal_half).to_num(), 0.0, 0.0)); + let rel_right = trf.transform_point(Vec3::new(self.horizontal_half.to_num(), 0.0, 0.0)); [rel_up, rel_right, rel_down, rel_left] } - pub fn get_bottom_point(&self) -> Vec2 { - Vec2::new(0.0, -self.vertical_half) + pub fn get_bottom_point(&self) -> FGVec2 { + FGVec2::new(FGi32::ZERO, -self.vertical_half) } } impl FighterPreviousECB { pub fn to_3d_lineloop(&self, trf: Transform) -> [Vec3; 4] { - let rel_up = trf.transform_point(Vec3::new(0.0, self.vertical_half, 0.0)); - let rel_down = trf.transform_point(Vec3::new(0.0, -self.vertical_half, 0.0)); - let rel_left = trf.transform_point(Vec3::new(-self.horizontal_half, 0.0, 0.0)); - let rel_right = trf.transform_point(Vec3::new(self.horizontal_half, 0.0, 0.0)); + let rel_up = trf.transform_point(Vec3::new(0.0, self.vertical_half.to_num(), 0.0)); + let rel_down = trf.transform_point(Vec3::new(0.0, (-self.vertical_half).to_num(), 0.0)); + let rel_left = trf.transform_point(Vec3::new((-self.horizontal_half).to_num(), 0.0, 0.0)); + let rel_right = trf.transform_point(Vec3::new(self.horizontal_half.to_num(), 0.0, 0.0)); [rel_up, rel_right, rel_down, rel_left] } - pub fn get_bottom_point(&self) -> Vec2 { - Vec2::new(0.0, -self.vertical_half) + pub fn get_bottom_point(&self) -> FGVec2 { + FGVec2::new(FGi32::ZERO, -self.vertical_half) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn previous_ecb_copies_current_dimensions() { + let current = FighterECB { + vertical_half: FGi32::lit("0.5"), + horizontal_half: FGi32::lit("0.25"), + }; + let previous = FighterPreviousECB::from(current); + + assert_eq!(previous.vertical_half, current.vertical_half); + assert_eq!(previous.horizontal_half, current.horizontal_half); } } diff --git a/src/fighter/mod.rs b/src/fighter/mod.rs index 9179205..c489b58 100644 --- a/src/fighter/mod.rs +++ b/src/fighter/mod.rs @@ -45,7 +45,12 @@ impl Plugin for FighterPlugin { // Every state gets a chance to interrupt itself, in priority order. .add_systems( GgrsSchedule, - (animation::setup_fighter_animation_player, state::state_interrupt_system,).in_set(GameplaySet::Interrupt), + ( + ecb::snapshot_fighter_ecb.before(state_interrupt_system), + animation::setup_fighter_animation_player, + state::state_interrupt_system, + ) + .in_set(GameplaySet::Interrupt), ) .add_systems( GgrsSchedule, @@ -74,6 +79,7 @@ impl Plugin for FighterPlugin { // components; a component simulated here but missing from this list // is a desync waiting to happen. .rollback_component_with_copy::() + .rollback_component_with_copy::() .rollback_component_with_copy::() .rollback_component_with_copy::() .rollback_component_with_copy::() diff --git a/src/fighter/motion.rs b/src/fighter/motion.rs index 7fb4512..0985765 100644 --- a/src/fighter/motion.rs +++ b/src/fighter/motion.rs @@ -3,20 +3,20 @@ use bevy::prelude::*; -use crate::fighter::FighterAttributes; +use crate::{fighter::FighterAttributes, math::vec::FGVec2}; #[derive(Component, Clone, Copy)] pub struct Grounded; #[derive(Component, Deref, DerefMut, Default, Debug, Clone, Copy)] -pub struct FighterVelocity(pub Vec2); +pub struct FighterVelocity(pub FGVec2); #[derive(Component, Deref, DerefMut, Default, Clone, Copy)] -pub struct FighterPreviousTranslation(pub Vec2); +pub struct FighterPreviousTranslation(pub FGVec2); #[derive(Component, Deref, DerefMut, Default, Clone, Copy)] #[require(FighterPreviousTranslation)] -pub struct FighterTranslation(pub Vec2); +pub struct FighterTranslation(pub FGVec2); pub fn apply_air_motion( fighters: Query<( @@ -37,7 +37,7 @@ pub fn copy_fighter_transform_to_visuals( fighters: Query<(&FighterTranslation, &mut Transform)>, ) { for (translation, mut transform) in fighters { - transform.translation = Vec3::new(translation.x, translation.y, 0.0); + transform.translation = Vec3::new(translation.x.to_num(), translation.y.to_num(), 0.0); } } diff --git a/src/fighter/state/dash.rs b/src/fighter/state/dash.rs index e1467b5..2cab4ab 100644 --- a/src/fighter/state/dash.rs +++ b/src/fighter/state/dash.rs @@ -2,7 +2,7 @@ use std::hash::{Hash, Hasher}; use bevy::prelude::*; -use super::{FighterStateImpl, FighterStateContext, FighterStateTransition, ground, run, wait, walk}; +use super::{FighterStateImpl, FighterStateContext, ground, run, wait, walk}; use crate::fighter::state::ground::GroundedStateCommon; use crate::fighter::state::wait::WaitState; use crate::fighter::state::{FighterState}; @@ -12,11 +12,12 @@ use crate::input::{ FighterCommands::{SmashMoveLeft, SmashMoveRight}, FighterInput, }; +use crate::math::int::FGi32; #[derive(Debug, Clone)] pub struct DashState { frames_in_dash: u32, - direction: f32, + direction: FGi32, ground_common: GroundedStateCommon } @@ -28,7 +29,7 @@ impl std::hash::Hash for DashState { } impl DashState { - pub fn new(direction: f32, ground_common: GroundedStateCommon) -> Self { + pub fn new(direction: FGi32, ground_common: GroundedStateCommon) -> Self { Self { frames_in_dash: 0, direction, @@ -98,11 +99,11 @@ impl FighterStateImpl for DashState { } } -fn check_smash_input_with_dir(state_context: &FighterStateContext) -> Option { +fn check_smash_input_with_dir(state_context: &FighterStateContext) -> Option { if state_context.input.has_command(SmashMoveLeft) { - return Some(-1.0); + return Some(FGi32::NEG_ONE); } else if state_context.input.has_command(SmashMoveRight) { - return Some(1.0); + return Some(FGi32::ONE); } else { None } diff --git a/src/fighter/state/fall.rs b/src/fighter/state/fall.rs index 422aa0e..b0f4825 100644 --- a/src/fighter/state/fall.rs +++ b/src/fighter/state/fall.rs @@ -1,6 +1,6 @@ use bevy::prelude::*; -use super::{FighterStateImpl, FighterStateContext, FighterStateTransition, ground, walk}; +use super::{FighterStateImpl, FighterStateContext, ground, walk}; use crate::fighter::state::{FighterState, air}; use crate::fighter::state::ground::GroundedStateCommon; use crate::fighter::state::wait::WaitState; diff --git a/src/fighter/state/ground.rs b/src/fighter/state/ground.rs index d2fb2d2..03a54f7 100644 --- a/src/fighter/state/ground.rs +++ b/src/fighter/state/ground.rs @@ -7,17 +7,20 @@ use crate::fighter::{ FighterAttributes, FighterECB, FighterPreviousTranslation, FighterTranslation, FighterVelocity, }; use crate::game_settings::GameSettings; +use crate::math::int::FGi32; +use crate::math::vec::FGVec2; use crate::stage::StagePoly; use crate::stage::line::{StageCollision, StageLineID, StagePolyLineSegment, StagePolyLineSegmentType}; use bevy::prelude::*; +use fixed::traits::Fixed; #[derive(Reflect, Hash, Clone, Debug)] pub struct GroundedStateCommon { pub current_line_id: StageLineID, } -pub fn apply_grounded_friction(friction: f32, ground_vel: f32) -> f32 { +pub fn apply_grounded_friction(friction: FGi32, ground_vel: FGi32) -> FGi32 { if friction.abs() > ground_vel.abs() { -ground_vel } else { @@ -28,21 +31,21 @@ pub fn apply_grounded_friction(friction: f32, ground_vel: f32) -> f32 { pub fn move_accelerate() {} pub fn compute_ground_accel( - accel: f32, - target_vel: f32, - gr_vel: f32, + accel: FGi32, + target_vel: FGi32, + gr_vel: FGi32, attribs: &FighterAttributes, game_settings: &GameSettings, -) -> f32 { +) -> FGi32 { if target_vel == 0.0 { apply_grounded_friction(attribs.ground_friction, gr_vel) } else { let mut accel = accel; let ground_max_horizontal_velocity = game_settings.figher_common.ground_max_horizontal_velocity; - if !(gr_vel * accel < 0.0) { + if !(gr_vel * accel < FGi32::ZERO) { // accelerating, not reversing - if accel > 0.0 { + if accel > FGi32::ZERO { if gr_vel + accel > target_vel { accel = -attribs.ground_friction; if gr_vel + accel < target_vel { @@ -78,7 +81,7 @@ pub fn apply_grounded_motion( ) { // Vertical velocity should be 0 on the ground prev_translation.0 = translation.0; - translation.0 += Vec2::new(velocity.0.x ,0.0); + translation.0 += FGVec2::new(velocity.0.x ,FGi32::ZERO); } pub fn collide_with_stage_grounded( @@ -96,34 +99,29 @@ pub fn collide_with_stage_grounded( if let Some(stage_poly) = stage_poly { let ecb_bottom_pos = translation.0 + ecb.get_bottom_point(); - let left_segment_idx = ground_common - .current_line_id - .segment - .checked_sub(1) - .unwrap_or(0); - let right_segment_idx = - (ground_common.current_line_id.segment + 1) % stage_poly.segments.len(); + 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 = [ - ground_common.current_line_id.segment.clone(), - left_segment_idx, - right_segment_idx, + Some(current_segment_idx), + stage_poly.get_prev_segment_index(current_segment_idx), + stage_poly.get_next_segment_index(current_segment_idx), ] - .iter() + .into_iter() + .flatten() .find_map(|segment_idx| { - let segment = stage_poly.get_segment(*segment_idx)?; + let segment = stage_poly.get_segment(segment_idx)?; if segment.segment_type != StagePolyLineSegmentType::Floor { return None; } let line_dir = segment.segment.direction(); - let projected_point = segment.segment.point1() + (ecb_bottom_pos - segment.segment.point1()).project_onto(line_dir.as_vec2()); + 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)); + return Some((projected_point, segment_idx, segment.normal)); } None diff --git a/src/fighter/state/jump.rs b/src/fighter/state/jump.rs index 2075985..d6cd28a 100644 --- a/src/fighter/state/jump.rs +++ b/src/fighter/state/jump.rs @@ -1,41 +1,106 @@ use bevy::prelude::*; -use crate::fighter::state::{FighterState, FighterStateImpl}; +use crate::{ + fighter::state::{ + FighterState, FighterStateContext, FighterStateImpl, + fall::FallState, + ground::{self, GroundedMotionResult, GroundedStateCommon}, + }, + input::FighterCommands, +}; -pub struct JumpState { - pub counter: u32 +pub enum JumpType { + ShortHop, + LongJump, + DoubleJump, } +#[derive(Debug, Clone, Hash)] +pub struct JumpState {} + +#[derive(Debug, Clone, Hash)] pub struct JumpSquatState { - pub counter: u32 + pub grounded_common: GroundedStateCommon, + pub duration_counter: u32, } +impl JumpSquatState { + pub fn create(grounded_common: GroundedStateCommon) -> Self { + Self { + grounded_common, + duration_counter: 0, + } + } +} impl FighterStateImpl for JumpSquatState { const NAME: &'static str = "JumpSquat"; - fn check_interrupt( - &self, - state_context: &super::FighterStateContext, - ) -> Option { - None + + fn check_interrupt(&self, state_context: &FighterStateContext) -> Option { + if self.duration_counter >= state_context.fighter_attribs.jumpsquat_duration { + Some(FighterState::Jump(JumpState {})) + } else { + None + } } - fn update(&mut self, _state_context: &mut super::FighterStateContext) { - todo!() + fn update(&mut self, state_context: &mut FighterStateContext) { + self.duration_counter += 1; + + let friction = + if state_context.velocity.x.abs() > state_context.fighter_attribs.max_walk_vel { + state_context.fighter_attribs.ground_friction + * state_context + .game_settings + .figher_common + .ground_friction_over_walk_speed_multiplier + } else { + state_context.fighter_attribs.ground_friction + }; + + let ground_velocity = state_context.velocity.x; + + state_context.velocity.x += ground::apply_grounded_friction(friction, ground_velocity); + + ground::apply_grounded_motion( + state_context.velocity, + state_context.translation, + state_context.prev_translation, + ); + } + + fn check_collision_interrupt( + &mut self, + state_context: &mut FighterStateContext, + ) -> Option { + if let GroundedMotionResult::InAir = + ground::collide_with_stage_grounded(state_context, &mut self.grounded_common, false) + { + Some(FighterState::Fall(FallState)) + } else { + None + } } - } impl FighterStateImpl for JumpState { const NAME: &'static str = "Jump"; - fn check_interrupt( - &self, - state_context: &super::FighterStateContext, - ) -> Option { + fn check_interrupt(&self, state_context: &FighterStateContext) -> Option { None } - fn update(&mut self, _state_context: &mut super::FighterStateContext) { - todo!() - } -} \ No newline at end of file + fn update(&mut self, _state_context: &mut FighterStateContext) {} +} + +pub fn check_input( + state_context: &FighterStateContext, + ground_common: &GroundedStateCommon, +) -> Option { + state_context + .input + .has_command(FighterCommands::Jump) + .then_some(FighterState::JumpSquat(JumpSquatState::create( + ground_common.clone(), + ))); + None +} diff --git a/src/fighter/state/mod.rs b/src/fighter/state/mod.rs index 0361e89..2aef511 100644 --- a/src/fighter/state/mod.rs +++ b/src/fighter/state/mod.rs @@ -11,6 +11,7 @@ use crate::fighter::ecb::FighterPreviousECB; use crate::fighter::{FighterAttributes, FighterECB, FighterPreviousTranslation, FighterTranslation, FighterVelocity}; use crate::game_settings::GameSettings; use crate::input::FighterInput; +use crate::math::int::FGi32; use crate::schedule::GameplaySet; use crate::stage::StagePoly; use crate::stage::line::StageCollision; @@ -24,14 +25,6 @@ pub mod fall; pub mod air; pub mod jump; -pub enum FighterStateTransition { - Wait, - Walk, - Dash(f32), - Run, - Fall, -} - macro_rules! fighter_states { ($($variant:ident => $ty:ty),* $(,)?) => { #[derive(Component, Clone, Debug, Hash)] @@ -80,7 +73,7 @@ pub trait FighterStateImpl: Sized { } } -pub struct FighterStateContext<'a> { +pub struct FighterStateContext<'a, 'w, 's> { pub translation: &'a mut FighterTranslation, pub prev_translation: &'a mut FighterPreviousTranslation, pub input: &'a mut FighterInput, @@ -91,6 +84,7 @@ pub struct FighterStateContext<'a> { pub game_settings: &'a GameSettings, pub entity: Entity, pub stage_collision: &'a StageCollision, + pub gizmos: Option<&'a mut Gizmos<'w, 's>>, } fighter_states! { @@ -98,7 +92,9 @@ fighter_states! { Walk => walk::WalkState, Dash => dash::DashState, Run => run::RunState, - Fall => fall::FallState + Fall => fall::FallState, + JumpSquat => jump::JumpSquatState, + Jump => jump::JumpState } #[derive(Component)] @@ -128,6 +124,7 @@ pub fn state_update_system( game_settings: &game_settings, entity: ent, stage_collision: &stage_collision, + gizmos: Some(&mut gizmos), }; state.update(&mut update_ctx); } @@ -150,7 +147,8 @@ pub fn state_interrupt_system ( fighter_attribs: &attribs, game_settings: &game_settings, entity: ent, - stage_collision: &stage_collision + stage_collision: &stage_collision, + gizmos: None, }; if let Some(new_state) = state.check_interrupt(&state_context) { let mut new_state = new_state; @@ -171,6 +169,7 @@ pub fn state_collision_interrupt_system( stage_collision: Res, game_settings: Res, mut commands: Commands, + mut gizmos: Gizmos, ) { for (ent, mut state, mut translation, mut prev_translation, mut input, mut velocity, mut ecb, mut prev_ecb, attribs) in query { let mut state_context = FighterStateContext { @@ -183,7 +182,8 @@ pub fn state_collision_interrupt_system( fighter_attribs: &attribs, game_settings: &game_settings, entity: ent, - stage_collision: &stage_collision + stage_collision: &stage_collision, + gizmos: Some(&mut gizmos), }; if let Some(new_state) = state.check_collision_interrupt(&mut state_context) { let mut new_state = new_state; @@ -197,4 +197,4 @@ pub fn state_collision_interrupt_system( info!("State {} -> {}", old_state_name, new_state_name); } } -} \ No newline at end of file +} diff --git a/src/fighter/state/run.rs b/src/fighter/state/run.rs index 917c57e..a2f1a2c 100644 --- a/src/fighter/state/run.rs +++ b/src/fighter/state/run.rs @@ -1,6 +1,6 @@ use bevy::prelude::*; -use super::{FighterStateImpl, FighterStateContext, FighterStateTransition, ground, wait, walk}; +use super::{FighterStateImpl, FighterStateContext, ground, wait, walk}; use crate::fighter::state::FighterState; use crate::fighter::state::fall::FallState; use crate::fighter::state::ground::{GroundedMotionResult, GroundedStateCommon}; diff --git a/src/fighter/state/wait.rs b/src/fighter/state/wait.rs index 433365b..b497448 100644 --- a/src/fighter/state/wait.rs +++ b/src/fighter/state/wait.rs @@ -1,6 +1,6 @@ use bevy::prelude::*; -use super::{FighterStateImpl, FighterStateContext, FighterStateTransition, ground, walk}; +use super::{FighterStateImpl, FighterStateContext, ground, walk}; use crate::fighter::state::FighterState; use crate::fighter::state::fall::FallState; use crate::fighter::state::ground::{GroundedMotionResult, GroundedStateCommon}; @@ -33,7 +33,10 @@ impl FighterStateImpl for WaitState { } else { state_context.fighter_attribs.ground_friction }; - state_context.velocity.x += ground::apply_grounded_friction(friction, state_context.velocity.x); + + let ground_velocity = state_context.velocity.x; + + state_context.velocity.x += ground::apply_grounded_friction(friction, ground_velocity); ground::apply_grounded_motion( state_context.velocity, diff --git a/src/fighter/state/walk.rs b/src/fighter/state/walk.rs index 0239468..6cfb7eb 100644 --- a/src/fighter/state/walk.rs +++ b/src/fighter/state/walk.rs @@ -1,6 +1,6 @@ use bevy::prelude::*; -use super::{FighterStateImpl, FighterStateContext, FighterStateTransition, dash, ground, wait}; +use super::{FighterStateImpl, FighterStateContext, dash, ground, wait}; use crate::fighter::state::fall::FallState; use crate::fighter::state::ground::{GroundedMotionResult, GroundedStateCommon}; use crate::fighter::state::{self, FighterState}; diff --git a/src/game_settings.rs b/src/game_settings.rs index 1eff816..dd7e300 100644 --- a/src/game_settings.rs +++ b/src/game_settings.rs @@ -1,9 +1,11 @@ use bevy::prelude::*; +use crate::math::int::FGi32; + pub struct FighterSettingsCommon { - pub walk_speed_ease: f32, - pub ground_max_horizontal_velocity: f32, - pub ground_friction_over_walk_speed_multiplier: f32 + pub walk_speed_ease: FGi32, + pub ground_max_horizontal_velocity: FGi32, + pub ground_friction_over_walk_speed_multiplier: FGi32 } pub struct InputSettingsCommon { pub stick_deadzone: f32, diff --git a/src/input/buffer.rs b/src/input/buffer.rs index 6ab4fc1..2981a80 100644 --- a/src/input/buffer.rs +++ b/src/input/buffer.rs @@ -17,6 +17,7 @@ struct FighterInputBufferedActions {} pub enum FighterCommands { SmashMoveLeft, SmashMoveRight, + Jump, } /// Frames of life remaining for each buffered command. Indexed by @@ -25,6 +26,7 @@ pub enum FighterCommands { pub struct FighterCommandLifetimes { smash_move_left: u8, smash_move_right: u8, + jump: u8, } impl Index for FighterCommandLifetimes { @@ -34,6 +36,7 @@ impl Index for FighterCommandLifetimes { match command { FighterCommands::SmashMoveLeft => &self.smash_move_left, FighterCommands::SmashMoveRight => &self.smash_move_right, + FighterCommands::Jump => &self.jump, } } } @@ -43,6 +46,7 @@ impl IndexMut for FighterCommandLifetimes { match command { FighterCommands::SmashMoveLeft => &mut self.smash_move_left, FighterCommands::SmashMoveRight => &mut self.smash_move_right, + FighterCommands::Jump => &mut self.jump, } } } diff --git a/src/input/frame.rs b/src/input/frame.rs index a711314..f9028db 100644 --- a/src/input/frame.rs +++ b/src/input/frame.rs @@ -1,10 +1,12 @@ use bevy::prelude::*; use serde::{Deserialize, Serialize}; +use crate::math::vec::FGVec2; + /// One frame of a player's raw input. This is the type GGRS serializes and /// sends over the wire, so keep it small and keep it `PartialEq`. #[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize)] pub struct FighterInputFrame { - pub movement: Vec2, - pub directional_attack: Vec2, + pub movement: FGVec2, + pub directional_attack: FGVec2, } diff --git a/src/input/keyboard.rs b/src/input/keyboard.rs index 826b38f..110e058 100644 --- a/src/input/keyboard.rs +++ b/src/input/keyboard.rs @@ -6,6 +6,7 @@ use bevy::prelude::*; use bevy_ggrs::{LocalInputs, LocalPlayers}; use crate::input::{BaseInputMap, FighterInputFrame, InputActionState, InputMapAction}; +use crate::math::int::FGi32; use crate::netcode::GGRSCfg; pub fn preprocess_keyboard_input( @@ -32,16 +33,16 @@ pub fn preprocess_keyboard_input( }; let action_strength = match action_state { - InputActionState::JustPressed | InputActionState::Pressed => 1.0f32, - InputActionState::JustReleased | InputActionState::Released => 0.0f32, + InputActionState::JustPressed | InputActionState::Pressed => FGi32::ONE, + InputActionState::JustReleased | InputActionState::Released => FGi32::ZERO, }; match keyboard_elment.action { InputMapAction::MovementXDir(sign) => { - input_frame.movement.x += (sign as f32) * action_strength + input_frame.movement.x += (sign) * action_strength } InputMapAction::MovementYDir(sign) => { - input_frame.movement.y += (sign as f32) * action_strength + input_frame.movement.y += (sign) * action_strength } InputMapAction::Jump => todo!(), } diff --git a/src/lib.rs b/src/lib.rs index 28e2e97..276fa52 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,7 +29,7 @@ use jackdaw_runtime::prelude::*; use crate::{ fighter::{ FighterAttributes, FighterECB, FighterTranslation, FighterVelocity, FighterVisual, animation::FighterAnimations, state::{FighterState, fall::FallState, wait::WaitState}, - }, game_settings::{FighterSettingsCommon, GameSettings, InputSettingsCommon}, input::FighterInput, player::Player, schedule::GameplaySchedulePlugin, + }, game_settings::{FighterSettingsCommon, GameSettings, InputSettingsCommon}, input::FighterInput, math::{int::FGi32, vec::FGVec2}, player::Player, schedule::GameplaySchedulePlugin, }; mod args; @@ -62,24 +62,24 @@ pub fn setup(mut commands: Commands, asset_server: Res) { Name("Fighter".into()), Player { handle: 0 }, FighterECB { - vertical_half: 0.5, - horizontal_half: 0.25, + vertical_half: FGi32::lit("0.5"), + horizontal_half: FGi32::lit("0.25"), }, FighterVelocity::default(), - FighterTranslation(Vec2::new(0.5, 1.25)), + FighterTranslation(FGVec2::lit("0.5", "1.25")), FighterAttributes { - base_walk_accel: 0.01, - stick_walk_accel: 0.02, - max_walk_vel: 0.16, - ground_friction: 0.008, + base_walk_accel: FGi32::lit("0.01"), + stick_walk_accel: FGi32::lit("0.02"), + max_walk_vel: FGi32::lit("0.16"), + ground_friction: FGi32::lit("0.008"), dash_duration: 15, - base_dash_accel: 0.002, - stick_dash_accel: 0.01, - max_dash_vel: 0.22, - jump_vertical_velocity: 0.25, - dash_initial_velocity: 0.19, - terminal_velocity: 0.28, - gravity: 0.023, + base_dash_accel: FGi32::lit("0.002"), + stick_dash_accel: FGi32::lit("0.01"), + max_dash_vel: FGi32::lit("0.22"), + jump_vertical_velocity: FGi32::lit("0.25"), + dash_initial_velocity: FGi32::lit("0.19"), + terminal_velocity: FGi32::lit("0.28"), + gravity: FGi32::lit("0.023"), }, FighterState::Fall(FallState), FighterInput::default(), @@ -115,9 +115,9 @@ impl Plugin for GamePlugin { .insert_resource(args::Args::parse()) .insert_resource(GameSettings { figher_common: FighterSettingsCommon { - walk_speed_ease: 0.5, - ground_max_horizontal_velocity: 0.3, - ground_friction_over_walk_speed_multiplier: 2.0, + walk_speed_ease: FGi32::lit("0.5"), + ground_max_horizontal_velocity: FGi32::lit("0.3"), + ground_friction_over_walk_speed_multiplier: FGi32::lit("2.0"), }, input_common: InputSettingsCommon::default(), }); diff --git a/src/math/segment.rs b/src/math/segment.rs index 24cca3a..6b42b55 100644 --- a/src/math/segment.rs +++ b/src/math/segment.rs @@ -1,12 +1,13 @@ use bevy::math::{Vec2, primitives::Segment2d}; +use bevy::prelude::*; use fixed::prelude::*; use crate::math::vec::FGVec2; type FGi32 = fixed::FixedI32; -#[derive(Clone, Copy, PartialEq)] +#[derive(Clone, Copy, PartialEq, Reflect)] pub struct FGSegment2d { point1: FGVec2, point2: FGVec2 @@ -29,14 +30,72 @@ impl FGSegment2d { pub fn point2(&self) -> FGVec2 { self.point2 } -} -pub trait SegmentIntersection { - fn segment_intersection(&self, rhs: &Segment2d) -> Option; -} + /// Compute the normalized direction pointing from the first endpoint to the second endpoint. + /// + /// For the non-panicking version, see [`FGVec2::try_direction`]. + /// + /// # Panics + /// + /// Panics if a valid direction could not be computed, for example when the endpoints are coincident, NaN, or infinite. + #[inline] + pub fn direction(&self) -> FGVec2 { + self.try_direction().expect("Failed to compute the direction of a line segment") + } -impl SegmentIntersection for Segment2d { - fn segment_intersection(&self, rhs: &Segment2d) -> Option { + /// Try to compute the normalized direction pointing from the first endpoint to the second endpoint. + /// + /// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if a valid direction could not be computed, + /// for example when the endpoints are coincident, NaN, or infinite. + #[inline] + pub fn try_direction(&self) -> Option { + self.point1.direction_to(self.point2) + } + + /// Returns the point on the [`Segment2d`] that is closest to the specified `point`. + #[inline] + pub fn closest_point(&self, point: FGVec2) -> FGVec2 { + // `point` + // x + // ^| + // / | + //`offset`/ | + // / | `segment_vector` + // x----.-------------->x + // 0 t 1 + 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); + + // `point` is too far "left" in the picture + if projection_scaled <= 0.0 { + return self.point1; + } + + let length_squared = segment_vector.length_squared(); + // `point` is too far "right" in the picture + if projection_scaled >= length_squared { + return self.point2; + } + + // Point lies somewhere in the middle, we compute the closest point by finding the parameter along the line. + let t = projection_scaled / length_squared; + self.point1 + segment_vector * t + } + + /// Compute the normalized counterclockwise normal on the left-hand side of the line segment. + /// + /// For the non-panicking version, see [`Segment2d::try_left_normal`]. + /// + /// # Panics + /// + /// Panics if a valid normal could not be computed, for example when the endpoints are coincident + pub fn left_normal(&self) -> FGVec2 { + self.point1.direction_to(self.point2).map(|o| { FGVec2::new(-o.y, o.x)}).expect("a valid normal could not be computed") + } + + pub fn segment_intersection(&self, rhs: &FGSegment2d) -> Option { let p0 = self.point1(); let p1 = self.point2(); let p2 = rhs.point1(); @@ -48,45 +107,31 @@ impl SegmentIntersection for Segment2d { let s2_y = p3.y - p2.y; let denom = -s2_x * s1_y + s1_x * s2_y; - if denom.abs() < f32::EPSILON { + if denom == FGi32::ZERO { return None; // Parallel or collinear } let s = (-s1_y * (p0.x - p2.x) + s1_x * (p0.y - p2.y)) / denom; let t = ( s2_x * (p0.y - p2.y) - s2_y * (p0.x - p2.x)) / denom; - if s >= 0.0 && s <= 1.0 && t >= 0.0 && t <= 1.0 { - Some(Vec2::new(p0.x + (t * s1_x), p0.y + (t * s1_y))) + if s >= FGi32::ZERO && s <= FGi32::ONE && t >= FGi32::ZERO && t <= FGi32::ONE { + Some(FGVec2::new(p0.x + (t * s1_x), p0.y + (t * s1_y))) } else { None } } } -impl SegmentIntersection for Segment2d { - fn segment_intersection(&self, rhs: &Segment2d) -> Option { - let p0 = self.point1(); - let p1 = self.point2(); - let p2 = rhs.point1(); - let p3 = rhs.point2(); - - let s1_x = p1.x - p0.x; - let s1_y = p1.y - p0.y; - let s2_x = p3.x - p2.x; - let s2_y = p3.y - p2.y; +#[cfg(test)] +mod tests { + use super::*; - let denom = -s2_x * s1_y + s1_x * s2_y; - if denom.abs() < f32::EPSILON { - return None; // Parallel or collinear - } + #[test] + fn closest_point_projects_and_clamps_to_segment() { + let segment = FGSegment2d::new(FGVec2::lit("-4", "0"), FGVec2::lit("4", "0")); - let s = (-s1_y * (p0.x - p2.x) + s1_x * (p0.y - p2.y)) / denom; - let t = ( s2_x * (p0.y - p2.y) - s2_y * (p0.x - p2.x)) / denom; - - if s >= 0.0 && s <= 1.0 && t >= 0.0 && t <= 1.0 { - Some(Vec2::new(p0.x + (t * s1_x), p0.y + (t * s1_y))) - } else { - None - } + assert_eq!(segment.closest_point(FGVec2::lit("2", "1")), FGVec2::lit("2", "0")); + assert_eq!(segment.closest_point(FGVec2::lit("-6", "1")), segment.point1()); + assert_eq!(segment.closest_point(FGVec2::lit("6", "1")), segment.point2()); } -} \ No newline at end of file +} diff --git a/src/math/vec.rs b/src/math/vec.rs index 600dff6..cbcaa09 100644 --- a/src/math/vec.rs +++ b/src/math/vec.rs @@ -1,14 +1,24 @@ use bevy::math::Vec2; -use std::ops::{Add, Div, Mul, Sub}; +use bevy::prelude::*; +use fixed::types::I32F32; +use serde::{Deserialize, Serialize}; +use std::ops::{AddAssign, Add, Div, Mul, Sub}; use super::int::FGi32; -#[derive(Clone, Copy, PartialEq)] +#[derive(Clone, Copy, PartialEq, Default, Debug, Reflect, Serialize, Deserialize)] +#[reflect(opaque)] pub struct FGVec2 { pub x: FGi32, pub y: FGi32, } +impl std::fmt::Display for FGVec2 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "[{}, {}]", self.x, self.y) + } +} + impl FGVec2 { pub const ZERO: Self = Self::new(FGi32::ZERO, FGi32::ZERO); @@ -17,6 +27,11 @@ impl FGVec2 { Self { x, y } } + #[inline(always)] + pub const fn lit(x: &str, y: &str) -> Self { + Self { x: FGi32::lit(x), y: FGi32::lit(y) } + } + #[inline(always)] pub fn from_i32(x: i32, y: i32) -> Self { Self { @@ -31,17 +46,78 @@ impl FGVec2 { } #[inline] - pub fn length(&self) -> FGi32 { + pub fn length(self) -> FGi32 { (self.x * self.x + self.y * self.y).sqrt() } + /// Returns the vector projection of `self` onto `rhs`. + /// + /// `rhs` must be normalized. + /// + /// # Panics + /// + /// Will panic if `rhs` is not normalized when `glam_assert` is enabled. #[inline] - pub fn normalized_or_zero(&self) -> Self { - let len = self.length(); - if len.is_zero() { - Self::ZERO + #[must_use] + pub fn project_onto_normalized(self, rhs: Self) -> Self { + assert!(rhs.is_normalized()); + + rhs * self.dot(rhs) + } + + const EPS: FGi32 = FGi32::from_bits(6); + + #[inline] + #[must_use] + pub fn is_normalized(self) -> bool { + let diff = self.length() - FGi32::ONE; + diff.abs() <= Self::EPS + } + + #[inline] + #[must_use] + pub fn dot(self, rhs: Self) -> FGi32 { + (self.x * rhs.x) + (self.y * rhs.y) + } + + #[inline] + #[must_use] + pub fn project_onto(self, rhs: Self) -> Self { + let other_len_sq = rhs.dot(rhs); + assert!(other_len_sq != FGi32::ZERO); + rhs * (self.dot(rhs) / other_len_sq) + } + + #[inline] + pub fn normalize_or_zero(&self) -> Self { + self.normalize().unwrap_or(FGVec2::ZERO) + } + + #[inline] + pub fn direction_to(self, to: Self) -> Option { + (to - self).normalize() + } + + #[inline] + #[must_use] + pub fn distance(self, to: Self) -> FGi32 { + (to - self).length() + } + + #[inline] + pub fn normalize(&self) -> Option { + let x = I32F32::from_num(self.x); + let y = I32F32::from_num(self.y); + let length_squared = x * x + y * y; + + if length_squared.is_zero() { + None } else { - self / len + let length = length_squared.sqrt(); + Some(Self::new( + FGi32::from_num(x / length), + FGi32::from_num(y / length), + )) } } @@ -78,6 +154,14 @@ impl Sub for FGVec2 { } } +impl Sub<&FGVec2> for &FGVec2 { + type Output = FGVec2; + #[inline] + fn sub(self, rhs: &FGVec2) -> FGVec2 { + (*self).sub(*rhs) + } +} + impl Div for FGVec2 { type Output = Self; @@ -113,3 +197,117 @@ impl Div for &FGVec2 { (*self).div(rhs) } } + +impl AddAssign for FGVec2 { + fn add_assign(&mut self, rhs: Self) { + *self = Self { + x: self.x + rhs.x, + y: self.y + rhs.y + } + } +} + +#[cfg(test)] +mod is_normalized_tests { + use super::*; + use fixed::types::I16F16 as FGi32; + + // Normalization is computed in Q32.32, so the Q16.16 error comes from the + // two final component conversions and length_squared()'s multiplications. + const EPS_FIXED_BITS: i32 = 4; + + #[test] + fn normalizes_small_axis_vector_without_losing_squared_precision() { + let normalized = FGVec2::lit("0.01", "0").normalize().unwrap(); + + assert_eq!(normalized, FGVec2::lit("1", "0")); + assert!(normalized.is_normalized()); + } + + #[test] + fn measure_normalize_error_bound() { + let mut max_abs_diff_bits: i32 = 0; + let mut worst_case: Option<(f64, f64, i32)> = None; + + let angle_steps = 3600; // 0.1 degree resolution + let magnitudes: &[f64] = &[ + 0.001, 0.01, 0.1, 0.5, 1.0, 2.0, 10.0, 100.0, 1000.0, 30000.0, + ]; + + for &mag in magnitudes { + for i in 0..angle_steps { + let theta = (i as f64) * std::f64::consts::TAU / (angle_steps as f64); + let x = mag * theta.cos(); + let y = mag * theta.sin(); + + let Some(fx) = FGi32::checked_from_num(x) else { continue }; + let Some(fy) = FGi32::checked_from_num(y) else { continue }; + + let v = FGVec2 { x: fx, y: fy }; + + if let Some(normalized) = v.normalize() { + let ls = normalized.length_squared(); + let diff_bits = ls.to_bits() - FGi32::ONE.to_bits(); + let abs_diff_bits = diff_bits.abs(); + + if abs_diff_bits > max_abs_diff_bits { + max_abs_diff_bits = abs_diff_bits; + worst_case = Some((x, y, abs_diff_bits)); + } + } + } + } + + let ulp_value = FGi32::DELTA.to_num::(); + println!( + "Max |length_squared - 1.0| = {} raw bits ({:e} real), worst case: {:?}", + max_abs_diff_bits, + max_abs_diff_bits as f64 * ulp_value, + worst_case + ); + + assert!( + max_abs_diff_bits <= EPS_FIXED_BITS, + "measured error ({} bits) exceeds analytical bound ({} bits) — \ + revisit the epsilon derivation", + max_abs_diff_bits, + EPS_FIXED_BITS + ); + + println!( + "Bound: {} bits, measured: {} bits (headroom: {} bits)", + EPS_FIXED_BITS, + max_abs_diff_bits, + EPS_FIXED_BITS - max_abs_diff_bits + ); + } + + #[test] + fn is_normalized_behaves_correctly() { + let angle_steps = 360; + for i in 0..angle_steps { + let theta = (i as f64) * std::f64::consts::TAU / (angle_steps as f64); + for &mag in &[0.01, 1.0, 100.0, 10000.0] { + let x = mag * theta.cos(); + let y = mag * theta.sin(); + let Some(fx) = FGi32::checked_from_num(x) else { continue }; + let Some(fy) = FGi32::checked_from_num(y) else { continue }; + let v = FGVec2 { x: fx, y: fy }; + + if let Some(normalized) = v.normalize() { + assert!( + normalized.is_normalized(), + "normalized({}, {}) failed is_normalized(): length_squared = {:?}", + x, y, normalized.length_squared() + ); + } + } + } + + let non_unit = FGVec2 { x: FGi32::from_num(2.0), y: FGi32::from_num(0.0) }; + assert!(!non_unit.is_normalized()); + + let non_unit_small = FGVec2 { x: FGi32::from_num(0.5), y: FGi32::from_num(0.0) }; + assert!(!non_unit_small.is_normalized()); + } +} diff --git a/src/stage/debug.rs b/src/stage/debug.rs index 2b359c9..838fd1a 100644 --- a/src/stage/debug.rs +++ b/src/stage/debug.rs @@ -5,11 +5,11 @@ use crate::stage::{StagePoly, line::StageCollision}; pub fn debug_draw_scene(mut gizmos: Gizmos, stage_collision: Res) { for poly in stage_collision.stage_polys.iter() { for segment in poly.segments.iter() { - let from = segment.segment.point1(); - let to = segment.segment.point2(); + let from = segment.segment.point1().to_vec2(); + let to = segment.segment.point2().to_vec2(); let from_3d = Vec3::new(from.x, from.y, 0.0); let end_3d = Vec3::new(to.x, to.y, 0.0); - let normal_3d = Vec3::new(segment.normal.x, segment.normal.y, 0.0); + let normal_3d = Vec3::new(segment.normal.x.to_num(), segment.normal.y.to_num(), 0.0); gizmos.line(from_3d, end_3d, RED); let midpoint = (from_3d + end_3d) * 0.5; gizmos.arrow(midpoint, midpoint + normal_3d, RED); diff --git a/src/stage/line.rs b/src/stage/line.rs index d3b53df..07260f9 100644 --- a/src/stage/line.rs +++ b/src/stage/line.rs @@ -4,7 +4,7 @@ use std::default; use bevy::{math::InvalidDirectionError, prelude::*}; -use crate::math::segment::SegmentIntersection; +use crate::math::{segment::{FGSegment2d}, vec::FGVec2}; #[derive(Reflect, Clone, Copy, PartialEq)] pub enum StagePolyLineSegmentType { @@ -14,8 +14,8 @@ pub enum StagePolyLineSegmentType { #[derive(Reflect, Clone)] pub struct StagePolyLineSegment { - pub segment: Segment2d, - pub normal: Vec2, + pub segment: FGSegment2d, + pub normal: FGVec2, pub segment_type: StagePolyLineSegmentType } @@ -40,9 +40,9 @@ pub struct StagePoly { } pub struct StagePlaneIntersectResult { - pub position: Vec2, + pub position: FGVec2, pub segment_idx: usize, - pub normal: Vec2 + pub normal: FGVec2 } #[derive(Resource, Reflect, Clone, Default)] @@ -52,18 +52,30 @@ pub struct StageCollision { } impl StagePoly { - pub fn intersect_ray(&self, ray_segment: Segment2d) -> Option { - for (i, segment) in self.segments.iter().enumerate() { - if let Some(intersection) = segment.segment.segment_intersection(&ray_segment) { - println!("DIST3 {} {}", intersection, segment.segment.closest_point(intersection)); - return Some(StagePlaneIntersectResult { - position: intersection, - segment_idx: i, - normal: segment.normal - }); - } - } - None + pub fn intersect_ray(&self, ray_segment: FGSegment2d) -> Option { + let ray_start = ray_segment.point1(); + let (segment_idx, segment, position) = self + .segments + .iter() + .enumerate() + .filter_map(|(segment_idx, segment)| { + segment + .segment + .segment_intersection(&ray_segment) + .map(|position| (segment_idx, segment, position)) + }) + .min_by_key(|(_, _, position)| { + let offset = *position - ray_start; + let x = i128::from(offset.x.to_bits()); + let y = i128::from(offset.y.to_bits()); + x * x + y * y + })?; + + Some(StagePlaneIntersectResult { + position, + segment_idx, + normal: segment.normal, + }) } pub fn get_segment(&self, segment_idx: usize) -> Option<&StagePolyLineSegment> { @@ -71,24 +83,65 @@ impl StagePoly { } pub fn get_next_segment(&self, segment_idx: usize) -> Option<&StagePolyLineSegment> { - self.segments.get(segment_idx % segment_idx + 1) + self.get_next_segment_index(segment_idx) + .and_then(|index| self.segments.get(index)) } pub fn get_prev_segment(&self, segment_idx: usize) -> Option<&StagePolyLineSegment> { - self.segments.get(segment_idx % segment_idx + 1) + self.get_prev_segment_index(segment_idx) + .and_then(|index| self.segments.get(index)) } - pub fn build(poly_type: StagePolyType, points: &[(Vec2, StagePolyLineSegmentType)]) -> Self { - let windows = points.iter().zip(points.iter().cycle().skip(1)); + pub fn get_next_segment_index(&self, segment_idx: usize) -> Option { + if segment_idx >= self.segments.len() { + return None; + } + + let next = segment_idx + 1; + if next < self.segments.len() { + Some(next) + } else if matches!(&self.poly_type, StagePolyType::Closed) { + Some(0) + } else { + None + } + } + + pub fn get_prev_segment_index(&self, segment_idx: usize) -> Option { + if segment_idx >= self.segments.len() { + return None; + } + + if segment_idx > 0 { + Some(segment_idx - 1) + } else if matches!(&self.poly_type, StagePolyType::Closed) { + self.segments.len().checked_sub(1) + } else { + None + } + } + + pub fn build(poly_type: StagePolyType, points: &[(FGVec2, StagePolyLineSegmentType)]) -> Self { + let segment_count = if points.len() < 2 { + 0 + } else if matches!(&poly_type, StagePolyType::Closed) { + points.len() + } else { + points.len() - 1 + }; + let windows = points + .iter() + .zip(points.iter().cycle().skip(1)) + .take(segment_count); let segments = windows.map(|segment_window| { let (first_point, segment_type) = segment_window.0; let (second_point, _) = segment_window.1; - let segment = Segment2d::new(*first_point, *second_point); + let segment = FGSegment2d::new(*first_point, *second_point); StagePolyLineSegment { segment_type: *segment_type, segment, - normal: segment.left_normal().as_vec2() + normal: segment.left_normal() } }).collect(); @@ -100,46 +153,97 @@ impl StagePoly { } impl StagePolyLineSegment { - pub fn project(&self, vec: Vec2) -> Vec2 { + pub fn project(&self, vec: FGVec2) -> FGVec2 { vec - vec.project_onto_normalized(self.normal) } } #[cfg(test)] mod tests { + use crate::math::int::FGi32; + use super::*; + + fn stage_segment(point1: FGVec2, point2: FGVec2) -> StagePolyLineSegment { + let segment = FGSegment2d::new(point1, point2); + StagePolyLineSegment { + segment, + normal: segment.left_normal(), + segment_type: StagePolyLineSegmentType::Floor, + } + } + + #[test] + fn platform_is_open_and_closed_poly_wraps() { + let points = [ + (FGVec2::lit("0", "0"), StagePolyLineSegmentType::Floor), + (FGVec2::lit("1", "0"), StagePolyLineSegmentType::Floor), + (FGVec2::lit("2", "0"), StagePolyLineSegmentType::Floor), + ]; + let platform = StagePoly::build(StagePolyType::Platform, &points); + let closed = StagePoly::build(StagePolyType::Closed, &points); + + assert_eq!(platform.segments.len(), 2); + assert_eq!(platform.get_next_segment_index(0), Some(1)); + assert_eq!(platform.get_next_segment_index(1), None); + assert_eq!(platform.get_prev_segment_index(0), None); + assert_eq!(platform.get_prev_segment_index(1), Some(0)); + + assert_eq!(closed.segments.len(), 3); + assert_eq!(closed.get_next_segment_index(2), Some(0)); + assert_eq!(closed.get_prev_segment_index(0), Some(2)); + } + + #[test] + fn ray_intersection_returns_nearest_hit() { + let stage_poly = StagePoly { + poly_type: StagePolyType::Platform, + segments: vec![ + stage_segment(FGVec2::lit("-1", "0"), FGVec2::lit("1", "0")), + stage_segment(FGVec2::lit("-1", "1"), FGVec2::lit("1", "1")), + ], + }; + + let hit = stage_poly + .intersect_ray(FGSegment2d::new(FGVec2::lit("0", "2"), FGVec2::lit("0", "-1"))) + .unwrap(); + + assert_eq!(hit.segment_idx, 1); + assert_eq!(hit.position, FGVec2::lit("0", "1")); + } + #[test] fn ray_intersection_test() { let stage_poly = StagePoly::build(crate::stage::line::StagePolyType::Platform, &[ - (Vec2::new(-4.5, 0.0), StagePolyLineSegmentType::Floor), - (Vec2::new(4.5, 0.0), StagePolyLineSegmentType::Floor), + (FGVec2::lit("-4.5", "0.0"), StagePolyLineSegmentType::Floor), + (FGVec2::lit("4.5", "0.0"), StagePolyLineSegmentType::Floor), ]); - let res = stage_poly.intersect_ray(Segment2d::new(Vec2::new(0.0, 1.0), Vec2::new(0.0, -1.0))); + let res = stage_poly.intersect_ray(FGSegment2d::new(FGVec2::lit("0.0", "1.0"), FGVec2::lit("0.0", "-1.0"))); assert!(res.is_some()); - assert_eq!(res.unwrap().position, Vec2::ZERO); + assert_eq!(res.unwrap().position, FGVec2::ZERO); } #[test] fn ray_intersection_test_side() { let stage_poly = StagePoly::build(crate::stage::line::StagePolyType::Platform, &[ - (Vec2::new(-4.5, 0.0), StagePolyLineSegmentType::Floor), - (Vec2::new(4.5, 0.0), StagePolyLineSegmentType::Floor), + (FGVec2::lit("-4.5", "0.0"), StagePolyLineSegmentType::Floor), + (FGVec2::lit("4.5", "0.0"), StagePolyLineSegmentType::Floor), ]); - let res = stage_poly.intersect_ray(Segment2d::new(Vec2::new(2.0, 1.0), Vec2::new(2.0, -1.0))); + let res = stage_poly.intersect_ray(FGSegment2d::new(FGVec2::lit("2.0", "1.0"), FGVec2::lit("2.0", "-1.0"))); assert!(res.is_some()); - assert_eq!(res.unwrap().position, Vec2::new(2.0, 0.0)); + assert!(res.unwrap().position.distance(FGVec2::lit("2.0", "0.0")) < FGi32::from_bits(4)); } #[test] fn ray_intersection_test_diagonal() { let stage_poly = StagePoly::build(crate::stage::line::StagePolyType::Platform, &[ - (Vec2::new(0.0, 0.0), StagePolyLineSegmentType::Floor), - (Vec2::new(2.0, 2.0), StagePolyLineSegmentType::Floor), + (FGVec2::lit("0.0", "0.0"), StagePolyLineSegmentType::Floor), + (FGVec2::lit("2.0", "2.0"), StagePolyLineSegmentType::Floor), ]); - let res = stage_poly.intersect_ray(Segment2d::new(Vec2::new(1.0, 2.0), Vec2::new(1.0, -2.0))); + let res = stage_poly.intersect_ray(FGSegment2d::new(FGVec2::lit("1.0", "2.0"), FGVec2::lit("1.0", "-2.0"))); assert!(res.is_some()); - assert_eq!(res.unwrap().position, Vec2::new(1.0, 1.0)); + assert_eq!(res.unwrap().position, FGVec2::lit("1.0", "1.0")); } -} \ No newline at end of file +} diff --git a/src/stage/scene.rs b/src/stage/scene.rs index 9a6cd5f..cc0527f 100644 --- a/src/stage/scene.rs +++ b/src/stage/scene.rs @@ -4,19 +4,19 @@ use std::f32::consts::PI; use bevy::{camera_controller::free_camera::FreeCamera, prelude::*}; -use crate::stage::{StagePoly, line::StagePolyLineSegmentType, line::StageCollision}; +use crate::{math::vec::FGVec2, stage::{StagePoly, line::{StageCollision, StagePolyLineSegmentType}}}; /// set up a simple 3D scene pub fn test_scene() -> impl SceneList { let stage_poly = StagePoly::build(crate::stage::line::StagePolyType::Closed, &[ - (Vec2::new(-5.6, -0.35), StagePolyLineSegmentType::Floor), - (Vec2::new(-3.92, 0.0), StagePolyLineSegmentType::Floor), - (Vec2::new(0.0, 0.0), StagePolyLineSegmentType::Floor), - (Vec2::new(3.92, 0.0), StagePolyLineSegmentType::Floor), - (Vec2::new(5.6, -0.35), StagePolyLineSegmentType::Wall), - (Vec2::new(5.6, -20.0), StagePolyLineSegmentType::Ceiling), - (Vec2::new(-5.6, -20.0), StagePolyLineSegmentType::Wall) + (FGVec2::lit("-5.6", "-0.35"), StagePolyLineSegmentType::Floor), + (FGVec2::lit("-3.92", "0.0"), StagePolyLineSegmentType::Floor), + (FGVec2::lit("0.0", "0.0"), StagePolyLineSegmentType::Floor), + (FGVec2::lit("3.92", "0.0"), StagePolyLineSegmentType::Floor), + (FGVec2::lit("5.6", "-0.35"), StagePolyLineSegmentType::Wall), + (FGVec2::lit("5.6", "-20.0"), StagePolyLineSegmentType::Ceiling), + (FGVec2::lit("-5.6", "-20.0"), StagePolyLineSegmentType::Wall) ]); bsn_list! [