Scripting
Entities & hooks
Inspect live towers, bloons and projectiles, edit them by ID, and intercept native behaviour.
These interfaces are local-only, like the rest of the script runtime. When nothing listens, the native path is unchanged: no entity snapshots or script calls are made.
Inspect live entities
fn on_new_game(ctx) {
[#{op: "inspect", query: #{kind: "towers", offset: 0, limit: 8, tag: "my-list"}}]
}
fn on_query(ctx) {
for tower in ctx.event.items {
// tower.id is a stable entity ID, not an array index.
}
[]
}
- Kinds:
towers,bloons,projectiles. - Responses arrive through the requesting script's
on_queryat a simulation boundary, withkind,tag,offset,totalanditems. - Pages hold 1–128 items. At most eight queries are serviced per step, with up to 64 pending.
- Paging uses current storage order, so entities can appear, disappear or reorder between pages. Deduplicate by ID.
Snapshot fields
| Kind | Fields |
|---|---|
| Tower | id, tower (definition ID), position, tiers, pops, damage, target, point |
| Bloon | id, layer, bloon (definition ID), hp, position, distance, path |
| Projectile | id, owner (tower ID or 0), position, velocity, damage, pierce, remaining |
Positions are native world coordinates. Projectile velocity is world units per step; remaining is seconds.
Position and remaining can be null for native infinities.
Edit by ID
#{op: "entity_patch", patch: #{kind: "bloons", id: b.id, layer: b.layer, fields: #{hp: 50}}}
#{op: "entity_patch", patch: #{kind: "projectiles", id: p.id, fields: #{damage: 10, pierce: 20, remaining: 2.0, velocity: [1.0, 0.0]}}}
#{op: "entity_patch", patch: #{kind: "towers", id: t.id, fields: #{target: "Strong"}}}
#{op: "damage_bloon", id: b.id, layer: b.layer, amount: 5}
- HP edits set positive current HP directly. Use
damage_bloonfor real damage: child spawning, cash, pops and damage hooks. It runs at the next simulation boundary and doesn't credit a tower. layeris optional but recommended: a first child can reuse its parent's object ID with a new layer ID.- Tower targets: First, Last, Close, Strong or Point, when that tower allows them.
point: [x, y]moves a point target. - Missing entities and mismatched layers are ignored. IDs are per match.
- Projectile movement controllers can overwrite velocity changes.
Change or cancel native behaviour
Interceptors return a map (event callbacks return arrays):
fn before_damage(ctx) {
if ctx.event.bloon.bloon == "Red" { return #{cancel: true}; }
#{damage: ctx.event.damage * 2}
}
fn before_fire(ctx) { #{damage: ctx.event.damage * 2, count: 3} }
fn before_place(ctx) { #{cancel: ctx.event.tower == "BananaFarm"} }
fn before_sell(ctx) { #{cancel: ctx.event.tower == "DartMonkey"} }
fn before_upgrade(ctx) { #{cancel: ctx.event.paragon} }
| Interceptor | Event | Can return |
|---|---|---|
before_damage | bloon snapshot, damage | damage (rounded to native integer), cancel |
before_fire | owner, position, direction (radians), count, damage, speed, pierce, tiers | any of those, or cancel |
before_place | tower, position, cost | cancel |
before_sell | tower snapshot | cancel |
before_upgrade | tower, paragon, path/cost or extra | cancel |
- An empty map
#{}keeps native behaviour.cancel: truestops later scripts for that action. - Scripts compose in stable order; later scripts see earlier overrides.
- Interceptors can read
ctxand updatethis, but can't return game commands. before_damageruns once per top-level health hit, not for child overflow. Cancelling prevents health loss only, not other hit effects or pierce use. Whole-bloon destruction that bypasses health damage doesn't enter it.before_firealso sees emitted sub-projectiles. Cancelling doesn't refund the cooldown.- Forced dependent cleanup when a parent tower is sold bypasses child sell vetoes.
Turn hooks on and off
fn on_toggle(ctx) {
this.enabled = !this.enabled;
[#{op: "listen", event: "before_fire", enabled: this.enabled}]
}
// Apply the last shield charge, then stop receiving damage callbacks:
fn before_damage(ctx) { #{cancel: true, unsubscribe: true} }
All declared callbacks start subscribed. listen pauses or resumes one of your own callbacks after the
current one, keeping state and widgets. An interceptor can return unsubscribe: true to mute only itself
after its decision. When nobody listens, the native hook skips script execution and snapshots entirely.
See Entity Lab for a complete example.