Fixed targetPreview

This commit is contained in:
aronmal 2023-05-24 10:10:56 +02:00
parent 986555a368
commit b3695916a0
Signed by: aronmal
GPG key ID: 816B7707426FC612
15 changed files with 237 additions and 302 deletions

View file

@ -1,23 +1,22 @@
import { Position, MouseCursor } from "../../interfaces/frontend"
import { MouseCursor } from "../../interfaces/frontend"
import { count } from "./Gamefield"
import { borderCN, cornerCN, fieldIndex } from "@lib/utils/helpers"
import { CSSProperties, Dispatch, SetStateAction } from "react"
type TilesType = {
key: number
isGameTile: boolean
classNameString: string
className: string
x: number
y: number
}
function BorderTiles({
props: { count, settingTarget, setMouseCursor, setLastLeftTile },
props: { settingTarget, setMouseCursor },
}: {
props: {
count: number
settingTarget: (isGameTile: boolean, x: number, y: number) => void
setMouseCursor: Dispatch<SetStateAction<MouseCursor>>
setLastLeftTile: Dispatch<SetStateAction<Position>>
}
}) {
let tilesProperties: TilesType[] = []
@ -31,10 +30,10 @@ function BorderTiles({
const classNames = ["border-tile"]
if (borderType) classNames.push("edge", borderType)
if (isGameTile) classNames.push("game-tile")
const classNameString = classNames.join(" ")
const className = classNames.join(" ")
tilesProperties.push({
key,
classNameString,
className,
isGameTile,
x: x + 1,
y: y + 1,
@ -44,17 +43,16 @@ function BorderTiles({
return (
<>
{tilesProperties.map(({ key, classNameString, isGameTile, x, y }) => {
{tilesProperties.map(({ key, className, isGameTile, x, y }) => {
return (
<div
key={key}
className={classNameString}
className={className}
style={{ "--x": x, "--y": y } as CSSProperties}
onClick={() => settingTarget(isGameTile, x, y)}
onMouseEnter={() =>
setMouseCursor({ x, y, shouldShow: isGameTile })
}
onMouseLeave={() => setLastLeftTile({ x, y })}
></div>
)
})}

View file

@ -1,35 +1,98 @@
// import Bluetooth from './Bluetooth'
// import FogImages from './FogImages'
import { Hit, MouseCursor, Target } from "../../interfaces/frontend"
import Labeling from "./Labeling"
import Ships from "./Ships"
import useGameEvent from "@hooks/useGameEvent"
import { CSSProperties } from "react"
import BorderTiles from "@components/Gamefield/BorderTiles"
import EventBar from "@components/Gamefield/EventBar"
import HitElems from "@components/Gamefield/HitElems"
import Targets from "@components/Gamefield/Targets"
import {
hitReducer,
initlialTarget,
initlialTargetPreview,
initlialMouseCursor,
overlapsWithAnyBorder,
isHit,
composeTargetTiles,
} from "@lib/utils/helpers"
import { CSSProperties, useCallback } from "react"
import { useEffect, useReducer, useState } from "react"
export const count = 12
function Gamefield() {
const count = 12
const { BorderTiles, HitElems, Targets, EventBar } = useGameEvent(count)
const [target, setTarget] = useState<Target>(initlialTarget)
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 settingTarget = useCallback(
(isGameTile: boolean, x: number, y: number) => {
if (!isGameTile || isHit(x, y, hits).length) return
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 = { show: true, x, y }
if (overlapsWithAnyBorder(target, mode)) return t
return target
}
})
},
[hits, mode]
)
useEffect(() => {
console.log(1)
const { x, y, show } = target
const { shouldShow, ...position } = mouseCursor
if (!shouldShow || overlapsWithAnyBorder(position, mode))
setTargetPreview((e) => ({ ...e, show: false }))
else {
console.log(2, position)
setTargetPreview({
...position,
show: !show || x !== position.x || y !== position.y,
})
}
}, [mode, mouseCursor, target])
return (
<div id="gamefield">
{/* <Bluetooth /> */}
<div id="game-frame" style={{ "--i": count } as CSSProperties}>
<div
id="game-frame"
style={{ "--i": count } as CSSProperties}
onMouseLeave={() =>
setMouseCursor((e) => ({ ...e, shouldShow: false }))
}
>
{/* Bordes */}
<BorderTiles />
<BorderTiles props={{ settingTarget, setMouseCursor }} />
{/* Collumn lettes and row numbers */}
<Labeling count={count} />
{/* Ships */}
<Ships />
<HitElems />
<HitElems hits={hits} />
{/* Fog images */}
{/* <FogImages /> */}
<Targets />
<Targets props={{ target, targetPreview, mode, hits }} />
{/* <span id='dev-debug' style={{gridArea: '1 / 12 / 1 / 15', backgroundColor: 'red', zIndex: 3} as CSSProperties}>Debug</span> */}
</div>
<EventBar />
<EventBar props={{ setMode, setTarget }} />
</div>
)
}

View file

@ -10,9 +10,11 @@ export interface PointerProps extends Target, TargetList {
}
function GamefieldPointer({
props: { preview, x, y, show, type, edges, imply },
props: { x, y, show, type, edges, imply },
preview,
}: {
props: PointerProps
preview?: boolean
}) {
const isRadar = type === "radar"
const style = !(isRadar && !edges.filter((s) => s).length)

View file

@ -1,24 +1,26 @@
import { Target } from "../../interfaces/frontend"
import GamefieldPointer, { PointerProps } from "./GamefieldPointer"
import { Hit, Target } from "../../interfaces/frontend"
import GamefieldPointer from "./GamefieldPointer"
import { composeTargetTiles } from "@lib/utils/helpers"
import React from "react"
function Targets({
props: { composeTargetTiles, target, targetPreview },
props: { target, targetPreview, mode, hits },
}: {
props: {
composeTargetTiles: (target: Target) => PointerProps[]
target: Target
targetPreview: Target
mode: number
hits: Hit[]
}
}) {
return (
<>
{[
...composeTargetTiles(target).map((props, i) => (
...composeTargetTiles(target, mode, hits).map((props, i) => (
<GamefieldPointer key={"t" + i} props={props} />
)),
...composeTargetTiles(targetPreview).map((props, i) => (
<GamefieldPointer key={"p" + i} props={props} />
...composeTargetTiles(targetPreview, mode, hits).map((props, i) => (
<GamefieldPointer key={"p" + i} props={props} preview />
)),
]}
</>

View file

@ -9,9 +9,26 @@ import { useSession } from "next-auth/react"
import { useRouter } from "next/router"
import { Fragment, useEffect, useState } from "react"
function WithDots({ children }: { children: string }) {
const [dots, setDots] = useState(3)
useEffect(() => {
const interval = setInterval(() => setDots((e) => (e % 3) + 1), 1000)
return () => clearInterval(interval)
}, [])
return (
<>
{children + " "}
{Array.from(Array(dots), () => ".").join("")}
{Array.from(Array(3 - dots), (_, i) => (
<Fragment key={i}>&nbsp;</Fragment>
))}
</>
)
}
function LobbyFrame({ openSettings }: { openSettings: () => void }) {
const { payload, full, leave, reset } = useGameProps()
const [dots, setDots] = useState(3)
const { isConnected } = useSocket()
const router = useRouter()
const { data: session } = useSession()
@ -21,12 +38,6 @@ function LobbyFrame({ openSettings }: { openSettings: () => void }) {
socket.emit("update", full)
}, [full, payload?.game?.id, isConnected])
useEffect(() => {
if (payload?.player2) return
const interval = setInterval(() => setDots((e) => (e % 3) + 1), 1000)
return () => clearInterval(interval)
}, [payload?.player2])
return (
<div className="mx-32 flex flex-col self-stretch rounded-3xl bg-gray-400">
<div className="flex items-center justify-between border-b-2 border-slate-900">
@ -58,11 +69,9 @@ function LobbyFrame({ openSettings }: { openSettings: () => void }) {
/>
) : (
<p className="font-farro w-96 text-center text-4xl font-medium">
Warte auf {isConnected ? "Spieler 2" : "Verbindung"}{" "}
{Array.from(Array(dots), () => ".").join("")}
{Array.from(Array(3 - dots), (_, i) => (
<Fragment key={i}>&nbsp;</Fragment>
))}
<WithDots>
{"Warte auf " + (isConnected ? "Spieler 2" : "Verbindung")}
</WithDots>
</p>
)}
</div>

View file

@ -14,7 +14,7 @@ function Player({
}) {
const text =
player?.name ?? "Spieler " + (player?.index === "player2" ? "2" : "1")
const primary = userId === player?.id
const primary = userId && userId === player?.id
return (
<div className="flex w-96 flex-col items-center gap-16 py-8">

View file

@ -1,235 +0,0 @@
import {
Hit,
Mode,
MouseCursor,
Target,
Position,
} from "../interfaces/frontend"
import BorderTiles from "@components/Gamefield/BorderTiles"
import EventBar from "@components/Gamefield/EventBar"
import type { PointerProps } from "@components/Gamefield/GamefieldPointer"
import HitElems from "@components/Gamefield/HitElems"
import Targets from "@components/Gamefield/Targets"
import {
hitReducer,
initlialLastLeftTile,
initlialTarget,
initlialTargetPreview,
initlialMouseCursor,
} from "@lib/utils/helpers"
import { useCallback, useEffect, useReducer, useState } from "react"
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: Array.from(Array(1), () => Array.from(Array(1))),
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 {
BorderTiles: () => (
<BorderTiles
props={{ count, settingTarget, setMouseCursor, setLastLeftTile }}
/>
),
HitElems: () => <HitElems hits={hits} />,
Targets: () => (
<Targets props={{ composeTargetTiles, target, targetPreview }} />
),
EventBar: () => <EventBar props={{ setMode, setTarget }} />,
}
}
function isBorder(x: number, y: number, count: number) {
return x < 2 || x > count + 1 || y < 2 || y > count + 1
}
export default useGameEvent

View file

@ -3,7 +3,6 @@ export interface Position {
y: number
}
export interface Target extends Position {
preview: boolean
show: boolean
}
export interface MouseCursor extends Position {

View file

@ -1,4 +1,13 @@
import type { Hit, HitDispatch } from "../../interfaces/frontend"
import type {
Hit,
HitDispatch,
Mode,
Position,
Target,
TargetList,
} from "../../interfaces/frontend"
import { count } from "@components/Gamefield/Gamefield"
import { PointerProps } from "@components/Gamefield/GamefieldPointer"
export function borderCN(count: number, x: number, y: number) {
if (x === 0) return "left"
@ -28,10 +37,92 @@ export function hitReducer(formObject: Hit[], action: HitDispatch) {
return formObject
}
}
export const initlialLastLeftTile = {
x: 0,
y: 0,
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: Array.from(Array(1), () => Array.from(Array(1))),
type: "missile",
},
]
function isBorder(x: number, y: number, count: number) {
return x < 2 || x > count + 1 || y < 2 || y > count + 1
}
export function isHit(x: number, y: number, hits: Hit[]) {
return hits.filter((h) => h.x === x && h.y === y)
}
function isSet(x: number, y: number, targetList: TargetList[], show: boolean) {
return (
!!targetList.filter((field) => x === field.x && y === field.y).length &&
show
)
}
function targetList(
{ x: targetX, y: targetY }: Position,
mode: number
): TargetList[] {
const { pointerGrid, type } = modes[mode]
const xLength = pointerGrid.length
const yLength = pointerGrid[0].length
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], [])
}
export function overlapsWithAnyBorder(target: Position, mode: number) {
return targetList(target, mode).filter(({ x, y }) => isBorder(x, y, count))
.length
}
export function composeTargetTiles(
target: Target,
mode: number,
hits: Hit[]
): PointerProps[] {
const { show } = target
return targetList(target, mode).map((targetItem) => {
const { x, y } = targetItem
return {
...targetItem,
show,
imply: !!isHit(x, y, hits).length,
}
})
}
export const initlialTarget = {
preview: false,
show: false,

View file

@ -43,6 +43,11 @@ export default async function create(
create: {
userId: id,
index: "player1",
chats: {
create: {
event: "created",
},
},
},
},
},

View file

@ -119,7 +119,6 @@ const SocketHandler = async (
})
let body: GamePropsSchema
if (user_Game.index === "player1" && enemy) {
console.log(1)
body = composeBody(
(
await prisma.user_Game.update({
@ -140,7 +139,6 @@ const SocketHandler = async (
).game
)
} else {
console.log(2)
const game = await prisma.game.findUnique({
where: {
id: socket.data.gameId,
@ -151,7 +149,6 @@ const SocketHandler = async (
body = composeBody(game)
}
const { payload, hash } = body
console.log(payload?.player1, payload?.player2)
if (!payload || !hash) return cb(false)
io.to(socket.data.gameId).emit(
"playerEvent",

View file

@ -16,6 +16,12 @@ export default function Lobby() {
<meta name="description" content="Generated by create next app" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
<link
rel="preload"
href="/fonts/cpfont_ote/CP Font.otf"
as="font"
type="font/woff2"
/>
</Head>
<div
className={classNames(

View file

@ -15,8 +15,11 @@ import { toast } from "react-toastify"
import { Icons } from "react-toastify"
export function isAuthenticated(res: Response) {
if (status[`${res.status}_CLASS`] === status.classes.SUCCESSFUL)
return res.json()
switch (status[`${res.status}_CLASS`]) {
case status.classes.SUCCESSFUL:
case status.classes.REDIRECTION:
return res.json()
}
const resStatus = status[`${res.status}_CLASS`]
if (typeof resStatus !== "string") return
@ -60,10 +63,13 @@ export default function Start() {
const gameFetch = useCallback(
async (pin?: string) => {
const gamePromise = fetch("/api/game/" + (!pin ? "create" : "join"), {
method: "POST",
body: JSON.stringify({ pin }),
})
const gameRequestPromise = fetch(
"/api/game/" + (!pin ? "create" : "join"),
{
method: "POST",
body: JSON.stringify({ pin }),
}
)
.then(isAuthenticated)
.then((game) => GamePropsSchema.parse(game))
@ -76,9 +82,9 @@ export default function Start() {
hideProgressBar: true,
closeButton: false,
})
const res = await gamePromise.catch(() =>
const res = await gameRequestPromise.catch(() =>
toast.update(toastId, {
render: "Es ist ein Fehler aufgetreten 🤯",
render: "Es ist ein Fehler aufgetreten bei der Anfrage 🤯",
type: "error",
icon: Icons.error,
theme: "colored",
@ -108,7 +114,7 @@ export default function Start() {
)
.catch(() =>
toast.update(toastId, {
render: "Es ist ein Fehler aufgetreten 🤯",
render: "Es ist ein Fehler aufgetreten beim Seiten wechsel 🤯",
type: "error",
icon: Icons.error,
theme: "colored",

View file

@ -197,7 +197,7 @@ body {
}
&.preview {
--color: lawngreen;
--color: forestgreen;
background-color: #0001;
// border: 5px dashed var(--color);
}

View file

@ -1,8 +0,0 @@
@font-face {
font-family: "CP_Font";
src: url("/fonts/cpfont_ote/CP Font.otf") format("opentype");
}
@mixin CP_Font {
font-family: "CP_Font", sans-serif;
}