Give the Sleight of Hand, Steady Aim and Cold-Blooded Pro perks effect

MW2's class scripts set specialty_quickdraw, specialty_holdbreath and
specialty_spygame for the pro perks, but no perk bit was mapped, so they
did nothing. With MW2's bits and values:
- quickdraw (perk 26) moves into and out of ADS 1.5 times as fast
  (perk_quickDrawSpeedScale) and shortens the ADS-only fire delay to match;
- holdbreath (perk 4) holds the breath 5 s longer (perk_extraBreath), in
  the movement code, the breath hint and the gasp sound;
- spygame (perk 32, bit 0 of perks[1]) hides the player's name from
  enemies aiming at them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
neptune
2026-10-06 00:26:43 -04:00
parent b4736cadb1
commit b60d4b4170
9 changed files with 74 additions and 19 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
use asset_core::AssetNamespace;
use bevy::prelude::*;
use net::{LocalPresentClient, PresentedSnapshot};
use playerstate_iw4::{BREATH_HOLD_TIME_MS, weap_flags};
use playerstate_iw4::{breath_hold_time_ms, weap_flags};
use crate::sources::{DesiredSource, SourceCueRequest, SourceKey};
use crate::{AliasCommand, PlayAlias};
@@ -112,7 +112,7 @@ pub(crate) fn update(
});
}
if let Some(ps) = ps {
let alias = if ps.hold_breath_timer > BREATH_HOLD_TIME_MS {
let alias = if ps.hold_breath_timer > breath_hold_time_ms(ps) {
cues.gasp
} else {
cues.exhale
+2 -2
View File
@@ -4,7 +4,7 @@ use asset_game::MenuCatalog;
use assets::{PreparedLocalizedStrings, PreparedWeapons};
use bevy::prelude::*;
use net::{LocalPresentClient, PresentedSnapshot};
use playerstate_iw4::{BREATH_HOLD_TIME_MS, weap_flags};
use playerstate_iw4::{breath_hold_time_ms, weap_flags};
use crate::draw2d::{Draw2dCmd, Draw2dList, Draw2dOp, Draw2dProvenance, tessellate_fonts};
use crate::gpu_list::{HudTessPass, TessJob};
@@ -75,7 +75,7 @@ pub(crate) fn update(
let text = if ps.weap_flags & weap_flags::HOLD_BREATH != 0 {
format!(
"Holding breath {:.1}s",
(BREATH_HOLD_TIME_MS - ps.hold_breath_timer).max(0) as f32 * 0.001
(breath_hold_time_ms(ps) - ps.hold_breath_timer).max(0) as f32 * 0.001
)
} else if ps.hold_breath_timer > 0 {
format!(
+10
View File
@@ -296,6 +296,16 @@ fn update_overhead_names(
if meta.lifecycle != sim::ClientLifecycle::Alive {
continue;
}
let enemy = local_meta.client_state_team == 0
|| local_meta.client_state_team != meta.client_state_team;
if enemy
&& snapshot
.players
.iter()
.any(|(id, ps)| *id == client && ps.perks[1] & playerstate_iw4::PERK1_SPYGAME != 0)
{
continue;
}
let Some(name) = entity_iw4::client_state_name(&meta.name) else {
continue;
};
+4 -1
View File
@@ -61,7 +61,10 @@ pub fn update_ads_frac(ps: &mut PlayerState, msec: i32, context: AdsFracContext)
{
ads_requested = true;
}
let dt = msec.max(0) as f32;
let mut dt = msec.max(0) as f32;
if ps.perks[0] & playerstate_iw4::PERK_QUICKDRAW != 0 {
dt *= playerstate_iw4::PERK_QUICKDRAW_SPEED_SCALE;
}
if ads_requested {
if ps.f_weapon_pos_frac >= 1.0 {
+5 -4
View File
@@ -1,5 +1,5 @@
use playerstate_iw4::{
BREATH_GASP_TIME_MS, BREATH_HOLD_TIME_MS, PlayerState, buttons, pm_flags, weap_flags,
BREATH_GASP_TIME_MS, PlayerState, breath_hold_time_ms, buttons, pm_flags, weap_flags,
};
pub(crate) fn update_hold_breath(ps: &mut PlayerState, buttons: u32, msec: i32, eligible: bool) {
@@ -10,12 +10,13 @@ pub(crate) fn update_hold_breath(ps: &mut PlayerState, buttons: u32, msec: i32,
&& ps.pm_flags & (pm_flags::SPRINTING | pm_flags::MANTLE | pm_flags::LADDER) == 0
&& ps.weap_flags & weap_flags::OFFHAND_VIEW == 0
&& ps.health > 0;
let hold_time = breath_hold_time_ms(ps);
let holding = wants_hold && (was_holding || ps.hold_breath_timer == 0);
ps.weap_flags &= !weap_flags::HOLD_BREATH;
if holding {
ps.hold_breath_timer = ps.hold_breath_timer.saturating_add(msec);
if ps.hold_breath_timer >= BREATH_HOLD_TIME_MS {
ps.hold_breath_timer = BREATH_HOLD_TIME_MS + BREATH_GASP_TIME_MS;
if ps.hold_breath_timer >= hold_time {
ps.hold_breath_timer = hold_time + BREATH_GASP_TIME_MS;
} else {
ps.weap_flags |= weap_flags::HOLD_BREATH;
}
@@ -26,7 +27,7 @@ pub(crate) fn update_hold_breath(ps: &mut PlayerState, buttons: u32, msec: i32,
let target = if holding {
0.0
} else {
1.0 + 3.5 * ps.hold_breath_timer as f32 / (BREATH_HOLD_TIME_MS + BREATH_GASP_TIME_MS) as f32
1.0 + 3.5 * ps.hold_breath_timer as f32 / (hold_time + BREATH_GASP_TIME_MS) as f32
};
let target = 1.0 + (target - 1.0) * ps.f_weapon_pos_frac;
let rate = if holding { 1.0 } else { 6.0 };
+4 -3
View File
@@ -15,9 +15,10 @@ pub use chrome::{
};
pub use playerstate::{
AnimPair, BREATH_GASP_TIME_MS, BREATH_HOLD_TIME_MS, ENTITYNUM_NONE, PERK_COLDBLOODED,
PERK_FASTMANTLE, PERK_HEARTBREAKER, PERK_PISTOLDEATH, PERK_QUIETER, PERK_SCAVENGER,
PlayerState, eflags, get_viewmodel_weapon_index, mantle_flags, other_flags, pm_flags,
weap_flags,
PERK_EXTRA_BREATH_MS, PERK_FASTMANTLE, PERK_HEARTBREAKER, PERK_HOLDBREATH, PERK_PISTOLDEATH,
PERK_QUICKDRAW, PERK_QUICKDRAW_SPEED_SCALE, PERK_QUIETER, PERK_SCAVENGER, PERK1_SPYGAME,
PlayerState, breath_hold_time_ms, eflags, get_viewmodel_weapon_index, mantle_flags,
other_flags, pm_flags, weap_flags,
};
pub use seat::{
HITSCAN_KILL_CAM_ENTITY, SeatFocus, apply_killcam_seat, rebase_archived_timers,
+25
View File
@@ -357,10 +357,35 @@ pub const PERK_FASTMANTLE: u32 = 1 << 19;
pub const PERK_SCAVENGER: u32 = 1 << 22;
/// Steady Aim Pro: breath holds `PERK_EXTRA_BREATH_MS` longer.
pub const PERK_HOLDBREATH: u32 = 1 << 4;
/// `perk_extraBreath`, in milliseconds.
pub const PERK_EXTRA_BREATH_MS: i32 = 5000;
/// Sleight of Hand Pro: aiming down sights is `PERK_QUICKDRAW_SPEED_SCALE` faster.
pub const PERK_QUICKDRAW: u32 = 1 << 26;
/// `perk_quickDrawSpeedScale`.
pub const PERK_QUICKDRAW_SPEED_SCALE: f32 = 1.5;
pub const PERK_COLDBLOODED: u32 = 1 << 27;
pub const PERK_HEARTBREAKER: u32 = 1 << 28;
/// Cold-Blooded Pro (perk 32, so bit 0 of `perks[1]`): enemies aiming at the
/// player see no name.
pub const PERK1_SPYGAME: u32 = 1;
/// How long this player can hold their breath before gasping.
pub fn breath_hold_time_ms(ps: &PlayerState) -> i32 {
if ps.perks[0] & PERK_HOLDBREATH != 0 {
BREATH_HOLD_TIME_MS + PERK_EXTRA_BREATH_MS
} else {
BREATH_HOLD_TIME_MS
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct AnimPair {
pub legs_anim: i32,
+16 -5
View File
@@ -716,7 +716,7 @@ pub(crate) fn give_start_ammo(world: &mut FrameWorld, id: ClientId, weapon: u32)
set_ammo_stock(world, id, weapon, stock);
}
fn perk_bits(name: &str) -> (u32, u32) {
fn perk_bits(name: &str) -> ([u32; 2], u32) {
let perk = match name {
"specialty_fastreload" => weapon_iw4::PERK_FASTRELOAD,
"specialty_coldblooded" => playerstate_iw4::PERK_COLDBLOODED,
@@ -728,23 +728,34 @@ fn perk_bits(name: &str) -> (u32, u32) {
"specialty_bulletaccuracy" => weapon_iw4::PERK_BULLETACCURACY,
"specialty_pistoldeath" => playerstate_iw4::PERK_PISTOLDEATH,
"specialty_fastmantle" => playerstate_iw4::PERK_FASTMANTLE,
"specialty_quickdraw" => playerstate_iw4::PERK_QUICKDRAW,
"specialty_holdbreath" => playerstate_iw4::PERK_HOLDBREATH,
_ => 0,
};
let perk1 = match name {
"specialty_spygame" => playerstate_iw4::PERK1_SPYGAME,
_ => 0,
};
let e_flags = match name {
"specialty_localjammer" => playerstate_iw4::eflags::RADAR_JAM,
_ => 0,
};
(perk, e_flags)
([perk, perk1], e_flags)
}
pub(crate) fn set_perk(world: &mut FrameWorld, id: ClientId, name: &str, on: bool) {
let (perk, e_flags) = perk_bits(name);
let (perks, e_flags) = perk_bits(name);
if let Some(ps) = world.player_mut(id) {
for (word, perk) in ps.perks.iter_mut().zip(perks) {
if on {
*word |= perk;
} else {
*word &= !perk;
}
}
if on {
ps.perks[0] |= perk;
ps.e_flags |= e_flags;
} else {
ps.perks[0] &= !perk;
ps.e_flags &= !e_flags;
}
}
+6 -2
View File
@@ -1207,8 +1207,12 @@ fn finish_weapon_tick(
hand.weaponstate = WeaponState::Firing as i32;
hand.weapon_time = facts.fire_time_ms.max(1);
if facts.ads_fire_only {
hand.weapon_delay =
ads_fire_only_delay_ms(cmd.f_weapon_pos_frac, facts.ads_in_rate);
let ads_in_rate = if cmd.perks0 & playerstate_iw4::PERK_QUICKDRAW != 0 {
facts.ads_in_rate * playerstate_iw4::PERK_QUICKDRAW_SPEED_SCALE
} else {
facts.ads_in_rate
};
hand.weapon_delay = ads_fire_only_delay_ms(cmd.f_weapon_pos_frac, ads_in_rate);
} else if facts.fire_delay_ms > 0 {
hand.weapon_delay = facts.fire_delay_ms;
}