NPC Support (Savage)

From HLKitWiki
Revision as of 17:03, 29 January 2009 by Rob (Talk | contribs) (Showing Resources Differently)

Jump to: navigation, search

Context: HL KitAuthoring Examples … Savage Worlds Walk-Through 

Overview

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.

<field
  id="acIsNPC"
  name="Is NPC?"
  type="user"
  defvalue="0">
  </field>

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
    done
    endif

  ~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]
  ]]></eval>

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.

<portal
  id="isnpc"
  style="chkNormal"
  tiptext="Check this option to construct the character as an unlimited NPC">
  <checkbox
    field="acIsNPC"
    message="Create Unlimited NPC?">
    </checkbox>
  </portal>

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
else
  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
  endif

~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
trustme
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">
  !Hero.NPC
  </containerreq>

Calculating the XP

Reporting Inconsistencies

Integrating the XP

Creating NPCs as the Default

new source to control Eval script that sets the value of the fields at the very beginning