diff --git a/Cargo.toml b/Cargo.toml index d891239..d5f1ff4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,11 +45,6 @@ fixed_math = "0.4.1" [profile.dev.package."*"] opt-level = 3 -# The rollback simulation runs multiple times per rendered frame in sync-test -# mode, so optimizing the game crate is important even for development builds. -[profile.dev] -opt-level = 1 - [profile.production] inherits = "release" lto = "thin" diff --git a/src/fighter/baked_animation/mod.rs b/src/fighter/baked_animation/mod.rs index 5ab11cd..fbb5ae0 100644 --- a/src/fighter/baked_animation/mod.rs +++ b/src/fighter/baked_animation/mod.rs @@ -487,7 +487,7 @@ mod tests { } #[test] - fn repeating_root_motion_preserves_cycle_displacement_at_wrap() { + fn repeating_root_motion_preserves_cycle_displacement_at_every_wrap() { let names = ["root".into()]; let parents = [None]; let pose = |x| FixedMat4::IDENTITY.with_translation(FGVec3::lit(x, "0", "0")); @@ -498,7 +498,13 @@ mod tests { repeat: true, }; - for (frame_number, x) in [(0, "0"), (1, "1")] { + for (frame_number, x, expected_delta) in [ + (0, "0", FGVec3::ZERO), + (1, "1", FGVec3::lit("1", "0", "0")), + (2, "0", FGVec3::lit("1", "0", "0")), + (3, "1", FGVec3::lit("1", "0", "0")), + (4, "0", FGVec3::lit("1", "0", "0")), + ] { matrices.set_pose_with_frame( frame(frame_number), Some(0), @@ -511,20 +517,8 @@ mod tests { &[pose(x)], FGi32::ONE, ); + assert_eq!(matrices.root_motion_delta(), expected_delta); } - matrices.set_pose_with_frame( - frame(2), - Some(0), - Some(FGVec3::ZERO), - FGVec3::lit("2", "0", "0"), - Some(2), - AnimKind::Wait, - &names, - &parents, - &[pose("0")], - FGi32::ONE, - ); - assert_eq!(matrices.root_motion_delta(), FGVec3::lit("1", "0", "0")); } #[test] diff --git a/src/fighter/baked_animation/runtime.rs b/src/fighter/baked_animation/runtime.rs index 65b58f4..513c9ed 100644 --- a/src/fighter/baked_animation/runtime.rs +++ b/src/fighter/baked_animation/runtime.rs @@ -110,6 +110,9 @@ impl BakedFighterAnimations { .get(&kind) .and_then(|index| self.clips.get(*index)) } + pub fn get_animation_duration(&self, kind: AnimKind) -> Option { + self.clip(kind).map(|clip| clip.frames.len()) + } } #[derive(Component, Clone, Debug, Default, Hash)] @@ -411,7 +414,9 @@ impl FighterBoneMatrices { previous.kind == animation_frame.kind && previous.frame.saturating_add(1) == animation_frame.frame }) - && root_motion_loop_frame_count == Some(animation_frame.frame) + && root_motion_loop_frame_count.is_some_and(|loop_frame_count| { + loop_frame_count > 0 && animation_frame.frame % loop_frame_count == 0 + }) { delta = delta + root_motion_cycle_delta; } diff --git a/src/fighter/state/ledge_action.rs b/src/fighter/state/ledge_action.rs index d18dc45..d504b9f 100644 --- a/src/fighter/state/ledge_action.rs +++ b/src/fighter/state/ledge_action.rs @@ -7,7 +7,7 @@ use crate::fighter::FighterFacingDirection; use crate::input::FighterCommands; use crate::math::int::FGi32; use crate::math::vec::FGVec2; -use crate::stage::line::{StageLedgeSide, StageLineID, StageVertexID}; +use crate::stage::line::{StageLedgeSide, StageVertexID}; #[derive(Debug, Clone, Hash)] pub enum LedgeAction { @@ -96,17 +96,15 @@ impl FighterStateImpl for LedgeActionState { let normalized_root_position = FGVec2::new(normalized_root_position.x * ledge_modifier, normalized_root_position.y); if normalized_root_position.x.is_positive() && normalized_root_position.y.is_positive() { - self.is_in_grounded_phase = true; - - let segment_id = StageLineID { - polygon: self.ledge_id.polygon, - segment: self.ledge_id.vertex - }; - let Some(poly) = state_context.stage_collision.get_segment(segment_id) else { + let Some((segment_id, _)) = state_context + .stage_collision + .find_ledge_ground_segment(self.ledge_id) + else { return; }; - - state_context.translation.0 = poly.segment.point1(); + + self.is_in_grounded_phase = true; + state_context.translation.0 = ledge.position; state_context.surface.land(segment_id); } } @@ -136,4 +134,3 @@ impl FighterStateImpl for LedgeActionState { self.is_in_grounded_phase } } - diff --git a/src/fighter/state/ledge_wait.rs b/src/fighter/state/ledge_wait.rs index e638e61..57149e4 100644 --- a/src/fighter/state/ledge_wait.rs +++ b/src/fighter/state/ledge_wait.rs @@ -3,9 +3,9 @@ use crate::fighter::animation::AnimKind; use crate::fighter::state::air::AirborneStateCommon; use crate::fighter::state::fall::FallState; use crate::fighter::state::FighterState; -use crate::fighter::FighterFacingDirection; use crate::fighter::state::jump::JumpStateInitiator; use crate::fighter::state::ledge_action::{LedgeActionState}; +use crate::fighter::{FighterFacingDirection, FighterInvulnerabilityState}; use crate::input::FighterCommands; use crate::math::vec::FGVec2; use crate::stage::line::{StageLedgeSide, StageVertexID}; @@ -58,6 +58,14 @@ impl FighterStateImpl for LedgeWaitState { fn on_enter(&mut self, state_context: &mut EnterContext) { state_context.velocity.0 = FGVec2::ZERO; state_context.knockback_velocity.0 = FGVec2::ZERO; + *state_context.vulnerability = FighterInvulnerabilityState::Intangible { + frames_remaining: Some( + state_context + .game_settings + .fighter_common + .ledge_grab_invulnerability_duration, + ), + }; let Some(ledge) = state_context.stage_collision.find_ledge_by_vertex(self.ledge_id) else { return; }; @@ -127,4 +135,3 @@ fn is_ledge_drop_input( false } - diff --git a/src/fighter/state/mod.rs b/src/fighter/state/mod.rs index ff9941d..466f6c1 100644 --- a/src/fighter/state/mod.rs +++ b/src/fighter/state/mod.rs @@ -195,6 +195,10 @@ impl EnterContext<'_> { + animation_position_to_gameplay(position, self.facing_direction), ) } + + pub fn get_animation_duration(&self, kind: AnimKind) -> Option { + self.baked_animations.get_animation_duration(kind) + } } pub struct ExitContext<'a> { diff --git a/src/game_settings.rs b/src/game_settings.rs index 1c0b053..62b4138 100644 --- a/src/game_settings.rs +++ b/src/game_settings.rs @@ -52,6 +52,7 @@ pub struct FighterSettingsCommon { pub intangible_pulse_duration: u32, pub angel_platform_invulnerability_duration: u32, + pub ledge_grab_invulnerability_duration: u32, /// When hit while the receiver is in the air, use this angle pub sakurai_angle_aerial_angle: FGi32, diff --git a/src/stage/line.rs b/src/stage/line.rs index 1b0f1e2..d6b38c4 100644 --- a/src/stage/line.rs +++ b/src/stage/line.rs @@ -95,6 +95,32 @@ impl StageCollision { self.ledges.iter().find(|ledge| ledge.id == vertex) } + /// Returns the walkable segment on the stage side of a ledge. + pub fn find_ledge_ground_segment( + &self, + vertex: StageVertexID, + ) -> Option<(StageLineID, &StagePolyLineSegment)> { + let ledge = self.find_ledge_by_vertex(vertex)?; + let polygon = self.stage_polys.get(vertex.polygon)?; + let segment = match ledge.hang_side { + StageLedgeSide::Left => vertex.vertex, + StageLedgeSide::Right => { + if vertex.vertex > 0 { + vertex.vertex - 1 + } else if matches!(polygon.poly_type, StagePolyType::Closed) { + polygon.segments.len().checked_sub(1)? + } else { + return None; + } + } + }; + let line_id = StageLineID { + polygon: vertex.polygon, + segment, + }; + Some((line_id, polygon.get_segment(segment)?)) + } + pub fn get_segment(&self, segment: StageLineID) -> Option<&StagePolyLineSegment> { self.stage_polys.get(segment.polygon).map(|poly| poly.segments.get(segment.segment))? } diff --git a/src/stage/manifest.rs b/src/stage/manifest.rs index a76dbb8..508932b 100644 --- a/src/stage/manifest.rs +++ b/src/stage/manifest.rs @@ -428,6 +428,7 @@ impl StageCameraProfile { #[cfg(test)] mod tests { use super::*; + use crate::stage::line::StageLineID; fn platform(ledges: Vec) -> StagePolyDefinition { StagePolyDefinition { @@ -461,6 +462,35 @@ mod tests { assert_eq!(ledges[1].position, FGVec2::lit("5", "0")); } + #[test] + fn ledge_ground_segments_point_toward_the_stage_interior() { + let definition = platform(vec![ + StageLedgeDefinition { + vertex: 0, + hang_side: StageLedgeSide::Left, + }, + StageLedgeDefinition { + vertex: 1, + hang_side: StageLedgeSide::Right, + }, + ]); + let collision = StageCollision { + stage_polys: vec![StagePoly::build(definition.poly_type, &definition.lines)], + ledges: definition.resolved_ledges(0).collect(), + }; + + for vertex in [0, 1] { + let (line_id, segment) = collision + .find_ledge_ground_segment(StageVertexID { polygon: 0, vertex }) + .unwrap(); + assert_eq!(line_id, StageLineID { polygon: 0, segment: 0 }); + assert!(matches!( + segment.segment_type, + StagePolyLineSegmentType::Floor + )); + } + } + #[test] fn invalid_and_duplicate_ledge_vertices_are_rejected() { let missing = platform(vec![StageLedgeDefinition {