Initial work on ledge & improved baking of root motion. (#90)
Reviewed-on: #90 Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
This commit was merged in pull request #90.
This commit is contained in:
+189
-2
@@ -9,7 +9,10 @@ use strum_macros::EnumIter;
|
||||
use crate::{
|
||||
AppState, game_settings::GameSettings, math::{vec::FGVec2}, stage::{
|
||||
StagePoly,
|
||||
line::{StageCollision, StagePolyLineSegmentType, StagePolyType},
|
||||
line::{
|
||||
StageCollision, StageLedge, StageLedgeSide, StagePolyLineSegment,
|
||||
StagePolyLineSegmentType, StagePolyType, StageVertexID,
|
||||
},
|
||||
}, texture_reference::TextureReference,
|
||||
};
|
||||
use bevy::prelude::*;
|
||||
@@ -71,6 +74,16 @@ pub enum StageId {
|
||||
pub struct StagePolyDefinition {
|
||||
poly_type: StagePolyType,
|
||||
lines: Vec<(FGVec2, StagePolyLineSegmentType)>,
|
||||
#[serde(default)]
|
||||
ledges: Vec<StageLedgeDefinition>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Reflect)]
|
||||
pub struct StageLedgeDefinition {
|
||||
/// Index into the polygon's `lines`, identifying the corner position.
|
||||
vertex: usize,
|
||||
/// Side of the corner on which a grabbing fighter will hang.
|
||||
hang_side: StageLedgeSide,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Asset, Reflect, RonAsset)]
|
||||
@@ -176,6 +189,13 @@ pub fn prepare_stage_manifests(
|
||||
);
|
||||
return;
|
||||
}
|
||||
if let Err(error) = manifest.validate_ledges() {
|
||||
fail(
|
||||
&mut next_state,
|
||||
&format!("{:?} has invalid ledges: {error}", manifest.id),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if registry.insert(manifest.id, handle.clone()).is_some() {
|
||||
fail(
|
||||
&mut next_state,
|
||||
@@ -267,6 +287,14 @@ impl StageManifest {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_ledges(&self) -> Result<(), String> {
|
||||
for (polygon_index, definition) in self.polygons.iter().enumerate() {
|
||||
definition.validate_ledges(polygon_index)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn to_collision(&self) -> StageCollision {
|
||||
let stage_polys: Vec<StagePoly> = self
|
||||
.polygons
|
||||
@@ -274,7 +302,84 @@ impl StageManifest {
|
||||
.map(|def| StagePoly::build(def.poly_type, &def.lines))
|
||||
.collect();
|
||||
|
||||
StageCollision { stage_polys }
|
||||
let ledges = self
|
||||
.polygons
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(polygon, definition)| definition.resolved_ledges(polygon))
|
||||
.collect();
|
||||
|
||||
StageCollision {
|
||||
stage_polys,
|
||||
ledges,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StagePolyDefinition {
|
||||
fn validate_ledges(&self, polygon_index: usize) -> Result<(), String> {
|
||||
let polygon = StagePoly::build(self.poly_type, &self.lines);
|
||||
let mut vertices = HashSet::with_capacity(self.ledges.len());
|
||||
|
||||
for ledge in &self.ledges {
|
||||
if ledge.vertex >= self.lines.len() {
|
||||
return Err(format!(
|
||||
"polygon {polygon_index} references missing vertex {}",
|
||||
ledge.vertex
|
||||
));
|
||||
}
|
||||
if !vertices.insert(ledge.vertex) {
|
||||
return Err(format!(
|
||||
"polygon {polygon_index} defines vertex {} as a ledge more than once",
|
||||
ledge.vertex
|
||||
));
|
||||
}
|
||||
|
||||
let outgoing = polygon.get_segment(ledge.vertex);
|
||||
let incoming_index = if ledge.vertex > 0 {
|
||||
Some(ledge.vertex - 1)
|
||||
} else if matches!(self.poly_type, StagePolyType::Closed) {
|
||||
polygon.segments.len().checked_sub(1)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let incoming = incoming_index.and_then(|index| polygon.get_segment(index));
|
||||
let is_walkable_floor = |segment: Option<&StagePolyLineSegment>| {
|
||||
segment.is_some_and(|segment| {
|
||||
segment.segment_type == StagePolyLineSegmentType::Floor
|
||||
&& segment.normal.y > crate::math::int::FGi32::ZERO
|
||||
})
|
||||
};
|
||||
let (interior, exterior) = match ledge.hang_side {
|
||||
StageLedgeSide::Left => (outgoing, incoming),
|
||||
StageLedgeSide::Right => (incoming, outgoing),
|
||||
};
|
||||
if !is_walkable_floor(interior) {
|
||||
return Err(format!(
|
||||
"polygon {polygon_index} vertex {} has no upward-facing floor opposite its {:?} hang side",
|
||||
ledge.vertex, ledge.hang_side
|
||||
));
|
||||
}
|
||||
if is_walkable_floor(exterior) {
|
||||
return Err(format!(
|
||||
"polygon {polygon_index} vertex {} is between two walkable floors rather than at an exposed corner",
|
||||
ledge.vertex
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolved_ledges(&self, polygon: usize) -> impl Iterator<Item = StageLedge> + '_ {
|
||||
self.ledges.iter().map(move |ledge| StageLedge {
|
||||
id: StageVertexID {
|
||||
polygon,
|
||||
vertex: ledge.vertex,
|
||||
},
|
||||
position: self.lines[ledge.vertex].0,
|
||||
hang_side: ledge.hang_side,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,3 +424,85 @@ impl StageCameraProfile {
|
||||
< 90.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn platform(ledges: Vec<StageLedgeDefinition>) -> StagePolyDefinition {
|
||||
StagePolyDefinition {
|
||||
poly_type: StagePolyType::Platform,
|
||||
lines: vec![
|
||||
(FGVec2::lit("-5", "0"), StagePolyLineSegmentType::Floor),
|
||||
(FGVec2::lit("5", "0"), StagePolyLineSegmentType::Floor),
|
||||
],
|
||||
ledges,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn platform_endpoints_can_be_defined_as_ledges() {
|
||||
let definition = platform(vec![
|
||||
StageLedgeDefinition {
|
||||
vertex: 0,
|
||||
hang_side: StageLedgeSide::Left,
|
||||
},
|
||||
StageLedgeDefinition {
|
||||
vertex: 1,
|
||||
hang_side: StageLedgeSide::Right,
|
||||
},
|
||||
]);
|
||||
|
||||
assert_eq!(definition.validate_ledges(3), Ok(()));
|
||||
let ledges = definition.resolved_ledges(3).collect::<Vec<_>>();
|
||||
assert_eq!(ledges[0].id, StageVertexID { polygon: 3, vertex: 0 });
|
||||
assert_eq!(ledges[0].position, FGVec2::lit("-5", "0"));
|
||||
assert_eq!(ledges[1].id, StageVertexID { polygon: 3, vertex: 1 });
|
||||
assert_eq!(ledges[1].position, FGVec2::lit("5", "0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_and_duplicate_ledge_vertices_are_rejected() {
|
||||
let missing = platform(vec![StageLedgeDefinition {
|
||||
vertex: 2,
|
||||
hang_side: StageLedgeSide::Right,
|
||||
}]);
|
||||
assert!(missing.validate_ledges(0).is_err());
|
||||
|
||||
let duplicate = platform(vec![
|
||||
StageLedgeDefinition {
|
||||
vertex: 0,
|
||||
hang_side: StageLedgeSide::Left,
|
||||
},
|
||||
StageLedgeDefinition {
|
||||
vertex: 0,
|
||||
hang_side: StageLedgeSide::Right,
|
||||
},
|
||||
]);
|
||||
assert!(duplicate.validate_ledges(0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ledge_must_border_an_upward_facing_floor() {
|
||||
let definition = StagePolyDefinition {
|
||||
poly_type: StagePolyType::Platform,
|
||||
lines: vec![
|
||||
(FGVec2::lit("0", "0"), StagePolyLineSegmentType::Wall),
|
||||
(FGVec2::lit("0", "5"), StagePolyLineSegmentType::Wall),
|
||||
],
|
||||
ledges: vec![StageLedgeDefinition {
|
||||
vertex: 0,
|
||||
hang_side: StageLedgeSide::Left,
|
||||
}],
|
||||
};
|
||||
|
||||
assert!(definition.validate_ledges(0).is_err());
|
||||
|
||||
let wrong_side = platform(vec![StageLedgeDefinition {
|
||||
vertex: 0,
|
||||
hang_side: StageLedgeSide::Right,
|
||||
}]);
|
||||
assert!(wrong_side.validate_ledges(0).is_err());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user