Statblock Output (Savage)

From HLKitWiki
Jump to navigationJump to search

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

Overview

Most game systems have a standard format for presenting a character in a relatively compact, text-only format. The more commonly used term for this format is a "statblock". You'll find NPCs and monsters generally presented using this format within the various rulebooks. Savage Worlds has such a format, so we'll now make sure that we generate the appropriate output.

The Game Plan

The statblock format we'll use for Savage Worlds can be found in the Bestiary section of the core rulebook. It's a simple text format that makes use of bold text for important names. While most creatures in the Bestiary don't possess any gear, there are a few that do. We'll be using the way those entries are formatted as a guideline for how we should be synthesizing our statblock output.

All statblock output is generated via a Synthesize script within a "dossier" element. We're going to start with the statblock that is provided by the Skeleton data files. This starting point can be readily adapted for our purposes with Savage Worlds. It can be found within the file "out_statblock.dat", which also includes a number of procedures that are called directly from the Synthesize script.

How Output Works

As mentioned above, text output is generated via a Synthesize script. Since text output is really just a single big stream of text, it's very clunky to require the author to keep remembering to append data to a string variable. It's also very inefficient from a scripting standpoint. Consequently, the Kit provides a special mechanism that is is specific to text output. The text being synthesized is managed internally by HL and the "append" statement allows the author to easily tack more material onto the end of the output. This results in a simplified process for authors, although it also requires the output be generated in a serialized fashion.

The general mechanism is designed to support various different types of output, including plain text, HTML, and BBCode. In order to make this easy for authors, there are a number of different special symbols that are pre-defined by the Kit. Based on the output format chosen by the user, these symbols map to the appropriate codes for that format. For example, the "@boldon" symbol is used to enable bold text within the output. When the user selects HTML output, this symbol maps to "{b}", while it is empty for plain text output. This allows authors to synthesize the output once and let HL do the work of tailoring it to the appropriate format.

The following sequence of script code will output a series of attributes as text, where each is listed on a new line, the name of the field is in bold, and the description is not.

foreach pick in hero where "component.Attribute"
  append @boldon & eachpick.field[name].text & @boldoff
  append ": " & eachpick.field[description]
  append @newline
  nexteach

Outputting the Basics

The mechanism provided by the Skeleton files generates all of the basic details for a generic statblock. We're going to adapt it to the specifics of Savage Worlds, and we'll start with the attributes, skills, and derived traits.

The sample script includes the age as an example of inserting personal information about the character. The Savage Worlds format does not include such data, so we need to omit it. That means deleting the code that outputs the age.

For attributes and skills, a procedure is used that is passed in the tag expression to select the proper picks. In both uses, we need to modify the tag expression to omit the attributes and skills that are hidden from output. This results in the revised code below.

~output attributes
append @boldon & "Attributes: " & @boldoff
tagexpr = "component.Attribute & !Hide.Attribute"
call sbtraits

~output skills
append @boldon & "Skills: " & @boldoff
tagexpr = "component.Skill & !Hide.Skill"
call sbtraits

Now we need to revise the procedure contents. The current procedure works perfectly for attributes and most skills. However, it doesn't handle "Knowledge" skills that possess a domain. If a skill has a domain, we need to put that domain in parentheses. This entails modifying the procedure to look like the following.

var tagexpr as string
var ismore as number
ismore = 0
foreach pick in hero where tagexpr
  if (ismore <> 0) then
    append ", "
    endif
  append eachpick.field[name].text
  if (eachpick.tagis[User.NeedDomain] <> 0) then
    append " (" & eachpick.field[domDomain].text & ")"
    endif
  append " " & eachpick.field[trtDisplay].text
  ismore = 1
  nexteach
append @newline

Attributes and skills are now handled, so that leaves derived traits. Savage Worlds uses a different format for derived traits, so we need to implement them separately. Since the format is only used for derived traits, we can either write a procedure that does it or put the code directly into the Synthesize script. If the Synthesize script was large and complex, it would probably we worth using a separate procedure, but the script is relatively simple, so we'll just insert the code directly.

The Skeleton files provide logic for outputting derived traits after special abilities. We'll start by moving that code up to just below the output of skills. Once that's done, we'll adapt the code. Since Savage Worlds uses a comma-separated list for derived traits, we need to track when we need to insert a comma. We'll use the same technique as the various procedures, which have an "ismore" variable that starts at zero and gets changed to one after something is output. If the variable is non-zero, we need to insert a comma before the next item. This yields the following block of code.

~output derived traits
var ismore as number
ismore = 0
foreach pick in hero where "component.Derived & !Hide.Trait" sortas explicit
  if (ismore <> 0) then
    append ", "
    endif
  append @boldon & eachpick.field[name].text & ": " & @boldoff
  append eachpick.field[trtDisplay].text
  ismore = 1
  nexteach
append @newline

Gear Output

After all of the traits are output, the Savage Worlds format includes whatever gear the character possesses. All weapons, armor, and miscellaneous gear are lumped into this one block of material. In the case of weapons, the damage and range are shown in parentheses. In the case of armor, we'll include the defense rating and any parry bonus in parentheses. Simple equipment will be listed by name, along with any quantity possessed.

The first thing we need to do is handle how all the gear will be assembled for output. We need a single, comma-separate list, but we also need to synthesize each type of gear differently using separate procedures. To deal with this, we need to build up our gear list as a string. Once the final string is constructed, we can then output it. This results in the logic below.

~synthesize all gear
var details as string
details = ""
call sbweapons
call sbarmor
call sbgear
if (empty(details) = 0) then
  append @boldon & "Gear: " & @boldoff & details & @newline
  endif

Now we need to go into each of the procedures we rely upon and make them work the way they should. Within each of the procedures, we first need to setup the "details" variable for use as a parameter. The Synthesize script defines "details" and initializes it to empty. Each procedure will then append its items to the end of the "details" variable. This way, each procedure will build on the results of the preceding procedures.

Each procedure must also determine whether to start with a comma before the first item. Each procedure maintains it's own notion of whether a comma is needed, and its state must be initialized based on whether we already have anything within the "details" variable. If "details" is non-empty, we assume we'll need a comma before the first item we add in the procedure.

We can now adapt the existing procedures for weapons and armor to generate the text as we outline above. We can also add a new procedure for basic equipment, which will use the "grStkName" field to incorporate the quantity information. This results in the three procedures below.

<procedure id="sbweapons" scripttype="synthesize"><![CDATA[
  var details as string
  var ismore as number
  ismore = !empty(details)
  ~output a list of all weapons
  foreach pick in hero where "component.WeaponBase" sortas Armory
    if (ismore <> 0) then
      details &= ", "
      endif
    details &= eachpick.field[name].text
    details &= " (" & eachpick.field[wpDamage].text
    if (eachpick.tagis[component.WeapRange] <> 0) then
      details &= ", " & eachpick.field[wpShort].text & "/" & eachpick.field[wpMedium].text & "/" & eachpick.field[wpLong].text
      endif
    details &= ")"
    ismore = 1
    nexteach
  ]]></procedure>

<procedure id="sbarmor" scripttype="synthesize"><![CDATA[
  var details as string
  var ismore as number
  ismore = !empty(details)
  ~output the details of all armor
  foreach pick in hero where "component.Defense" sortas Armory
    if (ismore <> 0) then
      details &= ", "
      endif
    details &= eachpick.field[name].text
    details &= " (" & signed(eachpick.field[defDefense].text)
    if (eachpick.tagis[component.Shield] <> 0) then
      if (eachpick.field[defParry].value <> 0) then
        details &= ", Parry" & signed(eachpick.field[defParry].text)
        endif
      endif
    details &= ")"
    ismore = 1
    nexteach
  ]]></procedure>

<procedure id="sbgear" scripttype="synthesize"><![CDATA[
  var details as string
  var ismore as number
  ismore = !empty(details)
  ~output the list of all gear
  foreach pick in hero where "component.Equipment"
    if (ismore <> 0) then
      details &= ", "
      endif
    details &= eachpick.field[grStkName].text
    nexteach
  ]]></procedure>

Special Abilities

The final component of statblock output is the various special abilities for the character. All of the abilities are lumped together within the statblock, including edges, hindrances, and racial abilities. We could use a similar approach to gear for abilities, but all abilities are simply output with a name and summary, so can use a single mechanism for all abilities.

We only want to output special abilities if we have at least one. Fortunately, we can easily detect this. Since all abilities derived from the shared "Ability" component, we can check to see if the hero contains any picks that possess the "component.Ability" tag. If so, then we know that we have at least one ability to output and can do so. We'll output the various abilities via a called procedure, although we could just as easily include the code directly here. This results in the following code for orchestrating the output of abilities.

~output special abilities
if (hero.haschild["component.Ability"] <> 0) then
  append @boldon & "Special Abilities:" & @boldoff & @newline
  call sbability
  endif

Lastly, we need to implement the procedure that outputs the actual abilities. Since we have all of the abilities lumped into one list, we should organize it appropriately to show racial abilities, edges, and hindrances together. The most obvious way to do this is to use three different "foreach" loops, but there is an easier way. If we use a single "foreach" loop on all abilities and sort the sequence appropriately, we can get the desired order. We already have a sort set that will work perfectly for us, and that's the "SpecialTab" sort set. This sort set organizes everything by type, and since we only have abilities in our list, those abilities will be sorted just the way we want them.

When outputting the individual special abilities, the Savage Worlds format indents each ability. Unfortunately, there is no reliable way to indent with the various different text formats we need to support. So the easiest solution is to simply ignore the indentation and otherwise output the abilities using the same presentation style.

Putting this all together yields a procedure that looks the one below.

<procedure id="sbability" scripttype="synthesize"><![CDATA[
  ~output a list of all abilities
  foreach pick in hero where "component.Ability" sortas SpecialTab
    append chr(149) & eachpick.field[name].text & ": "
    append eachpick.field[summary].text & @newline
    nexteach
  ]]></procedure>