Harden gameplay timing and map loading

This commit is contained in:
2026-10-05 15:15:35 +05:00
parent 8b31891776
commit dcc3779262
36 changed files with 1050 additions and 205 deletions

View File

@@ -1,29 +1,8 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { useEffect, useRef } from "react";
import { Plus, Minus, RotateCcw } from "lucide-react";
import * as THREE from "three";
const textureCache = new Map<string, Promise<THREE.Texture>>();
export function preloadFaces(urls: string[]) {
return Promise.all(
urls.map((url) => {
if (!textureCache.has(url))
textureCache.set(
url,
new THREE.TextureLoader()
.loadAsync(url)
.then((t) => {
t.colorSpace = THREE.SRGBColorSpace;
return t;
})
.catch((e) => {
textureCache.delete(url);
throw e;
}),
);
return textureCache.get(url)!;
}),
);
}
import { loadPanoramaTextures } from "@/lib/panorama-textures";
// Explicit projection matches the six positive-FOV camera images emitted by
// both exporters; it avoids CubeTexture's renderer-dependent orientation.
const fragment = `uniform sampler2D px;uniform sampler2D nx;uniform sampler2D py;uniform sampler2D ny;uniform sampler2D pz;uniform sampler2D nz;varying vec3 vDirection;
@@ -43,8 +22,7 @@ export function Panorama({
onError: (message: string) => void;
}) {
const mount = useRef<HTMLDivElement>(null),
view = useRef({ yaw: 0, pitch: 0, fov: 80 }),
[fov, setFov] = useState(80);
view = useRef({ yaw: 0, pitch: 0, fov: 80 });
const callbacks = useRef({ onReady, onError });
callbacks.current = { onReady, onError };
useEffect(() => {
@@ -53,7 +31,8 @@ export function Panorama({
frame = 0,
observer: ResizeObserver | undefined;
let material: THREE.ShaderMaterial | undefined,
geometry: THREE.SphereGeometry | undefined;
geometry: THREE.SphereGeometry | undefined,
ownedTextures: THREE.Texture[] = [];
const element = mount.current!;
view.current = { yaw: 0, pitch: 0, fov: 80 };
const down = (e: PointerEvent) => {
@@ -81,6 +60,13 @@ export function Panorama({
Math.min(100, view.current.fov + e.deltaY * 0.04),
);
};
const contextLost = (event: Event) => {
event.preventDefault();
cancelAnimationFrame(frame);
callbacks.current.onError(
"The panorama graphics context was lost. Reconnect to restore the view.",
);
};
let drag: { x: number; y: number } | null = null;
if (urls.length !== 6) {
callbacks.current.onError(
@@ -88,13 +74,18 @@ export function Panorama({
);
return;
}
preloadFaces(urls)
loadPanoramaTextures(urls)
.then((textures) => {
if (disposed) return;
if (disposed) {
textures.forEach((texture) => texture.dispose());
return;
}
ownedTextures = textures;
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.outputColorSpace = THREE.SRGBColorSpace;
element.appendChild(renderer.domElement);
renderer.domElement.addEventListener("webglcontextlost", contextLost);
const scene = new THREE.Scene(),
camera = new THREE.PerspectiveCamera(80, 1, 0.1, 10);
geometry = new THREE.SphereGeometry(5, 32, 16);
@@ -112,9 +103,11 @@ export function Panorama({
});
scene.add(new THREE.Mesh(geometry, material));
const resize = () => {
camera.aspect = element.clientWidth / element.clientHeight;
const width = Math.max(1, element.clientWidth),
height = Math.max(1, element.clientHeight);
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer!.setSize(element.clientWidth, element.clientHeight);
renderer!.setSize(width, height);
};
observer = new ResizeObserver(resize);
observer.observe(element);
@@ -157,13 +150,12 @@ export function Panorama({
element.removeEventListener("wheel", wheel);
geometry?.dispose();
material?.dispose();
ownedTextures.forEach((texture) => texture.dispose());
renderer?.domElement.removeEventListener("webglcontextlost", contextLost);
renderer?.dispose();
renderer?.domElement.remove();
};
}, [urls, zoom]);
useEffect(() => {
view.current.fov = fov;
}, [fov]);
return (
<>
<div
@@ -177,13 +169,17 @@ export function Panorama({
<>
<button
aria-label="Zoom in"
onClick={() => setFov(Math.max(35, view.current.fov - 10))}
onClick={() => {
view.current.fov = Math.max(35, view.current.fov - 10);
}}
>
<Plus size={17} />
</button>
<button
aria-label="Zoom out"
onClick={() => setFov(Math.min(100, view.current.fov + 10))}
onClick={() => {
view.current.fov = Math.min(100, view.current.fov + 10);
}}
>
<Minus size={17} />
</button>
@@ -193,7 +189,6 @@ export function Panorama({
aria-label="Reset view"
onClick={() => {
view.current = { yaw: 0, pitch: 0, fov: 80 };
setFov(80);
}}
>
<RotateCcw size={17} />