From fb4c6f501caaa1fe315446468d06392b61ff8d29 Mon Sep 17 00:00:00 2001 From: PROMETHIA-27 <42193387+PROMETHIA-27@users.noreply.github.com> Date: Sat, 27 Sep 2025 16:04:19 -0400 Subject: [PATCH] Sync reloading and cash (#66) --- crates/server/src/main.rs | 1 + crates/server/src/player.rs | 94 +++++++++++++ crates/server/src/server.rs | 2 +- crates/shared/src/backpack/backpack_ui.rs | 147 +++++++++++--------- crates/shared/src/backpack/mod.rs | 4 +- crates/shared/src/backpack/ui_head_state.rs | 5 +- crates/shared/src/cash.rs | 24 ++-- crates/shared/src/cash_heal.rs | 2 +- crates/shared/src/heads/heads_ui.rs | 108 +++++++------- crates/shared/src/heads/mod.rs | 12 +- crates/shared/src/player.rs | 92 +----------- crates/shared/src/protocol.rs | 19 ++- 12 files changed, 285 insertions(+), 225 deletions(-) create mode 100644 crates/server/src/player.rs diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs index 1038f8f..ee42bde 100644 --- a/crates/server/src/main.rs +++ b/crates/server/src/main.rs @@ -15,6 +15,7 @@ use shared::{DebugVisuals, GameState, heads_database::HeadDatabaseAsset}; use std::time::Duration; mod config; +mod player; mod server; mod tb_entities; diff --git a/crates/server/src/player.rs b/crates/server/src/player.rs new file mode 100644 index 0000000..1490ad3 --- /dev/null +++ b/crates/server/src/player.rs @@ -0,0 +1,94 @@ +use bevy::prelude::*; +use lightyear::prelude::{input::native::ActionState, *}; +use shared::{ + backpack::{Backpack, backpack_ui::BackpackUiState}, + camera::{CameraArmRotation, CameraTarget}, + cash::CashResource, + character::AnimatedCharacter, + control::{ControlState, controller_common::PlayerCharacterController}, + head::ActiveHead, + head_drop::HeadDrops, + heads::{ActiveHeads, HeadChanged, HeadState, heads_ui::UiActiveHeads}, + heads_database::HeadsDatabase, + hitpoints::{Hitpoints, Kill}, + npc::SpawnCharacter, + player::{Player, PlayerBodyMesh}, + tb_entities::SpawnPoint, +}; + +pub fn spawn( + mut commands: Commands, + owner: Entity, + query: Query<&Transform, With>, + asset_server: Res, + heads_db: Res, +) { + let Some(spawn) = query.iter().next() else { + return; + }; + + let transform = Transform::from_translation(spawn.translation + Vec3::new(0., 3., 0.)); + + let mut player = commands.spawn(( + ( + Name::from("player"), + Player, + ActiveHead(0), + ActiveHeads::new([ + Some(HeadState::new(0, heads_db.as_ref())), + Some(HeadState::new(3, heads_db.as_ref())), + Some(HeadState::new(6, heads_db.as_ref())), + Some(HeadState::new(10, heads_db.as_ref())), + Some(HeadState::new(9, heads_db.as_ref())), + ]), + Hitpoints::new(100), + CashResource::default(), + CameraTarget, + transform, + Visibility::default(), + PlayerCharacterController, + ), + ActionState::::default(), + Backpack::default(), + BackpackUiState::default(), + UiActiveHeads::default(), + Replicate::to_clients(NetworkTarget::All), + PredictionTarget::to_clients(NetworkTarget::All), + ControlledBy { + owner, + lifetime: Lifetime::SessionBased, + }, + children![( + Name::new("player-rig"), + PlayerBodyMesh, + CameraArmRotation, + children![AnimatedCharacter::new(0)], + )], + )); + player.observe(on_kill); + + commands.spawn(( + AudioPlayer::new(asset_server.load("sfx/heads/angry demonstrator.ogg")), + PlaybackSettings::DESPAWN, + )); + + commands.trigger(SpawnCharacter(transform.translation)); +} + +fn on_kill( + trigger: Trigger, + mut commands: Commands, + mut query: Query<(&Transform, &ActiveHead, &mut ActiveHeads, &mut Hitpoints)>, +) { + let Ok((transform, active, mut heads, mut hp)) = query.get_mut(trigger.target()) else { + return; + }; + + commands.trigger(HeadDrops::new(transform.translation, active.0)); + + if let Some(new_head) = heads.loose_current() { + hp.set_health(heads.current().unwrap().health); + + commands.trigger(HeadChanged(new_head)); + } +} diff --git a/crates/server/src/server.rs b/crates/server/src/server.rs index b692ad2..a139d1e 100644 --- a/crates/server/src/server.rs +++ b/crates/server/src/server.rs @@ -38,7 +38,7 @@ fn handle_new_client( .entity(trigger.target()) .insert(ReplicationSender::default()); - shared::player::spawn(commands, trigger.target(), query, asset_server, heads_db); + crate::player::spawn(commands, trigger.target(), query, asset_server, heads_db); Ok(()) } diff --git a/crates/shared/src/backpack/backpack_ui.rs b/crates/shared/src/backpack/backpack_ui.rs index 8f6b4c1..f32a7c2 100644 --- a/crates/shared/src/backpack/backpack_ui.rs +++ b/crates/shared/src/backpack/backpack_ui.rs @@ -1,10 +1,17 @@ -use super::{Backpack, UiHeadState}; +#[cfg(feature = "server")] +use super::Backpack; +use super::UiHeadState; use crate::{ - GameState, HEDZ_GREEN, backpack::BackbackSwapEvent, control::ControlState, heads::HeadsImages, - loading_assets::UIAssets, sounds::PlaySound, + 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::*}; -use lightyear::prelude::input::native::ActionState; +#[cfg(feature = "server")] +use lightyear::prelude::{ActionsChannel, TriggerSender, input::native::ActionState}; +use serde::{Deserialize, Serialize}; static HEAD_SLOTS: usize = 5; @@ -14,18 +21,21 @@ 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(Resource, Default, Debug, Reflect)] -#[reflect(Resource, Default)] -struct BackpackUiState { +#[derive(Component, Default, Debug, Reflect, Serialize, Deserialize, PartialEq)] +#[reflect(Component, Default)] +pub struct BackpackUiState { heads: [Option; 5], scroll: usize, count: usize, @@ -33,6 +43,7 @@ struct BackpackUiState { open: bool, } +#[cfg(feature = "client")] impl BackpackUiState { fn relative_current_slot(&self) -> usize { self.current_slot.saturating_sub(self.scroll) @@ -41,14 +52,22 @@ impl BackpackUiState { pub fn plugin(app: &mut App) { app.register_type::(); - app.init_resource::(); app.add_systems(OnEnter(GameState::Playing), setup); + #[cfg(feature = "server")] app.add_systems( - Update, - (update, sync_on_change, update_visibility, update_count) - .run_if(in_state(GameState::Playing)), + 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) { @@ -185,80 +204,75 @@ fn spawn_head_ui( ) } +#[cfg(feature = "client")] fn update_visibility( - state: Res, - mut backpack: Query<&mut Visibility, (With, Without)>, - mut count: Query<&mut Visibility, (Without, With)>, + state: Single<&BackpackUiState, Changed>, + mut backpack: Single<&mut Visibility, (With, Without)>, + mut count: Single<&mut Visibility, (Without, With)>, ) { - if state.is_changed() { - for mut vis in backpack.iter_mut() { - *vis = if state.open { - Visibility::Visible - } else { - Visibility::Hidden - }; - } + **backpack = if state.open { + Visibility::Visible + } else { + Visibility::Hidden + }; - for mut vis in count.iter_mut() { - *vis = if !state.open { - Visibility::Visible - } else { - Visibility::Hidden - }; - } - } + **count = if !state.open { + Visibility::Visible + } else { + Visibility::Hidden + }; } +#[cfg(feature = "client")] fn update_count( - state: Res, - text: Query>, + state: Single<&BackpackUiState, Changed>, + text: Option>>, mut writer: TextUiWriter, ) { - if state.is_changed() { - let Some(text) = text.iter().next() else { - return; - }; + let Some(text) = text else { + return; + }; - *writer.text(text, 0) = state.count.to_string(); - } + *writer.text(*text, 0) = state.count.to_string(); } +#[cfg(feature = "client")] fn update( - state: Res, + state: Single<&BackpackUiState, Changed>, heads_images: Res, mut head_image: Query<(&HeadImage, &mut Visibility, &mut ImageNode), Without>, mut head_damage: Query<(&HeadDamage, &mut Node), Without>, mut head_selector: Query<(&HeadSelector, &mut Visibility), Without>, ) { - 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 (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 (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 - }; - } + 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, Ref)>, + mut trigger: Single<&mut TriggerSender>, mut commands: Commands, - mut state: ResMut, + mut state: Single<&mut BackpackUiState>, time: Res