Difference between revisions of "Allies (Savage)"

From HLKitWiki
Jump to: navigation, search
(Recap Summary)
 
Line 298: Line 298:
 
   var rankvalue as number
 
   var rankvalue as number
 
   var ranktext as string
 
   var ranktext as string
   rankvalue = herofield[acRank].value
+
   rankvalue = field[acRank].value
 
   call RankName
 
   call RankName
 
   recap &= ranktext & "  (" & #resmax[resXP] & " XP)"
 
   recap &= ranktext & "  (" & #resmax[resXP] & " XP)"

Latest revision as of 13:05, 29 January 2009

Context: HL KitAuthoring Examples … Savage Worlds Walk-Through 

Overview

The Savage Worlds game system emphasizes the use of allies. As such, our data files should provide for the creation and management of allies associated with the characters.

Setting Up Ally Support

Allies are essentially independent characters that are children of the main characters. The Kit provides a framework for handling such characters very easily via the "minion" mechanism. Minions work very similarly to gizmos. They are attached to a character via a thing, and the minion is automatically added to the character whenever a pick based on that thing is added. Deleting the pick also deletes the minion. The parent character of a minion is referred to as the "master".

Just about any "thing" can have a minion attached. However, we want to be able to readily identify the picks that add our allies from other picks. We'll define a new component and component set to accomplish this. We'll assign the component a unique id of "Ally" and have it automatically define a component set with the same id, since we don't need to re-use any other special behaviors.

It would also be useful to let the user enter some arbitrary notes about each ally that can be used to readily identify them. For this, we'll include a field on the component where we can store those details. This results in a component set that looks like below, which we can add to the file "miscellaneous.str".

<component
  id="Ally"
  name="Ally">

  <!-- Brief summary of ally for display in the list of allies -->
  <field
    id="alySummary"
    name="Summary"
    type="user"
    maxlength="100">
    </field>

  </component>

Defining the Minion

We can now define a thing based on our new component set. We can add the thing to the file "thing_miscellaneous.dat". The only item of note about this thing is that the the minion will be attached by it.

Associating a minion with a thing is accomplished via the "minion" child element. Each minion needs to be assigned a unique id, which makes it possible to identify different minions when multiple types of minions are added to a character. We'll only have one type of minion, but we need to assign a unique id anyway.

The other important facet of our minion is that we want it to inherit all of the settings associated with the master character. For example, if the master has only the "Futuristic" time period selected, then we want to assume that the minion has the same behaviors. This is achieved via the "isinherit" attribute within the "minion" element.

Putting this all together, we end up with a thing definition that looks like the following.

<thing
  id="mscAlly"
  name="Ally"
  compset="Ally">

  <minion
    id="ally"
    isinherit="yes">
    </minion>

  </thing>

Manipulating Allies

We now need to figure out how to let users add and manage allies. We could add allies to an existing tab, but none of them really seem appropriate. There also is a space consideration, as many of our tabs are already quite packed with information. Since we only have a rather small number of tabs, it would be quite reasonable to add another tab for tracking allies.

When adding our tab, we'll want something very simple. We'll have a single table on the tab where the user can add and access allies. The "Skills" tab is very similar, so we'll copy the file "tab_skills.dat" as "tab_allies.dat" and then adapt the file to our needs.

The first thing we need to do is revise the table portal at the top. All allies will be attached to the character via the same "mscAlly" thing that we defined above. Consequently, we need to utilize an "auto" table that automatically adds a new pick based on a specific thing instead of prompting the user to select a thing. This requires that we specify the thing id to be used. We also need to utilize a custom template for showing the contents of each ally. The resulting portal should look like the one shown below.

<portal
  id="alAllies"
  style="tblNormal">
  <table_auto
    component="Ally"
    showtemplate="alPick"
    autothing="mscAlly">
    <headertitle><![CDATA[
      @text = "Allies Associated with Character"
      ]]></headertitle>
    <additem><![CDATA[
      @text = "Add a New Ally to the Character"
      ]]></additem>
    </table_auto>
  </portal>

For the moment, we'll keep the template very simple. We'll start with just the name of the pick, plus the standard info and delete portals. We'll come back in a moment to refine the template and make it more useful. This yields a template like the one below.

<template
  id="alPick"
  name="Ally Pick"
  compset="Ally"
  marginhorz="3"
  marginvert="2">

  <portal
    id="name"
    style="lblNormal"
    showinvalid="yes">
    <label
      field="name">
      </label>
    </portal>

  <portal
    id="info"
    style="actInfo">
    <action
      action="info">
      </action>
    <mouseinfo/>
    </portal>

  <portal
    id="delete"
    style="actDelete"
    tiptext="Click to delete this item">
    <action
      action="delete">
      </action>
    </portal>

  <position><![CDATA[
    ~set up our height based on the tallest portal
    height = portal[info].height

    ~if this is a "sizing" calculation, we're done
    if (issizing <> 0) then
      done
      endif

    ~position our tallest portal at the top and center other portals vertically
    portal[info].top = 0
    perform portal[name].centervert
    perform portal[delete].centervert

    ~position the delete and info portals on the far right
    perform portal[delete].alignedge[right,0]
    perform portal[info].alignrel[rtol,delete,-8]

    ~position the name on the left
    portal[name].left = 0
    ]]></position>

The next step is to revise the layout to show allies. All that entails is a switch to the new ids, which looks like below.

<layout
  id="allies">
  <portalref portal="alAllies" taborder="10"/>

  <position><![CDATA[
    ~position and size the table to span the full layout; it will only use the
    ~vertical space that it actually needs
    perform portal[alAllies].autoplace
    ]]></position>

  </layout>

The final step is to modify the panel. We'll position the new tab between the "Personal" and "Journal" tabs, which means we need to assign it an order of 315. This yields the following panel.

<panel
  id="allies"
  name="Allies"
  marginhorz="5"
  marginvert="5"
  order="315">
  <layoutref layout="allies"/>
  <position><![CDATA[
    ]]></position>
  </panel>

We can now give things try. Reload the data files and you should see the new "Allies" tab. On the tab is a table, and clicking on the "add item" of the table automatically adds a new ally pick to the character. When this happens, you should also see a new character appear on the Dashboard. This is our new ally.

If you switch to ally via the Dashboard, you can see that the ally is a standard character. You can also verify that the various settings associated with the master character are properly inherited into the minion. At the top left of the minion, next to the name, a button should appear. This button allows you to quickly return to the master of the minion by clicking on it. Click the button and you should again be looking at the master character. Now delete the ally pick from the table, at which point our minion disappears. The basics of allies are now operational.

Revising the Template

We should now do something more useful that just show the name of our allies. One thing that would be extremely useful is to add a button that lets the user go directly to a particular ally. We can always rely on the Dashboard for this, but a button next to each ally would be much nicer.

We want to accomplish the exact same behavior as the Dashboard, and we should probably use the exact same button for consistency. So take a look at how the Dashboard accomplishes this. It uses a special action portal that handles all the mechanics automatically. We'll copy the portal into our template and re-use all that same logic.

When we defined the "Ally" component, we included a field where the user can specify details about the character. We should show an edit portal next to the name that allows the user to edit those details. We'll size the edit portal based on whatever space exists between the ally name and the "info" portal on the right.

This results in a revised template that looks like the one below.

<template
  id="alPick"
  name="Ally Pick"
  compset="Ally"
  marginhorz="3"
  marginvert="2">

  <portal
    id="load"
    style="actLoad"
    tiptext="Click here to make this the active character.">
    <action
      action="minion">
      </action>
    </portal>

  <portal
    id="name"
    style="lblNormal"
    showinvalid="yes">
    <label
      field="name">
      </label>
    </portal>

  <portal
    id="summary"
    style="editNormal">
    <edit
      field="alySummary">
      </edit>
    </portal>

  <portal
    id="info"
    style="actInfo">
    <action
      action="info">
      </action>
    <mouseinfo/>
    </portal>

  <portal
    id="delete"
    style="actDelete"
    tiptext="Click to delete this item">
    <action
      action="delete">
      </action>
    </portal>

  <position><![CDATA[
    ~set up our height based on the tallest portal
    height = portal[info].height

    ~if this is a "sizing" calculation, we're done
    if (issizing <> 0) then
      done
      endif

    ~position our tallest portal at the top and center other portals on it
    portal[info].top = 0
    perform portal[name].centeron[vert,info]
    perform portal[delete].centeron[vert,info]
    perform portal[load].centeron[vert,info]

    perform portal[summary].alignrel[btob,name,2]

    ~position the delete and info portals on the far right
    perform portal[delete].alignedge[right,0]
    perform portal[info].alignrel[rtol,delete,-8]

    ~position the load portal on the left, with the name and summary adjacent
    portal[load].left = 0
    perform portal[name].alignrel[ltor,load,8]
    perform portal[summary].alignrel[ltor,name,10]
    portal[summary].width = portal[info].left - 10 - portal[summary].left
    ]]></position>

  </template>

If we reload the files, we can use the button next to the name to go directly to a given ally, plus we can enter notes about the ally for easy access and viewing.

Recap Summary

Showing the name of each ally and a few summary notes is of limited use. What would be ideal is if we could actually show a detailed summary of each ally, much like the contents of a statblock. We could easily show this summary beneath the current information for each ally. However, we need to have the summary available.

We could generate the summary on-the-fly via a Label script. However, we may also want to show the recap else where. For example, within the mouse-info for the ally. In order to make sure we can have access to the recap from various places, we need to synthesize the results into a field. This field can be added to the "Actor" component and generated via an Eval script.

The contents of the recap summary should be as compact as possible. Consequently, we'll minimize spacing and punctuation to the minimum necessary. We'll also use abbreviations and short names wherever possible. The resulting field and script are demonstrated below.

<field
  id="acRecap"
  name="Recap Summary"
  type="derived"
  maxlength="2000">
  </field>

<eval index="5" phase="Render" priority="10000"><![CDATA[
  var txt as string
  var recap as string

  ~output any race
  txt = hero.firstchild["Race.?"].field[name].text
  if (empty(txt) = 0) then
    recap &= txt & ", "
    endif

  ~output the XP and rank
  var rankvalue as number
  var ranktext as string
  rankvalue = field[acRank].value
  call RankName
  recap &= ranktext & "  (" & #resmax[resXP] & " XP)"

  ~output attributes
  foreach pick in hero where "component.Attribute & !Hide.Attribute"
    recap &= ", " & eachpick.field[trtAbbrev].text & " " & eachpick.field[trtDisplay].text
    nexteach

  ~output derived traits
  foreach pick in hero where "component.Derived & !Hide.Trait" sortas explicit
    recap &= ", " & eachpick.field[trtAbbrev].text & " " & eachpick.field[trtDisplay].text
    nexteach

  ~output special abilities
  foreach pick in hero where "component.Ability" sortas SpecialTab
    recap &= ", " & eachpick.field[shortname].text
    nexteach

  ~output arcane powers
  foreach pick in hero where "component.Power"
    recap &= ", " & eachpick.field[name].text
    nexteach

  ~output skills
  foreach pick in hero where "component.Skill & !Hide.Skill"
    recap &= ", " & eachpick.field[trtAbbrev].text
    if (eachpick.tagis[User.NeedDomain] <> 0) then
      recap &= " (" & eachpick.field[domDomain].text & ")"
      endif
    recap &= " " & eachpick.field[trtDisplay].text
    nexteach

  ~save the final contents
  field[acRecap].text = recap
  ]]></eval>

Now that we've got the field being synthesized, we can put it to use. We need to add a new portal to the template for displaying the field. We'll allocate three lines of text to the recap for each ally, which should allow us to show a handful of allies at a time when the main window is at its smallest height. In the interest of keeping things as tight as possible, we'll also decrease the spacing between lines by a one pixel. This results in the following portal.

<portal
  id="recap"
  style="lblSmlLeft">
  <label
    ismultiline="yes">
    <labeltext><![CDATA[
      ~output the recap field, but squeeze the line spacing a little bit
      @text = "{leading -1}" & minion.herofield[acRecap].text
      ]]></labeltext>
    </label>
  </portal>

We need to factor the height of the new portal into our overall height for the template. Once that's done, we can then place the portal beneath the current line of portals, leaving a margin on each side to set it off better and clearly break up individual allies in the table. The pertinent changes and additions to the Position script are shown below.

~set up our height based on our full extent
height = portal[summary].height + 5 + portal[recap].fontheight * 3
~position the recap portal beneath the top line and limit it to 3 lines
perform portal[recap].alignrel[ttob,summary,3]
portal[recap].lineheight = 3

~position and size the recap horizontally
perform portal[recap].alignrel[ltol,name,0]
portal[recap].width = portal[delete].left - 10 - portal[recap].left

~resize the contents of the recap portal if needed and ensure a 3-line height
perform portal[recap].sizetofit[30]
portal[recap].lineheight = 3

After reloading the data files, the recap text is quite helpful. Unfortunately, it's also still a bit too big and bold. It's competing with the primary information for each ally instead of being clearly supporting information. We need to switch to a different style that uses a smaller font and a less intense color. We could use a soft grey, but that's a little too subtle, so we'll choose a soft cyan instead. This yields the new style shown below, which can be swapped into use by the portal.

<style
  id="lblNotes">
  <style_label
    textcolor="99efed"
    font="fntnotes"
    alignment="left">
    </style_label>
  <resource
    id="fntnotes">
    <font
      face="Arial"
      size="34">
      </font>
    </resource>
  </style>

Refinements

Allies are basically working, but there are still a few things we should clean up. First of all, the mouse-info text shown for each ally is just the standard name and description text. We should really show the full recap information for the ally, since the three lines of space we've allocated may not be enough in some cases. This requires that we change the MouseInfo script from using the default behavior to more appropriate custom behavior.

We don't want to replace the standard behavior entirely, though. What we want is to append additional information at the end of the standard material. To accomplish this, we'll call the "MouseInfo" procedure to get the standard information and then append our own data at the end. This yields a new MouseInfo script that looks like the following.

<mouseinfo><![CDATA[
  var mouseinfo as string
  call MouseInfo
  @text = mouseinfo & "{br}{br}{b}Ally Summary:{/b}{br}" & minion.herofield[acRecap].text
  ]]></mouseinfo>

Another issue with our implementation can be seen when we switch to an ally. The "Allies" tab is visible for our allies, which means that we can theoretically add allies to our allies, and those allies can have their own allies as well. While this is technically valid, it doesn't make sense within the context of a Savage Worlds game. So we need to hide the "Allies" tab for allies.

Hiding the "Allies" tab in general is easy, and we've done it before. First, we define a new "HideTab.allies" tag. Then we add a Live tag expression to the panel that verifies the tag doesn't exist on the character. But how do we get the tag onto the character properly?

Remember that minions work very much like gizmos. We can assign tags to a child entity within the definition by use of the "tag" element on the entity. We can do the same within our "minion" element. All we need to do is an additional line to our thing, which ends up looking like the following.

<thing
  id="mscAlly"
  name="Ally"
  compset="Ally">

  <minion
    id="ally"
    isinherit="yes">
    <tag group="HideTab" tag="allies"/>
    </minion>

  </thing>

If we reload the files now, we'll see the "Allies" tab appear normally for our main characters, but it disappears when we're manipulating an ally.