use crate::{ GameState, client::control::CharacterInputEnabled, control::{ BackpackButtonPress, CashHealPressed, ClientInputs, ControllerSet, Inputs, LocalInputs, LookDirMovement, SelectLeftPressed, SelectRightPressed, }, player::{LocalPlayer, PlayerBodyMesh}, }; use bevy::{ input::{ gamepad::{GamepadConnection, GamepadEvent}, mouse::MouseMotion, }, prelude::*, }; use bevy_replicon::client::ClientSystems; pub fn plugin(app: &mut App) { app.add_systems( PreUpdate, ( gamepad_connections.run_if(on_message::), reset_lookdir, keyboard_controls, gamepad_controls, mouse_rotate, get_lookdir, send_inputs, ) .chain() .in_set(ControllerSet::CollectInputs) .before(ClientSystems::Receive) .run_if( in_state(GameState::Playing) .and(resource_exists_and_equals(CharacterInputEnabled::On)), ), ); // 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( Update, reset_control_state_on_disable.run_if(in_state(GameState::Playing)), ); } /// Overwrite inputs for this client that were replicated from the server with the local inputs fn overwrite_local_inputs( mut inputs: Single<&mut Inputs, With>, local_inputs: Single<&LocalInputs>, ) { **inputs = local_inputs.0; } /// Write inputs from combined keyboard/gamepad state into the networked input buffer /// for the local player. fn send_inputs(mut writer: MessageWriter, local_inputs: Single<&LocalInputs>) { writer.write(ClientInputs(local_inputs.0)); } fn reset_lookdir(mut look_dir: ResMut) { look_dir.0 = Vec2::ZERO; } /// Reset character inputs to default when character input is disabled. fn reset_control_state_on_disable( state: Res, mut inputs: Single<&mut LocalInputs>, ) { if state.is_changed() && matches!(*state, CharacterInputEnabled::Off) { inputs.0 = Inputs { look_dir: inputs.0.look_dir, ..default() }; } } fn get_lookdir( mut inputs: Single<&mut LocalInputs>, rig_transform: Option>>, ) { inputs.0.look_dir = if let Some(ref rig_transform) = rig_transform { rig_transform.forward().as_vec3() } else { Vec3::NEG_Z }; } /// Applies a square deadzone to a Vec2 fn deadzone_square(v: Vec2, min: f32) -> Vec2 { Vec2::new( if v.x.abs() < min { 0. } else { v.x }, if v.y.abs() < min { 0. } else { v.y }, ) } /// Collect gamepad inputs #[allow(clippy::too_many_arguments)] fn gamepad_controls( gamepads: Query<&Gamepad>, mut inputs: Single<&mut LocalInputs>, mut look_dir: ResMut, mut backpack_inputs: MessageWriter, mut select_left_pressed: MessageWriter, mut select_right_pressed: MessageWriter, mut cash_heal_pressed: MessageWriter, ) { let deadzone_left_stick = 0.15; let deadzone_right_stick = 0.15; for gamepad in gamepads.iter() { let rotate = gamepad .get(GamepadButton::RightTrigger2) .unwrap_or_default(); // 8BitDo Ultimate wireless Controller for PC look_dir.0 = if gamepad.vendor_id() == Some(11720) && gamepad.product_id() == Some(12306) { const EPSILON: f32 = 0.015; Vec2::new( if rotate < 0.5 - EPSILON { 40. * (rotate - 0.5) } else if rotate > 0.5 + EPSILON { -40. * (rotate - 0.5) } else { 0. }, 0., ) } else { deadzone_square(gamepad.right_stick(), deadzone_right_stick) * 40. }; let move_dir = deadzone_square(gamepad.left_stick(), deadzone_left_stick); 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); if gamepad.just_pressed(GamepadButton::DPadUp) { backpack_inputs.write(BackpackButtonPress::Toggle); } if gamepad.just_pressed(GamepadButton::DPadDown) { backpack_inputs.write(BackpackButtonPress::Swap); } if gamepad.just_pressed(GamepadButton::DPadLeft) { backpack_inputs.write(BackpackButtonPress::Left); } if gamepad.just_pressed(GamepadButton::DPadRight) { backpack_inputs.write(BackpackButtonPress::Right); } if gamepad.just_pressed(GamepadButton::LeftTrigger) { select_left_pressed.write(SelectLeftPressed); } if gamepad.just_pressed(GamepadButton::RightTrigger) { select_right_pressed.write(SelectRightPressed); } if gamepad.just_pressed(GamepadButton::East) { cash_heal_pressed.write(CashHealPressed); } } } /// Collect mouse movement input fn mouse_rotate(mut mouse: MessageReader, mut look_dir: ResMut) { for ev in mouse.read() { look_dir.0 += ev.delta; } } /// Collect keyboard input #[allow(clippy::too_many_arguments)] fn keyboard_controls( keyboard: Res>, mouse: Res>, mut inputs: Single<&mut LocalInputs>, mut backpack_inputs: MessageWriter, mut select_left_pressed: MessageWriter, mut select_right_pressed: MessageWriter, mut cash_heal_pressed: MessageWriter, ) { let up_binds = [KeyCode::KeyW, KeyCode::ArrowUp]; let down_binds = [KeyCode::KeyS, KeyCode::ArrowDown]; let left_binds = [KeyCode::KeyA, KeyCode::ArrowLeft]; let right_binds = [KeyCode::KeyD, KeyCode::ArrowRight]; let up = keyboard.any_pressed(up_binds); let down = keyboard.any_pressed(down_binds); let left = keyboard.any_pressed(left_binds); let right = keyboard.any_pressed(right_binds); let horizontal = right as i8 - left as i8; let vertical = up as i8 - down as i8; let direction = Vec2::new(horizontal as f32, vertical as f32).clamp_length_max(1.0); 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); if keyboard.just_pressed(KeyCode::KeyB) { backpack_inputs.write(BackpackButtonPress::Toggle); } if keyboard.just_pressed(KeyCode::Enter) { backpack_inputs.write(BackpackButtonPress::Swap); } if keyboard.just_pressed(KeyCode::Comma) { backpack_inputs.write(BackpackButtonPress::Left); } if keyboard.just_pressed(KeyCode::Period) { backpack_inputs.write(BackpackButtonPress::Right); } if keyboard.just_pressed(KeyCode::KeyQ) { select_left_pressed.write(SelectLeftPressed); } if keyboard.just_pressed(KeyCode::KeyE) { select_right_pressed.write(SelectRightPressed); } if keyboard.just_pressed(KeyCode::Enter) { cash_heal_pressed.write(CashHealPressed); } } /// Receive gamepad connections and disconnections fn gamepad_connections(mut evr_gamepad: MessageReader) { for ev in evr_gamepad.read() { if let GamepadEvent::Connection(connection) = ev { match &connection.connection { GamepadConnection::Connected { name, vendor_id, product_id, } => { info!( "New gamepad connected: {:?}, name: {name}, vendor: {vendor_id:?}, product: {product_id:?}", connection.gamepad, ); } GamepadConnection::Disconnected => { info!("Lost connection with gamepad: {:?}", connection.gamepad); } } } } }