Port to fixed point math
This commit is contained in:
Generated
+1
@@ -3234,6 +3234,7 @@ dependencies = [
|
|||||||
"az",
|
"az",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"half",
|
"half",
|
||||||
|
"serde",
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -29,7 +29,7 @@ serde = "1.0.229"
|
|||||||
bitfields = "3.0.0"
|
bitfields = "3.0.0"
|
||||||
clap = { version = "4.6.6", features = ["derive"] }
|
clap = { version = "4.6.6", features = ["derive"] }
|
||||||
bevy_wind_waker_shader = { git = "https://github.com/lavaeater/bevy_wind_waker_shader.git" }
|
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.
|
# Enable a small amount of optimization in the dev profile.
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ pub struct FighterAnimationPlayerLink(Entity);
|
|||||||
pub fn apply_fighter_translation_to_visuals(query: Query<(&FighterTranslation, &FighterECB, &mut Transform)>) {
|
pub fn apply_fighter_translation_to_visuals(query: Query<(&FighterTranslation, &FighterECB, &mut Transform)>) {
|
||||||
for (translation, ecb, mut transform) in query {
|
for (translation, ecb, mut transform) in query {
|
||||||
let translation = translation.0 + ecb.get_bottom_point();
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+15
-14
@@ -1,31 +1,32 @@
|
|||||||
use bevy::prelude::*;
|
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 —
|
/// Per-fighter tuning values. These are character data, not global rules —
|
||||||
/// anything that applies to every fighter belongs in
|
/// anything that applies to every fighter belongs in
|
||||||
/// [`crate::game_settings::FighterSettingsCommon`] instead.
|
/// [`crate::game_settings::FighterSettingsCommon`] instead.
|
||||||
#[derive(Component, Clone, Copy)]
|
#[derive(Component, Clone, Copy)]
|
||||||
pub struct FighterAttributes {
|
pub struct FighterAttributes {
|
||||||
pub base_walk_accel: f32,
|
pub base_walk_accel: FGi32,
|
||||||
pub stick_walk_accel: f32,
|
pub stick_walk_accel: FGi32,
|
||||||
pub max_walk_vel: f32,
|
pub max_walk_vel: FGi32,
|
||||||
|
|
||||||
pub dash_duration: u32,
|
pub dash_duration: u32,
|
||||||
pub dash_initial_velocity: f32,
|
pub dash_initial_velocity: FGi32,
|
||||||
pub base_dash_accel: f32,
|
pub base_dash_accel: FGi32,
|
||||||
pub stick_dash_accel: f32,
|
pub stick_dash_accel: FGi32,
|
||||||
pub max_dash_vel: f32,
|
pub max_dash_vel: FGi32,
|
||||||
|
|
||||||
pub jump_vertical_velocity: f32,
|
pub jumpsquat_duration: u32,
|
||||||
|
pub jump_vertical_velocity: FGi32,
|
||||||
|
|
||||||
pub ground_friction: f32,
|
pub ground_friction: FGi32,
|
||||||
pub terminal_velocity: f32,
|
pub terminal_velocity: FGi32,
|
||||||
pub gravity: f32,
|
pub gravity: FGi32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FighterAttributes {
|
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 = input.movement.x * self.stick_dash_accel;
|
||||||
let accel = accel + input.movement.x.signum() * self.base_dash_accel;
|
let accel = accel + input.movement.x.signum() * self.base_dash_accel;
|
||||||
let target_vel = input.movement.x * self.max_dash_vel;
|
let target_vel = input.movement.x * self.max_dash_vel;
|
||||||
@@ -33,7 +34,7 @@ impl FighterAttributes {
|
|||||||
(accel, target_vel)
|
(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 = input.movement.x * self.stick_walk_accel;
|
||||||
let accel = accel + input.movement.x.signum() * self.base_walk_accel;
|
let accel = accel + input.movement.x.signum() * self.base_walk_accel;
|
||||||
let target_vel = input.movement.x * self.max_walk_vel;
|
let target_vel = input.movement.x * self.max_walk_vel;
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ use bevy::{color::palettes::css::HOT_PINK, prelude::*};
|
|||||||
use crate::fighter::ecb::FighterPreviousECB;
|
use crate::fighter::ecb::FighterPreviousECB;
|
||||||
use crate::fighter::state::FighterStateContext;
|
use crate::fighter::state::FighterStateContext;
|
||||||
use crate::fighter::{FighterECB, FighterPreviousTranslation, FighterTranslation, FighterVelocity};
|
use crate::fighter::{FighterECB, FighterPreviousTranslation, FighterTranslation, FighterVelocity};
|
||||||
|
use crate::math::segment::FGSegment2d;
|
||||||
|
use crate::math::vec::FGVec2;
|
||||||
use crate::stage::StagePoly;
|
use crate::stage::StagePoly;
|
||||||
use crate::stage::line::{StageCollision, StageLineID};
|
use crate::stage::line::{StageCollision, StageLineID};
|
||||||
/*
|
/*
|
||||||
@@ -36,14 +38,14 @@ pub fn collide_fighter_with_scene(
|
|||||||
|
|
||||||
pub struct AirCollisionWithStageResult {
|
pub struct AirCollisionWithStageResult {
|
||||||
pub line_id: StageLineID,
|
pub line_id: StageLineID,
|
||||||
pub hit_position: Vec2,
|
pub hit_position: FGVec2,
|
||||||
pub hit_normal: Vec2,
|
pub hit_normal: FGVec2,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn air_collide_with_stage(
|
pub fn air_collide_with_stage(
|
||||||
state_context: &FighterStateContext,
|
state_context: &FighterStateContext,
|
||||||
) -> Option<AirCollisionWithStageResult> {
|
) -> Option<AirCollisionWithStageResult> {
|
||||||
let ray_segment = Segment2d::new(
|
let ray_segment = FGSegment2d::new(
|
||||||
state_context.prev_translation.0 + state_context.prev_ecb.get_bottom_point(),
|
state_context.prev_translation.0 + state_context.prev_ecb.get_bottom_point(),
|
||||||
state_context.translation.0 + state_context.ecb.get_bottom_point(),
|
state_context.translation.0 + state_context.ecb.get_bottom_point(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
//! Fighter debug views: gizmos and the egui inspector window.
|
//! Fighter debug views: gizmos and the egui inspector window.
|
||||||
|
|
||||||
|
use bevy::color::palettes::css::ORANGE;
|
||||||
use bevy::{color::palettes::css::YELLOW, prelude::*};
|
use bevy::{color::palettes::css::YELLOW, prelude::*};
|
||||||
use bevy_egui::{EguiContexts, egui};
|
use bevy_egui::{EguiContexts, egui};
|
||||||
|
|
||||||
|
use crate::fighter::ecb::FighterPreviousECB;
|
||||||
use crate::fighter::state::StateNameDebug;
|
use crate::fighter::state::StateNameDebug;
|
||||||
use crate::fighter::{FighterECB, FighterTranslation, FighterVelocity};
|
use crate::fighter::{FighterECB, FighterPreviousTranslation, FighterTranslation, FighterVelocity};
|
||||||
use crate::input::FighterInput;
|
use crate::input::FighterInput;
|
||||||
|
|
||||||
pub fn debug_draw_ecb(ecbs: Query<(&FighterECB, &FighterTranslation)>, mut gizmos: Gizmos) {
|
pub fn debug_draw_ecb(ecbs: Query<(&FighterECB, &FighterTranslation, &FighterPreviousECB, &FighterPreviousTranslation)>, mut gizmos: Gizmos) {
|
||||||
for (ecb, trf) in ecbs {
|
for (ecb, trf, ecb_prev, trf_prev) in ecbs {
|
||||||
let transformed = ecb.to_3d_lineloop(Transform::from_xyz(trf.x, trf.y, 0.0));
|
let transformed = ecb.to_3d_lineloop(Transform::from_xyz(trf.x.to_num(), trf.y.to_num(), 0.0));
|
||||||
gizmos.lineloop(transformed, YELLOW);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+53
-16
@@ -1,4 +1,7 @@
|
|||||||
use bevy::prelude::*;
|
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
|
/// Environment collision box: the diamond used to resolve a fighter against
|
||||||
/// the stage. Purely geometry — the collision response lives in
|
/// the stage. Purely geometry — the collision response lives in
|
||||||
@@ -6,40 +9,74 @@ use bevy::prelude::*;
|
|||||||
#[derive(Component, Debug, Clone, Copy)]
|
#[derive(Component, Debug, Clone, Copy)]
|
||||||
#[require(FighterPreviousECB)]
|
#[require(FighterPreviousECB)]
|
||||||
pub struct FighterECB {
|
pub struct FighterECB {
|
||||||
pub vertical_half: f32,
|
pub vertical_half: FGi32,
|
||||||
pub horizontal_half: f32,
|
pub horizontal_half: FGi32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Component, Debug, Clone, Copy, Default)]
|
#[derive(Component, Debug, Clone, Copy, Default)]
|
||||||
pub struct FighterPreviousECB {
|
pub struct FighterPreviousECB {
|
||||||
pub vertical_half: f32,
|
pub vertical_half: FGi32,
|
||||||
pub horizontal_half: f32,
|
pub horizontal_half: FGi32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FighterECB> 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 {
|
impl FighterECB {
|
||||||
pub fn to_3d_lineloop(&self, trf: Transform) -> [Vec3; 4] {
|
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_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, 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, 0.0, 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, 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]
|
[rel_up, rel_right, rel_down, rel_left]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_bottom_point(&self) -> Vec2 {
|
pub fn get_bottom_point(&self) -> FGVec2 {
|
||||||
Vec2::new(0.0, -self.vertical_half)
|
FGVec2::new(FGi32::ZERO, -self.vertical_half)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FighterPreviousECB {
|
impl FighterPreviousECB {
|
||||||
pub fn to_3d_lineloop(&self, trf: Transform) -> [Vec3; 4] {
|
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_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, 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, 0.0, 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, 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]
|
[rel_up, rel_right, rel_down, rel_left]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_bottom_point(&self) -> Vec2 {
|
pub fn get_bottom_point(&self) -> FGVec2 {
|
||||||
Vec2::new(0.0, -self.vertical_half)
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-1
@@ -45,7 +45,12 @@ impl Plugin for FighterPlugin {
|
|||||||
// Every state gets a chance to interrupt itself, in priority order.
|
// Every state gets a chance to interrupt itself, in priority order.
|
||||||
.add_systems(
|
.add_systems(
|
||||||
GgrsSchedule,
|
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(
|
.add_systems(
|
||||||
GgrsSchedule,
|
GgrsSchedule,
|
||||||
@@ -74,6 +79,7 @@ impl Plugin for FighterPlugin {
|
|||||||
// components; a component simulated here but missing from this list
|
// components; a component simulated here but missing from this list
|
||||||
// is a desync waiting to happen.
|
// is a desync waiting to happen.
|
||||||
.rollback_component_with_copy::<FighterECB>()
|
.rollback_component_with_copy::<FighterECB>()
|
||||||
|
.rollback_component_with_copy::<ecb::FighterPreviousECB>()
|
||||||
.rollback_component_with_copy::<FighterVelocity>()
|
.rollback_component_with_copy::<FighterVelocity>()
|
||||||
.rollback_component_with_copy::<FighterTranslation>()
|
.rollback_component_with_copy::<FighterTranslation>()
|
||||||
.rollback_component_with_copy::<FighterPreviousTranslation>()
|
.rollback_component_with_copy::<FighterPreviousTranslation>()
|
||||||
|
|||||||
@@ -3,20 +3,20 @@
|
|||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
use crate::fighter::FighterAttributes;
|
use crate::{fighter::FighterAttributes, math::vec::FGVec2};
|
||||||
|
|
||||||
#[derive(Component, Clone, Copy)]
|
#[derive(Component, Clone, Copy)]
|
||||||
pub struct Grounded;
|
pub struct Grounded;
|
||||||
|
|
||||||
#[derive(Component, Deref, DerefMut, Default, Debug, Clone, Copy)]
|
#[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)]
|
#[derive(Component, Deref, DerefMut, Default, Clone, Copy)]
|
||||||
pub struct FighterPreviousTranslation(pub Vec2);
|
pub struct FighterPreviousTranslation(pub FGVec2);
|
||||||
|
|
||||||
#[derive(Component, Deref, DerefMut, Default, Clone, Copy)]
|
#[derive(Component, Deref, DerefMut, Default, Clone, Copy)]
|
||||||
#[require(FighterPreviousTranslation)]
|
#[require(FighterPreviousTranslation)]
|
||||||
pub struct FighterTranslation(pub Vec2);
|
pub struct FighterTranslation(pub FGVec2);
|
||||||
|
|
||||||
pub fn apply_air_motion(
|
pub fn apply_air_motion(
|
||||||
fighters: Query<(
|
fighters: Query<(
|
||||||
@@ -37,7 +37,7 @@ pub fn copy_fighter_transform_to_visuals(
|
|||||||
fighters: Query<(&FighterTranslation, &mut Transform)>,
|
fighters: Query<(&FighterTranslation, &mut Transform)>,
|
||||||
) {
|
) {
|
||||||
for (translation, mut transform) in fighters {
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::hash::{Hash, Hasher};
|
|||||||
|
|
||||||
use bevy::prelude::*;
|
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::ground::GroundedStateCommon;
|
||||||
use crate::fighter::state::wait::WaitState;
|
use crate::fighter::state::wait::WaitState;
|
||||||
use crate::fighter::state::{FighterState};
|
use crate::fighter::state::{FighterState};
|
||||||
@@ -12,11 +12,12 @@ use crate::input::{
|
|||||||
FighterCommands::{SmashMoveLeft, SmashMoveRight},
|
FighterCommands::{SmashMoveLeft, SmashMoveRight},
|
||||||
FighterInput,
|
FighterInput,
|
||||||
};
|
};
|
||||||
|
use crate::math::int::FGi32;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct DashState {
|
pub struct DashState {
|
||||||
frames_in_dash: u32,
|
frames_in_dash: u32,
|
||||||
direction: f32,
|
direction: FGi32,
|
||||||
ground_common: GroundedStateCommon
|
ground_common: GroundedStateCommon
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,7 +29,7 @@ impl std::hash::Hash for DashState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DashState {
|
impl DashState {
|
||||||
pub fn new(direction: f32, ground_common: GroundedStateCommon) -> Self {
|
pub fn new(direction: FGi32, ground_common: GroundedStateCommon) -> Self {
|
||||||
Self {
|
Self {
|
||||||
frames_in_dash: 0,
|
frames_in_dash: 0,
|
||||||
direction,
|
direction,
|
||||||
@@ -98,11 +99,11 @@ impl FighterStateImpl for DashState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_smash_input_with_dir(state_context: &FighterStateContext) -> Option<f32> {
|
fn check_smash_input_with_dir(state_context: &FighterStateContext) -> Option<FGi32> {
|
||||||
if state_context.input.has_command(SmashMoveLeft) {
|
if state_context.input.has_command(SmashMoveLeft) {
|
||||||
return Some(-1.0);
|
return Some(FGi32::NEG_ONE);
|
||||||
} else if state_context.input.has_command(SmashMoveRight) {
|
} else if state_context.input.has_command(SmashMoveRight) {
|
||||||
return Some(1.0);
|
return Some(FGi32::ONE);
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use bevy::prelude::*;
|
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::{FighterState, air};
|
||||||
use crate::fighter::state::ground::GroundedStateCommon;
|
use crate::fighter::state::ground::GroundedStateCommon;
|
||||||
use crate::fighter::state::wait::WaitState;
|
use crate::fighter::state::wait::WaitState;
|
||||||
|
|||||||
+20
-22
@@ -7,17 +7,20 @@ use crate::fighter::{
|
|||||||
FighterAttributes, FighterECB, FighterPreviousTranslation, FighterTranslation, FighterVelocity,
|
FighterAttributes, FighterECB, FighterPreviousTranslation, FighterTranslation, FighterVelocity,
|
||||||
};
|
};
|
||||||
use crate::game_settings::GameSettings;
|
use crate::game_settings::GameSettings;
|
||||||
|
use crate::math::int::FGi32;
|
||||||
|
use crate::math::vec::FGVec2;
|
||||||
use crate::stage::StagePoly;
|
use crate::stage::StagePoly;
|
||||||
use crate::stage::line::{StageCollision, StageLineID, StagePolyLineSegment, StagePolyLineSegmentType};
|
use crate::stage::line::{StageCollision, StageLineID, StagePolyLineSegment, StagePolyLineSegmentType};
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
use fixed::traits::Fixed;
|
||||||
|
|
||||||
#[derive(Reflect, Hash, Clone, Debug)]
|
#[derive(Reflect, Hash, Clone, Debug)]
|
||||||
pub struct GroundedStateCommon {
|
pub struct GroundedStateCommon {
|
||||||
pub current_line_id: StageLineID,
|
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() {
|
if friction.abs() > ground_vel.abs() {
|
||||||
-ground_vel
|
-ground_vel
|
||||||
} else {
|
} else {
|
||||||
@@ -28,21 +31,21 @@ pub fn apply_grounded_friction(friction: f32, ground_vel: f32) -> f32 {
|
|||||||
pub fn move_accelerate() {}
|
pub fn move_accelerate() {}
|
||||||
|
|
||||||
pub fn compute_ground_accel(
|
pub fn compute_ground_accel(
|
||||||
accel: f32,
|
accel: FGi32,
|
||||||
target_vel: f32,
|
target_vel: FGi32,
|
||||||
gr_vel: f32,
|
gr_vel: FGi32,
|
||||||
attribs: &FighterAttributes,
|
attribs: &FighterAttributes,
|
||||||
game_settings: &GameSettings,
|
game_settings: &GameSettings,
|
||||||
) -> f32 {
|
) -> FGi32 {
|
||||||
if target_vel == 0.0 {
|
if target_vel == 0.0 {
|
||||||
apply_grounded_friction(attribs.ground_friction, gr_vel)
|
apply_grounded_friction(attribs.ground_friction, gr_vel)
|
||||||
} else {
|
} else {
|
||||||
let mut accel = accel;
|
let mut accel = accel;
|
||||||
let ground_max_horizontal_velocity =
|
let ground_max_horizontal_velocity =
|
||||||
game_settings.figher_common.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
|
// accelerating, not reversing
|
||||||
if accel > 0.0 {
|
if accel > FGi32::ZERO {
|
||||||
if gr_vel + accel > target_vel {
|
if gr_vel + accel > target_vel {
|
||||||
accel = -attribs.ground_friction;
|
accel = -attribs.ground_friction;
|
||||||
if gr_vel + accel < target_vel {
|
if gr_vel + accel < target_vel {
|
||||||
@@ -78,7 +81,7 @@ pub fn apply_grounded_motion(
|
|||||||
) {
|
) {
|
||||||
// Vertical velocity should be 0 on the ground
|
// Vertical velocity should be 0 on the ground
|
||||||
prev_translation.0 = translation.0;
|
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(
|
pub fn collide_with_stage_grounded(
|
||||||
@@ -96,34 +99,29 @@ pub fn collide_with_stage_grounded(
|
|||||||
|
|
||||||
if let Some(stage_poly) = stage_poly {
|
if let Some(stage_poly) = stage_poly {
|
||||||
let ecb_bottom_pos = translation.0 + ecb.get_bottom_point();
|
let ecb_bottom_pos = translation.0 + ecb.get_bottom_point();
|
||||||
let left_segment_idx = ground_common
|
let current_segment_idx = ground_common.current_line_id.segment;
|
||||||
.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();
|
|
||||||
|
|
||||||
// First, we check the current segment, if that fails, we check the surrounding ones
|
// First, we check the current segment, if that fails, we check the surrounding ones
|
||||||
let grounded_ecb_bottom_pos = [
|
let grounded_ecb_bottom_pos = [
|
||||||
ground_common.current_line_id.segment.clone(),
|
Some(current_segment_idx),
|
||||||
left_segment_idx,
|
stage_poly.get_prev_segment_index(current_segment_idx),
|
||||||
right_segment_idx,
|
stage_poly.get_next_segment_index(current_segment_idx),
|
||||||
]
|
]
|
||||||
.iter()
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
.find_map(|segment_idx| {
|
.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 {
|
if segment.segment_type != StagePolyLineSegmentType::Floor {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let line_dir = segment.segment.direction();
|
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 closest_point = segment.segment.closest_point(ecb_bottom_pos);
|
||||||
let distance = closest_point.distance(projected_point);
|
let distance = closest_point.distance(projected_point);
|
||||||
|
|
||||||
if distance < 0.01 {
|
if distance < 0.01 {
|
||||||
return Some((projected_point, *segment_idx, segment.normal));
|
return Some((projected_point, segment_idx, segment.normal));
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
|
|||||||
+83
-18
@@ -1,41 +1,106 @@
|
|||||||
use bevy::prelude::*;
|
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 enum JumpType {
|
||||||
pub counter: u32
|
ShortHop,
|
||||||
|
LongJump,
|
||||||
|
DoubleJump,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Hash)]
|
||||||
|
pub struct JumpState {}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Hash)]
|
||||||
pub struct JumpSquatState {
|
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 {
|
impl FighterStateImpl for JumpSquatState {
|
||||||
const NAME: &'static str = "JumpSquat";
|
const NAME: &'static str = "JumpSquat";
|
||||||
fn check_interrupt(
|
|
||||||
&self,
|
fn check_interrupt(&self, state_context: &FighterStateContext) -> Option<FighterState> {
|
||||||
state_context: &super::FighterStateContext,
|
if self.duration_counter >= state_context.fighter_attribs.jumpsquat_duration {
|
||||||
) -> Option<FighterState> {
|
Some(FighterState::Jump(JumpState {}))
|
||||||
|
} else {
|
||||||
None
|
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<FighterState> {
|
||||||
|
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 {
|
impl FighterStateImpl for JumpState {
|
||||||
const NAME: &'static str = "Jump";
|
const NAME: &'static str = "Jump";
|
||||||
fn check_interrupt(
|
fn check_interrupt(&self, state_context: &FighterStateContext) -> Option<FighterState> {
|
||||||
&self,
|
|
||||||
state_context: &super::FighterStateContext,
|
|
||||||
) -> Option<FighterState> {
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, _state_context: &mut super::FighterStateContext) {
|
fn update(&mut self, _state_context: &mut FighterStateContext) {}
|
||||||
todo!()
|
}
|
||||||
}
|
|
||||||
|
pub fn check_input(
|
||||||
|
state_context: &FighterStateContext,
|
||||||
|
ground_common: &GroundedStateCommon,
|
||||||
|
) -> Option<FighterState> {
|
||||||
|
state_context
|
||||||
|
.input
|
||||||
|
.has_command(FighterCommands::Jump)
|
||||||
|
.then_some(FighterState::JumpSquat(JumpSquatState::create(
|
||||||
|
ground_common.clone(),
|
||||||
|
)));
|
||||||
|
None
|
||||||
}
|
}
|
||||||
+12
-12
@@ -11,6 +11,7 @@ use crate::fighter::ecb::FighterPreviousECB;
|
|||||||
use crate::fighter::{FighterAttributes, FighterECB, FighterPreviousTranslation, FighterTranslation, FighterVelocity};
|
use crate::fighter::{FighterAttributes, FighterECB, FighterPreviousTranslation, FighterTranslation, FighterVelocity};
|
||||||
use crate::game_settings::GameSettings;
|
use crate::game_settings::GameSettings;
|
||||||
use crate::input::FighterInput;
|
use crate::input::FighterInput;
|
||||||
|
use crate::math::int::FGi32;
|
||||||
use crate::schedule::GameplaySet;
|
use crate::schedule::GameplaySet;
|
||||||
use crate::stage::StagePoly;
|
use crate::stage::StagePoly;
|
||||||
use crate::stage::line::StageCollision;
|
use crate::stage::line::StageCollision;
|
||||||
@@ -24,14 +25,6 @@ pub mod fall;
|
|||||||
pub mod air;
|
pub mod air;
|
||||||
pub mod jump;
|
pub mod jump;
|
||||||
|
|
||||||
pub enum FighterStateTransition {
|
|
||||||
Wait,
|
|
||||||
Walk,
|
|
||||||
Dash(f32),
|
|
||||||
Run,
|
|
||||||
Fall,
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! fighter_states {
|
macro_rules! fighter_states {
|
||||||
($($variant:ident => $ty:ty),* $(,)?) => {
|
($($variant:ident => $ty:ty),* $(,)?) => {
|
||||||
#[derive(Component, Clone, Debug, Hash)]
|
#[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 translation: &'a mut FighterTranslation,
|
||||||
pub prev_translation: &'a mut FighterPreviousTranslation,
|
pub prev_translation: &'a mut FighterPreviousTranslation,
|
||||||
pub input: &'a mut FighterInput,
|
pub input: &'a mut FighterInput,
|
||||||
@@ -91,6 +84,7 @@ pub struct FighterStateContext<'a> {
|
|||||||
pub game_settings: &'a GameSettings,
|
pub game_settings: &'a GameSettings,
|
||||||
pub entity: Entity,
|
pub entity: Entity,
|
||||||
pub stage_collision: &'a StageCollision,
|
pub stage_collision: &'a StageCollision,
|
||||||
|
pub gizmos: Option<&'a mut Gizmos<'w, 's>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fighter_states! {
|
fighter_states! {
|
||||||
@@ -98,7 +92,9 @@ fighter_states! {
|
|||||||
Walk => walk::WalkState,
|
Walk => walk::WalkState,
|
||||||
Dash => dash::DashState,
|
Dash => dash::DashState,
|
||||||
Run => run::RunState,
|
Run => run::RunState,
|
||||||
Fall => fall::FallState
|
Fall => fall::FallState,
|
||||||
|
JumpSquat => jump::JumpSquatState,
|
||||||
|
Jump => jump::JumpState
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
@@ -128,6 +124,7 @@ pub fn state_update_system(
|
|||||||
game_settings: &game_settings,
|
game_settings: &game_settings,
|
||||||
entity: ent,
|
entity: ent,
|
||||||
stage_collision: &stage_collision,
|
stage_collision: &stage_collision,
|
||||||
|
gizmos: Some(&mut gizmos),
|
||||||
};
|
};
|
||||||
state.update(&mut update_ctx);
|
state.update(&mut update_ctx);
|
||||||
}
|
}
|
||||||
@@ -150,7 +147,8 @@ pub fn state_interrupt_system (
|
|||||||
fighter_attribs: &attribs,
|
fighter_attribs: &attribs,
|
||||||
game_settings: &game_settings,
|
game_settings: &game_settings,
|
||||||
entity: ent,
|
entity: ent,
|
||||||
stage_collision: &stage_collision
|
stage_collision: &stage_collision,
|
||||||
|
gizmos: None,
|
||||||
};
|
};
|
||||||
if let Some(new_state) = state.check_interrupt(&state_context) {
|
if let Some(new_state) = state.check_interrupt(&state_context) {
|
||||||
let mut new_state = new_state;
|
let mut new_state = new_state;
|
||||||
@@ -171,6 +169,7 @@ pub fn state_collision_interrupt_system(
|
|||||||
stage_collision: Res<StageCollision>,
|
stage_collision: Res<StageCollision>,
|
||||||
game_settings: Res<GameSettings>,
|
game_settings: Res<GameSettings>,
|
||||||
mut commands: Commands,
|
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 {
|
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 {
|
let mut state_context = FighterStateContext {
|
||||||
@@ -183,7 +182,8 @@ pub fn state_collision_interrupt_system(
|
|||||||
fighter_attribs: &attribs,
|
fighter_attribs: &attribs,
|
||||||
game_settings: &game_settings,
|
game_settings: &game_settings,
|
||||||
entity: ent,
|
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) {
|
if let Some(new_state) = state.check_collision_interrupt(&mut state_context) {
|
||||||
let mut new_state = new_state;
|
let mut new_state = new_state;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use bevy::prelude::*;
|
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::FighterState;
|
||||||
use crate::fighter::state::fall::FallState;
|
use crate::fighter::state::fall::FallState;
|
||||||
use crate::fighter::state::ground::{GroundedMotionResult, GroundedStateCommon};
|
use crate::fighter::state::ground::{GroundedMotionResult, GroundedStateCommon};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use bevy::prelude::*;
|
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::FighterState;
|
||||||
use crate::fighter::state::fall::FallState;
|
use crate::fighter::state::fall::FallState;
|
||||||
use crate::fighter::state::ground::{GroundedMotionResult, GroundedStateCommon};
|
use crate::fighter::state::ground::{GroundedMotionResult, GroundedStateCommon};
|
||||||
@@ -33,7 +33,10 @@ impl FighterStateImpl for WaitState {
|
|||||||
} else {
|
} else {
|
||||||
state_context.fighter_attribs.ground_friction
|
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(
|
ground::apply_grounded_motion(
|
||||||
state_context.velocity,
|
state_context.velocity,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use bevy::prelude::*;
|
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::fall::FallState;
|
||||||
use crate::fighter::state::ground::{GroundedMotionResult, GroundedStateCommon};
|
use crate::fighter::state::ground::{GroundedMotionResult, GroundedStateCommon};
|
||||||
use crate::fighter::state::{self, FighterState};
|
use crate::fighter::state::{self, FighterState};
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
use crate::math::int::FGi32;
|
||||||
|
|
||||||
pub struct FighterSettingsCommon {
|
pub struct FighterSettingsCommon {
|
||||||
pub walk_speed_ease: f32,
|
pub walk_speed_ease: FGi32,
|
||||||
pub ground_max_horizontal_velocity: f32,
|
pub ground_max_horizontal_velocity: FGi32,
|
||||||
pub ground_friction_over_walk_speed_multiplier: f32
|
pub ground_friction_over_walk_speed_multiplier: FGi32
|
||||||
}
|
}
|
||||||
pub struct InputSettingsCommon {
|
pub struct InputSettingsCommon {
|
||||||
pub stick_deadzone: f32,
|
pub stick_deadzone: f32,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ struct FighterInputBufferedActions {}
|
|||||||
pub enum FighterCommands {
|
pub enum FighterCommands {
|
||||||
SmashMoveLeft,
|
SmashMoveLeft,
|
||||||
SmashMoveRight,
|
SmashMoveRight,
|
||||||
|
Jump,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Frames of life remaining for each buffered command. Indexed by
|
/// Frames of life remaining for each buffered command. Indexed by
|
||||||
@@ -25,6 +26,7 @@ pub enum FighterCommands {
|
|||||||
pub struct FighterCommandLifetimes {
|
pub struct FighterCommandLifetimes {
|
||||||
smash_move_left: u8,
|
smash_move_left: u8,
|
||||||
smash_move_right: u8,
|
smash_move_right: u8,
|
||||||
|
jump: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Index<FighterCommands> for FighterCommandLifetimes {
|
impl Index<FighterCommands> for FighterCommandLifetimes {
|
||||||
@@ -34,6 +36,7 @@ impl Index<FighterCommands> for FighterCommandLifetimes {
|
|||||||
match command {
|
match command {
|
||||||
FighterCommands::SmashMoveLeft => &self.smash_move_left,
|
FighterCommands::SmashMoveLeft => &self.smash_move_left,
|
||||||
FighterCommands::SmashMoveRight => &self.smash_move_right,
|
FighterCommands::SmashMoveRight => &self.smash_move_right,
|
||||||
|
FighterCommands::Jump => &self.jump,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,6 +46,7 @@ impl IndexMut<FighterCommands> for FighterCommandLifetimes {
|
|||||||
match command {
|
match command {
|
||||||
FighterCommands::SmashMoveLeft => &mut self.smash_move_left,
|
FighterCommands::SmashMoveLeft => &mut self.smash_move_left,
|
||||||
FighterCommands::SmashMoveRight => &mut self.smash_move_right,
|
FighterCommands::SmashMoveRight => &mut self.smash_move_right,
|
||||||
|
FighterCommands::Jump => &mut self.jump,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -1,10 +1,12 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::math::vec::FGVec2;
|
||||||
|
|
||||||
/// One frame of a player's raw input. This is the type GGRS serializes and
|
/// 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`.
|
/// sends over the wire, so keep it small and keep it `PartialEq`.
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
|
||||||
pub struct FighterInputFrame {
|
pub struct FighterInputFrame {
|
||||||
pub movement: Vec2,
|
pub movement: FGVec2,
|
||||||
pub directional_attack: Vec2,
|
pub directional_attack: FGVec2,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use bevy::prelude::*;
|
|||||||
use bevy_ggrs::{LocalInputs, LocalPlayers};
|
use bevy_ggrs::{LocalInputs, LocalPlayers};
|
||||||
|
|
||||||
use crate::input::{BaseInputMap, FighterInputFrame, InputActionState, InputMapAction};
|
use crate::input::{BaseInputMap, FighterInputFrame, InputActionState, InputMapAction};
|
||||||
|
use crate::math::int::FGi32;
|
||||||
use crate::netcode::GGRSCfg;
|
use crate::netcode::GGRSCfg;
|
||||||
|
|
||||||
pub fn preprocess_keyboard_input(
|
pub fn preprocess_keyboard_input(
|
||||||
@@ -32,16 +33,16 @@ pub fn preprocess_keyboard_input(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let action_strength = match action_state {
|
let action_strength = match action_state {
|
||||||
InputActionState::JustPressed | InputActionState::Pressed => 1.0f32,
|
InputActionState::JustPressed | InputActionState::Pressed => FGi32::ONE,
|
||||||
InputActionState::JustReleased | InputActionState::Released => 0.0f32,
|
InputActionState::JustReleased | InputActionState::Released => FGi32::ZERO,
|
||||||
};
|
};
|
||||||
|
|
||||||
match keyboard_elment.action {
|
match keyboard_elment.action {
|
||||||
InputMapAction::MovementXDir(sign) => {
|
InputMapAction::MovementXDir(sign) => {
|
||||||
input_frame.movement.x += (sign as f32) * action_strength
|
input_frame.movement.x += (sign) * action_strength
|
||||||
}
|
}
|
||||||
InputMapAction::MovementYDir(sign) => {
|
InputMapAction::MovementYDir(sign) => {
|
||||||
input_frame.movement.y += (sign as f32) * action_strength
|
input_frame.movement.y += (sign) * action_strength
|
||||||
}
|
}
|
||||||
InputMapAction::Jump => todo!(),
|
InputMapAction::Jump => todo!(),
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-18
@@ -29,7 +29,7 @@ use jackdaw_runtime::prelude::*;
|
|||||||
use crate::{
|
use crate::{
|
||||||
fighter::{
|
fighter::{
|
||||||
FighterAttributes, FighterECB, FighterTranslation, FighterVelocity, FighterVisual, animation::FighterAnimations, state::{FighterState, fall::FallState, wait::WaitState},
|
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;
|
mod args;
|
||||||
@@ -62,24 +62,24 @@ pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|||||||
Name("Fighter".into()),
|
Name("Fighter".into()),
|
||||||
Player { handle: 0 },
|
Player { handle: 0 },
|
||||||
FighterECB {
|
FighterECB {
|
||||||
vertical_half: 0.5,
|
vertical_half: FGi32::lit("0.5"),
|
||||||
horizontal_half: 0.25,
|
horizontal_half: FGi32::lit("0.25"),
|
||||||
},
|
},
|
||||||
FighterVelocity::default(),
|
FighterVelocity::default(),
|
||||||
FighterTranslation(Vec2::new(0.5, 1.25)),
|
FighterTranslation(FGVec2::lit("0.5", "1.25")),
|
||||||
FighterAttributes {
|
FighterAttributes {
|
||||||
base_walk_accel: 0.01,
|
base_walk_accel: FGi32::lit("0.01"),
|
||||||
stick_walk_accel: 0.02,
|
stick_walk_accel: FGi32::lit("0.02"),
|
||||||
max_walk_vel: 0.16,
|
max_walk_vel: FGi32::lit("0.16"),
|
||||||
ground_friction: 0.008,
|
ground_friction: FGi32::lit("0.008"),
|
||||||
dash_duration: 15,
|
dash_duration: 15,
|
||||||
base_dash_accel: 0.002,
|
base_dash_accel: FGi32::lit("0.002"),
|
||||||
stick_dash_accel: 0.01,
|
stick_dash_accel: FGi32::lit("0.01"),
|
||||||
max_dash_vel: 0.22,
|
max_dash_vel: FGi32::lit("0.22"),
|
||||||
jump_vertical_velocity: 0.25,
|
jump_vertical_velocity: FGi32::lit("0.25"),
|
||||||
dash_initial_velocity: 0.19,
|
dash_initial_velocity: FGi32::lit("0.19"),
|
||||||
terminal_velocity: 0.28,
|
terminal_velocity: FGi32::lit("0.28"),
|
||||||
gravity: 0.023,
|
gravity: FGi32::lit("0.023"),
|
||||||
},
|
},
|
||||||
FighterState::Fall(FallState),
|
FighterState::Fall(FallState),
|
||||||
FighterInput::default(),
|
FighterInput::default(),
|
||||||
@@ -115,9 +115,9 @@ impl Plugin for GamePlugin {
|
|||||||
.insert_resource(args::Args::parse())
|
.insert_resource(args::Args::parse())
|
||||||
.insert_resource(GameSettings {
|
.insert_resource(GameSettings {
|
||||||
figher_common: FighterSettingsCommon {
|
figher_common: FighterSettingsCommon {
|
||||||
walk_speed_ease: 0.5,
|
walk_speed_ease: FGi32::lit("0.5"),
|
||||||
ground_max_horizontal_velocity: 0.3,
|
ground_max_horizontal_velocity: FGi32::lit("0.3"),
|
||||||
ground_friction_over_walk_speed_multiplier: 2.0,
|
ground_friction_over_walk_speed_multiplier: FGi32::lit("2.0"),
|
||||||
},
|
},
|
||||||
input_common: InputSettingsCommon::default(),
|
input_common: InputSettingsCommon::default(),
|
||||||
});
|
});
|
||||||
|
|||||||
+78
-33
@@ -1,12 +1,13 @@
|
|||||||
use bevy::math::{Vec2, primitives::Segment2d};
|
use bevy::math::{Vec2, primitives::Segment2d};
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
use fixed::prelude::*;
|
use fixed::prelude::*;
|
||||||
|
|
||||||
use crate::math::vec::FGVec2;
|
use crate::math::vec::FGVec2;
|
||||||
|
|
||||||
type FGi32 = fixed::FixedI32<fixed::types::extra::U16>;
|
type FGi32 = fixed::FixedI32<fixed::types::extra::U16>;
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
#[derive(Clone, Copy, PartialEq, Reflect)]
|
||||||
pub struct FGSegment2d {
|
pub struct FGSegment2d {
|
||||||
point1: FGVec2,
|
point1: FGVec2,
|
||||||
point2: FGVec2
|
point2: FGVec2
|
||||||
@@ -29,14 +30,72 @@ impl FGSegment2d {
|
|||||||
pub fn point2(&self) -> FGVec2 {
|
pub fn point2(&self) -> FGVec2 {
|
||||||
self.point2
|
self.point2
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub trait SegmentIntersection {
|
/// Compute the normalized direction pointing from the first endpoint to the second endpoint.
|
||||||
fn segment_intersection(&self, rhs: &Segment2d) -> Option<FGVec2>;
|
///
|
||||||
}
|
/// 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 {
|
/// Try to compute the normalized direction pointing from the first endpoint to the second endpoint.
|
||||||
fn segment_intersection(&self, rhs: &Segment2d) -> Option<Vec2> {
|
///
|
||||||
|
/// 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<FGVec2> {
|
||||||
|
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<FGVec2> {
|
||||||
let p0 = self.point1();
|
let p0 = self.point1();
|
||||||
let p1 = self.point2();
|
let p1 = self.point2();
|
||||||
let p2 = rhs.point1();
|
let p2 = rhs.point1();
|
||||||
@@ -48,45 +107,31 @@ impl SegmentIntersection for Segment2d {
|
|||||||
let s2_y = p3.y - p2.y;
|
let s2_y = p3.y - p2.y;
|
||||||
|
|
||||||
let denom = -s2_x * s1_y + s1_x * s2_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
|
return None; // Parallel or collinear
|
||||||
}
|
}
|
||||||
|
|
||||||
let s = (-s1_y * (p0.x - p2.x) + s1_x * (p0.y - p2.y)) / denom;
|
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;
|
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 {
|
if s >= FGi32::ZERO && s <= FGi32::ONE && t >= FGi32::ZERO && t <= FGi32::ONE {
|
||||||
Some(Vec2::new(p0.x + (t * s1_x), p0.y + (t * s1_y)))
|
Some(FGVec2::new(p0.x + (t * s1_x), p0.y + (t * s1_y)))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SegmentIntersection for Segment2d {
|
#[cfg(test)]
|
||||||
fn segment_intersection(&self, rhs: &Segment2d) -> Option<Vec2> {
|
mod tests {
|
||||||
let p0 = self.point1();
|
use super::*;
|
||||||
let p1 = self.point2();
|
|
||||||
let p2 = rhs.point1();
|
|
||||||
let p3 = rhs.point2();
|
|
||||||
|
|
||||||
let s1_x = p1.x - p0.x;
|
#[test]
|
||||||
let s1_y = p1.y - p0.y;
|
fn closest_point_projects_and_clamps_to_segment() {
|
||||||
let s2_x = p3.x - p2.x;
|
let segment = FGSegment2d::new(FGVec2::lit("-4", "0"), FGVec2::lit("4", "0"));
|
||||||
let s2_y = p3.y - p2.y;
|
|
||||||
|
|
||||||
let denom = -s2_x * s1_y + s1_x * s2_y;
|
assert_eq!(segment.closest_point(FGVec2::lit("2", "1")), FGVec2::lit("2", "0"));
|
||||||
if denom.abs() < f32::EPSILON {
|
assert_eq!(segment.closest_point(FGVec2::lit("-6", "1")), segment.point1());
|
||||||
return None; // Parallel or collinear
|
assert_eq!(segment.closest_point(FGVec2::lit("6", "1")), segment.point2());
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+206
-8
@@ -1,14 +1,24 @@
|
|||||||
use bevy::math::Vec2;
|
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;
|
use super::int::FGi32;
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
#[derive(Clone, Copy, PartialEq, Default, Debug, Reflect, Serialize, Deserialize)]
|
||||||
|
#[reflect(opaque)]
|
||||||
pub struct FGVec2 {
|
pub struct FGVec2 {
|
||||||
pub x: FGi32,
|
pub x: FGi32,
|
||||||
pub y: 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 {
|
impl FGVec2 {
|
||||||
pub const ZERO: Self = Self::new(FGi32::ZERO, FGi32::ZERO);
|
pub const ZERO: Self = Self::new(FGi32::ZERO, FGi32::ZERO);
|
||||||
|
|
||||||
@@ -17,6 +27,11 @@ impl FGVec2 {
|
|||||||
Self { x, y }
|
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)]
|
#[inline(always)]
|
||||||
pub fn from_i32(x: i32, y: i32) -> Self {
|
pub fn from_i32(x: i32, y: i32) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -31,17 +46,78 @@ impl FGVec2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn length(&self) -> FGi32 {
|
pub fn length(self) -> FGi32 {
|
||||||
(self.x * self.x + self.y * self.y).sqrt()
|
(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]
|
#[inline]
|
||||||
pub fn normalized_or_zero(&self) -> Self {
|
#[must_use]
|
||||||
let len = self.length();
|
pub fn project_onto_normalized(self, rhs: Self) -> Self {
|
||||||
if len.is_zero() {
|
assert!(rhs.is_normalized());
|
||||||
Self::ZERO
|
|
||||||
|
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<Self> {
|
||||||
|
(to - self).normalize()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn distance(self, to: Self) -> FGi32 {
|
||||||
|
(to - self).length()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn normalize(&self) -> Option<Self> {
|
||||||
|
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 {
|
} 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 {
|
impl Div for FGVec2 {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
@@ -113,3 +197,117 @@ impl Div<FGi32> for &FGVec2 {
|
|||||||
(*self).div(rhs)
|
(*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::<f64>();
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+3
-3
@@ -5,11 +5,11 @@ use crate::stage::{StagePoly, line::StageCollision};
|
|||||||
pub fn debug_draw_scene(mut gizmos: Gizmos, stage_collision: Res<StageCollision>) {
|
pub fn debug_draw_scene(mut gizmos: Gizmos, stage_collision: Res<StageCollision>) {
|
||||||
for poly in stage_collision.stage_polys.iter() {
|
for poly in stage_collision.stage_polys.iter() {
|
||||||
for segment in poly.segments.iter() {
|
for segment in poly.segments.iter() {
|
||||||
let from = segment.segment.point1();
|
let from = segment.segment.point1().to_vec2();
|
||||||
let to = segment.segment.point2();
|
let to = segment.segment.point2().to_vec2();
|
||||||
let from_3d = Vec3::new(from.x, from.y, 0.0);
|
let from_3d = Vec3::new(from.x, from.y, 0.0);
|
||||||
let end_3d = Vec3::new(to.x, to.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);
|
gizmos.line(from_3d, end_3d, RED);
|
||||||
let midpoint = (from_3d + end_3d) * 0.5;
|
let midpoint = (from_3d + end_3d) * 0.5;
|
||||||
gizmos.arrow(midpoint, midpoint + normal_3d, RED);
|
gizmos.arrow(midpoint, midpoint + normal_3d, RED);
|
||||||
|
|||||||
+140
-36
@@ -4,7 +4,7 @@ use std::default;
|
|||||||
|
|
||||||
use bevy::{math::InvalidDirectionError, prelude::*};
|
use bevy::{math::InvalidDirectionError, prelude::*};
|
||||||
|
|
||||||
use crate::math::segment::SegmentIntersection;
|
use crate::math::{segment::{FGSegment2d}, vec::FGVec2};
|
||||||
|
|
||||||
#[derive(Reflect, Clone, Copy, PartialEq)]
|
#[derive(Reflect, Clone, Copy, PartialEq)]
|
||||||
pub enum StagePolyLineSegmentType {
|
pub enum StagePolyLineSegmentType {
|
||||||
@@ -14,8 +14,8 @@ pub enum StagePolyLineSegmentType {
|
|||||||
|
|
||||||
#[derive(Reflect, Clone)]
|
#[derive(Reflect, Clone)]
|
||||||
pub struct StagePolyLineSegment {
|
pub struct StagePolyLineSegment {
|
||||||
pub segment: Segment2d,
|
pub segment: FGSegment2d,
|
||||||
pub normal: Vec2,
|
pub normal: FGVec2,
|
||||||
pub segment_type: StagePolyLineSegmentType
|
pub segment_type: StagePolyLineSegmentType
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,9 +40,9 @@ pub struct StagePoly {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct StagePlaneIntersectResult {
|
pub struct StagePlaneIntersectResult {
|
||||||
pub position: Vec2,
|
pub position: FGVec2,
|
||||||
pub segment_idx: usize,
|
pub segment_idx: usize,
|
||||||
pub normal: Vec2
|
pub normal: FGVec2
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Resource, Reflect, Clone, Default)]
|
#[derive(Resource, Reflect, Clone, Default)]
|
||||||
@@ -52,18 +52,30 @@ pub struct StageCollision {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl StagePoly {
|
impl StagePoly {
|
||||||
pub fn intersect_ray(&self, ray_segment: Segment2d) -> Option<StagePlaneIntersectResult> {
|
pub fn intersect_ray(&self, ray_segment: FGSegment2d) -> Option<StagePlaneIntersectResult> {
|
||||||
for (i, segment) in self.segments.iter().enumerate() {
|
let ray_start = ray_segment.point1();
|
||||||
if let Some(intersection) = segment.segment.segment_intersection(&ray_segment) {
|
let (segment_idx, segment, position) = self
|
||||||
println!("DIST3 {} {}", intersection, segment.segment.closest_point(intersection));
|
.segments
|
||||||
return Some(StagePlaneIntersectResult {
|
.iter()
|
||||||
position: intersection,
|
.enumerate()
|
||||||
segment_idx: i,
|
.filter_map(|(segment_idx, segment)| {
|
||||||
normal: segment.normal
|
segment
|
||||||
});
|
.segment
|
||||||
}
|
.segment_intersection(&ray_segment)
|
||||||
}
|
.map(|position| (segment_idx, segment, position))
|
||||||
None
|
})
|
||||||
|
.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> {
|
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> {
|
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> {
|
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 {
|
pub fn get_next_segment_index(&self, segment_idx: usize) -> Option<usize> {
|
||||||
let windows = points.iter().zip(points.iter().cycle().skip(1));
|
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<usize> {
|
||||||
|
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 segments = windows.map(|segment_window| {
|
||||||
let (first_point, segment_type) = segment_window.0;
|
let (first_point, segment_type) = segment_window.0;
|
||||||
let (second_point, _) = segment_window.1;
|
let (second_point, _) = segment_window.1;
|
||||||
let segment = Segment2d::new(*first_point, *second_point);
|
let segment = FGSegment2d::new(*first_point, *second_point);
|
||||||
StagePolyLineSegment {
|
StagePolyLineSegment {
|
||||||
segment_type: *segment_type,
|
segment_type: *segment_type,
|
||||||
segment,
|
segment,
|
||||||
normal: segment.left_normal().as_vec2()
|
normal: segment.left_normal()
|
||||||
}
|
}
|
||||||
}).collect();
|
}).collect();
|
||||||
|
|
||||||
@@ -100,46 +153,97 @@ impl StagePoly {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl StagePolyLineSegment {
|
impl StagePolyLineSegment {
|
||||||
pub fn project(&self, vec: Vec2) -> Vec2 {
|
pub fn project(&self, vec: FGVec2) -> FGVec2 {
|
||||||
vec - vec.project_onto_normalized(self.normal)
|
vec - vec.project_onto_normalized(self.normal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use crate::math::int::FGi32;
|
||||||
|
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn ray_intersection_test() {
|
fn ray_intersection_test() {
|
||||||
let stage_poly = StagePoly::build(crate::stage::line::StagePolyType::Platform, &[
|
let stage_poly = StagePoly::build(crate::stage::line::StagePolyType::Platform, &[
|
||||||
(Vec2::new(-4.5, 0.0), StagePolyLineSegmentType::Floor),
|
(FGVec2::lit("-4.5", "0.0"), StagePolyLineSegmentType::Floor),
|
||||||
(Vec2::new(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!(res.is_some());
|
||||||
assert_eq!(res.unwrap().position, Vec2::ZERO);
|
assert_eq!(res.unwrap().position, FGVec2::ZERO);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ray_intersection_test_side() {
|
fn ray_intersection_test_side() {
|
||||||
let stage_poly = StagePoly::build(crate::stage::line::StagePolyType::Platform, &[
|
let stage_poly = StagePoly::build(crate::stage::line::StagePolyType::Platform, &[
|
||||||
(Vec2::new(-4.5, 0.0), StagePolyLineSegmentType::Floor),
|
(FGVec2::lit("-4.5", "0.0"), StagePolyLineSegmentType::Floor),
|
||||||
(Vec2::new(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!(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]
|
#[test]
|
||||||
fn ray_intersection_test_diagonal() {
|
fn ray_intersection_test_diagonal() {
|
||||||
let stage_poly = StagePoly::build(crate::stage::line::StagePolyType::Platform, &[
|
let stage_poly = StagePoly::build(crate::stage::line::StagePolyType::Platform, &[
|
||||||
(Vec2::new(0.0, 0.0), StagePolyLineSegmentType::Floor),
|
(FGVec2::lit("0.0", "0.0"), StagePolyLineSegmentType::Floor),
|
||||||
(Vec2::new(2.0, 2.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!(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"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+8
-8
@@ -4,19 +4,19 @@ use std::f32::consts::PI;
|
|||||||
|
|
||||||
use bevy::{camera_controller::free_camera::FreeCamera, prelude::*};
|
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
|
/// set up a simple 3D scene
|
||||||
pub fn test_scene() -> impl SceneList {
|
pub fn test_scene() -> impl SceneList {
|
||||||
|
|
||||||
let stage_poly = StagePoly::build(crate::stage::line::StagePolyType::Closed, &[
|
let stage_poly = StagePoly::build(crate::stage::line::StagePolyType::Closed, &[
|
||||||
(Vec2::new(-5.6, -0.35), StagePolyLineSegmentType::Floor),
|
(FGVec2::lit("-5.6", "-0.35"), StagePolyLineSegmentType::Floor),
|
||||||
(Vec2::new(-3.92, 0.0), StagePolyLineSegmentType::Floor),
|
(FGVec2::lit("-3.92", "0.0"), StagePolyLineSegmentType::Floor),
|
||||||
(Vec2::new(0.0, 0.0), StagePolyLineSegmentType::Floor),
|
(FGVec2::lit("0.0", "0.0"), StagePolyLineSegmentType::Floor),
|
||||||
(Vec2::new(3.92, 0.0), StagePolyLineSegmentType::Floor),
|
(FGVec2::lit("3.92", "0.0"), StagePolyLineSegmentType::Floor),
|
||||||
(Vec2::new(5.6, -0.35), StagePolyLineSegmentType::Wall),
|
(FGVec2::lit("5.6", "-0.35"), StagePolyLineSegmentType::Wall),
|
||||||
(Vec2::new(5.6, -20.0), StagePolyLineSegmentType::Ceiling),
|
(FGVec2::lit("5.6", "-20.0"), StagePolyLineSegmentType::Ceiling),
|
||||||
(Vec2::new(-5.6, -20.0), StagePolyLineSegmentType::Wall)
|
(FGVec2::lit("-5.6", "-20.0"), StagePolyLineSegmentType::Wall)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
bsn_list! [
|
bsn_list! [
|
||||||
|
|||||||
Reference in New Issue
Block a user