Difference between revisions of "Character Sheet Phase 4 (Savage)"

From HLKitWiki
Jump to: navigation, search
(Gear)
 
(4 intermediate revisions by the same user not shown)
Line 47: Line 47:
 
   <layoutref layout="oValidate"/>
 
   <layoutref layout="oValidate"/>
 
   <position><![CDATA[
 
   <position><![CDATA[
     ~set this global variable to 1 if you want the logos to be stacked; a value
+
     ~set this scene variable to 1 if you want the logos to be stacked; a value
 
     ~of zero places them side-by-side
 
     ~of zero places them side-by-side
     global[stacklogos] = 0
+
     scenevalue[stacklogos] = 0
  
 
     ~setup the gap to be used between the various sections of the character sheet
 
     ~setup the gap to be used between the various sections of the character sheet
 
     autogap = 40
 
     autogap = 40
     global[sectiongap] = autogap
+
     scenevalue[sectiongap] = autogap
  
 
     ~calculate the width of the two columns of the character sheet, leaving a
 
     ~calculate the width of the two columns of the character sheet, leaving a
Line 71: Line 71:
 
       perform layout[oValidate].render
 
       perform layout[oValidate].render
 
       layout[oValidate].top = height - layout[oValidate].height
 
       layout[oValidate].top = height - layout[oValidate].height
       extent = layout[oValidate].top - global[sectiongap]
+
       extent = layout[oValidate].top - scenevalue[sectiongap]
 
       endif
 
       endif
  
Line 93: Line 93:
 
     ~position the rightside layout in the remaining space on the right
 
     ~position the rightside layout in the remaining space on the right
 
     layout[oRightSide].width = colwidth
 
     layout[oRightSide].width = colwidth
     layout[oRightSide].top = layout[oLogos].bottom + global[sectiongap]
+
     layout[oRightSide].top = layout[oLogos].bottom + scenevalue[sectiongap]
 
     layout[oRightSide].left = layout[oLogos].left
 
     layout[oRightSide].left = layout[oLogos].left
     layout[oRightSide].height = layout[oAdjust].top - global[sectiongap] - layout[oRightSide].top
+
     layout[oRightSide].height = layout[oAdjust].top - scenevalue[sectiongap] - layout[oRightSide].top
 
     perform layout[oRightSide].render
 
     perform layout[oRightSide].render
  
 
     ~position the armory layout within the remaining space on the right
 
     ~position the armory layout within the remaining space on the right
 
     layout[oArmory].width = colwidth
 
     layout[oArmory].width = colwidth
     layout[oArmory].top = layout[oRightSide].bottom + global[sectiongap]
+
     layout[oArmory].top = layout[oRightSide].bottom + scenevalue[sectiongap]
 
     layout[oArmory].left = layout[oLogos].left
 
     layout[oArmory].left = layout[oLogos].left
  
Line 108: Line 108:
 
       layout[oArmory].visible = 0
 
       layout[oArmory].visible = 0
 
     else
 
     else
       layout[oArmory].height = layout[oAdjust].top - global[sectiongap] - layout[oArmory].top
+
       layout[oArmory].height = layout[oAdjust].top - scenevalue[sectiongap] - layout[oArmory].top
 
       perform layout[oArmory].render
 
       perform layout[oArmory].render
 
       endif
 
       endif
Line 114: Line 114:
 
     ~position the gear layout in whatever space is left on the right
 
     ~position the gear layout in whatever space is left on the right
 
     layout[oGear].width = colwidth
 
     layout[oGear].width = colwidth
     layout[oGear].top = layout[oArmory].bottom + global[sectiongap]
+
     layout[oGear].top = layout[oArmory].bottom + scenevalue[sectiongap]
 
     layout[oGear].left = layout[oLogos].left
 
     layout[oGear].left = layout[oLogos].left
  
Line 122: Line 122:
 
       layout[oGear].visible = 0
 
       layout[oGear].visible = 0
 
     else
 
     else
       layout[oGear].height = layout[oAdjust].top - global[sectiongap] - layout[oGear].top
+
       layout[oGear].height = layout[oAdjust].top - scenevalue[sectiongap] - layout[oGear].top
 
       perform layout[oGear].render
 
       perform layout[oGear].render
 
       endif
 
       endif
Line 176: Line 176:
  
 
     ~size the name to fit the available space
 
     ~size the name to fit the available space
 +
    portal[name].width = width
 
     perform portal[name].sizetofit[36]
 
     perform portal[name].sizetofit[36]
 +
    perform portal[name].autoheight
 +
    perform portal[name].centervert
 
     ]]></position>
 
     ]]></position>
  
Line 196: Line 199:
 
     ~Note: We don't use autoplace, since we never want the table hidden
 
     ~Note: We don't use autoplace, since we never want the table hidden
 
     portal[oInjury].left = 0
 
     portal[oInjury].left = 0
     portal[oInjury].top = autotop + global[sectiongap]
+
     portal[oInjury].top = autotop + scenevalue[sectiongap]
 
     portal[oInjury].width = width * .4
 
     portal[oInjury].width = width * .4
 
     portal[oInjury].height = 5000
 
     portal[oInjury].height = 5000
Line 229: Line 232:
 
     ~Note: We don't use autoplace, since we never want the table hidden
 
     ~Note: We don't use autoplace, since we never want the table hidden
 
     portal[oInjury].left = 0
 
     portal[oInjury].left = 0
     portal[oInjury].top = autotop + global[sectiongap]
+
     portal[oInjury].top = autotop + scenevalue[sectiongap]
 
     portal[oInjury].width = width * .4
 
     portal[oInjury].width = width * .4
 
     portal[oInjury].height = 5000
 
     portal[oInjury].height = 5000
Line 393: Line 396:
 
     ~Note: We don't use autoplace, since we never want the table hidden
 
     ~Note: We don't use autoplace, since we never want the table hidden
 
     portal[oInjury].left = 0
 
     portal[oInjury].left = 0
     portal[oInjury].top = autotop + global[sectiongap]
+
     portal[oInjury].top = autotop + scenevalue[sectiongap]
 
     portal[oInjury].width = width * .4
 
     portal[oInjury].width = width * .4
 
     portal[oInjury].height = 5000
 
     portal[oInjury].height = 5000
Line 490: Line 493:
 
The final thing we need to add to the condition tracking section is a place to mark off power points that are spent. All the various character sheets tend to show 30 boxes, so we'll do the same. We'll place a sequence of boxes beneath the health tracking region and allow the user to check boxes as points are spent. Given our limited space, we won't be able to fit 30 boxes, so we'll instead output two rows of 15 boxes each. To make tracking easier for the user, we'll ensure that every fifth box is larger and stands out from the others.
 
The final thing we need to add to the condition tracking section is a place to mark off power points that are spent. All the various character sheets tend to show 30 boxes, so we'll do the same. We'll place a sequence of boxes beneath the health tracking region and allow the user to check boxes as points are spent. Given our limited space, we won't be able to fit 30 boxes, so we'll instead output two rows of 15 boxes each. To make tracking easier for the user, we'll ensure that every fifth box is larger and stands out from the others.
  
Synthesizing output like we need is most easily handled via a script. By using some nested "for/next" loops, we can construct the text for output. However, we need to figure out how we're going to output boxes without having to use bitmaps. The answer is the "Wingdings 2" font that is built into Windows. We can utilize the "Character Map" tool provided by Windows to view the various characters supported by each font. Only a small number of fonts are built into every installation of Windows, and these include the "Webdings" font and the three separate "Wingdings" fonts. Scanning through the character map for "Wingdings 2", we spot a suitable box character that we can use (character code "A3" in hex).
+
Synthesizing output like we need is most easily handled via a script. By using some nested "for/next" loops, we can construct the text for output. However, we need to figure out how we're going to output boxes without having to use bitmaps. The answer is the "Wingdings" font that is built into Windows. We can utilize the "Character Map" tool provided by Windows to view the various characters supported by each font. Only a small number of fonts are built into every installation of Windows, and these include the "Webdings" font and the "Wingdings" font. Scanning through the character map for "Wingdings", we spot a suitable box character that we can use (character code "A8" in hex).
  
Let's think through our logic now. We start by switching to the "Wingdings 2" font via the "{font wingdings 2}" encoded text sequence. Between each character, we'll need to insert a tiny extra bit of spacing, which can be accomplished via the "{horz 1}" specification. For every fifth character, we need to increase the font size and then shrink it back down, which entails using "{size 52}". After every 15 characters, a line break needs to be inserted.
+
Let's think through our logic now. We start by switching to the "Wingdings" font via the "{font Wingdings}" encoded text sequence. Between each character, we'll need to insert a tiny extra bit of spacing, which can be accomplished via the "{horz 1}" specification. For every fifth character, we need to increase the font size and then shrink it back down, which entails using "{size 52}". After every 15 characters, a line break needs to be inserted.
  
 
Since the border looks good around the health tracking, we'll use a separate border around the power points tracking region. So we'll need two new portals, with one being generating the text for output and the other providing a border. This yields the two new portals below and the additional code for the Position script given below.
 
Since the border looks good around the health tracking, we'll use a separate border around the power points tracking region. So we'll need two new portals, with one being generating the text for output and the other providing a border. This yields the two new portals below and the additional code for the Position script given below.
Line 505: Line 508:
 
       var j as number
 
       var j as number
 
       var k as number
 
       var k as number
       @text = "{font wingdings 2}"
+
       @text = "{font Wingdings}"
 
       for i = 1 to 2
 
       for i = 1 to 2
 
         for j = 1 to 3
 
         for j = 1 to 3
 
           for k = 1 to 4
 
           for k = 1 to 4
             @text &= chr(163) & "{horz 1}"
+
             @text &= chr(168) & "{horz 1}"
 
             next
 
             next
           @text &= "{size 52}" & chr(163) & "{size 40}"
+
           @text &= "{size 52}" & chr(168) & "{size 40}"
 
           next
 
           next
 
         @text &= "{br}"
 
         @text &= "{br}"
Line 544: Line 547:
  
 
This doesn't provide anything impressive, but it offers a clean and effective solution in a very compact space. This will do quite nicely.
 
This doesn't provide anything impressive, but it offers a clean and effective solution in a very compact space. This will do quite nicely.
 
===Armor===
 
 
The next section for us to deal with on the character sheet is armor. In Savage Worlds, armor is relatively simple, but there are still some important details we should include for each piece. Obviously, the armor rating is needed. For shields, we need to include the parry rating if it is non-zero. For armor, we need to include the areas covered by the armor.
 
 
One option is to do this with columns of data, just like we did for arcane powers. However, shields and armor need to show different details, and the number of details is small, so we'll just list the details in text appended after the name. The Skeleton files provide handling for armor that uses this same technique, so we'll adapt the provided "oArmorPick" template for our needs.
 
 
Before we do that, though, we need to consider how we're going to display the various areas covered by the armor. If we use the names "Head", "Torso", and such, we'll never be able to fit the details text without switching to an incredibly small font or running onto a second line. Since space is at a premium, we need something to fit on one line. The easiest solution is to only show the first letter of each area, so "Head" would be shown as "H", etc. This will be immediately obvious to someone reviewing the character sheet and be very compact.
 
 
The question now becomes how best can be get the first letter of every area. Within the script, we'll be able to retrieve the names of each area, so we can then muck with the string we get back to strip out only the first characters. However, that will be a real pain to do. A much easier solution is to define an abbreviation for each of the tags for the different areas that is the first character. Once we do that, we can use the "tagabbrevs" target reference instead of "tagnames" and retrieve the abbreviations for display.
 
 
To add the abbreviations, we need to modify the various armor location tags. Open the file "tags.1st" and locate the tag group "ArmorLoc". Then add appropriate abbreviations to each tag. The result should look like the following.
 
 
<pre>
 
<group
 
  id="ArmorLoc">
 
  <value id="Torso" abbrev="T"/>
 
  <value id="Head" abbrev="H"/>
 
  <value id="Arms" abbrev="A"/>
 
  <value id="Legs" abbrev="L"/>
 
  </group>
 
</pre>
 
 
Now that the tags have been modified, we can shift our focus to the template showing each piece of defensive equipment. First of all, we don't need the "badstr" portal, so can rip that out. Next we can revise the "name" portal to synthesize the information we identified above for output. Since we're changing the font for the details text, sizing the "name" field to fit is inappropriate, so we must also eliminate that behavior. This yields the following revised template.
 
 
<pre>
 
<template
 
  id="oArmorPick"
 
  name="Output Armor Table"
 
  compset="Equipment"
 
  marginvert="1">
 
 
  <portal
 
    id="equipped"
 
    style="outNameMed">
 
    <output_label>
 
      <labeltext><![CDATA[
 
        @text = "{bmpscale 3 output_armor}"
 
        ]]></labeltext>
 
      </output_label>
 
    </portal>
 
 
  <portal
 
    id="name"
 
    style="outNameMed">
 
    <output_label>
 
      <labeltext><![CDATA[
 
        ~start with the name and switch to a smaller, non-bold font for details
 
        var temp as string
 
        @text = field[name].text
 
        @text &= "{size 36}{/b} ("
 
 
        ~add the defense rating
 
        @text &= field[defDefense].text
 
 
        ~if this is a shield, include the parry rating (if non-zero)
 
        if (tagis[component.Shield] <> 0) then
 
          if (field[defParry].value <> 0) then
 
            @text &= ", Parry: " & signed(field[defParry].value)
 
            endif
 
          endif
 
 
        ~if this is armor, include the areas covered by the equipment
 
        if (tagis[component.Armor] <> 0) then
 
          temp = tagabbrevs[ArmorLoc.?,","]
 
          if (empty(temp) = 0) then
 
            @text &= ", Covers: " & temp
 
            endif
 
          endif
 
 
        ~wrapup the details
 
        @text &= ")"
 
        ]]></labeltext>
 
      </output_label>
 
    </portal>
 
 
  <position><![CDATA[
 
    ~our height is the height of the tallest portal
 
    height = maximum(portal[name].height,portal[equipped].height)
 
    if (issizing <> 0) then
 
      done
 
      endif
 
 
    ~if the armor is not equipped, hide the bitmap
 
    if (tagis[Equipped.Equipped] = 0) then
 
      portal[equipped].visible = 0
 
      endif
 
 
    ~center all portals vertically
 
    perform portal[equipped].centervert
 
    perform portal[name].centervert
 
 
    ~shift the "equipped" bitmap downward a little bit; this is because it is a
 
    ~lone bitmap drawn via encoded text, and bitmaps are never drawn within the
 
    ~descender portion of the text, which causes it to appear higher than we want it
 
    portal[equipped].top += 4
 
 
    ~align everything horizontally
 
    perform portal[name].alignrel[ltor,equipped,5]
 
    ]]></position>
 
 
  </template>
 
</pre>
 
 
===Weapons===
 
 
Weapons are next on our list. We need to decide whether we want to use columns of data for weapons or stream the info as details like we just did for armor. There are four primary pieces of info for weapons, aside from the name. These are the attack roll, damage, range (if applicable), and armor piercing rating (if any). We should also include a notation if there are special rules for the weapon that need to be remembered by the player, as well as indicating if the minimum strength requirement for the weapon is not satisfied.
 
 
We'll make use of columnar data, since that will probably look better. We'll note a failed strength requirement via a bitmap indicator the prefixes the name. We'll indicate is there are special rules with a bitmap after the name. Since we'll have limited space, we'll display the "shortname" field for each weapon. In terms of columns, we'll have one for the attack, another for damage, a third for armor piercing, and a fourth for range.
 
 
We'll use the same implementation approach as we employed for arcane powers. We first create the table without the header, and then we add the header information.
 
 
====Table Portals====
 
 
For the table itself, we'll first figure out what indicators to use for insufficient strength and special notes. Within the interface, we use the universal "no" symbol and a star symbol, respectively. We should strive to be consistent, so we'll use the same symbols on the character sheet. The star symbol in the interface is a bitmap that is designed for the on-screen use, so it will look poor on the character sheet. If we bring up the "Character Map" tool and scan through the various symbol fonts we discussed earlier, we can quickly find a suitable character in the "Wingdings 2" font.
 
 
Now that we've identified all the pieces we need, we can define the additional portals. We'll add a "special" portal to indicate special details for the weapon. We'll also add an "ap" portal to display the armor piercing rating for the weapon. For the "range" portal, we want to show something other than just blank when there is no range, so we'll revise the Label script accordingly. Lastly, we need to realize that the current portals mostly use the "outNameMed" style, which utilizes a rather large, bold font. This simply won't fit in the space we've got to work with, so we need to switch to an alternate style that will fit. We can create our own or use something that already exists. Probably the best choice is to try the "outPlain" style and see if that works, after which we can change it if there are problems. We'll use this style for all portals except for the name, which we'll leave using the "outNameMed" style.
 
 
Based on the above considerations, we'll define the portals. This yields a collection of portals that looks like the set below.
 
 
<pre>
 
<portal
 
  id="badstr"
 
  style="outPlain">
 
  <output_label>
 
    <labeltext><![CDATA[
 
      @text = "{font Webdings}" & chr(120)
 
      ]]></labeltext>
 
    </output_label>
 
  </portal>
 
 
<portal
 
  id="name"
 
  style="outNameMed">
 
  <output_label
 
    field="shortname">
 
    </output_label>
 
  </portal>
 
 
<portal
 
  id="special"
 
  style="outPlain">
 
  <output_label>
 
    <labeltext><![CDATA[
 
      @text = "{font Wingdings 2}" & chr(234)
 
      ]]></labeltext>
 
    </output_label>
 
  </portal>
 
 
<portal
 
  id="attack"
 
  style="outPlain">
 
  <output_label
 
    field="wpNetAtk">
 
    </output_label>
 
  </portal>
 
 
<portal
 
  id="damage"
 
  style="outPlain">
 
  <output_label
 
    field="wpDamage">
 
    </output_label>
 
  </portal>
 
 
<portal
 
  id="ap"
 
  style="outPlain">
 
  <output_label>
 
    <labeltext><![CDATA[
 
      if (field[wpPiercing].value <> 0) then
 
        @text = field[wpPiercing].text
 
      else
 
        @text = "-"
 
        endif
 
      ]]></labeltext>
 
    </output_label>
 
  </portal>
 
 
<portal
 
  id="range"
 
  style="outPlain">
 
  <output_label>
 
    <labeltext><![CDATA[
 
      if (tagis[component.WeapRange] <> 0) then
 
        @text = field[wpRange].text
 
      else
 
        @text = "-"
 
        endif
 
      ]]></labeltext>
 
    </output_label>
 
  </portal>
 
 
<portal
 
  id="dots"
 
  style="outDots">
 
  <output_dots>
 
    </output_dots>
 
  </portal>
 
</pre>
 
 
====Positioning the Portals====
 
 
Now that we have all the portals, we need to position them appropriately. We'll start by centering the name. We'll also center our indicator symbols vertically, since they will be flanking the name on either side. All other portals use a smaller font than the name, so we need to align them along the same baseline as the name. Since we're using columns, we need to specify appropriate widths for each of the four columns other than the name. The name will then inherit whatever space remains. If we don't pick the correct widths for the columns, we can easily refine the values with a little bit of experimentation.
 
 
Once we have the space for the name identified, we can position the portals for each column. Now we are left with positioning the name appropriately. The two indicator bitmaps will be displayed as part of the name column. Consequently, we must determine which of the indicators is applicable and set our visibility accordingly. If the strength requirement is shown, then the name itself must be positioned to the right of the indicator.
 
 
Since the indicators are part of the space for the name, we need to subtract them from the width allocated to the name portal. Once that's done, we can automatically shrink the name to fit within the available space if it's too large. When we do this, we have to remember that shrinking the name keeps its "top" position the same, which means that smaller text will creep upwards. We want it to retain the same baseline position, so we have to re-position the portal after the resize to ensure the baseline doesn't change.
 
 
At this point, the name portal extends to the rightmost edge of the space we reserved for the name. However, we want the "special" portal to appear immediately adjacent to the name. We also want to include a sequence of dots between the name and the attack portal for better clarity. To accomplish this, we must now force the "name" portal to re-calculate its width based on the new font. Assuming the "sizetofit" operation above shrunk the name sufficiently, this will work perfectly. However, if the name is extremely long, we specified a minimum font size, so the operation may have stopped when the font size reached the minimum. If that's the case, re-calculating the size will end up making the portal wider, which will extend it into other columns. We can't allow that, so we must first get the current width, then re-calculate the width, and finally limit the new width to the original width.
 
 
Everything has been figured out now. So we can position the "name" portal properly and then position the "special" portal to its right, if necessary. The final step is showing the "dots" portal between the right edge of the used name region and the left edge of the attack column.
 
 
All of this logic can be seen in the Position script shown below.
 
 
<pre>
 
~our height is based on the tallest portal within
 
height = portal[name].height
 
if (issizing <> 0) then
 
  done
 
  endif
 
 
~center the name and indicators vertically
 
perform portal[name].centervert
 
perform portal[badstr].centervert
 
perform portal[special].centervert
 
 
~position the other portals with the same baseline as the name; since they
 
~use a smaller font, they will have a smaller height, so centering them will
 
~make them appear to float up relative to the name
 
perform portal[attack].alignrel[btob,name,0]
 
perform portal[damage].alignrel[btob,name,0]
 
perform portal[ap].alignrel[btob,name,0]
 
perform portal[range].alignrel[btob,name,0]
 
perform portal[dots].alignrel[btob,name,0]
 
 
~establish suitable fixed widths for the various columns of data
 
portal[attack].width = 135
 
portal[damage].width = 135
 
portal[ap].width = 50
 
portal[range].width = 225
 
 
~assign the name the remaining horizontal space
 
~Note: We must also account for a gap of 5 between portals.
 
portal[name].width = width - 4 * 5
 
portal[name].width -= portal[attack].width + portal[damage].width
 
portal[name].width -= portal[ap].width + portal[range].width
 
 
~position our columns now, except for the name
 
portal[attack].left = portal[name].right + 5
 
perform portal[damage].alignrel[ltor,attack,5]
 
perform portal[ap].alignrel[ltor,damage,5]
 
perform portal[range].alignrel[ltor,ap,5]
 
 
~setup to track our position when positioning portals along the x-axis
 
var x as number
 
x = 0
 
 
~if the weapon satisfies the minimum strength requirement, hide the bitmap,
 
~else position it on the left
 
if (tagis[Helper.BadStrReq] = 0) then
 
  portal[badstr].visible = 0
 
else
 
  portal[badstr].left = 0
 
  x = portal[badstr].right
 
  endif
 
 
~if there are no special details for the weapon, hide the bitmap
 
if (field[wpNotes].isempty = 1) then
 
  portal[special].visible = 0
 
  endif
 
 
~shrink the space for the name based on the presence of the two bitmaps
 
portal[name].width -= portal[badstr].width * portal[badstr].visible
 
portal[name].width -= portal[special].width * portal[special].visible
 
 
~size the name to fit the available space, then reposition it at the baseline
 
~Note: This is needed since smaller text will have the same top position.
 
perform portal[name].sizetofit[36]
 
perform portal[name].alignrel[btob,attack,0]
 
 
~recalculate the width of the name based on the sized font
 
~Note: This is needed so we can determine the span for the row of dots.
 
~Note: We must also cap the name portal to its previous width, just in case
 
~      the "sizetofit" didn't shrink the name far enough to fully fit.
 
var limit as number
 
limit = portal[name].width
 
perform portal[name].autowidth
 
if (portal[name].width > limit) then
 
  portal[name].width = limit
 
  endif
 
 
~position the name and special details indicator portals horizontally now
 
portal[name].left = x
 
x = portal[name].right
 
if (portal[special].visible <> 0) then
 
  portal[special].left = x
 
  x = portal[special].right
 
  endif
 
 
~extend the dots from the right of the name across to the attack portal
 
if (x > portal[attack].left - 10) then
 
  portal[dots].visible = 0
 
else
 
  portal[dots].left = x + 5
 
  portal[dots].width = portal[attack].left - 5 - portal[dots].left
 
  endif
 
</pre>
 
 
====Adding the Header====
 
 
Our table is looking good, so we can now deal get the header working correctly. We already have three header portals that we'll want to retain. These portals are being positioned relative to the portals in the main table, so they are working perfectly. All we need to do is add another header portal for the armor piercing rating. This can be added and then positioned just like the other header portals in the Header script.
 
 
Looking at it all, though, the header labels appear to be a little bit larger than would be optimal. Since we're using a smaller font than the provided Skeleton files, we need to ensure that the header is shrunk a bit, too. We can accomplish this by either switching to a new style or simply changing the font size used for font in the existing style. We'll do the latter and shrink the font a full point (from 36 to 32). Once we shrink the font size, we can also change the column headers to show the proper name instead of an abbreviation.
 
 
Putting it all together, this yields the following portals and Header script for the template.
 
 
<pre>
 
<portal
 
  id="hdrtitle"
 
  style="outTitle"
 
  isheader="yes">
 
  <output_label
 
    text="Weapons">
 
    </output_label>
 
  </portal>
 
 
<portal
 
  id="hdrattack"
 
  style="outHeader"
 
  isheader="yes">
 
  <output_label
 
    text="Attack">
 
    </output_label>
 
  </portal>
 
 
<portal
 
  id="hdrdamage"
 
  style="outHeader"
 
  isheader="yes">
 
  <output_label
 
    text="Damage">
 
    </output_label>
 
  </portal>
 
 
<portal
 
  id="hdrap"
 
  style="outHeader"
 
  isheader="yes">
 
  <output_label
 
    text="AP">
 
    </output_label>
 
  </portal>
 
 
<portal
 
  id="hdrrange"
 
  style="outHeader"
 
  isheader="yes">
 
  <output_label
 
    text="Range">
 
    </output_label>
 
  </portal>
 
</pre>
 
 
<pre>
 
<header><![CDATA[
 
  ~our header height is the title plus a gap plus the header text
 
  height = portal[hdrtitle].height + 2 + portal[hdrattack].height
 
  if (issizing <> 0) then
 
    done
 
    endif
 
 
  ~our title spans the entire width of the template
 
  portal[hdrtitle].width = width
 
 
  ~each of our header labels has the same width as the corresponding data beneath
 
  portal[hdrattack].width = portal[attack].width
 
  portal[hdrdamage].width = portal[damage].width
 
  portal[hdrap].width = portal[ap].width
 
  portal[hdrrange].width = portal[range].width
 
 
  ~center each header label on the corresponding data beneath
 
  perform portal[hdrattack].centeron[horz,attack]
 
  perform portal[hdrdamage].centeron[horz,damage]
 
  perform portal[hdrap].centeron[horz,ap]
 
  perform portal[hdrrange].centeron[horz,range]
 
 
  ~align all header labels at the bottom of the header region
 
  perform portal[hdrattack].alignedge[bottom,0]
 
  perform portal[hdrdamage].alignedge[bottom,0]
 
  perform portal[hdrap].alignedge[bottom,0]
 
  perform portal[hdrrange].alignedge[bottom,0]
 
  ]]></header>
 
</pre>
 
 
===Gear===
 
 
The final piece of the character sheet is handling the gear. The Skeleton files provide us with a gear table that lists all the gear in a single column. Since a single column is used, the font is large and bold, plus there is plenty of unused space. We need something compact, using a smaller, non-bold font, and employing at least two columns. In fact, if we can switch to a significantly smaller font, we can probably even squeeze three columns in, which would be ideal. So we'll start by modifying the table portal to use three columns.
 
 
Looking at the existing template, we don't want to keep the quantity in a separate column from the actual gear name. So we'll eliminate the separate "value" portal, but we'll move the logic for generating the quantity into the "name" portal. The template uses a large horizontal margin, which we need to shrink to something that will be just enough to separate our three columns. A value of 5 ought to be about right.
 
 
We need to use a small font to squeeze the gear into three columns, so we'll simply define a style specifically for use with gear. In the file "styles_output.aug", we'll create the "outGear" style and a corresponding font for use with the style. We'll use a small point size for the font and not use bold. This results in the new definition shown below.
 
 
<pre>
 
<style
 
  id="outGear">
 
  <style_output
 
    textcolor="000000"
 
    font="ofntgear"
 
    alignment="left">
 
    </style_output>
 
  <resource
 
    id="ofntgear">
 
    <font
 
      face="Arial"
 
      size="36">
 
      </font>
 
    </resource>
 
  </style>
 
</pre>
 
 
We can now implement the portal appropriately. We'll use the new style and prepend the quantity onto the name, which yields the following portal.
 
 
<pre>
 
<portal
 
  id="name"
 
  style="outGear">
 
  <output_label>
 
    <labeltext><![CDATA[
 
      if (stackable = 0) then
 
        @text = ""
 
      elseif (field[stackQty].value = 1) then
 
        @text = ""
 
      else
 
        @text = field[stackQty].text & "x "
 
        endif
 
      @text &= field[name].text
 
      ]]></labeltext>
 
    </output_label>
 
  </portal>
 
</pre>
 
 
With the portal in place, we need to position it. The name portal is sized based on the text within it, so our first action is the limit the width of the portal to the width of the template. Once we do that, we can try shrinking the font size to better fit longer names into the available space. Some names may still be too long, so we now limit the height to a single line. Lastly, we align the portal at the bottom of the template, which ensures that all gear entries across a row share the same baseline. This yields the following Position script.
 
 
<pre>
 
~our height is the height of the tallest portal
 
height = portal[name].height
 
if (issizing <> 0) then
 
  done
 
  endif
 
 
~limit the width of the name to the width of the template
 
if (portal[name].width > width) then
 
  portal[name].width = width
 
  endif
 
 
~size the name to fit the available space, if needed
 
perform portal[name].sizetofit[30]
 
 
~limit the name to a single line based on the updated font size
 
portal[name].lineheight = 1
 
 
~align the name at the bottom of the template
 
perform portal[name].alignedge[bottom,0]
 
</pre>
 

Latest revision as of 23:13, 4 May 2009

Context: HL KitAuthoring Examples … Savage Worlds Walk-Through 

Overview

In the previous stage of creating the character sheet, we got started on the right side and solved the most complex piece of the output in arcane powers. The remainder of the right side of the sheet introduce a few more wrinkles, but everything should proceed smoothly from this point forward.

Breaking the Right Side into Sections

The biggest remaining issue with the right side of the sheet is managing our vertical space appropriately. If a character has arcane powers, a bunch of weapons, and lots of gear, there might not be enough space on the right side in which to fit it all. It's perfectly reasonable for that to happen, and we can safely spill the remaining items onto a second page. However, we should give thought to which information is given priority for being shown on the first page and what we're happy to let fall to the second page.

It's very important that we show the region for tracking health on the first page, since that will come into play regularly for many games. Since we're pairing the health and injuries up side-by-side, we need to ensure those get included on the first page. We should also be sure to include a reminder of any actively enabled adjustments on the first page. Weapons and armor are a secondary priority, with gear being the lowest priority for the first page.

In order to ensure that we honor these priorities, we need to carve the right side of the page up into pieces that can be placed in an appropriate sequence. So we need create a new layout that encompasses the material we'll place in the lower right corner, then we need to position it after the powers are handled. Within the remaining space, we can position a separate layout that includes weapons and armor. If we still have space left, then we can squeeze our gear into that region via a third layout.

So our next order of business is to setup these three layouts. We already have a layout for the weapons and armor that we can use. This layout is designed to split its space between the weapons and armor tables, so we don't have to do anything special to get weapons and armor to work smoothly.

For the gear, we have the table portal and simply need a layout to contain it. That layout can be defined easily as shown below, and we must remove the portal reference from the "oRightSide" layout.

<layout
  id="oGear">
  <portalref portal="oGear"/>
  <position><![CDATA[
    ~position the various tables appropriately
    perform portal[oGear].autoplace

    ~our layout height is the extent of the elements within
    height = autotop
    ]]></position>
  </layout>

The Skeleton files provide us with a table of adjustments and a layout that contains them. For simplicity, we'll just use this layout as our third layout. When we need to add injuries and such, we'll integrate them into this layout. So we don't need to do anything further with this layout.

The final thing we need to do is properly revise the sheet to incorporate these three layouts. We also need to handle these layouts within the Position script for the sheet. This entails positioning the current "oRightSide" layout, then placing the "oAdjust" layout anchored to the bottom of the sheet. Once that's done, we can position the "oArmory" layout beneath the "oRightSide" layout, using up whatever space is available. We'll place the "oGear" layout last, using the final remnants of space on the character sheet. The updated contents of the "standard1" sheet should look like the one shown below.

<sheet
  id="standard1"
  name="Standard character sheet, page #1">
  <layoutref layout="oLogos"/>
  <layoutref layout="oLeftSide"/>
  <layoutref layout="oRightSide"/>
  <layoutref layout="oAdjust"/>
  <layoutref layout="oArmory"/>
  <layoutref layout="oGear"/>
  <layoutref layout="oValidate"/>
  <position><![CDATA[
    ~set this scene variable to 1 if you want the logos to be stacked; a value
    ~of zero places them side-by-side
    scenevalue[stacklogos] = 0

    ~setup the gap to be used between the various sections of the character sheet
    autogap = 40
    scenevalue[sectiongap] = autogap

    ~calculate the width of the two columns of the character sheet, leaving a
    ~suitable center gap between them
    var colwidth as number
    colwidth = (width - 50) / 2

    ~if the user wants to omit the validation report, the hide it and allow the
    ~rest of the sheet to fill that space; otherwise, output the layout and the
    ~top of the validation report establishes the bottom for all other output
    var extent as number
    if (hero.tagis[source.Validation] = 0) then
      layout[oValidate].visible = 0
      extent = height
    else
      layout[oValidate].width = width
      perform layout[oValidate].render
      layout[oValidate].top = height - layout[oValidate].height
      extent = layout[oValidate].top - scenevalue[sectiongap]
      endif

    ~position the leftside layout in the upper left corner
    layout[oLeftSide].width = colwidth
    layout[oLeftSide].height = extent - layout[oLeftSide].top
    perform layout[oLeftSide].render

    ~position the logos layout in the upper right corner
    layout[oLogos].width = colwidth
    perform layout[oLogos].render
    layout[oLogos].left = width - colwidth

    ~position the activated adjustments at the bottom on the right; this will
    ~establish the remaining space available on the right for other layouts
    layout[oAdjust].width = colwidth
    layout[oAdjust].left = layout[oLogos].left
    perform layout[oAdjust].render
    layout[oAdjust].top = extent - layout[oAdjust].height

    ~position the rightside layout in the remaining space on the right
    layout[oRightSide].width = colwidth
    layout[oRightSide].top = layout[oLogos].bottom + scenevalue[sectiongap]
    layout[oRightSide].left = layout[oLogos].left
    layout[oRightSide].height = layout[oAdjust].top - scenevalue[sectiongap] - layout[oRightSide].top
    perform layout[oRightSide].render

    ~position the armory layout within the remaining space on the right
    layout[oArmory].width = colwidth
    layout[oArmory].top = layout[oRightSide].bottom + scenevalue[sectiongap]
    layout[oArmory].left = layout[oLogos].left

    ~if the top of the armory layout is below the adjustments layout, there is no
    ~room for it, so hide it; otherwise, set height and render the layout properly
    if (layout[oArmory].top >= layout[oAdjust].top) then
      layout[oArmory].visible = 0
    else
      layout[oArmory].height = layout[oAdjust].top - scenevalue[sectiongap] - layout[oArmory].top
      perform layout[oArmory].render
      endif

    ~position the gear layout in whatever space is left on the right
    layout[oGear].width = colwidth
    layout[oGear].top = layout[oArmory].bottom + scenevalue[sectiongap]
    layout[oGear].left = layout[oLogos].left

    ~if the top of the gear layout is below the adjustments layout, there is no
    ~room for it, so hide it; otherwise, set height and render the layout properly
    if (layout[oGear].top >= layout[oAdjust].top) then
      layout[oGear].visible = 0
    else
      layout[oGear].height = layout[oAdjust].top - scenevalue[sectiongap] - layout[oGear].top
      perform layout[oGear].render
      endif
    ]]></position>
  </sheet>

Adjustments

Since the adjustments layout is always going to be given priority in the lower right corner, we should make sure it's all working now. The only time the adjustments table appears is when one or more in-play or permanent adjustments are enabled for the character when the sheet is printed. For testing, we can add a few of each and preview the character sheet. They appear nicely in the lower right corner, so there's nothing further we need to do to handle them now.

Injuries and Health

The plan is to always show any injuries in the lower right corner, along with a place to track health and power points. The injuries can be managed as a simple table portal, while the condition tracking requires that we implement something that is a little more complex. We could keep things easy by just inserting a graphic, or we could actually construct something that will work. We'll try the latter, without going overboard.

Injuries Table

Before we do that, though, we'll first get the injuries table into place. The table simply lists all injuries, and we'll need to provide a template that displays each individual injury appropriately. For the template, we just need something simple that will show the name of the injury. We can clone and adapt the template used for adjustments. This results in the table portal and template shown below.

<portal
  id="oInjury"
  style="outNormal">
  <output_table
    component="Injury"
    showtemplate="oInjury">
    <headertitle><![CDATA[
      @text = "Injuries"
      ]]></headertitle>
    </output_table>
  </portal>

<template
  id="oInjury"
  name="Output Injuries Table"
  compset="Injury"
  marginhorz="10">

  <portal
    id="name"
    style="outNormLt">
    <output_label
      field="name">
      </output_label>
    </portal>

  <position><![CDATA[
    ~our height is the vertical extent of our portals
    height = portal[name].height
    if (issizing <> 0) then
      done
      endif

    ~size the name to fit the available space
    portal[name].width = width
    perform portal[name].sizetofit[36]
    perform portal[name].autoheight
    perform portal[name].centervert
    ]]></position>

  </template>

Once the table is in place, we need to add it to the layout. We'll make it 40% of the total width of the layout, leaving the rest for condition tracking. Unlike our usual approach, we cannot use "autoplace" for this table. If the table is empty, automatic placement will hide the table, which will leave an empty gap in the character sheet that will look odd. So we need to position and size the table ourselves. Since the table will use no more space than it requires, we can set the height to something very large and HL will truncate it to the appropriate height. This results in the revised layout below.

<layout
  id="oAdjust">
  <portalref portal="oAdjust"/>
  <portalref portal="oInjury"/>
  <position><![CDATA[
    ~position the adjustments table at the top
    perform portal[oAdjust].autoplace

    ~position the injuries table in the left 40% beneath the adjustments
    ~Note: We don't use autoplace, since we never want the table hidden
    portal[oInjury].left = 0
    portal[oInjury].top = autotop + scenevalue[sectiongap]
    portal[oInjury].width = width * .4
    portal[oInjury].height = 5000

    ~our layout height is the bottom extent of the elements within
    height = portal[oInjury].bottom
    ]]></position>
  </layout>

If our character has no injuries, though, this looks strange. All we see is the header above the table and nothing within it. What we need is a portal that simply says "None" that we can show just below the header if the table is empty. So we'll define a new label portal and show it appropriately via the layout. The new portal and updated layout are shown below.

<portal
  id="oNoInjury"
  style="outNormal">
  <output_label
    text="-None-">
    </output_label>
  </portal>

<layout
  id="oAdjust">
  <portalref portal="oAdjust"/>
  <portalref portal="oInjury"/>
  <portalref portal="oNoInjury"/>
  <position><![CDATA[
    ~position the adjustments table at the top
    perform portal[oAdjust].autoplace

    ~position the injuries table in the left 40% beneath the adjustments
    ~Note: We don't use autoplace, since we never want the table hidden
    portal[oInjury].left = 0
    portal[oInjury].top = autotop + scenevalue[sectiongap]
    portal[oInjury].width = width * .4
    portal[oInjury].height = 5000

    ~if there are no injuries, indicate that fact
    if (portal[oInjury].itemsshown <> 0) then
      portal[oNoInjury].visible = 0
    else
      perform portal[oNoInjury].centeron[horz,oInjury]
      perform portal[oNoInjury].alignrel[ttob,oInjury,10]
      endif

    ~our layout height is the bottom extent of the elements within
    height = maximum(portal[oInjury].bottom,portal[oNoInjury].bottom)
    ]]></position>
  </layout>

Health in Template

For tracking health, the standard representation in Savage Worlds is a Wounds progression from one end and a Fatigue progression from the other end. This can probably be best implemented via a handful of portals. Since the contents of those portals have no relationship to any aspects of the character, no template is truly necessary. However, it's probably easiest to manage this via a template, so that's the approach we'll use.

We need a total of eight portals within our template. We need one to indicate "Wounds" on the left and another to indicate "Fatigue" on the right. We then need the "Incapacitated" indicator as a third portal, along with three progressive increments for wounds and two for fatigue. We'll place the two labels in the upper left and upper right corners of the span within which the condition tracking is shown. The six health levels will then be arrayed across the full width of the span. This results in a template that should look like the following.

<template
  id="oCondition"
  name="Output Condition"
  compset="Actor">

  <portal
    id="wounds"
    style="outNameLg">
    <output_label
      text="Wounds">
      </output_label>
    </portal>

  <portal
    id="fatigue"
    style="outNameLg">
    <output_label
      text="Fatigue">
      </output_label>
    </portal>

  <portal
    id="wound1"
    style="outNameLg">
    <output_label
      text="-1">
      </output_label>
    </portal>

  <portal
    id="wound2"
    style="outNameLg">
    <output_label
      text="-2">
      </output_label>
    </portal>

  <portal
    id="wound3"
    style="outNameLg">
    <output_label
      text="-3">
      </output_label>
    </portal>

  <portal
    id="fatigue1"
    style="outNameLg">
    <output_label
      text="-1">
      </output_label>
    </portal>

  <portal
    id="fatigue2"
    style="outNameLg">
    <output_label
      text="-2">
      </output_label>
    </portal>

  <portal
    id="incapac"
    style="outNameLg">
    <output_label
      text="INC">
      </output_label>
    </portal>

  <position><![CDATA[
    ~position the labels at the top
    portal[wounds].top = 0
    portal[fatigue].top = 0

    ~position all health levels beneath the labels with a common baseline
    perform portal[wound1].alignrel[ttob,wounds,10]
    perform portal[wound2].alignrel[btob,wound1,0]
    perform portal[wound3].alignrel[btob,wound1,0]
    perform portal[fatigue1].alignrel[btob,wound1,0]
    perform portal[fatigue2].alignrel[btob,wound1,0]
    perform portal[incapac].alignrel[btob,wound1,0]

    ~position the labels at the left and right edges
    portal[wounds].left = 0
    perform portal[fatigue].alignedge[right,0]

    ~put the -1 levels at each end
    portal[wound1].left = 0
    perform portal[fatigue1].alignedge[right,0]

    ~determine the remaining span over which the portals extend
    var span as number
    span = portal[fatigue1].left - portal[wound1].right

    ~tally the total width of the remaining health level portals
    var used as number
    used = portal[wound2].width + portal[wound3].width
    used += portal[fatigue2].width + portal[incapac].width

    ~divvy up the remaining horizontal space into equal pieces and use as spacing
    ~for positioning the various health level horizontally
    var each as number
    each = span - used
    each = round(each / 5,0,0)
    perform portal[wound2].alignrel[ltor,wound1,each]
    perform portal[wound3].alignrel[ltor,wound2,each]
    perform portal[fatigue2].alignrel[rtol,fatigue1,-each]

    ~size and center the incapacitated cell in the remaining space
    each = (portal[fatigue2].left - portal[wound3].right - portal[incapac].width) / 2
    perform portal[incapac].alignrel[ltor,wound3,each]

    ~our height is the bottom extent of the portals
    height = portal[wound1].bottom
    ]]></position>

  </template>

Integration Into Layout

Now that we've got our template created, we need to integrate it into the layout. We first add a "templateref" element. Since the template doesn't actually reference any live data within the portfolio, we can hook the template up to any pick or thing we choose. In general, the best tactic in situations like this is to utilize the "actor" pick, so that's what we'll do.

Once the template is part of the layout, we then need to position it properly. It gets placed to the right of the injuries table, leaving a little bit of a gap. The top of the template should be the same as the top of the injury table. The template will calculate its own height when it is rendered. Once rendered, it's conceivable (albeit unlikely) that the template will be taller than the table, so we need to set our height to the tallest of the two visual elements. This results in the updated layout shown below.

<layout
  id="oAdjust">
  <portalref portal="oAdjust"/>
  <portalref portal="oInjury"/>
  <portalref portal="oNoInjury"/>
  <templateref template="oCondition" thing="actor" ispick="yes"/>
  <position><![CDATA[
    ~position the adjustments table at the top
    perform portal[oAdjust].autoplace

    ~position the injuries table in the left 40% beneath the adjustments
    ~Note: We don't use autoplace, since we never want the table hidden
    portal[oInjury].left = 0
    portal[oInjury].top = autotop + scenevalue[sectiongap]
    portal[oInjury].width = width * .4
    portal[oInjury].height = 5000

    ~if there are no injuries, indicate that fact
    if (portal[oInjury].itemsshown <> 0) then
      portal[oNoInjury].visible = 0
    else
      perform portal[oNoInjury].centeron[horz,oInjury]
      perform portal[oNoInjury].alignrel[ttob,oInjury,10]
      endif

    ~position the condition template to the right of the injuries
    template[oCondition].left = portal[oInjury].right + 50
    template[oCondition].top = portal[oInjury].top
    template[oCondition].width = width - template[oCondition].left
    perform template[oCondition].render

    ~our layout height is the bottom extent of the elements within
    height = maximum(portal[oInjury].bottom,template[oCondition].bottom)
    ]]></position>
  </layout>

Refining the Health Display

After previewing our updated character sheet, the health region looks passable, but it would really benefit by having a soft grey border around the region to set it off nicely. Unlike portals, we cannot simply put a border around a template. So we'll need to add the border via a portal within the template. This entails adding a new portal whose sole purpose is to provide the border. We then setup a margin around the edges by positioning the portals with a gap all the way around. Once that's done, we can size the border portal to encompass the full region of the other portals, resulting in a border being drawn around a collection of portals. The new portal and the revised Position script are shown below.

<portal
  id="border1"
  style="outGreyBox">
  <output_label
    text=" ">
    </output_label>
  </portal>

<position><![CDATA[
  ~leave a margin around all edges to draw our border
  var margin as number
  margin = 10

  ~position the labels at the top
  portal[wounds].top = margin
  portal[fatigue].top = margin

  ~position all health levels beneath the labels with a common baseline
  perform portal[wound1].alignrel[ttob,wounds,10]
  perform portal[wound2].alignrel[btob,wound1,0]
  perform portal[wound3].alignrel[btob,wound1,0]
  perform portal[fatigue1].alignrel[btob,wound1,0]
  perform portal[fatigue2].alignrel[btob,wound1,0]
  perform portal[incapac].alignrel[btob,wound1,0]

  ~position the labels at the left and right edges
  portal[wounds].left = margin
  perform portal[fatigue].alignedge[right,-margin]

  ~put the -1 levels at each end
  portal[wound1].left = margin
  perform portal[fatigue1].alignedge[right,-margin]

  ~determine the remaining span over which the portals extend
  var span as number
  span = portal[fatigue1].left - portal[wound1].right

  ~tally the total width of the remaining health level portals
  var used as number
  used = portal[wound2].width + portal[wound3].width
  used += portal[fatigue2].width + portal[incapac].width

  ~divvy up the remaining horizontal space into equal pieces and use as spacing
  ~for positioning the various health level horizontally
  var each as number
  each = span - used
  each = round(each / 5,0,0)
  perform portal[wound2].alignrel[ltor,wound1,each]
  perform portal[wound3].alignrel[ltor,wound2,each]
  perform portal[fatigue2].alignrel[rtol,fatigue1,-each]

  ~size and center the incapacitated cell in the remaining space
  each = (portal[fatigue2].left - portal[wound3].right - portal[incapac].width) / 2
  perform portal[incapac].alignrel[ltor,wound3,each]

  ~set the first border to span the full dimensions of the health tracker
  portal[border1].width = width
  portal[border1].height = portal[wound1].bottom + margin

  ~our height is the bottom extent of the border
  height = portal[border1].bottom
  ]]></position>

Power Points Tracking

The final thing we need to add to the condition tracking section is a place to mark off power points that are spent. All the various character sheets tend to show 30 boxes, so we'll do the same. We'll place a sequence of boxes beneath the health tracking region and allow the user to check boxes as points are spent. Given our limited space, we won't be able to fit 30 boxes, so we'll instead output two rows of 15 boxes each. To make tracking easier for the user, we'll ensure that every fifth box is larger and stands out from the others.

Synthesizing output like we need is most easily handled via a script. By using some nested "for/next" loops, we can construct the text for output. However, we need to figure out how we're going to output boxes without having to use bitmaps. The answer is the "Wingdings" font that is built into Windows. We can utilize the "Character Map" tool provided by Windows to view the various characters supported by each font. Only a small number of fonts are built into every installation of Windows, and these include the "Webdings" font and the "Wingdings" font. Scanning through the character map for "Wingdings", we spot a suitable box character that we can use (character code "A8" in hex).

Let's think through our logic now. We start by switching to the "Wingdings" font via the "{font Wingdings}" encoded text sequence. Between each character, we'll need to insert a tiny extra bit of spacing, which can be accomplished via the "{horz 1}" specification. For every fifth character, we need to increase the font size and then shrink it back down, which entails using "{size 52}". After every 15 characters, a line break needs to be inserted.

Since the border looks good around the health tracking, we'll use a separate border around the power points tracking region. So we'll need two new portals, with one being generating the text for output and the other providing a border. This yields the two new portals below and the additional code for the Position script given below.

<portal
  id="power"
  style="outPlain">
  <output_label>
    <labeltext><![CDATA[
      var i as number
      var j as number
      var k as number
      @text = "{font Wingdings}"
      for i = 1 to 2
        for j = 1 to 3
          for k = 1 to 4
            @text &= chr(168) & "{horz 1}"
            next
          @text &= "{size 52}" & chr(168) & "{size 40}"
          next
        @text &= "{br}"
        next
      ]]></labeltext>
    </output_label>
  </portal>

<portal
  id="border2"
  style="outGreyBox">
  <output_label
    text=" ">
    </output_label>
  </portal>
~position and size the power level condition
perform portal[power].alignrel[ttob,border1,10 + margin]
portal[power].left = margin
portal[power].width = width - margin

~set the first border to span the full dimensions of the power tracker
portal[border2].top = portal[power].top - margin
portal[border2].width = width
portal[border2].height = portal[power].height + margin * 2

~our height is the bottom extent of the second border
height = portal[border2].bottom

This doesn't provide anything impressive, but it offers a clean and effective solution in a very compact space. This will do quite nicely.