101 lines
3.3 KiB
TypeScript
101 lines
3.3 KiB
TypeScript
import { CuboidCollider, RigidBody } from "@react-three/rapier";
|
|
import { useMemo, useSyncExternalStore } from "react";
|
|
import type { Object3D } from "three";
|
|
import {
|
|
activeDungeonEncounterRevision,
|
|
subscribeDungeonEncounterSnapshots,
|
|
} from "../game/dungeonEncounterRuntime";
|
|
import { activeDungeonWorldObjectStates } from "../game/dungeonWorldInteraction";
|
|
import {
|
|
dungeonWorldObjectState,
|
|
type DungeonWorldObjectDefinition,
|
|
} from "../game/dungeonWorldObjects";
|
|
import {
|
|
DUNGEON_PHYSICAL_COLLISION_GROUPS,
|
|
DUNGEON_SIGHT_BLOCKER_COLLISION_GROUPS,
|
|
} from "./dungeonLineOfSight";
|
|
import { useGameGLTF } from "./useGameGLTF";
|
|
|
|
function AssetObject({ definition }: { readonly definition: DungeonWorldObjectDefinition }) {
|
|
const gltf = useGameGLTF(definition.visual!.assetUrl!);
|
|
const visual = useMemo<Object3D>(() => gltf.scene.clone(true), [gltf.scene]);
|
|
return <primitive object={visual} scale={definition.visual?.scale ?? [1, 1, 1]} />;
|
|
}
|
|
|
|
function ProceduralObject({ definition }: { readonly definition: DungeonWorldObjectDefinition }) {
|
|
const [x, y, z] = definition.halfExtents;
|
|
const round = definition.kind === "trap" || definition.kind === "ritual" || definition.kind === "trigger";
|
|
return (
|
|
<mesh castShadow receiveShadow>
|
|
{round
|
|
? <cylinderGeometry args={[Math.max(x, z), Math.max(x, z), y * 2, 16]} />
|
|
: <boxGeometry args={[x * 2, y * 2, z * 2]} />}
|
|
<meshStandardMaterial
|
|
color={definition.visual?.color ?? (definition.kind === "switch" ? "#dca24a" : "#65584b")}
|
|
emissive={definition.visual?.emissive ?? "#000000"}
|
|
roughness={0.72}
|
|
/>
|
|
</mesh>
|
|
);
|
|
}
|
|
|
|
function WorldObject({
|
|
definition,
|
|
stateId,
|
|
}: {
|
|
readonly definition: DungeonWorldObjectDefinition;
|
|
readonly stateId?: string;
|
|
}) {
|
|
const state = dungeonWorldObjectState(definition, stateId);
|
|
if (!state.visible) return null;
|
|
const showVisual = definition.visual?.hidden !== true;
|
|
if (!showVisual && !state.blocksMovement && !state.blocksSight) return null;
|
|
return (
|
|
<RigidBody
|
|
key={`${definition.id}:${state.id}`}
|
|
type="fixed"
|
|
colliders={false}
|
|
position={definition.position}
|
|
rotation={[0, definition.yaw ?? 0, 0]}
|
|
name={`dungeon-object:${definition.id}:${state.id}`}
|
|
>
|
|
{state.blocksMovement && (
|
|
<CuboidCollider
|
|
args={[...definition.halfExtents]}
|
|
collisionGroups={DUNGEON_PHYSICAL_COLLISION_GROUPS}
|
|
/>
|
|
)}
|
|
{state.blocksSight && (
|
|
<CuboidCollider
|
|
args={[...definition.halfExtents]}
|
|
collisionGroups={DUNGEON_SIGHT_BLOCKER_COLLISION_GROUPS}
|
|
sensor
|
|
/>
|
|
)}
|
|
{showVisual && (definition.visual?.assetUrl
|
|
? <AssetObject definition={definition} />
|
|
: <ProceduralObject definition={definition} />)}
|
|
</RigidBody>
|
|
);
|
|
}
|
|
|
|
export function DungeonWorldObjects({
|
|
definitions,
|
|
}: {
|
|
readonly definitions: readonly DungeonWorldObjectDefinition[];
|
|
}) {
|
|
useSyncExternalStore(
|
|
subscribeDungeonEncounterSnapshots,
|
|
activeDungeonEncounterRevision,
|
|
activeDungeonEncounterRevision,
|
|
);
|
|
const states = activeDungeonWorldObjectStates();
|
|
return (
|
|
<group name="dungeon-world-objects">
|
|
{definitions.map((definition) => (
|
|
<WorldObject key={definition.id} definition={definition} stateId={states[definition.id]} />
|
|
))}
|
|
</group>
|
|
);
|
|
}
|