Add affine transformations for capsules.
This commit is contained in:
@@ -111,6 +111,7 @@ enum DebugFlag {
|
|||||||
StageCollision,
|
StageCollision,
|
||||||
AnimationBones,
|
AnimationBones,
|
||||||
AttackHitboxes,
|
AttackHitboxes,
|
||||||
|
Hurtboxes,
|
||||||
Camera,
|
Camera,
|
||||||
DrawThroughGeometry,
|
DrawThroughGeometry,
|
||||||
}
|
}
|
||||||
@@ -156,6 +157,10 @@ const FIGHTER_MENU: &[DebugMenuEntry] = &[
|
|||||||
label: "Attack hitboxes",
|
label: "Attack hitboxes",
|
||||||
flag: DebugFlag::AttackHitboxes,
|
flag: DebugFlag::AttackHitboxes,
|
||||||
},
|
},
|
||||||
|
DebugMenuEntry::Toggle {
|
||||||
|
label: "Hurtboxes",
|
||||||
|
flag: DebugFlag::Hurtboxes,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const WORLD_MENU: &[DebugMenuEntry] = &[
|
const WORLD_MENU: &[DebugMenuEntry] = &[
|
||||||
@@ -227,6 +232,7 @@ pub struct DebugSettings {
|
|||||||
pub stage_collision: bool,
|
pub stage_collision: bool,
|
||||||
pub animation_bones: bool,
|
pub animation_bones: bool,
|
||||||
pub attack_hitboxes: bool,
|
pub attack_hitboxes: bool,
|
||||||
|
pub hurtboxes: bool,
|
||||||
pub camera: bool,
|
pub camera: bool,
|
||||||
pub draw_through_geometry: bool,
|
pub draw_through_geometry: bool,
|
||||||
pub motion_sampling: PresentationMotionMode,
|
pub motion_sampling: PresentationMotionMode,
|
||||||
@@ -246,6 +252,7 @@ impl Default for DebugSettings {
|
|||||||
stage_collision: false,
|
stage_collision: false,
|
||||||
animation_bones: false,
|
animation_bones: false,
|
||||||
attack_hitboxes: false,
|
attack_hitboxes: false,
|
||||||
|
hurtboxes: false,
|
||||||
camera: false,
|
camera: false,
|
||||||
draw_through_geometry: false,
|
draw_through_geometry: false,
|
||||||
motion_sampling: PresentationMotionMode::default(),
|
motion_sampling: PresentationMotionMode::default(),
|
||||||
@@ -309,6 +316,7 @@ impl DebugSettings {
|
|||||||
DebugFlag::StageCollision => self.stage_collision,
|
DebugFlag::StageCollision => self.stage_collision,
|
||||||
DebugFlag::AnimationBones => self.animation_bones,
|
DebugFlag::AnimationBones => self.animation_bones,
|
||||||
DebugFlag::AttackHitboxes => self.attack_hitboxes,
|
DebugFlag::AttackHitboxes => self.attack_hitboxes,
|
||||||
|
DebugFlag::Hurtboxes => self.hurtboxes,
|
||||||
DebugFlag::DrawThroughGeometry => self.draw_through_geometry,
|
DebugFlag::DrawThroughGeometry => self.draw_through_geometry,
|
||||||
DebugFlag::Camera => self.camera,
|
DebugFlag::Camera => self.camera,
|
||||||
}
|
}
|
||||||
@@ -321,6 +329,7 @@ impl DebugSettings {
|
|||||||
DebugFlag::StageCollision => self.stage_collision = !self.stage_collision,
|
DebugFlag::StageCollision => self.stage_collision = !self.stage_collision,
|
||||||
DebugFlag::AnimationBones => self.animation_bones = !self.animation_bones,
|
DebugFlag::AnimationBones => self.animation_bones = !self.animation_bones,
|
||||||
DebugFlag::AttackHitboxes => self.attack_hitboxes = !self.attack_hitboxes,
|
DebugFlag::AttackHitboxes => self.attack_hitboxes = !self.attack_hitboxes,
|
||||||
|
DebugFlag::Hurtboxes => self.hurtboxes = !self.hurtboxes,
|
||||||
DebugFlag::DrawThroughGeometry => {
|
DebugFlag::DrawThroughGeometry => {
|
||||||
self.draw_through_geometry = !self.draw_through_geometry
|
self.draw_through_geometry = !self.draw_through_geometry
|
||||||
}
|
}
|
||||||
@@ -623,6 +632,10 @@ pub fn attack_hitboxes_enabled(settings: Res<DebugSettings>) -> bool {
|
|||||||
settings.attack_hitboxes
|
settings.attack_hitboxes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn hurtboxes_enabled(settings: Res<DebugSettings>) -> bool {
|
||||||
|
settings.hurtboxes
|
||||||
|
}
|
||||||
|
|
||||||
pub fn camera_debug_enabled(settings: Res<DebugSettings>) -> bool {
|
pub fn camera_debug_enabled(settings: Res<DebugSettings>) -> bool {
|
||||||
settings.camera
|
settings.camera
|
||||||
}
|
}
|
||||||
|
|||||||
+76
-3
@@ -1,11 +1,22 @@
|
|||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
|
|
||||||
use bevy::{asset::Handle, ecs::component::Component, reflect::Reflect};
|
use bevy::prelude::*;
|
||||||
|
use bevy_ggrs::prelude::GgrsSchedule;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
fighter::{FighterAttributes, animation::AnimKind},
|
fighter::{
|
||||||
math::{int::FGi32, vec3::FGVec3},
|
Fighter, FighterAttributes, FighterFacingDirection, FighterTranslation,
|
||||||
|
animation::AnimKind,
|
||||||
|
baked_animation::{FighterBoneMatrices, FixedMat4},
|
||||||
|
hurtbox::FixedAffineCapsule,
|
||||||
|
manifest::FighterManifest,
|
||||||
|
},
|
||||||
|
math::{
|
||||||
|
int::{FGi32, FGi32Ext},
|
||||||
|
vec3::FGVec3,
|
||||||
|
},
|
||||||
|
schedule::GameplaySet,
|
||||||
scripting::FighterAttackScript,
|
scripting::FighterAttackScript,
|
||||||
};
|
};
|
||||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||||
@@ -164,7 +175,69 @@ pub fn update_active_hitbox_list(script: &FighterAttackScript, list: &mut Vec<us
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct FighterSolvedHurtbox {
|
||||||
|
pub hurtbox_idx: usize,
|
||||||
|
pub capsule: FixedAffineCapsule,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub struct FighterAttackScriptAssets {
|
pub struct FighterAttackScriptAssets {
|
||||||
pub scripts: HashMap<AttackKind, Handle<FighterAttackScript>>,
|
pub scripts: HashMap<AttackKind, Handle<FighterAttackScript>>,
|
||||||
}
|
}
|
||||||
|
#[derive(Component, Clone, Debug, Default)]
|
||||||
|
pub struct FighterSolvedHurtboxes(pub Vec<FighterSolvedHurtbox>);
|
||||||
|
|
||||||
|
pub fn solve_hurtboxes(
|
||||||
|
mut query: Query<(
|
||||||
|
&FighterBoneMatrices,
|
||||||
|
&FighterTranslation,
|
||||||
|
&FighterFacingDirection,
|
||||||
|
&mut FighterSolvedHurtboxes,
|
||||||
|
&Fighter,
|
||||||
|
)>,
|
||||||
|
manifests: Res<Assets<FighterManifest>>,
|
||||||
|
) {
|
||||||
|
for (matrices, translation, facing_direction, mut solved_hurtboxes, fighter) in &mut query {
|
||||||
|
let manifest = manifests
|
||||||
|
.get(&fighter.manifest)
|
||||||
|
.expect("Manifest should be valid");
|
||||||
|
let fighter_trf = translation.get_3d_transform(facing_direction);
|
||||||
|
solved_hurtboxes.0.clear();
|
||||||
|
for (hurtbox_idx, hurtbox) in manifest.hurtboxes.iter().enumerate() {
|
||||||
|
let bone_matrix = matrices.get_current(&hurtbox.bone);
|
||||||
|
|
||||||
|
if let Some(bone_matrix) = bone_matrix {
|
||||||
|
let to_global = fighter_trf.mul(bone_matrix);
|
||||||
|
let hurtbox_trf = FixedMat4::IDENTITY
|
||||||
|
.with_rotation(FGVec3::new(
|
||||||
|
hurtbox.rotation.x.to_radians(),
|
||||||
|
hurtbox.rotation.y.to_radians(),
|
||||||
|
hurtbox.rotation.z.to_radians(),
|
||||||
|
))
|
||||||
|
.with_translation(hurtbox.offset);
|
||||||
|
solved_hurtboxes.0.push(FighterSolvedHurtbox {
|
||||||
|
hurtbox_idx,
|
||||||
|
capsule: FixedAffineCapsule::new(
|
||||||
|
to_global.mul(hurtbox_trf),
|
||||||
|
hurtbox.half_length,
|
||||||
|
hurtbox.radius,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FighterAttackPlugin;
|
||||||
|
|
||||||
|
impl Plugin for FighterAttackPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_systems(
|
||||||
|
GgrsSchedule,
|
||||||
|
solve_hurtboxes
|
||||||
|
.after(crate::fighter::baked_animation::update_fighter_bone_matrices)
|
||||||
|
.in_set(GameplaySet::Animation),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ use crate::{
|
|||||||
visual::FighterAnimations,
|
visual::FighterAnimations,
|
||||||
},
|
},
|
||||||
math::{
|
math::{
|
||||||
int::{FGWide, FGi32},
|
int::{FGWide, FGi32, FGi32Ext},
|
||||||
vec3::FGVec3,
|
vec3::FGVec3,
|
||||||
},
|
},
|
||||||
schedule::GameplaySet,
|
schedule::GameplaySet,
|
||||||
@@ -108,6 +108,28 @@ impl FixedMat4 {
|
|||||||
self.cols[3][2] = translation.z;
|
self.cols[3][2] = translation.z;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_translation(mut self, translation: FGVec3) -> Self {
|
||||||
|
self.translate(translation);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replaces the rotation with XYZ Euler angles expressed in radians.
|
||||||
|
pub fn with_rotation(mut self, rotation: FGVec3) -> Self {
|
||||||
|
let (sin_x, cos_x) = rotation.x.sin_cos();
|
||||||
|
let (sin_y, cos_y) = rotation.y.sin_cos();
|
||||||
|
let (sin_z, cos_z) = rotation.z.sin_cos();
|
||||||
|
self.cols[0][0] = cos_y * cos_z;
|
||||||
|
self.cols[0][1] = cos_x * sin_z + sin_x * sin_y * cos_z;
|
||||||
|
self.cols[0][2] = sin_x * sin_z - cos_x * sin_y * cos_z;
|
||||||
|
self.cols[1][0] = -cos_y * sin_z;
|
||||||
|
self.cols[1][1] = cos_x * cos_z - sin_x * sin_y * sin_z;
|
||||||
|
self.cols[1][2] = sin_x * cos_z + cos_x * sin_y * sin_z;
|
||||||
|
self.cols[2][0] = sin_y;
|
||||||
|
self.cols[2][1] = -sin_x * cos_y;
|
||||||
|
self.cols[2][2] = cos_x * cos_y;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_translation(&self) -> FGVec3 {
|
pub fn get_translation(&self) -> FGVec3 {
|
||||||
FGVec3::new(self.cols[3][0], self.cols[3][1], self.cols[3][2])
|
FGVec3::new(self.cols[3][0], self.cols[3][1], self.cols[3][2])
|
||||||
}
|
}
|
||||||
@@ -949,4 +971,26 @@ mod tests {
|
|||||||
FGVec3::lit("10", "2", "-4")
|
FGVec3::lit("10", "2", "-4")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fixed_matrix_builders_set_euler_rotation_and_translation() {
|
||||||
|
let transform = FixedMat4::IDENTITY
|
||||||
|
.with_rotation(FGVec3::lit("0", "0", "1.5707963267948966"))
|
||||||
|
.with_translation(FGVec3::lit("10", "2", "-3"));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
transform.transform_point(FGVec3::lit("1", "0", "0")),
|
||||||
|
FGVec3::lit("10", "3", "-3")
|
||||||
|
);
|
||||||
|
assert_eq!(transform.get_translation(), FGVec3::lit("10", "2", "-3"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fixed_matrix_rotation_builder_preserves_translation() {
|
||||||
|
let transform = FixedMat4::IDENTITY
|
||||||
|
.with_translation(FGVec3::lit("4", "5", "6"))
|
||||||
|
.with_rotation(FGVec3::lit("0", "0", "1.5707963267948966"));
|
||||||
|
|
||||||
|
assert_eq!(transform.get_translation(), FGVec3::lit("4", "5", "6"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+151
-46
@@ -1,21 +1,21 @@
|
|||||||
//! Fighter debug views: gizmos and the egui inspector window.
|
//! Fighter debug views: gizmos and the egui inspector window.
|
||||||
|
|
||||||
use std::ops::Mul;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
use bevy::color::palettes::css::ORANGE;
|
use bevy::color::palettes::css::ORANGE;
|
||||||
use bevy::gizmos::primitives::dim3::Capsule3dBuilder;
|
|
||||||
use bevy::{color::palettes::css::YELLOW, prelude::*};
|
use bevy::{color::palettes::css::YELLOW, prelude::*};
|
||||||
use bevy_egui::{EguiContexts, egui};
|
use bevy_egui::{EguiContexts, egui};
|
||||||
|
|
||||||
use crate::debug_tools::{DebugSettings, FighterDebugRowKind};
|
use crate::debug_tools::{DebugSettings, FighterDebugRowKind};
|
||||||
use crate::fighter::animation::FighterAnimationFrame;
|
use crate::fighter::animation::FighterAnimationFrame;
|
||||||
|
use crate::fighter::attack::FighterSolvedHurtboxes;
|
||||||
use crate::fighter::baked_animation::{BakedFighterAnimations, FighterBoneMatrices};
|
use crate::fighter::baked_animation::{BakedFighterAnimations, FighterBoneMatrices};
|
||||||
use crate::fighter::ecb::FighterPreviousECB;
|
use crate::fighter::ecb::FighterPreviousECB;
|
||||||
use crate::fighter::manifest::FighterManifest;
|
use crate::fighter::hurtbox::FixedAffineCapsule;
|
||||||
use crate::fighter::state::StateNameDebug;
|
use crate::fighter::state::StateNameDebug;
|
||||||
use crate::fighter::visual::FighterAnimations;
|
use crate::fighter::visual::FighterAnimations;
|
||||||
use crate::fighter::{
|
use crate::fighter::{
|
||||||
Fighter, FighterECB, FighterFacingDirection, FighterHitboxes, FighterPreviousTranslation,
|
FighterECB, FighterFacingDirection, FighterHitboxes, FighterPreviousTranslation,
|
||||||
FighterTranslation, FighterVelocity,
|
FighterTranslation, FighterVelocity,
|
||||||
};
|
};
|
||||||
use crate::input::FighterInput;
|
use crate::input::FighterInput;
|
||||||
@@ -160,52 +160,157 @@ pub fn animation_debug(query: Query<(&GlobalTransform, &FighterBoneMatrices)>, m
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hurtbox_debug(
|
pub fn hurtbox_debug(query: Query<&FighterSolvedHurtboxes>, mut gizmos: Gizmos) {
|
||||||
query: Query<(
|
for hurtboxes in &query {
|
||||||
&FighterBoneMatrices,
|
for hurtbox in &hurtboxes.0 {
|
||||||
&FighterTranslation,
|
draw_affine_capsule(&mut gizmos, hurtbox.capsule, YELLOW);
|
||||||
&FighterFacingDirection,
|
|
||||||
&Fighter,
|
|
||||||
)>,
|
|
||||||
manifests: Res<Assets<FighterManifest>>,
|
|
||||||
mut gizmos: Gizmos,
|
|
||||||
) {
|
|
||||||
for (matrices, translation, facing_direction, fighter) in query {
|
|
||||||
let manifest = manifests
|
|
||||||
.get(&fighter.manifest)
|
|
||||||
.expect("Manifest should be valid");
|
|
||||||
let fighter_trf = translation.get_3d_transform(facing_direction);
|
|
||||||
for hurtbox in manifest.hurtboxes.iter() {
|
|
||||||
let bone_matrix = matrices.get_current(&hurtbox.bone);
|
|
||||||
|
|
||||||
if let Some(bone_matrix) = bone_matrix {
|
|
||||||
let to_global = fighter_trf.mul(bone_matrix);
|
|
||||||
let capsule = Capsule3d {
|
|
||||||
half_length: hurtbox.half_length.to_num(),
|
|
||||||
radius: hurtbox.radius.to_num(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let hurtbox_trf = Transform::IDENTITY
|
|
||||||
.with_rotation(Quat::from_euler(
|
|
||||||
EulerRot::XYZ,
|
|
||||||
f32::to_radians(hurtbox.rotation.x.to_num()),
|
|
||||||
f32::to_radians(hurtbox.rotation.y.to_num()),
|
|
||||||
f32::to_radians(hurtbox.rotation.z.to_num()),
|
|
||||||
))
|
|
||||||
.with_translation(hurtbox.offset.to_vec3());
|
|
||||||
|
|
||||||
let hurtbox_trf = Transform::from_matrix(to_global.to_mat4()).mul(hurtbox_trf);
|
|
||||||
|
|
||||||
gizmos.primitive_3d(
|
|
||||||
&capsule,
|
|
||||||
hurtbox_trf.to_isometry(),
|
|
||||||
bevy::color::palettes::css::YELLOW,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn draw_affine_capsule(
|
||||||
|
gizmos: &mut Gizmos,
|
||||||
|
capsule: FixedAffineCapsule,
|
||||||
|
color: impl Into<Color> + Copy,
|
||||||
|
) {
|
||||||
|
let color = color.into();
|
||||||
|
let wireframe = capsule_wireframe();
|
||||||
|
let transform = capsule.transform.to_mat4();
|
||||||
|
let radius = capsule.radius.to_num::<f32>();
|
||||||
|
let half_length = capsule.half_length.to_num::<f32>();
|
||||||
|
let vertices: Vec<_> = wireframe
|
||||||
|
.vertices
|
||||||
|
.iter()
|
||||||
|
.map(|vertex| {
|
||||||
|
transform.transform_point3(
|
||||||
|
vertex.direction * radius + Vec3::Y * vertex.center_sign * half_length,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
for &(start, end) in &wireframe.lines {
|
||||||
|
gizmos.line(vertices[start], vertices[end], color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CapsuleWireVertex {
|
||||||
|
direction: Vec3,
|
||||||
|
center_sign: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CapsuleWireframe {
|
||||||
|
vertices: Vec<CapsuleWireVertex>,
|
||||||
|
lines: Vec<(usize, usize)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn capsule_wireframe() -> &'static CapsuleWireframe {
|
||||||
|
const RESOLUTION: usize = 16;
|
||||||
|
const HEMISPHERE_STEPS: usize = 4;
|
||||||
|
static WIREFRAME: OnceLock<CapsuleWireframe> = OnceLock::new();
|
||||||
|
|
||||||
|
WIREFRAME.get_or_init(|| {
|
||||||
|
let mut vertices = Vec::new();
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
let mut lower_ring = Vec::with_capacity(RESOLUTION);
|
||||||
|
let mut upper_ring = Vec::with_capacity(RESOLUTION);
|
||||||
|
|
||||||
|
for index in 0..RESOLUTION {
|
||||||
|
let angle = index as f32 * std::f32::consts::TAU / RESOLUTION as f32;
|
||||||
|
let (sin, cos) = angle.sin_cos();
|
||||||
|
lower_ring.push(vertices.len());
|
||||||
|
vertices.push(CapsuleWireVertex {
|
||||||
|
direction: Vec3::new(cos, 0.0, sin),
|
||||||
|
center_sign: -1.0,
|
||||||
|
});
|
||||||
|
upper_ring.push(vertices.len());
|
||||||
|
vertices.push(CapsuleWireVertex {
|
||||||
|
direction: Vec3::new(cos, 0.0, sin),
|
||||||
|
center_sign: 1.0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let lower_pole = vertices.len();
|
||||||
|
vertices.push(CapsuleWireVertex {
|
||||||
|
direction: Vec3::NEG_Y,
|
||||||
|
center_sign: -1.0,
|
||||||
|
});
|
||||||
|
let upper_pole = vertices.len();
|
||||||
|
vertices.push(CapsuleWireVertex {
|
||||||
|
direction: Vec3::Y,
|
||||||
|
center_sign: 1.0,
|
||||||
|
});
|
||||||
|
|
||||||
|
for index in 0..RESOLUTION {
|
||||||
|
let next = (index + 1) % RESOLUTION;
|
||||||
|
lines.push((lower_ring[index], lower_ring[next]));
|
||||||
|
lines.push((upper_ring[index], upper_ring[next]));
|
||||||
|
lines.push((lower_ring[index], upper_ring[index]));
|
||||||
|
|
||||||
|
let angle = index as f32 * std::f32::consts::TAU / RESOLUTION as f32;
|
||||||
|
let (sin_azimuth, cos_azimuth) = angle.sin_cos();
|
||||||
|
let mut lower_previous = lower_ring[index];
|
||||||
|
let mut upper_previous = upper_ring[index];
|
||||||
|
for step in 1..HEMISPHERE_STEPS {
|
||||||
|
let latitude = step as f32 * std::f32::consts::FRAC_PI_2
|
||||||
|
/ HEMISPHERE_STEPS as f32;
|
||||||
|
let (sin_latitude, cos_latitude) = latitude.sin_cos();
|
||||||
|
|
||||||
|
let lower = vertices.len();
|
||||||
|
vertices.push(CapsuleWireVertex {
|
||||||
|
direction: Vec3::new(
|
||||||
|
cos_azimuth * cos_latitude,
|
||||||
|
-sin_latitude,
|
||||||
|
sin_azimuth * cos_latitude,
|
||||||
|
),
|
||||||
|
center_sign: -1.0,
|
||||||
|
});
|
||||||
|
lines.push((lower_previous, lower));
|
||||||
|
lower_previous = lower;
|
||||||
|
|
||||||
|
let upper = vertices.len();
|
||||||
|
vertices.push(CapsuleWireVertex {
|
||||||
|
direction: Vec3::new(
|
||||||
|
cos_azimuth * cos_latitude,
|
||||||
|
sin_latitude,
|
||||||
|
sin_azimuth * cos_latitude,
|
||||||
|
),
|
||||||
|
center_sign: 1.0,
|
||||||
|
});
|
||||||
|
lines.push((upper_previous, upper));
|
||||||
|
upper_previous = upper;
|
||||||
|
}
|
||||||
|
lines.push((lower_previous, lower_pole));
|
||||||
|
lines.push((upper_previous, upper_pole));
|
||||||
|
}
|
||||||
|
|
||||||
|
CapsuleWireframe { vertices, lines }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod capsule_debug_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn capsule_wireframe_caps_extend_in_opposite_directions() {
|
||||||
|
let wireframe = capsule_wireframe();
|
||||||
|
let lower: Vec<_> = wireframe
|
||||||
|
.vertices
|
||||||
|
.iter()
|
||||||
|
.filter(|vertex| vertex.center_sign < 0.0)
|
||||||
|
.collect();
|
||||||
|
let upper: Vec<_> = wireframe
|
||||||
|
.vertices
|
||||||
|
.iter()
|
||||||
|
.filter(|vertex| vertex.center_sign > 0.0)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
assert!(lower.iter().all(|vertex| vertex.direction.y <= 0.0));
|
||||||
|
assert!(upper.iter().all(|vertex| vertex.direction.y >= 0.0));
|
||||||
|
assert!(lower.iter().any(|vertex| vertex.direction == Vec3::NEG_Y));
|
||||||
|
assert!(upper.iter().any(|vertex| vertex.direction == Vec3::Y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn attack_debug(
|
pub fn attack_debug(
|
||||||
query: Query<(
|
query: Query<(
|
||||||
&FighterHitboxes,
|
&FighterHitboxes,
|
||||||
|
|||||||
@@ -0,0 +1,267 @@
|
|||||||
|
//! Fixed-point convex geometry used by fighter hurtboxes.
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
fighter::baked_animation::FixedMat4,
|
||||||
|
math::{
|
||||||
|
int::{FGWide, FGi32},
|
||||||
|
vec3::FGVec3,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A capsule after an arbitrary affine transform. Non-uniform scale turns the
|
||||||
|
/// spherical caps and circular cylinder into ellipsoidal geometry.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct FixedAffineCapsule {
|
||||||
|
pub transform: FixedMat4,
|
||||||
|
pub half_length: FGi32,
|
||||||
|
pub radius: FGi32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An ordinary world-space capsule, used for swept attack volumes.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct FixedCapsule {
|
||||||
|
pub start: FGVec3,
|
||||||
|
pub end: FGVec3,
|
||||||
|
pub radius: FGi32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FixedCapsule {
|
||||||
|
pub fn new(start: FGVec3, end: FGVec3, radius: FGi32) -> Self {
|
||||||
|
Self { start, end, radius }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FixedAffineCapsule {
|
||||||
|
pub fn new(transform: FixedMat4, half_length: FGi32, radius: FGi32) -> Self {
|
||||||
|
Self {
|
||||||
|
transform,
|
||||||
|
half_length,
|
||||||
|
radius,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn transform_point(self, point: FGVec3) -> FGVec3 {
|
||||||
|
self.transform.transform_point(point)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests the exact affine hurtbox against an ordinary world-space capsule.
|
||||||
|
pub fn intersects(self, rhs: FixedCapsule) -> bool {
|
||||||
|
let mut direction = rhs.start - self.transform.get_translation();
|
||||||
|
if direction == FGVec3::ZERO {
|
||||||
|
direction = FGVec3::new(FGi32::ONE, FGi32::ZERO, FGi32::ZERO);
|
||||||
|
}
|
||||||
|
let mut simplex = [FGVec3::ZERO; 4];
|
||||||
|
let mut simplex_len = 0;
|
||||||
|
for _ in 0..20 {
|
||||||
|
let point = self.support(direction) - rhs.support(-direction);
|
||||||
|
if point.dot(direction) < FGWide::ZERO {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for index in (1..=simplex_len.min(3)).rev() {
|
||||||
|
simplex[index] = simplex[index - 1];
|
||||||
|
}
|
||||||
|
simplex[0] = point;
|
||||||
|
simplex_len += 1;
|
||||||
|
if update_simplex(&mut simplex, &mut simplex_len, &mut direction) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if direction == FGVec3::ZERO {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A capped GJK iteration means the origin remains enclosed by the
|
||||||
|
// current simplex to fixed-point precision, so conservatively collide.
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
trait SupportMap {
|
||||||
|
fn support(self, direction: FGVec3) -> FGVec3;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SupportMap for FixedAffineCapsule {
|
||||||
|
fn support(self, direction: FGVec3) -> FGVec3 {
|
||||||
|
let local_direction = FGVec3::new(
|
||||||
|
dot_column(self.transform, 0, direction),
|
||||||
|
dot_column(self.transform, 1, direction),
|
||||||
|
dot_column(self.transform, 2, direction),
|
||||||
|
);
|
||||||
|
let round = local_direction.normalized().unwrap_or(FGVec3::new(
|
||||||
|
FGi32::ONE,
|
||||||
|
FGi32::ZERO,
|
||||||
|
FGi32::ZERO,
|
||||||
|
)) * self.radius;
|
||||||
|
let endpoint = if local_direction.y >= FGi32::ZERO {
|
||||||
|
self.half_length
|
||||||
|
} else {
|
||||||
|
-self.half_length
|
||||||
|
};
|
||||||
|
self.transform_point(round + FGVec3::new(FGi32::ZERO, endpoint, FGi32::ZERO))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SupportMap for FixedCapsule {
|
||||||
|
fn support(self, direction: FGVec3) -> FGVec3 {
|
||||||
|
let endpoint = if self.start.dot(direction) >= self.end.dot(direction) {
|
||||||
|
self.start
|
||||||
|
} else {
|
||||||
|
self.end
|
||||||
|
};
|
||||||
|
endpoint
|
||||||
|
+ direction
|
||||||
|
.normalized()
|
||||||
|
.unwrap_or(FGVec3::new(FGi32::ONE, FGi32::ZERO, FGi32::ZERO))
|
||||||
|
* self.radius
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dot_column(matrix: FixedMat4, column: usize, vector: FGVec3) -> FGi32 {
|
||||||
|
FGi32::from_num(
|
||||||
|
FGWide::from_num(matrix.cols[column][0]) * FGWide::from_num(vector.x)
|
||||||
|
+ FGWide::from_num(matrix.cols[column][1]) * FGWide::from_num(vector.y)
|
||||||
|
+ FGWide::from_num(matrix.cols[column][2]) * FGWide::from_num(vector.z),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cross(lhs: FGVec3, rhs: FGVec3) -> FGVec3 {
|
||||||
|
FGVec3::new(
|
||||||
|
FGi32::from_num(
|
||||||
|
FGWide::from_num(lhs.y) * FGWide::from_num(rhs.z)
|
||||||
|
- FGWide::from_num(lhs.z) * FGWide::from_num(rhs.y),
|
||||||
|
),
|
||||||
|
FGi32::from_num(
|
||||||
|
FGWide::from_num(lhs.z) * FGWide::from_num(rhs.x)
|
||||||
|
- FGWide::from_num(lhs.x) * FGWide::from_num(rhs.z),
|
||||||
|
),
|
||||||
|
FGi32::from_num(
|
||||||
|
FGWide::from_num(lhs.x) * FGWide::from_num(rhs.y)
|
||||||
|
- FGWide::from_num(lhs.y) * FGWide::from_num(rhs.x),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn triple_cross(lhs: FGVec3, middle: FGVec3, rhs: FGVec3) -> FGVec3 {
|
||||||
|
cross(cross(lhs, middle), rhs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_simplex(
|
||||||
|
simplex: &mut [FGVec3; 4],
|
||||||
|
simplex_len: &mut usize,
|
||||||
|
direction: &mut FGVec3,
|
||||||
|
) -> bool {
|
||||||
|
let a = simplex[0];
|
||||||
|
let ao = -a;
|
||||||
|
match *simplex_len {
|
||||||
|
2 => {
|
||||||
|
let ab = simplex[1] - a;
|
||||||
|
if ab.dot(ao) > FGWide::ZERO {
|
||||||
|
*direction = triple_cross(ab, ao, ab);
|
||||||
|
} else {
|
||||||
|
*simplex_len = 1;
|
||||||
|
*direction = ao;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
3 => {
|
||||||
|
let b = simplex[1];
|
||||||
|
let c = simplex[2];
|
||||||
|
let ab = b - a;
|
||||||
|
let ac = c - a;
|
||||||
|
let abc = cross(ab, ac);
|
||||||
|
if cross(abc, ac).dot(ao) > FGWide::ZERO {
|
||||||
|
if ac.dot(ao) > FGWide::ZERO {
|
||||||
|
simplex[1] = c;
|
||||||
|
*simplex_len = 2;
|
||||||
|
*direction = triple_cross(ac, ao, ac);
|
||||||
|
} else {
|
||||||
|
*simplex_len = 2;
|
||||||
|
*direction = triple_cross(ab, ao, ab);
|
||||||
|
}
|
||||||
|
} else if cross(ab, abc).dot(ao) > FGWide::ZERO {
|
||||||
|
*simplex_len = 2;
|
||||||
|
*direction = triple_cross(ab, ao, ab);
|
||||||
|
} else if abc.dot(ao) > FGWide::ZERO {
|
||||||
|
*direction = abc;
|
||||||
|
} else {
|
||||||
|
simplex.swap(1, 2);
|
||||||
|
*direction = -abc;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
4 => {
|
||||||
|
let b = simplex[1];
|
||||||
|
let c = simplex[2];
|
||||||
|
let d = simplex[3];
|
||||||
|
for (first, second, opposite) in [(b, c, d), (c, d, b), (d, b, c)] {
|
||||||
|
let mut normal = cross(first - a, second - a);
|
||||||
|
if normal.dot(opposite - a) > FGWide::ZERO {
|
||||||
|
normal = -normal;
|
||||||
|
}
|
||||||
|
if normal.dot(ao) > FGWide::ZERO {
|
||||||
|
simplex[0] = a;
|
||||||
|
simplex[1] = first;
|
||||||
|
simplex[2] = second;
|
||||||
|
*simplex_len = 3;
|
||||||
|
*direction = normal;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
*direction = ao;
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn affine_capsule(x: &str, scale_y: &str) -> FixedAffineCapsule {
|
||||||
|
let mut transform = FixedMat4::IDENTITY;
|
||||||
|
transform.cols[1][1] = FGi32::lit(scale_y);
|
||||||
|
transform.translate(FGVec3::lit(x, "0", "0"));
|
||||||
|
FixedAffineCapsule::new(transform, FGi32::lit("1"), FGi32::lit("1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn capsule(x: &str) -> FixedCapsule {
|
||||||
|
FixedCapsule::new(
|
||||||
|
FGVec3::lit(x, "-1", "0"),
|
||||||
|
FGVec3::lit(x, "1", "0"),
|
||||||
|
FGi32::lit("1"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn detects_touching_and_separated_capsules() {
|
||||||
|
assert!(affine_capsule("0", "1").intersects(capsule("2")));
|
||||||
|
assert!(!affine_capsule("0", "1").intersects(capsule("2.01")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn affine_scale_changes_capsule_intersection() {
|
||||||
|
let sphere = FixedAffineCapsule::new(
|
||||||
|
FixedMat4::IDENTITY,
|
||||||
|
FGi32::ZERO,
|
||||||
|
FGi32::lit("1"),
|
||||||
|
);
|
||||||
|
let mut squashed_transform = FixedMat4::IDENTITY;
|
||||||
|
squashed_transform.cols[1][1] = FGi32::lit("0.5");
|
||||||
|
let squashed = FixedAffineCapsule::new(
|
||||||
|
squashed_transform,
|
||||||
|
FGi32::ZERO,
|
||||||
|
FGi32::lit("1"),
|
||||||
|
);
|
||||||
|
let target = FixedCapsule::new(
|
||||||
|
FGVec3::lit("0", "1.4", "0"),
|
||||||
|
FGVec3::lit("0", "1.4", "0"),
|
||||||
|
FGi32::lit("0.5"),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(sphere.intersects(target));
|
||||||
|
assert!(!squashed.intersects(target));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,10 @@ use strum::IntoEnumIterator;
|
|||||||
use strum_macros::EnumIter;
|
use strum_macros::EnumIter;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AppState, fighter::{FighterAttributes, FighterCameraProfile, animation::AnimKind, attack::AttackKind}, game_settings::GameSettings, math::{int::FGi32, vec3::FGVec3},
|
AppState,
|
||||||
|
fighter::{FighterAttributes, FighterCameraProfile, animation::AnimKind, attack::AttackKind},
|
||||||
|
game_settings::GameSettings,
|
||||||
|
math::{int::FGi32, vec3::FGVec3},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Reflect, EnumIter)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Reflect, EnumIter)]
|
||||||
@@ -23,7 +26,7 @@ pub struct FighterHurtbox {
|
|||||||
#[reflect(ignore)]
|
#[reflect(ignore)]
|
||||||
pub half_length: FGi32,
|
pub half_length: FGi32,
|
||||||
#[reflect(ignore)]
|
#[reflect(ignore)]
|
||||||
pub radius: FGi32
|
pub radius: FGi32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Asset, Reflect)]
|
#[derive(Serialize, Deserialize, Clone, Asset, Reflect)]
|
||||||
|
|||||||
+12
-7
@@ -13,6 +13,7 @@ pub mod baked_animation;
|
|||||||
pub mod collision;
|
pub mod collision;
|
||||||
pub mod debug;
|
pub mod debug;
|
||||||
pub mod ecb;
|
pub mod ecb;
|
||||||
|
pub mod hurtbox;
|
||||||
pub mod manifest;
|
pub mod manifest;
|
||||||
pub mod motion;
|
pub mod motion;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
@@ -29,7 +30,7 @@ pub use motion::{FighterPreviousTranslation, FighterTranslation, FighterVelocity
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
fighter::{
|
fighter::{
|
||||||
attack::FighterAttackScriptAssets,
|
attack::{FighterAttackPlugin, FighterAttackScriptAssets, FighterSolvedHurtboxes},
|
||||||
manifest::FighterManifest,
|
manifest::FighterManifest,
|
||||||
state::{FighterState, fall::FallState},
|
state::{FighterState, fall::FallState},
|
||||||
visual::FighterAnimations,
|
visual::FighterAnimations,
|
||||||
@@ -105,7 +106,7 @@ pub struct Fighter {
|
|||||||
|
|
||||||
impl Plugin for FighterPlugin {
|
impl Plugin for FighterPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app
|
app.add_plugins(FighterAttackPlugin)
|
||||||
// Every state gets a chance to interrupt itself, in priority order.
|
// Every state gets a chance to interrupt itself, in priority order.
|
||||||
.add_systems(
|
.add_systems(
|
||||||
GgrsSchedule,
|
GgrsSchedule,
|
||||||
@@ -171,10 +172,11 @@ impl Plugin for FighterPlugin {
|
|||||||
)
|
)
|
||||||
.add_systems(
|
.add_systems(
|
||||||
FixedPostUpdate,
|
FixedPostUpdate,
|
||||||
debug::hurtbox_debug
|
debug::hurtbox_debug.run_if(crate::debug_tools::hurtboxes_enabled),
|
||||||
)
|
)
|
||||||
.rollback_component_with_clone::<FighterState>()
|
.rollback_component_with_clone::<FighterState>()
|
||||||
.rollback_component_with_clone::<FighterHitboxes>()
|
.rollback_component_with_clone::<FighterHitboxes>()
|
||||||
|
.rollback_component_with_clone::<FighterSolvedHurtboxes>()
|
||||||
.checksum_component_with_hash::<FighterState>()
|
.checksum_component_with_hash::<FighterState>()
|
||||||
.checksum_component_with_hash::<FighterFacingDirection>()
|
.checksum_component_with_hash::<FighterFacingDirection>()
|
||||||
.register_type::<FighterFacingDirection>();
|
.register_type::<FighterFacingDirection>();
|
||||||
@@ -230,9 +232,12 @@ pub fn spawn_fighter(
|
|||||||
attack_scripts,
|
attack_scripts,
|
||||||
FighterState::Fall(FallState),
|
FighterState::Fall(FallState),
|
||||||
animation::FighterAnimationFrame::new(animation::AnimKind::Wait, true),
|
animation::FighterAnimationFrame::new(animation::AnimKind::Wait, true),
|
||||||
baked_animation::FighterBoneMatrices::default(),
|
(
|
||||||
FighterVisual,
|
baked_animation::FighterBoneMatrices::default(),
|
||||||
FighterInput::default(),
|
FighterSolvedHurtboxes::default(),
|
||||||
crate::camera::FighterCameraExtents::default(),
|
FighterVisual,
|
||||||
|
FighterInput::default(),
|
||||||
|
crate::camera::FighterCameraExtents::default(),
|
||||||
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
fighter::{
|
fighter::{
|
||||||
animation::AnimKind,
|
animation::AnimKind,
|
||||||
collision,
|
collision,
|
||||||
state::{
|
state::{
|
||||||
FighterState, FighterStateContext, FighterStateImpl, air,
|
FighterState, FighterStateContext, FighterStateImpl, air, fall::FallState,
|
||||||
fall::FallState,
|
ground::GroundedStateCommon, land::LandingState,
|
||||||
ground::GroundedStateCommon,
|
|
||||||
land::LandingState,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
input,
|
input,
|
||||||
@@ -51,9 +47,7 @@ impl FighterStateImpl for AirDodgeState {
|
|||||||
let grounded_common = GroundedStateCommon {
|
let grounded_common = GroundedStateCommon {
|
||||||
current_line_id: res.line_id,
|
current_line_id: res.line_id,
|
||||||
};
|
};
|
||||||
Some(FighterState::Land(LandingState {
|
Some(FighterState::Land(LandingState { grounded_common }))
|
||||||
grounded_common
|
|
||||||
}))
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::hash::{Hasher};
|
use std::hash::Hasher;
|
||||||
|
|
||||||
use super::{FighterStateContext, FighterStateImpl, ground, run, wait, walk};
|
use super::{FighterStateContext, FighterStateImpl, ground, run, wait, walk};
|
||||||
use crate::fighter::FighterFacingDirection;
|
use crate::fighter::FighterFacingDirection;
|
||||||
|
|||||||
@@ -45,9 +45,7 @@ impl FighterStateImpl for FallState {
|
|||||||
let grounded_common = GroundedStateCommon {
|
let grounded_common = GroundedStateCommon {
|
||||||
current_line_id: res.line_id,
|
current_line_id: res.line_id,
|
||||||
};
|
};
|
||||||
Some(FighterState::Land(LandingState {
|
Some(FighterState::Land(LandingState { grounded_common }))
|
||||||
grounded_common
|
|
||||||
}))
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
//! 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::{FighterState, FighterStateContext, dash, ground_attack, turn, wait, walk};
|
use crate::fighter::state::{
|
||||||
|
FighterState, FighterStateContext, dash, ground_attack, turn, wait, walk,
|
||||||
|
};
|
||||||
use crate::fighter::{
|
use crate::fighter::{
|
||||||
FighterAttributes, FighterPreviousTranslation, FighterTranslation, FighterVelocity,
|
FighterAttributes, FighterPreviousTranslation, FighterTranslation, FighterVelocity,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
use fixed::types::I16F16;
|
use fixed::types::I16F16;
|
||||||
|
|
||||||
use crate::{fighter::{attack::{AttackKind, update_active_hitbox_list}, state::{FighterState, FighterStateImpl, ground::GroundedStateCommon, wait::WaitState}}, math::int::FGi32};
|
use crate::{
|
||||||
|
fighter::{
|
||||||
|
attack::{AttackKind, update_active_hitbox_list},
|
||||||
|
state::{FighterState, FighterStateImpl, ground::GroundedStateCommon, wait::WaitState},
|
||||||
|
},
|
||||||
|
math::int::FGi32,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Hash)]
|
#[derive(Clone, Debug, Hash)]
|
||||||
pub struct GroundAttackState {
|
pub struct GroundAttackState {
|
||||||
@@ -11,10 +17,13 @@ pub struct GroundAttackState {
|
|||||||
impl FighterStateImpl for GroundAttackState {
|
impl FighterStateImpl for GroundAttackState {
|
||||||
const NAME: &'static str = "GroundAttack";
|
const NAME: &'static str = "GroundAttack";
|
||||||
|
|
||||||
fn check_interrupt(&self, state_context: &mut super::FighterStateContext) -> Option<FighterState> {
|
fn check_interrupt(
|
||||||
|
&self,
|
||||||
|
state_context: &mut super::FighterStateContext,
|
||||||
|
) -> Option<FighterState> {
|
||||||
if state_context.is_current_animation_finished() {
|
if state_context.is_current_animation_finished() {
|
||||||
Some(FighterState::Wait(WaitState {
|
Some(FighterState::Wait(WaitState {
|
||||||
grounded_common: self.grounded_common.clone()
|
grounded_common: self.grounded_common.clone(),
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -22,23 +31,30 @@ impl FighterStateImpl for GroundAttackState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, state_context: &mut super::FighterStateContext) {
|
fn update(&mut self, state_context: &mut super::FighterStateContext) {
|
||||||
let script = state_context.attack_scripts.get(&state_context.attack_script_assets.scripts[&self.attack_kind]);
|
let script = state_context
|
||||||
if let Some(script) = script {
|
.attack_scripts
|
||||||
|
.get(&state_context.attack_script_assets.scripts[&self.attack_kind]);
|
||||||
|
if let Some(script) = script {
|
||||||
let animation_frame = state_context.get_current_animation_frame();
|
let animation_frame = state_context.get_current_animation_frame();
|
||||||
update_active_hitbox_list(script, &mut state_context.hitboxes.active_hitboxes, animation_frame);
|
update_active_hitbox_list(
|
||||||
|
script,
|
||||||
|
&mut state_context.hitboxes.active_hitboxes,
|
||||||
|
animation_frame,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_enter(&mut self, state_context: &mut super::FighterStateContext) {
|
fn on_enter(&mut self, state_context: &mut super::FighterStateContext) {
|
||||||
state_context.play_animation(self.attack_kind.get_animation(), false);
|
state_context.play_animation(self.attack_kind.get_animation(), false);
|
||||||
state_context.hitboxes.attack_script = Some(state_context.attack_script_assets.scripts[&self.attack_kind].clone());
|
state_context.hitboxes.attack_script =
|
||||||
|
Some(state_context.attack_script_assets.scripts[&self.attack_kind].clone());
|
||||||
state_context.hitboxes.active_hitboxes.clear();
|
state_context.hitboxes.active_hitboxes.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_exit(&mut self, state_context: &mut super::FighterStateContext) {
|
fn on_exit(&mut self, state_context: &mut super::FighterStateContext) {
|
||||||
state_context.hitboxes.clear();
|
state_context.hitboxes.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_collision_interrupt(
|
fn check_collision_interrupt(
|
||||||
&mut self,
|
&mut self,
|
||||||
_state_context: &mut super::FighterStateContext,
|
_state_context: &mut super::FighterStateContext,
|
||||||
@@ -47,9 +63,14 @@ impl FighterStateImpl for GroundAttackState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_interrupt(state_context: &super::FighterStateContext, grounded_common: &GroundedStateCommon) -> Option<FighterState> {
|
pub fn check_interrupt(
|
||||||
if state_context.input.has_command(crate::input::FighterCommands::Attack) {
|
state_context: &super::FighterStateContext,
|
||||||
|
grounded_common: &GroundedStateCommon,
|
||||||
|
) -> Option<FighterState> {
|
||||||
|
if state_context
|
||||||
|
.input
|
||||||
|
.has_command(crate::input::FighterCommands::Attack)
|
||||||
|
{
|
||||||
let attack_kind = if state_context.input.get_last_frame().movement.y > FGi32::ZERO {
|
let attack_kind = if state_context.input.get_last_frame().movement.y > FGi32::ZERO {
|
||||||
AttackKind::UpTilt
|
AttackKind::UpTilt
|
||||||
} else {
|
} else {
|
||||||
@@ -58,9 +79,9 @@ pub fn check_interrupt(state_context: &super::FighterStateContext, grounded_comm
|
|||||||
|
|
||||||
Some(FighterState::GroundAttack(GroundAttackState {
|
Some(FighterState::GroundAttack(GroundAttackState {
|
||||||
attack_kind,
|
attack_kind,
|
||||||
grounded_common: grounded_common.clone()
|
grounded_common: grounded_common.clone(),
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -239,9 +238,7 @@ impl FighterStateImpl for JumpState {
|
|||||||
let grounded_common = GroundedStateCommon {
|
let grounded_common = GroundedStateCommon {
|
||||||
current_line_id: res.line_id,
|
current_line_id: res.line_id,
|
||||||
};
|
};
|
||||||
Some(FighterState::Land(LandingState {
|
Some(FighterState::Land(LandingState { grounded_common }))
|
||||||
grounded_common
|
|
||||||
}))
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,14 +13,12 @@ use crate::fighter::{
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Hash)]
|
#[derive(Debug, Clone, Hash)]
|
||||||
pub struct LandingState {
|
pub struct LandingState {
|
||||||
pub grounded_common: GroundedStateCommon
|
pub grounded_common: GroundedStateCommon,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LandingState {
|
impl LandingState {
|
||||||
pub fn create(grounded_common: GroundedStateCommon) -> Self {
|
pub fn create(grounded_common: GroundedStateCommon) -> Self {
|
||||||
Self {
|
Self { grounded_common }
|
||||||
grounded_common
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +28,9 @@ impl FighterStateImpl for LandingState {
|
|||||||
fn check_interrupt(&self, state_context: &mut FighterStateContext) -> Option<FighterState> {
|
fn check_interrupt(&self, state_context: &mut FighterStateContext) -> Option<FighterState> {
|
||||||
if state_context.is_current_animation_finished() {
|
if state_context.is_current_animation_finished() {
|
||||||
grounded_movement_standstill_common_interrupts(state_context, &self.grounded_common)
|
grounded_movement_standstill_common_interrupts(state_context, &self.grounded_common)
|
||||||
} else if state_context.get_current_animation_frame() >= state_context.fighter_manifest.attributes.landing_iasa {
|
} else if state_context.get_current_animation_frame()
|
||||||
|
>= state_context.fighter_manifest.attributes.landing_iasa
|
||||||
|
{
|
||||||
dash::check_input(state_context, &self.grounded_common).or_else(|| {
|
dash::check_input(state_context, &self.grounded_common).or_else(|| {
|
||||||
ground::grounded_movement_common_interrupts(state_context, &self.grounded_common)
|
ground::grounded_movement_common_interrupts(state_context, &self.grounded_common)
|
||||||
.or_else(|| walk::check_input(state_context, &self.grounded_common))
|
.or_else(|| walk::check_input(state_context, &self.grounded_common))
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
use super::{FighterStateContext, FighterStateImpl, ground, wait, walk};
|
use super::{FighterStateContext, FighterStateImpl, ground, wait, walk};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use super::{FighterStateContext, FighterStateImpl};
|
|||||||
use crate::fighter::animation::AnimKind;
|
use crate::fighter::animation::AnimKind;
|
||||||
use crate::fighter::state::fall::FallState;
|
use crate::fighter::state::fall::FallState;
|
||||||
use crate::fighter::state::ground::{self, GroundedMotionResult, GroundedStateCommon};
|
use crate::fighter::state::ground::{self, GroundedMotionResult, GroundedStateCommon};
|
||||||
use crate::fighter::state::{dash, jump, wait, walk, FighterState};
|
use crate::fighter::state::{FighterState, dash, jump, wait, walk};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Hash)]
|
#[derive(Debug, Clone, Hash)]
|
||||||
pub struct TurnState {
|
pub struct TurnState {
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
|
|
||||||
|
|
||||||
use super::{FighterStateContext, FighterStateImpl, ground};
|
use super::{FighterStateContext, FighterStateImpl, ground};
|
||||||
use crate::fighter::animation::AnimKind;
|
use crate::fighter::animation::AnimKind;
|
||||||
use crate::fighter::state::fall::FallState;
|
|
||||||
use crate::fighter::state::ground::{
|
|
||||||
GroundedMotionResult, GroundedStateCommon,
|
|
||||||
};
|
|
||||||
use crate::fighter::state::FighterState;
|
use crate::fighter::state::FighterState;
|
||||||
|
use crate::fighter::state::fall::FallState;
|
||||||
|
use crate::fighter::state::ground::{GroundedMotionResult, GroundedStateCommon};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Hash)]
|
#[derive(Debug, Clone, Hash)]
|
||||||
pub struct WaitState {
|
pub struct WaitState {
|
||||||
@@ -16,12 +12,14 @@ pub struct WaitState {
|
|||||||
impl FighterStateImpl for WaitState {
|
impl FighterStateImpl for WaitState {
|
||||||
const NAME: &'static str = "Wait";
|
const NAME: &'static str = "Wait";
|
||||||
fn check_interrupt(&self, state_context: &mut FighterStateContext) -> Option<FighterState> {
|
fn check_interrupt(&self, state_context: &mut FighterStateContext) -> Option<FighterState> {
|
||||||
if let Some(state) = ground::grounded_movement_common_interrupts(state_context, &self.grounded_common) {
|
if let Some(state) =
|
||||||
|
ground::grounded_movement_common_interrupts(state_context, &self.grounded_common)
|
||||||
|
{
|
||||||
Some(state)
|
Some(state)
|
||||||
} else if let Some(state) = ground::grounded_movement_standstill_common_interrupts(
|
} else if let Some(state) = ground::grounded_movement_standstill_common_interrupts(
|
||||||
state_context,
|
state_context,
|
||||||
&self.grounded_common)
|
&self.grounded_common,
|
||||||
{
|
) {
|
||||||
// Don't transition into yourself
|
// Don't transition into yourself
|
||||||
if let FighterState::Wait(_) = state {
|
if let FighterState::Wait(_) = state {
|
||||||
None
|
None
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
|
|
||||||
use super::{FighterStateContext, FighterStateImpl, dash, ground, wait};
|
use super::{FighterStateContext, FighterStateImpl, dash, ground, wait};
|
||||||
use crate::fighter::animation::AnimKind;
|
use crate::fighter::animation::AnimKind;
|
||||||
use crate::fighter::state::FighterState;
|
use crate::fighter::state::FighterState;
|
||||||
use crate::fighter::state::fall::FallState;
|
use crate::fighter::state::fall::FallState;
|
||||||
use crate::fighter::state::turn;
|
|
||||||
use crate::fighter::state::ground::{GroundedMotionResult, GroundedStateCommon};
|
use crate::fighter::state::ground::{GroundedMotionResult, GroundedStateCommon};
|
||||||
|
use crate::fighter::state::turn;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Hash)]
|
#[derive(Debug, Clone, Hash)]
|
||||||
pub struct WalkState {
|
pub struct WalkState {
|
||||||
@@ -15,9 +14,8 @@ impl FighterStateImpl for WalkState {
|
|||||||
const NAME: &'static str = "Walk";
|
const NAME: &'static str = "Walk";
|
||||||
fn check_interrupt(&self, state_context: &mut FighterStateContext) -> Option<FighterState> {
|
fn check_interrupt(&self, state_context: &mut FighterStateContext) -> Option<FighterState> {
|
||||||
if let Some(state) = turn::check_input(state_context, &self.grounded_common) {
|
if let Some(state) = turn::check_input(state_context, &self.grounded_common) {
|
||||||
return Some(state)
|
return Some(state);
|
||||||
}
|
} else if let Some(state) =
|
||||||
else if let Some(state) =
|
|
||||||
ground::grounded_movement_common_interrupts(state_context, &self.grounded_common)
|
ground::grounded_movement_common_interrupts(state_context, &self.grounded_common)
|
||||||
{
|
{
|
||||||
return Some(state);
|
return Some(state);
|
||||||
|
|||||||
@@ -1,2 +1,99 @@
|
|||||||
pub type FGi32 = fixed::FixedI32<fixed::types::extra::U16>;
|
pub type FGi32 = fixed::FixedI32<fixed::types::extra::U16>;
|
||||||
pub type FGWide = fixed::FixedI128<fixed::types::extra::U32>;
|
pub type FGWide = fixed::FixedI128<fixed::types::extra::U32>;
|
||||||
|
|
||||||
|
/// Angle conversions for the game's Q16.16 fixed-point values.
|
||||||
|
pub trait FGi32Ext {
|
||||||
|
fn to_radians(self) -> FGi32;
|
||||||
|
fn sin_cos(self) -> (FGi32, FGi32);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FGi32Ext for FGi32 {
|
||||||
|
fn to_radians(self) -> FGi32 {
|
||||||
|
FGi32::from_num(FGWide::from_num(self) * FGWide::lit("0.01745329251994329576923690768489"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sin_cos(self) -> (FGi32, FGi32) {
|
||||||
|
const PI: FGi32 = FGi32::lit("3.14159265358979323846");
|
||||||
|
const HALF_PI: FGi32 = FGi32::lit("1.57079632679489661923");
|
||||||
|
const TWO_PI: FGi32 = FGi32::lit("6.28318530717958647692");
|
||||||
|
const CORDIC_GAIN_INVERSE: FGi32 = FGi32::lit("0.60725293500888125617");
|
||||||
|
const ANGLES: [FGi32; 16] = [
|
||||||
|
FGi32::lit("0.78539816339744830962"),
|
||||||
|
FGi32::lit("0.46364760900080611621"),
|
||||||
|
FGi32::lit("0.24497866312686415417"),
|
||||||
|
FGi32::lit("0.12435499454676143503"),
|
||||||
|
FGi32::lit("0.06241880999595734847"),
|
||||||
|
FGi32::lit("0.03123983343026827625"),
|
||||||
|
FGi32::lit("0.01562372862047683080"),
|
||||||
|
FGi32::lit("0.00781234106010111130"),
|
||||||
|
FGi32::lit("0.00390623013196697183"),
|
||||||
|
FGi32::lit("0.00195312251647881869"),
|
||||||
|
FGi32::lit("0.00097656218955931943"),
|
||||||
|
FGi32::lit("0.00048828121119489829"),
|
||||||
|
FGi32::lit("0.00024414062014936177"),
|
||||||
|
FGi32::lit("0.00012207031189367021"),
|
||||||
|
FGi32::lit("0.00006103515617420878"),
|
||||||
|
FGi32::lit("0.00003051757811552610"),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut angle = self % TWO_PI;
|
||||||
|
if angle > PI {
|
||||||
|
angle -= TWO_PI;
|
||||||
|
} else if angle < -PI {
|
||||||
|
angle += TWO_PI;
|
||||||
|
}
|
||||||
|
let mut sign = FGi32::ONE;
|
||||||
|
if angle > HALF_PI {
|
||||||
|
angle -= PI;
|
||||||
|
sign = FGi32::NEG_ONE;
|
||||||
|
} else if angle < -HALF_PI {
|
||||||
|
angle += PI;
|
||||||
|
sign = FGi32::NEG_ONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut x = CORDIC_GAIN_INVERSE;
|
||||||
|
let mut y = FGi32::ZERO;
|
||||||
|
for (index, step) in ANGLES.into_iter().enumerate() {
|
||||||
|
let scale = FGi32::from_bits(1 << (16 - index));
|
||||||
|
if angle >= FGi32::ZERO {
|
||||||
|
let next_x = x - y * scale;
|
||||||
|
y += x * scale;
|
||||||
|
x = next_x;
|
||||||
|
angle -= step;
|
||||||
|
} else {
|
||||||
|
let next_x = x + y * scale;
|
||||||
|
y -= x * scale;
|
||||||
|
x = next_x;
|
||||||
|
angle += step;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let snap_axis = |value: FGi32| {
|
||||||
|
const EPSILON: FGi32 = FGi32::from_bits(4);
|
||||||
|
if value.abs() <= EPSILON {
|
||||||
|
FGi32::ZERO
|
||||||
|
} else if (value.abs() - FGi32::ONE).abs() <= EPSILON {
|
||||||
|
if value < FGi32::ZERO {
|
||||||
|
FGi32::NEG_ONE
|
||||||
|
} else {
|
||||||
|
FGi32::ONE
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
value
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(snap_axis(y * sign), snap_axis(x * sign))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fixed_degrees_convert_to_radians() {
|
||||||
|
assert_eq!(
|
||||||
|
FGi32::lit("180").to_radians(),
|
||||||
|
FGi32::lit("3.1415863037109375")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+65
-8
@@ -1,7 +1,8 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::ops::{Add, Mul, Neg, Sub};
|
||||||
|
|
||||||
use crate::math::int::FGi32;
|
use crate::math::int::{FGWide, FGi32};
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Default, Debug, Reflect, Serialize, Deserialize, Hash)]
|
#[derive(Clone, Copy, PartialEq, Default, Debug, Reflect, Serialize, Deserialize, Hash)]
|
||||||
#[reflect(opaque)]
|
#[reflect(opaque)]
|
||||||
@@ -12,13 +13,14 @@ pub struct FGVec3 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FGVec3 {
|
impl FGVec3 {
|
||||||
|
pub const ZERO: Self = Self {
|
||||||
|
x: FGi32::ZERO,
|
||||||
|
y: FGi32::ZERO,
|
||||||
|
z: FGi32::ZERO,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn new(x: FGi32, y: FGi32, z: FGi32) -> Self {
|
pub const fn new(x: FGi32, y: FGi32, z: FGi32) -> Self {
|
||||||
Self {
|
Self { x, y, z }
|
||||||
x,
|
|
||||||
y,
|
|
||||||
z
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn lit(x: &str, y: &str, z: &str) -> Self {
|
pub fn lit(x: &str, y: &str, z: &str) -> Self {
|
||||||
@@ -32,10 +34,65 @@ impl FGVec3 {
|
|||||||
pub fn to_vec3(&self) -> Vec3 {
|
pub fn to_vec3(&self) -> Vec3 {
|
||||||
Vec3::new(self.x.to_num(), self.y.to_num(), self.z.to_num())
|
Vec3::new(self.x.to_num(), self.y.to_num(), self.z.to_num())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn dot(self, rhs: Self) -> FGWide {
|
||||||
|
FGWide::from_num(self.x) * FGWide::from_num(rhs.x)
|
||||||
|
+ FGWide::from_num(self.y) * FGWide::from_num(rhs.y)
|
||||||
|
+ FGWide::from_num(self.z) * FGWide::from_num(rhs.z)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn length_squared(self) -> FGWide {
|
||||||
|
self.dot(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn normalized(self) -> Option<Self> {
|
||||||
|
let length_squared = self.length_squared();
|
||||||
|
if length_squared == FGWide::ZERO {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let length = length_squared.sqrt();
|
||||||
|
Some(Self::new(
|
||||||
|
FGi32::from_num(FGWide::from_num(self.x) / length),
|
||||||
|
FGi32::from_num(FGWide::from_num(self.y) / length),
|
||||||
|
FGi32::from_num(FGWide::from_num(self.z) / length),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Add for FGVec3 {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn add(self, rhs: Self) -> Self::Output {
|
||||||
|
Self::new(self.x + rhs.x, self.y + rhs.y, self.z + rhs.z)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sub for FGVec3 {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn sub(self, rhs: Self) -> Self::Output {
|
||||||
|
Self::new(self.x - rhs.x, self.y - rhs.y, self.z - rhs.z)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Neg for FGVec3 {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn neg(self) -> Self::Output {
|
||||||
|
Self::new(-self.x, -self.y, -self.z)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mul<FGi32> for FGVec3 {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn mul(self, rhs: FGi32) -> Self::Output {
|
||||||
|
Self::new(self.x * rhs, self.y * rhs, self.z * rhs)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<(&str, &str, &str)> for FGVec3 {
|
impl From<(&str, &str, &str)> for FGVec3 {
|
||||||
fn from((x, y, z): (&str, &str, &str)) -> Self {
|
fn from((x, y, z): (&str, &str, &str)) -> Self {
|
||||||
Self::lit(x, y, z)
|
Self::lit(x, y, z)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user