Setting up the new Formula System
The basics
The new formula system is designed to do a few things relative to data:
- Validate formulas at LST load instead of runtime
- Dramatically increase flexibility
- It will replace many tokens, including all the BONUS tokens
- It allows a sensible location for global modifications (things generally related to rule book type modifications that impact all objects of a given type)
Key concepts
To use the new system a few key things need to be remembered:
- It is possible to have local variables
- All variables must be defined prior to use
- Variables are not only numbers, but can be Strings, Dice, etc.
- We call this the variable FORMAT
You will want to keep track of what these key terms mean as you learn the new formula system:
- FORMAT: The type of variable, e.g. NUMBER, BOOLEAN, ORDEREDPAIR.
- SCOPE: The part of the data in which a (local) variable is available
- MODIFIER: An argument to MODIFY* tokens, specifically the "command" of what the modification represents
- OPERATOR: A mathematical operator as you would see in a formula ("+", "-", etc.)
- GROUP: A name for a set of objects in PCGen
- ASSOCIATION: An additional set of information attached to a MODIFY* token. This is a key-value pair of information, e.g. PRIORITY=100
Creating and Using Variables
Required Setup
Every format that needs to be used requires a default value. This value is shared throughout an entire system (it CANNOT be redefined). This is placed into a file referred to in a PCC file from the DATACONTROL: token.
The token used within that lst file is DEFAULTVARIABLEVALUE:
The format is DEFAULTVARIABLEVALUE:X|Y
- X is the format name
- Y is the default value
- Example: DEFAULTVARIABLEVALUE:NUMBER|0
How to define a variable
To define a variable, you need to add an LST file that is referred to in a PCC file from the VARIABLE: token.
There are 2 basic tokens for the VARIABLE file: GLOBAL and LOCAL.
- GLOBAL defines a global variable, usable from anywhere within the PlayerCharacter.
- LOCAL defines a local variable, usable only within the object on which the variable exists (or its children)
- The place where a local variable is usable is called the SCOPE. This could include a STAT (such as the stat's SCORE or STATMOD) or EQUIPMENT. The scopes possible in PCGen are hard-coded (see below for more info)
Both of those tokens can take an additional token on the line called EXPLANATION, which it is advisable to use to communicate to other data monkeys (and it is likely any future editor will also draw on this information)
The format of GLOBAL is GLOBAL:X=Y
- X is the format name
- Y is the variable name
- Example: GLOBAL:ORDEREDPAIR=Face
The format of LOCAL is LOCAL:W|X=Y
- W is the SCOPE of the variable
- X is the format name
- Y is the variable name
- Example: LOCAL:STAT|NUMBER=Score
A few notes about the VARIABLE files: If you see an item without a leading token, e.g.:
- ORDEREDPAIR=Face
...then the system is using the default (GLOBAL). For now, the data standard is to avoid using this shortcut (as far as I know)
If you see an item without a leading format ("X" in both GLOBAL and LOCAL above), e.g.:
- LOCAL:STAT|Score
...then the format is a NUMBER. For now, the data standard is to avoid using this shortcut (as far as I know)
Note both defaults can be combined, so a variable name with no other information on that line is a Global NUMBER
Setting and Modifying a variable
The MODIFY token is used to set a variable that is accessible in the current SCOPE. (Note: It could be local to the current scope or any ancestor of the current scope... so modifying a global variable always just requires MODIFY)
The format of the token is: MODIFY:V|W|X|Y=Z|Y=Z
- V is the Variable name for which the value will be modified
- This variable MUST be visible in the current scope. Therefore, it must be either a global variable or a local variable on the current object or its parents.
- W is the MODIFIER to be taken on the variable (e.g. SET)
- There can be multiple actions (more below), but SET will ALWAYS be available for any format.
- X is the value related to the action. If the action is set, the variable will be set to the value in this part of the token.
- Note: The value CAN be a formula. It is possible, for example, to SET a variable to OtherVar+4 ... the system will solve that formula. For legal OPERATORS (e.g. "+") , see below.
- Y is the name of an (optional) ASSOCIATION. For now, PRIORITY (more below) is the only available association.
- Z is the value of the association.
The MODIFYOTHER token is used to set a variable that is not accessible in the current SCOPE. Beyond what is necessary in MODIFY, this token also requires the "address" of where to go before it runs the modification. Think of it as the ability to remotely place a MODIFY on another object.
The format of the token is: MODIFYOTHER:T|U|V|W|X|Y=Z|Y=Z
- T is the FORMAT of the object on which the resulting MODIFY (as defined by V-Z) should occur
- U is the object or GROUP of objects on which the MODIFY (as defined by V-Z) should occur
- Note that this does not and will not support TYPE.
- V is the Variable name for which the value will be modified
- This variable MUST be visible in the current scope. Therefore, it must be either a global variable or a local variable on the current object or its parents.
- W is the MODIFIER to be taken on the variable (e.g. SET)
- There can be multiple actions (more below), but SET will ALWAYS be available for any format.
- X is the value related to the action. If the action is set, the variable will be set to the value in this part of the token.
- Note: The value CAN be a formula. It is possible, for example, to SET a variable to OtherVar+4 ... the system will solve that formula. For legal OPERATORS (e.g. "+") , see below.
- Y is the name of an (optional) ASSOCIATION. For now, PRIORITY (more below) is the only available association.
- Z is the value of the association.
Legal Values
FORMATs
Currently, the following are built-in formats:
- NUMBER
- STRING
- ORDEREDPAIR
- DICE
OPERATORs
For NUMBER, the following modifiers are available:
- +: performs addition
- -: performs subtraction (or is the unary minus operator to make a value negative)
- *: performs multiplication
- /: performs division
- %: performs a remainder function
- ^: performs an exponential calculation
- ==: Checks for equality (NOTE: Returns a BOOLEAN, not a NUMBER)
- !=: Checks for inequality (NOTE: Returns a BOOLEAN, not a NUMBER)
- >: Checks for greater than (NOTE: Returns a BOOLEAN, not a NUMBER)
- <: Checks for less than (NOTE: Returns a BOOLEAN, not a NUMBER)
- >=: Checks for greater than or equal to (NOTE: Returns a BOOLEAN, not a NUMBER)
- <=: Checks for less than or equal to (NOTE: Returns a BOOLEAN, not a NUMBER)
For BOOLEAN, the following modifiers are available:
- ==: Checks for equality
- !=: Checks for inequality
- &&: Performs a logical AND
- ||: Performs a logical OR
- !: Negates the value
MODIFIERs
For NUMBER, the following modifiers are available:
- SET: This sets the variable to the value provided in the "X" parameter
- ADD: This adds the current value of the variable to the value provided in the "X" parameter
- MAX: This takes the maximum value of the current value of the variable and the value provided in the "X" parameter (i.e. min(current value, "X"))
- MIN: This takes the minimum value of the current value of the variable and the value provided in the "X" parameter (i.e. max(current value, "X"))
- MULTIPLY: This multiplies the current value of the variable and the value provided in the "X" parameter
- DIVIDE: This divides the current value of the variable by the value provided in the "X" parameter
GROUPs
At present, the second argument of MODIFYOTHER supports 3 possible values:
- ALL
- This means the MODIFYOTHER will impact ALL objects of that type. This is useful for setting a general default for that value (e.g. you could default "HandsRequired" on weapons to 1)
- GROUP=X
- This is set by the GROUP token in LST data
- A specific object KEY
More will be added over time.
SCOPEs
The following are the legal scopes (not including GLOBAL):
- EQUIPMENT: This is a variable local to equipment
- EQUIPMENT.PART: This is a variable local to an equipment PART (or in older terms, an Equipment HEAD). Note that due to double sided weapons, Damage, CritMult and other items are generally on an equipment PART, not on the Equipment itself. Note also that this is a SUBSCOPE of the EQUIPMENT scope (so all variables in the EQUIPMENT scope can also be seen in the EQUIPMENT.PART scope)
- SAVE: This is a variable local to Save (or if you prefer, Check)
- SIZE: This is a variable local to the Size of a PC
- SKILL: This is a variable local to a Skill
- STAT: This is a variable local to a Stat
More will be added over time.
Examples
Understanding SubScope in practice
To understand how local variables work in SubScopes, consider the following (note this is not all the variables we will use but a critical set to define the scope):
- GLOBAL:NUMBER=Hands
- LOCAL:EQUIPMENT|NUMBER=HandsRequired
- LOCAL:EQUIPMENT|BOOLEAN=Penalized
- LOCAL:EQUIPMENT.PART|NUMBER=CritMult
The Equipment part is what the traditional system called a "head" (so a double headed axe has two "parts" in the new system - this was done since in some game systems, an item could have more than 2)
To understand how variables are available and how they get modified consider this:
Inside of a piece of equipment it could do something like:
- MODIFY:Penalized|SET|HandsRequired>Hands
This would be "true" if HandsRequired on the Equipment were more than the Hands on the PC. The "Hands" variable is Global, so it is available anywhere. A Template of "Extra Hands" could do something like: MODIFY:Hands|ADD|2 ... and that adds to the Global variable "Hands".
Now consider an Equipment Modifier that was something like "Lighter Than Normal". Assume the game benefit was one less hand is required. This would look like:
- Lighter Than Normal <> MODIFY:HandsRequired|ADD|-1
(This ignores for the moment any situation like "can't be reduced to below one".)
Note that the EquipmentModifier falls into EQUIPMENT.PART, so that it has access to the HandsRequired variable from the EQUIPMENT scope and can modify it. Note that this change could then impact the result of the BOOLEAN variable Penalized, since HandsRequired changed. Note also that "HandsRequired", when evaluated anywhere in that Equipment is now 1 less. Even if another EquipmentModifier evaluates HandsRequired, it is analyzing the variable from its parent scope, so that value on a variable from EQUIPMENT is the same in every EquipmentModifier (in the EQUIPMENT.PART scope) of that piece of Equipment.
Understanding SubScopes in relation to MODIFYOTHER
Normally MODIFYOTHER allows you to "address" another scope. For example:
- MODIFYOTHER:EQUIPMENT|Bastard Sword|HandsRequired|ADD|-1
However, since items like Damage and CritMult are actually on the EQUIPMENT.PART, that is different. If you consider how you would have to "address" the first head on "Bastard Sword", you would need something like:
- SOMEWEIRDTOKEN:EQUIPMENT|Bastard Sword|PART|1|CritMult|ADD|1
Note that this requires both a primary and secondary address. No token like this exists (or is planned). To alter items in a SubScope, you need to pass them down to the SubScope. An attempt to modify it at the Equipment level, such as:
- MyAbility <> MODIFYOTHER:EQUIPMENT:CritMult|ADD|1
... will fail (at LST load) because CritMult was on EQUIPMENT.PART, so it is not visible to the EQUIPMENT scope. Said another way, you can't universally modify all EQUIPMENT.PART variables by attempting to use that variable at a higher scope.
A more appropriate method (since this is relevant to a Feat that might alter CritMult) is to add another variable at the EQUIPMENT level:
- LOCAL:EQUIPMENT|NUMBER=CritMultAdder
If all the heads then get
- MODIFY:CritMult|ADD|CritMultAdder
... you can have the Feat do a MODIFYOTHER to alter CritMultAdder and the MODIFY will "pull" that into each PART:
- SomeFeat <> MODIFYOTHER:EQUIPMENT|ALL|CritMultAdd|ADD|1
So the data can be set up to only require one modification from a Feat and each Equipment Part can pull that modification down into the part.
Understanding PRIORITY and processing of MODIFY and MODIFYOTHER
When a PlayerCharacter has multiple items that attempt to modify a variable, they are sorted by two systems:
- First, they are sorted by their user priority. This is provided in the PRIORITY=Z association. Lower PRIORITY will be processed first.
- Second, they are sorted by their inherent priority. This is a built-in system that approximates mathematical priority rules. It ensures, for example, that a SET will occur before ADD if they both have a matching user priority.
Example: Assume we had the following items:
- MODIFY:Hands|MULTIPLY|2|PRIORITY=100
- MODIFY:Hands|SET|2|PRIORITY=50
- MODIFY:Hands|ADD|1|PRIORITY=50
The first thing is that we can tell from the PRIORITY items that the SET and ADD will both occur BEFORE the multiply (because 50 < 100). For the SET and ADD, we would need to know the inherent priority (For what it's worth, SET is 0, ADD is 1). This means the SET will happen first. So the result is Hands is SET to 2, then ADD 1 to get 3, then MULTIPLY by 2 to get 6.
By controlling the PRIORITY, the data team can reorder any calculation and perform much more complicated calculations than was possible with BONUS (where movement, for example, had multiple different BONUS tokens to try to do adding before and after a multiply). This is much the same as using parenthesis in a formula in normal math, but this allows you trigger any required ordering even if the MODIFY statements are in different objects.
Global Modifier File
There is a new file type that is defined in the PCC files with GLOBALMODIFIER. This file allows for a centralized (and clear to the data user) location for global modifications (don't hide things on stats or elsewhere anymore). This supports MODIFY for global variables (if you want to start all PCs with 2 hands for example) or MODIFYOTHER for local variables.