All files / renderer/village seabedLayout.ts

100% Statements 105/105
100% Branches 9/9
100% Functions 4/4
100% Lines 105/105

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 1761x                             4x 4x 4x 2361x 2361x 2361x 2361x 2361x 2361x 4x                                                                   1x 1x 1x 1x 1x 1x   1x 1x 1x 1x 1x 1x   1x 1x 1x 1x 1x 1x 1x 1x   1x 1x 1x 1x     1x                 1x     445x 445x 445x 445x 445x 445x 445x     210x 210x 210x 210x 210x 210x 210x 210x   1x 4x 4x 4x 4x 4x 4x   4x 4x 85x 85x 85x 85x 85x 85x 85x 85x 85x 85x 85x 85x 85x   4x 4x 72x 72x 72x 72x 282x 282x 282x 282x 282x 282x 72x 72x 72x 72x 72x 72x   4x 4x 53x 53x 53x 53x 53x 53x 53x 53x   4x 4x   1x  
/**
 * Deterministic scatter helpers for the seabed decorations.
 *
 * The seabed itself is a subdivided, gently displaced plane built
 * directly in the renderer component. This module generates the
 * positions + per-instance parameters for:
 *  - rocks (grey chunks)
 *  - seagrass clusters (tufts of tall thin blades that sway)
 *  - corals / sea flowers (coloured tufts)
 *
 * All layouts are seeded so the ocean floor is reproducible.
 */
 
import { SEABED_RADIUS, MAIN_ISLAND_RADIUS } from "./sceneConstants";
 
function mulberry32(seed: number): () => number {
  let state = seed >>> 0;
  return () => {
    state = (state + 0x6d2b79f5) >>> 0;
    let t = state;
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}
 
/** Rocks: scattered chunks of grey stone. */
export interface RockPlacement {
  position: [number, number]; // XZ, y comes from the seabed height at that point
  scale: [number, number, number];
  rotationY: number;
  /** 0-based index into a small grey palette. */
  shadeIndex: number;
}
 
/** Seagrass: a cluster of 3-5 tall thin blades at one location. */
export interface SeagrassCluster {
  position: [number, number];
  /** Per-blade offsets (relative to cluster centre) + height. */
  blades: Array<{ offset: [number, number]; height: number; phase: number }>;
  colorIndex: number;
}
 
/** Coral / sea flower tuft. */
export interface CoralPlacement {
  position: [number, number];
  scale: number;
  colorIndex: number;
  /** 0: cone, 1: icosahedron, 2: sphere cluster. */
  shape: 0 | 1 | 2;
}
 
export interface SeabedLayout {
  rocks: RockPlacement[];
  seagrass: SeagrassCluster[];
  corals: CoralPlacement[];
}
 
export const ROCK_SHADES: readonly string[] = Object.freeze([
  "#6e6a66",
  "#7a7470",
  "#8a8581",
  "#5e5a55"
]);
 
export const SEAGRASS_COLORS: readonly string[] = Object.freeze([
  "#2f6a3a",
  "#4a8f4a",
  "#3a7d4e",
  "#5ca55f"
]);
 
export const CORAL_COLORS: readonly string[] = Object.freeze([
  "#ff7f7f",
  "#ffbdbd",
  "#ff99cc",
  "#a65fd6",
  "#ffe066",
  "#ff6ba3"
]);
 
export const SEABED_SEED = 0x5ea7bed0;
export const DEFAULT_ROCK_COUNT = 60;
export const DEFAULT_SEAGRASS_COUNT = 45;
export const DEFAULT_CORAL_COUNT = 30;
 
/** Inner scatter radius - stay clear of the main island's underwater footprint. */
export const SEABED_INNER_RADIUS = MAIN_ISLAND_RADIUS + 2;
 
/**
 * Height of the seabed surface at position (x, z), relative to the
 * plane's own y=0. The seabed mesh sits at `SEABED_Y`, and vertices are
 * displaced by this function to give gentle dunes. Pure function so
 * it's reusable from the renderer (for placing rocks on the surface)
 * and from unit tests.
 */
export function seabedHeightAt(x: number, z: number): number {
  // Sum of two cheap sinusoids + a low-frequency radial mound so the
  // floor is not completely flat near the centre.
  const a = Math.sin(x * 0.12) * 0.8;
  const b = Math.cos(z * 0.15) * 0.7;
  const c = Math.sin((x + z) * 0.07) * 0.5;
  const r = Math.sqrt(x * x + z * z);
  const mound = Math.cos(r * 0.04) * 0.6;
  return a + b + c + mound;
}
 
/** Sample a point uniformly in the seabed annulus. */
function sampleSeabedPoint(rand: () => number): [number, number] {
  const innerSq = SEABED_INNER_RADIUS * SEABED_INNER_RADIUS;
  const outerSq = SEABED_RADIUS * SEABED_RADIUS;
  const u = rand();
  const r = Math.sqrt(u * (outerSq - innerSq) + innerSq);
  const theta = rand() * Math.PI * 2;
  return [Math.cos(theta) * r, Math.sin(theta) * r];
}
 
export function generateSeabedLayout(
  rockCount: number = DEFAULT_ROCK_COUNT,
  seagrassCount: number = DEFAULT_SEAGRASS_COUNT,
  coralCount: number = DEFAULT_CORAL_COUNT,
  seed: number = SEABED_SEED
): SeabedLayout {
  const rand = mulberry32(seed);
 
  const rocks: RockPlacement[] = [];
  for (let i = 0; i < rockCount; i++) {
    const [x, z] = sampleSeabedPoint(rand);
    const baseScale = 0.4 + rand() * 1.1;
    rocks.push({
      position: [x, z],
      scale: [
        baseScale * (0.8 + rand() * 0.4),
        baseScale * (0.6 + rand() * 0.3),
        baseScale * (0.8 + rand() * 0.4)
      ],
      rotationY: rand() * Math.PI * 2,
      shadeIndex: Math.floor(rand() * ROCK_SHADES.length)
    });
  }
 
  const seagrass: SeagrassCluster[] = [];
  for (let i = 0; i < seagrassCount; i++) {
    const [x, z] = sampleSeabedPoint(rand);
    const bladeCount = 3 + Math.floor(rand() * 3); // [3,5]
    const blades = [];
    for (let b = 0; b < bladeCount; b++) {
      blades.push({
        offset: [(rand() - 0.5) * 0.4, (rand() - 0.5) * 0.4] as [number, number],
        height: 0.9 + rand() * 0.8,
        phase: rand() * Math.PI * 2
      });
    }
    seagrass.push({
      position: [x, z],
      blades,
      colorIndex: Math.floor(rand() * SEAGRASS_COLORS.length)
    });
  }
 
  const corals: CoralPlacement[] = [];
  for (let i = 0; i < coralCount; i++) {
    const [x, z] = sampleSeabedPoint(rand);
    corals.push({
      position: [x, z],
      scale: 0.35 + rand() * 0.5,
      colorIndex: Math.floor(rand() * CORAL_COLORS.length),
      shape: Math.floor(rand() * 3) as 0 | 1 | 2
    });
  }
 
  return { rocks, seagrass, corals };
}
 
export const SEABED_LAYOUT: Readonly<SeabedLayout> = Object.freeze(generateSeabedLayout());