mirror of
https://github.com/vladtrc/iw4L
synced 2026-10-26 02:11:39 -04:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>);
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)]
|
||||
}
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -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
@@ -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" {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user