109 lines
3.5 KiB
TypeScript
109 lines
3.5 KiB
TypeScript
import { Suspense, useCallback, useEffect } from "react";
|
|
import { Canvas, useThree } from "@react-three/fiber";
|
|
import { Physics } from "@react-three/rapier";
|
|
import * as THREE from "three";
|
|
import { DebugPerf } from "@/components/debug/DebugPerf";
|
|
import { RepairGame } from "@/components/three/gameplay/RepairGame";
|
|
import { logger } from "@/utils/core/Logger";
|
|
import type { RepairMissionId } from "@/types/gameplay/repairMission";
|
|
import type { Vector3Tuple } from "@/types/three/three";
|
|
|
|
// Isolated scene — no world offset, no terrain. The repair game runs
|
|
// fully centred in its own context so the heavy map never loads here.
|
|
const REPAIR_SCENE_POSITION: Vector3Tuple = [0, 0, 0];
|
|
|
|
// Background: very dark blue-grey to match Altera's night-time mood
|
|
const REPAIR_SCENE_BG = "#0b0d14";
|
|
|
|
// Lighting tuned to match the main world defaults from lightingConfig.ts
|
|
const AMBIENT_COLOR = "#dfe7d8";
|
|
const AMBIENT_INTENSITY = 0.9;
|
|
const SUN_COLOR = "#ffe2bf";
|
|
const SUN_INTENSITY = 2.2;
|
|
const SUN_POSITION: Vector3Tuple = [5, 8, 4];
|
|
|
|
// Mimic the first-person view from the main world:
|
|
// - PLAYER_EYE_HEIGHT = 1.75 → camera Y
|
|
// - Case floats at [0, 0.4, 1.8] (inspected) → [0, 1.05, 2.05] (repairing)
|
|
// - Look-at target averaged between those two states
|
|
const CAMERA_POSITION: Vector3Tuple = [5, 2, 2];
|
|
const CAMERA_LOOK_AT: Vector3Tuple = [0, 0.7, 1.9];
|
|
|
|
function RepairSceneCamera(): null {
|
|
const { camera } = useThree();
|
|
|
|
useEffect(() => {
|
|
camera.lookAt(...CAMERA_LOOK_AT);
|
|
}, [camera]);
|
|
|
|
return null;
|
|
}
|
|
|
|
interface RepairGameSceneProps {
|
|
mission: RepairMissionId;
|
|
}
|
|
|
|
export function RepairGameScene({
|
|
mission,
|
|
}: RepairGameSceneProps): React.JSX.Element {
|
|
const handleCreated = useCallback(({ gl }: { gl: THREE.WebGLRenderer }) => {
|
|
const canvas = gl.domElement;
|
|
const loseContextExt = gl.getContext().getExtension("WEBGL_lose_context");
|
|
|
|
const handleContextLost = (event: Event) => {
|
|
event.preventDefault();
|
|
logger.error("WebGL", "Repair scene context lost — attempting restore");
|
|
window.setTimeout(() => loseContextExt?.restoreContext(), 500);
|
|
};
|
|
|
|
const handleContextRestored = () => {
|
|
logger.info("WebGL", "Repair scene context restored");
|
|
};
|
|
|
|
canvas.addEventListener("webglcontextlost", handleContextLost);
|
|
canvas.addEventListener("webglcontextrestored", handleContextRestored);
|
|
}, []);
|
|
|
|
return (
|
|
<Canvas
|
|
camera={{ position: CAMERA_POSITION, fov: 42 }}
|
|
shadows={{ type: THREE.PCFShadowMap }}
|
|
gl={{
|
|
powerPreference: "high-performance",
|
|
antialias: true,
|
|
stencil: false,
|
|
}}
|
|
onCreated={handleCreated}
|
|
>
|
|
<color attach="background" args={[REPAIR_SCENE_BG]} />
|
|
|
|
<RepairSceneCamera />
|
|
|
|
{/* Lighting — mirrors the game world defaults */}
|
|
<ambientLight intensity={AMBIENT_INTENSITY} color={AMBIENT_COLOR} />
|
|
<directionalLight
|
|
position={SUN_POSITION}
|
|
intensity={SUN_INTENSITY}
|
|
color={SUN_COLOR}
|
|
castShadow
|
|
shadow-mapSize-width={1024}
|
|
shadow-mapSize-height={1024}
|
|
/>
|
|
|
|
<Suspense fallback={null}>
|
|
{/* Physics is required: TriggerObject and GrabbableObject both use
|
|
RigidBody. The world is minimal — no octree, no character bodies. */}
|
|
<Physics>
|
|
<RepairGame
|
|
mission={mission}
|
|
position={REPAIR_SCENE_POSITION}
|
|
snapToTerrain={false}
|
|
/>
|
|
</Physics>
|
|
</Suspense>
|
|
|
|
<DebugPerf />
|
|
</Canvas>
|
|
);
|
|
}
|