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
|
|
||
Screenshots |
|||
author: Robert Straughan, editors: Charles Feduke, Mistress, contributor: Ken Cotterill