Skip to content

Rules: actions

A rule’s do is an ordered list of actions that run top-to-bottom when the condition matches. This page documents every action kind in the Action union from packages/wage-protocol/src/rules.ts.

Actions act on players named by a selector (same/other/any/{ byTeam }/{ byId }) bound by the condition.

{ kind: "set_state"; target: PlayerSelector; key: string; value: unknown }

Set target’s state[key] to value. value accepts a Value: a bare primitive is a literal; { ref } / { lit } resolve through the binding (so you can copy other.state.X into same.state.Y).

- { kind: set_state, target: other, key: it, value: true }
- { kind: set_state, target: same, key: targetId, value: { ref: other.state.targetId } }
{ kind: "set_state_delayed"; target; key; value; delayMs: number }

Like set_state, but applied after delayMs milliseconds. Useful for timed unlocks (lift a locked flag after a countdown).

- { kind: set_state_delayed, target: same, key: locked, value: false, delayMs: 5000 }
{ kind: "increment_state"; target: PlayerSelector; key: string; by: number }

Add by to a numeric state[key] (missing/non-number treated as 0). by may be negative. Targeting { byTeam } increments every member.

- { kind: increment_state, target: { byTeam: blue }, key: score, by: 1 }
- { kind: increment_state, target: other, key: health, by: -10 }
{
kind: "assign_state_random";
assigns: { key: string; value: unknown }[];
filters?: { key: string; value: unknown }[];
exclude?: "same" | "other";
notify?: Record<string, unknown>;
}

Pick one random player from the pool — optionally narrowed by filters (keys are teamId or state.<name>) and with exclude removing the current same/other — and apply every assigns to them. notify sends a game event directly to the chosen player.

# Bomb Tag: hand the bomb to a random living non-it player who isn't the tagger.
- kind: assign_state_random
assigns:
- { key: hasBomb, value: true }
- { key: bombTick, value: 0 }
filters:
- { key: alive, value: true }
- { key: it, value: false }
exclude: same
notify: { type: toast, text: "You have the bomb!" }

Source: apps/wage-engine/src/games/bomb_tag/game.json.

{ kind: "random_action"; branches: { weight?: number; do: Action[] }[] }

Pick one branch at random (weighted; default weight 1) and run its actions with the current binding. Express a stochastic outcome without splitting into mutually-exclusive rules.

# Coins drip in ~1-in-6 ticks: weight 1 spawns, weight 5 does nothing.
- kind: random_action
branches:
- weight: 1
do: [{ kind: spawn_item, item: coin, inZone: treasure_island }]
- weight: 5
do: []

Source: apps/wage-engine/src/games/pirates_booty/game.yaml.

# Bots vs Humans: a 50/50 battle applying opposite health swings.
- kind: random_action
branches:
- do:
- { kind: increment_state, target: same, key: health, by: -10 }
- { kind: increment_state, target: other, key: health, by: 10 }
- do:
- { kind: increment_state, target: same, key: health, by: 10 }
- { kind: increment_state, target: other, key: health, by: -10 }

Source: apps/wage-engine/src/games/bots_vs_humans/game.json.

See Items & interactions for the item model. In item interactions, the matched item is bound as other, and the custody actions default to it when item is omitted.

{ kind: "spawn_item"; item: string; at: PlayerSelector } // at a player's position
{ kind: "spawn_item"; item: string; inZone: string } // random point in a zone
{ kind: "spawn_item"; item: string; atPoint: string } // a named map point

Create a new item of kind item. Provide exactly one of at, inZone, atPoint. Each is silently skipped if its anchor isn’t available yet (player has no position, zone/point not placed). inZone honors exclusions.

- { kind: spawn_item, item: red_flag, atPoint: red_flag } # respawn captured flag home
- { kind: spawn_item, item: red_flag, at: same } # drop where the carrier stood
- { kind: spawn_item, item: coin, inZone: treasure_island } # random drip
{ kind: "hold_item"; target: PlayerSelector; item?: Value }

Give the item to target (sets its holder, clears its world position). item defaults to the item bound as other.

{ kind: "drop_item"; item?: Value; at: PlayerSelector } // at a player
{ kind: "drop_item"; item?: Value; inZone: string } // random point in a zone
{ kind: "drop_item"; item?: Value; atPoint: string } // a named point

Return a held item to the world. Exactly one of at/inZone/atPoint.

{ kind: "destroy_item"; item?: Value }

Remove an item entirely (consume it). item defaults to the item bound as other — so in a pickup rule, destroy_item with no args consumes the thing just grabbed.

- id: pickup_chest
when: { kind: interaction, verb: grab_chest }
do:
- { kind: set_state, target: same, key: carrying, value: true }
- { kind: destroy_item }
{ kind: "send_event"; to: PlayerSelector; event: Record<string, unknown> }

Send a game event to the players in to. The event is an arbitrary object the client interprets by its type. Common types: toast (a transient message) and countdown.

- { kind: send_event, to: same, event: { type: toast, text: "You're it!" } }
- { kind: send_event, to: { byTeam: red }, event: { type: toast, text: "Your flag was stolen!" } }
{ kind: "log"; message: string }

Write a line to the session event log (visible to the GM). {player} interpolates same’s name and {other} interpolates other’s. Your primary debugging tool while authoring.

- { kind: log, message: "{player} tagged {other}" }
{ kind: "end_game" }

End the game immediately: sets status to ended and broadcasts a game-over (reason rules). Use it in the do of a win rule. (Games can also end via countdownSeconds expiry.)

do:
- { kind: set_state, target: same, key: won, value: true }
- { kind: send_event, to: any, event: { type: toast, text: "Game over!" } }
- { kind: end_game }