Grimlar - Introduction To Tag Based Scripting
Introduction
With the introduction of the second expansion pack, scripting for items took a big step forward in terms of how easy it is to write the code to make the items do the stuff you want them to do. The main reason it became a lot easier was the introduction of Tag Based Scripting.
The premise is simple: instead of all the code for all the items having to go in the various events, such as OnActivateItem, these events now just retrieve the Tag from the item and execute a script with the same name as that Tag. As such, carrying an item from one module to another is no more difficult than importing an erf containing the item blueprint, the single script file, and then figuring out how and where to place the item in the game. Oh, and then compiling the module. No existing scripts need to be altered.
So how does all this actually work?
Prerequisites
This tutorial assumes that you have an understanding of the NWN scripting language and are familiar with the Aurora Toolset, i.e. you know how to place and set up encounters, traps, doors etc. If this isn't the case, please go and have a look at Celowin's scripting tutorials and Bioware's module construction tutorial first.
Whilst this tutorial has been written with HotU in mind, the main script files for Tag Based Scripting have been included in the recent patches for NWN and SoU, so the majority of this tutorial should apply to them too, except possibly for the OnSpellCastAt event. An example module has been included demonstrating Tag Based Scripting as a method of creating a semi intelligent weapon, it can be downloaded here.
Setup
Setting your module up to use Tag Based Scripting is actually quite easy since it is turned on by default for new modules once you've installed HoTU.
If you check your module properties page, you should notice a script called x2_mod_def_load in the OnModuleLoad event. This script provides the builder with the option of turning on or off various subsystems in the revised game engine. One of these is Tag Based Scripting.
To ensure that tag based scripting is turned on, make sure the following line is uncommented and you should be ready to start coding.
Once x2_mod_def_load has been set up to your liking, all that remains to do is to check that the following scripts have been entered for the indicated events:
Event | Script (see below) |
---|---|
OnActivateItem | x2_mod_def_act.nss |
OnAcquireItem | x2_mod_def_aqu.nss |
OnPlayerEquipItem | x2_mod_def_equ.nss |
OnPlayerUnEquipItem | x2_mod_def_unequ.nss |
OnUnAcquireItem | x2_mod_def_unaqu.nss |
Again, these should all be set correctly by default when you build a new module after you've installed HoTU.
You will also find x2_s3_onhitcast and x2_inc_spellhook are necessary for the OnHitCastSpell and OnSpellCastAt events to work properly. However, there is nothing you need to set or check with these scripts.
Note: There is a security feature in x2_mod_def_load intended for servers running with Local Vault characters. By calling SetUserDefinedItemEventPrefix() you can set a prefix for the item scripts (e.g., SetUserDefinedItemEventPrefix("1_") will make an item with a Tag of "test" call the script "1_test" instead of "test". The intention is that this additional level of indirection will make it harder for people to execute unauthorised scripts. This security fix is not necessary for server-vault-only servers.
Code
All you need to do now is create a script with the same name as the Tag of the item you want the script to work for. So if the item has a Tag of "Test", the script you create must be called "test".
When writing your own item scripts it's probably a good idea to start with an example script and adjust it to suit your own needs. There is an example of how such a script might look in "x2_it_example". It can be found by opening the Load Script File window, clicking on the "All Resources" checkbox, and typing the name into the Filename box. You'll notice that this one script handles all five of the events mentioned above as well as the On Hit Cast Spell and OnSpellCastAt events. (Also please note that this script has been tidied up a little for the purpose of this tutorial.)
//:: Example Item Event Script
//:: x2_it_example
//:: Copyright (c) 2003 Bioware Corp.
//:://////////////////////////////////////////////
/*
This is an example of how to use the
new default module events for NWN to
have all code concerning one item in
a single file.
Note that this system only works if
the following scripts are set in your
module events
OnEquip - x2_mod_def_equ
OnUnEquip - x2_mod_def_unequ
OnAcquire - x2_mod_def_aqu
OnUnAcqucire - x2_mod_def_unaqu
OnActivate - x2_mod_def_act
*/
//:://////////////////////////////////////////////
//:: Created By: Georg Zoeller
//:: Created On: 2003-09-10
//:: Modified By: Grimlar
//:: Modified On: March 2004
//:://////////////////////////////////////////////
#include "x2_inc_switches"
void main()
{
int nEvent = GetUserDefinedItemEventNumber(); // Which event triggered this
object oPC; // The player character using the item
object oItem; // The item being used
object oSpellOrigin; // The origin of the spell
object oSpellTarget; // The target of the spell
int iSpell; // The Spell ID number
// Set the return value for the item event script
// * X2_EXECUTE_SCRIPT_CONTINUE - continue calling script after executed script is done
// * X2_EXECUTE_SCRIPT_END - end calling script after executed script is done
int nResult = X2_EXECUTE_SCRIPT_END;
switch (nEvent)
{
case X2_ITEM_EVENT_ONHITCAST:
// * This code runs when the item has the 'OnHitCastSpell: Unique power' property
// * and it hits a target (if it's a weapon) or is being hit (if it's a piece of armor)
// * Note that this event fires for non PC creatures as well.
oItem = GetSpellCastItem(); // The item triggering this spellscript
oPC = OBJECT_SELF; // The player triggering it
oSpellOrigin = OBJECT_SELF ; // Where the spell came from
oSpellTarget = GetSpellTargetObject(); // What the spell is aimed at
// Your code goes here
break;
case X2_ITEM_EVENT_ACTIVATE:
// * This code runs when the Unique Power property of the item is used or the item
// * is activated. Note that this event fires for PCs only
oPC = GetItemActivator(); // The player who activated the item
oItem = GetItemActivated(); // The item that was activated
// Your code goes here
break;
case X2_ITEM_EVENT_EQUIP:
// * This code runs when the item is equipped
// * Note that this event fires for PCs only
oPC = GetPCItemLastEquippedBy(); // The player who equipped the item
oItem = GetPCItemLastEquipped(); // The item that was equipped
// Your code goes here
break;
case X2_ITEM_EVENT_UNEQUIP:
// * This code runs when the item is unequipped
// * Note that this event fires for PCs only
oPC = GetPCItemLastUnequippedBy(); // The player who unequipped the item
oItem = GetPCItemLastUnequipped(); // The item that was unequipped
// Your code goes here
break;
case X2_ITEM_EVENT_ACQUIRE:
// * This code runs when the item is acquired
// * Note that this event fires for PCs only
oPC = GetModuleItemAcquiredBy(); // The player who acquired the item
oItem = GetModuleItemAcquired(); // The item that was acquired
// Your code goes here
break;
case X2_ITEM_EVENT_UNACQUIRE:
// * This code runs when the item is unacquired
// * Note that this event fires for PCs only
oPC = GetModuleItemLostBy(); // The player who dropped the item
oItem = GetModuleItemLost(); // The item that was dropped
// Your code goes here
break;
case X2_ITEM_EVENT_SPELLCAST_AT:
//* This code runs when a PC or DM casts a spell from one of the
//* standard spellbooks on the item
oPC = OBJECT_SELF; // The player who cast the spell
oItem = GetSpellTargetObject(); // The item targeted by the spell
iSpell = GetSpellId(); // The id of the spell that was cast
// See the list of SPELL_* constants
// Your code goes here
// Change the following line from X2_EXECUTE_SCRIPT_CONTINUE to
// X2_EXECUTE_SCRIPT_END if you want to prevent the spell that was
// cast on the item from taking effect
nResult = X2_EXECUTE_SCRIPT_CONTINUE;
break;
}
// Pass the return value back to the calling script
SetExecutedScriptReturnValue(nResult);
}
Demo Module
The Dagger of Irritation
This demo module includes a relatively simple item, a semi-intelligent (irritating) dagger. It says something for each of the events that Tag Based Scripting handles and has one or two powers that are chosen randomly when the item is activated.
The following code is the complete script for the Dagger of Irritation itself. In order for this to all work there also needs to be an invisible object placeable to act as the dagger's voice, and obviously a conversation file so that the dagger has something to say.
//:: Dagger Of Random Annoyance Item Event Script
//:: daggerofrandom.nss
//:: Copyright (c) 2003 Bioware Corp.
//:://////////////////////////////////////////////
/*
This is an example of how to use the
new default module events for NWN to
have all code concerning one item in
a single file.
Note that this system only works if
the following scripts are set in your
module events
OnEquip - x2_mod_def_equ
OnUnEquip - x2_mod_def_unequ
OnAcquire - x2_mod_def_aqu
OnUnAcqucire - x2_mod_def_unaqu
OnActivate - x2_mod_def_act
*/
//:://////////////////////////////////////////////
//:: Created By: Georg Zoeller
//:: Created On: 2003-09-10
//:: Modified By: Grimlar
//:: Modified On: March 2004
//:://////////////////////////////////////////////
#include "x0_i0_position"
#include "x2_inc_switches"
// A short function that creates an invisible placeable to hold a conversation with
void SpeakOneLiner(int nConvLine, object oPC, string sPlaceableRef);
// Returns a location directly ahead of the target and
// facing the target
location GetStepOppositeLocation(object oTarget);
// The resref for the hidden object used to allow a conversation with the item.
const string PL_REF = "plitdaggerofirri";
const string CV_REF = "cvitdaggerofirri";
void main()
{
int nEvent = GetUserDefinedItemEventNumber(); //Which event triggered this
object oPC;
object oItem;
object oSpellOrigin;
object oSpellTarget;
location lSpellLocation;
int iSpell;
int nRand;
int nResult = X2_EXECUTE_SCRIPT_END;
switch (nEvent)
{
case X2_ITEM_EVENT_ONHITCAST:
// * This code runs when the item has the 'OnHitCastSpell: Unique power' property
// * and it hits a target(if it is a weapon) or is being hit (if it is a piece of armor)
// * Note that this event fires for non PC creatures as well.
oItem = GetSpellCastItem(); // The item triggering this spellscript
oPC = OBJECT_SELF; // The player triggering it
oSpellOrigin = OBJECT_SELF ; // Where the spell came from
oSpellTarget = GetSpellTargetObject(); // What the spell is aimed at
// Your code goes here
// Choose what to do, paralyse or extra damage
nRand = 6 + Random(2);
if (nRand == 6)
{
//Create a magical damage effect, 1 to 20 points of damage
effect eDam = EffectDamage(1+Random(20));
ApplyEffectToObject(DURATION_TYPE_INSTANT, eDam, oSpellTarget);
}
else
{
// Create a magical paralyze effect, apply it for 2 to 6 seconds
effect ePar = EffectParalyze();
float fDur = IntToFloat(2 + Random(5));
effect eDur = EffectVisualEffect(VFX_DUR_CESSATE_NEGATIVE);
effect eDur2 = EffectVisualEffect(VFX_DUR_PARALYZED);
effect eDur3 = EffectVisualEffect(VFX_DUR_PARALYZE_HOLD);
effect eLink = EffectLinkEffects(eDur2, eDur);
eLink = EffectLinkEffects(eLink, ePar);
eLink = EffectLinkEffects(eLink, eDur3);
ApplyEffectToObject(DURATION_TYPE_TEMPORARY, eLink, oSpellTarget, fDur);
}
// Let the dagger say its piece
SpeakOneLiner(nRand, oPC, PL_REF);
break;
case X2_ITEM_EVENT_ACTIVATE:
// * This code runs when the Unique Power property of the item is used
// * Note that this event fires for PCs only
oPC = GetItemActivator(); // The player who activated the item
oItem = GetItemActivated(); // The item that was activated
lSpellLocation = GetItemActivatedTargetLocation(); // The target creature
// Your code goes here
// Choose what to do, fireball or banshee
nRand = 8 + Random(2);
if (nRand == 8)
{
// Cast a wail of the banshee
AssignCommand(oPC,ActionCastSpellAtLocation(SPELL_WAIL_OF_THE_BANSHEE, lSpellLocation, METAMAGIC_NONE, TRUE, PROJECTILE_PATH_TYPE_DEFAULT, TRUE));
}
else
{
// Cast a fireball spell
AssignCommand(oPC,ActionCastSpellAtLocation(SPELL_FIREBALL, lSpellLocation, METAMAGIC_NONE, TRUE, PROJECTILE_PATH_TYPE_DEFAULT, TRUE));
}
// Let the dagger say its piece
SpeakOneLiner(nRand, oPC, PL_REF);
break;
case X2_ITEM_EVENT_EQUIP:
// * This code runs when the item is equipped
// * Note that this event fires PCs only
oPC = GetPCItemLastEquippedBy(); // The player who equipped the item
oItem = GetPCItemLastEquipped(); // The item that was equipped
// Your code goes here
// Let the dagger say its piece
SpeakOneLiner(4, oPC, PL_REF);
break;
case X2_ITEM_EVENT_UNEQUIP:
// * This code runs when the item is unequipped
// * Note that this event fires PCs only
oPC = GetPCItemLastUnequippedBy(); // The player who unequipped the item
oItem = GetPCItemLastUnequipped(); // The item that was unequipped
// Your code goes here
// Let the dagger say its piece
SpeakOneLiner(5, oPC, PL_REF);
break;
case X2_ITEM_EVENT_ACQUIRE:
// * This code runs when the item is acquired
// * Note that this event fires PCs only
oPC = GetModuleItemAcquiredBy(); // The player who acquired the item
oItem = GetModuleItemAcquired(); // The item that was acquired
// Your code goes here
// Let the dagger say its piece
SpeakOneLiner(2, oPC,PL_REF);
break;
case X2_ITEM_EVENT_UNACQUIRE:
// * This code runs when the item is unacquired
// * Note that this event fires PCs only
oPC = GetModuleItemLostBy(); // The player who dropped the item
oItem = GetModuleItemLost(); // The item that was dropped
// Your code goes here
// Let the dagger say its piece
SpeakOneLiner(3, oPC, PL_REF);
break;
case X2_ITEM_EVENT_SPELLCAST_AT:
//* This code runs when a PC or DM casts a spell from one of the
//* standard spellbooks on the item
oPC = OBJECT_SELF; // The player who cast the spell
oItem = GetSpellTargetObject(); // The item targeted by the spell
iSpell = GetSpellId(); // The id of the spell that was cast
// See the list of SPELL_* constants
//Your code goes here
//Let the dagger say its piece
SpeakOneLiner(1, oPC, PL_REF);
// Change the following line from X2_EXECUTE_SCRIPT_CONTINUE to
// X2_EXECUTE_SCRIPT_END if you want to prevent the spell that was
// cast on the item from taking effect
nResult = X2_EXECUTE_SCRIPT_CONTINUE;
break;
}
// Set the return value for the item event script
// * X2_EXECUTE_SCRIPT_CONTINUE - continue calling script after executed script is done
// * X2_EXECUTE_SCRIPT_END - end calling script after executed script is done
SetExecutedScriptReturnValue(nResult);
}
void SpeakOneLiner(int nConvLine, object oPC, string sPlaceableRef)
{
location lLoc = GetStepOppositeLocation(oPC);
// Create the invisible placeable
object oPlace = CreateObject(OBJECT_TYPE_PLACEABLE, sPlaceableRef, lLoc);
// Set the line of conversation to be spoken
SetLocalInt(oPlace,"IT_CONV",nConvLine);
// Delay the speech slightly to give the object time to appear properly.
DelayCommand(0.2f,AssignCommand(oPlace, SpeakOneLinerConversation(CV_REF, oPC)));
}
// Returns a location directly ahead of the target and
// facing the target
location GetStepOppositeLocation(object oTarget)
{
float fDir = GetFacing(oTarget);
float fAngleOpposite = GetOppositeDirection(fDir);
return GenerateNewLocation(oTarget, DISTANCE_TINY, fDir, fAngleOpposite);
}
The comments in the code explain most of what is actually happening. Simply put, each event has at least one line of conversation that can be said so that you can see the code is working.
The OnHitCast event handler also makes a choice between two types of effect, either causing extra damage to the creature or paralyzing them. Each effect will be accompanied by a different line of conversation.
The OnActivate event handler similarly chooses between two types of effect, either causing a wail of the banshee or a fireball. Again, each effect will have a different line of conversation.
The GetStepOppositeLocation function, included at the end of the script, simply returns a location one pace in front of the player (oTarget), which will be where the dagger will appear to be speaking from. INote: without the slight delay to the conversation, the portrait of the placeable does not appear properly.
The SpeakOneLiner function starts a one line conversation between the player (oPC) and the invisible dagger placeable (sPlaceableRef). The actual line of conversation used is determined by the integer nConvLine.
That's pretty much all there is to it.
Adding a script to your list of templates
When you are using the script editor, you may have noticed a button over to the top right of your screen marked "Templates".
Clicking on this button brings up a list of various script templates, usually for things like user-defined scripts and OnSpawn scripts. The idea is that these partly complete scripts can be copied into your own script file to give you an idea of what to put, leaving you with a kind of "fill in the blanks" exercise.
It's actually quite simple to add a new script to this list of templates. First of all, choose the script you want to use as a template. Then, before you actually turn that script into a template, there are a few things you may wish to check. (These aren't exactly requirements, but they may well save you time and effort in the long run.)
- Check that the script contains nothing specific to your module (i.e., nothing that would cause the script to fail if you used it in a different module).
- You should probably check that the script is well commented, clearly explaining what the script does and what bits must be completed to turn it into a fully functional script. Just because you understand it now doesn't mean you will when you look at it again in 3 months time.
- You may also want to include additional code, commented out, that is likely to be required but not necessarily all the time.
- Finally, it's also a good idea to make sure that the script actually compiles before you do anything else with it.
When you are happy with the script, open a text editor (something like Notepad will be fine), cut and paste the script into it, and save the resulting file as whatever you want the template to be called. So if you want the template to appear as "Item Event Script", save the file as "Item Event Script.txt". This text file then needs to be moved to the "scripttemplates" directory located within your NWN directory.
That's it! The next time you open your script editor the new template should appear in the list, ready to use.
Did You Know...?
Tag based scripting started with a simple one line example script posted by tjm on NWVault that was later incorporated into SoU before being expanded into the current system. Have a look and see... .
author: Grimlar