303 lines
9.8 KiB
TypeScript
303 lines
9.8 KiB
TypeScript
import { useRef, useEffect } from 'react'
|
|
import { subscribe, useSnapshot } from 'valtio'
|
|
import { useUtilsEffect } from '@zardoy/react-util'
|
|
import { options } from '../optionsStorage'
|
|
import { activeModalStack, isGameActive, miscUiState } from '../globalState'
|
|
import { onCameraMove, CameraMoveEvent } from '../cameraRotationControls'
|
|
import { pointerLock, isInRealGameSession } from '../utils'
|
|
import { handleMovementStickDelta, joystickPointer } from './TouchAreasControls'
|
|
|
|
/** after what time of holding the finger start breaking the block */
|
|
const touchStartBreakingBlockMs = 500
|
|
|
|
function GameInteractionOverlayInner ({
|
|
zIndex,
|
|
setJoystickOrigin,
|
|
updateJoystick
|
|
}: {
|
|
zIndex: number,
|
|
setJoystickOrigin: (e: PointerEvent | null) => void
|
|
updateJoystick: (e: PointerEvent) => void
|
|
}) {
|
|
const overlayRef = useRef<HTMLDivElement>(null)
|
|
|
|
|
|
useUtilsEffect(({ signal }) => {
|
|
if (!overlayRef.current) return
|
|
|
|
const cameraControlEl = overlayRef.current
|
|
let virtualClickActive = false
|
|
let virtualClickTimeout: NodeJS.Timeout | undefined
|
|
let screenTouches = 0
|
|
const capturedPointer = {
|
|
active: null as {
|
|
id: number;
|
|
x: number;
|
|
y: number;
|
|
sourceX: number;
|
|
sourceY: number;
|
|
activateCameraMove: boolean;
|
|
time: number
|
|
} | null
|
|
}
|
|
|
|
const pointerDownHandler = (e: PointerEvent) => {
|
|
const clickedEl = e.composedPath()[0]
|
|
if (!isGameActive(true) || clickedEl !== cameraControlEl || e.pointerId === undefined) {
|
|
return
|
|
}
|
|
screenTouches++
|
|
if (screenTouches === 3) {
|
|
// todo maybe mouse wheel click?
|
|
}
|
|
const usingModernMovement = options.touchMovementType === 'modern'
|
|
if (usingModernMovement) {
|
|
if (!joystickPointer.pointer && e.clientX < window.innerWidth / 2) {
|
|
cameraControlEl.setPointerCapture(e.pointerId)
|
|
setJoystickOrigin(e)
|
|
return
|
|
}
|
|
}
|
|
if (capturedPointer.active) {
|
|
return
|
|
}
|
|
cameraControlEl.setPointerCapture(e.pointerId)
|
|
capturedPointer.active = {
|
|
id: e.pointerId,
|
|
x: e.clientX,
|
|
y: e.clientY,
|
|
sourceX: e.clientX,
|
|
sourceY: e.clientY,
|
|
activateCameraMove: false,
|
|
time: Date.now()
|
|
}
|
|
if (options.touchInteractionType === 'classic') {
|
|
virtualClickTimeout ??= setTimeout(() => {
|
|
virtualClickActive = true
|
|
document.dispatchEvent(new MouseEvent('mousedown', { button: 0 }))
|
|
}, touchStartBreakingBlockMs)
|
|
}
|
|
}
|
|
|
|
const pointerMoveHandler = (e: PointerEvent) => {
|
|
if (e.pointerId === undefined) return
|
|
const scale = window.visualViewport?.scale || 1
|
|
|
|
const supportsPressure = (e as any).pressure !== undefined &&
|
|
(e as any).pressure !== 0 &&
|
|
(e as any).pressure !== 0.5 &&
|
|
(e as any).pressure !== 1 &&
|
|
(e.pointerType === 'touch' || e.pointerType === 'pen')
|
|
|
|
if (e.pointerId === joystickPointer.pointer?.pointerId) {
|
|
updateJoystick(e)
|
|
if (supportsPressure && (e as any).pressure > 0.5) {
|
|
bot.setControlState('sprint', true)
|
|
}
|
|
return
|
|
}
|
|
if (e.pointerId !== capturedPointer.active?.id) return
|
|
// window.scrollTo(0, 0)
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
|
|
const allowedJitter = 1.1
|
|
if (supportsPressure) {
|
|
bot.setControlState('jump', (e as any).pressure > 0.5)
|
|
}
|
|
|
|
// Adjust coordinates for scale
|
|
const currentX = e.clientX / scale
|
|
const currentY = e.clientY / scale
|
|
const sourceX = capturedPointer.active.sourceX / scale
|
|
const sourceY = capturedPointer.active.sourceY / scale
|
|
const lastX = capturedPointer.active.x / scale
|
|
const lastY = capturedPointer.active.y / scale
|
|
|
|
const xDiff = Math.abs(currentX - sourceX) > allowedJitter
|
|
const yDiff = Math.abs(currentY - sourceY) > allowedJitter
|
|
|
|
if (!capturedPointer.active.activateCameraMove && (xDiff || yDiff)) {
|
|
capturedPointer.active.activateCameraMove = true
|
|
}
|
|
if (capturedPointer.active.activateCameraMove) {
|
|
clearTimeout(virtualClickTimeout)
|
|
}
|
|
|
|
onCameraMove({
|
|
movementX: (currentX - lastX),
|
|
movementY: (currentY - lastY),
|
|
type: 'touchmove',
|
|
stopPropagation: () => e.stopPropagation()
|
|
} as CameraMoveEvent)
|
|
|
|
capturedPointer.active.x = e.clientX
|
|
capturedPointer.active.y = e.clientY
|
|
}
|
|
|
|
const pointerUpHandler = (e: PointerEvent) => {
|
|
if (e.pointerId === undefined) return
|
|
if (e.pointerId === joystickPointer.pointer?.pointerId) {
|
|
setJoystickOrigin(null)
|
|
return
|
|
}
|
|
if (e.pointerId !== capturedPointer.active?.id) return
|
|
clearTimeout(virtualClickTimeout)
|
|
virtualClickTimeout = undefined
|
|
|
|
if (virtualClickActive) {
|
|
// button 0 is left click
|
|
document.dispatchEvent(new MouseEvent('mouseup', { button: 0 }))
|
|
virtualClickActive = false
|
|
} else if (!capturedPointer.active.activateCameraMove && (Date.now() - capturedPointer.active.time < touchStartBreakingBlockMs)) {
|
|
// single click action
|
|
const MOUSE_BUTTON_RIGHT = 2
|
|
const MOUSE_BUTTON_LEFT = 0
|
|
const gonnaAttack = !!bot.mouse.getCursorState().entity
|
|
document.dispatchEvent(new MouseEvent('mousedown', { button: gonnaAttack ? MOUSE_BUTTON_LEFT : MOUSE_BUTTON_RIGHT }))
|
|
bot.mouse.update()
|
|
document.dispatchEvent(new MouseEvent('mouseup', { button: gonnaAttack ? MOUSE_BUTTON_LEFT : MOUSE_BUTTON_RIGHT }))
|
|
}
|
|
|
|
if (screenTouches > 0) {
|
|
screenTouches--
|
|
}
|
|
capturedPointer.active = null
|
|
}
|
|
|
|
const contextMenuHandler = (e: Event) => {
|
|
e.preventDefault()
|
|
}
|
|
|
|
const blurHandler = () => {
|
|
bot.clearControlStates()
|
|
}
|
|
|
|
cameraControlEl.addEventListener('pointerdown', pointerDownHandler, { signal })
|
|
cameraControlEl.addEventListener('pointermove', pointerMoveHandler, { signal })
|
|
cameraControlEl.addEventListener('pointerup', pointerUpHandler, { signal })
|
|
cameraControlEl.addEventListener('pointercancel', pointerUpHandler, { signal })
|
|
cameraControlEl.addEventListener('lostpointercapture', pointerUpHandler, { signal })
|
|
cameraControlEl.addEventListener('contextmenu', contextMenuHandler, { signal })
|
|
window.addEventListener('blur', blurHandler, { signal })
|
|
|
|
// Add zoom detection and reset
|
|
const detectAndResetZoom = () => {
|
|
const { visualViewport } = window
|
|
if (!visualViewport) return
|
|
|
|
if (visualViewport.scale !== 1) {
|
|
// Reset zoom by updating viewport meta tag
|
|
const viewport = document.querySelector('meta[name=viewport]')
|
|
if (viewport) {
|
|
viewport.setAttribute('content', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no')
|
|
// Force re-layout
|
|
setTimeout(() => {
|
|
viewport.setAttribute('content', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no')
|
|
}, 300)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Listen for zoom changes
|
|
window.visualViewport?.addEventListener('resize', detectAndResetZoom, { signal })
|
|
detectAndResetZoom()
|
|
|
|
// Prevent zoom gestures
|
|
document.addEventListener('gesturestart', (e) => e.preventDefault(), { signal })
|
|
document.addEventListener('gesturechange', (e) => e.preventDefault(), { signal })
|
|
document.addEventListener('gestureend', (e) => e.preventDefault(), { signal })
|
|
|
|
|
|
// Debug method to simulate zoom
|
|
window.debugSimulateZoom = (scale = 1.1, x = 0, y = 0) => {
|
|
const viewport = document.querySelector('meta[name=viewport]')
|
|
if (viewport) {
|
|
viewport.setAttribute('content', `width=device-width, initial-scale=${scale}, user-scalable=no, viewport-fit=cover, transform-origin: ${x}px ${y}px`)
|
|
}
|
|
// This will trigger the visualViewport resize event
|
|
setTimeout(() => {
|
|
window.visualViewport?.dispatchEvent(new Event('resize'))
|
|
}, 100)
|
|
}
|
|
|
|
// Debug method to reset zoom
|
|
window.debugResetZoom = () => {
|
|
const viewport = document.querySelector('meta[name=viewport]')
|
|
if (viewport) {
|
|
viewport.setAttribute('content', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no')
|
|
}
|
|
setTimeout(() => {
|
|
window.visualViewport?.dispatchEvent(new Event('resize'))
|
|
}, 100)
|
|
}
|
|
|
|
signal.addEventListener('abort', () => {
|
|
setJoystickOrigin(null)
|
|
})
|
|
}, [setJoystickOrigin])
|
|
|
|
return (
|
|
<OverlayElement divRef={overlayRef} zIndex={zIndex} />
|
|
)
|
|
|
|
|
|
}
|
|
|
|
const OverlayElement = ({ divRef, zIndex }: { divRef: React.RefObject<HTMLDivElement>, zIndex: number }) => {
|
|
return <div
|
|
className='game-interaction-overlay'
|
|
ref={divRef}
|
|
style={{
|
|
position: 'fixed',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
zIndex,
|
|
touchAction: 'none',
|
|
userSelect: 'none'
|
|
}}
|
|
/>
|
|
}
|
|
|
|
export default function GameInteractionOverlay ({ zIndex }: { zIndex: number }) {
|
|
const modalStack = useSnapshot(activeModalStack)
|
|
const { currentTouch } = useSnapshot(miscUiState)
|
|
|
|
const setJoystickOrigin = useRef((e: PointerEvent | null) => {
|
|
if (!e) {
|
|
handleMovementStickDelta()
|
|
joystickPointer.pointer = null
|
|
return
|
|
}
|
|
|
|
joystickPointer.pointer = {
|
|
pointerId: e.pointerId,
|
|
x: e.clientX,
|
|
y: e.clientY
|
|
}
|
|
}).current
|
|
|
|
const updateJoystick = useRef((e: PointerEvent) => {
|
|
handleMovementStickDelta(e)
|
|
}).current
|
|
|
|
if (modalStack.length > 0 || !currentTouch) return null
|
|
return <GameInteractionOverlayInner
|
|
zIndex={zIndex}
|
|
setJoystickOrigin={setJoystickOrigin}
|
|
updateJoystick={updateJoystick}
|
|
/>
|
|
|
|
}
|
|
|
|
subscribe(activeModalStack, () => {
|
|
if (activeModalStack.length === 0) {
|
|
if (isInRealGameSession()) {
|
|
void pointerLock.requestPointerLock()
|
|
}
|
|
} else {
|
|
document.exitPointerLock?.()
|
|
}
|
|
})
|