From 1effb1d4df132e0c9bbdb48a284cd7c9340c14d1 Mon Sep 17 00:00:00 2001 From: mhzrerfani Date: Sun, 29 Sep 2024 01:28:51 +0330 Subject: [PATCH] Add dealing card and bet animations --- .../web/src/app/games/[id]/components/Pot.tsx | 43 ++++ .../games/[id]/components/PrivateCards.tsx | 88 ++++++++ .../app/games/[id]/components/PublicCards.tsx | 21 ++ .../src/app/games/[id]/components/Table.tsx | 88 ++------ .../[id]/components/WaitingIndicator.tsx | 9 + apps/web/src/app/games/[id]/page.tsx | 204 ++++++++---------- .../[id]/state/selectors/gameSelectors.ts | 2 + apps/web/src/app/games/[id]/state/state.ts | 4 +- apps/web/src/app/globals.css | 186 ++++++++++------ apps/web/src/utils/audio.ts | 4 +- .../base-tailwind.config.js | 24 +++ packages/ts-sdk/src/BettingManager.ts | 2 +- packages/ts-sdk/src/Game.ts | 4 + 13 files changed, 425 insertions(+), 254 deletions(-) create mode 100644 apps/web/src/app/games/[id]/components/Pot.tsx create mode 100644 apps/web/src/app/games/[id]/components/PrivateCards.tsx create mode 100644 apps/web/src/app/games/[id]/components/PublicCards.tsx create mode 100644 apps/web/src/app/games/[id]/components/WaitingIndicator.tsx diff --git a/apps/web/src/app/games/[id]/components/Pot.tsx b/apps/web/src/app/games/[id]/components/Pot.tsx new file mode 100644 index 0000000..b825c23 --- /dev/null +++ b/apps/web/src/app/games/[id]/components/Pot.tsx @@ -0,0 +1,43 @@ +import { useSelector } from "@legendapp/state/react"; +import chips from "@src/assets/images/chips/chips-3-stacks.png"; +import Image from "next/image"; +import { useEffect, useState } from "react"; +import { selectPot$ } from "../state/selectors/gameSelectors"; + +export default function Pot() { + const [started, setStarted] = useState(false); + const pot = useSelector(selectPot$()); + const raisedSeats = [2, 3, 6, 8]; + + useEffect(() => { + setTimeout(() => { + setStarted(true); + }, 4000); + }); + + return ( + <> +
+ ${pot} +
+ {raisedSeats.map((seat) => { + return ( + chips + ); + })} + + ); +} diff --git a/apps/web/src/app/games/[id]/components/PrivateCards.tsx b/apps/web/src/app/games/[id]/components/PrivateCards.tsx new file mode 100644 index 0000000..638893d --- /dev/null +++ b/apps/web/src/app/games/[id]/components/PrivateCards.tsx @@ -0,0 +1,88 @@ +import dealCardSound from "@src/assets/audio/effects/card-place.mp3"; +import { useAudio } from "@src/hooks/useAudio"; +import { CARDS_MAP } from "@src/lib/constants/cards"; +import { useEffect, useMemo, useRef, useState } from "react"; +import Card from "./Card"; +export default function PrivateCards({ + playersPrivateCards, +}: { + playersPrivateCards: Record; +}) { + const [startRevealing, setStartRevealing] = useState(false); + const [revealedCards, setRevealedCards] = useState(false); + const [dealedCards, setDealedCards] = useState([]); + const dealCardEffect = useAudio(dealCardSound, "effect"); + + const mounted = useRef(false); + + const seats = useMemo(() => { + return Object.keys(playersPrivateCards).map((seat) => Number(seat)); + }, [playersPrivateCards]); + + useEffect(() => { + if (mounted.current) return; + + mounted.current = true; + + seats.forEach((seat, index) => { + setTimeout(() => { + dealCardEffect.play(); + setDealedCards((prev) => [...prev, seat]); + }, index * 200); + }); + }, [seats, dealCardEffect]); + + return ( + <> + {seats.map((seat, i) => { + return ( +
+ {revealedCards ? ( + <> + {playersPrivateCards[seat]?.map( + (cardName, i) => + CARDS_MAP[cardName] && ( + + ), + )} + + ) : ( +
+
+
+
+ // + )} +
+ ); + })} + + ); +} diff --git a/apps/web/src/app/games/[id]/components/PublicCards.tsx b/apps/web/src/app/games/[id]/components/PublicCards.tsx new file mode 100644 index 0000000..825acfa --- /dev/null +++ b/apps/web/src/app/games/[id]/components/PublicCards.tsx @@ -0,0 +1,21 @@ +import { CARDS_MAP } from "@src/lib/constants/cards"; +import Card from "./Card"; + +export default function PublicCards({ cards }: { cards: number[] }) { + return ( +
+ {cards.map((cardIndex) => { + const cardName = CARDS_MAP[cardIndex]; + return ( + cardName && ( + + ) + ); + })} +
+ ); +} diff --git a/apps/web/src/app/games/[id]/components/Table.tsx b/apps/web/src/app/games/[id]/components/Table.tsx index 9381661..b5653aa 100644 --- a/apps/web/src/app/games/[id]/components/Table.tsx +++ b/apps/web/src/app/games/[id]/components/Table.tsx @@ -1,76 +1,22 @@ -"use client"; +import TableBackground from "@src/assets/images/table.png"; +import Image from "next/image"; +import type { ReactNode } from "react"; -import { useWallet } from "@aptos-labs/wallet-adapter-react"; -import { GameEventTypes } from "@jeton/ts-sdk"; -import FullPageLoading from "@jeton/ui/FullPageLoading"; -import { useSelector } from "@legendapp/state/react"; -import { useRouter } from "next/navigation"; -import { type FC, useEffect, useState } from "react"; -import { initGame, setTableId } from "../state/actions/gameActions"; -import { - selectGamePlayers$, - selectGameStatus$, - selectIsGameLoading$, - selectShufflingPlayer$, -} from "../state/selectors/gameSelectors"; -import { useSubscribeToGameEvent } from "./useSubscribeToGameEvent"; - -type TableComponentProps = { - id: string; -}; - -export const TableComponent: FC = ({ id }) => { - const [toffState, setToffState] = useState(false); - const players = useSelector(selectGamePlayers$()); - const gameStatus = useSelector(selectGameStatus$()); - const shufflingPlayer = useSelector(selectShufflingPlayer$()); - const [{ percentage }] = useSubscribeToGameEvent(GameEventTypes.DOWNLOAD_PROGRESS) || [ - { percentage: undefined }, - ]; - const router = useRouter(); - const { - connected, - isLoading: isWalletLoading, - signMessage, - signAndSubmitTransaction, - account, - } = useWallet(); - - useEffect(() => { - if (!isWalletLoading && !connected && toffState) { - router.push("/"); - } else if (!isWalletLoading && !connected) { - setTimeout(() => setToffState(true), 100); - } - }, [isWalletLoading, connected, router, toffState]); - - useEffect(() => { - if (!isWalletLoading && account) { - initGame(account.address, signMessage, signAndSubmitTransaction); - } - setTableId(id); - }, [id, signMessage, signAndSubmitTransaction, isWalletLoading, account]); +export function Table({ children }: { children: ReactNode }) { + return ( +
+
+ table - const isLoading = useSelector(selectIsGameLoading$()) || isWalletLoading; - if (isLoading) - return ( -
- {percentage &&

downloading... {percentage}%

} - + {children}
- ); - - return ( -
-

game state is ${gameStatus}

-

this is the actual game page

-

players are:

- {players?.map((p) => ( -
-

player id: {p.id}

- {p === shufflingPlayer && shuffling} -
- ))}
); -}; +} diff --git a/apps/web/src/app/games/[id]/components/WaitingIndicator.tsx b/apps/web/src/app/games/[id]/components/WaitingIndicator.tsx new file mode 100644 index 0000000..9f88d32 --- /dev/null +++ b/apps/web/src/app/games/[id]/components/WaitingIndicator.tsx @@ -0,0 +1,9 @@ +export default function WaitingIndicator() { + return ( +
+ Waiting for players
.
+
.
+
.
+
+ ); +} diff --git a/apps/web/src/app/games/[id]/page.tsx b/apps/web/src/app/games/[id]/page.tsx index 64fc923..7a1511c 100644 --- a/apps/web/src/app/games/[id]/page.tsx +++ b/apps/web/src/app/games/[id]/page.tsx @@ -11,8 +11,6 @@ import Avatar1 from "@src/assets/images/avatars/avatar-1.png"; import Avatar2 from "@src/assets/images/avatars/avatar-2.png"; import Avatar3 from "@src/assets/images/avatars/avatar-3.png"; import Avatar4 from "@src/assets/images/avatars/avatar-4.png"; -import chips from "@src/assets/images/chips/chips-3-stacks.png"; -import TableBackground from "@src/assets/images/table.png"; import Modal from "@src/components/Modal"; import { useAudio } from "@src/hooks/useAudio"; import { orderPlayersSeats } from "@src/utils/seat"; @@ -23,7 +21,12 @@ import type { ReactNode } from "react"; import { useEffect, useMemo, useRef, useState } from "react"; import { CARDS_MAP } from "../../../lib/constants/cards"; import Card from "./components/Card"; +import Pot from "./components/Pot"; +import PrivateCards from "./components/PrivateCards"; +import PublicCards from "./components/PublicCards"; import ShufflingCards from "./components/ShufflingCards"; +import { Table } from "./components/Table"; +import WaitingIndicator from "./components/WaitingIndicator"; import { useSubscribeToGameEvent } from "./components/useSubscribeToGameEvent"; import { initGame, placeBet, setTableId } from "./state/actions/gameActions"; import { @@ -43,6 +46,9 @@ export default function PlayPage({ params }: { params: { id: string } }) { const players = useSelector(selectGamePlayers$()); const [toffState, setToffState] = useState(false); const shufflingPlayer = useSelector(selectShufflingPlayer$()); + const cards = useSelector(selectPublicCards$); + const gameStatus = useSelector(selectGameStatus$()); + const [gameStarted, setGameStarted] = useState(true); const router = useRouter(); const { connected, @@ -51,6 +57,7 @@ export default function PlayPage({ params }: { params: { id: string } }) { signAndSubmitTransaction, account, } = useWallet(); + const mainPlayer = useMemo( () => players?.find((player) => player.id === account?.address), [players, account], @@ -58,6 +65,13 @@ export default function PlayPage({ params }: { params: { id: string } }) { const reorderedPlayers = !players || !mainPlayer ? [] : orderPlayersSeats(players, mainPlayer.id); + // biome-ignore lint/correctness/useExhaustiveDependencies: + useEffect(() => { + if (gameStatus === GameStatus.Shuffle && !gameStarted) { + setGameStarted(true); + } + }, [gameStatus]); + useEffect(() => { if (!isWalletLoading && account) { initGame(account.address, signMessage, signAndSubmitTransaction); @@ -84,73 +98,31 @@ export default function PlayPage({ params }: { params: { id: string } }) { (player, i) => player && , )} {shufflingPlayer?.id && } +
+ {/* + {gameStatus === GameStatus.AwaitingStart && } */} +
+ + {gameStarted && } - + {/* */}
); } -function Table({ children }: { children: ReactNode }) { - const cards = useSelector(selectPublicCards$); - const gameStatus = useSelector(selectGameStatus$()); - const pot = useSelector(selectPot$()); - const [gameStarted, setGameStarted] = useState(true); - - // biome-ignore lint/correctness/useExhaustiveDependencies: - useEffect(() => { - if (gameStatus === GameStatus.Shuffle && !gameStarted) { - setGameStarted(true); - } - }, [gameStatus]); - - return ( -
-
- table - - {children} -
- -
-
- {[1, 2, 3, 4, 5].map((cardIndex) => { - const cardName = CARDS_MAP[cardIndex]; - return ( - cardName && ( - - ) - ); - })} -
- {gameStatus === GameStatus.AwaitingStart && ( -
- Waiting for players
.
-
.
-
.
-
- )} - {gameStarted && ( -
- ${pot} -
- )} -
-
- ); -} function PlayerSeat({ player, seat }: { player: Player; seat: number }) { const avatars = [Avatar1, Avatar2, Avatar3, Avatar4]; const mounted = useRef(false); @@ -161,8 +133,7 @@ function PlayerSeat({ player, seat }: { player: Player; seat: number }) { const isPlayerTurn = awaitingBetFrom?.id === player.id; const myCards = useSelector(selectMyCards$()); const dealer = useSelector(selectDealer$()); - const [startRevealing, setStartRevealing] = useState(false); - const [revealedCards, setRevealedCards] = useState(false); + const gameStatus = useSelector(selectGameStatus$()); const [lastAction, setLastAction] = useState(""); @@ -170,14 +141,6 @@ function PlayerSeat({ player, seat }: { player: Player; seat: number }) { if (mounted.current) return; mounted.current = true; - - setTimeout(() => { - setStartRevealing(true); - - setTimeout(() => { - setRevealedCards(true); - }, 1000); - }, 2000); }, []); useEffect(() => { @@ -198,7 +161,7 @@ function PlayerSeat({ player, seat }: { player: Player; seat: number }) { return (
)} - {seat !== 1 && ( -
- {revealedCards ? ( - <> - {[1, 5]?.map( - (cardName, i) => - CARDS_MAP[cardName] && ( - - ), - )} - - ) : ( - <> -
-
- - )} -
- )} {seat === 1 && myCards && myCards.length > 0 && (
@@ -405,6 +329,62 @@ function GameStatusBox() { ); } +function UnrevealedCards({ startRevealing }: { startRevealing: boolean }) { + const [startAnimating, setStartAnimating] = useState(false); + + // Start animation after a short delay + useEffect(() => { + setTimeout(() => setStartAnimating(true), 200); // Delay to start animation + }, []); + + return ( + <> + {/* First card */} + + + {/* Second card */} + + + ); +} + // mock shuffling for testing // const [dealerSeat, setDealerSeat] = useState(1); diff --git a/apps/web/src/app/games/[id]/state/selectors/gameSelectors.ts b/apps/web/src/app/games/[id]/state/selectors/gameSelectors.ts index 5861dbf..c719b3e 100644 --- a/apps/web/src/app/games/[id]/state/selectors/gameSelectors.ts +++ b/apps/web/src/app/games/[id]/state/selectors/gameSelectors.ts @@ -18,3 +18,5 @@ export const selectPublicCards$ = () => { return [flopCards, turnCard, riverCard].flat(); }; + +export const selectRaiseAmount = () => state$.game.getRaiseAmount(); diff --git a/apps/web/src/app/games/[id]/state/state.ts b/apps/web/src/app/games/[id]/state/state.ts index 3bf1147..c27b8bd 100644 --- a/apps/web/src/app/games/[id]/state/state.ts +++ b/apps/web/src/app/games/[id]/state/state.ts @@ -14,8 +14,8 @@ type GameState = Omit & { shufflingPlayer?: Player; myCards?: [number, number]; flopCards?: [number, number, number]; - turnCard: [number]; - riverCard: [number]; + turnCard?: [number]; + riverCard?: [number]; pot: number[]; betState?: { round: BettingRounds; diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index 59e720b..f0b6cea 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -13,125 +13,179 @@ @media (min-width: 768px) { .seat-1 { top: 80% !important; - left: 50% !important; - transform: translateX(-50%); - } - - .seat-1 .chips { - top: -50%; left: 50%; transform: translateX(-50%); } .seat-2 { top: 90% !important; - left: 20% !important; + left: 22% !important; transform: translateY(-50%); } - .seat-2 .cards, - .seat-2 .chips { - top: -35%; - left: 50%; - transform: translate(50%); - } - .seat-3 { top: 55%; left: 4% !important; transform: translateY(-50%); } - .seat-3 .cards, - .seat-3 .chips { - top: 0%; - left: 80%; - transform: translate(50%); - } - .seat-4 { top: 25%; left: 4% !important; transform: translateY(-50%); } - .seat-4 .cards, - .seat-4 .chips { - top: 70%; - left: 150%; - transform: translate(-50%); - } - .seat-5 { - top: -20%; - left: 18%; + top: -22%; + left: 22% !important; transform: translateY(-50%); } - .seat-5 .cards, - .seat-5 .chips { - top: 100%; - left: 140%; - transform: translate(-50%); - } - .seat-6 { - top: -20%; + top: -22%; left: 70%; transform: translateY(-50%); } - .seat-6 .cards, - .seat-6 .chips { - top: 100%; - left: -30%; - transform: translate(-50%); - } - .seat-7 { top: 25%; left: 87%; transform: translateY(-50%); } - .seat-7 .cards, - .seat-7 .chips { - bottom: -10%; - left: -40%; - transform: translate(-50%); - } - .seat-8 { top: 55%; left: 87%; transform: translateY(-50%); } - .seat-8 .cards, - .seat-8 .chips { - bottom: 60%; - left: -50%; - transform: translate(-50%); - } - .seat-9 { top: 90%; left: 75%; transform: translateY(-50%); } - .seat-9 .cards, - .seat-9 .chips { - top: -40% !important; - left: -40% !important; - transform: translate(-50%); - } - + /* Dealer */ .seat-dealer { top: -5%; left: 45%; transform: translate(0); } + + .cards-center { + top: 50% !important; + left: 50% !important; + } + + /* Cards near the center of the table */ + + .cards-2 { + top: 68%; + left: 30%; + transform: translateX(-50%); + } + + .cards-3 { + top: 55%; + left: 15%; + transform: translateX(-50%); + } + + .cards-4 { + top: 35%; + left: 15%; + transform: translateX(-50%); + } + + .cards-5 { + top: 20%; + left: 30%; + transform: translateX(-50%); + } + + .cards-6 { + top: 20%; + left: 65%; + transform: translateX(-50%); + } + + .cards-7 { + top: 35%; + left: 82%; + transform: translateX(-50%); + } + + .cards-8 { + top: 60%; + left: 82%; + transform: translateX(-50%); + } + + .cards-9 { + top: 70%; + left: 70%; + transform: translateX(-50%); + } + + /* Chips near the center of the table */ + .chips-1 { + top: 60%; + left: 50%; + transform: translateX(-50%); + } + + .chips-2 { + top: 75%; + left: 25%; + transform: translateX(-50%); + } + + .chips-3 { + top: 55%; + left: 10%; + transform: translateX(-50%); + } + + .chips-4 { + top: 35%; + left: 10%; + transform: translateX(-50%); + } + + .chips-5 { + top: 5%; + left: 20%; + transform: translateX(-50%); + } + + .chips-6 { + top: 5%; + left: 65%; + transform: translateX(-50%); + } + + .chips-7 { + top: 35%; + left: 85%; + transform: translateX(-50%); + } + + .chips-8 { + top: 55%; + left: 85%; + transform: translateX(-50%); + } + + .chips-9 { + top: 75%; + left: 65%; + transform: translateX(-50%); + } + + .pot { + top: 65%; + left: 50%; + } } .seat-1 { diff --git a/apps/web/src/utils/audio.ts b/apps/web/src/utils/audio.ts index d694804..9db566c 100644 --- a/apps/web/src/utils/audio.ts +++ b/apps/web/src/utils/audio.ts @@ -5,14 +5,14 @@ interface AudioGroupSettings { } export const getAudioGroupSettings = (): AudioGroupSettings => { - const settings = JSON.parse(localStorage.getItem("audioGroupSettings") || "{}"); + const settings = JSON.parse(localStorage?.getItem("audioGroupSettings") || "{}"); return settings; }; export const setAudioGroupState = (group: AudioGroup, state: boolean): void => { const settings = getAudioGroupSettings(); settings[group] = state; - localStorage.setItem("audioGroupSettings", JSON.stringify(settings)); + localStorage?.setItem("audioGroupSettings", JSON.stringify(settings)); }; export const isAudioGroupAllowed = (group: AudioGroup): boolean => { diff --git a/packages/tailwindcss-config/base-tailwind.config.js b/packages/tailwindcss-config/base-tailwind.config.js index ad3460e..69eb5a5 100644 --- a/packages/tailwindcss-config/base-tailwind.config.js +++ b/packages/tailwindcss-config/base-tailwind.config.js @@ -65,6 +65,28 @@ const config = { transform: "rotateY(0deg)", }, }, + fading: { + "0%": { + opacity: "0", + }, + "10%": { + opacity: "1", + }, + "80%": { + opacity: "1", + }, + "100%": { + opacity: "0", + }, + }, + headShake: { + "0%": { transform: "translateX(0)" }, + "6.5%": { transform: "translateX(-6px) rotateY(-9deg) scale(1.3)" }, + "18.5%": { transform: "translateX(5px) rotateY(7deg) scale(1.3)" }, + "31.5%": { transform: "translateX(-3px) rotateY(-5deg) scale(1.3)" }, + "43.5%": { transform: "translateX(2px) rotateY(3deg) scale(1.3)" }, + "50%": { transform: "translateX(0) scale(1)" }, + }, }, animation: { fadeIn: "fadeIn .5s cubic-bezier(0.165, 0.840, 0.440, 1.000) forwards", @@ -76,6 +98,8 @@ const config = { "slide-in": "slideIn 1s ease-out", "grow-in": "growIn 0.5s ease-out", "flip-y": "flip 1s", + fading: "fading .7s", + headShake: "headShake 1s ease-in-out .5s", }, }, }, diff --git a/packages/ts-sdk/src/BettingManager.ts b/packages/ts-sdk/src/BettingManager.ts index 779f6b9..8361f1e 100644 --- a/packages/ts-sdk/src/BettingManager.ts +++ b/packages/ts-sdk/src/BettingManager.ts @@ -245,7 +245,7 @@ export class BettingManager { return newPot; } - private get raiseAmount() { + public get raiseAmount() { if (!this.active || !this.activeRound) throw new Error("betting round is not defined"); return [BettingRounds.PRE_FLOP, BettingRounds.FLOP].includes(this.activeRound) ? this.tableInfo.smallBlind * 2 diff --git a/packages/ts-sdk/src/Game.ts b/packages/ts-sdk/src/Game.ts index afaa076..da7a0d0 100644 --- a/packages/ts-sdk/src/Game.ts +++ b/packages/ts-sdk/src/Game.ts @@ -128,6 +128,10 @@ export class Game extends EventEmitter { this.elGamalPublicKey = zkDeck.generatePublicKey(this.elGamalSecretKey); } + public getRaiseAmount() { + return this.handState.bettingManager?.raiseAmount; + } + private addOnChainListeners() { this.onChainDataSource.on(OnChainEventTypes.PLAYER_CHECKED_IN, this.newPlayerCheckedIn); this.onChainDataSource.on(OnChainEventTypes.GAME_STARTED, this.gameStarted);