add: settings menu + menu store
This commit is contained in:
@@ -0,0 +1,203 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import { useSettingsStore } from "@/managers/stores/useSettingsStore";
|
||||||
|
import type {
|
||||||
|
RepairRuntime,
|
||||||
|
SubtitleLanguage,
|
||||||
|
} from "@/managers/stores/useSettingsStore";
|
||||||
|
|
||||||
|
function formatPercent(value: number): string {
|
||||||
|
return `${Math.round(value * 100)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearCookies(): void {
|
||||||
|
document.cookie.split(";").forEach((cookie) => {
|
||||||
|
const cookieName = cookie.split("=")[0]?.trim();
|
||||||
|
if (!cookieName) return;
|
||||||
|
|
||||||
|
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VolumeSliderProps {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
onChange: (value: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function VolumeSlider({
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: VolumeSliderProps): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<label className="game-settings-menu__slider" htmlFor={id}>
|
||||||
|
<span>
|
||||||
|
{label}
|
||||||
|
<strong>{formatPercent(value)}</strong>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
id={id}
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
step="0.01"
|
||||||
|
value={value}
|
||||||
|
onChange={(event) => onChange(Number(event.target.value))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GameSettingsMenu(): React.JSX.Element | null {
|
||||||
|
const {
|
||||||
|
isSettingsMenuOpen,
|
||||||
|
musicVolume,
|
||||||
|
sfxVolume,
|
||||||
|
dialogueVolume,
|
||||||
|
subtitlesEnabled,
|
||||||
|
subtitleLanguage,
|
||||||
|
repairRuntime,
|
||||||
|
setMusicVolume,
|
||||||
|
setSfxVolume,
|
||||||
|
setDialogueVolume,
|
||||||
|
setSettingsMenuOpen,
|
||||||
|
setSubtitlesEnabled,
|
||||||
|
setSubtitleLanguage,
|
||||||
|
setRepairRuntime,
|
||||||
|
} = useSettingsStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (event: KeyboardEvent): void => {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (!isSettingsMenuOpen) document.exitPointerLock();
|
||||||
|
setSettingsMenuOpen(!isSettingsMenuOpen);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown, { capture: true });
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", handleKeyDown, { capture: true });
|
||||||
|
};
|
||||||
|
}, [isSettingsMenuOpen, setSettingsMenuOpen]);
|
||||||
|
|
||||||
|
if (!isSettingsMenuOpen) return null;
|
||||||
|
|
||||||
|
const handleQuit = (): void => {
|
||||||
|
clearCookies();
|
||||||
|
window.location.assign("/");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="game-settings-menu" role="dialog" aria-modal="true">
|
||||||
|
<div className="game-settings-menu__panel">
|
||||||
|
<header className="game-settings-menu__header">
|
||||||
|
<div>
|
||||||
|
<span>Pause</span>
|
||||||
|
<h2>Options</h2>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="game-settings-menu__close"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSettingsMenuOpen(false)}
|
||||||
|
aria-label="Fermer le menu"
|
||||||
|
>
|
||||||
|
<X size={20} aria-hidden="true" />
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
{(["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>
|
||||||
|
|
||||||
|
<section
|
||||||
|
className="game-settings-menu__section"
|
||||||
|
aria-labelledby="repair-settings-heading"
|
||||||
|
>
|
||||||
|
<h3 id="repair-settings-heading">Repair game</h3>
|
||||||
|
<div className="game-settings-menu__choice-group game-settings-menu__choice-group--stacked">
|
||||||
|
{(["js", "python"] satisfies RepairRuntime[]).map((runtime) => (
|
||||||
|
<button
|
||||||
|
key={runtime}
|
||||||
|
type="button"
|
||||||
|
className={repairRuntime === runtime ? "active" : undefined}
|
||||||
|
onClick={() => setRepairRuntime(runtime)}
|
||||||
|
aria-pressed={repairRuntime === runtime}
|
||||||
|
>
|
||||||
|
{runtime === "js"
|
||||||
|
? "Repair game en JS (local)"
|
||||||
|
: "Repair game en Python (server)"}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="game-settings-menu__quit"
|
||||||
|
type="button"
|
||||||
|
onClick={handleQuit}
|
||||||
|
>
|
||||||
|
Quitter
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Crosshair } from "@/components/ui/Crosshair";
|
import { Crosshair } from "@/components/ui/Crosshair";
|
||||||
import { DebugOverlayLayout } from "@/components/ui/debug/DebugOverlayLayout";
|
import { DebugOverlayLayout } from "@/components/ui/debug/DebugOverlayLayout";
|
||||||
|
import { GameSettingsMenu } from "@/components/ui/GameSettingsMenu";
|
||||||
import { HandTrackingVisualizer } from "@/components/ui/HandTrackingVisualizer";
|
import { HandTrackingVisualizer } from "@/components/ui/HandTrackingVisualizer";
|
||||||
import { InteractPrompt } from "@/components/ui/InteractPrompt";
|
import { InteractPrompt } from "@/components/ui/InteractPrompt";
|
||||||
|
|
||||||
@@ -10,6 +11,7 @@ export function GameUI(): React.JSX.Element {
|
|||||||
<Crosshair />
|
<Crosshair />
|
||||||
<InteractPrompt />
|
<InteractPrompt />
|
||||||
<HandTrackingVisualizer />
|
<HandTrackingVisualizer />
|
||||||
|
<GameSettingsMenu />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+147
@@ -397,6 +397,153 @@ canvas {
|
|||||||
letter-spacing: 0.03em;
|
letter-spacing: 0.03em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* In-game settings menu */
|
||||||
|
.game-settings-menu {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 40;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
color: #ffffff;
|
||||||
|
pointer-events: auto;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-settings-menu__panel {
|
||||||
|
width: min(460px, 100%);
|
||||||
|
max-height: calc(100vh - 40px);
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 18px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
border-radius: 24px;
|
||||||
|
background: rgba(8, 8, 8, 0.94);
|
||||||
|
box-shadow: 0 28px 90px rgba(0, 0, 0, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-settings-menu__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 4px 4px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-settings-menu__header span {
|
||||||
|
color: #8f8f8f;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.16em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-settings-menu__header h2 {
|
||||||
|
margin: 0.25rem 0 0;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
letter-spacing: -0.06em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-settings-menu__close {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #111111;
|
||||||
|
color: #ffffff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-settings-menu__section {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px 4px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-settings-menu__section h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: #d7d7d7;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-settings-menu__slider {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-settings-menu__slider span,
|
||||||
|
.game-settings-menu__checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
color: #f2f2f2;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-settings-menu__slider strong {
|
||||||
|
color: #8f8f8f;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-settings-menu__slider input[type="range"] {
|
||||||
|
width: 100%;
|
||||||
|
accent-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-settings-menu__checkbox {
|
||||||
|
justify-content: flex-start;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-settings-menu__checkbox input {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
accent-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-settings-menu__choice-group {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-settings-menu__choice-group--stacked {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-settings-menu__choice-group button,
|
||||||
|
.game-settings-menu__quit {
|
||||||
|
width: 100%;
|
||||||
|
padding: 11px 12px;
|
||||||
|
border: 1px solid #242424;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: #101010;
|
||||||
|
color: #f2f2f2;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
font-weight: 680;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-settings-menu__choice-group button.active {
|
||||||
|
border-color: #ffffff;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #050505;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-settings-menu__quit {
|
||||||
|
margin-top: 8px;
|
||||||
|
border-color: rgba(248, 113, 113, 0.35);
|
||||||
|
color: #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
/* Debug overlay panels */
|
/* Debug overlay panels */
|
||||||
.debug-overlay-layout {
|
.debug-overlay-layout {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import { AudioManager } from "@/managers/AudioManager";
|
||||||
|
import type { AudioCategory } from "@/managers/AudioManager";
|
||||||
|
|
||||||
|
export type SubtitleLanguage = "fr" | "en";
|
||||||
|
export type RepairRuntime = "js" | "python";
|
||||||
|
|
||||||
|
interface SettingsState {
|
||||||
|
isSettingsMenuOpen: boolean;
|
||||||
|
musicVolume: number;
|
||||||
|
sfxVolume: number;
|
||||||
|
dialogueVolume: number;
|
||||||
|
subtitlesEnabled: boolean;
|
||||||
|
subtitleLanguage: SubtitleLanguage;
|
||||||
|
repairRuntime: RepairRuntime;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SettingsActions {
|
||||||
|
setSettingsMenuOpen: (open: boolean) => void;
|
||||||
|
setMusicVolume: (volume: number) => void;
|
||||||
|
setSfxVolume: (volume: number) => void;
|
||||||
|
setDialogueVolume: (volume: number) => void;
|
||||||
|
setSubtitlesEnabled: (enabled: boolean) => void;
|
||||||
|
setSubtitleLanguage: (language: SubtitleLanguage) => void;
|
||||||
|
setRepairRuntime: (runtime: RepairRuntime) => void;
|
||||||
|
resetSettings: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type SettingsStore = SettingsState & SettingsActions;
|
||||||
|
|
||||||
|
const DEFAULT_SETTINGS: SettingsState = {
|
||||||
|
isSettingsMenuOpen: false,
|
||||||
|
musicVolume: 1,
|
||||||
|
sfxVolume: 1,
|
||||||
|
dialogueVolume: 1,
|
||||||
|
subtitlesEnabled: true,
|
||||||
|
subtitleLanguage: "fr",
|
||||||
|
repairRuntime: "js",
|
||||||
|
};
|
||||||
|
|
||||||
|
function clampVolume(volume: number): number {
|
||||||
|
return Math.max(0, Math.min(1, volume));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAudioCategoryVolume(
|
||||||
|
category: AudioCategory,
|
||||||
|
volume: number,
|
||||||
|
): number {
|
||||||
|
const nextVolume = clampVolume(volume);
|
||||||
|
AudioManager.getInstance().setCategoryVolume(category, nextVolume);
|
||||||
|
return nextVolume;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyDefaultAudioSettings(): void {
|
||||||
|
AudioManager.getInstance().setCategoryVolume(
|
||||||
|
"music",
|
||||||
|
DEFAULT_SETTINGS.musicVolume,
|
||||||
|
);
|
||||||
|
AudioManager.getInstance().setCategoryVolume(
|
||||||
|
"sfx",
|
||||||
|
DEFAULT_SETTINGS.sfxVolume,
|
||||||
|
);
|
||||||
|
AudioManager.getInstance().setCategoryVolume(
|
||||||
|
"dialogue",
|
||||||
|
DEFAULT_SETTINGS.dialogueVolume,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
applyDefaultAudioSettings();
|
||||||
|
|
||||||
|
export const useSettingsStore = create<SettingsStore>()((set) => ({
|
||||||
|
...DEFAULT_SETTINGS,
|
||||||
|
setSettingsMenuOpen: (isSettingsMenuOpen) => set({ isSettingsMenuOpen }),
|
||||||
|
setMusicVolume: (volume) =>
|
||||||
|
set({ musicVolume: setAudioCategoryVolume("music", volume) }),
|
||||||
|
setSfxVolume: (volume) =>
|
||||||
|
set({ sfxVolume: setAudioCategoryVolume("sfx", volume) }),
|
||||||
|
setDialogueVolume: (volume) =>
|
||||||
|
set({ dialogueVolume: setAudioCategoryVolume("dialogue", volume) }),
|
||||||
|
setSubtitlesEnabled: (subtitlesEnabled) => set({ subtitlesEnabled }),
|
||||||
|
setSubtitleLanguage: (subtitleLanguage) => set({ subtitleLanguage }),
|
||||||
|
setRepairRuntime: (repairRuntime) => set({ repairRuntime }),
|
||||||
|
resetSettings: () => {
|
||||||
|
applyDefaultAudioSettings();
|
||||||
|
set(DEFAULT_SETTINGS);
|
||||||
|
},
|
||||||
|
}));
|
||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
PLAYER_XZ_DAMPING_FACTOR,
|
PLAYER_XZ_DAMPING_FACTOR,
|
||||||
} from "@/data/player/playerConfig";
|
} from "@/data/player/playerConfig";
|
||||||
import { InteractionManager } from "@/managers/InteractionManager";
|
import { InteractionManager } from "@/managers/InteractionManager";
|
||||||
|
import { useSettingsStore } from "@/managers/stores/useSettingsStore";
|
||||||
import type { Vector3Tuple } from "@/types/three/three";
|
import type { Vector3Tuple } from "@/types/three/three";
|
||||||
|
|
||||||
type Keys = {
|
type Keys = {
|
||||||
@@ -108,6 +109,8 @@ export function PlayerController({
|
|||||||
const interaction = InteractionManager.getInstance();
|
const interaction = InteractionManager.getInstance();
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent): void => {
|
const handleKeyDown = (event: KeyboardEvent): void => {
|
||||||
|
if (useSettingsStore.getState().isSettingsMenuOpen) return;
|
||||||
|
|
||||||
if (setMovementKey(keys.current, event.key, true)) {
|
if (setMovementKey(keys.current, event.key, true)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return;
|
return;
|
||||||
@@ -128,12 +131,15 @@ export function PlayerController({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyUp = (event: KeyboardEvent): void => {
|
const handleKeyUp = (event: KeyboardEvent): void => {
|
||||||
|
if (useSettingsStore.getState().isSettingsMenuOpen) return;
|
||||||
|
|
||||||
if (setMovementKey(keys.current, event.key, false)) {
|
if (setMovementKey(keys.current, event.key, false)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseDown = (event: MouseEvent): void => {
|
const handleMouseDown = (event: MouseEvent): void => {
|
||||||
|
if (useSettingsStore.getState().isSettingsMenuOpen) return;
|
||||||
if (event.button !== PRIMARY_INTERACT_MOUSE_BUTTON) return;
|
if (event.button !== PRIMARY_INTERACT_MOUSE_BUTTON) return;
|
||||||
if (interaction.getState().focused?.kind === "grab") {
|
if (interaction.getState().focused?.kind === "grab") {
|
||||||
interaction.pressInteract();
|
interaction.pressInteract();
|
||||||
@@ -141,6 +147,7 @@ export function PlayerController({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseUp = (event: MouseEvent): void => {
|
const handleMouseUp = (event: MouseEvent): void => {
|
||||||
|
if (useSettingsStore.getState().isSettingsMenuOpen) return;
|
||||||
if (event.button !== PRIMARY_INTERACT_MOUSE_BUTTON) return;
|
if (event.button !== PRIMARY_INTERACT_MOUSE_BUTTON) return;
|
||||||
if (interaction.getState().holding) {
|
if (interaction.getState().holding) {
|
||||||
interaction.releaseInteract();
|
interaction.releaseInteract();
|
||||||
@@ -162,6 +169,13 @@ export function PlayerController({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useFrame((_, delta) => {
|
useFrame((_, delta) => {
|
||||||
|
if (useSettingsStore.getState().isSettingsMenuOpen) {
|
||||||
|
keys.current = { ...DEFAULT_KEYS };
|
||||||
|
velocity.current.set(0, 0, 0);
|
||||||
|
wantsJump.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const dt = Math.min(delta, PLAYER_MAX_DELTA);
|
const dt = Math.min(delta, PLAYER_MAX_DELTA);
|
||||||
|
|
||||||
camera.getWorldDirection(_forward);
|
camera.getWorldDirection(_forward);
|
||||||
|
|||||||
Reference in New Issue
Block a user