Allies (Savage): Difference between revisions
(5 intermediate revisions by the same user not shown) | |||
Line 298: | Line 298: | ||
var rankvalue as number | var rankvalue as number | ||
var ranktext as string | var ranktext as string | ||
rankvalue = | rankvalue = field[acRank].value | ||
call RankName | call RankName | ||
recap &= ranktext & " (" & #resmax[resXP] & " XP)" | recap &= ranktext & " (" & #resmax[resXP] & " XP)" | ||
Line 429: | Line 429: | ||
If we reload the files now, we'll see the "Allies" tab appear normally for our main characters, but it disappears when we're manipulating an ally. | If we reload the files now, we'll see the "Allies" tab appear normally for our main characters, but it disappears when we're manipulating an ally. | ||
Latest revision as of 21:05, 29 January 2009
Context: HL Kit … Authoring Examples … Savage Worlds Walk-Through
Overview
The Savage Worlds game system emphasizes the use of allies. As such, our data files should provide for the creation and management of allies associated with the characters.
Setting Up Ally Support
Allies are essentially independent characters that are children of the main characters. The Kit provides a framework for handling such characters very easily via the "minion" mechanism. Minions work very similarly to gizmos. They are attached to a character via a thing, and the minion is automatically added to the character whenever a pick based on that thing is added. Deleting the pick also deletes the minion. The parent character of a minion is referred to as the "master".
Just about any "thing" can have a minion attached. However, we want to be able to readily identify the picks that add our allies from other picks. We'll define a new component and component set to accomplish this. We'll assign the component a unique id of "Ally" and have it automatically define a component set with the same id, since we don't need to re-use any other special behaviors.
It would also be useful to let the user enter some arbitrary notes about each ally that can be used to readily identify them. For this, we'll include a field on the component where we can store those details. This results in a component set that looks like below, which we can add to the file "miscellaneous.str".
<component id="Ally" name="Ally"> <!-- Brief summary of ally for display in the list of allies --> <field id="alySummary" name="Summary" type="user" maxlength="100"> </field> </component>
Defining the Minion
We can now define a thing based on our new component set. We can add the thing to the file "thing_miscellaneous.dat". The only item of note about this thing is that the the minion will be attached by it.
Associating a minion with a thing is accomplished via the "minion" child element. Each minion needs to be assigned a unique id, which makes it possible to identify different minions when multiple types of minions are added to a character. We'll only have one type of minion, but we need to assign a unique id anyway.
The other important facet of our minion is that we want it to inherit all of the settings associated with the master character. For example, if the master has only the "Futuristic" time period selected, then we want to assume that the minion has the same behaviors. This is achieved via the "isinherit" attribute within the "minion" element.
Putting this all together, we end up with a thing definition that looks like the following.
<thing id="mscAlly" name="Ally" compset="Ally"> <minion id="ally" isinherit="yes"> </minion> </thing>
Manipulating Allies
We now need to figure out how to let users add and manage allies. We could add allies to an existing tab, but none of them really seem appropriate. There also is a space consideration, as many of our tabs are already quite packed with information. Since we only have a rather small number of tabs, it would be quite reasonable to add another tab for tracking allies.
When adding our tab, we'll want something very simple. We'll have a single table on the tab where the user can add and access allies. The "Skills" tab is very similar, so we'll copy the file "tab_skills.dat" as "tab_allies.dat" and then adapt the file to our needs.
The first thing we need to do is revise the table portal at the top. All allies will be attached to the character via the same "mscAlly" thing that we defined above. Consequently, we need to utilize an "auto" table that automatically adds a new pick based on a specific thing instead of prompting the user to select a thing. This requires that we specify the thing id to be used. We also need to utilize a custom template for showing the contents of each ally. The resulting portal should look like the one shown below.
<portal id="alAllies" style="tblNormal"> <table_auto component="Ally" showtemplate="alPick" autothing="mscAlly"> <headertitle><![CDATA[ @text = "Allies Associated with Character" ]]></headertitle> <additem><![CDATA[ @text = "Add a New Ally to the Character" ]]></additem> </table_auto> </portal>
For the moment, we'll keep the template very simple. We'll start with just the name of the pick, plus the standard info and delete portals. We'll come back in a moment to refine the template and make it more useful. This yields a template like the one below.
<template id="alPick" name="Ally Pick" compset="Ally" 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/> </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 the 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 and center other portals vertically portal[info].top = 0 perform portal[name].centervert perform portal[delete].centervert ~position the delete and info portals on the far right perform portal[delete].alignedge[right,0] perform portal[info].alignrel[rtol,delete,-8] ~position the name on the left portal[name].left = 0 ]]></position>
The next step is to revise the layout to show allies. All that entails is a switch to the new ids, which looks like below.
<layout id="allies"> <portalref portal="alAllies" taborder="10"/> <position><![CDATA[ ~position and size the table to span the full layout; it will only use the ~vertical space that it actually needs perform portal[alAllies].autoplace ]]></position> </layout>
The final step is to modify the panel. We'll position the new tab between the "Personal" and "Journal" tabs, which means we need to assign it an order of 315. This yields the following panel.
<panel id="allies" name="Allies" marginhorz="5" marginvert="5" order="315"> <layoutref layout="allies"/> <position><![CDATA[ ]]></position> </panel>
We can now give things try. Reload the data files and you should see the new "Allies" tab. On the tab is a table, and clicking on the "add item" of the table automatically adds a new ally pick to the character. When this happens, you should also see a new character appear on the Dashboard. This is our new ally.
If you switch to ally via the Dashboard, you can see that the ally is a standard character. You can also verify that the various settings associated with the master character are properly inherited into the minion. At the top left of the minion, next to the name, a button should appear. This button allows you to quickly return to the master of the minion by clicking on it. Click the button and you should again be looking at the master character. Now delete the ally pick from the table, at which point our minion disappears. The basics of allies are now operational.
Revising the Template
We should now do something more useful that just show the name of our allies. One thing that would be extremely useful is to add a button that lets the user go directly to a particular ally. We can always rely on the Dashboard for this, but a button next to each ally would be much nicer.
We want to accomplish the exact same behavior as the Dashboard, and we should probably use the exact same button for consistency. So take a look at how the Dashboard accomplishes this. It uses a special action portal that handles all the mechanics automatically. We'll copy the portal into our template and re-use all that same logic.
When we defined the "Ally" component, we included a field where the user can specify details about the character. We should show an edit portal next to the name that allows the user to edit those details. We'll size the edit portal based on whatever space exists between the ally name and the "info" portal on the right.
This results in a revised template that looks like the one below.
<template id="alPick" name="Ally Pick" compset="Ally" marginhorz="3" marginvert="2"> <portal id="load" style="actLoad" tiptext="Click here to make this the active character."> <action action="minion"> </action> </portal> <portal id="name" style="lblNormal" showinvalid="yes"> <label field="name"> </label> </portal> <portal id="summary" style="editNormal"> <edit field="alySummary"> </edit> </portal> <portal id="info" style="actInfo"> <action action="info"> </action> <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 the 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 and center other portals on it portal[info].top = 0 perform portal[name].centeron[vert,info] perform portal[delete].centeron[vert,info] perform portal[load].centeron[vert,info] perform portal[summary].alignrel[btob,name,2] ~position the delete and info portals on the far right perform portal[delete].alignedge[right,0] perform portal[info].alignrel[rtol,delete,-8] ~position the load portal on the left, with the name and summary adjacent portal[load].left = 0 perform portal[name].alignrel[ltor,load,8] perform portal[summary].alignrel[ltor,name,10] portal[summary].width = portal[info].left - 10 - portal[summary].left ]]></position> </template>
If we reload the files, we can use the button next to the name to go directly to a given ally, plus we can enter notes about the ally for easy access and viewing.
Recap Summary
Showing the name of each ally and a few summary notes is of limited use. What would be ideal is if we could actually show a detailed summary of each ally, much like the contents of a statblock. We could easily show this summary beneath the current information for each ally. However, we need to have the summary available.
We could generate the summary on-the-fly via a Label script. However, we may also want to show the recap else where. For example, within the mouse-info for the ally. In order to make sure we can have access to the recap from various places, we need to synthesize the results into a field. This field can be added to the "Actor" component and generated via an Eval script.
The contents of the recap summary should be as compact as possible. Consequently, we'll minimize spacing and punctuation to the minimum necessary. We'll also use abbreviations and short names wherever possible. The resulting field and script are demonstrated below.
<field id="acRecap" name="Recap Summary" type="derived" maxlength="2000"> </field> <eval index="5" phase="Render" priority="10000"><![CDATA[ var txt as string var recap as string ~output any race txt = hero.firstchild["Race.?"].field[name].text if (empty(txt) = 0) then recap &= txt & ", " endif ~output the XP and rank var rankvalue as number var ranktext as string rankvalue = field[acRank].value call RankName recap &= ranktext & " (" & #resmax[resXP] & " XP)" ~output attributes foreach pick in hero where "component.Attribute & !Hide.Attribute" recap &= ", " & eachpick.field[trtAbbrev].text & " " & eachpick.field[trtDisplay].text nexteach ~output derived traits foreach pick in hero where "component.Derived & !Hide.Trait" sortas explicit recap &= ", " & eachpick.field[trtAbbrev].text & " " & eachpick.field[trtDisplay].text nexteach ~output special abilities foreach pick in hero where "component.Ability" sortas SpecialTab recap &= ", " & eachpick.field[shortname].text nexteach ~output arcane powers foreach pick in hero where "component.Power" recap &= ", " & eachpick.field[name].text nexteach ~output skills foreach pick in hero where "component.Skill & !Hide.Skill" recap &= ", " & eachpick.field[trtAbbrev].text if (eachpick.tagis[User.NeedDomain] <> 0) then recap &= " (" & eachpick.field[domDomain].text & ")" endif recap &= " " & eachpick.field[trtDisplay].text nexteach ~save the final contents field[acRecap].text = recap ]]></eval>
Now that we've got the field being synthesized, we can put it to use. We need to add a new portal to the template for displaying the field. We'll allocate three lines of text to the recap for each ally, which should allow us to show a handful of allies at a time when the main window is at its smallest height. In the interest of keeping things as tight as possible, we'll also decrease the spacing between lines by a one pixel. This results in the following portal.
<portal id="recap" style="lblSmlLeft"> <label ismultiline="yes"> <labeltext><![CDATA[ ~output the recap field, but squeeze the line spacing a little bit @text = "{leading -1}" & minion.herofield[acRecap].text ]]></labeltext> </label> </portal>
We need to factor the height of the new portal into our overall height for the template. Once that's done, we can then place the portal beneath the current line of portals, leaving a margin on each side to set it off better and clearly break up individual allies in the table. The pertinent changes and additions to the Position script are shown below.
~set up our height based on our full extent height = portal[summary].height + 5 + portal[recap].fontheight * 3
~position the recap portal beneath the top line and limit it to 3 lines perform portal[recap].alignrel[ttob,summary,3] portal[recap].lineheight = 3 ~position and size the recap horizontally perform portal[recap].alignrel[ltol,name,0] portal[recap].width = portal[delete].left - 10 - portal[recap].left ~resize the contents of the recap portal if needed and ensure a 3-line height perform portal[recap].sizetofit[30] portal[recap].lineheight = 3
After reloading the data files, the recap text is quite helpful. Unfortunately, it's also still a bit too big and bold. It's competing with the primary information for each ally instead of being clearly supporting information. We need to switch to a different style that uses a smaller font and a less intense color. We could use a soft grey, but that's a little too subtle, so we'll choose a soft cyan instead. This yields the new style shown below, which can be swapped into use by the portal.
<style id="lblNotes"> <style_label textcolor="99efed" font="fntnotes" alignment="left"> </style_label> <resource id="fntnotes"> <font face="Arial" size="34"> </font> </resource> </style>
Refinements
Allies are basically working, but there are still a few things we should clean up. First of all, the mouse-info text shown for each ally is just the standard name and description text. We should really show the full recap information for the ally, since the three lines of space we've allocated may not be enough in some cases. This requires that we change the MouseInfo script from using the default behavior to more appropriate custom behavior.
We don't want to replace the standard behavior entirely, though. What we want is to append additional information at the end of the standard material. To accomplish this, we'll call the "MouseInfo" procedure to get the standard information and then append our own data at the end. This yields a new MouseInfo script that looks like the following.
<mouseinfo><![CDATA[ var mouseinfo as string call MouseInfo @text = mouseinfo & "{br}{br}{b}Ally Summary:{/b}{br}" & minion.herofield[acRecap].text ]]></mouseinfo>
Another issue with our implementation can be seen when we switch to an ally. The "Allies" tab is visible for our allies, which means that we can theoretically add allies to our allies, and those allies can have their own allies as well. While this is technically valid, it doesn't make sense within the context of a Savage Worlds game. So we need to hide the "Allies" tab for allies.
Hiding the "Allies" tab in general is easy, and we've done it before. First, we define a new "HideTab.allies" tag. Then we add a Live tag expression to the panel that verifies the tag doesn't exist on the character. But how do we get the tag onto the character properly?
Remember that minions work very much like gizmos. We can assign tags to a child entity within the definition by use of the "tag" element on the entity. We can do the same within our "minion" element. All we need to do is an additional line to our thing, which ends up looking like the following.
<thing id="mscAlly" name="Ally" compset="Ally"> <minion id="ally" isinherit="yes"> <tag group="HideTab" tag="allies"/> </minion> </thing>
If we reload the files now, we'll see the "Allies" tab appear normally for our main characters, but it disappears when we're manipulating an ally.