Movement & the World
The hex map, pathfinding, speed, stamina and world geography.
In plain terms (for players)#
In Taurion you pilot a vehicle (the game calls each one a "character" — e.g.
the starter ship is internally rv st but shown in the UI as Raider for Red,
Barracuda for Blue, Scarab for Green). The world is one giant map
made of six-sided hex tiles. To travel, you tap a destination and your client
sends the game a list of waypoints (corner points of your route); your ship
then walks tile-by-tile from one waypoint to the next.
A few rules shape every trip:
- Bigger, heavier ships move slower; scouts (Finder / Octo / Flea) are the fastest. Speed is a number you can read in §6.
- Mountains/walls (called obstacles on the map) block you completely, and so do buildings. Driving onto a tile where another ship already sits is allowed but very slow.
- Each faction has a large starter zone around its home: your own faction travels through it three times faster, but rival factions cannot enter it at all.
- You cannot teleport diagonally across the map: each leg of your route must run in a straight hex line. Your client's path-finder handles this for you.
Everything below is the precise, code-derived version of those rules, written for client developers. Player-facing display names (Trimideum, Raider, …) appear next to the internal codes throughout.
This document is the authoritative reference for how characters (vehicles) move across the Taurion world, how the hex world is laid out, what blocks movement, how speed is computed, and how waypoints are encoded into on-chain moves. It is derived directly from the Game State Processor (GSP) source, which is the single source of truth for consensus rules.
Jargon, once: GSP = Game State Processor, the program that every node runs to compute the canonical game state from on-chain moves. Consensus-relevant / consensus rule = a rule that affects that shared state, so all nodes must agree on it exactly. Axial coordinates = the
(x, y)scheme used to address hex tiles. L1 (hex) distance = the number of single-tile steps between two tiles. Edge weight = the movement-point cost to step onto a given tile. Waypoint = one(x, y)corner of a planned route. Fitment = an equippable module (weapon/utility) that modifies a vehicle's stats.
All rules below are consensus-relevant (they affect game state) unless explicitly marked as client-side helpers.
1. The hex coordinate system#
Taurion uses a hexagonal grid expressed in axial coordinates (x, y)
(proto/geometry.proto:28-32, hexagonal/coord.hpp:36-49). Internally each
coordinate is a signed 16-bit integer (HexCoord::IntT = int16_t,
hexagonal/coord.hpp:42).
A third "cube" coordinate
zis derived asz = -x - y(hexagonal/coord.tpp:76-80).The six neighbours of a tile are reached by adding the six principal direction vectors, in this exact enumeration order (
hexagonal/coord.tpp:181-189):Index Delta (x, y)Meaning 0 (+1, 0)East 1 (-1, 0)West 2 ( 0, +1)South-east 3 ( 0, -1)North-west 4 (+1, -1)North-east 5 (-1, +1)South-west This order matters: it determines how the path finder breaks ties between paths of equal length (
hexagonal/coord.tpp:179-180).L1 (hex) distance between two tiles is
(|Δx| + |Δy| + |Δz|) / 2(hexagonal/coord.tpp:140-148). This is the number of single-tile steps on the shortest unobstructed path and is used throughout the game (enter radius, spawn radius, safe-zone radius, path-finding range).
Principal directions and waypoints#
A "principal direction" between two tiles exists only when the two tiles lie on a
straight hex line, i.e. when one of Δx == 0, Δy == 0, or Δx + Δy == 0
(hexagonal/coord.tpp:117-138). Movement between consecutive waypoints must be
along a principal direction — see §6.
2. World map size and shape#
The map is loaded from a precomputed obstacle bit-vector. Its extent
(mapdata/basemap_tests.cpp:51-64, mapdata/tiledata.hpp:30-56):
| Property | Value | Source |
|---|---|---|
minY (axial) |
-4096 |
basemap_tests.cpp:54 |
maxY (axial) |
4095 |
basemap_tests.cpp:55 |
X range at y = 0 |
-4096 .. 4095 |
basemap_tests.cpp:59-63 |
| Total tile slots | 67,108,864 (= 2^26) |
tiledata.hpp:56 |
The X range varies per row (the map is roughly hexagon/disc shaped, not a
rectangle). For each y, minX[y] and maxX[y] define the valid X span
(tiledata.hpp:38-41, basemap.tpp:50-59). BaseMap::IsOnMap returns false for
any tile outside these per-row bounds (basemap.tpp:50-59).
numTiles = 67,108,864 is only an allocation/indexing constant; many of those
slots are off-map and unused.
The raw map data files (obstacledata.dat.xz, regiondata.dat.xz) are too large
for version control and must be fetched and placed in data/ before building
(data/README.md). They are compiled into the binary as blobs
(mapdata/tiledata.hpp:90-112).
3. Static obstacles (the base map)#
Each on-map tile is either passable or an obstacle, encoded as a single
bit in blob_obstacles (mapdata/tiledata.hpp:93-100, basemap.tpp:61-73).
BaseMap::IsPassable(c) returns false if the tile is off-map or its bit is 0
(basemap.tpp:61-73).
Base edge weight#
Path-finding and movement use an edge weight = the cost (in movement points)
to step from one tile to a neighbour. The base map weight is
(mapdata/basemap.tpp:75-82):
Condition (target tile to) |
Edge weight |
|---|---|
to is passable |
1000 |
to is an obstacle / off-map |
NO_CONNECTION (= 2^32 - 1, impassable) |
So a normal tile costs 1000 movement points to enter (basemap.tpp:78).
NO_CONNECTION = std::numeric_limits<uint32_t>::max() is the sentinel for "no
path" (hexagonal/pathfinder.hpp:47-54). The weight depends only on the
destination tile, never the source.
4. Safe zones and faction starter zones#
On top of the obstacle layer sit safe zones, defined in
proto/roconfig/safezones.pb.text. Each zone is a filled L1 disc: a centre, a
radius, and optionally a faction (proto/config.proto:464-479,
mapdata/safezones.cpp:60-74). Every tile with L1 distance <= radius of the
centre belongs to the zone (safezones.cpp:60-61). Zones may not overlap — the
config loader CHECK-fails on overlap (safezones.cpp:62-67).
Two kinds exist:
Faction starter zones (
faction: "r"|"g"|"b"): one per faction, radius 300 (safezones.pb.text:3-22).Faction Display Centre (x, y)Radius RED ( r)Red (1960, -2601)300 GREEN ( g)Green (-3472, 1824)300 BLUE ( b)Blue (547, 2497)300 Neutral no-combat zones: no faction set, radius 30, placed around all neutral / partner buildings (
safezones.pb.text:24-148). There are exactly 23 of them.
Both kinds are no-combat zones (SafeZones::IsNoCombat,
safezones.tpp:46-62) — that matters for combat, not movement, but is relevant
to building placement (§9).
Starter-zone effect on movement (movement.tpp:24-40)#
MovementEdgeWeight(map, faction, from, to) modifies the base weight based on
the destination tile's starter zone:
Destination to |
Moving faction | Resulting edge weight |
|---|---|---|
| Not a starter zone | any | base weight (1000) |
| Starter zone of my faction | matching | base / 3 (= 333 for normal tiles) |
| Starter zone of another faction | mismatched | NO_CONNECTION (blocked) |
So a faction's starter zone is a wall to outsiders but gives its owners 3×
faster travel inside (integer division: 1000 / 3 = 333,
movement.tpp:38). Note the modifier applies on the step into the starter
tile; moving out of one's own starter zone onto a normal tile costs the normal
1000 (movement_tests.cpp:266-290).
The official web client mirrors these exact numbers: base 1000, safe zone 333,
vehicle slowdown 8000, blocked = NO_CONNECTION.
5. Dynamic obstacles (buildings and vehicles)#
Beyond the static map there is a per-block dynamic obstacle layer
(src/dynobstacles.hpp:42-123) holding all buildings and all vehicles. It is
rebuilt from the database every block before movement is processed
(src/dynobstacles.cpp:32-50, src/logic.cpp:101).
FullMovementEdgeWeight combines the base map weight with dynamic obstacles
(src/movement.cpp:133-154):
Destination tile to |
Effect on edge weight |
|---|---|
| Covered by a building footprint | NO_CONNECTION — fully blocked (movement.cpp:147-148) |
| Occupied by another vehicle | base weight × 8 (movement.cpp:150-151) |
| Otherwise | unchanged |
MULTI_VEHICLE_SLOWDOWN = 8 (src/movement.hpp:43). So stepping onto a tile
that already has a vehicle costs 1000 × 8 = 8000 points (or 333 × 8 inside a
friendly starter zone). Vehicles are therefore passable but very slow to
move through; buildings are never passable.
- From and to must differ — the function CHECK-fails on
from == to(movement.cpp:141). The moving vehicle is temporarily removed from the dynamic map before its own step is computed, so it never sees itself as an obstacle (MoveInDynObstacles,movement.cpp:352-369). DynObstacles::IsFree(c)(no building and no vehicle) is used for building placement and spawn-tile selection, not for movement (dynobstacles.hpp:84-88).
Worked example from the test suite (movement_tests.cpp:243-264): with a base
weight of 10 and a vehicle sitting on (0,0), stepping (1,0) → (0,0) costs
10 × 8 = 80; stepping away (0,0) → (1,0) costs the plain 10; a building at
(123,0) makes (123,1) → (123,0) return NO_CONNECTION.
6. Movement speed#
6.1 The speed stat (milli-tiles per block)#
A vehicle's base movement speed is configured per vehicle type as speed, in
milli-tiles per block (proto/config.proto:120-121). Because the base edge
weight of a tile is 1000, speed is numerically "movement points accumulated
per block" and a vehicle with speed = 1000 advances exactly one normal tile
per block. A vehicle with speed = 4500 accumulates 4500 points/block, i.e. up
to 4.5 normal tiles/block.
Speed is stored on the character proto as c.proto.speed() and is set during
stat derivation from the vehicle config plus fitments (src/fitments.cpp:137,
:237). It is exposed in game-state JSON as speed
(src/gamestatejson.cpp:275).
Vehicle base speeds (all factions, proto/roconfig/items/vehicles_*.pb.text)
Speeds are identical across the three factions for equivalent classes (red/blue/
green share the same numbers; vehicles_others.pb.text). Display names from
the official web client.
Code (per faction prefix rv/bv/gv) |
Red / Blue / Green display | speed |
|---|---|---|
*v st (starter) |
Raider / Barracuda / Scarab | 4500 |
*v s |
Looter / Urchin / Mantis | 4500 |
*v m |
Pillager / Crocodile / Centipede | 3500 |
*v l |
Marauder / Moray / Millipede | 2500 |
*v sc (scout) |
Finder / Octo / Flea | 5500 |
*v mt (medium transport) |
Blood Carrier / Cobra / Horse | 3500 |
*v lt (large transport) |
Bone Carrier / Trident / Ox | 2500 |
*v vlt (very large transport) |
Skull Carrier / Hydra / Elephant | 2000 |
*v sa (small attack) |
Devourer / Gorgon / Bee | 4500 |
*v ma (medium attack) |
Exterminator / Siren / Wasp | 3500 |
*v la (large attack) |
Devastator / Poseidon / Hornet | 3000 |
*v vla (very large attack) |
Annihilator / Leviathan / Widow | 2500 |
(Starter vehicles rv st/bv st/gv st are speed: 4500,
vehicles_starter.pb.text:11,35,60.)
6.2 Fitment speed bonuses (Energy Enhancement / "turbo")#
The only fitments that change base speed are the Energy Enhancement line
(internal * turbo), which add a percent StatModifier to speed
(fitments.pb.text lf/mf/hf/vhf turbo; applied at fitments.cpp:213, :237):
| Fitment code | Display name | Speed StatModifier |
|---|---|---|
lf turbo |
Light Energy Enhancement | +10% |
mf turbo |
Medium Energy Enhancement | +20% |
hf turbo |
Heavy Energy Enhancement | +30% |
vhf turbo |
Very Heavy Energy Enhancement | +40% |
StatModifier math (src/modifier.hpp:79-91): `result = base + base*percent/100
- absolute
. **Modifiers from multiple fitments are summed first, then applied once** (not compounded) (fitments.cpp:165-167,:213). E.g. twovhf turbogive+80%, not1.4 × 1.4`.
6.3 Combat slow effects (Temporal Disrupter / "retarder")#
Some weapons apply a runtime speed-reduction effect to enemy vehicles (a
combat effect, not a fitment on the slowed vehicle). These are the Temporal
Disrupter (* retarder) and Wide Temporal Disrupter (* longretard) weapons
(fitments.pb.text lines ~263-465; display names "Light/…/Very Heavy Temporal
Disrupter" / "… Wide Temporal Disrupter" in the official client). The two
differ in how they hit:
* retarderhasarea: 3but norangefield, so it is an area-of-effect burst centred on the attacker itself — it slows every enemy within L1 distance 3 of the firing vehicle, with no individual target picked (fitments.pb.text:263-364;area/rangesemanticsproto/combat.proto:96-111).* longretardhasrange: 4andarea: 2, so it picks a target up to L1 distance 4 away and applies a smaller AoE (radius 2) around that target — hence "Wide" / longer-reach (fitments.pb.text:365-465).
The percent slow applied to each affected enemy:
| Weapon code | Display | Speed effect on target |
|---|---|---|
lf retarder |
Light Temporal Disrupter | -15% |
mf retarder |
Medium Temporal Disrupter | -20% |
hf retarder |
Heavy Temporal Disrupter | -25% |
vhf retarder |
Very Heavy Temporal Disrupter | -30% |
lf longretard |
Light Wide Temporal Disrupter | -10% |
mf longretard |
Medium Wide Temporal Disrupter | -15% |
hf longretard |
Heavy Wide Temporal Disrupter | -20% |
vhf longretard |
Very Heavy Wide Temporal Disrupter | -25% |
These set the character's effects.speed StatModifier, which is read every
block in GetCharacterSpeed (src/movement.cpp:161-177).
6.4 Effective speed computation (movement.cpp:161-177)#
Each block, the effective speed is computed as:
- Start from base
c.proto.speed()(vehicle + turbo fitments already baked in). - Apply the combat
effects.speedStatModifier(movement.cpp:167-168). - Floor at 0: if the result is negative it becomes 0 — the vehicle does not
move but stays "moving" with its waypoints intact
(
movement.cpp:169-170; testCombatEffectBelowZero,movement_tests.cpp:467-476). - If a chosen speed cap is set, take
min(effective, chosen_speed)(movement.cpp:172-173).
Example (movement_tests.cpp:455-465): base 10, effect -50%, chosen speed 2 →
effective min(10 - 5, 2) = 2.
6.5 Chosen speed cap (chosen_speed)#
Players may cap a vehicle's speed below its maximum (useful to keep a fast
vehicle in a slower convoy). It is stored as movement.chosen_speed
(proto/movement.proto:42-48) and set via the speed move field (§7.4).
- Valid range:
1 .. MAX_CHOSEN_SPEEDwhereMAX_CHOSEN_SPEED = 1,000,000(src/moveprocessor.hpp:59,src/moveprocessor.cpp:1396-1403). speed: 0, negatives, non-integers, or values> 1,000,000are rejected (moveprocessor.cpp:1385-1403; testsmoveprocessor_tests.cpp:1154,1193-1201).- Setting a chosen speed higher than the actual speed has no effect (the
minkeeps the real speed) (movement_tests.cpp:433-442). - A chosen speed can only be set on a character that is currently moving
(
moveprocessor.cpp:1388-1394). The handler runs after waypoints, so a single move may set bothwpandspeed(moveprocessor.cpp:1868-1873).
Exposed in JSON as movement.chosenspeed (gamestatejson.cpp:115-116).
7. Issuing movement: the move (RPC) format#
7.1 Top-level move envelope#
A Taurion move is a JSON object wrapped as g: { tn: <movedata> }, where tn
is the Taurion game id. Inside, character
commands live under the c key
(src/moveprocessor.cpp:175). c may be either a single object or an array
of command objects (moveprocessor.cpp:177-192). Each command must carry an
id field, which itself may be a single ID or an array of IDs to batch the
same operation across several characters (moveprocessor.cpp:203-217).
A command is only applied if the character exists and is owned by the move's
sender (moveprocessor.cpp:230-244).
Minimal structure:
{
"g": {
"tn": {
"c": [
{ "id": 1, "wp": "<encoded-waypoints>" }
]
}
}
}
7.2 Setting waypoints — wp (moveprocessor.cpp:559-619, applied at 1453-1483)#
wpis a string: the base64-of-deflate encoded waypoint list (§8).wp: nullis the explicit stop command — clears all movement (moveprocessor.cpp:570-578; the official client sends this for "stop").- Rejected if:
wpis not a string/null (:580-587); the characterIsBusy(an ongoing operation like prospecting/construction) (:589-595); the character is inside a building (:597-604); decoding fails (:606-613). - Setting waypoints first stops any current movement and mining
(
StopCharacter+StopMining,moveprocessor.cpp:1464-1465). - An empty decoded list is treated as a stop (no waypoints set)
(
moveprocessor.cpp:1467-1468). - Waypoints are ignored if the vehicle's base
speedis 0 (moveprocessor.cpp:1472-1478).
Example (from tests, moveprocessor_tests.cpp:1024-1034 — here the encoded
string is shown decoded for clarity):
{"g":{"tn":{"c":{"id":1,"wp":"<encodes [{\"x\":-3,\"y\":4},{\"x\":5,\"y\":0}]>"}}}}
→ character 1 gets waypoints (-3,4) then (5,0).
7.3 Extending a path — wpx (moveprocessor.cpp:621-659, applied at 1485-1499)#
wpx appends additional waypoints to the end of the current list without
restarting movement. It is valid only if the character is already moving
(has non-empty waypoints) (moveprocessor.cpp:638-644). wp and wpx may
appear in the same command; wp is processed first, then wpx
(moveprocessor.cpp:1871-1872; test moveprocessor_tests.cpp:1124-1145).
7.4 Chosen-speed cap — speed (moveprocessor.cpp:1380-1409)#
Integer 1 .. 1,000,000. See §6.5. Example:
{"id": 1, "wp": "<enc>", "speed": 1000} sets waypoints and caps speed to 1000
milli-tiles/block (moveprocessor_tests.cpp:1184-1186).
7.5 Enter / exit building — eb / xb (see §10)#
7.6 Processing order within one character command (moveprocessor.cpp:1847-1905)#
The order is fixed and lets several effects be combined in one move:
send(transfer character)prospect- change vehicle, then fitments
- start mining
- set waypoints (
wp), then extend (wpx), thenspeed - found building
- mobile refining
- drop (
drop) then pickup (pu) - enter building (
eb) then exit building (xb)
8. Waypoint encoding (the wp / wpx string)#
Waypoints are transmitted as a compact, compressed string. The exact pipeline
(src/movement.cpp:49-114, mirrored client-side in the official web client):
Encode: array of {x, y} → JSON string (no whitespace, e.g.
[{"x":-3,"y":4},{"x":5,"y":0}]) → raw DEFLATE compress → base64.
Decode: base64 → raw inflate → parse JSON array of {x, y}.
Critical implementation details:
- Compression uses Xaya's libxayautil
CompressJson/UncompressJson(movement.cpp:58,:80). This is raw DEFLATE,windowBits = -15, i.e. no zlib header. In JavaScript you must usepako.deflateRaw/pako.inflateRaw, not the zlib-wrapped variants. - Coordinates serialise as
{"x": <int>, "y": <int>}(CoordToJson/CoordFromJson). - DoS / consensus guard: decoding is limited to
MAX_WAYPOINT_SIZE = 1 MiB(1 << 20) of uncompressed JSON, and to a maximum JSON nesting depth of 3 (movement.cpp:45,:80-81). A path that compresses fine but decompresses to more than 1 MiB is rejected. This is consensus-relevant: it can make some very large waypoint moves invalid. "Normal paths across the whole map are only about 3-4 KiB" (movement.cpp:43). EncodeWaypointsitself also fails if the serialised JSON exceeds 1 MiB (movement.cpp:59-65; testTooLargeForEncoding,movement_tests.cpp:95-107).- Decode rejects: non-array JSON, entries that are not
{x:int, y:int}, extra nested fields exceeding depth 3, and oversized payloads (movement.cpp:90-110; testInvalidDataForDecode,movement_tests.cpp:109-136). An empty array is valid and round-trips (movement_tests.cpp:81-93).
Helper RPCs (non-state, client convenience)#
The GSP exposes non-state RPCs (no blockchain state needed; served by
NonStateRpcServer in pxrpcserver.cpp):
encodewaypoints(wp)— encodes a raw[{x,y},…]array to the string (pxrpcserver.cpp:361-384; stubrpc-stubs/nonstate.json:24-27). Returns the encoded string.findpath(exbuildings, faction, l1range, source, target)— runs the same path-finder the GSP uses and returns the route (pxrpcserver.cpp:233-359; stubnonstate.json:12). Output object:{ "dist": <total weight>, "wp": [ {x,y}, … ], "encoded": "<string>" }(pxrpcserver.cpp:353-356). It compacts collinear steps into the minimal set of waypoints (one per direction change) (pxrpcserver.cpp:327-345).exbuildingsis an array of building IDs to ignore as obstacles (so you can path into a building you intend to enter) (pxrpcserver.cpp:276-312).l1rangebounds the search radius (DoS guard) — only tiles within that L1 distance of the target are considered (pathfinder.hpp:102-117).- Returns
FINDPATH_NO_CONNECTIONif no route exists within range (pxrpcserver.cpp:322-325). - The path finder uses Dijkstra with the exact movement edge weights, incl.
starter-zone and vehicle-slowdown rules (
pxrpcserver.cpp:298-318).
The official web client either calls findpath/encodewaypoints over RPC or
computes paths locally with an identical WASM/JS pathfinder and pako.
9. How movement is processed each block#
Movement runs once per block, after moves are applied and after combat HP/mining,
before building-entry and target selection (src/logic.cpp:101-121):
ProcessAll(moves) → HP updates → ongoings → mining → ProcessAllMovement
→ ProcessEnterBuildings → FindCombatTargets
For each moving character (queried via QueryMoving,
movement.cpp:329-330), CharacterMovement runs
(movement.cpp:251-322):
- Compute effective
speed(§6.4). If 0, do nothing this block (movement.cpp:263-265). - Accumulate movement points:
partial_step += speed(movement.cpp:272-273).partial_stepis volatile state carried between blocks (proto/movement.proto:57-69), exposed asmovement.partialstep(gamestatejson.cpp:106-107). - Loop, stepping one tile at a time toward the first waypoint:
- If the current position already equals the next waypoint, that waypoint is
popped; duplicate / already-reached waypoints are silently skipped
(
movement.cpp:286-302; testDuplicateWaypoints,movement_tests.cpp:478-491). When the list empties, movement stops entirely (StopCharacter,movement.cpp:294-298). - The direction to the next waypoint must be a principal direction;
otherwise movement is stopped and the remaining waypoints discarded
(
movement.cpp:304-317; testWaypointsNotInPrincipalDirection,movement_tests.cpp:493-500). Clients must therefore only emit waypoints that are pairwise collinear — this is whatfindpathguarantees. - Each single step costs the edge weight of the target tile (§3-§5). The step
happens only if
partial_step >= edgeWeight; thenpartial_step -= edgeWeightand the position advances (movement.cpp:239-248). Several tiles can be crossed in one block if enough points accumulated (movement_tests.cpp:412-420). - When
partial_stepis insufficient for the next step, processing pauses until the next block (movement.cpp:239-243).
- If the current position already equals the next waypoint, that waypoint is
popped; duplicate / already-reached waypoints are silently skipped
(
Multiple vehicles in one block (movement_tests.cpp:620-652)#
Movement is processed ordered by character ID (QueryMoving). The dynamic
obstacle map is updated live as each vehicle moves, so if two vehicles aim for
the same tile in the same block, the lower ID wins the tile and the higher
ID is blocked there for that step.
10. Blocked steps and the retry mechanism#
When the next step is into an obstacle (building, off-map, enemy starter zone, or
— transiently — a vehicle), the step fails (movement.cpp:198-226):
partial_stepis reset to 0 and ablocked_turnscounter is incremented (movement.cpp:211-213), exposed asmovement.blockedturns(gamestatejson.cpp:108-109).- The character keeps retrying the same step each block, because the blocker
might just be a passing vehicle (
movement.cpp:206-209). - If
blocked_turnsexceedsparams.blocked_step_retries, movement is cancelled entirely (StopCharacter) (movement.cpp:217-223). - As soon as the way is clear again (even if a full step still isn't affordable),
blocked_turnsis reset to 0 (movement.cpp:228-237).
blocked_step_retries = 25 (mainnet and testnet — only params.pb.text:6 sets
it, and the testnet merge does not override it). Per the proto comment, this is
the number of retries, so movement is cancelled after N+1 = 26 consecutive
blocked turns (proto/config.proto:540-546; test logic
movement_tests.cpp:519-559).
Note (movement_tests.cpp:502-517): after a successful step that uses up all
points, the engine already tries (and may fail) the following step in the same
block, so the blocked counter can be incremented "for free" on the same block the
vehicle reaches the obstacle.
11. Buildings: footprints and enter / exit rules#
11.1 Building footprints block movement#
A building occupies a set of tiles defined by shape_tiles in its config,
rotated by the building's shape_trafo.rotation_steps (in 60° hex steps) and
translated to the building's centre (src/buildings.cpp:36-53,
GetBuildingShape). Every tile in that footprint is an absolute movement
blocker (movement.cpp:147-148).
Footprint sizes vary widely (counts from proto/roconfig/buildings/*.pb.text):
| Building (examples) | Footprint tiles | enter_radius |
|---|---|---|
r_r / g_r / b_r (refinery hut, smallest) |
9-13 | 8 |
*_rt (refinery/tower) |
19 | 6 |
*_ss (space station, largest player build) |
699-1149 | 150 |
ancient1/2/3 |
352-378 | 54-58 |
obelisk1/2/3 |
970-1075 | 31-35 |
enter_radius per building type ranges from 6 (*_rt) up to 150
(*_ss); ancient/obelisk structures are larger. The full table is in the
config; representative values: most red/green/blue named buildings sit in the
6-16 range. test building uses enter_radius: 5.
11.2 Queuing entry — eb (moveprocessor.cpp:661-727, applied 1501-1509)#
eb: <buildingId> sets an intent to enter; it does not move the
character. Rules:
eb: nullcancels a pending enter intent (moveprocessor.cpp:681-689).- Rejected if the character is already in a building
(
:670-677), the building doesn't exist (:700-707), or the building's faction is neither ANCIENT nor the character's own faction (:709-720). I.e. anyone may enter ancient buildings; otherwise only same-faction buildings. - Entry intent is not blocked by
IsBusy— it just sets a flag (moveprocessor.cpp:1891-1892). - Stored on the character as
enterbuilding(the target building ID), exposed in JSON when set (gamestatejson.cpp:271-272).
11.3 Actually entering (ProcessEnterBuildings, buildings.cpp:178-233)#
Each block, after movement, every character with an enter intent is checked:
- Skipped if
IsBusy(buildings.cpp:192-197). - If the target building was destroyed meanwhile, the intent is cleared
(
buildings.cpp:204-213). - The character enters only if the L1 distance from its position to the
building centre is
<= enter_radiusof that building type (buildings.cpp:215-221). Otherwise it keeps the intent and keeps moving toward it on later blocks. - On entry (
EnterBuilding,buildings.cpp:167-176): the vehicle is removed from the dynamic obstacle map, its position is replaced byinbuilding = <buildingId>, its combat target is cleared, movement and mining are stopped, and the enter intent is cleared.
Because entry is checked against the centre with a generous radius, a vehicle
typically enters automatically as soon as it is "near enough", and a combined
{ "wp": "<path to building>", "eb": <id> } move (the official client's "move
and enter") makes the vehicle path in and enter as soon as it
arrives.
11.4 While inside a building#
A character with inbuilding set has no map position
(gamestatejson.cpp:266-269) and cannot receive waypoints (wp is rejected,
moveprocessor.cpp:597-604). It can still do in-building actions (refit, trade,
construct, drop/pickup).
11.5 Exiting — xb (moveprocessor.cpp:729-767, applied 1511-1518)#
xb: {} (an empty object) exits the building immediately. Rules:
- The value must be an empty object — any non-object, or an object with
fields, is rejected (
moveprocessor.cpp:734-744; testsmoveprocessor_tests.cpp:2125-2156). - Rejected if the character is
IsBusy(:746-752) or not in any building (:754-760). - On exit (
LeaveBuilding,buildings.cpp:235-252): the game picks a spawn location near the building centre within itsenter_radius, sets the vehicle's position there, and re-adds it to the dynamic obstacle map.
11.6 Spawn-location selection on exit / spawn (spawn.cpp:39-114)#
ChooseSpawnLocation(centre, radius, …):
- Pick a uniformly random tile within L1
radiusof the centre (rejection sampling,RandomSpawnLocation,spawn.cpp:39-73). - From that point, scan outward in growing L1 rings and return the first tile
that is on the map, passable, and free of dynamic obstacles
(no building, no vehicle) (
spawn.cpp:84-113). Vehicles are skipped even though they are technically passable, so the exiting vehicle does not land on another (spawn.cpp:100-104).
New characters are spawned into a building (not onto the map): the spawn
building per faction is configured in params.spawn_areas
(spawn.cpp:147-151, params.pb.text:33-47):
| Faction | Spawn building_id |
|---|---|
RED (r) |
6 |
GREEN (g) |
4 |
BLUE (b) |
5 |
(Newly spawned vehicles: RED rv st, GREEN gv st, BLUE bv st, with one
lf gun fitted — spawn.cpp:126-143.)
12. State JSON: movement-related fields#
For a character, the game-state JSON includes (gamestatejson.cpp:255-281):
| Field | Meaning |
|---|---|
position |
{x, y} — present only when not in a building (:266-269) |
inbuilding |
building ID — present only when inside a building (:266-267) |
enterbuilding |
building ID of a pending enter intent, if any (:271-272) |
speed |
current effective base speed (vehicle + fitments), milli-tiles/block (:275) |
movement.waypoints |
array of remaining {x,y} waypoints (first = current target) (:118-122) |
movement.chosenspeed |
speed cap, if set (:115-116) |
movement.partialstep |
accumulated movement points toward the next tile (:106-107) |
movement.blockedturns |
consecutive blocked-step count (:108-109) |
The movement sub-object is omitted entirely when empty
(gamestatejson.cpp:279-281).
13. Mainnet vs testnet / regtest differences#
- Map data, edge weights, obstacle layer, safe zones, vehicle speeds, fitment
modifiers,
MULTI_VEHICLE_SLOWDOWN,MAX_CHOSEN_SPEED, waypoint encoding / size limits, andblocked_step_retries(25) are identical across mainnet (Polygon) and testnet —testnet.pb.textis an empty merge (proto/roconfig/testnet.pb.text:1) and the movement-relevant params are not overridden intest_params.pb.text. test_safezones.pb.textexists as a regtest-only variant of the safe-zone layout; tests run againstChain::REGTEST(basemap_tests.cpp:46). The three faction starter zones (radius 300) and neutral zones (radius 30) above are the production layout (safezones.pb.text).- Spawn building IDs (RED 6 / GREEN 4 / BLUE 5) come from initialised ancient
buildings; the same
params.spawn_areasare used on all chains. - Path-finding
l1rangeis supplied by the caller (client), not a chain param.
Open questions#
- Exact
minX[]/maxX[]per row. The precise map silhouette lives in the compiledobstacledata.datblob (not in version control,data/README.md); only the bounding box (Y ∈ [-4096, 4095], X ∈ [-4096, 4095] at the centre row) is verifiable frombasemap_tests.cpp. The full per-row X bounds and the obstacle bitmap itself could not be enumerated from source alone. - Per-building
shape_trafoused in production. Footprint shapes (shape_tiles) andenter_radiusare in config, but each placed building's actual rotation comes frominitial_buildings/ player foundation moves; the resulting absolute footprints are not enumerated here. obstacle/passable semantics of partner safe-zone interiors for movement. No-combat zones affect combat and building placement (buildings.cpp:76-80), but do not by themselves change movement edge weight (only faction starter zones do, §4). Confirmed from code; flagged only because the naming ("safe zone") might suggest movement effects that do not exist.- Speed-affecting items (resolved by exhaustive scan). Every
speed:modifier infitments.pb.textwas enumerated: the only positive fitmentspeedmodifiers are the four* turbolines (+10/+20/+30/+40%), and the only negative ones are combat effects on* retarder(-15/-20/-25/-30%) and* longretard(-10/-15/-20/-25%). No other fungible item carries aspeedStatModifier, so §6.2/§6.3 are complete for fitments. (Vehicle base speeds themselves are listed in §6.1.)