# buildspace的create-turn-based-nft-game课程 **Published by:** [Coinman.eth](https://paragraph.com/@coinman-eth/) **Published on:** 2022-12-02 **URL:** https://paragraph.com/@coinman-eth/buildspace-create-turn-based-nft-game ## Content 大家好,我是帝哥(推特:@CoinmanLabs),大家或多或少都知道帝哥是程序员出身,因为确实官方的项目的文档很久没更新了,帝哥就跟官方沟通了,官方要邀请帝哥参与修改他们的代码,所以今天继续buildspace的课程-create-turn-based-nft-game,这里也按照官方给的阶段来。1.入门大致介绍了下这次的项目主要是做什么的,目的是什么,大家了解下就行了,最后问你,特别希望将任何加密货币公司添加到列表中,我选择了币安,大家可以自行答题,然后给我们介绍了下Axie Infinity游戏,最后问你喜欢什么游戏,我选择了Mario NFT填写。2.构建NFT角色step1.设置好replit环境 使用的编码网站还是replit, 我们来到replit中首先新建一个项目,这里我就叫做003了(这里可能很多人就奇怪了为啥不直接跟着官网走,创建一个项目后直接安装依赖呢,如果使用replit较多的话,你就知道了,replit不可创建.env文件,但是采用这样的方式是可以创建的)创建项目还是原来的,这里在说下,我将整个页面分为了三个区,便于后续针对每个区操作说明简单。分区当我们的工作台准备工作完成后,在shell区执行下面的命令mkdir epic-game cd epic-game npm init -y npm install --save-dev hardhat@latest # 等待上面都执行完成后在输入下面的命令,全部按回车即可 npx hardhat npm install --save-dev chai @nomiclabs/hardhat-ethers ethers @nomicfoundation/hardhat-toolbox @nomicfoundation/hardhat-chai-matchers npm install @openzeppelin/contracts 按照依赖当我们上面的依赖全部安装完成后,因为hardhat项目框架本身就携带了合约和测试部署的脚本(默认使用的是hardhat的本地节点),当你需要测试是否安装成功则需要执行下面的命令。npx hardhat run scripts/deploy.js 如果你正常执行则说明你配置正确,在这章节就可以把你正常运行deploy.js的截图上传。上传deploy结果step2.运行一个简单的合约 在contracts文件夹下新建一个MyEpicGame.sol文件,写入下面的代码// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.17; import "hardhat/console.sol"; contract MyEpicGame { constructor() { console.log("THIS IS MY GAME CONTRACT. NICE."); } } 同时在scripts文件夹下面新建一个run.js,写入下面的代码const main = async () => { const gameContractFactory = await hre.ethers.getContractFactory('MyEpicGame'); const gameContract = await gameContractFactory.deploy(); await gameContract.deployed(); console.log("Contract deployed to:", gameContract.address); }; const runMain = async () => { try { await main(); process.exit(0); } catch (error) { console.log(error); process.exit(1); } }; runMain(); 当前面全部完成后在shell区输入下面的命令,将输出的结果作为这一步的截图提交。第二步提交的截图step3.设置人物属性 当我们完成了上面的测试之后,首先将我们的MyEpicGame.sol文件替换成下面的。// SPDX-License-Identifier: MIT pragma solidity ^0.8.17; import "hardhat/console.sol"; contract MyEpicGame { // We'll hold our character's attributes in a struct. Feel free to add // whatever you'd like as an attribute! (ex. defense, crit chance, etc). struct CharacterAttributes { uint characterIndex; string name; string imageURI; uint hp; uint maxHp; uint attackDamage; } // A lil array to help us hold the default data for our characters. // This will be helpful when we mint new characters and need to know // things like their HP, AD, etc. CharacterAttributes[] defaultCharacters; // Data passed in to the contract when it's first created initializing the characters. // We're going to actually pass these values in from run.js. constructor( string[] memory characterNames, string[] memory characterImageURIs, uint[] memory characterHp, uint[] memory characterAttackDmg ) { // Loop through all the characters, and save their values in our contract so // we can use them later when we mint our NFTs. for(uint i = 0; i < characterNames.length; i += 1) { defaultCharacters.push(CharacterAttributes({ characterIndex: i, name: characterNames[i], imageURI: characterImageURIs[i], hp: characterHp[i], maxHp: characterHp[i], attackDamage: characterAttackDmg[i] })); CharacterAttributes memory c = defaultCharacters[i]; console.log("Done initializing %s w/ HP %s, img %s", c.name, c.hp, c.imageURI); } } } 同时需要替换run.js的内容如下。const main = async () => { const gameContractFactory = await hre.ethers.getContractFactory('MyEpicGame'); const gameContract = await gameContractFactory.deploy( ["Leo", "Aang", "Pikachu"], // Names ["https://i.imgur.com/pKd5Sdk.png", // Images "https://i.imgur.com/xVu4vFL.png", "https://i.imgur.com/WMB6g9u.png"], [100, 200, 300], // HP values [100, 50, 25] // Attack damage values ); await gameContract.deployed(); console.log("Contract deployed to:", gameContract.address); }; const runMain = async () => { try { await main(); process.exit(0); } catch (error) { console.log(error); process.exit(1); } }; runMain(); 当替换完成后再次执行测试脚本,将测试脚本的结果截图作为本步骤。npx hardhat run scripts/run.js 第三步提交的截图step4.在本地铸造 NFT 我们再次更新合约sol文件,将下面的代码进行替换。// SPDX-License-Identifier: MIT pragma solidity ^0.8.17; // NFT contract to inherit from. import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; // Helper functions OpenZeppelin provides. import "@openzeppelin/contracts/utils/Counters.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; import "hardhat/console.sol"; import "./Base64.sol"; // Our contract inherits from ERC721, which is the standard NFT contract! contract MyEpicGame is ERC721 { struct CharacterAttributes { uint characterIndex; string name; string imageURI; uint hp; uint maxHp; uint attackDamage; } // The tokenId is the NFTs unique identifier, it's just a number that goes // 0, 1, 2, 3, etc. using Counters for Counters.Counter; Counters.Counter private _tokenIds; CharacterAttributes[] defaultCharacters; // We create a mapping from the nft's tokenId => that NFTs attributes. mapping(uint256 => CharacterAttributes) public nftHolderAttributes; // A mapping from an address => the NFTs tokenId. Gives me an ez way // to store the owner of the NFT and reference it later. mapping(address => uint256) public nftHolders; constructor( string[] memory characterNames, string[] memory characterImageURIs, uint[] memory characterHp, uint[] memory characterAttackDmg // Below, you can also see I added some special identifier symbols for our NFT. // This is the name and symbol for our token, ex Ethereum and ETH. I just call mine // Heroes and HERO. Remember, an NFT is just a token! ) ERC721("Heroes", "HERO") { for(uint i = 0; i < characterNames.length; i += 1) { defaultCharacters.push(CharacterAttributes({ characterIndex: i, name: characterNames[i], imageURI: characterImageURIs[i], hp: characterHp[i], maxHp: characterHp[i], attackDamage: characterAttackDmg[i] })); CharacterAttributes memory c = defaultCharacters[i]; // Hardhat's use of console.log() allows up to 4 parameters in any order of following types: uint, string, bool, address console.log("Done initializing %s w/ HP %s, img %s", c.name, c.hp, c.imageURI); } // I increment _tokenIds here so that my first NFT has an ID of 1. // More on this in the lesson! _tokenIds.increment(); } // Users would be able to hit this function and get their NFT based on the // characterId they send in! function mintCharacterNFT(uint _characterIndex) external { // Get current tokenId (starts at 1 since we incremented in the constructor). uint256 newItemId = _tokenIds.current(); // The magical function! Assigns the tokenId to the caller's wallet address. _safeMint(msg.sender, newItemId); // We map the tokenId => their character attributes. More on this in // the lesson below. nftHolderAttributes[newItemId] = CharacterAttributes({ characterIndex: _characterIndex, name: defaultCharacters[_characterIndex].name, imageURI: defaultCharacters[_characterIndex].imageURI, hp: defaultCharacters[_characterIndex].hp, maxHp: defaultCharacters[_characterIndex].maxHp, attackDamage: defaultCharacters[_characterIndex].attackDamage }); console.log("Minted NFT w/ tokenId %s and characterIndex %s", newItemId, _characterIndex); // Keep an easy way to see who owns what NFT. nftHolders[msg.sender] = newItemId; // Increment the tokenId for the next person that uses it. _tokenIds.increment(); } function tokenURI(uint256 _tokenId) public view override returns (string memory) { CharacterAttributes memory charAttributes = nftHolderAttributes[_tokenId]; string memory strHp = Strings.toString(charAttributes.hp); string memory strMaxHp = Strings.toString(charAttributes.maxHp); string memory strAttackDamage = Strings.toString(charAttributes.attackDamage); string memory json = Base64.encode( abi.encodePacked( '{"name": "', charAttributes.name, ' -- NFT #: ', Strings.toString(_tokenId), '", "description": "This is an NFT that lets people play in the game Metaverse Slayer!", "image": "', charAttributes.imageURI, '", "attributes": [ { "trait_type": "Health Points", "value": ',strHp,', "max_value":',strMaxHp,'}, { "trait_type": "Attack Damage", "value": ', strAttackDamage,'} ]}' ) ); string memory output = string( abi.encodePacked("data:application/json;base64,", json) ); return output; } } 同时我们在contract下面新建一个Base64.sol文件,写入下面的内容。// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; /// [MIT License] /// @title Base64 /// @notice Provides a function for encoding some bytes in base64 /// @author Brecht Devos <brecht@loopring.org> library Base64 { bytes internal constant TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; /// @notice Encodes some bytes to the base64 representation function encode(bytes memory data) internal pure returns (string memory) { uint256 len = data.length; if (len == 0) return ""; // multiply by 4/3 rounded up uint256 encodedLen = 4 * ((len + 2) / 3); // Add some extra buffer at the end bytes memory result = new bytes(encodedLen + 32); bytes memory table = TABLE; assembly { let tablePtr := add(table, 1) let resultPtr := add(result, 32) for { let i := 0 } lt(i, len) { } { i := add(i, 3) let input := and(mload(add(data, i)), 0xffffff) let out := mload(add(tablePtr, and(shr(18, input), 0x3F))) out := shl(8, out) out := add( out, and(mload(add(tablePtr, and(shr(12, input), 0x3F))), 0xFF) ) out := shl(8, out) out := add( out, and(mload(add(tablePtr, and(shr(6, input), 0x3F))), 0xFF) ) out := shl(8, out) out := add( out, and(mload(add(tablePtr, and(input, 0x3F))), 0xFF) ) out := shl(224, out) mstore(resultPtr, out) resultPtr := add(resultPtr, 4) } switch mod(len, 3) case 1 { mstore(sub(resultPtr, 2), shl(240, 0x3d3d)) } case 2 { mstore(sub(resultPtr, 1), shl(248, 0x3d)) } mstore(result, encodedLen) } return string(result); } } 当将sol文件替换完之后,将我们的run.js进行替换,替换的如下。const main = async () => { const gameContractFactory = await hre.ethers.getContractFactory('MyEpicGame'); const gameContract = await gameContractFactory.deploy( ["Leo", "Aang", "Pikachu"], // Names ["https://i.imgur.com/pKd5Sdk.png", // Images "https://i.imgur.com/xVu4vFL.png", "https://i.imgur.com/WMB6g9u.png"], [100, 200, 300], // HP values [100, 50, 25] // Attack damage values ); await gameContract.deployed(); console.log("Contract deployed to:", gameContract.address); let txn; // We only have three characters. // an NFT w/ the character at index 2 of our array. txn = await gameContract.mintCharacterNFT(2); await txn.wait(); // Get the value of the NFT's URI. let returnedTokenUri = await gameContract.tokenURI(1); console.log("Token URI:", returnedTokenUri); }; const runMain = async () => { try { await main(); process.exit(0); } catch (error) { console.log(error); process.exit(1); } }; runMain(); 再次运行run.js,将输出的token url新开一个tab页显示作为本步骤截图。npx hardhat run scripts/run.js 运行结果本步骤提交截图step5.将合约部署到测试网络 将deploy.js将下面的内容替换过去即可。const main = async () => { const gameContractFactory = await hre.ethers.getContractFactory('MyEpicGame'); const gameContract = await gameContractFactory.deploy( ["Leo", "Aang", "Pikachu"], ["https://i.imgur.com/pKd5Sdk.png", "https://i.imgur.com/xVu4vFL.png", "https://i.imgur.com/u7T87A6.png"], [100, 200, 300], [100, 50, 25] ); await gameContract.deployed(); console.log("Contract deployed to:", gameContract.address); let txn; txn = await gameContract.mintCharacterNFT(0); await txn.wait(); console.log("Minted NFT #1"); txn = await gameContract.mintCharacterNFT(1); await txn.wait(); console.log("Minted NFT #2"); txn = await gameContract.mintCharacterNFT(2); await txn.wait(); console.log("Minted NFT #3"); txn = await gameContract.mintCharacterNFT(1); await txn.wait(); console.log("Minted NFT #4"); console.log("Done deploying and minting!"); }; const runMain = async () => { try { await main(); process.exit(0); } catch (error) { console.log(error); process.exit(1); } }; runMain(); 现在 让我们来修改下hardhat.config.js,替换如下。require("@nomicfoundation/hardhat-toolbox"); module.exports = { solidity: '0.8.17', networks: { goerli: { url: '你的rpc的地址 可以使用alchemy', accounts: ['你的私钥'], }, }, }; 都完成后在shell区输入下面的命令部署。npx hardhat run scripts/deploy.js --network goerli 部署结果当部署完成后,去到https://goerli.pixxiti.com/nfts/你的合约地址/你的tokenid(选择一个即可1-4) # 比如帝哥的地址查看的https://goerli.pixxiti.com/nfts/0x5410BF0144d2f6844815f629CE4Fb29456FE9C8a/1 本步骤提交的截图2.构建游戏逻辑step1.构建boss和购机逻辑 将我们的sol文件替换为下面的内容。// SPDX-License-Identifier: MIT pragma solidity ^0.8.17; // NFT contract to inherit from. import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; // Helper functions OpenZeppelin provides. import "@openzeppelin/contracts/utils/Counters.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; import "hardhat/console.sol"; import "./Base64.sol"; // Our contract inherits from ERC721, which is the standard NFT contract! contract MyEpicGame is ERC721 { struct CharacterAttributes { uint characterIndex; string name; string imageURI; uint hp; uint maxHp; uint attackDamage; } struct BigBoss { string name; string imageURI; uint hp; uint maxHp; uint attackDamage; } // The tokenId is the NFTs unique identifier, it's just a number that goes // 0, 1, 2, 3, etc. using Counters for Counters.Counter; Counters.Counter private _tokenIds; CharacterAttributes[] defaultCharacters; BigBoss public bigBoss; // We create a mapping from the nft's tokenId => that NFTs attributes. mapping(uint256 => CharacterAttributes) public nftHolderAttributes; // A mapping from an address => the NFTs tokenId. Gives me an ez way // to store the owner of the NFT and reference it later. mapping(address => uint256) public nftHolders; uint randNonce = 0; constructor( string[] memory characterNames, string[] memory characterImageURIs, uint[] memory characterHp, uint[] memory characterAttackDmg, string memory bossName, string memory bossImageURI, uint bossHp, uint bossAttackDamage ) ERC721("Heroes", "HERO") { for(uint i = 0; i < characterNames.length; i += 1) { defaultCharacters.push(CharacterAttributes({ characterIndex: i, name: characterNames[i], imageURI: characterImageURIs[i], hp: characterHp[i], maxHp: characterHp[i], attackDamage: characterAttackDmg[i] })); bigBoss = BigBoss({ name: bossName, imageURI: bossImageURI, hp: bossHp, maxHp: bossHp, attackDamage: bossAttackDamage }); console.log("Done initializing boss %s w/ HP %s, img %s", bigBoss.name, bigBoss.hp, bigBoss.imageURI); CharacterAttributes memory c = defaultCharacters[i]; // Hardhat's use of console.log() allows up to 4 parameters in any order of following types: uint, string, bool, address console.log("Done initializing %s w/ HP %s, img %s", c.name, c.hp, c.imageURI); } // I increment _tokenIds here so that my first NFT has an ID of 1. // More on this in the lesson! _tokenIds.increment(); } function attackBoss() public { // Get the state of the player's NFT. uint256 nftTokenIdOfPlayer = nftHolders[msg.sender]; CharacterAttributes storage player = nftHolderAttributes[nftTokenIdOfPlayer]; console.log("\nPlayer w/ character %s about to attack. Has %s HP and %s AD", player.name, player.hp, player.attackDamage); console.log("Boss %s has %s HP and %s AD", bigBoss.name, bigBoss.hp, bigBoss.attackDamage); // Make sure the player has more than 0 HP. require ( player.hp > 0, "Error: character must have HP to attack boss." ); // Make sure the boss has more than 0 HP. require ( bigBoss.hp > 0, "Error: boss must have HP to attack character." ); console.log("%s swings at %s...", player.name, bigBoss.name); if (bigBoss.hp < player.attackDamage) { bigBoss.hp = 0; console.log("The boss is dead!"); } else { if (randomInt(10) > 5) { // by passing 10 as the mod, we elect to only grab the last digit (0-9) of the hash! bigBoss.hp = bigBoss.hp - player.attackDamage; console.log("%s attacked boss. New boss hp: %s", player.name, bigBoss.hp); } else { console.log("%s missed!\n", player.name); } } // Console for ease. console.log("Player attacked boss. New boss hp: %s", bigBoss.hp); console.log("Boss attacked player. New player hp: %s\n", player.hp); } function randomInt(uint _modulus) internal returns(uint) { randNonce++; // increase nonce return uint(keccak256(abi.encodePacked(block.timestamp, // an alias for 'block.timestamp' msg.sender, // your address randNonce))) % _modulus; // modulo using the _modulus argument } // Users would be able to hit this function and get their NFT based on the // characterId they send in! function mintCharacterNFT(uint _characterIndex) external { // Get current tokenId (starts at 1 since we incremented in the constructor). uint256 newItemId = _tokenIds.current(); // The magical function! Assigns the tokenId to the caller's wallet address. _safeMint(msg.sender, newItemId); // We map the tokenId => their character attributes. More on this in // the lesson below. nftHolderAttributes[newItemId] = CharacterAttributes({ characterIndex: _characterIndex, name: defaultCharacters[_characterIndex].name, imageURI: defaultCharacters[_characterIndex].imageURI, hp: defaultCharacters[_characterIndex].hp, maxHp: defaultCharacters[_characterIndex].maxHp, attackDamage: defaultCharacters[_characterIndex].attackDamage }); console.log("Minted NFT w/ tokenId %s and characterIndex %s", newItemId, _characterIndex); // Keep an easy way to see who owns what NFT. nftHolders[msg.sender] = newItemId; // Increment the tokenId for the next person that uses it. _tokenIds.increment(); } function tokenURI(uint256 _tokenId) public view override returns (string memory) { CharacterAttributes memory charAttributes = nftHolderAttributes[_tokenId]; string memory strHp = Strings.toString(charAttributes.hp); string memory strMaxHp = Strings.toString(charAttributes.maxHp); string memory strAttackDamage = Strings.toString(charAttributes.attackDamage); string memory json = Base64.encode( abi.encodePacked( '{"name": "', charAttributes.name, ' -- NFT #: ', Strings.toString(_tokenId), '", "description": "This is an NFT that lets people play in the game Metaverse Slayer!", "image": "', charAttributes.imageURI, '", "attributes": [ { "trait_type": "Health Points", "value": ',strHp,', "max_value":',strMaxHp,'}, { "trait_type": "Attack Damage", "value": ', strAttackDamage,'} ]}' ) ); string memory output = string( abi.encodePacked("data:application/json;base64,", json) ); return output; } } 将run,js代码替换如下。const main = async () => { const gameContractFactory = await hre.ethers.getContractFactory('MyEpicGame'); const gameContract = await gameContractFactory.deploy( ["Leo", "Aang", "Pikachu"], ["https://i.imgur.com/pKd5Sdk.png", "https://i.imgur.com/xVu4vFL.png", "https://i.imgur.com/u7T87A6.png"], [100, 200, 300], [100, 50, 25], "Elon Musk", // Boss name "https://i.imgur.com/AksR0tt.png", // Boss image 10000, // Boss hp 50 // Boss attack damage ); await gameContract.deployed(); console.log("Contract deployed to:", gameContract.address); let txn; // We only have three characters. // an NFT w/ the character at index 2 of our array. txn = await gameContract.mintCharacterNFT(2); await txn.wait(); txn = await gameContract.attackBoss(); await txn.wait(); txn = await gameContract.attackBoss(); await txn.wait(); }; const runMain = async () => { try { await main(); process.exit(0); } catch (error) { console.log(error); process.exit(1); } }; runMain(); 当上面两者都替换完成后,执行run.js将输出的结果作为本步骤的截图。本步骤的截图step2. 将下面的代码替换到deploy.js。const main = async () => { const gameContractFactory = await hre.ethers.getContractFactory('MyEpicGame'); const gameContract = await gameContractFactory.deploy( ["Leo", "Aang", "Pikachu"], ["https://i.imgur.com/pKd5Sdk.png", "https://i.imgur.com/xVu4vFL.png", "https://i.imgur.com/u7T87A6.png"], [100, 200, 300], [100, 50, 25], "Elon Musk", "https://i.imgur.com/AksR0tt.png", 10000, 50 ); await gameContract.deployed(); console.log("Contract deployed to:", gameContract.address); let txn; // We only have three characters. // an NFT w/ the character at index 2 of our array. txn = await gameContract.mintCharacterNFT(2); await txn.wait(); txn = await gameContract.attackBoss(); await txn.wait(); txn = await gameContract.attackBoss(); await txn.wait(); console.log("Done!"); }; const runMain = async () => { try { await main(); process.exit(0); } catch (error) { console.log(error); process.exit(1); } }; runMain(); 运行部署脚本,去到网站查看属性作为了本步骤的提交结果。npx hardhat run scripts/deploy.js --network goerli 当部署完成后进行查看。https://goerli.pixxiti.com/nfts/你的合约地址/你的tokenid(选择一个即可1-4) # 比如帝哥的地址查看的https://goerli.pixxiti.com/nfts/0x5410BF0144d2f6844815f629CE4Fb29456FE9C8a/1 step3.部署并查看 NFT 在产品中的变化。(合约也需要修改才能看到变化,请继续下一步完善合约) 将下面的代码替换到deploy.js中。const main = async () => { const gameContractFactory = await hre.ethers.getContractFactory('MyEpicGame'); const gameContract = await gameContractFactory.deploy( ["Leo", "Aang", "Pikachu"], ["https://i.imgur.com/pKd5Sdk.png", "https://i.imgur.com/xVu4vFL.png", "https://i.imgur.com/u7T87A6.png"], [100, 200, 300], [100, 50, 25], "Elon Musk", "https://i.imgur.com/AksR0tt.png", 10000, 50 ); await gameContract.deployed(); console.log("Contract deployed to:", gameContract.address); let txn; // We only have three characters. // an NFT w/ the character at index 2 of our array. txn = await gameContract.mintCharacterNFT(2); await txn.wait(); txn = await gameContract.attackBoss(); await txn.wait(); txn = await gameContract.attackBoss(); await txn.wait(); console.log("Done!"); }; const runMain = async () => { try { await main(); process.exit(0); } catch (error) { console.log(error); process.exit(1); } }; runMain(); 这里我们需要注意一个地方,因为在产生攻击的时候作者采用了一个10以内的随机数可能会存在你的nft的属性还是原来的,不会有更改,我给大家解释下上面deploy.js里面的意思。# 这里部署了一个名字叫做Pikachu hp为300 攻击力为25的人物 txn = await gameContract.mintCharacterNFT(2); 上面说了可能不会触发攻击来导致属性变化,那我们怎么改下,可以让他一定攻击的到呢?记得这一步提交截图后再次改回来。需要修改的地方我们可以看到任务的生命值确实变化了。记得将刚才修改的值改回去。本步骤提交的截图step3.完善合约 我们将合约文件sol再次替换为下面的。// SPDX-License-Identifier: MIT pragma solidity ^0.8.17; // NFT contract to inherit from. import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; // Helper functions OpenZeppelin provides. import "@openzeppelin/contracts/utils/Counters.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; import "hardhat/console.sol"; import "./Base64.sol"; // Our contract inherits from ERC721, which is the standard NFT contract! contract MyEpicGame is ERC721 { event CharacterNFTMinted(address sender, uint256 tokenId, uint256 characterIndex); event AttackComplete(address sender, uint newBossHp, uint newPlayerHp); struct CharacterAttributes { uint characterIndex; string name; string imageURI; uint hp; uint maxHp; uint attackDamage; } struct BigBoss { string name; string imageURI; uint hp; uint maxHp; uint attackDamage; } // The tokenId is the NFTs unique identifier, it's just a number that goes // 0, 1, 2, 3, etc. using Counters for Counters.Counter; Counters.Counter private _tokenIds; CharacterAttributes[] defaultCharacters; BigBoss public bigBoss; // We create a mapping from the nft's tokenId => that NFTs attributes. mapping(uint256 => CharacterAttributes) public nftHolderAttributes; // A mapping from an address => the NFTs tokenId. Gives me an ez way // to store the owner of the NFT and reference it later. mapping(address => uint256) public nftHolders; uint randNonce = 0; constructor( string[] memory characterNames, string[] memory characterImageURIs, uint[] memory characterHp, uint[] memory characterAttackDmg, string memory bossName, string memory bossImageURI, uint bossHp, uint bossAttackDamage ) ERC721("Heroes", "HERO") { for(uint i = 0; i < characterNames.length; i += 1) { defaultCharacters.push(CharacterAttributes({ characterIndex: i, name: characterNames[i], imageURI: characterImageURIs[i], hp: characterHp[i], maxHp: characterHp[i], attackDamage: characterAttackDmg[i] })); bigBoss = BigBoss({ name: bossName, imageURI: bossImageURI, hp: bossHp, maxHp: bossHp, attackDamage: bossAttackDamage }); console.log("Done initializing boss %s w/ HP %s, img %s", bigBoss.name, bigBoss.hp, bigBoss.imageURI); CharacterAttributes memory c = defaultCharacters[i]; // Hardhat's use of console.log() allows up to 4 parameters in any order of following types: uint, string, bool, address console.log("Done initializing %s w/ HP %s, img %s", c.name, c.hp, c.imageURI); } // I increment _tokenIds here so that my first NFT has an ID of 1. // More on this in the lesson! _tokenIds.increment(); } function attackBoss() public { // Get the state of the player's NFT. uint256 nftTokenIdOfPlayer = nftHolders[msg.sender]; CharacterAttributes storage player = nftHolderAttributes[nftTokenIdOfPlayer]; console.log("\nPlayer w/ character %s about to attack. Has %s HP and %s AD", player.name, player.hp, player.attackDamage); console.log("Boss %s has %s HP and %s AD", bigBoss.name, bigBoss.hp, bigBoss.attackDamage); // Make sure the player has more than 0 HP. require ( player.hp > 0, "Error: character must have HP to attack boss." ); // Make sure the boss has more than 0 HP. require ( bigBoss.hp > 0, "Error: boss must have HP to attack character." ); // Allow boss to attack player. if (player.hp < bigBoss.attackDamage) { player.hp = 0; } else { player.hp = player.hp - bigBoss.attackDamage; } console.log("%s swings at %s...", player.name, bigBoss.name); if (bigBoss.hp < player.attackDamage) { bigBoss.hp = 0; console.log("The boss is dead!"); } else { if (randomInt(10) > 9) { // by passing 10 as the mod, we elect to only grab the last digit (0-9) of the hash! bigBoss.hp = bigBoss.hp - player.attackDamage; console.log("%s attacked boss. New boss hp: %s", player.name, bigBoss.hp); } else { console.log("%s missed!\n", player.name); } } // Console for ease. console.log("Player attacked boss. New boss hp: %s", bigBoss.hp); console.log("Boss attacked player. New player hp: %s\n", player.hp); emit AttackComplete(msg.sender, bigBoss.hp, player.hp); } function randomInt(uint _modulus) internal returns(uint) { randNonce++; // increase nonce return uint(keccak256(abi.encodePacked(block.timestamp, // an alias for 'block.timestamp' msg.sender, // your address randNonce))) % _modulus; // modulo using the _modulus argument } // Users would be able to hit this function and get their NFT based on the // characterId they send in! function mintCharacterNFT(uint _characterIndex) external { // Get current tokenId (starts at 1 since we incremented in the constructor). uint256 newItemId = _tokenIds.current(); // The magical function! Assigns the tokenId to the caller's wallet address. _safeMint(msg.sender, newItemId); // We map the tokenId => their character attributes. More on this in // the lesson below. nftHolderAttributes[newItemId] = CharacterAttributes({ characterIndex: _characterIndex, name: defaultCharacters[_characterIndex].name, imageURI: defaultCharacters[_characterIndex].imageURI, hp: defaultCharacters[_characterIndex].hp, maxHp: defaultCharacters[_characterIndex].maxHp, attackDamage: defaultCharacters[_characterIndex].attackDamage }); console.log("Minted NFT w/ tokenId %s and characterIndex %s", newItemId, _characterIndex); // Keep an easy way to see who owns what NFT. nftHolders[msg.sender] = newItemId; // Increment the tokenId for the next person that uses it. _tokenIds.increment(); emit CharacterNFTMinted(msg.sender, newItemId, _characterIndex); } function tokenURI(uint256 _tokenId) public view override returns (string memory) { CharacterAttributes memory charAttributes = nftHolderAttributes[_tokenId]; string memory strHp = Strings.toString(charAttributes.hp); string memory strMaxHp = Strings.toString(charAttributes.maxHp); string memory strAttackDamage = Strings.toString(charAttributes.attackDamage); string memory json = Base64.encode( abi.encodePacked( '{"name": "', charAttributes.name, ' -- NFT #: ', Strings.toString(_tokenId), '", "description": "This is an NFT that lets people play in the game Metaverse Slayer!", "image": "', charAttributes.imageURI, '", "attributes": [ { "trait_type": "Health Points", "value": ',strHp,', "max_value":',strMaxHp,'}, { "trait_type": "Attack Damage", "value": ', strAttackDamage,'} ]}' ) ); string memory output = string( abi.encodePacked("data:application/json;base64,", json) ); return output; } function checkIfUserHasNFT() public view returns (CharacterAttributes memory) { // Get the tokenId of the user's character NFT uint256 userNftTokenId = nftHolders[msg.sender]; // If the user has a tokenId in the map, return their character. if (userNftTokenId > 0) { return nftHolderAttributes[userNftTokenId]; } // Else, return an empty character. else { CharacterAttributes memory emptyStruct; return emptyStruct; } } function getBigBoss() public view returns (BigBoss memory) { return bigBoss; } function getAllDefaultCharacters() public view returns (CharacterAttributes[] memory) { return defaultCharacters; } } 同步替换我们的deploy.jsconst main = async () => { const gameContractFactory = await hre.ethers.getContractFactory('MyEpicGame'); const gameContract = await gameContractFactory.deploy( ["Leo", "Aang", "Pikachu"], ["https://i.imgur.com/pKd5Sdk.png", "https://i.imgur.com/xVu4vFL.png", "https://i.imgur.com/u7T87A6.png"], [100, 200, 300], [100, 50, 25], "Elon Musk", "https://i.imgur.com/AksR0tt.png", 10000, 50 ); await gameContract.deployed(); console.log("Contract deployed to:", gameContract.address); }; const runMain = async () => { try { await main(); process.exit(0); } catch (error) { console.log(error); process.exit(1); } }; runMain(); 当上面都替换好之后,运行部署脚本。npx hardhat run scripts/deploy.js --network goerli 当部署完成后,我们可以去到测试网络的区块浏览器,查看mint事件和攻击事件的event。这一步不是提交截图了,但是你需要把你的合约的地址记录下来,而是问我们event的作用,大家可以采用我的:It is convenient to present the data visually to the user, and it is also convenient for the project side to better view the log 4.创建前端上面我们把合约编写完成了,下面就来做一个前端也便于用户操作。 step1.获取前端代码 还是使用replit来操作,首先去到该项目的github地址 ,在新建项目的时候选择从github导入。从github导入选择模版等待工作台安装完毕,当准备好之后,在shell去输入下面的命令。npm install npm run start 安装依赖当我们使用start命令启动项目后,将项目的截图最为这一步的提交截图。本步骤的截图step2.添加连接钱包按钮 将下面的代码替换到app,jsx中。import React, { useEffect, useState } from 'react'; import './App.css'; import twitterLogo from './assets/twitter-logo.svg'; // Constants const TWITTER_HANDLE = '_buildspace'; const TWITTER_LINK = `https://twitter.com/${TWITTER_HANDLE}`; const App = () => { // State const [currentAccount, setCurrentAccount] = useState(null); // Actions const checkIfWalletIsConnected = async () => { try { const { ethereum } = window; if (!ethereum) { console.log('Make sure you have MetaMask!'); return; } else { console.log('We have the ethereum object', ethereum); const accounts = await ethereum.request({ method: 'eth_accounts' }); if (accounts.length !== 0) { const account = accounts[0]; console.log('Found an authorized account:', account); setCurrentAccount(account); } else { console.log('No authorized account found'); } } } catch (error) { console.log(error); } }; /* * Implement your connectWallet method here */ const connectWalletAction = async () => { try { const { ethereum } = window; if (!ethereum) { alert('Get MetaMask!'); return; } /* * Fancy method to request access to account. */ const accounts = await ethereum.request({ method: 'eth_requestAccounts', }); /* * Boom! This should print out public address once we authorize Metamask. */ console.log('Connected', accounts[0]); setCurrentAccount(accounts[0]); } catch (error) { console.log(error); } }; useEffect(() => { checkIfWalletIsConnected(); }, []); return ( <div className="App"> <div className="container"> <div className="header-container"> <p className="header gradient-text">⚔️ Metaverse Slayer ⚔️</p> <p className="sub-text">Team up to protect the Metaverse!</p> <div className="connect-wallet-container"> <img src="https://64.media.tumblr.com/tumblr_mbia5vdmRd1r1mkubo1_500.gifv" alt="Monty Python Gif" /> {/* * Button that we will use to trigger wallet connect * Don't forget to add the onClick event to call your method! */} <button className="cta-button connect-wallet-button" onClick={connectWalletAction} > Connect Wallet To Get Started </button> </div> </div> <div className="footer-container"> <img alt="Twitter Logo" className="twitter-logo" src={twitterLogo} /> <a className="footer-text" href={TWITTER_LINK} target="_blank" rel="noreferrer" >{`built with @${TWITTER_HANDLE}`}</a> </div> </div> </div> ); }; export default App; 查看我们的网站,将这一步的结果作为本步骤的截图。本步骤的截图step3.初始化应用 首先去到src/Components/SelectCharacter新建一个index.js文件,内容如下。import React, { useEffect, useState } from 'react'; import './SelectCharacter.css'; /* * Don't worry about setCharacterNFT just yet, we will talk about it soon! */ const SelectCharacter = ({ setCharacterNFT }) => { return ( <div className="select-character-container"> <h2>Mint Your Hero. Choose wisely.</h2> </div> ); }; export default SelectCharacter; 同时将app.jsx替换。import React, { useEffect, useState } from 'react'; import './App.css'; import SelectCharacter from './Components/SelectCharacter'; import twitterLogo from './assets/twitter-logo.svg'; // Constants const TWITTER_HANDLE = '_buildspace'; const TWITTER_LINK = `https://twitter.com/${TWITTER_HANDLE}`; const App = () => { // State const [currentAccount, setCurrentAccount] = useState(null); const [characterNFT, setCharacterNFT] = useState(null); // Actions const checkIfWalletIsConnected = async () => { try { const { ethereum } = window; if (!ethereum) { console.log('Make sure you have MetaMask!'); return; } else { console.log('We have the ethereum object', ethereum); const accounts = await ethereum.request({ method: 'eth_accounts' }); if (accounts.length !== 0) { const account = accounts[0]; console.log('Found an authorized account:', account); setCurrentAccount(account); } else { console.log('No authorized account found'); } } } catch (error) { console.log(error); } }; // Render Methods const renderContent = () => { /* * Scenario #1 */ if (!currentAccount) { return ( <div className="connect-wallet-container"> <img src="https://64.media.tumblr.com/tumblr_mbia5vdmRd1r1mkubo1_500.gifv" alt="Monty Python Gif" /> <button className="cta-button connect-wallet-button" onClick={connectWalletAction} > Connect Wallet To Get Started </button> </div> ); /* * Scenario #2 */ } else if (currentAccount && !characterNFT) { return <SelectCharacter setCharacterNFT={setCharacterNFT} />; } }; /* * Implement your connectWallet method here */ const connectWalletAction = async () => { try { const { ethereum } = window; if (!ethereum) { alert('Get MetaMask!'); return; } /* * Fancy method to request access to account. */ const accounts = await ethereum.request({ method: 'eth_requestAccounts', }); /* * Boom! This should print out public address once we authorize Metamask. */ console.log('Connected', accounts[0]); setCurrentAccount(accounts[0]); } catch (error) { console.log(error); } }; useEffect(() => { checkIfWalletIsConnected(); }, []); return ( <div className="App"> <div className="container"> <div className="header-container"> <p className="header gradient-text">⚔️ Metaverse Slayer ⚔️</p> <p className="sub-text">Team up to protect the Metaverse!</p> {/* This is where our button and image code used to be! * Remember we moved it into the render method. */} {renderContent()} </div> <div className="footer-container"> <img alt="Twitter Logo" className="twitter-logo" src={twitterLogo} /> <a className="footer-text" href={TWITTER_LINK} target="_blank" rel="noreferrer" >{`built with @${TWITTER_HANDLE}`}</a> </div> </div> </div> ); }; export default App; 再次运行npm run start 启动项目,点击连接钱包后将页面作为本步骤作为截图提交。本步骤截图step4.校验角色是否有NFT 首先在src下面新建一个constants.js,写入下面的内容,需要替换你的合约地址。const CONTRACT_ADDRESS = '你的合约地址'; /* * Add this method and make sure to export it on the bottom! */ const transformCharacterData = (characterData) => { return { name: characterData.name, imageURI: characterData.imageURI, hp: characterData.hp.toNumber(), maxHp: characterData.maxHp.toNumber(), attackDamage: characterData.attackDamage.toNumber(), }; }; export { CONTRACT_ADDRESS, transformCharacterData }; 在src下新建一个MyEpicGame.json文件,同时去到你编写合约的replit的仓库找到artifacts/contracts/MyEpicGame.sol/MyEpicGame.json,将这个文件全部复制到src下面的MyEpicGame.json。使用你们自己的哦。 修改你的app.jsx替换为下面的。import React, { useEffect, useState } from 'react'; import './App.css'; import SelectCharacter from './Components/SelectCharacter'; import twitterLogo from './assets/twitter-logo.svg'; import { CONTRACT_ADDRESS, transformCharacterData } from './constants'; import myEpicGame from './MyEpicGame.json'; import { ethers } from 'ethers'; // Constants const TWITTER_HANDLE = '_buildspace'; const TWITTER_LINK = `https://twitter.com/${TWITTER_HANDLE}`; const App = () => { // State const [currentAccount, setCurrentAccount] = useState(null); const [characterNFT, setCharacterNFT] = useState(null); // Actions const checkIfWalletIsConnected = async () => { try { const { ethereum } = window; if (!ethereum) { console.log('Make sure you have MetaMask!'); return; } else { console.log('We have the ethereum object', ethereum); const accounts = await ethereum.request({ method: 'eth_accounts' }); if (accounts.length !== 0) { const account = accounts[0]; console.log('Found an authorized account:', account); setCurrentAccount(account); } else { console.log('No authorized account found'); } } } catch (error) { console.log(error); } }; // Render Methods const renderContent = () => { /* * Scenario #1 */ if (!currentAccount) { return ( <div className="connect-wallet-container"> <img src="https://64.media.tumblr.com/tumblr_mbia5vdmRd1r1mkubo1_500.gifv" alt="Monty Python Gif" /> <button className="cta-button connect-wallet-button" onClick={connectWalletAction} > Connect Wallet To Get Started </button> </div> ); /* * Scenario #2 */ } else if (currentAccount && !characterNFT) { return <SelectCharacter setCharacterNFT={setCharacterNFT} />; } }; /* * Implement your connectWallet method here */ const connectWalletAction = async () => { try { const { ethereum } = window; if (!ethereum) { alert('Get MetaMask!'); return; } /* * Fancy method to request access to account. */ const accounts = await ethereum.request({ method: 'eth_requestAccounts', }); /* * Boom! This should print out public address once we authorize Metamask. */ console.log('Connected', accounts[0]); setCurrentAccount(accounts[0]); } catch (error) { console.log(error); } }; const checkNetwork = async () => { try { if (window.ethereum.networkVersion !== '5') { alert("Please connect to Goerli!") } } catch(error) { console.log(error) } } useEffect(() => { checkIfWalletIsConnected(); checkNetwork(); }, []); useEffect(() => { const fetchNFTMetadata = async () => { console.log('Checking for Character NFT on address:', currentAccount); const provider = new ethers.providers.Web3Provider(window.ethereum); const signer = provider.getSigner(); const gameContract = new ethers.Contract( CONTRACT_ADDRESS, myEpicGame.abi, signer ); const txn = await gameContract.checkIfUserHasNFT(); if (txn.name) { console.log('User has character NFT'); setCharacterNFT(transformCharacterData(txn)); } else { console.log('No character NFT found'); } }; /* * We only want to run this, if we have a connected wallet */ if (currentAccount) { console.log('CurrentAccount:', currentAccount); fetchNFTMetadata(); } }, [currentAccount]); return ( <div className="App"> <div className="container"> <div className="header-container"> <p className="header gradient-text">⚔️ Metaverse Slayer ⚔️</p> <p className="sub-text">Team up to protect the Metaverse!</p> {/* This is where our button and image code used to be! * Remember we moved it into the render method. */} {renderContent()} </div> <div className="footer-container"> <img alt="Twitter Logo" className="twitter-logo" src={twitterLogo} /> <a className="footer-text" href={TWITTER_LINK} target="_blank" rel="noreferrer" >{`built with @${TWITTER_HANDLE}`}</a> </div> </div> </div> ); }; export default App; 当我们再次npm run start可以看到输出结果(No character NFT found),将这一步结果作本步骤的截图提交。切记如果你连接的钱包和你部署的合约的钱包是同一个,则会输(User has character NFT)。本步骤提交截图step5.构建角色选择页面 将我们的Components/SelectCharacter/index.js替换为下面的即可。import React, { useEffect, useState } from 'react'; import './SelectCharacter.css'; import { ethers } from 'ethers'; import { CONTRACT_ADDRESS, transformCharacterData } from '../../constants'; import myEpicGame from '../../MyEpicGame.json'; const SelectCharacter = ({ setCharacterNFT }) => { const [characters, setCharacters] = useState([]); const [gameContract, setGameContract] = useState(null); // UseEffect useEffect(() => { const { ethereum } = window; if (ethereum) { const provider = new ethers.providers.Web3Provider(ethereum); const signer = provider.getSigner(); const gameContract = new ethers.Contract( CONTRACT_ADDRESS, myEpicGame.abi, signer ); /* * This is the big difference. Set our gameContract in state. */ setGameContract(gameContract); } else { console.log('Ethereum object not found'); } }, []); useEffect(() => { const getCharacters = async () => { try { console.log('Getting contract characters to mint'); const charactersTxn = await gameContract.getAllDefaultCharacters(); console.log('charactersTxn:', charactersTxn); const characters = charactersTxn.map((characterData) => transformCharacterData(characterData) ); setCharacters(characters); } catch (error) { console.error('Something went wrong fetching characters:', error); } }; /* * Add a callback method that will fire when this event is received */ const onCharacterMint = async (sender, tokenId, characterIndex) => { console.log( `CharacterNFTMinted - sender: ${sender} tokenId: ${tokenId.toNumber()} characterIndex: ${characterIndex.toNumber()}` ); /* * Once our character NFT is minted we can fetch the metadata from our contract * and set it in state to move onto the Arena */ if (gameContract) { const characterNFT = await gameContract.checkIfUserHasNFT(); console.log('CharacterNFT: ', characterNFT); setCharacterNFT(transformCharacterData(characterNFT)); } }; if (gameContract) { getCharacters(); /* * Setup NFT Minted Listener */ gameContract.on('CharacterNFTMinted', onCharacterMint); } return () => { /* * When your component unmounts, let;s make sure to clean up this listener */ if (gameContract) { gameContract.off('CharacterNFTMinted', onCharacterMint); } }; }, [gameContract]); // Render Methods const renderCharacters = () => characters.map((character, index) => ( <div className="character-item" key={character.name}> <div className="name-container"> <p>{character.name}</p> </div> <img src={character.imageURI} alt={character.name} /> <button type="button" className="character-mint-button" onClick={()=> mintCharacterNFTAction(index)} >{`Mint ${character.name}`}</button> </div> )); // Actions const mintCharacterNFTAction = async (characterId) => { try { if (gameContract) { console.log('Minting character in progress...'); const mintTxn = await gameContract.mintCharacterNFT(characterId); await mintTxn.wait(); console.log('mintTxn:', mintTxn); } } catch (error) { console.warn('MintCharacterAction Error:', error); } }; return ( <div className="select-character-container"> <h2>Mint Your Hero. Choose wisely.</h2> {/* Only show this when there are characters in state */} {characters.length > 0 && ( <div className="character-grid">{renderCharacters()}</div> )} </div> ); }; export default SelectCharacter; 点击mint按钮即可去mint属于你的nft,去到opesnsea的测试网络找到这次你mint放入nft将链接作为本次的提交。mint结果os显示step6.构建竞技场 在Arena下面新建一个index.js,内容如下。import React, { useEffect, useState } from 'react'; import { ethers } from 'ethers'; import { CONTRACT_ADDRESS, transformCharacterData } from '../../constants'; import myEpicGame from '../../MyEpicGame.json'; import './Arena.css'; /* * We pass in our characterNFT metadata so we can show a cool card in our UI */ const Arena = ({ characterNFT }) => { // State const [gameContract, setGameContract] = useState(null); const [boss, setBoss] = useState(null); const [attackState, setAttackState] = useState(''); const runAttackAction = async () => { try { if (gameContract) { setAttackState('attacking'); console.log('Attacking boss...'); const attackTxn = await gameContract.attackBoss(); await attackTxn.wait(); console.log('attackTxn:', attackTxn); setAttackState('hit'); } } catch (error) { console.error('Error attacking boss:', error); setAttackState(''); } }; const Arena = ({ characterNFT, setCharacterNFT }) => { ... // UseEffects useEffect(() => { const fetchBoss = async () => { const bossTxn = await gameContract.getBigBoss(); console.log('Boss:', bossTxn); setBoss(transformCharacterData(bossTxn)); }; /* * Setup logic when this event is fired off */ const onAttackComplete = (from, newBossHp, newPlayerHp) => { const bossHp = newBossHp.toNumber(); const playerHp = newPlayerHp.toNumber(); const sender = from.toString(); console.log(`AttackComplete: Boss Hp: ${bossHp} Player Hp: ${playerHp}`); /* * If player is our own, update both player and boss Hp */ if (currentAccount === sender.toLowerCase()) { setBoss((prevState) => { return { ...prevState, hp: bossHp }; }); setCharacterNFT((prevState) => { return { ...prevState, hp: playerHp }; }); } /* * If player isn't ours, update boss Hp only */ else { setBoss((prevState) => { return { ...prevState, hp: bossHp }; }); } } if (gameContract) { fetchBoss(); gameContract.on('AttackComplete', onAttackComplete); } /* * Make sure to clean up this event when this component is removed */ return () => { if (gameContract) { gameContract.off('AttackComplete', onAttackComplete); } } }, [gameContract]); } useEffect(() => { /* * Setup async function that will get the boss from our contract and sets in state */ const fetchBoss = async () => { const bossTxn = await gameContract.getBigBoss(); console.log('Boss:', bossTxn); setBoss(transformCharacterData(bossTxn)); }; if (gameContract) { /* * gameContract is ready to go! Let's fetch our boss */ fetchBoss(); } }, [gameContract]); // UseEffects useEffect(() => { const { ethereum } = window; if (ethereum) { const provider = new ethers.providers.Web3Provider(ethereum); const signer = provider.getSigner(); const gameContract = new ethers.Contract( CONTRACT_ADDRESS, myEpicGame.abi, signer ); setGameContract(gameContract); } else { console.log('Ethereum object not found'); } }, []); return ( <div className="arena-container"> {/* Boss */} {boss && ( <div className="boss-container"> {/* Add attackState to the className! After all, it's just class names */} <div className={`boss-content ${attackState}`}> <h2>🔥 {boss.name} 🔥</h2> <div className="image-content"> <img src={boss.imageURI} alt={`Boss ${boss.name}`} /> <div className="health-bar"> <progress value={boss.hp} max={boss.maxHp} /> <p>{`${boss.hp} / ${boss.maxHp} HP`}</p> </div> </div> </div> <div className="attack-container"> <button className="cta-button" onClick={runAttackAction}> {`💥 Attack ${boss.name}`} </button> </div> </div> )} {/* Replace your Character UI with this */} {characterNFT && ( <div className="players-container"> <div className="player-container"> <h2>Your Character</h2> <div className="player"> <div className="image-content"> <h2>{characterNFT.name}</h2> <img src={characterNFT.imageURI} alt={`Character ${characterNFT.name}`} /> <div className="health-bar"> <progress value={characterNFT.hp} max={characterNFT.maxHp} /> <p>{`${characterNFT.hp} / ${characterNFT.maxHp} HP`}</p> </div> </div> <div className="stats"> <h4>{`⚔️ Attack Damage: ${characterNFT.attackDamage}`}</h4> </div> </div> </div> </div> )} </div> ); }; export default Arena; 同时去替换app.js,代码如下。import React, { useEffect, useState } from 'react'; import './App.css'; import SelectCharacter from './Components/SelectCharacter'; import twitterLogo from './assets/twitter-logo.svg'; import { CONTRACT_ADDRESS, transformCharacterData } from './constants'; import myEpicGame from './MyEpicGame.json'; import { ethers } from 'ethers'; import Arena from './Components/Arena'; // Constants const TWITTER_HANDLE = '_buildspace'; const TWITTER_LINK = `https://twitter.com/${TWITTER_HANDLE}`; const App = () => { // State const [currentAccount, setCurrentAccount] = useState(null); const [characterNFT, setCharacterNFT] = useState(null); // Actions const checkIfWalletIsConnected = async () => { try { const { ethereum } = window; if (!ethereum) { console.log('Make sure you have MetaMask!'); return; } else { console.log('We have the ethereum object', ethereum); const accounts = await ethereum.request({ method: 'eth_accounts' }); if (accounts.length !== 0) { const account = accounts[0]; console.log('Found an authorized account:', account); setCurrentAccount(account); } else { console.log('No authorized account found'); } } } catch (error) { console.log(error); } }; // Render Methods const renderContent = () => { if (!currentAccount) { return ( <div className="connect-wallet-container"> <img src="https://64.media.tumblr.com/tumblr_mbia5vdmRd1r1mkubo1_500.gifv" alt="Monty Python Gif" /> <button className="cta-button connect-wallet-button" onClick={connectWalletAction} > Connect Wallet To Get Started </button> </div> ); } else if (currentAccount && !characterNFT) { return <SelectCharacter setCharacterNFT={setCharacterNFT} />; /* * If there is a connected wallet and characterNFT, it's time to battle! */ } else if (currentAccount && characterNFT) { return <Arena characterNFT={characterNFT} setCharacterNFT={setCharacterNFT} />; } }; /* * Implement your connectWallet method here */ const connectWalletAction = async () => { try { const { ethereum } = window; if (!ethereum) { alert('Get MetaMask!'); return; } /* * Fancy method to request access to account. */ const accounts = await ethereum.request({ method: 'eth_requestAccounts', }); /* * Boom! This should print out public address once we authorize Metamask. */ console.log('Connected', accounts[0]); setCurrentAccount(accounts[0]); } catch (error) { console.log(error); } }; const checkNetwork = async () => { try { if (window.ethereum.networkVersion !== '5') { alert("Please connect to Goerli!") } } catch(error) { console.log(error) } } useEffect(() => { checkIfWalletIsConnected(); checkNetwork(); }, []); useEffect(() => { const fetchNFTMetadata = async () => { console.log('Checking for Character NFT on address:', currentAccount); const provider = new ethers.providers.Web3Provider(window.ethereum); const signer = provider.getSigner(); const gameContract = new ethers.Contract( CONTRACT_ADDRESS, myEpicGame.abi, signer ); const txn = await gameContract.checkIfUserHasNFT(); if (txn.name) { console.log('User has character NFT'); setCharacterNFT(transformCharacterData(txn)); } else { console.log('No character NFT found'); } }; /* * We only want to run this, if we have a connected wallet */ if (currentAccount) { console.log('CurrentAccount:', currentAccount); fetchNFTMetadata(); } }, [currentAccount]); return ( <div className="App"> <div className="container"> <div className="header-container"> <p className="header gradient-text">⚔️ Metaverse Slayer ⚔️</p> <p className="sub-text">Team up to protect the Metaverse!</p> {/* This is where our button and image code used to be! * Remember we moved it into the render method. */} {renderContent()} </div> <div className="footer-container"> <img alt="Twitter Logo" className="twitter-logo" src={twitterLogo} /> <a className="footer-text" href={TWITTER_LINK} target="_blank" rel="noreferrer" >{`built with @${TWITTER_HANDLE}`}</a> </div> </div> </div> ); }; export default App; 大家可以试玩下,帝哥玩了两次后小李子,已经挂了。。。,因为官方需要你的人物死的截图,真变态。本步骤截图5.优化网站step1.ui改变,提示等 因为这一步的代码太多了,我将自己的仓库公开了,大家去replit复制即可,地址在下面: https://replit.com/@paulCoinman/buildspace-nft-game-starter-coinmanlabs?v=1 需要复制的文件:app.jsComponents/SelectCharacter/index.js和cssArena的index.js和css最终的结果,大家后面的可以选择的做。最终结果关注coinmanlabs获取区块链最新讯息。 推特:coinmanlabs 公众号:coinmanlabs 加客服微信进群了解更多信息。客服微信 ## Publication Information - [Coinman.eth](https://paragraph.com/@coinman-eth/): Publication homepage - [All Posts](https://paragraph.com/@coinman-eth/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@coinman-eth): Subscribe to updates