diff --git a/.gitignore b/.gitignore index 398a7d55..5a83c1ba 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules -packages/contracts/src/codegen/* \ No newline at end of file +.env +packages/contracts/src/codegen/ diff --git a/packages/client/abi/Mississippi.json b/packages/client/abi/Mississippi.json new file mode 100644 index 00000000..fc456c04 --- /dev/null +++ b/packages/client/abi/Mississippi.json @@ -0,0 +1,441 @@ +[ + { + "inputs": [ + { + "internalType": "bytes32", + "name": "root", + "type": "bytes32" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "player", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "target", + "type": "address" + } + ], + "name": "AttackStart", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "player", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint16", + "name": "x", + "type": "uint16" + }, + { + "indexed": false, + "internalType": "uint16", + "name": "y", + "type": "uint16" + } + ], + "name": "MoveEvent", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "BattleList", + "outputs": [ + { + "internalType": "address", + "name": "attacker", + "type": "address" + }, + { + "internalType": "address", + "name": "defender", + "type": "address" + }, + { + "internalType": "address", + "name": "winer", + "type": "address" + }, + { + "internalType": "uint16", + "name": "round", + "type": "uint16" + }, + { + "internalType": "uint256", + "name": "attackerHP", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "defenderHP", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "isEnd", + "type": "bool" + }, + { + "internalType": "enum Mississippi_.BattleState", + "name": "attackerState", + "type": "uint8" + }, + { + "internalType": "enum Mississippi_.BattleState", + "name": "defenderState", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "attackerAction", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "defenderAction", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "attackerBuffHash", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "defenderBuffHash", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "attackerArg", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "defenderArg", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + }, + { + "internalType": "uint16", + "name": "", + "type": "uint16" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "MapBoard", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "UserInfo", + "outputs": [ + { + "internalType": "uint256", + "name": "SuitId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "EquipmentId", + "type": "uint256" + }, + { + "internalType": "uint16", + "name": "x", + "type": "uint16" + }, + { + "internalType": "uint16", + "name": "y", + "type": "uint16" + }, + { + "internalType": "enum Mississippi_.UserState", + "name": "state", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "HP", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "UserLocationLock", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "battleId", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_targetAddress", + "type": "address" + }, + { + "components": [ + { + "internalType": "uint16", + "name": "x", + "type": "uint16" + }, + { + "internalType": "uint16", + "name": "y", + "type": "uint16" + }, + { + "internalType": "bytes32[]", + "name": "proof", + "type": "bytes32[]" + } + ], + "internalType": "struct Mississippi_.Move[]", + "name": "moveList", + "type": "tuple[]" + } + ], + "name": "battleInvitation", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "board", + "outputs": [ + { + "internalType": "uint16", + "name": "x", + "type": "uint16" + }, + { + "internalType": "uint16", + "name": "y", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "maxAttackzDistance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "maxMoveDistance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "maxTimeLimit", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "maxUserLocationLockTime", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "merkleRoot", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint16", + "name": "x", + "type": "uint16" + }, + { + "internalType": "uint16", + "name": "y", + "type": "uint16" + }, + { + "internalType": "bytes32[]", + "name": "proof", + "type": "bytes32[]" + } + ], + "internalType": "struct Mississippi_.Move[]", + "name": "moveList", + "type": "tuple[]" + } + ], + "name": "move", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "root", + "type": "bytes32" + } + ], + "name": "setMerkleRoot", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint16", + "name": "x", + "type": "uint16" + }, + { + "internalType": "uint16", + "name": "y", + "type": "uint16" + } + ], + "name": "transfer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/packages/client/package.json b/packages/client/package.json index a5eaa61f..69791cca 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -14,24 +14,33 @@ "@ethersproject/providers": "^5.7.2", "@latticexyz/common": "2.0.0-next.4", "@latticexyz/dev-tools": "2.0.0-next.4", + "@latticexyz/react": "2.0.0-alpha.1.177", "@latticexyz/recs": "2.0.0-next.4", "@latticexyz/schema-type": "2.0.0-next.4", "@latticexyz/services": "2.0.0-next.4", "@latticexyz/store-sync": "2.0.0-next.4", "@latticexyz/utils": "2.0.0-next.4", "@latticexyz/world": "2.0.0-next.4", - "@latticexyz/react": "2.0.0-alpha.1.177", + "@openzeppelin/contracts": "^4.9.3", + "@types/node": "^18.15.11", + "antd": "^5.9.2", + "buffer": "^6.0.3", "contracts": "workspace:*", "ethers": "^5.7.2", - "rxjs": "7.5.5", + "keccak256": "^1.0.6", + "merkletreejs": "^0.3.10", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-router-dom": "^6.16.0", + "rxjs": "7.5.5", "sass": "^1.64.1", "viem": "1.6.0" }, "devDependencies": { + "@nomicfoundation/hardhat-toolbox": "^3.0.0", "@types/react": "^18.0.11", "@types/react-dom": "^18.0.11", + "hardhat": "^2.17.4", "vite": "^4.2.1", "wait-port": "^1.0.4" } diff --git a/packages/client/src/App.tsx b/packages/client/src/App.tsx index 1fea0790..e915b6ec 100644 --- a/packages/client/src/App.tsx +++ b/packages/client/src/App.tsx @@ -1,60 +1,20 @@ -import { loadMapData } from './utils'; -import { useEffect, useState, useRef } from 'react'; -import Map from './components/Map'; -import { MapConfig } from './config'; +import { BrowserRouter, Route, Routes } from 'react-router-dom'; import './App.scss'; +import Home from './pages/home'; +import Game from './pages/game'; export const App = () => { - const [renderMapData, setRenderMapData] = useState([]); - const [vertexCoordinate, setVertexCoordinate] = useState({ - x: 0, - y: 0 - }); - - const mapDataRef = useRef([]); - - const onKeyDown = (e) => { - const mapData = mapDataRef.current; - if (mapData.length === 0 || e.keyCode < 37 || e.keyCode > 40) { - return; - } - switch (e.keyCode) { - case 37: - vertexCoordinate.x = Math.max(0, vertexCoordinate.x - 1); - break; - case 38: - vertexCoordinate.y = Math.max(0, vertexCoordinate.y - 1); - break; - case 39: - vertexCoordinate.x = Math.min(mapData[0].length - 1 - MapConfig.visualWidth, vertexCoordinate.x + 1); - break; - case 40: - vertexCoordinate.y = Math.min(mapData.length - 1 - MapConfig.visualHeight, vertexCoordinate.y + 1); - break; - } - setVertexCoordinate({ - ...vertexCoordinate - }); - } - - useEffect(() => { - loadMapData().then((csv) => { - setRenderMapData(csv); - mapDataRef.current = csv; - }); - - }, []); - return ( -
- +
+ + + + } /> + } /> + + +
) } \ No newline at end of file diff --git a/packages/client/src/assets/avatar/elephant.png b/packages/client/src/assets/avatar/elephant.png new file mode 100644 index 00000000..9c4fdcc7 Binary files /dev/null and b/packages/client/src/assets/avatar/elephant.png differ diff --git a/packages/client/src/assets/avatar/giraffe.png b/packages/client/src/assets/avatar/giraffe.png new file mode 100644 index 00000000..f1802c21 Binary files /dev/null and b/packages/client/src/assets/avatar/giraffe.png differ diff --git a/packages/client/src/assets/avatar/hippo.png b/packages/client/src/assets/avatar/hippo.png new file mode 100644 index 00000000..c72481a2 Binary files /dev/null and b/packages/client/src/assets/avatar/hippo.png differ diff --git a/packages/client/src/assets/avatar/monkey.png b/packages/client/src/assets/avatar/monkey.png new file mode 100644 index 00000000..703632a2 Binary files /dev/null and b/packages/client/src/assets/avatar/monkey.png differ diff --git a/packages/client/src/assets/avatar/panda.png b/packages/client/src/assets/avatar/panda.png new file mode 100644 index 00000000..dca8f479 Binary files /dev/null and b/packages/client/src/assets/avatar/panda.png differ diff --git a/packages/client/src/assets/avatar/parrot.png b/packages/client/src/assets/avatar/parrot.png new file mode 100644 index 00000000..575f9eb3 Binary files /dev/null and b/packages/client/src/assets/avatar/parrot.png differ diff --git a/packages/client/src/assets/avatar/penguin.png b/packages/client/src/assets/avatar/penguin.png new file mode 100644 index 00000000..4f874f20 Binary files /dev/null and b/packages/client/src/assets/avatar/penguin.png differ diff --git a/packages/client/src/assets/avatar/pig.png b/packages/client/src/assets/avatar/pig.png new file mode 100644 index 00000000..95357168 Binary files /dev/null and b/packages/client/src/assets/avatar/pig.png differ diff --git a/packages/client/src/assets/avatar/rabbit.png b/packages/client/src/assets/avatar/rabbit.png new file mode 100644 index 00000000..beeb795a Binary files /dev/null and b/packages/client/src/assets/avatar/rabbit.png differ diff --git a/packages/client/src/assets/avatar/snake.png b/packages/client/src/assets/avatar/snake.png new file mode 100644 index 00000000..938b310f Binary files /dev/null and b/packages/client/src/assets/avatar/snake.png differ diff --git a/packages/client/src/common.scss b/packages/client/src/common.scss index 9e6a4fbb..315ec675 100644 --- a/packages/client/src/common.scss +++ b/packages/client/src/common.scss @@ -1,3 +1,18 @@ +$avatars: 'elephant', 'hippo', 'panda', 'penguin', 'rabbit', 'giraffe', 'monkey', 'parrot', 'pig', 'snake'; +$avatarsPath: './assets/avatar/'; + +@for $i from 1 through 10 { + .avatar-#{nth($avatars, $i)} { + background-image: url("#{$avatarsPath}#{nth($avatars, $i)}.png"); + } +} + +.avatar-box { + background-size: contain; + background-position: center; + background-repeat: no-repeat; +} + * { margin: 0; padding: 0; @@ -5,4 +20,8 @@ body { background: #dfc380; +} + +ul, li, ol { + list-style: none; } \ No newline at end of file diff --git a/packages/client/src/components/AvatarSelector/index.tsx b/packages/client/src/components/AvatarSelector/index.tsx new file mode 100644 index 00000000..61e08882 --- /dev/null +++ b/packages/client/src/components/AvatarSelector/index.tsx @@ -0,0 +1,57 @@ +import React, { useEffect, useState } from 'react'; +import { Button } from 'antd'; +import './styles.scss'; + +const Avatars = ['elephant', 'hippo', 'panda', 'penguin', 'rabbit', 'giraffe', 'monkey', 'parrot', 'pig', 'snake']; + +interface IProps { + onChange: (avatar: string | null) => void; +} + +const AvatarSelector = (props: IProps) => { + + const [avatarsVisible, setAvatarsVisible] = useState(false); + const [avatar, setAvatar] = useState(null); + + useEffect(() => { + props.onChange(avatar); + }, [avatar]) + + const toggleAvatars = () => { + setAvatarsVisible(!avatarsVisible); + } + + return ( +
+ { + avatar ? +
+ : + + } + { + avatarsVisible && ( +
    + { + Avatars.map((avatar) => { + return ( +
  • { + setAvatar(avatar); + setAvatarsVisible(false); + }} + /> + ) + }) + } +
+ ) + } + +
+ ); +}; + +export default AvatarSelector; \ No newline at end of file diff --git a/packages/client/src/components/AvatarSelector/styles.scss b/packages/client/src/components/AvatarSelector/styles.scss new file mode 100644 index 00000000..a0454fce --- /dev/null +++ b/packages/client/src/components/AvatarSelector/styles.scss @@ -0,0 +1,25 @@ + +.mi-c-avatars-wrap { + position: relative; + width: 100px; + height: 100px; + + .avatars { + position: absolute; + top: 100%; + display: grid; + width: 400px; + grid-template-columns: repeat(5, 1fr); + background: #fff; + + .avatar-item { + height: 80px; + } + } + + .avatar-selected { + width: 100px; + height: 100px; + } + +} \ No newline at end of file diff --git a/packages/client/src/components/Map/index.tsx b/packages/client/src/components/Map/index.tsx index c07e964c..7193b0a2 100644 --- a/packages/client/src/components/Map/index.tsx +++ b/packages/client/src/components/Map/index.tsx @@ -1,28 +1,52 @@ -import React, { useEffect, useMemo, useRef } from 'react'; +import React, { useMemo, useRef } from 'react'; import { IPlayer } from '../Player'; -import MapCell from '../MapCell'; +import MapCell, { ICellClassCache, ICoordinate } from '../MapCell'; import './styles.scss'; +import { bfs, simplifyMapData } from '@/utils/map'; +import useMerkel from '@/hooks/useMerkel'; interface IProps { width: number; height: number; players: IPlayer[]; data: number[][]; + curId: number; vertexCoordinate: { x: number, y: number, - } + }; + onPlayerMove: (paths: ICoordinate[], simpleMapData: number[][]) => void; } const Map = (props: IProps) => { - const { width, height, vertexCoordinate, data } = props; + const { width, height, vertexCoordinate, data = [], players, curId, onPlayerMove } = props; const { x: startX, y: startY } = vertexCoordinate; const staticData = useMemo(() => { - return Array(height).fill(0).map(_ => Array(width).fill(0)); + return Array(height).fill(0).map(() => Array(width).fill(0)); }, [width, height]); - const cellClassCache = useRef({}); + const simpleMapData = useMemo(() => { + return simplifyMapData(data); + }, [data]); + + const formatMovePath = useMerkel(simpleMapData); + + const playerData = useMemo(() => { + const obj = {}; + players.forEach((player) => { + obj[`${player.x}-${player.y}`] = player; + }); + return obj; + }, [players]); + + const cellClassCache = useRef({}); + + const onMoveTo = (coordinate) => { + const { x, y} = players.find((player) => player.id === curId); + const paths = bfs(simpleMapData, { x, y }, coordinate).slice(1); + onPlayerMove(paths, formatMovePath(paths)); + } if (data.length === 0) { @@ -34,19 +58,23 @@ const Map = (props: IProps) => {
{ staticData.map((row, rowIndex) => { + const y = startY + rowIndex return ( -
+
{ row.map((_, colIndex) => { + const x = startX + colIndex; return ( ) }) diff --git a/packages/client/src/components/Map/styles.scss b/packages/client/src/components/Map/styles.scss index 65de8c85..d83d4621 100644 --- a/packages/client/src/components/Map/styles.scss +++ b/packages/client/src/components/Map/styles.scss @@ -9,7 +9,6 @@ .mi-map-content { border: 1px solid; width: $cellSize * 24; - font-size: 0; } .mi-map-row { diff --git a/packages/client/src/components/MapCell/index.tsx b/packages/client/src/components/MapCell/index.tsx index cdb5e6ff..16409276 100644 --- a/packages/client/src/components/MapCell/index.tsx +++ b/packages/client/src/components/MapCell/index.tsx @@ -1,7 +1,8 @@ import React from 'react'; import { CellType } from '../../constants'; -import { getCellClass } from '../../utils'; +import { getCellClass, isMovable } from '@/utils'; import './styles.scss'; +import Player, { IPlayer } from '@/components/Player'; interface ITransform { index: number; @@ -13,42 +14,61 @@ interface ICellClass { classList: number[]; } +export interface ICellClassCache { + [k: string]: ICellClass +} + +export interface ICoordinate { + x: number; + y: number; +} + interface IProps { - coordinate: { - x: number; - y: number; - }, + coordinate: ICoordinate, mapData: number[][]; - cellClassCache: { - [k: string]: ICellClass - }; + cellClassCache: ICellClassCache; + player?: IPlayer; + onMoveTo: (ICoordinate) => void; } const MapCell = (props: IProps) => { - const { coordinate: { x, y}, mapData, cellClassCache } = props; + const { coordinate: { x, y}, mapData, cellClassCache, player, onMoveTo } = props; if (!cellClassCache[`${y}-${x}`]) { cellClassCache[`${y}-${x}`] = getCellClass(mapData, { x, y}); } - const { transforms, classList } = cellClassCache[`${y}-${x}`] + const { transforms, classList } = cellClassCache[`${y}-${x}`]; + + const onContextMenu = (e) => { + e.preventDefault(); + const curMapDataType = mapData[y][x]; + if (isMovable(curMapDataType) && !player) { + onMoveTo({ x, y}); + } + } return ( -
+
+
+ { + classList.map((item, index) => { + const transformStyle = transforms.find((item) => item.index === index); + const style = transformStyle ? { + transform: transformStyle.transform + } : {}; + return ( +
+ ) + }) + } +
{ - classList.map((item, index) => { - const transformStyle = transforms.find((item) => item.index === index); - const style = transformStyle ? { - transform: transformStyle.transform - } : {}; - return ( -
- ) - }) + player && }
); diff --git a/packages/client/src/components/MapCell/styles.scss b/packages/client/src/components/MapCell/styles.scss index 02921c15..f4b7a5b2 100644 --- a/packages/client/src/components/MapCell/styles.scss +++ b/packages/client/src/components/MapCell/styles.scss @@ -1,10 +1,12 @@ - .mi-map-cell { - flex-wrap: wrap; - font-size: 0; - display: grid; - grid-template-columns: 1fr 1fr 1fr; - grid-template-rows: 1fr 1fr 1fr; + position: relative; + + .cell-map-box { + display: grid; + height: 100%; + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: 1fr 1fr 1fr; + } @for $i from 1 through 25 { .mi-wall-#{$i} { diff --git a/packages/client/src/components/Player/index.tsx b/packages/client/src/components/Player/index.tsx index df8038af..d1bfa875 100644 --- a/packages/client/src/components/Player/index.tsx +++ b/packages/client/src/components/Player/index.tsx @@ -1,15 +1,18 @@ import React from 'react'; +import './styles.scss'; export interface IPlayer { x: number; y: number; - + id: number; + username: string; } const Player = (props: IPlayer) => { return ( -
- +
+
{props.username}
+
); }; diff --git a/packages/client/src/components/Player/styles.scss b/packages/client/src/components/Player/styles.scss new file mode 100644 index 00000000..b0bab3b3 --- /dev/null +++ b/packages/client/src/components/Player/styles.scss @@ -0,0 +1,13 @@ +.mi-player { + position: absolute; + top: 0; + left: 0; + height: 100%; + display: flex; + flex-direction: column; + + .player-body { + flex: 1; + //background: url(""); + } +} \ No newline at end of file diff --git a/packages/client/src/components/Rank/index.tsx b/packages/client/src/components/Rank/index.tsx new file mode 100644 index 00000000..b8624545 --- /dev/null +++ b/packages/client/src/components/Rank/index.tsx @@ -0,0 +1,53 @@ +import React, { useState } from 'react'; +import './styles.scss'; + +interface IUser { + id: string | number; + name: string; + score: number; +} + +interface IProps { + data: IUser[]; + curId: number; +} + +const Rank = (props: IProps) => { + + const { data, curId } = props; + const curIndex = data.findIndex(item => item.id === curId); + const [visible, setVisible] = useState(false); + + const toggleVisible = () => { + setVisible(!visible); + } + + return ( +
+
Rank
+
    + { + data.map((item, index) => { + return ( +
  • +
    {index + 1}
    +
    {item.name}
    +
    {item.score}
    +
  • + ) + }) + } +
+
+
{curIndex + 1}
+
ME
+
{data[curIndex].score}
+
+
+
+
+
+ ); +}; + +export default Rank; \ No newline at end of file diff --git a/packages/client/src/components/Rank/styles.scss b/packages/client/src/components/Rank/styles.scss new file mode 100644 index 00000000..7333eec6 --- /dev/null +++ b/packages/client/src/components/Rank/styles.scss @@ -0,0 +1,77 @@ +.mi-c-rank { + position: absolute; + left: 0; + padding: 16px; + width: 160px; + height: 500px; + background: #eee; + top: 50%; + transform: translate3d(0, -50%, 0); + transition: transform 0.3s; + + .rank-title { + border-bottom: 2px solid; + line-height: 2; + text-align: center; + } + + .rank-list { + padding: 8px 0; + } + + .my-rank-info { + border-top: 2px solid; + padding-top: 8px; + } + + .rank-index { + width: 30px; + } + + .name { + flex: 1; + } + + .score { + width: 50px; + } + + .rank-info { + display: flex; + + & > div { + padding: 0 6px; + } + } + + .opt { + position: absolute; + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 60px; + left: 100%; + top: 50%; + transform: translateY(-50%); + background: #eee; + cursor: pointer; + + .toggle-visible{ + content: ''; + display: block; + border-width: 28px 16px 28px 0; + border-color: transparent blue transparent transparent; + border-style: solid; + } + } + + &.hidden { + transform: translate3d(-100%, -50%, 0); + + .opt .toggle-visible { + border-width: 28px 0 28px 16px; + border-color: transparent transparent transparent blue; + } + } +} \ No newline at end of file diff --git a/packages/client/src/components/UserAvatar/index.tsx b/packages/client/src/components/UserAvatar/index.tsx new file mode 100644 index 00000000..5c1b92e0 --- /dev/null +++ b/packages/client/src/components/UserAvatar/index.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import './styles.scss'; + +interface IProps { + avatar: string; + username: string; + hp: number; + maxHp: number; + ap: number; + maxAp: number; + roomId: string; +} + +const UserAvatar = (props: IProps) => { + + const { avatar, username, hp, maxHp, ap, maxAp, roomId } = props; + + return ( +
+
+
{username}
+
+
+
+
+
+
+
Room ID: {roomId}
+
+ ); +}; + +export default UserAvatar; \ No newline at end of file diff --git a/packages/client/src/components/UserAvatar/styles.scss b/packages/client/src/components/UserAvatar/styles.scss new file mode 100644 index 00000000..bebfab9d --- /dev/null +++ b/packages/client/src/components/UserAvatar/styles.scss @@ -0,0 +1,35 @@ +.mi-c-user-avatar { + position: relative; + padding-left: 80px; + width: 160px; + height: 80px; + + .avatar-box { + position: absolute; + left: 0; + top: 0; + width: 80px; + height: 80px; + border: 2px solid #80c6df; + border-radius: 10px; + background-color: #fff; + box-sizing: border-box; + } + + .hp { + background: red; + } + + .ap { + background: yellow; + } + + .hp-wrapper, .ap-wrapper { + //height: 20px; + border: 2px solid; + + & > div { + height: 16px; + } + } +} \ No newline at end of file diff --git a/packages/client/src/hooks/useMerkel.tsx b/packages/client/src/hooks/useMerkel.tsx new file mode 100644 index 00000000..6806fffa --- /dev/null +++ b/packages/client/src/hooks/useMerkel.tsx @@ -0,0 +1,66 @@ +import React, { useEffect, useRef } from 'react'; +import { MerkleTree } from 'merkletreejs'; +import { solidityKeccak256, keccak256 } from 'ethers/lib/utils'; +import { Buffer } from 'buffer'; + + +const useMerkel = (mapData) => { + + const convertToLeafs = (mapData) => { + const result = []; + for (let y = 0; y < mapData.length; y++) { + for (let x = 0; x < mapData[y].length; x++) { + result.push({ x, y, value: mapData[y][x] }); + } + } + return result; + } + + const leafs = useRef([]); + + const merkel = useRef(null); + + const getProof = (x, y) => { + const leaf = generateLeaf(x, y, 1); + return merkel.current!.getHexProof(leaf); + } + + // 通过本函数将地图初始化为默克尔树节点的字符串数组,每个字符串的格式为"x,y-value" +// 其中value为0或1,表示该位置是否可以走,默认0不可走,1以及以后数字可以约定分别代表不同的可行性 + const generateLeaf = (x, y, value) => { + + return Buffer.from( + solidityKeccak256( + ["uint16", "string", "uint16", "string", "uint8"], + [x, ",", y, ",", value] + ).slice(2), + "hex" + ); + } + + const formatMovePath = (paths) => { + const steps = paths.map(({ x, y }) => [x, y]); + const result = []; + for (let i = 0; i < steps.length; i++) { + const proof = getProof(steps[i][0], steps[i][1]); + result.push([steps[i][0], steps[i][1], proof]); + } + return result; + } + + useEffect(() => { + if (mapData.length === 0) { + return; + } + leafs.current = convertToLeafs(mapData); + merkel.current = new MerkleTree( + leafs.current.map((info) => generateLeaf(info.x, info.y, info.value)), + keccak256, + { sortPairs: true } + ); + }, [mapData]); + + return formatMovePath; +} + +export default useMerkel; \ No newline at end of file diff --git a/packages/client/src/mock/data.ts b/packages/client/src/mock/data.ts new file mode 100644 index 00000000..1b209ede --- /dev/null +++ b/packages/client/src/mock/data.ts @@ -0,0 +1,42 @@ +import { IPlayer } from '@/components/Player'; + +export const RankMockData = [ + { + name: 'aaaa', + score: 100, + id: 1 + }, + { + name: 'aaaa1', + score: 99, + id: 2 + }, + { + name: 'aaaa2', + score: 50, + id: 3 + }, + { + name: 'aaaa3', + score: 5, + id: 4 + }, +]; + + +export const PlayersMockData: IPlayer[] = [ + { + id: 3, + username: 'Me', + x: 4, + y: 4, + }, + { + id: 1, + username: 'other', + x: 18, + y: 10, + } +]; + +export const CurIdMockData = 3; \ No newline at end of file diff --git a/packages/client/src/pages/game/index.tsx b/packages/client/src/pages/game/index.tsx new file mode 100644 index 00000000..25142480 --- /dev/null +++ b/packages/client/src/pages/game/index.tsx @@ -0,0 +1,107 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { MapConfig } from '@/config'; +import { loadMapData } from '@/utils'; +import Map from '@/components/Map'; +import UserAvatar from '@/components/UserAvatar'; +import { useLocation } from 'react-router-dom'; +import './styles.scss'; +import Rank from '@/components/Rank'; +import { CurIdMockData, PlayersMockData, RankMockData } from '@/mock/data'; +import { IPlayer } from '@/components/Player'; +import { uploadUserMove } from '@/service/user'; + +const Game = () => { + const [renderMapData, setRenderMapData] = useState([]); + const [vertexCoordinate, setVertexCoordinate] = useState({ + x: 0, + y: 0 + }); + + const [curPlayer, setCurPlayer] = useState(null); + const [players, setPlayers] = useState(PlayersMockData); + + const mapDataRef = useRef([]); + const location = useLocation(); + const { username = '', avatar = 'snake', roomId = '000000' } = location.state ?? {}; + + const onKeyDown = (e) => { + const mapData = mapDataRef.current; + if (mapData.length === 0 || e.keyCode < 37 || e.keyCode > 40) { + return; + } + switch (e.keyCode) { + case 37: + vertexCoordinate.x = Math.max(0, vertexCoordinate.x - 1); + break; + case 38: + vertexCoordinate.y = Math.max(0, vertexCoordinate.y - 1); + break; + case 39: + vertexCoordinate.x = Math.min(mapData[0].length - 1 - MapConfig.visualWidth, vertexCoordinate.x + 1); + break; + case 40: + vertexCoordinate.y = Math.min(mapData.length - 1 - MapConfig.visualHeight, vertexCoordinate.y + 1); + break; + } + setVertexCoordinate({ + ...vertexCoordinate + }); + }; + + const movePlayer = (paths, merkelData) => { + let pathIndex = 0; + const curPlayerIndex = players.findIndex(item => item.id === curPlayer!.id); + const interval = setInterval(() => { + Object.assign(players[curPlayerIndex], paths[pathIndex]); + pathIndex++; + setPlayers([...players]); + if (pathIndex === paths.length) { + clearInterval(interval); + } + }, 300); + uploadUserMove(merkelData); + } + + useEffect(() => { + loadMapData().then((csv) => { + setRenderMapData(csv); + mapDataRef.current = csv; + }); + + const player = players.find((item) => item.id === CurIdMockData); + setCurPlayer(player as IPlayer); + + }, []); + + return ( +
+
+ +
+ + + +
+ ) +}; + +export default Game; \ No newline at end of file diff --git a/packages/client/src/pages/game/styles.scss b/packages/client/src/pages/game/styles.scss new file mode 100644 index 00000000..2267a4b9 --- /dev/null +++ b/packages/client/src/pages/game/styles.scss @@ -0,0 +1,13 @@ +.mi-game { + position: relative; + display: flex; + height: 100vh; + align-items: center; + justify-content: center; + + .mi-game-user-avatar { + position: absolute; + left: 6px; + top: 6px; + } +} \ No newline at end of file diff --git a/packages/client/src/pages/home/index.tsx b/packages/client/src/pages/home/index.tsx new file mode 100644 index 00000000..5537edb3 --- /dev/null +++ b/packages/client/src/pages/home/index.tsx @@ -0,0 +1,76 @@ +import React, { useState } from 'react'; +import { Col, Row, Button, Input, message } from 'antd'; +import './styles.scss'; +import { useNavigate } from 'react-router-dom'; +import AvatarSelector from '@/components/AvatarSelector'; +import { connect } from '@/service/connection'; + +const Home = () => { + + const [roomId, setRoomId] = useState(''); + const [username, setUsername] = useState(''); + const [avatar, setAvatar] = useState(null); + const navigate = useNavigate(); + + const join = () => { + if (!setRoomId) { + message.error('Please input the room id'); + return; + } + if (!username) { + message.error('Please input your username'); + return; + } + if (!avatar) { + message.error('Please select an avatar'); + return; + } + navigate('/game', { + state: { + avatar, + roomId, + username + } + }); + } + + + return ( +
+
+

MSSP

+ + + Season time: xxxx Min + + + + + + + + setRoomId(e.target.value)}/> + + + + + + + + setUsername(e.target.value)}/> + + + setAvatar(value)}/> + + + + + + + +
+
+ ); +}; + +export default Home; \ No newline at end of file diff --git a/packages/client/src/pages/home/styles.scss b/packages/client/src/pages/home/styles.scss new file mode 100644 index 00000000..c8da06b2 --- /dev/null +++ b/packages/client/src/pages/home/styles.scss @@ -0,0 +1,26 @@ +.mi-home-page { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + + + .home-content { + padding: 20px; + width: 800px; + height: 600px; + border: 2px solid; + + .ant-row { + margin-bottom: 30px; + } + + .ant-col { + padding: 0 20px; + } + } + + h1 { + text-align: center; + } +} \ No newline at end of file diff --git a/packages/client/src/service/connection.ts b/packages/client/src/service/connection.ts new file mode 100644 index 00000000..7e64ad5a --- /dev/null +++ b/packages/client/src/service/connection.ts @@ -0,0 +1,14 @@ +import { ethers } from 'ethers'; + +export const connect = async () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const provider = new ethers.providers.Web3Provider(window.ethereum) + + await provider.send("eth_requestAccounts", []); + + const signer = provider.getSigner(); + + const addr = await signer.getAddress(); + console.log('connected', addr); +} \ No newline at end of file diff --git a/packages/client/src/service/user.ts b/packages/client/src/service/user.ts new file mode 100644 index 00000000..d96f0fec --- /dev/null +++ b/packages/client/src/service/user.ts @@ -0,0 +1,19 @@ +import { ethers } from 'ethers'; +import Mississippi from '../../abi/Mississippi.json'; + +export const uploadUserMove = async (steps) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const provider = new ethers.providers.Web3Provider(window.ethereum) + + await provider.send("eth_requestAccounts", []); + const signer = provider.getSigner(); + + const miss = new ethers.Contract('0xc86c785620e9d9a14374ea203b34b6312bce6d03', Mississippi, signer); + + const transaction = await miss.connect(signer).move(steps); + const tx = await transaction.wait(); + + console.log(tx.events) + +} \ No newline at end of file diff --git a/packages/client/src/utils/index.ts b/packages/client/src/utils/index.ts index a1b0e097..cd6829f0 100644 --- a/packages/client/src/utils/index.ts +++ b/packages/client/src/utils/index.ts @@ -1 +1 @@ -export { cutMapData, loadMapData, getCellClass } from './map' \ No newline at end of file +export { cutMapData, loadMapData, getCellClass, isMovable } from './map' \ No newline at end of file diff --git a/packages/client/src/utils/map.ts b/packages/client/src/utils/map.ts index 209e4d8f..6f6482e8 100644 --- a/packages/client/src/utils/map.ts +++ b/packages/client/src/utils/map.ts @@ -1,4 +1,6 @@ -import { CellType } from '../constants'; +import { CellType } from '@/constants'; +import { ICoordinate } from '@/components/MapCell'; +import { IPlayer } from '@/components/Player'; export const cutMapData = (mapData, startCoordinate, endCoordinate) => { const { x: startX, y: startY} = startCoordinate; @@ -232,3 +234,47 @@ export const getCellClass = (data, coordinate) => { classList: [...wallIndexArr.slice(6), ...wallIndexArr.slice(3, 6),...wallIndexArr.slice(0, 3)] } } + + +export const isMovable = (type) => { + return type === CellType.movable; +} + +export const bfs = (mapData: number[][], from: ICoordinate, to: ICoordinate) => { + const data = mapData.map((row) => [...row]); + data[from.y][from.x] = 0; + + let paths = [[from]]; + const dirs = [[-1, 0], [1, 0], [0, -1], [0, 1]]; + + do { + const newPaths = []; + const hasFind = paths.some((path) => { + const last = path[path.length - 1]; + return dirs.some((dir) => { + const [dX, dY] = dir; + const nextX = last.x + dX; + const nextY = last.y + dY; + + if (data[nextY][nextX] === 1) { + newPaths.push([...path, { x: nextX, y: nextY}]); + data[nextY][nextX] = 0; + } + return nextX === to.x && nextY === to.y; + }); + }); + if (hasFind) { + return newPaths[newPaths.length - 1] + } + paths = newPaths; + } while (paths.length !== 0); + + return []; +}; + +export const simplifyMapData = (mapData: number[][]) => { + if (mapData.length === 0) { + return mapData; + } + return mapData.map((row) => row.map(type => isMovable(type) ? 1 : 0)); +} \ No newline at end of file diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json index d8bb624b..e3762749 100644 --- a/packages/client/tsconfig.json +++ b/packages/client/tsconfig.json @@ -12,7 +12,11 @@ "esModuleInterop": true, "noEmit": true, "skipLibCheck": true, - "jsx": "react-jsx" + "jsx": "react-jsx", + "baseUrl": ".", + "paths": { + "@/*": ["src/*"], + } }, "include": ["src"] } diff --git a/packages/client/vite.config.ts b/packages/client/vite.config.ts index 4e4d5a06..f54701c6 100644 --- a/packages/client/vite.config.ts +++ b/packages/client/vite.config.ts @@ -1,4 +1,5 @@ import { defineConfig } from "vite"; +import path from 'path'; export default defineConfig({ server: { @@ -12,4 +13,10 @@ export default defineConfig({ minify: true, sourcemap: true, }, + // base: './', + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + } });