Initiative (Savage)
Context: HL Kit … Authoring Examples … Savage Worlds Walk-Through
Overview
As mentioned in the previous section on the Tactical Console, initiative is usually pretty simple to implement. However, Savage Worlds uses a deck of playing cards for initiative. That requires a bit more work, so this topic works through the various steps involved in setting up such an initiative mechanism within HL.
Orientation
Before we launch into the changes that need to be made, we'll start with a quick orientation to how initiative is managed within the Kit.
Every game system possesses some sort of mechanic for determining the order in which characters act during conflicts. Consequently, the Kit provides a built-in framework for handling the process. The most common term used for the game mechanic is "initiative", so the Kit uses the same terminology.
The specifics of initiative range from the simple to the complex. To handle the full spectrum of games, the initiative framework within the Kit provide a fair amount of sophistication. Game systems that don't require all that sophistication can simply ignore what they don't need.
Internally, every character possesses an assortment of fields that are specific to initiative management. However, only two of these fields are exposed to you, as the author, for control. These fields track the primary initiative score for the character and a secondary tie-breaker score. As its name suggests, the tie-breaker is used to resolve situations where two characters possess the same primary initiative score. In many game systems, it's usual for ties to arise, so a mechanic is introduce to decide which character goes first.
The HL engine maintains the initiative for all characters. It automatically sorts characters into appropriate order based on the established rules for the game system. It also will automatically assign initiative values to characters, including the use of random die rolls when the game system calls for it. Provisions are made to allow users to either accept the assigned values or assign their own values. All of this is orchestrated via the Tactical Console, which is described in the User Manual for the product.
The data files are responsible for defining how the initiative score is determined for each character, as well as the tie-breaker. This is achieved via the Initiative script, which is defined within the file "definition.def". The script is invoked whenever an initiative score is needed for a character.
Two additional scripts can be defined for a game system. The "NewCombat" script is invoked for each character at the start of each new combat, while the "NewTurn" script is invoked for each character at the start of each combat turn. Both of these scripts provide the author with the opportunity to control state information for the characters. For example, many game systems have the concept of "holding" an action (although it may be called "delaying", "waiting", or something else). This state must be reset either at the start of a new combat or at the start of a new turn, and the two scripts make it easy to accomplish this.
You can dictate various characteristics of initiative for the game system. This is done via a number of attributes on the "behavior" element within the definition file. For example, you can control whether the game system generates new initiative values at the start of each turn or once at the start of the combat.
Lastly, there is a fourth script that can be defined for initiative. The vast majority of game systems use a simple numeric value to indicate the initiative score. Sometimes the score is generated randomly via a die roll, while other times it is calculated, but it is usually a number. In the few cases where the initiative score is not a number, you can define an "InitFinalize" script. This script behaves as a standard Finalize script that is applied to the initiative score for the character. This makes it possible to display the initiative to the user in whatever fashion is most appropriate for the game system.
Modeling a Deck of Cards
Savage Worlds used a deck of cards for initiative. This presents a number of wrinkles that we need to handle, the first of which is how to even model a deck of cards. There are 52 cards in a standard deck, plus the two jokers, for a total of 54 cards. This means that we can use a value in the range of 1 through 54 to represent the entire deck, with each value corresponding to a single card.
But we can't just use a random number generator, since all the cards in the deck must be unique and random numbers can be duplicated. We also need to effectively "shuffle" the deck to get all 54 values in a random order. Fortunately, the Kit provides a mechanism to generate a random set of values within a fixed range. This mechanism can be used for a variety of different purposes, but it's perfect for what we need.
Within the "state" script context, there are a number of target references that specifically pertain to managing random sets of values. The "setrandom" target reference allows us to create a new random set of values, giving it a name and the number of values to hold. We can use this to create a "deck" with 54 values in it, ranging from 0 to 53. The "setextract" target reference pulls a value (card) from the set (deck).
In the NewCombat script, we can create a newly shuffled deck of cards. In the Initiative script for each character, we can extract a card from the deck as our initiative value. The only consideration we have to worry about is that the NewCombat script is invoked for every character and we only want to shuffle a new deck once. This can be handled by keying on the "@isfirst" special symbol, which indicated when the first character is being processed.
Within the Initiative script, there are two special symbols that we need to assign. The "@initiative" special symbol represents that actual initiative value, so we extract a value from the set for that purpose. The "@tiebreaker" special symbol is used by HL to differentiate when two characters have the same initiative. Since a deck of cards is used, all values are guaranteed unique, so there is no tie-breaker in Savage Worlds. Consequently, we can set this symbol to zero.
Putting this together yields an initial two scripts as shown below.
<newcombat><![CDATA[ ~if this is the first actor, shuffle a new deck for initiative if (@isfirst <> 0) then perform state.setrandom[deck,54] endif ~reset the abandon state in case it's still set from the previous combat herofield[acAbandon].value = 0 ]]></newcombat> <initiative><![CDATA[ ~generate the primary initiative rating by drawing a card from the deck @initiative = state.setextract[deck] ~we have no tiebreaker initiative rating since the cards are always unique @tiebreaker = 0 ]]></initiative>
After putting the above code into place, we can test our data files. Create a portfolio with a number of different characters in it, then show the Tactical Console. Every time we start a new combat, each character is randomly issued a new initiative value in the range of 0 to 53. There are never any duplicates due to the use of the set mechanism.
Configuring the Initiative Behavior
When we did our testing above, there were a few things you probably noticed. First of all, a new initiative is only generated at the start of a new combat. Savage Worlds issues a new initiative at the start of each round, so we need to change that behavior. This is controlled via the "initperturn" attribute within the "behavior" element of the definition file. The default value is "no", so we need to specify a value of "yes".
While we're here, we should also impose an appropriate minimum and maximum value on the initiative range. The user is allowed to adjust the value freely within the TacCon, so negative values are possible and so are values larger than the number of cards in the deck. If we only have a deck of 54 cards to work with, we need to specify a minimum value of zero and maximum of 54. This is done via the "initminimum" and "initmaximum" attributes.
Applying these changes to the "behavior" element results in something that looks like below.
<behavior initperturn="yes" initminimum="0" initmaximum="53">
We also need to tell HL that the term used for each turn of combat within Savage Worlds is "round". We can accomplish this by specifying the "combatturnterm" attribute within the "structure" element of the definition file. By making this change, references to the "turn" within menus and the like will use the proper "round" term. The resulting "structure" element should look like the following.
<structure folder="savage" combatturnterm="round"> </structure>
Forcing a Re-Shuffle
The next thing we need to deal with is when to re-shuffle the deck. We currently shuffle the deck at the start of the combat. However, we pull new cards from the deck every round, so we're bound to run out at some point. We also have to accommodate the rule that the deck is re-shuffled any round after a character draws a joker.
The fact that a joker is drawn can be tracked through a global state variable. Most everything with the HL is tied to a specific actor. However, in situations like this, you can set and retrieve state information that is global across all characters. Within the "state" script context, you can use the "value" target reference for this purpose. By specifying a unique id, you can save and retrieve a named value, which the following code demonstrates.
var temp as number state.value[joker] = 42 temp = state.value[joker]
Let's put this technique to use. We only need to know whether a joker has been drawn of not, so we'll use a value of 1 to indicate a joker being drawn and a value of zero to indicate no joker. Whenever we re-shuffle the deck, we need to set the state value to zero to indicate no joker. Whenever we draw a card from the deck, we need to set the state value to one. We can accomplish this by revising the NewCombat and Initiative scripts to those shown below.
<newcombat><![CDATA[ ~if this is the first actor, shuffle a new deck for initiative if (@isfirst <> 0) then perform state.setrandom[deck,54] state.value[joker] = 0 endif ~reset the abandon state in case it's still set from the previous combat herofield[acAbandon].value = 0 ]]></newcombat> <initiative><![CDATA[ ~if this actor is holding his action, he does not get a new card if (herofield[acAbandon].value <> 0) then done endif ~generate the primary initiative rating by drawing a card from the deck @initiative = state.setextract[deck] ~if we drew a joker, set the state accordingly if (@initiative >= 52) then state.value[joker] = 1 endif ~we have no tiebreaker initiative rating since the cards are always unique @tiebreaker = 0 ]]></initiative>
NOTE! Since HL assumes that higher values go first for initiative, and since jokers are the best cards in Savage Worlds, we assume that the jokers are the highest card values (52 and 53).
Now we have to decide how we're going to handle things every round. The NewTurn script is invoked for every actor, but we only want to trigger a re-shuffle once. Fortunately, the NewTurn script has the same "@isfirst" special symbol that the NewCombat script possesses. This means we can check for whether to re-shuffle the deck within the NewTurn script, and we only do it for the first actor.
We need to trigger a re-shuffle whenever the "joker" state value is non-zero. However, we also need to re-shuffle in another situation. If don't have enough cards to pass out to all the actors, we need to re-shuffle the deck to ensure we do have enough cards. We can check this condition first by using the "setremain" target reference. If we don't have enough cards, we can simply set the "joker" state to one. When we then check the "joker" state, we'll then perform the shuffle if either condition occurred.
The NewTurn script shown below shows an implementation of this logic.
<newturn><![CDATA[ ~if this is the very first actor for the turn, we've got some work to do if (@isfirst <> 0) then ~if we don't have enough cards left for all actors, force a re-shuffle if (state.actorcount > state.setremain[deck]) then state.value[joker] = 1 endif ~if a joker has been pulled, re-shuffle the deck for initiative if (state.value[joker] <> 0) then perform state.setrandom[deck,54] state.value[joker] = 0 endif endif ]]></newturn>
We can now reload the data files and put our changes to the test. If you want to make absolutely sure that the logic is performing the way you want it, you can insert a few "debug" statements into the scripts and watch the debug output while you start new combats and new turns.
Omitting Held Cards from Re-Shuffle
There is one important game mechanic that we have not addressed thus far. Characters who choose to "hold" their action may do so indefinitely. As long as they don't act, they hold onto the same card. This means that we need to ensure that any initiative cards dealt to actors on "hold" are not included in the next re-shuffle. If we don't, then the same card can be re-issued to a new actor.
The mechanism for managing random sets within the Kit does not allow us to omit cards from the deck. However, it does allow us to discard specific cards after a new deck is shuffled. The "setdiscard" target reference makes this possible.
After we re-shuffle the deck, we must go through all actors and identify the ones that are holding their action. The cards possessed by those actors must then be discarded from the newly shuffled deck. This will achieve the same set result as if we omitted the cards before shuffling. Integrating the code for this behavior into the NewTurn script results in the updated logic.
<newturn><![CDATA[ ~if this is the very first actor for the turn, we've got some work to do if (@isfirst <> 0) then ~if we don't have enough cards left for all actors, force a re-shuffle if (state.actorcount > state.setremain[deck]) then state.value[joker] = 1 endif ~if a joker has been pulled, re-shuffle the deck for initiative var isshuffle as number if (state.value[joker] <> 0) then perform state.setrandom[deck,54] state.value[joker] = 0 isshuffle = 1 endif ~if we re-shuffled the deck, discard any cards possessed by actors on hold if (isshuffle <> 0) then foreach actor in portfolio if (eachpick.herofield[acAbandon].value <> 0) then perform state.setdiscard[deck,eachpick.herofield[tactInit].value] endif nexteach endif endif ~held actions persist from the previous round of combat, so there's nothing to do ]]></newturn>
At this point, our logic is finally working the way it needs to.
Displaying Cards
The internal behaviors are in place, so we'll shift our focus to the presentation. On the Tactical Console, the initiative is managed via an incrementer. Within the incrementer, the cards are still being shown as a numeric value between 0 and 53. That's not very intuitive to players, and it's not going to work at all for GMs who want to actually deal out the cards in the game and then specify the initiative for each actor.
We need to convert the numeric value to a suitable card for display. The good news is that we can easily do so by defining an InitFinalize script. This script behaves like a standard Finalize script for fields, and it is applied to the field containing the initiative for each actor. If we implement this script correctly, the incrementer will show a traditional card instead of a numeric value.
We'll break the process up into two pieces. For each card, we need to determine the card symbol to be shown and the suit to be shown. We'll need to handle the jokers specially, since they don't have a suit.
We already determined that higher values are better, so that means the top two values belong to the jokers. Working downwards in value, the next four values are the aces, the next four the kings, and so on down to the deuces. Once we get to the single digit values, we can use math to quickly identify the specific character to be shown.
For the suit, we need to come up with something appropriate. We could easily us "S" for spades, "H" for hearts, etc. However, it would be even better if we could use the proper symbol for each suit. We can fire up the "Character Map" tool and take a look at the various fonts that are built into Windows. One of those fonts is the "Symbol" font, and within that font are characters for each of the different suits. Since the suits are progressive in nature, we can readily convert the card value to a value between 0 and 3. Once we do that, it's easy to convert it to the appropriate character code.
The last thing we do is combine the two pieces into the final text displayed. The whole process amounts to the InitFinalize script shown below, which we'll add just beneath the Initiative script.
<initfinalize><![CDATA[ var card as string var suit as string var temp as number ~determine the card symbol to be used if (@value >= 52) then card = "Jkr" elseif (@value >= 48) then card = "A" elseif (@value >= 44) then card = "K" elseif (@value >= 40) then card = "Q" elseif (@value >= 36) then card = "J" elseif (@value >= 32) then card = "10" else temp = (@value + 8) / 4 card = chr(48 + round(temp,0,-1)) endif ~determine the suit to be used if (@value >= 52) then suit = "" else temp = @value % 4 suit = "{font Symbol}" & chr(167 + temp) endif ~assemble the final value @text = card & suit ]]></initfinalize>
Reload the data files and take a look at all the initiatives on the Tactical Console. Proper cards are now shown. If you use the up/down arrows on the incrementers to adjust the initiative, it will cycle through the cards of the deck in an intuitive manner.
Revising the Incrementer
The card symbols are appearing in the incrementer, but they don't look as good as they could. The font used and the size characteristics need to be tweaked. In addition, if the user clicks within the incrementer, the numeric value appear and the user can enter a custom value. This won't make any sense to the user, so we need to stop it. The solution to each of these is to come up with our own incrementer style that is tailored to the task.
We can open the file "styles_ui.aug" and locate that various existing incrementer styles. The "incrSimple" style is currently being used, so we'll clone that and then adapt it. We'll switch to the slightly smaller "fntincrsml" font and see how that looks. The card text looks good, so we'll stick with that. Now let's examine the horizontal spacing. We'll widen the text area a little bit, which means we must increase the full width and move the position of the "plus" arrow the same amount. Lastly, we need to make sure that the user can't directly edit the contents. Putting this all together yields the new style below.
<style id="incrCard"> <style_incrementer textcolor="f0f0f0" font="fntincrsml" editable="no" textleft="14" texttop="0" textwidth="28" textheight="20" fullwidth="54" fullheight="20" plusup="incplusup" plusdown="incplusdn" plusoff="incplusof" plusx="43" plusy="0" minusup="incminusup" minusdown="incminusdn" minusoff="incminusof" minusx="0" minusy="0"> </style_incrementer> </style>
Our style is defined, so we need to switch to using it. Modify the "totalinit" portal to use the new "incrCard" style and reload the files. That looks much better.
Refinements
Everything is working properly, but there are still a few refinements we need to make. For example, there are multiple places where the term "turn" is being used within text strings. These should all be changed to use the term "round" appropriately.
Next to the incrementer is an initiative adjustment. This is intended for use in game systems where characters have adjustments that apply to rolled initiative values. By having the adjustment visible, the GM can roll dice, apply the adjustment, enter the proper values for use. The problem is that this has absolutely zero pertinence to Savage Worlds.
We'll delete the "init" portal from the template. This requires that we revise the logic within the Position. We have to both eliminate all references to the portal and make sure that portals are re-positioned appropriately in the absence of the portal. The revised section of script code impacted by this removal is shown below.
~position the initiative incrementer right-aligned with the activated abilities perform portal[totalinit].alignrel[rtor,active,-10] perform portal[totalinit].alignedge[bottom,-margin - 4] ~position the initiative label to the left of the incrementer perform portal[lblinit].alignrel[rtol,totalinit,-3] ~vertically align the initiative labels based on the incrementer perform portal[lblinit].centeron[vert,totalinit] ~all initiative details are only visible within combat show = state.iscombat portal[totalinit].visible = show portal[lblinit].visible = show ~adjust our right edge leftward past the special abilities mouse-over icon rightedge = portal[special].left - 4
We now have the initiative working properly for Savage Worlds and integrated smoothly into the Tactical Console.