Creature Customization (Savage)
- 1 Overview
- 2 Customizing Derived Traits
- 3 Let User Add Abilities
- 4 Next
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.
Customizing Derived Traits
At this point, we have the basic framework in place for applying custom adjustments to derived traits for creatures. What we need to add now is the ability for the user to further tailor the derived trait values for a creature. Attributes and skills utilize an incrementer that allows the user to modify the values. If we use the same basic approach for derived traits, then the user can adjust the values freely.
Leveraging the "trtUser" Field
This approach 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 use the "trtUser" value instead of "trtCreatur" within the calculation of "trtFinal". 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 GM 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 liability in this approach. All of the current mechanisms we've implemented are built around the "trtUser" field being used with attributes and skills. The field will need to be handled in many different ways for derived traits. Our bounding limits will be different, the initial value will have to change, and how the field is utilized in various places will need to be revised. The one field will need to be used in two very different ways for different traits. That's going to make implementing our changes more complicated and will make maintaining our data files in the future much more difficult.
On top of this, there is 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 derived traits. 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 bottom line of all this is that we really shouldn't consider using the "trtUser" field for our purposes with derived traits.
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'll 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 change the code to use in the "trtUserCre" field for creatures instead of the "trtCreatur" field, as shown below.
~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
Hookup User Manipulation
At this point, we have the basic internal workings in place. Before we spend more time here, let's see how things work within the interface. We 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. We should be able to make adjustments via the incrementers and see the corresponding values change for the creature.
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, since the user can freely modify the value to silly numbers. We'll achieve this by setting up appropriate values for "trtMinimum" and "trtMaximum", ensuring that the user can adjust the starting value within reason.
In order to properly 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 already have the starting value in the "trtCreatur" field. However, we need to introduce a new field on the "Derived" component to track the "floor" value. Since this value will be setup appropriately by the creature, it is a simple derived value. The new field should look like below.
<field id="trtFloor" name="Creature Floor" type="derived"> </field>
We need to setup the "floor" value every evaluation cycle, just like we're already doing for the "trtCreatur" field. This is done by augmenting the existing Eval script on the "Creature" component. We simply need to assign the appropriate values to the field for each derived trait, which is accomplished by adding the following lines of code to the script.
~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
We can now utilize these 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>
Reload the data files and use our test creature. 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.
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 value. 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>
We should also verify our delta is working properly. Reload the data files and select our test creature. 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.
Let User Add Abilities
We allow the user to customize creature traits from the defaults. It would optimal if we allowed the user to do the same for creature abilities.
Dynamic Abilities Table
The first step in this process is to convert the table of abilities to be dynamic. We only want to show common abilities that are used across multiple creatures, so we'll leverage the "User.Creature" tag we defined earlier for this purpose. The changes are simple and result in the new portal below.
<portal id="abAbility" style="tblNormal"> <table_dynamic component="RaceAbil" showtemplate="SimpleItem" choosetemplate="SimpleItem"> <candidate>User.Creature</candidate> <titlebar><![CDATA[ @text = "Add a Monstrous Ability" ]]></titlebar> <headertitle><![CDATA[ @text = "Monstrous Abilities" ]]></headertitle> <additem><![CDATA[ @text = "Add Monstrous Abilities" ]]></additem> </table_dynamic> </portal>
This works great, but it doesn't handle abilities that need to be customized. For example, the "Immunity" ability needs to specify the nature of the immunity, while the "Size" ability needs to specify the rating adjustment. We need to let the user customize these abilities properly.
In order to do that, we need to identify the abilities that can be customized. We also need to identify how they can be customized. We can define a pair of tags for this purpose. The approach will be similar to the way that domains are handled, so we'll have one tag to identify when a value is needed and another to identify when text is needed. The two tags will be defined in the "User" tag group and should look like the following.
<value id="NeedText"/> <value id="NeedValue"/>
With the tags defined, we can go back to all of our abilities and flag them appropriately. The assumption we'll make is that no ability requires both a value and text - always one or the other.
We now need to expose the customization fields to the user within the table. This requires that we define a new template for the purpose. Since our needs a similar, we can clone the template used for skills and adapt it. We're going to need the usual "name", "info", and "delete" portals. In addition, we're going to need one edit portal for entering text and another for values, plus a label to show next to the edit portal. We'll use a Label script to tailor the label based on whether a value or text is required. For the name, we need to show the customized name if the pick is not user-added or the original thing name if we're showing the portals to customize the ability. In the Position script, we only show one of the two edit portals, and we only show it if the pick has been added by the user (i.e. can be deleted). This results in the template presented below.
<template id="abPick" name="Ability Pick" compset="RaceAbil" marginhorz="3" marginvert="2"> <portal id="name" style="lblNormal" showinvalid="yes"> <label> <labeltext><![CDATA[ if (isuser = 0) then @text = field[name].text else @text = field[thingname].text endif ]]></labeltext> </label> </portal> <portal id="label" style="lblSecond"> <label> <labeltext><![CDATA[ if (tagis[User.NeedText] <> 0) then @text = "Details:" else @text = "Rating:" endif ]]></labeltext> </label> </portal> <portal id="text" style="editNormal" width="100"> <edit field="abilText"> </edit> </portal> <portal id="value" style="editNormal" width="25"> <edit field="abilValue" format="integer" signed="yes"> </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 our 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 portal[info].top = 0 ~determine whether our user fields are visible ~Note: If the pick cannot be deleted, it has been bootstrapped, so we assume ~ that no users fields can be specified portal[value].visible = 0 portal[text].visible = 0 if (candelete <> 0) then if (tagis[User.NeedValue] <> 0) then portal[value].visible = 1 elseif (tagis[User.NeedText] <> 0) then portal[text].visible = 1 endif endif if (portal[value].visible + portal[text].visible = 0) then portal[label].visible = 0 endif ~position the other portals vertically perform portal[name].centervert perform portal[delete].centervert perform portal[label].centervert perform portal[text].centervert perform portal[value].centervert ~position the delete portal on the far right perform portal[delete].alignedge[right,0] ~position the info portal to the left of the delete button perform portal[info].alignrel[rtol,delete,-8] ~position the name on the left portal[name].left = 0 ~position the label and edit portals to the right of the name perform portal[label].alignrel[ltor,name,15] perform portal[value].alignrel[ltor,label,3] portal[text].left = portal[value].left ~if the ability is auto-added, change its font to indicate that fact if (candelete = 0) then perform portal[name].setstyle[lblAuto] endif ]]></position> </template>
Surrogate User Fields
Unfortunately, our new template won't compile. The problem is that we can't associated "derived" fields with edit portals - only "user" fields are allowed. So we'll change the two fields to be "user" fields. However, this creates a new problem, as we can't assign values via bootstraps to "user" fields on unique picks. We could change all of our abilities to be non-unique, but then the user could assign the any ability multiple times, even when doing so is meaningless. That's not a good option.
We seem to be at a stalemate, so it's time to get creative. We need "user" fields to work with the edit portals, and we need "derived" fields to be assigned via bootstraps. It looks like the solution is to have both. We'll start by defining a pair of new fields on the "RaceAbil" component, as shown below.
<field id="abilUsrVal" name="User Value" type="user"> </field> <field id="abilUsrTxt" name="User Text" type="user" maxlength="25"> </field>
Once we hook up the edit portals to these two fields, the compiler is happy. We can reload our files and actually enter values for abilities that we add. Now we need to connect our "user" fields with the "derived" fields that are used by the Eval scripts on the various abilities.
We can accomplish this by defining an Eval script on the "RaceAbil" component. This script will copy the contents of the "user" fields into the corresponding "derived" fields. Since "user" fields are always in place from very beginning, we'll schedule the script to occur early in the evaluation cycle, ensuring that the "derived" fields contain the proper values when accessed. The one thing we need to be careful of, though, is that we must not copy the field values if the user did not add the pick. If we do, then we'll overwrite the values assigned via the bootstraps with empty values, since no user values will exist for those abilities. Putting this all together yields the Eval script shown below.
<eval index="3" phase="Setup" priority="5000"><![CDATA[ ~if this pick was NOT user-added, no user fields can be accessed if (isuser = 0) then done endif ~if we have a user value, put it into the derived field if (tagis[User.NeedValue] <> 0) then field[abilValue].value = field[abilUsrVal].value ~if we have user text, put it into the derived field elseif (tagis[User.NeedText] <> 0) then field[abilText].text = field[abilUsrTxt].text endif ]]></eval>
With the script in place, we can reload the data files and everything works the way we want it. We can easily mix and match bootstrapped abilities with user-added abilities, and the user can properly customize any abilities that he adds.