279 lines
8.2 KiB
Rust
279 lines
8.2 KiB
Rust
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<Handle<Image>>,
|
|
}
|
|
|
|
#[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<UiHeadState>; 5],
|
|
selected_slot: usize,
|
|
}
|
|
|
|
pub fn plugin(app: &mut App) {
|
|
app.register_type::<HeadDamage>();
|
|
app.register_type::<UiActiveHeads>();
|
|
|
|
app.init_resource::<UiActiveHeads>();
|
|
|
|
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<AssetServer>,
|
|
heads: Res<HeadsDatabase>,
|
|
) {
|
|
// 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<UIAssets>) {
|
|
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<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![
|
|
(
|
|
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<UiActiveHeads>,
|
|
heads_images: Res<HeadsImages>,
|
|
mut head_image: Query<(&HeadImage, &mut Visibility, &mut ImageNode), 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) = 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<UiActiveHeads>,
|
|
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<UiActiveHeads>, 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<Ref<ActiveHeads>, With<LocalPlayer>>,
|
|
mut state: ResMut<UiActiveHeads>,
|
|
time: Res<Time>,
|
|
) {
|
|
if active_heads.is_changed() || active_heads.reloading() {
|
|
state.selected_slot = active_heads.slot();
|
|
|
|
for i in 0..HEAD_SLOTS {
|
|
state.heads[i] = active_heads
|
|
.head(i)
|
|
.map(|state| UiHeadState::new(state, time.elapsed_secs()));
|
|
}
|
|
}
|
|
}
|