Working game logic

This commit is contained in:
aronmal 2023-06-16 08:22:34 +02:00
parent e0e0f0a728
commit 0a6fd88733
Signed by: aronmal
GPG key ID: 816B7707426FC612
27 changed files with 1458 additions and 718 deletions

View file

@ -1,5 +1,6 @@
import { count } from "./Gamefield"
import { useGameProps } from "@hooks/useGameProps"
import useIndex from "@hooks/useIndex"
import useShips from "@hooks/useShips"
import {
borderCN,
@ -22,12 +23,10 @@ type TilesType = {
}
function BorderTiles() {
const { activeUser } = useIndex()
const {
DispatchAction,
payload,
mode,
hits,
target,
targetPreview,
mouseCursor,
setTarget,
@ -41,17 +40,18 @@ function BorderTiles() {
const list = targetList(targetPreview, mode)
if (
!isGameTile ||
!list.filter(({ x, y }) => !isAlreadyHit(x, y, hits)).length
!list.filter(
({ x, y }) => !isAlreadyHit(x, y, activeUser?.hits ?? [])
).length
)
return
if (target.show && target.x == x && target.y == y) {
DispatchAction({
action: "missile",
...target,
if (!overlapsWithAnyBorder(targetPreview, mode))
setTarget({
show: true,
x,
y,
orientation: targetPreview.orientation,
})
setTarget((t) => ({ ...t, show: false }))
} else if (!overlapsWithAnyBorder(targetPreview, mode))
setTarget({ show: true, x, y })
} else if (
payload?.game?.state === "starting" &&
targetPreview.show &&
@ -62,15 +62,13 @@ function BorderTiles() {
}
},
[
DispatchAction,
hits,
activeUser?.hits,
mode,
payload?.game?.state,
setMouseCursor,
setShips,
setTarget,
ships,
target,
targetPreview,
]
)

View file

@ -7,13 +7,14 @@ import {
faSquare4,
} from "@fortawesome/pro-regular-svg-icons"
import {
faArrowRightFromBracket,
faBroomWide,
faCheck,
faComments,
faEye,
faEyeSlash,
faFlag,
faGlasses,
faLock,
faPalette,
faReply,
faRotate,
@ -21,14 +22,18 @@ import {
faShip,
faSparkles,
faSwords,
faXmark,
} from "@fortawesome/pro-solid-svg-icons"
import { useDrawProps } from "@hooks/useDrawProps"
import { useGameProps } from "@hooks/useGameProps"
import useIndex from "@hooks/useIndex"
import useShips from "@hooks/useShips"
import { socket } from "@lib/socket"
import { modes } from "@lib/utils/helpers"
import { GamePropsSchema } from "@lib/zodSchemas"
import { useSession } from "next-auth/react"
import { useRouter } from "next/router"
import { useCallback, useEffect, useMemo } from "react"
import { Icons, toast } from "react-toastify"
export function setGameSetting(
payload: GameSettings,
@ -47,26 +52,27 @@ export function setGameSetting(
function EventBar({ clear }: { clear: () => void }) {
const { shouldHide, color } = useDrawProps()
const { data: session } = useSession()
const { selfIndex, isActiveIndex, selfUser } = useIndex()
const { ships } = useShips()
const router = useRouter()
const {
payload,
userStates,
menu,
mode,
setSetting,
full,
target,
setTarget,
setTargetPreview,
setIsReady,
reset,
} = useGameProps()
const { ships } = useShips()
const gameSetting = useCallback(
(payload: GameSettings) => setGameSetting(payload, setSetting, full),
[full, setSetting]
)
const self = useMemo(
() => payload?.users.find((e) => e?.id === session?.user.id),
[payload?.users, session?.user.id]
)
const items = useMemo<EventBarModes>(
() => ({
main: [
@ -82,14 +88,14 @@ function EventBar({ clear }: { clear: () => void }) {
icon: faSwords,
text: "Attack",
callback: () => {
useGameProps.setState({ menu: "actions" })
useGameProps.setState({ menu: "moves" })
},
}
: {
icon: faShip,
text: "Ships",
callback: () => {
useGameProps.setState({ menu: "actions" })
useGameProps.setState({ menu: "moves" })
},
},
{
@ -109,20 +115,21 @@ function EventBar({ clear }: { clear: () => void }) {
],
menu: [
{
icon: faArrowRightFromBracket,
text: "Leave",
icon: faFlag,
text: "Surrender",
iconColor: "darkred",
callback: () => {
// router.push()
useGameProps.setState({ menu: "surrender" })
},
},
],
actions:
moves:
payload?.game?.state === "running"
? [
{
icon: "scope",
text: "Fire missile",
enabled: mode === 0,
callback: () => {
useGameProps.setState({ mode: 0 })
setTarget((e) => ({ ...e, show: false }))
@ -131,10 +138,11 @@ function EventBar({ clear }: { clear: () => void }) {
{
icon: "torpedo",
text: "Fire torpedo",
enabled: mode === 1 || mode === 2,
amount:
2 -
(self?.moves.filter(
(e) => e.action === "htorpedo" || e.action === "vtorpedo"
((selfUser?.moves ?? []).filter(
(e) => e.type === "htorpedo" || e.type === "vtorpedo"
).length ?? 0),
callback: () => {
useGameProps.setState({ mode: 1 })
@ -144,9 +152,11 @@ function EventBar({ clear }: { clear: () => void }) {
{
icon: "radar",
text: "Radar scan",
enabled: mode === 3,
amount:
1 -
(self?.moves.filter((e) => e.action === "radar").length ?? 0),
((selfUser?.moves ?? []).filter((e) => e.type === "radar")
.length ?? 0),
callback: () => {
useGameProps.setState({ mode: 3 })
setTarget((e) => ({ ...e, show: false }))
@ -191,19 +201,6 @@ function EventBar({ clear }: { clear: () => void }) {
}))
},
},
{
icon: faCheck,
text: "Done",
disabled: mode >= 0,
callback: () => {
if (!payload || !session?.user.id) return
const i = payload.users.findIndex(
(user) => session.user.id === user?.id
)
setIsReady({ isReady: true, i })
socket.emit("isReady", true)
},
},
],
draw: [
{ icon: faBroomWide, text: "Clear", callback: clear },
@ -248,39 +245,89 @@ function EventBar({ clear }: { clear: () => void }) {
}),
},
],
surrender: [
{
icon: faCheck,
text: "Yes",
iconColor: "green",
callback: async () => {
socket.emit("gameState", "aborted")
await router.push("/")
reset()
},
},
{
icon: faXmark,
text: "No",
iconColor: "red",
callback: () => {
useGameProps.setState({ menu: "main" })
},
},
],
}),
[
payload?.game?.state,
payload?.game?.allowSpectators,
payload?.game?.allowSpecials,
payload?.game?.allowChat,
payload?.game?.allowMarkDraw,
mode,
selfUser?.moves,
ships,
clear,
color,
shouldHide,
gameSetting,
mode,
payload,
self?.moves,
session?.user.id,
setIsReady,
setTarget,
setTargetPreview,
ships,
shouldHide,
router,
reset,
]
)
useEffect(() => {
if (
menu !== "actions" ||
menu !== "moves" ||
payload?.game?.state !== "starting" ||
mode < 0 ||
items.actions[mode].amount
items.moves[mode].amount
)
return
const index = items.actions.findIndex((e) => e.amount)
const index = items.moves.findIndex((e) => e.amount)
useGameProps.setState({ mode: index })
}, [items.actions, menu, mode, payload?.game?.state])
}, [items.moves, menu, mode, payload?.game?.state])
useEffect(() => {
useDrawProps.setState({ enable: menu === "draw" })
}, [menu])
useEffect(() => {
if (payload?.game?.state !== "running") return
let toastId = "otherPlayer"
if (isActiveIndex) toast.dismiss(toastId)
else
toast.info("Waiting for other player...", {
toastId,
position: "top-right",
icon: Icons.spinner(),
autoClose: false,
hideProgressBar: true,
closeButton: false,
})
// toastId = "connect_error"
// const isActive = toast.isActive(toastId)
// console.log(toastId, isActive)
// if (isActive)
// toast.update(toastId, {
// autoClose: 5000,
// })
// else
// toast.warn("Spie", { toastId })
}, [isActiveIndex, menu, payload?.game?.state])
return (
<div className="event-bar">
{menu !== "main" && (
@ -295,9 +342,50 @@ function EventBar({ clear }: { clear: () => void }) {
}}
></Item>
)}
{items[menu].map((e, i) => (
<Item key={i} props={e} />
))}
{items[menu].map((e, i) => {
if (!isActiveIndex && menu === "main" && i === 1) return
return <Item key={i} props={e} />
})}
{menu === "moves" && (
<Item
props={{
icon:
selfIndex >= 0 && userStates[selfIndex].isReady
? faLock
: faCheck,
text:
selfIndex >= 0 && userStates[selfIndex].isReady
? "unready"
: "Done",
disabled:
payload?.game?.state === "starting" ? mode >= 0 : undefined,
enabled:
payload?.game?.state === "running" && mode >= 0 && target.show,
callback: () => {
if (selfIndex < 0) return
if (payload?.game?.state === "starting") {
const isReady = !userStates[selfIndex].isReady
setIsReady({ isReady, i: selfIndex })
socket.emit("isReady", isReady)
}
if (payload?.game?.state === "running") {
const i = (selfUser?.moves ?? [])
.map((e) => e.index)
.reduce((prev, curr) => (curr > prev ? curr : prev), 0)
const props = {
type: modes[mode].type,
x: target.x,
y: target.y,
orientation: target.orientation,
index: (selfUser?.moves ?? []).length ? i + 1 : 0,
}
socket.emit("dispatchMove", props)
setTarget((t) => ({ ...t, show: false }))
}
},
}}
></Item>
)}
</div>
)
}

View file

@ -9,15 +9,20 @@ import Targets from "@components/Gamefield/Targets"
import { useDraw } from "@hooks/useDraw"
import { useDrawProps } from "@hooks/useDrawProps"
import { useGameProps } from "@hooks/useGameProps"
import useIndex from "@hooks/useIndex"
import useSocket from "@hooks/useSocket"
import { socket } from "@lib/socket"
import { overlapsWithAnyBorder } from "@lib/utils/helpers"
import { useRouter } from "next/router"
import { CSSProperties } from "react"
import { useEffect } from "react"
import { toast } from "react-toastify"
export const count = 12
function Gamefield() {
const { isActiveIndex, selfUser } = useIndex()
const router = useRouter()
const {
userStates,
mode,
@ -27,17 +32,22 @@ function Gamefield() {
payload,
setTargetPreview,
full,
reset,
} = useGameProps()
const { isConnected } = useSocket()
const { canvasRef, onMouseDown, clear } = useDraw()
const { enable, color, shouldHide } = useDrawProps()
useEffect(() => {
if (
payload?.game?.state !== "starting" ||
userStates.reduce((prev, curr) => prev || !curr.isReady, false)
)
return
socket.emit("ships", selfUser?.ships ?? [])
socket.emit("gameState", "running")
}, [payload?.game?.state, userStates])
}, [payload?.game?.state, selfUser?.ships, userStates])
useEffect(() => {
if (payload?.game?.id || !isConnected) return
@ -78,8 +88,18 @@ function Gamefield() {
}
}, [mode, mouseCursor, payload?.game?.state, setTargetPreview, target])
const { canvasRef, onMouseDown, clear } = useDraw()
const { enable, color, shouldHide } = useDrawProps()
useEffect(() => {
if (payload?.game?.state !== "aborted") return
toast.info("Enemy gave up!")
router.push("/")
reset()
}, [payload?.game?.state, reset, router])
useEffect(() => {
if (payload?.game?.id) return
const timeout = setTimeout(() => router.push("/"), 5000)
return () => clearTimeout(timeout)
}, [payload?.game?.id, router])
return (
<div id="gamefield">
@ -98,8 +118,8 @@ function Gamefield() {
<Labeling />
{/* Ships */}
<Ships />
<HitElems />
{(payload?.game?.state !== "running" || !isActiveIndex) && <Ships />}
{/* Fog images */}
{/* <FogImages /> */}

View file

@ -1,14 +1,10 @@
import { Target, TargetList } from "../../interfaces/frontend"
import { PointerProps } from "../../interfaces/frontend"
import { faCrosshairs } from "@fortawesome/pro-solid-svg-icons"
import { faRadar } from "@fortawesome/pro-thin-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import classNames from "classnames"
import { CSSProperties } from "react"
export interface PointerProps extends Target, TargetList {
imply: boolean
}
function GamefieldPointer({
props: { x, y, show, type, edges, imply },
preview,

View file

@ -1,7 +1,7 @@
import { Hit } from "../../interfaces/frontend"
import { faBurst, faXmark } from "@fortawesome/pro-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { useGameProps } from "@hooks/useGameProps"
import useIndex from "@hooks/useIndex"
import { CSSProperties } from "react"
function HitElems({
@ -9,11 +9,11 @@ function HitElems({
}: {
props?: { hits: Hit[]; colorOverride?: string }
}) {
const { hits } = useGameProps()
const { activeUser } = useIndex()
return (
<>
{(props?.hits ?? hits).map(({ hit, x, y }, i) => (
{(props?.hits ?? activeUser?.hits ?? []).map(({ hit, x, y }, i) => (
<div
key={i}
className="hit-svg"

View file

@ -2,11 +2,12 @@ import { ItemProps } from "../../interfaces/frontend"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { useDrawProps } from "@hooks/useDrawProps"
import classNames from "classnames"
import { enable } from "colors"
import React, { CSSProperties, useEffect, useRef, useState } from "react"
import { HexColorPicker } from "react-colorful"
function Item({
props: { icon, text, amount, iconColor, disabled, callback },
props: { icon, text, amount, iconColor, disabled, enabled, callback },
}: {
props: ItemProps
}) {
@ -47,7 +48,7 @@ function Item({
className={classNames("container", {
amount: typeof amount !== "undefined",
disabled: disabled || amount === 0,
enabled: disabled === false,
enabled: disabled === false || enabled,
})}
style={
typeof amount !== "undefined"

View file

@ -1,12 +1,12 @@
import Ship from "./Ship"
import useShips from "@hooks/useShips"
import useIndex from "@hooks/useIndex"
function Ships() {
const { ships } = useShips()
const { selfUser } = useIndex()
return (
<>
{ships.map((props, i) => (
{selfUser?.ships.map((props, i) => (
<Ship key={i} props={props} />
))}
</>

View file

@ -2,6 +2,7 @@ import GamefieldPointer from "./GamefieldPointer"
import HitElems from "./HitElems"
import Ship from "./Ship"
import { useGameProps } from "@hooks/useGameProps"
import useIndex from "@hooks/useIndex"
import useShips from "@hooks/useShips"
import {
composeTargetTiles,
@ -10,17 +11,22 @@ import {
} from "@lib/utils/helpers"
function Targets() {
const { payload, target, targetPreview, mode, hits } = useGameProps()
const { activeUser } = useIndex()
const { payload, target, targetPreview, mode } = useGameProps()
const { ships } = useShips()
if (payload?.game?.state === "running")
return (
<>
{[
...composeTargetTiles(target, mode, hits).map((props, i) => (
<GamefieldPointer key={"t" + i} props={props} />
)),
...composeTargetTiles(targetPreview, mode, hits).map((props, i) => (
...composeTargetTiles(target, mode, activeUser?.hits ?? []).map(
(props, i) => <GamefieldPointer key={"t" + i} props={props} />
),
...composeTargetTiles(
targetPreview,
mode,
activeUser?.hits ?? []
).map((props, i) => (
<GamefieldPointer key={"p" + i} props={props} preview />
)),
]}

View file

@ -48,7 +48,7 @@ function LobbyFrame({ openSettings }: { openSettings: () => void }) {
useEffect(() => {
if (!launching || launchTime > 0) return
socket.emit("gameState", "running")
socket.emit("gameState", "starting")
}, [launching, launchTime, router])
useEffect(() => {
@ -73,7 +73,7 @@ function LobbyFrame({ openSettings }: { openSettings: () => void }) {
payload?.game?.state === "lobby"
)
return
router.push("gamefield")
router.push("/gamefield")
})
return (

View file

@ -7,12 +7,12 @@ import { ReactNode } from "react"
function OptionButton({
icon,
action,
callback,
children,
disabled,
}: {
icon: FontAwesomeIconProps["icon"]
action?: () => void
callback?: () => void
children: ReactNode
disabled?: boolean
}) {
@ -24,7 +24,7 @@ function OptionButton({
? "border-b-4 border-shield-gray bg-voidDark active:border-b-0 active:border-t-4"
: "border-4 border-dashed border-slate-600 bg-red-950"
)}
onClick={() => action && setTimeout(action, 200)}
onClick={() => callback && setTimeout(callback, 200)}
disabled={disabled}
title={!disabled ? "" : "Please login"}
>

View file

@ -1,7 +1,6 @@
import {
ActionDispatchProps,
MoveDispatchProps,
EventBarModes,
Hit,
MouseCursor,
ShipProps,
Target,
@ -14,13 +13,15 @@ import {
initlialMouseCursor,
initlialTarget,
initlialTargetPreview,
intersectingShip,
targetList,
} from "@lib/utils/helpers"
import {
GamePropsSchema,
optionalGamePropsSchema,
PlayerSchema,
} from "@lib/zodSchemas"
import { GameState } from "@prisma/client"
import { GameState, MoveType } from "@prisma/client"
import { produce } from "immer"
import { SetStateAction } from "react"
import { toast } from "react-toastify"
@ -34,16 +35,14 @@ const initialState: optionalGamePropsSchema & {
}[]
menu: keyof EventBarModes
mode: number
hits: Hit[]
target: Target
targetPreview: TargetPreview
mouseCursor: MouseCursor
} = {
menu: "actions",
menu: "moves",
mode: 0,
payload: null,
hash: null,
hits: [],
target: initlialTarget,
targetPreview: initlialTargetPreview,
mouseCursor: initlialMouseCursor,
@ -56,7 +55,7 @@ const initialState: optionalGamePropsSchema & {
export type State = typeof initialState
export type Action = {
DispatchAction: (props: ActionDispatchProps) => void
DispatchMove: (props: MoveDispatchProps, i: number) => void
setTarget: (target: SetStateAction<Target>) => void
setTargetPreview: (targetPreview: SetStateAction<TargetPreview>) => void
setMouseCursor: (mouseCursor: SetStateAction<MouseCursor>) => void
@ -66,26 +65,54 @@ export type Action = {
leave: (cb: () => void) => void
setIsReady: (payload: { i: number; isReady: boolean }) => void
gameState: (newState: GameState) => void
setShips: (ships: ShipProps[], userId: string) => void
removeShip: (props: ShipProps, userId: string) => void
setShips: (ships: ShipProps[], index: number) => void
removeShip: (props: ShipProps, index: number) => void
setIsConnected: (payload: { i: number; isConnected: boolean }) => void
reset: () => void
setActiveIndex: (i: number, selfIndex: number) => void
}
export const useGameProps = create<State & Action>()(
devtools(
(set) => ({
...initialState,
DispatchAction: (action) =>
setActiveIndex: (i, selfIndex) =>
set(
produce((state: State) => {
// switch (action.type) {
// case "fireMissile":
// case "htorpedo":
// case "vtorpedo": {
// state.hits.push(...action.payload)
// }
// }
if (!state.payload) return
state.payload.activeIndex = i
if (i === selfIndex) {
state.menu = "moves"
state.mode = 0
} else {
state.menu = "main"
state.mode = -1
}
})
),
DispatchMove: (move, i) =>
set(
produce((state: State) => {
if (!state.payload) return
const list = targetList(move, move.type)
state.payload.users.map((e) => {
if (!e) return e
if (i === e.index) e.moves.push(move)
else if (move.type !== MoveType.radar)
e.hits.push(
...list.map(({ x, y }) => ({
hit: !!intersectingShip(e.ships, {
...move,
size: 1,
variant: 0,
}).fields.length,
x,
y,
}))
)
return e
})
})
),
setTarget: (dispatch) =>
@ -112,22 +139,22 @@ export const useGameProps = create<State & Action>()(
else state.mouseCursor = dispatch
})
),
setShips: (ships, userId) =>
setShips: (ships, index) =>
set(
produce((state: State) => {
if (!state.payload) return
state.payload.users = state.payload.users.map((e) => {
if (!e || e.id !== userId) return e
if (!e || e.index !== index) return e
e.ships = ships
return e
})
})
),
removeShip: ({ size, variant, x, y }, userId) =>
removeShip: ({ size, variant, x, y }, index) =>
set(
produce((state: State) => {
state.payload?.users.map((e) => {
if (!e || e.id !== userId) return
if (!e || e.index !== index) return
const indexToRemove = e.ships.findIndex(
(ship) =>
ship.size === size &&

View file

@ -0,0 +1,24 @@
import { useGameProps } from "./useGameProps"
import { useSession } from "next-auth/react"
function useIndex() {
const { payload } = useGameProps()
const { data: session } = useSession()
const selfIndex =
payload?.users.findIndex((e) => e?.id === session?.user.id) ?? -1
const activeIndex = payload?.activeIndex ?? -1
const isActiveIndex = selfIndex >= 0 && payload?.activeIndex === selfIndex
const selfUser = payload?.users[selfIndex]
const activeUser = payload?.users[activeIndex === 0 ? 1 : 0]
return {
selfIndex,
activeIndex,
isActiveIndex,
selfUser,
activeUser,
}
}
export default useIndex

View file

@ -0,0 +1,27 @@
import { ShipProps } from "../interfaces/frontend"
import { useGameProps } from "./useGameProps"
import useIndex from "./useIndex"
import { useCallback, useMemo } from "react"
function useShips() {
const gameProps = useGameProps()
const { selfIndex } = useIndex()
const ships = useMemo(
() =>
gameProps.payload?.users.find((e) => e?.index === selfIndex)?.ships ?? [],
[gameProps.payload?.users, selfIndex]
)
const setShips = useCallback(
(ships: ShipProps[]) => gameProps.setShips(ships, selfIndex),
[gameProps, selfIndex]
)
const removeShip = useCallback(
(ship: ShipProps) => gameProps.removeShip(ship, selfIndex),
[gameProps, selfIndex]
)
return { ships, setShips, removeShip }
}
export default useShips

View file

@ -1,28 +0,0 @@
import { ShipProps } from "../interfaces/frontend"
import { useGameProps } from "./useGameProps"
import { useSession } from "next-auth/react"
import { useCallback, useMemo } from "react"
function useShips() {
const gameProps = useGameProps()
const { data: session } = useSession()
const ships = useMemo(
() =>
gameProps.payload?.users.find((e) => e?.id === session?.user.id)?.ships ??
[],
[gameProps.payload?.users, session?.user.id]
)
const setShips = useCallback(
(ships: ShipProps[]) => gameProps.setShips(ships, session?.user.id ?? ""),
[gameProps, session?.user.id]
)
const removeShip = useCallback(
(ship: ShipProps) => gameProps.removeShip(ship, session?.user.id ?? ""),
[gameProps, session?.user.id]
)
return { ships, setShips, removeShip }
}
export default useShips

View file

@ -1,9 +1,9 @@
import { isAuthenticated } from "../pages/start"
import { useGameProps } from "./useGameProps"
import useIndex from "./useIndex"
import { socket } from "@lib/socket"
import { GamePropsSchema } from "@lib/zodSchemas"
import status from "http-status"
import { useSession } from "next-auth/react"
import { useRouter } from "next/router"
import { useEffect, useMemo, useState } from "react"
import { toast } from "react-toastify"
@ -11,6 +11,7 @@ import { toast } from "react-toastify"
/** This function should only be called once per page, otherwise there will be multiple socket connections and duplicate event listeners. */
function useSocket() {
const [isConnectedState, setIsConnectedState] = useState(false)
const { selfIndex } = useIndex()
const {
payload,
userStates,
@ -20,28 +21,25 @@ function useSocket() {
setIsReady,
gameState,
setIsConnected,
setActiveIndex,
DispatchMove,
setShips,
} = useGameProps()
const { data: session } = useSession()
const router = useRouter()
const { i, isIndex } = useMemo(() => {
const i = payload?.users.findIndex((user) => session?.user?.id === user?.id)
const isIndex = !(i === undefined || i < 0)
if (!isIndex) return { i: undefined, isIndex }
return { i, isIndex }
}, [payload?.users, session?.user?.id])
const isConnected = useMemo(
() => (isIndex ? userStates[i].isConnected : isConnectedState),
[i, isConnectedState, isIndex, userStates]
() =>
selfIndex >= 0 ? userStates[selfIndex].isConnected : isConnectedState,
[selfIndex, isConnectedState, userStates]
)
useEffect(() => {
if (!isIndex) return
if (selfIndex < 0) return
setIsConnected({
i,
i: selfIndex,
isConnected: isConnectedState,
})
}, [i, isConnectedState, isIndex, setIsConnected])
}, [selfIndex, isConnectedState, setIsConnected])
useEffect(() => {
socket.on("connect", () => {
@ -97,7 +95,7 @@ function useSocket() {
i,
isConnected: true,
})
socket.emit("isReady", userStates[i].isReady)
socket.emit("isReady", userStates[selfIndex].isReady)
message = "Player has joined the lobby."
break
@ -122,6 +120,12 @@ function useSocket() {
socket.on("gameState", gameState)
socket.on("dispatchMove", DispatchMove)
socket.on("activeIndex", (i) => setActiveIndex(i, selfIndex))
socket.on("ships", setShips)
socket.on("disconnect", () => {
console.log("disconnect")
setIsConnectedState(false)
@ -131,13 +135,17 @@ function useSocket() {
socket.removeAllListeners()
}
}, [
DispatchMove,
full,
gameState,
router,
selfIndex,
setActiveIndex,
setIsConnected,
setIsReady,
setPlayer,
setSetting,
setShips,
userStates,
])
@ -150,7 +158,7 @@ function useSocket() {
.then(isAuthenticated)
.then((game) => GamePropsSchema.parse(game))
.then((res) => full(res))
.catch()
.catch((e) => console.log(e))
return
}
if (isConnected) return
@ -162,7 +170,10 @@ function useSocket() {
})
}, [full, isConnected, payload?.game?.id])
return { isConnected: isIndex ? userStates[i].isConnected : isConnectedState }
return {
isConnected:
selfIndex >= 0 ? userStates[selfIndex].isConnected : isConnectedState,
}
}
export default useSocket

View file

@ -1,7 +1,7 @@
import { DrawLineProps, ShipProps } from "./frontend"
import { MoveDispatchProps, DrawLineProps, ShipProps } from "./frontend"
import { GameSettings } from "@components/Lobby/SettingsFrame/Setting"
import { GamePropsSchema, PlayerSchema } from "@lib/zodSchemas"
import { GameState, Ship } from "@prisma/client"
import { GameState } from "@prisma/client"
import type { Server as HTTPServer } from "http"
import type { Socket as NetSocket } from "net"
import type { NextApiResponse } from "next"
@ -50,7 +50,9 @@ export interface ServerToClientEvents {
"draw-line": (props: DrawLineProps, userIndex: number) => void
"canvas-clear": () => void
gameState: (newState: GameState) => void
ships: (ships: ShipProps[], userId: string) => void
ships: (ships: ShipProps[], index: number) => void
activeIndex: (index: number) => void
dispatchMove: (props: MoveDispatchProps, i: number) => void
}
export interface ClientToServerEvents {
@ -66,6 +68,7 @@ export interface ClientToServerEvents {
"canvas-clear": () => void
gameState: (newState: GameState) => void
ships: (ships: ShipProps[]) => void
dispatchMove: (props: MoveDispatchProps) => void
}
interface InterServerEvents {

View file

@ -7,10 +7,13 @@ export interface Position {
}
export interface Target extends Position {
show: boolean
}
export interface TargetPreview extends Target {
orientation: Orientation
}
export interface TargetPreview extends Target {}
export interface PointerProps extends TargetList {
show: boolean
imply: boolean
}
export interface MouseCursor extends Position {
shouldShow: boolean
}
@ -28,14 +31,16 @@ export interface ItemProps {
amount?: number
iconColor?: string
disabled?: boolean
enabled?: boolean
callback?: () => void
}
export interface EventBarModes {
main: ItemProps[]
menu: ItemProps[]
actions: ItemProps[]
moves: ItemProps[]
draw: ItemProps[]
settings: ItemProps[]
surrender: ItemProps[]
}
export interface Field extends Position {
field: string
@ -60,7 +65,8 @@ export interface ShipProps extends Position {
export interface IndexedPosition extends Position {
i?: number
}
export interface ActionDispatchProps extends Position {
index?: number
action: MoveType
export interface MoveDispatchProps extends Position {
index: number
type: MoveType
orientation: Orientation
}

View file

@ -2,14 +2,13 @@ import type {
Hit,
IndexedPosition,
Mode,
PointerProps,
Position,
ShipProps,
Target,
TargetList,
TargetPreview,
} from "../../interfaces/frontend"
import { count } from "@components/Gamefield/Gamefield"
import { PointerProps } from "@components/Gamefield/GamefieldPointer"
import { Orientation } from "@prisma/client"
export function borderCN(count: number, x: number, y: number) {
@ -30,7 +29,7 @@ export function fieldIndex(count: number, x: number, y: number) {
return y * (count + 2) + x
}
const modes: Mode[] = [
export const modes: Mode[] = [
{
pointerGrid: Array.from(Array(1), () => Array.from(Array(1))),
type: "missile",
@ -59,8 +58,12 @@ export function isAlreadyHit(x: number, y: number, hits: Hit[]) {
export function targetList(
{ x: targetX, y: targetY }: Position,
mode: number
modeInput: number | string
): TargetList[] {
const mode =
typeof modeInput === "number"
? modeInput
: modes.findIndex((e) => e.type === modeInput)
if (mode < 0) return []
const { pointerGrid, type } = modes[mode]
const xLength = pointerGrid.length
@ -112,6 +115,7 @@ export const initlialTarget = {
x: 2,
y: 2,
show: false,
orientation: Orientation.h,
}
export const initlialTargetPreview = {
x: 2,

View file

@ -16,11 +16,11 @@ export const PlayerSchema = z
.array(),
moves: z
.object({
id: z.string(),
index: z.number(),
action: z.nativeEnum(MoveType),
type: z.nativeEnum(MoveType),
x: z.number(),
y: z.number(),
orientation: z.nativeEnum(Orientation),
})
.array(),
ships: z
@ -32,6 +32,13 @@ export const PlayerSchema = z
orientation: z.nativeEnum(Orientation),
})
.array(),
hits: z
.object({
x: z.number(),
y: z.number(),
hit: z.boolean(),
})
.array(),
})
.nullable()

View file

@ -42,7 +42,7 @@ export default async function create(
users: {
create: {
userId: id,
index: 1,
index: 0,
chats: {
create: {
event: "created",

View file

@ -64,7 +64,7 @@ export default async function join(
data: {
gameId: game.id,
userId: id,
index: 2,
index: 1,
},
select: {
game: gameSelects,

View file

@ -34,9 +34,8 @@ export const gameSelects = {
},
moves: {
select: {
id: true,
index: true,
action: true,
type: true,
x: true,
y: true,
orientation: true,
@ -51,6 +50,13 @@ export const gameSelects = {
orientation: true,
},
},
hits: {
select: {
x: true,
y: true,
hit: true,
},
},
user: {
select: {
id: true,
@ -102,10 +108,17 @@ export function composeBody(
...user,
}))
.sort((user1, user2) => user1.index - user2.index)
let activeIndex = undefined
if (game.state === "running") {
const l1 = game.users[0].moves.length
const l2 = game.users[1].moves.length
activeIndex = l1 > l2 ? 1 : 0
}
const payload = {
game: game,
gamePin: gamePin?.pin ?? null,
users,
activeIndex,
}
return getPayloadwithChecksum(payload)
}

View file

@ -214,10 +214,17 @@ const SocketHandler = async (
},
})
io.to(socket.data.gameId).emit("gameState", newState)
if (newState === "running")
io.to(socket.data.gameId).emit("activeIndex", 0)
})
socket.on("ships", async (ships) => {
if (!socket.data.gameId || !socket.data.user?.id) return
if (
!socket.data.gameId ||
!socket.data.user?.id ||
typeof socket.data.index === "undefined"
)
return
await prisma.user_Game.update({
where: {
gameId_userId: {
@ -234,7 +241,38 @@ const SocketHandler = async (
},
},
})
socket.to(socket.data.gameId).emit("ships", ships, socket.data.user.id)
socket.to(socket.data.gameId).emit("ships", ships, socket.data.index)
})
socket.on("dispatchMove", async (props) => {
if (
!socket.data.gameId ||
!socket.data.user?.id ||
typeof socket.data.index === "undefined"
)
return
const user_Game = await prisma.user_Game
.update({
where: {
gameId_userId: {
gameId: socket.data.gameId,
userId: socket.data.user?.id,
},
},
data: {
moves: {
create: props,
},
},
select: { game: gameSelects },
})
.catch((e) => console.log(e, props))
if (!user_Game?.game) return
const game = user_Game.game
const l1 = game.users[0].moves.length
const l2 = game.users[1].moves.length
io.to(socket.data.gameId).emit("dispatchMove", props, socket.data.index)
io.to(socket.data.gameId).emit("activeIndex", l1 > l2 ? 1 : 0)
})
socket.on("disconnecting", async () => {

View file

@ -1,7 +1,5 @@
import BurgerMenu from "@components/BurgerMenu"
import Logo from "@components/Logo"
import { faCirclePlay } from "@fortawesome/pro-thin-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { useRouter } from "next/router"
export default function Home() {
@ -12,11 +10,11 @@ export default function Home() {
<div className="mx-auto flex h-full max-w-screen-md flex-col items-center justify-evenly">
<Logo />
<BurgerMenu />
<div className="flex h-36 w-64 items-center justify-center rounded-xl border-4 border-black bg-[#2227] sm:h-48 sm:w-96 md:h-72 md:w-[32rem] md:border-[6px] xl:h-[26rem] xl:w-[48rem]">
<FontAwesomeIcon
className="text-6xl sm:text-7xl md:text-8xl"
icon={faCirclePlay}
/>
<div className="flex h-36 w-64 items-center justify-center overflow-hidden rounded-xl border-8 border-black bg-[#2227] sm:h-48 sm:w-96 md:h-72 md:w-[32rem] md:border-[6px] xl:h-[26rem] xl:w-[48rem]">
<video controls>
<source src="/Regelwerk.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video>
</div>
<button
className="font-farro rounded-lg border-b-4 border-orange-400 bg-warn px-12 pb-4 pt-5 text-2xl font-bold duration-100 active:border-b-0 active:border-t-4 sm:rounded-xl sm:border-b-[6px] sm:px-14 sm:pb-5 sm:pt-6 sm:text-3xl sm:active:border-t-[6px] md:rounded-2xl md:border-b-8 md:px-20 md:pb-6 md:pt-7 md:text-4xl md:active:border-t-8 xl:px-24 xl:pb-8 xl:pt-10 xl:text-5xl"

View file

@ -73,9 +73,9 @@ export default function Start() {
.then(isAuthenticated)
.then((game) => GamePropsSchema.parse(game))
const action = !pin ? "erstellt" : "angefragt"
const move = !pin ? "erstellt" : "angefragt"
const toastId = "pageLoad"
toast("Raum wird " + action, {
toast("Raum wird " + move, {
icon: Icons.spinner(),
toastId,
autoClose: false,
@ -150,14 +150,14 @@ export default function Start() {
</button>
<div className="flex flex-col items-center gap-6 sm:gap-12">
<OptionButton
action={() => gameFetch()}
callback={() => gameFetch()}
icon={faPlus}
disabled={!session}
>
Raum erstellen
</OptionButton>
<OptionButton
action={() => {
callback={() => {
router.push({
pathname: router.pathname,
query: { q: "join" },
@ -185,7 +185,7 @@ export default function Start() {
</OptionButton>
<OptionButton
icon={faEye}
action={() => {
callback={() => {
router.push({
pathname: router.pathname,
query: { q: "watch" },

File diff suppressed because it is too large Load diff

View file

@ -68,27 +68,12 @@ model VerificationToken {
@@map("verificationtokens")
}
enum Orientation {
h
v
}
model Ship {
id String @id @default(cuid())
size Int
variant Int
x Int
y Int
orientation Orientation
user_GameId String
User_Game User_Game @relation(fields: [user_GameId], references: [id], onDelete: Cascade)
}
enum GameState {
lobby
starting
running
ended
aborted
}
model Game {
@ -112,6 +97,31 @@ model Gamepin {
game Game @relation(fields: [gameId], references: [id], onDelete: Cascade)
}
enum Orientation {
h
v
}
model Ship {
id String @id @default(cuid())
size Int
variant Int
x Int
y Int
orientation Orientation
user_GameId String
User_Game User_Game @relation(fields: [user_GameId], references: [id], onDelete: Cascade)
}
model Hit {
id String @id @default(cuid())
x Int
y Int
hit Boolean
user_GameId String
User_Game User_Game @relation(fields: [user_GameId], references: [id], onDelete: Cascade)
}
model User_Game {
id String @id @default(cuid())
createdAt DateTime @default(now())
@ -120,6 +130,7 @@ model User_Game {
index Int
moves Move[]
ships Ship[]
hits Hit[]
chats Chat[]
game Game @relation(fields: [gameId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id])
@ -129,25 +140,22 @@ model User_Game {
}
enum MoveType {
radar
htorpedo
vtorpedo
missile
vtorpedo
htorpedo
radar
}
model Move {
id String @id @default(cuid())
createdAt DateTime @default(now())
index Int
action MoveType
type MoveType
x Int
y Int
orientation Orientation
user_game_id String
user_game User_Game @relation(fields: [user_game_id], references: [id], onDelete: Cascade)
@@unique([user_game_id, index])
@@unique([action, x, y])
}
model Chat {