478 lines
14 KiB
TypeScript
478 lines
14 KiB
TypeScript
import { Vec3 } from 'vec3'
|
|
import { useRef, useEffect, useState, CSSProperties, Dispatch, SetStateAction } from 'react'
|
|
import { WorldWarp } from 'flying-squid/dist/lib/modules/warps'
|
|
import { TransformWrapper, TransformComponent, ReactZoomPanPinchRef } from 'react-zoom-pan-pinch'
|
|
import { DrawerAdapter } from './MinimapDrawer'
|
|
import Button from './Button'
|
|
import Input from './Input'
|
|
import './Fullmap.css'
|
|
|
|
|
|
type FullmapProps = {
|
|
adapter: DrawerAdapter,
|
|
toggleFullMap?: () => void,
|
|
}
|
|
|
|
export default ({ adapter, toggleFullMap }: FullmapProps) => {
|
|
const [grid, setGrid] = useState(() => new Set<string>())
|
|
const zoomRef = useRef<ReactZoomPanPinchRef>(null)
|
|
const [redraw, setRedraw] = useState<Set<string> | null>(null)
|
|
const [lastWarpPos, setLastWarpPos] = useState({ x: 0, y: 0, z: 0 })
|
|
const stateRef = useRef({ scale: 1, positionX: 0, positionY: 0 })
|
|
const cells = useRef({ columns: 0, rows: 0 })
|
|
const [isWarpInfoOpened, setIsWarpInfoOpened] = useState(false)
|
|
const [initWarp, setInitWarp] = useState<WorldWarp | undefined>(undefined)
|
|
const [warpPreview, setWarpPreview] = useState<{ name: string, x: number, z: number, clientX: number, clientY: number } | undefined>(undefined)
|
|
|
|
const updateGrid = () => {
|
|
const wrapperRect = zoomRef.current?.instance.wrapperComponent?.getBoundingClientRect()
|
|
if (!wrapperRect) return
|
|
const cellSize = 64
|
|
const columns = Math.ceil(wrapperRect.width / (cellSize * stateRef.current.scale))
|
|
const rows = Math.ceil(wrapperRect.height / (cellSize * stateRef.current.scale))
|
|
cells.current.rows = rows
|
|
cells.current.columns = columns
|
|
const leftBorder = - Math.floor(stateRef.current.positionX / (stateRef.current.scale * cellSize)) * cellSize
|
|
const topBorder = - Math.floor(stateRef.current.positionY / (stateRef.current.scale * cellSize)) * cellSize
|
|
const newGrid = new Set<string>()
|
|
for (let row = -1; row < rows; row += 1) {
|
|
for (let col = -1; col < columns; col += 1) {
|
|
const x = leftBorder + col * cellSize
|
|
const y = topBorder + row * cellSize
|
|
newGrid.add(`${x},${y}`)
|
|
}
|
|
}
|
|
setGrid(newGrid)
|
|
}
|
|
|
|
useEffect(() => {
|
|
adapter.full = true
|
|
console.log('[fullmap] set full property to true')
|
|
updateGrid()
|
|
}, [])
|
|
|
|
return <div
|
|
style={{
|
|
position: 'fixed',
|
|
isolation: 'isolate',
|
|
inset: '0px',
|
|
display: 'flex',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
backgroundColor: 'rgba(0, 0, 0, 0.4)',
|
|
zIndex: 100
|
|
}}
|
|
>
|
|
{window.screen.width > 500 ? <div
|
|
style={{
|
|
position: 'absolute',
|
|
width: '100%',
|
|
height: '100%',
|
|
zIndex: '-1'
|
|
}}
|
|
onClick={toggleFullMap}
|
|
> </div>
|
|
: <Button
|
|
icon="close-box"
|
|
onClick={toggleFullMap}
|
|
style={{
|
|
position: 'absolute',
|
|
top: '20px',
|
|
right: '20px',
|
|
zIndex: 1
|
|
}}
|
|
/>}
|
|
<TransformWrapper
|
|
limitToBounds={false}
|
|
ref={zoomRef}
|
|
minScale={0.1}
|
|
doubleClick={{
|
|
disabled: false
|
|
}}
|
|
panning={{
|
|
allowLeftClickPan: true,
|
|
allowRightClickPan: false
|
|
}}
|
|
onTransformed={(ref, state) => {
|
|
stateRef.current = { ...state }
|
|
}}
|
|
onPanningStop={() => {
|
|
updateGrid()
|
|
}}
|
|
onZoomStop={() => {
|
|
updateGrid()
|
|
}}
|
|
>
|
|
<TransformComponent
|
|
wrapperClass="map"
|
|
wrapperStyle={{
|
|
willChange: 'transform',
|
|
}}
|
|
>
|
|
{[...grid].map((cellCoords) => {
|
|
const [x, y] = cellCoords.split(',').map(Number)
|
|
const playerChunkLeft = Math.floor(adapter.playerPosition.x / 16) * 16
|
|
const playerChunkTop = Math.floor(adapter.playerPosition.z / 16) * 16
|
|
const wrapperRect = zoomRef.current?.instance.wrapperComponent?.getBoundingClientRect()
|
|
const offsetX = Math.floor((wrapperRect?.width ?? 0) / (8 * 16)) * 16
|
|
const offsetY = Math.floor((wrapperRect?.height ?? 0) / (8 * 16)) * 16
|
|
|
|
return <MapChunk
|
|
key={'mapcell:' + cellCoords}
|
|
x={x}
|
|
y={y}
|
|
scale={stateRef.current.scale}
|
|
adapter={adapter}
|
|
worldX={playerChunkLeft + x / 4 - offsetX}
|
|
worldZ={playerChunkTop + y / 4 - offsetY}
|
|
setIsWarpInfoOpened={setIsWarpInfoOpened}
|
|
setLastWarpPos={setLastWarpPos}
|
|
redraw={redraw}
|
|
setInitWarp={setInitWarp}
|
|
setWarpPreview={setWarpPreview}
|
|
/>
|
|
})}
|
|
</TransformComponent>
|
|
</TransformWrapper>
|
|
{warpPreview && <div
|
|
style={{
|
|
position: 'absolute',
|
|
top: warpPreview.clientY - 70,
|
|
left: warpPreview.clientX - 70,
|
|
textAlign: 'center',
|
|
fontSize: '1.5em',
|
|
textShadow: '0.1em 0 black, 0 0.1em black, -0.1em 0 black, 0 -0.1em black, -0.1em -0.1em black, -0.1em 0.1em black, 0.1em -0.1em black, 0.1em 0.1em black'
|
|
} as any}
|
|
>
|
|
{warpPreview.name}
|
|
<div>
|
|
{warpPreview.x} {warpPreview.z}
|
|
</div>
|
|
</div>}
|
|
{
|
|
isWarpInfoOpened && <WarpInfo
|
|
adapter={adapter}
|
|
warpPos={lastWarpPos}
|
|
setIsWarpInfoOpened={setIsWarpInfoOpened}
|
|
setRedraw={setRedraw}
|
|
initWarp={initWarp}
|
|
setInitWarp={setInitWarp}
|
|
toggleFullMap={toggleFullMap}
|
|
/>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
|
|
const MapChunk = (
|
|
{ x, y, scale, adapter, worldX, worldZ, setIsWarpInfoOpened, setLastWarpPos, redraw, setInitWarp, setWarpPreview }:
|
|
{
|
|
x: number,
|
|
y: number,
|
|
scale: number,
|
|
adapter: DrawerAdapter,
|
|
worldX: number,
|
|
worldZ: number,
|
|
setIsWarpInfoOpened: (x: boolean) => void,
|
|
setLastWarpPos: (obj: { x: number, y: number, z: number }) => void,
|
|
redraw?: Set<string> | null
|
|
setInitWarp?: (warp: WorldWarp | undefined) => void
|
|
setWarpPreview?: (warpInfo) => void
|
|
}
|
|
) => {
|
|
const containerRef = useRef(null)
|
|
const touchTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
|
|
const longPress = (e) => {
|
|
touchTimer.current = setTimeout(() => {
|
|
touchTimer.current = null
|
|
handleClick(e)
|
|
}, 500)
|
|
}
|
|
|
|
const cancel = () => {
|
|
if (touchTimer.current) clearTimeout(touchTimer.current)
|
|
}
|
|
|
|
const handleClick = (e: MouseEvent | TouchEvent) => {
|
|
if (!adapter.mapDrawer) return
|
|
console.log('click:', e)
|
|
let clientX: number
|
|
let clientY: number
|
|
if ('buttons' in e && e.button === 2) {
|
|
clientX = e.clientX
|
|
clientY = e.clientY
|
|
} else if ('changedTouches' in e) {
|
|
clientX = (e).changedTouches[0].clientX
|
|
clientY = (e).changedTouches[0].clientY
|
|
} else { return }
|
|
const [x, z] = getXZ(clientX, clientY)
|
|
const mapX = Math.floor(x + worldX)
|
|
const mapZ = Math.floor(z + worldZ)
|
|
const y = adapter.getHighestBlockY(mapX, mapZ)
|
|
adapter.mapDrawer.setWarpPosOnClick(new Vec3(mapX, y, mapZ))
|
|
setLastWarpPos(adapter.mapDrawer.lastWarpPos)
|
|
const { lastWarpPos } = adapter.mapDrawer
|
|
const initWarp = adapter.warps.find(warp => Math.hypot(lastWarpPos.x - warp.x, lastWarpPos.z - warp.z) < 2)
|
|
setInitWarp?.(initWarp)
|
|
setIsWarpInfoOpened(true)
|
|
}
|
|
|
|
const getXZ = (clientX: number, clientY: number) => {
|
|
const rect = canvasRef.current!.getBoundingClientRect()
|
|
const factor = scale * (adapter.mapDrawer.mapPixel ?? 1)
|
|
const x = (clientX - rect.left) / factor
|
|
const y = (clientY - rect.top) / factor
|
|
return [x, y]
|
|
}
|
|
|
|
const handleMouseMove = (e: MouseEvent) => {
|
|
const [x, z] = getXZ(e.clientX, e.clientY)
|
|
const warp = adapter.warps.find(w => Math.hypot(w.x - x - worldX, w.z - z - worldZ) < 2)
|
|
setWarpPreview?.(
|
|
warp ? { name: warp.name, x: warp.x, z: warp.z, clientX: e.clientX, clientY: e.clientY } : undefined
|
|
)
|
|
}
|
|
|
|
const handleRedraw = (key?: string) => {
|
|
if (key !== `${worldX / 16},${worldZ / 16}`) return
|
|
adapter.mapDrawer.canvas = canvasRef.current!
|
|
adapter.mapDrawer.full = true
|
|
console.log('[mapChunk] update', key, `${worldX / 16},${worldZ / 16}`)
|
|
const timeout = setTimeout(() => {
|
|
if (canvasRef.current) void adapter.drawChunkOnCanvas(`${worldX / 16},${worldZ / 16}`, canvasRef.current)
|
|
clearTimeout(timeout)
|
|
}, 100)
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (canvasRef.current) void adapter.drawChunkOnCanvas(`${worldX / 16},${worldZ / 16}`, canvasRef.current)
|
|
}, [canvasRef.current])
|
|
|
|
useEffect(() => {
|
|
canvasRef.current?.addEventListener('contextmenu', handleClick)
|
|
canvasRef.current?.addEventListener('touchstart', longPress)
|
|
canvasRef.current?.addEventListener('touchend', cancel)
|
|
canvasRef.current?.addEventListener('touchmove', cancel)
|
|
canvasRef.current?.addEventListener('mousemove', handleMouseMove)
|
|
|
|
return () => {
|
|
canvasRef.current?.removeEventListener('contextmenu', handleClick)
|
|
canvasRef.current?.removeEventListener('touchstart', longPress)
|
|
canvasRef.current?.removeEventListener('touchend', cancel)
|
|
canvasRef.current?.removeEventListener('touchmove', cancel)
|
|
canvasRef.current?.removeEventListener('mousemove', handleMouseMove)
|
|
}
|
|
}, [canvasRef.current])
|
|
|
|
useEffect(() => {
|
|
if (redraw) {
|
|
for (const key of redraw) {
|
|
handleRedraw(key)
|
|
}
|
|
}
|
|
}, [redraw])
|
|
|
|
return <div
|
|
ref={containerRef}
|
|
style={{
|
|
position: 'absolute',
|
|
width: '64px',
|
|
height: '64px',
|
|
top: `${y}px`,
|
|
left: `${x}px`,
|
|
}}
|
|
>
|
|
<canvas
|
|
ref={canvasRef}
|
|
style={{
|
|
width: '100%',
|
|
height: '100%',
|
|
imageRendering: 'pixelated'
|
|
}}
|
|
width={64}
|
|
height={64}
|
|
/>
|
|
</div>
|
|
}
|
|
|
|
const WarpInfo = (
|
|
{ adapter, warpPos, setIsWarpInfoOpened, afterWarpIsSet, initWarp, toggleFullMap, setRedraw }:
|
|
{
|
|
adapter: DrawerAdapter,
|
|
warpPos: { x: number, y: number, z: number },
|
|
setIsWarpInfoOpened: Dispatch<SetStateAction<boolean>>,
|
|
afterWarpIsSet?: () => void
|
|
initWarp?: WorldWarp,
|
|
setInitWarp?: React.Dispatch<React.SetStateAction<WorldWarp | undefined>>,
|
|
toggleFullMap?: () => void,
|
|
setRedraw?: React.Dispatch<React.SetStateAction<Set<string> | null>>
|
|
}
|
|
) => {
|
|
const [warp, setWarp] = useState<WorldWarp>(initWarp ?? {
|
|
name: '',
|
|
x: warpPos?.x ?? 100,
|
|
y: warpPos?.y ?? 100,
|
|
z: warpPos?.z ?? 100,
|
|
color: '',
|
|
disabled: false,
|
|
world: adapter.world
|
|
})
|
|
|
|
const posInputStyle: CSSProperties = {
|
|
flexGrow: '1',
|
|
}
|
|
const fieldCont: CSSProperties = {
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '5px'
|
|
}
|
|
|
|
const updateChunk = () => {
|
|
const redraw = new Set<string>()
|
|
for (let i = -1; i < 2; i += 1) {
|
|
for (let j = -1; j < 2; j += 1) {
|
|
redraw.add(`${Math.floor(warp.x / 16) + j},${Math.floor(warp.z / 16) + i}`)
|
|
}
|
|
}
|
|
setRedraw?.(redraw)
|
|
console.log('[warpInfo] update', redraw)
|
|
}
|
|
|
|
const tpNow = () => {
|
|
adapter.off('updateChunk', tpNow)
|
|
}
|
|
|
|
const quickTp = () => {
|
|
toggleFullMap?.()
|
|
adapter.quickTp?.(warp.x, warp.z)
|
|
}
|
|
|
|
return <div
|
|
style={{
|
|
position: 'absolute',
|
|
inset: '0px',
|
|
display: 'flex',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
flexDirection: 'column',
|
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
fontSize: '0.8em',
|
|
transform: 'scale(2)'
|
|
}}
|
|
>
|
|
<form
|
|
style={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '10px',
|
|
width: window.screen.width > 500 ? '100%' : '50%',
|
|
minWidth: '100px',
|
|
maxWidth: '300px',
|
|
padding: '20px',
|
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
border: '2px solid black'
|
|
}}
|
|
>
|
|
<h2 style={{ alignSelf: 'center' }}>Point on the map</h2>
|
|
<div style={fieldCont}>
|
|
<div>
|
|
Name:
|
|
</div>
|
|
<Input
|
|
defaultValue={warp.name}
|
|
onChange={(e) => {
|
|
if (!e.target) return
|
|
setWarp(prev => { return { ...prev, name: e.target.value } })
|
|
}}
|
|
autoFocus
|
|
/>
|
|
</div>
|
|
<div style={fieldCont}>
|
|
<div>
|
|
X:
|
|
</div>
|
|
<Input
|
|
rootStyles={posInputStyle}
|
|
defaultValue={warp.x ?? 100}
|
|
onChange={(e) => {
|
|
if (!e.target) return
|
|
setWarp(prev => { return { ...prev, x: Number(e.target.value) } })
|
|
}}
|
|
/>
|
|
<div>
|
|
Z:
|
|
</div>
|
|
<Input
|
|
rootStyles={posInputStyle}
|
|
defaultValue={warp.z ?? 100}
|
|
onChange={(e) => {
|
|
if (!e.target) return
|
|
setWarp(prev => { return { ...prev, z: Number(e.target.value) } })
|
|
}}
|
|
/>
|
|
</div>
|
|
<div style={fieldCont}>
|
|
<div>Color:</div>
|
|
<Input
|
|
type='color'
|
|
defaultValue={warp.color === '' ? '#232323' : warp.color}
|
|
onChange={(e) => {
|
|
if (!e.target) return
|
|
setWarp(prev => { return { ...prev, color: e.target.value } })
|
|
}}
|
|
rootStyles={{ width: '30px', }}
|
|
style={{ left: '0px' }}
|
|
/>
|
|
</div>
|
|
<div style={fieldCont} >
|
|
<label htmlFor='warp-disabled'>Disabled:</label>
|
|
<input
|
|
id='warp-disabled'
|
|
type="checkbox"
|
|
checked={warp.disabled ?? false}
|
|
onChange={(e) => {
|
|
if (!e.target) return
|
|
setWarp(prev => { return { ...prev, disabled: e.target.checked } })
|
|
}}
|
|
/>
|
|
</div>
|
|
<Button
|
|
style={{ alignSelf: 'center' }}
|
|
onClick={() => {
|
|
quickTp()
|
|
}}
|
|
>Quick TP</Button>
|
|
<div style={fieldCont}>
|
|
<Button
|
|
onClick={() => {
|
|
setIsWarpInfoOpened(false)
|
|
}}
|
|
>Cancel</Button>
|
|
<Button
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
adapter.setWarp({ ...warp })
|
|
console.log(adapter.warps)
|
|
setIsWarpInfoOpened(false)
|
|
updateChunk()
|
|
afterWarpIsSet?.()
|
|
}}
|
|
type='submit'
|
|
>Add Warp</Button>
|
|
{initWarp && <Button
|
|
onClick={() => {
|
|
const index = adapter.warps.findIndex(thisWarp => thisWarp.name === warp.name)
|
|
if (index !== -1) {
|
|
adapter.setWarp({ name: warp.name, x: 0, y: 0, z: 0, color: '', disabled: false, world: '' }, true)
|
|
setIsWarpInfoOpened(false)
|
|
updateChunk()
|
|
afterWarpIsSet?.()
|
|
}
|
|
}}
|
|
>Delete</Button>}
|
|
</div>
|
|
</form>
|
|
</div>
|
|
}
|