Celowin - Part IX Functions that Return Default Parameters Libraries

From NWN Lexicon
Jump to: navigation, search

Celowin - Part IX: Functions that Return, Default Parameters, Libraries

Preamble


The purpose of this sequence of lessons is to take a complete beginner to programming, and teach him or her how to use NWNScript to write modules. The early lessons will be very basic, and anyone that has done any coding at all will be able to skip over them. The goal here is to make the lessons so that even the people that just shudder at any type of code can learn.


Feel free to post these lessons on any forum, print them out, or modify them. However, just give me credit for doing them.


Any comments on these lessons, good or bad, can be sent to me, Celowin .


I am going to assume that anyone looking at these lessons has at least played around with the Aurora Toolset a bit. If there is enough feedback that people don't know how to do the simple placements that I have in these lessons, I will consider spelling out in more detail what needs to be done.


Introduction


It has been awhile since my last lesson, and this one is going to build heavily on that one. So if you were at all shaky on lesson 8, I recommend re-reading it. You don't have to have it mastered, and in fact I would expect that most people won't be that far along. In fact, I'm hoping that seeing more examples in this lesson will help people that are still a bit confused on the subject. However, if you don't at least get the general idea of the last lesson, this one won't make much sense at all.


We're going to be taking the idea of the function from the last lesson, and extending that idea in a couple of different ways. The last time, we talked about the very basics of using a function, and while there were uses for it, the concept was very limited. Here, we're going to add a few things to make functions much more versatile.


Functions that Return


Up until this point, every function that we've written ourselves has been an "action." That is, it has done things, but it hasn't returned an answer. We specified that our function was an action by declaring it as type void.


My guess is that nearly all of the functions you will end up writing will end up being actions. However, there are a few times when you need to calculate something. Let's do a simple example... suppose we have a mage's guild. As they are fond of their privacy, their tower can only be accessed through a magical portal, and that portal makes sure only to transport in people whose primary focus is magery.


The way I'm going to handle this is to write a function to check the percentage, by level, of a character's magical classes. A pure level 1 Wizard with no multiclass would return 100%. A second level Fighter, third level Sorcerer multiclassed character would return 60%. Both of those would be let in, but the level 7 Rogue, level 2 Wizard would only be 22.22%, and would be left out in the cold.


First, let's write our function. We want it to take in an object (a creature), and spit out the percentage. We'll give the percentage in decimal form. If you remember, a decimal number is type float. So to declare the function, it is:


float GetMagePercent(object oPerson)

Really, the calculation done here is fairly easy. We need to get the total level of the creature passed to the function. We also need the levels of wizard and sorcerer, easily gotten with GetLevelByClass. A simple division, and we have our percentage.


The thing is, how do we tell the function we are writing that the number we found is our "answer"? It is fairly simple... we tell it to return the value. Let's take a look at the entire code.


// This function will return the percent of a creature's
// levels that are mage-related, expressed as a decimal
// number. It will return 0.0 if the object passed to it
// is not a creature.
float GetMagePercent(object oPerson)
{
  int nTotalLevel = GetHitDice(oPerson);
  if (nTotalLevel == 0)  // if not a creature, return 0.0
    return 0.0;
  else  // else, calculate the percent and return it
  {
    int nMageLevel = GetLevelByClass(CLASS_TYPE_WIZARD, oPerson) + GetLevelByClass(CLASS_TYPE_SORCERER, oPerson);
    float fMagePercent = IntToFloat(nMageLevel)/IntToFloat(nTotalLevel);
    return fMagePercent;
  }
}

Most of it should be familiar to you, it is just the last line that is new. We've spent the rest of the script calculating fMagePercent, the last line just says that when the function is called, that will be the output.


A further note on what I did: notice that I am careful about what happens if GetHitDice returns 0. If we use our function properly, that should never happen. However, the more "checks" you put in to deal with errors, the better. It may be that a weapon accidentally gets passed to our function. If that were to happen, we'd get a "division by 0" error, and cause all sorts of havoc.


Now that the function is written, how do we make use of it? Well, pretty much like any BioWare written function. (Again, remember that for the moment, we have to put the whole function code into our script where we want to call it). So, plop down a portal placeable (I actually used a magic sparks, for variety), and put a trigger around it. Paint a waypoint elsewhere, and call it WIZTOWER_WP


Put the following into the OnEnter code for the trigger:


// This function will return the percent of a creature's
// levels that are mage-related, expressed as a decimal
// number. It will return 0.0 if the object passed to it
// is not a creature.
float GetMagePercent(object oPerson)
{
  int nTotalLevel = GetHitDice(oPerson);
  if (nTotalLevel == 0)  // if not a creature, return 0.0
    return 0.0;
  else  // else, calculate the percent and return it
  {
    int nMageLevel = GetLevelByClass(CLASS_TYPE_WIZARD, oPerson) + GetLevelByClass(CLASS_TYPE_SORCERER, oPerson);
    float fMagePercent = IntToFloat(nMageLevel)/IntToFloat(nTotalLevel);
    return fMagePercent;
  }
}
 
void main()
{
  object oPC = GetEnteringObject();
  if (GetIsPC(oPC) && (GetMagePercent(oPC) > 0.50))
    AssignCommand(oPC, JumpToLocation(GetLocation(GetWaypointByTag("WIZTOWER_WP"))));
}

It might not be a bad idea to break the AssignCommand line into several steps... first get the waypoint, then the location, and then jump to that location. It is sort of a matter of style... here, I can follow the levels of nesting fairly easily, so I don't feel the need to break it down into steps.


Another Example


Here's another example of a function to calculate something. Sometimes (well, OK, often), I like to be a little bit sadistic to my PCs. I made an imp that had a key that they needed. He offers to sell the key to them, for a certain amount of gold. The kicker is that the gold he asks for is always 1GP more than the entire party has....


So, we need a function for finding the total party gold. Obviously, this will be an integer. There is a function to get the gold from one PC, so we'll just loop through all the PCs, check if they are in the party, and if so add the gold to the count.


Here is my version of the function, then:


// Return the total gold of all PCs in the same party as oPC.
// It will return 0 if the input is not a player character.
int GetPartyGold(object oPC)
{
  if (!GetIsPC(oPC))  // if not a PC, return 0
    return 0;
  else  // loop to get the gold
  {
    object oCharacter = GetFirstPC();
    int nGoldCount = 0;
    while (oCharacter != OBJECT_INVALID)
    {
      if (GetFactionEqual(oPC, oCharacter))  // different parties have different factions
        nGoldCount = nGoldCount + GetGold(oCharacter);
      oCharacter = GetNextPC();
    }
    return nGoldCount;
  }
}

To test this, go ahead and put an NPC into your module. Make a conversation for it, getting as detailed as you want. At some point, put in a node for the NPC to offer to sell the key. Make the text:


"Of course I will sell you the key, mortal. It shall cost you a mere gold coins."


Then, on the "ActionsTaken" tab, put in the following script:


// Return the total gold of all PCs in the same party as oPC.
// It will return 0 if the input is not a player character.
int GetPartyGold(object oPC)
{
  if (!GetIsPC(oPC))  // if not a PC, return 0
    return 0;
  else  // loop to get the gold
  {
    object oCharacter = GetFirstPC();
    int nGoldCount = 0;
    while (oCharacter != OBJECT_INVALID)
    {
      if (GetFactionEqual(oPC, oCharacter))  // different parties have different factions
        nGoldCount = nGoldCount + GetGold(oCharacter);
      oCharacter = GetNextPC();
    }
    return nGoldCount;
  }
}
 
void main()
{
  object oTalker = GetPCSpeaker();  // Get the speaking PC
  int nPrice = GetPartyGold(oTalker) + 1;  // Set the price to 1 more than party gold
  SetCustomToken(1000,IntToString(nPrice));  // Set the token for the conversation
}

The SetCustomToken is an interesting function... it is only used inside a conversation. Basically, it allows you to stick any kind of text into a conversation. Note in the text of the conversation, we put . When the conversation is "spoken", that will be taken out and replaced with whatever we have set as custom token number 1000.


Thus, that last line of the script is saying to convert the price to a string, and store that price into token 1000.


A note about custom tokens: It is best if you always set the custom token where you are going to use it. While it is possible to set it and refer back to it later, this is generally considered bad form. (Especially when dealing with erfs.) Further, note that tokens 0 through 9 are used by BioWare, and attempting to use them in your conversation can result in unexpected behavior, so it is best to avoid those numbers.


Default Inputs


Here is a very generic little function that is quite handy in many situations.


// Flip a lever (or some other placeables) between two positions.
// The STATE variable on the lever is set to 1 the first time
// the lever is used, and 0 when used again.
//
// Written by Celowin
// Last Updated: 7/23/02
//
void FlipSwitch(object oLever = OBJECT_SELF)
{
  // STATE will be 0 for off, 1 for on
  int nUsed = GetLocalInt(oLever, "STATE");
  if (nUsed == 0)
  {
    AssignCommand(oLever, ActionPlayAnimation(ANIMATION_PLACEABLE_DEACTIVATE));
    SetLocalInt(oLever, "STATE", 1);
  }
  else
  {
    AssignCommand(oLever, ActionPlayAnimation(ANIMATION_PLACEABLE_ACTIVATE));
    SetLocalInt(oLever, "STATE", 0);
  }
}

Basically, all this does is takes in as input a placeable (like a lever), and causes it to flip between two states... on and off.


Nothing in this script is all that exciting, but there is a new concept inside the declaration.


void FlipSwitch(object oLever = OBJECT_SELF)


What is the " = OBJECT_SELF" doing there? Basically, it is saying that if no input is given to the function, it is going to assume that the input is OBJECT_SELF.


So, if we have a script on a lever, we could just put the command:


FlipSwitch();


into the script, and it would to the animation and change the state of the lever which the script was attached to.


If, on the other hand, we wanted the switch to flip under some other circumstance (say when a chest was opened), we could put into the script something like:


oRemoteLever = GetObjectByTag("Lever6");


FlipSwitch(oRemoteLever);


You never have to declare this kind of default input, and in fact I would be kind of careful about when I would do so. However, under the right circumstances, it adds another layer of versatility to what you can do with functions.


Another Example


Here's another example of default inputs. I like to add a lot of horror and mystery to my adventures. Most of the time, I go for subtlety, keeping the players guessing as to what is really going on. However, sometimes nothing can beat a good dose of old fashioned gore. Hence, I use the following function once in awhile:


// Kill a creature in an explosion of gore.
//
// oVictim is the target of the effect
// If bAffectPlot is TRUE, then the script will work on
// creatures with the plot flag set.
//
// Written by Celowin
// Last Updated: 7/23/02
//
void BloodExplode(object oVictim = OBJECT_SELF, int bAffectPlot = FALSE)
{
  if ((!GetPlotFlag(oVictim) || bAffectPlot) && (GetObjectType(oVictim) == OBJECT_TYPE_CREATURE))
  // If the victim doesn't have the plot flag, it works
  // If bAffectPlot is TRUE, it works
  // Only works on creatures
  {
    // Create the effects:  the explosion, and the death
    effect eBloodShower = EffectVisualEffect(VFX_COM_CHUNK_RED_LARGE);
    effect eDeath = EffectDeath();
    // Make sure the victim can be killed
    SetPlotFlag(oVictim, FALSE);
    // Apply the effects
    ApplyEffectToObject(DURATION_TYPE_INSTANT, eDeath, oVictim);
    ApplyEffectAtLocation(DURATION_TYPE_INSTANT, eBloodShower, GetLocation(oVictim));
  }
}

In this function, we have two inputs: the creature we want to explode (defaults to OBJECT_SELF) and a true/false input that says whether we wish it to affect creatures with the plot flag set (defaults to FALSE).


So, any one of these declarations would be exactly the same if used in a script:


BloodExplode();


BloodExplode(OBJECT_SELF);


BloodExplode(OBJECT_SELF, FALSE);


Note that we can leave off as many variables from the end as we want, but we can't leave off an input from the front if we're going to change one of the later ones. That sentence is more confusing than the concept is, so let me try to give an example.


Suppose we want to have a zombie explode when it is hit, regardless of whether the plot flag is set. So, the first input would be OBJECT_SELF (the default) but the second input would be TRUE (non-default). There is no way to actually make use of the default for the first input, since we're changing the second. The only way to declare this would be


BloodExplode(OBJECT_SELF, TRUE);


Something like:


BloodExplode(,TRUE);


is not valid syntax.


The other thing from this function that I should probably talk about is the conditional, the line:


if ( (!GetPlotFlag(oVictim) || bAffectPlot) && (GetObjectType(oVictim) == OBJECT_TYPE_CREATURE) )


Unless you are really familiar with this stuff, you probably got to this line, scratched your head for a bit, looked at it again, and then gave up on figuring it out. Again, it is sort of a matter of style... I loathe nested ifs. Sometimes they are needed, but if you go overboard on them, it makes your code a nightmare to trace through. Perhaps it is the fact that I have a mathematical background rather than a programming one, but the statement I just wrote is far easier for me to follow than a bunch of ifs inside each other.


So, let's break it down. When do we want the script to actually work?


It will only affect creatures.


It will affect creatures without the plot flag set.


If a creature has the plot flag set, it will still affect it if bAffectPlot is TRUE.


The first one of these is immutable. It has to be a creature, otherwise nothing else matters. In other words, it has to be a creature AND other conditions need to be true. The && stands for logical "and", so the script is checking that the object type of the input is creature, and also something else.


That leaves us with the part of it that reads:


(!GetPlotFlag(oVictim) || bAffectPlot)


The || is "or". If either side of it is true, then this part of the statement is true. bAffectPlot is the input we gave... TRUE or FALSE. If it is true, we don't have anything more to worry about. The !GetPlotFlag(oVictim) is saying that if oVictim does not have the plot flag, it will be true. (The ! in front sort of "flips" what we are looking for. Without the !, it would be true if oVictim did have the plot flag.)


So, putting it all together.... it has to be a creature, AND either that creature doesn't have the plot flag OR the function is set to affect things with the plot flag. Confusing, but if you learn to use the logical operators, your code will be much cleaner.


Function Libraries


Over and over again, to use our functions we have had to put them into the script where we were going to use them. While they can still be useful, this is kind of limiting.


In particular, both the FlipSwitch and BloodExplode are functions that you might want to use over and over again, in all sorts of places in the module. (Well, maybe most people wouldn't use the BloodExplode as much as I do, but anyway...) Sure, we can cut and paste again and again, but there is a better way.


Before I begin, just a general note: I only put things into a library like this if I'm going to be using them in many different scripts. If I'm only going to use a function in one place, I won't worry about it at all. If I'm going to be using that function in two or three scripts, I'll probably just cut and paste it to where I'm going to need it. It is only the ones that I use repeatedly that I end up putting into a library.


Let's go ahead and set up a simple library to use in our module. Edit a new script, call it something like "tm_myfunctions". Cut and paste both the FlipSwitch function and the BloodExplode function into it. Save it.


When you try to save, it will pop up with something like NO FUNCTION MAIN() IN SCRIPT. Just ignore it.


Now for the cool part. Now, in any script we write, we can put at the top the line


#include "tm_myfunctions"


And we can use any function we've put into that file!


So, as a simple example, suppose I want to pull a lever and make the nearest zombie explode. (I'll give the zombie the tag ZOMBOMB, and the lever the tag ZOMBLEV.) The script is then simplicity in itself:


// tm_zomblev_ou
// Destroy the nearest zombie with tag ZOMBOMB to the lever.
//
// Written by Celowin
// Last Updated: 7/23/02
//
#include "tm_myfunctions"
void main()
{
  FlipSwitch();
  if (GetLocalInt(OBJECT_SELF, "STATE") == 1)
  {
    object oZomb = GetNearestObjectByTag("ZOMBOMB");
    BloodExplode(oZomb);
  }
}

Basically, all the #include does is take the text of the file and stick it in place of itself. In essence, it does the "cut and paste" for you.


Warnings about Libraries


As amazingly useful as libraries are, there are a few things you have to be very careful about.


First, I'm going to reiterate what I said before: only put functions that you are going to use and reuse into your libraries. The larger your libraries get, the longer they take to compile. In a big module, this gets annoying very fast.


Even if you're only putting in repeatedly used functions, you might find a library getting large and clunky. If so, consider breaking it down into smaller libraries: one for creatures, one for placeables, one for doors... or however else you want to divide it. It is up to you, whatever helps you to know what library to use for the function you want.


Along with that, also realize that since the include statement is just sticking in text, there is no reason why you have to restrict yourself one include statement in your script.


#include "tm_creaturelib"


#include "tm_doorlib"


would be perfectly acceptable.


Finally, a very important point that many non-programmers forget about... if you modify your library, you should recompile any script that includes that library. This could get ugly, since it is very easy to lose track of what scripts call your libraries. Luckily, there is a way around it.


In the toolset, there is a "Build" button. This basically says "recompile all of the scripts for this module." Hence, if you ever modify a library, you should immediately hit that Build button to make sure everything "checks out." Again, you will get error messages that the libraries themselves have no void main() in them... but as long as that is the only error message you get, you're set.


Onward


This lesson has been huge and has a lot of little concepts. I want to stress for the people reading that if you don't understand everything in the lesson, you're probably not hurting yourself too badly. While the things here can certainly make your scripting lives easier, none of them are strictly needed. Definitely attempt to learn it, and ask questions if you're having problems, but if you feel you're in over your head it isn't the end of the world.


I think my next lesson is going to be on henchmen, if I can figure it out. I really don't think there is a lot of actual scripting that has to be done to set up a henchman, but I'm sick of seeing 50 questions a day on the subject.


After that, I'm going to change the format of the lessons. You really have almost all the general syntax needed for scripting at this point, so there really isn't a whole lot more I can do in that regard. Instead, I'm going to focus more on the "scripting process."


I'll start with an idea, talk about design considerations, limiting the scope, and implementation. (I'll probably go light on the debugging phase, because the forum moderators wouldn't appreciate the amount of profanity that is involved in my debugging methods.)





 author: Celowin, editor: Charles Feduke