o think is not that
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "dev",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "dev"],
|
||||
"port": 5173
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import { useGLTF } from "@react-three/drei";
|
||||
import * as THREE from "three";
|
||||
@@ -33,13 +33,27 @@ export function PylonDownedPylon(): React.JSX.Element | null {
|
||||
}, [step]);
|
||||
|
||||
const { scene } = useGLTF(PYLON_MODEL_PATH);
|
||||
const clonedScene = useMemo(() => scene.clone(true), [scene]);
|
||||
|
||||
const showUpright =
|
||||
mainState !== "pylon" ||
|
||||
step === "waiting" ||
|
||||
step === "inspected" ||
|
||||
step === "fragmented" ||
|
||||
step === "scanning" ||
|
||||
step === "repairing" ||
|
||||
step === "reassembling" ||
|
||||
step === "done" ||
|
||||
step === "narrator-outro";
|
||||
|
||||
useFrame(() => {
|
||||
const group = groupRef.current;
|
||||
if (!group) return;
|
||||
|
||||
if (!isStraightening || straightenStartRef.current === null) {
|
||||
group.rotation.set(...(showUpright ? PYLON_UPRIGHT_ROTATION : PYLON_DOWNED_ROTATION));
|
||||
group.rotation.set(
|
||||
...(showUpright ? PYLON_UPRIGHT_ROTATION : PYLON_DOWNED_ROTATION),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -55,19 +69,19 @@ export function PylonDownedPylon(): React.JSX.Element | null {
|
||||
);
|
||||
});
|
||||
|
||||
const showUpright =
|
||||
mainState !== "pylon" ||
|
||||
step === "waiting" ||
|
||||
step === "inspected" ||
|
||||
step === "fragmented" ||
|
||||
step === "scanning" ||
|
||||
step === "repairing" ||
|
||||
step === "reassembling" ||
|
||||
step === "done" ||
|
||||
step === "narrator-outro";
|
||||
|
||||
const isPylonInteractive = step === "arrived" || step === "npc-return";
|
||||
|
||||
// During these steps the RepairGame renders its own pylon model
|
||||
// (exploded / reassembling / completion). Rendering the solid world
|
||||
// pylon on top would double the heaviest model's GPU cost at the same
|
||||
// spot — a prime cause of WebGL context loss. Let RepairGame own it.
|
||||
const repairGameOwnsModel =
|
||||
mainState === "pylon" &&
|
||||
(step === "fragmented" ||
|
||||
step === "scanning" ||
|
||||
step === "reassembling" ||
|
||||
step === "done");
|
||||
|
||||
const beginStraighten = (): void => {
|
||||
setIsStraightening(true);
|
||||
pylonStraighteningSignal.started = true;
|
||||
@@ -80,17 +94,19 @@ export function PylonDownedPylon(): React.JSX.Element | null {
|
||||
setIsStraightening(false);
|
||||
pylonStraighteningSignal.started = false;
|
||||
setCanMove(true);
|
||||
setMissionStep("pylon", "inspected");
|
||||
setMissionStep("pylon", "waiting");
|
||||
}, PYLON_STRAIGHTEN_ANIMATION_DURATION_MS);
|
||||
};
|
||||
|
||||
if (repairGameOwnsModel) return null;
|
||||
|
||||
return (
|
||||
<group
|
||||
ref={groupRef}
|
||||
position={PYLON_WORLD_POSITION}
|
||||
rotation={PYLON_DOWNED_ROTATION}
|
||||
>
|
||||
<primitive object={scene.clone(true)} />
|
||||
<primitive object={clonedScene} />
|
||||
{isPylonInteractive ? (
|
||||
<InteractableObject
|
||||
kind="trigger"
|
||||
@@ -117,7 +133,10 @@ export function PylonDownedPylon(): React.JSX.Element | null {
|
||||
void (async () => {
|
||||
const m = await loadDialogueManifest();
|
||||
if (!m) return;
|
||||
await playDialogueById(m, PYLON_NARRATIVE_DIALOGUES.demandeAide);
|
||||
await playDialogueById(
|
||||
m,
|
||||
PYLON_NARRATIVE_DIALOGUES.demandeAide,
|
||||
);
|
||||
})();
|
||||
},
|
||||
{ once: true },
|
||||
@@ -127,7 +146,10 @@ export function PylonDownedPylon(): React.JSX.Element | null {
|
||||
void (async () => {
|
||||
const manifest = await loadDialogueManifest();
|
||||
if (!manifest) return;
|
||||
await playDialogueById(manifest, PYLON_NARRATIVE_DIALOGUES.demandeAide);
|
||||
await playDialogueById(
|
||||
manifest,
|
||||
PYLON_NARRATIVE_DIALOGUES.demandeAide,
|
||||
);
|
||||
})();
|
||||
}
|
||||
} else if (step === "npc-return" && !isStraightening) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useEffect } from "react";
|
||||
import { useGameStore } from "@/managers/stores/useGameStore";
|
||||
import { useDialoguePlayback } from "@/hooks/gameplay/useDialoguePlayback";
|
||||
import { ZoneDetection } from "@/components/zone/ZoneDetection";
|
||||
@@ -28,6 +29,16 @@ export function PylonNarrativeFlow(): React.JSX.Element | null {
|
||||
onComplete: () => completeMission("pylon"),
|
||||
});
|
||||
|
||||
// Advance waiting → inspected in a separate macrotask so React and Rapier
|
||||
// finish their current commit before RigidBody colliders are created.
|
||||
useEffect(() => {
|
||||
if (mainState !== "pylon" || step !== "waiting") return undefined;
|
||||
const id = window.setTimeout(() => {
|
||||
setMissionStep("pylon", "inspected");
|
||||
}, 0);
|
||||
return () => window.clearTimeout(id);
|
||||
}, [mainState, step, setMissionStep]);
|
||||
|
||||
if (mainState !== "pylon") return null;
|
||||
|
||||
if (step === "locked") {
|
||||
|
||||
@@ -219,7 +219,11 @@ export function RepairCaseModel({
|
||||
parsedScale[2] * pop.current.scale,
|
||||
);
|
||||
|
||||
if (placeholderNodes.current.length > 0) {
|
||||
// Placeholders are only consumed when the case is open (repairing). While
|
||||
// floating (inspected/scanning) the case bobs every frame, so emitting here
|
||||
// would fire a React setState on every frame, re-rendering the whole
|
||||
// RepairGame subtree continuously. Only compute when not floating.
|
||||
if (!floating && placeholderNodes.current.length > 0) {
|
||||
const placeholders: RepairCasePlaceholder[] = [];
|
||||
placeholderNodes.current.forEach((child) => {
|
||||
child.getWorldPosition(placeholderPosition.current);
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useEffect } from "react";
|
||||
import { useGLTF } from "@react-three/drei";
|
||||
import { REPAIR_CASE_MODEL_PATH } from "@/data/gameplay/repairCaseConfig";
|
||||
import { REPAIR_MISSIONS } from "@/data/gameplay/repairMissions";
|
||||
import type { RepairMissionId } from "@/types/gameplay/repairMission";
|
||||
|
||||
function getPreloadPaths(mission: RepairMissionId): string[] {
|
||||
const config = REPAIR_MISSIONS[mission];
|
||||
return [
|
||||
...new Set([
|
||||
REPAIR_CASE_MODEL_PATH,
|
||||
config.modelPath,
|
||||
...config.brokenParts.flatMap((p) => p.modelPath ?? []),
|
||||
...config.replacementParts.flatMap((p) => p.modelPath ?? []),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
interface RepairGamePreloaderProps {
|
||||
mission: RepairMissionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires useGLTF.preload() for every asset used by a repair mission.
|
||||
* Renders nothing — pure background loading.
|
||||
*/
|
||||
export function RepairGamePreloader({
|
||||
mission,
|
||||
}: RepairGamePreloaderProps): null {
|
||||
useEffect(() => {
|
||||
for (const path of getPreloadPaths(mission)) {
|
||||
useGLTF.preload(path);
|
||||
}
|
||||
}, [mission]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
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";
|
||||
@@ -91,6 +92,14 @@ export function GameStageContent(): React.JSX.Element {
|
||||
|
||||
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. */}
|
||||
{mainState === "intro" || mainState === "ebike" ? (
|
||||
<RepairGamePreloader mission="pylon" />
|
||||
) : null}
|
||||
{mainState === "pylon" ? <RepairGamePreloader mission="farm" /> : null}
|
||||
|
||||
{mainState === "intro" ? <StageAnchor {...INTRO_STAGE_ANCHOR} /> : null}
|
||||
<Ebike position={EBIKE_WORLD_POSITION} />
|
||||
<PylonDownedPylon />
|
||||
@@ -104,7 +113,11 @@ export function GameStageContent(): React.JSX.Element {
|
||||
{REPAIR_MISSION_POSITION_ENTRIES.map(({ mission }) => {
|
||||
const position = getRepairMissionPosition(mission, anchors);
|
||||
if (!position) return null;
|
||||
if (mission === "pylon" && pylonInNarrative) return null;
|
||||
if (
|
||||
mission === "pylon" &&
|
||||
(pylonInNarrative || pylonStep === "waiting")
|
||||
)
|
||||
return null;
|
||||
return (
|
||||
<RepairGame key={mission} mission={mission} position={position} />
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user