Crie um jogo descentralizado com a blockchain da ethereum.
Foto por Zoltan Tasi no Unsplash
Neste artigo, veremos o passo a passo de como criar um jogo descentralizado usando a blockchain pública Ethereum usando:
- Hardhat
- Solidity
- Vue.js
Vamos nos concentrar mais no lado do frontend na próxima parte, mas primeiro, darei uma breve explicação sobre o lado do Solidity e como implantá-lo no testnet
Rinkeby .
Este é meu primeiro artigo e sinto que faltam informações sobre Web3 e Vue.js, pois também precisam de atenção.
Antes de começarmos, quero dar crédito ao buildspace por ter feito este projeto. O que eu adicionei é a parte do Vue.js. Se você é novo neste espaço, sinta-se à vontade para dar uma olhada! Eles têm as melhores ferramentas de aprendizado e uma boa comunidade!
Então, antes de começarmos, vamos falar sobre o que você realmente vai precisar se estiver começando neste espaço:
Você precisa instalar a MetaMask e habilitar as extensões no Chrome
Conhecimento básico sobre Metamask
Conhecimentos básicos sobre Solidity
Conhecimento de JavaScript e Vue.js.
O que vamos construir hoje
Construiremos um jogo baseado em blockchain (inspirado por buildspace
) onde você pode mintar seu personagem e lutar contra o chefe!
Você pode verificar os resultados aqui:
Solidity
Para iniciantes em Solidity, recomendo que você siga o buildspace.
Nosso contrato inteligente nos permitirá criar personagens, mintar o nosso personagem selecionado e depois lutar contra um chefe com ele! Simples né?
Aqui está nosso contrato inteligente MyEpicGame.sol
:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "./libraries/Base64.sol";
import "hardhat/console.sol";
contract MyEpicGame is ERC721 {
struct CharacterAttributes {
uint256 characterIndex;
string name;
string imageURI;
uint256 hp;
uint256 maxHp;
uint256 attackDamage;
}
struct BigBoss {
string name;
string imageURI;
uint256 hp;
uint256 maxHp;
uint256 attackDamage;
}
BigBoss public bigBoss;
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
CharacterAttributes[] defaultCharacters;
mapping(uint256 => CharacterAttributes) public nftHolderAttributes;
mapping(address => uint256) public nftHolders;
event CharacterNFTMinted(
address sender,
uint256 tokenId,
uint256 characterIndex
);
event AttackComplete(uint256 newBossHp, uint256 newPlayerHp);
constructor(
string[] memory characterNames,
string[] memory characterImageURIs,
uint256[] memory characterHp,
uint256[] memory characterAttackDmg,
string memory bossName,
string memory bossImageURI,
uint256 bossHp,
uint256 bossAttackDamage
) ERC721("Heroes", "HERO") {
for (uint256 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
);
}
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
);
_tokenIds.increment();
}
function mintCharacterNFT(uint256 _characterIndex) external {
uint256 newItemId = _tokenIds.current();
_safeMint(msg.sender, newItemId);
nftHolderAttributes[newItemId] = CharacterAttributes({
characterIndex: _characterIndex,
name: defaultCharacters[_characterIndex].name,
imageURI: defaultCharacters[_characterIndex].imageURI,
hp: defaultCharacters[_characterIndex].hp,
maxHp: defaultCharacters[_characterIndex].hp,
attackDamage: defaultCharacters[_characterIndex].attackDamage
});
console.log(
"Minted NFT w/ tokenId %s and characterIndex %s",
newItemId,
_characterIndex
);
nftHolders[msg.sender] = newItemId;
_tokenIds.increment();
emit CharacterNFTMinted(msg.sender, newItemId, _characterIndex);
}
function attackBoss() public {
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
);
require(player.hp > 0, "Error: character must have HP to attack boss.");
require(bigBoss.hp > 0, "Error: boss must have HP to attack boss.");
if (bigBoss.hp < player.attackDamage) {
bigBoss.hp = 0;
} else {
bigBoss.hp = bigBoss.hp - player.attackDamage;
}
if (player.hp < bigBoss.attackDamage) {
player.hp = 0;
} else {
player.hp = player.hp - bigBoss.attackDamage;
}
console.log("Boss attacked player. New player hp: %s\n", player.hp);
emit AttackComplete(bigBoss.hp, player.hp);
}
function checkIfUserHasNFT()
public
view
returns (CharacterAttributes memory)
{
uint256 userNftTokenId = nftHolders[msg.sender];
if (userNftTokenId > 0) {
return nftHolderAttributes[userNftTokenId];
} else {
CharacterAttributes memory emptyStruct;
return emptyStruct;
}
}
function getAllDefaultCharacters()
public
view
returns (CharacterAttributes[] memory)
{
return defaultCharacters;
}
function getBigBoss() public view returns (BigBoss memory) {
return bigBoss;
}
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(
bytes(
string(
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;
}
}
Para o arquivo Base64.sol
, você pode encontrá-lo aqui.
Isso basicamente nos fornece algumas funções auxiliares para nos permitir codificar qualquer dado em uma string Base64 — que é uma maneira padrão de codificar alguns dados em uma string.
Teste
Antes de implantar. Devemos testar o contrato para ter certeza de que podemos usá-lo.
Crie uma nova pasta chamada [test](https://buildspace.so/)
no diretório raiz. Essa pasta pode conter testes do lado do cliente e do Ethereum.
Dentro da pasta test
, adicione um novo arquivo JS chamado test.js
. Este arquivo conteria os testes de contratos em um arquivo. Você pode criar o seu próprio, eu crio um arquivo de teste simples:
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("MyEpicGame", function () {
let gameContract;
before(async () => {
const gameContractFactory = await ethers.getContractFactory("MyEpicGame");
gameContract = await gameContractFactory.deploy(
["Leo", "Aang", "Pikachu"],
[
"<https://i.imgur.com/pKd5Sdk.png>",
"<https://i.imgur.com/xVu4vFL.png>",
"<https://i.imgur.com/WMB6g9u.png>",
],
[100, 200, 300],
[100, 50, 25],
"Elon Musk",
"<https://i.imgur.com/AksR0tt.png>",
10000,
50
);
await gameContract.deployed();
});
it("Should have 3 default characters", async () => {
let characters = await gameContract.getAllDefaultCharacters();
expect(characters.length).to.equal(3);
});
it("Should have a boss", async () => {
let boss = await gameContract.getBigBoss();
expect(boss.name).to.equal("Elon Musk");
});
});
para iniciar o teste:
npx hardhat test
Implantação (na rede de teste Rinkeby)
Vamos criar um novo arquivo deploy.js
na pasta de scripts do nosso projeto hardhat. E coloque esse código nele.
Isso criará 3 caracteres padrão e um chefe do nosso construtor.
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);
};
const runMain = async () => {
try {
await main();
process.exit(0);
} catch (error) {
console.log(error);
process.exit(1);
}
};
runMain();
Para implantar o contrato, execute este comando:
npx hardhat run scripts/deploy.js --network rinkeby
E terminamos com a nossa parte Solidity. Agora temos que fazer uma interface frontend para interagir com ele.
Front end vue.js
Não vou compartilhar o CSS aqui, fique à vontade para verificar no meu repositório do GitHub.
Vamos começar criando nosso projeto:
vue create frontend
cd frontend
Usaremos ethers para nossas interações Web3 e Vuex para nosso gerenciamento de estado. Veja como instalá-los:
npm install --save vuex ethers
Pronto, agora o projeto está pronto para começar! Vamos falar sobre as etapas pelas quais passaremos para fazer nosso aplicativo frontend:
- Conecte a carteira do usuário
- Escolha um personagem
- Ataque o chefe
Conecte a Carteira
Para que os usuários possam interagir com nosso aplicativo, eles devem ter a Metamask instalada e a rede Rinkeby selecionada. Mas vamos cuidar disso na última parte.
Nosso modelo App.vue
deverá ficar assim, com um botão de conexão que abrirá um prompt na Metamask para permitir que nosso aplicativo solicite transações para o usuário aceitar:
<template>
<div class="app" id="app">
<div class="container mx-auto">
<div class="header-container">
<p class="header gradient-text">⚔️ Metaverse Slayer ⚔️</p>
<p class="sub-text">Team up to protect the Metaverse!</p>
<div class="connect-wallet-container">
<img
src="<https://64.media.tumblr.com/tumblr_mbia5vdmRd1r1mkubo1_500.gifv>"
alt="Monty Python Gif"
/>
<button class="cta-button connect-wallet-button" @click="connect">
Connect Wallet To Get Started
</button>
</div>
</div>
<div class="footer-container">
<img
alt="Twitter Logo"
class="twitter-logo"
src="./assets/twitter-logo.svg"
/>
<a
class="footer-text"
:href="twitter_link"
target="_blank"
rel="noreferrer"
>built by @{{ twitter_handle }}</a
>
</div>
</div>
</div>
</template>
<script>
export default {
name: "App",
data() {
return {
twitter_handle: "zouln96",
twitter_link: "<https://twitter.com/zouln96>",
};
},
methods: {
async connect() {
await this.$store.dispatch("connect", true);
},
},
async mounted() {
await this.$store.dispatch("connect", false);
},
};
</script>
O botão connect tem um evento de clique que irá despachar uma ação para nossa loja (Vuex), falaremos sobre isso mais tarde — por enquanto, vamos ver a estrutura da nossa loja:
<template>
<div class="app" id="app">
<div class="container mx-auto">
<div class="header-container">
<p class="header gradient-text">⚔️ Metaverse Slayer ⚔️</p>
<p class="sub-text">Team up to protect the Metaverse!</p>
<div class="connect-wallet-container">
<img
src="<https://64.media.tumblr.com/tumblr_mbia5vdmRd1r1mkubo1_500.gifv>"
alt="Monty Python Gif"
/>
<button class="cta-button connect-wallet-button" @click="connect">
Connect Wallet To Get Started
</button>
</div>
</div>
<div class="footer-container">
<img
alt="Twitter Logo"
class="twitter-logo"
src="./assets/twitter-logo.svg"
/>
<a
class="footer-text"
:href="twitter_link"
target="_blank"
rel="noreferrer"
>built by @{{ twitter_handle }}</a
>
</div>
</div>
</div>
</template>
<script>
export default {
name: "App",
data() {
return {
twitter_handle: "zouln96",
twitter_link: "<https://twitter.com/zouln96>",
};
},
methods: {
async connect() {
await this.$store.dispatch("connect", true);
},
},
async mounted() {
await this.$store.dispatch("connect", false);
},
};
</script>
O botão connect tem um evento click que irá despachar uma ação para nossa loja (Vuex), falaremos sobre isso mais tarde — por enquanto, vamos ver a estrutura da nossa loja:
import Vue from "vue";
import Vuex from "vuex";
import { ethers } from "ethers";
import MyEpicGame from "../utils/MyEpicGame.json";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
account: null,
error: null,
mining: false,
characterNFT: null,
characters: [],
boss: null,
attackState: null,
contract_address: "0x91b5483e35EC485C68FF33f0ACfD51a26F3F1EcA",
},
getters: {
account: (state) => state.account,
error: (state) => state.error,
mining: (state) => state.mining,
characterNFT: (state) => state.characterNFT,
characters: (state) => state.characters,
boss: (state) => state.boss,
attackState: (state) => state.attackState,
},
mutations: {
setAccount(state, account) {
state.account = account;
},
setError(state, error) {
state.error = error;
},
setMining(state, mining) {
state.mining = mining;
},
setCharacterNFT(state, characterNFT) {
state.characterNFT = characterNFT;
},
setCharacters(state, characters) {
state.characters = characters;
},
setBoss(state, boss) {
state.boss = boss;
},
setAttackState(state, attackState) {
state.attackState = attackState;
},
},
actions: {},
});
O objeto de estado tem os seguintes atributos:
-
account
: onde nossa conta conectada será salva -
error
: para exibir erros -
mining
: um booleano para verificar se uma transação está sendo minerada -
characterNFT
: onde nosso personagem selecionado será salvo -
characters
: onde os caracteres padrão serão salvos -
boss
: o chefe que vai lutar com nosso personagem -
attackState
: ao atacar o chefe, o estado muda enquanto a transação está sendo minerada -
contract_address
: o endereço que foi retornado quando implantamos o contrato na rede Rinkeby.
E não se esqueça de importar MyEpicGame.json
da compilação após implantar o contrato. Precisaremos dele para nossas chamadas web3 com o contrato na blockchain.
Criamos getters e setters (mutações) para os estados. Agora vamos às nossas ações.
Para começar, temos a ação conectar
da qual falamos anteriormente, que vou detalhar para você agora:
actions: {
async connect({ commit, dispatch }, connect) {
try {
const { ethereum } = window;
if (!ethereum) {
commit("setError", "Metamask not installed!");
return;
}
if (!(await dispatch("checkIfConnected")) && connect) {
await dispatch("requestAccess");
}
await dispatch("checkNetwork");
} catch (error) {
console.log(error);
commit("setError", "Account request refused.");
}
},
async checkNetwork({ commit, dispatch }) {
let chainId = await ethereum.request({ method: "eth_chainId" });
const rinkebyChainId = "0x4";
if (chainId !== rinkebyChainId) {
if (!(await dispatch("switchNetwork"))) {
commit(
"setError",
"You are not connected to the Rinkeby Test Network!"
);
}
}
},
async switchNetwork() {
try {
await ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: "0x4" }],
});
return 1;
} catch (switchError) {
return 0;
}
},
async checkIfConnected({ commit }) {
const { ethereum } = window;
const accounts = await ethereum.request({ method: "eth_accounts" });
if (accounts.length !== 0) {
commit("setAccount", accounts[0]);
return 1;
} else {
return 0;
}
},
async requestAccess({ commit }) {
const { ethereum } = window;
const accounts = await ethereum.request({
method: "eth_requestAccounts",
});
commit("setAccount", accounts[0]);
},
}
Primeiro, verificamos se a Metamask está instalada:
const { ethereum } = window;
if (!ethereum) {
commit("setError", "Metamask not installed!");
return;
}
Se estiver tudo certo, verificamos se o usuário já concedeu ao nosso aplicativo acesso à Metamask, então basta conectar a conta, caso contrário, retorna 0, o número de contas encontradas. Isso significa que teremos que solicitar acesso do usuário:
if (!(await dispatch("checkIfConnected")) && connect) {
await dispatch("requestAccess");
}
Observação: a variável connect
nos ajuda a saber se é um botão clicado ou se realmente será a função montada que a está chamando
Após verificarmos a rede selecionada, caso não seja a rede Rinkeby, enviamos uma solicitação para alterá-la:
await dispatch("checkNetwork");
Assim que a conta for encontrada, comprometemos a conta com a mutação para salvá-la em nosso estado:
// in checkIfConnected action
commit("setAccount", accounts[0]);
E é isso para nossa ação de conexão.
Agora vamos criar uma ação para obter os caracteres padrão para nosso usuário escolher em nosso contrato inteligente:
async getCharacters({ state, commit, dispatch }) {
try {
const connectedContract = await dispatch("getContract");
const charactersTxn = await connectedContract.getAllDefaultCharacters();
const characters = charactersTxn.map((characterData) =>
transformCharacterData(characterData)
);
commit("setCharacters", characters);
} catch (error) {
console.log(error);
}
},
Para chamar uma função do nosso contrato, precisamos buscar o contrato, criando uma ação para isso também, e retorná-lo. Fornecemos um provedor, o API do contrato e o signatário:
async getContract({ state }) {
try {
const { ethereum } = window;
const provider = new ethers.providers.Web3Provider(ethereum);
const signer = provider.getSigner();
const connectedContract = new ethers.Contract(
state.contract_address,
MyEpicGame.abi,
signer
);
return connectedContract;
} catch (error) {
console.log(error);
console.log("connected contract not found");
return null;
}
},
Em seguida, podemos chamar a função em nosso contrato inteligente que retorna os caracteres padrão e mapear em cada um com a ajuda de nossa função que transforma os dados do caractere em um objeto utilizável por JavaScript:
const charactersTxn = await connectedContract.getAllDefaultCharacters();
const characters = charactersTxn.map((characterData) =>
transformCharacterData(characterData)
);
A função transformCharacterData
é adicionada à inicialização do Vuex.Store. Ela transforma o hp
, attackDamage
de bigNumber
para números legíveis:
const transformCharacterData = (characterData) => {
return {
name: characterData.name,
imageURI: characterData.imageURI,
hp: characterData.hp.toNumber(),
maxHp: characterData.maxHp.toNumber(),
attackDamage: characterData.attackDamage.toNumber(),
};
};
Agora vamos voltar ao nosso App.vue
para configurar nossas visualizações e criar um componente chamado SelectCharacter
.
Modifique nosso App.vue
, para que quando o usuário conectar sua carteira, tenhamos uma conta salva em nossa loja e ele possa escolher o personagem entre os padrões que buscamos anteriormente.
Adicione um v-if
ao nosso suporte de div de conexão e adicione nosso componente de seleção de caracteres na visualização:
<div class="connect-wallet-container" v-if="!account">
<img
src="<https://64.media.tumblr.com/tumblr_mbia5vdmRd1r1mkubo1_500.gifv>"
alt="Monty Python Gif"
/>
<button class="cta-button connect-wallet-button" @click="connect">
Connect Wallet To Get Started
</button>
</div>
<select-character v-else-if="account" />
E para a conta, na verdade é uma variável computada que é retornada de nossa loja:
computed: {
account() {
return this.$store.getters.account;
},
}
Chegando ao nosso componente SelectCharacter
:
<template>
<div class="select-character-container">
<h2 class="mt-5">Mint Your Hero. Choose wisely.</h2>
<div v-if="characters.length && !minting" class="character-grid">
<div
class="character-item cursor-pointer mt-10"
:key="character.name"
v-for="(character, index) in characters"
>
<div class="name-container">
<p>{{ character.name }}</p>
</div>
<img :src="character.imageURI" :alt="character.name" />
<button
type="button"
class="character-mint-button"
@click="mintCharacterNFTAction(index)"
>
{{ `Mint ${character.name}` }}
</button>
</div>
</div>
<div class="loading" v-else>
<div class="indicator">
<loading-indicator />
<p>Minting In Progress...</p>
</div>
<img
src="<https://media2.giphy.com/media/61tYloUgq1eOk/giphy.gif?cid=ecf05e47dg95zbpabxhmhaksvoy8h526f96k4em0ndvx078s&rid=giphy.gif&ct=g>"
alt="Minting loading indicator"
/>
</div>
</div>
</template>
<script>
import LoadingIndicator from "./LoadingIndicator.vue";
export default {
data() {
return {
minting: false,
};
},
components: {
LoadingIndicator,
},
methods: {
async mintCharacterNFTAction(index) {
if (this.minting) return;
this.minting = true;
await this.$store.dispatch("mintCharacterNFT", index);
this.minting = false;
},
},
async mounted() {
this.minting = true;
await this.$store.dispatch("getCharacters");
this.minting = false;
},
computed: {
characters() {
return this.$store.getters.characters;
},
},
};
</script>
Uma vez que o componente é montado, temos que buscar os defaultCharacters
e exibi-los em nossa visão.
Para cada item, temos um evento de clique que enviará uma ação mint para nossa loja chamada mintCharacterNFT
com base no characterId
ou índice selecionado. Vamos adicionar esta ação à nossa loja:
async mintCharacterNFT({ commit, dispatch }, characterId) {
try {
const connectedContract = await dispatch("getContract");
const mintTxn = await connectedContract.mintCharacterNFT(characterId);
await mintTxn.wait();
} catch (error) {
console.log(error);
}
},
Como antes, chamamos nossa função de contrato inteligente responsável pela mintagem.
Mas há um problema aqui, não definimos nosso personagem mintado em nosso estado? Não se preocupe, se você se lembrar da nossa função no contrato inteligente, teremos um evento assim que o personagem for mintado CharacterNFTMinted
.
Então, o que temos que fazer agora é ouvir esse evento e definir o personagem dele. Vamos criar uma ação para configurar nossos ouvintes de eventos:
async setupEventListeners({ state, commit, dispatch }) {
try {
const connectedContract = await dispatch("getContract");
if (!connectedContract) return;
connectedContract.on(
"CharacterNFTMinted",
async (from, tokenId, characterIndex) => {
console.log(
`CharacterNFTMinted - sender: ${from} tokenId: ${tokenId.toNumber()} characterIndex: ${characterIndex.toNumber()}`
);
const characterNFT = await connectedContract.checkIfUserHasNFT();
console.log(characterNFT);
commit("setCharacterNFT", transformCharacterData(characterNFT));
alert(
`Your NFT is all done -- see it here: <https://testnets.opensea.io/assets/$>{
state.contract_address
}/${tokenId.toNumber()}`
);
}
);
} catch (error) {
console.log(error);
}
},
Para ouvir um evento na web3, basta usar o contract.on("event_name", callback)
.
Dentro do evento, verificamos o usuário NFT selecionado com esta função checkIfUserHasNFT
e o submetemos ao nosso estado. O alerta é apenas uma informação adicional se o usuário quiser ver o link NFT. Então, onde você acha que essa ação deve ser chamada?
Vamos adicioná-lo à nossa ação de conexão abaixo do despacho checkNetwork
:
await dispatch("setupEventListeners");
await dispatch("fetchNFTMetadata");
Vamos também adicionar outra ação para verificar se o usuário já possui um NFT ao acessar nosso aplicativo:
async fetchNFTMetadata({ state, commit, dispatch }) {
try {
const connectedContract = await dispatch("getContract");
const txn = await connectedContract.checkIfUserHasNFT();
if (txn.name) {
commit("setCharacterNFT", transformCharacterData(txn));
}
} catch (error) {
console.log(error);
}
},
Esta ação é quase igual ao evento, mas só a verifica quando é chamada.
Agora que terminamos nossa seleção de personagens, vamos voltar ao nosso App.vue
e configurar nossa Arena para lutar contra o chefe. Temos que modificar nosso filho select-character chamado em App.vue
, se o usuário já tiver um NFT selecionado, temos que ir direto para a arena:
<select-character v-else-if="account && !characterNFT" />
<arena v-else-if="account && characterNFT" />
A variável characterNFT
é a variável calculada como conta:
characterNFT() {
return this.$store.getters.characterNFT;
},
Vamos criar o componente Arena
<template>
<div class="arena-container">
<div class="boss-container" v-if="boss">
<div :class="`boss-content ${attackState}`">
<h2>🔥 {{ boss.name }} 🔥</h2>
<div class="image-content">
<img :src="boss.imageURI" :alt="`Boss ${boss.name}`" />
<div class="health-bar">
<progress :value="boss.hp" :max="boss.maxHp" />
<p>{{ `${boss.hp} / ${boss.maxHp} HP` }}</p>
</div>
</div>
</div>
<div class="attack-container">
<button class="cta-button" @click="attackAction">
{{ `💥 Attack ${boss.name}` }}
</button>
<div class="loading-indicator" v-if="attackState === 'attacking'">
<LoadingIndicator />
<p>Attacking ⚔️</p>
</div>
</div>
</div>
<div class="players-container" v-if="characterNFT">
<div class="player-container">
<h2>Your Character</h2>
<div class="player">
<div class="image-content">
<h2>{{ characterNFT.name }}</h2>
<img
:src="characterNFT.imageURI"
:alt="`Character
${characterNFT.name}`"
/>
<div class="health-bar">
<progress :value="characterNFT.hp" :max="characterNFT.maxHp" />
<p>{{ `${characterNFT.hp} / ${characterNFT.maxHp} HP` }}</p>
</div>
</div>
<div class="stats">
<h4>{{ `⚔️ Attack Damage: ${characterNFT.attackDamage}` }}</h4>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import LoadingIndicator from "./LoadingIndicator.vue";
export default {
components: {
LoadingIndicator,
},
methods: {
async attackAction() {
await this.$store.dispatch("attackBoss");
},
},
async mounted() {
await this.$store.dispatch("fetchBoss");
},
computed: {
boss() {
return this.$store.getters.boss;
},
characterNFT() {
return this.$store.getters.characterNFT;
},
attackState() {
return this.$store.getters.attackState;
},
},
};
</script>
Uma vez que este componente é montado, chamamos uma ação para buscar o chefe e outra ação quando o botão de ataque é clicado, e é aí que o attackState
se altera entre (atacar/acertar):
async fetchBoss({ state, commit, dispatch }) {
try {
const connectedContract = await dispatch("getContract");
const bossTxn = await connectedContract.getBigBoss();
commit("setBoss", transformCharacterData(bossTxn));
} catch (error) {
console.log(error);
}
},
async attackBoss({ state, commit, dispatch }) {
try {
const connectedContract = await dispatch("getContract");
commit("setAttackState", "attacking");
console.log("Attacking boss...");
const attackTxn = await connectedContract.attackBoss();
await attackTxn.wait();
console.log("attackTxn:", attackTxn);
commit("setAttackState", "hit");
} catch (error) {
console.error("Error attacking boss:", error);
setAttackState("");
}
},
E não vamos esquecer nosso evento attackComplete
em nossa ação setupEventListeners
, isso atualiza o chefe e o jogador hp
:
connectedContract.on(
"AttackComplete",
async (newBossHp, newPlayerHp) => {
console.log(
`AttackComplete: Boss Hp: ${newBossHp} Player Hp: ${newPlayerHp}`
);
let boss = state.boss;
boss.hp = newBossHp;
commit("setBoss", boss); let character = state.characterNFT;
character.hp = newPlayerHp;
commit("setCharacterNFT", character);
}
);
Você pode adicionar este componente indicador de carregamento para melhor UX:
<template>
<div class="lds-ring">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</template><script>
export default {};
</script>
Agora, você completou seu primeiro jogo web3 usando Vue.js. Você pode hospedá-lo no vercel
como eu fiz - de graça.
Aqui está o meu aplicativo e o Repositório do GitHub para o código fonte completo.
Novamente, um grande abraço ao buildspace por ajudar a fazer este projeto!
Também recebi este NFT por concluir o projeto:
Este artigo foi criado por Zouheir Layine e traduzido por aiengineer13 siga este link
Latest comments (0)