RPG-JS tutorial - Adding an intro GUI screen for selecting a Bitshares account

in BitShares4 months ago (edited)

image.png

Continuing the RPG-JS theme, let's delve into some further development topics relating to Bitshares!

How to add an intro GUI screen!

I found myself requesting the user provide their Bitshares username at one point in my project using RPG-JS and figured that this info should be requested much earlier, so I worked on adding a GUI prompt for when the user first joins the map.

The starter project template showed us that we could easily show a message to the user when they join the map:

  async onJoinMap(player: RpgPlayer) {
    if (player.getVariable("AFTER_INTRO")) {
      return;
    }
    await player.showText("Welcome to the rpg js");
    player.setVariable("AFTER_INTRO", true);
  },

.
So, with a small change we can change the showText into a show GUI:

// Launch the intro GUI
async function playerIntro(player: RpgPlayer) {
  await player.gui("intro").open();
}

async onJoinMap(player: RpgPlayer) {
    if (player.getVariable("AFTER_INTRO")) {
      return;
    }
    await playerIntro(player);
    player.setVariable("AFTER_INTRO", true);
},

.
That's all the code that's needed to launch a GUI when the user first joins the map!

With regards to the content of the intro GUI, check it out!

<script>
import { defineComponent, computed, watchEffect, ref, onMounted, inject } from "vue";
import { useStore } from "@nanostores/vue";

import {
  $currentUser,
  setCurrentUser,
  $userStorage,
  removeUser,
} from "../nanostores/users.ts";
import { createUserSearchStore } from "../nanoeffects/UserSearch";

import "@shoelace-style/shoelace/dist/components/button/button.js";
import "@shoelace-style/shoelace/dist/components/dialog/dialog.js";
import "@shoelace-style/shoelace/dist/components/input/input.js";

export default defineComponent({
  name: "intro",
  setup() {
    const open = ref(true);

    const chain = ref();
    const method = ref();
    const inputText = ref();

    const inProgress = ref(false);
    const searchResult = ref(null);
    const searchError = ref(false);

    const storedUsers = useStore($userStorage);

    async function search() {
      if (!chain.value || !inputText.value) {
        console.log("Invalid search parameters..");
        return;
      }

      searchError.value = false;
      inProgress.value = true;

      const searchStore = createUserSearchStore([chain.value, inputText.value]);

      const unsub = searchStore.subscribe((result) => {
        if (result.error) {
          searchError.value = true;
          inProgress.value = false;
          console.error(result.error);
        }

        if (!result.loading) {
          if (result.data) {
            const res = result.data;
            searchResult.value = res;
            inProgress.value = false;
          }
        }
      });

      return () => {
        unsub();
      };
    }

    function handleCloseRequest(event) {
      if (event.detail.source === "overlay" || event.detail.source === "close-button") {
        event.preventDefault();
      }
    }

    return {
      // basic dialog functionality
      open,
      chain,
      handleCloseRequest,
      // mode and storage
      method,
      storedUsers,
      setCurrentUser,
      // account search functionality
      inputText,
      inProgress,
      searchError,
      searchResult,
      search,
    };
  },
});
</script>

<template>
  <div class="computer">
    <sl-dialog
      :open="open"
      label="Select a blockchain account to proceed!"
      class="dialog-overview"
      @sl-request-close="handleCloseRequest"
    >
      <div v-if="!chain">
        <p>Please select the blockchain you want to use.</p>
        <div class="smallGrid">
          <sl-button slot="footer" variant="neutral" @click="chain = 'bitshares'"
            >Bitshares
          </sl-button>
          <sl-button slot="footer" variant="neutral" @click="chain = 'bitshares_testnet'">
            Bitshares testnet
          </sl-button>
        </div>
      </div>

      <div v-if="chain && !method">
        <p>
          {{
            chain === "bitshares"
              ? "You can either search for a new or a previous Bitshares account"
              : "You can either search for a new or a previous Bitshares testnet account"
          }}
        </p>
        <p>How do you want to proceed?</p>
        <div class="smallGrid">
          <sl-button slot="footer" variant="neutral" @click="method = 'new'">
            Search for an account
          </sl-button>
          <sl-button slot="footer" variant="neutral" @click="method = 'existing'">
            Previously used accounts
          </sl-button>
          <sl-button slot="footer" variant="neutral" @click="chain = null">Back</sl-button>
        </div>
      </div>

      <div v-if="chain && method === 'new'">
        <p>
          {{
            chain === "bitshares"
              ? "Searching for a replacement Bitshares (BTS) account"
              : "Searching for a replacement Bitshares testnet (TEST) account"
          }}
        </p>
        <sl-input
          type="text"
          placeholder="Username"
          @input="
            inputText = $event.target.value;
            searchResult = null;
            searchError = false;
          "
          @keypress.enter="search"
        ></sl-input>

        <div class="smallGrid">
          <sl-button slot="footer" variant="primary" @click="search">Search</sl-button>
          <sl-button slot="footer" variant="neutral" @click="method = null">Back</sl-button>
        </div>
        <sl-divider v-if="searchResult"></sl-divider>
      </div>

      <div v-if="searchResult">
        <p>
          {{
            chain === "bitshares"
              ? "Found the following Bitshares (BTS) account!"
              : "Found the following Bitshares testnet (TEST) account!"
          }}
        </p>
        <p>{{ searchResult.name }} ({{ searchResult.id }})</p>
        <sl-button
          slot="footer"
          variant="primary"
          @click="
            setCurrentUser(searchResult.name, searchResult.id, searchResult.referrer, chain);
            open = false;
          "
        >
          Proceed with this account
        </sl-button>
      </div>

      <div v-if="chain && method === 'existing'">
        <p>
          {{
            chain === "bitshares"
              ? "Selecting a previously used Bitshares (BTS) account"
              : "Selecting a previously used Bitshares testnet (TEST) account"
          }}
        </p>
        <div
          v-if="
            storedUsers &&
            storedUsers.users &&
            storedUsers.users.length > 0 &&
            storedUsers.users.filter((user) => user.chain === chain)
          "
        >
          <p>Choose an account from the list below:</p>

          <sl-button
            v-for="user in storedUsers.users.filter((user) => user.chain === chain)"
            @click="
              setCurrentUser(user.username, user.id, user.referrer, chain);
              open = false;
            "
            :key="user.id"
            variant="neutral"
            style="margin: 5px"
          >
            {{ user.username }} ({{ user.id }})
          </sl-button>

          <sl-button slot="footer" variant="neutral" @click="method = null">Back</sl-button>
        </div>
        <div v-else>
          <p>No previously used accounts found, please use a new account.</p>
          <sl-button slot="footer" variant="neutral" @click="method = 'new'">
            Find account
          </sl-button>
        </div>
      </div>
    </sl-dialog>
  </div>
</template>

<style scoped>
sl-dialog::part(header) {
  padding-bottom: 5px;
}
sl-dialog::part(body) {
  padding-top: 5px;
}
.smallGrid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  grid-gap: 5px;
  margin-top: 30px;
}
</style>

So with this GUI component we launch a dialog which can only be dismissed by selecting a Bitshares or Bitshares testnet account to use in the game.

Check it out:

This account select dialog functions very similarly to the account select component in the astro bitshares nft tool & astro market UI projects, just without the avatar following the mouse.

The chosen user and blockchain are stored in a persistent nanostore:

import { map } from "nanostores";
import { persistentMap } from "@nanostores/persistent";

type User = {
  username: string;
  id: string;
  chain: string;
  referrer: string;
};

/**
 * Declaring the current user map nanostore
 */
const $currentUser = map<User>({
  username: "",
  id: "",
  chain: "",
  referrer: "",
});

/**
 * Setting the current user in a non persistent nanostore map
 * @param username
 * @param id
 * @param referrer
 * @param chain
 */
function setCurrentUser(username: string, id: string, referrer: string, chain: string) {
  $currentUser.set({
    username,
    id,
    chain,
    referrer,
  });

  try {
    addUser(username, id, referrer, chain);
  } catch (e) {
    console.log(e);
  }
}

function eraseCurrentUser() {
  $currentUser.set({
    username: "",
    id: "",
    chain: "",
    referrer: "",
  });
}

type StoredUsers = {
  users: User[];
  lastAccount: User[];
};

/**
 * Declaring the persistent user store
 */
const $userStorage = persistentMap<StoredUsers>(
  "storedUsers:",
  {
    users: [],
    lastAccount: [],
  },
  {
    encode(value) {
      return JSON.stringify(value);
    },
    decode(value) {
      try {
        return JSON.parse(value);
      } catch (e) {
        console.log(e);
        return value;
      }
    },
  }
);

/**
 * Add an user to the persistent user store
 * @param username
 * @param id
 * @param chain
 */
function addUser(username: string, id: string, referrer: string, chain: string) {
  const users = $userStorage.get().users;
  const user = { username, id, referrer, chain };
  $userStorage.setKey("lastAccount", [user]);
  if (users.find((user) => user.id === id)) {
    //console.log("Using existing user");
    return;
  }
  const newUsers = [...users, user];
  $userStorage.setKey("users", newUsers);
}

/**
 * Removing a single user from the persistent user store
 * @param id
 */
function removeUser(id: string) {
  const users = $userStorage.get().users;
  const newUsers = users.filter((user) => user.id !== id);
  $userStorage.setKey("users", newUsers);

  const lastUser = $userStorage.get().lastAccount;
  if (lastUser && lastUser.length && lastUser[0].id === id) {
    $userStorage.setKey("lastAccount", []);
  }
}

export { $currentUser, setCurrentUser, eraseCurrentUser, $userStorage, addUser, removeUser };

This can be easily fetched within another GUI component via the following:

import { useStore } from "@nanostores/vue";

const currentUser = useStore($currentUser);
const storedUsers = useStore($userStorage);

Have fun implementing your own startup screen GUIs using the above technique!


Have any questions? Comment below!


These developments were brought to you by the NFTEA Gallery.

Consider collecting an NFTEA NFT to or donate to the following BTS or EOS blockchain accounts directly, to continued developments.