Bonus Subsystem Design
This is a page designed to review a proposal for the concepts behind a rebuild of the Bonus Subsystem of PCGen.
Contents
Background
The Bonus subsystem calculates numeric values primarily provided as inputs from the LST data. These values can be integer or fractional values. The values can be fixed (such as "3") or variable (such as "INT" or "CL=Fighter"). For the developers, these are stored as a pcgen.base.Formula object. For the data team, they will recognize the BONUS: LST token as providing inputs to the Bonus Subsystem.
Design Goals
- Ensure all BONUS: tokens are validated at input, equivalent to the base tokens (like ABILITY: or CHOOSE:). This error checking has proven valuable for the base tokens and significant helps to avoid issues in the core.
- Allow the calculations of Bonuses to be exposed to diagnosis without code modification. Currently, the BONUS system is a very tight loop and visibility to any given BONUS is limited. We should be able to have a method of diagnosis that evaluates each BONUS and how it was calculated.
- Enable the resolution of long chains of dependency. If, for example, a variable controls whether an Ability is granted and that granted Ability adds a BONUS to another variable that controls granting a second Ability, that becomes a long chain of dependencies. In the current design, PCGen cannot handle this depth of resolution when it interacts between subsystems. (Note: This is "enable", not "succeed at". This distinction will be explained later)
- Handle %LIST and LIST in an active fashion (push) rather than passive (pull). This will enable the Bonus subsystem to behave better in an event context (and be modular) rather than being triggered as a massive update.
- Get rid of BarJack's Transient error!
Context
This work is going on in parallel to making certain design patterns in the core more obvious. There are new terms, including Perspective and Scope from the Architecture Update 1Q2013. These concepts will reflect on this proposal. Becoming familiar with those concepts would be valuable before you progress through this proposal.
Note also that this explanation glosses over the replacement of %LIST and LIST within BONUS objects in the current design. Effectively this happens while Bonuses are being calculated and before they are combined.
Current Design
The current BONUS subsystem is managed by pcgen.core.BonusManager.
BonusManager stores the bonus values in a map, with a key that is carefully constructed based on how the Bonus was defined in the LST data. For example, BONUS:MISC|SR|formula|TYPE=Defensive.STACK is stored as: "MISC.SR:Defensive.STACK" and maps to the resolved value of "formula".
We will call: MISC the "Bonus Name" SR the "Bonus Info" Defensive the "Bonus Type" STACK as the "Stack Type"
As any individual Bonus is calculated, there are a number of steps that are performed: (1) Ensure all dependencies are pre-calculated (2) Calculate non-modified, STACK and REPLACE Stack Types individually (3) Appropriately combine the StackTypes for each BONUSTYPE. This combination is dependent upon whether the particular Bonus (a) stacks in certain ways (b) allows fractions (4) Combine all of the values for each BonusType for a given Bonus Name and Bonus Info.
Initial thoughts on a new structure
The first observation is that Bonuses cut across various scopes. This allows us to leverage those thoughts (and potentially any developed code) to assist in the design of the new system.
The second observation is that Bonuses end up having a rather similar structure to how other objects (like a Template) would be applied to a character. When added to a PC, they can be conditional or unconditional. This can be split up and resolved just as is done with Known and Available Spells. A similar situation exists for the formula resolution of a BONUS drawing parallels to other places. Today, we are not "pushing" any formula based items, but the structure can appear very similar to how a conditional object works.
With that, the picture shows a proposed facet structure for the Bonus system. Yes, this produces a significant number of facets to perform the calculation. Such complexity across classes has the effect of isolating each step in the calculation. This is useful for debugging, and tremendously valuable if the Core View feature (currently in a branch) is brought into the trunk to help debugging. So when looking at the facets, consider that all of the connections, inputs and outputs will be visible for debugging.
First look at the 7 facets within the inner brown box. Any time a Bonus is added to the Player Character, it is acted upon by the Input Facet. In this case, we have defined the scope to be: A Perspective (we can use "Spell Resistance" or "SR") The Bonus Type (like "Defensive" above" The Stack Type (nothing, STACK, or REPLACE).
So the add call to the Input Facet will look something like: add(CharID, BonusPerspective, BonusType, StackType, Formula, Object[source])
This Input facet sorts out the Bonus into two categories. The first category (following the green arrow because it has prerequisites) is passed to a CondiditionalFacet. The second category (those that are unconditional) are passed along the red arrow directly to the Active list of bonuses.
As some point (actually, when a character is made "dirty"), an Update Manager comes around to tell the ConditionalFacets to "udpate". This performs a resolution of the Prerequisites and for any that pass, the Formula is passed to the ConditionallyGrantedFacet. Objects added to that facet are then added to the ActiveFacet (active list of Bonuses).
At that point, the Bonus is still a Formula, so we need to resolve that formula. In some cases, that is static ("3"), in other cases it is variable ("INT"). We can again distinguish between these, passing the variable ones (along the lower green arrow) to a VariableFacet, and immediately resolving any static ones and passing those (along the lower red arrow) to a ResolvedFacet.
Again, the Update Manager can provide an update event that allows the variables to be calculated into their final values. This then leads to all of the Bonuses for a given Perspective, Bonus Type, and Stack Type to be put into the ResolvedFacet.
That structure produces the effective equivalent of #2 in the old process (but since the Scope of the calculation includes the Stack Type, there have been a few parallel calculations performed. This is represented by the multiple overlapping brown boxes. The multiple Stack Types (Stacking Rules in the picture - sorry) are then combined (old step #3) into the Bonus Resolution value. This is - of course - dependent on the stacking and fractional rules for that Perspective (note the dark red).
At that point, the total still has only been calculated for a specific BONUSTYPE (TYPE=Defensive in the example above). Each type is also calculated "in parallel" (as shown by the slate blue boxes) and are combined in a Bonus Total Facet (equivalent of #4 in the old process). At last we have just a Bonus Perspective ("Spell Resistance" and can return that value.