From 070c3c37ffd6b9cee442f8dc59db7b9f5290cba5 Mon Sep 17 00:00:00 2001 From: chazmm Date: Fri, 25 Sep 2026 19:38:53 +0100 Subject: [PATCH] Add a persistent FOV slider to video options --- crates/console/src/user_settings.rs | 6 ++++++ crates/frame/src/settings.rs | 11 +++++++++++ crates/render_anim/src/occupancy/view_kick.rs | 17 +++++++++-------- crates/ui/src/model.rs | 1 + crates/ui/src/options.rs | 9 ++++++++- crates/ui/src/screens.rs | 12 ++++++++++++ 6 files changed, 47 insertions(+), 9 deletions(-) diff --git a/crates/console/src/user_settings.rs b/crates/console/src/user_settings.rs index a7752e7..a5569bc 100644 --- a/crates/console/src/user_settings.rs +++ b/crates/console/src/user_settings.rs @@ -219,6 +219,7 @@ fn serialize_settings(settings: &frame::GameSettings, binds: &KeyBinds) -> Strin ), format!("fullscreen={}", settings.fullscreen), format!("vsync={}", settings.vsync), + format!("fov={:.0}", settings.fov), format!("master_volume={:.3}", settings.master_volume), format!("sensitivity={:.3}", settings.sensitivity), format!("invert_mouse={}", settings.invert_mouse), @@ -269,6 +270,11 @@ fn parse_settings(source: &str, settings: &mut frame::GameSettings, binds: &mut settings.master_volume = value; } } + "fov" => { + if let Ok(value) = value.parse() { + settings.fov = value; + } + } "sensitivity" => { if let Ok(value) = value.parse() { settings.sensitivity = value; diff --git a/crates/frame/src/settings.rs b/crates/frame/src/settings.rs index a8bb657..1ba7c38 100644 --- a/crates/frame/src/settings.rs +++ b/crates/frame/src/settings.rs @@ -28,6 +28,7 @@ pub struct GameSettings { pub resolution: DisplayResolution, pub fullscreen: bool, pub vsync: bool, + pub fov: f32, pub master_volume: f32, pub sensitivity: f32, pub invert_mouse: bool, @@ -42,6 +43,7 @@ impl Default for GameSettings { resolution: DisplayResolution::HD, fullscreen: false, vsync: true, + fov: Self::FOV_DEFAULT, master_volume: 1.0, sensitivity: 5.0, invert_mouse: false, @@ -52,6 +54,10 @@ impl Default for GameSettings { } impl GameSettings { + pub const FOV_DEFAULT: f32 = 65.0; + pub const FOV_MIN: f32 = 65.0; + pub const FOV_MAX: f32 = 120.0; + pub fn touch(&mut self) { self.revision = self.revision.wrapping_add(1); } @@ -59,6 +65,11 @@ impl GameSettings { pub fn sanitize(&mut self) { self.resolution.width = self.resolution.width.clamp(640, 7680); self.resolution.height = self.resolution.height.clamp(480, 4320); + self.fov = if self.fov.is_finite() { + self.fov.clamp(Self::FOV_MIN, Self::FOV_MAX) + } else { + Self::FOV_DEFAULT + }; self.master_volume = self.master_volume.clamp(0.0, 1.0); self.sensitivity = self.sensitivity.clamp(0.1, 30.0); self.player_name = self.player_name.trim().chars().take(16).collect(); diff --git a/crates/render_anim/src/occupancy/view_kick.rs b/crates/render_anim/src/occupancy/view_kick.rs index fade077..3958e96 100644 --- a/crates/render_anim/src/occupancy/view_kick.rs +++ b/crates/render_anim/src/occupancy/view_kick.rs @@ -2,9 +2,8 @@ use assets::{PreparedWeapons, WeaponBodyFacts, WeaponKickFacts}; use bevy::prelude::*; use frame::{LifeStarted, ViewSubject}; use hud_iw4::{ - CG_FOV_DEFAULT, CG_FOV_MIN_DEFAULT, CG_FOV_SCALE_DEFAULT, CgCalcFovInputs, - WeaponAdsOverlayFacts, cg_calc_fov_from_ads, cg_horizontal_to_vertical_fov_deg, - cg_zoom_sensitivity, + CG_FOV_MIN_DEFAULT, CG_FOV_SCALE_DEFAULT, CgCalcFovInputs, WeaponAdsOverlayFacts, + cg_calc_fov_from_ads, cg_horizontal_to_vertical_fov_deg, cg_zoom_sensitivity, }; use math_iw4::{add_lean_to_position, angle_vectors}; use net::{ @@ -240,6 +239,7 @@ pub fn sync_camera_from_presented( presented: Res, local: Res, sim_cam: Res, + settings: Res, mut kick: ResMut, mut hurt: ResMut, weapons: Option>, @@ -319,6 +319,7 @@ pub fn sync_camera_from_presented( } apply_fpv_lens_fov( &mut lenses, + settings.fov, ps.pm_type, ps.link_flags, ps.e_flags, @@ -439,6 +440,7 @@ pub fn sync_camera_from_presented( kick.last_weapon_pos_frac = ps.f_weapon_pos_frac; if let Some(horiz) = apply_fpv_lens_fov( &mut lenses, + settings.fov, ps.pm_type, ps.link_flags, ps.e_flags, @@ -454,6 +456,7 @@ pub fn sync_camera_from_presented( fn apply_fpv_lens_fov( lenses: &mut Query<&mut Projection, With>, + base_fov: f32, pm_type: i32, link_flags: u32, e_flags: u32, @@ -463,9 +466,7 @@ fn apply_fpv_lens_fov( b_position_to_ads: bool, actions: Option<&mut ClientActionInput>, ) -> Option { - let Some(facts) = facts.filter(|f| f.body_resolved) else { - return None; - }; + let facts = facts.filter(|f| f.body_resolved).unwrap_or_default(); let overlay = WeaponAdsOverlayFacts { ads_zoom_in_frac: facts.ads_zoom_in_frac, ads_zoom_out_frac: facts.ads_zoom_out_frac, @@ -475,10 +476,10 @@ fn apply_fpv_lens_fov( let ads_target = if facts.ads_zoom_fov > 0.0 { facts.ads_zoom_fov } else { - CG_FOV_DEFAULT + base_fov }; let inputs = CgCalcFovInputs { - cg_fov: CG_FOV_DEFAULT, + cg_fov: base_fov, pm_type, link_flags, e_flags, diff --git a/crates/ui/src/model.rs b/crates/ui/src/model.rs index b560051..67556f3 100644 --- a/crates/ui/src/model.rs +++ b/crates/ui/src/model.rs @@ -143,6 +143,7 @@ pub enum SettingKey { Resolution, Fullscreen, Vsync, + Fov, MasterVolume, Sensitivity, InvertMouse, diff --git a/crates/ui/src/options.rs b/crates/ui/src/options.rs index 0ed0169..d258b97 100644 --- a/crates/ui/src/options.rs +++ b/crates/ui/src/options.rs @@ -167,7 +167,7 @@ pub(crate) fn options_widget_is_active(state: &OptionsState, id: &str) -> bool { OptionsDepth::SectionRows => match state.tab { OptionsTab::Video => matches!( id, - "options/resolution" | "options/fullscreen" | "options/vsync" + "options/resolution" | "options/fullscreen" | "options/vsync" | "options/fov" ), OptionsTab::Audio => id == "options/volume", OptionsTab::Controls => OptionsControlGroup::from_widget_id(id).is_some(), @@ -273,6 +273,7 @@ pub(crate) fn drive_options_navigation( focus.widget.as_deref(), Some( "options/volume" + | "options/fov" | "options/sensitivity" | "options/fullscreen" | "options/vsync" @@ -445,6 +446,12 @@ pub(crate) fn apply_option_intents( (SettingKey::Vsync, SettingValue::Bool(value)) => { replace(&mut settings.vsync, *value) } + (SettingKey::Fov, SettingValue::Float(value)) if value.is_finite() => { + let value = value + .clamp(frame::GameSettings::FOV_MIN, frame::GameSettings::FOV_MAX) + .round(); + replace(&mut settings.fov, value) + } (SettingKey::MasterVolume, SettingValue::Float(value)) => { let value = value.clamp(0.0, 1.0); replace(&mut settings.master_volume, value) diff --git a/crates/ui/src/screens.rs b/crates/ui/src/screens.rs index c5f1cd7..8becede 100644 --- a/crates/ui/src/screens.rs +++ b/crates/ui/src/screens.rs @@ -1343,6 +1343,18 @@ pub fn options(host: Host<'_>) -> Screen { None, "Synchronize presentation to the display refresh.", ), + slider( + "options/fov", + 238.0, + 122.0, + "Field of View", + settings.fov, + frame::GameSettings::FOV_MIN, + frame::GameSettings::FOV_MAX, + 1.0, + crate::SettingKey::Fov, + "Show more of your surroundings. Default: 65.", + ), ]); } OptionsTab::Audio => {