Skill Trees ~ Integrating complex skill (combined, random, sequential, etc.) implementation - uchicago-cs/chiventure GitHub Wiki
Complex skills
The current implementation of skill effects supported by chiventure only assumes that one effect with a fixed amount of damage or modification and duration can be executed at a time. However, most RPGs (and other types of games) have the capability to use skills with several skill effects, creating complex skills.
There can be several types of complex skills that we can implement in chiventure. The most common ones, as well as the easiest ones to incorporate into skill effect execution, are combined skills, sequential skills, random skills, and conditional skills.
While we are unable to execute several skills at the same time with the current implementation, complex skills can be executed in order, where one complex skill consists of several simple skill effects that are executed one by one while a given condition is true (for example, if the first attack that works 70% of the time is successful).
Types of complex skills
Combined skills
This is the most common type of complex skills that can be incorporated into games. As an example, in Divinity: Original Sin II, an action RPG developed on 2017, skill crafting is supported, where a player can create a new skill by combining two different types of skills (an elemental skill and a non-elemental skill). While such a complex implementation of skill crafting is a goal for the future in the development of chiventure, we can combine skills so that they are technically executed at the same time, but implementation-wise executed in order.
Some examples:
- Deal 20 damage and incapacitate a victim for 30 seconds
- Modify two player statistics at the same time (e.x. health and combat)
- Poison the target for 60 seconds with 20% probability AND deal 10 damage
Current ideas for designing implementation for combined skills mostly involve utilizing simple boolean logic, so that if one skill is unable to be executed, the other one is still able to applied. For this, we can utilize the current skill_execute() function in skill.c that returns 0 if the skill execution was successful and 1 otherwise. If we can change the return type in that function to bool instead of int, we can add simple OR conditional statements (if skill_1 or skill_2 is true) to execute combined skills. That way the skills in the actual game will be applied at the same time and the execution function will return true if any of the skills were executed.
Combined skills can also consist of a combination of several simple and/or complex skills (we are currently thinking or setting a limit of 5-10 skills, since a combined skills can also include several combined or sequential skills). We are in progress of designing the combined skill implementation so that combined skills consisting of complex skills can be supported in the future without changing the existing code too much.
Sequential skills
Sequential skills are very similar to combined skills, but are meant to be executed in order within the game itself. The key difference between sequential skills and combined skills is that the execution of the next skill incorporated into a sequential skill is impossible if the execution of the first skill fails. An example of a sequential skill can be dealing 40 damage with 50% success rate, and if the attack is successful, the enemy is also poisoned / incapacitated for a certain amount of time.
There implementation that can be used for sequential skills also involves simple boolean logic (similar to combined skills), but instead of using OR conditions in the skill_execute() function, we can use AND (if skill_1 is true AND skill_2 is true) so that the function only returns true if both skills were successfully executed.
Conditional skills
Heads up: We currently plan to implement the above effect types first; what follows may or may not be added later this quarter, depending on how things play out.
For the most part, the skill effects above are missing one thing: context. RPGs often include skills that are aware of the world they exist in. Consider the following example skills we may want to implement:
- "Holy Blast: Deal 20 damage if the target is undead, or deal 10 damage otherwise." This skill somehow needs to determine if the enemy has a specific attribute ("undead") before splitting into one of two options.
- "Enrage: If the player is below 50% health, gain 30 physical attack." In this case, the skill checks something about the player before proceeding.
- "Impatience: Gain strength equal to the number of rounds that have passed this combat." This skill is particularly tricky, since the effect's strength depends directly on the result of any check on the world.
While some of the details of these effects might be challenging or overly specific, this pattern of skills that depend on context is common, and it is something we would definitely like to support at some point, even if the conditions we can check are limited at first.
Somehow, we need the information to flow from skill effect to skill effect; how might we do this?
Sequential Skills Revisited
In some sense, sequential skills already have some of the capabilities of conditional skills: they stop executing once one skill fails. What if we added a type of effect (perhaps the name "effect_component" is more apt) that looks at the world, and either fails or succeeds depending on what it sees.
Consider the Enrage effect again. Imagine for a moment that we had an effect with the following execution logic:
// Pseudocode
if (player_below_50_health):
return SUCCESS;
else:
return FAILURE;
If that were the case, we would simply be able the put it in a sequential effect. If the check failed, the sequential effect would halt; if it succeeded, the trivial "gain 30 physical attack" would trigger.
The Either Effect
Sequential Effects will not work for every skill; consider the "Holy Blast" skill, which breaks into different branches depending on if a condition is true. Technically, the effect could be implemented as the Sequence: deal 10 damage, check if the opponent is undead, and deal 10 damage. However, this relies on two 10 damage attacks being treated the same as one 20 damage attack, and it will not work for other skills like this.
Sometimes, failure of a sub-effect doesn't mean the whole effect fails. We need something like an if-else block in our logic. For now, I will call it the "Either" effect. The Either Effect is structurally quite simple; it points to three sub effects (which can be any type of effect themselves). The first one is the "if", it executes (maybe just looking at the world, maybe actually doing something), and returns either a success signal or a fail signal. On success, the "then" pointer executes, but on failure, the "else" pointer executes. The Either effect itself returns the result of its last executed sub-effect, so for "Holy Blast", successfully dealing 10 damage is considered a success, even though the initial check failed.
The Reader Effect
So far, we have relied upon a not-yet-explained mechanism for "looking at the world." This may be the hardest part of conditional skills to actually implement; while figuring out something about the world is relatively easy (with access to the chiventure_ctx object), being able to allow the game devs to specify what they are looking at will be very tricky.
In order to allow skills to understand context, we must add a group of effects (effect_components) called "Readers", each of which looks at something in the world and returns a value (a bool, or SUCCESS / FAILURE) to be used by the parent effect (Sequential or Either, as of now). Perhaps we have one that looks at stats, one that looks at attributes, and so on. If each reader only has one type of thing it looks at, this actually might be quite feasible. For the "Holy Blast" effect, we might expect the constructor function for such a reader to look like this:
//Pseudo code
new_attribute_reader_effect("undead", SINGLE_TARGET);
The SINGLE_TARGET enum tells the reader to go to "whatever the player is targeting." The first argument is simply the attribute to look at. While this may still seem hard, note that finding the target is something even simple skills will have to do, and accessing the attribute would be easy after that.
Complex Information Flow
Up until this point, we have relied on parent effects (Alternative and Sequential) to determine what sub-effects are executed. In order to move forward, we need to rethink the idea that SUCCESS and FAIL signals (or booleans) are sufficient for creating complex skills.
The third example skill, "Impatience," is troublesome under our previous framework; we need some way to allow more complex information to flow from effect to effect. The solution is simple, but it means changing some things about every other effect type. We need to, at the very least, have our effects return any int, instead of just SUCCESS and FAIL signals (or booleans). Doing this is feasible, if we declare certain numbers to represent success and failure when not being used for complex information, but this is messy. We may, instead of this, have a effect_result
or effect_context
struct, that contains both relevant information (the int), and the success code (a bool), and maybe some other stuff (the result string, the targets, the caster, etc). This would be passed into, modified, and returned by effects.
Now, in addition to succeeding and failing, effects might read some info about the world into memory. This could then be utilized by events downstream.
Variable Effects
We can now read in complex information about the state of the game; how do we use it?
Typically, we would specify how strong an effect is when we generate it, something like:
//Pseudocode
new_damage_effect(15) //Creates a damage effect that deals 15 damage;
new_stats_mod_effect("strength", duration=4, change=8) //A stat changing effect that buffs strength by 8 and lasts for 4 turns
This will not work for "Impatience," since it needs a different effect strength each time it is used. Instead, we might declare "Variable" effects, which wait until execution time to determine how much damage they do, or how strong an effect is:
//Pseudocode
new_variable_damage_effect() // The damage argument is read at execution time, instead of being locked in here.
new_variable_strength_stats_mod_effect("strength", duration=4) // The change argument is variable.
Now, when this effect executes, it receives an int through the effect_context object, which it uses that as its missing argument. If it doesn't find anything (-1, or perhaps an uninitialized flag in the context object), it simply fails.
The once impossible "Impatience" is impossible no more; it requires a Reader that finds the turn number, in sequence with a variable strength stat-up effect.
Random skills
It is possible to do Random Skills without Conditional Skills, but Random Skills become fairly easy if the full power of Conditional Skills is realized first (for the most part). In fact, Random skills are a huge reason to go all out on conditional skills.
Some skills we might want:
- "All-Out Attack: Deal 100 damage; misses 30% of the time." This effect succeeds or fails randomly.
- "Flex: Gain 10 strength-up for 3-5 turns." This effect has a random range in the outcome.
- "Chaos Beam: Equal chance to Stun, Freeze, or Burn target." This executes one of three potential outcomes.
In order to implement these skills, we need a few more effect types.
Random Chance
The Chance effect_component is super simple: when constructed, it is given a double that represents its success rate. When it executes, it generates a random number that determines whether it succeeds or fails (which it returns in the effect_context).
"All-Out Attack" simply becomes Chance(70%) in sequence with Damage(100). We actually do not need Conditional Skills in this case.
Random Range
The Range effect_component is a bit trickier: It is constructed with a range of values, representing the lower and upper bounds (inclusive). When it executes, it generates a number between those bounds, and passes that result forward through the effect_context type.
"Flex" is now possible: It is a Range(3, 5) effect in sequence with a Variable_Duration_Stat_Mod("strength", 10).
The Switch Block
The previous two effect types are "simple" in that they do not execute any other effects. This next effect type will join the ranks of Sequential, Either, and Combined effects as a "compound" effect that executes sub-effects.
"Chaos Beam" could be done with what we have so far, but it is rather ugly:
// A loose representation of the effect structure. Perhaps the DSL method of creating effects could look something vaguely similar.
Either{
if: Chance(.33),
then: Apply("Stun"),
else: Either{
if: Chance(.5),
then: Apply("Freeze")
else: Apply("Burn")
}
}
It is good that we can do this, but this is a solution for devs, who will likely shy away from nesting and weird math. Also, the nesting only gets worse as we add more possible outcomes.
We should introduce another effect_component, the Switch effect. It would take a list of possible outcomes, and (maybe) a list of ranges that cause each effect to trigger. For example, our chaos beam effect looks something like this:
Sequence{
Range(1, 3)
Options{
1-1: Apply("Freeze")
2-2: Apply("Burn")
3-3: Apply("Stun")
}
}
Why ranges instead of just an int? Ranges allow for giving different effects different weights; also, it opens up another type of skill. What if we have some skill "Intimidate," which lowers an opponent's Confidence based on their Charisma stat. We can feasibly do this now:
Sequence{
Reader_Stat("Charisma", SINGLE_TARGET)
Options{
0-5: Constant(20) // The Constant effect simply writes its argument into the the effect_ctx.
6-10: Constant(10)
11-15: Constant(5)
16+: Always_Fail() // This always returns a fail signal in the effect_ctx.
}
Variable_Strength_Stat_Mod("Confidence", duration:5)
}
That all being said, we might include convenience constructors for the cases where all the weights are equal (and for a Random Range and Options in sequence pattern), because they are likely to be used much more than the full Options effect.