Run trigger logic client side (#86)

This commit is contained in:
extrawurst
2025-12-12 04:36:29 +01:00
committed by GitHub
parent e044558a93
commit e7ebff2029
6 changed files with 113 additions and 124 deletions

View File

@@ -1,12 +1,11 @@
use super::TriggerArrow;
use crate::{
GameState, billboards::Billboard, global_observer, heads_database::HeadsDatabase,
hitpoints::Hit, loading_assets::GameAssets, physics_layers::GameLayer, protocol::PlaySound,
hitpoints::Hit, loading_assets::GameAssets, physics_layers::GameLayer,
utils::sprite_3d_animation::AnimationTimer,
};
use avian3d::prelude::*;
use bevy::{light::NotShadowCaster, prelude::*};
use bevy_replicon::prelude::{SendMode, ServerTriggerExt, ToClients};
use bevy_sprite3d::Sprite3d;
#[derive(Component)]
@@ -45,10 +44,8 @@ fn on_trigger_arrow(
) {
let state = trigger.0;
commands.server_trigger(ToClients {
mode: SendMode::Broadcast,
message: PlaySound::Crossbow,
});
#[cfg(feature = "client")]
commands.trigger(crate::protocol::PlaySound::Crossbow);
let rotation = if let Some(target) = state.target {
let t = query_transform

View File

@@ -1,12 +1,11 @@
use super::TriggerGun;
use crate::{
GameState, billboards::Billboard, global_observer, heads_database::HeadsDatabase,
hitpoints::Hit, loading_assets::GameAssets, physics_layers::GameLayer, protocol::PlaySound,
tb_entities::EnemySpawn, utils::sprite_3d_animation::AnimationTimer,
hitpoints::Hit, loading_assets::GameAssets, physics_layers::GameLayer, tb_entities::EnemySpawn,
utils::sprite_3d_animation::AnimationTimer,
};
use avian3d::prelude::*;
use bevy::{light::NotShadowCaster, prelude::*};
use bevy_replicon::prelude::{SendMode, ServerTriggerExt, ToClients};
use bevy_sprite3d::Sprite3d;
#[derive(Component)]
@@ -92,10 +91,8 @@ fn on_trigger_gun(
) {
let state = trigger.0;
commands.server_trigger(ToClients {
mode: SendMode::Broadcast,
message: PlaySound::Gun,
});
#[cfg(feature = "client")]
commands.trigger(crate::protocol::PlaySound::Gun);
let rotation = if let Some(t) = state
.target

View File

@@ -12,13 +12,12 @@ use crate::{
protocol::PlaySound,
utils::{billboards::Billboard, explosions::Explosion, sprite_3d_animation::AnimationTimer},
};
#[cfg(feature = "server")]
use crate::{
aim::AimTarget, character::CharacterHierarchy, control::Inputs, head::ActiveHead,
heads::ActiveHeads, heads_database::HeadsDatabase, player::Player,
aim::AimTarget, character::CharacterHierarchy, control::Inputs, heads::ActiveHeads,
heads_database::HeadsDatabase, player::Player,
};
use bevy::{light::NotShadowCaster, prelude::*};
use bevy_replicon::prelude::{ClientState, SendMode, ServerTriggerExt, ToClients};
use bevy_replicon::prelude::{SendMode, ServerTriggerExt, ToClients};
use bevy_sprite3d::Sprite3d;
pub use healing::Healing;
use serde::{Deserialize, Serialize};
@@ -82,14 +81,14 @@ pub struct TriggerMissile(pub TriggerData);
#[derive(Event, Reflect)]
pub struct TriggerCurver(pub TriggerData);
#[derive(Resource, Default)]
pub struct TriggerStateRes {
#[cfg(feature = "server")]
#[derive(Component, Default, Reflect)]
#[reflect(Component)]
pub struct PlayerTriggerState {
next_trigger_timestamp: f32,
active: bool,
}
impl TriggerStateRes {
impl PlayerTriggerState {
pub fn is_active(&self) -> bool {
self.active
}
@@ -115,8 +114,6 @@ pub enum ExplodingProjectileSet {
}
pub fn plugin(app: &mut App) {
app.init_resource::<TriggerStateRes>();
app.add_plugins(gun::plugin);
app.add_plugins(thrown::plugin);
app.add_plugins(arrow::plugin);
@@ -130,9 +127,7 @@ pub fn plugin(app: &mut App) {
.after(ExplodingProjectileSet::Mark)
.run_if(in_state(GameState::Playing)),
);
app.add_systems(OnEnter(GameState::Playing), setup);
#[cfg(feature = "server")]
app.add_systems(
FixedUpdate,
(on_trigger_state, update, update_heal_ability)
@@ -141,9 +136,7 @@ pub fn plugin(app: &mut App) {
);
app.add_systems(
FixedUpdate,
explode_projectiles
.run_if(in_state(ClientState::Disconnected))
.in_set(ExplodingProjectileSet::Explode),
explode_projectiles.in_set(ExplodingProjectileSet::Explode),
);
global_observer!(app, build_explosion_sprite);
@@ -183,114 +176,113 @@ fn explode_projectiles(mut commands: Commands, query: Query<(Entity, &ExplodingP
}
}
#[cfg(feature = "server")]
fn on_trigger_state(
mut res: ResMut<TriggerStateRes>,
players: Query<(&ActiveHead, &Inputs), With<Player>>,
) {
for (_, inputs) in players.iter() {
res.active = inputs.trigger;
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;
}
}
#[cfg(feature = "server")]
fn update(
mut res: ResMut<TriggerStateRes>,
mut commands: Commands,
player_query: Query<(Entity, &AimTarget, &Inputs), With<Player>>,
mut active_heads: Single<&mut ActiveHeads, With<Player>>,
mut query: Query<
(
Entity,
&mut ActiveHeads,
&mut PlayerTriggerState,
&AimTarget,
&Inputs,
),
With<Player>,
>,
query_transform: Query<&Transform>,
heads_db: Res<HeadsDatabase>,
time: Res<Time>,
character: CharacterHierarchy,
) {
if res.active && res.next_trigger_timestamp < time.elapsed_secs() {
let Some(state) = active_heads.current() else {
return;
};
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() {
let Some(state) = active_heads.current() else {
return;
};
if !state.has_ammo() {
commands.server_trigger(ToClients {
mode: SendMode::Broadcast,
message: PlaySound::Invalid,
});
return;
let target = if let Some(target) = target.0
&& query_transform.get(target).is_ok()
{
Some(target)
} else {
None
};
let Some(projectile_origin) = character
.projectile_origin(player)
.map(|origin| origin.translation())
else {
return;
};
let head = heads_db.head_stats(state.head);
if matches!(head.ability, HeadAbility::None | HeadAbility::Medic) {
return;
}
active_heads.use_ammo(time.elapsed_secs());
trigger_state.next_trigger_timestamp = time.elapsed_secs() + (1. / head.aps);
let trigger_state = TriggerData {
dir: Dir3::try_from(inputs.look_dir).unwrap_or(Dir3::NEG_Z),
pos: projectile_origin,
target,
target_layer: GameLayer::Npc,
head: state.head,
};
match head.ability {
HeadAbility::Thrown => commands.trigger(TriggerThrow(trigger_state)),
HeadAbility::Gun => commands.trigger(TriggerGun(trigger_state)),
HeadAbility::Missile => commands.trigger(TriggerMissile(trigger_state)),
HeadAbility::Arrow => commands.trigger(TriggerArrow(trigger_state)),
HeadAbility::Curver => commands.trigger(TriggerCurver(trigger_state)),
_ => panic!("Unhandled head ability"),
};
}
let Some((player, target, inputs)) = player_query.iter().next() else {
return;
};
let Some(projectile_origin) = character
.projectile_origin(player)
.map(|origin| origin.translation())
else {
return;
};
let head = heads_db.head_stats(state.head);
if matches!(head.ability, HeadAbility::None | HeadAbility::Medic) {
return;
}
active_heads.use_ammo(time.elapsed_secs());
res.next_trigger_timestamp = time.elapsed_secs() + (1. / head.aps);
let trigger_state = TriggerData {
dir: Dir3::try_from(inputs.look_dir).unwrap_or(Dir3::NEG_Z),
pos: projectile_origin,
target: target.0,
target_layer: GameLayer::Npc,
head: state.head,
};
match head.ability {
HeadAbility::Thrown => commands.trigger(TriggerThrow(trigger_state)),
HeadAbility::Gun => commands.trigger(TriggerGun(trigger_state)),
HeadAbility::Missile => commands.trigger(TriggerMissile(trigger_state)),
HeadAbility::Arrow => commands.trigger(TriggerArrow(trigger_state)),
HeadAbility::Curver => commands.trigger(TriggerCurver(trigger_state)),
_ => panic!("Unhandled head ability"),
};
}
}
#[cfg(feature = "server")]
fn update_heal_ability(
res: Res<TriggerStateRes>,
mut commands: Commands,
active_heads: Single<(Entity, &ActiveHeads), With<Player>>,
players: Query<(Entity, &ActiveHeads, Ref<PlayerTriggerState>), With<Player>>,
heads_db: Res<HeadsDatabase>,
) {
if res.is_changed() {
let Some(state) = active_heads.1.current() else {
return;
};
for (player, active_heads, trigger_state) in players.iter() {
if trigger_state.is_changed() {
let Some(state) = active_heads.current() else {
return;
};
let player = active_heads.0;
let head = heads_db.head_stats(state.head);
let head = heads_db.head_stats(state.head);
if !matches!(head.ability, HeadAbility::Medic) {
return;
}
if !matches!(head.ability, HeadAbility::Medic) {
return;
}
use crate::abilities::healing::HealingState;
if trigger_state.active {
use crate::abilities::healing::HealingStateChanged;
use crate::abilities::healing::HealingState;
if res.active {
use crate::abilities::healing::HealingStateChanged;
commands.trigger(HealingStateChanged {
state: HealingState::Started,
entity: player,
});
} else {
use crate::abilities::healing::HealingStateChanged;
commands.trigger(HealingStateChanged {
state: HealingState::Started,
entity: player,
});
} else {
use crate::abilities::healing::HealingStateChanged;
commands.trigger(HealingStateChanged {
state: HealingState::Stopped,
entity: player,
});
commands.trigger(HealingStateChanged {
state: HealingState::Stopped,
entity: player,
});
}
}
}
}

View File

@@ -9,7 +9,7 @@ use crate::{
use avian3d::prelude::*;
use bevy::prelude::*;
use bevy_ballistic::launch_velocity;
use bevy_replicon::prelude::{Replicated, SendMode, ServerTriggerExt, ToClients};
use bevy_replicon::prelude::Replicated;
use serde::{Deserialize, Serialize};
#[derive(Component, Serialize, Deserialize, PartialEq)]
@@ -35,10 +35,8 @@ fn on_trigger_thrown(
) {
let state = trigger.event().0;
commands.server_trigger(ToClients {
mode: SendMode::Broadcast,
message: PlaySound::Throw,
});
#[cfg(feature = "client")]
commands.trigger(PlaySound::Throw);
const SPEED: f32 = 35.;

View File

@@ -1,7 +1,7 @@
use super::{ControllerSet, ControllerSwitchEvent};
use crate::{
GameState,
abilities::TriggerStateRes,
abilities::PlayerTriggerState,
animation::AnimationFlags,
control::{ControllerSettings, Inputs, SelectedController},
physics_layers::GameLayer,
@@ -42,13 +42,17 @@ pub fn plugin(app: &mut App) {
}
fn set_animation_flags(
trigger: Res<TriggerStateRes>,
mut player: Query<
(&Grounding, &mut AnimationFlags, &Inputs),
(
&Grounding,
&mut AnimationFlags,
&Inputs,
&PlayerTriggerState,
),
(With<Player>, Without<ConfirmHistory>),
>,
) {
for (grounding, mut flags, inputs) in player.iter_mut() {
for (grounding, mut flags, inputs, trigger_state) in player.iter_mut() {
let direction = inputs.move_dir;
let deadzone = 0.2;
@@ -60,7 +64,7 @@ fn set_animation_flags(
flags.any_direction = true;
}
flags.shooting = trigger.is_active();
flags.shooting = trigger_state.is_active();
// `apply_controls` sets the jump flag when the player actually jumps.
// Unset the flag on hitting the ground

View File

@@ -1,5 +1,6 @@
use crate::{
GameState,
abilities::PlayerTriggerState,
cash::{Cash, CashCollectEvent},
character::HedzCharacter,
protocol::PlayerId,
@@ -16,7 +17,7 @@ use happy_feet::debug::DebugInput;
use serde::{Deserialize, Serialize};
#[derive(Component, Default, Serialize, Deserialize, PartialEq)]
#[require(HedzCharacter, DebugInput = DebugInput)]
#[require(HedzCharacter, DebugInput = DebugInput, PlayerTriggerState)]
pub struct Player;
#[cfg(feature = "client")]