353 lines
10 KiB
Rust
353 lines
10 KiB
Rust
#[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<UiHeadState>; 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::<BackpackUiState>();
|
|
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<UIAssets>) {
|
|
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<Image>,
|
|
regular: Handle<Image>,
|
|
selector: Handle<Image>,
|
|
damage: Handle<Image>,
|
|
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<BackpackUiState>>,
|
|
mut backpack: Single<&mut Visibility, (With<BackpackMarker>, Without<BackpackCountText>)>,
|
|
mut count: Single<&mut Visibility, (Without<BackpackMarker>, With<BackpackCountText>)>,
|
|
) {
|
|
**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<BackpackUiState>>,
|
|
text: Option<Single<Entity, With<BackpackCountText>>>,
|
|
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<BackpackUiState>>,
|
|
heads_images: Res<HeadsImages>,
|
|
mut head_image: Query<(&HeadImage, &mut Visibility, &mut ImageNode), Without<HeadSelector>>,
|
|
mut head_damage: Query<(&HeadDamage, &mut Node), Without<HeadSelector>>,
|
|
mut head_selector: Query<(&HeadSelector, &mut Visibility), Without<HeadImage>>,
|
|
) {
|
|
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<ControlState>, Ref<Backpack>)>,
|
|
mut trigger: Single<&mut TriggerSender<PlayBackpackSound>>,
|
|
mut commands: Commands,
|
|
mut state: Single<&mut BackpackUiState>,
|
|
time: Res<Time>,
|
|
) {
|
|
for (controls, backpack) in player.iter() {
|
|
if state.count == 0 {
|
|
return;
|
|
}
|
|
|
|
if controls.backpack_toggle {
|
|
state.open = !state.open;
|
|
trigger.trigger::<ActionsChannel>(PlayBackpackSound { open: state.open });
|
|
}
|
|
|
|
if !state.open {
|
|
return;
|
|
}
|
|
|
|
let mut changed = false;
|
|
if controls.backpack_left && state.current_slot > 0 {
|
|
state.current_slot -= 1;
|
|
changed = true;
|
|
}
|
|
if controls.backpack_right && state.current_slot < state.count.saturating_sub(1) {
|
|
state.current_slot += 1;
|
|
changed = true;
|
|
}
|
|
if controls.backpack_swap {
|
|
commands.trigger(BackbackSwapEvent(state.current_slot));
|
|
}
|
|
|
|
if changed {
|
|
commands.trigger(PlaySound::Selection);
|
|
sync(&backpack, &mut state, time.elapsed_secs());
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "client")]
|
|
fn play_backpack_sound(trigger: Trigger<PlayBackpackSound>, mut commands: Commands) {
|
|
commands.trigger(PlaySound::Backpack {
|
|
open: trigger.event().open,
|
|
});
|
|
}
|
|
|
|
#[cfg(feature = "server")]
|
|
fn sync_on_change(
|
|
backpack: Query<Ref<Backpack>>,
|
|
mut state: Single<&mut BackpackUiState>,
|
|
time: Res<Time>,
|
|
) {
|
|
for backpack in backpack.iter() {
|
|
if backpack.is_changed() || backpack.reloading() {
|
|
sync(&backpack, &mut state, time.elapsed_secs());
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "server")]
|
|
fn sync(backpack: &Backpack, state: &mut Single<&mut BackpackUiState>, time: f32) {
|
|
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(UiHeadState::new(*head, time));
|
|
} else {
|
|
state.heads[i] = None;
|
|
}
|
|
}
|
|
}
|