> ## Documentation Index
> Fetch the complete documentation index at: https://frenglish.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Juego Aviator Web3 con WebGL

> Aprenda cómo integrar WebGL en un juego usando herramientas del Sequence Stack para ganar logros y usar ERC1155 personalizados.

Tiempo estimado: 40 minutos

En esta guía, repasaremos el proceso de integración de WebGL en un juego, aprovechando herramientas del Sequence Stack para ganar logros y usar ERC1155 personalizados para jugar dentro del juego.

<Note>
  Puede jugar una versión en vivo del juego [aquí](https://0xsequence.github.io/aviator-demo/)

  El código completo de este juego se puede encontrar [aquí](https://github.com/0xsequence/aviator-demo/)

  Y el código plantilla completo que usaremos para la guía se puede encontrar [aquí](https://github.com/0xsequence-demos/template-webgl-js-sequence-kit-starter)
</Note>

Estas herramientas le permitirán realizar:

1. [Configuración del Proyecto con Webpack](/guides/webgl-guide#1-project-setup-with-webpack): Habilite una estructura de proyecto con WebGL para ser compilada por Webpack
2. [Integrar Web SDK](/guides/webgl-guide#2-integrate-sequence-kit): Permita que todos los EOAs y Sequence Wallet autentiquen al usuario
3. [Desplegar un contrato de coleccionables](/guides/webgl-guide#3-deploy-a-collectibles-contract): Cree su propio contrato de coleccionables
4. [Desplegar un Remote Minter y mintear tokens dentro del juego](/guides/webgl-guide#4-deploy-a-remote-minter-and-mint-in-game-achievement-tokens): Realice transacciones relay sin gas con Cloudflare workers
5. [Aproveche los ítems dentro del juego](/guides/webgl-guide#5-leverage-items-in-game): Integre coleccionables en el juego usando Sequence Indexer
6. [Queme tokens de logros dentro del juego](/guides/webgl-guide#6-burn-in-game-achievement-tokens): Queme logros del juego con wagmi
7. [(Opcional) Integre Embedded Wallet en Web SDK](/guides/webgl-guide#7-optional-integrate-embedded-wallet-into-sequence-kit): Permita una experiencia fluida sin el uso de mensajes firmados por el firmante

## 1. Configuración del proyecto con webpack

#### Clonar el repositorio

Primero comenzaremos clonando un proyecto plantilla, que incluye algunos componentes basados en WebGL creados con `three`, todos compilados usando [webpack](https://webpack.js.org/)

[Plantilla WebGL JS Web SDK Starter](https://github.com/0xsequence-demos/template-webgl-js-sequence-kit-starter/tree/simple-start)

Clona el repositorio anterior y accede a él con `cd template-webgl-js-sequence-kit-starter`.

#### Actualice `.env`

Cree un archivo `.env` (usando el `.env.example`) con las variables de entorno

```
PROJECT_ACCESS_KEY=
WALLET_CONNECT_ID=
```

Y ejecute los siguientes comandos para iniciar la aplicación

```
# or your choice of package manager
pnpm install
pnpm run dev
```

¡Listo! Debería ver un avión volando sobre el agua

## 2. Integrar Web SDK

Ahora que tenemos la estructura del proyecto lista, podemos integrar Web SDK

#### Configurar el componente `App.jsx`

Cree una carpeta dentro de `src` llamada `react` y cree 2 archivos: `App.jsx` y `Login.jsx`

En `App.jsx` incluya el siguiente código

```js theme={null}
import React from "react";
import { useOpenConnectModal } from "@0xsequence/kit";
import { useDisconnect, useAccount } from "wagmi";
import Login from "./Login.jsx";

import { KitProvider } from "@0xsequence/kit";
import { getDefaultConnectors } from "@0xsequence/kit-connectors";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createConfig, http, WagmiConfig } from "wagmi";
import { arbitrumSepolia, Chain } from "wagmi/chains";

const queryClient = new QueryClient();

function App(props) {
  const chains = [arbitrumSepolia];
  const projectAccessKey = process.env.PROJECT_ACCESS_KEY;

  const connectors = getDefaultConnectors("universal", {
    walletConnectProjectId: process.env.WALLET_CONNECT_ID,
    defaultChainId: 421614,
    appName: "demo app",
    projectAccessKey,
  });

  const transports = {};

  chains.forEach((chain) => {
    transports[chain.id] = http();
  });

  const config = createConfig({
    transports,
    connectors,
    chains,
  });

  return (
    <WagmiConfig config={config}>
      <QueryClientProvider client={queryClient}>
        <KitProvider config={{ defaultTheme: "dark" }}>
          <Login scene={props.scene} />
        </KitProvider>
      </QueryClientProvider>
    </WagmiConfig>
  );
}

export default App;
```

Luego, en el archivo `Login.jsx` agregue el siguiente código para crear un botón en la parte superior de la pantalla para iniciar sesión en la aplicación

```js theme={null}
import React, { useEffect } from "react";
import { useOpenConnectModal, useKitWallets } from "@0xsequence/kit";
import { useWalletClient } from "wagmi";

function Login(props) {
  const { setOpenConnectModal } = useOpenConnectModal();
  const { data: walletClient } = useWalletClient();
  const {
    wallets, // Array of connected wallets
    linkedWallets, // Array of linked wallets (for embedded wallets)
    setActiveWallet, // Function to set a wallet as active
    disconnectWallet, // Function to disconnect a wallet
  } = useKitWallets();

  const isConnected = wallets.length;

  useEffect(() => {
    if (isConnected) {
      props.scene.login();
    } else {
      props.scene.logout();
    }
  }, [isConnected]);

  const sendBurnToken = async () => {
    // empty for now
  };

  useEffect(() => {
    if (isConnected && walletClient) {
      props.scene.sequenceController.init(walletClient, sendBurnToken);
    }
  }, [isConnected, walletClient]);

  return (
    <>
      <div style={{ textAlign: "center" }}>
        <br />
        {isConnected && (
          <div
            onClick={() => disconnectWallet(wallets[0].address) // assuming one wallet is connected. you can also disconnect a specific wallet from useKitWallets by specifying the address}
            style={{
              cursor: "pointer",
              position: "fixed",
              top: "30px",
              right: "30px",
              zIndex: "1",
            }}
          >
            sign out
          </div>
        )}
      </div>
    </>
  );
}

export default Login;
```

#### Renderizar el componente en Javascript `index.js`

Finalmente, en `index.js` importe el componente `App.jsx` y renderícelo para que se agregue al elemento con id `root` en `index.html`

```js theme={null}
import * as ReactDOM from 'react-dom/client';
import App from './react/App.jsx'

...

const root = ReactDOM.createRoot(document.getElementById('root'))

root.render(
    <App scene={mainScene}/>
);
```

##### Crear un manejador de clic para llamar al modal de inicio de sesión

Agregue el siguiente código al componente `Login.jsx`

```js theme={null}
window.setOpenConnectModal = () => {
  setOpenConnectModal(true);
};
```

Y el siguiente código del manejador de clic en `index.js`

```ts theme={null}
function handleMouseUp(event) {
  window.setOpenConnectModal();
}

document
  .getElementById("world")
  .addEventListener("mouseup", handleMouseUp, false);
```

Y agregue estos elementos a su `index.html`

```html theme={null}
<div id="mintBtn" className="btn" onclick="window.mintPlane()">mint plane</div>
<div id="mintAchievementBtn" className="btn" onclick="window.mintAchievement()">
  mint achievement
</div>
<div id="burnBtn" className="btn" onclick="window.burn()">burn achievement</div>

<div id="login">click to login</div>
<div className="world" id="world"></div>
```

¡Listo! Ahora tendrá un botón que hace aparecer un modal

## 3. Implemente un contrato de coleccionables

Necesitará crear un coleccionable desde el [Sequence Builder](https://sequence.build), lo cual puede lograr siguiendo esta [guía](/solutions/collectibles/contracts/deploy-an-item-collection)

Deberíamos crear 2 colecciones: una para los tokens de logros y otra para los aviones

## 4. Desplegar un Remote Minter y mintear tokens de logros dentro del juego

Luego, para enviar transacciones a la blockchain de manera fluida y sin gas, implemente un [Cloudflare Worker](/guides/mint-collectibles-serverless) para mintear ítems desde un contrato desplegado en el paso anterior, asegurándose de que la dirección del contrato de la API de transacciones esté ingresada como `Minter Role`

Permitiremos que existan múltiples caminos para mintear coleccionables: un coleccionable de avión y un coleccionable de logro.

Esto se logra en el código agregando una clave/valor `isPlane` al cuerpo de la solicitud normal de cloudflare, y creando un `if/else` adicional en el cloudflare worker.

Puede ver el código para esto en este [repositorio de github](https://github.com/0xsequence-demos/template-webgl-js-sequence-kit-starter/blob/master/cloudflare/noisy-silence-ee19/src/index.ts#L80)

Para esta guía, ejecutaremos todo el código de cloudflare en un entorno de desarrollo local, esto se puede hacer iniciando el cloudflare worker en la carpeta del proyecto, así:

```
wrangler dev
```

## 5. Aproveche los ítems dentro del juego

Esta sección se dividirá en 2 implementaciones para actualizar la UI según los cambios de propiedad de activos dentro del juego:

* Mostrar cambios de avión según los activos del wallet
* Mostrar cambios en la UI según los activos del wallet

#### Mostrar cambios de avión según los activos del wallet

Para implementar cambios en el juego según los activos que tiene el wallet, puede implementar un botón que mintea un token y luego, en la respuesta, verifica los cambios en el indexer

En el `index.js` incluimos un botón conectado al atributo `onclick` del elemento en index.html

```js theme={null}
window.mintPlane = () => {
  const tokenID = 1;
  mainScene.sequenceController.callContract(tokenID, true, (res) => {
    mainScene.sequenceController.fetchPlaneTokens(tokenID);
  });
};
```

Donde `callContract` se encarga del minteo llamando a un fetch que está envuelto en un mutex para asegurar que solo ocurra un minteo a la vez, evitando clics repetidos, y se agrega a la clase `SequenceController` en `/API/SequenceController.js`

```js theme={null}
import { Mutex, E_CANCELED} from 'async-mutex';

const mutexMinting = new Mutex();
...
async callContract(tokenId, isPlane, callback) {
  if(!mutexMinting.isLocked()){
    try {
      await mutexMinting.runExclusive(async () => {
        console.log('Minting token:', tokenId);
        const url = 'http://localhost:8787';
        const data = {
          address: this.walletAddress,
          tokenId: tokenId,
          isPlane: isPlane
        };

        try {
          const res = await fetch(url, {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
            },
            body: JSON.stringify(data),
          })
          const txHash = await res.text();
          mutexMinting.release();
          callback(txHash);
        } catch(err) {
          mutexMinting.release();
          callback(err);
        }
      });
    } catch (err) {
      if (err === E_CANCELED) {
        mutexMinting.release();
      }
    }
  } else {
    console.log('mutex is locked')
  }
}
```

y `fetchPlaneTokens` hará polling del resultado hasta que haya un activo en su wallet, actualizando el `plane color` para representar un avión diferente.

`fetchPlaneTokens` se implementa con el siguiente código, donde la condición de balance es mayor que 1, y el `tokenID` es igual al id buscado.

Esta lógica condicional de la UI cambiaría según su aplicación

```js theme={null}
import { SequenceIndexer } from '@0xsequence/indexer';
...
async fetchPlaneTokens(){

  // a polling wait
  const wait = (ms) => new Promise((res) => setTimeout(res, ms))
  let hasFoundPlane = false

  while(!hasFoundPlane) {
      const response = await this.indexer.getTokenBalances({
      accountAddress: this.walletAddress,
      contractAddress: '0x10ac72ada55ed46ee35deed371b8d215c2e870e1', // the collection address
    })
    await wait(1000)
    for(let i = 0; i < response.balances.length; i++){
      // a check on the inventory
      if(response.balances[i].tokenID == '1' && Number(response.balances[i].balance) > 0){
        // implement any UI update here
        this.scene.airplane.addPlane(Number(response.balances[i].tokenID))
        hasFoundPlane = true // breaks from the loop
      }
    }
  }
}
```

#### Mostrar cambios en la UI según los activos del wallet

A continuación, implementamos un cambio en la UI donde agregamos un botón de `burn achievement`, dependiendo de si el usuario tiene un logro o no

Primero, implementar la lógica de manejador de clic HTML/JS similar a la anterior

donde esta vez, el valor `isPlane` de `callContract` se establece en `false`

```js theme={null}
// index.js
window.mintAchievement = () => {
  const tokenID = 0;
  mainScene.sequenceController.callContract(tokenID, false, (res) => {
    mainScene.sequenceController.fetchTokensFromAchievementMint(tokenID);
  });
};
```

<Warning>
  Nota: en un juego real, este minteo del token de logro ocurriría en base a
  algún evento disparador dentro del juego, pero para simplificar hemos incluido un botón
</Warning>

Esta vez, llamamos a `fetchTokensFromAchievementMint` que se agrega al `SequenceController`

```js theme={null}
  async fetchTokensFromAchievementMint(tokenID) {
    // check for achievement balance
    const wait = (ms) => new Promise((res) => setTimeout(res, ms))
    let hasFoundPlane = false
    let tokenIDs = []
    while(!hasFoundPlane) {
      const response = await this.indexer.getTokenBalances({
        accountAddress: this.walletAddress,
        contractAddress: '0x856de99d7647fb7f1d0f60a04c08340db3875340', // you achievements collection address
      })
      await wait(1000)
      for(let i = 0; i < response.balances.length; i++){
        // can update this logic to see if there is any balance: i.e. if(response.balances.length > 0)
        if(response.balances[i].tokenID == String(tokenID)){
          hasFoundPlane = true
          // making the button appear
          document.getElementById('burnBtn').style.display = 'flex'
        }
      }
    }
  }
```

Esto hace que solo si hay un balance devuelto por el indexer, el atributo `display` haga aparecer el botón

## 6. Quemar tokens de logro dentro del juego

Finalmente, para quemar el token de logro, ya no podemos usar un Cloudflare Worker para las acciones enviadas a la blockchain, porque cuando el minteo se realizó 'en nombre de' la dirección usando la API de transacciones (haciendo que el `msg.sender` en el contrato sea una de las direcciones del `relayer`), para esto, queremos asegurarnos de que el `msg.sender` en el contrato demuestre la propiedad del token y que la transacción sea enviada directamente por el usuario. Usaremos funciones frontend de `wagmi` y algo de composición de clases para lograrlo.

```js theme={null}
// index.js
window.burn = () => {
  const tokenID = 0;
  mainScene.sequenceController.burnToken(tokenID, (res) => {
    mainScene.sequenceController.fetchTokensFromBurn(tokenID);
  });
};
```

Donde `burnToken` es una función pasada desde nuestro componente react que usa un patrón similar de mutexes, y enviamos la transacción usando `sendTransaction` del paquete `wagmi`, esperando la actualización del hash de la transacción para devolver el callback

```js theme={null}
// react/Login.jsx
import {
    useAccount,
    useWalletClient,
    useSendTransaction,
} from 'wagmi';
import { useMutex } from 'react-context-mutex';
import { ethers } from 'ethers'
import { SequenceIndexer } from '@0xsequence/indexer';

let burnCallback = null
const ContractAddress = '0x856de99d7647fb7f1d0f60a04c08340db3875340';

function Login() {
  const MutexRunner = useMutex();
  const mutexBurn = new MutexRunner('sendMutexBurn');

  const { isConnected } = useAccount()
  const { data: walletClient } = useWalletClient();
  const { data: txnData, sendTransaction, isLoading: isSendTxnLoading } = useSendTransaction();

  useEffect(() => {
    if (isConnected && walletClient) {
        props.scene.sequenceController.init(
            walletClient,
            sendBurnToken
        );
    }
  }, [isConnected, walletClient]);

  const sendBurnToken = async (tokenID, callback) => {
      if(!mutexBurn.isLocked()){
          const contractABI = ['function burn(uint256 tokenId, uint256 amount)']; // Replace with your contract's ABI
          const contract = new ethers.Contract(ContractAddress, contractABI);

          // call indexer
          // check for achievement balance
          const indexer = new SequenceIndexer(
              'https://arbitrum-sepolia-indexer.sequence.app',
              process.env.PROJECT_ACCESS_KEY
          );

          const response = await indexer.getTokenBalances({
              accountAddress: walletClient.account.address,
              contractAddress: '0x856de99d7647fb7f1d0f60a04c08340db3875340',
          })

          const data = contract.interface.encodeFunctionData('burn', [
              tokenID,
              response.balances[0].balance, // get the balance from the indexer
          ]);

          try {
              mutexBurn.lock()
              burnCallback = callback
              await sendTransaction({
                  to: ContractAddress,
                  data: data,
                  value: '0',
                  gas: null,
              })
          } catch (error) {
              console.log(error)
          callback(error);
          }
      } else {
          console.log('burn in progress')
      }
  };

  useEffect(() => {
    if(txnData && burnCallback && mutexBurn.isLocked()) {
        mutexBurn.unlock();
        burnCallback(txnData)
    }
  }, [burnCallback, txnData])
...
}
```

Y en nuestro `SequenceController`, llamamos a la función `sendBurnToken` envuelta en `burnToken` para hacer que la función de react sea accesible para el resto de la aplicación

```js theme={null}
async burnToken(tokenID, callback) {
  this.sendBurnToken(tokenID, callback);
}

async init(walletClient, sendTransactionBurn) {
  this.walletAddress = walletClient.account.address;

  this.sendBurnToken = sendTransactionBurn;
}
```

Luego, para que el token quemado tenga un efecto en la UI, ocultamos el botón usado para quemar el token en el lugar inicial, lo cual se logra con el siguiente código en el `SequenceController`

```js theme={null}
async fetchTokensFromBurn(tokenID){
  const wait = (ms) => new Promise((res) => setTimeout(res, ms))
  let hasBeenBurned = false
  while(!hasBeenBurned) {
    let tokenIDs = [] // create an empty array to include all the tokens
    const response = await this.indexer.getTokenBalances({
      accountAddress: this.walletAddress,
      contractAddress: '0x856de99d7647fb7f1d0f60a04c08340db3875340',
    })
    await wait(1000)
    for(let i = 0; i < response.balances.length; i++){
        tokenIDs.push(response.balances[i].tokenID)
    }
    if(!tokenIDs.includes(String(tokenID))) { // check that the token id is not contained in the array
      hasBeenBurned = true
      // can apply any UI logic here
      document.getElementById('burnBtn').style.display = 'none' // hide the button
    }
  }
}
```

¡Y listo! Puede ver un ejemplo completo del código [aquí](https://github.com/0xsequence-demos/template-webgl-js-sequence-kit-starter)

## 7. (Opcional) Integre el Embedded Wallet en Web SDK

Si desea facilitar el recorrido del usuario para que no tenga que firmar transacciones en ningún momento, puede habilitar el Embedded Wallet actualizando la configuración de su componente react de Web SDK.

Al hacer esto, reducimos los pop-ups al quemar tokens con `wagmi`, ya que la entrega de tokens de logro y el minteo de coleccionables se completan usando un Cloudflare Worker para transacciones sin gas.

Esto se puede lograr agregando algunas variables de entorno y cambiando el tipo de conector que usamos.

Primero actualice su archivo `.env` con los siguientes secretos de entorno

```
WAAS_CONFIG_KEY=
GOOGLE_CLIENT_ID=
APPLE_CLIENT_ID=
```

Luego pase estas variables a su conector de Web SDK en `App.jsx`

```js theme={null}

import { getKitConnectWallets } from '@0xsequence/kit';
import { getDefaultWaasConnectors } from '@0xsequence/kit-connectors';
import { createConfig, http, WagmiProvider } from 'wagmi';
...
const projectAccessKey = process.env.PROJECT_ACCESS_KEY_NEXT;
const waasConfigKey = process.env.WAAS_CONFIG_KEY;
const googleClientId = process.env.GOOGLE_CLIENT_ID;
const appleClientId = process.env.APPLE_CLIENT_ID;

function App(props) {

  const appleRedirectURI =
  'https://' + window.location.host + '/aviator-demo';

  const connectors = [
    ...getDefaultWaasConnectors({
      walletConnectProjectId: process.env.WALLET_CONNECT_ID,
      defaultChainId: 421614,
      waasConfigKey,
      googleClientId,
      appleClientId,
      appleRedirectURI,
      appName: 'demo app',
      projectAccessKey,
      enableConfirmationModal: false,
    }),
    ...getKitConnectWallets(projectAccessKey, []),
  ];

  const transports = {};

  chains.forEach(chain => {
    transports[chain.id] = http();
  });

  const config = createConfig({
    transports,
    connectors,
    chains,
  });

  return (
    <WagmiProvider config={config}>
    ...
    <WagmiProvider/>
  )
}
```

Y eso es todo, no se requiere más integración para completar los flujos de transacción

<Note>
  Para aprender más sobre Wallets dentro del juego, vea
  [aquí](/solutions/wallets/embedded-wallet/overview)
</Note>
