Merge branch 'develop' into feat/polish-mission1
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled

This commit is contained in:
Tom Boullay
2026-06-01 00:15:46 +02:00
1075 changed files with 1242 additions and 1722 deletions
@@ -0,0 +1,36 @@
import { useCallback, useEffect, useRef, useState } from "react";
const DEFAULT_LOADING_DURATION_MS = 900;
export function useTransientLoadingIndicator(): {
showLoading: (durationMs?: number) => void;
visible: boolean;
} {
const [visible, setVisible] = useState(false);
const timeoutRef = useRef<number | null>(null);
const showLoading = useCallback(
(durationMs = DEFAULT_LOADING_DURATION_MS) => {
if (timeoutRef.current !== null) {
window.clearTimeout(timeoutRef.current);
}
setVisible(true);
timeoutRef.current = window.setTimeout(() => {
setVisible(false);
timeoutRef.current = null;
}, durationMs);
},
[],
);
useEffect(() => {
return () => {
if (timeoutRef.current !== null) {
window.clearTimeout(timeoutRef.current);
}
};
}, []);
return { showLoading, visible };
}
+15
View File
@@ -1,5 +1,20 @@
import { GRAPHICS_PRESETS } from "@/data/world/graphicsConfig";
import type {
GraphicsPreset,
GraphicsPresetConfig,
} from "@/data/world/graphicsConfig";
import { useWorldSettingsStore } from "@/managers/stores/useWorldSettingsStore";
export function useGraphicsPreset(): GraphicsPreset {
return useWorldSettingsStore((state) => state.graphics.preset);
}
export function useGraphicsPresetConfig(): GraphicsPresetConfig {
return useWorldSettingsStore(
(state) => GRAPHICS_PRESETS[state.graphics.preset],
);
}
export function useDynamicGrass(): boolean {
return useWorldSettingsStore((state) => state.graphics.dynamicGrass);
}
+64
View File
@@ -0,0 +1,64 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useFrame, useThree } from "@react-three/fiber";
import { CHUNK_CONFIG } from "@/data/world/chunkStreamingConfig";
import { selectMapModelPathByDistance } from "@/data/world/mapLodConfig";
import { useGraphicsPreset } from "@/hooks/world/useGraphicsSettings";
interface UseMapLodModelPathArgs {
modelName: string;
modelPath: string;
position: readonly [number, number, number];
}
export function useMapLodModelPath({
modelName,
modelPath,
position,
}: UseMapLodModelPathArgs): string {
const camera = useThree((state) => state.camera);
const graphicsPreset = useGraphicsPreset();
const lastUpdateRef = useRef(-CHUNK_CONFIG.updateInterval);
const initialModelPath = selectMapModelPathByDistance({
distance: Math.hypot(
position[0] - camera.position.x,
position[2] - camera.position.z,
),
modelName,
modelPath,
preset: graphicsPreset,
});
const activeModelPathRef = useRef(initialModelPath);
const [activeModelPath, setActiveModelPath] = useState(initialModelPath);
const updateModelPath = useCallback(() => {
const distance = Math.hypot(
position[0] - camera.position.x,
position[2] - camera.position.z,
);
const nextModelPath = selectMapModelPathByDistance({
distance,
modelName,
modelPath,
preset: graphicsPreset,
});
if (nextModelPath === activeModelPathRef.current) return;
activeModelPathRef.current = nextModelPath;
setActiveModelPath(nextModelPath);
}, [camera, graphicsPreset, modelName, modelPath, position]);
useEffect(() => {
updateModelPath();
}, [updateModelPath]);
useFrame(({ clock }) => {
const now = clock.elapsedTime * 1000;
if (now - lastUpdateRef.current < CHUNK_CONFIG.updateInterval) return;
lastUpdateRef.current = now;
updateModelPath();
});
return activeModelPath;
}
+23 -5
View File
@@ -1,4 +1,4 @@
import { useCallback, useMemo, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useFrame, useThree } from "@react-three/fiber";
import { CHUNK_CONFIG } from "@/data/world/chunkStreamingConfig";
@@ -8,6 +8,11 @@ export interface WorldChunkLike {
key: string;
}
interface WorldChunkVisibilityConfig {
loadRadius: number;
unloadRadius: number;
}
function areSetsEqual(a: ReadonlySet<string>, b: ReadonlySet<string>): boolean {
return a.size === b.size && [...a].every((key) => b.has(key));
}
@@ -15,6 +20,7 @@ function areSetsEqual(a: ReadonlySet<string>, b: ReadonlySet<string>): boolean {
export function useVisibleWorldChunks<TChunk extends WorldChunkLike>(
chunks: readonly TChunk[],
streamingEnabled: boolean,
visibilityConfig: WorldChunkVisibilityConfig = CHUNK_CONFIG,
): readonly TChunk[] {
const camera = useThree((state) => state.camera);
const lastUpdateRef = useRef(-CHUNK_CONFIG.updateInterval);
@@ -35,8 +41,8 @@ export function useVisibleWorldChunks<TChunk extends WorldChunkLike>(
);
const wasActive = activeChunkKeysRef.current.has(chunk.key);
const radius = wasActive
? CHUNK_CONFIG.unloadRadius
: CHUNK_CONFIG.loadRadius;
? visibilityConfig.unloadRadius
: visibilityConfig.loadRadius;
if (distance <= radius) {
nextKeys.add(chunk.key);
@@ -47,7 +53,18 @@ export function useVisibleWorldChunks<TChunk extends WorldChunkLike>(
activeChunkKeysRef.current = nextKeys;
setActiveChunkKeys(nextKeys);
}, [camera, chunks]);
}, [
camera,
chunks,
visibilityConfig.loadRadius,
visibilityConfig.unloadRadius,
]);
useEffect(() => {
if (!streamingEnabled) return;
updateActiveChunks();
}, [streamingEnabled, updateActiveChunks]);
useFrame(({ clock }) => {
if (!streamingEnabled) return;
@@ -71,7 +88,7 @@ export function useVisibleWorldChunks<TChunk extends WorldChunkLike>(
Math.hypot(
chunk.centerX - camera.position.x,
chunk.centerZ - camera.position.z,
) <= CHUNK_CONFIG.loadRadius
) <= visibilityConfig.loadRadius
);
});
}, [
@@ -80,5 +97,6 @@ export function useVisibleWorldChunks<TChunk extends WorldChunkLike>(
camera.position.z,
chunks,
streamingEnabled,
visibilityConfig.loadRadius,
]);
}