Robert Straughan - Capture Glory: Raging Battle

Onwards


Now, I don't want the player leaving the caves until they've spoken to Alluen. To do this, I open up the properties for the exit, and click on edit for the OnAreaTransition.


You'll notice the script editor pre-generates some script for you. I've altered this to the following:


//Script "cave_exit"
void main()
{
     object oAlluen = GetObjectByTag("HENCH_ELF");
     object oClicker = GetClickingObject();
     object oTarget = GetTransitionTarget(OBJECT_SELF);
     location lLoc = GetLocation(oTarget);

     if (!GetIsObjectValid(oAlluen) || 
          GetMaster(oAlluen) != oClicker)
     {
          SendMessageToPC(oClicker, 
               "You must have Alluen in your party to continue."
          );
          
         return;
     }
     AssignCommand(oClicker, JumpToLocation(lLoc));
}

I've added the declaration of Alluen, and then I have a conditional statement which checks two things. The last section probably looks familiar to you, as I've done it before (!= is NOT EQUAL TO, as opposed to ==).


That should clue you to what the ! in front of the GetIsObjectValid function does. You can place an exclamation mark before a value to logically invert it.


The majority of GetIs functions return TRUE or FALSE. By placing the exclamation mark before a function (or even before the values themselves) you invert their value. So GetIsObjectValid would return TRUE if Alluen exists, but I've changed it to equal FALSE.


This means that the function is effectively GetIsObjectNOTValid. If you get at all confused about whether to use the exclamation mark, remember that if statements only run their lines of code of the conditional returns TRUE.


There is another symbol here that I haven't used before, and thats the || symbol (it's two | marks. On UK keyboards, this is in the bottom left). This denotes logical OR, which means if one or other of the conditionals returns TRUE, then the if statement will fire its code.


In this case, if Alluen is not a valid object, or if she is not the henchman to the player attempting to pass through the door, then the message function will fire, and the return command will exit the script, preventing the jump to the exit transition target.


Notice that the use of OR means that as soon as Alluen is a valid object, the player may pass through the door. The last check is not actually of any use. If I wanted to prevent the player from passing through until they got Alluen as a henchman, then I'd make the || symbol an && one (or alternatively, I could just remove the first part altogether).


For the conversation with Alluen, I've entirely used the same scripts as Kilinar had on him. This is always something to bare in mind, if you can use the same script in multiple places, do so. It saves cluttering up your script library.



Into the Flames!


Right, now, outside I want their to be a battle going on, since Alluen has mentioned one. I could script this in a number of ways, but I've gone for simplicity.


I've created some orcs and some guards, and two new factions. I've set the factions to be hostile to one another, but neutral to everybody else. This way, they will fight each other, but leave the player alone.


To set up the battle so that it starts when the player gets outside, I've not placed any of the creatures at all into the module. Instead, we'll create them when the time is right.


//Script "out_oe"
void main()
{
     object oEnter = GetEnteringObject();
     if (GetIsPC(oEnter) && GetLocalInt(OBJECT_SELF, "FIRED") == 0)
     {
          SetLocalInt(OBJECT_SELF, "FIRED", 1);
          int nNth;
          object oWP = GetObjectByTag("SPAWN_FIGHT", nNth);
          while (GetIsObjectValid(oWP))
          {
               string sResRef = "guard";
             
               if (d2() == 1)
                    sResRef = "orc";
                    
               object oCreate = CreateObject(OBJECT_TYPE_CREATURE, 
                    sResRef, 
                    GetLocation(oWP)
               );
               
               nNth++;
               oWP = GetObjectByTag("SPAWN_FIGHT", nNth);
          }
          AddJournalQuestEntry("HenchAlluen", 20, oEnter);
     }
     if (GetTag(oEnter) == "HENCH_ELF")
          AssignCommand(oEnter, 
               SpeakString(
                    "Quickly! We must find Thar! Run for it!"
               )
          );
}

There's a new trick to learn here. This script is fired from the OnEnter handler for the area, so as soon as any object enters (not just PCs) this script will fire. Most of this should look familiar.


If the entering object is a player (as I said, OnEnter fires every time an object enters or spawns), and this has not been done before, then we get an object with the appropriate tag.


Note that I've filled in the usually pre-declared parameter for the GetObjectByTag function. nNth hasn't been initialised, so it will start at zero. The purpose of the integer value in the last parameter is to get the object with the tag that many times.


It's like a cycle. Imagine I have several LEGO bricks to choose from, and they are all of different colours. If I want a specific red one, I can't just say "Pick up the red one." because there are several red ones. I have to say "Pick up the third red brick from the top."


So, by giving GetObjectByTag a value, it will cycle through all the objects with the tag that many times, and return the selected object to the script. This is important, because if you are using GetObjectByTag, and have multiple objects with the same tag, you don't know that the script has gotten the one you want to use.


But here, I'm not using this because I want to be certain of which waypoint I get. If you look at the while loop that follows, you'll notice that it continues to run its code as long as the object I've gotten is valid.


But that's going to result in a continuous loop unless I change the object. Look at the end of the while loop. Notice that I've incremented nNth by one, and then gotten the object with the same tag but of the newly incremented amount.


This setup is like having a GetFirst/NextObjectByTag set of functions. It allows me to cycle through every waypoint with that tag until there are no more (since it would get to a value higher than the number of waypoints available, and return an OBJECT_INVALID).


So what does the loop do? Well, as it gets each waypoint, it declares and initialises a string variable. It then rolls a d2, and on a 1, changes that variable to something else.


It then uses CreateObject to make a creature with the resref (or tag, but the string variable refers to a resref in this case, read the box if this doesn't make sense). It does so at the location of the current waypoint object declared as oWP.


Now you can see how easy it can be to do mass processes. When scripting, if you find you are using the same function over and over, just as with deciding when to declare a function or to just use it straight off, you should try to refine your script to use those functions the least possible number of times.


If the battle has been started, the last thing we do is advance the journal for the player. Notice the bottom two lines. If the entering object turns out to be Alluen, we get her to say some lines. This was to encourage the player to do the correct thing.



Fe-Fi-Fo-Fum


As a nice little touch, and to add a level of atmosphere to the battle, when the player runs down into the gulley, I'd like to have a bunch of soldiers running away from a giant which is chasing them.


To do this, I first place a bunch of waypoints where I'm going to spawn the creatures involved. I then place a trigger where the player would be just in the right place to witness the chase. Finally, I add the following script to the trigger:


//Script "out_runnerstart"
void main()
{
     int nNth;
     object oDest = GetObjectByTag("RUNNER_DEST");
     for (nNth = 1; nNth <= 4; nNth++)
     {
          object oWP = GetObjectByTag("RUNNER_" 
               + IntToString(nNth)
          );
          
          object oRunner = CreateObject(OBJECT_TYPE_CREATURE, 
               "runningguard", 
               GetLocation(oWP)
          );
          
          AssignCommand(oRunner, ActionMoveToObject(oDest, TRUE));
     }
     object oGiant = CreateObject(OBJECT_TYPE_CREATURE, 
          "gntmount001", 
          GetLocation(GetObjectByTag("GIANT_SPAWNER"))
     );
     
    AssignCommand(oGiant, ActionMoveToObject(oDest, TRUE));
    DestroyObject(OBJECT_SELF);
}

Notice that I use DestroyObject at the end of this to prevent the script from firing more than once, instead of a locally stored value. This is technically a bad move, since I haven't put any of this within an if statement.


Why an if statement? Consider the line: if (GetIsPC (GetEnteringObject())). I haven't placed a check to ensure the object triggering this script is a player. What if one of the guards or orcs walks into the trigger? What if the player's henchman does?


These are the sorts of things that a scripter has to take into account. Just like with GetIsPC/DM checks, you have to try and account for every possible outcome or possibility when writing a script, or else something might happen how you didn't intend.


So, what about the rest of the script? Well, we get an object by a given tag. We then use a for loop which runs four times. We know this, since nNth starts at 1, and then increases by 1 every time the loop completes, until nNth is 5 or more, when it will stop.


The loop runs through a set of waypoints. Notice how I haven't used the hidden parameter this time. Instead, I've actually tagged the waypoints with a number, and added the nNth value onto the end of the tag I'm getting.


Also notice that I've placed the nNth within a function called IntToString. This is because I am doing a mathematical process with two different variable types. RUNNER_ is a string, but nNth is an integer. I would get an error if I tried to do "RUNNER_" + nNth.


In this example, it isn't actually necessary for me to use individual tags for the waypoints, I could have done what I did earlier and used the hidden parameter on the end of GetObjectByTag.


It's useful to note that you can do this though, since you may wish to target objects in a specific order. Using the tag means you know exactly which object the script is targetting, whereas using the parameter, you don't actually know which one is which.


We then create an object of value runningguard at the location of the currently targetted waypoint. We also tell them to run towards the waypoint I declared at the beginning of the script (the TRUE/FALSE parameter in MoveToObject states whether to run or not).


Finally, once all four guards have been created, I then make a giant at its own waypoint, and tell it to run towards the same point also. Note that I didn't actually tell it to go any slower, I altered the movement speed in the creature editor instead.


There are two things I now need to consider. One, what if the player somehow stops them, or if they get stuck on a piece of terrain. To solve this, I can use the OnHeartbeat handler for these creatures:


//Script "out_runner1"
void main()
{

    object oRunTo = GetObjectByTag ("RUNNER_DEST");

    if (GetTag (OBJECT_SELF) == "RUNNER")
         if (d2() == 1)
               SpeakString ("Aargh! Runaway!");

    if (GetDistanceToObject (oRunTo) >= 1.0f)
         ActionMoveToObject (oRunTo, TRUE);

}

This script will fire for each creature I place this on every 6 seconds. The bottom two lines means that if the creature is still too far away from the target that they are moving to, then they will be told to move towards it again (avoids problems with the NPC stopping).


The first three lines have two interesting points. First is the fact that I actually check the tag of OBJECT_SELF. This means that I can place this same script in multiple creatures' OnHeartbeat handler, but the following lines won't occur for anything but the one with the tag I've looked for.


Also note that I've got two if statements one after the other. Remember that you do not need to place the curly brackets after an if statement if only one line follows it. This makes the script easier to read.


You must be careful with this sort of placement though. Look at the following script:


//Example script
void main()
{
     if (nNth == 1)
     {
          nNth = nNth + 10;
     }
}

This is the same as this:


//Example script
void main()
{
     if (nNth == 1)
     nNth = nNth + 10;
}

But, what about the following:


//Example script
void main()
{
     if (nNth == 1)
          nNth = nNth + 10;
     if (nNth == 2)
          nNth = nNth - 10;
     if (nNth == 1)
     { 
          nNth = nNth + 10;
          if (nNth == 2)
          {
               nNth = nNth - 10;
          }
     }
} 

Now, you've got two sets of if statements that do the same thing, right? Wrong. The bottom set will only run the if (nNth == 2) line if the first if statement was true. Whereas the top set will run both if statements regardless of whether one or the other is true.


Always remember, when skipping out the curly brackets in script, the if statement applies only to the line immediately after it. Any lines beyond the first will run regardless of the if statement's validity.


So what did the script do? If the creature running the script has a tag of RUNNER, and when a d2 is rolled a 1 is the result, then they will speak a line. (Read the box for more on using if statements in OnHeartbeats.)


Finally, my runners are all going to stop when they reach their destination. So I need to destroy them when they get there, and the easiest way to do that is with a trigger.


//Script "out_runner2"
void main()
{
     object oEnter = GetEnteringObject();
     string sTag = GetTag(oEnter);
     string sLeft = GetStringLeft(sTag, 6);
     if (sLeft == "RUNNER")
          DestroyObject(oEnter);
}

This script gets the object entering the trigger (because I'm using the OnEnter handler of the trigger). It then gets the tag of the entering object.


Because the tags for the runners and the giant are different, I can't just check the tags to see if they are the right objects to destroy (I could, but it would make this tutorial a bit incomplete).


So, for this, I do a bit of string manipulation. Using the GetStringLeft function, I can get a number of characters from the left end of a specified string. I choose to use the tag of the entering object (which is a string) and to get the first 6 characters from the left end.


Six characters would be the same length as the word RUNNER, which is how both the guards' and the giant's tags start. So if the entering object is one of the runners, the script will destroy them.



It goes on and on...


You've just learned how to use loops to do mass processes, and to save you from having to type out every single thing you want the script to do. You've also learned how to use the fi statements without brackets a bit more accurately.


Remember, the difference between a beginner and an advanced scripter is not what functions you use or how you use them, but how efficient you are at writing the script.


If you've got this all downpat, head on. If not, try following the scripts on this page through again. Sometimes a lengthy explanation may help, but sometimes simply trying to understand the sequence that a script runs through to achieve its end results will help more.



Tasks


Create an area with a room. In this room, you must have at least six creatures spaced out. Write a script which will have each creature say a different line, before being destroyed with a flashy effect. The script must be as efficient as possible.



Hint


Create the room, and place the six creatures into the room. Make their tags all the same. Create a loop which cycles through each monster, assigns a SpeakString command with a string determined from a random number, and then destroys the current creature.

Syntax


Throughout my scripting, you've noticed that the names I use for declaring variables tend to start with a lower case letter.


This is just a scripting practice, to keep the names of the variables recognisable throughout the code, and making sure we can see what type of variable we are using.


Jump Vs Move


A small point, but one to be aware of. JumpToLocation is a teleport effect, moving an object from one point to the next without passing through the intervening space.


MoveToLocation means the object will actually attempt to walk the distance.


Hail the Badger


Badger syndrome is an early error found by most scripters. It has to do with the CreateObject function.


When creating creatures, you must obey the rule regarding tag vs resref of the object you are creating with regard to which palette it is in.


If the string you give the CreateObject function does not match the tag of any creature in the standard palette, or the resref of any creature in the custom palette, it defaults to the first creature in its listing alphabetically.


Thus the almighty badger is born.


OnHeartbeat


This is the only way scripting can have a timer outside of DelayCommand. The script in any OnHeartbeat handler fires every 6 seconds.


This allows us to not only time events, but also keep track of changing values, such as distance from an object.


There is a major drawback to this though. The script fires every 6 seconds regardless of whether it is necessary or not.


Consider a large battlefield of creatures. Each one has an OnHeartbeat handler. Each one fires a script every 6 seconds. That's a lot of processing the computer has to do.


Most people avoid using OnHeartbeat scripts for the reason that it can slow things down. While most scripts are not processor intensive when run, if you start ending up with a lot of scripts running at the same time, it can have a performance hit.


This is usually why people prefer to build modules using encounters or custom scripted spawnings, since the creature is not in the game until we know it is needed, thus reducing the amount of unnecessary processes being run.


Another common point about those OnHeartbeat scripts that do get written is that they often have a lot of conditional statements in them. This means that none of its scripting will run unless its absolutely certain it has to do so, also reducing the amount of processing a script does.


When writing if statements in OnHeartbeat scripts, always try to avoid using &&. Instead, write out each if statement seperately. If the processing should be done anyway, then you aren't increasing the amount of work it has to do, but if something isn't TRUE, and you use &&, the if statement will still work out all the values first.


Pre/Post-Fix


You may have noticed that in order to increment numbers I have used the notation ++ (-- works for decrement).


x++ is the same as saying x = x + 1. It is also the same as saying x += 1.


There are various operators such as *=, or -=, which essentially do the indicated maths with the values you give. For example, x *= 2 would do x = x * 2.


The ++ notation is what's called a post-fix increment. ++x is the prefix version. Essentially they both do the same thing, however, in some cases it can be important to know which one to use.


The prefix version does the increment as soon as it encounters the notation in the script. The post-fix version does the increment after it has done any calculation in the script.


This is especially important when you include a pre/post-fix increment or decrement directly into a calculation.


For example, x = y + z++ would work out x to be y + z, and then increment z by one. Whereas x = y - ++z would increment z first, and then subtract it from y to get x.



Screenshots





 author: Robert Straughan, editors: Charles Feduke, Mistress, contributor: Ken Cotterill