SELECTION - A CHOOSE Replacement
Introduction
First draft proposal for SELECTION (replaces CHOOSE/supports multiple CHOOSE).
Please note that the token names here are for example purposes. Some discussion has taken place but they are not finalized. They are here to show the concepts and how the structures of this new system would work in LST files.
Code Concepts
Current Issues
Multiple Methods
There are currently 4 different methods for how CHOOSE operates in the code
- Abilities
- Race and Template
- Language
- Temporary Bonuses
The desire is to reduce this to one methodology (or two at worst if temporary bonuses need to be separate)
Single Selection
CHOOSE currently supports only one selection. In many cases this produces a challenging format that is forced into the code. If multiple selections were allowed, this would not be necessary, as one could be CLASS and the other LEVEL for example.
Automatic Expansion
CHOOSE currently automatically expands items. If you CHOOSE:FEAT, you get things like Weapon Proficiency(x) and Weapon Proficiency (y). This also then creates an interesting visual challenge if there are multiple tiers. If a Feat allows a selection from another MULT:YES feat, then you get Something(a(b)) and Something(a(c)). This can rapidly proliferate into a huge list to scroll through. It would be visually cleaner to select "a" and then select "b or c" within the context of "a" being selected under "something". This requires a new UI to handle this hierarchical behavior.
(This item is not currently addressed further here, as I would like to talk to some of our UI folks before I propose too much here)
CHOOSE:NOCHOICE
Simply shouldn't have to exist.
Stacking Ambiguities
This is true more for MULT:YES than CHOOSE in particular, but imagine something like this:
ABILITY:FEAT|VIRTUAL|Dodge
Now put that on a MULT:YES Feat and select it twice.
Do you get 2 copies of Dodge or 1 copy?
This has already been encountered in the past and discussed on the -dev list. The BONUSes also suffer from the same issue and there have been a few debates over proper behavior.
The solution to this problem is eliminate all ambiguity. We should to distinguish between the ability itself (applied only once ever) and what I will call the "instances" of the ability (applied once per time taken by the user). Note that I do NOT say "per selection" or "per choice" because if an item has 3 choices, we still have struggles. There are ways to apply once per choice as well, but we need ALL of the capabilities, so we have to have a method to do once per time taken by the user).
From this point onward, I will have an INSTANCE: token that then refers to instances rather than the parent object. This is much like the current "PART:x|..." token that can appear on equipment to handle equipment heads. Just like PART, it will have an argument (as to where it is applied) followed by another full token.
Current Quirks
Stacking
Currently the system defaults to STACK:NO. Should this be the default behavior when a lot more control exists for the data to define what is selectable?
Race Conditions
Part of the reason we have multiple different selection methods is that the code has certain race conditions (two actions that need to happen in a certain order).
For Race and Template, they get added to the PC first, THEN the selection is made. This means they have to be defending against undefined choices.
For Abilities, they only get added to the PC when an AbilitySelection object (an ability with its selection) is added to the PlayerCharacter. This means the selection occurs BEFORE the object is added.
Other Considerations
Conversion
No conversion will be automated, as the semantics here are completely different than the old system.
Also, since backwards compatibility will not be supported (CHOOSE will not be available in the new formula system at least under the current plan), we will need a new token name. From this point onward, it will be called SELECTION.
Design
Instances
As mentioned above, the Selections will be placed into instances. These instances are separate objects with a separate SCOPE. For Skills (For example), the scope will be SKILL.INSTANCE
Aligning Concepts to the new Formula System
In general, the new formula system uses the concept of a Scope to determine where a variable is processed.
A naive design would be to keep two lists on the main object. That way, the first set of choices are [0] in the lists, the next set [1]. While I don't have a specific rule associated to this, I can quickly come up with some plausible use cases where that would break. For example: Imagine a feat that allows you to select a class and make 2 skills from the classskilllist of that class get +1 bonus rank.
Therefore, we delegate containing the result of the selection down into an instance object. We then make that a subscope of the main scope for that object type. Consider the CHOOSE on skills for Speak Language. We then get something like:
INSTANCE:ALL|SELECTION:SpokenLanguage|LANGUAGE|getAll("LANGUAGE")
Now, every time a new instance of that Skill is selected (which happens to be from getting a new rank of that skill), a new Instance will be added to the PC and a new selection will need to be made.
It could then be added to a list of languages via:
INSTANCE:ALL|MODIFY:LanguageList|ADD|getSelection("SpokenLanguage")
(This assumes LanguageList is a global variable of Format "ARRAY[LANGUAGE]")
To be VERY CLEAR: getSelection("SpokenLanguage") MUST BE USED IN AN INSTANCE. If you attempt to use it up at the parent ability/skill/whatever, it will either (a) fail or (b) be empty. (It should fail, and we will endeavor to do that, but there will need to be enhancements to have functions available only in a limited scope... that's new effort)
Resolving the Race Condition
The race condition is particularly problematic for the new formula system. Consider a naive design like this:
MODIFY:MyList|ADD|Foo SELECTION:MySelection|STUFF|myList
This immediately sets up a problem based on how the formula system works. Because it only processes items that are actually "granted" to the PC, the MODIFY of MyList would not happen unless the object itself was already granted. This traps us (a bit) in making sure the object is added to the PC BEFORE a selection is made, so that if it chooses to do any list modifications, they can be done BEFORE the selection.
To resolve this race condition, we put the MODIFY on the parent object, which is granted BEFORE any selection. We then put the SELECTION in the INSTANCE, which is granted AFTER any selection. Note this also means that if (for some reason) the data team to have the MODIFY occur AFTER the selection, they have that power. This gives the data team full feature, supporting add both BEFORE and AFTER selection:
MODIFY:MyList|ADD|AddBeforeSelection INSTANCE:1|MODIFY:MyList|Add|AddAfterSelection
As a nice side effect, this actually gives us full power to avoid ANY popup windows in a new UI should that be a feature we want to implement. Because all instance applications can be deferred, they could be "TODOs" on the PC rather than stopping for user input.
Replicating NUMCHOICES
To replicate the behavior of NUMCHOICES, we then need to control the number of instances that we are allowed to apply.
This is a new token, MAXINSTANCES.
Replicating SELECT
To replace the behavior of SELECT, we need to control the number of actual selections that exist in an instance for that SELECTION. Since this is associated to the SELECTION, we need to keep it local to that token, so it is appended as an optional control on the SELECTION:
SELECTION:MySelection|LANGUAGE|getAll("LANGUAGE")|SELECT=2
Happy Side Effects of INSTANCE
Please note: ALL objects that are instance-able ALWAYS will produce an instance. This is true REGARDLESS of whether there is a SELECTION on the object. Many of these Instances will be empty shell objects, but they can also have some interesting uses.
"At Rank 5 this skill adds 2 legs to the PC"
INSTANCE:5|MODIFY:Legs|ADD|2
Note that this means there is no equivalent PRERANK: needed on a MODIFY. It is directly applied to the "5" instance as a MODIFY, and when the Rank hits 5, that INSTANCE will be granted and the MODIFY will be applied. (If you are thinking this could be useful for how pathfinder handles Skill Focus... yes, possibly... stay tuned :) )
Using a Selection as a Target
In some cases, we end up in situations like "This will increase the range of one type of weapon by 5'". Today there is
BONUS:WEAPONPROF=%LIST|RANGE|5
The %LIST here is actually in the target, which is a bit of a problem! The second argument to MODIFYOTHER is definitely NOT a formula, so we need to address that somehow. In the future, there may be elegant ways of doing this. For right now, we end up with a not-so-great method, which looks like:
MODIFYOTHER:EQUIPMENT.PART|ALL|Range|ADD|if(getFact(parent(),"WEAPONPROF")==getSelection("MySelection"),5,0)
Data Information
New Tokens
INSTANCE
INSTANCE is only supported on object types that are INSTANCE-able. This will require code support for all existing Formats.
Note: It is the intent to allow DYNAMIC to be instance-able if the data team so specifies. More on this later.
Format:
INSTANCE:x|y
- x is the instances to which the token given in y should be applied. Currently this supports either an integer value or "ALL"
- y is a full token to be applied to that instance.
Please note INSTANCE objects are NOT full CDOMObjects. They cannot support arbitrary tokens. Only a limited set (defined here) will be supported.
SELECTION
SELECTION will ONLY be available on INSTANCE objects. This means it needs to appear as a subtoken to INSTANCE (which itself must only appear on instance-able objects).
Format:
SELECTION:w|x|y|z|z
- w is the name of the selection. This is relevant when the data is using the value of the selection in the data
- x is the FORMAT of the selection. See the Setting up the new formula system for more information on formats.
- y is the array of available objects for the selection.
- z is an optional set of controls for the selection.
Selection Control: SELECT
This replaces the SELECT: token in the current CHOOSE System. It is a z value for SELECTION:
SELECT=x
- x is a formula for the number of selections to be made.
Selection Control: ORDER
In the scenario where two SELECTION items would interact (such as choosing a level of spell for a given class, where the class would need to be selected first), there is an ORDER control:
ORDER=x
- x is an integer constant. The SELECTION with the lowest ORDER will be processed first (just like PRIORITY on
MODIFY). If two SELECTION items have the same ORDER, the system will not guarantee which is processed first. If it matters, specify it. The default value is zero.
getAll(x) Function
getAll will be a function in the new formula system.
Format of this function:
getAll("x")
- x is the name of a FORMAT from the new formula system.
- Note: This format MUST be an limited format, in that you can't do getAll("NUMBER") as that is an unbounded set.
Note also this function will take some cleanup - we eventually need to figure out how to do the things that the existing items in the CHOOSE system do... this is a patch for now to get the discussion started
getSelection(x) Function
getSelection will be a function in the new formula system.
Format of this function:
getSelection("x")
- x is the name of the selection from the SELECTION token.
NOTE: The FORMAT that getSelection will return will depend on the SELECTION token. It also depends on whether there is a SELECT=x control on the SELECTION
MAXINSTANCES LST Token
This token will be usable on any format which is instance-able.
Format:
MAXINSTANCES:x
- x is a formula in the new formula system to identify the maximum number of instances allowed for that item.
Issues
- The current design of tokens shown for instances assumes one instance type per object type, which is actually not true
- The current design does not consider how to process ALLOWED/ENABLED for selections - is that a filter, or not - it should be clear and under control of the data team