diff --git a/crates/shared/src/abilities/curver.rs b/crates/shared/src/abilities/curver.rs index dc8ccf7..6a3164d 100644 --- a/crates/shared/src/abilities/curver.rs +++ b/crates/shared/src/abilities/curver.rs @@ -1,21 +1,19 @@ use crate::{ GameState, - abilities::TriggerCurver, - billboards::Billboard, + abilities::{BuildExplosionSprite, TriggerCurver}, heads_database::HeadsDatabase, hitpoints::Hit, - loading_assets::GameAssets, physics_layers::GameLayer, protocol::GltfSceneRoot, tb_entities::EnemySpawn, utils::{ - auto_rotate::AutoRotation, commands::CommandExt, global_observer, - sprite_3d_animation::AnimationTimer, + auto_rotate::AutoRotation, + commands::{CommandExt, EntityCommandExt}, + global_observer, }, }; use avian3d::prelude::*; -use bevy::{pbr::NotShadowCaster, prelude::*}; -use bevy_sprite3d::{Sprite3dBuilder, Sprite3dParams}; +use bevy::prelude::*; use lightyear::prelude::{NetworkTarget, Replicate}; use std::f32::consts::PI; @@ -27,14 +25,7 @@ struct CurverProjectile { damage: u32, } -#[derive(Resource)] -struct ShotAssets { - image: Handle, - layout: Handle, -} - pub fn plugin(app: &mut App) { - app.add_systems(OnEnter(GameState::Playing), setup); app.add_systems( Update, (shot_collision, enemy_hit).run_if(in_state(GameState::Playing)), @@ -47,16 +38,6 @@ pub fn plugin(app: &mut App) { global_observer!(app, on_trigger_missile); } -fn setup(mut commands: Commands, assets: Res, mut sprite_params: Sprite3dParams) { - let layout = TextureAtlasLayout::from_grid(UVec2::splat(256), 7, 6, None, None); - let texture_atlas_layout = sprite_params.atlas_layouts.add(layout); - - commands.insert_resource(ShotAssets { - image: assets.impact_atlas.clone(), - layout: texture_atlas_layout, - }); -} - fn on_trigger_missile( trigger: Trigger, mut commands: Commands, @@ -156,8 +137,6 @@ fn shot_collision( mut collision_event_reader: EventReader, query_shot: Query<&Transform, With>, sensors: Query<(), With>, - assets: Res, - mut sprite_params: Sprite3dParams, ) { for CollisionStarted(e1, e2) in collision_event_reader.read() { if !query_shot.contains(*e1) && !query_shot.contains(*e2) { @@ -180,27 +159,10 @@ fn shot_collision( continue; } - let texture_atlas = TextureAtlas { - layout: assets.layout.clone(), - index: 0, - }; - - commands - .spawn( - Sprite3dBuilder { - image: assets.image.clone(), - pixels_per_metre: 128., - alpha_mode: AlphaMode::Blend, - unlit: true, - ..default() - } - .bundle_with_atlas(&mut sprite_params, texture_atlas), - ) - .insert(( - Billboard::All, - Transform::from_translation(shot_pos), - NotShadowCaster, - AnimationTimer::new(Timer::from_seconds(0.01, TimerMode::Repeating)), - )); + commands.trigger_server(BuildExplosionSprite { + pos: shot_pos, + pixels_per_meter: 128., + time: 0.01, + }); } } diff --git a/crates/shared/src/abilities/missile.rs b/crates/shared/src/abilities/missile.rs index 45b8449..9c3f21b 100644 --- a/crates/shared/src/abilities/missile.rs +++ b/crates/shared/src/abilities/missile.rs @@ -1,20 +1,20 @@ use super::TriggerMissile; use crate::{ GameState, - billboards::Billboard, + abilities::BuildExplosionSprite, heads_database::HeadsDatabase, - loading_assets::GameAssets, physics_layers::GameLayer, protocol::GltfSceneRoot, sounds::PlaySound, utils::{ - commands::CommandExt, explosions::Explosion, global_observer, - sprite_3d_animation::AnimationTimer, trail::Trail, + commands::{CommandExt, EntityCommandExt}, + explosions::Explosion, + global_observer, + trail::Trail, }, }; use avian3d::prelude::*; -use bevy::{pbr::NotShadowCaster, prelude::*}; -use bevy_sprite3d::{Sprite3dBuilder, Sprite3dParams}; +use bevy::prelude::*; use lightyear::prelude::{NetworkTarget, Replicate}; use std::f32::consts::PI; @@ -27,14 +27,7 @@ struct MissileProjectile { damage: u32, } -#[derive(Resource)] -struct ShotAssets { - image: Handle, - layout: Handle, -} - pub fn plugin(app: &mut App) { - app.add_systems(OnEnter(GameState::Playing), setup); app.add_systems(Update, shot_collision.run_if(in_state(GameState::Playing))); app.add_systems( FixedUpdate, @@ -44,16 +37,6 @@ pub fn plugin(app: &mut App) { global_observer!(app, on_trigger_missile); } -fn setup(mut commands: Commands, assets: Res, mut sprite_params: Sprite3dParams) { - let layout = TextureAtlasLayout::from_grid(UVec2::splat(256), 7, 6, None, None); - let texture_atlas_layout = sprite_params.atlas_layouts.add(layout); - - commands.insert_resource(ShotAssets { - image: assets.impact_atlas.clone(), - layout: texture_atlas_layout, - }); -} - fn on_trigger_missile( trigger: Trigger, mut commands: Commands, @@ -144,8 +127,6 @@ fn shot_collision( mut collision_event_reader: EventReader, query_shot: Query<(&MissileProjectile, &Transform)>, sensors: Query<(), With>, - assets: Res, - mut sprite_params: Sprite3dParams, ) { for CollisionStarted(e1, e2) in collision_event_reader.read() { if !query_shot.contains(*e1) && !query_shot.contains(*e2) { @@ -175,27 +156,10 @@ fn shot_collision( radius: 6., }); - let texture_atlas = TextureAtlas { - layout: assets.layout.clone(), - index: 0, - }; - - commands - .spawn( - Sprite3dBuilder { - image: assets.image.clone(), - pixels_per_metre: 16., - alpha_mode: AlphaMode::Blend, - unlit: true, - ..default() - } - .bundle_with_atlas(&mut sprite_params, texture_atlas), - ) - .insert(( - Billboard::All, - Transform::from_translation(shot_pos), - NotShadowCaster, - AnimationTimer::new(Timer::from_seconds(0.01, TimerMode::Repeating)), - )); + commands.trigger_server(BuildExplosionSprite { + pos: shot_pos, + pixels_per_meter: 16., + time: 0.01, + }); } } diff --git a/crates/shared/src/abilities/mod.rs b/crates/shared/src/abilities/mod.rs index 3c8c797..50f851f 100644 --- a/crates/shared/src/abilities/mod.rs +++ b/crates/shared/src/abilities/mod.rs @@ -1,9 +1,9 @@ -mod arrow; -mod curver; -mod gun; -mod healing; -mod missile; -mod thrown; +pub mod arrow; +pub mod curver; +pub mod gun; +pub mod healing; +pub mod missile; +pub mod thrown; use crate::{ GameState, @@ -13,11 +13,14 @@ use crate::{ head::ActiveHead, heads::ActiveHeads, heads_database::HeadsDatabase, + loading_assets::GameAssets, physics_layers::GameLayer, player::{Player, PlayerBodyMesh}, sounds::PlaySound, + utils::{billboards::Billboard, sprite_3d_animation::AnimationTimer}, }; -use bevy::prelude::*; +use bevy::{pbr::NotShadowCaster, prelude::*}; +use bevy_sprite3d::{Sprite3dBuilder, Sprite3dParams}; pub use healing::Healing; use healing::HealingStateChanged; use serde::{Deserialize, Serialize}; @@ -109,12 +112,14 @@ pub fn plugin(app: &mut App) { app.add_plugins(healing::plugin); app.add_plugins(curver::plugin); + app.add_systems(OnEnter(GameState::Playing), setup); app.add_systems( Update, (update, update_heal_ability).run_if(in_state(GameState::Playing)), ); global_observer!(app, on_trigger_state); + global_observer!(app, build_explosion_sprite); } fn on_trigger_state( @@ -222,3 +227,57 @@ fn update_heal_ability( } } } + +#[derive(Resource)] +struct ShotAssets { + image: Handle, + layout: Handle, +} + +fn setup(mut commands: Commands, assets: Res, mut sprite_params: Sprite3dParams) { + let layout = TextureAtlasLayout::from_grid(UVec2::splat(256), 7, 6, None, None); + let texture_atlas_layout = sprite_params.atlas_layouts.add(layout); + + commands.insert_resource(ShotAssets { + image: assets.impact_atlas.clone(), + layout: texture_atlas_layout, + }); +} + +#[derive(Clone, Copy, Event, Serialize, Deserialize, PartialEq)] +pub struct BuildExplosionSprite { + pos: Vec3, + pixels_per_meter: f32, + time: f32, +} + +fn build_explosion_sprite( + trigger: Trigger, + mut commands: Commands, + assets: Res, + mut sprite_params: Sprite3dParams, +) { + commands.spawn(( + Transform::from_translation(trigger.event().pos), + Sprite3dBuilder { + image: assets.image.clone(), + pixels_per_metre: trigger.event().pixels_per_meter, + alpha_mode: AlphaMode::Blend, + unlit: true, + ..default() + } + .bundle_with_atlas( + &mut sprite_params, + TextureAtlas { + layout: assets.layout.clone(), + index: 0, + }, + ), + Billboard::All, + NotShadowCaster, + AnimationTimer::new(Timer::from_seconds( + trigger.event().time, + TimerMode::Repeating, + )), + )); +} diff --git a/crates/shared/src/abilities/thrown.rs b/crates/shared/src/abilities/thrown.rs index b6c352b..64accd3 100644 --- a/crates/shared/src/abilities/thrown.rs +++ b/crates/shared/src/abilities/thrown.rs @@ -1,21 +1,21 @@ use super::TriggerThrow; use crate::{ GameState, - billboards::Billboard, + abilities::BuildExplosionSprite, heads_database::HeadsDatabase, - loading_assets::GameAssets, physics_layers::GameLayer, protocol::GltfSceneRoot, sounds::PlaySound, utils::{ - auto_rotate::AutoRotation, commands::CommandExt, explosions::Explosion, global_observer, - sprite_3d_animation::AnimationTimer, + auto_rotate::AutoRotation, + commands::{CommandExt, EntityCommandExt}, + explosions::Explosion, + global_observer, }, }; use avian3d::prelude::*; -use bevy::{pbr::NotShadowCaster, prelude::*}; +use bevy::prelude::*; use bevy_ballistic::launch_velocity; -use bevy_sprite3d::{Sprite3dBuilder, Sprite3dParams}; use lightyear::prelude::{NetworkTarget, Replicate}; use serde::{Deserialize, Serialize}; use std::f32::consts::PI; @@ -26,29 +26,12 @@ pub struct ThrownProjectile { damage: u32, } -#[derive(Resource)] -struct ShotAssets { - image: Handle, - layout: Handle, -} - pub fn plugin(app: &mut App) { - app.add_systems(OnEnter(GameState::Playing), setup); app.add_systems(Update, shot_collision.run_if(in_state(GameState::Playing))); global_observer!(app, on_trigger_thrown); } -fn setup(mut commands: Commands, assets: Res, mut sprite_params: Sprite3dParams) { - let layout = TextureAtlasLayout::from_grid(UVec2::splat(256), 7, 6, None, None); - let texture_atlas_layout = sprite_params.atlas_layouts.add(layout); - - commands.insert_resource(ShotAssets { - image: assets.impact_atlas.clone(), - layout: texture_atlas_layout, - }); -} - fn on_trigger_thrown( trigger: Trigger, mut commands: Commands, @@ -113,8 +96,6 @@ fn shot_collision( mut collision_event_reader: EventReader, query_shot: Query<(&ThrownProjectile, &Transform)>, sensors: Query<(), With>, - assets: Res, - mut sprite_params: Sprite3dParams, ) { for CollisionStarted(e1, e2) in collision_event_reader.read() { if !query_shot.contains(*e1) && !query_shot.contains(*e2) { @@ -156,29 +137,11 @@ fn shot_collision( //TODO: support different impact animations if animation { - commands - .spawn( - Sprite3dBuilder { - image: assets.image.clone(), - pixels_per_metre: 32., - alpha_mode: AlphaMode::Blend, - unlit: true, - ..default() - } - .bundle_with_atlas( - &mut sprite_params, - TextureAtlas { - layout: assets.layout.clone(), - index: 0, - }, - ), - ) - .insert(( - Billboard::All, - Transform::from_translation(shot_pos), - NotShadowCaster, - AnimationTimer::new(Timer::from_seconds(0.02, TimerMode::Repeating)), - )); + commands.trigger_server(BuildExplosionSprite { + pos: shot_pos, + pixels_per_meter: 32., + time: 0.02, + }); } } } diff --git a/crates/shared/src/protocol.rs b/crates/shared/src/protocol.rs index 21004f8..b1ff2b4 100644 --- a/crates/shared/src/protocol.rs +++ b/crates/shared/src/protocol.rs @@ -1,12 +1,17 @@ -use crate::{global_observer, loading_assets::GameAssets}; +use crate::{ + abilities::BuildExplosionSprite, global_observer, loading_assets::GameAssets, + utils::triggers::TriggerAppExt, +}; use bevy::prelude::*; -use lightyear::prelude::AppComponentExt; +use lightyear::prelude::{ActionsChannel, AppComponentExt}; use serde::{Deserialize, Serialize}; pub fn plugin(app: &mut App) { app.register_component::(); app.register_component::(); + app.replicate_trigger::(); + global_observer!(app, spawn_gltf_scene_roots); } diff --git a/crates/shared/src/utils/commands.rs b/crates/shared/src/utils/commands.rs index f13591d..9b69eec 100644 --- a/crates/shared/src/utils/commands.rs +++ b/crates/shared/src/utils/commands.rs @@ -1,21 +1,49 @@ use bevy::ecs::{ - bundle::Bundle, resource::Resource, system::EntityCommands, world::EntityWorldMut, + bundle::Bundle, + event::Event, + resource::Resource, + system::{Commands, EntityCommands}, + world::{EntityWorldMut, World}, }; #[derive(Default, Resource)] pub struct IsServer; pub trait CommandExt { - fn insert_server(&mut self, bundle: impl Bundle) -> &mut Self; + fn trigger_server(&mut self, event: impl Event) -> &mut Self; } -impl<'w> CommandExt for EntityCommands<'w> { - fn insert_server(&mut self, bundle: impl Bundle) -> &mut Self { - self.queue(|mut entity: EntityWorldMut| { - if entity.world().contains_resource::() { - entity.insert(bundle); +impl<'w, 's> CommandExt for Commands<'w, 's> { + fn trigger_server(&mut self, event: impl Event) -> &mut Self { + self.queue(|world: &mut World| { + if world.contains_resource::() { + world.trigger(event); } }); self } } + +pub trait EntityCommandExt { + fn insert_server(&mut self, bundle: impl Bundle) -> &mut Self; + + fn trigger_server(&mut self, event: impl Event) -> &mut Self; +} + +impl<'w> EntityCommandExt for EntityCommands<'w> { + fn insert_server(&mut self, bundle: impl Bundle) -> &mut Self { + self.queue(|mut entity: EntityWorldMut| { + if entity.world().contains_resource::() { + entity.insert(bundle); + } + }) + } + + fn trigger_server(&mut self, event: impl Event) -> &mut Self { + self.queue(|mut entity: EntityWorldMut| { + if entity.world().contains_resource::() { + entity.trigger(event); + } + }) + } +} diff --git a/crates/shared/src/utils/mod.rs b/crates/shared/src/utils/mod.rs index 1c6dc10..f98fa8f 100644 --- a/crates/shared/src/utils/mod.rs +++ b/crates/shared/src/utils/mod.rs @@ -6,6 +6,7 @@ pub mod observers; pub mod sprite_3d_animation; pub mod squish_animation; pub mod trail; +pub mod triggers; use bevy::prelude::*; pub(crate) use observers::global_observer; diff --git a/crates/shared/src/utils/triggers.rs b/crates/shared/src/utils/triggers.rs new file mode 100644 index 0000000..1689a8e --- /dev/null +++ b/crates/shared/src/utils/triggers.rs @@ -0,0 +1,55 @@ +use crate::utils::commands::IsServer; +use bevy::{ecs::system::SystemParam, prelude::*}; +use lightyear::prelude::{AppTriggerExt, Channel, NetworkDirection, RemoteTrigger, TriggerSender}; +use serde::{Deserialize, Serialize}; + +#[derive(SystemParam)] +pub struct ServerMultiTriggerSender<'w, 's, M: Event + Clone> { + senders: Query<'w, 's, &'static mut TriggerSender>, + is_server: Option>, +} + +impl<'w, 's, M: Event + Clone> ServerMultiTriggerSender<'w, 's, M> { + pub fn server_trigger_targets(&mut self, trigger: M, target: &[Entity]) { + if self.is_server.is_none() { + return; + } + + for mut sender in self.senders.iter_mut() { + sender.trigger_targets::(trigger.clone(), target.iter().copied()); + } + } +} + +pub trait TriggerAppExt { + fn replicate_trigger Deserialize<'de>, C: Channel>( + &mut self, + ); +} + +impl TriggerAppExt for App { + fn replicate_trigger Deserialize<'de>, C: Channel>( + &mut self, + ) { + self.add_trigger::() + .add_direction(NetworkDirection::ServerToClient); + self.add_observer(replicate_trigger_to_clients::); + self.add_observer(remote_to_local_trigger::); + } +} + +fn replicate_trigger_to_clients( + trigger: Trigger, + mut sender: ServerMultiTriggerSender, +) { + let targets: &[Entity] = if trigger.target() == Entity::PLACEHOLDER { + &[] + } else { + &[trigger.target()] + }; + sender.server_trigger_targets::(trigger.event().clone(), targets); +} + +fn remote_to_local_trigger(trigger: Trigger>, mut c: Commands) { + c.trigger_targets(trigger.event().trigger.clone(), trigger.target()); +}