Feat/map-environment #6
@@ -110,6 +110,12 @@ npm run format:check
|
||||
npm run build
|
||||
```
|
||||
|
||||
Regenerate runtime map data after editing `public/map_raw.json`:
|
||||
|
||||
```bash
|
||||
npm run map:transform
|
||||
```
|
||||
|
||||
## Optional Hand-Tracking Backend
|
||||
|
||||
The app can use the local Python backend, but the default debug source is browser-side MediaPipe.
|
||||
|
||||
@@ -14,7 +14,6 @@ The store owns the `missionFlow` slice:
|
||||
|
||||
```ts
|
||||
missionFlow: {
|
||||
step: GameStep;
|
||||
activityCity: boolean;
|
||||
playerName: string;
|
||||
canMove: boolean;
|
||||
@@ -31,14 +30,14 @@ Managers stay responsible for local runtime services:
|
||||
- `AudioManager` owns audio elements, audio pools, music playback, category volume, and stereo pan.
|
||||
- `InteractionManager` owns transient focused/nearby/held interaction handles.
|
||||
|
||||
Mission progression is not owned by a manager. Components update the store through explicit actions such as `setFlowStep`, `setCanMove`, `showDialog`, and `hideDialog`.
|
||||
Mission progression is not owned by a manager. Components update the store through explicit actions such as `setIntroStep`, `setCanMove`, `showDialog`, and `hideDialog`.
|
||||
|
||||
## Runtime Components
|
||||
|
||||
- `src/components/game/GameFlow.tsx` reacts to `missionFlow.step` and triggers one-off side effects such as intro audio and movement unlocks.
|
||||
- `src/components/game/GameFlow.tsx` reacts to intro state and triggers one-off side effects such as intro audio and movement unlocks.
|
||||
- `src/components/zone/ZoneDetection.tsx` reads the camera position and moves the flow to a target step when the player enters a configured zone.
|
||||
- `src/world/GameStageContent.tsx` mounts repair games and their mission-start triggers.
|
||||
- `src/pages/page.tsx` mounts mission HTML overlays: `IntroUI`, `BienvenueDisplay`, and `DialogMessage`.
|
||||
- `src/pages/page.tsx` mounts mission HTML overlays: `IntroUI`, `DialogMessage`, and subtitles.
|
||||
- `src/world/player/PlayerController.tsx` reads `missionFlow.canMove` as an additional movement lock.
|
||||
|
||||
## Step Sequence
|
||||
|
||||
Generated
+1
@@ -22,6 +22,7 @@
|
||||
"react-markdown": "^10.1.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"three": "0.182.0",
|
||||
"three-stdlib": "^2.36.1",
|
||||
"zustand": "^5.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"lint:fix": "eslint . --fix",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"map:transform": "node scripts/transformMap.cjs",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "tsc -b"
|
||||
},
|
||||
@@ -32,6 +33,7 @@
|
||||
"react-markdown": "^10.1.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"three": "0.182.0",
|
||||
"three-stdlib": "^2.36.1",
|
||||
"zustand": "^5.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -55,5 +57,10 @@
|
||||
"r3f-perf": {
|
||||
"@react-three/drei": "$@react-three/drei"
|
||||
}
|
||||
},
|
||||
"knip": {
|
||||
"ignore": [
|
||||
"src/types/three/three-addons.d.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -39565,7 +39565,8 @@
|
||||
"rotation": [0, 0.0027, 0.0819],
|
||||
"scale": [1, 1, 1]
|
||||
}
|
||||
]
|
||||
],
|
||||
"id": "repair:pylon"
|
||||
},
|
||||
{
|
||||
"name": "pylone",
|
||||
|
||||
@@ -25,6 +25,8 @@ const IDENTITY_NODE = {
|
||||
rotation: [0, 0, 0],
|
||||
scale: [1, 1, 1],
|
||||
};
|
||||
const REPAIR_PYLON_ANCHOR_ID = "repair:pylon";
|
||||
const REPAIR_PYLON_FALLBACK_POSITION = [64, 0, -66];
|
||||
const MAX_MESH_Y_PLACEMENT_OFFSET = 2;
|
||||
const RAW_INDEX = {
|
||||
directionGroup: 5,
|
||||
@@ -55,6 +57,7 @@ const RAW_RANGES = {
|
||||
|
||||
function cloneNode(node) {
|
||||
return {
|
||||
...(node.id ? { id: node.id } : {}),
|
||||
name: node.name,
|
||||
type: node.type,
|
||||
position: node.position,
|
||||
@@ -63,6 +66,60 @@ function cloneNode(node) {
|
||||
};
|
||||
}
|
||||
|
||||
function isOriginPosition(position) {
|
||||
return position.every((value) => Math.abs(value) < 0.0001);
|
||||
}
|
||||
|
||||
function hasDistinctPylonTransform(node) {
|
||||
return (
|
||||
node.rotation.some((value) => Math.abs(value) > 0.0001) ||
|
||||
node.scale.some((value) => Math.abs(value - 1) > 0.0001)
|
||||
);
|
||||
}
|
||||
|
||||
function distanceToPosition(node, position) {
|
||||
return Math.hypot(
|
||||
node.position[0] - position[0],
|
||||
node.position[2] - position[2],
|
||||
);
|
||||
}
|
||||
|
||||
function collectMapNodes(root, predicate) {
|
||||
const results = [];
|
||||
const stack = [root];
|
||||
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop();
|
||||
if (predicate(node)) {
|
||||
results.push(node);
|
||||
}
|
||||
stack.push(...(node.children ?? []));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function assignRepairPylonAnchorId(root) {
|
||||
const pylones = collectMapNodes(
|
||||
root,
|
||||
(node) =>
|
||||
node.name === "pylone" &&
|
||||
node.type === "Object3D" &&
|
||||
!isOriginPosition(node.position),
|
||||
);
|
||||
const distinctPylones = pylones.filter(hasDistinctPylonTransform);
|
||||
const candidates = distinctPylones.length > 0 ? distinctPylones : pylones;
|
||||
if (candidates.length === 0) return;
|
||||
|
||||
const anchor = [...candidates].sort(
|
||||
(a, b) =>
|
||||
distanceToPosition(a, REPAIR_PYLON_FALLBACK_POSITION) -
|
||||
distanceToPosition(b, REPAIR_PYLON_FALLBACK_POSITION),
|
||||
)[0];
|
||||
|
||||
anchor.id = REPAIR_PYLON_ANCHOR_ID;
|
||||
}
|
||||
|
||||
function createGroup(name, sourceNode) {
|
||||
return {
|
||||
name,
|
||||
@@ -434,6 +491,8 @@ function transformMap() {
|
||||
blocking.children.push(unclassified);
|
||||
}
|
||||
|
||||
assignRepairPylonAnchorId(scene);
|
||||
|
||||
fs.writeFileSync(OUTPUT_PATH, JSON.stringify(scene, null, 2));
|
||||
console.log(`Written hierarchical map to ${OUTPUT_PATH}`);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
import { useCallback, useEffect, useLayoutEffect, useRef } from "react";
|
||||
import { Grid, TransformControls } from "@react-three/drei";
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
Suspense,
|
||||
} from "react";
|
||||
import { TransformControls } from "@react-three/drei";
|
||||
import type { ThreeEvent } from "@react-three/fiber";
|
||||
import * as THREE from "three";
|
||||
|
||||
import { TerrainModel } from "@/components/three/world/TerrainModel";
|
||||
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
||||
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
||||
import { useTerrainHeightSampler } from "@/hooks/three/useTerrainHeight";
|
||||
import {
|
||||
getObjectBottomOffset,
|
||||
useTerrainHeightSampler,
|
||||
} from "@/hooks/three/useTerrainHeight";
|
||||
import type { SceneData, MapNode, TransformMode } from "@/types/editor/editor";
|
||||
import {
|
||||
isEditorVisibleMapNode,
|
||||
@@ -94,6 +104,30 @@ function getEditorModelVisualScaleMultiplier(name: string): number {
|
||||
);
|
||||
}
|
||||
|
||||
function getEditorModelVisualYOffset(
|
||||
object: THREE.Object3D,
|
||||
node: MapNode,
|
||||
terrainHeight: ReturnType<typeof useTerrainHeightSampler>,
|
||||
visualScaleMultiplier: number,
|
||||
): number {
|
||||
const [x, y, z] = node.position;
|
||||
const height = terrainHeight.getHeight(x, z);
|
||||
if (height === null) return 0;
|
||||
|
||||
const finalScale: [number, number, number] = [
|
||||
node.scale[0] * visualScaleMultiplier,
|
||||
node.scale[1] * visualScaleMultiplier,
|
||||
node.scale[2] * visualScaleMultiplier,
|
||||
];
|
||||
const originalPosition = object.position.clone();
|
||||
object.position.set(0, 0, 0);
|
||||
const bottomOffset = getObjectBottomOffset(object, finalScale);
|
||||
object.position.copy(originalPosition);
|
||||
const parentScaleY = Math.abs(node.scale[1]) > 0.0001 ? node.scale[1] : 1;
|
||||
|
||||
return (height + bottomOffset - y) / parentScaleY;
|
||||
}
|
||||
|
||||
function applyNodeTransform(object: THREE.Object3D, node: MapNode): void {
|
||||
object.position.set(...node.position);
|
||||
object.rotation.set(...node.rotation);
|
||||
@@ -222,7 +256,6 @@ export function EditorMap({
|
||||
selectedNodeIndex !== null
|
||||
? (sceneData.mapNodes[selectedNodeIndex]?.name ?? null)
|
||||
: null;
|
||||
|
||||
const getTransformObject = useCallback(() => {
|
||||
if (isMultiSelection) {
|
||||
return transformGroupRef.current;
|
||||
@@ -407,35 +440,22 @@ export function EditorMap({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid
|
||||
args={[100, 100]}
|
||||
cellSize={1}
|
||||
cellThickness={0.5}
|
||||
cellColor="#242424"
|
||||
sectionSize={5}
|
||||
sectionThickness={1}
|
||||
sectionColor="#3a3a3a"
|
||||
fadeDistance={50}
|
||||
fadeStrength={1}
|
||||
followCamera={false}
|
||||
infiniteGrid={false}
|
||||
/>
|
||||
<axesHelper args={[10]} />
|
||||
|
||||
<group>
|
||||
{terrainNode ? (
|
||||
<EditorTerrainNode
|
||||
index={terrainNodeIndex}
|
||||
node={terrainNode}
|
||||
isSelected={selectedIndexSet.has(terrainNodeIndex)}
|
||||
isHovered={hoveredNodeIndex === terrainNodeIndex}
|
||||
lockTerrainSelection={lockTerrainSelection}
|
||||
objectsMapRef={objectsMapRef}
|
||||
onSelectNode={onSelectNode}
|
||||
onToggleNodeSelection={onToggleNodeSelection}
|
||||
isSelectionLocked={isSelectionLocked}
|
||||
onHoverNode={onHoverNode}
|
||||
/>
|
||||
<Suspense fallback={null}>
|
||||
<EditorTerrainNode
|
||||
index={terrainNodeIndex}
|
||||
node={terrainNode}
|
||||
isSelected={selectedIndexSet.has(terrainNodeIndex)}
|
||||
isHovered={hoveredNodeIndex === terrainNodeIndex}
|
||||
lockTerrainSelection={lockTerrainSelection}
|
||||
objectsMapRef={objectsMapRef}
|
||||
onSelectNode={onSelectNode}
|
||||
onToggleNodeSelection={onToggleNodeSelection}
|
||||
isSelectionLocked={isSelectionLocked}
|
||||
onHoverNode={onHoverNode}
|
||||
/>
|
||||
</Suspense>
|
||||
) : null}
|
||||
{sceneData.mapNodes.map((node, index) => {
|
||||
if (!shouldRenderEditorNode(node, selectedNodeName)) {
|
||||
@@ -446,19 +466,35 @@ export function EditorMap({
|
||||
|
||||
if (modelUrl) {
|
||||
return (
|
||||
<EditorModelNode
|
||||
<Suspense
|
||||
key={index}
|
||||
index={index}
|
||||
node={node}
|
||||
modelUrl={modelUrl}
|
||||
isSelected={selectedIndexSet.has(index)}
|
||||
isHovered={hoveredNodeIndex === index}
|
||||
objectsMapRef={objectsMapRef}
|
||||
onSelectNode={onSelectNode}
|
||||
onToggleNodeSelection={onToggleNodeSelection}
|
||||
isSelectionLocked={isSelectionLocked}
|
||||
onHoverNode={onHoverNode}
|
||||
/>
|
||||
fallback={
|
||||
<EditorFallbackNode
|
||||
index={index}
|
||||
node={node}
|
||||
isSelected={selectedIndexSet.has(index)}
|
||||
isHovered={hoveredNodeIndex === index}
|
||||
objectsMapRef={objectsMapRef}
|
||||
onSelectNode={onSelectNode}
|
||||
onToggleNodeSelection={onToggleNodeSelection}
|
||||
isSelectionLocked={isSelectionLocked}
|
||||
onHoverNode={onHoverNode}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EditorModelNode
|
||||
index={index}
|
||||
node={node}
|
||||
modelUrl={modelUrl}
|
||||
isSelected={selectedIndexSet.has(index)}
|
||||
isHovered={hoveredNodeIndex === index}
|
||||
objectsMapRef={objectsMapRef}
|
||||
onSelectNode={onSelectNode}
|
||||
onToggleNodeSelection={onToggleNodeSelection}
|
||||
isSelectionLocked={isSelectionLocked}
|
||||
onHoverNode={onHoverNode}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
@@ -519,7 +555,18 @@ function EditorModelNode({
|
||||
scale: node.scale,
|
||||
});
|
||||
const sceneInstance = useClonedObject(scene);
|
||||
const terrainHeight = useTerrainHeightSampler();
|
||||
const visualScaleMultiplier = getEditorModelVisualScaleMultiplier(node.name);
|
||||
const visualYOffset = useMemo(
|
||||
() =>
|
||||
getEditorModelVisualYOffset(
|
||||
sceneInstance,
|
||||
node,
|
||||
terrainHeight,
|
||||
visualScaleMultiplier,
|
||||
),
|
||||
[node, sceneInstance, terrainHeight, visualScaleMultiplier],
|
||||
);
|
||||
const pointerHandlers = createEditorNodePointerHandlers(
|
||||
index,
|
||||
onSelectNode,
|
||||
@@ -588,7 +635,11 @@ function EditorModelNode({
|
||||
scale={node.scale}
|
||||
{...pointerHandlers}
|
||||
>
|
||||
<primitive object={sceneInstance} scale={visualScaleMultiplier} />
|
||||
<primitive
|
||||
object={sceneInstance}
|
||||
position={[0, visualYOffset, 0]}
|
||||
scale={visualScaleMultiplier}
|
||||
/>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { OrbitControls } from "@react-three/drei";
|
||||
import { Suspense, useCallback, useEffect, useRef } from "react";
|
||||
import { Grid, OrbitControls } from "@react-three/drei";
|
||||
import { useThree } from "@react-three/fiber";
|
||||
import gsap from "gsap";
|
||||
import * as THREE from "three";
|
||||
import type { OrbitControls as OrbitControlsImpl } from "three-stdlib";
|
||||
import { EditorMap } from "@/components/editor/scene/EditorMap";
|
||||
import { FlyController } from "@/controls/editor/FlyController";
|
||||
import { PersonnageSystem } from "@/world/personnages/PersonnageSystem";
|
||||
import type { CinematicDefinition } from "@/types/cinematics/cinematics";
|
||||
import type { MapNode, TransformMode, SceneData } from "@/types/editor/editor";
|
||||
|
||||
@@ -214,26 +213,41 @@ export function EditorScene({
|
||||
/>
|
||||
)}
|
||||
|
||||
<EditorMap
|
||||
sceneData={sceneData}
|
||||
selectedNodeIndex={selectedNodeIndex}
|
||||
selectedNodeIndexes={selectedNodeIndexes}
|
||||
onSelectNode={onSelectNode}
|
||||
onToggleNodeSelection={onToggleNodeSelection}
|
||||
isSelectionLocked={isSelectionLocked}
|
||||
hoveredNodeIndex={hoveredNodeIndex}
|
||||
onHoverNode={onHoverNode}
|
||||
transformMode={transformMode}
|
||||
snapToTerrain={snapToTerrain}
|
||||
lockTerrainSelection={lockTerrainSelection}
|
||||
onTransformStart={onTransformStart}
|
||||
onTransformEnd={onTransformEnd}
|
||||
onNodeTransform={onNodeTransform}
|
||||
snapAllToTerrainRequest={snapAllToTerrainRequest}
|
||||
onSnapAllToTerrain={onSnapAllToTerrain}
|
||||
<Grid
|
||||
args={[100, 100]}
|
||||
cellSize={1}
|
||||
cellThickness={0.5}
|
||||
cellColor="#242424"
|
||||
sectionSize={5}
|
||||
sectionThickness={1}
|
||||
sectionColor="#3a3a3a"
|
||||
fadeDistance={50}
|
||||
fadeStrength={1}
|
||||
followCamera={false}
|
||||
infiniteGrid={false}
|
||||
/>
|
||||
<axesHelper args={[10]} />
|
||||
|
||||
<PersonnageSystem />
|
||||
<Suspense fallback={null}>
|
||||
<EditorMap
|
||||
sceneData={sceneData}
|
||||
selectedNodeIndex={selectedNodeIndex}
|
||||
selectedNodeIndexes={selectedNodeIndexes}
|
||||
onSelectNode={onSelectNode}
|
||||
onToggleNodeSelection={onToggleNodeSelection}
|
||||
isSelectionLocked={isSelectionLocked}
|
||||
hoveredNodeIndex={hoveredNodeIndex}
|
||||
onHoverNode={onHoverNode}
|
||||
transformMode={transformMode}
|
||||
snapToTerrain={snapToTerrain}
|
||||
lockTerrainSelection={lockTerrainSelection}
|
||||
onTransformStart={onTransformStart}
|
||||
onTransformEnd={onTransformEnd}
|
||||
onNodeTransform={onNodeTransform}
|
||||
snapAllToTerrainRequest={snapAllToTerrainRequest}
|
||||
onSnapAllToTerrain={onSnapAllToTerrain}
|
||||
/>
|
||||
</Suspense>
|
||||
|
||||
<ambientLight intensity={0.6} />
|
||||
<directionalLight position={[10, 20, 10]} intensity={1.5} castShadow />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useEffect } from "react";
|
||||
import * as THREE from "three";
|
||||
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
||||
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
||||
import type { ModelTransformProps, Vector3Tuple } from "@/types/three/three";
|
||||
|
||||
@@ -41,7 +42,7 @@ export function SimpleModel({
|
||||
rotation,
|
||||
scale,
|
||||
});
|
||||
const model = useMemo(() => scene.clone(true), [scene]);
|
||||
const model = useClonedObject(scene);
|
||||
|
||||
useEffect(() => {
|
||||
applyShadowSettings(model, castShadow, receiveShadow);
|
||||
|
||||
@@ -5,6 +5,7 @@ import * as THREE from "three";
|
||||
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
||||
|
||||
interface SkyModelProps {
|
||||
fallbackModelPath?: string | undefined;
|
||||
modelPath: string;
|
||||
fallbackColor?: string | undefined;
|
||||
scale?: number | undefined;
|
||||
@@ -52,12 +53,20 @@ class SkyModelErrorBoundary extends Component<
|
||||
|
||||
export function SkyModel({
|
||||
fallbackColor,
|
||||
fallbackModelPath,
|
||||
modelPath,
|
||||
scale = SKY_MODEL_SCALE,
|
||||
}: SkyModelProps): React.JSX.Element {
|
||||
const fallback = fallbackColor ? (
|
||||
const colorFallback = fallbackColor ? (
|
||||
<color attach="background" args={[fallbackColor]} />
|
||||
) : null;
|
||||
const fallback = fallbackModelPath ? (
|
||||
<SkyModelErrorBoundary key={fallbackModelPath} fallback={colorFallback}>
|
||||
<SkyModelContent modelPath={fallbackModelPath} scale={scale} />
|
||||
</SkyModelErrorBoundary>
|
||||
) : (
|
||||
colorFallback
|
||||
);
|
||||
|
||||
return (
|
||||
<SkyModelErrorBoundary key={modelPath} fallback={fallback}>
|
||||
|
||||
@@ -4,7 +4,13 @@ import type {
|
||||
RepairMissionTriggerConfig,
|
||||
} from "@/types/gameplay/repairMission";
|
||||
|
||||
export const EBIKE_REPAIR_POSITION = [
|
||||
export const REPAIR_MISSION_ANCHOR_IDS: Partial<
|
||||
Record<RepairMissionId, string>
|
||||
> = {
|
||||
pylon: "repair:pylon",
|
||||
};
|
||||
|
||||
const EBIKE_REPAIR_POSITION = [
|
||||
42.2399, 4.5484, 34.6468,
|
||||
] as const satisfies Vector3Tuple;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export const GAME_SCENE_SKY_MODEL_PATH = "/models/skybox/model.gltf";
|
||||
export const GAME_SCENE_SKY_FALLBACK_MODEL_PATH = "/models/sky/model.glb";
|
||||
export const GAME_SCENE_SKY_MODEL_SCALE = 100;
|
||||
export const GAME_SCENE_FALLBACK_BACKGROUND_COLOR = "#0b1018";
|
||||
export const PHYSICS_SCENE_BACKGROUND_COLOR = "#0b1018";
|
||||
|
||||
@@ -81,7 +81,7 @@ export const MAP_INSTANCING_ASSETS = {
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const MAP_SINGLE_MODEL_SCALE_MULTIPLIERS = {
|
||||
const MAP_SINGLE_MODEL_SCALE_MULTIPLIERS = {
|
||||
ebike: 0.3,
|
||||
} as const satisfies Record<string, number>;
|
||||
|
||||
@@ -93,7 +93,7 @@ export function getMapSingleModelScaleMultiplier(name: string): number {
|
||||
);
|
||||
}
|
||||
|
||||
export function getMapInstancedModelScaleMultiplier(name: string): number {
|
||||
function getMapInstancedModelScaleMultiplier(name: string): number {
|
||||
return (
|
||||
Object.values(MAP_INSTANCING_ASSETS).find(
|
||||
(config) => config.mapName === name,
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { TerrainSurfaceColorConfig } from "@/types/world/terrainSurface";
|
||||
export const TERRAIN_MODEL_PATH = "/models/terrain/model.gltf";
|
||||
export const TERRAIN_WATER_HEIGHT = 0.8;
|
||||
|
||||
export const TERRAIN_TILE_SIZE = 1;
|
||||
const TERRAIN_TILE_SIZE = 1;
|
||||
|
||||
export const TERRAIN_COLORS = {
|
||||
grass1: {
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
import { useRef, useEffect, useState, useCallback, useMemo } from "react";
|
||||
import { useAnimations } from "@react-three/drei";
|
||||
import type { AnimationAction, AnimationMixer } from "three";
|
||||
import * as THREE from "three";
|
||||
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
||||
|
||||
export interface CharacterAnimationConfig {
|
||||
modelPath: string;
|
||||
initialAnimation?: string;
|
||||
fadeDuration?: number;
|
||||
}
|
||||
|
||||
interface UseCharacterAnimationReturn {
|
||||
scene: THREE.Group;
|
||||
actions: { [key: string]: AnimationAction | null };
|
||||
names: string[];
|
||||
mixer: AnimationMixer;
|
||||
groupRef: React.MutableRefObject<THREE.Group | null>;
|
||||
currentAnimation: string;
|
||||
play: (name: string) => void;
|
||||
stop: () => void;
|
||||
fadeTo: (name: string, duration?: number) => void;
|
||||
setAnimationSpeed: (speed: number) => void;
|
||||
}
|
||||
|
||||
const DEFAULT_FADE_DURATION = 0.3;
|
||||
|
||||
export function useCharacterAnimation(
|
||||
config: CharacterAnimationConfig,
|
||||
): UseCharacterAnimationReturn {
|
||||
const {
|
||||
modelPath,
|
||||
initialAnimation = "Idle",
|
||||
fadeDuration = DEFAULT_FADE_DURATION,
|
||||
} = config;
|
||||
|
||||
const groupRef = useRef<THREE.Group | null>(null);
|
||||
const { scene, animations } = useLoggedGLTF(modelPath, {
|
||||
scope: "useCharacterAnimation",
|
||||
});
|
||||
const model = useMemo(() => scene.clone(true), [scene]);
|
||||
const { actions, names, mixer } = useAnimations(animations, groupRef);
|
||||
const [currentAnimation, setCurrentAnimation] = useState(initialAnimation);
|
||||
|
||||
const play = useCallback(
|
||||
(name: string) => {
|
||||
const action = actions[name];
|
||||
if (action) {
|
||||
Object.values(actions).forEach((a) => {
|
||||
if (a && a !== action) a.fadeOut(fadeDuration);
|
||||
});
|
||||
action.reset().fadeIn(fadeDuration).play();
|
||||
setCurrentAnimation(name);
|
||||
}
|
||||
},
|
||||
[actions, fadeDuration],
|
||||
);
|
||||
|
||||
const stop = useCallback(() => {
|
||||
Object.values(actions).forEach((a) => a?.fadeOut(fadeDuration));
|
||||
const defaultAction = actions[initialAnimation as string];
|
||||
if (defaultAction) {
|
||||
defaultAction.reset().fadeIn(fadeDuration).play();
|
||||
setCurrentAnimation(initialAnimation);
|
||||
}
|
||||
}, [actions, initialAnimation, fadeDuration]);
|
||||
|
||||
const fadeTo = useCallback(
|
||||
(name: string, duration = fadeDuration) => {
|
||||
const targetAction = actions[name];
|
||||
if (targetAction) {
|
||||
Object.values(actions).forEach((a) => {
|
||||
if (a && a !== targetAction) a.fadeOut(duration);
|
||||
});
|
||||
targetAction.reset().fadeIn(duration).play();
|
||||
setCurrentAnimation(name);
|
||||
}
|
||||
},
|
||||
[actions, fadeDuration],
|
||||
);
|
||||
|
||||
const setAnimationSpeed = useCallback(
|
||||
(speed: number) => {
|
||||
Object.values(actions).forEach((action) => {
|
||||
action?.setEffectiveTimeScale(speed);
|
||||
});
|
||||
},
|
||||
[actions],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const defaultAction = actions[initialAnimation as string];
|
||||
if (defaultAction) {
|
||||
defaultAction.play();
|
||||
}
|
||||
}, [actions, initialAnimation]);
|
||||
|
||||
return {
|
||||
scene: model,
|
||||
actions,
|
||||
names,
|
||||
mixer,
|
||||
groupRef,
|
||||
currentAnimation,
|
||||
play,
|
||||
stop,
|
||||
fadeTo,
|
||||
setAnimationSpeed,
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,30 @@
|
||||
import { useMemo } from "react";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import * as THREE from "three";
|
||||
import { disposeObject3D } from "@/utils/three/dispose";
|
||||
|
||||
function cloneObjectWithOwnedResources<T extends THREE.Object3D>(object: T): T {
|
||||
const clone = object.clone(true) as T;
|
||||
|
||||
clone.traverse((child) => {
|
||||
if (!(child instanceof THREE.Mesh)) return;
|
||||
|
||||
child.geometry = child.geometry.clone();
|
||||
child.material = Array.isArray(child.material)
|
||||
? child.material.map((material) => material.clone())
|
||||
: child.material.clone();
|
||||
});
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
export function useClonedObject<T extends THREE.Object3D>(object: T): T {
|
||||
return useMemo(() => object.clone(true) as T, [object]);
|
||||
const clone = useMemo(() => cloneObjectWithOwnedResources(object), [object]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
disposeObject3D(clone);
|
||||
};
|
||||
}, [clone]);
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
@@ -7,12 +7,7 @@ import {
|
||||
type MapPerformanceModelName,
|
||||
} from "@/data/world/mapPerformanceConfig";
|
||||
|
||||
export {
|
||||
MAP_PERFORMANCE_GROUP_NAMES,
|
||||
MAP_PERFORMANCE_MODEL_NAMES,
|
||||
type MapPerformanceGroupName,
|
||||
type MapPerformanceModelName,
|
||||
};
|
||||
export { MAP_PERFORMANCE_GROUP_NAMES, MAP_PERFORMANCE_MODEL_NAMES };
|
||||
|
||||
export interface MapPerformanceVisibility {
|
||||
groups: Record<MapPerformanceGroupName, boolean>;
|
||||
|
||||
+38
-90
@@ -1,12 +1,10 @@
|
||||
import { Suspense, useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Canvas } from "@react-three/fiber";
|
||||
import { useProgress } from "@react-three/drei";
|
||||
import { EditorControls } from "@/components/editor/EditorControls";
|
||||
import { EditorScene } from "@/components/editor/scene/EditorScene";
|
||||
import type { EditorCinematicPreviewRequest } from "@/components/editor/scene/EditorScene";
|
||||
import { SceneLoadingOverlay } from "@/components/ui/SceneLoadingOverlay";
|
||||
import { Subtitles } from "@/components/ui/Subtitles";
|
||||
import { INITIAL_SCENE_LOADING_STATE } from "@/data/world/sceneLoadingConfig";
|
||||
import { useEditorHistory } from "@/hooks/editor/useEditorHistory";
|
||||
import type { CinematicDefinition } from "@/types/cinematics/cinematics";
|
||||
import { useEditorSceneData } from "@/hooks/editor/useEditorSceneData";
|
||||
@@ -16,19 +14,12 @@ import type {
|
||||
SceneData,
|
||||
TransformMode,
|
||||
} from "@/types/editor/editor";
|
||||
import {
|
||||
type SceneLoadingChangeHandler,
|
||||
type SceneLoadingState,
|
||||
} from "@/types/world/sceneLoading";
|
||||
import type { SceneLoadingState } from "@/types/world/sceneLoading";
|
||||
import { logger } from "@/utils/core/Logger";
|
||||
|
||||
const SAVE_ERROR_MESSAGE = "Erreur lors de l'enregistrement";
|
||||
const DEFAULT_NEW_NODE_NAME = "new-model";
|
||||
|
||||
interface EditorSceneLoadingTrackerProps {
|
||||
onLoadingStateChange: SceneLoadingChangeHandler;
|
||||
}
|
||||
|
||||
function serializeMapNodes(sceneData: SceneData): string {
|
||||
const mapPayload = sceneData.mapTree
|
||||
? mergeFlatNodeTransformsIntoTree(sceneData)
|
||||
@@ -43,6 +34,7 @@ function createSourcePathKey(sourcePath: readonly number[]): string {
|
||||
|
||||
function removeEditorMetadata(node: MapNode): MapNode {
|
||||
return {
|
||||
...(node.id ? { id: node.id } : {}),
|
||||
name: node.name,
|
||||
type: node.type,
|
||||
position: node.position,
|
||||
@@ -67,6 +59,9 @@ function mergeFlatNodeTransformsIntoTree(
|
||||
): HierarchicalMapNode => {
|
||||
const updatedNode = nodesBySourcePath.get(createSourcePathKey(path));
|
||||
const nextNode: HierarchicalMapNode = {
|
||||
...((updatedNode?.id ?? node.id)
|
||||
? { id: updatedNode?.id ?? node.id }
|
||||
: {}),
|
||||
name: node.name,
|
||||
type: node.type,
|
||||
position: updatedNode?.position ?? node.position,
|
||||
@@ -116,6 +111,7 @@ function collectEditableMapNodes(
|
||||
function visit(node: HierarchicalMapNode, path: number[]): void {
|
||||
if (node.role !== "group" && node.type !== "Mesh") {
|
||||
nodes.push({
|
||||
...(node.id ? { id: node.id } : {}),
|
||||
name: node.name,
|
||||
position: node.position,
|
||||
rotation: node.rotation,
|
||||
@@ -276,31 +272,6 @@ function createNewMapNode(name: string): HierarchicalMapNode {
|
||||
};
|
||||
}
|
||||
|
||||
function EditorSceneLoadingTracker({
|
||||
onLoadingStateChange,
|
||||
}: EditorSceneLoadingTrackerProps): null {
|
||||
const { active, progress } = useProgress();
|
||||
|
||||
useEffect(() => {
|
||||
if (active) {
|
||||
onLoadingStateChange({
|
||||
currentStep: "Importation des models",
|
||||
progress: 0.2 + (progress / 100) * 0.7,
|
||||
status: "loading",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
onLoadingStateChange({
|
||||
currentStep: "Gameplay prêt",
|
||||
progress: 1,
|
||||
status: "ready",
|
||||
});
|
||||
}, [active, onLoadingStateChange, progress]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function EditorPage(): React.JSX.Element {
|
||||
const {
|
||||
hasMapJson,
|
||||
@@ -329,35 +300,17 @@ export function EditorPage(): React.JSX.Element {
|
||||
const [cameraViewMode, setCameraViewMode] = useState<"home" | "object">(
|
||||
"home",
|
||||
);
|
||||
const [sceneLoadingState, setSceneLoadingState] = useState<SceneLoadingState>(
|
||||
{
|
||||
...INITIAL_SCENE_LOADING_STATE,
|
||||
currentStep: "Montage progressif des models",
|
||||
progress: 0.2,
|
||||
},
|
||||
);
|
||||
const handleSceneLoadingStateChange = useCallback(
|
||||
(nextState: SceneLoadingState) => {
|
||||
setSceneLoadingState((currentState) => {
|
||||
const shouldRestartProgress = currentState.status === "ready";
|
||||
|
||||
return {
|
||||
...nextState,
|
||||
progress: shouldRestartProgress
|
||||
? nextState.progress
|
||||
: Math.max(currentState.progress, nextState.progress),
|
||||
};
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
const editorLoadingState = isMapLoading
|
||||
const editorLoadingState: SceneLoadingState = isMapLoading
|
||||
? {
|
||||
currentStep: "Récupération blocking",
|
||||
progress: 0.08,
|
||||
status: "loading" as const,
|
||||
}
|
||||
: sceneLoadingState;
|
||||
: {
|
||||
currentStep: "Gameplay prêt",
|
||||
progress: 1,
|
||||
status: "ready" as const,
|
||||
};
|
||||
const [cinematicPreviewRequest, setCinematicPreviewRequest] =
|
||||
useState<EditorCinematicPreviewRequest | null>(null);
|
||||
|
||||
@@ -720,37 +673,32 @@ export function EditorPage(): React.JSX.Element {
|
||||
);
|
||||
}}
|
||||
>
|
||||
<EditorSceneLoadingTracker
|
||||
onLoadingStateChange={handleSceneLoadingStateChange}
|
||||
<EditorScene
|
||||
sceneData={sceneData!}
|
||||
selectedNodeIndex={selectedNodeIndex}
|
||||
selectedNodeIndexes={selectedNodeIndexes}
|
||||
onSelectNode={handleSelectNode}
|
||||
onToggleNodeSelection={handleToggleNodeSelection}
|
||||
isSelectionLocked={isSelectionLocked}
|
||||
hoveredNodeIndex={hoveredNodeIndex}
|
||||
onHoverNode={handleHoverNode}
|
||||
transformMode={transformMode}
|
||||
snapToTerrain={snapToTerrain}
|
||||
lockTerrainSelection={lockTerrainSelection}
|
||||
onTransformModeChange={handleTransformModeChange}
|
||||
onTransformStart={handleTransformStart}
|
||||
onTransformEnd={handleTransformEnd}
|
||||
onNodeTransform={handleNodeTransform}
|
||||
snapAllToTerrainRequest={snapAllToTerrainRequest}
|
||||
onSnapAllToTerrain={handleSnapAllToTerrain}
|
||||
onUndo={handleUndo}
|
||||
onRedo={handleRedo}
|
||||
resetCameraRequest={resetCameraRequest}
|
||||
focusSelectedCameraRequest={focusSelectedCameraRequest}
|
||||
isPlayerMode={isPlayerMode}
|
||||
cinematicPreviewRequest={cinematicPreviewRequest}
|
||||
onCinematicPreviewComplete={handleCinematicPreviewComplete}
|
||||
/>
|
||||
<Suspense fallback={null}>
|
||||
<EditorScene
|
||||
sceneData={sceneData!}
|
||||
selectedNodeIndex={selectedNodeIndex}
|
||||
selectedNodeIndexes={selectedNodeIndexes}
|
||||
onSelectNode={handleSelectNode}
|
||||
onToggleNodeSelection={handleToggleNodeSelection}
|
||||
isSelectionLocked={isSelectionLocked}
|
||||
hoveredNodeIndex={hoveredNodeIndex}
|
||||
onHoverNode={handleHoverNode}
|
||||
transformMode={transformMode}
|
||||
snapToTerrain={snapToTerrain}
|
||||
lockTerrainSelection={lockTerrainSelection}
|
||||
onTransformModeChange={handleTransformModeChange}
|
||||
onTransformStart={handleTransformStart}
|
||||
onTransformEnd={handleTransformEnd}
|
||||
onNodeTransform={handleNodeTransform}
|
||||
snapAllToTerrainRequest={snapAllToTerrainRequest}
|
||||
onSnapAllToTerrain={handleSnapAllToTerrain}
|
||||
onUndo={handleUndo}
|
||||
onRedo={handleRedo}
|
||||
resetCameraRequest={resetCameraRequest}
|
||||
focusSelectedCameraRequest={focusSelectedCameraRequest}
|
||||
isPlayerMode={isPlayerMode}
|
||||
cinematicPreviewRequest={cinematicPreviewRequest}
|
||||
onCinematicPreviewComplete={handleCinematicPreviewComplete}
|
||||
/>
|
||||
</Suspense>
|
||||
</Canvas>
|
||||
|
||||
<SceneLoadingOverlay state={editorLoadingState} />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Vector3Tuple } from "@/types/three/three";
|
||||
|
||||
export interface MapNode {
|
||||
id?: string;
|
||||
name: string;
|
||||
type: string;
|
||||
position: Vector3Tuple;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type TerrainSurfaceKind =
|
||||
type TerrainSurfaceKind =
|
||||
| "grass"
|
||||
| "path"
|
||||
| "water"
|
||||
@@ -6,7 +6,7 @@ export type TerrainSurfaceKind =
|
||||
| "dirt"
|
||||
| "rock";
|
||||
|
||||
export type TerrainSurfaceRgb = readonly [number, number, number];
|
||||
type TerrainSurfaceRgb = readonly [number, number, number];
|
||||
|
||||
export interface TerrainSurfaceBounds {
|
||||
minX: number;
|
||||
|
||||
@@ -99,6 +99,7 @@ function flattenMapNode(node: HierarchicalMapNode, path: number[]): MapNode[] {
|
||||
|
||||
return [
|
||||
{
|
||||
...(node.id ? { id: node.id } : {}),
|
||||
name: node.name,
|
||||
type: node.type,
|
||||
position: node.position,
|
||||
|
||||
@@ -23,6 +23,7 @@ function isMapNode(value: unknown): value is MapNode {
|
||||
}
|
||||
|
||||
return (
|
||||
(value.id === undefined || typeof value.id === "string") &&
|
||||
typeof value.name === "string" &&
|
||||
typeof value.type === "string" &&
|
||||
isVector3Tuple(value.position) &&
|
||||
@@ -53,6 +54,7 @@ function isHierarchicalMapNode(value: unknown): value is HierarchicalMapNode {
|
||||
|
||||
function flattenMapNode(node: HierarchicalMapNode, path: number[]): MapNode[] {
|
||||
const mapNode: MapNode = {
|
||||
...(node.id ? { id: node.id } : {}),
|
||||
name: node.name,
|
||||
type: node.type,
|
||||
position: node.position,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { MapNode } from "@/types/map/mapScene";
|
||||
|
||||
export const POTAGER_MAP_NAME = "potager";
|
||||
export const POTAGER_DEFAULT_ROTATION_OFFSET = [0, 0, 0] as const;
|
||||
const POTAGER_DEFAULT_ROTATION_OFFSET = [0, 0, 0] as const;
|
||||
|
||||
export const POTAGER_SOURCE_MAP_NAMES = new Set([
|
||||
const POTAGER_SOURCE_MAP_NAMES = new Set([
|
||||
"champdeble",
|
||||
"champdesoja",
|
||||
"champsdetournesol",
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
REPAIR_MISSION_ANCHOR_IDS,
|
||||
REPAIR_MISSION_POSITION_ENTRIES,
|
||||
} from "@/data/gameplay/repairMissionAnchors";
|
||||
import type { RepairMissionId } from "@/types/gameplay/repairMission";
|
||||
import type { MapNode } from "@/types/map/mapScene";
|
||||
import type { Vector3Tuple } from "@/types/three/three";
|
||||
@@ -8,10 +12,67 @@ const REPAIR_MISSION_MAP_NODE_NAMES = {
|
||||
farm: "fermeverticale",
|
||||
} as const satisfies Record<RepairMissionId, string>;
|
||||
|
||||
const REPAIR_MISSION_FALLBACK_POSITIONS = new Map(
|
||||
REPAIR_MISSION_POSITION_ENTRIES.map(({ mission, position }) => [
|
||||
mission,
|
||||
position,
|
||||
]),
|
||||
);
|
||||
|
||||
function isOriginPosition(position: Vector3Tuple): boolean {
|
||||
return position.every((value) => Math.abs(value) < 0.0001);
|
||||
}
|
||||
|
||||
function hasDistinctTransform(node: MapNode): boolean {
|
||||
return (
|
||||
node.rotation.some((value) => Math.abs(value) > 0.0001) ||
|
||||
node.scale.some((value) => Math.abs(value - 1) > 0.0001)
|
||||
);
|
||||
}
|
||||
|
||||
function distanceToPosition(node: MapNode, position: Vector3Tuple): number {
|
||||
return Math.hypot(
|
||||
node.position[0] - position[0],
|
||||
node.position[2] - position[2],
|
||||
);
|
||||
}
|
||||
|
||||
function getAnchorNode(
|
||||
mapNodes: readonly MapNode[],
|
||||
mission: RepairMissionId,
|
||||
mapName: string,
|
||||
): MapNode | null {
|
||||
const anchorId = REPAIR_MISSION_ANCHOR_IDS[mission];
|
||||
if (anchorId) {
|
||||
const nodeById = mapNodes.find((candidate) => candidate.id === anchorId);
|
||||
if (nodeById) return nodeById;
|
||||
}
|
||||
|
||||
const candidates = mapNodes.filter(
|
||||
(candidate) =>
|
||||
candidate.name === mapName &&
|
||||
candidate.type === "Object3D" &&
|
||||
!isOriginPosition(candidate.position),
|
||||
);
|
||||
|
||||
if (mission !== "pylon") return candidates[0] ?? null;
|
||||
|
||||
const distinctCandidates = candidates.filter(hasDistinctTransform);
|
||||
const pylonCandidates =
|
||||
distinctCandidates.length > 0 ? distinctCandidates : candidates;
|
||||
const fallbackPosition = REPAIR_MISSION_FALLBACK_POSITIONS.get(mission);
|
||||
|
||||
if (!fallbackPosition) return pylonCandidates[0] ?? null;
|
||||
|
||||
return (
|
||||
[...pylonCandidates].sort(
|
||||
(a, b) =>
|
||||
distanceToPosition(a, fallbackPosition) -
|
||||
distanceToPosition(b, fallbackPosition),
|
||||
)[0] ?? null
|
||||
);
|
||||
}
|
||||
|
||||
export function getRepairMissionMapAnchors(
|
||||
mapNodes: readonly MapNode[],
|
||||
): Partial<Record<RepairMissionId, Vector3Tuple>> {
|
||||
@@ -20,12 +81,7 @@ export function getRepairMissionMapAnchors(
|
||||
for (const [mission, mapName] of Object.entries(
|
||||
REPAIR_MISSION_MAP_NODE_NAMES,
|
||||
) as [RepairMissionId, string][]) {
|
||||
const node = mapNodes.find(
|
||||
(candidate) =>
|
||||
candidate.name === mapName &&
|
||||
candidate.type === "Object3D" &&
|
||||
!isOriginPosition(candidate.position),
|
||||
);
|
||||
const node = getAnchorNode(mapNodes, mission, mapName);
|
||||
|
||||
if (node) {
|
||||
anchors[mission] = node.position;
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import * as THREE from "three";
|
||||
|
||||
type TextureMaterialKey = Extract<
|
||||
| keyof THREE.MeshBasicMaterial
|
||||
| keyof THREE.MeshStandardMaterial
|
||||
| keyof THREE.MeshPhysicalMaterial
|
||||
| keyof THREE.MeshToonMaterial,
|
||||
string
|
||||
>;
|
||||
|
||||
type MaterialWithTextureSlots = THREE.Material &
|
||||
Partial<Record<TextureMaterialKey, THREE.Texture | null>>;
|
||||
|
||||
interface DisposeObject3DOptions {
|
||||
disposeTextures?: boolean;
|
||||
}
|
||||
|
||||
const MATERIAL_TEXTURE_KEYS = [
|
||||
"alphaMap",
|
||||
"aoMap",
|
||||
"bumpMap",
|
||||
"clearcoatMap",
|
||||
"clearcoatNormalMap",
|
||||
"clearcoatRoughnessMap",
|
||||
"displacementMap",
|
||||
"emissiveMap",
|
||||
"envMap",
|
||||
"gradientMap",
|
||||
"lightMap",
|
||||
"map",
|
||||
"metalnessMap",
|
||||
"normalMap",
|
||||
"roughnessMap",
|
||||
"sheenColorMap",
|
||||
"sheenRoughnessMap",
|
||||
"specularColorMap",
|
||||
"specularIntensityMap",
|
||||
"specularMap",
|
||||
"thicknessMap",
|
||||
"transmissionMap",
|
||||
] as const satisfies readonly TextureMaterialKey[];
|
||||
|
||||
export function disposeObject3D(
|
||||
object: THREE.Object3D,
|
||||
options: DisposeObject3DOptions = {},
|
||||
): void {
|
||||
object.traverse((child) => {
|
||||
if (!(child instanceof THREE.Mesh)) return;
|
||||
|
||||
child.geometry?.dispose();
|
||||
|
||||
if (Array.isArray(child.material)) {
|
||||
child.material.forEach((material) => disposeMaterial(material, options));
|
||||
} else if (child.material) {
|
||||
disposeMaterial(child.material, options);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function disposeMaterial(
|
||||
material: THREE.Material,
|
||||
options: DisposeObject3DOptions,
|
||||
): void {
|
||||
material.dispose();
|
||||
if (!options.disposeTextures) return;
|
||||
|
||||
const materialWithTextures = material as MaterialWithTextureSlots;
|
||||
|
||||
for (const key of MATERIAL_TEXTURE_KEYS) {
|
||||
const value = materialWithTextures[key];
|
||||
|
||||
if (value instanceof THREE.Texture) {
|
||||
value.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
GAME_SCENE_FALLBACK_BACKGROUND_COLOR,
|
||||
GAME_SCENE_SKY_FALLBACK_MODEL_PATH,
|
||||
GAME_SCENE_SKY_MODEL_PATH,
|
||||
GAME_SCENE_SKY_MODEL_SCALE,
|
||||
PHYSICS_SCENE_BACKGROUND_COLOR,
|
||||
@@ -35,6 +36,7 @@ export function Environment(): React.JSX.Element {
|
||||
{showSky ? (
|
||||
<SkyModel
|
||||
fallbackColor={GAME_SCENE_FALLBACK_BACKGROUND_COLOR}
|
||||
fallbackModelPath={GAME_SCENE_SKY_FALLBACK_MODEL_PATH}
|
||||
modelPath={GAME_SCENE_SKY_MODEL_PATH}
|
||||
scale={GAME_SCENE_SKY_MODEL_SCALE}
|
||||
/>
|
||||
|
||||
+1
-1
@@ -90,7 +90,7 @@ export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element {
|
||||
onLoadingStateChange={onLoadingStateChange}
|
||||
onOctreeReady={handleOctreeReady}
|
||||
/>
|
||||
<PersonnageSystem />
|
||||
{showGameStage ? <PersonnageSystem /> : null}
|
||||
{showGameStage ? (
|
||||
<Physics>
|
||||
<GameStageLoaded onLoaded={handleGameStageLoaded} />
|
||||
|
||||
@@ -194,17 +194,18 @@ function createInstanceMatrices(
|
||||
const position = new THREE.Vector3();
|
||||
const rotation = new THREE.Euler();
|
||||
const quaternion = new THREE.Quaternion();
|
||||
const scale = new THREE.Vector3(
|
||||
scaleMultiplier,
|
||||
scaleMultiplier,
|
||||
scaleMultiplier,
|
||||
);
|
||||
const scale = new THREE.Vector3();
|
||||
|
||||
for (const instance of instances) {
|
||||
const matrix = new THREE.Matrix4();
|
||||
|
||||
position.set(...instance.position);
|
||||
position.y += -geometryBottomY * scaleMultiplier;
|
||||
scale.set(
|
||||
instance.scale[0] * scaleMultiplier,
|
||||
instance.scale[1] * scaleMultiplier,
|
||||
instance.scale[2] * scaleMultiplier,
|
||||
);
|
||||
position.y += -geometryBottomY * scale.y;
|
||||
rotation.set(
|
||||
instance.rotation[0] + rotationOffset[0],
|
||||
instance.rotation[1] + rotationOffset[1],
|
||||
|
||||
Reference in New Issue
Block a user