add: highlight broken repair parts during scan
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
import { useRef } from "react";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import * as THREE from "three";
|
||||
|
||||
interface RepairBrokenPartHighlightProps {
|
||||
target: THREE.Object3D;
|
||||
}
|
||||
|
||||
const _box = new THREE.Box3();
|
||||
const _sphere = new THREE.Sphere();
|
||||
const _worldPosition = new THREE.Vector3();
|
||||
const _localPosition = new THREE.Vector3();
|
||||
|
||||
export function RepairBrokenPartHighlight({
|
||||
target,
|
||||
}: RepairBrokenPartHighlightProps): React.JSX.Element {
|
||||
const groupRef = useRef<THREE.Group>(null);
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
const group = groupRef.current;
|
||||
if (!group) return;
|
||||
|
||||
_box.setFromObject(target).getBoundingSphere(_sphere);
|
||||
|
||||
_worldPosition.copy(_sphere.center);
|
||||
_localPosition.copy(_worldPosition);
|
||||
group.parent?.worldToLocal(_localPosition);
|
||||
group.position.copy(_localPosition);
|
||||
|
||||
const pulse = 1 + Math.sin(clock.elapsedTime * 5) * 0.08;
|
||||
const radius = Math.max(_sphere.radius, 0.35) * pulse;
|
||||
group.scale.setScalar(radius);
|
||||
});
|
||||
|
||||
return (
|
||||
<group ref={groupRef}>
|
||||
<mesh>
|
||||
<sphereGeometry args={[1, 32, 16]} />
|
||||
<meshBasicMaterial color="#ef4444" transparent opacity={0.14} />
|
||||
</mesh>
|
||||
<mesh>
|
||||
<sphereGeometry args={[1.06, 32, 16]} />
|
||||
<meshBasicMaterial
|
||||
color="#ef4444"
|
||||
wireframe
|
||||
transparent
|
||||
opacity={0.65}
|
||||
/>
|
||||
</mesh>
|
||||
<mesh rotation={[Math.PI / 2, 0, 0]}>
|
||||
<torusGeometry args={[1.12, 0.025, 8, 96]} />
|
||||
<meshBasicMaterial color="#dc2626" transparent opacity={0.9} />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import * as THREE from "three";
|
||||
import { RepairBrokenPartHighlight } from "@/components/three/gameplay/RepairBrokenPartHighlight";
|
||||
import { ExplodableModel } from "@/components/three/models/ExplodableModel";
|
||||
import { RepairScanVisual } from "@/components/three/gameplay/RepairScanVisual";
|
||||
import { REPAIR_SCAN_PART_SECONDS } from "@/data/gameplay/repairGameConfig";
|
||||
import type { RepairMissionConfig } from "@/data/gameplay/repairMissions";
|
||||
import type {
|
||||
RepairMissionConfig,
|
||||
RepairMissionPartConfig,
|
||||
} from "@/data/gameplay/repairMissions";
|
||||
import type { ExplodedPart } from "@/utils/three/ExplodedModel";
|
||||
|
||||
interface RepairScanSequenceProps {
|
||||
@@ -17,6 +22,10 @@ export function RepairScanSequence({
|
||||
const [parts, setParts] = useState<readonly ExplodedPart[]>([]);
|
||||
const [activePartIndex, setActivePartIndex] = useState(0);
|
||||
const activePart = parts[activePartIndex];
|
||||
const brokenPartIndexes = getBrokenPartIndexes(parts, config.brokenParts);
|
||||
const visibleBrokenPartIndexes = brokenPartIndexes.filter(
|
||||
(partIndex) => partIndex <= activePartIndex,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (parts.length === 0) return undefined;
|
||||
@@ -46,6 +55,55 @@ export function RepairScanSequence({
|
||||
onPartsReady={setParts}
|
||||
/>
|
||||
<RepairScanVisual target={activePart?.object} />
|
||||
{visibleBrokenPartIndexes.map((partIndex) => {
|
||||
const part = parts[partIndex];
|
||||
if (!part) return null;
|
||||
|
||||
return (
|
||||
<RepairBrokenPartHighlight
|
||||
key={part.object.uuid}
|
||||
target={part.object}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
function getBrokenPartIndexes(
|
||||
parts: readonly ExplodedPart[],
|
||||
brokenParts: readonly RepairMissionPartConfig[],
|
||||
): number[] {
|
||||
if (parts.length === 0 || brokenParts.length === 0) return [];
|
||||
|
||||
const matchedIndexes = brokenParts.flatMap((brokenPart) => {
|
||||
const { nodeName } = brokenPart;
|
||||
if (!nodeName) return [];
|
||||
|
||||
const index = parts.findIndex((part) =>
|
||||
objectContainsNodeName(part.object, nodeName),
|
||||
);
|
||||
|
||||
return index >= 0 ? [index] : [];
|
||||
});
|
||||
|
||||
if (matchedIndexes.length > 0) return [...new Set(matchedIndexes)];
|
||||
|
||||
return parts.slice(0, brokenParts.length).map((_, index) => index);
|
||||
}
|
||||
|
||||
function objectContainsNodeName(
|
||||
object: THREE.Object3D,
|
||||
nodeName: string,
|
||||
): boolean {
|
||||
if (object.name === nodeName) return true;
|
||||
|
||||
let found = false;
|
||||
object.traverse((child) => {
|
||||
if (child.name === nodeName) {
|
||||
found = true;
|
||||
}
|
||||
});
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user