Compare commits

...
7 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
EIREXE 946bbc4cbd Implement crouching (#62)
CI / build-and-test (push) Successful in 1m29s
CI / production-build (push) Successful in 10m48s
Reviewed-on: #62
Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
2026-09-25 18:14:09 +00:00
21 changed files with 723 additions and 41 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,
+2 -2
View File
@@ -153,7 +153,7 @@ pub(super) fn save_or_restore(world: &mut World) {
entity.insert(saved.hitboxes.clone()); entity.insert(saved.hitboxes.clone());
entity.insert(saved.solved_hurtboxes.clone()); entity.insert(saved.solved_hurtboxes.clone());
entity.insert(saved.bone_matrices.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.pending_hits.clone());
world.insert_resource(checkpoint.stage_collision); world.insert_resource(checkpoint.stage_collision);
@@ -187,7 +187,7 @@ fn capture_fighter(world: &World, entity: Entity) -> Option<FighterCheckpoint> {
hitboxes: fighter.get::<FighterHitboxes>()?.clone(), hitboxes: fighter.get::<FighterHitboxes>()?.clone(),
solved_hurtboxes: fighter.get::<FighterSolvedHurtboxes>()?.clone(), solved_hurtboxes: fighter.get::<FighterSolvedHurtboxes>()?.clone(),
bone_matrices: fighter.get::<FighterBoneMatrices>()?.clone(), bone_matrices: fighter.get::<FighterBoneMatrices>()?.clone(),
surface: *fighter.get::<FighterSurfaceState>()?, surface: fighter.get::<FighterSurfaceState>()?.clone(),
}) })
} }
+5
View File
@@ -84,6 +84,11 @@ pub enum AnimKind {
AirDodge, AirDodge,
Fall, Fall,
Turn, Turn,
CrouchEnter,
CrouchIdle,
CrouchExit,
PlatformPass,
FlinchGround, FlinchGround,
FlinchAir, FlinchAir,
+138 -1
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)>,
@@ -44,6 +44,8 @@ pub fn air_collide_with_stage(
prev_ecb: &FighterPreviousECB, prev_ecb: &FighterPreviousECB,
ecb: &FighterECB, ecb: &FighterECB,
stage_collision: &StageCollision, stage_collision: &StageCollision,
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(),
@@ -54,6 +56,10 @@ pub fn air_collide_with_stage(
.stage_polys .stage_polys
.iter() .iter()
.enumerate() .enumerate()
.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| {
( (
@@ -76,3 +82,134 @@ pub fn air_collide_with_stage(
.max_by(|(_, a), (_, b)| a.cmp(b)) .max_by(|(_, a), (_, b)| a.cmp(b))
.map(|(result, _)| result) .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),
],
)
}
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]
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],
false,
)
.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")],
};
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"));
}
}
+3 -2
View File
@@ -105,8 +105,8 @@ pub fn update_ecb(
let left = FGVec2::new(min_x, midpoint_height); let left = FGVec2::new(min_x, midpoint_height);
let right = FGVec2::new(max_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 { let ecb_lock_frames_remaining = if let FighterSurfaceState::Airborne { ecb_lock_frames_remaining, .. } = surface_state {
ecb_lock_frames_remaining *ecb_lock_frames_remaining
} else { } else {
0 0
}; };
@@ -129,6 +129,7 @@ pub fn update_ecb(
pub fn decrement_ecb_lockout(surface_states: Query<&mut FighterSurfaceState>) { pub fn decrement_ecb_lockout(surface_states: Query<&mut FighterSurfaceState>) {
for mut state in surface_states { for mut state in surface_states {
state.tick_ecb_lock(); state.tick_ecb_lock();
state.tick_ignored_polygons();
} }
} }
+10 -2
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,11 +207,13 @@ 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>()
.checksum_component_with_hash::<animation::FighterAnimationFrame>() .checksum_component_with_hash::<animation::FighterAnimationFrame>()
.rollback_component_with_copy::<FighterSurfaceState>() .rollback_component_with_clone::<FighterSurfaceState>()
.checksum_component_with_hash::<FighterSurfaceState>() .checksum_component_with_hash::<FighterSurfaceState>()
.rollback_component_with_copy::<FighterFacingDirection>() .rollback_component_with_copy::<FighterFacingDirection>()
.rollback_component_with_copy::<FighterDamage>() .rollback_component_with_copy::<FighterDamage>()
@@ -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)))
} }
+160
View File
@@ -0,0 +1,160 @@
use super::{
CollisionContext, EnterContext, FighterStateImpl, InterruptContext, UpdateContext, ground,
};
use crate::fighter::FighterSurfaceState;
use crate::fighter::animation::AnimKind;
use crate::fighter::state::air::AirborneStateCommon;
use crate::fighter::state::ground::{
grounded_movement_common_interrupts, grounded_movement_standstill_common_interrupts,
};
use crate::fighter::state::platform_pass::PlatformPassState;
use crate::fighter::state::{FighterState, dash, ground_attack, turn, walk};
use crate::input::FighterCommands;
use crate::stage::line::StagePolyType;
#[derive(Debug, Clone, Hash)]
pub enum CrouchSubState {
Entering { pass_queued: bool },
Idle,
Exiting,
}
#[derive(Debug, Clone, Hash)]
pub struct CrouchState {
sub_state: CrouchSubState,
}
pub fn crouch_idle_interrupts(state_context: &InterruptContext) -> Option<FighterState> {
if let Some(state) = ground_attack::check_interrupt(state_context) {
Some(state)
} else if let Some(state) = dash::check_input(state_context) {
Some(state)
} else if let Some(state) = turn::check_input(state_context) {
Some(state)
} else if let Some(state) = walk::check_input(state_context) {
Some(state)
} else {
None
}
}
pub fn crouch_entering_interrupts(state_context: &InterruptContext) -> Option<FighterState> {
if let Some(state) = ground_attack::check_interrupt(state_context) {
Some(state)
} else {
None
}
}
impl FighterStateImpl for CrouchState {
const NAME: &'static str = "Crouch";
fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option<FighterState> {
if let Some(state) = grounded_movement_common_interrupts(state_context) {
return Some(state);
}
match self.sub_state {
CrouchSubState::Entering { pass_queued } => {
// pass
if let FighterSurfaceState::Grounded { line_id } = state_context.surface {
if let Some(stage_poly) = state_context
.stage_collision
.stage_polys
.get(line_id.polygon)
{
let is_inputting_crouch = -state_context.game_settings.input_common.run_stick_threshold > state_context.input.get_last_frame().movement.y;
let is_on_a_soft_platform = matches!(stage_poly.poly_type, StagePolyType::Platform);
if pass_queued && is_inputting_crouch && is_on_a_soft_platform
{
return Some(FighterState::PlatformPass(PlatformPassState::new(
AirborneStateCommon::default(),
line_id.polygon,
)));
}
}
}
// While entering the crouch animation we do not allow movement inputs, but we do allow attacks and jumps
if state_context.is_current_animation_finished() {
return Some(FighterState::Crouch(CrouchState {
sub_state: CrouchSubState::Idle,
}));
} else {
return crouch_entering_interrupts(state_context);
}
}
CrouchSubState::Idle => {
if state_context.input.get_last_frame().movement.y
> -state_context.game_settings.input_common.run_stick_threshold
{
return Some(FighterState::Crouch(CrouchState {
sub_state: CrouchSubState::Exiting,
}));
} else {
return crouch_idle_interrupts(state_context);
}
}
CrouchSubState::Exiting => {
if state_context.is_current_animation_finished() {
return grounded_movement_standstill_common_interrupts(state_context);
} else {
return crouch_idle_interrupts(state_context);
}
}
}
}
fn on_enter(&mut self, state_context: &mut EnterContext) {
match self.sub_state {
CrouchSubState::Entering { .. } => {
state_context.play_animation(AnimKind::CrouchEnter, false);
}
CrouchSubState::Idle => {
state_context.play_animation(AnimKind::CrouchIdle, true);
}
CrouchSubState::Exiting => {
state_context.play_animation(AnimKind::CrouchExit, false);
}
}
}
fn update(&mut self, state_context: &mut UpdateContext) {
if let CrouchSubState::Entering {
pass_queued,
} = &mut self.sub_state
{
if state_context
.input
.has_command(FighterCommands::SmashMoveDown)
{
state_context
.input
.clear_command(FighterCommands::SmashMoveDown);
*pass_queued = true;
}
}
ground::update_friction_motion(state_context);
}
fn check_collision_interrupt(
&mut self,
state_context: &mut CollisionContext,
) -> Option<FighterState> {
ground::collision_interrupt(state_context, false)
}
fn is_visually_grounded<'a>(&'a self) -> bool {
true
}
}
pub fn check_input(state_context: &InterruptContext) -> Option<FighterState> {
let input_frame = state_context.input.get_last_frame();
if input_frame.movement.y <= -state_context.game_settings.input_common.run_stick_threshold {
return Some(FighterState::Crouch(CrouchState {
sub_state: CrouchSubState::Entering { pass_queued: false },
}));
}
None
}
+12 -2
View File
@@ -3,8 +3,7 @@
//! bit-for-bit deterministic for rollback. //! bit-for-bit deterministic for rollback.
use crate::fighter::state::{ use crate::fighter::state::{
CollisionContext, FighterState, InterruptContext, air, dash, fall, ground_attack, turn, wait, CollisionContext, FighterState, InterruptContext, air, crouch, dash, fall, ground_attack, turn, wait, walk,
walk,
}; };
use crate::fighter::{ use crate::fighter::{
FighterAttributes, FighterPreviousTranslation, FighterTranslation, FighterVelocity, FighterAttributes, FighterPreviousTranslation, FighterTranslation, FighterVelocity,
@@ -249,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(
@@ -376,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!(
@@ -427,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!(
@@ -461,6 +469,8 @@ pub fn grounded_movement_standstill_common_interrupts(
Some(state) Some(state)
} else if let Some(state) = turn::check_input(state_context) { } else if let Some(state) = turn::check_input(state_context) {
Some(state) Some(state)
} else if let Some(state) = crouch::check_input(state_context) {
Some(state)
} else if let Some(state) = walk::check_input(state_context) { } else if let Some(state) = walk::check_input(state_context) {
Some(state) Some(state)
} else if let Some(state) = wait::check_input(state_context) { } else if let Some(state) = wait::check_input(state_context) {
+33 -2
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;
@@ -41,6 +42,8 @@ pub mod run;
pub mod turn; pub mod turn;
pub mod wait; pub mod wait;
pub mod walk; pub mod walk;
pub mod crouch;
pub mod platform_pass;
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum SurfacePolicy { pub enum SurfacePolicy {
@@ -126,7 +129,9 @@ pub struct InterruptContext<'a> {
pub facing_direction: &'a mut FighterFacingDirection, pub facing_direction: &'a mut FighterFacingDirection,
pub game_settings: &'a GameSettings, pub game_settings: &'a GameSettings,
pub fighter_attributes: &'a FighterAttributes, pub fighter_attributes: &'a FighterAttributes,
pub surface: &'a FighterSurfaceState,
pub animation: AnimationStatus, pub animation: AnimationStatus,
pub stage_collision: &'a StageCollision,
} }
impl InterruptContext<'_> { impl InterruptContext<'_> {
@@ -147,8 +152,10 @@ pub struct EnterContext<'a> {
pub stale_move_queue: &'a mut FighterStaleMoveQueue, pub stale_move_queue: &'a mut FighterStaleMoveQueue,
pub facing_direction: &'a mut FighterFacingDirection, pub facing_direction: &'a mut FighterFacingDirection,
pub animation_frame: &'a mut FighterAnimationFrame, pub animation_frame: &'a mut FighterAnimationFrame,
pub surface: &'a mut FighterSurfaceState,
pub fighter_attributes: &'a FighterAttributes, pub fighter_attributes: &'a FighterAttributes,
pub attack_script_assets: &'a FighterAttackScriptAssets, pub attack_script_assets: &'a FighterAttackScriptAssets,
pub game_settings: &'a GameSettings,
} }
impl EnterContext<'_> { impl EnterContext<'_> {
@@ -159,6 +166,7 @@ impl EnterContext<'_> {
pub struct ExitContext<'a> { pub struct ExitContext<'a> {
pub hitboxes: &'a mut FighterHitboxes, pub hitboxes: &'a mut FighterHitboxes,
pub surface: &'a mut FighterSurfaceState,
} }
pub struct UpdateContext<'a> { pub struct UpdateContext<'a> {
@@ -186,24 +194,32 @@ 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<'_> {
pub fn air_collide_with_stage( pub fn air_collide_with_stage(
&self, &self,
) -> Option<crate::fighter::collision::AirCollisionWithStageResult> { ) -> Option<crate::fighter::collision::AirCollisionWithStageResult> {
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,
self.prev_ecb, self.prev_ecb,
self.ecb, self.ecb,
self.stage_collision, self.stage_collision,
&ignored_polys,
ignore_soft_platforms,
) )
} }
} }
@@ -211,6 +227,8 @@ impl CollisionContext<'_> {
fighter_states! { fighter_states! {
Wait => wait::WaitState, Wait => wait::WaitState,
Walk => walk::WalkState, Walk => walk::WalkState,
PlatformPass => platform_pass::PlatformPassState,
Crouch => crouch::CrouchState,
Dash => dash::DashState, Dash => dash::DashState,
Run => run::RunState, Run => run::RunState,
Fall => fall::FallState, Fall => fall::FallState,
@@ -228,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,
@@ -264,6 +283,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
animations: &'a FighterAnimations, animations: &'a FighterAnimations,
baked_animations: &'a Assets<BakedFighterAnimations>, baked_animations: &'a Assets<BakedFighterAnimations>,
attributes: &'a FighterAttributes, attributes: &'a FighterAttributes,
stage_collision: &'a StageCollision,
) -> InterruptContext<'a> { ) -> InterruptContext<'a> {
let animation = self.animation_status(animations, baked_animations); let animation = self.animation_status(animations, baked_animations);
InterruptContext { InterruptContext {
@@ -272,6 +292,8 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
game_settings, game_settings,
fighter_attributes: attributes, fighter_attributes: attributes,
animation, animation,
surface: &self.surface,
stage_collision,
} }
} }
@@ -279,6 +301,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
&'a mut self, &'a mut self,
attributes: &'a FighterAttributes, attributes: &'a FighterAttributes,
attack_script_assets: &'a FighterAttackScriptAssets, attack_script_assets: &'a FighterAttackScriptAssets,
game_settings: &'a GameSettings,
) -> EnterContext<'a> { ) -> EnterContext<'a> {
EnterContext { EnterContext {
input: &mut self.input, input: &mut self.input,
@@ -288,14 +311,17 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
stale_move_queue: &mut self.stale_move_queue, stale_move_queue: &mut self.stale_move_queue,
facing_direction: &mut self.facing_direction, facing_direction: &mut self.facing_direction,
animation_frame: &mut self.animation_frame, animation_frame: &mut self.animation_frame,
surface: &mut self.surface,
fighter_attributes: attributes, fighter_attributes: attributes,
attack_script_assets, attack_script_assets,
game_settings
} }
} }
fn exit_context(&mut self) -> ExitContext<'_> { fn exit_context(&mut self) -> ExitContext<'_> {
ExitContext { ExitContext {
hitboxes: &mut self.hitboxes, hitboxes: &mut self.hitboxes,
surface: &mut self.surface,
} }
} }
@@ -331,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,
} }
} }
} }
@@ -403,7 +432,7 @@ fn transition_state(
game_settings.fighter_common.leave_ground_ecb_frame_lock_duration, game_settings.fighter_common.leave_ground_ecb_frame_lock_duration,
); );
} }
next.on_enter(&mut frame.enter_context(attributes, attack_script_assets)); next.on_enter(&mut frame.enter_context(attributes, attack_script_assets, game_settings));
*current = next; *current = next;
info!("{} -> {}", old_name, new_name); info!("{} -> {}", old_name, new_name);
} }
@@ -413,6 +442,7 @@ pub fn state_interrupt_system(
game_settings: Res<GameSettings>, game_settings: Res<GameSettings>,
baked_animations: Res<Assets<BakedFighterAnimations>>, baked_animations: Res<Assets<BakedFighterAnimations>>,
fighter_manifests: Res<Assets<FighterManifest>>, fighter_manifests: Res<Assets<FighterManifest>>,
stage_collision: Res<StageCollision>,
) { ) {
for fighter in &mut query { for fighter in &mut query {
let StatefulFighterQueryItem { let StatefulFighterQueryItem {
@@ -486,6 +516,7 @@ pub fn state_interrupt_system(
animations, animations,
&baked_animations, &baked_animations,
&manifest.attributes, &manifest.attributes,
&stage_collision
)) ))
}; };
+61
View File
@@ -0,0 +1,61 @@
use super::{CollisionContext, EnterContext, FighterStateImpl, InterruptContext, UpdateContext};
use crate::fighter::animation::AnimKind;
use crate::fighter::state::air::AirborneStateCommon;
use crate::fighter::state::{ExitContext, FighterState, air, jump};
#[derive(Debug, Clone, Copy, Hash, Default)]
pub struct PlatformPassState {
airborne_state_common: AirborneStateCommon,
ignored_polygon: usize,
}
impl PlatformPassState {
pub fn new(airborne_state_common: AirborneStateCommon, ignored_polygon: usize) -> Self {
Self {
airborne_state_common,
ignored_polygon
}
}
}
impl FighterStateImpl for PlatformPassState {
const NAME: &'static str = "PlatformPass";
fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option<FighterState> {
jump::check_input_double_jump(state_context)
}
fn on_enter(&mut self, state_context: &mut EnterContext) {
state_context.surface.ignore_polygon(self.ignored_polygon, state_context.game_settings.fighter_common.platform_pass_platform_ignore_duration_frames);
state_context.velocity.y = state_context.game_settings.fighter_common.platform_pass_initial_downwards_velocity;
state_context.play_animation(AnimKind::PlatformPass, false);
}
fn on_exit(&mut self, state_context: &mut ExitContext) {
state_context.surface.clear_polygon_ignore(self.ignored_polygon);
}
fn update(&mut self, state_context: &mut UpdateContext) {
air::update_standard_motion(state_context, &mut self.airborne_state_common);
}
fn check_collision_interrupt(
&mut self,
state_context: &mut CollisionContext,
) -> Option<FighterState> {
air::collision_interrupt(
state_context,
crate::fighter::state::land::LandingType::Normal,
)
}
fn is_visually_grounded<'a>(
&'a self,
) -> bool {
false
}
fn surface_policy(&self) -> super::SurfacePolicy {
super::SurfacePolicy::Airborne(crate::fighter::surface::EcbLockPolicy::StartOnDetach)
}
}
+108 -11
View File
@@ -1,6 +1,6 @@
use bevy::prelude::*; use bevy::prelude::*;
use crate::{stage::line::StageLineID}; use crate::stage::line::StageLineID;
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum EcbLockPolicy { pub enum EcbLockPolicy {
@@ -15,10 +15,17 @@ pub enum EcbLockPolicy {
/// This is physics state rather than fighter-state-machine state. Keeping it /// This is physics state rather than fighter-state-machine state. Keeping it
/// separate means grounded, landing, and hitstun states all share one source /// separate means grounded, landing, and hitstun states all share one source
/// of truth for stage contact. /// 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 { pub enum FighterSurfaceState {
Airborne { Airborne {
ecb_lock_frames_remaining: u32 ecb_lock_frames_remaining: u32,
ignored_polys: Vec<IgnoredPolygon>,
}, },
Grounded { Grounded {
line_id: StageLineID, line_id: StageLineID,
@@ -28,20 +35,21 @@ pub enum FighterSurfaceState {
impl Default for FighterSurfaceState { impl Default for FighterSurfaceState {
fn default() -> Self { fn default() -> Self {
Self::Airborne { Self::Airborne {
ecb_lock_frames_remaining: 0 ecb_lock_frames_remaining: 0,
ignored_polys: vec![],
} }
} }
} }
impl FighterSurfaceState { impl FighterSurfaceState {
pub fn is_grounded(self) -> bool { pub fn is_grounded(&self) -> bool {
matches!(self, Self::Grounded { .. }) matches!(self, Self::Grounded { .. })
} }
pub fn line_id(self) -> Option<StageLineID> { pub fn line_id(&self) -> Option<StageLineID> {
match self { match self {
Self::Airborne { .. } => None, Self::Airborne { .. } => None,
Self::Grounded { line_id } => Some(line_id), Self::Grounded { line_id } => Some(*line_id),
} }
} }
@@ -49,13 +57,54 @@ impl FighterSurfaceState {
*self = Self::Grounded { line_id }; *self = Self::Grounded { line_id };
} }
pub fn clear_polygon_ignore(&mut self, polygon: usize) {
let Self::Airborne { ignored_polys, .. } = self else {
return;
};
ignored_polys.retain(|element| element.polygon != polygon);
}
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( pub(crate) fn apply_airborne_policy(
&mut self, &mut self,
policy: EcbLockPolicy, policy: EcbLockPolicy,
ecb_lock_duration: u32, ecb_lock_duration: u32,
) { ) {
let current_lock = match *self { let current_lock = match self {
Self::Airborne { ecb_lock_frames_remaining } => ecb_lock_frames_remaining, Self::Airborne { ecb_lock_frames_remaining, .. } => *ecb_lock_frames_remaining,
Self::Grounded { .. } => 0, Self::Grounded { .. } => 0,
}; };
let ecb_lock_frames_remaining = match policy { let ecb_lock_frames_remaining = match policy {
@@ -65,14 +114,27 @@ impl FighterSurfaceState {
EcbLockPolicy::Preserve => current_lock, EcbLockPolicy::Preserve => current_lock,
EcbLockPolicy::Clear => 0, 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) { 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); *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)] #[cfg(test)]
@@ -109,6 +171,7 @@ mod tests {
surface, surface,
FighterSurfaceState::Airborne { FighterSurfaceState::Airborne {
ecb_lock_frames_remaining: 4, ecb_lock_frames_remaining: 4,
ignored_polys: vec![],
} }
); );
} }
@@ -124,7 +187,41 @@ mod tests {
surface, surface,
FighterSurfaceState::Airborne { FighterSurfaceState::Airborne {
ecb_lock_frames_remaining: 5, 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));
}
} }
+3 -1
View File
@@ -39,7 +39,9 @@ pub struct FighterSettingsCommon {
/// Maximum angle changed caused by directional influence /// Maximum angle changed caused by directional influence
pub di_max_angle: FGi32, pub di_max_angle: FGi32,
pub leave_ground_ecb_frame_lock_duration: u32 pub leave_ground_ecb_frame_lock_duration: u32,
pub platform_pass_platform_ignore_duration_frames: u32,
pub platform_pass_initial_downwards_velocity: FGi32,
} }
#[derive(Reflect, Serialize, Deserialize, Clone)] #[derive(Reflect, Serialize, Deserialize, Clone)]
#[reflect(opaque)] #[reflect(opaque)]
+2
View File
@@ -233,6 +233,8 @@ pub fn postprocess_input(
input.clear_command(FighterCommands::SmashMoveUp); input.clear_command(FighterCommands::SmashMoveUp);
input.clear_command(FighterCommands::SmashMoveDown); input.clear_command(FighterCommands::SmashMoveDown);
input.frames_in_y_flick_deadzone = 0xFF;
let command_to_set = if input.current_frame.movement.y > 0.0 { let command_to_set = if input.current_frame.movement.y > 0.0 {
FighterCommands::SmashMoveUp FighterCommands::SmashMoveUp
} else { } else {
+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]
+1 -1
View File
@@ -34,7 +34,7 @@ pub struct StageLineID {
#[derive(Reflect, Clone, Default)] #[derive(Reflect, Clone, Default)]
pub struct StagePoly { pub struct StagePoly {
poly_type: StagePolyType, pub poly_type: StagePolyType,
pub segments: Vec<StagePolyLineSegment>, pub segments: Vec<StagePolyLineSegment>,
} }
+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();
}
} }
} }