> ## Documentation Index
> Fetch the complete documentation index at: https://hyperscape-ai-mintlify-docs-update.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Terrain System

> Procedural terrain generation with worker-based height and normal computation

# Terrain System

The terrain system generates procedural 3D terrain using multi-layer Perlin noise with support for flat zones under stations and buildings. The system uses **worker-based computation** for height and normal calculations to maximize performance.

<Info>
  Location: `packages/shared/src/systems/shared/world/TerrainSystem.ts`
</Info>

## Architecture

### Worker-Based Computation

The terrain system offloads height and normal calculations to a Web Worker for optimal performance:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// From TerrainSystem.ts
// Height and normal computation happens in TerrainWorker
// Main thread only recomputes for tiles overlapping flat zones
private async generateTileGeometry(tileX: number, tileZ: number): Promise<void> {
  // Request worker computation
  const workerResult = await this.terrainWorker.computeTile(tileX, tileZ);
  
  // Use worker heights + normals directly for tiles without flat zones
  if (!this.tileOverlapsFlatZone(tileX, tileZ)) {
    this.applyWorkerResults(workerResult);
    return;
  }
  
  // Fall back to main-thread recomputation only for flat zone tiles
  this.recomputeWithFlatZones(tileX, tileZ);
}
```

**Performance Benefits:**

* Zero noise calls on main thread for non-flat-zone tiles
* Parallel computation across multiple tiles
* Normals computed in worker with centered finite differences
* Main thread only handles flat zone blending

### Height Parameter Synchronization

All terrain generation constants are centralized in `TerrainHeightParams.ts` to prevent parameter drift between main thread and worker:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// From TerrainHeightParams.ts
export interface NoiseLayerDef {
  scale: number;
  weight: number;
  octaves?: number;
  persistence?: number;
  lacunarity?: number;
  iterations?: number;  // For erosion noise
}

export const CONTINENT_LAYER: NoiseLayerDef = {
  scale: 0.0008,
  octaves: 5,
  persistence: 0.7,
  lacunarity: 2.0,
  weight: 0.35,
};

export const HILL_LAYER: NoiseLayerDef = {
  scale: 0.02,
  octaves: 4,
  persistence: 0.6,
  lacunarity: 2.2,
  weight: 0.25,
};

// Island configuration
export const ISLAND_RADIUS = 350;
export const ISLAND_FALLOFF = 100;
export const BASE_ELEVATION = 0.42;
export const OCEAN_FLOOR_HEIGHT = 0.05;

// Pond configuration
export const POND_RADIUS = 50;
export const POND_DEPTH = 0.55;
export const POND_CENTER_X = -80;
export const POND_CENTER_Z = 60;

// Coastline noise for irregular shorelines
export const COASTLINE_CIRCLE_SAMPLE_RADIUS = 2;
export const COAST_LARGE = {
  octaves: 3,
  persistence: 0.5,
  lacunarity: 2.0,
  weight: 0.2,
};

// Mountain boost
export const MOUNTAIN_BOOST_MAX_NORM_DIST = 2.5;
export const MOUNTAIN_BOOST_GAUSSIAN_COEFF = 0.3;

// Worker code generation
export function buildGetBaseHeightAtJS(): string {
  // Injects all constants into worker string
  return `function getBaseHeightAt(worldX, worldZ) { ... }`;
}
```

**Implementation:**

* `TerrainSystem` imports params as TypeScript constants
* `TerrainWorker` receives params via `buildGetBaseHeightAtJS()` injection
* Single source of truth prevents parameter drift
* Worker and main thread produce identical heights
* All numeric constants baked into worker code at runtime

### Worker Height Computation

The worker computes fully correct heights including shoreline adjustments:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// From TerrainWorker (injected into worker string)
function getBaseHeightAt(x: number, z: number): number {
  // 1. Multi-layer Perlin noise
  let height = 0;
  height += perlin(x * continentScale, z * continentScale) * continentWeight;
  height += perlin(x * ridgeScale, z * ridgeScale) * ridgeWeight;
  height += perlin(x * hillScale, z * hillScale) * hillWeight;
  height += perlin(x * erosionScale, z * erosionScale) * erosionWeight;
  height += perlin(x * detailScale, z * detailScale) * detailWeight;
  
  // 2. Island mask
  const islandMask = calculateIslandMask(x, z);
  height *= islandMask;
  
  // 3. Pond depression
  const pondMask = calculatePondMask(x, z);
  height -= pondMask;
  
  // 4. Shoreline adjustment
  const slope = calculateBaseSlopeAt(x, z);
  height = adjustHeightForShoreline(height, slope);
  
  // 5. Mountain boost
  if (height > mountainBoostThreshold) {
    height = mountainBoostThreshold + 
      (height - mountainBoostThreshold) * mountainBoostMultiplier;
  }
  
  return height * MAX_HEIGHT;
}
```

### Normal Computation in Worker

Normals are computed in the worker using centered finite differences on an overflow grid:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// Worker computes (resolution+2)² grid with 1-tile border
// Allows centered finite differences at all vertices
const gridSize = resolution + 2;
const heights = new Float32Array(gridSize * gridSize);

// Sample heights with overflow
for (let z = -1; z <= resolution; z++) {
  for (let x = -1; x <= resolution; x++) {
    const worldX = tileWorldX + x * step;
    const worldZ = tileWorldZ + z * step;
    heights[(z + 1) * gridSize + (x + 1)] = getBaseHeightAt(worldX, worldZ);
  }
}

// Compute normals with centered finite differences
for (let z = 0; z < resolution; z++) {
  for (let x = 0; x < resolution; x++) {
    const hL = heights[(z + 1) * gridSize + x];
    const hR = heights[(z + 1) * gridSize + (x + 2)];
    const hD = heights[z * gridSize + (x + 1)];
    const hU = heights[(z + 2) * gridSize + (x + 1)];
    
    const nx = (hL - hR) / (2 * step);
    const nz = (hD - hU) / (2 * step);
    const ny = 1.0;
    
    // Normalize
    const len = Math.sqrt(nx * nx + ny * ny + nz * nz);
    normals[idx] = nx / len;
    normals[idx + 1] = ny / len;
    normals[idx + 2] = nz / len;
  }
}
```

**Benefits:**

* Accurate normals at tile boundaries (no edge artifacts)
* Centered differences produce smoother lighting
* Worker handles all computation (zero main thread cost)

## World Specs

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// From TerrainSystem.ts CONFIG
const CONFIG = {
  TILE_SIZE: 100,           // 100m × 100m tiles
  WORLD_SIZE: 100,          // 100×100 grid = 10km × 10km world
  TILE_RESOLUTION: 64,      // 64×64 vertices per tile
  MAX_HEIGHT: 50,           // 50m max height variation
  WATER_THRESHOLD: 9.0,     // Water appears below 9m
  CAMERA_FAR: 400,          // Draw distance
};
```

## Noise Layers

Terrain height is generated from multiple Perlin noise layers defined in `TerrainHeightParams.ts`:

| Layer     | Scale  | Weight | Octaves      | Persistence | Lacunarity | Purpose                   |
| --------- | ------ | ------ | ------------ | ----------- | ---------- | ------------------------- |
| Continent | 0.0008 | 0.35   | 5            | 0.7         | 2.0        | Large-scale landmasses    |
| Ridge     | 0.003  | 0.15   | -            | -           | -          | Mountain ridges           |
| Hill      | 0.02   | 0.25   | 4            | 0.6         | 2.2        | Rolling hills             |
| Erosion   | 0.005  | 0.1    | 3 iterations | -           | -          | Weathering patterns       |
| Detail    | 0.04   | 0.08   | 2            | 0.3         | 2.5        | Local bumps and variation |

Combined height is normalized to \[0, 1], raised to power curve (1.1), then scaled by `MAX_HEIGHT` (50m).

**Coastline Variation:**
Natural irregular shorelines created by sampling noise on a circle around the island:

* Large-scale: 3 octaves, weight 0.2
* Medium-scale: 3x frequency, 2 octaves, weight 0.08
* Small-scale: 8x frequency, weight 0.02
* Varies island radius by ±20% for organic coastlines

## Flat Zones

Flat zones create level terrain under stations and buildings with smooth blending to procedural terrain.

### How It Works

**Height Calculation Priority**:

1. Flat zones checked before procedural terrain
2. **Core Flat Area**: Inside the zone, terrain returns exact height value
3. **Blend Area**: Within `blendRadius` of zone edge, smoothstep interpolation blends to procedural terrain
4. **Spatial Indexing**: Terrain tiles (100m) used for O(1) lookup
5. **Manifest-Driven**: Stations with `flattenGround: true` automatically create flat zones

**Blend Formula:**

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// Smoothstep interpolation from flat to procedural
const t = smoothstep(0, blendRadius, distanceFromEdge);
const blendedHeight = flatHeight * (1 - t) + proceduralHeight * t;
```

### Flat Zone Registration

When a station spawns, `TerrainSystem` registers a flat zone:

**Dimensions calculated from:**

* Station footprint (from model bounds)
* `flattenPadding` (extra space around footprint)
* `flattenBlendRadius` (smooth transition zone)

**Height sampled from:**

* Procedural terrain at station center
* Ensures flat zone matches surrounding terrain elevation

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// From StationSpawnerSystem.ts
terrainSystem.registerFlatZone({
  id: `station_${stationId}`,
  centerX: position.x,
  centerZ: position.z,
  width: footprint.width + flattenPadding * 2,
  depth: footprint.depth + flattenPadding * 2,
  height: terrainSystem.getHeightAt(position.x, position.z),
  blendRadius: flattenBlendRadius,
});
```

### Height Calculation

When terrain height is requested, flat zones are checked first:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
getHeightAt(worldX: number, worldZ: number): number {
  // 1. Check flat zones (O(1) via spatial index)
  const flatHeight = getFlatZoneHeight(worldX, worldZ);
  if (flatHeight !== null) {
    return flatHeight;
  }
  
  // 2. Fall back to procedural height
  return getProceduralHeightWithBoost(worldX, worldZ);
}
```

### TerrainSystem API

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// Register a flat zone (for dynamic structures)
terrainSystem.registerFlatZone({
  id: "player_house_1",
  centerX: 50.0,
  centerZ: 50.0,
  width: 10.0,
  depth: 10.0,
  height: 42.5,
  blendRadius: 1.0,
});

// Remove a flat zone
terrainSystem.unregisterFlatZone("player_house_1");

// Query flat zone at position
const zone = terrainSystem.getFlatZoneAt(worldX, worldZ);
if (zone) {
  console.log(`Standing on flat zone: ${zone.id}`);
}
```

## Duel Arena Floor Fix (commits b8f56e81, 7a60135e, 51453da)

Players and agents were sinking \~0.4m into duel arena floors because flat zones were not being registered with the terrain system. This caused `getHeightAt()` to return raw procedural terrain height instead of floor-level height, and also allowed grass to grow through floor surfaces.

**Problem:**

* Flat zones were not registered for duel arena floors
* `getHeightAt()` returned procedural terrain height (\~0.4m below arena floors)
* Players/agents spawned at procedural height, sinking into visual floor meshes
* Grass system used procedural heights, rendering grass through floors
* Terrain mesh rendered at procedural height, creating z-fighting with floor geometry

**Solution:**

Flat zones are now registered programmatically from `DuelArenaVisualsSystem` for all 8 floor areas (6 arenas + lobby + hospital):

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// From DuelArenaVisualsSystem.ts
// Register flat zones during system initialization
private registerArenaFlatZones(): void {
  const FLAT_ZONE_HEIGHT_OFFSET = 0.4; // Where players stand above procedural terrain
  const BLEND_RADIUS = 1.0;
  const CARVE_INSET = 1.0;

  // Register flat zones for all 6 arenas
  for (let i = 0; i < ARENA_COUNT; i++) {
    const row = Math.floor(i / 2);
    const col = i % 2;
    const centerX = ARENA_BASE_X + col * (ARENA_WIDTH + ARENA_GAP) + ARENA_WIDTH / 2;
    const centerZ = ARENA_BASE_Z + row * (ARENA_LENGTH + ARENA_GAP) + ARENA_LENGTH / 2;

    const proceduralHeight = this.getProceduralTerrainHeight(centerX, centerZ);
    const zone: FlatZone = {
      id: `duel_arena_floor_${i + 1}`,
      centerX,
      centerZ,
      width: ARENA_WIDTH,
      depth: ARENA_LENGTH,
      height: proceduralHeight + FLAT_ZONE_HEIGHT_OFFSET,
      blendRadius: BLEND_RADIUS,
      carveInset: CARVE_INSET,
    };

    this.terrainSystem.registerFlatZone(zone);
    this.flatZoneIds.push(zone.id);
  }

  // Register lobby and hospital floors (similar logic)
  // ...
}
```

**Key Implementation Details:**

1. **Procedural Height Sampling**: Uses `getProceduralTerrainHeight()` to get the raw terrain height at each arena center, then adds 0.4m offset for player standing height

2. **Flat Zone Parameters**:
   * `height`: Procedural terrain height + 0.4m (where players stand)
   * `blendRadius`: 1.0m smooth transition to surrounding terrain
   * `carveInset`: 1.0m inset from zone edges to preserve blend padding

3. **Registration Timing**: Flat zones are registered in `DuelArenaVisualsSystem.start()` after terrain system is initialized but before arena meshes are created

4. **Cleanup**: Flat zones are unregistered in `DuelArenaVisualsSystem.destroy()` to prevent memory leaks

**Affected Areas:**

* 6 duel arenas (20m × 24m each)
* Lobby floor (40m × 25m)
* Hospital floor (30m × 25m)

**Impact:**

* Terrain height queries now return correct floor-level values (procedural + 0.4m)
* Players/agents spawn at proper height (no sinking)
* Grass system respects flat zones (no grass through floors)
* Terrain mesh is carved under floor areas to prevent overdraw
* Visual floor meshes positioned 2cm above terrain mesh to prevent z-fighting
* Terrain tiles are automatically regenerated when flat zones are registered after initial tile generation

**Terrain Mesh Regeneration:**

When flat zones are registered after terrain tiles have already been generated, the terrain system automatically regenerates affected tiles to reflect the flat zone heights:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// From TerrainSystem.ts
// Regenerate any existing terrain tiles to apply flat zone heights
if (tilesToRegenerate.length > 0) {
  console.log(
    `[TerrainSystem] Regenerating ${tilesToRegenerate.length} terrain tiles for flat zone "${zone.id}"`
  );
  for (const tile of filteredTiles) {
    this.queueTerrainTileRegeneration(tile.x, tile.z, \"flat_zone\");
  }
}
```

This ensures terrain meshes always reflect flat zone heights, even when buildings or arenas are added after terrain initialization.

**Console Output:**

```
[DuelArenaVisualsSystem] Registered 8 flat zones (6 arenas + lobby + hospital)
[DuelArenaVisualsSystem] Created floor 1 at (70.0, 0.67, 92.0) - terrain=0.27
[TerrainSystem] Flat zone stats: 8 zones registered, 24 tile keys in spatial index
[TerrainSystem] Regenerating 2 terrain tiles for flat zone "duel_arena_floor_1"
```

## Console Logging

`TerrainSystem` logs flat zone activity:

```
[TerrainSystem] Registered flat zone "station_bank_lumbridge_1" -> tile keys: [0_-1, 0_0]
[TerrainSystem] FLAT ZONE HIT: "station_bank_lumbridge_1" at (2.5, -24.8) -> height=42.15
[TerrainSystem] Flat zone stats: 5 zones registered, 12 tile keys in spatial index, 1247 height lookups used flat zones
[TerrainSystem] Regenerating 2 terrain tiles for flat zone "duel_arena_0"
```

## Performance Characteristics

**Worker-Based Computation:**

* Height calculation: \~0ms on main thread (worker handles all noise)
* Normal calculation: \~0ms on main thread (worker computes with overflow grid)
* Tile generation: \~5-10ms total (worker + transfer + geometry build)
* Flat zone tiles: \~15-20ms (requires main thread recomputation)

**Spatial Indexing:**

* Flat zone lookup: O(1) via tile-based spatial index
* Typical world: 5-10 flat zones, 10-20 tile keys
* Negligible memory overhead (\~1KB per zone)

## Related Systems

* [Movement System](/wiki/game-systems/movement) - Uses terrain heights for collision
* [Collision System](/wiki/game-systems/collision) - Integrates with flat zones
* [World Generation](/wiki/game-systems/overview) - Terrain is part of world initialization
