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;

Button settings:
x, y, z - The position of the button. Required.
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;

Lever settings:
x, y, z - The position of the lever. Required.
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.
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;


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.
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.
server_bound - Set to yes to broadcast this trigger to all players on the map. See MULTIPLAYER SYNCHRONIZATION.


CREATING A SEQUENCE TRIGGER

A sequence trigger requires inputs to arrive in a specific order, like a puzzle combination. Each button in the sequence must send its own ID to the sequence trigger using the sequence_input action.

Example:
button id=red; x=10; y=5; z=0; on_activate=sequence_input:puzzle,red;
button id=blue; x=12; y=5; z=0; on_activate=sequence_input:puzzle,blue;
button id=green; x=14; y=5; z=0; on_activate=sequence_input:puzzle,green;
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 the wrong button resets the sequence.

The sequence_input action format is: sequence_input:sequence_id,button_id
The first part is the sequence trigger's ID. The second part is the ID of the button being pressed.

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.


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.
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, or timed_source 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;

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.


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,trigger_id - Sends a button press to a sequence trigger. The second value must be the ID of the button being pressed.

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.


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

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)

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


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", "1", "yes", or "on".

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; on_activate=sequence_input:combo,b1;
button id=b2; x=11; y=5; z=0; on_activate=sequence_input:combo,b2;
button id=b3; x=12; y=5; z=0; on_activate=sequence_input:combo,b3;
button id=b4; x=13; y=5; z=0; on_activate=sequence_input:combo,b4;
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)

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;
