Como construir um jogo Play To Earn no NEAR Protocol

18 min read
To Share and +4 nLEARNs

Revisão detalhada do protótipo atual — BerryClub

Este guia te faz entender a dinâmica básica que permitirá que você crie um jogo Play to Earn no Near Protocol.

Não queremos deixar você ou seus jogadores ricos, mas a mecânica do Near funciona de forma que pequenas transações sejam muito úteis em uma interface HTML5 simples e criem um jogo de farming simples e divertido. Farming cultivo do inglês,seria no sentido de fazer uma atividade no jogo para receber ganhos em criptomoedas.

Analisaremos um contrato inteligente e os arquivos js/rust por trás de um jogo de farming existente,  berryclub. Se você aplicar a mesma lógica às suas ideias de jogo, poderá obter resultados ainda mais divertidos!

Para seguir o lado técnico deste tutorial, recomendamos verificar o figment Near Pathway para  construir uma transação  e  construir seu primeiro contrato inteligente em Near. Os conceitos presentes nesses guias não estão incluídos neste tutorial. Será muito mais fácil seguir este tutorial se você mantiver uma guia aberta no arquivo de origem vinculado enquanto lê a explicação, porque grande parte do código mencionado é referenciado com números de linha, mas não reproduzido aqui. O repositório original do BerryClub foi bifurcado (fork) para este tutorial a fim de manter esse esforço válido desde que o código ainda funcione no blockchain; como você verá na próxima seção, o jogo em si evoluiu ao longo do tempo e estará evoluindo novamente, queríamos enfatizar seu estado agora.

A interface do Berryclub

Berryclub é construído com React, então a primeira coisa que vamos fazer é entrar no  arquivo app.js  localizado na pasta src do repositório do github, isso nos poupará tempo na análise do contrato, permitindo que nos concentremos no que precisamos extrapolar a lógica fora do jogo real (que é divertido e jogado por uma grande comunidade).

Depois de importar o react a primeira biblioteca necessária é o bn.js  um utilitário simples para gerenciar números inteiros ou não decimais, muitos recursos podem chegar à sua interface com esta biblioteca, antes de tudo isto aqui é usado para gerenciar as transações:

const PixelPrice = new BN(“10000000000000000000000”);

O jogo Berryclub é baseado na economia imobiliária, existe um tabuleiro administrado por uma parte do contrato chamado board.rs, ele é subdividido em pixels, e cada pixel tem um preço que deve ser pago para poder sacar nele. As mecânicas da ação de “empate” são o núcleo das habilidades de farming e autossustentação do jogo e serão analisadas em profundidade quando chegarmos ao contrato inteligente.

Como você pode ver aqui, o preço de um pixel é declarado como uma constante no início de nosso aplicativo e pode ser modificado usando as ferramentas de frontend e a biblioteca bn.j. A segunda importação é o sdk near, que nos permite interagir com o blockchain Near, conforme explicado no caminho do NEAR Figment. O primeiro uso da API Near é declarar as variáveis ​​de contrato usadas e garantir que a rede principal seja usada quando o código for executado a partir da url do Berryclub:

const IsMainnet = window.location.hostname === "berryclub.io";
const TestNearConfig = {
  networkId: "testnet",
  nodeUrl: "https://rpc.testnet.near.org",
  contractName: "berryclub.testnet",
  walletUrl: "https://wallet.testnet.near.org",
};
const MainNearConfig = {
  networkId: "mainnet",
  nodeUrl: "https://rpc.mainnet.near.org",
  contractName: "berryclub.ek.near",
  walletUrl: "https://wallet.near.org",
};
const NearConfig = IsMainnet ? MainNearConfig : TestNearConfig;

Em seguida, importamos os utilitários react UI para construir nossa interface e permitir que as pessoas desenhem,  react-colorreact-switch  e  react-compound-timer. O primeiro desses utilitários que é usado é o timer, ele é usado para definir um tempo limite para atualizar a tabuleiro na linha 62.

A “atualização” do tabuleiro é feita pelo frontend para exibir o estado atualizado do tabuleiro usando uma chamada RPC para o contrato inteligente.

const BatchTimeout = 500;
const RefreshBoardTimeout = 1000;
const MaxWorkTime = 10 * 60 * 1000;
const OneDayMs = 24 * 60 * 60 * 1000;

O que vemos aqui são duas const a mais do que as necessárias para atualizar, as duas últimas na verdade são usadas para o farming dos pixels depois de desenhá-los e definir um intervalo de tempo de um dia para calcular as recompensas. Outras consts também são declaradas para gerenciar a tabuleiro de acordo com o contrato inteligente, e aqui, pela primeira vez, cruzamos o conceito de linhas, que será muito importante para entender a gestão do tabuleiro e é o item mais reutilizável de toda a interface:

const BoardHeight = 50;
const BoardWidth = 50;
const NumLinesPerFetch = 50;
const ExpectedLineLength = 4 + 8 * BoardWidth;

Como você pode ver, depois de subdividir a tabuleiro 50×50, informamos nossa interface para buscar apenas as linhas seguindo a instrução RefreshBoardTimeout e considerar o comprimento de cada linha como Boardwidth multiplicado por 12, o tamanho de uma única célula.

const CellWidth = 12;
const CellHeight = 12;
const MaxNumColors = 31;
const BatchOfPixels = 100;

Os pixels são considerados em lote, não de forma independente, tanto quando a ação de desenho é chamada quanto quando a interface é atualizada.

Por último, mas não menos importante, nossas importações incluem um componente de interface do usuário personalizado, chamado Weapons,js: este componente foi desenvolvido posteriormente na história da comunidade berryclub, para que cada usuário possa fazer upload e desenhar uma imagem inteira no quadro, e cunhar no mercado de NFT berrycards.

Mecânica DeFi

As linhas entre 27 e 51 são uma referência útil de como este Dapp constrói suas habilidades de farming sobre algumas mecânicas básicas de DeFi que serão analisadas nas últimas partes deste tutorial. Até agora, apenas mencionamos brevemente que, para desenhar/comprar um pixel, o berryclub canaliza você através de algumas operações DeFi no ref.finance usando seus próprios Tokens específicos, abacates para comprar pixels e bananas obtidas com os pixels que você comprou.

Existe um clone uniswap simples criado para trocar por bananas/abacates que funcionou no mesmo contrato inteligente construído para os outros tokens deste jogo/protótipo. Há também um token de farming criado para o jogo, chamado pepino, ele permite que as pessoas ganhem uma parte dos tokens que toda a comunidade que joga o jogo paga pelo gás para sacar no tabuleiro.

A Conta ou como os usuários ganham dinheiro

Este é o primeiro passo que damos no código Rust do contrato inteligente, mas senti a necessidade de lembrá-lo que a mecânica DeFi não é a única maneira que o Berryclub permite que você ganhe tokens. A conta tem um arquivo específico no contrato inteligente do Berryclub, não precisamos entrar nisso imediatamente, o que precisamos saber é que algumas informações são coletadas no objeto da conta que são cruciais para a mecânica de farm e ganhos:

  •         ID da conta
  •         accountIndex para a lista (vetor) de contas que tocaram o pixel board pela última vez
  •         balance (vetor para vários tokens de propriedade)
  •         número de pixels
  •         timestamp reivindicado (nanossegundos, quando a conta em questão reivindicou recompensas pela última vez)
  •         preferências de farming (bananas ou pepinos)

As duas últimas informações são para calcular as recompensas em um determinado momento, por exemplo, se você possui 5 pixels por um dia, adquire 5 bananas. Se você comprar de outra pessoa, seus ganhos diminuem porque o número de pixels que eles possuem diminui, de modo que a quantidade de ganhos é calculada e o carimbo de data/hora renovado relacionando a nova quantidade de pixels de propriedade. Como veremos, as recompensas são calculadas com base nessas duas variáveis. A operação aplicada à conta do proprietário anterior quando um pixel é desenhado é chamado de “toque” e você pode encontrá-lo no arquivo Rust account.rs . A propriedade da unidade de pixel único é a base para ganhar no berryclub e, dessa forma, essa mecânica é praticamente a mesma que uma interface de staking NFT poderia usar, recompensando a propriedade NFT.

pub fn touch(&mut self) -> (Berry, Balance) {
        let block_timestamp = env::block_timestamp();
        let time_diff = block_timestamp - self.claim_timestamp;
        let farm_bonus = if self.farming_preference == Berry::Avocado {
            1
        } else {
            0
        };
        let farmed = Balance::from(self.num_pixels + farm_bonus)
            * Balance::from(time_diff)
            * REWARD_PER_PIXEL_PER_NANOSEC;
        self.claim_timestamp = block_timestamp;
        self.balances[self.farming_preference as usize] += farmed;
        (self.farming_preference, farmed)
    }

Para tirar qualquer dúvida o dono inicial do tabuleiro é 0, o próprio contrato, e se não for possível encontrar um dono anterior, o contrato é usado como dono anterior. Finalmente para iniciar o jogo alguns tokens foram armazenados na conta do contrato e são sempre aumentados usando o preço do gás estabelecido para as pessoas comprarem abacates e bananas, para que o “cofre” do jogo seja sempre preenchido com alguns tokens para os usuários ganharem. Agora vamos voltar à nossa interface.

Números para Cores e Vice-Versa

As linhas entre 67 e 82 em app.js  são usadas para decodificar números em cores e vice-versa, para que os elementos da interface do usuário interajam com a tabuleiro, são definidas duas variáveis ​​constantes, intToColor e rgbaToInt. O que podemos notar aqui é que para transformar um número inteiro em uma string de cor, métodos são usados ​​para dividir os 3 números para vermelho verde e azul:

const intToColor = (c) => `#${c.toString(16).padStart(6, "0")}`;
const intToColorWithAlpha = (c, a) =>
  `#${c.toString(16).padStart(6, "0")}${Math.round(255 * a)
    .toString(16)
    .padStart(2, "0")}`;

Para reverter a string de cor em inteiro, simplesmente aplicamos uma função math.round() e usamos o inteiro resultante.

const rgbaToInt = (cr, cg, cb, ca, bgColor) => {
  const bb = bgColor & 255;
  const bg = (bgColor >> 8) & 255;
  const br = (bgColor >> 16) & 255;  const r = Math.round(cr * ca + br * (1 - ca));
  const g = Math.round(cg * ca + bg * (1 - ca));
  const b = Math.round(cb * ca + bb * (1 - ca));
  return (r << 16) + (g << 8) + b;
};

As linhas abaixo são sobre upload e impressão de imagens no tabuleiro usando  o componente de arma  e não vamos falar delas em profundidade: imgColorToInt e int2hsv transformam números em dois tipos diferentes de escalas de cores, então transparenteColor é definido e um gamma para a imagem a ser impressa com generateGamma. Em decodeLine transformamos o buffer em um array de pixels a ser impresso no tabuleiro usando as cores acima, iterando entre elas.

Primeiro construtor React

Nas próximas linhas do  app.js  , definimos um construtor que definirá os estados que usaremos posteriormente em nossa interface do usuário para interagir com o blockchain.

class App extends React.Component {
  constructor(props) {
    super(props);

Usando construtor e super nos manterá aptos a usar isso no construtor. Os estados definidos aqui são a cor selecionada padrão e a paleta de cores:

const colors = [
      "#000000",
      "#666666",
      "#aaaaaa",
      "#FFFFFF",
      "#F44E3B",
      "#D33115",
      "#9F0500",
      "#FE9200",
      "#E27300",
      "#C45100",
      "#FCDC00",
      "#FCC400",
      "#FB9E00",
      "#DBDF00",
      "#B0BC00",
      "#808900",
      "#A4DD00",
      "#68BC00",
      "#194D33",
      "#68CCCA",
      "#16A5A5",
      "#0C797D",
      "#73D8FF",
      "#009CE0",
      "#0062B1",
      "#AEA1FF",
      "#7B64FF",
      "#653294",
      "#FDA1FF",
      "#FA28FF",
      "#AB149E",
    ].map((c) => c.toLowerCase());
    // const currentColor = parseInt(colors[Math.floor(Math.random() * colors.length)].substring(1), 16);
    const currentColor = parseInt(colors[0].substring(1), 16);
    const defaultAlpha = 0.25;

E para o timer que atualiza a tabuleiro:

const timeMs = new Date().getTime();
    const freeDrawingStartMsEstimated =
      timeMs -
      ((timeMs - new Date("2021-05-09")) % (7 * OneDayMs)) +
      OneDayMs * 6;

Em seguida, os estados da conta de usuário em uso são definidos, mais importante se o usuário fez login, se houver alguma transação pendente (definida como pendentePixels), o estado boardLoaded carregará o quadro de tela para desenhar o alfa selectedCell e os estados pickerColor definem o estados dos componentes interativos para adicionar cores ao quadro, junto com pickingColor para escolher a cor do quadro e gammaColors é útil para impressão de imagens no quadro juntamente com os estados armasOn e armasCodePosition.

Esses outros estados são úteis para a conta ganhar no jogo, baseado em pixel e baseado em DeFi:

owners: [],
      accounts: {},
      highlightedAccountIndex: -1,
      selectedOwnerIndex: false,
      farmingBanana: false,

Enquanto os últimos três estados configuram o cronômetro para uso posterior:

freeDrawingStart: new Date(freeDrawingStartMsEstimated),
      freeDrawingEnd: new Date(freeDrawingStartMsEstimated + OneDayMs),
      watchMode: false,

A lista a seguir (linhas 203-215) define objetos e ações que irão interagir com os estados, referenciando pela primeira vez um elemento DOM, o tabuleiro canvas.

this._buttonDown = false;
    this._oldCounts = {};
    this._numFailedTxs = 0;
    this._balanceRefreshTimer = null;
    this.canvasRef = React.createRef();
    this._context = false;
    this._lines = false;
    this._queue = [];
    this._pendingPixels = [];
    this._refreshBoardTimer = null;
    this._sendQueueTimer = null;
    this._stopRefreshTime = new Date().getTime() + MaxWorkTime;
    this._accounts = {};

Por fim, definimos alguns dos estados após o login ser feito:

this._initNear().then(() => {
      this.setState(
        {
          connected: true,
          signedIn: !!this._accountId,
          accountId: this._accountId,
          ircAccountId: this._accountId.replace(".", "_"),
          freeDrawingStart: this._freeDrawingStart,
          freeDrawingEnd: this._freeDrawingEnd,
        },
        () => {
          if (window.location.hash.indexOf("watch") >= 0) {
            setTimeout(() => this.enableWatchMode(), 500);
          }
        }
      );
    });

Interações básicas

Agora começamos a descrever as interações no quadro/tela conectando-as aos estados previamente definidos. Para essas interações, usamos funções. O primeiro usará nossa referência anterior ao elemento canvas para criá-lo e instruí-lo com detalhes sobre o tipo de movimento do mouse que permitimos aos nossos usuários. No primeiro clique, ativamos o watchmode para que nosso timer inicie:

const click = async () => {
      if (this.state.watchMode) {
        return;
      }

E modo de renderização de imagem se o usuário quiser imprimir uma imagem no quadro:

if (this.state.rendering) {
        await this.drawImg(this.state.selectedCell);
      } else if (this.state.pickingColor) {
        this.pickColor(this.state.selectedCell);
      } else {
        this.saveColor();
        await this.drawPixel(this.state.selectedCell);
      }

A próxima é a parte importante, definimos como a interface lê o movimento do mouse e do toque sobre o tabuleiro:

if ("touches" in e) {
        if (e.touches.length > 1) {
          return true;
        } else {
          const rect = e.target.getBoundingClientRect();
          x = e.targetTouches[0].clientX - rect.left;
          y = e.targetTouches[0].clientY - rect.top;
        }
      } else {
        x = e.offsetX;
        y = e.offsetY;
      }

O código utilizado leva em consideração tanto os usuários mobile, construindo uma função ad-hoc para calcular a posição e adicionando um listener ao canvas/tabuleiro para eventos de toque: canvas.addEventListener(“touchmove”, mouseMove); Em seguida, essas interações são usadas para definir o estado selectedCell e rastrear o início e o fim da ação do mouse/toque na tela junto com seu movimento em cada célula:

const mouseDown = async (e) => {
      this._buttonDown = true;
      if (this.state.selectedCell !== null) {
        await click();
      }
    };    canvas.addEventListener("mousedown", mouseDown);
    canvas.addEventListener("touchstart", mouseDown);    const unselectCell = () => {
      this.setState(
        {
          selectedCell: null,
        },
        () => this.renderCanvas()
      );
    };    const mouseUp = async (e) => {
      this._buttonDown = false;
      if ("touches" in e) {
        unselectCell();
      }
    };    canvas.addEventListener("mouseup", mouseUp);
    canvas.addEventListener("touchend", mouseUp);    canvas.addEventListener("mouseleave", unselectCell);    canvas.addEventListener("mouseenter", (e) => {
      if (this._buttonDown) {
        if (!("touches" in e) && !(e.buttons & 1)) {
          this._buttonDown = false;
        }
      }
    });

A interação aqui funciona nos estados previamente definidos, como por exemplo, o seletor de cores nos permite escolher as cores do quadro e usá-las para desenhar. A chave usada pelo seletor de cores é a tecla alt e podemos fazer upload e imprimir imagens no quadro somente se o seletor de cores estiver desabilitado, pois então acionaremos a função generategamma. Desta forma, a função pickcolor(), referenciada à célula, poderá ser usada para definir um único pixel ou, em vez disso, todo o quadro para renderizar uma imagem:

pickColor(cell) {
    if (!this.state.signedIn || !this._lines || !this._lines[cell.y]) {
      return;
    }
    const color = this._lines[cell.y][cell.x].color;    this.setState(
      {
        currentColor: color,
        alpha: 1,
        pickerColor: intToColorWithAlpha(color, 1),
        gammaColors: generateGamma(int2hsv(color)[0]),
        pickingColor: false,
      },
      () => {
        this.renderCanvas();
      }
    );
  }

Agora, pessoal, chegamos ao núcleo, então esteja pronto para começar a mergulhar no contrato inteligente. Sabemos como desenhar o pixel na interface, mas precisamos anexar as transações a eles para que nossa interface seja um Play to Earn real. Portanto, preste muita atenção ao que estou prestes a dizer, porque mesmo que seu jogo pareça completamente diferente disso em termos de interface do usuário, a mecânica de ganho pode muito bem ser adequada para qualquer outro tipo de jogo e será explicada aqui da maneira mais simples que eu posso.

O contrato inteligente Berryclub

Linhas

Encontramos linhas pela primeira vez no início deste artigo, considerando as definições de UI dos estados. As linhas são um conceito importante da interface do Berryclub, são as linhas pelas quais o quadro/tela é subdividido e cada pixel nelas é um pedaço de metadados. Eles fazem parte da interface do usuário que interage com o contrato inteligente e são o objeto mais reutilizável do jogo (por exemplo, para criar níveis em um jogo mais articulado), então vamos gastar um pouco de tempo analisando como eles são usados para armazenar dados do tabuleiro e avaliados enquanto os usuários jogam o jogo.

Em primeiro lugar, no  arquivo board.rs  , encontramos uma definição de PixelLine logo após a definição de Pixel:

pub struct PixelLine(pub Vec<Pixel>);impl Default for PixelLine {
    fn default() -> Self {
        Self(vec![Pixel::default(); BOARD_WIDTH as usize])
    }
}

Um vetor (array) de dados de string subdivididos pela largura da tabuleiro.

E então definimos no PixelBoard como um vetor do PixelLines desta forma:

pub struct PixelBoard {
    pub lines: Vector<PixelLine>,
    pub line_versions: Vec<u32>,
}

Assim, cada linha é armazenada no quadro como um único registro com um campo de metadados chamado line_versions incrementando cada vez que você modifica uma linha. Então cada vez que nossa interface busca o tabuleiro você recebe 50 linhas mas também um metadado para cada linha que representa quantas vezes a linha foi atualizada, e ao buscar esses metadados a interface sabe qual é o número de vezes que a linha foi alterada. Se a linha foi alterada da busca anterior, então você busca os dados para cada pixel, caso contrário, simplesmente não o faz.

impl Place {
    pub fn get_lines(&self, lines: Vec<u32>) -> Vec<Base64VecU8> {
        lines
            .into_iter()
            .map(|i| {
                let line = self.board.get_line(i);
                line.try_to_vec().unwrap().into()
            })
            .collect()
    }    pub fn get_line_versions(&self) -> Vec<u32> {
        self.board.line_versions.clone()
    }
}

Esta é uma maneira inteligente de armazenar e buscar dados da interface que podem ser úteis para usar em seu próximo jogo Play to Earn em Near.

Transações

Vamos voltar para nossa interface do usuário em  app.js  por um momento para ter certeza de que entendemos como as transações são gerenciadas no front-end. Primeiro precisamos de uma função para verificar a conta se algo der errado e é isso:

async refreshAllowance() {
    alert(
      "You're out of access key allowance. Need sign in again to refresh it"
    );
    await this.logOut();
    await this.requestSignIn();
  }

Então você se lembra dos arrays _queue e _pendingPixels que definimos em nosso construtor? Definitivamente, é hora de usá-los, pois as transações são gerenciadas dependendo de quais pixels você desenhou no quadro:

async _sendQueue() {
    const pixels = this._queue.slice(0, BatchOfPixels);
    this._queue = this._queue.slice(BatchOfPixels);
    this._pendingPixels = pixels;    try {
      await this._contract.draw(
        {
          pixels,
        },
        new BN("75000000000000")
      );
      this._numFailedTxs = 0;
    } catch (error) {
      const msg = error.toString();
      if (msg.indexOf("does not have enough balance") !== -1) {
        await this.refreshAllowance();
        return;
 }
      console.log("Failed to send a transaction", error);
      this._numFailedTxs += 1;
      if (this._numFailedTxs < 3) {
        this._queue = this._queue.concat(this._pendingPixels);
        this._pendingPixels = [];
      } else {
        this._pendingPixels = [];
        this._queue = [];
      }
    }
    try {
      await Promise.all([this.refreshBoard(true), this.refreshAccountStats()]);
    } catch (e) {
      // ignore
    }
    this._pendingPixels.forEach((p) => {
      if (this._pending[p.y][p.x] === p.color) {
        this._pending[p.y][p.x] = -1;
      }
    });
    this._pendingPixels = [];
  }

Espere, eu não estava pronto para esse monte de código… Sim, você está! Mas vamos olhar com atenção, criamos um objeto de pixels (vetor), modificamos nosso objeto _queue para caber em pixels e atribuímos seu valor ao objeto _pendingPixel em uma  função assíncrona .

E depois? Nós apenas desenhamos em um objeto de contrato que é chamado do SDK próximo, e a ação para desenhar (uma parte das ações que definimos para o usuário) é definida no  arquivo lib.rs  rust.

pub fn draw(&mut self, pixels: Vec<SetPixelRequest>) {
        if pixels.is_empty() {
            return;
        }
        let mut account = self.get_mut_account(env::predecessor_account_id());
        let new_pixels = pixels.len() as u32;
        if ms_time() < self.get_free_drawing_timestamp() {
            let cost = account.charge(Berry::Avocado, new_pixels);
            self.burned_balances[Berry::Avocado as usize] += cost;
        }
    let mut old_owners = self.board.set_pixels(account.account_index, &pixels);
        let replaced_pixels = old_owners.remove(&account.account_index).unwrap_or(0);
        account.num_pixels += new_pixels - replaced_pixels;
        self.save_account(account);        for (account_index, num_pixels) in old_owners {
            let mut account = self.get_internal_account_by_index(account_index).unwrap();
            self.touch(&mut account);
            account.num_pixels -= num_pixels;
            self.save_account(account);
        }        self.maybe_send_reward();
    }

Para os pixels de contrato inteligente são uma cor e um id de conta (o proprietário místico), e é um jogo baseado em imóveis: então temos um antigo proprietário que desenhou o pixel antes e um novo proprietário que deseja desenhá-lo agora. Com a ação de sorteio, obtemos o old_owner e o substituímos pela conta do novo proprietário alterando o valor da cor de todos os pixels dentro do vetor PixelRequest, então enviamos recompensas para o antigo proprietário enquanto cobramos o novo. 

Os carimbos de data e hora para recompensas são redefinidos e a contagem começa novamente do zero com um pixel a menos para o antigo proprietário e um a mais para o novo. A ação setPixelRequest é definida no  arquivo board.rs  do nosso contrato, mas voltemos ao nosso  libs.rs .

Como é a função Maybe_send_rewards()? Aqui está em toda a sua glória:

impl Place {
    fn maybe_send_reward(&mut self) {
        let current_time = env::block_timestamp();
        let next_reward_timestamp: u64 = self.get_next_reward_timestamp().into();
        if next_reward_timestamp > current_time {
            return;
        }
        self.last_reward_timestamp = current_time;
        let reward: Balance = self.get_expected_reward().into();
        env::log(format!("Distributed reward of {}", reward).as_bytes());
        Promise::new(format!(
            "{}.{}",
            FARM_CONTRACT_ID_PREFIX,
            env::current_account_id()
        ))
        .function_call(
            b"take_my_near".to_vec(),
            b"{}".to_vec(),
            reward,
            GAS_BASE_COMPUTE,
        );
    }
}

Por favor, não seja preguiçoso, se você não puder se ajudar, pode chegar a isso mais tarde com  este vídeo do autor do jogo . As explicações que vou usar são tiradas desse vídeo também!

A função verifica a hora no blockchain (não estamos usando o timer na interface aqui, porque queremos ter certeza!) e usa os recursos de farm do contrato em um timestamp global com a função get_next_reward_timestamp() e last_reward_timestamp() então finalmente chama get_expected_reward() para calcular as recompensas devidas à conta.

pub fn get_expected_reward(&self) -> U128 {
        let account_balance = env::account_balance();
        let storage_usage = env::storage_usage();
        let locked_for_storage = Balance::from(storage_usage) * STORAGE_PRICE_PER_BYTE + SAFETY_BAR;
        if account_balance <= locked_for_storage {
            return 0.into();
        }
        let liquid_balance = account_balance - locked_for_storage;
        let reward = liquid_balance / PORTION_OF_REWARDS;
        reward.into()
    }

Então, pegamos o saldo atual da conta berryclub (lembra que temos um campo de saldo na conta?), o uso e custos atuais de armazenamento e um limite de segurança de 50 abacates. Se o saldo for seguro para uso fora do custo de armazenamento, dividimos em 24 (horas) * 60 (minutos) de recompensa, o que significa que você basicamente obtém exatamente o mesmo saldo que possui uma vez, se ligar a cada minuto, poderá encontrá-lo definido no início do  arquivo lib.rs :

const PORTION_OF_REWARDS: Balance = 24 * 60;
const SAFETY_BAR: Balance = 50_000000_000000_000000_000000;

Aposto que vocês acham que o processo de recompensa acabou. Errado.

Na verdade, precisamos voltar à nossa função Maybe_send_reward() para ver que ela chama o novo contrato de farm Berryclub para distribuir as recompensas de staking, que são… pepinos, o token de staking no Berryclub 🙂

const FARM_CONTRACT_ID_PREFIX: &str = "farm";

Na verdade, essa não é a única fonte de receita de distribuição com essa função, ela também aproveita os custos de gás pagos pelas pessoas para comprar abacates e trocar bananas para recompensar toda a comunidade!

Como isso é possível? Primeiramente, o GAS_BASE_COMPUTE é definido no  arquivo token.rs  onde a quantidade de gás para o contrato inteligente é definida. Sim, você está certo! O preço do gás é baixo e pode ser usado para recompensar os usuários que interagem com seu videogame!!!

Para entender melhor como as taxas de GAS funcionam no Near, consulte esta documentação detalhada!

Este tutorial é trazido a você por jilt.near e seu  projeto NFT Gaming. Apoie-o na compra de NFTs!

14
Scroll to Top