diff --git a/leaky-ships/package.json b/leaky-ships/package.json index 6d0da24..ffbf40e 100644 --- a/leaky-ships/package.json +++ b/leaky-ships/package.json @@ -6,7 +6,8 @@ "build": "solid-start build", "lint": "eslint --fix \"**/*.{ts,tsx,js,jsx}\"", "push": "drizzle-kit push:pg", - "test": "pnpm playwright test --ui" + "test": "pnpm playwright test --ui", + "typecheck": "tsc --noEmit --checkJs false --skipLibCheck" }, "type": "module", "dependencies": { @@ -30,6 +31,7 @@ "drizzle-zod": "^0.5.0", "http-status": "^1.6.2", "nodemailer": "6.9.4", + "object-hash": "^3.0.0", "postgres": "^3.3.5", "socket.io": "^4.7.2", "socket.io-client": "^4.7.2", @@ -44,6 +46,7 @@ "@total-typescript/ts-reset": "^0.4.2", "@types/node": "^20.5.0", "@types/nodemailer": "^6.4.9", + "@types/object-hash": "^3.0.3", "@types/web-bluetooth": "^0.0.17", "@typescript-eslint/eslint-plugin": "^6.4.0", "autoprefixer": "^10.4.15", diff --git a/leaky-ships/pnpm-lock.yaml b/leaky-ships/pnpm-lock.yaml index e09250d..8097912 100644 --- a/leaky-ships/pnpm-lock.yaml +++ b/leaky-ships/pnpm-lock.yaml @@ -65,6 +65,9 @@ dependencies: nodemailer: specifier: 6.9.4 version: 6.9.4 + object-hash: + specifier: ^3.0.0 + version: 3.0.0 postgres: specifier: ^3.3.5 version: 3.3.5 @@ -103,6 +106,9 @@ devDependencies: '@types/nodemailer': specifier: ^6.4.9 version: 6.4.9 + '@types/object-hash': + specifier: ^3.0.3 + version: 3.0.3 '@types/web-bluetooth': specifier: ^0.0.17 version: 0.0.17 @@ -2075,6 +2081,10 @@ packages: '@types/node': 18.17.5 dev: true + /@types/object-hash@3.0.3: + resolution: {integrity: sha512-Mb0SDIhjhBAz4/rDNU0cYcQR4lSJIwy+kFlm0whXLkx+o0pXwEszwyrWD6gXWumxVbAS6XZ9gXK82LR+Uk+cKQ==} + dev: true + /@types/resolve@1.20.2: resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} @@ -4257,7 +4267,6 @@ packages: /object-hash@3.0.0: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} - dev: true /object-inspect@1.12.3: resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} diff --git a/leaky-ships/src/components/Gamefield/BorderTiles.tsx b/leaky-ships/src/components/Gamefield/BorderTiles.tsx index e02348f..5a29274 100644 --- a/leaky-ships/src/components/Gamefield/BorderTiles.tsx +++ b/leaky-ships/src/components/Gamefield/BorderTiles.tsx @@ -1,7 +1,16 @@ import { For } from "solid-js" -import { useGameProps } from "~/hooks/useGameProps" -import useIndex from "~/hooks/useIndex" -import useShips from "~/hooks/useShips" +import { + gameState, + mode, + mouseCursor, + removeShip, + setMode, + setMouseCursor, + setShips, + setTarget, + targetPreview, +} from "~/hooks/useGameProps" +import { useSession } from "~/hooks/useSession" import { borderCN, cornerCN, @@ -23,40 +32,36 @@ type TilesType = { } function BorderTiles() { - const { activeUser } = useIndex() - const { - payload, - mode, - targetPreview, - mouseCursor, - setTarget, - setMouseCursor, - } = useGameProps() - const { ships, setShips, removeShip } = useShips() + const { selfIndex, activeUser, ships } = useSession() const settingTarget = (isGameTile: boolean, x: number, y: number) => { - if (payload?.game?.state === "running") { - const list = targetList(targetPreview, mode) + if (gameState() === "running") { + const list = targetList(targetPreview(), mode()) if ( !isGameTile || - !list.filter(({ x, y }) => !isAlreadyHit(x, y, activeUser?.hits ?? [])) - .length + !list.filter( + ({ x, y }) => !isAlreadyHit(x, y, activeUser()?.hits() ?? []), + ).length ) return - if (!overlapsWithAnyBorder(targetPreview, mode)) + if (!overlapsWithAnyBorder(targetPreview(), mode())) setTarget({ show: true, x, y, - orientation: targetPreview.orientation, + orientation: targetPreview().orientation, }) } else if ( - payload?.game?.state === "starting" && - targetPreview.show && - !intersectingShip(ships(), shipProps(ships(), mode, targetPreview)).score + gameState() === "starting" && + targetPreview().show && + !intersectingShip(ships(), shipProps(ships(), mode(), targetPreview())) + .score ) { 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} style={{ "--x": props.x, "--y": props.y }} onClick={() => { - if (payload?.game?.state === "running") { + if (gameState() === "running") { settingTarget(props.isGameTile, props.x, props.y) - } else if (payload?.game?.state === "starting") { + } else if (gameState() === "starting") { const { index } = intersectingShip(ships(), { - ...mouseCursor, + ...mouseCursor(), size: 1, variant: 0, orientation: "h", @@ -102,8 +107,8 @@ function BorderTiles() { settingTarget(props.isGameTile, props.x, props.y) else { const ship = ships()[index] - useGameProps.setState({ mode: ship.size - 2 }) - removeShip(ship) + setMode(ship.size - 2) + removeShip(ship, selfIndex()) setMouseCursor((e) => ({ ...e, shouldShow: true })) } } @@ -114,13 +119,13 @@ function BorderTiles() { y: props.y, shouldShow: props.isGameTile && - (payload?.game?.state === "starting" + (gameState() === "starting" ? intersectingShip( ships(), - shipProps(ships(), mode, { + shipProps(ships(), mode(), { x: props.x, y: props.y, - orientation: targetPreview.orientation, + orientation: targetPreview().orientation, }), true, ).score < 2 diff --git a/leaky-ships/src/components/Gamefield/EventBar.tsx b/leaky-ships/src/components/Gamefield/EventBar.tsx index e7faf0c..5717944 100644 --- a/leaky-ships/src/components/Gamefield/EventBar.tsx +++ b/leaky-ships/src/components/Gamefield/EventBar.tsx @@ -23,52 +23,36 @@ import { } from "@fortawesome/pro-solid-svg-icons" import { socket } from "~/lib/socket" import { modes } from "~/lib/utils/helpers" -import { GamePropsSchema } from "~/lib/zodSchemas" // import { Icons, toast } from "react-toastify" import { For, Show, createEffect } from "solid-js" import { useNavigate } from "solid-start" import { useDrawProps } from "~/hooks/useDrawProps" -import { useGameProps } from "~/hooks/useGameProps" -import useIndex from "~/hooks/useIndex" -import useShips from "~/hooks/useShips" -import { EventBarModes, GameSettings } from "../../interfaces/frontend" +import { + allowChat, + allowMarkDraw, + 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" -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 }) { const { shouldHide, color } = useDrawProps() - const { selfIndex, isActiveIndex, selfUser } = useIndex() - const { ships } = useShips() - 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 { selfIndex, isActiveIndex, selfUser, ships } = useSession() + const navigator = useNavigate() const items = (): EventBarModes => ({ main: [ @@ -76,36 +60,36 @@ function EventBar(props: { clear: () => void }) { icon: "burger-menu", text: "Menu", callback: () => { - useGameProps.setState({ menu: "menu" }) + setMenu("menu") }, }, - payload?.game?.state === "running" + gameState() === "running" ? { icon: faSwords, text: "Attack", callback: () => { - useGameProps.setState({ menu: "moves" }) + setMenu("moves") }, } : { icon: faShip, text: "Ships", callback: () => { - useGameProps.setState({ menu: "moves" }) + setMenu("moves") }, }, { icon: "pen", text: "Draw", callback: () => { - useGameProps.setState({ menu: "draw" }) + setMenu("draw") }, }, { icon: "gear", text: "Settings", callback: () => { - useGameProps.setState({ menu: "settings" }) + setMenu("settings") }, }, ], @@ -115,47 +99,49 @@ function EventBar(props: { clear: () => void }) { text: "Surrender", iconColor: "darkred", callback: () => { - useGameProps.setState({ menu: "surrender" }) + setMenu("surrender") }, }, ], moves: - payload?.game?.state === "running" + gameState() === "running" ? [ { icon: "scope", text: "Fire missile", - enabled: mode === 0, + enabled: mode() === 0, callback: () => { - useGameProps.setState({ mode: 0 }) - setTarget((e) => ({ ...e, show: false })) + setMode(0) + setTarget((t) => ({ ...t, show: false })) }, }, { icon: "torpedo", text: "Fire torpedo", - enabled: mode === 1 || mode === 2, + enabled: mode() === 1 || mode() === 2, amount: 2 - - ((selfUser?.moves ?? []).filter( - (e) => e.type === "htorpedo" || e.type === "vtorpedo", - ).length ?? 0), + (selfUser() + ?.moves() + .filter((e) => e.type === "htorpedo" || e.type === "vtorpedo") + .length ?? 0), callback: () => { - useGameProps.setState({ mode: 1 }) - setTarget((e) => ({ ...e, show: false })) + setMode(1) + setTarget((t) => ({ ...t, show: false })) }, }, { icon: "radar", text: "Radar scan", - enabled: mode === 3, + enabled: mode() === 3, amount: 1 - - ((selfUser?.moves ?? []).filter((e) => e.type === "radar") - .length ?? 0), + (selfUser() + ?.moves() + .filter((e) => e.type === "radar").length ?? 0), callback: () => { - useGameProps.setState({ mode: 3 }) - setTarget((e) => ({ ...e, show: false })) + setMode(3) + setTarget((t) => ({ ...t, show: false })) }, }, ] @@ -166,7 +152,7 @@ function EventBar(props: { clear: () => void }) { amount: 1 - ships().filter((e) => e.size === 2).length, callback: () => { 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, callback: () => { 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, callback: () => { 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, text: "Spectators", - disabled: !payload?.game?.allowSpectators, - callback: gameSetting({ - allowSpectators: !payload?.game?.allowSpectators, + disabled: !allowSpectators(), + callback: setGameSetting({ + allowSpectators: !allowSpectators(), }), }, { icon: faSparkles, text: "Specials", - disabled: !payload?.game?.allowSpecials, - callback: gameSetting({ - allowSpecials: !payload?.game?.allowSpecials, + disabled: !allowSpecials(), + callback: setGameSetting({ + allowSpecials: !allowSpecials(), }), }, { icon: faComments, text: "Chat", - disabled: !payload?.game?.allowChat, - callback: gameSetting({ allowChat: !payload?.game?.allowChat }), + disabled: !allowChat(), + callback: setGameSetting({ allowChat: !allowChat() }), }, { icon: faScribble, text: "Mark/Draw", - disabled: !payload?.game?.allowMarkDraw, - callback: gameSetting({ - allowMarkDraw: !payload?.game?.allowMarkDraw, + disabled: !allowMarkDraw(), + callback: setGameSetting({ + allowMarkDraw: !allowMarkDraw(), }), }, ], @@ -248,7 +234,7 @@ function EventBar(props: { clear: () => void }) { iconColor: "green", callback: async () => { socket.emit("gameState", "aborted") - await navigate("/") + await navigator("/") reset() }, }, @@ -257,7 +243,7 @@ function EventBar(props: { clear: () => void }) { text: "No", iconColor: "red", callback: () => { - useGameProps.setState({ menu: "main" }) + setMenu("main") }, }, ], @@ -265,22 +251,22 @@ function EventBar(props: { clear: () => void }) { createEffect(() => { if ( - menu !== "moves" || - payload?.game?.state !== "starting" || - mode < 0 || - items().moves[mode].amount + menu() !== "moves" || + gameState() !== "starting" || + mode() < 0 || + items().moves[mode()].amount ) return const index = items().moves.findIndex((e) => e.amount) - useGameProps.setState({ mode: index }) + setMode(index) }) createEffect(() => { - useDrawProps.setState({ enable: menu === "draw" }) + useDrawProps.setState({ enable: menu() === "draw" }) }) // createEffect(() => { - // if (payload?.game?.state !== "running") return + // if (gameState() !== "running") return // const toastId = "otherPlayer" // if (isActiveIndex) toast.dismiss(toastId) @@ -307,59 +293,51 @@ function EventBar(props: { clear: () => void }) { return (
- + { - useGameProps.setState({ menu: "main" }) + setMenu("main") }, }} /> - + {(e, i) => ( - + )} - + = 0 && userStates[selfIndex].isReady() - ? faLock - : faCheck, - text: - selfIndex >= 0 && userStates[selfIndex].isReady() - ? "unready" - : "Done", - disabled: - payload?.game?.state === "starting" ? mode >= 0 : undefined, - enabled: - payload?.game?.state === "running" && mode >= 0 && target.show, + icon: selfUser()?.isReady() ? faLock : faCheck, + text: selfUser()?.isReady() ? "unready" : "Done", + disabled: gameState() === "starting" ? mode() >= 0 : undefined, + enabled: gameState() === "running" && mode() >= 0 && target().show, callback: () => { - if (selfIndex < 0) return - switch (payload?.game?.state) { + const i = selfIndex() + if (i === -1) return + switch (gameState()) { case "starting": - const isReady = !userStates[selfIndex].isReady - setIsReady({ isReady, i: selfIndex }) + const isReady = !users[i].isReady() + setIsReadyFor({ isReady, i }) socket.emit("isReady", isReady) break case "running": - const i = (selfUser?.moves ?? []) - .map((e) => e.index) - .reduce((prev, curr) => (curr > prev ? curr : prev), 0) + const moves = selfUser()?.moves() + const length = moves?.length const props = { - type: modes[mode].type, - x: target.x, - y: target.y, - orientation: target.orientation, - index: (selfUser?.moves ?? []).length ? i + 1 : 0, + type: modes[mode()].type, + x: target().x, + y: target().y, + orientation: target().orientation, + index: length ?? 0, } socket.emit("dispatchMove", props) setTarget((t) => ({ ...t, show: false })) diff --git a/leaky-ships/src/components/Gamefield/Gamefield.tsx b/leaky-ships/src/components/Gamefield/Gamefield.tsx index a448886..ab29c0f 100644 --- a/leaky-ships/src/components/Gamefield/Gamefield.tsx +++ b/leaky-ships/src/components/Gamefield/Gamefield.tsx @@ -9,8 +9,20 @@ import HitElems from "~/components/Gamefield/HitElems" import Targets from "~/components/Gamefield/Targets" import { useDraw } from "~/hooks/useDraw" import { useDrawProps } from "~/hooks/useDrawProps" -import { useGameProps } from "~/hooks/useGameProps" -import useIndex from "~/hooks/useIndex" +import { + 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 { socket } from "~/lib/socket" import { overlapsWithAnyBorder } from "~/lib/utils/helpers" @@ -20,47 +32,35 @@ import Ships from "./Ships" export const count = 12 function Gamefield() { - const { selfUser } = useIndex() - const navigate = useNavigate() - const { - userStates, - mode, - target, - mouseCursor, - setMouseCursor, - payload, - setTargetPreview, - full, - reset, - } = useGameProps() + const { ships } = useSession() + const navigator = useNavigate() const { isConnected } = useSocket() - const usingDraw = useDraw() const { enable, color, shouldHide } = useDrawProps() createEffect(() => { if ( - payload?.game?.state !== "starting" || - userStates.reduce((prev, curr) => prev || !curr.isReady, false) + gameState() !== "starting" || + !users[0].isReady() || + !users[1].isReady() ) return - socket.emit("ships", selfUser?.ships ?? []) + socket.emit("ships", ships() ?? []) socket.emit("gameState", "running") }) createEffect(() => { - if (payload?.game?.id || !isConnected) return + if (gameId() || !isConnected()) return socket.emit("update", full) }) createEffect(() => { - if (mode < 0) return - const { x, y, show } = target - const { shouldShow, ...position } = mouseCursor + if (mode() < 0) return + const { x, y, show } = target() + const { shouldShow, ...position } = mouseCursor() if ( !shouldShow || - (payload?.game?.state === "running" && - overlapsWithAnyBorder(position, mode)) + (gameState() === "running" && overlapsWithAnyBorder(position, mode())) ) setTargetPreview((t) => ({ ...t, show: false })) else { @@ -71,14 +71,14 @@ function Gamefield() { })) const handleKeyPress = (event: KeyboardEvent) => { if (event.key !== "r") return - if (payload?.game?.state === "starting") { + if (gameState() === "starting") { setTargetPreview((t) => ({ ...t, orientation: t.orientation === "h" ? "v" : "h", })) } - if (payload?.game?.state === "running" && (mode === 1 || mode === 2)) - useGameProps.setState({ mode: mode === 1 ? 2 : 1 }) + if (gameState() === "running" && (mode() === 1 || mode() === 2)) + setMode(mode() === 1 ? 2 : 1) } document.addEventListener("keydown", handleKeyPress) return () => { @@ -88,15 +88,15 @@ function Gamefield() { }) createEffect(() => { - if (payload?.game?.state !== "aborted") return + if (gameState() !== "aborted") return // toast.info("Enemy gave up!") - navigate("/") + navigator("/") reset() }) createEffect(() => { - if (payload?.game?.id) return - const timeout = setTimeout(() => navigate("/"), 5000) + if (gameId()) return + const timeout = setTimeout(() => navigator("/"), 5000) return () => clearTimeout(timeout) }) diff --git a/leaky-ships/src/components/Gamefield/HitElems.tsx b/leaky-ships/src/components/Gamefield/HitElems.tsx index e7f5a30..eca91ce 100644 --- a/leaky-ships/src/components/Gamefield/HitElems.tsx +++ b/leaky-ships/src/components/Gamefield/HitElems.tsx @@ -1,16 +1,16 @@ import { faBurst, faXmark } from "@fortawesome/pro-solid-svg-icons" import { For } from "solid-js" import { FontAwesomeIcon } from "~/components/FontAwesomeIcon" -import useIndex from "~/hooks/useIndex" +import { useSession } from "~/hooks/useSession" import { Hit } from "../../interfaces/frontend" function HitElems(props: { hits?: Hit[]; colorOverride?: string }) { - const { activeUser } = useIndex() + const { activeUser } = useSession() const hits = () => props?.hits const colorOverride = () => props?.colorOverride return ( - + {(props) => (
- {isColor() ? ( +
{/* */}
- ) : null} +
- {(props) => } + + {(props) => } ) } diff --git a/leaky-ships/src/components/Gamefield/Targets.tsx b/leaky-ships/src/components/Gamefield/Targets.tsx index 4c3550c..3dcf977 100644 --- a/leaky-ships/src/components/Gamefield/Targets.tsx +++ b/leaky-ships/src/components/Gamefield/Targets.tsx @@ -1,7 +1,6 @@ import { For, Match, Switch } from "solid-js" -import { useGameProps } from "~/hooks/useGameProps" -import useIndex from "~/hooks/useIndex" -import useShips from "~/hooks/useShips" +import { gameState, mode, target, targetPreview } from "~/hooks/useGameProps" +import { useSession } from "~/hooks/useSession" import { composeTargetTiles, intersectingShip, @@ -12,29 +11,29 @@ import HitElems from "./HitElems" import Ship from "./Ship" function Targets() { - const { activeUser } = useIndex() - const { payload, target, targetPreview, mode } = useGameProps() - const { ships } = useShips() + const { activeUser, ships } = useSession() - const ship = shipProps(ships(), mode, targetPreview) + const ship = shipProps(ships(), mode(), targetPreview()) const { fields, borders, score } = intersectingShip(ships(), ship) return ( - - + + {(props) => } {(props) => } = 0 && targetPreview.show - } + when={gameState() === "starting" && mode() >= 0 && targetPreview().show} > void }) { - const { payload, userStates, full, leave, reset } = useGameProps() const { isConnected } = useSocket() - const navigate = useNavigate() - const session = useSession() + const navigator = useNavigate() + const { session } = useSession() const [launchTime, setLaunchTime] = createSignal(3) - - const launching = () => - payload?.users.length === 2 && - !userStates.filter((user) => !user.isReady).length + const launching = () => users[0].isReady() && users[1].isReady() createEffect(() => { if (!launching() || launchTime() > 0) return @@ -61,17 +64,13 @@ function LobbyFrame(props: { openSettings: () => void }) { }) createEffect(() => { - if (payload?.game?.id || !isConnected) return + if (gameId() || !isConnected()) return socket.emit("update", full) }) createEffect(() => { - if ( - typeof payload?.game?.state !== "string" || - payload?.game?.state === "lobby" - ) - return - navigate("/gamefield") + if (gameState() === "unknown" || gameState() === "lobby") return + navigator("/gamefield") }) return ( @@ -79,53 +78,50 @@ function LobbyFrame(props: { openSettings: () => void }) {
Chat

- {launching() ? ( - - {launchTime() < 0 - ? "Game starts" - : "Game is starting in " + launchTime()} - - ) : ( - <> - {"Game-PIN: "} - {isConnected() ? ( - {payload?.gamePin ?? "----"} - ) : ( - - )} - - )} + + {launchTime() < 0 + ? "Game starts" + : "Game is starting in " + launchTime()} + + } + > + {"Game-PIN: "} + } + > + {gamePin() ?? "----"} + +

Settings
- {isConnected() ? ( + + Warte auf Verbindung +

+ } + > <> - +

VS

- {payload?.users[1] ? ( - + {users[1].id() ? ( + ) : (

Warte auf Spieler 2

)} - ) : ( -

- Warte auf Verbindung -

- )} +
- ) : null} +
}> - - - + + + + + diff --git a/leaky-ships/src/routes/[...404].tsx b/leaky-ships/src/routes/[...404].tsx new file mode 100644 index 0000000..a375fa6 --- /dev/null +++ b/leaky-ships/src/routes/[...404].tsx @@ -0,0 +1,5 @@ +import { redirect } from "solid-start" + +export function GET() { + return redirect("/") +} diff --git a/leaky-ships/src/routes/api/game/create.ts b/leaky-ships/src/routes/api/game/create.ts index 024a8d7..7c46ef6 100644 --- a/leaky-ships/src/routes/api/game/create.ts +++ b/leaky-ships/src/routes/api/game/create.ts @@ -1,4 +1,5 @@ import { getSession } from "@auth/solid-start" +import { createId } from "@paralleldrive/cuid2" import { eq } from "drizzle-orm" import { APIEvent } from "solid-start" import db from "~/drizzle" @@ -30,17 +31,35 @@ export async function POST({ request }: APIEvent) { message: "Running game already exists.", }) } 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 = ( await db .insert(user_games) - .values({ gameId, userId: id, index: 0 }) + .values({ + id: createId(), + gameId, + userId: id, + index: 0, + }) .returning() )[0] - await db.insert(gamepins).values({ gameId, pin }) - await db - .insert(chats) - .values({ user_game_id: user_Game.id, event: "created" }) + await db.insert(gamepins).values({ + id: createId(), + gameId, + pin, + }) + await db.insert(chats).values({ + id: createId(), + user_game_id: user_Game.id, + event: "created", + }) game = await db.query.games.findFirst({ where: eq(games.id, gameId), ...gameSelects, diff --git a/leaky-ships/src/routes/api/game/join.ts b/leaky-ships/src/routes/api/game/join.ts index 31ee043..724a0f5 100644 --- a/leaky-ships/src/routes/api/game/join.ts +++ b/leaky-ships/src/routes/api/game/join.ts @@ -1,8 +1,9 @@ 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 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 getPinFromBody from "~/lib/backend/getPinFromBody" import logging from "~/lib/backend/logging" @@ -34,28 +35,15 @@ export async function POST({ request }: APIEvent) { }) } - let game = await db.query.games.findFirst({ - where: (game) => - and( - ne(game.state, "ended"), - exists( - db - .select() - .from(user_games) - .where( - inArray( - user_games.userId, - db - .select({ data: users.id }) - .from(users) - .where(eq(users.id, id)), - ), - ), - ), - ), - ...gameSelects, + const user_Game = await db.query.user_games.findFirst({ + where: and( + eq(user_games.userId, id), + eq(user_games.gameId, gamePin.gameId), + ), + with: { game: gameSelects }, }) - if (!game) { + + if (user_Game) { return sendResponse(request, { message: "Spieler ist bereits in Spiel!", redirectUrl: "/api/game/running", @@ -63,18 +51,17 @@ export async function POST({ request }: APIEvent) { }) } - const gameId = ( - await db - .insert(user_games) - .values({ - gameId: game.id, - userId: id, - index: 1, - }) - .returning() - )[0].gameId + await db + .insert(user_games) + .values({ + id: createId(), + gameId: gamePin.gameId, + userId: id, + index: 1, + }) + .returning() - game = await getGameById(gameId) + const game = await getGameById(gamePin.gameId) if (!game) return @@ -87,11 +74,8 @@ export async function POST({ request }: APIEvent) { }) // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { - await logging( - "HERE".red + err.code + err.meta + err.message, - ["error"], - request, - ) + await logging(err.code + err.meta + err.message, ["error"], request) + console.log(err) throw sendError(request, rejectionErrors.gameNotFound) } } diff --git a/leaky-ships/src/routes/api/game/running.ts b/leaky-ships/src/routes/api/game/running.ts index 1d73509..b083545 100644 --- a/leaky-ships/src/routes/api/game/running.ts +++ b/leaky-ships/src/routes/api/game/running.ts @@ -2,7 +2,7 @@ import { getSession } from "@auth/solid-start" import { and, eq, exists, ne } from "drizzle-orm" import { APIEvent } from "solid-start/api" 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 sendResponse from "~/lib/backend/sendResponse" import { getPayloadwithChecksum } from "~/lib/getPayloadwithChecksum" @@ -87,10 +87,7 @@ export const getRunningGameToUser = async (userId: string) => { and( ne(game.state, "ended"), exists( - db - .select() - .from(user_games) - .where(exists(db.select().from(users).where(eq(users.id, userId)))), + db.select().from(user_games).where(eq(user_games.userId, userId)), ), ), ...gameSelects, @@ -100,23 +97,41 @@ export const getRunningGameToUser = async (userId: string) => { export function composeBody( gameDB: NonNullable>>, ): GamePropsSchema { - const { gamePin, ...game } = gameDB - const users = gameDB.users - .map(({ user, ...props }) => ({ - ...props, - ...user, - })) - .sort((user1, user2) => user1.index - user2.index) - let activeIndex = undefined + const { gamePin, users, ...game } = gameDB + const mappedUsers = users.map(({ user, ...props }) => ({ + ...props, + ...user, + })) + const composedUsers = { + 0: mappedUsers.find((e) => e.index === 0) ?? { + 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") { - const l1 = game.users[0].moves.length - const l2 = game.users[1].moves.length + const l1 = users[0]?.moves.length ?? 0 + const l2 = users[1]?.moves.length ?? 0 activeIndex = l1 > l2 ? 1 : 0 } const payload = { game: game, gamePin: gamePin?.pin ?? null, - users, + users: composedUsers, activeIndex, } return getPayloadwithChecksum(payload) diff --git a/leaky-ships/src/routes/api/ws.ts b/leaky-ships/src/routes/api/ws.ts index 19a1b7c..108969a 100644 --- a/leaky-ships/src/routes/api/ws.ts +++ b/leaky-ships/src/routes/api/ws.ts @@ -1,4 +1,5 @@ import { getSession } from "@auth/solid-start" +import { createId } from "@paralleldrive/cuid2" import colors from "colors" import { and, eq } from "drizzle-orm" import status from "http-status" @@ -6,7 +7,7 @@ import { Server } from "socket.io" import { APIEvent } from "solid-start" import db from "~/drizzle" 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 { GamePropsSchema } from "~/lib/zodSchemas" import { authOptions } from "~/server/auth" @@ -19,27 +20,33 @@ import { colors.enable() -const res = new Response() as ResponseWithSocket - -export async function GET({ request }: APIEvent) { - if (res.socket.server.io) { +export async function GET({ + request, + httpServer, +}: APIEvent & { httpServer: SocketServer }) { + if (httpServer.io) { logging("Socket is already running " + request.url, ["infoCyan"], request) } else { 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", cors: { origin: "https://leaky-ships.mal-noh.de", }, }) - res.socket.server.io = io + httpServer.io = io // io.use(authenticate) io.use(async (socket, next) => { try { - // @ts-expect-error TODO add correct server - const session = await getSession(socket.request, authOptions) + const url = process.env.AUTH_URL! + socket.request.url + const session = await getSession( + new Request(url, { + headers: socket.request.headers as Record, + }), + authOptions, + ) if (!session) return next(new Error(status["401"])) socket.data.user = session.user @@ -55,18 +62,15 @@ export async function GET({ request }: APIEvent) { } const { payload, hash } = composeBody(game) - // let index: number | null = null - const index = payload.users.findIndex( - (user) => socket.data.user?.id === user?.id, - ) - if (index < 0) return next(new Error(status["401"])) + const index = payload.users[0]?.id === socket.data.user?.id ? 0 : 1 + if (index !== 0 && index !== 1) return next(new Error(status["401"])) socket.data.index = index socket.data.gameId = game.id socket.join(game.id) socket.to(game.id).emit("playerEvent", { type: "connect", i: socket.data.index, - payload: { users: payload.users }, + users: payload.users, hash, }) @@ -89,14 +93,16 @@ export async function GET({ request }: APIEvent) { socket.on("update", async (cb) => { const game = await getGameById(socket.data.gameId ?? "") if (!game) return + if (socket.data.index === 1 && game.users.length === 1) + socket.data.index = 0 const body = composeBody(game) cb(body) }) - socket.on("gameSetting", async (payload, cb) => { + socket.on("gameSetting", async (newSettings, cb) => { const game = await db .update(games) - .set(payload) + .set(newSettings) .where(eq(games.id, socket.data.gameId)) .returning() .then((updatedGame) => @@ -109,41 +115,41 @@ export async function GET({ request }: APIEvent) { const { hash } = composeBody(game) if (!hash) return 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("leave", async (cb) => { - if (!socket.data.gameId || !socket.data.user?.id) return cb(false) - const user_Game = await db - .delete(user_games) - .where( - and( - eq(user_games.gameId, socket.data.gameId), - eq(user_games.userId, socket.data.user?.id ?? ""), - ), - ) - .returning() + const user_Game = ( + await db + .delete(user_games) + .where( + and( + eq(user_games.gameId, socket.data.gameId), + eq(user_games.userId, socket.data.user?.id ?? ""), + ), + ) + .returning() + )[0] if (!user_Game) return const enemy = await db.query.user_games.findFirst({ where: eq(user_games.gameId, socket.data.gameId), }) let body: GamePropsSchema - if (user_Game[0].index === 1 && enemy) { + if (user_Game.index === 0 && enemy) { const game = await db .update(user_games) .set({ - index: 1, + index: 0, }) .where( and( eq(user_games.gameId, socket.data.gameId), - eq(user_games.index, 2), + eq(user_games.index, 1), ), ) .returning() - .then((user_Game) => db.query.games.findFirst({ where: eq(games.id, user_Game[0].gameId), @@ -161,17 +167,16 @@ export async function GET({ request }: APIEvent) { body = composeBody(game) } const { payload, hash } = body - if (!payload || !hash || socket.data.index === undefined) - return cb(false) socket.to(socket.data.gameId).emit("playerEvent", { type: "leave", i: socket.data.index, - payload: { users: payload.users }, + users: payload.users, hash, }) + socket.data.gameId = "" cb(true) - if (!payload.users.length) { + if (!payload.users[0] && !payload.users[1]) { 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 - await db - .insert(ships) - .values( - shipsData.map((ship) => ({ ...ship, user_game_id: user_Game.id })), - ) + await db.insert(ships).values( + shipsData.map((ship) => ({ + id: createId(), + user_game_id: user_Game.id, + ...ship, + })), + ) socket .to(socket.data.gameId) @@ -270,7 +277,7 @@ export async function GET({ request }: APIEvent) { if (!user_Game?.game) return await db .insert(moves) - .values({ ...props, user_game_id: user_Game.id }) + .values({ ...props, id: createId(), user_game_id: user_Game.id }) .returning() const game = user_Game.game @@ -286,7 +293,7 @@ export async function GET({ request }: APIEvent) { ["debug"], socket.request, ) - if (socket.data.index === undefined || !socket.data.gameId) return + if (!socket.data.gameId) return socket.to(socket.data.gameId).emit("playerEvent", { type: "disconnect", i: socket.data.index, @@ -298,5 +305,5 @@ export async function GET({ request }: APIEvent) { }) }) } - return res + return new Response() } diff --git a/leaky-ships/src/routes/game.tsx b/leaky-ships/src/routes/game.tsx deleted file mode 100644 index 7b22717..0000000 --- a/leaky-ships/src/routes/game.tsx +++ /dev/null @@ -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 ( -
-
-
- ) -} diff --git a/leaky-ships/src/routes/index.tsx b/leaky-ships/src/routes/index.tsx index f40e9dc..b2a6c2b 100644 --- a/leaky-ships/src/routes/index.tsx +++ b/leaky-ships/src/routes/index.tsx @@ -3,7 +3,7 @@ import BurgerMenu from "~/components/BurgerMenu" import Logo from "~/components/Logo" export default function Home() { - const navigate = useNavigate() + const navigator = useNavigate() return (
@@ -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" onClick={() => setTimeout(() => { - navigate("/start") + navigator("/start") }, 200) } > diff --git a/leaky-ships/src/routes/lobby.tsx b/leaky-ships/src/routes/lobby.tsx index 73ceed0..9f74295 100644 --- a/leaky-ships/src/routes/lobby.tsx +++ b/leaky-ships/src/routes/lobby.tsx @@ -1,6 +1,6 @@ // import Head from "next/head" import classNames from "classnames" -import { createSignal } from "solid-js" +import { Show, createSignal } from "solid-js" import BurgerMenu from "~/components/BurgerMenu" import LobbyFrame from "~/components/Lobby/LobbyFrame" import Settings from "~/components/Lobby/SettingsFrame/Settings" @@ -26,16 +26,16 @@ export default function Lobby() {
setSettings(true)} />
- {settings() ? ( + setSettings(false)} /> - ) : null} +
) } diff --git a/leaky-ships/src/routes/signin.tsx b/leaky-ships/src/routes/signin.tsx index a421293..9a07dea 100644 --- a/leaky-ships/src/routes/signin.tsx +++ b/leaky-ships/src/routes/signin.tsx @@ -1,7 +1,7 @@ import { signIn } from "@auth/solid-start/client" import { faLeftLong } from "@fortawesome/pro-solid-svg-icons" import classNames from "classnames" -import { createEffect, createSignal } from "solid-js" +import { Show, createEffect, createSignal } from "solid-js" import { useNavigate, useSearchParams } from "solid-start" import { FontAwesomeIcon } from "~/components/FontAwesomeIcon" import { useSession } from "~/hooks/useSession" @@ -39,19 +39,20 @@ const errors: Record = { function Login() { const [email, setEmail] = createSignal("") - const { latest } = useSession() - const navigate = useNavigate() + const { session } = useSession() + const navigator = useNavigate() const [searchParams] = useSearchParams() const errorType = searchParams["error"] as SignInErrorTypes createEffect(() => { if (!errorType) return + console.error(errors[errorType] ?? errors.default) // toast.error(errors[errorType] ?? errors.default, { theme: "colored" }) }) createEffect(() => { - if (latest?.user?.id) navigate("/") + if (session()) navigator("/signout") }) const login = (provider: "email" | "azure-ad") => @@ -79,7 +80,14 @@ function Login() {
{errorType &&
}
-
+
{ + e.preventDefault() + if (!email()) return + login("email") + }} + > @@ -95,12 +103,11 @@ function Login() { -
+

@@ -137,21 +144,19 @@ function Login() {
- {errorType ? ( - <> -
-
- -
- - ) : null} + +
+
+ +
+
) diff --git a/leaky-ships/src/routes/signout.tsx b/leaky-ships/src/routes/signout.tsx index a442cc2..157dbc0 100644 --- a/leaky-ships/src/routes/signout.tsx +++ b/leaky-ships/src/routes/signout.tsx @@ -1,15 +1,16 @@ import { signOut } from "@auth/solid-start/client" +import { faLeftLong } from "@fortawesome/pro-solid-svg-icons" import { createEffect } from "solid-js" import { useNavigate } from "solid-start" +import { FontAwesomeIcon } from "~/components/FontAwesomeIcon" import { useSession } from "~/hooks/useSession" function Logout() { - const { state } = useSession() - + const { session } = useSession() const navigator = useNavigate() createEffect(() => { - if (state === "ready") navigator("/signin") // TODO + if (!session()) navigator("/signin") }) return ( @@ -36,6 +37,17 @@ function Logout() { Sign out +
+
+ +
diff --git a/leaky-ships/src/routes/start.tsx b/leaky-ships/src/routes/start.tsx index 66a0eeb..a0a5745 100644 --- a/leaky-ships/src/routes/start.tsx +++ b/leaky-ships/src/routes/start.tsx @@ -10,7 +10,7 @@ import BurgerMenu from "~/components/BurgerMenu" import { FontAwesomeIcon } from "~/components/FontAwesomeIcon" import Logo from "~/components/Logo" import OptionButton from "~/components/OptionButton" -import { useGameProps } from "~/hooks/useGameProps" +import { full } from "~/hooks/useGameProps" import { useSession } from "~/hooks/useSession" export function isAuthenticated(res: Response) { @@ -44,11 +44,11 @@ export function isAuthenticated(res: Response) { // } export default function Start() { - const [otp] = createSignal("") - const gameProps = useGameProps() + const [otp, setOtp] = createSignal("") const location = useLocation() - const navigate = useNavigate() - const session = useSession() + const navigator = useNavigate() + const { session } = useSession() + const [searchParams] = useSearchParams() const query = () => { @@ -82,7 +82,8 @@ export default function Start() { // hideProgressBar: true, // closeButton: false, // }) - const res = await gameRequestPromise.catch(() => { + const res = await gameRequestPromise.catch((err) => { + console.log(err) // toast.update(toastId, { // render: "Es ist ein Fehler aufgetreten bei der Anfrage 🤯", // type: "error", @@ -94,13 +95,13 @@ export default function Start() { // }) }) if (!res) return - gameProps.full(res) + full(res) // toast.update(toastId, { // render: "Weiterleitung", // }) - navigate("/lobby") + navigator("/lobby") // .then(() => // toast.update(toastId, { // 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" onClick={() => setTimeout(() => { - navigate("/") + navigator("/") }, 200) } > - {!session.latest?.user?.id && ( + {!session()?.user?.id && (