Files
vim-sudoku/src_old/useGame/useCursor.ts
T
2025-10-19 17:59:12 +01:00

123 lines
3.8 KiB
TypeScript

import { useRef, useState, useCallback, useEffect } from "react";
import type { UseRenderer } from "./useSudokuRenderer";
export function useCursor(rendererManager: UseRenderer) {
const { containerRef } = rendererManager;
const [cursor, setCursor] = useState<{ r: number; c: number }>({ r: 0, c: 0 });
const animatingRef = useRef(false);
const getCellEl = useCallback((r: number, c: number): HTMLElement | null => {
const container = containerRef.current;
if (!container) return null;
const sudokuDiv = container.querySelector("#sudoku");
if (!sudokuDiv) return null;
const selector = `.sudoku-cell[data-r="${r}"][data-c="${c}"]`;
return sudokuDiv.querySelector(selector) as HTMLElement | null;
}, [containerRef]);
const flashInvalid = useCallback(() => {
const el = getCellEl(cursor.r, cursor.c);
if (!el) return;
el.classList.add("invalid-move");
setTimeout(() => el.classList.remove("invalid-move"), 180);
}, [cursor, getCellEl]);
const flashLocked = useCallback(() => {
const el = getCellEl(cursor.r, cursor.c);
if (!el) return;
el.classList.add("locked-cell");
setTimeout(() => el.classList.remove("locked-cell"), 300);
}, [cursor, getCellEl]);
const flashValid = useCallback(() => {
const el = getCellEl(cursor.r, cursor.c);
if (!el) return;
el.classList.add("valid-move");
setTimeout(() => el.classList.remove("valid-move"), 300);
}, [cursor, getCellEl]);
const highlightConflicts = useCallback((conflictIndices: number[]) => {
const container = containerRef.current;
if (!container) return;
const sudokuDiv = container.querySelector("#sudoku");
if (!sudokuDiv) return;
conflictIndices.forEach((idx) => {
const r = Math.floor(idx / 9);
const c = idx % 9;
const el = getCellEl(r, c);
if (el) {
el.classList.add("conflict");
}
});
setTimeout(() => {
conflictIndices.forEach((idx) => {
const r = Math.floor(idx / 9);
const c = idx % 9;
const el = getCellEl(r, c);
if (el) {
el.classList.remove("conflict");
}
});
}, 2000);
}, [containerRef, getCellEl]);
const moveCursor = useCallback(
(dr: number, dc: number, steps = 1) => {
if (animatingRef.current) return;
setCursor((prev) => {
const targetR = prev.r + (dr * steps);
const targetC = prev.c + (dc * steps);
// Check if the full counted move would go out of bounds
if (targetR < 0 || targetR > 8 || targetC < 0 || targetC > 8) {
// Flash invalid and don't move
setTimeout(() => flashInvalid(), 0);
return prev;
}
// Valid move - apply it
return { r: targetR, c: targetC };
});
},
[flashInvalid]
);
useEffect(() => {
const onLockedCell = () => flashLocked();
const onValidMove = () => flashValid();
const onInvalidMove = (e: Event) => {
const detail = (e as CustomEvent).detail;
if (detail?.conflicts) {
highlightConflicts(detail.conflicts);
}
flashInvalid();
};
window.addEventListener("sudoku-locked-cell", onLockedCell);
window.addEventListener("sudoku-valid-move", onValidMove);
window.addEventListener("sudoku-invalid-move", onInvalidMove as EventListener);
return () => {
window.removeEventListener("sudoku-locked-cell", onLockedCell);
window.removeEventListener("sudoku-valid-move", onValidMove);
window.removeEventListener("sudoku-invalid-move", onInvalidMove as EventListener);
};
}, [flashLocked, flashValid, flashInvalid, highlightConflicts]);
return {
cursor,
setCursor,
moveCursor,
getCellEl,
flashInvalid,
flashValid,
highlightConflicts,
};
}
export type UseCursorType = ReturnType<typeof useCursor>;