diff --git a/UNIFIED_API.md b/UNIFIED_API.md index 3247fed..e25b13e 100644 --- a/UNIFIED_API.md +++ b/UNIFIED_API.md @@ -321,6 +321,7 @@ The unified API is designed to accommodate: - ✅ **Specialized hooks** - Complete (useMazeNavigation, useVimMotions, useAnimation) - ✅ **Single source of truth** - Complete (hero position as authoritative) - ✅ **React-first architecture** - Complete (state-driven rendering) +- ✅ **Hero movement implementation** - Complete (hjkl, anchors, repeat with counts) - ✅ **Platform integration** - Ready - ⏳ **Advanced features** - Pending (scoring, animations, etc.) diff --git a/src/useBoard/useMazeNavigation.ts b/src/useBoard/useMazeNavigation.ts index 660340b..816dd51 100644 --- a/src/useBoard/useMazeNavigation.ts +++ b/src/useBoard/useMazeNavigation.ts @@ -26,43 +26,35 @@ export function useMazeNavigation(maze: MazeCell[][]): MazeNavigator { if (direction === 'left' || direction === 'right') { const step = direction === 'left' ? -1 : 1; let cc = c; + let lastValidPos = { row: r, col: c }; while (true) { const next = cc + step; - if (next < 0 || next >= colsCount) return null; - if (maze[r][next].includes('wall')) { - const wallIdx = next; - const expectedWallIdx = direction === 'left' ? 0 : colsCount - 1; - const expectedCellIdx = direction === 'left' ? 1 : colsCount - 2; - - if (wallIdx !== expectedWallIdx) return null; - if (cc !== expectedCellIdx) return null; - if (cc === c) return null; - - return { row: r, col: cc }; + if (next < 0 || next >= colsCount) break; + if (maze[r][next].includes('wall')) break; + if (!maze[r][next].includes('wall')) { + lastValidPos = { row: r, col: next }; } cc = next; } + + return lastValidPos.col !== c ? lastValidPos : null; } else { const step = direction === 'top' ? -1 : 1; let rr = r; + let lastValidPos = { row: r, col: c }; while (true) { const next = rr + step; - if (next < 0 || next >= rowsCount) return null; - if (maze[next][c].includes('wall')) { - const wallIdx = next; - const expectedWallIdx = direction === 'top' ? 0 : rowsCount - 1; - const expectedCellIdx = direction === 'top' ? 1 : rowsCount - 2; - - if (wallIdx !== expectedWallIdx) return null; - if (rr !== expectedCellIdx) return null; - if (rr === r) return null; - - return { row: rr, col: c }; + if (next < 0 || next >= rowsCount) break; + if (maze[next][c].includes('wall')) break; + if (!maze[next][c].includes('wall')) { + lastValidPos = { row: next, col: c }; } rr = next; } + + return lastValidPos.row !== r ? lastValidPos : null; } } diff --git a/src/useCursor/useCursor.ts b/src/useCursor/useCursor.ts index 822ff08..751587d 100644 --- a/src/useCursor/useCursor.ts +++ b/src/useCursor/useCursor.ts @@ -37,6 +37,10 @@ export function useCursor(board: BoardManager, gameStatusManager: GameStatusMana } position.current = targetCoord; + if (hero && hero.heroPos) { + hero.moveHero(dRows, dCols, steps, gameStatusManager.gameStatus, gameStatusManager.setGameStatus); + vimMotions.setLastMotion({ dr: dRows, dc: dCols, steps }); + } } function moveLeft(count = 1): void { @@ -73,7 +77,13 @@ export function useCursor(board: BoardManager, gameStatusManager: GameStatusMana anchorTarget.col < currentPos.col ? -1 : 0; const steps = Math.abs(anchorTarget.row - currentPos.row) + Math.abs(anchorTarget.col - currentPos.col); - move(dc, dr, steps); + position.current = anchorTarget; + if (hero && hero.heroPos) { + hero.moveTo(anchorTarget); + if (steps > 0) { + vimMotions.setLastMotion({ dr, dc, steps }); + } + } } function moveToStart(): void { @@ -94,9 +104,20 @@ export function useCursor(board: BoardManager, gameStatusManager: GameStatusMana function repeatLastMotion(): void { const motion = vimMotions.repeatLastMotion(); - if (!motion) return; + if (!motion || !hero || !hero.heroPos) return; - move(motion.dc, motion.dr, motion.steps); + const currentPos = position.current; + const targetCoord = { + row: currentPos.row + motion.dr * motion.steps, + col: currentPos.col + motion.dc * motion.steps + }; + + if (navigator.validatePath(currentPos, { row: motion.dr, col: motion.dc }, motion.steps)) { + position.current = targetCoord; + hero.moveHero(motion.dr, motion.dc, motion.steps, gameStatusManager.gameStatus, gameStatusManager.setGameStatus); + } else { + window.dispatchEvent(new Event("maze-invalid")); + } } function resetCount(): void { diff --git a/src/useCursor/useGameKeys.ts b/src/useCursor/useGameKeys.ts index 2b8aca2..09f9dc0 100644 --- a/src/useCursor/useGameKeys.ts +++ b/src/useCursor/useGameKeys.ts @@ -14,7 +14,7 @@ const RELEVANT_KEYS = new Set([ export const useGameKeys = (props: UseGameKeysParams) => { const { cursor, gameStatusManager } = props; - const { gameStatus, setGameStatus } = gameStatusManager; + const { gameStatus } = gameStatusManager; const [keyLog, setKeyLog] = useState([]); const logRef = useRef([]); const { hero } = cursor; @@ -96,16 +96,15 @@ export const useGameKeys = (props: UseGameKeysParams) => { const count = Math.max(1, parseInt(cursor.getCount() || "1", 10)); cursor.resetCount(); - if (!hero) return; switch (ev.key) { case "h": - case "H": hero.moveHero(0, -1, count, gameStatus, setGameStatus); break; + case "H": cursor.moveLeft(count); break; case "j": - case "J": hero.moveHero(1, 0, count, gameStatus, setGameStatus); break; + case "J": cursor.moveDown(count); break; case "k": - case "K": hero.moveHero(-1, 0, count, gameStatus, setGameStatus); break; + case "K": cursor.moveUp(count); break; case "l": - case "L": hero.moveHero(0, 1, count, gameStatus, setGameStatus); break; + case "L": cursor.moveRight(count); break; default: return; } };