o think is not that

This commit is contained in:
math-pixel
2026-06-01 22:26:58 +02:00
parent cd0afcda8c
commit d975aac018
6 changed files with 117 additions and 19 deletions
@@ -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;
}