replication for client side projectiles (#87)

This commit is contained in:
extrawurst
2025-12-14 19:41:45 +01:00
committed by GitHub
parent e7ebff2029
commit d93b38d1c8
15 changed files with 1139 additions and 962 deletions

1748
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -55,8 +55,8 @@ bevy_asset_loader = "=0.24.0-rc.1"
bevy_ballistic = { git = "https://github.com/rustunit/bevy_ballistic.git", rev = "b08ffec" } bevy_ballistic = { git = "https://github.com/rustunit/bevy_ballistic.git", rev = "b08ffec" }
bevy_common_assets = { version = "0.14.0", features = ["ron"] } bevy_common_assets = { version = "0.14.0", features = ["ron"] }
bevy_debug_log = { git = "https://github.com/rustunit/bevy_debug_log.git", rev = "86051a0" } bevy_debug_log = { git = "https://github.com/rustunit/bevy_debug_log.git", rev = "86051a0" }
bevy_replicon = "0.36.1" bevy_replicon = "0.37.1"
bevy_replicon_renet = "0.12.0" bevy_replicon_renet = "0.13.0"
bevy_sprite3d = "7.0.0" bevy_sprite3d = "7.0.0"
bevy_trenchbroom = { version = "0.10", default-features = false, features = [ bevy_trenchbroom = { version = "0.10", default-features = false, features = [
"physics-integration", "physics-integration",

View File

@@ -6,6 +6,7 @@ use bevy::{
}, },
prelude::*, prelude::*,
}; };
use bevy_replicon::client::ClientSystems;
use shared::{ use shared::{
control::{ control::{
BackpackButtonPress, CashHealPressed, ClientInputs, ControllerSet, Inputs, LocalInputs, BackpackButtonPress, CashHealPressed, ClientInputs, ControllerSet, Inputs, LocalInputs,
@@ -28,12 +29,21 @@ pub fn plugin(app: &mut App) {
) )
.chain() .chain()
.in_set(ControllerSet::CollectInputs) .in_set(ControllerSet::CollectInputs)
.before(ClientSystems::Receive)
.run_if( .run_if(
in_state(GameState::Playing) in_state(GameState::Playing)
.and(resource_exists_and_equals(CharacterInputEnabled::On)), .and(resource_exists_and_equals(CharacterInputEnabled::On)),
), ),
) );
.add_systems(PreUpdate, overwrite_local_inputs);
// run this deliberately after local input processing ended
// TODO: can and should be ordered using a set to guarantee it gets send out ASAP but after local input processing
app.add_systems(
PreUpdate,
overwrite_local_inputs.after(ClientSystems::Receive).run_if(
in_state(GameState::Playing).and(resource_exists_and_equals(CharacterInputEnabled::On)),
),
);
app.add_systems( app.add_systems(
Update, Update,

View File

@@ -1,18 +1,16 @@
use crate::{ use crate::{
GameState, GameState,
abilities::{BuildExplosionSprite, TriggerCurver}, abilities::{BuildExplosionSprite, ProjectileId, TriggerCurver},
heads_database::HeadsDatabase, heads_database::HeadsDatabase,
hitpoints::Hit, hitpoints::Hit,
physics_layers::GameLayer, physics_layers::GameLayer,
protocol::GltfSceneRoot,
tb_entities::EnemySpawn, tb_entities::EnemySpawn,
utils::{auto_rotate::AutoRotation, global_observer}, utils::global_observer,
}; };
use avian3d::prelude::*; use avian3d::prelude::*;
use bevy::prelude::*; use bevy::prelude::*;
use bevy_replicon::prelude::{Replicated, SendMode, ServerTriggerExt, ToClients}; use bevy_replicon::prelude::{Replicated, SendMode, ServerTriggerExt, ToClients};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::f32::consts::PI;
const MAX_SHOT_AGES: f32 = 15.; const MAX_SHOT_AGES: f32 = 15.;
@@ -21,6 +19,7 @@ const MAX_SHOT_AGES: f32 = 15.;
pub struct CurverProjectile { pub struct CurverProjectile {
time: f32, time: f32,
damage: u32, damage: u32,
projectile: String,
} }
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
@@ -30,6 +29,8 @@ pub fn plugin(app: &mut App) {
Update, Update,
(shot_collision, enemy_hit).run_if(in_state(GameState::Playing)), (shot_collision, enemy_hit).run_if(in_state(GameState::Playing)),
); );
#[cfg(feature = "client")]
app.add_systems(Update, shot_visuals.run_if(in_state(GameState::Playing)));
app.add_systems( app.add_systems(
FixedUpdate, FixedUpdate,
(update, timeout).run_if(in_state(GameState::Playing)), (update, timeout).run_if(in_state(GameState::Playing)),
@@ -38,6 +39,27 @@ pub fn plugin(app: &mut App) {
global_observer!(app, on_trigger_curver); global_observer!(app, on_trigger_curver);
} }
#[cfg(feature = "client")]
fn shot_visuals(
mut commands: Commands,
query: Query<(Entity, &CurverProjectile), Added<CurverProjectile>>,
) {
for (entity, projectile) in query.iter() {
if commands.get_entity(entity).is_ok() {
let child = commands
.spawn((
crate::utils::auto_rotate::AutoRotation(
Quat::from_rotation_x(0.4) * Quat::from_rotation_z(0.3),
),
crate::protocol::GltfSceneRoot::Projectile(projectile.projectile.clone()),
))
.id();
commands.entity(entity).add_child(child);
}
}
}
fn on_trigger_curver( fn on_trigger_curver(
trigger: On<TriggerCurver>, trigger: On<TriggerCurver>,
mut commands: Commands, mut commands: Commands,
@@ -63,30 +85,30 @@ fn on_trigger_curver(
let mut transform = Transform::from_translation(state.pos).with_rotation(rotation); let mut transform = Transform::from_translation(state.pos).with_rotation(rotation);
transform.translation += transform.forward().as_vec3() * 2.0; transform.translation += transform.forward().as_vec3() * 2.0;
commands.spawn(( let id = commands
Name::new("projectile-missile"), .spawn((
CurverProjectile { Name::new("projectile-curver"),
time: time.elapsed_secs(), CurverProjectile {
damage: head.damage, time: time.elapsed_secs(),
}, damage: head.damage,
Collider::capsule_endpoints(0.4, Vec3::new(0., 0., 2.), Vec3::new(0., 0., -2.)), projectile: head.projectile.clone(),
CollisionLayers::new( },
LayerMask(GameLayer::Projectile.to_bits()), Collider::capsule_endpoints(0.5, Vec3::new(0., 0., 1.), Vec3::new(0., 0., -1.)),
LayerMask(state.target_layer.to_bits() | GameLayer::Level.to_bits()), CollisionLayers::new(
), LayerMask(GameLayer::Projectile.to_bits()),
Sensor, LayerMask(state.target_layer.to_bits() | GameLayer::Level.to_bits()),
CollisionEventsEnabled, ),
Visibility::default(), Sensor,
transform, RigidBody::Kinematic,
Replicated, CollisionEventsEnabled,
//TODO: put in client only system Visibility::default(),
children![( transform,
Transform::from_rotation(Quat::from_rotation_x(PI / 2.).inverse()),
AutoRotation(Quat::from_rotation_x(0.4) * Quat::from_rotation_z(0.3)),
GltfSceneRoot::Projectile(head.projectile.clone()),
Replicated, Replicated,
),], ProjectileId(state.trigger_id),
)); ))
.id();
debug!(id=?id, trigger_id = state.trigger_id, "Curver");
} }
fn enemy_hit( fn enemy_hit(

View File

@@ -1,11 +1,12 @@
use super::TriggerGun; use super::TriggerGun;
use crate::{ use crate::{
GameState, billboards::Billboard, global_observer, heads_database::HeadsDatabase, GameState, abilities::ProjectileId, billboards::Billboard, global_observer,
hitpoints::Hit, loading_assets::GameAssets, physics_layers::GameLayer, tb_entities::EnemySpawn, heads_database::HeadsDatabase, hitpoints::Hit, loading_assets::GameAssets,
utils::sprite_3d_animation::AnimationTimer, physics_layers::GameLayer, tb_entities::EnemySpawn, utils::sprite_3d_animation::AnimationTimer,
}; };
use avian3d::prelude::*; use avian3d::prelude::*;
use bevy::{light::NotShadowCaster, prelude::*}; use bevy::{light::NotShadowCaster, prelude::*};
use bevy_replicon::prelude::Replicated;
use bevy_sprite3d::Sprite3d; use bevy_sprite3d::Sprite3d;
#[derive(Component)] #[derive(Component)]
@@ -123,6 +124,8 @@ fn on_trigger_gun(
CollisionEventsEnabled, CollisionEventsEnabled,
Visibility::default(), Visibility::default(),
transform, transform,
Replicated,
ProjectileId(state.trigger_id),
Children::spawn(Spawn(Gizmo { Children::spawn(Spawn(Gizmo {
handle: gizmo_assets.add({ handle: gizmo_assets.add({
let mut g = GizmoAsset::default(); let mut g = GizmoAsset::default();

View File

@@ -1,7 +1,7 @@
use super::TriggerMissile; use super::TriggerMissile;
use crate::{ use crate::{
GameState, GameState,
abilities::{ExplodingProjectile, ExplodingProjectileSet}, abilities::{ExplodingProjectile, ExplodingProjectileSet, ProjectileId},
heads_database::HeadsDatabase, heads_database::HeadsDatabase,
physics_layers::GameLayer, physics_layers::GameLayer,
protocol::{GltfSceneRoot, PlaySound}, protocol::{GltfSceneRoot, PlaySound},
@@ -60,36 +60,41 @@ fn on_trigger_missile(
let mut transform = Transform::from_translation(state.pos).with_rotation(rotation); let mut transform = Transform::from_translation(state.pos).with_rotation(rotation);
transform.translation += transform.forward().as_vec3() * 2.0; transform.translation += transform.forward().as_vec3() * 2.0;
commands.spawn(( let id = commands
Name::new("projectile-missile"), .spawn((
MissileProjectile { Name::new("projectile-missile"),
time: time.elapsed_secs(), MissileProjectile {
damage: head.damage, time: time.elapsed_secs(),
}, damage: head.damage,
SpawnTrail::new( },
12, SpawnTrail::new(
LinearRgba::rgb(1., 0.0, 0.), 12,
LinearRgba::rgb(0.9, 0.9, 0.), LinearRgba::rgb(1., 0.0, 0.),
10., LinearRgba::rgb(0.9, 0.9, 0.),
) 10.,
.init_with_pos(), )
Collider::capsule_endpoints(0.4, Vec3::new(0., 0., 2.), Vec3::new(0., 0., -2.)), .init_with_pos(),
CollisionLayers::new( Collider::capsule_endpoints(0.4, Vec3::new(0., 0., 2.), Vec3::new(0., 0., -2.)),
LayerMask(GameLayer::Projectile.to_bits()), CollisionLayers::new(
LayerMask(state.target_layer.to_bits() | GameLayer::Level.to_bits()), LayerMask(GameLayer::Projectile.to_bits()),
), LayerMask(state.target_layer.to_bits() | GameLayer::Level.to_bits()),
Sensor, ),
RigidBody::Kinematic, Sensor,
CollisionEventsEnabled, RigidBody::Kinematic,
Visibility::default(), CollisionEventsEnabled,
transform, Visibility::default(),
Replicated, transform,
children![( Replicated,
Transform::from_rotation(Quat::from_rotation_x(PI / 2.).inverse()), ProjectileId(state.trigger_id),
GltfSceneRoot::Projectile("missile".to_string()), children![(
Replicated Transform::from_rotation(Quat::from_rotation_x(PI / 2.).inverse()),
)], GltfSceneRoot::Projectile("missile".to_string()),
)); Replicated
)],
))
.id();
debug!(id=?id, trigger_id = state.trigger_id, "Missile");
} }
fn update(mut query: Query<&mut Transform, With<MissileProjectile>>) { fn update(mut query: Query<&mut Transform, With<MissileProjectile>>) {

View File

@@ -6,18 +6,21 @@ pub mod missile;
pub mod thrown; pub mod thrown;
use crate::{ use crate::{
GameState, global_observer, GameState,
control::ControllerSet,
global_observer,
loading_assets::GameAssets, loading_assets::GameAssets,
physics_layers::GameLayer, physics_layers::GameLayer,
player::Player,
protocol::PlaySound, protocol::PlaySound,
utils::{billboards::Billboard, explosions::Explosion, sprite_3d_animation::AnimationTimer}, utils::{billboards::Billboard, explosions::Explosion, sprite_3d_animation::AnimationTimer},
}; };
use crate::{ use crate::{
aim::AimTarget, character::CharacterHierarchy, control::Inputs, heads::ActiveHeads, aim::AimTarget, character::CharacterHierarchy, control::Inputs, heads::ActiveHeads,
heads_database::HeadsDatabase, player::Player, heads_database::HeadsDatabase,
}; };
use bevy::{light::NotShadowCaster, prelude::*}; use bevy::{light::NotShadowCaster, prelude::*};
use bevy_replicon::prelude::{SendMode, ServerTriggerExt, ToClients}; use bevy_replicon::prelude::{SendMode, ServerTriggerExt, Signature, ToClients};
use bevy_sprite3d::Sprite3d; use bevy_sprite3d::Sprite3d;
pub use healing::Healing; pub use healing::Healing;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -44,6 +47,7 @@ pub struct TriggerData {
pos: Vec3, pos: Vec3,
target_layer: GameLayer, target_layer: GameLayer,
head: usize, head: usize,
trigger_id: usize,
} }
impl TriggerData { impl TriggerData {
@@ -53,6 +57,7 @@ impl TriggerData {
pos: Vec3, pos: Vec3,
target_layer: GameLayer, target_layer: GameLayer,
head: usize, head: usize,
trigger_id: usize,
) -> Self { ) -> Self {
Self { Self {
target, target,
@@ -60,6 +65,7 @@ impl TriggerData {
pos, pos,
target_layer, target_layer,
head, head,
trigger_id,
} }
} }
@@ -85,14 +91,13 @@ pub struct TriggerCurver(pub TriggerData);
#[reflect(Component)] #[reflect(Component)]
pub struct PlayerTriggerState { pub struct PlayerTriggerState {
next_trigger_timestamp: f32, next_trigger_timestamp: f32,
active: bool, projectile_count: usize,
} }
impl PlayerTriggerState { #[derive(Component, Reflect, Deserialize, Serialize, Hash)]
pub fn is_active(&self) -> bool { #[reflect(Component)]
self.active #[require(Signature::of::<ProjectileId>())]
} pub struct ProjectileId(pub usize);
}
#[derive(Component)] #[derive(Component)]
#[component(storage = "SparseSet")] #[component(storage = "SparseSet")]
@@ -107,6 +112,7 @@ pub struct ExplodingProjectile {
anim_time: f32, anim_time: f32,
} }
// TODO: move explosions into separate modul
#[derive(SystemSet, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] #[derive(SystemSet, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub enum ExplodingProjectileSet { pub enum ExplodingProjectileSet {
Mark, Mark,
@@ -128,12 +134,14 @@ pub fn plugin(app: &mut App) {
.run_if(in_state(GameState::Playing)), .run_if(in_state(GameState::Playing)),
); );
app.add_systems(OnEnter(GameState::Playing), setup); app.add_systems(OnEnter(GameState::Playing), setup);
app.add_systems( app.add_systems(
FixedUpdate, FixedUpdate,
(on_trigger_state, update, update_heal_ability) (update, update_heal_ability)
.chain() .chain()
.run_if(in_state(GameState::Playing)), .run_if(in_state(GameState::Playing)),
); );
app.add_systems( app.add_systems(
FixedUpdate, FixedUpdate,
explode_projectiles.in_set(ExplodingProjectileSet::Explode), explode_projectiles.in_set(ExplodingProjectileSet::Explode),
@@ -144,6 +152,8 @@ pub fn plugin(app: &mut App) {
fn explode_projectiles(mut commands: Commands, query: Query<(Entity, &ExplodingProjectile)>) { fn explode_projectiles(mut commands: Commands, query: Query<(Entity, &ExplodingProjectile)>) {
for (shot_entity, projectile) in query.iter() { for (shot_entity, projectile) in query.iter() {
debug!(id=?shot_entity, "Projectile explosion");
if let Ok(mut entity) = commands.get_entity(shot_entity) { if let Ok(mut entity) = commands.get_entity(shot_entity) {
entity.try_despawn(); entity.try_despawn();
} else { } else {
@@ -176,12 +186,6 @@ fn explode_projectiles(mut commands: Commands, query: Query<(Entity, &ExplodingP
} }
} }
fn on_trigger_state(mut players: Query<(&mut PlayerTriggerState, &Inputs), With<Player>>) {
for (mut trigger_state, inputs) in players.iter_mut() {
trigger_state.active = inputs.trigger;
}
}
fn update( fn update(
mut commands: Commands, mut commands: Commands,
mut query: Query< mut query: Query<
@@ -200,11 +204,15 @@ fn update(
character: CharacterHierarchy, character: CharacterHierarchy,
) { ) {
for (player, mut active_heads, mut trigger_state, target, inputs) in query.iter_mut() { for (player, mut active_heads, mut trigger_state, target, inputs) in query.iter_mut() {
if trigger_state.active && trigger_state.next_trigger_timestamp < time.elapsed_secs() { if inputs.trigger && trigger_state.next_trigger_timestamp < time.elapsed_secs() {
let Some(state) = active_heads.current() else { let Some(state) = active_heads.current() else {
return; return;
}; };
if !state.has_ammo() {
return;
}
let target = if let Some(target) = target.0 let target = if let Some(target) = target.0
&& query_transform.get(target).is_ok() && query_transform.get(target).is_ok()
{ {
@@ -229,6 +237,7 @@ fn update(
active_heads.use_ammo(time.elapsed_secs()); active_heads.use_ammo(time.elapsed_secs());
trigger_state.next_trigger_timestamp = time.elapsed_secs() + (1. / head.aps); trigger_state.next_trigger_timestamp = time.elapsed_secs() + (1. / head.aps);
trigger_state.projectile_count += 1;
let trigger_state = TriggerData { let trigger_state = TriggerData {
dir: Dir3::try_from(inputs.look_dir).unwrap_or(Dir3::NEG_Z), dir: Dir3::try_from(inputs.look_dir).unwrap_or(Dir3::NEG_Z),
@@ -236,6 +245,7 @@ fn update(
target, target,
target_layer: GameLayer::Npc, target_layer: GameLayer::Npc,
head: state.head, head: state.head,
trigger_id: trigger_state.projectile_count,
}; };
match head.ability { match head.ability {
@@ -252,11 +262,11 @@ fn update(
fn update_heal_ability( fn update_heal_ability(
mut commands: Commands, mut commands: Commands,
players: Query<(Entity, &ActiveHeads, Ref<PlayerTriggerState>), With<Player>>, players: Query<(Entity, &ActiveHeads, Ref<Inputs>), With<Player>>,
heads_db: Res<HeadsDatabase>, heads_db: Res<HeadsDatabase>,
) { ) {
for (player, active_heads, trigger_state) in players.iter() { for (player, active_heads, inputs) in players.iter() {
if trigger_state.is_changed() { if inputs.is_changed() {
let Some(state) = active_heads.current() else { let Some(state) = active_heads.current() else {
return; return;
}; };
@@ -268,7 +278,7 @@ fn update_heal_ability(
} }
use crate::abilities::healing::HealingState; use crate::abilities::healing::HealingState;
if trigger_state.active { if inputs.trigger {
use crate::abilities::healing::HealingStateChanged; use crate::abilities::healing::HealingStateChanged;
commands.trigger(HealingStateChanged { commands.trigger(HealingStateChanged {

View File

@@ -1,10 +1,11 @@
use super::TriggerThrow; use super::TriggerThrow;
use crate::{ use crate::{
abilities::{ExplodingProjectile, ExplodingProjectileSet}, GameState,
abilities::{ExplodingProjectile, ExplodingProjectileSet, ProjectileId},
heads_database::HeadsDatabase, heads_database::HeadsDatabase,
physics_layers::GameLayer, physics_layers::GameLayer,
protocol::{GltfSceneRoot, PlaySound}, protocol::PlaySound,
utils::{auto_rotate::AutoRotation, global_observer}, utils::global_observer,
}; };
use avian3d::prelude::*; use avian3d::prelude::*;
use bevy::prelude::*; use bevy::prelude::*;
@@ -16,17 +17,38 @@ use serde::{Deserialize, Serialize};
pub struct ThrownProjectile { pub struct ThrownProjectile {
impact_animation: bool, impact_animation: bool,
damage: u32, damage: u32,
projectile: String,
} }
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.add_systems( app.add_systems(
FixedUpdate, FixedUpdate,
shot_collision.in_set(ExplodingProjectileSet::Mark), shot_collision
.in_set(ExplodingProjectileSet::Mark)
.run_if(in_state(GameState::Playing)),
); );
#[cfg(feature = "client")]
app.add_systems(Update, shot_visuals.run_if(in_state(GameState::Playing)));
global_observer!(app, on_trigger_thrown); global_observer!(app, on_trigger_thrown);
} }
#[cfg(feature = "client")]
fn shot_visuals(
mut commands: Commands,
query: Query<(Entity, &ThrownProjectile), Added<ThrownProjectile>>,
) {
for (entity, thrown) in query.iter() {
commands.entity(entity).try_insert((
crate::utils::auto_rotate::AutoRotation(
Quat::from_rotation_x(0.4) * Quat::from_rotation_z(0.3),
),
crate::protocol::GltfSceneRoot::Projectile(thrown.projectile.clone()),
));
}
}
fn on_trigger_thrown( fn on_trigger_thrown(
trigger: On<TriggerThrow>, trigger: On<TriggerThrow>,
mut commands: Commands, mut commands: Commands,
@@ -57,13 +79,14 @@ fn on_trigger_thrown(
//TODO: projectile db? //TODO: projectile db?
let explosion_animation = !matches!(state.head, 8 | 16); let explosion_animation = !matches!(state.head, 8 | 16);
commands let id = commands
.spawn(( .spawn((
Transform::from_translation(pos), Transform::from_translation(pos),
Name::new("projectile-thrown"), Name::new("projectile-thrown"),
ThrownProjectile { ThrownProjectile {
impact_animation: explosion_animation, impact_animation: explosion_animation,
damage: head.damage, damage: head.damage,
projectile: head.projectile.clone(),
}, },
Collider::sphere(0.4), Collider::sphere(0.4),
CollisionLayers::new( CollisionLayers::new(
@@ -77,12 +100,11 @@ fn on_trigger_thrown(
Visibility::default(), Visibility::default(),
Sensor, Sensor,
Replicated, Replicated,
ProjectileId(state.trigger_id),
)) ))
.with_child(( .id();
AutoRotation(Quat::from_rotation_x(0.4) * Quat::from_rotation_z(0.3)),
GltfSceneRoot::Projectile(head.projectile.clone()), debug!(id=?id, trigger_id = state.trigger_id, "Thrown");
Replicated,
));
} }
fn shot_collision( fn shot_collision(

View File

@@ -169,6 +169,8 @@ fn engage_and_throw(
t.translation, t.translation,
crate::physics_layers::GameLayer::Player, crate::physics_layers::GameLayer::Player,
npc_head.head, npc_head.head,
// TODO: we probably need to make sure the ai's projectile does not get deduped, zero should not be used by anyone though
0,
))); )));
} }
} }

View File

@@ -1,8 +1,3 @@
#[cfg(feature = "client")]
use crate::{
backpack::backpack_ui::BackpackUiState, control::BackpackButtonPress, player::LocalPlayer,
protocol::PlaySound,
};
use crate::{ use crate::{
cash::CashCollectEvent, cash::CashCollectEvent,
global_observer, global_observer,
@@ -62,11 +57,16 @@ pub fn plugin(app: &mut App) {
#[cfg(feature = "client")] #[cfg(feature = "client")]
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn backpack_inputs( fn backpack_inputs(
backpacks: Single<(&Backpack, &mut BackpackUiState), With<LocalPlayer>>, backpacks: Single<
mut backpack_inputs: MessageReader<BackpackButtonPress>, (&Backpack, &mut backpack_ui::BackpackUiState),
With<crate::player::LocalPlayer>,
>,
mut backpack_inputs: MessageReader<crate::control::BackpackButtonPress>,
mut commands: Commands, mut commands: Commands,
time: Res<Time>, time: Res<Time>,
) { ) {
use crate::{control::BackpackButtonPress, protocol::PlaySound};
let (backpack, mut state) = backpacks.into_inner(); let (backpack, mut state) = backpacks.into_inner();
for input in backpack_inputs.read() { for input in backpack_inputs.read() {
@@ -117,7 +117,7 @@ fn backpack_inputs(
#[cfg(feature = "client")] #[cfg(feature = "client")]
fn sync_on_change( fn sync_on_change(
backpack: Query<Ref<Backpack>>, backpack: Query<Ref<Backpack>>,
mut state: Single<&mut BackpackUiState>, mut state: Single<&mut backpack_ui::BackpackUiState>,
time: Res<Time>, time: Res<Time>,
) { ) {
for backpack in backpack.iter() { for backpack in backpack.iter() {
@@ -128,7 +128,7 @@ fn sync_on_change(
} }
#[cfg(feature = "client")] #[cfg(feature = "client")]
fn sync_backpack_ui(backpack: &Backpack, state: &mut BackpackUiState, time: f32) { fn sync_backpack_ui(backpack: &Backpack, state: &mut backpack_ui::BackpackUiState, time: f32) {
use crate::backpack::backpack_ui::BACKPACK_HEAD_SLOTS; use crate::backpack::backpack_ui::BACKPACK_HEAD_SLOTS;
state.count = backpack.heads.len(); state.count = backpack.heads.len();

View File

@@ -1,7 +1,6 @@
use super::{ControllerSet, ControllerSwitchEvent}; use super::{ControllerSet, ControllerSwitchEvent};
use crate::{ use crate::{
GameState, GameState,
abilities::PlayerTriggerState,
animation::AnimationFlags, animation::AnimationFlags,
control::{ControllerSettings, Inputs, SelectedController}, control::{ControllerSettings, Inputs, SelectedController},
physics_layers::GameLayer, physics_layers::GameLayer,
@@ -43,16 +42,11 @@ pub fn plugin(app: &mut App) {
fn set_animation_flags( fn set_animation_flags(
mut player: Query< mut player: Query<
( (&Grounding, &mut AnimationFlags, &Inputs),
&Grounding,
&mut AnimationFlags,
&Inputs,
&PlayerTriggerState,
),
(With<Player>, Without<ConfirmHistory>), (With<Player>, Without<ConfirmHistory>),
>, >,
) { ) {
for (grounding, mut flags, inputs, trigger_state) in player.iter_mut() { for (grounding, mut flags, inputs) in player.iter_mut() {
let direction = inputs.move_dir; let direction = inputs.move_dir;
let deadzone = 0.2; let deadzone = 0.2;
@@ -64,7 +58,7 @@ fn set_animation_flags(
flags.any_direction = true; flags.any_direction = true;
} }
flags.shooting = trigger_state.is_active(); flags.shooting = inputs.trigger;
// `apply_controls` sets the jump flag when the player actually jumps. // `apply_controls` sets the jump flag when the player actually jumps.
// Unset the flag on hitting the ground // Unset the flag on hitting the ground
@@ -97,14 +91,13 @@ pub fn reset_upon_switch(
let flat_look_dir = inputs.look_dir.with_y(0.0).normalize(); let flat_look_dir = inputs.look_dir.with_y(0.0).normalize();
rig_transform.look_to(flat_look_dir, Dir3::Y); rig_transform.look_to(flat_look_dir, Dir3::Y);
match selected_controller.0 { match *selected_controller {
ControllerSet::ApplyControlsFly => { SelectedController::Flying => {
c.entity(controller).insert(FLYING_MOVEMENT_CONFIG); c.entity(controller).insert(FLYING_MOVEMENT_CONFIG);
} }
ControllerSet::ApplyControlsRun => { SelectedController::Running => {
c.entity(controller).insert(RUNNING_MOVEMENT_CONFIG); c.entity(controller).insert(RUNNING_MOVEMENT_CONFIG);
} }
_ => unreachable!(),
} }
} }
} }

View File

@@ -16,16 +16,19 @@ pub mod controller_common;
pub mod controller_flying; pub mod controller_flying;
pub mod controller_running; pub mod controller_running;
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone, Default)] #[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
pub enum ControllerSet { pub enum ControllerSet {
CollectInputs, CollectInputs,
ApplyControlsFly, ApplyControlsFly,
#[default]
ApplyControlsRun, ApplyControlsRun,
} }
#[derive(Resource, Debug, Default, PartialEq)] #[derive(Resource, Debug, Clone, Copy, PartialEq, Default)]
pub struct SelectedController(pub ControllerSet); pub enum SelectedController {
Flying,
#[default]
Running,
}
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.register_type::<ControllerSettings>() app.register_type::<ControllerSettings>()
@@ -48,12 +51,8 @@ pub fn plugin(app: &mut App) {
app.configure_sets( app.configure_sets(
FixedUpdate, FixedUpdate,
( (
ControllerSet::ApplyControlsFly.run_if(resource_equals(SelectedController( ControllerSet::ApplyControlsFly.run_if(resource_equals(SelectedController::Flying)),
ControllerSet::ApplyControlsFly, ControllerSet::ApplyControlsRun.run_if(resource_equals(SelectedController::Running)),
))),
ControllerSet::ApplyControlsRun.run_if(resource_equals(SelectedController(
ControllerSet::ApplyControlsRun,
))),
) )
.chain() .chain()
.run_if(in_state(GameState::Playing)), .run_if(in_state(GameState::Playing)),
@@ -104,7 +103,6 @@ impl MapEntities for Inputs {
pub struct ClientInputs(pub Inputs); pub struct ClientInputs(pub Inputs);
/// A cache to collect inputs into clientside, so that they don't get overwritten by replication from the server /// A cache to collect inputs into clientside, so that they don't get overwritten by replication from the server
#[cfg(feature = "client")]
#[derive(Component, Default, Reflect)] #[derive(Component, Default, Reflect)]
#[reflect(Component)] #[reflect(Component)]
pub struct LocalInputs(pub Inputs); pub struct LocalInputs(pub Inputs);
@@ -172,14 +170,14 @@ fn head_change(
for (entity, head) in query.iter() { for (entity, head) in query.iter() {
let stats = heads_db.head_stats(head.0); let stats = heads_db.head_stats(head.0);
let controller = match stats.controls { let controller = match stats.controls {
HeadControls::Plane => ControllerSet::ApplyControlsFly, HeadControls::Plane => SelectedController::Flying,
HeadControls::Walk => ControllerSet::ApplyControlsRun, HeadControls::Walk => SelectedController::Running,
}; };
if selected_controller.0 != controller { if *selected_controller != controller {
event_controller_switch.write(ControllerSwitchEvent { controller: entity }); event_controller_switch.write(ControllerSwitchEvent { controller: entity });
selected_controller.0 = controller; *selected_controller = controller;
} }
} }
} }

View File

@@ -5,7 +5,6 @@ use crate::{
character::HedzCharacter, character::HedzCharacter,
protocol::PlayerId, protocol::PlayerId,
}; };
#[cfg(feature = "client")]
use crate::{backpack::backpack_ui::BackpackUiState, control::LocalInputs}; use crate::{backpack::backpack_ui::BackpackUiState, control::LocalInputs};
use avian3d::prelude::*; use avian3d::prelude::*;
use bevy::{ use bevy::{
@@ -20,7 +19,6 @@ use serde::{Deserialize, Serialize};
#[require(HedzCharacter, DebugInput = DebugInput, PlayerTriggerState)] #[require(HedzCharacter, DebugInput = DebugInput, PlayerTriggerState)]
pub struct Player; pub struct Player;
#[cfg(feature = "client")]
#[derive(Component, Debug, Reflect)] #[derive(Component, Debug, Reflect)]
#[reflect(Component)] #[reflect(Component)]
#[require(LocalInputs, BackpackUiState)] #[require(LocalInputs, BackpackUiState)]

View File

@@ -26,8 +26,9 @@ pub enum NetworkedCollider {
half_extents: Vec3, half_extents: Vec3,
}, },
Capsule { Capsule {
a: Vec3,
b: Vec3,
radius: f32, radius: f32,
length: f32,
}, },
/// If a collider value wasn't set up to be replicated, it is replicated as unknown /// If a collider value wasn't set up to be replicated, it is replicated as unknown
/// and a warning is logged, and unwraps to `sphere(0.1)` on the other side. Likely /// and a warning is logged, and unwraps to `sphere(0.1)` on the other side. Likely
@@ -47,8 +48,9 @@ impl From<Collider> for NetworkedCollider {
} }
} else if let Some(value) = value.shape().as_capsule() { } else if let Some(value) = value.shape().as_capsule() {
NetworkedCollider::Capsule { NetworkedCollider::Capsule {
a: value.segment.a.into(),
b: value.segment.b.into(),
radius: value.radius, radius: value.radius,
length: value.height(),
} }
} else { } else {
warn!( warn!(
@@ -66,7 +68,9 @@ impl From<NetworkedCollider> for Collider {
NetworkedCollider::Cuboid { half_extents } => { NetworkedCollider::Cuboid { half_extents } => {
Collider::cuboid(half_extents.x, half_extents.y, half_extents.z) Collider::cuboid(half_extents.x, half_extents.y, half_extents.z)
} }
NetworkedCollider::Capsule { radius, length } => Collider::capsule(radius, length), NetworkedCollider::Capsule { a, b, radius } => {
Collider::capsule_endpoints(radius, a, b)
}
NetworkedCollider::Unknown => Collider::sphere(0.1), NetworkedCollider::Unknown => Collider::sphere(0.1),
} }
} }

View File

@@ -1,6 +1,8 @@
use crate::{ use crate::{
GameState, GameState,
abilities::{BuildExplosionSprite, curver::CurverProjectile, healing::Healing}, abilities::{
BuildExplosionSprite, curver::CurverProjectile, healing::Healing, thrown::ThrownProjectile,
},
animation::AnimationFlags, animation::AnimationFlags,
backpack::{Backpack, BackpackSwapEvent}, backpack::{Backpack, BackpackSwapEvent},
camera::{CameraArmRotation, CameraTarget}, camera::{CameraArmRotation, CameraTarget},
@@ -79,7 +81,7 @@ pub fn plugin(app: &mut App) {
app.replicate::<ChildOf>(); app.replicate::<ChildOf>();
app.sync_related_entities::<ChildOf>(); app.sync_related_entities::<ChildOf>();
app.replicate::<components::GltfSceneRoot>() app.replicate_once::<components::GltfSceneRoot>()
.replicate_once::<components::PlayerId>() .replicate_once::<components::PlayerId>()
.replicate::<components::TbMapEntityId>() .replicate::<components::TbMapEntityId>()
.replicate::<ActiveHead>() .replicate::<ActiveHead>()
@@ -88,7 +90,6 @@ pub fn plugin(app: &mut App) {
.replicate::<AnimatedCharacter>() .replicate::<AnimatedCharacter>()
.replicate::<AnimationFlags>() .replicate::<AnimationFlags>()
.replicate_once::<AutoRotation>() .replicate_once::<AutoRotation>()
.replicate_once::<CurverProjectile>()
.replicate::<Backpack>() .replicate::<Backpack>()
.replicate::<Billboard>() .replicate::<Billboard>()
.replicate_once::<CameraArmRotation>() .replicate_once::<CameraArmRotation>()
@@ -108,6 +109,9 @@ pub fn plugin(app: &mut App) {
.replicate::<UiActiveHeads>() .replicate::<UiActiveHeads>()
.replicate_as::<Visibility, SerVisibility>(); .replicate_as::<Visibility, SerVisibility>();
app.replicate_once::<ThrownProjectile>()
.replicate_once::<CurverProjectile>();
// Physics components // Physics components
app.replicate::<AngularInertia>() app.replicate::<AngularInertia>()
.replicate::<AngularVelocity>() .replicate::<AngularVelocity>()