Robert Straughan - Capture Glory: Death Rules

Starting Off


Okay, for this module, I decided to keep to just two areas. This is not a tutorial on how to use the toolset, or how to do custom content. I am going to run you through how I came up with the scripts that make this module happen.



Module Properties


The first thing I did was removed the default OnDeath, OnDying, and OnRespawn scripts from the Module properties, found in the Edit menu. I then decided what sort of special rules I would want to implement for these.


I chose:


When a player is reduced to 0 or less hit points, they are automatically killed (ie. there is no bleeding).


If the player chooses to respawn, they lose a point of Constitution, as though they had been resurrected.


If the player chooses to respawn, they respawn at a point near the beginning of the area.



OnDying


In order to ensure that the player actually dies when they are reduced to 0 or less hit points, I wrote the script "tutorial_ody". In it, you will find the following lines:


//Script "tutorial_ody" 
void main() 
{ 
     ApplyEffectToObject(DURATION_TYPE_INSTANT, 
          EffectDeath(), 
          GetLastPlayerDying()
     ); 
} 

Now, this is probably one of the simplest pieces of scripting you'll find in this tutorial. Be sure to read about functions in the box to the right before continuing.


If you look at the definition of ApplyEffectToObject in the box at the bottom of the script editor, you'll see this:


//Definition of the ApplyEffectToObject function
void ApplyEffectToObject(int nDurationType, effect eEffect, 
     object oTarget, float fDuration=0.0f)

Now, the function is a void function, which means it will simply run by itself, as it returns nothing. Notice it has four parameters. We need to fill in that information so that the function knows what to do.


The first value is an integer, called nDurationType. We don't have to worry about the name of the parameter, since that is used within the functions code to refer to whatever value we place there. In this case, we have used DURATION_TYPE_INSTANT.


Notice that despite the fact this is an integer parameter, the value uses letters. This is because certain constants used by the engine have been given predefined text values so that we can easily know which value to use.


In this case, DURATION_TYPE_INSTANT is actually the equivalent of the value zero. Since some functions use values up to 300 or more, its easier for us to use the text equivalents. That way we know what is going on.


The next parameter is an effect, called eEffect. In this case, we have gone and put EffectDeath() into it. Look in the editor for this function, and you'll find the following definition:


//Definition of the EffectDeath function
effect EffectDeath(int nSpectacularDeath=FALSE, int nDisplayFeedback=TRUE)

As you can see, this function returns an effect value. Since the parameter requires an effect piece of information, we can place this function into the parameter (told you it was like LEGO).


Notice that I haven't entered any information into the parameters of the function, even though it asks for them. That's because the parameters already have values by default. When this occurs, if I don't want to change those values, I don't have to put them in.


The final parameter I've entered is GetLastPlayerDying(), for the parameter oTarget. As you can see, oTarget must be an object. Find the GetLastPlayerDying function, and you'll get this:


//Definition of the GetLastPlayerDying function
object GetLastPlayerDying()

This function has no parameters, and returns an object value, so what I've entered will be valid. Note that there are many functions like this, and you must choose the one which matches the event handler that fired the script.


In this instance, it was the OnDying handler, so I used the GetLastPlayerDying function. Some other examples include:


  • OnUsed = GetLastUsedBy
  • OnClosed = GetLastClosedBy
  • OnAreaTransition = GetClickingObject

Notice that there is one last parameter for the function we are using. A float value called fDuration. This has already been declared by the function as being zero.


It is actually used for when we have a DURATION_TYPE_TEMPORARY effect. Since we aren't using one of those, and the value has already been given a default of zero, we can skip it out.


And that's it. The script will run whenever a player is between 0 and -10 hit points, and will apply the effect of death to them.



OnDeath


Now that we've established that players die when they drop to or below 0 hit points, we need to present them with whatever options they have. To do this, we place a script in the OnDeath handler, with the following lines:


//Script "tutorial_odth"
void main()
{	
     object oPlayer = GetLastPlayerDied();
     
     PopUpDeathGUIPanel(oPlayer, TRUE, FALSE, 1, 
          "If you press respawn, you will be resurrected at the beginning of " +
          "the level, and will suffer a -1 to your constitution.");
}

Now, as ever, the void main() means that the script will run whatever is in those curly brackets as soon as the script is fired. In this case, whenever a player actually dies.


Notice how I have declared an object. The first line means I have created a value that the script can use. I've made it an object value, and I've called it oPlayer. I could have left it at that.


Instead, I've also initialised the value, by giving it a value equal to whatever the GetLastPlayerDied function equals. This is one of those functions that gets an object based on the handler firing this script.


The PopUpDeathGUIPanel is the function which makes that set of buttons you get when you die appear. It has a number of paremeters we can give it, as defined below:


//Definition of the PopUpDeathGUIPanel function
void PopUpDeathGUIPanel(object oPC, int bRespawnButtonEnabled=TRUE, 
     int bWaitForHelpButtonEnabled=TRUE, int nHelpStringReference=0, 
     string sHelpString="")

So, another void function, which means it returns no information. This is also an 'internal' function, which is why I don't have to declare it, and can just run it straight away. Bioware has already written and defined this function for me, its not something you need to do yourself, you just use it.


The first parameter is an object called oPC, and indicates the object that we wish to target. Notice I place oPlayer into this parameter. Once I've declared oPlayer, and initialised it with a value, I can refer to the declared name from then on to use that object.


Notice that even though the next parameter has been declared for me by the function, I've still put it in. This is because I wish to change the values of parameters after that one in the sequence. I cannot skip out a parameter, even if already declared, if I wish to change ones after it.


I have changed the next parameter to FALSE. The next parameter I have set to 1, to let the function know that it should show the string I have entered into the last parameter. If in doubt as to what you should put in, read the function definition in the bottom box of the editor.


The final parameter is a string value, which is a message put up on the screen when the death panel shows.


Before moving on, just to make sure I have made the point clear, consider the following script:


//Example Script
void main()
{	
     object oPlayer = GetLastPlayerDied();
     string sMessage = "If you press respawn, you will be resurrected at " +
     "the beginning of the level, and will suffer a -1 to your constitution.";
     PopUpDeathGUIPanel(oPlayer, TRUE, FALSE, 1, sMessage);
}

What does this do? If you've read it correctly, you'll notice it does the exact same thing as the previous script. All that this does differently is that it declares a string called sMessage, and initialises it. I then use the declared name to represent that value in the function's parameter.



OnRespawn


Finally, the OnRespawn script. This is where we put everything we've learned from the previous two scripts together. Observe the following:


//Script "tutorial_ores"
void main()
{				
     object oRespawner = GetLastRespawnButtonPresser();
     object oWP = GetNearestObjectByTag("RESPAWN_WAYPOINT", oRespawner);
     
     ApplyEffectToObject(DURATION_TYPE_INSTANT, EffectResurrection(), oRespawner);
	 AssignCommand(oRespawner, JumpToLocation(GetLocation (oWP)));
	 
	 DelayCommand(0.1f, ApplyEffectToObject(DURATION_TYPE_INSTANT, 
	      EffectHeal(GetMaxHitPoints (oRespawner)), oRespawner)
	 );
	 
	 DelayCommand(0.2f, 
	      ApplyEffectToObject(DURATION_TYPE_PERMANENT, 
	           SupernaturalEffect(
	                EffectAbilityDecrease(ABILITY_CONSTITUTION, 1)
	           ),
	           oRespawner
	      )
	 );
	 
	 DelayCommand(0.2f, 
	      FloatingTextStringOnCreature(
	           "*constitution decrease*", 
	           oRespawner, 
	           FALSE
	      )
	 );
}

So, what does it do? By now void main() should be familiar to you. I've declared and initialised two values. Both are objects, and one is called oRespawner while the other is oWP.


oRespawner is one of the simple 'get an object based on the handler' functions. But what about oWP. Here, I've used the function GetNearestObjectByTag function. Here's the definition:


//Definition of the GetNearestObjectByTag function
object GetNearestObjectByTag(string sTag, object oTarget=OBJECT_SELF, 
     int nNth=1)

This function returns an object, so I won't get an error for declaring it as an object. The first parameter is a string value. Here, we enter the tag of the object we are after, namely a waypoint with the tag RESPAWN_WAYPOINT.


Notice that although the tag for the waypoint does not have the quote marks, the string does. Any string, regardless of whether it contains numbers or letters, must be encased in quote marks.


The next parameter is an object. In this function, oTarget has already been declared, but the script is in the OnRespawn handler, which means OBJECT_SELF, is the module. That won't work.


I've placed the declared name for the last person to push the respawn button into this parameter, since it is an object. I haven't specified the last value, since I don't need it for the moment.


What this function will do is get the nearest object to the person who pressed the respawn button with the tag that I have specified. It will not be able to get objects outside the area that the respawning player is in.


Next I use the same ApplyEffectToObject function as I did in the OnDying script, only this time, I've specified that it is using an EffectResurrection() instead. It has no parameters, so I've entered none.


The next line is interesting. I've used the function AssignCommand. Look at the definition:


//Definition of the AssignCommand function
void AssignCommand(object oActionSubject, action aActionToAssign)

As you can see, for the object parameter I've given the respawning player as the oActionSubject. But what about this action?


The majority of commands that can be given to an object in the module have Action at the front of their names. Some don't have this, but all Action commands are placed into the action queue of the target object.


You can also place void commands into the action slot. Why? Well, look at the listed Action functions in the editor. See their definitions? They are all void commands. The only difference is that functions with Action in their name are queued.


So what does this mean? Well, AssignCommand means we can tell an object other than the calling object to do something. As this is the OnRespawn script, using the action command I've given without this extra function means it would try to get the module to do this action.


//Definition of JumpToLocation function
void JumpToLocation(location lDestination)

This is the action in question. Notice that there is actually an ActionJumpToLocation function. I'm using this one since I don't want the target to queue this command, I want it to happen instantly.


The parameter is a location value, and I've used the GetLocation function to specify the location of oWP, as follows:


//Definition of GetLocation function
location GetLocation(object oObject)

The function returns a location value, and has an object value for a parameter. So I can combine the two together, and specify an object within GetLocation, in this case, oWP. Obviously I didn't want the module to jump to the location of oWP, which is why I used AssignCommand.


The next two lines are also ApplyEffectToObject functions, with two different effect functions used. They are slightly modified however, using the following function:


//Definition of DelayCommand function
void DelayCommand(float fSeconds, action aActionToDelay)

This is exactly the same in application as the AssignCommand function. It is used to delay the firing of a void function, typically one with an Action in its name. You can delay the function using a float value that represents the number of seconds before the function will fire.


Note that the DelayCommand functions will fire themselves as soon as this script has fired. They wait for the specified amount of time, and then trigger the commands they have been given.


This does not mean that commands beneath the DelayCommand will wait until the delayed function has fired. The script remains open and comes back for the delayed commands when their time is up.


Imagine I'm sitting at the end of a conveyor belt, which is sending random LEGO bricks in my direction. I'm in the process of building a wall when a window shows up. But I'm not ready to place the window.


So I put it to one side, and continue building the wall from the bricks the conveyor is sending me. Eventually, I see where I want the window to go, so I pick up the brick and place it. This is how DelayCommand works, the script keeps running, but you can put a function to one side for a while.


The first effect used is EffectHeal:


//Definition of EffectHeal function
effect EffectHeal(int nDamageToHeal)

This will heal an amount of damage equal to the integer value we specify in the parameter. In this case, I've used GetMaxHitPoints:


//Definition of GetMaxHitPoints function
int GetMaxHitPoints(object oObject=OBJECT_SELF)

This returns an integer value, but since I don't want to get the hit points of OBJECT_SELF (which is the caller of this script, ie. the module) I have specified that oRespawner should be the value for oObject.


The next one is also an odd one:


//Definition of EffectAbilityDecrease function
effect EffectAbilityDecrease(int nAbility, int nModifyBy)

This in itself does not look all that complicated. An effect returning function, with two integer parameters. For the first, we specify ABILITY_CONSTITUTION to tell it we wish to decrease the target's constitution score.


For the second, we tell it 1. Note that this is not -1. The function itself has already taken into account that it is subtracting, so had you placed -1 into this parameter, it would add instead.


The interesting part of all this is that this effect has been encased within another function:


//Definition for SupernaturalEffect function
effect SupernaturalEffect(effect eEffect)

The duration type for the ApplyEffectToObject function has been set to DURATION_TYPE_PERMANENT, but there's a small nuance with this. It isn't permanent. Not truly. This is to make an effect very long lasting, but dispellable.


By placing the effect we wish to apply within this effect returner, we aren't changing anything other than the fact the effect is no longer dispellable. This makes it truly permanent, and not even a script can remove this effect any more.


And finally, the last line:


//Definition of FloatingTextStringOnCreature function
void FloatingTextStringOnCreature(string sStringToDisplay, 
     object oCreatureToFloatAbove, int bBroadcastToFaction=TRUE)

Floating text is that slightly transparent text which floats up the player's model on certain events, such as a locked door. To let the player know what is happening, I've decided to add some in for the respawn.


It's a void function, so I can delay it. The string value I've typed directly into the function parameter. For the object target, oRespawner makes sure I'm sending the message to the right player.


The last parameter I've set to FALSE, since I don't want this message to appear to other party members. A lot of working out what parameters to specify is an application of logical thought, or simply looking at the definition in the box at the bottom of the editor.


So, what does this script do? When a player pushes the respawn button, it will resurrect them, send them to the nearest object with the tag I specified, heal them to their maximum hit points, decrease their constitution by 1 permanently, and inform them of the loss.



What have you just learned?


Believe it or not, on this one page, you have learned all there is to know about the basics of scripting. If you think you can handle this, move onto the next section. From now on, I'll be going a lot faster.


Don't believe me? Well, perhaps you should read through it all again one more time to be sure you understand it. Or better yet, close this tutorial, and try and write the above three scripts in the editor yourself, then see if you got close.



Tasks


Here's a task for you. In the OnRespawn script, I healed the player of their hit points. Alter the script so that it heals them by exactly the amount they need to be fully healed.



Hint


You need to change the integer amount that the player is healed by to the exact amount they have remaining. You can use the functions for retrieving the player's hit point values to do the maths and get the correct value.

White Space


The first important concept to be aware of is that the compiler ignores white space.


There is no must or must not on how you space out the lines in your code, it is simply for presentation and to ensure your code is easy to read.


To tell the compiler where each line begins and ends, you place a semi-colon to mark the end of a line. There are some exceptions to this, which I will explain later.


Functions


Functions are the majority of script. They are the processes that the game does to get the results you want.


So what do you need to know about functions? Basically every function is made up of four parts:


The Returners


At the beginning of every function will be a returner. This indicates what kind of information is returned by this function when it is run.


The Function


This is the name of the function, and usually gives you an indication of what the function does.


Note that you can tell the difference between a function and a value name because functions will always have a pair of parentheses () after their names.


The Parameters


The last section of the function is contained within brackets. These are the pieces of information that the function needs to be given to do what it does.


There may be no parameters for a function. There may be several parameters, and these are seperated by commas.


Some parameters might be pre-declared, in which case, provided it is the information you need, you can skip entering that information in.


The Code


Underneath every function is a set of curly brackets. Within these are the code that forms what the function does.


Two Scripts


At the end of the day, there is only ever two kinds of scripts.


Those which start with void main() are the majority of them. void main() is the function which is run straight away when a script is called. It does so because it is a void function, and thus returns no information.


There are some which start with int StartingConditional(), which means the script returns an integer value based upon the scripts results. It is used to determine whether a conversation line should be spoken or not.


True or False


A nice little nuance with the NWScript language is that we can use the boolean expressions TRUE and FALSE in our scripting. What's even nicer is that FALSE is equal to the integer value zero, and TRUE is equal to one. This helps out a great deal in a number of situations.


Duration Types


One of the little nuances that I've seen some people have difficulty with is duration types.


With the example script to the left in mind, EffectDeath you might think is a permanent effect. This is in fact wrong.


EffectDeath is the act of killing the target. We don't want to tell the script to continually kill the target. Once will suffice. This means we use an instant effect instead.


The area where this really applies is when dealing with visual effects, which come in different types. Generally, VFX_DUR type visuals are duration or permanent visual effects, and VFX_IMP and VFX_FNF effects are instant.


Declarations


Rather than placing a value straight into a function's parameter, I can declare the information seperately.


For example, I could have a string called sMyString, and set it to "Hello" if I wanted, or I could use a function which returns a string instead.


When declaring a value, if I do not initialise it, its default status is 0. With strings this means it starts as a blank string, and with objects it starts as OBJECT_INVALID.


The advantage of declaring a value is two-fold:


First, I don't have to type out values over and over. I can declare it once and then just refer to the name I assigned it.


This also means that the value in question will never change. It will always be the exact same value on line x as it is on line y (unless of course I change it over the course of the script).


Second, it's efficient when you need to use a function multiple times. Functions that get or set information to and from the module take a little processing power each time the script is run.


By declaring a function once, the script only has to do that process once. Even if the function in question takes up only a small amount of procesing power, where you have lots of scripts firing in multiple places, you should attempt to only ever use a function once in any one script.


It reduces the amount of processing the computer has to do to run a script, and it also ensures that you are manipulating the same piece of information throughout the script, and not getting or setting different ones.


The last thing to remember about declaring values is that, when you declare them, the reason you are declaring them is to tell the script what type of value they are.


From then on, it knows what the value type is whenever you type it into the script. This means when you initialise the value, and you later wish to change what it equals, you do not need to give its value type again.


This is why you nearly always see declarations in a group near the beginning of the script. If you don't declare what type of value it is before using it in the script, the script doesn't understand what you're asking it to do.


Once declared, from then on you can use the value in mathematical operations, or in any other way, and you won't have to tell the script what kind of value it is.





Screenshot





 author: Robert Straughan, editor: Grimlar