Implement land from below, fix attacks not lifting fighters off the ground (#96)
Reviewed-on: #96 Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
This commit was merged in pull request #96.
This commit is contained in:
+216
-12
@@ -48,6 +48,14 @@ pub struct AirCollisionWithStageResult {
|
||||
pub hit_normal: FGVec2,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct AirCollisionOptions {
|
||||
/// Allow downward input to disable collision with pass-through platforms.
|
||||
pub can_pass_through_soft_platforms: bool,
|
||||
/// Treat an upward crossing of a pass-through platform by the ECB bottom as a landing.
|
||||
pub land_on_soft_platforms_from_below: bool,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn air_collide_with_stage(
|
||||
prev_translation: &FighterPreviousTranslation,
|
||||
@@ -56,7 +64,7 @@ fn air_collide_with_stage(
|
||||
ecb: &FighterECB,
|
||||
stage_collision: &StageCollision,
|
||||
ignored_polys: &[usize],
|
||||
can_ignore_soft_platforms: bool,
|
||||
options: AirCollisionOptions,
|
||||
) -> Option<AirCollisionWithStageResult> {
|
||||
let mut translation = *translation;
|
||||
let mut velocity = FighterVelocity::default();
|
||||
@@ -73,7 +81,7 @@ fn air_collide_with_stage(
|
||||
&FighterDesiredECB::from(*ecb),
|
||||
stage_collision,
|
||||
ignored_polys,
|
||||
can_ignore_soft_platforms,
|
||||
options,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -101,7 +109,7 @@ pub fn air_collide_with_stage_substepped(
|
||||
desired: &FighterDesiredECB,
|
||||
stage_collision: &StageCollision,
|
||||
ignored_polys: &[usize],
|
||||
can_ignore_soft_platforms: bool,
|
||||
options: AirCollisionOptions,
|
||||
) -> Option<AirCollisionWithStageResult> {
|
||||
let requested_translation = translation.0;
|
||||
let movement = requested_translation - prev_translation.0;
|
||||
@@ -151,7 +159,7 @@ pub fn air_collide_with_stage_substepped(
|
||||
&lower_edges,
|
||||
StagePolyLineSegmentType::Floor,
|
||||
ignored_polys,
|
||||
can_ignore_soft_platforms,
|
||||
options.can_pass_through_soft_platforms,
|
||||
) {
|
||||
*ecb = current_ecb;
|
||||
translation.0 = current_position + result.correction;
|
||||
@@ -193,6 +201,26 @@ pub fn air_collide_with_stage_substepped(
|
||||
velocity.0 -= velocity.0.project_onto_normalized(contact.hit_normal);
|
||||
}
|
||||
|
||||
if options.land_on_soft_platforms_from_below
|
||||
&& let Some(result) = sweep_bottom_point_through_platforms(
|
||||
stage_collision,
|
||||
previous_position + previous_ecb.bottom,
|
||||
current_position + current_ecb.bottom,
|
||||
ignored_polys,
|
||||
)
|
||||
{
|
||||
*ecb = current_ecb;
|
||||
translation.0 = current_position + result.correction;
|
||||
velocity.0 -= velocity.0.project_onto_normalized(result.hit_normal);
|
||||
let bottom_contact = translation.0 + current_ecb.bottom;
|
||||
return Some(AirCollisionWithStageResult {
|
||||
line_id: result.line_id,
|
||||
hit_position: bottom_contact,
|
||||
resolved_translation: translation.0,
|
||||
hit_normal: result.hit_normal,
|
||||
});
|
||||
}
|
||||
|
||||
let side_edges = [
|
||||
(
|
||||
previous_ecb.bottom,
|
||||
@@ -245,6 +273,59 @@ struct PointSweepResult {
|
||||
correction: FGVec2,
|
||||
}
|
||||
|
||||
fn sweep_bottom_point_through_platforms(
|
||||
stage_collision: &StageCollision,
|
||||
previous_bottom: FGVec2,
|
||||
current_bottom: FGVec2,
|
||||
ignored_polys: &[usize],
|
||||
) -> Option<PointSweepResult> {
|
||||
let bottom_sweep = FGSegment2d::new(previous_bottom, current_bottom);
|
||||
|
||||
stage_collision
|
||||
.stage_polys
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(poly_idx, poly)| {
|
||||
!ignored_polys.contains(poly_idx) && matches!(poly.poly_type, StagePolyType::Platform)
|
||||
})
|
||||
.flat_map(|(poly_idx, poly)| {
|
||||
poly.segments
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, segment)| {
|
||||
matches!(segment.segment_type, StagePolyLineSegmentType::Floor)
|
||||
})
|
||||
.filter_map(move |(segment_idx, segment)| {
|
||||
let previous_distance = segment
|
||||
.normal
|
||||
.dot(previous_bottom - segment.segment.point1());
|
||||
let current_distance = segment
|
||||
.normal
|
||||
.dot(current_bottom - segment.segment.point1());
|
||||
if previous_distance >= crate::math::int::FGWide::ZERO
|
||||
|| current_distance < crate::math::int::FGWide::ZERO
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let hit_position = segment.segment.segment_intersection(&bottom_sweep)?;
|
||||
Some((
|
||||
PointSweepResult {
|
||||
line_id: StageLineID {
|
||||
polygon: poly_idx,
|
||||
segment: segment_idx,
|
||||
},
|
||||
hit_normal: segment.normal,
|
||||
correction: hit_position - current_bottom,
|
||||
},
|
||||
hit_position.distance_squared(previous_bottom),
|
||||
))
|
||||
})
|
||||
})
|
||||
.min_by_key(|(_, distance)| *distance)
|
||||
.map(|(result, _)| result)
|
||||
}
|
||||
|
||||
fn sweep_edges(
|
||||
stage_collision: &StageCollision,
|
||||
previous_position: FGVec2,
|
||||
@@ -442,7 +523,7 @@ mod tests {
|
||||
&desired,
|
||||
&stage_collision,
|
||||
&[],
|
||||
false,
|
||||
Default::default(),
|
||||
)
|
||||
.expect("the floor should be hit");
|
||||
|
||||
@@ -482,7 +563,7 @@ mod tests {
|
||||
&desired,
|
||||
&stage_collision,
|
||||
&[],
|
||||
false,
|
||||
Default::default(),
|
||||
)
|
||||
.is_none());
|
||||
assert_eq!(translation.0, FGVec2::lit("3", "0"));
|
||||
@@ -518,7 +599,7 @@ mod tests {
|
||||
&desired,
|
||||
&stage_collision,
|
||||
&[],
|
||||
false,
|
||||
Default::default(),
|
||||
)
|
||||
.is_none());
|
||||
assert_eq!(translation.0.x, FGi32::lit("3"));
|
||||
@@ -554,7 +635,7 @@ mod tests {
|
||||
&desired,
|
||||
&stage_collision,
|
||||
&[],
|
||||
false,
|
||||
Default::default(),
|
||||
)
|
||||
.is_none());
|
||||
assert_eq!(translation.0, FGVec2::lit("0", "1"));
|
||||
@@ -589,7 +670,7 @@ mod tests {
|
||||
&ecb,
|
||||
&stage_collision,
|
||||
&[1],
|
||||
false,
|
||||
Default::default(),
|
||||
)
|
||||
.expect("the non-ignored platform should still be collidable");
|
||||
|
||||
@@ -626,7 +707,10 @@ mod tests {
|
||||
&ecb,
|
||||
&stage_collision,
|
||||
&[],
|
||||
true,
|
||||
AirCollisionOptions {
|
||||
can_pass_through_soft_platforms: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
@@ -639,7 +723,7 @@ mod tests {
|
||||
&ecb,
|
||||
&stage_collision,
|
||||
&[],
|
||||
false,
|
||||
Default::default(),
|
||||
)
|
||||
.expect("air dodges still collide with soft platforms")
|
||||
.hit_position,
|
||||
@@ -675,11 +759,131 @@ mod tests {
|
||||
&ecb,
|
||||
&stage_collision,
|
||||
&[],
|
||||
true,
|
||||
AirCollisionOptions {
|
||||
can_pass_through_soft_platforms: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("solid floors remain collidable while passing down");
|
||||
|
||||
assert_eq!(collision.line_id.polygon, 0);
|
||||
assert_eq!(collision.hit_position, FGVec2::lit("0", "0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upward_bottom_crossing_can_land_on_soft_platform() {
|
||||
let stage_collision = StageCollision {
|
||||
stage_polys: vec![horizontal_platform("0")],
|
||||
..Default::default()
|
||||
};
|
||||
let previous_translation = FighterPreviousTranslation(FGVec2::lit("0", "-1"));
|
||||
let mut translation = FighterTranslation(FGVec2::lit("0", "1"));
|
||||
let mut velocity = FighterVelocity(FGVec2::lit("0", "2"));
|
||||
let mut ecb = FighterECB::default();
|
||||
let desired = FighterDesiredECB::from(ecb);
|
||||
|
||||
let collision = air_collide_with_stage_substepped(
|
||||
&previous_translation,
|
||||
&mut translation,
|
||||
&mut velocity,
|
||||
&mut ecb,
|
||||
&desired,
|
||||
&stage_collision,
|
||||
&[],
|
||||
AirCollisionOptions {
|
||||
land_on_soft_platforms_from_below: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("the upward bottom-point crossing should land on the platform");
|
||||
|
||||
assert_eq!(collision.hit_position, FGVec2::ZERO);
|
||||
assert_eq!(collision.line_id, StageLineID { polygon: 0, segment: 0 });
|
||||
assert_eq!(translation.0, FGVec2::ZERO);
|
||||
assert_eq!(velocity.y, FGi32::ZERO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upward_platform_landing_requires_crossing_and_opt_in() {
|
||||
let stage_collision = StageCollision {
|
||||
stage_polys: vec![horizontal_platform("0")],
|
||||
..Default::default()
|
||||
};
|
||||
let previous_ecb = FighterPreviousECB::default();
|
||||
let ecb = FighterECB::default();
|
||||
let options = AirCollisionOptions {
|
||||
land_on_soft_platforms_from_below: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert!(
|
||||
air_collide_with_stage(
|
||||
&FighterPreviousTranslation(FGVec2::lit("0", "-2")),
|
||||
&FighterTranslation(FGVec2::lit("0", "-1")),
|
||||
&previous_ecb,
|
||||
&ecb,
|
||||
&stage_collision,
|
||||
&[],
|
||||
options,
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
air_collide_with_stage(
|
||||
&FighterPreviousTranslation(FGVec2::lit("0", "-1")),
|
||||
&FighterTranslation(FGVec2::lit("0", "1")),
|
||||
&previous_ecb,
|
||||
&ecb,
|
||||
&stage_collision,
|
||||
&[],
|
||||
Default::default(),
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upward_crossing_does_not_land_on_solid_or_ignored_platforms() {
|
||||
let previous_translation = FighterPreviousTranslation(FGVec2::lit("0", "-1"));
|
||||
let translation = FighterTranslation(FGVec2::lit("0", "1"));
|
||||
let previous_ecb = FighterPreviousECB::default();
|
||||
let ecb = FighterECB::default();
|
||||
let options = AirCollisionOptions {
|
||||
land_on_soft_platforms_from_below: true,
|
||||
..Default::default()
|
||||
};
|
||||
let solid_stage = StageCollision {
|
||||
stage_polys: vec![solid_floor()],
|
||||
..Default::default()
|
||||
};
|
||||
let platform_stage = StageCollision {
|
||||
stage_polys: vec![horizontal_platform("0")],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert!(
|
||||
air_collide_with_stage(
|
||||
&previous_translation,
|
||||
&translation,
|
||||
&previous_ecb,
|
||||
&ecb,
|
||||
&solid_stage,
|
||||
&[],
|
||||
options,
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
air_collide_with_stage(
|
||||
&previous_translation,
|
||||
&translation,
|
||||
&previous_ecb,
|
||||
&ecb,
|
||||
&platform_stage,
|
||||
&[0],
|
||||
options,
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user