player stopped moving
This commit is contained in:
@@ -89,6 +89,7 @@ import { MazeRenderer } from './MazeRenderer';
|
|||||||
- **Destructuring**: Use object destructuring for parameters and returns
|
- **Destructuring**: Use object destructuring for parameters and returns
|
||||||
- **Arrow functions**: Prefer arrow functions for consistency
|
- **Arrow functions**: Prefer arrow functions for consistency
|
||||||
- **No useCallback/useMemo**: React 19 optimization makes these hooks unnecessary
|
- **No useCallback/useMemo**: React 19 optimization makes these hooks unnecessary
|
||||||
|
- **useEffect dependencies**: Only use values, never functions as dependencies
|
||||||
|
|
||||||
### Error Handling
|
### Error Handling
|
||||||
- **Early returns**: Guard clauses at function start
|
- **Early returns**: Guard clauses at function start
|
||||||
@@ -178,6 +179,7 @@ export function useGame(options: GameOptions, platformHook?: unknown): GameManag
|
|||||||
- Write overly complex implementations
|
- Write overly complex implementations
|
||||||
- Add unnecessary features
|
- Add unnecessary features
|
||||||
- Use `useCallback` or `useMemo` anywhere in the codebase
|
- Use `useCallback` or `useMemo` anywhere in the codebase
|
||||||
|
- Use functions as useEffect dependencies
|
||||||
|
|
||||||
### Always DO
|
### Always DO
|
||||||
- Follow existing code patterns exactly
|
- Follow existing code patterns exactly
|
||||||
|
|||||||
+125
-128
@@ -1,4 +1,4 @@
|
|||||||
import { useRef, useState, useCallback, useEffect } from "react";
|
import { useRef, useState, useEffect } from "react";
|
||||||
import type { BoardManager, GameStatus, Coord } from "../types";
|
import type { BoardManager, GameStatus, Coord } from "../types";
|
||||||
import { useHeroRender } from "./useHeroRender";
|
import { useHeroRender } from "./useHeroRender";
|
||||||
|
|
||||||
@@ -8,35 +8,35 @@ export function useHero(board: BoardManager, gameStatus: GameStatus) {
|
|||||||
|
|
||||||
const animatingRef = useRef(false);
|
const animatingRef = useRef(false);
|
||||||
|
|
||||||
const getCellEl = useCallback((r: number, c: number): HTMLElement | null => {
|
function getCellEl(r: number, c: number): HTMLElement | null {
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
if (!container) return null;
|
if (!container) return null;
|
||||||
const mazeDiv = container.querySelector("#maze");
|
const mazeDiv = container.querySelector("#maze");
|
||||||
if (!mazeDiv) return null;
|
if (!mazeDiv) return null;
|
||||||
const selector = `.maze-row > div[data-r="${r}"][data-c="${c}"]`;
|
const selector = `.maze-row > div[data-r="${r}"][data-c="${c}"]`;
|
||||||
return mazeDiv.querySelector(selector) as HTMLElement | null;
|
return mazeDiv.querySelector(selector) as HTMLElement | null;
|
||||||
}, [containerRef]);
|
}
|
||||||
|
|
||||||
const leaveTrail = useCallback((r: number, c: number) => {
|
function leaveTrail(r: number, c: number) {
|
||||||
const el = getCellEl(r, c);
|
const el = getCellEl(r, c);
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
el.classList.add("trail");
|
el.classList.add("trail");
|
||||||
setTimeout(() => el.classList.remove("trail"), 220);
|
setTimeout(() => el.classList.remove("trail"), 220);
|
||||||
}, [getCellEl]);
|
}
|
||||||
|
|
||||||
const flashInvalid = useCallback(() => {
|
function flashInvalid() {
|
||||||
if (!heroPos) return;
|
if (!heroPos) return;
|
||||||
const el = getCellEl(heroPos.row, heroPos.col);
|
const el = getCellEl(heroPos.row, heroPos.col);
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
el.classList.add("invalid-move");
|
el.classList.add("invalid-move");
|
||||||
setTimeout(() => el.classList.remove("invalid-move"), 180);
|
setTimeout(() => el.classList.remove("invalid-move"), 180);
|
||||||
}, [heroPos, getCellEl]);
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onInvalid = () => flashInvalid();
|
const onInvalid = () => flashInvalid();
|
||||||
window.addEventListener("maze-invalid", onInvalid);
|
window.addEventListener("maze-invalid", onInvalid);
|
||||||
return () => window.removeEventListener("maze-invalid", onInvalid);
|
return () => window.removeEventListener("maze-invalid", onInvalid);
|
||||||
}, [flashInvalid]);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (gameStatus === 'started' && !heroPos) {
|
if (gameStatus === 'started' && !heroPos) {
|
||||||
@@ -61,142 +61,139 @@ export function useHero(board: BoardManager, gameStatus: GameStatus) {
|
|||||||
}
|
}
|
||||||
}, [gameStatus, containerRef]);
|
}, [gameStatus, containerRef]);
|
||||||
|
|
||||||
const moveHero = useCallback(
|
function moveHero(
|
||||||
(
|
dr: number,
|
||||||
dr: number,
|
dc: number,
|
||||||
dc: number,
|
steps: number | undefined,
|
||||||
steps: number | undefined,
|
gameStatus: GameStatus,
|
||||||
gameStatus: GameStatus,
|
setGameStatus: (s: GameStatus) => void
|
||||||
setGameStatus: (s: GameStatus) => void
|
) {
|
||||||
) => {
|
const m = mazeRef.current;
|
||||||
const m = mazeRef.current;
|
if (!heroPos || !m.length) return;
|
||||||
if (!heroPos || !m.length) return;
|
if (gameStatus !== "started" && gameStatus !== "hasKey") return;
|
||||||
if (gameStatus !== "started" && gameStatus !== "hasKey") return;
|
if (animatingRef.current) return;
|
||||||
if (animatingRef.current) return;
|
|
||||||
|
|
||||||
const n = Math.max(1, steps ?? 1);
|
const n = Math.max(1, steps ?? 1);
|
||||||
|
|
||||||
const r0 = heroPos.row;
|
const r0 = heroPos.row;
|
||||||
const c0 = heroPos.col;
|
const c0 = heroPos.col;
|
||||||
let r = r0;
|
let r = r0;
|
||||||
let c = c0;
|
let c = c0;
|
||||||
let valid = true;
|
let valid = true;
|
||||||
|
|
||||||
for (let i = 1; i <= n; i++) {
|
for (let i = 1; i <= n; i++) {
|
||||||
r += dr;
|
r += dr;
|
||||||
c += dc;
|
c += dc;
|
||||||
if (!m[r] || !m[r][c] || m[r][c].includes("wall")) {
|
if (!m[r] || !m[r][c] || m[r][c].includes("wall")) {
|
||||||
valid = false;
|
valid = false;
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
if (m[r][c].includes("exit") && gameStatus !== "hasKey") {
|
|
||||||
valid = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!valid) {
|
if (m[r][c].includes("exit") && gameStatus !== "hasKey") {
|
||||||
flashInvalid();
|
valid = false;
|
||||||
return;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (!valid) {
|
||||||
|
flashInvalid();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (n > 1) {
|
if (n > 1) {
|
||||||
animatingRef.current = true;
|
animatingRef.current = true;
|
||||||
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
let rNow = r0;
|
let rNow = r0;
|
||||||
let cNow = c0;
|
let cNow = c0;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
let rafId: number | null = null;
|
let rafId: number | null = null;
|
||||||
|
|
||||||
const TOTAL_MS = 64;
|
const TOTAL_MS = 64;
|
||||||
const framesTarget = Math.max(1, Math.round(TOTAL_MS / 16));
|
const framesTarget = Math.max(1, Math.round(TOTAL_MS / 16));
|
||||||
const stepsPerFrame = Math.max(1, Math.ceil(n / framesTarget));
|
const stepsPerFrame = Math.max(1, Math.ceil(n / framesTarget));
|
||||||
|
|
||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
if (rafId !== null) cancelAnimationFrame(rafId);
|
if (rafId !== null) cancelAnimationFrame(rafId);
|
||||||
|
window.removeEventListener("keydown", onEsc);
|
||||||
|
animatingRef.current = false;
|
||||||
|
};
|
||||||
|
const onEsc = (ev: KeyboardEvent) => {
|
||||||
|
if (ev.key === "Escape") cancel();
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", onEsc);
|
||||||
|
|
||||||
|
const tick = () => {
|
||||||
|
if (cancelled) return;
|
||||||
|
|
||||||
|
let processed = 0;
|
||||||
|
while (processed < stepsPerFrame && i < n) {
|
||||||
|
i += 1;
|
||||||
|
rNow += dr;
|
||||||
|
cNow += dc;
|
||||||
|
leaveTrail(rNow, cNow);
|
||||||
|
processed += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
setHeroPos({ row: rNow, col: cNow });
|
||||||
|
window.dispatchEvent(new CustomEvent("maze-logic-tick", { detail: { t: performance.now() } }));
|
||||||
|
|
||||||
|
if (i < n) {
|
||||||
|
rafId = requestAnimationFrame(tick);
|
||||||
|
} else {
|
||||||
|
if (m[rNow][cNow].includes("key")) {
|
||||||
|
setGameStatus("hasKey");
|
||||||
|
m[rNow][cNow] = [];
|
||||||
|
const container = containerRef.current;
|
||||||
|
const mazeDiv = container?.querySelector("#maze");
|
||||||
|
const keyCell = mazeDiv?.querySelector<HTMLElement>(
|
||||||
|
`.maze-row > div[data-r="${rNow}"][data-c="${cNow}"]`
|
||||||
|
);
|
||||||
|
keyCell?.classList.remove("key");
|
||||||
|
}
|
||||||
|
if (m[rNow][cNow].includes("exit")) {
|
||||||
|
setGameStatus("game-won");
|
||||||
|
}
|
||||||
window.removeEventListener("keydown", onEsc);
|
window.removeEventListener("keydown", onEsc);
|
||||||
animatingRef.current = false;
|
animatingRef.current = false;
|
||||||
};
|
}
|
||||||
const onEsc = (ev: KeyboardEvent) => {
|
};
|
||||||
if (ev.key === "Escape") cancel();
|
|
||||||
};
|
|
||||||
window.addEventListener("keydown", onEsc);
|
|
||||||
|
|
||||||
const tick = () => {
|
rafId = requestAnimationFrame(tick);
|
||||||
if (cancelled) return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let processed = 0;
|
const newR = r0 + dr;
|
||||||
while (processed < stepsPerFrame && i < n) {
|
const newC = c0 + dc;
|
||||||
i += 1;
|
|
||||||
rNow += dr;
|
|
||||||
cNow += dc;
|
|
||||||
leaveTrail(rNow, cNow);
|
|
||||||
processed += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
setHeroPos({ row: rNow, col: cNow });
|
if (!m[newR] || !m[newR][newC]) {
|
||||||
window.dispatchEvent(new CustomEvent("maze-logic-tick", { detail: { t: performance.now() } }));
|
flashInvalid();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (m[newR][newC].includes("wall")) {
|
||||||
|
flashInvalid();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (m[newR][newC].includes("exit") && gameStatus !== "hasKey") {
|
||||||
|
flashInvalid();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (i < n) {
|
if (m[newR][newC].includes("key")) {
|
||||||
rafId = requestAnimationFrame(tick);
|
setGameStatus("hasKey");
|
||||||
} else {
|
m[newR][newC] = [];
|
||||||
if (m[rNow][cNow].includes("key")) {
|
const container = containerRef.current;
|
||||||
setGameStatus("hasKey");
|
const mazeDiv = container?.querySelector("#maze");
|
||||||
m[rNow][cNow] = [];
|
const cell = mazeDiv?.querySelector<HTMLElement>(
|
||||||
const container = containerRef.current;
|
`.maze-row > div[data-r="${newR}"][data-c="${newC}"]`
|
||||||
const mazeDiv = container?.querySelector("#maze");
|
);
|
||||||
const keyCell = mazeDiv?.querySelector<HTMLElement>(
|
cell?.classList.remove("key");
|
||||||
`.maze-row > div[data-r="${rNow}"][data-c="${cNow}"]`
|
}
|
||||||
);
|
if (m[newR][newC].includes("exit")) {
|
||||||
keyCell?.classList.remove("key");
|
setGameStatus("game-won");
|
||||||
}
|
}
|
||||||
if (m[rNow][cNow].includes("exit")) {
|
|
||||||
setGameStatus("game-won");
|
|
||||||
}
|
|
||||||
window.removeEventListener("keydown", onEsc);
|
|
||||||
animatingRef.current = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
rafId = requestAnimationFrame(tick);
|
setHeroPos({ row: newR, col: newC });
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const newR = r0 + dr;
|
|
||||||
const newC = c0 + dc;
|
|
||||||
|
|
||||||
if (!m[newR] || !m[newR][newC]) {
|
|
||||||
flashInvalid();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (m[newR][newC].includes("wall")) {
|
|
||||||
flashInvalid();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (m[newR][newC].includes("exit") && gameStatus !== "hasKey") {
|
|
||||||
flashInvalid();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m[newR][newC].includes("key")) {
|
|
||||||
setGameStatus("hasKey");
|
|
||||||
m[newR][newC] = [];
|
|
||||||
const container = containerRef.current;
|
|
||||||
const mazeDiv = container?.querySelector("#maze");
|
|
||||||
const cell = mazeDiv?.querySelector<HTMLElement>(
|
|
||||||
`.maze-row > div[data-r="${newR}"][data-c="${newC}"]`
|
|
||||||
);
|
|
||||||
cell?.classList.remove("key");
|
|
||||||
}
|
|
||||||
if (m[newR][newC].includes("exit")) {
|
|
||||||
setGameStatus("game-won");
|
|
||||||
}
|
|
||||||
|
|
||||||
setHeroPos({ row: newR, col: newC });
|
|
||||||
},
|
|
||||||
[heroPos, mazeRef, containerRef, flashInvalid, leaveTrail]
|
|
||||||
);
|
|
||||||
|
|
||||||
const hero = {
|
const hero = {
|
||||||
heroPos,
|
heroPos,
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export const useKeyBindings = ({ cursor, gameStatus, setGameStatus }: UseKeyBind
|
|||||||
|
|
||||||
window.addEventListener("keydown", handler);
|
window.addEventListener("keydown", handler);
|
||||||
return () => window.removeEventListener("keydown", handler);
|
return () => window.removeEventListener("keydown", handler);
|
||||||
}, [cursor, gameStatus]);
|
}, [gameStatus]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
keyLog,
|
keyLog,
|
||||||
|
|||||||
Reference in New Issue
Block a user