SciFi — Observation
The player observation stream is a WebSocket. Each player-frame message carries a frame, and the frame carries the observation. The state field is universal (see Observation envelope). The fields described here are SciFi-specific.
Everything in observation reflects only what the unit can currently perceive. Walls, terrain, vision arc, and line of sight constrain all entity and zone visibility.
Frame layout
Session constants and per-tick data live at different levels of the frame:
{
"gaii": "<your-gaii>",
"playerId": "player-1",
"arenaBaseline": { "ruleset": "scifi", "boundary": {…}, "agentStats": {…}, … },
"arenaBaselineRevision": "<hash>",
"state": { "phase": "live", "generation": …, "sessionId": "…", … },
"observation": {
"arena": { "allowedActions": […], "modeParams": {…}, "worldZones": […] },
"self": {…},
"visibleTiles": […], "visiblePlayers": […], "events": […], "actionResults": […]
}
}
| Where | What | Cadence |
|---|---|---|
frame.arenaBaseline |
Session constants: ruleset, mode, mapWidth, mapHeight, minRequiredPlayers, boundary, agentStats, towParams, recoveryStationEnergyPerSecond |
Sent once, then only when it changes |
frame.observation.arena |
Per-tick arena state: allowedActions, modeParams, worldZones |
Every frame |
arenaBaseline is omitted from frames where it has not changed, so a client that reads it only from the current frame loses boundary and agentStats after the first frame. Cache it from whichever frame last carried it and refresh when arenaBaselineRevision changes.
Over HTTP, GET /api/arenas/:arenaId/players/:gaii/frame wraps the same frame:
{ "ok": true, "data": { "frame": { … } } }
frame.arenaBaseline
Session constants. Cache these; do not expect them on every frame.
| Field | Type | Description |
|---|---|---|
ruleset |
string | Always "scifi" |
mode |
string | Objective mode: "last-man-standing", "navigation", "king-of-the-hill", etc. |
mapWidth, mapHeight |
number | Arena size in tiles |
minRequiredPlayers |
number | Players needed before the match starts |
boundary |
object | Arena extent and elimination thresholds (see below) |
agentStats |
object | Unit parameters for this session (see below) |
towParams |
object | Tow and grab envelope; present when grab is allowed |
recoveryStationEnergyPerSecond |
number | Energy restored per second inside a recovery zone |
boundary
| Field | Type | Description |
|---|---|---|
kind |
string | "walled" (standard) or "open" (open-football only) |
minX, maxX |
number | Arena tile span on X axis |
minY, maxY |
number | Arena tile span on Y axis |
safeMinX, safeMaxX |
number | X span of the walkable interior |
safeMinY, safeMaxY |
number | Y span of the walkable interior |
penalty |
string | "none" (walled) or "out-of-bounds" (open) |
Every standard SciFi arena is "walled" with penalty: "none". The outermost ring of tiles is solid wall — impassable and sight-blocking — so a unit cannot walk off the edge. Arenas generated with an irregular footprint are enclosed the same way: the eroded boundary is re-walled.
The safe* values are the inside face of that wall, not a danger margin.
Out-of-bounds elimination comes from falling, not from walking: a unit is eliminated when z < -2.0 (void floor), or when its position exceeds the outer boundary by more than 1 m. In practice that means stepping off an elevated-platform or into a gap tile.
agentStats
Unit capability parameters for this session. Read on first observation; changes are re-broadcast if the arena is reconfigured.
| Field | Type | Description |
|---|---|---|
energyMax |
number | Maximum energy reserve |
energyRegenPerSecond |
number | Passive energy regeneration rate. 0 in the default arena configuration, in which case recovery zones are the only energy source. |
motorEnergyCost |
number | Cost coefficient for move: energy spent each physics tick is motorEnergyCost × appliedAcceleration × dt, not a flat per-tick charge. appliedAcceleration is near zero at a steady cruise speed and rises toward accelerationMps2 while speeding up, slowing down, or blocked by terrain. Nonzero (0.06) in the default SciFi configuration. |
aimEnergyCost |
number | Energy cost of aim. 0 in the default configuration. |
moveSpeedMetersPerAction |
number | Maximum movement speed |
accelerationMps2 |
number | Acceleration rate |
bodyTurnRateDegPerSec |
number | Rotation speed |
reactionTimeSeconds |
number | Action latency budget |
projectOverheatThreshold |
number | Heat level that triggers combat channel lockout |
projectOverheatLockSeconds |
number | How long the combat channel stays locked after overheat |
projectHeatDecayPerSecond |
number | Heat dissipation rate between shots |
projectCooldownSeconds |
number | Minimum time between project actions, independent of heat |
siphonCooldownSeconds |
number | Minimum time between siphon actions |
droneMaxCount |
number | Maximum drone supply per match |
sightRange |
number | The arena's diagonal length in meters, a fixed reference value. self.resolvedMaxSightRangeMeters is the value that actually gates visibility and varies with the unit's position. |
aimTurnRateDegPerSec |
number | Torso turn rate, derived from mass, radius and torque. Limits how fast aim can re-centre the vision arc. |
projectHeatPerShot |
number | Heat added to self.projectHeatLevel per project action. Independent of projectHeatDecayPerSecond below. |
Movement (move) and torso rotation (aim) each carry their own heat model, independent of the project overheat system above and independent of each other:
| Field | Type | Description |
|---|---|---|
motorHeatFactor |
number | Heat gained per unit of energy the move motor spends. 0 disables motor heat entirely. |
motorCoolingRate |
number | Heat lost per second, continuously, regardless of whether the unit is moving |
motorSafeHeat |
number | Once accumulated motor heat reaches this level, movement acceleration is derated by motorThermalDerate |
motorThermalDerate |
number | Fraction of movement acceleration lost while at or above motorSafeHeat (0.4 = 40% less acceleration) |
motorCriticalHeat |
number | Once accumulated motor heat reaches this level, motorDamageRate energy is drained from self.energyReserve every second until it cools back down |
motorDamageRate |
number | Energy drained from self.energyReserve per second while motor heat is at or above motorCriticalHeat — sustained overheating can eliminate the unit the same way running out of energy does |
aimHeatFactor |
number | Heat gained per unit of energy the aim motor spends. 0 in the default configuration — aiming carries no thermal cost by default. |
aimCoolingRate |
number | Heat lost per second, continuously, regardless of whether the unit is turning |
aimSafeHeat |
number | Once accumulated aim heat reaches this level, aim torque is derated by aimThermalDerate |
aimThermalDerate |
number | Fraction of aim torque lost while at or above aimSafeHeat |
aimCriticalHeat |
number | Once accumulated aim heat reaches this level, aimDamageRate energy is drained from self.energyReserve every second |
aimDamageRate |
number | Energy drained from self.energyReserve per second while aim heat is at or above aimCriticalHeat |
Heat accumulates as heat = max(0, heat + energyUsed × heatFactor − coolingRate × dt) every physics tick, where energyUsed is the energy the motor/aim actuator actually spent that tick. A 0 for any *SafeHeat/*CriticalHeat field disables that threshold. Heat and any resulting damage are internal engine state, not exposed directly in the observation — only their effect (reduced effective acceleration/torque, and self.energyReserve draining faster than the plain motorEnergyCost/aimEnergyCost numbers would predict) is externally visible.
towParams
Always present in SciFi (towing is always enabled).
| Field | Type | Description |
|---|---|---|
grabEdgeGapMeters |
number | Maximum edge distance to initiate a grab |
tetherClearanceMeters |
number | Clearance margin for the tow link |
breakDistanceMeters |
number | Distance at which the tow tether snaps |
loadCoefficient |
number | Movement and energy penalty factor per kg of cargo |
observation.arena
Per-tick arena state, present on every frame. Only these three keys are here; everything else is in frame.arenaBaseline above.
| Field | Type | Description |
|---|---|---|
allowedActions |
string[] | Action types the engine will accept right now |
modeParams |
object | Mode-specific parameters (see below) |
worldZones |
Zone[] | Zones currently visible to this unit |
allowedActions
Not constant across a match:
- Combat actions are absent when the arena runs with combat disabled.
- During a scenario pause the list collapses to
["withdraw"]only.
worldZones
Only zones the unit can currently observe. A zone becomes visible when it is within detection range and has line of sight, so the set of known zones grows as the unit explores.
| Field | Type | Description |
|---|---|---|
id |
string | Zone id |
type |
string | One of the zone types below |
position |
{x, y, z} |
Zone center |
radiusMeters |
number | Zone radius |
sizeX, sizeY |
number | undefined | Footprint for non-circular zones |
label |
string | undefined | Display label |
sequenceOrder |
number | undefined | Order index for navigation-ordered goals |
portalPairId |
string | undefined | Id of the paired portal zone |
surfaceNormal |
{x, y, z} | undefined |
Surface tilt, for zones on ramps or relief |
ramps |
array | undefined | Ramp entry points, for elevated-platform zones |
| Zone type | Description |
|---|---|
recovery |
Energy recovery station. Standing inside restores energy at arenaBaseline.recoveryStationEnergyPerSecond. self.recoveryContact tells you when you are inside one. |
portal |
Teleporter. Entering moves the unit to the paired portal. Portals come in pairs. |
drone-resource |
Drone resupply zone. Grants additional drone supply when occupied. |
elevated-platform |
Raised platform area. Improves line of sight; stepping off the edge eliminates the unit. |
navigation-objective |
Navigation mode goal. Reaching all goals ends the match. |
king-of-the-hill |
Zone control area. Occupying scores toward the win condition. |
Zones are often placed close to the arena wall, which is not a hazard.
modeParams
Varies by mode. Fields guaranteed present:
| Mode | objectiveType |
Additional fields |
|---|---|---|
last-man-standing |
"elimination" |
— |
navigation |
"reach-goal" |
goalCount, orderedGoals |
navigation-ordered |
"reach-goal" |
goalCount, orderedGoals: true |
king-of-the-hill |
"zone-control" |
scoreTarget, playerScores |
football |
"football" |
See below |
Team-keyed fields (teamProgress, teamScores, teamTileCounts) appear whenever any player has a team assigned — including a single cooperative team — not only in multi-team matches.
football
| Field | Type | Description |
|---|---|---|
goalTarget |
number | Score needed to win outright, independent of the timer |
winCondition |
string | "timer" (only the clock ends it), "goal-target" (only reaching the score ends it), or "both" |
combat |
boolean | Whether combat actions are enabled in this football configuration |
incapacitation |
string | "none", "respawn-delay", "permanent", or "freeze-in-place" — see below |
openness |
string | "open" or "cluttered" arena layout |
fallPenaltySeconds |
number | Time penalty applied for falling off the field |
resetEnergyOnKickoff |
boolean | Whether energy resets to full at each kickoff |
teamScores |
object | Goals scored, keyed by team id |
sides |
{sideA, sideB} | null |
The two team ids, once both are assigned |
actionsPaused |
boolean | true during a goal-reset pause; matches allowedActions collapsing to ["withdraw"] |
restartNotice |
object | null | {text, remainingSeconds, durationSeconds} during a kickoff/restart countdown |
playerPenaltyUntilMs |
object | Per-player penalty expiry timestamps, keyed by player id |
incapacitation governs what happens to a unit at zero energy, and is the field to read before relying on hack or on a corpse persisting: "permanent" behaves like other modes (the unit stays down, visibleCorpses persists, hack has a stable target); "respawn-delay" and "none" bring the unit back into play, so a corpse from these is transient or absent — hack may be present in allowedActions without ever having a usable target. Other modes don't set incapacitation in modeParams at all; they always behave as "permanent".
observation.self
Authenticated unit state.
Identity
| Field | Type | Description |
|---|---|---|
id |
string | In-arena player id, e.g. "player-1". Not the GAII. This is how other units refer to this one, and the value passed as targetPlayerId / targetId. |
gaii |
string | The algorithm's GAII — the value used in request bodies and endpoint paths |
name |
string | Display name |
team |
string | undefined | Team identifier |
ready |
boolean | Whether the unit has joined this session |
alive |
boolean | Whether the unit is alive |
Position and orientation
| Field | Type | Description |
|---|---|---|
position |
{x, y, z} |
World position in meters |
lowerBodyFacingDegrees |
number | Direction the body is facing; the reference for move in body frame |
upperBodyFacingDegrees |
number | Direction the torso is facing; the centre of the vision arc, and the reference for aim and project |
physicsVelocity |
{x, y, z} |
Current velocity |
physicsGrounded |
boolean | Whether the unit is standing on a surface |
surfaceNormal |
{x, y, z} |
Normal of the surface underfoot; tilts on ramps and relief |
lastMoveSpeedMetersPerSecond |
number | Speed actually achieved last tick |
resolvedMaxMoveSpeedMetersPerSecond |
number | Speed ceiling right now, after terrain and cargo penalties |
bodyRadiusMeters, bodyHeightMeters |
number | Physical dimensions |
The two facings are independent, so the unit can move in one direction while looking in another.
Vision
| Field | Type | Description |
|---|---|---|
resolvedMaxSightRangeMeters |
number | Maximum sight range — the distance to the arena's farthest corner |
activeVisionRangeMeters |
number | Sight range in effect right now |
activeVisionArcDegrees |
number | Field-of-view arc in effect right now, centred on upperBodyFacingDegrees |
visionStartedAtMs |
number | When the current active scan began; 0 when passive |
visionExpiresAtMs |
number | When the current scan lapses back to passive; 0 when passive |
seenPlayerIds |
string[] | Ids of players this unit has observed at some point this match |
The Vision section below describes what these mean in practice.
Energy and combat
| Field | Type | Description |
|---|---|---|
energyReserve |
number | Current energy. Reaching zero eliminates the unit. The ceiling is arenaBaseline.agentStats.energyMax; there is no maxEnergy field in self. |
shieldPool |
number | Shield energy currently absorbing damage |
projectHeatLevel |
number | Current weapon heat |
projectOverheatSecondsRemaining |
number | Time left on an overheat lock |
projectCooldownSecondsRemaining |
number | Time left before the next projection |
siphonCooldownSecondsRemaining |
number | Time left before the next siphon |
dronesRemaining |
number | Remaining drone supply |
stunExecutesRemaining |
number | Remaining partial-stun executes (halves the effectiveness of some actions; does not block anything) |
fullStunExecutesRemaining |
number | Remaining full-stun executes (blocks all actions) |
Recovery, tow and hacking
| Field | Type | Description |
|---|---|---|
recoveryContact |
boolean | Whether the unit is currently inside a recovery zone. Engine-computed; no need to derive it from zone geometry. |
recoveryOverlapRatio |
number | How much of the unit is inside the zone, 0–1; scales the recovery rate |
recoveryEnergyGainLastUpdate |
number | Energy gained from recovery on the last tick |
towingTargetId |
string | undefined | Id of the entity currently being towed |
towedById |
string | undefined | Id of the unit towing this unit |
allowsTow |
boolean | undefined | Whether this unit has consented to being towed |
hackingCorpseId |
string | undefined | Id of the enemy corpse currently being hacked |
activeChannels |
array | Actions resolved on the last tick, with their energy cost and labels |
agentStats is not carried inside self — it lives in frame.arenaBaseline.agentStats.
Two further self fields — lastActionDebug (latest command resolution breakdown, including exact energy cost) and lastContactFeedback (physics contact/collision detail) — are omitted by default and can be requested per stream. See Observation envelope — Optional detail fields.
Vision
A unit that has never scanned still has a full field of view. Passive vision is always on and costs nothing:
| Range | self.resolvedMaxSightRangeMeters — the distance to the arena's farthest corner |
| Arc | 180°, centred on self.upperBodyFacingDegrees |
Range is therefore rarely the limit. Arc, walls and line of sight are: nothing behind the unit is visible, and nothing through a wall or rift-wall tile.
aim re-centres the arc and costs agentStats.aimEnergyCost energy (0 by default). move changes position and costs agentStats.motorEnergyCost scaled by the motor's applied acceleration — see agentStats.motorEnergyCost above for the exact shape; a steady cruise costs little, accelerating or being blocked costs close to the stated rate.
scan replaces the passive arc with one you specify — up to 360° — for durationSeconds, and costs rangeMeters × (arcDegrees / 360) × 0.4 × durationSeconds. Two constraints follow from the implementation:
aimcancels an active scan, resetting vision to the passive 180° arc.rangeMeterscannot exceedresolvedMaxSightRangeMeters, andrangeMeters: 0is a zero-metre scan, not an unlimited one. Omit the field to scan at full range.
visibleTiles accumulates as terrain enters the arc, so the map is built by moving and aiming rather than by scanning from a fixed position.
Stun
Two independent mechanics, both counted in executes — resolved commands, not physics ticks or seconds — and both capped at 6.
Full stun (self.fullStunExecutesRemaining) blocks all actions. Triggered by falling: 1 execute per meter fallen, capped at 6. While active, submitting a command discards it entirely — the first action in the command gets outcome full-stunned in actionResults, the rest of the command's actions get no result at all, and events logs "Full stun: N executes remaining. Actions blocked." Submitting a command consumes one execute regardless of what the command contained; not submitting one does not.
Partial stun (self.stunExecutesRemaining) halves the effectiveness of move speed, scan range, and the energyAmount of project, defend and siphon. It does not block or reject anything: the action resolves normally at half magnitude, success/outcome in actionResults are unaffected, and the only visible signal is events logging "Stunned: 50% effectiveness. N stuns remaining." Triggered by:
| Trigger | Executes |
|---|---|
| Colliding with a wall at ≥0.8 m/s impact velocity | 1 |
| Being hit by a projectile | 3 |
A trigger raises stunExecutesRemaining to the higher of its current value and the trigger's amount (capped at 6) — repeated hits don't stack additively. Wall collisions only trigger a new stun while neither stun counter is already above zero; projectile hits always apply. Submitting a command consumes one execute from whichever counter is active, same as full stun.
Visible entities
All entity lists reflect what the unit can currently observe. Range, line of sight, and scan state determine what appears.
Entity id paths are not uniform across these lists — most are a direct [].id, but visiblePlayers nests it one level deeper:
| List | Id path |
|---|---|
self |
self.id |
teammates |
teammates[].id |
visiblePlayers |
visiblePlayers[].player.id |
visibleCorpses |
visibleCorpses[].id |
visibleDrones |
visibleDrones[].id |
observation.visiblePlayers
Living observed players within detection range and line of sight. The authenticated unit is represented by self; dead players are represented by visibleCorpses.
Each entry has three fields:
| Field | Type | Description |
|---|---|---|
player |
object | The observed unit, limited to externally observable fields (below) |
visible |
boolean | Whether the unit is currently observed (entries are only emitted when true) |
relative |
object | Offset from the authenticated unit: dx, dy, dz, manhattanDistance, chebyshevDistance, horizontalDirection, verticalDirection, heightDirection, bearing, elevationAngleDegrees |
player exposes externally observable fields for another unit:
player field |
Type | Description |
|---|---|---|
id |
string | Player id |
position |
{x, y, z} |
World position |
upperBodyFacingDegrees |
number | Facing direction |
team |
string | null | Team identifier |
bodyRadiusMeters |
number | Physical radius |
alive |
boolean | Always true here (dead units appear in visibleCorpses) |
observation.visibleCorpses
Dead players within detection range and line of sight. See the visibleCorpses structure (id, team, position, bodyRadiusMeters, towedById).
observation.visibleProjectiles
| Field | Type | Description |
|---|---|---|
id |
string | Projectile id |
position |
{x, y, z} |
Current position |
velocity |
{x, y, z} |
Current velocity |
ownerId |
string | Player who fired it |
observation.visibleDrones
The authenticated unit's own drones are always visible regardless of distance. Other players' drones are visible within normal detection range.
| Field | Type | Description |
|---|---|---|
id |
string | Drone id |
ownerId |
string | Owning player id |
position |
{x, y, z} |
Current position |
phase |
string | "scout", "seek", or "chase" |
observation.visibleObjects
World objects (crates, tow targets, etc.) within detection range.
observation.visibleTiles
Tiles within the unit's current sight cone. Terrain enters the observation stream as the unit discovers it, so this list grows as you explore.
| Field | Type | Description |
|---|---|---|
x, y |
number | Tile coordinates |
z |
number | Tile elevation; platforms sit above the base floor |
terrain |
string | Terrain type (see below) |
heightMeters |
number | Height of the tile's solid volume |
blocksMovement |
boolean | Whether the unit can walk into this tile |
blocksSight |
boolean | Whether the tile blocks line of sight through it |
frictionCoefficient |
number | Surface friction; low values are slippery |
absorptionCoefficient |
number | undefined | How much projectile energy the tile absorbs |
surfaceKind |
"flat" | "ramp" | undefined |
Whether the tile can be climbed |
rampDirection |
"north" | "south" | "east" | "west" | undefined |
Uphill direction of a ramp |
sizeX, sizeY |
number | undefined | Footprint for tiles larger than one cell |
riftWallDurability |
number | undefined | Remaining durability (rift-wall tiles only) |
blocksMovement is the field to route around. Movement on a straight bearing will stall against a wall.
Tile x/y are spaced 1 meter apart and share the same coordinate space as self.position — round a unit's position to the nearest integer to find the tile it currently occupies. arenaBaseline.mapWidth/mapHeight are given in the same meters/tiles, since the arena is a square grid with one tile per meter.
Terrain types:
| Terrain | Description |
|---|---|
floor |
Passable ground |
wall |
Impassable solid. The arena's outer ring is always wall. |
gap |
Void opening. Passable but unsupported — entering drops the unit and eliminates it. |
hazard |
Passable but drains energy continuously while occupied. |
rift-wall |
Destructible wall with finite durability. Projectiles reduce durability; at zero it becomes passable. |
Action feedback
Neither of these is returned by the action POST. Both arrive on the next frame after a command resolves.
observation.actionResults
One structured entry per action in the last command.
| Field | Type | Description |
|---|---|---|
commandId |
string | The command this result belongs to |
actionCount |
number | Position of this action within the command |
actionType |
string | Which action this is a result for |
channel |
string | undefined | Channel classification |
success |
boolean | Whether the action took effect |
outcome |
string | scanned, projected, out-of-energy, ruleset-disallowed, … |
details |
string | undefined | Extra text for the outcome |
targetPlayerId |
string | undefined | Target of siphon or hack |
blocked |
boolean | undefined | Whether physics prevented the action from having its full effect |
resolvedSpeedMetersPerSecond |
number | undefined | Speed actually applied, for move |
rangeMeters, arcDegrees |
number | undefined | Resolved values, for scan |
energySpent, energyGained |
number | undefined | Energy delta for this action |
projectileId |
string | undefined | Id of the projectile fired, for project |
droneId |
string | undefined | Id of the deployed drone, for deploy-drone |
directionDegrees, directionFrame, elevationDegrees |
number, string, number | undefined | Resolved aim/fire direction |
actuatorFeedback |
object | undefined | Physics result of a move, see below |
actuatorFeedback
Present on move results. This is the field to read for real-time collision detection: visibleTiles only reflects terrain the unit has already seen, so a unit can be blocked by a tile it has never observed, and blocksMovement gives no signal for that case.
| Field | Type | Description |
|---|---|---|
requested |
number | Speed requested by the action |
applied |
number | Speed the motor attempted to apply after acceleration/energy limits |
achieved |
number | Speed actually achieved this tick |
error |
number | applied − achieved |
saturation |
number | 0–1, how close the motor is to its output limit |
blocked |
boolean | Whether a physics contact prevented movement this tick |
slip |
number | undefined | Fraction of applied force lost to low friction |
energyUsed |
number | undefined | Energy spent this tick |
heat |
number | undefined | Motor heat added this tick |
damage |
number | undefined | Energy drained this tick from motor overheat |
stress |
number | undefined | Load factor from cargo or terrain |
fuelUsed |
number | undefined | Reserved, unused in SciFi |
observation.events
Human-readable log lines for this tick, including the exact energy a command cost:
"Execute e55dedfd: cost 6.28e, reserve 93.72."
"Arena started. 1 players ready. You may now submit actions."
For a structured version of the same energy breakdown, open the stream with actionDebug=1 and read self.lastActionDebug.
observation.incomingHits
Projectiles that struck this unit since the last observation.
| Field | Type | Description |
|---|---|---|
type |
string | Always "projectile" |
projectileId |
string | Projectile id |
ownerId |
string | Player who fired it |
position |
{x, y, z} |
Impact position |
originPosition |
{x, y, z} | undefined |
Where it was fired from |
bearingDegrees |
number | undefined | Direction the shot came from |
strength |
number | undefined | Energy carried by the projectile |
absorbed |
number | undefined | Energy absorbed by shield or terrain |
observation.contacts and observation.capabilities
contacts reports physics contact state for this tick, independent of whether a move was submitted. capabilities reports actuator feedback for movement, aim, body and combat: how the engine executed the last intent, which resolves cases where a command was accepted but the unit did not move as expected.
contacts:
| Field | Type | Description |
|---|---|---|
grounded |
boolean | Whether the unit is standing on a surface |
groundNormal |
{x, y, z} |
Normal of the surface underfoot |
contactNormals |
{x, y, z}[] |
Normals of all current physics contacts |
blockedDirections |
{x, y, z}[] |
Directions currently obstructed by a contact — the general-purpose collision signal, always populated regardless of the last submitted action |
frictionAvailable |
number | Friction currently usable, after load |
frictionCoefficient |
number | Friction of the surface underfoot |
slip |
number | Fraction of applied force lost to low friction |
recentEvents |
array | Recent contact events: kind ("wall", "player-alive", "player-dead"), impactVelocityMps, normal, otherEntityId |
capabilities.movement, capabilities.aim, capabilities.body and capabilities.combat each have the same shape as actionResults[].actuatorFeedback above, and update every tick regardless of whether a new command was submitted.
Teammates and signals
observation.teammates
Living teammates. Always visible, regardless of range or line of sight.
| Field | Type | Description |
|---|---|---|
id |
string | Teammate's player id |
position |
{x, y, z} |
World position |
energyReserve |
number | The teammate's current energy |
shieldPool |
number | The teammate's current shield |
alive |
boolean | Whether the teammate is alive |
bodyRadiusMeters |
number | Physical radius |
upperBodyFacingDegrees |
number | undefined | Direction the teammate's torso is facing |
towingTargetId, towedById |
string | undefined | Tow state |
observation.teamSignals
Signals sent by teammates since the last observation.
| Field | Type | Description |
|---|---|---|
fromId |
string | Sender's player id |
kind |
string | Signal label (max 16 chars, engine delivers as-is) |
position |
{x, y} | undefined |
Sender's position at send time |
targetId |
string | undefined | Player reference attached by the sender |
teamSignals is delivered exactly once: a signal appears in the one frame built after it was sent, and is gone from every frame after that — there is no backlog, and resync does not bring back a signal from a frame the client never read. A client that wants to reliably catch every signal must read teamSignals on every frame it receives, not just the frames following its own submitted actions.
observation.interceptedSignals
Signals intercepted via hack. Present when self.hackingCorpseId is set. Same structure as teamSignals, and delivered the same one-frame-only way. These originate from the hacked unit's team.