mirror of
https://github.com/vladtrc/iw4L
synced 2026-10-26 10:19:50 -04:00
Hold use to pick up a weapon; tap use+reload over one to reload
Picking up a dropped weapon took the press itself. It now takes use held for ITEM_USE_HOLD_MS (250 ms) on the same weapon; thrown items such as knives still go on the press. The press start travels in the client match state, so the protocol is 110. Over a weapon, use+reload used to do nothing on a tap. The client holds the reload back while the button is down and sends it on a release before the hold time, if the same item is still selected, so a tap reloads and a hold picks the weapon up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,11 @@ pub struct ClientActionInput {
|
||||
pub pad_lockon: Option<u64>,
|
||||
pub pad_autoaim: Option<(u64, f32)>,
|
||||
pub pad_was_ads: bool,
|
||||
/// Use+reload is down.
|
||||
pub use_reload_down: bool,
|
||||
/// When use+reload went down over an item, and the item's entity: a tap
|
||||
/// reloads once released, a hold is the item's.
|
||||
pub use_reload_over_item: Option<(i32, i32)>,
|
||||
}
|
||||
|
||||
impl Default for ClientActionInput {
|
||||
@@ -98,6 +103,8 @@ impl Default for ClientActionInput {
|
||||
pad_lockon: None,
|
||||
pad_autoaim: None,
|
||||
pad_was_ads: false,
|
||||
use_reload_down: false,
|
||||
use_reload_over_item: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -749,6 +749,36 @@ fn apply_weapon_switch_requests(
|
||||
}
|
||||
}
|
||||
|
||||
/// Use+reload reloads, except over an item: there a hold is the item's
|
||||
/// (`sim::ITEM_USE_HOLD_MS`, the authority takes it) and a tap still reloads,
|
||||
/// sent once it is released, if the item is still the one it began on.
|
||||
fn use_reload_as_reload(
|
||||
actions: &mut ClientActionInput,
|
||||
cmd: &mut playerstate_iw4::UserCmd,
|
||||
ps: Option<&playerstate_iw4::PlayerState>,
|
||||
) {
|
||||
use playerstate_iw4::buttons::{RELOAD, USE_RELOAD};
|
||||
let down = cmd.buttons & USE_RELOAD != 0;
|
||||
let now = actions.now_msec;
|
||||
let item = ps
|
||||
.filter(|ps| ps.cursor_hint != 0)
|
||||
.map(|ps| ps.cursor_hint_ent_index);
|
||||
if down {
|
||||
if !actions.use_reload_down {
|
||||
actions.use_reload_over_item = item.map(|ent| (now, ent));
|
||||
}
|
||||
if actions.use_reload_over_item.is_none() && item.is_none() {
|
||||
cmd.buttons |= RELOAD;
|
||||
}
|
||||
} else if let Some((since, ent)) = actions.use_reload_over_item.take()
|
||||
&& now.wrapping_sub(since) < sim::ITEM_USE_HOLD_MS
|
||||
&& item == Some(ent)
|
||||
{
|
||||
cmd.buttons |= RELOAD;
|
||||
}
|
||||
actions.use_reload_down = down;
|
||||
}
|
||||
|
||||
pub fn sample_client_input(
|
||||
time: Res<Time<Real>>,
|
||||
mut actions: ResMut<ClientActionInput>,
|
||||
@@ -1063,11 +1093,7 @@ pub fn sample_client_input(
|
||||
actions.client.airborne =
|
||||
ps.is_some_and(|ps| ps.ground_entity_num == playerstate_iw4::ENTITYNUM_NONE);
|
||||
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;
|
||||
}
|
||||
use_reload_as_reload(&mut actions, &mut cmd, ps);
|
||||
look.angles = cmd.angles;
|
||||
if !frozen
|
||||
&& aim.live
|
||||
|
||||
@@ -174,6 +174,6 @@ pub use transport::wire::{WireError, WireReader, WireWriter};
|
||||
|
||||
pub use svc_script_audio::SvcScriptAudio;
|
||||
|
||||
pub const PROTOCOL_VERSION: u32 = 109;
|
||||
pub const PROTOCOL_VERSION: u32 = 110;
|
||||
|
||||
pub use client::fire_verdict::{FireVerdictState, PredictedFireStatus};
|
||||
|
||||
@@ -1343,6 +1343,7 @@ fn encode_client_meta(out: &mut WireWriter, meta: &ClientSnapshotMeta) {
|
||||
out.put_u32(meta.life_sequence.0);
|
||||
out.put_i32(meta.item_use_spawn_ms);
|
||||
encode_optional_entity_ref(out, meta.item_use_entity);
|
||||
out.put_i32(meta.item_use_press_ms);
|
||||
out.put_i32(meta.ammo_clip);
|
||||
out.put_i32(meta.ammo_stock);
|
||||
out.put_i32(meta.score);
|
||||
@@ -1564,6 +1565,7 @@ fn decode_client_meta(input: &mut WireReader<'_>) -> Result<ClientSnapshotMeta,
|
||||
let life_sequence = LifeSequence(input.get_u32()?);
|
||||
let item_use_spawn_ms = input.get_i32()?;
|
||||
let item_use_entity = decode_optional_entity_ref(input)?;
|
||||
let item_use_press_ms = input.get_i32()?;
|
||||
let ammo_clip = input.get_i32()?;
|
||||
let ammo_stock = input.get_i32()?;
|
||||
let score = input.get_i32()?;
|
||||
@@ -1781,6 +1783,7 @@ fn decode_client_meta(input: &mut WireReader<'_>) -> Result<ClientSnapshotMeta,
|
||||
life_sequence,
|
||||
item_use_spawn_ms,
|
||||
item_use_entity,
|
||||
item_use_press_ms,
|
||||
ammo_clip,
|
||||
ammo_stock,
|
||||
score,
|
||||
|
||||
+13
-2
@@ -27,6 +27,9 @@ pub const WEAP_INVENTORY_PRIMARY: i32 = 0;
|
||||
pub const ITEM_MINS: [f32; 3] = [0.0, 0.0, 0.0];
|
||||
pub const ITEM_MAXS: [f32; 3] = [1.0, 1.0, 1.0];
|
||||
|
||||
/// How long use must stay down on a dropped weapon before it is taken.
|
||||
pub const ITEM_USE_HOLD_MS: i32 = 250;
|
||||
|
||||
pub const PLAYER_DROP_Z: f32 = (PLAYER_MAXS[2] - PLAYER_MINS[2]) * 0.5;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
@@ -909,14 +912,22 @@ pub(crate) fn phase_use_items(
|
||||
.expect("occupied item")
|
||||
});
|
||||
let meta = world.client_meta_mut(id);
|
||||
if !held || selected.is_none() {
|
||||
// A press counts only while it stays on the item it began on.
|
||||
if !held || selected.is_none() || selected_ref != meta.item_use_entity {
|
||||
meta.item_use_entity = None;
|
||||
}
|
||||
if pressed {
|
||||
meta.item_use_entity = selected_ref;
|
||||
meta.item_use_press_ms = now;
|
||||
}
|
||||
let pending = meta.item_use_entity;
|
||||
let ready = now - meta.item_use_spawn_ms >= 500;
|
||||
// A weapon is taken once use is held; a thrown item on the press.
|
||||
let hold_ms = if selected.is_some_and(|item| item.projectile) {
|
||||
0
|
||||
} else {
|
||||
ITEM_USE_HOLD_MS
|
||||
};
|
||||
let ready = now - meta.item_use_spawn_ms >= 500 && now - meta.item_use_press_ms >= hold_ms;
|
||||
if held && ready && pending.is_some() && selected_ref == pending {
|
||||
let item = selected.expect("selected use item");
|
||||
if item.projectile {
|
||||
|
||||
@@ -18,6 +18,7 @@ pub mod identities;
|
||||
pub mod input;
|
||||
mod item;
|
||||
mod local_profile;
|
||||
pub use item::ITEM_USE_HOLD_MS;
|
||||
pub use local_profile::LocalPlayerProfile;
|
||||
mod mantle_xanim;
|
||||
pub mod match_state;
|
||||
|
||||
@@ -94,6 +94,8 @@ pub struct ClientMatchState {
|
||||
|
||||
pub item_use_spawn_ms: i32,
|
||||
pub item_use_entity: Option<crate::EntityRef>,
|
||||
/// When the use press on `item_use_entity` began.
|
||||
pub item_use_press_ms: i32,
|
||||
|
||||
pub(crate) ammo_clip: i32,
|
||||
|
||||
@@ -222,6 +224,7 @@ impl ClientMatchState {
|
||||
life_sequence: self.life_sequence,
|
||||
item_use_spawn_ms: self.item_use_spawn_ms,
|
||||
item_use_entity: self.item_use_entity,
|
||||
item_use_press_ms: self.item_use_press_ms,
|
||||
ammo_clip: self.ammo_clip,
|
||||
ammo_stock: self.ammo_stock,
|
||||
score: self.score,
|
||||
@@ -280,6 +283,7 @@ impl ClientMatchState {
|
||||
self.life_sequence = meta.life_sequence;
|
||||
self.item_use_spawn_ms = meta.item_use_spawn_ms;
|
||||
self.item_use_entity = meta.item_use_entity;
|
||||
self.item_use_press_ms = meta.item_use_press_ms;
|
||||
self.ammo_clip = meta.ammo_clip;
|
||||
self.ammo_stock = meta.ammo_stock;
|
||||
self.score = meta.score;
|
||||
|
||||
@@ -24,6 +24,7 @@ pub struct ClientSnapshotMeta {
|
||||
|
||||
pub item_use_spawn_ms: i32,
|
||||
pub item_use_entity: Option<crate::EntityRef>,
|
||||
pub item_use_press_ms: i32,
|
||||
|
||||
pub ammo_clip: i32,
|
||||
|
||||
|
||||
Reference in New Issue
Block a user