diff --git a/package.json b/package.json index 90b35bb..2a7822a 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@tanstack/react-query-devtools": "^5.55.4", "axios": "^1.7.7", "next": "14.2.8", + "polished": "^4.3.1", "react": "^18", "react-dom": "^18", "react-hot-toast": "^2.4.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1a11e03..de5f1e8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: next: specifier: 14.2.8 version: 14.2.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + polished: + specifier: ^4.3.1 + version: 4.3.1 react: specifier: ^18 version: 18.3.1 @@ -85,6 +88,10 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/runtime@7.25.6': + resolution: {integrity: sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==} + engines: {node: '>=6.9.0'} + '@babel/types@7.25.6': resolution: {integrity: sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==} engines: {node: '>=6.9.0'} @@ -1878,6 +1885,10 @@ packages: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} + polished@4.3.1: + resolution: {integrity: sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==} + engines: {node: '>=10'} + possible-typed-array-names@1.0.0: resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} engines: {node: '>= 0.4'} @@ -2017,6 +2028,9 @@ packages: resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} engines: {node: '>= 0.4'} + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + regexp.prototype.flags@1.5.2: resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==} engines: {node: '>= 0.4'} @@ -2358,6 +2372,10 @@ snapshots: dependencies: '@babel/types': 7.25.6 + '@babel/runtime@7.25.6': + dependencies: + regenerator-runtime: 0.14.1 + '@babel/types@7.25.6': dependencies: '@babel/helper-string-parser': 7.24.8 @@ -4376,6 +4394,10 @@ snapshots: pluralize@8.0.0: {} + polished@4.3.1: + dependencies: + '@babel/runtime': 7.25.6 + possible-typed-array-names@1.0.0: {} postcss-discard-duplicates@7.0.0(postcss@8.4.41): @@ -4511,6 +4533,8 @@ snapshots: globalthis: 1.0.4 which-builtin-type: 1.1.4 + regenerator-runtime@0.14.1: {} + regexp.prototype.flags@1.5.2: dependencies: call-bind: 1.0.7 diff --git a/src/components/organisms/GameBoard/Footer/Counters/index.tsx b/src/components/organisms/GameBoard/Footer/Counters/index.tsx index 8323f29..644cb3c 100644 --- a/src/components/organisms/GameBoard/Footer/Counters/index.tsx +++ b/src/components/organisms/GameBoard/Footer/Counters/index.tsx @@ -1,78 +1,133 @@ -import { useState } from "react"; +import { PlayerData, PlayerDataProps } from "@/types/card"; +import { useHover } from "@/utils/hooks/useHover"; +import { useRouter } from "next/router"; +import { useMemo, useRef } from "react"; import { cva } from "styled-system/css/cva.mjs"; import { Flex, Grid, HStack, VStack } from "styled-system/jsx"; import { button } from "styled-system/recipes"; -export const CountersTray = () => { +import { GiHealthNormal as IconHealth } from "react-icons/gi"; + +import { mix } from "polished"; + +export const CountersTray = (props: PlayerDataProps) => { + const { query } = useRouter(); + const name = query?.name ?? "p1"; + const me = props?.players?.[name as string].data; + + function setField(field: keyof PlayerData) { + return (value: number) => { + const newData = { ...me, [field]: value }; + props.setMyData(newData as PlayerData); + }; + } + return ( - {(["earth", "fire", "water", "wind"] as const).map((type) => ( - + {(["life", "earth", "fire", "water", "wind"] as const).map((type) => ( + ))} ); }; -const Resource = (props: { type: "fire" | "water" | "wind" | "earth" }) => { - const [amount, setAmount] = useState(0); +const Resource = (props: { + type: "fire" | "water" | "wind" | "earth" | "life"; + setValue(value: number): void; + value: number; +}) => { + const hoverRef = useRef(null); + const isHovering = useHover(hoverRef); + function increment() { - setAmount((prev) => prev + 1); + props.setValue(props.value + 1); } function decrement() { - setAmount((prev) => (prev === 0 ? prev : prev - 1)); + if (props.value === 0) return; + props.setValue(props.value - 1); } + const bg = useMemo(() => { + if (props.type !== "life") return ""; + const percent = Math.min(props.value / 20, 1); + if (props.value > 10) return mix(percent, "#00b8a9", "#f8f3d4"); + if (props.value === 10) return "#f8f3d4"; + if (props.value > 1) return mix(props.value / 20, "#f8f3d4", "#f6416c"); + + return "purple"; + }, [props.value]); + return ( - - - fire -

{amount}

-
- - + + + {isHovering && ( + + - - + + + )} + {!isHovering && props.type === "life" && ( + + )} + {!isHovering && props.type !== "life" && ( + fire + )} +

{props.value}

+
); }; diff --git a/src/components/organisms/GameBoard/Footer/index.tsx b/src/components/organisms/GameBoard/Footer/index.tsx index 6cdabb4..c64d7db 100644 --- a/src/components/organisms/GameBoard/Footer/index.tsx +++ b/src/components/organisms/GameBoard/Footer/index.tsx @@ -11,7 +11,7 @@ import { CardAtlas } from "@/components/atoms/mock-cards/atlas"; import { CardImage } from "@/components/atoms/mock-cards/card"; import { DecksTray } from "./Decks"; import { GraveTray } from "./Grave"; -import { GameCard } from "@/types/card"; +import { GameCard, PlayerDataProps } from "@/types/card"; import { useState } from "react"; import { Modal } from "@/components/atoms/Modal"; import { FullCardAtlas } from "@/components/atoms/card-view/atlas"; @@ -28,7 +28,7 @@ import { CountersTray } from "./Counters"; /** * HAND - Drag and Drop tray of all the cards in your hand * */ -export const GameFooter = (props: GameStateActions) => { +export const GameFooter = (props: GameStateActions & PlayerDataProps) => { const gridIndex = GRIDS.HAND; const cardsInHand = props.gridItems[gridIndex] ?? []; @@ -50,7 +50,7 @@ export const GameFooter = (props: GameStateActions) => { > - + { + const life = player.data.life; + + function length(position: number) { + return player.state[position].length; + } + + const bg = useMemo(() => { + const percent = Math.min(life / 20, 1); + if (life > 10) return mix(percent, "#00b8a9", "#f8f3d4"); + if (life > 1) return mix(life / 20, "#f8f3d4", "#f6416c"); + + return "purple"; + }, [life]); + + return ( + +

{name}

+ + + +

{life}

+
+ + + + + + + + + + + + + + +
+ ); +}; + +const Resource = (props: { value: number; icon: keyof PlayerData }) => ( + + fire +

{props.value}

+
+); + +const Stat = ({ + Icon, + value, + ...props +}: { Icon: IconType; value: number } & JsxStyleProps) => { + return ( + + +

{value}

+
+ ); +}; + +const textStyle = cva({ + base: { + fontFamily: "monospace", + }, + variants: { + visual: { + bold: { fontWeight: 600, fontFamily: "serif" }, + }, + }, +}); diff --git a/src/components/organisms/GameBoard/Header/index.tsx b/src/components/organisms/GameBoard/Header/index.tsx new file mode 100644 index 0000000..eff1426 --- /dev/null +++ b/src/components/organisms/GameBoard/Header/index.tsx @@ -0,0 +1,32 @@ +import Link from "next/link"; +import { PlayersState } from "@/types/card"; +import { LAYOUT_HEIGHTS } from "../constants"; +import { PlayerBox } from "./PlayerBox"; + +export const GameHeader = (props: { players?: PlayersState }) => { + const playerKeys = Object.keys(props?.players ?? {}).filter( + (key) => key !== "GLOBAL", + ); + return ( +
+ {playerKeys?.map((key) => ( + + {props?.players?.[key] && ( + + )} + + ))} +
+ ); +}; diff --git a/src/components/organisms/GameBoard/Layout.tsx b/src/components/organisms/GameBoard/Layout.tsx index 80ebe97..d30d94d 100644 --- a/src/components/organisms/GameBoard/Layout.tsx +++ b/src/components/organisms/GameBoard/Layout.tsx @@ -5,35 +5,17 @@ import { LAYOUT_HEIGHTS } from "./constants"; import { GameFooter } from "./Footer"; import { GameStateActions } from "."; import { Auras } from "./Auras"; -import Link from "next/link"; +import { PlayerDataProps } from "@/types/card"; +import { GameHeader } from "./Header"; const { nav, body, footer } = LAYOUT_HEIGHTS; export const GameLayout = ( - props: GameStateActions & { children: ReactNode }, + props: GameStateActions & PlayerDataProps & { children: ReactNode }, ) => { return ( -
-

- Playtest cards on a grid. Click to tap. Right click to view big. -

- -

p1

- - - -

p2

- -
+
void; }; -export const GameBoard = ({ gridItems, setGridItems }: GameStateActions) => { +export const GameBoard = ({ + gridItems, + setGridItems, + ...playerDataProps +}: GameStateActions & PlayerDataProps) => { const { query } = useRouter(); const { activeCard, activeId, ...dragProps } = useHandleDrag({ @@ -33,7 +37,11 @@ export const GameBoard = ({ gridItems, setGridItems }: GameStateActions) => { return ( - + {(isReversed ? gridItems.slice(0, 20).reverse() : gridItems.slice(0, 20) diff --git a/src/pages/game.tsx b/src/pages/game.tsx index e6693ba..5325db0 100644 --- a/src/pages/game.tsx +++ b/src/pages/game.tsx @@ -1,24 +1,30 @@ import { LoadDeck } from "@/components/molecules/LoadDeck"; import { GameBoard } from "@/components/organisms/GameBoard"; import { GRIDS } from "@/components/organisms/GameBoard/constants"; -import { GameCard, GameState, PlayersState } from "@/types/card"; +import { GameCard, GameState, PlayerData, PlayersState } from "@/types/card"; import { useRouter } from "next/router"; import { useMemo, useState } from "react"; const initGameState: GameCard[][] = Array.from({ length: 36 }, () => []); +const initGameData: PlayerData = { + earth: 0, + wind: 0, + fire: 0, + water: 0, + life: 20, +}; export default function GamePage() { const { query } = useRouter(); const name = (query?.name as string | undefined) ?? "p1"; - // const [gridItems, setGridItems] = useState(initGameState); const [players, setPlayers] = useState({ - p1: initGameState, - p2: initGameState, - GLOBAL: initGameState, + p1: { state: initGameState, data: initGameData }, + p2: { state: initGameState, data: initGameData }, + GLOBAL: { state: initGameState, data: initGameData }, }); - function setPlayer(playerName: keyof typeof players) { + function setPlayerState(playerName: keyof typeof players) { return (state: GameState) => { const newState = [...state]; // make a copy of state const GLOBAL = newState.slice(0, GRIDS.AURA_12 + 1); // GLOBAL takes game grid @@ -26,36 +32,58 @@ export default function GamePage() { const newGlobal = [...GLOBAL, ...GLOBAL_EMPTY]; setPlayers((prev) => ({ ...prev, - GLOBAL: newGlobal, - [playerName]: state, + GLOBAL: { state: newGlobal, data: initGameData }, + [playerName]: { state, data: prev[playerName].data }, + })); + }; + } + + function setPlayerData(playerName: keyof typeof players) { + return (data: PlayersState["GLOBAL"]["data"]) => { + setPlayers((prev) => ({ + ...prev, + [playerName]: { state: prev[playerName].state, data }, })); }; } const state = useMemo(() => { return [ - ...players.GLOBAL.slice(0, GRIDS.HAND), - ...players[name as keyof typeof players].slice(GRIDS.HAND), + ...players.GLOBAL.state.slice(0, GRIDS.HAND), + ...players[name as keyof typeof players].state.slice(GRIDS.HAND), ]; }, [players, name]); - if (needsDeck(players["p1"])) + if (needsDeck(players["p1"].state)) return (

Load deck for player 1

- +
); - if (needsDeck(players["p2"])) + if (needsDeck(players["p2"].state)) return (

Load deck for player 2

- +
); - return ; + return ( + + ); } function needsDeck(state: GameState) { diff --git a/src/styles/globals.css b/src/styles/globals.css index e27a23b..acc752e 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -1 +1,21 @@ @layer reset, base, tokens, recipes, utilities; + +/* custom scrollbar */ +::-webkit-scrollbar { + width: 20px; +} + +::-webkit-scrollbar-track { + background-color: transparent; +} + +::-webkit-scrollbar-thumb { + background-color: #d6dee1; + border-radius: 20px; + border: 6px solid transparent; + background-clip: content-box; +} + +::-webkit-scrollbar-thumb:hover { + background-color: #a8bbbf; +} diff --git a/src/types/card.ts b/src/types/card.ts index e10ffcb..df9ffec 100644 --- a/src/types/card.ts +++ b/src/types/card.ts @@ -49,4 +49,17 @@ export type GridItem = GameCard[]; * */ export type GameState = GridItem[]; -export type PlayersState = Record & { GLOBAL: GameState }; +export type PlayerData = { + earth: number; + wind: number; + fire: number; + water: number; + life: number; +}; +type PState = { state: GameState; data: PlayerData }; +export type PlayersState = Record & { GLOBAL: PState }; + +export type PlayerDataProps = { + players?: PlayersState; + setMyData(data: PlayersState["GLOBAL"]["data"]): void; +};