Difference between revisions of "Follower tutorial"

From Dragon Age Toolset Wiki
Jump to: navigation, search
(Capture the EVENT_TYPE_PARTYMEMBER_ADDED Event)
m (Split tutorials in this, Basic follower tutorial and Follower tutorial (alternative) to reduce page size; added category)
 
Line 1: Line 1:
== Simple Follower Creation ==
 
 
Follow these steps to create a follower that
 
 
- Levels up with a default package
 
 
- Can be chosen from the party picker
 
 
- Can gain XP
 
 
This guide assumes you know how to create a creature and are comfortable with basic scripting.
 
 
You should only use this simple method if you are sure there will be empty space in the active party when your follower is recruited.
 
 
A more comprehensive approach when there are more than three potential party members is discussed in the FAQ below. It's worth taking the time to understand the basics first, though.
 
 
=== Create the creature ===
 
 
Create a creature to act as your follower.  Set its name, appearance, gender, head morph, conversation, inventory etc as you want them to behave in-game.
 
 
Set an appropriate '''Tag''' (you'll be using it a lot).  I suggest "party_charname".
 
 
Make sure you choose a '''Class'''.  For most followers this should be Rogue, Warrior or Wizard.
 
 
[[File:class.jpg]]
 
 
 
Under Package/Scaling set:
 
 
'''General Package Type''' to be '''Party Members'''
 
 
'''Package''' to be an appropriate value (probably "Generic - Wizard" or similar)
 
 
'''Package AI''' (there should only be one choice)
 
 
'''Rank''' to be '''Player'''
 
 
[[File:package.jpg]]
 
 
Save and export your character as normal.
 
 
 
=== Overlay char_stage ===
 
 
In the official campaign, the party picker uses a special area called char_stage. Whether you're extending the official campaign or making a standalone campaign, there is a function that allows you to overlap the offical stage with your own, so that both stages are active simultaneously in game.
 
 
In your resource palette, right-click char_stage under the Global folder and select Duplicate to make a copy of the stage with a new resource name, e.g. my_char_stage. In the resource properties, ensure that both Module and Owner Module are set to your module, and the folder of your choice.
 
 
 
[[File:area_palette.jpg]]
 
 
 
 
Create a new waypoint for your follower to appear on the party picker.  This waypoint '''must''' have a tag in the form of "char_" followed by the exact tag of your follower.
 
 
 
[[File:char_stage.jpg]]
 
 
In this example the tag of my follower is '''bc_party_miera''' so her waypoint tag must be '''char_bc_party_miera'''
 
 
For a standalone module (such as in this example), put your waypoint wherever you please.  The illustrated one is directly on top of Morrigan's.  For an add-in to the main campaign, you should position your wp appropriately relative to the core party members.
 
 
Save and export the area.
 
 
Add the following to your module event script:
 
<dascript>
 
case EVENT_TYPE_MODULE_GETCHARSTAGE:
 
{
 
  // Overlay the existing stage with my stage
 
  // "my_char_stage" is the resource name of the overlay area
 
  // "partypicker" is the name of the default GDA
 
  SetPartyPickerStage("my_char_stage", "partypicker");
 
  break;
 
}
 
</dascript>
 
 
=== Create m2DAs for the Party Picker ===
 
 
You will need to create two Excel spreadsheets.
 
 
The first should be named (both worksheet and file) '''partypicker_''' with a unique suffix.  In this example, my first spreadsheet is named partypicker_fofbc.xls with a worksheet name of partypicker_fofbc - the suffix being the acronym of my module.
 
 
Set up your columns as follows:
 
 
 
[[File:partypickerm2da.jpg]]
 
 
The '''ID''' for your follower must be '''12 or higher'''.  11 is the highest value used in the base 2DA.  12 is fine for standalone modules, add-ins will probably want to use an arbitrarily high number to avoid potential conflicts.
 
 
The '''Label''' should be the follower's name as you wish it to appear on the party picker.
 
 
The '''Tag''' must be your follower's tag.
 
 
All other values are non-functioning defaults and should be specified as in the image above unless you know explicitly what you're doing.
 
 
 
'''NOTE - ON SELECTION ANIMATIONS'''
 
 
Add Animation and Remove Animation are actually Id numbers of different animations, taken from anim_base.gda
 
 
Enter and Exit version of animations are generally the best ones to use, altough one can try others. NOT ALL ANIMATIONS WILL WORK, since some require special conditions.
 
Here are some examples you can use:
 
 
819 - talk cursing
 
 
629 - reading a book (doesn't work, since it probably requires a book object)
 
 
844 - hands behind back (848 and 849 are Enter and Exit versions respectfully)
 
 
850 - chest pounding salute
 
 
811 - fist pounding
 
 
277 - dance
 
 
247 - cast area spell
 
 
500 - vfx cast
 
 
600 - surprised
 
 
603 - praying
 
 
607 - head bow
 
 
609 - standing at attention
 
 
651,652 - crouch pray (Enter and exit)
 
 
808 - point forward
 
 
825 - nodding
 
 
840 - hand chop or frustration
 
 
905,906 - crouch (Enter, Exit)
 
 
919,920 - sit on ground (enter, exit)
 
 
965 - kneel down loop
 
 
972 - wipe nose
 
 
976,977 - squat (Enter, Exit)
 
 
986 - wipe eyes
 
 
998,999 - hands clasped (Enter, exit)
 
 
3029 - inspect nails
 
 
3031,3032 - playful (enter, exit)
 
 
3054,3056 - slouch (enter, exit)
 
255 - in-place fly
 
 
 
'''NOTE - ON SELECTION VFX'''
 
 
The VFX column is the ID of the vFX effect taken from vFx_base.gda. This one is a bit more tricky, since it also references the BlendTree value from the same file.
 
Find the ID of the spell effect and look for the BlendTreeName column (should the the 12th columun) and enter BOTH into the respective columns for your character in the partypicker file.
 
 
Some examples:
 
 
ID      ---      BLENDTREENAME      ---  EFFECT
 
 
6039    ---    fxm_energy_up_p    ---    Lady of the Forest pillar of light
 
 
6040    ---    fxm_power_in_p      ---  Branka - power in
 
 
3054    ---    fxc_lotf_c          ---  Lady of the Forest - swirling leaves
 
 
3009    ---    fxc_succubus_c    ---    Succubus crust
 
 
1549    ---    fxa_hly_imp_c      ---  Holy Impact crust
 
 
1076    ---    fxa_spi_aur_mht_c  ---  Spirit - Aura Might crust
 
 
 
 
 
The second spreadsheet should be named '''party_picker_''' (note middle underscore).  Once again append your unique suffix (in this example party_picker_fofbc.xls with party_picker_fofbc as its worksheet).
 
 
Set up your columns and data like so:
 
 
 
[[File:party_pickerm2da.jpg]]
 
 
'''ID''' and '''Tag''' should match what you did in the first spreadsheet.  Specify INVALID COLUMN for the third column.
 
 
 
If you're familiar with m2DAs, generate them from these files, copy them to your module's override directory, and skip to the next step.  Otherwise, read on:
 
 
- Go to '''\Program Files\Dragon Age\tools\ResourceBuild\Processors''' (or wherever you installed Dragon Age)
 
 
- Copy '''ExcelProcessor.exe''' from that folder to whichever folder has the excel sheets you just created.
 
 
- Drag and drop your xls files onto ExcelProcessor.  This will create .gda files.
 
 
- Copy these .gda files to your module's export directory (probably \Documents\Bioware\Dragon Age\AddIns\yourModule\module\override\toolsetexport).  Make sure they are included in your .dazip when the time comes to build your module.
 
 
If you are an OpenOffice user and have trouble with ExcelProcessor, you can use [http://social.bioware.com/project/755/ GDApp] to directly create the 2DAs.  It rocks!
 
 
=== Capture the EVENT_TYPE_PARTYMEMBER_ADDED Event ===
 
 
Amusingly enough, the Party Picker does not actually add followers to the party.  However it raises an event that allows you to do so.  Your module script needs to capture this event and execute some code.
 
 
The following example shows what to do with the event.  The full script would work as a module script for an add-in (assuming it didn't need to do anything else), but otherwise you'll have to incorporate the event into your own module script. 
 
 
If you're not sure how to set a module script, go to '''File''' then '''Manage Modules,''' select your module and click '''Properties.'''
 
 
<dascript>
 
#include "utility_h"
 
 
void main()
 
{
 
    event ev = GetCurrentEvent();
 
    int nEventType = GetEventType(ev);
 
    switch(nEventType)
 
    {
 
        case EVENT_TYPE_PARTYMEMBER_ADDED:
 
        {
 
            object oFollower = GetEventObject(ev, 0);
 
            SetLocalInt(oFollower, CREATURE_REWARD_FLAGS, 0);  //Allows the follower to gain XP
 
            AddCommand(oFollower, CommandJumpToLocation(GetLocation(GetHero())));  //Ensures follower appears at PC's location.
 
            WR_SetFollowerState(oFollower, FOLLOWER_STATE_ACTIVE);  //Adds follower to the active party
 
            break;
 
        }
 
 
    }
 
}
 
</dascript>
 
 
'''WR_SetFollowerState(oFollower, FOLLOWER_STATE_ACTIVE)''' is the key statement to add the follower to the active party.  You must have this.
 
 
'''SetLocalInt(oFollower, CREATURE_REWARD_FLAGS, 0)''' is a bug-fix, as followers hired with UT_HireFollower() do not receive XP by default.  This statement fixes that, and I consider it best practice to keep it in this event to ensure it is always set.  You will not wish to do this if you want a follower that should not gain XP to be on the party picker.
 
 
 
Note that you do not need to intercept the corresponding event for a party member being removed - the party picker handles spawning/despawning, and thus will successfully remove members.
 
 
 
Remember to save and export your module script.
 
 
 
If you're not sure how to set a module script, go to '''File''' then '''Manage Modules,''' select your module and click '''Properties.'''  The module script is in the General category, as seen below:
 
 
[[File:module_script.jpg]]
 
 
You can set this to any script you've created.  See [[Scripting tutorial]] and [[Character generation]] for more background and some simple examples of event-handling scripts.  In general, you will want to make sure standalone module scripts pass events through to module_core (as in the [[Character generation]] examples) and add-in scripts do not (as in the example above).
 
 
=== Create Your Hiring Script ===
 
 
Now all that remains is to actually hire the follower :)
 
 
Create a script to handle the hiring (which will most likely be fired from a conversation).  The script is quite simple:
 
 
<dascript>
 
#include "utility_h"
 
 
void main() {
 
 
        object oFollower = GetObjectByTag("bc_party_miera"); //Use CreateObject() if the creature isn't present in the module yet
 
 
        UT_HireFollower(oFollower);  //Hires the follower
 
 
        SetPartyPickerGUIStatus(2);
 
 
        ShowPartyPickerGUI();  //Shows the Party Picker; necessary for the follower to gain XP
 
 
}
 
</dascript>
 
 
Make sure you use your own follower's tag and not the example one :)
 
 
This script fires the party picker after hiring the follower.  That is absolutely necessary via this method, as we have put the XP fix onto an event fired by the party picker.  You cannot put the XP fix into this script, it must be called from a later one (the bug is caused by an errant call to an event in player_core, which will be executed AFTER this script).
 
 
 
The easiest way to use this script is directly from conversation, putting it as an action on a line of dialogue where the PC invites the follower to join them.  If you're not sure how to associate a script with a dialogue line, see the following image:
 
 
[[File:hire_conv.jpg]]
 
 
Create your dialogue as normal, select the line you want to fire the script, and click the '''Plots and Scripting''' tab.  Use the '''Script''' file chooser in the Action section to browse to the script you created.
 
 
 
If everything worked, you should see something like this:
 
 
 
[[File:picker_success.jpg|thumb|200px|center]]
 
 
== Advanced Follower Creation ==
 
 
 
 
Follow these steps to have full control over the creation of your follower, with options such as:
 
Follow these steps to have full control over the creation of your follower, with options such as:
  
- Unique level-up template
+
* Unique level-up template
 
+
* Class and specialisation chosen via script
- Class and specialisation chosen via script
+
* Any starting state
 
+
* Level higher than the PC
- Any starting state
+
* Starts with a specialisation point rather than a specific specialisation
 
+
* Set plot flags in the call to the hire script
- Level higher than the PC
+
 
+
- Starts with a specialisation point rather than a specific specialisation
+
 
+
- Set plot flags in the call to the hire script
+
 
+
 
+
=== Prepare Creature, char_stage and Party Picker m2DAs ===
+
  
 +
== Prepare Creature, char_stage and Party Picker m2DAs ==
  
Follow the same steps to create your follower creature, char_stage and Party Picker m2DAs as above.
+
Follow the same steps to create your follower creature, char_stage and Party Picker m2DAs as you did in the [[basic follower tutorial]].
  
=== Create Party Plot ===
+
== Create Party Plot ==
  
While not necessary, it's very helpful to have plot flags set when a follower is hired or joins/leaves the active party.  This makes conversation interjections and the like very easy.
+
While   not necessary, it's very helpful to have plot flags set when a follower is hired or joins/leaves the active party.  This makes conversation interjections and the like very easy.
  
 
Create a plot with appropriate flags.  There's no real need to associate journal text with them:
 
Create a plot with appropriate flags.  There's no real need to associate journal text with them:
  
[[File:follower_plot.jpg]]
+
[[File:follower_plot.jpg|300px]]
  
 
See FAQ for an alternative approach to plots when there are more than three potential party members.
 
See FAQ for an alternative approach to plots when there are more than three potential party members.
  
=== Add Plot Flags to Party Picker Event Intercept ===
+
== Add Plot Flags to Party Picker Event Intercept ==
  
 
We then update our module script to make use of that plot, like so:
 
We then update our module script to make use of that plot, like so:
Line 364: Line 65:
 
}
 
}
 
</dascript>
 
</dascript>
 +
== Create a Level Up Template ==
  
=== Create a Level Up Template ===
+
You   can skip this step if you're content to use one of the generic Rogue,   Wizard or Warrior templates, but I don't recommend it.  Making a   template that suits your character is easy and will almost always be   better for the player than a generic one that spends points poorly.
 
+
 
+
You can skip this step if you're content to use one of the generic Rogue, Wizard or Warrior templates, but I don't recommend it.  Making a template that suits your character is easy and will almost always be better for the player than a generic one that spends points poorly.
+
  
 
Go to \Program Files\Dragon Age\tools\Source\2DA (or wherever you installed Dragon Age).
 
Go to \Program Files\Dragon Age\tools\Source\2DA (or wherever you installed Dragon Age).
  
You should see a number of excel sheets of the form '''ALCharacter.xls''' (such as ALAlistair.xls, ALLeliana.xls, ALRogue_Default.xls etc). Open the one closest to your character (ie Morrigan or Wynne for a wizard, Leliana or Zevran for a rogue).  Save a copy as '''ALYourcharacter.xls''' in whatever directory you're using to create your 2DAs, remembering to rename the worksheet '''ALYourcharacter''' (in this example, ALMiera.xls with ALMiera as its worksheet).
+
You should see a number of excel sheets of the form '''ALCharacter.xls'''   (such as ALAlistair.xls, ALLeliana.xls, ALRogue_Default.xls etc).   Open the one closest to your character (ie Morrigan or Wynne for a wizard, Leliana or Zevran for a rogue).  Save a copy as '''ALYourcharacter.xls''' in whatever directory you're using to create your 2DAs, remembering to rename the worksheet '''ALYourcharacter''' (in this example, ALMiera.xls with ALMiera as its worksheet).
  
 
It should look something like this:
 
It should look something like this:
  
[[File:ALtable.jpg|thumb|500px|center]]
+
[[File:ALtable.jpg|300px]]
 
+
  
 
'''Columns B''' and '''C''' are the talents/spells available to this character.  Do not change them.
 
'''Columns B''' and '''C''' are the talents/spells available to this character.  Do not change them.
Line 385: Line 83:
 
We will edit the remaining columns like so:
 
We will edit the remaining columns like so:
  
 +
=== Setting Stat Weights ===
  
==== Setting Stat Weights ====
+
The stat weights in '''column J'''  determine how the follower will spend their attribute points, in a  rough ratio.  So if Dexterity is set to 1.5 and Intelligence to 1, you  should expect to see 3 points of Dex for every 2 points of Cunning in-game (note Intelligence is the label used in the toolset for Cunning).
  
 
+
Simply   change the values in J to reflect how you'd like the character to spend their points.  In this example we're creating a wizard, so we're not going to mess around:
The stat weights in '''column J''' determine how the follower will spend their attribute points, in a rough ratio.  So if Dexterity is set to 1.5 and Intelligence to 1, you should expect to see 3 points of Dex for every 2 points of Cunning in-game (note Intelligence is the label used in the toolset for Cunning).
+
 
+
Simply change the values in J to reflect how you'd like the character to spend their points.  In this example we're creating a wizard, so we're not going to mess around:
+
  
 
[[File:miera_stat_weights.jpg]]
 
[[File:miera_stat_weights.jpg]]
  
This character will only raise magic.  I set the value to 5 rather than something like 1 to provide room underneath for the other stats while still overwhelmingly favouring magic, but in practice I only really ever want that one stat.  
+
This   character will only raise magic.  I set the value to 5 rather than   something like 1 to provide room underneath for the other stats while   still overwhelmingly favouring magic, but in practice I only really ever want that one stat.  
  
In a note left on this column in the existing templates, Bioware's Georg Zoeller writes, "This is the weight of each attribute (row id links into properties.xls.id). 1.0 means 'try to keep this attribute level' (spend 1 point per level). 0.5 means 'try to spend 1 point every two levels' and so on."
+
In a note left on this column in the   existing templates, Bioware's Georg Zoeller writes, "This is the weight   of each attribute (row id links into properties.xls.id). 1.0 means 'try to keep this attribute level' (spend 1 point per level). 0.5 means 'try to spend 1 point every two levels' and so on."
  
 
The default Mage, Rogue and Warrior templates include '''column L''' labeled "AttInit." Editing these values seems to have no effect on followers set to use these templates.
 
The default Mage, Rogue and Warrior templates include '''column L''' labeled "AttInit." Editing these values seems to have no effect on followers set to use these templates.
  
 
+
=== Setting Talent and Skill Priorities ===
==== Setting Talent and Skill Priorities ====
+
 
+
  
 
'''Columns D''' and '''E''' are the talents/spells that the character will buy, in preference order from top to bottom.
 
'''Columns D''' and '''E''' are the talents/spells that the character will buy, in preference order from top to bottom.
  
 
'''Columns H''' and '''I''' are the skills that the character will buy, in preference order from top to bottom.
 
'''Columns H''' and '''I''' are the skills that the character will buy, in preference order from top to bottom.
 
  
 
To change these, just copy the appropriate two cells from columns B&C or F&G over the ones you want to replace.
 
To change these, just copy the appropriate two cells from columns B&C or F&G over the ones you want to replace.
Line 418: Line 111:
 
We decide we'd prefer Flame Blast, so we find it in columns B&C and copy both cells:
 
We decide we'd prefer Flame Blast, so we find it in columns B&C and copy both cells:
  
[[File:ALMori_copy.jpg]]
+
[[File:ALMori_copy.jpg|300px]]
  
 
Then we select the cells we want to replace in columns D&E and paste over them:
 
Then we select the cells we want to replace in columns D&E and paste over them:
  
[[File:ALMori_paste.jpg]]
+
[[File:ALMori_paste.jpg|300px]]
  
 +
Continue  this process until your priorities list for both skills and  talents/spells is exactly as you want it.  Make sure you have at least  as many priorities as the core follower you're copying - points that cannot be spent according to these priorities have a habit of vanishing.
  
 +
If  you used any abilities from a specialisation, make sure you remember  to  set that specialisation with the function we'll introduce later.  The  autolevel scripts will add specialisation abilities to a character  regardless of whether they have that spec or not.
  
Continue this process until your priorities list for both skills and talents/spells is exactly as you want it.  Make sure you have at least as many priorities as the core follower you're copying - points that cannot be spent according to these priorities have a habit of vanishing.
+
=== Create a M2DA_base_ m2DA ===
 
+
If you used any abilities from a specialisation, make sure you remember to set that specialisation with the function we'll introduce later.  The autolevel scripts will add specialisation abilities to a character regardless of whether they have that spec or not.
+
 
+
==== Create a M2DA_base_ m2DA ====
+
  
 
Dragon Age will need to know where to find your autolevel template.  We tell it by extending M2DA_base.gda
 
Dragon Age will need to know where to find your autolevel template.  We tell it by extending M2DA_base.gda
Line 438: Line 129:
 
Set up its columns and data like so (note I used GDApp because Open Office wasn't cooperating for this one!):
 
Set up its columns and data like so (note I used GDApp because Open Office wasn't cooperating for this one!):
  
[[File:m2da_base_fofbc.jpg]]
+
[[File:m2da_base_fofbc.jpg|300px]]
  
The '''ID''' should be very high to avoid conflicts.  I've arbitrarily chosen 50,000+ here.  Carefully note the ID you've chosen for your character, you will need it later.
+
The '''ID'''   should be very high to avoid conflicts.  I've arbitrarily chosen   50,000+ here.  Carefully note the ID you've chosen for your character,   you will need it later.
  
 
Set the '''Label''' and '''Worksheet''' to be the name of your autolevel template worksheet (ALCharactername if you've been following this).
 
Set the '''Label''' and '''Worksheet''' to be the name of your autolevel template worksheet (ALCharactername if you've been following this).
  
 
Set the '''PackageIDForAI''' to be 0, it shouldn't be needed for followers.
 
Set the '''PackageIDForAI''' to be 0, it shouldn't be needed for followers.
 
  
 
When you're done, use ExcelProcessor to make GDAs of both spreadsheets and copy them to your module's export folder.
 
When you're done, use ExcelProcessor to make GDAs of both spreadsheets and copy them to your module's export folder.
  
 +
FOR AUTOLEVEL TO WORK AFTER RECRUITING:
  
 +
Look in packages.xls.
  
FOR AUTOLEVEL TO WORK AFTER RECRUITING:
+
There  is a column called LevelupTable. That links to a corresponding AL*  table. For instance,  row 81 is for Leliana. Her LevelupTable value is  258. If you look that up in 2DA_base, you'll see it links to ALLeliana.
  
Look in packages.xls.
 
There is a column called LevelupTable. That links to a corresponding AL* table. For instance,  row 81 is for Leliana. Her LevelupTable value is 258. If you look that up in 2DA_base, you'll see it links to ALLeliana.
 
 
(alternatively, you could try packages_base.gda)
 
(alternatively, you could try packages_base.gda)
  
=== Create a New Hire Function Include ===
+
== Create a New Hire Function Include ==
  
 
This section is probably only relevent if you have a specific need to override the functionality of player_core.
 
This section is probably only relevent if you have a specific need to override the functionality of player_core.
  
Many vital steps of follower addition happen inside an event in player_core.  Followers tend to be extremely buggy (no skill tree, for example) if this event does not fire.
+
Many   vital steps of follower addition happen inside an event in   player_core.  Followers tend to be extremely buggy (no skill tree, for   example) if this event does not fire.
  
However, that event is not very flexible.  In order to control it to our requirements, we need to replicate its functionality inside our own script.  This is probably much safer than messing with player_core directly!
+
However, that event is not   very flexible.  In order to control it to our requirements, we need to   replicate its functionality inside our own script.  This is probably   much safer than messing with player_core directly!
  
 
Create a new script file, naming it something like '''hireCustomFollower_h'''.  We will be including this wherever we want to hire a follower.
 
Create a new script file, naming it something like '''hireCustomFollower_h'''.  We will be including this wherever we want to hire a follower.
Line 482: Line 172:
 
-  Followers can gain XP
 
-  Followers can gain XP
 
-  Autolevel status can be set (default off)
 
-  Autolevel status can be set (default off)
- Followers can be set to any starting state (default Available) and will still be properly initalised and added to the party pool
+
-   Followers can be set to any starting state (default Available) and will still be properly initalised and added to the party pool
 
-  Autolevel tables for non-core followers can be explicitly set.
 
-  Autolevel tables for non-core followers can be explicitly set.
 
-  Class and Specialisation can be chosen via script
 
-  Class and Specialisation can be chosen via script
Line 504: Line 194:
 
hireCustomFollower(oFollower, CLASS_WARRIOR, PLT_YOUR_PARTY_PLOT, YOUR_FOLLOWER_JOINED_FLAG, ABILITY_TALENT_HIDDEN_CHAMPION);
 
hireCustomFollower(oFollower, CLASS_WARRIOR, PLT_YOUR_PARTY_PLOT, YOUR_FOLLOWER_JOINED_FLAG, ABILITY_TALENT_HIDDEN_CHAMPION);
  
Where the plot and flag are those for your module (remember to create the plot and include it on the calling script), and ABILITY_TALENT_HIDDEN etc is the desired spec.
+
Where   the plot and flag are those for your module (remember to create the   plot and include it on the calling script), and ABILITY_TALENT_HIDDEN   etc is the desired spec.
  
 
You should also have a custom ALTable set up.   
 
You should also have a custom ALTable set up.   
See wiki for details, and remember to edit it in to GetCustomFollowerALTable below or pass it directly as an argument to hireCustomFollower.
+
 
+
See   wiki for details, and remember to edit it in to   GetCustomFollowerALTable below or pass it directly as an argument to   hireCustomFollower.
  
 
Full argument list:
 
Full argument list:
Line 517: Line 207:
 
         int nForceClass,    //Pass a Class constant here, usually CLASS_ROGUE, CLASS_WARRIOR, CLASS_WIZARD.  Mandatory due to a bug.
 
         int nForceClass,    //Pass a Class constant here, usually CLASS_ROGUE, CLASS_WARRIOR, CLASS_WIZARD.  Mandatory due to a bug.
 
          
 
          
         string sPlot = "",  //It's recommended you have a plot flag to be set when the follower joins.  Pass the plot constant here.  Remember to #include in calling script
+
         string   sPlot = "",  //It's recommended you have a plot flag to be set when   the follower joins.  Pass the plot constant here.  Remember to #include   in calling script
 
          
 
          
 
         int nPlotFlag = "",  //And then pass the flag constant.  Will be set to TRUE if available.
 
         int nPlotFlag = "",  //And then pass the flag constant.  Will be set to TRUE if available.
 
          
 
          
         int nForceSpec = 0,  //This is the ID of the Specialisation you want. Note they are NOT classes, but abilities.  The full list is:
+
         int   nForceSpec = 0,  //This is the ID of the Specialisation you want.   Note they are NOT classes, but abilities.  The full list is:
                             //ABILITY_SPELL_HIDDEN_ARCANE_WARRIOR, ABILITY_SPELL_HIDDEN_BLOODMAGE, ABILITY_SPELL_HIDDEN_SHAPESHIFTER, ABILITY_SPELL_HIDDEN_SPIRIT_HEALER
+
                             //ABILITY_SPELL_HIDDEN_ARCANE_WARRIOR,   ABILITY_SPELL_HIDDEN_BLOODMAGE, ABILITY_SPELL_HIDDEN_SHAPESHIFTER,   ABILITY_SPELL_HIDDEN_SPIRIT_HEALER
 
                             //ABILITY_SPELL_HIDDEN_BARD, ABILITY_TALENT_HIDDEN_ASSASSIN, ABILITY_TALENT_HIDDEN_DUELIST, ABILITY_TALENT_HIDDEN_RANGER
 
                             //ABILITY_SPELL_HIDDEN_BARD, ABILITY_TALENT_HIDDEN_ASSASSIN, ABILITY_TALENT_HIDDEN_DUELIST, ABILITY_TALENT_HIDDEN_RANGER
 
                             //ABILITY_TALENT_HIDDEN_BERSERKER, ABILITY_TALENT_HIDDEN_CHAMPION, ABILITY_TALENT_HIDDEN_REAVER, ABILITY_TALENT_HIDDEN_TEMPLAR
 
                             //ABILITY_TALENT_HIDDEN_BERSERKER, ABILITY_TALENT_HIDDEN_CHAMPION, ABILITY_TALENT_HIDDEN_REAVER, ABILITY_TALENT_HIDDEN_TEMPLAR
 
                             //I recommended forcing a spec, particularly if your ALTable includes abilities from one.
 
                             //I recommended forcing a spec, particularly if your ALTable includes abilities from one.
 
          
 
          
         int nALTable = 0,    //This is the ID of an ALTable from 2DA_base.GDA or your module's m2DA_base_*.GDA  I recommended the latter, but you can edit that into GetCustomFollowerALTable below rather than passing it.
+
         int   nALTable = 0,    //This is the ID of an ALTable from 2DA_base.GDA or   your module's m2DA_base_*.GDA  I recommended the latter, but you can   edit that into GetCustomFollowerALTable below rather than passing it.
 
          
 
          
         int bInvokePicker = FALSE,  //Sets whether the party picker should be opened on hiring.  I think it's cleaner to call the picker outside this script, particularly if you have multiple hires at once.
+
         int   bInvokePicker = FALSE,  //Sets whether the party picker should be   opened on hiring.  I think it's cleaner to call the picker outside this   script, particularly if you have multiple hires at once.
 
          
 
          
 
         int nInitialState = FOLLOWER_STATE_AVAILABLE,  //This sets whether the follower joins the active party or not.  Options are:
 
         int nInitialState = FOLLOWER_STATE_AVAILABLE,  //This sets whether the follower joins the active party or not.  Options are:
Line 537: Line 227:
 
                                                       //Plus some others you're unlikely to need at this time.  Defaults to AVAILABLE because having 4+ active followers is screwy.
 
                                                       //Plus some others you're unlikely to need at this time.  Defaults to AVAILABLE because having 4+ active followers is screwy.
 
                                                        
 
                                                        
         string sCurrPlot = "",  //If you set FOLLOWER_STATE_ACTIVE or FOLLOWER_STATE_LOCKEDACTIVE, the script will check to see if you passed this.
+
         string   sCurrPlot = "",  //If you set FOLLOWER_STATE_ACTIVE or   FOLLOWER_STATE_LOCKEDACTIVE, the script will check to see if you passed   this.
                                 //It is recommended that you have a plot flag set for a given follower being in the active party, this makes conversation interjection etc. much easier.
+
                                 //It   is recommended that you have a plot flag set for a given follower being in the active party, this makes conversation interjection etc. much easier.
  
 
         int nCurrPlotFlag = 0,  //This flag will be set if FOLLOWER_STATE_ACTIVE or FOLLOWER_STATE_LOCKEDACTIVE are true
 
         int nCurrPlotFlag = 0,  //This flag will be set if FOLLOWER_STATE_ACTIVE or FOLLOWER_STATE_LOCKEDACTIVE are true
Line 544: Line 234:
 
                                 //ie if you added someone to the active party and have a plot flag to cope with it.
 
                                 //ie if you added someone to the active party and have a plot flag to cope with it.
  
         int nAutolevel = 0,    //Sets the Autolevel flag on the character sheet. 0 is off, 1 is on, 2 forces it on and removes it so the player can't turn it off.
+
         int   nAutolevel = 0,    //Sets the Autolevel flag on the character sheet.   0 is off, 1 is on, 2 forces it on and removes it so the player can't turn it off.
 
          
 
          
 
          
 
          
Line 550: Line 240:
 
                                 //It's important to set this false for classes that do not have specs, such as CLASS_DOG.
 
                                 //It's important to set this false for classes that do not have specs, such as CLASS_DOG.
 
                                  
 
                                  
         int nTargetLevel = 0,  //If you want a specific level, set this. Generally not worthwhile unless you set it higher than the player, since they'll just get XP from the party picker anyway.
+
         int   nTargetLevel = 0,  //If you want a specific level, set this.   Generally not worthwhile unless you set it higher than the player, since they'll just get XP from the party picker anyway.
 
          
 
          
         int nMinLevel = 0      //Set this if there's a specific level you don't want the follower to go below.  Probably only useful if the PC might be very low level but not necessarily so.  
+
         int   nMinLevel = 0      //Set this if there's a specific level you don't   want the follower to go below.  Probably only useful if the PC might be   very low level but not necessarily so.  
 
          
 
          
 
         )
 
         )
Line 559: Line 249:
 
This function is where you put your custom table assignments.
 
This function is where you put your custom table assignments.
  
You should explicitly test for the tag of your follower (not mine!) and assign a value to nTable from your m2DA extension to M2DA_base  
+
You   should explicitly test for the tag of your follower (not mine!) and   assign a value to nTable from your m2DA extension to M2DA_base  
  
 
See wiki for details on how to do this, or ignore it to get the default Warrior/Rogue/Wizard AL tables.
 
See wiki for details on how to do this, or ignore it to get the default Warrior/Rogue/Wizard AL tables.
Line 566: Line 256:
 
*/
 
*/
  
int GetCustomFollowerALTable(object oFollower) {
+
int GetCustomFollowerALTable(object oFollower)
 +
{
 
     int nTable = _GetTableToUseForAL(oFollower);
 
     int nTable = _GetTableToUseForAL(oFollower);
 +
    string sTag = GetTag(oFollower);
 
      
 
      
     if (GetTag(oFollower) == "bc_party_miera") {               
+
     if(sTag  == "bc_party_miera")  
 +
    {               
 
         nTable = 50143;   
 
         nTable = 50143;   
 
     }   
 
     }   
      
+
     else if(sTag == "bc_party_jysavin")  
    if (GetTag(oFollower) == "bc_party_jysavin") {
+
    {
 
         nTable = 50144;   
 
         nTable = 50144;   
 
     }
 
     }
      
+
     else if(sTag == "bc_party_geldual")  
    if (GetTag(oFollower) == "bc_party_geldual") {
+
    {
 
         nTable = 50145;   
 
         nTable = 50145;   
 
     }
 
     }
      
+
     else  if(sTag == "bc_party_braghon")  
    if (GetTag(oFollower) == "bc_party_braghon") {
+
    {
 
         nTable = 50146;   
 
         nTable = 50146;   
 
     }
 
     }
   
+
 
 
     return nTable;     
 
     return nTable;     
 
}   
 
}   
  
 
// This just cleans up the main function a little
 
// This just cleans up the main function a little
 +
int GetCustomFollowerTargetLevel(object oFollower, object oHero, int nPackage, int nMinLevel = 0)
 +
{
 +
    int nPlayerLevel = GetLevel(oHero);
 +
    int nTargetLevel = 0;
  
int GetCustomFollowerTargetLevel(object oFollower, object oHero, int nPackage, int nMinLevel = 0) {
+
    if(nPlayerLevel >= 13 || nPlayerLevel == 1 || !_UT_GetIsPlotFollower(oFollower))
            int nPlayerLevel = GetLevel(oHero);
+
    {
            int nTargetLevel = 0;
+
        nTargetLevel = nPlayerLevel;
 +
    }
 +
    else
 +
    {
 +
        nTargetLevel = nPlayerLevel + 1;
 +
    }
  
            if((nPlayerLevel >= 13) || (nPlayerLevel == 1) || (!_UT_GetIsPlotFollower(oFollower))) {
+
    // If nMinLevel is not specified, checks package 2DA for a value
              nTargetLevel = nPlayerLevel;
+
    if(nMinLevel == 0)  
            } else {
+
    {
              nTargetLevel = nPlayerLevel + 1;
+
        nMinLevel = GetM2DAInt(TABLE_PACKAGES, "MinLevel", nPackage);
            }
+
    }
  
            if (nMinLevel == 0) {  //If nMinLevel is not specified, checks package 2DA for a value
+
    if(nMinLevel > 0 && nMinLevel > nTargetLevel)  
              nMinLevel = GetM2DAInt(TABLE_PACKAGES, "MinLevel", nPackage);
+
    {
            }
+
        nTargetLevel = nMinLevel;
            if(nMinLevel > 0 && nMinLevel > nTargetLevel) {
+
    }
              nTargetLevel = nMinLevel;
+
 
            }        
+
    return nTargetLevel;
           
+
            return nTargetLevel;
+
   
+
 
}   
 
}   
  
 
// Moving this black box out :)  I don't really understand it, but it should function if you have tactics set up in a package.
 
// Moving this black box out :)  I don't really understand it, but it should function if you have tactics set up in a package.
  
void InitCustomFollowerTactics(object oFollower, int nPackage) {
+
void InitCustomFollowerTactics(object oFollower, int nPackage)  
 +
{
 
         int nTableID = GetM2DAInt(TABLE_PACKAGES, "FollowerTacticsTable", nPackage);
 
         int nTableID = GetM2DAInt(TABLE_PACKAGES, "FollowerTacticsTable", nPackage);
 
         if (nTableID != -1)
 
         if (nTableID != -1)
Line 716: Line 416:
  
 
         /* #################  BEGIN PLAYER_CORE EVENT_TYPE_PARTY_MEMBER_HIRED EMULATION #################
 
         /* #################  BEGIN PLAYER_CORE EVENT_TYPE_PARTY_MEMBER_HIRED EMULATION #################
         This replicates the EVENT_TYPE_PARTY_MEMBER_HIRED handler from player_core, stripped down for simplicity and allowing our custom options.
+
         This   replicates the EVENT_TYPE_PARTY_MEMBER_HIRED handler from player_core,   stripped down for simplicity and allowing our custom options.
  
  
Line 810: Line 510:
 
Yeah, I know.  It can't really be any smaller.  Feel free to modify it if you're confident with scripting.
 
Yeah, I know.  It can't really be any smaller.  Feel free to modify it if you're confident with scripting.
 
==== Add Your Custom Autolevel Template to GetCustomFollowerALTable() ====
 
==== Add Your Custom Autolevel Template to GetCustomFollowerALTable() ====
While you can pass the ID you made for your autolevel template to that monster function as an argument, it's better to have them all in one place if you have multiple followers.
+
While   you can pass the ID you made for your autolevel template to that   monster function as an argument, it's better to have them all in one   place if you have multiple followers.
  
GetCustomFollowerALTable() is the first function in our include, and you can add an explicit if test for your follower there to assign the correct table id (the one from your M2DA_base_ m2DA).  There is a function very much like it in sys_autolevel_h.nss for the core followers, so we'll copy Bioware's practice.
+
GetCustomFollowerALTable()   is the first function in our include, and you can add an explicit if   test for your follower there to assign the correct table id (the one   from your M2DA_base_ m2DA).  There is a function very much like it in   sys_autolevel_h.nss for the core followers, so we'll copy Bioware's   practice.
  
 
Let's take a look at the function by itself:
 
Let's take a look at the function by itself:
Line 841: Line 541:
 
</dascript>
 
</dascript>
  
So we have a test for each follower tag from my module, matching up to an ID which is assigned to nTable.  All you need to do is change a tag from my follower to yours, and my ID to the correct one from your M2DA_base_* m2DA.  Then you should delete the rest of the example if statements :)
+
So   we have a test for each follower tag from my module, matching up to an   ID which is assigned to nTable.  All you need to do is change a tag from my follower to yours, and my ID to the correct one from your   M2DA_base_* m2DA.  Then you should delete the rest of the example if   statements :)
  
 
Save and export the script.  Ignore the compiler error about lack of main();
 
Save and export the script.  Ignore the compiler error about lack of main();
  
=== Include Function In Your Hire Script and Call It ===
+
== Include Function In Your Hire Script and Call It ==
  
 
So instead of a hire script that calls UT_HireFollower(), we want one that includes our shiny new function and calls it.
 
So instead of a hire script that calls UT_HireFollower(), we want one that includes our shiny new function and calls it.
Line 876: Line 576:
 
                     //More complex example - Follower added as a unique class (Dog), not granted a specialisation or spec point.   
 
                     //More complex example - Follower added as a unique class (Dog), not granted a specialisation or spec point.   
 
                     //Note unique classes must have an ALTable passed here or specified in GetCustomFollowerALTable() or they won't work
 
                     //Note unique classes must have an ALTable passed here or specified in GetCustomFollowerALTable() or they won't work
                     hireCustomFollower(oSpider, CLASS_DOG, PLT_BC_CREATE_PARTY, PARTY_GELDUAL_JOINED, 0, 0, FALSE, FOLLOWER_STATE_AVAILABLE, "", 0, 0, FALSE);
+
                     hireCustomFollower(oSpider,   CLASS_DOG, PLT_BC_CREATE_PARTY, PARTY_GELDUAL_JOINED, 0, 0, FALSE,   FOLLOWER_STATE_AVAILABLE, "", 0, 0, FALSE);
 
                    
 
                    
 
                     //Show the party picker to let the player choose from their new companions!
 
                     //Show the party picker to let the player choose from their new companions!
 
                     SetPartyPickerGUIStatus(2);
 
                     SetPartyPickerGUIStatus(2);
                     ShowPartyPickerGUI();                                                                                                                  
+
                     ShowPartyPickerGUI();                                                                                                                      
 
}
 
}
 
</dascript>
 
</dascript>
  
The example shows several of the more simple ways of invoking the function.  Check the comments at the start of the function for a full list of arguments.
+
The   example shows several of the more simple ways of invoking the   function.  Check the comments at the start of the function for a full   list of arguments.
  
 
I would suggest best practice for most followers would be to call as follows:
 
I would suggest best practice for most followers would be to call as follows:
Line 890: Line 590:
 
'''hireCustomFollower(oFollower, CLASS, PLOT, PLOT_FLAG, SPECIALISATION)'''
 
'''hireCustomFollower(oFollower, CLASS, PLOT, PLOT_FLAG, SPECIALISATION)'''
  
This will safely set the follower up as the desired class and specialisation (doubly important if there are spec abilities in their ALTable) while setting your plot flag for them being in the party. hireCustomFollower(oFollower, CLASS, PLOT, PLOT_FLAG, SPECIALISATION, 0, TRUE) will do the same while invoking the Party Picker automatically.
+
This   will safely set the follower up as the desired class and specialisation (doubly important if there are spec abilities in their ALTable) while setting your plot flag for them being in the party.   hireCustomFollower(oFollower, CLASS, PLOT, PLOT_FLAG, SPECIALISATION, 0, TRUE) will do the same while invoking the Party Picker automatically.
  
 
Note that the specialisations are abilities and not classes - you'll find them as ABILITY_HIDDEN_ constants.
 
Note that the specialisations are abilities and not classes - you'll find them as ABILITY_HIDDEN_ constants.
Line 896: Line 596:
 
If it's all worked, you should find you can now add followers with a lot more flexibility!
 
If it's all worked, you should find you can now add followers with a lot more flexibility!
  
[[File:picker_advanced.jpg|thumb|500px|center]]
+
[[File:picker_advanced.jpg|300px]]
  
 
== Common Follower Problems & FAQ ==
 
== Common Follower Problems & FAQ ==
  
====Why don't my followers gain XP?====
+
=== Why don't my followers gain XP? ===
 
There is a bug in UT_HireFollower. Make a copy of UT_HireFollower in a function of your own, then make the following change:
 
There is a bug in UT_HireFollower. Make a copy of UT_HireFollower in a function of your own, then make the following change:
 
     WR_SetFollowerState(oFollower, FOLLOWER_STATE_ACTIVE, TRUE, 0, TRUE);
 
     WR_SetFollowerState(oFollower, FOLLOWER_STATE_ACTIVE, TRUE, 0, TRUE);
Line 906: Line 606:
 
     WR_SetFollowerState(oFollower, FOLLOWER_STATE_ACTIVE, TRUE, 0, bPreventLevelup);
 
     WR_SetFollowerState(oFollower, FOLLOWER_STATE_ACTIVE, TRUE, 0, bPreventLevelup);
  
====When I choose followers from the Party Picker, they spawn into the area but do not join.====
+
=== When I choose followers from the Party Picker, they spawn into the area but do not join ===
  
You need to intercept the EVENT_TYPE_PARTYMEMBER_ADDED event and set the follower to FOLLOWER_STATE_ACTIVE.  See Simple Follower Creation earlier in this document.
+
You   need to intercept the EVENT_TYPE_PARTYMEMBER_ADDED event and set the   follower to FOLLOWER_STATE_ACTIVE.  See Simple Follower Creation earlier in this document.
  
====My followers don't have skill trees!====
+
=== My followers don't have skill trees! ===
  
If a follower hasn't been through an initial chargen/autolevel event (via player_core/sys_autolevel_h) then the skill tree doesn't show.  You're probably trying to be clever and get around UT_HireFollower without going all the way (see monster function above ^_^).
+
If   a follower hasn't been through an initial chargen/autolevel event (via   player_core/sys_autolevel_h) then the skill tree doesn't show.  You're   probably trying to be clever and get around UT_HireFollower without   going all the way (see monster function above ^_^).
  
 +
=== My followers don't have a class ===
  
====My followers don't have a class====
+
GetCreatureCoreClass()  seems flaky under some conditions.  It's best to explicitly set the  class yourself; this is why class is currently a mandatory argument to  hireCustomFollower()
  
GetCreatureCoreClass() seems flaky under some conditions.  It's best to explicitly set the class yourself; this is why class is currently a mandatory argument to hireCustomFollower()
+
=== Dog talents and equipment slots don't work ===
  
====Dog talents and equipment slots don't work====
+
Symptoms:   dog talents appear on the talents screen but remain greyed out on level up. Warrior talents appear in the quickslots. Equipment slots are for humans, not dogs.
Symptoms: dog talents appear on the talents screen but remain greyed out on level up. Warrior talents appear in the quickslots. Equipment slots are for humans, not dogs.
+
  
 
Line 78 of packages_base in packages.xls (Dog) should have the LevelUpTable column set to 257 which is the ALDog table.
 
Line 78 of packages_base in packages.xls (Dog) should have the LevelUpTable column set to 257 which is the ALDog table.
Line 926: Line 626:
 
The EquipmentLayout 2DA needs a new entry for the follower:
 
The EquipmentLayout 2DA needs a new entry for the follower:
  
  ID=same as partypicker 2DA
+
  ID=same as partypicker 2DA
  Tag=follower tag
+
  Tag=follower tag
 
  AvailableEquipmentSlots=794624
 
  AvailableEquipmentSlots=794624
  Layout=2
+
  Layout=2
 
  EnableWeaponSets=0
 
  EnableWeaponSets=0
  
If the creature is not using the appearance "Dog, Party Member", it needs a new appearance line in APR_base. Most of the entries can be copied from the standard appearance (e.g. Wolf), but  
+
If   the creature is not using the appearance "Dog, Party Member", it needs a new appearance line in APR_base. Most of the entries can be copied from the standard appearance (e.g. Wolf), but  
  
 
  MaxScaleLevel=-1
 
  MaxScaleLevel=-1
Line 938: Line 638:
 
  AppearanceRestrictionGroup=1
 
  AppearanceRestrictionGroup=1
  
Otherwise, it won't be able to progress beyond a specified level. The player will receive an unwanted set of armour when the it is hired, and it won't be able to use Marabi crunch items. You may also need to tweak PERSPACE and/or BumpLevel if excessive clipping occurs.
+
Otherwise,   it won't be able to progress beyond a specified level. The player will   receive an unwanted set of armour when the it is hired, and it won't be able to use Marabi crunch items. You may also need to tweak PERSPACE and/or BumpLevel if excessive clipping occurs.
  
====Isn't there an easier way to do this?====
+
=== Isn't there an easier way to do this? ===
  
Possibly. There is a way of recruiting a follower by setting a plot flag. However I don't understand it, and I expect it still doesn't allow custom autolevel templates, full control over specialisations etc. There's still a fair bit of stuff hardcoded for the core followers, I'm not sure putting a custom follower through the same process as Al, Leli et al will have good results.
+
Possibly.   There is a way of recruiting a follower by setting a plot flag.   However I don't understand it, and I expect it still doesn't allow   custom autolevel templates, full control over specialisations etc.   There's still a fair bit of stuff hardcoded for the core followers, I'm   not sure putting a custom follower through the same process as Al, Leli et al will have good results.
  
 
The next section answers this question, up to a point.
 
The next section answers this question, up to a point.
  
==== What if there are more than three potential followers? ====
+
=== What if there are more than three potential followers? ===
This example handles larger numbers of followers, and doesn't force the player to use the party picker unless the party is already full.
+
 
 +
This   example handles larger numbers of followers, and doesn't force the   player to use the party picker unless the party is already full.
  
 
The prefix zzz, used throughout, can be replaced with whatever [[Prefixes_in_use | prefix]] you are using for your resources.
 
The prefix zzz, used throughout, can be replaced with whatever [[Prefixes_in_use | prefix]] you are using for your resources.
  
If you're making a new campaign, to keep life simple, allocate a unique number to each follower. You could either use a creature variable or a script, e.g.
+
If   you're making a new campaign, to keep life simple, allocate a unique   number to each follower. You could either use a creature variable or a   script, e.g.
<pre>
+
<dascript>
 
// Party member id (e.g. 1=Alicia, 2=Godwin...)
 
// Party member id (e.g. 1=Alicia, 2=Godwin...)
 
int zzzPartyMemberID(object oPartyMember)
 
int zzzPartyMemberID(object oPartyMember)
Line 964: Line 665:
 
   return 0;
 
   return 0;
 
}
 
}
</pre>
+
</dascript>
  
Make two separate plots, e.g. zzzpt_hired for when a follower is first recruited, and zzzpt_party to flag whether they're currently in the party. Use flag values that correspond to the unique follower id, e.g. ZZZPT_HIRED_GODWIN will be 2.
+
Make   two separate plots, e.g. zzzpt_hired for when a follower is first   recruited, and zzzpt_party to flag whether they're currently in the   party. Use flag values that correspond to the unique follower id, e.g.   ZZZPT_HIRED_GODWIN will be 2.
  
If you're modifying the official campaign, you won't be able to make this simplification - you'll need a set of plot flags for your new party members, similar to the official ones. The logic of what follows is still correct, it just means that instead of having one set of common code that works for everyone, you have to explicitly script each party member individually using their personal plot flags.
+
If you're modifying the official campaign, you won't be able to make this simplification -   you'll need a set of plot flags for your new party members, similar to   the official ones. The logic of what follows is still correct, it just   means that instead of having one set of common code that works for   everyone, you have to explicitly script each party member individually   using their personal plot flags.
  
Follower conversation is now very simple. In Godwin's dialogue, the hiring line will be conditional - when ZZZPT_HIRED_GODWIN is clear - and it will set ZZZPT_HIRED_GODWIN.  No conversation script is necessary. Instead, in the properties of the plot zzzpt_hired, we add a plot event script, as follows.
+
Follower conversation is now very simple. In Godwin's dialogue, the hiring line will be conditional - when ZZZPT_HIRED_GODWIN is clear -   and it will set ZZZPT_HIRED_GODWIN.  No conversation script is   necessary. Instead, in the properties of the plot zzzpt_hired, we add a   plot event script, as follows.
<pre>
+
<dascript>
 
// PARTY HIRE PLOT SCRIPT
 
// PARTY HIRE PLOT SCRIPT
 
//
 
//
Line 1,049: Line 750:
 
     return nResult;
 
     return nResult;
 
}
 
}
</pre>
+
</dascript>
 
We still need to handle the party picker events in our module event script:
 
We still need to handle the party picker events in our module event script:
<pre>
+
<dascript>
 
         // PARTY MEMBER ADDED - Allow XP gain. Come here, follow me, flag as party member.
 
         // PARTY MEMBER ADDED - Allow XP gain. Come here, follow me, flag as party member.
 
         case EVENT_TYPE_PARTYMEMBER_ADDED:
 
         case EVENT_TYPE_PARTYMEMBER_ADDED:
Line 1,068: Line 769:
 
             WR_SetPlotFlag(PLT_ZZZPT_PARTY, zzzPartyMemberID(oFollower), FALSE);
 
             WR_SetPlotFlag(PLT_ZZZPT_PARTY, zzzPartyMemberID(oFollower), FALSE);
 
         }
 
         }
</pre>
+
</dascript>
 
When we need to refer to a particular follower explicitly, we can still do so - for example, the flag ZZZPT_PARTY_GODWIN will tell use whether Godwin is currently in the party or not.
 
When we need to refer to a particular follower explicitly, we can still do so - for example, the flag ZZZPT_PARTY_GODWIN will tell use whether Godwin is currently in the party or not.
  
==== How do I turn off sustained abilities when a follower is dismissed? ====
+
=== How do I turn off sustained abilities when a follower is dismissed? ===
 
'''Please note:''' The 1.04 DA:O patch is bugged and removed/broke several features of the toolkit, if you are using it please follow [http://social.bioware.com/forum/1/topic/71/index/3172230/1#3172537 these steps] to re-enable them as one is required for the script below to compile.
 
'''Please note:''' The 1.04 DA:O patch is bugged and removed/broke several features of the toolkit, if you are using it please follow [http://social.bioware.com/forum/1/topic/71/index/3172230/1#3172537 these steps] to re-enable them as one is required for the script below to compile.
  
  
There is a potential exploit in what we've done so far. If a follower casts a sustained spell like Flaming Weapons, it affects all party members. When the follower is dismissed, the remaining party members continue to benefit from the spell.
+
There   is a potential exploit in what we've done so far. If a follower casts a sustained spell like Flaming Weapons, it affects all party members.   When the follower is dismissed, the remaining party members continue to   benefit from the spell.
  
We can prevent this by deactivating modal abilities before opening the party picker. Add these snippets to the module event script:
+
We can prevent this by deactivating modal abilities before opening the party picker. Add these snippets to the module event script:
<pre>
+
<dascript>
 
#include "ability_h"
 
#include "ability_h"
 
void zDeactivateModalAbilities();
 
void zDeactivateModalAbilities();
</pre>
+
</dascript>
 
and
 
and
<pre>
+
<dascript>
 
         case EVENT_TYPE_PARTYPICKER_INIT:
 
         case EVENT_TYPE_PARTYPICKER_INIT:
 
           {
 
           {
Line 1,091: Line 792:
 
             break;
 
             break;
 
           }
 
           }
</pre>
+
</dascript>
 
The function looks like this:
 
The function looks like this:
<pre>
+
<dascript>
 
// Deactivate party's modal abilities
 
// Deactivate party's modal abilities
  
 
void zDeactivateModalAbilities()
 
void zDeactivateModalAbilities()
 
{
 
{
   object   oPC            = GetHero();
+
   object   oPC            = GetHero();
   object [] oFollowerTable = GetPartyList(oPC);
+
   object[] oFollowerTable = GetPartyList(oPC);
   object   oFollower;
+
   object   oFollower;
   int       nPartySize    = GetArraySize(oFollowerTable);
+
   int     nPartySize    = GetArraySize(oFollowerTable);
   int []    nAbilityTable;
+
   int[]    nAbilityTable;
   int       nAbilityCount;
+
   int     nAbilityCount;
   int       nAbility;
+
   int     nAbility;
   int       i              = -1;
+
   int     i              = -1;
   int       j;
+
   int     j;
  
 
   while (++i < nPartySize)
 
   while (++i < nPartySize)
    {
+
  {
 
       oFollower      = oFollowerTable[i];
 
       oFollower      = oFollowerTable[i];
 
       nAbilityTable  = GetAbilityList(oFollower);
 
       nAbilityTable  = GetAbilityList(oFollower);
Line 1,116: Line 817:
  
 
       while (++j < nAbilityCount)
 
       while (++j < nAbilityCount)
        {
+
      {
 
           nAbility = nAbilityTable[j];
 
           nAbility = nAbilityTable[j];
  
Line 1,125: Line 826:
 
     }
 
     }
 
}
 
}
</pre>
+
</dascript>
 
If other ways of dismissing party members are permitted, the same function can be used.
 
If other ways of dismissing party members are permitted, the same function can be used.
  
== Advanced Follower Creation: An Alternative Approach ==
+
[[Category:Follower tutorials]]
 
+
While these things are to a large extent a matter of taste, I see no reason to reinvent the wheel when there is a perfectly good wheel at hand. The following describes how to adapt the existing machinery for the recruitment and tracking of your own party members.
+
 
+
First create the setup as described above in Simple Follower Creation, except for the scripts and plot tables. In addition, create the GDA extensions for a level-up table and the M2DA which points to those tables, as described in Advanced Follower Creation above.
+
 
+
Now ...
+
 
+
=== Step 1: Create a party plot table ===
+
 
+
Have a look at the gen00pt_party plot table and create a similar table for your own followers. Don't bother with the Defined Flags for the moment, just the Main Flags will do, ie recruited, in-camp and in-party for each follower. (Critical Note: Do not duplicate the plot table, create one from scratch. This is the case for ALL plot tables because of a show-stopping bug which means a new GUID is not assigned on Duplicate of a plot.)
+
 
+
=== Step 2: Create a party script ===
+
 
+
Duplicate the gen00pt_party script and rename for your module (conventionally plot tables and their plot scripts have the same name). Delete (or comment out) the Defined Flags section at the end and any lines dealing with logging and tutorials. Change names to those of your party members. If you are using gifting, approval or forced inclusion you may wish to retain those sections of code, but additional machinery will be needed for these facilities to work. For approval you can specify a starting level of approval here for each follower if you want it to be other than zero (as per the existing section dealing with Dog). Note that the custom hire function at the head of the script allows you to specify whether or not to invoke the party picker on recruitment. You may not want to do so for the first couple of followers (but see below). You should already have defined all required constants in a separate file for your module, eg <module prefix>_constants_h. If not, do so now and include it, as well as your plot table, at the head of the script. Delete includes that only refer to the main campaign. (If this is all Greek to you, spend a couple of hours examining the set-up of the Demo, which is packaged with the toolset, and go through the introductory tutorials referenced on the main page of this wiki).
+
 
+
Looking at the new party script, notice that the follower-in-camp function is not defined. This is because for some arcane reason it is defined in party_h, and unhappily references the main campaign's plot table. So we will have to create a new function to replace it, change the other function's flag reference (for the sake of neatness), and change the call in the main body of the script. (You may decide later for other reasons to replicate and customise party_h, but that won't interfere with re-creating this function here.)
+
 
+
Our two functions at the head of the script should be:
+
 
+
<dascript>void SetFollowerInParty(object oFollower, string sPlot, int nCampFlag)
+
{
+
    WR_SetPlotFlag(sPlot, nCampFlag, FALSE);
+
    WR_SetObjectActive(oFollower, TRUE);
+
    WR_SetFollowerState(oFollower, FOLLOWER_STATE_ACTIVE, TRUE);
+
    command cJump = CommandJumpToObject(GetPartyLeader());
+
    WR_AddCommand(oFollower, cJump);
+
}
+
 
+
void SetFollowerInCamp(object oFollower, string sPlot, int nPartyFlag)
+
{
+
    WR_SetPlotFlag(sPlot, nPartyFlag, FALSE);
+
    WR_SetObjectActive(oFollower, FALSE);
+
    WR_SetFollowerState(oFollower, FOLLOWER_STATE_AVAILABLE, FALSE);
+
}</dascript>
+
 
+
and the line in the in-camp sections for each follower should now be:
+
 
+
<dascript>SetFollowerInCamp(o<follower>, strPlot, <follower>_IN_PARTY);</dascript>
+
 
+
+
 
+
=== Step 3: Create package GDAs and reference them in you 2DA_base extension ===
+
 
+
Look at the packages_base GDA and create an extension GDA with one line for each follower based on a main campaign follower that is most like your own follower, eg a sword-shield warrior can be a copy of Alistair, a battle mage a copy of Morrigan, etc. You can look at the Excel version of this GDA to more easily see what the fields mean. It is here that you can specify what if any specialisation will be assigned on hire and the level at which this spec point becomes available. (The IDs for each spec are in the 4000-range of ABI_base.)
+
 
+
Most importantly, it is here also that you define the ID of both your level-up table and the AIP table you are about to create.
+
 
+
Now create aip_follower_<follower name> gda tables, again based on a existing tables most like your own followers. No changes should be required to the copies you create.
+
 
+
In the m2da_base_<your module> extension you have already created, add lines for your AIP tables with a package ID referencing the IDs you assigned in your packages extension table.
+
 
+
Carefully check that all IDs in your GDAs are correct and cross-reference properly, and save the GDAs to your override directory.
+
 
+
Desirable but not obligatory (except for non-humanoid characters), is an extension to the Portraits gda. Finally the Equipment Layout gda will need an extension for non-humanoids, who use a different equipment mask.
+
 
+
=== Step 4: Module script addition ===
+
 
+
Modify your own module script to point to the new plot table and your own followers. To see an example of the required code for the two new sections look at the relevant section of module_core, which are for EVENT_TYPE_PARTYMEMBER_ADDED and EVENT_TYPE_PARTYMEMBER_DROPPED.
+
 
+
=== How it works ===
+
 
+
The recruit flag is set in a conversation (usually, but it could be a script) and that event is then processed by the plot script, which among other things sets both the in-camp and in-party flags, and fires up the party picker by default. If the player then selects the follower in the picker, the in-camp flag is unset (or vice versa if the follower is not selected to join). Subsequently, if the character is selected (or unselected) in the picker, the event is picked up by the module script, which sets the relevant plot flag as true, and that plot event is then sent to the plot scipt which sets the other flag as false.
+
 
+
(Note that if the party picker is turned off for a particular follower's recruit event, that follower will be placed in the active party regardless of the number of members, so ONLY do this for a follower who cannot be recruited when there are already 3 possible followers.)
+
 
+
On recruitment, player_core rebuilds the character according to the specifications laid out in the GDAs, so no further scripting is required.
+
 
+
=== Uses ===
+
 
+
To recruit a party member, set the recruit flag from your plot table in the relevant dialogue. Adding or subtracting followers from the active party is automatically tracked via this machinery, and the plot flags can be interrogated in all your scripts and conversations if you need to know who is currently in the party and who is in camp.
+
 
+
Defined flags can be added as you need them to the script and plot table but there is no point having flags that you will not use, so as with all Defined Flags this is an as-you-go decision, but quite easy to do.
+
 
+
Hiring and firing of temporary party members at the start of a campaign (such as Ser Jory, or Jowan, or Soris, etc) can be handled by UT_HireFollower, and the party picker is not invoked (nor is the Party Camp an available destination).
+
 
+
=== Troubleshooting ===
+
 
+
If you haven't done so already, create a simple debugging area for your module where you can dump the player, all followers and a couple of conversation dummies, and set up a dialogue in which you can invoke all required flags and conditions and see if they actually work as expected. (This setup can be used to test many other components as well.) Global machinery such as this can be painful to set up and get right (unless you are of an exceptionally methodical cast of mind), but it is worthwhile to ensure they are absolutely bullet-proof before you develop the specifics of your module.
+
 
+
=== The KISS principle ===
+
 
+
This is an "out-of-the-box" solution, and once set up is quite robust. If you want variants not catered for here, do NOT try to modify what you already have working.
+
 
+
'''Use-case:''' You want a follower (example tag: dairren) to be recruited without a set specialisation and one level higher than the PC.
+
 
+
Go to your packages gda and set the spec field to 0. In the character's plot table (you should have at least one for each follower) create a flag which we will call BLAH_BLAH. In the table's plot scipt insert the following very simple event handler:
+
 
+
<dascript>case BLAH_BLAH:
+
        {
+
          SetCreatureProperty(oDairren, 38, 1.00); //awards the spec point; field from the properties gda
+
          int aLevel = GetLevel(oHero); //some simple arithmetic to find out the XP we need to award
+
          int bLevel = (aLevel+1);
+
          int aPoints = RW_GetXPNeededForLevel(aLevel);
+
          int bPoints = RW_GetXPNeededForLevel(bLevel);
+
          int targetPoints = (bPoints - aPoints);
+
          RewardXP(oDairren, targetPoints, FALSE, TRUE); //award the XP, and we're done
+
          break;
+
}</dascript>
+
+
The flag can be set in the same conversation as the recruit flag, so long as it comes afterwards. This way you not only maintain the integrity of your core machinery, but keep exceptions/variations for particular characters where they belong -- with that character's plot tables and scripts.
+
 
+
 
+
[[Category:Tutorials]]
+
{{Languages}}
+

Latest revision as of 20:54, 26 October 2014

Follow these steps to have full control over the creation of your follower, with options such as:

  • Unique level-up template
  • Class and specialisation chosen via script
  • Any starting state
  • Level higher than the PC
  • Starts with a specialisation point rather than a specific specialisation
  • Set plot flags in the call to the hire script

Prepare Creature, char_stage and Party Picker m2DAs

Follow the same steps to create your follower creature, char_stage and Party Picker m2DAs as you did in the basic follower tutorial.

Create Party Plot

While not necessary, it's very helpful to have plot flags set when a follower is hired or joins/leaves the active party. This makes conversation interjections and the like very easy.

Create a plot with appropriate flags. There's no real need to associate journal text with them:

Follower plot.jpg

See FAQ for an alternative approach to plots when there are more than three potential party members.

Add Plot Flags to Party Picker Event Intercept

We then update our module script to make use of that plot, like so:

#include "utility_h"
#include "wrappers_h"
#include "plt_bc_create_party"   //make sure you include your own plot, not mine
 
void main()
{
    event ev = GetCurrentEvent();
    int nEventType = GetEventType(ev);
    switch(nEventType)
    {
        case EVENT_TYPE_PARTYMEMBER_ADDED:
        {
            object oFollower = GetEventObject(ev, 0);
            SetLocalInt(oFollower, CREATURE_REWARD_FLAGS, 0);
            WR_SetFollowerState(oFollower, FOLLOWER_STATE_ACTIVE);
 
            AddCommand(oFollower, CommandJumpToLocation(GetLocation(GetHero())));   //Ensures follower appears at PC's location.
 
            if (GetTag(oFollower) == "bc_party_miera") {               //You must explicitly test for your follower's tag.
                WR_SetPlotFlag(PLT_BC_CREATE_PARTY, PARTY_MIERA_IN_PARTY, TRUE);     //Make sure you use your own flags!
            }
 
            break;
        }  
 
        case EVENT_TYPE_PARTYMEMBER_DROPPED:                    
        {
              object oFollower = GetEventObject(ev, 0); 
 
              if (GetTag(oFollower) == "bc_party_miera") { 
                WR_SetPlotFlag(PLT_BC_CREATE_PARTY, PARTY_MIERA_IN_PARTY, FALSE);     //As above, but set false.
              }
 
        }
 
    }
}

Create a Level Up Template

You can skip this step if you're content to use one of the generic Rogue, Wizard or Warrior templates, but I don't recommend it. Making a template that suits your character is easy and will almost always be better for the player than a generic one that spends points poorly.

Go to \Program Files\Dragon Age\tools\Source\2DA (or wherever you installed Dragon Age).

You should see a number of excel sheets of the form ALCharacter.xls (such as ALAlistair.xls, ALLeliana.xls, ALRogue_Default.xls etc). Open the one closest to your character (ie Morrigan or Wynne for a wizard, Leliana or Zevran for a rogue). Save a copy as ALYourcharacter.xls in whatever directory you're using to create your 2DAs, remembering to rename the worksheet ALYourcharacter (in this example, ALMiera.xls with ALMiera as its worksheet).

It should look something like this:

ALtable.jpg

Columns B and C are the talents/spells available to this character. Do not change them.

Columns F and G are the skills available to this character. Do not change them.

We will edit the remaining columns like so:

Setting Stat Weights

The stat weights in column J determine how the follower will spend their attribute points, in a rough ratio. So if Dexterity is set to 1.5 and Intelligence to 1, you should expect to see 3 points of Dex for every 2 points of Cunning in-game (note Intelligence is the label used in the toolset for Cunning).

Simply change the values in J to reflect how you'd like the character to spend their points. In this example we're creating a wizard, so we're not going to mess around:

Miera stat weights.jpg

This character will only raise magic. I set the value to 5 rather than something like 1 to provide room underneath for the other stats while still overwhelmingly favouring magic, but in practice I only really ever want that one stat.

In a note left on this column in the existing templates, Bioware's Georg Zoeller writes, "This is the weight of each attribute (row id links into properties.xls.id). 1.0 means 'try to keep this attribute level' (spend 1 point per level). 0.5 means 'try to spend 1 point every two levels' and so on."

The default Mage, Rogue and Warrior templates include column L labeled "AttInit." Editing these values seems to have no effect on followers set to use these templates.

Setting Talent and Skill Priorities

Columns D and E are the talents/spells that the character will buy, in preference order from top to bottom.

Columns H and I are the skills that the character will buy, in preference order from top to bottom.

To change these, just copy the appropriate two cells from columns B&C or F&G over the ones you want to replace.

For example, here we're copying Morrigan's template. Morrigan has Spider Shape high in her preferences, which we do not want.

AlMori talent pref.jpg

We decide we'd prefer Flame Blast, so we find it in columns B&C and copy both cells:

ALMori copy.jpg

Then we select the cells we want to replace in columns D&E and paste over them:

ALMori paste.jpg

Continue this process until your priorities list for both skills and talents/spells is exactly as you want it. Make sure you have at least as many priorities as the core follower you're copying - points that cannot be spent according to these priorities have a habit of vanishing.

If you used any abilities from a specialisation, make sure you remember to set that specialisation with the function we'll introduce later. The autolevel scripts will add specialisation abilities to a character regardless of whether they have that spec or not.

Create a M2DA_base_ m2DA

Dragon Age will need to know where to find your autolevel template. We tell it by extending M2DA_base.gda

Create a spreadsheet with the name and worksheet name in the form M2DA_base_ with your unique suffix (in this example, M2DA_base_fofbc.xls with M2DA_base_fofbc as a worksheet).

Set up its columns and data like so (note I used GDApp because Open Office wasn't cooperating for this one!):

M2da base fofbc.jpg

The ID should be very high to avoid conflicts. I've arbitrarily chosen 50,000+ here. Carefully note the ID you've chosen for your character, you will need it later.

Set the Label and Worksheet to be the name of your autolevel template worksheet (ALCharactername if you've been following this).

Set the PackageIDForAI to be 0, it shouldn't be needed for followers.

When you're done, use ExcelProcessor to make GDAs of both spreadsheets and copy them to your module's export folder.

FOR AUTOLEVEL TO WORK AFTER RECRUITING:

Look in packages.xls.

There is a column called LevelupTable. That links to a corresponding AL* table. For instance, row 81 is for Leliana. Her LevelupTable value is 258. If you look that up in 2DA_base, you'll see it links to ALLeliana.

(alternatively, you could try packages_base.gda)

Create a New Hire Function Include

This section is probably only relevent if you have a specific need to override the functionality of player_core.

Many vital steps of follower addition happen inside an event in player_core. Followers tend to be extremely buggy (no skill tree, for example) if this event does not fire.

However, that event is not very flexible. In order to control it to our requirements, we need to replicate its functionality inside our own script. This is probably much safer than messing with player_core directly!

Create a new script file, naming it something like hireCustomFollower_h. We will be including this wherever we want to hire a follower.

Paste in the following script:

#include "sys_chargen_h"
#include "wrappers_h"
#include "utility_h"
#include "sys_rewards_h"
#include "approval_h"
#include "sys_autolevelup_h"
 
/*  Jye Nicolson 5-Jan-2010
This function set duplicates the full functionality chain of UT_HireFollower, with the following exceptions:
 
-  Followers can gain XP
-  Autolevel status can be set (default off)
-    Followers can be set to any starting state (default Available) and  will  still be properly initalised and added to the party pool
-  Autolevel tables for non-core followers can be explicitly set.
-  Class and Specialisation can be chosen via script
-  Followers without specialisations are granted a spec point by default.
 
It should only ever be called once each for characters you intend to be full followers.
Much of the protective code handling summoned creatures etc. in player_core is not present here.
 
Calling the function:
 
Simple:
 
hireCustomFollower(oFollower, CLASS_WARRIOR);
 
Change the class to CLASS_WIZARD or CLASS_ROGUE as appropriate.  
This will hire your follower and make them available.  
They will auto level up with a default package, and receive a free spec point.
 
Best Practice:
 
hireCustomFollower(oFollower, CLASS_WARRIOR, PLT_YOUR_PARTY_PLOT, YOUR_FOLLOWER_JOINED_FLAG, ABILITY_TALENT_HIDDEN_CHAMPION);
 
Where   the plot and flag are those for your module (remember to create the   plot and include it on the calling script), and ABILITY_TALENT_HIDDEN   etc is the desired spec.
 
You should also have a custom ALTable set up.  
 
See   wiki for details, and remember to edit it in to   GetCustomFollowerALTable below or pass it directly as an argument to   hireCustomFollower.
 
Full argument list:
 
void hireCustomFollower (
        object oFollower,   //Pass your follower object, mandatory
 
        int nForceClass,    //Pass a Class constant here, usually CLASS_ROGUE, CLASS_WARRIOR, CLASS_WIZARD.  Mandatory due to a bug.
 
        string   sPlot = "",   //It's recommended you have a plot flag to be set when   the follower joins.  Pass the plot constant here.  Remember to #include   in calling script
 
        int nPlotFlag = "",  //And then pass the flag constant.  Will be set to TRUE if available.
 
        int   nForceSpec = 0,  //This is the ID of the Specialisation you want.   Note  they are NOT classes, but abilities.  The full list is:
                             //ABILITY_SPELL_HIDDEN_ARCANE_WARRIOR,   ABILITY_SPELL_HIDDEN_BLOODMAGE, ABILITY_SPELL_HIDDEN_SHAPESHIFTER,   ABILITY_SPELL_HIDDEN_SPIRIT_HEALER
                             //ABILITY_SPELL_HIDDEN_BARD, ABILITY_TALENT_HIDDEN_ASSASSIN, ABILITY_TALENT_HIDDEN_DUELIST, ABILITY_TALENT_HIDDEN_RANGER
                             //ABILITY_TALENT_HIDDEN_BERSERKER, ABILITY_TALENT_HIDDEN_CHAMPION, ABILITY_TALENT_HIDDEN_REAVER, ABILITY_TALENT_HIDDEN_TEMPLAR
                             //I recommended forcing a spec, particularly if your ALTable includes abilities from one.
 
        int   nALTable = 0,    //This is the ID of an ALTable from 2DA_base.GDA or   your module's m2DA_base_*.GDA  I recommended the latter, but you can   edit that into GetCustomFollowerALTable below rather than passing it.
 
        int   bInvokePicker = FALSE,  //Sets whether the party picker should be   opened on hiring.  I think it's cleaner to call the picker outside this   script, particularly if you have multiple hires at once.
 
        int nInitialState = FOLLOWER_STATE_AVAILABLE,  //This sets whether the follower joins the active party or not.  Options are:
                                                       //FOLLOWER_STATE_ACTIVE (put them in the active party)
                                                       //FOLLOWER_STATE_LOCKEDACTIVE (force them into the active party and keep them there, remember to change this later.
                                                       //FOLLOWER_STATE_AVAILABLE (make them available on the party picker (if you've set it up for them), but not in the active party)
                                                       //Plus some others you're unlikely to need at this time.  Defaults to AVAILABLE because having 4+ active followers is screwy.
 
        string   sCurrPlot = "",  //If you set FOLLOWER_STATE_ACTIVE or   FOLLOWER_STATE_LOCKEDACTIVE, the script will check to see if you passed   this.
                                //It   is recommended that you have a plot flag set for a given follower  being  in the active party, this makes conversation interjection etc.  much  easier.
 
        int nCurrPlotFlag = 0,  //This flag will be set if FOLLOWER_STATE_ACTIVE or FOLLOWER_STATE_LOCKEDACTIVE are true
                                //AND sCurrPlot has a value AND nCurrPlotFlag is > 0.  
                                //ie if you added someone to the active party and have a plot flag to cope with it.
 
        int   nAutolevel = 0,     //Sets the Autolevel flag on the character sheet.   0  is off, 1 is on, 2 forces it on and removes it so the player can't  turn  it off.
 
 
        bFreeSpecPoint = TRUE,  //This grants a specialisation point to the follower if they do not have a specialisation.  
                                //It's important to set this false for classes that do not have specs, such as CLASS_DOG.
 
        int   nTargetLevel = 0,   //If you want a specific level, set this.    Generally not worthwhile unless you set it higher than the player,  since  they'll just get XP from the party picker anyway.
 
        int   nMinLevel = 0       //Set this if there's a specific level you don't   want the follower to go below.  Probably only useful if the PC might be   very low level but not necessarily so. 
 
        )
*/       
/* GetCustomFollowerALTable()  
This function is where you put your custom table assignments.
 
You   should explicitly test for the tag of your follower (not mine!) and   assign a value to nTable from your m2DA extension to M2DA_base 
 
See wiki for details on how to do this, or ignore it to get the default Warrior/Rogue/Wizard AL tables.
 
NOTE: you MUST explicitly set a table for non-Warrior/Rogue/Wizards, eg dogs.  Use TABLE_AL_DOG for a default Mabari.
*/
 
int GetCustomFollowerALTable(object oFollower)
{
    int nTable = _GetTableToUseForAL(oFollower);
    string sTag = GetTag(oFollower);
 
    if(sTag  == "bc_party_miera") 
    {               
        nTable = 50143;   
    }   
    else if(sTag == "bc_party_jysavin") 
    {
        nTable = 50144;   
    }
    else if(sTag == "bc_party_geldual") 
    {
        nTable = 50145;   
    }
    else  if(sTag == "bc_party_braghon") 
    {
        nTable = 50146;   
    }
 
    return nTable;    
}   
 
// This just cleans up the main function a little
int GetCustomFollowerTargetLevel(object oFollower, object oHero, int nPackage, int nMinLevel = 0) 
{
    int nPlayerLevel = GetLevel(oHero);
    int nTargetLevel = 0;
 
    if(nPlayerLevel >= 13 || nPlayerLevel == 1 || !_UT_GetIsPlotFollower(oFollower)) 
    {
        nTargetLevel = nPlayerLevel;
    }
    else 
    {
        nTargetLevel = nPlayerLevel + 1;
    }
 
    // If nMinLevel is not specified, checks package 2DA for a value
    if(nMinLevel == 0) 
    {
        nMinLevel = GetM2DAInt(TABLE_PACKAGES, "MinLevel", nPackage);
    }
 
    if(nMinLevel > 0 && nMinLevel > nTargetLevel) 
    {
        nTargetLevel = nMinLevel;
    }
 
    return nTargetLevel;
}   
 
// Moving this black box out :)  I don't really understand it, but it should function if you have tactics set up in a package.
 
void InitCustomFollowerTactics(object oFollower, int nPackage) 
{
         int nTableID = GetM2DAInt(TABLE_PACKAGES, "FollowerTacticsTable", nPackage);
         if (nTableID != -1)
            {
             int nRows = GetM2DARows(nTableID);
             int nMaxTactics = GetNumTactics(oFollower);
 
             int nTacticsEntry = 1;
             int i;
             for (i = 1; i <= nRows && nTacticsEntry <= nMaxTactics; ++i)
                {
                        int bAddEntry = FALSE;
                        int nTargetType = GetM2DAInt(nTableID, "TargetType", i);
                        int nCondition = GetM2DAInt(nTableID, "Condition", i);
                        int nCommandType = GetM2DAInt(nTableID, "Command", i);
                        int nCommandParam = GetM2DAInt(nTableID, "SubCommand", i);
 
                        int nUseType = GetM2DAInt(TABLE_COMMAND_TYPES, "UseType", nCommandType);
                        if (nUseType == 0)
                        {
                            bAddEntry = TRUE;
                        }
                        else
                        {
                            bAddEntry = HasAbility(oFollower, nCommandParam);
                        }
 
                        if (bAddEntry)
                        {
                            SetTacticEntry(oFollower, nTacticsEntry, TRUE, nTargetType, nCondition, nCommandType, nCommandParam);
                            ++nTacticsEntry;
                        }
                    }
                }
}  
 
/* InitCustomFollowerSpec:
 
This function tries to set the forced Specialisation.  If there is none, it checks the package for one.  
 
If there isn't either of those, it grants a free spec point if bFreeSpecPoint is true.
 
*/
 
void InitCustomFollowerSpec(object oFollower, int nPackage, int nForceSpec, int bFreeSpecPoint) {
    // Find specialization, and optionally add a spec point if none is found.
 
        if (nForceSpec == 0) {
 
        int nSpecAbility = GetM2DAInt(TABLE_PACKAGES, "switch1_class", nPackage); // followers can have only 1 advanced class
         if(nSpecAbility > 0)
         {
          AddAbility(oFollower, nSpecAbility);
         } else {
             if (bFreeSpecPoint) {
                 SetCreatureProperty(oFollower, 38, 1.00);
             }
         }                    
 
        } else {
 
             AddAbility(oFollower, nForceSpec);
 
        }
}
 
/* hireCustomFollower()  (See doco at top of page)
 
I strongly suggest you reorder the parameters if you're adding many followers with advanced options.
 
Feel free to leave them alone if you only want to set class, plot, spec or don't mind long declarations.
 
Note nForceClass is currently compulsory due to flakiness with GetCreatureCoreClass()
*/
 
void hireCustomFollower(object oFollower, int nForceClass, string sPlot = "", int nPlotFlag = 0, int nForceSpec = 0, 
int nALTable = 0, int bInvokePicker = FALSE, int nInitialState = FOLLOWER_STATE_AVAILABLE, string sCurrPlot = "", 
int nCurrPlotFlag = 0, int nAutolevel = 0, int bFreeSpecPoint = TRUE, int nTargetLevel = 0, int nMinLevel = 0) 
{
 
        object oHero = GetHero();
 
        /* #################  BEGIN BASIC FOLLOWER JOIN BLOCK   ###################
 
        This loosely replicates WR_SetFollowerState.
 
 
        */    
 
        if (nForceClass == 0) {
            nForceClass = GetCreatureCoreClass(oFollower);           //This is not working.  Hence nForceClass mandatory.
        }
 
 
        SetGroupId(oFollower, GetGroupId(oHero));      //Puts the follower in the pc's Group.
        SetEventScript(oFollower, RESOURCE_SCRIPT_PLAYER_CORE);  //This makes them act like a player.
        SetFollowerState(oFollower, nInitialState);  //This sets whether they are available, in the active party etc.
 
        /* #################  END BASIC FOLLOWER JOIN BLOCK ##################### */
 
 
 
        /* #################  BEGIN PLAYER_CORE EVENT_TYPE_PARTY_MEMBER_HIRED EMULATION #################
         This   replicates the EVENT_TYPE_PARTY_MEMBER_HIRED handler from player_core,   stripped down for simplicity and allowing our custom options.
 
 
        */
 
        Chargen_EnableTacticsPresets(oFollower);    //I assume this is important.
 
        SetLocalInt(oFollower, FOLLOWER_SCALED, 1);  //This should prevent the follower being rescaled by player_core or what have you
 
        int nPackage = GetPackage(oFollower);  //Gets the package, which will be used to find a number of 2DA IDs.
        int nPackageClass = GetM2DAInt(TABLE_PACKAGES, "StartingClass", nPackage);  //I don't think this is used, even by player_core
 
 
        // set behavior according to package
        int nBehavior = GetM2DAInt(TABLE_PACKAGES, "FollowerBehavior", nPackage);
 
        if(nBehavior >= 0) {
            SetAIBehavior(oFollower, nBehavior);
        }
 
        Chargen_InitializeCharacter(oFollower);      //We initialise the follower and choose race/class.
 
        Chargen_SelectRace(oFollower,GetCreatureRacialType(oFollower));
        Chargen_SelectCoreClass(oFollower,nForceClass);        
 
 
         if (nTargetLevel == 0) {   //This block picks a target level if not specified
 
              nTargetLevel = GetCustomFollowerTargetLevel(oFollower, oHero, nPackage, nMinLevel);
         }
 
         int nXp = RW_GetXPNeededForLevel(Max(nTargetLevel, 1));      //Here is where the XP is calculated and rewarded
         RewardXP(oFollower, nXp, FALSE, FALSE);
 
         // -------------------------------------------------------------
         // add hidden approval talents - (JN: I don't know how to set these yet, but when I figure it out this should make it work)
         // -------------------------------------------------------------
         int nIndex = Approval_GetFollowerIndex(oFollower);
         Approval_AddFollowerBonusAbility(nIndex, 0);
 
          //Handle Specialisation
          InitCustomFollowerSpec(oFollower, nPackage, nForceSpec, bFreeSpecPoint);
 
         // -------------------------------------------------------------
         // This spends all available attribute and stat points on the
         // creature according to the levelup table.  (JN:  this replicates AL_DoAutoLevelUp but with our choice of table)
         // -------------------------------------------------------------
 
         if (nALTable == 0) {
            nALTable = GetCustomFollowerALTable(oFollower);
         }
 
         AL_SpendAttributePoints(oFollower, nALTable, FALSE);
         AL_SpendSkillPoints(oFollower, nALTable, TRUE);
         AL_SpendSpecializationPoints(oFollower, nALTable);
         AL_SpendTalentSpellPoints(oFollower, nALTable, TRUE);
 
        // -------------------------------------------------------------------------
        // Update various UIs
        // -------------------------------------------------------------------------
        Chargen_SetNumTactics(oFollower);
        SetCanLevelUp(oFollower,Chargen_HasPointsToSpend(oFollower));
 
        // load tactics
         InitCustomFollowerTactics(oFollower, nPackage);
 
         /* #################  END PLAYER_CORE EVENT_TYPE_PARTY_MEMBER_HIRED EMULATION ################# */     
 
 
         SetAutoLevelUp(oFollower, nAutolevel);         //This is the autolevel flag on the character sheet.
 
         //Set plot flags
 
         if (!((sPlot == "") || (nPlotFlag == 0))) {           //Joined Party
            WR_SetPlotFlag(sPlot, nPlotFlag, TRUE);   
         }
 
         if ((nInitialState == FOLLOWER_STATE_ACTIVE) || (nInitialState == FOLLOWER_STATE_LOCKEDACTIVE)) {
            if (!((sCurrPlot == "") || (nCurrPlotFlag == 0))) {
                WR_SetPlotFlag(sCurrPlot, nCurrPlotFlag, TRUE);   //Currently in Party
            }
         }
 
        // Invoke picker if requested.
 
        if (bInvokePicker) {
             SetPartyPickerGUIStatus(2);
             ShowPartyPickerGUI();
        }
}

Yeah, I know. It can't really be any smaller. Feel free to modify it if you're confident with scripting.

Add Your Custom Autolevel Template to GetCustomFollowerALTable()

While you can pass the ID you made for your autolevel template to that monster function as an argument, it's better to have them all in one place if you have multiple followers.

GetCustomFollowerALTable() is the first function in our include, and you can add an explicit if test for your follower there to assign the correct table id (the one from your M2DA_base_ m2DA). There is a function very much like it in sys_autolevel_h.nss for the core followers, so we'll copy Bioware's practice.

Let's take a look at the function by itself:

int GetCustomFollowerALTable(object oFollower) {
    int nTable = _GetTableToUseForAL(oFollower);
 
    if (GetTag(oFollower) == "bc_party_miera") {               
        nTable = 50143;   
    }   
 
    if (GetTag(oFollower) == "bc_party_jysavin") {
        nTable = 50144;   
    }
 
    if (GetTag(oFollower) == "bc_party_geldual") {
        nTable = 50145;   
    }
 
    if (GetTag(oFollower) == "bc_party_braghon") {
        nTable = 50146;   
    }
 
    return nTable;    
 
}

So we have a test for each follower tag from my module, matching up to an ID which is assigned to nTable. All you need to do is change a tag from my follower to yours, and my ID to the correct one from your M2DA_base_* m2DA. Then you should delete the rest of the example if statements :)

Save and export the script. Ignore the compiler error about lack of main();

Include Function In Your Hire Script and Call It

So instead of a hire script that calls UT_HireFollower(), we want one that includes our shiny new function and calls it.

Take a look at the following example:

#include "plt_bc_create_party"   //Make sure you include your party handling plot
#include "hireCustomFollower_h"  // And include the function script - which will in turn include a bunch of stuff
 
void main() {
 
                    //Initialising my objects, not super-relevant to the example 
 
                    object oHero = GetHero();
                    object oMiera = CreateObject(OBJECT_TYPE_CREATURE, R"bc_party_miera.utc", GetLocation(oHero));
                    object oJysavin = CreateObject(OBJECT_TYPE_CREATURE, R"bc_party_jysavin.utc", GetLocation(oHero));
                    object oBraghon = CreateObject(OBJECT_TYPE_CREATURE, R"bc_party_braghon.utc", GetLocation(oHero));
                    object oSpider = CreateObject(OBJECT_TYPE_CREATURE, R"bc_party_geldual.utc", GetLocation(oHero));
 
                    //Simplest hire call - adds to the party as a wizard.  Class is currently compulsory due to a bug.
                    hireCustomFollower(oMiera, CLASS_WIZARD);
 
                    //Add to the party and set joining plot flags
                    hireCustomFollower(oJysavin, CLASS_WARRIOR, PLT_BC_CREATE_PARTY, PARTY_JYSAVIN_JOINED);
 
                    //Add to the party, set plot flags, force a specialisation
                    hireCustomFollower(oBraghon, CLASS_ROGUE, PLT_BC_CREATE_PARTY, PARTY_BRAGHON_JOINED, ABILITY_TALENT_HIDDEN_ASSASSIN);
 
                    //More complex example - Follower added as a unique class (Dog), not granted a specialisation or spec point.  
                    //Note unique classes must have an ALTable passed here or specified in GetCustomFollowerALTable() or they won't work
                    hireCustomFollower(oSpider,   CLASS_DOG, PLT_BC_CREATE_PARTY, PARTY_GELDUAL_JOINED, 0, 0, FALSE,   FOLLOWER_STATE_AVAILABLE, "", 0, 0, FALSE);
 
                    //Show the party picker to let the player choose from their new companions!
                    SetPartyPickerGUIStatus(2);
                    ShowPartyPickerGUI();                                                                                                                        
}

The example shows several of the more simple ways of invoking the function. Check the comments at the start of the function for a full list of arguments.

I would suggest best practice for most followers would be to call as follows:

hireCustomFollower(oFollower, CLASS, PLOT, PLOT_FLAG, SPECIALISATION)

This will safely set the follower up as the desired class and specialisation (doubly important if there are spec abilities in their ALTable) while setting your plot flag for them being in the party. hireCustomFollower(oFollower, CLASS, PLOT, PLOT_FLAG, SPECIALISATION, 0, TRUE) will do the same while invoking the Party Picker automatically.

Note that the specialisations are abilities and not classes - you'll find them as ABILITY_HIDDEN_ constants.

If it's all worked, you should find you can now add followers with a lot more flexibility!

Picker advanced.jpg

Common Follower Problems & FAQ

Why don't my followers gain XP?

There is a bug in UT_HireFollower. Make a copy of UT_HireFollower in a function of your own, then make the following change:

   WR_SetFollowerState(oFollower, FOLLOWER_STATE_ACTIVE, TRUE, 0, TRUE);

to

   WR_SetFollowerState(oFollower, FOLLOWER_STATE_ACTIVE, TRUE, 0, bPreventLevelup);

When I choose followers from the Party Picker, they spawn into the area but do not join

You need to intercept the EVENT_TYPE_PARTYMEMBER_ADDED event and set the follower to FOLLOWER_STATE_ACTIVE. See Simple Follower Creation earlier in this document.

My followers don't have skill trees!

If a follower hasn't been through an initial chargen/autolevel event (via player_core/sys_autolevel_h) then the skill tree doesn't show. You're probably trying to be clever and get around UT_HireFollower without going all the way (see monster function above ^_^).

My followers don't have a class

GetCreatureCoreClass() seems flaky under some conditions. It's best to explicitly set the class yourself; this is why class is currently a mandatory argument to hireCustomFollower()

Dog talents and equipment slots don't work

Symptoms: dog talents appear on the talents screen but remain greyed out on level up. Warrior talents appear in the quickslots. Equipment slots are for humans, not dogs.

Line 78 of packages_base in packages.xls (Dog) should have the LevelUpTable column set to 257 which is the ALDog table.

The EquipmentLayout 2DA needs a new entry for the follower:

ID=same as partypicker 2DA
Tag=follower tag
AvailableEquipmentSlots=794624
Layout=2
EnableWeaponSets=0

If the creature is not using the appearance "Dog, Party Member", it needs a new appearance line in APR_base. Most of the entries can be copied from the standard appearance (e.g. Wolf), but

MaxScaleLevel=-1
DefaultArmor=
AppearanceRestrictionGroup=1

Otherwise, it won't be able to progress beyond a specified level. The player will receive an unwanted set of armour when the it is hired, and it won't be able to use Marabi crunch items. You may also need to tweak PERSPACE and/or BumpLevel if excessive clipping occurs.

Isn't there an easier way to do this?

Possibly. There is a way of recruiting a follower by setting a plot flag. However I don't understand it, and I expect it still doesn't allow custom autolevel templates, full control over specialisations etc. There's still a fair bit of stuff hardcoded for the core followers, I'm not sure putting a custom follower through the same process as Al, Leli et al will have good results.

The next section answers this question, up to a point.

What if there are more than three potential followers?

This example handles larger numbers of followers, and doesn't force the player to use the party picker unless the party is already full.

The prefix zzz, used throughout, can be replaced with whatever prefix you are using for your resources.

If you're making a new campaign, to keep life simple, allocate a unique number to each follower. You could either use a creature variable or a script, e.g.

// Party member id (e.g. 1=Alicia, 2=Godwin...)
int zzzPartyMemberID(object oPartyMember)
{
  string sPartyMember = GetTag(oPartyMember);
 
  if (sPartyMember == "zzzcr_alicia") return 1;
  if (sPartyMember == "zzzcr_godwin") return 2;
  if (sPartyMember == "zzzcr_harold") return 3;
  if (sPartyMember == "zzzcr_lara"  ) return 4;
  return 0;
}

Make two separate plots, e.g. zzzpt_hired for when a follower is first recruited, and zzzpt_party to flag whether they're currently in the party. Use flag values that correspond to the unique follower id, e.g. ZZZPT_HIRED_GODWIN will be 2.

If you're modifying the official campaign, you won't be able to make this simplification - you'll need a set of plot flags for your new party members, similar to the official ones. The logic of what follows is still correct, it just means that instead of having one set of common code that works for everyone, you have to explicitly script each party member individually using their personal plot flags.

Follower conversation is now very simple. In Godwin's dialogue, the hiring line will be conditional - when ZZZPT_HIRED_GODWIN is clear - and it will set ZZZPT_HIRED_GODWIN. No conversation script is necessary. Instead, in the properties of the plot zzzpt_hired, we add a plot event script, as follows.

// PARTY HIRE PLOT SCRIPT
//
// This is called in conversation when a party member is hired for the first time.
// If the party is full, the party picker is displayed, which forces the PARTYMEMBER_ADDED
// module event.
//
// The flag value is never referenced, because the code is common for all party members.
 
#include "events_h"
#include "global_objects_h"
#include "utility_h"
#include "sys_rewards_h"
#include "log_h"
#include "utility_h"
#include "wrappers_h"
#include "plot_h"
 
#include "zzz_h"                  // A header containing the zzzPartyMemberID function
#include "plt_zzzpt_hired"
#include "plt_zzzpt_party"
 
int StartingConditional()
{
    event  eParms             = GetCurrentEvent();
    int    nType              = GetEventType(eParms);       // GET or SET
    string strPlot            = GetEventString(eParms, 0);  // Plot GUID
    int    nFlag              = GetEventInteger(eParms, 1); // Plot flag
    object oParty             = GetEventCreator(eParms);    // Plot table owner
    object oFollower          = GetEventObject(eParms, 0);  // Conversation owner (if any)
    int    nPlotType          = GetEventInteger(eParms, 5); // Plot type
 
    int    bIsTutorial        = GetM2DAInt(TABLE_PLOT_TYPES, "IsTutorial", nPlotType);
    int    bIsCodex           = GetM2DAInt(TABLE_PLOT_TYPES, "IsCodex", nPlotType);
 
    int    nResult            = FALSE;                      // return value for DEFINED GET
    object oPC                = GetPartyLeader();
 
    plot_GlobalPlotHandler(eParms); // any global plot operations, including debug info
 
    if (nType == EVENT_TYPE_SET_PLOT) // actions -> normal flags only
    {
        int nValue    = GetEventInteger(eParms, 2); // 0=Clear 1=Set
        int nOldValue = GetEventInteger(eParms, 3); // Current flag value
 
        if (nValue)
          {
            if (GetArraySize(GetPartyList(oPC)) < 4)
              {
                // Code from UT_HireFollower with level up bug fixed
                SetAutoLevelUp(oFollower, 2);
                SetGroupId(oFollower, GetGroupId(GetHero()));
                WR_SetFollowerState(oFollower, FOLLOWER_STATE_ACTIVE, TRUE, 0, FALSE);  
                // End of code from UT_HireFollower
                AddCommand(oFollower, CommandJumpToLocation(GetLocation(GetHero())));
                WR_SetPlotFlag(PLT_ZZZPT_PARTY, zzzPartyMemberID(oFollower), TRUE);
              }
            else
              {
                WR_SetFollowerState(oFollower, FOLLOWER_STATE_AVAILABLE, FALSE);
                SetEventScript(oFollower, RESOURCE_SCRIPT_PLAYER_CORE);
                SendPartyMemberHiredEvent(oFollower, TRUE);
//                SetPartyPickerGUIStatus(PP_GUI_STATUS_USE);
//                ShowPartyPickerGUI();
              }
          }
     }
     else // EVENT_TYPE_GET_PLOT -> defined conditions only
     {
        switch(nFlag)
        {
 
        }
    }
 
    plot_OutputDefinedFlag(eParms, nResult);
    return nResult;
}

We still need to handle the party picker events in our module event script:

         // PARTY MEMBER ADDED - Allow XP gain. Come here, follow me, flag as party member.
         case EVENT_TYPE_PARTYMEMBER_ADDED:
         {
            object oFollower = GetEventObject(ev, 0);
            SetLocalInt(oFollower, CREATURE_REWARD_FLAGS, 0);
            AddCommand(oFollower, CommandJumpToLocation(GetLocation(GetHero())));
            WR_SetFollowerState(oFollower, FOLLOWER_STATE_ACTIVE);
            WR_SetPlotFlag(PLT_ZZZPT_PARTY, zzzPartyMemberID(oFollower), TRUE);
            break;
         }
         // PARTY MEMBER DROPPED - flag as not party member.
         case EVENT_TYPE_PARTYMEMBER_DROPPED:
        {
            object oFollower = GetEventObject(ev, 0);
            WR_SetPlotFlag(PLT_ZZZPT_PARTY, zzzPartyMemberID(oFollower), FALSE);
        }

When we need to refer to a particular follower explicitly, we can still do so - for example, the flag ZZZPT_PARTY_GODWIN will tell use whether Godwin is currently in the party or not.

How do I turn off sustained abilities when a follower is dismissed?

Please note: The 1.04 DA:O patch is bugged and removed/broke several features of the toolkit, if you are using it please follow these steps to re-enable them as one is required for the script below to compile.


There is a potential exploit in what we've done so far. If a follower casts a sustained spell like Flaming Weapons, it affects all party members. When the follower is dismissed, the remaining party members continue to benefit from the spell.

We can prevent this by deactivating modal abilities before opening the party picker. Add these snippets to the module event script:

#include "ability_h"
void zDeactivateModalAbilities();

and

         case EVENT_TYPE_PARTYPICKER_INIT:
           {
             // Deactivate party modal abilities. This ensures that the party no longer benefits
             // from spells like Flaming Weapons if the caster is dismissed from the party.
             zDeactivateModalAbilities();
             break;
           }

The function looks like this:

// Deactivate party's modal abilities
 
void zDeactivateModalAbilities()
{
  object   oPC            = GetHero();
  object[]  oFollowerTable = GetPartyList(oPC);
  object   oFollower;
  int      nPartySize     = GetArraySize(oFollowerTable);
  int[]    nAbilityTable;
  int      nAbilityCount;
  int      nAbility;
  int      i              = -1;
  int      j;
 
  while (++i < nPartySize)
  {
      oFollower      = oFollowerTable[i];
      nAbilityTable  = GetAbilityList(oFollower);
      nAbilityCount  = GetArraySize(nAbilityTable);
      j              = -1;
 
      while (++j < nAbilityCount)
      {
          nAbility = nAbilityTable[j];
 
          if (Ability_IsAbilityActive(oFollower, nAbility))
            if (Ability_IsModalAbility(nAbility))
              Ability_DeactivateModalAbility(oFollower, nAbility);
        }
    }
}

If other ways of dismissing party members are permitted, the same function can be used.