make backwork in multiplayer

This commit is contained in:
2025-12-21 13:53:57 -05:00
parent 56ca801992
commit 181b617620
11 changed files with 208 additions and 209 deletions

View File

@@ -1,39 +0,0 @@
use super::UiHeadState;
use bevy::prelude::*;
pub static BACKPACK_HEAD_SLOTS: usize = 5;
#[derive(Component, Default)]
pub struct BackpackMarker;
#[derive(Component, Default)]
pub struct BackpackCountText;
#[derive(Component, Default)]
pub struct HeadSelector(pub usize);
#[derive(Component, Default)]
pub struct HeadImage(pub usize);
#[derive(Component, Default)]
pub struct HeadDamage(pub usize);
#[derive(Component, Default, Debug, Reflect)]
#[reflect(Component, Default)]
pub struct BackpackUiState {
pub heads: [Option<UiHeadState>; 5],
pub scroll: usize,
pub count: usize,
pub current_slot: usize,
pub open: bool,
}
impl BackpackUiState {
pub fn relative_current_slot(&self) -> usize {
self.current_slot.saturating_sub(self.scroll)
}
}
pub fn plugin(app: &mut App) {
app.register_type::<BackpackUiState>();
}

View File

@@ -6,13 +6,7 @@ use crate::{
heads_database::HeadsDatabase,
};
use bevy::prelude::*;
#[cfg(feature = "client")]
use bevy_replicon::prelude::ClientTriggerExt;
use serde::{Deserialize, Serialize};
pub use ui_head_state::UiHeadState;
pub mod backpack_ui;
pub mod ui_head_state;
#[derive(Component, Default, Reflect, Serialize, Deserialize, PartialEq)]
#[reflect(Component)]
@@ -40,120 +34,15 @@ impl Backpack {
}
}
#[derive(Event, Serialize, Deserialize)]
#[derive(Event, Debug, Serialize, Deserialize)]
pub struct BackpackSwapEvent(pub usize);
pub fn plugin(app: &mut App) {
app.register_type::<Backpack>();
app.add_plugins(backpack_ui::plugin);
#[cfg(feature = "client")]
app.add_systems(FixedUpdate, (backpack_inputs, sync_on_change));
global_observer!(app, on_head_collect);
}
#[cfg(feature = "client")]
fn backpack_inputs(
backpacks: Single<
(&Backpack, &mut backpack_ui::BackpackUiState),
With<crate::player::LocalPlayer>,
>,
mut backpack_inputs: MessageReader<crate::control::BackpackButtonPress>,
mut commands: Commands,
time: Res<Time>,
) {
use crate::{control::BackpackButtonPress, protocol::PlaySound};
let (backpack, mut state) = backpacks.into_inner();
for input in backpack_inputs.read() {
match input {
BackpackButtonPress::Toggle => {
if state.count == 0 {
return;
}
state.open = !state.open;
commands.trigger(PlaySound::Backpack { open: state.open });
}
BackpackButtonPress::Swap => {
if !state.open {
return;
}
commands.client_trigger(BackpackSwapEvent(state.current_slot));
}
BackpackButtonPress::Left => {
if !state.open {
return;
}
if state.current_slot > 0 {
state.current_slot -= 1;
commands.trigger(PlaySound::Selection);
sync_backpack_ui(backpack, &mut state, time.elapsed_secs());
}
}
BackpackButtonPress::Right => {
if !state.open {
return;
}
if state.current_slot < state.count.saturating_sub(1) {
state.current_slot += 1;
commands.trigger(PlaySound::Selection);
sync_backpack_ui(backpack, &mut state, time.elapsed_secs());
}
}
}
}
}
#[cfg(feature = "client")]
fn sync_on_change(
backpack: Query<Ref<Backpack>>,
mut state: Single<&mut backpack_ui::BackpackUiState>,
time: Res<Time>,
) {
for backpack in backpack.iter() {
if backpack.is_changed() || backpack.reloading() {
sync_backpack_ui(&backpack, &mut state, time.elapsed_secs());
}
}
}
#[cfg(feature = "client")]
fn sync_backpack_ui(backpack: &Backpack, state: &mut backpack_ui::BackpackUiState, time: f32) {
use crate::backpack::backpack_ui::BACKPACK_HEAD_SLOTS;
state.count = backpack.heads.len();
state.scroll = state
.scroll
.min(state.count.saturating_sub(BACKPACK_HEAD_SLOTS));
if state.current_slot >= state.scroll + BACKPACK_HEAD_SLOTS {
state.scroll = state.current_slot.saturating_sub(BACKPACK_HEAD_SLOTS - 1);
}
if state.current_slot < state.scroll {
state.scroll = state.current_slot;
}
for i in 0..BACKPACK_HEAD_SLOTS {
if let Some(head) = backpack.heads.get(i + state.scroll) {
use crate::backpack::ui_head_state::UiHeadState;
state.heads[i] = Some(UiHeadState::new(*head, time));
} else {
state.heads[i] = None;
}
}
}
fn on_head_collect(
trigger: On<HeadCollected>,
mut cmds: Commands,

View File

@@ -1,40 +0,0 @@
use crate::heads::HeadState;
use bevy::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq, Reflect, Default, Serialize, Deserialize)]
pub struct UiHeadState {
pub head: usize,
pub health: f32,
pub ammo: f32,
pub reloading: Option<f32>,
}
impl UiHeadState {
pub fn damage(&self) -> f32 {
1. - self.health
}
pub fn ammo_used(&self) -> f32 {
1. - self.ammo
}
pub fn reloading(&self) -> Option<f32> {
self.reloading
}
pub fn new(value: HeadState, time: f32) -> Self {
let reloading = if value.has_ammo() {
None
} else {
Some((time - value.last_use) / value.reload_duration)
};
Self {
head: value.head,
ammo: value.ammo as f32 / value.ammo_max as f32,
health: value.health as f32 / value.health_max as f32,
reloading,
}
}
}

View File

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

View File

@@ -0,0 +1,98 @@
use crate::{
backpack::{Backpack, BackpackSwapEvent},
client::ui::{BACKPACK_HEAD_SLOTS, BackpackUiState, UiHeadState},
control::BackpackButtonPress,
player::LocalPlayer,
protocol::PlaySound,
};
use bevy::prelude::*;
use bevy_replicon::prelude::ClientTriggerExt;
pub fn plugin(app: &mut App) {
app.add_systems(FixedUpdate, (backpack_inputs, sync_on_change));
}
fn backpack_inputs(
backpack: Single<&Backpack, With<LocalPlayer>>,
mut state: ResMut<BackpackUiState>,
mut backpack_inputs: MessageReader<crate::control::BackpackButtonPress>,
mut commands: Commands,
time: Res<Time>,
) {
for input in backpack_inputs.read() {
match input {
BackpackButtonPress::Toggle => {
if state.count == 0 {
return;
}
state.open = !state.open;
commands.trigger(PlaySound::Backpack { open: state.open });
}
BackpackButtonPress::Swap => {
if !state.open {
return;
}
commands.client_trigger(BackpackSwapEvent(state.current_slot));
}
BackpackButtonPress::Left => {
if !state.open {
return;
}
if state.current_slot > 0 {
state.current_slot -= 1;
commands.trigger(PlaySound::Selection);
sync_backpack_ui(&backpack, &mut state, time.elapsed_secs());
}
}
BackpackButtonPress::Right => {
if !state.open {
return;
}
if state.current_slot < state.count.saturating_sub(1) {
state.current_slot += 1;
commands.trigger(PlaySound::Selection);
sync_backpack_ui(&backpack, &mut state, time.elapsed_secs());
}
}
}
}
}
fn sync_on_change(
backpack: Single<Ref<Backpack>, With<LocalPlayer>>,
mut state: ResMut<BackpackUiState>,
time: Res<Time>,
) {
if backpack.is_changed() || backpack.reloading() {
sync_backpack_ui(&backpack, &mut state, time.elapsed_secs());
}
}
fn sync_backpack_ui(backpack: &Backpack, state: &mut BackpackUiState, time: f32) {
state.count = backpack.heads.len();
state.scroll = state
.scroll
.min(state.count.saturating_sub(BACKPACK_HEAD_SLOTS));
if state.current_slot >= state.scroll + BACKPACK_HEAD_SLOTS {
state.scroll = state.current_slot.saturating_sub(BACKPACK_HEAD_SLOTS - 1);
}
if state.current_slot < state.scroll {
state.scroll = state.current_slot;
}
for i in 0..BACKPACK_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;
}
}
}

View File

@@ -24,6 +24,7 @@ use bevy_trenchbroom::geometry::Brushes;
pub mod aim;
pub mod audio;
mod backpack;
pub mod control;
pub mod debug;
pub mod enemy;
@@ -47,6 +48,7 @@ pub fn plugin(app: &mut App) {
steam::plugin,
ui::plugin,
settings::plugin,
backpack::plugin,
));
app.add_systems(

View File

@@ -1,15 +1,47 @@
use crate::{
GameState, HEDZ_GREEN,
backpack::backpack_ui::{
BACKPACK_HEAD_SLOTS, BackpackCountText, BackpackMarker, BackpackUiState, HeadDamage,
HeadImage, HeadSelector,
},
client::ui::heads_ui::HeadsImages,
client::ui::heads_ui::{HeadsImages, UiHeadState},
loading_assets::UIAssets,
};
use bevy::{ecs::spawn::SpawnIter, prelude::*};
pub static BACKPACK_HEAD_SLOTS: usize = 5;
#[derive(Component, Default)]
pub struct BackpackMarker;
#[derive(Component, Default)]
pub struct BackpackCountText;
#[derive(Component, Default)]
pub struct HeadSelector(pub usize);
#[derive(Component, Default)]
pub struct HeadImage(pub usize);
#[derive(Component, Default)]
pub struct HeadDamage(pub usize);
#[derive(Resource, Default, Debug, Reflect)]
#[reflect(Resource)]
pub struct BackpackUiState {
pub heads: [Option<UiHeadState>; 5],
pub scroll: usize,
pub count: usize,
pub current_slot: usize,
pub open: bool,
}
impl BackpackUiState {
pub fn relative_current_slot(&self) -> usize {
self.current_slot.saturating_sub(self.scroll)
}
}
pub fn plugin(app: &mut App) {
app.register_type::<BackpackUiState>();
app.init_resource::<BackpackUiState>();
app.add_systems(OnEnter(GameState::Playing), setup);
app.add_systems(
FixedUpdate,
@@ -152,10 +184,14 @@ fn spawn_head_ui(
}
fn update_visibility(
state: Single<&BackpackUiState, Changed<BackpackUiState>>,
state: Res<BackpackUiState>,
mut backpack: Single<&mut Visibility, (With<BackpackMarker>, Without<BackpackCountText>)>,
mut count: Single<&mut Visibility, (Without<BackpackMarker>, With<BackpackCountText>)>,
) {
if !state.is_changed() {
return;
}
**backpack = if state.open {
Visibility::Visible
} else {
@@ -170,10 +206,14 @@ fn update_visibility(
}
fn update_count(
state: Single<&BackpackUiState, Changed<BackpackUiState>>,
state: Res<BackpackUiState>,
text: Option<Single<Entity, With<BackpackCountText>>>,
mut writer: TextUiWriter,
) {
if !state.is_changed() {
return;
}
let Some(text) = text else {
return;
};
@@ -182,12 +222,16 @@ fn update_count(
}
fn update(
state: Single<&BackpackUiState, Changed<BackpackUiState>>,
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() {
return;
}
for (HeadImage(head), mut vis, mut image) in head_image.iter_mut() {
if let Some(head) = &state.heads[*head] {
*vis = Visibility::Inherited;

View File

@@ -1,7 +1,6 @@
use crate::{
GameState,
backpack::UiHeadState,
heads::{ActiveHeads, HEAD_COUNT, HEAD_SLOTS},
heads::{ActiveHeads, HEAD_COUNT, HEAD_SLOTS, HeadState},
heads_database::HeadsDatabase,
loading_assets::UIAssets,
player::LocalPlayer,
@@ -34,6 +33,43 @@ struct UiActiveHeads {
selected_slot: usize,
}
#[derive(Clone, Copy, Debug, PartialEq, Reflect, Default, Serialize, Deserialize)]
pub struct UiHeadState {
pub head: usize,
pub health: f32,
pub ammo: f32,
pub reloading: Option<f32>,
}
impl UiHeadState {
pub fn damage(&self) -> f32 {
1. - self.health
}
pub fn ammo_used(&self) -> f32 {
1. - self.ammo
}
pub fn reloading(&self) -> Option<f32> {
self.reloading
}
pub fn new(value: HeadState, time: f32) -> Self {
let reloading = if value.has_ammo() {
None
} else {
Some((time - value.last_use) / value.reload_duration)
};
Self {
head: value.head,
ammo: value.ammo as f32 / value.ammo_max as f32,
health: value.health as f32 / value.health_max as f32,
reloading,
}
}
}
pub fn plugin(app: &mut App) {
app.register_type::<HeadDamage>();
app.register_type::<UiActiveHeads>();

View File

@@ -2,7 +2,8 @@ mod backpack_ui;
mod heads_ui;
mod pause;
pub use heads_ui::HeadsImages;
pub use backpack_ui::{BACKPACK_HEAD_SLOTS, BackpackUiState};
pub use heads_ui::{HeadsImages, UiHeadState};
use bevy::prelude::*;

View File

@@ -292,12 +292,15 @@ fn on_select_active_head(
fn on_swap_backpack(
trigger: On<FromClient<BackpackSwapEvent>>,
clients: ClientToController,
mut commands: Commands,
mut query: Query<(Entity, &mut ActiveHeads, &mut Hitpoints, &mut Backpack), With<Player>>,
) {
let player = clients.get_controller(trigger.client_id);
let backpack_slot = trigger.event().0;
let Ok((player, mut active_heads, mut hp, mut backpack)) = query.single_mut() else {
let Ok((player, mut active_heads, mut hp, mut backpack)) = query.get_mut(player) else {
return;
};

View File

@@ -1,7 +1,7 @@
use crate::{
GameState,
abilities::PlayerTriggerState,
backpack::{Backpack, backpack_ui::BackpackUiState},
backpack::Backpack,
camera::{CameraArmRotation, CameraTarget},
cash::{Cash, CashCollectEvent, CashInventory},
character::{AnimatedCharacter, HedzCharacter},
@@ -32,7 +32,7 @@ pub struct Player;
#[derive(Component, Debug, Reflect)]
#[reflect(Component)]
#[require(LocalInputs, BackpackUiState)]
#[require(LocalInputs)]
pub struct LocalPlayer;
#[derive(Component, Default, Serialize, Deserialize, PartialEq)]
@@ -95,7 +95,6 @@ pub fn spawn(
id,
),
Backpack::default(),
BackpackUiState::default(),
Inputs::default(),
Replicated,
))
@@ -165,7 +164,7 @@ fn on_update_head_mesh(
animated_characters: Query<&AnimatedCharacter>,
mut active_head: Query<&mut ActiveHead>,
) -> Result {
let player_id = player_id.get(trigger.entity)?.clone();
let player_id = *(player_id.get(trigger.entity)?);
let player_body_mesh = children
.get(trigger.entity)?