use crate::{ GameState, backpack::UiHeadState, heads::{ActiveHeads, HEAD_COUNT, HEAD_SLOTS}, heads_database::HeadsDatabase, loading_assets::UIAssets, player::LocalPlayer, }; use bevy::{ecs::spawn::SpawnIter, prelude::*}; use serde::{Deserialize, Serialize}; use std::f32::consts::PI; #[derive(Resource, Default)] pub struct HeadsImages { pub heads: Vec>, } #[derive(Component, Reflect, Default)] #[reflect(Component)] struct HeadSelector(pub usize); #[derive(Component, Reflect, Default)] #[reflect(Component)] struct HeadImage(pub usize); #[derive(Component, Reflect, Default)] #[reflect(Component)] struct HeadDamage(pub usize); #[derive(Resource, Default, Reflect, Serialize, Deserialize, PartialEq)] #[reflect(Resource)] struct UiActiveHeads { heads: [Option; 5], selected_slot: usize, } pub fn plugin(app: &mut App) { app.register_type::(); app.register_type::(); app.init_resource::(); app.add_systems(OnEnter(GameState::Playing), (setup, setup_heads_images)); app.add_systems(FixedUpdate, sync.run_if(in_state(GameState::Playing))); app.add_systems( FixedUpdate, (update, update_ammo, update_health).run_if(in_state(GameState::Playing)), ); } fn setup_heads_images( 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 setup(mut commands: Commands, assets: Res) { commands.spawn(( Name::new("heads-ui"), Node { position_type: PositionType::Absolute, bottom: 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, ) } }))), )); } 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![ ( Node { position_type: PositionType::Absolute, top: Val::Px(-30.0), ..default() }, Visibility::Hidden, ImageNode::new(selector), HeadSelector(head_slot), ), ( Node { position_type: PositionType::Absolute, ..default() }, ImageNode::new(bg), ), ( Name::new("head-icon"), Node { position_type: PositionType::Absolute, ..default() }, BorderRadius::all(Val::Px(9999.)), ImageNode::default(), Visibility::Hidden, HeadImage(head_slot), children![( Node { width: Val::Percent(100.0), height: Val::Percent(100.0), ..default() }, BorderRadius::all(Val::Px(9999.)), HeadImage(0), ImageNode { color: Color::linear_rgba(0.0, 0.0, 0.0, 0.0), ..default() }, BackgroundGradient::from(ConicGradient { start: 0., stops: vec![ AngularColorStop::new(Color::linear_rgba(0., 0., 0., 0.9), 0.), AngularColorStop::new(Color::linear_rgba(0., 0., 0., 0.9), PI * 1.5), AngularColorStop::new(Color::linear_rgba(0., 0., 0., 0.0), PI * 1.5), ], position: UiPosition::CENTER, color_space: InterpolationColorSpace::Srgba, }), )] ), ( Node { position_type: PositionType::Absolute, ..default() }, ImageNode::new(regular), ), ( Node { height: Val::Px(DAMAGE_SIZE), width: Val::Px(DAMAGE_SIZE), ..default() }, children![( 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(25.), ..default() }, children![ImageNode::new(damage)] )] ) ], ) } #[cfg(feature = "client")] fn update( res: Res, heads_images: Res, mut head_image: Query<(&HeadImage, &mut Visibility, &mut ImageNode), 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) = res.heads[*head] { *vis = Visibility::Visible; image.image = heads_images.heads[head.head].clone(); } else { *vis = Visibility::Hidden; } } for (HeadSelector(head), mut vis) in head_selector.iter_mut() { *vis = if *head == res.selected_slot { Visibility::Visible } else { Visibility::Hidden }; } } #[cfg(feature = "client")] fn update_ammo( res: Res, heads: Query<&HeadImage>, mut gradients: Query<(&mut BackgroundGradient, &ChildOf)>, ) { if !res.is_changed() { return; } for (mut gradient, child_of) in gradients.iter_mut() { let Ok(HeadImage(head)) = heads.get(child_of.parent()) else { continue; }; if let Some(head) = res.heads[*head] { let Gradient::Conic(gradient) = &mut gradient.0[0] else { continue; }; let progress = if let Some(reloading) = head.reloading() { 1. - reloading } else { head.ammo_used() }; let angle = progress * PI * 2.0; gradient.stops[1].angle = Some(angle); gradient.stops[2].angle = Some(angle); } } } #[cfg(feature = "client")] fn update_health(res: Res, mut query: Query<(&mut Node, &HeadDamage)>) { if res.is_changed() { for (mut node, HeadDamage(head)) in query.iter_mut() { node.height = Val::Percent(res.heads[*head].map(|head| head.damage()).unwrap_or(0.) * 100.); } } } fn sync( active_heads: Single, With>, mut state: ResMut, time: Res