RPG-JS Tutorial - Switching character sprite using an item that launches a custom GUI

in #rpgjs3 months ago (edited)

image.png

As you may have been following, I've recently been blogging about my efforts working with the rpg-js framework, check out my profile if you've missed out on this content.

Today's tutorial covers several areas - creating an item and custom GUI for switching character sprites!

Let's create an item which launches our GUI!

Saving this new item to the location /main/database/changeSprite.ts

import { RpgPlayer } from '@rpgjs/server'
import { Item } from '@rpgjs/database'

import { $currentUser } from '../nanostores/users';

@Item({
    id: 'changeSprite',
    name: 'Change sprite',
    description: 'Change your sprite for a different look',
    price: 0,
    consumable: true // Must be true for GUI to launch
})
export default class ChangeSprite {
    async onUse(player: RpgPlayer) {
        await player.gui("sprite").open({}, {waitingAction: true, blockPlayerInput: true});
        const usr = $currentUser.get();
        player.setGraphic(usr.sprite);
    }
    
    onRemove(player: RpgPlayer) {
        player.addItem('changeSprite', 1); // avoid selling the tool
    }
}

.
This item is heavily inspired by the recently created and blogged about ChangeAccount item; we make it consumable so it can launch a GUI then we add it back to the user inventory so it becomes a permanent inventory 'consumable'.

Let's add it to the user's inventory when they join the game!

This is within the /main/player.ts file:

const player: RpgPlayerHooks = {
  async onJoinMap(player: RpgPlayer) {
    if (player.getVariable("AFTER_INTRO")) {
      return;
    }

    await player.gui("intro").open({}, {waitingAction: true, blockPlayerInput: true});

    const usr = $currentUser.get();
    await playerGold(player, usr);
    player.setGraphic(usr.sprite);

    player.addItem("changeSprite", 1); // default user item

    player.setVariable("AFTER_INTRO", true);
  },
};

So you can see that when the user first joins the initial map we check if they've initialized their character, if not we prompt them to select their character then we place the changeSprite item in their inventory as the dialog window closes!

Let's create the custom GUI!

Saved to the following location: /main/gui/sprite.vue

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

import {
  //type User,
  $currentUser,
  changeSprite,
} from "../nanostores/users.ts";

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";
import "@shoelace-style/shoelace/dist/components/radio-group/radio-group.js";
import "@shoelace-style/shoelace/dist/components/radio-button/radio-button.js";

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

    const spriteType = ref("male");
    const spriteValue = ref(0);
    
    onMounted(() => {
      const user = $currentUser.get();
      const currentSprite = user.sprite;
      const [type, value] = currentSprite.split("-");
      spriteType.value = type;
      spriteValue.value = parseInt(value);
    });

    const spriteTypeQty = computed(() => {
      if (spriteType.value === "male") {
        return 74;
      } else {
        return 96;
      }
    });

    watchEffect(() => {
        if (spriteValue.value > spriteTypeQty.value) {
          spriteValue.value = spriteTypeQty.value - 1;
        }
    });

    const spriteURL = computed(() => {
      return `/main/spritesheets/characters/${spriteType.value}-${spriteValue.value}.png`;
    });

    async function closeGUI() {     
      try {
        await rpgGuiClose('sprite');
      } catch (error) {
        console.error(error);
      }
      
      open.value = false;
    }

    return {
      // basic dialog functionality
      open,
      closeGUI,
      changeSprite,
      // sprite selection
      spriteType,
      spriteValue,
      spriteTypeQty,
      spriteURL,
    };
  },
});
</script>

<template>
  <div class="computer">
    <sl-dialog
      :open="open"
      label="Selecting a new sprite"
      class="dialog-overview"
    >
      <div>
        <h3>Choose how your character appears</h3>
        <sl-radio-group :value="spriteType" @sl-change="spriteType = $event.target.value" size="medium" label="Select a sprite type" name="gender">
          <sl-radio-button style="margin-top: 10px;" pill value="male">Male</sl-radio-button>
          <sl-radio-button style="margin-top: 10px;" pill value="female">Female</sl-radio-button>
        </sl-radio-group>

         // IMAGE REMOVED BY HIVE - CHECK GITHUB

        <p>Viewing {{ spriteValue + 1 }} of {{ spriteTypeQty }} {{ spriteType }} sprites</p>

        <div class="smallGrid">
          <sl-button
            variant="neutral"
            @click="spriteValue > 0 ? (spriteValue -= 1) : (spriteValue = spriteTypeQty - 1)"
            size="small"
            pill
          >
            Previous
          </sl-button>
          <sl-button
            variant="neutral"
            @click="spriteValue < spriteTypeQty - 1 ? (spriteValue += 1) : (spriteValue = 0)"
            size="small"
            pill
          >
            Next
          </sl-button>
        </div>

        <div class="microGrid">
          <sl-button
            variant="primary"
            @click="
              changeSprite(`${spriteType}-${spriteValue}`);
              closeGUI();
            "
            size="small"
            pill
          >
            Use this sprite
          </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;
}
.microGrid {
  display: grid;
  grid-template-columns: repeat(1, 1fr);
  grid-gap: 5px;
  margin-top: 15px;
}
.parent {
  display: flex;
  align-items: center;
}
.parent > div:first-child {
  flex-shrink: 0;
  flex-basis: 32px;
  margin-right: 10px;
}
.parent > div:last-child {
  flex-grow: 1;
}
</style>

.
Most of this functionality was already implemented in the change account custom GUI, we've repurposed it for just changing the sprite instead of setting the whole user nanostore state.

Here's how it looks in action!


Have any questions? Comment below!

Which sprite will you use?

Do you want to use other types of character sprites instead of just male/female humans?

What custom GUI or item do you want implemented next?


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 fund continued developments.

Don't have a Bitshares account? Make one today!