More Cleanup (Savage)
- 1 Overview
- 2 Damage and Non-Wildcards
- 3 Lead Summary Script
- 4 Arcane Power Totals
- 5 Show Arcane Powers Resource
- 6 Starting Bennies
- 7 Super Power Skill Names
- 8 Alternate Pace Values
- 9 Natural Armor
- 10 Revise "Configure Hero" Form
- 11 Customizable Abilities
- 12 Vigor and Fighting Beyond "d12"
- 13 Bounding of Traits
- 14 Adjustments Require Hiding
- 15 Centralize Rank Name Handling
Many of the tasks we came up with previously have been addressed. Let's take this opportunity to do another round of testing everything and see if there's anything we missed last time. Then we can knock off most of the remaining little things on our list. The sections below identify the various issues we can spot and the fixes that we need to make.
Damage and Non-Wildcards
On the "In-Play" tab, we make the assumption that all characters can take four levels of damage. However, this only applies to wildcards. Non-wildcards take one hit and go down. After looking into this a little bit further, it seems that we've made this assumption through the data files. We need to properly handle the situation where a character has a different maximum number of wounds based on the wildcard designation.
The first thing we need to do is setup a new field into which we can save the appropriate maximum number of wounds. Since the field value is based solely on the wildcard state of the character, we can use a Calculate script on the field to easily determine the value. We need to be sure to schedule the script early, since other scripts will rely on this value. This yields a field that looks like the following.
<field id="acMaxWound" name="Maximum Wounds" type="derived"> <calculate phase="Setup" priority="1000"><![CDATA[ ~if we're a wildcard, we can take 4 wounds, else only one if (field[acIsWild].value <> 0) then @value = 4 else @value = 1 endif ]]></calculate> </field>
With the field in place, we can now go through the data files and identify places where the field needs to be utilized. We'll do a search for references to the "acWounds" field to locate places where a change may be needed. It turns out there are three places within the "Actor" component and another two places within the "In-Play" tab.
The Finalize script for the "acDmgSumm" field uses a hard-coded value of four when determining whether to show the wounds as "Inc". We can replace the four with a reference to the "acMaxWound" field. The new line of code should look like below.
if (wounds <= -field[acMaxWound].value) then
Similar logic is utilized within the Finalize script for the "acDmgTac" field. Again, we replace the four with a reference to the field. The third reference is in the Eval script that assigns the "Hero.Dead" tag. Simply swapping out the literal value for the field is all we need to do.
Shifting our focus to the "In-Play" tab, both the "wounds" and "wndsustain" portals reference a hard-coded value of four. Both of these references need to be replaced with the field. The new line of code for the Label script in the "wounds" portal is shown below.
if (wounds > -field[acMaxWound].value) then
All of the handling for wounds is now properly differentiating between wildcards and non-wildcards.
Lead Summary Script
When a lead character is saved out to a portfolio, a summary of that character is synthesized and stored in the portfolio. This summary is displayed along with the name when the user attempts to import characters from that portfolio. The summary is synthesized via the LoadSummary script, whose purpose is to provide a brief (but useful) synopsis of the character.
The LoadSummary script is defined in the file "definition.def", and the Skeleton files provide us with a basic implementation. We'll adapt the one provided for Savage Worlds. The question is what information we should include. Savage Worlds characters have no clear role (such as classes), so there's really nothing definitive we can say about a character beyond its race and rank/XP. This leaves us with a script implementation that looks like the following.
~start with the race var txt as string txt = hero.firstchild["Race.?"].field[name].text if (empty(txt) <> 0) then txt = "No Race" endif @text &= txt ~append the rank and XP var rankvalue as number var ranktext as string rankvalue = herofield[acRank].value call RankName @text &= " - " & ranktext & " - " & herofield[acFinalXP].value & " XP"
Arcane Power Totals
If a character with an arcane background adds a foreign gizmo on the "Gear" tab, that gizmo is being counted towards the total number of arcane powers selected by that character. Foreign gizmos need to be ignored when tallying up the number of powers chosen for the character.
The tallying of each arcane power is performed within an Eval script on the "Power" copmonent. This script always consumes one power slot for each power. We need to modify the script to skip any power with the "Helper.Foreign" tag. The revised script should look like below.
if (tagis[Helper.Foreign] = 0) then perform #resspent[resPowers,+,1,field[name].text] endif
Show Arcane Powers Resource
The "Basics" tab contains all of the various resources that a user will want to monitor during character creation, except for one. If the character has an arcane background, the list of resources does not include the one for arcane powers.
We can add the resource to the list by assigning it the "Helper.Creation" tag. We can then control its position in the list by assigning it an appropriate "explicit" tag. We'll put it at the end, after "Skills", so we'll assign it the tag "explicit.6".
The only problem now is that the resource shows up for characters with no arcane background. What we want is for the resource to only show up when an arcane background is possessed. There are a number of ways we can solve this, but the best is to recognize that the resource serves no purpose unless we an arcane background is selected. As such, we can use the same approach as with the "Advances" resource, except that pre-condition changes. We can assign a ContainerReq test to the "resPowers" resource and make the thing dependent upon the hero possessing any tag from the "Arcane" group, which will assigned if any arcane background is chosen. This results in the revised "resPowers" thing below.
<thing id="resPowers" name="Arcane Powers" compset="Resource"> <fieldval field="resObject" value="Arcane Power"/> <tag group="Helper" tag="Bootstrap"/> <tag group="Helper" tag="Creation"/> <tag group="explicit" tag="6"/> <containerreq phase="Initialize" priority="2000"> Arcane.? </containerreq> </thing>
Normal player characters receive three Bennies at the start of each game. However, NPCs that are wildcards only receive two personal Bennies and non-wildcards receive zero. We have the necessary information to setup everything properly, so let's add this in properly.
Bennies are managed via a tracker that is defined in the file "thing_miscellaneous.dat". This tracker starts with a fixed range of zero to three. By defining an Eval script for the thing, we can customize the starting values based on the characteristics assigned to the hero. The new Eval script should look similar to the one below.
<eval index="1" phase="Initialize" priority="6000"><![CDATA[ ~if we're not a wildcard, we get zero starting Bennies if (herofield[acIsWild].value = 0) then field[trkMax].value = 0 ~if we're an NPC, we get two starting Bennies elseif (hero.tagis[Hero.NPC] <> 0) then field[trkMax].value = 2 ~otherwise, we're a normal character and get three starting Bennies else field[trkMax].value = 3 endif ]]></eval>
Super Power Skill Names
The character sheet output makes the assumption that all skills that lack a domain can safely fit within the narrow width of the two-column table. While this is true for all normal skills, the skills associated with super powers have comparatively much longer names. As such, they are shrunk as far as possible and still cut off.
A simple solution is to handle super power skills the same way we handle skills with domains, moving them into the single-column table beneath the two-column table. Since all super skills possess an "Arcane.?" tag, we can readily identify such skills. This makes it easy to integrate them into the second table instead.
The first step is to revise the List tag expression for the skills shown in the two-column table, omitting the super skills. The second step is to revise the List tag expression of the other table to include the super skills. The two revised List tag expressions should look like below. Note the addition of the "CDATA" block around the first one due to the use of the '&' character.
<list><![CDATA[!User.NeedDomain & !Arcane.?]]></list> <list>User.NeedDomain | Arcane.?</list>
Once the tag expressions are defined, we then need to refine the "oSkillPick" template. The template bases the sizes for its contents based on the same criteria used in the List tag expressions for the tables. This means all we need to do is change the one line in the Position script that identifies whether the pick belongs in the narrow or wide table. The revised line of code should look like the following.
if (tagis[User.NeedDomain] + tagis[Arcane.?] = 0) then
Alternate Pace Values
There are some abilities that necessitate having two values for the "Pace" trait. All characters possess a ground-based speed, which is the standard value for "Pace". However, some abilities confer swimming or flying speeds. We need a way to track and convey that information to the user.
The easiest solution is to add a new field to the "Derived" component. This one field will serve to track a generic alternate value, and we can integrate the field value into the display for the trait. If there is no alternate value needed, then it can be left at a default value of zero, in which case it will be omitted from display. For the "Pace" trait, this field will identify whichever alternate speed is appropriate (flying or swimming). There is currently no need for this field on other derived traits.
The new field is extremely simple. We'll designate it as a "special" value, since it's actual purpose will value from one trait to another. The definition below is all we need.
<field id="trtSpecial" name="Special Value" type="derived"> </field>
With the field defined, we then need to decide how to integrate it into the displayed value for the trait, which is stored in the "trtDisplay" field. An Eval script on the "Trait" component currently handles this, so we'll need to break up the logic. We'll modify the existing Eval script on the "Trait" component to do nothing for a "Derived" trait. Then we'll add a new Eval script on the "Derived" component to synthesize the proper display contents. The new Eval script should look like the following.
<eval index="2" phase="Render" priority="5000" name="Calc trtDisplay"><![CDATA[ ~our display text is the final value, plus any "special" value given field[trtDisplay].text = field[trtFinal].value if (field[trtSpecial].value <> 0) then field[trtDisplay].text &= "/" & field[trtSpecial].value endif ]]></eval>
If a particular ability confers a non-standard value for "Pace", it can assign that value to the "trtSpecial" field. That field will then be integrated into the displayed value for the trait.
The Skeleton files provide an entry for "Natural Armor". This thing can be used for any situation where an actor has an innate (or natural) defensive protection that behaves like armor. For example, an extra thick hide or scaly skin might confer the equivalent defense of armor.
When this situation presents itself, you can bootstrap the "armNatural" thing onto the character. As part of the bootstrap, you can specify the level of defensive protection that is afforded by the natural armor via the "defDefense" field. If not specified, the default defensive rating is "1". The sample "bootstrap" element below shows the assignment of natural armor with a defensive rating of "3".
<bootstrap thing="armNatural"> <assignval field="defDefense" value="2"/> </bootstrap>
Since natural armor covers the entire body, we need to assign all of the various "ArmorLoc" tags to the thing. We also need to assign an appropriate armor type, which means we need to define an appropriate tag in the "ArmorType" group. When we're done, the revised thing should look like below.
<thing id="armNatural" name="Natural Armor" compset="Armor" description="Description goes here" isunique="yes" holdable="no"> <fieldval field="defDefense" value="1"/> <tag group="Equipment" tag="Natural"/> <tag group="Equipment" tag="AutoEquip"/> <tag group="ArmorType" tag="Natural"/> <tag group="ArmorLoc" tag="Torso"/> <tag group="ArmorLoc" tag="Arms"/> <tag group="ArmorLoc" tag="Legs"/> <tag group="ArmorLoc" tag="Head"/> </thing>
Revise "Configure Hero" Form
The "Configure Hero" form currently shows the starting cash, followed by the starting XP, and then the checkboxes to designate a wildcard and an NPC. The result is a sequence where toggling the NPC checkbox causes the portals for starting XP to appear and disappear above it. This behavior is rather disorienting to the user, as the checkbox portal just toggled moves under the mouse.
What we need is a sequence that allows the visibility change in an intuitive manner. That sequence should be the NPC checkbox, followed by the wildcard checkbox, then the starting cash, and lastly the starting XP. Since the concept of starting cash really doesn't make any sense for NPCs, we'll also hide the starting cash for an NPC.
We'll first re-order the portals within the template to this new sequence. This will ensure that attempts by the user to use the <Tab> key to move through the portals will work smoothly.
We now need to revise the Position script logic to orchestrate the display properly. We start by determining the visibility of the starting cash and XP. After that, we'll position the label at the top, with the NPC checkbox beneath it. We'll allow for the remaining portals to be optionally visible, tracking our vertical position as we proceed downward through the template. This results in the following revised Position script.
~set the width of the template to something we like width = 185 ~determine whether the starting cash is visible based on whether we're a pc portal[cash].visible = !hero.tagis[Hero.NPC] portal[lblcash].visible = portal[cash].visible ~determine whether the starting xp is visible based on if we're a pc 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 npc checkbox beneath the title perform portal[isnpc].centerhorz perform portal[isnpc].alignrel[ttob,label,15] ~start tracking our vertical position because portals may not be visible var y as number y = portal[isnpc].bottom ~if visible, position the wildcard checkbox beneath the character type if (portal[iswild].visible <> 0) then perform portal[iswild].centerhorz portal[iswild].top = y + 15 y = portal[iswild].bottom endif ~if visible, position the starting cash next if (portal[cash].visible <> 0) then portal[cash].top = y + 15 portal[cash].width = 50 perform portal[lblcash].centeron[vert,cash] portal[lblcash].left = (width - portal[lblcash].width - portal[cash].width - 10) / 2 perform portal[cash].alignrel[ltor,lblcash,10] y = portal[cash].bottom endif ~if visible, position the starting xp beneath the wildcard checkbox if (portal[xp].visible <> 0) then portal[xp].top = y + 15 portal[xp].width = 50 perform portal[lblxp].centeron[vert,xp] portal[lblxp].left = (width - portal[lblxp].width - portal[xp].width - 10) / 2 perform portal[xp].alignrel[ltor,lblxp,10] y = portal[xp].bottom endif ~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 = y + 3
The final thing we'll do is change the label at the top of the template. It currently says "Starting Resources", but the NPC state is not a resource, so the name is misleading. We'll change it to "Starting Characteristics". This requires simply changing the literal text for the "label" portal, and we're done.
There are no races actually defined in the core rulebook (other than Humans). However, there are many races defined in the various supplements. Abilities will also be used with all the various creatures throughout the core rulebook and supplements. Many of the special abilities are basically the same thing with slight variations. For example, different races and creatures possess natural weapons and/or natural armor.
The names and bonuses of these special abilities often differ, but the mechanics are all the same. This results in the need to define lots of very similar abilities. It would be much easier if we simply provided some customizable special abilities that could be easily re-used and adapted.
Let's take a look at the types of customization that we need. For natural armor, we need to modify the name (e.g. "Carapace") and specify the numeric armor rating to be used (e.g. "2"). If we scan through the various creature abilities, the "Size" ability requires a numeric rating, so it can be handled the same way as natural armor. For natural weapons, we need to modify the name (e.g. "Bite", "Claw", etc.) and specify the appropriate damage the attack. This may need to be done as a weapon die or as text for odd situations (e.g. "2d6"). We also need to handle situation like weaknesses and immunities for creatures, which need to specify custom text with the details.
This leaves us with the need for three separate fields that can be used to customize a special ability. For the weapon die, we'll use the existing tag mechanism that is already in place. We can already modify the name via use of the "livename" field. So we really only need two new customization fields - a value and a string. We'll add these to the "RaceAbil" component as shown below.
<field id="abilValue" name="Extra Value" type="derived"> </field> <field id="abilText" name="Extra Text" type="derived" maxlength="25"> </field>
We can now put these fields to use by defining fully customizable versions of natural armor and weapons. We'll start with the natural armor, which we'll define as racial ability. There is already an existing "Natural Armor" piece of armor, so we're defining the corresponding special ability here. Our ability must bootstrap the actual armor. Then we can take the value assigned to the ability and use it to set the defensive rating of the armor, as well as customize the name of the ability. This yields the following ability.
<thing id="abArmor" name="Natural Armor" compset="RaceAbil" isunique="yes" description=""> <bootstrap thing="armNatural"/> <eval index="1" phase="Initialize" priority="1000"> <before name="Calc trtFinal"/><![CDATA[ ~set the defensive value for the armor perform hero.child[armNatural].setfocus focus.field[defDefense].value = field[abilValue].value ~if we've been assigned a custom name, assign it to the armor if (empty(field[livename].text) = 0) then focus.field[livename].text = field[livename].text endif ~append the value to both names field[livename].text = field[name].text & " " & signed(field[abilValue].value) focus.field[livename].text = focus.field[name].text & " " & signed(field[abilValue].value) ]]></eval> </thing>
Our new ability can be both used and customized by a race quite easily. The ability is bootstrapped onto the actor, and the appropriate fields are assigned as part of the bootstrap. The "abilValue" field dictates the armor rating to be used. If the ability name also needs to be customized, the "livename" field can specified. An example of this is shown below for natural armor that is called a "Carapace" and confers a "+2" defensive bonus.
<bootstrap thing="abArmor"> <assignval field="livename" value="Carapace"/> <assignval field="abilValue" value="2"/> </bootstrap>
We can define a similar special ability for use as a natural weapon. The "Unarmed Attack" weapon is always bootstrapped to every actor, so we don't need to bootstrap anything here. Instead, we simply customize the already existing weapon to suit our needs. The damage associated with the natural weapon can be specified in two different ways. In general, it will be assigned via the appropriate "WeaponDie" tag, but it can also be specified via the "abilText" field. The damage will be appended to both the weapon damage and the name of the ability. We handle it this way so that the ability name shows the extra damage, but the weapon entry itself omits the extra damage from the name. This yields the following special ability.
<thing id="abWeapon" name="Weapon" compset="RaceAbil" isunique="yes" description=""> <eval index="1" phase="Initialize" priority="1000"><![CDATA[ ~customize the Unarmed Attack pick with the appropriate name ~Note: Do this BEFORE modifying our name below to use an undecorated name. perform hero.child[wpUnarmed].setfocus focus.field[livename].text = field[name].text ~if we have a weapon die, forward it, else assign the damage text if (tagis[WeaponDie.?] <> 0) then perform focus.pushtags[WeaponDie.?] else focus.field[wpDamage].text = "Str" & field[abilText].text endif ~update our name appropriately for display if (tagis[WeaponDie.?] <> 0) then var die as number die = tagvalue[WeaponDie.?] * 2 field[livename].text &= " (Str+d" & die & ")" else field[livename].text &= " (" & field[abilText].text & ")" endif ]]></eval> </thing>
Putting our new special ability to use is very simple. Using the "WeaponDie" method, we bootstrap the ability onto the actor, customize "livename" field, and assign the proper tag. The example below shows a natural weapon that is called a "Bite" and does "Str+d6" damage.
<bootstrap thing="abWeapon"> <autotag group="WeaponDie" tag="3"/> <assignval field="livename" value="Bite"/> </bootstrap>
If the damage were something non-standard, we would use the alternate method. We would bootstrap the ability onto the actor and customize both the "abilText" and "livename" fields. The example below shows a natural weapon that is called a "Non-Standard" and confers "2d6" damage.
<bootstrap thing="abWeapon"> <assignval field="livename" value="Non-Standard"/> <assignval field="abilText" value="2d6"/> </bootstrap>
Vigor and Fighting Beyond "d12"
One of the things we deferred early on is the special handling needed for a couple of derived traits. If the "Vigor" attribute is greater than a "d12" rating, we need to factor that into the "Toughness" trait calculation. Similarly, if the "Fighting" skill exceeds "d12", we need to factor that into the "Parry" trait.
For the "Toughness" trait, we can revise the Eval script that calculates the trait bonus. If the trait is at a "d12" rating (i.e. a value of 6), then we add half the "trtRoll" field value. Since the rules call for the bonus to be increased by "+1" for every two full points, we need to divide by two and round the value down. This yields the revised Eval script shown below.
~toughness is 2 plus half the character's Vigor, but we track attributes at ~the half value (2-6), so we add Vigor directly; we get the Vigor by using ~the "#trait" macro ~Note: We must also handle an overage beyond d12 on a +1 per 2 full points basis. ~Note: We ADD the amount in case other effects have already applied adjustments. var bonus as number bonus = #trait[attrVig] if (bonus >= 6) then bonus += round(hero.child[attrVig].field[trtRoll].value / 2,0,-1) endif perform field[trtBonus].modify[+,bonus,"Half Vigor"] ~equipped armor should add to the Toughness, so we add that from the armor
We can apply the exact same logic to the "Parry" trait. This yields the revised Eval script below.
~parry is 2 plus half the Fighting skill, but we track all skills at the half ~value (2-6), so we add the Fighting skill; since the skill might not exist ~for the character, we use the "#traitfound" macro, which returns the value ~of the trait if found and zero if not ~Note: We must also handle an overage beyond d12 on a +1 per 2 full points basis. ~Note: We ADD the amount in case other effects have already applied adjustments. if (hero.childexists[skFighting] <> 0) then var bonus as number bonus = #trait[skFighting] if (bonus >= 6) then bonus += round(hero.child[skFighting].field[trtRoll].value / 2,0,-1) endif perform field[trtBonus].modify[+,bonus,"Half Fighting"] endif
Bounding of Traits
The contents of the "trtUser" field for each trait are bounded. For attributes and skills, the bounding applies a minimum value of "2" and a maximum of "6". These values correspond to the die-types "d4" through "d12".
Under normal conditions, these bounds work great. The problem arises when effects are applied that adjust the traits via the "trtBonus" field. For example, the "Dwarven" race possesses the "Tough" special ability that confers a free extra die on the "Vigor" trait. If the "Dwarven" race is selected, the starting "d4" in "Vigor" changes to "d6", just as it should. Increasing one notch goes to "d8", then to "d10", and then to "d12". When we reach "d12", we should not be allowed to go any higher, but we're allowed to increment again, which does nothing but consume an attribute point for no benefit.
The reason for this behavior is that we are bounding the "trtUser" field value alone. We are not taking into consideration any adjustment from the "trtBonus" field value. If the "trtBonus" value is greater than zero, we need to decrement the maximum by the corresponding amount. This ensures we stop the user from increasing the trait as soon as it reaches the maximum of "d12". The revised Bound script for the field should look like below.
@minimum = field[trtMinimum].value @maximum = field[trtMaximum].value if (field[trtBonus].value > 0) then @maximum -= field[trtBonus].value endif
Adjustments Require Hiding
All of our permanent and in-play adjustments appear to be working correctly. However, the list of traits shown in the menu includes choices that should not be visible. For example, the list of attributes show our special attribute for super powers that should never be visible to the user.
The problem is that the tag expressions used on the various adjustments don't include handling for the hidden traits. The tag expression for the attribute die adjustment simply specifies the "Attribute" component. It also needs to to omit any attributes that possess the "Hide.Attribute" tag. This same basic change is needed on a total of five of the adjustments.
The "adjAttrD" and "adjAttrR" adjustments both need to omit attributes with the "Hide.Attribute" tag. The revised "adjCandid" field for both of these things should look like below.
<fieldval field="adjCandid" value="component.Attribute & !Hide.Attribute"/>
Similarly, the "adjSkillD" and "adjSkillRS" adjustments both need to omit skills with the "Hide.Skill" tag. The revised "adjCandid" field for both of these things should look like the following.
<fieldval field="adjCandid" value="component.Skill & !Hide.Skill"/>
Lastly, the "adjDerived" adjustment needs to omit traits with the "Hide.Trait" tag. The revised "adjCandid" field for this thing should be as follows.
<fieldval field="adjCandid" value="component.Derived & !Hide.Trait"/>
Centralize Rank Name Handling
There are more than a half-dozen places where the "RankName" procedure is called to convert the rank as a value to the corresponding name for display. In all but one of these places, the rank value being used is the rank of the current actor. There is no reason for us to go through the logic of setting up to call the procedure each time.
Instead, we can simplify everything by adding a new field to the "Actor" component where we can synthesize and store the rank name a single time. Then we can have each of these places simply retrieve the field from the hero. Our new field should look similar to the one below.
<field id="acRankName" name="Current Rank as Name" type="derived" maxlength="15"> </field>
We can then add a new Eval script to the component that will synthesize and save the name properly. We need to schedule this script after the value is determine, and it must be done before we need to reference the field. The new script should look like below.
<eval index="8" phase="Render" priority="1000"><![CDATA[ ~convert our current rank value to the corresponding name and save it var rankvalue as number var ranktext as string rankvalue = field[acRank].value call RankName field[acRankName].text = ranktext ]]></eval>
A suitable field is now in place on the actor. We can go through all calls to the "RankName" procedure and change most of them to retrieve the new field instead of calling the procedure itself. The first instance is in the "Actor" component itself, within the Eval script that synthesizes the recap summary for allies. We can replace the code that invokes the procedure with a single line, as shown below.
recap &= field[acRankName].text & " (" & field[acFinalXP].value & " XP)"
The next instance is in the LeadSummary script within the definition file. As we did above, we can replace the block of code with a single line shown below.
@text &= " - " & herofield[acRankName].text & " - " & herofield[acFinalXP].value & " XP"
The instance we come across is in the "MinRank" component, where the rank is referenced within the pre-requisite test. This time, we are determining the displayed rank based on the requirement specified instead of the actual rank of the actor. So there is nothing to change here.
There are two separate tabs where the rank name is displayed, and both need to be changed. On the "Basics" tab, the "baRank" portal can be replaced with a single line of code, just like we did above. On the "Journal" tab, the synthesis of the info shown across the top and eliminate the procedure call and simply reference the field.
The remaining two places are within the character sheet output. Both on the first sheet and the second sheet, the rank name is included in the output. As we did above, both instances can be swapped out for a single line of code.