'use client' import { useEffect, useRef, useCallback } from 'react' import { usePlayerStore } from '@/stores/playerStore' import { useGameStore } from '@/stores/gameStore' import { useUIStore } from '@/stores/uiStore' import { useWorldStore } from '@/stores/worldStore' import { WALK_SPEED, SPRINT_SPEED, FLY_SPEED, SNEAK_SPEED, GRAVITY, JUMP_VELOCITY, PLAYER_HEIGHT, PLAYER_EYE_HEIGHT, SEA_LEVEL, MIN_BUILD_HEIGHT, REACH_DISTANCE, BLOCK_AIR, BLOCK_DEFS, CREATIVE_BLOCKS, } from '@/engine/constants' interface KeyState { forward: boolean backward: boolean left: boolean right: boolean jump: boolean sneak: boolean sprint: boolean } export function usePlayerControls() { const keys = useRef({ forward: false, backward: false, left: false, right: false, jump: false, sneak: false, sprint: false, }) const isPointerLocked = useRef(false) // Block interaction via raycasting (DDA algorithm) const raycastBlock = useCallback((maxDist: number = REACH_DISTANCE) => { const { player } = usePlayerStore.getState() if (!player) return null const [px, py, pz] = player.position const [yaw, pitch] = player.rotation const ox = px const oy = py + PLAYER_EYE_HEIGHT const oz = pz const dirX = -Math.sin(yaw) * Math.cos(pitch) const dirY = -Math.sin(pitch) const dirZ = -Math.cos(yaw) * Math.cos(pitch) let x = Math.floor(ox) let y = Math.floor(oy) let z = Math.floor(oz) const stepX = dirX >= 0 ? 1 : -1 const stepY = dirY >= 0 ? 1 : -1 const stepZ = dirZ >= 0 ? 1 : -1 const eps = 1e-10 const tDeltaX = Math.abs(1 / (dirX || eps)) const tDeltaY = Math.abs(1 / (dirY || eps)) const tDeltaZ = Math.abs(1 / (dirZ || eps)) let tMaxX = dirX >= 0 ? (x + 1 - ox) / (dirX || eps) : (ox - x) / (-dirX || eps) let tMaxY = dirY >= 0 ? (y + 1 - oy) / (dirY || eps) : (oy - y) / (-dirY || eps) let tMaxZ = dirZ >= 0 ? (z + 1 - oz) / (dirZ || eps) : (oz - z) / (-dirZ || eps) let prevX = x, prevY = y, prevZ = z const { getBlock } = useWorldStore.getState() for (let i = 0; i < maxDist * 3; i++) { const block = getBlock(x, y, z) if (block !== BLOCK_AIR) { const def = BLOCK_DEFS[block] if (def && (def.solid || def.transparent)) { return { blockPos: [x, y, z] as [number, number, number], placePos: [prevX, prevY, prevZ] as [number, number, number], blockId: block, } } } prevX = x; prevY = y; prevZ = z if (tMaxX < tMaxY) { if (tMaxX < tMaxZ) { if (tMaxX > maxDist) return null x += stepX; tMaxX += tDeltaX } else { if (tMaxZ > maxDist) return null z += stepZ; tMaxZ += tDeltaZ } } else { if (tMaxY < tMaxZ) { if (tMaxY > maxDist) return null y += stepY; tMaxY += tDeltaY } else { if (tMaxZ > maxDist) return null z += stepZ; tMaxZ += tDeltaZ } } } return null }, []) const handleKeyDown = useCallback((e: KeyboardEvent) => { const { setShowPause } = useUIStore.getState() const { setPaused } = useGameStore.getState() switch (e.code) { case 'KeyW': keys.current.forward = true; break case 'KeyS': keys.current.backward = true; break case 'KeyA': keys.current.left = true; break case 'KeyD': keys.current.right = true; break case 'Space': keys.current.jump = true; e.preventDefault(); break case 'ShiftLeft': case 'ShiftRight': keys.current.sneak = true; break case 'ControlLeft': keys.current.sprint = true; e.preventDefault(); break case 'Escape': if (isPointerLocked.current) { document.exitPointerLock() } else { setShowPause(true); setPaused(true) } break case 'KeyF': usePlayerStore.getState().setFlying(!usePlayerStore.getState().player?.flying) break case 'KeyE': useUIStore.getState().toggleInventory() break case 'F3': e.preventDefault() useGameStore.getState().setDebug(!useGameStore.getState().debug) break case 'KeyG': const current = usePlayerStore.getState().player?.gamemode || 'creative' usePlayerStore.getState().setGamemode(current === 'creative' ? 'survival' : 'creative') break case 'KeyT': const w = useWorldStore.getState().world if (w) useWorldStore.getState().initWorld(w.seed) // Reset time by re-init break case 'Digit1': case 'Digit2': case 'Digit3': case 'Digit4': case 'Digit5': case 'Digit6': case 'Digit7': case 'Digit8': case 'Digit9': useUIStore.getState().setHotbarSlot(parseInt(e.code.slice(5)) - 1) break } }, []) const handleKeyUp = useCallback((e: KeyboardEvent) => { switch (e.code) { case 'KeyW': keys.current.forward = false; break case 'KeyS': keys.current.backward = false; break case 'KeyA': keys.current.left = false; break case 'KeyD': keys.current.right = false; break case 'Space': keys.current.jump = false; break case 'ShiftLeft': case 'ShiftRight': keys.current.sneak = false; break case 'ControlLeft': keys.current.sprint = false; break } }, []) const handleMouseMove = useCallback((e: MouseEvent) => { if (!isPointerLocked.current) return const { player, updateRotation } = usePlayerStore.getState() if (!player) return const sensitivity = 0.002 const yaw = player.rotation[0] - e.movementX * sensitivity const pitch = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, player.rotation[1] - e.movementY * sensitivity)) updateRotation([yaw, pitch]) }, []) const handlePointerLockChange = useCallback(() => { isPointerLocked.current = document.pointerLockElement !== null }, []) const handleMouseDown = useCallback((e: MouseEvent) => { if (!isPointerLocked.current) return e.preventDefault() const hit = raycastBlock() if (!hit) return const { setBlock } = useWorldStore.getState() const { player } = usePlayerStore.getState() if (e.button === 0) { // Left click = break block setBlock(hit.blockPos[0], hit.blockPos[1], hit.blockPos[2], BLOCK_AIR) } else if (e.button === 2 && player) { // Right click = place block const slot = useUIStore.getState().hotbarSlot const item = player.inventory.hotbar[slot] if (item && BLOCK_DEFS[item.id]) { setBlock(hit.placePos[0], hit.placePos[1], hit.placePos[2], item.id) } } }, [raycastBlock]) const handleContextMenu = useCallback((e: Event) => e.preventDefault(), []) const handleWheel = useCallback((e: WheelEvent) => { const { hotbarSlot, setHotbarSlot } = useUIStore.getState() if (e.deltaY > 0) { setHotbarSlot((hotbarSlot + 1) % 9) } else { setHotbarSlot((hotbarSlot + 8) % 9) } }, []) const requestPointerLock = useCallback(() => { document.body.requestPointerLock() useUIStore.getState().setShowPause(false) useGameStore.getState().setPaused(false) }, []) useEffect(() => { document.addEventListener('keydown', handleKeyDown) document.addEventListener('keyup', handleKeyUp) document.addEventListener('mousemove', handleMouseMove) document.addEventListener('pointerlockchange', handlePointerLockChange) document.addEventListener('mousedown', handleMouseDown) document.addEventListener('contextmenu', handleContextMenu) document.addEventListener('wheel', handleWheel) return () => { document.removeEventListener('keydown', handleKeyDown) document.removeEventListener('keyup', handleKeyUp) document.removeEventListener('mousemove', handleMouseMove) document.removeEventListener('pointerlockchange', handlePointerLockChange) document.removeEventListener('mousedown', handleMouseDown) document.removeEventListener('contextmenu', handleContextMenu) document.removeEventListener('wheel', handleWheel) } }, []) return { keys, isPointerLocked, requestPointerLock, raycastBlock } } /** * Update player position every frame */ export function updatePlayerMovement(keys: KeyState, delta: number) { const store = usePlayerStore.getState() const player = store.player if (!player) return const { paused } = useGameStore.getState() if (paused) return const [yaw, pitch] = player.rotation const speed = player.flying ? FLY_SPEED : player.sprinting ? SPRINT_SPEED : player.sneaking ? SNEAK_SPEED : WALK_SPEED // Keyboard movement const forward = [-Math.sin(yaw), -Math.cos(yaw)] const right = [Math.cos(yaw), -Math.sin(yaw)] let moveX = 0, moveZ = 0 if (keys.forward) { moveX += forward[0]; moveZ += forward[1] } if (keys.backward) { moveX -= forward[0]; moveZ -= forward[1] } if (keys.left) { moveX -= right[0]; moveZ -= right[1] } if (keys.right) { moveX += right[0]; moveZ += right[1] } // Add mobile movement if ((window as any).__MC_MOBILE_MOVE) { moveX += (window as any).__MC_MOBILE_MOVE.x moveZ += (window as any).__MC_MOBILE_MOVE.z } const len = Math.sqrt(moveX * moveX + moveZ * moveZ) if (len > 0) { moveX = (moveX / len) * speed moveZ = (moveZ / len) * speed } let [vx, vy, vz] = player.velocity let [px, py, pz] = player.position const jumping = keys.jump || (window as any).__MC_MOBILE_JUMP const sneaking = keys.sneak || (window as any).__MC_MOBILE_SNEAK if (player.flying) { vx = moveX; vz = moveZ; vy = 0 if (jumping) vy = speed if (sneaking) vy = -speed } else { vx = moveX; vz = moveZ vy -= GRAVITY * delta if (jumping && player.onGround) vy = JUMP_VELOCITY vy = Math.max(vy, -78.4) } // Apply velocity px += vx * delta py += vy * delta pz += vz * delta // Collision detection const { getBlock } = useWorldStore.getState() if (!player.flying) { // Ground collision const blockBelow = getBlock(Math.floor(px), Math.floor(py - 0.05), Math.floor(pz)) const defBelow = BLOCK_DEFS[blockBelow] if (defBelow && defBelow.solid && vy < 0) { // Snap to top of block const groundY = Math.floor(py - 0.05) + 1 if (py - groundY < 0.5) { py = groundY vy = 0 } } // Head collision const blockAbove = getBlock(Math.floor(px), Math.floor(py + PLAYER_HEIGHT + 0.1), Math.floor(pz)) const defAbove = BLOCK_DEFS[blockAbove] if (defAbove && defAbove.solid && vy > 0) { vy = 0 } } // X/Z collision const playerWidth = 0.3 const feetY = Math.floor(py) const headY = Math.floor(py + PLAYER_HEIGHT) for (const [dx, dz] of [[-1, 0], [1, 0], [0, -1], [0, 1]] as [number, number][]) { for (let checkY = feetY; checkY <= headY; checkY++) { const checkX = Math.floor(px + dx * playerWidth) const checkZ = Math.floor(pz + dz * playerWidth) const block = getBlock(checkX, checkY, checkZ) if (BLOCK_DEFS[block]?.solid) { if (dx !== 0) px -= vx * delta if (dz !== 0) pz -= vz * delta break } } } // World bounds if (py < MIN_BUILD_HEIGHT + PLAYER_HEIGHT) { py = MIN_BUILD_HEIGHT + PLAYER_HEIGHT; vy = 0 } // Fall damage (survival only) const onGround = !!(BLOCK_DEFS[getBlock(Math.floor(px), Math.floor(py - 0.05), Math.floor(pz))]?.solid) if (player.gamemode === 'survival' && !player.flying && player.onGround === false && onGround && vy < -15) { const fallDistance = (vy * delta) * -1 if (fallDistance > 3) { store.damage(Math.floor(fallDistance - 3)) } } store.updatePosition([px, py, pz]) store.updateVelocity([vx, vy, vz]) if (player.onGround !== onGround) { const p = usePlayerStore.getState().player if (p) usePlayerStore.setState({ player: { ...p, onGround } }) } } declare global { interface Window { __MC_MOBILE_MOVE: { x: number; z: number } | null __MC_MOBILE_JUMP: boolean __MC_MOBILE_SNEAK: boolean __MC_MOBILE_BREAK: boolean __MC_MOBILE_PLACE: boolean } }