Skip to main content

Terrain Height Cache Fix (February 2026)

Commit: 21e0860993131928edf3cd6e90265b0d2ba1b2a7
Author: Ting Chien Meng (@tcm390)

Summary

Fixed a consistent 50m offset in terrain height lookups caused by incorrect tile index calculation and grid coordinate mapping. The bug affected pathfinding, resource spawning, and player positioning.

Symptoms

  • Players floating ~50m above ground
  • Resources (trees, rocks) spawning in mid-air
  • Pathfinding failures (incorrect walkability checks)
  • Incorrect terrain color lookups

Root Cause

getHeightAtCached() had two bugs:

Bug 1: Tile Index Calculation

Problem: PlaneGeometry is centered at origin with range [-50, +50], but Math.floor(worldX / TILE_SIZE) assumes origin at [0, 0]. Example:
  • World position: x = 25 (should be tile 0)
  • Broken calculation: Math.floor(25 / 100) = 0 ✅ (accidentally correct)
  • World position: x = -25 (should be tile 0)
  • Broken calculation: Math.floor(-25 / 100) = -1 ❌ (wrong tile!)

Bug 2: Grid Index Calculation

Problem: PlaneGeometry vertices are in range [-50, +50], but grid indices are [0, 100]. The formula needs to add halfSize to shift the range. Example:
  • Local position: x = 0 (center of tile, should be grid index 50)
  • Broken calculation: Math.floor(0) = 0 ❌ (wrong index!)
  • Correct calculation: Math.floor(0 + 50) = 50

Fix

Canonical Helper Functions

worldToTerrainTileIndex() - Convert world coordinates to tile indices:
localToGridIndex() - Convert local tile coordinates to grid indices:

Updated getHeightAtCached()

Updated getTerrainColorAt()

Also fixed comma-vs-underscore key typo:

Impact

Before Fix

  • Height lookups: ~50m offset (consistent error)
  • Color lookups: Always returned null (key mismatch)
  • Pathfinding: Incorrect walkability (wrong height data)
  • Resource spawning: Mid-air placement

After Fix

  • Height lookups: Accurate to terrain mesh
  • Color lookups: Correct biome colors
  • Pathfinding: Correct walkability checks
  • Resource spawning: Ground-level placement

Migration

For Users

No migration needed - fix is automatic on update. If you see floating/sinking issues after update:
  1. Clear browser cache
  2. Reload page
  3. Terrain cache will rebuild with correct calculations

For Developers

Use canonical helpers for all terrain coordinate conversions:
Don’t use:
  • Math.floor(worldX / TILE_SIZE) - doesn’t account for centered geometry
  • Math.floor(localX) - doesn’t account for PlaneGeometry range

Testing

Test Cases

packages/shared/src/systems/shared/world/tests/TerrainSystem.test.ts:

Visual Verification

TerrainSystem

File: packages/shared/src/systems/shared/world/TerrainSystem.ts Uses Height Cache For:
  • getHeightAt() - Primary height query (falls back to procedural if cache miss)
  • getTerrainColorAt() - Biome color lookup
  • Flat zone blending
  • Grass exclusion

PathfindingSystem

File: packages/shared/src/systems/shared/movement/BFSPathfinder.ts Impact: Walkability checks now use correct heights, preventing:
  • Paths through “air” (where terrain was actually solid)
  • Blocked paths (where terrain was actually walkable)

ResourceSystem

File: packages/shared/src/systems/shared/entities/ResourceSystem.ts Impact: Resources now spawn at correct ground level:
  • Trees no longer float
  • Rocks sit on terrain surface
  • Fishing spots at water level

Performance

Cache Performance

Before Fix:
  • Cache hit rate: ~95% (but wrong data)
  • Fallback to procedural: ~5%
After Fix:
  • Cache hit rate: ~95% (correct data)
  • Fallback to procedural: ~5%
  • Performance: Unchanged (same cache hit rate)

Coordinate Conversion Cost

worldToTerrainTileIndex(): ~5 arithmetic operations localToGridIndex(): ~3 arithmetic operations Negligible overhead compared to procedural height calculation (~1000 operations).

Known Limitations

Cache Key Format

Cache keys use underscore separator: ${tileX}_${tileZ} Don’t use:
  • Comma separator: ${tileX},${tileZ} (won’t find tiles)
  • Colon separator: ${tileX}:${tileZ} (won’t find tiles)

Grid Size Assumptions

Helpers assume:
  • PlaneGeometry is centered at origin
  • Grid size is even (e.g., 100, 200)
  • Tile size matches geometry size
If you change these assumptions, update the helpers accordingly.

References