Skip to main content

State model

state.data is everything this player can see right now, and each message replaces the one before it.

Read a state

RuleClient behavior
A new message arrivesReplace the previous PlayerState. Do not merge arrays.
You read an objectCheck kind first, then read the fields listed for that kind.
You need its ownercontrolled: true means yours; false means a visible enemy.
A field is missingIts value is unknown or does not apply. The server does not send null.
Minimal state message
{
"type": "state",
"data": {
"status": "ACTIVE",
"resources": 20,
"population": 1,
"population_tier": 0,
"upkeep_next_tick": 0,
"champion_beacon": {"position": [0, 0]},
"objects": [
{
"kind": "CORE",
"id": "2ea3c3dc-42b0-4b92-9754-7558bd4ff834",
"controlled": true,
"position": [12, 8],
"hp": 5,
"shield": 5,
"state": "NORMAL"
},
{
"kind": "UNIT",
"id": "9d3e4941-2816-4a39-a220-df8cd95e877d",
"controlled": true,
"position": [11, 8],
"hp": 2,
"unit_type": "WORKER",
"cargo": 0
}
],
"events": []
}
}

If you want machine-readable definitions, use the AsyncAPI schema.

PlayerState

FieldFormatRequiredMeaning
status"ACTIVE" or "RESPAWNING"YesWhether the player has an active Core or is waiting to respawn.
respawn_at_tickpositive int64Only when respawningTick of the next respawn attempt.
resourcesinteger ≥ 0YesResources stored by the Core; Worker cargo is separate.
populationinteger ≥ 0YesLiving owned Units; the Core is not counted.
population_tierinteger ≥ 0Yesfloor(population / 20).
upkeep_next_tickinteger ≥ 0Yestier × (tier + 1) / 2 for the current population.
champion_beaconobjectYesPublic position and, when visible, carrier state.
objectsarrayYesOwned entities plus currently visible terrain and enemies.
eventsarrayYesResolution results addressed to this player.

When there is nothing to report, objects and events come through as empty arrays rather than going missing. While you are RESPAWNING the resource and population fields are still there, but you may have no Core of your own until CORE_RESPAWNED arrives.

Champion Beacon

The position is always public. Everything else depends on what you can see.

Outside vision

{
"position": [120, 85]
}

You know where it is and nothing more — not whether it is lying on the ground or riding along with someone.

Visible on the ground

{
"position": [120, 85],
"status": "GROUND"
}

There is no carrier_id here.

Visible and carried

{
"position": [120, 85],
"status": "CARRIED",
"carrier_id": "9d3e4941-2816-4a39-a220-df8cd95e877d"
}

carrier_id names the Core or Unit doing the carrying. If the next state leaves status or carrier_id out, throw the old value away rather than keeping it around.

World objects

Every entry in objects begins with kind.

kindRepresentsIdentity
"CORE"One Coreid
"UNIT"One Worker, Vanguard, or Rangerid
"OBSTACLE"All visible obstacle cellsIndividual positions
"RESOURCE"All visible resource cellsIndividual positions
Dispatch by kind
for (const object of state.objects) {
if (object.kind === 'CORE') handleCore(object);
else if (object.kind === 'UNIT') handleUnit(object);
else handleTerrain(object);
}

Terrain

{
"kind": "OBSTACLE",
"positions": [[4, 7], [4, 8], [5, 8]]
}
FieldFormatMeaning
kind"OBSTACLE" or "RESOURCE"Terrain type.
positionsnon-empty array of [x, y]Visible cells, sorted by x and then y.

All visible cells of one terrain kind arrive in a single entry. If a kind is missing altogether, none of its cells are currently visible. Terrain carries no id, no controlled, no HP, and no resource quantity.

Core

Normal Core
{
"kind": "CORE",
"id": "2ea3c3dc-42b0-4b92-9754-7558bd4ff834",
"controlled": true,
"position": [12, 8],
"hp": 5,
"shield": 4,
"state": "NORMAL"
}
Moving Core
{
"kind": "CORE",
"id": "2ea3c3dc-42b0-4b92-9754-7558bd4ff834",
"controlled": true,
"position": [12, 8],
"hp": 5,
"shield": 4,
"state": "MOVING",
"move_direction": "RIGHT",
"move_progress": 2,
"move_required_ticks": 4,
"destination": [13, 8]
}
FieldFormatRequired
kind"CORE"Yes
idUUIDYes
controlledbooleanYes
position[x, y]Yes; remains the origin while moving
hpinteger ≥ 0Yes
shieldinteger ≥ 0Yes
state"NORMAL" or "MOVING"Yes
move_directiondirection stringMoving only
move_progressinteger ≥ 1Moving only
move_required_ticksinteger ≥ 1Moving only; currently 4
destination[x, y]Moving only

A normal Core has none of the movement fields. A visible enemy Core exposes the same ones you would see on your own.

Unit

Owned Worker
{
"kind": "UNIT",
"id": "9d3e4941-2816-4a39-a220-df8cd95e877d",
"controlled": true,
"position": [11, 8],
"hp": 2,
"unit_type": "WORKER",
"cargo": 1
}
FieldFormatRequired
kind"UNIT"Yes
idUUIDYes
controlledbooleanYes
position[x, y]Yes
hpinteger ≥ 0Yes
unit_type"WORKER", "VANGUARD", or "RANGER"Yes
cargointeger ≥ 0Owned Worker only

An enemy Worker's cargo is hidden from you. Vanguards and Rangers never carry a cargo field at all, not even your own.

Visibility

DataIncluded whenHidden fields
Owned Core and UnitsAlwaysNone from their object format
Enemy Core and UnitsTheir cell is currently visibleOwner identity; enemy Worker cargo
TerrainIts cell is currently visibleResource quantity
Beacon positionAlwaysNone
Beacon status and carrierBeacon cell is currently visibleBoth fields outside vision

Nothing here carries a last-seen timestamp, so keep whatever terrain you remember in its own store, apart from current server state.

Updating state

Rebuild your entity maps from each new state:

const entities = new Map();

for (const object of nextState.objects) {
if (object.kind === 'CORE' || object.kind === 'UNIT') {
entities.set(object.id, object);
}
}

The server emits objects in a deterministic order:

  1. obstacle batch;
  2. resource batch;
  3. owned Core;
  4. owned Units by UUID;
  5. visible enemy Cores by UUID;
  6. visible enemy Units by UUID.

Groups with nothing in them are skipped, which is exactly why an array index is never an object's identity.