use crate::{ GameState, animation::AnimationFlags, backpack::{Backpack, BackpackSwapEvent}, control::{SelectLeftPressed, SelectRightPressed}, global_observer, heads_database::HeadsDatabase, hitpoints::Hitpoints, player::Player, protocol::{ClientToController, PlaySound, is_server}, }; use bevy::prelude::*; use bevy_replicon::prelude::FromClient; use serde::{Deserialize, Serialize}; pub mod heads_ui; pub static HEAD_COUNT: usize = 18; pub static HEAD_SLOTS: usize = 5; #[derive(Resource, Default)] pub struct HeadsImages { pub heads: Vec>, } #[derive(Clone, Copy, Debug, PartialEq, Reflect, Serialize, Deserialize)] pub struct HeadState { pub head: usize, pub health: u32, pub health_max: u32, pub ammo: u32, pub ammo_max: u32, pub reload_duration: f32, pub last_use: f32, } impl HeadState { pub fn new(head: usize, heads_db: &HeadsDatabase) -> Self { let ammo = heads_db.head_stats(head).ammo; Self { head, health: 100, health_max: 100, ammo, ammo_max: ammo, reload_duration: 5., last_use: 0., } } pub fn has_ammo(&self) -> bool { self.ammo > 0 } } #[derive(Component, Default, Reflect, Debug, Serialize, Deserialize, PartialEq)] #[reflect(Component)] pub struct ActiveHeads { heads: [Option; 5], current_slot: usize, selected_slot: usize, } impl ActiveHeads { pub fn new(heads: [Option; 5]) -> Self { Self { heads, current_slot: 0, selected_slot: 0, } } pub fn current(&self) -> Option { self.heads[self.current_slot] } pub fn use_ammo(&mut self, time: f32) { let Some(head) = &mut self.heads[self.current_slot] else { error!("cannot use ammo of empty head"); return; }; head.last_use = time; head.ammo = head.ammo.saturating_sub(1); } pub fn medic_heal(&mut self, heal_amount: u32, time: f32) -> Option { let mut healed = false; for (index, head) in self.heads.iter_mut().enumerate() { if index == self.current_slot { continue; } if let Some(head) = head && head.health < head.health_max { head.health = head .health .saturating_add(heal_amount) .clamp(0, head.health_max); healed = true; } } if healed { let Some(head) = &mut self.heads[self.current_slot] else { error!("cannot heal with empty head"); return None; }; head.last_use = time; head.health = head.health.saturating_sub(1); Some(head.health) } else { None } } pub fn head(&self, slot: usize) -> Option { self.heads[slot] } pub fn reloading(&self) -> bool { for head in self.heads { let Some(head) = head else { continue; }; if head.ammo == 0 { return true; } } false } pub fn hp(&self) -> Hitpoints { if let Some(head) = &self.heads[self.current_slot] { Hitpoints::new(head.health_max).with_health(head.health) } else { Hitpoints::new(0) } } pub fn set_hitpoint(&mut self, hp: &Hitpoints) { let Some(head) = &mut self.heads[self.current_slot] else { error!("cannot use ammo of empty head"); return; }; (head.health, head.health_max) = hp.get() } // returns new current head id pub fn loose_current(&mut self) -> Option { self.heads[self.current_slot] = None; self.next_head() } fn next_head(&mut self) -> Option { let start_idx = self.current_slot; for offset in 1..5 { let new_idx = (start_idx + offset) % 5; if let Some(head) = self.heads[new_idx] { self.current_slot = new_idx; return Some(head.head); } } None } pub fn contains(&self, head: usize) -> bool { self.heads.iter().any(|h| h.is_some_and(|h| h.head == head)) } } #[derive(Event)] pub struct HeadChanged(pub usize); pub fn plugin(app: &mut App) { app.add_plugins(heads_ui::plugin); app.register_type::(); app.add_systems(OnEnter(GameState::Playing), setup); app.add_systems( FixedUpdate, ( (reload, sync_hp).run_if(in_state(GameState::Playing)), on_select_active_head, ) .run_if(is_server), ); global_observer!(app, on_swap_backpack); } fn setup(mut commands: Commands, asset_server: Res, heads: Res) { // TODO: load via asset loader let heads = (0usize..HEAD_COUNT) .map(|i| asset_server.load(format!("ui/heads/{}.png", heads.head_key(i)))) .collect(); commands.insert_resource(HeadsImages { heads }); } fn sync_hp(mut query: Query<(&mut ActiveHeads, &Hitpoints)>) { for (mut active_heads, hp) in query.iter_mut() { if active_heads.hp().get() != hp.get() { active_heads.set_hitpoint(hp); } } } fn reload( mut commands: Commands, mut active: Query<&mut ActiveHeads>, time: Res