Creature Customization (Savage)

From HLKitWiki
Revision as of 21:07, 10 February 2009 by Rob (Talk | contribs) (Next)

Jump to: navigation, search

Context: HL KitAuthoring Examples … Savage Worlds Walk-Through 

Overview

In the previous sections, we managed to get creatures working smoothly. However, there was one area where we definitely could have provided a bit more flexibility. We allowed the user to customize attributes and skills from their starting points, but we did not do so for derived traits and abilities.

What if the GM wants a creature that's a little bit faster or tougher than normal? The answer is that we should allow him to adjust any of the creature's derived traits in whatever way suits his needs.

What if the GM wants to create an undead version of a normal creature? The simple solution would be to add the "Undead" ability to that creature, which will automatically add the Toughness adjustment and other notes for reference.

The focus of the sections below is on adding that flexibility to our data files.

Assigning Derived Traits

Derived traits work quite differently from attributes and skills for normal characters. That's because they are 100% derived from facets of the character. For creatures, though, we need to handle them differently. Various creatures have values assigned for certain derived traits that are not purely derived. The most common trait is "Pace", but other traits vary as well.

The problem is that derived traits are always calculated in the end. We need to handle them in a way that will smoothly integrate with their calculated nature. Unfortunately, there is no clean way of doing that by having the derived trait value be specified explicitly for a creature. The only way to reasonably accomplish it is to utilize an adjustment that gets incorporated into the calculated final value.

Specifying the Derived Traits Values

This means that we need to define four new fields for the "Creature" component. We need one field apiece for the four derived traits, where each field specifies an adjustment from the standard calculated value. If a field is not specified, then no extra adjustment will be applied for that trait.

From a usage standpoint, the problem with this approach centers on Pace. Virtually all creatures have a custom Pace, so we should really assume that Pace has a starting value of zero instead of the current six. That way, the actual Pace can be specified instead of an adjustment. Since the other derived traits rarely differ from the calculated value, requiring an adjustment to the calculated value is perfectly reasonable.

In order to specify the Pace as an explicit value, we need to modify the trait itself. The "trPace" trait currently assumes a default value of six for the "trtBonus" field. We need to override that behavior when we have a creature. So we'll define the following Eval script on the trait to apply the special handling.

<eval index="2" phase="Initialize" priority="3000"><![CDATA[
  if (hero.tagis[Hero.Creature] <> 0) then
    field[trtBonus].value = 0
    endif
  ]]></eval>

With the change in place, we can shift back to the fields on the "Creature" component. We'll end up with four fields, where the Pace indicates the actual starting value to use and the others indicate an adjustment. Our new fields should looks like the following.

<field
  id="crePace"
  name="Base Pace"
  type="static">
  </field>

<field
  id="creParry"
  name="Parry Adjustment"
  type="static">
  </field>

<field
  id="creTough"
  name="Toughness Adjustment"
  type="static">
  </field>

<field
  id="creCharis"
  name="Charisma Adjustment"
  type="static">
  </field>

Supporting User Adjustment

Integrating these new fields into the calculation is relatively simple. All we need is an Eval script that applies the adjustment values to the traits via the "traitadjust" script macro. If we do that, then the adjustment included in the standard calculation of the derived trait, without any special handling.

Unfortunately, there is a serious limitation with this approach. The user will not be able to directly modify the values for derived traits. We've made sure that the user can easily adjust the values for attributes and skills via the standard incrementers. What we need is a similar solution for derived traits, and applying the adjustments via an Eval script won't get it done.

If we use the same basic approach as attributes and skills, then the user can adjust the values freely. That would entail assigning the adjustment value to the "trtUser" field for each derived trait. But that field is not used for those traits. We're going to need to change that behavior and use the "trtUser" field for derived traits on creatures.

Derived traits calculate their final value via an Eval script on the "Derived" component. Within that script, the "trtUser" field is never used. It would be easy to add the "trtUser" value to the calculation for creatures. If we properly initialize the "trtUser" field values to the adjustments via the Creation script of the "Creature" component, then we can expose the "trtUser" field via an incrementer and allow the user to modify it.

Before we go down this path, though, we should think through the implications and how it's all going to work. The starting adjustments will be loaded into the "trtUser" field, which will be modified by the user. We'll want to bound that field so that it can be adjusted within a reasonable range of the starting value (e.g. -3 to +3). This will allow the user to tailor a creature appropriately without going completely wild. We'll then add the user value into the final trait calculation. This all seems perfectly sound.

Unfortunately, there's a critical hole in this approach. It's a subtle timing issue that comes into play with the bounding of the user value. Since the "trtUser" field is also used for normal (i.e. non-derived) traits, the bounding logic needs to be changed for our purposes. That can be handled, but the bounding must be performed before the initial calculation of "trtFinal" within the "Traits" component. For our purposes, we need to access the net values calculated for our derived traits in order to do proper bounding. For example, the Toughness trait must be calculated so that we know what the bounding range can be. This means we'll need to do the bounding after "trtFinal" is calculated. We have a chicken-and-egg problem.

The timing of the Bound script for the "trtUser" field is fixed. If we could schedule it at one time for normal traits and a different time for derived traits, we could make this work. Sadly, it must always be invoked at the same time for all traits. This means we can't use the "trtUser" field for our purposes.

Alternate Field for User Adjustment

The good news is that we can simply create a different field for our needs and proceed with our original plan. Our new field will behave very similarly to "trtUser", so we start by cloning it. The field is only needed for derived traits, so we'll add it to the "Derived" component.

We can re-use the "trtMinimum" and "trtMaximum" fields for bounding, and we won't need to do any special handling of the bounds. We want to display the final value within the incrementer, which we can pull from the "trtDisplay" field. Lastly, we want to allow the user to edit that value directly, so we'll include delta handling for our field. Our resulting field should look like the following.

<field
  id="trtUserCre"
  name="User Value for Creature"
  type="user"
  defvalue="0"
  usedelta="yes"
  maxfinal="50">
  <!-- Bound the user value to the limits established for the trait -->
  <bound phase="Traits" priority="5500" name="Bound trtUserCre">
    <before name="Derived trtFinal"/><![CDATA[
    @minimum = field[trtMinimum].value
    @maximum = field[trtMaximum].value
    ]]></bound>
  <!-- Display the final calculated value to the user -->
  <finalize><![CDATA[
    @text = field[trtDisplay].text
    ]]></finalize>
  </field>

With the field in place, we need to set it up properly. To accomplish this, we'll revise the Creation script for the "Creature" component. In addition to setting up the attributes, we must also assign the initial user values for the derived traits. This consists of adding the following lines of code to the script.

~assign the appropriate adjustment values to derived traits
hero.child[trPace].field[trtUserCre].value = field[crePace].value
hero.child[trParry].field[trtUserCre].value = field[creParry].value
hero.child[trTough].field[trtUserCre].value = field[creTough].value
hero.child[trCharisma].field[trtUserCre].value = field[creCharis].value

The next step is to include the "trtUserCre" field value in the calculation of the final value. For this, we need to modify the Eval script that calculates "trtFinal" within the "Derived" component. After the value is calculated normally, we can add the lines of code below to factor in the "trtUserCre" field for creatures.

~if this is a creature, we need to add the user value as a custom adjustment
if (hero.tagis[Hero.Creature] <> 0) then
  field[trtFinal].value += field[trtUserCre].value
  endif

Bounding the Derived Traits

At this point, we've got a user-modifiable value that will be setup properly and factored into the final adjustment calculation. The final piece we're missing is the bounding. We need to setup appropriate value for "trtMinimum" and "trtMaximum" so that the user can adjust the starting value within reason. In order to bound the value, we need to know the original starting value, since we'll establish our limits relative to that value. We also need to know what the absolute minimum value is for a given trait (i.e. its "floor" value). This value differs for each derived trait, since Charisma can go negative, Pace can't drop below one, and the others can't drop below two.

We need to introduce two new fields on the "Derived" component to track these values. Both of these values will be setup appropriately by the creature, so they are simple derived values. The two fields should look like below.

<field
  id="trtCreatur"
  name="Creature Start"
  type="derived">
  </field>

<field
  id="trtFloor"
  name="Creature Floor"
  type="derived">
  </field>

These values are derived, which means they don't persist like "user" fields. We need to set them up every evaluation cycle. This is done by adding a new Eval script to the "Creature" component. All this script needs to do is assign the appropriate values to both of these fields for each derived trait. This needs to be done early in the evaluation cycle, resulting in the following new script.

<eval index="3" phase="Setup" priority="5000"><![CDATA[
  ~setup the proper starting values for each trait
  hero.child[trPace].field[trtCreatur].value = field[crePace].value
  hero.child[trParry].field[trtCreatur].value = field[creParry].value
  hero.child[trTough].field[trtCreatur].value = field[creTough].value
  hero.child[trCharisma].field[trtCreatur].value = field[creCharis].value

  ~setup the proper floor values for each trait
  hero.child[trPace].field[trtFloor].value = 1
  hero.child[trParry].field[trtFloor].value = 2
  hero.child[trTough].field[trtFloor].value = 2
  hero.child[trCharisma].field[trtFloor].value = -4
  ]]></eval>

We can now utilize those values for properly bounding the "trtUserCre" value. We'll define a new Eval script on the "Derived" component for this purpose. Our minimum and maximum will range from -3 to +3 from the original starting value. We'll also use the floor value to verify that we don't let the user drop a trait below its absolute minimum. The only special detail about this script is its timing. We have to schedule this script after all standard traits are calculated and before the Bound script is evaluated on the "trtUserCre" field. This results in the new Eval script below.

<eval index="3" phase="Traits" priority="5300">
  <before name="Bound trtUserCre"/><![CDATA[
  ~setup a minimum at 3 below the starting value; if our minimum will yield a
  ~value below the minimum for this trait, limit it to the minimum
  field[trtMinimum].value = field[trtCreatur].value - 3
  var bonus as number
  bonus = field[trtBonus].value + field[trtInPlay].value
  if (field[trtMinimum].value + bonus < field[trtFloor].value) then
    field[trtMinimum].value = field[trtFloor].value - bonus
    endif

  ~now setup a maximum at 3 above the starting value
  field[trtMaximum].value = field[trtCreatur].value + 3
  ]]></eval>

Setup the Delta

All of our behaviors are in place, except for one. Derived trait values are simple numbers and not die types. Consequently, we want to let the user edit the value directly within the incrementer where they are shown. However, we want the user to edit a value that makes sense to him (i.e. the final result) instead of the adjustment. To do this, we need to set the "delta" for the "trtUserCre" field to the difference between the actual user value and the value the user will see. Once that's done, HL will handle the rest for us. The new Eval script should look like the following.

<eval index="4" phase="Render" priority="5000"><![CDATA[
  if (hero.tagis[Hero.Creature] <> 0) then
    field[trtUserCre].delta = field[trtBonus].value + field[trtInPlay].value + herofield[acNetPenal].value
    endif
  ]]></eval>

That was a fair amount of work, but everything should now be in place to setup the initial values for each creature and allow users to adjust them.

User Manipulation of Derived Traits

At this point, we have the internal workings in place and need to expose the field value for change by the user. This entails modifying the "baTrtPick" template that is used to show derived traits on the "Basics" tab. If we want the user to modify the value, we need to add a new incrementer portal. We can use a simple incrementer style that is provided by the Skeleton files, which results in the new portal shown below.

<portal
  id="value"
  style="incrSimple">
  <incrementer
    field="trtUserCre">
    </incrementer>
  <mouseinfo><![CDATA[
    @text = "Adjust this trait by clicking on the arrows to increase/decrease the value assigned."
    ]]></mouseinfo>
  </portal>

Within the Position script, we need to do two things. First, we must make sure that we only show either the new incrementer or the old "details" portal, which means controlling visibility based on whether we have a creature or not. This can be done at any point in the script. Second, we need to center our new incrementer in the same general region used by the existing "details" portal so that it occupies the same region. We have to do this after the "details" portal is positioned. This results in the following code being added to the Position script.

~the incrementer is visible if we have a creature, else the details
if (hero.tagis[Hero.Creature] <> 0) then
  portal[details].visible = 0
else
  portal[value].visible = 0
  endif

~center the incrementer over the details portal
perform portal[value].centeron[horz,details]
perform portal[value].centervert

We're now ready to give our changes a try. When we create a new creature, our incrementers show up where they should and have appropriate initial values displayed. Let's put our changes to the test with an actual creature that applies adjustments.

We'll modify our sample creature for this purpose. We can assign a few field values on the creature to apply our adjustments. We'll assign a "Pace" of "5" and a "Parry" adjustment of "1". To do this, we'll add the following XML elements to the creature's definition.

<fieldval field="crePace" value="5"/>
<fieldval field="creParry" value="1"/>

Reload the data files and create a creature. Take a look at the default values shown for the derived traits. From the chooser, select our test creature. The "Pace" value should immediately change to "5", and the "Parry" value should change from "2" to "3". Our derived traits are working smoothly now.

Let's attempt to adjust the derived traits. Decreasing Parry stops us at "2", since we set that up as our absolute floor. However, we can increase Parry a total of three notches. Our Toughness defaults to "4" and has the same behaviors. Our Pace is limited to a range of "2" to "8". Everything checks out.

We should also verify our delta is working properly. Click within the incrementer showing the Parry and it should show the current value for editing. Enter the new value "4" and everything appears to work fine. Now try entering a value of "9". The new value is automatically bounded to the maximum we established of "6" (i.e. our initial value of 3 plus and additional 3). Derived traits are fully operational.

Next