Implement hitbox viewer #81

Merged
EIREXE merged 3 commits from feature/hitbox-viewer into master 2026-10-04 11:32:28 +00:00
5 changed files with 484 additions and 155 deletions
Showing only changes of commit 7beb22cdfe - Show all commits
Generated
+407 -151
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -13,7 +13,7 @@ debug_features = ["bevy/free_camera"]
gamecube-adapter = ["dep:gc-adapter-neo"]
[dependencies]
bevy = { version = "0.19", features = ["asset_processor", "basis-universal", "compressed_image_saver", "bevy_settings"] }
bevy = { version = "0.19.1", features = ["asset_processor", "basis-universal", "compressed_image_saver", "bevy_settings"] }
# Loads the scenes you author in the editor (assets/*.bsn) and builds
# physics colliders from them. An ordinary dependency; remove it if you
# stop using authored scenes.
@@ -24,6 +24,7 @@ arrayvec = "0.7.8"
bevy-inspector-egui = "0.37.0"
bevy_egui = "0.40.0"
bevy_ggrs = { git = "https://github.com/gschup/bevy_ggrs" }
bevygraph = { git = "https://github.com/tbillington/bevy_debug_view" }
serde = "1.0.229"
serde_json = "1.0.145"
bincode = "1.3.3"
+1
View File
@@ -99,4 +99,5 @@ pub enum AnimKind {
AttackForwardTilt,
AttackDownTilt,
AttackNeutralAir,
AttackForwardAir,
}
+1 -1
View File
@@ -200,7 +200,7 @@ impl AttackKind {
AttackKind::NeutralAir => AnimKind::AttackNeutralAir,
AttackKind::Jab => AnimKind::AttackJab1,
AttackKind::UpTilt => AnimKind::AttackUpTilt,
AttackKind::ForwardAir => AnimKind::AttackUpTilt,
AttackKind::ForwardAir => AnimKind::AttackForwardAir,
AttackKind::BackAir => AnimKind::AttackUpTilt,
AttackKind::DownAir => AnimKind::AttackUpTilt,
AttackKind::UpAir => AnimKind::AttackUpTilt,
+73 -2
View File
@@ -1,6 +1,6 @@
//! Standalone, read-only attack-script previewer used by `hitbox-editor`.
use bevy::{gltf::Gltf, prelude::*, world_serialization::WorldAssetRoot};
use bevy::{camera::CameraProjection, gltf::Gltf, prelude::*, world_serialization::WorldAssetRoot};
use bevy_egui::{EguiContexts, EguiPrimaryContextPass, egui};
use clap::{Parser, ValueEnum};
@@ -116,7 +116,7 @@ impl Plugin for HitboxViewerPlugin {
})
.add_systems(Startup, setup)
.add_systems(
Update,
FixedUpdate,
(
request_fighter_dependencies,
spawn_preview_fighter,
@@ -532,11 +532,82 @@ fn draw_ui(
ui.label("Waiting for the processed attack script…");
return;
};
draw_summary_timeline(ui, &mut state, script, end);
draw_timeline(ui, &mut state, script, end);
});
Ok(())
}
fn draw_summary_timeline(
ui: &mut egui::Ui,
state: &mut ViewerState,
script: &FighterAttackScript,
end: u32,
) {
let row_height = 20.0;
let label_width = 55.0;
let height = (row_height + 25.0f32).min(ui.available_height());
let (response, painter) = ui.allocate_painter(
egui::vec2(ui.available_width(), height),
egui::Sense::click_and_drag(),
);
let rect = response.rect;
let timeline = egui::Rect::from_min_max(
egui::pos2(rect.left() + label_width, rect.top()),
rect.right_bottom(),
);
if (response.clicked() || response.dragged())
&& let Some(pointer) = response.interact_pointer_pos()
&& timeline.contains(pointer)
{
let fraction = ((pointer.x - timeline.left()) / timeline.width()).clamp(0.0, 1.0);
state.frame = ((end.saturating_sub(1)) as f32 * fraction).round() as u32;
state.playing = false;
}
for frame in 0..end {
if frame % 5 != 0 {
continue;
}
let x = frame_x(timeline, frame, end);
painter.line_segment(
[egui::pos2(x, rect.top()), egui::pos2(x, rect.bottom())],
egui::Stroke::new(1.0_f32, egui::Color32::from_gray(55)),
);
painter.text(
egui::pos2(x + 2.0, rect.top()),
egui::Align2::LEFT_TOP,
frame.to_string(),
egui::FontId::monospace(10.0),
egui::Color32::GRAY,
);
}
for frame in 0..end {
let top = rect.top() + 20.0 as f32;
painter.text(
egui::pos2(rect.left(), top + 2.0),
egui::Align2::LEFT_TOP,
format!("HB {}", 2),
egui::FontId::monospace(12.0),
egui::Color32::LIGHT_GRAY,
);
let active_hitbox = script
.hitboxes
.iter()
.filter(|hitbox| hitbox.start_frame <= frame && hitbox.end_frame > frame)
.max_by(|a, b| a.id.cmp(&b.id));
let bar = egui::Rect::from_min_max(
egui::pos2(frame_x(timeline, frame, end), top + 2.0),
egui::pos2(frame_x(timeline, frame + 1, end), top + row_height - 2.0),
);
if let Some(_active_hitbox) = active_hitbox {
painter.rect_filled(bar, 2.0, egui::Color32::from_rgb(245, 75, 55));
} else {
painter.rect_stroke(bar, 2.0, egui::Stroke::new(2.0f32, egui::Color32::from_rgb(75, 75, 75)), egui::StrokeKind::Inside);
}
}
}
fn draw_timeline(
ui: &mut egui::Ui,
state: &mut ViewerState,