← Natura·Docs/Modules/Source

worldgen.js

66 lines · in natura-src-1.0.0.zip as natura/worldgen.js · a module · its entry in the module reference

// worldgen.js -- a LARGE, boundless CONTINENT for Natura (the operator's "large nearly-infinite spaces,
// not islands"). Periodic value-noise fBm plus ridged mountain ranges, TILEABLE on the [-1,1]^2 domain:
// because the lattice wraps, the world is a seamless torus you can walk forever, with real climate zones
// (mountain ranges throw rain-shadows, so the windward side is wet and the lee is dry) that creatures
// migrate across. Land-dominant with inland seas, so life has continuous room to roam. Deterministic per
// seed (no Date.now / Math.random). UMD (node + browser). heightAt(x,y): >0 land, <0 sea, ~0.3+ mountains.
(function (root, factory) {
  if (typeof module !== 'undefined' && module.exports) module.exports = factory();
  else root.WorldGen = factory();
})(typeof self !== 'undefined' ? self : this, function () {
  'use strict';

  const smooth = (t) => t * t * t * (t * (t * 6 - 15) + 10);

  // hash a lattice point, WRAPPING the lattice mod P so the noise tiles with period P
  function vh(ix, iy, P, s) {
    ix = ((ix % P) + P) % P; iy = ((iy % P) + P) % P;
    let h = (Math.imul(ix, 374761393) + Math.imul(iy, 668265263) + Math.imul(s | 0, 2147483647)) | 0;
    h = Math.imul(h ^ (h >>> 13), 1274126177);
    return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
  }
  // periodic value noise: u,v in [0,1); P integer lattice cells -> tiles seamlessly
  function pnoise(u, v, P, s) {
    const x = u * P, y = v * P, ix = Math.floor(x), iy = Math.floor(y), fx = x - ix, fy = y - iy;
    const ux = smooth(fx), uy = smooth(fy);
    const a = vh(ix, iy, P, s), b = vh(ix + 1, iy, P, s), c = vh(ix, iy + 1, P, s), d = vh(ix + 1, iy + 1, P, s);
    return (a * (1 - ux) + b * ux) * (1 - uy) + (c * (1 - ux) + d * ux) * uy;
  }
  function pfbm(u, v, base, oct, s) {
    let amp = 1, sum = 0, norm = 0, P = base;
    for (let o = 0; o < oct; o++) { sum += amp * pnoise(u, v, P, s + o * 101); norm += amp; amp *= 0.5; P *= 2; }
    return sum / norm;                                          // [0,1]
  }
  function pridge(u, v, base, oct, s) {                          // sharp mountain ridges (1-|2n-1|)^2
    let amp = 1, sum = 0, norm = 0, P = base;
    for (let o = 0; o < oct; o++) { const n = 1 - Math.abs(2 * pnoise(u, v, P, s + o * 131) - 1); sum += amp * n * n; norm += amp; amp *= 0.5; P *= 2; }
    return sum / norm;                                          // [0,1], ridged
  }

  // make a continent. opts: { period(base lattice, integer), lift(land bias), mountain(range height),
  //   ridgePeriod }. Larger period -> more, smaller zones.
  function makeContinent(seed, opts) {
    opts = opts || {};
    seed = (seed | 0) || 1;
    const period = opts.period || 6;                            // base zones across the world
    const ridgeP = opts.ridgePeriod || Math.max(2, period >> 1);
    const relief = opts.relief != null ? opts.relief : 0.85;    // land/sea relief (continent shape)
    const shore = opts.shore != null ? opts.shore : 0.47;       // fBm level of the coastline (lower -> more land)
    const mtn = opts.mountain != null ? opts.mountain : 0.9;    // mountain-range height
    return {
      seed, period,
      heightAt(x, y) {
        const u = ((x + 1) / 2 % 1 + 1) % 1, v = ((y + 1) / 2 % 1 + 1) % 1;
        const cont = pfbm(u, v, period, 5, seed);                // rolling continent 0..1
        const ridge = pridge(u, v, ridgeP, 4, seed * 3 + 7);     // mountain ranges 0..1
        // land/sea from the continent fBm (so there ARE real oceans -- the water cycle's moisture source);
        // mountains rise ONLY on land where the ridges run, throwing rain-shadows -> wet coasts, dry lee.
        let e = relief * (cont - shore);
        if (e > 0) e += mtn * Math.pow(Math.max(0, ridge - 0.45), 1.4);
        return e;                                                // >0 land, <0 sea
      }
    };
  }

  return { makeContinent, pnoise, pfbm, pridge };
});