diff --git a/packages/client/public/assets/chess.jpg b/packages/client/public/assets/chess.jpg new file mode 100644 index 00000000..83b23479 Binary files /dev/null and b/packages/client/public/assets/chess.jpg differ diff --git a/packages/client/public/assets/createRoom.jpg b/packages/client/public/assets/createRoom.jpg new file mode 100644 index 00000000..234be14d Binary files /dev/null and b/packages/client/public/assets/createRoom.jpg differ diff --git a/packages/client/public/assets/gameBar.jpg b/packages/client/public/assets/gameBar.jpg new file mode 100644 index 00000000..45479d8d Binary files /dev/null and b/packages/client/public/assets/gameBar.jpg differ diff --git a/packages/client/public/assets/shop.jpg b/packages/client/public/assets/shop.jpg new file mode 100644 index 00000000..7048b0b2 Binary files /dev/null and b/packages/client/public/assets/shop.jpg differ diff --git a/packages/client/src/App.tsx b/packages/client/src/App.tsx index bff50f85..d0e31b71 100644 --- a/packages/client/src/App.tsx +++ b/packages/client/src/App.tsx @@ -1,3 +1,4 @@ +import { useRef, useState } from "react"; import { useComponentValue } from "@latticexyz/react"; import { useMUD } from "./MUDContext"; import AutoChess from "./ui/ChessMain"; @@ -6,6 +7,8 @@ import "./index.css"; import { SelectNetwork } from "./ui/SelectNetwork"; import Feedback from "./ui/Feedback"; import usePreload from "./hooks/usePreload"; +import { Tour } from "antd"; +import type { TourProps } from "antd"; export const App = () => { const { @@ -16,6 +19,10 @@ export const App = () => { usePreload(); + const ref1 = useRef(null); + const ref2 = useRef(null); + const ref3 = useRef(null); + const playerObj = useComponentValue(PlayerGlobal, playerEntity); const isPlay = playerObj?.status == 1; diff --git a/packages/client/src/hooks/useAutoBattle.ts b/packages/client/src/hooks/useAutoBattle.ts index 5ef97c5d..9b12702d 100644 --- a/packages/client/src/hooks/useAutoBattle.ts +++ b/packages/client/src/hooks/useAutoBattle.ts @@ -1,5 +1,5 @@ import dayjs from "dayjs"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useRef } from "react"; import { useAutoBattleFn } from "./useAutoBattleFn"; export function useAutoBattle() { @@ -7,6 +7,7 @@ export function useAutoBattle() { const [shouldRun, setShouldRun] = useState(false); const [isRunning, setIsRunning] = useState(false); const [runningStart, setRunningStart] = useState(0); + const errorCountRef = useRef(0); useEffect(() => { if (shouldRun) { @@ -21,8 +22,13 @@ export function useAutoBattle() { autoBattleFn() .then(() => { setIsRunning(false); + errorCountRef.current = 0; }) .catch((e) => { + errorCountRef.current++; + if (errorCountRef.current < 3) { + setIsRunning(false); + } setIsRunning(false); console.error(e); }); diff --git a/packages/client/src/hooks/useAutoBattleFn.ts b/packages/client/src/hooks/useAutoBattleFn.ts index 12cd58df..d4502781 100644 --- a/packages/client/src/hooks/useAutoBattleFn.ts +++ b/packages/client/src/hooks/useAutoBattleFn.ts @@ -18,7 +18,7 @@ export function useAutoBattleFn() { } await autoBattle(_playerGlobal.gameId, localAccount); return true; - }, [_playerGlobal, autoBattle, localAccount]); + }, [_playerGlobal?.gameId, autoBattle, localAccount]); return { autoBattleFn }; } diff --git a/packages/client/src/hooks/useChessboard.ts b/packages/client/src/hooks/useChessboard.ts index 963090a3..f9ba8f19 100644 --- a/packages/client/src/hooks/useChessboard.ts +++ b/packages/client/src/hooks/useChessboard.ts @@ -35,6 +35,7 @@ export interface boardInterface { } export interface HeroBaseAttr { + [x: string]: number | string; race: HeroRace; class: HeroClass; attack: number; @@ -84,6 +85,15 @@ const useChessboard = () => { return srcObj.perUrl + heroIdString + ".png"; }; + const creatureMap = new Map( + useEntityQuery([Has(Creature)]) + ?.map((row) => ({ + ...getComponentValueStrict(Creature, row), + key: row, + })) + .map((c) => [Number(c.key), c]) + ); + const BattlePieceList = useMemo(() => { if (PieceInBattleList.length > 0) { const battlePieces: any[] = []; @@ -93,21 +103,20 @@ const useChessboard = () => { const isEnemy = BoardList?.enemyPieces.includes(piece.key); if (isOwner || isEnemy) { - const creature = getComponentValue( - Creature, - piece.creatureId as unknown as Entity - ); + const creature = creatureMap.get(Number(piece.creatureId)); if (!creature) { return; } - const { tier } = decodeHero(creature as unknown as bigint); + const decodeHeroData = decodeHero( + piece.creatureId as unknown as bigint + ); battlePieces.push({ enemy: isEnemy, image: getHeroImg(piece.creatureId), - tier: tier, + ...decodeHeroData, ...creature, ...piece, maxHealth: creature?.health, @@ -118,28 +127,31 @@ const useChessboard = () => { return battlePieces; } return []; - }, [BoardList, PieceInBattleList]); + }, [BoardList, PieceInBattleList, creatureMap]); const PiecesList = playerObj?.heroes.map((row, _index: any) => { - const hero = getComponentValueStrict( - Hero, - encodeEntity({ id: "bytes32" }, { id: numberToHex(row, { size: 32 }) }) - ); - const creature = getComponentValue( - Creature, - encodeCreatureEntity(hero.creatureId) - ); - - const { tier } = decodeHero(hero.creatureId); - return { - ...hero, - ...creature, - key: row, - _index, - tier: tier, - image: getHeroImg(hero.creatureId), - maxHealth: creature?.health, - }; + try { + const hero = getComponentValueStrict( + Hero, + encodeEntity({ id: "bytes32" }, { id: numberToHex(row, { size: 32 }) }) + ); + const creature = getComponentValue( + Creature, + encodeCreatureEntity(hero.creatureId) + ); + + const decodeHeroData = decodeHero(hero.creatureId); + + return { + ...hero, + ...creature, + key: row, + _index, + ...decodeHeroData, + image: getHeroImg(hero.creatureId), + maxHealth: creature?.health, + }; + } catch (error) {} }); const playerListData = currentGame?.players?.map((_player: string) => { @@ -168,6 +180,7 @@ const useChessboard = () => { currentRoundStartTime: currentGame?.startFrom, startFrom: currentGame?.startFrom, currentGameStatus: currentGame?.status, + isSinglePlay: currentGame?.single, playerListData, localAccount, playerObj, diff --git a/packages/client/src/hooks/useHeroAttr.ts b/packages/client/src/hooks/useHeroAttr.ts index 4afafc1a..378658f5 100644 --- a/packages/client/src/hooks/useHeroAttr.ts +++ b/packages/client/src/hooks/useHeroAttr.ts @@ -55,6 +55,7 @@ export function useHeroesAttr(arr: bigint[]): HeroBaseAttr[] { if (creature) { return { + ...item, cost: item.rarity, lv: item.tier, url: srcObj.perUrl + item.heroIdString + ".png", diff --git a/packages/client/src/hooks/useTick.ts b/packages/client/src/hooks/useTick.ts index 66bbc78c..9786c057 100644 --- a/packages/client/src/hooks/useTick.ts +++ b/packages/client/src/hooks/useTick.ts @@ -42,6 +42,7 @@ const useTick = () => { const roundInterval = _GameConfig?.roundInterval; const expUpgrade = _GameConfig?.expUpgrade; const currentRoundStartTime = currentGame?.startFrom; + const finishedBoard = currentGame?.finishedBoard; const { currentBoardStatus } = useBoardStatus(); @@ -78,7 +79,7 @@ const useTick = () => { setTimeLeft(0); setWidth(0); } - }, [currentBoardStatus, roundInterval, currentRoundStartTime]); + }, [currentBoardStatus, roundInterval, currentRoundStartTime, finishedBoard]); useEffect(() => { if ( diff --git a/packages/client/src/index.css b/packages/client/src/index.css index 0c4013c5..e8b0387b 100644 --- a/packages/client/src/index.css +++ b/packages/client/src/index.css @@ -70,7 +70,7 @@ width: 100vw; height: 100vh; overflow: hidden; - background: url(./assets/bg.svg); + background: url(/assets/bg.svg); background-size: 240px; background-repeat: repeat; background-position: top right; diff --git a/packages/client/src/lib/utils.ts b/packages/client/src/lib/utils.ts index 3de6cafc..33ce75cf 100644 --- a/packages/client/src/lib/utils.ts +++ b/packages/client/src/lib/utils.ts @@ -100,8 +100,10 @@ function shortenAddress(address: string) { return ""; } - const firstPart = address.substring(0, 6); - const lastPart = address.substring(address.length - 4); + const checksumAddress =address.length>42?'0x'+address.substring(address.length - 40):address + + const firstPart = checksumAddress.substring(0, 6); + const lastPart = checksumAddress.substring(checksumAddress.length - 4); return `${firstPart}.....${lastPart}`; } diff --git a/packages/client/src/mud/createSystemCalls.ts b/packages/client/src/mud/createSystemCalls.ts index 14ff8bbd..12304e6d 100644 --- a/packages/client/src/mud/createSystemCalls.ts +++ b/packages/client/src/mud/createSystemCalls.ts @@ -58,6 +58,11 @@ export function createSystemCalls( await waitForTransaction(tx); }; + const singlePlay = async () => { + const tx = await worldContract.write.singlePlay(); + await waitForTransaction(tx); + }; + const leaveRoom = async (gameId: `0x${string}`, index: bigint) => { const tx = await worldContract.write.leaveRoom([gameId, index]); await waitForTransaction(tx); @@ -187,6 +192,7 @@ export function createSystemCalls( joinPrivateRoom, leaveRoom, startGame, + singlePlay, surrender, buyRefreshHero, buyHero, diff --git a/packages/client/src/ui/ChessMain.tsx b/packages/client/src/ui/ChessMain.tsx index 0413e35d..f4d39082 100644 --- a/packages/client/src/ui/ChessMain.tsx +++ b/packages/client/src/ui/ChessMain.tsx @@ -11,11 +11,16 @@ import { useDrop } from "ahooks"; import useChessboard, { HeroBaseAttr } from "@/hooks/useChessboard"; import usePreload from "@/hooks/usePreload"; -import { Button, Popconfirm } from "antd"; +import { Button, Popconfirm, Tour } from "antd"; +import type { TourProps } from "antd"; + import { Inventory } from "./Inventory"; import HeroInfo from "./HeroInfo"; import { shallowEqual } from "@/lib/utils"; -import { Synergy } from "./Synergy"; + +import shopPic from "/assets/shop.jpg"; +import gameBarPic from "/assets/gameBar.jpg"; +import chessPic from "/assets/chess.jpg"; export interface boardInterface { creatureId?: any; @@ -44,8 +49,6 @@ const Game = () => { const [acHero, setAcHero] = useState(null); - // const [isDrag, setisDrag] = useState(second) - useEffect(() => { let calculateInterval: any; @@ -103,9 +106,43 @@ const Game = () => { } }; + const ref1 = useRef(null); + const ref2 = useRef(null); + const ref3 = useRef(null); + + const [open, setOpen] = useState(false); + + const steps: TourProps["steps"] = [ + { + title: "Open Shop", + description: "Buy and refresh heroes.", + cover: tour.png, + target: () => ref1.current, + }, + { + title: "Game Status", + description: "", + cover: tour.png, + target: () => ref2.current, + }, + { + title: "Chessboard", + description: "Click for details.", + cover: tour.png, + target: () => ref3.current, + }, + ]; + return (
- + setOpen(false)} steps={steps} /> + +
+ +
+
-
+
-
diff --git a/packages/client/src/ui/Chessboard.css b/packages/client/src/ui/Chessboard.css index e225f728..a06f53d9 100644 --- a/packages/client/src/ui/Chessboard.css +++ b/packages/client/src/ui/Chessboard.css @@ -236,7 +236,7 @@ .hero-info-box { width: 131px; - height: 327px; + /* height: 327px; */ background: #323846; border: 1px solid #2c84a8; border-radius: 9px; diff --git a/packages/client/src/ui/Chessboard.tsx b/packages/client/src/ui/Chessboard.tsx index 3ed53863..41403181 100644 --- a/packages/client/src/ui/Chessboard.tsx +++ b/packages/client/src/ui/Chessboard.tsx @@ -36,8 +36,16 @@ const DragItem = ({ data, children }) => { }; const Chessboard = ({ setAcHeroFn }: { setAcHeroFn: (any) => void }) => { - const { PiecesList, BattlePieceList, placeToBoard, changeHeroCoordinate } = - useChessboard(); + const { + PiecesList, + BattlePieceList, + placeToBoard, + changeHeroCoordinate, + currentBoardStatus = 0, + BoardList, + } = useChessboard(); + + const turn = (BoardList?.turn as number) || 0; const dropRef = useRef(null); @@ -45,6 +53,9 @@ const Chessboard = ({ setAcHeroFn }: { setAcHeroFn: (any) => void }) => { useDrop(dropRef, { onDom: (content: any, e) => { + if (currentBoardStatus !== 0) { + return; + } const index = (e as any).srcElement.dataset.index; const [x, y] = convertToPos(index); @@ -61,11 +72,18 @@ const Chessboard = ({ setAcHeroFn }: { setAcHeroFn: (any) => void }) => { }, onDragEnter: (e) => { + // if (currentBoardStatus !== 0) { + // return; + // } if (!dragIng && !BattlePieceList.length) { setDragIng(true); } }, onDrop: (e) => { + // console.log(currentBoardStatus); + // if (currentBoardStatus !== 0) { + // return; + // } setDragIng(false); }, onDragLeave: (e) => { @@ -94,11 +112,11 @@ const Chessboard = ({ setAcHeroFn }: { setAcHeroFn: (any) => void }) => { }, [PiecesList, BattlePieceList]); const renderSquare = (i) => { - const [x] = convertToPos(i); + const [x, y] = convertToPos(i); const className = dragIng ? x < 4 ? "draging" // left - : "bg-green-200" // right + : "bg-red-600" // right : ""; const percent = @@ -119,7 +137,7 @@ const Chessboard = ({ setAcHeroFn }: { setAcHeroFn: (any) => void }) => { BattlePieceList?.length > 0 ? `HP ${squares[i]?.["health"]}` : null; // `HP ${squares[i]?.["maxHealth"]}`; - const dynamicKey = i + "key" + squares[i]?.["health"]; + const dynamicKey = i + "key" + squares[i]?.["health"] + turn; return (
diff --git a/packages/client/src/ui/GameStatusBar.tsx b/packages/client/src/ui/GameStatusBar.tsx index 87f48700..2e9a450c 100644 --- a/packages/client/src/ui/GameStatusBar.tsx +++ b/packages/client/src/ui/GameStatusBar.tsx @@ -8,7 +8,7 @@ import { useMUD } from "../MUDContext"; dayjs.extend(duration); -function GameStatusBar({ showModal }) { +function GameStatusBar({ showModal, customRef, customRef2 }) { const { systemCalls: { buyExp }, } = useMUD(); @@ -18,7 +18,10 @@ function GameStatusBar({ showModal }) { const time = Math.floor(timeLeft); return ( -
+
-
+
PIECE {playerObj?.heroes?.length}/{(playerObj?.tier as number) + 1} @@ -43,34 +46,38 @@ function GameStatusBar({ showModal }) {
-
showModal()}> +
showModal()} + >
-
+
ROUND {currentGame?.round}
-
+
COIN {playerObj?.coin}
-
+
- - {status} + + {status} {status == "Preparing" && ( {timeLeft >= 0 ? time : null} )} diff --git a/packages/client/src/ui/HeroInfo.tsx b/packages/client/src/ui/HeroInfo.tsx index f686be8c..99d18754 100644 --- a/packages/client/src/ui/HeroInfo.tsx +++ b/packages/client/src/ui/HeroInfo.tsx @@ -1,5 +1,7 @@ import { HeroBaseAttr } from "@/hooks/useChessboard"; import React from "react"; +import { BG_COLOR } from "./Shop"; +import { getClassImage, getRaceImage } from "./Synergy"; interface HeroInfoProps { hero: HeroBaseAttr; @@ -9,7 +11,19 @@ const HeroInfo: React.FC = ({ hero }) => { if (!hero || !hero.health) { return null; } - const { attack, health, defense, lv, tier, range, speed, url, image } = hero; + const { + attack, + health, + defense, + lv, + tier, + range, + speed, + url, + image, + cost, + rarity, + } = hero; return (
@@ -30,6 +44,11 @@ const HeroInfo: React.FC = ({ hero }) => {
Level: + {Number(lv || tier)} +
+ +
+ Cost: {Number(lv) || Number(tier)}
@@ -43,11 +62,25 @@ const HeroInfo: React.FC = ({ hero }) => { {speed}
+
+ {/* show class and race */} +
+ + +
+
+
Hero Image
diff --git a/packages/client/src/ui/Inventory.tsx b/packages/client/src/ui/Inventory.tsx index b49a9393..e71762c2 100644 --- a/packages/client/src/ui/Inventory.tsx +++ b/packages/client/src/ui/Inventory.tsx @@ -4,6 +4,7 @@ import { useComponentValue } from "@latticexyz/react"; import PieceImg from "./Piece"; import { numberArrayToBigIntArray } from "@/lib/utils"; import { HeroBaseAttr } from "@/hooks/useChessboard"; +import { Synergy } from "./Synergy"; // eslint-disable-next-line react/prop-types export function Inventory({ setAcHeroFn }) { @@ -20,7 +21,8 @@ export function Inventory({ setAcHeroFn }) { return (
-
+ +
{heroAttrs?.map((hero: HeroBaseAttr, index: number) => (
setAcHeroFn(hero)}> diff --git a/packages/client/src/ui/JoinGame.tsx b/packages/client/src/ui/JoinGame.tsx index 6e351e8e..533d23d1 100644 --- a/packages/client/src/ui/JoinGame.tsx +++ b/packages/client/src/ui/JoinGame.tsx @@ -1,6 +1,5 @@ "use client"; -import { useState } from "react"; -import { useEffect } from "react"; +import { useEffect, useState, useRef } from "react"; import { useMUD } from "../MUDContext"; import { useComponentValue, useEntityQuery } from "@latticexyz/react"; import { Entity, getComponentValueStrict, Has } from "@latticexyz/recs"; @@ -12,8 +11,9 @@ import { sha256, toUtf8Bytes, } from "ethers/lib/utils"; -import { Input, Button, Table, Modal, message, Tooltip } from "antd"; +import { Input, Button, Table, Modal, message, Tooltip, Tour } from "antd"; import type { ColumnsType } from "antd/es/table"; +import type { TourProps } from "antd"; import { BigNumberish } from "ethers"; import { shortenAddress } from "../lib/utils"; import { Hex, numberToHex, stringToHex, toHex } from "viem"; @@ -21,6 +21,8 @@ import { useSetState } from "react-use"; import Logo from "/assets/logo.png"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; +import Rank from "./Rank"; +import createRoomPic from "/assets/createRoom.jpg"; dayjs.extend(relativeTime); @@ -62,10 +64,17 @@ const JoinGame = (/**{}: JoinGameProps */) => { joinPrivateRoom, leaveRoom, startGame, + singlePlay, }, network: { playerEntity, localAccount }, } = useMUD(); + const ref1 = useRef(null); + const ref2 = useRef(null); + const ref3 = useRef(null); + + const [open, setOpen] = useState(false); + const params = new URLSearchParams(window.location.search); const roomId = params?.get("roomId"); @@ -80,11 +89,13 @@ const JoinGame = (/**{}: JoinGameProps */) => { joinRoom: boolean; leaveRoom: boolean; startGame: boolean; + singlePlay: boolean; }>({ createRoom: false, joinRoom: false, leaveRoom: false, startGame: false, + singlePlay: false, }); const playerObj = useComponentValue(PlayerGlobal, playerEntity); @@ -248,6 +259,25 @@ const JoinGame = (/**{}: JoinGameProps */) => { parseBytes32String(playerObj?.roomId as BytesLike) != "" ); + const steps: TourProps["steps"] = [ + { + title: "Create Room", + description: "How to start playing with friends.", + cover: tour.png, + target: () => ref1.current, + }, + { + title: "Start Single Player", + description: "Start the PVE battle.", + target: () => ref2.current, + }, + { + title: "Leaderboard", + description: "Click for details.", + target: () => ref3.current, + }, + ]; + const columns: ColumnsType = [ { title: "RoomName", @@ -398,6 +428,16 @@ const JoinGame = (/**{}: JoinGameProps */) => { return ( <> {contextHolder} + setOpen(false)} steps={steps} /> +
+ +
+
+ +
+
{/*

@@ -409,14 +449,33 @@ const JoinGame = (/**{}: JoinGameProps */) => {
+
+ +
+
diff --git a/packages/client/src/ui/Playlist.tsx b/packages/client/src/ui/Playlist.tsx index 00976e69..e08f4775 100644 --- a/packages/client/src/ui/Playlist.tsx +++ b/packages/client/src/ui/Playlist.tsx @@ -15,14 +15,24 @@ interface Props { } const PlayerList: React.FC = () => { - const { playerListData, localAccount: currentUserId } = useChessboard(); + const { + playerListData, + localAccount: currentUserId, + isSinglePlay, + } = useChessboard(); + + const isCurrentUserFn = (id: string) => + id.toLocaleLowerCase() === currentUserId.toLocaleLowerCase(); + + const mapList = isSinglePlay + ? playerListData?.filter((player) => isCurrentUserFn(player.id)) + : playerListData; return ( -
+
Players Info
- {playerListData?.map((player) => { - const isCurrentUser = - player.id.toLocaleLowerCase() === currentUserId.toLocaleLowerCase(); + {mapList?.map((player) => { + const isCurrentUser = isCurrentUserFn(player.id); const healthPercentage = (player.hp / player.maxHp) * 100; return (
{ ${player.coin} Lv. {player.level}
-
+
{ + const { + components: { Rank }, + network: { localAccount, playerEntity }, + } = useMUD(); + + const rankList = useEntityQuery([Has(Rank)]) + .map((row) => ({ + ...getComponentValue(Rank, row), + addr: row, + })) + ?.sort((a, b) => ((b.score as number) - a.score) as number); + + const [open, setOpen] = useState(false); + + return ( +
+

setOpen((prev) => !prev)} + > + Leaderboard +

+ +
+ {rankList?.map((user) => ( +
+ + {shortenAddress(user.addr)} + + + {user.score} + + + + {dayjs(Number(user.createdAtBlock) * 1000).fromNow()} + +
+ ))} +
+
+ ); +}; + +export default Leaderboard; diff --git a/packages/client/src/ui/Shop.tsx b/packages/client/src/ui/Shop.tsx index 0ae57296..70168479 100644 --- a/packages/client/src/ui/Shop.tsx +++ b/packages/client/src/ui/Shop.tsx @@ -1,10 +1,10 @@ import React, { useCallback, useState } from "react"; -import { Button, Modal } from "antd"; +import { Button, Modal, message } from "antd"; import { HeroBaseAttr } from "@/hooks/useChessboard"; import { useMUD } from "@/MUDContext"; import { useComponentValue } from "@latticexyz/react"; import { useHeroesAttr } from "@/hooks/useHeroAttr"; -import { numberArrayToBigIntArray } from "@/lib/utils"; +import { decodeHero, numberArrayToBigIntArray } from "@/lib/utils"; import { getClassImage, getRaceImage } from "./Synergy"; type HeroListItem = HeroBaseAttr | null; @@ -16,12 +16,21 @@ interface IShopProps { const SHOW_INFO_LIST = ["health", "attack", "defense", "range"] as const; +export const BG_COLOR = [ + "bg-white", + "bg-green-500", + "bg-blue-500", + "bg-purple-500", + "bg-yellow-500", +]; + const Shop: React.FC = ({ isModalOpen, handleCancel }) => { const { components: { Player }, systemCalls: { buyHero, buyRefreshHero }, network: { playerEntity }, } = useMUD(); + const [messageApi, contextHolder] = message.useMessage(); const [loading, setLoading] = useState(false); @@ -44,98 +53,118 @@ const Shop: React.FC = ({ isModalOpen, handleCancel }) => { numberArrayToBigIntArray(playerValue?.heroAltar) ); + const buyHeroFn = (index: number, hero: HeroBaseAttr) => { + if (Number(hero.cost) + 1 > (playerValue?.coin as number)) { + messageApi.open({ + type: "error", + content: "Not enough coins", + }); + return; + } else { + buyHero(index); + } + }; + return ( -
- -
-
- {heroAttrs?.map( - (hero: HeroListItem, index: number) => - hero && ( -
buyHero(index)} - > -
- {hero?.url} - {/* show class and race */} -
+ <> + {contextHolder} +
+ +
+
+ {heroAttrs?.map( + (hero: HeroListItem, index: number) => + hero && ( +
buyHeroFn(index, hero)} + > +
- -
-
-
- {Array(hero?.["lv"]) - .fill(hero?.["lv"]) - ?.map((item, index) => ( - index - ? "text-yellow-400 mr-[10px]" - : "text-gray-500 mr-[10px]" - } - key={index} - > - ★ - - ))} + src={hero?.url} + alt={hero?.url} + style={{ width: "100%", height: 120 }} + className={`w-[120px] h-[120px] bg-blue-600 rounded-lg opacity-100 ${ + BG_COLOR[Number(hero.rarity || 0)] + }`} + /> +
+ {/* show class and race */} +
+ + +
- - $ {Number(hero?.cost) + 1} - -
- {/* TODO: add icon */} - {SHOW_INFO_LIST.map((attr) => ( -
- {attr} +
+
+ {Array(hero?.["lv"]) + .fill(hero?.["lv"]) + ?.map((item, index) => ( + index + ? "text-yellow-400 mr-[10px]" + : "text-gray-500 mr-[10px]" + } + key={index} + > + ★ + + ))} +
+ - {hero[attr]} + $ {Number(hero?.cost) + 1}
- ))} + {/* TODO: add icon */} + {SHOW_INFO_LIST.map((attr, _index) => ( +
+ {attr} + + {hero[attr]} + +
+ ))} +
-
- ) - )} -
+ ) + )} +
-
- +
+ +
-
- -
+ +
+ ); }; diff --git a/packages/client/src/ui/Synergy.tsx b/packages/client/src/ui/Synergy.tsx index 8d261162..741e7b62 100644 --- a/packages/client/src/ui/Synergy.tsx +++ b/packages/client/src/ui/Synergy.tsx @@ -82,8 +82,8 @@ export function Synergy() { const { raceSynergy, classSynergy } = useSynergy(); return ( -
-
+
+
{Object.keys(raceSynergy).map((r) => { const race = Number(r) as HeroRace; if (race === HeroRace.UNKNOWN) return; diff --git a/packages/contracts/mud.config.ts b/packages/contracts/mud.config.ts index 2c26d013..05b83034 100644 --- a/packages/contracts/mud.config.ts +++ b/packages/contracts/mud.config.ts @@ -31,6 +31,11 @@ export default mudConfig({ openAccess: false, accessList: [], }, + PveBotSystem: { + name: "PveBotSystem", + openAccess: false, + accessList: [], + }, ExperienceSystem: { name: "experience", openAccess: false, @@ -254,6 +259,15 @@ export default mudConfig({ passwordHash: "bytes32", }, }, + Rank: { + keySchema: { + addr: "address", + }, + schema: { + createdAtBlock: "uint32", + score: "uint32", + }, + }, GameRecord: { keySchema: { index: "uint32", @@ -271,6 +285,7 @@ export default mudConfig({ round: "uint32", startFrom: "uint32", // current round start block timestamp finishedBoard: "uint8", + single: "bool", globalRandomNumber: "uint256", players: "address[]", }, diff --git a/packages/contracts/src/library/Utils.sol b/packages/contracts/src/library/Utils.sol index 1583e068..2f18339c 100644 --- a/packages/contracts/src/library/Utils.sol +++ b/packages/contracts/src/library/Utils.sol @@ -8,6 +8,7 @@ import { PlayerGlobal, Player, GameConfig, + ShopConfig, Hero, Piece, Creature, @@ -212,4 +213,8 @@ library Utils { } Game.popPlayers(_gameId); } + + function getBotAddress(address _player) internal returns (address randomAddr) { + randomAddr = address(uint160(uint256(keccak256(abi.encodePacked(_player))))); + } } diff --git a/packages/contracts/src/systems/AutoBattleSystem.sol b/packages/contracts/src/systems/AutoBattleSystem.sol index 8445063d..646a9562 100644 --- a/packages/contracts/src/systems/AutoBattleSystem.sol +++ b/packages/contracts/src/systems/AutoBattleSystem.sol @@ -9,22 +9,29 @@ import {Board, BoardData} from "../codegen/Tables.sol"; import {Hero, HeroData} from "../codegen/Tables.sol"; import {Piece, PieceData} from "../codegen/Tables.sol"; import {GameRecord, Game, GameData} from "../codegen/Tables.sol"; -import {PlayerGlobal, Player} from "../codegen/Tables.sol"; +import {PlayerGlobal, Player, Rank} from "../codegen/Tables.sol"; import {GameStatus, BoardStatus, PlayerStatus} from "../codegen/Types.sol"; +// import {Rank, RankData} from "../codegen/Types.sol"; import {Coordinate as Coord} from "cement/utils/Coordinate.sol"; import {RTPiece} from "../library/RunTimePiece.sol"; import {Utils} from "../library/Utils.sol"; contract AutoBattleSystem is System { function tick(uint32 _gameId, address _player) public { - // the first tick for every board would be initializing pieces from heroes - if (beforeTurn(_gameId, _player)) { - return; - } + bool isSinglePlay = Game.getSingle(_gameId); - (uint8 winner, uint256 damageTaken) = IWorld(_world()).startBattle(_player); + if (isSinglePlay) { + IWorld(_world()).pveTick(_gameId, _player); + } else { + // the first tick for every board would be initializing pieces from heroes + if (beforeTurn(_gameId, _player)) { + return; + } - endTurn(_gameId, _player, winner, damageTaken); + (uint8 winner, uint256 damageTaken) = IWorld(_world()).startBattle(_player); + + endTurn(_gameId, _player, winner, damageTaken); + } } function beforeTurn(uint32 _gameId, address _player) internal returns (bool firstTurn) { @@ -61,6 +68,13 @@ contract AutoBattleSystem is System { } } + // PVE end + // TODO only pvp + function endRoundPublic(uint32 _gameId) public { + _updateWhenRoundEnded(_gameId); + endGame(_gameId); + } + function endRound(uint32 _gameId) private { if (_roundEnded(_gameId)) { _updateWhenRoundEnded(_gameId); @@ -169,8 +183,8 @@ contract AutoBattleSystem is System { } function _updateWhenGameFinished(uint32 _gameId) internal { - // push winner into GameRecord address[] memory players = Game.getPlayers(_gameId); + // push winner into GameRecord uint256 num = players.length; assert(num < 2); if (num == 1) { diff --git a/packages/contracts/src/systems/MatchingSystem.sol b/packages/contracts/src/systems/MatchingSystem.sol index f4251bb9..dda78419 100644 --- a/packages/contracts/src/systems/MatchingSystem.sol +++ b/packages/contracts/src/systems/MatchingSystem.sol @@ -139,6 +139,44 @@ contract MatchingSystem is System { } } + // single + function singlePlay() public { + uint32 gameIndex = GameConfig.getGameIndex(0); + GameConfig.setGameIndex(0, gameIndex + 1); + uint32 roundInterval = GameConfig.getRoundInterval(0); + + address[] memory _players = new address[](2); + _players[0] = _msgSender(); + _players[1] = Utils.getBotAddress(_msgSender()); + + Game.set( + gameIndex, + GameStatus.PREPARING, + 1, // round + uint32(block.timestamp) + roundInterval, // round start timestamp + 0, // finished board + true, + 0, // global random number, initially set it to 0 + _players + ); + + uint24[] memory inventory = new uint24[](GameConfig.getInventorySlotNum(0)); + + address player = _players[0]; + PlayerGlobal.set(player, bytes32(0), gameIndex, PlayerStatus.INGAME); + Player.setHealth(player, 30); + Player.setInventory(player, inventory); + + // set bot + address _bot = _players[1]; + PlayerGlobal.set(_bot, bytes32(0), gameIndex, PlayerStatus.INGAME); + Player.setHealth(_bot, 30); + Player.setInventory(_bot, inventory); + + // init round 0 for each player + IWorld(_world()).settleRound(gameIndex); + } + function _startGame(address[] memory _players) private { uint32 gameIndex = GameConfig.getGameIndex(0); GameConfig.setGameIndex(0, gameIndex + 1); @@ -149,6 +187,7 @@ contract MatchingSystem is System { 1, // round uint32(block.timestamp) + roundInterval, // round start timestamp 0, // finished board + false, // single 0, // global random number, initially set it to 0 _players ); diff --git a/packages/contracts/src/systems/PlaceSystem.sol b/packages/contracts/src/systems/PlaceSystem.sol index 86a46e5c..1bbef8c5 100644 --- a/packages/contracts/src/systems/PlaceSystem.sol +++ b/packages/contracts/src/systems/PlaceSystem.sol @@ -104,21 +104,6 @@ contract PlaceSystem is System { Player.updateInventory(player, toIndex, fromHero); } - function checkCorValidity(address player, uint32 x, uint32 y) public view { - // check x, y validity - require(x < GameConfig.getLength(0), "x too large"); - require(y < GameConfig.getWidth(0), "y too large"); - - // check whether (x,y) is empty - uint256 cor = Coord.compose(x, y); - // loop piece to check whether is occupied - for (uint256 i = 0; i < Player.lengthHeroes(player); i++) { - bytes32 key = Player.getItemHeroes(player, i); - HeroData memory hero = Hero.get(key); - require(cor != Coord.compose(hero.x, hero.y), "this location is not empty"); - } - } - function _checkGamePreparing() internal view { address player = _msgSender(); uint32 gameId = PlayerGlobal.getGameId(player); @@ -129,11 +114,26 @@ contract PlaceSystem is System { function _getHeroIdx(address player) internal returns (bytes32 idx) { uint32 i = Player.getHeroOrderIdx(player); - idx = bytes32(uint256((uint160(player) << 32) + ++i)); + idx = bytes32((uint256(uint160(player)) << 96) + ++i); Player.setHeroOrderIdx(player, i); } + function checkCorValidity(address player, uint32 x, uint32 y) internal view { + // check x, y validity + require(x < GameConfig.getLength(0), "x too large"); + require(y < GameConfig.getWidth(0), "y too large"); + + // check whether (x,y) is empty + uint256 cor = Coord.compose(x, y); + // loop piece to check whether is occupied + for (uint256 i = 0; i < Player.lengthHeroes(player); i++) { + bytes32 key = Player.getItemHeroes(player, i); + HeroData memory hero = Hero.get(key); + require(cor != Coord.compose(hero.x, hero.y), "this location is not empty"); + } + } + modifier onlyWhenGamePreparing() { _checkGamePreparing(); _; diff --git a/packages/contracts/src/systems/PveBotSystem.sol b/packages/contracts/src/systems/PveBotSystem.sol new file mode 100644 index 00000000..73032ce8 --- /dev/null +++ b/packages/contracts/src/systems/PveBotSystem.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.0; + +import "forge-std/Test.sol"; +import {System} from "@latticexyz/world/src/System.sol"; +import {IWorld} from "../codegen/world/IWorld.sol"; +import {Creature, CreatureData, GameConfig, ShopConfig, Rank} from "../codegen/Tables.sol"; +import {Board, BoardData} from "../codegen/Tables.sol"; +import {Hero, HeroData} from "../codegen/Tables.sol"; +import {Piece, PieceData} from "../codegen/Tables.sol"; +import {GameRecord, Game, GameData} from "../codegen/Tables.sol"; +import {PlayerGlobal, Player} from "../codegen/Tables.sol"; +import {GameStatus, BoardStatus, PlayerStatus} from "../codegen/Types.sol"; +import {Coordinate as Coord} from "cement/utils/Coordinate.sol"; +import {RTPiece} from "../library/RunTimePiece.sol"; +import {Utils} from "../library/Utils.sol"; + +contract PveBotSystem is System { + function _getHeroIdx(address player) internal returns (bytes32 idx) { + uint32 i = Player.getHeroOrderIdx(player); + + idx = bytes32((uint256(uint160(player)) << 96) + ++i); + + Player.setHeroOrderIdx(player, i); + } + + // TODO Upgrade piece with round + + function _botSetPiece(uint32 _gameId, address _player) public { + uint32 round = Game.getRound(_gameId); + + if (round % 2 == 1) { + uint256 r = IWorld(_world()).getRandomNumberInGame(_gameId); + + address bot = Utils.getBotAddress(_player); + + bytes32 pieceKey = _getHeroIdx(bot); + + IWorld(_world()).refreshHeroes(bot); + + uint24 creatureId = Player.getItemHeroAltar(bot, r % 5); + r >>= 8; + + uint32 x = uint32(r % 4); + r >>= 8; + + uint32 y = uint32((r / 4) % 8); + + bool hasErr = checkCorValidity(bot, x, y); + + if (hasErr) { + _botSetPiece(_gameId, _player); + } else { + // create piece + Hero.set(pieceKey, creatureId, x, y); + // add piece to player + Player.pushHeroes(bot, pieceKey); + // } + } + } + } + + function checkCorValidity(address player, uint32 x, uint32 y) internal view returns (bool hasErr) { + // check x, y validity + require(x < GameConfig.getLength(0), "x too large"); + require(y < GameConfig.getWidth(0), "y too large"); + + // check whether (x,y) is empty + uint256 cor = Coord.compose(x, y); + // loop piece to check whether is occupied + for (uint256 i = 0; i < Player.lengthHeroes(player); i++) { + bytes32 key = Player.getItemHeroes(player, i); + HeroData memory hero = Hero.get(key); + if (cor != Coord.compose(hero.x, hero.y)) { + hasErr = true; + } + // require(cor != Coord.compose(hero.x, hero.y), "this location is not empty"); + } + + hasErr = false; + } +} diff --git a/packages/contracts/src/systems/PveSystem.sol b/packages/contracts/src/systems/PveSystem.sol new file mode 100644 index 00000000..3f21e5a6 --- /dev/null +++ b/packages/contracts/src/systems/PveSystem.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.0; + +import "forge-std/Test.sol"; +import {System} from "@latticexyz/world/src/System.sol"; +import {IWorld} from "../codegen/world/IWorld.sol"; +import {Creature, CreatureData, GameConfig, ShopConfig, Rank} from "../codegen/Tables.sol"; +import {Board, BoardData} from "../codegen/Tables.sol"; +import {Hero, HeroData} from "../codegen/Tables.sol"; +import {Piece, PieceData} from "../codegen/Tables.sol"; +import {GameRecord, Game, GameData} from "../codegen/Tables.sol"; +import {PlayerGlobal, Player} from "../codegen/Tables.sol"; +import {GameStatus, BoardStatus, PlayerStatus} from "../codegen/Types.sol"; +import {Coordinate as Coord} from "cement/utils/Coordinate.sol"; +import {RTPiece} from "../library/RunTimePiece.sol"; +import {Utils} from "../library/Utils.sol"; + +contract PveSystem is System { + function pveTick(uint32 _gameId, address _player) public { + if (_singleTickInit(_gameId, _player)) { + return; + } + + (uint8 winner, uint256 damageTaken) = IWorld(_world()).startBattle(_player); + + endTurnForSinglePlayer(_gameId, _player, winner, damageTaken); + } + + function _singleTickInit(uint32 _gameId, address _player) private returns (bool firstTurn) { + require(PlayerGlobal.getStatus(_player) == PlayerStatus.INGAME, "not in game"); + require(PlayerGlobal.getGameId(_player) == _gameId, "mismatch game id"); + GameStatus gameStatus = Game.getStatus(_gameId); + console2.log("block.timestamp", uint256(block.timestamp), Game.getStartFrom(_gameId)); + + require(gameStatus != GameStatus.FINISHED, "bad game status"); + + if (gameStatus == GameStatus.PREPARING) { + require(uint256(block.timestamp) >= Game.getStartFrom(_gameId), "preparing time"); + } + BoardStatus boardStatus = Board.getStatus(_player); + + if (boardStatus == BoardStatus.UNINITIATED) { + IWorld(_world())._botSetPiece(_gameId, _player); + _initPieceOnBoardBot(_player); + Game.setStatus(_gameId, GameStatus.INBATTLE); + firstTurn = true; + } + } + + // TODO + function endTurnForSinglePlayer(uint32 _gameId, address _player, uint256 _winner, uint256 _damageTaken) private { + if (_winner == 0) { + Board.setTurn(_player, Board.getTurn(_player) + 1); + } else { + _updateWhenBoardFinished(_gameId, _player, _winner, _damageTaken); + IWorld(_world()).endRoundPublic(_gameId); + } + } + + function _initPieceOnBoardBot(address _player) internal { + address bot = Utils.getBotAddress(_player); + (bytes32[] memory allies, bytes32[] memory enemies) = IWorld(_world()).initPieces(_player, bot); + + Board.set( + _player, + BoardData({enemy: bot, status: BoardStatus.INBATTLE, turn: 0, pieces: allies, enemyPieces: enemies}) + ); + } + + function _updateWhenBoardFinished(uint32 _gameId, address _player, uint256 _winner, uint256 _damageTaken) + internal + { + uint32 turn = Board.getTurn(_player); + + // update board status fix status + Board.setStatus(_player, BoardStatus.UNINITIATED); + + // delete piece in battle + Utils.deleteAllPieces(_player); + + // update player's health and streak + Utils.updatePlayerStreakCount(_player, _winner); + uint256 playerHealth = Utils.updatePlayerHealth(_player, _winner, _damageTaken); + + // clear player if it's defeated, update finishedBoard if else + if (playerHealth == 0) { + uint32 score = Rank.getScore(_player); + + if (turn >= score) { + console.log(turn, score, "score"); + Rank.set(_player, uint32(block.timestamp), turn); + } + Utils.clearPlayer(_gameId, Utils.getBotAddress(_player)); + Utils.clearPlayer(_gameId, _player); + } else { + Game.setFinishedBoard(_gameId, Game.getFinishedBoard(_gameId) + 1); + } + } +} diff --git a/packages/contracts/test/PveTest.t.sol b/packages/contracts/test/PveTest.t.sol new file mode 100644 index 00000000..c6c149c6 --- /dev/null +++ b/packages/contracts/test/PveTest.t.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.0; + +import "forge-std/Test.sol"; +import {MudTest} from "@latticexyz/store/src/MudTest.sol"; +import {Creature, CreatureData, GameConfig, Board, Player, ShopConfig} from "../src/codegen/Tables.sol"; +import {GameRecord, Game, GameData} from "../src/codegen/Tables.sol"; +import {Hero, HeroData} from "../src/codegen/Tables.sol"; +import {Piece, PieceData} from "../src/codegen/Tables.sol"; +import {IWorld} from "../src/codegen/world/IWorld.sol"; +import {GameStatus} from "../src/codegen/Types.sol"; + +contract PveTest is MudTest { + IWorld public world; + + function setUp() public override { + super.setUp(); + world = IWorld(worldAddress); + } + + // function testSinglePlay() public { + // vm.startPrank(address(1)); + // world.singlePlay(); + // vm.stopPrank(); + // } + + function testSingleTick() public { + vm.startPrank(address(1)); + world.singlePlay(); + + for (uint256 index = 0; index < 30; index++) { + vm.warp(block.timestamp + 10 seconds); + world.tick(0, address(1)); + } + + vm.stopPrank(); + } +} diff --git a/packages/contracts/worlds.json b/packages/contracts/worlds.json index bfa6dac5..4a9d870c 100644 --- a/packages/contracts/worlds.json +++ b/packages/contracts/worlds.json @@ -4,7 +4,7 @@ "blockNumber": 23288565 }, "31337": { - "address": "0x4c0bF4C73f2Cf53259C84694b2F26Adc4916921e" + "address": "0xb6fF5715B244c2cDEDEF4211b24199b8EFDFCB98" }, "421613": { "address": "0x0CfAA708Da4599be2cd5eE40304223b7618558f3",