Specialized Edges (Savage)

From HLKitWiki
Jump to: navigation, search

Context: HL KitAuthoring Examples … Savage Worlds Walk-Through 

Overview

Early in the development of our data files, we determined that we would need to do some special handling for a few of the edges. For example, the "Professional" edge requires the user to select a trait that with a rating of d12. Similarly, the "Scholar" edge requires the user to select two skills with a rating of d8 or higher. We need to allow the user to select the appropriate choices via menus, plus we need to properly validate that everything has been done correctly.

Built-In Support

Situations where the player must select a trait or two to associate with an ability are reasonably common across game systems. There are also many situations where a pick's state needs to be toggled or choices need to be selected from a dynamic list. Consequently, the Skeleton files provide a substantial amount of built-in support to handle these cases. This support takes the form of a component to handle the internal mechanics and a template for displaying picks with menu selections.

The "UserSelect" component provides support for all the mechanics. In general, this component can be used to handle the vast majority of the customizations that you'll need. The "UserSelect" component supports a variety of behaviors, including a toggle state that can be selected by the user (generally via a checkbox), a menu of choices that are driven dyanmically at run-time, and up to two different menus for selecting other things or picks within the data files. Between these four mechanisms, you should be able to use the "UserSelect" component almost all the time.

The Skeleton files also provide a "UserSelect" template within the file "visual.dat". This template is similar to the "SimpleItem" template, except that it is designed to orchestrate the display and handling of picks derived from the "UserSelect" component. Fields within the component are defined to tailor how the visuals behave, while the template interprets these fields to appropriately display information and allow the user to modify it.

You will almost certainly find situations where the "UserSelect" component will come in handy. When integrating the visual behaviors, you can either use the corresponding template or adapt aspects of it for use within your own templates. Either approach is perfectly reasonable and up to you as the author.

Integrating User-Selection

As mentioned above, the "UserSelect" component encapsulates most of the behaviors you'll need. We'll now review how those various behaviors work and how to tailor them appropriately. There are a total of three different behaviors.

The first behavior is a checkbox, which we've already used on a few occasions (e.g. equipping weapons and armor). The component provides two fields for managing the checkbox state. The "usrIsCheck" field tracks whether the checkbox is actually checked, and the "usrChkText" field dictates the text to be displayed with the checkbox. If this field is empty, then pick is assumed to not utilize the checkbox and it is not shown.

The second behavior is a thing-based menu, which populates the menu with a list of things or picks that can be chosen. The specific list of items shown is dictated by a tag expression that must be placed into the "usrCandid1" field. If this field is empty, it indicates that no thing-based menu is to be shown. You can control whether list to choose from consists of things, picks that have been added to the hero, or picks that have been added to the current container. This is accomplished by specifying one of the tags from the "ChooseSrc1" tag group, with no tag resulting the "hero" behavior.

Once the user selects an item, it is stored in the field "usrChosen1". If the user does not select an item, a validation error is automatically triggered. If you want to display a label next to the menu, you can specify the text to be shown in the "usrLabel1" field.

It is also possible to specify two separate thing-based menus at the same time. Two sets of the various fields are provided. In most cases, only one menu will be needed, but there will be times that two are required. When that occurs, you can use the second set of fields. They work exactly as the first set, although the first set of fields must be specified before you use the second set. This means that the field "usrCandid1" must be defined if you want to also use "usrCandid2".

The third behavior is an array-based menu, which pulls the menu choices from an array of strings. The array can contain any strings that are appropriate to the game system, and you can even populate the array via scripts at run-time, affording you with complete control over the options presented to the user. The array of options must be specified within the "usrArray" field, and the selected item is stored in the "usrSelect" field. If you want to display a label next to the menu, you can specify it within the "usrLabelAr" field. This behavior is only enabled when the 0th element of the "usrArray" field is non-empty.

If any of these behaviors are utilized, the selected choices are automatically integrated into the name of the pick. If you don't want the name to be changed, or if you want to synthesize your own name, you can assign the "User.NoAutoName" tag to the thing. This disables the automatic name synthesis for all picks derived from that thing.

NOTE! It is invalid to utilize more than one of the three behaviors on the same pick. It is perfectly reasonable to have one pick use a checkbox, another use a thing-based menu, a third use two thing-based menus, and a fourth use an array-based menu. However, you cannot have the same pick use a combination of behaviors, so a pick using a checkbox and a thing-based menu is not supported. If you attempt this, the results are undefined, so we recommend you not bother trying it.

Revising Our Code

We need to utilize the thing-based menus for our specialized edges. To do that, our first task is to add the "UserSelect" component to the "Edge" component set. By doing that, we'll automatically add all the mechanisms for managing user-selection behaviors into every edge we define. The revised component set looks like the following.

<compset
  id="Edge">
  <compref component="Edge"/>
  <compref component="MinRank"/>
  <compref component="Ability"/>
  <compref component="UserSelect"/>
  <compref component="SpecialTab"/>
  <compref component="CanAdvance"/>
  </compset>

The next thing we need to do is add support for properly displaying our edges. We could easily utilize the built-in "UserSelect" template by swapping it into the "edEdges" table portal as the "showtemplate" in place of the "edEdge" template. However, the default display behaviors aren't exactly what we need. The defaults would work fine for the "Professional" edge, since we simply want to show a menu to select an attribute or skill. The same would also work for the similar "Expert" and "Master" edges.

The problem arises with the "Scholar" edge. The various "Knowledge" skills will often have very long names once we append the domain. A limitation of thing-based menus is that we can only show the full name of the picks within the menu. This means that we need to show two separate menus, and each needs to contain a long name (e.g. "Knowledge: Area Knowledge, Battle"). The "UserSelect" template uses normal sized menus, which use a rather large font, so it's going to be very tight trying to show two long skill names in two separate menus. Instead of having the skill names cut off, we'll switch to using a smaller menu style. This will give us plenty of space to show everything, but it requires that we not use the "UserSelect" template and implement the menus ourselves.

We don't need to display any labels next to our menus, so all we need is two menu portals. We can copy the two thing-based menu portals from the "UserSelect" template into the "edEdge" template. Once we do that, we can change the style for the portals to "menuSmall" so that we get small menus. This gives us the following two new portals.

<portal
  id="menu1"
  style="menuSmall">
  <menu_things
    field="usrChosen1"
    component="none"
    maxvisible="10"
    usepicksfield="usrSource1"
    candidatefield="usrCandid1">
    </menu_things>
  </portal>

<portal
  id="menu2"
  style="menuSmall">
  <menu_things
    field="usrChosen2"
    component="none"
    maxvisible="10"
    usepicksfield="usrSource2"
    candidatefield="usrCandid2">
    </menu_things>
  </portal>

Since the names of our picks are going to be modified to incorporate the selected options, we face the same problem we had with domains for skills. Using the "name" field for showing the name of a pick will show the modified name. This means we'll be showing the modified name and showing the menus with the same contents. That's silly, so we need to only show the base name for each pick. We accomplish that by changing the "name" portal to reference the "thingname" field.

We can now look at the contents of the Position script for the "UserSelect" template. We're going to need to determine whether our menus are visible, so we can steal that block of code. We're also going to want to highlight the menus in red if nothing is selected within them, so we can steal that code as well. We'll need to center the menus vertically, but that's nothing special. The general logic for positioning everything in the "UserSelect" template is much too complicated for our needs - we only have two menu portals - so we'll figure out how to integrate them ourselves.

When we determine the width of the "name" portal, we need to reserve some space for the menus, just in case the name is extremely long. If the name is shorter than the reserved space, we'll happily use whatever space is available. Once the name is sized properly, we can easily insert one menu into the space or split the space between two menus. Putting all this together yields the revised Position script below.

~set up our height based on our tallest portal
height = portal[info].height

~if this is a "sizing" calculation, we're done
if (issizing <> 0) then
  done
  endif

~determine whether our menus are visible
~Note: Remember that a non-empty tagexpr field indicates menu selection is used.
if (field[usrCandid1].isempty <> 0) then
  portal[menu1].visible = 0
  endif
if (field[usrCandid2].isempty <> 0) then
  portal[menu2].visible = 0
  endif

~position our tallest portal at the top
portal[info].top = 0

~position the other portals vertically
perform portal[name].centervert
perform portal[delete].centervert
perform portal[menu1].centervert
perform portal[menu2].centervert

~position the delete portal on the far right
perform portal[delete].alignedge[right,0]

~position the info portal to the left of the delete button
perform portal[info].alignrel[rtol,delete,-8]

~position the name on the left and use availble space, with a gap for menus
portal[name].left = 0
portal[name].width = minimum(portal[name].width,portal[info].left - portal[name].left - 150)

~position the menus to the right of the name in the available space
perform portal[menu1].alignrel[ltor,name,10]
portal[menu1].width = (portal[info].left - portal[menu1].left - 20) / 2
portal[menu2].width = portal[menu1].width
perform portal[menu2].alignrel[ltor,menu1,10]

~if a menu is visible, make sure it has a selection
if (portal[menu1].visible <> 0) then
  if (field[usrChosen1].ischosen = 0) then
    perform portal[menu1].setstyle[menuErrSm]
    endif
  endif
if (portal[menu2].visible <> 0) then
  if (field[usrChosen2].ischosen = 0) then
    perform portal[menu2].setstyle[menuErrSm]
    endif
  endif

The "Scholar" Edge

It's time for us to actually define one of our special edges. We'll start with the "Scholar" edge.

Hooking Up User-Selection

The first thing we need to do is hook up all the internal behaviors we need for user selection. We're going to be needing two separate thing-based menus for the two "Knowledge" skills that need to be selected. Since we only want the user to select skills that have been added to the character, we'll choose picks on the hero. This can be specified by assigning the "ChooseSrc1.Hero" and "ChooseSrc2.Hero" tags to the thing.

<tag group="ChooseSrc1" tag="Hero"/>
<tag group="ChooseSrc2" tag="Hero"/>

Each of the menus will be identical in the set of picks they show. We'll assign the same tag expression to both the "usrCandid1" and "usrCandid2" fields. We need to determine how to identify "Knowledge" skills that have a rating of d8 or higher via a tag expression. The "Knowledge" skill has a unique id of "skKnow", so every "Knowledge" skill added to the character will possess a "thingid.skKnow" tag. The die type for traits can be identified via the "trtFinal" field, which indicates half the die type value. So we need to select skills with a "trtFinal" value that is greater than or equal to 4. This yields the tag expression below.

thingid.skKnow & fieldval:trtFinal >= 4

The tag expression must be assigned to a field value. This means we need to specify the above tag expression within the "value" attribute of a "fieldval" element. However, our tag expression uses two characters that are special within XML. If this were within a PCDATA region, we could utilize a CDATA block to include the special characters freely, but this is within an attribute. Our only option is to use the proper special character sequences. This results in the two "fieldval" elements looking like the ones below.

<fieldval field="usrCandid1" value="thingid.skKnow & fieldval:trtFinal >= 4"/>
<fieldval field="usrCandid2" value="thingid.skKnow & fieldval:trtFinal >= 4"/>

Revising the Pre-Requisites

Due to the tag expression used above, the user cannot select any skills lack the necessary rating. This means the role of our pre-requisite changes a bit. First of all, we still need to recognize that the thing is invalid when the user brings up the list of edges to choose from. If there are insufficient "Knowledge" skills with a d8 rating on the character, we want to show the skill as invalid. We only do this test when we're checking the pre-requisite on a thing.

The other thing we need to handle is the case where the user changes a skill rating on us. Our menus will only allow the user to choose a skill if it satisfies the requirements. However, the user could select a skill that is valid and then change the rating on that skill after it's selected. At that point, the item is already selected and the menu won't recognize a problem. Our pre-requisite can re-verify that the two selected skills retain the minimum die type rating whenever we have a pick.

Putting these two separate tests together, we get a single pre-requisite that handles things and picks differently. But the purpose of the test remains the same, so we use the same pre-requisite to perform the test. The revised Validate script on the pre-requisite should look like the following.

var total as number
~if this is a thing, make sure at least two skills exist with a d8 rating
if (@ispick = 0) then
  ~go through all Knowledge skills and tally those with a d8+ rating
  foreach pick in hero where "thingid.skKnow"
    if (eachpick.field[trtFinal].value >= 4) then
      total += 1
      endif
    nexteach

~this is a pick, so make sure our chosen skills have a d8+ rating
else
  if (altpick.field[usrChosen1].ischosen <> 0) then
    if (altpick.field[usrChosen1].chosen.field[trtFinal].value >= 4) then
      total += 1
      endif
    endif
  if (altpick.field[usrChosen2].ischosen <> 0) then
    if (altpick.field[usrChosen2].chosen.field[trtFinal].value >= 4) then
      total += 1
      endif
    endif
  endif

~if we have at least two valid skills, we're valid
if (total >= 2) then
  @valid = 1
  done
  endif

~if we got here, we're invalid
altthing.linkvalid = 0

Assigning Bonuses to Selected Skills

Our skills are now being properly selected and we can access those skills via scripts. This means that we can now automatically assign the "+2" bonus to each of those skills. We can use the "ischosen" target reference to determine if a menu has something selected. Each menu with a chosen item contains a skill that can then be accessed via the "chosen" transition. We can write a simple Eval script that applies the bonus to the selected skills, and it should look like the following.

<eval index="1" phase="PreTraits" priority="5000">
  <before name="Calc trtFinal"/><![CDATA[
  ~apply the +2 modifier to each selected skill
  if (field[usrChosen1].ischosen <> 0) then
    perform field[usrChosen1].chosen.field[trtRoll].modify[+,2,"Scholar"]
    endif
  if (field[usrChosen2].ischosen <> 0) then
    perform field[usrChosen2].chosen.field[trtRoll].modify[+,2,"Scholar"]
    endif
  ]]></eval>

Custom Name

The menus are appearing properly and listing the correct skills. However, once we select skills for the edge, the name being synthesized is very unwieldy. By default, the "UserSelect" component is automatically integrating the menu selections into the name. This includes the "Knowledge" skill name, which takes up lots of room and is implied by the nature of the edge.

What we really need is a more appropriate name for our edge. Ideally, the name should just list the domains assigned to each of the "Knowledge" skills. We can accomplish this if we generate a custom name. To do that, we first have to tell the "UserSelect" component to not generate a name for us. This requires that we assign the "User.NoAutoName" tag to the thing.

<tag group="User" tag="NoAutoName"/>

We can now generate our own custom name through the use of an Eval script. If nothing is chosen from either menu, we'll include an indication that something needs to be chosen. Otherwise, we'll append only the domain names for each of the skills selected for the edge. The resulting Eval script should look like the one below.

<eval index="2" phase="Render" priority="5000"><![CDATA[
  ~determine the text to append to the name
  var choices as string
  if (field[usrChosen1].ischosen <> 0) then
    choices = field[usrChosen1].chosen.field[domDomain].text
  else
    choices = "-Choose-"
    endif
  if (field[usrChosen2].ischosen <> 0) then
    choices &= ", " & field[usrChosen2].chosen.field[domDomain].text
    endif

  ~add the selection to both the livename and shortname (if present) fields
  field[livename].text = field[thingname].text & ": " & choices
  if (tagis[component.shortname] <> 0) then
    field[shortname].text &= " (" & choices & ")"
    endif
  ]]></eval>

Safeguarding Against Duplicate Skills

At this point, our revised edge is working quite nicely. The menus are appearing properly and only allowing the user to select valid skills. If the skills are modified, the pre-requisite flags a suitable error. And a reasonably compact name is being synthesized for display. The only detail we haven't dealt with yet is that the user can select the same skill in each of the menus, which must be treated as an error.

We can define an Eval Rule script to detect this condition. This particular rule can only be invalid if we actually have selections in both menus. If we do, then we need to determine if the exact same pick is selected in each menu. We can't use the unique id, since every "Knowledge" skill is a separate instance of the exact same thing and will therefore have the same unique id. Fortunately, every pick within the entire portfolio (i.e. across all heroes) has a uniquely assigned index. This means we can simply check to see if the unique index values match between the two menu selections. If they do, then we have a problem. Our Eval Rule script should look like the one shown below.

<evalrule index="1" phase="Validate" priority="5000" message="Duplicate skill selected"><![CDATA[
  if (field[usrChosen1].ischosen + field[usrChosen2].ischosen < 2) then
    @valid = 1
  elseif (field[usrChosen1].chosen.uniqindex <> field[usrChosen2].chosen.uniqindex) then
    @valid = 1
    endif
  ]]></evalrule>

The "Professional" Edge

We can now shift our focus to the "Professional" edge. For this edge, we need to show a menu from which the user can select a trait. We'll use the same basic approach as we used for the "Scholar" edge.

Fields and Tags

The first thing we need to do is determine what we want to show in the menu. In this case, the user can select an attribute or skill, but we only want skills that the user has added to the character. This means we'll specify the "ChooseSrc1.Hero" tag. We only need a single menu, so we can ignore adding a second tag.

Our menu needs to show traits with a minimum die-type rating of d12. Only attributes and skills can possess a die-type rating, so that means we need to include those two types of traits in the menu. We can test the field value to verify the die-type using the same technique we used for the previous edge. This results in the following field value and tag being assigned to the thing.

<fieldval 
  field="usrCandid1" 
  value="(component.Attribute | component.Skill) & fieldval:trtFinal >= 6"/>
<tag group="ChooseSrc1" tag="Hero"/>

Confer the Bonus

The next step is to confer the "+1" bonus to the selected trait. We'll use an Eval script to accomplish this, just like we did for the "Scholar" edge. The new script should look like below.

<eval index="1" phase="PreTraits" priority="5000">
  <before name="Calc trtFinal"/><![CDATA[
  if (field[usrChosen1].ischosen <> 0) then
    perform field[usrChosen1].chosen.field[trtRoll].modify[+,1,"Professional"]
    endif
  ]]></eval>

The Pre-Requisite

The pre-requisite for the "Professional" edge will also work a lot like the one for "Scholar". When testing a thing, we need to make sure that at least one trait exists with a d12 rating. When testing a pick, we need to make sure that any selected trait still has a d12 rating. The resulting Validate script for the pre-requisite should look like the following.

<prereq iserror="yes" message="Any Trait of d12 required.">
  <validate><![CDATA[
    ~if this is a thing, make sure a trait exists with a d12 rating
    if (@ispick = 0) then
      foreach pick in hero where "(component.Attribute | component.Skill)"
        if (eachpick.field[trtFinal].value >= 6) then
          @valid = 1
          done
          endif
        nexteach

    ~if a skill is chosen, make sure it has a rating of d12
    elseif (altpick.field[usrChosen1].ischosen <> 0) then
      if (altpick.field[usrChosen1].chosen.field[trtFinal].value >= 6) then
        @valid = 1
        done
        endif
      endif

    ~if we got here, we're invalid
    altthing.linkvalid = 0
    ]]></validate>
  </prereq>

Detect Duplicates

The final task is to detect duplicates. The "Professional" edge can be selected multiple times, but a different trait must be selected every time. In order to verify this, we need to compare separate edges. One solution would be to use a "foreach" loop to scan all of the "Professional" edges and build up a list of all the selected traits, then identify any duplicates. That's a fair amount of work, and it will be even more work when we need to do the same thing for the "Expert" and "Master" edges in moment. Fortunately, there's a much easier solution we can use.

What we need to know is whether the same trait is selected multiple times by the same edge. We can detect this by assigning a tag to each trait at the same time that we confer the "+1" bonus. At the end of evaluation, we can check each trait to see if any possess two of that tag. Since we're going to be needing this same mechanism for the "Expert" and "Master" edges, we'll implement it in a way that can be easily extended for those edges.

We start by defining a new tag group, which we'll give the unique id "Duplicate". Within this group, we'll define tags for each of the edges that will require checking for duplicates. The resulting group should look like the following.

<group
  id="Duplicate">
  <value id="Profession"/>
  <value id="Expert"/>
  <value id="Master"/>
  </group>

We the assign the "Duplicate.Profession" tag to each trait that is selected for the "Professional" edge. As mentioned above, we can do this easily during the Eval script that confers the "+1" bonus by adding the line of code below.

perform field[usrChosen1].chosen.assign[Duplicate.Profession]

At this point, every trait possesses a "Duplicate" tag for each "Professional" edge that selects the trait. We can tally up the total number of "Duplicate" tags assigned to the trait. We can also tally up the number of unique "Duplicate" tags assigned to the trait, which eliminates all duplicates of the same tag. If two different "Professional" edges select the same trait, then that trait will possess two "Duplicate" tags, but it will only possess one unique "Duplicate" tag. By comparing the two quantities, we can tell if there is a problem, as shown in the new Eval Rule script below.

<evalrule index="5" phase="Validate" priority="5000" 
      message="Selected multiple times for same edge"><![CDATA[
  ~if all of our tags are unique, then there are no duplicates to report
  if (tagcount[Duplicate.?] = tagunique[Duplicate.?]) then
    @valid = 1
    done
    endif
  ]]></evalrule>

The above technique easily extends to the "Expert" and "Master" edges. If the trait is selected for both "Professional" and "Expert", it will have two "Duplicate" tags. However, both of those tags will be unique, so the Eval Rule will report all is well. If the trait is selected by another "Expert" edge, it will gain another tag that will not be unique and the validation error will be reported.

Avoiding Duplicates

The logic above allows us to detect duplicate selections and report them as errors. It also affords us the chance to avoid duplicates during the selection of edges. Every traits that is selected by the edge is now assigned a "Duplicate" tag. We can test that condition and only show the edge as valid for selection if there are any candidate traits that don't already have the "Duplicate" tag. We can accomplish this by amending the tag expression used within the "foreach" statement in the Validate script to look like the following.

foreach pick in hero where "(component.Attribute | component.Skill) & !Duplicate.Profession"

In other words, if there is one trait at d12, the "Professional" edge will appear as valid in the list of edges. Once the edge is added and that one trait selected, the edge will then appear as invalid within the list of edges. If another trait reaches the d12 rating, the edge will appear as valid again. This helps to avoid situations with duplicate selections, since the user will be warned before actually adding the edge.

The "Expert" and "Master" Edges

Adding the "Expert" and "Master" edges at this point is quite easy. We'll use the exact same logic from the "Professional" edge, except for a few important tweaks.

The first change is to the tag expression within "usrCandad1" field. Since the "Expert" edge requires the "Professional" edge already be selected for the trait, we need to verify that is the case. This is done by adding a suitable term to the tag expression that checks for the "Duplicate.Profession" tag. However, we can also assume that having this tag implies that the trait is also at a d12 rating, so we can drop that requirement because it is implied. In fact, if a pick has the "Duplicate.Profession" tag, it must also be a valid trait, so we don't have to check that condition either. This leaves us with the simple requirement that the pick possess the one tag. For the "Master" edge, the same logic applies, so we only need to check for the lone "Duplicate" tag on each. The two field values end up as shown below.

<fieldval field="usrCandid1" value="Duplicate.Profession"/>

<fieldval field="usrCandid1" value="Duplicate.Expert"/>

The second change is within the Eval script that confers the bonus. The explanation for the "modify" target reference should be changed to identify the appropriate edge by name. In addition, the appropriate "Duplicate" tag should be assigned for the edge. The revised Eval script for the "Expert" edge is shown below.

<eval index="1" phase="PreTraits" priority="5000">
  <before name="Calc trtFinal"/><![CDATA[
  if (field[usrChosen1].ischosen <> 0) then
    perform field[usrChosen1].chosen.field[trtRoll].modify[+,1,"Expert"]
    perform field[usrChosen1].chosen.assign[Duplicate.Expert]
    endif
  ]]></eval>

There remain two final changes within the pre-requisite test. The first is within the logic used for things, wherein we can make the same assumptions about dependencies that we made above. If a trait has the "Duplicate.Profession" tag, we assume it's a valid trait, so the "foreach" statement changes to incorporate the alternate requirements, as shown below for the "Expert" edge.

foreach pick in hero where "Duplicate.Profession & !Duplicate.Expert"

When validating a pick, we need to verify both the die-type rating and the presence of the lesser edge that is depended upon. In the case of the "Expert" edge, we need to add an additional qualification, as shown in the revised code block below.

if (altpick.field[usrChosen1].chosen.field[trtFinal].value >= 6) then
  if (altpick.field[usrChosen1].chosen.tagis[Duplicate.Profession] <> 0) then
    @valid = 1
    done
    endif
  endif

As long as all the equivalent changes are applied to the "Master" edge, both edges should behave exactly as they should, with all the proper dependencies and conferring the proper bonuses.