Robert Straughan - Capture Glory Minotaur Death

From NWN Lexicon
Jump to: navigation, search

Robert Straughan - Capture Glory: Minotaur Death

Granthos


When the player leaves the caves, I want them to meet Granthos the minotaur. Since Granthos is probably too high a level for the player to beat, even though I've made them level 3, I'm going to rig the fight.


(I'd like to just point out that I've actually seen Kilinar win this fight, so it is possible for the player to defeat Granthos by themselves. However, since I wanted to kill Kilinar and bring in Alluen, I decided to go ahead and rig it anyway.)


When the player gets to the exit, I want them to stop and be forced to talk to Granthos. To do this, I use the following script:


//Script "cave_triggerboss"
void main()
{
     object oEnter = GetEnteringObject();
     AssignCommand(oEnter, ClearAllActions());
     SetCommandable(FALSE, oEnter);
     if (GetIsPC(oEnter) && GetLocalInt (OBJECT_SELF, "FIRED") == 0)
     {
          SetLocalInt(OBJECT_SELF, "FIRED", 1);
          object oBoss = CreateObject(OBJECT_TYPE_CREATURE, 
               "minotaur001", 
               GetLocation(GetObjectByTag("DOOR_SPAWN"))
          );
 
          AssignCommand(oBoss, 
               ActionMoveToObject(GetObjectByTag("CAVE_WALK_TO"))
          );
 
          DelayCommand(3.0f, 
             AssignCommand(oBoss, ActionStartConversation (oEnter))
          );
     }
}

If you read through this, you'll notice nothing that I haven't done before. Declare the object that triggered this handler (which is an OnEnter handler for a trigger I've placed so the player must cross it before reaching the exit).


I've assigned a command to that object (since I don't want the caller to do it) that tells it to stop everything it's doing. I've then told it that it can no longer take any actions.


I don't know whether the object involved was a player or not (it might be Kilinar), and I also don't want this to fire more than once. In which case, I have a conditional statement that looks to see if this has been done once, and if the entering object was a player.


Provided the checks are both true (because of the logical AND), I make sure the loop won't happen again, and then create the minotaur. Notice two things, first, I haven't declared or initialized either the location or the waypoint prior to this function, I've just slotted them straight in.


Second, I used the resref of the creature to create it, since it is in the custom palette. Neither of these should be particularly difficult to fathom out at this point, so if you have difficulty understanding that line, try some of the previous tasks again.


Notice also that I have actually declared the newly created object. Rather than using the function as though it were a void function, I've given it a name. This is so I can immediately give it command in this same script.


The next two lines shouldn't be difficult either, using AssignCommand and DelayCommand to tell the minotaur what to do. Notice I've delayed the second command by 3 seconds, to ensure that the actions are stacked in the queue in the correct order.



Go Kilinar! Go!


So, now that I have my boss creature, and the player can't do anything but talk to them, I create a conversation to use. At this stage, the player may or may not have Kilinar with them. If they don't, I get rid of the Kilinar object, to prevent confusion.


//Script "cave_bosscon1"
int StartingConditional()
{
     object oDwarf = GetObjectByTag("HENCH_DWARF");
     if (GetMaster(oDwarf) == GetPCSpeaker())
          return 1;
     DestroyObject(oDwarf);
          return 0;
}

This is a starting conditional statement for a conversation. Notice the order this does things in. We get the dwarf, and if they are the henchman to the speaker, we allow the node to be spoken.


If the statement was not true, the script will continue on, and destroy the dwarf. It will then return zero, meaning the line won't fire. So all I do is attach this to the line that responds assuming the dwarf is in your party, and place it above the one where he isn't.


//Script "cave_bosscon2"
#include "nw_i0_generic"
void main()
{
     object oDwarf = GetObjectByTag("HENCH_DWARF");
     SetCommandable(TRUE, oDwarf);
     RemoveHenchman(GetMaster(oDwarf), oDwarf);
     AdjustReputation(OBJECT_SELF, oDwarf, -100);
     AdjustReputation(oDwarf, OBJECT_SELF, -100);
     AssignCommand(oDwarf, DetermineCombatRound());
     DetermineCombatRound();
}

Nothing new here in terms of the scripting, but there's a nice little trick I've done here. This script is part of the node that fires when the dwarf is found to be in the PC's party. It gets him, and makes him commandable (since he might have made uncommandable by the trigger).


I then remove him from the player's party. I could have specified GetPCSpeaker for the first parameter of the RemoveHenchman function, but instead I used GetMaster. This is a nice line to bare in mind, since it can be used anywhere to remove the target from any party.


We then do a bit of manipulation of reputations. If you've been fiddling with the editor, you will have noticed factions. I've explained them a bit in the box, but otherwise, these functions make the minotaur and the dwarf hostile towards each other.


I've then gone and used a function in the final two lines that you won't find in the function list. So how can I use it? Remember custom functions? Remember how #include works?


The file this script includes, nw_i0_generic, is Bioware's extra function library. If you can't find a function in the list of functions that does what you want, before writing it yourself, check to see if the function already exists in this file.


By #including the file, I now have access to all the functions in that file. Note that any script which #includes this file will take noticeably longer to compile than normal. That's because the file is a very large script.


In this instance, I've used DetermineCombatRound. While most people find it has different uses, purposes or results, I've always found that by using it on an object, it causes that object to determine the best course of action to take in combat.


So, when this conversation ends, the dwarf and the minotaur will have a fight. However, I know the dwarf can win on occasion, so I want to be sure that he dies.


To do this, I've modified his OnPhysicalAttacked script to the following:


//Script "cave_insurance"
void main()
{
     if (GetTag(GetLastAttacker()) == "BOSS_GRANTHOS")
          DelayCommand (3.0f, 
               ApplyEffectToObject(DURATION_TYPE_INSTANT, 
                    EffectDeath(), 
                    OBJECT_SELF
               )
          );
 
/* rest of script */

What does it do? If the last attacker was Granthos, then the dwarf will die. Simple. Note that I could have left this script intact, and used a User Defined Event (read the box).


Notice that I place a DelayCommand on the death effect. This is because the OnPhysicalAttacked handler fires the instant the object is attacked, which wouldn't look too good. By delaying the death by 3 seconds, the attack animation will have time to play out.


I then put into the OnDeath script handler for Kilinar the following:


//Script "cave_diedwarf"
void main()
{
     object oKiller = GetLastHostileActor();
     if (GetTag(oKiller) == "BOSS_GRANTHOS")
     {
          object oPC = GetNearestCreature(CREATURE_TYPE_PLAYER_CHAR, 
               PLAYER_CHAR_IS_PC
          );
          SetLocalInt(oKiller, "DWARF_DIED", 1);
          AssignCommand(oKiller, ClearAllActions());
          AssignCommand(oKiller, ActionStartConversation(oPC));
          AddJournalQuestEntry("HenchKilinar", 40, oPC);
     }
}

What does this do? GetLastHostileActor gets the last person to do anything vaguely hostile towards the caller, such as attack, or cast a spell. I could have used GetLastAttacker, given the handler I'm using, but I've chosen this one (it's more accurate in case the object was killed by means other than being physically attacked).


If the last person to be hostile towards the dwarf turns out to be Granthos, I get hold of the player using the GetNearestCreature function and the right criteria.


The local variable I set is to let the minotaur's conversation know the dwarf has died. Somewhere in the conversation, you'll find a node with the following script:


//Script "cave_bosscon4"
int StartingConditional()
{
     int iResult;
     iResult = GetLocalInt(OBJECT_SELF, "DWARF_DIED") == 1;
     return iResult;
}

This is a standard StartingConditional script which checks against that variable to see if the dwarf has died or not. This is a matter of how I've ordered the conversation nodes in the conversation.


As for the rest of the script, we tell the killer of the dwarf, which we know is Granthos, to stop what he's doing, and talk to the nearest player. In addition, we advance the journal entry for the dwarf to the one where he's dead.


The last script attached to the minotaur's conversation is the following one:


//Script "cave_bosscon3"
void main()
{
     SetCommandable (TRUE, GetPCSpeaker());
     object oAlluen = CreateObject(OBJECT_TYPE_CREATURE, 
          "alluen", 
          GetLocation(GetObjectByTag("DOOR_SPAWN"))
     );
 
     object oGranthos = OBJECT_SELF;
     AdjustReputation(OBJECT_SELF, oAlluen, -100);
     DelayCommand(2.0f, 
          AssignCommand(oAlluen, 
              ActionSpeakString("Not so fast, Granthos!")
          )
     );
 
     DelayCommand(2.3f, 
          AssignCommand(oAlluen, ActionAttack(oGranthos))
     );
}

This is where the player is made free again. Also, we create Alluen, an NPC, near the doorway (where I've placed the waypoint that I'm using in this script).


I've then adjusted the way Alluen feels about Granthos, and setup her action queue to do two things. First, actually say something to Granthos, and second, to actually attack Granthos.


Notice that I've declared oGranthos as OBJECT_SELF. Why have I done this? Becasue when I AssignCommand, I am changing the caller of a function. OBJECT_SELF refers to the caller, so by having OBJECT_SELF directly in the Attack function, I would actually be telling Alluen to attack herself.


To avoid this, I have declared OBJECT_SELF while Granthos is the object calling this script, and then referred to this declaration in the function. That way, I know for certain I am telling her to attack Granthos.


Since I want Granthos to die, I'm going to place the same kind of script in his OnPhysicalAttacked as I did with Kilinar:


//Script "cave_dieboss"
void main()
{
     if (GetTag(GetLastAttacker()) == "HENCH_ELF")
          DelayCommand(3.0f, 
               ApplyEffectToObject(DURATION_TYPE_INSTANT, 
                    EffectDeath(), 
                    OBJECT_SELF
               )
          );
 
/* rest of script */

So, by adding these two lines into the script, I've got the same effect as I did with Kilinar, only it checks to see if it was Alluen instead of Granthos (since he couldn't kill himself... theoretically).



Next!


This entire section was about creating a sequence of events. I had the minotaur enter the area, and speak with the player. If the dwarf was there, the two had a fight, which the minotaur won. Then the elf showed up and slew the minotaur.


If you didn't keep up, I suggest looking at how the minotaur's conversation is setup, since nearly all of the order in which this scripting occurred was handled by that one file. When you're ready, move onto the next section.



Tasks


I haven't been totally efficient in my scripting here. Try re-writing the scripting for Granthos and Kilinar to have the extra lines that I placed in there fired by their OnUserDefined handler (you'll need to read the box if you don't know how to do this).


Better yet, re-write the scripting for all three NPCs so that they use their plot flag to prevent the NPC that we want to remain alive from dying. This way the fight will be longer, and more realistic.



Hint


The UserDefinedEvent is a matter of moving the scripting from the individual scripts to the UDE script, and enabling the SignalEvent lines in the OnSpawn scripts for those particular UDEs.


Switching to the Plot Flag is simply working out when you want the creatures to be killable. So Granthos' plot flag gets removed when Alluen shows, and Alluens gets removed when she speaks to the PC.


UDEs


You may come across this term, it's short for User Defined Event. Essentially, it allows you to write script for an NPC without altering too many of their default scripts.


If you look through the various scripts on an NPC, at the bottom of each of them, you'll find a SignalEvent function used.


This is to send a number value to the NPC having the signal sent to. This causes the OnUserDefined script to fire.


There is a function which will retrieve the calling number of the event that just triggered the OnUserDefined script, and thus we can determine which piece of code to run using conditional statements.


To enable the default user events fired by the default scripts on an NPC, you must alter the OnSpawn script. Open one up now, and scroll down to the bottom. You'll find a number of commented out lines, which refer to the various default script handlers.


By deleting the double // marks before a line, you will enable the user defined event for that handler. Note the number it calls, and then adjust the OnUserDefined script accordingly.


PC or DM


The GetIsPC and GetFirst/NextPC functions will check against whether or not an object is a player. That is, any player in the game, including a DM.


GetIsDM must be used if you wish to determine if the target object is a player or a DM, since the PC functions will target anybody in the game.


Prevention


You've seen me do a number of if statements and loops, but I always have some way to determine whether or not the loop should end, or if it should only fire once.


There are two things to bare in mind. Firstly, if the script that you want to fire only once is on an object such as a trigger, you could simply use DestroyObject on the caller.


Secondly, if using a locally stored variable to determine how many times the statement should occur, always set the variable's new value before doing anything else in the conditional statement.


This is because in some cases where you have a lot of instructions in the conditional statement, you may find the player can trigger the script twice before it gets to the part where it sets the new variable state.


Reputations


Reputations are values used to determine whether two objects are friendly, neutral or hostile towards each other.


They range from 0-100, where 0 is hostile, and 100 is allied. Every pair of objects has values for their behaviour towards each other.


Factions


Factions are initial sets of reputation values. They establish how objects feel about each other when they first are created.


Changing the reputation of a faction towards another faction will change the way all objects in those factions feel towards each other.


However, the reputation values are still individually owned by those objects, so you can still alter the individual reputations, overriding the reputation values that were set because of their assigned faction.


Be aware that the AdjustReputation function does not alter the reputation values of individual objects in the way you might think.


While it will target a single object and adjust its reputation towards another object, it does so by changing the way the object's faction feels about the target.


So while you are still adjusting an individual object's reputation value, you are also adjusting how an entire faction feels about that object.


Be sure to pay attention to the definition of the AdjustReputation function, so that you know which one is which.



Screenshots


  • Granthos
  • He's Big
  • Go Badger!
  • Dead Dwarf
  • Dead Granthos




 author: Robert Straughan, editor: Charles Feduke