Damage (Savage)

From HLKitWiki
Jump to navigationJump to search

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

Overview

The next facet of the Savage Worlds game system that we're going to address is the management of damage. In Savage Worlds, damage works completely differently from the Skeleton files, so we're going to need to make substantial changes.

New Mechanics

The damage mechanics for Savage Worlds are very simple, with characters suffering three wounds before being incapacitated. Fatigue works similarly, with there being two levels beyond zero before incapacitated. Characters must also track whether they are shaken, which is independent of the other two values. We could use a tracker for the wounds and fatigue, but the levels are so few and simple that it just doesn't make sense. Instead, we'll use a field to track the current value and use the normal health buttons for adjusting the damage level.

This results in the need for three new fields, which we'll add to the "Actor" component for simplicity. The "shaken" field must be designated as a "user" field, since the user will be responsible for toggling the state on/off. The other two fields will be controlled indirectly via up/down buttons. As such, they must both be declared as "derived" and must be assigned "full" persistence so that the values are saved and only change in response to the buttons. The three fields should look similar to the ones shown below.

<field
  id="acShaken"
  name="Is Shaken?"
  type="user">
  </field> 

<field
  id="acWounds"
  name="Wounds"
  type="derived"
  persistence="full">
  </field> 

<field
  id="acFatigue"
  name="Fatigue"
  type="derived"
  persistence="full">
  </field> 

Delete Old Mechanics

Since most game systems have a rather complex mechanic for damage, the Skeleton files provide support for that type of mechanic. However, none of that is needed for Savage Worlds, so we need to dispose of it all. The first item we need to delete is the "mscDamage" thing, which will be found in the file "thing_miscellaneous.dat". This particular thing is of zero use to us when managing damage for Savage Worlds.

The second task is to dispose of the "Damage" component and component set, which are both of no use to us. You'll find the component defined in the file "miscellaneous.str". The component set is auto-defined, since there is no need to blend other components into the component sets. Consequently, there is nothing to actually delete for the component set.

The third task is to dispose of all the health-related fields on the "Actor" component, except for the health summary ("acHPSumm"). These consist of the fields "acHPMin", "acHPMax", "acHPNow", and "acHPPenal".

Once the obsolete fields on the "Actor" component are gone, we need to adapt the health summary to the new mechanisms. The new logic should look something like the example below, where it properly reports the shaken state, wounds, and fatigue, highlighting any penalties appropriately.

<field
  id="acHPSumm"
  name="Health Summary"
  type="derived"
  maxfinal="50">
  <calculate phase="Render" priority="1000"><![CDATA[
    ~make sure this value consists of the elements that could cause the summary to change
    @value = field[acShaken].value * 10000 + field[acWounds].value * 100 + field[acFatigue].value
    ]]></calculate>

  <finalize><![CDATA[
    ~if we're not shaken and have incurred no negative effects, all is good
    var net as number
    net = field[acWounds].value + field[acFatigue].value
    if (field[acShaken].value + net = 0) then
      @text = "-"
      done
      endif

    ~if we're shaken, signal it with a special indicator
    @text = ""
    if (field[acShaken].value <> 0) then
      @text &= "{font wingdings}v{revert}"
      endif

    ~if we've incurred wounds, report them
    var wounds as number
    wounds = -field[acWounds].value
    if (wounds <> 0) then
      if (empty(@text) = 0) then
        @text &= "/"
        endif
      if (wounds <= -4) then
        @text &= "Inc"
      else
        @text &= wounds & "W"
        endif
      endif

    ~if we've incurred fatigue, report it
    var fatigue as number
    fatigue = -field[acFatigue].value
    if (fatigue <> 0) then
      if (empty(@text) = 0) then
        @text &= "/"
        endif
      if (fatigue <= -3) then
        @text &= "Inc"
      else
        @text &= fatigue & "F"
        endif
      endif

    ~anything we output must be in red to highlight the penalty
    @text = "{text ff0000}" & @text
    ]]></finalize>
  </field> 

The next step is to delete the usage pools associated with damage tracking. There are two of these ("DmgAdjust" and "DmgNet") and they will be found in the file "control.1st".

Once the usage pools are deleted, Eval script #1 within the "Actor" component will need to be revised to eliminate the reference to the usage pool. Change the script so that the test is based on the shaken state and the number of wounds and fatigue levels. The script below should serve nicely.

<eval index="1" phase="Final" priority="1000"><![CDATA[
  ~if no damage has been incurred, assign a tag to indicate that state
  if (field[acShaken].value + field[acWounds].value + field[acFatigue].value = 0) then
    perform hero.assign[Hero.NoDamage]

  ~if the hero is dead or otherwise out of combat, indicate that state
  elseif (field[acWounds].value >= 4) then
    perform hero.assign[Hero.Dead]
  elseif (field[acFatigue].value >= 3) then
    perform hero.assign[Hero.Dead]
    endif
  ]]></eval> 

The final big task is to eliminate most of the portals on the "In-Play" tab that are associated with damage tracking. These will be found in the file "tab_inplay.dat" and will be located in the template "ipHealth". Since we'll still be wanting to adapt a few of these portals to the new mechanism, it's better to simply comment out the various portal elements instead of outright deleting them right now.

A few minor cleanup steps still remain. With all of the portals eliminated from the template, the Position script for the template needs to be omitted as well - we'll restore it and adapt it once we put some portals back in. The template itself needs to be switched over to the "Actor" component instead of the deleted "Damage" component. And lastly, the "templateref" within the layout needs to reference the "actor" thing instead of the deleted "mscDamage" thing.

At this point, you should now be able to re-compile the data files, although the "Health" section of the "In-Play" tab will not yet behave in any useful fashion.

Managing Damage

We can now add damage management back into the data files in a systematic fashion. The first thing we need to add is a mechanism for the user to toggle the shaken state of the character. For now, we'll use a simple checkbox - we can always refine it later. In order to highlight the checkbox more prominently when a character is shaken, we'll modify the checkbox to display dynamically changing text, where the text changes to a bright red color when the character is shaken. This requires that we add another field to the "Actor" component wherein the dynamic text can be managed. The new field should look something like the following.

<field
  id="acShakeTxt"
  name="Shaken Text"
  type="derived"
  maxfinal="50">

  <calculate phase="Render" priority="1000">
    @value = field[acShaken].value
    </calculate>

  <finalize><![CDATA[
    @text = ""
    if (@value <> 0) then
      @text = "{text ff0000}"
      endif
    @text &= "Shaken"
    ]]></finalize>
  </field>

The checkbox portal that we're adding to the template should look similar to the following. 

<portal
  id="shaken"
  style="chkLarge"
  tiptext="Click to indicate the character is shaken">
  <checkbox
    field="acShaken"
    dynamic="acShakeTxt">
    </checkbox>
  </portal>

In the above checkbox, we specified a new checkbox style that is not pre-defined in the file "styles_ui.aug". We need a checkbox that shows larger text so that the shaken state is more prominently visible. This entails us defining a new style, which in turn requires that we define a new font. The new style definition should be very similar to the one shown below.

<style
  id="chkLarge">
  <style_checkbox
    textcolor="clrnormal"
    font="fntchecklg">
    </style_checkbox>
  <resource
    id="fntchecklg">
    <font
      face="Arial"
      size="48"
      style="bold">
      </font>
    </resource>
  </style>

We also need to track wounds and fatigue levels here. Both of them work pretty much the same, except that the fatigue track transitions to an incapacitated state one step earlier than wounds, so we'll use the same approach for both. Each will have a label portal to identify the grouping (wounds vs. fatigue), a script-based label portal to show the actual value, an action portal to sustain one level of negative progression, and an action portal to restore a level. We'll use the damage and healing buttons for sustaining and restoring, and we'll display the actual value with color highlighting when non-zero. We'll also make sure to use a large font to make things highly visible to the user. To keep wounds and fatigue distinct, we'll visually group the portals together for each purpose. All of the additional portals (besides the "shaken" checkbox and the title) are presented below.

<portal
  id="lblwounds"
  style="lblLarge">
  <label
    text="Wounds:">
    </label>
  </portal>

<portal
  id="wounds"
  style="lblXLarge">
  <label>
    <labeltext><![CDATA[
      var wounds as number
      wounds = -field[acWounds].value
      if (wounds > -4) then
        @text = wounds
      else
        @text = "Inc"
        endif
      if (wounds <> 0) then
        @text = "{text ff0000}" & @text
        endif
      ]]></labeltext>
    </label>
  </portal>

<portal
  id="wndsustain"
  style="actDamage"
  tiptext="Click here to sustain one wound.">
  <action
    action="trigger">
    <trigger><![CDATA[
      ~if we've reached our maximum, there's nothing left to do
      if (field[acWounds].value >= 4) then
        done
        endif
      ~sustain one new wound
      field[acWounds].value += 1
      ~any wound sustained automatically makes the character shaken
      field[acShaken].value = 1
      ]]></trigger>
    </action>
  </portal>

<portal
  id="wndheal"
  style="actHeal"
  tiptext="Click here to heal one wound.">
  <action
    action="trigger">
    <trigger><![CDATA[
      ~if we've reached our limit, there's nothing left to do
      if (field[acWounds].value = 0) then
        done
        endif
      ~heal one wound
      field[acWounds].value -= 1
      ]]></trigger>
    </action>
  </portal>

<portal
  id="lblfatigue"
  style="lblLarge">
  <label
    text="Fatigue:">
    </label>
  </portal>

<portal
  id="fatigue"
  style="lblXLarge">
  <label>
    <labeltext><![CDATA[
      var fatigue as number
      fatigue = -field[acFatigue].value
      if (fatigue > -3) then
        @text = fatigue
      else
        @text = "Inc"
        endif
      if (fatigue <> 0) then
        @text = "{text ff0000}" & @text
      endif
      ]]></labeltext>
    </label>
  </portal>

<portal
  id="ftgsustain"
  style="actDamage"
  tiptext="Click here to sustain one level of fatigue.">
  <action
    action="trigger">
    <trigger><![CDATA[
      ~if we've reached our maximum, there's nothing left to do
      if (field[acFatigue].value >= 3) then
        done
        endif
      ~sustain one new fatigue level
      field[acFatigue].value += 1
      ]]></trigger>
    </action>
  </portal>

<portal
  id="ftgheal"
  style="actHeal"
  tiptext="Click here to restore one fatigue level.">
  <action
    action="trigger">
    <trigger><![CDATA[
      ~if we've reached our limit, there's nothing left to do
      if (field[acFatigue].value = 0) then
        done
        endif
      ~restore one fatigue level
      field[acFatigue].value -= 1
      ]]></trigger>
    </action>
  </portal>

The Position script for the template that appropriately arranges everything should look something like the one presented below.

~set up our height based on our title, a gap, and our tallest portal
height = portal[title].height + portal[wounds].height + 8

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

~the title should span the full width
portal[title].width = width

~position the tallest portal beneath the title
perform portal[wounds].alignrel[ttob,title,8]

~center all non-text portals vertically on the wounds portal
perform portal[shaken].centeron[vert,wounds]
perform portal[wndsustain].centeron[vert,wounds]
perform portal[wndheal].centeron[vert,wounds]
perform portal[fatigue].centeron[vert,wounds]
perform portal[ftgsustain].centeron[vert,wounds]
perform portal[ftgheal].centeron[vert,wounds]

~align the smaller text portals to have the same baseline
perform portal[lblwounds].alignrel[btob,wounds,-3]
perform portal[lblfatigue].alignrel[btob,wounds,-3]

~position the shaken portal on the left
portal[shaken].left = 15

~position the wound portals next to the shaken portal
perform portal[lblwounds].alignrel[ltor,shaken,35]
perform portal[wounds].alignrel[ltor,lblwounds,3]
portal[wounds].width = 30

~position the sustain and heal portals next to the wounds
perform portal[wndsustain].alignrel[ltor,wounds,6]
perform portal[wndheal].alignrel[ltor,wndsustain,6]

~position the fatigue portals next to the wound portals
perform portal[lblfatigue].alignrel[ltor,wndheal,35]
perform portal[fatigue].alignrel[ltor,lblfatigue,3]
portal[fatigue].width = 30

~position the sustain and heal portals next to the fatigue
perform portal[ftgsustain].alignrel[ltor,fatigue,6]
perform portal[ftgheal].alignrel[ltor,ftgsustain,6] 

Rename the "acHPSumm" Field

When we were overhauling everything above, we left the "acHPSumm" field in place and simply changed its behavior. However, that field is inappropriate for Savage Worlds, so we should change it now that everything is again stable. We'll change the id to "acDmgSumm". When we re-compile, three errors are reported, which we can chase down and correct quickly. The three errors occur in the files "form_static.dat", "form_dashboard.dat", and "form_taccon.dat". All we need is a quick search-and-replace to swap in the new unique id.

Applying Damage to Trait Rolls

The final task we need to perform with regards to damage is to properly apply the resulting penalties to all trait rolls. Before we can do that, we must accrue the net penalty from both wounds and fatigue into a single value. This is most easily done by adding a new calculated field to the "Actor" component. The new field simply tallies the various values into one value that can be used whenever needed to adjust the trait rolls. Below is an example of what this field should look like.

<field
  id="acNetPenal"
  name="Net Penalties"
  type="derived">
  <calculate phase="Traits" priority="5000">
    <before name="Derived trtFinal"/><![CDATA[
    @value = -1 * (field[acWounds].value + field[acFatigue].value)
    ]]></calculate>
  </field>

The critical detail of the above field is its timing. We need to have the net penalties tallied up prior to the final calculation of derived traits, as well as before the final display text is generated for attributes and skills (to include the trait roll adjustments). The earlier of the two occurs at a timing of Traits/6000, so we need to pick something earlier. We'll use a timing of Traits/5000 out of convenience. Once the net penalty is properly calculated at a suitable timing, we can apply the penalty to the various trait rolls. For derived traits, the penalty needs to be added directly to the final calculated value. This is performed in the lone Eval script for the "Derived" component. By revising the script to simply add the penalty, we get a script that looks like the one below.

<eval index="1" phase="Traits" priority="6000" name="Derived trtFinal"><![CDATA[
  field[trtFinal].value = field[trtBonus].value + field[trtInPlay].value + herofield[acNetPenal].value
  ]]></eval>

For attributes and skills, the penalty is applied to the actual trait "roll" instead of the trait itself. The trait roll is determined as part of the process for synthesizing the "trtDisplay" field in an Eval script for the "Trait" component. When the bonus is tallied from the "trtRoll" and "trtNoStack" fields, the penalty should also be factored in, resulting in the appropriate net roll adjustment being calculated, as shown below.

<eval index="4" phase="Render" priority="5000" name="Calc trtDisplay">
  <after name="Calc trtFinal"/><![CDATA[
  ~if this is a derived trait, our display text is the final value
  if (tagis[component.Derived] <> 0) then
    field[trtDisplay].text = field[trtFinal].value
    done
    endif

  ~bound our final value including in-play adjustments
  var final as number
  final = field[trtFinal].value
  if (final < 2) then
    final = 2
  elseif (final > 6) then
    final = 6
    endif

  ~convert the final value for the trait to the proper die type for display
  var dietype as number
  var display as string
  dietype = final * 2
  display = "d" & dietype

  ~if there are any bonuses or penalties on the roll, append those the final result
  var bonus as number
  bonus = field[trtRoll].value + field[trtNoStack].value + herofield[acNetPenal].value
  if (bonus <> 0) then
    display &= signed(bonus)
    endif

  ~put the final result into the proper field
  field[trtDisplay].text = display
  ]]></eval>