Difference between revisions of "Fundamental Scripting Guide"

From The DarkMod Wiki
Jump to navigationJump to search
m (→‎Example: sending a message to the console: In 1st println example, replaced asymmetric double quotes with symmetric)
Line 63: Line 63:
  void test_script()
  void test_script()
  {
  {
  sys.println(“Test message.);
  sys.println("Test message.");
  }
  }


Line 78: Line 78:


* sys.println("Test message");
* 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.  
** 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: simple cutscene ====
==== Example: simple cutscene ====

Revision as of 00:46, 20 December 2020

Primary contributor - Dragofer

This is an in-depth resource for scripting as a mapper in TDM. It assumes no prior knowledge of scripting and is aimed in particular at those (like myself) who prefer to learn by looking at existing examples.

The first sections up to "Practical exercise: subtly teleporting the player" are written more in the style of a beginner's guide, while later sections aim to collect everything related to scripting in one place and out of one hand. So the recommended usage of this resource for a beginner would be to use the initial sections to learn the basics, then gradually acquire the remaining techniques & principles whenever you need something new for what you're trying to do.


Anatomy of a script

This section will teach some basic scripting literacy, first by defining a handful essential terms, then picking apart some basic scripts of slightly increasing complexity.

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 many script events, and some can also be used like script events ("utility scripts"). 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 have variable effects and to store data for further use elsewhere. 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. This is needed to inform the engine whether it should be read as a text (string), number (float), 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 the system entity.
  • wait(3)
    • This is the actual script event. Every script event must be accompanied by input brackets, even if they're 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".
  • setOrigin('0 150 0')
    • Every new word in a script event's name starts capitalised. Scripting is case-sensitive, so this is an important detail.


Scripts

Example: sending a message to the console

Script events can't work on their own, they need to be inside a script. The below script is as basic as it gets, containing a single script event:

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 body of the script. Script events go here.
  • 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: simple cutscene

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

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

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.
  • sys.fadeOut('0 0 0', 2);
    • This script event uses 2 different inputs:
      • '0 0 0' is a vector defining the color to which the screen should fade. Every number represents the intensity of red, green or blue color.
      • 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. You can't call a script or use a variable if the engine hasn't seen it defined yet.
  • 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.
  • Commenting can be done either by putting // in front of every line of comment, or by putting /* at the top and */ at the bottom. The latter is useful for multi-line comments or temporarily disabling a section of your script.
  • To make changes to a (map) script take effect, you need to reload the map. This can be sped up by binding the "map" command to a hotkey (note that this only works when youre already in a map). Entering the following into the console would bind opening a map called test_script.map to the p key:
bind "p" "map test_script"


Data types

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

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 colors (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. 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 or modified by other script events.


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;						//define "attempts" as a new float variable
attempts = attempts + 1;				//increase the number of attempts made by 1
sys.println("Player has " + attempts + " left.");	//print a console message with the number of 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. 3 Examples:

vector starting_origin = $func_static_1.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 visible to that script. If it's defined outside of a script, all later scripts will be able to use it.


Example: puzzle with max 3 attempts

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

float attempts_left = 3;		//the starting number of attempts the player can make, defined as a float

void attempt_failed()			//called every time the player fails an attempt
{
	attempts_left	= attempts_left - 1; 			//reduce the number of attempts left by 1

	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
{
	attempts_left = 3;

	$puzzle.setFrobable(1);			//also make sure the puzzle entity is frobable
}



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 grouped 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 entry:

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 event calls this entity "ent", but this name doesn't matter to the scripter
  • 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.


It might take some experimentation to figure out which entity the script event should be called on and which entity should go into the input 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);		//if you haven't defined the "guard_sees_player" variable yet


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 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 set up 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.

A map .script file must always have 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

General scripts are available in all missions, which is useful if you're making a campaign or some kind of scripting addon. The downside is that you have to restart TDM (rather than just the map) before changes take effect. Also, 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) 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 files into the engine's inclusion chain. 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"


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. Some methods overlap, but there are subtle differences that justify each method in specific situations. For example a frob_action_script is very easy to apply, while a frob response can be deactivated.


From other scripts

To call one script from another script, use "thread". 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 script you're calling must be higher up in the .script file or in the #inclusion chain of .script files than the script from which you're calling it. If the script should start at the beginning of the map, you could use void main() to call it.

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 in the call. 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 to move and a vector for the movement
{
	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 the mover move to the 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'); 		//tell move_any_mover() to move box1 100 units along the x axis
}


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 input parameters when calling a script this way.

Example:

The script in the .script file:

void script1()
{
...
}

The spawnarg on the target_callscriptfunction entity in DR:

"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 their creation and the various types.

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 stepped into the trigger brush.");
}
  • Potential pitfall: AIs will stop activating trigger_multiple brushes if they become stationary.
  • Potential pitfall: trigger_touch brushes can be resource-intensive if continuously active. It's recommended to activate them only for a single frame, then switch them off again.


Path nodes

Many Path Nodes trigger all of their non-path-node targets whenever an AI reaches them. They can target a target_callscriptfunction entity in order to call a script.


Stim/Response

The Stim/Response system is very versatile, in particular when augmented 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 by executing various effects, such as running a script. Many other effects are available from the dropdown list.


One common use is to make an entity frobable and give it a "Response" to frobbing with an effect to run 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 add a "Frob" entry, 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 is the name of the script. 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. uses a key on a door). The inventory item must be named 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 a held apple by pressing enter).


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 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_1, "script1");

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

sys.clearSignalThread(SIG_TRIGGER, $func_static_1);


Objective system

The Objectives Editor can be seen as a visual scripting editor. Not unsurprisingly it synergises well with scripting.

The main way to use the objective system for scripting is by specifying completion scripts and failure scripts for when an 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 use 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, with the value being the 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. The command is the same as when calling a script from another script, i.e. thread name_of_script();


Getting information from 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 and properties of 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 to choose 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: checking an AI's health for a "No harm" 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, then compare that with the AI's current health. If there's a difference, the objective fails.

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

	if(health_current != health_max) $player1.setObjectiveState(5, OBJ_FAILED);	//if health is no longer max, set objective 5 to failed

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


Practical exercise: subtly teleporting the player

So far we've seen the basics of scripting, including the basic composition of a script, how variables are made and used, how to "get" information about entities in the map, all the ways to call a script, how to use the TDM Script Reference as well as how to set up your .script file. That would already be enough 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 would 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 fixed position, but will instead have to get his current position and modify it with 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 a piece of furniture, 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). AIs should have no way of pathing into these rooms.

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 you have to keep identical). You will also need to place the trigger brushes so that the player doesn't teleport and immediately stumble into the brush that teleports him back where he came from.


The mapping

Next you will want to do the mapping in DR, as you'll need the entity names later on for the script. After the brainstorming, this is a setup that could work:

Comp script silent tele.png

These are the key features of the mapping setup:

  • the table in the centre of each room acts as the reference point for calculating the teleportation offset. For easier reference in the script, they've been given special names (table_1, table_2).
  • there are 2 multi-use trigger brushes, arranged in such a way that when the player crosses the room he only activates one of them. One has a "call" spawnarg to call the script "teleport_forward", the other calls "teleport_backward".
  • all 4 corridors are blind and L-shaped. The player enters the setup via the bottom left corridor and leaves via the top right corridor, so you'd attach the rest of your map to these 2 corridors.


The scripting

Now that all the planning and setup are out of the way, the scripting itself can begin. Start with thinking about what important components are involved: there will be 2 scripts (teleport_forward, teleport_backward) and one variable for storing the teleportation offset. Another variable will be needed to tell the script where to teleport the player to.

The variables should be available to both scripts, so they should be defined at the top (making them visible to all scripts below them) and outside of a script (so they're not specific to only one script).

vector teleport_offset;		//the vector for moving between room 1 and room 2
vector teleport_destination;	//the position the player should be teleported to

On to the first teleportation script. Much of the work lies in calculating the variables (the offset and then the destination).

void teleport_forward()
{
	teleport_offset		= $table_2.getOrigin() - $table_1.getOrigin();		//calculate the vector to get to room 2 from room 1, using the tables as reference points
	teleport_destination	= $player1.getOrigin() + teleport_offset;		//modify the player's current position with the "teleport_offset" in order to get the destination

	$player1.setOrigin(teleport_destination);					//perform the teleportation to the position stored as "teleport_destination"
}

For teleporting back, the first script can be copied. The only change needed is to subtract the teleport_offset from the player's origin, rather than adding it.

void teleport_backward()
{
	teleport_offset		= $table_2.getOrigin() - $table_1.getOrigin();		//calculate the vector to get to room 2 from room 1, using the tables as reference points
	teleport_destination	= $player1.getOrigin() - teleport_offset;		//modify the player's current position with the teleportation offset in order to get the destination

	$player1.setOrigin(teleport_destination);					//perform the teleportation
}

The setup is now done. If everything is done right, the player would now teleport seamlessly between the 2 rooms without ever noticing. You could place some kind of unique item in one of the rooms (i.e. a big torch) to demonstrate this more visibly.


Notes:

  • A lot of the work in this case went into the planning and mapping phases, while the scripting itself was quite straightforward (possibly thanks in part to the advance planning). Planning setups like this should become easier the more you've done with scripting in your maps.
  • The variable "teleport_offset" is always the same, so it's not ideal to recalculate it every time a teleportation script is called (even though get() events are very lightweight). Alternatively you could calculate it a single time in void main() or in a 3rd script that gets called after the map starts. The downside would be that your scripts are more spread out. With the above approach, everything is compact and in one place.


Scripts that use 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. Note that it won't be possible to call these scripts without input.

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 a variable amount of damage ("damage_amount") to any ai ("guard"):

void damage_guard(float damage_amount, ai guard)
{
	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
}


(Note: the more correct way to damage an AI or the player is by applying a damageDef. These cause the entity on the receiving end to emit appropriate sounds, get blasted away etc.)


Example: multiple func_movers and buttons, movement in multiple directions

This is the main script that does the heavy lifting in applying the movements. It accepts input variables for a mover entity ("box"), movement duration ("move_time") and the movement vector ("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 the box is stationary, calculates new target position, sets time taken to move and then initiates movement.
*/

	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 to the target position
	}

}

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');		//box1, 2 seconds move_time, 50 units along the x axis
}

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');
}


Conditionals

Basics

Scripts can check whether conditions have been met before carrying out the associated set of instructions. This is 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 the instructions can be on the same line as the conditional itself. No extra brackets needed:

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 now 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 the following... 


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: secret conversation under specific circumstances

if ( ( $lord_marlow.getLocation() == $mansion_bedchamber || $mansion_secretchamber ) && ( !$lord_marlow.canSee($player1) ) )
{
	$lord_marlow.bark("snd_secretconversation");	//say a secret pass phrase if he believes no one's nearby
}

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: lever with 3 positions and an inactive state

void lever()			//intended as one of several scripts controlling a lever. Checks which position the lever is in and performs an appropriate action
{
	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
	else				sys.trigger($door3);	//only remaining possibility is the lever is in position 3
}


Looping a script

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

The scripting engine allows up to 10,000 events per frame, so if you're doing something a finite number of times (i.e. checking all entities of a certain type) you can usually do it all in a single frame.

However, if the script should keep running continuously (i.e. for a "No harm" objective) you need to build some kind of wait() event into the script. This avoids that the script tries to run an infinite number of times in a single frame (crash with "runaway loop" error).


Looping with thread

The easiest way to loop is by making the script call itself at the end with "thread". Unlike other methods, this will always repeat the whole script.

Example: "No harm" objective

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: timer for a mover

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


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: fading 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 begin even if the condition is false at first (the conditional is at the end, not at the beginning).

Example: 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);	//find the next entity which has this spawnarg value
		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 simply "return". This causes the script to terminate. Example:

if ( attempts == 3 ) return;


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, 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();	//read the game clock to see what time the mover started

	$func_mover_1.speed(50);
	$func_mover_1.moveToPos('150 0 0');

	sys.waitFor($func_mover_1);
	float time_elapsed = sys.getTime() - time_starting;	//read the game clock again and subtract the starting time to get time_elapsed
}


  • 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;	//increase time_elapsed immediately, then wait. This way around keeps the timer a little more up to date.
		sys.waitFrame();
	}
}


AIs

AI flags

AI flags are pre-made variables contained in the "ai" scriptobject. They contain useful information about an AI, such as how alerted it is, whether it has been knocked out, whether it can see an enemy and so on. Some of these flags are also used by 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


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);

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 at map start. 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 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. 9 instead of SND_CHANNEL_UNUSED) in sound script events.

Channels suited for scripted sounds:

As a string As a float
SND_CHANNEL_ANY
SND_CHANNEL_UNUSED 9
SND_CHANNEL_UNUSED_2 11


Other channels:

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 applications of various scripting principles and to 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 scripts", the combination of do + while() is 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_1.numTargets(); i++)	//for every target of func_static_1...
	{
		entity m_target = $func_static_1.getTarget(i);	//store target #i 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
{
	float i;

	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;				//...store it for later
		}
	}
}

Going through all spawnargs with the same prefix

This works quite similar to going through all entities of a certain type. The difference is that there's a 2nd step: first find the name of the spawnarg, then find the value of that spawnarg.

Example: finding all frob_peers of a door and making sure they're all frobable

void find_peers()
{
	string key;			//name of the spawnarg
	entity frob_peer;		//value of the spawnarg; in this case, it'll be a frob_peer entity

	do
	{
		key 		= $mover_door_1.getNextKey("frob_peer", key);	//find the exact name of the next spawnarg that begins with "frob_peer", starting from the previous result
		frob_peer	= $mover_door_1.getEntityKey(key);		//find the value of this spawnarg, which should be an entity

		if(frob_peer) frob_peer.setFrobable(1);				//if a valid entity has been found, set it to frobable 
	}	while(key);							//keep repeating until this script stops finding valid spawnarg names
}

Note:

  • Alternatively, you could pass getNextKey() an empty string, "". That should make the script find all spawnargs of this entity.


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:

void spawn_stagecoach()
{
	entity spawned_entity 	= sys.spawn("func_static");		//spawn a "func_static" entity
	spawned_entity.setName("stagecoach");
	spawned_entity.setModel("models/darkmod/misc/carriages/stagecoach.lwo");
	spawned_entity.setOrigin('483 723 41');
}


Doing more with vector variables

Accessing individual components as floats

It's possible to access individual components of 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"
}


Getting the magnitude of a vector

Another option is to get the magnitude, or length, of a vector with sys.vecLength(), which turns the vector into a float. This is useful for example in calculating distances or expressing the progress of a mover as a percentage.

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 so far in units
	float percent_progress	= distance_moved / sys.vecLength(target_position - start_position);	//how far the mover has moved so far in percent 
}


Spline movers

A spline is a curved line which is stored on a spline mover entity (misleading name). A regular mover (func_mover) can be scripted to move along this spline. Usually the spline mover 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 other buttons which allow you to append, insert or remove control points from this curve, or convert it into a curve which uses the CatmullRom algorithm instead of NURBS. The control points can be moved around to change the shape of the curve, quite similar to how the control vertices of patches work.

When your spline is finished, 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). Select the brush and right-click > create entity > func_splinemover.

Copy the "curve_Nurbs" or "curve_CatmullRomSpline" spawnarg from the curve, paste it onto the nodraw brush and delete the original curve. 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 like the wisps in Thief.


You'll now need a script to make the func_mover move along the spline:

void start_spline()
{
	entity spline		= $func_splinemover_2;		//the name of the nodraw splinemover brush carrying the curve data 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 per second
	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
	}
}


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);


Scriptobjects

Scriptobjects are sets of scripts that are assigned to individual entities. They're mainly used for making entities with scripted behaviours (i.e. a healing fountain) and for entities that carry out scripted effects (i.e. atdm:target_setteam, which changes the team of targeted AIs when triggered).

They allow many entities to carry the same, complex scripts without conflicting, since each entity has its own values for the variables. They are also well suited for taking spawnargs into account, allowing mappers to change how the scripts play out for each entity. An entity can only have one scriptobject.

Each scriptobject has 3 sections:

  • The object definition, where all variables and scripts must be defined in advance
  • The init() script, which is called when the entity spawns. Used for initial setup, such as determining what script to call when triggered or frobbed
  • The actual scripts that do the interesting stuff.

Optional, but recommended, is to create an entity definition. This allows you and others to easily create entities in DR that have your scriptobject and all relevant spawnargs, including tooltips.

.script files with scriptobjects should be stored in the /script folder and #included in tdm_custom_scripts.script. This is because saving/loading will no longer work if the map .script contains scriptobjects. However, it's recommended to develop scriptobjects in a map .script, since it's faster to reload the map than to restart the game in order to make changes take effect.


Part 1: object definition

  • All variables and scripts have to be defined in advance here. It's not possible to assign values or run script events yet.

Example:

object healing_fountain			//scriptobject for a fountain that heals the player when frobbrd
{
	//define the scripts
	void init();				//every scriptobject must have an init() script, even if empty
	void heal_player();			//the script that does the healing

 	//define all variables
	//my preference is to group them depending on whether they come from spawnargs

	//SPAWNARGS
	float heal_amount;			//the fountain will heal the player by this amount
	float cooldown;				//fountain will need this amount of time to recharge
	//snd_heal				//healing sound; no need to define as a variable, because sound events are started directly from the spawnarg
	//snd_inactive				//sound played when player frobs the fountain during the cooldown period

	//INTERNAL
	float active;				//whether the fountain will heal the player when frobbed
	float last_used;			//time on the game clock when the fountain was last used; for cooldown period
	float health_target;			//how much health the player will heal to
};


Part 2: init()

  • This script is called when the entity spawns and is used to perform initial setup. That includes retrieving values of spawnargs or setting up the entity to respond to trigger events.
  • Alternatively you can retrieve spawnarg values later in the actual scripts each time they run, if you'd like to allow mappers to change spawnargs after the map has started.
  • Since scriptobjects are entity-specific, all events are called on self by default, so you can just write i.e. getFloatKey("cooldown");
void healing_fountain::init()			//init() function of the healing_fountain scriptobject
{
	sys.onSignal(SIG_TOUCH, self, "heal_player");		//when touched (= frobbed), this entity will call the script heal_player(), contained in this scriptobject

	heal_amount	= getFloatKey("heal_amount");		//retrieve spawnarg values
	cooldown	= getFloatKey("cooldown");

	active		= 1;					//assign a starting value to this variable
}


Part 3: the actual scripts

  • From now on this is almost like regular scripting and can be as simple or complex as you like. There is some added convenience:
    • all events are called on self by default, and you can use self as an input parameter.
    • Any script can call any other script, regardless of which one is written first. This is because you've already named them all in the object definition.
void healing_fountain::heal_player()
{
	if ($player1.getHealth == 100) return;		//do nothing if the player is at full health
	
	elseif (!active)				//if the fountain is inactive, check if enough time has passed to reactivate it...
	{
		if ( (last_used + cooldown) > sys.getTime() ) active = 1;
	}


	if (active)					//if the fountain is (now) ready, heal the player
	{
		health_target	= $player1.getHealth() + heal_amount;		//calculate how much health the player will have
			if(health_target > 100) health_target = 100;		//limit the new health to max 100 (not strictly necessary for setHealth())

		$player1.setHealth(health_target);				//heal the player
		startSound("snd_heal", SND_CHANNEL_ANY, false);			//play a heal sound on the fountain, using the "snd_heal" spawnarg on the fountain

		active		= 0;						//fountain is now inactive until it has recharged
		last_used	= sys.getTime();				//keep track of when this fountain was last used
	}

	else 						//fountain is not ready; play inactive sound and abort
	{
		startSound("snd_inactive", SND_CHANNEL_ANY, false);	//play inactive sound
		setFrobable(0); sys.wait(3); setFrobable(1);		//wait 3s before allowing next frob (prevent sound spamming)
		return;							//abort
	}
}


Note:

  • If you want to access the variables of this scriptobject from elsewhere, i.e. if you want to check whether a particular fountain is "active" or not, you will need to make the variables visible.
    • The easiest way is to let the scriptobject set custom spawnargs on itself, i.e. setKey("active", "1"). Any other script can access that.
    • Alternatively, you can use the approach seen earlier when accessing "AI flags". You can create a variable where the the fountain is defined as a subcategory of entity named after the scriptobject. Example:
healing_fountain fountain = $fountain_1;		//define $fountain_1 as a variable of type "healing_fountain" (identical to the scriptobject's name)

ai guard = $guard_westwing_1;

Then:

if ( fountain.active ) sys.println(fountain + " is ready to heal the player.");

if ( guard.AI_KNOCKEDOUT ) sys.println(guard + " is knocked out.");


Recommended: the entity definition

Scriptobjects are made to be used by entities, so it definitely makes sense to create a premade entity with all the correct spawnargs already set by making an entity definition.

The entity definition allows you to:

  • Define tooltips and types for any spawnarg. If you define a spawnarg as a "model" spawnarg, it'll have a "Choose model..." button.
  • Set default values for spawnargs, making them show up in "inherited properties".
  • Set values for spawnargs in the same way as a mapper would. This makes them show up even if "inherited properties" are hidden.


Example: atdm:target_unbind, for unbinding targeted entities when triggered

This will create a small box-like entity for use in DR. When it's triggered by something, it'll run a script on all its targets that unbinds them from whatever they're bound to. It's similar to i.e. atdm:target_setteam, which changes the team of all targeted AIs when it's triggered.


The script

As an example, this script could be stored in tdm_target_unbind.script in the script folder, and #included in tdm_custom_scripts.script.

object target_unbind				//create a new scriptobject
{
	//Name the scripts belonging to this scriptobject
	void init();				//mandatory; performs initial setup, mainly to make the entity respond to trigger signals by calling unbind_targets()
	void unbind_targets();			//performs the unbinding

	//Define all variables
	entity target;				//a target of this entity	
	float i;				//used for cycling through all targets of this entity
	float delay;				//wait this amount before performing the unbind; value comes from a spawnarg
};						//semicolon needed after the closing curly bracket of a scriptobject definition

void target_unbind::init()			//initial setup; gets called automatically when this entity spawns
{
	sys.onSignal(SIG_TRIGGER, self, "unbind_targets");	//when receiving a trigger signal on this entity, call the script unbind_targets() (contained in this scriptobject)
	delay	= getFloatKey("delay");				//retrieve a spawnarg value on self. "self" is implied, since it's a scriptobject
}

void target_unbind::unbind_targets()		//performs the unbinding
{
	sys.wait(delay);			//wait for "delay" seconds before running the script
	
	for (i = 0; i < numTargets() ; i++)	//repeat for every target of this entity
	{
		target = getTarget(i);		//get the next target of this entity
		target.unbind();		//run the unbind() script event on the target
	}
}

The entity definition

An entity definition allows you to get an entity carrying this scriptobject listed in DR's "Create entity" menu, allowing you and others to easily use it in your map. Create a .txt file in the def folder and change the extension to .def, the name doesn't matter.

entityDef atdm:target_unbind
{ 
	"inherit" 			"atdm:entity_base"		//inherit from a basic entity
	"spawnclass"			"idTarget"			//use a generic spawnclass without too much inbuilt coding
	"scriptobject"			"target_unbind"			//assign this scriptobject		
	"editor_displayFolder"		"Targets"			//entity will be listed in the Targets folder in DR

	"editor_color"			"0.3 0.1 0.6"			//borrow the appearance from the trigger_relay entity (purple box)
	"editor_mins"			"-8 -8 -8"			//dimensions (bottom left corner)
	"editor_maxs"			"8 8 8"				//dimensions (top right corner)
	"editor_material"		"textures/common/trigrelay"	

	"editor_usage"			"Unbinds all targeted entities when triggered." 	//description, shown in the Create entity menu
	"editor_usage1"			"Multi-line descriptions..."
	"editor_usage2"			"... are done like this."

	"editor_float delay"		"Optional. Delay action by this amount in seconds."	//tell DR that "delay" is a float and the tooltip
	"delay"				"0"			//delay will inherit a value of 0 (shown in inherited properties)
	//"editor_setKeyValue delay"	"0"			//alternatively, delay will automatically be assigned a value of 0 as if a mapper had done it
}


Your entity is now ready to be used in DR, performing unbind() on all its targets when triggered.


Utility scripts

Utility scripts are scripts, but they're designed to be used in a similar way to script events. Their main purpose is to save time and space by compressing commonly used pieces of generic scripting into a single line.

The difference to script events is that they aren't called on an entity. Example using the sign() utility script:

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


Existing utility scripts

Various utility scripts can be found in tdm_base01.pk4 > script/tdm_util.script. Some of the most versatile have been included here:

//These scripts perform various mathematical operations

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 color "source" to color "dest"


Writing your own utility scripts

Returning a value as ouput

Remember how most scripts start with void, such as void main()? That initial void defines the data type of the script's output. If you want your script to output something useful, like many script events do, then the first step is to change this to an appropriate data type. For a mathematical script, that could be a float or a vector. For a script that should find a particular kind of entity, it could be an entity.

You can then use "return" at any point in the script to deliver a variable as the output. This also terminates the script.

Example: utility script for calculating a remainder

float remainder(float numerator, float denominator)		//this script calculates the remainder of a division, putting out the result as a float
{
	float i		= abs(numerator/denominator);		//make everything positive and find the total number of divisions
	float diff	= i - sys.floor(i);			//find the remaining partial division

	i = diff * abs(denominator);				//find remainder
	i = sys.floor(i) * sign(numerator/denominator);		//ensure this is an integer and restore the correct sign

	return i;						//return the current value of "i" as the output of this script
}

Utility script in action:

float remainder1	= remainder(22, 5);			//remainder of dividing 22 by 5; store the result (2) as a variable


Script addons for players

Script addons are additional custom scripts that players can download in order to modify their gameplay in all missions. Examples might be a script for players with arachnophobia that kills and hides all spiders at map start, or a script that automatically adds a custom item to the player's inventory at the start of every mission, such as a scroll displaying statistics about the current mission.

These are the steps in creating an addon:

  • 1) Create a .script file in the "script" folder and write your script(s) into it.
  • 2) Use tdm_user_addons.script to #include your .script file (works the same way as tdm_custom_scripts.script). You can also get your script called at map start here.
  • 3) Package your files into a .pk4, named to sort alphabetically after existing TDM .pk4's.


Example addon: kill & hide all spiders for players with arachnophobia

1) Write the script

The first thing to do is find all entities that are spiders. You will need to find a spawnarg value which is used only by spiders: I've decided to check if the entity has got an "ik_foot8" spawnarg, since having 8 legs is a unique feature of spiders.

As shown in the Special Methods section, do + while() is well suited for going through all entities that match a certain spawnarg. Once the entity has been found, we can run kill() and hide() on it:

void destroy_spiders()
{
	ai spider;

do { spider = sys.getNextEntity("ik_foot8", spider); //find the next entity which has an "ik_foot8" spawnarg (no value specified, because it doesn't matter in this case)

									//also, continue the search from the previously discovered spider entity
		
		if(spider)						//if a valid entity has been found, kill and hide it

{

			spider.kill();
			spider.hide();

}

} while (spider); //keep going for as long as getNextEntity finds valid entities

}

(Note: you might want to add a second do + while() block for finding & hiding spiders that start as ragdolls.)


2) Use tdm_user_addons.script to #include your .script file (works the same way as tdm_custom_scripts.script). You can also get your script called at map start here.

tdm_user_addons.script works the same way as tdm_custom_scripts.script, but its purpose is to be used for addons rather than FM-specific scripts. An important difference is that it also contains an empty script which gets called at map start. This can be used to get your scripts called when the map starts.

You can either create this file from scratch or extract it from tdm_base01.pk4 > tdm_user_addons.script.

First, #include your .script file. I've decided to call it tdm_arachnophobia.script:

#include "script/tdm_arachnophobia.script"

Then, if your addon needs it, let the void user_addon_init() script call your script. This works the same as the void main() script in a map .script:

void user_addon_init()			//If any of your addon scripts need to be initialised at map start, add their init function here. See the line that has been commented out with // for an example.
{
	sys.waitFrame();		//wait one frame so all entities can be spawned
	thread destroy_spiders();	//call the script to find & destroy & hide all spiders in the map
}


3) Package your files into a .pk4, named to sort alphabetically after existing TDM .pk4's.

Package your /script folder (containing tdm_user_addons.script and tdm_arachnophobia.script) into a .zip archive, then change the extension to .pk4. The name should sort alphabetically after tdm_base01.pk4, for example z_arachnophobia.pk4. This ensures that your tdm_user_addons.script overwrites the stock version, not the other way around.

You can now upload this to a file sharing website and post a link to the forums. The .pk4 doesn't need to be extracted on the user's end.


Compatibility with other addons

There can only be one tdm_user_addons.script, so if a player wants to use multiple addons these files need to get merged. It's quite straightforward: extract all addons, merge the #include lines and the contents oft the user_addon_init() script, place the "script" folder in the base installation.

Alternatively, you can do this for them for some popular combinations of addons.


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.