Robert Straughan - Capture Glory: Light Show

The Final Battle


To make the final battle interesting, I've decided to add something more than just a straight out fight. Sometimes a simple time limit on the battle will do, but for this, I've decided the player must solve a simple puzzle.


First, when the player gets to the temple, he will or will not have Alluen with him (most likely he will, but I have to account for both possibilties). So, I place a trigger around the temple, and when the player enters, the following script fires:


//Script "out_templestart"
void main()
{
     object oEnter = GetEnteringObject();
     object oElf = GetObjectByTag("HENCH_ELF");
     if (GetIsPC(oEnter) && GetLocalInt(OBJECT_SELF, "ENTERED") == 0)
     {
          SetLocalInt(OBJECT_SELF, "ENTERED", 1);
          if (GetIsObjectValid(oElf) && GetMaster(oElf) == oEnter)
          {
               RemoveHenchman(GetMaster(oElf), oElf);
               DelayCommand(0.1f, AssignCommand(oElf, 
                    ClearAllActions())
               );
               
               DelayCommand(0.2f, 
                    AssignCommand(oElf, 
                         ActionMoveToObject(GetObjectByTag("EVIL_ALTAR"), 
                              TRUE
                         )
                    )
               );
          }
          if (!GetIsObjectValid(oElf))
          {
               object oSpawn = GetObjectByTag("SPAWN_AID");
               object oGuard = CreateObject(OBJECT_TYPE_CREATURE, 
                    "soldier", 
                    GetLocation(oSpawn)
               );
          
               AssignCommand(oGuard, ActionMoveToObject(oEnter, TRUE));
               AddHenchman(oEnter, oGuard);
          }
     }
}

First, the script gets the entering object and Alluen. Then, provided this hasn't been done before and the object is the player, it has two possible options.


First, if Alluen is the player's henchman, then it removes Alluen from their party, and tells her to drop what she's doing, and run for the altar. Remember that using Action commands stacks the queue, so you need to clear the object's queue if you want the action to be immediate.


The second option is if Alluen is not valid. If this is the case, it gets the waypoint I have indicated, and creates a creature there. Because I have actually declared to a variable name, I can assign commands to it immediately.


Notice the third option? There is a third option, but it requires that you understand the process this script is going through. What if Alluen exists, but is not the player's henchman? Then Alluen will not do anything, and the player won't get a henchman.


Still can't see it? Think about it, I've checked for two possible options, but this third option could still happen. Since I've done nothing to prevent it, that's what will happen.


So, I've given Alluen a conversation option which won't appear unless the following StartingConditional returns true:


// Script "out_hench2"
int StartingConditional()
{
    int iResult;
iResult = GetDistanceBetween (GetObjectByTag ("EVIL_ALTAR"), OBJECT_SELF) <= 2.5f;
return iResult; }

Like all other StartingConditionals, this boils down to a check for whether something is true or not. In this case, it gets the distance between the NPC and the altar, and checks to see if it's less than 2.5 metres. If so, then the line will be stated.


What happens next is that the player must remove all the gems from the altar. To check this, since the altar is a container, I've used the following script in the OnClosed handler:


//Script "out_altarclose"
void StartThar(object oWP, object oPC)
{
     object oAlluen = GetObjectByTag("HENCH_ELF");
     object oThar = CreateObject(OBJECT_TYPE_CREATURE, 
          "thar001", 
          GetLocation(oWP)
     );
    
     if (GetIsObjectValid(oAlluen))
          AdjustReputation(oThar, oAlluen, 100);
          
     AssignCommand (oThar, ActionStartConversation(oPC));
}

void main()
{
     object oInv = GetFirstItemInInventory();
     object oPC = GetLastClosedBy();
     object oWP;
     while (GetIsObjectValid(oInv))
     {
          string sTag = GetTag(oInv);
          string sLeft = GetStringLeft(sTag, 4);
          if (sLeft == "GEM_")
               return;
          oInv = GetNextItemInInventory();
     }
     if (GetLocalInt(OBJECT_SELF, "FIRED") == 0)
     {
          SetLocalInt(OBJECT_SELF, "FIRED", 1);
          int nNth;
          effect eLightning = EffectVisualEffect(VFX_IMP_LIGHTNING_M);
          ApplyEffectAtLocation(DURATION_TYPE_TEMPORARY, 
               eLightning, 
               GetLocation(OBJECT_SELF), 
               15.0f
          );
          
          for (nNth = 1; nNth <= 5; nNth++)
          {
               oWP = GetObjectByTag("WP_" + IntToString(nNth));
               string sResRef = "invisobj00" + IntToString(nNth); 
               object oNode = CreateObject(OBJECT_TYPE_PLACEABLE, 
                    sResRef, 
                    GetLocation(oWP)
               );
                
               effect eNode = EffectBeam(VFX_BEAM_LIGHTNING, 
                    oNode,
                    BODY_NODE_CHEST
               );
            
               DelayCommand(0.5f, 
                    ApplyEffectAtLocation(DURATION_TYPE_INSTANT,
                         eLightning, 
                         GetLocation(oWP)
                    )
               );
                
               DelayCommand(1.0f, 
                    ApplyEffectToObject(DURATION_TYPE_PERMANENT, 
                         eNode, 
                         OBJECT_SELF
                    )
               );
               
          }
          oWP = GetObjectByTag("TEMPLE_SPAWN");
          DelayCommand(1.0f, 
               ApplyEffectAtLocation(DURATION_TYPE_INSTANT, 
                    EffectVisualEffect(VFX_FNF_SUMMON_CELESTIAL), 
                    GetLocation(oWP)
               )
          );
          
        DelayCommand(5.0f, StartThar(oWP, oPC));
    }
}

Complicated? Nope, just long winded. Walk through it, and you'll see I'm doing nothing new here.


First of all, we start out with a custom function called StartThar. Remember, I have to do this before the void main(), or else when I use it in the main script, it won't know what I'm talking about.


This function is void, so it returns nothing to the script, and it has two parameters. Using those parameters, we find Alluen, create Thar at the location of one of those parameters, make Alluen friendly to Thar, and then have Thar speak to the object given in the other parameter.


Why make Alluen friendly to Thar? Well, I need Thar to talk to the player, and don't want her to attack him and interrupt. But her faction isn't hostile to Thar's faction? Yes it is, because we altered the way her faction feels about Thar's faction when we had her attack Granthos.


Don't worry if you don't think of this sort of thing when actually writing the script, a lot of the bugs in a script are found when you play through the module, and find things not happening the way they should.


So what does this script do? Okay, first we get the first item in the inventory of the container this script is called by. Then we get the object which just closed this container.


Notice that we also tell the script there's an object variable called oWP, but we haven't initialised it. Which means its equal to OBJECT_INVALID at the moment. This is because we're going to set it to a new value later on.


We've next got a while loop that does a GetFirst/Next loop through the items in the container's inventory. It gets the tag, and then the first four characters of that tag from the left end. If those characters are equal to GEM_, then we quit the script.


Just to make sure you understand the concept of return, it only quits the script here beacuse its being used in the void main() script. If I placed a return in the custom function StartThar, it would actually quit back to the void Main() script where it left off.


This is why void scripts are void. Because they don't return anything, they just go back to where they were. Earlier, in the StartingConditional scripts, I had to return a value, because the function they were being used in was an int returner.


Okay, moving on, this means that the rest of the script won't even get touched if the script finds one of the gems still in the container. Had I used the OnOpen handler, the player would have had to open the container again after removing the gems before this would have fired.


If it manages to get past the while loop, it means there are none of the gems left in the container, and the next part can fire. Because I'm using an if statement, it means I can prevent the rest happening more than once.


Notice that I declare a visual effect. This is because I wish to use the same effect in multiple places throughout the rest fo this script, and it saves me some typing to declare it once.


So, even though I've used a DURATION_TYPE_TEMPORARY in the apply effect, a lightning bolt will strike the alter only once, because its not a visual effect designed to be run for a duration of time (actually, it appears to do so, I've targetted the location of the altar, not the altar itself).


From the for statement, we can tell that whatever comes next will occur 5 times. We also set oWP to a value. This is an example of when you would want to use string manipulation to target specific waypoints. I'm using the numbers on the end of several objects to identify which colour light is where.


I create the energy node for the currently targetted waypoint (it's an invisible object with a container property) at the location of oWP (notice I've again used nNth to determine which one to make).


The next thing is an application of logic. The beam effect requires that you specify the emitter, which I have set to the node which I just created. I then fire the lightning effect at the location of oWP, and have the beam effect target the altar.


What's the end result of this for loop? Where the five waypoints are, we place an invisible object with a container, and fire a lightning bolt at it. A moment after the lightning strikes, the bolt appears to shoot from the node to the altar (it's not the same bolt, it's a different effect altogether, but by delaying it, that's what it appears as).


The last bit shows why I declared oWP outside this section. If you declare a variable inside a conditional statement, it cannot be used outside the curly brackets, because there's a chance the variable won't get declared if the conditional locks it out (and the compiler won't risk this).


So, I now change the target to be another waypoint, and apply a new visual effect to its location. I then run the custom function I created at the beginning of the script.


Now, an important thing to bare in mind. Why did I not just place the script in the custom function at the end of this script? Note that the CreateObject function returns an object variable. In the past, you've seen that you can use this function either to declare an object or use it as a void function.


Unfortunately, the DelayCommand function won't differentiate like this, and since it requires the use of a void function, we cannot use an object returner in a DelayCommand. The way to delay a Create command is to place it within a void returner, that way the DelayCommand works.


So far, we've gotten a whole bunch of special effects to occur, and for some lightning beams to be set up running from the node objects to the altar. We've also created Thar and made sure that Alluen won't attack him.


Thar's conversation ends with the following script:


//Script "out_bosscon1"
#include "nw_i0_generic"
void main()
{
     object oHench = GetHenchman(
          GetNearestCreature(CREATURE_TYPE_PLAYER_CHAR, 
               PLAYER_CHAR_IS_PC
          )
     );
     
     object oThar = OBJECT_SELF;
     if (GetIsObjectValid(oHench))
     {
          AdjustReputation(oThar, oHench, -100);
          AdjustReputation(oHench, oThar, -100);
          AssignCommand(oHench, ActionAttack(oThar));
     }
    ChangeToStandardFaction(oThar, STANDARD_FACTION_HOSTILE);
    AssignCommand(oHench, DetermineCombatRound());
}

Like earlier, this script will cause the player's henchman to go hostile on Thar. Nothing here is anything I haven't done before, and it should all be making sense to you now.


We get the henchman of the nearest player, and we declare OBJECT_SELF so that we can properly refer to the caller in AssignCommand functions.


If the henchman is valid we run some functions to make them hostile towards each other, and then have the henchman attack Thar. Regardless of if this happens though, Thar will become hostile and attack anyone.


We end this final battle with the following script, which is placed in the OnClose handler of the nodes which we created (notice I didn't just place them down in the editor, this was for realism, since I didn't want the player clicking on them before they had the gems).


//Script "out_nodeclose"
void main()
{
     object oMod = GetModule();
     object oInv = GetFirstItemInInventory();
     string sInv = GetTag(oInv);
     string sTag = GetTag(OBJECT_SELF);
     string sCheck = GetStringRight(sInv, 1);
     string sSelf = GetStringRight(sTag, 1);
     if (!GetIsObjectValid(oInv) || 
          GetStringLeft(sInv, 4) != "GEM_" ||
          GetIsObjectValid(GetNextItemInInventory()) ||
          sCheck != sSelf
     )
          return; // if true
          
     int nCount = GetLocalInt(oMod, "NODE_COUNT");
     nCount++;
     SetLocalInt(oMod, "NODE_COUNT", nCount);
     if (nCount >= 5)
     {
          object oThar = GetObjectByTag("BOSS_THAR");
          
          SetPlotFlag(oThar, FALSE);
          ApplyEffectAtLocation(DURATION_TYPE_INSTANT, 
               EffectVisualEffect(VFX_IMP_LIGHTNING_M), 
               GetLocation(oThar)
          );
                
          AssignCommand(oThar, SpeakString("Nooooo!"));
                
          ApplyEffectToObject(DURATION_TYPE_INSTANT, 
               EffectDeath(), 
               oThar
          );
     }
    DestroyObject(GetObjectByTag("SHAFT_" + sSelf));
    DestroyObject(oInv);
    DestroyObject(OBJECT_SELF);
}

This script gets the module, and the first item in the node. It gets the tag of the item, and the tag of the node. I then get the last character of the item's and the node's tags.


Note the if statement. Although there is only one line afterwards, a return command, there are four conditions that must be met for that command to happen.


If the item is not valid (exclamation mark, so its inverted), or if the item's tag doesn't begin with GEM_, or if there is more than one item in the container, or if the last character of the item's and node's tags do not match, then this script will stop running.


Now, the next three lines are a common sight in many scripts. This is a stored counter. We get the value of a local variable, we then increment it, and set the exact same local variable to the new value. This allows us to keep track of things outside of the script.


Now, if that counter has reached 5 or more, then we get Thar, remove his plot flag status, create a lightning effect at his location, tell him to shout, and then kill him.


Regardless of if the counter has reached 5 or more, this script will then destroy the appropriate shaft of light (see why I was using numbers to match these up?), the gemstone itself, and the node.


What's the effect? When a player puts the right gemstone into the right node, the node will vanish (which incidently also removes the beam effect, since I applied them from the node) and so will the light. When they're all gone, Thar will be killed.


Well, this may have taken a bit of effort to script, but it adds a new dimension to the experience beyond just fighting (in fact, throughout this module, it's possible the player didn't have to attack anything!).


To wrap the module up, I've placed the following into the OnDeath script for Thar:


//Script "out_questboss"
void main()
{
     object oPC = GetNearestCreature(CREATURE_TYPE_PLAYER_CHAR, 
          PLAYER_CHAR_IS_PC
     );
     
     AddJournalQuestEntry ("NPCThar", 40, oPC);
     object oWP = GetObjectByTag("SPAWN_AID");
     object oHarris = CreateObject(OBJECT_TYPE_CREATURE, 
          "dand", 
          GetLocation(oWP)
     );
     
     AssignCommand(oHarris, ActionStartConversation(oPC));
}

The first line will actually happen since there's about a second before the object that fires an OnDeath script actually becomes invalid. It's important to bare that in mind when dealing with OnDeath, because any number of problems with scripting OnDeath scripts can occur.


We advance the player's journal, and then create a new NPC at the waypoint specified. We then tell this NPC to go and speak to the player. This way, as soon as Thar dies, it'll only be about a second or so before the NPC comes running in.


The conversation is pretty basic and has two scripts:


//Script "out_gold"
void main()
{
     GiveGoldToCreature(GetPCSpeaker(), 1000);
}
//Script "out_endgame"
void main()
{
     DelayCommand(10.0f, EndGame(""));
}

The first one will give some gold to the player. The second one will actually cause the module to end, and the credits video to run. This can be useful if you want to definitively let the player know the module is over.



Hurrah!


That's it! The end! In this last section, you learned how to use while and for loops to reduce the amount of scripting you needed to do in order to get a reasonably complex puzzle going for the final battle.


If you feel that this last page went too fast, just read back through the scripts (not the descriptions), and use your basic knowledge of scripting to work out what they are doing.



Tasks


Write a script that will fire every 60 seconds, and will reset any of the nodes which have already been deactivated (thus meaning the player must place the gems correctly within one minute or do it all again). Put the gems that have been destroyed back into the altar.


Erm, free doughnut for the writer from each person who reads this?



Screenshots





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