Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 68253fae41 | |||
| 2dabb73d3d | |||
| 4f1b3b4ff3 | |||
| 627c8d4eb9 | |||
| 0b801888f0 | |||
| a180b89ee6 | |||
| 3e66e31117 | |||
| 2c194cdd2e | |||
| feaf502e44 | |||
| 489499f5d2 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -105,6 +105,9 @@ function GraphicsPresetButton({
|
||||
const lodLabel = config.forceLodModels
|
||||
? "LOD forcé"
|
||||
: `HD ${config.lodHighDetailDistance}m`;
|
||||
const chunkLabel = config.chunkStreamingEnabled
|
||||
? formatChunkDistance(config.chunkLoadRadius)
|
||||
: "All";
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -115,8 +118,7 @@ function GraphicsPresetButton({
|
||||
>
|
||||
<span>{config.label}</span>
|
||||
<small>
|
||||
{formatChunkDistance(config.chunkLoadRadius)} · {lodLabel} ·{" "}
|
||||
{config.fogEnabled ? "Fog" : "Clear"}
|
||||
{chunkLabel} · {lodLabel} · {config.fogEnabled ? "Fog" : "Clear"}
|
||||
</small>
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -14,6 +14,7 @@ export function MissionNotification({
|
||||
}: MissionNotificationProps): React.JSX.Element {
|
||||
const src =
|
||||
imagePath ?? (mission ? MISSION_NOTIFICATION_IMAGE_PATHS[mission] : "");
|
||||
const isVideo = src.toLowerCase().endsWith(".webm");
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -22,11 +23,24 @@ export function MissionNotification({
|
||||
>
|
||||
<div className="mission-notification__glow" />
|
||||
<span className="mission-notification__image-wrap">
|
||||
<img
|
||||
className="mission-notification__image"
|
||||
src={src}
|
||||
alt="Nouvel objectif de mission"
|
||||
/>
|
||||
{isVideo ? (
|
||||
<video
|
||||
className="mission-notification__image"
|
||||
src={src}
|
||||
aria-label="Nouvel objectif de mission"
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
preload="auto"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
className="mission-notification__image"
|
||||
src={src}
|
||||
alt="Nouvel objectif de mission"
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -22,7 +22,6 @@ export const EBIKE_WORLD_SCALE = 0.35;
|
||||
export const EBIKE_INTRO_BREAKDOWN_DISTANCE = 15;
|
||||
export const EBIKE_BREAKDOWN_DIALOGUE_DELAY_MS = 250;
|
||||
|
||||
export const EBIKE_MAX_SPEED = 3;
|
||||
export const EBIKE_ACCELERATION_DURATION_MS = 2000;
|
||||
export const EBIKE_DECELERATION_DURATION_MS = 2000;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ export const INTRO_MISSION_NOTIFICATION_IMAGE_PATH =
|
||||
|
||||
export const MISSION_NOTIFICATION_IMAGE_PATHS: Record<RepairMissionId, string> =
|
||||
{
|
||||
ebike: "/assets/world/UI/ebike-mission-notification.png",
|
||||
pylon: "/assets/world/UI/pylon-mission-notification.png",
|
||||
farm: "/assets/world/UI/farm-mission-notification.png",
|
||||
ebike: "/assets/world/UI/ebike-mission-notification.webm",
|
||||
pylon: "/assets/world/UI/pylon-mission-notification.webm",
|
||||
farm: "/assets/world/UI/farm-mission-notification.webm",
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ export const REPAIR_MISSIONS: Record<RepairMissionId, RepairMissionConfig> = {
|
||||
"Repair the damaged cooling module before relaunching the bike",
|
||||
modelPath: "/models/ebike/model.gltf",
|
||||
modelScale: 0.3,
|
||||
stageUiPath: "/assets/world/UI/ebike.webm",
|
||||
stageUiPath: "/assets/world/UI/ebike-mission-notification.webm",
|
||||
interactUiPath: REPAIR_INTERACT_UI_PATH,
|
||||
brokenUiPath: REPAIR_BROKEN_UI_PATH,
|
||||
case: DEFAULT_REPAIR_CASE,
|
||||
@@ -59,7 +59,7 @@ export const REPAIR_MISSIONS: Record<RepairMissionId, RepairMissionConfig> = {
|
||||
description:
|
||||
"Restore the pylon lamp relay and damaged panel before reconnecting the grid",
|
||||
modelPath: "/models/pylone/model.gltf",
|
||||
stageUiPath: "/assets/world/UI/centrale.webm",
|
||||
stageUiPath: "/assets/world/UI/pylon-mission-notification.webm",
|
||||
interactUiPath: REPAIR_INTERACT_UI_PATH,
|
||||
brokenUiPath: REPAIR_BROKEN_UI_PATH,
|
||||
case: DEFAULT_REPAIR_CASE,
|
||||
@@ -104,7 +104,7 @@ export const REPAIR_MISSIONS: Record<RepairMissionId, RepairMissionConfig> = {
|
||||
description:
|
||||
"Stabilize the irrigation loop and humidity sensor before restarting the farm",
|
||||
modelPath: "/models/fermeverticale/model.gltf",
|
||||
stageUiPath: "/assets/world/UI/laferme.webm",
|
||||
stageUiPath: "/assets/world/UI/farm-mission-notification.webm",
|
||||
interactUiPath: REPAIR_INTERACT_UI_PATH,
|
||||
brokenUiPath: REPAIR_BROKEN_UI_PATH,
|
||||
case: DEFAULT_REPAIR_CASE,
|
||||
|
||||
@@ -5,7 +5,7 @@ export const PLAYER_EYE_HEIGHT = 1.75;
|
||||
export const PLAYER_CAPSULE_RADIUS = 0.35;
|
||||
|
||||
export const PLAYER_WALK_SPEED = 5;
|
||||
export const PLAYER_EBIKE_SPEED = 30;
|
||||
export const PLAYER_EBIKE_SPEED = 20;
|
||||
export const PLAYER_AIR_CONTROL_FACTOR = 0.35;
|
||||
export const PLAYER_JUMP_SPEED = 9;
|
||||
export const PLAYER_GRAVITY = 30;
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
export const GRAPHICS_PRESET_KEYS = ["low", "medium", "high", "ultra"] as const;
|
||||
export const GRAPHICS_PRESET_KEYS = [
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"ultra",
|
||||
"max",
|
||||
] as const;
|
||||
|
||||
export type GraphicsPreset = (typeof GRAPHICS_PRESET_KEYS)[number];
|
||||
|
||||
export interface GraphicsPresetConfig {
|
||||
chunkLoadRadius: number;
|
||||
chunkStreamingEnabled: boolean;
|
||||
chunkUnloadRadius: number;
|
||||
fogEnabled: boolean;
|
||||
forceLodModels: boolean;
|
||||
@@ -16,6 +23,7 @@ export const GRAPHICS_PRESETS = {
|
||||
label: "Basse",
|
||||
chunkLoadRadius: 10,
|
||||
chunkUnloadRadius: 18,
|
||||
chunkStreamingEnabled: true,
|
||||
fogEnabled: true,
|
||||
forceLodModels: true,
|
||||
lodHighDetailDistance: 0,
|
||||
@@ -24,25 +32,37 @@ export const GRAPHICS_PRESETS = {
|
||||
label: "Moyenne",
|
||||
chunkLoadRadius: 20,
|
||||
chunkUnloadRadius: 30,
|
||||
chunkStreamingEnabled: true,
|
||||
fogEnabled: true,
|
||||
forceLodModels: true,
|
||||
lodHighDetailDistance: 0,
|
||||
},
|
||||
high: {
|
||||
label: "High",
|
||||
chunkLoadRadius: 35,
|
||||
chunkUnloadRadius: 45,
|
||||
chunkLoadRadius: 30,
|
||||
chunkUnloadRadius: 40,
|
||||
chunkStreamingEnabled: true,
|
||||
fogEnabled: false,
|
||||
forceLodModels: false,
|
||||
lodHighDetailDistance: 10,
|
||||
lodHighDetailDistance: 20,
|
||||
},
|
||||
ultra: {
|
||||
label: "Ultra",
|
||||
chunkLoadRadius: 50,
|
||||
chunkUnloadRadius: 65,
|
||||
chunkStreamingEnabled: true,
|
||||
fogEnabled: false,
|
||||
forceLodModels: false,
|
||||
lodHighDetailDistance: 20,
|
||||
lodHighDetailDistance: 30,
|
||||
},
|
||||
max: {
|
||||
label: "Max",
|
||||
chunkLoadRadius: 50,
|
||||
chunkUnloadRadius: 65,
|
||||
chunkStreamingEnabled: false,
|
||||
fogEnabled: false,
|
||||
forceLodModels: false,
|
||||
lodHighDetailDistance: 50,
|
||||
},
|
||||
} as const satisfies Record<GraphicsPreset, GraphicsPresetConfig>;
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ const SUN_LIGHT_COLOR = "#ffe2bf";
|
||||
|
||||
export const LIGHTING_DEFAULTS = {
|
||||
ambientColor: AMBIENT_LIGHT_COLOR,
|
||||
ambientIntensity: 0.9,
|
||||
ambientIntensity: 0.7,
|
||||
sunColor: SUN_LIGHT_COLOR,
|
||||
sunIntensity: 2.2,
|
||||
sunIntensity: 1.9,
|
||||
sunX: 70,
|
||||
sunY: 45,
|
||||
sunZ: 35,
|
||||
|
||||
@@ -14,6 +14,9 @@ export const MAP_LOD_MODEL_PATHS = {
|
||||
maison1: "/models/maison1-LOD/model.gltf",
|
||||
panneauaffichage: "/models/panneauaffichage-LOD/model.gltf",
|
||||
talkie: "/models/talkie-LOD/model.gltf",
|
||||
arbre: "/models/arbre-LOD/model.glb",
|
||||
buisson: "/models/buisson-LOD/model.glb",
|
||||
sapin: "/models/sapin-LOD/model.glb",
|
||||
} as const satisfies Record<string, string>;
|
||||
|
||||
export function getMapLodModelPath(modelName: string): string | null {
|
||||
@@ -22,6 +25,20 @@ export function getMapLodModelPath(modelName: string): string | null {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export const MAP_LOD_SCALE_MULTIPLIERS = {
|
||||
sapin: 0.35,
|
||||
buisson: 0.7,
|
||||
} as const satisfies Partial<Record<keyof typeof MAP_LOD_MODEL_PATHS, number>>;
|
||||
|
||||
export function getMapLodScaleMultiplier(modelName: string): number {
|
||||
return (
|
||||
MAP_LOD_SCALE_MULTIPLIERS[
|
||||
modelName as keyof typeof MAP_LOD_SCALE_MULTIPLIERS
|
||||
] ?? 1
|
||||
);
|
||||
}
|
||||
|
||||
export function selectMapModelPathByDistance({
|
||||
distance,
|
||||
modelName,
|
||||
|
||||
+1
-1
@@ -1544,7 +1544,7 @@ canvas {
|
||||
}
|
||||
|
||||
.game-settings-menu__choice-group--presets {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.game-settings-menu__choice-group button,
|
||||
|
||||
@@ -149,6 +149,7 @@ export function MapInstancingSystem({
|
||||
const streamingEnabled =
|
||||
streaming &&
|
||||
CHUNK_CONFIG.enabled &&
|
||||
graphicsPresetConfig.chunkStreamingEnabled &&
|
||||
sceneMode === "game" &&
|
||||
cameraMode === "player";
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
EBIKE_ACCELERATION_DURATION_MS,
|
||||
EBIKE_CAMERA_TRANSFORM,
|
||||
EBIKE_DECELERATION_DURATION_MS,
|
||||
EBIKE_MAX_SPEED,
|
||||
} from "@/data/ebike/ebikeConfig";
|
||||
|
||||
/** Global window properties used for ebike communication */
|
||||
@@ -415,7 +414,7 @@ export function PlayerController({
|
||||
}
|
||||
|
||||
const movementSpeed = isEbikeMounted
|
||||
? EBIKE_MAX_SPEED * ebikeSpeedFactor.current
|
||||
? currentSpeed * ebikeSpeedFactor.current
|
||||
: currentSpeed;
|
||||
const accel = onFloor.current
|
||||
? movementSpeed
|
||||
|
||||
@@ -1,8 +1,24 @@
|
||||
import { Suspense, useMemo } from "react";
|
||||
import {
|
||||
Suspense,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { CHUNK_CONFIG } from "@/data/world/chunkStreamingConfig";
|
||||
import {
|
||||
getMapLodModelPath,
|
||||
getMapLodScaleMultiplier,
|
||||
selectMapModelPathByDistance,
|
||||
} from "@/data/world/mapLodConfig";
|
||||
import { useCameraMode } from "@/hooks/debug/useCameraMode";
|
||||
import { useSceneMode } from "@/hooks/debug/useSceneMode";
|
||||
import { useGraphicsPresetConfig } from "@/hooks/world/useGraphicsSettings";
|
||||
import {
|
||||
useGraphicsPreset,
|
||||
useGraphicsPresetConfig,
|
||||
} from "@/hooks/world/useGraphicsSettings";
|
||||
import { useVisibleWorldChunks } from "@/hooks/world/useVisibleWorldChunks";
|
||||
import {
|
||||
isMapModelVisible,
|
||||
@@ -18,6 +34,7 @@ import {
|
||||
} from "@/data/world/vegetationConfig";
|
||||
import { isInsideLaFabrikFootprint } from "@/data/world/laFabrikConfig";
|
||||
import { createWorldInstanceChunks } from "@/utils/world/chunkInstances";
|
||||
import type { GraphicsPreset } from "@/data/world/graphicsConfig";
|
||||
|
||||
interface VegetationSystemProps {
|
||||
onlyMapName?: string | null;
|
||||
@@ -70,12 +87,78 @@ function removeLaFabrikVegetation(
|
||||
});
|
||||
}
|
||||
|
||||
function areChunkModelPathsEqual(
|
||||
a: ReadonlyMap<string, string>,
|
||||
b: ReadonlyMap<string, string>,
|
||||
): boolean {
|
||||
return (
|
||||
a.size === b.size && [...a].every(([key, value]) => b.get(key) === value)
|
||||
);
|
||||
}
|
||||
|
||||
function useVegetationChunkModelPaths(
|
||||
chunks: readonly VegetationChunk[],
|
||||
preset: GraphicsPreset,
|
||||
): ReadonlyMap<string, string> {
|
||||
const camera = useThree((state) => state.camera);
|
||||
const lastUpdateRef = useRef(-CHUNK_CONFIG.updateInterval);
|
||||
const modelPathsRef = useRef<Map<string, string>>(new Map());
|
||||
const [modelPaths, setModelPaths] = useState<ReadonlyMap<string, string>>(
|
||||
() => new Map(),
|
||||
);
|
||||
|
||||
const updateModelPaths = useCallback(() => {
|
||||
const cameraX = camera.position.x;
|
||||
const cameraZ = camera.position.z;
|
||||
const next = new Map<string, string>();
|
||||
|
||||
for (const chunk of chunks) {
|
||||
let nearestDistance = Number.POSITIVE_INFINITY;
|
||||
for (const instance of chunk.instances) {
|
||||
const distance = Math.hypot(
|
||||
instance.position[0] - cameraX,
|
||||
instance.position[2] - cameraZ,
|
||||
);
|
||||
if (distance < nearestDistance) nearestDistance = distance;
|
||||
}
|
||||
|
||||
const modelPath = selectMapModelPathByDistance({
|
||||
distance: nearestDistance,
|
||||
modelName: VEGETATION_TYPES[chunk.type].mapName,
|
||||
modelPath: chunk.modelPath,
|
||||
preset,
|
||||
});
|
||||
next.set(chunk.key, modelPath);
|
||||
}
|
||||
|
||||
if (areChunkModelPathsEqual(next, modelPathsRef.current)) return;
|
||||
|
||||
modelPathsRef.current = next;
|
||||
setModelPaths(next);
|
||||
}, [camera, chunks, preset]);
|
||||
|
||||
useEffect(() => {
|
||||
updateModelPaths();
|
||||
}, [updateModelPaths]);
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
const now = clock.elapsedTime * 1000;
|
||||
if (now - lastUpdateRef.current < CHUNK_CONFIG.updateInterval) return;
|
||||
lastUpdateRef.current = now;
|
||||
|
||||
updateModelPaths();
|
||||
});
|
||||
|
||||
return modelPaths;
|
||||
}
|
||||
|
||||
export function VegetationSystem({
|
||||
onlyMapName = null,
|
||||
streaming = true,
|
||||
}: VegetationSystemProps): React.JSX.Element | null {
|
||||
const cameraMode = useCameraMode();
|
||||
const sceneMode = useSceneMode();
|
||||
const graphicsPresetKey = useGraphicsPreset();
|
||||
const graphicsPreset = useGraphicsPresetConfig();
|
||||
const groups = useMapPerformanceStore((state) => state.groups);
|
||||
const models = useMapPerformanceStore((state) => state.models);
|
||||
@@ -83,6 +166,7 @@ export function VegetationSystem({
|
||||
const streamingEnabled =
|
||||
streaming &&
|
||||
CHUNK_CONFIG.enabled &&
|
||||
graphicsPreset.chunkStreamingEnabled &&
|
||||
sceneMode === "game" &&
|
||||
cameraMode === "player";
|
||||
|
||||
@@ -112,25 +196,38 @@ export function VegetationSystem({
|
||||
unloadRadius: graphicsPreset.chunkUnloadRadius,
|
||||
});
|
||||
|
||||
const chunkModelPaths = useVegetationChunkModelPaths(
|
||||
visibleChunks,
|
||||
graphicsPresetKey,
|
||||
);
|
||||
|
||||
if (isLoading || !data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<group name="vegetation-system">
|
||||
{visibleChunks.map((chunk) => (
|
||||
<Suspense key={chunk.key} fallback={null}>
|
||||
<InstancedVegetation
|
||||
modelPath={chunk.modelPath}
|
||||
instances={chunk.instances}
|
||||
scaleMultiplier={chunk.scaleMultiplier}
|
||||
castShadow={chunk.castShadow}
|
||||
receiveShadow={chunk.receiveShadow}
|
||||
windStrength={chunk.windStrength}
|
||||
rotationOffset={chunk.rotationOffset}
|
||||
/>
|
||||
</Suspense>
|
||||
))}
|
||||
{visibleChunks.map((chunk) => {
|
||||
const modelPath = chunkModelPaths.get(chunk.key) ?? chunk.modelPath;
|
||||
const mapName = VEGETATION_TYPES[chunk.type].mapName;
|
||||
const isLod = modelPath === getMapLodModelPath(mapName);
|
||||
const scaleMultiplier =
|
||||
chunk.scaleMultiplier *
|
||||
(isLod ? getMapLodScaleMultiplier(mapName) : 1);
|
||||
return (
|
||||
<Suspense key={`${chunk.key}:${modelPath}`} fallback={null}>
|
||||
<InstancedVegetation
|
||||
modelPath={modelPath}
|
||||
instances={chunk.instances}
|
||||
scaleMultiplier={scaleMultiplier}
|
||||
castShadow={chunk.castShadow}
|
||||
receiveShadow={chunk.receiveShadow}
|
||||
windStrength={chunk.windStrength}
|
||||
rotationOffset={chunk.rotationOffset}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
})}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user