diff --git a/crates/asset_game/src/weapon_catalog.rs b/crates/asset_game/src/weapon_catalog.rs index d6ea6a8..78bcc4c 100644 --- a/crates/asset_game/src/weapon_catalog.rs +++ b/crates/asset_game/src/weapon_catalog.rs @@ -217,6 +217,12 @@ pub struct WeaponBodyFacts { pub sprint_drop_time_ms: i32, pub fuse_time_ms: i32, + pub auto_aim_range: f32, + + pub aim_assist_range: f32, + + pub aim_assist_range_ads: f32, + pub cook_off_hold: bool, pub clip_only: bool, @@ -1729,6 +1735,9 @@ impl WeaponCatalog { sprint_loop_time_ms: geometry.sprint_loop_time_ms, sprint_drop_time_ms: geometry.sprint_drop_time_ms, fuse_time_ms: geometry.fuse_time_ms, + auto_aim_range: geometry.auto_aim_range, + aim_assist_range: geometry.aim_assist_range, + aim_assist_range_ads: geometry.aim_assist_range_ads, cook_off_hold: geometry.cook_off_hold, clip_only: geometry.clip_only, timed_detonation: geometry.timed_detonation, @@ -4691,6 +4700,11 @@ fn merge_body_facts(dst: &mut WeaponBodyFacts, src: WeaponBodyFacts) { if dst.fuse_time_ms == 0 { dst.fuse_time_ms = src.fuse_time_ms; } + if dst.aim_assist_range == 0.0 { + dst.auto_aim_range = src.auto_aim_range; + dst.aim_assist_range = src.aim_assist_range; + dst.aim_assist_range_ads = src.aim_assist_range_ads; + } if dst.sprint_raise_time_ms == 0 { dst.sprint_raise_time_ms = src.sprint_raise_time_ms; } diff --git a/crates/audio/src/rumble.rs b/crates/audio/src/rumble.rs index 09888b5..1ef441b 100644 --- a/crates/audio/src/rumble.rs +++ b/crates/audio/src/rumble.rs @@ -151,8 +151,18 @@ fn update( gamepads: Query>, mut state: Local, mut output: MessageWriter, + settings: Res, + active: Option>, ) { let now = clock.time(); + if !settings.pad_vibration { + for _ in requests.read() {} + state.active.clear(); + if let Some((gamepad, _)) = state.output.take() { + output.write(GamepadRumbleRequest::Stop { gamepad }); + } + return; + } let owner = bank.as_ref().and_then(|bank| { let meta = presented.snapshot()?.meta.for_client(local.0)?; (meta.lifecycle == sim::ClientLifecycle::Alive).then_some(( @@ -198,7 +208,10 @@ fn update( intensity.strong_motor = intensity.strong_motor.max(current.strong_motor); intensity.weak_motor = intensity.weak_motor.max(current.weak_motor); } - let selected = gamepads.iter().min_by_key(|entity| entity.to_bits()); + let selected = active + .and_then(|active| active.0) + .filter(|entity| gamepads.contains(*entity)) + .or_else(|| gamepads.iter().min_by_key(|entity| entity.to_bits())); if state .output .is_some_and(|(entity, _)| Some(entity) != selected) diff --git a/crates/console/src/binds.rs b/crates/console/src/binds.rs index 855d183..1542fe2 100644 --- a/crates/console/src/binds.rs +++ b/crates/console/src/binds.rs @@ -3,6 +3,7 @@ use input_iw4::{ClientInput, command_id_lookup, command_name, key_event}; use std::collections::HashMap; use bevy::input::ButtonInput; +use bevy::input::gamepad::{Gamepad, GamepadButton}; use bevy::input::keyboard::KeyCode; use bevy::input::mouse::{MouseButton, MouseScrollUnit}; use bevy::prelude::Resource; @@ -21,6 +22,174 @@ pub enum BindButton { Mouse(MouseButton), WheelUp, WheelDown, + Pad(PadButton), +} + +impl BindButton { + pub fn is_pad(self) -> bool { + matches!(self, Self::Pad(_)) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum PadButton { + A, + B, + X, + Y, + LeftBumper, + RightBumper, + LeftTrigger, + RightTrigger, + LeftStick, + RightStick, + DpadUp, + DpadDown, + DpadLeft, + DpadRight, + Back, + Start, +} + +impl PadButton { + pub const ALL: [Self; 16] = [ + Self::A, + Self::B, + Self::X, + Self::Y, + Self::LeftBumper, + Self::RightBumper, + Self::LeftTrigger, + Self::RightTrigger, + Self::LeftStick, + Self::RightStick, + Self::DpadUp, + Self::DpadDown, + Self::DpadLeft, + Self::DpadRight, + Self::Back, + Self::Start, + ]; + + pub const fn gamepad_button(self) -> GamepadButton { + match self { + Self::A => GamepadButton::South, + Self::B => GamepadButton::East, + Self::X => GamepadButton::West, + Self::Y => GamepadButton::North, + Self::LeftBumper => GamepadButton::LeftTrigger, + Self::RightBumper => GamepadButton::RightTrigger, + Self::LeftTrigger => GamepadButton::LeftTrigger2, + Self::RightTrigger => GamepadButton::RightTrigger2, + Self::LeftStick => GamepadButton::LeftThumb, + Self::RightStick => GamepadButton::RightThumb, + Self::DpadUp => GamepadButton::DPadUp, + Self::DpadDown => GamepadButton::DPadDown, + Self::DpadLeft => GamepadButton::DPadLeft, + Self::DpadRight => GamepadButton::DPadRight, + Self::Back => GamepadButton::Select, + Self::Start => GamepadButton::Start, + } + } + + pub fn from_gamepad_button(button: GamepadButton) -> Option { + Self::ALL + .into_iter() + .find(|pad| pad.gamepad_button() == button) + } + + pub const fn console_name(self) -> &'static str { + match self { + Self::A => "BUTTON_A", + Self::B => "BUTTON_B", + Self::X => "BUTTON_X", + Self::Y => "BUTTON_Y", + Self::LeftBumper => "BUTTON_LSHLDR", + Self::RightBumper => "BUTTON_RSHLDR", + Self::LeftTrigger => "BUTTON_LTRIG", + Self::RightTrigger => "BUTTON_RTRIG", + Self::LeftStick => "BUTTON_LSTICK", + Self::RightStick => "BUTTON_RSTICK", + Self::DpadUp => "DPAD_UP", + Self::DpadDown => "DPAD_DOWN", + Self::DpadLeft => "DPAD_LEFT", + Self::DpadRight => "DPAD_RIGHT", + Self::Back => "BUTTON_BACK", + Self::Start => "BUTTON_START", + } + } + + pub const fn label(self) -> &'static str { + match self { + Self::A => "A", + Self::B => "B", + Self::X => "X", + Self::Y => "Y", + Self::LeftBumper => "LB", + Self::RightBumper => "RB", + Self::LeftTrigger => "LT", + Self::RightTrigger => "RT", + Self::LeftStick => "LS", + Self::RightStick => "RS", + Self::DpadUp => "D-UP", + Self::DpadDown => "D-DOWN", + Self::DpadLeft => "D-LEFT", + Self::DpadRight => "D-RIGHT", + Self::Back => "BACK", + Self::Start => "START", + } + } + + const fn keynum(self) -> usize { + 190 + self as usize + } +} + +pub fn pad_layout(layout: usize) -> Vec<(PadButton, &'static str)> { + use PadButton::*; + let mut binds = vec![ + (RightTrigger, "+attack"), + (LeftTrigger, "+speed_throw"), + (RightBumper, "+frag"), + (LeftBumper, "+smoke"), + (A, "+gostand"), + (B, "+stance"), + (X, "+usereload"), + (Y, "weapnext"), + (LeftStick, "+breath_sprint"), + (RightStick, "+melee"), + (DpadUp, "+actionslot 1"), + (DpadDown, "+actionslot 2"), + (DpadLeft, "+actionslot 3"), + (DpadRight, "+actionslot 4"), + (Back, "+scores"), + ]; + let mut set = |button: PadButton, command: &'static str| { + binds.retain(|(b, _)| *b != button); + binds.push((button, command)); + }; + let tactical = |set: &mut dyn FnMut(PadButton, &'static str)| { + set(B, "+melee"); + set(RightStick, "+stance"); + }; + match layout { + 1 => tactical(&mut set), + 2 => { + set(LeftTrigger, "+attack"); + set(RightTrigger, "+speed_throw"); + set(LeftBumper, "+frag"); + set(RightBumper, "+smoke"); + } + 3 | 4 => { + set(LeftBumper, "+gostand"); + set(A, "+smoke"); + if layout == 4 { + tactical(&mut set); + } + } + _ => {} + } + binds } pub(crate) fn wheel_button(y: f32) -> Option { @@ -50,11 +219,21 @@ pub(crate) fn wheel_detents(unit: MouseScrollUnit, y: f32, carry: &mut f32) -> i pub struct BindInputs<'a> { pub keys: &'a ButtonInput, pub mouse: &'a ButtonInput, + pub pad: Option<&'a Gamepad>, } impl<'a> BindInputs<'a> { pub fn new(keys: &'a ButtonInput, mouse: &'a ButtonInput) -> Self { - Self { keys, mouse } + Self { + keys, + mouse, + pad: None, + } + } + + pub fn with_pad(mut self, pad: Option<&'a Gamepad>) -> Self { + self.pad = pad; + self } pub fn pressed(&self, button: BindButton) -> bool { @@ -62,6 +241,9 @@ impl<'a> BindInputs<'a> { BindButton::Key(key) => self.keys.pressed(key), BindButton::Mouse(btn) => self.mouse.pressed(btn), BindButton::WheelUp | BindButton::WheelDown => false, + BindButton::Pad(btn) => self + .pad + .is_some_and(|pad| pad.pressed(btn.gamepad_button())), } } @@ -70,6 +252,9 @@ impl<'a> BindInputs<'a> { BindButton::Key(key) => self.keys.just_pressed(key), BindButton::Mouse(btn) => self.mouse.just_pressed(btn), BindButton::WheelUp | BindButton::WheelDown => false, + BindButton::Pad(btn) => self + .pad + .is_some_and(|pad| pad.just_pressed(btn.gamepad_button())), } } @@ -78,6 +263,9 @@ impl<'a> BindInputs<'a> { BindButton::Key(key) => self.keys.just_released(key), BindButton::Mouse(btn) => self.mouse.just_released(btn), BindButton::WheelUp | BindButton::WheelDown => false, + BindButton::Pad(btn) => self + .pad + .is_some_and(|pad| pad.just_released(btn.gamepad_button())), } } } @@ -91,6 +279,7 @@ impl KeyBinds { pub fn apply_defaults(&mut self) { self.map.clear(); let _ = self.apply_script(DEFAULT_CONTROLS); + self.apply_pad_layout(0); } pub fn apply_script(&mut self, script: &str) -> Vec { @@ -154,6 +343,26 @@ impl KeyBinds { self.map.clear(); } + pub fn clear_command_on(&mut self, id: u32, pad: bool) -> bool { + let before = self.map.len(); + self.map + .retain(|button, bound| *bound != id || button.is_pad() != pad); + self.map.len() != before + } + + pub fn apply_pad_layout(&mut self, layout: usize) { + self.map.retain(|button, _| !button.is_pad()); + for (button, command) in pad_layout(layout) { + if let Some(id) = command_id_lookup(command) { + self.set(BindButton::Pad(button), id); + } + } + } + + pub fn has_pad_binds(&self) -> bool { + self.map.keys().any(|button| button.is_pad()) + } + pub fn get(&self, button: BindButton) -> Option { self.map.get(&button).copied() } diff --git a/crates/console/src/binds/key_names.rs b/crates/console/src/binds/key_names.rs index 0deb946..0f930bc 100644 --- a/crates/console/src/binds/key_names.rs +++ b/crates/console/src/binds/key_names.rs @@ -33,6 +33,21 @@ macro_rules! key_catalog { $( $($m_hint,)? )* "mwheelup", "mwheeldown", + "BUTTON_A", + "BUTTON_B", + "BUTTON_X", + "BUTTON_Y", + "BUTTON_LSHLDR", + "BUTTON_RSHLDR", + "BUTTON_LTRIG", + "BUTTON_RTRIG", + "BUTTON_LSTICK", + "BUTTON_RSTICK", + "DPAD_UP", + "DPAD_DOWN", + "DPAD_LEFT", + "DPAD_RIGHT", + "BUTTON_BACK", ]; pub fn host_keynum(button: BindButton) -> usize { @@ -43,6 +58,7 @@ macro_rules! key_catalog { }, BindButton::WheelUp => 186, BindButton::WheelDown => 187, + BindButton::Pad(pad) => pad.keynum(), BindButton::Key(key) => match key { $(KeyCode::$k_button => $k_code,)* _ => 62, @@ -65,7 +81,13 @@ macro_rules! key_catalog { )* "mwheelup" | "wheelup" => Some(vec![BindButton::WheelUp]), "mwheeldown" | "wheeldown" => Some(vec![BindButton::WheelDown]), - _ => None, + other => { + let upper = other.to_ascii_uppercase(); + super::PadButton::ALL + .into_iter() + .find(|pad| pad.console_name() == upper || pad.label() == upper) + .map(|pad| vec![BindButton::Pad(pad)]) + } } } @@ -82,6 +104,7 @@ macro_rules! key_catalog { }, BindButton::WheelUp => "MWHEELUP".into(), BindButton::WheelDown => "MWHEELDOWN".into(), + BindButton::Pad(pad) => pad.console_name().into(), } } diff --git a/crates/console/src/gamepad.rs b/crates/console/src/gamepad.rs new file mode 100644 index 0000000..5538b7b --- /dev/null +++ b/crates/console/src/gamepad.rs @@ -0,0 +1,167 @@ +use bevy::input::gamepad::{Gamepad, GamepadButton, GamepadConnection, GamepadConnectionEvent}; +use bevy::prelude::*; +use frame::{UiMenuKey, UiMenuRequest}; + +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub(crate) struct Sticks { + /// Forward and right. + pub movement: Vec2, + /// Right and up. + pub look: Vec2, +} + +fn radial(stick: Vec2, deadzone: f32) -> Vec2 { + let length = stick.length(); + if length <= deadzone || length <= f32::EPSILON { + return Vec2::ZERO; + } + let scaled = ((length - deadzone) / (1.0 - deadzone).max(0.01)).min(1.0); + stick / length * scaled +} + +pub(crate) fn sticks(pad: &Gamepad, settings: &frame::GameSettings) -> Sticks { + let left = radial(pad.left_stick(), settings.pad_deadzone_left); + let right = radial(pad.right_stick(), settings.pad_deadzone_right); + match settings.pad_stick_layout { + 1 => Sticks { + movement: Vec2::new(right.y, right.x), + look: left, + }, + 2 => Sticks { + movement: Vec2::new(left.y, right.x), + look: Vec2::new(left.x, right.y), + }, + 3 => Sticks { + movement: Vec2::new(right.y, left.x), + look: Vec2::new(right.x, left.y), + }, + _ => Sticks { + movement: Vec2::new(left.y, left.x), + look: right, + }, + } +} + +fn curve(deflection: f32, kind: u8) -> f32 { + let d = deflection.clamp(0.0, 1.0); + match kind { + 1 => d, + 2 => 1.0 - (1.0 - d) * (1.0 - d), + _ => 0.35 * d + 0.65 * d * d * d, + } +} + +pub(crate) fn shaped_look(look: Vec2, settings: &frame::GameSettings) -> Vec2 { + let deflection = look.length(); + if deflection <= f32::EPSILON { + return Vec2::ZERO; + } + let shaped = look / deflection * curve(deflection, settings.pad_curve); + Vec2::new( + shaped.x, + if settings.pad_invert { + -shaped.y + } else { + shaped.y + }, + ) +} + +pub(crate) fn track_active_pad( + gamepads: Query<(Entity, &Gamepad, Option<&Name>)>, + mut connections: MessageReader, + mut active: ResMut, +) { + for event in connections.read() { + match &event.connection { + GamepadConnection::Connected { name, .. } => { + diag::info!(Ui, "controller connected: {name}"); + } + GamepadConnection::Disconnected => { + diag::info!(Ui, "controller disconnected"); + } + } + } + if active.0.is_some_and(|entity| gamepads.get(entity).is_err()) { + active.0 = None; + } + for (entity, pad, name) in &gamepads { + let moved = pad.left_stick().length() > 0.5 || pad.right_stick().length() > 0.5; + if (pad.get_just_pressed().next().is_some() || moved) && active.0 != Some(entity) { + diag::info!( + Ui, + "controller in use: {}", + name.map_or("unnamed", |n| n.as_str()) + ); + active.0 = Some(entity); + } + } +} + +const REPEAT_DELAY: f32 = 0.4; +const REPEAT_EVERY: f32 = 0.12; + +pub(crate) fn drive_menus_with_pad( + gamepads: Query<&Gamepad>, + active: Res, + script_menus: Option>, + capture: Res, + time: Res