Grimlar - Introduction To Tag Based Scripting

From NWN Lexicon
Jump to: navigation, search

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.

Warning icon orange.png The link to the demo module seems to be broken. If anyone has access to it, please post the link.

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.

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:

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.)

Information icon.png 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.
//::///////////////////////////////////////////////
//:: 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