Dev Diaries - Building a JRPG - Part 12

A short, and relaxing exercise awaits us today. We'll be using the Jrpg.MenuSystem we have built so far to render 3 menus which mimic the Party menu found in Final Fantasy VII.

The challenge is to see if we can render these menus through definitions, and see if the rendering code we have written actually works beyond the cases we have discussed so far in Part 10, and Part 11.

FF7 Party Menu

For this exercise, 3 menus will be created:

  1. The menu to show all party members, and their basic statistics. This is the largest menu which will be displayed on screen.
  2. A smaller menu to display miscellaneous statistics shown in other RPGs: time, gil, and number of steps taken.
  3. A menu containing the list of options which the player can choose to configure the characters, and perform other system functions.

The cursor is focused on the menu with the list of options. Therefore, the order in which we create, and push menus to the MenuStack matters here. From the rules of our system, it means that any menu which displays the instance of Cursor should be the active, or in other words, the top-most menu.

We will keep this in mind while designing the menu.

Here is the definition of the party menu. The values of the statistics should be tokens, but for the purposes of demonstration, I have made the statistics content a plain TEXT type and have hard coded the values.

By now, I hope that the schema of the menu definition is easier to read now that we have gone over a few examples.

{
    "Key": "menu-party",
    "Location": {
        "X": 7,
        "Y": 3
    },
    "Size": {
        "Width": 50,
        "Height": 34
    },
    "Contents": [
        {
            "Key": "party-hero-face",
            "Type": "IMAGE",
            "Content": "bonez",
            "Size": {
                "Width": 6,
                "Height": 6
            },
            "Location": {
                "X": 3,
                "Y": 3
            }
        },
        {
            "Key": "party-hero-name",
            "Type": "TEXT",
            "Content": "BONEZ",
            "Size": {
                "Width": 5,
                "Height": 1
            },
            "Location": {
                "X": 10,
                "Y": 4
            }
        },
        {
            "Key": "party-hero-level",
            "Type": "TEXT",
            "Content": "LV  1",
            "Size": {
                "Width": 10,
                "Height": 1
            },
            "Location": {
                "X": 10,
                "Y": 5
            }
        },
        {
            "Key": "party-hero-hp",
            "Type": "TEXT",
            "Content": "HP  35/35",
            "Size": {
                "Width": 10,
                "Height": 1
            },
            "Location": {
                "X": 10,
                "Y": 6
            }
        },
        {
            "Key": "party-hero-band-hp",
            "Type": "IMAGE",
            "Content": "bar",
            "Size": {
                "Width": 8,
                "Height": 1
            },
            "Location": {
                "X": 10,
                "Y": 7
            }
        },
        {
            "Key": "party-hero-mp",
            "Type": "TEXT",
            "Content": "MP  5/5",
            "Size": {
                "Width": 10,
                "Height": 1
            },
            "Location": {
                "X": 10,
                "Y": 8
            }
        },
        {
            "Key": "party-hero-band-mp",
            "Type": "IMAGE",
            "Content": "bar",
            "Size": {
                "Width": 8,
                "Height": 1
            },
            "Location": {
                "X": 10,
                "Y": 9
            }
        }
    ]
}

I have decided to use a few images in this menu to make things less boring (portrait, and gradient bar as a rule under HP, and MP). Other than that, this menu is just displaying content, which its contents should ideally be a TOKEN type for any dynamic values.

Oh, and for those who are wondering what the heck happened to Cloud, I decided to change the hero sprite for this post since I have been working with an artist, Aoraki_Arts (AorakiTowardTerra@gmail.com). He provided a cool skeleton sprite sheet I had requested, and worked quickly to get it done for this demo:

Large Skeleton

I highly recommend you contact him if you would like anything commissioned!

For the small menu displaying miscellaneous information, we can hard code the values copied from the Final Fantasy VII Party menu:

{
    "Key": "menu-time",
    "Location": {
        "X": 45,
        "Y": 0
    },
    "Size": {
        "Width": 17,
        "Height": 5
    },
    "Contents": [
        {
            "Key": "time-time",
            "Type": "TEXT",
            "Size": {
                "Width": 10,
                "Height": 1
            },
            "Location": {
                "X": 1,
                "Y": 1
            },
            "Content": "Time:   0:44:37"
        },
        {
            "Key": "time-gil",
            "Type": "TEXT",
            "Size": {
                "Width": 10,
                "Height": 1
            },
            "Location": {
                "X": 1,
                "Y": 2
            },
            "Content": "Gil:    2244"
        },
        {
            "Key": "time-steps",
            "Type": "TEXT",
            "Size": {
                "Width": 10,
                "Height": 1
            },
            "Location": {
                "X": 1,
                "Y": 3
            },
            "Content": "Steps:  678"
        }
    ]
}

Again, these are all TEXT types, and so, we should use our imagination for now that these are dynamically changing. 😊

For the list of options available in the party menu, we'll just directly copy most of the options from the Final Fantasy VII Party menu I have been inspired by:

{
    "Key": "menu-party-options",
    "Location": {
        "X": 50,
        "Y": 27
    },
    "Size": {
        "Width": 12,
        "Height": 11
    },
    "Contents": [
        {
            "Key": "option-item",
            "Type": "OPTION",
            "Size": {
                "Width": 10,
                "Height": 1
            },
            "Location": {
                "X": 1,
                "Y": 1
            },
            "Content": "Items",
            "Index": 0,
            "Handler": "MenuContentOptionItemHandler, Assembly-CSharp"
        },
        {
            "Key": "option-magic",
            "Type": "OPTION",
            "Size": {
                "Width": 10,
                "Height": 1
            },
            "Location": {
                "X": 1,
                "Y": 2
            },
            "Content": "Magic",
            "Index": 1,
            "Handler": "MenuContentOptionMagicHandler, Assembly-CSharp"
        },
        {
            "Key": "option-equip",
            "Type": "OPTION",
            "Size": {
                "Width": 10,
                "Height": 1
            },
            "Location": {
                "X": 1,
                "Y": 3
            },
            "Content": "Equip",
            "Index": 2,
            "Handler": "MenuContentOptionEquipHandler, Assembly-CSharp"
        },
        {
            "Key": "option-status",
            "Type": "OPTION",
            "Size": {
                "Width": 10,
                "Height": 1
            },
            "Location": {
                "X": 1,
                "Y": 4
            },
            "Content": "Status",
            "Index": 3,
            "Handler": "MenuContentOptionStatusHandler, Assembly-CSharp"
        },
        {
            "Key": "option-formation",
            "Type": "OPTION",
            "Size": {
                "Width": 10,
                "Height": 1
            },
            "Location": {
                "X": 1,
                "Y": 5
            },
            "Content": "Formation",
            "Index": 4,
            "Handler": "MenuContentOptionStatusHandler, Assembly-CSharp"
        },
        {
            "Key": "option-customize",
            "Type": "OPTION",
            "Size": {
                "Width": 10,
                "Height": 1
            },
            "Location": {
                "X": 1,
                "Y": 6
            },
            "Content": "Customize",
            "Index": 5,
            "Handler": "MenuContentOptionStatusHandler, Assembly-CSharp"
        },
        {
            "Key": "option-config",
            "Type": "OPTION",
            "Size": {
                "Width": 10,
                "Height": 1
            },
            "Location": {
                "X": 1,
                "Y": 7
            },
            "Content": "Config",
            "Index": 6,
            "Handler": "MenuContentOptionStatusHandler, Assembly-CSharp"
        },
        {
            "Key": "option-save",
            "Type": "OPTION",
            "Size": {
                "Width": 10,
                "Height": 1
            },
            "Location": {
                "X": 1,
                "Y": 9
            },
            "Content": "Save",
            "Index": 7,
            "Handler": "MenuContentOptionStatusHandler, Assembly-CSharp"
        }
    ]
}

Each element within the Contents collection is a MenuContent with OPTION type. The Handler implementations aren't important for this discussion, so I will not be implementing them here.

Now, within our game, we will want to build the game menus, and push them into MenuStack in the proper order. Menus other than menu-party-options can be inserted in any other order since we are not dependent on the cursor to be focused on the items there.

private void BuildGameMenus()
{
    Menus.Push(MenuBuilder.BuildFromDefinition("menu-party"));
    Menus.Push(MenuBuilder.BuildFromDefinition("menu-time"));
    Menus.Push(MenuBuilder.BuildFromDefinition("menu-party-options"));
}

With just 3 definitions, we have a rendered Party menu!

Party Menu

Here's a quick demo video of it all in action!

Conclusion

Okay, I think I have played around with menus long enough for now. Let's get back on track, and work on some Enemy AI in the next chapter!