added the games route and two test game sub-routes for colyseus testing.

This commit is contained in:
HP
2026-01-16 21:12:54 -05:00
parent e3141030ab
commit ff6eb30f96
8 changed files with 580 additions and 5 deletions

184
package-lock.json generated
View File

@@ -15,6 +15,7 @@
"@mui/material": "^7.3.7",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.5.0",
"colyseus.js": "^0.16.22",
"next": "16.1.1",
"pg": "^8.17.0",
"react": "19.2.3",
@@ -270,6 +271,34 @@
"node": ">=6.9.0"
}
},
"node_modules/@colyseus/httpie": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@colyseus/httpie/-/httpie-2.0.1.tgz",
"integrity": "sha512-JvABMZzPLiyrUsVj3ElXGORRDTu+NKzXHWd1uV1R1SThAKMm06cVW6bOyADARD65bs8JJoHNNbUkW8KoRvRDzA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/@colyseus/msgpackr": {
"version": "1.11.2",
"resolved": "https://registry.npmjs.org/@colyseus/msgpackr/-/msgpackr-1.11.2.tgz",
"integrity": "sha512-MuwPFhizFKC3zmGfy0fpo+kcnZdNdnQHFVjw81v4WXHCelDeCX8yNRVtuEm8kGlHqq7qiASLC0pu0RPqYOhxXg==",
"license": "MIT",
"optionalDependencies": {
"msgpackr-extract": "^3.0.2"
}
},
"node_modules/@colyseus/schema": {
"version": "3.0.76",
"resolved": "https://registry.npmjs.org/@colyseus/schema/-/schema-3.0.76.tgz",
"integrity": "sha512-i+ceBZyhB7lTn5+BoG/xxYfzW4dKKyLOywsGKVgXHe9fD905AS/Lk180jd1bICEJhebGeiRXEQ2YUPl/xwFg2g==",
"license": "MIT",
"bin": {
"schema-codegen": "bin/schema-codegen",
"schema-debug": "bin/schema-debug"
}
},
"node_modules/@dimforge/rapier3d-compat": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
@@ -1196,6 +1225,84 @@
"three": ">= 0.159.0"
}
},
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz",
"integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz",
"integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz",
"integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz",
"integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz",
"integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz",
"integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@mui/core-downloads-tracker": {
"version": "7.3.7",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.7.tgz",
@@ -3043,6 +3150,25 @@
"dev": true,
"license": "MIT"
},
"node_modules/colyseus.js": {
"version": "0.16.22",
"resolved": "https://registry.npmjs.org/colyseus.js/-/colyseus.js-0.16.22.tgz",
"integrity": "sha512-xyiajukHvlwOtcziVbXZWmz7yBH3EImovYrGPAe2kVkdubLVYmOjskJuXh2VLlO8XGjyhmNwig9ELz18sTUo9g==",
"license": "MIT",
"dependencies": {
"@colyseus/httpie": "^2.0.0",
"@colyseus/msgpackr": "^1.11.2",
"@colyseus/schema": "^3.0.0",
"tslib": "^2.1.0",
"ws": "^8.13.0"
},
"engines": {
"node": ">= 12.x"
},
"funding": {
"url": "https://github.com/sponsors/endel"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -5254,6 +5380,28 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/msgpackr-extract": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz",
"integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-gyp-build-optional-packages": "5.2.2"
},
"bin": {
"download-msgpackr-prebuilds": "bin/download-prebuilds.js"
},
"optionalDependencies": {
"@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3",
"@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3",
"@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3",
"@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3",
"@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3",
"@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -5348,6 +5496,21 @@
}
}
},
"node_modules/node-gyp-build-optional-packages": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz",
"integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==",
"license": "MIT",
"optional": true,
"dependencies": {
"detect-libc": "^2.0.1"
},
"bin": {
"node-gyp-build-optional-packages": "bin.js",
"node-gyp-build-optional-packages-optional": "optional.js",
"node-gyp-build-optional-packages-test": "build-test.js"
}
},
"node_modules/node-releases": {
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
@@ -7162,6 +7325,27 @@
"node": ">=0.10.0"
}
},
"node_modules/ws": {
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View File

@@ -16,6 +16,7 @@
"@mui/material": "^7.3.7",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.5.0",
"colyseus.js": "^0.16.22",
"next": "16.1.1",
"pg": "^8.17.0",
"react": "19.2.3",

80
src/app/games/page.tsx Normal file
View File

@@ -0,0 +1,80 @@
"use client"
import React from "react"
import {
Box,
Card,
Container,
List,
ListItemButton,
ListItemText,
Stack,
Typography,
} from "@mui/material";
import { useRouter } from "next/navigation";
import { paths } from "@/paths";
const navItems = [
{ label: "Visions Of Reality", path: paths.games.vor },
{ label: "Tix-Tax", path: paths.games.tixtax },
];
export default function VisionsOfReality() {
const router = useRouter();
return (
<Box
sx={{
minHeight: "100vh",
position: "relative",
overflow: "hidden",
}}
>
<Stack spacing={2}>
<Box>
<Typography variant="h5" fontWeight={700}>
Some online games available to you.
</Typography>
<Typography variant="body2" sx={{ opacity: 0.85 }}>
Choose a one to continue.
</Typography>
</Box>
<List
disablePadding
sx={{
display: "flex",
flexDirection: "column",
gap: 1,
}}
>
{navItems.map((item) => (
<ListItemButton
key={item.path}
onClick={() => router.push(item.path)}
sx={{
borderRadius: 2,
border: "1px solid rgba(255,255,255,0.16)",
backgroundColor: "rgba(0,0,0,0.18)",
transition: "transform 120ms ease, background-color 120ms ease",
"&:hover": {
backgroundColor: "rgba(255,255,255,0.12)",
transform: "translateX(-4px)",
},
"&:active": {
transform: "translateX(-2px) scale(0.99)",
},
}}
>
<ListItemText
primary={item.label}
primaryTypographyProps={{
fontWeight: 600,
}}
/>
</ListItemButton>
))}
</List>
</Stack>
</Box>
)
}

View File

@@ -0,0 +1,100 @@
'use client'
import React from "react"
import { Client, Room } from "colyseus.js"
import { Box, Button } from "@mui/material";
interface TixTaxLike{
playerOne: { name: string; coins: number },
playerTwo: { name: string; coins: number }
}
export default function Page() {
const [client] = React.useState(() => new Client("http://prospera:2567"));
const [room, setRoom] = React.useState<Room<TixTaxLike>>();
const [state, setState] = React.useState<TixTaxLike | undefined>(undefined);
const handleFindMatch = async () => {
const room = await client.joinOrCreate("tix_tax")
setRoom(room);
room.onStateChange((state) => {
setState(state)
})
room.onLeave(() => {
})
}
return (
<>
<Box>
Welcome to Tix Tax
<Button>
How to play
</Button>
</Box>
{state ? <TixTax state={state}/> : <>
<Button onClick={handleFindMatch} >
Find a match
</Button>
</>}
<div style={{
display: "grid",
gridTemplateColumns: "300px 300px 300px",
gridAutoRows: "300 px",
gap: "1em",
justifyItems: "center",
alignItems: "center",
justifyContent: "center",
alignContent: "center"
}}>
{Array.from({ length: 11 }, (_, index) => (
<div style={{
height: "200px",
width: "200px",
padding: "2em",
backgroundColor: "var(--primary-color)",
color: "white",
textAlign: "center",
"borderRadius": "8px",
fontSize: "2rem",
}}>{index + 1}</div>
))}
</div>
</>
)
}
function TixTax({ state }: { state: TixTaxLike} ){
return (
<>
<Box>
Player one Coins {state.playerOne.coins}
</Box>
<Box>
{Array.from({ length: 3 }, (_, i) => (
<Box sx={{
border: 2,
display: "grid",
placeContent: "center"
}}>
{Array.from({ length: 3 }, (_, j) => (
<Box sx={{
border: 2,
}}>
</Box>
))}
</Box>
))}
</Box>
</>
)
}

View File

@@ -0,0 +1,192 @@
"use client"
import React from "react"
import { Client, Room } from "colyseus.js"
import { Button } from "@mui/material"
type RealityLike = {
barracks?: Record<string, number>
entities?: Record<string, unknown>
hero?: unknown
}
type GameStateLike = {
realities?: Record<string, RealityLike> | Map<string, RealityLike>
home_reality_index?: number
target_reality_index?: number
}
const ROOM_NAME = "main_game";
function toRealityEntries(realities: GameStateLike["realities"]): Array<[string, RealityLike]> {
if (!realities) return []
if (typeof (realities as Map<string, RealityLike>).entries === "function") {
return Array.from((realities as Map<string, RealityLike>).entries())
}
return Object.entries(realities as Record<string, RealityLike>)
}
export default function VisionsOfReality() {
const [client] = React.useState(() => new Client(process.env.SERVER_URL))
const [room, setRoom] = React.useState<Room<GameStateLike> | null>(null)
const [status, setStatus] = React.useState("disconnected")
const [tick, setTick] = React.useState(0)
const [homeRealityIndex, setHomeRealityIndex] = React.useState<number | null>(null)
const [targetRealityIndex, setTargetRealityIndex] = React.useState<number | null>(null)
const [viewRealityIndex, setViewRealityIndex] = React.useState<number | null>(null)
React.useEffect(() => {
return () => {
room?.leave()
}
}, [room])
const connect = async () => {
setStatus("connecting")
try {
const joined = await client.joinOrCreate<GameStateLike>(ROOM_NAME)
setRoom(joined)
setStatus("connected")
setHomeRealityIndex(joined.state?.home_reality_index ?? null)
setTargetRealityIndex(joined.state?.target_reality_index ?? null)
setViewRealityIndex(joined.state?.home_reality_index ?? null)
joined.onStateChange((state) => {
setHomeRealityIndex(state.home_reality_index ?? null)
setTargetRealityIndex(state.target_reality_index ?? null)
setViewRealityIndex((prev) => (prev === null ? state.home_reality_index ?? null : prev))
setTick((t) => t + 1)
})
joined.onLeave(() => {
setStatus("disconnected")
setRoom(null)
})
} catch (err) {
console.error("Failed to join room", err)
setStatus("error")
}
}
const disconnect = async () => {
await room?.leave()
setRoom(null)
setStatus("disconnected")
}
const realities = toRealityEntries(room?.state?.realities)
const currentReality =
viewRealityIndex !== null
? realities.find(([id]) => Number(id) === viewRealityIndex)?.[1]
: undefined
const sendTarget = (index: number) => {
if (!room) return
setTargetRealityIndex(index)
room.send("set_target_reality_index", { index })
}
return (
<div
style={{
minHeight: "100vh",
display: "grid",
gridTemplateRows: "auto 1fr",
gap: 12,
padding: 12,
}}
>
<div style={{ display: "flex", gap: 12, alignItems: "center", flexWrap: "wrap" }}>
<Button variant="contained" disabled={status === "connected"} onClick={connect}>
Join
</Button>
<Button variant="outlined" disabled={!room} onClick={disconnect}>
Leave
</Button>
<div>status: {status}</div>
<div>tick: {tick}</div>
<div>home: {homeRealityIndex ?? "?"}</div>
<div>view: {viewRealityIndex ?? "?"}</div>
<div>target: {targetRealityIndex ?? "?"}</div>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(3, 44px)",
gap: 6,
}}
>
{Array.from({ length: 9 })
.map((_, index) => index)
.filter((index) => index !== 4)
.filter((index) => index !== homeRealityIndex)
.map((index) => {
const isTarget = targetRealityIndex === index
const isViewing = viewRealityIndex === index
return (
<button
key={index}
onClick={() => {
sendTarget(index)
setViewRealityIndex(index)
}}
style={{
height: 44,
borderRadius: "50%",
border: "1px solid #333",
background: isTarget ? "#f0b429" : isViewing ? "#9ae6b4" : "#f7f7f7",
cursor: "pointer",
}}
title={`Reality ${index}`}
>
{index}
</button>
)
})}
</div>
</div>
<div
style={{
display: "grid",
gap: 12,
gridTemplateRows: "auto 1fr auto",
minHeight: 0,
}}
>
<div style={{ fontWeight: 600, fontSize: 18 }}>
Reality {viewRealityIndex ?? "?"} {viewRealityIndex === homeRealityIndex ? "(home)" : ""}
</div>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(15, 1fr)",
gridAutoRows: "1fr",
gap: 2,
height: "100%",
minHeight: 0,
background: "#fafafa",
padding: 8,
borderRadius: 8,
border: "1px solid #e5e7eb",
}}
>
{Array.from({ length: 225 }).map((__, cellIndex) => (
<div
key={cellIndex}
style={{
aspectRatio: "1 / 1",
background: "#e5e7eb",
}}
/>
))}
</div>
<div style={{ display: "flex", gap: 12, flexWrap: "wrap" }}>
<div>barracks: {currentReality?.barracks ? Object.keys(currentReality.barracks).length : 0}</div>
<div>entities: {currentReality?.entities ? Object.keys(currentReality.entities).length : 0}</div>
<div>hero: {currentReality?.hero ? "yes" : "no"}</div>
</div>
</div>
</div>
)
}

View File

@@ -16,7 +16,7 @@ import { paths } from "@/paths";
const navItems = [
{ label: "Products", path: paths.products.home },
{ label: "Pricing", path: "/pricing" },
{ label: "Games", path: paths.games.home },
{ label: "About", path: "/about" },
{ label: "Contact", path: "/contact" },
];
@@ -48,7 +48,6 @@ export default function Home() {
zIndex: 0,
}}
/>
<Box
sx={{
position: "fixed",
@@ -59,6 +58,11 @@ export default function Home() {
}}
/>
<Typography sx={{
}}>
Tartarus
</Typography>
<Container
maxWidth="lg"
sx={{
@@ -130,9 +134,6 @@ export default function Home() {
</List>
</Stack>
<br />
<Typography color="textPrimary" style={{ fontSize: 12, color: "##808080" }}>
Tartarus
</Typography>
</Card>
</Container>
</Box>

12
src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
export interface Env {
DB_STRING?: string;
SERVER_URL?: string;
TIX_TAX_ROOM_NAME?: string;
}
declare global {
namespace NodeJS{
interface ProcessEnv extends Env {}
}
}

View File

@@ -3,6 +3,11 @@ export const paths = {
products: {
home: '/products',
},
games: {
home: "/games",
vor: "/games/visions-of-reality",
tixtax: "/games/tix-tax"
},
auth: {
signIn: '/auth/sign-in',
resetPassword: '/auth/reset-password',