diff --git a/backend/index.ts b/backend/index.ts index 6c96a61..3a13a99 100644 --- a/backend/index.ts +++ b/backend/index.ts @@ -15,6 +15,7 @@ const requestDuration = new Histogram({ help: "Request duration", labelNames: ["action"], }); +// promClient.register.registerMetric(requestDuration); const metricsUser = process.env.METRICS_USER; const metricsPassword = process.env.METRICS_PASSWORD; diff --git a/shared/utils.ts b/shared/utils.ts index ac48d73..9e99651 100644 --- a/shared/utils.ts +++ b/shared/utils.ts @@ -3,11 +3,19 @@ export const pickRandom = (arr: T[]) => { return arr[index]; }; +function bashHashStr(str: string) { + let hash = 5381, + i = str.length; + + while (i) { + hash = (hash * 33) ^ str.charCodeAt(--i); + } + + return hash >>> 0; +} + export const hashStr = (str: string) => { - return [...str].reduce( - (hash, c) => (Math.imul(31, hash) + c.charCodeAt(0)) | 0, - 0, - ); + return Number(`0.${bashHashStr(str)}`); }; export const weightedPickRandom = ( diff --git a/src/components/Board.tsx b/src/components/Board.tsx index 76e3327..c4acd69 100644 --- a/src/components/Board.tsx +++ b/src/components/Board.tsx @@ -1,20 +1,25 @@ import { - ReactNode, + type ReactNode, useCallback, useEffect, useMemo, useRef, useState, } from "react"; -import { LoadedTheme, Theme, useTheme } from "../themes/Theme"; +import { + type LoadedTexture, + type LoadedTheme, + type Theme, + useTheme, +} from "../themes/Theme"; import { Container, Sprite, Stage, useTick } from "@pixi/react"; import Viewport from "./pixi/PixiViewport"; import type { Viewport as PixiViewport } from "pixi-viewport"; import { - ClientGame, + type ClientGame, getValue, isServerGame, - ServerGame, + type ServerGame, } from "../../shared/game"; import { useWSQuery } from "../hooks"; import { Texture } from "pixi.js"; @@ -37,6 +42,7 @@ import "@pixi/canvas-sprite-tiling"; import "@pixi/canvas-sprite"; import "@pixi/canvas-text"; import { themes } from "../themes"; +import { hashStr, weightedPickRandom } from "../../shared/utils"; interface BoardProps { className?: string; @@ -271,6 +277,20 @@ const Tile = ({ onRightClick, onLeftClick, }: TileProps) => { + const resolveSprite = useCallback( + (lt: LoadedTexture) => { + if (Array.isArray(lt)) { + console.log("hash:", hashStr(game.uuid + ";" + x + ";" + y)); + return weightedPickRandom( + lt, + (i) => i.weight, + (tw) => hashStr(game.uuid + ";" + x + ";" + y) * tw, + ).sprite; + } + return lt; + }, + [game.uuid, x, y], + ); const i = x; const j = y; const isRevealed = game.isRevealed[i][j]; @@ -285,11 +305,13 @@ const Tile = ({ const isQuestionMark = game.isQuestionMark[i][j]; const base = isRevealed || (isMine && !isFlagged) ? ( - + ) : ( - + ); - const extra = isLastPos ? : null; + const extra = isLastPos ? ( + + ) : null; const touchStart = useRef(0); const isMove = useRef(false); const startX = useRef(0); @@ -323,14 +345,24 @@ const Tile = ({ ); let content: ReactNode = null; if (isFlagged) { - content = ; + content = ( + + ); } else if (isMine) { - content = ; + content = ( + + ); } else if (value !== -1 && isRevealed) { const img = theme[value.toString() as keyof Theme] as Texture; content = img ? : null; } else if (isQuestionMark) { - content = ; + content = ( + + ); } const [, setCursorX] = useAtom(cursorXAtom); const [, setCursorY] = useAtom(cursorYAtom); diff --git a/src/themes/Theme.ts b/src/themes/Theme.ts index be87a12..f5c739d 100644 --- a/src/themes/Theme.ts +++ b/src/themes/Theme.ts @@ -4,12 +4,17 @@ import { useEffect, useState } from "react"; type Png = typeof import("*.png"); type LazySprite = () => Promise; +interface WeightedLazySprites { + weight: number; + sprite: LazySprite; +} + export interface Theme { size: number; - mine: LazySprite; - tile: LazySprite; + mine: LazySprite | WeightedLazySprites[]; + tile: LazySprite | WeightedLazySprites[]; revealed: LazySprite; - flag: LazySprite; + flag: LazySprite | WeightedLazySprites[]; questionMark: LazySprite; lastPos: LazySprite; 1: LazySprite; @@ -22,7 +27,17 @@ export interface Theme { 8: LazySprite; } -export type LoadedTheme = Record, Texture> & { +export type LoadedTexture = + | Texture + | { + weight: number; + sprite: Texture; + }[]; + +export type LoadedTheme = Record< + Exclude, + LoadedTexture +> & { size: number; }; @@ -34,13 +49,25 @@ export const useTheme = (theme: Theme) => { const loadTheme = async () => { const loadedEntries = await Promise.all( Object.entries(theme).map(async ([key, value]) => { - const loaded = - typeof value === "function" - ? await Assets.load((await value()).default) - : value; + let loaded = value; + if (typeof value === "function") { + loaded = await Assets.load((await value()).default); + } + if (Array.isArray(value)) { + loaded = await Promise.all( + loaded.map(async (sprite: WeightedLazySprites) => { + return { + weight: sprite.weight, + sprite: await Assets.load((await sprite.sprite()).default), + }; + }), + ); + } + return [key, loaded] as const; }), ); + console.log("loaded", Object.fromEntries(loadedEntries)); setLoadedTheme(Object.fromEntries(loadedEntries) as LoadedTheme); }; loadTheme(); diff --git a/src/themes/techies-dire.ts b/src/themes/techies-dire.ts index ad7a799..627163e 100644 --- a/src/themes/techies-dire.ts +++ b/src/themes/techies-dire.ts @@ -1,8 +1,17 @@ -import { Theme } from "./Theme"; +import type { Theme } from "./Theme"; export const techiesDireTheme: Theme = { size: 32, - mine: () => import("../assets/themes/techies/dire/mine-1.png"), + mine: [ + { + weight: 0.5, + sprite: () => import("../assets/themes/techies/dire/mine-1.png"), + }, + { + weight: 0.5, + sprite: () => import("../assets/themes/techies/dire/mine-2.png"), + }, + ], tile: () => import("../assets/themes/techies/dire/tile-1.png"), revealed: () => import("../assets/themes/techies/dire/revealed.png"), flag: () => import("../assets/themes/techies/flag.png"), diff --git a/src/views/store/Store.tsx b/src/views/store/Store.tsx index 49a156d..6e147e6 100644 --- a/src/views/store/Store.tsx +++ b/src/views/store/Store.tsx @@ -184,11 +184,11 @@ const Store = () => { variant="outline" size="default" className="mx-auto items-center" - onClick={() => + onClick={() => { openLootbox .mutateAsync({ id: lootbox.id }) - .then(() => refetch()) - } + .then(() => refetch()); + }} > Buy for {lootbox.priceText}