diff --git a/client/src/api/indexer.js b/client/src/api/indexer.js index 182f0c3..ebab1cd 100644 --- a/client/src/api/indexer.js +++ b/client/src/api/indexer.js @@ -517,4 +517,41 @@ export async function getTournamentScores(tournament_id) { } catch (ex) { console.log(ex) } -} \ No newline at end of file +} + +export async function getTokenMetadata(game_id) { + const document = gql` + { + ${NS_SHORT}TokenMetadataModels(where:{token_id:"${game_id}"}) { + edges { + node { + token_id + player_name + settings_id + lifecycle { + start { + Some + } + end { + Some + } + } + } + } + } + }` + + const res = await request(GQL_ENDPOINT, document); + const metadata = res?.[`${NS_SHORT}TokenMetadataModels`]?.edges[0]?.node; + + if (!metadata) return null; + + return { + id: parseInt(metadata.token_id, 16), + playerName: hexToAscii(metadata.player_name), + settingsId: parseInt(metadata.settings_id, 16), + expires_at: parseInt(metadata.lifecycle.end.Some || 0, 16) * 1000, + available_at: parseInt(metadata.lifecycle.start.Some || 0, 16) * 1000 + }; +} + diff --git a/client/src/components/battle/death.jsx b/client/src/components/battle/death.jsx index 6f47e64..18ac325 100644 --- a/client/src/components/battle/death.jsx +++ b/client/src/components/battle/death.jsx @@ -1,9 +1,9 @@ import { Box, Button, Typography } from "@mui/material"; +import { motion, useAnimationControls } from "framer-motion"; import React, { useContext, useEffect, useState } from "react"; -import { GameContext } from "../../contexts/gameContext"; +import { isBrowser, isMobile } from 'react-device-detect'; import { BattleContext } from "../../contexts/battleContext"; -import { motion, useAnimationControls } from "framer-motion"; -import { isMobile, isBrowser } from 'react-device-detect' +import { GameContext } from "../../contexts/gameContext"; function DeathDialog(props) { const game = useContext(GameContext) @@ -11,7 +11,7 @@ function DeathDialog(props) { const controls = useAnimationControls() const [text, showText] = useState(false) - const [tweetMsg] = useState(`I fell to the darkness in Dark Shuffle after slaying ${game.values.monstersSlain} beasts, with a final score of ${game.score}. Can you outlast me? Play now at darkshuffle.io 🕷️⚔️ @provablegames @darkshuffle_gg`); + const [tweetMsg] = useState(`I fell to the darkness in Dark Shuffle after slaying ${game.values.monstersSlain} beasts, with a final score of ${game.score}. Want to see how I did it? Watch my replay here: darkshuffle.io/replay/${game.id} 🕷️⚔️ @provablegames @darkshuffle_gg`); useEffect(() => { startAnimation() diff --git a/client/src/components/header.jsx b/client/src/components/header.jsx index 59ffd3b..561ffa4 100644 --- a/client/src/components/header.jsx +++ b/client/src/components/header.jsx @@ -5,9 +5,11 @@ import { LoadingButton } from '@mui/lab'; import { Box, Button, IconButton, Typography } from '@mui/material'; import { useConnect } from "@starknet-react/core"; import React, { useContext, useState } from 'react'; -import { Link } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import logo from '../assets/images/logo.svg'; +import { BattleContext } from '../contexts/battleContext'; import { DojoContext } from '../contexts/dojoContext'; +import { GameContext } from '../contexts/gameContext'; import { ellipseAddress } from '../helpers/utilities'; import ChooseName from './dialogs/chooseName'; import ConnectWallet from './dialogs/connectWallet'; @@ -23,6 +25,10 @@ const menuItems = [ ] function Header(props) { + const game = useContext(GameContext) + const battle = useContext(BattleContext) + const navigate = useNavigate() + const { connect, connector, connectors } = useConnect(); let cartridgeConnector = connectors.find(conn => conn.id === "controller") @@ -42,11 +48,17 @@ function Header(props) { setAnchorEl(null); }; + const backToMenu = () => { + navigate('/') + battle.utils.resetBattleState() + game.endGame() + } + return ( - + diff --git a/client/src/components/landing/leaderboard.jsx b/client/src/components/landing/leaderboard.jsx index 907ef93..06acfdc 100644 --- a/client/src/components/landing/leaderboard.jsx +++ b/client/src/components/landing/leaderboard.jsx @@ -1,3 +1,4 @@ +import CheckIcon from '@mui/icons-material/Check'; import TheatersIcon from '@mui/icons-material/Theaters'; import VisibilityIcon from '@mui/icons-material/Visibility'; import { Box, IconButton, Pagination, Tab, Tabs, Typography } from '@mui/material'; @@ -9,7 +10,6 @@ import { getActiveLeaderboard, getLeaderboard, getTournamentRegistrations, popul import { useReplay } from '../../contexts/replayContext'; import { useTournament } from "../../contexts/tournamentContext"; import { formatNumber } from '../../helpers/utilities'; -import CheckIcon from '@mui/icons-material/Check'; function Leaderboard() { const tournamentProvider = useTournament() @@ -65,6 +65,7 @@ function Leaderboard() { }, [page, tab, registrations]) const seasonPool = Math.floor(season.rewardPool / 1e18 * 0.98) + const cashPrizes = [99, 60, 39, 30, 21, 15, 12, 9, 9, 6] return ( @@ -89,7 +90,7 @@ function Leaderboard() { # - + Player @@ -98,7 +99,7 @@ function Leaderboard() { {tab === 'one' ? 'Score' : 'XP'} - + {loading && } @@ -124,7 +125,7 @@ function Leaderboard() { {rank} - + {game.playerName} @@ -132,13 +133,19 @@ function Leaderboard() { {game.xp} - + {tab === 'one' && rank <= season.distribution?.length && <> - + + {formatNumber(seasonPool * season.distribution[rank - 1] / 100)} + {season.leaderboard[i] === game.tokenId && } + + {!isMobile && cashPrizes[i] && + +${cashPrizes[i]} + } } diff --git a/client/src/contexts/replayContext.jsx b/client/src/contexts/replayContext.jsx index d8e1a99..6da642c 100644 --- a/client/src/contexts/replayContext.jsx +++ b/client/src/contexts/replayContext.jsx @@ -72,7 +72,8 @@ export const ReplayProvider = ({ children }) => { fetchEvents(1, txs[1].tx_hash) setTxHashes(txs.map(tx => tx.tx_hash)) } else { - enqueueSnackbar('Failed to load replay', { variant: 'error', anchorOrigin: { vertical: 'bottom', horizontal: 'right' } }) + endReplay() + enqueueSnackbar('Failed to load replay', { variant: 'error', anchorOrigin: { vertical: 'top', horizontal: 'center' } }) } } diff --git a/client/src/helpers/routes.jsx b/client/src/helpers/routes.jsx index df909ef..109b11f 100644 --- a/client/src/helpers/routes.jsx +++ b/client/src/helpers/routes.jsx @@ -6,6 +6,14 @@ export const routes = [ path: '/', content: }, + { + path: '/replay/:replayGameId', + content: + }, + { + path: '/spectate/:spectateGameId', + content: + }, { path: '/library', content: diff --git a/client/src/pages/ArenaPage.jsx b/client/src/pages/ArenaPage.jsx index 16b0c13..d8e6eb2 100644 --- a/client/src/pages/ArenaPage.jsx +++ b/client/src/pages/ArenaPage.jsx @@ -1,18 +1,48 @@ -import React, { useContext } from 'react' +import { useSnackbar } from 'notistack' +import React, { useContext, useEffect } from 'react' import { Scrollbars } from 'react-custom-scrollbars' +import { useParams } from 'react-router-dom' +import { getTokenMetadata } from '../api/indexer' import StartDraft from '../components/landing/startDraft' import BattleContainer from '../container/BattleContainer' import DraftContainer from '../container/DraftContainer' import StartBattleContainer from '../container/StartBattleContainer' import { GameContext } from '../contexts/gameContext' +import { useReplay } from '../contexts/replayContext' function ArenaPage() { const gameState = useContext(GameContext) - const { gameId, state, replay } = gameState.values + const { state } = gameState.values + + const replay = useReplay() + const { replayGameId, spectateGameId } = useParams() + const { enqueueSnackbar } = useSnackbar() + + useEffect(() => { + async function fetchGame() { + let game = await getTokenMetadata(replayGameId || spectateGameId) + + if (game) { + if (replayGameId) { + replay.startReplay(game) + } + + if (spectateGameId) { + replay.spectateGame(game) + } + } else { + enqueueSnackbar('Game not found', { variant: 'error', anchorOrigin: { vertical: 'top', horizontal: 'center' } }) + } + } + + if (replayGameId || spectateGameId) { + fetchGame() + } + }, [replayGameId, spectateGameId]) return ( - - {gameId === null && } + + {gameState.values.gameId === null && } {state === 'Draft' && }