Files
shinespark/src/fighter/debug.rs
T
EIREXE 10cad6d944
CI / build-and-test (pull_request) Successful in 2m48s
CI / production-build (pull_request) Successful in 11m57s
Add ECB bone markers
2026-10-12 17:30:02 +02:00

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,
);
}
}