Skip to content

Theme, objectives & visibility

Three top-level blocks shape what players see and know. All three are first-match-wins: the engine evaluates entries top-down and uses the first that matches, so order them most-specific-first with a catch-all last.

interface GameTheme {
playerMarkers?: MarkerColorRule[];
playerHeadings?: { others?: HeadingVisibilityRule[] };
}
interface MarkerColorRule { when?: { key: string; value: unknown }; color: string }
interface HeadingVisibilityRule { when?: { key: string; value: unknown }; visible: boolean }

Colors each player’s map marker from their state. First match wins; an entry with no when is the catch-all default.

theme:
playerMarkers:
- when: { key: it, value: true }
color: "#dc2626" # red — the it player
- color: "#16a34a" # green — everyone else

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

when matches against the player being colored. A richer example layers several states (winner > carrier > dead > default):

playerMarkers:
- { when: { key: won, value: true }, color: "#f59e0b" } # gold
- { when: { key: inHill, value: true }, color: "#eab308" } # yellow

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

Controls whether other players’ facing direction is shown to a viewer. Evaluated against the target player’s state; if no rule matches, that player’s heading is hidden. Players always see their own heading.

theme:
playerHeadings:
others:
- { when: { key: it, value: true }, visible: true } # you can see which way the tagger faces
- { visible: false } # everyone else's heading hidden

Use this to hide directional info that would give away a hider or a runner.

interface ObjectiveRule {
when?: { key: string; value: unknown };
text: string;
navigation?: { target: NavigationTarget; mode?: "suggested" | "required" };
}
type NavigationTarget =
| { kind: "point"; pointId: string }
| { kind: "zone"; zoneId: string }
| { kind: "itemKind"; itemKind: string };

Sets each player’s objective — the short goal text on their phone — from their state, every tick. First match wins; the no-when entry is the default.

objectives:
- { when: { key: it, value: true }, text: "Tag someone!" }
- { text: "Don't get tagged." }

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

An objective can attach a navigation target — a point, a zone, or “the nearest item of a kind” — that the phone surfaces as a waypoint. mode: "suggested" lets the player override it; mode: "required" keeps it pinned (gameplay enforcement is still your rules’ job).

objectives:
- when: { key: carrying, value: true }
text: "Get it home!"
navigation: { target: { kind: zone, zoneId: red_ship }, mode: suggested }
- text: "Grab some loot."
navigation: { target: { kind: itemKind, itemKind: treasure_chest }, mode: suggested }
interface VisibilityRule {
when?: { key: string; value: unknown }; // on the VIEWER's state
canSeeOthers: "any" | { key: string; value: unknown }[];
}

Per-role position visibility. For each viewer, the engine evaluates rules top-down and applies the first matching rule’s canSeeOthers to strip the positions of non-matching players from that viewer’s world snapshot. A rule with no when is a catch-all. Viewers always see themselves; the GM always sees everyone unfiltered.

  • canSeeOthers: "any" — the viewer sees all positions.
  • canSeeOthers: [{ key, value }, …] — the viewer sees only players matching all those predicates. Keys are teamId or state.<name>.
# Sardines-style: seekers only see other seekers; hiders see everyone.
visibility:
- when: { key: seeker, value: true } # if the viewer is a seeker…
canSeeOthers: [{ key: state.seeker, value: true }] # …they see only seekers
- canSeeOthers: any # everyone else (hiders) sees all

This is what makes hide-and-seek work: hiders are invisible to the people hunting them, but can watch the hunters close in.