Advancement Support (Savage)

From HLKitWiki
Jump to navigationJump to search

Context: HL Kit … Authoring Examples … Savage Worlds Walk-Through 

Overview

The Savage Worlds game system uses a character advancement system that requires each separate advancement be performed in a strict sequence. This is because different types of advancements are restricted based on the relative state of different traits (e.g. skill values relative to their linked attributes), and that state changes with each advancement. Sequential advancement logic can be somewhat complex to support, so the Skeleton files provide a built-in mechanism to orchestrate serialized advancement that can be readily adapted for use with Savage Worlds. This section outlines how that adaptation is achieved.

Understand the Basics

Before continuing with the rest of this section, it's critical that you familiarize yourself with the basics of how the various serialized advancement logic works within the Skeleton files. If you have not already done so, please review how advancement is handled in general.

You'll find details in the file "?????", which can be found here.

Accessing Linked Attributes

A fundamental facet of the advancement logic is that each advance is actually a gizmo that contains the details of the advance within it as child picks. Selections that reside within a gizmo are not directly attached to the hero. However, Hero Lab provides a mechanism called displacement that allows specific selections within a gizmo to appear and behave as if they are directly attached to the hero. The advancement logic makes use of displacement so that new skills and edges added to a character via an advance behave as if the user added them directly to the hero. But this only applies to new skills and edges. If the advance increases an existing skill or attribute, the selected trait is not displaced to the hero, as it already exists on the hero. This distinction is critical when working with the advancement logic, as it has important implications for linkages.

Linkages look for the linked pick within the same parent context as the reference pick. For example, when accessing the linked attribute for a skill on the hero, that attribute will be sought within the hero. However, if the skill is within the gizmo for an advance, accessing the attribute linkage will look for the attribute within the gizmo. This will obviously fail, since there is no attribute within the gizmo. This results in a run-time error being reported.

The use of displacement makes this whole mechanism a bit more murky. If a skill within a gizmo is displaced to the hero, then that skill behaves as if it exists directly on the hero. As such, attempts to access the attribute linkage for a displaced skill will succeed, as it works exactly like a skill that is directly added to the hero. However, skills within a gizmo that are not displaced will fail to access the linkage and report the run-time error.

The way that advancements work, new skills (and edges) get displaced onto the hero. However, increases to existing skills (and attributes) are not displaced, so the skill selection lives solely within the context of the gizmo. This means that any script performed on a skill will be processed in two different contexts. Skills added directly to the hero (or displaced onto the hero) will be able to access their linked attribute, while skills within the gizmos will not. Any script that accesses the attribute linkage must verify whether the linkage is available before accessing it. This can be determined by simply verifying whether the effective container for the pick is the hero, which is achieved by the script logic shown below.

if (container.ishero <> 0) then
  ~the container is the hero, so accessing linkages will succeed
  endif

So this begs the question of what happens when the linkage isn't accessible. The answer is "nothing", and that's perfectly acceptable in this situation. The reason for this is that attribute linkages are only needed for the skills that get added to the hero (both directly and via displacement). Skills that are selected as increases don't need to actually perform normal handling for skills. Instead, all they need to do is apply the appropriate increase to the existing skill within the hero, and that does not require access to the attribute linkage.

This conditional handling of access to the attribute linkage is needed in the Eval script that retrieves the identity tag of the linked attribute into the skill. The revised version of the script should look similar to the one below.

<eval value="3" phase="Setup" priority="5000"><![CDATA[
  ~only access the linkage if the skill is directly on the hero; if not, we assume
  ~it is within an advancement gizmo and no linkage will exist there; we also don't
  ~need the linked attribute tag on advancement skills, so it's a non-issue
  if (container.ishero <> 0) then
    perform linkage[attribute].pullidentity[Attribute]
    endif
  ]]></eval>

NOTE! The attribute linkage of skills should not be confused with the "basis" linkage used within advances. Whenever an existing pick is selected via a chooser, that pick is automatically tracked by Hero Lab as a "basis" pick, and accessing that pick is achieved via the "linkage[basis]" script transition. This mechanism ensures that any advance applied to an existing pick (e.g. an attribute or skill increase) can always identify that "basis" pick in order to apply adjustments to it. The "basis" linkage is a special type of linkage that is very different from normal linkages, but it is accessed via the "linkage[]" transition for syntactic and semantic consistency within scripts.

Increasing Skills Via Advances

After assessing the different advancement options, the only ones that introduce complexity are those pertaining to skills, and there are two issues to contend with. The first wrinkle is the distinction between skills that are less than their linked attribute and those that are not, as a different advance applies to each. The easiest way to solve this is by having each skill identify its own nature during evaluation, with an appropriate tag being assigned. Since it's an either-or situation, only a single tag is needed, with its presence indicating one state and its absence indicating the other.

We define a "Helper.LessThan" tag and add an Eval script to the "Skill" component to make the appropriate determination. The script can be processed very late in the overall logic and should look similar to the one below. Since the attribute linkage must be accessed, we need to limit our processing to skills that are directly on the hero (or displaced onto the hero).

<eval value="4" phase="Final" priority="10000"><![CDATA[
  ~only access the linkage if the skill is directly on the hero; if not, it is
  ~likely within an advancement gizmo and no linkage will exist there; we also
  ~don't need the linked attribute tag on advancement skills, so it's a non-issue
  if (container.ishero <> 0) then
    if (field[trtFinal].value < linkage[attribute].field[trtFinal].value) then
      perform assign[Helper.LessThan]
      endif
    endif
  ]]></eval>

The second area of complexity is the advance that allows the character to increase two separate skills as part of the one advance. Doing this is possible, but it would require a fair amount of work and would go beyond the framework provided by the built-in advancement mechanism. So a simpler alternative would be preferable. Fortunately, this is easy to solve if we can increase the two skills separately via distinct advances. We can accomplish this by introducing a new advance that only increases a single skill and assigning it an advancement "cost" of only half an advance. This way, the user can select two of these advances for the standard cost of a normal advance. Since there is only one situation like this, we can safely introduce this new mechanism and have it be relatively intuitive for the user to work with. The specifics of this approach will be implemented in the separate section below where we define the skill-related advances.

Identify Existing Skills

There are some advances that need to uniquely identify individual skills for appropriate handling. To support this, every skill must possess a suitable identity tag, and those tags must be forwarded up to the hero for reference. This entails defining the identity tag and forwarding it exactly the same way that we've already done for abilities. The XML element below will define the identity tag behavior, and the script below will handle the forwarding.

<identity group="Skill"/> 

<eval value="1" phase="Setup" priority="5000"><![CDATA[
  perform forward[Skill.?]
  ]]></eval> 

Don't Apply Creation Costs

Attributes, skills, and edges can now be added/increased via advances. These advances must not count towards the number of attribute and skill points that are spent on a given character, nor any starting edges that a character may be entitled to. This requires appropriate handling for each different type of trait.

For attributes, we only want to accrue attribute points for character creation when the user increases the attribute during creation. All other attribute increases (e.g. via advances) must be ignored. This is achieved by ensuring that our attribute points only utilize the user-specified value for the attribute, as all advances increase the "bonus" field. Fortunately, we're already doing this, so no adjustments are required.

For skills, we need to address two separate situations. First, skill advances must be handled the same way as attributes, limiting the cost to the value assigned to the user-specified field for the skill. Just like with attributes, we're already doing this, so no adjustments are required. The second situation is for new skills that are added via advances - not just increased. For such skills, the entire skill needs to be ignored when tallying the skill point cost. This requires that we modify the existing Eval script for tallying the points so that skills that are added via advances are ignored, such as shown in the revised script below.

<eval value="2" phase="Setup" priority="5000"><![CDATA[
  ~if this skill is not added directly to the hero (i.e. an advance), skip it entirely
  if (origin.ishero = 0) then
    done
    endif
  ~the base value for skills is two, so we need to adjust by one to get the proper cost
  hero.child[resSkill].field[resSpent].value += field[trtUser].value - 1
  ]]></eval>

Edges are never increased via advances, but they are added via advances just like skills. As such, they need to be handled the same way that skills are handled. The edge needs to be ignored when tallying edge slots when it is added via an advance. This entails that same change as above, yielding the following revised script.

<eval value="2" phase="Setup" priority="5000"><![CDATA[
  ~if this edge is not added directly to the hero (i.e. an advance), skip it entirely
  if (origin.ishero = 0) then
    done
    endif
  ~consume another edge slot
  #resspent[resEdge] += 1
  ]]></eval> 

Configuring Serialized Advancement

The logic for serialized advancement can be enabled and configured for a game system via the file "definition.def". Near the top of this file, the "advancement" element encapsulates all the generalized details for serialized advancement. Enabling advancement is controlled via the "enable" attribute. You can also customize the terminology Hero Lab uses in conjunction with advancement via the "createterm" and "advanceterm" attributes, although the default terms are usually acceptable.

Within the "advancement" element, there is a child "canadvance" element that contains the CanAdvance script. This script allows you to perform verification before allowing the user to transition from the initial "creation" mode into "advancement" mode. For example, if you want to ensure that the user has allocated all the appropriate starting resources before switching to "advancement" mode, you can perform the proper checks in this script. If the script returns an empty "message" special symbol, Hero Lab assumes all is well and allows the transition to occur. If message text of some sort is generated by the script, failure is assumed and the message is displayed to the user as an explanation of what the problem is. For Savage Worlds, we need to verify that the user has properly allocated all attribute and skill points, as well as any starting edges and any rewards for starting hindrances.

Also within the "advancement" element is a child "transition" element that contains the Transition script. This script determines the message to be displayed to the user when the character transitions into and out of "advancement" mode. Normally, a simple reminder of the transition's implications and how to transition back is all that is needed, but you can customize the script to whatever extent you feel is appropriate. The default script presents a suitable notification that should work in most cases. For Savage Worlds, the default script works great.

Putting it all together, the "advancement" element for Savage Worlds should look similar to the example shown below. The mechanism is enabled, the terms are left in their default state, the Transition script is unchanged, and the CanAdvance script is tailored for the needs of a Savage Worlds character.

<advancement
  enable="yes"
  <canadvance><![CDATA[
    var bullet as string
    bullet = "{bmp bullet_red}{horz 4}"
    @message = ""
    ~perform tests to assure all starting resources have been assigned
    if (#resleft[resCP] <> 0) then
      @message = @message & bullet & "Character points must be assigned for the character.\n"
      endif
    if (#resleft[resAbility] <> 0) then
      @message = @message & bullet & "Ability slots must be assigned for the character.\n"
      endif
    ]]></canadvance>
  <transition><![CDATA[
    if (#iscreate[@newmode] = 1) then
      @message = "{b}{text ffff00}Creation Phase{text 010101}{/b}"
      @message = @message & "{br}{br}"
      @message = @message & "{align left}You have unlocked your character, thereby exiting the Character Advancement phase and moving back to the Character Creation phase. "
      @message = @message & "{br}{br}"
      @message = @message & "While unlocked, traits defined during character creation can be adjusted, as long as those traits have not yet been altered on the Advances tab. "
      @message = @message & "Traits that already have advancements applied to them will remain locked unless those advancements are deleted. "
      @message = @message & "{br}{br}"
      @message = @message & "Lock your character and remove any advancements on a trait if you wish to revise the rating that trait was given during character creation. "
    else
      @message = "{b}{text ffff00}Advancement Phase{text 010101}{/b}"
      @message = @message & "{br}{br}"
      @message = @message & "{align left}You have locked your character creation traits. "
      @message = @message & "{br}{br}"
      @message = @message & "By locking your character creation traits, you have begun the Character Advancement phase of play. "
      @message = @message & "While locked, you cannot alter traits defined during character creation. "
      @message = @message & "Use the Advances tab while the character is locked to allocate advances to new abilities or to increase existing traits. "
      @message = @message & "{br}{br}"
      @message = @message & "Unlock the character to go back to the Character Creation phase. "
      endif
    ]]></transition>
  </advancement> 

Dynamic Tag Expressions

Each advance entails the selection of something slightly different by the user, yet the advancement mechanism uses a single chooser through which the user makes the selections. Consequently, each advancement must properly dictate what can be selected, and this is achieved by using a dynamic tag expression within the chooser. Each advance possesses a field that contains the tag expression to be used, and it can be either hard-wired or synthesized on the fly via scripts, as necessary. This field must contain a valid tag expression that is applied against all things/picks to appropriately filter the list to the set of valid choices for the user.

Handling Attribute Increases

Increasing an attribute is simple, as it can be readily adapted from the example provided within the Skeleton data files. First, we need to specify suitable action text. Next, we need to assign the "Advance.Increase" tag so that it is handled as an increase of an existing trait. The last step is to specify the appropriate tag expression for identifying valid attributes that can be increased. These consist of any attribute that is not hidden and not already at its maximum. The final result looks like the thing definition below.

<thing
  id="advAttrib"
  name="Increase Attribute"
  compset="Advance"
  description="Increase an attribute by one die type.">
  <fieldval field="advAction" value="Boost Attribute"/>
  <fieldval field="advDynamic" value="component.Attribute & !Helper.Maximum & !Hide.Attribute"/>
  <fieldval field="advCost" value="1"/>
  <tag group="Advance" tag="Increase"/>
  <child entity="Advance">
    </child>
  </thing> 

Handling New Edges

Adding new edges via an advance is a little more complicated. The action text is easy and we assign the "Advance.AddNew" tag so that it is handled as the addition of something new. In addition, we need to require that the user choose something to be added, so we must assign the "Advance.MustChoose" tag to the child gizmo. The final piece is the tag expression, which needs to be synthesized on-the-fly based on the list of edges that have already been added to the character. Specifically, the list of edges that is valid for selection consists of all edges that have not yet been added. Since the identity tags of all edges are forwarded to the hero, we can request a list of those ids from the hero and massage the list so that it can be used as part of our tag expression. Once we append the list of precluded edges to the initial tag expression, we have a result that will allow the user to choose any edge that has not already been added. This yields an advance similar to the one below.

<thing
  id="advEdge"
  name="Gain a New Edge"
  compset="Advance"
  description="Select a new edge of your choice.">
  <fieldval field="advAction" value="New Edge"/>
  <fieldval field="advDynamic" value="component.Edge"/>
  <fieldval field="advCost" value="1"/>
  <tag group="Advance" tag="AddNew"/>
  <eval value="1" phase="Render" priority="1000">
    <before name="Assign Dynamic Tagexpr"/><![CDATA[
    ~get the list of all edges on the hero and assemble it as a list of precluded tags
    var tagexpr as string
    tagexpr = hero.tagids[Edge.?," & !"]
    ~if there are any tags to exclude, append them to the tagexpr appropriately
    if (empty(tagexpr) = 0) then
      field[advDynamic].text &= " & !" & tagexpr
      endif
    ]]></eval>
  <child entity="Advance">
    <tag group="Advance" tag="MustChoose"/>
    </child>
  </thing> 

Adding New Skills

The advance for adding new skills is very similar to the one for adding new edges. The key difference is the script that synthesizes the tag expression that controls which skills can be added by the user. We can't use the same technique as for edges, because there are some skills that can be added multiple times (e.g. "Knowledge"). Instead, we have to iterate through all of the skills that have been added to the hero and determine whether each skill is unique or not. If a skill is unique, it gets added to the list of tags that must be precluded from user selection, while non-unique skills can be added any number of times. The net result should look similar to the advance shown below.

<thing
  id="advSkill"
  name="Gain a New Skill"
  compset="Advance"
  description="Select a new skill at a d4 rating.">
  <fieldval field="advCost" value="1"/>
  <fieldval field="advAction" value="New Skill"/>
  <fieldval field="advDynamic" value="component.Skill & !Hide.Skill"/>
  <tag group="Advance" tag="AddNew"/> 
  <eval value="1" phase="Render" priority="1000">
    <before name="Assign Dynamic Tagexpr"/><![CDATA[
    ~get the list of all unique skills on the hero and assemble it as a list of precluded tags
    var tagexpr as string
    foreach pick in hero where "component.Skill & !Hide.Skill"
      if (each.isunique <> 0) then
        tagexpr &= " & !Skill." & each.idstring
        endif
      nexteach 
    ~if there are any tags to exclude, append them to the tagexpr appropriately
    if (empty(tagexpr) = 0) then
      field[advDynamic].text &= tagexpr
      endif
    ]]></eval> 
  <child entity="Advance">
    <tag group="Advance" tag="MustChoose"/>
    </child>
  </thing> 

Handling Skill Increases

As mentioned above, we need two separate mechanisms for adding skill increases. One advance is used for increases skills that are equal to or greater than the linked attribute, while the other is for skills that are less than the attribute. This allows us to apply a cost of one full advance for one type of increase and only a cost of a half-advance for the other type, which enables users to choose two of the latter advance for the price of any other advance. We've already instrumented each skill to possess a "Helper.LessThan" tag if it is less than the linked attribute value, so all we need to do is test against that tag. The only other details we have to make sure are distinct are the action text, description, and cost in advance slots. In the end, we have two separate advances that differ from each other in only those few ways, as shown below.

<thing
  id="advBoost1"
  name="Increase Greater Skill"
  compset="Advance"
  description="Increase one skill by one die type that the skill is equal to or higher than the linked attribute.">
  <fieldval field="advCost" value="1"/>
  <fieldval field="advAction" value="Boost Greater Skill"/>
  <fieldval field="advDynamic" value="component.Skill & !Helper.Maximum & !Hide.Skill & !Helper.LessThan"/>
  <tag group="Advance" tag="Increase"/> 
  <child entity="Advance">
    <tag group="Advance" tag="MustChoose"/>
    </child>
  </thing> 

<thing
  id="advBoost2"
  name="Increase Lesser Skill"
  compset="Advance"
  description="Increase one skill by one die type, provided that the skill is currently less than its linked attribute.\n\n{b}{text ffff00}Note!{text 010101}{/b} Select this option {b}{i}twice{/i}{/b} as a single advance, specifying a separate skill for each. This selection consumes only half an advance slot.">
  <fieldval field="advCost" value=".5"/>
  <fieldval field="advAction" value="Boost Lesser Skill"/>
  <fieldval field="advDynamic" value="component.Skill & !Helper.Maximum & !Hide.Skill & Helper.LessThan"/>
  <tag group="Advance" tag="Increase"/> 
  <child entity="Advance">
    <tag group="Advance" tag="MustChoose"/>
    </child>
  </thing> 

Attribute Limits

All of our advances are now in place and should be working. However, we still have a special requirement regarding advances that we need to properly enforce. Attributes can only be increased once every rank. Solving this entails a rather crude, but effective, approach. We can use a "foreach" loop to iterate through all of the advances and tally the total number of attribute advances within each rank. If any rank has more than one attribute advance within it, we need to report an error to the user so that it can be rectified. We must process the advances in the exact order in which they were added to the character, which means we need to utilize the built-in "_CompSeq_" sort set.

Unfortunately, there's an interesting wrinkle. Since there are four advances per rank through 80 XP and two advances per rank thereafter, we need to differentiate the two progressions appropriately. This means an attribute advance is allowed only once out of every block of four advances during the first 16 advances, but only once out of every two advances after that. During our loop, we need to handle this distinction properly.

To enforce this check, we'll create a new thing that is intended solely for validation. That means we need to add it to the file "thing_validate.dat". Since the thing is performing checks on the overall hero, it can derive from the "Simple" component set. And since the thing needs to perform its checks on all actors, we assign it the "Helper.Bootstrap" tag to ensure it gets automatically applied to all heroes.

Most validation things possess only an EvalRule script, since they merely need to perform the validation test, and we could implement this thing the same way. However, we can make things a little simpler and more efficient by performing the validation in two stages. Each EvalRule script assumes the test is failed, so all the checks must be performed to ascertain whether test is satisfied, but it's much easier for us to simply assume success and bail out if we ever determine that the requirements aren't satisfied. Consequently, the first stage is an Eval script that determines whether all the requirements are satisfied and assigns a tag to the hero if anything is failed. The second stage is the EvalRule that merely checks for the tag and reports the validation result accordingly. All we need to do is define a new "Hero.MultiAttr" tag and write the two scripts, which yields something that looks like the following.

<thing
  id="valAdvance"
  name="Advances"
  compset="Simple">
  <tag group="Helper" tag="Bootstrap"/> 
  <eval value="1" phase="Validate" priority="8000"><![CDATA[
    var index as number
    var total as number
    var is_check as number
    ~iterate through all advances in the order they were added to the character
    foreach pick in hero where "component.Advance" sortas _CompSeq_ 
      ~determine whether we need to perform an actual check at this interval
      is_check = 0
      if (index % 4 = 0) then
        is_check = 1
      elseif (index % 2 = 0) then
        if (index > 16) then
          is_check = 1
          endif
        endif 
      ~if we're supposed to check, do so, then reset the tally count
      ~Note: If we find an error, assign a tag and there's nothing more to do.
      if (is_check <> 0) then
        if (total > 1) then
          perform hero.assign[Hero.MultiAttr]
          done
          endif
        total = 0
        endif 
      ~if this is an attribute increase, tally it
      if (each.tagis[AdvanceId.advAttrib] <> 0) then
        total += 1
        endif 
      ~increment our index based on our cost and continue
      ~Note: Using the cost allows us to properly identify the transitions between
      ~ experience point levels where we need to perform our check.
      index += each.field[advCost].value
      nexteach 
    ~perform a check on the final set of advances
    if (total > 1) then
      perform hero.assign[Hero.MultiAttr]
      endif
    ]]></eval> 
  <evalrule value="1" 
    phase="Validate" priority="9000" 
    message="Multiple attributes increased within a rank" summary="Multiple attributes"><![CDATA[
    ~if we have no instances of multiple advances within a level, we're good
    if (hero.tagcount[Hero.MultiAttr] = 0) then
      @valid = 1
      done
      endif 
    ~mark associated tab as invalid
    container.panelvalid[advances] = 0
    ]]></evalrule> 
  </thing>