feat: implement Phase 40.1 - basic cursor with hjkl movement

Add cursor navigation with hjkl keys (vim-style):

New Module Structure:
- src/useCursor/types.ts: Coord and CursorManager types
- src/useCursor/useCursor.ts: Cursor movement logic
- src/useCursor/useCursorRender.ts: Visual cursor highlight
- src/useCursor/index.ts: Public exports

Cursor Implementation:
- Position tracking with useRef (row/col from 0-8)
- Basic movement: moveLeft, moveRight, moveUp, moveDown
- Bounds checking (prevents moving outside 9x9 grid)
- Keyboard listener for h/j/k/l keys
- Only active when gameStatus === 'started'

Cursor Rendering:
- useCursorRender hook tracks cursor position
- Adds 'cursor' CSS class to current cell
- Removes from previous cell on movement
- Blue highlight with border (rgba(59, 130, 246, 0.3))

Integration:
- src/useGame.ts: Integrate useCursor and useCursorRender
- src/types.ts: Export CursorManager and Coord types
- src/useBoard/types.ts: NEW - BoardManager type
- src/game.css: Add cursor highlight styles

Game Flow:
- Press Space → Game starts → Cursor appears at (0,0)
- Press h/j/k/l → Cursor moves (bounded to grid)
- Visual feedback with blue highlight
- Press Q → Return to waiting state

Phase 40.1 complete. Next: Add number prefixes (5j) and other vim motions.
This commit is contained in:
2025-10-22 18:49:24 +01:00
parent 4e14d54122
commit be41ccee14
8 changed files with 125 additions and 2 deletions
+67
View File
@@ -0,0 +1,67 @@
import { useRef, useEffect } from 'react';
import type { Coord, CursorManager } from './types';
import type { GameStatusManager } from '../useGameStatus/types';
export function useCursor(gameStatusManager: GameStatusManager) {
const position = useRef<Coord>({ row: 0, col: 0 });
const { gameStatus } = gameStatusManager;
function move(dRows: number, dCols: number, count: number = 1): void {
const currentPos = position.current;
const newRow = currentPos.row + (dRows * count);
const newCol = currentPos.col + (dCols * count);
if (newRow < 0 || newRow > 8 || newCol < 0 || newCol > 8) {
return;
}
position.current = { row: newRow, col: newCol };
}
function moveLeft(count = 1): void {
move(0, -1, count);
}
function moveRight(count = 1): void {
move(0, 1, count);
}
function moveUp(count = 1): void {
move(-1, 0, count);
}
function moveDown(count = 1): void {
move(1, 0, count);
}
useEffect(() => {
if (gameStatus !== 'started') return;
function handleKeyDown(e: KeyboardEvent) {
const key = e.key;
if (key === 'h') {
moveLeft();
} else if (key === 'j') {
moveDown();
} else if (key === 'k') {
moveUp();
} else if (key === 'l') {
moveRight();
}
}
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [gameStatus]);
const cursor: CursorManager = {
position: () => position.current,
moveLeft,
moveRight,
moveUp,
moveDown,
};
return cursor;
}