Special Weapons (Savage)
Context: HL Kit … Authoring Examples … Savage Worlds Walk-Through
Overview
Now that all the basic gear is in place and being handled properly, we can go back and start adding special kinds of gear. We'll start with the various "Special Weapons" given in the Savage Worlds rulebook.
New Component and Component Set
Special weapons are virtually the same as ranged weapons in terms of what we need to manage them properly. However, it's critical that we clearly delineate between ranged weapons and special weapons. Consequently, we need to define a new component and component set for special weapons. Provided that we ensure that we have a clear and simple means of distinguishing between ranged and special weapons, we are free to re-use all the logic for ranged weapons. This allows us to define a component for special weapons that does nothing and use the "WeapRange" component in our new component set. We can rely on the automatically defined component tag to identify a special weapon distinctly from a ranged weapon. Based on this, our new component and component set should look similar to the following.
<component id="WeapSpec" name="Special Weapon" autocompset="no"> </component> <compset id="SpecWeap" stackable="yes"> <compref component="BaseWeapon" primary="yes"/> <compref component="WeapRange"/> <compref component="WeapSpec"/> <compref component="Equippable"/> <compref component="Gear"/> </compset>
Distinguish from Ranged Weapons
All special weapons will identify themselves as ranged weapons now because both possess the "component.WeapRange" tag. Anywhere that we currently rely on this tag to identify a ranged weapon needs to be revised to properly exclude special weapons. In order to determine whether a given weapon is solely a ranged weapon, we need to verify that it possesses the "component.WeapRange" tag and that it does not possess the "component.WeapSpec" tag.
Scanning through our data files, most references to the component tag are simply to determine whether to show range information for the weapon, so we need to preserve those references intact. The only case where we need to actively apply this new restriction is within the "arRange" table portal on the "Armory" tab, which will be found in the file "tab_armory.dat". Both the List and Candidate tag expressions for the table must be revised to explicitly exclude things that possess the "component.WeapSpec" tag. The new versions of each of these elements is shown below.
<list>component.WeapRange & !component.WeapSpec</list> <candidate>component.WeapRange & !component.WeapSpec & !Equipment.Natural</candidate>
New Behaviors
There are a variety of new behaviors the must be handled in support of special weapons. The first new behavior is that some special weapons possess a standard range, while others (e.g. mines) have no range. For the latter group of special weapons, we need to appropriately deal with the lack of range by displaying something appropriate. The "WeapRange" component already possesses a "wpRange" field that uses a Finalize script to synthesize the range appropriately. So we'll modify this Finalize script to handle this new case, resulting in a revised script that looks similar to the following.
if (tagis[Weapon.SpecRange] <> 0) then @text = "Special" elseif (field[wpShort].value + field[wpMedium].value + field[wpLong].value = 0) then @text = "--" else @text = field[wpShort].value & "/" & field[wpMedium].value & "/" & field[wpLong].value endif
In order to sort special weapons by grouping in the same way that we do for other weapons, we'll need to define a number of new "WeaponType" tags. Each new tag will correspond to a grouping that is used within the Savage Worlds rulebook, and we'll also preserve the order used within the rulebook. The new tags should consist of the following:
<value id="SpecCannon" name="Special: Cannons" order="20"/> <value id="SpecRocket" name="Special: Rocket Launchers" order="21"/> <value id="SpecMines" name="Special: Mines" order="22"/> <value id="SpecFlame" name="Special: Flamethrowers" order="23"/> <value id="SpecGren" name="Special: Grenades" order="24"/> <value id="SpecExplos" name="Special: Explosives" order="25"/>
The final new behavior that we must handle is an assortment of new special abilities. The new abilities are those pertaining to the various templates that are used by the different weapons. By adding new "Weapon" tags for these abilities, simply assigning the tags to the weapons will automatically incorporate the details into the description and other output for each weapon, thanks to the revisions we made earlier. The new tags should consist of the following:
<value id="MedBurst" name="Medium Burst Template"/> <value id="SmallBurst" name="Small Burst Template"/> <value id="LargeBurst" name="Large Burst Template"/> <value id="ConeTempl" name="Cone Template"/>
Table for Special Weapons
All of the mechanics are now in place for special weapons, but we still need to make them accessible to the user. We'll add a new table portal to the "Armory" tab for this purpose. Since all of the behaviors will be very similar to those for the existing ranged weapons table, we'll start by copying the "arRange" portal. We can then rename it to "arSpecial" and change it to only list things belonging to the "WeapSpec" component. We need to revise the List and Candidate tag expressions to only process the appropriate weapons, then we can change the various strings used in scripts to properly indicate that we're manipulating special weapons instead of ranged weapons. Everything else can remain the same. Putting it all together yields the revised table portal shown below.
<portal id="arSpecial" style="tblNormal"> <table_dynamic component="WeapSpec" showtemplate="arWpnPick" choosetemplate="arWpnThing" choosesortset="Weapon" addspace="2" buytemplate="BuyCash" selltemplate="SellCash" descwidth="275"> <list>component.WeapSpec</list> <candidate>component.WeapSpec & !Equipment.Natural</candidate> <description><![CDATA[ var descript as string call Descript @text = descript ]]></description> <headertitle><![CDATA[ @text = "Special Weapons" ]]></headertitle> <additem><![CDATA[ @text = "Add New Special Weapons" ]]></additem> </table_dynamic> </portal>
We've got a new table portal defined now, so we need to integrate it into the layout that controls what's shown for the tab. This entails adding it to the layout and then properly sizing and positioning the new portal. We'll put it at the bottom of the other portals, since the others are more likely to see regular access by users. We'll reserve space to show at least two items in the table, unless there are fewer items selected by the user. At the end, if there is still unused space left, we'll extend the table downward to use up whatever space is available. This makes it possible to preserve all of the existing logic for adaptively sizing the other table portals within the layout. All we need to do is integrate our new portal in appropriately. The revised layout element that cleanly integrates the new portal is presented below.
<layout id="armory"> <portalref portal="arMelee" taborder="10"/> <portalref portal="arRange" taborder="20"/> <portalref portal="arDefense" taborder="30"/> <portalref portal="arSpecial" taborder="40"/> <position><![CDATA[ ~set the width of all tables to the full width of the layout portal[arMelee].width = width portal[arRange].width = width portal[arDefense].width = width portal[arSpecial].width = width ~determine the gap to use between tables var gap as number gap = 10 ~position the special weapons table at the bottom, allowing for at most two rows portal[arSpecial].maxrows = 2 portal[arSpecial].top = height - portal[arSpecial].height ~determine the height remaining that can be used by other tables var ht as number ht = height - portal[arSpecial].height - gap ~position the armor/shield table above special weapons, allowing for at most two rows portal[arDefense].maxrows = 2 portal[arDefense].top = ht - portal[arDefense].height ~position the melee table at the top portal[arMelee].top = 0 ~set the heights of the two weapon tables to use all the space available portal[arMelee].height = ht portal[arRange].height = ht ~determine how much space we have left for the two tables; be sure to exclude ~the extra title and the extra spacing we'll use inbetween ~NOTE! If a value of 10 is added to the bottom coordinate of a portal, the ~net value will yield an actual GAP of one less. For example, if the bottom ~is at pixel 15, that pixel is part of the physical height of the portal. If ~you add 10 to that position for the next portal, it starts on pixel 25, so ~pixel 25 is part of the next portal. That means that pixels 16 through 24 ~represent the dead space inbetween, which is a span of 9 pixels. We have to ~factor this detail in when adjusting the space remaining by our gaps. var remain as number remain = portal[arDefense].top - portal[arMelee].top remain -= (gap - 1) * 2 ~if the height of both tables exceeds what we have left, we need to divvy up ~that space between the two tables if (portal[arMelee].height + portal[arRange].height > remain) then ~if the melee table is less than half the space, limit the ranged table ~to whatever space is leftover if (portal[arMelee].height < remain / 2) then portal[arRange].height = remain - portal[arMelee].height ~if the ranged table is less than half the space, limit the melee table ~to whatever space is leftover elseif (portal[arRange].height < remain / 2) then portal[arMelee].height = remain - portal[arRange].height ~otherwise, both tables are larger than half the space, so we need to limit ~the height of both of them ~NOTE! If we just divide the remaining amount by two and set both tables to ~that height, we could end up with both tables being truncated by more than ~a half item, with the combined height being a full item short of taking up ~the full space. So we have to set the height of one table to half the ~remaining space, then subtract that table's final height from our remaining ~space, and finally set that as the height for the second table. else portal[arRange].height = remain / 2 portal[arMelee].height = remain - portal[arRange].height endif endif ~position the ranged weapons table beneath the melee table portal[arRange].top = portal[arMelee].bottom + gap ~position the armor/shields table beneath the ranged weapons table ~NOTE! we already positioned this table, but the above logic could result in ~a gap between the tables, so we close that gap by repositioning again portal[arDefense].top = portal[arRange].bottom + gap ~set the height of the armor/shields table to the whatever height is left; ~if the armor list is long and the weapon lists are short, this will show as ~much armor as there is remaining room to accommodate portal[arDefense].height = ht - portal[arDefense].top ~position the special weapons table beneath the armor/shields table portal[arSpecial].top = portal[arDefense].bottom + gap ~set the height of the special weapons table to the whatever height is left; ~if the above tables are short, this will show as many special weapons as ~there is remaining room to accommodate portal[arSpecial].height = height - portal[arSpecial].top ]]></position> </layout>