Multiball and Ball Lifecycle Events
Multiball and Ball Lifecycle Events
Cade emits a fixed set of system.ball.* and system.play.* events as a ball turn progresses. Getting multiball right means picking the right ball-firing action (eject_ball vs kick_ball), listening for the right lifecycle event, and tracking in-play ball counts correctly.
eject_ball vs kick_ball
Two actions drive ball devices, and they are not interchangeable.
eject_ball | kick_ball | |
|---|---|---|
| Use for | Trough or launcher: a new ball enters play. | Gate or held-ball kicker: an existing ball is fired out. |
| Ball count | Increments the session’s in-play ball count. | Does not change the in-play ball count — the ball is already in play. |
| Physics parameters | On the action block (angle, strength). | On the kicker device declaration (settings { kick_angle, kick_strength }). |
Using eject_ball where kick_ball is correct can spawn a phantom ball on each gate hit (the simulator creates a new ball and kicks it). For kicker-held balls, always use kick_ball. See Kick Ball Action for the full action reference.
Ball Drain Behavior
When a ball drains, the session decides which branch to take based on how many balls are still in play, whether the player has extra balls, and whether other players remain.
| Scenario | Condition | Events emitted (in order) | Result |
|---|---|---|---|
| Multiball intermediate drain | More than one ball still in play | system.ball.drain | Decrement in-play count, no turn change |
| Extra ball | Player has an extra ball | system.ball.drain → system.ball.end | Relaunch for same player |
| Game over | Last ball drains, all players finished | system.ball.drain → system.play.end → system.ball.end | End the game |
| Player rotation | Last ball drains, other players remain | system.ball.drain → system.play.end → system.ball.end | Advance to next player |
The key invariant: system.ball.end does not fire on multiball intermediate drains. A mode with stop_events = ["system.ball.end"] therefore stays active through the whole multiball sequence — it ends only when the last ball drains.
Emission order is strict within each branch: system.ball.drain first, then system.play.end (last-ball branches only), then system.ball.end. This ordering lets modes that listen for system.play.end run before anything that reacts to system.ball.end.
system.ball.end Payload
Handlers listening for system.ball.end can read the following fields:
| Field | Description |
|---|---|
player | Current player ID. |
ball | Which ball just ended (1-indexed). |
final_score | Player’s score at ball end. |
drain_device | Device that triggered the drain. |
timestamp | Time the event was emitted. |
Tracking In-Play Ball Counts
Two independent counters are involved in multiball — make sure each is updated by the right action.
- Session in-play count — maintained by Cade itself.
eject_ballincrements it.kick_balldoes not. - HCL
balls_in_playvariable — only exists if you declare it. Increment it from HCL with anincrementaction when your table needs a user-visible count (for example, to condition a “multiball has ended” check on it).
For trough ejection, increment both (session count is automatic, HCL variable via an increment action). For gate-to-kicker flows, only the HCL variable needs updating.
Sequence Example — Gate-to-Kicker Multiball
1. system.ball.start (in-play count = 1)
2. device.Gate1.cleared
├─ kick_ball Kicker1 (ball already in play, no ejection)
├─ increment var.balls_in_play
└─ start_mode multiball
3. device.Drain.activated (first ball drains)
└─ system.ball.drain (in-play count drops from 2 to 1)
Mode's drain handler:
condition = var.balls_in_play <= 1
action: emit = "multiball.ended"
4. device.Drain.activated (last ball drains)
├─ system.ball.drain
├─ system.play.end
└─ system.ball.end → advance to next player
Configuration Example
device "switch" "kicker" "Kicker1" {
id = 41
settings {
kick_angle = 190
kick_strength = 10
}
}
event_handler "gate_hit" {
event = "device.Gate1.cleared"
actions {
kick_ball { device = "Kicker1" }
increment = "balls_in_play"
start_mode = "multiball"
}
}
Trough ejection looks similar, but uses eject_ball with physics on the action:
event_handler "drain_auto_serve" {
event = "device.Drain.activated"
actions {
eject_ball {
device = "BallRelease"
angle = 90
strength = 7
}
}
}