Step towards full multiplayer compatibility

This commit is contained in:
aronmal 2023-06-10 20:26:46 +02:00
parent 9895a286a3
commit 53a07b21b0
Signed by: aronmal
GPG key ID: 816B7707426FC612
16 changed files with 207 additions and 104 deletions

View file

@ -1,4 +1,4 @@
import { EventBarModes, Target } from "../../interfaces/frontend"
import { EventBarModes } from "../../interfaces/frontend"
import Item from "./Item"
import { GameSettings } from "@components/Lobby/SettingsFrame/Setting"
import {
@ -17,6 +17,7 @@ import {
import { useDrawProps } from "@hooks/useDrawProps"
import { useGameProps } from "@hooks/useGameProps"
import { socket } from "@lib/socket"
import { GamePropsSchema } from "@lib/zodSchemas"
import {
Dispatch,
SetStateAction,
@ -26,30 +27,38 @@ import {
useState,
} from "react"
export function setGameSetting(
payload: GameSettings,
setSetting: (settings: GameSettings) => string | null,
full: (payload: GamePropsSchema) => void
) {
return () => {
const hash = setSetting(payload)
socket.emit("gameSetting", payload, (newHash) => {
if (newHash === hash) return
console.log("hash", hash, newHash)
socket.emit("update", full)
})
}
}
function EventBar({
props: { setMode, setTarget, clear },
props: { setMode, clear },
}: {
props: {
setMode: Dispatch<SetStateAction<number>>
setTarget: Dispatch<SetStateAction<Target>>
clear: () => void
}
}) {
const [menu, setMenu] = useState<keyof EventBarModes>("main")
const { shouldHide, color } = useDrawProps()
const { payload, setSetting, full } = useGameProps()
const { payload, setSetting, full, setTarget } = useGameProps()
const gameSetting = useCallback(
(payload: GameSettings) => {
const hash = setSetting(payload)
socket.emit("gameSetting", payload, (newHash) => {
if (newHash === hash) return
console.log("hash", hash, newHash)
socket.emit("update", full)
})
},
(payload: GameSettings) => setGameSetting(payload, setSetting, full),
[full, setSetting]
)
const items = useMemo<EventBarModes>(
() => ({
main: [
@ -168,33 +177,31 @@ function EventBar({
icon: faGlasses,
text: "Spectators",
disabled: !payload?.game?.allowSpectators,
callback: () => {
gameSetting({ allowSpectators: !payload?.game?.allowSpectators })
},
callback: gameSetting({
allowSpectators: !payload?.game?.allowSpectators,
}),
},
{
icon: faSparkles,
text: "Specials",
disabled: !payload?.game?.allowSpecials,
callback: () => {
gameSetting({ allowSpecials: !payload?.game?.allowSpecials })
},
callback: gameSetting({
allowSpecials: !payload?.game?.allowSpecials,
}),
},
{
icon: faComments,
text: "Chat",
disabled: !payload?.game?.allowChat,
callback: () => {
gameSetting({ allowChat: !payload?.game?.allowChat })
},
callback: gameSetting({ allowChat: !payload?.game?.allowChat }),
},
{
icon: faScribble,
text: "Mark/Draw",
disabled: !payload?.game?.allowMarkDraw,
callback: () => {
gameSetting({ allowMarkDraw: !payload?.game?.allowMarkDraw })
},
callback: gameSetting({
allowMarkDraw: !payload?.game?.allowMarkDraw,
}),
},
],
}),

View file

@ -1,4 +1,4 @@
import { Hit, MouseCursor, Target } from "../../interfaces/frontend"
import { MouseCursor } from "../../interfaces/frontend"
// import Bluetooth from "./Bluetooth"
// import FogImages from "./FogImages"
import Labeling from "./Labeling"
@ -9,28 +9,29 @@ import HitElems from "@components/Gamefield/HitElems"
import Targets from "@components/Gamefield/Targets"
import { useDraw } from "@hooks/useDraw"
import { useDrawProps } from "@hooks/useDrawProps"
import { useGameProps } from "@hooks/useGameProps"
import {
hitReducer,
initlialTarget,
initlialTargetPreview,
initlialMouseCursor,
overlapsWithAnyBorder,
isAlreadyHit,
targetList,
} from "@lib/utils/helpers"
import { CSSProperties, useCallback } from "react"
import { useEffect, useReducer, useState } from "react"
import { useEffect, useState } from "react"
export const count = 12
function Gamefield() {
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 {
hits,
target,
targetPreview,
DispatchHits,
setTarget,
setTargetPreview,
} = useGameProps()
const [mode, setMode] = useState(0)
const settingTarget = useCallback(
@ -54,7 +55,7 @@ function Gamefield() {
} else if (!overlapsWithAnyBorder(targetPreview, mode))
setTarget({ show: true, x, y })
},
[hits, mode, target, targetPreview]
[DispatchHits, hits, mode, setTarget, target, targetPreview]
)
useEffect(() => {
@ -68,7 +69,7 @@ function Gamefield() {
show: !show || x !== position.x || y !== position.y,
})
}
}, [mode, mouseCursor, target])
}, [mode, mouseCursor, setTargetPreview, target])
const { canvasRef, onMouseDown, clear } = useDraw()
const { enable, color, shouldHide } = useDrawProps()
@ -113,7 +114,7 @@ function Gamefield() {
height="648"
/>
</div>
<EventBar props={{ setMode, setTarget, clear }} />
<EventBar props={{ setMode, clear }} />
</div>
)
}

View file

@ -1,29 +1,28 @@
import { Ship } from "../../interfaces/frontend"
import classNames from "classnames"
import { CSSProperties } from "react"
function Ships() {
let shipIndexes = [
{ size: 2, index: null },
{ size: 3, index: 1 },
{ size: 3, index: 2 },
{ size: 3, index: 3 },
{ size: 4, index: 1 },
{ size: 4, index: 2 },
const shipIndexes: Ship[] = [
{ size: 2, variant: 1, x: 3, y: 3 },
{ size: 3, variant: 1, x: 4, y: 3 },
{ size: 3, variant: 2, x: 5, y: 3 },
{ size: 3, variant: 3, x: 6, y: 3 },
{ size: 4, variant: 1, x: 7, y: 3 },
{ size: 4, variant: 2, x: 8, y: 3 },
]
return (
<>
{shipIndexes.map(({ size, index }, i) => {
const filename = `/assets/ship_blue_${size}x${
index ? "_" + index : ""
}.gif`
{shipIndexes.map(({ size, variant, x, y }, i) => {
const filename = `ship_blue_${size}x_${variant}.gif`
return (
<div
key={i}
className={classNames("ship", "s" + size)}
style={{ "--x": i + 3 } as CSSProperties}
style={{ "--x": x, "--y": y } as CSSProperties}
>
<img src={filename} alt={filename} />
<img src={"/assets/" + filename} alt={filename} />
</div>
)
})}

View file

@ -1,3 +1,4 @@
import { setGameSetting } from "@components/Gamefield/EventBar"
import {
faToggleLargeOff,
faToggleLargeOn,
@ -17,12 +18,12 @@ export type GameSettings = { [key in GameSettingKeys]?: boolean }
function Setting({
children,
props: { prop, gameSetting },
prop,
}: {
children: ReactNode
props: { prop: GameSettingKeys; gameSetting: (payload: GameSettings) => void }
prop: GameSettingKeys
}) {
const { payload } = useGameProps()
const { payload, setSetting, full } = useGameProps()
const state = useMemo(() => payload?.game?.[prop], [payload?.game, prop])
return (
@ -46,12 +47,15 @@ function Setting({
checked={state}
type="checkbox"
id={prop}
onChange={() => {
const payload = {
[prop]: !state,
}
gameSetting(payload)
}}
onChange={() =>
setGameSetting(
{
[prop]: !state,
},
setSetting,
full
)
}
hidden={true}
/>
</label>

View file

@ -44,15 +44,14 @@ function Settings({ closeSettings }: { closeSettings: () => void }) {
<div className="flex items-center justify-end">
<button
className="right-12 top-8 h-14 w-14"
onClick={() => {
const payload = {
onClick={() =>
gameSetting({
allowSpectators: true,
allowSpecials: true,
allowChat: true,
allowMarkDraw: true,
}
gameSetting(payload)
}}
})
}
>
<FontAwesomeIcon
className="h-full w-full text-gray-800 drop-shadow-md"
@ -62,18 +61,10 @@ function Settings({ closeSettings }: { closeSettings: () => void }) {
</button>
</div>
<div className="flex flex-col gap-8">
<Setting props={{ gameSetting, prop: "allowSpectators" }}>
Erlaube Zuschauer
</Setting>
<Setting props={{ gameSetting, prop: "allowSpecials" }}>
Erlaube spezial Items
</Setting>
<Setting props={{ gameSetting, prop: "allowChat" }}>
Erlaube den Chat
</Setting>
<Setting props={{ gameSetting, prop: "allowMarkDraw" }}>
Erlaube zeichen/makieren
</Setting>
<Setting prop="allowSpectators">Erlaube Zuschauer</Setting>
<Setting prop="allowSpecials">Erlaube spezial Items</Setting>
<Setting prop="allowChat">Erlaube den Chat</Setting>
<Setting prop="allowMarkDraw">Erlaube zeichen/makieren</Setting>
</div>
</div>
</div>

View file

@ -1,8 +1,9 @@
import { Draw, Point } from "../interfaces/frontend"
import { Draw, DrawLineProps, Point } from "../interfaces/frontend"
import { useDrawProps } from "./useDrawProps"
import { socket } from "@lib/socket"
import { useEffect, useRef, useState } from "react"
function onDraw({ prevPoint, currentPoint, ctx, color }: Draw) {
function drawLine({ prevPoint, currentPoint, ctx, color }: Draw) {
const { x: currX, y: currY } = currentPoint
const lineColor = color
const lineWidth = 5
@ -52,7 +53,7 @@ export const useDraw = () => {
const ctx = canvasRef.current?.getContext("2d")
if (!ctx || !currentPoint) return
onDraw({ ctx, currentPoint, prevPoint: prevPoint.current, color })
drawLine({ ctx, currentPoint, prevPoint: prevPoint.current, color })
prevPoint.current = currentPoint
}
@ -80,5 +81,39 @@ export const useDraw = () => {
}
}, [color, mouseDown])
useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext("2d")
if (!ctx) return
socket.on("playerEvent", (event) => {
if (!canvasRef.current?.toDataURL() || event.type !== "connect") return
console.log("sending canvas state")
socket.emit("canvas-state", canvasRef.current.toDataURL())
})
socket.on("canvas-state-from-server", (state: string, index) => {
console.log("I received the state")
const img = new Image()
img.src = state
img.onload = () => {
ctx?.drawImage(img, 0, 0)
}
})
socket.on("draw-line", ({ prevPoint, currentPoint, color }, index) => {
if (!ctx) return console.log("no ctx here")
drawLine({ prevPoint, currentPoint, ctx, color })
})
socket.on("clear", clear)
return () => {
socket.removeAllListeners()
}
})
return { canvasRef, onMouseDown, clear }
}

View file

@ -1,12 +1,15 @@
import { Hit, HitDispatch, Target } from "../interfaces/frontend"
import { GameSettings } from "@components/Lobby/SettingsFrame/Setting"
import { getPayloadwithChecksum } from "@lib/getPayloadwithChecksum"
import { socket } from "@lib/socket"
import { initlialTarget, initlialTargetPreview } from "@lib/utils/helpers"
import {
GamePropsSchema,
optionalGamePropsSchema,
PlayerSchema,
} from "@lib/zodSchemas"
import { produce } from "immer"
import { SetStateAction } from "react"
import { toast } from "react-toastify"
import { create } from "zustand"
import { devtools } from "zustand/middleware"
@ -16,9 +19,15 @@ const initialState: optionalGamePropsSchema & {
isReady: boolean
isConnected: boolean
}[]
hits: Hit[]
target: Target
targetPreview: Target
} = {
payload: null,
hash: null,
hits: [],
target: initlialTarget,
targetPreview: initlialTargetPreview,
userStates: Array.from(Array(2), () => ({
isReady: false,
isConnected: false,
@ -28,8 +37,11 @@ const initialState: optionalGamePropsSchema & {
export type State = typeof initialState
export type Action = {
setSetting: (settings: GameSettings) => string | null
DispatchHits: (action: HitDispatch) => void
setTarget: (target: SetStateAction<Target>) => void
setTargetPreview: (targetPreview: SetStateAction<Target>) => void
setPlayer: (payload: { users: PlayerSchema[] }) => string | null
setSetting: (settings: GameSettings) => string | null
full: (newProps: GamePropsSchema) => void
leave: (cb: () => void) => void
setIsReady: (payload: { i: number; isReady: boolean }) => void
@ -41,6 +53,34 @@ export const useGameProps = create<State & Action>()(
devtools(
(set) => ({
...initialState,
DispatchHits: (action) =>
set(
produce((state: State) => {
switch (action.type) {
case "fireMissile":
case "htorpedo":
case "vtorpedo": {
state.hits.push(...action.payload)
}
}
})
),
setTarget: (target) =>
set(
produce((state: State) => {
if (typeof target === "function")
state.target = target(state.target)
else state.target = target
})
),
setTargetPreview: (targetPreview) =>
set(
produce((state: State) => {
if (typeof targetPreview === "function")
state.targetPreview = targetPreview(state.target)
else state.targetPreview = targetPreview
})
),
setPlayer: (payload) => {
let hash: string | null = null
set(

View file

@ -17,7 +17,6 @@ function useSocket() {
full,
setIsReady,
setIsConnected,
hash: stateHash,
} = useGameProps()
const { data: session } = useSession()
const router = useRouter()
@ -39,7 +38,6 @@ function useSocket() {
useEffect(() => {
if (!session?.user.id) return
socket.connect()
socket.on("connect", () => {
console.log("connected")
@ -115,9 +113,7 @@ function useSocket() {
})
})
socket.on("isReady", (payload) => {
setIsReady(payload)
})
socket.on("isReady", setIsReady)
socket.on("disconnect", () => {
console.log("disconnect")

View file

@ -1,3 +1,4 @@
import { DrawLineProps } from "./frontend"
import { GameSettings } from "@components/Lobby/SettingsFrame/Setting"
import { GamePropsSchema, PlayerSchema } from "@lib/zodSchemas"
import type { Server as HTTPServer } from "http"
@ -43,6 +44,10 @@ export interface ServerToClientEvents {
) => void
isReady: (payload: { i: number; isReady: boolean }) => void
isConnected: (payload: { i: number; isConnected: boolean }) => void
"get-canvas-state": () => void
"canvas-state-from-server": (state: string, userIndex: number) => void
"draw-line": (props: DrawLineProps, userIndex: number) => void
clear: () => void
}
export interface ClientToServerEvents {
@ -53,6 +58,9 @@ export interface ClientToServerEvents {
join: (withAck: (ack: boolean) => void) => void
gameSetting: (payload: GameSettings, callback: (hash: string) => void) => void
leave: (withAck: (ack: boolean) => void) => void
"canvas-state": (state: string) => void
"draw-line": (props: DrawLineProps) => void
clear: () => void
}
interface InterServerEvents {

View file

@ -64,9 +64,17 @@ export interface Point {
y: number
}
export interface Draw {
ctx: CanvasRenderingContext2D
export interface DrawLineProps {
currentPoint: Point
prevPoint: Point | null
color: string
}
export interface Draw extends DrawLineProps {
ctx: CanvasRenderingContext2D
}
export interface Ship extends Position {
size: number
variant: number
}

View file

@ -3,5 +3,4 @@ import { io } from "socket.io-client"
export const socket: cSocket = io({
path: "/api/ws",
autoConnect: false,
})

View file

@ -26,19 +26,6 @@ export function cornerCN(count: number, x: number, y: number) {
export function fieldIndex(count: number, x: number, y: number) {
return y * (count + 2) + x
}
export function hitReducer(formObject: Hit[], action: HitDispatch) {
switch (action.type) {
case "fireMissile":
case "htorpedo":
case "vtorpedo": {
const result = [...formObject, ...action.payload]
return result
}
default:
return formObject
}
}
const modes: Mode[] = [
{

View file

@ -2,6 +2,7 @@ import {
NextApiResponseWithSocket,
sServer,
} from "../../interfaces/NextApiSocket"
import { DrawLineProps } from "../../interfaces/frontend"
import {
composeBody,
gameSelects,
@ -183,6 +184,33 @@ const SocketHandler = async (
.emit("isConnected", { i: socket.data.index, isConnected: true })
})
socket.on("canvas-state", (state) => {
if (!socket.data.gameId || !socket.data.index) return
console.log("received canvas state")
socket
.to(socket.data.gameId)
.emit("canvas-state-from-server", state, socket.data.index)
})
socket.on(
"draw-line",
({ prevPoint, currentPoint, color }: DrawLineProps) => {
if (!socket.data.gameId || !socket.data.index) return
socket
.to(socket.data.gameId)
.emit(
"draw-line",
{ prevPoint, currentPoint, color },
socket.data.index
)
}
)
socket.on("clear", () => {
if (!socket.data.gameId) return
socket.to(socket.data.gameId).emit("clear")
})
socket.on("disconnecting", async () => {
logging(
"Disconnecting: " + JSON.stringify(Array.from(socket.rooms)),

View file

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Before After
Before After

View file

@ -120,15 +120,15 @@ body {
}
&.s2 {
grid-column: 3 / 5;
grid-column: var(--y) / 5;
}
&.s3 {
grid-column: 3 / 6;
grid-column: var(--y) / 6;
}
&.s4 {
grid-column: 3 / 7;
grid-column: var(--y) / 7;
}
}