Hold aim alongside toggle aim; mouse wheel directions in key bindings

Controls list Aim Down the Sight and Hold Aim Down the Sight; pressing hold
clears a toggle latch so release always leaves ADS. MWHEELUP and MWHEELDOWN
bind from the menu, the console and saved settings, and wheel scroll reaches
the game as discrete presses, so weapon cycling works on the wheel.

Closes #6
Closes #7
This commit is contained in:
awsms
2026-09-28 17:51:18 +03:00
committed by vtroc7
parent a6fa0b9514
commit 665d4ff6df
6 changed files with 98 additions and 8 deletions
+54 -2
View File
@@ -1,10 +1,10 @@
use input_iw4::{command_id_lookup, command_name};
use input_iw4::{ClientInput, cl_key_event, command_id_lookup, command_name};
use std::collections::HashMap;
use bevy::input::ButtonInput;
use bevy::input::keyboard::KeyCode;
use bevy::input::mouse::MouseButton;
use bevy::input::mouse::{MouseButton, MouseScrollUnit};
use bevy::prelude::Resource;
pub const DEFAULT_CONTROLS: &str = include_str!("../assets/default_controls.cfg");
@@ -73,12 +73,40 @@ pub const BINDABLE_KEYS: &[&str] = &[
"mouse3",
"mouse4",
"mouse5",
"mwheelup",
"mwheeldown",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BindButton {
Key(KeyCode),
Mouse(MouseButton),
WheelUp,
WheelDown,
}
pub(crate) fn wheel_button(y: f32) -> Option<BindButton> {
if y > 0.0 {
Some(BindButton::WheelUp)
} else if y < 0.0 {
Some(BindButton::WheelDown)
} else {
None
}
}
pub(crate) fn wheel_detents(unit: MouseScrollUnit, y: f32, carry: &mut f32) -> i32 {
if !y.is_finite() {
return 0;
}
let delta = match unit {
MouseScrollUnit::Line => y,
MouseScrollUnit::Pixel => y / 100.0,
};
*carry = (*carry + delta).clamp(-32.0, 32.0);
let detents = carry.trunc() as i32;
*carry -= detents as f32;
detents
}
pub struct BindInputs<'a> {
@@ -95,6 +123,7 @@ impl<'a> BindInputs<'a> {
match button {
BindButton::Key(key) => self.keys.pressed(key),
BindButton::Mouse(btn) => self.mouse.pressed(btn),
BindButton::WheelUp | BindButton::WheelDown => false,
}
}
@@ -102,6 +131,7 @@ impl<'a> BindInputs<'a> {
match button {
BindButton::Key(key) => self.keys.just_pressed(key),
BindButton::Mouse(btn) => self.mouse.just_pressed(btn),
BindButton::WheelUp | BindButton::WheelDown => false,
}
}
@@ -109,6 +139,7 @@ impl<'a> BindInputs<'a> {
match button {
BindButton::Key(key) => self.keys.just_released(key),
BindButton::Mouse(btn) => self.mouse.just_released(btn),
BindButton::WheelUp | BindButton::WheelDown => false,
}
}
}
@@ -261,6 +292,20 @@ impl KeyBinds {
}
}
pub(crate) fn pulse_wheel_binding(
binds: &KeyBinds,
client: &mut ClientInput,
button: BindButton,
now_msec: i32,
frame_msec: u32,
) {
let Some(id) = binds.get(button) else { return };
let key_num = host_keynum(button);
client.keys[key_num].binding = id;
cl_key_event(client, key_num, true, now_msec, frame_msec);
cl_key_event(client, key_num, false, now_msec, frame_msec);
}
pub fn host_keynum(button: BindButton) -> usize {
match button {
BindButton::Mouse(MouseButton::Left) => 180,
@@ -269,6 +314,8 @@ pub fn host_keynum(button: BindButton) -> usize {
BindButton::Mouse(MouseButton::Back) => 183,
BindButton::Mouse(MouseButton::Forward) => 184,
BindButton::Mouse(_) => 185,
BindButton::WheelUp => 186,
BindButton::WheelDown => 187,
BindButton::Key(key) => keycode_keynum(key),
}
}
@@ -424,6 +471,8 @@ pub fn parse_button_name(name: &str) -> Option<Vec<BindButton>> {
"mouse3" | "mousemiddle" | "mmb" => BindButton::Mouse(MouseButton::Middle),
"mouse4" => BindButton::Mouse(MouseButton::Back),
"mouse5" => BindButton::Mouse(MouseButton::Forward),
"mwheelup" | "wheelup" => BindButton::WheelUp,
"mwheeldown" | "wheeldown" => BindButton::WheelDown,
_ => return None,
};
Some(vec![button])
@@ -442,6 +491,8 @@ pub fn display_button(button: BindButton) -> String {
BindButton::Mouse(MouseButton::Back) => "MOUSE4".into(),
BindButton::Mouse(MouseButton::Forward) => "MOUSE5".into(),
BindButton::Mouse(other) => format!("{other:?}"),
BindButton::WheelUp => "MWHEELUP".into(),
BindButton::WheelDown => "MWHEELDOWN".into(),
}
}
@@ -509,3 +560,4 @@ fn display_key(key: KeyCode) -> String {
}
.to_owned()
}
+24 -3
View File
@@ -2,7 +2,7 @@ use bevy::{
input::ButtonState,
input::InputSystems,
input::keyboard::{Key, KeyCode, KeyboardInput, NativeKeyCode},
input::mouse::{MouseButton, MouseMotion},
input::mouse::{MouseButton, MouseMotion, MouseWheel},
input_focus::{FocusCause, InputFocus, InputFocusSystems},
picking::{
events::{Drag, Pointer, Press, Release},
@@ -20,8 +20,8 @@ use render_frontend::prepare::scene::world::WorldScene;
use ui::{MenuEnabled, UiLayer};
use crate::{
BINDABLE_KEYS, ConsoleCommand, ConsoleEditor, ConsoleInputState, KeyBinds,
binds::{BindInputs, host_keynum},
BINDABLE_KEYS, BindButton, ConsoleCommand, ConsoleEditor, ConsoleInputState, KeyBinds,
binds::{BindInputs, host_keynum, pulse_wheel_binding, wheel_detents},
is_bind_command,
registry::ConsoleRegistry,
suggest::{SuggestSpan, SuggestTone, suggestion_spans},
@@ -419,6 +419,8 @@ fn publish_client_action_input(
keys: Res<ButtonInput<KeyCode>>,
mouse_buttons: Res<ButtonInput<MouseButton>>,
mut motion: MessageReader<MouseMotion>,
mut wheel: MessageReader<MouseWheel>,
mut wheel_carry: Local<f32>,
binds: Res<KeyBinds>,
mut scripted: ResMut<ConsoleInputState>,
console: Res<ConsoleState>,
@@ -464,9 +466,14 @@ fn publish_client_action_input(
let now = out.now_msec;
let frame = out.frame_msec;
if binds.is_changed() {
*wheel_carry = 0.0;
}
if console.open || menu.0 || keys.just_pressed(KeyCode::Escape) {
for _ in motion.read() {}
for _ in wheel.read() {}
*wheel_carry = 0.0;
for key_num in 0..input_iw4::KEY_COUNT {
if out.client.keys[key_num].down != 0 {
cl_key_event(&mut out.client, key_num, false, now, frame);
@@ -489,6 +496,20 @@ fn publish_client_action_input(
cl_key_event(&mut out.client, key_num, false, now, frame);
}
}
for event in wheel.read() {
let steps = wheel_detents(event.unit, event.y, &mut wheel_carry);
if steps == 0 {
continue;
}
let button = if steps > 0 {
BindButton::WheelUp
} else {
BindButton::WheelDown
};
for _ in 0..steps.unsigned_abs() {
pulse_wheel_binding(&binds, &mut out.client, button, now, frame);
}
}
let scripted_now: std::collections::BTreeSet<u32> = scripted.ids().collect();
let down: Vec<u32> = scripted_now
+13 -3
View File
@@ -2,11 +2,15 @@ use std::{fs, path::PathBuf};
use bevy::{
audio::{AudioSink, AudioSinkPlayback, GlobalVolume, Volume},
input::{ButtonInput, keyboard::KeyCode, mouse::MouseButton},
input::{
ButtonInput,
keyboard::KeyCode,
mouse::{MouseButton, MouseWheel},
},
prelude::*,
};
use crate::{BindButton, KeyBinds, display_button};
use crate::{BindButton, KeyBinds, binds::wheel_button, display_button};
#[derive(Resource, Default)]
pub(crate) struct PendingMenuBinding {
@@ -47,10 +51,14 @@ pub(crate) fn consume_menu_binding(
mut intents: MessageReader<ui::UiIntent>,
keys: Res<ButtonInput<KeyCode>>,
mouse: Res<ButtonInput<MouseButton>>,
mut wheel: MessageReader<MouseWheel>,
mut pending: ResMut<PendingMenuBinding>,
mut binds: ResMut<KeyBinds>,
mut view: ResMut<ui::BindingView>,
) {
let wheel_direction = wheel
.read()
.fold(None, |first, event| first.or_else(|| wheel_button(event.y)));
let mut began = false;
for intent in intents.read() {
if let ui::UiIntent::BeginBinding { id } = intent {
@@ -82,7 +90,8 @@ pub(crate) fn consume_menu_binding(
.copied()
.map(BindButton::Mouse)
.next()
});
})
.or(wheel_direction);
let Some(button) = button else { return };
binds.clear_command(id);
binds.set(button, id);
@@ -305,3 +314,4 @@ fn parse_settings(source: &str, settings: &mut frame::GameSettings, binds: &mut
binds.set(BindButton::Key(KeyCode::Digit4), 21);
}
}
+3
View File
@@ -289,6 +289,9 @@ pub fn cl_input_cmd(
9 | 10 => panic!("+breath_sprint kbutton EAX unread; not merged with +sprint"),
11 | 12 => apply_pair(&mut client.kb.usereload, cmd_id, key, now_msec, frame_msec),
13 | 14 => {
if pair_down(cmd_id) {
client.using_ads = false;
}
apply_pair(&mut client.kb.speed, cmd_id, key, now_msec, frame_msec);
apply_pair(&mut client.kb.throw_btn, cmd_id, key, now_msec, frame_msec);
}
+1
View File
@@ -1239,6 +1239,7 @@ const MOVEMENT_BINDS: &[(u32, &str)] = &[
const ACTION_BINDS: &[(u32, &str)] = &[
(1, "Fire Weapon"),
(57, "Aim Down the Sight"),
(13, "Hold Aim Down the Sight"),
(51, "Reload"),
(66, "Switch Weapon"),
(3, "Melee"),
+3
View File
@@ -52,6 +52,9 @@ on host and clients — the host records authority, a client the snapshots it
received plus its own presented state; `demo LATEST` plays it back.
* ADS is `hold +speed_throw`, not `+speed`;
* Aim binding: `bind MOUSE2 +speed_throw` aims only while held;
`bind MOUSE2 +toggleads_throw` toggles aim on each press. Both are available
under Options → Controls → Actions, and the chosen bind is saved in settings.
* bolt action (`fire_type=1`): `hold +attack` is **one** shot, and a full
magazine will not reload itself (`press +attack` ×N, then `press +reload`);
* `look` without `LookState` only writes `ps.viewangles` — no aiming;