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 a Core ownerRead owner_username and add @ only when displaying it.
You need a Unit ownercontrolled: true means yours; false means a visible enemy. Unit owner identity stays private.
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": 5,
"population": 1,
"champion_beacon": {"position": [0, 0]},
"objects": [
{
"kind": "CORE",
"id": "2ea3c3dc-42b0-4b92-9754-7558bd4ff834",
"controlled": true,
"owner_username": "arena_hero",
"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 for a spawn retry.
respawn_at_tickpositive int64Only when respawningTick of the next spawn attempt after a placement failure.
resourcesinteger ≥ 0YesResources stored by the Core, capped at max(10, population × 5); Worker cargo is separate.
populationinteger ≥ 0YesLiving owned Units; the Core is not counted.
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. Core destruction normally respawns in the same Tick, so RESPAWNING is published only during initial admission or after the resolver cannot find a legal spawn. The resource and population fields remain, but you have no Core until CORE_RESPAWNED arrives.

Use population to estimate the next Unit price: round_half_up(base_price × (13/10)^k), where k = max(0, floor((population - 20) / 5) + 1). The server settles the price after same-Tick self-destruction and combat, so the spawn result event is authoritative.

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, currently available resource pointsIndividual 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"Visible map-feature type.
positionsnon-empty array of [x, y]Visible cells, sorted by x and then y.

All visible positions of one kind arrive in a single entry. If a kind is missing altogether, none of its positions are currently visible. These batches carry no id, no controlled, no HP, and no resource quantity.

OBSTACLE positions are permanent terrain. RESOURCE positions are current availability, not permanent terrain memory. They may be natural points or cargo piles left by dead Workers. One successful harvest consumes a natural point; a partially recovered cargo pile keeps the same position present. Replenishment may later create a natural replacement elsewhere in the chunk.

Core

Normal Core
{
"kind": "CORE",
"id": "2ea3c3dc-42b0-4b92-9754-7558bd4ff834",
"controlled": true,
"owner_username": "arena_hero",
"position": [12, 8],
"hp": 5,
"shield": 4,
"state": "NORMAL"
}
Moving Core
{
"kind": "CORE",
"id": "2ea3c3dc-42b0-4b92-9754-7558bd4ff834",
"controlled": true,
"owner_username": "arena_hero",
"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
owner_username3–24 lowercase letters, digits, or underscoresYes
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. Every Core includes its owner's public username without a leading @; display it as @owner_username. A visible enemy Core exposes the same Core fields 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 CoreIts cell is currently visibleInternal owner ID and account details other than owner_username
Enemy UnitsTheir cell is currently visibleOwner identity; enemy Worker cargo
Obstacles and resource pointsTheir cells are currently visibleResource quantity
Beacon positionAlwaysNone
Beacon status and carrierBeacon cell is currently visibleBoth fields outside vision

Nothing here carries a last-seen timestamp. Remembered obstacles stay valid, but remembered resource points can be stale until their cells are visible again. Keep both kinds of exploration memory apart from current server state and do not treat an out-of-vision resource coordinate as currently available.

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.