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

View file

@ -7,13 +7,14 @@ import {
faSquare4, faSquare4,
} from "@fortawesome/pro-regular-svg-icons" } from "@fortawesome/pro-regular-svg-icons"
import { import {
faArrowRightFromBracket,
faBroomWide, faBroomWide,
faCheck, faCheck,
faComments, faComments,
faEye, faEye,
faEyeSlash, faEyeSlash,
faFlag,
faGlasses, faGlasses,
faLock,
faPalette, faPalette,
faReply, faReply,
faRotate, faRotate,
@ -21,14 +22,18 @@ import {
faShip, faShip,
faSparkles, faSparkles,
faSwords, faSwords,
faXmark,
} from "@fortawesome/pro-solid-svg-icons" } from "@fortawesome/pro-solid-svg-icons"
import { useDrawProps } from "@hooks/useDrawProps" import { useDrawProps } from "@hooks/useDrawProps"
import { useGameProps } from "@hooks/useGameProps" import { useGameProps } from "@hooks/useGameProps"
import useIndex from "@hooks/useIndex"
import useShips from "@hooks/useShips" import useShips from "@hooks/useShips"
import { socket } from "@lib/socket" import { socket } from "@lib/socket"
import { modes } from "@lib/utils/helpers"
import { GamePropsSchema } from "@lib/zodSchemas" import { GamePropsSchema } from "@lib/zodSchemas"
import { useSession } from "next-auth/react" import { useRouter } from "next/router"
import { useCallback, useEffect, useMemo } from "react" import { useCallback, useEffect, useMemo } from "react"
import { Icons, toast } from "react-toastify"
export function setGameSetting( export function setGameSetting(
payload: GameSettings, payload: GameSettings,
@ -47,26 +52,27 @@ export function setGameSetting(
function EventBar({ clear }: { clear: () => void }) { function EventBar({ clear }: { clear: () => void }) {
const { shouldHide, color } = useDrawProps() const { shouldHide, color } = useDrawProps()
const { data: session } = useSession() const { selfIndex, isActiveIndex, selfUser } = useIndex()
const { ships } = useShips()
const router = useRouter()
const { const {
payload, payload,
userStates,
menu, menu,
mode, mode,
setSetting, setSetting,
full, full,
target,
setTarget, setTarget,
setTargetPreview, setTargetPreview,
setIsReady, setIsReady,
reset,
} = useGameProps() } = useGameProps()
const { ships } = useShips()
const gameSetting = useCallback( const gameSetting = useCallback(
(payload: GameSettings) => setGameSetting(payload, setSetting, full), (payload: GameSettings) => setGameSetting(payload, setSetting, full),
[full, setSetting] [full, setSetting]
) )
const self = useMemo(
() => payload?.users.find((e) => e?.id === session?.user.id),
[payload?.users, session?.user.id]
)
const items = useMemo<EventBarModes>( const items = useMemo<EventBarModes>(
() => ({ () => ({
main: [ main: [
@ -82,14 +88,14 @@ function EventBar({ clear }: { clear: () => void }) {
icon: faSwords, icon: faSwords,
text: "Attack", text: "Attack",
callback: () => { callback: () => {
useGameProps.setState({ menu: "actions" }) useGameProps.setState({ menu: "moves" })
}, },
} }
: { : {
icon: faShip, icon: faShip,
text: "Ships", text: "Ships",
callback: () => { callback: () => {
useGameProps.setState({ menu: "actions" }) useGameProps.setState({ menu: "moves" })
}, },
}, },
{ {
@ -109,20 +115,21 @@ function EventBar({ clear }: { clear: () => void }) {
], ],
menu: [ menu: [
{ {
icon: faArrowRightFromBracket, icon: faFlag,
text: "Leave", text: "Surrender",
iconColor: "darkred", iconColor: "darkred",
callback: () => { callback: () => {
// router.push() useGameProps.setState({ menu: "surrender" })
}, },
}, },
], ],
actions: moves:
payload?.game?.state === "running" payload?.game?.state === "running"
? [ ? [
{ {
icon: "scope", icon: "scope",
text: "Fire missile", text: "Fire missile",
enabled: mode === 0,
callback: () => { callback: () => {
useGameProps.setState({ mode: 0 }) useGameProps.setState({ mode: 0 })
setTarget((e) => ({ ...e, show: false })) setTarget((e) => ({ ...e, show: false }))
@ -131,10 +138,11 @@ function EventBar({ clear }: { clear: () => void }) {
{ {
icon: "torpedo", icon: "torpedo",
text: "Fire torpedo", text: "Fire torpedo",
enabled: mode === 1 || mode === 2,
amount: amount:
2 - 2 -
(self?.moves.filter( ((selfUser?.moves ?? []).filter(
(e) => e.action === "htorpedo" || e.action === "vtorpedo" (e) => e.type === "htorpedo" || e.type === "vtorpedo"
).length ?? 0), ).length ?? 0),
callback: () => { callback: () => {
useGameProps.setState({ mode: 1 }) useGameProps.setState({ mode: 1 })
@ -144,9 +152,11 @@ function EventBar({ clear }: { clear: () => void }) {
{ {
icon: "radar", icon: "radar",
text: "Radar scan", text: "Radar scan",
enabled: mode === 3,
amount: amount:
1 - 1 -
(self?.moves.filter((e) => e.action === "radar").length ?? 0), ((selfUser?.moves ?? []).filter((e) => e.type === "radar")
.length ?? 0),
callback: () => { callback: () => {
useGameProps.setState({ mode: 3 }) useGameProps.setState({ mode: 3 })
setTarget((e) => ({ ...e, show: false })) 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: [ draw: [
{ icon: faBroomWide, text: "Clear", callback: clear }, { 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, clear,
color, color,
shouldHide,
gameSetting, gameSetting,
mode,
payload,
self?.moves,
session?.user.id,
setIsReady,
setTarget, setTarget,
setTargetPreview, setTargetPreview,
ships, router,
shouldHide, reset,
] ]
) )
useEffect(() => { useEffect(() => {
if ( if (
menu !== "actions" || menu !== "moves" ||
payload?.game?.state !== "starting" || payload?.game?.state !== "starting" ||
mode < 0 || mode < 0 ||
items.actions[mode].amount items.moves[mode].amount
) )
return return
const index = items.actions.findIndex((e) => e.amount) const index = items.moves.findIndex((e) => e.amount)
useGameProps.setState({ mode: index }) useGameProps.setState({ mode: index })
}, [items.actions, menu, mode, payload?.game?.state]) }, [items.moves, menu, mode, payload?.game?.state])
useEffect(() => { useEffect(() => {
useDrawProps.setState({ enable: menu === "draw" }) useDrawProps.setState({ enable: menu === "draw" })
}, [menu]) }, [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 ( return (
<div className="event-bar"> <div className="event-bar">
{menu !== "main" && ( {menu !== "main" && (
@ -295,9 +342,50 @@ function EventBar({ clear }: { clear: () => void }) {
}} }}
></Item> ></Item>
)} )}
{items[menu].map((e, i) => ( {items[menu].map((e, i) => {
<Item key={i} props={e} /> 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> </div>
) )
} }

View file

@ -9,15 +9,20 @@ import Targets from "@components/Gamefield/Targets"
import { useDraw } from "@hooks/useDraw" import { useDraw } from "@hooks/useDraw"
import { useDrawProps } from "@hooks/useDrawProps" import { useDrawProps } from "@hooks/useDrawProps"
import { useGameProps } from "@hooks/useGameProps" import { useGameProps } from "@hooks/useGameProps"
import useIndex from "@hooks/useIndex"
import useSocket from "@hooks/useSocket" import useSocket from "@hooks/useSocket"
import { socket } from "@lib/socket" import { socket } from "@lib/socket"
import { overlapsWithAnyBorder } from "@lib/utils/helpers" import { overlapsWithAnyBorder } from "@lib/utils/helpers"
import { useRouter } from "next/router"
import { CSSProperties } from "react" import { CSSProperties } from "react"
import { useEffect } from "react" import { useEffect } from "react"
import { toast } from "react-toastify"
export const count = 12 export const count = 12
function Gamefield() { function Gamefield() {
const { isActiveIndex, selfUser } = useIndex()
const router = useRouter()
const { const {
userStates, userStates,
mode, mode,
@ -27,17 +32,22 @@ function Gamefield() {
payload, payload,
setTargetPreview, setTargetPreview,
full, full,
reset,
} = useGameProps() } = useGameProps()
const { isConnected } = useSocket() const { isConnected } = useSocket()
const { canvasRef, onMouseDown, clear } = useDraw()
const { enable, color, shouldHide } = useDrawProps()
useEffect(() => { useEffect(() => {
if ( if (
payload?.game?.state !== "starting" || payload?.game?.state !== "starting" ||
userStates.reduce((prev, curr) => prev || !curr.isReady, false) userStates.reduce((prev, curr) => prev || !curr.isReady, false)
) )
return return
socket.emit("ships", selfUser?.ships ?? [])
socket.emit("gameState", "running") socket.emit("gameState", "running")
}, [payload?.game?.state, userStates]) }, [payload?.game?.state, selfUser?.ships, userStates])
useEffect(() => { useEffect(() => {
if (payload?.game?.id || !isConnected) return if (payload?.game?.id || !isConnected) return
@ -78,8 +88,18 @@ function Gamefield() {
} }
}, [mode, mouseCursor, payload?.game?.state, setTargetPreview, target]) }, [mode, mouseCursor, payload?.game?.state, setTargetPreview, target])
const { canvasRef, onMouseDown, clear } = useDraw() useEffect(() => {
const { enable, color, shouldHide } = useDrawProps() 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 ( return (
<div id="gamefield"> <div id="gamefield">
@ -98,8 +118,8 @@ function Gamefield() {
<Labeling /> <Labeling />
{/* Ships */} {/* Ships */}
<Ships />
<HitElems /> <HitElems />
{(payload?.game?.state !== "running" || !isActiveIndex) && <Ships />}
{/* Fog images */} {/* Fog images */}
{/* <FogImages /> */} {/* <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 { faCrosshairs } from "@fortawesome/pro-solid-svg-icons"
import { faRadar } from "@fortawesome/pro-thin-svg-icons" import { faRadar } from "@fortawesome/pro-thin-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import classNames from "classnames" import classNames from "classnames"
import { CSSProperties } from "react" import { CSSProperties } from "react"
export interface PointerProps extends Target, TargetList {
imply: boolean
}
function GamefieldPointer({ function GamefieldPointer({
props: { x, y, show, type, edges, imply }, props: { x, y, show, type, edges, imply },
preview, preview,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,12 +7,12 @@ import { ReactNode } from "react"
function OptionButton({ function OptionButton({
icon, icon,
action, callback,
children, children,
disabled, disabled,
}: { }: {
icon: FontAwesomeIconProps["icon"] icon: FontAwesomeIconProps["icon"]
action?: () => void callback?: () => void
children: ReactNode children: ReactNode
disabled?: boolean 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-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" : "border-4 border-dashed border-slate-600 bg-red-950"
)} )}
onClick={() => action && setTimeout(action, 200)} onClick={() => callback && setTimeout(callback, 200)}
disabled={disabled} disabled={disabled}
title={!disabled ? "" : "Please login"} title={!disabled ? "" : "Please login"}
> >

View file

@ -1,7 +1,6 @@
import { import {
ActionDispatchProps, MoveDispatchProps,
EventBarModes, EventBarModes,
Hit,
MouseCursor, MouseCursor,
ShipProps, ShipProps,
Target, Target,
@ -14,13 +13,15 @@ import {
initlialMouseCursor, initlialMouseCursor,
initlialTarget, initlialTarget,
initlialTargetPreview, initlialTargetPreview,
intersectingShip,
targetList,
} from "@lib/utils/helpers" } from "@lib/utils/helpers"
import { import {
GamePropsSchema, GamePropsSchema,
optionalGamePropsSchema, optionalGamePropsSchema,
PlayerSchema, PlayerSchema,
} from "@lib/zodSchemas" } from "@lib/zodSchemas"
import { GameState } from "@prisma/client" import { GameState, MoveType } from "@prisma/client"
import { produce } from "immer" import { produce } from "immer"
import { SetStateAction } from "react" import { SetStateAction } from "react"
import { toast } from "react-toastify" import { toast } from "react-toastify"
@ -34,16 +35,14 @@ const initialState: optionalGamePropsSchema & {
}[] }[]
menu: keyof EventBarModes menu: keyof EventBarModes
mode: number mode: number
hits: Hit[]
target: Target target: Target
targetPreview: TargetPreview targetPreview: TargetPreview
mouseCursor: MouseCursor mouseCursor: MouseCursor
} = { } = {
menu: "actions", menu: "moves",
mode: 0, mode: 0,
payload: null, payload: null,
hash: null, hash: null,
hits: [],
target: initlialTarget, target: initlialTarget,
targetPreview: initlialTargetPreview, targetPreview: initlialTargetPreview,
mouseCursor: initlialMouseCursor, mouseCursor: initlialMouseCursor,
@ -56,7 +55,7 @@ const initialState: optionalGamePropsSchema & {
export type State = typeof initialState export type State = typeof initialState
export type Action = { export type Action = {
DispatchAction: (props: ActionDispatchProps) => void DispatchMove: (props: MoveDispatchProps, i: number) => void
setTarget: (target: SetStateAction<Target>) => void setTarget: (target: SetStateAction<Target>) => void
setTargetPreview: (targetPreview: SetStateAction<TargetPreview>) => void setTargetPreview: (targetPreview: SetStateAction<TargetPreview>) => void
setMouseCursor: (mouseCursor: SetStateAction<MouseCursor>) => void setMouseCursor: (mouseCursor: SetStateAction<MouseCursor>) => void
@ -66,26 +65,54 @@ export type Action = {
leave: (cb: () => void) => void leave: (cb: () => void) => void
setIsReady: (payload: { i: number; isReady: boolean }) => void setIsReady: (payload: { i: number; isReady: boolean }) => void
gameState: (newState: GameState) => void gameState: (newState: GameState) => void
setShips: (ships: ShipProps[], userId: string) => void setShips: (ships: ShipProps[], index: number) => void
removeShip: (props: ShipProps, userId: string) => void removeShip: (props: ShipProps, index: number) => void
setIsConnected: (payload: { i: number; isConnected: boolean }) => void setIsConnected: (payload: { i: number; isConnected: boolean }) => void
reset: () => void reset: () => void
setActiveIndex: (i: number, selfIndex: number) => void
} }
export const useGameProps = create<State & Action>()( export const useGameProps = create<State & Action>()(
devtools( devtools(
(set) => ({ (set) => ({
...initialState, ...initialState,
DispatchAction: (action) => setActiveIndex: (i, selfIndex) =>
set( set(
produce((state: State) => { produce((state: State) => {
// switch (action.type) { if (!state.payload) return
// case "fireMissile": state.payload.activeIndex = i
// case "htorpedo": if (i === selfIndex) {
// case "vtorpedo": { state.menu = "moves"
// state.hits.push(...action.payload) 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) => setTarget: (dispatch) =>
@ -112,22 +139,22 @@ export const useGameProps = create<State & Action>()(
else state.mouseCursor = dispatch else state.mouseCursor = dispatch
}) })
), ),
setShips: (ships, userId) => setShips: (ships, index) =>
set( set(
produce((state: State) => { produce((state: State) => {
if (!state.payload) return if (!state.payload) return
state.payload.users = state.payload.users.map((e) => { 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 e.ships = ships
return e return e
}) })
}) })
), ),
removeShip: ({ size, variant, x, y }, userId) => removeShip: ({ size, variant, x, y }, index) =>
set( set(
produce((state: State) => { produce((state: State) => {
state.payload?.users.map((e) => { state.payload?.users.map((e) => {
if (!e || e.id !== userId) return if (!e || e.index !== index) return
const indexToRemove = e.ships.findIndex( const indexToRemove = e.ships.findIndex(
(ship) => (ship) =>
ship.size === size && 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 { isAuthenticated } from "../pages/start"
import { useGameProps } from "./useGameProps" import { useGameProps } from "./useGameProps"
import useIndex from "./useIndex"
import { socket } from "@lib/socket" import { socket } from "@lib/socket"
import { GamePropsSchema } from "@lib/zodSchemas" import { GamePropsSchema } from "@lib/zodSchemas"
import status from "http-status" import status from "http-status"
import { useSession } from "next-auth/react"
import { useRouter } from "next/router" import { useRouter } from "next/router"
import { useEffect, useMemo, useState } from "react" import { useEffect, useMemo, useState } from "react"
import { toast } from "react-toastify" 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. */ /** This function should only be called once per page, otherwise there will be multiple socket connections and duplicate event listeners. */
function useSocket() { function useSocket() {
const [isConnectedState, setIsConnectedState] = useState(false) const [isConnectedState, setIsConnectedState] = useState(false)
const { selfIndex } = useIndex()
const { const {
payload, payload,
userStates, userStates,
@ -20,28 +21,25 @@ function useSocket() {
setIsReady, setIsReady,
gameState, gameState,
setIsConnected, setIsConnected,
setActiveIndex,
DispatchMove,
setShips,
} = useGameProps() } = useGameProps()
const { data: session } = useSession()
const router = useRouter() 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( const isConnected = useMemo(
() => (isIndex ? userStates[i].isConnected : isConnectedState), () =>
[i, isConnectedState, isIndex, userStates] selfIndex >= 0 ? userStates[selfIndex].isConnected : isConnectedState,
[selfIndex, isConnectedState, userStates]
) )
useEffect(() => { useEffect(() => {
if (!isIndex) return if (selfIndex < 0) return
setIsConnected({ setIsConnected({
i, i: selfIndex,
isConnected: isConnectedState, isConnected: isConnectedState,
}) })
}, [i, isConnectedState, isIndex, setIsConnected]) }, [selfIndex, isConnectedState, setIsConnected])
useEffect(() => { useEffect(() => {
socket.on("connect", () => { socket.on("connect", () => {
@ -97,7 +95,7 @@ function useSocket() {
i, i,
isConnected: true, isConnected: true,
}) })
socket.emit("isReady", userStates[i].isReady) socket.emit("isReady", userStates[selfIndex].isReady)
message = "Player has joined the lobby." message = "Player has joined the lobby."
break break
@ -122,6 +120,12 @@ function useSocket() {
socket.on("gameState", gameState) socket.on("gameState", gameState)
socket.on("dispatchMove", DispatchMove)
socket.on("activeIndex", (i) => setActiveIndex(i, selfIndex))
socket.on("ships", setShips)
socket.on("disconnect", () => { socket.on("disconnect", () => {
console.log("disconnect") console.log("disconnect")
setIsConnectedState(false) setIsConnectedState(false)
@ -131,13 +135,17 @@ function useSocket() {
socket.removeAllListeners() socket.removeAllListeners()
} }
}, [ }, [
DispatchMove,
full, full,
gameState, gameState,
router, router,
selfIndex,
setActiveIndex,
setIsConnected, setIsConnected,
setIsReady, setIsReady,
setPlayer, setPlayer,
setSetting, setSetting,
setShips,
userStates, userStates,
]) ])
@ -150,7 +158,7 @@ function useSocket() {
.then(isAuthenticated) .then(isAuthenticated)
.then((game) => GamePropsSchema.parse(game)) .then((game) => GamePropsSchema.parse(game))
.then((res) => full(res)) .then((res) => full(res))
.catch() .catch((e) => console.log(e))
return return
} }
if (isConnected) return if (isConnected) return
@ -162,7 +170,10 @@ function useSocket() {
}) })
}, [full, isConnected, payload?.game?.id]) }, [full, isConnected, payload?.game?.id])
return { isConnected: isIndex ? userStates[i].isConnected : isConnectedState } return {
isConnected:
selfIndex >= 0 ? userStates[selfIndex].isConnected : isConnectedState,
}
} }
export default useSocket 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 { GameSettings } from "@components/Lobby/SettingsFrame/Setting"
import { GamePropsSchema, PlayerSchema } from "@lib/zodSchemas" 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 { Server as HTTPServer } from "http"
import type { Socket as NetSocket } from "net" import type { Socket as NetSocket } from "net"
import type { NextApiResponse } from "next" import type { NextApiResponse } from "next"
@ -50,7 +50,9 @@ export interface ServerToClientEvents {
"draw-line": (props: DrawLineProps, userIndex: number) => void "draw-line": (props: DrawLineProps, userIndex: number) => void
"canvas-clear": () => void "canvas-clear": () => void
gameState: (newState: GameState) => 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 { export interface ClientToServerEvents {
@ -66,6 +68,7 @@ export interface ClientToServerEvents {
"canvas-clear": () => void "canvas-clear": () => void
gameState: (newState: GameState) => void gameState: (newState: GameState) => void
ships: (ships: ShipProps[]) => void ships: (ships: ShipProps[]) => void
dispatchMove: (props: MoveDispatchProps) => void
} }
interface InterServerEvents { interface InterServerEvents {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -34,9 +34,8 @@ export const gameSelects = {
}, },
moves: { moves: {
select: { select: {
id: true,
index: true, index: true,
action: true, type: true,
x: true, x: true,
y: true, y: true,
orientation: true, orientation: true,
@ -51,6 +50,13 @@ export const gameSelects = {
orientation: true, orientation: true,
}, },
}, },
hits: {
select: {
x: true,
y: true,
hit: true,
},
},
user: { user: {
select: { select: {
id: true, id: true,
@ -102,10 +108,17 @@ export function composeBody(
...user, ...user,
})) }))
.sort((user1, user2) => user1.index - user2.index) .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 = { const payload = {
game: game, game: game,
gamePin: gamePin?.pin ?? null, gamePin: gamePin?.pin ?? null,
users, users,
activeIndex,
} }
return getPayloadwithChecksum(payload) return getPayloadwithChecksum(payload)
} }

View file

@ -214,10 +214,17 @@ const SocketHandler = async (
}, },
}) })
io.to(socket.data.gameId).emit("gameState", newState) io.to(socket.data.gameId).emit("gameState", newState)
if (newState === "running")
io.to(socket.data.gameId).emit("activeIndex", 0)
}) })
socket.on("ships", async (ships) => { 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({ await prisma.user_Game.update({
where: { where: {
gameId_userId: { 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 () => { socket.on("disconnecting", async () => {

View file

@ -1,7 +1,5 @@
import BurgerMenu from "@components/BurgerMenu" import BurgerMenu from "@components/BurgerMenu"
import Logo from "@components/Logo" import Logo from "@components/Logo"
import { faCirclePlay } from "@fortawesome/pro-thin-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { useRouter } from "next/router" import { useRouter } from "next/router"
export default function Home() { 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"> <div className="mx-auto flex h-full max-w-screen-md flex-col items-center justify-evenly">
<Logo /> <Logo />
<BurgerMenu /> <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]"> <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]">
<FontAwesomeIcon <video controls>
className="text-6xl sm:text-7xl md:text-8xl" <source src="/Regelwerk.mp4" type="video/mp4" />
icon={faCirclePlay} Your browser does not support the video tag.
/> </video>
</div> </div>
<button <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" 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(isAuthenticated)
.then((game) => GamePropsSchema.parse(game)) .then((game) => GamePropsSchema.parse(game))
const action = !pin ? "erstellt" : "angefragt" const move = !pin ? "erstellt" : "angefragt"
const toastId = "pageLoad" const toastId = "pageLoad"
toast("Raum wird " + action, { toast("Raum wird " + move, {
icon: Icons.spinner(), icon: Icons.spinner(),
toastId, toastId,
autoClose: false, autoClose: false,
@ -150,14 +150,14 @@ export default function Start() {
</button> </button>
<div className="flex flex-col items-center gap-6 sm:gap-12"> <div className="flex flex-col items-center gap-6 sm:gap-12">
<OptionButton <OptionButton
action={() => gameFetch()} callback={() => gameFetch()}
icon={faPlus} icon={faPlus}
disabled={!session} disabled={!session}
> >
Raum erstellen Raum erstellen
</OptionButton> </OptionButton>
<OptionButton <OptionButton
action={() => { callback={() => {
router.push({ router.push({
pathname: router.pathname, pathname: router.pathname,
query: { q: "join" }, query: { q: "join" },
@ -185,7 +185,7 @@ export default function Start() {
</OptionButton> </OptionButton>
<OptionButton <OptionButton
icon={faEye} icon={faEye}
action={() => { callback={() => {
router.push({ router.push({
pathname: router.pathname, pathname: router.pathname,
query: { q: "watch" }, query: { q: "watch" },

File diff suppressed because it is too large Load diff

View file

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