TerrainGrid: add a query for the level a tile is drawn at, not the level it was sampled at #46

Closed
opened 2026-08-29 15:04:00 +01:00 by liamjd · 0 comments
Owner

TerrainGrid.level_at() returns a tile's sampled level, which for a submerged tile is its seabed. But _paint_cells() deliberately draws submerged tiles at _datum_index instead, so the sea reads as one flat surface rather than a lumpy bed:

# _paint_cells()
var draw_level := _datum_index if kind == Kind.WATER else level
set_cell_item(Vector3i(x, draw_level, z), _item_id(kind, _variant_for(x, z)))

So for water, surface_y(level_at(cell)) is not the height the tile is drawn at — it is somewhere below it, by however deep the seabed happens to be. Any caller that positions something on the ground by that expression puts it underwater.

There is no public query for the drawn level. height_at() already contains the correction, but it takes world-space X/Z and returns a global Y, so it does not fit a caller that has a cell and wants a level.

Where this already bites

#37's placement highlight draws one quad per footprint cell at surface_y(level_at(cell)). On a shoreline site the quads over water sink beneath the visible surface and disappear — exactly when the player most needs to see which tiles are rejecting the placement.

The building ghost in PlacementController reads the same expression to find the height of the tallest tile under the footprint, so it has the same flaw.

Proposed change

Add a public query to TerrainGrid alongside level_at():

## The level a tile is *drawn* at, which is not its sampled level for water:
## _paint_cells() lifts submerged tiles to the datum so the sea reads as one
## surface. Callers positioning anything on the visible ground want this;
## callers reasoning about the terrain itself want level_at().
func drawn_level_at(cell: Vector2i) -> int

Then have height_at() use it rather than repeating the datum check inline, so the rule lives in one place.

Notes

  • _kind_for() is the existing test for water and already handles preset.has_water being off, so drawn_level_at() should go through it rather than comparing against _datum_index directly. A preset with water disabled has no water, and low ground is still ground — the same trap is_walkable() documents.
  • Out-of-bounds behaviour should match level_at(), which returns _datum_index rather than erroring.
  • Both queries need to stay: level_at() is the right answer for slope, pathfinding and PlacementRules, which reason about the terrain rather than about what is on screen. This is not a replacement.

Acceptance criteria

  • drawn_level_at() returns the datum for a submerged tile and the sampled level for dry land.
  • With preset.has_water off, it equals level_at() for every cell.
  • height_at() is expressed in terms of it rather than duplicating the check.
  • Covered in test/unit/test_terrain_*.gd: a submerged tile, a dry tile, a low dry tile on a no-water preset, and an out-of-bounds cell.
`TerrainGrid.level_at()` returns a tile's *sampled* level, which for a submerged tile is its seabed. But `_paint_cells()` deliberately draws submerged tiles at `_datum_index` instead, so the sea reads as one flat surface rather than a lumpy bed: ```gdscript # _paint_cells() var draw_level := _datum_index if kind == Kind.WATER else level set_cell_item(Vector3i(x, draw_level, z), _item_id(kind, _variant_for(x, z))) ``` So for water, `surface_y(level_at(cell))` is **not** the height the tile is drawn at — it is somewhere below it, by however deep the seabed happens to be. Any caller that positions something on the ground by that expression puts it underwater. There is no public query for the drawn level. `height_at()` already contains the correction, but it takes world-space X/Z and returns a global Y, so it does not fit a caller that has a cell and wants a level. ## Where this already bites #37's placement highlight draws one quad per footprint cell at `surface_y(level_at(cell))`. On a shoreline site the quads over water sink beneath the visible surface and disappear — exactly when the player most needs to see which tiles are rejecting the placement. The building ghost in `PlacementController` reads the same expression to find the height of the tallest tile under the footprint, so it has the same flaw. ## Proposed change Add a public query to `TerrainGrid` alongside `level_at()`: ```gdscript ## The level a tile is *drawn* at, which is not its sampled level for water: ## _paint_cells() lifts submerged tiles to the datum so the sea reads as one ## surface. Callers positioning anything on the visible ground want this; ## callers reasoning about the terrain itself want level_at(). func drawn_level_at(cell: Vector2i) -> int ``` Then have `height_at()` use it rather than repeating the datum check inline, so the rule lives in one place. ## Notes - `_kind_for()` is the existing test for water and already handles `preset.has_water` being off, so `drawn_level_at()` should go through it rather than comparing against `_datum_index` directly. A preset with water disabled has no water, and low ground is still ground — the same trap `is_walkable()` documents. - Out-of-bounds behaviour should match `level_at()`, which returns `_datum_index` rather than erroring. - Both queries need to stay: `level_at()` is the right answer for slope, pathfinding and `PlacementRules`, which reason about the terrain rather than about what is on screen. This is not a replacement. ## Acceptance criteria - `drawn_level_at()` returns the datum for a submerged tile and the sampled level for dry land. - With `preset.has_water` off, it equals `level_at()` for every cell. - `height_at()` is expressed in terms of it rather than duplicating the check. - Covered in `test/unit/test_terrain_*.gd`: a submerged tile, a dry tile, a low dry tile on a no-water preset, and an out-of-bounds cell.
Sign in to join this conversation.
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
liamjd/UntitledColonyBuilder#46
No description provided.