Weapons (Savage): Difference between revisions

From HLKitWiki
Jump to navigationJump to search
New page: {{context|Authoring Examples|Savage Worlds Walk-Through}} ===Overview=== Now that basic gear is in, this section will examine a more complex type of equipment: weapons. We'll continue wi...
 
No edit summary
 
(23 intermediate revisions by the same user not shown)
Line 7: Line 7:
===Weapons in General===
===Weapons in General===


There is only one facet of weapons in general that is distinct from our default behaviors. Both hand weapons and ranged weapons can possess the armor piercing attribute, with a corresponding rating being specified. Since there is a rating involved, we need to add a new field to the "BaseWeapon" component in order to track the rating. You'll find the component in the file "equipment.pri", and the new field should look similar to the one shown below.
There is only one facet of weapons in general that is distinct from the default behaviors of the Skeleton files. Both hand weapons and ranged weapons can possess the "armor piercing" attribute, with a corresponding rating being specified. Since there is a rating involved, we need to add a new field to the "BaseWeapon" component in order to track the rating. You'll find the component in the file "equipment.str", and the new field should look similar to the one shown below.


<pre>
<pre>
Line 22: Line 22:
~report the armor piercing rating of the weapon (if any)
~report the armor piercing rating of the weapon (if any)
if (field[wpPiercing].value > 0) then
if (field[wpPiercing].value > 0) then
   iteminfo &= "Armor Piercing: " & field[wpPiercing].value & "\n"
   iteminfo &= "Armor Piercing: " & field[wpPiercing].value & "{br}"
   endif
   endif
</pre>
</pre>


The final thing we need to do is add the armor piercing rating to the special notes that are shown for weapons on the Armory tab. The notes to be displayed are synthesized into the "wpNotes" field for weapons via an Eval script within the "BaseWeapon" component. Locate this script and add the appropriate new code to include the armor piercing rating at the start. The new code should look similar to the lines below.
The final thing we need to do is add the armor piercing rating to the special notes that are shown for weapons on the Armory tab. The notes to be displayed are synthesized into the "wpNotes" field for weapons via an Eval script within the "WeaponBase" component. Locate this script and add the appropriate new code to include the armor piercing rating at the start. The new code should look similar to the lines below.


<pre>
<pre>
Line 40: Line 40:
===Minimum Strength Requirement===
===Minimum Strength Requirement===


Weapons may be specified as having a minimum strength requirement. If a weapon is equipped when that requirement is not meant, using the weapon incurs a penalty of -1 for each step of shortfall. The Skeleton files provide some basic mechanisms for managing the strength requirement, but we must apply the penalty appropriately ourselves. Open the file "equipment.pri" and locate the "BaseWeapon" component. The first Eval script handles the minimum strength requirement logic, applying a penalty to the weapon and having a place to apply a more general penalty. Within Savage Worlds, there is no generalized penalty, so those lines can be deleted from the script. The penalty adjustment applied is currently a flat "-1", so we must change it to represent the shortfall. The net result is a revised Eval script that looks similar to the following script.
Weapons may be specified as having a minimum strength requirement. If a weapon is equipped when that requirement is not meant, using the weapon incurs a penalty of -1 for each step of shortfall. The Skeleton files provide some basic mechanisms for managing the strength requirement, but we must apply the penalty appropriately ourselves. Open the file "equipment.str" and locate the "WeaponBase" component. The first Eval script handles the minimum strength requirement logic, applying a penalty to the weapon and having a place to apply a more general penalty. Within Savage Worlds, there is no generalized penalty, so those lines can be deleted from the script. The penalty adjustment applied is currently a flat "-1", so we must change it to represent the shortfall. The net result is a revised Eval script that looks similar to the following script.


<pre>
<pre>
<eval value="1" phase="Final" priority="5000"><![CDATA[
<eval index="1" phase="Final" priority="5000"><![CDATA[
   ~if the minimum strength is satisfied, there's nothing to do
   ~if the minimum strength is satisfied, there's nothing to do
     if (field[wpStrReq].value <= #trait[attrStr]) then
     if (field[wpStrReq].value <= #trait[attrStr]) then
       done
       done
       endif
       endif
   ~assign a tag to indicate the requirement isn't met
   ~assign a tag to indicate the requirement isn't met
   perform assign[Helper.BadStrReq]
   perform assign[Helper.BadStrReq]
   ~apply any penalty required with the specific weapon
   ~apply any penalty required with the specific weapon
   field[wpPenalty].value = #trait[attrStr] - field[wpStrReq].value
   if (#trait[attrStr] < field[wpStrReq].value) then
    field[wpPenalty].value = #trait[attrStr] - field[wpStrReq].value
    endif
 
   ~if not equipped, there's nothing more to do
   ~if not equipped, there's nothing more to do
   if (field[grIsEquip].value = 0) then
   if (field[grIsEquip].value = 0) then
Line 61: Line 66:
===Weapon Categories===
===Weapon Categories===


In the same way we encountered for basic gear, weapons in Savage Worlds are grouped into categories or types. If we want our weapons to be displayed to the user in appropriate groupings to match the rulebook, we'll need to manage these groupings. This entails the creation of a new tag group that we'll call "WeaponType" and for which we'll define tags for each grouping. We can then assign that appropriate tag to each weapon. We could potentially create separate tag groups for hand weapons and ranged weapons, but there is no real benefit in doing it, so we'll just use a single tag group for all weapons. As we did for basic gear, the tag group needs to be assigned the "explicit" sequencing behavior, and each tag needs to be assigned an appropriate "order" attribute to designate the proper sort order to match the rulebook. The net result is a tag group that looks like the following.
In the same way we encountered for basic gear, weapons in Savage Worlds are grouped into categories or types. If we want our weapons to be displayed to the user in appropriate groupings to match the rulebook, we'll need to manage these groupings. This entails the creation of a new tag group that we'll call "WeaponType" and for which we'll define tags for each grouping. This gets added to the file "tags.1st". We can then assign the appropriate tag to each weapon. We could potentially create separate tag groups for hand weapons and ranged weapons, but there is no real benefit in doing it, so we'll just use a single tag group for all weapons. As we did for basic gear, the tag group needs to be assigned the "explicit" sequencing behavior, and each tag needs to be assigned an appropriate "order" attribute to designate the proper sort order to match the rulebook. The net result is a tag group that looks like the following.


<pre>
<pre>
Line 84: Line 89:
</pre>
</pre>


With the weapon type available, we need to put it to use. This will be done through a new sort set that parallels what we did for basic gear, and we'll call it "Weapon". The sort set sorts first on the weapon type and then on the name of the thing, which results in the following definition.
With the weapon type available, we need to put it to use. This will be done through a new sort set that parallels what we did for basic gear, and we'll call it "Weapon". The sort set first sorts on the weapon type and then on the name of the thing, which results in the following definition to be added to the file "control.1st".


<pre>
<pre>
Line 97: Line 102:
===Hand Weapons===
===Hand Weapons===


There are two aspects of hand weapons that differ from what's provided by the Skeleton files. The first is the parry adjustment, and the second is the weapon's reach. As with how we handled armor piercing, both of these aspects are numeric ratings, so we'll add new fields to track both of them. Since these aspects only apply to hand weapons, we'll add them to the "WeapMelee" component, which is in the file "equipment.pri". The new fields should look similar to the ones shown below.
We'll focus on hand weapons first. The critical issue with hand weapons is the "weapon die". The damage for most hand weapons is based on the wielder's Strength plus a weapon die. The interesting wrinkle is that the weapon die cannot exceed the wielder's Strength die. If a weapon is listed with a damage of "Str+d8" and the wielder's Strength is "d6", the actual damage is downgraded to "Str+d6". In addition, none of the benefits of the weapon (e.g. Parry bonus) are conferred if the weapon die exceeds the Strength die.
 
To handle this, we're going to need to add a new field to track the weapon die. Each weapon will specify the appropriate weapon die, and then we'll use an Eval script to downgrade it if the wielder's Strength die is less. The actual damage we display can then be synthesized via another script, after everything has been resolved. We'll worry about how (and whether) to confer the weapon benefits in the next section.
 
There are also a few weapons that don't have a weapon die and do a fixed amount of damage. For example, the Bangstick does 3d6 damage. In this case, we must not adjust the weapon damage. To handle this, we'll need to distinguish a weapon with a weapon die from a weapon without one.
 
The simplest solution to all of this is to use a tag to indicate the weapon die for a given weapon. If no tag is assigned to the weapon, then the damage is fixed. Otherwise, we'll use the tag to determine the weapon die and adjust it as appropriate based on the Strength.
 
To handle the weapon die tags, we must define a new tag group in the file "tags.1st". The tag group needs to have separate tags for each of the different die types. We can allow the tag value to be the weapon die value we want to use internally (e.g. "2" for "d4", just like we do for attributes). However, we'll make sure the name of each tag is the proper die type for display. The resulting tag group should look like the following.
 
<pre>
<group
  id="WeaponDie"
  name="Weapon Die-Type">
  <value id="2" name="d4"/>
  <value id="3" name="d6"/>
  <value id="4" name="d8"/>
  <value id="5" name="d10"/>
  <value id="6" name="d12"/>
  </group>
</pre>
 
We can now add a new field in which we'll track the weapon die. As part of this field, we can define a Calculate script that interprets the tag and sets up the die type appropriately. Since the weapon die only applies to hand weapons, we'll add the handling to the "WeapMelee" component, which is in the file "equipment.str". The resulting field definition looks like below.
 
<pre>
<field
  id="wpDie"
  name="Weapon Die"
  type="derived">
  <calculate phase="Initialize" priority="5000"><![CDATA[
    ~setup our die type based on any assigned tag
    @value = tagvalue[WeaponDie.?]
    ]]></calculate>
  </field>
</pre>
 
The field is in place, so we can define the Eval script to handle the downgrading. Since we know that the weapon is failing it's strength requirement here, we can assign it a suitable tag to indicate that fact. We have to time script to occur after the Strength is resolved. If a downgrade occurs, we need to check that state before the derived traits like Parry are resolved. This means our script timing must occur right after the Strength is resolved. This yields the following Eval script.
 
<pre>
<eval index="4" phase="Traits" priority="4000">
  <after name="Calc trtFinal"/><![CDATA[
  ~if the weapon die is greater than the strength die, downgrade it
  if (field[wpDie].value > #trait[attrStr]) then
    field[wpDie].value = #trait[attrStr]
    perform assign[Helper.BadStrReq]
    endif
  ]]></eval>
</pre>
 
The final piece we need to deal with is displaying the damage for a weapon. When we display a thing to the user, no Calculate or Eval scripts are evaluated. That means we have two choices for displaying weapons based on their die type. The first option is to duplicate the damage within the "wpDamage" field. That's going to be error prone, so it's a poor choice. The alternative is to use a Finalize script, since a Finalize script is always evaluated for a thing. Unfortunately, Finalize scripts can't be used on text-based fields, so we can't define one for the "wpDamage" field.
 
The solution is to define a new value-based field and put a Finalize script on it. Within the Finalize script, we need to handle the display of both things and picks. For a thing, the Calculate script on the "wpDie" field has not been performed, so we need to pull the value from the tag. Otherwise, we can pull the value from the (possibly downgraded) field.
 
Since we'll want to always use this field for showing damage to the user, it needs to exist for all weapons. That means it needs to be defined on the "WeaponBase" component. If we have a ranged weapon that doesn't include the "wpDie" field, we must be careful not to try accessing the non-existent field. We can safeguard against this by checking for the "component.WeapMelee" tag before accessing the weapon die logic.
 
The role of our new field will be solely for display of the final damage for a weapon, so we'll name it accordingly. Putting all this together results in the new field definition shown below.
 
<pre>
<field
  id="wpShowDmg"
  name="Final Damage for Display"
  type="derived"
  maxfinal="20">
  <!-- If we have a weapon die, synthesize for display, else use wpDamage -->
  <finalize><![CDATA[
    ~use the weapon die to display the damage, else use the explicit damage
    if (tagis[component.WeapMelee] + tagis[WeaponDie.?] < 2) then
      @text = field[wpDamage].text
    else
      var die as number
      if (@ispick = 0) then
        die = tagvalue[WeaponDie.?] * 2
      else
        die = field[wpDie].value * 2
        endif
      @text = "Str+d" & die
      endif
    ]]></finalize>
  </field>
</pre>
 
The handling of the weapon die is now in place. However, we'll need to remember to always use the "wpShowDmg" field for displaying weapon damage. The Skeleton files assume the "wpDamage" field is used, so we need to go through everywhere and change the reference where appropriate. The list is lengthy, so we won't go through each here. You can do a search for "wpDamage" within all the data files and replace it will "wpShowDmg" anywhere that it is being used for display or output of some sort. In general, the only exception would be the definitions of weapons themselves.
 
===Parry and Reach===
 
There are two remaining aspects of hand weapons that differ from what's provided by the Skeleton files. The first is the parry adjustment, and the second is the weapon's reach. As with how we handled armor piercing, both of these aspects are numeric ratings, so we'll add new fields to track both of them. Since these aspects only apply to hand weapons, we'll add them to the "WeapMelee" component. The new fields should look similar to the ones shown below.


<pre>
<pre>
Line 113: Line 203:
</pre>
</pre>


With the tracking of the Parry adjustment, we need to apply that adjustment appropriately when the weapon is equipped. That entails defining a new Eval script for the component. All it needs to do is apply the proper adjustment to Parry, which is achieved via the script below.  
With the tracking of the Parry adjustment, we need to apply that adjustment appropriately when the weapon is equipped. That entails defining a new Eval script for the component. All it needs to do is apply the proper adjustment to Parry prior to when the final Parry value is calculated. However, there is an important wrinkle. If the weapon die requirement is not satisfied, the Parry bonus is not applied. This means we also have to schedule our script after we downgrade the weapon die. Fortunately, there is a small window in which we can achieve this, resulting in the script below.  


<pre>
<pre>
<eval value="4" phase="PreTraits" priority="5000">
<eval index="5" phase="Traits" priority="5000">
   <before name="Calc trtFinal"/><![CDATA[
   <before name="Derived trtFinal"/>
  <after name="Downgrade Weapon Die"/><![CDATA[
   if (field[grIsEquip].value <> 0) then
   if (field[grIsEquip].value <> 0) then
     #traitbonus[trParry] += field[wpParry].value
     ~negative adjustments are always applied
    if (field[wpParry].value < 0) then
      perform #traitadjust[trParry,+,field[wpParry].value,"Equipped Weapon"]
    ~otherwise, adjustments are only applied when the strength is satisfied
    elseif (tagis[Helper.BadStrReq] = 0) then
      perform #traitadjust[trParry,+,field[wpParry].value,"Equipped Weapon"]
      endif
     endif
     endif
  ]]></eval>
</pre>
</pre>


Again as we did for armor piercing, we need to add these ratings to the weapon description details. This can be done within the "InfoMelee" procedure within the file "procedures.dat". Since the rulebook omits these ratings unless they are non-zero, we'll do the same within the description text. This yields code that should look similar to the following.  
Again as we did for armor piercing, we need to add these ratings to the weapon description details. This can be done within the "InfoMelee" procedure within the file "procedures.dat". Since the rulebook omits these ratings unless they are non-zero, we'll do the same within the description text. It would be ideal if we can indicate to the user when these abilities don't apply for a weapon. We could accomplish that by highlighting the adjustments in red when the weapon does not satisfy the strength requirements. This yields code that should look similar to the following.  


<pre>
<pre>
~report the parry adjustment of the weapon (if any)
~report the parry adjustment of the weapon (if any)
if (field[wpParry].value <> 0) then
if (field[wpParry].value <> 0) then
   iteminfo &= "Parry Adjustment: " & signed(field[wpPiercing].value) & "\n"
   iteminfo &= "Parry Adjustment: " & signed(field[wpParry].value) & "{br}"
   endif  
   endif  
~report the reach of the weapon (if any)
~report the reach of the weapon (if any)
if (field[wpReach].value > 0) then
if (field[wpReach].value > 0) then
   iteminfo &= "Reach: " & field[wpReach].value & "\n"
   iteminfo &= "Reach: " & field[wpReach].value & "{br}"
  endif
 
~if the weapon fails its strength requirement, change color and style
if (empty(special) = 0) then
  if (tagis[Helper.BadStrReq] <> 0) then
    special = "{text a00000}{i}" & special & "{/i}{text 010101}"
    endif
   endif
   endif
</pre>
</pre>


Our last task is to add these two ratings to the special notes shown for weapons on the Armory tab. This can be done via the Eval script within the "WeapMelee" component that performs this function. Since the logic is basically the same, we can use the code from adding the armor piercing as a template. The new lines of code should look like below.
Our last task is to add these two ratings to the special notes shown for weapons on the Armory tab. This can be done via the Eval script within the "WeapMelee" component that performs this function. Since the logic is basically the same, we can use the code from adding the armor piercing as a template. As we did above, we'll highlight the abilities if they aren't available due to insufficient Strength. The new lines of code should look like below.


<pre>
<pre>
~if the weapon fails its strength requirement, change color and style
if (tagis[Helper.BadStrReq] <> 0) then
  iteminfo &= "{text a00000}{i}"
  endif
~report the parry adjustment of the weapon (if any)
~report the parry adjustment of the weapon (if any)
if (field[wpParry].value <> 0) then
if (field[wpParry].value <> 0) then
Line 147: Line 256:
   special &= "Parry " & signed(field[wpParry].value)
   special &= "Parry " & signed(field[wpParry].value)
   endif
   endif
~report the reach rating of the weapon (if any)
~report the reach rating of the weapon (if any)
if (field[wpReach].value <> 0) then
if (field[wpReach].value <> 0) then
Line 154: Line 264:
   special &= "Reach " & field[wpReach].value
   special &= "Reach " & field[wpReach].value
   endif  
   endif  
~if the weapon fails its strength requirement, restore color and style
if (tagis[Helper.BadStrReq] <> 0) then
  iteminfo &= "{/i}{text 010101}"
  endif
</pre>
===Revised Strength Requirements===
There's an important detail that we overlooked in getting the weapon die working. The way we handled things changes the way that minimum strength requirements are detected for hand weapons. As we saw a few moments ago, the Skeleton files provide basic handling that keys on the "wpStrReq" field value. The will continue to work for ranged weapons, but it no longer works for hand weapons, so we need to revise those mechanisms.
There are two separate instances on the "WeaponBase" component that deal with strength requirements. One is an Eval script and the other is a pre-requisite. Since the method is changing for hand weapons only, we have two options. We can move these existing scripts to the "WeapRange" component and then create new versions within the "WeapMelee" component. Alternately, we can adapt these scripts to handle both cases. The logic change is rather simple, so we'll choose the latter option.
Within the pre-requisite test, the only change we need to make is how we determine the minimum strength to be tested. For hand weapons, we use the weapon die if it's available. Otherwise, we use the the field value. We'll use the same test from earlier to determine which method to employ. The revised Validate script is shown below.
<pre>
<validate><![CDATA[
  ~if this is a pick, we're valid
  ~NOTE! We assume that equipping an item without the strength just applies penalties.
  if (@ispick <> 0) then
    @valid = 1
    done
    endif
  ~get the minimum strength required for the weapon
  var minstr as number
  if (altthing.tagis[component.WeapMelee] + altthing.tagis[WeaponDie.?] < 2) then
    minstr = altthing.field[wpStrReq].value
  else
    minstr = altthing.tagvalue[WeaponDie.?]
    endif
  ~verify whether we meet the strength requirement
  if (#trait[attrStr] >= minstr) then
    @valid = 1
    endif
  ]]></validate>
</pre>
The same logic needs to be applied to the Eval script. The only other difference is that we only want to assign the "Helper.BadStrReq" tag if it's not already present. We assign that tag when we downgrade the weapon die so that we can detect it for disabled abilities like Parry. Assigning it again is rather silly (albeit harmless). The resulting Eval script should look like the one below.
<pre>
<eval index="1" phase="Final" priority="5000"><![CDATA[
  ~get the minimum strength required for the weapon
  var minstr as number
  if (tagis[component.WeapMelee] + tagis[WeaponDie.?] < 2) then
    minstr = field[wpStrReq].value
  else
    minstr = tagvalue[WeaponDie.?]
    endif
  ~if the minimum strength is satisfied, there's nothing to do
  if (minstr <= #trait[attrStr]) then
    done
    endif
  ~assign a tag to indicate the requirement isn't met
  ~Note: This tag may already be assigned, so don't duplicte it.
  if (tagis[Helper.BadStrReq] = 0) then
    perform assign[Helper.BadStrReq]
    endif
  ~apply any penalty required with the specific weapon
  field[wpPenalty].value = #trait[attrStr] - field[wpStrReq].value
  ~if not equipped, there's nothing more to do
  if (field[grIsEquip].value = 0) then
    done
    endif
  ]]></eval>
</pre>
At this point, our strength requirements logic should be operating properly.
===Damage Bonus===
There's one thing that we have forgotten to handle for hand weapons. The "Katana" has a damage of "Str+d6+2". If the "d6" indicates the weapon die, then the extra "+2" is not able to be accommodated in any way. The "wpBonus" field represents an attack bonus - not damage - so we need something else.
To solve this, we can add a new "wpDmgBonus" field that tracks an independent damage bonus. The field definition should look like the following.
<pre>
<field
  id="wpDmgBonus"
  name="Damage Bonus"
  type="derived">
  </field>
</pre>
Once the field is defined, we can integrate it into the final damage displayed by modifying the Finalize script for the "wpShowDmg" field. We add the script code below to the damage.
<pre>
~append any weapon damage bonus
if (field[wpDmgBonus].value <> 0) then
  @text &= signed(field[wpDmgBonus].value)
  endif
</pre>
</pre>


Line 165: Line 370:
   name="Greatsword"
   name="Greatsword"
   compset="Melee"
   compset="Melee"
   description="Description goes here">
   description="">
  <fieldval field="wpDamage" value="Str+4"/>
  <fieldval field="wpStrReq" value="5"/>
   <fieldval field="wpParry" value="-1"/>
   <fieldval field="wpParry" value="-1"/>
   <fieldval field="grCost" value="400"/>
   <fieldval field="grCost" value="400"/>
   <fieldval field="grWeight" value="12"/>
   <fieldval field="gearWeight" value="12"/>
   <usesource id="TimeMedi"/>
   <usesource source="TimeMedi"/>
   <usesource id="TimePowder"/>
   <usesource source="TimePowder"/>
  <tag group="WeaponDie" tag="5"/>
   <tag group="WeaponType" tag="MedBlades"/>
   <tag group="WeaponType" tag="MedBlades"/>
   <tag group="Equipment" tag="TwoHand"/>
   <tag group="Equipment" tag="TwoHand"/>
Line 181: Line 385:
   name="Spear"
   name="Spear"
   compset="Melee"
   compset="Melee"
   description="Description goes here">
   description="">
  <fieldval field="wpDamage" value="Str+2"/>
  <fieldval field="wpStrReq" value="3"/>
   <fieldval field="wpParry" value="1"/>
   <fieldval field="wpParry" value="1"/>
   <fieldval field="wpReach" value="1"/>
   <fieldval field="wpReach" value="1"/>
   <fieldval field="grCost" value="250"/>
   <fieldval field="grCost" value="100"/>
   <fieldval field="grWeight" value="5"/>
   <fieldval field="gearWeight" value="5"/>
   <usesource id="TimeMedi"/>
   <usesource source="TimeMedi"/>
   <usesource id="TimePowder"/>
   <usesource source="TimePowder"/>
  <tag group="WeaponDie" tag="3"/>
   <tag group="WeaponType" tag="MedPoles"/>
   <tag group="WeaponType" tag="MedPoles"/>
   <tag group="Equipment" tag="TwoHand"/>
   <tag group="Equipment" tag="TwoHand"/>
Line 198: Line 401:
   name="Survival Knife"
   name="Survival Knife"
   compset="Melee"
   compset="Melee"
   description="Description goes here">
   description="">
  <fieldval field="wpDamage" value="Str+1"/>
   <fieldval field="wpSpecial" value="Contains supplies that add +1 to Survival rolls"/>
   <fieldval field="wpSpecial" value="Contains supplies that add +1 to Survival rolls"/>
   <fieldval field="grCost" value="50"/>
   <fieldval field="grCost" value="50"/>
   <fieldval field="grWeight" value="3"/>
   <fieldval field="gearWeight" value="3"/>
   <usesource id="TimeModern"/>
   <usesource source="TimeModern"/>
  <tag group="WeaponDie" tag="2"/>
   <tag group="WeaponType" tag="ModMelee"/>
   <tag group="WeaponType" tag="ModMelee"/>
   <eval value="1" phase="PreTraits" priority="5000">
   <eval index="1" phase="PreTraits" priority="5000">
     <before name="Calc trtFinal"/><![CDATA[
     <before name="Calc trtFinal"/><![CDATA[
     #traitroll[skSurvival] += 1
     perform #traitroll[skSurvival,+,1,"Survival Knife"]
     ]]></eval>
     ]]></eval>
   </thing>
   </thing>
</pre>


While we're adding all of the hand weapons, we also need to revamp the provided "Unarmed Strike" weapon so that it behaves as a Savage Worlds weapon. The revised version should look similar to the following.
While we're adding all of the hand weapons, we also need to revamp the provided "Unarmed Strike" weapon so that it behaves properly as a Savage Worlds weapon. The revised version should look similar to the following.


<pre>
<pre>
Line 219: Line 421:
   name="Unarmed Strike"
   name="Unarmed Strike"
   compset="Melee"
   compset="Melee"
   description="Description goes here"
   description=""
   isunique="yes"
   isunique="yes"
   holdable="no">
   holdable="no">
Line 225: Line 427:
   <fieldval field="wpSpecial" value=""/>
   <fieldval field="wpSpecial" value=""/>
   <fieldval field="grCost" value="0"/>
   <fieldval field="grCost" value="0"/>
   <fieldval field="grWeight" value="0"/>
   <fieldval field="gearWeight" value="0"/>
   <tag group="Equipment" tag="Natural"/>
   <tag group="Equipment" tag="Natural"/>
   </thing>  
   </thing>
</pre>
</pre>


===Revise Table for Hand Weapons===
===Revise Table for Hand Weapons===


All of the weapons are in place, so we can now revise how the weapons are managed visually within the table. The entirety of the "Amory" tab is defined within the file "tab_armory.dat", so that's where we'll be making our next changes. The table portal for hand weapons is named "arMelee". 
All of the hand weapons are in place, so we can now revise how the weapons are managed visually within the table. The entirety of the "Amory" tab is defined within the file "tab_armory.dat", so that's where we'll be making our next changes. The table portal for hand weapons is named "arMelee". 


We'll start with the same two changes we made to the table for showing basic gear. First, the width of the area for showing weapons descriptions is too narrow, so add the "descwidth" attribute to the "table_dynamic" element with a value of either 275 or 300. Second, we need to sort the weapons using the sort set we just defined for weapons. So add the "choosesortset" attribute to the same "table_dynamic" element and assign it the unique id of our new sort set: "Weapon".
We'll start with the same two changes we made to the table for showing basic gear. First, the width of the area for showing weapons descriptions is too narrow, so add the "descwidth" attribute to the "table_dynamic" element with a value of either 275 or 300. Second, we need to sort the weapons using the sort set we just defined for weapons. So add the "choosesortset" attribute to the same "table_dynamic" element and assign it the unique id of our new sort set: "Weapon".
Line 249: Line 451:
     choosetemplate="arWpnThing"
     choosetemplate="arWpnThing"
     choosesortset="Weapon"
     choosesortset="Weapon"
    addspace="2"
     buytemplate="BuyCash"
     buytemplate="BuyCash"
     selltemplate="SellCash"
     selltemplate="SellCash"
     descwidth="275">
     descwidth="275">
     <list>component.WeapMelee</list>
     <list>component.WeapMelee</list>
     <candidate>component.WeapMelee &amp; !Equipment.Natural</candidate>
     <candidate inheritlist="yes">!Equipment.Natural</candidate>
     <description><![CDATA[
     <titlebar><![CDATA[
      var descript as string
       @text = "Select Hand Weapons to Purchase from the List Below"
      call Descript
       ]]></titlebar>
       @text = descript
       ]]></description>
     <headertitle><![CDATA[
     <headertitle><![CDATA[
       @text = "Hand Weapons"
       @text = "Hand Weapons"
Line 274: Line 473:
Showing the AP rating within the template entails a number of additions and revisions. It all centers upon the template, which is named "arWpnPick" and will be found in the file "tab_armory.dat". Before we proceed, note that this one template is used for both hand weapons and ranged weapons, so our additions will appear for both weapon types unless we takes steps to only show it for one type. Since the AP rating is highly applicable for both weapon types, we'll just add it so that it's visible for all weapons.
Showing the AP rating within the template entails a number of additions and revisions. It all centers upon the template, which is named "arWpnPick" and will be found in the file "tab_armory.dat". Before we proceed, note that this one template is used for both hand weapons and ranged weapons, so our additions will appear for both weapon types unless we takes steps to only show it for one type. Since the AP rating is highly applicable for both weapon types, we'll just add it so that it's visible for all weapons.


The first thing we need to do is add a new portal to the template. Since we want the new portal to appear between the damage rating and any range information for the weapon, we'll position the new portal between those two portals within the template. If we use a field-based label portal, we'll only be able to display the raw value, which won't be very clear to the user. So we'll use a script-based label portal instead, allowing us to include a useful prefix and display the AP rating in the form "AP2". This means our new field should look similar to the one below.
The first thing we need to do is add a new portal to the template. Since we want the new portal to appear between the damage rating and any range information for the weapon, we'll position the new portal between those two portals within the template. If we use a field-based label portal, we'll only be able to display the raw value, which won't be very clear to the user. So we'll use a script-based label portal instead, allowing us to include a useful prefix and display the AP rating in the form "AP2". This means our new portal should look similar to the one below.


<pre>
<pre>
Line 297: Line 496:
~set up our height based on our tallest portal
~set up our height based on our tallest portal
height = portal[info].height
height = portal[info].height
~if this is a "sizing" calculation, we're done
~if this is a "sizing" calculation, we're done
if (issizing <> 0) then
if (issizing <> 0) then
   done
   done
   endif
   endif
~center the portals vertically
~center the portals vertically
perform portal[info].centervert
perform portal[info].centervert
Line 313: Line 514:
perform portal[strreq].centervert
perform portal[strreq].centervert
perform portal[heldby].centervert
perform portal[heldby].centervert
~position the delete portal on the far right
~position the delete portal on the far right
perform portal[delete].alignedge[right,0]
perform portal[delete].alignedge[right,0]
~position the info portal to the left of the delete button
~position the info portal to the left of the delete button
perform portal[info].alignrel[rtol,delete,-6]
perform portal[info].alignrel[rtol,delete,-6]
position the gear portal to the left of the info button
position the gear portal to the left of the info button
perform portal[gearmanage].alignrel[rtol,info,-6]
perform portal[gearmanage].alignrel[rtol,info,-6]
~position the special portal to the left of the gear button
~position the special portal to the left of the gear button
perform portal[special].alignrel[rtol,gearmanage,-6]
perform portal[special].alignrel[rtol,gearmanage,-6]
~position the range portal to the left of the delete button; we want the
~position the range portal to the left of the delete button; we want the
~damage to be centered in its own column relative to a centerpoint position
~damage to be centered in its own column relative to a centerpoint position
perform portal[range].centerpoint[horz,335]
perform portal[range].centerpoint[horz,340]
 
~position the AP portal to the left of the range column; we want the AP to
~position the AP portal to the left of the range column; we want the AP to
~be centered in its own column relative to a centerpoint position
~be centered in its own column relative to a centerpoint position
perform portal[piercing].centerpoint[horz,280]
perform portal[piercing].centerpoint[horz,290]
 
~position the damage portal to the left of the AP column; we want the damage
~position the damage portal to the left of the AP column; we want the damage
~to be centered in its own column relative to a centerpoint position
~to be centered in its own column relative to a centerpoint position
perform portal[damage].centerpoint[horz,240]
perform portal[damage].centerpoint[horz,240]
~position the attack portal to the left of damage column; we want the
~position the attack portal to the left of damage column; we want the
~attack to be centered in its own column relative to a centerpoint position
~attack to be centered in its own column relative to a centerpoint position
perform portal[attack].centerpoint[horz,195]
perform portal[attack].centerpoint[horz,190]
 
~position the name on the left and let it use all available space
~position the name on the left and let it use all available space
var limit as number
var limit as number
Line 338: Line 547:
portal[name].left = 0
portal[name].left = 0
portal[name].width = minimum(portal[name].width,limit)
portal[name].width = minimum(portal[name].width,limit)
~show the 'strength requirement' icon to the right of the name
~show the 'strength requirement' icon to the right of the name
perform portal[strreq].alignrel[ltor,name,2]
perform portal[strreq].alignrel[ltor,name,2]
portal[strreq].visible = tagis[Helper.BadStrReq]
portal[strreq].visible = tagis[Helper.BadStrReq]
~show the 'held by' icon to the right of the strenght requirement if appropriate
~show the 'held by' icon to the right of the strenght requirement if appropriate
if (portal[strreq].visible = 0) then
if (portal[strreq].visible = 0) then
Line 348: Line 559:
   endif
   endif
portal[heldby].visible = isgearheld
portal[heldby].visible = isgearheld
~if this is not a ranged weapon, hide the range portal
~if this is not a ranged weapon, hide the range portal
if (tagis[component.WeapRange] = 0) then
if (tagis[component.WeapRange] = 0) then
   portal[range].visible = 0
   portal[range].visible = 0
   endif
   endif
~only show the special portal if there are special abilities/notes to view
~only show the special portal if there are special abilities/notes to view
portal[special].visible = 1 - field[wpNotes].isempty  
portal[special].visible = 1 - field[wpNotes].isempty  
</pre>
===Ranged Weapons===
Ranged weapons introduce four new variable ratings that need to be tracked, as well as a handful of special attributes that are either present or not. The variable  ratings corresponding to the following characteristics: rate of fire, number of shots, number of actions to reload, and ammunition type. They can be managed via new fields that we'll add to the "WeapRange" component, which is defined in the file "equipment.pri". They should look similar to the field definitions provided below.
<pre>
<field
  id="wpReload"
  name="Actions to Reload"
  type="static">
  </field>
<field
  id="wpFireRate"
  name="Rate of Fire"
  type="static"
  defvalue="1"
  maxlength="10">
  </field>
<field
  id="wpShots"
  name="Number of Shots"
  type="static">
  </field>
<field
  id="wpAmmo"
  name="Ammunition"
  type="static"
  maxlength="10">
  </field>
</pre>
The special attributes represent facets of weapons that are optionally present, such as snap fire, double tap, three-round burst, etc. All of these are either present or not, so they can be best represented as individual tags. The Skeleton files provide a tag group intended just for this purpose, with the id "Weapon". So we can add each of the tags we need to this tag group in the file "tags.1st". Whichever tags apply to a given weapon can simply be assigned to the thing. The revised tag group should look similar to the example shown below.
<pre>
<group
  id="Weapon">
  <value id="SpecRange" name="Range is special"/>
  <value id="SnapFire" name="Snap Fire"/>
  <value id="DoubleTap" name="Double Tap"/>
  <value id="HvyWeapon" name="Heavy Weapon"/>
  <value id="HighExplos" name="High Explosive"/>
  <value id="ThreeRound" name="Three-Round Burst"/>
  <value id="NoMove" name="May Not Move"/>
  <value id="Revolver"/>
  <value id="SemiAuto" name="Semi-Auto"/>
  <value id="FullAuto" name="Full-Auto"/>
  <value id="Shotgun"/>
  </group>
</pre>
Just like we did for hand weapons, the new fields and attributes need to be included within the weapon description details. The description for ranged weapons is handled by the "InfoRange" procedure within the file "procedures.dat". We only add the new information when it applies. This yields code that should look similar to the following.
<pre>
<procedure id="InfoRange" context="info"><![CDATA[
  ~declare variables that are used to communicate with our caller
  var iteminfo as string
  ~start with generic details for all weapons
  call InfoWeapon
  ~add the range details for the weapon
  iteminfo &= "Range: " & field[wpRange].text & "\n"
  ~report the fire rate of the weapon (if any)
  iteminfo &= "Rate of Fire: " & field[wpFireRate].text & "\n"
  ~report the number of shots for the weapon (if any)
  if (field[wpShots].value > 0) then
    iteminfo &= "# Shots: " & field[wpShots].value & "\n"
    endif
  ~report the number of actions to reload the weapon (if any)
  if (field[wpReload].value > 0) then
    iteminfo &= "Actions to Reload: " & field[wpReload].value & "\n"
    endif
  ~report the ammunition for the weapon (if any)
  if (field[wpAmmo].isempty = 0) then
    iteminfo &= "Ammunition: " & field[wpAmmo].text & "\n"
    endif
  ]]></procedure>
</pre>
We also need to add the new fields and attribute to the special notes shown for weapons on the Armory tab. This can be done via the Eval script within the "WeapRange" component that performs this function. The exception is the ammunition type, which doesn't belong here and can be accessed via the description text if the user wants to reference that info. The code parallels what we've already done for hand weapons, which results in a revised script that look like the following.
<pre>
<eval value="2" phase="Render" priority="2000"><![CDATA[
  var special as string
  ~report the rate of fire for the weapon
  if (empty(special) = 0) then
    special &= ", "
    endif
  special &= "RoF " & field[wpFireRate].text
~report the number of shots for the weapon (if any)
  if (field[wpShots].value <> 0) then
    if (empty(special) = 0) then
      special &= ", "
      endif
    special &= "Shots " & field[wpShots].value
    endif
  ~report the reload actions of the weapon (if any)
  if (field[wpReload].value <> 0) then
    if (empty(special) = 0) then
      special &= ", "
      endif
    special &= "Reload " & field[wpReload].value & " action(s)"
    endif
  ~prepend any existing special details with the notes for this weapon
  if (empty(special) = 0) then
    if (field[wpNotes].isempty = 0) then
      special &= ", "
      endif
    field[wpNotes].text = special & field[wpNotes].text
    endif
  ]]></eval>
</pre>
===Add Ranged Weapons===
We can now begin adding all of the various ranged weapons to the data files. As with hand weapons, they should be added to the file "thing_armory.dat". A few examples are provided below. You can either add the rest or pull them out of the complete Savage Worlds data files that are provided.
<pre>
<thing
  id="wpCrossbow"
  name="Crossbow"
  compset="Ranged"
  description="Description goes here">
  <fieldval field="wpDamage" value="2d6"/>
  <fieldval field="wpShort" value="15"/>
  <fieldval field="wpMedium" value="30"/>
  <fieldval field="wpLong" value="60"/>
  <fieldval field="wpStrReq" value="3"/>
  <fieldval field="wpPiercing" value="2"/>
  <fieldval field="wpReload" value="1"/>
  <fieldval field="grCost" value="500"/>
  <fieldval field="grWeight" value="10"/>
  <usesource id="TimeMedi"/>
  <tag group="WeaponType" tag="MedRange"/>
  <tag group="Equipment" tag="TwoHand"/>
  </thing>
<thing
  id="wpColt1911"
  name="Colt 1911"
  compset="Ranged"
  description="Description goes here">
  <fieldval field="wpDamage" value="2d6+1"/>
  <fieldval field="wpShort" value="12"/>
  <fieldval field="wpMedium" value="24"/>
  <fieldval field="wpLong" value="48"/>
  <fieldval field="wpPiercing" value="1"/>
  <fieldval field="wpShots" value="7"/>
  <fieldval field="wpAmmo" value=".45"/>
  <fieldval field="grCost" value="200"/>
  <fieldval field="grWeight" value="4"/>
  <usesource id="TimeModern"/>
  <tag group="WeaponType" tag="ModPistol"/>
  <tag group="Weapon" tag="DoubleTap"/>
  <tag group="Weapon" tag="SemiAuto"/>
  </thing>
<thing
  id="wpBarrett"
  name="Barrett"
  compset="Ranged"
  description="Description goes here">
  <fieldval field="wpDamage" value="2d10"/>
  <fieldval field="wpShort" value="50"/>
  <fieldval field="wpMedium" value="100"/>
  <fieldval field="wpLong" value="200"/>
  <fieldval field="wpStrReq" value="4"/>
  <fieldval field="wpPiercing" value="4"/>
  <fieldval field="wpShots" value="11"/>
  <fieldval field="wpAmmo" value=".50"/>
  <fieldval field="grCost" value="750"/>
  <fieldval field="grWeight" value="35"/>
  <usesource id="TimeModern"/>
  <tag group="WeaponType" tag="ModRifle"/>
  <tag group="Weapon" tag="SnapFire"/>
  <tag group="Weapon" tag="HvyWeapon"/>
  <tag group="Equipment" tag="TwoHand"/>
  </thing>
</pre>
===Revise Table for Ranged Weapons===
With all of the ranged weapons in place, we can now revise how they are managed visually within the table. The table portal for ranged weapons is named "arRange" within the file "tab_armory.dat". After a quick review, there are only two things that we need to change, and those are the same changes we already made to the "arMelee" table portal above. We first need to increase the width of the area for showing weapon descriptions, so we add the "descwidth" attribute to the "table_dynamic" element with a value of either 275 or 300. Then we add the "choosesortset" attribute to the same "table_dynamic" element and assign it the unique id of our custom sort set, which is "Weapon".
===Weapon Attack Calculations===
Early in the overall development process for these data files, we determined that Savage Worlds calculated the attack for weapons very differently from the default logic provided by the Skeleton files, so we disabled it. Now that we have all the various weapons in place, it's a perfect time to re-visit that and get it working correctly. The "wpNetAtk" field within the "BaseWeapon" component is provided for exactly this purpose. However, that field is value-based and it needs to be text-based so that it can display something like "d8+2". This means our first task is to change the "wpNetAtk" field to be text-based, which we can do by assigning it a "maxlength" attribute of "50". The revised field should look similar to the following.
<pre>
<field
  id="wpNetAtk"
  name="Net Attack"
  type="derived"
  maxlength="50">
  </field>
</pre>
Once we change the field's nature, we are going to get a variety of compiler errors, since there are a handful of places where the field is accessed as a value-based field. Try compiling the file and use the list of errors to locate the handful of locations where the field usage needs to be adjusted. All you should need to do is change references to "field[wpNetAtk].value" over to "field[wpNetAtk].text".
While we're mucking with fields, we might as well address another need. In order to independently synthesize an appropriately adjusted trait roll for the weapon attacks, those scripts will need to access the net bonus/penalty for the particular trait. Currently, that information is tracked across three separate fields, so we need to introduce a new field that calculates the net adjustment and can be readily accessed. We'll call our new field "trtNetRoll", and it must be generated after any penalties are applied due to the load limit, so we'll base our timing on the script we defined earlier that performs that role. In the end, our new field should look a lot like the definition below.
<pre>
<field
  id="trtNetRoll"
  name="Net Roll Bonus"
  type="derived"
  calcphase="Traits"
  calcprior="9000">
  <calculate name="Calc trtNetRoll">
    <after name="Apply LoadLimit"/><![CDATA[
    @value = field[trtRoll].value + field[trtProf].value + herofield[acNetPenal].value
    ]]></calculate>
  </field>
</pre>
The logic we'll want to use when synthesizing the net attack for display will be very similar to the logic used to generate the final trait rolls for display within the "trtDisplay" field. Consequently, our next task should be to carve out that logic for re-use by putting it into a procedure. Once it's in a procedure, it can be used from both the current script that synthesizes "trtDisplay" and any other script that synthesizes "wpNetAtk". 
Looking closely at the logic, there are two distinct values that must be passed into the procedure, with a since string being returned for use by the caller. The two inbound values are the die type for the trait and any bonus/penalty to the roll. Based on these two inputs, our procedure can interpret the values correctly and synthesize the appropriate string for display to the user. The only thing special we need to do is ensure that the die type is properly bounded, just like is already done within the logic that synthesizes "trtDisplay". In the end, we should end up with a procedure that looks very similar to the one provided below.
<pre>
<procedure id="FinalRoll" scripttype="none"><![CDATA[
  ~declare variables that are used to communicate with our caller
  var finaldie as number
  var finalbonus as number
  var finaltext as string
  ~bound our final die type appropriately
  var final as number
  final = finaldie
  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
  dietype = final * 2
  finaltext = "d" & dietype
  ~if there are any bonuses or penalties on the roll, append those the final result
  if (finalbonus <> 0) then
    finaltext &= signed(finalbonus)
    endif
  ]]></procedure>
</pre>
Once the procedure is in place, we need to revise the Eval script that synthesizes the "trtDisplay" field so that it uses the new procedure. The net result should closely parallel the revised script below.
<pre>
<eval value="4" phase="Render" priority="5000" name="Calc trtDisplay">
  <after name="Calc trtNetRoll"/>
  <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
  ~calculate the net bonuses and penalties for the roll
  var finalbonus as number
  finalbonus = field[trtNetRoll].value
  ~generate the appropriate results for display
  var finaldie as number
  var finaltext as string
  finaldie = field[trtFinal].value
  call FinalRoll
  ~put the final result into the proper field
  field[trtDisplay].text = finaltext
  ]]></eval>
</pre>
We've now got all of the pieces in place to be able to properly synthesize the "wpNetAtk" field for display to the user. There are two separate scripts in the file "equipment.pri" that synthesize the "wpNetAtk" field - one for ranged weapons and another for hand weapons. We need two separate scripts because one is based on the "Shooting" skill, while the other is based on the "Fighting" skill. We'll start by modifying the Eval script within the "WeapRange" component. The timing does not need to be touched, but we otherwise need to replace the script with revised logic that is similar to the synthesis of "trtDisplay" above and uses the "Shooting"  skill. The key differences are two-fold. First, the bonus needs to incorporate both any bonus/penalty from the trait and any separate bonus/penalty from the weapon itself. Second, we need to handle the case where the character does not possess the needed skill. If not, then the die type will be zero due to the use of the "childfound." transition, which the procedure will bound as if it's a "d4". We must also apply the appropriate "-2" adjustment to the roll for being unskilled. The net result should yield a script that looks similar to the one below.
<pre>
<eval value="1" phase="Final" priority="7000" name="Calc wpNetAtk">
  <after name="Calc trtNetRoll"/>
  <after name="Calc trtFinal"/><![CDATA[
  ~start with the bonuses and penalties associated with the weapon
  var finalbonus as number
  finalbonus = field[wpBonus].value + field[wpPenalty].value
  ~apply the appropriate adjustment for the associated skill
  if (hero.childexists[skShooting] = 0) then
    finalbonus -= 2
  else
    finalbonus += hero.child[skShooting].field[trtNetRoll].value
    endif
  ~get the proper die type for the associated skill
  var finaldie as number
  finaldie = hero.childfound[skShooting].field[trtFinal].value
  ~generate the appropriate results for display
  var finaltext as string
  call FinalRoll
  ~put the final result into the proper field
  field[wpNetAtk].text = finaltext
  ]]></eval>
</pre>
We can now define a very similar Eval script for the "WeapMelee" component that synthesizes the net attack based on the "Fighting" skill. The existing script can have the comments removed from around it and can then have its contents replaced with the revised logic shown below.
<pre>
<eval value="2" phase="Final" priority="7000" name="Calc wpNetAtk">
  <after name="Calc trtNetRoll"/>
  <after name="Calc trtFinal"/><![CDATA[
  ~start with the bonuses and penalties associated with the weapon
  var finalbonus as number
  finalbonus = field[wpBonus].value + field[wpPenalty].value
  ~apply the appropriate adjustment for the associated skill
  if (hero.childexists[skFighting] = 0) then
    finalbonus -= 2
  else
    finalbonus += hero.child[skFighting].field[trtNetRoll].value
    endif
  ~get the proper die type for the associated skill
  var finaldie as number
  finaldie = hero.childfound[skFighting].field[trtFinal].value
  ~generate the appropriate results for display
  var finaltext as string
  call FinalRoll
  ~put the final result into the proper field
  field[wpNetAtk].text = finaltext
  ]]></eval>
</pre>
===Ammunition===
The final aspect of weapons that we need to address is ammunition. All ammunition is actually purchased and managed as gear via the Gear tab, but we'll address it as part of the weapons. There really isn't anything to deal with for ammunition, except that we need to add a new tag for the new type of gear. Once that's done, it's just a matter of adding all of the appropriate entries for each type of ammunition. A couple of examples are provided below. You can either add the rest or pull them out of the complete Savage Worlds data files that are provided.
<pre>
<thing
  id="eqBulletSm"
  name="Bullets (Small)"
  compset="Ammunition"
  lotsize="50"
  stacking="merge"
  description="Includes .22 to .32 caliber weapons">
  <fieldval field="grCost" value=".2"/>
  <fieldval field="grWeight" value=".06"/>
  <tag group="GearType" tag="Ammo"/>
  </thing>
<thing
  id="eqLsrBatt"
  name="Laser Battery"
  compset="Ammunition"
  lotsize="1"
  stacking="merge"
  description="Provides one full load of shots for the laser pistol, rifle, or MG">
  <fieldval field="grCost" value="25"/>
  <fieldval field="grWeight" value="1"/>
  <usesource id="TimeFuture"/>
  <tag group="GearType" tag="Ammo"/>
  </thing>
</pre>
</pre>

Latest revision as of 05:51, 18 February 2009

Context: HL Kit &#133; Authoring Examples &#133; Savage Worlds Walk-Through 

Overview

Now that basic gear is in, this section will examine a more complex type of equipment: weapons. We'll continue with the same process we began in the previous chapter, assessing the differences between the Savage Worlds game system and the default mechanisms provided by the Skeleton data files.

Weapons in General

There is only one facet of weapons in general that is distinct from the default behaviors of the Skeleton files. Both hand weapons and ranged weapons can possess the "armor piercing" attribute, with a corresponding rating being specified. Since there is a rating involved, we need to add a new field to the "BaseWeapon" component in order to track the rating. You'll find the component in the file "equipment.str", and the new field should look similar to the one shown below.

<field
  id="wpPiercing
  name="Armor Piercing"
  type="static">
  </field>

Once the new field is added, we then need to include it in the description text for the weapon. All descriptions are routed through the assorted procedures in the file "procedures.dat", so open that file. Locate the "InfoWeapon" procedure, which synthesizes the generic details of all weapons. Insert the necessary lines of script code to include the armor piercing rating within the details. It's probably best to only include the info for weapons that possess a non-zero armor piercing rating, so the new code should look similar to the lines below.

~report the armor piercing rating of the weapon (if any)
if (field[wpPiercing].value > 0) then
  iteminfo &= "Armor Piercing: " & field[wpPiercing].value & "{br}"
  endif

The final thing we need to do is add the armor piercing rating to the special notes that are shown for weapons on the Armory tab. The notes to be displayed are synthesized into the "wpNotes" field for weapons via an Eval script within the "WeaponBase" component. Locate this script and add the appropriate new code to include the armor piercing rating at the start. The new code should look similar to the lines below.

~report the armor piercing rating of the weapon (if any)
if (field[wpPiercing].value > 0) then
  if (empty(special) = 0) then
    special &= ", "
    endif
  special &= "AP " & field[wpPiercing].value
  endif 

Minimum Strength Requirement

Weapons may be specified as having a minimum strength requirement. If a weapon is equipped when that requirement is not meant, using the weapon incurs a penalty of -1 for each step of shortfall. The Skeleton files provide some basic mechanisms for managing the strength requirement, but we must apply the penalty appropriately ourselves. Open the file "equipment.str" and locate the "WeaponBase" component. The first Eval script handles the minimum strength requirement logic, applying a penalty to the weapon and having a place to apply a more general penalty. Within Savage Worlds, there is no generalized penalty, so those lines can be deleted from the script. The penalty adjustment applied is currently a flat "-1", so we must change it to represent the shortfall. The net result is a revised Eval script that looks similar to the following script.

<eval index="1" phase="Final" priority="5000"><![CDATA[
  ~if the minimum strength is satisfied, there's nothing to do
    if (field[wpStrReq].value <= #trait[attrStr]) then
      done
      endif

  ~assign a tag to indicate the requirement isn't met
  perform assign[Helper.BadStrReq]

  ~apply any penalty required with the specific weapon
  if (#trait[attrStr] < field[wpStrReq].value) then
    field[wpPenalty].value = #trait[attrStr] - field[wpStrReq].value
    endif

  ~if not equipped, there's nothing more to do
  if (field[grIsEquip].value = 0) then
    done
    endif
  ]]></eval> 

Weapon Categories

In the same way we encountered for basic gear, weapons in Savage Worlds are grouped into categories or types. If we want our weapons to be displayed to the user in appropriate groupings to match the rulebook, we'll need to manage these groupings. This entails the creation of a new tag group that we'll call "WeaponType" and for which we'll define tags for each grouping. This gets added to the file "tags.1st". We can then assign the appropriate tag to each weapon. We could potentially create separate tag groups for hand weapons and ranged weapons, but there is no real benefit in doing it, so we'll just use a single tag group for all weapons. As we did for basic gear, the tag group needs to be assigned the "explicit" sequencing behavior, and each tag needs to be assigned an appropriate "order" attribute to designate the proper sort order to match the rulebook. The net result is a tag group that looks like the following.

<group
  id="WeaponType"
  sequence="explicit">
  <value id="MedBlades" name="Medieval Blades" order="1"/>
  <value id="MedAxes" name="Medieval Axes and Mauls" order="2"/>
  <value id="MedPoles" name="Medieval Pole Arms" order="3"/>
  <value id="ModMelee" name="Modern Melee" order="4"/>
  <value id="FutMelee" name="Futuristic Melee" order="5"/>
  <value id="MedRange" name="Medieval Ranged" order="10"/>
  <value id="Black" name="Black Powder" order="11"/>
  <value id="ModPistol" name="Modern Pistol" order="12"/>
  <value id="ModSMG" name="Modern Submachine Gun" order="13"/>
  <value id="ModShotgun" name="Modern Shotgun" order="14"/>
  <value id="ModRifle" name="Modern Rifle" order="15"/>
  <value id="ModAssault" name="Modern Assault Rifle" order="16"/>
  <value id="ModMachine" name="Modern Machine Gun" order="17"/>
  <value id="FutRange" name="Futuristic Ranged" order="18"/>
  </group>

With the weapon type available, we need to put it to use. This will be done through a new sort set that parallels what we did for basic gear, and we'll call it "Weapon". The sort set first sorts on the weapon type and then on the name of the thing, which results in the following definition to be added to the file "control.1st".

<sortset
  id="Weapon"
  name="Weapon By Type and Name">
  <sortkey isfield="no" id="WeaponType"/>
  <sortkey isfield="no" id="_Name_"/>
  </sortset> 

Hand Weapons

We'll focus on hand weapons first. The critical issue with hand weapons is the "weapon die". The damage for most hand weapons is based on the wielder's Strength plus a weapon die. The interesting wrinkle is that the weapon die cannot exceed the wielder's Strength die. If a weapon is listed with a damage of "Str+d8" and the wielder's Strength is "d6", the actual damage is downgraded to "Str+d6". In addition, none of the benefits of the weapon (e.g. Parry bonus) are conferred if the weapon die exceeds the Strength die.

To handle this, we're going to need to add a new field to track the weapon die. Each weapon will specify the appropriate weapon die, and then we'll use an Eval script to downgrade it if the wielder's Strength die is less. The actual damage we display can then be synthesized via another script, after everything has been resolved. We'll worry about how (and whether) to confer the weapon benefits in the next section.

There are also a few weapons that don't have a weapon die and do a fixed amount of damage. For example, the Bangstick does 3d6 damage. In this case, we must not adjust the weapon damage. To handle this, we'll need to distinguish a weapon with a weapon die from a weapon without one.

The simplest solution to all of this is to use a tag to indicate the weapon die for a given weapon. If no tag is assigned to the weapon, then the damage is fixed. Otherwise, we'll use the tag to determine the weapon die and adjust it as appropriate based on the Strength.

To handle the weapon die tags, we must define a new tag group in the file "tags.1st". The tag group needs to have separate tags for each of the different die types. We can allow the tag value to be the weapon die value we want to use internally (e.g. "2" for "d4", just like we do for attributes). However, we'll make sure the name of each tag is the proper die type for display. The resulting tag group should look like the following.

<group
  id="WeaponDie"
  name="Weapon Die-Type">
  <value id="2" name="d4"/>
  <value id="3" name="d6"/>
  <value id="4" name="d8"/>
  <value id="5" name="d10"/>
  <value id="6" name="d12"/>
  </group>

We can now add a new field in which we'll track the weapon die. As part of this field, we can define a Calculate script that interprets the tag and sets up the die type appropriately. Since the weapon die only applies to hand weapons, we'll add the handling to the "WeapMelee" component, which is in the file "equipment.str". The resulting field definition looks like below.

<field
  id="wpDie"
  name="Weapon Die"
  type="derived">
  <calculate phase="Initialize" priority="5000"><![CDATA[
    ~setup our die type based on any assigned tag
    @value = tagvalue[WeaponDie.?]
    ]]></calculate>
  </field>

The field is in place, so we can define the Eval script to handle the downgrading. Since we know that the weapon is failing it's strength requirement here, we can assign it a suitable tag to indicate that fact. We have to time script to occur after the Strength is resolved. If a downgrade occurs, we need to check that state before the derived traits like Parry are resolved. This means our script timing must occur right after the Strength is resolved. This yields the following Eval script.

<eval index="4" phase="Traits" priority="4000">
  <after name="Calc trtFinal"/><![CDATA[
  ~if the weapon die is greater than the strength die, downgrade it
  if (field[wpDie].value > #trait[attrStr]) then
    field[wpDie].value = #trait[attrStr]
    perform assign[Helper.BadStrReq]
    endif
  ]]></eval>

The final piece we need to deal with is displaying the damage for a weapon. When we display a thing to the user, no Calculate or Eval scripts are evaluated. That means we have two choices for displaying weapons based on their die type. The first option is to duplicate the damage within the "wpDamage" field. That's going to be error prone, so it's a poor choice. The alternative is to use a Finalize script, since a Finalize script is always evaluated for a thing. Unfortunately, Finalize scripts can't be used on text-based fields, so we can't define one for the "wpDamage" field.

The solution is to define a new value-based field and put a Finalize script on it. Within the Finalize script, we need to handle the display of both things and picks. For a thing, the Calculate script on the "wpDie" field has not been performed, so we need to pull the value from the tag. Otherwise, we can pull the value from the (possibly downgraded) field.

Since we'll want to always use this field for showing damage to the user, it needs to exist for all weapons. That means it needs to be defined on the "WeaponBase" component. If we have a ranged weapon that doesn't include the "wpDie" field, we must be careful not to try accessing the non-existent field. We can safeguard against this by checking for the "component.WeapMelee" tag before accessing the weapon die logic.

The role of our new field will be solely for display of the final damage for a weapon, so we'll name it accordingly. Putting all this together results in the new field definition shown below.

<field
  id="wpShowDmg"
  name="Final Damage for Display"
  type="derived"
  maxfinal="20">
  <!-- If we have a weapon die, synthesize for display, else use wpDamage -->
  <finalize><![CDATA[
    ~use the weapon die to display the damage, else use the explicit damage
    if (tagis[component.WeapMelee] + tagis[WeaponDie.?] < 2) then
      @text = field[wpDamage].text
    else
      var die as number
      if (@ispick = 0) then
        die = tagvalue[WeaponDie.?] * 2
      else
        die = field[wpDie].value * 2
        endif
      @text = "Str+d" & die
      endif
    ]]></finalize>
  </field>

The handling of the weapon die is now in place. However, we'll need to remember to always use the "wpShowDmg" field for displaying weapon damage. The Skeleton files assume the "wpDamage" field is used, so we need to go through everywhere and change the reference where appropriate. The list is lengthy, so we won't go through each here. You can do a search for "wpDamage" within all the data files and replace it will "wpShowDmg" anywhere that it is being used for display or output of some sort. In general, the only exception would be the definitions of weapons themselves.

Parry and Reach

There are two remaining aspects of hand weapons that differ from what's provided by the Skeleton files. The first is the parry adjustment, and the second is the weapon's reach. As with how we handled armor piercing, both of these aspects are numeric ratings, so we'll add new fields to track both of them. Since these aspects only apply to hand weapons, we'll add them to the "WeapMelee" component. The new fields should look similar to the ones shown below.

<field
  id="wpParry"
  name="Parry Adjustment"
  type="static">
  </field>

<field
  id="wpReach"
  name="Reach Distance"
  type="static">
  </field> 

With the tracking of the Parry adjustment, we need to apply that adjustment appropriately when the weapon is equipped. That entails defining a new Eval script for the component. All it needs to do is apply the proper adjustment to Parry prior to when the final Parry value is calculated. However, there is an important wrinkle. If the weapon die requirement is not satisfied, the Parry bonus is not applied. This means we also have to schedule our script after we downgrade the weapon die. Fortunately, there is a small window in which we can achieve this, resulting in the script below.

<eval index="5" phase="Traits" priority="5000">
  <before name="Derived trtFinal"/>
  <after name="Downgrade Weapon Die"/><![CDATA[
  if (field[grIsEquip].value <> 0) then
    ~negative adjustments are always applied
    if (field[wpParry].value < 0) then
      perform #traitadjust[trParry,+,field[wpParry].value,"Equipped Weapon"]
    ~otherwise, adjustments are only applied when the strength is satisfied
    elseif (tagis[Helper.BadStrReq] = 0) then
      perform #traitadjust[trParry,+,field[wpParry].value,"Equipped Weapon"]
      endif
    endif

Again as we did for armor piercing, we need to add these ratings to the weapon description details. This can be done within the "InfoMelee" procedure within the file "procedures.dat". Since the rulebook omits these ratings unless they are non-zero, we'll do the same within the description text. It would be ideal if we can indicate to the user when these abilities don't apply for a weapon. We could accomplish that by highlighting the adjustments in red when the weapon does not satisfy the strength requirements. This yields code that should look similar to the following.

~report the parry adjustment of the weapon (if any)
if (field[wpParry].value <> 0) then
  iteminfo &= "Parry Adjustment: " & signed(field[wpParry].value) & "{br}"
  endif 

~report the reach of the weapon (if any)
if (field[wpReach].value > 0) then
  iteminfo &= "Reach: " & field[wpReach].value & "{br}"
  endif

~if the weapon fails its strength requirement, change color and style
if (empty(special) = 0) then
  if (tagis[Helper.BadStrReq] <> 0) then
    special = "{text a00000}{i}" & special & "{/i}{text 010101}"
    endif
  endif

Our last task is to add these two ratings to the special notes shown for weapons on the Armory tab. This can be done via the Eval script within the "WeapMelee" component that performs this function. Since the logic is basically the same, we can use the code from adding the armor piercing as a template. As we did above, we'll highlight the abilities if they aren't available due to insufficient Strength. The new lines of code should look like below.

~if the weapon fails its strength requirement, change color and style
if (tagis[Helper.BadStrReq] <> 0) then
  iteminfo &= "{text a00000}{i}"
  endif

~report the parry adjustment of the weapon (if any)
if (field[wpParry].value <> 0) then
  if (empty(special) = 0) then
    special &= ", "
    endif
  special &= "Parry " & signed(field[wpParry].value)
  endif

~report the reach rating of the weapon (if any)
if (field[wpReach].value <> 0) then
  if (empty(special) = 0) then
    special &= ", "
    endif
  special &= "Reach " & field[wpReach].value
  endif 

~if the weapon fails its strength requirement, restore color and style
if (tagis[Helper.BadStrReq] <> 0) then
  iteminfo &= "{/i}{text 010101}"
  endif

Revised Strength Requirements

There's an important detail that we overlooked in getting the weapon die working. The way we handled things changes the way that minimum strength requirements are detected for hand weapons. As we saw a few moments ago, the Skeleton files provide basic handling that keys on the "wpStrReq" field value. The will continue to work for ranged weapons, but it no longer works for hand weapons, so we need to revise those mechanisms.

There are two separate instances on the "WeaponBase" component that deal with strength requirements. One is an Eval script and the other is a pre-requisite. Since the method is changing for hand weapons only, we have two options. We can move these existing scripts to the "WeapRange" component and then create new versions within the "WeapMelee" component. Alternately, we can adapt these scripts to handle both cases. The logic change is rather simple, so we'll choose the latter option.

Within the pre-requisite test, the only change we need to make is how we determine the minimum strength to be tested. For hand weapons, we use the weapon die if it's available. Otherwise, we use the the field value. We'll use the same test from earlier to determine which method to employ. The revised Validate script is shown below.

<validate><![CDATA[
  ~if this is a pick, we're valid
  ~NOTE! We assume that equipping an item without the strength just applies penalties.
  if (@ispick <> 0) then
    @valid = 1
    done
    endif

  ~get the minimum strength required for the weapon
  var minstr as number
  if (altthing.tagis[component.WeapMelee] + altthing.tagis[WeaponDie.?] < 2) then
    minstr = altthing.field[wpStrReq].value
  else
    minstr = altthing.tagvalue[WeaponDie.?]
    endif

  ~verify whether we meet the strength requirement
  if (#trait[attrStr] >= minstr) then
    @valid = 1
    endif
  ]]></validate>

The same logic needs to be applied to the Eval script. The only other difference is that we only want to assign the "Helper.BadStrReq" tag if it's not already present. We assign that tag when we downgrade the weapon die so that we can detect it for disabled abilities like Parry. Assigning it again is rather silly (albeit harmless). The resulting Eval script should look like the one below.

<eval index="1" phase="Final" priority="5000"><![CDATA[
  ~get the minimum strength required for the weapon
  var minstr as number
  if (tagis[component.WeapMelee] + tagis[WeaponDie.?] < 2) then
    minstr = field[wpStrReq].value
  else
    minstr = tagvalue[WeaponDie.?]
    endif

  ~if the minimum strength is satisfied, there's nothing to do
  if (minstr <= #trait[attrStr]) then
    done
    endif

  ~assign a tag to indicate the requirement isn't met
  ~Note: This tag may already be assigned, so don't duplicte it.
  if (tagis[Helper.BadStrReq] = 0) then
    perform assign[Helper.BadStrReq]
    endif

  ~apply any penalty required with the specific weapon
  field[wpPenalty].value = #trait[attrStr] - field[wpStrReq].value

  ~if not equipped, there's nothing more to do
  if (field[grIsEquip].value = 0) then
    done
    endif
  ]]></eval>

At this point, our strength requirements logic should be operating properly.

Damage Bonus

There's one thing that we have forgotten to handle for hand weapons. The "Katana" has a damage of "Str+d6+2". If the "d6" indicates the weapon die, then the extra "+2" is not able to be accommodated in any way. The "wpBonus" field represents an attack bonus - not damage - so we need something else.

To solve this, we can add a new "wpDmgBonus" field that tracks an independent damage bonus. The field definition should look like the following.

<field
  id="wpDmgBonus"
  name="Damage Bonus"
  type="derived">
  </field>

Once the field is defined, we can integrate it into the final damage displayed by modifying the Finalize script for the "wpShowDmg" field. We add the script code below to the damage.

~append any weapon damage bonus
if (field[wpDmgBonus].value <> 0) then
  @text &= signed(field[wpDmgBonus].value)
  endif

Add Hand Weapons

Everything is now in place for us to start adding all of the various hand weapons to the data files. All weapons should be added to the file "thing_armory.dat". A few examples are provided below. You can either add the rest or pull them out of the complete Savage Worlds data files that are provided.

<thing
  id="wpGreatswd"
  name="Greatsword"
  compset="Melee"
  description="">
  <fieldval field="wpParry" value="-1"/>
  <fieldval field="grCost" value="400"/>
  <fieldval field="gearWeight" value="12"/>
  <usesource source="TimeMedi"/>
  <usesource source="TimePowder"/>
  <tag group="WeaponDie" tag="5"/>
  <tag group="WeaponType" tag="MedBlades"/>
  <tag group="Equipment" tag="TwoHand"/>
  </thing>

<thing
  id="wpSpear"
  name="Spear"
  compset="Melee"
  description="">
  <fieldval field="wpParry" value="1"/>
  <fieldval field="wpReach" value="1"/>
  <fieldval field="grCost" value="100"/>
  <fieldval field="gearWeight" value="5"/>
  <usesource source="TimeMedi"/>
  <usesource source="TimePowder"/>
  <tag group="WeaponDie" tag="3"/>
  <tag group="WeaponType" tag="MedPoles"/>
  <tag group="Equipment" tag="TwoHand"/>
  </thing>

<thing
  id="wpSurvKnif"
  name="Survival Knife"
  compset="Melee"
  description="">
  <fieldval field="wpSpecial" value="Contains supplies that add +1 to Survival rolls"/>
  <fieldval field="grCost" value="50"/>
  <fieldval field="gearWeight" value="3"/>
  <usesource source="TimeModern"/>
  <tag group="WeaponDie" tag="2"/>
  <tag group="WeaponType" tag="ModMelee"/>
  <eval index="1" phase="PreTraits" priority="5000">
    <before name="Calc trtFinal"/><![CDATA[
    perform #traitroll[skSurvival,+,1,"Survival Knife"]
    ]]></eval>
  </thing>

While we're adding all of the hand weapons, we also need to revamp the provided "Unarmed Strike" weapon so that it behaves properly as a Savage Worlds weapon. The revised version should look similar to the following.

<pre>
<thing
  id="wpUnarmed"
  name="Unarmed Strike"
  compset="Melee"
  description=""
  isunique="yes"
  holdable="no">
  <fieldval field="wpDamage" value="Str"/>
  <fieldval field="wpSpecial" value=""/>
  <fieldval field="grCost" value="0"/>
  <fieldval field="gearWeight" value="0"/>
  <tag group="Equipment" tag="Natural"/>
  </thing>

Revise Table for Hand Weapons

All of the hand weapons are in place, so we can now revise how the weapons are managed visually within the table. The entirety of the "Amory" tab is defined within the file "tab_armory.dat", so that's where we'll be making our next changes. The table portal for hand weapons is named "arMelee". 

We'll start with the same two changes we made to the table for showing basic gear. First, the width of the area for showing weapons descriptions is too narrow, so add the "descwidth" attribute to the "table_dynamic" element with a value of either 275 or 300. Second, we need to sort the weapons using the sort set we just defined for weapons. So add the "choosesortset" attribute to the same "table_dynamic" element and assign it the unique id of our new sort set: "Weapon".

The next thing we need to adjust is that the Skeleton files we inherited refer to "Melee Weapons", while Savage Worlds uses the term "Hand Weapons". Internally, this distinction is not important, but we want everything to look familiar to users. So we need to modify the scripts within the "headertitle" and "additem" elements to change the terms used.

After making the above changes, we should end up with a revised "arMelee" portal that looks similar to the one below.

<portal
  id="arMelee"
  style="tblNormal">
  <table_dynamic
    component="Gear"
    showtemplate="arWpnPick"
    choosetemplate="arWpnThing"
    choosesortset="Weapon"
    buytemplate="BuyCash"
    selltemplate="SellCash"
    descwidth="275">
    <list>component.WeapMelee</list>
    <candidate inheritlist="yes">!Equipment.Natural</candidate>
    <titlebar><![CDATA[
      @text = "Select Hand Weapons to Purchase from the List Below"
      ]]></titlebar>
    <headertitle><![CDATA[
      @text = "Hand Weapons"
      ]]></headertitle>
    <additem><![CDATA[
      @text = "Add New Hand Weapons"
      ]]></additem>
    </table_dynamic>
  </portal>

We should now ask ourselves if there are any of the new fields that we'd like to see shown for weapons that have been added to the character. The two solid candidates are the Parry adjustment and the AP rating for the weapon. Whenever a weapon is equipped, any Parry adjustment is automatically applied, so it's not something that the user can forget to add, which means it's really not important to remind the user about it. The AP rating, though, it quite different. It's very important that the user not forget about an AP rating for a wielded weapon, so we really should make a point of showing it. 

Showing the AP rating within the template entails a number of additions and revisions. It all centers upon the template, which is named "arWpnPick" and will be found in the file "tab_armory.dat". Before we proceed, note that this one template is used for both hand weapons and ranged weapons, so our additions will appear for both weapon types unless we takes steps to only show it for one type. Since the AP rating is highly applicable for both weapon types, we'll just add it so that it's visible for all weapons.

The first thing we need to do is add a new portal to the template. Since we want the new portal to appear between the damage rating and any range information for the weapon, we'll position the new portal between those two portals within the template. If we use a field-based label portal, we'll only be able to display the raw value, which won't be very clear to the user. So we'll use a script-based label portal instead, allowing us to include a useful prefix and display the AP rating in the form "AP2". This means our new portal should look similar to the one below.

<portal
  id="piercing"
  style="lblDisable">
  <label>
    <labeltext><![CDATA[
      if (field[wpPiercing].value > 0) then
        @text = "AP" & field[wpPiercing].value
      else
        @text = ""
        endif
      ]]></labeltext>
    </label>
  </portal>

Once the portal has been added, we now need to position it properly. In order to make room for it, we'll also need to nudge around some of the other portals. After a little bit of experimenting, a suitable result can be achieved, yielding the following new Position script.

~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

~center the portals vertically
perform portal[info].centervert
perform portal[name].centervert
perform portal[attack].centervert
perform portal[damage].centervert
perform portal[piercing].centervert
perform portal[range].centervert
perform portal[special].centervert
perform portal[gearmanage].centervert
perform portal[delete].centervert
perform portal[strreq].centervert
perform portal[heldby].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,-6]
position the gear portal to the left of the info button
perform portal[gearmanage].alignrel[rtol,info,-6]

~position the special portal to the left of the gear button
perform portal[special].alignrel[rtol,gearmanage,-6]

~position the range portal to the left of the delete button; we want the
~damage to be centered in its own column relative to a centerpoint position
perform portal[range].centerpoint[horz,340]

~position the AP portal to the left of the range column; we want the AP to
~be centered in its own column relative to a centerpoint position
perform portal[piercing].centerpoint[horz,290]

~position the damage portal to the left of the AP column; we want the damage
~to be centered in its own column relative to a centerpoint position
perform portal[damage].centerpoint[horz,240]

~position the attack portal to the left of damage column; we want the
~attack to be centered in its own column relative to a centerpoint position
perform portal[attack].centerpoint[horz,190]

~position the name on the left and let it use all available space
var limit as number
limit = portal[attack].left - 8 - portal[heldby].width - 5 - portal[strreq].width - 2
portal[name].left = 0
portal[name].width = minimum(portal[name].width,limit)

~show the 'strength requirement' icon to the right of the name
perform portal[strreq].alignrel[ltor,name,2]
portal[strreq].visible = tagis[Helper.BadStrReq]

~show the 'held by' icon to the right of the strenght requirement if appropriate
if (portal[strreq].visible = 0) then
  portal[heldby].left = portal[strreq].left
else
  perform portal[heldby].alignrel[ltor,strreq,5]
  endif
portal[heldby].visible = isgearheld

~if this is not a ranged weapon, hide the range portal
if (tagis[component.WeapRange] = 0) then
  portal[range].visible = 0
  endif

~only show the special portal if there are special abilities/notes to view
portal[special].visible = 1 - field[wpNotes].isempty