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}