More on Arcane Backgrounds (Savage)

From HLKitWiki
Revision as of 19:19, 27 January 2009 by Rob (Talk | contribs) (Hiding Arcane Skills)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

Context: HL KitAuthoring Examples … Savage Worlds Walk-Through 

Overview

It's time to switch our focus back to arcane backgrounds so we can continue fleshing out their behavior.

Mutual Exclusion

The Savage Worlds rules utilize the same basic mechanics for all types of arcane backgrounds, with the only difference being the trappings associated with the powers. As such, there is no practical benefit for a character to choose more than one arcane background, so we make the assumption that characters cannot choose multiple arcane backgrounds. This will make things much simpler for us to manage in the data files.

Imposing mutual exclusion on the various arcane backgrounds requires a little bit of work. Since arcane backgrounds can only be added via the various edges, we need to control the exclusion on the edges. One way of solving this would be to use a ContainerReq tag expression on each arcane background edge. This approach is optimal when a pick must be effectively ignored when the user makes an independent change to the character, but it is otherwise a more complex solution. In our case, all the logic for selecting arcane backgrounds is managed through the edges, so we simply need to ensure that only a single edge is ever added to the character at one time. If the user adds an arcane background and wants to change it to something else, he must delete the first one and can then add the new one back in.

Controlling this is achieved through the Candidate tag expression on the table portal, which governs the list of things that the user can choose from. The table of edges doesn't currently have a Candidate tag expression, so we'll need to add one. Candidate tag expressions can either be designed to include a subset of things that satisfy certain criteria or to exclude a subset that meet the criteria. Since we want to include all arcane backgrounds by default, our Candidate test needs to be designed to exclude things only when certain conditions exist.

We now need to figure out what tags we'll be filtering on. We need to limit our exclusion logic to arcane background edges. Looking at the various arcane background edges, there is nothing that identifies them clearly as arcane backgrounds. We need some way to do this, so we might as well assign the appropriate "Arcane" tag to each one. We can then focus our exclusion test only on edges that possess an "Arcane.?" tag. Or, by reversing the logic, we can exempt all edges lacking the tag from our exclusion test. Half of our test is now figured out. Just be sure to open the file "thing_edges.dat" and assign the appropriate tag to each arcane background edge.

If the user has already selected an arcane background for the character, we want to hide all others from selection. So we now need to figure out how to determine whether the user has selected an arcane background already. Fortunately, this is easy. Any time an arcane background is added, the "Arcane.?" tag is automatically forwarded to the character. This means we can check the character for one of the tags and instantly determine whether an arcane background has already been added.

Both halves of the problem have been solved. All that's left is for us to piece the tag expression together properly. The first half is to exempt from our tests any edges that lack an arcane background tag, so that translates to "!Arcane.?". This limits our focus in the rest of the tag expression to edges that are arcane backgrounds. The second half is to allow any arcane background if none has yet been added, and that translates to "!Arcane.?". But wait a minute. That's the same test, isn't it? Yes and no. It's the same test, but this time it needs to be applied to the character directly. By default, the Candidate tag expression is applying the tests to the thing that will potentially be shown to the user for selection. For the second half of our tag expression, we need to explicitly perform the test on the character instead of the thing. This is accomplished by prefixing the tag template with "hero#". Putting it all together yields the final Candidate tag expression below.

<candidate>!Arcane.? | !hero#Arcane.?</candidate>

Re-load the data files and go to the "Edges" tab. Click the option to add a new edge, and you should see all five arcane backgrounds listed properly. Add one to the character and the remaining arcane backgrounds all disappear. Exit the chooser form and delete the arcane background you just added. Now go back in and they are all visible again for selection. Our mutual exclusion logic is working.

Hiding Arcane Skills

There is one glaring problem with the solution we've put into place regarding arcane skills. All of the various arcane skills are always visible for user selection, regardless of whether the character possesses the corresponding arcane background. It would be vastly better to only show the proper arcane skill for selection on the "Skills" tab if the character actually has the proper arcane background. One way of doing this would be to revise the Candidate tag expression on the table of skills to only let the user select the skill if the background is present. The drawback of this approach, though, is that a user can add an arcane background, add the skill, then delete the background, and the skill will continue to exist without any warning to the user. 

We're going to employ an even better solution by leveraging a ContainerReq tag expression. The ContainerReq test is applied to the prospective container when things are being selected by the user. It is also applied to the actual container when picks have been added to the character. If the ContainerReq test fails when showing a thing for user selection, the thing is treated as if it doesn't exist, so it is physically omitted from the list of things presented to the user. If a thing is added and then its ContainerReq test fails due to other changes to the character, the pick is disabled by HL, the pick can be easily highlighted as invalid, and a suitable validation error is automatically reported. The pick still remains visible to the user, but that's basically just so that the user can respond to the error by deleting the pick.

When a ContainerReq test is defined for a thing, Hero Lab must be told when to perform the test within the overall evaluation cycle. The ContainerReq test must be the very first task performed for the thing, so it must be scheduled rather early in the overall cycle. However, it's quite possible that other tasks need to be processed first within the evaluation cycle. In our case, that's important, since our ContainerReq test needs to check against the presence of the "Arcane" tag on the character, but a script is needed to forward the tag from the arcane background up to the character. Consequently, we need to forward the tag from the arcane background before the ContainerReq test is performed.

If we want to find out the latest point in the evaluation cycle that we can schedule our ContainerReq test, we can add a ContainerReq test that is scheduled to occur at an extremely late timing (e.g. Render/10000). By re-compiling, HL will report an error, telling us when the earliest task on the thing is scheduled. Doing that yields an error that states we need to schedule our ContainerReq test at a timing of Initialize/3000 or earlier. We'll use Initialize/2000, which is specified via the "phase" and "priority" attributes. This results in a revised "Spellcasting" thing that looks like the following.

<thing
  id="skSpellcst"
  name="Spellcasting"
  compset="Skill"
  isunique="yes"
  description="Description goes here">
  <fieldval field="trtAbbrev" value="Spl"/>
  <tag group="Arcane" tag="Magic"/>
  <containerreq phase="Initialize" priority="2000">Arcane.Magic</containerreq>
  <link linkage="attribute" thing="attrSma"/>
  </thing>

NOTE! The above definition might seem a bit odd at first, since it defines the tag "Arcane.Magic" and has a ContainerReq test on the same tag. The important distinction is that the tag assignment assigns the tag to the thing, while the ContainerReq test checks for the tag on the container of the thing/pick. Since skills are always added directly to the character, the container is the character. This means that the tag is added to the thing but the test is performed on the character.

We have a problem, though. If we re-load the data files and check the table of skills, the "Spellcasting" skill will never appear, even if we properly select the "Arcane Background: Magic" edge. The problem is our timing, since the "Arcane" tag is still being forwarded by the "Arcane" component at the timing we previously assigned (Initialize/3000). We need to revise the timing of this script so that it occurs before the ContainerReq test. We'll use Initialize/1000, which yields a revised script of the following.

<eval index="1" phase="Initialize" priority="1000"><![CDATA[
  perform forward[Arcane.?]
  ]]></eval>

Everything should now work. Until the arcane background edge is selected, the corresponding skill never appears. Once the edge is added, the skill appears and can be added. If the user deletes the edge, the skill turns red and a validation error is reported until we delete it (or re-add the edge).

Arcane Power Mechanics

The mechanics for arcane backgrounds are already in place, but we still need to add the mechanics for arcane powers. This entails adding a new component and a new component set. Arcane powers have an assortment of characteristics that need to be tracked. We'll manage them all via fields for simplicity. All of these fields can be customized in some way beyond just tracking a number, except for the minimum rank required, so they need to be treated as text-based fields. Putting it all together yields the following component and component set, which can both be added to the file "miscellaneous.str".

<component
  id="Power"
  name="Arcane Power"
  autocompset="no"> 
  <field
    id="powMinRank"
    name="Minimum Rank"
    type="static">
    </field> 
  <field
    id="powPoints"
    name="Power Point Cost"
    type="static"
    maxlength="25">
    </field> 
  <field
    id="powRange"
    name="Range"
    type="static"
    maxlength="25">
    </field> 
  <field
    id="powLength"
    name="Duration"
    type="static"
    maxlength="25">
    </field> 
  <field
    id="powMaint"
    name="Maintenance"
    type="static"
    maxlength="25">
    </field> 
  <field
    id="powTraps"
    name="Trappings"
    type="user"
    maxlength="100">
    </field>
  </component> 

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

There is one thing missing from the above component, though. Each component needs to properly consume one instance from the resource that tracks the number of available powers. This ensures that the number of powers left decreases as new powers are added to the character, and it can be accomplished with a simple component-based Eval script like the one shown below.

<eval index="1" phase="Setup" priority="5000"><![CDATA[
  #resspent[resPowers] += 1
  ]]></eval> 

Re-Use of Minimum Rank

Powers have a minimum required rank that behaves exactly the same as for edges. That leaves us with two options. First, we could copy the logic over and adapt it for powers. Second, we could look for a way to carve out the minimum rank logic into a separate component that could be easily re-used. Since the minimum rank logic is identical for edges and powers, and since proper handling of the minimum rank entails testing the pre-requisites, it makes much more sense to carve out the logic for re-use.

Extracting the logic entails creating a new component that we'll call "MinRank". This new component needs to possess a single field for tracking the minimum rank and the pre-requisite logic. So we move the "edgMinRank" field to the new component and change its name to "rnkMinRank". Then we move the pre-requisite test to the new component. This results in a component that looks like the following.

<component
  id="MinRank"
  name="Minimum Rank"
  autocompset="no">
  <field
    id="rnkMinRank"
    name="Minimum Rank"
    type="static">
    </field>

  <prereq iserror="yes" message="Veteran Rank required.">
    <valid><![CDATA[
    ~get the minimum rank required for the thing to be valid
    var rank as number
    rank = altthing.field[rnkMinRank].value

    ~if the minimum rank is satisfied, we're good to go
    if (herofield[acRank].value >= rank) then
      @valid = 1
      done
      endif

    ~mark the panel as invalid
    altthing.linkvalid = 0

    ~synthesize an appropriate validation error message
    if (rank = 1) then
      @message = "Seasoned"
    elseif (rank = 2) then
      @message = "Veteran"
    elseif (rank = 3) then
      @message = "Heroic"
    elseif (rank = 4) then
      @message = "Legendary"
      endif
    @message &= " rank required."
    ]]></valid>
    </prereq>
  </component>

Unfortunately, we can't put the new component in the file "traits.str". We need to use this component in the component set for edges (in "traits.str") and the component set for powers (in "miscellaneous.str"). Since the compiler processes files with the same file extension (e.g. ".str") in whatever order that Windows gives them to HL, and since that order is not consistent across versions of Windows, we can't safely put the component in one file or the other. Instead, it needs to be placed in a file that is guaranteed to be compiled prior to both files. Such a file already exists as the file "components.core", which already contains a few components that are shared by different component sets. So we add our new component to the file "components.core", and everything should work smoothly.

Once the component is defined, we need to add it to the two component sets. This is easily achieved by adding the line below to both component sets.

<compref component="MinRank"/>

The last thing we need to do is deal with the fact that we've renamed the field for use within a different component. That means that all of our existing edges must have the field properly renamed. Open the file "thing_edges.dat" and perform a search-and-replace operation that changes all instances of "edgMinRank" to "rnkMinRank". After that, the logic has been factored out and is now being cleanly re-used in multiple places.

Adding Arcane Powers

Now that all the mechanics are in place, we can add the actual arcane powers. Since we're putting all arcane content in the file "thing_arcane.dat", that's where we'll add the powers. No arcane powers have special behaviors that we need to handle, so we'll present one as an example below. The rest can be defined following this example, or you can find them all in the completed Savage Worlds data files.

<thing
  id="powArmor"
  name="Armor"
  compset="Power"
  isunique="yes"
  description="Description goes here">
  <fieldval field="rnkMinRank" value="0"/>
  <fieldval field="powPoints" value="2"/>
  <fieldval field="powRange" value="Touch"/>
  <fieldval field="powLength" value="3"/>
  <fieldval field="powMaint" value="1/round"/>
  <fieldval field="powTraps" value="A mystical glow..."/>
  </thing>