backpack implementation (#15)
This commit is contained in:
329
src/backpack/backpack_ui.rs
Normal file
329
src/backpack/backpack_ui.rs
Normal file
@@ -0,0 +1,329 @@
|
||||
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<BackpackHead>; 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::<BackpackUiState>();
|
||||
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<AssetServer>) {
|
||||
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<Image>,
|
||||
regular: Handle<Image>,
|
||||
selector: Handle<Image>,
|
||||
damage: Handle<Image>,
|
||||
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<BackpackUiState>,
|
||||
mut backpack: Query<&mut Visibility, (With<BackpackMarker>, Without<BackpackCountText>)>,
|
||||
mut count: Query<&mut Visibility, (Without<BackpackMarker>, With<BackpackCountText>)>,
|
||||
) {
|
||||
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<BackpackUiState>,
|
||||
text: Query<Entity, With<BackpackCountText>>,
|
||||
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<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>>,
|
||||
) {
|
||||
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((1. - head.health) * 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<BackpackAction>,
|
||||
backpack: Res<Backpack>,
|
||||
mut commands: Commands,
|
||||
mut state: ResMut<BackpackUiState>,
|
||||
) {
|
||||
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<Backpack>, mut state: ResMut<BackpackUiState>) {
|
||||
if backpack.is_changed() {
|
||||
sync(&backpack, &mut state);
|
||||
}
|
||||
}
|
||||
|
||||
fn sync(backpack: &Res<Backpack>, state: &mut ResMut<BackpackUiState>) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user