├── game
├── .env
├── public
│ ├── favicon.ico
│ ├── assets
│ │ ├── images
│ │ │ ├── bg.jpeg
│ │ │ ├── vs.png
│ │ │ └── btn_overlay.png
│ │ └── fonts
│ │ │ └── HarmonyOS_Sans_SC_Regular.ttf
│ └── index.html
├── babel.config.js
├── src
│ ├── assets
│ │ ├── logo.png
│ │ └── twitter-logo.svg
│ ├── main.js
│ ├── components
│ │ ├── Fighting.vue
│ │ ├── LoadingIndicator.vue
│ │ ├── SelectCharacter.vue
│ │ └── Arena.vue
│ ├── index.css
│ ├── App.vue
│ └── store
│ │ └── index.js
├── postcss.config.js
├── tailwind.config.js
├── .gitignore
├── README.md
└── package.json
├── .gitignore
├── hardhat.config.js
├── package.json
├── scripts
└── deploy.js
├── README.md
├── test
└── test.js
└── contracts
├── libraries
└── Base64.sol
└── EpicGame.sol
/game/.env:
--------------------------------------------------------------------------------
1 | CONTRACT_ADDRESS="0xeE45c5A2C4a44bDD17e7C5Ba73ee47F63a9244d8"
--------------------------------------------------------------------------------
/game/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuintionTang/vue-game-dapp/HEAD/game/public/favicon.ico
--------------------------------------------------------------------------------
/game/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/cli-plugin-babel/preset'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/game/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuintionTang/vue-game-dapp/HEAD/game/src/assets/logo.png
--------------------------------------------------------------------------------
/game/public/assets/images/bg.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuintionTang/vue-game-dapp/HEAD/game/public/assets/images/bg.jpeg
--------------------------------------------------------------------------------
/game/public/assets/images/vs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuintionTang/vue-game-dapp/HEAD/game/public/assets/images/vs.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | game/node_modules
3 | package-lock.json
4 | cache
5 | artifacts
6 | yarn.lock
7 | .DS_Store
8 | yarn-error.log
9 |
--------------------------------------------------------------------------------
/game/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/game/public/assets/images/btn_overlay.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuintionTang/vue-game-dapp/HEAD/game/public/assets/images/btn_overlay.png
--------------------------------------------------------------------------------
/game/public/assets/fonts/HarmonyOS_Sans_SC_Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QuintionTang/vue-game-dapp/HEAD/game/public/assets/fonts/HarmonyOS_Sans_SC_Regular.ttf
--------------------------------------------------------------------------------
/game/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import App from "./App.vue";
3 |
4 | import store from "./store";
5 | import "./index.css";
6 |
7 | Vue.config.productionTip = false;
8 |
9 | new Vue({
10 | store,
11 | render: (h) => h(App),
12 | }).$mount("#app");
13 |
--------------------------------------------------------------------------------
/game/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | purge: ["./index.html", "./src/**/*.vue,js,ts,jsx,tsx}"],
3 | darkMode: false, // or 'media' or 'class'
4 | theme: {
5 | extend: {},
6 | },
7 | variants: {
8 | extend: {},
9 | },
10 | plugins: [],
11 | };
12 |
--------------------------------------------------------------------------------
/game/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 | cache
5 | artifacts
6 | package-lock.json
7 |
8 | # local env files
9 | .env.local
10 | .env.*.local
11 |
12 | # Log files
13 | npm-debug.log*
14 | yarn-debug.log*
15 | yarn-error.log*
16 | pnpm-debug.log*
17 |
18 | # Editor directories and files
19 | .idea
20 | .vscode
21 | *.suo
22 | *.ntvs*
23 | *.njsproj
24 | *.sln
25 | *.sw?
26 |
--------------------------------------------------------------------------------
/game/README.md:
--------------------------------------------------------------------------------
1 | # game
2 |
3 | ## Project setup
4 | ```
5 | yarn install
6 | ```
7 |
8 | ### Compiles and hot-reloads for development
9 | ```
10 | yarn serve
11 | ```
12 |
13 | ### Compiles and minifies for production
14 | ```
15 | yarn build
16 | ```
17 |
18 | ### Lints and fixes files
19 | ```
20 | yarn lint
21 | ```
22 |
23 | ### Customize configuration
24 | See [Configuration Reference](https://cli.vuejs.org/config/).
25 |
--------------------------------------------------------------------------------
/hardhat.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @type import('hardhat/config').HardhatUserConfig
3 | */
4 | require("@nomiclabs/hardhat-waffle");
5 |
6 | const config = {
7 | alchemy: "9aa3d95b3bc440fa88ea12eaa4456161", // 测试网络token
8 | privateKey: "57517bb18fa510a7f80315c63122", // 钱包私钥
9 | };
10 |
11 | module.exports = {
12 | solidity: "0.8.4",
13 | networks: {
14 | goerli: {
15 | url: `https://goerli.infura.io/v3/${config.alchemy}`,
16 | accounts: [config.privateKey],
17 | chainId: 5,
18 | },
19 | sepolia: {
20 | url: `https://sepolia.infura.io/v3/${config.alchemy}`,
21 | accounts: [config.privateKey],
22 | chainId: 11155111,
23 | },
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-game-dapp",
3 | "version": "1.0.0",
4 | "description": "使用 Solidity、Web3 和 Vue.js 创建区块链游戏",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": [
10 | "web3.0"
11 | ],
12 | "author": "QuintionTang@gmail.com",
13 | "license": "MIT",
14 | "devDependencies": {
15 | "@nomiclabs/hardhat-ethers": "^2.0.5",
16 | "@nomiclabs/hardhat-waffle": "^2.0.2",
17 | "chai": "^4.3.6",
18 | "ethereum-waffle": "^3.4.0",
19 | "ethers": "^5.5.4",
20 | "hardhat": "^2.13.0"
21 | },
22 | "dependencies": {
23 | "@openzeppelin/contracts": "^4.8.2"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/game/src/components/Fighting.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |

8 |
9 |
10 |
11 |
12 |
15 |
16 |
34 |
--------------------------------------------------------------------------------
/game/src/components/LoadingIndicator.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |

8 |
9 |
10 |
11 |
12 |
15 |
16 |
34 |
--------------------------------------------------------------------------------
/game/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | 三英战吕布:使用 Solidity、Web3 和 Vue.js 创建区块链游戏
12 |
13 |
14 |
15 |
16 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/game/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "game",
3 | "description": "使用 Solidity、Web3 和 Vue.js 创建区块链游戏:三英战吕布 ",
4 | "version": "0.1.0",
5 | "private": true,
6 | "scripts": {
7 | "serve": "vue-cli-service serve",
8 | "build": "vue-cli-service build",
9 | "lint": "vue-cli-service lint"
10 | },
11 | "dependencies": {
12 | "core-js": "^3.6.5",
13 | "ethers": "^5.5.4",
14 | "vue": "^2.6.11",
15 | "vue-page-title": "^2.1.1",
16 | "vuex": "^3.3.0"
17 | },
18 | "devDependencies": {
19 | "@vue/cli-plugin-babel": "~4.5.0",
20 | "@vue/cli-plugin-vuex": "~4.5.0",
21 | "@vue/cli-service": "~4.5.0",
22 | "autoprefixer": "^9.8.8",
23 | "postcss": "^7.0.39",
24 | "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.17",
25 | "vue-template-compiler": "^2.6.11"
26 | },
27 | "eslintConfig": {
28 | "root": true,
29 | "env": {
30 | "node": true
31 | },
32 | "extends": [
33 | "plugin:vue/essential",
34 | "eslint:recommended"
35 | ],
36 | "parserOptions": {
37 | "parser": "babel-eslint"
38 | },
39 | "rules": {}
40 | },
41 | "browserslist": [
42 | "> 1%",
43 | "last 2 versions",
44 | "not dead"
45 | ]
46 | }
47 |
--------------------------------------------------------------------------------
/scripts/deploy.js:
--------------------------------------------------------------------------------
1 | const main = async () => {
2 | const gameContractFactory = await hre.ethers.getContractFactory("EpicGame");
3 | const gameContract = await gameContractFactory.deploy(
4 | ["刘备", "关羽", "张飞"],
5 | [
6 | "https://resources.crayon.dev/suangguosha/liubei.png",
7 | "https://resources.crayon.dev/suangguosha/guanyu.png",
8 | "https://resources.crayon.dev/suangguosha/zhangfei.png",
9 | ],
10 | [100, 200, 300],
11 | [100, 50, 25],
12 | "吕布",
13 | "https://resources.crayon.dev/suangguosha/lvbu.png", // boss
14 | 1000,
15 | 50
16 | );
17 | const [deployer] = await ethers.getSigners();
18 |
19 | console.log("Deploying contracts with the account: ", deployer.address);
20 |
21 | console.log("Account balance: ", (await deployer.getBalance()).toString());
22 | await gameContract.deployed();
23 | console.log("Contract deployed to: ", gameContract.address);
24 | };
25 | const runMain = async () => {
26 | try {
27 | await main();
28 | process.exit(0);
29 | } catch (error) {
30 | console.log(error);
31 | process.exit(1);
32 | }
33 | };
34 | runMain();
35 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vue-game-dapp
2 |
3 | 使用 Solidity、Web3 和 Vue.js 创建区块链游戏
4 |
5 | ### 开始
6 |
7 | 进入文件 `hardhat.config.js` ,增加一个钱包私钥和网络 Token。
8 |
9 | 安装依赖:
10 |
11 | ```
12 | npm install
13 | ```
14 |
15 | 然后测试合约
16 |
17 | ```
18 | npx hardhat test
19 | ```
20 |
21 | ### 合约部署
22 |
23 | #### 部署到 Goerli 测试网络
24 |
25 | 发布部署合约,下面的指令为部署到 ETH 测试网络 Goerli
26 |
27 | ```
28 | npx hardhat run scripts/deploy.js --network goerli
29 | ```
30 |
31 | #### 部署到 Sepolia 测试网络
32 |
33 | 发布部署合约,下面的指令为部署到 ETH 测试网络 Sepolia
34 |
35 | ```
36 | npx hardhat run scripts/deploy.js --network sepolia
37 | ```
38 |
39 | #### 水龙头
40 |
41 | - 测试网络 Goerli:https://goerlifaucet.com/, 每个账号每天可以获取 `0.1ETH`,由于水龙头紧张,现在领取的钱包地址必须主网有用一点额度
42 | - 测试网络 Sepolia:https://sepoliafaucet.com/ ,每个账号每天可以获取 `0.5ETH`
43 |
44 | ### 前端运行
45 |
46 | 进入前端目录:
47 |
48 | ```
49 | cd game
50 | ```
51 |
52 | 安装依赖
53 |
54 | ```
55 | yarn install
56 | ```
57 |
58 | 设置合约地址
59 |
60 | 编辑文件 `.env`,把部署的合约地址填入。
61 |
62 | ```
63 | CONTRACT_ADDRESS="0xe"
64 | ```
65 |
66 | 启动前端
67 |
68 | ```
69 | yarn serve
70 | ```
71 |
72 | 即可正常启动。
73 |
74 | ### 体验
75 |
76 | 
77 |
78 | **体验地址:**[https://web3-game.crayon.dev/](https://web3-game.crayon.dev/)
79 |
--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------
1 | const { expect } = require("chai");
2 | const { ethers } = require("hardhat");
3 | describe("EpicGame", function () {
4 | let gameContract;
5 | before(async () => {
6 | const gameContractFactory = await ethers.getContractFactory("EpicGame");
7 | gameContract = await gameContractFactory.deploy(
8 | ["刘备", "关羽", "张飞"],
9 | [
10 | "https://resources.crayon.dev/suangguosha/liubei.png",
11 | "https://resources.crayon.dev/suangguosha/guanyu.png",
12 | "https://resources.crayon.dev/suangguosha/zhangfei.png",
13 | ],
14 | [100, 200, 300],
15 | [100, 50, 25],
16 | "吕布",
17 | "https://resources.crayon.dev/suangguosha/lvbu.png", // boss
18 | 1000,
19 | 50
20 | );
21 | await gameContract.deployed();
22 | });
23 | it("Should have 3 default characters", async () => {
24 | let characters = await gameContract.getAllDefaultCharacters();
25 | expect(characters.length).to.equal(3);
26 | });
27 | it("Should have a boss", async () => {
28 | let boss = await gameContract.getBigBoss();
29 | expect(boss.name).to.equal("吕布");
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/game/src/components/SelectCharacter.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
铸造英雄,重温历史
4 |
5 |
11 |
12 |
{{ character.name }}
13 |
14 |
![]()
15 |
18 |
19 |
20 |
26 |
27 |
28 |
29 |
60 |
--------------------------------------------------------------------------------
/contracts/libraries/Base64.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | pragma solidity ^0.8.0;
4 |
5 | /// [MIT License]
6 | /// @title Base64
7 | /// @notice Provides a function for encoding some bytes in base64
8 | /// @author Brecht Devos
9 | library Base64 {
10 | bytes internal constant TABLE =
11 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
12 |
13 | /// @notice Encodes some bytes to the base64 representation
14 | function encode(bytes memory data) internal pure returns (string memory) {
15 | uint256 len = data.length;
16 | if (len == 0) return "";
17 |
18 | // multiply by 4/3 rounded up
19 | uint256 encodedLen = 4 * ((len + 2) / 3);
20 |
21 | // Add some extra buffer at the end
22 | bytes memory result = new bytes(encodedLen + 32);
23 |
24 | bytes memory table = TABLE;
25 |
26 | assembly {
27 | let tablePtr := add(table, 1)
28 | let resultPtr := add(result, 32)
29 |
30 | for {
31 | let i := 0
32 | } lt(i, len) {
33 |
34 | } {
35 | i := add(i, 3)
36 | let input := and(mload(add(data, i)), 0xffffff)
37 |
38 | let out := mload(add(tablePtr, and(shr(18, input), 0x3F)))
39 | out := shl(8, out)
40 | out := add(
41 | out,
42 | and(mload(add(tablePtr, and(shr(12, input), 0x3F))), 0xFF)
43 | )
44 | out := shl(8, out)
45 | out := add(
46 | out,
47 | and(mload(add(tablePtr, and(shr(6, input), 0x3F))), 0xFF)
48 | )
49 | out := shl(8, out)
50 | out := add(
51 | out,
52 | and(mload(add(tablePtr, and(input, 0x3F))), 0xFF)
53 | )
54 | out := shl(224, out)
55 |
56 | mstore(resultPtr, out)
57 |
58 | resultPtr := add(resultPtr, 4)
59 | }
60 |
61 | switch mod(len, 3)
62 | case 1 {
63 | mstore(sub(resultPtr, 2), shl(240, 0x3d3d))
64 | }
65 | case 2 {
66 | mstore(sub(resultPtr, 1), shl(248, 0x3d))
67 | }
68 |
69 | mstore(result, encodedLen)
70 | }
71 |
72 | return string(result);
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/game/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @font-face {
6 | font-style: normal;
7 | font-family: "Harmony_Regular";
8 | src: url("/assets/fonts/HarmonyOS_Sans_SC_Regular.ttf") format("truetype");
9 | font-weight: normal;
10 | font-style: normal;
11 | }
12 | body {
13 | font-family: "Harmony_Regular", "pingfang sc", "Microsoft YaHei", "微软雅黑",
14 | "宋体", Arial, Helvetica, sans-serif;
15 | background: radial-gradient(circle, #4a009a 0%, #080223 100%);
16 | margin: 0px;
17 | padding: 0px;
18 | font-size: 14px;
19 | }
20 | html {
21 | background: radial-gradient(circle, #4a009a 0%, #080223 100%);
22 | }
23 | .header-container {
24 | padding-top: 30px;
25 | margin: 0;
26 | font-size: 50px;
27 | font-weight: 700;
28 | color: #fff;
29 | }
30 |
31 | .header {
32 | margin: 0;
33 | font-size: 50px;
34 | font-weight: 700;
35 | color: #fff;
36 | }
37 | .sub-text {
38 | font-size: 16px;
39 | padding: 20px 50px;
40 | color: #fff;
41 | }
42 |
43 | .select-character-container {
44 | width: 100%;
45 | height: 100%;
46 | display: flex;
47 | font-size: 18px;
48 | flex-direction: column;
49 | align-items: center;
50 | color: #fff;
51 | }
52 | .footer-container {
53 | margin-top: 30px;
54 | }
55 | .mt-5 {
56 | margin-bottom: 50px;
57 | }
58 | .select-character-container .character-grid {
59 | width: 100%;
60 | display: grid;
61 | grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
62 | grid-template-rows: repeat(auto-fit, minmax(300px, 1fr));
63 | grid-row-gap: 15px;
64 | }
65 | .character-grid .character-item {
66 | display: flex;
67 | flex-direction: column;
68 |
69 | position: relative;
70 | justify-self: center;
71 | align-self: center;
72 | background: radial-gradient(circle, #4a009a 0%, #080223 100%);
73 | box-shadow: 0px 1px 13px 0px rgb(12 11 15 / 32%);
74 | will-change: transform;
75 | transform-style: preserve-3d;
76 | transform: perspective(300px) rotateX(0deg) rotateY(0deg);
77 | }
78 | .character-grid .character-item:hover {
79 | will-change: transform;
80 | transform: perspective(300px) rotateX(0deg) rotateY(0deg);
81 | margin: 0;
82 | }
83 | .character-item .name-container {
84 | display: none;
85 | position: absolute;
86 | background-color: #838383;
87 | border-radius: 5px;
88 | margin: 10px;
89 | }
90 | .character-item .name-container p {
91 | margin: 0;
92 | padding: 5px 10px 5px 10px;
93 | font-weight: 700;
94 | }
95 | .character-item img {
96 | height: 300px;
97 | width: 350px;
98 | border-radius: 10px;
99 | -o-object-fit: cover;
100 | object-fit: cover;
101 | }
102 | .character-item .character-mint-button {
103 | position: absolute;
104 | bottom: 0;
105 | width: 100%;
106 | height: 40px;
107 | border: none;
108 | cursor: pointer;
109 | background-color: rgba(0, 0, 0, 0.5);
110 | color: #fff;
111 | font-weight: 700;
112 | font-size: 16px;
113 | }
114 | .github-container {
115 | position: absolute;
116 | width: 100px;
117 | height: 40px;
118 | padding-top: 10px;
119 | right: 10px;
120 | top: 0px;
121 | text-align: right;
122 | z-index: 10;
123 | }
124 | .github-container a {
125 | display: inline-flex;
126 | padding: 5px 10px;
127 | color: #24292f;
128 | background-color: #ebf0f4;
129 | border: 1px sold rgba(31, 35, 40, 0.15);
130 | border-radius: 5px;
131 | background-image: -moz-linear-gradient(top, #f6f8fa, #ebf0f4 90%);
132 | background-image: linear-gradient(180deg, #f6f8fa, #ebf0f4 90%);
133 | font-size: 12px;
134 | }
135 | .github-container a:hover {
136 | background-color: #e9ebef;
137 | background-position: 0 -0.5em;
138 | border-color: #cbcdd1;
139 | border-color: rgba(31, 35, 40, 0.15);
140 | background-image: -moz-linear-gradient(top, #f3f4f6, #e9ebef 90%);
141 | background-image: linear-gradient(180deg, #f3f4f6, #e9ebef 90%);
142 | }
143 | .github-container a svg,
144 | .github-container a span {
145 | float: left;
146 | font-weight: bold;
147 | }
148 | .github-container a svg {
149 | margin: 2px 5px 2px 0px;
150 | }
151 | @media (min-width: 1536px) {
152 | .container {
153 | max-width: 1280px;
154 | }
155 | }
156 | @media (min-width: 1280px) {
157 | .container {
158 | max-width: 1140px;
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/game/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
24 |
38 |
39 |
58 |
59 |
60 |
61 |
98 |
99 |
259 |
--------------------------------------------------------------------------------
/contracts/EpicGame.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.4.22 <0.9.0;
3 |
4 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
5 | import "@openzeppelin/contracts/utils/Counters.sol";
6 | import "@openzeppelin/contracts/utils/Strings.sol";
7 |
8 | import "./libraries/Base64.sol";
9 |
10 | import "hardhat/console.sol";
11 |
12 | contract EpicGame is ERC721 {
13 | struct CharacterAttributes {
14 | uint256 characterIndex;
15 | string name;
16 | string imageURI;
17 | uint256 hp;
18 | uint256 maxHp;
19 | uint256 attackDamage;
20 | }
21 |
22 | struct BigBoss {
23 | string name;
24 | string imageURI;
25 | uint256 hp;
26 | uint256 maxHp;
27 | uint256 attackDamage;
28 | }
29 |
30 | BigBoss public bigBoss;
31 |
32 | using Counters for Counters.Counter;
33 | Counters.Counter private _tokenIds;
34 |
35 | CharacterAttributes[] defaultCharacters;
36 |
37 | mapping(uint256 => CharacterAttributes) public nftHolderAttributes;
38 |
39 | mapping(address => uint256) public nftHolders;
40 |
41 | event CharacterNFTMinted(
42 | address sender,
43 | uint256 tokenId,
44 | uint256 characterIndex
45 | );
46 | event AttackComplete(uint256 newBossHp, uint256 newPlayerHp);
47 |
48 | constructor(
49 | string[] memory characterNames,
50 | string[] memory characterImageURIs,
51 | uint256[] memory characterHp,
52 | uint256[] memory characterAttackDmg,
53 | string memory bossName,
54 | string memory bossImageURI,
55 | uint256 bossHp,
56 | uint256 bossAttackDamage
57 | ) ERC721("Heroes", "HERO") {
58 | for (uint256 i = 0; i < characterNames.length; i += 1) {
59 | defaultCharacters.push(
60 | CharacterAttributes({
61 | characterIndex: i,
62 | name: characterNames[i],
63 | imageURI: characterImageURIs[i],
64 | hp: characterHp[i],
65 | maxHp: characterHp[i],
66 | attackDamage: characterAttackDmg[i]
67 | })
68 | );
69 |
70 | CharacterAttributes memory c = defaultCharacters[i];
71 | console.log(
72 | "Done initializing %s w/ HP %s, img %s",
73 | c.name,
74 | c.hp,
75 | c.imageURI
76 | );
77 | }
78 |
79 | bigBoss = BigBoss({
80 | name: bossName,
81 | imageURI: bossImageURI,
82 | hp: bossHp,
83 | maxHp: bossHp,
84 | attackDamage: bossAttackDamage
85 | });
86 |
87 | console.log(
88 | "Done initializing boss %s w/ HP %s, img %s",
89 | bigBoss.name,
90 | bigBoss.hp,
91 | bigBoss.imageURI
92 | );
93 |
94 | _tokenIds.increment();
95 | }
96 |
97 | function mintCharacterNFT(uint256 _characterIndex) external {
98 | uint256 newItemId = _tokenIds.current();
99 |
100 | _safeMint(msg.sender, newItemId);
101 |
102 | nftHolderAttributes[newItemId] = CharacterAttributes({
103 | characterIndex: _characterIndex,
104 | name: defaultCharacters[_characterIndex].name,
105 | imageURI: defaultCharacters[_characterIndex].imageURI,
106 | hp: defaultCharacters[_characterIndex].hp,
107 | maxHp: defaultCharacters[_characterIndex].hp,
108 | attackDamage: defaultCharacters[_characterIndex].attackDamage
109 | });
110 |
111 | console.log(
112 | "Minted NFT w/ tokenId %s and characterIndex %s",
113 | newItemId,
114 | _characterIndex
115 | );
116 |
117 | nftHolders[msg.sender] = newItemId;
118 |
119 | _tokenIds.increment();
120 | emit CharacterNFTMinted(msg.sender, newItemId, _characterIndex);
121 | }
122 |
123 | function attackBoss() public {
124 | uint256 nftTokenIdOfPlayer = nftHolders[msg.sender];
125 | CharacterAttributes storage player = nftHolderAttributes[
126 | nftTokenIdOfPlayer
127 | ];
128 | console.log(
129 | "\nPlayer w/ character %s about to attack. Has %s HP and %s AD",
130 | player.name,
131 | player.hp,
132 | player.attackDamage
133 | );
134 | console.log(
135 | "Boss %s has %s HP and %s AD",
136 | bigBoss.name,
137 | bigBoss.hp,
138 | bigBoss.attackDamage
139 | );
140 |
141 | require(player.hp > 0, "Error: character must have HP to attack boss.");
142 | require(bigBoss.hp > 0, "Error: boss must have HP to attack boss.");
143 |
144 | if (bigBoss.hp < player.attackDamage) {
145 | bigBoss.hp = 0;
146 | } else {
147 | bigBoss.hp = bigBoss.hp - player.attackDamage;
148 | }
149 |
150 | if (player.hp < bigBoss.attackDamage) {
151 | player.hp = 0;
152 | } else {
153 | player.hp = player.hp - bigBoss.attackDamage;
154 | }
155 |
156 | console.log("Boss attacked player. New player hp: %s\n", player.hp);
157 | emit AttackComplete(bigBoss.hp, player.hp);
158 | }
159 |
160 | function checkIfUserHasNFT()
161 | public
162 | view
163 | returns (CharacterAttributes memory)
164 | {
165 | uint256 userNftTokenId = nftHolders[msg.sender];
166 | if (userNftTokenId > 0) {
167 | return nftHolderAttributes[userNftTokenId];
168 | } else {
169 | CharacterAttributes memory emptyStruct;
170 | return emptyStruct;
171 | }
172 | }
173 |
174 | function getAllDefaultCharacters()
175 | public
176 | view
177 | returns (CharacterAttributes[] memory)
178 | {
179 | return defaultCharacters;
180 | }
181 |
182 | function getBigBoss() public view returns (BigBoss memory) {
183 | return bigBoss;
184 | }
185 |
186 | function tokenURI(uint256 _tokenId)
187 | public
188 | view
189 | override
190 | returns (string memory)
191 | {
192 | CharacterAttributes memory charAttributes = nftHolderAttributes[
193 | _tokenId
194 | ];
195 |
196 | string memory strHp = Strings.toString(charAttributes.hp);
197 | string memory strMaxHp = Strings.toString(charAttributes.maxHp);
198 | string memory strAttackDamage = Strings.toString(
199 | charAttributes.attackDamage
200 | );
201 |
202 | string memory json = Base64.encode(
203 | bytes(
204 | string(
205 | abi.encodePacked(
206 | '{"name": "',
207 | charAttributes.name,
208 | " -- NFT #: ",
209 | Strings.toString(_tokenId),
210 | '", "description": "This is an NFT that lets people play in the game Metaverse Slayer!", "image": "',
211 | charAttributes.imageURI,
212 | '", "attributes": [ { "trait_type": "Health Points", "value": ',
213 | strHp,
214 | ', "max_value":',
215 | strMaxHp,
216 | '}, { "trait_type": "Attack Damage", "value": ',
217 | strAttackDamage,
218 | "} ]}"
219 | )
220 | )
221 | )
222 | );
223 |
224 | string memory output = string(
225 | abi.encodePacked("data:application/json;base64,", json)
226 | );
227 |
228 | return output;
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/game/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import Vuex from "vuex";
3 | import { ethers } from "ethers";
4 | import EpicGame from "../utils/EpicGame.json";
5 |
6 | Vue.use(Vuex);
7 |
8 | const transformCharacterData = (characterData) => {
9 | return {
10 | name: characterData.name,
11 | imageURI: characterData.imageURI,
12 | hp: characterData.hp.toNumber(),
13 | maxHp: characterData.maxHp.toNumber(),
14 | attackDamage: characterData.attackDamage.toNumber(),
15 | };
16 | };
17 | export default new Vuex.Store({
18 | state: {
19 | account: null,
20 | error: null,
21 | mining: false,
22 | characterNFT: null,
23 | characters: [],
24 | boss: null,
25 | attackState: null,
26 | contract_address: "0xeE45c5A2C4a44bDD17e7C5Ba73ee47F63a9244d8", // process.env.CONTRACT_ADDRESS, // 合约地址
27 | },
28 | getters: {
29 | account: (state) => state.account,
30 | error: (state) => state.error,
31 | mining: (state) => state.mining,
32 | characterNFT: (state) => state.characterNFT,
33 | characters: (state) => state.characters,
34 | boss: (state) => state.boss,
35 | attackState: (state) => state.attackState,
36 | },
37 | mutations: {
38 | setAccount(state, account) {
39 | state.account = account;
40 | },
41 | setError(state, error) {
42 | state.error = error;
43 | },
44 | setMining(state, mining) {
45 | state.mining = mining;
46 | },
47 | setCharacterNFT(state, characterNFT) {
48 | state.characterNFT = characterNFT;
49 | },
50 | setCharacters(state, characters) {
51 | state.characters = characters;
52 | },
53 | setBoss(state, boss) {
54 | state.boss = boss;
55 | },
56 | setAttackState(state, attackState) {
57 | state.attackState = attackState;
58 | },
59 | },
60 | actions: {
61 | async connect({ commit, dispatch }, connect) {
62 | try {
63 | const { ethereum } = window;
64 | if (!ethereum) {
65 | // 验证是否安装 Metamask
66 | commit("setError", "Metamask not installed!");
67 | return;
68 | }
69 | if (!(await dispatch("checkIfConnected")) && connect) {
70 | // 验证连接状态
71 | await dispatch("requestAccess");
72 | }
73 | await dispatch("checkNetwork"); // 检查选择的网络
74 | await dispatch("fetchNFTMetadata"); //
75 | await dispatch("setupEventListeners");
76 | } catch (error) {
77 | console.log(error);
78 | commit("setError", "Account request refused.");
79 | }
80 | },
81 | async checkNetwork({ commit, dispatch }) {
82 | let chainId = await ethereum.request({ method: "eth_chainId" });
83 | const goerliChainId = "0x5";
84 | if (chainId !== goerliChainId) {
85 | if (!(await dispatch("switchNetwork"))) {
86 | commit(
87 | "setError",
88 | "You are not connected to the Goerli Test Network!"
89 | );
90 | }
91 | }
92 | },
93 | async switchNetwork() {
94 | try {
95 | await ethereum.request({
96 | method: "wallet_switchEthereumChain",
97 | params: [{ chainId: "0x5" }],
98 | });
99 | return 1;
100 | } catch (switchError) {
101 | return 0;
102 | }
103 | },
104 | async checkIfConnected({ commit }) {
105 | const { ethereum } = window;
106 | const accounts = await ethereum.request({ method: "eth_accounts" });
107 | if (accounts.length !== 0) {
108 | commit("setAccount", accounts[0]);
109 | return 1;
110 | } else {
111 | return 0;
112 | }
113 | },
114 | async requestAccess({ commit }) {
115 | const { ethereum } = window;
116 | const accounts = await ethereum.request({
117 | method: "eth_requestAccounts",
118 | });
119 | commit("setAccount", accounts[0]);
120 | },
121 | async getContract({ state }) {
122 | try {
123 | const { ethereum } = window;
124 | const provider = new ethers.providers.Web3Provider(ethereum);
125 | const signer = provider.getSigner();
126 | const connectedContract = new ethers.Contract(
127 | state.contract_address,
128 | EpicGame.abi,
129 | signer
130 | );
131 | return connectedContract;
132 | } catch (error) {
133 | console.log(error);
134 | console.log("connected contract not found");
135 | return null;
136 | }
137 | },
138 | async setupEventListeners({ state, commit, dispatch }) {
139 | try {
140 | const connectedContract = await dispatch("getContract");
141 | if (!connectedContract) return;
142 | connectedContract.on(
143 | "CharacterNFTMinted",
144 | async (from, tokenId, characterIndex) => {
145 | console.log(
146 | `CharacterNFTMinted - sender: ${from} tokenId: ${tokenId.toNumber()} characterIndex: ${characterIndex.toNumber()}`
147 | );
148 | const characterNFT =
149 | await connectedContract.checkIfUserHasNFT();
150 | console.log(characterNFT);
151 | commit(
152 | "setCharacterNFT",
153 | transformCharacterData(characterNFT)
154 | );
155 | alert(
156 | `Your NFT is all done -- see it here: https://testnets.opensea.io/assets/${
157 | state.contract_address
158 | }/${tokenId.toNumber()}`
159 | );
160 | }
161 | );
162 |
163 | connectedContract.on(
164 | "AttackComplete",
165 | async (newBossHp, newPlayerHp) => {
166 | console.log(
167 | `AttackComplete: Boss Hp: ${newBossHp} Player Hp: ${newPlayerHp}`
168 | );
169 | let boss = state.boss;
170 | boss.hp = newBossHp;
171 | commit("setBoss", boss);
172 |
173 | let character = state.characterNFT;
174 | character.hp = newPlayerHp;
175 | commit("setCharacterNFT", character);
176 | }
177 | );
178 | } catch (error) {
179 | console.log(error);
180 | }
181 | },
182 | async disableEventListeners({ state, commit, dispatch }) {
183 | const connectedContract = await dispatch("getContract");
184 | connectedContract.off("CharacterNFTMinted");
185 | },
186 | async fetchNFTMetadata({ state, commit, dispatch }) {
187 | try {
188 | const connectedContract = await dispatch("getContract");
189 | const txn = await connectedContract.checkIfUserHasNFT();
190 | if (txn.name) {
191 | commit("setCharacterNFT", transformCharacterData(txn));
192 | }
193 | } catch (error) {
194 | console.log(error);
195 | }
196 | },
197 | async getCharacters({ state, commit, dispatch }) {
198 | try {
199 | const connectedContract = await dispatch("getContract");
200 | const charactersTxn =
201 | await connectedContract.getAllDefaultCharacters();
202 | const characters = charactersTxn.map((characterData) =>
203 | transformCharacterData(characterData)
204 | );
205 | commit("setCharacters", characters);
206 | } catch (error) {
207 | console.log(error);
208 | }
209 | },
210 | async mintCharacterNFT({ commit, dispatch }, characterId) {
211 | try {
212 | const connectedContract = await dispatch("getContract");
213 | const mintTxn = await connectedContract.mintCharacterNFT(
214 | characterId
215 | );
216 | await mintTxn.wait();
217 | } catch (error) {
218 | console.log(error);
219 | }
220 | },
221 | async fetchBoss({ state, commit, dispatch }) {
222 | try {
223 | const connectedContract = await dispatch("getContract");
224 | const bossTxn = await connectedContract.getBigBoss();
225 | commit("setBoss", transformCharacterData(bossTxn));
226 | } catch (error) {
227 | console.log(error);
228 | }
229 | },
230 | async attackBoss({ state, commit, dispatch }) {
231 | try {
232 | const connectedContract = await dispatch("getContract");
233 | commit("setAttackState", "attacking");
234 | console.log("Attacking boss...");
235 | const attackTxn = await connectedContract.attackBoss();
236 | await attackTxn.wait();
237 | console.log("attackTxn:", attackTxn);
238 | commit("setAttackState", "hit");
239 | } catch (error) {
240 | console.error("Error attacking boss:", error);
241 | setAttackState("");
242 | }
243 | },
244 | },
245 | });
246 |
--------------------------------------------------------------------------------
/game/src/components/Arena.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
14 |
15 |
16 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | {{
27 | `💥 ${boss.name} 战斗损耗 ${characterNFT.attackDamage}`
28 | }}
29 |
30 |
31 |
32 |
33 |
34 | 🔥 {{ boss.name }} 🔥
35 |
36 |
37 |
![]()
41 |
42 |
43 |
44 |
48 |
49 | {{
50 | `${boss.hp} / ${boss.maxHp} HP`
51 | }}
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |

61 |
62 |
63 |
64 |
65 |
66 | {{
67 | `⚔️ 战斗损耗: ${characterNFT.attackDamage}`
68 | }}
69 |
70 |
71 |
72 |
73 |
74 | ⚔️ {{ characterNFT.name }} ⚔️
75 |
76 |
77 |
![]()
82 |
83 |
84 |
85 |
89 |
90 | {{
91 | `${characterNFT.hp} / ${characterNFT.maxHp} HP`
92 | }}
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
133 |
134 |
415 |
--------------------------------------------------------------------------------
/game/src/assets/twitter-logo.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------