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:
@@ -116,6 +116,7 @@ enum DebugFlag {
|
|||||||
AttackHitboxes,
|
AttackHitboxes,
|
||||||
Hurtboxes,
|
Hurtboxes,
|
||||||
Camera,
|
Camera,
|
||||||
|
LedgeGrab,
|
||||||
DrawThroughGeometry,
|
DrawThroughGeometry,
|
||||||
FrameStepping,
|
FrameStepping,
|
||||||
}
|
}
|
||||||
@@ -164,6 +165,10 @@ const FIGHTER_MENU: &[DebugMenuEntry] = &[
|
|||||||
label: "Attack hitboxes",
|
label: "Attack hitboxes",
|
||||||
flag: DebugFlag::AttackHitboxes,
|
flag: DebugFlag::AttackHitboxes,
|
||||||
},
|
},
|
||||||
|
DebugMenuEntry::Toggle {
|
||||||
|
label: "Ledge grab",
|
||||||
|
flag: DebugFlag::LedgeGrab,
|
||||||
|
},
|
||||||
DebugMenuEntry::Toggle {
|
DebugMenuEntry::Toggle {
|
||||||
label: "Hurtboxes",
|
label: "Hurtboxes",
|
||||||
flag: DebugFlag::Hurtboxes,
|
flag: DebugFlag::Hurtboxes,
|
||||||
@@ -260,6 +265,7 @@ pub struct DebugSettings {
|
|||||||
pub attack_hitboxes: bool,
|
pub attack_hitboxes: bool,
|
||||||
pub hurtboxes: bool,
|
pub hurtboxes: bool,
|
||||||
pub camera: bool,
|
pub camera: bool,
|
||||||
|
pub ledge_grab: bool,
|
||||||
pub draw_through_geometry: bool,
|
pub draw_through_geometry: bool,
|
||||||
pub motion_sampling: PresentationMotionMode,
|
pub motion_sampling: PresentationMotionMode,
|
||||||
pub game_speed: GameSpeedDebug,
|
pub game_speed: GameSpeedDebug,
|
||||||
@@ -283,6 +289,7 @@ impl Default for DebugSettings {
|
|||||||
attack_hitboxes: false,
|
attack_hitboxes: false,
|
||||||
hurtboxes: false,
|
hurtboxes: false,
|
||||||
camera: false,
|
camera: false,
|
||||||
|
ledge_grab: false,
|
||||||
draw_through_geometry: false,
|
draw_through_geometry: false,
|
||||||
motion_sampling: PresentationMotionMode::default(),
|
motion_sampling: PresentationMotionMode::default(),
|
||||||
game_speed: GameSpeedDebug::Full,
|
game_speed: GameSpeedDebug::Full,
|
||||||
@@ -350,6 +357,7 @@ impl DebugSettings {
|
|||||||
DebugFlag::Hurtboxes => self.hurtboxes,
|
DebugFlag::Hurtboxes => self.hurtboxes,
|
||||||
DebugFlag::DrawThroughGeometry => self.draw_through_geometry,
|
DebugFlag::DrawThroughGeometry => self.draw_through_geometry,
|
||||||
DebugFlag::Camera => self.camera,
|
DebugFlag::Camera => self.camera,
|
||||||
|
DebugFlag::LedgeGrab => self.ledge_grab,
|
||||||
DebugFlag::FrameStepping => self.frame_stepping,
|
DebugFlag::FrameStepping => self.frame_stepping,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -368,6 +376,7 @@ impl DebugSettings {
|
|||||||
self.draw_through_geometry = !self.draw_through_geometry
|
self.draw_through_geometry = !self.draw_through_geometry
|
||||||
}
|
}
|
||||||
DebugFlag::Camera => self.camera = !self.camera,
|
DebugFlag::Camera => self.camera = !self.camera,
|
||||||
|
DebugFlag::LedgeGrab => self.ledge_grab = !self.ledge_grab,
|
||||||
DebugFlag::FrameStepping => self.frame_stepping = !self.frame_stepping,
|
DebugFlag::FrameStepping => self.frame_stepping = !self.frame_stepping,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -926,6 +935,10 @@ pub fn camera_debug_enabled(settings: Res<DebugSettings>) -> bool {
|
|||||||
settings.camera
|
settings.camera
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn ledge_grab_enabled(settings: Res<DebugSettings>) -> bool {
|
||||||
|
settings.ledge_grab
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -87,6 +87,8 @@ pub enum AnimKind {
|
|||||||
CrouchEnter,
|
CrouchEnter,
|
||||||
CrouchIdle,
|
CrouchIdle,
|
||||||
CrouchExit,
|
CrouchExit,
|
||||||
|
LedgeWait,
|
||||||
|
LedgeJump,
|
||||||
|
|
||||||
PlatformPass,
|
PlatformPass,
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ struct FighterBakeSource {
|
|||||||
model_bytes: Vec<u8>,
|
model_bytes: Vec<u8>,
|
||||||
buffers: Vec<Vec<u8>>,
|
buffers: Vec<Vec<u8>>,
|
||||||
animations: HashMap<AnimKind, String>,
|
animations: HashMap<AnimKind, String>,
|
||||||
|
root_motion_bone: String,
|
||||||
}
|
}
|
||||||
#[derive(Default, TypePath)]
|
#[derive(Default, TypePath)]
|
||||||
struct FighterBakeSourceLoader;
|
struct FighterBakeSourceLoader;
|
||||||
@@ -82,6 +83,7 @@ impl AssetLoader for FighterBakeSourceLoader {
|
|||||||
model_bytes,
|
model_bytes,
|
||||||
buffers,
|
buffers,
|
||||||
animations: manifest.animations,
|
animations: manifest.animations,
|
||||||
|
root_motion_bone: manifest.root_motion_bone,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
fn extensions(&self) -> &[&str] {
|
fn extensions(&self) -> &[&str] {
|
||||||
@@ -123,7 +125,12 @@ impl AssetTransformer for FighterBakeTransformer {
|
|||||||
source: TransformedAsset<Self::AssetInput>,
|
source: TransformedAsset<Self::AssetInput>,
|
||||||
_: &'a Self::Settings,
|
_: &'a Self::Settings,
|
||||||
) -> Result<TransformedAsset<Self::AssetOutput>, Self::Error> {
|
) -> Result<TransformedAsset<Self::AssetOutput>, Self::Error> {
|
||||||
let baked = bake_gltf(&source.model_bytes, &source.buffers, &source.animations)?;
|
let baked = bake_gltf(
|
||||||
|
&source.model_bytes,
|
||||||
|
&source.buffers,
|
||||||
|
&source.animations,
|
||||||
|
&source.root_motion_bone,
|
||||||
|
)?;
|
||||||
Ok(source.replace_asset(baked))
|
Ok(source.replace_asset(baked))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,6 +208,7 @@ fn bake_gltf(
|
|||||||
bytes: &[u8],
|
bytes: &[u8],
|
||||||
buffer_bytes: &[Vec<u8>],
|
buffer_bytes: &[Vec<u8>],
|
||||||
animations: &HashMap<AnimKind, String>,
|
animations: &HashMap<AnimKind, String>,
|
||||||
|
root_motion_bone: &str,
|
||||||
) -> io::Result<BakedFighterAnimations> {
|
) -> io::Result<BakedFighterAnimations> {
|
||||||
let gltf = gltf::Gltf::from_slice(bytes)
|
let gltf = gltf::Gltf::from_slice(bytes)
|
||||||
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
|
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
|
||||||
@@ -228,13 +236,14 @@ fn bake_gltf(
|
|||||||
"glTF buffer data is incomplete",
|
"glTF buffer data is incomplete",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
bake_gltf_document(gltf.document, buffers, animations)
|
bake_gltf_document(gltf.document, buffers, animations, root_motion_bone)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn bake_gltf_document(
|
pub(super) fn bake_gltf_document(
|
||||||
document: gltf::Document,
|
document: gltf::Document,
|
||||||
buffers: Vec<gltf::buffer::Data>,
|
buffers: Vec<gltf::buffer::Data>,
|
||||||
animations: &HashMap<AnimKind, String>,
|
animations: &HashMap<AnimKind, String>,
|
||||||
|
root_motion_bone: &str,
|
||||||
) -> io::Result<BakedFighterAnimations> {
|
) -> io::Result<BakedFighterAnimations> {
|
||||||
let nodes: Vec<_> = document.nodes().collect();
|
let nodes: Vec<_> = document.nodes().collect();
|
||||||
if nodes.is_empty() {
|
if nodes.is_empty() {
|
||||||
@@ -260,6 +269,15 @@ pub(super) fn bake_gltf_document(
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect::<io::Result<Vec<_>>>()?;
|
.collect::<io::Result<Vec<_>>>()?;
|
||||||
|
let root_bone_index = names
|
||||||
|
.iter()
|
||||||
|
.position(|name| name == root_motion_bone)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
format!("fighter model has no root-motion bone named {root_motion_bone}"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
let mut pending = HashMap::<&str, Vec<AnimKind>>::new();
|
let mut pending = HashMap::<&str, Vec<AnimKind>>::new();
|
||||||
for (kind, name) in animations {
|
for (kind, name) in animations {
|
||||||
pending.entry(name).or_default().push(*kind);
|
pending.entry(name).or_default().push(*kind);
|
||||||
@@ -286,17 +304,28 @@ pub(super) fn bake_gltf_document(
|
|||||||
.fold(0.0_f32, f32::max);
|
.fold(0.0_f32, f32::max);
|
||||||
let frame_count = baked_frame_count(duration, BAKED_ANIMATION_FPS);
|
let frame_count = baked_frame_count(duration, BAKED_ANIMATION_FPS);
|
||||||
let mut frames = Vec::with_capacity(frame_count);
|
let mut frames = Vec::with_capacity(frame_count);
|
||||||
|
let mut root_positions = Vec::with_capacity(frame_count);
|
||||||
for frame in 0..frame_count {
|
for frame in 0..frame_count {
|
||||||
let time = (frame as f32 / BAKED_ANIMATION_FPS as f32).min(duration);
|
let time = (frame as f32 / BAKED_ANIMATION_FPS as f32).min(duration);
|
||||||
frames.push(
|
let local = nodes
|
||||||
nodes
|
.iter()
|
||||||
.iter()
|
.map(|node| local_transform(node.clone(), &channels, time))
|
||||||
.map(|node| local_transform(node.clone(), &channels, time))
|
.collect::<io::Result<Vec<_>>>()?;
|
||||||
.collect::<io::Result<Vec<_>>>()?,
|
let mut model = vec![None; local.len()];
|
||||||
);
|
let root = super::runtime::resolve_model_transform(
|
||||||
|
root_bone_index,
|
||||||
|
&parents,
|
||||||
|
&local,
|
||||||
|
&mut model,
|
||||||
|
)?;
|
||||||
|
root_positions.push(root.get_translation());
|
||||||
|
frames.push(local);
|
||||||
}
|
}
|
||||||
let index = clips.len();
|
let index = clips.len();
|
||||||
clips.push(BakedAnimationClip { frames });
|
clips.push(BakedAnimationClip {
|
||||||
|
frames,
|
||||||
|
root_positions,
|
||||||
|
});
|
||||||
for kind in kinds {
|
for kind in kinds {
|
||||||
clip_indices.insert(kind, index);
|
clip_indices.insert(kind, index);
|
||||||
}
|
}
|
||||||
@@ -322,6 +351,7 @@ pub(super) fn bake_gltf_document(
|
|||||||
frame_rate: BAKED_ANIMATION_FPS,
|
frame_rate: BAKED_ANIMATION_FPS,
|
||||||
bone_names: names,
|
bone_names: names,
|
||||||
parents,
|
parents,
|
||||||
|
root_bone_index,
|
||||||
clips,
|
clips,
|
||||||
clip_indices,
|
clip_indices,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -36,7 +36,13 @@ mod tests {
|
|||||||
))
|
))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let (document, buffers) = test_gltf(&manifest.animations);
|
let (document, buffers) = test_gltf(&manifest.animations);
|
||||||
let baked = bake_gltf_document(document, buffers, &manifest.animations).unwrap();
|
let baked = bake_gltf_document(
|
||||||
|
document,
|
||||||
|
buffers,
|
||||||
|
&manifest.animations,
|
||||||
|
&manifest.root_motion_bone,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(baked.frame_rate, BAKED_ANIMATION_FPS);
|
assert_eq!(baked.frame_rate, BAKED_ANIMATION_FPS);
|
||||||
assert_eq!(baked.frame_count(AnimKind::Wait), Some(121));
|
assert_eq!(baked.frame_count(AnimKind::Wait), Some(121));
|
||||||
@@ -51,6 +57,15 @@ mod tests {
|
|||||||
baked.bone_names.len()
|
baked.bone_names.len()
|
||||||
);
|
);
|
||||||
assert_eq!(baked.parents.len(), baked.bone_names.len());
|
assert_eq!(baked.parents.len(), baked.bone_names.len());
|
||||||
|
assert_eq!(baked.root_bone_index, 0);
|
||||||
|
assert_eq!(
|
||||||
|
baked.initial_root_position(AnimKind::Wait, FGi32::lit("2")),
|
||||||
|
Some(FGVec3::ZERO)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
baked.root_motion_cycle_delta(AnimKind::Wait, FGi32::lit("2")),
|
||||||
|
Some(FGVec3::lit("2", "0", "0"))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds the smallest glTF document that still exercises the complete
|
/// Builds the smallest glTF document that still exercises the complete
|
||||||
@@ -162,8 +177,14 @@ mod tests {
|
|||||||
frame_rate: BAKED_ANIMATION_FPS,
|
frame_rate: BAKED_ANIMATION_FPS,
|
||||||
bone_names: vec!["root".into()],
|
bone_names: vec!["root".into()],
|
||||||
parents: vec![None],
|
parents: vec![None],
|
||||||
|
root_bone_index: 0,
|
||||||
clips: vec![BakedAnimationClip {
|
clips: vec![BakedAnimationClip {
|
||||||
frames: vec![vec![first], vec![second], vec![terminal]],
|
frames: vec![vec![first], vec![second], vec![terminal]],
|
||||||
|
root_positions: vec![
|
||||||
|
FGVec3::ZERO,
|
||||||
|
FGVec3::lit("2", "0", "0"),
|
||||||
|
FGVec3::lit("4", "0", "0"),
|
||||||
|
],
|
||||||
}],
|
}],
|
||||||
clip_indices: HashMap::from([(AnimKind::Wait, 0)]),
|
clip_indices: HashMap::from([(AnimKind::Wait, 0)]),
|
||||||
};
|
};
|
||||||
@@ -339,7 +360,8 @@ mod tests {
|
|||||||
|
|
||||||
matrices.set_pose_with_frame(
|
matrices.set_pose_with_frame(
|
||||||
frame,
|
frame,
|
||||||
"root",
|
Some(0),
|
||||||
|
Some(FGVec3::lit("2", "0", "0")),
|
||||||
FGVec3::ZERO,
|
FGVec3::ZERO,
|
||||||
None,
|
None,
|
||||||
AnimKind::Wait,
|
AnimKind::Wait,
|
||||||
@@ -349,10 +371,15 @@ mod tests {
|
|||||||
FGi32::lit("2"),
|
FGi32::lit("2"),
|
||||||
);
|
);
|
||||||
assert_eq!(matrices.root_motion_delta(), FGVec3::ZERO);
|
assert_eq!(matrices.root_motion_delta(), FGVec3::ZERO);
|
||||||
|
assert_eq!(
|
||||||
|
matrices.root_motion_position(),
|
||||||
|
Some(FGVec3::lit("2", "0", "0"))
|
||||||
|
);
|
||||||
|
|
||||||
matrices.set_pose_with_frame(
|
matrices.set_pose_with_frame(
|
||||||
crate::fighter::animation::FighterAnimationFrame { frame: 1, ..frame },
|
crate::fighter::animation::FighterAnimationFrame { frame: 1, ..frame },
|
||||||
"root",
|
Some(0),
|
||||||
|
Some(FGVec3::lit("6", "0", "0")),
|
||||||
FGVec3::ZERO,
|
FGVec3::ZERO,
|
||||||
None,
|
None,
|
||||||
AnimKind::Wait,
|
AnimKind::Wait,
|
||||||
@@ -377,7 +404,8 @@ mod tests {
|
|||||||
|
|
||||||
matrices.set_pose_with_frame(
|
matrices.set_pose_with_frame(
|
||||||
frame,
|
frame,
|
||||||
"root",
|
Some(0),
|
||||||
|
Some(FGVec3::lit("3", "4", "5")),
|
||||||
FGVec3::ZERO,
|
FGVec3::ZERO,
|
||||||
None,
|
None,
|
||||||
AnimKind::Wait,
|
AnimKind::Wait,
|
||||||
@@ -396,6 +424,68 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_animation_extracts_root_translation_and_retains_its_position() {
|
||||||
|
let names = ["root".into(), "hand".into()];
|
||||||
|
let parents = [None, Some(0)];
|
||||||
|
let mut root = FixedMat4::IDENTITY;
|
||||||
|
root.translate(FGVec3::lit("3", "4", "5"));
|
||||||
|
let mut hand = FixedMat4::IDENTITY;
|
||||||
|
hand.translate(FGVec3::lit("1", "2", "0"));
|
||||||
|
let mut matrices = FighterBoneMatrices::default();
|
||||||
|
let frame = crate::fighter::animation::FighterAnimationFrame::new(
|
||||||
|
AnimKind::LedgeWait,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
matrices.set_pose_with_frame(
|
||||||
|
frame,
|
||||||
|
Some(0),
|
||||||
|
Some(FGVec3::lit("3", "4", "5")),
|
||||||
|
FGVec3::ZERO,
|
||||||
|
None,
|
||||||
|
AnimKind::LedgeWait,
|
||||||
|
&names,
|
||||||
|
&parents,
|
||||||
|
&[root, hand],
|
||||||
|
FGi32::ONE,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(matrices.root_motion_delta(), FGVec3::ZERO);
|
||||||
|
assert_eq!(
|
||||||
|
matrices.root_motion_position(),
|
||||||
|
Some(FGVec3::lit("3", "4", "5"))
|
||||||
|
);
|
||||||
|
assert_eq!(matrices.local(0).unwrap().get_translation(), FGVec3::ZERO);
|
||||||
|
assert_eq!(
|
||||||
|
matrices.get_current("root").unwrap().get_translation(),
|
||||||
|
FGVec3::ZERO
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
matrices.get_current("hand").unwrap().get_translation(),
|
||||||
|
FGVec3::lit("1", "2", "0")
|
||||||
|
);
|
||||||
|
|
||||||
|
root.translate(FGVec3::lit("6", "4", "5"));
|
||||||
|
matrices.set_pose_with_frame(
|
||||||
|
crate::fighter::animation::FighterAnimationFrame { frame: 1, ..frame },
|
||||||
|
Some(0),
|
||||||
|
Some(FGVec3::lit("6", "4", "5")),
|
||||||
|
FGVec3::ZERO,
|
||||||
|
None,
|
||||||
|
AnimKind::LedgeWait,
|
||||||
|
&names,
|
||||||
|
&parents,
|
||||||
|
&[root, hand],
|
||||||
|
FGi32::ONE,
|
||||||
|
);
|
||||||
|
assert_eq!(matrices.root_motion_delta(), FGVec3::lit("3", "0", "0"));
|
||||||
|
assert_eq!(
|
||||||
|
matrices.get_current("root").unwrap().get_translation(),
|
||||||
|
FGVec3::ZERO
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn repeating_root_motion_preserves_cycle_displacement_at_wrap() {
|
fn repeating_root_motion_preserves_cycle_displacement_at_wrap() {
|
||||||
let names = ["root".into()];
|
let names = ["root".into()];
|
||||||
@@ -411,7 +501,8 @@ mod tests {
|
|||||||
for (frame_number, x) in [(0, "0"), (1, "1")] {
|
for (frame_number, x) in [(0, "0"), (1, "1")] {
|
||||||
matrices.set_pose_with_frame(
|
matrices.set_pose_with_frame(
|
||||||
frame(frame_number),
|
frame(frame_number),
|
||||||
"root",
|
Some(0),
|
||||||
|
Some(FGVec3::lit(x, "0", "0")),
|
||||||
FGVec3::lit("2", "0", "0"),
|
FGVec3::lit("2", "0", "0"),
|
||||||
Some(2),
|
Some(2),
|
||||||
AnimKind::Wait,
|
AnimKind::Wait,
|
||||||
@@ -423,7 +514,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
matrices.set_pose_with_frame(
|
matrices.set_pose_with_frame(
|
||||||
frame(2),
|
frame(2),
|
||||||
"root",
|
Some(0),
|
||||||
|
Some(FGVec3::ZERO),
|
||||||
FGVec3::lit("2", "0", "0"),
|
FGVec3::lit("2", "0", "0"),
|
||||||
Some(2),
|
Some(2),
|
||||||
AnimKind::Wait,
|
AnimKind::Wait,
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ use crate::{
|
|||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct BakedAnimationClip {
|
pub struct BakedAnimationClip {
|
||||||
pub frames: Vec<Vec<FixedMat4>>,
|
pub frames: Vec<Vec<FixedMat4>>,
|
||||||
|
/// Unscaled model-space position of the configured root-motion bone at
|
||||||
|
/// each baked frame.
|
||||||
|
pub root_positions: Vec<FGVec3>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Asset, bevy::reflect::TypePath, Clone, Debug, Serialize, Deserialize)]
|
#[derive(Asset, bevy::reflect::TypePath, Clone, Debug, Serialize, Deserialize)]
|
||||||
@@ -29,11 +32,22 @@ pub struct BakedFighterAnimations {
|
|||||||
pub frame_rate: u32,
|
pub frame_rate: u32,
|
||||||
pub bone_names: Vec<String>,
|
pub bone_names: Vec<String>,
|
||||||
pub parents: Vec<Option<usize>>,
|
pub parents: Vec<Option<usize>>,
|
||||||
|
pub(super) root_bone_index: usize,
|
||||||
pub(super) clips: Vec<BakedAnimationClip>,
|
pub(super) clips: Vec<BakedAnimationClip>,
|
||||||
pub(super) clip_indices: HashMap<AnimKind, usize>,
|
pub(super) clip_indices: HashMap<AnimKind, usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BakedFighterAnimations {
|
impl BakedFighterAnimations {
|
||||||
|
fn sample_index(&self, frame: &FighterAnimationFrame) -> Option<usize> {
|
||||||
|
let frames = &self.clip(frame.kind)?.frames;
|
||||||
|
let frame_index = if frame.repeat {
|
||||||
|
let loop_frame_count = frames.len().saturating_sub(1).max(1) as u32;
|
||||||
|
frame.frame % loop_frame_count
|
||||||
|
} else {
|
||||||
|
frame.frame
|
||||||
|
};
|
||||||
|
Some((frame_index as usize).min(frames.len().saturating_sub(1)))
|
||||||
|
}
|
||||||
pub fn contains_bone(&self, bone: &str) -> bool {
|
pub fn contains_bone(&self, bone: &str) -> bool {
|
||||||
self.bone_names.iter().any(|name| name == bone)
|
self.bone_names.iter().any(|name| name == bone)
|
||||||
}
|
}
|
||||||
@@ -42,17 +56,20 @@ impl BakedFighterAnimations {
|
|||||||
self.bone_names.iter().all(|name| names.insert(name))
|
self.bone_names.iter().all(|name| names.insert(name))
|
||||||
}
|
}
|
||||||
pub fn sample_pose(&self, frame: &FighterAnimationFrame) -> Option<&[FixedMat4]> {
|
pub fn sample_pose(&self, frame: &FighterAnimationFrame) -> Option<&[FixedMat4]> {
|
||||||
let frames = &self.clip(frame.kind)?.frames;
|
let clip = self.clip(frame.kind)?;
|
||||||
let frame_index = if frame.repeat {
|
clip.frames.get(self.sample_index(frame)?).map(Vec::as_slice)
|
||||||
let loop_frame_count = frames.len().saturating_sub(1).max(1) as u32;
|
}
|
||||||
frame.frame % loop_frame_count
|
pub fn sample_root_position(
|
||||||
} else {
|
&self,
|
||||||
frame.frame
|
frame: &FighterAnimationFrame,
|
||||||
};
|
model_scale: FGi32,
|
||||||
frames
|
) -> Option<FGVec3> {
|
||||||
.get(frame_index as usize)
|
let clip = self.clip(frame.kind)?;
|
||||||
.or_else(|| frames.last())
|
let position = *clip.root_positions.get(self.sample_index(frame)?)?;
|
||||||
.map(Vec::as_slice)
|
Some(position * model_scale)
|
||||||
|
}
|
||||||
|
pub fn initial_root_position(&self, kind: AnimKind, model_scale: FGi32) -> Option<FGVec3> {
|
||||||
|
self.sample_root_position(&FighterAnimationFrame::new(kind, false), model_scale)
|
||||||
}
|
}
|
||||||
pub fn frame_count(&self, kind: AnimKind) -> Option<u32> {
|
pub fn frame_count(&self, kind: AnimKind) -> Option<u32> {
|
||||||
self.clip(kind).map(|clip| clip.frames.len() as u32)
|
self.clip(kind).map(|clip| clip.frames.len() as u32)
|
||||||
@@ -60,27 +77,10 @@ impl BakedFighterAnimations {
|
|||||||
pub fn root_motion_cycle_delta(
|
pub fn root_motion_cycle_delta(
|
||||||
&self,
|
&self,
|
||||||
kind: AnimKind,
|
kind: AnimKind,
|
||||||
bone: &str,
|
|
||||||
model_scale: FGi32,
|
model_scale: FGi32,
|
||||||
) -> Option<FGVec3> {
|
) -> Option<FGVec3> {
|
||||||
let last = self.frame_count(kind)?.saturating_sub(1);
|
let clip = self.clip(kind)?;
|
||||||
let first = FighterAnimationFrame {
|
Some((*clip.root_positions.last()? - *clip.root_positions.first()?) * model_scale)
|
||||||
kind,
|
|
||||||
frame: 0,
|
|
||||||
repeat: false,
|
|
||||||
};
|
|
||||||
let last = FighterAnimationFrame {
|
|
||||||
kind,
|
|
||||||
frame: last,
|
|
||||||
repeat: false,
|
|
||||||
};
|
|
||||||
Some(
|
|
||||||
self.sample_bone_model_transform(&last, bone, model_scale)?
|
|
||||||
.get_translation()
|
|
||||||
- self
|
|
||||||
.sample_bone_model_transform(&first, bone, model_scale)?
|
|
||||||
.get_translation(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/// Samples one bone in model space without depending on the runtime's
|
/// Samples one bone in model space without depending on the runtime's
|
||||||
/// previous-frame buffers. Tooling uses this when the user scrubs between
|
/// previous-frame buffers. Tooling uses this when the user scrubs between
|
||||||
@@ -124,13 +124,12 @@ pub struct FighterBoneMatrices {
|
|||||||
model_matrices: Vec<Option<FixedMat4>>,
|
model_matrices: Vec<Option<FixedMat4>>,
|
||||||
presentation_kind: Option<AnimKind>,
|
presentation_kind: Option<AnimKind>,
|
||||||
previous_animation_frame: Option<FighterAnimationFrame>,
|
previous_animation_frame: Option<FighterAnimationFrame>,
|
||||||
|
root_motion_position: Option<FGVec3>,
|
||||||
/// The raw, scaled model-space root translation used as the baseline for
|
/// The raw, scaled model-space root translation used as the baseline for
|
||||||
/// root-motion extraction. This is kept separately because the published
|
/// root-motion extraction. This is kept separately because the published
|
||||||
/// pose has the root translation removed.
|
/// pose has the root translation removed.
|
||||||
previous_root_motion_translation: Option<FGVec3>,
|
previous_root_motion_translation: Option<FGVec3>,
|
||||||
root_motion_delta: crate::math::vec3::FGVec3,
|
root_motion_delta: crate::math::vec3::FGVec3,
|
||||||
cached_root_motion_kind: Option<AnimKind>,
|
|
||||||
cached_root_motion_cycle_delta: FGVec3,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Component, Clone, Debug, Default)]
|
#[derive(Component, Clone, Debug, Default)]
|
||||||
@@ -149,7 +148,8 @@ impl FighterBoneMatrices {
|
|||||||
};
|
};
|
||||||
self.set_pose_with_frame(
|
self.set_pose_with_frame(
|
||||||
*frame,
|
*frame,
|
||||||
"root",
|
Some(baked.root_bone_index),
|
||||||
|
baked.sample_root_position(frame, model_scale),
|
||||||
FGVec3::ZERO,
|
FGVec3::ZERO,
|
||||||
None,
|
None,
|
||||||
frame.kind,
|
frame.kind,
|
||||||
@@ -203,6 +203,11 @@ impl FighterBoneMatrices {
|
|||||||
pub fn root_motion_delta(&self) -> FGVec3 {
|
pub fn root_motion_delta(&self) -> FGVec3 {
|
||||||
self.root_motion_delta
|
self.root_motion_delta
|
||||||
}
|
}
|
||||||
|
/// Current raw, scaled model-space position of the configured root-motion
|
||||||
|
/// bone. The published skeleton has this translation removed.
|
||||||
|
pub fn root_motion_position(&self) -> Option<FGVec3> {
|
||||||
|
self.root_motion_position
|
||||||
|
}
|
||||||
pub fn iter(&self) -> impl Iterator<Item = (&str, FixedMat4)> {
|
pub fn iter(&self) -> impl Iterator<Item = (&str, FixedMat4)> {
|
||||||
self.bone_names
|
self.bone_names
|
||||||
.iter()
|
.iter()
|
||||||
@@ -236,7 +241,8 @@ impl FighterBoneMatrices {
|
|||||||
) {
|
) {
|
||||||
self.set_pose_with_frame(
|
self.set_pose_with_frame(
|
||||||
FighterAnimationFrame::new(kind, false),
|
FighterAnimationFrame::new(kind, false),
|
||||||
"",
|
None,
|
||||||
|
None,
|
||||||
FGVec3::ZERO,
|
FGVec3::ZERO,
|
||||||
None,
|
None,
|
||||||
kind,
|
kind,
|
||||||
@@ -249,7 +255,8 @@ impl FighterBoneMatrices {
|
|||||||
pub(super) fn set_pose_with_frame(
|
pub(super) fn set_pose_with_frame(
|
||||||
&mut self,
|
&mut self,
|
||||||
animation_frame: FighterAnimationFrame,
|
animation_frame: FighterAnimationFrame,
|
||||||
root_motion_bone: &str,
|
root_bone_index: Option<usize>,
|
||||||
|
root_motion_position: Option<FGVec3>,
|
||||||
root_motion_cycle_delta: FGVec3,
|
root_motion_cycle_delta: FGVec3,
|
||||||
root_motion_loop_frame_count: Option<u32>,
|
root_motion_loop_frame_count: Option<u32>,
|
||||||
kind: AnimKind,
|
kind: AnimKind,
|
||||||
@@ -260,7 +267,8 @@ impl FighterBoneMatrices {
|
|||||||
) {
|
) {
|
||||||
self.sample_pose(
|
self.sample_pose(
|
||||||
animation_frame,
|
animation_frame,
|
||||||
root_motion_bone,
|
root_bone_index,
|
||||||
|
root_motion_position,
|
||||||
root_motion_cycle_delta,
|
root_motion_cycle_delta,
|
||||||
root_motion_loop_frame_count,
|
root_motion_loop_frame_count,
|
||||||
kind,
|
kind,
|
||||||
@@ -282,7 +290,8 @@ impl FighterBoneMatrices {
|
|||||||
) {
|
) {
|
||||||
self.resample_pose_with_frame(
|
self.resample_pose_with_frame(
|
||||||
FighterAnimationFrame::new(kind, false),
|
FighterAnimationFrame::new(kind, false),
|
||||||
"",
|
None,
|
||||||
|
None,
|
||||||
FGVec3::ZERO,
|
FGVec3::ZERO,
|
||||||
None,
|
None,
|
||||||
kind,
|
kind,
|
||||||
@@ -295,7 +304,8 @@ impl FighterBoneMatrices {
|
|||||||
pub(super) fn resample_pose_with_frame(
|
pub(super) fn resample_pose_with_frame(
|
||||||
&mut self,
|
&mut self,
|
||||||
animation_frame: FighterAnimationFrame,
|
animation_frame: FighterAnimationFrame,
|
||||||
root_motion_bone: &str,
|
root_bone_index: Option<usize>,
|
||||||
|
root_motion_position: Option<FGVec3>,
|
||||||
_root_motion_cycle_delta: FGVec3,
|
_root_motion_cycle_delta: FGVec3,
|
||||||
_root_motion_loop_frame_count: Option<u32>,
|
_root_motion_loop_frame_count: Option<u32>,
|
||||||
kind: AnimKind,
|
kind: AnimKind,
|
||||||
@@ -306,7 +316,8 @@ impl FighterBoneMatrices {
|
|||||||
) {
|
) {
|
||||||
self.sample_pose(
|
self.sample_pose(
|
||||||
animation_frame,
|
animation_frame,
|
||||||
root_motion_bone,
|
root_bone_index,
|
||||||
|
root_motion_position,
|
||||||
FGVec3::ZERO,
|
FGVec3::ZERO,
|
||||||
None,
|
None,
|
||||||
kind,
|
kind,
|
||||||
@@ -320,7 +331,8 @@ impl FighterBoneMatrices {
|
|||||||
fn sample_pose(
|
fn sample_pose(
|
||||||
&mut self,
|
&mut self,
|
||||||
animation_frame: FighterAnimationFrame,
|
animation_frame: FighterAnimationFrame,
|
||||||
root_motion_bone: &str,
|
root_index: Option<usize>,
|
||||||
|
raw_root_motion_translation: Option<FGVec3>,
|
||||||
root_motion_cycle_delta: FGVec3,
|
root_motion_cycle_delta: FGVec3,
|
||||||
root_motion_loop_frame_count: Option<u32>,
|
root_motion_loop_frame_count: Option<u32>,
|
||||||
kind: AnimKind,
|
kind: AnimKind,
|
||||||
@@ -360,16 +372,6 @@ impl FighterBoneMatrices {
|
|||||||
self.local_matrices.clear();
|
self.local_matrices.clear();
|
||||||
self.local_matrices.extend_from_slice(local);
|
self.local_matrices.extend_from_slice(local);
|
||||||
|
|
||||||
let root_index = if root_motion_bone.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(
|
|
||||||
self.bone_names
|
|
||||||
.iter()
|
|
||||||
.position(|name| name == root_motion_bone)
|
|
||||||
.expect("configured root motion bone should exist"),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
let model_scale = FixedMat4 {
|
let model_scale = FixedMat4 {
|
||||||
cols: [
|
cols: [
|
||||||
[model_scale, FGi32::ZERO, FGi32::ZERO, FGi32::ZERO],
|
[model_scale, FGi32::ZERO, FGi32::ZERO, FGi32::ZERO],
|
||||||
@@ -378,21 +380,8 @@ impl FighterBoneMatrices {
|
|||||||
[FGi32::ZERO, FGi32::ZERO, FGi32::ZERO, FGi32::ONE],
|
[FGi32::ZERO, FGi32::ZERO, FGi32::ZERO, FGi32::ONE],
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
// Root motion only needs the configured bone's model transform. Resolve
|
// Gameplay always owns root displacement. Keep rotation and scale in
|
||||||
// that ancestry first, then do the normal full-pose pass once below.
|
// the pose while removing the configured root bone's translation.
|
||||||
// Previously the complete hierarchy was resolved once before removing
|
|
||||||
// root translation and once afterwards.
|
|
||||||
let raw_root_motion_translation = root_index.map(|root_index| {
|
|
||||||
self.model_matrices.resize(local.len(), None);
|
|
||||||
self.model_matrices.fill(None);
|
|
||||||
let root = resolve_model_transform(root_index, parents, local, &mut self.model_matrices)
|
|
||||||
.expect("baked bone hierarchy should be valid");
|
|
||||||
model_scale.mul(root).get_translation()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Gameplay owns the root's displacement. Keep rotation and scale in
|
|
||||||
// the animation, but remove its translation before publishing the pose
|
|
||||||
// used by visuals and gameplay volumes.
|
|
||||||
if let Some(root_index) = root_index {
|
if let Some(root_index) = root_index {
|
||||||
self.local_matrices[root_index].translate(FGVec3::ZERO);
|
self.local_matrices[root_index].translate(FGVec3::ZERO);
|
||||||
}
|
}
|
||||||
@@ -419,7 +408,8 @@ impl FighterBoneMatrices {
|
|||||||
let mut delta = current - previous;
|
let mut delta = current - previous;
|
||||||
if animation_frame.repeat
|
if animation_frame.repeat
|
||||||
&& self.previous_animation_frame.is_some_and(|previous| {
|
&& self.previous_animation_frame.is_some_and(|previous| {
|
||||||
previous.frame.saturating_add(1) == animation_frame.frame
|
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 == Some(animation_frame.frame)
|
||||||
{
|
{
|
||||||
@@ -429,6 +419,7 @@ impl FighterBoneMatrices {
|
|||||||
} else {
|
} else {
|
||||||
FGVec3::ZERO
|
FGVec3::ZERO
|
||||||
};
|
};
|
||||||
|
self.root_motion_position = raw_root_motion_translation;
|
||||||
self.previous_root_motion_translation = raw_root_motion_translation;
|
self.previous_root_motion_translation = raw_root_motion_translation;
|
||||||
if advance_history {
|
if advance_history {
|
||||||
self.previous_animation_frame = Some(animation_frame);
|
self.previous_animation_frame = Some(animation_frame);
|
||||||
@@ -519,23 +510,18 @@ fn sample_fighter_bone_matrices(
|
|||||||
let manifest = manifests
|
let manifest = manifests
|
||||||
.get(&fighter.manifest)
|
.get(&fighter.manifest)
|
||||||
.expect("fighter manifest should be loaded before the match starts");
|
.expect("fighter manifest should be loaded before the match starts");
|
||||||
let root_motion_cycle_delta = if matrices.cached_root_motion_kind == Some(frame.kind) {
|
let root_motion_position = baked.sample_root_position(frame, manifest.model_scale);
|
||||||
matrices.cached_root_motion_cycle_delta
|
let root_motion_cycle_delta = baked
|
||||||
} else {
|
.root_motion_cycle_delta(frame.kind, manifest.model_scale)
|
||||||
let delta = baked
|
.unwrap_or(FGVec3::ZERO);
|
||||||
.root_motion_cycle_delta(frame.kind, &manifest.root_motion_bone, manifest.model_scale)
|
|
||||||
.unwrap_or(FGVec3::ZERO);
|
|
||||||
matrices.cached_root_motion_kind = Some(frame.kind);
|
|
||||||
matrices.cached_root_motion_cycle_delta = delta;
|
|
||||||
delta
|
|
||||||
};
|
|
||||||
let root_motion_loop_frame_count = baked
|
let root_motion_loop_frame_count = baked
|
||||||
.frame_count(frame.kind)
|
.frame_count(frame.kind)
|
||||||
.map(|count| count.saturating_sub(1).max(1));
|
.map(|count| count.saturating_sub(1).max(1));
|
||||||
if advance_history {
|
if advance_history {
|
||||||
matrices.set_pose_with_frame(
|
matrices.set_pose_with_frame(
|
||||||
*frame,
|
*frame,
|
||||||
&manifest.root_motion_bone,
|
Some(baked.root_bone_index),
|
||||||
|
root_motion_position,
|
||||||
root_motion_cycle_delta,
|
root_motion_cycle_delta,
|
||||||
root_motion_loop_frame_count,
|
root_motion_loop_frame_count,
|
||||||
frame.kind,
|
frame.kind,
|
||||||
@@ -547,7 +533,8 @@ fn sample_fighter_bone_matrices(
|
|||||||
} else {
|
} else {
|
||||||
matrices.resample_pose_with_frame(
|
matrices.resample_pose_with_frame(
|
||||||
*frame,
|
*frame,
|
||||||
&manifest.root_motion_bone,
|
Some(baked.root_bone_index),
|
||||||
|
root_motion_position,
|
||||||
FGVec3::ZERO,
|
FGVec3::ZERO,
|
||||||
None,
|
None,
|
||||||
frame.kind,
|
frame.kind,
|
||||||
@@ -568,7 +555,7 @@ fn sample_fighter_bone_matrices(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_model_transform(
|
pub(super) fn resolve_model_transform(
|
||||||
index: usize,
|
index: usize,
|
||||||
parents: &[Option<usize>],
|
parents: &[Option<usize>],
|
||||||
local: &[FixedMat4],
|
local: &[FixedMat4],
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
use crate::fighter::ecb::FighterPreviousECB;
|
use crate::fighter::ecb::FighterPreviousECB;
|
||||||
use crate::fighter::{FighterECB, FighterPreviousTranslation, FighterTranslation};
|
use crate::fighter::{FighterECB, FighterPreviousTranslation, FighterTranslation};
|
||||||
|
use crate::math::rect::FGRect;
|
||||||
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, StagePolyType, StageVertexID};
|
||||||
/*
|
/*
|
||||||
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)>,
|
||||||
@@ -83,6 +84,20 @@ pub fn air_collide_with_stage(
|
|||||||
.map(|(result, _)| result)
|
.map(|(result, _)| result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AirCollisionWithLedgeResult {
|
||||||
|
pub ledge: StageVertexID,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn air_collide_with_ledges(
|
||||||
|
ledge_box: &FGRect,
|
||||||
|
stage_collision: &StageCollision,
|
||||||
|
) -> Option<AirCollisionWithLedgeResult> {
|
||||||
|
stage_collision.intersect_ledges(ledge_box).map(|result| {
|
||||||
|
AirCollisionWithLedgeResult { ledge: result.vertex_id }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -115,6 +130,7 @@ mod tests {
|
|||||||
fn airborne_collision_skips_ignored_polygons() {
|
fn airborne_collision_skips_ignored_polygons() {
|
||||||
let stage_collision = StageCollision {
|
let stage_collision = StageCollision {
|
||||||
stage_polys: vec![horizontal_platform("0"), horizontal_platform("1")],
|
stage_polys: vec![horizontal_platform("0"), horizontal_platform("1")],
|
||||||
|
..StageCollision::default()
|
||||||
};
|
};
|
||||||
let previous_translation = FighterPreviousTranslation(FGVec2::lit("0", "2"));
|
let previous_translation = FighterPreviousTranslation(FGVec2::lit("0", "2"));
|
||||||
let translation = FighterTranslation(FGVec2::lit("0", "-1"));
|
let translation = FighterTranslation(FGVec2::lit("0", "-1"));
|
||||||
@@ -150,6 +166,7 @@ mod tests {
|
|||||||
fn airborne_collision_skips_soft_platforms_while_passing_down() {
|
fn airborne_collision_skips_soft_platforms_while_passing_down() {
|
||||||
let stage_collision = StageCollision {
|
let stage_collision = StageCollision {
|
||||||
stage_polys: vec![horizontal_platform("0")],
|
stage_polys: vec![horizontal_platform("0")],
|
||||||
|
..StageCollision::default()
|
||||||
};
|
};
|
||||||
let previous_translation = FighterPreviousTranslation(FGVec2::lit("0", "2"));
|
let previous_translation = FighterPreviousTranslation(FGVec2::lit("0", "2"));
|
||||||
let translation = FighterTranslation(FGVec2::lit("0", "-1"));
|
let translation = FighterTranslation(FGVec2::lit("0", "-1"));
|
||||||
@@ -197,6 +214,7 @@ mod tests {
|
|||||||
fn airborne_collision_still_hits_solid_floors_while_passing_down() {
|
fn airborne_collision_still_hits_solid_floors_while_passing_down() {
|
||||||
let stage_collision = StageCollision {
|
let stage_collision = StageCollision {
|
||||||
stage_polys: vec![solid_floor()],
|
stage_polys: vec![solid_floor()],
|
||||||
|
..StageCollision::default()
|
||||||
};
|
};
|
||||||
let previous_translation = FighterPreviousTranslation(FGVec2::lit("0", "2"));
|
let previous_translation = FighterPreviousTranslation(FGVec2::lit("0", "2"));
|
||||||
let translation = FighterTranslation(FGVec2::lit("0", "-1"));
|
let translation = FighterTranslation(FGVec2::lit("0", "-1"));
|
||||||
|
|||||||
@@ -216,6 +216,26 @@ pub fn hurtbox_debug(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn ledge_grab_debug(query: Query<(&FighterTranslation, &Fighter)>, fighters: Res<Assets<FighterManifest>>, mut gizmos: Gizmos) {
|
||||||
|
for (translation, fighter) in query {
|
||||||
|
let Some(manifest) = fighters.get(&fighter.manifest) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let start_2d = translation.0 + manifest.ledge_grab_box_start;
|
||||||
|
let start_2d = Vec2::new(start_2d.x.to_num(), start_2d.y.to_num());
|
||||||
|
|
||||||
|
let end_2d = translation.0 + manifest.ledge_grab_box_end;
|
||||||
|
let end_2d = Vec2::new(end_2d.x.to_num(), end_2d.y.to_num());
|
||||||
|
|
||||||
|
|
||||||
|
let center = (start_2d + end_2d) * 0.5;
|
||||||
|
let size = (start_2d - end_2d).abs();
|
||||||
|
|
||||||
|
gizmos.rect(Vec3::new(center.x, center.y, 0.0), size, bevy::color::palettes::css::BLUE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*fn draw_affine_capsule(
|
/*fn draw_affine_capsule(
|
||||||
gizmos: &mut Gizmos,
|
gizmos: &mut Gizmos,
|
||||||
capsule: FixedAffineCapsule,
|
capsule: FixedAffineCapsule,
|
||||||
|
|||||||
@@ -8,11 +8,7 @@ use strum::IntoEnumIterator;
|
|||||||
use strum_macros::EnumIter;
|
use strum_macros::EnumIter;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AppState,
|
AppState, fighter::{FighterAttributes, FighterCameraProfile, animation::AnimKind, attack::AttackKind}, game_settings::GameSettings, math::{int::FGi32, vec::FGVec2, vec3::FGVec3}, texture_reference::TextureReference,
|
||||||
fighter::{FighterAttributes, FighterCameraProfile, animation::AnimKind, attack::AttackKind},
|
|
||||||
game_settings::GameSettings,
|
|
||||||
math::{int::FGi32, vec3::FGVec3},
|
|
||||||
texture_reference::TextureReference,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
@@ -69,6 +65,8 @@ pub struct FighterManifest {
|
|||||||
pub attack_scripts: HashMap<AttackKind, String>,
|
pub attack_scripts: HashMap<AttackKind, String>,
|
||||||
pub hurtboxes: Vec<FighterHurtbox>,
|
pub hurtboxes: Vec<FighterHurtbox>,
|
||||||
pub ecb_bones: Vec<String>,
|
pub ecb_bones: Vec<String>,
|
||||||
|
pub ledge_grab_box_start: FGVec2,
|
||||||
|
pub ledge_grab_box_end: FGVec2,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_root_motion_bone() -> String {
|
fn default_root_motion_bone() -> String {
|
||||||
|
|||||||
+64
-1
@@ -86,6 +86,50 @@ pub struct FighterHitstop {
|
|||||||
pub remaining_frames: u32,
|
pub remaining_frames: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Component, Default, Copy, Clone, Hash, Debug, PartialEq, Eq)]
|
||||||
|
pub struct FighterLedgeGrabLockout {
|
||||||
|
pub remaining_frames: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FighterLedgeGrabLockout {
|
||||||
|
pub fn start(&mut self, duration_frames: u32) {
|
||||||
|
self.remaining_frames = duration_frames;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_active(&self) -> bool {
|
||||||
|
self.remaining_frames > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tick(&mut self) {
|
||||||
|
self.remaining_frames = self.remaining_frames.saturating_sub(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod ledge_grab_lockout_tests {
|
||||||
|
use super::FighterLedgeGrabLockout;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lockout_expires_after_the_requested_number_of_ticks() {
|
||||||
|
let mut lockout = FighterLedgeGrabLockout::default();
|
||||||
|
lockout.start(2);
|
||||||
|
|
||||||
|
assert!(lockout.is_active());
|
||||||
|
lockout.tick();
|
||||||
|
assert!(lockout.is_active());
|
||||||
|
lockout.tick();
|
||||||
|
assert!(!lockout.is_active());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zero_duration_does_not_enable_lockout() {
|
||||||
|
let mut lockout = FighterLedgeGrabLockout::default();
|
||||||
|
lockout.start(0);
|
||||||
|
|
||||||
|
assert!(!lockout.is_active());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Component, Default, Copy, Clone, Hash)]
|
#[derive(Component, Default, Copy, Clone, Hash)]
|
||||||
pub enum FighterInvulnerabilityState {
|
pub enum FighterInvulnerabilityState {
|
||||||
#[default]
|
#[default]
|
||||||
@@ -148,6 +192,7 @@ pub struct FighterPendingRespawn(bool);
|
|||||||
FighterPendingHitReaction,
|
FighterPendingHitReaction,
|
||||||
FighterPendingRespawn,
|
FighterPendingRespawn,
|
||||||
FighterHitstop,
|
FighterHitstop,
|
||||||
|
FighterLedgeGrabLockout,
|
||||||
FighterKnockbackVelocity,
|
FighterKnockbackVelocity,
|
||||||
FighterTranslation,
|
FighterTranslation,
|
||||||
FighterInvulnerabilityState,
|
FighterInvulnerabilityState,
|
||||||
@@ -193,7 +238,13 @@ impl Plugin for FighterPlugin {
|
|||||||
.add_systems(
|
.add_systems(
|
||||||
GgrsSchedule,
|
GgrsSchedule,
|
||||||
(
|
(
|
||||||
(blast_zones::apply_blast_zones, ecb::update_ecb, ecb::decrement_ecb_lockout, state::state_collision_interrupt_system).chain(),
|
(
|
||||||
|
blast_zones::apply_blast_zones,
|
||||||
|
ecb::update_ecb,
|
||||||
|
ecb::decrement_ecb_lockout,
|
||||||
|
state::state_collision_interrupt_system,
|
||||||
|
decay_ledge_grab_lockout,
|
||||||
|
).chain(),
|
||||||
baked_animation::resample_fighter_bone_matrices,
|
baked_animation::resample_fighter_bone_matrices,
|
||||||
)
|
)
|
||||||
.chain()
|
.chain()
|
||||||
@@ -247,6 +298,8 @@ impl Plugin for FighterPlugin {
|
|||||||
.checksum_component_with_hash::<FighterPendingRespawn>()
|
.checksum_component_with_hash::<FighterPendingRespawn>()
|
||||||
.rollback_component_with_copy::<FighterHitstop>()
|
.rollback_component_with_copy::<FighterHitstop>()
|
||||||
.checksum_component_with_hash::<FighterHitstop>()
|
.checksum_component_with_hash::<FighterHitstop>()
|
||||||
|
.rollback_component_with_copy::<FighterLedgeGrabLockout>()
|
||||||
|
.checksum_component_with_hash::<FighterLedgeGrabLockout>()
|
||||||
.rollback_component_with_copy::<FighterInvulnerabilityState>()
|
.rollback_component_with_copy::<FighterInvulnerabilityState>()
|
||||||
.checksum_component_with_hash::<FighterInvulnerabilityState>()
|
.checksum_component_with_hash::<FighterInvulnerabilityState>()
|
||||||
.rollback_component_with_copy::<FighterKnockbackVelocity>()
|
.rollback_component_with_copy::<FighterKnockbackVelocity>()
|
||||||
@@ -258,6 +311,10 @@ impl Plugin for FighterPlugin {
|
|||||||
FixedPostUpdate,
|
FixedPostUpdate,
|
||||||
debug::debug_draw_ecb.run_if(crate::debug_tools::ecb_enabled),
|
debug::debug_draw_ecb.run_if(crate::debug_tools::ecb_enabled),
|
||||||
)
|
)
|
||||||
|
.add_systems(
|
||||||
|
FixedPostUpdate,
|
||||||
|
debug::ledge_grab_debug.run_if(crate::debug_tools::ledge_grab_enabled),
|
||||||
|
)
|
||||||
.add_systems(
|
.add_systems(
|
||||||
EguiPrimaryContextPass,
|
EguiPrimaryContextPass,
|
||||||
debug::fighter_debug.run_if(crate::debug_tools::fighter_info_enabled),
|
debug::fighter_debug.run_if(crate::debug_tools::fighter_info_enabled),
|
||||||
@@ -373,6 +430,12 @@ pub fn decay_hitstop(query: Query<(&mut FighterHitstop, &mut FighterHitstopShake
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn decay_ledge_grab_lockout(query: Query<&mut FighterLedgeGrabLockout>) {
|
||||||
|
for mut lockout in query {
|
||||||
|
lockout.tick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn decay_invulnerability(query: Query<&mut FighterInvulnerabilityState>) {
|
pub fn decay_invulnerability(query: Query<&mut FighterInvulnerabilityState>) {
|
||||||
for invuln_state in query {
|
for invuln_state in query {
|
||||||
let invuln_state = invuln_state.into_inner();
|
let invuln_state = invuln_state.into_inner();
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ mod tests {
|
|||||||
(FGVec2::lit("30", "0"), StagePolyLineSegmentType::Floor),
|
(FGVec2::lit("30", "0"), StagePolyLineSegmentType::Floor),
|
||||||
],
|
],
|
||||||
)],
|
)],
|
||||||
|
..StageCollision::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::fighter::state::jump::{self};
|
use crate::fighter::state::jump::{self};
|
||||||
use crate::fighter::state::land::{LandingState, LandingType};
|
use crate::fighter::state::land::{LandingState, LandingType};
|
||||||
|
use crate::fighter::state::ledge_wait::LedgeWaitState;
|
||||||
use crate::fighter::state::{
|
use crate::fighter::state::{
|
||||||
CollisionContext, FighterState, InterruptContext, air_attack, air_dodge,
|
CollisionContext, FighterState, InterruptContext, air_attack, air_dodge,
|
||||||
};
|
};
|
||||||
@@ -126,6 +127,15 @@ pub fn collision_interrupt(
|
|||||||
landing_type: LandingType,
|
landing_type: LandingType,
|
||||||
can_ignore_soft_platforms: bool,
|
can_ignore_soft_platforms: bool,
|
||||||
) -> Option<FighterState> {
|
) -> Option<FighterState> {
|
||||||
|
if ctx.velocity.y <= FGi32::ZERO {
|
||||||
|
if let Some(ledge_result) = ctx.air_collide_with_ledges() {
|
||||||
|
return Some(FighterState::LedgeWait(LedgeWaitState {
|
||||||
|
airborne_state_common: AirborneStateCommon::default(),
|
||||||
|
ledge_id: ledge_result.ledge,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let res = ctx.air_collide_with_stage(can_ignore_soft_platforms)?;
|
let res = ctx.air_collide_with_stage(can_ignore_soft_platforms)?;
|
||||||
let diff = res.hit_position - ctx.translation.0;
|
let diff = res.hit_position - ctx.translation.0;
|
||||||
ctx.ecb.bottom -= diff;
|
ctx.ecb.bottom -= diff;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
//! ECS types so it stays cheap to reason about and to unit-test — it has to be
|
//! ECS types so it stays cheap to reason about and to unit-test — it has to be
|
||||||
//! bit-for-bit deterministic for rollback.
|
//! bit-for-bit deterministic for rollback.
|
||||||
|
|
||||||
|
use crate::fighter::state::air_dodge::AirDodgeState;
|
||||||
use crate::fighter::state::{
|
use crate::fighter::state::{
|
||||||
CollisionContext, FighterState, InterruptContext, air, crouch, dash, fall, ground_attack, turn, wait, walk,
|
CollisionContext, FighterState, InterruptContext, air, crouch, dash, fall, ground_attack, turn, wait, walk,
|
||||||
};
|
};
|
||||||
@@ -9,6 +10,7 @@ use crate::fighter::{
|
|||||||
FighterAttributes, FighterTranslation, FighterVelocity,
|
FighterAttributes, FighterTranslation, FighterVelocity,
|
||||||
};
|
};
|
||||||
use crate::game_settings::GameSettings;
|
use crate::game_settings::GameSettings;
|
||||||
|
use crate::input::FighterInput;
|
||||||
use crate::math::int::{FGWide, FGi32};
|
use crate::math::int::{FGWide, FGi32};
|
||||||
use crate::math::vec::FGVec2;
|
use crate::math::vec::FGVec2;
|
||||||
use crate::stage::line::{StageLineID, StagePoly, StagePolyLineSegment, StagePolyLineSegmentType};
|
use crate::stage::line::{StageLineID, StagePoly, StagePolyLineSegment, StagePolyLineSegmentType};
|
||||||
@@ -245,7 +247,8 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::fighter::ecb::FighterPreviousECB;
|
use crate::fighter::ecb::FighterPreviousECB;
|
||||||
use crate::fighter::{FighterECB, FighterPreviousTranslation, FighterSurfaceState};
|
use crate::fighter::{FighterECB, FighterPreviousTranslation, FighterSurfaceState};
|
||||||
use crate::stage::line::StagePolyType;
|
use crate::math::rect::FGRect;
|
||||||
|
use crate::stage::line::StagePolyType;
|
||||||
use crate::input::FighterInput;
|
use crate::input::FighterInput;
|
||||||
|
|
||||||
fn two_segment_floor() -> StagePoly {
|
fn two_segment_floor() -> StagePoly {
|
||||||
@@ -357,6 +360,7 @@ mod tests {
|
|||||||
fn grounded_collision_updates_surface_segment() {
|
fn grounded_collision_updates_surface_segment() {
|
||||||
let stage_collision = crate::stage::line::StageCollision {
|
let stage_collision = crate::stage::line::StageCollision {
|
||||||
stage_polys: vec![two_segment_floor()],
|
stage_polys: vec![two_segment_floor()],
|
||||||
|
..crate::stage::line::StageCollision::default()
|
||||||
};
|
};
|
||||||
let mut surface = FighterSurfaceState::Grounded {
|
let mut surface = FighterSurfaceState::Grounded {
|
||||||
line_id: StageLineID {
|
line_id: StageLineID {
|
||||||
@@ -387,6 +391,11 @@ mod tests {
|
|||||||
leave_ground_ecb_lock_duration: 10,
|
leave_ground_ecb_lock_duration: 10,
|
||||||
presentation_snap: &mut presentation_snap,
|
presentation_snap: &mut presentation_snap,
|
||||||
downward_stick_threshold: FGi32::ONE,
|
downward_stick_threshold: FGi32::ONE,
|
||||||
|
fighter_ledge_collision_box: FGRect {
|
||||||
|
origin: FGVec2::ZERO,
|
||||||
|
end: FGVec2::ZERO,
|
||||||
|
},
|
||||||
|
ledge_grab_lockout_active: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
@@ -412,6 +421,7 @@ mod tests {
|
|||||||
(FGVec2::lit("10", "0"), StagePolyLineSegmentType::Floor),
|
(FGVec2::lit("10", "0"), StagePolyLineSegmentType::Floor),
|
||||||
],
|
],
|
||||||
)],
|
)],
|
||||||
|
..crate::stage::line::StageCollision::default()
|
||||||
};
|
};
|
||||||
let mut surface = FighterSurfaceState::Grounded {
|
let mut surface = FighterSurfaceState::Grounded {
|
||||||
line_id: StageLineID {
|
line_id: StageLineID {
|
||||||
@@ -442,6 +452,11 @@ mod tests {
|
|||||||
leave_ground_ecb_lock_duration: 10,
|
leave_ground_ecb_lock_duration: 10,
|
||||||
presentation_snap: &mut presentation_snap,
|
presentation_snap: &mut presentation_snap,
|
||||||
downward_stick_threshold: FGi32::ONE,
|
downward_stick_threshold: FGi32::ONE,
|
||||||
|
fighter_ledge_collision_box: FGRect {
|
||||||
|
origin: FGVec2::ZERO,
|
||||||
|
end: FGVec2::ZERO,
|
||||||
|
},
|
||||||
|
ledge_grab_lockout_active: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
@@ -477,3 +492,27 @@ pub fn grounded_movement_standstill_common_interrupts(
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn perform_wavedash(input: &FighterInput, game_settings: &GameSettings) -> FighterState {
|
||||||
|
// If aiming sideways and in the deadzone, do a perfect wavedash
|
||||||
|
// TODO: Improve this so we don't have to go through the airborne state first
|
||||||
|
let mut wavedash_dir =
|
||||||
|
input
|
||||||
|
.get_last_frame()
|
||||||
|
.movement
|
||||||
|
.normalize_or_zero();
|
||||||
|
let movement = input.get_last_frame().movement;
|
||||||
|
if movement.y.is_zero() && !movement.x.is_zero() {
|
||||||
|
wavedash_dir.y =
|
||||||
|
-game_settings.input_common.stick_deadzone - FGi32::lit("-0.01");
|
||||||
|
wavedash_dir.x = movement.x.signum();
|
||||||
|
}
|
||||||
|
|
||||||
|
if wavedash_dir == FGVec2::ZERO {
|
||||||
|
wavedash_dir = FGVec2::lit("0.0", "-1.0");
|
||||||
|
}
|
||||||
|
|
||||||
|
FighterState::AirDodge(AirDodgeState {
|
||||||
|
direction: wavedash_dir.normalize_or_zero(),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
fighter::{
|
fighter::{
|
||||||
animation::AnimKind, state::{
|
animation::AnimKind, state::{
|
||||||
CollisionContext, EnterContext, FighterState, FighterStateImpl, InterruptContext, UpdateContext, air::{self, AirborneStateCommon}, air_dodge::AirDodgeState, ground, ground_attack,
|
CollisionContext, EnterContext, FighterState, FighterStateImpl, InterruptContext, UpdateContext, air::{self, AirborneStateCommon}, ground::{self}, ground_attack,
|
||||||
},
|
},
|
||||||
}, input::{DigitalAction, FighterCommands}, math::{int::FGi32, vec::FGVec2},
|
}, input::{DigitalAction, FighterCommands},
|
||||||
};
|
};
|
||||||
#[derive(Debug, Hash, Clone, Copy)]
|
#[derive(Debug, Hash, Clone, Copy)]
|
||||||
pub enum JumpType {
|
pub enum JumpType {
|
||||||
@@ -50,27 +50,7 @@ impl FighterStateImpl for JumpSquatState {
|
|||||||
}
|
}
|
||||||
// Special handling for wavedash from jump squat
|
// Special handling for wavedash from jump squat
|
||||||
if state_context.input.has_command(FighterCommands::Shield) {
|
if state_context.input.has_command(FighterCommands::Shield) {
|
||||||
// If aiming sideways and in the deadzone, do a perfect wavedash
|
Some(ground::perform_wavedash(state_context.input, state_context.game_settings))
|
||||||
// TODO: Improve this so we don't have to go through the airborne state first
|
|
||||||
let mut wavedash_dir = state_context
|
|
||||||
.input
|
|
||||||
.get_last_frame()
|
|
||||||
.movement
|
|
||||||
.normalize_or_zero();
|
|
||||||
let movement = state_context.input.get_last_frame().movement;
|
|
||||||
if movement.y.is_zero() && !movement.x.is_zero() {
|
|
||||||
wavedash_dir.y =
|
|
||||||
-state_context.game_settings.input_common.stick_deadzone - FGi32::lit("-0.01");
|
|
||||||
wavedash_dir.x = movement.x.signum();
|
|
||||||
}
|
|
||||||
|
|
||||||
if wavedash_dir == FGVec2::ZERO {
|
|
||||||
wavedash_dir = FGVec2::lit("0.0", "-1.0");
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(FighterState::AirDodge(AirDodgeState {
|
|
||||||
direction: wavedash_dir.normalize_or_zero(),
|
|
||||||
}))
|
|
||||||
} else if self.duration_counter >= state_context.fighter_attributes.jumpsquat_duration {
|
} else if self.duration_counter >= state_context.fighter_attributes.jumpsquat_duration {
|
||||||
let tap_jump_held = matches!(self.initiator, JumpStateInitiator::Tap)
|
let tap_jump_held = matches!(self.initiator, JumpStateInitiator::Tap)
|
||||||
&& state_context.input.get_last_frame().movement.y
|
&& state_context.input.get_last_frame().movement.y
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
use super::{CollisionContext, EnterContext, FighterStateImpl, InterruptContext, UpdateContext};
|
||||||
|
use crate::fighter::animation::AnimKind;
|
||||||
|
use crate::fighter::state::air::AirborneStateCommon;
|
||||||
|
use crate::fighter::state::jump::{JumpState, JumpStateInitiator, JumpType};
|
||||||
|
use crate::fighter::state::{FighterState, ground};
|
||||||
|
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};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Hash)]
|
||||||
|
pub enum LedgeAction {
|
||||||
|
Jump(JumpStateInitiator)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Hash)]
|
||||||
|
pub struct LedgeActionState {
|
||||||
|
pub ledge_id: StageVertexID,
|
||||||
|
pub ledge_action: LedgeAction,
|
||||||
|
pub is_in_grounded_phase: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LedgeActionState {
|
||||||
|
pub fn create_jump(ledge_id: StageVertexID, initiator: JumpStateInitiator) -> Self {
|
||||||
|
LedgeActionState {
|
||||||
|
ledge_id,
|
||||||
|
ledge_action: LedgeAction::Jump(initiator),
|
||||||
|
is_in_grounded_phase: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FighterStateImpl for LedgeActionState {
|
||||||
|
const NAME: &'static str = "LedgeAction";
|
||||||
|
fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option<FighterState> {
|
||||||
|
match &self.ledge_action {
|
||||||
|
LedgeAction::Jump(initiator) => {
|
||||||
|
if state_context.is_current_animation_finished() {
|
||||||
|
if state_context.input.has_command(FighterCommands::Shield) {
|
||||||
|
Some(ground::perform_wavedash(state_context.input, state_context.game_settings))
|
||||||
|
} else {
|
||||||
|
Some(FighterState::Jump(JumpState {
|
||||||
|
jump_type: JumpType::DoubleJump,
|
||||||
|
airborne_state_common: AirborneStateCommon::default(),
|
||||||
|
initiator: initiator.clone()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_enter(&mut self, state_context: &mut EnterContext) {
|
||||||
|
match self.ledge_action {
|
||||||
|
LedgeAction::Jump(_) => {
|
||||||
|
state_context.play_animation(AnimKind::LedgeJump, false);
|
||||||
|
state_context.input.enable_hold_all(10);
|
||||||
|
state_context.input.clear_command(FighterCommands::Jump);
|
||||||
|
state_context.input.clear_command(FighterCommands::TapJump);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
state_context.velocity.0 = FGVec2::ZERO;
|
||||||
|
state_context.knockback_velocity.0 = FGVec2::ZERO;
|
||||||
|
let Some(ledge) = state_context.stage_collision.find_ledge_by_vertex(self.ledge_id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
state_context.translation.0 = ledge.position;
|
||||||
|
*state_context.facing_direction = match ledge.hang_side {
|
||||||
|
StageLedgeSide::Left => FighterFacingDirection::Right,
|
||||||
|
StageLedgeSide::Right => FighterFacingDirection::Left,
|
||||||
|
};
|
||||||
|
if let Some(root_position) = state_context.initial_animation_root_position() {
|
||||||
|
state_context.translation.0 = root_position;
|
||||||
|
}
|
||||||
|
state_context.presentation_snap.0 = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, state_context: &mut UpdateContext) {
|
||||||
|
if !self.is_in_grounded_phase {
|
||||||
|
let root_pos = state_context.animation_root_position.unwrap_or_default();
|
||||||
|
let Some(ledge) = state_context.stage_collision.find_ledge_by_vertex(self.ledge_id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let ledge_pos = ledge.position;
|
||||||
|
let ledge_modifier = match ledge.hang_side {
|
||||||
|
StageLedgeSide::Left => FGi32::ONE,
|
||||||
|
StageLedgeSide::Right => FGi32::NEG_ONE,
|
||||||
|
};
|
||||||
|
|
||||||
|
let normalized_root_position = root_pos - ledge_pos;
|
||||||
|
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 {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
state_context.translation.0 = poly.segment.point1();
|
||||||
|
state_context.surface.land(segment_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.is_in_grounded_phase {
|
||||||
|
state_context.velocity.0.x = state_context.animation_root_motion_delta.x;
|
||||||
|
ground::apply_grounded_motion(
|
||||||
|
state_context.velocity,
|
||||||
|
state_context.translation,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if let Some(root_position) = state_context.animation_root_position {
|
||||||
|
state_context.translation.0 = root_position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_collision_interrupt(
|
||||||
|
&mut self,
|
||||||
|
_state_context: &mut CollisionContext,
|
||||||
|
) -> Option<FighterState> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_visually_grounded<'a>(&'a self) -> bool {
|
||||||
|
self.is_in_grounded_phase
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
use super::{CollisionContext, EnterContext, FighterStateImpl, InterruptContext, UpdateContext};
|
||||||
|
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::input::FighterCommands;
|
||||||
|
use crate::math::vec::FGVec2;
|
||||||
|
use crate::stage::line::{StageLedgeSide, StageVertexID};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Hash)]
|
||||||
|
pub struct LedgeWaitState {
|
||||||
|
pub airborne_state_common: AirborneStateCommon,
|
||||||
|
pub ledge_id: StageVertexID,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FighterStateImpl for LedgeWaitState {
|
||||||
|
const NAME: &'static str = "LedgeWait";
|
||||||
|
fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option<FighterState> {
|
||||||
|
if state_context.input.has_command(FighterCommands::Jump) {
|
||||||
|
return Some(FighterState::LedgeAction(LedgeActionState::create_jump(self.ledge_id, JumpStateInitiator::Button)));
|
||||||
|
} else if state_context.input.has_command(FighterCommands::TapJump) {
|
||||||
|
return Some(FighterState::LedgeAction(LedgeActionState::create_jump(self.ledge_id, JumpStateInitiator::Tap)));
|
||||||
|
}
|
||||||
|
if is_ledge_drop_input(state_context.input.get_last_frame().movement.x,
|
||||||
|
state_context.game_settings.input_common.run_stick_threshold,
|
||||||
|
state_context.facing_direction) {
|
||||||
|
state_context.ledge_grab_lockout.start(
|
||||||
|
state_context
|
||||||
|
.game_settings
|
||||||
|
.fighter_common
|
||||||
|
.ledge_drop_regrab_lockout_duration_frames,
|
||||||
|
);
|
||||||
|
return Some(FighterState::Fall(FallState::new(
|
||||||
|
AirborneStateCommon { is_fast_falling: false },
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if is_ledge_drop_fastfall_input(
|
||||||
|
state_context.input.get_last_frame().movement.y,
|
||||||
|
state_context.game_settings.input_common.run_stick_threshold,
|
||||||
|
) {
|
||||||
|
state_context.ledge_grab_lockout.start(
|
||||||
|
state_context
|
||||||
|
.game_settings
|
||||||
|
.fighter_common
|
||||||
|
.ledge_drop_regrab_lockout_duration_frames,
|
||||||
|
);
|
||||||
|
return Some(FighterState::Fall(FallState::new(
|
||||||
|
AirborneStateCommon { is_fast_falling: true },
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_enter(&mut self, state_context: &mut EnterContext) {
|
||||||
|
state_context.velocity.0 = FGVec2::ZERO;
|
||||||
|
state_context.knockback_velocity.0 = FGVec2::ZERO;
|
||||||
|
let Some(ledge) = state_context.stage_collision.find_ledge_by_vertex(self.ledge_id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
state_context.translation.0 = ledge.position;
|
||||||
|
*state_context.facing_direction = match ledge.hang_side {
|
||||||
|
StageLedgeSide::Left => FighterFacingDirection::Right,
|
||||||
|
StageLedgeSide::Right => FighterFacingDirection::Left,
|
||||||
|
};
|
||||||
|
state_context.play_animation(AnimKind::LedgeWait, true);
|
||||||
|
if let Some(root_position) = state_context.initial_animation_root_position() {
|
||||||
|
state_context.translation.0 = root_position;
|
||||||
|
}
|
||||||
|
state_context.presentation_snap.0 = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, state_context: &mut UpdateContext) {
|
||||||
|
if let Some(root_position) = state_context.animation_root_position {
|
||||||
|
state_context.translation.0 = root_position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_collision_interrupt(
|
||||||
|
&mut self,
|
||||||
|
_state_context: &mut CollisionContext,
|
||||||
|
) -> Option<FighterState> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_visually_grounded<'a>(&'a self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::math::int::FGi32;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ledge_drop_uses_the_run_stick_threshold() {
|
||||||
|
let threshold = FGi32::lit("0.625");
|
||||||
|
|
||||||
|
assert!(is_ledge_drop_fastfall_input(FGi32::lit("-0.625"), threshold));
|
||||||
|
assert!(is_ledge_drop_fastfall_input(FGi32::lit("-1.0"), threshold));
|
||||||
|
assert!(!is_ledge_drop_fastfall_input(FGi32::lit("-0.624"), threshold));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ledge_drop_fastfall_input(
|
||||||
|
vertical_input: crate::math::int::FGi32,
|
||||||
|
threshold: crate::math::int::FGi32,
|
||||||
|
) -> bool {
|
||||||
|
vertical_input <= -threshold
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ledge_drop_input(
|
||||||
|
horizontal_input: crate::math::int::FGi32,
|
||||||
|
threshold: crate::math::int::FGi32,
|
||||||
|
facing_dir: &FighterFacingDirection,
|
||||||
|
) -> bool {
|
||||||
|
let dir = facing_dir.to_sign();
|
||||||
|
if horizontal_input.abs() > threshold {
|
||||||
|
if dir != horizontal_input.signum() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
+176
-11
@@ -20,14 +20,16 @@ use crate::fighter::visual::FighterAnimations;
|
|||||||
use crate::fighter::{
|
use crate::fighter::{
|
||||||
Fighter, FighterDamage, FighterECB, FighterFacingDirection, FighterHitboxes, FighterHitstop,
|
Fighter, FighterDamage, FighterECB, FighterFacingDirection, FighterHitboxes, FighterHitstop,
|
||||||
FighterHitstopShake, FighterHitstopShakeKind, FighterHitstopShakeRole,
|
FighterHitstopShake, FighterHitstopShakeKind, FighterHitstopShakeRole,
|
||||||
FighterInvulnerabilityState, FighterPendingRespawn, FighterPresentationSnap,
|
FighterInvulnerabilityState, FighterLedgeGrabLockout, FighterPendingRespawn,
|
||||||
FighterPreviousTranslation, FighterSurfaceState, FighterTranslation, FighterVelocity,
|
FighterPresentationSnap, FighterPreviousTranslation, FighterSurfaceState, FighterTranslation,
|
||||||
|
FighterVelocity,
|
||||||
};
|
};
|
||||||
use crate::game_settings::GameSettings;
|
use crate::game_settings::GameSettings;
|
||||||
use crate::input::FighterInput;
|
use crate::input::FighterInput;
|
||||||
use crate::match_loading::MatchConfig;
|
use crate::match_loading::MatchConfig;
|
||||||
use crate::match_runtime::MatchState;
|
use crate::match_runtime::MatchState;
|
||||||
use crate::math::int::FGi32;
|
use crate::math::int::FGi32;
|
||||||
|
use crate::math::rect::FGRect;
|
||||||
use crate::math::vec::FGVec2;
|
use crate::math::vec::FGVec2;
|
||||||
use crate::math::vec3::FGVec3;
|
use crate::math::vec3::FGVec3;
|
||||||
use crate::player::Player;
|
use crate::player::Player;
|
||||||
@@ -52,6 +54,8 @@ pub mod run;
|
|||||||
pub mod turn;
|
pub mod turn;
|
||||||
pub mod wait;
|
pub mod wait;
|
||||||
pub mod walk;
|
pub mod walk;
|
||||||
|
pub mod ledge_wait;
|
||||||
|
pub mod ledge_action;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||||
pub enum SurfacePolicy {
|
pub enum SurfacePolicy {
|
||||||
@@ -135,6 +139,7 @@ pub struct AnimationStatus {
|
|||||||
pub struct InterruptContext<'a> {
|
pub struct InterruptContext<'a> {
|
||||||
pub input: &'a FighterInput,
|
pub input: &'a FighterInput,
|
||||||
pub facing_direction: &'a mut FighterFacingDirection,
|
pub facing_direction: &'a mut FighterFacingDirection,
|
||||||
|
pub ledge_grab_lockout: &'a mut FighterLedgeGrabLockout,
|
||||||
pub game_settings: &'a GameSettings,
|
pub game_settings: &'a GameSettings,
|
||||||
pub fighter_attributes: &'a FighterAttributes,
|
pub fighter_attributes: &'a FighterAttributes,
|
||||||
pub surface: &'a FighterSurfaceState,
|
pub surface: &'a FighterSurfaceState,
|
||||||
@@ -158,21 +163,38 @@ pub struct EnterContext<'a> {
|
|||||||
pub input: &'a mut FighterInput,
|
pub input: &'a mut FighterInput,
|
||||||
pub hitboxes: &'a mut FighterHitboxes,
|
pub hitboxes: &'a mut FighterHitboxes,
|
||||||
pub velocity: &'a mut FighterVelocity,
|
pub velocity: &'a mut FighterVelocity,
|
||||||
|
pub translation: &'a mut FighterTranslation,
|
||||||
pub knockback_velocity: &'a mut FighterKnockbackVelocity,
|
pub knockback_velocity: &'a mut FighterKnockbackVelocity,
|
||||||
pub stale_move_queue: &'a mut FighterStaleMoveQueue,
|
pub stale_move_queue: &'a mut FighterStaleMoveQueue,
|
||||||
pub facing_direction: &'a mut FighterFacingDirection,
|
pub facing_direction: &'a mut FighterFacingDirection,
|
||||||
pub animation_frame: &'a mut FighterAnimationFrame,
|
pub animation_frame: &'a mut FighterAnimationFrame,
|
||||||
|
pub presentation_snap: &'a mut FighterPresentationSnap,
|
||||||
pub surface: &'a mut FighterSurfaceState,
|
pub surface: &'a mut FighterSurfaceState,
|
||||||
pub vulnerability: &'a mut FighterInvulnerabilityState,
|
pub vulnerability: &'a mut FighterInvulnerabilityState,
|
||||||
pub fighter_attributes: &'a FighterAttributes,
|
pub fighter_attributes: &'a FighterAttributes,
|
||||||
pub attack_script_assets: &'a FighterAttackScriptAssets,
|
pub attack_script_assets: &'a FighterAttackScriptAssets,
|
||||||
pub game_settings: &'a GameSettings,
|
pub game_settings: &'a GameSettings,
|
||||||
|
pub stage_collision: &'a StageCollision,
|
||||||
|
baked_animations: &'a BakedFighterAnimations,
|
||||||
|
model_scale: FGi32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EnterContext<'_> {
|
impl EnterContext<'_> {
|
||||||
pub fn play_animation(&mut self, kind: AnimKind, repeat: bool) {
|
pub fn play_animation(&mut self, kind: AnimKind, repeat: bool) {
|
||||||
self.animation_frame.reset(kind, repeat);
|
self.animation_frame.reset(kind, repeat);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// World-space root position authored for frame zero of the animation most
|
||||||
|
/// recently selected with [`Self::play_animation`].
|
||||||
|
pub fn initial_animation_root_position(&self) -> Option<FGVec2> {
|
||||||
|
let position = self
|
||||||
|
.baked_animations
|
||||||
|
.initial_root_position(self.animation_frame.kind, self.model_scale)?;
|
||||||
|
Some(
|
||||||
|
self.translation.0
|
||||||
|
+ animation_position_to_gameplay(position, self.facing_direction),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ExitContext<'a> {
|
pub struct ExitContext<'a> {
|
||||||
@@ -195,18 +217,22 @@ pub struct UpdateContext<'a> {
|
|||||||
/// Root motion in the fighter's gameplay plane: X is world-right and Y is
|
/// Root motion in the fighter's gameplay plane: X is world-right and Y is
|
||||||
/// world-up. Animation +Z is forward and is mirrored by facing direction.
|
/// world-up. Animation +Z is forward and is mirrored by facing direction.
|
||||||
pub animation_root_motion_delta: FGVec2,
|
pub animation_root_motion_delta: FGVec2,
|
||||||
|
/// Current world-space root position implied by this animation frame.
|
||||||
|
/// States opt into root motion by assigning this to [`FighterTranslation`].
|
||||||
|
pub animation_root_position: Option<FGVec2>,
|
||||||
pub animation_frame: u32,
|
pub animation_frame: u32,
|
||||||
pub fighter_attributes: &'a FighterAttributes,
|
pub fighter_attributes: &'a FighterAttributes,
|
||||||
pub attack_scripts: &'a Assets<FighterAttackScript>,
|
pub attack_scripts: &'a Assets<FighterAttackScript>,
|
||||||
pub attack_script_assets: &'a FighterAttackScriptAssets,
|
pub attack_script_assets: &'a FighterAttackScriptAssets,
|
||||||
pub facing_direction: &'a FighterFacingDirection,
|
pub facing_direction: &'a FighterFacingDirection,
|
||||||
|
pub stage_collision: &'a StageCollision,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn animation_root_motion_to_gameplay(
|
fn animation_position_to_gameplay(
|
||||||
delta: FGVec3,
|
position: FGVec3,
|
||||||
facing_direction: &FighterFacingDirection,
|
facing_direction: &FighterFacingDirection,
|
||||||
) -> FGVec2 {
|
) -> FGVec2 {
|
||||||
FGVec2::new(delta.z * facing_direction.to_sign(), delta.y)
|
FGVec2::new(position.z * facing_direction.to_sign(), position.y)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpdateContext<'_> {
|
impl UpdateContext<'_> {
|
||||||
@@ -226,6 +252,8 @@ pub struct CollisionContext<'a> {
|
|||||||
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 presentation_snap: &'a mut FighterPresentationSnap,
|
||||||
|
pub fighter_ledge_collision_box: FGRect,
|
||||||
|
pub ledge_grab_lockout_active: bool,
|
||||||
pub downward_stick_threshold: FGi32,
|
pub downward_stick_threshold: FGi32,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,6 +275,17 @@ impl CollisionContext<'_> {
|
|||||||
ignore_soft_platforms,
|
ignore_soft_platforms,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
pub fn air_collide_with_ledges(
|
||||||
|
&self,
|
||||||
|
) -> Option<crate::fighter::collision::AirCollisionWithLedgeResult> {
|
||||||
|
if self.ledge_grab_lockout_active {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
crate::fighter::collision::air_collide_with_ledges(
|
||||||
|
&self.fighter_ledge_collision_box,
|
||||||
|
self.stage_collision,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fighter_states! {
|
fighter_states! {
|
||||||
@@ -260,6 +299,8 @@ fighter_states! {
|
|||||||
JumpSquat => jump::JumpSquatState,
|
JumpSquat => jump::JumpSquatState,
|
||||||
Jump => jump::JumpState,
|
Jump => jump::JumpState,
|
||||||
Land => land::LandingState,
|
Land => land::LandingState,
|
||||||
|
LedgeWait => ledge_wait::LedgeWaitState,
|
||||||
|
LedgeAction => ledge_action::LedgeActionState,
|
||||||
AirDodge => air_dodge::AirDodgeState,
|
AirDodge => air_dodge::AirDodgeState,
|
||||||
Turn => turn::TurnState,
|
Turn => turn::TurnState,
|
||||||
GroundAttack => ground_attack::GroundAttackState,
|
GroundAttack => ground_attack::GroundAttackState,
|
||||||
@@ -285,6 +326,7 @@ pub struct FighterFrameQuery {
|
|||||||
pub facing_direction: &'static mut FighterFacingDirection,
|
pub facing_direction: &'static mut FighterFacingDirection,
|
||||||
pub animation_frame: &'static mut FighterAnimationFrame,
|
pub animation_frame: &'static mut FighterAnimationFrame,
|
||||||
pub vulnerability: &'static mut FighterInvulnerabilityState,
|
pub vulnerability: &'static mut FighterInvulnerabilityState,
|
||||||
|
pub ledge_grab_lockout: &'static mut FighterLedgeGrabLockout,
|
||||||
pub fighter: &'static Fighter,
|
pub fighter: &'static Fighter,
|
||||||
pub bone_matrices: &'static FighterBoneMatrices,
|
pub bone_matrices: &'static FighterBoneMatrices,
|
||||||
}
|
}
|
||||||
@@ -320,6 +362,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
|
|||||||
InterruptContext {
|
InterruptContext {
|
||||||
input: &self.input,
|
input: &self.input,
|
||||||
facing_direction: &mut self.facing_direction,
|
facing_direction: &mut self.facing_direction,
|
||||||
|
ledge_grab_lockout: &mut self.ledge_grab_lockout,
|
||||||
game_settings,
|
game_settings,
|
||||||
fighter_attributes: attributes,
|
fighter_attributes: attributes,
|
||||||
animation,
|
animation,
|
||||||
@@ -335,20 +378,28 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
|
|||||||
attributes: &'a FighterAttributes,
|
attributes: &'a FighterAttributes,
|
||||||
attack_script_assets: &'a FighterAttackScriptAssets,
|
attack_script_assets: &'a FighterAttackScriptAssets,
|
||||||
game_settings: &'a GameSettings,
|
game_settings: &'a GameSettings,
|
||||||
|
stage_collision: &'a StageCollision,
|
||||||
|
baked_animations: &'a BakedFighterAnimations,
|
||||||
|
model_scale: FGi32,
|
||||||
) -> EnterContext<'a> {
|
) -> EnterContext<'a> {
|
||||||
EnterContext {
|
EnterContext {
|
||||||
input: &mut self.input,
|
input: &mut self.input,
|
||||||
hitboxes: &mut self.hitboxes,
|
hitboxes: &mut self.hitboxes,
|
||||||
velocity: &mut self.velocity,
|
velocity: &mut self.velocity,
|
||||||
knockback_velocity: &mut self.knockback_velocity,
|
knockback_velocity: &mut self.knockback_velocity,
|
||||||
|
translation: &mut self.translation,
|
||||||
stale_move_queue: &mut self.stale_move_queue,
|
stale_move_queue: &mut self.stale_move_queue,
|
||||||
facing_direction: &mut self.facing_direction,
|
facing_direction: &mut self.facing_direction,
|
||||||
animation_frame: &mut self.animation_frame,
|
animation_frame: &mut self.animation_frame,
|
||||||
surface: &mut self.surface,
|
surface: &mut self.surface,
|
||||||
vulnerability: &mut self.vulnerability,
|
vulnerability: &mut self.vulnerability,
|
||||||
|
presentation_snap: &mut self.presentation_snap,
|
||||||
fighter_attributes: attributes,
|
fighter_attributes: attributes,
|
||||||
attack_script_assets,
|
attack_script_assets,
|
||||||
game_settings,
|
game_settings,
|
||||||
|
stage_collision,
|
||||||
|
baked_animations,
|
||||||
|
model_scale,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,7 +418,16 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
|
|||||||
attributes: &'a FighterAttributes,
|
attributes: &'a FighterAttributes,
|
||||||
attack_scripts: &'a Assets<FighterAttackScript>,
|
attack_scripts: &'a Assets<FighterAttackScript>,
|
||||||
attack_script_assets: &'a FighterAttackScriptAssets,
|
attack_script_assets: &'a FighterAttackScriptAssets,
|
||||||
|
stage_collision: &'a StageCollision,
|
||||||
) -> UpdateContext<'a> {
|
) -> UpdateContext<'a> {
|
||||||
|
let animation_root_motion_delta = animation_position_to_gameplay(
|
||||||
|
self.bone_matrices.root_motion_delta(),
|
||||||
|
&self.facing_direction,
|
||||||
|
);
|
||||||
|
let animation_root_position = self
|
||||||
|
.bone_matrices
|
||||||
|
.root_motion_position()
|
||||||
|
.map(|_| self.translation.0 + animation_root_motion_delta);
|
||||||
UpdateContext {
|
UpdateContext {
|
||||||
translation: &mut self.translation,
|
translation: &mut self.translation,
|
||||||
prev_translation: &mut self.prev_translation,
|
prev_translation: &mut self.prev_translation,
|
||||||
@@ -377,24 +437,28 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
|
|||||||
surface: &mut self.surface,
|
surface: &mut self.surface,
|
||||||
knockback_velocity: &mut self.knockback_velocity,
|
knockback_velocity: &mut self.knockback_velocity,
|
||||||
game_settings,
|
game_settings,
|
||||||
animation_root_motion_delta: animation_root_motion_to_gameplay(
|
animation_root_motion_delta,
|
||||||
self.bone_matrices.root_motion_delta(),
|
animation_root_position,
|
||||||
&self.facing_direction,
|
|
||||||
),
|
|
||||||
animation_frame: self.animation_frame.frame,
|
animation_frame: self.animation_frame.frame,
|
||||||
vulnerability: &mut self.vulnerability,
|
vulnerability: &mut self.vulnerability,
|
||||||
fighter_attributes: attributes,
|
fighter_attributes: attributes,
|
||||||
attack_scripts,
|
attack_scripts,
|
||||||
attack_script_assets,
|
attack_script_assets,
|
||||||
facing_direction: &self.facing_direction,
|
facing_direction: &self.facing_direction,
|
||||||
|
stage_collision: &stage_collision,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collision_context<'a>(
|
fn collision_context<'a>(
|
||||||
&'a mut self,
|
&'a mut self,
|
||||||
stage_collision: &'a StageCollision,
|
stage_collision: &'a StageCollision,
|
||||||
|
fighter_manifest: &'a FighterManifest,
|
||||||
game_settings: &'a GameSettings,
|
game_settings: &'a GameSettings,
|
||||||
) -> CollisionContext<'a> {
|
) -> CollisionContext<'a> {
|
||||||
|
let fighter_ledge_collision_box = FGRect {
|
||||||
|
origin: self.translation.0 + fighter_manifest.ledge_grab_box_start,
|
||||||
|
end: self.translation.0 + fighter_manifest.ledge_grab_box_end,
|
||||||
|
};
|
||||||
CollisionContext {
|
CollisionContext {
|
||||||
translation: &mut self.translation,
|
translation: &mut self.translation,
|
||||||
prev_translation: &self.prev_translation,
|
prev_translation: &self.prev_translation,
|
||||||
@@ -409,6 +473,8 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
|
|||||||
.fighter_common
|
.fighter_common
|
||||||
.leave_ground_ecb_frame_lock_duration,
|
.leave_ground_ecb_frame_lock_duration,
|
||||||
downward_stick_threshold: game_settings.input_common.run_stick_threshold,
|
downward_stick_threshold: game_settings.input_common.run_stick_threshold,
|
||||||
|
fighter_ledge_collision_box,
|
||||||
|
ledge_grab_lockout_active: self.ledge_grab_lockout.is_active(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -432,6 +498,7 @@ pub struct StatefulFighterQuery {
|
|||||||
pub fn state_update_system(
|
pub fn state_update_system(
|
||||||
mut query: Query<StatefulFighterQuery>,
|
mut query: Query<StatefulFighterQuery>,
|
||||||
game_settings: Res<GameSettings>,
|
game_settings: Res<GameSettings>,
|
||||||
|
stage_collision: Res<StageCollision>,
|
||||||
attack_scripts: Res<Assets<FighterAttackScript>>,
|
attack_scripts: Res<Assets<FighterAttackScript>>,
|
||||||
manifests: Res<Assets<FighterManifest>>,
|
manifests: Res<Assets<FighterManifest>>,
|
||||||
) {
|
) {
|
||||||
@@ -455,6 +522,7 @@ pub fn state_update_system(
|
|||||||
&manifest.attributes,
|
&manifest.attributes,
|
||||||
&attack_scripts,
|
&attack_scripts,
|
||||||
&fighter.attack_script_assets,
|
&fighter.attack_script_assets,
|
||||||
|
&stage_collision,
|
||||||
);
|
);
|
||||||
state.update(&mut update_ctx);
|
state.update(&mut update_ctx);
|
||||||
}
|
}
|
||||||
@@ -466,7 +534,10 @@ fn transition_state(
|
|||||||
frame: &mut FighterFrameQueryItem,
|
frame: &mut FighterFrameQueryItem,
|
||||||
attributes: &FighterAttributes,
|
attributes: &FighterAttributes,
|
||||||
attack_script_assets: &FighterAttackScriptAssets,
|
attack_script_assets: &FighterAttackScriptAssets,
|
||||||
|
stage_collision: &StageCollision,
|
||||||
game_settings: &GameSettings,
|
game_settings: &GameSettings,
|
||||||
|
baked_animations: &BakedFighterAnimations,
|
||||||
|
model_scale: FGi32,
|
||||||
) {
|
) {
|
||||||
let old_name = current.name();
|
let old_name = current.name();
|
||||||
let new_name = next.name();
|
let new_name = next.name();
|
||||||
@@ -480,7 +551,14 @@ fn transition_state(
|
|||||||
.leave_ground_ecb_frame_lock_duration,
|
.leave_ground_ecb_frame_lock_duration,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
next.on_enter(&mut frame.enter_context(attributes, attack_script_assets, game_settings));
|
next.on_enter(&mut frame.enter_context(
|
||||||
|
attributes,
|
||||||
|
attack_script_assets,
|
||||||
|
game_settings,
|
||||||
|
stage_collision,
|
||||||
|
baked_animations,
|
||||||
|
model_scale,
|
||||||
|
));
|
||||||
*current = next;
|
*current = next;
|
||||||
info!("{} -> {}", old_name, new_name);
|
info!("{} -> {}", old_name, new_name);
|
||||||
}
|
}
|
||||||
@@ -508,6 +586,9 @@ pub fn state_interrupt_system(
|
|||||||
let manifest = fighter_manifests
|
let manifest = fighter_manifests
|
||||||
.get(&fighter.manifest)
|
.get(&fighter.manifest)
|
||||||
.expect("Fighter manifest should be valid");
|
.expect("Fighter manifest should be valid");
|
||||||
|
let baked = baked_animations
|
||||||
|
.get(&animations.baked)
|
||||||
|
.expect("Animation should exist");
|
||||||
|
|
||||||
let new_state = if let Some(reaction) = pending_hit_reaction.take() {
|
let new_state = if let Some(reaction) = pending_hit_reaction.take() {
|
||||||
hitstop.remaining_frames = *reaction.victim_hitstop_frames;
|
hitstop.remaining_frames = *reaction.victim_hitstop_frames;
|
||||||
@@ -579,7 +660,10 @@ pub fn state_interrupt_system(
|
|||||||
&mut frame,
|
&mut frame,
|
||||||
&manifest.attributes,
|
&manifest.attributes,
|
||||||
attack_script_assets,
|
attack_script_assets,
|
||||||
|
&stage_collision,
|
||||||
&game_settings,
|
&game_settings,
|
||||||
|
baked,
|
||||||
|
manifest.model_scale,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -594,6 +678,7 @@ pub fn state_collision_interrupt_system(
|
|||||||
mut match_state: ResMut<MatchState>,
|
mut match_state: ResMut<MatchState>,
|
||||||
stage_registry: Res<StageManifestRegistry>,
|
stage_registry: Res<StageManifestRegistry>,
|
||||||
stage_manifests: Res<Assets<StageManifest>>,
|
stage_manifests: Res<Assets<StageManifest>>,
|
||||||
|
baked_animations: Res<Assets<BakedFighterAnimations>>,
|
||||||
) {
|
) {
|
||||||
let Some(stage_manifest_handle) = stage_registry.get(match_config.stage) else {
|
let Some(stage_manifest_handle) = stage_registry.get(match_config.stage) else {
|
||||||
return;
|
return;
|
||||||
@@ -608,6 +693,7 @@ pub fn state_collision_interrupt_system(
|
|||||||
mut state,
|
mut state,
|
||||||
mut frame,
|
mut frame,
|
||||||
fighter,
|
fighter,
|
||||||
|
animations,
|
||||||
attack_script_assets,
|
attack_script_assets,
|
||||||
mut hitstop,
|
mut hitstop,
|
||||||
mut pending_respawn,
|
mut pending_respawn,
|
||||||
@@ -619,6 +705,9 @@ pub fn state_collision_interrupt_system(
|
|||||||
let manifest = fighter_manifests
|
let manifest = fighter_manifests
|
||||||
.get(&fighter.manifest)
|
.get(&fighter.manifest)
|
||||||
.expect("Fighter manifest should be valid");
|
.expect("Fighter manifest should be valid");
|
||||||
|
let baked = baked_animations
|
||||||
|
.get(&animations.baked)
|
||||||
|
.expect("Animation should exist");
|
||||||
|
|
||||||
if pending_respawn.0 {
|
if pending_respawn.0 {
|
||||||
**pending_respawn = false;
|
**pending_respawn = false;
|
||||||
@@ -665,7 +754,10 @@ pub fn state_collision_interrupt_system(
|
|||||||
&mut frame,
|
&mut frame,
|
||||||
&manifest.attributes,
|
&manifest.attributes,
|
||||||
attack_script_assets,
|
attack_script_assets,
|
||||||
|
&stage_collision,
|
||||||
&game_settings,
|
&game_settings,
|
||||||
|
baked,
|
||||||
|
manifest.model_scale,
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -675,7 +767,7 @@ pub fn state_collision_interrupt_system(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let new_state = state.check_collision_interrupt(
|
let new_state = state.check_collision_interrupt(
|
||||||
&mut frame.collision_context(&stage_collision, &game_settings),
|
&mut frame.collision_context(&stage_collision, manifest, &game_settings),
|
||||||
);
|
);
|
||||||
if let Some(new_state) = new_state {
|
if let Some(new_state) = new_state {
|
||||||
transition_state(
|
transition_state(
|
||||||
@@ -684,8 +776,81 @@ pub fn state_collision_interrupt_system(
|
|||||||
&mut frame,
|
&mut frame,
|
||||||
&manifest.attributes,
|
&manifest.attributes,
|
||||||
&attack_script_assets,
|
&attack_script_assets,
|
||||||
|
&stage_collision,
|
||||||
&game_settings,
|
&game_settings,
|
||||||
|
baked,
|
||||||
|
manifest.model_scale,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::fighter::ecb::FighterPreviousECB;
|
||||||
|
use crate::stage::line::{StageLedge, StageLedgeSide, StageVertexID};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn animation_positions_map_forward_to_facing_and_preserve_height() {
|
||||||
|
let position = FGVec3::lit("99", "3", "5");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
animation_position_to_gameplay(position, &FighterFacingDirection::Right),
|
||||||
|
FGVec2::lit("5", "3")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
animation_position_to_gameplay(position, &FighterFacingDirection::Left),
|
||||||
|
FGVec2::lit("-5", "3")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ledge_lockout_suppresses_ledge_collision() {
|
||||||
|
let ledge_id = StageVertexID {
|
||||||
|
polygon: 0,
|
||||||
|
vertex: 0,
|
||||||
|
};
|
||||||
|
let stage_collision = StageCollision {
|
||||||
|
ledges: vec![StageLedge {
|
||||||
|
id: ledge_id,
|
||||||
|
position: FGVec2::ZERO,
|
||||||
|
hang_side: StageLedgeSide::Left,
|
||||||
|
}],
|
||||||
|
..default()
|
||||||
|
};
|
||||||
|
let previous_translation = FighterPreviousTranslation::default();
|
||||||
|
let input = FighterInput::default();
|
||||||
|
let mut translation = FighterTranslation::default();
|
||||||
|
let mut velocity = FighterVelocity::default();
|
||||||
|
let mut surface = FighterSurfaceState::default();
|
||||||
|
let mut ecb = FighterECB::default();
|
||||||
|
let previous_ecb = FighterPreviousECB::from(ecb);
|
||||||
|
let mut presentation_snap = FighterPresentationSnap::default();
|
||||||
|
let mut context = CollisionContext {
|
||||||
|
translation: &mut translation,
|
||||||
|
prev_translation: &previous_translation,
|
||||||
|
input: &input,
|
||||||
|
velocity: &mut velocity,
|
||||||
|
surface: &mut surface,
|
||||||
|
ecb: &mut ecb,
|
||||||
|
prev_ecb: &previous_ecb,
|
||||||
|
stage_collision: &stage_collision,
|
||||||
|
leave_ground_ecb_lock_duration: 0,
|
||||||
|
presentation_snap: &mut presentation_snap,
|
||||||
|
fighter_ledge_collision_box: FGRect {
|
||||||
|
origin: FGVec2::lit("-1", "1"),
|
||||||
|
end: FGVec2::lit("1", "-1"),
|
||||||
|
},
|
||||||
|
ledge_grab_lockout_active: true,
|
||||||
|
downward_stick_threshold: FGi32::ONE,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(context.air_collide_with_ledges().is_none());
|
||||||
|
context.ledge_grab_lockout_active = false;
|
||||||
|
assert_eq!(
|
||||||
|
context.air_collide_with_ledges().map(|result| result.ledge),
|
||||||
|
Some(ledge_id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ pub struct FighterSettingsCommon {
|
|||||||
pub leave_ground_ecb_frame_lock_duration: u32,
|
pub leave_ground_ecb_frame_lock_duration: u32,
|
||||||
pub platform_pass_platform_ignore_duration_frames: u32,
|
pub platform_pass_platform_ignore_duration_frames: u32,
|
||||||
pub platform_pass_initial_downwards_velocity: FGi32,
|
pub platform_pass_initial_downwards_velocity: FGi32,
|
||||||
|
/// Frames during which a fighter cannot grab any ledge after dropping from one.
|
||||||
|
pub ledge_drop_regrab_lockout_duration_frames: u32,
|
||||||
|
|
||||||
pub intangible_color: Color,
|
pub intangible_color: Color,
|
||||||
pub intangible_pulse_duration: u32,
|
pub intangible_pulse_duration: u32,
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ pub mod segment;
|
|||||||
pub mod vec;
|
pub mod vec;
|
||||||
pub mod fixed_transform;
|
pub mod fixed_transform;
|
||||||
pub mod vec3;
|
pub mod vec3;
|
||||||
|
pub mod rect;
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
use crate::math::vec::FGVec2;
|
||||||
|
|
||||||
|
pub struct FGRect {
|
||||||
|
pub origin: FGVec2,
|
||||||
|
pub end: FGVec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FGRect {
|
||||||
|
pub fn contains_point(&self, point: FGVec2) -> bool {
|
||||||
|
point.x >= self.origin.x && point.y <= self.origin.y && point.x <= self.end.x && point.y >= self.end.y
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
-2
@@ -2,14 +2,14 @@ use bevy::{color::palettes::css::RED, prelude::*};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
match_loading::MatchConfig, math::{int::FGi32, vec3::FGVec3}, stage::{
|
match_loading::MatchConfig, math::{int::FGi32, vec3::FGVec3}, stage::{
|
||||||
line::StageCollision,
|
line::{StageCollision, StageLedgeSide},
|
||||||
manifest::{StageManifest, StageManifestRegistry},
|
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() {
|
||||||
for segment in poly.segments.iter() {
|
for segment in &poly.segments {
|
||||||
let from = segment.segment.point1().to_vec2();
|
let from = segment.segment.point1().to_vec2();
|
||||||
let to = segment.segment.point2().to_vec2();
|
let to = segment.segment.point2().to_vec2();
|
||||||
let from_3d = Vec3::new(from.x, from.y, 0.0);
|
let from_3d = Vec3::new(from.x, from.y, 0.0);
|
||||||
@@ -20,6 +20,20 @@ pub fn debug_draw_scene(mut gizmos: Gizmos, stage_collision: Res<StageCollision>
|
|||||||
gizmos.arrow(midpoint, midpoint + normal_3d, RED);
|
gizmos.arrow(midpoint, midpoint + normal_3d, RED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for ledge in &stage_collision.ledges {
|
||||||
|
let position = ledge.position.to_vec2().extend(0.0);
|
||||||
|
let direction = match ledge.hang_side {
|
||||||
|
StageLedgeSide::Left => Vec3::NEG_X,
|
||||||
|
StageLedgeSide::Right => Vec3::X,
|
||||||
|
};
|
||||||
|
gizmos.circle(position, 1.5, bevy::color::palettes::css::LIME);
|
||||||
|
gizmos.arrow(
|
||||||
|
position,
|
||||||
|
position + direction * 4.0,
|
||||||
|
bevy::color::palettes::css::LIME,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn debug_draw_blast_zones(
|
pub fn debug_draw_blast_zones(
|
||||||
|
|||||||
+51
-1
@@ -3,7 +3,7 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::math::{segment::FGSegment2d, vec::FGVec2};
|
use crate::math::{rect::FGRect, segment::FGSegment2d, vec::FGVec2};
|
||||||
|
|
||||||
#[derive(Reflect, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
#[derive(Reflect, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum StagePolyLineSegmentType {
|
pub enum StagePolyLineSegmentType {
|
||||||
@@ -32,6 +32,27 @@ pub struct StageLineID {
|
|||||||
pub segment: usize,
|
pub segment: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Identifies a corner in the stage collision geometry.
|
||||||
|
#[derive(Reflect, Hash, Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||||
|
pub struct StageVertexID {
|
||||||
|
pub polygon: usize,
|
||||||
|
pub vertex: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The side of a ledge on which a grabbing fighter hangs.
|
||||||
|
#[derive(Reflect, Serialize, Deserialize, Hash, Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum StageLedgeSide {
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Reflect, Hash, Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct StageLedge {
|
||||||
|
pub id: StageVertexID,
|
||||||
|
pub position: FGVec2,
|
||||||
|
pub hang_side: StageLedgeSide,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Reflect, Clone, Default)]
|
#[derive(Reflect, Clone, Default)]
|
||||||
pub struct StagePoly {
|
pub struct StagePoly {
|
||||||
pub poly_type: StagePolyType,
|
pub poly_type: StagePolyType,
|
||||||
@@ -44,10 +65,39 @@ pub struct StagePlaneIntersectResult {
|
|||||||
pub normal: FGVec2,
|
pub normal: FGVec2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct StageLedgeIntersectResult {
|
||||||
|
pub vertex_id: StageVertexID,
|
||||||
|
pub ledge_idx: usize
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Resource, Reflect, Clone, Default)]
|
#[derive(Resource, Reflect, Clone, Default)]
|
||||||
#[reflect(Resource)]
|
#[reflect(Resource)]
|
||||||
pub struct StageCollision {
|
pub struct StageCollision {
|
||||||
pub stage_polys: Vec<StagePoly>,
|
pub stage_polys: Vec<StagePoly>,
|
||||||
|
pub ledges: Vec<StageLedge>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StageCollision {
|
||||||
|
pub fn intersect_ledges(&self, ledge_grab_box: &FGRect) -> Option<StageLedgeIntersectResult> {
|
||||||
|
for (ledge_idx, ledge) in self.ledges.iter().enumerate() {
|
||||||
|
if ledge_grab_box.contains_point(ledge.position) {
|
||||||
|
return Some(StageLedgeIntersectResult {
|
||||||
|
vertex_id: ledge.id,
|
||||||
|
ledge_idx
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_ledge_by_vertex(&self, vertex: StageVertexID) -> Option<&StageLedge> {
|
||||||
|
self.ledges.iter().find(|ledge| ledge.id == vertex)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_segment(&self, segment: StageLineID) -> Option<&StagePolyLineSegment> {
|
||||||
|
self.stage_polys.get(segment.polygon).map(|poly| poly.segments.get(segment.segment))?
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StagePoly {
|
impl StagePoly {
|
||||||
|
|||||||
+189
-2
@@ -9,7 +9,10 @@ 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, StageLedge, StageLedgeSide, StagePolyLineSegment,
|
||||||
|
StagePolyLineSegmentType, StagePolyType, StageVertexID,
|
||||||
|
},
|
||||||
}, texture_reference::TextureReference,
|
}, texture_reference::TextureReference,
|
||||||
};
|
};
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
@@ -71,6 +74,16 @@ pub enum StageId {
|
|||||||
pub struct StagePolyDefinition {
|
pub struct StagePolyDefinition {
|
||||||
poly_type: StagePolyType,
|
poly_type: StagePolyType,
|
||||||
lines: Vec<(FGVec2, StagePolyLineSegmentType)>,
|
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)]
|
#[derive(Deserialize, Clone, Asset, Reflect, RonAsset)]
|
||||||
@@ -176,6 +189,13 @@ pub fn prepare_stage_manifests(
|
|||||||
);
|
);
|
||||||
return;
|
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() {
|
if registry.insert(manifest.id, handle.clone()).is_some() {
|
||||||
fail(
|
fail(
|
||||||
&mut next_state,
|
&mut next_state,
|
||||||
@@ -267,6 +287,14 @@ impl StageManifest {
|
|||||||
Ok(())
|
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 {
|
pub fn to_collision(&self) -> StageCollision {
|
||||||
let stage_polys: Vec<StagePoly> = self
|
let stage_polys: Vec<StagePoly> = self
|
||||||
.polygons
|
.polygons
|
||||||
@@ -274,7 +302,84 @@ impl StageManifest {
|
|||||||
.map(|def| StagePoly::build(def.poly_type, &def.lines))
|
.map(|def| StagePoly::build(def.poly_type, &def.lines))
|
||||||
.collect();
|
.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
|
< 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