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 (
-
-
+
);
};
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'),
+ },
+ }
});