Undertale plays a lot more like a visual novel than an RPG. Most of the sequences that aren't random encounters are pretty heavily scripted. But here are my protips on RPG games:
First, develop how the rules of the RPG works. Let's make a character object. We can divide this into enemy/ally characters later. Define your stats and how they interact with each other. So for example, let's say each character has the following stats:
HP. Amount of points of damage you can take before dying.
MP. Amount of points of magic you can use before empty.
Attack. Amount of damage added to an attack you initiate.
Defense. Amount of damage removed to an attack you receive.
Speed. Determines when you can attack according to the function speedOrder()
We need to define how each of them works. Above I added a definition for each.
Now, with some stats, like HP and MP, there's actually two variables responsible for these stats – current and max HP. In addition it's generally a good idea to store other stats so that you can easily reference them for stat modifier calculations. If we add that, it's also a good idea to include a counter that tracks how much that stat modifier has changed in the course of a battle without having to calculate things in reverse. That way you can apply stat boost caps and stuff like that.
I don't know anything about the language you're writing in, but if this were a Java-like language, I'd probably have each stat be an instance of a stat object, so that calculations can be calculated quickly. You can override functions like "calculate current" or "level up" easily.
We now have some basic stats laid out. Now we need to define how characters can interact with each other. We're going to do this in a global controller object of some sort.
Usually, in RPG battles, there is a WORKFLOW that defines how the battle plays out. Think of this as a diagram that illustrates what happens when you click through different menus or play the game. Let's break down Undertale as an example:
In Undertale, you have a set of options when battling. The following global effect is applied to all menus:
If there are more than 1 monster on the battlefield, have an additional menu that inputs target selection.
The following menus have the following functionality.
Fight – A small UI appears that involves a cursor moving to the right. Try to center the cursor for maximum damage. Various items change how this functions. Enemy takes damage if attack hits. Then waits for the next step.
Interact – Depending on your target, have a selection of options that change various characteristics of the enemy. Then waits for the next step.
Item – Pulls up a list of the inventory and allows the user to select an item to use. Then waits for next step.
Mercy – If conditions have been met, can let go some monsters for GOLD. Then waits for next step.
Flee – If conditions have been met, can leave the battle.
Then, the game initiates with the following logic:
If there are no enemies on the field, you win the battle, and leave the battle. Else, the enemy attacks with a pattern. This can be either pre-scripted or selected randomly out of a set of patterns.
This loop will continue until one of the following happens:
- The enemies are all gone (all spared or killed).
- Frisk is killed
- Frisk flees
- Alternate condition (script, item)
We're going to focus on the first three, since the latter is usually added after the main engine is in place. Let's call these conditions Victory, Defeat and Flee. Let's define what action occurs when these conditions are met:
Victory – Distributes GOLD and EXP received in battle. Displays any stat changes if increase in LV.
Defeat – Displays game over screen, and allows player to load from last checkpoint.
Flee – Displays flee animation and leaves the battle with no rewards.
Now that we have the basic workflow defined, we can define each step in a function, and then call each step when they occur.
The last step of this process is defining the details of the movesets. This will be a more general discussion of RPGs instead of Undertale since Undertale is a poor example of this. Most moves are defined by a few properties – power, accuracy, element, MP cost, flavor text, icon, to name a few. With the exception of the last two, we can define the effects of these attacks to change the stats we defined before. For this, it would probably be best to use a move object that calculates all these things when a value is triggered.
Finally, put all the pieces in place. Have the global controller object monitoring the following things:
Enemy[] enemyList (an array of enemies)
Ally[] allyList (an array of allies)
Then as the battle progresses, you can use the index of the array (or whatever data structure you're using) to store the value of the target, making it easy to apply damage.
Things like status effects are built out similarly, and you can add triggers for them in moves and items.
So, for example, if you want the ally to attack an enemy with ally i, enemy k and move j, you could do something like this
allyList[i].attack(allyList[i].move[j], enemyList[k])
Which is a function that takes in a Move object and a Character object, and calculates the values accordingly.
Hope this helps. I'm not sure how coherent this all is but if you have any questions feel free to drop a line.