Tuesday, September 23, 2014

GuildCraft Dev Diary: Part XI

This post will continue the topic started in Part X of building a rules engine that can handle the complexities of a pen and paper RPG. The previous entry discussed the structure of Effects and how they should be applied to characters. This one will discuss how to use Effects to calculate the complexities of character statistics in a generic way.

Many RPGs have complicated rules for how different stat increases work. For example, various editions of Dungeons and Dragons have had the following stat calculation rules:
  • Bonuses of the same type from different sources do not stack.
  • Untyped bonuses are allowed to stack.
  • Dodge AC bonuses are a special case and allowed to stack.
  • Shields share an AC bonus with certain spells.
  • Some magic items can set an ability score to a specific value instead of modifying it.
  • Certain stats have a maximum value.
  • There are ways of altering a stat's maximum value.

Handling all of the above rules for stat calculation in a generic way can be challenging. In part, because part of the problem's scope is hidden.

When one looks at an RPG like Dungeons and Dragons, it is easy to underestimate just how many statistics a character in the game has. Using 5e D&D Basic as an example, consider a stat like Strength. A first glance at the rules or a character sheet would suggest that Strength is three stats: strength, strength modifier, strength saving throw. That is not the case.

A (partial) list of Strength statistics in Basic D&D:
  • Strength
  • Strength modifier
  • Strength maximum
  • Strength override (via gauntlets of ogre power)
  • Strength check advantage
  • Strength check proficiency
  • Strength saving throw

In combination with the FatEnum pattern described in Part III, a set of Interfaces can be used to label an 'AbilityType' enum so that it operates as multiple statistics. This is an important insight for the pattern that follows. It eliminates the need to have redundant enums such as 'AbilityMaximumType', 'AbilityModType', 'AbilitySavingThrowType', and 'AbilityOverrideType'. It also provides an automatic coupling between a stat and its related statistics.

IImmunity, IResistance, IVulnerability, ICyberEVirtualNet.

The diagrams and examples in this post will limit discussion to a base statistic, its maximum, and its override value. The pattern calls for a FatEnum class that implements IStat, IStatMaximum, and IStatOverride.
Aside: The same techniques can be used to implement other statistics, such as IStatMinimum.
Building upon the Effects class from Part X, an EffectsManager class was introduced to aggregate and manage the Effects on a character. When a stat is requested from the EffectsManager, it iterates over its list of Effects to create a set of modifiers grouped by BonusType. The full set of modifiers is then processed based on the rules for each individual BonusType category to generate a raw stat value.

Each BonusType contains its own information for how it is calculated. For example, a bonus that is "untyped" may stack with every other "untyped" bonus, but only the highest bonus of type "shield armor" will apply. It would also be possible to implement a more complicated calculation, such as a bonus type that added together the two highest modifiers and ignored the others.

If the IStat is an IStatMaximum the process is repeated for the related maximum value stat. If necessary, the two values are compared and the raw stat value is restricted.

If the IStat is an IStatOverride the process is repeated for the related override value stat. If an override value is specified, that value is returned instead of the raw value.
Aside: Performing the stat override check first is a performance improvement.
Since several stats are represented by the same enum value, the EffectsManager implements GetStatMaximum(IStatMaximum stat) and GetStatOverride(IStatOverride stat) methods in addition to the GetStat(IStat stat) method. Since IStatMaximum is a specialization of IStat, we are guaranteed that all IStatMaximums have a related IStat and incorrectly calling GetStatMaximum() for a stat that does not have a maximum will result in a compile error. A proper implementation of GetStat() will use these methods to prevent code duplication.

If your RPG system is masochistic, feel free to convert this to a floating point implementation.

A future dev diary will discuss techniques for providing situational and conditional Effects with the up-to-date state information they require.


No comments:

Post a Comment