|
On the fourth Monday of each month, we explore the code underneath The Broken Hourglass, the game environment called "WeiNGINE." This month, we explore the underpinnings of experience progression, the level path system. Last week we discussed level paths in The Broken Hourglass from a player's perspective. As a follow-up, we will use this month's Inside the Engine department to consider how level paths work from an engine perspective, and show how to create a new one.
There are a number of engine functions which govern the spending of experience points on character abilities. _attribute_upgrade causes a character to spend XP to purchase points in a valid skill or attribute, while _toggle_trait adds a trait to that character. Supporting functions like _can_toggle_trait (to ensure that there are no unmet prerequisites for the trait) and _attribute_upgrade_cost can help us determine whether the purchases we want to make are possible.
But rather than reinvent the wheel and apply this logic every single time a character levels up, we have a function which performs the skill buying and experience bookkeeping automatically. This makes the creation of a new level path extremely easy by following just a few simple rules.
The function is called _generic_level_path and its arguments are listed below:
_generic_level_path
buyer : thing -> max_num_of_traits : int -> trait_percentage : int -> non_trait_percentage : int -> trait_list : string list -> non_trait_list : string * int list -> unit
While this may look daunting, the arguments are in fact quite simple to understand and all of them are easily identifiable as part of the leveling process. We explain them below:
buyer: This is a thing (always a CREATURE, in fact) which will spend experience along this path. The buyer will typically be provided to us automatically by the engine.
max_num_of_traits: This integer tells the level path function the limit on the number of traits we want the level path to buy at each level-up. In general, paths with many traits available will have this value set to 2 or 3, while paths with fewer associated traits will have this value set to 1.
trait_percentage: This integer tells the level path function the limit on the amount of experience we want the level path to spend on traits at each level-up. For example, if Nekos levels up when he has 150 points to spend and he is following a path with a trait_percentage of 30, the game will not spend more than 150 * 0.3 = 45 points on traits--even if we have not reached the max_num_of_traits limit during that level-up. So both the max_num_of_traits value, and the actual amount of experience available for traits, act to limit the traits which may be bought.
non_trait_percentage: This integer tells the level path function the limit on the amount of experience we want the level path to spend on attributes at each level-up. For example, if Carind levels up when she has 150 points to spend and she is following a path with a non_trait_percentage of 50, the game will put 150 * 0.5 = 75 points into her attribute pool. As you may remember from last week, points not spent on traits are also added into the attribute pool. Therefore, the attribute pool will almost always be at least a little larger than you would expect from the non_trait_percentage value. trait_list: This is an ordered string list giving the name of each trait which could be bought for this path. It is ordered in the sense that traits are considered from left to right for purchase. For example, if Ruvanet is following a path which has a trait list of [ "life-force", "armor-optimization", "battlefield-magic", "consecrated", "magic-wielder"] the level path will try to buy life-force first. If it cannot afford the trait, cannot buy it for some other reason, or Ruvanet already has the trait from a previous purchase, it then moves on to armor-optimization, and so forth. The path stops evaluating traits when it reaches the end of the list, or the trait pool is exhausted, or the max_num_of_traits limit is reached during this level-up.
non_trait_list: This is a special paired list which pairs the name of each attribute which should be purchased during this level-up with an integer which tells the path what share of the points that attribute should receive. Unlike the trait list, the order of elements in this list does not matter. For example, consider the "simple fighter" path proposed last week. It asks to spend 50% of points on Strength, 25% on Health and 25% on Sword Precision, so its non_trait_list looks like this: ["strength"::50, "health"::25, "sword_precision"::25] The level path splits up the total amount of points in the attribute pool (from the non_trait_percentage amount and any leftovers from trait purchases) and buys attributes according to this list. Any unspent remainder points will be left behind in the character's free spending pool. Note that the integer values do not have to add up to 100--the level path function will automatically factor the shares correctly even if the list is ["strength"::10, "health"::5, "sword_precision"::5]
With all that in mind, we can quickly design a new level path.
Consider a hypothetical "Earth Warrior" path, designed for an individual who favors the use of Earth element magic to resolve disputes but also isn't afraid to pick up a stout stick and swing it at opponents.
Desirable traits, in order, for such a path would include (truncated for simplicity in the example):
lore-of-earth: Receive bonus damage when you inflict Earth-based wounds on a target. speedy-attacker: Your fighting style gives the Rapid quality to any weapon you wield, making your attacks harder to defend against. regeneration: Gain the ability to heal wounds more quickly without rest or other intervention. guarded-invocation: Learn how to cast spells without dropping your guard.
Desirable attributes for this path, and a recommended point share (adding up to 100 here for clarity) for each, would include:
earth_precision::30 -- Bone up on the ability to cast more effective Earth-based magics. mana::15 -- Gain more magical strength. hafted_precision::20 -- The right skill for a club-wielding Earth Warrior. health::10 -- Has anybody ever had too many hit points? strength::25 -- Strike harder and carry a bigger stick without encumbrance.
The only other details we need to decide on are the maximum number of traits per level up and percentages for each spending pool. Assuming there would be many more traits in a finalized path, a max_num_of_traits of 2 seems reasonable. We will spend 30% of the points on traits and 45% on attributes, leaving 25% (plus any leftovers) for the player to freely spend. By convention, trait_percentage and non_trait_percentage should usually add up to no more than 80.
We can now create our path. The path needs two different game resources in order to do its magic. The first is a PATH resource, consisting of a name and description of the path so that it may be shown to the player. The second is a script, which indicates what should happen when the path is selected at the time a character levels up.
First, we will create the Path, a short and simple XML file.
<<<<<<<< path/earthwarrior-path <xml> <Name value=~Earth Warrior~/> <Description value=~A fervent defender of the land, the <b>Earth Warrior</b> specializes both in the magic of natural phenomenon and a mastery of primitive weapons in order to hold his or her opponents at bay.~/> </xml>
Now, we need a script to power this path. One of the game engine's many scriptable events is the @after_level_up event, which occurs when a player selects a character and asks for a level increase. @after_level_up's arguments are the creature leveling up, and the path being taken. Here's what our Earth Warrior script looks like.
<<<<<<<< script/earthwarrior-path @after_level_up "earthwarrior" fun buyer buyer_path { _generic_level_path buyer // We know who the "buyer" is, as that argument was passed in when this event was triggered by a character performing a level-up. 2 // This is the max_num_of_traits value, we settled on two. 30 // This is trait_percentage, 30%. 45 // This is non_trait_percentage, 45%. ["lore-of-earth", "speedy-attacker", "regeneration", "guarded-invocation"] // This is our string list of traits to buy, in the order we want to consider buying them. ["earth_precision"::30, "mana"::15, "hafted_precision"::20, "health"::10, "strength"::25] // This is our special list of attributes paired with the share of points we want to spend on that attribute. } foreach ["earthwarrior-path"::"PATH"] // This foreach line associates our script with the Earth Warrior path.
That is all one needs to do in order to introduce a new path to the game! When a player is following our Earth Warrior example path and clicks Level Up, this script will be called, and the character will buy up to two traits from our list, spending no more than 30% of existing experience on the purchase. It will then move on to buy points in five attributes according to the shares indicated, spending 45% of existing experience, plus anything left over from the trait phase. After that, the player will be free to spend the rest of the points.
Of course, if a particular path concept required the creation of new and unique traits, those traits would also have to be created before the path could use them. A level path need not use _generic_level_path -- a custom level path could be created using _attribute_upgrade, _toggle_trait, and other scripting commands. But _generic_level_path provides a great deal of flexibility for developers and a simple, uniform way to introduce new "character classes" to the game. |