497 lines
18 KiB
Rust
497 lines
18 KiB
Rust
//! Fighter debug views: gizmos and the egui inspector window.
|
|
|
|
|
|
use bevy::color::palettes::css::{ORANGE, ORANGE_RED, WHITE};
|
|
use bevy::prelude::*;
|
|
use bevy_egui::{egui, EguiContexts};
|
|
|
|
use crate::debug_tools::{DebugSettings, FighterDebugRowKind};
|
|
use crate::fighter::animation::FighterAnimationFrame;
|
|
use crate::fighter::attack::{
|
|
self, FighterKnockbackVelocity, FighterLastAttackDebug, FighterPendingHitReaction,
|
|
FighterSolvedHurtboxes,
|
|
};
|
|
use crate::fighter::baked_animation::{BakedFighterAnimations, FighterBoneMatrices};
|
|
use crate::fighter::ecb::FighterPreviousECB;
|
|
use crate::fighter::errors::FighterErrors;
|
|
use crate::fighter::manifest::FighterManifest;
|
|
use crate::fighter::state::{air, FighterState};
|
|
use crate::fighter::visual::FighterAnimations;
|
|
use crate::fighter::{
|
|
Fighter, FighterECB, FighterFacingDirection, FighterHitboxes, FighterPreviousTranslation,
|
|
FighterTranslation, FighterVelocity,
|
|
};
|
|
use crate::filled_gizmos::FilledGizmos;
|
|
use crate::game_settings::GameSettings;
|
|
use crate::input::FighterInput;
|
|
use crate::math::int::FGi32;
|
|
use crate::math::vec::FGVec2;
|
|
use crate::math::vec3::FGVec3;
|
|
use crate::player::Player;
|
|
use crate::scripting::FighterAttackScript;
|
|
|
|
pub fn debug_draw_ecb(
|
|
ecbs: Query<(
|
|
&FighterECB,
|
|
&FighterTranslation,
|
|
&FighterPreviousECB,
|
|
&FighterPreviousTranslation,
|
|
)>,
|
|
mut gizmos: Gizmos,
|
|
mut filled_gizmos: FilledGizmos,
|
|
) {
|
|
for (ecb, trf, ecb_prev, trf_prev) in ecbs {
|
|
gizmos.cross(
|
|
FGVec3::new(trf.0.x, trf.0.y, FGi32::ZERO).to_vec3(),
|
|
2.0,
|
|
bevy::color::palettes::css::BLUE,
|
|
);
|
|
|
|
let prev_transformed = ecb_prev.to_3d_lineloop(Transform::from_xyz(
|
|
trf_prev.x.to_num::<f32>(),
|
|
trf_prev.y.to_num(),
|
|
0.0,
|
|
));
|
|
filled_gizmos.polygon(&prev_transformed, ORANGE_RED);
|
|
|
|
let transformed =
|
|
ecb.to_3d_lineloop(Transform::from_xyz(trf.x.to_num(), trf.y.to_num(), 0.0));
|
|
filled_gizmos.polygon(&transformed, ORANGE);
|
|
gizmos.lineloop(transformed, WHITE);
|
|
|
|
}
|
|
}
|
|
pub fn fighter_debug(
|
|
mut contexts: EguiContexts,
|
|
query: Query<(
|
|
&Player,
|
|
&FighterVelocity,
|
|
&FighterInput,
|
|
&FighterAnimationFrame,
|
|
&FighterState,
|
|
&FighterAnimations,
|
|
&FighterTranslation,
|
|
&FighterFacingDirection,
|
|
)>,
|
|
anims: Res<Assets<BakedFighterAnimations>>,
|
|
settings: Res<DebugSettings>,
|
|
) -> Result {
|
|
let mut fighters = query.iter().collect::<Vec<_>>();
|
|
fighters.sort_unstable_by_key(|(player, ..)| player.handle);
|
|
|
|
for (player, velocity, input, animation_frame, state, animations, translation, facing) in
|
|
fighters
|
|
{
|
|
let Some(rows) = settings.player_rows(player.handle) else {
|
|
continue;
|
|
};
|
|
if rows.iter().all(|row| *row == FighterDebugRowKind::None) {
|
|
continue;
|
|
}
|
|
|
|
egui::Window::new(format!("Player {}", player.handle + 1)).show(
|
|
contexts.ctx_mut()?,
|
|
|ui| {
|
|
for row in rows {
|
|
match row {
|
|
FighterDebugRowKind::None => {}
|
|
FighterDebugRowKind::CurrentState => {
|
|
let frame_count = anims
|
|
.get(&animations.baked)
|
|
.and_then(|animation| animation.frame_count(animation_frame.kind));
|
|
if let Some(frame_count) = frame_count {
|
|
ui.label(format!(
|
|
"State: {:?} ({:?} {}/{})",
|
|
state.name(),
|
|
animation_frame.kind,
|
|
animation_frame.frame,
|
|
frame_count
|
|
));
|
|
} else {
|
|
ui.label(format!(
|
|
"State: {:?} ({:?})",
|
|
state.name(),
|
|
animation_frame.kind
|
|
));
|
|
}
|
|
}
|
|
FighterDebugRowKind::Velocity => {
|
|
ui.label(format!(
|
|
"Velocity: ({:.2}, {:.2})",
|
|
velocity.0.x, velocity.0.y
|
|
));
|
|
}
|
|
FighterDebugRowKind::Input => {
|
|
const STICK_SIZE: f32 = 50.0;
|
|
const STICK_SIZE_FRACTION: f32 = 0.75;
|
|
let last_frame = input.get_last_frame();
|
|
let movement = egui::Vec2::new(
|
|
last_frame.movement.x.to_num(),
|
|
-last_frame.movement.y.to_num::<f32>(),
|
|
);
|
|
|
|
ui.horizontal(|ui| {
|
|
let (_, rect) = ui.allocate_space(egui::Vec2::new(
|
|
STICK_SIZE * (1.0 + STICK_SIZE_FRACTION),
|
|
STICK_SIZE * (1.0 + STICK_SIZE_FRACTION),
|
|
));
|
|
let painter = ui.painter();
|
|
let button_bg =
|
|
egui::Rgba::WHITE * egui::Rgba::from_white_alpha(0.25);
|
|
painter.circle_filled(rect.center(), STICK_SIZE * 0.5, button_bg);
|
|
painter.circle_filled(
|
|
rect.center() + STICK_SIZE * 0.5 * movement,
|
|
STICK_SIZE * STICK_SIZE_FRACTION * 0.5,
|
|
button_bg,
|
|
);
|
|
ui.label(format!(
|
|
"X: {:.2}\nY:{:.2}",
|
|
last_frame.movement.x, last_frame.movement.y
|
|
));
|
|
});
|
|
|
|
ui.label(format!("Flick age Y {}", input.peek_movement_flick_dbg()));
|
|
}
|
|
FighterDebugRowKind::Position => {
|
|
ui.label(format!("Position: {:?}", translation.0));
|
|
}
|
|
FighterDebugRowKind::Facing => {
|
|
ui.label(format!("Facing: {:?}", facing));
|
|
}
|
|
}
|
|
}
|
|
},
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn animation_debug(
|
|
query: Query<(
|
|
&FighterTranslation,
|
|
&FighterFacingDirection,
|
|
&FighterBoneMatrices,
|
|
&Fighter,
|
|
)>,
|
|
mut gizmos: Gizmos,
|
|
fighter_manifests: Res<Assets<FighterManifest>>,
|
|
) {
|
|
for (global_transform, facing_dir, matrices, fighter) in query {
|
|
let Some(manifest) = fighter_manifests.get(&fighter.manifest) else {
|
|
continue;
|
|
};
|
|
let global_transform = global_transform.get_3d_transform(facing_dir);
|
|
let marker_transform = global_transform.to_mat4();
|
|
for (bone_name, matrix) in matrices.iter() {
|
|
let pos = marker_transform.transform_point(Vec3::new(
|
|
matrix.cols[3][0].to_num(),
|
|
matrix.cols[3][1].to_num(),
|
|
matrix.cols[3][2].to_num(),
|
|
));
|
|
|
|
if manifest.ecb_bones.iter().any(|ecb_bone_name| ecb_bone_name == bone_name) {
|
|
gizmos.cross(pos, 1.0, bevy::color::palettes::css::GREEN);
|
|
} else {
|
|
gizmos.cross(pos, 0.5, bevy::color::palettes::css::RED);
|
|
gizmos.axes(marker_transform, 1.0);
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn hurtbox_debug(
|
|
query: Query<&FighterSolvedHurtboxes>,
|
|
mut filled_gizmos: FilledGizmos,
|
|
) {
|
|
for hurtboxes in &query {
|
|
for hurtbox in &hurtboxes.0 {
|
|
filled_gizmos.affine_capsule(
|
|
hurtbox.capsule.transform.to_mat4(),
|
|
hurtbox.capsule.radius.to_num(),
|
|
hurtbox.capsule.half_length.to_num(),
|
|
Color::srgba(1.0, 1.0, 0.0, 0.75),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*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 animation_markers_ignore_visual_scale() {
|
|
let global = GlobalTransform::from(Transform {
|
|
translation: Vec3::new(3.0, 4.0, 5.0),
|
|
rotation: Quat::from_rotation_y(std::f32::consts::FRAC_PI_2),
|
|
scale: Vec3::splat(10.0),
|
|
});
|
|
let (_, rotation, translation) = global.to_scale_rotation_translation();
|
|
let marker_transform = Transform::from_translation(translation).with_rotation(rotation);
|
|
|
|
let marker = marker_transform.transform_point(Vec3::new(2.0, 0.0, 0.0));
|
|
assert!(marker.abs_diff_eq(Vec3::new(3.0, 4.0, 3.0), 1e-5));
|
|
}
|
|
|
|
/*#[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(
|
|
query: Query<(
|
|
&FighterHitboxes,
|
|
&FighterBoneMatrices,
|
|
&FighterTranslation,
|
|
&FighterFacingDirection,
|
|
&FighterAnimationFrame,
|
|
)>,
|
|
mut gizmos: Gizmos,
|
|
mut filled_gizmos: FilledGizmos,
|
|
attack_scripts: Res<Assets<FighterAttackScript>>
|
|
) {
|
|
for (hitboxes, _matrices, _translation, _facing_direction, _frame) in query {
|
|
if let Some(active_attack) = &hitboxes.0 {
|
|
for solved_hitbox in active_attack.solved_hitboxes.iter() {
|
|
let start = solved_hitbox.capsule.start.to_vec3();
|
|
let end = solved_hitbox.capsule.end.to_vec3();
|
|
let center =
|
|
(solved_hitbox.capsule.start + solved_hitbox.capsule.end) * FGi32::lit("0.5");
|
|
let length = (start - end).length();
|
|
|
|
let aim_dir = (end - start).normalize_or(Vec3::Y);
|
|
|
|
let mut capsule_draw_trf = Transform::IDENTITY;
|
|
capsule_draw_trf.translation = center.to_vec3();
|
|
capsule_draw_trf.align(Dir3::Y, aim_dir, Dir3::X, aim_dir.any_orthonormal_vector());
|
|
|
|
let Some(script) = attack_scripts.get(&active_attack.script) else {
|
|
continue;
|
|
};
|
|
|
|
let hitbox_color = script.hitboxes.get(solved_hitbox.hitbox_idx).expect("This hitbox should be valid").get_debug_draw_color();
|
|
/*
|
|
let attack_capsule = Capsule3d {
|
|
half_length: length * 0.5,
|
|
radius: solved_hitbox.capsule.radius.to_num(),
|
|
};
|
|
gizmos.primitive_3d(
|
|
&attack_capsule,
|
|
capsule_draw_trf.to_isometry(),
|
|
hitbox_color,
|
|
);*/
|
|
|
|
let hitbox_inner_color = bevy::color::Srgba { alpha: 0.18, ..hitbox_color };
|
|
|
|
filled_gizmos.capsule(
|
|
capsule_draw_trf.to_isometry(),
|
|
solved_hitbox.capsule.radius.to_num(),
|
|
length * 0.5,
|
|
hitbox_inner_color,
|
|
);
|
|
|
|
gizmos.cross(start, 1.0, bevy::color::palettes::css::GREEN);
|
|
gizmos.cross(end, 1.0, bevy::color::palettes::css::GREEN);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn attack_debug_gather(
|
|
query: Query<(
|
|
Entity,
|
|
&FighterPendingHitReaction,
|
|
&FighterTranslation,
|
|
&Fighter,
|
|
&FighterInput,
|
|
)>,
|
|
mut commands: Commands,
|
|
manifests: Res<Assets<FighterManifest>>,
|
|
game_settings: Res<GameSettings>,
|
|
) -> Result {
|
|
for (entity, reaction, translation, fighter, input) in query {
|
|
let Some(ref hit) = reaction.0 else {
|
|
continue;
|
|
};
|
|
|
|
let manifest = manifests
|
|
.get(&fighter.manifest)
|
|
.ok_or(FighterErrors::FighterManifestNotFound)?;
|
|
|
|
let launch_dir = attack::directional_influence::apply_directional_influence(
|
|
input.get_last_frame().movement,
|
|
hit.launch_angle,
|
|
game_settings.fighter_common.di_max_angle,
|
|
);
|
|
|
|
let mut knockback_vel = FighterKnockbackVelocity(launch_dir * hit.launch_velocity);
|
|
let mut fighter_vel = FighterVelocity(FGVec2::ZERO);
|
|
let mut translation = Vec3::new(translation.x.to_num(), translation.y.to_num(), 0.0);
|
|
|
|
let mut points = vec![];
|
|
points.push(translation);
|
|
points.reserve(hit.hitstun_frames as usize);
|
|
|
|
for _ in 1..hit.hitstun_frames {
|
|
air::integrate_gravity(&mut fighter_vel, &manifest.attributes, false);
|
|
|
|
let vel = knockback_vel.length();
|
|
let new_vel = vel
|
|
.saturating_sub(game_settings.fighter_common.knockback_decay_per_frame)
|
|
.max(FGi32::ZERO);
|
|
knockback_vel = FighterKnockbackVelocity(knockback_vel.0.normalize_or_zero() * new_vel);
|
|
translation += Vec3::new(knockback_vel.x.to_num(), knockback_vel.y.to_num(), 0.0);
|
|
translation += Vec3::new(fighter_vel.x.to_num(), fighter_vel.y.to_num(), 0.0);
|
|
points.push(translation);
|
|
}
|
|
commands.entity(entity).insert(FighterLastAttackDebug {
|
|
trajectory_points: points,
|
|
});
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn attack_launch_debug_draw(query: Query<&FighterLastAttackDebug>, mut gizmos: Gizmos) {
|
|
for debug_attack in query {
|
|
gizmos.linestrip(
|
|
debug_attack.trajectory_points.iter().copied(),
|
|
bevy::color::palettes::css::GREEN,
|
|
);
|
|
}
|
|
}
|