# World Coordinate System and Camera

Underroot is a vertically deep 2D world where digging goes *down* into positive Y. The coordinate system, the tile size, the horizontal landmarks that structure the map, and the camera's default framing and clamping.

## Y is down; the surface is world Y 0

The ground surface sits at world Y **0**. Underground is **positive Y** — tiles go downward as the player digs, and depth increases with Y. There is no negative-Y underground; the sky and village scenery occupy the small negative-Y band above the surface.

`GameManager._on_tile_dug()` treats the dug tile's `y` directly as depth:

```gdscript
func _on_tile_dug(tile_pos: Vector2i, _mat: String) -> void:
    current_run.blocks_mined = int(current_run.get("blocks_mined", 0)) + 1
    var depth: int = tile_pos.y
    if depth > int(current_run.get("depth_reached", 0)):
        current_run.depth_reached = depth
```

## TILE_SIZE and coordinate conversion

The canonical tile size is `TILE_SIZE = 32` pixels, defined once in `scripts/core/GameConstants.gd` as `GameConstants.TILE_SIZE`. Controller and display scripts source it from there (`const TILE_SIZE := GameConstants.TILE_SIZE`) rather than hardcoding 32.

Tile coordinates are `Vector2i`; world (pixel) coordinates are `Vector2`. Conversion is a straight multiply by the tile size — a tile at `(x, y)` maps to world `(x * 32, y * 32)`. `World.gd` performs this inline when following activity:

```gdscript
_camera_target = _clamped_target(Vector2(pos.x * 32.0, pos.y * 32.0))
```

`WorldManager` stores tiles keyed by `Vector2i` (`modified_tiles`, `walls`, `rooted_walls`, `_connected`) and works in tile space throughout; pixel conversion happens at the display and camera layers.

## Horizontal landmarks

The map is organised left-to-right. The forest is on the left (negative X), the village and base sit near X 0, the east defense zone is to the right, and the Maw approaches from the far right. The horizontal constants live in `scenes/world/World.gd`, with the west-side lines and the graveyard in `scripts/core/GameConstants.gd`.

| Constant | Value (world X px) | Meaning | Source |
|---|---|---|---|
| `WORLD_LEFT` | `-3200.0` | Left world boundary | `World.gd` |
| `WORLD_RIGHT` | `1000.0` | Right world boundary | `World.gd` |
| `FOREST_RIGHT` | `-250.0` | Forest / base boundary | `World.gd` |
| `DEFENSE_LEFT` | `250.0` | Start of the east defense zone | `World.gd` |
| `MAW_FRONT_X` | `700.0` | East Maw's approach line | `World.gd` |
| `DEFENSE_RIGHT` | `-1500.0` (`= WEST_WALL_LINE_X`) | West defense-line marker (Two Fronts) | `World.gd` / `GameConstants.gd` |
| `WEST_WALL_LINE_X` | `-1500.0` | Inner edge of the west build zone | `GameConstants.gd` |
| `WEST_BUILD_OUTER_X` | `-2195.0` | Outer (forest-side) edge of the west build zone | `GameConstants.gd` |
| `GRAVEYARD_X` | `-2050.0` | Centre of the deep-forest graveyard hill | `GameConstants.gd` |

> The architecture guide points to `WorldManager.gd` for `WORLD_LEFT` / `WORLD_RIGHT`, but these constants (and the other X landmarks) actually live in `scenes/world/World.gd`. `WorldManager.gd` works in tile space and holds no world-boundary constants.

The vertical extent is bounded by the camera limits, also in `World.gd`: `CAM_Y_MIN = -345.0` (top edge, village sky) and `CAM_Y_MAX = 10976.0` (bottom edge, the quartz floor). Depth layer bands are defined in the `LAYERS` array in `World.gd`, whose Y ranges match `data/layers.json` multiplied by 32.

## The camera

The camera is a `Camera2D` child of `World` (`scenes/world/World.tscn`), driven by `World.gd`. Its scene-default transform is:

```text
position = Vector2(0.0, 120.0)
zoom     = Vector2(0.85, 0.85)
```

So the default position is `(0, 120)` at zoom `0.85`. On a fresh start or a shallow save (`depth_reached == 0`), `World._ready()` pans the view fully up to `CAM_Y_MIN` so the village and sky fill the screen; a deep save keeps its loaded depth instead.

The camera follows activity and moves downward as the player digs. `World.gd` retargets `_camera_target` on `tile_dug`, `wall_built`, and `machine_placed` (with a guard so an autonomous machine dig does not steal the view from the player), then lerps toward the target each frame. Manual control is WASD/arrow panning and mouse-wheel/drag scrolling, all suppressed while a full-screen menu registers an input block via `GameManager.is_world_input_blocked()`.

Every target passes through `_clamped_target()`, which keeps the view inside the world bounds accounting for the current zoom and viewport half-size:

```gdscript
func _clamped_target(pos: Vector2) -> Vector2:
    var vp := get_viewport_rect().size * 0.5 / camera.zoom
    return Vector2(
        clampf(pos.x, WORLD_LEFT + vp.x, MAW_FRONT_X - vp.x),
        clampf(pos.y, CAM_Y_MIN + vp.y, CAM_Y_MAX - vp.y)
    )
```

Note that the X clamp runs to `MAW_FRONT_X` (700), not `WORLD_RIGHT` (1000) — the player cannot scroll the view past the east Maw's line.

## Key files

- `scripts/core/GameConstants.gd` — `TILE_SIZE`, west-zone lines, graveyard X.
- `scenes/world/World.gd` — world boundaries, zone landmarks, layer bands, camera follow/clamp/shake.
- `scenes/world/World.tscn` — the `Camera2D` node's default `position` and `zoom`.
- `scripts/world/WorldManager.gd` — tile-space grid keyed by `Vector2i`.

## Related

- [The Scene Tree](/docs/underroot/the-scene-tree)
- [World and Terrain](/docs/underroot/world-and-terrain)
- [The Autoload Model](/docs/underroot/the-autoload-model)
- [GameManager and the Run Lifecycle](/docs/underroot/gamemanager-and-the-run-lifecycle)