From 3678d6a53d43bca4f5ea568b710c5c7048793669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Rom=C3=A1n=20N=C3=BA=C3=B1ez?= Date: Sat, 19 Sep 2026 17:13:05 +0200 Subject: [PATCH 1/6] Implement damage UI --- Cargo.lock | 47 ------------------------ Cargo.toml | 2 -- src/fighter/attack/mod.rs | 9 ++++- src/fighter/attack/reaction.rs | 4 ++- src/fighter/state/ground_attack.rs | 1 - src/lib.rs | 7 ++-- src/ui/battle_ui.rs | 51 ++++++++++++++++++++++++++ src/ui/errors.rs | 14 ++++++++ src/ui/fighter_info.rs | 58 ++++++++++++++++++++++++++++++ src/ui/mod.rs | 17 +++++++++ 10 files changed, 154 insertions(+), 56 deletions(-) create mode 100644 src/ui/battle_ui.rs create mode 100644 src/ui/errors.rs create mode 100644 src/ui/fighter_info.rs create mode 100644 src/ui/mod.rs diff --git a/Cargo.lock b/Cargo.lock index d204ff3..00c62e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2096,14 +2096,6 @@ dependencies = [ "thread_local", ] -[[package]] -name = "bevy_wind_waker_shader" -version = "0.6.0" -source = "git+https://github.com/lavaeater/bevy_wind_waker_shader.git#1b017c3ef6a4fbdaff115df9ec5ae4954b2fb6ba" -dependencies = [ - "bevy", -] - [[package]] name = "bevy_window" version = "0.19.0" @@ -2232,32 +2224,6 @@ dependencies = [ "varinteger", ] -[[package]] -name = "bitfields" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca5a55e8cbc3b75d1f002a3b62261ab67954755f7e7866f079b8aa63a409b549" -dependencies = [ - "bitfields-impl", -] - -[[package]] -name = "bitfields-impl" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "797cb6b6e8d5e8c2ecbb5ef52b7c273596d58ce1ab79ef2a6c38fa8dca0817c5" -dependencies = [ - "derive_more", - "getset", - "proc-macro2", - "quote", - "rustversion", - "strum", - "strum_macros", - "syn 2.0.119", - "thiserror 2.0.19", -] - [[package]] name = "bitflags" version = "1.3.2" @@ -3523,17 +3489,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "getset" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cf442baaabe4213ce7d1239afc26c039180b6456da2cededa316ae2c8a77a77" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "ggrs" version = "0.13.0" @@ -6170,9 +6125,7 @@ dependencies = [ "bevy_asset_loader", "bevy_egui", "bevy_ggrs", - "bevy_wind_waker_shader", "bincode", - "bitfields", "clap", "enum_cycling", "enum_cycling_derive", diff --git a/Cargo.toml b/Cargo.toml index 6083800..5dc950d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,9 +22,7 @@ serde_json = "1.0.145" bincode = "1.3.3" gltf = "1.4.1" ron = "0.12.0" -bitfields = "3.0.0" clap = { version = "4.6.6", features = ["derive"] } -bevy_wind_waker_shader = { git = "https://github.com/lavaeater/bevy_wind_waker_shader.git" } fixed = { version = "1.31.0", features = ["serde", "serde-str"] } strum = "0.28.0" strum_macros = "0.28.0" diff --git a/src/fighter/attack/mod.rs b/src/fighter/attack/mod.rs index c9f6ef9..6696767 100644 --- a/src/fighter/attack/mod.rs +++ b/src/fighter/attack/mod.rs @@ -32,9 +32,15 @@ use crate::{ #[derive(Deref, Component, Default, Hash, Debug, Copy, Clone)] pub struct FighterKnockbackVelocity(pub FGVec2); -#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Component, Hash)] +#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Component, Hash, PartialEq)] pub struct FighterDamage(FGi32); +impl std::fmt::Display for FighterDamage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:.2}", self.0) + } +} + #[derive(Debug, Hash, Clone)] pub enum HitReactionKind { Flinch, @@ -48,6 +54,7 @@ pub struct FighterHitReaction { pub hitstop_frames: HitStop, pub hitstun_frames: u32, pub kind: HitReactionKind, + pub damage: FighterDamage, } #[derive(Component, DerefMut, Deref, Debug, Default, Hash, Clone)] diff --git a/src/fighter/attack/reaction.rs b/src/fighter/attack/reaction.rs index f9105f2..8d9e1ad 100644 --- a/src/fighter/attack/reaction.rs +++ b/src/fighter/attack/reaction.rs @@ -49,10 +49,11 @@ pub fn resolve_fighter_hits( }; let attack_script = attack_scripts.get(&hit.attack_script)?; let hitbox = attack_script.hitboxes.get(hit.hitbox_index)?; + dbg!(hitbox.damage.0); let damage_received = stale_move_queue .get_attack_damage_multiplier(hit.attack_kind) * hitbox.damage.0; - + dbg!(damage_received); Some(( hit.attacker, damage_received, @@ -100,6 +101,7 @@ pub fn resolve_fighter_hits( hitstun_frames: knockback.get_hitstun_frames(&game_settings), hitstop_frames: hitstop, kind: HitReactionKind::Flinch, + damage: FighterDamage(attack_damage) }, )); } diff --git a/src/fighter/state/ground_attack.rs b/src/fighter/state/ground_attack.rs index 3005266..3f60990 100644 --- a/src/fighter/state/ground_attack.rs +++ b/src/fighter/state/ground_attack.rs @@ -47,7 +47,6 @@ impl FighterStateImpl for GroundAttackState { fn on_enter(&mut self, state_context: &mut EnterContext) { state_context.play_animation(self.attack_kind.get_animation(), false); let script = state_context.attack_script_assets.scripts[&self.attack_kind].clone(); - state_context.stale_move_queue.push(self.attack_kind); state_context.hitboxes.0 = Some(crate::fighter::FighterActiveAttack { kind: self.attack_kind, script, diff --git a/src/lib.rs b/src/lib.rs index 077b068..93d25cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,13 +28,11 @@ use bevy::{ tasks::block_on, }; use bevy_asset_loader::prelude::*; -use bevy_wind_waker_shader::prelude::*; use clap::Parser; use ron_asset_manager::RonAssetPlugin; use crate::{ - camera::MatchCameraPlugin, fighter::manifest::FighterManifest, game_settings::GameSettings, - schedule::GameplaySchedulePlugin, stage::manifest::StageManifest, + camera::MatchCameraPlugin, fighter::manifest::FighterManifest, game_settings::GameSettings, schedule::GameplaySchedulePlugin, stage::manifest::StageManifest, ui::InGameUIPlugin, }; mod args; @@ -53,6 +51,7 @@ pub mod schedule; pub mod scripting; pub mod stage; pub mod texture_reference; +pub mod ui; #[derive(States, Debug, Clone, Copy, Eq, PartialEq, Hash, Default)] pub enum AppState { @@ -89,7 +88,6 @@ impl Plugin for GamePlugin { app.init_state::() .add_plugins(( SettingsPlugin::new("moe.eirteam.shinespark.debug_settings"), - WindWakerShaderPlugin::default(), // Brings up GGRS (and with it `GgrsSchedule`), so it goes first. netcode::FighterNetcodePlugin::default(), GameplaySchedulePlugin, @@ -104,6 +102,7 @@ impl Plugin for GamePlugin { RonAssetPlugin::::create("fighter.ron"), RonAssetPlugin::::create("stage.ron"), RonAssetPlugin::::create("ron"), + InGameUIPlugin, )) .add_plugins(menus::MenuPlugin) .add_loading_state( diff --git a/src/ui/battle_ui.rs b/src/ui/battle_ui.rs new file mode 100644 index 0000000..34cd379 --- /dev/null +++ b/src/ui/battle_ui.rs @@ -0,0 +1,51 @@ +use bevy::prelude::*; + +use crate::{ + fighter::manifest::{FighterManifest, FighterManifestRegistry}, match_loading::{PendingMatch}, ui::{errors::UIErrors, fighter_info::FighterInfoUI}, +}; + +#[derive(Component, FromTemplate)] +pub struct BattleUI { +} + +pub fn spawn_battle_ui(mut commands: Commands, request: Res, fighter_registry: Res, manifests: Res>) -> Result { + + let mut fighter_uis = vec![]; + + for player in &request.players { + let manifest = fighter_registry.get(player.fighter).ok_or_else(|| UIErrors::FighterManifestHandleNotFound(player.fighter))?; + let manifest = manifests.get(manifest).ok_or_else(|| UIErrors::FighterManifestNotFound(player.fighter))?; + + fighter_uis.push(FighterInfoUI::build(manifest, player)); + } + + let scene = bsn! { + BattleUI { + } + Node { + width: percent(100), + height: percent(100), + flex_direction: FlexDirection::Column, + align_items: AlignItems::Center, + justify_content: JustifyContent::End, + } + Children [ + ( + #FighterInfos + Node { + width: percent(100), + flex_direction: FlexDirection::Row, + align_items: AlignItems::End, + justify_content: JustifyContent::SpaceEvenly, + } + Children [ + {fighter_uis} + ] + ) + ] + }; + + commands.spawn_scene(scene); + + Ok(()) +} diff --git a/src/ui/errors.rs b/src/ui/errors.rs new file mode 100644 index 0000000..09ebbc7 --- /dev/null +++ b/src/ui/errors.rs @@ -0,0 +1,14 @@ +use bevy::{scene::SpawnSceneError}; + +use crate::{fighter::FighterId}; + +#[derive(thiserror::Error, Debug)] +pub enum UIErrors { + #[error("Scene spawn failed: {0:?}")] + SceneSpawnFailed(#[from] SpawnSceneError), + #[error("Fighter manifest handle not found for fighter id {0:?}")] + FighterManifestHandleNotFound(FighterId), + + #[error("Fighter manifest not found for fighter id {0:?}")] + FighterManifestNotFound(FighterId), +} diff --git a/src/ui/fighter_info.rs b/src/ui/fighter_info.rs new file mode 100644 index 0000000..75cfc3d --- /dev/null +++ b/src/ui/fighter_info.rs @@ -0,0 +1,58 @@ +use bevy::prelude::*; + +use crate::{fighter::{attack::FighterDamage, manifest::{FighterManifest}}, match_loading::MatchPlayer, player::Player}; + +#[derive(Component, FromTemplate)] +pub struct FighterInfoUI { + percentage_text: Entity, + _name_text: Entity, + player_handle: usize, + visible_damage: FighterDamage, +} + +impl FighterInfoUI { + pub fn build(_manifest: &FighterManifest, match_player: &MatchPlayer) -> impl Scene { + let fighter_name = "El grande padre"; + bsn! { + FighterInfoUI { + percentage_text: #PercentageText, + _name_text: #NameText, + player_handle: {match_player.handle} + } + Node { + flex_direction: FlexDirection::Column, + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + + } + Children [ + ( + #PercentageText + Text("0.00%") + TextFont { + font_size: px(crate::menus::style::H2_SIZE), + } + ), + ( + #NameText + Text({fighter_name}) + ) + ] + } + } +} + +pub fn update_fighter_info_damage(fighters: Query<(&Player, &FighterDamage)>, ui_elements: Query<&mut FighterInfoUI>, mut commands: Commands) { + for mut ui_element in ui_elements { + let Some((_, damage)) = fighters.iter().find(|(player, _)| { + player.handle == ui_element.player_handle + }) else { + continue; + }; + + if *damage != ui_element.visible_damage { + commands.entity(ui_element.percentage_text).insert(Text(format!("{}%", damage))); + ui_element.visible_damage = damage.clone(); + } + } +} \ No newline at end of file diff --git a/src/ui/mod.rs b/src/ui/mod.rs new file mode 100644 index 0000000..75b319f --- /dev/null +++ b/src/ui/mod.rs @@ -0,0 +1,17 @@ +use bevy::prelude::*; + +use crate::AppState; + +pub mod battle_ui; +pub mod errors; +pub mod fighter_info; + +#[derive(Default)] +pub struct InGameUIPlugin; + +impl Plugin for InGameUIPlugin { + fn build(&self, app: &mut App) { + app.add_systems(OnEnter(AppState::InMatch), battle_ui::spawn_battle_ui) + .add_systems(PostUpdate, fighter_info::update_fighter_info_damage); + } +} -- 2.54.0 From 31f400f7fbca4c03143676fe6f370ffe16a951ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Rom=C3=A1n=20N=C3=BA=C3=B1ez?= Date: Sat, 19 Sep 2026 17:31:07 +0200 Subject: [PATCH 2/6] Implement stale move queue --- src/fighter/attack/reaction.rs | 26 ++++++++++++++++++++++---- src/fighter/mod.rs | 1 + src/fighter/state/ground_attack.rs | 1 + 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/fighter/attack/reaction.rs b/src/fighter/attack/reaction.rs index 8d9e1ad..5c5fb18 100644 --- a/src/fighter/attack/reaction.rs +++ b/src/fighter/attack/reaction.rs @@ -1,3 +1,5 @@ +use std::collections::HashSet; + use bevy::prelude::*; use bevy_ggrs::prelude::GgrsSchedule; @@ -11,9 +13,9 @@ use crate::{ pub fn resolve_fighter_hits( mut pending_hits: ResMut, mut attacker: Query<( - &FighterStaleMoveQueue, + &mut FighterStaleMoveQueue, &FighterFacingDirection, - &FighterHitboxes, + &mut FighterHitboxes, &Player, &mut FighterHitstop, &mut FighterHitstopShake, @@ -26,6 +28,8 @@ pub fn resolve_fighter_hits( let hits = std::mem::take(&mut pending_hits.0); let mut hit_offset = 0; + let mut stalled_attackers = HashSet::::new(); + while hit_offset < hits.len() { let defender = hits[hit_offset].defender; let next_defender = hits[hit_offset..] @@ -42,7 +46,7 @@ pub fn resolve_fighter_hits( let damage_data = hits[hit_offset..next_defender] .iter() .filter_map(|hit| { - let Ok((stale_move_queue, facing_dir, _hitboxes, player, _, _)) = + let Ok((stale_move_queue, facing_dir, _, player, _, _)) = attacker.get(hit.attacker) else { return None; @@ -91,6 +95,8 @@ pub fn resolve_fighter_hits( angle }; + // Attacker hitstop and data + stalled_attackers.insert(attacker_entity); let hitstop = HitStop::calculate(FighterDamage(attack_damage), false); reactions.push(( attacker_entity, @@ -109,7 +115,7 @@ pub fn resolve_fighter_hits( receiver_damage.0 += total_damage_received_this_frame; for (attacker_entity, _, reaction) in &reactions { - let Ok((.., mut hitstop, mut hitstop_shake)) = attacker.get_mut(*attacker_entity) else { + let Ok((.., _, mut hitstop, mut hitstop_shake)) = attacker.get_mut(*attacker_entity) else { continue; }; @@ -136,6 +142,18 @@ pub fn resolve_fighter_hits( hit_offset = next_defender; } + for hit in hits { + let Ok((mut stale_move_queue, _facing_direction, mut hitboxes, ..)) = attacker.get_mut(hit.attacker) else { + continue; + }; + + if let Some(ref mut hitbox) = hitboxes.0 { + if !hitbox.stalled { + stale_move_queue.push(hitbox.kind); + } + } + } + Ok(()) } diff --git a/src/fighter/mod.rs b/src/fighter/mod.rs index 24d1317..9eb70f5 100644 --- a/src/fighter/mod.rs +++ b/src/fighter/mod.rs @@ -259,6 +259,7 @@ pub struct FighterActiveAttack { pub active_hitboxes: Vec, pub solved_hitboxes: Vec, pub hit_fighters: Vec, + pub stalled: bool } #[derive(Component, Clone, Debug, Default, Hash)] diff --git a/src/fighter/state/ground_attack.rs b/src/fighter/state/ground_attack.rs index 3f60990..59bfee2 100644 --- a/src/fighter/state/ground_attack.rs +++ b/src/fighter/state/ground_attack.rs @@ -53,6 +53,7 @@ impl FighterStateImpl for GroundAttackState { active_hitboxes: Vec::new(), solved_hitboxes: Vec::new(), hit_fighters: Vec::new(), + stalled: false }); } -- 2.54.0 From 54acfe8bfedf43082f8c5f38580740380f651a3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Rom=C3=A1n=20N=C3=BA=C3=B1ez?= Date: Sat, 19 Sep 2026 17:31:24 +0200 Subject: [PATCH 3/6] Remove DBG! call --- src/fighter/attack/reaction.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/fighter/attack/reaction.rs b/src/fighter/attack/reaction.rs index 5c5fb18..db888e1 100644 --- a/src/fighter/attack/reaction.rs +++ b/src/fighter/attack/reaction.rs @@ -53,11 +53,9 @@ pub fn resolve_fighter_hits( }; let attack_script = attack_scripts.get(&hit.attack_script)?; let hitbox = attack_script.hitboxes.get(hit.hitbox_index)?; - dbg!(hitbox.damage.0); let damage_received = stale_move_queue .get_attack_damage_multiplier(hit.attack_kind) * hitbox.damage.0; - dbg!(damage_received); Some(( hit.attacker, damage_received, -- 2.54.0 From 6c1e5beeec9ea289849a36242b365d008cadd729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Rom=C3=A1n=20N=C3=BA=C3=B1ez?= Date: Sat, 19 Sep 2026 17:33:07 +0200 Subject: [PATCH 4/6] Cleanup battle UI on exit --- src/ui/battle_ui.rs | 6 ++++++ src/ui/mod.rs | 1 + 2 files changed, 7 insertions(+) diff --git a/src/ui/battle_ui.rs b/src/ui/battle_ui.rs index 34cd379..3013b58 100644 --- a/src/ui/battle_ui.rs +++ b/src/ui/battle_ui.rs @@ -49,3 +49,9 @@ pub fn spawn_battle_ui(mut commands: Commands, request: Res, fight Ok(()) } + +pub fn cleanup_battle_ui(mut commands: Commands, battle_ui: Query>) { + for entity in battle_ui { + commands.entity(entity).despawn(); + } +} \ No newline at end of file diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 75b319f..f393c32 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -12,6 +12,7 @@ pub struct InGameUIPlugin; impl Plugin for InGameUIPlugin { fn build(&self, app: &mut App) { app.add_systems(OnEnter(AppState::InMatch), battle_ui::spawn_battle_ui) + .add_systems(OnExit(AppState::InMatch), battle_ui::cleanup_battle_ui) .add_systems(PostUpdate, fighter_info::update_fighter_info_damage); } } -- 2.54.0 From 4e7549b705aed4d292ce6d8ff4d644137f2182e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Rom=C3=A1n=20N=C3=BA=C3=B1ez?= Date: Sat, 19 Sep 2026 17:35:38 +0200 Subject: [PATCH 5/6] Whoops --- src/fighter/attack/reaction.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/fighter/attack/reaction.rs b/src/fighter/attack/reaction.rs index db888e1..7937c61 100644 --- a/src/fighter/attack/reaction.rs +++ b/src/fighter/attack/reaction.rs @@ -28,8 +28,6 @@ pub fn resolve_fighter_hits( let hits = std::mem::take(&mut pending_hits.0); let mut hit_offset = 0; - let mut stalled_attackers = HashSet::::new(); - while hit_offset < hits.len() { let defender = hits[hit_offset].defender; let next_defender = hits[hit_offset..] @@ -94,7 +92,6 @@ pub fn resolve_fighter_hits( }; // Attacker hitstop and data - stalled_attackers.insert(attacker_entity); let hitstop = HitStop::calculate(FighterDamage(attack_damage), false); reactions.push(( attacker_entity, @@ -147,6 +144,7 @@ pub fn resolve_fighter_hits( if let Some(ref mut hitbox) = hitboxes.0 { if !hitbox.stalled { + hitbox.stalled = true; stale_move_queue.push(hitbox.kind); } } -- 2.54.0 From e425daeff4af2bc07c6279da19c96a2670109a0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Rom=C3=A1n=20N=C3=BA=C3=B1ez?= Date: Sat, 19 Sep 2026 17:37:27 +0200 Subject: [PATCH 6/6] Shut up warnings --- src/fighter/attack/reaction.rs | 2 -- src/ui/fighter_info.rs | 1 - 2 files changed, 3 deletions(-) diff --git a/src/fighter/attack/reaction.rs b/src/fighter/attack/reaction.rs index 7937c61..cc792e0 100644 --- a/src/fighter/attack/reaction.rs +++ b/src/fighter/attack/reaction.rs @@ -1,5 +1,3 @@ -use std::collections::HashSet; - use bevy::prelude::*; use bevy_ggrs::prelude::GgrsSchedule; diff --git a/src/ui/fighter_info.rs b/src/ui/fighter_info.rs index 75cfc3d..f1b8681 100644 --- a/src/ui/fighter_info.rs +++ b/src/ui/fighter_info.rs @@ -23,7 +23,6 @@ impl FighterInfoUI { flex_direction: FlexDirection::Column, align_items: AlignItems::Center, justify_content: JustifyContent::Center, - } Children [ ( -- 2.54.0