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",
"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",

View file

@ -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==}

View file

@ -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

View file

@ -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 (
<div class="event-bar">
<Show when={menu !== "main"}>
<Show when={menu() !== "main"}>
<Item
{...{
icon: faReply,
text: "Return",
iconColor: "#555",
callback: () => {
useGameProps.setState({ menu: "main" })
setMenu("main")
},
}}
/>
</Show>
<For each={items()[menu]}>
<For each={items()[menu()]}>
{(e, i) => (
<Show when={isActiveIndex && menu !== "main" && i() !== 1}>
<Show when={isActiveIndex() && menu() !== "main" && i() !== 1}>
<Item {...e} />
</Show>
)}
</For>
<Show when={menu === "moves"}>
<Show when={menu() === "moves"}>
<Item
{...{
icon:
selfIndex >= 0 && userStates[selfIndex].isReady()
? faLock
: faCheck,
text:
selfIndex >= 0 && userStates[selfIndex].isReady()
? "unready"
: "Done",
disabled:
payload?.game?.state === "starting" ? mode >= 0 : undefined,
enabled:
payload?.game?.state === "running" && mode >= 0 && target.show,
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 }))

View file

@ -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)
})

View file

@ -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 (
<For each={hits() ?? activeUser?.hits ?? []}>
<For each={hits() ?? activeUser()?.hits()}>
{(props) => (
<div class="hit-svg" style={{ "--x": props.x, "--y": props.y }}>
<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 { useDrawProps } from "~/hooks/useDrawProps"
// import { HexColorPicker } from "react-colorful"
@ -33,14 +33,14 @@ function Item(props: ItemProps) {
isColor() ? setActive(true) : props.callback && props.callback()
}
>
{isColor() ? (
<Show when={isColor()}>
<div
ref={cpRef!}
class={classNames("react-colorful-wrapper", { active: active })}
>
{/* <HexColorPicker color={color} onChange={setColor} /> */}
</div>
) : null}
</Show>
<div
class={classNames("container", {
amount: typeof props.amount !== "undefined",

View file

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

View file

@ -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 (
<Switch>
<Match when={payload?.game?.state === "running"}>
<For each={composeTargetTiles(target, mode, activeUser?.hits ?? [])}>
<Match when={gameState() === "running"}>
<For each={composeTargetTiles(target(), mode(), activeUser().hits())}>
{(props) => <GamefieldPointer {...props} />}
</For>
<For
each={composeTargetTiles(targetPreview, mode, activeUser?.hits ?? [])}
each={composeTargetTiles(
targetPreview(),
mode(),
activeUser().hits(),
)}
>
{(props) => <GamefieldPointer {...props} preview />}
</For>
</Match>
<Match
when={
payload?.game?.state === "starting" && mode >= 0 && targetPreview.show
}
when={gameState() === "starting" && mode() >= 0 && targetPreview().show}
>
<Ship
{...ship}

View file

@ -2,14 +2,21 @@ import {
faRightFromBracket,
faSpinnerThird,
} 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 { 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 useSocket from "~/hooks/useSocket"
import { socket } from "~/lib/socket"
import Button from "./Button"
import Icon from "./Icon"
import Player from "./Player"
@ -34,15 +41,11 @@ function WithDots(props: { children: JSX.Element }) {
}
function LobbyFrame(props: { openSettings: () => 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 }) {
<div class="flex items-center justify-between border-b-2 border-slate-900">
<Icon src="speech_bubble.png">Chat</Icon>
<h1 class="font-farro text-5xl font-medium">
{launching() ? (
<Show
when={!launching()}
fallback={
<WithDots>
{launchTime() < 0
? "Game starts"
: "Game is starting in " + launchTime()}
</WithDots>
) : (
<>
}
>
{"Game-PIN: "}
{isConnected() ? (
<span class="underline">{payload?.gamePin ?? "----"}</span>
) : (
<FontAwesomeIcon icon={faSpinnerThird} spin />
)}
</>
)}
<Show
when={isConnected()}
fallback={<FontAwesomeIcon icon={faSpinnerThird} spin />}
>
<span class="underline">{gamePin() ?? "----"}</span>
</Show>
</Show>
</h1>
<Icon src="gear.png" onClick={props.openSettings}>
Settings
</Icon>
</div>
<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
src="player_blue.png"
i={0}
userId={session.latest?.user?.id}
/>
<Player src="player_blue.png" i={0} userId={session()?.user?.id} />
<p class="font-farro m-4 text-6xl font-semibold">VS</p>
{payload?.users[1] ? (
<Player
src="player_red.png"
i={1}
userId={session.latest?.user?.id}
/>
{users[1].id() ? (
<Player src="player_red.png" i={1} userId={session()?.user?.id} />
) : (
<p class="font-farro w-96 text-center text-4xl font-medium">
<WithDots>Warte auf Spieler 2</WithDots>
</p>
)}
</>
) : (
<p class="font-farro m-48 text-center text-6xl font-medium">
Warte auf Verbindung
</p>
)}
</Show>
</div>
<div class="flex items-center justify-around border-t-2 border-slate-900 p-4">
<Button
@ -134,7 +130,7 @@ function LobbyFrame(props: { openSettings: () => void }) {
onClick={() => {
leave(async () => {
reset()
await navigate("/")
navigator("/")
})
}}
>

View file

@ -8,9 +8,9 @@ import {
} from "@fortawesome/pro-solid-svg-icons"
import { faCaretDown } from "@fortawesome/sharp-solid-svg-icons"
import classNames from "classnames"
import { createEffect, createSignal } from "solid-js"
import { Show, createEffect, createSignal } from "solid-js"
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon"
import { useGameProps } from "~/hooks/useGameProps"
import { setIsReadyFor, users } from "~/hooks/useGameProps"
import { socket } from "~/lib/socket"
import Button from "./Button"
@ -42,13 +42,11 @@ function HourGlass() {
)
}
function Player(props: { src: string; i: number; userId?: string }) {
const { payload, userStates, setIsReady } = useGameProps()
const player = () => payload?.users[props.i]
const isReady = () => userStates[props.i].isReady
const isConnected = () => userStates[props.i].isConnected
const primary = () =>
props.userId && props.userId === payload?.users[props.i]?.id
function Player(props: { src: string; i: 0 | 1; userId?: string }) {
const player = () => users[props.i]
const isReady = () => users[props.i].isReady()
const isConnected = () => users[props.i].isConnected()
const primary = () => props.userId && props.userId === users[props.i].id()
return (
<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",
)}
>
{player?.name ?? "Spieler " + (player()?.index === 2 ? "2" : "1")}
{player().name() ?? "Spieler " + (props.i === 1 ? "2" : "1")}
</p>
<div class="relative">
<img
@ -66,14 +64,14 @@ function Player(props: { src: string; i: number; userId?: string }) {
src={"/assets/" + 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">
<FontAwesomeIcon
class="h-full w-full text-warn"
icon={faCaretDown}
/>
</button>
) : null}
</Show>
</div>
<Button
type={isConnected() ? (isReady() ? "green" : "orange") : "gray"}
@ -81,11 +79,11 @@ function Player(props: { src: string; i: number; userId?: string }) {
isLatched={!!isReady()}
onClick={() => {
if (!player()) return
setIsReady({
socket.emit("isReady", !isReady())
setIsReadyFor({
i: props.i,
isReady: !isReady(),
})
socket.emit("isReady", !isReady())
}}
disabled={!primary()}
>

View file

@ -5,13 +5,25 @@ import {
import classNames from "classnames"
import { JSX } from "solid-js"
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon"
import { setGameSetting } from "~/components/Gamefield/EventBar"
import { useGameProps } from "~/hooks/useGameProps"
import {
allowChat,
allowMarkDraw,
allowSpecials,
allowSpectators,
setGameSetting,
} from "~/hooks/useGameProps"
import { GameSettingKeys } from "../../../interfaces/frontend"
function Setting(props: { children: JSX.Element; key: GameSettingKeys }) {
const { payload, setSetting, full } = useGameProps()
const state = () => payload?.game?.[props.key]
const state = () => {
const gameProps = {
allowChat,
allowMarkDraw,
allowSpecials,
allowSpectators,
}
return gameProps[props.key]()
}
return (
<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",
state() ? "text-blue-500" : "text-gray-800",
{
"bg-gray-300 ": state,
"bg-gray-300 ": state(),
},
)}
size="3x"
@ -35,13 +47,9 @@ function Setting(props: { children: JSX.Element; key: GameSettingKeys }) {
type="checkbox"
id={props.key}
onChange={() =>
setGameSetting(
{
[props.key]: !state,
},
setSetting,
full,
)
setGameSetting({
[props.key]: !state(),
})
}
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 {} from "solid-js"
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon"
import { useGameProps } from "~/hooks/useGameProps"
import { full, setSetting } from "~/hooks/useGameProps"
import { socket } from "~/lib/socket"
import { GameSettings } from "../../../interfaces/frontend"
import Setting from "./Setting"
function Settings(props: { closeSettings: () => void }) {
const { setSetting, full } = useGameProps()
const gameSetting = (payload: GameSettings) => {
const hash = setSetting(payload)
socket.emit("gameSetting", payload, (newHash) => {
const gameSetting = (newSettings: GameSettings) => {
const hash = setSetting(newSettings)
socket.emit("gameSetting", newSettings, (newHash) => {
if (newHash === hash) return
console.log("hash", hash, newHash)
socket.emit("update", full)

View file

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

View file

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

View file

@ -1,110 +1,82 @@
import { getPayloadwithChecksum } from "~/lib/getPayloadwithChecksum"
/* eslint-disable solid/reactivity */
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 {
initialUser,
initlialMouseCursor,
initlialTarget,
initlialTargetPreview,
intersectingShip,
targetList,
} 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 {
EventBarModes,
GameSettings,
MouseCursor,
MoveDispatchProps,
NewUsers,
ShipProps,
Target,
TargetPreview,
} from "../interfaces/frontend"
const initialState: optionalGamePropsSchema & {
userStates: {
isReady: Accessor<boolean>
setIsReady: Setter<boolean>
isConnected: Accessor<boolean>
setIsConnected: Setter<boolean>
}[]
menu: keyof EventBarModes
mode: number
target: Target
targetPreview: TargetPreview
mouseCursor: MouseCursor
} = {
menu: "moves",
mode: 0,
payload: null,
hash: null,
target: initlialTarget,
targetPreview: initlialTargetPreview,
mouseCursor: initlialMouseCursor,
userStates: Array.from(Array(2), () => {
const [isReady, setIsReady] = createSignal(false)
const [isConnected, setIsConnected] = createSignal(false)
return { isReady, setIsReady, isConnected, setIsConnected }
}),
export const [hash, setHash] = createSignal<string | null>(null)
export const [activeIndex, setActiveIndex] = createSignal<0 | 1>(0)
export const [gamePin, setGamePin] = createSignal<string | null>(null)
export const [gameId, setGameId] = createSignal<string>("")
export const [gameState, setGameState] = createSignal<GameState>("unknown")
export const [allowChat, setAllowChat] = createSignal(false)
export const [allowMarkDraw, setAllowMarkDraw] = createSignal(false)
export const [allowSpecials, setAllowSpecials] = createSignal(false)
export const [allowSpectators, setallowSpectators] = createSignal(false)
export const [menu, setMenu] = createSignal<keyof EventBarModes>("moves")
export const [mode, setMode] = createSignal(0)
export const [target, setTarget] = createSignal<Target>(initlialTarget)
export const [targetPreview, setTargetPreview] = createSignal<TargetPreview>(
initlialTargetPreview,
)
export const [mouseCursor, setMouseCursor] =
createSignal<MouseCursor>(initlialMouseCursor)
export const users = {
0: initialUser(),
1: initialUser(),
forEach(cb: (user: ReturnType<typeof initialUser>, i: 0 | 1) => void) {
cb(this[0], 0)
cb(this[1], 1)
},
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 type Action = {
DispatchMove: (props: MoveDispatchProps, i: number) => void
setTarget: (target: Target | ((i: Target) => Target)) => void
setTargetPreview: (
targetPreview: TargetPreview | ((i: TargetPreview) => TargetPreview),
) => void
setMouseCursor: (
mouseCursor: MouseCursor | ((i: MouseCursor) => MouseCursor),
) => void
setPlayer: (payload: { users: PlayerSchema[] }) => string | null
setSetting: (settings: GameSettings) => string | null
full: (newProps: GamePropsSchema) => void
leave: (cb: () => void) => void
setIsReady: (payload: { i: number; isReady: boolean }) => void
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 const useGameProps = create<State & Action>()((set) => ({
...initialState,
setActiveIndex: (i, selfIndex) =>
set((state: State) => {
if (!state.payload) return state
state.payload.activeIndex = i
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
// 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)
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(
users.forEach((user, i) => {
if (!user) return
if (index === i) {
user.setMoves((e) => [...e, move])
} else {
if (move.type === MoveType.Enum.radar) return
user.setHits((e) => [
...e,
...list.map(({ x, y }) => ({
hit: !!intersectingShip(e.ships, {
hit: !!intersectingShip(user.ships(), {
...move,
size: 1,
variant: 0,
@ -112,150 +84,179 @@ export const useGameProps = create<State & Action>()((set) => ({
x,
y,
})),
)
return e
])
}
})
return state
}),
setTarget: (dispatch) =>
set((state: State) => {
if (typeof dispatch === "function") state.target = dispatch(state.target)
else state.target = dispatch
return state
}),
setTargetPreview: (dispatch) =>
set((state: State) => {
if (typeof dispatch === "function")
state.targetPreview = dispatch(state.targetPreview)
else state.targetPreview = dispatch
return state
}),
setMouseCursor: (dispatch) =>
set((state: State) => {
if (typeof dispatch === "function")
state.mouseCursor = dispatch(state.mouseCursor)
else state.mouseCursor = dispatch
return state
}),
setShips: (ships, index) =>
set((state: State) => {
if (!state.payload) return state
state.payload.users = state.payload.users.map((e) => {
if (!e || e.index !== index) return e
e.ships = ships
return e
}
export function setShips(ships: ShipProps[], index: number) {
users.forEach(({ setShips }, i) => {
if (index !== i) return
setShips(ships)
})
return state
}),
removeShip: ({ size, variant, x, y }, index) =>
set((state: State) => {
state.payload?.users.map((e) => {
if (!e || e.index !== index) return
const indexToRemove = e.ships.findIndex(
}
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,
)
e.ships.splice(indexToRemove, 1)
return e
user.setShips((ships) => ships.filter((_, i) => i !== indexToRemove))
})
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
}
hash = body.hash
state.hash = hash
return state
})
return hash
},
setSetting: (settings) => {
export function setPlayer(newUsers: NewUsers): string | null {
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)
console.log(newUsers)
users.forEach((user, i) => {
const newUser = newUsers[i]
if (!newUser) return defaultUser(user)
user.setId(newUser.id)
user.setName(newUser.name)
user.setChats(newUser.chats)
user.setMoves(newUser.moves)
user.setShips(newUser.ships)
user.setHits(newUser.hits)
})
const body = getPayloadwithChecksum(getPayloadFromProps())
if (!body.hash) {
console.log("Something is wrong... ")
// toast.warn("Something is wrong... ", {
// toastId: "st_wrong",
// theme: "colored",
// })
return state
return null
}
hash = body.hash
state.hash = hash
return state
})
setHash(hash)
return hash
},
full: (newGameProps) =>
// eslint-disable-next-line solid/reactivity
set((state) => {
if (state.hash === newGameProps.hash) {
}
export function setSetting(newSettings: GameSettings): string | null {
let hash: string | null = null
setAllowChat((e) => newSettings.allowChat ?? e)
setAllowMarkDraw((e) => newSettings.allowMarkDraw ?? e)
setAllowSpecials((e) => newSettings.allowSpecials ?? e)
setallowSpectators((e) => newSettings.allowSpectators ?? e)
const body = getPayloadwithChecksum(getPayloadFromProps())
if (!body.hash) {
console.log("Something is wrong... ")
// toast.warn("Something is wrong... ", {
// toastId: "st_wrong",
// theme: "colored",
// })
return null
}
hash = body.hash
setHash(hash)
return hash
}
export function setGameSetting(newSettings: GameSettings) {
return () => {
const hash = setSetting(newSettings)
socket.emit("gameSetting", newSettings, (newHash) => {
if (newHash === hash) return
console.log("hash", hash, newHash)
socket.emit("update", full)
})
}
}
export function full(newProps: GamePropsSchema) {
if (hash() === newProps.hash) {
console.log("Everything up to date.")
} else {
console.log("Update was needed.", state.hash, newGameProps.hash)
console.log("Update was needed.", hash(), newProps.hash)
if (
state.payload?.game?.id &&
state.payload?.game?.id !== newGameProps.payload?.game?.id
) {
if (gameId() !== newProps.payload?.game?.id)
console.warn(
"Different gameId detected on update: ",
state.payload?.game?.id,
newGameProps.payload?.game?.id,
gameId(),
newProps.payload?.game?.id,
)
}
return newGameProps
setHash(newProps.hash)
setActiveIndex(newProps.payload.activeIndex)
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)
})
}
return state
}),
leave: (cb) => {
}
export function leave(cb: () => void) {
socket.emit("leave", (ack) => {
if (!ack) {
console.log("Something is wrong... ")
// toast.error("Something is wrong...")
}
cb()
})
},
setIsReady: ({ i, isReady }) =>
set((state: State) => {
state.userStates[i].setIsReady(isReady)
state.userStates[i].setIsConnected(true)
return state
}),
gameState: (newState: GameState) =>
set((state: State) => {
if (!state.payload?.game) return state
state.payload.game.state = newState
state.userStates.forEach((e) => {
e.setIsReady(false)
})
return state
}),
setIsConnected: ({ i, isConnected }) =>
set((state: State) => {
state.userStates[i].setIsConnected(isConnected)
if (!isConnected) state.userStates[i].setIsReady(false)
return state
}),
reset: () => {
set(initialState)
},
}))
}
export function setIsReadyFor({ i, isReady }: { i: 0 | 1; isReady: boolean }) {
users[i].setIsReady(isReady)
users[i].setIsConnected(true)
}
export function newGameState(newState: GameState) {
setGameState(newState)
users.forEach((e) => e.setIsReady(false))
}
export function setIsConnectedFor({
i,
isConnected,
}: {
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 { isAuthenticated } from "~/routes/start"
import { GameSettings, PlayerEvent } from "../interfaces/frontend"
import { useGameProps } from "./useGameProps"
import useIndex from "./useIndex"
import {
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. */
function useSocket() {
const [isConnectedState, setIsConnectedState] = createSignal(false)
const { selfIndex } = useIndex()
const {
payload,
userStates,
setPlayer,
setSetting,
full,
setIsReady,
gameState,
setIsConnected,
setActiveIndex,
DispatchMove,
setShips,
} = useGameProps()
const { selfIndex } = useSession()
const navigate = useNavigate()
const isConnected = () =>
selfIndex >= 0 ? userStates[selfIndex].isConnected() : isConnectedState()
const isConnected = () => {
const i = selfIndex()
return i !== -1
? users[i].isConnected() && isConnectedState()
: isConnectedState()
}
createEffect(() => {
if (selfIndex < 0) return
setIsConnected({
i: selfIndex,
const i = selfIndex()
if (i === -1) return
setIsConnectedFor({
i,
isConnected: isConnectedState(),
})
})
@ -63,39 +69,51 @@ function useSocket() {
const playerEvent = (event: PlayerEvent) => {
const { type, i } = event
// let message: string
let message: string
console.log("playerEvent", type)
switch (type) {
case "disconnect":
setIsConnected({
setIsConnectedFor({
i,
isConnected: false,
})
// message = "Player is disconnected."
message = "Player is disconnected."
break
case "leave":
// message = "Player has left the lobby."
message = "Player has left the lobby."
break
case "connect":
setIsConnected({
setIsConnectedFor({
i,
isConnected: true,
})
socket.emit("isReady", userStates[selfIndex].isReady())
// message = "Player has joined the lobby."
const index = selfIndex()
if (index !== -1) socket.emit("isReady", users[index].isReady())
message = "Player has joined the lobby."
break
default:
// message = "Not defined yet."
message = "Not defined yet."
break
}
// toast.info(message, { toastId: message })
console.log(message)
if (type === "disconnect") return
const { payload, hash } = event
const newHash = setPlayer(payload)
console.log(newHash, hash, !newHash, newHash === hash)
const { hash } = event
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
console.log("hash", hash, newHash)
socket.emit("update", (body) => {
@ -104,17 +122,16 @@ function useSocket() {
})
}
const gameSetting = (payload: GameSettings, hash: string) => {
const newHash = setSetting(payload)
if (!newHash || newHash === hash) return
console.log("hash", hash, newHash)
socket.emit("update", (body) => {
console.log("update")
full(body)
})
const activeIndex = (i: 0 | 1) => {
setActiveIndex(i)
if (i === selfIndex()) {
setMenu("moves")
setMode(0)
} else {
setMenu("main")
setMode(-1)
}
}
const activeIndex = (i: number) => setActiveIndex(i, selfIndex)
const disconnect = () => {
console.log("disconnect")
@ -125,7 +142,7 @@ function useSocket() {
socket.on("connect_error", connectError)
socket.on("gameSetting", gameSetting)
socket.on("playerEvent", playerEvent)
socket.on("isReady", setIsReady)
socket.on("isReady", setIsReadyFor)
socket.on("gameState", gameState)
socket.on("dispatchMove", DispatchMove)
socket.on("activeIndex", activeIndex)
@ -137,7 +154,7 @@ function useSocket() {
socket.off("connect_error", connectError)
socket.off("gameSetting", gameSetting)
socket.off("playerEvent", playerEvent)
socket.off("isReady", setIsReady)
socket.off("isReady", setIsReadyFor)
socket.off("gameState", gameState)
socket.off("dispatchMove", DispatchMove)
socket.off("activeIndex", activeIndex)
@ -147,7 +164,7 @@ function useSocket() {
})
createEffect(() => {
if (!payload?.game?.id) {
if (!gameId()) {
socket.disconnect()
fetch("/api/game/running", {
method: "GET",
@ -168,8 +185,7 @@ function useSocket() {
})
return {
isConnected:
selfIndex >= 0 ? userStates[selfIndex].isConnected : isConnectedState,
isConnected,
}
}

View file

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

View file

@ -76,15 +76,16 @@ export type GameSettingKeys =
| "allowChat"
| "allowMarkDraw"
export type GameSettings = { [key in GameSettingKeys]?: boolean }
export type GameSettings = Partial<Record<GameSettingKeys, boolean>>
export type PlayerEvent =
| {
type: "connect" | "leave"
i: number
payload: { users: PlayerSchema[] }
i: 0 | 1
users: NewUsers
hash: string
}
| {
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"]) {
try {
const body = request.json()
const body = await request.json()
const { pin } = pinBodySchema.parse(body)
return pin
} catch {

View file

@ -59,10 +59,8 @@ async function logging(
const xForwardedFor =
typeof request.headers.get === "function"
? request.headers.get("x-forwarded-for")
: "0.0.0.0" // request.headers
if (typeof request.headers.get !== "function")
console.log("IncomingHttpHeaders", request.headers)
// ("x-forwarded-for")
: // @ts-expect-error Bad IncomingHttpHeaders Type
request.headers["x-forwarded-for"]
const ip = (xForwardedFor || "127.0.0.1, 192.168.178.1").split(",")
const route = request.url
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"
export function getPayloadwithChecksum(
export const getPayloadwithChecksum = (
payload: GamePropsSchema["payload"],
): GamePropsSchema {
const objString = JSON.stringify(payload)
const hash = crypto.createHash("md5").update(objString).digest("hex")
return { payload, hash }
}
): GamePropsSchema => ({ payload, hash: hash(payload) })

View file

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

View file

@ -1,3 +1,4 @@
import { createSignal } from "solid-js"
import { count } from "~/components/Gamefield/Gamefield"
import type {
Hit,
@ -10,7 +11,7 @@ import type {
TargetList,
TargetPreview,
} from "../../interfaces/frontend"
import { Orientation } from "../zodSchemas"
import { ChatSchema, MoveSchema, Orientation } from "../zodSchemas"
export function borderCN(count: number, x: number, y: number) {
if (x === 0) return "left"
@ -129,6 +130,34 @@ export const initlialMouseCursor = {
x: 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 = (
ships: ShipProps[],

View file

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

View file

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

View file

@ -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)),
const user_Game = await db.query.user_games.findFirst({
where: and(
eq(user_games.userId, id),
eq(user_games.gameId, gamePin.gameId),
),
),
),
),
...gameSelects,
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,
id: createId(),
gameId: gamePin.gameId,
userId: id,
index: 1,
})
.returning()
)[0].gameId
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)
}
}

View file

@ -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<Awaited<ReturnType<typeof getRunningGameToUser>>>,
): GamePropsSchema {
const { gamePin, ...game } = gameDB
const users = gameDB.users
.map(({ user, ...props }) => ({
const { gamePin, users, ...game } = gameDB
const mappedUsers = users.map(({ user, ...props }) => ({
...props,
...user,
}))
.sort((user1, user2) => user1.index - user2.index)
let activeIndex = undefined
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)

View file

@ -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<string, string>,
}),
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,14 +115,14 @@ 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
const user_Game = (
await db
.delete(user_games)
.where(
and(
@ -125,25 +131,25 @@ export async function GET({ request }: APIEvent) {
),
)
.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,10 +244,12 @@ 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
@ -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()
}

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"
export default function Home() {
const navigate = useNavigate()
const navigator = useNavigate()
return (
<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"
onClick={() =>
setTimeout(() => {
navigate("/start")
navigator("/start")
}, 200)
}
>

View file

@ -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() {
<div
class={classNames(
"mx-auto flex h-full max-w-screen-2xl flex-col items-center justify-evenly",
{ "blur-sm": settings },
{ "blur-sm": settings() },
)}
>
<Logo small={true} />
<LobbyFrame openSettings={() => setSettings(true)} />
</div>
<BurgerMenu blur={settings()} />
{settings() ? (
<Show when={settings()}>
<Settings closeSettings={() => setSettings(false)} />
) : null}
</Show>
</div>
)
}

View file

@ -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<SignInErrorTypes, string> = {
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() {
</div>
{errorType && <hr class="mb-8 border-gray-400" />}
<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">
Email
</label>
@ -95,12 +103,11 @@ function Login() {
<button
id="email-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"
>
Sign in with Email
</button>
</div>
</form>
<div class="flex flex-row items-center">
<hr class="w-full" />
@ -137,21 +144,19 @@ function Login() {
</button>
</div>
</div>
{errorType ? (
<>
<Show when={errorType}>
<hr class="mt-8 border-gray-400" />
<div class="flex flex-col items-center">
<button
id="back"
onClick={() => navigate("/")}
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>
</>
) : null}
</Show>
</div>
</div>
)

View file

@ -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
</button>
</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>

View file

@ -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)
}
>
<FontAwesomeIcon icon={faLeftLong} />
</button>
{!session.latest?.user?.id && (
{!session()?.user?.id && (
<button
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"
onClick={() =>
setTimeout(() => {
navigate("/signin")
navigator("/signin")
}, 200)
}
>
@ -166,12 +167,12 @@ export default function Start() {
text="Raum erstellen"
callback={gameFetch}
icon={faPlus}
disabled={!session.latest}
disabled={!session()}
/>
<OptionButton
text="Raum beitreten"
callback={() =>
navigate(
navigator(
location.pathname.concat(
"?",
new URLSearchParams({ q: "join" }).toString(),
@ -179,11 +180,11 @@ export default function Start() {
)
}
icon={faUserPlus}
disabled={!session.latest}
nodeWhen={query().join && !!session.latest}
disabled={!session()}
nodeWhen={query().join && !!session()}
node={
<>
{
<input value={otp()} onInput={(e) => setOtp(e.target.value)} />
// <OtpInput
// shouldAutoFocus
// containerStyle={{ color: "initial" }}
@ -197,14 +198,12 @@ export default function Start() {
// renderInput={(props) => <input {...props} />}
// />
}
</>
}
/>
<OptionButton
text="Zuschauen"
icon={faEye}
callback={() =>
navigate(
navigator(
location.pathname +
"?" +
new URLSearchParams({