Compare commits

..
1 Commits
Author SHA1 Message Date
EIREXE e80a961021 Make triggers be bound to airdodge on non-gamecube controllers
CI / production-build (pull_request) Successful in 10m30s
CI / build-and-test (pull_request) Successful in 1m45s
2026-09-26 13:28:36 +02:00
21 changed files with 74 additions and 997 deletions
+5 -9
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_with_snap, FighterPresentationSnap}}, fighter::{manifest::FighterManifest, motion::sample_presentation_translation},
stage::manifest::StageCameraProfile, stage::manifest::StageCameraProfile,
}; };
@@ -313,7 +313,6 @@ 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>,
@@ -330,17 +329,16 @@ fn update_match_camera(
let subjects = fighters let subjects = fighters
.iter_mut() .iter_mut()
.map( .map(
|(translation, previous_translation, fighter, facing, presentation_snap, mut extents)| { |(translation, previous_translation, fighter, facing, 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_with_snap( sample_presentation_translation(
*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,
@@ -467,7 +465,6 @@ 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>,
@@ -489,16 +486,15 @@ 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, presentation_snap, extents) in &fighters { for (translation, previous_translation, fighter, _facing, 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_with_snap( let center = sample_presentation_translation(
*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,
-13
View File
@@ -109,7 +109,6 @@ impl FighterDebugRowKind {
enum DebugFlag { enum DebugFlag {
Network, Network,
Ecb, Ecb,
BlastZones,
StageCollision, StageCollision,
AnimationBones, AnimationBones,
AttackHitboxes, AttackHitboxes,
@@ -151,10 +150,6 @@ const FIGHTER_MENU: &[DebugMenuEntry] = &[
label: "ECB", label: "ECB",
flag: DebugFlag::Ecb, flag: DebugFlag::Ecb,
}, },
DebugMenuEntry::Toggle {
label: "Blast Zones",
flag: DebugFlag::BlastZones,
},
DebugMenuEntry::Toggle { DebugMenuEntry::Toggle {
label: "Animation bones", label: "Animation bones",
flag: DebugFlag::AnimationBones, flag: DebugFlag::AnimationBones,
@@ -248,7 +243,6 @@ pub struct DebugSettings {
pub frame_stepping: bool, pub frame_stepping: bool,
pub network: bool, pub network: bool,
pub ecb: bool, pub ecb: bool,
pub blast_zones: bool,
pub stage_collision: bool, pub stage_collision: bool,
pub animation_bones: bool, pub animation_bones: bool,
pub attack_hitboxes: bool, pub attack_hitboxes: bool,
@@ -270,7 +264,6 @@ impl Default for DebugSettings {
frame_stepping: false, frame_stepping: false,
network: false, network: false,
ecb: false, ecb: false,
blast_zones: false,
stage_collision: false, stage_collision: false,
animation_bones: false, animation_bones: false,
attack_hitboxes: false, attack_hitboxes: false,
@@ -335,7 +328,6 @@ impl DebugSettings {
match flag { match flag {
DebugFlag::Network => self.network, DebugFlag::Network => self.network,
DebugFlag::Ecb => self.ecb, DebugFlag::Ecb => self.ecb,
DebugFlag::BlastZones => self.blast_zones,
DebugFlag::StageCollision => self.stage_collision, DebugFlag::StageCollision => self.stage_collision,
DebugFlag::AnimationBones => self.animation_bones, DebugFlag::AnimationBones => self.animation_bones,
DebugFlag::AttackHitboxes => self.attack_hitboxes, DebugFlag::AttackHitboxes => self.attack_hitboxes,
@@ -350,7 +342,6 @@ impl DebugSettings {
match flag { match flag {
DebugFlag::Network => self.network = !self.network, DebugFlag::Network => self.network = !self.network,
DebugFlag::Ecb => self.ecb = !self.ecb, DebugFlag::Ecb => self.ecb = !self.ecb,
DebugFlag::BlastZones => self.blast_zones = !self.blast_zones,
DebugFlag::StageCollision => self.stage_collision = !self.stage_collision, DebugFlag::StageCollision => self.stage_collision = !self.stage_collision,
DebugFlag::AnimationBones => self.animation_bones = !self.animation_bones, DebugFlag::AnimationBones => self.animation_bones = !self.animation_bones,
DebugFlag::AttackHitboxes => self.attack_hitboxes = !self.attack_hitboxes, DebugFlag::AttackHitboxes => self.attack_hitboxes = !self.attack_hitboxes,
@@ -893,10 +884,6 @@ pub fn stage_collision_enabled(settings: Res<DebugSettings>) -> bool {
settings.stage_collision settings.stage_collision
} }
pub fn blast_zones_enabled(settings: Res<DebugSettings>) -> bool {
settings.blast_zones
}
pub fn animation_bones_enabled(settings: Res<DebugSettings>) -> bool { pub fn animation_bones_enabled(settings: Res<DebugSettings>) -> bool {
settings.animation_bones settings.animation_bones
} }
+2 -86
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, StagePolyType}; use crate::stage::line::{StageCollision, StageLineID};
/* /*
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,7 +45,6 @@ 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(),
@@ -56,10 +55,7 @@ pub fn air_collide_with_stage(
.stage_polys .stage_polys
.iter() .iter()
.enumerate() .enumerate()
.filter(|(poly_idx, poly)| { .filter(|(poly_idx, _)| !ignored_polys.contains(poly_idx))
!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| {
( (
@@ -99,18 +95,6 @@ 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 {
@@ -138,78 +122,10 @@ 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"));
}
} }
+23 -41
View File
@@ -1,9 +1,10 @@
//! Fighter debug views: gizmos and the egui inspector window. //! Fighter debug views: gizmos and the egui inspector window.
use std::sync::OnceLock;
use bevy::color::palettes::css::{ORANGE, ORANGE_RED, WHITE}; use bevy::color::palettes::css::ORANGE;
use bevy::prelude::*; use bevy::{color::palettes::css::YELLOW, prelude::*};
use bevy_egui::{egui, EguiContexts}; use bevy_egui::{EguiContexts, egui};
use crate::debug_tools::{DebugSettings, FighterDebugRowKind}; use crate::debug_tools::{DebugSettings, FighterDebugRowKind};
use crate::fighter::animation::FighterAnimationFrame; use crate::fighter::animation::FighterAnimationFrame;
@@ -14,14 +15,14 @@ use crate::fighter::attack::{
use crate::fighter::baked_animation::{BakedFighterAnimations, FighterBoneMatrices}; use crate::fighter::baked_animation::{BakedFighterAnimations, FighterBoneMatrices};
use crate::fighter::ecb::FighterPreviousECB; use crate::fighter::ecb::FighterPreviousECB;
use crate::fighter::errors::FighterErrors; use crate::fighter::errors::FighterErrors;
use crate::fighter::hurtbox::FixedAffineCapsule;
use crate::fighter::manifest::FighterManifest; use crate::fighter::manifest::FighterManifest;
use crate::fighter::state::{air, FighterState}; use crate::fighter::state::{FighterState, air};
use crate::fighter::visual::FighterAnimations; use crate::fighter::visual::FighterAnimations;
use crate::fighter::{ use crate::fighter::{
Fighter, FighterECB, FighterFacingDirection, FighterHitboxes, FighterPreviousTranslation, Fighter, FighterECB, FighterFacingDirection, FighterHitboxes, FighterPreviousTranslation,
FighterTranslation, FighterVelocity, FighterTranslation, FighterVelocity,
}; };
use crate::filled_gizmos::FilledGizmos;
use crate::game_settings::GameSettings; use crate::game_settings::GameSettings;
use crate::input::FighterInput; use crate::input::FighterInput;
use crate::math::int::FGi32; use crate::math::int::FGi32;
@@ -37,7 +38,6 @@ pub fn debug_draw_ecb(
&FighterPreviousTranslation, &FighterPreviousTranslation,
)>, )>,
mut gizmos: Gizmos, mut gizmos: Gizmos,
mut filled_gizmos: FilledGizmos,
) { ) {
for (ecb, trf, ecb_prev, trf_prev) in ecbs { for (ecb, trf, ecb_prev, trf_prev) in ecbs {
gizmos.cross( gizmos.cross(
@@ -46,18 +46,15 @@ pub fn debug_draw_ecb(
bevy::color::palettes::css::BLUE, bevy::color::palettes::css::BLUE,
); );
let prev_transformed = ecb_prev.to_3d_lineloop(Transform::from_xyz(
trf_prev.x.to_num::<f32>(),
trf_prev.y.to_num(),
0.0,
));
filled_gizmos.polygon(&prev_transformed, ORANGE_RED);
let transformed = let transformed =
ecb.to_3d_lineloop(Transform::from_xyz(trf.x.to_num(), trf.y.to_num(), 0.0)); ecb.to_3d_lineloop(Transform::from_xyz(trf.x.to_num(), trf.y.to_num(), 0.0));
filled_gizmos.polygon(&transformed, ORANGE); gizmos.lineloop(transformed, YELLOW);
gizmos.lineloop(transformed, WHITE); let prev_transformed = ecb_prev.to_3d_lineloop(Transform::from_xyz(
trf_prev.x.to_num(),
trf_prev.y.to_num(),
-0.1,
));
gizmos.lineloop(prev_transformed, ORANGE);
} }
} }
pub fn fighter_debug( pub fn fighter_debug(
@@ -186,23 +183,15 @@ pub fn animation_debug(
} }
} }
pub fn hurtbox_debug( pub fn hurtbox_debug(query: Query<&FighterSolvedHurtboxes>, mut gizmos: Gizmos) {
query: Query<&FighterSolvedHurtboxes>,
mut filled_gizmos: FilledGizmos,
) {
for hurtboxes in &query { for hurtboxes in &query {
for hurtbox in &hurtboxes.0 { for hurtbox in &hurtboxes.0 {
filled_gizmos.affine_capsule( draw_affine_capsule(&mut gizmos, hurtbox.capsule, YELLOW);
hurtbox.capsule.transform.to_mat4(),
hurtbox.capsule.radius.to_num(),
hurtbox.capsule.half_length.to_num(),
Color::srgba(1.0, 1.0, 0.0, 0.75),
);
} }
} }
} }
/*fn draw_affine_capsule( fn draw_affine_capsule(
gizmos: &mut Gizmos, gizmos: &mut Gizmos,
capsule: FixedAffineCapsule, capsule: FixedAffineCapsule,
color: impl Into<Color> + Copy, color: impl Into<Color> + Copy,
@@ -224,9 +213,9 @@ pub fn hurtbox_debug(
for &(start, end) in &wireframe.lines { for &(start, end) in &wireframe.lines {
gizmos.line(vertices[start], vertices[end], color); gizmos.line(vertices[start], vertices[end], color);
} }
}*/ }
/*struct CapsuleWireVertex { struct CapsuleWireVertex {
direction: Vec3, direction: Vec3,
center_sign: f32, center_sign: f32,
} }
@@ -234,9 +223,9 @@ pub fn hurtbox_debug(
struct CapsuleWireframe { struct CapsuleWireframe {
vertices: Vec<CapsuleWireVertex>, vertices: Vec<CapsuleWireVertex>,
lines: Vec<(usize, usize)>, lines: Vec<(usize, usize)>,
}*/ }
/*fn capsule_wireframe() -> &'static CapsuleWireframe { fn capsule_wireframe() -> &'static CapsuleWireframe {
const RESOLUTION: usize = 16; const RESOLUTION: usize = 16;
const HEMISPHERE_STEPS: usize = 4; const HEMISPHERE_STEPS: usize = 4;
static WIREFRAME: OnceLock<CapsuleWireframe> = OnceLock::new(); static WIREFRAME: OnceLock<CapsuleWireframe> = OnceLock::new();
@@ -317,7 +306,7 @@ struct CapsuleWireframe {
CapsuleWireframe { vertices, lines } CapsuleWireframe { vertices, lines }
}) })
}*/ }
#[cfg(test)] #[cfg(test)]
mod capsule_debug_tests { mod capsule_debug_tests {
@@ -337,7 +326,7 @@ mod capsule_debug_tests {
assert!(marker.abs_diff_eq(Vec3::new(3.0, 4.0, 3.0), 1e-5)); assert!(marker.abs_diff_eq(Vec3::new(3.0, 4.0, 3.0), 1e-5));
} }
/*#[test] #[test]
fn capsule_wireframe_caps_extend_in_opposite_directions() { fn capsule_wireframe_caps_extend_in_opposite_directions() {
let wireframe = capsule_wireframe(); let wireframe = capsule_wireframe();
let lower: Vec<_> = wireframe let lower: Vec<_> = wireframe
@@ -355,7 +344,7 @@ mod capsule_debug_tests {
assert!(upper.iter().all(|vertex| vertex.direction.y >= 0.0)); assert!(upper.iter().all(|vertex| vertex.direction.y >= 0.0));
assert!(lower.iter().any(|vertex| vertex.direction == Vec3::NEG_Y)); assert!(lower.iter().any(|vertex| vertex.direction == Vec3::NEG_Y));
assert!(upper.iter().any(|vertex| vertex.direction == Vec3::Y)); assert!(upper.iter().any(|vertex| vertex.direction == Vec3::Y));
}*/ }
} }
pub fn attack_debug( pub fn attack_debug(
@@ -367,7 +356,6 @@ pub fn attack_debug(
&FighterAnimationFrame, &FighterAnimationFrame,
)>, )>,
mut gizmos: Gizmos, mut gizmos: Gizmos,
mut filled_gizmos: FilledGizmos,
) { ) {
for (hitboxes, _matrices, _translation, _facing_direction, _frame) in query { for (hitboxes, _matrices, _translation, _facing_direction, _frame) in query {
if let Some(active_attack) = &hitboxes.0 { if let Some(active_attack) = &hitboxes.0 {
@@ -394,12 +382,6 @@ pub fn attack_debug(
capsule_draw_trf.to_isometry(), capsule_draw_trf.to_isometry(),
bevy::color::palettes::css::RED, bevy::color::palettes::css::RED,
); );
filled_gizmos.capsule(
capsule_draw_trf.to_isometry(),
solved_hitbox.capsule.radius.to_num(),
length * 0.5,
Color::srgba(1.0, 0.0, 0.0, 0.18),
);
gizmos.cross(start, 1.0, bevy::color::palettes::css::GREEN); gizmos.cross(start, 1.0, bevy::color::palettes::css::GREEN);
gizmos.cross(end, 1.0, bevy::color::palettes::css::GREEN); gizmos.cross(end, 1.0, bevy::color::palettes::css::GREEN);
+1 -9
View File
@@ -29,8 +29,7 @@ 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::{
FighterPresentationSnap, FighterPreviousFacingDirection, FighterPreviousTranslation, FighterPreviousFacingDirection, FighterPreviousTranslation, FighterTranslation, FighterVelocity,
FighterTranslation, FighterVelocity,
}; };
pub use surface::FighterSurfaceState; pub use surface::FighterSurfaceState;
@@ -148,10 +147,6 @@ 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,
( (
@@ -207,8 +202,6 @@ 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>()
@@ -311,7 +304,6 @@ 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,
+2 -42
View File
@@ -21,11 +21,6 @@ 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);
@@ -57,31 +52,10 @@ 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,
)>, )>,
@@ -90,13 +64,12 @@ 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, presentation_snap, hitstop_shake, mut transform) in query { for (translation, previous_translation, hitstop_shake, mut transform) in query {
let translation = sample_presentation_translation_with_snap( let translation = sample_presentation_translation(
*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,
@@ -196,19 +169,6 @@ 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,7 +123,6 @@ 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)))
} }
-9
View File
@@ -248,7 +248,6 @@ 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,19 +375,15 @@ 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!(
@@ -431,19 +426,15 @@ 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!(
+1 -12
View File
@@ -20,7 +20,6 @@ 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;
@@ -194,15 +193,12 @@ 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<'_> {
@@ -210,8 +206,6 @@ 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,
@@ -219,7 +213,6 @@ impl CollisionContext<'_> {
self.ecb, self.ecb,
self.stage_collision, self.stage_collision,
&ignored_polys, &ignored_polys,
ignore_soft_platforms,
) )
} }
} }
@@ -246,7 +239,6 @@ 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,
@@ -357,15 +349,12 @@ 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,
} }
} }
} }
-30
View File
@@ -1,30 +0,0 @@
#import bevy_pbr::mesh_functions::{get_world_from_local, mesh_position_local_to_clip}
@group(#{MATERIAL_BIND_GROUP}) @binding(0) var<uniform> material_color: vec4<f32>;
struct Vertex {
@builtin(instance_index) instance_index: u32,
@location(0) position: vec3<f32>,
@location(1) color: vec4<f32>,
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) color: vec4<f32>,
};
@vertex
fn vertex(vertex: Vertex) -> VertexOutput {
var output: VertexOutput;
output.clip_position = mesh_position_local_to_clip(
get_world_from_local(vertex.instance_index),
vec4<f32>(vertex.position, 1.0),
);
output.color = vertex.color;
return output;
}
@fragment
fn fragment(input: VertexOutput) -> @location(0) vec4<f32> {
return material_color * input.color;
}
-417
View File
@@ -1,417 +0,0 @@
//! Immediate-mode filled debug geometry.
//!
//! Unlike Bevy's line-only `Gizmos`, this collects triangles into one dynamic
//! mesh. It is intended for small, transient debug overlays rather than game
//! geometry.
use std::f32::consts::{FRAC_PI_2, PI, TAU};
use bevy::{
asset::{load_internal_asset, uuid_handle, RenderAssetUsages},
camera::visibility::NoFrustumCulling,
ecs::system::SystemParam,
mesh::{Indices, MeshVertexBufferLayoutRef, PrimitiveTopology},
pbr::{MaterialPipeline, MaterialPipelineKey},
prelude::*,
render::render_resource::{
AsBindGroup, RenderPipelineDescriptor, SpecializedMeshPipelineError,
},
shader::ShaderRef,
};
const SPHERE_SECTORS: usize = 16;
const SPHERE_STACKS: usize = 8;
const CAPSULE_SECTORS: usize = 16;
const CAPSULE_HEMISPHERE_STEPS: usize = 4;
#[derive(Default)]
struct FilledGizmoGeometry {
positions: Vec<[f32; 3]>,
normals: Vec<[f32; 3]>,
colors: Vec<[f32; 4]>,
indices: Vec<u32>,
}
#[derive(Resource, Default)]
struct FilledGizmoBuffer {
/// Geometry requested by the current schedule invocation.
pending: FilledGizmoGeometry,
/// The last completed fixed-timestep geometry. This is retained between
/// render frames, just like Bevy's fixed-timestep gizmos.
retained: FilledGizmoGeometry,
mesh: Option<Handle<Mesh>>,
fixed_commit: bool,
}
/// Immediate-mode API for drawing filled debug shapes.
#[derive(SystemParam)]
pub struct FilledGizmos<'w> {
buffer: ResMut<'w, FilledGizmoBuffer>,
}
impl FilledGizmos<'_> {
/// Draw a filled planar polygon. Vertices must be ordered around the
/// perimeter and the polygon must be convex.
pub fn polygon(&mut self, vertices: &[Vec3], color: impl Into<Color> + Copy) {
if vertices.len() < 3 {
return;
}
let color = color.into().to_linear().to_f32_array();
let normal = (vertices[1] - vertices[0])
.cross(vertices[2] - vertices[0])
.normalize_or_zero();
let base = self.buffer.pending.positions.len() as u32;
for &vertex in vertices {
self.push_vertex(vertex, normal, color);
}
for index in 2..vertices.len() as u32 {
self.buffer
.pending
.indices
.extend([base, base + index - 1, base + index]);
}
}
pub fn sphere(&mut self, isometry: Isometry3d, radius: f32, color: impl Into<Color> + Copy) {
if radius <= 0.0 {
return;
}
let color = color.into().to_linear().to_f32_array();
let base = self.buffer.pending.positions.len() as u32;
for stack in 0..=SPHERE_STACKS {
let latitude = PI * 0.5 - stack as f32 * PI / SPHERE_STACKS as f32;
let (sin_lat, cos_lat) = latitude.sin_cos();
for sector in 0..=SPHERE_SECTORS {
let longitude = sector as f32 * TAU / SPHERE_SECTORS as f32;
let (sin_lon, cos_lon) = longitude.sin_cos();
let normal = Vec3::new(cos_lat * cos_lon, sin_lat, cos_lat * sin_lon);
self.push_vertex(
isometry.transform_point(normal * radius).into(),
isometry.rotation * normal,
color,
);
}
}
let stride = SPHERE_SECTORS + 1;
for stack in 0..SPHERE_STACKS {
for sector in 0..SPHERE_SECTORS {
let a = base + (stack * stride + sector) as u32;
let b = a + stride as u32;
self.buffer
.pending
.indices
.extend([a, b, a + 1, a + 1, b, b + 1]);
}
}
}
pub fn capsule(
&mut self,
isometry: Isometry3d,
radius: f32,
half_length: f32,
color: impl Into<Color> + Copy,
) {
let transform = Mat4::from_scale_rotation_translation(
Vec3::ONE,
isometry.rotation,
isometry.translation.into(),
);
self.affine_capsule(transform, radius, half_length, color);
}
/// Draw a capsule transformed by an arbitrary affine transform.
pub fn affine_capsule(
&mut self,
transform: Mat4,
radius: f32,
half_length: f32,
color: impl Into<Color> + Copy,
) {
if radius <= 0.0 {
return;
}
let color = color.into().to_linear().to_f32_array();
let base = self.buffer.pending.positions.len() as u32;
let mut rings = Vec::new();
for step in 1..=CAPSULE_HEMISPHERE_STEPS {
let angle = -FRAC_PI_2 + step as f32 * FRAC_PI_2 / CAPSULE_HEMISPHERE_STEPS as f32;
rings.push((
radius * angle.cos(),
-half_length + radius * angle.sin(),
angle.sin(),
));
}
rings.push((radius, half_length, 0.0));
for step in 1..CAPSULE_HEMISPHERE_STEPS {
let angle = step as f32 * FRAC_PI_2 / CAPSULE_HEMISPHERE_STEPS as f32;
rings.push((
radius * angle.cos(),
half_length + radius * angle.sin(),
angle.sin(),
));
}
let mut ring_indices = Vec::new();
let bottom_pole = self.push_capsule_vertex(
&mut ring_indices,
transform,
Vec3::NEG_Y,
-half_length,
radius,
color,
);
for &(ring_radius, y, normal_y) in &rings {
let mut ring = Vec::with_capacity(CAPSULE_SECTORS);
for sector in 0..CAPSULE_SECTORS {
let angle = sector as f32 * TAU / CAPSULE_SECTORS as f32;
let (sin, cos) = angle.sin_cos();
let normal = Vec3::new(
cos * (1.0 - normal_y * normal_y).sqrt(),
normal_y,
sin * (1.0 - normal_y * normal_y).sqrt(),
);
ring.push(self.push_transformed_vertex(
transform,
Vec3::new(cos * ring_radius, y, sin * ring_radius),
normal,
color,
));
}
ring_indices.push(ring);
}
let top_pole = self.push_capsule_vertex(
&mut ring_indices,
transform,
Vec3::Y,
half_length,
radius,
color,
);
for sector in 0..CAPSULE_SECTORS {
let next = (sector + 1) % CAPSULE_SECTORS;
self.buffer.pending.indices.extend([
bottom_pole,
ring_indices[0][next],
ring_indices[0][sector],
]);
for ring in ring_indices.windows(2) {
self.buffer.pending.indices.extend([
ring[0][sector],
ring[0][next],
ring[1][sector],
ring[1][sector],
ring[0][next],
ring[1][next],
]);
}
let last = ring_indices.len() - 1;
self.buffer.pending.indices.extend([
ring_indices[last][sector],
ring_indices[last][next],
top_pole,
]);
}
// Keep the base used by this shape observable to avoid accidental
// future changes making the generated indices refer to another shape.
debug_assert!(base <= bottom_pole);
}
fn push_capsule_vertex(
&mut self,
_rings: &mut Vec<Vec<u32>>,
transform: Mat4,
normal: Vec3,
y: f32,
radius: f32,
color: [f32; 4],
) -> u32 {
self.push_transformed_vertex(
transform,
Vec3::new(0.0, y + normal.y * radius, 0.0),
normal,
color,
)
}
fn push_transformed_vertex(
&mut self,
transform: Mat4,
position: Vec3,
normal: Vec3,
color: [f32; 4],
) -> u32 {
let transformed_normal = transform
.inverse()
.transpose()
.transform_vector3(normal)
.normalize_or_zero();
let index = self.buffer.pending.positions.len() as u32;
self.push_vertex(
transform.transform_point3(position),
transformed_normal,
color,
);
index
}
fn push_vertex(&mut self, position: Vec3, normal: Vec3, color: [f32; 4]) {
self.buffer.pending.positions.push(position.to_array());
self.buffer.pending.normals.push(normal.to_array());
self.buffer.pending.colors.push(color);
}
}
pub struct FilledGizmosPlugin;
const FILLED_GIZMO_SHADER_HANDLE: Handle<Shader> =
uuid_handle!("0e4a979f-ea11-4e5a-91cd-19c89d4e9b53");
/// A deliberately minimal material: unlike `StandardMaterial`, its fragment
/// shader writes vertex colors directly and therefore bypasses PBR lighting
/// and camera tonemapping.
#[derive(Asset, AsBindGroup, Reflect, Clone, Debug)]
struct FilledGizmoMaterial {
#[uniform(0)]
color: LinearRgba,
}
impl Material for FilledGizmoMaterial {
fn vertex_shader() -> ShaderRef {
FILLED_GIZMO_SHADER_HANDLE.clone().into()
}
fn fragment_shader() -> ShaderRef {
FILLED_GIZMO_SHADER_HANDLE.clone().into()
}
fn alpha_mode(&self) -> AlphaMode {
AlphaMode::Blend
}
fn enable_prepass() -> bool {
false
}
fn enable_shadows() -> bool {
false
}
fn specialize(
_pipeline: &MaterialPipeline,
descriptor: &mut RenderPipelineDescriptor,
layout: &MeshVertexBufferLayoutRef,
_key: MaterialPipelineKey<Self>,
) -> Result<(), SpecializedMeshPipelineError> {
descriptor.vertex.buffers = vec![layout.0.get_layout(&[
Mesh::ATTRIBUTE_POSITION.at_shader_location(0),
Mesh::ATTRIBUTE_COLOR.at_shader_location(1),
])?];
descriptor.primitive.cull_mode = None;
Ok(())
}
}
impl Plugin for FilledGizmosPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(
app,
FILLED_GIZMO_SHADER_HANDLE,
"filled_gizmo.wgsl",
Shader::from_wgsl
);
app.add_plugins(MaterialPlugin::<FilledGizmoMaterial>::default())
.init_resource::<FilledGizmoBuffer>()
.add_systems(Startup, setup_filled_gizmo_mesh)
.add_systems(FixedFirst, begin_fixed_filled_gizmos)
.add_systems(FixedLast, commit_fixed_filled_gizmos)
.add_systems(Last, upload_filled_gizmo_mesh);
}
}
fn setup_filled_gizmo_mesh(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<FilledGizmoMaterial>>,
mut buffer: ResMut<FilledGizmoBuffer>,
) {
let mesh = meshes.add(
Mesh::new(
PrimitiveTopology::TriangleList,
RenderAssetUsages::default(),
)
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, vec![[0.0; 3]; 3])
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, vec![[0.0, 0.0, 1.0]; 3])
.with_inserted_attribute(Mesh::ATTRIBUTE_COLOR, vec![[0.0, 0.0, 0.0, 0.0]; 3])
.with_inserted_indices(Indices::U32(vec![0, 1, 2])),
);
let material = materials.add(FilledGizmoMaterial {
color: LinearRgba::WHITE,
});
commands.spawn((
Mesh3d(mesh.clone()),
MeshMaterial3d(material),
NoFrustumCulling,
));
buffer.mesh = Some(mesh);
}
fn begin_fixed_filled_gizmos(mut buffer: ResMut<FilledGizmoBuffer>) {
buffer.pending = FilledGizmoGeometry::default();
buffer.fixed_commit = false;
}
fn commit_fixed_filled_gizmos(mut buffer: ResMut<FilledGizmoBuffer>) {
let pending = std::mem::take(&mut buffer.pending);
buffer.pending = std::mem::replace(&mut buffer.retained, pending);
buffer.fixed_commit = true;
}
fn upload_filled_gizmo_mesh(
mut meshes: ResMut<Assets<Mesh>>,
mut buffer: ResMut<FilledGizmoBuffer>,
) {
// No fixed tick and no render-frame requests means the existing mesh is
// already the correct image. In particular, do not replace it with the
// empty placeholder between fixed updates.
if !buffer.fixed_commit && buffer.pending.positions.is_empty() {
return;
}
let Some(handle) = buffer.mesh.clone() else {
return;
};
let (positions, normals, colors, indices) = if buffer.fixed_commit {
// Retained geometry must remain in the CPU-side buffer after upload;
// otherwise the next render frame would have nothing to draw.
(
buffer.retained.positions.clone(),
buffer.retained.normals.clone(),
buffer.retained.colors.clone(),
buffer.retained.indices.clone(),
)
} else {
(
std::mem::take(&mut buffer.pending.positions),
std::mem::take(&mut buffer.pending.normals),
std::mem::take(&mut buffer.pending.colors),
std::mem::take(&mut buffer.pending.indices),
)
};
if let Some(mut mesh) = meshes.get_mut(&handle) {
if positions.is_empty() {
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, vec![[0.0; 3]; 3]);
mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, vec![[0.0, 0.0, 1.0]; 3]);
mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, vec![[0.0, 0.0, 0.0, 0.0]; 3]);
mesh.insert_indices(Indices::U32(vec![0, 1, 2]));
} else {
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals);
mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, colors);
mesh.insert_indices(Indices::U32(indices));
}
}
buffer.pending = FilledGizmoGeometry::default();
buffer.fixed_commit = false;
}
-4
View File
@@ -21,8 +21,6 @@
//! save, click Rebuild in jackdaw (or run `jd build`) and it //! save, click Rebuild in jackdaw (or run `jd build`) and it
//! appears in `Add Component`. No registration code is needed. //! appears in `Add Component`. No registration code is needed.
#![recursion_limit = "512"]
use bevy::{ use bevy::{
asset::processor::{AssetProcessor, ProcessorState}, asset::processor::{AssetProcessor, ProcessorState},
prelude::*, prelude::*,
@@ -44,7 +42,6 @@ pub mod blend_asset;
pub mod camera; pub mod camera;
pub mod debug_tools; pub mod debug_tools;
pub mod fighter; pub mod fighter;
pub mod filled_gizmos;
pub mod game_settings; pub mod game_settings;
pub mod input; pub mod input;
pub mod match_loading; pub mod match_loading;
@@ -117,7 +114,6 @@ impl Plugin for GamePlugin {
input::FighterInputPlugin, input::FighterInputPlugin,
replay::ReplayPlugin, replay::ReplayPlugin,
debug_tools::DebugToolsPlugin, debug_tools::DebugToolsPlugin,
filled_gizmos::FilledGizmosPlugin,
fighter::FighterPlugin, fighter::FighterPlugin,
fighter::baked_animation::BakedAnimationPlugin, fighter::baked_animation::BakedAnimationPlugin,
scripting::importer::FighterScriptImportPlugin, scripting::importer::FighterScriptImportPlugin,
+17 -53
View File
@@ -21,6 +21,7 @@ use crate::{
}, },
input::{LocalInputAssignments, LocalInputSource}, input::{LocalInputAssignments, LocalInputSource},
match_runtime::MatchState, match_runtime::MatchState,
math::{int::FGi32, vec::FGVec2},
netcode::{ netcode::{
GGRSCfg, GGRSCfg,
session::{ConnectionStatus, SessionConfig, create_session}, session::{ConnectionStatus, SessionConfig, create_session},
@@ -276,7 +277,7 @@ pub fn prepare_match(
if !valid_player_handles(&config.players) { if !valid_player_handles(&config.players) {
fail_match( fail_match(
&mut next_state, &mut next_state,
"player handles must be unique and non-empty", "player handles must be unique and cover 0..player_count",
); );
return; return;
} }
@@ -286,20 +287,6 @@ pub fn prepare_match(
return; return;
} }
let stage_handle = stage_registry
.get(config.stage)
.expect("Initialization check should ensure all stages exist");
let stage_manifest = stage_manifests
.get(&stage_handle)
.expect("Initialization check should ensure stage manifests are kept alive");
let spawn_positions = match stage_manifest.neutral_spawn_positions(config.players.len()) {
Ok(positions) => positions,
Err(error) => {
fail_match(&mut next_state, &error);
return;
}
};
if assets.fighters.len() != config.players.len() { if assets.fighters.len() != config.players.len() {
fail_match( fail_match(
&mut next_state, &mut next_state,
@@ -462,8 +449,7 @@ pub fn prepare_match(
)); ));
} }
let session_player_count = player_count_for_handles(&config.players); let session_config = match SessionConfig::from_args(&args, config.players.len()) {
let session_config = match SessionConfig::from_args(&args, session_player_count) {
Ok(config) => config, Ok(config) => config,
Err(error) => { Err(error) => {
fail_match( fail_match(
@@ -473,7 +459,7 @@ pub fn prepare_match(
return; return;
} }
}; };
let session = match create_session(&args, session_player_count) { let session = match create_session(&args, config.players.len()) {
Ok(session) => session, Ok(session) => session,
Err(error) => { Err(error) => {
fail_match( fail_match(
@@ -486,6 +472,12 @@ pub fn prepare_match(
let root = commands.spawn((Name::new("Match"), MatchRoot)).id(); let root = commands.spawn((Name::new("Match"), MatchRoot)).id();
let stage_handle = stage_registry
.get(config.stage)
.expect("Initialization check should ensure all stages exist");
let stage_manifest = stage_manifests
.get(&stage_handle)
.expect("Initialization check should ensure stage manifests are kept alive");
stage::spawn_stage( stage::spawn_stage(
&mut commands, &mut commands,
root, root,
@@ -495,15 +487,14 @@ pub fn prepare_match(
); );
let mut fighter_roster = Vec::with_capacity(config.players.len()); let mut fighter_roster = Vec::with_capacity(config.players.len());
for (index, (player, (scene, animations, attack_scripts, manifest_handle, model_scale, ecb_bone_indices))) in
for (spawn_index, (player, (scene, animations, attack_scripts, manifest_handle, model_scale, ecb_bone_indices))) in
config.players.iter().zip(prepared).enumerate() config.players.iter().zip(prepared).enumerate()
{ {
let spawn_point = spawn_positions[spawn_index]; let spawn_x = (index as i32 * 2 + 1 - config.players.len() as i32) * 5;
fighter_roster.push(spawn_fighter( fighter_roster.push(spawn_fighter(
&mut commands, &mut commands,
player.handle, player.handle,
spawn_point, FGVec2::new(FGi32::from_num(spawn_x), FGi32::lit("12.5")),
manifest_handle.clone(), manifest_handle.clone(),
model_scale, model_scale,
animations, animations,
@@ -527,22 +518,12 @@ fn fail_match(next_state: &mut NextState<AppState>, message: &str) {
} }
fn valid_player_handles(players: &[MatchPlayer]) -> bool { fn valid_player_handles(players: &[MatchPlayer]) -> bool {
if players.is_empty() { let mut handles = players
return false;
}
let mut handles = players.iter().map(|player| player.handle).collect::<Vec<_>>();
handles.sort_unstable();
handles.windows(2).all(|handles| handles[0] != handles[1])
&& handles.last().is_none_or(|handle| *handle != usize::MAX)
}
fn player_count_for_handles(players: &[MatchPlayer]) -> usize {
players
.iter() .iter()
.map(|player| player.handle) .map(|player| player.handle)
.max() .collect::<Vec<_>>();
.map_or(0, |handle| handle + 1) handles.sort_unstable();
handles == (0..players.len()).collect::<Vec<_>>()
} }
pub fn cleanup_match( pub fn cleanup_match(
@@ -609,23 +590,6 @@ mod tests {
assert!(!valid_player_handles(&players)); assert!(!valid_player_handles(&players));
} }
#[test]
fn player_handles_may_have_gaps() {
let players = [
MatchPlayer {
handle: 0,
fighter: FighterId::TestFighter,
},
MatchPlayer {
handle: 2,
fighter: FighterId::TestFighter,
},
];
assert!(valid_player_handles(&players));
assert_eq!(player_count_for_handles(&players), 3);
}
#[test] #[test]
fn match_rules_reject_zero_limits() { fn match_rules_reject_zero_limits() {
assert!( assert!(
+4 -65
View File
@@ -1,14 +1,9 @@
//! 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::{RunGgrsSystems, prelude::*}; use bevy_ggrs::prelude::*;
use crate::{ use crate::match_loading::{MatchConfig, MatchRules};
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 {
@@ -24,11 +19,6 @@ 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 {
@@ -40,12 +30,7 @@ impl MatchState {
Self { Self {
elapsed_frames: 0, elapsed_frames: 0,
players: (0..config players: (0..config.players.len())
.players
.iter()
.map(|player| player.handle)
.max()
.map_or(0, |handle| handle + 1))
.map(|_| PlayerMatchState { .map(|_| PlayerMatchState {
stocks_remaining, stocks_remaining,
score: 0, score: 0,
@@ -61,56 +46,10 @@ 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.start.accumulate(&status); input.accept.accumulate(&status);
} }
} }
} }
-17
View File
@@ -42,15 +42,6 @@ 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);
@@ -205,13 +196,6 @@ 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]
@@ -223,7 +207,6 @@ 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]
+3 -14
View File
@@ -140,7 +140,7 @@ fn validate(replay: &Replay) -> Result<(), String> {
)); ));
} }
if replay.players_are_invalid() { if replay.players_are_invalid() {
return Err("replay player handles must be unique".into()); return Err("replay player handles must cover 0..player_count".into());
} }
if replay.frames.is_empty() { if replay.frames.is_empty() {
return Err("replay must contain at least one frame".into()); return Err("replay must contain at least one frame".into());
@@ -169,16 +169,7 @@ fn validate(replay: &Replay) -> Result<(), String> {
if replay if replay
.frames .frames
.iter() .iter()
.any(|frame| { .any(|frame| frame.inputs.len() != replay.match_config.players.len())
frame.inputs.len()
!= replay
.match_config
.players
.iter()
.map(|player| player.handle)
.max()
.map_or(0, |handle| handle + 1)
})
{ {
return Err("replay frame input count does not match player count".into()); return Err("replay frame input count does not match player count".into());
} }
@@ -194,9 +185,7 @@ impl Replay {
.map(|player| player.handle) .map(|player| player.handle)
.collect(); .collect();
handles.sort_unstable(); handles.sort_unstable();
handles.is_empty() handles != (0..handles.len()).collect::<Vec<_>>()
|| handles.windows(2).any(|handles| handles[0] == handles[1])
|| handles.last() == Some(&usize::MAX)
} }
fn frame(&self, frame: i32) -> Option<&ReplayFrame> { fn frame(&self, frame: i32) -> Option<&ReplayFrame> {
+1 -29
View File
@@ -1,12 +1,6 @@
use bevy::{color::palettes::css::RED, prelude::*}; use bevy::{color::palettes::css::RED, prelude::*};
use crate::{ use crate::stage::line::StageCollision;
match_loading::MatchConfig,
stage::{
line::StageCollision,
manifest::{StageManifest, StageManifestRegistry},
},
};
pub fn debug_draw_scene(mut gizmos: Gizmos, stage_collision: Res<StageCollision>) { pub fn debug_draw_scene(mut gizmos: Gizmos, stage_collision: Res<StageCollision>) {
for poly in stage_collision.stage_polys.iter() { for poly in stage_collision.stage_polys.iter() {
@@ -22,25 +16,3 @@ pub fn debug_draw_scene(mut gizmos: Gizmos, stage_collision: Res<StageCollision>
} }
} }
} }
pub fn debug_draw_blast_zones(
mut gizmos: Gizmos,
stage_registry: Res<StageManifestRegistry>,
stage_manifests: Res<Assets<StageManifest>>,
match_config: Res<MatchConfig>,
) {
let Some(stage_manifest_handle) = stage_registry.get(match_config.stage) else {
return;
};
let Some(stage_manifest) = stage_manifests.get(&stage_manifest_handle) else {
return;
};
let top_left = stage_manifest.blast_zone_top_left.to_vec2();
let bottom_right = stage_manifest.blast_zone_bottom_right.to_vec2();
let center = (top_left + bottom_right) / 2.0;
let size = (bottom_right - top_left).abs();
gizmos.rect(Vec3::new(center.x, center.y, 0.0), size, bevy::color::palettes::css::BLUE);
}
+7 -82
View File
@@ -1,4 +1,4 @@
use std::collections::{HashMap, HashSet}; use std::collections::HashMap;
use bevy_asset_loader::asset_collection::AssetCollection; use bevy_asset_loader::asset_collection::AssetCollection;
use ron_asset_manager::prelude::RonAsset; use ron_asset_manager::prelude::RonAsset;
@@ -7,10 +7,14 @@ use strum::IntoEnumIterator;
use strum_macros::EnumIter; use strum_macros::EnumIter;
use crate::{ use crate::{
AppState, game_settings::GameSettings, math::{vec::FGVec2}, stage::{ AppState,
game_settings::GameSettings,
math::vec::FGVec2,
stage::{
StagePoly, StagePoly,
line::{StageCollision, StagePolyLineSegmentType, StagePolyType}, line::{StageCollision, StagePolyLineSegmentType, StagePolyType},
}, texture_reference::TextureReference, },
texture_reference::TextureReference,
}; };
use bevy::prelude::*; use bevy::prelude::*;
@@ -83,15 +87,6 @@ pub struct StageManifest {
pub icon: TextureReference, pub icon: TextureReference,
pub camera: StageCameraProfile, pub camera: StageCameraProfile,
pub polygons: Vec<StagePolyDefinition>, pub polygons: Vec<StagePolyDefinition>,
#[reflect(ignore)]
pub blast_zone_top_left: FGVec2,
#[reflect(ignore)]
pub blast_zone_bottom_right: FGVec2,
#[reflect(ignore)]
pub spawn_points: Vec<FGVec2>,
pub neutral_spawn_point_sets: Vec<Vec<usize>>
} }
#[derive(Resource)] #[derive(Resource)]
@@ -167,13 +162,6 @@ pub fn prepare_stage_manifests(
); );
return; return;
} }
if let Err(error) = manifest.validate_spawn_points() {
fail(
&mut next_state,
&format!("{:?} has invalid spawn points: {error}", manifest.id),
);
return;
}
if registry.insert(manifest.id, handle.clone()).is_some() { if registry.insert(manifest.id, handle.clone()).is_some() {
fail( fail(
&mut next_state, &mut next_state,
@@ -198,69 +186,6 @@ pub fn prepare_stage_manifests(
} }
impl StageManifest { impl StageManifest {
pub fn neutral_spawn_positions(&self, player_count: usize) -> Result<Vec<FGVec2>, String> {
if player_count == 0 {
return Err("a match must contain at least one player".to_owned());
}
let layout = self
.neutral_spawn_point_sets
.get(player_count - 1)
.ok_or_else(|| format!("no spawn layout for {player_count} players"))?;
if layout.len() != player_count {
return Err(format!(
"the {player_count}-player layout contains {} positions",
layout.len()
));
}
layout
.iter()
.map(|&index| {
self.spawn_points.get(index).copied().ok_or_else(|| {
format!("the {player_count}-player layout references spawn point {index}")
})
})
.collect()
}
fn validate_spawn_points(&self) -> Result<(), String> {
if self.spawn_points.is_empty() {
return Err("no spawn points are configured".to_owned());
}
if self.neutral_spawn_point_sets.is_empty() {
return Err("no neutral spawn layouts are configured".to_owned());
}
for (layout_index, layout) in self.neutral_spawn_point_sets.iter().enumerate() {
let player_count = layout_index + 1;
if layout.len() != player_count {
return Err(format!(
"the {player_count}-player layout contains {} positions",
layout.len()
));
}
let mut unique_indices = HashSet::with_capacity(layout.len());
for &spawn_point_index in layout {
if spawn_point_index >= self.spawn_points.len() {
return Err(format!(
"the {player_count}-player layout references spawn point {spawn_point_index}"
));
}
if !unique_indices.insert(spawn_point_index) {
return Err(format!(
"the {player_count}-player layout uses spawn point {spawn_point_index} more than once"
));
}
}
}
Ok(())
}
pub fn to_collision(&self) -> StageCollision { pub fn to_collision(&self) -> StageCollision {
let stage_polys: Vec<StagePoly> = self let stage_polys: Vec<StagePoly> = self
.polygons .polygons
+3 -8
View File
@@ -10,7 +10,7 @@ pub mod scene;
pub use line::{StagePlaneIntersectResult, StagePoly}; pub use line::{StagePlaneIntersectResult, StagePoly};
use crate::{match_loading::MatchConfig, stage::{line::StageCollision, manifest::StageCameraProfile}}; use crate::stage::{line::StageCollision, manifest::StageCameraProfile};
/// Owns the stage geometry: which scene gets spawned and how it is drawn. /// Owns the stage geometry: which scene gets spawned and how it is drawn.
#[derive(Default)] #[derive(Default)]
@@ -20,14 +20,9 @@ impl Plugin for StagePlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_systems( app.add_systems(
Update, Update,
( debug::debug_draw_scene
(debug::debug_draw_scene
.run_if(resource_exists::<line::StageCollision>) .run_if(resource_exists::<line::StageCollision>)
.run_if(crate::debug_tools::stage_collision_enabled)), .run_if(crate::debug_tools::stage_collision_enabled),
(debug::debug_draw_blast_zones
.run_if(resource_exists::<MatchConfig>)
.run_if(crate::debug_tools::blast_zones_enabled),),
),
); );
} }
} }
+2 -53
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, PauseState}, match_runtime::MatchState,
}; };
pub mod battle_ui; pub mod battle_ui;
@@ -11,9 +11,6 @@ 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;
@@ -35,54 +32,6 @@ impl Plugin for InGameUIPlugin {
} }
}), }),
), ),
) );
.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();
}
} }
} }