added collection

This commit is contained in:
MasterGordon 2024-10-12 14:17:05 +02:00
parent 538750b691
commit 3e0ace5230
13 changed files with 156 additions and 59 deletions

View File

@ -8,15 +8,12 @@ import {
parseGameState, parseGameState,
upsertGameState, upsertGameState,
} from "../repositories/gameRepository"; } from "../repositories/gameRepository";
import { import { serverToClientGame, type ServerGame } from "../../shared/game";
serverGame,
serverToClientGame,
type ServerGame,
} from "../../shared/game";
import crypto from "crypto"; import crypto from "crypto";
import { game } from "../entities/game"; import { game } from "../entities/game";
import { UnauthorizedError } from "../errors/UnauthorizedError"; import { UnauthorizedError } from "../errors/UnauthorizedError";
import { emit } from "../events"; import { emit } from "../events";
import { serverGame } from "../../shared/gameType";
export const gameController = createController({ export const gameController = createController({
getGameState: createEndpoint(z.string(), async (uuid, ctx) => { getGameState: createEndpoint(z.string(), async (uuid, ctx) => {

BIN
bun.lockb

Binary file not shown.

View File

@ -16,6 +16,8 @@
}, },
"dependencies": { "dependencies": {
"@msgpack/msgpack": "^3.0.0-beta2", "@msgpack/msgpack": "^3.0.0-beta2",
"@pixi/canvas-display": "^7.4.2",
"@pixi/canvas-renderer": "^7.4.2",
"@pixi/react": "^7.1.2", "@pixi/react": "^7.1.2",
"@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2",
@ -32,6 +34,7 @@
"lucide-react": "^0.452.0", "lucide-react": "^0.452.0",
"pixi-viewport": "^5.0.3", "pixi-viewport": "^5.0.3",
"pixi.js": "^7.0.0", "pixi.js": "^7.0.0",
"pixi.js-legacy": "^7.4.2",
"react": "^18.3.1", "react": "^18.3.1",
"react-confetti-boom": "^1.0.0", "react-confetti-boom": "^1.0.0",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",

View File

@ -1,38 +1,5 @@
import { z } from "zod"; import type { ServerGame, ClientGame } from "./gameType";
export type { ServerGame, ClientGame } from "./gameType";
export const clientGame = z.object({
user: z.string(),
uuid: z.string(),
width: z.number(),
height: z.number(),
isRevealed: z.array(z.array(z.boolean())),
isFlagged: z.array(z.array(z.boolean())),
isQuestionMark: z.array(z.array(z.boolean())),
values: z.array(z.array(z.number())),
minesCount: z.number(),
lastClick: z.tuple([z.number(), z.number()]),
started: z.number(),
stage: z.number(),
});
export const serverGame = z.object({
user: z.string(),
uuid: z.string(),
width: z.number(),
height: z.number(),
isRevealed: z.array(z.array(z.boolean())),
isFlagged: z.array(z.array(z.boolean())),
isQuestionMark: z.array(z.array(z.boolean())),
mines: z.array(z.array(z.boolean())),
minesCount: z.number(),
lastClick: z.tuple([z.number(), z.number()]),
started: z.number(),
finished: z.number().default(0),
stage: z.number(),
});
export type ClientGame = z.infer<typeof clientGame>;
export type ServerGame = z.infer<typeof serverGame>;
export const isServerGame = (game: ServerGame | ClientGame) => "mines" in game; export const isServerGame = (game: ServerGame | ClientGame) => "mines" in game;
export const isClientGame = ( export const isClientGame = (

35
shared/gameType.ts Normal file
View File

@ -0,0 +1,35 @@
import { z } from "zod";
export const clientGame = z.object({
user: z.string(),
uuid: z.string(),
width: z.number(),
height: z.number(),
isRevealed: z.array(z.array(z.boolean())),
isFlagged: z.array(z.array(z.boolean())),
isQuestionMark: z.array(z.array(z.boolean())),
values: z.array(z.array(z.number())),
minesCount: z.number(),
lastClick: z.tuple([z.number(), z.number()]),
started: z.number(),
stage: z.number(),
});
export const serverGame = z.object({
user: z.string(),
uuid: z.string(),
width: z.number(),
height: z.number(),
isRevealed: z.array(z.array(z.boolean())),
isFlagged: z.array(z.array(z.boolean())),
isQuestionMark: z.array(z.array(z.boolean())),
mines: z.array(z.array(z.boolean())),
minesCount: z.number(),
lastClick: z.tuple([z.number(), z.number()]),
started: z.number(),
finished: z.number().default(0),
stage: z.number(),
});
export type ClientGame = z.infer<typeof clientGame>;
export type ServerGame = z.infer<typeof serverGame>;

41
shared/testBoard.ts Normal file
View File

@ -0,0 +1,41 @@
import { ServerGame } from "./gameType";
const rotate = (arr: boolean[][]) => {
return arr[0].map((_, colIndex) => arr.map((row) => row[colIndex]));
};
export const testBoard: ServerGame = {
user: "TestUser",
uuid: "C270D7CD-AF97-42CE-A6C9-CB765102CA17",
width: 11,
height: 4,
isRevealed: rotate([
[false, false, false, false, false, ...Array<boolean>(6).fill(true)],
[...Array<boolean>(11).fill(true)],
[...Array<boolean>(11).fill(true)],
[...Array<boolean>(6).fill(true), ...Array<boolean>(5).fill(false)],
]),
isFlagged: rotate([
[true, ...Array<boolean>(10).fill(false)],
[...Array<boolean>(11).fill(false)],
[...Array<boolean>(11).fill(false)],
[...Array<boolean>(11).fill(false)],
]),
finished: 1,
started: 1,
stage: 420,
lastClick: [2, 2],
mines: rotate([
[false, false, false, false, false, ...Array<boolean>(6).fill(true)],
[...Array<boolean>(8).fill(false), true, false, true],
[false, false, ...Array<boolean>(9).fill(true)],
[...Array<boolean>(11).fill(false)],
]),
minesCount: 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8,
isQuestionMark: rotate([
[...Array<boolean>(11).fill(false)],
[...Array<boolean>(11).fill(false)],
[...Array<boolean>(11).fill(false)],
[...Array<boolean>(11).fill(false)],
]),
};

View File

@ -5,9 +5,5 @@ export const userSettings = z.object({
longPressOnDesktop: z.boolean().default(false), longPressOnDesktop: z.boolean().default(false),
}); });
export const getDefaultSettings = () => {
return userSettings.parse({});
};
export type UserSettings = z.infer<typeof userSettings>; export type UserSettings = z.infer<typeof userSettings>;
export type UserSettingsInput = z.input<typeof userSettings>; export type UserSettingsInput = z.input<typeof userSettings>;

View File

@ -95,7 +95,7 @@ const Shell: React.FC<PropsWithChildren> = ({ children }) => {
transition={{ type: "tween" }} transition={{ type: "tween" }}
layout layout
/> />
<motion.div className="flex flex-col gap-4 grow max-w-6xl mx-auto w-[calc(100vw-256px)]"> <motion.div className="flex flex-col gap-4 grow max-w-7xl mx-auto w-[calc(100vw-256px)]">
<div className="flex flex-col justify-center gap-4 sm:mx-16 mt-16 sm:mt-2 mx-2"> <div className="flex flex-col justify-center gap-4 sm:mx-16 mt-16 sm:mt-2 mx-2">
<Header /> <Header />
{children} {children}

View File

@ -26,6 +26,16 @@ import { Button } from "./Button";
import { Maximize2, Minimize2, RotateCcw } from "lucide-react"; import { Maximize2, Minimize2, RotateCcw } from "lucide-react";
import useSound from "use-sound"; import useSound from "use-sound";
import explosion from "../sound/explosion.mp3"; import explosion from "../sound/explosion.mp3";
import "@pixi/canvas-display";
import "@pixi/canvas-extract";
import "@pixi/canvas-graphics";
import "@pixi/canvas-mesh";
import "@pixi/canvas-particle-container";
import "@pixi/canvas-prepare";
import "@pixi/canvas-renderer";
import "@pixi/canvas-sprite-tiling";
import "@pixi/canvas-sprite";
import "@pixi/canvas-text";
interface BoardProps { interface BoardProps {
theme: Theme; theme: Theme;
@ -33,6 +43,8 @@ interface BoardProps {
onLeftClick: (x: number, y: number) => void; onLeftClick: (x: number, y: number) => void;
onRightClick: (x: number, y: number) => void; onRightClick: (x: number, y: number) => void;
restartGame: () => void; restartGame: () => void;
width?: number;
height?: number;
} }
interface ViewportInfo { interface ViewportInfo {
@ -88,7 +100,7 @@ const Board: React.FC<BoardProps> = (props) => {
}); });
}, []); }, []);
useEffect(() => { useEffect(() => {
setTimeout(() => { setInterval(() => {
if (viewportRef.current) onViewportChange(viewportRef.current); if (viewportRef.current) onViewportChange(viewportRef.current);
}, 200); }, 200);
}, [game.width, game.height, onViewportChange]); }, [game.width, game.height, onViewportChange]);
@ -113,14 +125,25 @@ const Board: React.FC<BoardProps> = (props) => {
const viewportRef = useRef<PixiViewport>(null); const viewportRef = useRef<PixiViewport>(null);
const [zenMode, setZenMode] = useState(false); const [zenMode, setZenMode] = useState(false);
useEffect(() => {
if (ref.current) {
ref.current.addEventListener("wheel", (e) => {
e.preventDefault();
});
}
}, [ref]);
return ( return (
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
<div <div
className={cn( className={cn(
"w-full h-[70vh] overflow-hidden border-white/40 border-2 flex flex-col", "w-full h-[70vh] overflow-hidden outline-white/40 outline-2 flex flex-col",
zenMode && "fixed top-0 left-0 z-50 right-0 bottom-0 h-[100vh]", zenMode && "fixed top-0 left-0 z-50 right-0 bottom-0 h-[100vh]",
)} )}
style={{
width: props.width ? `${props.width}px` : undefined,
height: props.height ? `${props.height}px` : undefined,
}}
ref={ref} ref={ref}
> >
<div className="relative"> <div className="relative">
@ -135,7 +158,7 @@ const Board: React.FC<BoardProps> = (props) => {
onClick={() => setZenMode(!zenMode)} onClick={() => setZenMode(!zenMode)}
size="sm" size="sm"
> >
{!zenMode ? ( {props.width || props.height ? undefined : !zenMode ? (
<Maximize2 className="size-4" /> <Maximize2 className="size-4" />
) : ( ) : (
<Minimize2 className="size-4" /> <Minimize2 className="size-4" />
@ -152,7 +175,7 @@ const Board: React.FC<BoardProps> = (props) => {
</div> </div>
{theme && ( {theme && (
<Stage <Stage
options={{ hello: true }} options={{ hello: true, forceCanvas: !!props.width }}
width={width} width={width}
height={height} height={height}
className="select-none" className="select-none"
@ -163,12 +186,16 @@ const Board: React.FC<BoardProps> = (props) => {
worldHeight={boardHeight} worldHeight={boardHeight}
width={width} width={width}
height={height} height={height}
clamp={{ clamp={
left: -theme.size, props.width || props.height
right: boardWidth + theme.size, ? { left: 0, right: boardWidth, top: 0, bottom: boardHeight }
top: -theme.size, : {
bottom: boardHeight + theme.size, left: -theme.size,
}} right: boardWidth + theme.size,
top: -theme.size,
bottom: boardHeight + theme.size,
}
}
clampZoom={{ clampZoom={{
minScale: 1, minScale: 1,
}} }}
@ -204,7 +231,7 @@ const Board: React.FC<BoardProps> = (props) => {
</Stage> </Stage>
)} )}
</div> </div>
<Coords /> {!props.width && !props.height && <Coords />}
</div> </div>
); );
}; };

View File

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import * as PIXI from "pixi.js"; import type { Application } from "pixi.js";
import { IClampZoomOptions, Viewport as PixiViewport } from "pixi-viewport"; import { IClampZoomOptions, Viewport as PixiViewport } from "pixi-viewport";
import { PixiComponent, useApp } from "@pixi/react"; import { PixiComponent, useApp } from "@pixi/react";
import { BaseTexture, SCALE_MODES } from "pixi.js"; import { BaseTexture, SCALE_MODES } from "pixi.js";
@ -23,7 +23,7 @@ export interface ViewportProps {
} }
export interface PixiComponentViewportProps extends ViewportProps { export interface PixiComponentViewportProps extends ViewportProps {
app: PIXI.Application; app: Application;
} }
const PixiComponentViewport = PixiComponent("Viewport", { const PixiComponentViewport = PixiComponent("Viewport", {

View File

@ -9,7 +9,7 @@ import {
} from "@tanstack/react-query"; } from "@tanstack/react-query";
import { Routes } from "../backend/router"; import { Routes } from "../backend/router";
import { wsClient } from "./wsClient"; import { wsClient } from "./wsClient";
import { z } from "zod"; import type { z } from "zod";
export const useWSQuery = < export const useWSQuery = <
TController extends keyof Routes, TController extends keyof Routes,

View File

@ -11,6 +11,7 @@ import { queryClient } from "./queryClient.ts";
import Home from "./views/home/Home.tsx"; import Home from "./views/home/Home.tsx";
import Settings from "./views/settings/Settings.tsx"; import Settings from "./views/settings/Settings.tsx";
import MatchHistory from "./views/match-history/MatchHistory.tsx"; import MatchHistory from "./views/match-history/MatchHistory.tsx";
import Collection from "./views/collection/Collection.tsx";
const setup = async () => { const setup = async () => {
const token = localStorage.getItem("loginToken"); const token = localStorage.getItem("loginToken");
@ -38,6 +39,7 @@ setup().then(() => {
</Route> </Route>
<Route path="/history" component={MatchHistory} /> <Route path="/history" component={MatchHistory} />
<Route path="/settings" component={Settings} /> <Route path="/settings" component={Settings} />
<Route path="/collection" component={Collection} />
</Switch> </Switch>
</Shell> </Shell>
<ReactQueryDevtools initialIsOpen={false} /> <ReactQueryDevtools initialIsOpen={false} />

View File

@ -0,0 +1,29 @@
import { testBoard } from "../../../shared/testBoard";
import Board from "../../components/Board";
import { themes } from "../../themes";
const Collection = () => {
return (
<div className="flex flex-col gap-4 w-full">
<h2 className="text-white/90 text-xl">Collection</h2>
<div className="flex flex-row gap-y-4 gap-x-8 items-center w-full flex-wrap justify-center">
{themes.map((theme) => (
<div key={theme.id}>
<h3 className="text-white/90 text-lg">{theme.name}</h3>
<Board
game={testBoard}
theme={theme.theme}
onLeftClick={() => {}}
restartGame={() => {}}
onRightClick={() => {}}
width={11 * 32}
height={4 * 32}
/>
</div>
))}
</div>
</div>
);
};
export default Collection;