Fundamental Scripting Guide

From The DarkMod Wiki
Revision as of 11:49, 16 December 2020 by Dragofer (talk | contribs)
Jump to navigationJump to search

Primary contributor - Dragofer

This is a (work in progress) resource written primarily for mappers with no background in scripting or coding, such as myself, though others too may find practical guidance for working with TDM's scripting system. This is what I've picked up over the years through experimentation, looking at existing scripts and interacting with other forum members.

My aim is for the article to be both comprehensive and down to earth, using many examples and avoiding unnecessary technical terms. There will be some overlap with existing wiki articles, since this aims to have everything in one place out of one hand.

The concept will be to start with teaching some basic script literacy, then introduce scripting principles one after another. Towards the end, practical examples show ways of tying everything together to get more complex scripted effects.


Anatomy of a script

This section will define essential terms, then look at some basic scripts of slightly increasing complexity to demonstrate what a script is made up of.

Basic terms

Script events Script events are an action of some kind, i.e. waiting x seconds or triggering an entity. They are the basic building blocks of scripting.
Script function This is the technically correct term for a "script". Script functions often contain multiple script events, and some can also be used like script events in other script functions. Usually this guide will refer to them simply as "scripts".
Scriptobject This is a powerful set of scripts that are applied to single entities. This allows you to write code once, but let it apply to an unlimited number of entities without conflicts. An example is the tdm_light_holder scriptobject, which controls the skin, particle, lit state etc. of its candle.
Variables Variables are another basic building block and allow a script to store data for further processing and to have variable effects. Each variable must be named and given a data type.
Data types Every variable and every input/output of a script event needs to have a data type to inform the engine whether it should be read as a text, number, vector etc.

Script events

Example: wait()

This is a very basic script event which instructs the script to wait for 3s:

sys.wait(3);

Here's a closer look at its components:

  • sys.
    • Every script event must be called on an entity. For very generic events like "wait", sys is typically used, shorthand for system.
  • wait(3)
    • This is the actual script event. Every script event must be accompanied by input brackets, even if they're left empty. In this case, "3" is the input.
  • ;
    • A semicolon is needed to mark the end of a line of instructions. Forgetting these is a common mistake that stops the map from loading.


Example: setOrigin()

This is another simple script event. It's called on player1 and teleports (sets the origin of) him to the position '0 150 0'.

$player1.setOrigin('0 150 0');
  • $player1
    • Notice the $ sign: this means the script engine should look for a specific entity in the game with that name. The player is always called "player1".


Scripts

Example script: sending a message to the console

Script events can't work on their own, they need to be part of a script:

void test_script()
{
	sys.println(“Test message.”);
}
  • void
    • This defines what type of result the script puts out to other scripts: "void" means nothing while i.e. "float" would mean a number. In the large majority of cases you'd use void.
  • test_script()
    • This is the name of the script: "test_script".
    • Like with script events, input brackets are mandatory for scripts, even if the script doesn't use them. For a script that should damage various entities by various amounts, you could define an entity input and a float (number) input.
    • Note that no semicolon is needed at the end of this top line.
  • {}
    • Curly brackets mark the beginning and end of the body of the script.
  • sys.println("Test message");
    • This is a script event which prints a text to the console and then leaves a line (ln), so that every text is on its own line.


Example script: simple cutscene

void simple_cutscene()
{
	sys.fadeOut('0 0 0', 2);		//fade to black (colour vector '0 0 0') in 2s
	sys.wait(2);				//wait 2s for the fadeOut
	sys.trigger($speaker_narrator);	        //trigger a speaker with a narrator's voice
	$camera.activate($player1);		//activate a camera to disable player controls

	sys.wait(10);				//wait narrator is speaking
	sys.fadeIn('0 0 0', 2);			//fade back in from a black screen in 2s
	$camera.activate($player1);		//return player controls
}

This scripts a basic cutscene: fade to black, activate a speaker, fade back in. At both ends a camera is toggled so that the player can't move while the screen is black.

  • //
    • This is a comment for the reader: anything behind a double slash gets ignored. This is a useful habit for helping others and your future self understand how your script works.
    • An alternative way to comment, useful for mutli-line comments or temporarily disabling a section of the script, is to use /* at the beginning and */ at the end. This way you don't need to mark every single line as a comment.
  • sys.fadeOut('0 0 0', 2);
    • This script event uses 2 different inputs:
      • '0 0 0' is a vector defining the colour to which the screen should fade. Every number represents the intensity of red, green or blue colour.
      • 2 is a float (number) defining how long the fadeout should take.
  • sys.trigger($speaker_narrator);
    • This is a script event to trigger a specific entity called "speaker_narrator", which would simply be a speaker in the map with a custom soundshader.
  • $camera.activate($player1);
    • This is an alternative way to trigger, aka activate, an entity. This is needed when you want one entity to be triggered by another entity, instead of "sys". In this case, player1 is the activator who activates the camera. Another example would be the player activating a teleporter entity.


More scripting basics

General

  • Scripting is case-sensitive.
  • The scripting engine reads from top to bottom. That makes the order in which scripts and variables are arranged quite important.
  • The $ sign instructs the engine to look for an entity ingame with that name. If there's no $ sign, the engine will instead look for a variable with that name in the scripts.


Data types

Data type refers to whether something is to be treated as a text (string), number (float), vector etc. You will always want to be aware of this, since i.e. a script event designed to apply a force vector won't work if you give it a string instead of a vector.


String A string of letters/numbers, in other words plain text. It must always have double quotation marks. Example: “Text message 123”
Float A float is a single number, decimals are allowed. Example: 15.2
Vector A vector consists of 3 numbers, often used for 3D movements (x z y) or colours (red green blue). It must always have single quotation marks. Example: '0 90 0'
Boolean Can be true or false. Easily replaced by floats (0 or 1). Example: TRUE
Void No type. Most commonly used for scripts or script events that have no direct output.
Entity Indicates an entity, usually using the name as seen in DR. If it's a specific entity, instead of a variable, then a $ sign is required. Example: $player1
Subcategories of “entity” You can be more specific and define an entity as, for example, an ai, light or tdm_elevator. These are names of scriptobjects, and by defining an entity like this you gain access to that scriptobject's internal variables and scripts. For an "ai", useful internal variables are i.e. whether the AI is alerted or knocked out. (see "AI flags" for more)


Variables

Variables are a key principle in scripting. They allow the same script to have different results depending on the input, rather than always doing the exact same thing. Another use is for storing data, allowing it to be used by other script events or scripts.


Creating new variables

The first time the engine finds a variable in a script it needs to be given a data type. Afterwards you use just the name. Example:

float attempts;

sys.println("Player has " + attempts + " left.");

You may also want to assign an initial value. Otherwise, floats will default to 0, strings to "", vectors to '0 0 0' and entities to $null_entity. Example:

float attempts = 3;

Another option is to define a variable using the output of a script event. Examples:

vector starting_origin = $func_static_267.getOrigin();
float can_see = $guard_westwing_1.canSee($player1);
float sound_duration = $entity1.startSound("snd_move", SND_CHANNEL_ANY, false);  //stores the sound duration of the sound being played

Where a variable is defined is important. If it's inside a script, the variable will only be useable inside that script. If it's defined outside of a script, all later scripts will be able to use it.


Special notes on vector variables

It's possible to access individual numbers from a vector by appending _x, _z or _y to the end of the vector variable's name. These will get treated as floats.

Example: teleporting the player up by 16 units from wherever he is now

void teleport_player_up()
{
	vector player_position	= $player1.getOrigin();		//where is the player now? Store as a variable named "player_position"
	player_position_z	= player_position_z + 16;	//increase the z-component (height) of "player_position" by 16

	$player1.setOrigin(player_position);			//set the new origin of the player to the modified "player_position"
}


Another option is to get the magnitude of a vector, turning it into a float. This is useful for example in getting a percentage when checking the progress of a movement.

Example: checking the progress of a func_mover

void check_progress()
{
	vector start_position 	= '0 0 0';
	vector target_position	= '200 0 0';
	vector current_position	= $func_mover_1.getOrigin();

	float distance_moved 	= sys.vecLength(current_position - start_position);			//how far the mover has moved in units
	float percent_progress	= distance_moved / sys.vecLength(target_position - start_position);	//how far the mover has moved in percent
}


Example usage: puzzle with max 3 attempts

Say you wanted to give a player 3 attempts to solve an optional puzzle, with a chance to reset his number of attempts:

float attempts;				//how many attempts the player has made. Defined outside of a script so both scripts can access it.
float attempts_left = 3;			//how many attempts the player has left, max 3

void attempt_failed()				//called every time the player fails an attempt
{
	attempts	= attempts + 1;		//increase attempts counter by 1
	attempts_left	= 3 – attempts;		//calculate how many attempts left

	if (attempts_left == 0) $puzzle.setFrobable(0);	//if no more attempts, make the puzzle entity unfrobable
}

void reset_attempts()				//calling this resets the player's attempts to 0
{
	attempts = 0;
	attempts_left = 3 – attempts;

	$puzzle.setFrobable(1);			//make sure the puzzle entity is frobable
}
  • The default value for new floats is 0, therefore "float attempts;" is the same as "float attempts = 0;"
  • There is some redundancy in this setup, i.e. it's not necessary to give attempts_left a value of 3 at the beginning because this gets calculated in the 2 scripts. But in my opinion it makes the script easier to follow, and it's good to make sure your variables are always up to date in case you later add another script that should work with them.



Using the TDM Script Reference

The TDM Script Reference is an essential wiki resource for scripting, listing all available script events for the current version of TDM. All script events are listed twice: the first time they're sorted alphabetically, the second time they're sorted based on which kinds of entities they can be called on. By far not all script events have received manual descriptions, so sometimes you may need to experiment with them to properly figure them out.

Here is an example:

scriptEvent float canSee(entity ent);

    no description

    Spawnclasses responding to this event: idAI

Interpretation:

  • the script event canSee() is suitable for being called on AIs.
  • an entity must be entered into its input bracket.
  • the script event returns a float. This means you could for example store the result of this check in a float variable and use that elsewhere in your scripting.


Actually using the script event may take some experimentation, mainly to see which entity the event should be called on and which entity should go into the brackets. In the end it could look something like this:

$guard_westwing_1.canSee($player1);


Alternatively, if you want to store the result as a variable (that we name "guard_sees_player") for later use:

float guard_sees_player = $guard_westwing_1.canSee($player1);


Setting up the .script files

A .script file is simply a .txt file whose .txt extension has been renamed to .script. If you can't see or change the .txt extension, make sure your operating system isn't set to automatically hide known file extensions.

Any simple text editor, such as Notepad or Wordpad, can be used to work with scripts, though you may want to download a more elaborate text editor to profit from features such as conditional highlighting to help you keep track of which brackets go together.

If you're opening a .script file for the first time, you will be asked to set your preferred text editor as the standard program for opening this kind of file.


Map scripts

The easiest way to setup scripts is as a map script. Any scripts in this file will only be available in that map, which is often all you need. Create a new .txt file in your maps folder with the same name as your map and change the .txt extension to .script.

The next step is always to add a main() script. This script must always stay at the bottom of your .script file:

void main()
{
	sys.waitFrame();
}

The main() script is automatically called at map start. This makes it convenient for testing scripts and initialising scripts that should run from the beginning of the mission.

It's good practice to let the main() script begin with sys.waitFrame();. This is because all entities are spawned in the first frame of the mission, so waiting a single frame makes sure they're ready for script events.


General scripts

It's possible to let your scripts be available in all missions, which is useful if you're making a campaign or some kind of scripting addon. This has the downside that you have to restart TDM (rather than just the map) before changes take effect. Any mistakes stop TDM from starting up with a blue screen, and you will need to press "copy" and paste the error message somewhere like Notepad or Word to read it. Therefore it's recommended to develop your script as a map script first.

The .script file can have any name and must be placed within the "script" folder (without an s, create one if needed) instead of the "maps" folder. Unlike map scripts, general scripts should not contain a main() function.

Your script folder always needs to contain a tdm_custom_scripts.script file. This instructs the game to #include your custom script into the game's inclusion chain for scripts. As an example, see below what line is needed in tdm_custom_scripts.script to #include a .script file called "my_custom_script":

#include script/my_custom_script.script


Scriptobjects

Scriptobjects are entity-specific sets of scripts. They should be defined in general scripts, since having scriptobjects in map scripts breaks saving/loading. However, it's recommended to develop scriptobjects in map scripts since you don't need to restart TDM to make the changes take effect. Scriptobjects will be discussed in more detail elsewhere.



Practical exercise: subtly teleporting the player

So far we've seen the basics of scripting: the basic composition of a script, how variables are made and modified, how to use the TDM Script Reference as well as how to set up your .script file. THat would already be enough (albeit borrowing a little from later, more detailed topics) to try an early scripting exercise:

Say you were making some kind of wizard's tower, or had 2 separate areas that should look like they're physically connected: you'd want to teleport the player between 2 rooms without him realising it. That means both rooms should look identical, and the player should be teleported to the exactly same position in the other room.


Brainstorming

Before you start writing any lines of script, it's good to plan this out, maybe even writing down notes somewhere. The more you think of now, the fewer roadblocks there might be down the line:

  • 1) What will be teleported? Just the player, or maybe also moveables and even AIs?
  • 2) How will the script know where the player/etc. should be teleported?
  • 3) How should the rooms be made? Should AIs have access? Should there be extinguishable lamps? Where should the trigger brushes be?


1) What will be teleported? Just the player, or maybe also moveables and even AIs?

Since this is a basic script, it's better to keep things simple, so only the player will be teleported for now. Moveables would be more more advanced, since they would require some way of detecting them (i.e. with a room-filling trigger_touch brush) and a way of running the teleportation script on every one of them. AIs most likely shouldn't be teleported since it might disrupt their pathfinding.


2) How will the script know where the player should be teleported?

To maintain the illusion, we want the player to be teleported to the exact same position in the other room. That means we can't teleport him to a pre-determined position, but will instead have to modify his current position by an offset.

Say the 2nd room was 1024 units off to the right compared to the first unit. In that case, you can simply add or subtract 1024 from the player's origin on the x axis to move him between the 2 rooms. But if you ever moved these rooms, you'd have to manually update this number.

Therefore it's better to automatically calculate the offset as a variable. All you need is to take the position of some kind of reference point in both rooms, such as the origin of the central table and find the difference. This vector will always take you to the corresponding point in the other room.


3) How should the rooms be made? Should AIs have access? Should there be extinguishable lamps? Where should the trigger brushes be?

This is more of a mapping question. We want there to be as few differences as possible between the 2 rooms, so any non-static entities like AIs, loot, moveables or extinguishable lamps shouldn't be found in or near these rooms (unless you want to put in the extra work of synchronising the 2 rooms' copies via script).

The next mapping question is layout. Corridors leading to/from the rooms could be L-shaped to minimise what the player can see when the transition happens (to reduce how much has to be kept identical). You will also need to place the trigger brushes so that the player doesn't immediately stumble into the brush that teleports him back where he came from.


Laying the groundwork

Next you will want to do the mapping in DR. This gives you all the entities you need for the later script. After going through the brainstorming, this is a setup that could work:



Ways of calling a script

TDM has many methods for calling scripts. Often you can combine systems such as the objective systems with scripting in order to get powerful results that wouldn't easily be possible otherwise.

From other scripts

To call one script from another script, use the thread event. The main advantage of this method is that you can pass as much input as you like. You can only call scripts that the engine already knows about, in other words if the called script is higher up in the .script file or in the #inclusion chain of .script files. If the script should start at the beginning of the map, you could use void main() since it gets called automatically.

Example:

void script1()			//prints a line to the console
{
	sys.println("Running script1");
}

void main()			//automatically runs script1 at the start of the map
{
	sys.waitFrame();	//give all entities enough time to spawn
	thread script1();	//run script1
}


If the script you're calling needs input parameters, don't forget to include them. See section "Scripts with input variables" for more.

Example:

void move_any_mover(entity func_mover, vector move_vector)	//this script expects 2 parameters: an entity and a vector
{
	vector current_position = func_mover.getOrigin();	//get current position
	func_mover.time(2);					//movement will take 2s
	func_mover.moveToPos(current_position + move_vector);	//make mover move to new position
}

void call_movement()						//this script calls move_any_mover() and passes it the required parameters
{
	thread move_any_mover($box1, '100 0 0'); 		//the parameters are the name of a func_mover entity to be moved and a vector for the movement
}


target_callscriptfunction

You can create an entity in DR called target_callscriptfunction and give it the spawnarg "call" with the name of the script without brackets. Whenever this entity is triggered, i.e. by a button or if an AI reaches a path_node that targets this, the script will be called. As far as I'm aware, it's not possible to pass any parameters this way.

Example:

The script in the .script file:

void script1()
{
...
}

The spawnarg on the target_callscriptfunction:

"call" "script1" 


Trigger brushes

Trigger brushes are a common way to call scripts, activated when the player or an AI enters the volume of the brush. Like with target_callscriptfunction entities, they need a "call" spawnarg naming the script to be called. See Triggers for details on how to create one.

If you want your script to know which entity has activated the trigger brush, you can give your script an entity as an input parameter.

Example:

void script1(entity activator)
{
	sys.println(activator.getName() + "has activated the trigger brush.");
}

Potential pitfall: stationary AIs will not activate trigger_multiple brushes. Potential pitfall: trigger_touch brushes can be resource-intensive if left running. It's recommended to activate them only for a single frame before switching them off again.


Path nodes

Many Path Nodes trigger all of their non-path-node targets whenever an AI reaches them. This can be combined with target_callscriptfunction entities to call a script.


Stim/Response

The Stim/Response system is very powerful, in particular when combined with scripting.

In most cases, you'll likely be interested in only the "Response" tab of the S/R editor interface. This allows you to make an entity respond to certain stimuli, such as a frob, a trigger or water (left side of the window) by executing various effects (right side of the window). Many different effects are available from the dropdown list.


One common use is to make an entity frobable and give it a "Response" to frobbing which runs a script. To do so, set the spawnarg "frobable" "1" on the entity to make it frobable. Then open the S/R editor, go to the Response tab, in the left half of the window select "Frob", in the right half of the window right-click and add an effect to "Run script", naming the script you want to run.

To make this single-use you could add another effect to either:

  • disable frobability of this entity
  • or deactivate the response to frobbing (the ID of the frob stim is 0).


Action scripts

Entities can be given spawnargs that make them run scripts when certain actions are performed on them, such as frobbing or using. The value of the spawnarg must be the name of the script to be run. Example: "frob_action_script" "script1".

Like with trigger brushes (see earlier), these entities pass their own name to the script function. This allows you to reuse the same spawnarg & script on many entities.

frob_action_script Calls the specified script when the entity is frobbed by the player.
use_action_script Calls the specified script when the player uses an inventory item on the entity (i.e. use a key on a door). The inventory item must be specified in a "used_by" spawnarg on the entity. More info here: Tool, Key, custom used by inventory actions
equip_action_script Calls the specified script when the player is holding the entity and uses it (i.e. eating an apple).


Signals

Signals are events which can be setup to call a script. Examples of events are the entity being triggered, frobbed, removed or damaged. Some signals only work for movers i.e. when a mover is blocked or a door is closed/opened. See the Signals wiki article for a full list of available signals.

Note that the signal system is very old, so the signal for frobbing is called SIG_TOUCH. The SIG_USE signal refers to a player holding an object and pressing the 'use' key (default enter), as far as I'm aware.

To use the signal system, you need to use a script event to instruct the entity to respond to a certain signal by running a certain script. Example:

sys.onSignal(SIG_TRIGGER, $func_static_267, "script1");

You may later want to disable this again. In that case:

sys.onSignal(SIG_TRIGGER, $func_static_267, "script1");


Objective system

The Objectives Editor can be seen as a visual scripting editor. In that sense it synergises well with scripting.

The main way to use the objective system for scripting is by specifying completion scripts and failure scripts when editing an individual objective. These scripts will get called if the objective is completed or failed. Note that if the objective is reversible, it might call the scripts multiple times.

The objectives system allows you to do some things much more easily than with regular scripting, such as calling a script when reaching a certain page in a book (i.e. playing an ominous sound). Many maps therefore have hidden objectives, or a mix of hidden objectives and scripts, in order to achieve interesting scripted effects.


Location system

The location system (wiki page: Location Settings allows you to call scripts whenever the player enters or leaves specific locations.

It works by setting spawnargs on the info_location entity, referencing a script's name:

"call_on_entry"
"call_on_exit"
"call_once_on_entry"
"call_once_on_exit"

On the scripting side of things, your script must specify the location where it's called in its input brackets. Note that this is without a $ sign. Example:

void enter_location_streets(entity info_location_streets)		//for an info_location entity with the name "info_location_streets"

To my knowledge, this way of setting up means a script can only be called from one location. If you have multiple locations, you will need multiple scripts even if they do the same thing.


From the console

This is only for testing purposes because you will no longer be able to save the game after calling a script from the console. Simply use thread name_of_script();, just as when calling a script from another script.


Making scripts read the map with "get" events

Your scripts unlock much potential if they can see and react to what's happening in the map. The way to get up-to-date information about the state of the entities is with "get" script events. There are a lot of them, and as said they have a ton of potential.


Often you'll store their results as a variable, like this:

	float health_current = $lord_marlow.getHealth();	//Lord Marlow's current health

Or use them directly in a conditional:

	if ($lord_marlow.getHealth() < 100)			//If Lord Marlow has been hurt...


Getting spawnargs

Spawnargs are a special case. All spawnargs are strings, so if you want to store them as a variable you need a "get" event which converts them into the matching data type:

string name		= $mover.getKey("name");		//for strings
float move_time		= $mover.getFloatKey("move_time");	//for floats
vector rotate		= $mover.getVectorKey("rotate");	//for vectors
boolean open		= $mover.getBoolKey("open");		//for booleans
entity frob_master	= $mover.getEntityKey("frob_master");	//for entities


Example useage: checking the current health of an AI for an objective

This script is for an objective to let no harm come to an AI. It would check the AI's "health" spawnarg to find what his max health is and check the AI's current health. Then it compares the two: if there's a difference, the objective fails.

void check_health()
{
	float health_max	= $lord_marlow.getFloatKey("health"); 	//get spawnarg for max health
	float health_current	= $lord_marlow.getHealth();		//get current health

	if(health_current != health_max) $player1.setObjectiveState(5, OBJ_FAILED);

	sys.wait(0.5);			//wait 0.5s...
	thread check_health();		//... before repeating this script
}


Conditionals

Basics

Conditionals check whether conditions have been met before carrying out the associated set of instructions. They're very useful for making dynamic scripts that react to or wait for events in the map.


If there's only a single line of instructions to carry out, then it can be on the same line as the conditional check itself without any extra brackets:

if($player1.getHealth == 100) sys.println("player1 has full health");


If you have a set of instructions, use curly brackets. Note that the line with the conditional needs no semicolon:

if($player1.getHealth != 100)
{
	sys.println("player1 doesn't have full health");
	$player1.setHealth(100);
	sys.println("player1 has been restored to full health");
}


You can either:

  • compare values with each other (i.e. "is an entity's health less than or equal to x?")
  • or you can evaluate whether a single value is true or false. You can evaluate any type of variable, not just booleans: any non-0 or non-default value counts as "true". So i.e. for an entity variable, $null_entity is considered false, for a string "" is considered false, for a float 0 is false, for a vector '0 0 0' is false.

Various examples:

if (attempts_left)				//check whether the player has a non-0 number of attempts left (note: also negative values count as true)
if (attempts < max_attempts)			//another way of checking whether the player has attempts left
if ($lord_marlow.getFloatKey("can_unlock"))	//check whether this AI can unlock doors

//If you want to check whether something is false, you need an exclamation mark within the brackets.
if (!$lord_marlow.canSee($player1))		//if the AI CAN'T see the player, do... 


Available symbols

==, != equal, not equal
<, > less than, greater than
>=, <= less or equal to, greater or equal to


An easy mistake is to use only a single equals sign in a conditional:

= means "set to this value"

== means "equal to this value"

Example:

attempts = 3;		//translation: set "attempts" to 3
if (attempts == 3)	//translation: check if "attempts" is equal to 3


Multiple conditionals

Sometimes you want to check multiple conditionals, i.e. if A and B are true but C is false. Important symbols here are:


&& and

|| or


Example useage: secret conversation occurring under specific circumstances

if ( ( ( $lord_marlow.getLocation() == $mansion_bedchamber ) || ( $lord_marlow.getLocation() == $mansion_secretchamber ) ) && ( !$lord_marlow.canSee($player1) )
{
	$lord_marlow.bark("snd_secretconversation");	//say a secret phrase
}

In other words: if Lord Marlow is either in his bed chamber or his secret chamber, and he can't see the player, make him say a secret line.

Getting the right number of brackets in the right place can be tricky. It can be helpful to write each conditional on its own line first, then combine them one by one into a single line.


Chains of conditionals

Other than "if", there are also "else" and "elseif". These only get considered if the preceding "if" has failed.

"Else" will always get carried out, while "elseif" checks whether its conditions are met.


Example useage: script for a lever with 3 positions and an inactive state

void lever()
{
	if ( lever_position == "inactive" ) return;		//if lever is inactive, stop processing this script

	if 	( lever_position == 1 )	sys.trigger($door1);	//check for lever position 1
	elseif	( lever_position == 2 )	sys.trigger($door2);	//otherwise check for lever position 2
	elseif	( lever_position == 3 )
sys.trigger($door3);	//otherwise check for lever_position 3
}

Looping / repeating scripts

You may want a script to keep repeating until stopped, for as long as a condition stays true, or repeat for a number of times.

With some exceptions there should always be a wait built into the script, otherwise the script will attempt to repeat an infinite number of times in one go and crash.


Looping with thread

The easiest way to loop is by making the script call itself at the end with "thread".

Example:

void looping_script()
{
	if ( $lord_marlow.getHealth() != 100 ) $player1.setObjectiveState(5, OBJ_FAILED);

	sys.wait(0.5);
	thread looping_script();
}


Looping with while()

while() lets you loop a script for as long as the condition inside the brackets is true. If it should loop permanently, put a 1 into the brackets (true).

Example useage: continually checking an AI's health for a "Don't harm" objective

while(1)
{
	if ( $lord_marlow.getHealth() != 100 ) $player1.setObjectiveState(5, OBJ_FAILED);
	sys.wait(0.5);
}

Example useage: timer for how long a box has been moving

while( $box.isMoving() )
{
	sys.wait(0.5);
	float time_moved = time_moved + 0.5;
}


Repeating with for()

for() lets a script repeat for a number of times that can be either pre-determined or variable. It's well-suited for gradually making a change in many small steps (increments).


Example useage: gradually changing a shader parameter on an entity

void materialise()
{
    float i;					//define a float (integer) to keep track of the number of steps
    for (i=0;i<100;i++)			//go from 0 to (one under) 100, one at a time
    {
        fade_entity.setShaderParm(5,i);	//set shaderParm5 to the current number of steps
        sys.waitFrame();			//wait one frame until the next step
    }
}


Repeating with do + while()

"Do this script for as long as this condition is met". This one is quite similar to just using while, the difference is it can start even if the condition is false at first. It also doesn't need to include a wait if you know the condition will only hold true a finite number of times.

Example usage: running a script on every entity of a certain type

//Script by Obsttorte
void lamps_toggle()				//finds and triggers all electric lamps in the map
{
	light lamp;
	do					//do repeat this...
	{
		lamp = sys.getNextEntity("lightType","AIUSE_LIGHTTYPE_ELECTRIC",lamp);
		if (lamp != $null_entity) sys.trigger(lamp);
	}	while (lamp != $null_entity);		//repeat for as long as sys.getNextEntity finds electric lights
}


Terminating a running script

Terminating with killthread()

It's possible to manually terminate any script by giving it a (thread) name, and letting another script terminate it.

Example:

void check_health()
{
	sys.threadname("looping_script");		//assign a name to this script or thread

	if ( $lord_marlow.getHealth() != 100 ) $player1.setObjectiveState(5, OBJ_FAILED);
	sys.wait(0.5);
}

void terminate_check_health()
{
	sys.killthread("looping_script");	//terminate the script that was named "looping_script"
}

Terminating with return

Alternatively you can terminate a script with "return". This causes the script to terminate and return a value, if it has one. Example:

if ( attempts == 3 ) return;


Giving scripts input variables

A good way to avoid script duplication is to make use of input variables, allowing you to write a single longer script and pass it instructions from elsewhere, such as smaller scripts.

Input variables are defined in the round brackets next to the name of the script. They can then be used in the script itself:


Example: this script can be used to apply to any ai ("guard") a variable amount of damage ("damage_amount"):

void damage_guard(ai guard, float damage_amount)
{
	float health_current	= guard.getHealth();
	float health_new	= health_current – damage_amount; 

	guard.setHealth(health_new);
}


Other, shorter scripts can call the above script and give it input parameters to tell it which AI should be affected and how much damage:

void hurt_guard1_severe()
{
	thread damage_guard($guard1, 50);		//reduce guard1's health by 50
}

void hurt_guard2_light()
{
	thread damage_guard($guard2, 20);		//reduce guard2's health by 20
}


Example useage: room with multiple func_movers and buttons, movement in various directions

This is the main script that does the heavy lifting in doing the movements. It's setup to be used on various entities (named "box") with variable "move_time" and "move_vector".

void move_mover(entity box, float move_time, vector move_vector)
{

/*
Main script that performs the movements.
Gets called by and receives input parameters from move_forward() or move_backward().
Checks if the box is stationary.
If box is stationary, calculates new target position, sets time taken to move and then initiates.
*/

	if(!box.isMoving())		//check if box has stopped moving
	{
		//define new target position based on current position + input move_vector
		vector target_position	= box.getOrigin() + move_vector;

		box.time(move_time);		//set time taken to move
		box.moveToPos(target_position);	//initiate movement
	}

}

These are the smaller scripts that pass instructions to the main script. Each one is called from a separate button in the map.

void move_forward_box1()		//called by button1 in the map
{
	thread move_mover($box1, 2, '50 0 0');
}

void move_backward_box1()		//called by button2 in the map
{
	thread move_mover($box1, 3, '-50 0 0');
}

void move_forward_box2()		//called by button3 in the map
{
	thread move_mover($box2, 2, '50 0 0');
}

void move_backward_box2()		//called by button4 in the map
{
	thread move_mover($box2, 3, '-50 0 0');
}


Scripting Sounds

Script events for sounds are slightly atypical and counter-intuitive to use, which is why they have their own section.

Ways of starting a sound

First of all, there are two similar-looking script events for starting a sound on an entity:

startSound(string sound, float channel, float netSync);
startSoundShader(string shaderName, float channel);

In general, startSound() is the preferred method. This is because it can only play soundshaders that are defined on the entity in spawnargs beginning with snd_. What this spawnarg does is that it precaches (preloads) the sound automatically. If a sound is not precached, it might pop when it's played for the first time, as a result of not loading fast enough (a "cache miss").

The downside of startSound() is that the soundshaders must be defined in advance as spawnargs. You can use startSoundShader instead, but should use an earlier script event to precache the soundshader on that entity.

Another difference is that startSound() requires you to input a "netSync" float variable. I believe this has to do with multiplayer, making sure the sound is synchronised for all players. In any case, it's always set to false.


Thus, here are the 2 correct ways to start a sound on an entity, shown as examples:

$torch_1.startSound("snd_extinguish", SND_CHANNEL_ANY, false);		//snd_extinguish is a spawnarg on the torch containing the name of the soundshader, it's not the name of the soundshader itself. The spawnarg is set in DR or in the entity def of the torch
$torch_1.cacheSoundShader("torch_extinguished");			//makes sure this soundshader won't pop when it's played for the first time
sys.waitFrame();							//give time for the cache to occur; probably even better would be to cache the sound at map start
$torch_1.startSoundShader("torch_extinguished", SND_CHANNEL_ANY);	//"torch_extinguished" would be the name of a soundshader in the TDM assets


Sound channels

You can play multiple sounds simultaneously on the same entity by playing them on different channels. Some channels are reserved for important game sounds (i.e. ambient music, objective completions) and shouldn't be used for scripted sounds.

Instead of typing out the name in full, you may instead use the channel's ID number (i.e. 11 instead of SND_CHANNEL_UNUSED1) in sound script events.

Channels suited for scripted sounds:

As a string As a float
SND_CHANNEL_ANY (Unknown)
SND_CHANNEL_UNUSED 9
SND_CHANNEL_UNUSED_2 11


Other channels

SND_CHANNEL_BODY2 SND_CHANNEL_BODY3 SND_CHANNEL_WEAPON SND_CHANNEL_ITEM SND_CHANNEL_HEART SND_CHANNEL_DEMONIC
SND_CHANNEL_VOICE
SND_CHANNEL_VOICE2
SND_CHANNEL_BODY
SND_CHANNEL_BODY2
SND_CHANNEL_BODY3
SND_CHANNEL_WEAPON
SND_CHANNEL_ITEM
SND_CHANNEL_HEART
SND_CHANNEL_DEMONIC
SND_CHANNEL_AMBIENT
SND_CHANNEL_DAMAGE


Sound properties

When a sound is initiated via script, its properties (looping, min/max distances etc.) will be primarily controlled through settings in the soundshader.


Stopping sound

The script event for stopping a sound on an entity is stopSound(), with the channel as input. If you want all sounds on all channels to stop, use SND_CHANNEL_ANY as input for stopSound().


AI barks

The correct term for a soundshader spoken by an AI is "bark", thus the script event bark() would be used to make an AI speak via script. Compared to i.e. startSound(), this has the advantage that lipsync is enabled and that the AI wouldn't randomly say something else at the same time. It does still work on dead or unconscious AIs, so you may want to check the AI_DEAD and AI_KNOCKEDOUT flags first (see "AI flags").



Special methods

This section is devoted to useful practical applications of various scripting principles and for special cases.

Going through all entities of a certain type

The getNextEntity() function allows you to find every entity in the map that has a certain spawnarg. It finds one entity at a time, so you'll need to run it many times. Each time it runs you'll have an opportunity to run script events on the entity that was found.

As shown in "Looping/Repeating scripts", do + while() are well suited for this application:

//Example script by Obsttorte
void lamps_toggle()				//finds and triggers all electric lamps in the map. They're identified based on the value of the "lightType" spawnarg
{
	light lamp;
	do					//do repeat this...
	{
		lamp = sys.getNextEntity("lightType","AIUSE_LIGHTTYPE_ELECTRIC",lamp);	//find the next entity with this spawnarg; continue the search from the previously discovered lamp entity
		if (lamp != $null_entity) sys.trigger(lamp);
	}	while (lamp != $null_entity);		//repeat for as long as sys.getNextEntity finds electric lights
}


Going through all targets of an entity

numTargets() returns the number of targets that an entity has. This can be combined with "for" to run script events once on every target:

void hide_targets()						//finds all targets of an entity and runs a script event on them (in this case: hide)
{
	float i;						//define a float (i for integer) to keep track of how many targets have been identified

	for(i = 0; i < $func_static_267.numTargets(); i++)	//for every target of func_static_267...
	{
		entity m_target = $func_static_267.getTarget(i);	//store this target as a variable so we can call an event on it. Will get overwritten by the next target
		m_target.hide();					//call hide() on this target
	}
}


Going through all entities bound to an entity

This is almost identical to going through all targets of an entity. Important is the distinction between bindChildren and bindMasters: bindChildren are lower in the bind hierarchy, bindMasters are higher in the bind hierarchy relative to the entity in question.

void identify_handle()					//goes through all entities bound to a door in order to identify a handle
{
	for(i = 0; i < $mover_door_1.numBindChildren(); i++)			
	{
		entity m_bind_child = $mover_door_1.getBindChild(i);		//find the nth bound entity
		if(m_bind_child.getKey("spawnclass") == "CFrobDoorHandle")	//check if this bound entity is a handle. If yes...
		{
			entity m_handle = m_bind_child;	save it as a handle	//...store it for later
		}
	}
}


Scripting with time

wait() events

The game engine runs by default at 60 frames (or tics) per second, which is independent of the number of visual frames rendered per second. The most accurate expression for the duration of a frame is therefore 1/60 of a second. This is roughly 0.0167 seconds. The most reliable way to wait for only one frame is waitFrame().

When you run a wait() event, the engine performs a check every frame (every ~0.0167s) to see whether the total time elapsed has surpassed the time it was instructed to wait. This means that if you use wait() with very small durations, then the engine will probably overshoot that duration due to a lack of precision. So i.e. sys.wait(0.02) would only complete after 2 frames, or 0.0333s.

Timekeeping

There are a couple options for keeping track of time:

  • sys.getTime(); will get the current game time. Example:
void move_mover()					//move a mover and keep track of how much time it needed
{
	float time_starting	= sys.getTime();
	float time_elapsed	= 0;			//make sure time_elapsed is always set to 0 when this script starts

	$func_mover_1.speed(50);
	$func_mover_1.moveToPos('150 0 0');
	sys.waitFor($func_mover_1);
	
	time_elapsed = sys.getTime() - time_starting;
}


  • in a looping script, increment a float variable representing time elapsed with every cycle of the script. Prefer waitFrame() and fractions over wait() and small values due to the aforementioned advantage in precision. Example:
void time_keeping()
{
	float time_elapsed = 0;		//make sure time_elapsed is always set to 0 when this script starts
	sys.waitFrame();		//wait the first frame now

	while(1)			//insert a condition of your liking here, i.e. whether a mover isMoving()
	{
		time_elapsed = time_elapsed + 1/60;	//time_elapsed increasing directly after the conditional avoids the situation where a condition stops being true but the script still adds another frame to time_elapsed
		sys.waitFrame();
	}
}


Scripting with AIs: AI flags

AI flags are pre-made variables contained in the "ai" scriptobject. They contain useful information about an AI, such as how alert it is, whether it has been knocked out, whether it can see an enemy and so on. Some of these flags are also contained in the "player" scriptobject", such as AI_CROUCHING.

In order to access the AI flags of an entity, you first need to define the entity as an "ai" or "player". Examples:

ai guard = $guard_warehouse_1;

player player_self = $player1;			//recommended not to use only "self" as the name, since "self" is used in various other ways as well


Then you can use the flags like this:

if ((!guard.AI_KNOCKEDOUT) && (!guard.AI_DEAD)) guard.bark("snd_conversation");	//if the ai is neither KO nor dead, make him bark a line for a (monologue) conversation

if(player_self.AI_CROUCHING)			//check if the player is crouching


Here's a selection of useful AI flags. You can find an exhaustive listing of all AI_flags in the ai scriptobject, which is defined in tdm_base01.pk4 > script/tdm_ai.script > jump to "object ai".

	boolean		AI_TALK;		//AI is talking
	boolean		AI_DEAD;		//AI is dead
	boolean		AI_KNOCKEDOUT;		//AI is knocked out
	boolean		AI_ENEMY_VISIBLE;	//enemy is visible
	boolean		AI_ENEMY_IN_FOV;	//enemy is in field of view
	boolean		AI_CROUCH;		//AI is inspecting a corpse, or the player is crouched
	boolean		AI_RUN;			//AI is running
	float		AI_AlertLevel;		//0-5, where 0 is completely idle and 5 is combat
	float		AI_AlertIndex;		//corresponds to AlertLevel, as far as I'm aware


Spline movers

A spline is a curved line which is stored on a spline mover. A regular mover (func_mover) can be scripted to move along this spline. Usually the spline mover itself is invisible, while the func_mover is visible or has visible entities bound to it.

To create a spline in DarkRadiant, click on the button "Create a NURBS curve" along the left edge of the window. Next to this button there are several others which allow you to append, insert or remove control points from this curve, or convert it into a curve which uses the CatmullRom algorithm. The control points can be moved around to change the shape of the curve, quite similar to the control vertices in patches.

When your spline is done, create a small ca. 8x8x8 brush textured with textures/common/nodraw around the origin of the curve (make sure you look at it in all 3 axis) and right-click > create entity > func_splinemover.

Copy the "curve_Nurbs" or "curve_CatmullRomSpline" spawnarg from the curve and paste it onto the nodraw brush. You now have a spline. Next you need to create a func_mover with the same origin as the spline. This func_mover can be a visible model. Or it can be another nodraw brush to which you can bind a func_smoke particle, which would leave a trail.


You'll now need a script to make the func_mover move along the spline. In many ways it's similar to how a func_mover works:

void start_spline()
{
	entity spline		= $func_splinemover_2;		//the name of the nodraw splinemover brush in your map
	entity func_mover	= $func_mover_1;		//the name of the func_mover in your map 

	func_mover.time(10);					//let the mover take 10s per lap. Alternatively, set speed(), in units.
	func_mover.disableSplineAngles();			//optional: use this to stop the mover from rotating wildly depending on how the curve is angled
 	sys.threadname("spline_1");				//optional: give this thread a name so the spline mover can be stopped by another script with sys.killthread("spline_1");

	while(1)						//loop the following indefinitely
	{
		func_mover.startSpline(spline);			//start the mover along the spline
	 	sys.waitFor(func_mover);			//wait for the mover to finish its movement
	}
}


Spawning entities

You can spawn an entity of a specified classname while the map is running. In the same frame, you should also set any custom values that you need for spawnargs. Example:

entity spawned_entity 	= sys.spawn("func_static");		//classname is "func_static"
spawned_entity.setName("stagecoach");
spawned_entity.setModel("models/darkmod/misc/carriages/stagecoach.lwo");
spawned_entity.setOrigin('483 723 41');


Setting up Stim/Response via script

It's possible to apply Stim/Response settings to an entity at run time, rather than doing so manually in DR. This is especially useful for setting up a spawned entity to respond to triggers or frobs. The below example is for setting up an entity to respond to triggering by running "script1":

$func_static_1.ResponseAdd(STIM_TRIGGER);
$func_static_1.ResponseSetAction(STIM_TRIGGER,"script1");
$func_static_1.ResponseEnable(STIM_TRIGGER,1);


Utility scripts

In addition to the script events listed in the TDM Script Reference, you can also find numerous utility scripts in tdm_base01.pk4 > script/tdm_util.script. These utility scripts are similar to script events, the difference is they aren't called on an entity. Example:

float sign_of_variable		= sign(-3);	//determine whether the number in the input brackets is positive or negative


See below for some of the most versatile utility scripts:

abs(float value) 		Gets the absolute value of the number you put in (in other words, makes any negative numbers positive)
sign(float value) 		Gets the sign of the number you put in (positive returns 1, negative returns -1)
//These scripts compute times in the future, based on the game clock and sys.getTime().
RandomTime( float delay )	 	Computes a random game time that is at least "delay" seconds in the future
RandomDelay( float min, float max ) 	Computes a random game time that is between "min" and "max" seconds in the future
DelayTime( float delay )		Computes the game time that is "delay" seconds in the future 
//These scripts gradually modify the _color of an entity, which makes sense on lights or models with colorme materials. Note that the script will wait while these fades take place, so you may want to run them in a separate script that runs at the same time as the main script
fadeOutEnt( entity ent, vector color, float totalTime )			//fade from "color" to black
fadeInEnt( entity ent, vector color, float totalTime )				//fade from black to "color"
crossFadeEnt( entity ent, vector source, vector dest, float totalTime )	//fade from colour "source" to colour "dest"


Troubleshooting

Debugging your script

If your script doesn't do what you expect it to, it can be helpful to add sys.println() at various stages in your script to print information about what's happened so far. This helps you narrow down where the problem lies.

For one, you can check whether a certain part of the script is actually running. Example:

void check_progress()		//checks whether the player has completed an objective. If yes, starts the patrol of a previously inactive AI.
{
	sys.println("Script check_progress has been called.");

	if($player1.getObjectiveState(5) == OBJ_COMPLETE)
	{
		sys.println("Player has completed objective 5");
		sys.trigger($ai_thief_1);				//start patrol of an AI that was previously sitting at a table
	}
}


You can also print variables to the console, allowing you to see if they have the values you expect at that stage of the script. Example:

void move_mover()		//moves a harmful mover towards the player as part of a trap
{
	entity func_mover	= $func_mover_1;
	vector starting_origin	= func_mover.getOrigin();
	vector target_origin	= $player1.getOrigin();

	sys.println(func_mover + " will move from " + starting_origin + " to " + target_origin);

	func_mover.time(1);
	func_mover.moveToPos(target_origin);

	sys.waitFor(func_mover);
	sys.println(func_mover + " has arrived at + " target_origin);
}


Common errors

  • Problem: after loading the map, the screen shows only a close-up of a random texture, can't move or use the inventory
    • This happens if you just started TDM but an error prevented you from loading the map the first time, you fixed the error and tried to reload your map again.
    • Fix: load into another map or restart TDM, then try to load the map again. The fastest way is to try to load a map that hasn't been dmapped yet, since that'll abort within seconds.