Final Cleanup Continued (Savage): Difference between revisions
(14 intermediate revisions by the same user not shown) | |||
Line 2: | Line 2: | ||
===Overview=== | ===Overview=== | ||
The testing and cleanup process continues.... | |||
===Identify Hindrance Severity in Sheet Output=== | ===Identify Hindrance Severity in Sheet Output=== | ||
Line 116: | Line 118: | ||
Since we've gone through and analyzed all these dependencies, we'll do one additional thing. Each of the various scripts we identified should be named. Then we can specify appropriate timing dependencies between these scripts. Now we can let the compiler safety check our timing dependencies for us all the time. If we need to adjust the timing of one of these scripts in the future, we can rely on the compiler to let us know if we messed something up. | Since we've gone through and analyzed all these dependencies, we'll do one additional thing. Each of the various scripts we identified should be named. Then we can specify appropriate timing dependencies between these scripts. Now we can let the compiler safety check our timing dependencies for us all the time. If we need to adjust the timing of one of these scripts in the future, we can rely on the compiler to let us know if we messed something up. | ||
===Custom Domains for Hindrances=== | |||
There are quite a few hindrances that utilize domains, and the domain for each is quite a bit different from the others. We currently just use the label "Domain" for each hindrance, but it would be great to customize the term for each one. We already used the "DomainTerm" tag on the "Skills" component, but we can also use the tag on individual things. | |||
To support a varying domain term for each hindrance, all we need to do is modify the template that displays the hindrances and add the appropriate tag to each hindrance. Modifying the template simply requires us to change the "lbldomain" portal to use a Label script instead of a fixed string. If a "DomainTerm" tag is found, we use the term, else we default to the original "Domain:" label. The revised portal is shown below. | |||
<pre> | |||
<portal | |||
id="lbldomain" | |||
style="lblSecond"> | |||
<label> | |||
<labeltext><![CDATA[ | |||
if (tagis[DomainTerm.?] <> 0) then | |||
@text = tagnames[DomainTerm.?] & ":" | |||
else | |||
@text = "Domain:" | |||
endif | |||
]]></labeltext> | |||
</label> | |||
</portal> | |||
</pre> | |||
Since the "DomainTerm" tag group is dynamic, we don't need to define all the tags within the group. We can instead define them whenever we need them. So we can go through all the various hindrances and add a custom tag to each, which will then be used throughout HL. For example, the "Phobia" hindrance could be assigned a "Fear" domain term via the tag below. | |||
<pre> | |||
<tag group="DomainTerm" tag="Fear"/> | |||
</pre> | |||
===Edges Need Domain Support=== | ===Edges Need Domain Support=== | ||
Line 157: | Line 187: | ||
===Buying Off Hindrances=== | ===Buying Off Hindrances=== | ||
All the various advancements appear to be working, but, there's one situation we haven't dealt with yet. Although not in the official list of advancement options, the rules indicate that it is possible to spend an advance to "buy off" a hindrance. If a GM allows this option, we need to provide a way to handle it within the data files. | |||
Unless the user adds a new advance, our validation logic will continue to report an error. So the easiest way to solve this is to add a new advance for the purpose of buying off a hindrance. This advance won't have any special behaviors, as it's really just a placeholder to consume a slot. However, we should allow the user to annotate the hindrance that was actually bought off and show it within the name of the advance. | |||
Fortunately, the Skeleton files provide something simple like this that we can use. It's referred to as a "Notation" advance, which simply displays an edit field for the user to enter some text (such as the name of the hindrance that was bought off). We can define a new advancement that leverages this mechanism, as shown below. | |||
<pre> | |||
<thing | |||
id="advBuyOff" | |||
name="Buy Off Hindrance" | |||
compset="Advance" | |||
description="Description goes here"> | |||
<fieldval field="advAction" value="Buy Off Hindrance"/> | |||
<fieldval field="advCost" value="1"/> | |||
<tag group="Advance" tag="Notation"/> | |||
<child entity="Advance"> | |||
</child> | |||
</thing> | |||
</pre> | |||
This works great, but a bit of testing reveals another problem. If the user takes a hindrance at character creation and then buys it off via an advance, he'll need to delete it. The entails switching back to creation mode, deleting, the hindrance, and then switching back. Unfortunately, once the hindrance is deleted, the number of rewards taken won't match the hindrances and the users won't be able to return to advancement mode. | |||
The solution here is to define a new hindrance that serves as a placeholder for another hindrance that is bought off via an advance. This new hindrance can be added after the original hindrance is deleted and all the validation checks will be satisfied. This new hindrance must be fully customizable. We also don't want it to appear on the "Special" tab or within character sheet output. This results in the new hindrance shown below. | |||
<pre> | |||
<thing | |||
id="hinBuyOff" | |||
name="– Buy Off –" | |||
compset="Hindrance" | |||
summary="Buy off a previous hindrance" | |||
description="Description goes here"> | |||
<fieldval field="hinMajor" value="0"/> | |||
<tag group="User" tag="UserSelect"/> | |||
<tag group="User" tag="NeedDomain"/> | |||
<tag group="DomainTerm" tag="Hindrance"/> | |||
<tag group="Hide" tag="Special"/> | |||
<tag group="Print" tag="NoPrint"/> | |||
</thing> | |||
</pre> | |||
===Injuries For NPCs=== | |||
When we first added support for injuries, treating them as advancements made perfect sense. Unfortunately, with the support of NPCs, treating them as advancements doesn't work very well unless the character happens to be a PC. The reason for this is that advancements are not applicable to NPCs, so we hide the entire tab. | |||
We need to allow injuries to be assigned to NPCs via some mechanism. Since advancements are intended for the application of permanent injuries, it would also be helpful to provide a convenient way to apply temporary injuries to PCs. | |||
The first idea that comes to mind is to add the injuries as in-play adjustments. The problem with this approach is that we can only modify existing picks via adjustments. Menus do not let us add new picks to the character, so we would have to improvise something really creative in order to get injuries like "Hideous Scar" to work (i.e. to bootstrap the hindrance onto the character). | |||
Adding a separate table of injuries to the "In-Play" tab would consume a good amount of space, and the tab is already pretty cramped. There might be plenty of room on a big screen monitor, but we have to be mindful that some users will have HL running on a small laptop at the game table. Consequently, we have to make sure everything will work on a relatively small screen. | |||
Looking through everything, there is one convenient place where we could easily add injuries. On the "Personal" tab, there is a gap beneath the image gallery that would fit a small table of injuries very easily. We'll anchor the injury list to the bottom, immediately above the permanent adjustments list. That way, we can show as many character images as we have space for. For PCs, we'll clearly label the table as "Temporary Injuries", just to be safe. | |||
There are a number of steps involved, but the first thing we'll do is create a new sort set. Our table of injuries will potentially exist in a very small space. When injuries are added to the table, they will be sorted alphabetically by default. What we want is to show the injuries with the most recent ones at the top. That way, the user will be able to easily identify wounds from the most recent combat. The Kit provides a special tag group with the unique id "_CreateSeq" that allows the sorting of items in the order they were created. By reversing this order, we can have a sort set that gives us exactly what we want, as shown below. | |||
<pre> | |||
<sortset | |||
id="MostRecent" | |||
name="Reverse Chronological Order"> | |||
<sortkey isfield="no" id="_CreateSeq" isascend="no"/> | |||
</sortset> | |||
</pre> | |||
With the sort set in place, we'll define a table portal in which we can manage injuries on the "Personal" tab. We can easily use the "SimpleItem" template for showing the selected injuries, and we can use the "LargeItem" template when the user chooses injuries. The resulting table portal is quite simple should look like the following. | |||
<pre> | |||
<portal | |||
id="peInjury" | |||
style="tblNormal"> | |||
<table_dynamic | |||
component="Injury" | |||
showtemplate="SimpleItem" | |||
choosetemplate="LargeItem" | |||
showsortset="MostRecent"> | |||
<titlebar><![CDATA[ | |||
@text = "Select a New Injury from the List Below" | |||
]]></titlebar> | |||
<headertitle><![CDATA[ | |||
if (hero.tagis[Hero.PC] <> 0) then | |||
@text = "Temporary Injuries" | |||
else | |||
@text = "Injuries" | |||
endif | |||
]]></headertitle> | |||
<additem><![CDATA[ | |||
@text = "Add New Injury" | |||
]]></additem> | |||
</table_dynamic> | |||
</portal> | |||
</pre> | |||
Our final task is to integrate the new table portal into the layout on the "Personal" tab. We're inserting the portal between the images and adjustments tables, so we need to ensure that the tab order reflects that when we add our "portalref" element. After that, we can readily splice the logic for the new portal into the Position script for the layout. The revised logic that includes the impacted script code is presented below. | |||
<pre> | |||
~reserve a width of 185 pixels for the user images table | |||
portal[peImages].width = 185 | |||
~use the same horizontal space for the injuries table | |||
portal[peInjury].width = portal[peImages].width | |||
=== | ~position the injuries table immediately above the permanent adjustments and | ||
~reserve space for at least two items | |||
portal[peInjury].left = width - portal[peInjury].width | |||
portal[peInjury].maxrows = 2 | |||
portal[peInjury].top = portal[peAdjust].top - portal[peInjury].height - 10 | |||
~position the images table in the upper right corner | |||
portal[peImages].left = width - portal[peImages].width | |||
portal[peImages].height = portal[peInjury].top - portal[peImages].top - 10 | |||
~if there is vertical space for more injuries, take advantage of it | |||
portal[peInjury].height = portal[peAdjust].top - portal[peImages].bottom - 20 | |||
portal[peInjury].top = portal[peAdjust].top - portal[peInjury].height - 10 | |||
</pre> | |||
===Hiding Equipment=== | |||
Various creatures define their own custom equipment, such as the "Goblin" and "Lich". This gear needs to be private to the particular creature and not make public for general selection by the user. To solve this, we need to define a new "Hide.Equipment" tag and then assign that tag to all such gear. | |||
Once the tags are assigned, we need to modify all of the table portals on the "Armory" tab. This new tag must be integrated into the Candidate tag expression of each portal so that equipment with the tag is not shown for selection. In each case, the revised element should look like the one shown below. | |||
<pre> | |||
<candidate inheritlist="yes"><![CDATA[ | |||
!Equipment.Natural & !Hide.Equipment | |||
]]></candidate> | |||
</pre> | |||
===Show Linked Attributes on Skills=== | |||
On the "Skills" tab, we neglected to show the linked attribute with each skill. While not critical, having the linked skill visible can be helpful. We should also include the linked attribute within the description text for each skill. | |||
We'll start by modifying the description text. We can define a new procedure to synthesize the information for skills and integrate it into the "Descript" procedure when a skill is processed. All we need to do is add the one piece of data, which is solved with the simple procedure below. | |||
<pre> | |||
<procedure id="InfoSkill" context="info"><![CDATA[ | |||
~declare variables that are used to communicate with our caller | |||
var iteminfo as string | |||
iteminfo = "" | |||
~report the linked attribute | |||
iteminfo &= "Linked Attribute: " & linkage[attribute].field[name].text & "{br}" | |||
]]></procedure> | |||
</pre> | |||
There are two different places where we should show the linked attribute within tables. The first is within the list of selected skills that appears on the "Skills" tab. The other is within the table presented for the user to select skills to be added. | |||
The linked attribute is a secondary piece of information, so it should be less prominent than the other facets of the skill. As such, we'll add it in a separate portal that appears at the far right and in a softer color. Within the "skPick" template that shows the skills added to the character, we can add the portal shown below. | |||
<pre> | |||
<portal | |||
id="attribute" | |||
style="lblSecond"> | |||
<label> | |||
<labeltext><![CDATA[ | |||
@text = "(" & linkage[attribute].field[trtAbbrev].text & ")" | |||
]]></labeltext> | |||
</label> | |||
</portal> | |||
</pre> | |||
Once the portal is added, we can integrate it into the Position script for the template. There are a few things we need to tweak to complete the integration. The pertinent lines of the script are shown below. | |||
<pre> | |||
~position the other portals vertically | |||
... | |||
perform portal[attribute].centervert | |||
... | |||
~position the attribute portal to the left of the info button | |||
perform portal[attribute].alignrel[rtol,info,-8] | |||
... | |||
~position the name next to the adjustment | |||
perform portal[name].alignrel[ltor,adjust,8] | |||
~if we don't need a domain, hide it and let the name use all available space | |||
if (tagis[User.NeedDomain] = 0) then | |||
portal[lbldomain].visible = 0 | |||
portal[domain].visible = 0 | |||
portal[name].width = minimum(portal[name].width,portal[attribute].left - portal[name].left - 10) | |||
~otherwise, position the domain portals next to the name | |||
else | |||
perform portal[lbldomain].alignrel[ltor,name,15] | |||
perform portal[domain].alignrel[ltor,lbldomain,2] | |||
portal[domain].width = minimum(150,portal[attribute].left - portal[domain].left - 5) | |||
endif | |||
</pre> | |||
Adding the linked attribute to the table for choosing skills requires a little bit more work. The "skSkills" table portal currently just uses the "SimpleItem" template for selection. In order to add the linked attribute, we'll need to create our own new template. We can do this easily by copying the "SimpleItem" template and calling it "skThing". Once that's done, we can modify the "skSkills" portal to reference our new template for choosing a skill. | |||
The final thing we need to do is revise the template for our purposes. We can get rid of all the portals except for the name, then we can add a new portal similar to the one we added above for showing the linked attribute. When we're finished, the new template should look similar to the one below. | |||
<pre> | |||
<template | |||
id="skThing" | |||
name="Skill Thing" | |||
compset="Skill" | |||
marginhorz="3" | |||
marginvert="2"> | |||
<portal | |||
id="name" | |||
style="lblNormal" | |||
showinvalid="yes"> | |||
<label | |||
field="name"> | |||
</label> | |||
</portal> | |||
<portal | |||
id="attribute" | |||
style="lblSecond"> | |||
<label> | |||
<labeltext><![CDATA[ | |||
@text = linkage[attribute].field[name].text | |||
]]></labeltext> | |||
</label> | |||
</portal> | |||
<position><![CDATA[ | |||
~set up our height based on our tallest portal | |||
height = portal[name].height | |||
~if this is a "sizing" calculation, we're done | |||
if (issizing <> 0) then | |||
done | |||
endif | |||
~center the portals vertically | |||
perform portal[name].centervert | |||
perform portal[attribute].centervert | |||
~position the attribute portal on the far right | |||
perform portal[attribute].alignedge[right,0] | |||
~position the name on the left and let it use all available space | |||
portal[name].left = 0 | |||
portal[name].width = minimum(portal[name].width,portal[attribute].left - 10) | |||
]]></position> | |||
</template> | |||
</pre> | |||
===Hindrance Validation Incorrect=== | |||
The validation logic we added for hindrances works great when there are only user-added hindrances on the character. The moment that new hindrances are added via advancements and/or injuries, the logic fails. The problem is that we forgot to take into account these other types of hindrances within the validation rule. Fortunately, we can easily detect they other types of hindrances. The code below shows the revised logic needed within the script for tallying up the hindrances we care about. | |||
<pre> | |||
~iterate through all hindrances and tally up the number of majors and minors | |||
~Note: We must only tally hindrances that are user added and not an advance. | |||
foreach pick in hero where "component.Hindrance" | |||
if (eachpick.isuser + !eachpick.tagis[Advance.?] >= 2) then | |||
if (eachpick.field[hinMajor].value = 0) then | |||
minor += 1 | |||
else | |||
major += 1 | |||
endif | |||
endif | |||
nexteach | |||
</pre> | |||
===Skill Points Must Ignore Advancement=== | |||
Our skill points are tallying up fine during character creation, but we run into a problem once the character starts taking advances. The issue is that any attribute advances apply that advance to the "trtBonus" field. That's the same field we rely on to indicate when racial adjustments are applied to the character. The net result is that an attribute advance is being interpreted the same as a bonus at character creation, so our skill points tally becomes wrong once the advance is taken. | |||
You might be wondering what the problem is with this, since the tracking of skill points isn't important once a character is created and enters advancement mode. The issue arises if the user decides to switch back to creation mode for some reason, make an adjustment, and then switch back to advancement mode. Once the user switches to creation mode, he won't be allowed to return to advancement mode due to the incorrect calculation. There is also the niggly issue of an errant validation error being reported. | |||
This problem goes beyond advancements, though. The same issue will arise with injuries or any other permanent adjustment of the "trtBonus" field that occurs after character creation is completed. So we can't just fix this within the advancement mechanism. We need a more general solution that differentiates between bonuses applied during creation and post-creation. | |||
The solution to this problem is actually pretty easy. We can define a new field to track only the bonuses applied during character creation. Anything that needs to apply a creation bonus must adjust this new field instead of "trtBonus". The new field should look like below. | |||
<pre> | |||
<field | |||
id="trtCreate" | |||
name="Bonus During Creation" | |||
type="derived"> | |||
</field> | |||
</pre> | |||
To make use of this field as easy as possible, we'll define a new script macro to access the creation bonus, as shown below. | |||
<pre> | |||
<scriptmacro | |||
name="traitcreation" | |||
param1="trait" | |||
result="hero.child[#trait].field[trtCreate].value"/> | |||
</pre> | |||
With the macro in place, we can apply the appropriate changes to the various racial abilities. For example the Eval script for the "abSpirited" ability will change the code below. | |||
<pre> | |||
#traitcreation[attrSpi] += 1 | |||
</pre> | |||
We can then define an Eval script on the "Traits" component that adds the creation bonus to the total bonus at an appropriate point in the evaluation cycle. This will ensure that all of the logic that keys on "trtBonus" remains perfectly valid. The best time to schedule this script is probably at the very beginning of the Traits phase, yielding the script below. | |||
<pre> | |||
<eval index="6" phase="Traits" priority="1"><![CDATA[ | |||
field[trtBonus].value += field[trtCreate].value | |||
]]></eval> | |||
</pre> | |||
The final step is to modify the skill point calculation to use the new field instead of "trtBonus". This is a simple swap-out of one field for the other. The impacted line of code is shown below. | |||
<pre> | |||
attrib += linkage[attribute].field[trtCreate].value | |||
</pre> | |||
After a quick reload, our skill point calculations should be operating smoothly after advancement changes modify attributes. |
Latest revision as of 12:30, 12 February 2009
Context: HL Kit … Authoring Examples … Savage Worlds Walk-Through
Overview
The testing and cleanup process continues....
Identify Hindrance Severity in Sheet Output
The list of hindrances that are output within the character sheet use the same mechanism as edges and racial abilities. However, hindrances can be taken in both major and minor forms, and that aspect of each hindrance is important to the player.
We can modify the Label script currently in use within the "oAbilPick" template to detect the presence of a hindrance and output it specially. We'll only flag major hindrances, and we'll do it by including a special symbol. This will keep the space requirements to an absolute minimum. We can pick a suitable character from the "Wingdings" font, resulting in the following revised Label script.
var major as string if (tagis[component.Hindrance] <> 0) then if (field[hinMajor].value <> 0) then major = " {font Wingdings}" & chr(181) & "{revert}" endif endif @text = field[shortname].text & major & "{/b} {size 32}" & field[summary].text
Add Drawbacks to Character Sheet
It seems that we overlooked including the details of arcane drawbacks on the character sheet. Since it's something the player should not forget, we should probably include a reminder. One solution would be to show the drawback like a hindrance. Another would be to include the name of the drawback within the title above the table of arcane powers. And a third would be to add a new table that listed the drawback just like an edge or hindrance.
None of these options is ideal, and they each have their trade-offs. Treating drawbacks as hindrances would entail a fair amount of work to revise the data files. Since all we need is to display them, the extra work probably isn't worth it. Including the drawback within the title above the arcane powers table would not show its summary, plus it would result in a very cramped title area. Adding a new table is quick and easy, but the downside is that we'll consume additional vertical space on the sheet.
We'll go with the new table for the sake of simplicity. Our table portal can be adapted from the edges table, giving us the following.
<portal id="oDrawback" style="outNormal"> <output_table component="Drawback" showtemplate="oDrawPick"> <headertitle><![CDATA[ @text = "Arcane Drawbacks" ]]></headertitle> </output_table> </portal>
The template can be readily adapted from the one used for special abilities. This results in the template shown below.
<template id="oDrawPick" name="Output Drawbacks Table" compset="Drawback" marginvert="2"> <portal id="details" style="outMedLt"> <output_label> <labeltext><![CDATA[ @text = field[name].text & "{/b} {size 32}" & field[summary].text ]]></labeltext> </output_label> </portal> <position><![CDATA[ ~our details width spans the entire template width portal[details].width = width ~our height is the height of our portal height = portal[details].bottom ]]></position> </template>
The last step is to integrate the portal into a layout. We'll add it to the "oRightSide" layout and place immediately above the table of arcane powers. If we include it above the powers, it will always be prominently visible and immediately adjacent to the list of powers. All we need to do is add the "portalref" element and then auto-place the portal immediately beneath the continuation of the edges output.
Ignoring Wound Penalties
There are two edges that allow the character to ignore wound penalties (Nerves of Steel and the improved version). Since HL automatically applies the effects of all wounds penalties to rolls, users will assume the effects of these edges are properly factored in. We'd better make good on that assumption.
The wound and fatigue penalties are all handled via the "acNetPenal" field on the "Actor" component. We can add a new field to track the number of wound penalties that are ignored, and then we can factor that value into the final calculation for "acNetPenal". Our new field is shown below.
<field id="acIgnWound" name="Ignored Wound Penalties" type="derived"> </field>
We can now modify the Calculate script on the "acNetPenal" field to take the ignored wounds into account. The revised script should look like the following.
@value = -field[acFatigue].value if (field[acWounds].value > field[acIgnWound].value) then @value -= field[acWounds].value - field[acIgnWound].value endif
All the mechanics are in place. Our two edges can now use a simple Eval script to adjust the "acIgnWound" field, and everything works exactly as we want.
<eval index="1" phase="Setup" priority="5000"><![CDATA[ herofield[acIgnWound].value += 1 ]]></eval>
Dependencies on Load Limit
The "Acrobat" edge exposes a timing issue with our data files. The edge confers a +1 to a character's Parry if the character has no encumbrance penalty. To implement this, we need to have the encumbrance penalty determined before we act, and we need to act before the final value for the Parry trait is resolved. Our current script timing does not work this way, although we should be able to do this.
Our goal is to be able to adjust the Parry trait prior to its final resolution. Derived traits are officially calculated at a timing of Traits/6000. So we need to get the encumbrance penalty resolved before then if at all possible.
The encumbrance penalty is tracked by the "resEncumb" resource. The final value is currently calculated at a timing of Final/500. The only timing requirements for this calculation are the accrual of weight carried (which occurs at Effects/10000) and the load limit (which occurs at Traits/7000). The weight accrual occurs long before we need to act for the "Acrobat" edge, so we can move the timing of the penalty if we can move the timing of the load limit calculation.
The load limit is tracked by the "resLoadLim" resource and it calculates its maximum (the value we need for the encumbrance penalty) at a timing of Traits/7000. This calculation depends on the "acLoadMult" field, which is nailed down very early in the evaluation cycle, so that's not a problem. It also depends on the final value of the Strength attribute. Attributes are finalized within the "trtFinal" field, which is calculated at a timing of Traits/3000. This means we can move the load limit calculation to a timing of Traits/4000 without any problems.
Once we move the load limit calculation, we can then safely move the encumbrance penalty calculation. We'll move that to a timing of Traits/4500. This leaves nice gap in the evaluation cycle where we can inspect the encumbrance and adjust the Parry trait based on it. We'll schedule the Eval script for the edge at Traits/5000.
Since we've gone through and analyzed all these dependencies, we'll do one additional thing. Each of the various scripts we identified should be named. Then we can specify appropriate timing dependencies between these scripts. Now we can let the compiler safety check our timing dependencies for us all the time. If we need to adjust the timing of one of these scripts in the future, we can rely on the compiler to let us know if we messed something up.
Custom Domains for Hindrances
There are quite a few hindrances that utilize domains, and the domain for each is quite a bit different from the others. We currently just use the label "Domain" for each hindrance, but it would be great to customize the term for each one. We already used the "DomainTerm" tag on the "Skills" component, but we can also use the tag on individual things.
To support a varying domain term for each hindrance, all we need to do is modify the template that displays the hindrances and add the appropriate tag to each hindrance. Modifying the template simply requires us to change the "lbldomain" portal to use a Label script instead of a fixed string. If a "DomainTerm" tag is found, we use the term, else we default to the original "Domain:" label. The revised portal is shown below.
<portal id="lbldomain" style="lblSecond"> <label> <labeltext><![CDATA[ if (tagis[DomainTerm.?] <> 0) then @text = tagnames[DomainTerm.?] & ":" else @text = "Domain:" endif ]]></labeltext> </label> </portal>
Since the "DomainTerm" tag group is dynamic, we don't need to define all the tags within the group. We can instead define them whenever we need them. So we can go through all the various hindrances and add a custom tag to each, which will then be used throughout HL. For example, the "Phobia" hindrance could be assigned a "Fear" domain term via the tag below.
<tag group="DomainTerm" tag="Fear"/>
Edges Need Domain Support
When we implemented edges, we identified all the ones that looked to be special in some way and added appropriate handling for them. We missed one. The "Connections" edge requires that the user specify the organization with which the connections exist. This means we have to add domain support for edges. Fortunately, we've already done that a couple times, so it should not be complicated. In fact, it's rather simple.
The first thing we need to do is add the "Domain" component to the "Edge" component set. This will integrate domain handling internally into edges. The "Domain" component handles modifying the name, so all the mechanics we need are provided.
Next, we need to add support for domains to the interface. We can copy the two portals dealing with domains from the "edHinder" template and add them to the "edEdge" template. Then we can copy the positioning code for those portals, which we can drop into place with a single change. The "lbldomain" portal can be aligned directly relative to the "name" portal instead of using the non-existent "edge" variable.
Domain support is fully operational for edges now. All that remains to be done is assign the "User.NeedDomain" tag to the "Connections" edge. Once we do that, the edge will prompt the user for a domain and integrate into the name for display.
Background Edges are Creation-Only
All background edges are generally only valid for selection during character creation. The Skeleton files provide built-in support for designating abilities as creation-only (via the "User.CreateOnly" tag). However, adding the tag individually to all background edges is error-prone. It would be best if we automatically treated all background edges as creation-only.
Doing this is easy. We can clone the pre-requisite used for this purpose within the "Ability" component and add it to the "Edge" component. We can then adapt it to only apply to background edges. Since GMs can allow the selection of background edges after creation, we'll only flag an error when showing things. Once an edge is added, we'll assume the player did so with the GM's approval. The resulting pre-requisite should look like below.
<prereq message="Background edges are only available at character creation"> <!-- This pre-req is only applicable to background edges --> <match><![CDATA[ EdgeType.Background ]]></match> <validate><![CDATA[ ~we only report a failure on things (once added, we assume the user knows best) if (@ispick <> 0) then @valid = 1 done endif ~if the mode is creation, we're valid if (state.iscreate <> 0) then @valid = 1 endif ]]></validate> </prereq>
Buying Off Hindrances
All the various advancements appear to be working, but, there's one situation we haven't dealt with yet. Although not in the official list of advancement options, the rules indicate that it is possible to spend an advance to "buy off" a hindrance. If a GM allows this option, we need to provide a way to handle it within the data files.
Unless the user adds a new advance, our validation logic will continue to report an error. So the easiest way to solve this is to add a new advance for the purpose of buying off a hindrance. This advance won't have any special behaviors, as it's really just a placeholder to consume a slot. However, we should allow the user to annotate the hindrance that was actually bought off and show it within the name of the advance.
Fortunately, the Skeleton files provide something simple like this that we can use. It's referred to as a "Notation" advance, which simply displays an edit field for the user to enter some text (such as the name of the hindrance that was bought off). We can define a new advancement that leverages this mechanism, as shown below.
<thing id="advBuyOff" name="Buy Off Hindrance" compset="Advance" description="Description goes here"> <fieldval field="advAction" value="Buy Off Hindrance"/> <fieldval field="advCost" value="1"/> <tag group="Advance" tag="Notation"/> <child entity="Advance"> </child> </thing>
This works great, but a bit of testing reveals another problem. If the user takes a hindrance at character creation and then buys it off via an advance, he'll need to delete it. The entails switching back to creation mode, deleting, the hindrance, and then switching back. Unfortunately, once the hindrance is deleted, the number of rewards taken won't match the hindrances and the users won't be able to return to advancement mode.
The solution here is to define a new hindrance that serves as a placeholder for another hindrance that is bought off via an advance. This new hindrance can be added after the original hindrance is deleted and all the validation checks will be satisfied. This new hindrance must be fully customizable. We also don't want it to appear on the "Special" tab or within character sheet output. This results in the new hindrance shown below.
<thing id="hinBuyOff" name="– Buy Off –" compset="Hindrance" summary="Buy off a previous hindrance" description="Description goes here"> <fieldval field="hinMajor" value="0"/> <tag group="User" tag="UserSelect"/> <tag group="User" tag="NeedDomain"/> <tag group="DomainTerm" tag="Hindrance"/> <tag group="Hide" tag="Special"/> <tag group="Print" tag="NoPrint"/> </thing>
Injuries For NPCs
When we first added support for injuries, treating them as advancements made perfect sense. Unfortunately, with the support of NPCs, treating them as advancements doesn't work very well unless the character happens to be a PC. The reason for this is that advancements are not applicable to NPCs, so we hide the entire tab.
We need to allow injuries to be assigned to NPCs via some mechanism. Since advancements are intended for the application of permanent injuries, it would also be helpful to provide a convenient way to apply temporary injuries to PCs.
The first idea that comes to mind is to add the injuries as in-play adjustments. The problem with this approach is that we can only modify existing picks via adjustments. Menus do not let us add new picks to the character, so we would have to improvise something really creative in order to get injuries like "Hideous Scar" to work (i.e. to bootstrap the hindrance onto the character).
Adding a separate table of injuries to the "In-Play" tab would consume a good amount of space, and the tab is already pretty cramped. There might be plenty of room on a big screen monitor, but we have to be mindful that some users will have HL running on a small laptop at the game table. Consequently, we have to make sure everything will work on a relatively small screen.
Looking through everything, there is one convenient place where we could easily add injuries. On the "Personal" tab, there is a gap beneath the image gallery that would fit a small table of injuries very easily. We'll anchor the injury list to the bottom, immediately above the permanent adjustments list. That way, we can show as many character images as we have space for. For PCs, we'll clearly label the table as "Temporary Injuries", just to be safe.
There are a number of steps involved, but the first thing we'll do is create a new sort set. Our table of injuries will potentially exist in a very small space. When injuries are added to the table, they will be sorted alphabetically by default. What we want is to show the injuries with the most recent ones at the top. That way, the user will be able to easily identify wounds from the most recent combat. The Kit provides a special tag group with the unique id "_CreateSeq" that allows the sorting of items in the order they were created. By reversing this order, we can have a sort set that gives us exactly what we want, as shown below.
<sortset id="MostRecent" name="Reverse Chronological Order"> <sortkey isfield="no" id="_CreateSeq" isascend="no"/> </sortset>
With the sort set in place, we'll define a table portal in which we can manage injuries on the "Personal" tab. We can easily use the "SimpleItem" template for showing the selected injuries, and we can use the "LargeItem" template when the user chooses injuries. The resulting table portal is quite simple should look like the following.
<portal id="peInjury" style="tblNormal"> <table_dynamic component="Injury" showtemplate="SimpleItem" choosetemplate="LargeItem" showsortset="MostRecent"> <titlebar><![CDATA[ @text = "Select a New Injury from the List Below" ]]></titlebar> <headertitle><![CDATA[ if (hero.tagis[Hero.PC] <> 0) then @text = "Temporary Injuries" else @text = "Injuries" endif ]]></headertitle> <additem><![CDATA[ @text = "Add New Injury" ]]></additem> </table_dynamic> </portal>
Our final task is to integrate the new table portal into the layout on the "Personal" tab. We're inserting the portal between the images and adjustments tables, so we need to ensure that the tab order reflects that when we add our "portalref" element. After that, we can readily splice the logic for the new portal into the Position script for the layout. The revised logic that includes the impacted script code is presented below.
~reserve a width of 185 pixels for the user images table portal[peImages].width = 185 ~use the same horizontal space for the injuries table portal[peInjury].width = portal[peImages].width ~position the injuries table immediately above the permanent adjustments and ~reserve space for at least two items portal[peInjury].left = width - portal[peInjury].width portal[peInjury].maxrows = 2 portal[peInjury].top = portal[peAdjust].top - portal[peInjury].height - 10 ~position the images table in the upper right corner portal[peImages].left = width - portal[peImages].width portal[peImages].height = portal[peInjury].top - portal[peImages].top - 10 ~if there is vertical space for more injuries, take advantage of it portal[peInjury].height = portal[peAdjust].top - portal[peImages].bottom - 20 portal[peInjury].top = portal[peAdjust].top - portal[peInjury].height - 10
Hiding Equipment
Various creatures define their own custom equipment, such as the "Goblin" and "Lich". This gear needs to be private to the particular creature and not make public for general selection by the user. To solve this, we need to define a new "Hide.Equipment" tag and then assign that tag to all such gear.
Once the tags are assigned, we need to modify all of the table portals on the "Armory" tab. This new tag must be integrated into the Candidate tag expression of each portal so that equipment with the tag is not shown for selection. In each case, the revised element should look like the one shown below.
<candidate inheritlist="yes"><![CDATA[ !Equipment.Natural & !Hide.Equipment ]]></candidate>
Show Linked Attributes on Skills
On the "Skills" tab, we neglected to show the linked attribute with each skill. While not critical, having the linked skill visible can be helpful. We should also include the linked attribute within the description text for each skill.
We'll start by modifying the description text. We can define a new procedure to synthesize the information for skills and integrate it into the "Descript" procedure when a skill is processed. All we need to do is add the one piece of data, which is solved with the simple procedure below.
<procedure id="InfoSkill" context="info"><![CDATA[ ~declare variables that are used to communicate with our caller var iteminfo as string iteminfo = "" ~report the linked attribute iteminfo &= "Linked Attribute: " & linkage[attribute].field[name].text & "{br}" ]]></procedure>
There are two different places where we should show the linked attribute within tables. The first is within the list of selected skills that appears on the "Skills" tab. The other is within the table presented for the user to select skills to be added.
The linked attribute is a secondary piece of information, so it should be less prominent than the other facets of the skill. As such, we'll add it in a separate portal that appears at the far right and in a softer color. Within the "skPick" template that shows the skills added to the character, we can add the portal shown below.
<portal id="attribute" style="lblSecond"> <label> <labeltext><![CDATA[ @text = "(" & linkage[attribute].field[trtAbbrev].text & ")" ]]></labeltext> </label> </portal>
Once the portal is added, we can integrate it into the Position script for the template. There are a few things we need to tweak to complete the integration. The pertinent lines of the script are shown below.
~position the other portals vertically ... perform portal[attribute].centervert ... ~position the attribute portal to the left of the info button perform portal[attribute].alignrel[rtol,info,-8] ... ~position the name next to the adjustment perform portal[name].alignrel[ltor,adjust,8] ~if we don't need a domain, hide it and let the name use all available space if (tagis[User.NeedDomain] = 0) then portal[lbldomain].visible = 0 portal[domain].visible = 0 portal[name].width = minimum(portal[name].width,portal[attribute].left - portal[name].left - 10) ~otherwise, position the domain portals next to the name else perform portal[lbldomain].alignrel[ltor,name,15] perform portal[domain].alignrel[ltor,lbldomain,2] portal[domain].width = minimum(150,portal[attribute].left - portal[domain].left - 5) endif
Adding the linked attribute to the table for choosing skills requires a little bit more work. The "skSkills" table portal currently just uses the "SimpleItem" template for selection. In order to add the linked attribute, we'll need to create our own new template. We can do this easily by copying the "SimpleItem" template and calling it "skThing". Once that's done, we can modify the "skSkills" portal to reference our new template for choosing a skill.
The final thing we need to do is revise the template for our purposes. We can get rid of all the portals except for the name, then we can add a new portal similar to the one we added above for showing the linked attribute. When we're finished, the new template should look similar to the one below.
<template id="skThing" name="Skill Thing" compset="Skill" marginhorz="3" marginvert="2"> <portal id="name" style="lblNormal" showinvalid="yes"> <label field="name"> </label> </portal> <portal id="attribute" style="lblSecond"> <label> <labeltext><![CDATA[ @text = linkage[attribute].field[name].text ]]></labeltext> </label> </portal> <position><![CDATA[ ~set up our height based on our tallest portal height = portal[name].height ~if this is a "sizing" calculation, we're done if (issizing <> 0) then done endif ~center the portals vertically perform portal[name].centervert perform portal[attribute].centervert ~position the attribute portal on the far right perform portal[attribute].alignedge[right,0] ~position the name on the left and let it use all available space portal[name].left = 0 portal[name].width = minimum(portal[name].width,portal[attribute].left - 10) ]]></position> </template>
Hindrance Validation Incorrect
The validation logic we added for hindrances works great when there are only user-added hindrances on the character. The moment that new hindrances are added via advancements and/or injuries, the logic fails. The problem is that we forgot to take into account these other types of hindrances within the validation rule. Fortunately, we can easily detect they other types of hindrances. The code below shows the revised logic needed within the script for tallying up the hindrances we care about.
~iterate through all hindrances and tally up the number of majors and minors ~Note: We must only tally hindrances that are user added and not an advance. foreach pick in hero where "component.Hindrance" if (eachpick.isuser + !eachpick.tagis[Advance.?] >= 2) then if (eachpick.field[hinMajor].value = 0) then minor += 1 else major += 1 endif endif nexteach
Skill Points Must Ignore Advancement
Our skill points are tallying up fine during character creation, but we run into a problem once the character starts taking advances. The issue is that any attribute advances apply that advance to the "trtBonus" field. That's the same field we rely on to indicate when racial adjustments are applied to the character. The net result is that an attribute advance is being interpreted the same as a bonus at character creation, so our skill points tally becomes wrong once the advance is taken.
You might be wondering what the problem is with this, since the tracking of skill points isn't important once a character is created and enters advancement mode. The issue arises if the user decides to switch back to creation mode for some reason, make an adjustment, and then switch back to advancement mode. Once the user switches to creation mode, he won't be allowed to return to advancement mode due to the incorrect calculation. There is also the niggly issue of an errant validation error being reported.
This problem goes beyond advancements, though. The same issue will arise with injuries or any other permanent adjustment of the "trtBonus" field that occurs after character creation is completed. So we can't just fix this within the advancement mechanism. We need a more general solution that differentiates between bonuses applied during creation and post-creation.
The solution to this problem is actually pretty easy. We can define a new field to track only the bonuses applied during character creation. Anything that needs to apply a creation bonus must adjust this new field instead of "trtBonus". The new field should look like below.
<field id="trtCreate" name="Bonus During Creation" type="derived"> </field>
To make use of this field as easy as possible, we'll define a new script macro to access the creation bonus, as shown below.
<scriptmacro name="traitcreation" param1="trait" result="hero.child[#trait].field[trtCreate].value"/>
With the macro in place, we can apply the appropriate changes to the various racial abilities. For example the Eval script for the "abSpirited" ability will change the code below.
#traitcreation[attrSpi] += 1
We can then define an Eval script on the "Traits" component that adds the creation bonus to the total bonus at an appropriate point in the evaluation cycle. This will ensure that all of the logic that keys on "trtBonus" remains perfectly valid. The best time to schedule this script is probably at the very beginning of the Traits phase, yielding the script below.
<eval index="6" phase="Traits" priority="1"><![CDATA[ field[trtBonus].value += field[trtCreate].value ]]></eval>
The final step is to modify the skill point calculation to use the new field instead of "trtBonus". This is a simple swap-out of one field for the other. The impacted line of code is shown below.
attrib += linkage[attribute].field[trtCreate].value
After a quick reload, our skill point calculations should be operating smoothly after advancement changes modify attributes.