Spaces:
Sleeping
Sleeping
| 'use client' | |
| import { useCallback, useRef, useEffect, useState } from 'react' | |
| import { usePlayerStore } from '@/stores/playerStore' | |
| export function MobileControls() { | |
| const [moveActive, setMoveActive] = useState(false) | |
| const [lookActive, setLookActive] = useState(false) | |
| const moveRef = useRef<{ x: number; z: number }>({ x: 0, z: 0 }) | |
| const lookRef = useRef<{ lastX: number; lastY: number }>({ lastX: 0, lastY: 0 }) | |
| const moveTouchId = useRef<number | null>(null) | |
| const lookTouchId = useRef<number | null>(null) | |
| // Move joystick | |
| const handleMoveStart = useCallback((e: React.TouchEvent) => { | |
| e.preventDefault() | |
| const touch = e.changedTouches[0] | |
| moveTouchId.current = touch.identifier | |
| lookRef.current.lastX = touch.clientX | |
| lookRef.current.lastY = touch.clientY | |
| setMoveActive(true) | |
| }, []) | |
| const handleMoveMove = useCallback((e: React.TouchEvent) => { | |
| e.preventDefault() | |
| for (let i = 0; i < e.changedTouches.length; i++) { | |
| const touch = e.changedTouches[i] | |
| if (touch.identifier === moveTouchId.current) { | |
| const dx = (touch.clientX - lookRef.current.lastX) / 40 | |
| const dz = (touch.clientY - lookRef.current.lastY) / 40 | |
| moveRef.current = { | |
| x: Math.max(-1, Math.min(1, dx)), | |
| z: Math.max(-1, Math.min(1, dz)), | |
| } | |
| ;(window as any).__MC_MOBILE_MOVE = moveRef.current | |
| } | |
| } | |
| }, []) | |
| const handleMoveEnd = useCallback(() => { | |
| moveTouchId.current = null | |
| moveRef.current = { x: 0, z: 0 } | |
| ;(window as any).__MC_MOBILE_MOVE = null | |
| setMoveActive(false) | |
| }, []) | |
| // Look joystick | |
| const handleLookStart = useCallback((e: React.TouchEvent) => { | |
| e.preventDefault() | |
| const touch = e.changedTouches[0] | |
| lookTouchId.current = touch.identifier | |
| lookRef.current = { lastX: touch.clientX, lastY: touch.clientY } | |
| setLookActive(true) | |
| }, []) | |
| const handleLookMove = useCallback((e: React.TouchEvent) => { | |
| e.preventDefault() | |
| for (let i = 0; i < e.changedTouches.length; i++) { | |
| const touch = e.changedTouches[i] | |
| if (touch.identifier === lookTouchId.current) { | |
| const dx = touch.clientX - lookRef.current.lastX | |
| const dy = touch.clientY - lookRef.current.lastY | |
| lookRef.current = { lastX: touch.clientX, lastY: touch.clientY } | |
| const player = usePlayerStore.getState().player | |
| if (player) { | |
| const sensitivity = 0.004 | |
| const yaw = player.rotation[0] - dx * sensitivity | |
| const pitch = Math.max(-Math.PI / 2 + 0.01, | |
| Math.min(Math.PI / 2 - 0.01, player.rotation[1] - dy * sensitivity)) | |
| usePlayerStore.getState().updateRotation([yaw, pitch]) | |
| } | |
| } | |
| } | |
| }, []) | |
| const handleLookEnd = useCallback(() => { | |
| lookTouchId.current = null | |
| setLookActive(false) | |
| }, []) | |
| const handleJump = useCallback(() => { | |
| ;(window as any).__MC_MOBILE_JUMP = true | |
| setTimeout(() => { (window as any).__MC_MOBILE_JUMP = false }, 100) | |
| }, []) | |
| const handleBreak = useCallback(() => { | |
| ;(window as any).__MC_MOBILE_BREAK = true | |
| }, []) | |
| const handlePlace = useCallback(() => { | |
| ;(window as any).__MC_MOBILE_PLACE = true | |
| }, []) | |
| return ( | |
| <div className="absolute inset-0 z-20 pointer-events-none"> | |
| {/* Move joystick - left side */} | |
| <div | |
| className="absolute bottom-16 left-4 w-28 h-28 rounded-full border-2 border-white/30 bg-white/10 pointer-events-auto touch-none" | |
| onTouchStart={handleMoveStart} | |
| onTouchMove={handleMoveMove} | |
| onTouchEnd={handleMoveEnd} | |
| > | |
| <div className={`absolute inset-0 flex items-center justify-center text-white/50 text-xs | |
| ${moveActive ? 'text-green-400/80' : ''}`}> | |
| {moveActive ? '●' : 'Move'} | |
| </div> | |
| </div> | |
| {/* Look area - right side */} | |
| <div | |
| className="absolute bottom-16 right-4 w-28 h-28 rounded-full border-2 border-white/30 bg-white/10 pointer-events-auto touch-none" | |
| onTouchStart={handleLookStart} | |
| onTouchMove={handleLookMove} | |
| onTouchEnd={handleLookEnd} | |
| > | |
| <div className={`absolute inset-0 flex items-center justify-center text-white/50 text-xs | |
| ${lookActive ? 'text-blue-400/80' : ''}`}> | |
| {lookActive ? '●' : 'Look'} | |
| </div> | |
| </div> | |
| {/* Action buttons */} | |
| <div className="absolute bottom-16 left-1/2 -translate-x-1/2 flex gap-3 pointer-events-auto"> | |
| {/* Jump */} | |
| <button | |
| className="w-12 h-12 rounded-full bg-white/15 border border-white/30 flex items-center justify-center text-white/70 active:bg-white/30" | |
| onTouchStart={handleJump} | |
| > | |
| ⬆ | |
| </button> | |
| </div> | |
| {/* Break/Place buttons */} | |
| <div className="absolute bottom-32 right-6 flex flex-col gap-2 pointer-events-auto"> | |
| <button | |
| className="w-14 h-14 rounded-xl bg-red-500/30 border border-red-400/50 flex items-center justify-center text-white text-xs active:bg-red-500/50" | |
| onTouchStart={handleBreak} | |
| > | |
| ⛏ | |
| </button> | |
| <button | |
| className="w-14 h-14 rounded-xl bg-blue-500/30 border border-blue-400/50 flex items-center justify-center text-white text-xs active:bg-blue-500/50" | |
| onTouchStart={handlePlace} | |
| > | |
| 🧱 | |
| </button> | |
| </div> | |
| {/* Fly toggle */} | |
| <button | |
| className="absolute top-4 right-4 px-3 py-1.5 bg-white/15 border border-white/30 rounded text-white/70 text-xs pointer-events-auto active:bg-white/30" | |
| onClick={() => { | |
| const { player } = usePlayerStore.getState() | |
| if (player) usePlayerStore.getState().setFlying(!player.flying) | |
| }} | |
| > | |
| ✈ Fly | |
| </button> | |
| {/* Inventory */} | |
| <button | |
| className="absolute top-4 right-20 px-3 py-1.5 bg-white/15 border border-white/30 rounded text-white/70 text-xs pointer-events-auto active:bg-white/30" | |
| onClick={() => usePlayerStore.getState().setGamemode('creative')} | |
| > | |
| 📦 Inv | |
| </button> | |
| </div> | |
| ) | |
| } | |