Final Cleanup Continued (Savage)

From HLKitWiki
Revision as of 05:16, 11 February 2009 by Rob (Talk | contribs) (Overview)

Jump to: navigation, search

Context: HL KitAuthoring Examples … Savage Worlds Walk-Through 

Overview

The testing and cleanup process continues....

Identify Hindrance Severity in Sheet Output

The list of hindrances that are output within the character sheet use the same mechanism as edges and racial abilities. However, hindrances can be taken in both major and minor forms, and that aspect of each hindrance is important to the player.

We can modify the Label script currently in use within the "oAbilPick" template to detect the presence of a hindrance and output it specially. We'll only flag major hindrances, and we'll do it by including a special symbol. This will keep the space requirements to an absolute minimum. We can pick a suitable character from the "Wingdings" font, resulting in the following revised Label script.

var major as string
if (tagis[component.Hindrance] <> 0) then
  if (field[hinMajor].value <> 0) then
    major = " {font Wingdings}" & chr(181) & "{revert}"
    endif
  endif
@text = field[shortname].text & major & "{/b}  {size 32}" & field[summary].text

Add Drawbacks to Character Sheet

It seems that we overlooked including the details of arcane drawbacks on the character sheet. Since it's something the player should not forget, we should probably include a reminder. One solution would be to show the drawback like a hindrance. Another would be to include the name of the drawback within the title above the table of arcane powers. And a third would be to add a new table that listed the drawback just like an edge or hindrance.

None of these options is ideal, and they each have their trade-offs. Treating drawbacks as hindrances would entail a fair amount of work to revise the data files. Since all we need is to display them, the extra work probably isn't worth it. Including the drawback within the title above the arcane powers table would not show its summary, plus it would result in a very cramped title area. Adding a new table is quick and easy, but the downside is that we'll consume additional vertical space on the sheet.

We'll go with the new table for the sake of simplicity. Our table portal can be adapted from the edges table, giving us the following.

<portal
  id="oDrawback"
  style="outNormal">
  <output_table
    component="Drawback"
    showtemplate="oDrawPick">
    <headertitle><![CDATA[
      @text = "Arcane Drawbacks"
      ]]></headertitle>
    </output_table>
  </portal>

The template can be readily adapted from the one used for special abilities. This results in the template shown below.

<template
  id="oDrawPick"
  name="Output Drawbacks Table"
  compset="Drawback"
  marginvert="2">

  <portal
    id="details"
    style="outMedLt">
    <output_label>
      <labeltext><![CDATA[
        @text = field[name].text & "{/b}  {size 32}" & field[summary].text
        ]]></labeltext>
      </output_label>
    </portal>

  <position><![CDATA[
    ~our details width spans the entire template width
    portal[details].width = width

    ~our height is the height of our portal
    height = portal[details].bottom
    ]]></position>
  </template>

The last step is to integrate the portal into a layout. We'll add it to the "oRightSide" layout and place immediately above the table of arcane powers. If we include it above the powers, it will always be prominently visible and immediately adjacent to the list of powers. All we need to do is add the "portalref" element and then auto-place the portal immediately beneath the continuation of the edges output.

Ignoring Wound Penalties

There are two edges that allow the character to ignore wound penalties (Nerves of Steel and the improved version). Since HL automatically applies the effects of all wounds penalties to rolls, users will assume the effects of these edges are properly factored in. We'd better make good on that assumption.

The wound and fatigue penalties are all handled via the "acNetPenal" field on the "Actor" component. We can add a new field to track the number of wound penalties that are ignored, and then we can factor that value into the final calculation for "acNetPenal". Our new field is shown below.

<field
  id="acIgnWound"
  name="Ignored Wound Penalties"
  type="derived">
  </field>

We can now modify the Calculate script on the "acNetPenal" field to take the ignored wounds into account. The revised script should look like the following.

@value = -field[acFatigue].value
if (field[acWounds].value > field[acIgnWound].value) then
  @value -= field[acWounds].value - field[acIgnWound].value
  endif

All the mechanics are in place. Our two edges can now use a simple Eval script to adjust the "acIgnWound" field, and everything works exactly as we want.

<eval index="1" phase="Setup" priority="5000"><![CDATA[
  herofield[acIgnWound].value += 1
  ]]></eval>

Dependencies on Load Limit

The "Acrobat" edge exposes a timing issue with our data files. The edge confers a +1 to a character's Parry if the character has no encumbrance penalty. To implement this, we need to have the encumbrance penalty determined before we act, and we need to act before the final value for the Parry trait is resolved. Our current script timing does not work this way, although we should be able to do this.

Our goal is to be able to adjust the Parry trait prior to its final resolution. Derived traits are officially calculated at a timing of Traits/6000. So we need to get the encumbrance penalty resolved before then if at all possible.

The encumbrance penalty is tracked by the "resEncumb" resource. The final value is currently calculated at a timing of Final/500. The only timing requirements for this calculation are the accrual of weight carried (which occurs at Effects/10000) and the load limit (which occurs at Traits/7000). The weight accrual occurs long before we need to act for the "Acrobat" edge, so we can move the timing of the penalty if we can move the timing of the load limit calculation.

The load limit is tracked by the "resLoadLim" resource and it calculates its maximum (the value we need for the encumbrance penalty) at a timing of Traits/7000. This calculation depends on the "acLoadMult" field, which is nailed down very early in the evaluation cycle, so that's not a problem. It also depends on the final value of the Strength attribute. Attributes are finalized within the "trtFinal" field, which is calculated at a timing of Traits/3000. This means we can move the load limit calculation to a timing of Traits/4000 without any problems.

Once we move the load limit calculation, we can then safely move the encumbrance penalty calculation. We'll move that to a timing of Traits/4500. This leaves nice gap in the evaluation cycle where we can inspect the encumbrance and adjust the Parry trait based on it. We'll schedule the Eval script for the edge at Traits/5000.

Since we've gone through and analyzed all these dependencies, we'll do one additional thing. Each of the various scripts we identified should be named. Then we can specify appropriate timing dependencies between these scripts. Now we can let the compiler safety check our timing dependencies for us all the time. If we need to adjust the timing of one of these scripts in the future, we can rely on the compiler to let us know if we messed something up.

Edges Need Domain Support

When we implemented edges, we identified all the ones that looked to be special in some way and added appropriate handling for them. We missed one. The "Connections" edge requires that the user specify the organization with which the connections exist. This means we have to add domain support for edges. Fortunately, we've already done that a couple times, so it should not be complicated. In fact, it's rather simple.

The first thing we need to do is add the "Domain" component to the "Edge" component set. This will integrate domain handling internally into edges. The "Domain" component handles modifying the name, so all the mechanics we need are provided.

Next, we need to add support for domains to the interface. We can copy the two portals dealing with domains from the "edHinder" template and add them to the "edEdge" template. Then we can copy the positioning code for those portals, which we can drop into place with a single change. The "lbldomain" portal can be aligned directly relative to the "name" portal instead of using the non-existent "edge" variable.

Domain support is fully operational for edges now. All that remains to be done is assign the "User.NeedDomain" tag to the "Connections" edge. Once we do that, the edge will prompt the user for a domain and integrate into the name for display.

Background Edges are Creation-Only

All background edges are generally only valid for selection during character creation. The Skeleton files provide built-in support for designating abilities as creation-only (via the "User.CreateOnly" tag). However, adding the tag individually to all background edges is error-prone. It would be best if we automatically treated all background edges as creation-only.

Doing this is easy. We can clone the pre-requisite used for this purpose within the "Ability" component and add it to the "Edge" component. We can then adapt it to only apply to background edges. Since GMs can allow the selection of background edges after creation, we'll only flag an error when showing things. Once an edge is added, we'll assume the player did so with the GM's approval. The resulting pre-requisite should look like below.

<prereq message="Background edges are only available at character creation">
  <!-- This pre-req is only applicable to background edges -->
  <match><![CDATA[
    EdgeType.Background
    ]]></match>

  <validate><![CDATA[
    ~we only report a failure on things (once added, we assume the user knows best)
    if (@ispick <> 0) then
      @valid = 1
      done
      endif

    ~if the mode is creation, we're valid
    if (state.iscreate <> 0) then
      @valid = 1
      endif
    ]]></validate>
  </prereq>

Buying Off Hindrances

  • use an advancement to buy off a hindrance

Injuries For NPCs

When we first added support for injuries, treating them as advancements made perfect sense. Unfortunately, with the support of NPCs, treating them as advancements doesn't work very well unless the character happens to be a PC. The reason for this is that advancements are not applicable to NPCs, so we hide the entire tab.

We need to allow injuries to be assigned to NPCs via some mechanism. Since advancements are intended for the application of permanent injuries, it would also be helpful to provide a convenient way to apply temporary injuries to PCs.

The first idea that comes to mind is to add the injuries as in-play adjustments. The problem with this approach is that we can only modify existing picks via adjustments. Menus do not let us add new picks to the character, so we would have to improvise something really creative in order to get injuries like "Hideous Scar" to work (i.e. to bootstrap the hindrance onto the character).

Adding a separate table of injuries to the "In-Play" tab would consume a good amount of space, and the tab is already pretty cramped. There might be plenty of room on a big screen monitor, but we have to be mindful that some users will have HL running on a small laptop at the game table. Consequently, we have to make sure everything will work on a relatively small screen.

Looking through everything, there is one convenient place where we could easily add injuries. On the "Personal" tab, there is a gap beneath the image gallery that would fit a small table of injuries very easily. We'll anchor the injury list to the bottom, immediately above the permanent adjustments list. That way, we can show as many character images as we have space for. For PCs, we'll clearly label the table as "Temporary Injuries", just to be safe.

There are a number of steps involved, but the first thing we'll do is create a new sort set. Our table of injuries will potentially exist in a very small space. When injuries are added to the table, they will be sorted alphabetically by default. What we want is to show the injuries with the most recent ones at the top. That way, the user will be able to easily identify wounds from the most recent combat. The Kit provides a special tag group with the unique id "_CreateSeq" that allows the sorting of items in the order they were created. By reversing this order, we can have a sort set that gives us exactly what we want, as shown below.

<sortset
  id="MostRecent"
  name="Reverse Chronological Order">
  <sortkey isfield="no" id="_CreateSeq" isascend="no"/>
  </sortset>

With the sort set in place, we'll define a table portal in which we can manage injuries on the "Personal" tab. We can easily use the "SimpleItem" template for showing the selected injuries, and we can use the "LargeItem" template when the user chooses injuries. The resulting table portal is quite simple should look like the following.

<portal
  id="peInjury"
  style="tblNormal">
  <table_dynamic
    component="Injury"
    showtemplate="SimpleItem"
    choosetemplate="LargeItem"
    showsortset="MostRecent">
    <titlebar><![CDATA[
      @text = "Select a New Injury from the List Below"
      ]]></titlebar>
    <headertitle><![CDATA[
      if (hero.tagis[Hero.PC] <> 0) then
        @text = "Temporary Injuries"
      else
        @text = "Injuries"
        endif
      ]]></headertitle>
    <additem><![CDATA[
      @text = "Add New Injury"
      ]]></additem>
    </table_dynamic>
  </portal>

Our final task is to integrate the new table portal into the layout on the "Personal" tab. We're inserting the portal between the images and adjustments tables, so we need to ensure that the tab order reflects that when we add our "portalref" element. After that, we can readily splice the logic for the new portal into the Position script for the layout. The revised logic that includes the impacted script code is presented below.

~reserve a width of 185 pixels for the user images table
portal[peImages].width = 185

~use the same horizontal space for the injuries table
portal[peInjury].width = portal[peImages].width

~position the injuries table immediately above the permanent adjustments and
~reserve space for at least two items
portal[peInjury].left = width - portal[peInjury].width
portal[peInjury].maxrows = 2
portal[peInjury].top = portal[peAdjust].top - portal[peInjury].height - 10

~position the images table in the upper right corner
portal[peImages].left = width - portal[peImages].width
portal[peImages].height = portal[peInjury].top - portal[peImages].top - 10

~if there is vertical space for more injuries, take advantage of it
portal[peInjury].height = portal[peAdjust].top - portal[peImages].bottom - 20
portal[peInjury].top = portal[peAdjust].top - portal[peInjury].height - 10

Hiding Equipment

Various creatures define their own custom equipment, such as the "Goblin" and "Lich". This gear needs to be private to the particular creature and not make public for general selection by the user. To solve this, we need to define a new "Hide.Equipment" tag and then assign that tag to all such gear.

Once the tags are assigned, we need to modify all of the table portals on the "Armory" tab. This new tag must be integrated into the Candidate tag expression of each portal so that equipment with the tag is not shown for selection. In each case, the revised element should look like the one shown below.

<candidate inheritlist="yes"><![CDATA[
  !Equipment.Natural & !Hide.Equipment
  ]]></candidate>

Show Linked Attributes on Skills

On the "Skills" tab, we neglected to show the linked attribute with each skill. While not critical, having the linked skill visible can be helpful. We should also include the linked attribute within the description text for each skill.

We'll start by modifying the description text. We can define a new procedure to synthesize the information for skills and integrate it into the "Descript" procedure when a skill is processed. All we need to do is add the one piece of data, which is solved with the simple procedure below.

<procedure id="InfoSkill" context="info"><![CDATA[
  ~declare variables that are used to communicate with our caller
  var iteminfo as string
  iteminfo = ""

  ~report the linked attribute
  iteminfo &= "Linked Attribute: " & linkage[attribute].field[name].text & "{br}"
  ]]></procedure>

There are two different places where we should show the linked attribute within tables. The first is within the list of selected skills that appears on the "Skills" tab. The other is within the table presented for the user to select skills to be added.

The linked attribute is a secondary piece of information, so it should be less prominent than the other facets of the skill. As such, we'll add it in a separate portal that appears at the far right and in a softer color. Within the "skPick" template that shows the skills added to the character, we can add the portal shown below.

<portal
  id="attribute"
  style="lblSecond">
  <label>
    <labeltext><![CDATA[
      @text = "(" & linkage[attribute].field[trtAbbrev].text & ")"
      ]]></labeltext>
    </label>
  </portal>

Once the portal is added, we can integrate it into the Position script for the template. There are a few things we need to tweak to complete the integration. The pertinent lines of the script are shown below.

~position the other portals vertically
...
perform portal[attribute].centervert

...

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

...

~position the name next to the adjustment
perform portal[name].alignrel[ltor,adjust,8]

~if we don't need a domain, hide it and let the name use all available space
if (tagis[User.NeedDomain] = 0) then
  portal[lbldomain].visible = 0
  portal[domain].visible = 0
  portal[name].width = minimum(portal[name].width,portal[attribute].left - portal[name].left - 10)

~otherwise, position the domain portals next to the name
else
  perform portal[lbldomain].alignrel[ltor,name,15]
  perform portal[domain].alignrel[ltor,lbldomain,2]
  portal[domain].width = minimum(150,portal[attribute].left - portal[domain].left - 5)
  endif

Adding the linked attribute to the table for choosing skills requires a little bit more work. The "skSkills" table portal currently just uses the "SimpleItem" template for selection. In order to add the linked attribute, we'll need to create our own new template. We can do this easily by copying the "SimpleItem" template and calling it "skThing". Once that's done, we can modify the "skSkills" portal to reference our new template for choosing a skill.

The final thing we need to do is revise the template for our purposes. We can get rid of all the portals except for the name, then we can add a new portal similar to the one we added above for showing the linked attribute. When we're finished, the new template should look similar to the one below.

<template
  id="skThing"
  name="Skill Thing"
  compset="Skill"
  marginhorz="3"
  marginvert="2">

  <portal
    id="name"
    style="lblNormal"
    showinvalid="yes">
    <label
      field="name">
      </label>
    </portal>

  <portal
    id="attribute"
    style="lblSecond">
    <label>
      <labeltext><![CDATA[
        @text = linkage[attribute].field[name].text
        ]]></labeltext>
      </label>
    </portal>

  <position><![CDATA[
    ~set up our height based on our tallest portal
    height = portal[name].height

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

    ~center the portals vertically
    perform portal[name].centervert
    perform portal[attribute].centervert

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

    ~position the name on the left and let it use all available space
    portal[name].left = 0
    portal[name].width = minimum(portal[name].width,portal[attribute].left - 10)
    ]]></position>

  </template>

Hindrance Validation Incorrect

The validation logic we added for hindrances works great when there are only user-added hindrances on the character. The moment that new hindrances are added via advancements and/or injuries, the logic fails. The problem is that we forgot to take into account these other types of hindrances within the validation rule. Fortunately, we can easily detect they other types of hindrances. The code below shows the revised logic needed within the script for tallying up the hindrances we care about.

~iterate through all hindrances and tally up the number of majors and minors
~Note: We must only tally hindrances that are user added and not an advance.
foreach pick in hero where "component.Hindrance"
  if (eachpick.isuser + !eachpick.tagis[Advance.?] >= 2) then
    if (eachpick.field[hinMajor].value = 0) then
      minor += 1
    else
      major += 1
      endif
    endif
  nexteach

Skill Points Must Ignore Advancement

Our skill points are tallying up fine during character creation, but we run into a problem once the character starts taking advances. The issue is that any attribute advances apply that advance to the "trtBonus" field. That's the same field we rely on to indicate when racial adjustments are applied to the character. The net result is that an attribute advance is being interpreted the same as a bonus at character creation, so our skill points tally becomes wrong once the advance is taken.

You might be wondering what the problem is with this, since the tracking of skill points isn't important once a character is created and enters advancement mode. The issue arises if the user decides to switch back to creation mode for some reason, make an adjustment, and then switch back to advancement mode. Once the user switches to creation mode, he won't be allowed to return to advancement mode due to the incorrect calculation. There is also the niggly issue of an errant validation error being reported.

This problem goes beyond advancements, though. The same issue will arise with injuries or any other permanent adjustment of the "trtBonus" field that occurs after character creation is completed. So we can't just fix this within the advancement mechanism. We need a more general solution that differentiates between bonuses applied during creation and post-creation.

The solution to this problem is actually pretty easy. We can define a new field to track only the bonuses applied during character creation. Anything that needs to apply a creation bonus must adjust this new field instead of "trtBonus". The new field should look like below.

<field
  id="trtCreate"
  name="Bonus During Creation"
  type="derived">
  </field>

To make use of this field as easy as possible, we'll define a new script macro to access the creation bonus, as shown below.

<scriptmacro
  name="traitcreation"
  param1="trait"
  result="hero.child[#trait].field[trtCreate].value"/>

With the macro in place, we can apply the appropriate changes to the various racial abilities. For example the Eval script for the "abSpirited" ability will change the code below.

#traitcreation[attrSpi] += 1

We can then define an Eval script on the "Traits" component that adds the creation bonus to the total bonus at an appropriate point in the evaluation cycle. This will ensure that all of the logic that keys on "trtBonus" remains perfectly valid. The best time to schedule this script is probably at the very beginning of the Traits phase, yielding the script below.

<eval index="6" phase="Traits" priority="1"><![CDATA[
  field[trtBonus].value += field[trtCreate].value
  ]]></eval>

The final step is to modify the skill point calculation to use the new field instead of "trtBonus". This is a simple swap-out of one field for the other. The impacted line of code is shown below.

attrib += linkage[attribute].field[trtCreate].value

After a quick reload, our skill point calculations should be operating smoothly after advancement changes modify attributes.

Next