leaky-ships/leaky-ships/lib/hooks/useGameEvent.ts

222 lines
6.1 KiB
TypeScript

import { useCallback, useEffect, useMemo, useReducer, useState } from "react"
import {
hitReducer,
initlialLastLeftTile,
initlialTarget,
initlialTargetPreview,
initlialMouseCursor,
} from "../utils/helpers"
import {
Hit,
Mode,
MouseCursor,
Target,
Position,
} from "../../interfaces/frontend"
import { PointerProps } from "../../components/GamefieldPointer"
const modes: Mode[] = [
{
pointerGrid: Array.from(Array(3), () => Array.from(Array(3))),
type: "radar",
},
{
pointerGrid: Array.from(Array(3), () => Array.from(Array(1))),
type: "htorpedo",
},
{
pointerGrid: Array.from(Array(1), () => Array.from(Array(3))),
type: "vtorpedo",
},
{ pointerGrid: [[{ x: 0, y: 0 }]], type: "missile" },
]
function useGameEvent(count: number) {
const [lastLeftTile, setLastLeftTile] =
useState<Position>(initlialLastLeftTile)
const [target, setTarget] = useState<Target>(initlialTarget)
const [eventReady, setEventReady] = useState(false)
const [appearOK, setAppearOK] = useState(false)
const [targetPreview, setTargetPreview] = useState<Target>(
initlialTargetPreview
)
const [mouseCursor, setMouseCursor] =
useState<MouseCursor>(initlialMouseCursor)
const [hits, DispatchHits] = useReducer(hitReducer, [] as Hit[])
const [mode, setMode] = useState(0)
const targetList = useCallback(
(target: Position) => {
const { pointerGrid, type } = modes[mode]
const xLength = pointerGrid.length
const yLength = pointerGrid[0].length
const { x: targetX, y: targetY } = target
return pointerGrid
.map((arr, i) => {
return arr.map((_, i2) => {
const relativeX = -Math.floor(xLength / 2) + i
const relativeY = -Math.floor(yLength / 2) + i2
const x = targetX + (relativeX ?? 0)
const y = targetY + (relativeY ?? 0)
return {
x,
y,
type,
edges: [
i === 0 ? "left" : "",
i === xLength - 1 ? "right" : "",
i2 === 0 ? "top" : "",
i2 === yLength - 1 ? "bottom" : "",
],
}
})
})
.reduce((prev, curr) => [...prev, ...curr], [])
},
[mode]
)
const isHit = useCallback(
(x: number, y: number) => {
return hits.filter((h) => h.x === x && h.y === y)
},
[hits]
)
const settingTarget = useCallback(
(isGameTile: boolean, x: number, y: number) => {
if (!isGameTile || isHit(x, y).length) return
setMouseCursor((e) => ({ ...e, shouldShow: false }))
setTarget((t) => {
if (t.x === x && t.y === y && t.show) {
DispatchHits({
type: "fireMissile",
payload: { hit: (x + y) % 2 !== 0, x, y },
})
return { preview: false, show: false, x, y }
} else {
const target = { preview: false, show: true, x, y }
const hasAnyBorder = targetList(target).filter(({ x, y }) =>
isBorder(x, y, count)
).length
if (hasAnyBorder) return t
return target
}
})
},
[count, isHit, targetList]
)
const isSet = useCallback(
(x: number, y: number) => {
return (
!!targetList(target).filter((field) => x === field.x && y === field.y)
.length && target.show
)
},
[target, targetList]
)
const composeTargetTiles = useCallback(
(target: Target): PointerProps[] => {
const { preview, show } = target
const result = targetList(target).map(({ x, y, type, edges }) => {
return {
preview,
x,
y,
show,
type,
edges,
imply: !!isHit(x, y).length || (!!isSet(x, y) && preview),
}
})
return result
},
[isHit, isSet, targetList]
)
// handle visibility and position change of targetPreview
useEffect(() => {
const { show, x, y } = targetPreview
// if mouse has moved too quickly and last event was entering and leaving the same field, it must have gone outside the grid
const hasLeft = x === lastLeftTile.x && y === lastLeftTile.y
const isSet = x === target.x && y === target.y && target.show
if (show && !appearOK) setTargetPreview((e) => ({ ...e, show: false }))
if (
!show &&
mouseCursor.shouldShow &&
eventReady &&
appearOK &&
!isHit(x, y).length &&
!isSet &&
!hasLeft
)
setTargetPreview((e) => ({ ...e, show: true }))
}, [
targetPreview,
mouseCursor.shouldShow,
isHit,
eventReady,
appearOK,
lastLeftTile,
target,
])
// enable targetPreview event again after 200 ms.
useEffect(() => {
setEventReady(false)
const previewTarget = { x: mouseCursor.x, y: mouseCursor.y }
const hasAnyBorder = targetList(previewTarget).filter(({ x, y }) =>
isBorder(x, y, count)
).length
if (targetPreview.show || !appearOK || hasAnyBorder) return
const autoTimeout = setTimeout(() => {
setTargetPreview((e) => ({ ...e, ...previewTarget }))
setEventReady(true)
setAppearOK(true)
}, 300)
// or abort if state has changed early
return () => {
clearTimeout(autoTimeout)
}
}, [
appearOK,
count,
mouseCursor.x,
mouseCursor.y,
targetList,
targetPreview.show,
])
// approve targetPreview new position after 200 mil. sec.
useEffect(() => {
// early return to start cooldown only when about to show up
const autoTimeout = setTimeout(
() => {
setAppearOK(!targetPreview.show)
},
targetPreview.show ? 500 : 300
)
// or abort if movement is repeated early
return () => {
clearTimeout(autoTimeout)
}
}, [targetPreview.show])
return {
tilesProps: { count, settingTarget, setMouseCursor, setLastLeftTile },
pointersProps: { composeTargetTiles, target, targetPreview },
targetsProps: { setMode, setTarget },
hits,
}
}
function isBorder(x: number, y: number, count: number) {
return x < 2 || x > count + 1 || y < 2 || y > count + 1
}
export default useGameEvent