Grimlar - Introduction To Tag Based Scripting

Prerequisites


It is assumed 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 Celowins 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.


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?


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 pages 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 sub systems in the revised game engine. One of these is Tag Based Scripting.


Tag based scripting is actually turned on by default, make sure the following line is uncommented and you should be ready to start coding.


SetModuleSwitch (MODULE_SWITCH_ENABLE_TAGBASED_SCRIPTS, TRUE);


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:


EventScript (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: Its perhaps worth mentioning that there is an additional 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, ie '1_test' will be called instead of 'test' for an item with a Tag of 'test'. The intention is that this additional level of indirection will make it harder for people to execute unauthorised scripts.


Code


All you need to do 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" then the script you create must be called "test".


There is an example of how such a script might look in 'x2_it_example' which can be found by opening the load script file window, clicking on the 'All Resources' checkbox and typing the name into the filename box.


When writing your own item scripts its probably a good idea to start with the example script and adjust it to suit your own needs.


Note: The 'x2_it_example' script can be added to your templates to make it easier to use in future. Instructions for this have been included at the end of this tutorial.


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 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
            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 daggers 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 paralyse 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 paralysing 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. Its worth noting that 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 on spawn 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.


Its 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. (NB. These aren't exactly requirements, but they may well save you time and effort in the long run.)

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... http://nwvault.ign.com/Files/scripts/data/1033068060707.shtml.





 author: Grimlar