Further fixes for SolidJS

This commit is contained in:
aronmal 2023-08-31 09:20:33 +02:00
parent db7fb9213e
commit 89b79fa245
Signed by: aronmal
GPG key ID: 816B7707426FC612
42 changed files with 1009 additions and 913 deletions

View file

@ -6,7 +6,8 @@
"build": "solid-start build", "build": "solid-start build",
"lint": "eslint --fix \"**/*.{ts,tsx,js,jsx}\"", "lint": "eslint --fix \"**/*.{ts,tsx,js,jsx}\"",
"push": "drizzle-kit push:pg", "push": "drizzle-kit push:pg",
"test": "pnpm playwright test --ui" "test": "pnpm playwright test --ui",
"typecheck": "tsc --noEmit --checkJs false --skipLibCheck"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
@ -30,6 +31,7 @@
"drizzle-zod": "^0.5.0", "drizzle-zod": "^0.5.0",
"http-status": "^1.6.2", "http-status": "^1.6.2",
"nodemailer": "6.9.4", "nodemailer": "6.9.4",
"object-hash": "^3.0.0",
"postgres": "^3.3.5", "postgres": "^3.3.5",
"socket.io": "^4.7.2", "socket.io": "^4.7.2",
"socket.io-client": "^4.7.2", "socket.io-client": "^4.7.2",
@ -44,6 +46,7 @@
"@total-typescript/ts-reset": "^0.4.2", "@total-typescript/ts-reset": "^0.4.2",
"@types/node": "^20.5.0", "@types/node": "^20.5.0",
"@types/nodemailer": "^6.4.9", "@types/nodemailer": "^6.4.9",
"@types/object-hash": "^3.0.3",
"@types/web-bluetooth": "^0.0.17", "@types/web-bluetooth": "^0.0.17",
"@typescript-eslint/eslint-plugin": "^6.4.0", "@typescript-eslint/eslint-plugin": "^6.4.0",
"autoprefixer": "^10.4.15", "autoprefixer": "^10.4.15",

View file

@ -65,6 +65,9 @@ dependencies:
nodemailer: nodemailer:
specifier: 6.9.4 specifier: 6.9.4
version: 6.9.4 version: 6.9.4
object-hash:
specifier: ^3.0.0
version: 3.0.0
postgres: postgres:
specifier: ^3.3.5 specifier: ^3.3.5
version: 3.3.5 version: 3.3.5
@ -103,6 +106,9 @@ devDependencies:
'@types/nodemailer': '@types/nodemailer':
specifier: ^6.4.9 specifier: ^6.4.9
version: 6.4.9 version: 6.4.9
'@types/object-hash':
specifier: ^3.0.3
version: 3.0.3
'@types/web-bluetooth': '@types/web-bluetooth':
specifier: ^0.0.17 specifier: ^0.0.17
version: 0.0.17 version: 0.0.17
@ -2075,6 +2081,10 @@ packages:
'@types/node': 18.17.5 '@types/node': 18.17.5
dev: true dev: true
/@types/object-hash@3.0.3:
resolution: {integrity: sha512-Mb0SDIhjhBAz4/rDNU0cYcQR4lSJIwy+kFlm0whXLkx+o0pXwEszwyrWD6gXWumxVbAS6XZ9gXK82LR+Uk+cKQ==}
dev: true
/@types/resolve@1.20.2: /@types/resolve@1.20.2:
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
@ -4257,7 +4267,6 @@ packages:
/object-hash@3.0.0: /object-hash@3.0.0:
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
dev: true
/object-inspect@1.12.3: /object-inspect@1.12.3:
resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==}

View file

@ -1,7 +1,16 @@
import { For } from "solid-js" import { For } from "solid-js"
import { useGameProps } from "~/hooks/useGameProps" import {
import useIndex from "~/hooks/useIndex" gameState,
import useShips from "~/hooks/useShips" mode,
mouseCursor,
removeShip,
setMode,
setMouseCursor,
setShips,
setTarget,
targetPreview,
} from "~/hooks/useGameProps"
import { useSession } from "~/hooks/useSession"
import { import {
borderCN, borderCN,
cornerCN, cornerCN,
@ -23,40 +32,36 @@ type TilesType = {
} }
function BorderTiles() { function BorderTiles() {
const { activeUser } = useIndex() const { selfIndex, activeUser, ships } = useSession()
const {
payload,
mode,
targetPreview,
mouseCursor,
setTarget,
setMouseCursor,
} = useGameProps()
const { ships, setShips, removeShip } = useShips()
const settingTarget = (isGameTile: boolean, x: number, y: number) => { const settingTarget = (isGameTile: boolean, x: number, y: number) => {
if (payload?.game?.state === "running") { if (gameState() === "running") {
const list = targetList(targetPreview, mode) const list = targetList(targetPreview(), mode())
if ( if (
!isGameTile || !isGameTile ||
!list.filter(({ x, y }) => !isAlreadyHit(x, y, activeUser?.hits ?? [])) !list.filter(
.length ({ x, y }) => !isAlreadyHit(x, y, activeUser()?.hits() ?? []),
).length
) )
return return
if (!overlapsWithAnyBorder(targetPreview, mode)) if (!overlapsWithAnyBorder(targetPreview(), mode()))
setTarget({ setTarget({
show: true, show: true,
x, x,
y, y,
orientation: targetPreview.orientation, orientation: targetPreview().orientation,
}) })
} else if ( } else if (
payload?.game?.state === "starting" && gameState() === "starting" &&
targetPreview.show && targetPreview().show &&
!intersectingShip(ships(), shipProps(ships(), mode, targetPreview)).score !intersectingShip(ships(), shipProps(ships(), mode(), targetPreview()))
.score
) { ) {
setMouseCursor((e) => ({ ...e, shouldShow: false })) setMouseCursor((e) => ({ ...e, shouldShow: false }))
setShips([...ships(), shipProps(ships(), mode, targetPreview)]) setShips(
[...ships(), shipProps(ships(), mode(), targetPreview())],
selfIndex(),
)
} }
} }
@ -89,11 +94,11 @@ function BorderTiles() {
class={props.className} class={props.className}
style={{ "--x": props.x, "--y": props.y }} style={{ "--x": props.x, "--y": props.y }}
onClick={() => { onClick={() => {
if (payload?.game?.state === "running") { if (gameState() === "running") {
settingTarget(props.isGameTile, props.x, props.y) settingTarget(props.isGameTile, props.x, props.y)
} else if (payload?.game?.state === "starting") { } else if (gameState() === "starting") {
const { index } = intersectingShip(ships(), { const { index } = intersectingShip(ships(), {
...mouseCursor, ...mouseCursor(),
size: 1, size: 1,
variant: 0, variant: 0,
orientation: "h", orientation: "h",
@ -102,8 +107,8 @@ function BorderTiles() {
settingTarget(props.isGameTile, props.x, props.y) settingTarget(props.isGameTile, props.x, props.y)
else { else {
const ship = ships()[index] const ship = ships()[index]
useGameProps.setState({ mode: ship.size - 2 }) setMode(ship.size - 2)
removeShip(ship) removeShip(ship, selfIndex())
setMouseCursor((e) => ({ ...e, shouldShow: true })) setMouseCursor((e) => ({ ...e, shouldShow: true }))
} }
} }
@ -114,13 +119,13 @@ function BorderTiles() {
y: props.y, y: props.y,
shouldShow: shouldShow:
props.isGameTile && props.isGameTile &&
(payload?.game?.state === "starting" (gameState() === "starting"
? intersectingShip( ? intersectingShip(
ships(), ships(),
shipProps(ships(), mode, { shipProps(ships(), mode(), {
x: props.x, x: props.x,
y: props.y, y: props.y,
orientation: targetPreview.orientation, orientation: targetPreview().orientation,
}), }),
true, true,
).score < 2 ).score < 2

View file

@ -23,52 +23,36 @@ import {
} from "@fortawesome/pro-solid-svg-icons" } from "@fortawesome/pro-solid-svg-icons"
import { socket } from "~/lib/socket" import { socket } from "~/lib/socket"
import { modes } from "~/lib/utils/helpers" import { modes } from "~/lib/utils/helpers"
import { GamePropsSchema } from "~/lib/zodSchemas"
// import { Icons, toast } from "react-toastify" // import { Icons, toast } from "react-toastify"
import { For, Show, createEffect } from "solid-js" import { For, Show, createEffect } from "solid-js"
import { useNavigate } from "solid-start" import { useNavigate } from "solid-start"
import { useDrawProps } from "~/hooks/useDrawProps" import { useDrawProps } from "~/hooks/useDrawProps"
import { useGameProps } from "~/hooks/useGameProps" import {
import useIndex from "~/hooks/useIndex" allowChat,
import useShips from "~/hooks/useShips" allowMarkDraw,
import { EventBarModes, GameSettings } from "../../interfaces/frontend" allowSpecials,
allowSpectators,
gameState,
menu,
mode,
reset,
setGameSetting,
setIsReadyFor,
setMenu,
setMode,
setTarget,
setTargetPreview,
target,
users,
} from "~/hooks/useGameProps"
import { useSession } from "~/hooks/useSession"
import { EventBarModes } from "../../interfaces/frontend"
import Item from "./Item" import Item from "./Item"
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: { clear: () => void }) { function EventBar(props: { clear: () => void }) {
const { shouldHide, color } = useDrawProps() const { shouldHide, color } = useDrawProps()
const { selfIndex, isActiveIndex, selfUser } = useIndex() const { selfIndex, isActiveIndex, selfUser, ships } = useSession()
const { ships } = useShips() const navigator = useNavigate()
const navigate = useNavigate()
const {
payload,
userStates,
menu,
mode,
setSetting,
full,
target,
setTarget,
setTargetPreview,
setIsReady,
reset,
} = useGameProps()
const gameSetting = (payload: GameSettings) =>
setGameSetting(payload, setSetting, full)
const items = (): EventBarModes => ({ const items = (): EventBarModes => ({
main: [ main: [
@ -76,36 +60,36 @@ function EventBar(props: { clear: () => void }) {
icon: "burger-menu", icon: "burger-menu",
text: "Menu", text: "Menu",
callback: () => { callback: () => {
useGameProps.setState({ menu: "menu" }) setMenu("menu")
}, },
}, },
payload?.game?.state === "running" gameState() === "running"
? { ? {
icon: faSwords, icon: faSwords,
text: "Attack", text: "Attack",
callback: () => { callback: () => {
useGameProps.setState({ menu: "moves" }) setMenu("moves")
}, },
} }
: { : {
icon: faShip, icon: faShip,
text: "Ships", text: "Ships",
callback: () => { callback: () => {
useGameProps.setState({ menu: "moves" }) setMenu("moves")
}, },
}, },
{ {
icon: "pen", icon: "pen",
text: "Draw", text: "Draw",
callback: () => { callback: () => {
useGameProps.setState({ menu: "draw" }) setMenu("draw")
}, },
}, },
{ {
icon: "gear", icon: "gear",
text: "Settings", text: "Settings",
callback: () => { callback: () => {
useGameProps.setState({ menu: "settings" }) setMenu("settings")
}, },
}, },
], ],
@ -115,47 +99,49 @@ function EventBar(props: { clear: () => void }) {
text: "Surrender", text: "Surrender",
iconColor: "darkred", iconColor: "darkred",
callback: () => { callback: () => {
useGameProps.setState({ menu: "surrender" }) setMenu("surrender")
}, },
}, },
], ],
moves: moves:
payload?.game?.state === "running" gameState() === "running"
? [ ? [
{ {
icon: "scope", icon: "scope",
text: "Fire missile", text: "Fire missile",
enabled: mode === 0, enabled: mode() === 0,
callback: () => { callback: () => {
useGameProps.setState({ mode: 0 }) setMode(0)
setTarget((e) => ({ ...e, show: false })) setTarget((t) => ({ ...t, show: false }))
}, },
}, },
{ {
icon: "torpedo", icon: "torpedo",
text: "Fire torpedo", text: "Fire torpedo",
enabled: mode === 1 || mode === 2, enabled: mode() === 1 || mode() === 2,
amount: amount:
2 - 2 -
((selfUser?.moves ?? []).filter( (selfUser()
(e) => e.type === "htorpedo" || e.type === "vtorpedo", ?.moves()
).length ?? 0), .filter((e) => e.type === "htorpedo" || e.type === "vtorpedo")
.length ?? 0),
callback: () => { callback: () => {
useGameProps.setState({ mode: 1 }) setMode(1)
setTarget((e) => ({ ...e, show: false })) setTarget((t) => ({ ...t, show: false }))
}, },
}, },
{ {
icon: "radar", icon: "radar",
text: "Radar scan", text: "Radar scan",
enabled: mode === 3, enabled: mode() === 3,
amount: amount:
1 - 1 -
((selfUser?.moves ?? []).filter((e) => e.type === "radar") (selfUser()
.length ?? 0), ?.moves()
.filter((e) => e.type === "radar").length ?? 0),
callback: () => { callback: () => {
useGameProps.setState({ mode: 3 }) setMode(3)
setTarget((e) => ({ ...e, show: false })) setTarget((t) => ({ ...t, show: false }))
}, },
}, },
] ]
@ -166,7 +152,7 @@ function EventBar(props: { clear: () => void }) {
amount: 1 - ships().filter((e) => e.size === 2).length, amount: 1 - ships().filter((e) => e.size === 2).length,
callback: () => { callback: () => {
if (1 - ships().filter((e) => e.size === 2).length === 0) return if (1 - ships().filter((e) => e.size === 2).length === 0) return
useGameProps.setState({ mode: 0 }) setMode(0)
}, },
}, },
{ {
@ -175,7 +161,7 @@ function EventBar(props: { clear: () => void }) {
amount: 3 - ships().filter((e) => e.size === 3).length, amount: 3 - ships().filter((e) => e.size === 3).length,
callback: () => { callback: () => {
if (3 - ships().filter((e) => e.size === 3).length === 0) return if (3 - ships().filter((e) => e.size === 3).length === 0) return
useGameProps.setState({ mode: 1 }) setMode(1)
}, },
}, },
{ {
@ -184,7 +170,7 @@ function EventBar(props: { clear: () => void }) {
amount: 2 - ships().filter((e) => e.size === 4).length, amount: 2 - ships().filter((e) => e.size === 4).length,
callback: () => { callback: () => {
if (2 - ships().filter((e) => e.size === 4).length === 0) return if (2 - ships().filter((e) => e.size === 4).length === 0) return
useGameProps.setState({ mode: 2 }) setMode(2)
}, },
}, },
{ {
@ -213,31 +199,31 @@ function EventBar(props: { clear: () => void }) {
{ {
icon: faGlasses, icon: faGlasses,
text: "Spectators", text: "Spectators",
disabled: !payload?.game?.allowSpectators, disabled: !allowSpectators(),
callback: gameSetting({ callback: setGameSetting({
allowSpectators: !payload?.game?.allowSpectators, allowSpectators: !allowSpectators(),
}), }),
}, },
{ {
icon: faSparkles, icon: faSparkles,
text: "Specials", text: "Specials",
disabled: !payload?.game?.allowSpecials, disabled: !allowSpecials(),
callback: gameSetting({ callback: setGameSetting({
allowSpecials: !payload?.game?.allowSpecials, allowSpecials: !allowSpecials(),
}), }),
}, },
{ {
icon: faComments, icon: faComments,
text: "Chat", text: "Chat",
disabled: !payload?.game?.allowChat, disabled: !allowChat(),
callback: gameSetting({ allowChat: !payload?.game?.allowChat }), callback: setGameSetting({ allowChat: !allowChat() }),
}, },
{ {
icon: faScribble, icon: faScribble,
text: "Mark/Draw", text: "Mark/Draw",
disabled: !payload?.game?.allowMarkDraw, disabled: !allowMarkDraw(),
callback: gameSetting({ callback: setGameSetting({
allowMarkDraw: !payload?.game?.allowMarkDraw, allowMarkDraw: !allowMarkDraw(),
}), }),
}, },
], ],
@ -248,7 +234,7 @@ function EventBar(props: { clear: () => void }) {
iconColor: "green", iconColor: "green",
callback: async () => { callback: async () => {
socket.emit("gameState", "aborted") socket.emit("gameState", "aborted")
await navigate("/") await navigator("/")
reset() reset()
}, },
}, },
@ -257,7 +243,7 @@ function EventBar(props: { clear: () => void }) {
text: "No", text: "No",
iconColor: "red", iconColor: "red",
callback: () => { callback: () => {
useGameProps.setState({ menu: "main" }) setMenu("main")
}, },
}, },
], ],
@ -265,22 +251,22 @@ function EventBar(props: { clear: () => void }) {
createEffect(() => { createEffect(() => {
if ( if (
menu !== "moves" || menu() !== "moves" ||
payload?.game?.state !== "starting" || gameState() !== "starting" ||
mode < 0 || mode() < 0 ||
items().moves[mode].amount items().moves[mode()].amount
) )
return return
const index = items().moves.findIndex((e) => e.amount) const index = items().moves.findIndex((e) => e.amount)
useGameProps.setState({ mode: index }) setMode(index)
}) })
createEffect(() => { createEffect(() => {
useDrawProps.setState({ enable: menu === "draw" }) useDrawProps.setState({ enable: menu() === "draw" })
}) })
// createEffect(() => { // createEffect(() => {
// if (payload?.game?.state !== "running") return // if (gameState() !== "running") return
// const toastId = "otherPlayer" // const toastId = "otherPlayer"
// if (isActiveIndex) toast.dismiss(toastId) // if (isActiveIndex) toast.dismiss(toastId)
@ -307,59 +293,51 @@ function EventBar(props: { clear: () => void }) {
return ( return (
<div class="event-bar"> <div class="event-bar">
<Show when={menu !== "main"}> <Show when={menu() !== "main"}>
<Item <Item
{...{ {...{
icon: faReply, icon: faReply,
text: "Return", text: "Return",
iconColor: "#555", iconColor: "#555",
callback: () => { callback: () => {
useGameProps.setState({ menu: "main" }) setMenu("main")
}, },
}} }}
/> />
</Show> </Show>
<For each={items()[menu]}> <For each={items()[menu()]}>
{(e, i) => ( {(e, i) => (
<Show when={isActiveIndex && menu !== "main" && i() !== 1}> <Show when={isActiveIndex() && menu() !== "main" && i() !== 1}>
<Item {...e} /> <Item {...e} />
</Show> </Show>
)} )}
</For> </For>
<Show when={menu === "moves"}> <Show when={menu() === "moves"}>
<Item <Item
{...{ {...{
icon: icon: selfUser()?.isReady() ? faLock : faCheck,
selfIndex >= 0 && userStates[selfIndex].isReady() text: selfUser()?.isReady() ? "unready" : "Done",
? faLock disabled: gameState() === "starting" ? mode() >= 0 : undefined,
: faCheck, enabled: gameState() === "running" && mode() >= 0 && target().show,
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: () => { callback: () => {
if (selfIndex < 0) return const i = selfIndex()
switch (payload?.game?.state) { if (i === -1) return
switch (gameState()) {
case "starting": case "starting":
const isReady = !userStates[selfIndex].isReady const isReady = !users[i].isReady()
setIsReady({ isReady, i: selfIndex }) setIsReadyFor({ isReady, i })
socket.emit("isReady", isReady) socket.emit("isReady", isReady)
break break
case "running": case "running":
const i = (selfUser?.moves ?? []) const moves = selfUser()?.moves()
.map((e) => e.index) const length = moves?.length
.reduce((prev, curr) => (curr > prev ? curr : prev), 0)
const props = { const props = {
type: modes[mode].type, type: modes[mode()].type,
x: target.x, x: target().x,
y: target.y, y: target().y,
orientation: target.orientation, orientation: target().orientation,
index: (selfUser?.moves ?? []).length ? i + 1 : 0, index: length ?? 0,
} }
socket.emit("dispatchMove", props) socket.emit("dispatchMove", props)
setTarget((t) => ({ ...t, show: false })) setTarget((t) => ({ ...t, show: false }))

View file

@ -9,8 +9,20 @@ import HitElems from "~/components/Gamefield/HitElems"
import Targets from "~/components/Gamefield/Targets" 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 {
import useIndex from "~/hooks/useIndex" full,
gameId,
gameState,
mode,
mouseCursor,
reset,
setMode,
setMouseCursor,
setTargetPreview,
target,
users,
} from "~/hooks/useGameProps"
import { useSession } from "~/hooks/useSession"
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"
@ -20,47 +32,35 @@ import Ships from "./Ships"
export const count = 12 export const count = 12
function Gamefield() { function Gamefield() {
const { selfUser } = useIndex() const { ships } = useSession()
const navigate = useNavigate() const navigator = useNavigate()
const {
userStates,
mode,
target,
mouseCursor,
setMouseCursor,
payload,
setTargetPreview,
full,
reset,
} = useGameProps()
const { isConnected } = useSocket() const { isConnected } = useSocket()
const usingDraw = useDraw() const usingDraw = useDraw()
const { enable, color, shouldHide } = useDrawProps() const { enable, color, shouldHide } = useDrawProps()
createEffect(() => { createEffect(() => {
if ( if (
payload?.game?.state !== "starting" || gameState() !== "starting" ||
userStates.reduce((prev, curr) => prev || !curr.isReady, false) !users[0].isReady() ||
!users[1].isReady()
) )
return return
socket.emit("ships", selfUser?.ships ?? []) socket.emit("ships", ships() ?? [])
socket.emit("gameState", "running") socket.emit("gameState", "running")
}) })
createEffect(() => { createEffect(() => {
if (payload?.game?.id || !isConnected) return if (gameId() || !isConnected()) return
socket.emit("update", full) socket.emit("update", full)
}) })
createEffect(() => { createEffect(() => {
if (mode < 0) return if (mode() < 0) return
const { x, y, show } = target const { x, y, show } = target()
const { shouldShow, ...position } = mouseCursor const { shouldShow, ...position } = mouseCursor()
if ( if (
!shouldShow || !shouldShow ||
(payload?.game?.state === "running" && (gameState() === "running" && overlapsWithAnyBorder(position, mode()))
overlapsWithAnyBorder(position, mode))
) )
setTargetPreview((t) => ({ ...t, show: false })) setTargetPreview((t) => ({ ...t, show: false }))
else { else {
@ -71,14 +71,14 @@ function Gamefield() {
})) }))
const handleKeyPress = (event: KeyboardEvent) => { const handleKeyPress = (event: KeyboardEvent) => {
if (event.key !== "r") return if (event.key !== "r") return
if (payload?.game?.state === "starting") { if (gameState() === "starting") {
setTargetPreview((t) => ({ setTargetPreview((t) => ({
...t, ...t,
orientation: t.orientation === "h" ? "v" : "h", orientation: t.orientation === "h" ? "v" : "h",
})) }))
} }
if (payload?.game?.state === "running" && (mode === 1 || mode === 2)) if (gameState() === "running" && (mode() === 1 || mode() === 2))
useGameProps.setState({ mode: mode === 1 ? 2 : 1 }) setMode(mode() === 1 ? 2 : 1)
} }
document.addEventListener("keydown", handleKeyPress) document.addEventListener("keydown", handleKeyPress)
return () => { return () => {
@ -88,15 +88,15 @@ function Gamefield() {
}) })
createEffect(() => { createEffect(() => {
if (payload?.game?.state !== "aborted") return if (gameState() !== "aborted") return
// toast.info("Enemy gave up!") // toast.info("Enemy gave up!")
navigate("/") navigator("/")
reset() reset()
}) })
createEffect(() => { createEffect(() => {
if (payload?.game?.id) return if (gameId()) return
const timeout = setTimeout(() => navigate("/"), 5000) const timeout = setTimeout(() => navigator("/"), 5000)
return () => clearTimeout(timeout) return () => clearTimeout(timeout)
}) })

View file

@ -1,16 +1,16 @@
import { faBurst, faXmark } from "@fortawesome/pro-solid-svg-icons" import { faBurst, faXmark } from "@fortawesome/pro-solid-svg-icons"
import { For } from "solid-js" import { For } from "solid-js"
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon" import { FontAwesomeIcon } from "~/components/FontAwesomeIcon"
import useIndex from "~/hooks/useIndex" import { useSession } from "~/hooks/useSession"
import { Hit } from "../../interfaces/frontend" import { Hit } from "../../interfaces/frontend"
function HitElems(props: { hits?: Hit[]; colorOverride?: string }) { function HitElems(props: { hits?: Hit[]; colorOverride?: string }) {
const { activeUser } = useIndex() const { activeUser } = useSession()
const hits = () => props?.hits const hits = () => props?.hits
const colorOverride = () => props?.colorOverride const colorOverride = () => props?.colorOverride
return ( return (
<For each={hits() ?? activeUser?.hits ?? []}> <For each={hits() ?? activeUser()?.hits()}>
{(props) => ( {(props) => (
<div class="hit-svg" style={{ "--x": props.x, "--y": props.y }}> <div class="hit-svg" style={{ "--x": props.x, "--y": props.y }}>
<FontAwesomeIcon <FontAwesomeIcon

View file

@ -1,4 +1,4 @@
import { createEffect, createSignal } from "solid-js" import { Show, createEffect, createSignal } from "solid-js"
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon" import { FontAwesomeIcon } from "~/components/FontAwesomeIcon"
// import { useDrawProps } from "~/hooks/useDrawProps" // import { useDrawProps } from "~/hooks/useDrawProps"
// import { HexColorPicker } from "react-colorful" // import { HexColorPicker } from "react-colorful"
@ -33,14 +33,14 @@ function Item(props: ItemProps) {
isColor() ? setActive(true) : props.callback && props.callback() isColor() ? setActive(true) : props.callback && props.callback()
} }
> >
{isColor() ? ( <Show when={isColor()}>
<div <div
ref={cpRef!} ref={cpRef!}
class={classNames("react-colorful-wrapper", { active: active })} class={classNames("react-colorful-wrapper", { active: active })}
> >
{/* <HexColorPicker color={color} onChange={setColor} /> */} {/* <HexColorPicker color={color} onChange={setColor} /> */}
</div> </div>
) : null} </Show>
<div <div
class={classNames("container", { class={classNames("container", {
amount: typeof props.amount !== "undefined", amount: typeof props.amount !== "undefined",

View file

@ -1,15 +1,14 @@
import { For, Show } from "solid-js" import { For, Show } from "solid-js"
import { useGameProps } from "~/hooks/useGameProps" import { gameState } from "~/hooks/useGameProps"
import useIndex from "~/hooks/useIndex" import { useSession } from "~/hooks/useSession"
import Ship from "./Ship" import Ship from "./Ship"
function Ships() { function Ships() {
const { payload } = useGameProps() const { isActiveIndex, selfUser } = useSession()
const { isActiveIndex, selfUser } = useIndex()
return ( return (
<Show when={payload?.game?.state === "running" && isActiveIndex}> <Show when={gameState() === "running" && isActiveIndex}>
<For each={selfUser?.ships}>{(props) => <Ship {...props} />}</For> <For each={selfUser()?.ships()}>{(props) => <Ship {...props} />}</For>
</Show> </Show>
) )
} }

View file

@ -1,7 +1,6 @@
import { For, Match, Switch } from "solid-js" import { For, Match, Switch } from "solid-js"
import { useGameProps } from "~/hooks/useGameProps" import { gameState, mode, target, targetPreview } from "~/hooks/useGameProps"
import useIndex from "~/hooks/useIndex" import { useSession } from "~/hooks/useSession"
import useShips from "~/hooks/useShips"
import { import {
composeTargetTiles, composeTargetTiles,
intersectingShip, intersectingShip,
@ -12,29 +11,29 @@ import HitElems from "./HitElems"
import Ship from "./Ship" import Ship from "./Ship"
function Targets() { function Targets() {
const { activeUser } = useIndex() const { activeUser, ships } = useSession()
const { payload, target, targetPreview, mode } = useGameProps()
const { ships } = useShips()
const ship = shipProps(ships(), mode, targetPreview) const ship = shipProps(ships(), mode(), targetPreview())
const { fields, borders, score } = intersectingShip(ships(), ship) const { fields, borders, score } = intersectingShip(ships(), ship)
return ( return (
<Switch> <Switch>
<Match when={payload?.game?.state === "running"}> <Match when={gameState() === "running"}>
<For each={composeTargetTiles(target, mode, activeUser?.hits ?? [])}> <For each={composeTargetTiles(target(), mode(), activeUser().hits())}>
{(props) => <GamefieldPointer {...props} />} {(props) => <GamefieldPointer {...props} />}
</For> </For>
<For <For
each={composeTargetTiles(targetPreview, mode, activeUser?.hits ?? [])} each={composeTargetTiles(
targetPreview(),
mode(),
activeUser().hits(),
)}
> >
{(props) => <GamefieldPointer {...props} preview />} {(props) => <GamefieldPointer {...props} preview />}
</For> </For>
</Match> </Match>
<Match <Match
when={ when={gameState() === "starting" && mode() >= 0 && targetPreview().show}
payload?.game?.state === "starting" && mode >= 0 && targetPreview.show
}
> >
<Ship <Ship
{...ship} {...ship}

View file

@ -2,14 +2,21 @@ import {
faRightFromBracket, faRightFromBracket,
faSpinnerThird, faSpinnerThird,
} from "@fortawesome/pro-solid-svg-icons" } from "@fortawesome/pro-solid-svg-icons"
import { JSX, createEffect, createSignal } from "solid-js" import { JSX, Show, createEffect, createSignal } from "solid-js"
import { useNavigate } from "solid-start" import { useNavigate } from "solid-start"
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon" import { FontAwesomeIcon } from "~/components/FontAwesomeIcon"
import { useGameProps } from "~/hooks/useGameProps" import {
full,
gameId,
gamePin,
gameState,
leave,
reset,
users,
} from "~/hooks/useGameProps"
import { useSession } from "~/hooks/useSession" import { useSession } from "~/hooks/useSession"
import useSocket from "~/hooks/useSocket" import useSocket from "~/hooks/useSocket"
import { socket } from "~/lib/socket" import { socket } from "~/lib/socket"
import Button from "./Button" import Button from "./Button"
import Icon from "./Icon" import Icon from "./Icon"
import Player from "./Player" import Player from "./Player"
@ -34,15 +41,11 @@ function WithDots(props: { children: JSX.Element }) {
} }
function LobbyFrame(props: { openSettings: () => void }) { function LobbyFrame(props: { openSettings: () => void }) {
const { payload, userStates, full, leave, reset } = useGameProps()
const { isConnected } = useSocket() const { isConnected } = useSocket()
const navigate = useNavigate() const navigator = useNavigate()
const session = useSession() const { session } = useSession()
const [launchTime, setLaunchTime] = createSignal(3) const [launchTime, setLaunchTime] = createSignal(3)
const launching = () => users[0].isReady() && users[1].isReady()
const launching = () =>
payload?.users.length === 2 &&
!userStates.filter((user) => !user.isReady).length
createEffect(() => { createEffect(() => {
if (!launching() || launchTime() > 0) return if (!launching() || launchTime() > 0) return
@ -61,17 +64,13 @@ function LobbyFrame(props: { openSettings: () => void }) {
}) })
createEffect(() => { createEffect(() => {
if (payload?.game?.id || !isConnected) return if (gameId() || !isConnected()) return
socket.emit("update", full) socket.emit("update", full)
}) })
createEffect(() => { createEffect(() => {
if ( if (gameState() === "unknown" || gameState() === "lobby") return
typeof payload?.game?.state !== "string" || navigator("/gamefield")
payload?.game?.state === "lobby"
)
return
navigate("/gamefield")
}) })
return ( return (
@ -79,53 +78,50 @@ function LobbyFrame(props: { openSettings: () => void }) {
<div class="flex items-center justify-between border-b-2 border-slate-900"> <div class="flex items-center justify-between border-b-2 border-slate-900">
<Icon src="speech_bubble.png">Chat</Icon> <Icon src="speech_bubble.png">Chat</Icon>
<h1 class="font-farro text-5xl font-medium"> <h1 class="font-farro text-5xl font-medium">
{launching() ? ( <Show
<WithDots> when={!launching()}
{launchTime() < 0 fallback={
? "Game starts" <WithDots>
: "Game is starting in " + launchTime()} {launchTime() < 0
</WithDots> ? "Game starts"
) : ( : "Game is starting in " + launchTime()}
<> </WithDots>
{"Game-PIN: "} }
{isConnected() ? ( >
<span class="underline">{payload?.gamePin ?? "----"}</span> {"Game-PIN: "}
) : ( <Show
<FontAwesomeIcon icon={faSpinnerThird} spin /> when={isConnected()}
)} fallback={<FontAwesomeIcon icon={faSpinnerThird} spin />}
</> >
)} <span class="underline">{gamePin() ?? "----"}</span>
</Show>
</Show>
</h1> </h1>
<Icon src="gear.png" onClick={props.openSettings}> <Icon src="gear.png" onClick={props.openSettings}>
Settings Settings
</Icon> </Icon>
</div> </div>
<div class="flex items-center justify-around"> <div class="flex items-center justify-around">
{isConnected() ? ( <Show
when={isConnected()}
fallback={
<p class="font-farro m-48 text-center text-6xl font-medium">
Warte auf Verbindung
</p>
}
>
<> <>
<Player <Player src="player_blue.png" i={0} userId={session()?.user?.id} />
src="player_blue.png"
i={0}
userId={session.latest?.user?.id}
/>
<p class="font-farro m-4 text-6xl font-semibold">VS</p> <p class="font-farro m-4 text-6xl font-semibold">VS</p>
{payload?.users[1] ? ( {users[1].id() ? (
<Player <Player src="player_red.png" i={1} userId={session()?.user?.id} />
src="player_red.png"
i={1}
userId={session.latest?.user?.id}
/>
) : ( ) : (
<p class="font-farro w-96 text-center text-4xl font-medium"> <p class="font-farro w-96 text-center text-4xl font-medium">
<WithDots>Warte auf Spieler 2</WithDots> <WithDots>Warte auf Spieler 2</WithDots>
</p> </p>
)} )}
</> </>
) : ( </Show>
<p class="font-farro m-48 text-center text-6xl font-medium">
Warte auf Verbindung
</p>
)}
</div> </div>
<div class="flex items-center justify-around border-t-2 border-slate-900 p-4"> <div class="flex items-center justify-around border-t-2 border-slate-900 p-4">
<Button <Button
@ -134,7 +130,7 @@ function LobbyFrame(props: { openSettings: () => void }) {
onClick={() => { onClick={() => {
leave(async () => { leave(async () => {
reset() reset()
await navigate("/") navigator("/")
}) })
}} }}
> >

View file

@ -8,9 +8,9 @@ import {
} from "@fortawesome/pro-solid-svg-icons" } from "@fortawesome/pro-solid-svg-icons"
import { faCaretDown } from "@fortawesome/sharp-solid-svg-icons" import { faCaretDown } from "@fortawesome/sharp-solid-svg-icons"
import classNames from "classnames" import classNames from "classnames"
import { createEffect, createSignal } from "solid-js" import { Show, createEffect, createSignal } from "solid-js"
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon" import { FontAwesomeIcon } from "~/components/FontAwesomeIcon"
import { useGameProps } from "~/hooks/useGameProps" import { setIsReadyFor, users } from "~/hooks/useGameProps"
import { socket } from "~/lib/socket" import { socket } from "~/lib/socket"
import Button from "./Button" import Button from "./Button"
@ -42,13 +42,11 @@ function HourGlass() {
) )
} }
function Player(props: { src: string; i: number; userId?: string }) { function Player(props: { src: string; i: 0 | 1; userId?: string }) {
const { payload, userStates, setIsReady } = useGameProps() const player = () => users[props.i]
const player = () => payload?.users[props.i] const isReady = () => users[props.i].isReady()
const isReady = () => userStates[props.i].isReady const isConnected = () => users[props.i].isConnected()
const isConnected = () => userStates[props.i].isConnected const primary = () => props.userId && props.userId === users[props.i].id()
const primary = () =>
props.userId && props.userId === payload?.users[props.i]?.id
return ( return (
<div class="flex w-96 flex-col items-center gap-4 p-4"> <div class="flex w-96 flex-col items-center gap-4 p-4">
@ -58,7 +56,7 @@ function Player(props: { src: string; i: number; userId?: string }) {
primary() ? "font-semibold" : "font-normal", primary() ? "font-semibold" : "font-normal",
)} )}
> >
{player?.name ?? "Spieler " + (player()?.index === 2 ? "2" : "1")} {player().name() ?? "Spieler " + (props.i === 1 ? "2" : "1")}
</p> </p>
<div class="relative"> <div class="relative">
<img <img
@ -66,14 +64,14 @@ function Player(props: { src: string; i: number; userId?: string }) {
src={"/assets/" + props.src} src={"/assets/" + props.src}
alt={props.src} alt={props.src}
/> />
{primary() ? ( <Show when={primary()}>
<button class="absolute right-4 top-4 h-14 w-14 rounded-lg border-2 border-dashed border-warn bg-gray-800 bg-opacity-90"> <button class="absolute right-4 top-4 h-14 w-14 rounded-lg border-2 border-dashed border-warn bg-gray-800 bg-opacity-90">
<FontAwesomeIcon <FontAwesomeIcon
class="h-full w-full text-warn" class="h-full w-full text-warn"
icon={faCaretDown} icon={faCaretDown}
/> />
</button> </button>
) : null} </Show>
</div> </div>
<Button <Button
type={isConnected() ? (isReady() ? "green" : "orange") : "gray"} type={isConnected() ? (isReady() ? "green" : "orange") : "gray"}
@ -81,11 +79,11 @@ function Player(props: { src: string; i: number; userId?: string }) {
isLatched={!!isReady()} isLatched={!!isReady()}
onClick={() => { onClick={() => {
if (!player()) return if (!player()) return
setIsReady({ socket.emit("isReady", !isReady())
setIsReadyFor({
i: props.i, i: props.i,
isReady: !isReady(), isReady: !isReady(),
}) })
socket.emit("isReady", !isReady())
}} }}
disabled={!primary()} disabled={!primary()}
> >

View file

@ -5,13 +5,25 @@ import {
import classNames from "classnames" import classNames from "classnames"
import { JSX } from "solid-js" import { JSX } from "solid-js"
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon" import { FontAwesomeIcon } from "~/components/FontAwesomeIcon"
import { setGameSetting } from "~/components/Gamefield/EventBar" import {
import { useGameProps } from "~/hooks/useGameProps" allowChat,
allowMarkDraw,
allowSpecials,
allowSpectators,
setGameSetting,
} from "~/hooks/useGameProps"
import { GameSettingKeys } from "../../../interfaces/frontend" import { GameSettingKeys } from "../../../interfaces/frontend"
function Setting(props: { children: JSX.Element; key: GameSettingKeys }) { function Setting(props: { children: JSX.Element; key: GameSettingKeys }) {
const { payload, setSetting, full } = useGameProps() const state = () => {
const state = () => payload?.game?.[props.key] const gameProps = {
allowChat,
allowMarkDraw,
allowSpecials,
allowSpectators,
}
return gameProps[props.key]()
}
return ( return (
<label class="flex items-center justify-between" for={props.key}> <label class="flex items-center justify-between" for={props.key}>
@ -23,7 +35,7 @@ function Setting(props: { children: JSX.Element; key: GameSettingKeys }) {
"text-md mx-auto rounded-full px-4 drop-shadow-md transition-all", "text-md mx-auto rounded-full px-4 drop-shadow-md transition-all",
state() ? "text-blue-500" : "text-gray-800", state() ? "text-blue-500" : "text-gray-800",
{ {
"bg-gray-300 ": state, "bg-gray-300 ": state(),
}, },
)} )}
size="3x" size="3x"
@ -35,13 +47,9 @@ function Setting(props: { children: JSX.Element; key: GameSettingKeys }) {
type="checkbox" type="checkbox"
id={props.key} id={props.key}
onChange={() => onChange={() =>
setGameSetting( setGameSetting({
{ [props.key]: !state(),
[props.key]: !state, })
},
setSetting,
full,
)
} }
hidden={true} hidden={true}
/> />

View file

@ -2,17 +2,15 @@ import { faRotateLeft } from "@fortawesome/pro-regular-svg-icons"
import { faXmark } from "@fortawesome/pro-solid-svg-icons" import { faXmark } from "@fortawesome/pro-solid-svg-icons"
import {} from "solid-js" import {} from "solid-js"
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon" import { FontAwesomeIcon } from "~/components/FontAwesomeIcon"
import { useGameProps } from "~/hooks/useGameProps" import { full, setSetting } from "~/hooks/useGameProps"
import { socket } from "~/lib/socket" import { socket } from "~/lib/socket"
import { GameSettings } from "../../../interfaces/frontend" import { GameSettings } from "../../../interfaces/frontend"
import Setting from "./Setting" import Setting from "./Setting"
function Settings(props: { closeSettings: () => void }) { function Settings(props: { closeSettings: () => void }) {
const { setSetting, full } = useGameProps() const gameSetting = (newSettings: GameSettings) => {
const hash = setSetting(newSettings)
const gameSetting = (payload: GameSettings) => { socket.emit("gameSetting", newSettings, (newHash) => {
const hash = setSetting(payload)
socket.emit("gameSetting", payload, (newHash) => {
if (newHash === hash) return if (newHash === hash) return
console.log("hash", hash, newHash) console.log("hash", hash, newHash)
socket.emit("update", full) socket.emit("update", full)

View file

@ -1,8 +1,6 @@
import type { AdapterAccount } from "@auth/core/adapters" import type { AdapterAccount } from "@auth/core/adapters"
import { createId } from "@paralleldrive/cuid2"
import { relations } from "drizzle-orm" import { relations } from "drizzle-orm"
import { import {
AnyPgColumn,
boolean, boolean,
integer, integer,
pgTable, pgTable,
@ -15,7 +13,7 @@ import { gameState, moveType, orientation } from "./Types"
export const users = pgTable("user", { export const users = pgTable("user", {
id: text("id").notNull().primaryKey(), id: text("id").notNull().primaryKey(),
name: text("name"), name: text("name").notNull(),
email: text("email").notNull().unique(), email: text("email").notNull().unique(),
emailVerified: timestamp("emailVerified", { mode: "date" }), emailVerified: timestamp("emailVerified", { mode: "date" }),
image: text("image"), image: text("image"),
@ -69,7 +67,7 @@ export const verificationTokens = pgTable(
) )
export const games = pgTable("game", { export const games = pgTable("game", {
id: text("id").notNull().primaryKey().default(createId()), id: text("id").notNull().primaryKey(),
createdAt: timestamp("created_at").defaultNow(), createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").notNull().defaultNow(), updatedAt: timestamp("updated_at").notNull().defaultNow(),
state: gameState("state").notNull().default("lobby"), state: gameState("state").notNull().default("lobby"),
@ -80,17 +78,17 @@ export const games = pgTable("game", {
}) })
export const gamepins = pgTable("gamepin", { export const gamepins = pgTable("gamepin", {
id: text("id").notNull().primaryKey().default(createId()), id: text("id").notNull().primaryKey(),
createdAt: timestamp("created_at").defaultNow(), createdAt: timestamp("created_at").defaultNow(),
pin: text("pin").notNull().unique(), pin: text("pin").notNull().unique(),
gameId: text("game_id") gameId: text("game_id")
.notNull() .notNull()
.unique() .unique()
.references((): AnyPgColumn => games.id), .references(() => games.id, { onDelete: "cascade" }),
}) })
export const ships = pgTable("ship", { export const ships = pgTable("ship", {
id: text("id").notNull().primaryKey().default(createId()), id: text("id").notNull().primaryKey(),
size: integer("size").notNull(), size: integer("size").notNull(),
variant: integer("variant").notNull(), variant: integer("variant").notNull(),
x: integer("x").notNull(), x: integer("x").notNull(),
@ -102,7 +100,7 @@ export const ships = pgTable("ship", {
}) })
export const hits = pgTable("hit", { export const hits = pgTable("hit", {
id: text("id").notNull().primaryKey().default(createId()), id: text("id").notNull().primaryKey(),
x: integer("x").notNull(), x: integer("x").notNull(),
y: integer("y").notNull(), y: integer("y").notNull(),
hit: boolean("hit").notNull(), hit: boolean("hit").notNull(),
@ -112,7 +110,7 @@ export const hits = pgTable("hit", {
}) })
export const moves = pgTable("move", { export const moves = pgTable("move", {
id: text("id").notNull().primaryKey().default(createId()), id: text("id").notNull().primaryKey(),
createdAt: timestamp("created_at").defaultNow(), createdAt: timestamp("created_at").defaultNow(),
index: integer("index").notNull(), index: integer("index").notNull(),
type: moveType("type").notNull(), type: moveType("type").notNull(),
@ -125,7 +123,7 @@ export const moves = pgTable("move", {
}) })
export const chats = pgTable("chat", { export const chats = pgTable("chat", {
id: text("id").notNull().primaryKey().default(createId()), id: text("id").notNull().primaryKey(),
createdAt: timestamp("created_at").notNull().defaultNow(), createdAt: timestamp("created_at").notNull().defaultNow(),
message: text("message"), message: text("message"),
event: text("event"), event: text("event"),
@ -137,7 +135,7 @@ export const chats = pgTable("chat", {
export const user_games = pgTable( export const user_games = pgTable(
"user_game", "user_game",
{ {
id: text("id").notNull().primaryKey().default(createId()), id: text("id").notNull().primaryKey(),
createdAt: timestamp("created_at").defaultNow(), createdAt: timestamp("created_at").defaultNow(),
gameId: text("game_id") gameId: text("game_id")
.notNull() .notNull()

View file

@ -1,6 +1,7 @@
import { pgEnum } from "drizzle-orm/pg-core" import { pgEnum } from "drizzle-orm/pg-core"
export const gameState = pgEnum("game_state", [ export const gameState = pgEnum("game_state", [
"unknown",
"lobby", "lobby",
"starting", "starting",
"running", "running",

View file

@ -1,261 +1,262 @@
import { getPayloadwithChecksum } from "~/lib/getPayloadwithChecksum" /* eslint-disable solid/reactivity */
import { socket } from "~/lib/socket" import { socket } from "~/lib/socket"
import { GamePropsSchema, GameState, MoveType } from "~/lib/zodSchemas"
// import { toast } from "react-toastify"
import { createSignal } from "solid-js"
import { getPayloadFromProps } from "~/lib/getPayloadFromProps"
import { getPayloadwithChecksum } from "~/lib/getPayloadwithChecksum"
import { import {
initialUser,
initlialMouseCursor, initlialMouseCursor,
initlialTarget, initlialTarget,
initlialTargetPreview, initlialTargetPreview,
intersectingShip, intersectingShip,
targetList, targetList,
} from "~/lib/utils/helpers" } from "~/lib/utils/helpers"
import {
GamePropsSchema,
GameState,
MoveType,
PlayerSchema,
optionalGamePropsSchema,
} from "~/lib/zodSchemas"
// import { toast } from "react-toastify"
import create from "solid-zustand"
import { Accessor, Setter, createSignal } from "solid-js"
import { import {
EventBarModes, EventBarModes,
GameSettings, GameSettings,
MouseCursor, MouseCursor,
MoveDispatchProps, MoveDispatchProps,
NewUsers,
ShipProps, ShipProps,
Target, Target,
TargetPreview, TargetPreview,
} from "../interfaces/frontend" } from "../interfaces/frontend"
const initialState: optionalGamePropsSchema & { export const [hash, setHash] = createSignal<string | null>(null)
userStates: { export const [activeIndex, setActiveIndex] = createSignal<0 | 1>(0)
isReady: Accessor<boolean> export const [gamePin, setGamePin] = createSignal<string | null>(null)
setIsReady: Setter<boolean> export const [gameId, setGameId] = createSignal<string>("")
isConnected: Accessor<boolean> export const [gameState, setGameState] = createSignal<GameState>("unknown")
setIsConnected: Setter<boolean> export const [allowChat, setAllowChat] = createSignal(false)
}[] export const [allowMarkDraw, setAllowMarkDraw] = createSignal(false)
menu: keyof EventBarModes export const [allowSpecials, setAllowSpecials] = createSignal(false)
mode: number export const [allowSpectators, setallowSpectators] = createSignal(false)
target: Target export const [menu, setMenu] = createSignal<keyof EventBarModes>("moves")
targetPreview: TargetPreview export const [mode, setMode] = createSignal(0)
mouseCursor: MouseCursor export const [target, setTarget] = createSignal<Target>(initlialTarget)
} = { export const [targetPreview, setTargetPreview] = createSignal<TargetPreview>(
menu: "moves", initlialTargetPreview,
mode: 0, )
payload: null, export const [mouseCursor, setMouseCursor] =
hash: null, createSignal<MouseCursor>(initlialMouseCursor)
target: initlialTarget, export const users = {
targetPreview: initlialTargetPreview, 0: initialUser(),
mouseCursor: initlialMouseCursor, 1: initialUser(),
userStates: Array.from(Array(2), () => { forEach(cb: (user: ReturnType<typeof initialUser>, i: 0 | 1) => void) {
const [isReady, setIsReady] = createSignal(false) cb(this[0], 0)
const [isConnected, setIsConnected] = createSignal(false) cb(this[1], 1)
return { isReady, setIsReady, isConnected, setIsConnected } },
}), map<T>(cb: (user: ReturnType<typeof initialUser>, i: 0 | 1) => T) {
return { 0: cb(this[0], 0), 1: cb(this[1], 1) }
},
} }
export type State = typeof initialState // export function setActiveIndex(i: number, selfIndex: number) {
// if (!payload()) return
// payload().activeIndex = i
// if (i === selfIndex) {
// setMenu("moves")
// setMode(0)
// } else {
// setMenu("main")
// setMode(-1)
// }
// }
export function DispatchMove(move: MoveDispatchProps, index: number) {
const list = targetList(move, move.type)
users.forEach((user, i) => {
if (!user) return
export type Action = { if (index === i) {
DispatchMove: (props: MoveDispatchProps, i: number) => void user.setMoves((e) => [...e, move])
setTarget: (target: Target | ((i: Target) => Target)) => void } else {
setTargetPreview: ( if (move.type === MoveType.Enum.radar) return
targetPreview: TargetPreview | ((i: TargetPreview) => TargetPreview), user.setHits((e) => [
) => void ...e,
setMouseCursor: ( ...list.map(({ x, y }) => ({
mouseCursor: MouseCursor | ((i: MouseCursor) => MouseCursor), hit: !!intersectingShip(user.ships(), {
) => void ...move,
setPlayer: (payload: { users: PlayerSchema[] }) => string | null size: 1,
setSetting: (settings: GameSettings) => string | null variant: 0,
full: (newProps: GamePropsSchema) => void }).fields.length,
leave: (cb: () => void) => void x,
setIsReady: (payload: { i: number; isReady: boolean }) => void y,
gameState: (newState: GameState) => 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 function setShips(ships: ShipProps[], index: number) {
users.forEach(({ setShips }, i) => {
if (index !== i) return
setShips(ships)
})
}
export function removeShip({ size, variant, x, y }: ShipProps, index: number) {
users.forEach((user, i) => {
if (index !== i) return
const indexToRemove = user
.ships()
.findIndex(
(ship) =>
ship.size === size &&
ship.variant === variant &&
ship.x === x &&
ship.y === y,
)
user.setShips((ships) => ships.filter((_, i) => i !== indexToRemove))
})
} }
export const useGameProps = create<State & Action>()((set) => ({ export function setPlayer(newUsers: NewUsers): string | null {
...initialState, let hash: string | null = null
setActiveIndex: (i, selfIndex) => console.log(newUsers)
set((state: State) => { users.forEach((user, i) => {
if (!state.payload) return state const newUser = newUsers[i]
state.payload.activeIndex = i if (!newUser) return defaultUser(user)
if (i === selfIndex) {
state.menu = "moves"
state.mode = 0
} else {
state.menu = "main"
state.mode = -1
}
return state
}),
DispatchMove: (move, i) =>
set((state: State) => {
if (!state.payload) return state
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.Enum.radar)
e.hits.push(
...list.map(({ x, y }) => ({
hit: !!intersectingShip(e.ships, {
...move,
size: 1,
variant: 0,
}).fields.length,
x,
y,
})),
)
return e user.setId(newUser.id)
}) user.setName(newUser.name)
return state user.setChats(newUser.chats)
}), user.setMoves(newUser.moves)
setTarget: (dispatch) => user.setShips(newUser.ships)
set((state: State) => { user.setHits(newUser.hits)
if (typeof dispatch === "function") state.target = dispatch(state.target) })
else state.target = dispatch const body = getPayloadwithChecksum(getPayloadFromProps())
return state if (!body.hash) {
}), console.log("Something is wrong... ")
setTargetPreview: (dispatch) => // toast.warn("Something is wrong... ", {
set((state: State) => { // toastId: "st_wrong",
if (typeof dispatch === "function") // theme: "colored",
state.targetPreview = dispatch(state.targetPreview) // })
else state.targetPreview = dispatch return null
return state }
}), hash = body.hash
setMouseCursor: (dispatch) => setHash(hash)
set((state: State) => { return hash
if (typeof dispatch === "function") }
state.mouseCursor = dispatch(state.mouseCursor) export function setSetting(newSettings: GameSettings): string | null {
else state.mouseCursor = dispatch let hash: string | null = null
return state setAllowChat((e) => newSettings.allowChat ?? e)
}), setAllowMarkDraw((e) => newSettings.allowMarkDraw ?? e)
setShips: (ships, index) => setAllowSpecials((e) => newSettings.allowSpecials ?? e)
set((state: State) => { setallowSpectators((e) => newSettings.allowSpectators ?? e)
if (!state.payload) return state const body = getPayloadwithChecksum(getPayloadFromProps())
state.payload.users = state.payload.users.map((e) => { if (!body.hash) {
if (!e || e.index !== index) return e console.log("Something is wrong... ")
e.ships = ships // toast.warn("Something is wrong... ", {
return e // toastId: "st_wrong",
}) // theme: "colored",
return state // })
}), return null
removeShip: ({ size, variant, x, y }, index) => }
set((state: State) => { hash = body.hash
state.payload?.users.map((e) => { setHash(hash)
if (!e || e.index !== index) return return hash
const indexToRemove = e.ships.findIndex( }
(ship) =>
ship.size === size &&
ship.variant === variant &&
ship.x === x &&
ship.y === y,
)
e.ships.splice(indexToRemove, 1)
return e
})
return state
}),
setPlayer: (payload) => {
let hash: string | null = null
set((state: State) => {
if (!state.payload) return state
state.payload.users = payload.users
const body = getPayloadwithChecksum(state.payload)
if (!body.hash) {
// toast.warn("Something is wrong... ", {
// toastId: "st_wrong",
// theme: "colored",
// })
return state export function setGameSetting(newSettings: GameSettings) {
} return () => {
hash = body.hash const hash = setSetting(newSettings)
state.hash = hash socket.emit("gameSetting", newSettings, (newHash) => {
return state if (newHash === hash) return
console.log("hash", hash, newHash)
socket.emit("update", full)
}) })
return hash }
}, }
setSetting: (settings) => {
let hash: string | null = null
set((state: State) => {
if (!state.payload?.game) return state
Object.assign(state.payload.game, settings)
const body = getPayloadwithChecksum(state.payload)
if (!body.hash) {
// toast.warn("Something is wrong... ", {
// toastId: "st_wrong",
// theme: "colored",
// })
return state
}
hash = body.hash
state.hash = hash
return state
})
return hash
},
full: (newGameProps) =>
// eslint-disable-next-line solid/reactivity
set((state) => {
if (state.hash === newGameProps.hash) {
console.log("Everything up to date.")
} else {
console.log("Update was needed.", state.hash, newGameProps.hash)
if ( export function full(newProps: GamePropsSchema) {
state.payload?.game?.id && if (hash() === newProps.hash) {
state.payload?.game?.id !== newGameProps.payload?.game?.id console.log("Everything up to date.")
) { } else {
console.warn( console.log("Update was needed.", hash(), newProps.hash)
"Different gameId detected on update: ",
state.payload?.game?.id,
newGameProps.payload?.game?.id,
)
}
return newGameProps if (gameId() !== newProps.payload?.game?.id)
} console.warn(
return state "Different gameId detected on update: ",
}), gameId(),
leave: (cb) => { newProps.payload?.game?.id,
socket.emit("leave", (ack) => { )
if (!ack) {
// toast.error("Something is wrong...") setHash(newProps.hash)
} setActiveIndex(newProps.payload.activeIndex)
cb() setGamePin(newProps.payload.gamePin)
setGameId(newProps.payload.game?.id ?? "")
setGameState(newProps.payload.game?.state ?? "unknown")
setAllowChat(newProps.payload.game?.allowChat ?? false)
setAllowMarkDraw(newProps.payload.game?.allowMarkDraw ?? false)
setAllowSpecials(newProps.payload.game?.allowSpecials ?? false)
setallowSpectators(newProps.payload.game?.allowSpectators ?? false)
users.forEach((user, i) => {
const newUser = newProps.payload.users[i]
if (!newUser) return
user.setId(newUser.id)
user.setName(newUser.name)
user.setChats(newUser.chats)
user.setMoves(newUser.moves)
user.setShips(newUser.ships)
user.setHits(newUser.hits)
}) })
}, }
setIsReady: ({ i, isReady }) => }
set((state: State) => { export function leave(cb: () => void) {
state.userStates[i].setIsReady(isReady) socket.emit("leave", (ack) => {
state.userStates[i].setIsConnected(true) if (!ack) {
return state console.log("Something is wrong... ")
}), // toast.error("Something is wrong...")
gameState: (newState: GameState) => }
set((state: State) => { cb()
if (!state.payload?.game) return state })
state.payload.game.state = newState }
state.userStates.forEach((e) => { export function setIsReadyFor({ i, isReady }: { i: 0 | 1; isReady: boolean }) {
e.setIsReady(false) users[i].setIsReady(isReady)
}) users[i].setIsConnected(true)
return state }
}), export function newGameState(newState: GameState) {
setIsConnected: ({ i, isConnected }) => setGameState(newState)
set((state: State) => { users.forEach((e) => e.setIsReady(false))
state.userStates[i].setIsConnected(isConnected) }
if (!isConnected) state.userStates[i].setIsReady(false) export function setIsConnectedFor({
return state i,
}), isConnected,
reset: () => { }: {
set(initialState) i: 0 | 1
}, isConnected: boolean
})) }) {
users[i].setIsConnected(isConnected)
if (isConnected) return
users[i].setIsReady(false)
}
export function reset() {
setHash(null)
setActiveIndex(0)
setGamePin(null)
setGameId("")
setGameState("unknown")
setallowSpectators(false)
setAllowSpecials(false)
setAllowChat(false)
setAllowMarkDraw(false)
setMenu("moves")
setMode(0)
setTarget(initlialTarget)
setTargetPreview(initlialTargetPreview)
setMouseCursor(initlialMouseCursor)
users.forEach(defaultUser)
}
function defaultUser(user: ReturnType<typeof initialUser>) {
user.setIsReady(false)
user.setIsConnected(false)
user.setId("")
user.setName("")
user.setChats([])
user.setMoves([])
user.setShips([])
user.setHits([])
}

View file

@ -1,25 +0,0 @@
import { useSession } from "~/hooks/useSession"
import { useGameProps } from "./useGameProps"
function useIndex() {
const { payload } = useGameProps()
const session = useSession()
const selfIndex =
payload?.users.findIndex((e) => e?.id === session.latest?.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

@ -1,12 +0,0 @@
import { getSession } from "@auth/solid-start"
import { createServerData$ } from "solid-start/server"
import { authOptions } from "~/server/auth"
export const useSession = () => {
return createServerData$(
async (_, { request }) => {
return await getSession(request, authOptions)
},
{ key: () => ["auth_user"] },
)
}

View file

@ -0,0 +1,77 @@
import { Session } from "@auth/core/types"
import { getSession } from "@auth/solid-start"
import {
Accessor,
JSX,
createContext,
createSignal,
useContext,
} from "solid-js"
import { createServerData$ } from "solid-start/server"
import { ShipProps } from "~/interfaces/frontend"
import { initialUser } from "~/lib/utils/helpers"
import { authOptions } from "~/server/auth"
import { activeIndex, users } from "./useGameProps"
interface Concext {
session: Accessor<Session | null>
selfIndex: () => 0 | 1 | -1
activeIndex: Accessor<0 | 1>
isActiveIndex: () => boolean
selfUser: () => ReturnType<typeof initialUser> | null
activeUser: () => ReturnType<typeof initialUser>
ships: () => ShipProps[]
}
const [state, setState] = createSignal<Session | null>(null)
const selfIndex = () => {
switch (state()?.user?.id) {
case users[0].id():
return 0
case users[1].id():
return 1
default:
return -1
}
}
const isActiveIndex = () => {
const sI = selfIndex()
return sI >= 0 && activeIndex() === sI
}
const selfUser = () => {
const i = selfIndex()
if (i === -1) return null
return users[i]
}
const activeUser = () => users[activeIndex() === 0 ? 1 : 0]
const ships = () => selfUser()?.ships() ?? []
const contextValue = {
session: state,
selfIndex,
activeIndex,
isActiveIndex,
selfUser,
activeUser,
ships,
}
export const SessionCtx = createContext<Concext>(contextValue)
export function SessionProvider(props: { children: JSX.Element }) {
const session = createServerData$(
async (_, { request }) => {
return await getSession(request, authOptions)
},
{ key: () => ["auth_user"] },
)()
setState(session ?? null)
return (
<SessionCtx.Provider value={contextValue}>
{props.children}
</SessionCtx.Provider>
)
}
export const useSession = () => useContext(SessionCtx)

View file

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

View file

@ -6,35 +6,41 @@ import { socket } from "~/lib/socket"
import { GamePropsSchema } from "~/lib/zodSchemas" import { GamePropsSchema } from "~/lib/zodSchemas"
import { isAuthenticated } from "~/routes/start" import { isAuthenticated } from "~/routes/start"
import { GameSettings, PlayerEvent } from "../interfaces/frontend" import { GameSettings, PlayerEvent } from "../interfaces/frontend"
import { useGameProps } from "./useGameProps" import {
import useIndex from "./useIndex" DispatchMove,
full,
gameId,
gameState,
setActiveIndex,
setIsConnectedFor,
setIsReadyFor,
setMenu,
setMode,
setPlayer,
setSetting,
setShips,
users,
} from "./useGameProps"
import { useSession } from "./useSession"
/** 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] = createSignal(false) const [isConnectedState, setIsConnectedState] = createSignal(false)
const { selfIndex } = useIndex() const { selfIndex } = useSession()
const {
payload,
userStates,
setPlayer,
setSetting,
full,
setIsReady,
gameState,
setIsConnected,
setActiveIndex,
DispatchMove,
setShips,
} = useGameProps()
const navigate = useNavigate() const navigate = useNavigate()
const isConnected = () => const isConnected = () => {
selfIndex >= 0 ? userStates[selfIndex].isConnected() : isConnectedState() const i = selfIndex()
return i !== -1
? users[i].isConnected() && isConnectedState()
: isConnectedState()
}
createEffect(() => { createEffect(() => {
if (selfIndex < 0) return const i = selfIndex()
setIsConnected({ if (i === -1) return
i: selfIndex, setIsConnectedFor({
i,
isConnected: isConnectedState(), isConnected: isConnectedState(),
}) })
}) })
@ -63,39 +69,51 @@ function useSocket() {
const playerEvent = (event: PlayerEvent) => { const playerEvent = (event: PlayerEvent) => {
const { type, i } = event const { type, i } = event
// let message: string let message: string
console.log("playerEvent", type) console.log("playerEvent", type)
switch (type) { switch (type) {
case "disconnect": case "disconnect":
setIsConnected({ setIsConnectedFor({
i, i,
isConnected: false, isConnected: false,
}) })
// message = "Player is disconnected." message = "Player is disconnected."
break break
case "leave": case "leave":
// message = "Player has left the lobby." message = "Player has left the lobby."
break break
case "connect": case "connect":
setIsConnected({ setIsConnectedFor({
i, i,
isConnected: true, isConnected: true,
}) })
socket.emit("isReady", userStates[selfIndex].isReady()) const index = selfIndex()
// message = "Player has joined the lobby." if (index !== -1) socket.emit("isReady", users[index].isReady())
message = "Player has joined the lobby."
break break
default: default:
// message = "Not defined yet." message = "Not defined yet."
break break
} }
// toast.info(message, { toastId: message }) // toast.info(message, { toastId: message })
console.log(message)
if (type === "disconnect") return if (type === "disconnect") return
const { payload, hash } = event
const newHash = setPlayer(payload) const { hash } = event
console.log(newHash, hash, !newHash, newHash === hash) const newHash = setPlayer(event.users)
if (!newHash || newHash === hash) return
console.log("hash", hash, newHash)
socket.emit("update", (body) => {
console.log("Update is needed after ", type)
full(body)
})
}
const gameSetting = (newSettings: GameSettings, hash: string) => {
const newHash = setSetting(newSettings)
if (!newHash || newHash === hash) return if (!newHash || newHash === hash) return
console.log("hash", hash, newHash) console.log("hash", hash, newHash)
socket.emit("update", (body) => { socket.emit("update", (body) => {
@ -104,18 +122,17 @@ function useSocket() {
}) })
} }
const gameSetting = (payload: GameSettings, hash: string) => { const activeIndex = (i: 0 | 1) => {
const newHash = setSetting(payload) setActiveIndex(i)
if (!newHash || newHash === hash) return if (i === selfIndex()) {
console.log("hash", hash, newHash) setMenu("moves")
socket.emit("update", (body) => { setMode(0)
console.log("update") } else {
full(body) setMenu("main")
}) setMode(-1)
}
} }
const activeIndex = (i: number) => setActiveIndex(i, selfIndex)
const disconnect = () => { const disconnect = () => {
console.log("disconnect") console.log("disconnect")
setIsConnectedState(false) setIsConnectedState(false)
@ -125,7 +142,7 @@ function useSocket() {
socket.on("connect_error", connectError) socket.on("connect_error", connectError)
socket.on("gameSetting", gameSetting) socket.on("gameSetting", gameSetting)
socket.on("playerEvent", playerEvent) socket.on("playerEvent", playerEvent)
socket.on("isReady", setIsReady) socket.on("isReady", setIsReadyFor)
socket.on("gameState", gameState) socket.on("gameState", gameState)
socket.on("dispatchMove", DispatchMove) socket.on("dispatchMove", DispatchMove)
socket.on("activeIndex", activeIndex) socket.on("activeIndex", activeIndex)
@ -137,7 +154,7 @@ function useSocket() {
socket.off("connect_error", connectError) socket.off("connect_error", connectError)
socket.off("gameSetting", gameSetting) socket.off("gameSetting", gameSetting)
socket.off("playerEvent", playerEvent) socket.off("playerEvent", playerEvent)
socket.off("isReady", setIsReady) socket.off("isReady", setIsReadyFor)
socket.off("gameState", gameState) socket.off("gameState", gameState)
socket.off("dispatchMove", DispatchMove) socket.off("dispatchMove", DispatchMove)
socket.off("activeIndex", activeIndex) socket.off("activeIndex", activeIndex)
@ -147,7 +164,7 @@ function useSocket() {
}) })
createEffect(() => { createEffect(() => {
if (!payload?.game?.id) { if (!gameId()) {
socket.disconnect() socket.disconnect()
fetch("/api/game/running", { fetch("/api/game/running", {
method: "GET", method: "GET",
@ -168,8 +185,7 @@ function useSocket() {
}) })
return { return {
isConnected: isConnected,
selfIndex >= 0 ? userStates[selfIndex].isConnected : isConnectedState,
} }
} }

View file

@ -1,6 +1,5 @@
import { Session } from "@auth/core/types" import { Session } from "@auth/core/types"
import type { Server as HTTPServer } from "http" import http from "http"
import type { Socket as NetSocket } from "net"
import type { import type {
Server as IOServer, Server as IOServer,
Server, Server,
@ -17,33 +16,25 @@ import {
ShipProps, ShipProps,
} from "./frontend" } from "./frontend"
interface SocketServer extends HTTPServer { export interface SocketServer extends http.Server {
io?: IOServer io?: IOServer
} }
interface SocketWithIO extends NetSocket {
server: SocketServer
}
export interface ResponseWithSocket extends Response {
socket: SocketWithIO
}
export interface ServerToClientEvents { export interface ServerToClientEvents {
// noArg: () => void // noArg: () => void
// basicEmit: (a: number, b: string, c: Buffer) => void // basicEmit: (a: number, b: string, c: Buffer) => void
// withAck: (d: string, ) => void // withAck: (d: string, ) => void
gameSetting: (payload: GameSettings, hash: string) => void gameSetting: (payload: GameSettings, hash: string) => void
playerEvent: (event: PlayerEvent) => void playerEvent: (event: PlayerEvent) => void
isReady: (payload: { i: number; isReady: boolean }) => void isReady: (payload: { i: 0 | 1; isReady: boolean }) => void
isConnected: (payload: { i: number; isConnected: boolean }) => void isConnected: (payload: { i: 0 | 1; isConnected: boolean }) => void
"get-canvas-state": () => void "get-canvas-state": () => void
"canvas-state-from-server": (state: string, userIndex: number) => void "canvas-state-from-server": (state: string, userIndex: number) => void
"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[], index: number) => void ships: (ships: ShipProps[], index: number) => void
activeIndex: (index: number) => void activeIndex: (index: 0 | 1) => void
dispatchMove: (props: MoveDispatchProps, i: number) => void dispatchMove: (props: MoveDispatchProps, i: number) => void
} }
@ -75,7 +66,7 @@ interface SocketData {
} }
user: Session["user"] user: Session["user"]
gameId: string gameId: string
index: number index: 0 | 1
} }
export type sServer = Server< export type sServer = Server<

View file

@ -76,15 +76,16 @@ export type GameSettingKeys =
| "allowChat" | "allowChat"
| "allowMarkDraw" | "allowMarkDraw"
export type GameSettings = { [key in GameSettingKeys]?: boolean } export type GameSettings = Partial<Record<GameSettingKeys, boolean>>
export type PlayerEvent = export type PlayerEvent =
| { | {
type: "connect" | "leave" type: "connect" | "leave"
i: number i: 0 | 1
payload: { users: PlayerSchema[] } users: NewUsers
hash: string hash: string
} }
| { | {
type: "disconnect" type: "disconnect"
i: number i: 0 | 1
} }
export type NewUsers = { 0: PlayerSchema | null; 1: PlayerSchema | null }

View file

@ -8,7 +8,7 @@ const pinBodySchema = z.object({
async function getPinFromBody(request: APIEvent["request"]) { async function getPinFromBody(request: APIEvent["request"]) {
try { try {
const body = request.json() const body = await request.json()
const { pin } = pinBodySchema.parse(body) const { pin } = pinBodySchema.parse(body)
return pin return pin
} catch { } catch {

View file

@ -59,10 +59,8 @@ async function logging(
const xForwardedFor = const xForwardedFor =
typeof request.headers.get === "function" typeof request.headers.get === "function"
? request.headers.get("x-forwarded-for") ? request.headers.get("x-forwarded-for")
: "0.0.0.0" // request.headers : // @ts-expect-error Bad IncomingHttpHeaders Type
if (typeof request.headers.get !== "function") request.headers["x-forwarded-for"]
console.log("IncomingHttpHeaders", request.headers)
// ("x-forwarded-for")
const ip = (xForwardedFor || "127.0.0.1, 192.168.178.1").split(",") const ip = (xForwardedFor || "127.0.0.1, 192.168.178.1").split(",")
const route = request.url const route = request.url
messages.console = [ip[0].yellow, route?.green, messages.console].join( messages.console = [ip[0].yellow, route?.green, messages.console].join(

View file

@ -0,0 +1,35 @@
import {
activeIndex,
allowChat,
allowMarkDraw,
allowSpecials,
allowSpectators,
gameId,
gamePin,
gameState,
users,
} from "~/hooks/useGameProps"
export function getPayloadFromProps() {
return {
game: {
id: gameId(),
state: gameState(),
allowChat: allowChat(),
allowMarkDraw: allowMarkDraw(),
allowSpecials: allowSpecials(),
allowSpectators: allowSpectators(),
},
gamePin: gamePin(),
users: users.map((user, i) => ({
index: i,
id: user.id(),
name: user.name(),
chats: user.chats(),
moves: user.moves(),
ships: user.ships(),
hits: user.hits(),
})),
activeIndex: activeIndex(),
}
}

View file

@ -1,10 +1,6 @@
import crypto from "crypto" import hash from "object-hash"
import { GamePropsSchema } from "./zodSchemas" import { GamePropsSchema } from "./zodSchemas"
export function getPayloadwithChecksum( export const getPayloadwithChecksum = (
payload: GamePropsSchema["payload"], payload: GamePropsSchema["payload"],
): GamePropsSchema { ): GamePropsSchema => ({ payload, hash: hash(payload) })
const objString = JSON.stringify(payload)
const hash = crypto.createHash("md5").update(objString).digest("hex")
return { payload, hash }
}

View file

@ -1,5 +1,5 @@
import { io } from "socket.io-client" import { io } from "socket.io-client"
import { cSocket } from "../interfaces/NextApiSocket" import { cSocket } from "../interfaces/ApiSocket"
export const socket: cSocket = io({ export const socket: cSocket = io({
path: "/api/ws", path: "/api/ws",

View file

@ -1,3 +1,4 @@
import { createSignal } from "solid-js"
import { count } from "~/components/Gamefield/Gamefield" import { count } from "~/components/Gamefield/Gamefield"
import type { import type {
Hit, Hit,
@ -10,7 +11,7 @@ import type {
TargetList, TargetList,
TargetPreview, TargetPreview,
} from "../../interfaces/frontend" } from "../../interfaces/frontend"
import { Orientation } from "../zodSchemas" import { ChatSchema, MoveSchema, Orientation } from "../zodSchemas"
export function borderCN(count: number, x: number, y: number) { export function borderCN(count: number, x: number, y: number) {
if (x === 0) return "left" if (x === 0) return "left"
@ -129,6 +130,34 @@ export const initlialMouseCursor = {
x: 0, x: 0,
y: 0, y: 0,
} }
export function initialUser() {
const [isReady, setIsReady] = createSignal(false)
const [isConnected, setIsConnected] = createSignal(false)
const [id, setId] = createSignal<string>("")
const [name, setName] = createSignal<string>("")
const [chats, setChats] = createSignal<ChatSchema[]>([])
const [moves, setMoves] = createSignal<MoveSchema[]>([])
const [ships, setShips] = createSignal<ShipProps[]>([])
const [hits, setHits] = createSignal<Hit[]>([])
return {
isReady,
setIsReady,
isConnected,
setIsConnected,
id,
setId,
name,
setName,
chats,
setChats,
moves,
setMoves,
ships,
setShips,
hits,
setHits,
}
}
export const shipProps = ( export const shipProps = (
ships: ShipProps[], ships: ShipProps[],

View file

@ -34,44 +34,52 @@ export const movesSchema = createSelectSchema(moves)
export const chatsSchema = createSelectSchema(chats) export const chatsSchema = createSelectSchema(chats)
export const user_gamesSchema = createSelectSchema(user_games) export const user_gamesSchema = createSelectSchema(user_games)
export const ChatSchema = z.object({
id: z.string(),
event: z.string().nullable(),
message: z.string().nullable(),
createdAt: z.coerce.date(),
})
export type ChatSchema = z.infer<typeof ChatSchema>
export const MoveSchema = z.object({
index: z.number(),
type: MoveType,
x: z.number(),
y: z.number(),
orientation: Orientation,
})
export type MoveSchema = z.infer<typeof MoveSchema>
export const ShipShema = z.object({
size: z.number(),
variant: z.number(),
x: z.number(),
y: z.number(),
orientation: Orientation,
})
export type ShipShema = z.infer<typeof ShipShema>
export const HitSchema = z.object({
x: z.number(),
y: z.number(),
hit: z.boolean(),
})
export type HitSchema = z.infer<typeof HitSchema>
export const PlayerSchema = z export const PlayerSchema = z
.object({ .object({
id: z.string(), id: z.string(),
name: z.string().nullable(), name: z.string(),
index: z.number(), index: z.number(),
chats: z chats: ChatSchema.array(),
.object({ moves: MoveSchema.array(),
id: z.string(), ships: ShipShema.array(),
event: z.string().nullable(), hits: HitSchema.array(),
message: z.string().nullable(),
createdAt: z.coerce.date(),
})
.array(),
moves: z
.object({
index: z.number(),
type: MoveType,
x: z.number(),
y: z.number(),
orientation: Orientation,
})
.array(),
ships: z
.object({
size: z.number(),
variant: z.number(),
x: z.number(),
y: z.number(),
orientation: Orientation,
})
.array(),
hits: z
.object({
x: z.number(),
y: z.number(),
hit: z.boolean(),
})
.array(),
}) })
.nullable() .nullable()
@ -89,8 +97,8 @@ export const CreateSchema = z.object({
}) })
.nullable(), .nullable(),
gamePin: z.string().nullable(), gamePin: z.string().nullable(),
users: PlayerSchema.array(), users: z.object({ 0: PlayerSchema, 1: PlayerSchema }),
activeIndex: z.number().optional(), activeIndex: z.literal(0).or(z.literal(1)),
}) })
export const GamePropsSchema = z.object({ export const GamePropsSchema = z.object({
@ -102,5 +110,6 @@ export const optionalGamePropsSchema = z.object({
hash: z.string().nullable(), hash: z.string().nullable(),
}) })
export type CreateSchema = z.infer<typeof CreateSchema>
export type GamePropsSchema = z.infer<typeof GamePropsSchema> export type GamePropsSchema = z.infer<typeof GamePropsSchema>
export type optionalGamePropsSchema = z.infer<typeof optionalGamePropsSchema> export type optionalGamePropsSchema = z.infer<typeof optionalGamePropsSchema>

View file

@ -13,6 +13,7 @@ import {
Scripts, Scripts,
Title, Title,
} from "solid-start" } from "solid-start"
import { SessionProvider } from "./hooks/useSession"
import "./styles/App.scss" import "./styles/App.scss"
import "./styles/globals.scss" import "./styles/globals.scss"
import "./styles/grid.scss" import "./styles/grid.scss"
@ -41,9 +42,11 @@ export default function Root() {
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<Suspense fallback={<div>Loading</div>}> <Suspense fallback={<div>Loading</div>}>
<ErrorBoundary> <ErrorBoundary>
<Routes> <SessionProvider>
<FileRoutes /> <Routes>
</Routes> <FileRoutes />
</Routes>
</SessionProvider>
</ErrorBoundary> </ErrorBoundary>
</Suspense> </Suspense>
<Scripts /> <Scripts />

View file

@ -0,0 +1,5 @@
import { redirect } from "solid-start"
export function GET() {
return redirect("/")
}

View file

@ -1,4 +1,5 @@
import { getSession } from "@auth/solid-start" import { getSession } from "@auth/solid-start"
import { createId } from "@paralleldrive/cuid2"
import { eq } from "drizzle-orm" import { eq } from "drizzle-orm"
import { APIEvent } from "solid-start" import { APIEvent } from "solid-start"
import db from "~/drizzle" import db from "~/drizzle"
@ -30,17 +31,35 @@ export async function POST({ request }: APIEvent) {
message: "Running game already exists.", message: "Running game already exists.",
}) })
} else { } else {
const gameId = (await db.insert(games).values({}).returning())[0].id const gameId = (
await db
.insert(games)
.values({
id: createId(),
})
.returning()
)[0].id
const user_Game = ( const user_Game = (
await db await db
.insert(user_games) .insert(user_games)
.values({ gameId, userId: id, index: 0 }) .values({
id: createId(),
gameId,
userId: id,
index: 0,
})
.returning() .returning()
)[0] )[0]
await db.insert(gamepins).values({ gameId, pin }) await db.insert(gamepins).values({
await db id: createId(),
.insert(chats) gameId,
.values({ user_game_id: user_Game.id, event: "created" }) pin,
})
await db.insert(chats).values({
id: createId(),
user_game_id: user_Game.id,
event: "created",
})
game = await db.query.games.findFirst({ game = await db.query.games.findFirst({
where: eq(games.id, gameId), where: eq(games.id, gameId),
...gameSelects, ...gameSelects,

View file

@ -1,8 +1,9 @@
import { getSession } from "@auth/solid-start" import { getSession } from "@auth/solid-start"
import { and, eq, exists, inArray, ne } from "drizzle-orm" import { createId } from "@paralleldrive/cuid2"
import { and, eq } from "drizzle-orm"
import { APIEvent } from "solid-start" import { APIEvent } from "solid-start"
import db from "~/drizzle" import db from "~/drizzle"
import { user_games, users } from "~/drizzle/schemas/Tables" import { user_games } from "~/drizzle/schemas/Tables"
import { rejectionErrors } from "~/lib/backend/errors" import { rejectionErrors } from "~/lib/backend/errors"
import getPinFromBody from "~/lib/backend/getPinFromBody" import getPinFromBody from "~/lib/backend/getPinFromBody"
import logging from "~/lib/backend/logging" import logging from "~/lib/backend/logging"
@ -34,28 +35,15 @@ export async function POST({ request }: APIEvent) {
}) })
} }
let game = await db.query.games.findFirst({ const user_Game = await db.query.user_games.findFirst({
where: (game) => where: and(
and( eq(user_games.userId, id),
ne(game.state, "ended"), eq(user_games.gameId, gamePin.gameId),
exists( ),
db with: { game: gameSelects },
.select()
.from(user_games)
.where(
inArray(
user_games.userId,
db
.select({ data: users.id })
.from(users)
.where(eq(users.id, id)),
),
),
),
),
...gameSelects,
}) })
if (!game) {
if (user_Game) {
return sendResponse(request, { return sendResponse(request, {
message: "Spieler ist bereits in Spiel!", message: "Spieler ist bereits in Spiel!",
redirectUrl: "/api/game/running", redirectUrl: "/api/game/running",
@ -63,18 +51,17 @@ export async function POST({ request }: APIEvent) {
}) })
} }
const gameId = ( await db
await db .insert(user_games)
.insert(user_games) .values({
.values({ id: createId(),
gameId: game.id, gameId: gamePin.gameId,
userId: id, userId: id,
index: 1, index: 1,
}) })
.returning() .returning()
)[0].gameId
game = await getGameById(gameId) const game = await getGameById(gamePin.gameId)
if (!game) return if (!game) return
@ -87,11 +74,8 @@ export async function POST({ request }: APIEvent) {
}) })
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) { } catch (err: any) {
await logging( await logging(err.code + err.meta + err.message, ["error"], request)
"HERE".red + err.code + err.meta + err.message, console.log(err)
["error"],
request,
)
throw sendError(request, rejectionErrors.gameNotFound) throw sendError(request, rejectionErrors.gameNotFound)
} }
} }

View file

@ -2,7 +2,7 @@ import { getSession } from "@auth/solid-start"
import { and, eq, exists, ne } from "drizzle-orm" import { and, eq, exists, ne } from "drizzle-orm"
import { APIEvent } from "solid-start/api" import { APIEvent } from "solid-start/api"
import db from "~/drizzle" import db from "~/drizzle"
import { games, user_games, users } from "~/drizzle/schemas/Tables" import { games, user_games } from "~/drizzle/schemas/Tables"
import { rejectionErrors } from "~/lib/backend/errors" import { rejectionErrors } from "~/lib/backend/errors"
import sendResponse from "~/lib/backend/sendResponse" import sendResponse from "~/lib/backend/sendResponse"
import { getPayloadwithChecksum } from "~/lib/getPayloadwithChecksum" import { getPayloadwithChecksum } from "~/lib/getPayloadwithChecksum"
@ -87,10 +87,7 @@ export const getRunningGameToUser = async (userId: string) => {
and( and(
ne(game.state, "ended"), ne(game.state, "ended"),
exists( exists(
db db.select().from(user_games).where(eq(user_games.userId, userId)),
.select()
.from(user_games)
.where(exists(db.select().from(users).where(eq(users.id, userId)))),
), ),
), ),
...gameSelects, ...gameSelects,
@ -100,23 +97,41 @@ export const getRunningGameToUser = async (userId: string) => {
export function composeBody( export function composeBody(
gameDB: NonNullable<Awaited<ReturnType<typeof getRunningGameToUser>>>, gameDB: NonNullable<Awaited<ReturnType<typeof getRunningGameToUser>>>,
): GamePropsSchema { ): GamePropsSchema {
const { gamePin, ...game } = gameDB const { gamePin, users, ...game } = gameDB
const users = gameDB.users const mappedUsers = users.map(({ user, ...props }) => ({
.map(({ user, ...props }) => ({ ...props,
...props, ...user,
...user, }))
})) const composedUsers = {
.sort((user1, user2) => user1.index - user2.index) 0: mappedUsers.find((e) => e.index === 0) ?? {
let activeIndex = undefined index: 0,
id: "",
name: "",
chats: [],
moves: [],
ships: [],
hits: [],
},
1: mappedUsers.find((e) => e.index === 1) ?? {
index: 1,
id: "",
name: "",
chats: [],
moves: [],
ships: [],
hits: [],
},
}
let activeIndex: 0 | 1 = 0
if (game.state === "running") { if (game.state === "running") {
const l1 = game.users[0].moves.length const l1 = users[0]?.moves.length ?? 0
const l2 = game.users[1].moves.length const l2 = users[1]?.moves.length ?? 0
activeIndex = l1 > l2 ? 1 : 0 activeIndex = l1 > l2 ? 1 : 0
} }
const payload = { const payload = {
game: game, game: game,
gamePin: gamePin?.pin ?? null, gamePin: gamePin?.pin ?? null,
users, users: composedUsers,
activeIndex, activeIndex,
} }
return getPayloadwithChecksum(payload) return getPayloadwithChecksum(payload)

View file

@ -1,4 +1,5 @@
import { getSession } from "@auth/solid-start" import { getSession } from "@auth/solid-start"
import { createId } from "@paralleldrive/cuid2"
import colors from "colors" import colors from "colors"
import { and, eq } from "drizzle-orm" import { and, eq } from "drizzle-orm"
import status from "http-status" import status from "http-status"
@ -6,7 +7,7 @@ import { Server } from "socket.io"
import { APIEvent } from "solid-start" import { APIEvent } from "solid-start"
import db from "~/drizzle" import db from "~/drizzle"
import { games, moves, ships, user_games } from "~/drizzle/schemas/Tables" import { games, moves, ships, user_games } from "~/drizzle/schemas/Tables"
import { ResponseWithSocket, sServer } from "~/interfaces/NextApiSocket" import { SocketServer, sServer } from "~/interfaces/ApiSocket"
import logging from "~/lib/backend/logging" import logging from "~/lib/backend/logging"
import { GamePropsSchema } from "~/lib/zodSchemas" import { GamePropsSchema } from "~/lib/zodSchemas"
import { authOptions } from "~/server/auth" import { authOptions } from "~/server/auth"
@ -19,27 +20,33 @@ import {
colors.enable() colors.enable()
const res = new Response() as ResponseWithSocket export async function GET({
request,
export async function GET({ request }: APIEvent) { httpServer,
if (res.socket.server.io) { }: APIEvent & { httpServer: SocketServer }) {
if (httpServer.io) {
logging("Socket is already running " + request.url, ["infoCyan"], request) logging("Socket is already running " + request.url, ["infoCyan"], request)
} else { } else {
logging("Socket is initializing " + request.url, ["infoCyan"], request) logging("Socket is initializing " + request.url, ["infoCyan"], request)
const io: sServer = new Server(res.socket.server, { const io: sServer = new Server(httpServer, {
path: "/api/ws", path: "/api/ws",
cors: { cors: {
origin: "https://leaky-ships.mal-noh.de", origin: "https://leaky-ships.mal-noh.de",
}, },
}) })
res.socket.server.io = io httpServer.io = io
// io.use(authenticate) // io.use(authenticate)
io.use(async (socket, next) => { io.use(async (socket, next) => {
try { try {
// @ts-expect-error TODO add correct server const url = process.env.AUTH_URL! + socket.request.url
const session = await getSession(socket.request, authOptions) const session = await getSession(
new Request(url, {
headers: socket.request.headers as Record<string, string>,
}),
authOptions,
)
if (!session) return next(new Error(status["401"])) if (!session) return next(new Error(status["401"]))
socket.data.user = session.user socket.data.user = session.user
@ -55,18 +62,15 @@ export async function GET({ request }: APIEvent) {
} }
const { payload, hash } = composeBody(game) const { payload, hash } = composeBody(game)
// let index: number | null = null const index = payload.users[0]?.id === socket.data.user?.id ? 0 : 1
const index = payload.users.findIndex( if (index !== 0 && index !== 1) return next(new Error(status["401"]))
(user) => socket.data.user?.id === user?.id,
)
if (index < 0) return next(new Error(status["401"]))
socket.data.index = index socket.data.index = index
socket.data.gameId = game.id socket.data.gameId = game.id
socket.join(game.id) socket.join(game.id)
socket.to(game.id).emit("playerEvent", { socket.to(game.id).emit("playerEvent", {
type: "connect", type: "connect",
i: socket.data.index, i: socket.data.index,
payload: { users: payload.users }, users: payload.users,
hash, hash,
}) })
@ -89,14 +93,16 @@ export async function GET({ request }: APIEvent) {
socket.on("update", async (cb) => { socket.on("update", async (cb) => {
const game = await getGameById(socket.data.gameId ?? "") const game = await getGameById(socket.data.gameId ?? "")
if (!game) return if (!game) return
if (socket.data.index === 1 && game.users.length === 1)
socket.data.index = 0
const body = composeBody(game) const body = composeBody(game)
cb(body) cb(body)
}) })
socket.on("gameSetting", async (payload, cb) => { socket.on("gameSetting", async (newSettings, cb) => {
const game = await db const game = await db
.update(games) .update(games)
.set(payload) .set(newSettings)
.where(eq(games.id, socket.data.gameId)) .where(eq(games.id, socket.data.gameId))
.returning() .returning()
.then((updatedGame) => .then((updatedGame) =>
@ -109,41 +115,41 @@ export async function GET({ request }: APIEvent) {
const { hash } = composeBody(game) const { hash } = composeBody(game)
if (!hash) return if (!hash) return
cb(hash) cb(hash)
socket.to(game?.id).emit("gameSetting", payload, hash) socket.to(game?.id).emit("gameSetting", newSettings, hash)
}) })
socket.on("ping", (callback) => callback()) socket.on("ping", (callback) => callback())
socket.on("leave", async (cb) => { socket.on("leave", async (cb) => {
if (!socket.data.gameId || !socket.data.user?.id) return cb(false) const user_Game = (
const user_Game = await db await db
.delete(user_games) .delete(user_games)
.where( .where(
and( and(
eq(user_games.gameId, socket.data.gameId), eq(user_games.gameId, socket.data.gameId),
eq(user_games.userId, socket.data.user?.id ?? ""), eq(user_games.userId, socket.data.user?.id ?? ""),
), ),
) )
.returning() .returning()
)[0]
if (!user_Game) return if (!user_Game) return
const enemy = await db.query.user_games.findFirst({ const enemy = await db.query.user_games.findFirst({
where: eq(user_games.gameId, socket.data.gameId), where: eq(user_games.gameId, socket.data.gameId),
}) })
let body: GamePropsSchema let body: GamePropsSchema
if (user_Game[0].index === 1 && enemy) { if (user_Game.index === 0 && enemy) {
const game = await db const game = await db
.update(user_games) .update(user_games)
.set({ .set({
index: 1, index: 0,
}) })
.where( .where(
and( and(
eq(user_games.gameId, socket.data.gameId), eq(user_games.gameId, socket.data.gameId),
eq(user_games.index, 2), eq(user_games.index, 1),
), ),
) )
.returning() .returning()
.then((user_Game) => .then((user_Game) =>
db.query.games.findFirst({ db.query.games.findFirst({
where: eq(games.id, user_Game[0].gameId), where: eq(games.id, user_Game[0].gameId),
@ -161,17 +167,16 @@ export async function GET({ request }: APIEvent) {
body = composeBody(game) body = composeBody(game)
} }
const { payload, hash } = body const { payload, hash } = body
if (!payload || !hash || socket.data.index === undefined)
return cb(false)
socket.to(socket.data.gameId).emit("playerEvent", { socket.to(socket.data.gameId).emit("playerEvent", {
type: "leave", type: "leave",
i: socket.data.index, i: socket.data.index,
payload: { users: payload.users }, users: payload.users,
hash, hash,
}) })
socket.data.gameId = ""
cb(true) cb(true)
if (!payload.users.length) { if (!payload.users[0] && !payload.users[1]) {
await db.delete(games).where(eq(games.id, socket.data.gameId)) await db.delete(games).where(eq(games.id, socket.data.gameId))
} }
}) })
@ -239,11 +244,13 @@ export async function GET({ request }: APIEvent) {
}) })
if (!user_Game) return if (!user_Game) return
await db await db.insert(ships).values(
.insert(ships) shipsData.map((ship) => ({
.values( id: createId(),
shipsData.map((ship) => ({ ...ship, user_game_id: user_Game.id })), user_game_id: user_Game.id,
) ...ship,
})),
)
socket socket
.to(socket.data.gameId) .to(socket.data.gameId)
@ -270,7 +277,7 @@ export async function GET({ request }: APIEvent) {
if (!user_Game?.game) return if (!user_Game?.game) return
await db await db
.insert(moves) .insert(moves)
.values({ ...props, user_game_id: user_Game.id }) .values({ ...props, id: createId(), user_game_id: user_Game.id })
.returning() .returning()
const game = user_Game.game const game = user_Game.game
@ -286,7 +293,7 @@ export async function GET({ request }: APIEvent) {
["debug"], ["debug"],
socket.request, socket.request,
) )
if (socket.data.index === undefined || !socket.data.gameId) return if (!socket.data.gameId) return
socket.to(socket.data.gameId).emit("playerEvent", { socket.to(socket.data.gameId).emit("playerEvent", {
type: "disconnect", type: "disconnect",
i: socket.data.index, i: socket.data.index,
@ -298,5 +305,5 @@ export async function GET({ request }: APIEvent) {
}) })
}) })
} }
return res return new Response()
} }

View file

@ -1,44 +0,0 @@
// import { toast } from "react-toastify"
// import { createEffect } from "solid-js"
// import { useNavigate } from "solid-start"
// import { useGameProps } from "~/hooks/useGameProps"
// import { useSession } from "~/hooks/useSession"
export default function Game() {
// const { payload } = useGameProps()
// const navigate = useNavigate()
// const session = useSession()
// createEffect(() => {
// const gameId = payload?.game?.id
// const path = gameId ? "/game" : "/start"
// toast.promise(navigate(path), {
// pending: {
// render: "Wird weitergeleitet...",
// toastId: "pageLoad",
// },
// success: {
// render: gameId
// ? "Spiel gefunden!"
// : session?.user.id
// ? "Kein laufendes Spiel."
// : "Kein laufendes Spiel. Bitte anmelden.",
// toastId: "pageLoad",
// theme: session?.user.id ? "dark" : undefined,
// type: gameId ? "success" : "info",
// },
// error: {
// render: "Es ist ein Fehler aufgetreten 🤯",
// type: "error",
// toastId: "pageLoad",
// theme: "colored",
// },
// })
// })
return (
<div class="h-full bg-theme">
<div class="mx-auto flex h-full max-w-screen-md flex-col items-center justify-evenly" />
</div>
)
}

View file

@ -3,7 +3,7 @@ import BurgerMenu from "~/components/BurgerMenu"
import Logo from "~/components/Logo" import Logo from "~/components/Logo"
export default function Home() { export default function Home() {
const navigate = useNavigate() const navigator = useNavigate()
return ( return (
<div class="h-full bg-theme"> <div class="h-full bg-theme">
@ -18,7 +18,7 @@ export default function Home() {
class="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" class="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"
onClick={() => onClick={() =>
setTimeout(() => { setTimeout(() => {
navigate("/start") navigator("/start")
}, 200) }, 200)
} }
> >

View file

@ -1,6 +1,6 @@
// import Head from "next/head" // import Head from "next/head"
import classNames from "classnames" import classNames from "classnames"
import { createSignal } from "solid-js" import { Show, createSignal } from "solid-js"
import BurgerMenu from "~/components/BurgerMenu" import BurgerMenu from "~/components/BurgerMenu"
import LobbyFrame from "~/components/Lobby/LobbyFrame" import LobbyFrame from "~/components/Lobby/LobbyFrame"
import Settings from "~/components/Lobby/SettingsFrame/Settings" import Settings from "~/components/Lobby/SettingsFrame/Settings"
@ -26,16 +26,16 @@ export default function Lobby() {
<div <div
class={classNames( class={classNames(
"mx-auto flex h-full max-w-screen-2xl flex-col items-center justify-evenly", "mx-auto flex h-full max-w-screen-2xl flex-col items-center justify-evenly",
{ "blur-sm": settings }, { "blur-sm": settings() },
)} )}
> >
<Logo small={true} /> <Logo small={true} />
<LobbyFrame openSettings={() => setSettings(true)} /> <LobbyFrame openSettings={() => setSettings(true)} />
</div> </div>
<BurgerMenu blur={settings()} /> <BurgerMenu blur={settings()} />
{settings() ? ( <Show when={settings()}>
<Settings closeSettings={() => setSettings(false)} /> <Settings closeSettings={() => setSettings(false)} />
) : null} </Show>
</div> </div>
) )
} }

View file

@ -1,7 +1,7 @@
import { signIn } from "@auth/solid-start/client" import { signIn } from "@auth/solid-start/client"
import { faLeftLong } from "@fortawesome/pro-solid-svg-icons" import { faLeftLong } from "@fortawesome/pro-solid-svg-icons"
import classNames from "classnames" import classNames from "classnames"
import { createEffect, createSignal } from "solid-js" import { Show, createEffect, createSignal } from "solid-js"
import { useNavigate, useSearchParams } from "solid-start" import { useNavigate, useSearchParams } from "solid-start"
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon" import { FontAwesomeIcon } from "~/components/FontAwesomeIcon"
import { useSession } from "~/hooks/useSession" import { useSession } from "~/hooks/useSession"
@ -39,19 +39,20 @@ const errors: Record<SignInErrorTypes, string> = {
function Login() { function Login() {
const [email, setEmail] = createSignal("") const [email, setEmail] = createSignal("")
const { latest } = useSession() const { session } = useSession()
const navigate = useNavigate() const navigator = useNavigate()
const [searchParams] = useSearchParams() const [searchParams] = useSearchParams()
const errorType = searchParams["error"] as SignInErrorTypes const errorType = searchParams["error"] as SignInErrorTypes
createEffect(() => { createEffect(() => {
if (!errorType) return if (!errorType) return
console.error(errors[errorType] ?? errors.default)
// toast.error(errors[errorType] ?? errors.default, { theme: "colored" }) // toast.error(errors[errorType] ?? errors.default, { theme: "colored" })
}) })
createEffect(() => { createEffect(() => {
if (latest?.user?.id) navigate("/") if (session()) navigator("/signout")
}) })
const login = (provider: "email" | "azure-ad") => const login = (provider: "email" | "azure-ad") =>
@ -79,7 +80,14 @@ function Login() {
</div> </div>
{errorType && <hr class="mb-8 border-gray-400" />} {errorType && <hr class="mb-8 border-gray-400" />}
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex flex-col"> <form
class="flex flex-col"
onSubmit={(e) => {
e.preventDefault()
if (!email()) return
login("email")
}}
>
<label for="email" class="mx-2 text-lg"> <label for="email" class="mx-2 text-lg">
Email Email
</label> </label>
@ -95,12 +103,11 @@ function Login() {
<button <button
id="email-submit" id="email-submit"
type="submit" type="submit"
onClick={() => login("email")}
class="my-1 rounded-lg bg-blue-500 bg-opacity-75 px-10 py-3 text-white shadow-inner drop-shadow-md backdrop-blur-md transition-colors duration-300 hover:bg-blue-600" class="my-1 rounded-lg bg-blue-500 bg-opacity-75 px-10 py-3 text-white shadow-inner drop-shadow-md backdrop-blur-md transition-colors duration-300 hover:bg-blue-600"
> >
Sign in with Email Sign in with Email
</button> </button>
</div> </form>
<div class="flex flex-row items-center"> <div class="flex flex-row items-center">
<hr class="w-full" /> <hr class="w-full" />
@ -137,21 +144,19 @@ function Login() {
</button> </button>
</div> </div>
</div> </div>
{errorType ? ( <Show when={errorType}>
<> <hr class="mt-8 border-gray-400" />
<hr class="mt-8 border-gray-400" /> <div class="flex flex-col items-center">
<div class="flex flex-col items-center"> <button
<button id="back"
id="back" onClick={() => navigator("/")}
onClick={() => navigate("/")} class="mt-10 rounded-lg border-2 border-gray-400 bg-gray-500 bg-opacity-75 px-16 py-2 text-white shadow-inner drop-shadow-md backdrop-blur-md transition-colors duration-300 hover:border-blue-600"
class="mt-10 rounded-lg border-2 border-gray-400 bg-gray-500 bg-opacity-75 px-16 py-2 text-white shadow-inner drop-shadow-md backdrop-blur-md transition-colors duration-300 hover:border-blue-600" >
> <FontAwesomeIcon icon={faLeftLong} />
<FontAwesomeIcon icon={faLeftLong} /> <span class="mx-4 font-bold">Return</span>
<span class="mx-4 font-bold">Return</span> </button>
</button> </div>
</div> </Show>
</>
) : null}
</div> </div>
</div> </div>
) )

View file

@ -1,15 +1,16 @@
import { signOut } from "@auth/solid-start/client" import { signOut } from "@auth/solid-start/client"
import { faLeftLong } from "@fortawesome/pro-solid-svg-icons"
import { createEffect } from "solid-js" import { createEffect } from "solid-js"
import { useNavigate } from "solid-start" import { useNavigate } from "solid-start"
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon"
import { useSession } from "~/hooks/useSession" import { useSession } from "~/hooks/useSession"
function Logout() { function Logout() {
const { state } = useSession() const { session } = useSession()
const navigator = useNavigate() const navigator = useNavigate()
createEffect(() => { createEffect(() => {
if (state === "ready") navigator("/signin") // TODO if (!session()) navigator("/signin")
}) })
return ( return (
@ -36,6 +37,17 @@ function Logout() {
Sign out Sign out
</button> </button>
</div> </div>
<hr class="mt-8 border-gray-400" />
<div class="flex flex-col items-center">
<button
id="back"
onClick={() => navigator("/")}
class="mt-10 rounded-lg border-2 border-gray-400 bg-gray-500 bg-opacity-75 px-16 py-2 text-white shadow-inner drop-shadow-md backdrop-blur-md transition-colors duration-300 hover:border-blue-600"
>
<FontAwesomeIcon icon={faLeftLong} />
<span class="mx-4 font-bold">Return</span>
</button>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -10,7 +10,7 @@ import BurgerMenu from "~/components/BurgerMenu"
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon" import { FontAwesomeIcon } from "~/components/FontAwesomeIcon"
import Logo from "~/components/Logo" import Logo from "~/components/Logo"
import OptionButton from "~/components/OptionButton" import OptionButton from "~/components/OptionButton"
import { useGameProps } from "~/hooks/useGameProps" import { full } from "~/hooks/useGameProps"
import { useSession } from "~/hooks/useSession" import { useSession } from "~/hooks/useSession"
export function isAuthenticated(res: Response) { export function isAuthenticated(res: Response) {
@ -44,11 +44,11 @@ export function isAuthenticated(res: Response) {
// } // }
export default function Start() { export default function Start() {
const [otp] = createSignal("") const [otp, setOtp] = createSignal("")
const gameProps = useGameProps()
const location = useLocation() const location = useLocation()
const navigate = useNavigate() const navigator = useNavigate()
const session = useSession() const { session } = useSession()
const [searchParams] = useSearchParams() const [searchParams] = useSearchParams()
const query = () => { const query = () => {
@ -82,7 +82,8 @@ export default function Start() {
// hideProgressBar: true, // hideProgressBar: true,
// closeButton: false, // closeButton: false,
// }) // })
const res = await gameRequestPromise.catch(() => { const res = await gameRequestPromise.catch((err) => {
console.log(err)
// toast.update(toastId, { // toast.update(toastId, {
// render: "Es ist ein Fehler aufgetreten bei der Anfrage 🤯", // render: "Es ist ein Fehler aufgetreten bei der Anfrage 🤯",
// type: "error", // type: "error",
@ -94,13 +95,13 @@ export default function Start() {
// }) // })
}) })
if (!res) return if (!res) return
gameProps.full(res) full(res)
// toast.update(toastId, { // toast.update(toastId, {
// render: "Weiterleitung", // render: "Weiterleitung",
// }) // })
navigate("/lobby") navigator("/lobby")
// .then(() => // .then(() =>
// toast.update(toastId, { // toast.update(toastId, {
// render: "Raum begetreten 👌", // render: "Raum begetreten 👌",
@ -141,19 +142,19 @@ export default function Start() {
class="-mt-2 h-14 w-20 self-start rounded-xl border-b-4 border-shield-gray bg-voidDark text-2xl text-grayish duration-100 active:border-b-0 active:border-t-4 sm:-mt-6 sm:w-40 sm:px-2 sm:text-5xl" class="-mt-2 h-14 w-20 self-start rounded-xl border-b-4 border-shield-gray bg-voidDark text-2xl text-grayish duration-100 active:border-b-0 active:border-t-4 sm:-mt-6 sm:w-40 sm:px-2 sm:text-5xl"
onClick={() => onClick={() =>
setTimeout(() => { setTimeout(() => {
navigate("/") navigator("/")
}, 200) }, 200)
} }
> >
<FontAwesomeIcon icon={faLeftLong} /> <FontAwesomeIcon icon={faLeftLong} />
</button> </button>
{!session.latest?.user?.id && ( {!session()?.user?.id && (
<button <button
id="login" id="login"
class="-mt-2 h-14 w-20 self-start rounded-xl border-b-4 border-orange-500 bg-yellow-500 text-2xl active:border-b-0 active:border-t-4 sm:-mt-6 sm:w-40 sm:px-2 sm:text-4xl" class="-mt-2 h-14 w-20 self-start rounded-xl border-b-4 border-orange-500 bg-yellow-500 text-2xl active:border-b-0 active:border-t-4 sm:-mt-6 sm:w-40 sm:px-2 sm:text-4xl"
onClick={() => onClick={() =>
setTimeout(() => { setTimeout(() => {
navigate("/signin") navigator("/signin")
}, 200) }, 200)
} }
> >
@ -166,12 +167,12 @@ export default function Start() {
text="Raum erstellen" text="Raum erstellen"
callback={gameFetch} callback={gameFetch}
icon={faPlus} icon={faPlus}
disabled={!session.latest} disabled={!session()}
/> />
<OptionButton <OptionButton
text="Raum beitreten" text="Raum beitreten"
callback={() => callback={() =>
navigate( navigator(
location.pathname.concat( location.pathname.concat(
"?", "?",
new URLSearchParams({ q: "join" }).toString(), new URLSearchParams({ q: "join" }).toString(),
@ -179,32 +180,30 @@ export default function Start() {
) )
} }
icon={faUserPlus} icon={faUserPlus}
disabled={!session.latest} disabled={!session()}
nodeWhen={query().join && !!session.latest} nodeWhen={query().join && !!session()}
node={ node={
<> <input value={otp()} onInput={(e) => setOtp(e.target.value)} />
{
// <OtpInput // <OtpInput
// shouldAutoFocus // shouldAutoFocus
// containerStyle={{ color: "initial" }} // containerStyle={{ color: "initial" }}
// value={otp} // value={otp}
// onChange={setOtp} // onChange={setOtp}
// numInputs={4} // numInputs={4}
// inputType="number" // inputType="number"
// inputStyle="inputStyle" // inputStyle="inputStyle"
// placeholder="0000" // placeholder="0000"
// renderSeparator={<span>-</span>} // renderSeparator={<span>-</span>}
// renderInput={(props) => <input {...props} />} // renderInput={(props) => <input {...props} />}
// /> // />
}
</>
} }
/> />
<OptionButton <OptionButton
text="Zuschauen" text="Zuschauen"
icon={faEye} icon={faEye}
callback={() => callback={() =>
navigate( navigator(
location.pathname + location.pathname +
"?" + "?" +
new URLSearchParams({ new URLSearchParams({