#[cfg(feature = "server")] use super::Backpack; use super::UiHeadState; use crate::{ GameState, HEDZ_GREEN, loading_assets::UIAssets, protocol::PlayBackpackSound, sounds::PlaySound, }; #[cfg(feature = "server")] use crate::{backpack::BackbackSwapEvent, control::ControlState}; #[cfg(feature = "client")] use crate::{global_observer, heads::HeadsImages}; use bevy::{ecs::spawn::SpawnIter, prelude::*}; #[cfg(feature = "server")] use lightyear::prelude::{ActionsChannel, TriggerSender, input::native::ActionState}; use serde::{Deserialize, Serialize}; static HEAD_SLOTS: usize = 5; #[derive(Component, Default)] struct BackpackMarker; #[derive(Component, Default)] struct BackpackCountText; #[allow(unused)] #[derive(Component, Default)] struct HeadSelector(pub usize); #[allow(unused)] #[derive(Component, Default)] struct HeadImage(pub usize); #[allow(unused)] #[derive(Component, Default)] struct HeadDamage(pub usize); #[derive(Component, Default, Debug, Reflect, Serialize, Deserialize, PartialEq)] #[reflect(Component, Default)] pub struct BackpackUiState { heads: [Option; 5], scroll: usize, count: usize, current_slot: usize, open: bool, } #[cfg(feature = "client")] impl BackpackUiState { fn relative_current_slot(&self) -> usize { self.current_slot.saturating_sub(self.scroll) } } pub fn plugin(app: &mut App) { app.register_type::(); app.add_systems(OnEnter(GameState::Playing), setup); #[cfg(feature = "server")] app.add_systems( FixedUpdate, sync_on_change.run_if(in_state(GameState::Playing)), ); #[cfg(feature = "client")] app.add_systems( FixedUpdate, (update, update_visibility, update_count).run_if(in_state(GameState::Playing)), ); #[cfg(feature = "server")] app.add_systems(FixedUpdate, swap_head_inputs); #[cfg(feature = "client")] global_observer!(app, play_backpack_sound); } fn setup(mut commands: Commands, assets: Res) { commands.spawn(( Name::new("backpack-ui"), BackpackMarker, Visibility::Hidden, Node { position_type: PositionType::Absolute, top: Val::Px(20.0), right: Val::Px(20.0), height: Val::Px(74.0), ..default() }, Children::spawn(SpawnIter((0..HEAD_SLOTS).map({ let bg = assets.head_bg.clone(); let regular = assets.head_regular.clone(); let selector = assets.head_selector.clone(); let damage = assets.head_damage.clone(); move |i| { spawn_head_ui( bg.clone(), regular.clone(), selector.clone(), damage.clone(), i, ) } }))), )); commands.spawn(( Name::new("backpack-head-count-ui"), Text::new("0"), TextShadow::default(), BackpackCountText, TextFont { font: assets.font.clone(), font_size: 34.0, ..default() }, TextColor(HEDZ_GREEN.into()), TextLayout::new_with_justify(JustifyText::Center), Node { position_type: PositionType::Absolute, top: Val::Px(20.0), right: Val::Px(20.0), ..default() }, )); } fn spawn_head_ui( bg: Handle, regular: Handle, selector: Handle, damage: Handle, head_slot: usize, ) -> impl Bundle { const SIZE: f32 = 90.0; const DAMAGE_SIZE: f32 = 74.0; ( Node { position_type: PositionType::Relative, justify_content: JustifyContent::Center, align_items: AlignItems::Center, width: Val::Px(SIZE), ..default() }, children![ ( Name::new("selector"), Node { position_type: PositionType::Absolute, bottom: Val::Px(-30.0), ..default() }, Visibility::Hidden, ImageNode::new(selector).with_flip_y(), HeadSelector(head_slot), ), ( Name::new("bg"), Node { position_type: PositionType::Absolute, ..default() }, ImageNode::new(bg), ), ( Name::new("head"), Node { position_type: PositionType::Absolute, ..default() }, ImageNode::default(), Visibility::Hidden, HeadImage(head_slot), ), ( Name::new("rings"), Node { position_type: PositionType::Absolute, ..default() }, ImageNode::new(regular), ), ( Name::new("health"), Node { height: Val::Px(DAMAGE_SIZE), width: Val::Px(DAMAGE_SIZE), ..default() }, children![( Name::new("damage_ring"), HeadDamage(head_slot), 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(0.), ..default() }, children![ImageNode::new(damage)] )] ) ], ) } #[cfg(feature = "client")] fn update_visibility( state: Single<&BackpackUiState, Changed>, mut backpack: Single<&mut Visibility, (With, Without)>, mut count: Single<&mut Visibility, (Without, With)>, ) { **backpack = if state.open { Visibility::Visible } else { Visibility::Hidden }; **count = if !state.open { Visibility::Visible } else { Visibility::Hidden }; } #[cfg(feature = "client")] fn update_count( state: Single<&BackpackUiState, Changed>, text: Option>>, mut writer: TextUiWriter, ) { let Some(text) = text else { return; }; *writer.text(*text, 0) = state.count.to_string(); } #[cfg(feature = "client")] fn update( state: Single<&BackpackUiState, Changed>, heads_images: Res, mut head_image: Query<(&HeadImage, &mut Visibility, &mut ImageNode), Without>, mut head_damage: Query<(&HeadDamage, &mut Node), Without>, mut head_selector: Query<(&HeadSelector, &mut Visibility), Without>, ) { for (HeadImage(head), mut vis, mut image) in head_image.iter_mut() { if let Some(head) = &state.heads[*head] { *vis = Visibility::Inherited; image.image = heads_images.heads[head.head].clone(); } else { *vis = Visibility::Hidden; } } for (HeadDamage(head), mut node) in head_damage.iter_mut() { if let Some(head) = &state.heads[*head] { node.height = Val::Percent(head.damage() * 100.0); } } for (HeadSelector(head), mut vis) in head_selector.iter_mut() { *vis = if *head == state.relative_current_slot() { Visibility::Inherited } else { Visibility::Hidden }; } } #[cfg(feature = "server")] fn swap_head_inputs( player: Query<(&ActionState, Ref)>, mut trigger: Single<&mut TriggerSender>, mut commands: Commands, mut state: Single<&mut BackpackUiState>, time: Res