use bevy::prelude::*; use crate::{ fighter::{ FighterFacingDirection, FighterSurfaceState, FighterTranslation, baked_animation::FighterBoneMatrices, }, math::{int::FGi32, vec::FGVec2}, }; /// Environment collision box: the diamond used to resolve a fighter against /// the stage. Purely geometry — the collision response lives in /// [`super::collision`]. #[derive(Component, Debug, Clone, Copy, Hash, Default)] #[require(FighterPreviousECB)] pub struct FighterECB { pub top: FGVec2, pub bottom: FGVec2, pub left: FGVec2, pub right: FGVec2, } #[derive(Component, Debug, Clone)] pub struct FighterECBBoneIndices { pub indices: Vec, } #[derive(Component, Debug, Clone, Copy, Default, Hash)] pub struct FighterPreviousECB { pub top: FGVec2, pub bottom: FGVec2, pub left: FGVec2, pub right: FGVec2, } impl From for FighterPreviousECB { fn from(ecb: FighterECB) -> Self { Self { top: ecb.top, bottom: ecb.bottom, left: ecb.left, right: ecb.right, } } } pub fn snapshot_fighter_ecb(fighters: Query<(&FighterECB, &mut FighterPreviousECB)>) { for (ecb, mut previous_ecb) in fighters { *previous_ecb = (*ecb).into(); } } impl FighterECB { pub fn to_3d_lineloop(&self, trf: Transform) -> [Vec3; 4] { [self.top, self.right, self.bottom, self.left] .map(|point| trf.transform_point(Vec3::new(point.x.to_num(), point.y.to_num(), 0.0))) } pub fn get_bottom_point(&self) -> FGVec2 { self.bottom } } impl FighterPreviousECB { pub fn to_3d_lineloop(&self, trf: Transform) -> [Vec3; 4] { [self.top, self.right, self.bottom, self.left] .map(|point| trf.transform_point(Vec3::new(point.x.to_num(), point.y.to_num(), 0.0))) } pub fn get_bottom_point(&self) -> FGVec2 { self.bottom } } pub fn update_ecb( query: Query<( &mut FighterECB, &FighterSurfaceState, &FighterECBBoneIndices, &FighterBoneMatrices, &FighterTranslation, &FighterFacingDirection, )>, ) { for (mut ecb, surface_state, bone_indices, bone_matrices, translation, facing_dir) in query { let trf = translation.get_3d_transform(facing_dir); let transformed_ecb_bones = bone_indices.indices.iter().filter_map(|idx| { let Some((_, bone_matrix)) = bone_matrices.get_by_idx(*idx) else { return None; }; let pt = trf.transform_point(bone_matrix.get_translation()); Some(FGVec2::new(pt.x, pt.y) - translation.0) }).collect::>(); let max_x = transformed_ecb_bones.iter().max_by(|a, b| a.x.cmp(&b.x)).map(|final_val| final_val.x).unwrap_or(FGi32::lit("0.5")); let min_x = transformed_ecb_bones.iter().min_by(|a, b| a.x.cmp(&b.x)).map(|final_val| final_val.x).unwrap_or(FGi32::lit("-0.5")); let max_y = transformed_ecb_bones.iter().max_by(|a, b| a.y.cmp(&b.y)).map(|final_val| final_val.y).unwrap_or(FGi32::lit("0.5")); let min_y = transformed_ecb_bones.iter().min_by(|a, b| a.y.cmp(&b.y)).map(|final_val| final_val.y).unwrap_or(FGi32::lit("-0.5")); let midpoint_height = (max_y + min_y) / FGi32::lit("2.0"); let top = FGVec2::new(FGi32::ZERO, max_y); let bottom = FGVec2::new(FGi32::ZERO, min_y); 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 } else { 0 }; ecb.top = top; if ecb_lock_frames_remaining == 0 { ecb.bottom = bottom; } ecb.left = left; ecb.right = right; if surface_state.is_grounded() { ecb.bottom = FGVec2::ZERO; } } } pub fn decrement_ecb_lockout(surface_states: Query<&mut FighterSurfaceState>) { for mut state in surface_states { state.tick_ecb_lock(); state.tick_ignored_polygons(); } } #[cfg(test)] mod tests { use super::*; #[test] fn previous_ecb_copies_current_dimensions() { let current = FighterECB { bottom: FGVec2::lit("0.0", "-0.5"), top: FGVec2::lit("0.0", "0.5"), left: FGVec2::lit("-0.5", "0.0"), right: FGVec2::lit("0.5", "0.0"), }; let previous = FighterPreviousECB::from(current); assert_eq!(previous.top, current.top); assert_eq!(previous.bottom, current.bottom); assert_eq!(previous.left, current.left); assert_eq!(previous.right, current.right); } }