Innocence Seekers: Akari of the Light – Battle system test (current progress)

Over the past few days, I have been working on the battle system test for Innocence Seekers: Akari of the Light, and this blog post will document my progress up to now. Right now, it’s far from complete; I haven’t even started on actually implementing battles. But I’ve set up the skeleton of what will become the UnitStats class, which will hold the base and calculated stats of every unit in the final game. So far, the class (which is implemented as a struct, meaning its members are public by default; I tend to prefer direct access over getter/setter functions and only use private members for members requiring restrictions on modification) has a name field, an ID field (which tells the game what kind of unit it is), level and experience fields, and the base and current stats.

What I have implemented is a basic unit stats editor. The following is what the stat modifier screen looks like at the moment:

Battle System Test stat modifier screen
A screenshot of the stat modifier screen in the Battle System Test

At the moment there are 11 options. The first option shows whether the unit is a player character or an enemy unit, as well as its index number (when I implement battles, indexes 1-4 will be the active party members) and name (in parentheses). Pressing the left or right arrow keys allows one to choose which unit to modify, while pressing Enter causes a prompt for a new name to appear. When this prompt appears, one can type in a new name, limited to 47 bytes (of UTF-8 text, not including the null terminator; the name field itself is 48 bytes). Note that one cannot move the “cursor” backwards; you’re always editing the end of the string (I also haven’t implemented copy-pasting); however, the backspace key works. Once one has finished typing in the new name, Enter can be pressed to commit the new name.

For all other options except the third, one can adjust its value by either pressing the left or right arrow keys, or the Page Up or Page Down keys. The arrow keys adjust the selected value by 1, while the Page Up/Down keys adjust by 10. For the third option (EXP), its value cannot be modified; it is changed whenever the level changes. Note that changing any of the stats other than level will modify the listed base stat instead of the actual value. Note that only level is capped (at 65,535); you can increase the other stats to the point of overflow, if you have the patience. However, you cannot decrease the base stats to zero (other than via overflow); one is the minimum. The calculated stats are themselves capped (HP at 40,000,000,000, stats other than MP at 9,999,999,999).

In the future, I plan to expand on this editor, and implement a way to type in a base HP (for those enemies with huge HP totals) and level (even with Page Up, it’s somewhat tedious to try and get to level 65,535). I don’t plan to implement a way to type in the base values for other stats (as I don’t envision base MP being higher than 20, and base values for other stats being higher than around 1000). Future versions of this editor will feature the enemy HP cap of 99,999,999,999,999,999,999 (yes, that’s 20 nines) To accommodate this value, I’ve implemented a custom 128-bit integer class to hold this value.

The following is the code of unit.h:

#include "int128.h"

#include <type_traits>
#include <cstdint>

#define LEVEL_CAP                              65535
#define PLAYER_HP_CAP                    40000000000
#define ENEMY_HP_CAP  UINT128_C(99999999999999999999)
#define STAT_CAP                          9999999999

struct UnitStats
{
    std::uint8_t name[48];
    std::uint32_t id;
    std::uint32_t level;
    std::uint64_t experience;
    struct BaseStats
    {
        std::uint64_t bhp;
        std::uint16_t bmp;
        std::uint16_t batk;
        std::uint16_t bdef;
        std::uint16_t bint;
        std::uint16_t bres;
        std::uint16_t bhit;
        std::uint16_t bspd;
        std::uint16_t dummy;
    } basestats;
    struct CurStats
    {
        UInt128 chp;
        UInt128 cmaxhp;
        std::uint32_t cmp;
        std::uint32_t cmaxmp;
        std::uint64_t catk;
        std::uint64_t cdef;
        std::uint64_t cint;
        std::uint64_t cres;
        std::uint64_t chit;
        std::uint64_t cspd;
    } curstats;

    std::uint64_t GetEXPToNext();
    std::uint32_t SetLevelFromEXP();
    void FillCurStats();
};

// Helper functions
std::uint64_t GetEXPToLevel(std::uint32_t level);
UInt128 CalcHP(std::uint64_t base, std::uint32_t level);
std::uint64_t CalcStats(std::uint16_t base, std::uint32_t level);
std::uint32_t CalcMP(std::uint16_t base, std::uint32_t level);

// Since this will be used in the program state structures, it must fulfil the
// requirements of a POD-struct.
static_assert(std::is_pod::value, "UnitStats structure is not a POD-struct.");
static_assert(sizeof(UnitStats) % 8 == 0, "The size of the UnitStats structure in bytes must be a multiple of 8.");

int128.h contains my custom 128-bit integer class (UInt128). type_traits contains std::is_pod, which I’ve used in a static assert to ensure the structure is a POD structure (i.e. it can be trivially copied, and is compatible with C; this is necessary to ensure the ability to save). cstdint supplies the std::uintX_t types, which are guaranteed to be X bits wide (the basic C types have implementation-defined sizes).

The sizes of members in this class are influenced by my choice of alignment. I expect 8-byte alignment (and placed a static assert to ensure that the struct size is a multiple of 8), so I’ve made bmp 16 bits (even though 8 bits will suffice) and inserted an unused member. level, cmp and cmaxmp are also 32-bit for this same reason (when normally 16 bits would suffice; I also made level 32-bit to guard against potential overflow, as the level cap is UINT16_MAX). Overall, this makes the BaseStats structure 24 bytes (if optimised for size, it would be 21 bytes) and the CurStats structure 88 bytes (instead of 84 if optimised for size). Overall, the UnitStats structure is 176 bytes.

The member functions are self-explanatory (note that SetLevelFromEXP() returns the amount of levels gained, and that FillCurStats() doesn’t do any healing, or even cap current HP and MP). The helper functions are declared in the header as they may be used by other parts of the game. I will insert more member and helper functions as I go.

I deliberately omitted parts of the header, such as the include guards, the copyright/description header and the preprocessor check to ensure that a C++ compiler is used. Also omitted is the version check; either a C++14 compatible compiler or Visual C++ 2015 (or later; note that Visual C++ has not updated its __cplusplus value for a long time, making it inaccurate when checking for compatibility) is required for the game. Note that this is a work in process, and many things (such as members holding spells, techniques and active status effects) are missing. I’ve set the compiler to compile without any compiler-specific extensions to ensure wide compatibility (I plan to ensure compatibility with GCC, Clang, and Visual C++).

That’s all I’ll say for now.

P.S. Moving from one person developing games to a group of girls developing games. The second Magic of Stella PV has been released. Watch it here.


Posted

in

by