State model
state.data is everything this player can see right now, and each message
replaces the one before it.
Read a state
| Rule | Client behavior |
|---|---|
| A new message arrives | Replace the previous PlayerState. Do not merge arrays. |
| You read an object | Check kind first, then read the fields listed for that kind. |
| You need its owner | controlled: true means yours; false means a visible enemy. |
| A field is missing | Its value is unknown or does not apply. The server does not send null. |
{
"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
| Field | Format | Required | Meaning |
|---|---|---|---|
status | "ACTIVE" or "RESPAWNING" | Yes | Whether the player has an active Core or is waiting to respawn. |
respawn_at_tick | positive int64 | Only when respawning | Tick of the next respawn attempt. |
resources | integer ≥ 0 | Yes | Resources stored by the Core; Worker cargo is separate. |
population | integer ≥ 0 | Yes | Living owned Units; the Core is not counted. |
population_tier | integer ≥ 0 | Yes | floor(population / 20). |
upkeep_next_tick | integer ≥ 0 | Yes | tier × (tier + 1) / 2 for the current population. |
champion_beacon | object | Yes | Public position and, when visible, carrier state. |
objects | array | Yes | Owned entities plus currently visible terrain and enemies. |
events | array | Yes | Resolution 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.
kind | Represents | Identity |
|---|---|---|
"CORE" | One Core | id |
"UNIT" | One Worker, Vanguard, or Ranger | id |
"OBSTACLE" | All visible obstacle cells | Individual positions |
"RESOURCE" | All visible resource cells | Individual positions |
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]]
}
| Field | Format | Meaning |
|---|---|---|
kind | "OBSTACLE" or "RESOURCE" | Terrain type. |
positions | non-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
{
"kind": "CORE",
"id": "2ea3c3dc-42b0-4b92-9754-7558bd4ff834",
"controlled": true,
"position": [12, 8],
"hp": 5,
"shield": 4,
"state": "NORMAL"
}
{
"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]
}
| Field | Format | Required |
|---|---|---|
kind | "CORE" | Yes |
id | UUID | Yes |
controlled | boolean | Yes |
position | [x, y] | Yes; remains the origin while moving |
hp | integer ≥ 0 | Yes |
shield | integer ≥ 0 | Yes |
state | "NORMAL" or "MOVING" | Yes |
move_direction | direction string | Moving only |
move_progress | integer ≥ 1 | Moving only |
move_required_ticks | integer ≥ 1 | Moving 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
{
"kind": "UNIT",
"id": "9d3e4941-2816-4a39-a220-df8cd95e877d",
"controlled": true,
"position": [11, 8],
"hp": 2,
"unit_type": "WORKER",
"cargo": 1
}
| Field | Format | Required |
|---|---|---|
kind | "UNIT" | Yes |
id | UUID | Yes |
controlled | boolean | Yes |
position | [x, y] | Yes |
hp | integer ≥ 0 | Yes |
unit_type | "WORKER", "VANGUARD", or "RANGER" | Yes |
cargo | integer ≥ 0 | Owned 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
| Data | Included when | Hidden fields |
|---|---|---|
| Owned Core and Units | Always | None from their object format |
| Enemy Core and Units | Their cell is currently visible | Owner identity; enemy Worker cargo |
| Terrain | Its cell is currently visible | Resource quantity |
| Beacon position | Always | None |
| Beacon status and carrier | Beacon cell is currently visible | Both 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:
- obstacle batch;
- resource batch;
- owned Core;
- owned Units by UUID;
- visible enemy Cores by UUID;
- 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.