Rules: conditions
A rule’s when is a condition — the trigger. When it’s satisfied, the rule’s
actions run. This page documents every condition
kind in the Condition union from packages/wage-protocol/src/rules.ts.
Conditions also bind the players involved so actions know who to act on:
same is the primary bound player, other is the partner. Each entry below notes
what it binds. See Selectors & values for the
binding model.
entered_zone
Section titled “entered_zone”{ kind: "entered_zone"; who: PlayerSelector; zoneId: string }Fires once when a player matching who crosses into the placed zone
zoneId. Binds that player as same. Edge-triggered.
- id: enter_hill when: { kind: entered_zone, who: any, zoneId: hill } do: - { kind: set_state, target: same, key: inHill, value: true }left_zone
Section titled “left_zone”{ kind: "left_zone"; who: PlayerSelector; zoneId: string }Fires once when a matching player crosses out of zoneId. Binds that player
as same. Edge-triggered. (Players in an exclusion
cutout count as outside.)
- id: out_of_bounds when: { kind: left_zone, who: any, zoneId: play_area } do: - { kind: send_event, to: same, event: { type: toast, text: "Out of bounds." } }proximity
Section titled “proximity”{ kind: "proximity"; a: PlayerSelector; b: PlayerSelector; meters: number }Fires when two players (one matching a, one matching b) come within meters
(haversine distance). Binds a as same and b as other. Edge-triggered
per pair — it won’t re-fire while they stay close; they must separate and
re-approach.
- id: tackle when: kind: and children: - { kind: proximity, a: any, b: any, meters: 3 } - { kind: state_equals, who: same, key: carrying, value: true } - { kind: compare, op: ne, left: { ref: same.teamId }, right: { ref: other.teamId } } do: - { kind: set_state, target: same, key: carrying, value: false }Source: apps/wage-engine/src/games/pirates_booty/game.yaml.
{ kind: "tick"; everyMs: number }Fires periodically, every everyMs milliseconds. Binds no one on its own —
use it to gate how often a rule fires, and put player-binding conditions before
it in an and.
- id: score_tick when: kind: and children: - { kind: state_equals, who: any, key: inHill, value: true } # binds same - { kind: count_state, who: any, key: inHill, value: true, n: 1 } - { kind: tick, everyMs: 1000 } do: - { kind: increment_state, target: same, key: score, by: 1 }state_equals
Section titled “state_equals”{ kind: "state_equals"; who: PlayerSelector; key: string; value: unknown }Matches each player (in who) whose state[key] strictly equals value,
binding them as same. Used both as a top-level trigger and (very commonly) as a
filter inside an and.
- { kind: state_equals, who: same, key: it, value: true }count_state
Section titled “count_state”{ kind: "count_state"; who: PlayerSelector; key: string; value: unknown; n: number }Fires while exactly n players in who have state[key] === value. Binds
the matching players. Two classic uses:
n: 0— “nobody is it anymore” (self-heal; see ensure a role exists).n: 1— “exactly one player on the hill” / “last one standing.”
# Win when only one player remains alive.- id: winner when: kind: and children: - { kind: count_state, who: any, key: alive, value: true, n: 1 } - { kind: state_equals, who: same, key: alive, value: true } do: - { kind: send_event, to: same, event: { type: toast, text: "You win." } }Source: apps/wage-engine/src/games/assassin/game.json.
compare
Section titled “compare”{ kind: "compare"; op: CompareOp; left: Value; right: Value; who?: PlayerSelector }type CompareOp = "eq" | "ne" | "gt" | "gte" | "lt" | "lte" | "contains";The general two-value comparison.
Iterates who (default same) and fires for each bound player whose left/right
satisfy op. Subsumes state_equals (it’s compare(eq, { ref: "same.state.K" }, V)), but is far more flexible because either side can be a { ref }.
# Anyone whose score reaches 30.- { kind: compare, who: any, op: gte, left: { ref: same.state.score }, right: 30 }
# A blue carrier vs. a red tagger (cross-team check).- { kind: compare, op: ne, left: { ref: same.teamId }, right: { ref: other.teamId } }
# Match a player against their own target id.- { kind: compare, op: eq, left: { ref: other.id }, right: { ref: same.state.targetId } }The operators: eq (=), ne (≠), gt/gte/lt/lte (numeric ordering),
contains (membership / substring).
interaction
Section titled “interaction”{ kind: "interaction"; verb: string; a?: PlayerSelector; b?: PlayerSelector; itemKind?: string }Fires when a player-initiated interaction
of the given verb resolves (immediately for confirm: none, on confirmation
for confirm: mutual). Binding depends on the interaction’s target:
- Player interactions — binds initiator as
same, target asother.aconstrains the initiator (defaultany),bthe target. - Item interactions (
worldItem/heldItem) — binds the initiator assameand the item asother(readother.kind,other.id,other.position.lat).itemKindoptionally filters by the item’s kind.
Range and eligibility are enforced server-side by the interaction’s own
rangeMeters/filters; this condition only matches the verb and the a/b
selectors.
- id: tag when: kind: and children: - { kind: interaction, verb: tag } - { kind: state_equals, who: same, key: it, value: true } - { kind: state_equals, who: other, key: it, value: false } do: - { kind: set_state, target: same, key: it, value: false } - { kind: set_state, target: other, key: it, value: true }Source: apps/wage-engine/src/games/tag/game.yaml.
Composition: and / or / not
Section titled “Composition: and / or / not”{ kind: "and"; children: Condition[] }{ kind: "or"; children: Condition[] }{ kind: "not"; child: Condition }and— all children must hold. Order matters: put a player-binding condition first so later children (state_equals who: same,compare) have asame/otherto filter, and putticklast as a frequency gate.or— any child holds.not— the child does not hold.
when: kind: and children: - { kind: proximity, a: any, b: any, meters: 5 } # binds same/other - { kind: state_equals, who: same, key: alive, value: true } - { kind: state_equals, who: other, key: alive, value: true }