Sync reloading and cash (#66)

This commit is contained in:
PROMETHIA-27
2025-09-27 16:04:19 -04:00
committed by GitHub
parent 83c59519e5
commit fb4c6f501c
12 changed files with 285 additions and 225 deletions

View File

@@ -15,6 +15,7 @@ use shared::{DebugVisuals, GameState, heads_database::HeadDatabaseAsset};
use std::time::Duration;
mod config;
mod player;
mod server;
mod tb_entities;

View 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));
}
}

View File

@@ -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(())
}

View File

@@ -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
};
**count = if !state.open {
Visibility::Visible
} else {
Visibility::Hidden
};
}
for mut vis in count.iter_mut() {
*vis = 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;
@@ -253,12 +266,13 @@ fn update(
};
}
}
}
#[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));

View File

@@ -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,

View File

@@ -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

View File

@@ -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,18 +50,16 @@ 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>) {
commands.spawn((

View File

@@ -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() {

View File

@@ -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;
@@ -176,13 +186,12 @@ fn update(
};
}
}
}
#[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 {
@@ -202,20 +211,21 @@ fn update_ammo(
}
}
}
}
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 {

View File

@@ -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>,

View File

@@ -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(

View File

@@ -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)]