diff --git a/client/src/three/GameRenderer.ts b/client/src/three/GameRenderer.ts index c8e254b6a..4c6fbd618 100644 --- a/client/src/three/GameRenderer.ts +++ b/client/src/three/GameRenderer.ts @@ -1,7 +1,7 @@ import { SetupResult } from "@/dojo/setup"; import useUIStore, { AppStore } from "@/hooks/store/useUIStore"; import { SceneName } from "@/types"; -import { IS_LOW_GRAPHICS_ENABLED } from "@/ui/config"; +import { GRAPHICS_SETTING, GraphicsSettings } from "@/ui/config"; import throttle from "lodash/throttle"; import { BloomEffect, @@ -69,10 +69,10 @@ export default class GameRenderer { private sceneManager!: SceneManager; private systemManager!: SystemManager; - private isLowGraphicsMode: boolean; + private graphicsSetting: GraphicsSettings; constructor(dojoContext: SetupResult) { - this.isLowGraphicsMode = IS_LOW_GRAPHICS_ENABLED; + this.graphicsSetting = GRAPHICS_SETTING; this.initializeRenderer(); this.dojo = dojoContext; this.locationManager = new LocationManager(); @@ -165,16 +165,13 @@ export default class GameRenderer { this.renderer = new THREE.WebGLRenderer({ powerPreference: "high-performance", antialias: false, - stencil: false, - depth: false, + stencil: this.graphicsSetting === GraphicsSettings.LOW, + depth: this.graphicsSetting === GraphicsSettings.LOW, }); - this.renderer.setPixelRatio(this.isLowGraphicsMode ? 0.75 : window.devicePixelRatio); - this.renderer.shadowMap.enabled = !this.isLowGraphicsMode; + this.renderer.setPixelRatio(this.graphicsSetting !== GraphicsSettings.HIGH ? 0.75 : window.devicePixelRatio); + this.renderer.shadowMap.enabled = this.graphicsSetting !== GraphicsSettings.LOW; this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; - this.renderer.setSize( - this.isLowGraphicsMode ? window.innerWidth : window.innerWidth, - this.isLowGraphicsMode ? window.innerHeight : window.innerHeight, - ); + this.renderer.setSize(window.innerWidth, window.innerHeight); this.renderer.toneMapping = THREE.NoToneMapping; this.renderer.toneMappingExposure = 1; this.renderer.autoClear = false; @@ -207,7 +204,7 @@ export default class GameRenderer { this.controls.enableDamping = true; this.controls.dampingFactor = 0.1; this.controls.target.set(0, 0, 0); - if (this.isLowGraphicsMode) { + if (this.graphicsSetting !== GraphicsSettings.HIGH) { this.controls.enableDamping = false; } this.controls.addEventListener( @@ -296,41 +293,45 @@ export default class GameRenderer { const obj = { brightness: -0.1, contrast: 0 }; const folder = GUIManager.addFolder("BrightnesContrastt"); - const BCEffect = new BrightnessContrastEffect({ - brightness: obj.brightness, - contrast: obj.contrast, - }); - folder - .add(obj, "brightness") - .name("Brightness") - .min(-1) - .max(1) - .step(0.01) - .onChange((value: number) => { - BCEffect.brightness = value; - }); - folder - .add(obj, "contrast") - .name("Contrast") - .min(-1) - .max(1) - .step(0.01) - .onChange((value: number) => { - BCEffect.contrast = value; + if (GRAPHICS_SETTING !== GraphicsSettings.LOW) { + const BCEffect = new BrightnessContrastEffect({ + brightness: obj.brightness, + contrast: obj.contrast, }); - this.composer.addPass( - new EffectPass( - this.camera, - - new FXAAEffect(), - new BloomEffect({ - luminanceThreshold: 1.1, - mipmapBlur: true, - intensity: 0.25, - }), - BCEffect, - ), - ); + + folder + .add(obj, "brightness") + .name("Brightness") + .min(-1) + .max(1) + .step(0.01) + .onChange((value: number) => { + BCEffect.brightness = value; + }); + folder + .add(obj, "contrast") + .name("Contrast") + .min(-1) + .max(1) + .step(0.01) + .onChange((value: number) => { + BCEffect.contrast = value; + }); + + this.composer.addPass( + new EffectPass( + this.camera, + + new FXAAEffect(), + new BloomEffect({ + luminanceThreshold: 1.1, + mipmapBlur: true, + intensity: 0.25, + }), + BCEffect, + ), + ); + } this.sceneManager.moveCameraForScene(); } @@ -391,8 +392,19 @@ export default class GameRenderer { }); return; } + const currentTime = performance.now(); const deltaTime = (currentTime - this.lastTime) / 1000; // Convert to seconds + + // Skip frame if not enough time has passed (for 30 FPS) + if (this.graphicsSetting !== GraphicsSettings.HIGH) { + const frameTime = 1000 / 30; // 33.33ms for 30 FPS + if (currentTime - this.lastTime < frameTime) { + requestAnimationFrame(() => this.animate()); + return; + } + } + this.lastTime = currentTime; if (this.stats) this.stats.update(); @@ -421,7 +433,6 @@ export default class GameRenderer { this.renderer.render(this.hudScene.getScene(), this.hudScene.getCamera()); this.labelRenderer.render(this.hudScene.getScene(), this.hudScene.getCamera()); - // Update the minimap requestAnimationFrame(() => { this.animate(); }); diff --git a/client/src/three/components/ArmyModel.ts b/client/src/three/components/ArmyModel.ts index f2a8ac156..3901cf448 100644 --- a/client/src/three/components/ArmyModel.ts +++ b/client/src/three/components/ArmyModel.ts @@ -1,4 +1,4 @@ -import { IS_LOW_GRAPHICS_ENABLED } from "@/ui/config"; +import { GRAPHICS_SETTING, GraphicsSettings } from "@/ui/config"; import * as THREE from "three"; import { AnimationClip, AnimationMixer } from "three"; import { gltfLoader } from "../helpers/utils"; @@ -159,6 +159,10 @@ export class ArmyModel { updateAnimations(deltaTime: number) { const time = performance.now() * 0.001; + if (GRAPHICS_SETTING === GraphicsSettings.LOW) { + return; + } + this.models.forEach((modelData) => { let needsMatrixUpdate = false; @@ -184,7 +188,7 @@ export class ArmyModel { for (let i = 0; i < modelData.mesh.count; i++) { const animationState = this.animationStates[i]; - if (IS_LOW_GRAPHICS_ENABLED && animationState === ANIMATION_STATE_IDLE) { + if (GRAPHICS_SETTING === GraphicsSettings.MID && animationState === ANIMATION_STATE_IDLE) { continue; } diff --git a/client/src/three/components/BattleModel.tsx b/client/src/three/components/BattleModel.tsx index c1d16db40..1c7911f43 100644 --- a/client/src/three/components/BattleModel.tsx +++ b/client/src/three/components/BattleModel.tsx @@ -1,3 +1,4 @@ +import { GRAPHICS_SETTING, GraphicsSettings } from "@/ui/config"; import * as THREE from "three"; import { AnimationClip, AnimationMixer } from "three"; import { gltfLoader } from "../helpers/utils"; @@ -74,6 +75,10 @@ export class BattleModel { } updateAnimations(deltaTime: number) { + if (GRAPHICS_SETTING === GraphicsSettings.LOW) { + return; + } + if (this.mixer && this.mesh && this.animation) { const time = performance.now() * 0.001; for (let i = 0; i < this.mesh.count; i++) { diff --git a/client/src/three/components/InstancedBiome.tsx b/client/src/three/components/InstancedBiome.tsx index 0a3979416..e97364d41 100644 --- a/client/src/three/components/InstancedBiome.tsx +++ b/client/src/three/components/InstancedBiome.tsx @@ -1,3 +1,4 @@ +import { GRAPHICS_SETTING, GraphicsSettings } from "@/ui/config"; import * as THREE from "three"; import { AnimationClip, AnimationMixer } from "three"; import { PREVIEW_BUILD_COLOR_INVALID } from "../scenes/constants"; @@ -43,7 +44,7 @@ export default class InstancedModel { tmp.userData.isInstanceModel = true; if (!enableRaycast) { - tmp.raycast = () => {}; + tmp.raycast = () => { }; } this.mixer = new AnimationMixer(gltf.scene); @@ -137,6 +138,10 @@ export default class InstancedModel { } updateAnimations(deltaTime: number) { + if (GRAPHICS_SETTING === GraphicsSettings.LOW) { + return; + } + if (this.mixer && this.animation) { const time = performance.now() * 0.001; this.instancedMeshes.forEach((mesh, meshIndex) => { diff --git a/client/src/three/components/InstancedModel.tsx b/client/src/three/components/InstancedModel.tsx index cfc1a605a..9b42733cd 100644 --- a/client/src/three/components/InstancedModel.tsx +++ b/client/src/three/components/InstancedModel.tsx @@ -1,3 +1,4 @@ +import { GRAPHICS_SETTING, GraphicsSettings } from "@/ui/config"; import { ResourcesIds, StructureType } from "@bibliothecadao/eternum"; import * as THREE from "three"; import { AnimationClip, AnimationMixer } from "three"; @@ -73,7 +74,7 @@ export default class InstancedModel { tmp.userData.isInstanceModel = true; if (!enableRaycast) { - tmp.raycast = () => {}; + tmp.raycast = () => { }; } this.mixer = new AnimationMixer(gltf.scene); @@ -168,6 +169,10 @@ export default class InstancedModel { } updateAnimations(deltaTime: number) { + if (GRAPHICS_SETTING === GraphicsSettings.LOW) { + return; + } + if (this.mixer && this.animation) { const time = performance.now() * 0.001; this.instancedMeshes.forEach((mesh, meshIndex) => { diff --git a/client/src/ui/config.tsx b/client/src/ui/config.tsx index 4d6365f53..f51a3d559 100644 --- a/client/src/ui/config.tsx +++ b/client/src/ui/config.tsx @@ -3,29 +3,47 @@ import { BuildingType, FELT_CENTER } from "@bibliothecadao/eternum"; export { FELT_CENTER }; -const checkIfGameIsRunningOnLaptop = async () => { +export enum GraphicsSettings { + LOW = "LOW", + MID = "MID", + HIGH = "HIGH" +} + +const checkGraphicsSettings = async () => { + // Handle migration from old LOW_GRAPHICS_FLAG + const oldLowGraphicsFlag = localStorage.getItem("LOW_GRAPHICS_FLAG"); + if (oldLowGraphicsFlag !== null) { + // Migrate old setting to new format + const newSetting = oldLowGraphicsFlag === "true" ? GraphicsSettings.LOW : GraphicsSettings.HIGH; + localStorage.setItem("GRAPHICS_SETTING", newSetting); + localStorage.removeItem("LOW_GRAPHICS_FLAG"); // Clean up old setting + return newSetting; + } + + // Check if initial laptop check has been done if (!localStorage.getItem("INITIAL_LAPTOP_CHECK")) { try { const battery = await (navigator as any).getBattery(); if (battery.charging && battery.chargingTime === 0) { // It's likely a desktop - localStorage.setItem("LOW_GRAPHICS_FLAG", "false"); + localStorage.setItem("GRAPHICS_SETTING", GraphicsSettings.HIGH); } else { - // It's likely a laptop or mobile device. - localStorage.setItem("LOW_GRAPHICS_FLAG", "true"); + // It's likely a laptop or mobile device + localStorage.setItem("GRAPHICS_SETTING", GraphicsSettings.LOW); } } catch (error) { console.error("Error calling getBattery():", error); // Set default values if getBattery() is not supported - localStorage.setItem("LOW_GRAPHICS_FLAG", "true"); + localStorage.setItem("GRAPHICS_SETTING", GraphicsSettings.LOW); } finally { localStorage.setItem("INITIAL_LAPTOP_CHECK", "true"); } } - return localStorage.getItem("LOW_GRAPHICS_FLAG") === "true"; + + return localStorage.getItem("GRAPHICS_SETTING") as GraphicsSettings || GraphicsSettings.HIGH; }; -export const IS_LOW_GRAPHICS_ENABLED = await checkIfGameIsRunningOnLaptop(); +export const GRAPHICS_SETTING = await checkGraphicsSettings(); export const IS_MOBILE = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); diff --git a/client/src/ui/modules/settings/Settings.tsx b/client/src/ui/modules/settings/Settings.tsx index 6abbe64f0..700b8e54d 100644 --- a/client/src/ui/modules/settings/Settings.tsx +++ b/client/src/ui/modules/settings/Settings.tsx @@ -12,7 +12,7 @@ import { useMusicPlayer } from "@/hooks/useMusic"; import useScreenOrientation from "@/hooks/useScreenOrientation"; import { settings } from "@/ui/components/navigation/Config"; import { OSWindow } from "@/ui/components/navigation/OSWindow"; -import { IS_LOW_GRAPHICS_ENABLED } from "@/ui/config"; +import { GraphicsSettings } from "@/ui/config"; import Avatar from "@/ui/elements/Avatar"; import Button from "@/ui/elements/Button"; import { Checkbox } from "@/ui/elements/Checkbox"; @@ -61,10 +61,11 @@ export const SettingsWindow = () => { const isOpen = useUIStore((state) => state.isPopupOpen(settings)); - const isLowGraphics = IS_LOW_GRAPHICS_ENABLED; + const GRAPHICS_SETTING = localStorage.getItem("GRAPHICS_SETTING") as GraphicsSettings || GraphicsSettings.HIGH; + return ( togglePopup(settings)} show={isOpen} title={settings}> -
+
setShowSettings(!showSettings)} @@ -93,25 +94,31 @@ export const SettingsWindow = () => { Graphics
+ ) : ( )}