minecraft-clone / src /hooks /usePlayerControls.ts
TomatitoToho's picture
Upload src/hooks/usePlayerControls.ts with huggingface_hub
4f4f2dc verified
Raw
History Blame Contribute Delete
12.2 kB
'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<KeyState>({
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
}
}