Bitmap Text.

This is the sixth article in a series that takes the final mini-RPG from the "How to Make an RPG" project and adds a little more polish to one element.

In the previous polish phase we updated the game text to use crisp bitmap fonts. This time we'll take a break from fonts and text, and add a new magic menu the player can browse outside of combat. The magic menu makes it easier to add another new feature; giving the player the ability use spells in the exploration mode.

Here's the current menu and where we'll add the "Magic" option.

The in-game menu without a magic option.

Spell Mark Up

Before creating the magic menu, let's look at how spells are defined:

["fire"] =
{
    name = "Fire",
    action = "element_spell",
    element = "fire",
    mp_cost = 8,
    base_damage = {3, 5},
    base_hit_chance = 1,
    time_points = 10,
    target =
    {
        selector = "WeakestEnemy",
        switch_sides = true,
        type = "One"
    }
},

We're going to add an extra field called "Description" to describe the spell. This description will be displayed in the magic menu.

["fire"] =
{
    name = "Fire",
    -- code omitted
    description = "Damages an enemy with elemental fire!"
}

Here's the description I've added for all the existing spells.

  • Burn - Heavily damages all enemies with elemental fire!
  • Ice - Damages an enemy with a freezing ice attack!
  • Bolt - Damages an enemy with an electric bolt!

With the spell descriptions added, let's move on to the menu itself.

Menus

The in-game menu sidebar has an option for "Item" but nothing for Magic. When the player selects "Item" the item-menu is opened using the ItemMenuState. We need to create a new complementary "Magic" option and MagicMenuState.

Now's a good time to create an empty MagicMenuState.lua file and add references to the manifest and dependancies files.

Before writing the code for MagicMenuState let's hook it into the menu code. We'll add it's state transition, in InGameMenu.lua, as shown below.

function InGameMenuState:Create(stack, mapDef)
    local this =
    {
        -- code omitted
    }

    this.mStateMachine = StateMachine:Create
    {
        ["frontmenu"] =
        function()
            return FrontMenuState:Create(this)
        end,
        ["items"] =
        function()
            return ItemMenuState:Create(this)
        end,
        -- This is the new entry for the magic menu!
        ["magic"] =
        function()
            return MagicMenuState:Create(this)
        end,

Next let's add the "Magic" text to the sidebar menu. Here's the change in FrontMenuState.

function FrontMenuState:Create(parent)

    -- code omitted

    local this
    this =
    {
        -- code omitted

        mSelections = Selection:Create
        {
            spacingY = 32,
            data =
            {
                { id = "items", text = "Items" },
                { id = "status", text = "Status" },
                { id = "equipment", text = "Equipment" },
                { id = "magic", text = "Magic"}, -- New Entry
                { id = "save", text = "Save" },
                { id = "load", text = "Load" }
            },

With those changes the menu now looks like this:

New magic menu option.

We've successfully added the menu option but the alignment is a bit off. Let's reduce the spacing, which is defined above, from 32 to 28. Here's the updated version that looks better:

Better aligned magic menu options.

Now if you click on the Magic option two things will happen:

  1. You'll be prompted to choose a party member
  2. After choosing one it will crash!

It crashes because we haven't written the MagicMenuState yet.

The prompt to choose a party member is more interesting. In our game, there's only one character that can use magic. Therefore there's an argument to skip the character selection and jump straight to the magic menu but then it's not quite clear whose spells we're browsing. We'll keep the character selection but only allow the mage to be selected, this might be more complicated to write but I think it flows better.

Party Member to Magic Menu

The next change ensures that when the player selects a party member, they get redirected to the magic menu.

function FrontMenuState:OnPartyMemberChosen(actorIndex, actorSummary)
    -- Need to move state according to menu selection

    local indexToStateId =
    {
        [2] = "status",
        [3] = "equip",
        [4] = "magic"  -- <-- we've added this option
    }

When a party member is selected, the OnPartyMemberChosen callback works out which state to transition to by getting the index of the option menu.

I don't like this.

Updating this indexToStateId table here is messy and easy to miss when adding a new menu option.

These polish articles are about polishing the user experience, not the code but let's refactor this and make it better!

The selection menu entries are defined like this:

data =
{
    { id = "items", text = "Items" },
    { id = "status", text = "Status" },
    { id = "equipment", text = "Equipment" },
    { id = "magic", text = "Magic"},
    { id = "save", text = "Save" },
    { id = "load", text = "Load" }
},

Ok, this is where we should have the state id, not in a random table in the OnPartyMemberChosen function. Let's update it.

data =
{
    { id = "items",     stateId = "items",  text = "Items" },
    { id = "status",    stateId = "status", text = "Status" },
    { id = "equipment", stateId = "equip",  text = "Equipment" },
    { id = "magic",     stateId = "magic",  text = "Magic"},
    { id = "save", text = "Save" },
    { id = "load", text = "Load" }
},

Then we can change some of the transition code. For instance, take the OnMenuClick function below:

function FrontMenuState:OnMenuClick(index, item)

    if item.id == "items" then
        return self.mStateMachine:Change("items")
    end

This can be changed to the following:

function FrontMenuState:OnMenuClick(index, item)

    if item.id == "items" then
        return self.mStateMachine:Change(item.stateId)
    end

A small change but a change for the better I'd say. Now let's get back to OnPartyMemberChosen and update that too. Here's the original:

function FrontMenuState:OnPartyMemberChosen(actorIndex, actorSummary)

    local indexToStateId =
    {
        [2] = "status",
        [3] = "equip",
        [4] = "magic"
        -- more states can go here.
    }

    local actor = actorSummary.mActor
    local index = self.mSelections:GetIndex()
    local stateId = indexToStateId[index]
    self.mStateMachine:Change(stateId, actor)
end

To this:

function FrontMenuState:OnPartyMemberChosen(actorIndex, actorSummary)

    local item = self.mSelections:SelectedItem()
    local actor = actorSummary.mActor

    self.mStateMachine:Change(item.stateId, actor)
end

Ah, that feels a bit better!

Restricting the Party Member Selector for Magic

Right, when the player selects Magic from the sidebar, they should only be able to choose the mage. The code that handles the selector is shown below:

function FrontMenuState:Update(dt)

    if self.mInPartyMenu then
        self.mPartyMenu:HandleInput()

Let's create a new function called HandlePartyMenuInput and replace the above code with a call to that.

function FrontMenuState:HandlePartyMenuInput()

    local item = self.mSelections:SelectedItem()

    if item.id == "magic" then
        if Keyboard.JustPressed(KEY_SPACE) then
            self.mPartyMenu:OnClick()
        end
        return
    end

    self.mPartyMenu:HandleInput()

end

function FrontMenuState:Update(dt)

    if self.mInPartyMenu then
        self:HandlePartyMenuInput()

If you run the code now and choose Magic you'll enter the party selector but won't be able to move because we've taken over the controls. You'll only be able to perform the selection task. (If you select the mage, it will crash - but don't worry about that, we'll get to it soon!)

The restricted selection works ... but it only works by chance! The first character selected happens to be the mage. We need to ensure that the mage character is the one selected no matter the party order. (If we had more than one spell caster I'd just let you move through all the party members but you'd only be able to click on on spell casters).

When we select an item like magic or status, OnMenuClick gets called. You can see the code below.

function FrontMenuState:OnMenuClick(index, item)

    if item.id == "items" then
        return self.mStateMachine:Change(item.stateId)
    end

    if item.id == "save" then
        if self.mParent.mMapDef.can_save then
            self.mStack:Pop()
            Save:Save()
            self.mStack:PushFit(gRenderer, 0, 0, "Saved!")
        end
        return
    end

    -- and so on

There's some bad behaviour with the way this currently works. Try the following:

  1. Choose Status
  2. Select the third party member
  3. Back out of the character select
  4. Chose Magic
  5. The third party member is the still selected!!

The party member chooser should default to the first party member.

To do this we'll use add a selection menu function called JumpToFirstItem and call it as shown below:

function FrontMenuState:OnMenuClick(index, item)

    if item.id == "items" then
        return self.mStateMachine:Change(item.stateId)
    end

    -- code omitted

    self.mPartyMenu:JumpToFirstItem() -- <- reset the selection cursor
    self.mInPartyMenu = true

Let's define JumpToFirstItem in Selection.lua.

function Selection:JumpToFirstItem()
    self.mFocusY = 1
    self.mFocusX = 1
    self.mDisplayStart = 1
end

When the players select Magic the mage character should be selected by default. On opening the selector we'll start with the top party member, check if they're a mage and if not then we'll move down through the list until we find one. If we don't find a mage, we'll reset the cursor to the first element and print out an error message. Here's the code:

function FrontMenuState:OnMenuClick(index, item)

    -- code omitted

    self.mPartyMenu:JumpToFirstItem()
    self.mInPartyMenu = true
    self.mSelections:HideCursor()
    self.mPartyMenu:ShowCursor()
    self.mPrevTopBarText = self.mTopBarText
    self.mTopBarText = "Choose a party member"

    if item.id == "magic" then
        local member = self.mPartyMenu:SelectedItem()
        local memberCount = #self.mPartyMenu.mDataSource

        -- Start at the top and scroll down
        -- until you hit a mage character

        while member.mActor.mId ~= "mage" and
            self.mPartyMenu.mFocusY < memberCount do

            self.mPartyMenu:MoveDown()
            member = self.mPartyMenu:SelectedItem()
        end

        if(member.mActor.mId ~= "mage") then
            print("Couldn't find mage!");
            self.mPartyMenu:JumpToFirstItem()
        end

    end
end

That's the selector working as we'd like! Next we need to open the magic menu itself.

Opening the Magic Menu

When writing the book I made a magic menu but it was cut when the book was getting too long. My original menu looked like this:

The original menu.

This menu is a good starting point but it needs tweaks:

  1. The top right panel was going to contain the magic types. We don't have magic types, so we'll leave it empty.
  2. Spell cost needs moving next to it's name.
  3. Let's join the second row of panels into one long panel. Keep the casters name and add their current mp.
  4. Activate the cursor so the player can automatically browse the spells

Thiis is a lot of UI code. Rather than write inline, you can check out the code commit over here.

Here's the rearranged menu:

The updated menu.

The player can now enter the magic menu from the in-game menu and browse the spells they've unlocked!

In the next polish article we'll add a feature so the player can use certain items and spells on the world map.

Source

Here's a github with an updated project here.