Add controller-driven debug menu
This commit is contained in:
@@ -785,23 +785,4 @@ mod tests {
|
|||||||
assert!((sixty_hz_lerp_alpha(0.09, 1.0 / 60.0) - 0.09).abs() < f32::EPSILON);
|
assert!((sixty_hz_lerp_alpha(0.09, 1.0 / 60.0) - 0.09).abs() < f32::EPSILON);
|
||||||
assert!(sixty_hz_lerp_alpha(0.09, 1.0 / 30.0) > 0.09);
|
assert!(sixty_hz_lerp_alpha(0.09, 1.0 / 30.0) > 0.09);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn battlefield_profile_matches_the_source_stage_data() {
|
|
||||||
let manifest: crate::stage::manifest::StageManifest =
|
|
||||||
ron::from_str(include_str!("../../assets/stages/test.stage.ron")).unwrap();
|
|
||||||
let profile = manifest.camera;
|
|
||||||
|
|
||||||
assert_eq!((profile.left, profile.right), (-200.0, 200.0));
|
|
||||||
assert_eq!((profile.bottom, profile.top), (-59.0, 170.0));
|
|
||||||
assert_eq!(profile.origin, Vec2::new(0.0, 44.0));
|
|
||||||
assert_eq!(profile.vertical_fov_degrees, 30.0);
|
|
||||||
assert_eq!((profile.min_depth, profile.max_depth), (83.0, 1000.0));
|
|
||||||
assert_eq!(profile.subject_scale, 1.5);
|
|
||||||
assert_eq!(profile.fighter_forward_extent_scale, 1.5);
|
|
||||||
assert_eq!(profile.tracking_smoothness, 1.8);
|
|
||||||
assert_eq!(profile.vertical_pan_degrees, -10.0);
|
|
||||||
assert_eq!(profile.horizontal_pan_degrees_per_unit, 0.1);
|
|
||||||
assert_eq!(profile.vertical_pan_degrees_per_unit, 0.1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,512 @@
|
|||||||
|
//! Local developer overlays and their controller-driven palette.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy_egui::{EguiContexts, EguiPrimaryContextPass, egui};
|
||||||
|
|
||||||
|
use crate::player::Player;
|
||||||
|
|
||||||
|
pub const PLAYER_DEBUG_ROW_COUNT: usize = 6;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum FighterDebugRowKind {
|
||||||
|
None,
|
||||||
|
CurrentState,
|
||||||
|
Velocity,
|
||||||
|
Input,
|
||||||
|
Position,
|
||||||
|
Facing,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FighterDebugRowKind {
|
||||||
|
const ALL: [Self; 6] = [
|
||||||
|
Self::None,
|
||||||
|
Self::CurrentState,
|
||||||
|
Self::Velocity,
|
||||||
|
Self::Input,
|
||||||
|
Self::Position,
|
||||||
|
Self::Facing,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::None => "None",
|
||||||
|
Self::CurrentState => "Current state",
|
||||||
|
Self::Velocity => "Velocity",
|
||||||
|
Self::Input => "Input",
|
||||||
|
Self::Position => "Position",
|
||||||
|
Self::Facing => "Facing",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cycle(self, direction: i8) -> Self {
|
||||||
|
let index = Self::ALL
|
||||||
|
.iter()
|
||||||
|
.position(|kind| *kind == self)
|
||||||
|
.expect("all row kinds must be listed") as i8;
|
||||||
|
let index = (index + direction).rem_euclid(Self::ALL.len() as i8) as usize;
|
||||||
|
Self::ALL[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum DebugFlag {
|
||||||
|
Network,
|
||||||
|
Ecb,
|
||||||
|
StageCollision,
|
||||||
|
AnimationBones,
|
||||||
|
AttackHitboxes,
|
||||||
|
DrawThroughGeometry,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum DebugMenuEntry {
|
||||||
|
Toggle {
|
||||||
|
label: &'static str,
|
||||||
|
flag: DebugFlag,
|
||||||
|
},
|
||||||
|
Submenu {
|
||||||
|
label: &'static str,
|
||||||
|
entries: &'static [DebugMenuEntry],
|
||||||
|
},
|
||||||
|
PlayerSlots {
|
||||||
|
label: &'static str,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const FIGHTER_MENU: &[DebugMenuEntry] = &[
|
||||||
|
DebugMenuEntry::PlayerSlots {
|
||||||
|
label: "Player windows",
|
||||||
|
},
|
||||||
|
DebugMenuEntry::Toggle {
|
||||||
|
label: "ECB",
|
||||||
|
flag: DebugFlag::Ecb,
|
||||||
|
},
|
||||||
|
DebugMenuEntry::Toggle {
|
||||||
|
label: "Animation bones",
|
||||||
|
flag: DebugFlag::AnimationBones,
|
||||||
|
},
|
||||||
|
DebugMenuEntry::Toggle {
|
||||||
|
label: "Attack hitboxes",
|
||||||
|
flag: DebugFlag::AttackHitboxes,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const WORLD_MENU: &[DebugMenuEntry] = &[DebugMenuEntry::Toggle {
|
||||||
|
label: "Stage collision",
|
||||||
|
flag: DebugFlag::StageCollision,
|
||||||
|
}];
|
||||||
|
|
||||||
|
const DIAGNOSTICS_MENU: &[DebugMenuEntry] = &[DebugMenuEntry::Toggle {
|
||||||
|
label: "Network stats",
|
||||||
|
flag: DebugFlag::Network,
|
||||||
|
}];
|
||||||
|
|
||||||
|
const RENDERING_MENU: &[DebugMenuEntry] = &[DebugMenuEntry::Toggle {
|
||||||
|
label: "Draw through geometry",
|
||||||
|
flag: DebugFlag::DrawThroughGeometry,
|
||||||
|
}];
|
||||||
|
|
||||||
|
const ROOT_MENU: &[DebugMenuEntry] = &[
|
||||||
|
DebugMenuEntry::Submenu {
|
||||||
|
label: "Fighter",
|
||||||
|
entries: FIGHTER_MENU,
|
||||||
|
},
|
||||||
|
DebugMenuEntry::Submenu {
|
||||||
|
label: "World",
|
||||||
|
entries: WORLD_MENU,
|
||||||
|
},
|
||||||
|
DebugMenuEntry::Submenu {
|
||||||
|
label: "Diagnostics",
|
||||||
|
entries: DIAGNOSTICS_MENU,
|
||||||
|
},
|
||||||
|
DebugMenuEntry::Submenu {
|
||||||
|
label: "Rendering",
|
||||||
|
entries: RENDERING_MENU,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum DebugMenuPage {
|
||||||
|
Static(&'static [DebugMenuEntry]),
|
||||||
|
PlayerList,
|
||||||
|
PlayerRows(usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct DebugSettings {
|
||||||
|
pub menu_open: bool,
|
||||||
|
controller: Option<Entity>,
|
||||||
|
pages: Vec<DebugMenuPage>,
|
||||||
|
selections: Vec<usize>,
|
||||||
|
pub player_rows: HashMap<usize, [FighterDebugRowKind; PLAYER_DEBUG_ROW_COUNT]>,
|
||||||
|
pub network: bool,
|
||||||
|
pub ecb: bool,
|
||||||
|
pub stage_collision: bool,
|
||||||
|
pub animation_bones: bool,
|
||||||
|
pub attack_hitboxes: bool,
|
||||||
|
pub draw_through_geometry: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DebugSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
menu_open: false,
|
||||||
|
controller: None,
|
||||||
|
pages: vec![DebugMenuPage::Static(ROOT_MENU)],
|
||||||
|
selections: vec![0],
|
||||||
|
player_rows: HashMap::new(),
|
||||||
|
network: false,
|
||||||
|
ecb: false,
|
||||||
|
stage_collision: false,
|
||||||
|
animation_bones: false,
|
||||||
|
attack_hitboxes: false,
|
||||||
|
draw_through_geometry: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DebugSettings {
|
||||||
|
pub fn player_rows(
|
||||||
|
&self,
|
||||||
|
handle: usize,
|
||||||
|
) -> Option<&[FighterDebugRowKind; PLAYER_DEBUG_ROW_COUNT]> {
|
||||||
|
self.player_rows.get(&handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset_navigation(&mut self) {
|
||||||
|
self.pages.truncate(1);
|
||||||
|
self.selections.truncate(1);
|
||||||
|
self.selections[0] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn page(&self) -> DebugMenuPage {
|
||||||
|
*self
|
||||||
|
.pages
|
||||||
|
.last()
|
||||||
|
.expect("debug menu always has a root page")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected(&self) -> usize {
|
||||||
|
*self
|
||||||
|
.selections
|
||||||
|
.last()
|
||||||
|
.expect("debug menu always has a root selection")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_mut(&mut self) -> &mut usize {
|
||||||
|
self.selections
|
||||||
|
.last_mut()
|
||||||
|
.expect("debug menu always has a root selection")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_page(&mut self, page: DebugMenuPage) {
|
||||||
|
self.pages.push(page);
|
||||||
|
self.selections.push(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pop_page(&mut self) -> bool {
|
||||||
|
if self.pages.len() == 1 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.pages.pop();
|
||||||
|
self.selections.pop();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flag(&self, flag: DebugFlag) -> bool {
|
||||||
|
match flag {
|
||||||
|
DebugFlag::Network => self.network,
|
||||||
|
DebugFlag::Ecb => self.ecb,
|
||||||
|
DebugFlag::StageCollision => self.stage_collision,
|
||||||
|
DebugFlag::AnimationBones => self.animation_bones,
|
||||||
|
DebugFlag::AttackHitboxes => self.attack_hitboxes,
|
||||||
|
DebugFlag::DrawThroughGeometry => self.draw_through_geometry,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_flag(&mut self, flag: DebugFlag) {
|
||||||
|
match flag {
|
||||||
|
DebugFlag::Network => self.network = !self.network,
|
||||||
|
DebugFlag::Ecb => self.ecb = !self.ecb,
|
||||||
|
DebugFlag::StageCollision => self.stage_collision = !self.stage_collision,
|
||||||
|
DebugFlag::AnimationBones => self.animation_bones = !self.animation_bones,
|
||||||
|
DebugFlag::AttackHitboxes => self.attack_hitboxes = !self.attack_hitboxes,
|
||||||
|
DebugFlag::DrawThroughGeometry => {
|
||||||
|
self.draw_through_geometry = !self.draw_through_geometry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rows_mut(&mut self, handle: usize) -> &mut [FighterDebugRowKind; PLAYER_DEBUG_ROW_COUNT] {
|
||||||
|
self.player_rows
|
||||||
|
.entry(handle)
|
||||||
|
.or_insert([FighterDebugRowKind::None; PLAYER_DEBUG_ROW_COUNT])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DebugToolsPlugin;
|
||||||
|
|
||||||
|
impl Plugin for DebugToolsPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.init_resource::<DebugSettings>()
|
||||||
|
.add_systems(
|
||||||
|
Update,
|
||||||
|
(handle_palette_input, sync_gizmo_depth_bias).chain(),
|
||||||
|
)
|
||||||
|
.add_systems(EguiPrimaryContextPass, draw_palette);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn player_handles(players: &Query<&Player>) -> Vec<usize> {
|
||||||
|
let mut handles = players
|
||||||
|
.iter()
|
||||||
|
.map(|player| player.handle)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
handles.sort_unstable();
|
||||||
|
handles.dedup();
|
||||||
|
handles
|
||||||
|
}
|
||||||
|
|
||||||
|
fn page_len(page: DebugMenuPage, player_handles: &[usize]) -> usize {
|
||||||
|
match page {
|
||||||
|
DebugMenuPage::Static(entries) => entries.len(),
|
||||||
|
DebugMenuPage::PlayerList => player_handles.len(),
|
||||||
|
DebugMenuPage::PlayerRows(_) => PLAYER_DEBUG_ROW_COUNT,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_palette_input(
|
||||||
|
keyboard: Res<ButtonInput<KeyCode>>,
|
||||||
|
gamepads: Query<(Entity, &Gamepad)>,
|
||||||
|
players: Query<&Player>,
|
||||||
|
mut settings: ResMut<DebugSettings>,
|
||||||
|
) {
|
||||||
|
if keyboard.just_pressed(KeyCode::KeyT) {
|
||||||
|
settings.toggle_flag(DebugFlag::DrawThroughGeometry);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !settings.menu_open {
|
||||||
|
for (entity, gamepad) in &gamepads {
|
||||||
|
if gamepad.just_pressed(GamepadButton::Start) && gamepad.pressed(GamepadButton::DPadUp)
|
||||||
|
{
|
||||||
|
settings.menu_open = true;
|
||||||
|
settings.controller = Some(entity);
|
||||||
|
settings.reset_navigation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(controller) = settings.controller else {
|
||||||
|
settings.menu_open = false;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok((_, gamepad)) = gamepads.get(controller) else {
|
||||||
|
settings.menu_open = false;
|
||||||
|
settings.controller = None;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let player_handles = player_handles(&players);
|
||||||
|
let page = settings.page();
|
||||||
|
let page_len = page_len(page, &player_handles);
|
||||||
|
|
||||||
|
if page_len != 0 && settings.selected() >= page_len {
|
||||||
|
*settings.selected_mut() = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if gamepad.just_pressed(GamepadButton::DPadUp) && page_len != 0 {
|
||||||
|
let selected = settings.selected_mut();
|
||||||
|
*selected = selected.checked_sub(1).unwrap_or(page_len - 1);
|
||||||
|
}
|
||||||
|
if gamepad.just_pressed(GamepadButton::DPadDown) && page_len != 0 {
|
||||||
|
let selected = settings.selected_mut();
|
||||||
|
*selected = (*selected + 1) % page_len;
|
||||||
|
}
|
||||||
|
if matches!(page, DebugMenuPage::PlayerRows(_)) {
|
||||||
|
let direction = if gamepad.just_pressed(GamepadButton::DPadLeft) {
|
||||||
|
Some(-1)
|
||||||
|
} else if gamepad.just_pressed(GamepadButton::DPadRight) {
|
||||||
|
Some(1)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
if let Some(direction) = direction {
|
||||||
|
let DebugMenuPage::PlayerRows(handle) = page else {
|
||||||
|
unreachable!("player row navigation must have a player row page")
|
||||||
|
};
|
||||||
|
let selected = settings.selected();
|
||||||
|
let rows = settings.rows_mut(handle);
|
||||||
|
rows[selected] = rows[selected].cycle(direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if gamepad.just_pressed(GamepadButton::South) {
|
||||||
|
match page {
|
||||||
|
DebugMenuPage::Static(entries) if page_len != 0 => match entries[settings.selected()] {
|
||||||
|
DebugMenuEntry::Toggle { flag, .. } => settings.toggle_flag(flag),
|
||||||
|
DebugMenuEntry::Submenu { entries, .. } => {
|
||||||
|
settings.push_page(DebugMenuPage::Static(entries));
|
||||||
|
}
|
||||||
|
DebugMenuEntry::PlayerSlots { .. } => settings.push_page(DebugMenuPage::PlayerList),
|
||||||
|
},
|
||||||
|
DebugMenuPage::PlayerList if page_len != 0 => {
|
||||||
|
let handle = player_handles[settings.selected()];
|
||||||
|
settings.rows_mut(handle);
|
||||||
|
settings.push_page(DebugMenuPage::PlayerRows(handle));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if gamepad.just_pressed(GamepadButton::West) && !settings.pop_page() {
|
||||||
|
settings.menu_open = false;
|
||||||
|
settings.controller = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sync_gizmo_depth_bias(settings: Res<DebugSettings>, mut configs: ResMut<GizmoConfigStore>) {
|
||||||
|
if !settings.is_changed() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let depth_bias = if settings.draw_through_geometry {
|
||||||
|
-1.0
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
for (_, config, _) in configs.iter_mut() {
|
||||||
|
config.depth_bias = depth_bias;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_palette(
|
||||||
|
mut contexts: EguiContexts,
|
||||||
|
settings: Res<DebugSettings>,
|
||||||
|
players: Query<&Player>,
|
||||||
|
) -> Result {
|
||||||
|
if !settings.menu_open {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let player_handles = player_handles(&players);
|
||||||
|
let page = settings.page();
|
||||||
|
let selected = settings.selected();
|
||||||
|
egui::Window::new("Debug")
|
||||||
|
.anchor(egui::Align2::RIGHT_TOP, egui::vec2(-12.0, 12.0))
|
||||||
|
.resizable(false)
|
||||||
|
.show(contexts.ctx_mut()?, |ui| {
|
||||||
|
ui.style_mut().override_font_id = Some(egui::FontId::proportional(20.0));
|
||||||
|
match page {
|
||||||
|
DebugMenuPage::Static(entries) => {
|
||||||
|
for (index, entry) in entries.iter().enumerate() {
|
||||||
|
let marker = if index == selected { ">" } else { " " };
|
||||||
|
match entry {
|
||||||
|
DebugMenuEntry::Toggle { label, flag } => {
|
||||||
|
let state = if settings.flag(*flag) { "on" } else { "off" };
|
||||||
|
ui.label(format!("{marker} {label}: {state}"));
|
||||||
|
}
|
||||||
|
DebugMenuEntry::Submenu { label, .. }
|
||||||
|
| DebugMenuEntry::PlayerSlots { label } => {
|
||||||
|
ui.label(format!("{marker} {label} >"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DebugMenuPage::PlayerList => {
|
||||||
|
for (index, handle) in player_handles.iter().enumerate() {
|
||||||
|
let marker = if index == selected { ">" } else { " " };
|
||||||
|
ui.label(format!("{marker} Player {} >", handle + 1));
|
||||||
|
}
|
||||||
|
if player_handles.is_empty() {
|
||||||
|
ui.label("No players in match");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DebugMenuPage::PlayerRows(handle) => {
|
||||||
|
let rows = settings
|
||||||
|
.player_rows(handle)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or([FighterDebugRowKind::None; PLAYER_DEBUG_ROW_COUNT]);
|
||||||
|
ui.label(format!("Player {}", handle + 1));
|
||||||
|
for (index, row) in rows.iter().enumerate() {
|
||||||
|
let marker = if index == selected { ">" } else { " " };
|
||||||
|
ui.label(format!("{marker} Row {}: {}", index + 1, row.label()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ui.separator();
|
||||||
|
let controls = if matches!(page, DebugMenuPage::PlayerRows(_)) {
|
||||||
|
"D-pad: select/change B: back"
|
||||||
|
} else {
|
||||||
|
"D-pad: select A: enter/toggle B: back"
|
||||||
|
};
|
||||||
|
ui.label(controls);
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fighter_info_enabled(settings: Res<DebugSettings>) -> bool {
|
||||||
|
settings
|
||||||
|
.player_rows
|
||||||
|
.values()
|
||||||
|
.any(|rows| rows.iter().any(|row| *row != FighterDebugRowKind::None))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn network_enabled(settings: Res<DebugSettings>) -> bool {
|
||||||
|
settings.network
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ecb_enabled(settings: Res<DebugSettings>) -> bool {
|
||||||
|
settings.ecb
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stage_collision_enabled(settings: Res<DebugSettings>) -> bool {
|
||||||
|
settings.stage_collision
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn animation_bones_enabled(settings: Res<DebugSettings>) -> bool {
|
||||||
|
settings.animation_bones
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn attack_hitboxes_enabled(settings: Res<DebugSettings>) -> bool {
|
||||||
|
settings.attack_hitboxes
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn player_rows_default_to_none_and_wrap() {
|
||||||
|
let mut settings = DebugSettings::default();
|
||||||
|
let rows = settings.rows_mut(2);
|
||||||
|
|
||||||
|
assert_eq!(*rows, [FighterDebugRowKind::None; PLAYER_DEBUG_ROW_COUNT]);
|
||||||
|
rows[0] = rows[0].cycle(-1);
|
||||||
|
assert_eq!(rows[0], FighterDebugRowKind::Facing);
|
||||||
|
rows[0] = rows[0].cycle(1);
|
||||||
|
assert_eq!(rows[0], FighterDebugRowKind::None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nested_navigation_preserves_parent_selection() {
|
||||||
|
let mut settings = DebugSettings::default();
|
||||||
|
*settings.selected_mut() = 2;
|
||||||
|
settings.push_page(DebugMenuPage::Static(DIAGNOSTICS_MENU));
|
||||||
|
|
||||||
|
assert!(settings.pop_page());
|
||||||
|
assert_eq!(settings.selected(), 2);
|
||||||
|
assert!(!settings.pop_page());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn player_row_configuration_is_slot_specific() {
|
||||||
|
let mut settings = DebugSettings::default();
|
||||||
|
settings.rows_mut(0)[0] = FighterDebugRowKind::CurrentState;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
settings.player_rows(0).unwrap()[0],
|
||||||
|
FighterDebugRowKind::CurrentState
|
||||||
|
);
|
||||||
|
assert!(settings.player_rows(1).is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -93,7 +93,7 @@ pub struct AttackHitbox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_active_hitbox_list(script: &FighterAttackScript, list: &mut Vec<usize>, frame: u32) {
|
pub fn update_active_hitbox_list(script: &FighterAttackScript, list: &mut Vec<usize>, frame: u32) {
|
||||||
*list = script.hitboxes.iter().enumerate().filter(|(i, hb)| {
|
*list = script.hitboxes.iter().enumerate().filter(|(_, hb)| {
|
||||||
hb.start_frame <= frame && hb.end_frame > frame
|
hb.start_frame <= frame && hb.end_frame > frame
|
||||||
}).map(|(i, _)| i).collect()
|
}).map(|(i, _)| i).collect()
|
||||||
}
|
}
|
||||||
|
|||||||
+72
-19
@@ -14,7 +14,9 @@ use crate::fighter::{
|
|||||||
};
|
};
|
||||||
use crate::input::FighterInput;
|
use crate::input::FighterInput;
|
||||||
use crate::math::vec3::FGVec3;
|
use crate::math::vec3::FGVec3;
|
||||||
|
use crate::player::Player;
|
||||||
use crate::scripting::FighterAttackScript;
|
use crate::scripting::FighterAttackScript;
|
||||||
|
use crate::debug_tools::{DebugSettings, FighterDebugRowKind};
|
||||||
|
|
||||||
pub fn debug_draw_ecb(
|
pub fn debug_draw_ecb(
|
||||||
ecbs: Query<(
|
ecbs: Query<(
|
||||||
@@ -39,13 +41,77 @@ pub fn debug_draw_ecb(
|
|||||||
}
|
}
|
||||||
pub fn fighter_debug(
|
pub fn fighter_debug(
|
||||||
mut contexts: EguiContexts,
|
mut contexts: EguiContexts,
|
||||||
query: Query<(&FighterVelocity, &FighterInput, &StateNameDebug)>,
|
query: Query<(
|
||||||
|
&Player,
|
||||||
|
&FighterVelocity,
|
||||||
|
&FighterInput,
|
||||||
|
&FighterAnimationFrame,
|
||||||
|
&StateNameDebug,
|
||||||
|
&FighterAnimations,
|
||||||
|
&FighterTranslation,
|
||||||
|
&FighterFacingDirection,
|
||||||
|
)>,
|
||||||
|
anims: Res<Assets<BakedFighterAnimations>>,
|
||||||
|
settings: Res<DebugSettings>,
|
||||||
) -> Result {
|
) -> Result {
|
||||||
for (i, (vel, input, name)) in query.iter().enumerate() {
|
let mut fighters = query.iter().collect::<Vec<_>>();
|
||||||
egui::Window::new(format!("Player {}", i)).show(contexts.ctx_mut()?, |ui| {
|
fighters.sort_unstable_by_key(|(player, ..)| player.handle);
|
||||||
ui.label(format!("State: {}", name.0));
|
|
||||||
ui.label(format!("{:?}", vel));
|
for (player, velocity, input, animation_frame, state_name, animations, translation, facing) in fighters {
|
||||||
ui.label(format!("{:?}", input.get_last_frame()));
|
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.0, animation_frame.kind, animation_frame.frame, frame_count
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
ui.label(format!("State: {:?} ({:?})", state_name.0, 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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
FighterDebugRowKind::Position => {
|
||||||
|
ui.label(format!("Position: {:?}", translation.0));
|
||||||
|
},
|
||||||
|
FighterDebugRowKind::Facing => {
|
||||||
|
ui.label(format!("Facing: {:?}", facing));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -110,16 +176,3 @@ pub fn attack_debug(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_config(
|
|
||||||
mut config_store: ResMut<GizmoConfigStore>,
|
|
||||||
keyboard: Res<ButtonInput<KeyCode>>,
|
|
||||||
_real_time: Res<Time<Real>>,
|
|
||||||
_virtual_time: ResMut<Time<Virtual>>,
|
|
||||||
) {
|
|
||||||
if keyboard.just_pressed(KeyCode::KeyT) {
|
|
||||||
for (_, config, _) in config_store.iter_mut() {
|
|
||||||
config.depth_bias = if config.depth_bias == 0. { -1. } else { 0. };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+16
-5
@@ -147,11 +147,22 @@ impl Plugin for FighterPlugin {
|
|||||||
.rollback_component_with_copy::<FighterFacingDirection>()
|
.rollback_component_with_copy::<FighterFacingDirection>()
|
||||||
.rollback_resource_with_reflect::<StageCollision>()
|
.rollback_resource_with_reflect::<StageCollision>()
|
||||||
// Debug views.
|
// Debug views.
|
||||||
.add_systems(FixedPostUpdate, debug::debug_draw_ecb)
|
.add_systems(
|
||||||
.add_systems(EguiPrimaryContextPass, debug::fighter_debug)
|
FixedPostUpdate,
|
||||||
.add_systems(FixedPostUpdate, debug::animation_debug)
|
debug::debug_draw_ecb.run_if(crate::debug_tools::ecb_enabled),
|
||||||
.add_systems(FixedPostUpdate, debug::attack_debug)
|
)
|
||||||
.add_systems(EguiPrimaryContextPass, debug::update_config)
|
.add_systems(
|
||||||
|
EguiPrimaryContextPass,
|
||||||
|
debug::fighter_debug.run_if(crate::debug_tools::fighter_info_enabled),
|
||||||
|
)
|
||||||
|
.add_systems(
|
||||||
|
FixedPostUpdate,
|
||||||
|
debug::animation_debug.run_if(crate::debug_tools::animation_bones_enabled),
|
||||||
|
)
|
||||||
|
.add_systems(
|
||||||
|
FixedPostUpdate,
|
||||||
|
debug::attack_debug.run_if(crate::debug_tools::attack_hitboxes_enabled),
|
||||||
|
)
|
||||||
.rollback_component_with_clone::<FighterState>()
|
.rollback_component_with_clone::<FighterState>()
|
||||||
.rollback_component_with_clone::<FighterHitboxes>()
|
.rollback_component_with_clone::<FighterHitboxes>()
|
||||||
.checksum_component_with_hash::<FighterState>()
|
.checksum_component_with_hash::<FighterState>()
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
//! this module is what turns that into a position.
|
//! this module is what turns that into a position.
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use fixed::types::I16F16;
|
|
||||||
|
|
||||||
use crate::{fighter::{FighterFacingDirection, baked_animation::FixedMat4}, math::{int::FGi32, vec::FGVec2, vec3::FGVec3}};
|
use crate::{fighter::{FighterFacingDirection, baked_animation::FixedMat4}, math::{int::FGi32, vec::FGVec2, vec3::FGVec3}};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
use crate::fighter::{attack::{AttackKind, update_active_hitbox_list}, state::{FighterState, FighterStateImpl, ground::GroundedStateCommon, wait::WaitState}};
|
use crate::fighter::{attack::{AttackKind, update_active_hitbox_list}, state::{FighterState, FighterStateImpl, ground::GroundedStateCommon, wait::WaitState}};
|
||||||
|
|
||||||
use bevy::prelude::*;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Hash)]
|
#[derive(Clone, Debug, Hash)]
|
||||||
pub struct GroundAttackState {
|
pub struct GroundAttackState {
|
||||||
pub attack_kind: AttackKind,
|
pub attack_kind: AttackKind,
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ use crate::fighter::attack::FighterAttackScriptAssets;
|
|||||||
use crate::fighter::baked_animation::{BakedFighterAnimations, FixedMat4};
|
use crate::fighter::baked_animation::{BakedFighterAnimations, FixedMat4};
|
||||||
use crate::fighter::ecb::FighterPreviousECB;
|
use crate::fighter::ecb::FighterPreviousECB;
|
||||||
use crate::fighter::manifest::FighterManifest;
|
use crate::fighter::manifest::FighterManifest;
|
||||||
use crate::fighter::state::ground_attack::GroundAttackState;
|
|
||||||
use crate::fighter::visual::FighterAnimations;
|
use crate::fighter::visual::FighterAnimations;
|
||||||
use crate::fighter::{
|
use crate::fighter::{
|
||||||
Fighter, FighterECB, FighterFacingDirection, FighterHitboxes, FighterPreviousTranslation, FighterTranslation, FighterVelocity,
|
Fighter, FighterECB, FighterFacingDirection, FighterHitboxes, FighterPreviousTranslation, FighterTranslation, FighterVelocity,
|
||||||
|
|||||||
@@ -47,3 +47,12 @@ impl FromWorld for GameSettings {
|
|||||||
.clone()
|
.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Reflect, Resource, Default)]
|
||||||
|
#[reflect(Resource)]
|
||||||
|
pub struct DebugSettings {
|
||||||
|
pub show_attack_hitboxes: bool,
|
||||||
|
pub show_stage_lines: bool,
|
||||||
|
pub show_ecb: bool,
|
||||||
|
pub debug_gizmos_draw_in_front: bool
|
||||||
|
}
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
use bevy_egui::prelude::*;
|
|
||||||
|
|
||||||
use crate::{fighter::{FighterHitboxes, animation::FighterAnimationFrame, visual::FighterAnimations}, input::FighterInput};
|
|
||||||
use bevy::prelude::*;
|
|
||||||
|
|
||||||
pub fn input_debug(mut contexts: EguiContexts, query: Query<&FighterInput>) -> Result {
|
|
||||||
for (i, input) in query.iter().enumerate() {
|
|
||||||
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>());
|
|
||||||
egui::Window::new(format!("Input {}", i)).show(contexts.ctx_mut()?, |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));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
use bevy::platform::collections::HashMap;
|
use bevy::platform::collections::HashMap;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy_egui::EguiPrimaryContextPass;
|
|
||||||
use bevy_ggrs::prelude::*;
|
use bevy_ggrs::prelude::*;
|
||||||
use bevy_ggrs::{LocalInputs, LocalPlayers};
|
use bevy_ggrs::{LocalInputs, LocalPlayers};
|
||||||
|
|
||||||
@@ -14,7 +13,6 @@ pub mod gamecube;
|
|||||||
pub mod gamepad;
|
pub mod gamepad;
|
||||||
pub mod keyboard;
|
pub mod keyboard;
|
||||||
pub mod map;
|
pub mod map;
|
||||||
pub mod debug;
|
|
||||||
|
|
||||||
pub use buffer::{FighterCommands, FighterInput};
|
pub use buffer::{FighterCommands, FighterInput};
|
||||||
pub use frame::FighterInputFrame;
|
pub use frame::FighterInputFrame;
|
||||||
@@ -106,7 +104,6 @@ impl Plugin for FighterInputPlugin {
|
|||||||
})
|
})
|
||||||
.add_plugins(gamecube::GamecubeAdapterPlugin)
|
.add_plugins(gamecube::GamecubeAdapterPlugin)
|
||||||
.add_systems(ReadInputs, read_local_inputs)
|
.add_systems(ReadInputs, read_local_inputs)
|
||||||
.add_systems(EguiPrimaryContextPass, debug::input_debug)
|
|
||||||
.add_systems(
|
.add_systems(
|
||||||
GgrsSchedule,
|
GgrsSchedule,
|
||||||
buffer::postprocess_input.in_set(GameplaySet::Input),
|
buffer::postprocess_input.in_set(GameplaySet::Input),
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ use crate::{
|
|||||||
|
|
||||||
mod args;
|
mod args;
|
||||||
pub mod camera;
|
pub mod camera;
|
||||||
|
pub mod debug_tools;
|
||||||
pub mod fighter;
|
pub mod fighter;
|
||||||
pub mod game_settings;
|
pub mod game_settings;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
@@ -79,6 +80,7 @@ impl Plugin for GamePlugin {
|
|||||||
netcode::FighterNetcodePlugin::default(),
|
netcode::FighterNetcodePlugin::default(),
|
||||||
GameplaySchedulePlugin,
|
GameplaySchedulePlugin,
|
||||||
input::FighterInputPlugin,
|
input::FighterInputPlugin,
|
||||||
|
debug_tools::DebugToolsPlugin,
|
||||||
fighter::FighterPlugin,
|
fighter::FighterPlugin,
|
||||||
fighter::baked_animation::BakedAnimationPlugin,
|
fighter::baked_animation::BakedAnimationPlugin,
|
||||||
scripting::importer::FighterScriptImportPlugin,
|
scripting::importer::FighterScriptImportPlugin,
|
||||||
|
|||||||
+4
-1
@@ -23,7 +23,10 @@ impl Plugin for FighterNetcodePlugin {
|
|||||||
// Gameplay components are registered by the plugin that owns them.
|
// Gameplay components are registered by the plugin that owns them.
|
||||||
.rollback_component_with_clone::<Transform>()
|
.rollback_component_with_clone::<Transform>()
|
||||||
.rollback_component_with_clone::<Name>()
|
.rollback_component_with_clone::<Name>()
|
||||||
.add_systems(EguiPrimaryContextPass, debug::network_debug)
|
.add_systems(
|
||||||
|
EguiPrimaryContextPass,
|
||||||
|
debug::network_debug.run_if(crate::debug_tools::network_enabled),
|
||||||
|
)
|
||||||
.add_systems(Update, session::print_events_system)
|
.add_systems(Update, session::print_events_system)
|
||||||
.insert_resource(RollbackFrameRate(60));
|
.insert_resource(RollbackFrameRate(60));
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -20,7 +20,9 @@ impl Plugin for StagePlugin {
|
|||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.add_systems(
|
app.add_systems(
|
||||||
Update,
|
Update,
|
||||||
debug::debug_draw_scene.run_if(resource_exists::<line::StageCollision>),
|
debug::debug_draw_scene
|
||||||
|
.run_if(resource_exists::<line::StageCollision>)
|
||||||
|
.run_if(crate::debug_tools::stage_collision_enabled),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user