Final Cleanup (Savage)

From HLKitWiki
Revision as of 00:51, 9 February 2009 by Rob (Talk | contribs) (Overview)

Jump to: navigation, search

Context: HL KitAuthoring Examples … Savage Worlds Walk-Through 


Our data files are complete. It's time to do a final round of cleanup. We'll be eliminating any extraneous mechanisms that we inherited from the Skeleton files. After that, we'll go through and implement every individual thing in the rulebook. This includes the complete list of edges, hindrances, skills, weapons, gear, etc. Lastly, we'll do one last round of testing so we can fix anything that has slipped past us.

Skeleton File Remnants

The Skeleton files provided us with a working set of functionality, and we used the vast majority of it. However, there are some bits left over that we didn't use. Before we consider our files complete, we should prune that material out, leaving ourselves with only material that is associated with our game system.

WARNING! Deleting information from the data files will result in lots of warnings about orphaned material when you load saved portfolios. This is normal and can be ignored, since we are intentionally deleting the information. If you encounter errors like this when you aren't intentionally deleting material, be sure to verify that you haven't made an inadvertent mistake.

We'll focus our attention first on the various things that are defined. That's because any internal mechanisms we might eliminate or change will impact these things, so it's much easier to remove the dependencies before removing the mechanisms that are depended upon. All of these things will be defined within the data files that start with the prefix "thing_". The Skeleton files included a variety of thing definitions that we're no longer using. Those we can readily identify and delete.

We'll start with any things defined for the purpose of validation. Using the logic above, validation rules often build on other things, so we eliminate them before eliminating the things they depend on. All validation rules are found in the file "thing_validate.dat", so we'll go through the each thing defined in the file and determine if we're still using it. There appears to be only one, the "valCP" thing used for testing character points. We can delete that and move on.

Proceeding down the dependency hierarchy, the next group of things to focus on are any resources or trackers, which are defined in the file "thing_miscellaneous.dat". There are two resources in this file that we inherited from the Skeleton files are no longer using. These are the "resCP" and "resAbility" things, so we'll delete them.

Once we do that, we'll discover a couple of old dependencies on those two things within the "resXP" thing. We're definitely using the "resXP" resource, but the Eval script that adjusts the available quantity of those resources based on the XP is extraneous. Once we delete that, this file is cleaned.

However, there is a second dependency. The "Ability" component we inherited from the Skeleton files has an Eval script that accrues the usage of ability points. With the elimination of the "resAbility" thing, we must also eliminate the script.

We can now go through all the other data files in which we defined things. Our goal is to eliminate any sample things that were provided or that we copied, adapted, and left hanging around. The list below spells out the various places we need to check.

  • Sample attributes
  • Sample skills
  • Sample abilities
  • Sample adjustments
  • Sample advances
  • Sample weapons
  • Sample armor
  • Sample gear
  • Sample races
  • Sample edges
  • Sample hindrances

After going through the data files in search of the above items, there should only be one file left that we haven't assessed and in which things are defined. That's the file "thing_traits.dat". The Skeleton files provided us with a number of traits that we didn't use, and there should no longer be any references to them. Consequently, we can now delete them easily. The set of things to remove are "trHealth", "trInit", "trDefense", and "trPowerPts".

Our data files should no longer have extraneous things lurking within them, so we can shift our focus to the internals. In general, we don't really need to worry much about mechanisms built into the various components. Most of those mechanisms have been fully adapted to our purposes for Savage Worlds. The few exceptions are causing no trouble and will require a fair amount of work to isolate and remove. So it's best to just leave them alone, unless they are either obvious or a source of potential problems.

In our case, there are two places that are worth addressing. The first is the "Ability" component. We changed this component so that it provides a number of shared behaviors, but the Skeleton files originally defined it without that in mind. As such, there is an important behavior provided by the component that is either redundant or a potential problem. The component defines an identity tag group and assigns a corresponding tag to the hero for every ability. Each separate type of ability already does this with its own identity group, so we can delete the identity group definition and the Eval script that forwards the tag to the actor.

The final cleanup task we should do is review the contents of the "Actor" component. For many game systems, at least a few mechanisms provided in this component will be unused. Since this component is rather extensive, keeping it pruned of unnecessary logic will simply make our data files easier to maintain. We can go through the list of fields and identify those that are now orphaned. This consists of the fields "acStartCP" and "acStartAbi", which we can delete. Scanning through the rest of the component, everything else is being used.

At this point, our data files have been properly cleaned of any unused remnants of the Skeleton files.

NOTE! If our game system did not use the advancements mechanism, we could also delete the data files specific to advancements and all the associated references.

New Characters Report Wrong Attribute Points

When we create a new character, the number of attribute points is being displayed as "-12 of 5" above the table of attributes. The moment that we make any change at all to the character, everything behaves correctly. We obviously have a timing problem.

The value "-12" is quite suspicious, too. We have a total of six attributes defined (including the hidden one for super powers). That translates to a difference of two points per attribute. It appears that we broke this when we changed the default value of each attribute from two to zero.

The attribute values are being properly bounded to two, as evidenced by everything being corrected by making any change to the character. A change triggers a new evaluation pass. This means that the field on the resource that contains the message for display is probably being synthesized before the bounding of the field value is occurring. Unfortunately, the message is being synthesized during the Render phase, so that's not the case.

Looking a bit more closely, the message being synthesized is using the "resLeft" field. That field is being calculated at a timing of Final/1000. However, the bounding is occurring at a timing of Final/10000. We've uncovered the source of the problem.

We need to change the timing of one or both actions. Before we can do that, though, we must first assess what other timing dependencies exist on these two actions. The calculation of the "resLeft" field is named, so we need to assess what other scripts depend on it. There are two, but both of them must occur before the calculation, so we are free to move the calculation later if we wish.

The bounding script is also named, and there is one dependency. The calculation of the delta must occur before the bounding is performed, and the delta depends on the fields "trtBonus" and "trtInPlay". Both of those fields must be in their final state before they are used to calculate the "trtFinal" value, and that happens at a timing of Traits/3000. This means that we can freely move the delta calculation to substantially earlier if we wish.

The pieces of the puzzle are now in place. The best solution is probably to change the timing of all three scripts. We'll calculate the delta for "trtUser" at a timing of Final/1000. Then we can move the timing of the bounding to Final/5000. Lastly, we can move the timing of the "resLeft" calculation to Final/7000. We'll also specify an "after" timing dependency of the "resLeft" calculation upon the bounding so that we don't run into this problem again in the future.

Once we make these changes, everything should work properly again. Unfortunately, the problem remains. We found a piece of the problem, but there is something else amiss still.

After adding some "debug" statements to our scripts, we'll confirm that "resLeft" calculation is yielding the wrong results. So we need to focus on the information that the "resLeft" calculation depends upon. This would be the "resMax" value and the "resSpent" value. Doing a quick search of where the "resMax" field is referenced in the data files confirms that the "resMax" value is not the culprit, so that leaves the "resSpent" field. Doing another scan of where the "resSpent" field is referenced uncovers that it is being tallied for attributes via an Eval script at a timing of Setup/5000. That's way before the bounding occurs, which explains the problem. If we change the timing to Final/6000 (i.e. after the bounding), everything starts to work correctly.

To make sure this problem doesn't arise again, we'll add a timing dependency to the erroneous Eval script. We'll ensure that this script always occurs after the bounding of the "trtUser" field.

But wait a minute. The script we fixed only applies to attributes. A similar script exists to tally the points spent for skills. Since skills are traits, they also need to be tallied after the bounding is performed. Skills start with a default "trtUser" value of zero, so we aren't noticing the error. However, if we changed the default value, we would see the same problem. The solution is to make sure that the Eval script on the "Skill" component is also changed to the same timing and that a suitable timing dependency is also added.

The only other use of the "Trait" component is with derived traits. Since there is no resource being tallied for derived traits, there are no other places where this problem could be lurking.

Rank and XP Shown for Creatures

There are a number of places where XP and rank are being shown for creatures. We eliminated this from a few obvious spots, but we overlooked some as well. We need to go through these locations and verify that the XP and rank are omitted for creatures. We can identify all the places that we need to check by doing a search for references to the "acRankName" field name.

The first instance we find is within the LeadSummary script in the definition file. We can wrap the logic for including the XP within an if/then block, as shown below.

~append the rank and XP (only for non-creatures)
if (hero.tagis[Hero.Creature] = 0) then
  @text &= " - " & herofield[acRankName].text & " - " & herofield[acFinalXP].value & " XP"

The next instance is within the Eval script that synthesizes the ally recap within the "Actor" component. We can use the same technique and wrap the code within an if/then block. However, this instance requires a little more tweaking than just an if/then block, since the punctuation output assumes the XP is always included. The revised logic should look like the code below.

~output any race
recap &= field[acRaceName].text

~output the XP and rank (only for non-creatures)
if (hero.tagis[Hero.Creature] = 0) then
  recap &= ", " & field[acRankName].text & "  (" & field[acFinalXP].value & " XP)"

There are two instances within character sheet output that need to be addressed. One is on the first page, where the character details are synthesized in the "oHeroInfo" portal. We can again wrap the logic within an if/then block, as shown below.

~append the rank and XP (only for non-creatures)
if (hero.tagis[Hero.Creature] = 0) then
  @text &= "; " & herofield[acRankName].text & "  ("
  @text &= herofield[acFinalXP].text & " XP)"

The other instance within the character sheet output is on the second page, within the output for allies. We can use an if/then block again, although we need to be sure to keep the newline outside of the conditional block. The resulting code is below.

~output the rank and XP (only for non-creatures)
if (hero.tagis[Hero.Creature] = 0) then
  @text &= "; {b}" & herofield[acRankName].text & "{/b} ("
  @text &= herofield[acFinalXP].text & " XP)"

~insert a newline before continuing our output
@text &= "{br}"

The XP and rank are also shown within the "Basics" and "Journal" tabs. However, we have already dealt with both. On the "Basics" tab, the portal with the information is hidden for creatures. Similarly, the entire "Journal" tab is hidden for creatures.

Derived Traits Positioning Within Sheets

It turns out we didn't properly anticipate some of the nuances of derived traits when we first implemented character sheet output. All of the portals are positioned on a left-to-right basis within the "oDerivPick" template. However, if a derived trait is more than one character in width, everything ends up being aligned oddly.

We need to change the behavior so that the "adjust" portal is placed against the right edge. Once that's done, we can then place the "value" portal relative to the "adjust" portal on a right-to-left basis. This will ensure that everything always lines up consistently, regardless of the width of the "value" portal.

We can accomplish this change by replacing the code within the Position script that handles horizontal placement. While we're at it, we'll also assign a "marginhorz" attribute of "25" to the template, which allows us to simply place both the "name" and "adjust" portals flush against their respective edges. The revised code for the Position script is shown below.

~position everything horizontally
portal[name].left = 0
perform portal[adjust].alignedge[right,0]
perform portal[value].alignrel[rtol,adjust,-20]

Show XP and Rank on Basics Summary Panel

There is plenty of available space at the bottom of the "Basics" summary panel, and we should consider putting that space to good use. One thing that would be potentially helpful on the panel is the character's current rank and XP. Adding it is easy, so let's do that now.

To keep things as simple as possible, we define a single label portal for displaying the rank and XP. We'll use the same approach as we did on the "Basics" tab. In fact, we might as well clone the "baRank" portal from that tab and adapt for the summary panel. After we change the unique id, the only detail we need to modify is the style. All other particulars remain the same, resulting in the new portal shown below.

      @text = herofield[acRankName].text & "  (" & herofield[acFinalXP].text & " XP)"

We now need to integrate the portal into the layout. We can add a "portalref" element and then simply auto-place the new portal beneath the "status" portal. In keeping with the policy we instituted a little bit ago, we also need to make sure that the new portal is not visible when the character is a creature. The revised layout should look like the following.

  <portalref portal="smAttrib"/>
  <portalref portal="smDerived"/>
  <portalref portal="smStatus"/>
  <portalref portal="smRank"/>

    ~the rank is only visible for non-creatures
    if (hero.tagis[Hero.Creature] <> 0) then
      portal[smRank].visible = 0

    ~position and size the tables to span the full layout
    perform portal[smAttrib].autoplace
    perform portal[smDerived].autoplace[20]
    perform portal[smStatus].autoplace[20]
    perform portal[smRank].autoplace[20]


Widen Description Widths

There are a number of dynamic tables that present choices with lengthy descriptions. In order to better accommodate these descriptions, we need to increase the space afforded for showing those descriptions. This is controlled through the "descwidth" attribute on the table portals. The following changes should be made.

  • Skills table - increase to 350
  • Edges table - increase to 350
  • Hindrances table - increase to 350

Consistent Terminology with Rulebook

The Savage Worlds rulebook sometimes uses terminology that is different from what the Skeleton data files provide. When that occurs, we need to be sure to use the terminology that is consistent with the rulebook. We neglected to do that on the "Skills" tab, where we use the term "domain" instead of "focus".

We can easily change the label within the "skPick" template to show the term "Focus". However, if we modify the "Domain" component directly, we'll also change the behavior for domains with other game elements, such as hindrances. The proper way to customize the behavior of the "Domain" component is by defining a new tag within the "DomainTerm" tag group that has the proper term to be used. The new tag is shown below and must be defined within the "DomainTerm" tag group.

<value id="Focus"/>

We can then assign the tag to the "Skill" component, which results in all skills inheriting the proper term, as shown below.

<tag group="DomainTerm" tag="Focus"/>

There is one other place where we need to handle this properly. The advancements mechanism also prompts for domains, but it doesn't use skills directly. The various advancement things are used instead. So we need to assign the same tag to the advancement thing used for adding new skills (i.e. "advSkill").

After making these changes, we can reload the files and verify that all references to the term "domain" are correctly being mapped to "focus" for skills.

Description Text for Hindrances

The description text shown for hindrances should include the appropriate minor or major designation. The best way to address this is to handle it within the "Descript" procedure. By defining a new procedure for handling the custom aspects of hindrances, the "Descript" procedure can call the new procedure and synthesize everything properly

The new procedure must distinguish between things and picks. For picks, any user-selected severity must be shown. For things, the fact that a hindrance has a user-selectable severity must be indicated. The new procedure for hindrances is shown below. This procedure should be called when the "component.Hindrance" tag is encountered.

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

  ~center the severity rating
  iteminfo &= "{align center}"

  ~if the severity is user-selected, report that fact; otherwise, report
  ~whether the hindrance is major or minor
  if (!ispick + tagis[User.UserSelect] >= 2) then
    iteminfo &= "(Minor or Major)"
  elseif (field[hinMajor].value <> 0) then
    iteminfo &= "(Major)"
    iteminfo &= "(Minor)"

  ~terminate the line and stop the centered alignment
  iteminfo &= "{br}{align left}"

Description Text for Arcane Backgrounds

Arcane backgrounds a conferred via the corresponding edge. Consequently, the arcane background itself is never shown anywhere, so we don't provide the user with any of the details regarding the various arcane backgrounds. We should display that the details of each arcane background within the description for the pertinent edges.

One solution would be to enter all the details for each background into the description for the edge. However, it would be much better to maintain the description text for the arcane backgrounds separately and pull that text into the description for the edge. We can accomplish this with a little bit of scripting.

We'll define a new procedure that synthesizes the description particulars of an edge. Then we can call this procedure from within the "Descript" procedure when an edge is being rendered. Within our new procedure, we can use a "foreach" statement to access the bootstraps assigned to the edge. We'll only access the arcane background things, and we'll integrate their description text appropriately. We can also use a nested "foreach" statement that pulls the details of any drawback associated with the arcane background. The resulting procedure should look like the one below.

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

  ~if this edge does not possess an arcane background, there's nothing special to do
  if (tagis[Arcane.?] = 0) then

  ~output the particulars of the arcane background
  foreach bootstrap in this where "component.Arcane"

    ~append the description of the arcane background
    iteminfo &= eachthing.field[descript].text & "{br}"

    ~append any drawback associated with the arcane background
    foreach bootstrap in eachthing where "component.Drawback"
      iteminfo &= "{br}{b}" & eachthing.field[name].text & ":{/b} "
      iteminfo &= eachthing.field[descript].text & "{br}"


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}"
@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.

      @text = "Arcane Drawbacks"

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

  name="Output Drawbacks Table"

        @text = field[name].text & "{/b}  {size 32}" & field[summary].text

    ~our details width spans the entire template width
    portal[details].width = width

    ~our height is the height of our portal
    height = portal[details].bottom

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.

  name="Ignored Wound Penalties"

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

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

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.