WEB3DEV

Cover image for Crie um jogo de Blockchain com solidity,Web3, e Vue.js
AIengineer13
AIengineer13

Posted on

Crie um jogo de Blockchain com solidity,Web3, e Vue.js

Crie um jogo descentralizado com a blockchain da ethereum.

Image description

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;

}

}



Enter fullscreen mode Exit fullscreen mode

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");

});

});


Enter fullscreen mode Exit fullscreen mode

para iniciar o teste:

npx hardhat test  

Enter fullscreen mode Exit fullscreen mode

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();


Enter fullscreen mode Exit fullscreen mode

Para implantar o contrato, execute este comando:

npx hardhat run scripts/deploy.js --network rinkeby
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Usaremos ethers para nossas interações Web3 e Vuex para nosso gerenciamento de estado. Veja como instalá-los:

npm install --save vuex ethers  

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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: {},

});

Enter fullscreen mode Exit fullscreen mode

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]);

},

}


Enter fullscreen mode Exit fullscreen mode

Primeiro, verificamos se a Metamask está instalada:


const { ethereum } = window;  
if (!ethereum) {  
  commit("setError", "Metamask not installed!");  
  return;  
}

Enter fullscreen mode Exit fullscreen mode

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");  
} 

Enter fullscreen mode Exit fullscreen mode

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]);

Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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;  
  }  
}, 



Enter fullscreen mode Exit fullscreen mode

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)  
);


Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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" />

Enter fullscreen mode Exit fullscreen mode

E para a conta, na verdade é uma variável computada que é retornada de nossa loja:

computed: {  
  account() {  
    return this.$store.getters.account;  
  },  
} 

Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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");

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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" />

Enter fullscreen mode Exit fullscreen mode

A variável characterNFT é a variável calculada como conta:

characterNFT() {  
  return this.$store.getters.characterNFT;  
},


Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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




Enter fullscreen mode Exit fullscreen mode

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);  
  }  
);

Enter fullscreen mode Exit fullscreen mode

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>  

Enter fullscreen mode Exit fullscreen mode

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:

Image description

Este artigo foi criado por Zouheir Layine e traduzido por aiengineer13 siga este link

Latest comments (0)