Fix ledge transitions and root motion loops (#91)
CI / build-and-test (push) Successful in 1m39s
CI / production-build (push) Successful in 12m8s

## Summary

- resolve each ledge action to the walkable segment on the stage-interior side
- preserve repeating root-motion displacement across every animation cycle
- add regression coverage for left/right ledge segments and multiple root-motion wraps
- Implement ledge intangibility

Reviewed-on: #91
Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
This commit was merged in pull request #91.
This commit is contained in:
2026-10-12 21:38:23 +00:00
committed by EIREXE
parent 9b974d0dd6
commit f53e67d8ad
9 changed files with 93 additions and 34 deletions
+8 -11
View File
@@ -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
}
}
+9 -2
View File
@@ -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
}
+4
View File
@@ -195,6 +195,10 @@ impl EnterContext<'_> {
+ animation_position_to_gameplay(position, self.facing_direction),
)
}
pub fn get_animation_duration(&self, kind: AnimKind) -> Option<usize> {
self.baked_animations.get_animation_duration(kind)
}
}
pub struct ExitContext<'a> {