Add an "Edges" Tab (Savage)
Context: HL Kit … Authoring Examples … Savage Worlds Walk-Through
Overview
The next logical step in the evolution of the Savage Worlds data files is to add a tab for edges. Due to the close relationship between edges and hindrances, we'll also include hindrances on the tab, as well as the rewards that will be selected to offset any chosen hindrances. However, we'll simply call the tab "Edges". The sections below outline the process of adding and tailoring this new tab.
Something Simple to Start With
We need to a create an entirely new tab, so one option would be to start from scratch. A faster approach is to find an existing tab that is extremely simple and adapt it for use as our starting point. The "Skills" tab is perfect for this purpose, so we'll use it.
We first copy the file "tab_skills.dat" to "tab_edges.dat". If we try to re-compile the data files now, we'll get lots of errors about duplicated unique ids and such. So we need to rename the contents of the new file, and we'll tailor everything to use "edge" instead of "skill". This entails renaming all unique ids and names, changing all references to components and component sets, modifying all uses of "addthing" and actual references to resources, etc.
We also need to add a new "HideTab" tag for the new "edges" tab and adjust the Live tag expression to use it. Lastly, the template for skills includes handling for domains and an incrementer for adjusting the die types, neither of which we need. So we must strip those portals out of the template and adjust the Position script accordingly. We could potentially switch to using the "SimpleItem" template, except that we plan to add special support later on for edges like "Scholar" and "Professional", so we need a separate template that we can customize.
When the conversion is done, we'll have a single portal, template, layout, and panel. All of them will be tailored for the display of edges, and our new tab will look just like the "Skills" tab except that it will manage the selection of edges. The four pieces will look like those shown below.
<portal id="edEdges" style="tblNormal"> <table_dynamic component="Edge" showtemplate="edEdge" choosetemplate="SimpleItem" addthing="resEdge" addspace="2"> <titlebar><![CDATA[ @text = "Add an Edge - " & hero.child[resEdge].field[resSummary].text ]]></titlebar> <headertitle><![CDATA[ @text = "Edges - " & hero.child[resEdge].field[resSummary].text ]]></headertitle> <additem><![CDATA[ ~if we're in advancement mode, we've been frozen, so display accordingly if (state.iscreate = 0) then @text = "{text a0a0a0}Add Edges Via Advances Tab" done endif ~get the color-highlighted "add" text @text = field[resAddItem].text ]]></additem> </table_dynamic> </portal> <template id="edEdge" name="Edge Pick" compset="Edge" marginhorz="3" marginvert="2"> <portal id="name" style="lblNormal" showinvalid="yes"> <label field="name"> </label> </portal> <portal id="info" style="actInfo"> <action action="info"> </action> <mouseinfo mousepos="middle+above"><![CDATA[ var mouseinfo as string call mouseinfo @text = mouseinfo ]]></mouseinfo> </portal> <portal id="delete" style="actDelete" tiptext="Click to delete this item"> <action action="delete"> </action> </portal> <position><![CDATA[ ~set up our height based on our tallest portal height = portal[info].height ~if this is a "sizing" calculation, we're done if (issizing <> 0) then done endif ~position our tallest portal at the top portal[info].top = 0 ~position the other portals vertically perform portal[name].centervert perform portal[delete].centervert ~position the delete portal on the far right perform portal[delete].alignedge[right,0] ~position the info portal to the left of the delete button perform portal[info].alignrel[rtol,delete,-8] ~position the name on the left and use all availble space portal[name].left = 0 portal[name].width = minimum(portal[name].width,portal[info].left - portal[name].left - 10) ]]></position> </template> <layout id="edges"> <portalref portal="edEdges" taborder="10"/> <position><![CDATA[ ~freeze our tables in advancement mode to disable adding new choices ~Note: All freezing must be done *before* any positioning is performed. if (state.iscreate = 0) then portal[edEdges].freeze = 1 endif ~size the table to span the full layout width portal[edEdges].width = width ~set the height of the table to the full height of the layout; the table ~will actually only use the vertical space it needs if it's smaller portal[edEdges].height = height - portal[edEdges].top ]]></position> </layout> <panel id="edges" name="Edges" marginhorz="5" marginvert="5" order="130"> <live>!HideTab.edges</live> <layoutref layout="edges"/> <position><![CDATA[ ]]></position> </panel>
Refining Edges
Our edges table is now in place, but it's still a little bit rough in how it works, so we need to refine its behavior. Clicking on the "Edges" table to add a new edge, we see all of edges listed properly. Our pre-requisites appear to be handled correctly, with any failed pre-requisites being listed in the description region and appropriate edges being greyed out when the pre-requisites aren't satisfied. However, some of the names of edges are being cut off due to the horizontal space being too narrow. This can be easily fixed by specifying a width of our own choosing for the "thing" template. Unfortunately, we're using the "SimpleItem" template as our "choose" template, so changing the width within that template will change it everywhere the template it used, and that's not what we want.
One solution would be to copy the "SimpleItem" template into the file "tab_edges.dat", rename it, and adapt it to our needs. That would work, but then we'd find ourselves copying the template whenever we wanted to make minor adjustments, and that will make the data files harder to maintain over time. Fortunately, the "SimpleItem" template has been designed so that it can be easily customized in situations like this. By assigning a "SimpleItem.widthXXX" tag to a thing, where "XXX" is the width value we want to use, the "SimpleItem" template will detect a custom width and use it. For example, a tag of "SimpleItem.width250" indicates a width of 250 pixels should be used. Since the tag needs to be assigned to all things to ensure that it will always be recognized by the template, we can assign the tag to the component and all things derived from that component will inherit the tag. This means that all we need to do to specify a custom width for the "SimpleItem" template when showing edges for selection is add the tag as shown below to the "Edge" component within the file "traits.pri".
<tag group="SimpleItem" tag="width250"/>
The next thing of note is that the edges are sorted alphabetically, but they are not grouped like they are within the rulebook. By default, all tables list their contents alphabetically unless we specify something else. This is accomplish via the use of a "sort set". Open the file "control.1st" and scroll towards the bottom, where you'll find a number of "sortset" elements. These elements define the various sort sets that are provided by the Skeleton files. We need to add a new one that will sort the edges first by their type and then by name within each type. Each sort key within a sort set can be either a field or a tag group, specified by its unique id, and the order of the sort keys dictates the ultimate ordering of the items. We want our edges to be sorted by the type and then the name, so we'll need two sort keys. Putting it all together yields a sort set that looks similar to the one shown below.
<sortset id="Edge" name="Edge By Type and Name"> <sortkey isfield="no" id="Edge"/> <sortkey isfield="no" id="_Name_"/> </sortset>
Once the sort set is defined, we can reference it from the table portal so that all edges are shown to the user in the order dictated by the sort set. This is accomplished by adding the "choosesortset" attribute to the "table_dynamic" element of the table portal and specifying the id of our new sort set. The new attribute should look like below.
choosesortset="Edge"
After making this change and experimenting with edges again, the behavior just doesn't seem very intuitive. While we've now listed the edges in the same organization as the rulebook, there is no clear structural association to the rulebook here. Coupled with the fact that there are some edge types with only a few edges and others with lengthy lists, this new ordering is likely to be more confusing than helpful to most users. Consequently, we're probably better off not using the new sort set and leaving the list in a purely alphabetical order. This is the sort of choice that you'll need to make at times. Sometimes, it's better to emulate the rulebook's organization, while sometimes it's better to use a different approach that is better suited to the way users will be putting your data files to work.
Adding Hindrances
Once we have a basic tab in place with the "Edges" table, we need to add tables for selection of hindrances and rewards. We'll handle hindrances next, since we should place those immediately below the table of edges. We put them below, because the list of edges will evolve as the character advances, while the hindrances will be fixed after character creation.
Our first step is to add a table portal in which we can manage the hindrances. Since hindrances have a number of complexities we have to deal with, we will need our own custom template instead of being able to use the "SimpleItem" template. However, using the "SimpleItem" template on an interim basis makes it easy for us to get the table added, so that's what we'll do.
We copy the table portal for edges, then we adapt it for our needs. This amounts to changing all edge references over to hindrances. Hindrances don't have a resource associated with them, so we must also revise a few of the scripts slightly to eliminate references to the resource. The net result is the portal below.
<portal id="edHinders" style="tblNormal"> <table_dynamic component="Hindrance" showtemplate="SimpleItem" choosetemplate="SimpleItem" addspace="2"> <titlebar><![CDATA[ @text = "Add a Hindrance" ]]></titlebar> <headertitle><![CDATA[ @text = "Hindrances" ]]></headertitle> <additem><![CDATA[ ~if we're in advancement mode, we've been frozen, so display accordingly if (state.iscreate = 0) then @text = "{text a0a0a0}Cannot Add Hindrances After Creation" done endif ~be sure to use a suitable color for a purely optional choice @text = "{text a0a0a0}Add New Hindrance" ]]></additem> </table_dynamic> </portal>
Our portal has now been added to the data files, but nothing is actually being done with it. We still need to integrate it into the layout and position it properly. Adding it to the layout is simple. We add a new "portalref" element that references the table portal. Since the table will be beneath the edges table, we'll assign a suitable tab order to ensure the flow proceeds from edges to hindrances. This results in the new element shown below.
<portalref portal="edHinders" taborder="20"/>
The next step is positioning the table portal properly. We'll reconcile the concerns of juggling three tables within the panel once they are all defined. For now, we'll simply position the new table beneath the table of edges. That can be achieved by adding the following lines of code to the Position script in the layout.
portal[edHinders].width = width portal[edHinders].top = portal[edEdges].bottom + 10
Customizing Hindrances
Now that the hindrances table portal is in place, we can customize how hindrances are displayed and behave. Although some hindrances are simple and involve only a name, there are some for which the user can selectively choose whether it is major or minor in impact. In addition, some hindrances need to specify a domain, just like skills. We need to provide our own template in order to support these behaviors.
Before we do anything, we need to get a new template that we can customize. We're currently using the "SimpleItem" template, and it would be an excellent starting point for our purposes. The template used for skills would also be an excellent starting point, so you could just as easily start with either one. We'll use the "SimpleItem" template, since that's what you will probably be using most often for this purpose. Open the file "visual.dat" and locate the "SimpleItem" template. Copy it into the file "tab_edges.dat", where we can adapt it. Change the unique id to something suitable, such as "edHinder", and the component set to "Hindrance". Eliminate the extra logic in the Position script that is expressly for use when showing things, since we're always showing picks. Lastly, we need to hook the new template into the table portal, so go to the "edHinders" portal and change the "choosetemplate" to reference our new "edHinder" template. At this point, you should be able to re-compile and load, with no visible change in behavior.
We can now adapt the template to our needs. Many hindrances have a fixed severity rating, and we should be sure to display that clearly to the user. The name of each hindrance is currently shown as a field-based label that simply shows the value of the field. We'll change that to a script-based label, wherein we'll start with the name and append an indication of whether the hindrance is major or minor. If the hindrance has a user-controlled severity, we'll leave the name unchanged and show the state differently, as outlined further below. The net result is a revised "name" portal that should look something like the following.
<portal id="name" style="lblNormal" showinvalid="yes"> <label> <labeltext><![CDATA[ @text = field[name].text if (tagis[User.UserSelect] = 0) then if (field[hinMajor].value = 0) then @text &= " (Minor)" else @text &= " (Major)" endif endif ]]></labeltext> </label> </portal>
For user-controlled hindrances, we need to add a new portal of some sort that allows the user to designate the severity. One option is to use a checkbox portal. A simple checkbox would probably look quite poor, so we won't consider that further, However, we could also use a bitmap-based checkbox. With a bitmap-based checkbox, we create two separate bitmaps to indicate the two different states and rely on the bitmaps indicating the state to the user. This technique is used in a few places for different game systems and can be very useful. The problem with this approach is how we can clearly convey "major" versus "minor" to the user via bitmaps - not an easy proposition.
Fortunately, we can also use a menu, just like we do for the character's gender on the "Personal" tab. We'll start by opening the file "tab_personal.dat", locating the "gender" portal, and copying it into the template we're working with so we can adapt it. We'll change the id to "severity" and hook it up to the "hinMajor" field. Then we can change the list of choices that the user is shown, making sure that the value zero reflects "minor" and one reflects "major", since that's the convention we've already established for the "hinMajor" field. We can designate either value as the default selection, but we'll choose "minor", since that's more likely what the user will be wanting to choose for a character.
The portal is added, so we now need to position it properly and control when it is made visible. We'll add the appropriate logic to the Position script for the template, keying on the presence of the "User.UserSelect" tag and positioning to the right of the hindrance name. The additions made to the Position script should look similar to the code below.
~center the portal vertically perform portal[severity].centervert ~if we don't need a menu, hide the portal var edge as number if (tagis[User.UserSelect] = 0) then portal[severity].visible = 0 edge = portal[name].right ~otherwise, position the menu portal to the right of the name/domain else perform portal[severity].alignrel[ltor,name,15] portal[severity].width = 55 edge = portal[severity].right endif
In addition to the severity menu, some portals also require a user-specified domain, just like certain skills. To support this, we need to add two portals that are virtually identical to the ones we used in the table of skills. So open the file "tab_skills.dat" and locate the "lbldomain" and "domain" portals within the "skPick" template. Copy them into the "edHinder" template that we're working on. The only aspect we need to change is the field associated with the "domain" edit portal, which needs to be hooked up to the "domDomain" field for hindrances. At this point, the two new portals should look similar to the ones below.
<portal id="lbldomain" style="lblSecond"> <label text="Domain:"> </label> </portal> <portal id="domain" style="editNormal"> <edit field="domDomain"> </edit> </portal>
Once the portals are properly defined, we need to position them and control their visibility. If the domain is not needed, we simply hide it, else we position the domain to the right of the name - or the severity menu, if any. The new logic required in the Position script should look like below.
~center the portals vertically perform portal[domain].centervert perform portal[lbldomain].alignrel[btob,domain,0] ~if we don't need a domain, hide the portals if (tagis[User.NeedDomain] = 0) then portal[lbldomain].visible = 0 portal[domain].visible = 0 ~otherwise, position the domain portals next to the name, making sure the ~domain edit portal doesn't run beyond the available space else portal[lbldomain].left = edge + 15 perform portal[domain].alignrel[ltor,lbldomain,5] portal[domain].width = minimum(150,portal[info].left - portal[domain].left - 10) endif
Adding Rewards
The one remaining table we need is for selecting rewards, which we'll place beneath hindrances. Rewards are very simple, consisting of only a name for display. As such, they don't need to take up the full width of the tab and we can display them in two columns instead of just one. We'll start by adding a table portal in which we can manage the rewards, and we'll configure it for two columns of display. Due to the simplicity of rewards, we can re-use the "SimpleItem" template and avoid having to define a new template. So we copy the table portal for edges and adapt it for our needs. This amounts to changing all references to edges over to rewards, as well as changing the edges resource over to the hindrance resource for tracking how many rewards are left to be selected by the user. Except for changing to two columns, everything else remains the same, resulting in the portal shown below.
<portal id="edRewards" style="tblNormal"> <table_dynamic component="Reward" showtemplate="SimpleItem" choosetemplate="SimpleItem" addthing="resHinder" addspace="2" columns="2"> <titlebar><![CDATA[ @text = "Add an Reward - " & hero.child[resHinder].field[resSummary].text ]]></titlebar> <headertitle><![CDATA[ @text = "Rewards - " & hero.child[resHinder].field[resSummary].text ]]></headertitle> <additem><![CDATA[ ~if we're in advancement mode, we've been frozen, so display accordingly if (state.iscreate = 0) then @text = "{text a0a0a0}Cannot Add Rewards After Creation" done endif ~get the color-highlighted "add" text @text = field[resAddItem].text ]]></additem> </table_dynamic> </portal>
Just like we did for the hindrances table, we must now integrate the portal into the layout and position it properly. Adding it to the layout is achieved by adding a new "portalref" element that reference the table portal. Since rewards will be at the bottom, we'll assign a tab order after the others. This results in the new element shown below.
<portalref portal="edRewards" taborder="30"/>
The final step is positioning the table portal properly. Just to get things working quickly, we position the rewards table beneath the table of hindrances by adding the following lines of code to the Position script in the layout.
portal[edRewards].width = width portal[edRewards].top = portal[edHinders].bottom + 10
Intelligent Positioning
We now have all three tables in place, but their positioning logic is incredibly crude. If enough edges are added, both the hindrances and rewards table could completely disappear beyond the bottom of the tab. So we need to change to logic that is more intelligent.
We start by establishing suitable minimum heights for the tables of hindrances and rewards, calculating the vertical space remaining. We then allow the edges table to use as much vertical space as remains when those minimums are factored in. If the edge table doesn't need all the space, then the hindrances table is first allowed to expand as much as necessary. Lastly, if there is still unused space remaining, the rewards table is given an opportunity to expand. This logic ensures that we optimize our use of the vertical space, giving preference to edges and then hindrances, while we also ensure that the maximum amount of edge table is kept visible at all times. The net result is the Position script shown below.
~freeze our tables in advancement mode to disable adding new choices ~Note: All freezing must be done *before* any positioning is performed. if (state.iscreate = 0) then portal[edEdges].freeze = 1 portal[edHinders].freeze = 1 portal[edRewards].freeze = 1 endif ~size all tables to span the full layout width portal[edEdges].width = width portal[edHinders].width = width portal[edRewards].width = width ~set the height of the small tables to be a maximum number of rows for now portal[edHinders].maxrows = 3 portal[edRewards].maxrows = 1 ~determine the vertical gap we want to use between tables var gap as number gap = 15 ~position the rewards table at the bottom portal[edRewards].top = height - portal[edRewards].height ~position the hindrances table above the rewards portal[edHinders].top = portal[edRewards].top - gap - portal[edHinders].height ~assign the remaining space to the edges table portal[edEdges].height = portal[edHinders].top - gap - portal[edEdges].top ~position the hindrances table beneath the edges table and let it expand to ~fill whatever vertical space is available between the edges and rewards portal[edHinders].top = portal[edEdges].bottom + gap portal[edHinders].height = portal[edRewards].top - gap - portal[edHinders].top ~position the rewards table beneath hindrance and let it expand to fill space portal[edRewards].top = portal[edHinders].bottom + gap portal[edRewards].height = height - portal[edRewards].top
Reporting Errors
Earlier in the evolution process of our data files, we established panel linkages to the "basics" panel for edges, rewards, and hindrances. Consequently, if we add one of those three items to the character and the item is invalid in some way, the "Basics" panel will turn red to highlight the error to the user. Now that we have an appropriate panel in place for those three objects, we need to highlight the proper panel when errors are encountered. This is simply a matter of opening the file "traits.pri" and changing the "panellink" attribute of the "Edge", "Hindrance", and "Reward" components. Change the attribute to associate the new "edges" panel with the component and everything is hooked up properly.