Sync reloading and cash (#66)
This commit is contained in:
@@ -15,6 +15,7 @@ use shared::{DebugVisuals, GameState, heads_database::HeadDatabaseAsset};
|
||||
use std::time::Duration;
|
||||
|
||||
mod config;
|
||||
mod player;
|
||||
mod server;
|
||||
mod tb_entities;
|
||||
|
||||
|
||||
94
crates/server/src/player.rs
Normal file
94
crates/server/src/player.rs
Normal file
@@ -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<SpawnPoint>>,
|
||||
asset_server: Res<AssetServer>,
|
||||
heads_db: Res<HeadsDatabase>,
|
||||
) {
|
||||
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::<ControlState>::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<Kill>,
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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<UiHeadState>; 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::<BackpackUiState>();
|
||||
app.init_resource::<BackpackUiState>();
|
||||
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<UIAssets>) {
|
||||
@@ -185,52 +204,46 @@ fn spawn_head_ui(
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
fn update_visibility(
|
||||
state: Res<BackpackUiState>,
|
||||
mut backpack: Query<&mut Visibility, (With<BackpackMarker>, Without<BackpackCountText>)>,
|
||||
mut count: Query<&mut Visibility, (Without<BackpackMarker>, With<BackpackCountText>)>,
|
||||
state: Single<&BackpackUiState, Changed<BackpackUiState>>,
|
||||
mut backpack: Single<&mut Visibility, (With<BackpackMarker>, Without<BackpackCountText>)>,
|
||||
mut count: Single<&mut Visibility, (Without<BackpackMarker>, With<BackpackCountText>)>,
|
||||
) {
|
||||
if state.is_changed() {
|
||||
for mut vis in backpack.iter_mut() {
|
||||
*vis = if state.open {
|
||||
**backpack = if state.open {
|
||||
Visibility::Visible
|
||||
} else {
|
||||
Visibility::Hidden
|
||||
};
|
||||
}
|
||||
|
||||
for mut vis in count.iter_mut() {
|
||||
*vis = if !state.open {
|
||||
**count = if !state.open {
|
||||
Visibility::Visible
|
||||
} else {
|
||||
Visibility::Hidden
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
fn update_count(
|
||||
state: Res<BackpackUiState>,
|
||||
text: Query<Entity, With<BackpackCountText>>,
|
||||
state: Single<&BackpackUiState, Changed<BackpackUiState>>,
|
||||
text: Option<Single<Entity, With<BackpackCountText>>>,
|
||||
mut writer: TextUiWriter,
|
||||
) {
|
||||
if state.is_changed() {
|
||||
let Some(text) = text.iter().next() else {
|
||||
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<BackpackUiState>,
|
||||
state: Single<&BackpackUiState, Changed<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;
|
||||
@@ -252,13 +265,14 @@ fn update(
|
||||
Visibility::Hidden
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
fn swap_head_inputs(
|
||||
player: Query<(&ActionState<ControlState>, Ref<Backpack>)>,
|
||||
mut trigger: Single<&mut TriggerSender<PlayBackpackSound>>,
|
||||
mut commands: Commands,
|
||||
mut state: ResMut<BackpackUiState>,
|
||||
mut state: Single<&mut BackpackUiState>,
|
||||
time: Res<Time>,
|
||||
) {
|
||||
for (controls, backpack) in player.iter() {
|
||||
@@ -268,7 +282,7 @@ fn swap_head_inputs(
|
||||
|
||||
if controls.backpack_toggle {
|
||||
state.open = !state.open;
|
||||
commands.trigger(PlaySound::Backpack { open: state.open });
|
||||
trigger.trigger::<ActionsChannel>(PlayBackpackSound { open: state.open });
|
||||
}
|
||||
|
||||
if !state.open {
|
||||
@@ -295,9 +309,17 @@ fn swap_head_inputs(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
fn play_backpack_sound(trigger: Trigger<PlayBackpackSound>, mut commands: Commands) {
|
||||
commands.trigger(PlaySound::Backpack {
|
||||
open: trigger.event().open,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
fn sync_on_change(
|
||||
backpack: Query<Ref<Backpack>>,
|
||||
mut state: ResMut<BackpackUiState>,
|
||||
mut state: Single<&mut BackpackUiState>,
|
||||
time: Res<Time>,
|
||||
) {
|
||||
for backpack in backpack.iter() {
|
||||
@@ -307,7 +329,8 @@ fn sync_on_change(
|
||||
}
|
||||
}
|
||||
|
||||
fn sync(backpack: &Backpack, state: &mut ResMut<BackpackUiState>, time: f32) {
|
||||
#[cfg(feature = "server")]
|
||||
fn sync(backpack: &Backpack, state: &mut Single<&mut BackpackUiState>, time: f32) {
|
||||
state.count = backpack.heads.len();
|
||||
|
||||
state.scroll = state.scroll.min(state.count.saturating_sub(HEAD_SLOTS));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
mod backpack_ui;
|
||||
mod ui_head_state;
|
||||
pub mod backpack_ui;
|
||||
pub mod ui_head_state;
|
||||
|
||||
use crate::{
|
||||
cash::CashCollectEvent, global_observer, head_drop::HeadCollected, heads::HeadState,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#[cfg(feature = "server")]
|
||||
use crate::heads::HeadState;
|
||||
use bevy::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Reflect, Default)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Reflect, Default, Serialize, Deserialize)]
|
||||
pub struct UiHeadState {
|
||||
pub head: usize,
|
||||
pub health: f32,
|
||||
@@ -22,6 +24,7 @@ impl UiHeadState {
|
||||
self.reloading
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
pub(crate) fn new(value: HeadState, time: f32) -> Self {
|
||||
let reloading = if value.has_ammo() {
|
||||
None
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use crate::{GameState, HEDZ_GREEN, global_observer, loading_assets::UIAssets, sounds::PlaySound};
|
||||
use crate::{GameState, HEDZ_GREEN, loading_assets::UIAssets};
|
||||
#[cfg(feature = "server")]
|
||||
use crate::{global_observer, sounds::PlaySound};
|
||||
use bevy::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Component, Reflect, Default)]
|
||||
#[reflect(Component)]
|
||||
@@ -10,7 +13,7 @@ pub struct Cash;
|
||||
#[reflect(Component)]
|
||||
struct CashText;
|
||||
|
||||
#[derive(Resource, Reflect, Default)]
|
||||
#[derive(Component, Reflect, Default, Serialize, Deserialize, PartialEq)]
|
||||
pub struct CashResource {
|
||||
pub cash: i32,
|
||||
}
|
||||
@@ -19,20 +22,21 @@ pub struct CashResource {
|
||||
pub struct CashCollectEvent;
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.init_resource::<CashResource>();
|
||||
app.add_systems(OnEnter(GameState::Playing), setup);
|
||||
app.add_systems(
|
||||
Update,
|
||||
(rotate, update_ui).run_if(in_state(GameState::Playing)),
|
||||
);
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
global_observer!(app, on_cash_collect);
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
fn on_cash_collect(
|
||||
_trigger: Trigger<CashCollectEvent>,
|
||||
mut commands: Commands,
|
||||
mut cash: ResMut<CashResource>,
|
||||
mut cash: Single<&mut CashResource>,
|
||||
) {
|
||||
commands.trigger(PlaySound::CashCollect);
|
||||
|
||||
@@ -46,17 +50,15 @@ fn rotate(time: Res<Time>, mut query: Query<&mut Transform, With<Cash>>) {
|
||||
}
|
||||
|
||||
fn update_ui(
|
||||
cash: Res<CashResource>,
|
||||
cash: Single<&CashResource, Changed<CashResource>>,
|
||||
text: Query<Entity, With<CashText>>,
|
||||
mut writer: TextUiWriter,
|
||||
) {
|
||||
if cash.is_changed() {
|
||||
let Some(text) = text.iter().next() else {
|
||||
return;
|
||||
};
|
||||
|
||||
*writer.text(text, 0) = cash.cash.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
fn setup(mut commands: Commands, assets: Res<UIAssets>) {
|
||||
|
||||
@@ -17,7 +17,7 @@ struct HealAction {
|
||||
|
||||
fn on_heal_trigger(
|
||||
mut cmds: Commands,
|
||||
mut cash: ResMut<CashResource>,
|
||||
mut cash: Single<&mut CashResource>,
|
||||
mut query: Query<(&mut Hitpoints, &ActionState<ControlState>), With<Player>>,
|
||||
) {
|
||||
for (mut hp, controls) in query.iter_mut() {
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
use super::{ActiveHeads, HEAD_SLOTS, HeadsImages};
|
||||
use crate::{GameState, backpack::UiHeadState, loading_assets::UIAssets, player::Player};
|
||||
#[cfg(feature = "server")]
|
||||
use super::ActiveHeads;
|
||||
use super::HEAD_SLOTS;
|
||||
#[cfg(feature = "client")]
|
||||
use super::HeadsImages;
|
||||
#[cfg(feature = "server")]
|
||||
use crate::player::Player;
|
||||
use crate::{GameState, backpack::UiHeadState, loading_assets::UIAssets};
|
||||
use bevy::{ecs::spawn::SpawnIter, prelude::*};
|
||||
use bevy_ui_gradients::{AngularColorStop, BackgroundGradient, ConicGradient, Gradient, Position};
|
||||
#[cfg(feature = "client")]
|
||||
use bevy_ui_gradients::Gradient;
|
||||
use bevy_ui_gradients::{AngularColorStop, BackgroundGradient, ConicGradient, Position};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::f32::consts::PI;
|
||||
|
||||
#[derive(Component, Reflect, Default)]
|
||||
@@ -16,9 +25,9 @@ struct HeadImage(pub usize);
|
||||
#[reflect(Component)]
|
||||
struct HeadDamage(pub usize);
|
||||
|
||||
#[derive(Resource, Default, Reflect)]
|
||||
#[reflect(Resource)]
|
||||
struct UiActiveHeads {
|
||||
#[derive(Component, Default, Reflect, Serialize, Deserialize, PartialEq)]
|
||||
#[reflect(Component)]
|
||||
pub struct UiActiveHeads {
|
||||
heads: [Option<UiHeadState>; 5],
|
||||
selected_slot: usize,
|
||||
}
|
||||
@@ -28,9 +37,12 @@ pub fn plugin(app: &mut App) {
|
||||
app.register_type::<UiActiveHeads>();
|
||||
|
||||
app.add_systems(OnEnter(GameState::Playing), setup);
|
||||
#[cfg(feature = "server")]
|
||||
app.add_systems(FixedUpdate, sync.run_if(in_state(GameState::Playing)));
|
||||
#[cfg(feature = "client")]
|
||||
app.add_systems(
|
||||
Update,
|
||||
(sync, update, update_ammo, update_health).run_if(in_state(GameState::Playing)),
|
||||
FixedUpdate,
|
||||
(update, update_ammo, update_health).run_if(in_state(GameState::Playing)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,8 +73,6 @@ fn setup(mut commands: Commands, assets: Res<UIAssets>) {
|
||||
}
|
||||
}))),
|
||||
));
|
||||
|
||||
commands.init_resource::<UiActiveHeads>();
|
||||
}
|
||||
|
||||
fn spawn_head_ui(
|
||||
@@ -153,13 +163,13 @@ fn spawn_head_ui(
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
fn update(
|
||||
res: Res<UiActiveHeads>,
|
||||
res: Single<&UiActiveHeads, Changed<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>>,
|
||||
) {
|
||||
if res.is_changed() {
|
||||
for (HeadImage(head), mut vis, mut image) in head_image.iter_mut() {
|
||||
if let Some(head) = res.heads[*head] {
|
||||
*vis = Visibility::Visible;
|
||||
@@ -175,14 +185,13 @@ fn update(
|
||||
Visibility::Hidden
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
fn update_ammo(
|
||||
res: Res<UiActiveHeads>,
|
||||
res: Single<&UiActiveHeads, Changed<UiActiveHeads>>,
|
||||
mut gradients: Query<(&mut BackgroundGradient, &HeadImage)>,
|
||||
) {
|
||||
if res.is_changed() {
|
||||
for (mut gradient, HeadImage(head)) in gradients.iter_mut() {
|
||||
if let Some(head) = res.heads[*head] {
|
||||
let Gradient::Conic(gradient) = &mut gradient.0[0] else {
|
||||
@@ -201,21 +210,22 @@ fn update_ammo(
|
||||
gradient.stops[2].angle = Some(angle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_health(res: Res<UiActiveHeads>, mut query: Query<(&mut Node, &HeadDamage)>) {
|
||||
if res.is_changed() {
|
||||
#[cfg(feature = "client")]
|
||||
fn update_health(
|
||||
res: Single<&UiActiveHeads, Changed<UiActiveHeads>>,
|
||||
mut query: Query<(&mut Node, &HeadDamage)>,
|
||||
) {
|
||||
for (mut node, HeadDamage(head)) in query.iter_mut() {
|
||||
node.height =
|
||||
Val::Percent(res.heads[*head].map(|head| head.damage()).unwrap_or(0.) * 100.);
|
||||
}
|
||||
node.height = Val::Percent(res.heads[*head].map(|head| head.damage()).unwrap_or(0.) * 100.);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
fn sync(
|
||||
active_heads: Query<Ref<ActiveHeads>, With<Player>>,
|
||||
mut state: ResMut<UiActiveHeads>,
|
||||
mut state: Single<&mut UiActiveHeads>,
|
||||
time: Res<Time>,
|
||||
) {
|
||||
let Ok(active_heads) = active_heads.single() else {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
mod heads_ui;
|
||||
pub mod heads_ui;
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
use crate::animation::AnimationFlags;
|
||||
use crate::{
|
||||
GameState,
|
||||
animation::AnimationFlags,
|
||||
backpack::{BackbackSwapEvent, Backpack},
|
||||
control::ControlState,
|
||||
global_observer,
|
||||
@@ -179,9 +180,12 @@ pub struct HeadChanged(pub usize);
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.add_plugins(heads_ui::plugin);
|
||||
|
||||
app.register_type::<ActiveHeads>();
|
||||
|
||||
app.add_systems(OnEnter(GameState::Playing), setup);
|
||||
#[cfg(feature = "server")]
|
||||
app.add_systems(
|
||||
Update,
|
||||
FixedUpdate,
|
||||
(reload, sync_hp).run_if(in_state(GameState::Playing)),
|
||||
);
|
||||
app.add_systems(FixedUpdate, on_select_active_head);
|
||||
@@ -198,6 +202,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>, heads: Res<Head
|
||||
commands.insert_resource(HeadsImages { heads });
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
fn sync_hp(mut query: Query<(&mut ActiveHeads, &Hitpoints)>) {
|
||||
for (mut active_heads, hp) in query.iter_mut() {
|
||||
if active_heads.hp().get() != hp.get() {
|
||||
@@ -206,6 +211,7 @@ fn sync_hp(mut query: Query<(&mut ActiveHeads, &Hitpoints)>) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
fn reload(
|
||||
mut commands: Commands,
|
||||
mut active: Query<&mut ActiveHeads>,
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
use crate::{
|
||||
GameState,
|
||||
backpack::Backpack,
|
||||
camera::{CameraArmRotation, CameraTarget},
|
||||
cash::{Cash, CashCollectEvent},
|
||||
character::{AnimatedCharacter, Character},
|
||||
control::{ControlState, controller_common::PlayerCharacterController},
|
||||
global_observer,
|
||||
head::ActiveHead,
|
||||
head_drop::HeadDrops,
|
||||
heads::{ActiveHeads, HeadChanged, HeadState},
|
||||
heads::HeadChanged,
|
||||
heads_database::{HeadControls, HeadsDatabase},
|
||||
hitpoints::{Hitpoints, Kill},
|
||||
loading_assets::AudioAssets,
|
||||
npc::SpawnCharacter,
|
||||
sounds::PlaySound,
|
||||
tb_entities::SpawnPoint,
|
||||
};
|
||||
use avian3d::prelude::*;
|
||||
use bevy::{
|
||||
@@ -22,9 +15,6 @@ use bevy::{
|
||||
prelude::*,
|
||||
window::{CursorGrabMode, PrimaryWindow},
|
||||
};
|
||||
use lightyear::prelude::input::native::ActionState;
|
||||
#[cfg(feature = "server")]
|
||||
use lightyear::prelude::{ControlledBy, Lifetime, NetworkTarget, PredictionTarget, Replicate};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Component, Default, Serialize, Deserialize, PartialEq)]
|
||||
@@ -50,86 +40,6 @@ pub fn plugin(app: &mut App) {
|
||||
global_observer!(app, on_update_head_mesh);
|
||||
}
|
||||
|
||||
pub fn spawn(
|
||||
mut commands: Commands,
|
||||
#[cfg(feature = "server")] owner: Entity,
|
||||
query: Query<&Transform, With<SpawnPoint>>,
|
||||
asset_server: Res<AssetServer>,
|
||||
heads_db: Res<HeadsDatabase>,
|
||||
) {
|
||||
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(player_bundle(transform, &heads_db));
|
||||
player.observe(on_kill);
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
player.insert((
|
||||
Replicate::to_clients(NetworkTarget::All),
|
||||
PredictionTarget::to_clients(NetworkTarget::All),
|
||||
ControlledBy {
|
||||
owner,
|
||||
lifetime: Lifetime::SessionBased,
|
||||
},
|
||||
));
|
||||
|
||||
commands.spawn((
|
||||
AudioPlayer::new(asset_server.load("sfx/heads/angry demonstrator.ogg")),
|
||||
PlaybackSettings::DESPAWN,
|
||||
));
|
||||
|
||||
commands.trigger(SpawnCharacter(transform.translation));
|
||||
}
|
||||
|
||||
fn player_bundle(transform: Transform, heads_db: &Res<HeadsDatabase>) -> impl Bundle {
|
||||
(
|
||||
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),
|
||||
CameraTarget,
|
||||
transform,
|
||||
Visibility::default(),
|
||||
PlayerCharacterController,
|
||||
ActionState::<ControlState>::default(),
|
||||
Backpack::default(),
|
||||
children![(
|
||||
Name::new("player-rig"),
|
||||
PlayerBodyMesh,
|
||||
CameraArmRotation,
|
||||
children![AnimatedCharacter::new(0)],
|
||||
)],
|
||||
)
|
||||
}
|
||||
|
||||
fn on_kill(
|
||||
trigger: Trigger<Kill>,
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
fn cursor_recenter(q_windows: Single<&mut Window, With<PrimaryWindow>>) {
|
||||
let mut primary_window = q_windows;
|
||||
let center = Vec2::new(
|
||||
|
||||
@@ -2,8 +2,9 @@ use crate::{
|
||||
GameState,
|
||||
abilities::BuildExplosionSprite,
|
||||
animation::AnimationFlags,
|
||||
backpack::Backpack,
|
||||
backpack::{Backpack, backpack_ui::BackpackUiState},
|
||||
camera::{CameraArmRotation, CameraTarget},
|
||||
cash::CashResource,
|
||||
character::{self, AnimatedCharacter},
|
||||
control::{
|
||||
ControlState,
|
||||
@@ -13,7 +14,7 @@ use crate::{
|
||||
cutscene::StartCutscene,
|
||||
global_observer,
|
||||
head::ActiveHead,
|
||||
heads::ActiveHeads,
|
||||
heads::{ActiveHeads, heads_ui::UiActiveHeads},
|
||||
hitpoints::Hitpoints,
|
||||
loading_assets::{GameAssets, HeadDropAssets},
|
||||
platforms::ActivePlatform,
|
||||
@@ -62,8 +63,10 @@ pub fn plugin(app: &mut App) {
|
||||
app.register_component::<AnimatedCharacter>();
|
||||
app.register_component::<AnimationFlags>();
|
||||
app.register_component::<Backpack>();
|
||||
app.register_component::<BackpackUiState>();
|
||||
app.register_component::<CameraArmRotation>();
|
||||
app.register_component::<CameraTarget>();
|
||||
app.register_component::<CashResource>();
|
||||
app.register_component::<happy_feet::prelude::Character>();
|
||||
app.register_component::<character::Character>();
|
||||
app.register_component::<CharacterDrag>();
|
||||
@@ -90,6 +93,7 @@ pub fn plugin(app: &mut App) {
|
||||
app.register_component::<Transform>()
|
||||
.add_prediction(PredictionMode::Full)
|
||||
.add_should_rollback(transform_should_rollback);
|
||||
app.register_component::<UiActiveHeads>();
|
||||
// `Visibility` isn't `(De)Serialize`, so we have to provide custom serde for it.
|
||||
app.register_component_custom_serde::<Visibility>(SerializeFns {
|
||||
serialize: |comp, writer| writer.write_u8(*comp as u8).map_err(SerializationError::Io),
|
||||
@@ -107,6 +111,8 @@ pub fn plugin(app: &mut App) {
|
||||
app.replicate_trigger::<BuildExplosionSprite, ActionsChannel>();
|
||||
app.replicate_trigger::<StartCutscene, ActionsChannel>();
|
||||
|
||||
app.replicate_trigger::<PlayBackpackSound, ActionsChannel>();
|
||||
|
||||
app.add_systems(
|
||||
OnEnter(GameState::MapLoading),
|
||||
|mut counter: ResMut<TbMapIdCounter>| counter.reset(),
|
||||
@@ -115,6 +121,10 @@ pub fn plugin(app: &mut App) {
|
||||
global_observer!(app, spawn_gltf_scene_roots);
|
||||
}
|
||||
|
||||
fn transform_should_rollback(this: &Transform, that: &Transform) -> bool {
|
||||
this.translation.distance_squared(that.translation) >= 0.01f32.powf(2.)
|
||||
}
|
||||
|
||||
/// Trenchbroom map entities (spawned during map loading) must be despawned manually if the server
|
||||
/// has already despawned it but the client has just loaded the map and connected
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
@@ -167,8 +177,9 @@ impl TbMapIdCounter {
|
||||
#[reflect(Resource)]
|
||||
pub struct TbMapEntityMapping(pub HashMap<u64, Entity>);
|
||||
|
||||
fn transform_should_rollback(this: &Transform, that: &Transform) -> bool {
|
||||
this.translation.distance_squared(that.translation) >= 0.01f32.powf(2.)
|
||||
#[derive(Clone, Event, Serialize, Deserialize)]
|
||||
pub struct PlayBackpackSound {
|
||||
pub open: bool,
|
||||
}
|
||||
|
||||
#[derive(Component, Reflect, Serialize, Deserialize, PartialEq)]
|
||||
|
||||
Reference in New Issue
Block a user