8 Commits

Author SHA1 Message Date
extrawurst
0735c429ca fixes head switching (#96) 2025-12-21 12:43:13 -05:00
PROMETHIA-27
c3c5ae6dfb Target UI multiplayer (#95) 2025-12-21 12:01:50 -05:00
extrawurst
cc7e2aae70 fix lookdir not syncing (#94)
Co-authored-by: PROMETHIA-27 <electriccobras@gmail.com>
2025-12-20 20:25:36 -05:00
PROMETHIA-27
dbcd822b50 Camera update multiplayer (#93) 2025-12-20 19:47:45 -05:00
ac8c834f2f allow multiple players moving 2025-12-20 19:07:51 -05:00
f35275ab9f sitch to bevy_persistent to allow two clients
bevy_pkv was holding a file lock to prevent that
2025-12-20 13:21:30 -05:00
3901ee1174 fix clippy warning 2025-12-20 13:20:59 -05:00
extrawurst
7d280af821 add renet_steam support (#92) 2025-12-20 13:19:13 -05:00
22 changed files with 379 additions and 227 deletions

98
Cargo.lock generated
View File

@@ -512,6 +512,19 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "bevy-persistent"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e58d92d32bb99fa22ed46aeabe5212f5d1bc8952ebf9c49b5271fc06a1359f8"
dependencies = [
"bevy",
"gloo-storage",
"ron 0.11.0",
"serde",
"thiserror 2.0.17",
]
[[package]] [[package]]
name = "bevy-steamworks" name = "bevy-steamworks"
version = "0.15.0" version = "0.15.0"
@@ -1359,25 +1372,6 @@ dependencies = [
"uuid", "uuid",
] ]
[[package]]
name = "bevy_pkv"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "356a9c6fdc13faf7897103b43a8b84aafe24e1bbf1599df1fb00dc4e9b7055db"
dependencies = [
"bevy_app",
"bevy_ecs",
"cfg_aliases",
"directories",
"redb",
"rmp-serde",
"serde",
"serde_json",
"thiserror 2.0.17",
"wasm-bindgen",
"web-sys",
]
[[package]] [[package]]
name = "bevy_platform" name = "bevy_platform"
version = "0.17.3" version = "0.17.3"
@@ -2781,10 +2775,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11277822c27bde750de02c5dc5159b91e88bf2661a2c1d98106f2fb1c5c6f590" checksum = "11277822c27bde750de02c5dc5159b91e88bf2661a2c1d98106f2fb1c5c6f590"
[[package]] [[package]]
name = "directories" name = "dirs"
version = "6.0.0" version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
dependencies = [ dependencies = [
"dirs-sys", "dirs-sys",
] ]
@@ -3473,6 +3467,34 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "gloo-storage"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a"
dependencies = [
"gloo-utils",
"js-sys",
"serde",
"serde_json",
"thiserror 1.0.69",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-utils"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa"
dependencies = [
"js-sys",
"serde",
"serde_json",
"wasm-bindgen",
"web-sys",
]
[[package]] [[package]]
name = "glow" name = "glow"
version = "0.16.0" version = "0.16.0"
@@ -3681,18 +3703,19 @@ dependencies = [
"avian3d", "avian3d",
"bevy", "bevy",
"bevy-inspector-egui", "bevy-inspector-egui",
"bevy-persistent",
"bevy-steamworks", "bevy-steamworks",
"bevy_asset_loader", "bevy_asset_loader",
"bevy_ballistic", "bevy_ballistic",
"bevy_common_assets", "bevy_common_assets",
"bevy_debug_log", "bevy_debug_log",
"bevy_pkv",
"bevy_replicon", "bevy_replicon",
"bevy_replicon_renet", "bevy_replicon_renet",
"bevy_sprite3d", "bevy_sprite3d",
"bevy_trenchbroom", "bevy_trenchbroom",
"bevy_trenchbroom_avian", "bevy_trenchbroom_avian",
"clap", "clap",
"dirs",
"happy_feet", "happy_feet",
"nil 0.14.0", "nil 0.14.0",
"rand 0.8.5", "rand 0.8.5",
@@ -5459,15 +5482,6 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0d463f2884048e7153449a55166f91028d5b0ea53c79377099ce4e8cf0cf9bb" checksum = "a0d463f2884048e7153449a55166f91028d5b0ea53c79377099ce4e8cf0cf9bb"
[[package]]
name = "redb"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae323eb086579a3769daa2c753bb96deb95993c534711e0dbe881b5192906a06"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.4.1" version = "0.4.1"
@@ -5587,28 +5601,6 @@ dependencies = [
"log", "log",
] ]
[[package]]
name = "rmp"
version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4"
dependencies = [
"byteorder",
"num-traits",
"paste",
]
[[package]]
name = "rmp-serde"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db"
dependencies = [
"byteorder",
"rmp",
"serde",
]
[[package]] [[package]]
name = "robust" name = "robust"
version = "1.2.0" version = "1.2.0"

View File

@@ -51,15 +51,12 @@ bevy = { version = "0.17.0", default-features = false, features = [
"track_location", "track_location",
] } ] }
bevy-inspector-egui = "0.34" bevy-inspector-egui = "0.34"
bevy-persistent = { version = "0.9", features = ["ron"] }
bevy-steamworks = "0.15.0" bevy-steamworks = "0.15.0"
bevy_asset_loader = "=0.24.0-rc.1" 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_pkv = { version = "0.14", default-features = false, features = [
"bevy",
"redb",
] }
bevy_replicon = "0.37.1" bevy_replicon = "0.37.1"
# TODO: i dont think we need this in dedicated server mode # TODO: i dont think we need this in dedicated server mode
bevy_replicon_renet = { version = "0.13.0", features = ["renet_steam"] } bevy_replicon_renet = { version = "0.13.0", features = ["renet_steam"] }
@@ -69,6 +66,7 @@ bevy_trenchbroom = { version = "0.10", default-features = false, features = [
] } ] }
bevy_trenchbroom_avian = "0.10" bevy_trenchbroom_avian = "0.10"
clap = { version = "=4.5.47", features = ["derive"] } clap = { version = "=4.5.47", features = ["derive"] }
dirs = "6.0.0"
happy_feet = { git = "https://github.com/rustunit/happy_feet.git", rev = "919657fa", features = [ happy_feet = { git = "https://github.com/rustunit/happy_feet.git", rev = "919657fa", features = [
"serde", "serde",
] } ] }

View File

@@ -26,18 +26,19 @@ dbg = ["avian3d/debug-plugin", "bevy/debug", "dep:bevy-inspector-egui"]
avian3d = { workspace = true } avian3d = { workspace = true }
bevy = { workspace = true } bevy = { workspace = true }
bevy-inspector-egui = { workspace = true, optional = true } bevy-inspector-egui = { workspace = true, optional = true }
bevy-persistent = { workspace = true }
bevy-steamworks = { workspace = true } bevy-steamworks = { workspace = true }
bevy_asset_loader = { workspace = true } bevy_asset_loader = { workspace = true }
bevy_ballistic = { workspace = true } bevy_ballistic = { workspace = true }
bevy_common_assets = { workspace = true } bevy_common_assets = { workspace = true }
bevy_debug_log = { workspace = true } bevy_debug_log = { workspace = true }
bevy_pkv = { workspace = true }
bevy_replicon = { workspace = true } bevy_replicon = { workspace = true }
bevy_replicon_renet = { workspace = true } bevy_replicon_renet = { workspace = true }
bevy_sprite3d = { workspace = true } bevy_sprite3d = { workspace = true }
bevy_trenchbroom = { workspace = true } bevy_trenchbroom = { workspace = true }
bevy_trenchbroom_avian = { workspace = true } bevy_trenchbroom_avian = { workspace = true }
clap = { workspace = true } clap = { workspace = true }
dirs = { workspace = true }
happy_feet = { workspace = true } happy_feet = { workspace = true }
nil = { workspace = true } nil = { workspace = true }
rand = { workspace = true } rand = { workspace = true }

View File

@@ -1,13 +1,15 @@
mod marker;
mod target_ui;
use crate::{ use crate::{
GameState, control::Inputs, head::ActiveHead, heads_database::HeadsDatabase, GameState,
hitpoints::Hitpoints, physics_layers::GameLayer, player::Player, tb_entities::EnemySpawn, control::Inputs,
head::ActiveHead,
heads_database::HeadsDatabase,
hitpoints::Hitpoints,
physics_layers::GameLayer,
player::{LocalPlayer, Player},
tb_entities::EnemySpawn,
}; };
use avian3d::prelude::*; use avian3d::prelude::*;
use bevy::prelude::*; use bevy::prelude::*;
use marker::MarkerEvent;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::f32::consts::PI; use std::f32::consts::PI;
@@ -21,7 +23,12 @@ pub struct AimTarget(pub Option<Entity>);
pub struct AimState { pub struct AimState {
pub range: f32, pub range: f32,
pub max_angle: f32, pub max_angle: f32,
pub spawn_marker: bool, }
#[derive(Event)]
pub enum MarkerEvent {
Spawn(Entity),
Despawn,
} }
impl Default for AimState { impl Default for AimState {
@@ -29,7 +36,6 @@ impl Default for AimState {
Self { Self {
range: 80., range: 80.,
max_angle: PI / 8., max_angle: PI / 8.,
spawn_marker: true,
} }
} }
} }
@@ -38,20 +44,12 @@ pub fn plugin(app: &mut App) {
app.register_type::<AimState>(); app.register_type::<AimState>();
app.register_type::<AimTarget>(); app.register_type::<AimTarget>();
app.add_plugins(target_ui::plugin); app.register_required_components::<ActiveHead, AimState>();
app.add_plugins(marker::plugin);
app.add_systems( app.add_systems(
Update, Update,
(update_player_aim, update_npc_aim, head_change).run_if(in_state(GameState::Playing)), (update_player_aim, update_npc_aim, head_change).run_if(in_state(GameState::Playing)),
); );
app.add_systems(Update, add_aim);
}
fn add_aim(mut commands: Commands, query: Query<Entity, Added<ActiveHead>>) {
for e in query.iter() {
commands.entity(e).insert(AimState::default());
}
} }
fn head_change( fn head_change(
@@ -70,12 +68,19 @@ fn update_player_aim(
mut commands: Commands, mut commands: Commands,
potential_targets: Query<(Entity, &Transform), With<Hitpoints>>, potential_targets: Query<(Entity, &Transform), With<Hitpoints>>,
mut player_aim: Query< mut player_aim: Query<
(Entity, &AimState, &mut AimTarget, &GlobalTransform, &Inputs), (
Entity,
&AimState,
&mut AimTarget,
&GlobalTransform,
&Inputs,
Has<LocalPlayer>,
),
With<Player>, With<Player>,
>, >,
spatial_query: SpatialQuery, spatial_query: SpatialQuery,
) { ) {
for (player, state, mut aim_target, global_tf, inputs) in player_aim.iter_mut() { for (player, state, mut aim_target, global_tf, inputs, is_local) in player_aim.iter_mut() {
let (player_pos, player_forward) = (global_tf.translation(), inputs.look_dir); let (player_pos, player_forward) = (global_tf.translation(), inputs.look_dir);
let mut new_target = None; let mut new_target = None;
@@ -114,13 +119,14 @@ fn update_player_aim(
} }
if new_target != aim_target.0 { if new_target != aim_target.0 {
if state.spawn_marker { if is_local {
if let Some(target) = new_target { if let Some(target) = new_target {
commands.trigger(MarkerEvent::Spawn(target)); commands.trigger(MarkerEvent::Spawn(target));
} else { } else {
commands.trigger(MarkerEvent::Despawn); commands.trigger(MarkerEvent::Despawn);
} }
} }
aim_target.0 = new_target; aim_target.0 = new_target;
} }
} }

View File

@@ -1,6 +1,7 @@
use crate::GameState; use crate::GameState;
#[cfg(feature = "client")] #[cfg(feature = "client")]
use crate::control::Inputs; use crate::control::Inputs;
use crate::control::ViewMode;
#[cfg(feature = "client")] #[cfg(feature = "client")]
use crate::physics_layers::GameLayer; use crate::physics_layers::GameLayer;
#[cfg(feature = "client")] #[cfg(feature = "client")]
@@ -31,7 +32,7 @@ pub struct CameraRotationInput(pub Vec2);
#[reflect(Resource)] #[reflect(Resource)]
pub struct CameraState { pub struct CameraState {
pub cutscene: bool, pub cutscene: bool,
pub look_around: bool, pub view_mode: ViewMode,
} }
#[derive(Component, Reflect, Debug, Default)] #[derive(Component, Reflect, Debug, Default)]
@@ -85,10 +86,10 @@ fn update_look_around(
inputs: Single<&Inputs, With<LocalPlayer>>, inputs: Single<&Inputs, With<LocalPlayer>>,
mut cam_state: ResMut<CameraState>, mut cam_state: ResMut<CameraState>,
) { ) {
let look_around = inputs.view_mode; let view_mode = inputs.view_mode;
if look_around != cam_state.look_around { if view_mode != cam_state.view_mode {
cam_state.look_around = look_around; cam_state.view_mode = view_mode;
} }
} }
@@ -100,9 +101,9 @@ fn update_ui(
query: Query<Entity, With<CameraUi>>, query: Query<Entity, With<CameraUi>>,
) { ) {
if cam_state.is_changed() { if cam_state.is_changed() {
let show_ui = cam_state.look_around || cam_state.cutscene; let show_free_cam_ui = cam_state.view_mode.is_free() || cam_state.cutscene;
if show_ui { if show_free_cam_ui {
commands.spawn(( commands.spawn((
CameraUi, CameraUi,
Node { Node {
@@ -131,12 +132,19 @@ fn update_ui(
#[cfg(feature = "client")] #[cfg(feature = "client")]
fn update( fn update(
mut cam: Query< cam: Single<
(&MainCamera, &mut Transform, &CameraRotationInput), (&MainCamera, &mut Transform, &CameraRotationInput),
(Without<CameraTarget>, Without<CameraArmRotation>), (Without<CameraTarget>, Without<CameraArmRotation>),
>, >,
target_q: Single<&Transform, (With<CameraTarget>, Without<CameraArmRotation>)>, target_q: Single<
arm_rotation: Single<&Transform, With<CameraArmRotation>>, (&Transform, &Children),
(
With<CameraTarget>,
With<LocalPlayer>,
Without<CameraArmRotation>,
),
>,
arm_rotation: Query<&Transform, With<CameraArmRotation>>,
spatial_query: SpatialQuery, spatial_query: SpatialQuery,
cam_state: Res<CameraState>, cam_state: Res<CameraState>,
) { ) {
@@ -144,11 +152,14 @@ fn update(
return; return;
} }
let arm_tf = arm_rotation; let (camera, mut cam_transform, cam_rotation_input) = cam.into_inner();
let Ok((camera, mut cam_transform, cam_rotation_input)) = cam.single_mut() else { let (target_q, children) = target_q.into_inner();
return;
}; let arm_tf = children
.iter()
.find_map(|child| arm_rotation.get(child).ok())
.unwrap();
if !camera.enabled { if !camera.enabled {
return; return;
@@ -184,7 +195,7 @@ fn rotate_view(
look_dir: Res<LookDirMovement>, look_dir: Res<LookDirMovement>,
mut cam: Single<&mut CameraRotationInput>, mut cam: Single<&mut CameraRotationInput>,
) { ) {
if !inputs.view_mode { if !inputs.view_mode.is_free() {
cam.x = 0.0; cam.x = 0.0;
return; return;
} }

View File

@@ -0,0 +1,8 @@
use bevy::prelude::*;
pub mod marker;
pub mod target_ui;
pub fn plugin(app: &mut App) {
app.add_plugins((marker::plugin, target_ui::plugin));
}

View File

@@ -1,4 +1,7 @@
use crate::{GameState, global_observer, loading_assets::UIAssets, utils::billboards::Billboard}; use crate::{
GameState, aim::MarkerEvent, global_observer, loading_assets::UIAssets,
utils::billboards::Billboard,
};
use bevy::prelude::*; use bevy::prelude::*;
use bevy_sprite3d::Sprite3d; use bevy_sprite3d::Sprite3d;
use ops::sin; use ops::sin;
@@ -7,12 +10,6 @@ use ops::sin;
#[reflect(Component)] #[reflect(Component)]
struct TargetMarker; struct TargetMarker;
#[derive(Event)]
pub enum MarkerEvent {
Spawn(Entity),
Despawn,
}
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.add_systems(Update, move_marker.run_if(in_state(GameState::Playing))); app.add_systems(Update, move_marker.run_if(in_state(GameState::Playing)));
global_observer!(app, marker_event); global_observer!(app, marker_event);

View File

@@ -1,12 +1,12 @@
use super::AimTarget;
use crate::{ use crate::{
GameState, GameState,
aim::AimTarget,
backpack::UiHeadState, backpack::UiHeadState,
heads::{ActiveHeads, HeadsImages}, heads::{ActiveHeads, HeadsImages},
hitpoints::Hitpoints, hitpoints::Hitpoints,
loading_assets::UIAssets, loading_assets::UIAssets,
npc::Npc, npc::Npc,
player::Player, player::LocalPlayer,
}; };
use bevy::prelude::*; use bevy::prelude::*;
@@ -18,12 +18,14 @@ struct HeadImage;
#[reflect(Component)] #[reflect(Component)]
struct HeadDamage; struct HeadDamage;
#[derive(Resource, Default, PartialEq)] #[derive(Component, Default, PartialEq)]
struct TargetUi { pub struct TargetUi {
head: Option<UiHeadState>, head: Option<UiHeadState>,
} }
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.register_required_components::<LocalPlayer, TargetUi>();
app.add_systems(OnEnter(GameState::Playing), setup); app.add_systems(OnEnter(GameState::Playing), setup);
app.add_systems(Update, (sync, update).run_if(in_state(GameState::Playing))); app.add_systems(Update, (sync, update).run_if(in_state(GameState::Playing)));
} }
@@ -44,8 +46,6 @@ fn setup(mut commands: Commands, assets: Res<UIAssets>) {
assets.head_damage.clone(), assets.head_damage.clone(),
)], )],
)); ));
commands.insert_resource(TargetUi::default());
} }
fn spawn_head_ui(bg: Handle<Image>, regular: Handle<Image>, damage: Handle<Image>) -> impl Bundle { fn spawn_head_ui(bg: Handle<Image>, regular: Handle<Image>, damage: Handle<Image>) -> impl Bundle {
@@ -110,7 +110,7 @@ fn spawn_head_ui(bg: Handle<Image>, regular: Handle<Image>, damage: Handle<Image
} }
fn update( fn update(
target: Res<TargetUi>, target: Single<&TargetUi, (Changed<TargetUi>, With<LocalPlayer>)>,
heads_images: Res<HeadsImages>, heads_images: Res<HeadsImages>,
mut head_image: Query< mut head_image: Query<
(&mut Visibility, &mut ImageNode), (&mut Visibility, &mut ImageNode),
@@ -118,30 +118,28 @@ fn update(
>, >,
mut head_damage: Query<&mut Node, (With<HeadDamage>, Without<HeadImage>)>, mut head_damage: Query<&mut Node, (With<HeadDamage>, Without<HeadImage>)>,
) { ) {
if target.is_changed() { if let Ok((mut vis, mut image)) = head_image.single_mut() {
if let Ok((mut vis, mut image)) = head_image.single_mut() { if let Some(head) = target.head {
if let Some(head) = target.head { *vis = Visibility::Visible;
*vis = Visibility::Visible; image.image = heads_images.heads[head.head].clone();
image.image = heads_images.heads[head.head].clone(); } else {
} else { *vis = Visibility::Hidden;
*vis = Visibility::Hidden;
}
} }
}
if let Ok(mut node) = head_damage.single_mut() { if let Ok(mut node) = head_damage.single_mut() {
node.height = Val::Percent(target.head.map(|head| head.damage()).unwrap_or(0.) * 100.); node.height = Val::Percent(target.head.map(|head| head.damage()).unwrap_or(0.) * 100.);
}
} }
} }
fn sync( fn sync(
mut target: ResMut<TargetUi>, mut target: Single<&mut TargetUi, With<LocalPlayer>>,
player_target: Query<&AimTarget, With<Player>>, player_target: Single<&AimTarget, With<LocalPlayer>>,
target_data: Query<(&Hitpoints, &ActiveHeads), With<Npc>>, target_data: Query<(&Hitpoints, &ActiveHeads), With<Npc>>,
) { ) {
let mut new_state = None; let mut new_state = None;
if let Some(e) = player_target.iter().next().and_then(|target| target.0) if let Some(target) = player_target.0
&& let Ok((hp, heads)) = target_data.get(e) && let Ok((hp, heads)) = target_data.get(target)
{ {
let head = heads.current().expect("target must have a head on"); let head = heads.current().expect("target must have a head on");
new_state = Some(UiHeadState { new_state = Some(UiHeadState {

View File

@@ -1,6 +1,6 @@
use crate::{ use crate::{
GameState, GameState,
control::{ControllerSet, Inputs, LookDirMovement}, control::{ControllerSet, Inputs, LookDirMovement, SelectedController},
player::{LocalPlayer, PlayerBodyMesh}, player::{LocalPlayer, PlayerBodyMesh},
}; };
use bevy::prelude::*; use bevy::prelude::*;
@@ -19,14 +19,20 @@ pub fn plugin(app: &mut App) {
fn rotate_rig( fn rotate_rig(
inputs: Single<&Inputs, With<LocalPlayer>>, inputs: Single<&Inputs, With<LocalPlayer>>,
look_dir: Res<LookDirMovement>, look_dir: Res<LookDirMovement>,
local_player: Single<&Children, With<LocalPlayer>>, local_player: Single<(&Children, &SelectedController), With<LocalPlayer>>,
mut player_mesh: Query<&mut Transform, With<PlayerBodyMesh>>, mut player_mesh: Query<&mut Transform, With<PlayerBodyMesh>>,
) { ) {
if inputs.view_mode { if inputs.view_mode.is_free() {
return; return;
} }
local_player.iter().find(|&child| { let (local_player_childer, selected_controller) = *local_player;
if !matches!(selected_controller, SelectedController::Flying) {
return;
}
local_player_childer.iter().find(|&child| {
if let Ok(mut rig_transform) = player_mesh.get_mut(child) { if let Ok(mut rig_transform) = player_mesh.get_mut(child) {
let look_dir = look_dir.0; let look_dir = look_dir.0;

View File

@@ -3,7 +3,7 @@ use crate::{
client::control::CharacterInputEnabled, client::control::CharacterInputEnabled,
control::{ control::{
BackpackButtonPress, CashHealPressed, ClientInputs, ControllerSet, Inputs, LocalInputs, BackpackButtonPress, CashHealPressed, ClientInputs, ControllerSet, Inputs, LocalInputs,
LookDirMovement, SelectLeftPressed, SelectRightPressed, LookDirMovement, SelectLeftPressed, SelectRightPressed, ViewMode,
}, },
player::{LocalPlayer, PlayerBodyMesh}, player::{LocalPlayer, PlayerBodyMesh},
}; };
@@ -85,9 +85,14 @@ fn reset_control_state_on_disable(
fn get_lookdir( fn get_lookdir(
mut inputs: Single<&mut LocalInputs>, mut inputs: Single<&mut LocalInputs>,
rig_transform: Option<Single<&GlobalTransform, With<PlayerBodyMesh>>>, player: Single<&Children, With<LocalPlayer>>,
rig_transform: Query<&GlobalTransform, With<PlayerBodyMesh>>,
) { ) {
inputs.0.look_dir = if let Some(ref rig_transform) = rig_transform { let rig_transform = player
.iter()
.find_map(|child| rig_transform.get(child).ok());
inputs.0.look_dir = if let Some(rig_transform) = rig_transform {
rig_transform.forward().as_vec3() rig_transform.forward().as_vec3()
} else { } else {
Vec3::NEG_Z Vec3::NEG_Z
@@ -141,8 +146,11 @@ fn gamepad_controls(
inputs.0.move_dir += move_dir.clamp_length_max(1.0); inputs.0.move_dir += move_dir.clamp_length_max(1.0);
inputs.0.jump |= gamepad.pressed(GamepadButton::South); inputs.0.jump |= gamepad.pressed(GamepadButton::South);
inputs.0.view_mode |= gamepad.pressed(GamepadButton::LeftTrigger2);
inputs.0.trigger |= gamepad.pressed(GamepadButton::RightTrigger2); inputs.0.trigger |= gamepad.pressed(GamepadButton::RightTrigger2);
inputs
.0
.view_mode
.merge_input(gamepad.pressed(GamepadButton::LeftTrigger2));
if gamepad.just_pressed(GamepadButton::DPadUp) { if gamepad.just_pressed(GamepadButton::DPadUp) {
backpack_inputs.write(BackpackButtonPress::Toggle); backpack_inputs.write(BackpackButtonPress::Toggle);
@@ -207,8 +215,8 @@ fn keyboard_controls(
inputs.0.move_dir = direction; inputs.0.move_dir = direction;
inputs.0.jump = keyboard.pressed(KeyCode::Space); inputs.0.jump = keyboard.pressed(KeyCode::Space);
inputs.0.view_mode = keyboard.pressed(KeyCode::Tab);
inputs.0.trigger = mouse.pressed(MouseButton::Left); inputs.0.trigger = mouse.pressed(MouseButton::Left);
inputs.0.view_mode = ViewMode::from_input(keyboard.pressed(KeyCode::Tab));
if keyboard.just_pressed(KeyCode::KeyB) { if keyboard.just_pressed(KeyCode::KeyB) {
backpack_inputs.write(BackpackButtonPress::Toggle); backpack_inputs.write(BackpackButtonPress::Toggle);

View File

@@ -22,6 +22,7 @@ use bevy_replicon_renet::{
use bevy_steamworks::Client; use bevy_steamworks::Client;
use bevy_trenchbroom::geometry::Brushes; use bevy_trenchbroom::geometry::Brushes;
pub mod aim;
pub mod audio; pub mod audio;
pub mod backpack; pub mod backpack;
pub mod control; pub mod control;
@@ -36,6 +37,8 @@ pub mod ui;
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.add_plugins(( app.add_plugins((
aim::plugin,
audio::plugin,
backpack::plugin, backpack::plugin,
control::plugin, control::plugin,
debug::plugin, debug::plugin,
@@ -43,7 +46,6 @@ pub fn plugin(app: &mut App) {
heal_effect::plugin, heal_effect::plugin,
player::plugin, player::plugin,
setup::plugin, setup::plugin,
audio::plugin,
steam::plugin, steam::plugin,
ui::plugin, ui::plugin,
settings::plugin, settings::plugin,
@@ -125,7 +127,7 @@ fn connect_to_server(
let authentication = bevy_replicon_renet::netcode::ClientAuthentication::Unsecure { let authentication = bevy_replicon_renet::netcode::ClientAuthentication::Unsecure {
client_id, client_id,
protocol_id: 0, protocol_id: 0,
server_addr: host_addr.clone(), server_addr: *host_addr,
user_data: None, user_data: None,
}; };
let transport = bevy_replicon_renet::netcode::NetcodeClientTransport::new( let transport = bevy_replicon_renet::netcode::NetcodeClientTransport::new(

View File

@@ -2,7 +2,7 @@ use crate::{
global_observer, global_observer,
heads_database::{HeadControls, HeadsDatabase}, heads_database::{HeadControls, HeadsDatabase},
loading_assets::AudioAssets, loading_assets::AudioAssets,
player::{LocalPlayer, PlayerBodyMesh}, player::{LocalPlayer, Player, PlayerBodyMesh},
protocol::{ClientHeadChanged, PlaySound, PlayerId, messages::AssignClientPlayer}, protocol::{ClientHeadChanged, PlaySound, PlayerId, messages::AssignClientPlayer},
}; };
use bevy::prelude::*; use bevy::prelude::*;
@@ -59,23 +59,38 @@ pub enum PlayerAssignmentState {
Confirmed, Confirmed,
} }
// TODO: currently a networked message.
// can be done by just using local change detection on `ActiveHead`?
fn on_client_update_head_mesh( fn on_client_update_head_mesh(
trigger: On<ClientHeadChanged>, trigger: On<ClientHeadChanged>,
mut commands: Commands, mut commands: Commands,
body_mesh: Single<(Entity, &Children), With<PlayerBodyMesh>>, player: Query<(&Children, &PlayerId), With<Player>>,
body_mesh: Query<(Entity, &Children), With<PlayerBodyMesh>>,
head_db: Res<HeadsDatabase>, head_db: Res<HeadsDatabase>,
audio_assets: Res<AudioAssets>, audio_assets: Res<AudioAssets>,
sfx: Query<&AudioPlayer>, sfx: Query<&AudioPlayer>,
) -> Result { ) -> Result {
let head = trigger.0 as usize; let (player_children, _) = player
let (body_mesh, mesh_children) = *body_mesh; .iter()
.find(|(_, player_id)| **player_id == trigger.player)
.unwrap();
let (body_mesh, body_mesh_children) = player_children
.iter()
.find_map(|child| body_mesh.get(child).ok())
.unwrap();
let head = trigger.head;
let head_str = head_db.head_key(head); let head_str = head_db.head_key(head);
commands.trigger(PlaySound::Head(head_str.to_string())); commands.trigger(PlaySound::Head(head_str.to_string()));
//TODO: make part of full character mesh later //TODO: make part of full character mesh later
for child in mesh_children.iter().filter(|child| sfx.contains(*child)) { for child in body_mesh_children
.iter()
.filter(|child| sfx.contains(*child))
{
commands.entity(child).despawn(); commands.entity(child).despawn();
} }
if head_db.head_stats(head).controls == HeadControls::Plane { if head_db.head_stats(head).controls == HeadControls::Plane {

View File

@@ -1,19 +1,30 @@
use bevy::prelude::*;
use bevy_pkv::prelude::*;
use crate::{client::audio::SoundSettings, utils::Debounce}; use crate::{client::audio::SoundSettings, utils::Debounce};
use bevy::prelude::*;
use bevy_persistent::{Persistent, StorageFormat};
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
#[cfg(not(feature = "dbg"))] app.insert_resource(
app.insert_resource(PkvStore::new("Rustunit", "HEDZ")); Persistent::<SoundSettings>::builder()
.name("audio")
.format(StorageFormat::Ron)
.path(
dirs::config_dir()
.unwrap()
.join("com.rustunit.hedzreloaded")
.join("audio.ron"),
)
.default(SoundSettings::default())
.build()
.unwrap(),
);
app.add_systems(Update, persist_settings.run_if(resource_exists::<PkvStore>)); app.add_systems(Update, persist_settings);
app.add_systems(Startup, load_settings.run_if(resource_exists::<PkvStore>)); app.add_systems(Startup, load_settings);
} }
fn persist_settings( fn persist_settings(
settings: Res<SoundSettings>, settings: Res<SoundSettings>,
mut pkv: ResMut<PkvStore>, mut persistent: ResMut<Persistent<SoundSettings>>,
mut debounce: Debounce<1000>, mut debounce: Debounce<1000>,
) -> Result { ) -> Result {
if settings.is_changed() { if settings.is_changed() {
@@ -21,16 +32,12 @@ fn persist_settings(
} }
if debounce.finished() { if debounce.finished() {
pkv.set("audio", &*settings)?; persistent.set(*settings)?;
} }
Ok(()) Ok(())
} }
fn load_settings(mut settings: ResMut<SoundSettings>, pkv: Res<PkvStore>) -> Result { fn load_settings(persistent: Res<Persistent<SoundSettings>>, mut settings: ResMut<SoundSettings>) {
if let Ok(loaded) = pkv.get::<SoundSettings>("audio") { *settings = *persistent.get();
*settings = loaded;
}
Ok(())
} }

View File

@@ -1,7 +1,6 @@
use std::net::SocketAddr;
use bevy::prelude::*; use bevy::prelude::*;
use clap::Parser; use clap::Parser;
use std::net::SocketAddr;
use steamworks::SteamId; use steamworks::SteamId;
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
@@ -81,8 +80,7 @@ impl From<NetworkingConfig> for NetConfig {
} }
fn parse_addr(addr: Option<String>) -> SocketAddr { fn parse_addr(addr: Option<String>) -> SocketAddr {
addr.map(|addr| addr.parse().ok()) addr.and_then(|addr| addr.parse().ok())
.flatten()
.unwrap_or_else(|| "127.0.0.1:31111".parse().unwrap()) .unwrap_or_else(|| "127.0.0.1:31111".parse().unwrap())
} }

View File

@@ -72,12 +72,22 @@ fn set_animation_flags(
pub fn reset_upon_switch( pub fn reset_upon_switch(
mut c: Commands, mut c: Commands,
mut event_controller_switch: MessageReader<ControllerSwitchEvent>, mut event_controller_switch: MessageReader<ControllerSwitchEvent>,
selected_controller: Res<SelectedController>,
mut rig_transforms: Query<&mut Transform, With<PlayerBodyMesh>>, mut rig_transforms: Query<&mut Transform, With<PlayerBodyMesh>>,
mut controllers: Query<(&mut KinematicVelocity, &Children, &Inputs), With<Player>>, mut controllers: Query<
(
&mut KinematicVelocity,
&Children,
&Inputs,
&SelectedController,
),
With<Player>,
>,
) { ) {
for &ControllerSwitchEvent { controller } in event_controller_switch.read() { for &ControllerSwitchEvent { controller } in event_controller_switch.read() {
let (mut velocity, children, inputs) = controllers.get_mut(controller).unwrap(); let (mut velocity, children, inputs, selected_controller) =
controllers.get_mut(controller).unwrap();
info!("resetting controller");
velocity.0 = Vec3::ZERO; velocity.0 = Vec3::ZERO;
@@ -165,6 +175,7 @@ impl Default for MovementSpeedFactor {
MoveInput, MoveInput,
MovementSpeedFactor, MovementSpeedFactor,
TransformInterpolation, TransformInterpolation,
SelectedController::Running,
CharacterMovement = RUNNING_MOVEMENT_CONFIG.movement, CharacterMovement = RUNNING_MOVEMENT_CONFIG.movement,
ControllerSettings = RUNNING_MOVEMENT_CONFIG.settings, ControllerSettings = RUNNING_MOVEMENT_CONFIG.settings,
CharacterGravity = RUNNING_MOVEMENT_CONFIG.gravity, CharacterGravity = RUNNING_MOVEMENT_CONFIG.gravity,

View File

@@ -1,7 +1,7 @@
use super::ControllerSet; use super::ControllerSet;
use crate::{ use crate::{
GameState, GameState,
control::{Inputs, controller_common::MovementSpeedFactor}, control::{Inputs, SelectedController, controller_common::MovementSpeedFactor},
}; };
use bevy::prelude::*; use bevy::prelude::*;
use happy_feet::prelude::MoveInput; use happy_feet::prelude::MoveInput;
@@ -19,8 +19,17 @@ impl Plugin for CharacterControllerPlugin {
} }
} }
pub fn apply_controls(character: Single<(&mut MoveInput, &MovementSpeedFactor, &Inputs)>) { pub fn apply_controls(
let (mut char_input, factor, inputs) = character.into_inner(); mut query: Query<(
&mut MoveInput,
char_input.set(inputs.look_dir * factor.0); &MovementSpeedFactor,
&Inputs,
&SelectedController,
)>,
) {
for (mut move_input, factor, inputs, selected_controller) in query.iter_mut() {
if *selected_controller == SelectedController::Flying {
move_input.set(inputs.look_dir * factor.0);
}
}
} }

View File

@@ -1,7 +1,10 @@
use crate::{ use crate::{
GameState, GameState,
animation::AnimationFlags, animation::AnimationFlags,
control::{ControllerSet, ControllerSettings, Inputs, controller_common::MovementSpeedFactor}, control::{
ControllerSet, ControllerSettings, Inputs, SelectedController,
controller_common::MovementSpeedFactor,
},
protocol::is_server, protocol::is_server,
}; };
#[cfg(feature = "client")] #[cfg(feature = "client")]
@@ -41,7 +44,7 @@ fn rotate_view(
) { ) {
let (inputs, children) = controller.into_inner(); let (inputs, children) = controller.into_inner();
if inputs.view_mode { if inputs.view_mode.is_free() {
return; return;
} }
@@ -56,7 +59,7 @@ fn rotate_view(
} }
fn apply_controls( fn apply_controls(
character: Single<( mut query: Query<(
&mut MoveInput, &mut MoveInput,
&mut Grounding, &mut Grounding,
&mut KinematicVelocity, &mut KinematicVelocity,
@@ -64,25 +67,39 @@ fn apply_controls(
&ControllerSettings, &ControllerSettings,
&MovementSpeedFactor, &MovementSpeedFactor,
&Inputs, &Inputs,
&SelectedController,
)>, )>,
) { ) {
let (mut move_input, mut grounding, mut velocity, mut flags, settings, move_factor, inputs) = for (
character.into_inner(); mut move_input,
mut grounding,
mut velocity,
mut flags,
settings,
move_factor,
inputs,
selected_controller,
) in query.iter_mut()
{
if *selected_controller != SelectedController::Running {
continue;
}
let ground_normal = *grounding.normal().unwrap_or(Dir3::Y); let ground_normal = *grounding.normal().unwrap_or(Dir3::Y);
let mut direction = inputs.move_dir.extend(0.0).xzy(); let mut direction = inputs.move_dir.extend(0.0).xzy();
let look_dir_right = inputs.look_dir.cross(Vec3::Y); let look_dir_right = inputs.look_dir.cross(Vec3::Y);
direction = (inputs.look_dir * direction.z) + (look_dir_right * direction.x); direction = (inputs.look_dir * direction.z) + (look_dir_right * direction.x);
let y_projection = direction.project_onto(ground_normal); let y_projection = direction.project_onto(ground_normal);
direction -= y_projection; direction -= y_projection;
direction = direction.normalize_or_zero(); direction = direction.normalize_or_zero();
move_input.set(direction * move_factor.0); move_input.set(direction * move_factor.0);
if inputs.jump && grounding.is_grounded() { if inputs.jump && grounding.is_grounded() {
flags.jumping = true; flags.jumping = true;
flags.jump_count += 1; flags.jump_count += 1;
happy_feet::movement::jump(settings.jump_force, &mut velocity, &mut grounding, Dir3::Y) happy_feet::movement::jump(settings.jump_force, &mut velocity, &mut grounding, Dir3::Y)
}
} }
} }

View File

@@ -20,7 +20,8 @@ pub enum ControllerSet {
ApplyControlsRun, ApplyControlsRun,
} }
#[derive(Resource, Debug, Clone, Copy, PartialEq, Default)] #[derive(Component, Reflect, Debug, Clone, Copy, PartialEq, Eq, Default)]
#[reflect(Component)]
pub enum SelectedController { pub enum SelectedController {
Flying, Flying,
#[default] #[default]
@@ -35,8 +36,9 @@ pub fn plugin(app: &mut App) {
#[cfg(feature = "client")] #[cfg(feature = "client")]
app.register_type::<LocalInputs>(); app.register_type::<LocalInputs>();
app.register_type::<SelectedController>();
app.init_resource::<LookDirMovement>(); app.init_resource::<LookDirMovement>();
app.init_resource::<SelectedController>();
app.add_message::<ControllerSwitchEvent>() app.add_message::<ControllerSwitchEvent>()
.add_message::<BackpackButtonPress>(); .add_message::<BackpackButtonPress>();
@@ -48,8 +50,8 @@ pub fn plugin(app: &mut App) {
app.configure_sets( app.configure_sets(
FixedUpdate, FixedUpdate,
( (
ControllerSet::ApplyControlsFly.run_if(resource_equals(SelectedController::Flying)), ControllerSet::ApplyControlsFly,
ControllerSet::ApplyControlsRun.run_if(resource_equals(SelectedController::Running)), ControllerSet::ApplyControlsRun,
) )
.chain() .chain()
.run_if(in_state(GameState::Playing)), .run_if(in_state(GameState::Playing)),
@@ -64,6 +66,35 @@ pub fn plugin(app: &mut App) {
app.add_systems(Update, head_change.run_if(in_state(GameState::Playing))); app.add_systems(Update, head_change.run_if(in_state(GameState::Playing)));
} }
#[derive(Reflect, Default, Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
pub enum ViewMode {
#[default]
Default,
FreeMode,
}
impl ViewMode {
pub fn from_input(button: bool) -> Self {
if button {
Self::FreeMode
} else {
Self::Default
}
}
pub fn merge_input(&mut self, button: bool) {
let new = Self::from_input(button);
*self = match (*self, new) {
(Self::FreeMode, _) | (_, Self::FreeMode) => Self::FreeMode,
_ => Self::Default,
};
}
pub fn is_free(&self) -> bool {
matches!(self, Self::FreeMode)
}
}
/// The continuous inputs of a client for a tick. The instant inputs are sent via messages like `BackpackTogglePressed`. /// The continuous inputs of a client for a tick. The instant inputs are sent via messages like `BackpackTogglePressed`.
#[derive(Component, Clone, Copy, Debug, Serialize, Deserialize, Reflect)] #[derive(Component, Clone, Copy, Debug, Serialize, Deserialize, Reflect)]
#[reflect(Component, Default)] #[reflect(Component, Default)]
@@ -75,7 +106,7 @@ pub struct Inputs {
pub look_dir: Vec3, pub look_dir: Vec3,
pub jump: bool, pub jump: bool,
/// Determines if the camera can rotate freely around the player /// Determines if the camera can rotate freely around the player
pub view_mode: bool, pub view_mode: ViewMode,
pub trigger: bool, pub trigger: bool,
} }
@@ -154,23 +185,24 @@ fn collect_player_inputs(
} }
fn head_change( fn head_change(
//TODO: needs a 'LocalPlayer' at some point for multiplayer mut commands: Commands,
query: Query<(Entity, &ActiveHead), (Changed<ActiveHead>, With<Player>)>, query: Query<(Entity, &ActiveHead, &SelectedController), (Changed<ActiveHead>, With<Player>)>,
heads_db: Res<HeadsDatabase>, heads_db: Res<HeadsDatabase>,
mut selected_controller: ResMut<SelectedController>,
mut event_controller_switch: MessageWriter<ControllerSwitchEvent>, mut event_controller_switch: MessageWriter<ControllerSwitchEvent>,
) { ) {
for (entity, head) in query.iter() { for (entity, head, selected_controller) 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 => SelectedController::Flying, HeadControls::Plane => SelectedController::Flying,
HeadControls::Walk => SelectedController::Running, HeadControls::Walk => SelectedController::Running,
}; };
info!("player head changed: {} ({:?})", head.0, controller);
if *selected_controller != controller { if *selected_controller != controller {
event_controller_switch.write(ControllerSwitchEvent { controller: entity }); event_controller_switch.write(ControllerSwitchEvent { controller: entity });
*selected_controller = controller; commands.entity(entity).insert(controller);
} }
} }
} }

View File

@@ -2,7 +2,8 @@ use super::{ActiveHeads, HEAD_SLOTS};
#[cfg(feature = "client")] #[cfg(feature = "client")]
use crate::heads::HeadsImages; use crate::heads::HeadsImages;
use crate::{ use crate::{
GameState, backpack::UiHeadState, loading_assets::UIAssets, player::Player, protocol::is_server, GameState, backpack::UiHeadState, loading_assets::UIAssets, player::LocalPlayer,
protocol::is_server,
}; };
use bevy::{ecs::spawn::SpawnIter, prelude::*}; use bevy::{ecs::spawn::SpawnIter, prelude::*};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -239,14 +240,10 @@ fn update_health(
} }
fn sync( fn sync(
active_heads: Query<Ref<ActiveHeads>, With<Player>>, active_heads: Single<Ref<ActiveHeads>, With<LocalPlayer>>,
mut state: Single<&mut UiActiveHeads>, mut state: Single<&mut UiActiveHeads>,
time: Res<Time>, time: Res<Time>,
) { ) {
let Ok(active_heads) = active_heads.single() else {
return;
};
if active_heads.is_changed() || active_heads.reloading() { if active_heads.is_changed() || active_heads.reloading() {
state.selected_slot = active_heads.selected_slot; state.selected_slot = active_heads.selected_slot;

View File

@@ -177,8 +177,11 @@ impl ActiveHeads {
} }
} }
#[derive(Event)] #[derive(EntityEvent)]
pub struct HeadChanged(pub usize); pub struct HeadChanged {
pub entity: Entity,
pub head: usize,
}
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.add_plugins(heads_ui::plugin); app.add_plugins(heads_ui::plugin);
@@ -217,11 +220,10 @@ fn sync_hp(mut query: Query<(&mut ActiveHeads, &Hitpoints)>) {
fn reload( fn reload(
mut commands: Commands, mut commands: Commands,
mut active: Query<&mut ActiveHeads>, mut active: Query<(&mut ActiveHeads, &mut AnimationFlags), With<Player>>,
time: Res<Time>, time: Res<Time>,
mut flags: Single<&mut AnimationFlags, With<Player>>,
) { ) {
for mut active in active.iter_mut() { for (mut active, mut flags) in active.iter_mut() {
if !active.reloading() { if !active.reloading() {
continue; continue;
} }
@@ -249,6 +251,7 @@ fn reload(
fn on_select_active_head( fn on_select_active_head(
mut commands: Commands, mut commands: Commands,
mut query: Query<(&mut ActiveHeads, &mut Hitpoints), With<Player>>, mut query: Query<(&mut ActiveHeads, &mut Hitpoints), With<Player>>,
// TODO: unify into just one message
mut select_lefts: MessageReader<FromClient<SelectLeftPressed>>, mut select_lefts: MessageReader<FromClient<SelectLeftPressed>>,
mut select_rights: MessageReader<FromClient<SelectRightPressed>>, mut select_rights: MessageReader<FromClient<SelectRightPressed>>,
controllers: ClientToController, controllers: ClientToController,
@@ -270,9 +273,10 @@ fn on_select_active_head(
active_heads.current_slot = active_heads.selected_slot; active_heads.current_slot = active_heads.selected_slot;
hp.set_health(active_heads.current().unwrap().health); hp.set_health(active_heads.current().unwrap().health);
commands.trigger(HeadChanged( commands.trigger(HeadChanged {
active_heads.heads[active_heads.current_slot].unwrap().head, entity: player,
)); head: active_heads.heads[active_heads.current_slot].unwrap().head,
});
} }
} }
@@ -293,9 +297,10 @@ fn on_select_active_head(
active_heads.current_slot = active_heads.selected_slot; active_heads.current_slot = active_heads.selected_slot;
hp.set_health(active_heads.current().unwrap().health); hp.set_health(active_heads.current().unwrap().health);
commands.trigger(HeadChanged( commands.trigger(HeadChanged {
active_heads.heads[active_heads.current_slot].unwrap().head, entity: player,
)); head: active_heads.heads[active_heads.current_slot].unwrap().head,
});
} }
} }
} }
@@ -303,11 +308,11 @@ fn on_select_active_head(
fn on_swap_backpack( fn on_swap_backpack(
trigger: On<FromClient<BackpackSwapEvent>>, trigger: On<FromClient<BackpackSwapEvent>>,
mut commands: Commands, mut commands: Commands,
mut query: Query<(&mut ActiveHeads, &mut Hitpoints, &mut Backpack), With<Player>>, mut query: Query<(Entity, &mut ActiveHeads, &mut Hitpoints, &mut Backpack), With<Player>>,
) { ) {
let backpack_slot = trigger.event().0; let backpack_slot = trigger.event().0;
let Ok((mut active_heads, mut hp, mut backpack)) = query.single_mut() else { let Ok((player, mut active_heads, mut hp, mut backpack)) = query.single_mut() else {
return; return;
}; };
@@ -326,7 +331,8 @@ fn on_swap_backpack(
hp.set_health(active_heads.current().unwrap().health); hp.set_health(active_heads.current().unwrap().health);
commands.trigger(HeadChanged( commands.trigger(HeadChanged {
active_heads.heads[active_heads.selected_slot].unwrap().head, entity: player,
)); head: active_heads.heads[active_heads.selected_slot].unwrap().head,
});
} }

View File

@@ -132,9 +132,16 @@ pub fn spawn(
fn on_kill( fn on_kill(
trigger: On<Kill>, trigger: On<Kill>,
mut commands: Commands, mut commands: Commands,
mut query: Query<(&Transform, &ActiveHead, &mut ActiveHeads, &mut Hitpoints)>, mut query: Query<(
Entity,
&Transform,
&ActiveHead,
&mut ActiveHeads,
&mut Hitpoints,
)>,
) { ) {
let Ok((transform, active, mut heads, mut hp)) = query.get_mut(trigger.event().entity) else { let Ok((player, transform, active, mut heads, mut hp)) = query.get_mut(trigger.event().entity)
else {
return; return;
}; };
@@ -143,31 +150,51 @@ fn on_kill(
if let Some(new_head) = heads.loose_current() { if let Some(new_head) = heads.loose_current() {
hp.set_health(heads.current().unwrap().health); hp.set_health(heads.current().unwrap().health);
commands.trigger(HeadChanged(new_head)); commands.trigger(HeadChanged {
entity: player,
head: new_head,
});
} }
} }
fn on_update_head_mesh( fn on_update_head_mesh(
trigger: On<HeadChanged>, trigger: On<HeadChanged>,
mut commands: Commands, mut commands: Commands,
mesh_children: Single<&Children, With<PlayerBodyMesh>>, player_id: Query<&PlayerId, With<Player>>,
children: Query<&Children>,
player_body_mesh: Query<&PlayerBodyMesh>,
animated_characters: Query<&AnimatedCharacter>, animated_characters: Query<&AnimatedCharacter>,
mut player: Single<&mut ActiveHead, With<Player>>, mut active_head: Query<&mut ActiveHead>,
) -> Result { ) -> Result {
let animated_char = mesh_children let player_id = player_id.get(trigger.entity)?.clone();
.iter()
.find(|child| animated_characters.contains(*child))
.ok_or("tried to update head mesh before AnimatedCharacter was readded")?;
player.0 = trigger.0; let player_body_mesh = children
.get(trigger.entity)?
.iter()
.find(|child| player_body_mesh.get(*child).is_ok())
.unwrap();
let animated_character = children
.get(player_body_mesh)?
.iter()
.find(|child| animated_characters.get(*child).is_ok())
.unwrap();
{
let mut active_head = active_head.get_mut(trigger.entity)?;
active_head.0 = trigger.head;
}
commands commands
.entity(animated_char) .entity(animated_character)
.insert(AnimatedCharacter::new(trigger.0)); .insert(AnimatedCharacter::new(trigger.head));
commands.server_trigger(ToClients { commands.server_trigger(ToClients {
mode: SendMode::Broadcast, mode: SendMode::Broadcast,
message: ClientHeadChanged(trigger.0 as u64), message: ClientHeadChanged {
player: player_id,
head: trigger.head,
},
}); });
Ok(()) Ok(())

View File

@@ -1,8 +1,14 @@
use bevy::prelude::*; use bevy::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::protocol::PlayerId;
// TODO: remove in favour of client side change detection
#[derive(Clone, Event, Serialize, Deserialize, PartialEq)] #[derive(Clone, Event, Serialize, Deserialize, PartialEq)]
pub struct ClientHeadChanged(pub u64); pub struct ClientHeadChanged {
pub player: PlayerId,
pub head: usize,
}
#[derive(Event, Clone, Debug, Serialize, Deserialize)] #[derive(Event, Clone, Debug, Serialize, Deserialize)]
pub enum PlaySound { pub enum PlaySound {