code wiki / (root) / nx_worldpipe.nx

nx_worldpipe.nx source

↩ module page · 466 lines · 24521 B

1// nx_worldpipe.nx -- ★R5 STAGED PROCGEN (the banked Minecraft-pipeline curriculum, engine-convergence rung 5): 2// worldgen as an ORDERED STAGED PIPELINE over a MULTI-PARAMETER space -- not one noise call. 3// STAGE 1 PARAMS six independent noise fields at (x,z): temperature / humidity / CONTINENTALNESS / erosion / 4// weirdness / river. (Their 6th, depth, is the 3D-density param -- ours is a heightfield: named residual.) 5// STAGE 2 SPLINE height BASE = piecewise-linear SPLINE over continentalness (deep ocean -> coast -> inland -> highland), 6// the curriculum's core lesson: params map to height THROUGH SPLINES. 7// STAGE 3 RELIEF ridged mountains scaled by WEIRDNESS and flattened by EROSION (high erosion = plains). 8// STAGE 4 CARVER rivers CUT the terrain where the river field crosses its band (geometry change, toggleable). 9// STAGE 5 BIOME classified from the parameter VECTOR (temp x humidity x cont x weirdness) -- NOT from height 10// (the anti-"height-only" lesson: two same-height points can be desert vs snowfield). 11// STAGE 6 SURFACE per-biome + slope + altitude surface colours (sand/grass/snow/rock/water). 12// STAGE 7 FEATURES per-biome placements by seeded hash (trees / cacti / spruce / boulders). 13// STAGE 8 SPAWN a valid spawn point (land, gentle slope) found deterministically. 14// Pure integer, deterministic per seed. Stages 4/7 have toggles so gates can prove CAUSALITY per stage. 15// license_tier: ORIGINAL 16import "nx_syscalls.nx" 17import "nx_trimesh.nx" 18import "nx_terrain_erode_lib.nx" 19// GE14a 2026-08-28: the PURE staged core (params -> spline -> relief) now lives in its own 20// lib so the wasm engine can import it without the allocating stages. Moved, never copied. 21import "nx_worldpipe_core.nx" 22const WP_MAGIC_9600: i64 = 9600 23const WP_MAGIC_1000000000: i64 = 1000000000 24const WP_MAGIC_10100: i64 = 10100 25const WP_MAGIC_1000000: i64 = 1000000 26const WP_MAGIC_999999: i64 = 999999 27const WP_MAGIC_65536: i64 = 65536 28 29static WP_CARVE: i64 // stage-4 toggle (1 = carve rivers). set via wp_set_carve 30static WP_FEAT: i64 // stage-7 toggle (1 = place features). set via wp_set_feat 31static WP_LIGHT: i64 // stage-6.5 lighting toggle (declared ABOVE all readers -- the AT_FDCWD law) 32// == STAGE 3.5 EROSION AS A PROCESS (PG20, procgen.plan). Statics declared HERE, above every reader, 33// by the same law the lighting toggle above records. WP_ERODE is ARMED BY DEFAULT and that is safe BY 34// CONSTRUCTION rather than by promise: wp_erode returns 0 until wp_erode_bake has run, so wp_height is 35// byte-identical for every consumer that never bakes. The gate asserts that instead of trusting it. 36static WP_ERODE: i64 // stage-3.5 toggle (1 = apply the eroded deltas). set via wp_set_erode 37static WP_ERG: i64 // delta grid HYW*HYW in WORLD units; 0 until wp_erode_bake has run 38static WP_ER_TICKS: i64 // ticks the last bake ran 39static WP_ER_MOVED: i64 // material the talus passes moved (0 = the pass did nothing at all) 40static WP_ER_HI0: i64 // Strahler hypsometric integral in permille, BEFORE erosion 41static WP_ER_HI1: i64 // ... and AFTER: the referee, read out through wp_erode_stat 42// Bake workspace, allocated once and REUSED. A re-bake must not leak: resource behaviour is a shipping 43// criterion here, and the gate bakes twice to prove determinism. 44static WP_ERHG: i64 45static WP_ERH0: i64 46static WP_ERDL: i64 47static WP_ERSC: i64 48static WP_ERST: i64 49static WP_ERPR: i64 50static WP_ERTL: i64 // talus-derivation workspace, allocated ONCE and REUSED (same law as the bake workspace above: a re-derive must not leak) 51func wp_init(seed: i64) -> i64 { WP_SEED = seed; WP_CARVE = 1; WP_FEAT = 1; WP_TEMP_BIAS = 0; WP_CONT_BIAS = 0; WP_MOUNT_SCALE = WP_MAGIC_1400; WP_LIGHT = 1; WP_ERODE = 1; return 0 } 52func wp_set_erode(v: i64) -> i64 { WP_ERODE = v; return 0 } 53func wp_set_carve(v: i64) -> i64 { WP_CARVE = v; return 0 } 54func wp_set_feat(v: i64) -> i64 { WP_FEAT = v; return 0 } 55func wp_set_temp_bias(v: i64) -> i64 { WP_TEMP_BIAS = v; return 0 } 56func wp_set_cont_bias(v: i64) -> i64 { WP_CONT_BIAS = v; return 0 } 57func wp_set_mount_scale(v: i64) -> i64 { WP_MOUNT_SCALE = v; return 0 } 58 59 60// ---- ★Q3 HYDROLOGY (2026-07-12, replaces the noise-band carver -- "the content was random"): rivers are 61// STEEPEST-DESCENT TRACES from high springs down to the sea. Baked once per seed into a coarse bed grid 62// (cell HYCELL over +-HYSPAN); wp_height carves terrain to the traced bed. By construction a river bed is 63// MONOTONE NON-INCREASING along its path (water cannot flow uphill) and terminates at the sea or a lake. 64const HYW: i64 = 320 65const HYCELL: i64 = 64 66const HYSPAN: i64 = 10240 67static WP_HYD: i64 // bed grid: HYW*HYW i64; 0 = no river; else bed height + HYOFF 68static WP_HYSTATS: i64 // per-spring stats: [startBed, endBed, steps, reachedSea] x 8 69static WP_HYN: i64 // springs traced 70const HYOFF: i64 = 5000000 71func wp_hyd_stamp(g: *i64, x: i64, z: i64, bed: i64, rad: i64) -> i64 { 72 var dz: i64 = 0 - rad 73 while dz <= rad { 74 var dx: i64 = 0 - rad 75 while dx <= rad { 76 let cx: i64 = (x + dx*HYCELL + HYSPAN)/HYCELL 77 let cz: i64 = (z + dz*HYCELL + HYSPAN)/HYCELL 78 if cx >= 0 { if cx < HYW { if cz >= 0 { if cz < HYW { 79 let gi: i64 = cz*HYW + cx 80 let v: i64 = bed + HYOFF 81 if g[gi] == 0 { g[gi] = v } else { if v < g[gi] { g[gi] = v } } 82 } } } } 83 dx = dx + 1 84 } 85 dz = dz + 1 86 } 87 return 0 88} 89func wp_hydro_bake() -> i64 { 90 if WP_HYD == 0 { WP_HYD = sys_mmap(HYW*HYW*8) as i64 } 91 if WP_HYSTATS == 0 { WP_HYSTATS = sys_mmap(8*4*8) as i64 } 92 let g: *i64 = WP_HYD as *i64 93 var i: i64 = 0 94 while i < HYW*HYW { g[i] = 0; i = i + 1 } 95 let st: *i64 = WP_HYSTATS as *i64 96 WP_HYN = 0 97 // springs: seeded picks on HIGH ground 98 var sz: i64 = 0-WP_MAGIC_9600 99 while sz <= WP_MAGIC_9600 { 100 var sx: i64 = 0-WP_MAGIC_9600 101 while sx <= WP_MAGIC_9600 { 102 if WP_HYN < 8 { 103 if wp_flow_h(sx, sz) > 140 { 104 if tm_hash3(sx/640, WP_SEED*17 + 3, sz/640) < 70 { 105 // trace steepest descent 106 var px: i64 = sx 107 var pz: i64 = sz 108 var bed: i64 = wp_flow_h(px, pz) - 14 109 let sb: i64 = bed 110 var steps: i64 = 0 111 var alive: i64 = 1 112 var reached: i64 = 0 113 while alive == 1 { 114 wp_hyd_stamp(g, px, pz, bed, 1) 115 var bestH: i64 = WP_MAGIC_1000000000 116 var bx: i64 = px 117 var bz: i64 = pz 118 var d: i64 = 0 119 while d < 16 { 120 var stp: i64 = 110 121 if d >= 8 { stp = 300 } // basin-escape ring 122 var nx: i64 = px 123 var nz: i64 = pz 124 let dd8: i64 = d % 8 125 if dd8 == 0 { nx = px + stp } 126 if dd8 == 1 { nx = px - stp } 127 if dd8 == 2 { nz = pz + stp } 128 if dd8 == 3 { nz = pz - stp } 129 if dd8 == 4 { nx = px + stp*7/10; nz = pz + stp*7/10 } 130 if dd8 == 5 { nx = px - stp*7/10; nz = pz + stp*7/10 } 131 if dd8 == 6 { nx = px + stp*7/10; nz = pz - stp*7/10 } 132 if dd8 == 7 { nx = px - stp*7/10; nz = pz - stp*7/10 } 133 let nh: i64 = wp_flow_h(nx, nz) 134 if nh < bestH { bestH = nh; bx = nx; bz = nz } 135 d = d + 1 136 } 137 let here: i64 = wp_flow_h(px, pz) 138 if bestH >= here { 139 // true basin even at the escape ring -> a LAKE, then stop 140 wp_hyd_stamp(g, px, pz, bed, 2) 141 alive = 0 142 } else { 143 // stamp ALONG the segment (escape jumps are 300 units; a single stamp leaves gaps) 144 wp_hyd_stamp(g, px + (bx-px)/3, pz + (bz-pz)/3, bed, 1) 145 wp_hyd_stamp(g, px + (bx-px)*2/3, pz + (bz-pz)*2/3, bed, 1) 146 px = bx 147 pz = bz 148 var nb: i64 = bestH - 14 149 if nb > bed { nb = bed } // water NEVER flows uphill 150 bed = nb 151 if bestH < 0 { reached = 1; alive = 0 } // reached the sea 152 } 153 steps = steps + 1 154 if steps > 480 { alive = 0 } 155 if px < 0-WP_MAGIC_10100 { alive = 0 } 156 if px > WP_MAGIC_10100 { alive = 0 } 157 if pz < 0-WP_MAGIC_10100 { alive = 0 } 158 if pz > WP_MAGIC_10100 { alive = 0 } 159 } 160 st[WP_HYN*4] = sb 161 st[WP_HYN*4+1] = bed 162 st[WP_HYN*4+2] = steps 163 st[WP_HYN*4+3] = reached 164 WP_HYN = WP_HYN + 1 165 } 166 } 167 } 168 sx = sx + 640 169 } 170 sz = sz + 640 171 } 172 return WP_HYN 173} 174func wp_hyd_n() -> i64 { return WP_HYN } 175func wp_hyd_stat(i: i64, k: i64) -> i64 { let st: *i64 = WP_HYSTATS as *i64; return st[i*4+k] } 176func wp_hyd_bed(x: i64, z: i64) -> i64 { 177 if WP_HYD == 0 { return 0-WP_MAGIC_1000000 } 178 let cx: i64 = (x + HYSPAN)/HYCELL 179 let cz: i64 = (z + HYSPAN)/HYCELL 180 if cx < 0 { return 0-WP_MAGIC_1000000 } 181 if cx >= HYW { return 0-WP_MAGIC_1000000 } 182 if cz < 0 { return 0-WP_MAGIC_1000000 } 183 if cz >= HYW { return 0-WP_MAGIC_1000000 } 184 let g: *i64 = WP_HYD as *i64 185 let v: i64 = g[cz*HYW + cx] 186 if v == 0 { return 0-WP_MAGIC_1000000 } 187 return v - HYOFF 188} 189// ---- STAGE 3.5: EROSION AS A PROCESS (PG20). Three of the estate's own census gates carried the same 190// residual in their prose -- "erosion is a scalar not a process" -- while the full Mei 2007 pipe model 191// sat in runtime/nx_water_erosion.nx, gate-proven (PG18) and imported by NOBODY but its own gate. This 192// stage is that composition, driven through nx_terrain_erode_lib so there is exactly one talus law and 193// exactly one referee in the estate. 194// 195// SHAPE TAKEN FROM THE HYDROLOGY STAGE ABOVE, DELIBERATELY: an explicit bake, and a reader that is 196// INERT until the bake has run. WP_ERG == 0 means wp_erode returns 0 means wp_height is byte-identical, 197// so arming WP_ERODE by default costs every existing consumer exactly nothing. 198// 199// GRID DERIVED, NOT PICKED: the erosion grid IS the hydrology grid this world already uses -- HYW cells 200// across HYSPAN at HYCELL world units each -- so the two bakes describe the same world and there is no 201// second resolution to fall out of step with the first. 202// 203// SCALE-FREE: heights and cell size both enter in world units, so the slope the simulation reads is a 204// pure ratio. nx_water_erosion names its units metres; nothing here binds a metre to anything. 205// 206// The bake samples wp_flow_h -- the VALLEY-scale surface, core without the detail octave -- for the 207// same reason the river tracer does: water shapes valleys, not micro-bumps. 208func wp_erode_stat(k: i64) -> i64 { 209 if k == 0 { return WP_ER_TICKS } 210 if k == 1 { return WP_ER_MOVED } 211 if k == 2 { return WP_ER_HI0 } 212 if k == 3 { return WP_ER_HI1 } 213 return 0-1 214} 215func wp_erode_bake(ticks: i64, rain_q14: i64, talus: i64, flux_q14: i64) -> i64 { 216 let gn: i64 = HYW 217 let n: i64 = gn * gn 218 if WP_ERG == 0 { WP_ERG = sys_mmap(te_grid_bytes(gn)) as i64 } 219 if WP_ERHG == 0 { WP_ERHG = sys_mmap(te_grid_bytes(gn)) as i64 } 220 if WP_ERH0 == 0 { WP_ERH0 = sys_mmap(te_grid_bytes(gn)) as i64 } 221 if WP_ERDL == 0 { WP_ERDL = sys_mmap(te_grid_bytes(gn)) as i64 } 222 if WP_ERSC == 0 { WP_ERSC = sys_mmap(te_grid_bytes(gn)) as i64 } 223 if WP_ERST == 0 { WP_ERST = sys_mmap(te_state_bytes(gn)) as i64 } 224 if WP_ERPR == 0 { WP_ERPR = sys_mmap(NX_WATER_PARAM_COUNT * NX_SIZEOF_NX_INT) as i64 } 225 let dg: *i64 = WP_ERG as *i64 226 let hg: *i64 = WP_ERHG as *i64 227 let h0: *i64 = WP_ERH0 as *i64 228 let dl: *i64 = WP_ERDL as *i64 229 let sc: *i64 = WP_ERSC as *i64 230 let st: *i64 = WP_ERST as *i64 231 let pr: *i64 = WP_ERPR as *i64 232 nx_water_erosion_params_default(pr) 233 pr[NX_WATER_PARAM_CELL_SIZE] = HYCELL * NX_WATER_Q 234 var y: i64 = 0 235 while y < gn { 236 var x: i64 = 0 237 while x < gn { 238 let v: i64 = wp_flow_h(x*HYCELL - HYSPAN, y*HYCELL - HYSPAN) * NX_WATER_Q 239 hg[y*gn + x] = v 240 h0[y*gn + x] = v 241 x = x + 1 242 } 243 y = y + 1 244 } 245 WP_ER_HI0 = te_hypsometric_permil(h0, n) 246 WP_ER_MOVED = te_erode_grid(hg, gn, ticks, rain_q14, talus, flux_q14, pr, st, sc, dl) 247 WP_ER_HI1 = te_hypsometric_permil(hg, n) 248 var i: i64 = 0 249 while i < n { dg[i] = (hg[i] - h0[i]) / NX_WATER_Q; i = i + 1 } 250 WP_ER_TICKS = ticks 251 return n 252} 253// ONE-CALL BAKE FOR A CONSUMER THAT WANTS EROSION AND HAS NO BUSINESS PICKING ITS PARAMETERS. Every 254// argument wp_erode_bake takes is either DERIVED from the very terrain about to be eroded, or is the 255// erosion library's own declared unit -- so a caller supplies a tick BUDGET and nothing else: 256// talus DERIVED as the terrain's own mean of the per-cell largest 4-neighbour drop, measured on 257// wp_flow_h -- the SAME valley-scale surface the bake itself reads, so the threshold and the 258// simulation cannot disagree about what the ground is. Material sheds where the ground is 259// steeper than the ground's own average, so no repose angle is picked by taste, and a flat 260// world derives a small threshold instead of inheriting an alpine one. 261// rain / flux NX_WATER_Q -- the erosion library's own unit rate. Q IS one; this is not a knob. 262// wp_erode_talus_derived RETURNS ITS MEASUREMENT so a caller can SEE a flat world rather than trust 263// that it is not one: 0 means the terrain has no relief, i.e. a bake whose outcome is decided before 264// it runs, and wp_erode_bake_derived REFUSES that case instead of reporting a bake it did not do. 265func wp_erode_talus_derived() -> i64 { 266 let gn: i64 = HYW 267 let n: i64 = gn * gn 268 if WP_ERTL == 0 { WP_ERTL = sys_mmap(te_grid_bytes(gn)) as i64 } 269 let g: *i64 = WP_ERTL as *i64 270 var y: i64 = 0 271 while y < gn { 272 var x: i64 = 0 273 while x < gn { g[y*gn + x] = wp_flow_h(x*HYCELL - HYSPAN, y*HYCELL - HYSPAN); x = x + 1 } 274 y = y + 1 275 } 276 var sum: i64 = 0 277 var y2: i64 = 0 278 while y2 < gn { 279 var x2: i64 = 0 280 while x2 < gn { 281 let k: i64 = y2*gn + x2 282 let h: i64 = g[k] 283 var m: i64 = 0 284 if x2 > 0 { let da: i64 = wp_iabs(h - g[k-1]); if da > m { m = da } } 285 if x2 < gn - 1 { let db: i64 = wp_iabs(h - g[k+1]); if db > m { m = db } } 286 if y2 > 0 { let dc: i64 = wp_iabs(h - g[k-gn]); if dc > m { m = dc } } 287 if y2 < gn - 1 { let dd: i64 = wp_iabs(h - g[k+gn]); if dd > m { m = dd } } 288 sum = sum + m 289 x2 = x2 + 1 290 } 291 y2 = y2 + 1 292 } 293 return sum / n 294} 295func wp_erode_bake_derived(ticks: i64) -> i64 { 296 let talus: i64 = wp_erode_talus_derived() 297 if talus <= 0 { return 0 } 298 wp_erode_bake(ticks, NX_WATER_Q, talus, NX_WATER_Q) 299 return talus 300} 301// THE CONTRACT SYMBOL. Returns the eroded height delta at a world point, in world units, bilinearly 302// interpolated off the baked grid. Zero before any bake, and zero outside the baked span -- the two 303// cases a caller must not be able to tell apart from "no erosion here", because that is what they are. 304func wp_erode(x: i64, z: i64) -> i64 { 305 if WP_ERG == 0 { return 0 } 306 let gn: i64 = HYW 307 let px: i64 = x + HYSPAN 308 let pz: i64 = z + HYSPAN 309 if px < 0 { return 0 } 310 if pz < 0 { return 0 } 311 let cx: i64 = px / HYCELL 312 let cz: i64 = pz / HYCELL 313 if cx >= gn - 1 { return 0 } 314 if cz >= gn - 1 { return 0 } 315 let fx: i64 = px - cx*HYCELL 316 let fz: i64 = pz - cz*HYCELL 317 let g: *i64 = WP_ERG as *i64 318 let d00: i64 = g[cz*gn + cx] 319 let d10: i64 = g[cz*gn + cx + 1] 320 let d01: i64 = g[(cz+1)*gn + cx] 321 let d11: i64 = g[(cz+1)*gn + cx + 1] 322 let d0: i64 = d00 + (d10 - d00)*fx/HYCELL 323 let d1: i64 = d01 + (d11 - d01)*fx/HYCELL 324 return d0 + (d1 - d0)*fz/HYCELL 325} 326func wp_height(x: i64, z: i64) -> i64 { 327 var h: i64 = wp_height_raw(x, z) 328 if WP_ERODE == 1 { h = h + wp_erode(x, z) } // STAGE 3.5: the eroded bed 329 if WP_CARVE == 1 { 330 let bed: i64 = wp_hyd_bed(x, z) 331 if bed > 0-WP_MAGIC_999999 { if bed < h { h = bed } } // STAGE 4: the traced river carves to its bed 332 } 333 return h 334} 335 336// ---- STAGE 5: biome from the parameter VECTOR (not height). ids: 337// 0 OCEAN, 1 BEACH, 2 RIVER, 3 DESERT, 4 SNOWY, 5 PEAKS, 6 FOREST, 7 PLAINS 338func wp_biome(x: i64, z: i64) -> i64 { 339 let cont: i64 = wp_param(x, z, 0) 340 if cont <= 0-120 { return 0 } 341 if cont <= 0-40 { return 1 } 342 let hraw: i64 = wp_height_raw(x, z) 343 if WP_CARVE == 1 { 344 let bed: i64 = wp_hyd_bed(x, z) 345 if bed > 0-WP_MAGIC_999999 { if bed < hraw { return 2 } } // RIVER = the traced watercourse (Q3 hydrology) 346 } 347 // ★Q3 ALTITUDE LAPSE: temperature FALLS with height -- snow tops mountains BECAUSE they are cold, 348 // deserts exist only in hot lowlands. te = temp - lapse(h). 349 var lapse: i64 = 0 350 if hraw > 0 { lapse = hraw*4/5 } 351 let te: i64 = wp_param(x, z, 1) - lapse 352 let humid: i64 = wp_param(x, z, 2) 353 let ero: i64 = wp_param(x, z, 3) 354 let weird: i64 = wp_param(x, z, 4) 355 if te > 170 { if humid < 0-90 { return 3 } } 356 if te < 0-210 { return 4 } 357 if weird > 230 { if ero < 80 { return 5 } } 358 if humid > 70 { return 6 } 359 return 7 360} 361 362// ---- STAGE 6: surface colour from biome + slope + altitude (r + g*256 + b*65536) ---- 363func wp_surface(x: i64, z: i64, h: i64, slope: i64) -> i64 { 364 let b: i64 = wp_biome(x, z) 365 if b == 0 { if h < 0-220 { return 14 + 42*256 + 120*WP_MAGIC_65536 } return 24 + 70*256 + 165*WP_MAGIC_65536 } 366 if h < 0 { return 52 + 118*256 + 196*WP_MAGIC_65536 } // carved river water 367 var col: i64 = 96 + 150*256 + 60*WP_MAGIC_65536 // plains grass 368 if b == 1 { col = 216 + 196*256 + 140*WP_MAGIC_65536 } // beach sand 369 if b == 3 { col = 228 + 200*256 + 120*WP_MAGIC_65536 } // desert sand 370 if b == 4 { col = 232 + 236*256 + 244*WP_MAGIC_65536 } // snow 371 if b == 5 { col = 130 + 124*256 + 120*WP_MAGIC_65536 } // peaks rock 372 if b == 6 { col = 52 + 110*256 + 44*WP_MAGIC_65536 } // forest grass 373 if slope > 110 { col = 122 + 112*256 + 96*WP_MAGIC_65536 } // steep -> exposed rock 374 if h > 330 { col = 236 + 239*256 + 246*WP_MAGIC_65536 } // snowcap by altitude 375 return col 376} 377 378// ---- STAGE 6.5: LIGHTING (2026-07-26, F1158 -- the stage the residual list always named). 379// WHY (measured, not taste): every head-to-head frame score names PALETTE as the gap -- the staged 380// world graded 667 RICH with det4 112 ~ Breeders' 125 but palette 227 vs their 1904, because flat 381// per-biome fills emit a handful of distinct colours. Lighting = sun-facing diffuse off the REAL 382// heightfield + per-cell albedo variation, so colour count comes from GEOMETRY, not from noise 383// (the GX-13/14 Goodhart line: a texture overlay that moves one stat is forbidden; shading that 384// follows the terrain is structure). Toggleable like the carver so the gate proves CAUSALITY. 385const WP_L_AMB: i64 = 256 // FLAT GROUND = unity (measured: an ambient floor of 168 made the 386 // stage darken-only -- 183/183 shade-darkened but 3/201 sun-brightened; 387 // centring at 256 gives true brighten/darken symmetry about unlit) 388const WP_L_GAIN: i64 = 22 // diffuse gain per height-step unit 389const WP_L_STEP: i64 = 90 // sample distance for the slope normal 390const WP_L_MAXB: i64 = 340 // brightness ceiling (overbright allowed; channels clamp at 255) 391const WP_L_MINB: i64 = 96 // brightness floor (shadow side never crushes to black) 392const WP_L_JIT: i64 = 14 // per-cell albedo jitter amplitude (small: variation, not noise) 393const WP_L_CELL: i64 = 96 // albedo cell size (matches the feature grid feel) 394func wp_set_light(v: i64) -> i64 { WP_LIGHT = v; return 0 } 395func wp_shade(x: i64, z: i64, h: i64, slope: i64) -> i64 { 396 let base: i64 = wp_surface(x, z, h, slope) 397 if WP_LIGHT == 0 { return base } 398 // sun from the north-west, elevation baked into the gain: faces dropping toward the sun catch it 399 let hw2: i64 = wp_height(x - WP_L_STEP, z - WP_L_STEP) 400 var d: i64 = (hw2 - h) 401 if d > WP_L_STEP { d = WP_L_STEP } 402 if d < 0-WP_L_STEP { d = 0-WP_L_STEP } 403 var bright: i64 = WP_L_AMB + (d * WP_L_GAIN) / 10 404 if bright > WP_L_MAXB { bright = WP_L_MAXB } 405 if bright < WP_L_MINB { bright = WP_L_MINB } 406 // per-cell albedo variation: a deterministic +-JIT on each channel, cell-stable (not per-pixel noise) 407 let cx: i64 = x / WP_L_CELL 408 let cz: i64 = z / WP_L_CELL 409 let jr: i64 = tm_hash3(cx, 91, cz) % (2*WP_L_JIT+1) - WP_L_JIT 410 let jg: i64 = tm_hash3(cx, 92, cz) % (2*WP_L_JIT+1) - WP_L_JIT 411 let jb2: i64 = tm_hash3(cx, 93, cz) % (2*WP_L_JIT+1) - WP_L_JIT 412 var r: i64 = ((base % 256) + jr) * bright / 256 413 var g: i64 = (((base / 256) % 256) + jg) * bright / 256 414 var b: i64 = (((base / WP_MAGIC_65536) % 256) + jb2) * bright / 256 415 r = wp_clamp(r, 0, 255) 416 g = wp_clamp(g, 0, 255) 417 b = wp_clamp(b, 0, 255) 418 return r + g*256 + b*WP_MAGIC_65536 419} 420 421// ---- STAGE 7: feature at a cell (0 none, 1 tree, 2 cactus, 3 spruce, 4 boulder) ---- 422func wp_feature(cx: i64, cz: i64) -> i64 { 423 if WP_FEAT == 0 { return 0 } 424 let h: i64 = wp_height(cx, cz) 425 if h < 6 { return 0 } // no features in water/rivers/beach-line 426 let b: i64 = wp_biome(cx, cz) 427 let hsh: i64 = tm_hash3(cx, WP_SEED*31 + 7, cz) 428 if b == 6 { if hsh < 190 { return 1 } } // forest: dense trees 429 if b == 7 { if hsh < 18 { return 1 } } // plains: sparse trees 430 if b == 3 { if hsh < 55 { return 2 } } // desert: cacti 431 if b == 4 { if hsh < 75 { return 3 } } // snowy: spruce 432 if b == 5 { if hsh < 45 { return 4 } } // peaks: boulders 433 return 0 434} 435 436// ---- STAGE 8: deterministic spawn -- first gentle land cell on an outward scan. out = [x, z, h]; 1 if found ---- 437func wp_spawn(out: *i64) -> i64 { 438 var r: i64 = 0 439 while r < 40 { 440 var i: i64 = 0 441 while i < 8 { 442 var x: i64 = 0 443 var z: i64 = 0 444 if i == 0 { x = r*130 } 445 if i == 1 { x = 0-r*130 } 446 if i == 2 { z = r*130 } 447 if i == 3 { z = 0-r*130 } 448 if i == 4 { x = r*130; z = r*130 } 449 if i == 5 { x = 0-r*130; z = r*130 } 450 if i == 6 { x = r*130; z = 0-r*130 } 451 if i == 7 { x = 0-r*130; z = 0-r*130 } 452 let h: i64 = wp_height(x, z) 453 if h > 10 { 454 let s1: i64 = wp_iabs(wp_height(x+90, z) - h) 455 let s2: i64 = wp_iabs(wp_height(x, z+90) - h) 456 if s1 + s2 < 80 { 457 let b: i64 = wp_biome(x, z) 458 if b != 5 { out[0] = x; out[1] = z; out[2] = h; return 1 } 459 } 460 } 461 i = i + 1 462 } 463 r = r + 1 464 } 465 return 0 466}