/* ============================================================================ TERRAGEN — landform structure: ridges, drainage, coastlines ---------------------------------------------------------------------------- The base heightfield is five octaves of value noise. Value noise makes BLOBS: smooth, rounded, and — the actual complaint — completely without structure. Real terrain is not noise, it is noise that WATER HAS RUN OVER. Ridgelines radiate from massifs, valleys branch upstream into dendritic networks, coasts fray where the sea works into soft rock. None of that is in a sum of octaves, at any amplitude, ever. Three passes add it, cheapest and highest-payoff first: 1. RIDGED MULTIFRACTAL (Musgrave's fold: 1-|n|, squared, each octave weighted by the previous one's signal) gives continuous crest lines instead of scattered bumps. Region-masked by a very low frequency field so ranges belong to parts of the map rather than covering it. 2. FLOW ACCUMULATION (D8 steepest-descent, height-ordered) computes how much water crosses every cell, then carves channel depth from log(accumulation). This is where the dendritic branching comes from, and it is ~100x cheaper than droplet erosion for that specific look: one O(N) bucket sort and two O(N) sweeps. 3. COAST WARP domain-warps the sample position in a band around sea level, so shorelines fray into inlets and headlands without touching height anywhere inland. WHY IT RUNS ON A 512 GRID. The mesh is TGRID=320 (10 world units a cell) and terrainH box-blurs +-7 units, so nothing finer than roughly 25-30 world units can reach the screen as geometry no matter how fine the field is. A 512 grid is 6.25 units a cell — already four times finer than the mesh can show. Working there costs a sixteenth of 2048 and loses nothing visible. WHY IT RETURNS A DELTA. The 2048 field carries a deliberate near-Nyquist octave that the per-pixel normal sheet displays. Upsampling absolute heights from 512 would erase it. Every pass writes into a delta grid instead, and only the delta is bilinearly upsampled and added — large-scale structure arrives, fine detail survives untouched. HARD RULES, both learned from what this codebase already guarantees: - Determinism. A private LCG, seeded from the map seed. Never the shared global _seed (planDistricts and friends run after us on that stream) and never Math.random(): context-loss rebuilds and map previews must reproduce the same ground, byte for byte. - No new water. Passability, the naval mask, water-mesh coverage and battlefieldNavalEnabled all key off WATER_H. A carve that drops dry land below sea level would silently punch holes in pathing and flood maps authored dry. Any cell that started at or above WATER_H is clamped so it can never finish below it. Combat craters follow the same rule via WATER_AUTH — punching a bowl below the water table must not spawn a pond; oceans/rivers/lakes stay authored. A shoreline crater may visually wet its lip (WATER_LIP in terrain.js) without rewriting this mask, PASS, or the naval grid. ============================================================================ */ const TERRA={ work:512, // erosion grid resolution (6.25 world units a cell) ridgeOct:5, ridgeLac:2.05, // not 2.0: exact doubling re-aligns octave harmonics ridgeGain:2.05, ridgeFreq:9.5, // lattice cells across the map at octave 0 regionFreq:2.4, // range-vs-lowland mask: 2-3 blobs across the map regionLo:0.46, regionHi:0.78, streamMin:55, // contributing cells before a channel exists carve:0.0132, // height units per log-unit of accumulation carveMax:0.055, // ~6.5 world units: deep enough to read, not a canyon coastBand:0.055, // height units either side of sea level that warp coastWarp:46, // world units a shoreline can wander ceiling:0.93, // crests must not clip into flat-topped mesas regionBias:0.0 // dev tool: shifts how much of the map is mountainous }; /* Private deterministic stream. Kept off the global LCG on purpose. */ function terraRng(seed){ let s=(seed|0)||1; return ()=>{ s=(Math.imul(s,1664525)+1013904223)|0; return ((s>>>9)&0x7fffff)/0x800000; }; } /* Value-noise lattice with wrap, matching the engine's existing sampler so the two stacks agree about what a "cell" is. */ function terraLattice(rand,n){ const g=new Float32Array((n+1)*(n+1)); for(let i=0;i1) weight=1; else if(weight<0) weight=0; freq*=lac; amp*=0.5; } return sum; } /* --------------------------------------------------------------------------- PLAYABILITY MASK — 0 where the ground must stay as authored, 1 where the landform passes may do as they like. The bumps, corridors and deposit pads are Math.max floors: they do not heal after something cuts into them. Attenuating DURING each pass (rather than re-stamping afterwards) is what makes a river route AROUND a base instead of being clipped flat where it crosses one — a re-stamp leaves a retaining-wall edge at every boundary, which is the same "sculpted" tell, just relocated. --------------------------------------------------------------------------- */ function terraPlayMask(W,bumps,corridorFns,depPts,roads,tight){ const m=new Float32Array(W*W).fill(1); const s=MAP/W; const soften=(i,d,r,feather)=>{ if(d>=r+feather) return; const v=d<=r?0:(d-r)/feather; const k=v*v*(3-2*v); if(k{ let lo=1e9; const x=i%W, y=(i/W)|0; for(let dy=-1;dy<=1;dy++) for(let dx=-1;dx<=1;dx++){ if(!dx&&!dy) continue; const nx=x+dx, ny=y+dy; if(nx<0||ny<0||nx>=W||ny>=W) continue; const v=F[ny*W+nx]; if(vcand?h:cand; }; for(let p=0;p0;y--) for(let x=W-2;x>0;x--){ const i=y*W+x; if(F[i]>H[i]) relax(i,H[i]); } } else { for(let y=1;yH[i]) relax(i,H[i]); } } } for(let i=0;i1e8) F[i]=H[i]; return F; } function terraFlow(H,W,mask,out,wet){ const N=W*W; const dir=new Int32Array(N).fill(-1); const acc=new Float32Array(N).fill(1); const F=terraFill(H,W,10); // routing surface: sinks removed let lo=Infinity, hi=-Infinity; for(let i=0;ihi)hi=v; } const span=(hi-lo)||1; for(let y=1;yB-1)b=B-1; next[i]=head[b]; head[b]=i; } for(let b=B-1;b>=0;b--) for(let i=head[b];i>=0;i=next[i]){ const d=dir[i]; if(d>=0) acc[d]+=acc[i]; } /* Depth from log(accumulation): a trunk carrying a whole basin cuts a few times deeper than a first-order tributary, not thousands of times. */ for(let i=0;iTERRA.carveMax) d=TERRA.carveMax; out[i]-=d*mask[i]; } return acc; } /* Separable box blur on a grid — used to widen carved channels so they survive the mesh's own +-7 unit smoothing, and to soften the ridge mask. */ function terraBlur(src,W,r,tmp){ const inv=1/(r*2+1); for(let y=0;y=WATER_H?1:0; // remembered before anything moves const mask=terraPlayMask(W,bumps,corridorFns,depPts,MD.roads,false); // carving: full protection const maskR=terraPlayMask(W,bumps,corridorFns,depPts,MD.roads,true); // ridges: build pads only const delta=new Float32Array(N); const rel=MD.relief||1; /* ---- 1. RIDGES --------------------------------------------------------- Amplitude scales with the map's own relief setting, so a "flat highway country" map gets hills and a "cliff arcology" gets a range. The region mask decides WHERE, and is blurred so ranges have shoulders instead of edges. Squaring the mask keeps foothills low and cores high. */ const ridgeAmp=(0.23+0.26*Math.max(0,Math.min(1.6,rel)-0.85))*(MD.crater?0.66:1); const region=new Float32Array(N), tmp=new Float32Array(N); for(let y=0;y1?1:m*m*(3-2*m); } terraBlur(region,W,3,tmp); for(let y=0;yTERRA.ceiling) v=TERRA.ceiling; delta[i]=v-H[i]; } /* ---- 5. UPSAMPLE THE DELTA -------------------------------------------- Bilinear, so the added structure is smooth, while the 2048 field keeps every octave it already had underneath. */ let lo=0, hiV=0; for(let y=0;y=WATER_H){ if(vTERRA.ceiling) v=TERRA.ceiling; heightF[i]=v; if(dhiV) hiV=d; } } const ms=((typeof performance!=='undefined')?performance.now():0)-t0; return {ms:Math.round(ms),work:W,ridgeAmp:+ridgeAmp.toFixed(3), deltaLo:+lo.toFixed(3),deltaHi:+hiV.toFixed(3)}; } let TERRA_ENABLED=true; let terraLastStats=null;