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(initlialLastLeftTile) const [target, setTarget] = useState(initlialTarget) const [eventReady, setEventReady] = useState(false) const [appearOK, setAppearOK] = useState(false) const [targetPreview, setTargetPreview] = useState( initlialTargetPreview ) const [mouseCursor, setMouseCursor] = useState(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