use super::{BackbackSwapEvent, Backpack, BackpackHead}; use crate::heads_ui::HeadsImages; use bevy::prelude::*; static HEAD_SLOTS: usize = 5; #[derive(Event, Clone, Copy, Reflect, PartialEq)] pub enum BackpackAction { Left, Right, Swap, OpenClose, } #[derive(Component, Default)] struct BackpackMarker; #[derive(Component, Default)] struct BackpackCountText; #[derive(Component, Default)] struct HeadSelector(pub usize); #[derive(Component, Default)] struct HeadImage(pub usize); #[derive(Component, Default)] struct HeadDamage(pub usize); #[derive(Resource, Default, Debug)] struct BackpackUiState { heads: [Option; 5], scroll: usize, count: usize, current_slot: usize, open: bool, } impl BackpackUiState { fn relative_current_slot(&self) -> usize { self.current_slot.saturating_sub(self.scroll) } } pub fn plugin(app: &mut App) { app.init_resource::(); app.add_systems(Startup, setup); app.add_systems( Update, (update, sync_on_change, update_visibility, update_count), ); app.add_observer(swap_head_inputs); } 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"); let selector = asset_server.load("ui/selector.png"); commands .spawn(( Name::new("backpack"), BackpackMarker, Visibility::Hidden, Node { position_type: PositionType::Absolute, top: Val::Px(20.0), right: Val::Px(20.0), height: Val::Px(74.0), ..default() }, )) .with_children(|parent| { for i in 0..HEAD_SLOTS { spawn_head_ui( parent, bg.clone(), regular.clone(), selector.clone(), damage.clone(), i, ); } }); commands.spawn(( Text::new("0"), BackpackCountText, TextFont { font: asset_server.load("font.ttf"), font_size: 34.0, ..default() }, TextColor(Color::Srgba(Srgba::rgb(0., 1., 0.))), 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( parent: &mut ChildBuilder, bg: Handle, regular: Handle, selector: Handle, damage: Handle, head_slot: usize, ) { 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(( 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), )); parent.spawn(( Name::new("bg"), Node { position_type: PositionType::Absolute, ..default() }, ImageNode::new(bg), )); parent.spawn(( Name::new("head"), Node { position_type: PositionType::Absolute, ..default() }, ImageNode::default(), Visibility::Hidden, HeadImage(head_slot), )); parent.spawn(( Name::new("rings"), Node { position_type: PositionType::Absolute, ..default() }, ImageNode::new(regular), )); parent .spawn(( Name::new("health"), Node { height: Val::Px(DAMAGE_SIZE), width: Val::Px(DAMAGE_SIZE), ..default() }, )) .with_children(|parent| { parent .spawn(( 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(25.), ..default() }, )) .with_child(ImageNode::new(damage)); }); }); } fn update_visibility( state: Res, mut backpack: Query<&mut Visibility, (With, Without)>, mut count: Query<&mut Visibility, (Without, With)>, ) { if state.is_changed() { for mut vis in backpack.iter_mut() { *vis = if state.open { Visibility::Visible } else { Visibility::Hidden }; } for mut vis in count.iter_mut() { *vis = if !state.open { Visibility::Visible } else { Visibility::Hidden }; } } } fn update_count( state: Res, text: Query>, mut writer: TextUiWriter, ) { if state.is_changed() { let Some(text) = text.iter().next() else { return; }; *writer.text(text, 0) = state.count.to_string(); } } fn update( state: Res, 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>, ) { if state.is_changed() { 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 }; } } } fn swap_head_inputs( trigger: Trigger, backpack: Res, mut commands: Commands, mut state: ResMut, ) { if state.count == 0 { return; } let action = *trigger.event(); if action == BackpackAction::OpenClose { state.open = !state.open; } if !state.open { return; } let mut changed = false; if action == BackpackAction::Left { if state.current_slot > 0 { state.current_slot -= 1; changed = true; } } if action == BackpackAction::Right { if state.current_slot < state.count.saturating_sub(1) { state.current_slot += 1; changed = true; } } if action == BackpackAction::Swap { commands.trigger(BackbackSwapEvent(state.current_slot)); } if changed { sync(&backpack, &mut state); } } fn sync_on_change(backpack: Res, mut state: ResMut) { if backpack.is_changed() { sync(&backpack, &mut state); } } fn sync(backpack: &Res, state: &mut ResMut) { state.count = backpack.heads.len(); state.scroll = state.scroll.min(state.count.saturating_sub(HEAD_SLOTS)); if state.current_slot >= state.scroll + HEAD_SLOTS { state.scroll = state.current_slot.saturating_sub(HEAD_SLOTS - 1); } if state.current_slot < state.scroll { state.scroll = state.current_slot; } for i in 0..HEAD_SLOTS { if let Some(head) = backpack.heads.get(i + state.scroll) { state.heads[i] = Some(*head); } else { state.heads[i] = None; } } }