diff --git a/src/data/world/octreeCollisionConfig.ts b/src/data/world/octreeCollisionConfig.ts new file mode 100644 index 0000000..88e6076 --- /dev/null +++ b/src/data/world/octreeCollisionConfig.ts @@ -0,0 +1,57 @@ +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; + +export const LA_FABRIK_INTERIOR_COLLISION_BOXES = [ + { + 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; +} diff --git a/src/world/GameMap.tsx b/src/world/GameMap.tsx index af31464..3b437d5 100644 --- a/src/world/GameMap.tsx +++ b/src/world/GameMap.tsx @@ -27,6 +27,7 @@ import { useMapLodModelPath } from "@/hooks/world/useMapLodModelPath"; import { GameMapCollision } from "@/world/GameMapCollision"; import { GeneratedMapNodeInstance } from "@/world/map-generated/GeneratedMapNodeInstance"; import { isGeneratedMapModelName } from "@/data/world/generatedMapModelConfig"; +import { hasMapOctreeCollisionBox } from "@/data/world/octreeCollisionConfig"; import { getMapSingleModelScaleMultiplier } from "@/data/world/mapInstancingConfig"; import { MapInstancingSystem } from "@/world/map-instancing/MapInstancingSystem"; import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading"; @@ -115,6 +116,9 @@ export function GameMap({ const [collisionMapNodes, setCollisionMapNodes] = useState( [], ); + const [proxyCollisionMapNodes, setProxyCollisionMapNodes] = useState< + MapNode[] + >([]); const [terrainNode, setTerrainNode] = useState(null); const [mapLoaded, setMapLoaded] = useState(false); const [settledMapNodeCount, setSettledMapNodeCount] = useState(0); @@ -134,6 +138,7 @@ export function GameMap({ (currentStep: string) => { setRenderMapNodes([]); setCollisionMapNodes([]); + setProxyCollisionMapNodes([]); setTerrainNode(null); setMapLoaded(true); settledMapNodesRef.current.clear(); @@ -191,6 +196,10 @@ export function GameMap({ const modelUrl = sceneData.models.get(node.name); return { node, modelUrl: modelUrl ?? null }; }); + const loadedProxyCollisionNodes = sceneData.mapNodes.filter( + (node) => + node.type === "Object3D" && hasMapOctreeCollisionBox(node.name), + ); const loadedTerrainNode = getTerrainMapNode(sceneData.mapNodes); const repairMissionAnchors = getRepairMissionMapAnchors( sceneData.mapNodes, @@ -211,6 +220,7 @@ export function GameMap({ setRenderMapNodes(loadedMapNodes); setCollisionMapNodes(loadedCollisionNodes); + setProxyCollisionMapNodes(loadedProxyCollisionNodes); setTerrainNode(loadedTerrainNode); setRepairMissionAnchors(repairMissionAnchors); setMapLoaded(true); @@ -285,6 +295,7 @@ export function GameMap({ buildOctree={buildOctree} mapReady={mapReady} nodes={collisionMapNodes} + proxyNodes={proxyCollisionMapNodes} onLoaded={onLoaded} onLoadingStateChange={onLoadingStateChange} onOctreeReady={onOctreeReady} diff --git a/src/world/GameMapCollision.tsx b/src/world/GameMapCollision.tsx index c745ddc..5bce413 100644 --- a/src/world/GameMapCollision.tsx +++ b/src/world/GameMapCollision.tsx @@ -17,9 +17,23 @@ import { normalizeMapScale, useTerrainHeightSampler, } from "@/hooks/three/useTerrainHeight"; +import { + CHARACTER_CONFIGS, + CHARACTER_IDS, + type CharacterId, +} from "@/data/world/characters/characterConfig"; +import { + CHARACTER_OCTREE_COLLISION_BOX, + LA_FABRIK_INTERIOR_COLLISION_BOXES, + MAP_OCTREE_COLLISION_BOXES, + hasMapOctreeCollisionBox, + type OctreeCollisionBox, +} from "@/data/world/octreeCollisionConfig"; +import { getMapModelScaleMultiplier } from "@/data/world/mapInstancingConfig"; +import { useCharacterDebugStore } from "@/managers/stores/useCharacterDebugStore"; import { WorldBoundsCollision } from "@/world/collision/WorldBoundsCollision"; import type { MapNode } from "@/types/map/mapScene"; -import type { OctreeReadyHandler } from "@/types/three/three"; +import type { OctreeReadyHandler, Vector3Tuple } from "@/types/three/three"; import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading"; import { logModelLoadError } from "@/utils/three/modelLoadLogger"; @@ -39,6 +53,7 @@ interface GameMapCollisionProps { buildOctree?: boolean; mapReady: boolean; nodes: readonly GameMapCollisionNode[]; + proxyNodes: readonly MapNode[]; onLoaded?: (() => void) | undefined; onLoadingStateChange?: SceneLoadingChangeHandler | undefined; onOctreeReady: OctreeReadyHandler; @@ -101,6 +116,7 @@ export function GameMapCollision({ buildOctree = true, mapReady, nodes, + proxyNodes, onLoaded, onLoadingStateChange, onOctreeReady, @@ -111,8 +127,23 @@ export function GameMapCollision({ const [settledCollisionNodeCount, setSettledCollisionNodeCount] = useState(0); const terrainHeight = useTerrainHeightSampler(); const collisionNodes = nodes.filter(isCollisionNode); + const collisionSourceCount = + collisionNodes.length + proxyNodes.length + CHARACTER_IDS.length; const collisionReady = mapReady && settledCollisionNodeCount >= collisionNodes.length; + const characterCollisionSignature = useCharacterDebugStore((state) => + CHARACTER_IDS.map((id) => { + const character = state.characters[id]; + return [ + ...character.position, + ...character.rotation, + ...character.scale, + ].join(","); + }).join("|"), + ); + const collisionRebuildKey = collisionReady + ? `${collisionNodes.length}:${collisionSourceCount}:${characterCollisionSignature}` + : "pending"; const notifyLoaded = useCallback(() => { if (loadedNotifiedRef.current) return; @@ -144,14 +175,14 @@ export function GameMapCollision({ useOctreeGraphNode( groupRef, handleOctreeReady, - collisionReady ? collisionNodes.length : 0, - buildOctree && collisionReady && collisionNodes.length > 0, + collisionRebuildKey, + buildOctree && collisionReady && collisionSourceCount > 0, ); useEffect(() => { if (!mapReady) return; - if (collisionNodes.length === 0) { + if (collisionSourceCount === 0) { notifyLoaded(); return; } @@ -171,6 +202,7 @@ export function GameMapCollision({ }, [ buildOctree, collisionNodes.length, + collisionSourceCount, collisionReady, mapReady, notifyLoaded, @@ -180,6 +212,18 @@ export function GameMapCollision({ return ( {mapReady ? : null} + {mapReady + ? proxyNodes.map((node, index) => ( + + )) + : null} + {mapReady ? ( + + ) : null} {mapReady ? collisionNodes.map((mapNode, index) => ( + <> + + {node.name === "lafabrik" ? ( + + {LA_FABRIK_INTERIOR_COLLISION_BOXES.map((box, index) => ( + + ))} + + ) : null} + + ); +} + +function CollisionBox({ box }: { box: OctreeCollisionBox }): React.JSX.Element { + return ( + + + + + ); +} + +function createScaledMapNodeScale(node: MapNode): Vector3Tuple { + const baseScale = normalizeMapScale(node.scale); + const scaleMultiplier = getMapModelScaleMultiplier(node.name); + + return [ + baseScale[0] * scaleMultiplier, + baseScale[1] * scaleMultiplier, + baseScale[2] * scaleMultiplier, + ]; +} + +function MapCollisionBoxProxy({ + node, + terrainHeight, +}: { + node: MapNode; + terrainHeight: TerrainHeightSampler; +}): React.JSX.Element | null { + const collisionBox = hasMapOctreeCollisionBox(node.name) + ? MAP_OCTREE_COLLISION_BOXES[node.name] + : null; + const normalizedScale = useMemo(() => createScaledMapNodeScale(node), [node]); + const position = useMemo(() => { + const [x, y, z] = node.position; + if (!collisionBox) return [x, y, z] satisfies Vector3Tuple; + + const height = terrainHeight.getHeight(x, z); + const bottomOffset = -collisionBox.bottomY * normalizedScale[1]; + + return [x, (height ?? y) + bottomOffset, z] satisfies Vector3Tuple; + }, [collisionBox, node.position, normalizedScale, terrainHeight]); + + if (!collisionBox) return null; + + return ( + + + + ); +} + +function CharacterCollisionProxies({ + terrainHeight, +}: { + terrainHeight: TerrainHeightSampler; +}): React.JSX.Element { + return ( + <> + {CHARACTER_IDS.map((id) => ( + + ))} + + ); +} + +function CharacterCollisionProxy({ + id, + terrainHeight, +}: { + id: CharacterId; + terrainHeight: TerrainHeightSampler; +}): React.JSX.Element { + const config = CHARACTER_CONFIGS[id]; + const state = useCharacterDebugStore((store) => store.characters[id]); + const position = useMemo(() => { + const [x, y, z] = state.position; + const height = terrainHeight.getHeight(x, z); + + return [x, height ?? y, z] satisfies Vector3Tuple; + }, [state.position, terrainHeight]); + + return ( + + + ); }