ALGOHOL Docs

SciFi — Actions

The SciFi action vocabulary: every action type the ruleset accepts, with parameters, defaults, ranges, energy costs and outcomes.

Actions are submitted with POST /api/arenas/:arenaId/live/action. The request body, the response semantics and the session fields every command must carry are ruleset-agnostic and documented in Action envelope.

The list of types accepted in the current session is observation.arena.allowedActions. For what the unit perceives, see SciFi — Observation.


Channels

Each action carries a channel label in its actionResults entry. The channel classifies the action for telemetry and for the breakdown in self.lastActionDebug; it is not a lock, and actions on the same channel do not exclude one another.

Channel Actions
locomotion move
orientation aim
vision scan
combat siphon, project, deploy-drone
defense defend
utility grab, allow-tow, signal, transfer-energy, hack, withdraw

Direction convention

Every directionDegrees in this reference uses the same compass:

  • 0° = world north = −y
  • Angles increase clockwise: 90° = east = +x, 180° = south = +y, 270° = west = −x
  • Range is 0–359.99°

directionFrame selects what 0° is measured from:

directionFrame 0° means
"world" World north, as above. Absolute.
"body" (default) The unit's current facing. Relative.

Body frame uses the lower body's facing for move and the upper body's facing for aim. The two rotate independently, so a unit can move in one direction while looking in another.

To convert a world-space offset (dx, dy) toward a target into a world-frame heading:

directionDegrees = (degrees(atan2(dx, -dy)) + 360) mod 360

Energy economy

The reserve is self.energyReserve, with ceiling arenaBaseline.agentStats.energyMax. An empty reserve eliminates the unit.

Passive regeneration is arenaBaseline.agentStats.energyRegenPerSecond, which is 0 in the default arena configuration. There, the only source is standing in a recovery zone, which restores arenaBaseline.recoveryStationEnergyPerSecond per second.

Action Energy cost
move agentStats.motorEnergyCost × appliedAcceleration × dt per physics tick — not flat; see below
aim agentStats.aimEnergyCost (0 by default)
grab, allow-tow, signal, hack, withdraw 0
scan rangeMeters × (arcDegrees / 360) × 0.4 × durationSeconds
project The energyAmount specified
defend The energyAmount specified
transfer-energy The energyAmount specified
deploy-drone 25
siphon 0; gains energy

Worked example: a 360° scan at full range in a 15 m arena costs 15 × 1.0 × 0.4 × 1 = 6.0 — 6% of a 100-point reserve per second of scanning. At one scan every three seconds, a full reserve is spent in under a minute.

move has no equivalent flat-rate worked example, because its cost isn't per second or per tick of holding a command — it's per physics tick (30 Hz by default) proportional to how hard the motor is accelerating that tick. appliedAcceleration is near 0 while cruising at a steady, unobstructed speed, and rises toward agentStats.accelerationMps2 while starting, stopping, turning, or pushing against a wall. So the same command can cost close to nothing for several seconds of open cruising and then cost close to the full rate the moment it's blocked by terrain.

The cost of a resolved action appears in observation.events ("Execute <id>: cost 6.28e, reserve 93.72.") and in self.lastActionDebug when the stream is opened with actionDebug=1.


move

Apply thrust in a direction.

{
  "type": "move",
  "directionDegrees": 90,
  "directionFrame": "body",
  "speedMetersPerSecond": 2
}
Parameter Type Required Default Range
directionDegrees number yes 0–359.99°
directionFrame "body" | "world" no "body"
speedMetersPerSecond number no 2 0.1–2 m/s

Body frame is relative to the lower body facing. Movement costs agentStats.motorEnergyCost scaled by the motor's applied acceleration that physics tick — see Energy economy above. Nonzero (0.06 by default), but a steady cruise costs far less than that number suggests; accelerating, braking or being blocked costs close to it.

A move keeps executing until replaced by a new command or ~2 seconds pass without one, whichever comes first — including while the client is disconnected. Energy is charged against applied force, not distance travelled, so a move blocked by terrain still drains energy for that window. See actionResults[].actuatorFeedback.blocked in SciFi — Observation to detect this in real time.

Outcomes: intent-setmove has no dedicated rejection outcome; only the common ones below apply.


aim

Rotate the upper body toward a direction. This steers the vision arc.

{
  "type": "aim",
  "directionDegrees": 180,
  "directionFrame": "world"
}
Parameter Type Required Default
directionDegrees number yes
directionFrame "body" | "world" no "body"

Body frame is relative to the current upper body facing. Aiming costs 0 energy.

aim cancels an active scan, resetting vision to the passive arc (full range, 180°). A command containing both scan and a later aim keeps the aim and loses the scan.

Outcomes: rotatedaim has no dedicated rejection outcome; only the common ones below apply.


scan

Temporarily reshape the vision arc. Vision is passive and always on (see Vision); scan widens the arc beyond the passive 180° for a limited time, at a cost.

{
  "type": "scan",
  "arcDegrees": 360,
  "durationSeconds": 1
}
Parameter Type Required Default when omitted Range
rangeMeters number no Full sight range 0 – full sight range
arcDegrees number no 180 1–360°
durationSeconds number no 1 0–10 s

Omit rangeMeters to scan at full range. rangeMeters: 0 is a zero-metre scan, not an unlimited one, and blinds the unit for durationSeconds. Values above full sight range are clamped to it.

Full sight range is self.resolvedMaxSightRangeMeters, the distance to the arena's farthest corner. Range is therefore rarely the limit; arc, walls and line of sight are.

Cost is rangeMeters × (arcDegrees / 360) × 0.4 × durationSeconds, so a narrower arc or shorter duration is proportionally cheaper. durationSeconds: 0 returns outcome no-duration and has no effect.

While a scan is active, self.activeVisionArcDegrees and self.activeVisionRangeMeters reflect it and self.visionExpiresAtMs gives its expiry. Detected players receive an indication that they were scanned.

Outcomes: scanned, no-duration


siphon

Drain energy from a target unit.

{
  "type": "siphon",
  "targetPlayerId": "<player-id>",
  "energyAmount": 5
}
Parameter Type Required Default
targetPlayerId string yes
energyAmount number yes

There is no hard range cutoff: transfer scales continuously down to 0 with distance (1 - distance / arenaDiagonal), and is separately capped by the target's current reserve. Line of sight is required outright.

Outcomes: siphoned, no-transfer (resolved to 0 energy — too far, or target already empty), invalid-target (missing, self, or nonexistent), friendly-fire (target is a teammate), blocked (no line of sight), on-cooldown


project

Fire a projectile.

{
  "type": "project",
  "directionDegrees": 45,
  "elevationDegrees": 0,
  "directionFrame": "world",
  "energyAmount": 10
}
Parameter Type Required Default Range
directionDegrees number yes 0–359.99°
elevationDegrees number no 0 -90–90°
directionFrame "body" | "world" no "body"
energyAmount number yes ≥ 0

Firing is limited to 90° from the torso. In "body" frame directionDegrees must be between -90 and 90; in "world" frame the resolved direction must be within 90° of self.upperBodyFacingDegrees. Anything wider is rejected with outcome outside-torso-direction at no cost. To fire wider, aim first — in an earlier command, or as an earlier entry in the same actions array.

Repeated firing accumulates heat. Above agentStats.projectOverheatThreshold, projection locks for agentStats.projectOverheatLockSeconds; self.projectOverheatSecondsRemaining counts it down.

Outcomes: spawned, outside-torso-direction, out-of-energy, on-cooldown

Friendly fire is never blocked at the physics level: a projectile that physically connects with a teammate deals full damage. Only intentional single-target actions (siphon) refuse a teammate target.


defend

Raise a defensive shield, absorbing incoming damage.

{
  "type": "defend",
  "energyAmount": 5
}
Parameter Type Required
energyAmount number yes

Outcomes: shielded, out-of-energy


deploy-drone

Launch an autonomous scout drone.

{ "type": "deploy-drone", "directionDegrees": 0 }
Parameter Type Required Default Range
directionDegrees number no 0 0–359.99°

Costs 25 energy. Requires self.dronesRemaining > 0. Default behavior: frontier scouting. If an enemy is visible at deploy time: lock-on mode. Your own drones appear in observation.visibleDrones at any distance. The result's droneId field carries the deployed drone's id.

Drones never lock onto or chase a teammate. Their explosion, once triggered, damages everyone within radius including teammates.

Outcomes: deployed, no-energy, no-drones-remaining


grab

Grab a nearby corpse, world object, or consenting ally (allow-tow). Requires edge distance ≤ 0.2 m: edgeGap = centerDistance - actorRadius - targetRadius, using each unit's bodyRadiusMeters (or the object's radiusMeters). A living target must be a teammate with allowsTow set.

grab also releases: if the unit is already towing something, sending grab — with any targetId, or none — releases it instead of attempting a new grab. targetId is only read when nothing is currently being towed.

{
  "type": "grab",
  "targetId": "<player-or-object-id>"
}
Parameter Type Required
targetId string yes to start a new grab (outcome no-target if omitted); ignored while releasing

The grabbed target follows kinematically. The tether snaps beyond towParams.breakDistanceMeters. Movement and energy penalties apply based on cargo mass.

Outcomes: grabbed, released, no-target, out-of-range, not-towable (the football), already-towed, not-teammate, consent-required


allow-tow

Toggle consent to being towed by an ally. self.allowsTow reflects the current state.

{ "type": "allow-tow" }
{ "type": "allow-tow", "enabled": true }
Parameter Type Required Default
enabled boolean no flips the current state

targetId is accepted by the envelope but not read by this action — consent applies to any teammate's grab, not a specific one.

Outcomes: consent-enabled, consent-disabled — both always success: true


signal

Send a labeled message to all living teammates.

{
  "type": "signal",
  "kind": "attack",
  "targetId": "<player-id>"
}
Parameter Type Required Max length
kind string yes 16 chars
targetId string no

kind is delivered as-is; the engine treats it as a player-defined protocol label. targetId attaches a player reference for directional context.

Interception: enemies who are currently hacking a dead teammate receive this signal as an intercepted message.

Outcomes: sent, missing-kind


transfer-energy

Transfer energy from the acting unit's reserve to an adjacent teammate.

{
  "type": "transfer-energy",
  "targetId": "<player-id>",
  "energyAmount": 20
}
Parameter Type Required
targetId string yes
energyAmount number yes

Target must be alive, on the acting unit's team, and within edge distance ≤ 0.2 m (same edge-gap formula as grab). Actual transfer amount is capped by the acting unit's reserve and the target's energy headroom.

Outcomes: transferred, no-target, target-not-alive, not-teammate, out-of-range, no-energy


hack

Link to a dead enemy to intercept their team's signal traffic.

{
  "type": "hack",
  "targetId": "<corpse-player-id>"
}
Parameter Type Required
targetId string yes

Target must be a dead enemy within edge distance ≤ 0.2 m (same edge-gap formula as grab). While hacking, intercepted signals appear in observation.interceptedSignals. The link breaks automatically beyond 1.6 m.

Send hack with the same targetId again to release the link.

Outcomes: hacking, released, no-target, target-alive, not-enemy, out-of-range


withdraw

Leave the current match. Always present in allowedActions, and the only action available while a scenario has actions paused.

{ "type": "withdraw" }

No parameters, no energy cost. The unit is eliminated immediately, and gets exactly one more real frame — the one carrying outcome: "withdrawn" in actionResults — before subsequent frames switch to observation: null.


Common rejection outcomes

Outcome Cause
ruleset-disallowed Action type not in the current allowedActions
out-of-energy Insufficient energy reserve
on-cooldown Projection locked by overheat
full-stunned Unit is fully stunned. The whole command is discarded; only the command's first action gets this outcome, the rest get none at all. See Stun in SciFi — Observation.

Rejection outcomes appear in observation.actionResults on the next frame, not in the POST response.