Weird Science Gizmos (Savage)

From HLKitWiki
Jump to: navigation, search

Context: HL KitAuthoring Examples … Savage Worlds Walk-Through 

Overview

We're going to dedicate a full section to this topic. The actual changes are not complex, but there are a variety of issues that need to be considered. This results in a series of steps that we'll address one at a time.

How Gizmos Differ

Gizmos are a special variety of arcane power. In addition to the power itself, gizmos need to be controlled. In general, the use of a gizmo entails use of the "Weird Science" skill. However, a "ray gun" gizmo might use the "Shooting" skill instead. We need to track the associated skill for each gizmo and allow the user to specify that skill.

Gizmos are devices, so they each have their own pool of power points. This pool needs to be tracked independently for each gizmo. That means we need to integrate the proper handling for trackers into gizmos and display them on the "In-Play" tab so that user can manage them.

The final wrinkle with gizmos is that they can be shared. This means that a character with no arcane background can possess a gizmo. We therefore have to allow all characters to possess gizmos, and gizmos from other characters must allow the player to specify the number of power points available to each gizmo. We even have to handle the case where a weird scientist has both his own gizmos and gizmos created by others.

Differentiating a Gizmo

The first thing we need to resolve is how we're going to differentiate a gizmo from a normal power. One option would be to handle gizmos and powers much like we do weapons. We could define a new "base" component that has all the common behaviors of the two, then have separate "Power" and "Gizmo" components with the distinct behaviors. This would entail adding a new component set for gizmos as well. While this approach would definitely work, it's a non-trivial amount of work, so we should only go that route if it's truly necessary.

Gizmos are a proper superset of powers (i.e. they add behaviors but don't change any behaviors). In addition, there are only two pieces of new information that need to be tracked for powers. This means that the various new components we're considering would each be relatively small. An easier solution might be to just add the gizmo behaviors to the "Power" component and then use a tag to indicate whether a power is actually a gizmo. Since the changes we need to make aren't very complicated, using a tag ought to be more than adequate, so we'll give this approach a try.

We're actually going to have two different types of gizmos when we're finished. We need to identify gizmos in general, which can be handled via one tag. We also need to differentiate between gizmos created by the character and gizmos borrowed from another character. We'll refer to borrowed gizmos as "foreign" gizmos, and we can handle this distinction via a second tag. For simplicity, we'll just define these tags within the "Helper" tag group, and the new tags should look like below.

<value id="Foreign"/>
<value id="Gizmo"/>

We'll worry about how and when to assign these tags in the next section.

Managing Shared Gizmos

Gizmos that are shared need to be managed appropriately by the user. We currently only show the "Arcane" tab when a character has an arcane background, but we could always change that behavior. If we always showed the "Arcane" tab, then shared gizmos could be handled on it. The problem with this approach is that shared gizmos are more generally perceived as "gear" - instead of arcane powers by - by characters that are using them. As such, it would probably be better if we added shared gizmos to the "Gear" tab.

Our first task is to decide how we're going to handle shared gizmos on the separate tab. We can easily share the same template for displaying shared gizmos and powers. The question is whether we can also share the same table portal. Unfortunately, we can't. The two tables need to show different collections of powers/gizmos, so we need to use a different List tag expression on each. The existing table portal on the "Arcane" tab needs to show powers that do not possess the "Helper.Foreign" tag, which the table on the "Gear" tab only shows powers that do possess the tag. We also want to change the various text displayed for the header, adding an item, etc.

We can modify the "apPowers" table portal on the "Arcane" tab to have a suitable List tag expression, yielding the portal shown below.

<portal
  id="apPowers"
  style="tblNormal">
  <table_dynamic
    component="Power"
    showtemplate="apPower"
    choosetemplate="SimpleItem"
    addpick="resPowers"
    descwidth="350">
    <list>!Helper.Foreign</list>
    <titlebar><![CDATA[
      @text = "Add an Arcane Power - " & hero.child[resPowers].field[resSummary].text
      ]]></titlebar>
    <description/>
    <headertitle><![CDATA[
      @text = "Arcane Powers -  " & hero.child[resPowers].field[resSummary].text
      ]]></headertitle>
    <additem><![CDATA[
      ~get the color-highlighted "add" text
      @text = field[resAddItem].text
      ]]></additem>
    </table_dynamic>
  </portal>

For our new table portal on the "Gear" tab, we can start by copying the "apPowers" portal. We can then give it a new id, change the List tag expression to only include powers that possess the tag, and revise the various text shown via the scripts. However, there is one important question we haven't resolved yet. How do we get the "Helper.Foreign" tag to be assigned to items added via our new table?

The answer to this is the "autotag" element. Table portals can assign tags to each pick that they add to the hero. The assigned tags behave as if they are part of the defined nature of the pick. Consequently, by specifying an "autotag" element that assigns the "Helper.Foreign" tag, all picks added by the new table possess the tag, while all picks added by the original table on the "Arcane" tab do not.

Putting all this together results in the following new table portal being defined.

<portal
  id="grGizmos"
  style="tblNormal">
  <table_dynamic
    component="Power"
    showtemplate="apPower"
    choosetemplate="SimpleItem"
    descwidth="350">
    <list>Helper.Foreign</list>
    <autotag group="Helper" tag="Foreign"/>
    <titlebar><![CDATA[
      @text = "Add a Borrowed Weird Science Gizmo"
      ]]></titlebar>
    <description/>
    <headertitle><![CDATA[
      @text = "Borrowed Weird Science Gizmos"
      ]]></headertitle>
    <additem><![CDATA[
      @text = "Add a Borrowed Weird Science Gizmo"
      ]]></additem>
    </table_dynamic>
  </portal>

The final thing we need to do is integrate our new table portal into the layout. The layout for gear currently contains all the gear and any vehicles. We'll insert the shared gizmos between the two tables. We can use the same approach as is already used for the vehicles, reserving space for a single item in the table. This way, we can easily integrate the new portal and dedicate the majority of space on the table to normal gear. If there is space left over, we'll allocate it first to additional shared gizmos and lastly to additional vehicles. The revised layout should look like the one below.

<layout
  id="gear">
  <portalref portal="grGear" taborder="10"/>
  <portalref portal="grGizmos" taborder="20"/>
  <portalref portal="grVehicle" taborder="30"/>

  <!-- This script sizes and positions the layout and its child visual elements. -->
  <position><![CDATA[
    ~set all tables to span the full width of the layout
    portal[grGear].width = width
    portal[grGizmos].width = width
    portal[grVehicle].width = width

    ~position the vehicles table at the bottom with a minimum height of 2 items
    portal[grVehicle].maxrows = 2
    portal[grVehicle].top = height - portal[grVehicle].height

    ~position the gizmos table above the vehicles table with a minimum of 1 item
    portal[grGizmos].maxrows = 1
    portal[grGizmos].top = portal[grVehicle].top - portal[grGizmos].height

    ~position and size the gear table to fill all remaining space
    portal[grGear].top = 0
    portal[grGear].height = portal[grGizmos].top - 10

    ~position and size the gizmos and vehicle tables to use the remaining space
    ~at the bottom
    portal[grGizmos].top = portal[grGear].bottom + 10
    portal[grGizmos].height = portal[grVehicle].top - portal[grGizmos].top
    portal[grVehicle].top = portal[grGizmos].bottom + 10
    portal[grVehicle].height = height - portal[grVehicle].top
    ]]></position>

  </layout>

If we reload the data files and give our new table a try, we discover that we have a problem. No matter what we do, HL tells us that there are no powers to choose from. But we know that there are powers available, because we can readily add those powers on the "Arcane" tab. The problem is due to our List tag expression, which only includes powers that possess the "Helper.Foreign" tag. By default, the Kit assumes that the restrictions set forth by the List tag expression should also apply when identifying items for selection. However, the "Helper.Foreign" tag is only added when things are added via the table, so none exist with the tag pre-assigned, hence there is nothing to select.

The solution is to add our own Candidate tag expression, which tells the Kit to ignore the List tag expression by default. The Candidate tag expression doesn't actually have to contain anything, since we don't need to perform any special candidate tests - it must only exist. So all we need to do to fix the problem is add an empty Candidate tag expression to our table portal, which consists of the XML element below.

<candidate/>

If we reload the data files and try again, we now have no problems selecting powers from our new table. More importantly, powers added via our new table only appear within that table, and powers added on the "Arcane" tab only appear within that table. This means that a character with an arcane background can also borrow gizmos from other characters, with the two groups being fully distinguished.

Adding the Fields

We can now begin adding the logic for gizmos to powers. There are two separate pieces of information that we need to maintain for gizmos. The first is the linked skill and the second is the power points for the gizmo. We'll need one new field for each of these bits of information.

The linked skill will need to be selected by the user from a list of all the various skills. As such, we'll need to utilize a menu for the skill. Menu selections require special handling for the fields in which the selection will be saved. We must properly designate the field as being used for saving a menu selection by assigning it the correct "style". The resulting field definition is shown below.

<field
  id="powSkill"
  name="Linked Skill"
  type="user"
  style="menu">
  </field>

The stored power points for a gizmo are simpler. It's a value-based field that can be modified by the user. This yields the following field definition.

<field
  id="powStorage"
  name="Stored Points"
  type="user">
  </field>

Now that we've added the fields, we should also determine when we'll be using them. This will be dictated by the presence of the "Helper.Gizmo" and "Helper.Foreign" tags. The latter tag will be handled exclusively via the table portal itself, with shared gizmos receiving the tag. However, the "Helper.Gizmo" tag needs to be assigned correctly, so we'll add an Eval script to the component for that purpose. There are two situations when we need to assign the tag. The first is when the power has the "Helper.Foreign" tag, since we know the power is intended for use as a shared gizmo. The second is when the character possesses the "Weird Science" arcane background, since all of his powers must be treated as gizmos. This results in the Eval script below.

<eval index="3" phase="Setup" priority="5000"><![CDATA[
  ~it's a gizmo if it's foreign or if the character is a weird scientist
  if (tagis[Helper.Foreign] + hero.tagis[Arcane.WeirdSci] <> 0) then
    perform assign[Helper.Gizmo]
    endif
  ]]></eval>

Revising the Template

Arcane powers for a character with the "Weird Science" arcane background must be treated as gizmos on the "Arcane" tab. So the same template will be used for both arcane powers and gizmos. We're also going to be using this template to assign the power points for shared gizmos on the "Gear" tab.

The first thing we need to do is decide how we're going to visually integrate the new fields for gizmos into the template. We'll need a menu and an edit portal for the linked skill and the power points, plus two labels to identify them. The best solution is probably to add the gizmo-related fields at the bottom, beneath the trappings display. This way, a gizmo looks identical to a non-gizmo, except for the additional information at the bottom. This approach also make things easiest to implement.

We'll now define the four new portals we need. Since they will be positioned beneath the trappings, we'll insert them into the template right after the "trappings" portal. This way, we'll get the desired order when the user employs the <Tab> key to move between portals. For the menu, we'll specify that we want to select skills and that the default selection should be the "Weird Science" skill. We also need to specify the selection of "things" instead of "picks", since shared gizmos may be used by characters without the requisite skill (e.g. "Weird Science"). For the edit portal, we'll specify an integer format with a maximum of two digits. This yields the portals shown below.

<portal
  id="lblmenu"
  style="lblSmPower">
  <label
    text="Skill:">
    </label>
  </portal>

<portal
  id="menu"
  style="menuSmall">
  <menu_things
    field="powSkill"
    component="Skill"
    maxvisible="10"
    defthing="skWeirdSci"
    usepicks="thing">
    </menu_things>
  </portal>

<portal
  id="lblstorage"
  style="lblSmPower">
  <label
    text="Points:">
    </label>
  </portal>

<portal
  id="storage"
  style="editNormal">
  <edit
    field="powStorage"
    format="integer"
    maxlength="2">
    </edit>
  </portal>

Now that the portals are defined, we can manage them appropriately within the Position script. Since the visibility of the gizmo-related portals will influence the total height of the template, we need to determine their visibility first. The menu portal and its associated label are only shown for gizmos. The power points edit portal and its associated label are only shown for foreign gizmos. This results in the new script code below being inserted at the top.

~determine if the menu portal and label should be visible or not
if (tagis[Helper.Gizmo] = 0) then
  portal[lblmenu].visible = 0
  portal[menu].visible = 0
  endif

~only show the power points edit field if this is a foreign gizmo
if (tagis[Helper.Foreign] = 0) then
  portal[lblstorage].visible = 0
  portal[storage].visible = 0
  endif

The menu portal will definitely be taller than the edit portal and either of the labels, so its height will be factored into the overall height of the template. If the menu portal is visible, we need to increase the height of the template accordingly. We should also insert a tiny gap between the menu and the trappings details above - one pixel should suffice. The revised code for calculating the height should look like below.

~set up our height based on our tallest vertical span, which is the three
~portals for the power details, plus the menu if visible
height = portal[details].height + portal[duration].height + portal[trappings].height
if (portal[menu].visible <> 0) then
  height += portal[menu].height + 1
  endif

After we position the various details portals for the power, we can position the gizmo portals beneath them. Since the menu portal is the tallest, we'll position it first and then center the other portals upon it. This results in the code below, which can be inserted immediately after the vertical positioning of the power details portals.

~position the gizmo-related portals vertically
perform portal[menu].alignrel[ttob,trappings,1]
perform portal[lblmenu].centeron[vert,menu]
perform portal[lblstorage].centeron[vert,menu]
perform portal[storage].centeron[vert,menu]

The final task is to position the gizmo-related portals horizontally. We must wait to do that until after we've positioned the details portals horizontally, since we need to utilize the same horizontal span. Within that span, we position the portals sequentially, with appropriate spacing. We also need to decide upon appropriate widths for the menu and edit portal. The result is the code below, which can be inserted after the details portals are positioned horizontally.

~position the gizmo-related portals horizontally
portal[lblmenu].left = portal[details].left
perform portal[menu].alignrel[ltor,lblmenu,3]
portal[menu].width = 135
perform portal[lblstorage].alignrel[ltor,menu,10]
perform portal[storage].alignrel[ltor,lblstorage,2]
portal[storage].width = 25

Our new portals are positioned properly and appear only when appropriate.

Tracking Power Points

Gizmos are now appearing as they should within the tables, but they still aren't behaving correctly. Each gizmo needs to appear in the list of trackers on the "In-Play" tab. This requires that we make every power incorporate all the behaviors of a tracker. To do that, we must modify the "Power" component set to include the "Tracker" component, which yields the revised component set below.

<compset
  id="Power">
  <compref component="Power"/>
  <compref component="MinRank"/>
  <compref component="Tracker"/>
  </compset>

Powers now possess all the behaviors of trackers, but we need to configure them properly. If we start experimenting, the first thing we'll notice is that every power now shows up in the list of trackers on the "In-Play" tab. We only want gizmos to show up in that list, since powers don't actually have a separate set of points. This means that every power that is not a gizmo must be assigned the "Hide.Tracker" tag. We already added an Eval script that determines when we have a gizmo, so we can simply modify it to assign the tag for non-gizmos. The revised Eval script looks like the following.

<eval index="3" phase="Setup" priority="5000"><![CDATA[
  ~it's a gizmo if it's foreign or if the character is a weird scientist
  if (tagis[Helper.Foreign] + hero.tagis[Arcane.WeirdSci] <> 0) then
    perform assign[Helper.Gizmo]

  ~if not a gizmo, hide the power from the list of trackers on in-play tab
  else
    perform assign[Hide.Tracker]
    endif
  ]]></eval>

The next thing we need to do is setup each gizmo to possess the proper number of power points. The actual number of power points used by the tracker is pulled from the "trkMax" field. So we need to make sure that this field is setup correctly. We'll define a new Eval script for this purpose. In the script, we'll pull the power points for a foreign gizmo from the "powStorage" field that the user will specify. For a gizmo belonging to a character with "Weird Science", we need to use the character's maximum power, which can be obtained from the "trkPower" tracker. Since the value we set must be in place before the tracker calculates the amount remaining, we must be sure to schedule our new script before the calculation occurs. This yields the new Eval script below, which we'll define for the component.

<eval index="4" phase="Traits" priority="10000">
  <before name="Calc trkLeft"/><![CDATA[
  ~if we're not a gizmo, there's nothing to do
  if (tagis[Helper.Gizmo] = 0) then
    done
    endif

  ~if the power is not foreign, setup its power points
  if (tagis[Helper.Foreign] = 0) then
    field[trkMax].value = #trkmax[trkPower]

  ~otherwise, setup the power points for a foreign gizmo
  else
    field[trkMax].value = field[powStorage].value
    endif
  ]]></eval>

We can now test out how gizmos are working. The trackers show up properly for each gizmo, and the total points for each gizmo reflect the proper values. The only problem is that resetting a gizmo on the "In-Play" tab sets the value to zero. When a gizmo is reset, it should be assigned the maximum. We can fix this by changing the behavior of the tracker, which requires assigning the "Helper.ResetMax" tag. Since we need this behavior on all gizmos, we can assign it to the "Power" component with the line below.

<tag group="Helper" tag="ResetMax"/>

Gizmos are now working the way the should.

Validation

There are a couple things left that we have not done. Conditions exist that we should be reporting as an error to the user. If a user doesn't select a skill for a gizmo, we should report it as an error. Similarly, if the user doesn't specify a non-zero number of power points for a foreign gizmo, we should also report an error. We can resolve each of these by adding suitable Eval Rule scripts to the component, as shown below.

<evalrule index="2" phase="Validate" priority="6000" message="Linked skill must be specified for gizmo"><![CDATA[
  if (tagis[Helper.Gizmo] = 0) then
    @valid = 1
  elseif (field[powSkill].ischosen <> 0) then
    @valid = 1
    endif
  ]]></evalrule>

<evalrule index="3" phase="Validate" priority="7000" message="Power points must be specified for gizmo"><![CDATA[
  if (tagis[Helper.Gizmo] = 0) then
    @valid = 1
  elseif (tagis[Helper.Foreign] = 0) then
    @valid = 1
  elseif (field[powStorage].value <> 0) then
    @valid = 1
    endif
  ]]></evalrule>

Since failing to select a linked skill is now considered a validation error, we should highlight such a condition to the user in red. If the menu is invalid, we can modify the menu style appear in red. Adding the code below at the end of the Position script will work nicely.

~if the menu is visible and nothing is chosen yet, flag it in red
if (portal[menu].visible <> 0) then
  if (field[powSkill].ischosen = 0) then
    perform portal[menu].setstyle[menuError]
    endif
  endif