Merge branch 'develop' into feat/polish-mission-2

This commit is contained in:
math-pixel
2026-06-02 20:27:48 +02:00
309 changed files with 2650 additions and 1127 deletions
+9 -1
View File
@@ -1,13 +1,15 @@
import type { Vector3Tuple } from "@/types/three/three";
import type { RepairMissionId } from "@/types/gameplay/repairMission";
const DEG_TO_RAD = Math.PI / 180;
export const TEST_SCENE_FLOOR_POSITION: Vector3Tuple = [0, -0.5, 0];
export const TEST_SCENE_FLOOR_SIZE: Vector3Tuple = [200, 1, 200];
export const TEST_SCENE_FLOOR_COLLIDER_HALF_EXTENTS: Vector3Tuple = [
100, 0.5, 100,
];
export const TEST_SCENE_GRABBABLE_POSITION: Vector3Tuple = [0, 1, -3];
export const TEST_SCENE_GRABBABLE_POSITION: Vector3Tuple = [0, 0.25, -3];
export const TEST_SCENE_GRABBABLE_BOX_SIZE: Vector3Tuple = [0.5, 0.5, 0.5];
export const TEST_SCENE_GRABBABLE_COLOR = "#e07b39";
export const TEST_SCENE_GRABBABLE_ROUGHNESS = 0.6;
@@ -23,6 +25,12 @@ export const TEST_SCENE_TRIGGER_METALNESS = 0.5;
export const TEST_SCENE_REPAIR_ZONE_MARKER_RADIUS = 1.65;
export const TEST_SCENE_REPAIR_ZONE_MARKER_TUBE_RADIUS = 0.045;
export const TEST_SCENE_GPS_PREVIEW_POSITION: Vector3Tuple = [0, 5, -4.8];
export const TEST_SCENE_GPS_PREVIEW_ROTATION: Vector3Tuple = [
-33 * DEG_TO_RAD,
0,
0,
];
export const GAME_REPAIR_ZONES = [
{
+6 -6
View File
@@ -6,22 +6,22 @@ export interface CameraTransform {
}
export const EBIKE_CAMERA_TRANSFORM: CameraTransform = {
position: [-3.5, 6, 0],
position: [-2.6, 4.5, 0],
rotation: [-10, -90, 0],
};
export const EBIKE_DROP_PLAYER_TRANSFORM: CameraTransform = {
position: [0, 1.5, -3],
position: [0, 1.3, -2.25],
rotation: [0, 0, 0],
};
export const EBIKE_WORLD_POSITION: Vector3Tuple = [61.5, 10, 62.4];
export const EBIKE_WORLD_ROTATION_Y = 2.4107;
export const EBIKE_WORLD_POSITION: Vector3Tuple = [65, 0.8, 72];
export const EBIKE_WORLD_ROTATION_Y = -2.5;
export const EBIKE_WORLD_SCALE = 0.35;
export const EBIKE_INTRO_RIDE_DURATION_MS = 5000;
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;
+1 -1
View File
@@ -143,7 +143,7 @@ export const galleryModels: GalleryModel[] = [
},
{ id: "sapin", name: "Sapin", path: "/models/sapin/model.gltf" },
{ id: "skybox", name: "Skybox", path: "/models/skybox/skybox.gltf" },
{ id: "talkie", name: "Talkie", path: "/models/talkie/model.gltf" },
{ id: "talkie", name: "Talkie", path: "/models/talkie/model.glb" },
{ id: "terrain", name: "Terrain", path: "/models/terrain/model.gltf" },
{
id: "tuyauxlac",
+6 -3
View File
@@ -1,8 +1,11 @@
import type { RepairMissionId } from "@/types/gameplay/repairMission";
export const INTRO_MISSION_NOTIFICATION_IMAGE_PATH =
"/assets/world/UI/intro-mission-notification.png";
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",
};
+3 -13
View File
@@ -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,
@@ -41,11 +41,6 @@ export const REPAIR_MISSIONS: Record<RepairMissionId, RepairMissionConfig> = {
label: "Replacement cooling core",
modelPath: "/models/refroidisseur/model.gltf",
},
{
id: "ebike-radio-distractor",
label: "Radio module",
modelPath: "/models/talkie/model.gltf",
},
{
id: "ebike-glove-distractor",
label: "Insulation glove",
@@ -59,7 +54,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 +99,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,
@@ -134,11 +129,6 @@ export const REPAIR_MISSIONS: Record<RepairMissionId, RepairMissionConfig> = {
label: "Tree sensor",
modelPath: "/models/sapin/model.gltf",
},
{
id: "farm-radio-distractor",
label: "Radio module",
modelPath: "/models/talkie/model.gltf",
},
],
},
};
+6
View File
@@ -8,3 +8,9 @@ export const HAND_TRACKING_BROWSER_WASM_URL =
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.35/wasm";
export const HAND_TRACKING_BROWSER_MODEL_URL =
"https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task";
export const HAND_TRACKING_BROWSER_DELEGATE: "CPU" | "GPU" = "CPU";
// Delay before the runtime actually starts after `enabled` flips to true.
// Absorbs React StrictMode's mount/unmount/mount cycle in dev and rapid
// `nearby` toggles at trigger borders. Invisible to the user (~5 frames).
export const HAND_TRACKING_RUNTIME_START_DELAY_MS = 80;
+11 -2
View File
@@ -1,4 +1,5 @@
import type { Vector3Tuple } from "@/types/three/three";
import { LA_FABRIK_PLAYER_SPAWN } from "@/data/world/laFabrikConfig";
export const PLAYER_EYE_HEIGHT = 1.75;
export const PLAYER_CAPSULE_RADIUS = 0.35;
@@ -14,5 +15,13 @@ export const PLAYER_XZ_DAMPING_FACTOR = 8;
export const PLAYER_FALL_RESPAWN_Y = -20;
export const PLAYER_FALL_RESPAWN_DELAY = 3;
export const PLAYER_SPAWN_POSITION_GAME: Vector3Tuple = [59.5, 10, 64.64];
export const PLAYER_SPAWN_POSITION_PHYSICS: Vector3Tuple = [0, 3, 0];
export const PLAYER_SPAWN_POSITION_GAME: Vector3Tuple = [
LA_FABRIK_PLAYER_SPAWN[0] + 1,
LA_FABRIK_PLAYER_SPAWN[1],
LA_FABRIK_PLAYER_SPAWN[2] - 1,
];
export const PLAYER_SPAWN_POSITION_PHYSICS: Vector3Tuple = [
0,
PLAYER_EYE_HEIGHT,
0,
];
+1 -1
View File
@@ -1,6 +1,6 @@
import type { CSSProperties } from "react";
const BACKGROUND_IMAGE = "/assets/bg-site.png";
const BACKGROUND_IMAGE = "/assets/bg-site.webp";
export const SITE_CONFIG = {
backgroundImage: BACKGROUND_IMAGE,
+1 -1
View File
@@ -19,7 +19,7 @@ export const CLOUD_DEFAULTS = {
maxRotation: Math.PI * 2,
minSpeedMultiplier: 0.4,
maxSpeedMultiplier: 1,
castShadow: false,
castShadow: true,
receiveShadow: false,
};
+25 -7
View File
@@ -1,11 +1,16 @@
import { CHUNK_CONFIG } from "@/data/world/chunkStreamingConfig";
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;
@@ -18,6 +23,7 @@ export const GRAPHICS_PRESETS = {
label: "Basse",
chunkLoadRadius: 10,
chunkUnloadRadius: 18,
chunkStreamingEnabled: true,
fogEnabled: true,
forceLodModels: true,
lodHighDetailDistance: 0,
@@ -26,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: CHUNK_CONFIG.loadRadius,
chunkUnloadRadius: CHUNK_CONFIG.unloadRadius,
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>;
+29
View File
@@ -0,0 +1,29 @@
import type { Vector3Tuple } from "@/types/three/three";
export const LA_FABRIK_CENTER: Vector3Tuple = [59.4973, 6.2746, 64.6354];
export const LA_FABRIK_ROTATION_Y = 2.4107;
export const LA_FABRIK_HALF_EXTENTS = {
x: 8.5,
z: 7.5,
} as const;
export const LA_FABRIK_PLAYER_SPAWN: Vector3Tuple = [59.5, 6.3, 64.64];
export const LA_FABRIK_INITIAL_LOOK_AT: Vector3Tuple = [58, 7.3, 62.5];
export const LA_FABRIK_INTERIOR_LIGHT_POSITION: Vector3Tuple = [59.5, 9, 64.64];
export function isInsideLaFabrikFootprint(
x: number,
z: number,
padding = 0,
): boolean {
const dx = x - LA_FABRIK_CENTER[0];
const dz = z - LA_FABRIK_CENTER[2];
const cos = Math.cos(-LA_FABRIK_ROTATION_Y);
const sin = Math.sin(-LA_FABRIK_ROTATION_Y);
const localX = dx * cos - dz * sin;
const localZ = dx * sin + dz * cos;
return (
Math.abs(localX) <= LA_FABRIK_HALF_EXTENTS.x + padding &&
Math.abs(localZ) <= LA_FABRIK_HALF_EXTENTS.z + padding
);
}
+11 -2
View File
@@ -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,
@@ -30,3 +30,12 @@ export const SUN_Y_STEP = 1;
export const SUN_Z_MIN = -100;
export const SUN_Z_MAX = 100;
export const SUN_Z_STEP = 1;
export const SHADOW_CONFIG = {
mapSize: 2048,
cameraSize: 95,
cameraNear: 0.5,
cameraFar: 300,
bias: 0,
normalBias: 0,
} as const;
+16 -1
View File
@@ -13,7 +13,9 @@ export const MAP_LOD_MODEL_PATHS = {
lafabrik: "/models/lafabrik-LOD/model.gltf",
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 +24,19 @@ 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,
+61
View File
@@ -0,0 +1,61 @@
import type { Vector3Tuple } from "@/types/three/three";
export interface OctreeCollisionBox {
center: Vector3Tuple;
size: Vector3Tuple;
}
export interface MapOctreeCollisionBox extends OctreeCollisionBox {
bottomY: number;
}
export const MAP_OCTREE_COLLISION_BOXES = {
immeuble1: {
center: [-0.0308, 5.8389, 0],
size: [17.2522, 11.6098, 9.2668],
bottomY: 0.034,
},
maison1: {
center: [0, 1.3638, 0.0536],
size: [2.7813, 3.022, 2.8609],
bottomY: -0.1472,
},
} as const satisfies Record<string, MapOctreeCollisionBox>;
export const LA_FABRIK_INTERIOR_COLLISION_BOXES = [
// NOTE: removed — this thin wall (size [0.2, 1.94, 3.71]) sat at x≈-6.93 and
// sealed the doorway despite the geometry having a hole there. The fabrik
// mesh octree already provides the surrounding wall collision, so this
// proxy was both redundant and bug-causing.
// {
// center: [-6.9351, 2.278, -0.0001],
// size: [0.2, 1.94, 3.711],
// },
{
center: [0.8026, 0.719, -3.639],
size: [4.346, 1.108, 1.181],
},
{
center: [-5.8519, 0.9362, 2.5742],
size: [1.67, 1.551, 2.566],
},
{
center: [-2.0627, 1.4875, -1.2243],
size: [0.691, 0.723, 0.687],
},
{
center: [-3.5502, 1.4378, -1.2485],
size: [1.055, 0.657, 0.563],
},
] as const satisfies readonly OctreeCollisionBox[];
export const CHARACTER_OCTREE_COLLISION_BOX = {
center: [0, 0.875, 0],
size: [0.62, 1.75, 0.62],
} as const satisfies OctreeCollisionBox;
export function hasMapOctreeCollisionBox(
name: string,
): name is keyof typeof MAP_OCTREE_COLLISION_BOXES {
return name in MAP_OCTREE_COLLISION_BOXES;
}