├── 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 | 11 | 12 | 15 | 16 | 34 | -------------------------------------------------------------------------------- /game/src/components/LoadingIndicator.vue: -------------------------------------------------------------------------------- 1 | 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 | ![GAME UI](https://s2.51cto.com/images/20220305/1646465891354624.jpeg) 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 | 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 | 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 | 105 | 106 | 133 | 134 | 415 | -------------------------------------------------------------------------------- /game/src/assets/twitter-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | --------------------------------------------------------------------------------