diff --git a/leaky-ships/components/BorderTiles.tsx b/leaky-ships/components/BorderTiles.tsx index df4e7b7..341efbe 100644 --- a/leaky-ships/components/BorderTiles.tsx +++ b/leaky-ships/components/BorderTiles.tsx @@ -46,7 +46,7 @@ function BorderTiles({ props: { count, setTarget, setTargetPreviewPos, hits, Dis return; setTargetPreviewPos(e => ({ ...e, shouldShow: false })) setTarget(t => { - if (t.x === x && t.y === y) { + if (t.x === x && t.y === y && t.show) { DispatchHits({ type: 'fireMissle', payload: { hit: (x + y) % 2 !== 0, x, y } }); return { show: false, x, y }; } else { diff --git a/leaky-ships/components/Gamefield.tsx b/leaky-ships/components/Gamefield.tsx index 7c6601b..abea1db 100644 --- a/leaky-ships/components/Gamefield.tsx +++ b/leaky-ships/components/Gamefield.tsx @@ -1,12 +1,11 @@ +import { CSSProperties } from 'react'; // import Bluetooth from './Bluetooth'; import BorderTiles from './BorderTiles'; // import FogImages from './FogImages'; import HitElems from './HitElems'; import Labeling from './Labeling'; import Ships from './Ships'; -import Item from './Item'; import useGameEvent from './useGameEvent'; -import { CSSProperties } from 'react'; function Gamefield() { @@ -19,7 +18,7 @@ function Gamefield() { setTargetPreviewPos, hits, DispatchHits - } = useGameEvent(); + } = useGameEvent(count); return (
diff --git a/leaky-ships/components/Item.tsx b/leaky-ships/components/Item.tsx index dcac540..be562dd 100644 --- a/leaky-ships/components/Item.tsx +++ b/leaky-ships/components/Item.tsx @@ -1,14 +1,14 @@ import React from 'react' -function Item({ props: { icon, text, cllFn } }: { +function Item({ props: { icon, text, callback } }: { props: { icon: string, text: string, - cllFn: () => void, + callback: () => void } }) { return ( -
+
{`${icon}.png`} {text}
diff --git a/leaky-ships/components/Target.tsx b/leaky-ships/components/Target.tsx index 2e83f4a..085ad8d 100644 --- a/leaky-ships/components/Target.tsx +++ b/leaky-ships/components/Target.tsx @@ -1,10 +1,11 @@ import { faCrosshairs } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { CSSProperties } from 'react'; +import classNames from 'classnames'; -function Target({ preview, target: { x, y, show } }: { preview?: boolean, target: { x: number, y: number, show: boolean } }) { +function Target({ props: { preview, type, edges }, target: { x, y, show } }: { props: { preview?: boolean, type: string, edges: string[] }, target: { x: number, y: number, show: boolean } }) { return ( -
+
) diff --git a/leaky-ships/components/useGameEvent.tsx b/leaky-ships/components/useGameEvent.tsx index 15a67a3..2c05269 100644 --- a/leaky-ships/components/useGameEvent.tsx +++ b/leaky-ships/components/useGameEvent.tsx @@ -1,10 +1,10 @@ -import { useEffect, useMemo, useReducer, useState } from 'react'; +import { useCallback, useEffect, useMemo, useReducer, useState } from 'react'; import { hitReducer, initlialLastLeftTile, initlialTarget, initlialTargetPreview, initlialTargetPreviewPos, isHit } from '../helpers'; import { HitType, LastLeftTileType, TargetPreviewPosType, TargetPreviewType, TargetType } from '../interfaces'; import Item from './Item'; import Target from './Target'; -function useGameEvent() { +function useGameEvent(count: number) { const [lastLeftTile, setLastLeftTile] = useState(initlialLastLeftTile); const [target, setTarget] = useState(initlialTarget); const [eventReady, setEventReady] = useState(false); @@ -12,20 +12,118 @@ function useGameEvent() { const [targetPreview, setTargetPreview] = useState(initlialTargetPreview); const [targetPreviewPos, setTargetPreviewPos] = useState(initlialTargetPreviewPos); const [hits, DispatchHits] = useReducer(hitReducer, [] as HitType[]); + const [mode, setMode] = useState('none') + const [targetList, setTargetList] = useState<{ + show: boolean; + x: number; + y: number; + edges: string[]; + }[]>([]) + const [targetPreviewList, setTargetPreviewList] = useState<{ + show: boolean; + x: number; + y: number; + edges: string[]; + }[]>([]) + + const modes = useMemo(() => ({ + none: { xEnable: true, yEnable: true, type: 'none' }, + radar: { xEnable: true, yEnable: true, type: 'radar' }, + hTorpedo: { xEnable: true, yEnable: false, type: 'torpedo' }, + vTorpedo: { xEnable: false, yEnable: true, type: 'torpedo' }, + missle: { xEnable: false, yEnable: false, type: 'missle' } + }), []) + + function modXY(e: { show: boolean, x: number, y: number }, mod: { x: number, y: number, edges: string[] }) { + return { show: e.show, x: e.x + (mod.x ?? 0), y: e.y + (mod.y ?? 0), edges: mod.edges } + } + + const isSet = useCallback((x: number, y: number) => targetList.filter(target => x === target.x && y === target.y).length && target.show, [targetList, target]) + + const scopeGrid = useMemo(() => { + const { xEnable, yEnable, type } = modes[mode] + const matrix: { x: number, y: number, edges: string[] }[][] = [] + let y = 0 + let x = 0 + const yLength = (yEnable ? 2 : 0) + const xLength = (xEnable ? 2 : 0) + for (let i = 0; i <= yLength; i++) { + for (let i2 = 0; i2 <= xLength; i2++) { + y = i + (yEnable ? -1 : 0); + x = i2 + (xEnable ? -1 : 0); + + (matrix[i] ??= [])[i2] = { + x, + y, + edges: [ + i2 === 0 ? 'left' : '', + i2 === xLength ? 'right' : '', + i === 0 ? 'top' : '', + i === yLength ? 'bottom' : '', + ] + } + } + } + const fields = matrix.reduce((prev, curr) => [...prev, ...curr], []) + return { fields, type } + }, [modes, mode]) + + const Targets = useCallback((targets: { show: boolean, x: number, y: number, edges: string[] }[], preview?: boolean) => { + const { type } = scopeGrid + return targets.map(({ edges, ...target }, i) => ) + }, [scopeGrid, mode]) + + useEffect(() => { + const { fields } = scopeGrid + const result = fields.map(e => modXY(target, e)) + .filter(({ x, y }) => { + const border = [ + x < 2, + x > count, + y < 2, + y > count, + ].reduce((prev, curr) => prev || curr, false) + // console.log(!isHit(hits, x, y).length, !borders) + return !isHit(hits, x, y).length && !border + }) + setTargetList(e => { + if (JSON.stringify(e) === JSON.stringify(result)) + return e + return result + }) + }, [scopeGrid, target, count, hits]); + + useEffect(() => { + const { fields } = scopeGrid + const result = fields.map(e => modXY(targetPreview, e)) + .filter(({ x, y }) => { + const border = [ + x < 2, + x > count + 1, + y < 2, + y > count + 1, + ].reduce((prev, curr) => prev || curr, false) + // console.log(!isHit(hits, x, y).length, !isSet(x, y), !borders) + return !isHit(hits, x, y).length && !isSet(x, y) && !border + }) + setTargetPreviewList(e => { + if (JSON.stringify(e) === JSON.stringify(result)) + return e + return result + }) + }, [scopeGrid, targetPreview, count, hits, isSet]); // handle visibility and position change of targetPreview useEffect(() => { const { show, x, y } = targetPreview; - const { shouldShow } = targetPreviewPos; // 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 if (show && !appearOK) setTargetPreview(e => ({ ...e, show: false })); - if (!show && shouldShow && eventReady && appearOK && !isHit(hits, x, y).length && !hasLeft && !isSet) + if (!show && targetPreviewPos.shouldShow && eventReady && appearOK && !isHit(hits, x, y).length && !hasLeft) setTargetPreview(e => ({ ...e, show: true })); - }, [targetPreview, hits, eventReady, appearOK, lastLeftTile]) + }, [targetPreview, targetPreviewPos.shouldShow, hits, eventReady, appearOK, lastLeftTile]) // enable targetPreview event again after 200 mil. sec. useEffect(() => { @@ -37,7 +135,7 @@ function useGameEvent() { setTargetPreview(e => ({ ...e, x: newX, y: newY })); setEventReady(true); setAppearOK(true); - }, 250); + }, 300); // or abort if state has changed early return () => { @@ -58,27 +156,27 @@ function useGameEvent() { } }, [targetPreview.show]); - const items = [ - { icon: 'burger-menu', text: 'Menu', cllFn: () => { } }, - { icon: 'radar', text: 'Radar scan', cllFn: () => { } }, - { icon: 'missle', text: 'Fire torpedo', cllFn: () => { } }, - { icon: 'scope', text: 'Fire missle', cllFn: () => { } }, - { icon: 'gear', text: 'Settings', cllFn: () => { } } - ] - - const targets = useMemo(() => - <> - - - , [target, targetPreview]) - - const eventBar = useMemo(() => -
- {items.map((e, i) => ( - - ))} -
, [items]) + const targets = useMemo(() => <> + {Targets(targetPreviewList, true)} + {Targets(targetList)} + , [Targets, targetList, targetPreviewList]) + const eventBar = useMemo(() => { + const items = [ + { icon: 'burger-menu', text: 'Menu' }, + { icon: 'radar', text: 'Radar scan', type: 'radar' }, + { icon: 'missle', text: 'Fire torpedo', type: 'hTorpedo' }, + { icon: 'scope', text: 'Fire missle', type: 'missle' }, + { icon: 'gear', text: 'Settings' } + ] + return ( +
+ {items.map((e, i) => ( + { setMode(e.type as any); setTarget(e => ({ ...e, show: false })) } }} /> + ))} +
+ ) + }, []) return { targets, eventBar, diff --git a/leaky-ships/package-lock.json b/leaky-ships/package-lock.json index b9054dc..00f43bd 100644 --- a/leaky-ships/package-lock.json +++ b/leaky-ships/package-lock.json @@ -16,6 +16,7 @@ "@types/node": "18.11.18", "@types/react": "18.0.26", "@types/react-dom": "18.0.10", + "classnames": "^2.3.2", "eslint": "8.31.0", "eslint-config-next": "13.1.1", "next": "13.1.1", @@ -889,6 +890,11 @@ "node": ">= 6" } }, + "node_modules/classnames": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", + "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -4008,6 +4014,11 @@ } } }, + "classnames": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", + "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" + }, "client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", diff --git a/leaky-ships/package.json b/leaky-ships/package.json index 9c9ef1c..539f794 100644 --- a/leaky-ships/package.json +++ b/leaky-ships/package.json @@ -17,6 +17,7 @@ "@types/node": "18.11.18", "@types/react": "18.0.26", "@types/react-dom": "18.0.10", + "classnames": "^2.3.2", "eslint": "8.31.0", "eslint-config-next": "13.1.1", "next": "13.1.1", diff --git a/leaky-ships/styles/App.scss b/leaky-ships/styles/App.scss index 5e36fce..73b72ee 100644 --- a/leaky-ships/styles/App.scss +++ b/leaky-ships/styles/App.scss @@ -154,14 +154,16 @@ body { color: red; } } - + &.target { - color: red; + --color: red; + color: var(--color); opacity: 0; } &.target-preview { - color: orange; + --color: orange; + color: var(--color); opacity: 0; @include transition(.5s); } @@ -169,6 +171,38 @@ body { &.show { opacity: 1; } + + &.left { + border-left: 2px solid var(--color); + } + + &.right { + border-right: 2px solid var(--color); + } + + &.top { + border-top: 2px solid var(--color); + } + + &.bottom { + border-bottom: 2px solid var(--color); + } + + &.left.top { + border-top-left-radius: 8px; + } + + &.right.top { + border-top-right-radius: 8px; + } + + &.left.bottom { + border-bottom-left-radius: 8px; + } + + &.right.bottom { + border-bottom-right-radius: 8px; + } } .r2 {