Scripting

Event scripts

Real running code: Rhai callbacks that react to the match and return commands.

Rhai is a small embedded language with functions, loops, conditions, arrays and maps. Embed a script from your build script, and it compiles once when a match loads.

mod.script("mechanics", file="mechanics.rhai", tick_interval=60)

Your first script

fn on_new_game(ctx) { this.reward = 100; [] }
fn on_round_end(ctx) {
    if ctx.sandbox { return []; }
    this.reward += 10;
    [#{op: "cash", amount: this.reward}]
}

Define only the callbacks you need. Each receives ctx and returns an array of commands. this is a per-script, per-match map for your state (not a save file). Top-level statements don't run; put helpers in functions, and call ones that use state as this.helper(ctx) so this stays bound.

Hooks

CallbackRuns
on_new_gameWhen the match starts
on_round_start / on_round_endAt each round boundary
on_tower_placedAfter a placement; ctx.event is the tower snapshot
on_tower_soldAfter a sale; ctx.event is the pre-removal snapshot
on_towers_changedWhen the tower list changes
on_popOnce per simulation step, aggregating that step's pops
on_tickEvery tick_interval simulation steps (60 per simulated second)
on_queryWhen an inspect request is answered

No callbacks run while paused, except explicit UI actions.

The ctx snapshot

FieldMeaning
cashCurrent cash
livesCurrent lives
roundCompleted rounds
tickSimulation step
poppedPops (in on_pop, this step's)
tower_count, bloon_countLive counts
sandboxWhether this is a Sandbox match
eventHook-specific data, when present

Commands

#{op: "cash", amount: 100}                       // add (negative subtracts)
#{op: "lives", value: 200}                       // set; doesn't revive an ended game
#{op: "spawn", name: "Blue", count: 30, spacing: 0.2}
#{op: "clear_bloons"}
#{op: "reset_cooldowns"}
#{op: "popup", title: "Hello", body: "Your message"}
#{op: "ui", widget: #{id: "panel", tree: node, actions: #{"Button": "on_click"}, modal: true}}
#{op: "remove_ui", id: "panel"}
#{op: "inspect", query: #{kind: "towers", offset: 0, limit: 8, tag: "list"}}
#{op: "entity_patch", patch: #{kind: "bloons", id: b.id, layer: b.layer, fields: #{hp: 50}}}
#{op: "damage_bloon", id: b.id, layer: b.layer, amount: 5}
#{op: "listen", event: "before_fire", enabled: true}

Commands in one callback are validated together before any apply. An invalid batch or an over-budget script disables that script for the match.

Limits

LimitValue
Operations per callback30,000
Operations per simulation tick (all scripts)100,000
Call depth16
Commands per callback32
Persistent this state64 KiB
Script source32 KiB (inside the 64 KiB manifest)
Callback output256 KiB
Limit

Scripts have no filesystem, network or process APIs. Runtime script mods work in local matches and are rejected in co-op until their state protocol exists.

A complete example is Mechanics Workshop.

Unofficial fan project. Not affiliated with Ninja Kiwi. rusttd6.com