From 0735c429cae3835aa9822784548782bb50e526b5 Mon Sep 17 00:00:00 2001 From: extrawurst <776816+extrawurst@users.noreply.github.com> Date: Sun, 21 Dec 2025 18:43:13 +0100 Subject: [PATCH] fixes head switching (#96) --- crates/hedz_reloaded/src/camera.rs | 15 +++--- .../src/client/control/controller_flying.rs | 14 +++-- .../src/client/control/controls.rs | 9 ++-- crates/hedz_reloaded/src/client/player.rs | 25 +++++++-- .../src/control/controller_common.rs | 17 ++++-- .../src/control/controller_flying.rs | 17 ++++-- .../src/control/controller_running.rs | 24 +++++++-- crates/hedz_reloaded/src/control/mod.rs | 52 ++++++++++++++---- crates/hedz_reloaded/src/heads/heads_ui.rs | 9 ++-- crates/hedz_reloaded/src/heads/mod.rs | 38 +++++++------ crates/hedz_reloaded/src/player.rs | 53 ++++++++++++++----- crates/hedz_reloaded/src/protocol/events.rs | 8 ++- 12 files changed, 205 insertions(+), 76 deletions(-) diff --git a/crates/hedz_reloaded/src/camera.rs b/crates/hedz_reloaded/src/camera.rs index 52c277a..4e34d71 100644 --- a/crates/hedz_reloaded/src/camera.rs +++ b/crates/hedz_reloaded/src/camera.rs @@ -1,6 +1,7 @@ use crate::GameState; #[cfg(feature = "client")] use crate::control::Inputs; +use crate::control::ViewMode; #[cfg(feature = "client")] use crate::physics_layers::GameLayer; #[cfg(feature = "client")] @@ -31,7 +32,7 @@ pub struct CameraRotationInput(pub Vec2); #[reflect(Resource)] pub struct CameraState { pub cutscene: bool, - pub look_around: bool, + pub view_mode: ViewMode, } #[derive(Component, Reflect, Debug, Default)] @@ -85,10 +86,10 @@ fn update_look_around( inputs: Single<&Inputs, With>, mut cam_state: ResMut, ) { - let look_around = inputs.view_mode; + let view_mode = inputs.view_mode; - if look_around != cam_state.look_around { - cam_state.look_around = look_around; + if view_mode != cam_state.view_mode { + cam_state.view_mode = view_mode; } } @@ -100,9 +101,9 @@ fn update_ui( query: Query>, ) { 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(( CameraUi, Node { @@ -194,7 +195,7 @@ fn rotate_view( look_dir: Res, mut cam: Single<&mut CameraRotationInput>, ) { - if !inputs.view_mode { + if !inputs.view_mode.is_free() { cam.x = 0.0; return; } diff --git a/crates/hedz_reloaded/src/client/control/controller_flying.rs b/crates/hedz_reloaded/src/client/control/controller_flying.rs index 20b5129..b93f91b 100644 --- a/crates/hedz_reloaded/src/client/control/controller_flying.rs +++ b/crates/hedz_reloaded/src/client/control/controller_flying.rs @@ -1,6 +1,6 @@ use crate::{ GameState, - control::{ControllerSet, Inputs, LookDirMovement}, + control::{ControllerSet, Inputs, LookDirMovement, SelectedController}, player::{LocalPlayer, PlayerBodyMesh}, }; use bevy::prelude::*; @@ -19,14 +19,20 @@ pub fn plugin(app: &mut App) { fn rotate_rig( inputs: Single<&Inputs, With>, look_dir: Res, - local_player: Single<&Children, With>, + local_player: Single<(&Children, &SelectedController), With>, mut player_mesh: Query<&mut Transform, With>, ) { - if inputs.view_mode { + if inputs.view_mode.is_free() { 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) { let look_dir = look_dir.0; diff --git a/crates/hedz_reloaded/src/client/control/controls.rs b/crates/hedz_reloaded/src/client/control/controls.rs index eb36b51..27ef377 100644 --- a/crates/hedz_reloaded/src/client/control/controls.rs +++ b/crates/hedz_reloaded/src/client/control/controls.rs @@ -3,7 +3,7 @@ use crate::{ client::control::CharacterInputEnabled, control::{ BackpackButtonPress, CashHealPressed, ClientInputs, ControllerSet, Inputs, LocalInputs, - LookDirMovement, SelectLeftPressed, SelectRightPressed, + LookDirMovement, SelectLeftPressed, SelectRightPressed, ViewMode, }, player::{LocalPlayer, PlayerBodyMesh}, }; @@ -146,8 +146,11 @@ fn gamepad_controls( inputs.0.move_dir += move_dir.clamp_length_max(1.0); inputs.0.jump |= gamepad.pressed(GamepadButton::South); - inputs.0.view_mode |= gamepad.pressed(GamepadButton::LeftTrigger2); inputs.0.trigger |= gamepad.pressed(GamepadButton::RightTrigger2); + inputs + .0 + .view_mode + .merge_input(gamepad.pressed(GamepadButton::LeftTrigger2)); if gamepad.just_pressed(GamepadButton::DPadUp) { backpack_inputs.write(BackpackButtonPress::Toggle); @@ -212,8 +215,8 @@ fn keyboard_controls( inputs.0.move_dir = direction; inputs.0.jump = keyboard.pressed(KeyCode::Space); - inputs.0.view_mode = keyboard.pressed(KeyCode::Tab); inputs.0.trigger = mouse.pressed(MouseButton::Left); + inputs.0.view_mode = ViewMode::from_input(keyboard.pressed(KeyCode::Tab)); if keyboard.just_pressed(KeyCode::KeyB) { backpack_inputs.write(BackpackButtonPress::Toggle); diff --git a/crates/hedz_reloaded/src/client/player.rs b/crates/hedz_reloaded/src/client/player.rs index cdfdce9..3d028ec 100644 --- a/crates/hedz_reloaded/src/client/player.rs +++ b/crates/hedz_reloaded/src/client/player.rs @@ -2,7 +2,7 @@ use crate::{ global_observer, heads_database::{HeadControls, HeadsDatabase}, loading_assets::AudioAssets, - player::{LocalPlayer, PlayerBodyMesh}, + player::{LocalPlayer, Player, PlayerBodyMesh}, protocol::{ClientHeadChanged, PlaySound, PlayerId, messages::AssignClientPlayer}, }; use bevy::prelude::*; @@ -59,23 +59,38 @@ pub enum PlayerAssignmentState { Confirmed, } +// TODO: currently a networked message. +// can be done by just using local change detection on `ActiveHead`? fn on_client_update_head_mesh( trigger: On, mut commands: Commands, - body_mesh: Single<(Entity, &Children), With>, + player: Query<(&Children, &PlayerId), With>, + body_mesh: Query<(Entity, &Children), With>, head_db: Res, audio_assets: Res, sfx: Query<&AudioPlayer>, ) -> Result { - let head = trigger.0 as usize; - let (body_mesh, mesh_children) = *body_mesh; + let (player_children, _) = player + .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); commands.trigger(PlaySound::Head(head_str.to_string())); //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(); } if head_db.head_stats(head).controls == HeadControls::Plane { diff --git a/crates/hedz_reloaded/src/control/controller_common.rs b/crates/hedz_reloaded/src/control/controller_common.rs index d972677..bae0761 100644 --- a/crates/hedz_reloaded/src/control/controller_common.rs +++ b/crates/hedz_reloaded/src/control/controller_common.rs @@ -72,12 +72,22 @@ fn set_animation_flags( pub fn reset_upon_switch( mut c: Commands, mut event_controller_switch: MessageReader, - selected_controller: Res, mut rig_transforms: Query<&mut Transform, With>, - mut controllers: Query<(&mut KinematicVelocity, &Children, &Inputs), With>, + mut controllers: Query< + ( + &mut KinematicVelocity, + &Children, + &Inputs, + &SelectedController, + ), + With, + >, ) { 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; @@ -165,6 +175,7 @@ impl Default for MovementSpeedFactor { MoveInput, MovementSpeedFactor, TransformInterpolation, + SelectedController::Running, CharacterMovement = RUNNING_MOVEMENT_CONFIG.movement, ControllerSettings = RUNNING_MOVEMENT_CONFIG.settings, CharacterGravity = RUNNING_MOVEMENT_CONFIG.gravity, diff --git a/crates/hedz_reloaded/src/control/controller_flying.rs b/crates/hedz_reloaded/src/control/controller_flying.rs index 5b85f1f..370dc90 100644 --- a/crates/hedz_reloaded/src/control/controller_flying.rs +++ b/crates/hedz_reloaded/src/control/controller_flying.rs @@ -1,7 +1,7 @@ use super::ControllerSet; use crate::{ GameState, - control::{Inputs, controller_common::MovementSpeedFactor}, + control::{Inputs, SelectedController, controller_common::MovementSpeedFactor}, }; use bevy::prelude::*; use happy_feet::prelude::MoveInput; @@ -19,8 +19,17 @@ impl Plugin for CharacterControllerPlugin { } } -pub fn apply_controls(mut query: Query<(&mut MoveInput, &MovementSpeedFactor, &Inputs)>) { - for (mut char_input, factor, inputs) in query.iter_mut() { - char_input.set(inputs.look_dir * factor.0); +pub fn apply_controls( + mut query: Query<( + &mut MoveInput, + &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); + } } } diff --git a/crates/hedz_reloaded/src/control/controller_running.rs b/crates/hedz_reloaded/src/control/controller_running.rs index 1ca70df..3a20f84 100644 --- a/crates/hedz_reloaded/src/control/controller_running.rs +++ b/crates/hedz_reloaded/src/control/controller_running.rs @@ -1,7 +1,10 @@ use crate::{ GameState, animation::AnimationFlags, - control::{ControllerSet, ControllerSettings, Inputs, controller_common::MovementSpeedFactor}, + control::{ + ControllerSet, ControllerSettings, Inputs, SelectedController, + controller_common::MovementSpeedFactor, + }, protocol::is_server, }; #[cfg(feature = "client")] @@ -41,7 +44,7 @@ fn rotate_view( ) { let (inputs, children) = controller.into_inner(); - if inputs.view_mode { + if inputs.view_mode.is_free() { return; } @@ -64,11 +67,24 @@ fn apply_controls( &ControllerSettings, &MovementSpeedFactor, &Inputs, + &SelectedController, )>, ) { - for (mut move_input, mut grounding, mut velocity, mut flags, settings, move_factor, inputs) in - query.iter_mut() + for ( + 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 mut direction = inputs.move_dir.extend(0.0).xzy(); diff --git a/crates/hedz_reloaded/src/control/mod.rs b/crates/hedz_reloaded/src/control/mod.rs index ac45f62..e9d2b54 100644 --- a/crates/hedz_reloaded/src/control/mod.rs +++ b/crates/hedz_reloaded/src/control/mod.rs @@ -20,7 +20,8 @@ pub enum ControllerSet { ApplyControlsRun, } -#[derive(Resource, Debug, Clone, Copy, PartialEq, Default)] +#[derive(Component, Reflect, Debug, Clone, Copy, PartialEq, Eq, Default)] +#[reflect(Component)] pub enum SelectedController { Flying, #[default] @@ -35,8 +36,9 @@ pub fn plugin(app: &mut App) { #[cfg(feature = "client")] app.register_type::(); + app.register_type::(); + app.init_resource::(); - app.init_resource::(); app.add_message::() .add_message::(); @@ -48,8 +50,8 @@ pub fn plugin(app: &mut App) { app.configure_sets( FixedUpdate, ( - ControllerSet::ApplyControlsFly.run_if(resource_equals(SelectedController::Flying)), - ControllerSet::ApplyControlsRun.run_if(resource_equals(SelectedController::Running)), + ControllerSet::ApplyControlsFly, + ControllerSet::ApplyControlsRun, ) .chain() .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))); } +#[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`. #[derive(Component, Clone, Copy, Debug, Serialize, Deserialize, Reflect)] #[reflect(Component, Default)] @@ -75,7 +106,7 @@ pub struct Inputs { pub look_dir: Vec3, pub jump: bool, /// Determines if the camera can rotate freely around the player - pub view_mode: bool, + pub view_mode: ViewMode, pub trigger: bool, } @@ -154,23 +185,24 @@ fn collect_player_inputs( } fn head_change( - //TODO: needs a 'LocalPlayer' at some point for multiplayer - query: Query<(Entity, &ActiveHead), (Changed, With)>, + mut commands: Commands, + query: Query<(Entity, &ActiveHead, &SelectedController), (Changed, With)>, heads_db: Res, - mut selected_controller: ResMut, mut event_controller_switch: MessageWriter, ) { - for (entity, head) in query.iter() { + for (entity, head, selected_controller) in query.iter() { let stats = heads_db.head_stats(head.0); let controller = match stats.controls { HeadControls::Plane => SelectedController::Flying, HeadControls::Walk => SelectedController::Running, }; + info!("player head changed: {} ({:?})", head.0, controller); + if *selected_controller != controller { event_controller_switch.write(ControllerSwitchEvent { controller: entity }); - *selected_controller = controller; + commands.entity(entity).insert(controller); } } } diff --git a/crates/hedz_reloaded/src/heads/heads_ui.rs b/crates/hedz_reloaded/src/heads/heads_ui.rs index 060f5bb..e4bf22c 100644 --- a/crates/hedz_reloaded/src/heads/heads_ui.rs +++ b/crates/hedz_reloaded/src/heads/heads_ui.rs @@ -2,7 +2,8 @@ use super::{ActiveHeads, HEAD_SLOTS}; #[cfg(feature = "client")] use crate::heads::HeadsImages; 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 serde::{Deserialize, Serialize}; @@ -239,14 +240,10 @@ fn update_health( } fn sync( - active_heads: Query, With>, + active_heads: Single, With>, mut state: Single<&mut UiActiveHeads>, time: Res