Harden gameplay timing and map loading
This commit is contained in:
@@ -26,12 +26,21 @@ export function GuessMap({
|
||||
const root = useRef<HTMLDivElement>(null),
|
||||
markers = useRef<Leaflet.LayerGroup | null>(null),
|
||||
lib = useRef<typeof Leaflet | null>(null),
|
||||
[generation, setGeneration] = useState(0);
|
||||
[generation, setGeneration] = useState(0),
|
||||
[error, setError] = useState(""),
|
||||
[attempt, setAttempt] = useState(0);
|
||||
const callbacks = useRef({ onPin, onReady, onError, disabled });
|
||||
callbacks.current = { onPin, onReady, onError, disabled };
|
||||
useEffect(() => {
|
||||
let disposed = false;
|
||||
let cleanup = () => {};
|
||||
setError("");
|
||||
const fail = (message: string) => {
|
||||
if (disposed) return;
|
||||
setError(message);
|
||||
callbacks.current.onError?.(message);
|
||||
};
|
||||
let loadTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
import("leaflet")
|
||||
.then((L) => {
|
||||
if (disposed || !root.current) return;
|
||||
@@ -48,13 +57,31 @@ export function GuessMap({
|
||||
maxBounds: b.pad(0.15),
|
||||
attributionControl: false,
|
||||
});
|
||||
// Install cleanup immediately, including when later initialization fails.
|
||||
let observer: ResizeObserver | undefined;
|
||||
cleanup = () => {
|
||||
observer?.disconnect();
|
||||
markers.current = null;
|
||||
lib.current = null;
|
||||
m.remove();
|
||||
};
|
||||
const overlay = L.imageOverlay(image, b);
|
||||
overlay.on("load", () => callbacks.current.onReady?.());
|
||||
overlay.on("error", () =>
|
||||
callbacks.current.onError?.(
|
||||
"The surface map could not load. Reconnect to retry.",
|
||||
),
|
||||
loadTimer = setTimeout(
|
||||
() =>
|
||||
fail("The surface map took too long to load. Reconnect to retry."),
|
||||
30000,
|
||||
);
|
||||
overlay.on("load", () => {
|
||||
clearTimeout(loadTimer);
|
||||
if (!disposed) {
|
||||
setError("");
|
||||
callbacks.current.onReady?.();
|
||||
}
|
||||
});
|
||||
overlay.on("error", () => {
|
||||
clearTimeout(loadTimer);
|
||||
fail("The surface map could not load. Reconnect to retry.");
|
||||
});
|
||||
overlay.addTo(m);
|
||||
m.setMaxZoom(Math.max(3, m.getBoundsZoom(b) + 3));
|
||||
m.fitBounds(b, { padding: [0, 0], animate: false });
|
||||
@@ -74,12 +101,33 @@ export function GuessMap({
|
||||
)
|
||||
callbacks.current.onPin?.({ x, z });
|
||||
});
|
||||
const keydown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
event.key !== "Enter" ||
|
||||
event.target !== m.getContainer() ||
|
||||
callbacks.current.disabled
|
||||
)
|
||||
return;
|
||||
event.preventDefault();
|
||||
const center = m.getCenter();
|
||||
callbacks.current.onPin?.({
|
||||
x: Math.max(bounds.minX, Math.min(bounds.maxX, center.lng)),
|
||||
z: Math.max(bounds.minZ, Math.min(bounds.maxZ, -center.lat)),
|
||||
});
|
||||
};
|
||||
m.getContainer().addEventListener("keydown", keydown);
|
||||
const removeMap = cleanup;
|
||||
cleanup = () => {
|
||||
m.getContainer().removeEventListener("keydown", keydown);
|
||||
removeMap();
|
||||
};
|
||||
let fittedZoom = fitZoom;
|
||||
const observer = new ResizeObserver(() => {
|
||||
observer = new ResizeObserver(() => {
|
||||
if (!root.current?.clientWidth || !root.current.clientHeight) return;
|
||||
// Expanding the map should reveal the whole survey at its new size.
|
||||
// Preserve a player's chosen zoom and center while inspecting terrain.
|
||||
const wasFitted = Math.abs(m.getZoom() - fittedZoom) < 0.01;
|
||||
m.invalidateSize();
|
||||
m.invalidateSize({ pan: false });
|
||||
m.setMinZoom(-5);
|
||||
fittedZoom = m.getBoundsZoom(b);
|
||||
m.setMaxZoom(Math.max(3, fittedZoom + 3));
|
||||
@@ -88,20 +136,14 @@ export function GuessMap({
|
||||
m.setMinZoom(fittedZoom);
|
||||
});
|
||||
observer.observe(root.current);
|
||||
cleanup = () => {
|
||||
observer.disconnect();
|
||||
markers.current = null;
|
||||
m.remove();
|
||||
};
|
||||
})
|
||||
.catch(() =>
|
||||
callbacks.current.onError?.("The map viewer could not initialize."),
|
||||
);
|
||||
.catch(() => fail("The map viewer could not initialize."));
|
||||
return () => {
|
||||
disposed = true;
|
||||
clearTimeout(loadTimer);
|
||||
cleanup();
|
||||
};
|
||||
}, [bounds.minX, bounds.maxX, bounds.minZ, bounds.maxZ, image]);
|
||||
}, [bounds.minX, bounds.maxX, bounds.minZ, bounds.maxZ, image, attempt]);
|
||||
useEffect(() => {
|
||||
const L = lib.current,
|
||||
group = markers.current;
|
||||
@@ -133,8 +175,23 @@ export function GuessMap({
|
||||
ref={root}
|
||||
style={{ height: "100%", width: "100%" }}
|
||||
role="application"
|
||||
aria-label="Minecraft map. North is up. Click to place a guess."
|
||||
aria-label={
|
||||
disabled
|
||||
? "Minecraft map. North is up."
|
||||
: "Minecraft map. North is up. Click to place a guess, or use arrow keys to pan and Enter to pin the center."
|
||||
}
|
||||
/>
|
||||
{error && (
|
||||
<div className="map-load-error" role="alert">
|
||||
<p>{error}</p>
|
||||
<button
|
||||
className="button"
|
||||
onClick={() => setAttempt((value) => value + 1)}
|
||||
>
|
||||
Retry map
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user