its functionning
This commit is contained in:
@@ -1,23 +1,18 @@
|
||||
import { Ebike } from "@/components/ebike/Ebike";
|
||||
import { InteractableObject } from "@/components/three/interaction/InteractableObject";
|
||||
import { RepairGame } from "@/components/three/gameplay/RepairGame";
|
||||
import { RepairGamePreloader } from "@/components/three/gameplay/RepairGamePreloader";
|
||||
import { PylonDownedPylon } from "@/components/gameplay/pylon/PylonDownedPylon";
|
||||
import { PylonNarrativeFlow } from "@/components/gameplay/pylon/PylonNarrativeFlow";
|
||||
import { ZoneDebugVisual } from "@/components/zone/ZoneDetection";
|
||||
import { PYLON_APPROACH_ZONE, PYLON_ARRIVED_ZONE } from "@/data/gameplay/zones";
|
||||
import { isDebugEnabled } from "@/utils/debug/isDebugEnabled";
|
||||
import {
|
||||
REPAIR_MISSION_POSITION_ENTRIES,
|
||||
REPAIR_MISSION_TRIGGERS,
|
||||
} from "@/data/gameplay/repairMissionAnchors";
|
||||
import { REPAIR_MISSION_TRIGGERS } from "@/data/gameplay/repairMissionAnchors";
|
||||
import {
|
||||
INTRO_STAGE_ANCHOR,
|
||||
OUTRO_STAGE_ANCHOR,
|
||||
} from "@/data/gameplay/gameStageAnchors";
|
||||
import { useGameStore } from "@/managers/stores/useGameStore";
|
||||
import { useRepairMissionAnchorStore } from "@/managers/stores/useRepairMissionAnchorStore";
|
||||
import { isPylonNarrativeStep } from "@/types/gameplay/repairMission";
|
||||
import type { RepairMissionTriggerConfig } from "@/types/gameplay/repairMission";
|
||||
import type { Vector3Tuple } from "@/types/three/three";
|
||||
import { getRepairMissionPosition } from "@/utils/gameplay/repairMissionPosition";
|
||||
@@ -84,17 +79,13 @@ function RepairMissionTrigger({
|
||||
|
||||
export function GameStageContent(): React.JSX.Element {
|
||||
const mainState = useGameStore((state) => state.mainState);
|
||||
const pylonStep = useGameStore((state) => state.pylon.currentStep);
|
||||
const anchors = useRepairMissionAnchorStore((state) => state.anchors);
|
||||
|
||||
const pylonInNarrative =
|
||||
mainState === "pylon" && isPylonNarrativeStep(pylonStep);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Preload ONLY the next upcoming mission's assets so we never keep a
|
||||
finished mission's heavy textures resident (ebike alone is ~40MB).
|
||||
Holding every mission at once is what saturated GPU memory. */}
|
||||
{/* Pre-load the next mission's repair assets while the player is still
|
||||
in the world, so the isolated repair scene mounts instantly.
|
||||
Only load pylon during ebike (not before) to avoid holding a
|
||||
finished mission's textures in VRAM. */}
|
||||
{mainState === "intro" || mainState === "ebike" ? (
|
||||
<RepairGamePreloader mission="pylon" />
|
||||
) : null}
|
||||
@@ -110,18 +101,13 @@ export function GameStageContent(): React.JSX.Element {
|
||||
</>
|
||||
) : null}
|
||||
{mainState === "pylon" ? <PylonNarrativeFlow /> : null}
|
||||
{REPAIR_MISSION_POSITION_ENTRIES.map(({ mission }) => {
|
||||
const position = getRepairMissionPosition(mission, anchors);
|
||||
if (!position) return null;
|
||||
if (
|
||||
mission === "pylon" &&
|
||||
(pylonInNarrative || pylonStep === "waiting")
|
||||
)
|
||||
return null;
|
||||
return (
|
||||
<RepairGame key={mission} mission={mission} position={position} />
|
||||
);
|
||||
})}
|
||||
|
||||
{/* RepairGame is NO LONGER rendered here. When a repair step becomes
|
||||
active, page.tsx unmounts this whole world and mounts the isolated
|
||||
RepairGameScene instead, freeing all map/character VRAM. */}
|
||||
|
||||
{/* Trigger sphere that starts the ebike repair (locked → waiting).
|
||||
The repair scene swap is then handled by useRepairGameStatus. */}
|
||||
{REPAIR_MISSION_TRIGGERS.map((config) => (
|
||||
<RepairMissionTrigger key={config.mission} config={config} />
|
||||
))}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
+27
-5
@@ -1,9 +1,10 @@
|
||||
import { Suspense, useEffect } from "react";
|
||||
import { Suspense, useEffect, useRef } from "react";
|
||||
import { Physics } from "@react-three/rapier";
|
||||
import {
|
||||
PLAYER_SPAWN_POSITION_GAME,
|
||||
PLAYER_SPAWN_POSITION_PHYSICS,
|
||||
} from "@/data/player/playerConfig";
|
||||
import { useRepairTransitionStore } from "@/managers/stores/useRepairTransitionStore";
|
||||
import { useCameraMode } from "@/hooks/debug/useCameraMode";
|
||||
import { useEnvironmentDebug } from "@/hooks/debug/useEnvironmentDebug";
|
||||
import { useMapPerformanceDebug } from "@/hooks/debug/useMapPerformanceDebug";
|
||||
@@ -53,10 +54,31 @@ export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element {
|
||||
handleShadowWarmupStarted,
|
||||
shouldWarmUpShadows,
|
||||
} = useWorldSceneLoading({ sceneMode, onLoadingStateChange });
|
||||
const playerSpawnPosition =
|
||||
sceneMode === "game"
|
||||
? PLAYER_SPAWN_POSITION_GAME
|
||||
: PLAYER_SPAWN_POSITION_PHYSICS;
|
||||
// Capture the spawn position once on mount via a ref so it never changes
|
||||
// mid-session (spawnPosition is reactive in Player and would re-spawn the
|
||||
// character on every prop change). If the player returns from a repair
|
||||
// scene, savedPlayerPosition holds their world position; otherwise fall
|
||||
// back to the default spawn from playerConfig.
|
||||
const savedPlayerPosition = useRepairTransitionStore(
|
||||
(s) => s.savedPlayerPosition,
|
||||
);
|
||||
const playerSpawnPositionRef = useRef(
|
||||
savedPlayerPosition ??
|
||||
(sceneMode === "game"
|
||||
? PLAYER_SPAWN_POSITION_GAME
|
||||
: PLAYER_SPAWN_POSITION_PHYSICS),
|
||||
);
|
||||
const playerSpawnPosition = playerSpawnPositionRef.current;
|
||||
|
||||
// Clear the saved position right after capturing it so the next world
|
||||
// mount uses the default spawn instead of the stale repair-exit position.
|
||||
useEffect(() => {
|
||||
if (savedPlayerPosition !== null) {
|
||||
useRepairTransitionStore.getState().setSavedPlayerPosition(null);
|
||||
}
|
||||
// Only on mount — intentionally no deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
const showHandTrackingGloves =
|
||||
sceneMode === "physics" ||
|
||||
(status !== "idle" && usageStatus !== "inactive");
|
||||
|
||||
Reference in New Issue
Block a user