Release v0.1.9 2026-07-13
This commit is contained in:
+97
-17
@@ -1,15 +1,22 @@
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import { useGLTF } from "@react-three/drei";
|
||||
import { Suspense, useEffect, useLayoutEffect, useMemo, useRef } from "react";
|
||||
import { Component, Suspense, useEffect, useLayoutEffect, useMemo, useRef, type ReactNode } from "react";
|
||||
import * as THREE from "three";
|
||||
import { ARENA_CENTER, ARENA_SIZE_MULTIPLIER, ARENA_WALL_RADIUS } from "../game/arena";
|
||||
import { bossRoomFor, type BossRoomDefinition, type BossRoomFloor } from "../game/bossRooms";
|
||||
import { useGameStore } from "../game/store";
|
||||
import { LEGACY_GAME_ASSETS_FORCED, useGameGLTF } from "./GameAssetProvider";
|
||||
|
||||
const ROOM_CENTER_Z = ARENA_CENTER[1];
|
||||
const KAYKIT_DUNGEON_PILLAR_URL = new URL("../assets/game/models/original/environment/kaykit-dungeon/pillar-decorated.glb", import.meta.url).href;
|
||||
const KAYKIT_DUNGEON_WALL_URL = new URL("../assets/game/models/original/environment/kaykit-dungeon/wall-pillar.glb", import.meta.url).href;
|
||||
const KAYKIT_DUNGEON_TORCH_URL = new URL("../assets/game/models/original/environment/kaykit-dungeon/torch-lit.glb", import.meta.url).href;
|
||||
const KAYKIT_DUNGEON_KIT_URL = new URL("../assets/game/models/original/environment/kaykit-dungeon/dungeon-kit-uastc.glb", import.meta.url).href;
|
||||
const DUNGEON_MESH_NAMES = {
|
||||
pillar: "pillar_decorated",
|
||||
wall: "wall_pillar",
|
||||
torch: "torch_lit",
|
||||
} as const;
|
||||
|
||||
type ArenaFixture = {
|
||||
position: readonly [number, number, number];
|
||||
@@ -106,21 +113,19 @@ function firstMesh(scene: THREE.Object3D) {
|
||||
return mesh as THREE.Mesh<THREE.BufferGeometry, THREE.Material | THREE.Material[]>;
|
||||
}
|
||||
|
||||
function DungeonAssetInstances({
|
||||
url,
|
||||
function DungeonMeshInstances({
|
||||
mesh,
|
||||
fixtures,
|
||||
tint,
|
||||
opacity = 1,
|
||||
colors,
|
||||
}: {
|
||||
url: string;
|
||||
mesh: THREE.Mesh<THREE.BufferGeometry, THREE.Material | THREE.Material[]>;
|
||||
fixtures: readonly ArenaFixture[];
|
||||
tint: string;
|
||||
opacity?: number;
|
||||
colors?: readonly THREE.Color[];
|
||||
}) {
|
||||
const gltf = useGLTF(url, false, true);
|
||||
const mesh = useMemo(() => firstMesh(gltf.scene), [gltf.scene]);
|
||||
const sourceMaterial = Array.isArray(mesh.material) ? mesh.material[0] : mesh.material;
|
||||
const material = useMemo(() => {
|
||||
const next = sourceMaterial.clone();
|
||||
@@ -156,20 +161,77 @@ function DungeonAssetInstances({
|
||||
return <instancedMesh ref={instances} args={[mesh.geometry, material, fixtures.length]} castShadow receiveShadow />;
|
||||
}
|
||||
|
||||
function ArenaArchitecture({ room }: { room: BossRoomDefinition }) {
|
||||
const walls = useMemo(() => ARENA_WALL_SEGMENTS.map((fixture) => ({
|
||||
function LegacyDungeonAssetInstances({
|
||||
url,
|
||||
...props
|
||||
}: Omit<Parameters<typeof DungeonMeshInstances>[0], "mesh"> & { url: string }) {
|
||||
const gltf = useGLTF(url, false, true);
|
||||
const mesh = useMemo(() => firstMesh(gltf.scene), [gltf.scene]);
|
||||
return <DungeonMeshInstances mesh={mesh} {...props} />;
|
||||
}
|
||||
|
||||
function arenaWalls(room: BossRoomDefinition) {
|
||||
return ARENA_WALL_SEGMENTS.map((fixture) => ({
|
||||
...fixture,
|
||||
scaleY: room.wallHeight / 4,
|
||||
})), [room.wallHeight]);
|
||||
}));
|
||||
}
|
||||
|
||||
function LegacyArenaArchitecture({ room }: { room: BossRoomDefinition }) {
|
||||
const walls = useMemo(() => arenaWalls(room), [room]);
|
||||
return (
|
||||
<group>
|
||||
<DungeonAssetInstances url={KAYKIT_DUNGEON_WALL_URL} fixtures={walls} tint={room.wallColor} opacity={0.54} />
|
||||
<DungeonAssetInstances url={KAYKIT_DUNGEON_PILLAR_URL} fixtures={ARENA_COLUMNS} tint={room.wallColor} />
|
||||
<DungeonAssetInstances url={KAYKIT_DUNGEON_TORCH_URL} fixtures={ARENA_TORCHES} tint="#ffffff" colors={ARENA_TORCH_COLORS} />
|
||||
<LegacyDungeonAssetInstances url={KAYKIT_DUNGEON_WALL_URL} fixtures={walls} tint={room.wallColor} opacity={0.54} />
|
||||
<LegacyDungeonAssetInstances url={KAYKIT_DUNGEON_PILLAR_URL} fixtures={ARENA_COLUMNS} tint={room.wallColor} />
|
||||
<LegacyDungeonAssetInstances url={KAYKIT_DUNGEON_TORCH_URL} fixtures={ARENA_TORCHES} tint="#ffffff" colors={ARENA_TORCH_COLORS} />
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
function namedMesh(scene: THREE.Object3D, name: string) {
|
||||
const object = scene.getObjectByName(name);
|
||||
if (object instanceof THREE.Mesh) {
|
||||
return object as THREE.Mesh<THREE.BufferGeometry, THREE.Material | THREE.Material[]>;
|
||||
}
|
||||
throw new Error(`Dungeon kit is missing mesh ${name}.`);
|
||||
}
|
||||
|
||||
function DungeonKitArchitecture({ room }: { room: BossRoomDefinition }) {
|
||||
const gltf = useGameGLTF(KAYKIT_DUNGEON_KIT_URL);
|
||||
const walls = useMemo(() => arenaWalls(room), [room]);
|
||||
const meshes = useMemo(() => ({
|
||||
pillar: namedMesh(gltf.scene, DUNGEON_MESH_NAMES.pillar),
|
||||
wall: namedMesh(gltf.scene, DUNGEON_MESH_NAMES.wall),
|
||||
torch: namedMesh(gltf.scene, DUNGEON_MESH_NAMES.torch),
|
||||
}), [gltf.scene]);
|
||||
return (
|
||||
<group>
|
||||
<DungeonMeshInstances mesh={meshes.wall} fixtures={walls} tint={room.wallColor} opacity={0.54} />
|
||||
<DungeonMeshInstances mesh={meshes.pillar} fixtures={ARENA_COLUMNS} tint={room.wallColor} />
|
||||
<DungeonMeshInstances mesh={meshes.torch} fixtures={ARENA_TORCHES} tint="#ffffff" colors={ARENA_TORCH_COLORS} />
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
class DungeonAssetErrorBoundary extends Component<{
|
||||
children: ReactNode;
|
||||
fallback: ReactNode;
|
||||
}, { failed: boolean }> {
|
||||
state = { failed: false };
|
||||
|
||||
static getDerivedStateFromError() {
|
||||
return { failed: true };
|
||||
}
|
||||
|
||||
componentDidCatch(error: unknown) {
|
||||
console.warn("Optimized dungeon asset failed; using legacy GLBs.", error);
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.state.failed ? this.props.fallback : this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
function RoomWallFallback({ room }: { room: BossRoomDefinition }) {
|
||||
const walls = useRef<THREE.Group>(null);
|
||||
const previousCameraPosition = useRef<THREE.Vector3 | null>(null);
|
||||
@@ -205,14 +267,30 @@ function RoomWallFallback({ room }: { room: BossRoomDefinition }) {
|
||||
);
|
||||
}
|
||||
|
||||
function RoomWalls({ room }: { room: BossRoomDefinition }) {
|
||||
function LegacyRoomWalls({ room }: { room: BossRoomDefinition }) {
|
||||
return (
|
||||
<Suspense fallback={<RoomWallFallback room={room} />}>
|
||||
<ArenaArchitecture room={room} />
|
||||
<LegacyArenaArchitecture room={room} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
function OptimizedRoomWalls({ room }: { room: BossRoomDefinition }) {
|
||||
return (
|
||||
<DungeonAssetErrorBoundary fallback={<LegacyRoomWalls room={room} />}>
|
||||
<Suspense fallback={<RoomWallFallback room={room} />}>
|
||||
<DungeonKitArchitecture room={room} />
|
||||
</Suspense>
|
||||
</DungeonAssetErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
function RoomWalls({ room }: { room: BossRoomDefinition }) {
|
||||
return LEGACY_GAME_ASSETS_FORCED
|
||||
? <LegacyRoomWalls room={room} />
|
||||
: <OptimizedRoomWalls room={room} />;
|
||||
}
|
||||
|
||||
function RoomMarks({ room }: { room: BossRoomDefinition }) {
|
||||
const rays = useRef<THREE.InstancedMesh>(null);
|
||||
const pattern = ROOM_PATTERNS[room.floor];
|
||||
@@ -345,6 +423,8 @@ export function BossRoom() {
|
||||
);
|
||||
}
|
||||
|
||||
useGLTF.preload(KAYKIT_DUNGEON_PILLAR_URL, false, true);
|
||||
useGLTF.preload(KAYKIT_DUNGEON_WALL_URL, false, true);
|
||||
useGLTF.preload(KAYKIT_DUNGEON_TORCH_URL, false, true);
|
||||
if (LEGACY_GAME_ASSETS_FORCED) {
|
||||
useGLTF.preload(KAYKIT_DUNGEON_PILLAR_URL, false, true);
|
||||
useGLTF.preload(KAYKIT_DUNGEON_WALL_URL, false, true);
|
||||
useGLTF.preload(KAYKIT_DUNGEON_TORCH_URL, false, true);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user