diff --git a/Cargo.lock b/Cargo.lock index 1fc7529..e7cb6e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3163,6 +3163,23 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "enum_cycling" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "946bc4ae524a5ca6b8f8d2fa284fc51943d1520410854e4630463217be6b2990" + +[[package]] +name = "enum_cycling_derive" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397cac860ca9b2ced0ff24d662806ad132471f12e371e8f579c1deb29de3a294" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "enumn" version = "0.1.14" @@ -3532,6 +3549,8 @@ dependencies = [ "bincode", "bitfields", "clap", + "enum_cycling", + "enum_cycling_derive", "fixed", "gc-adapter-neo", "gltf", diff --git a/Cargo.toml b/Cargo.toml index 36dec09..7fc7c80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,8 @@ bevy_common_assets = { version = "0.17.0", features = ["ron"] } bevy_asset_loader = "0.27.0" gc-adapter-neo = { version = "1.0.0", features = ["libusb"] } rhai = "1.26.0" +enum_cycling = "0.2.1" +enum_cycling_derive = "0.2.1" # Enable a large amount of optimization in the dev profile for dependencies. [profile.dev.package."*"] diff --git a/src/debug_tools.rs b/src/debug_tools.rs index 1c06633..cf1755c 100644 --- a/src/debug_tools.rs +++ b/src/debug_tools.rs @@ -2,8 +2,14 @@ use std::{collections::HashMap, time::Duration}; -use bevy::{prelude::*, settings::{SaveSettingsDeferred, SettingsGroup, ReflectSettingsGroup}}; +use bevy::{ + prelude::*, + settings::{ReflectSettingsGroup, SaveSettingsDeferred, SettingsGroup}, +}; use bevy_egui::{EguiContexts, EguiPrimaryContextPass, egui}; +use bevy_ggrs::RollbackFrameRate; +use enum_cycling::EnumCycle; +use enum_cycling_derive::EnumCycle; use crate::player::Player; @@ -42,6 +48,31 @@ impl PresentationMotionMode { } } +#[derive(EnumCycle, Reflect)] +pub enum GameSpeedDebug { + Full, + Half, + Quarter, +} + +impl GameSpeedDebug { + pub const fn label(&self) -> &'static str { + match self { + GameSpeedDebug::Full => "1.0x", + GameSpeedDebug::Half => "0.5x", + GameSpeedDebug::Quarter => "0.25x", + } + } + + pub fn frame_rate(&self) -> usize { + match self { + GameSpeedDebug::Full => 60, + GameSpeedDebug::Half => 30, + GameSpeedDebug::Quarter => 15, + } + } +} + impl FighterDebugRowKind { const ALL: [Self; 6] = [ Self::None, @@ -100,6 +131,9 @@ enum DebugMenuEntry { MotionSampling { label: &'static str, }, + GameSpeed { + label: &'static str, + }, } const FIGHTER_MENU: &[DebugMenuEntry] = &[ @@ -124,10 +158,15 @@ const FIGHTER_MENU: &[DebugMenuEntry] = &[ }, ]; -const WORLD_MENU: &[DebugMenuEntry] = &[DebugMenuEntry::Toggle { - label: "Stage collision", - flag: DebugFlag::StageCollision, -}]; +const WORLD_MENU: &[DebugMenuEntry] = &[ + DebugMenuEntry::Toggle { + label: "Stage collision", + flag: DebugFlag::StageCollision, + }, + DebugMenuEntry::GameSpeed { + label: "Game speed", + }, +]; const DIAGNOSTICS_MENU: &[DebugMenuEntry] = &[DebugMenuEntry::Toggle { label: "Network stats", @@ -169,6 +208,7 @@ enum DebugMenuPage { PlayerList, PlayerRows(usize), } + #[derive(Resource, SettingsGroup, Reflect)] #[reflect(Resource, SettingsGroup, Default)] pub struct DebugSettings { @@ -190,6 +230,7 @@ pub struct DebugSettings { pub camera: bool, pub draw_through_geometry: bool, pub motion_sampling: PresentationMotionMode, + pub game_speed: GameSpeedDebug, } impl Default for DebugSettings { @@ -208,6 +249,7 @@ impl Default for DebugSettings { camera: false, draw_through_geometry: false, motion_sampling: PresentationMotionMode::default(), + game_speed: GameSpeedDebug::Full, } } } @@ -290,6 +332,10 @@ impl DebugSettings { self.motion_sampling.cycle(); } + fn cycle_game_speed(&mut self) { + self.motion_sampling.cycle(); + } + fn rows_mut(&mut self, handle: usize) -> &mut [FighterDebugRowKind; PLAYER_DEBUG_ROW_COUNT] { self.player_rows .entry(handle) @@ -377,6 +423,50 @@ fn handle_palette_input( let selected = settings.selected_mut(); *selected = (*selected + 1) % page_len; } + + 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 { + match page { + DebugMenuPage::Static(entries) => { + if let Some(entry) = entries.get(settings.selected()) { + match entry { + DebugMenuEntry::MotionSampling { .. } => settings.cycle_motion_sampling(), + DebugMenuEntry::GameSpeed { .. } => { + if direction > 0 { + settings.game_speed = settings.game_speed.up(); + } else { + settings.game_speed = settings.game_speed.down(); + } + + commands.insert_resource(RollbackFrameRate( + settings.game_speed.frame_rate(), + )); + } + _ => {} + } + } + } + DebugMenuPage::PlayerList => todo!(), + DebugMenuPage::PlayerRows(_) => { + 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); + + commands.queue(SaveSettingsDeferred(Duration::from_secs_f32(0.1))); + } + } + } + if matches!(page, DebugMenuPage::PlayerRows(_)) { let direction = if gamepad.just_pressed(GamepadButton::DPadLeft) { Some(-1) @@ -385,26 +475,7 @@ fn handle_palette_input( } 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); - - commands.queue(SaveSettingsDeferred(Duration::from_secs_f32(0.1))); - } - } - if let DebugMenuPage::Static(entries) = page - && matches!( - entries.get(settings.selected()), - Some(DebugMenuEntry::MotionSampling { .. }) - ) - && (gamepad.just_pressed(GamepadButton::DPadLeft) - || gamepad.just_pressed(GamepadButton::DPadRight)) - { - settings.cycle_motion_sampling(); + if let Some(direction) = direction {} } if gamepad.just_pressed(GamepadButton::South) { match page { @@ -412,12 +483,13 @@ fn handle_palette_input( DebugMenuEntry::Toggle { flag, .. } => { settings.toggle_flag(flag); commands.queue(SaveSettingsDeferred(Duration::from_secs_f32(0.1))); - }, + } DebugMenuEntry::Submenu { entries, .. } => { settings.push_page(DebugMenuPage::Static(entries)); } DebugMenuEntry::PlayerSlots { .. } => settings.push_page(DebugMenuPage::PlayerList), DebugMenuEntry::MotionSampling { .. } => settings.cycle_motion_sampling(), + DebugMenuEntry::GameSpeed { .. } => {} }, DebugMenuPage::PlayerList if page_len != 0 => { let handle = player_handles[settings.selected()]; @@ -483,6 +555,12 @@ fn draw_palette( settings.motion_sampling.label() )); } + DebugMenuEntry::GameSpeed { label } => { + ui.label(format!( + "{marker} {label}: {}", + settings.game_speed.label() + )); + } } } } diff --git a/src/fighter/attack.rs b/src/fighter/attack.rs index de2fce5..26d567c 100644 --- a/src/fighter/attack.rs +++ b/src/fighter/attack.rs @@ -104,7 +104,7 @@ impl AttackHitbox { pub fn calculate_knockback( &self, attack: AttackKind, - + receiver_attribs: &FighterAttributes, receiver_damage: FighterDamage, total_damage_received_this_frame: FighterDamage, @@ -125,10 +125,10 @@ impl AttackHitbox { /// Knockback growth is authored in percent (110 means 1.1x). const KB_GROWTH_SCALE: FGi32 = FGi32::lit("0.01"); const DAMAGE_TERM_DIVISOR: FGi32 = FGi32::lit("20.0"); - const DAMAGE_BONUS: FGi32 = FGi32::lit("2.0"); // effective damage floor in the damage term + const DAMAGE_BONUS: FGi32 = FGi32::lit("2.0"); // effective damage floor in the damage term const CHARGE_SMASH_INTERRUPTION_MODIFIER: FGi32 = FGi32::lit("1.2"); const CROUCH_CANCEL_MODIFIER: FGi32 = FGi32::lit("0.666667"); - + let attack_damage_unstalled = self.damage.0; let modifier = { @@ -149,7 +149,7 @@ impl AttackHitbox { let scaled = damage_term * (FGi32::lit("2.0") * BASELINE_WEIGHT) / (receiver_attribs.weight + BASELINE_WEIGHT); let kb = (scaled * KB_GAIN + KB_OFFSET) * s + self.knockback.0; - + Knockback(modifier * kb) } } diff --git a/src/fighter/attributes.rs b/src/fighter/attributes.rs index a1062b3..6d011b5 100644 --- a/src/fighter/attributes.rs +++ b/src/fighter/attributes.rs @@ -64,4 +64,4 @@ impl FighterAttributes { (accel, target_vel) } -} \ No newline at end of file +} diff --git a/src/fighter/debug.rs b/src/fighter/debug.rs index 48f4f5c..1e0e488 100644 --- a/src/fighter/debug.rs +++ b/src/fighter/debug.rs @@ -1,5 +1,7 @@ //! Fighter debug views: gizmos and the egui inspector window. +use std::ops::Mul; + use bevy::color::palettes::css::ORANGE; use bevy::gizmos::primitives::dim3::Capsule3dBuilder; use bevy::{color::palettes::css::YELLOW, prelude::*}; @@ -9,10 +11,11 @@ use crate::debug_tools::{DebugSettings, FighterDebugRowKind}; use crate::fighter::animation::FighterAnimationFrame; use crate::fighter::baked_animation::{BakedFighterAnimations, FighterBoneMatrices}; use crate::fighter::ecb::FighterPreviousECB; +use crate::fighter::manifest::FighterManifest; use crate::fighter::state::StateNameDebug; use crate::fighter::visual::FighterAnimations; use crate::fighter::{ - FighterECB, FighterFacingDirection, FighterHitboxes, FighterPreviousTranslation, + Fighter, FighterECB, FighterFacingDirection, FighterHitboxes, FighterPreviousTranslation, FighterTranslation, FighterVelocity, }; use crate::input::FighterInput; @@ -157,6 +160,52 @@ pub fn animation_debug(query: Query<(&GlobalTransform, &FighterBoneMatrices)>, m } } +pub fn hurtbox_debug( + query: Query<( + &FighterBoneMatrices, + &FighterTranslation, + &FighterFacingDirection, + &Fighter, + )>, + manifests: Res>, + 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, + ); + } + } + } +} + pub fn attack_debug( query: Query<( &FighterHitboxes, @@ -164,12 +213,11 @@ pub fn attack_debug( &FighterTranslation, &FighterFacingDirection, &FighterAnimationFrame, - &GlobalTransform )>, attack_scripts: Res>, mut gizmos: Gizmos, ) { - for (hitboxes, matrices, translation, facing_direction, frame, global_trf) in query { + for (hitboxes, matrices, translation, facing_direction, frame) in query { if let Some(script) = &hitboxes.attack_script { let script = attack_scripts .get(script) @@ -194,7 +242,11 @@ pub fn attack_debug( ); if script.hitboxes[*i].start_frame == frame.frame { - gizmos.sphere(curr_pos_draw, radius.to_num(), bevy::color::palettes::css::RED); + gizmos.sphere( + curr_pos_draw, + radius.to_num(), + bevy::color::palettes::css::RED, + ); continue; } @@ -208,7 +260,6 @@ pub fn attack_debug( prev_pos.z.to_num(), ); - let dist = Dir3::new_and_length(curr_pos_draw - prev_pos_draw); if let Ok((aim_dir, dist)) = dist { @@ -231,9 +282,19 @@ pub fn attack_debug( ); } - gizmos.cross(curr_trf.transform_point(FGVec3::lit("0.0", "0.0", "0.0")).to_vec3(), 1.0, bevy::color::palettes::css::BLUE); - gizmos.cross(curr_trf.to_mat4().transform_point(Vec3::ZERO), 1.0, bevy::color::palettes::css::HOT_PINK); - + gizmos.cross( + curr_trf + .transform_point(FGVec3::lit("0.0", "0.0", "0.0")) + .to_vec3(), + 1.0, + bevy::color::palettes::css::BLUE, + ); + gizmos.cross( + curr_trf.to_mat4().transform_point(Vec3::ZERO), + 1.0, + bevy::color::palettes::css::HOT_PINK, + ); + gizmos.axes(curr_trf.to_mat4(), 1.0); } else { warn!("Bone not found required by attack script: {}", bone); diff --git a/src/fighter/manifest.rs b/src/fighter/manifest.rs index 36baa1d..3068d52 100644 --- a/src/fighter/manifest.rs +++ b/src/fighter/manifest.rs @@ -7,7 +7,7 @@ use strum::IntoEnumIterator; use strum_macros::EnumIter; use crate::{ - AppState, fighter::{FighterAttributes, FighterCameraProfile, animation::AnimKind, attack::AttackKind}, game_settings::GameSettings, + 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)] @@ -15,6 +15,17 @@ pub enum FighterId { TestFighter, } +#[derive(Serialize, Deserialize, Clone, Asset, Reflect, Default)] +pub struct FighterHurtbox { + pub bone: String, + pub offset: FGVec3, + pub rotation: FGVec3, + #[reflect(ignore)] + pub half_length: FGi32, + #[reflect(ignore)] + pub radius: FGi32 +} + #[derive(Serialize, Deserialize, Clone, Asset, Reflect)] pub struct FighterManifest { pub id: FighterId, @@ -24,6 +35,7 @@ pub struct FighterManifest { pub camera: FighterCameraProfile, pub animations: HashMap, pub attack_scripts: HashMap, + pub hurtboxes: Vec, } #[derive(Resource)] diff --git a/src/fighter/mod.rs b/src/fighter/mod.rs index 0ac81b1..229c82b 100644 --- a/src/fighter/mod.rs +++ b/src/fighter/mod.rs @@ -169,6 +169,10 @@ impl Plugin for FighterPlugin { FixedPostUpdate, debug::attack_debug.run_if(crate::debug_tools::attack_hitboxes_enabled), ) + .add_systems( + FixedPostUpdate, + debug::hurtbox_debug + ) .rollback_component_with_clone::() .rollback_component_with_clone::() .checksum_component_with_hash::() diff --git a/src/math/vec3.rs b/src/math/vec3.rs index 004a280..df64bbb 100644 --- a/src/math/vec3.rs +++ b/src/math/vec3.rs @@ -33,3 +33,9 @@ impl FGVec3 { Vec3::new(self.x.to_num(), self.y.to_num(), self.z.to_num()) } } + +impl From<(&str, &str, &str)> for FGVec3 { + fn from((x, y, z): (&str, &str, &str)) -> Self { + Self::lit(x, y, z) + } +} \ No newline at end of file