Difference between revisions of "NPC Support (Savage)"

From HLKitWiki
Jump to: navigation, search
(Testing Everything)
(Calculating the XP)
Line 251: Line 251:
<eval index="7" phase="Final" priority="1000" name="calc acFinalXP"><![CDATA[
<eval index="7" phase="Final" priority="1000" name="calc acFinalXP"><![CDATA[
   ~if this is not an NPC, our final XP tally is given by the resource
   ~if this is not an NPC, our final XP tally is given by the resource
   if (field[acIsNPC].value = 0) then
   if (hero.tagis[Hero.NPC] = 0) then
     field[acFinalXP].value = #resmax[resXP]
     field[acFinalXP].value = #resmax[resXP]

Revision as of 04:44, 4 February 2009

Context: HL KitAuthoring Examples … Savage Worlds Walk-Through 


A major benefit of HL to players is the speed and simplicity of creating characters. That same benefit should exist for GMs, but the advancement mechanism of Savage Worlds requires that characters be created one advance at a time. This leaves GMs with a choice of either creating an NPC using an incremental approach or ignoring all the validation errors by just "winging it". GMs typically need lots of NPCs, so the one-at-a-time advancement method is clunky and slow. GMs also need to have a good idea of the relative power levels of NPCs compared to the PCs, so ignoring the validation errors requires the GM to have an innate sense of how powerful a given NPC is.

There must be a better way to create NPCs using our data files. What we need is a way to create a character without any restrictions and tell the GM exactly how powerful that character is. Unless a character is created in a step-wise manner, we'll never be able to determine exactly how many advances a character used to reach a particular set of abilities. However, we can make a very close estimate, which is generally all that a GM really wants.

Our Approach

We're going to allow the GM to create NPCs freely, without any of the normal restrictions that apply to new characters. This requires that we let the user tell us whether he's creating an NPC. Since this is something fundamental to the character, we should add this to the "Configure Hero" form. This also requires that we turn off various validation tests that won't be applicable to NPCs.

Based on the various increases assigned to the character, we'll calculate how many advances were needed to reach that point. These advances must be tracked beyond the normal assignments for a starting character. Based on the advances, we can then determine how many XP were required for the character, which will then tell us the rank of the NPC.

The one main problem with this approach is that we don't know the exact order in which the advances are selected for the character. For edges, this isn't a problem, because we can simply assume that any edges satisfying its pre-requisites was taken in a valid order during the character's evolution. For attributes, this also isn't a problem, since characters are allowed exactly one attribute increase per rank. Skills are where we run into a problem with the order in which advances are taken. We can't know whether a skill was increased before or after its linked attribute was increased.

This leaves us with two options for determining the number of advances: optimism or pessimism. In an optimistic model, we assume that all skills are advanced after any linked attributes are first increased. While not always realistic, this method presumes the NPC optimized his advances to get the most out of them. The alternative is a pessimistic model, where we assume all skills are increased before their linked attributes. This approach is much less realistic than the optimistic model, so we'll go with the optimistic one.

Identifying an NPC

Before we can do anything else, we need to enable the user to identify an NPC as distinct from a normal PC. This requires that we track that state somewhere within each character. The obvious solution is to use a field on the "Actor" component, just like we did for identifying wildcards. We'll assume that all characters start out as PCs, which means our default state is for the NPC field to be zero. So we'll define a new first for the purpose, as shown below.

  name="Is NPC?"

While a field value is useful in most cases for identifying an NPC, there may be times we'll want to check for an NPC via a tag expression. In fact, using a tag is generally the best solution, as it's the most general technique. So we need to define a tag to identify NPCs, and we should make a point of using the tag everywhere instead of the field for consistency. Since the tag will only ever be on the hero, we'll add it to the "Hero" tag group.

<value id="NPC"/>

We now need to assign the tag to the hero appropriately. For that, we'll define an Eval script on the "Actor" component. Within this script, we'll also take the opportunity to configure other behaviors that we want for NPCs. For example, the "Advances" tab makes zero for NPCs, so we should hide it when the character is an NPC. This results in a script like the following.

<eval index="6" phase="Initialize" priority="1000"><![CDATA[
  ~if this is not an NPC, just get out
  if (field[acIsNPC].value = 0) then

  ~assign a tag to indicate we're an NPC
  perform hero.assign[Hero.NPC]

  ~hide components of the interface that don't apply for NPCs
  perform hero.assign[HideTab.advances]

We can track whether a character is an NPC internally, so it's time to expose that the user. We decided earlier that we'll add a new option to the "Configure Hero" form (within the "cnfStart" template). We can use a simple checkbox, just like we did for the wildcard state. This checkbox will be tied to the new "acIsNPC" field and should look like the one below.

  tiptext="Check this option to construct the character as an unlimited NPC">
    message="Create Unlimited NPC?">

The next step is to place the portal appropriately within the template. However, when we take a look at the current template, there is an option in there that requires special handling. The option to specify starting XP is rather silly for an NPC. As such, we should hide that option if the user chooses to create an NPC. If we hide the portal, we then need to shift other portals around to keep everything looking good. This results in the following revised Position script for the template.

~set the width of the template to something we like
width = 185

~determine whether the starting xp is visible based on if we're an npc
portal[xp].visible = !hero.tagis[Hero.NPC]
portal[lblxp].visible = portal[xp].visible

~position the title at the top
perform portal[label].centerhorz

~position the starting cash beneath the ability slots
perform portal[cash].alignrel[ttob,label,15]
perform portal[lblcash].centeron[vert,cash]
portal[cash].width = 50
portal[lblcash].left = (width - portal[lblcash].width - portal[cash].width - 10) / 2
perform portal[cash].alignrel[ltor,lblcash,10]

~if visible, position the starting xp beneath the starting cash
var y as number
if (portal[xp].visible = 0) then
  y = portal[cash].bottom
  perform portal[xp].alignrel[ttob,cash,15]
  perform portal[lblxp].centeron[vert,xp]
  portal[xp].width = 50
  portal[lblxp].left = (width - portal[lblxp].width - portal[xp].width - 10) / 2
  perform portal[xp].alignrel[ltor,lblxp,10]
  y = portal[xp].bottom

~position the wildcard checkbox beneath the starting xp
perform portal[iswild].centerhorz
portal[iswild].top = y + 15

~position the npc checkbox beneath the wildcard
perform portal[isnpc].centerhorz
perform portal[isnpc].alignrel[ttob,iswild,14]

~set the height of the template based on the extent of the portals
~Note: Include a little extra space at the bottom for borders and such.
height = portal[isnpc].bottom + 3

This looks good, but it's a little bit tight with the menu for the specifying whether the character is an ally or enemy of the party. We can increase the gap easily within the Position script for the layout. This is accomplished by changing the top position of the "cnfAlly" portal, which results in the new line of code shown below.

portal[cnfAlly].top = template[cnfStart].bottom + 20

The final thing we need to do here is add a small safeguard. If the user enters a value for the starting XP and then selects an NPC, the portal will disappear and the character will still have a non-zero starting XP. The solution is to forcibly zero out the field if the user ever selects the NPC option. We can do this in the Eval script we added to the "Actor" component earlier by just setting the field value.

Unfortunately, the compiler complains when we try to set the field value. This is because it's a "user" field, and such should generally only be modified by the user, so an error is reported. Fortunately, we can tell the compiler we know what we're doing via use of the "trustme" statement. This results in the following lines of code being added to the end of the Eval script.

~force the starting XP field to zero in case the user has modified it
field[acStartXP].value = 0

Showing Resources Differently

Once we designate a character as an NPC, we'll immediately notice a variety of behaviors that need to be modified for NPCs. The majority of these behaviors center around the way we show information to the user. For example, all the validation errors for attribute points, skill points, and edges are no longer applicable. The display of the allocation of points to those categories on the Basics tab are now inappropriate. In addition, the title bars above attributes, skills, and edges that show the number of points left to allocate are now erroneous when we go over the starting number of points for each.

Your first thought is likely to be that we need to go through and change all of these different places to display the proper information. While that's a valid solution, it's also not the best one. The one thing that all these places have in common is that they rely on various resources that we use to track the points that are allocated by the user. If we could simply modify the way resources are handled in general, we'd be able to resolve this much more easily.

The problem with this tactic is that we use resources for multiple different situations. If we change resources in general, then we'll change them for everything. We don't want the handling of arcane powers or rewards being changed for NPCs. A character still needs the proper edges assigned to gain powers, and hindrances still need to be selected to gain offsetting rewards.

What we need is a way to customize the behavior of resources for only a specific set of resources. Fortunately, we can accomplish this without a lot of work through the use of tags. We can define a new tag that identifies a resource as being special for NPCs. Then we can assign that tag only to the individual resources that require the special handling. Within the various facets of the "Resource" component, we can check this tag and the nature of the character, using the alternate behaviors as appropriate.

We'll start by defining the new tag. Since we might find other places besides resources where we need to do something like this, we'll give it a unique id that indicates its general purpose. We'll add the tag to the "Helper" tag group, since it really doesn't belong anywhere else. The new tag will look like below.

<value id="NPCImpact"/>

Once the tag is defined, we need to put it to use. The question is which resources need to be handled specially. Looking at the nature of NPCs, there appear to be four factors that we have to handle differently. These are attributes, skills, edges, and advances. The first three of these need to be displayed differently for NPCs, so we'll identify them appropriately by assigning our new tag to each of the three things for those resources.

<tag group="Helper" tag="NPCImpact"/>

Advances are special, though. When a character is an NPC, we want the character to behave as if advances don't exist. We've already hidden the "Advances" tab panel, so we should also hide the corresponding resource from display on the "Basics" tab. We can do this by adding a ContainerReq test to the "resAdvance" thing. If the character is an NPC, then we want the thing to behave as if it doesn't exist. This results in the addition of the following to the thing.

<containerreq phase="Initialize" priority="2000">

We can now look at the "Resource" component and decide what changes need to be made. In all cases, we'll only leverage the alternate behavior when two separate conditions are both satisfied. First of all, the resource must possess the "Helper.NPCImpact" tag. Secondly, the character must possess the "Hero.NPC" tag. This means that we'll always use a test that looks like the following.

if (tagis[Helper.NPCImpact] + hero.tagis[Hero.NPC] >= 2) then
  ~use alternate behavior for NPCs
  ~use normal behavior for PCs

Scanning through the "Resource" component from top to bottom, the first place where we'll need to tweak the behavior is in the Finalize script for the "resAddItem" field. If we have an NPC, we don't want to use any special highlighting, so we want to treat the table the same way as if all the points had been properly allocated. The code below reflects this change to the impacted lines.

if (tagis[Helper.NPCImpact] + hero.tagis[Hero.NPC] >= 2) then
  @text = "{text a0a0a0}"
elseif (unspent = 0) then

Moving downward, the synthesis of the short name for the resource also needs to be revised specially for NPCs. If we have an NPC, all we really want to show is the number of selections made. At the top of the Eval script, we'll insert the code below to generate the text specially and just get out.

~if we have an NPC and this resource is impacted, generate appropriate results
if (tagis[Helper.NPCImpact] + hero.tagis[Hero.NPC] >= 2) then
  field[resShort].text = field[resSpent].value

In the next Eval script, we're generating the summary for display, which needs to be different for NPCs. In this case, we again only want to the show the number of selections made, but we'll also append a little clarifying text after it. We'll use the same approach as above by just getting out after handling NPCs specially. This results in the new code below at the start of the Eval script.

~if we have an NPC and this resource is impacted, generate appropriate results
if (tagis[Helper.NPCImpact] + hero.tagis[Hero.NPC] >= 2) then
  field[resSummary].text = field[resSpent].value & " Points"

The Eval Rule script checks for the overspending of resources, which is definitely something that we want to stop for NPCs. In this case, we simply want to treat the rule as being automatically satisfied if the character is an NPC. So we can insert the code below at the start of the script to accomplish this behavior.

~if we have an NPC and this resource is impacted, we're always valid
if (tagis[Helper.NPCImpact] + hero.tagis[Hero.NPC] >= 2) then
  @valid = 1

At this point, we've now got resources properly instrumented so that specific resources will report different results appropriately for NPCs.

Calculating the XP

The next thing we need to do is actually calculate the estimated XP for our NPC. Before we can do that, we need to define a new field where we can store the XP. We currently use the "resXP" resource to track the XP, but that won't work for our calculated value. So we define a new field that will be assigned the proper value, depending on whether we have an NPC or not. We'll add a Finalize script to the field that will prefix the value with "~" if we have an NPC. This will provide an reminder to the user that the value is an optimistic estimation and should be treated as such. The resulting field should look like the following.

  name="Final XP Cost"
    ~if the character is an NPC, indicate the value is an approprixation
    if (hero.tagis[Hero.NPC] <> 0) then
      @text = "~" & @text

With our field in place, we can define a new Eval script where we can do all the necessary calculations for the final XP total. At the start of the script, we'll handle the special case of a non-NPC character. For a normal character, the total XP tally is given within the "resXP" resource, so we'll pull it from there and be done.

For an NPC, there are two different factors that we need to consider. The first is the number of attribute increases. The second is the total number of advancements for the character, spanning attributes, edges, and skills.

The number of attribute increases will dictate the minimum XP level for the character. This is because only one attribute increase is allowed for each rank. If a character takes 3 attribute increases and no other advances, then it must still be a Veteran rank character and have the minimum necessary XP for that rank. It also means that the character will have extra unused advances that need to be chosen. If we have unused advances, we'll definitely want to report it to the user, so we might as well define a new field right now to store that value. This new field should look like the following.

  name="Unused Advancements"

Returning to our calculation, we next calculate the total number of advances for the character. The attributes and edges are easy, since they count one apiece. Skills are where things get interesting. Since we're using an optimistic algorithm, we can iterate through each skill and determine how many advances it requires by comparing it the die type of the skill to the die type of the linked attribute. Each increment that is less than or equal to the attribute costs a half-advance (since two such skills can be raised as a single advance), while the others cost a full advance each.

Once we have the total number of advances for skills incorporated, we must adjust out the free advances we get as part of character creation. We can finally calculate the total number of XP based on the number of advances. As a last step, we then compare the minimum XP against the actual XP to determine if there are any unused advances and the final XP total to use for the character.

Putting all of this together yields the Eval script shown below. After the script is invoked, the two new fields will contain the proper values for the character.

<eval index="7" phase="Final" priority="1000" name="calc acFinalXP"><![CDATA[
  ~if this is not an NPC, our final XP tally is given by the resource
  if (hero.tagis[Hero.NPC] = 0) then
    field[acFinalXP].value = #resmax[resXP]

  ~determine our minimum XP level based on the number of attribute increases
  var attribs as number
  var minxp as number
  attribs = -#resleft[resAttrib]
  if (attribs <= 0) then
    minxp = 0
  elseif (attribs <= 4) then
    minxp = (attribs - 1) * 20 + 5
    minxp = 80 + (attribs - 5) * 20 + 10

  ~determine the number of advances due to attributes and edges
  var edges as number
  var advances as number
  edges = -#resleft[resEdge]
  advances = attribs + edges

  ~go through all skills and add in the number of advances needed for each
  ~Note: We compare the skill to the linked attribute and tally based on that.
  foreach pick in hero where "component.Skill"
    var level as number
    var attr as number
    level = eachpick.field[trtFinal].value
    attr = eachpick.linkage[attribute].field[trtFinal].value
    if (level <= attr) then
      advances += level / 2
      advances += attr / 2
      advances += (level - attr)

  ~subtract out the maximum number of skill slots allowed at creation
  ~Note: If fewer than the allotted number of starting skill slots are 
  ~     allocated, we must not grant a refund, so limit to the minimum.
  advances -= minimum(#resmax[resSkill],hero.child[resSkill].field[resSpent].value)

  ~round the total upwards in case we've got a half-slot for a skill used
  advances = round(advances,0,1)

  ~calculate the total number of XP necessary for all of the advances
  var xp as number
  if (advances <= 16) then
    xp = advances * 5
    xp = 80 + (advances - 16) * 10

  ~if our total xp equals or exceeds our minimum xp, setup appropriately
  if (xp >= minxp) then
    field[acFinalXP].value = xp
    field[acExtraAdv].value = 0
    field[acFinalXP].value = minxp
    if (minxp <= 80) then
      field[acExtraAdv].value = (minxp - xp) / 5
    elseif (xp >= 80) then
      field[acExtraAdv].value = (minxp - xp) / 10
      field[acExtraAdv].value = (80 - xp) / 5 + (minxp - 80) / 10

Integrating the XP

The final XP tally is now stored in the new "acFinalXP" field, but nothing is using it. We need to switch all the appropriate places over to use the new field. All references to the XP should be using the "resXP" resource within our data files, so we should be able to do a search through the files to identify uses of "resXP". We can then assess which ones need to be revised to use the new field.

The first instance we need to change is in the Calculate script for the "acRank" field of the "Actor" component. In addition to changing the field reference, we also need to modify the timing of the script. The new Eval script we defined to calculate the XP is assigned a priority of 1000. Since this script relies on that value, we need to make sure it occurs after the value is in place. For safety, we'll also add a timing dependency on the other script so that any future timing changes will be verified by the compiler. This results in a new Calculate script similar to the following.

<calculate phase="Final" priority="5000">
  <after name="calc acFinalXP"/><![CDATA[
  var xp as number
  xp = field[acFinalXP].value
  if (xp < 80) then
    @value = round(xp / 20,0,-1)
    @value = 4

The next use we come across that requires change is the Eval script that generates the recap summary for allies. We can simply change the one line that references the resource to the new field, which should look like below.

recap &= ranktext & "  (" & field[acFinalXP].value & " XP)"

There are two tab panels where the XP is shown to the user, and both can be changed very simply to the new field. On the "Basics" tab, the Label script of the "baRank" portal needs one line changed, which should look like below.

@text = ranktext & "  (" & herofield[acFinalXP].text & " XP)"

Similarly, one the "Journal" tab, a single line of the Label script of the "info" portal must be changed to the following.

@text &= "{horz 40} Total XP: " & herofield[acFinalXP].text

The remaining two instances that need to be modified are within the character sheet. On the first page of the character sheet, the Label script of the "oHeroInfo" portal should be changed to reference the new field. The same basic change must be made within the "details" portal of the "oAllyPick" template on the second page.

Reporting Inconsistencies

When we calculated the XP for NPCs, we identified a potential problem that needs to be reported to users as a validation error. If a character takes multiple attribute increases, it's possible that not enough other advances will be taken to fill in the gaps. We have this state already recorded in a field, so all we need to do is test is somewhere.

The best solution is to add a new thing specifically for validation of NPCs. That way, we can ensure that problem is automatically associated with the nature of the character being an NPC. We can then write a single Eval Rule for the thing that tests the field and reports a suitable error if there is a problem. Our new thing should be added to "thing_validate.dat" and should look like the one below.

  <tag group="Helper" tag="Bootstrap"/>

  <evalrule index="1" phase="Validate" priority="8000" 
      message="???" summary="Unused Advances"><![CDATA[
    ~if we have zero unused advances, we're good
    if (herofield[acExtraAdv].value = 0) then
      @valid = 1

    ~synthesize a validation message with the number of unused advances
    @message = "Selected attributes confer " & herofield[acExtraAdv].value & " unused advance(s) to allocate"

    ~mark associated tabs as invalid
    container.panelvalid[skills] = 0
    container.panelvalid[edges] = 0


Testing Everything

We can now go through everything and test how it all works. There don't appear to be any problems until we print out a character sheet for an NPC. We get an error that we're trying to access the non-live pick "resAdvance". That's the resource that we assigned the ContainerReq test, so somewhere our code is assuming that the "resAdvance" pick can be accessed all the time.

If we do a quick search for "resAdvance" within the data files associated with character sheet output, we can spot the problem pretty quickly. In the "oHeroInfo" portal on the first page, any unused advances are being reported. Since the pick is non-live for NPCs, the error is reported. We can easily add a test to ensure that we only append advances when the "resAdvance" pick is live. The revised code should look like below.

if (hero.childlives[resAdvance] <> 0) then
  if (#resleft[resAdvance] > 0) then
    @text &= "; " & #resleft[resAdvance] & " Advance(s)"

But wait a minute. This is a golden opportunity for us to include an indication of unused advances for NPCs as well. We have the "acExtraAdv" variable to tell us if there are any extra advances, so we might as well use it here. We'll change the code to use this alternate field for NPCs, which results in the following revised code.

~report any unspent advances
~Note: We have to handle NPCs and non-NPCs differently.
var unused as number
if (hero.tagis[Hero.NPC] = 0) then
  unused = #resleft[resAdvance]
  unused = herofield[acExtraAdv].value
if (unused > 0) then
  @text &= "; " & unused & " Advance(s)"