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.