From 17406b1f00e9ed03827ddaf331b35637c66404d4 Mon Sep 17 00:00:00 2001 From: extrawurst <776816+extrawurst@users.noreply.github.com> Date: Wed, 26 Mar 2025 00:41:57 +0100 Subject: [PATCH] target ui indicating health too (#17) --- src/{aim.rs => aim/mod.rs} | 5 ++ src/aim/target_ui.rs | 160 ++++++++++++++++++++++++++++++++++++ src/backpack/backpack_ui.rs | 2 +- src/backpack/mod.rs | 8 +- src/main.rs | 4 +- src/npc.rs | 48 +++++++++-- 6 files changed, 214 insertions(+), 13 deletions(-) rename src/{aim.rs => aim/mod.rs} (98%) create mode 100644 src/aim/target_ui.rs diff --git a/src/aim.rs b/src/aim/mod.rs similarity index 98% rename from src/aim.rs rename to src/aim/mod.rs index 46e2d39..048f6c6 100644 --- a/src/aim.rs +++ b/src/aim/mod.rs @@ -1,3 +1,5 @@ +mod target_ui; + use crate::{ billboards::Billboard, player::{Player, PlayerRig}, @@ -38,6 +40,9 @@ enum MarkerEvent { pub fn plugin(app: &mut App) { app.init_resource::(); + + app.add_plugins(target_ui::plugin); + app.add_systems(Update, (update, move_marker)); app.add_observer(marker_event); } diff --git a/src/aim/target_ui.rs b/src/aim/target_ui.rs new file mode 100644 index 0000000..943081b --- /dev/null +++ b/src/aim/target_ui.rs @@ -0,0 +1,160 @@ +use super::AimState; +use crate::{ + backpack::BackpackHead, + heads_ui::HeadsImages, + npc::{Hitpoints, NpcHead}, +}; +use bevy::prelude::*; + +#[derive(Component, Reflect, Default)] +#[reflect(Component)] +struct HeadImage; + +#[derive(Component, Reflect, Default)] +#[reflect(Component)] +struct HeadDamage; + +#[derive(Resource, Default, PartialEq)] +struct TargetUi { + head: Option, +} + +pub fn plugin(app: &mut App) { + app.add_systems(Startup, setup); + app.add_systems(Update, (sync, update)); +} + +fn setup(mut commands: Commands, asset_server: Res) { + let bg = asset_server.load("ui/head_bg.png"); + let regular = asset_server.load("ui/head_regular.png"); + let damage = asset_server.load("ui/head_damage.png"); + + commands + .spawn(( + Name::new("target-ui"), + Node { + position_type: PositionType::Absolute, + top: Val::Px(150.0), + left: Val::Px(20.0), + height: Val::Px(74.0), + ..default() + }, + )) + .with_children(|parent| { + spawn_head_ui(parent, bg.clone(), regular.clone(), damage.clone()); + }); + + commands.insert_resource(TargetUi::default()); +} + +fn spawn_head_ui( + parent: &mut ChildBuilder, + bg: Handle, + regular: Handle, + damage: Handle, +) { + const SIZE: f32 = 90.0; + const DAMAGE_SIZE: f32 = 74.0; + + parent + .spawn((Node { + position_type: PositionType::Relative, + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + width: Val::Px(SIZE), + ..default() + },)) + .with_children(|parent| { + parent.spawn(( + Node { + position_type: PositionType::Absolute, + ..default() + }, + ImageNode::new(bg), + )); + parent.spawn(( + Node { + position_type: PositionType::Absolute, + ..default() + }, + ImageNode::default(), + Visibility::Hidden, + HeadImage, + )); + parent.spawn(( + Node { + position_type: PositionType::Absolute, + ..default() + }, + ImageNode::new(regular), + )); + parent + .spawn((Node { + height: Val::Px(DAMAGE_SIZE), + width: Val::Px(DAMAGE_SIZE), + ..default() + },)) + .with_children(|parent| { + parent + .spawn(( + HeadDamage, + Node { + position_type: PositionType::Absolute, + display: Display::Block, + overflow: Overflow::clip(), + top: Val::Px(0.), + left: Val::Px(0.), + right: Val::Px(0.), + height: Val::Percent(25.), + ..default() + }, + )) + .with_child(ImageNode::new(damage)); + }); + }); +} + +fn update( + target: Res, + heads_images: Res, + mut head_image: Query< + (&mut Visibility, &mut ImageNode), + (Without, With), + >, + mut head_damage: Query<&mut Node, (With, Without)>, +) { + if target.is_changed() { + if let Ok((mut vis, mut image)) = head_image.get_single_mut() { + if let Some(head) = target.head { + *vis = Visibility::Visible; + image.image = heads_images.heads[head.head].clone(); + } else { + *vis = Visibility::Hidden; + } + } + + if let Ok(mut node) = head_damage.get_single_mut() { + node.height = Val::Percent(target.head.map(|head| head.damage()).unwrap_or(0.) * 100.); + } + } +} + +fn sync( + mut target: ResMut, + aim: Res, + target_data: Query<(&Hitpoints, &NpcHead)>, +) { + let mut new_state = None; + if let Some(e) = aim.target { + if let Ok((hp, head)) = target_data.get(e) { + new_state = Some(BackpackHead { + head: head.0, + health: hp.health(), + }); + } + } + + if new_state != target.head { + target.head = new_state; + } +} diff --git a/src/backpack/backpack_ui.rs b/src/backpack/backpack_ui.rs index 2fa142f..380303b 100644 --- a/src/backpack/backpack_ui.rs +++ b/src/backpack/backpack_ui.rs @@ -246,7 +246,7 @@ fn update( } for (HeadDamage(head), mut node) in head_damage.iter_mut() { if let Some(head) = &state.heads[*head] { - node.height = Val::Percent((1. - head.health) * 100.0); + node.height = Val::Percent(head.damage() * 100.0); } } diff --git a/src/backpack/mod.rs b/src/backpack/mod.rs index 56085c5..8d43737 100644 --- a/src/backpack/mod.rs +++ b/src/backpack/mod.rs @@ -5,12 +5,18 @@ use bevy::prelude::*; pub use backpack_ui::BackpackAction; -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] pub struct BackpackHead { pub head: usize, pub health: f32, } +impl BackpackHead { + pub fn damage(&self) -> f32 { + 1. - self.health + } +} + #[derive(Resource, Default)] pub struct Backpack { pub heads: Vec, diff --git a/src/main.rs b/src/main.rs index fdfbb06..4d74387 100644 --- a/src/main.rs +++ b/src/main.rs @@ -56,8 +56,8 @@ fn main() { app.add_plugins(DefaultPlugins.set(WindowPlugin { primary_window: Some(Window { - title: "HEDZ reloaded".into(), - // resolution: (1024., 768.).into(), + title: "HEDZ Reloaded".into(), + resolution: (1024., 768.).into(), ..default() }), ..default() diff --git a/src/npc.rs b/src/npc.rs index 82d2218..a67e614 100644 --- a/src/npc.rs +++ b/src/npc.rs @@ -1,4 +1,9 @@ -use crate::{keys::KeySpawn, sounds::PlaySound, tb_entities::EnemySpawn}; +use std::{collections::HashMap, usize}; + +use crate::{ + heads_ui::HEAD_COUNT, keys::KeySpawn, player::head_id_to_str, sounds::PlaySound, + tb_entities::EnemySpawn, +}; use bevy::prelude::*; #[derive(Event, Reflect)] @@ -7,22 +12,47 @@ pub struct Hit { } #[derive(Component, Reflect)] -struct Hp(i32); +pub struct Hitpoints { + max: i32, + current: i32, +} + +impl Hitpoints { + fn new(v: i32) -> Self { + Self { max: v, current: v } + } + + pub fn health(&self) -> f32 { + self.current as f32 / self.max as f32 + } +} + +#[derive(Component)] +pub struct NpcHead(pub usize); pub fn plugin(app: &mut App) { app.add_systems(Update, init); } -fn init(mut commands: Commands, query: Query, Without)>) { - for e in query.iter() { - commands.entity(e).insert(Hp(100)).observe(on_hit); +// TODO: use game state to initialize only once +fn init(mut commands: Commands, query: Query<(Entity, &EnemySpawn), Without>) { + let mut names: HashMap = HashMap::default(); + for i in 0..HEAD_COUNT { + names.insert(head_id_to_str(i).to_string(), i); + } + for (e, spawn) in query.iter() { + let id = names[&spawn.head]; + commands + .entity(e) + .insert((Hitpoints::new(100), NpcHead(id))) + .observe(on_hit); } } fn on_hit( trigger: Trigger, mut commands: Commands, - mut query: Query<(&mut Hp, &Transform, &EnemySpawn)>, + mut query: Query<(&mut Hitpoints, &Transform, &EnemySpawn)>, ) { let Hit { damage } = trigger.event(); @@ -32,11 +62,11 @@ fn on_hit( commands.trigger(PlaySound::Hit); - hp.0 = hp.0.saturating_sub(*damage as i32); + hp.current = hp.current.saturating_sub(*damage as i32); - info!("npc hp changed: {} [{}]", hp.0, trigger.entity()); + info!("npc hp changed: {} [{}]", hp.current, trigger.entity()); - if hp.0 <= 0 { + if hp.current <= 0 { commands.entity(trigger.entity()).despawn_recursive(); if !enemy.key.is_empty() {