//! fighter-to-fighter pushing use bevy::prelude::*; use crate::{ fighter::{ Fighter, FighterFacingDirection, FighterRoster, FighterSurfaceState, FighterTranslation, manifest::FighterManifest, }, game_settings::GameSettings, math::int::FGi32, stage::line::{StageCollision, StageLineID, StagePoly}, }; #[derive(Clone, Copy, Debug, PartialEq, Eq)] struct Pushbox { center: FGi32, radius: FGi32, line_id: StageLineID, } fn can_interact(stage: &StageCollision, first: StageLineID, second: StageLineID) -> bool { if first.polygon != second.polygon { return false; } if first.segment == second.segment { return true; } let Some(poly) = stage.stage_polys.get(first.polygon) else { return false; }; adjacent_segment(poly, first.segment, second.segment) } fn adjacent_segment(poly: &StagePoly, first: usize, second: usize) -> bool { poly.get_next_segment_index(first) == Some(second) || poly.get_prev_segment_index(first) == Some(second) } fn push_delta(first: Pushbox, second: Pushbox, nudge: FGi32) -> FGi32 { if (first.center - second.center).abs() >= first.radius + second.radius { return FGi32::ZERO; } (first.center - second.center).signum() * nudge } fn calculate_push_deltas( pushboxes: &[Option], stage: &StageCollision, nudge: FGi32, ) -> Vec { let mut deltas = vec![FGi32::ZERO; pushboxes.len()]; for first in 0..pushboxes.len() { let Some(first_pushbox) = pushboxes[first] else { continue; }; for second in 0..pushboxes.len() { if first == second { continue; } let Some(second_pushbox) = pushboxes[second] else { continue; }; if can_interact(stage, first_pushbox.line_id, second_pushbox.line_id) { deltas[first] += push_delta(first_pushbox, second_pushbox, nudge); } } } deltas } pub fn apply_grounded_pushes( roster: Res, mut fighters: Query<( &Fighter, &mut FighterTranslation, &FighterFacingDirection, &FighterSurfaceState, )>, manifests: Res>, settings: Res, stage: Res, ) { let pushboxes = roster .0 .iter() .map(|entity| { let Ok((fighter, translation, facing, surface)) = fighters.get(*entity) else { return None; }; let Some(line_id) = surface.line_id() else { return None; }; let manifest = manifests.get(&fighter.manifest)?; let attributes = &manifest.attributes; Some(Pushbox { center: translation.x + facing.to_sign() * attributes.pushbox_offset, radius: attributes.pushbox_radius, line_id, }) }) .collect::>(); let deltas = calculate_push_deltas( &pushboxes, &stage, settings.fighter_common.grounded_push_nudge, ); for (entity, delta) in roster.0.iter().zip(deltas) { if delta == FGi32::ZERO { continue; } if let Ok((_, mut translation, _, _)) = fighters.get_mut(*entity) { translation.x += delta; } } } #[cfg(test)] mod tests { use super::*; use crate::math::vec::FGVec2; use crate::stage::line::{StagePolyLineSegmentType, StagePolyType}; fn stage() -> StageCollision { StageCollision { stage_polys: vec![StagePoly::build( StagePolyType::Platform, &[ (FGVec2::lit("0", "0"), StagePolyLineSegmentType::Floor), (FGVec2::lit("10", "0"), StagePolyLineSegmentType::Floor), (FGVec2::lit("20", "0"), StagePolyLineSegmentType::Floor), (FGVec2::lit("30", "0"), StagePolyLineSegmentType::Floor), ], )], ..StageCollision::default() } } fn box_at(x: &str, segment: usize) -> Pushbox { Pushbox { center: FGi32::lit(x), radius: FGi32::lit("1"), line_id: StageLineID { polygon: 0, segment, }, } } #[test] fn overlapping_pair_gets_opposite_fixed_nudges() { let deltas = calculate_push_deltas( &[Some(box_at("0", 0)), Some(box_at("1", 0))], &stage(), FGi32::lit("0.3"), ); assert_eq!(deltas, vec![FGi32::lit("-0.3"), FGi32::lit("0.3")]); } #[test] fn touching_pushboxes_do_not_nudge() { let deltas = calculate_push_deltas( &[Some(box_at("0", 0)), Some(box_at("2", 0))], &stage(), FGi32::lit("0.3"), ); assert_eq!(deltas, vec![FGi32::ZERO, FGi32::ZERO]); } #[test] fn adjacent_segments_can_push_and_unrelated_segments_cannot() { let adjacent = calculate_push_deltas( &[Some(box_at("9.5", 0)), Some(box_at("10", 1))], &stage(), FGi32::lit("0.3"), ); assert_eq!(adjacent, vec![FGi32::lit("-0.3"), FGi32::lit("0.3")]); let unrelated = calculate_push_deltas( &[Some(box_at("1", 0)), Some(box_at("2", 2))], &stage(), FGi32::lit("0.3"), ); assert_eq!(unrelated, vec![FGi32::ZERO, FGi32::ZERO]); } #[test] fn multiple_overlaps_accumulate() { let deltas = calculate_push_deltas( &[ Some(box_at("0", 0)), Some(box_at("0.5", 0)), Some(box_at("1", 0)), ], &stage(), FGi32::lit("0.3"), ); assert_eq!( deltas, vec![FGi32::lit("-0.6"), FGi32::ZERO, FGi32::lit("0.6")] ); } #[test] fn system_parameters_initialize_without_query_conflicts() { let mut world = World::new(); let mut schedule = Schedule::default(); schedule.add_systems(apply_grounded_pushes); schedule.initialize(&mut world).unwrap(); } }