diff --git a/crates/audio/src/breath.rs b/crates/audio/src/breath.rs index 3149f8b..4acacf0 100644 --- a/crates/audio/src/breath.rs +++ b/crates/audio/src/breath.rs @@ -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 diff --git a/crates/hud/src/breath_hint.rs b/crates/hud/src/breath_hint.rs index 5e7645f..ac7282c 100644 --- a/crates/hud/src/breath_hint.rs +++ b/crates/hud/src/breath_hint.rs @@ -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!( diff --git a/crates/hud/src/overhead_names.rs b/crates/hud/src/overhead_names.rs index 7800578..3513a01 100644 --- a/crates/hud/src/overhead_names.rs +++ b/crates/hud/src/overhead_names.rs @@ -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; }; diff --git a/crates/movement_iw4/src/ads_frac.rs b/crates/movement_iw4/src/ads_frac.rs index 1760ebb..ffebc6a 100644 --- a/crates/movement_iw4/src/ads_frac.rs +++ b/crates/movement_iw4/src/ads_frac.rs @@ -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 { diff --git a/crates/movement_iw4/src/breath.rs b/crates/movement_iw4/src/breath.rs index a4e3914..e18f78b 100644 --- a/crates/movement_iw4/src/breath.rs +++ b/crates/movement_iw4/src/breath.rs @@ -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 }; diff --git a/crates/playerstate_iw4/src/lib.rs b/crates/playerstate_iw4/src/lib.rs index d70c6fa..5346476 100644 --- a/crates/playerstate_iw4/src/lib.rs +++ b/crates/playerstate_iw4/src/lib.rs @@ -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, diff --git a/crates/playerstate_iw4/src/playerstate.rs b/crates/playerstate_iw4/src/playerstate.rs index 73c6123..998327e 100644 --- a/crates/playerstate_iw4/src/playerstate.rs +++ b/crates/playerstate_iw4/src/playerstate.rs @@ -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, diff --git a/crates/sim/src/script_player.rs b/crates/sim/src/script_player.rs index 49dcb3d..554dd75 100644 --- a/crates/sim/src/script_player.rs +++ b/crates/sim/src/script_player.rs @@ -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; } } diff --git a/crates/weapon_iw4/src/tick.rs b/crates/weapon_iw4/src/tick.rs index 40c4a35..e6eb5c6 100644 --- a/crates/weapon_iw4/src/tick.rs +++ b/crates/weapon_iw4/src/tick.rs @@ -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; }