Specialized Edges (Savage)

From HLKitWiki
Revision as of 03:44, 2 February 2009 by Rob (Talk | contribs) (Safeguarding Against Duplicate Skills)

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

The "Expert" and "Master" Edges