Add mechanism to ignore platforms #63
@@ -153,7 +153,7 @@ pub(super) fn save_or_restore(world: &mut World) {
|
||||
entity.insert(saved.hitboxes.clone());
|
||||
entity.insert(saved.solved_hurtboxes.clone());
|
||||
entity.insert(saved.bone_matrices.clone());
|
||||
entity.insert(saved.surface);
|
||||
entity.insert(saved.surface.clone());
|
||||
}
|
||||
world.insert_resource(checkpoint.pending_hits.clone());
|
||||
world.insert_resource(checkpoint.stage_collision);
|
||||
@@ -187,7 +187,7 @@ fn capture_fighter(world: &World, entity: Entity) -> Option<FighterCheckpoint> {
|
||||
hitboxes: fighter.get::<FighterHitboxes>()?.clone(),
|
||||
solved_hurtboxes: fighter.get::<FighterSolvedHurtboxes>()?.clone(),
|
||||
bone_matrices: fighter.get::<FighterBoneMatrices>()?.clone(),
|
||||
surface: *fighter.get::<FighterSurfaceState>()?,
|
||||
surface: fighter.get::<FighterSurfaceState>()?.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ pub fn air_collide_with_stage(
|
||||
prev_ecb: &FighterPreviousECB,
|
||||
ecb: &FighterECB,
|
||||
stage_collision: &StageCollision,
|
||||
ignored_polys: &[usize],
|
||||
) -> Option<AirCollisionWithStageResult> {
|
||||
let ray_segment = FGSegment2d::new(
|
||||
prev_translation.0 + prev_ecb.get_bottom_point(),
|
||||
@@ -54,6 +55,7 @@ pub fn air_collide_with_stage(
|
||||
.stage_polys
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(poly_idx, _)| !ignored_polys.contains(poly_idx))
|
||||
.filter_map(|(poly_idx, poly)| {
|
||||
poly.intersect_ray(ray_segment).map(|intersect_result| {
|
||||
(
|
||||
@@ -76,3 +78,54 @@ pub fn air_collide_with_stage(
|
||||
.max_by(|(_, a), (_, b)| a.cmp(b))
|
||||
.map(|(result, _)| result)
|
||||
}
|
||||
|
||||
#[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),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn airborne_collision_skips_ignored_polygons() {
|
||||
let stage_collision = StageCollision {
|
||||
stage_polys: vec![horizontal_platform("0"), horizontal_platform("1")],
|
||||
};
|
||||
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],
|
||||
)
|
||||
.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"));
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -105,8 +105,8 @@ pub fn update_ecb(
|
||||
let left = FGVec2::new(min_x, midpoint_height);
|
||||
let right = FGVec2::new(max_x, midpoint_height);
|
||||
|
||||
let ecb_lock_frames_remaining = if let FighterSurfaceState::Airborne { ecb_lock_frames_remaining } = *surface_state {
|
||||
ecb_lock_frames_remaining
|
||||
let ecb_lock_frames_remaining = if let FighterSurfaceState::Airborne { ecb_lock_frames_remaining, .. } = surface_state {
|
||||
*ecb_lock_frames_remaining
|
||||
} else {
|
||||
0
|
||||
};
|
||||
@@ -129,6 +129,7 @@ pub fn update_ecb(
|
||||
pub fn decrement_ecb_lockout(surface_states: Query<&mut FighterSurfaceState>) {
|
||||
for mut state in surface_states {
|
||||
state.tick_ecb_lock();
|
||||
state.tick_ignored_polygons();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -206,7 +206,7 @@ impl Plugin for FighterPlugin {
|
||||
.checksum_component_with_hash::<FighterPreviousFacingDirection>()
|
||||
.rollback_component_with_copy::<animation::FighterAnimationFrame>()
|
||||
.checksum_component_with_hash::<animation::FighterAnimationFrame>()
|
||||
.rollback_component_with_copy::<FighterSurfaceState>()
|
||||
.rollback_component_with_clone::<FighterSurfaceState>()
|
||||
.checksum_component_with_hash::<FighterSurfaceState>()
|
||||
.rollback_component_with_copy::<FighterFacingDirection>()
|
||||
.rollback_component_with_copy::<FighterDamage>()
|
||||
|
||||
@@ -199,12 +199,14 @@ impl CollisionContext<'_> {
|
||||
pub fn air_collide_with_stage(
|
||||
&self,
|
||||
) -> Option<crate::fighter::collision::AirCollisionWithStageResult> {
|
||||
let ignored_polys = self.surface.ignored_polygon_ids();
|
||||
crate::fighter::collision::air_collide_with_stage(
|
||||
self.prev_translation,
|
||||
self.translation,
|
||||
self.prev_ecb,
|
||||
self.ecb,
|
||||
self.stage_collision,
|
||||
&ignored_polys,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+100
-11
@@ -1,6 +1,6 @@
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::{stage::line::StageLineID};
|
||||
use crate::stage::line::StageLineID;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||
pub enum EcbLockPolicy {
|
||||
@@ -15,10 +15,17 @@ pub enum EcbLockPolicy {
|
||||
/// This is physics state rather than fighter-state-machine state. Keeping it
|
||||
/// separate means grounded, landing, and hitstun states all share one source
|
||||
/// of truth for stage contact.
|
||||
#[derive(Component, Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
pub struct IgnoredPolygon {
|
||||
pub polygon: usize,
|
||||
pub frames_remaining: u32,
|
||||
}
|
||||
|
||||
#[derive(Component, Clone, Debug, Hash, PartialEq, Eq)]
|
||||
pub enum FighterSurfaceState {
|
||||
Airborne {
|
||||
ecb_lock_frames_remaining: u32
|
||||
ecb_lock_frames_remaining: u32,
|
||||
ignored_polys: Vec<IgnoredPolygon>,
|
||||
},
|
||||
Grounded {
|
||||
line_id: StageLineID,
|
||||
@@ -28,20 +35,21 @@ pub enum FighterSurfaceState {
|
||||
impl Default for FighterSurfaceState {
|
||||
fn default() -> Self {
|
||||
Self::Airborne {
|
||||
ecb_lock_frames_remaining: 0
|
||||
ecb_lock_frames_remaining: 0,
|
||||
ignored_polys: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FighterSurfaceState {
|
||||
pub fn is_grounded(self) -> bool {
|
||||
pub fn is_grounded(&self) -> bool {
|
||||
matches!(self, Self::Grounded { .. })
|
||||
}
|
||||
|
||||
pub fn line_id(self) -> Option<StageLineID> {
|
||||
pub fn line_id(&self) -> Option<StageLineID> {
|
||||
match self {
|
||||
Self::Airborne { .. } => None,
|
||||
Self::Grounded { line_id } => Some(line_id),
|
||||
Self::Grounded { line_id } => Some(*line_id),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,13 +57,46 @@ impl FighterSurfaceState {
|
||||
*self = Self::Grounded { line_id };
|
||||
}
|
||||
|
||||
pub fn ignore_polygon(&mut self, polygon: usize, duration_frames: u32) {
|
||||
let Self::Airborne { ignored_polys, .. } = self else {
|
||||
return;
|
||||
};
|
||||
|
||||
if duration_frames == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(ignored) = ignored_polys.iter_mut().find(|ignored| ignored.polygon == polygon) {
|
||||
ignored.frames_remaining = duration_frames;
|
||||
} else {
|
||||
ignored_polys.push(IgnoredPolygon {
|
||||
polygon,
|
||||
frames_remaining: duration_frames,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_polygon_ignored(&self, polygon: usize) -> bool {
|
||||
matches!(self, Self::Airborne { ignored_polys, .. }
|
||||
if ignored_polys.iter().any(|ignored| ignored.polygon == polygon))
|
||||
}
|
||||
|
||||
pub(crate) fn ignored_polygon_ids(&self) -> Vec<usize> {
|
||||
match self {
|
||||
Self::Airborne { ignored_polys, .. } => {
|
||||
ignored_polys.iter().map(|ignored| ignored.polygon).collect()
|
||||
}
|
||||
Self::Grounded { .. } => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn apply_airborne_policy(
|
||||
&mut self,
|
||||
policy: EcbLockPolicy,
|
||||
ecb_lock_duration: u32,
|
||||
) {
|
||||
let current_lock = match *self {
|
||||
Self::Airborne { ecb_lock_frames_remaining } => ecb_lock_frames_remaining,
|
||||
let current_lock = match self {
|
||||
Self::Airborne { ecb_lock_frames_remaining, .. } => *ecb_lock_frames_remaining,
|
||||
Self::Grounded { .. } => 0,
|
||||
};
|
||||
let ecb_lock_frames_remaining = match policy {
|
||||
@@ -65,14 +106,27 @@ impl FighterSurfaceState {
|
||||
EcbLockPolicy::Preserve => current_lock,
|
||||
EcbLockPolicy::Clear => 0,
|
||||
};
|
||||
*self = Self::Airborne { ecb_lock_frames_remaining };
|
||||
let ignored_polys = match self {
|
||||
Self::Airborne { ignored_polys, .. } => ignored_polys.clone(),
|
||||
Self::Grounded { .. } => vec![],
|
||||
};
|
||||
*self = Self::Airborne { ecb_lock_frames_remaining, ignored_polys };
|
||||
}
|
||||
|
||||
pub(crate) fn tick_ecb_lock(&mut self) {
|
||||
if let Self::Airborne { ecb_lock_frames_remaining } = self {
|
||||
if let Self::Airborne { ecb_lock_frames_remaining, .. } = self {
|
||||
*ecb_lock_frames_remaining = ecb_lock_frames_remaining.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn tick_ignored_polygons(&mut self) {
|
||||
if let Self::Airborne { ignored_polys, .. } = self {
|
||||
for ignored in ignored_polys.iter_mut() {
|
||||
ignored.frames_remaining = ignored.frames_remaining.saturating_sub(1);
|
||||
}
|
||||
ignored_polys.retain(|ignored| ignored.frames_remaining > 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -109,6 +163,7 @@ mod tests {
|
||||
surface,
|
||||
FighterSurfaceState::Airborne {
|
||||
ecb_lock_frames_remaining: 4,
|
||||
ignored_polys: vec![],
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -124,7 +179,41 @@ mod tests {
|
||||
surface,
|
||||
FighterSurfaceState::Airborne {
|
||||
ecb_lock_frames_remaining: 5,
|
||||
ignored_polys: vec![],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignored_polygon_expires_after_requested_frames() {
|
||||
let mut surface = FighterSurfaceState::default();
|
||||
surface.ignore_polygon(3, 2);
|
||||
|
||||
assert!(surface.is_polygon_ignored(3));
|
||||
surface.tick_ignored_polygons();
|
||||
assert!(surface.is_polygon_ignored(3));
|
||||
surface.tick_ignored_polygons();
|
||||
assert!(!surface.is_polygon_ignored(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignoring_polygon_refreshes_existing_entry() {
|
||||
let mut surface = FighterSurfaceState::default();
|
||||
surface.ignore_polygon(3, 2);
|
||||
surface.ignore_polygon(3, 5);
|
||||
surface.tick_ignored_polygons();
|
||||
surface.tick_ignored_polygons();
|
||||
assert!(surface.is_polygon_ignored(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn airborne_policy_preserves_ignored_polygons_and_landing_clears_them() {
|
||||
let mut surface = FighterSurfaceState::default();
|
||||
surface.ignore_polygon(3, 5);
|
||||
surface.apply_airborne_policy(EcbLockPolicy::Restart, 2);
|
||||
assert!(surface.is_polygon_ignored(3));
|
||||
|
||||
surface.land(StageLineID { polygon: 0, segment: 0 });
|
||||
assert!(!surface.is_polygon_ignored(3));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user