fix cash in multiplayer (#97)

* replicate it
* use collision observer to simplify
* pass entity of player that receives cash for a duplicate head
This commit is contained in:
extrawurst
2025-12-22 03:44:43 +01:00
committed by GitHub
parent 375b8a5b46
commit da5c0f8fb7
6 changed files with 69 additions and 74 deletions

View File

@@ -54,7 +54,7 @@ fn on_head_collect(
let (mut backpack, active_heads) = query.get_mut(entity)?; let (mut backpack, active_heads) = query.get_mut(entity)?;
if backpack.contains(head) || active_heads.contains(head) { if backpack.contains(head) || active_heads.contains(head) {
cmds.trigger(CashCollectEvent); cmds.trigger(CashCollectEvent { entity });
} else { } else {
backpack.insert(head, heads_db.as_ref()); backpack.insert(head, heads_db.as_ref());
} }

View File

@@ -1,9 +1,17 @@
use crate::{GameState, global_observer, protocol::PlaySound, server_observer}; use crate::{
use avian3d::prelude::Rotation; GameState, global_observer,
physics_layers::GameLayer,
player::Player,
protocol::{GltfSceneRoot, PlaySound, is_server},
server_observer,
tb_entities::CashSpawn,
};
use avian3d::prelude::*;
use bevy::prelude::*; use bevy::prelude::*;
use bevy_replicon::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Component, Reflect, Default)] #[derive(Component, Reflect, Default, Deserialize, Serialize)]
#[reflect(Component)] #[reflect(Component)]
#[require(Transform)] #[require(Transform)]
pub struct Cash; pub struct Cash;
@@ -17,28 +25,64 @@ pub struct CashInventory {
pub cash: i32, pub cash: i32,
} }
#[derive(Event)] #[derive(EntityEvent)]
pub struct CashCollectEvent; pub struct CashCollectEvent {
pub entity: Entity,
pub fn plugin(app: &mut App) {
app.add_systems(Update, rotate.run_if(in_state(GameState::Playing)));
server_observer!(app, on_cash_collect);
} }
fn on_cash_collect( pub fn plugin(app: &mut App) {
_trigger: On<CashCollectEvent>, app.add_systems(OnEnter(GameState::Playing), setup.run_if(is_server));
app.add_systems(Update, rotate.run_if(in_state(GameState::Playing)));
server_observer!(app, on_cash_collected);
}
fn setup(mut commands: Commands, query: Query<Entity, With<CashSpawn>>) {
for entity in query.iter() {
commands
.entity(entity)
.insert((
Name::new("cash"),
GltfSceneRoot::Cash,
Cash,
Collider::cuboid(2., 3.0, 2.),
CollisionLayers::new(GameLayer::CollectibleSensors, LayerMask::ALL),
RigidBody::Kinematic,
CollisionEventsEnabled,
Sensor,
Replicated,
))
.observe(on_cash_collision);
}
}
fn on_cash_collected(
trigger: On<CashCollectEvent>,
mut commands: Commands, mut commands: Commands,
mut cash: Single<&mut CashInventory>, mut query_player: Query<&mut CashInventory, With<Player>>,
) { ) {
use bevy_replicon::prelude::{SendMode, ServerTriggerExt, ToClients}; if let Ok(mut cash) = query_player.get_mut(trigger.entity) {
commands.server_trigger(ToClients {
mode: SendMode::Broadcast,
message: PlaySound::CashCollect,
});
commands.server_trigger(ToClients { cash.cash += 100;
mode: SendMode::Broadcast, }
message: PlaySound::CashCollect, }
});
cash.cash += 100; fn on_cash_collision(
trigger: On<CollisionStart>,
mut commands: Commands,
query_player: Query<&Player>,
) {
let collectable = trigger.event().collider1;
let collider = trigger.event().collider2;
if query_player.contains(collider) {
commands.trigger(CashCollectEvent { entity: collider });
commands.entity(collectable).despawn();
}
} }
fn rotate(time: Res<Time>, mut query: Query<&mut Rotation, With<Cash>>) { fn rotate(time: Res<Time>, mut query: Query<&mut Rotation, With<Cash>>) {

View File

@@ -3,7 +3,7 @@ use crate::{
abilities::PlayerTriggerState, abilities::PlayerTriggerState,
backpack::Backpack, backpack::Backpack,
camera::{CameraArmRotation, CameraTarget}, camera::{CameraArmRotation, CameraTarget},
cash::{Cash, CashCollectEvent, CashInventory}, cash::CashInventory,
character::{AnimatedCharacter, HedzCharacter}, character::{AnimatedCharacter, HedzCharacter},
control::{Inputs, LocalInputs, controller_common::PlayerCharacterController}, control::{Inputs, LocalInputs, controller_common::PlayerCharacterController},
global_observer, global_observer,
@@ -16,7 +16,6 @@ use crate::{
protocol::{ClientHeadChanged, OwnedByClient, PlaySound, PlayerId}, protocol::{ClientHeadChanged, OwnedByClient, PlaySound, PlayerId},
tb_entities::SpawnPoint, tb_entities::SpawnPoint,
}; };
use avian3d::prelude::*;
use bevy::{ use bevy::{
input::common_conditions::input_just_pressed, input::common_conditions::input_just_pressed,
prelude::*, prelude::*,
@@ -52,7 +51,6 @@ pub fn plugin(app: &mut App) {
app.add_systems( app.add_systems(
Update, Update,
( (
collect_cash,
setup_animations_marker_for_player, setup_animations_marker_for_player,
toggle_cursor_system.run_if(input_just_pressed(KeyCode::Escape)), toggle_cursor_system.run_if(input_just_pressed(KeyCode::Escape)),
) )
@@ -224,33 +222,6 @@ fn toggle_cursor_system(mut window: Single<&mut CursorOptions, With<PrimaryWindo
toggle_grab_cursor(&mut window); toggle_grab_cursor(&mut window);
} }
fn collect_cash(
mut commands: Commands,
mut collision_message_reader: MessageReader<CollisionStart>,
query_player: Query<&Player>,
query_cash: Query<&Cash>,
) {
for CollisionStart {
collider1: e1,
collider2: e2,
..
} in collision_message_reader.read()
{
let collect = if query_player.contains(*e1) && query_cash.contains(*e2) {
Some(*e2)
} else if query_player.contains(*e2) && query_cash.contains(*e1) {
Some(*e1)
} else {
None
};
if let Some(cash) = collect {
commands.trigger(CashCollectEvent);
commands.entity(cash).despawn();
}
}
}
fn setup_animations_marker_for_player( fn setup_animations_marker_for_player(
mut commands: Commands, mut commands: Commands,
animation_handles: Query<Entity, Added<AnimationGraphHandle>>, animation_handles: Query<Entity, Added<AnimationGraphHandle>>,

View File

@@ -188,6 +188,7 @@ pub enum GltfSceneRoot {
Projectile(String), Projectile(String),
HeadDrop(String), HeadDrop(String),
Key, Key,
Cash,
} }
pub fn spawn_gltf_scene_roots( pub fn spawn_gltf_scene_roots(
@@ -219,6 +220,7 @@ pub fn spawn_gltf_scene_roots(
get_scene(gltf, 0) get_scene(gltf, 0)
} }
GltfSceneRoot::Key => assets.mesh_key.clone(), GltfSceneRoot::Key => assets.mesh_key.clone(),
GltfSceneRoot::Cash => assets.mesh_cash.clone(),
}; };
commands commands

View File

@@ -6,7 +6,7 @@ use crate::{
animation::AnimationFlags, animation::AnimationFlags,
backpack::{Backpack, BackpackSwapEvent}, backpack::{Backpack, BackpackSwapEvent},
camera::{CameraArmRotation, CameraTarget}, camera::{CameraArmRotation, CameraTarget},
cash::CashInventory, cash::{Cash, CashInventory},
character::{AnimatedCharacter, HedzCharacter}, character::{AnimatedCharacter, HedzCharacter},
control::{ control::{
CashHealPressed, ClientInputs, ControllerSettings, Inputs, SelectLeftPressed, CashHealPressed, ClientInputs, ControllerSettings, Inputs, SelectLeftPressed,
@@ -106,6 +106,7 @@ pub fn plugin(app: &mut App) {
.replicate::<SquishAnimation>() .replicate::<SquishAnimation>()
.replicate_once::<Transform>() .replicate_once::<Transform>()
.replicate_once::<SpawnTrail>() .replicate_once::<SpawnTrail>()
.replicate_once::<Cash>()
.replicate_as::<Visibility, SerVisibility>(); .replicate_as::<Visibility, SerVisibility>();
app.replicate_once::<ThrownProjectile>() app.replicate_once::<ThrownProjectile>()

View File

@@ -1,6 +1,5 @@
use crate::{ use crate::{
GameState, GameState,
cash::Cash,
loading_assets::GameAssets, loading_assets::GameAssets,
physics_layers::GameLayer, physics_layers::GameLayer,
protocol::{ protocol::{
@@ -152,30 +151,8 @@ impl EnemySpawn {
#[point_class(base(Transform), model({ "path": "models/cash.glb" }))] #[point_class(base(Transform), model({ "path": "models/cash.glb" }))]
#[derive(Default)] #[derive(Default)]
#[component(on_add = Self::on_add)]
pub struct CashSpawn {} pub struct CashSpawn {}
impl CashSpawn {
fn on_add(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) {
let Some(assets) = world.get_resource::<GameAssets>() else {
return;
};
let mesh = assets.mesh_cash.clone();
world.commands().entity(entity).insert((
Name::new("cash"),
SceneRoot(mesh),
Cash,
Collider::cuboid(2., 3.0, 2.),
CollisionLayers::new(GameLayer::CollectibleSensors, LayerMask::ALL),
RigidBody::Static,
CollisionEventsEnabled,
Sensor,
));
}
}
#[point_class(base(Transform), model({ "path": "models/head_drop.glb" }))] #[point_class(base(Transform), model({ "path": "models/head_drop.glb" }))]
#[derive(Default)] #[derive(Default)]
pub struct SecretHead { pub struct SecretHead {