Add controller support: button layouts, stick aim and aim assist

Controller buttons bind through the same table as keys: every command on
the controls page shows its key and its button, and either is rebound by
pressing a key or a button. The options gain a Controller page: button
layout (Default, Tactical, Lefty, Bumper Jumper, Bumper Jumper Tactical),
stick layout, look and aim-down-sight sensitivity, invert, response curve,
aim assist, vibration and deadzones, all kept in settings.cfg.

The look stick turns at rates that build up to full deflection, slower
down the sight and scaled by the zoom. Aim assist works over visible
enemies within each weapon's own aim assist ranges, read from the weapon
data: slowdown and lock-on, and on Full a pull onto a target when the
sight comes up. It applies only while aiming with the controller.

Menus take the D-pad or left stick, A and B, and Start opens them. Focus
moves by screen position rather than by menu file order, which jumped
around the options pages for arrow keys too; left and right on a button
cross columns. The Advanced Video page no longer draws the FOV slider
over Bloom.

The controller in use is the last one pressed or moved, so a virtual
joystick or an idle second pad stays out of the way; rumble follows it and
the vibration setting. Use/reload reloads when nothing usable is in reach.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
chazmm
2026-09-29 14:04:22 +01:00
parent 1a0daffd18
commit f2fbe816f7
25 changed files with 2082 additions and 26 deletions
+14
View File
@@ -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;
}
+14 -1
View File
@@ -151,8 +151,18 @@ fn update(
gamepads: Query<Entity, With<Gamepad>>,
mut state: Local<RumblePlayback>,
mut output: MessageWriter<GamepadRumbleRequest>,
settings: Res<frame::GameSettings>,
active: Option<Res<frame::ActivePad>>,
) {
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)
+210 -1
View File
@@ -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> {
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<BindButton> {
@@ -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<KeyCode>,
pub mouse: &'a ButtonInput<MouseButton>,
pub pad: Option<&'a Gamepad>,
}
impl<'a> BindInputs<'a> {
pub fn new(keys: &'a ButtonInput<KeyCode>, mouse: &'a ButtonInput<MouseButton>) -> 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<String> {
@@ -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<u32> {
self.map.get(&button).copied()
}
+24 -1
View File
@@ -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(),
}
}
+167
View File
@@ -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<GamepadConnectionEvent>,
mut active: ResMut<frame::ActivePad>,
) {
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<frame::ActivePad>,
script_menus: Option<Res<hud::ScriptMenus>>,
capture: Res<frame::UiBindingCapture>,
time: Res<Time>,
mut requests: MessageWriter<UiMenuRequest>,
mut repeat: Local<Option<(UiMenuKey, f32)>>,
) {
// A binding being listened for takes the controller's buttons itself.
let pad = active
.0
.and_then(|entity| gamepads.get(entity).ok())
.filter(|_| capture.command.is_none());
let Some(pad) = pad else {
*repeat = None;
return;
};
if pad.just_pressed(GamepadButton::Start) {
requests.write(UiMenuRequest::Key(UiMenuKey::Escape));
}
if !script_menus.is_some_and(|menus| menus.captures_input()) {
*repeat = None;
return;
}
if pad.just_pressed(GamepadButton::South) {
requests.write(UiMenuRequest::Key(UiMenuKey::Enter));
}
if pad.just_pressed(GamepadButton::East) {
requests.write(UiMenuRequest::Key(UiMenuKey::Escape));
}
let stick = pad.left_stick();
let direction = if pad.pressed(GamepadButton::DPadUp) || stick.y > 0.6 {
Some(UiMenuKey::Up)
} else if pad.pressed(GamepadButton::DPadDown) || stick.y < -0.6 {
Some(UiMenuKey::Down)
} else if pad.pressed(GamepadButton::DPadLeft) || stick.x < -0.6 {
Some(UiMenuKey::Left)
} else if pad.pressed(GamepadButton::DPadRight) || stick.x > 0.6 {
Some(UiMenuKey::Right)
} else {
None
};
let now = time.elapsed_secs();
let fire = match (direction, *repeat) {
(Some(key), Some((held, next))) if key == held => {
if now >= next {
*repeat = Some((key, now + REPEAT_EVERY));
}
now >= next
}
(Some(key), _) => {
*repeat = Some((key, now + REPEAT_DELAY));
true
}
(None, _) => {
*repeat = None;
false
}
};
if fire && let Some(key) = direction {
requests.write(UiMenuRequest::Key(key));
}
}
+3 -2
View File
@@ -23,6 +23,7 @@ mod diagnostics;
pub mod editor;
mod feature_dispatch;
mod frontend;
mod gamepad;
pub mod input;
pub mod plugin;
pub mod registry;
@@ -33,8 +34,8 @@ mod user_settings;
mod weapon_dispatch;
pub use binds::{
BINDABLE_KEYS, BindButton, BindInputs, DEFAULT_CONTROLS, KeyBinds, display_button, host_keynum,
parse_button_name, parse_key_name,
BINDABLE_KEYS, BindButton, BindInputs, DEFAULT_CONTROLS, KeyBinds, PadButton, display_button,
host_keynum, parse_button_name, parse_key_name,
};
pub use class_dispatch::class_completions;
pub use command::{ConsoleCommand, ConsoleQueue, SubmittedCommand};
+52 -1
View File
@@ -99,6 +99,23 @@ impl Plugin for ConsolePlugin {
(setup_console, crate::user_settings::load_user_settings).chain(),
)
.add_systems(PreUpdate, feed_console_keyboard.before(InputSystems))
.init_resource::<frame::ActivePad>()
.add_systems(
PreUpdate,
(
crate::gamepad::track_active_pad,
crate::gamepad::drive_menus_with_pad,
)
.chain()
.after(InputSystems)
.before(publish_client_action_input),
)
.add_systems(
Update,
crate::user_settings::apply_pad_layout_setting
.after(crate::user_settings::consume_menu_binding)
.before(crate::user_settings::sync_binding_view),
)
.add_systems(
PreUpdate,
(
@@ -265,7 +282,21 @@ fn publish_client_action_input(
mut hud_input: ResMut<frame::HudInputView>,
settings: Res<frame::GameSettings>,
mut out: ResMut<ClientActionInput>,
(gamepads, active, mut aiming_with_pad): (
Query<&bevy::input::gamepad::Gamepad>,
Res<frame::ActivePad>,
Local<bool>,
),
) {
let pad = active.0.and_then(|entity| gamepads.get(entity).ok());
out.pad_aim_assist = if *aiming_with_pad && pad.is_some() {
settings.pad_aim_assist
} else {
0
};
out.pad_move = [0.0; 2];
out.pad_look = [0.0; 2];
out.pad_deflection = 0.0;
hud_input.console_open = console.open;
if binds.is_changed() || hud_input.binding_keys.is_empty() {
hud_input.binding_keys.clear();
@@ -337,7 +368,22 @@ fn publish_client_action_input(
return;
}
let inputs = BindInputs::new(&keys, &mouse_buttons);
out.pad_sensitivity = settings.pad_sensitivity / frame::GameSettings::PAD_SENSITIVITY_DEFAULT;
out.pad_ads_sensitivity = settings.pad_ads_sensitivity;
if let Some(pad) = pad {
let sticks = crate::gamepad::sticks(pad, &settings);
let look = crate::gamepad::shaped_look(sticks.look, &settings);
out.pad_move = [sticks.movement.x, sticks.movement.y];
out.pad_look = [look.x, look.y];
out.pad_deflection = sticks.movement.x.abs().max(sticks.look.length());
if sticks.look.length() > 0.0
|| sticks.movement.length() > 0.0
|| pad.get_just_pressed().next().is_some()
{
*aiming_with_pad = true;
}
}
let inputs = BindInputs::new(&keys, &mouse_buttons).with_pad(pad);
for (button, id) in binds.iter() {
let key_num = host_keynum(button);
if key_num >= input_iw4::KEY_COUNT {
@@ -392,9 +438,14 @@ fn publish_client_action_input(
let (rx, ry) = scripted.mouse_rate().unwrap_or((0.0, 0.0));
out.mouse_x += sx + rx;
out.mouse_y += sy + ry;
let mut moved = 0.0;
for ev in motion.read() {
out.mouse_x += ev.delta.x;
out.mouse_y += ev.delta.y;
moved += ev.delta.x.abs() + ev.delta.y.abs();
}
if moved > 2.0 || mouse_buttons.get_just_pressed().next().is_some() {
*aiming_with_pad = false;
}
}
+128 -8
View File
@@ -52,6 +52,11 @@ pub(crate) fn consume_menu_binding(
keys: Res<ButtonInput<KeyCode>>,
mouse: Res<ButtonInput<MouseButton>>,
mut wheel: MessageReader<MouseWheel>,
(gamepads, active, mut settings): (
Query<&bevy::input::gamepad::Gamepad>,
Res<frame::ActivePad>,
ResMut<frame::GameSettings>,
),
mut pending: ResMut<PendingMenuBinding>,
mut capture: ResMut<frame::UiBindingCapture>,
mut binds: ResMut<KeyBinds>,
@@ -85,7 +90,32 @@ pub(crate) fn consume_menu_binding(
pending.armed = true;
return;
}
if keys.just_pressed(KeyCode::Escape) {
let pad = active.0.and_then(|entity| gamepads.get(entity).ok());
let pad_start =
pad.is_some_and(|pad| pad.just_pressed(bevy::input::gamepad::GamepadButton::Start));
let pad_button = pad.and_then(|pad| {
pad.get_just_pressed()
.copied()
.filter_map(crate::PadButton::from_gamepad_button)
.find(|button| *button != crate::PadButton::Start)
});
if let Some(button) = pad_button.filter(|_| !pad_start) {
capture.command = None;
capture.consumed_input = true;
binds.clear_command_on(id, true);
binds.set(BindButton::Pad(button), id);
if settings.pad_layout != frame::GameSettings::PAD_LAYOUT_CUSTOM {
settings.pad_layout = frame::GameSettings::PAD_LAYOUT_CUSTOM;
settings.touch();
}
pending.id = None;
pending.armed = false;
view.listening = None;
view.revision = view.revision.wrapping_add(1);
return;
}
// Start cancels, as Escape does, rather than binding.
if keys.just_pressed(KeyCode::Escape) || pad_start {
capture.command = None;
capture.consumed_input = true;
pending.id = None;
@@ -110,7 +140,7 @@ pub(crate) fn consume_menu_binding(
let Some(button) = button else { return };
capture.command = None;
capture.consumed_input = true;
binds.clear_command(id);
binds.clear_command_on(id, false);
binds.set(button, id);
pending.id = None;
pending.armed = false;
@@ -126,15 +156,27 @@ pub(crate) fn sync_binding_view(
if !binds.is_changed() {
return;
}
let mut chords = std::collections::BTreeMap::<u32, Vec<String>>::new();
let mut chords = std::collections::BTreeMap::<u32, (Vec<String>, Vec<String>)>::new();
for (button, id) in binds.iter() {
chords.entry(id).or_default().push(display_button(button));
let (keys, buttons) = chords.entry(id).or_default();
match button {
BindButton::Pad(pad) => buttons.push(pad.label().to_owned()),
_ => keys.push(display_button(button)),
}
}
view.chords.clear();
for (id, mut names) in chords {
names.sort();
names.dedup();
view.chords.insert(id, names.join(" OR "));
for (id, (mut keys, mut buttons)) in chords {
keys.sort();
keys.dedup();
buttons.sort();
buttons.dedup();
let text = match (keys.is_empty(), buttons.is_empty()) {
(false, false) => format!("{} | {}", keys.join(" OR "), buttons.join(" OR ")),
(false, true) => keys.join(" OR "),
(true, false) => buttons.join(" OR "),
(true, true) => continue,
};
view.chords.insert(id, text);
}
// Publish every command so removing its last binding clears the old label.
for (id, command) in input_iw4::INPUT_COMMAND_NAMES.iter().enumerate().skip(1) {
@@ -254,6 +296,28 @@ fn settings_path(artifacts: &std::path::Path) -> Option<PathBuf> {
.map(|path| path.join("iw4l/settings.cfg"))
}
pub(crate) fn apply_pad_layout_setting(
settings: Res<frame::GameSettings>,
mut binds: ResMut<KeyBinds>,
mut seen: Local<Option<u8>>,
) {
let layout = settings.pad_layout;
let previous = seen.replace(layout);
if previous.is_none()
|| previous == Some(layout)
|| layout == frame::GameSettings::PAD_LAYOUT_CUSTOM
{
return;
}
binds.apply_pad_layout(usize::from(layout));
}
fn parse_into<T: std::str::FromStr>(value: &str, slot: &mut T) {
if let Ok(value) = value.parse() {
*slot = value;
}
}
fn serialize_settings(settings: &frame::GameSettings, binds: &KeyBinds) -> String {
let safe_name = settings.player_name.replace(['\n', '\r', '='], " ");
let mut lines = vec![
@@ -273,6 +337,16 @@ fn serialize_settings(settings: &frame::GameSettings, binds: &KeyBinds) -> Strin
format!("sensitivity={:.3}", settings.sensitivity),
format!("invert_mouse={}", settings.invert_mouse),
format!("player_name={safe_name}"),
format!("pad_layout={}", settings.pad_layout),
format!("pad_stick_layout={}", settings.pad_stick_layout),
format!("pad_sensitivity={:.2}", settings.pad_sensitivity),
format!("pad_ads_sensitivity={:.2}", settings.pad_ads_sensitivity),
format!("pad_invert={}", settings.pad_invert),
format!("pad_curve={}", settings.pad_curve),
format!("pad_aim_assist={}", settings.pad_aim_assist),
format!("pad_vibration={}", settings.pad_vibration),
format!("pad_deadzone_left={:.2}", settings.pad_deadzone_left),
format!("pad_deadzone_right={:.2}", settings.pad_deadzone_right),
"unbindall".to_owned(),
];
lines.extend(binds.list_lines());
@@ -355,6 +429,16 @@ fn parse_settings(source: &str, settings: &mut frame::GameSettings, binds: &mut
}
}
"player_name" => settings.player_name = value.to_owned(),
"pad_layout" => parse_into(value, &mut settings.pad_layout),
"pad_stick_layout" => parse_into(value, &mut settings.pad_stick_layout),
"pad_sensitivity" => parse_into(value, &mut settings.pad_sensitivity),
"pad_ads_sensitivity" => parse_into(value, &mut settings.pad_ads_sensitivity),
"pad_invert" => parse_into(value, &mut settings.pad_invert),
"pad_curve" => parse_into(value, &mut settings.pad_curve),
"pad_aim_assist" => parse_into(value, &mut settings.pad_aim_assist),
"pad_vibration" => parse_into(value, &mut settings.pad_vibration),
"pad_deadzone_left" => parse_into(value, &mut settings.pad_deadzone_left),
"pad_deadzone_right" => parse_into(value, &mut settings.pad_deadzone_right),
_ => warn!("ignored unknown setting `{key}`"),
}
}
@@ -364,6 +448,10 @@ fn parse_settings(source: &str, settings: &mut frame::GameSettings, binds: &mut
warn!("settings bind: {warning}");
}
}
// Settings saved before controller support carry no controller binds.
if !binds.has_pad_binds() {
binds.apply_pad_layout(usize::from(settings.pad_layout.min(4)));
}
if source
.lines()
.next()
@@ -423,6 +511,16 @@ pub(crate) fn native_menu_settings(
"ui_shadows" => settings.shadows = value == "1",
"ui_dof" => settings.depth_of_field = value == "1",
"ui_bloom" => settings.bloom = value == "1",
"ui_pad_layout" => parse_into(value, &mut settings.pad_layout),
"ui_pad_stick_layout" => parse_into(value, &mut settings.pad_stick_layout),
"ui_pad_sensitivity" => parse_into(value, &mut settings.pad_sensitivity),
"ui_pad_ads_sensitivity" => parse_into(value, &mut settings.pad_ads_sensitivity),
"ui_pad_invert" => settings.pad_invert = value == "1",
"ui_pad_curve" => parse_into(value, &mut settings.pad_curve),
"ui_pad_aim_assist" => parse_into(value, &mut settings.pad_aim_assist),
"ui_pad_vibration" => settings.pad_vibration = value == "1",
"ui_pad_deadzone_left" => parse_into(value, &mut settings.pad_deadzone_left),
"ui_pad_deadzone_right" => parse_into(value, &mut settings.pad_deadzone_right),
_ => continue,
}
settings.sanitize();
@@ -446,4 +544,26 @@ pub(crate) fn native_menu_settings(
if settings.fullscreen { "1" } else { "0" },
);
dvars.set("ui_r_vsync", if settings.vsync { "1" } else { "0" });
dvars.set("ui_pad_layout", settings.pad_layout.to_string());
dvars.set("ui_pad_stick_layout", settings.pad_stick_layout.to_string());
dvars.set("ui_pad_sensitivity", settings.pad_sensitivity.to_string());
dvars.set(
"ui_pad_ads_sensitivity",
settings.pad_ads_sensitivity.to_string(),
);
dvars.set("ui_pad_invert", if settings.pad_invert { "1" } else { "0" });
dvars.set("ui_pad_curve", settings.pad_curve.to_string());
dvars.set("ui_pad_aim_assist", settings.pad_aim_assist.to_string());
dvars.set(
"ui_pad_vibration",
if settings.pad_vibration { "1" } else { "0" },
);
dvars.set(
"ui_pad_deadzone_left",
settings.pad_deadzone_left.to_string(),
);
dvars.set(
"ui_pad_deadzone_right",
settings.pad_deadzone_right.to_string(),
);
}
+9
View File
@@ -250,6 +250,9 @@ pub(super) fn load_weapon(s: &mut ZoneStream<'_>, links: &mut dyn AssetLinkSink)
sprint_loop_time_ms: s.i32_at(body, s.layout(0x2ac, 1012))?,
sprint_drop_time_ms: s.i32_at(body, s.layout(0x2b0, 1016))?,
fuse_time_ms: s.i32_at(body, s.layout(0x2d8, 1056))?,
auto_aim_range: s.f32_at(body, s.layout(0x2e0, 1064))?,
aim_assist_range: s.f32_at(body, s.layout(0x2e4, 1068))?,
aim_assist_range_ads: s.f32_at(body, s.layout(0x2e8, 1072))?,
explosion_radius: s.i32_at(body, s.layout(0x3e8, 1348))?,
explosion_radius_min: s.i32_at(body, s.layout(0x3ec, 1352))?,
explosion_inner_damage: s.i32_at(body, s.layout(0x3f0, 1356))?,
@@ -528,6 +531,9 @@ pub(super) fn load_weapon(s: &mut ZoneStream<'_>, links: &mut dyn AssetLinkSink)
sprint_loop_time_ms: body_facts.sprint_loop_time_ms,
sprint_drop_time_ms: body_facts.sprint_drop_time_ms,
fuse_time_ms: body_facts.fuse_time_ms,
auto_aim_range: body_facts.auto_aim_range,
aim_assist_range: body_facts.aim_assist_range,
aim_assist_range_ads: body_facts.aim_assist_range_ads,
cook_off_hold: body_facts.cook_off_hold,
clip_only: body_facts.clip_only,
timed_detonation: body_facts.timed_detonation,
@@ -665,6 +671,9 @@ struct WeapDefScalars {
sprint_loop_time_ms: i32,
sprint_drop_time_ms: i32,
fuse_time_ms: i32,
auto_aim_range: f32,
aim_assist_range: f32,
aim_assist_range_ads: f32,
cook_off_hold: bool,
+3
View File
@@ -1253,6 +1253,9 @@ pub struct WeaponGeometry {
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,
+2
View File
@@ -1,4 +1,5 @@
pub mod class_presets;
pub mod pad;
pub mod retire;
pub mod schedule;
pub mod script_entity_notify;
@@ -8,6 +9,7 @@ pub mod settings;
pub mod ui;
pub use class_presets::{ClassPreset, pick_showcase, showcase_classes};
pub use pad::ActivePad;
pub use retire::Retiring;
pub use schedule::{
AUTHORITY_TOC, AuthorityBookkeeping, AuthorityEdge, AuthoritySet, CLIENT_TOC,
+6
View File
@@ -0,0 +1,6 @@
use bevy::prelude::{Entity, Resource};
/// The last controller pressed or moved, so a virtual joystick or an idle
/// second pad can stay connected without taking over.
#[derive(Resource, Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ActivePad(pub Option<Entity>);
+50
View File
@@ -38,6 +38,22 @@ pub struct GameSettings {
pub invert_mouse: bool,
pub player_name: String,
/// 0 default, 1 tactical, 2 lefty, 3 bumper jumper, 4 bumper jumper
/// tactical, or `PAD_LAYOUT_CUSTOM` once rebound.
pub pad_layout: u8,
/// 0 default, 1 southpaw, 2 legacy, 3 legacy southpaw.
pub pad_stick_layout: u8,
pub pad_sensitivity: f32,
pub pad_ads_sensitivity: f32,
pub pad_invert: bool,
/// 0 standard, 1 linear, 2 dynamic.
pub pad_curve: u8,
/// 0 off, 1 slowdown and lock-on, 2 also auto aim.
pub pad_aim_assist: u8,
pub pad_vibration: bool,
pub pad_deadzone_left: f32,
pub pad_deadzone_right: f32,
pub revision: u64,
}
@@ -56,6 +72,16 @@ impl Default for GameSettings {
sensitivity: 5.0,
invert_mouse: false,
player_name: "Player".to_owned(),
pad_layout: 0,
pad_stick_layout: 0,
pad_sensitivity: Self::PAD_SENSITIVITY_DEFAULT,
pad_ads_sensitivity: 1.0,
pad_invert: false,
pad_curve: 0,
pad_aim_assist: 1,
pad_vibration: true,
pad_deadzone_left: 0.12,
pad_deadzone_right: 0.12,
revision: 0,
}
}
@@ -65,6 +91,8 @@ impl GameSettings {
pub const FOV_DEFAULT: f32 = 65.0;
pub const FOV_MIN: f32 = 65.0;
pub const FOV_MAX: f32 = 120.0;
pub const PAD_SENSITIVITY_DEFAULT: f32 = 3.0;
pub const PAD_LAYOUT_CUSTOM: u8 = 255;
pub fn touch(&mut self) {
self.revision = self.revision.wrapping_add(1);
@@ -85,6 +113,28 @@ impl GameSettings {
};
self.master_volume = self.master_volume.clamp(0.0, 1.0);
self.sensitivity = self.sensitivity.clamp(0.1, 30.0);
if self.pad_layout != Self::PAD_LAYOUT_CUSTOM {
self.pad_layout = self.pad_layout.min(4);
}
self.pad_stick_layout = self.pad_stick_layout.min(3);
self.pad_curve = self.pad_curve.min(2);
self.pad_aim_assist = self.pad_aim_assist.min(2);
let finite = |v: f32, lo: f32, hi: f32, default: f32| {
if v.is_finite() {
v.clamp(lo, hi)
} else {
default
}
};
self.pad_sensitivity = finite(
self.pad_sensitivity,
1.0,
10.0,
Self::PAD_SENSITIVITY_DEFAULT,
);
self.pad_ads_sensitivity = finite(self.pad_ads_sensitivity, 0.5, 1.5, 1.0);
self.pad_deadzone_left = finite(self.pad_deadzone_left, 0.0, 0.4, 0.12);
self.pad_deadzone_right = finite(self.pad_deadzone_right, 0.0, 0.4, 0.12);
self.player_name = self.player_name.trim().chars().take(16).collect();
if self.player_name.is_empty() {
self.player_name = "Player".to_owned();
+8 -6
View File
@@ -661,15 +661,17 @@ fn handle_input(
};
if pressed.down {
runner.focus_step(&top, 1, false);
runner.focus_nav(&top, 0, 1);
}
if pressed.up {
runner.focus_step(&top, -1, false);
runner.focus_nav(&top, 0, -1);
}
if (pressed.left || pressed.right)
&& let Some(focus) = runner.menus.stack.last().and_then(|m| m.focus)
{
runner.adjust(&top, focus, if pressed.left { -1 } else { 1 });
if pressed.left || pressed.right {
let step = if pressed.left { -1 } else { 1 };
let focus = runner.menus.stack.last().and_then(|m| m.focus);
if !focus.is_some_and(|focus| runner.adjust(&top, focus, step)) {
runner.focus_nav(&top, step, 0);
}
return;
}
if pressed.enter {
+89
View File
@@ -484,6 +484,77 @@ impl Runner<'_, '_> {
self.set_focus(menu, next);
}
/// Menu files list items in no particular screen order, so focus moves
/// by position: within a column vertically, across columns sideways.
pub(crate) fn focus_nav(&mut self, menu: &str, dx: i32, dy: i32) -> bool {
let catalog = self.catalog;
let Some(def) = catalog.get(menu) else {
return false;
};
let current = self.menus.get_mut(menu).and_then(|m| m.focus);
let visible = self.visible_items(menu);
let candidates: Vec<(usize, [f32; 4])> = (0..def.items.len())
.filter(|&i| is_focusable(&def.items[i]) && visible.contains(&i))
.map(|i| (i, nav_rect(&def.items[i])))
.collect();
let Some(&(at, here)) = current.and_then(|c| candidates.iter().find(|(i, _)| *i == c))
else {
if dx != 0 {
return false;
}
self.focus_step(menu, dy, true);
return true;
};
let centre = |r: [f32; 4]| [(r[0] + r[2]) * 0.5, (r[1] + r[3]) * 0.5];
let c = centre(here);
let same_column = |r: [f32; 4]| {
let o = centre(r);
(r[0] <= c[0] && c[0] <= r[2]) || (here[0] <= o[0] && o[0] <= here[2])
};
let others = candidates.iter().filter(|(i, _)| *i != at);
let next = if dy != 0 {
let dir = dy.signum() as f32;
let column: Vec<_> = others.filter(|(_, r)| same_column(*r)).collect();
let ahead = column
.iter()
.filter(|(_, r)| (centre(*r)[1] - c[1]) * dir > 0.5)
.min_by(|a, b| {
let da = (centre(a.1)[1] - c[1]).abs();
let db = (centre(b.1)[1] - c[1]).abs();
da.total_cmp(&db)
});
let wrapped = || {
column
.iter()
.min_by(|a, b| (centre(a.1)[1] * dir).total_cmp(&(centre(b.1)[1] * dir)))
};
match ahead.or_else(wrapped) {
Some((index, _)) => *index,
None => {
self.focus_step(menu, dy, false);
return true;
}
}
} else {
let dir = dx.signum() as f32;
let Some((index, _)) = others
.filter(|(_, r)| !same_column(*r) && (centre(*r)[0] - c[0]) * dir > 0.0)
.min_by(|a, b| {
let score = |r: [f32; 4]| {
let o = centre(r);
(o[1] - c[1]).abs() * 4.0 + (o[0] - c[0]).abs()
};
score(a.1).total_cmp(&score(b.1))
})
else {
return false;
};
*index
};
self.set_focus(menu, next);
true
}
pub(crate) fn visible_items(&mut self, menu: &str) -> Vec<usize> {
let Some(def) = self.catalog.get(menu) else {
return Vec::new();
@@ -619,3 +690,21 @@ impl Runner<'_, '_> {
self.run_events(menu, Some(index), events);
}
}
/// Left, top, right, bottom on the 640 by 480 virtual screen.
fn nav_rect(item: &asset_game::MenuItem) -> [f32; 4] {
let r = &item.rect;
let ox = match r.horz_align {
2 => 320.0,
3 => 640.0,
_ => 0.0,
};
let oy = match r.vert_align {
2 => 240.0,
3 => 480.0,
_ => 0.0,
};
let (x0, x1) = (r.x + ox, r.x + ox + r.w);
let (y0, y1) = (r.y + oy, r.y + oy + r.h);
[x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1)]
}
+34 -4
View File
@@ -48,6 +48,21 @@ pub struct ClientActionInput {
pub cl_yawspeed: f32,
pub now_msec: i32,
pub frame_msec: u32,
/// Forward and right.
pub pad_move: [f32; 2],
/// Right and up.
pub pad_look: [f32; 2],
pub pad_deflection: f32,
pub pad_sensitivity: f32,
pub pad_ads_sensitivity: f32,
pub pad_turn_rate: [f32; 2],
/// Pitch and yaw, degrees.
pub pad_look_delta: [f32; 2],
pub pad_aim_assist: u8,
pub pad_lockon: Option<u64>,
pub pad_autoaim: Option<(u64, f32)>,
pub pad_was_ads: bool,
}
impl Default for ClientActionInput {
@@ -70,6 +85,17 @@ impl Default for ClientActionInput {
cl_yawspeed: CL_YAWSPEED_DEFAULT,
now_msec: 16,
frame_msec: 16,
pad_move: [0.0; 2],
pad_look: [0.0; 2],
pad_deflection: 0.0,
pad_sensitivity: 1.0,
pad_ads_sensitivity: 1.0,
pad_turn_rate: [0.0; 2],
pad_look_delta: [0.0; 2],
pad_aim_assist: 0,
pad_lockon: None,
pad_autoaim: None,
pad_was_ads: false,
}
}
}
@@ -140,15 +166,19 @@ pub fn build_usercmd(input: &mut ClientActionInput, look: &LookState, server_tim
input.fov_scale,
);
let (mouse_pitch, mouse_yaw) = mouse_move_angles(mx, my, input.m_yaw, input.m_pitch);
let pad_pitch = (input.pad_look_delta[0] * ANGLE2SHORT) as i32;
let pad_yaw = (input.pad_look_delta[1] * ANGLE2SHORT) as i32;
let forward = (axes.forward + input.pad_move[0]).clamp(-1.0, 1.0);
let right = (axes.right + input.pad_move[1]).clamp(-1.0, 1.0);
create_cmd(&CreateCmdInput {
server_time,
angles: look.angles,
buttons: bits,
forwardmove: axis_to_move(axes.forward),
rightmove: axis_to_move(axes.right),
mouse_pitch_delta: mouse_pitch,
mouse_yaw_delta: mouse_yaw,
forwardmove: axis_to_move(forward),
rightmove: axis_to_move(right),
mouse_pitch_delta: mouse_pitch + pad_pitch,
mouse_yaw_delta: mouse_yaw + pad_yaw,
key_pitch_delta: 0,
key_yaw_delta: 0,
frozen: false,
+1
View File
@@ -1,6 +1,7 @@
pub mod frame_census;
pub mod frame_clock;
pub mod input;
pub mod pad_aim;
pub mod predict;
pub mod predicted_error;
pub mod presentation;
+272
View File
@@ -0,0 +1,272 @@
use super::input::ClientActionInput;
/// Degrees a second at full deflection.
const TURN_YAW: f32 = 260.0;
const TURN_PITCH: f32 = 90.0;
const TURN_YAW_ADS: f32 = 90.0;
const TURN_PITCH_ADS: f32 = 55.0;
/// Degrees a second squared.
const TURN_ACCEL: f32 = 1200.0;
const SLOWDOWN_HIP: f32 = 0.4;
const SLOWDOWN_ADS: f32 = 0.5;
/// Full width and height on the 640 by 480 virtual screen.
const SLOWDOWN_REGION: [f32; 2] = [90.0, 90.0];
const LOCKON_REGION: [f32; 2] = [90.0, 90.0];
const AUTOAIM_REGION: [f32; 2] = [160.0, 120.0];
const LOCKON_STRENGTH: f32 = 0.6;
const LOCKON_DEFLECTION: f32 = 0.05;
/// Degrees a second, for at most `AUTOAIM_TIME` seconds.
const AUTOAIM_LERP: f32 = 40.0;
const AUTOAIM_TIME: f32 = 0.5;
const FALLBACK_RANGE: f32 = 1500.0;
#[derive(Clone, Copy, Debug)]
pub struct AimTarget {
pub key: u64,
pub mins: [f32; 3],
pub maxs: [f32; 3],
pub aim: [f32; 3],
pub velocity: [f32; 3],
}
#[derive(Clone, Copy, Debug)]
pub struct AimView {
pub eye: [f32; 3],
/// Pitch and yaw, degrees.
pub angles: [f32; 2],
pub velocity: [f32; 3],
pub ads_lerp: f32,
/// Against a 65 degree field of view.
pub fov_scale: f32,
pub ranges: weapon_iw4::AimAssistRanges,
pub dt: f32,
}
struct OnScreen {
target: AimTarget,
min: [f32; 2],
max: [f32; 2],
dist_sqr: f32,
crosshair_sqr: f32,
}
fn axes(angles: [f32; 2]) -> ([f32; 3], [f32; 3], [f32; 3]) {
let (sp, cp) = angles[0].to_radians().sin_cos();
let (sy, cy) = angles[1].to_radians().sin_cos();
let forward = [cp * cy, cp * sy, -sp];
let right = [sy, -cy, 0.0];
let up = [sp * cy, sp * sy, cp];
(forward, right, up)
}
fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn angles_to(eye: [f32; 3], point: [f32; 3]) -> [f32; 2] {
let d = [point[0] - eye[0], point[1] - eye[1], point[2] - eye[2]];
let flat = (d[0] * d[0] + d[1] * d[1]).sqrt();
[
-d[2].atan2(flat).to_degrees(),
d[1].atan2(d[0]).to_degrees(),
]
}
fn angle_delta(to: f32, from: f32) -> f32 {
(to - from + 540.0).rem_euclid(360.0) - 180.0
}
fn lerp(a: f32, b: f32, t: f32) -> f32 {
a + (b - a) * t
}
fn project(view: &AimView, targets: &[AimTarget]) -> Vec<OnScreen> {
let (forward, right, up) = axes(view.angles);
let tan_x = (32.5f32.to_radians().tan() * view.fov_scale).max(1e-3);
let tan_y = tan_x * 9.0 / 16.0;
let mut out: Vec<OnScreen> = targets
.iter()
.filter_map(|target| {
let mut min = [f32::MAX; 2];
let mut max = [f32::MIN; 2];
for corner in 0..8 {
let p = [
if corner & 1 == 0 {
target.mins[0]
} else {
target.maxs[0]
},
if corner & 2 == 0 {
target.mins[1]
} else {
target.maxs[1]
},
if corner & 4 == 0 {
target.mins[2]
} else {
target.maxs[2]
},
];
let d = [p[0] - view.eye[0], p[1] - view.eye[1], p[2] - view.eye[2]];
let depth = dot(d, forward);
if depth <= 0.0 {
continue;
}
let x = dot(d, right) / (depth * tan_x);
let y = -dot(d, up) / (depth * tan_y);
min = [min[0].min(x), min[1].min(y)];
max = [max[0].max(x), max[1].max(y)];
}
if max[0] <= min[0]
|| max[1] <= min[1]
|| min[0] > 1.0
|| min[1] > 1.0
|| max[0] < -1.0
|| max[1] < -1.0
{
return None;
}
let min = [min[0].clamp(-1.0, 1.0), min[1].clamp(-1.0, 1.0)];
let max = [max[0].clamp(-1.0, 1.0), max[1].clamp(-1.0, 1.0)];
let centre = [(min[0] + max[0]) * 0.5, (min[1] + max[1]) * 0.5];
let d = [
target.aim[0] - view.eye[0],
target.aim[1] - view.eye[1],
target.aim[2] - view.eye[2],
];
Some(OnScreen {
target: *target,
min,
max,
dist_sqr: dot(d, d),
crosshair_sqr: centre[0] * centre[0] + centre[1] * centre[1],
})
})
.collect();
out.sort_by(|a, b| a.crosshair_sqr.total_cmp(&b.crosshair_sqr));
out
}
fn best(screen: &[OnScreen], range: f32, region: [f32; 2], scale: f32) -> Option<&OnScreen> {
let half = [region[0] / 640.0 * scale, region[1] / 480.0 * scale];
screen.iter().find(|s| {
s.dist_sqr <= range * range
&& s.min[0] <= half[0]
&& s.max[0] >= -half[0]
&& s.min[1] <= half[1]
&& s.max[1] >= -half[1]
})
}
pub fn pad_look_frame(
input: &mut ClientActionInput,
view: &AimView,
targets: &[AimTarget],
ads: bool,
) {
let dt = view.dt;
let ads_lerp = view.ads_lerp.clamp(0.0, 1.0);
let sensitivity = input.pad_sensitivity * lerp(1.0, input.pad_ads_sensitivity, ads_lerp);
let zoom = view.fov_scale.clamp(0.02, 2.0);
let max_yaw = lerp(TURN_YAW, TURN_YAW_ADS, ads_lerp) * zoom * sensitivity;
let max_pitch = lerp(TURN_PITCH, TURN_PITCH_ADS, ads_lerp) * zoom * sensitivity;
let wanted = [-input.pad_look[1] * max_pitch, -input.pad_look[0] * max_yaw];
for (rate, goal) in input.pad_turn_rate.iter_mut().zip(wanted) {
if (goal.abs() > rate.abs() && goal.signum() == rate.signum())
|| (*rate == 0.0 && goal != 0.0)
{
let step = TURN_ACCEL * zoom * dt;
*rate = if goal > *rate {
(*rate + step).min(goal)
} else {
(*rate - step).max(goal)
};
} else {
*rate = goal;
}
}
let mut delta = [input.pad_turn_rate[0] * dt, input.pad_turn_rate[1] * dt];
let mode = input.pad_aim_assist;
if mode == 0 {
input.pad_lockon = None;
input.pad_autoaim = None;
input.pad_was_ads = ads;
input.pad_look_delta = delta;
return;
}
let screen = project(view, targets);
let range = |hip: f32, ads: f32| {
let hip = if hip > 0.0 { hip } else { FALLBACK_RANGE };
let ads = if ads > 0.0 { ads } else { hip };
lerp(hip, ads, ads_lerp)
};
let assist_range = range(view.ranges.hip, view.ranges.ads);
if best(&screen, assist_range, SLOWDOWN_REGION, 1.0).is_some() {
let slow = lerp(SLOWDOWN_HIP, SLOWDOWN_ADS, ads_lerp);
delta[0] *= slow;
delta[1] *= slow;
}
let kept = input
.pad_lockon
.and_then(|key| screen.iter().find(|s| s.target.key == key))
.filter(|s| best(std::slice::from_ref(*s), assist_range, LOCKON_REGION, 1.0).is_some());
let lock = kept.or_else(|| best(&screen, assist_range, LOCKON_REGION, 1.0));
input.pad_lockon = lock.map(|s| s.target.key);
if let Some(lock) = lock
&& input.pad_autoaim.is_none()
&& input.pad_deflection > LOCKON_DEFLECTION
{
let t = lock.target;
let moved = [
t.aim[0] + (t.velocity[0] - view.velocity[0]) * dt,
t.aim[1] + (t.velocity[1] - view.velocity[1]) * dt,
t.aim[2] + (t.velocity[2] - view.velocity[2]) * dt,
];
let now = angles_to(view.eye, t.aim);
let next = angles_to(view.eye, moved);
delta[0] += LOCKON_STRENGTH * angle_delta(next[0], now[0]);
delta[1] += LOCKON_STRENGTH * angle_delta(next[1], now[1]);
}
if mode == 2 && ads && !input.pad_was_ads {
let auto_range = if view.ranges.auto_aim > 0.0 {
view.ranges.auto_aim
} else {
FALLBACK_RANGE
};
input.pad_autoaim = best(&screen, auto_range, AUTOAIM_REGION, (1.0 / zoom).max(1.0))
.map(|s| (s.target.key, AUTOAIM_TIME));
}
if !ads {
input.pad_autoaim = None;
}
if let Some((key, left)) = input.pad_autoaim {
match screen
.iter()
.find(|s| s.target.key == key)
.filter(|_| left > 0.0)
{
Some(s) => {
let goal = angles_to(view.eye, s.target.aim);
let pitch = angle_delta(goal[0], view.angles[0]);
let yaw = angle_delta(goal[1], view.angles[1]);
let length = pitch.hypot(yaw);
let step = (AUTOAIM_LERP * dt).min(length);
if length > 1e-4 {
delta[0] += pitch / length * step;
delta[1] += yaw / length * step;
}
input.pad_autoaim = (length > 0.25).then_some((key, left - dt));
}
None => input.pad_autoaim = None,
}
}
input.pad_was_ads = ads;
input.pad_look_delta = delta;
}
+90
View File
@@ -801,6 +801,91 @@ pub fn sample_client_input(
if frozen {
actions.mouse_x = 0.0;
actions.mouse_y = 0.0;
actions.pad_move = [0.0; 2];
actions.pad_look = [0.0; 2];
}
if let Some(ps) = ps {
let world = prediction.0.world();
let eye = [
ps.origin[0],
ps.origin[1],
ps.origin[2] + ps.view_height_current,
];
let visible = |point: [f32; 3]| {
let hit =
world.trace_world(eye, point, [0.0; 3], [0.0; 3], hud_iw4::OVERHEAD_TRACE_MASK);
hit.fraction >= 1.0 && hit.startsolid == 0
};
let angles = [
look.angles[0] as f32 / input_iw4::ANGLE2SHORT,
look.angles[1] as f32 / input_iw4::ANGLE2SHORT,
];
let (sp, cp) = angles[0].to_radians().sin_cos();
let (sy, cy) = angles[1].to_radians().sin_cos();
let forward = [cp * cy, cp * sy, -sp];
let in_front = |origin: [f32; 3], radius: f32| {
let d = [
origin[0] - ps.origin[0],
origin[1] - ps.origin[1],
origin[2] - ps.origin[2],
];
d[0] * forward[0] + d[1] * forward[1] + d[2] * forward[2] + radius >= 0.0
};
let mut targets: Vec<crate::client::pad_aim::AimTarget> = Vec::new();
if actions.pad_aim_assist > 0 {
const RADIUS: f32 = 10.0;
let snapshot = presented.snapshot();
let team = |id: sim::ClientId| {
snapshot
.and_then(|s| s.meta.for_client(id))
.map(|m| m.client_state_team)
};
let teams = snapshot.is_some_and(|s| s.meta.kind.is_team());
let mine = team(local.0);
for id in snapshot
.into_iter()
.flat_map(|s| s.players.iter().map(|(id, _)| *id))
{
if id == local.0 || (teams && team(id) == mine) {
continue;
}
let Some(other) = presented.player(id).filter(|o| o.pm_type == 0) else {
continue;
};
let o = other.origin;
let head = [o[0], o[1], o[2] + other.view_height_current];
if !in_front(o, RADIUS) || !visible(head) {
continue;
}
let top = other.view_height_current + 8.0;
targets.push(crate::client::pad_aim::AimTarget {
key: u64::from(id.0),
mins: [o[0] - RADIUS, o[1] - RADIUS, o[2]],
maxs: [o[0] + RADIUS, o[1] + RADIUS, o[2] + top],
aim: [o[0], o[1], o[2] + top * 0.75],
velocity: other.velocity,
});
}
}
let ranges = world
.weapon_combat_row(playerstate_iw4::get_viewmodel_weapon_index(ps))
.map_or(weapon_iw4::AimAssistRanges::NONE, |facts| facts.aim_assist);
let view = crate::client::pad_aim::AimView {
eye,
angles,
velocity: ps.velocity,
ads_lerp: ps.f_weapon_pos_frac,
fov_scale: actions.fov_scale.max(0.01),
ranges,
dt: cls.frametime_secs(),
};
let ads = actions.client.using_ads || actions.client.kb.speed.active;
crate::client::pad_aim::pad_look_frame(&mut actions, &view, &targets, ads);
} else {
actions.pad_look_delta = [0.0; 2];
}
if frozen {
actions.pad_look_delta = [0.0; 2];
}
let choose_direction = presented
.snapshot()
@@ -983,6 +1068,11 @@ pub fn sample_client_input(
}
}
let mut cmd = build_usercmd(&mut actions, &look, 0);
if cmd.buttons & playerstate_iw4::buttons::USE_RELOAD != 0
&& ps.is_some_and(|ps| ps.cursor_hint == 0)
{
cmd.buttons |= playerstate_iw4::buttons::RELOAD;
}
look.angles = cmd.angles;
if let Some((mouse_x, mouse_y)) = remote_mouse {
cmd.remote_control = remote_control_axes(&actions, mouse_x, mouse_y);
+5
View File
@@ -163,6 +163,11 @@ pub fn from_registry(
WeaponCombatFacts::none()
});
facts.alternate_weapon = weapons.alternate_of(i as u32);
facts.aim_assist = weapon_iw4::AimAssistRanges {
auto_aim: f.auto_aim_range,
hip: f.aim_assist_range,
ads: f.aim_assist_range_ads,
};
facts
})
.collect();
+866
View File
@@ -0,0 +1,866 @@
[
{
"name": "options_controller",
"base": "pc_options_video",
"fields": {
"handlers": {
"open": [
{
"Script": "focusFirst;"
}
],
"close": [],
"esc": [
{
"Script": "close self;"
}
]
}
},
"item_overrides": [
{
"index": 22,
"fields": {
"vis_exp": "0"
}
},
{
"index": 23,
"fields": {
"vis_exp": "0"
}
},
{
"index": 24,
"fields": {
"vis_exp": "0"
}
},
{
"index": 25,
"fields": {
"vis_exp": "0"
}
},
{
"index": 26,
"fields": {
"vis_exp": "0"
}
},
{
"index": 27,
"fields": {
"vis_exp": "0"
}
},
{
"index": 28,
"fields": {
"vis_exp": "0"
}
},
{
"index": 29,
"fields": {
"vis_exp": "0"
}
},
{
"index": 30,
"fields": {
"vis_exp": "0"
}
},
{
"index": 31,
"fields": {
"vis_exp": "0"
}
},
{
"index": 32,
"fields": {
"vis_exp": "0"
}
},
{
"index": 33,
"fields": {
"vis_exp": "0"
}
},
{
"index": 34,
"fields": {
"vis_exp": "0"
}
},
{
"index": 21,
"fields": {
"text_key": "Controller"
}
}
],
"item_copies": [
{
"index": 27,
"fields": {
"item_type": 12,
"static_flags": 0,
"dvar": "ui_pad_layout",
"choices": [
[
"Default",
"0"
],
[
"Tactical",
"1"
],
[
"Lefty",
"2"
],
[
"Bumper Jumper",
"3"
],
[
"Bumper Jumper Tactical",
"4"
],
[
"Custom",
"255"
]
],
"vis_exp": "1",
"disabled_exp": "0",
"text_key": "",
"text_exp": "",
"text_align_mode": 10,
"text_align_x": -8,
"text_align_y": 0,
"fore_color": [
1,
1,
1,
1
],
"rect": {
"x": 232,
"y": 28,
"w": 344,
"h": 20
},
"handlers": {
"action": [
{
"Script": "play mouse_click;"
}
],
"focus": [
{
"Script": "play mouse_over; setItemColor self backcolor 0 0 0 1;"
}
],
"leave_focus": [
{
"Script": "setItemColor self backcolor 0 0 0 0;"
}
]
},
"text_scale": 0.375,
"style": 1,
"background": "menu_button_selection_bar",
"back_color": [
0,
0,
0,
0
]
}
},
{
"index": 28,
"fields": {
"vis_exp": "1",
"text_key": "Button Layout",
"rect": {
"x": 232,
"y": 28,
"w": 344,
"h": 20
},
"text_scale": 0.32
}
},
{
"index": 27,
"fields": {
"item_type": 12,
"static_flags": 0,
"dvar": "ui_pad_stick_layout",
"choices": [
[
"Default",
"0"
],
[
"Southpaw",
"1"
],
[
"Legacy",
"2"
],
[
"Legacy Southpaw",
"3"
]
],
"vis_exp": "1",
"disabled_exp": "0",
"text_key": "",
"text_exp": "",
"text_align_mode": 10,
"text_align_x": -8,
"text_align_y": 0,
"fore_color": [
1,
1,
1,
1
],
"rect": {
"x": 232,
"y": 48,
"w": 344,
"h": 20
},
"handlers": {
"action": [
{
"Script": "play mouse_click;"
}
],
"focus": [
{
"Script": "play mouse_over; setItemColor self backcolor 0 0 0 1;"
}
],
"leave_focus": [
{
"Script": "setItemColor self backcolor 0 0 0 0;"
}
]
},
"text_scale": 0.375,
"style": 1,
"background": "menu_button_selection_bar",
"back_color": [
0,
0,
0,
0
]
}
},
{
"index": 28,
"fields": {
"vis_exp": "1",
"text_key": "Stick Layout",
"rect": {
"x": 232,
"y": 48,
"w": 344,
"h": 20
},
"text_scale": 0.32
}
},
{
"index": 24,
"fields": {
"item_type": 10,
"static_flags": 0,
"dvar": "ui_pad_sensitivity",
"choices": [],
"vis_exp": "1",
"disabled_exp": "0",
"text_key": "",
"text_exp": "",
"text_align_mode": 10,
"text_align_x": -8,
"text_align_y": 0,
"fore_color": [
1,
1,
1,
1
],
"rect": {
"x": 420,
"y": 68,
"w": 112,
"h": 20
},
"handlers": {
"focus": [
{
"Script": "play mouse_over;"
}
]
},
"text_scale": 0.375,
"style": 1,
"background": "",
"back_color": [
0,
0,
0,
0
],
"slider": {
"min": 1,
"max": 10,
"step": 0.5
}
}
},
{
"index": 29,
"fields": {
"text_key": "Look Sensitivity",
"vis_exp": "1",
"rect": {
"x": 232,
"y": 68,
"w": 344,
"h": 20
}
}
},
{
"index": 24,
"fields": {
"item_type": 10,
"static_flags": 0,
"dvar": "ui_pad_ads_sensitivity",
"choices": [],
"vis_exp": "1",
"disabled_exp": "0",
"text_key": "",
"text_exp": "",
"text_align_mode": 10,
"text_align_x": -8,
"text_align_y": 0,
"fore_color": [
1,
1,
1,
1
],
"rect": {
"x": 420,
"y": 88,
"w": 112,
"h": 20
},
"handlers": {
"focus": [
{
"Script": "play mouse_over;"
}
]
},
"text_scale": 0.375,
"style": 1,
"background": "",
"back_color": [
0,
0,
0,
0
],
"slider": {
"min": 0.5,
"max": 1.5,
"step": 0.05,
"display_range": [
50,
150
],
"suffix": "%"
}
}
},
{
"index": 29,
"fields": {
"text_key": "ADS Sensitivity",
"vis_exp": "1",
"rect": {
"x": 232,
"y": 88,
"w": 344,
"h": 20
}
}
},
{
"index": 27,
"fields": {
"item_type": 12,
"static_flags": 0,
"dvar": "ui_pad_invert",
"choices": [
[
"@MENU_DISABLED",
"0"
],
[
"@MENU_ENABLED",
"1"
]
],
"vis_exp": "1",
"disabled_exp": "0",
"text_key": "",
"text_exp": "",
"text_align_mode": 10,
"text_align_x": -8,
"text_align_y": 0,
"fore_color": [
1,
1,
1,
1
],
"rect": {
"x": 232,
"y": 108,
"w": 344,
"h": 20
},
"handlers": {
"action": [
{
"Script": "play mouse_click;"
}
],
"focus": [
{
"Script": "play mouse_over; setItemColor self backcolor 0 0 0 1;"
}
],
"leave_focus": [
{
"Script": "setItemColor self backcolor 0 0 0 0;"
}
]
},
"text_scale": 0.375,
"style": 1,
"background": "menu_button_selection_bar",
"back_color": [
0,
0,
0,
0
]
}
},
{
"index": 28,
"fields": {
"vis_exp": "1",
"text_key": "Look Inversion",
"rect": {
"x": 232,
"y": 108,
"w": 344,
"h": 20
},
"text_scale": 0.32
}
},
{
"index": 27,
"fields": {
"item_type": 12,
"static_flags": 0,
"dvar": "ui_pad_curve",
"choices": [
[
"Standard",
"0"
],
[
"Linear",
"1"
],
[
"Dynamic",
"2"
]
],
"vis_exp": "1",
"disabled_exp": "0",
"text_key": "",
"text_exp": "",
"text_align_mode": 10,
"text_align_x": -8,
"text_align_y": 0,
"fore_color": [
1,
1,
1,
1
],
"rect": {
"x": 232,
"y": 128,
"w": 344,
"h": 20
},
"handlers": {
"action": [
{
"Script": "play mouse_click;"
}
],
"focus": [
{
"Script": "play mouse_over; setItemColor self backcolor 0 0 0 1;"
}
],
"leave_focus": [
{
"Script": "setItemColor self backcolor 0 0 0 0;"
}
]
},
"text_scale": 0.375,
"style": 1,
"background": "menu_button_selection_bar",
"back_color": [
0,
0,
0,
0
]
}
},
{
"index": 28,
"fields": {
"vis_exp": "1",
"text_key": "Response Curve",
"rect": {
"x": 232,
"y": 128,
"w": 344,
"h": 20
},
"text_scale": 0.32
}
},
{
"index": 27,
"fields": {
"item_type": 12,
"static_flags": 0,
"dvar": "ui_pad_aim_assist",
"choices": [
[
"Off",
"0"
],
[
"Standard",
"1"
],
[
"Full",
"2"
]
],
"vis_exp": "1",
"disabled_exp": "0",
"text_key": "",
"text_exp": "",
"text_align_mode": 10,
"text_align_x": -8,
"text_align_y": 0,
"fore_color": [
1,
1,
1,
1
],
"rect": {
"x": 232,
"y": 148,
"w": 344,
"h": 20
},
"handlers": {
"action": [
{
"Script": "play mouse_click;"
}
],
"focus": [
{
"Script": "play mouse_over; setItemColor self backcolor 0 0 0 1;"
}
],
"leave_focus": [
{
"Script": "setItemColor self backcolor 0 0 0 0;"
}
]
},
"text_scale": 0.375,
"style": 1,
"background": "menu_button_selection_bar",
"back_color": [
0,
0,
0,
0
]
}
},
{
"index": 28,
"fields": {
"vis_exp": "1",
"text_key": "Aim Assist",
"rect": {
"x": 232,
"y": 148,
"w": 344,
"h": 20
},
"text_scale": 0.32
}
},
{
"index": 27,
"fields": {
"item_type": 12,
"static_flags": 0,
"dvar": "ui_pad_vibration",
"choices": [
[
"@MENU_DISABLED",
"0"
],
[
"@MENU_ENABLED",
"1"
]
],
"vis_exp": "1",
"disabled_exp": "0",
"text_key": "",
"text_exp": "",
"text_align_mode": 10,
"text_align_x": -8,
"text_align_y": 0,
"fore_color": [
1,
1,
1,
1
],
"rect": {
"x": 232,
"y": 168,
"w": 344,
"h": 20
},
"handlers": {
"action": [
{
"Script": "play mouse_click;"
}
],
"focus": [
{
"Script": "play mouse_over; setItemColor self backcolor 0 0 0 1;"
}
],
"leave_focus": [
{
"Script": "setItemColor self backcolor 0 0 0 0;"
}
]
},
"text_scale": 0.375,
"style": 1,
"background": "menu_button_selection_bar",
"back_color": [
0,
0,
0,
0
]
}
},
{
"index": 28,
"fields": {
"vis_exp": "1",
"text_key": "Vibration",
"rect": {
"x": 232,
"y": 168,
"w": 344,
"h": 20
},
"text_scale": 0.32
}
},
{
"index": 24,
"fields": {
"item_type": 10,
"static_flags": 0,
"dvar": "ui_pad_deadzone_left",
"choices": [],
"vis_exp": "1",
"disabled_exp": "0",
"text_key": "",
"text_exp": "",
"text_align_mode": 10,
"text_align_x": -8,
"text_align_y": 0,
"fore_color": [
1,
1,
1,
1
],
"rect": {
"x": 420,
"y": 188,
"w": 112,
"h": 20
},
"handlers": {
"focus": [
{
"Script": "play mouse_over;"
}
]
},
"text_scale": 0.375,
"style": 1,
"background": "",
"back_color": [
0,
0,
0,
0
],
"slider": {
"min": 0,
"max": 0.4,
"step": 0.01,
"display_range": [
0,
40
],
"suffix": "%"
}
}
},
{
"index": 29,
"fields": {
"text_key": "Left Stick Deadzone",
"vis_exp": "1",
"rect": {
"x": 232,
"y": 188,
"w": 344,
"h": 20
}
}
},
{
"index": 24,
"fields": {
"item_type": 10,
"static_flags": 0,
"dvar": "ui_pad_deadzone_right",
"choices": [],
"vis_exp": "1",
"disabled_exp": "0",
"text_key": "",
"text_exp": "",
"text_align_mode": 10,
"text_align_x": -8,
"text_align_y": 0,
"fore_color": [
1,
1,
1,
1
],
"rect": {
"x": 420,
"y": 208,
"w": 112,
"h": 20
},
"handlers": {
"focus": [
{
"Script": "play mouse_over;"
}
]
},
"text_scale": 0.375,
"style": 1,
"background": "",
"back_color": [
0,
0,
0,
0
],
"slider": {
"min": 0,
"max": 0.4,
"step": 0.01,
"display_range": [
0,
40
],
"suffix": "%"
}
}
},
{
"index": 29,
"fields": {
"text_key": "Right Stick Deadzone",
"vis_exp": "1",
"rect": {
"x": 232,
"y": 208,
"w": 344,
"h": 20
}
}
}
]
}
]
+2
View File
@@ -87,6 +87,8 @@
{"index": 30, "fields": {"vis_exp": "0"}},
{"index": 31, "fields": {"vis_exp": "0"}},
{"index": 32, "fields": {"vis_exp": "0"}},
{"index": 33, "fields": {"vis_exp": "0"}},
{"index": 34, "fields": {"vis_exp": "0"}},
{"index": 21, "fields": {"text_key": "@MENU_ADVANCED_VIDEO"}}
],
"item_copies": [
+13 -1
View File
@@ -56,6 +56,7 @@ pub fn install_frontend_menus(catalog: &mut asset_game::MenuCatalog) -> Result<(
catalog.load_definitions(include_str!("../menus/connection_error.json"))?;
catalog.load_definitions(include_str!("../menus/classes.json"))?;
catalog.load_definitions(include_str!("../menus/settings.json"))?;
catalog.load_definitions(include_str!("../menus/controller.json"))?;
for (name, menu) in &mut catalog.menus {
if let Some(chat) = menu
.items
@@ -76,7 +77,15 @@ pub fn install_frontend_menus(catalog: &mut asset_game::MenuCatalog) -> Result<(
multiplayer.handlers.action = vec![asset_game::MenuEvent::Script(
"play mouse_click; close self; open options_multi;".into(),
)];
let mut controller = multiplayer.clone();
menu.items.push(multiplayer);
controller.name = "controller_settings".into();
controller.text_key = "Controller".into();
controller.rect.y = 108.0;
controller.handlers.action = vec![asset_game::MenuEvent::Script(
"play mouse_click; close self; open options_controller;".into(),
)];
menu.items.push(controller);
}
let removed_rows: Vec<_> = menu
@@ -93,7 +102,10 @@ pub fn install_frontend_menus(catalog: &mut asset_game::MenuCatalog) -> Result<(
.collect();
menu.items.retain(|item| {
!removed_rows.iter().any(|&(x, y)| {
item.rect.x == x && item.rect.y == y && item.name != "multiplayer_settings"
item.rect.x == x
&& item.rect.y == y
&& item.name != "multiplayer_settings"
&& item.name != "controller_settings"
})
});
if name == "pc_options_controls" {
+1 -1
View File
@@ -127,7 +127,7 @@ pub use sway::{
lerp_sway_params, sway_contribution, sway_shellshock_landing_scale, track, track_a,
};
pub use tick::{
BURST_COOLDOWN_DEFAULT_MS, BUTTON_ATTACK, BUTTON_RELOAD, BUTTON_THROW,
AimAssistRanges, BURST_COOLDOWN_DEFAULT_MS, BUTTON_ATTACK, BUTTON_RELOAD, BUTTON_THROW,
CHECK_FIRING_AMMO_DRY_FIRE_MS, CapturedCombatInput, MissingCombatFacts, PERK_FASTRELOAD,
PERK_WEAP_RELOAD_MULTIPLIER_DEFAULT, WeaponCmd, WeaponCombatFacts, WeaponHandState,
WeaponTickEvent, get_weapon_fire_button, perk_fastreload_eligible, spawn_clip_stock,
+19
View File
@@ -138,8 +138,25 @@ pub struct CapturedCombatInput {
pub dual_mag: Option<crate::reload::DualMagTimes>,
}
/// Map units; zero where the weapon sets none.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AimAssistRanges {
pub auto_aim: f32,
pub hip: f32,
pub ads: f32,
}
impl AimAssistRanges {
pub const NONE: Self = Self {
auto_aim: 0.0,
hip: 0.0,
ads: 0.0,
};
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct WeaponCombatFacts {
pub aim_assist: AimAssistRanges,
pub fire_time_ms: i32,
pub fire_delay_ms: i32,
pub raise_time_ms: i32,
@@ -273,6 +290,7 @@ impl Default for WeaponCombatFacts {
impl WeaponCombatFacts {
pub const fn none() -> Self {
Self {
aim_assist: AimAssistRanges::NONE,
fire_time_ms: 0,
fire_delay_ms: 0,
raise_time_ms: 0,
@@ -380,6 +398,7 @@ impl WeaponCombatFacts {
return Err(MissingCombatFacts::LocationDamage);
}
Ok(Self {
aim_assist: AimAssistRanges::NONE,
fire_time_ms: input.fire_time_ms,
fire_delay_ms: input.fire_delay_ms,
raise_time_ms: input.raise_time_ms,