feat(world): add map lod graphics presets

This commit is contained in:
Tom Boullay
2026-05-31 19:03:55 +02:00
parent 564a455520
commit 34c198ebfd
13 changed files with 717 additions and 131 deletions
+13 -1
View File
@@ -3,13 +3,25 @@ import {
MergedStaticMapModel,
type MergedStaticMapModelProps,
} from "@/components/three/world/MergedStaticMapModel";
import { getMapLodModelPath } from "@/data/world/mapLodConfig";
import { useMapLodModelPath } from "@/hooks/world/useMapLodModelPath";
const ECOLE_MODEL_PATH = "/models/ecole/model.gltf";
const ECOLE_LOD_MODEL_PATH = getMapLodModelPath("ecole");
type EcoleModelProps = Omit<MergedStaticMapModelProps, "modelPath">;
export function EcoleModel(props: EcoleModelProps): React.JSX.Element {
return <MergedStaticMapModel modelPath={ECOLE_MODEL_PATH} {...props} />;
const modelPath = useMapLodModelPath({
modelName: "ecole",
modelPath: ECOLE_MODEL_PATH,
position: props.position,
});
return <MergedStaticMapModel modelPath={modelPath} {...props} />;
}
useGLTF.preload(ECOLE_MODEL_PATH);
if (ECOLE_LOD_MODEL_PATH) {
useGLTF.preload(ECOLE_LOD_MODEL_PATH);
}
+158 -59
View File
@@ -1,7 +1,22 @@
import { useEffect } from "react";
import { RotateCcw, X } from "lucide-react";
import type { ReactNode } from "react";
import {
Captions,
Gauge,
LogOut,
Music2,
RotateCcw,
Volume2,
X,
} from "lucide-react";
import {
GRAPHICS_PRESET_KEYS,
GRAPHICS_PRESETS,
type GraphicsPreset,
} from "@/data/world/graphicsConfig";
import { useGameStore } from "@/managers/stores/useGameStore";
import { useSettingsStore } from "@/managers/stores/useSettingsStore";
import { useWorldSettingsStore } from "@/managers/stores/useWorldSettingsStore";
import type { SubtitleLanguage } from "@/types/settings/settings";
import { isDebugEnabled } from "@/utils/debug/isDebugEnabled";
@@ -21,6 +36,7 @@ function clearCookies(): void {
interface VolumeSliderProps {
id: string;
label: string;
icon: ReactNode;
value: number;
onChange: (value: number) => void;
}
@@ -28,13 +44,17 @@ interface VolumeSliderProps {
function VolumeSlider({
id,
label,
icon,
value,
onChange,
}: VolumeSliderProps): React.JSX.Element {
return (
<label className="game-settings-menu__slider" htmlFor={id}>
<span>
{label}
<em>
{icon}
{label}
</em>
<strong>{formatPercent(value)}</strong>
</span>
<input
@@ -50,8 +70,50 @@ function VolumeSlider({
);
}
function formatChunkDistance(distance: number): string {
return `${distance}m`;
}
interface GraphicsPresetButtonProps {
active: boolean;
preset: GraphicsPreset;
onSelect: (preset: GraphicsPreset) => void;
}
function GraphicsPresetButton({
active,
preset,
onSelect,
}: GraphicsPresetButtonProps): React.JSX.Element {
const config = GRAPHICS_PRESETS[preset];
const lodLabel = config.forceLodModels
? "LOD forcé"
: `HD ${config.lodHighDetailDistance}m`;
return (
<button
type="button"
className={active ? "active" : undefined}
onClick={() => onSelect(preset)}
aria-pressed={active}
>
<span>{config.label}</span>
<small>
{formatChunkDistance(config.chunkLoadRadius)} · {lodLabel} ·{" "}
{config.fogEnabled ? "Fog" : "Clear"}
</small>
</button>
);
}
export function GameSettingsMenu(): React.JSX.Element | null {
const resetGame = useGameStore((state) => state.resetGame);
const graphicsPreset = useWorldSettingsStore(
(state) => state.graphics.preset,
);
const setGraphicsPreset = useWorldSettingsStore(
(state) => state.setGraphicsPreset,
);
const {
isSettingsMenuOpen,
musicVolume,
@@ -103,8 +165,8 @@ export function GameSettingsMenu(): React.JSX.Element | null {
<div className="game-settings-menu__panel">
<header className="game-settings-menu__header">
<div>
<span>Pause</span>
<h2>Options</h2>
<span>La Fabrik</span>
<h2>Pause</h2>
</div>
<button
className="game-settings-menu__close"
@@ -116,62 +178,98 @@ export function GameSettingsMenu(): React.JSX.Element | null {
</button>
</header>
<section
className="game-settings-menu__section"
aria-labelledby="audio-settings-heading"
>
<h3 id="audio-settings-heading">Audio</h3>
<VolumeSlider
id="music-volume"
label="Musique"
value={musicVolume}
onChange={setMusicVolume}
/>
<VolumeSlider
id="sfx-volume"
label="Sound effects"
value={sfxVolume}
onChange={setSfxVolume}
/>
<VolumeSlider
id="dialogue-volume"
label="Dialogue"
value={dialogueVolume}
onChange={setDialogueVolume}
/>
</section>
<section
className="game-settings-menu__section"
aria-labelledby="subtitle-settings-heading"
>
<h3 id="subtitle-settings-heading">Sous-titres</h3>
<label className="game-settings-menu__checkbox">
<input
type="checkbox"
checked={subtitlesEnabled}
onChange={(event) => setSubtitlesEnabled(event.target.checked)}
/>
Afficher sous-titres
</label>
<div
className="game-settings-menu__choice-group"
aria-label="Langue des sous-titres"
<div className="game-settings-menu__grid">
<section
className="game-settings-menu__section game-settings-menu__section--wide"
aria-labelledby="graphics-settings-heading"
>
{(["fr", "en"] satisfies SubtitleLanguage[]).map((language) => (
<button
key={language}
type="button"
className={subtitleLanguage === language ? "active" : undefined}
onClick={() => setSubtitleLanguage(language)}
aria-pressed={subtitleLanguage === language}
>
{language === "fr" ? "Francais" : "English"}
</button>
))}
</div>
</section>
<div className="game-settings-menu__section-title">
<Gauge size={16} aria-hidden="true" />
<h3 id="graphics-settings-heading">Performance</h3>
</div>
<div
className="game-settings-menu__choice-group game-settings-menu__choice-group--presets"
aria-label="Preset graphique"
>
{GRAPHICS_PRESET_KEYS.map((preset) => (
<GraphicsPresetButton
key={preset}
preset={preset}
active={graphicsPreset === preset}
onSelect={setGraphicsPreset}
/>
))}
</div>
</section>
<section
className="game-settings-menu__section"
aria-labelledby="audio-settings-heading"
>
<div className="game-settings-menu__section-title">
<Volume2 size={16} aria-hidden="true" />
<h3 id="audio-settings-heading">Audio</h3>
</div>
<VolumeSlider
id="music-volume"
icon={<Music2 size={14} aria-hidden="true" />}
label="Musique"
value={musicVolume}
onChange={setMusicVolume}
/>
<VolumeSlider
id="sfx-volume"
icon={<Volume2 size={14} aria-hidden="true" />}
label="Effets"
value={sfxVolume}
onChange={setSfxVolume}
/>
<VolumeSlider
id="dialogue-volume"
icon={<Captions size={14} aria-hidden="true" />}
label="Dialogue"
value={dialogueVolume}
onChange={setDialogueVolume}
/>
</section>
<section
className="game-settings-menu__section"
aria-labelledby="subtitle-settings-heading"
>
<div className="game-settings-menu__section-title">
<Captions size={16} aria-hidden="true" />
<h3 id="subtitle-settings-heading">Sous-titres</h3>
</div>
<label className="game-settings-menu__checkbox">
<input
type="checkbox"
checked={subtitlesEnabled}
onChange={(event) => setSubtitlesEnabled(event.target.checked)}
/>
Afficher sous-titres
</label>
<div
className="game-settings-menu__choice-group"
aria-label="Langue des sous-titres"
>
{(["fr", "en"] satisfies SubtitleLanguage[]).map((language) => (
<button
key={language}
type="button"
className={
subtitleLanguage === language ? "active" : undefined
}
onClick={() => setSubtitleLanguage(language)}
aria-pressed={subtitleLanguage === language}
>
<span>{language === "fr" ? "Français" : "English"}</span>
</button>
))}
</div>
</section>
</div>
{showDebugRestart ? (
<button
@@ -189,6 +287,7 @@ export function GameSettingsMenu(): React.JSX.Element | null {
type="button"
onClick={handleQuit}
>
<LogOut size={14} aria-hidden="true" />
Quitter
</button>
</div>