Allies (Savage): Difference between revisions
Line 487: | Line 487: | ||
====The Ally Template==== | ====The Ally Template==== | ||
The template itself will consist of four pieces that will each be handled by a separate portal. At the top of each ally, we'll display a title that looks like a standard title above a table. Beneath the title, we'll output all of the details for the character, including traits, abilities, and gear. At the bottom, we'll present a convenient place for tracking health and other status for the character. Between the details and tracking sections, we'll insert a solid line as a separator. | |||
The overall template will look like the one shown below. This template omits the contents of the Label scripts that will be employed for three of the portals. Those contents will be discussed in the following sections. | |||
<pre> | <pre> | ||
Line 534: | Line 536: | ||
<position><![CDATA[ | <position><![CDATA[ | ||
~our name spans the entire width and is limited to a single line in height | |||
portal[name].width = width | portal[name].width = width | ||
portal[name]. | portal[name].lineheight = 1 | ||
~position the details beneath the name, spanning the full width | |||
perform portal[details].alignrel[ttob,name,10] | perform portal[details].alignrel[ttob,name,10] | ||
portal[details].width = width | portal[details].width = width | ||
portal[details]. | perform portal[details].autoheight | ||
~position the line beneath the details, spanning the full width; we set the | |||
~height to double the border size so that no contents are visible and we | |||
~end up with simply a solid horizontal line | |||
perform portal[line].alignrel[ttob,details,8] | perform portal[line].alignrel[ttob,details,8] | ||
portal[line].width = width | portal[line].width = width | ||
portal[line].height = portal[line].bordersize * 2 | portal[line].height = portal[line].bordersize * 2 | ||
~position the tracking beneath the line, spanning the full width | |||
perform portal[tracking].alignrel[ttob,line,5] | perform portal[tracking].alignrel[ttob,line,5] | ||
portal[tracking].width = width | portal[tracking].width = width | ||
portal[tracking]. | perform portal[tracking].autoheight | ||
~our final height is the bottom of the template contents | |||
height = portal[tracking].bottom | height = portal[tracking].bottom | ||
]]></position> | ]]></position> |
Revision as of 09:28, 28 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 = herofield[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.
Printing Allies
Allies are now working nicely. Each ally can be printed out on a separate character sheet, just like a normal character. However, it would be much more convenient to print a compact representation of each ally with the main character. We don't have room for allies on the first page of the character sheet, but we should be able to add them without any problem on the second page.
We'll add allies after everything else is output for the character. We can define a new table of allies and integrate it easily into the layout for the second sheet. In addition to printing all the basic details for each ally, we also need to include a separate space to track damage, power points, and ammo for each ally.
The Table Portal
Our table portal is a little bit different from the ones we normally define. We need our table to list all minions of the character. This can be done by operating on all picks in the "Ally" component, just like we did in the table on the tab. However, there is a better solution. We can instead specify that we want to list all minions via the "showpicks" attribute. This establishes the "actor" pick for the actual minion as the context for each item in the table, and that means that we can use the "hero" context readily within Label scripts in the template's portals.
For the sort set, we'll use the pre-defined "_ActorSeq_" sequence, which outputs the minions in an appropriate order for the character. Since our minions will not all be the same height, we need to specify that our height varies. Lastly, we don't want a header above the table. Instead, we'll draw our own header above each item in the table so that each minion looks like its own entry to the user. Consequently, we want to insert a gap between each item in the table, so we specify the "showgapy" attribute.
The net result of all of this is a table portal that looks like the following.
<portal id="oAlly" style="outNormal"> <output_table component="Ally" showtemplate="oAllyPick" showpicks="minion" showsortset="_ActorSeq_" varyheight="yes" showgapy="25"> </output_table> </portal>
Layout Integration
Integrating our new portal into the layout is trivial. We simply add a new "portalref" for the portal and then add a suitable "autoplace" of the portal within the Position script. The revised layout looks like the following.
<layout id="oStandard2"> <portalref portal="oPower"/> <portalref portal="oArmor"/> <portalref portal="oWeapon"/> <portalref portal="oGear"/> <portalref portal="oAlly"/> <position><![CDATA[ ~position the various tables in the desired sequence perform portal[oPower].autoplace perform portal[oArmor].autoplace perform portal[oWeapon].autoplace perform portal[oGear].autoplace perform portal[oAlly].autoplace ~the height of the layout is the bottommost extent of the elements within height = autotop ]]></position> </layout>
The Ally Template
The template itself will consist of four pieces that will each be handled by a separate portal. At the top of each ally, we'll display a title that looks like a standard title above a table. Beneath the title, we'll output all of the details for the character, including traits, abilities, and gear. At the bottom, we'll present a convenient place for tracking health and other status for the character. Between the details and tracking sections, we'll insert a solid line as a separator.
The overall template will look like the one shown below. This template omits the contents of the Label scripts that will be employed for three of the portals. Those contents will be discussed in the following sections.
<template id="oAllyPick" name="Output Allies Table" compset="Actor"> <portal id="name" style="outTitle"> <output_label> <labeltext><![CDATA[ ~insert script here ]]></labeltext> </output_label> </portal> <portal id="details" style="outAlly"> <output_label> <labeltext><![CDATA[ ~insert script here ]]></labeltext> </output_label> </portal> <portal id="line" style="outGreyBox"> <output_label text=" "> </output_label> </portal> <portal id="tracking" style="outAlly"> <output_label> <labeltext><![CDATA[ ~insert script here ]]></labeltext> </output_label> </portal> <position><![CDATA[ ~our name spans the entire width and is limited to a single line in height portal[name].width = width portal[name].lineheight = 1 ~position the details beneath the name, spanning the full width perform portal[details].alignrel[ttob,name,10] portal[details].width = width perform portal[details].autoheight ~position the line beneath the details, spanning the full width; we set the ~height to double the border size so that no contents are visible and we ~end up with simply a solid horizontal line perform portal[line].alignrel[ttob,details,8] portal[line].width = width portal[line].height = portal[line].bordersize * 2 ~position the tracking beneath the line, spanning the full width perform portal[tracking].alignrel[ttob,line,5] portal[tracking].width = width perform portal[tracking].autoheight ~our final height is the bottom of the template contents height = portal[tracking].bottom ]]></position> </template>
Showing the Title
@text = hero.actorname if (empty(hero.miniontext) = 0) then if (compare(hero.miniontext,@text) <> 0) then @text &= " (" & hero.miniontext & ")" endif endif
Character Details
var txt as string var ismore as number ~output any notes for the ally if (anchor.field[alySummary].isempty = 0) then @text &= "{b}Notes:{/b} " & anchor.field[alySummary].text & "{br}" endif ~output any race txt = hero.firstchild["Race.?"].field[name].text if (empty(txt) <> 0) then txt = "-none-" endif @text &= "{b}Race:{/b} " & txt ~output the rank and XP var rankvalue as number var ranktext as string rankvalue = herofield[acRank].value call RankName @text &= "; {b}" & ranktext & "{/b} (" @text &= hero.child[resXP].field[resMax].text & " XP)" @text &= "{br}" ~output attributes @text &= "{b}Attributes:{/b} " ismore = 0 foreach pick in hero where "component.Attribute & !Hide.Attribute" if (ismore <> 0) then @text &= ", " endif @text &= eachpick.field[trtAbbrev].text & " " & eachpick.field[trtDisplay].text ismore = 1 nexteach @text &= "{br}" ~output derived traits @text &= "{b}Traits:{/b} " ismore = 0 foreach pick in hero where "component.Derived & !Hide.Trait" sortas explicit if (ismore <> 0) then @text &= ", " endif @text &= eachpick.field[trtAbbrev].text & " " & eachpick.field[trtDisplay].text ismore = 1 nexteach @text &= "{br}" ~output special abilities (if any) if (hero.haschild["component.Ability"] <> 0) then @text &= "{b}Abilities:{/b} " ismore = 0 foreach pick in hero where "component.Ability" sortas SpecialTab if (ismore <> 0) then @text &= ", " endif @text &= eachpick.field[shortname].text ismore = 1 nexteach @text &= "{br}" endif ~output arcane powers (if any) if (hero.haschild["component.Power"] <> 0) then @text &= "{b}Arcane Powers (" & #trkmax[trkPower] & "):{/b} " ismore = 0 foreach pick in hero where "component.Power" if (ismore <> 0) then @text &= ", " endif @text &= eachpick.field[name].text ismore = 1 nexteach @text &= "{br}" endif ~output skills @text &= "{b}Skills:{/b} " ismore = 0 foreach pick in hero where "component.Skill & !Hide.Skill" if (ismore <> 0) then @text &= ", " endif @text &= eachpick.field[trtAbbrev].text if (eachpick.tagis[User.NeedDomain] <> 0) then @text &= " (" & eachpick.field[domDomain].text & ")" endif @text &= " " & eachpick.field[trtDisplay].text ismore = 1 nexteach @text &= "{br}" ~output weapons (if any) if (hero.haschild["component.WeaponBase"] <> 0) then @text &= "{b}Weapons:{/b} " ismore = 0 foreach pick in hero where "component.WeaponBase" sortas Armory if (ismore <> 0) then @text &= ", " endif @text &= eachpick.field[name].text @text &= " " & eachpick.field[wpNetAtk].text @text &= " (" & eachpick.field[wpDamage].text if (eachpick.field[wpPiercing].value <> 0) then @text &= ", AP" & eachpick.field[wpPiercing].text endif if (eachpick.tagis[component.WeapRange] <> 0) then @text &= ", " & eachpick.field[wpRange].text endif @text &= ")" ismore = 1 nexteach @text &= "{br}" endif ~output armor and gear together (if any) if (hero.haschild["component.Defense | component.Equipment"] <> 0) then ~output armor ismore = 0 foreach pick in hero where "component.Defense" sortas Armory if (ismore <> 0) then @text &= ", " endif @text &= eachpick.field[name].text @text &= " (" & signed(eachpick.field[defDefense].text) if (eachpick.tagis[component.Shield] <> 0) then if (eachpick.field[defParry].value <> 0) then @text &= ", Parry" & signed(eachpick.field[defParry].text) endif endif @text &= ")" ismore = 1 nexteach ~output gear foreach pick in hero where "component.Equipment" if (ismore <> 0) then @text &= ", " endif @text &= eachpick.field[grStkName].text ismore = 1 nexteach @text &= "{br}" endif
Status Tracking
var box as string var gap as string ~output mechanism for tracking health gap = "{horz 50}" @text &= "{b}Wounds:" & gap if (herofield[acIsWild].value <> 0) then @text &= "-1" & gap & "-2" & gap & "-3" & gap endif @text &= "INC" @text &= "{horz 100}Fatigue:" & gap @text &= "-1" & gap & "-2" & gap & "INC" @text &= "{/b}{br}" ~output boxes for tracking power points (if applicable) if (hero.haschild["component.Power"] <> 0) then @text &= "{vert 5}" @text &= "{b}Power:{/b}{horz 40}" var i as number var j as number @text &= "{font wingdings 2}" for i = 1 to 5 for j = 1 to 4 @text &= chr(163) next @text &= "{size 44}" & chr(163) & "{size 36}" next @text &= "{revert}{br}" endif ~output boxes for tracking the ammo supply (if any ranged weapons) if (hero.haschild["component.WeapRange"] <> 0) then @text &= "{vert 8}" box = "{font Wingdings 2}{size 40}" & chr(163) & "{revert}" gap = "{horz 65}" @text &= "{b}Ammo:{/b}" & gap & "Full " & box & gap & "High " & box & gap & "Low " & box & gap & "Out " & box endif