//! Resolving an integrated fighter position against the stage. use crate::fighter::ecb::{self as ecb_module, FighterDesiredECB}; use crate::fighter::{FighterPreviousTranslation, FighterTranslation}; use crate::fighter::FighterVelocity; #[cfg(test)] use crate::fighter::{FighterECB, ecb::FighterPreviousECB}; use crate::math::int::FGi32; use crate::math::rect::FGRect; use crate::math::segment::FGSegment2d; use crate::math::vec::FGVec2; use crate::stage::line::{ StageCollision, StageLineID, StagePolyLineSegmentType, StagePolyType, StageVertexID, }; /* pub fn collide_fighter_with_scene( fighters: Query<(&FighterECB, &mut FighterVelocity, &mut FighterTranslation)>, stage_planes: Query<&StagePoly>, mut gizmos: Gizmos, ) { for (ecb, mut velocity, mut translation) in fighters { for plane in stage_planes { let ray_from = translation.0; let ray_to = translation.0 + Vec2::new(0.0, -ecb.vertical_half); let intersection_result = plane.intersect_ray(Segment2d::new(ray_from, ray_to)); if let Some(result) = intersection_result { gizmos.sphere( Isometry3d::from(Vec3::new(result.position.x, result.position.y, 0.0)), 0.1f32, HOT_PINK, ); velocity.0 = plane.project(velocity.0); translation.0 = result.position + Vec2::new(0.0, ecb.vertical_half); } } } } */ #[derive(Clone)] pub struct AirCollisionWithStageResult { pub line_id: StageLineID, pub hit_position: FGVec2, /// Fighter origin at the collision substep. The ECB remains local to this /// origin; callers must not rebase the ECB by the frame-sized movement. pub resolved_translation: FGVec2, 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, translation: &FighterTranslation, prev_ecb: &FighterPreviousECB, ecb: &FighterECB, stage_collision: &StageCollision, ignored_polys: &[usize], options: AirCollisionOptions, ) -> Option { let mut translation = *translation; let mut velocity = FighterVelocity::default(); air_collide_with_stage_substepped( prev_translation, &mut translation, &mut velocity, &mut ecb_module::FighterECB { top: prev_ecb.top, bottom: prev_ecb.bottom, left: prev_ecb.left, right: prev_ecb.right, }, &FighterDesiredECB::from(*ecb), stage_collision, ignored_polys, options, ) } /// Sweeps movement and ECB shape changes together. pub(crate) fn collision_step_count(max_change: FGi32) -> u32 { let six = FGi32::lit("6.0"); if max_change <= six { return 1; } let mut quotient = 0; let mut remainder = max_change; while remainder >= six { quotient += 1; remainder -= six; } quotient + 1 } pub fn air_collide_with_stage_substepped( prev_translation: &FighterPreviousTranslation, translation: &mut FighterTranslation, velocity: &mut FighterVelocity, ecb: &mut crate::fighter::FighterECB, desired: &FighterDesiredECB, stage_collision: &StageCollision, ignored_polys: &[usize], options: AirCollisionOptions, ) -> Option { let requested_translation = translation.0; let movement = requested_translation - prev_translation.0; let mut largest_change = movement.x.abs().max(movement.y.abs()); largest_change = largest_change.max(ecb_module::ecb_max_delta(ecb, desired)); let steps = collision_step_count(largest_change); let step_count = FGi32::from_num(steps); let movement_per_step = movement / step_count; let mut current_position = prev_translation.0; let mut current_ecb = *ecb; let mut collision_offset = FGVec2::ZERO; for step in 0..steps { let previous_position = current_position; let previous_ecb = current_ecb; let remaining_steps = FGi32::from_num(steps - step); ecb_module::interpolate_ecb(&mut current_ecb, *desired, FGi32::ONE / remaining_steps); current_position = if step + 1 == steps { // Fixed-point division may truncate. Always make the final // substep land exactly on the requested frame endpoint. requested_translation + collision_offset } else { prev_translation.0 + movement_per_step * FGi32::from_num(step + 1) + collision_offset }; let lower_edges = [ ( previous_ecb.bottom, previous_ecb.left, current_ecb.bottom, current_ecb.left, ), ( previous_ecb.bottom, previous_ecb.right, current_ecb.bottom, current_ecb.right, ), ]; if let Some(result) = sweep_edges( stage_collision, previous_position, current_position, &lower_edges, StagePolyLineSegmentType::Floor, ignored_polys, options.can_pass_through_soft_platforms, ) { *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 upper_edges = [ ( previous_ecb.top, previous_ecb.left, current_ecb.top, current_ecb.left, ), ( previous_ecb.top, previous_ecb.right, current_ecb.top, current_ecb.right, ), ]; if let Some(contact) = sweep_edges( stage_collision, previous_position, current_position, &upper_edges, StagePolyLineSegmentType::Ceiling, ignored_polys, true, ) { current_position += contact.correction; collision_offset += contact.correction; 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, previous_ecb.right, current_ecb.bottom, current_ecb.right, ), ( previous_ecb.right, previous_ecb.top, current_ecb.right, current_ecb.top, ), ( previous_ecb.top, previous_ecb.left, current_ecb.top, current_ecb.left, ), ( previous_ecb.left, previous_ecb.bottom, current_ecb.left, current_ecb.bottom, ), ]; if let Some(contact) = sweep_edges( stage_collision, previous_position, current_position, &side_edges, StagePolyLineSegmentType::Wall, ignored_polys, true, ) { current_position += contact.correction; collision_offset += contact.correction; velocity.0 -= velocity.0.project_onto_normalized(contact.hit_normal); } } *ecb = current_ecb; translation.0 = current_position; None } struct PointSweepResult { line_id: StageLineID, hit_normal: FGVec2, correction: FGVec2, } fn sweep_bottom_point_through_platforms( stage_collision: &StageCollision, previous_bottom: FGVec2, current_bottom: FGVec2, ignored_polys: &[usize], ) -> Option { 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, current_position: FGVec2, edges: &[(FGVec2, FGVec2, FGVec2, FGVec2)], segment_type: StagePolyLineSegmentType, ignored_polys: &[usize], ignore_platforms: bool, ) -> Option { stage_collision .stage_polys .iter() .enumerate() .filter(|(poly_idx, poly)| { !ignored_polys.contains(poly_idx) && !(ignore_platforms && matches!(poly.poly_type, StagePolyType::Platform)) }) .flat_map(|(poly_idx, poly)| { poly.segments .iter() .enumerate() .filter(move |(_, segment)| segment.segment_type == segment_type) .filter_map(move |(segment_idx, segment)| { edges .iter() .filter_map(|(previous_a, previous_b, current_a, current_b)| { let previous_a = previous_position + *previous_a; let previous_b = previous_position + *previous_b; let current_a = current_position + *current_a; let current_b = current_position + *current_b; let previous_min = segment .normal .dot(previous_a - segment.segment.point1()) .min(segment.normal.dot(previous_b - segment.segment.point1())); let current_a_distance = segment.normal.dot(current_a - segment.segment.point1()); let current_b_distance = segment.normal.dot(current_b - segment.segment.point1()); let current_min = current_a_distance.min(current_b_distance); if previous_min < crate::math::int::FGWide::ZERO || current_min > crate::math::int::FGWide::ZERO { return None; } let boundary = [ FGSegment2d::new(previous_a, previous_b), FGSegment2d::new(previous_a, current_a), FGSegment2d::new(previous_b, current_b), FGSegment2d::new(current_a, current_b), ]; let quad = [previous_a, previous_b, current_b, current_a]; let position = boundary .iter() .filter_map(|edge| segment.segment.segment_intersection(edge)) .max_by_key(|position| position.distance_squared(current_a)) .or_else(|| { [segment.segment.point1(), segment.segment.point2()] .into_iter() .find(|point| point_in_convex_quad(*point, quad)) })?; let penetration = FGi32::from_num(-current_min); Some((position, segment.normal * penetration)) }) .max_by_key(|(position, _)| { position.distance_squared(segment.segment.point1()) }) .map(|(position, correction)| { ( PointSweepResult { line_id: StageLineID { polygon: poly_idx, segment: segment_idx, }, hit_normal: segment.normal, correction, }, position.distance_squared(segment.segment.point2()), ) }) }) }) .max_by(|(_, a), (_, b)| a.cmp(b)) .map(|(result, _)| result) } fn point_in_convex_quad(point: FGVec2, quad: [FGVec2; 4]) -> bool { let mut has_positive = false; let mut has_negative = false; for (a, b) in quad.into_iter().zip(quad.into_iter().cycle().skip(1)).take(4) { let edge = b - a; let offset = point - a; let cross = crate::math::int::FGWide::from_num(edge.x) * crate::math::int::FGWide::from_num(offset.y) - crate::math::int::FGWide::from_num(edge.y) * crate::math::int::FGWide::from_num(offset.x); has_positive |= cross > crate::math::int::FGWide::ZERO; has_negative |= cross < crate::math::int::FGWide::ZERO; if has_positive && has_negative { return false; } } true } #[derive(Clone)] pub struct AirCollisionWithLedgeResult { pub ledge: StageVertexID, } pub fn air_collide_with_ledges( ledge_box: &FGRect, stage_collision: &StageCollision, ) -> Option { stage_collision .intersect_ledges(ledge_box) .map(|result| AirCollisionWithLedgeResult { ledge: result.vertex_id, }) } #[cfg(test)] mod tests { use super::*; use crate::fighter::ecb::{FighterECB, FighterPreviousECB}; use crate::stage::line::{StagePoly, StagePolyLineSegmentType, StagePolyType}; fn horizontal_platform(y: &str) -> StagePoly { StagePoly::build( StagePolyType::Platform, &[ (FGVec2::lit("-2", y), StagePolyLineSegmentType::Floor), (FGVec2::lit("2", y), StagePolyLineSegmentType::Floor), ], ) } fn solid_floor() -> StagePoly { StagePoly::build( StagePolyType::Closed, &[ (FGVec2::lit("-2", "0"), StagePolyLineSegmentType::Floor), (FGVec2::lit("2", "0"), StagePolyLineSegmentType::Floor), (FGVec2::lit("2", "-2"), StagePolyLineSegmentType::Wall), (FGVec2::lit("-2", "-2"), StagePolyLineSegmentType::Wall), ], ) } fn single_segment( start: FGVec2, end: FGVec2, segment_type: StagePolyLineSegmentType, ) -> StagePoly { let segment = FGSegment2d::new(start, end); StagePoly { poly_type: StagePolyType::Closed, segments: vec![crate::stage::line::StagePolyLineSegment { segment, normal: segment.left_normal(), segment_type, }], } } #[test] fn melee_step_count_adds_one_step_after_each_six_unit_block() { assert_eq!(collision_step_count(FGi32::lit("6")), 1); assert_eq!(collision_step_count(FGi32::lit("6.01")), 2); assert_eq!(collision_step_count(FGi32::lit("12")), 3); assert_eq!(collision_step_count(FGi32::lit("18")), 4); } #[test] fn collision_result_keeps_ecb_local_to_resolved_origin() { let stage_collision = StageCollision { stage_polys: vec![horizontal_platform("0")], ..StageCollision::default() }; let previous_translation = FighterPreviousTranslation(FGVec2::lit("0", "2")); let mut translation = FighterTranslation(FGVec2::lit("0", "-1")); let mut velocity = FighterVelocity(FGVec2::lit("0", "-3")); let mut ecb = FighterECB { top: FGVec2::lit("0", "2"), bottom: FGVec2::lit("0", "-1"), left: FGVec2::lit("-1", "0.5"), right: FGVec2::lit("1", "0.5"), }; let desired = FighterDesiredECB::from(ecb); let collision = air_collide_with_stage_substepped( &previous_translation, &mut translation, &mut velocity, &mut ecb, &desired, &stage_collision, &[], Default::default(), ) .expect("the floor should be hit"); assert_eq!( collision.resolved_translation + ecb.bottom, collision.hit_position ); assert_eq!(velocity.0.y, FGi32::ZERO); } #[test] fn airborne_side_point_resolves_wall_and_projects_velocity() { let stage_collision = StageCollision { stage_polys: vec![single_segment( FGVec2::lit("2", "2"), FGVec2::lit("2", "-2"), StagePolyLineSegmentType::Wall, )], ..StageCollision::default() }; let previous = FighterPreviousTranslation(FGVec2::lit("4", "0")); let mut translation = FighterTranslation(FGVec2::lit("0", "0")); let mut velocity = FighterVelocity(FGVec2::lit("-4", "0")); let mut ecb = FighterECB { top: FGVec2::lit("0", "1"), bottom: FGVec2::lit("0", "-1"), left: FGVec2::lit("-1", "0"), right: FGVec2::lit("1", "0"), }; let desired = FighterDesiredECB::from(ecb); assert!(air_collide_with_stage_substepped( &previous, &mut translation, &mut velocity, &mut ecb, &desired, &stage_collision, &[], Default::default(), ) .is_none()); assert_eq!(translation.0, FGVec2::lit("3", "0")); assert_eq!(velocity.0.x, FGi32::ZERO); } #[test] fn stage_corner_cannot_pass_through_middle_of_diamond_edge() { let stage_collision = StageCollision { stage_polys: vec![single_segment( FGVec2::lit("2", "0.6"), FGVec2::lit("2", "0.4"), StagePolyLineSegmentType::Wall, )], ..StageCollision::default() }; let previous = FighterPreviousTranslation(FGVec2::lit("4", "0")); let mut translation = FighterTranslation(FGVec2::ZERO); let mut velocity = FighterVelocity(FGVec2::lit("-4", "0")); let mut ecb = FighterECB { top: FGVec2::lit("0", "1"), bottom: FGVec2::lit("0", "-1"), left: FGVec2::lit("-1", "0"), right: FGVec2::lit("1", "0"), }; let desired = FighterDesiredECB::from(ecb); assert!(air_collide_with_stage_substepped( &previous, &mut translation, &mut velocity, &mut ecb, &desired, &stage_collision, &[], Default::default(), ) .is_none()); assert_eq!(translation.0.x, FGi32::lit("3")); assert_eq!(velocity.0.x, FGi32::ZERO); } #[test] fn airborne_top_point_resolves_ceiling_and_projects_velocity() { let stage_collision = StageCollision { stage_polys: vec![single_segment( FGVec2::lit("2", "2"), FGVec2::lit("-2", "2"), StagePolyLineSegmentType::Ceiling, )], ..StageCollision::default() }; let previous = FighterPreviousTranslation(FGVec2::ZERO); let mut translation = FighterTranslation(FGVec2::lit("0", "4")); let mut velocity = FighterVelocity(FGVec2::lit("0", "4")); let mut ecb = FighterECB { top: FGVec2::lit("0", "1"), bottom: FGVec2::lit("0", "-1"), left: FGVec2::lit("-1", "0"), right: FGVec2::lit("1", "0"), }; let desired = FighterDesiredECB::from(ecb); assert!(air_collide_with_stage_substepped( &previous, &mut translation, &mut velocity, &mut ecb, &desired, &stage_collision, &[], Default::default(), ) .is_none()); assert_eq!(translation.0, FGVec2::lit("0", "1")); assert_eq!(velocity.0.y, FGi32::ZERO); } #[test] fn airborne_collision_skips_ignored_polygons() { let stage_collision = StageCollision { stage_polys: vec![horizontal_platform("0"), horizontal_platform("1")], ..StageCollision::default() }; let previous_translation = FighterPreviousTranslation(FGVec2::lit("0", "2")); let translation = FighterTranslation(FGVec2::lit("0", "-1")); let previous_ecb = FighterPreviousECB { bottom: FGVec2::ZERO, top: FGVec2::ZERO, left: FGVec2::ZERO, right: FGVec2::ZERO, }; let ecb = FighterECB { bottom: FGVec2::ZERO, top: FGVec2::ZERO, left: FGVec2::ZERO, right: FGVec2::ZERO, }; let collision = air_collide_with_stage( &previous_translation, &translation, &previous_ecb, &ecb, &stage_collision, &[1], Default::default(), ) .expect("the non-ignored platform should still be collidable"); assert_eq!(collision.line_id.polygon, 0); assert_eq!(collision.hit_position, FGVec2::lit("0", "0")); } #[test] fn airborne_collision_skips_soft_platforms_while_passing_down() { let stage_collision = StageCollision { stage_polys: vec![horizontal_platform("0")], ..StageCollision::default() }; let previous_translation = FighterPreviousTranslation(FGVec2::lit("0", "2")); let translation = FighterTranslation(FGVec2::lit("0", "-1")); let previous_ecb = FighterPreviousECB { bottom: FGVec2::ZERO, top: FGVec2::ZERO, left: FGVec2::ZERO, right: FGVec2::ZERO, }; let ecb = FighterECB { bottom: FGVec2::ZERO, top: FGVec2::ZERO, left: FGVec2::ZERO, right: FGVec2::ZERO, }; assert!( air_collide_with_stage( &previous_translation, &translation, &previous_ecb, &ecb, &stage_collision, &[], AirCollisionOptions { can_pass_through_soft_platforms: true, ..Default::default() }, ) .is_none() ); assert_eq!( air_collide_with_stage( &previous_translation, &translation, &previous_ecb, &ecb, &stage_collision, &[], Default::default(), ) .expect("air dodges still collide with soft platforms") .hit_position, FGVec2::lit("0", "0") ); } #[test] fn airborne_collision_still_hits_solid_floors_while_passing_down() { let stage_collision = StageCollision { stage_polys: vec![solid_floor()], ..StageCollision::default() }; let previous_translation = FighterPreviousTranslation(FGVec2::lit("0", "2")); let translation = FighterTranslation(FGVec2::lit("0", "-1")); let previous_ecb = FighterPreviousECB { bottom: FGVec2::ZERO, top: FGVec2::ZERO, left: FGVec2::ZERO, right: FGVec2::ZERO, }; let ecb = FighterECB { bottom: FGVec2::ZERO, top: FGVec2::ZERO, left: FGVec2::ZERO, right: FGVec2::ZERO, }; let collision = air_collide_with_stage( &previous_translation, &translation, &previous_ecb, &ecb, &stage_collision, &[], 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() ); } }