diff --git a/layout/pages/settings/video.xml b/layout/pages/settings/video.xml
index 06eb998f..8567cc2f 100644
--- a/layout/pages/settings/video.xml
+++ b/layout/pages/settings/video.xml
@@ -5,6 +5,8 @@
+
+
@@ -31,7 +33,21 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/scripts/pages/settings/fov.js b/scripts/pages/settings/fov.js
new file mode 100644
index 00000000..21684434
--- /dev/null
+++ b/scripts/pages/settings/fov.js
@@ -0,0 +1,62 @@
+class Fov {
+ static panels = {
+ /** @type {SettingsSlider} @static */
+ fov: $('#FOV'),
+ /** @type {TextEntry} @static */
+ horizontalFov: $('#FOV_Horizontal'),
+ aspectRatio: $('#FOV_Horizontal_AspectRatioEnum')
+ };
+
+ static loadSettings() {
+ this.panels.aspectRatio.SetSelected('aspectratio1');
+ this.updateFOV();
+ }
+
+ static aspectRatio() {
+ const id = this.panels.aspectRatio.GetSelected().id;
+ switch (id) {
+ case 'aspectratio0':
+ return 4 / 3;
+ case 'aspectratio1':
+ return 16 / 9;
+ case 'aspectratio2':
+ return 16 / 10;
+ }
+ return Number.NaN;
+ }
+
+ // based on https://casualhacks.net/Source-FOV-calculator.html
+ static fovToHorizontal(fov) {
+ const ratioRatio = this.aspectRatio() / (4 / 3);
+ return 2 * rad2deg(Math.atan(Math.tan(deg2rad(fov) / 2) * ratioRatio));
+ }
+
+ static horizontalToFov(horizontalFov) {
+ const ratioRatio = this.aspectRatio() / (4 / 3);
+ return 2 * rad2deg(Math.atan(Math.tan(deg2rad(horizontalFov) / 2) / ratioRatio));
+ }
+
+ static updateFOV() {
+ if (!this.panels.fov || !this.panels.horizontalFov) return;
+
+ let fov = GameInterfaceAPI.GetSettingFloat('fov_desired');
+ fov = Math.round(this.fovToHorizontal(fov));
+
+ if (!Number.isNaN(fov)) {
+ this.panels.horizontalFov.text = fov;
+ }
+ }
+
+ static updateHorizontalFov() {
+ if (!this.panels.fov || !this.panels.horizontalFov) return;
+
+ let fov = Number.parseFloat(this.panels.horizontalFov.text);
+ fov = Math.round(this.horizontalToFov(fov));
+
+ if (!Number.isNaN(fov)) {
+ const fovText = this.panels.fov.FindChildTraverse('Value');
+ fovText.text = fov;
+ fovText.Submit();
+ }
+ }
+}
diff --git a/scripts/pages/settings/settings.js b/scripts/pages/settings/settings.js
index 29768eb5..f7038ccf 100644
--- a/scripts/pages/settings/settings.js
+++ b/scripts/pages/settings/settings.js
@@ -244,9 +244,7 @@ class MainMenuSettings {
static initPanelsRecursive(panel) {
// Initialise info panel event handlers
- if (this.isSettingsPanel(panel) || this.isSpeedometerPanel(panel)) {
- this.setPanelInfoEvents(panel);
- }
+ this.setPanelInfoEvents(panel);
// Initialise all the settings using persistent storage
// Only Enum and EnumDropDown are currently supported, others can be added when/if needed
@@ -312,15 +310,19 @@ class MainMenuSettings {
static setPanelInfoEvents(panel) {
const message = panel.GetAttributeString('infomessage', '');
+ const title = panel.GetAttributeString('infotitle', '');
+
+ // Don't set events if there's no info to show
+ if (!this.isSettingsPanel(panel) && message === '' && title === '' && !panel.convar && !panel.bind) return;
+
// Default to true if not set
const hasDocs = !(panel.GetAttributeString('hasdocspage', '') === 'false');
+
panel.SetPanelEvent('onmouseover', () => {
// Set onmouseover events for all settings panels
this.showInfo(
// If a panel has a specific title use that, if not use the panel's name. Child ID names vary between panel types, blame Valve
- panel.GetAttributeString('infotitle', '') ||
- panel.FindChildTraverse('Title')?.text ||
- panel.FindChildTraverse('title')?.text,
+ title || panel.FindChildTraverse('Title')?.text || panel.FindChildTraverse('title')?.text,
message,
panel.convar ?? panel.bind,
hasDocs,
@@ -434,8 +436,4 @@ class MainMenuSettings {
'ConVarColorDisplay'
].includes(panel.paneltype);
}
-
- static isSpeedometerPanel(panel) {
- return ['SpeedometersContainer', 'RangeColorProfilesContainer'].includes(panel.id);
- }
}
diff --git a/scripts/util/math.ts b/scripts/util/math.ts
index f9096b09..306f3906 100644
--- a/scripts/util/math.ts
+++ b/scripts/util/math.ts
@@ -121,3 +121,11 @@ function mapAngleToScreenDist(angle: number, fov: number, length: number, scale:
return Math.round((1 + Math.tan(angle * 0.5) / Math.tan(fov * 0.5)) * screenDist * 0.5);
}
}
+
+function deg2rad(x: number): number {
+ return (x / 180) * Math.PI;
+}
+
+function rad2deg(x: number): number {
+ return (x * 180) / Math.PI;
+}