Make dashback configurable, fix movement bugs, add double jump forward and back.
This commit is contained in:
+218
-39
@@ -9,9 +9,9 @@ use crate::fighter::{
|
||||
FighterAttributes, FighterPreviousTranslation, FighterTranslation, FighterVelocity,
|
||||
};
|
||||
use crate::game_settings::GameSettings;
|
||||
use crate::math::int::FGi32;
|
||||
use crate::math::int::{FGWide, FGi32};
|
||||
use crate::math::vec::FGVec2;
|
||||
use crate::stage::line::{StageLineID, StagePolyLineSegmentType};
|
||||
use crate::stage::line::{StageLineID, StagePoly, StagePolyLineSegment, StagePolyLineSegmentType};
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
@@ -74,6 +74,85 @@ pub enum GroundedMotionResult {
|
||||
InAir,
|
||||
}
|
||||
|
||||
enum GroundedPositionResult {
|
||||
InGround {
|
||||
point: FGVec2,
|
||||
segment_idx: usize,
|
||||
blocked: bool,
|
||||
},
|
||||
InAir,
|
||||
}
|
||||
|
||||
fn is_walkable_floor(segment: &StagePolyLineSegment) -> bool {
|
||||
segment.segment_type == StagePolyLineSegmentType::Floor && segment.normal.y > FGi32::ZERO
|
||||
}
|
||||
|
||||
fn resolve_grounded_position(
|
||||
stage_poly: &StagePoly,
|
||||
starting_segment_idx: usize,
|
||||
point: FGVec2,
|
||||
can_walk_off: bool,
|
||||
) -> GroundedPositionResult {
|
||||
let mut segment_idx = starting_segment_idx;
|
||||
|
||||
// A single frame can cross several short connected segments. The bound also
|
||||
// prevents malformed closed polygons from cycling forever.
|
||||
for _ in 0..stage_poly.segments.len() {
|
||||
let Some(segment) = stage_poly.get_segment(segment_idx) else {
|
||||
return GroundedPositionResult::InAir;
|
||||
};
|
||||
if !is_walkable_floor(segment) {
|
||||
return GroundedPositionResult::InAir;
|
||||
}
|
||||
|
||||
let segment_vector = segment.segment.point2() - segment.segment.point1();
|
||||
let projection = segment_vector.dot(point - segment.segment.point1());
|
||||
let length_squared = segment_vector.length_squared();
|
||||
|
||||
let (next_segment_idx, edge_point) = if projection < FGWide::ZERO {
|
||||
(
|
||||
stage_poly.get_prev_segment_index(segment_idx),
|
||||
segment.segment.point1(),
|
||||
)
|
||||
} else if projection > length_squared {
|
||||
(
|
||||
stage_poly.get_next_segment_index(segment_idx),
|
||||
segment.segment.point2(),
|
||||
)
|
||||
} else {
|
||||
let projected_point = segment.segment.point1()
|
||||
+ (point - segment.segment.point1()).project_onto(segment.segment.direction());
|
||||
return GroundedPositionResult::InGround {
|
||||
point: projected_point,
|
||||
segment_idx,
|
||||
blocked: false,
|
||||
};
|
||||
};
|
||||
|
||||
if let Some(next_segment_idx) = next_segment_idx
|
||||
&& next_segment_idx != segment_idx
|
||||
&& stage_poly
|
||||
.get_segment(next_segment_idx)
|
||||
.is_some_and(is_walkable_floor)
|
||||
{
|
||||
segment_idx = next_segment_idx;
|
||||
continue;
|
||||
}
|
||||
|
||||
return if can_walk_off {
|
||||
GroundedPositionResult::InAir
|
||||
} else {
|
||||
GroundedPositionResult::InGround {
|
||||
point: edge_point,
|
||||
segment_idx,
|
||||
blocked: true,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
GroundedPositionResult::InAir
|
||||
}
|
||||
|
||||
pub fn apply_grounded_motion(
|
||||
velocity: &FighterVelocity,
|
||||
translation: &mut FighterTranslation,
|
||||
@@ -87,7 +166,7 @@ pub fn apply_grounded_motion(
|
||||
pub fn collide_with_stage_grounded(
|
||||
state_context: &mut FighterStateContext,
|
||||
ground_common: &mut GroundedStateCommon,
|
||||
_can_walk_off: bool,
|
||||
can_walk_off: bool,
|
||||
) -> GroundedMotionResult {
|
||||
let translation = &mut state_context.translation;
|
||||
let ecb = &state_context.ecb;
|
||||
@@ -99,49 +178,149 @@ pub fn collide_with_stage_grounded(
|
||||
|
||||
if let Some(stage_poly) = stage_poly {
|
||||
let ecb_bottom_pos = translation.0 + ecb.get_bottom_point();
|
||||
let current_segment_idx = ground_common.current_line_id.segment;
|
||||
|
||||
// First, we check the current segment, if that fails, we check the surrounding ones
|
||||
let grounded_ecb_bottom_pos = [
|
||||
Some(current_segment_idx),
|
||||
stage_poly.get_prev_segment_index(current_segment_idx),
|
||||
stage_poly.get_next_segment_index(current_segment_idx),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.find_map(|segment_idx| {
|
||||
let segment = stage_poly.get_segment(segment_idx)?;
|
||||
if segment.segment_type != StagePolyLineSegmentType::Floor {
|
||||
return None;
|
||||
match resolve_grounded_position(
|
||||
stage_poly,
|
||||
ground_common.current_line_id.segment,
|
||||
ecb_bottom_pos,
|
||||
can_walk_off,
|
||||
) {
|
||||
GroundedPositionResult::InGround {
|
||||
point,
|
||||
segment_idx,
|
||||
blocked,
|
||||
} => {
|
||||
let current_segment = stage_poly.get_segment(segment_idx).unwrap();
|
||||
translation.0 = point - ecb.get_bottom_point();
|
||||
state_context.velocity.0 = if blocked {
|
||||
FGVec2::ZERO
|
||||
} else {
|
||||
state_context.velocity.0
|
||||
- state_context
|
||||
.velocity
|
||||
.0
|
||||
.project_onto_normalized(current_segment.normal)
|
||||
};
|
||||
ground_common.current_line_id.segment = segment_idx;
|
||||
GroundedMotionResult::InGround
|
||||
}
|
||||
|
||||
let line_dir = segment.segment.direction();
|
||||
let projected_point = segment.segment.point1()
|
||||
+ (ecb_bottom_pos - segment.segment.point1()).project_onto(line_dir);
|
||||
let closest_point = segment.segment.closest_point(ecb_bottom_pos);
|
||||
let distance = closest_point.distance(projected_point);
|
||||
|
||||
if distance < 0.01 {
|
||||
return Some((projected_point, segment_idx, segment.normal));
|
||||
}
|
||||
|
||||
None
|
||||
});
|
||||
|
||||
if let Some((bp, new_segment_idx, normal)) = grounded_ecb_bottom_pos {
|
||||
translation.0 = bp - ecb.get_bottom_point();
|
||||
state_context.velocity.0 =
|
||||
state_context.velocity.0 - state_context.velocity.0.project_onto_normalized(normal);
|
||||
ground_common.current_line_id.segment = new_segment_idx;
|
||||
GroundedMotionResult::InGround
|
||||
} else {
|
||||
GroundedMotionResult::InAir
|
||||
GroundedPositionResult::InAir => GroundedMotionResult::InAir,
|
||||
}
|
||||
} else {
|
||||
GroundedMotionResult::InAir
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::stage::line::StagePolyType;
|
||||
|
||||
fn two_segment_floor() -> StagePoly {
|
||||
StagePoly::build(
|
||||
StagePolyType::Platform,
|
||||
&[
|
||||
(FGVec2::lit("0", "0"), StagePolyLineSegmentType::Floor),
|
||||
(FGVec2::lit("10", "0"), StagePolyLineSegmentType::Floor),
|
||||
(FGVec2::lit("20", "0"), StagePolyLineSegmentType::Floor),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiny_endpoint_crossing_moves_to_adjacent_floor() {
|
||||
let stage = two_segment_floor();
|
||||
let point = FGVec2::new(FGi32::from_bits(10 * 65_536 + 1), FGi32::ZERO);
|
||||
|
||||
let GroundedPositionResult::InGround {
|
||||
point: resolved,
|
||||
segment_idx,
|
||||
blocked,
|
||||
} = resolve_grounded_position(&stage, 0, point, false)
|
||||
else {
|
||||
panic!("connected floor should remain grounded");
|
||||
};
|
||||
|
||||
assert_eq!(segment_idx, 1);
|
||||
assert_eq!(resolved, point);
|
||||
assert!(!blocked);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocked_edge_keeps_floor_and_can_move_inward() {
|
||||
let stage = StagePoly::build(
|
||||
StagePolyType::Platform,
|
||||
&[
|
||||
(FGVec2::lit("0", "0"), StagePolyLineSegmentType::Floor),
|
||||
(FGVec2::lit("10", "0"), StagePolyLineSegmentType::Wall),
|
||||
(FGVec2::lit("10", "-2"), StagePolyLineSegmentType::Ceiling),
|
||||
],
|
||||
);
|
||||
let just_past_edge = FGVec2::new(FGi32::from_bits(10 * 65_536 + 1), FGi32::ZERO);
|
||||
|
||||
let GroundedPositionResult::InGround {
|
||||
point,
|
||||
segment_idx,
|
||||
blocked,
|
||||
} = resolve_grounded_position(&stage, 0, just_past_edge, false)
|
||||
else {
|
||||
panic!("non-walk-off movement should stop at the edge");
|
||||
};
|
||||
assert_eq!(point, FGVec2::lit("10", "0"));
|
||||
assert_eq!(segment_idx, 0);
|
||||
assert!(blocked);
|
||||
|
||||
let GroundedPositionResult::InGround {
|
||||
point,
|
||||
segment_idx,
|
||||
blocked,
|
||||
} = resolve_grounded_position(
|
||||
&stage,
|
||||
segment_idx,
|
||||
FGVec2::new(FGi32::from_bits(10 * 65_536 - 1), FGi32::ZERO),
|
||||
false,
|
||||
)
|
||||
else {
|
||||
panic!("fighter should be able to move back from the edge");
|
||||
};
|
||||
assert!(point.x < FGi32::lit("10"));
|
||||
assert_eq!(segment_idx, 0);
|
||||
assert!(!blocked);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn walk_off_edge_enters_air() {
|
||||
let stage = StagePoly::build(
|
||||
StagePolyType::Platform,
|
||||
&[
|
||||
(FGVec2::lit("0", "0"), StagePolyLineSegmentType::Floor),
|
||||
(FGVec2::lit("10", "0"), StagePolyLineSegmentType::Floor),
|
||||
],
|
||||
);
|
||||
let just_past_edge = FGVec2::new(FGi32::from_bits(10 * 65_536 + 1), FGi32::ZERO);
|
||||
|
||||
assert!(matches!(
|
||||
resolve_grounded_position(&stage, 0, just_past_edge, true),
|
||||
GroundedPositionResult::InAir
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vertical_segment_cannot_hold_grounded_fighter() {
|
||||
let stage = StagePoly::build(
|
||||
StagePolyType::Platform,
|
||||
&[
|
||||
(FGVec2::lit("10", "0"), StagePolyLineSegmentType::Floor),
|
||||
(FGVec2::lit("10", "-2"), StagePolyLineSegmentType::Floor),
|
||||
],
|
||||
);
|
||||
|
||||
assert!(matches!(
|
||||
resolve_grounded_position(&stage, 0, FGVec2::lit("10", "0"), false),
|
||||
GroundedPositionResult::InAir
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn grounded_movement_common_interrupts(
|
||||
state_context: &FighterStateContext,
|
||||
ground_common: &GroundedStateCommon,
|
||||
|
||||
Reference in New Issue
Block a user