Fix ledge transitions and root motion loops #91
@@ -45,11 +45,6 @@ fixed_math = "0.4.1"
|
|||||||
[profile.dev.package."*"]
|
[profile.dev.package."*"]
|
||||||
opt-level = 3
|
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]
|
[profile.production]
|
||||||
inherits = "release"
|
inherits = "release"
|
||||||
lto = "thin"
|
lto = "thin"
|
||||||
|
|||||||
@@ -487,7 +487,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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 names = ["root".into()];
|
||||||
let parents = [None];
|
let parents = [None];
|
||||||
let pose = |x| FixedMat4::IDENTITY.with_translation(FGVec3::lit(x, "0", "0"));
|
let pose = |x| FixedMat4::IDENTITY.with_translation(FGVec3::lit(x, "0", "0"));
|
||||||
@@ -498,7 +498,13 @@ mod tests {
|
|||||||
repeat: true,
|
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(
|
matrices.set_pose_with_frame(
|
||||||
frame(frame_number),
|
frame(frame_number),
|
||||||
Some(0),
|
Some(0),
|
||||||
@@ -511,20 +517,8 @@ mod tests {
|
|||||||
&[pose(x)],
|
&[pose(x)],
|
||||||
FGi32::ONE,
|
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]
|
#[test]
|
||||||
|
|||||||
@@ -110,6 +110,9 @@ impl BakedFighterAnimations {
|
|||||||
.get(&kind)
|
.get(&kind)
|
||||||
.and_then(|index| self.clips.get(*index))
|
.and_then(|index| self.clips.get(*index))
|
||||||
}
|
}
|
||||||
|
pub fn get_animation_duration(&self, kind: AnimKind) -> Option<usize> {
|
||||||
|
self.clip(kind).map(|clip| clip.frames.len())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Component, Clone, Debug, Default, Hash)]
|
#[derive(Component, Clone, Debug, Default, Hash)]
|
||||||
@@ -411,7 +414,9 @@ impl FighterBoneMatrices {
|
|||||||
previous.kind == animation_frame.kind
|
previous.kind == animation_frame.kind
|
||||||
&& previous.frame.saturating_add(1) == animation_frame.frame
|
&& 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;
|
delta = delta + root_motion_cycle_delta;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use crate::fighter::FighterFacingDirection;
|
|||||||
use crate::input::FighterCommands;
|
use crate::input::FighterCommands;
|
||||||
use crate::math::int::FGi32;
|
use crate::math::int::FGi32;
|
||||||
use crate::math::vec::FGVec2;
|
use crate::math::vec::FGVec2;
|
||||||
use crate::stage::line::{StageLedgeSide, StageLineID, StageVertexID};
|
use crate::stage::line::{StageLedgeSide, StageVertexID};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Hash)]
|
#[derive(Debug, Clone, Hash)]
|
||||||
pub enum LedgeAction {
|
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);
|
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() {
|
if normalized_root_position.x.is_positive() && normalized_root_position.y.is_positive() {
|
||||||
self.is_in_grounded_phase = true;
|
let Some((segment_id, _)) = state_context
|
||||||
|
.stage_collision
|
||||||
let segment_id = StageLineID {
|
.find_ledge_ground_segment(self.ledge_id)
|
||||||
polygon: self.ledge_id.polygon,
|
else {
|
||||||
segment: self.ledge_id.vertex
|
|
||||||
};
|
|
||||||
let Some(poly) = state_context.stage_collision.get_segment(segment_id) else {
|
|
||||||
return;
|
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);
|
state_context.surface.land(segment_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,4 +134,3 @@ impl FighterStateImpl for LedgeActionState {
|
|||||||
self.is_in_grounded_phase
|
self.is_in_grounded_phase
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ use crate::fighter::animation::AnimKind;
|
|||||||
use crate::fighter::state::air::AirborneStateCommon;
|
use crate::fighter::state::air::AirborneStateCommon;
|
||||||
use crate::fighter::state::fall::FallState;
|
use crate::fighter::state::fall::FallState;
|
||||||
use crate::fighter::state::FighterState;
|
use crate::fighter::state::FighterState;
|
||||||
use crate::fighter::FighterFacingDirection;
|
|
||||||
use crate::fighter::state::jump::JumpStateInitiator;
|
use crate::fighter::state::jump::JumpStateInitiator;
|
||||||
use crate::fighter::state::ledge_action::{LedgeActionState};
|
use crate::fighter::state::ledge_action::{LedgeActionState};
|
||||||
|
use crate::fighter::{FighterFacingDirection, FighterInvulnerabilityState};
|
||||||
use crate::input::FighterCommands;
|
use crate::input::FighterCommands;
|
||||||
use crate::math::vec::FGVec2;
|
use crate::math::vec::FGVec2;
|
||||||
use crate::stage::line::{StageLedgeSide, StageVertexID};
|
use crate::stage::line::{StageLedgeSide, StageVertexID};
|
||||||
@@ -58,6 +58,14 @@ impl FighterStateImpl for LedgeWaitState {
|
|||||||
fn on_enter(&mut self, state_context: &mut EnterContext) {
|
fn on_enter(&mut self, state_context: &mut EnterContext) {
|
||||||
state_context.velocity.0 = FGVec2::ZERO;
|
state_context.velocity.0 = FGVec2::ZERO;
|
||||||
state_context.knockback_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 {
|
let Some(ledge) = state_context.stage_collision.find_ledge_by_vertex(self.ledge_id) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -127,4 +135,3 @@ fn is_ledge_drop_input(
|
|||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -195,6 +195,10 @@ impl EnterContext<'_> {
|
|||||||
+ animation_position_to_gameplay(position, self.facing_direction),
|
+ 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> {
|
pub struct ExitContext<'a> {
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ pub struct FighterSettingsCommon {
|
|||||||
pub intangible_pulse_duration: u32,
|
pub intangible_pulse_duration: u32,
|
||||||
|
|
||||||
pub angel_platform_invulnerability_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
|
/// When hit while the receiver is in the air, use this angle
|
||||||
pub sakurai_angle_aerial_angle: FGi32,
|
pub sakurai_angle_aerial_angle: FGi32,
|
||||||
|
|||||||
@@ -95,6 +95,32 @@ impl StageCollision {
|
|||||||
self.ledges.iter().find(|ledge| ledge.id == vertex)
|
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> {
|
pub fn get_segment(&self, segment: StageLineID) -> Option<&StagePolyLineSegment> {
|
||||||
self.stage_polys.get(segment.polygon).map(|poly| poly.segments.get(segment.segment))?
|
self.stage_polys.get(segment.polygon).map(|poly| poly.segments.get(segment.segment))?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -428,6 +428,7 @@ impl StageCameraProfile {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::stage::line::StageLineID;
|
||||||
|
|
||||||
fn platform(ledges: Vec<StageLedgeDefinition>) -> StagePolyDefinition {
|
fn platform(ledges: Vec<StageLedgeDefinition>) -> StagePolyDefinition {
|
||||||
StagePolyDefinition {
|
StagePolyDefinition {
|
||||||
@@ -461,6 +462,35 @@ mod tests {
|
|||||||
assert_eq!(ledges[1].position, FGVec2::lit("5", "0"));
|
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]
|
#[test]
|
||||||
fn invalid_and_duplicate_ledge_vertices_are_rejected() {
|
fn invalid_and_duplicate_ledge_vertices_are_rejected() {
|
||||||
let missing = platform(vec![StageLedgeDefinition {
|
let missing = platform(vec![StageLedgeDefinition {
|
||||||
|
|||||||
Reference in New Issue
Block a user