Compare commits

..
Author SHA1 Message Date
EIREXE 90412fa80d Add the ability to pause singleplayer matches (#68)
CI / build-and-test (push) Successful in 1m54s
CI / production-build (push) Successful in 11m11s
Reviewed-on: #68
Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
2026-09-26 16:25:36 +00:00
EIREXE 94e292026e Fix fighter visuals after landing snap (#67)
CI / build-and-test (push) Successful in 1m47s
CI / production-build (push) Successful in 11m8s
## Summary

- Mark landing position corrections as presentation discontinuities.
- Render and frame fighters from the current position for the snap frame.
- Keep previous translation history unchanged for gameplay collision and attack logic.

Reviewed-on: #67
Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
2026-09-26 15:54:25 +00:00
EIREXE 62aa598324 Make triggers be bound to airdodge on non-gamecube controllers (#66)
CI / build-and-test (push) Successful in 1m51s
CI / production-build (push) Successful in 11m10s
Reviewed-on: #66
Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
2026-09-26 13:07:11 +00:00
EIREXE a58b5eeec7 Allow airborne fighters to pass through platforms while holding down (#65)
CI / build-and-test (push) Successful in 1m50s
CI / production-build (push) Canceled after 18s
## Summary
- Ignore soft-platform collision while an airborne fighter holds down.
- Preserve existing momentum and collision with solid floors.
- Add regression coverage for soft and solid platform behavior.

Reviewed-on: #65
Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
2026-09-26 12:07:38 +00:00
EIREXE 615872f29c Implement being able to drop through soft platforms (#64)
CI / build-and-test (push) Successful in 1m47s
CI / production-build (push) Successful in 10m53s
Reviewed-on: #64
Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
2026-09-26 10:38:51 +00:00
EIREXE 66a817cd05 Add mechanism to ignore platforms (#63)
CI / build-and-test (push) Successful in 1m30s
CI / production-build (push) Successful in 10m48s
Reviewed-on: #63
Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
2026-09-25 21:43:49 +00:00
13 changed files with 303 additions and 23 deletions
+9 -5
View File
@@ -5,7 +5,7 @@ use bevy_ggrs::GgrsFrameTiming;
use crate::{ use crate::{
debug_tools::DebugSettings, debug_tools::DebugSettings,
fighter::{manifest::FighterManifest, motion::sample_presentation_translation}, fighter::{manifest::FighterManifest, motion::{sample_presentation_translation_with_snap, FighterPresentationSnap}},
stage::manifest::StageCameraProfile, stage::manifest::StageCameraProfile,
}; };
@@ -313,6 +313,7 @@ fn update_match_camera(
&crate::fighter::motion::FighterPreviousTranslation, &crate::fighter::motion::FighterPreviousTranslation,
&crate::fighter::Fighter, &crate::fighter::Fighter,
&crate::fighter::FighterFacingDirection, &crate::fighter::FighterFacingDirection,
&FighterPresentationSnap,
&mut FighterCameraExtents, &mut FighterCameraExtents,
), ),
With<crate::player::Player>, With<crate::player::Player>,
@@ -329,16 +330,17 @@ fn update_match_camera(
let subjects = fighters let subjects = fighters
.iter_mut() .iter_mut()
.map( .map(
|(translation, previous_translation, fighter, facing, mut extents)| { |(translation, previous_translation, fighter, facing, presentation_snap, mut extents)| {
let manifest = manifests let manifest = manifests
.get(&fighter.manifest) .get(&fighter.manifest)
.expect("Fighter manifest should be valid"); .expect("Fighter manifest should be valid");
fighter_subject_bounds( fighter_subject_bounds(
sample_presentation_translation( sample_presentation_translation_with_snap(
*previous_translation, *previous_translation,
*translation, *translation,
timing.overstep_fraction(), timing.overstep_fraction(),
debug_settings.motion_sampling, debug_settings.motion_sampling,
*presentation_snap,
), ),
manifest.camera, manifest.camera,
*facing, *facing,
@@ -465,6 +467,7 @@ fn debug_draw_camera(
&crate::fighter::motion::FighterPreviousTranslation, &crate::fighter::motion::FighterPreviousTranslation,
&crate::fighter::Fighter, &crate::fighter::Fighter,
&crate::fighter::FighterFacingDirection, &crate::fighter::FighterFacingDirection,
&FighterPresentationSnap,
&FighterCameraExtents, &FighterCameraExtents,
), ),
With<crate::player::Player>, With<crate::player::Player>,
@@ -486,15 +489,16 @@ fn debug_draw_camera(
Color::srgb(1.0, 0.5, 0.0), Color::srgb(1.0, 0.5, 0.0),
); );
draw_rectangle(&mut gizmos, camera.frame, Color::srgb(1.0, 1.0, 0.0)); draw_rectangle(&mut gizmos, camera.frame, Color::srgb(1.0, 1.0, 0.0));
for (translation, previous_translation, fighter, _facing, extents) in &fighters { for (translation, previous_translation, fighter, _facing, presentation_snap, extents) in &fighters {
let manifest = manifests let manifest = manifests
.get(&fighter.manifest) .get(&fighter.manifest)
.expect("Fighter manifest should be valid"); .expect("Fighter manifest should be valid");
let center = sample_presentation_translation( let center = sample_presentation_translation_with_snap(
*previous_translation, *previous_translation,
*translation, *translation,
timing.overstep_fraction(), timing.overstep_fraction(),
debug_settings.motion_sampling, debug_settings.motion_sampling,
*presentation_snap,
) + Vec2::Y * manifest.camera.vertical_origin_offset.to_num::<f32>(); ) + Vec2::Y * manifest.camera.vertical_origin_offset.to_num::<f32>();
draw_rectangle( draw_rectangle(
&mut gizmos, &mut gizmos,
+86 -2
View File
@@ -4,7 +4,7 @@ use crate::fighter::ecb::FighterPreviousECB;
use crate::fighter::{FighterECB, FighterPreviousTranslation, FighterTranslation}; use crate::fighter::{FighterECB, FighterPreviousTranslation, FighterTranslation};
use crate::math::segment::FGSegment2d; use crate::math::segment::FGSegment2d;
use crate::math::vec::FGVec2; use crate::math::vec::FGVec2;
use crate::stage::line::{StageCollision, StageLineID}; use crate::stage::line::{StageCollision, StageLineID, StagePolyType};
/* /*
pub fn collide_fighter_with_scene( pub fn collide_fighter_with_scene(
fighters: Query<(&FighterECB, &mut FighterVelocity, &mut FighterTranslation)>, fighters: Query<(&FighterECB, &mut FighterVelocity, &mut FighterTranslation)>,
@@ -45,6 +45,7 @@ pub fn air_collide_with_stage(
ecb: &FighterECB, ecb: &FighterECB,
stage_collision: &StageCollision, stage_collision: &StageCollision,
ignored_polys: &[usize], ignored_polys: &[usize],
ignore_soft_platforms: bool,
) -> Option<AirCollisionWithStageResult> { ) -> Option<AirCollisionWithStageResult> {
let ray_segment = FGSegment2d::new( let ray_segment = FGSegment2d::new(
prev_translation.0 + prev_ecb.get_bottom_point(), prev_translation.0 + prev_ecb.get_bottom_point(),
@@ -55,7 +56,10 @@ pub fn air_collide_with_stage(
.stage_polys .stage_polys
.iter() .iter()
.enumerate() .enumerate()
.filter(|(poly_idx, _)| !ignored_polys.contains(poly_idx)) .filter(|(poly_idx, poly)| {
!ignored_polys.contains(poly_idx)
&& !(ignore_soft_platforms && matches!(poly.poly_type, StagePolyType::Platform))
})
.filter_map(|(poly_idx, poly)| { .filter_map(|(poly_idx, poly)| {
poly.intersect_ray(ray_segment).map(|intersect_result| { poly.intersect_ray(ray_segment).map(|intersect_result| {
( (
@@ -95,6 +99,18 @@ mod tests {
) )
} }
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),
],
)
}
#[test] #[test]
fn airborne_collision_skips_ignored_polygons() { fn airborne_collision_skips_ignored_polygons() {
let stage_collision = StageCollision { let stage_collision = StageCollision {
@@ -122,10 +138,78 @@ mod tests {
&ecb, &ecb,
&stage_collision, &stage_collision,
&[1], &[1],
false,
) )
.expect("the non-ignored platform should still be collidable"); .expect("the non-ignored platform should still be collidable");
assert_eq!(collision.line_id.polygon, 0); assert_eq!(collision.line_id.polygon, 0);
assert_eq!(collision.hit_position, FGVec2::lit("0", "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")],
};
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,
&[],
true,
)
.is_none());
}
#[test]
fn airborne_collision_still_hits_solid_floors_while_passing_down() {
let stage_collision = StageCollision {
stage_polys: vec![solid_floor()],
};
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,
&[],
true,
)
.expect("solid floors remain collidable while passing down");
assert_eq!(collision.line_id.polygon, 0);
assert_eq!(collision.hit_position, FGVec2::lit("0", "0"));
}
} }
+9 -1
View File
@@ -29,7 +29,8 @@ pub use attributes::FighterAttributes;
pub use ecb::FighterECB; pub use ecb::FighterECB;
pub use manifest::FighterId; pub use manifest::FighterId;
pub use motion::{ pub use motion::{
FighterPreviousFacingDirection, FighterPreviousTranslation, FighterTranslation, FighterVelocity, FighterPresentationSnap, FighterPreviousFacingDirection, FighterPreviousTranslation,
FighterTranslation, FighterVelocity,
}; };
pub use surface::FighterSurfaceState; pub use surface::FighterSurfaceState;
@@ -147,6 +148,10 @@ impl Plugin for FighterPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_plugins(FighterAttackPlugin) app.add_plugins(FighterAttackPlugin)
// Every state gets a chance to interrupt itself, in priority order. // Every state gets a chance to interrupt itself, in priority order.
.add_systems(
GgrsSchedule,
motion::clear_fighter_presentation_snaps.in_set(GameplaySet::Input),
)
.add_systems( .add_systems(
GgrsSchedule, GgrsSchedule,
( (
@@ -202,6 +207,8 @@ impl Plugin for FighterPlugin {
.checksum_component_with_hash::<FighterTranslation>() .checksum_component_with_hash::<FighterTranslation>()
.rollback_component_with_copy::<FighterPreviousTranslation>() .rollback_component_with_copy::<FighterPreviousTranslation>()
.checksum_component_with_hash::<FighterPreviousTranslation>() .checksum_component_with_hash::<FighterPreviousTranslation>()
.rollback_component_with_copy::<FighterPresentationSnap>()
.checksum_component_with_hash::<FighterPresentationSnap>()
.rollback_component_with_copy::<FighterPreviousFacingDirection>() .rollback_component_with_copy::<FighterPreviousFacingDirection>()
.checksum_component_with_hash::<FighterPreviousFacingDirection>() .checksum_component_with_hash::<FighterPreviousFacingDirection>()
.rollback_component_with_copy::<animation::FighterAnimationFrame>() .rollback_component_with_copy::<animation::FighterAnimationFrame>()
@@ -304,6 +311,7 @@ pub fn spawn_fighter(
FighterVelocity::default(), FighterVelocity::default(),
FighterTranslation(spawn_position), FighterTranslation(spawn_position),
FighterPreviousTranslation(spawn_position), FighterPreviousTranslation(spawn_position),
FighterPresentationSnap::default(),
), ),
visual_root, visual_root,
animations, animations,
+42 -2
View File
@@ -21,6 +21,11 @@ pub struct FighterVelocity(pub FGVec2);
#[derive(Component, Deref, DerefMut, Default, Clone, Copy, Hash)] #[derive(Component, Deref, DerefMut, Default, Clone, Copy, Hash)]
pub struct FighterPreviousTranslation(pub FGVec2); pub struct FighterPreviousTranslation(pub FGVec2);
/// Marks a simulation frame whose position changed discontinuously for
/// presentation purposes (for example, a landing correction).
#[derive(Component, Default, Clone, Copy, Hash, Debug, PartialEq, Eq)]
pub struct FighterPresentationSnap(pub bool);
#[derive(Component, Deref, DerefMut, Default, Clone, Copy, Hash)] #[derive(Component, Deref, DerefMut, Default, Clone, Copy, Hash)]
#[require(FighterPreviousTranslation)] #[require(FighterPreviousTranslation)]
pub struct FighterTranslation(pub FGVec2); pub struct FighterTranslation(pub FGVec2);
@@ -52,10 +57,31 @@ pub fn sample_presentation_translation(
} }
} }
pub fn sample_presentation_translation_with_snap(
previous: FighterPreviousTranslation,
current: FighterTranslation,
overstep_fraction: f32,
mode: PresentationMotionMode,
snapped: FighterPresentationSnap,
) -> Vec2 {
if snapped.0 {
Vec2::new(current.x.to_num(), current.y.to_num())
} else {
sample_presentation_translation(previous, current, overstep_fraction, mode)
}
}
pub fn clear_fighter_presentation_snaps(mut fighters: Query<&mut FighterPresentationSnap>) {
for mut snap in &mut fighters {
snap.0 = false;
}
}
pub fn apply_fighter_translation_to_visuals( pub fn apply_fighter_translation_to_visuals(
query: Query<( query: Query<(
&FighterTranslation, &FighterTranslation,
&FighterPreviousTranslation, &FighterPreviousTranslation,
&FighterPresentationSnap,
&FighterHitstopShake, &FighterHitstopShake,
&mut Transform, &mut Transform,
)>, )>,
@@ -64,12 +90,13 @@ pub fn apply_fighter_translation_to_visuals(
game_settings: Res<GameSettings>, game_settings: Res<GameSettings>,
) { ) {
let overstep_fraction = timing.overstep_fraction(); let overstep_fraction = timing.overstep_fraction();
for (translation, previous_translation, hitstop_shake, mut transform) in query { for (translation, previous_translation, presentation_snap, hitstop_shake, mut transform) in query {
let translation = sample_presentation_translation( let translation = sample_presentation_translation_with_snap(
*previous_translation, *previous_translation,
*translation, *translation,
overstep_fraction, overstep_fraction,
debug_settings.motion_sampling, debug_settings.motion_sampling,
*presentation_snap,
); );
transform.translation = Vec3::new( transform.translation = Vec3::new(
translation.x, translation.x,
@@ -169,6 +196,19 @@ mod tests {
assert_eq!(position, Vec2::new(7.0, 14.0)); assert_eq!(position, Vec2::new(7.0, 14.0));
} }
#[test]
fn presentation_translation_uses_current_sample_after_snap() {
let position = sample_presentation_translation_with_snap(
FighterPreviousTranslation(FGVec2::new(FGi32::lit("2"), FGi32::lit("4"))),
FighterTranslation(FGVec2::new(FGi32::lit("6"), FGi32::lit("12"))),
0.25,
PresentationMotionMode::Extrapolation,
FighterPresentationSnap(true),
);
assert_eq!(position, Vec2::new(6.0, 12.0));
}
#[test] #[test]
fn snapshot_fighter_motion_copies_current_world_state() { fn snapshot_fighter_motion_copies_current_world_state() {
let mut world = World::new(); let mut world = World::new();
+1
View File
@@ -123,6 +123,7 @@ pub fn collision_interrupt(
ctx.ecb.left -= diff; ctx.ecb.left -= diff;
ctx.ecb.right -= diff; ctx.ecb.right -= diff;
ctx.translation.0 = res.hit_position; ctx.translation.0 = res.hit_position;
ctx.presentation_snap.0 = true;
ctx.surface.land(res.line_id); ctx.surface.land(res.line_id);
Some(FighterState::Land(LandingState::create(landing_type))) Some(FighterState::Land(LandingState::create(landing_type)))
} }
+2 -2
View File
@@ -120,8 +120,8 @@ impl FighterStateImpl for CrouchState {
fn update(&mut self, state_context: &mut UpdateContext) { fn update(&mut self, state_context: &mut UpdateContext) {
if let CrouchSubState::Entering { if let CrouchSubState::Entering {
ref mut pass_queued, pass_queued,
} = self.sub_state } = &mut self.sub_state
{ {
if state_context if state_context
.input .input
+9
View File
@@ -248,6 +248,7 @@ mod tests {
use crate::fighter::ecb::FighterPreviousECB; use crate::fighter::ecb::FighterPreviousECB;
use crate::fighter::{FighterECB, FighterSurfaceState}; use crate::fighter::{FighterECB, FighterSurfaceState};
use crate::stage::line::StagePolyType; use crate::stage::line::StagePolyType;
use crate::input::FighterInput;
fn two_segment_floor() -> StagePoly { fn two_segment_floor() -> StagePoly {
StagePoly::build( StagePoly::build(
@@ -375,15 +376,19 @@ mod tests {
right: FGVec2::lit("0.5", "0.0"), right: FGVec2::lit("0.5", "0.0"),
}; };
let previous_ecb = FighterPreviousECB::from(ecb); let previous_ecb = FighterPreviousECB::from(ecb);
let mut presentation_snap = crate::fighter::FighterPresentationSnap::default();
let mut context = CollisionContext { let mut context = CollisionContext {
translation: &mut translation, translation: &mut translation,
prev_translation: &previous_translation, prev_translation: &previous_translation,
input: &FighterInput::default(),
velocity: &mut velocity, velocity: &mut velocity,
surface: &mut surface, surface: &mut surface,
ecb: &mut ecb, ecb: &mut ecb,
prev_ecb: &previous_ecb, prev_ecb: &previous_ecb,
stage_collision: &stage_collision, stage_collision: &stage_collision,
leave_ground_ecb_lock_duration: 10, leave_ground_ecb_lock_duration: 10,
presentation_snap: &mut presentation_snap,
downward_stick_threshold: FGi32::ONE,
}; };
assert!(matches!( assert!(matches!(
@@ -426,15 +431,19 @@ mod tests {
right: FGVec2::lit("0.5", "0.0"), right: FGVec2::lit("0.5", "0.0"),
}; };
let previous_ecb = FighterPreviousECB::from(ecb); let previous_ecb = FighterPreviousECB::from(ecb);
let mut presentation_snap = crate::fighter::FighterPresentationSnap::default();
let mut context = CollisionContext { let mut context = CollisionContext {
translation: &mut translation, translation: &mut translation,
prev_translation: &previous_translation, prev_translation: &previous_translation,
input: &FighterInput::default(),
velocity: &mut velocity, velocity: &mut velocity,
surface: &mut surface, surface: &mut surface,
ecb: &mut ecb, ecb: &mut ecb,
prev_ecb: &previous_ecb, prev_ecb: &previous_ecb,
stage_collision: &stage_collision, stage_collision: &stage_collision,
leave_ground_ecb_lock_duration: 10, leave_ground_ecb_lock_duration: 10,
presentation_snap: &mut presentation_snap,
downward_stick_threshold: FGi32::ONE,
}; };
assert!(matches!( assert!(matches!(
+12 -1
View File
@@ -20,6 +20,7 @@ use crate::fighter::{
Fighter, FighterECB, FighterFacingDirection, FighterHitboxes, FighterHitstop, Fighter, FighterECB, FighterFacingDirection, FighterHitboxes, FighterHitstop,
FighterHitstopShake, FighterHitstopShakeKind, FighterHitstopShakeRole, FighterHitstopShake, FighterHitstopShakeKind, FighterHitstopShakeRole,
FighterPreviousTranslation, FighterSurfaceState, FighterTranslation, FighterVelocity, FighterPreviousTranslation, FighterSurfaceState, FighterTranslation, FighterVelocity,
FighterPresentationSnap,
}; };
use crate::fighter::surface::EcbLockPolicy; use crate::fighter::surface::EcbLockPolicy;
use crate::game_settings::GameSettings; use crate::game_settings::GameSettings;
@@ -193,12 +194,15 @@ impl UpdateContext<'_> {
pub struct CollisionContext<'a> { pub struct CollisionContext<'a> {
pub translation: &'a mut FighterTranslation, pub translation: &'a mut FighterTranslation,
pub prev_translation: &'a FighterPreviousTranslation, pub prev_translation: &'a FighterPreviousTranslation,
pub input: &'a FighterInput,
pub velocity: &'a mut FighterVelocity, pub velocity: &'a mut FighterVelocity,
pub surface: &'a mut FighterSurfaceState, pub surface: &'a mut FighterSurfaceState,
pub ecb: &'a mut FighterECB, pub ecb: &'a mut FighterECB,
pub prev_ecb: &'a FighterPreviousECB, pub prev_ecb: &'a FighterPreviousECB,
pub stage_collision: &'a StageCollision, pub stage_collision: &'a StageCollision,
pub leave_ground_ecb_lock_duration: u32, pub leave_ground_ecb_lock_duration: u32,
pub presentation_snap: &'a mut FighterPresentationSnap,
pub downward_stick_threshold: FGi32,
} }
impl CollisionContext<'_> { impl CollisionContext<'_> {
@@ -206,6 +210,8 @@ impl CollisionContext<'_> {
&self, &self,
) -> Option<crate::fighter::collision::AirCollisionWithStageResult> { ) -> Option<crate::fighter::collision::AirCollisionWithStageResult> {
let ignored_polys = self.surface.ignored_polygon_ids(); let ignored_polys = self.surface.ignored_polygon_ids();
let ignore_soft_platforms = self.input.get_last_frame().movement.y
<= -self.downward_stick_threshold;
crate::fighter::collision::air_collide_with_stage( crate::fighter::collision::air_collide_with_stage(
self.prev_translation, self.prev_translation,
self.translation, self.translation,
@@ -213,6 +219,7 @@ impl CollisionContext<'_> {
self.ecb, self.ecb,
self.stage_collision, self.stage_collision,
&ignored_polys, &ignored_polys,
ignore_soft_platforms,
) )
} }
} }
@@ -239,6 +246,7 @@ fighter_states! {
pub struct FighterFrameQuery { pub struct FighterFrameQuery {
pub translation: &'static mut FighterTranslation, pub translation: &'static mut FighterTranslation,
pub prev_translation: &'static mut FighterPreviousTranslation, pub prev_translation: &'static mut FighterPreviousTranslation,
pub presentation_snap: &'static mut FighterPresentationSnap,
pub input: &'static mut FighterInput, pub input: &'static mut FighterInput,
pub hitboxes: &'static mut FighterHitboxes, pub hitboxes: &'static mut FighterHitboxes,
pub velocity: &'static mut FighterVelocity, pub velocity: &'static mut FighterVelocity,
@@ -349,12 +357,15 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
CollisionContext { CollisionContext {
translation: &mut self.translation, translation: &mut self.translation,
prev_translation: &self.prev_translation, prev_translation: &self.prev_translation,
presentation_snap: &mut self.presentation_snap,
input: &self.input,
velocity: &mut self.velocity, velocity: &mut self.velocity,
surface: &mut self.surface, surface: &mut self.surface,
ecb: &mut self.ecb, ecb: &mut self.ecb,
prev_ecb: &self.prev_ecb, prev_ecb: &self.prev_ecb,
stage_collision, stage_collision,
leave_ground_ecb_lock_duration: game_settings.fighter_common.leave_ground_ecb_frame_lock_duration leave_ground_ecb_lock_duration: game_settings.fighter_common.leave_ground_ecb_frame_lock_duration,
downward_stick_threshold: game_settings.input_common.run_stick_threshold,
} }
} }
} }
+2 -3
View File
@@ -116,13 +116,12 @@ impl Plugin for FighterInputPlugin {
binding: GamepadBinding::Button(GamepadButton::North), binding: GamepadBinding::Button(GamepadButton::North),
action: InputMapAction::Jump, action: InputMapAction::Jump,
}, },
// shield/airdodge -- both shoulder buttons, Melee-style
GamepadInputMapElement { GamepadInputMapElement {
binding: GamepadBinding::Button(GamepadButton::LeftTrigger), binding: GamepadBinding::Button(GamepadButton::LeftTrigger2),
action: InputMapAction::Shield, action: InputMapAction::Shield,
}, },
GamepadInputMapElement { GamepadInputMapElement {
binding: GamepadBinding::Button(GamepadButton::RightTrigger), binding: GamepadBinding::Button(GamepadButton::RightTrigger2),
action: InputMapAction::Shield, action: InputMapAction::Shield,
}, },
// attack // attack
+59 -3
View File
@@ -1,9 +1,14 @@
//! Rollback-simulated state for an active match. //! Rollback-simulated state for an active match.
use bevy::input::InputSystems;
use bevy::prelude::*; use bevy::prelude::*;
use bevy_ggrs::prelude::*; use bevy_ggrs::{RunGgrsSystems, prelude::*};
use crate::match_loading::{MatchConfig, MatchRules}; use crate::{
AppState,
match_loading::{MatchConfig, MatchRules},
netcode::session::SessionConfig,
};
#[derive(Resource, Clone, Debug, Hash, PartialEq, Eq)] #[derive(Resource, Clone, Debug, Hash, PartialEq, Eq)]
pub struct MatchState { pub struct MatchState {
@@ -19,6 +24,11 @@ pub struct PlayerMatchState {
pub deaths: u32, pub deaths: u32,
} }
#[derive(Resource, Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct PauseState {
pub paused: bool,
}
impl MatchState { impl MatchState {
pub fn from_config(config: &MatchConfig) -> Self { pub fn from_config(config: &MatchConfig) -> Self {
let stocks_remaining = match &config.rules { let stocks_remaining = match &config.rules {
@@ -46,10 +56,56 @@ pub struct MatchRuntimePlugin;
impl Plugin for MatchRuntimePlugin { impl Plugin for MatchRuntimePlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.rollback_resource_with_clone::<MatchState>() app.rollback_resource_with_clone::<MatchState>()
.checksum_resource_with_hash::<MatchState>(); .checksum_resource_with_hash::<MatchState>()
.init_resource::<PauseState>()
.configure_sets(PreUpdate, RunGgrsSystems.run_if(ggrs_should_run))
.add_systems(OnEnter(AppState::InMatch), reset_pause)
.add_systems(OnExit(AppState::InMatch), reset_pause)
.add_systems(
PreUpdate,
toggle_pause
.after(InputSystems)
.before(RunGgrsSystems)
.run_if(in_state(AppState::InMatch)),
);
} }
} }
pub fn ggrs_should_run(pause: Option<Res<PauseState>>) -> bool {
!pause.is_some_and(|pause| pause.paused)
}
fn toggle_pause(
key: Res<ButtonInput<KeyCode>>,
gamepads: Query<&Gamepad>,
session: Res<SessionConfig>,
mut pause: ResMut<PauseState>,
mut virtual_time: ResMut<Time<Virtual>>,
) {
if session.is_multiplayer() {
return;
}
let pressed = key.just_pressed(KeyCode::Escape)
|| gamepads
.iter()
.any(|gamepad| gamepad.just_pressed(GamepadButton::Start));
if !pressed {
return;
}
pause.paused = !pause.paused;
virtual_time.toggle();
}
fn reset_pause(
mut pause: ResMut<PauseState>,
mut virtual_time: ResMut<Time<Virtual>>,
) {
pause.paused = false;
virtual_time.unpause();
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
+1 -1
View File
@@ -226,7 +226,7 @@ fn gamepad_input(
input.accept.accumulate(&status); input.accept.accumulate(&status);
} }
MenuInputMapAction::Start => { MenuInputMapAction::Start => {
input.accept.accumulate(&status); input.start.accumulate(&status);
} }
} }
} }
+17
View File
@@ -42,6 +42,15 @@ pub enum SessionConfigError {
} }
impl SessionConfig { impl SessionConfig {
/// Returns whether this match has more than one remote participant.
pub fn is_multiplayer(&self) -> bool {
self.players.len() > 1
|| self
.players
.iter()
.any(|player| matches!(player, PlayerConnection::Remote(_)))
}
pub fn from_args(args: &Args, player_count: usize) -> Result<Self, SessionConfigError> { pub fn from_args(args: &Args, player_count: usize) -> Result<Self, SessionConfigError> {
if player_count == 0 { if player_count == 0 {
return Err(SessionConfigError::NoPlayers); return Err(SessionConfigError::NoPlayers);
@@ -196,6 +205,13 @@ mod tests {
config.players, config.players,
[PlayerConnection::Local, PlayerConnection::Local] [PlayerConnection::Local, PlayerConnection::Local]
); );
assert!(config.is_multiplayer());
}
#[test]
fn single_local_player_is_not_multiplayer() {
let config = SessionConfig::from_args(&args(&["game", "--players", "1"]), 1).unwrap();
assert!(!config.is_multiplayer());
} }
#[test] #[test]
@@ -207,6 +223,7 @@ mod tests {
.unwrap(); .unwrap();
assert!(matches!(config.players[0], PlayerConnection::Local)); assert!(matches!(config.players[0], PlayerConnection::Local));
assert!(matches!(config.players[1], PlayerConnection::Remote(_))); assert!(matches!(config.players[1], PlayerConnection::Remote(_)));
assert!(config.is_multiplayer());
} }
#[test] #[test]
+54 -3
View File
@@ -3,7 +3,7 @@ use bevy::prelude::*;
use crate::{ use crate::{
AppState, AppState,
match_loading::{MatchConfig, MatchRules}, match_loading::{MatchConfig, MatchRules},
match_runtime::MatchState, match_runtime::{MatchState, PauseState},
}; };
pub mod battle_ui; pub mod battle_ui;
@@ -11,6 +11,9 @@ pub mod errors;
pub mod fighter_info; pub mod fighter_info;
pub mod stock_icon; pub mod stock_icon;
#[derive(Component)]
pub struct PauseOverlay;
#[derive(Default)] #[derive(Default)]
pub struct InGameUIPlugin; pub struct InGameUIPlugin;
@@ -30,8 +33,56 @@ impl Plugin for InGameUIPlugin {
} else { } else {
false false
} }
}), }),
), ),
); )
.add_systems(Update, sync_pause_overlay);
}
}
pub fn sync_pause_overlay(
pause: Res<PauseState>,
overlay: Query<Entity, With<PauseOverlay>>,
mut commands: Commands,
) {
if !pause.is_changed() {
return;
}
if pause.paused {
if overlay.is_empty() {
commands.spawn((
PauseOverlay,
Node {
position_type: PositionType::Absolute,
left: px(0),
right: px(0),
top: px(0),
bottom: px(0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
flex_direction: FlexDirection::Column,
..default()
},
BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.7)),
ZIndex(100),
children![
(
Text::new("Paused"),
TextFont::from_font_size(crate::menus::style::H1_SIZE as f32),
TextColor(Color::WHITE),
),
(
Text::new("Press Esc or Start to resume"),
TextFont::from_font_size(crate::menus::style::TEXT_SIZE as f32),
TextColor(Color::WHITE),
),
],
));
}
} else {
for entity in &overlay {
commands.entity(entity).despawn();
}
} }
} }