move heads_ui in client only module

This commit is contained in:
2025-12-21 13:10:50 -05:00
parent 16cd95ae02
commit 56ca801992
7 changed files with 42 additions and 44 deletions

View File

@@ -1,12 +1,6 @@
use crate::{
GameState,
aim::AimTarget,
backpack::UiHeadState,
heads::{ActiveHeads, HeadsImages},
hitpoints::Hitpoints,
loading_assets::UIAssets,
npc::Npc,
player::LocalPlayer,
GameState, aim::AimTarget, backpack::UiHeadState, client::ui::HeadsImages, heads::ActiveHeads,
hitpoints::Hitpoints, loading_assets::UIAssets, npc::Npc, player::LocalPlayer,
};
use bevy::prelude::*;

View File

@@ -1,7 +0,0 @@
pub mod backpack_ui;
use bevy::prelude::*;
pub fn plugin(app: &mut App) {
app.add_plugins(backpack_ui::plugin);
}

View File

@@ -24,7 +24,6 @@ use bevy_trenchbroom::geometry::Brushes;
pub mod aim;
pub mod audio;
pub mod backpack;
pub mod control;
pub mod debug;
pub mod enemy;
@@ -39,7 +38,6 @@ pub fn plugin(app: &mut App) {
app.add_plugins((
aim::plugin,
audio::plugin,
backpack::plugin,
control::plugin,
debug::plugin,
enemy::plugin,

View File

@@ -4,7 +4,7 @@ use crate::{
BACKPACK_HEAD_SLOTS, BackpackCountText, BackpackMarker, BackpackUiState, HeadDamage,
HeadImage, HeadSelector,
},
heads::HeadsImages,
client::ui::heads_ui::HeadsImages,
loading_assets::UIAssets,
};
use bevy::{ecs::spawn::SpawnIter, prelude::*};

View File

@@ -0,0 +1,278 @@
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()));
}
}
}

View File

@@ -1,7 +1,13 @@
mod backpack_ui;
mod heads_ui;
mod pause;
pub use heads_ui::HeadsImages;
use bevy::prelude::*;
pub fn plugin(app: &mut App) {
app.add_plugins(heads_ui::plugin);
app.add_plugins(backpack_ui::plugin);
app.add_plugins(pause::plugin);
}