INTERACTABLE SYSTEM

This guide explains how to create interactive elements in your maps, such as buttons that unlock doors, levers that control gates, and pressure plates that trigger actions when stepped on.


WHAT ARE INTERACTABLES?

Interactables are map elements that respond to player actions. There are two main categories:

Triggers are things the player activates. These include buttons (press Enter to use), levers (press Enter to toggle on or off), pressure plates (activate automatically when you walk on them), key triggers (require an item), and more.

Targets are things that get controlled by triggers. These include doors, gates, lifts, vanishing platforms, and travel points.


HOW IT WORKS

Every target needs a unique name, called an ID. When you create a trigger, you tell it which target to affect by using that ID.

For example, if you have a door with id=my_door, you can create a button that says on_activate=unlock:my_door. When the player presses the button, the door unlocks.


BASIC TRIGGERS


CREATING A BUTTON

A button is activated when the player stands on it and presses Enter.

Example:
button x=10; y=5; z=0; on_activate=unlock:my_door;

A button can also span a box instead of a single tile, so Enter on any tile inside it activates the same button. Use minx, maxx, miny, maxy, minz, maxz instead of x, y, z:
button minx=10; maxx=12; miny=5; maxy=7; minz=0; maxz=0; on_activate=unlock:my_door;

Button settings:
x, y, z - The position of the button. Required unless you give minx/maxx/miny/maxy/minz/maxz instead.
minx, maxx, miny, maxy, minz, maxz - The button's box, for one that spans more than one tile. Use this instead of x, y, z.
id - A unique name for this button. Only needed if something else targets it.
sound - The sound file to play when pressed. Defaults to button1.ogg.
on_activate - What happens when the button is pressed.
single_use - Set to yes if the button can only be used once.
server_bound - Set to yes to broadcast this trigger to all players on the map. See MULTIPLAYER SYNCHRONIZATION.


CREATING A LEVER

A lever is a switch that toggles between on and off states. Each state can trigger different actions.

Example:
lever x=10; y=5; z=0; on_activate=unlock:my_door; on_deactivate=lock:my_door;

A lever can also span a box instead of a single tile, so Enter on any tile inside it throws the same lever. Use minx, maxx, miny, maxy, minz, maxz instead of x, y, z:
lever minx=10; maxx=12; miny=5; maxy=7; minz=0; maxz=0; on_activate=unlock:my_door; on_deactivate=lock:my_door;

Lever settings:
x, y, z - The position of the lever. Required unless you give minx/maxx/miny/maxy/minz/maxz instead.
minx, maxx, miny, maxy, minz, maxz - The lever's box, for one that spans more than one tile. Use this instead of x, y, z.
id - A unique name for this lever.
sound - The sound file to play when toggled. Defaults to lever1.ogg.
on_sound - Sound to play specifically when switching on.
off_sound - Sound to play specifically when switching off.
on_activate - What happens when the lever is switched on.
on_deactivate - What happens when the lever is switched off.
server_bound - Set to yes to broadcast this trigger to all players on the map. See MULTIPLAYER SYNCHRONIZATION.


CREATING A PRESSURE PLATE

A pressure plate activates automatically when the player steps on it, and deactivates when they step off.

Example:
pressure_plate minx=10; maxx=12; miny=5; maxy=7; minz=0; maxz=0; on_activate=unlock:my_door;

Pressure plate settings:
minx, maxx, miny, maxy, minz, maxz - The area the plate covers. Required.
id - A unique name for this pressure plate.
sound - The sound file to play. Defaults to plate.ogg.
on_activate - What happens when the player steps on the plate.
on_deactivate - What happens when the player steps off the plate.
server_bound - Set to yes to broadcast this trigger to all players on the map. See MULTIPLAYER SYNCHRONIZATION.


ADVANCED TRIGGERS


CREATING A KEY TRIGGER

A key trigger requires the player to have a specific item in their map inventory before it can be activated.

Example:
key_trigger x=10; y=5; z=0; required_item=golden_key; on_activate=unlock:treasure_door;

Key trigger settings:
x, y, z - The position of the trigger. Required.
required_item - The name of the item the player must have. Required.
id - A unique name for this trigger.
sound - The sound file to play when activated. Defaults to key_use.ogg.
on_activate - What happens when the trigger is activated.
consume_item - Set to yes to remove the item from the map inventory when used.
single_use - Set to yes if it can only be used once. Defaults to yes.
server_bound - Set to yes to broadcast this trigger to all players on the map. See MULTIPLAYER SYNCHRONIZATION.


CREATING A COUNTER TRIGGER

A counter trigger must be activated multiple times before it fires. Other triggers can increment or decrement it.

Example:
counter_trigger id=my_counter; required_count=3; on_activate=unlock:vault_door;

Counter trigger settings:
id - A unique name for this counter. Required.
required_count - How many times it must be incremented before firing. Required.
on_activate - What happens when the count is reached.
count_sound - Sound to play on each increment or decrement. Defaults to count.ogg.
complete_sound - Sound to play when the count is reached. Defaults to complete.ogg.
reset_on_complete - Set to yes to reset the counter after it fires, allowing it to be used again.
start_value - Where the counter begins, and where it returns to when reset. Optional. Set it higher than required_count and the counter counts down instead: it starts here, and fires when decrement actions bring it to required_count. Without it the counter starts at 0 and counts up.
min_value - The minimum value the counter can reach. Defaults to 0.
max_value - The maximum value the counter can reach. Set to -1 for no limit. Defaults to -1.
server_bound - Set to yes to broadcast this trigger to all players on the map. See MULTIPLAYER SYNCHRONIZATION.

To increment a counter from another trigger, use the increment action:
button x=10; y=5; z=0; on_activate=increment:my_counter;

To decrement a counter from another trigger, use the decrement action:
button x=12; y=5; z=0; on_activate=decrement:my_counter;

A counter that counts down, with spoken feedback on every press and the door unlocking on the last one:
counter_trigger id=presses; start_value=3; required_count=0; on_activate=unlock:door1;
button x=12; y=5; z=0; on_activate=decrement:presses,speak:[presses.current] presses left;


CREATING A TIMER TRIGGER

A timer trigger fires automatically after a delay, or repeatedly on an interval.

Example (one-shot, fires 5 seconds after map loads):
timer_trigger id=my_timer; delay_ms=5000; on_activate=unlock:timed_door; auto_start=yes;

Example (repeating, fires every 3 seconds):
timer_trigger id=alarm; delay_ms=1000; interval_ms=3000; on_activate=play_sound:alarm.ogg; auto_start=yes;

Timer trigger settings:
id - A unique name for this timer. Required.
delay_ms - How long to wait before the first fire, in milliseconds. Required.
interval_ms - How long between repeated fires. Set to 0 for one-shot. Defaults to 0.
on_activate - What happens when the timer fires.
auto_start - Set to yes to start the timer automatically when the map loads.
single_use - Set to yes so the timer never fires again after its first activation, even if restarted.

Timer triggers do not support server_bound. To sync a timer's effects to other players, have a server-bound button/lever/etc. call start_timer/stop_timer on it instead.

To start or stop a timer from another trigger:
button x=10; y=5; z=0; on_activate=start_timer:my_timer;
button x=12; y=5; z=0; on_activate=stop_timer:my_timer;


LOGIC GATES


CREATING AN AND GATE

An AND gate fires only when ALL of its input triggers are active at the same time.

Example:
lever id=lever1; x=10; y=5; z=0;
lever id=lever2; x=12; y=5; z=0;
and_gate id=both_levers; inputs=lever1,lever2; on_activate=unlock:vault_door;

The door only unlocks when both levers are switched on. If either lever is switched off, the AND gate fires its on_deactivate action.

AND gate settings:
id - A unique name for this gate. Required.
inputs - Comma-separated list of trigger IDs that must all be active. Required.
on_activate - What happens when all inputs become active.
on_deactivate - What happens when any input becomes inactive.
latch - Whether this gate keeps counting as active, when another gate uses it as an input, after its own inputs stop being satisfied. Defaults to yes. See CHAINING GATES TOGETHER.
server_bound - Set to yes to broadcast this trigger to all players on the map. See MULTIPLAYER SYNCHRONIZATION.


CREATING AN OR GATE

An OR gate fires when ANY of its input triggers is active.

Example:
button id=btn1; x=10; y=5; z=0;
button id=btn2; x=12; y=5; z=0;
or_gate id=any_button; inputs=btn1,btn2; on_activate=unlock:my_door;

The door unlocks when either button is pressed.

OR gate settings:
id - A unique name for this gate. Required.
inputs - Comma-separated list of trigger IDs, any of which can fire this. Required.
on_activate - What happens when any input becomes active.
on_deactivate - What happens when all inputs become inactive.
latch - Whether this gate keeps counting as active, when another gate uses it as an input, after its own inputs stop being satisfied. Defaults to yes. See CHAINING GATES TOGETHER.
server_bound - Set to yes to broadcast this trigger to all players on the map. See MULTIPLAYER SYNCHRONIZATION.


CHAINING GATES TOGETHER

A gate's ID can appear in another gate's inputs, which lets you build up more complicated logic out of simple pieces. When you do that, there's a question worth knowing the answer to: once the inner gate has fired, does it keep counting as an active input forever, or does it stop counting as soon as its own inputs stop being satisfied?

By default it keeps counting. An AND gate that has fired once stays an active input to whatever is watching it, even after its own levers are switched back off. This is how gates have always behaved, so maps built before this setting existed keep working exactly as they did.

If you want the other behaviour, set latch=no on the inner gate. It will then report what its inputs say right now, so it stops counting as active the moment they stop being satisfied.

Example:
lever id=lever1; x=10; y=5; z=0;
lever id=lever2; x=12; y=5; z=0;
and_gate id=both_levers; inputs=lever1,lever2; latch=no;
and_gate id=final; inputs=both_levers,lever3; on_activate=unlock:vault_door;

With latch=no, the player has to hold all three levers on at the same time. Without it, flipping the first two on and back off still leaves both_levers counting toward the final gate.

This only affects how a gate is read by another gate. It doesn't change when on_activate and on_deactivate fire, and it doesn't change what [gate_id.satisfied] or [gate_id.activated] report to a condition. Conditional gates have the same setting, but default it the other way round, to latch=no, since tracking a condition as it changes is the entire point of them.


CREATING A CONDITIONAL GATE

AND and OR gates only ask whether their inputs are on or off. A conditional gate asks a full question instead, and it keeps asking: it checks its condition every frame and fires the moment the answer turns from no to yes.

The condition is written exactly the way conditions are written for the if action, so everything under CONDITIONAL ACTIONS below applies here too, including [element.property] references, the comparison operators, ! for negation, and && and || for combining.

Example:
counter_trigger id=score; required_count=999;
conditional_gate id=score_watch; condition="[score.current]>=10"; on_activate=unlock:vault_door; on_deactivate=lock:vault_door;

The door unlocks as soon as the score counter reaches 10, no matter what changed it, and locks again if the score drops back below 10.

This is the difference between a conditional gate and an if action. An if action is only evaluated at the instant some other trigger runs it, so something has to fire in order for the condition to be looked at. A conditional gate watches on its own. Reach for it when the value can change without a trigger firing, and reach for if when a trigger is already doing the work:

button x=10; y=5; z=0; on_activate=increment:score,if:[score.current]>=10:unlock:vault_door;
conditional_gate id=carrying; condition="[inventory.torch]>0&&[inventory.flint]>0"; on_activate=speak:You could light something with these;

Note that a counter with its own required_count already fires at its threshold, so you do not need a conditional gate just to watch a counter you control. Use one when the threshold is not the counter's own, when you want several different thresholds watched separately, or when the value belongs to something that has no threshold of its own.

Conditional gate settings:
id - A unique name for this gate. Required.
condition - The condition expression to watch. Required. Quote it if it contains spaces or semicolons.
on_activate - What happens when the condition becomes true.
on_deactivate - What happens when the condition becomes false again.
single_use - Set to yes to have the gate fire only once and then stop watching.
latch - Whether this gate keeps counting as active, when another gate uses it as an input, after its condition stops holding. Defaults to no, the opposite of the AND and OR gates. See CHAINING GATES TOGETHER above.
server_bound - Set to yes to broadcast this trigger to all players on the map. See MULTIPLAYER SYNCHRONIZATION.

A conditional gate can be used as an input to an AND or OR gate, and by default it counts as active for exactly as long as its condition holds.

The gate fires once each time its condition becomes true, not repeatedly while it stays true. If the condition later stops holding, on_deactivate fires and the gate arms itself again, so it can fire once more the next time the condition comes back. Use single_use if you want it to fire only the very first time and never again.


CREATING A SEQUENCE TRIGGER

A sequence trigger requires inputs to arrive in a specific order, like a puzzle combination. Just list the IDs of the triggers you want it to watch in the sequence= setting. Any button, lever, pressure plate, key trigger, counter, timer, gate, or even another sequence trigger whose ID appears in that list will automatically be treated as input the moment it activates. You don't need to add anything to the watched triggers themselves.

Example:
button id=red; x=10; y=5; z=0;
button id=blue; x=12; y=5; z=0;
button id=green; x=14; y=5; z=0;
sequence_trigger id=puzzle; sequence=red,blue,green; on_activate=unlock:secret_door;

The door only unlocks if the player presses red, then blue, then green in that exact order. Pressing a wrong trigger from the list resets the sequence (unless reset_on_fail=no). Triggers whose ID isn't in the sequence= list are ignored entirely, even if the player activates them in between.

If you need to feed a sequence a token that doesn't match the firing trigger's own ID (for example, routing one physical button to different logical steps in two different sequences), you can still do so manually with the sequence_input action instead of relying on the automatic ID watch:
button x=10; y=5; z=0; on_activate=sequence_input:[puzzle,red];

The sequence_input action format is: sequence_input:[sequence_id,token]
The first part is the sequence trigger's ID. The second part is the token to feed it, as if a trigger with that ID had activated. The brackets are required, the same as with teleport: on_activate is comma-split into separate actions before any one action is parsed, so the sequence_id,token pair would be mistaken for two different actions without them.
Note: don't combine both approaches for the same step. If a trigger's own ID is already in the sequence= list, adding an explicit sequence_input action for that same ID as well will feed the sequence twice per press.

Sequence trigger settings:
id - A unique name for this sequence. Required.
sequence - Comma-separated list of trigger IDs in the order they must be activated. Required.
on_activate - What happens when the sequence is completed correctly.
fail_sound - Sound to play when the wrong trigger is pressed. Defaults to sequence_fail.ogg.
progress_sound - Sound to play when the correct trigger is pressed. Defaults to sequence_progress.ogg.
complete_sound - Sound to play when the sequence is completed. Defaults to sequence_complete.ogg.
reset_on_fail - Set to no to keep progress even when wrong input is received. Defaults to yes.
server_bound - Set to yes to broadcast this trigger to all players on the map. See MULTIPLAYER SYNCHRONIZATION.


INPUTS

Every trigger above reports one fact. It was pressed, or thrown, or stepped on. An input reports a value as well. The player types something or picks something, that answer is stored on the input, and the rest of the map can read it back as [id.value] anywhere a reference works.

Inputs are otherwise ordinary triggers. They take an id, they run on_activate when they are answered, they feed sequence triggers, and gates and links treat them exactly like a button.

One thing is different. An input does not have to be anywhere. Every other trigger needs a position, but an input with no coordinates at all has no place in the world and only opens when something runs activate on it. That is how you put a prompt behind a button, a pressure plate or a timer instead of underfoot.


CREATING A TEXT INPUT

A text input asks the player for a line of text. Standing on it and pressing Enter opens a box, and what they type lands in [id.value].

Example (a keypad that unlocks a door):
text_input x=10; y=5; z=0; id=keypad; prompt="Enter the code."; answer=7749; on_correct=unlock:vault; on_incorrect=play_sound:error1.ogg;

Example (a prompt opened by a button rather than by standing anywhere):
text_input id=name_prompt; prompt="What should we call you?";
button x=3; y=3; z=0; on_activate=activate:name_prompt;

Example (reading the answer back somewhere else):
text_input x=10; y=5; z=0; id=guestbook; prompt="Sign the book."; on_activate=speak:Thank you\, [guestbook.value];

Set answer and the input grades itself. [id.correct] says whether the last try matched, and on_correct or on_incorrect runs alongside on_activate, so whatever a right and a wrong answer have in common only needs saying once. Without answer, the input simply collects text and on_activate is all that runs.

Text input settings:
x, y, z - Where the input is. Optional. Give minx/maxx/miny/maxy/minz/maxz instead for a box, the same way a button can span one. Leave all of them out and the input has no position at all and must be opened by an action.
id - A unique name for this input. Needed to read [id.value], and to open it by action.
title - The title of the window that opens. Defaults to "Input".
prompt - The label on the box the player types into. Defaults to "Enter text".
default_value - What the box starts with.
max_length - The longest answer accepted. 0 or absent for no limit.
min_length - The shortest answer accepted. A shorter one is refused and the box stays open.
allow_empty - Set to yes to accept an empty answer. Defaults to no.
multiline - Set to yes to accept more than one line.
secure - Set to yes to hide what is typed, for a password.
allowed_chars - When set, only these characters can be typed.
disallowed_chars - When set, these characters cannot be typed.
invalid_text - What to say when an answer is refused for being too short or empty. Defaults to "Invalid input."
answer - The answer to grade against. Leave it out and the input does no grading.
case_sensitive - Set to yes to compare the answer exactly. Defaults to no.
max_attempts - How many answers this input will take before it stops opening. 0 or absent for unlimited.
exhausted_text - What to say to somebody who works an input that has run out of attempts, or a spent single_use one. Says nothing when left out.
sound - Played as the prompt opens.
submit_sound - Played when an answer is accepted.
cancel_sound - Played when the player backs out.
correct_sound, incorrect_sound - Played on a right and a wrong answer.
speak_result - Set to yes to speak the answer back after submitting.
clear_on_submit - Set to yes to forget the answer once it has been handed over. The value stays readable for the frame after submitting, which is long enough for on_activate, on_correct and a conditional_gate watching it to see it. An action given a delay will find it already gone. Whether the answer was correct is kept either way, so [id.correct] still reads true after the text itself has been forgotten.
on_activate - What happens when an answer is given, right or wrong. The builder calls this field On Submit.
on_correct, on_incorrect - What happens on a right and a wrong answer. Only used when answer is set.
on_cancel - What happens when the player backs out without answering.
single_use - Set to yes to let the input be answered only once.
server_bound - Set to yes to send the answer to everyone else on the map. See MULTIPLAYER SYNCHRONIZATION.


CREATING A MENU INPUT

A menu input asks the player to pick from a list. The chosen label lands in [id.value] and its position in [id.index].

Example (a lift panel where each floor drives its own element):
menu_input x=10; y=5; z=0; id=panel; intro_text="Where to?"; options="Ground floor=lift_down,First floor=lift_up,Nothing thanks";

Example (a plain list of choices, all handled in one place):
menu_input x=4; y=4; z=0; id=colour; options="Red,Green,Blue"; on_activate=speak:You chose [colour.value];

options is the list, comma separated. Write an entry as Label=target and choosing it also activates that element, which is the short way to give each choice its own behaviour without writing a condition. A plain Label leaves everything to on_activate and [id.value]. A label that needs a comma of its own can escape it as \, the same way an action can.

Menu input settings:
x, y, z - Where the input is. Optional, exactly as on a text input.
options - The list of choices. Required.
id - A unique name for this input.
title - The title of the menu. Used as the spoken intro when intro_text is left out.
intro_text - Spoken as the menu opens.
default_index - Which option to start on, counting from 0. Defaults to the first.
remember - Set to yes to reopen on the last choice instead of on default_index.
wrap - Whether moving past either end comes round to the other. Leave it out and each player's own menu setting decides, which is usually what you want.
allow_cancel - Set to no to stop the menu being escaped out of. Defaults to yes.
speak_result - Set to yes to speak the choice back after picking.
exhausted_text - What to say to somebody who works a spent single_use menu. Says nothing when left out.
sound - Played as the menu opens.
click_sound, select_sound, close_sound - The menu's own movement, selection and abort sounds. Each falls back to the player's usual menu sounds when left out.
submit_sound - Played after a choice is taken.
cancel_sound - Played when the player backs out.
on_activate - What happens when a choice is made. The builder calls this field On Submit.
on_cancel - What happens when the player backs out without choosing.
single_use - Set to yes to let the menu be answered only once.
server_bound - Set to yes to send the choice to everyone else on the map. See MULTIPLAYER SYNCHRONIZATION.


CREATING A FORM INPUT

A form input asks several things at once, as one audio form. Text boxes, checkboxes, lists, sliders and buttons, filled in however the player likes and handed over together when they press OK.

Each control has a name, and that name is how the answer is read back. A form with a name box called playername and a colour list called colour gives you [signup.playername] and [signup.colour].

Example (a sign-up sheet):
form_input x=10; y=5; z=0; id=signup; title="Sign up"; controls="text:playername:Your name::,list:colour:Favourite colour:Red:[Red,Green,Blue],checkbox:agree:I agree to the rules:no:"; on_activate=speak:Thanks [signup.playername];

Build the controls in the builder rather than by hand. The Controls field opens an editor with Add Control, Edit Control, Remove Control, Move Up, Move Down and Preview, the same way the action builder works. Preview puts the real form on screen so you can hear the labels and the tab order before saving anything.

Control types:
text - A box the player types into. Its extra setting is an optional maximum length.
number - The same, but only digits can be typed.
checkbox - Reads back as true or false. Its default is yes or no.
list - A list of choices. Its extra setting is the options, comma separated.
slider - A number the player scrolls through. Its extra setting is minimum, maximum and step, comma separated.
button - Carries no answer. Pressing it runs its own actions there and then without closing the form, which is what a Test button next to a volume slider is for. Its actions see whatever is currently filled in, not what was there when the form opened.

Form input settings:
x, y, z - Where the input is. Optional, exactly as on a text input.
controls - The controls themselves. Required.
id - A unique name for this form. Needed to read any answer back.
title - The title of the window that opens.
ok_label, cancel_label - Rename the two buttons at the bottom. They default to OK and Cancel.
speak_result - Set to yes to read every answer back after submitting.
exhausted_text - What to say to somebody who works a spent single_use form.
sound, submit_sound, cancel_sound - Played on opening, on submitting and on backing out.
on_activate - What happens when the form is submitted. The builder calls this field On Submit.
on_cancel - What happens when the player backs out without submitting.
single_use - Set to yes to let the form be filled in only once.
server_bound - Set to yes to send every answer to everyone else on the map.

Reading a form back:
[form_id.control_name] - Whatever that control was left holding
[form_id.submitted] - Whether it has ever been submitted (true/false)
[form_id.cancelled] - Whether the last visit was backed out of (true/false)
[form_id.activated] - Whether it has been submitted (true/false)
[form_id.count] - How many controls it has
[form_id.enabled] and [form_id.disabled] - Whether it is enabled

Those six names are reserved. A control called submitted or count cannot be read back, since the built-in meaning wins, so name controls after what they hold and the two never collide.

A control can be written as well as read, with the set action, so one form can seed another. set:signup.colour=Red fills that control in without the player having touched it.


MAP ITEMS


CREATING A MAP ITEM

A map item is a physical item placed in the world that the player can pick up. By default the player picks it up by standing on it and pressing Enter. You can also make it pick up automatically when the player walks over it.

When picked up, the item is added to the player's map inventory and any on_pickup actions are executed.

Example (keycard the player presses Enter to pick up):
map_item x=10; y=5; z=0; item_name=keycard; on_pickup=unlock:security_door,speak:You pick up the keycard;

Example (coin picked up automatically when walked over):
map_item x=7; y=3; z=0; item_name=coin; auto_pickup=yes;

Example (a note on the floor that speaks its contents — no inventory item):
map_item x=12; y=8; z=0; on_pickup=speak:The note reads\: access code is 7749;

Map item settings:
x, y, z - The position of the item. Required.
item_name - The name of the item added to the player's map inventory on pickup. Leave empty to only run on_pickup actions with no inventory change.
item_amount - How many of the item to give. Defaults to 1.
id - A unique name for this item. Only needed if something else targets it or you use element references.
sound - The sound file to play on pickup. Defaults to item_pickup.ogg.
loop_sound - A sound file to loop continuously while the item is on the map. Stops when the item is picked up. Optional.
on_pickup - What happens when the item is picked up. Uses the same action format as on_activate.
pickup_message - What is spoken when the item is picked up, in place of the usual "1 key received". Optional. It can include element references, such as pickup_message="You now have [inventory.key] keys".
auto_pickup - Set to yes to pick up automatically when the player walks onto the tile. Defaults to no (requires Enter).
single_use - Set to no to allow the item to be picked up again after being reset. Defaults to yes.


TARGETS


CREATING A LOCKABLE DOOR

To make a door that starts locked, add the locked setting.

Example:
door id=my_door; start_x=10; start_y=5; start_z=0; end_x=10; end_y=5; end_z=3; locked=yes; speed=100; loop=none.ogg; move=door_move.ogg; open=door_open.ogg; close=door_close.ogg; automatic=no;

Additional settings for triggerable doors:
id - A unique name so triggers can reference this door. Required for triggers.
locked - Set to yes to make the door start locked.
unlock_sound - Sound to play when the door is unlocked.
lock_sound - Sound to play when the door is locked.


CREATING A DISABLEABLE LIFT

To make a lift that starts disabled and won't move until enabled, add the disabled setting.

Example:
lift id=my_lift; minx=10; maxx=12; miny=5; maxy=7; minz=0; maxz=10; speed=100; surface=metal; disabled=yes;

Additional settings for triggerable lifts:
id - A unique name so triggers can reference this lift.
disabled - Set to yes to make the lift start disabled.
enable_sound - Sound to play when the lift is enabled.


CREATING A TRIGGER-CONTROLLED VANISHING PLATFORM

Vanishing platforms normally cycle on their own timer. To control them with triggers instead, add trigger_controlled=yes.

Example:
vanishing_platform minx=10; maxx=12; miny=5; maxy=7; minz=0; maxz=0; surface=glass; id=my_platform; trigger_controlled=yes;
button x=8; y=5; z=0; on_activate=show:my_platform;
button x=14; y=5; z=0; on_activate=hide:my_platform;

Additional settings for triggerable vanishing platforms:
id - A unique name so triggers can reference this platform.
trigger_controlled - Set to yes to stop automatic cycling and only respond to triggers.


CREATING A DISABLEABLE TRAVEL POINT

Travel points can be disabled so they don't work until enabled by a trigger.

Example:
travelpoint minx=10; maxx=12; miny=5; maxy=7; minz=0; maxz=0; dest_x=0; dest_y=0; dest_z=0; dest_map=secret_area; dest_text=You found the secret exit!; id=secret_exit; disabled=yes;
button x=8; y=5; z=0; on_activate=enable:secret_exit;

Additional settings for triggerable travel points:
id - A unique name so triggers can reference this travel point.
disabled - Set to yes to make the travel point start disabled.


CREATING A CONTROLLABLE SOURCE

Audio sources (ambient sounds, music, etc.) can be controlled by triggers if they have an ID. You can pause, resume, toggle, or restart them.

To give a source an ID, add the id parameter to any source, source3d, timed_source or source_playlist element.

Example:
source minx=10; maxx=20; miny=5; maxy=15; minz=0; maxz=0; path=music/radio.ogg; volume=0; pitch=100; id=my_radio;
button x=10; y=10; z=0; on_activate=toggle:my_radio;

A source is playing from the moment the map loads, so play and resume have nothing to start unless something paused it first. For a source that is meant to wait to be switched on, add paused=true. It comes up silent and stays that way until a trigger resumes it.

Example (music that only starts once the right answer is given):
source minx=10; maxx=20; miny=5; maxy=15; minz=0; maxz=0; path=music/radio.ogg; volume=0; pitch=100; id=my_radio; paused=true;
text_input x=1; y=0; z=0; prompt="What genre is it?"; answer=phonk; on_correct=resume:my_radio; id=genre;

paused=true needs an id. Without one nothing can ever reach the source to start it, so the map reports a warning rather than leaving you with a source that simply never makes a sound.

On a timed source, paused means it does not fire at all until it is resumed, rather than firing silently.

Note: Excludable sources (xsource) use their ID for a different purpose (defining exclusion zones where they stop playing) and cannot be controlled via the interactable system. Ambiences, streams and music playlists are not sources and cannot be controlled either.


AVAILABLE ACTIONS

When you write on_activate or on_deactivate, you specify an action and a target. The format is action:target_id.

Actions for doors and gates:
unlock:target_id - Unlocks the door or gate so it can be opened.
lock:target_id - Locks the door or gate so it cannot be opened.
toggle:target_id - Opens it if closed, or closes it if open.

Actions for lifts:
activate:target_id - Starts the lift moving.
deactivate:target_id - Stops the lift.
enable:target_id - Enables a disabled lift.
disable:target_id - Disables the lift so it stops working.

Actions for triggers:
activate:target_id - Fires the trigger's on_activate actions.
deactivate:target_id - Fires the trigger's on_deactivate actions.
enable:target_id - Enables a disabled trigger.
disable:target_id - Disables the trigger.

Actions for counters:
increment:target_id - Adds one to the counter's count (won't exceed max_value if set).
decrement:target_id - Subtracts one from the counter's count (won't go below min_value).

Actions for timers:
start_timer:target_id - Starts the timer.
stop_timer:target_id - Stops the timer.

Actions for vanishing platforms:
show:target_id - Makes the platform appear.
hide:target_id - Makes the platform disappear.
toggle:target_id - Toggles between visible and invisible.

Actions for travel points:
enable:target_id - Enables the travel point.
disable:target_id - Disables the travel point.

Actions for sources:
pause:target_id - Pauses the audio source.
resume:target_id - Resumes a paused audio source.
play:target_id - Same as resume.
toggle:target_id - Toggles between paused and playing.
restart:target_id - Restarts the audio from the beginning.

Actions for sequence triggers:
sequence_input:[sequence_id,token] - Manually feeds a token to a sequence trigger, as if a trigger with that ID had activated. Only needed when you want to feed a token that differs from the firing trigger's own ID; triggers whose ID is already listed in the sequence trigger's sequence= setting are watched automatically and don't need this. The brackets around sequence_id,token are required, see CREATING A SEQUENCE TRIGGER above.

Actions for text inputs, menu inputs and form inputs:
activate:target_id - Opens the input and asks the player. It does not run on_activate. Only an actual answer does that.
open:target_id - The same thing, said plainly.
clear:target_id - Forgets whatever was last answered, so the input can be asked again from scratch. reset does the same.
enable:target_id - Enables a disabled input.
disable:target_id - Disables the input so it cannot be opened.

Actions for dialogs (a dlg with an id; see the map syntax glossary):
open:target_id - Opens the dialog for the player. A dialog with activate_only=true opens only this way.
activate:target_id - The same thing.

Actions for map items:
collect:target_id - Triggers the item's pickup (gives inventory item and runs on_pickup actions).
enable:target_id - Enables a disabled item so it can be picked up.
disable:target_id - Disables the item so it cannot be picked up.
reset:target_id - Clears the collected state so a single_use item can be picked up again.

Special actions (no target needed):
play_sound:filename.ogg - Plays a sound effect.
speak:Your message here - Announces text to the player via speech.
teleport:[x,y,z] - Moves the player to the specified coordinates. The brackets are required: when this is combined with other actions in the same on_activate/on_deactivate string, those actions are separated by commas, so the x,y,z coordinates must be bracketed or they'd be mistaken for separate actions. Example: on_activate=play_sound:click.ogg,teleport:[10,20,0];


ELEMENT REFERENCES

You can access element properties dynamically using bracket notation: [element_id.property]

This works in three ways:
1. Dynamic text in speak messages
2. Conditional actions
3. Copying values between elements

Available properties by element type:

Counter triggers:
[counter_id.current] - Current count value
[counter_id.min_value] or [counter_id.min] - Minimum value
[counter_id.max_value] or [counter_id.max] - Maximum value
[counter_id.required_count] or [counter_id.req] - Required count to activate
[counter_id.start_value] or [counter_id.start] - The value the counter starts at and resets to
[counter_id.activated] - Whether it has been activated (true/false)
[counter_id.enabled] - Whether it is enabled (true/false, equivalent to [counter_id.disabled]==false)
[counter_id.disabled] - Whether it is disabled (true/false, equivalent to [counter_id.enabled]==false)

Buttons:
[button_id.activated] - Whether it has been pressed (true/false)
[button_id.enabled] - Whether it is enabled (true/false, equivalent to [button_id.disabled]==false)
[button_id.disabled] - Whether it is disabled (true/false, equivalent to [button_id.enabled]==false)

Levers:
[lever_id.state] - Current state (true=on, false=off)
[lever_id.activated] - Whether it has been activated (true/false)
[lever_id.enabled] - Whether it is enabled (true/false, equivalent to [lever_id.disabled]==false)
[lever_id.disabled] - Whether it is disabled (true/false, equivalent to [lever_id.enabled]==false)

Pressure plates:
[plate_id.player_on] - Whether player is currently on the plate (true/false)
[plate_id.activated] - Whether it has been activated (true/false)
[plate_id.enabled] - Whether it is enabled (true/false, equivalent to [plate_id.disabled]==false)
[plate_id.disabled] - Whether it is disabled (true/false, equivalent to [plate_id.enabled]==false)

Timer triggers:
[timer_id.running] - Whether timer is currently running (true/false)
[timer_id.delay_ms] - The initial delay in milliseconds
[timer_id.interval_ms] - The repeat interval in milliseconds
[timer_id.enabled] - Whether it is enabled (true/false, equivalent to [timer_id.disabled]==false)
[timer_id.disabled] - Whether it is disabled (true/false, equivalent to [timer_id.enabled]==false)

AND gates:
[gate_id.satisfied] or [gate_id.completed] - Whether all inputs are satisfied (true/false)
[gate_id.activated] - Whether it has been activated (true/false)
[gate_id.enabled] - Whether it is enabled (true/false, equivalent to [gate_id.disabled]==false)
[gate_id.disabled] - Whether it is disabled (true/false, equivalent to [gate_id.enabled]==false)

OR gates:
[gate_id.satisfied] or [gate_id.completed] - Whether any input is satisfied (true/false)
[gate_id.activated] - Whether it has been activated (true/false)
[gate_id.enabled] - Whether it is enabled (true/false, equivalent to [gate_id.disabled]==false)
[gate_id.disabled] - Whether it is disabled (true/false, equivalent to [gate_id.enabled]==false)

Conditional gates:
[gate_id.satisfied] or [gate_id.completed] - Whether the condition is true right now (true/false)
[gate_id.condition] - The condition expression the gate is watching
[gate_id.activated] - Whether it has been activated (true/false)
[gate_id.enabled] - Whether it is enabled (true/false, equivalent to [gate_id.disabled]==false)
[gate_id.disabled] - Whether it is disabled (true/false, equivalent to [gate_id.enabled]==false)

Note the difference between satisfied and activated on any of the three gates. satisfied is the live answer, and goes back to false when the gate's inputs or condition stop holding. activated means the gate has fired at least once at some point, and stays true afterwards. When you feed one gate into another as an input, the receiving gate reads the live state, so a gate that stops being satisfied correctly stops counting as an active input.

Sequence triggers:
[sequence_id.current_step] - Current step in the sequence (0-indexed)
[sequence_id.activated] - Whether the sequence has been completed (true/false)
[sequence_id.enabled] - Whether it is enabled (true/false, equivalent to [sequence_id.disabled]==false)
[sequence_id.disabled] - Whether it is disabled (true/false, equivalent to [sequence_id.enabled]==false)

Key triggers:
[key_id.activated] - Whether it has been activated (true/false)
[key_id.enabled] - Whether it is enabled (true/false, equivalent to [key_id.disabled]==false)
[key_id.disabled] - Whether it is disabled (true/false, equivalent to [key_id.enabled]==false)

Text inputs:
[input_id.value] or [input_id.text] - What the player last typed
[input_id.length] - How many characters that was
[input_id.answer] - The answer being graded against, if one is set
[input_id.correct] - Whether the last answer matched (true/false)
[input_id.attempts] - How many answers have been given
[input_id.max_attempts] - How many will be taken, or 0 for unlimited
[input_id.submitted] - Whether it has ever been answered (true/false)
[input_id.cancelled] - Whether the last visit was backed out of (true/false)
[input_id.activated] - Whether it has been answered (true/false)
[input_id.enabled] - Whether it is enabled (true/false, equivalent to [input_id.disabled]==false)
[input_id.disabled] - Whether it is disabled (true/false, equivalent to [input_id.enabled]==false)

Menu inputs:
[input_id.value] or [input_id.text] - The label the player last chose
[input_id.selected] - That option's target, or its label when it names none
[input_id.index] - Its position, counting from 0, or -1 before anything has been chosen
[input_id.count] - How many options there are
[input_id.submitted] - Whether it has ever been answered (true/false)
[input_id.cancelled] - Whether the last visit was backed out of (true/false)
[input_id.activated] - Whether it has been answered (true/false)
[input_id.enabled] - Whether it is enabled (true/false, equivalent to [input_id.disabled]==false)
[input_id.disabled] - Whether it is disabled (true/false, equivalent to [input_id.enabled]==false)

An input's value can be written as well as read, with the set action. set:my_input.value=hello seeds a text input without the player having typed anything, set:my_input.answer=[code.value] changes what it grades against, and on a menu set:my_panel.index=2 picks an option outright, exactly as though the player had chosen it. set:my_panel.options="Red,Green" replaces the whole list, for a menu whose choices depend on what has happened so far.

Doors and gates:
[door_id.locked] - Whether it is locked (true/false, equivalent to [door_id.unlocked]==false)
[door_id.unlocked] - Whether it is unlocked (true/false, equivalent to [door_id.locked]==false)
[door_id.is_moving] - Whether it is currently moving (true/false)

Lifts:
[lift_id.enabled] - Whether it is enabled (true/false, equivalent to [lift_id.disabled]==false)
[lift_id.disabled] - Whether it is disabled (true/false, equivalent to [lift_id.enabled]==false)
[lift_id.waiting] - Whether it is waiting at an endpoint (true/false)
[lift_id.direction] - Current direction ("up" or "down")
[lift_id.current_z] - Current Z position (3D mode)
[lift_id.current_y] - Current Y position (2D mode)
[lift_id.player_on_lift] or [lift_id.player_on] - Whether player is on the lift (true/false)

Vanishing platforms:
[platform_id.visible] or [platform_id.platform_visible] - Whether it is visible (true/false)
[platform_id.enabled] - Whether it is enabled (true/false, equivalent to [platform_id.disabled]==false)
[platform_id.disabled] - Whether it is disabled (true/false, equivalent to [platform_id.enabled]==false)

Travelpoints:
[travelpoint_id.enabled] - Whether it is enabled (true/false, equivalent to [travelpoint_id.disabled]==false)
[travelpoint_id.disabled] - Whether it is disabled (true/false, equivalent to [travelpoint_id.enabled]==false)

Map items:
[item_id.collected] - Whether the item has been picked up (true/false)
[item_id.enabled] - Whether it is enabled (true/false, equivalent to [item_id.disabled]==false)
[item_id.disabled] - Whether it is disabled (true/false, equivalent to [item_id.enabled]==false)
[item_id.item_name] - The name of the inventory item
[item_id.item_amount] - The amount of the inventory item

The player's inventory:
[inventory.item name] - How many of that item the player is currently holding, as a number

Unlike everything above, inventory is not an element you place. It is a reserved id that answers for whatever the player is carrying, so an item name goes where a property name normally would. An item they have never picked up, or have spent all of, reads as 0, so you can always compare against it without checking first.

[inventory.keycard]>0 - They are holding at least one keycard
[inventory.coin]>=5 - They are holding five or more coins
![inventory.keycard] - They are holding no keycards at all

Item names may contain spaces, as in [inventory.gold coin]. Only the first dot separates the id from the item name, so an item name with a dot in it cannot be read this way.

This is the map inventory, the one map items fill and that is emptied when a map loads (see keepitems in the map syntax glossary). Your main inventory is account-wide and deliberately cannot be read from a map.

Because a pickup is collectable again after the map reloads, this is what you want for gating an item the player may already be carrying:

item id=key1; x=10; y=5; z=0; item_name=keycard; on_activate=speak:You already have this;
button x=0; y=0; z=0; on_activate=if:[inventory.keycard]>0:disable:key1;


DYNAMIC TEXT IN SPEAK

You can include element values in speak messages:

button x=10; y=5; z=0; on_activate=speak:Your score is [score.current] out of [score.max_value];

The values are resolved when the action executes, so they show the current state.


CONDITIONAL ACTIONS

You can make actions only execute when a condition is met using the if action.

Basic syntax: if:[condition]:action:target[:delay]

Comparison operators: > < >= <= == !=

Logical operators: && (AND), || (OR)

You can also use truthy checks without a comparison operator. The condition is true if the value is "true", "yes", "on", or any number other than zero.

Numbers other than 1 counting as true is a fix rather than a rule change. Only the literal "1" used to pass, so if:[counter.current] was false for a counter sitting on 2, and if:[inventory.coin] would have been false for a player holding two coins. Anything that reads as a number now answers the way you would expect, and zero is still false.

Use the ! prefix to negate a condition.

Basic examples:
button x=10; y=5; z=0; on_activate=if:[keys.current]>=3:unlock:vault_door;
button x=10; y=5; z=0; on_activate=if:[score.current]>10:speak:You have a high score!;
button x=10; y=5; z=0; on_activate=if:[lever.enabled]:speak:The lever is enabled;
button x=10; y=5; z=0; on_activate=if:![door.locked]:speak:The door is already unlocked;
button x=10; y=5; z=0; on_activate=if:[lever1.state]&&[lever2.state]:unlock:vault_door;
button x=10; y=5; z=0; on_activate=if:[door.locked]||[gate.locked]:speak:Something is still locked;


MULTIPLE ACTIONS IN A SINGLE IF

You can execute multiple actions when a condition is met by separating them with &.

Syntax: if:[condition]:action1:target1&action2:target2&action3:target3

Each action can have its own delay: if:[condition]:action1:target1:delay1&action2:target2:delay2

Examples:
button x=10; y=5; z=0; on_activate=if:[my_lift.enabled]==true:disable:my_lift&enable:my_lift2;
button x=10; y=5; z=0; on_activate=if:[keys.current]>=3:unlock:vault_door&speak:Access granted&play_sound:success.ogg;
button x=10; y=5; z=0; on_activate=if:[score.current]>=10:unlock:door1:500&unlock:door2:1000&speak:Both doors unlocking:0;


IF-ELSE ACTIONS

You can specify actions to execute when the condition is NOT met using the | separator for else.

Syntax: if:[condition]:then_actions|else_actions

Examples:
button x=10; y=5; z=0; on_activate=if:[door.locked]==true:unlock:door|lock:door;
button x=10; y=5; z=0; on_activate=if:[my_lift.enabled]==true:disable:my_lift&enable:my_lift2|enable:my_lift&disable:my_lift2;
lever x=5; y=5; z=0; on_activate=if:[score.current]>=10:speak:You passed!|speak:Keep trying!;


IF-ELSEIF-ELSE ACTIONS

For complex branching logic, you can add multiple elseif conditions. Each elseif block starts with a condition (beginning with [ or !).

Syntax: if:[condition1]:actions1|[condition2]:actions2|[condition3]:actions3|else_actions

The system checks conditions in order:
1. If condition1 is true, execute actions1
2. Else if condition2 is true, execute actions2
3. Else if condition3 is true, execute actions3
4. Else execute else_actions (if the last block doesn't start with [ or !, it's the else)

Examples:
button x=10; y=5; z=0; on_activate=if:[counter.current]>=10:unlock:door1&speak:Level 3!|[counter.current]>=5:unlock:door2&speak:Level 2!|[counter.current]>=1:speak:Level 1!|speak:No progress yet;

button x=10; y=5; z=0; on_activate=if:[score.current]>=100:speak:Perfect score!|[score.current]>=75:speak:Great job!|[score.current]>=50:speak:Good effort!|speak:Try again;

lever x=5; y=5; z=0; on_activate=if:[door.locked]&&[gate.locked]:speak:Both locked|[door.locked]:unlock:door&speak:Door was locked|[gate.locked]:unlock:gate&speak:Gate was locked|speak:Nothing is locked;


MULTI-LINE ACTION BLOCKS (BRACE SYNTAX)

Long action chains, especially those using if/elseif/else, can become hard to read on a single line. You can instead write an action body as a multi-line block by opening it with { (left brace) and closing it with } (right brace). The element header stays in its normal single-line key=value form; only the action value is multi-line.

Basic example:

button x=10; y=5; z=0; on_activate={
    play_sound: click.ogg
    unlock: door1
    speak: Door unlocked
}

Each non-empty line inside the block becomes one action, and the lines are joined together as if you had separated them with commas on a single line.

Conditional branching inside a block uses an if/elseif/else structure:

button x=10; y=5; z=0; on_activate={
    if [score.current] >= 100 {
        speak: Perfect score!
        unlock: prize_door
    } elseif [score.current] >= 50 {
        speak: Halfway there
    } else {
        speak: Keep trying
    }
    play_sound: final.ogg
}

Rules for block syntax:
- Open the block with ={ (no space between = and {). Close it with a matching }.
- Each statement goes on its own line. Trailing semicolons are optional.
- Lines beginning with // or # are treated as comments and ignored.
- Inside an if, elseif, or else branch, statements are joined with & (all run when that branch fires). At the top level of the block, they are joined with , (run in sequence).
- The condition on an if/elseif is written naturally, e.g. if [score.current] >= 10 or if [door.locked] && [gate.locked]. Whitespace outside of [...] brackets is collapsed when the block is compiled.
- The colon between an action and its target may be followed by a space for readability: unlock: door is equivalent to unlock:door.
- Nested if/elseif/else blocks are not supported in this version; keep conditionals flat.
- Literal { and } characters inside action text (for example in a speak message) are not supported inside a block. If you need literal braces, use the single-line colon form instead.

The example above compiles to the same single-line colon DSL you would write by hand:

button x=10; y=5; z=0; on_activate=if:[score.current]>=100:speak:Perfect score!&unlock:prize_door|[score.current]>=50:speak:Halfway there|speak:Keep trying,play_sound:final.ogg;

Both forms are valid. The single-line form remains a shorthand for simple cases; reach for braces when the action body grows past a line or involves branching.

Block syntax works anywhere the existing colon DSL does, including on_activate, on_deactivate, and link action values.


SET ACTION (VALUE COPYING)

You can set element properties using the set action. This is useful for copying values between elements.

Syntax: set:element_id.property=value
Or: set:element_id.property=[other_element.property]

Examples:
button x=10; y=5; z=0; on_activate=set:counter2.current=[counter1.current];
lever x=5; y=5; z=0; on_activate=set:door.locked=false;
button x=10; y=5; z=0; on_activate=set:lever.state=true;

Writable properties:
- counter_trigger: current, disabled
- button: activated, disabled
- lever: state, disabled
- pressure_plate: disabled
- timer_trigger: disabled
- door/gate: locked
- lift: disabled, waiting
- vanishing_platform: disabled
- travelpoint: disabled
- map_item: collected, disabled


DOING MULTIPLE ACTIONS

You can make one trigger do several things by separating actions with commas.

Example:
button x=10; y=5; z=0; on_activate=unlock:door1,unlock:door2,play_sound:success.ogg,speak:Access granted;


ESCAPE CHARACTERS

Since colons are used as separators, use \: to include a literal colon in your text.

Example:
button x=10; y=5; z=0; on_activate=speak:Warning\: Door is locked;


DELAYED ACTIONS

You can add a delay before an action happens. The delay is in milliseconds, so 1000 means one second.

Example:
button x=10; y=5; z=0; on_activate=unlock:my_door:3000;

This button unlocks the door 3 seconds after being pressed.

You can mix delayed and immediate actions:
button x=10; y=5; z=0; on_activate=play_sound:click.ogg,unlock:door1:2000,speak:Door unlocked:2000;


USING LINKS

Links are an alternative way to connect triggers to targets. Instead of writing the action directly on the trigger, you create a separate link element.

Example:
button id=my_button; x=10; y=5; z=0;
link source=my_button; target=my_door; action=unlock;

Link settings:
source - The ID of the trigger. Required.
target - The ID of the target. Required.
action - What action to perform. Required.
delay - How long to wait in milliseconds. Defaults to 0.
event - Use "activate" or "deactivate". Defaults to activate.

Links also support element references. For special actions, put the content in target:

link source=my_button; target=Your score is [score.current]; action=speak;
link source=my_button; target=[keys.current]>=3:unlock:vault; action=if;
link source=my_button; target=counter2.current=[counter1.current]; action=set;


PRACTICAL EXAMPLES


Example 1: A locked door with a button

door id=secret_door; start_x=50; start_y=10; start_z=0; end_x=50; end_y=10; end_z=4; speed=100; loop=none.ogg; move=door_move.ogg; open=door_open.ogg; close=door_close.ogg; automatic=no; locked=yes;
button x=48; y=10; z=0; on_activate=unlock:secret_door;


Example 2: A lever-controlled gate

gate id=my_gate; minx=30; maxx=30; miny=10; maxy=14; minz=0; maxz=4; end_x_y=35; end_z=4; speed=100; loop=none.ogg; move=gate_move.ogg; open=gate_open.ogg; close=gate_close.ogg; automatic=no;
lever x=28; y=12; z=0; on_activate=toggle:my_gate; on_deactivate=toggle:my_gate;


Example 3: A pressure plate that opens a door temporarily

door id=timed_door; start_x=20; start_y=10; start_z=0; end_x=20; end_y=10; end_z=3; speed=100; loop=none.ogg; move=door_move.ogg; open=door_open.ogg; close=door_close.ogg; automatic=no;
pressure_plate minx=18; maxx=19; miny=9; maxy=10; minz=0; maxz=0; on_activate=toggle:timed_door; on_deactivate=toggle:timed_door:1000;


Example 4: A key-locked door

door id=treasure_room; start_x=50; start_y=10; start_z=0; end_x=50; end_y=10; end_z=4; speed=100; loop=none.ogg; move=door_move.ogg; open=door_open.ogg; close=door_close.ogg; automatic=no; locked=yes;
key_trigger x=50; y=10; z=0; required_item=golden_key; on_activate=unlock:treasure_room; consume_item=yes;


Example 5: Press three buttons to open a vault

counter_trigger id=vault_counter; required_count=3; on_activate=unlock:vault_door,speak:Vault unlocked;
button id=btn1; x=10; y=5; z=0; on_activate=increment:vault_counter;
button id=btn2; x=12; y=5; z=0; on_activate=increment:vault_counter;
button id=btn3; x=14; y=5; z=0; on_activate=increment:vault_counter;
door id=vault_door; start_x=16; start_y=5; start_z=0; end_x=16; end_y=5; end_z=3; speed=100; loop=none.ogg; move=door_move.ogg; open=door_open.ogg; close=door_close.ogg; automatic=no; locked=yes;


Example 6: Both levers must be on (AND gate)

lever id=lever1; x=10; y=5; z=0;
lever id=lever2; x=14; y=5; z=0;
and_gate id=both_on; inputs=lever1,lever2; on_activate=unlock:security_door; on_deactivate=lock:security_door;
door id=security_door; start_x=12; start_y=8; start_z=0; end_x=12; end_y=8; end_z=3; speed=100; loop=none.ogg; move=door_move.ogg; open=door_open.ogg; close=door_close.ogg; automatic=no; locked=yes;


Example 7: A combination lock puzzle

button id=b1; x=10; y=5; z=0;
button id=b2; x=11; y=5; z=0;
button id=b3; x=12; y=5; z=0;
button id=b4; x=13; y=5; z=0;
sequence_trigger id=combo; sequence=b2,b4,b1,b3; on_activate=unlock:safe,speak:Combination correct;
door id=safe; start_x=14; start_y=5; start_z=0; end_x=14; end_y=5; end_z=2; speed=100; loop=none.ogg; move=door_move.ogg; open=door_open.ogg; close=door_close.ogg; automatic=no; locked=yes;


Example 8: A timed challenge

timer_trigger id=challenge_timer; delay_ms=30000; on_activate=lock:exit_door,speak:Time is up!;
button x=10; y=5; z=0; on_activate=start_timer:challenge_timer,unlock:exit_door,speak:You have 30 seconds;
door id=exit_door; start_x=50; start_y=10; start_z=0; end_x=50; end_y=10; end_z=3; speed=100; loop=none.ogg; move=door_move.ogg; open=door_open.ogg; close=door_close.ogg; automatic=no; locked=yes;


Example 9: A counter with min/max range (score system)

This example creates a score counter that goes from 0 to 5. Reaching 5 unlocks a door.

counter_trigger id=score; required_count=5; min_value=0; max_value=5; on_activate=unlock:prize_door,speak:You win!; reset_on_complete=yes;
button x=10; y=5; z=0; on_activate=increment:score,speak:Plus one;
button x=12; y=5; z=0; on_activate=decrement:score,speak:Minus one;
door id=prize_door; start_x=14; start_y=5; start_z=0; end_x=14; end_y=5; end_z=3; speed=100; loop=none.ogg; move=door_move.ogg; open=door_open.ogg; close=door_close.ogg; automatic=no; locked=yes;


Example 10: Dynamic score display using references

This shows how to display current counter values in speech.

counter_trigger id=points; required_count=10; min_value=0; max_value=10;
button x=10; y=5; z=0; on_activate=increment:points;
button x=12; y=5; z=0; on_activate=speak:You have [points.current] out of [points.max_value] points;


Example 11: Conditional door toggle using if-else

This lever locks the door if unlocked, or unlocks it if locked, using the if-else syntax.

lever id=lock_lever; x=5; y=5; z=0; on_activate=if:[main_door.locked]==true:unlock:main_door&speak:Door unlocked|lock:main_door&speak:Door locked;
door id=main_door; start_x=10; start_y=5; start_z=0; end_x=10; end_y=5; end_z=3; speed=100; loop=none.ogg; move=door_move.ogg; open=door_open.ogg; close=door_close.ogg; automatic=no;


Example 12: Syncing counters (value copying)

Press a button to copy the value from counter A to counter B.

counter_trigger id=counter_a; required_count=100; min_value=0; max_value=100;
counter_trigger id=counter_b; required_count=100; min_value=0; max_value=100;
button x=10; y=5; z=0; on_activate=increment:counter_a;
button x=12; y=5; z=0; on_activate=set:counter_b.current=[counter_a.current],speak:Counter B now equals [counter_a.current];


Example 13: Tiered rewards using if-elseif-else

This button checks your score and announces different messages based on how high it is.

counter_trigger id=score; required_count=100; min_value=0; max_value=100;
button x=10; y=5; z=0; on_activate=increment:score;
button x=12; y=5; z=0; on_activate=if:[score.current]>=100:speak:Perfect score! You win!&unlock:prize_door|[score.current]>=75:speak:Great job! Almost there!|[score.current]>=50:speak:Good progress!|[score.current]>=25:speak:Keep going!|speak:You need more points;


Example 14: Multi-state lift controller

A button that toggles between two lifts - disabling one and enabling the other.

lift id=lift_a; minx=10; maxx=12; miny=5; maxy=7; minz=0; maxz=10; speed=100; surface=metal;
lift id=lift_b; minx=20; maxx=22; miny=5; maxy=7; minz=0; maxz=10; speed=100; surface=metal; disabled=yes;
button x=15; y=5; z=0; on_activate=if:[lift_a.enabled]==true:disable:lift_a&enable:lift_b&speak:Switched to lift B|enable:lift_a&disable:lift_b&speak:Switched to lift A;


Example 15: Complex puzzle with multiple conditions

A lever system where the outcome depends on the state of multiple elements.

lever id=lever1; x=10; y=5; z=0;
lever id=lever2; x=12; y=5; z=0;
door id=door1; start_x=20; start_y=5; start_z=0; end_x=20; end_y=5; end_z=3; speed=100; loop=none.ogg; move=door_move.ogg; open=door_open.ogg; close=door_close.ogg; automatic=no; locked=yes;
button x=14; y=5; z=0; on_activate=if:[lever1.state]&&[lever2.state]:unlock:door1&speak:Both levers on - door unlocked!|[lever1.state]:speak:Only lever 1 is on|[lever2.state]:speak:Only lever 2 is on|speak:Both levers are off;


Example 16: A keycard that unlocks a door

The player finds a keycard on the floor and picks it up with Enter. A key trigger on the door consumes it when used.

map_item x=5; y=3; z=0; item_name=keycard; on_pickup=speak:You pick up the keycard;
door id=lab_door; start_x=20; start_y=10; start_z=0; end_x=20; end_y=10; end_z=3; speed=100; loop=none.ogg; move=door_move.ogg; open=door_open.ogg; close=door_close.ogg; automatic=no; locked=yes;
key_trigger x=20; y=10; z=0; required_item=keycard; on_activate=unlock:lab_door,speak:Access granted; consume_item=yes;


Example 17: Collectible coins with a counter reward

Coins scattered around the map auto-collect when walked over. Gathering all five unlocks a prize door.

counter_trigger id=coin_count; required_count=5; on_activate=unlock:prize_door,speak:You collected all the coins!;
map_item x=5; y=5; z=0; item_name=coin; auto_pickup=yes; on_pickup=increment:coin_count;
map_item x=10; y=3; z=0; item_name=coin; auto_pickup=yes; on_pickup=increment:coin_count;
map_item x=15; y=8; z=0; item_name=coin; auto_pickup=yes; on_pickup=increment:coin_count;
map_item x=20; y=2; z=0; item_name=coin; auto_pickup=yes; on_pickup=increment:coin_count;
map_item x=25; y=6; z=0; item_name=coin; auto_pickup=yes; on_pickup=increment:coin_count;
door id=prize_door; start_x=30; start_y=5; start_z=0; end_x=30; end_y=5; end_z=3; speed=100; loop=none.ogg; move=door_move.ogg; open=door_open.ogg; close=door_close.ogg; automatic=no; locked=yes;


Example 18: A note with a clue — no inventory item

A piece of paper on the floor. Pressing Enter reads it aloud; nothing is added to inventory.

map_item x=8; y=4; z=0; on_pickup=speak:The note reads\: the combination is 4 8 15 16 23 42;


Example 19: Controllable radio

A radio that can be toggled on/off with a button and restarted with a lever.

source x1=10; x2=12; y1=5; y2=7; z1=0; z2=0; path=music/jazz.ogg; volume=0; pitch=100; id=radio;
button x=8; y=6; z=0; on_activate=toggle:radio;
lever x=14; y=6; z=0; on_activate=restart:radio;


MULTIPLAYER SYNCHRONIZATION

By default, when you trigger an interactable (press a button, flip a lever, etc.), only you see and hear the result. Other players on the same map do not see your interaction.

To make triggers visible to all players on the map, add server_bound=yes to the trigger. When a server-bound trigger is activated, the server broadcasts the event to all other players on the map, so they hear the sound and see the effect too.

Important notes:
- The trigger must have an id for server_bound to work.
- The player who triggers the interactable will not receive their own broadcast (they already triggered it locally).
- When a player joins the map, the server restores the final state of all server-bound interactables silently. This means no sounds or cascading actions will play for that player; they will simply see the world as it currently is. Intermediate steps are not replayed.
- server_bound has been tested to work in common scenarios, but edge cases are likely. Use it with caution and test thoroughly before relying on it in production maps.

What works reliably with server_bound:
- Buttons, levers, and key triggers (simple activate/deactivate)
- AND/OR gates, as long as all their inputs are also server_bound
- Passages
- Audio sources (pause, resume, restart)
- Counter triggers (the activated count is included in the broadcast)
- Text inputs, menu inputs and form inputs, which send the answer itself alongside the event, so every other player's [id.value] reads what was actually typed or chosen and their on_activate runs on it. Only the player who was asked ever sees a prompt. Nobody else's screen is interrupted. The last answer is kept for late joiners too, who read it without any of the actions firing, the same as every other restored state.

Known edge cases to be aware of:
- Sequence triggers: the current step is not synced between players. If one player is partway through a sequence, other players are still at step zero. This will cause the sequence to behave differently for each player.
- Pressure plates: concurrent player tracking can cause the plate to not deactivate properly if one of the players on it disconnects, since the "is anyone still on this?" check only knows about players tracked locally.
- Trigger chains: if a server_bound trigger activates another trigger via its action, the sender fires the chain locally and then again when it receives the broadcast back. The chained trigger will execute twice on the sender and once on all other players.
- Lifts and vanishing platforms: these do not participate in the server_bound broadcast system. If you disable or hide them via a trigger action, only the player who activated the trigger will see the change. Other players will still see the lift or platform as it was.

Example:
button id=shared_button; x=10; y=5; z=0; on_activate=unlock:my_door; server_bound=yes;

When any player presses this button, all other players on the map will hear the button click sound and see the door unlock.


Example 20: Cooperative puzzle with server-bound triggers

Two players must each stand on a button at the same time to open a door.

button id=btn_left; x=10; y=5; z=0; on_activate=activate:both_check; server_bound=yes;
button id=btn_right; x=20; y=5; z=0; on_activate=activate:both_check; server_bound=yes;
and_gate id=both_check; inputs=btn_left,btn_right; on_activate=unlock:coop_door;
door id=coop_door; start_x=15; start_y=8; start_z=0; end_x=15; end_y=8; end_z=3; speed=100; loop=none.ogg; move=door_move.ogg; open=door_open.ogg; close=door_close.ogg; automatic=no; locked=yes;
