Working multiselect/multi trageting

This commit is contained in:
aronmal 2023-01-11 21:05:17 +01:00
parent b928510632
commit b3fdf8dbeb
Signed by: aronmal
GPG key ID: 816B7707426FC612
8 changed files with 183 additions and 39 deletions

View file

@ -46,7 +46,7 @@ function BorderTiles({ props: { count, setTarget, setTargetPreviewPos, hits, Dis
return; return;
setTargetPreviewPos(e => ({ ...e, shouldShow: false })) setTargetPreviewPos(e => ({ ...e, shouldShow: false }))
setTarget(t => { 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 } }); DispatchHits({ type: 'fireMissle', payload: { hit: (x + y) % 2 !== 0, x, y } });
return { show: false, x, y }; return { show: false, x, y };
} else { } else {

View file

@ -1,12 +1,11 @@
import { CSSProperties } from 'react';
// import Bluetooth from './Bluetooth'; // import Bluetooth from './Bluetooth';
import BorderTiles from './BorderTiles'; import BorderTiles from './BorderTiles';
// import FogImages from './FogImages'; // import FogImages from './FogImages';
import HitElems from './HitElems'; import HitElems from './HitElems';
import Labeling from './Labeling'; import Labeling from './Labeling';
import Ships from './Ships'; import Ships from './Ships';
import Item from './Item';
import useGameEvent from './useGameEvent'; import useGameEvent from './useGameEvent';
import { CSSProperties } from 'react';
function Gamefield() { function Gamefield() {
@ -19,7 +18,7 @@ function Gamefield() {
setTargetPreviewPos, setTargetPreviewPos,
hits, hits,
DispatchHits DispatchHits
} = useGameEvent(); } = useGameEvent(count);
return ( return (
<div id='gamefield'> <div id='gamefield'>

View file

@ -1,14 +1,14 @@
import React from 'react' import React from 'react'
function Item({ props: { icon, text, cllFn } }: { function Item({ props: { icon, text, callback } }: {
props: { props: {
icon: string, icon: string,
text: string, text: string,
cllFn: () => void, callback: () => void
} }
}) { }) {
return ( return (
<div className='item' onClick={cllFn}> <div className='item' onClick={callback}>
<img src={`/assets/${icon}.png`} alt={`${icon}.png`} /> <img src={`/assets/${icon}.png`} alt={`${icon}.png`} />
<span>{text}</span> <span>{text}</span>
</div> </div>

View file

@ -1,10 +1,11 @@
import { faCrosshairs } from '@fortawesome/free-solid-svg-icons'; import { faCrosshairs } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { CSSProperties } from 'react'; 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 ( return (
<div className={`hit-svg target${preview ? '-preview' : ''} ${show ? 'show' : ''}`} style={{ '--x': x, '--y': y } as CSSProperties}> <div className={classNames('hit-svg', preview ? 'target-preview' : 'target', type, { show: show }, ...edges)} style={{ '--x': x, '--y': y } as CSSProperties}>
<FontAwesomeIcon icon={faCrosshairs} /> <FontAwesomeIcon icon={faCrosshairs} />
</div> </div>
) )

View file

@ -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 { hitReducer, initlialLastLeftTile, initlialTarget, initlialTargetPreview, initlialTargetPreviewPos, isHit } from '../helpers';
import { HitType, LastLeftTileType, TargetPreviewPosType, TargetPreviewType, TargetType } from '../interfaces'; import { HitType, LastLeftTileType, TargetPreviewPosType, TargetPreviewType, TargetType } from '../interfaces';
import Item from './Item'; import Item from './Item';
import Target from './Target'; import Target from './Target';
function useGameEvent() { function useGameEvent(count: number) {
const [lastLeftTile, setLastLeftTile] = useState<LastLeftTileType>(initlialLastLeftTile); const [lastLeftTile, setLastLeftTile] = useState<LastLeftTileType>(initlialLastLeftTile);
const [target, setTarget] = useState<TargetType>(initlialTarget); const [target, setTarget] = useState<TargetType>(initlialTarget);
const [eventReady, setEventReady] = useState(false); const [eventReady, setEventReady] = useState(false);
@ -12,20 +12,118 @@ function useGameEvent() {
const [targetPreview, setTargetPreview] = useState<TargetPreviewType>(initlialTargetPreview); const [targetPreview, setTargetPreview] = useState<TargetPreviewType>(initlialTargetPreview);
const [targetPreviewPos, setTargetPreviewPos] = useState<TargetPreviewPosType>(initlialTargetPreviewPos); const [targetPreviewPos, setTargetPreviewPos] = useState<TargetPreviewPosType>(initlialTargetPreviewPos);
const [hits, DispatchHits] = useReducer(hitReducer, [] as HitType[]); const [hits, DispatchHits] = useReducer(hitReducer, [] as HitType[]);
const [mode, setMode] = useState<keyof typeof modes>('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<T>(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) => <Target key={i} props={{ type, preview, edges }} target={target} />)
}, [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 // handle visibility and position change of targetPreview
useEffect(() => { useEffect(() => {
const { show, x, y } = targetPreview; 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 // 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 hasLeft = x === lastLeftTile.x && y === lastLeftTile.y
const isSet = x === target.x && y === target.y
if (show && !appearOK) if (show && !appearOK)
setTargetPreview(e => ({ ...e, show: false })); 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 })); 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. // enable targetPreview event again after 200 mil. sec.
useEffect(() => { useEffect(() => {
@ -37,7 +135,7 @@ function useGameEvent() {
setTargetPreview(e => ({ ...e, x: newX, y: newY })); setTargetPreview(e => ({ ...e, x: newX, y: newY }));
setEventReady(true); setEventReady(true);
setAppearOK(true); setAppearOK(true);
}, 250); }, 300);
// or abort if state has changed early // or abort if state has changed early
return () => { return () => {
@ -58,27 +156,27 @@ function useGameEvent() {
} }
}, [targetPreview.show]); }, [targetPreview.show]);
const items = [ const targets = useMemo(() => <>
{ icon: 'burger-menu', text: 'Menu', cllFn: () => { } }, {Targets(targetPreviewList, true)}
{ icon: 'radar', text: 'Radar scan', cllFn: () => { } }, {Targets(targetList)}
{ icon: 'missle', text: 'Fire torpedo', cllFn: () => { } }, </>, [Targets, targetList, targetPreviewList])
{ icon: 'scope', text: 'Fire missle', cllFn: () => { } },
{ icon: 'gear', text: 'Settings', cllFn: () => { } }
]
const targets = useMemo(() =>
<>
<Target target={target} />
<Target preview={true} target={targetPreview} />
</>, [target, targetPreview])
const eventBar = useMemo(() =>
<div className='event-bar'>
{items.map((e, i) => (
<Item key={i} props={e} />
))}
</div>, [items])
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 (
<div className='event-bar'>
{items.map((e, i) => (
<Item key={i} props={{ ...e, callback: () => { setMode(e.type as any); setTarget(e => ({ ...e, show: false })) } }} />
))}
</div>
)
}, [])
return { return {
targets, targets,
eventBar, eventBar,

View file

@ -16,6 +16,7 @@
"@types/node": "18.11.18", "@types/node": "18.11.18",
"@types/react": "18.0.26", "@types/react": "18.0.26",
"@types/react-dom": "18.0.10", "@types/react-dom": "18.0.10",
"classnames": "^2.3.2",
"eslint": "8.31.0", "eslint": "8.31.0",
"eslint-config-next": "13.1.1", "eslint-config-next": "13.1.1",
"next": "13.1.1", "next": "13.1.1",
@ -889,6 +890,11 @@
"node": ">= 6" "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": { "node_modules/client-only": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "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": { "client-only": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",

View file

@ -17,6 +17,7 @@
"@types/node": "18.11.18", "@types/node": "18.11.18",
"@types/react": "18.0.26", "@types/react": "18.0.26",
"@types/react-dom": "18.0.10", "@types/react-dom": "18.0.10",
"classnames": "^2.3.2",
"eslint": "8.31.0", "eslint": "8.31.0",
"eslint-config-next": "13.1.1", "eslint-config-next": "13.1.1",
"next": "13.1.1", "next": "13.1.1",

View file

@ -154,14 +154,16 @@ body {
color: red; color: red;
} }
} }
&.target { &.target {
color: red; --color: red;
color: var(--color);
opacity: 0; opacity: 0;
} }
&.target-preview { &.target-preview {
color: orange; --color: orange;
color: var(--color);
opacity: 0; opacity: 0;
@include transition(.5s); @include transition(.5s);
} }
@ -169,6 +171,38 @@ body {
&.show { &.show {
opacity: 1; 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 { .r2 {