222 lines
6.1 KiB
TypeScript
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
|