nx_world_event.nx source
↩ module page · 605 lines · 27580 B
1// nx_world_event.nx -- dynamic world events: comets hit, volcanoes
2// erupt, wizards terraform. The DYNAMIC layer that makes procgen
3// worlds feel ALIVE rather than static.
4//
5// Per user 2026-05-15: "comets hitting the earth randomly and
6// volcanoes going off etc our 14 day approach should really drive a
7// real immersion... even if fantasy where a real powerful wizard or
8// god could terraform."
9//
10// Captures the SAME TYPE OF CAUSAL LOGIC that solar-system bodies
11// have (impacts pock the surface; volcanism builds cones; tectonic
12// stress lifts mountains; floods carve channels), generalised so a
13// fantasy world's wizard / god / player can drive the same kinds of
14// mutations. Not solar-system-specific.
15//
16// Static landmarks (Olympus Mons, Caloris Basin) live in
17// nx_anchor_feature + nx_body_anchors. This primitive captures
18// EVENTS THAT HAPPENED IN THE WORLD'S HISTORY -- with a time stamp
19// and a decay model so old impacts gradually erode back toward
20// baseline.
21//
22// Event record (8 i64 fields):
23// rec[0] = kind (NX_WORLD_EVENT_*)
24// rec[1] = time_q14 when the event happened (arbitrary
25// unit; caller defines the time scale)
26// rec[2] = cx_q14 position x in world coords
27// rec[3] = cy_q14 position y in world coords
28// rec[4] = radius_q14 characteristic affected radius
29// rec[5] = intensity_q14_m peak elevation change (positive); the
30// sign is set by the kind (impacts dig
31// down, eruptions build up)
32// rec[6] = decay_age_q14 how long the event takes to erode back
33// to baseline (older than this -> 0
34// contribution). Setting decay_age to a
35// very large value gives "permanent"
36// features.
37// rec[7] = meta kind-specific extra parameter
38// (orientation, asymmetry seed, biome
39// shift kind, etc.)
40//
41// Per-kind interpretation of (intensity, meta) varies; documented at
42// each contribution function.
43//
44// genealogy_id: housen_holsapple_2011_impact_cratering_scaling +
45// wood_1980_volcano_geomorphology +
46// adams_2018_dwarf_fortress_world_history +
47// wojtan_2007_decay_geometry
48// lineage_id: nx_world_event_dynamic_q14_v1
49//
50// nx_safety_envelope:
51// intended_use: "8 deep-time event kinds (volcanic / impact /
52// glaciation / etc.) + decay model -- cardinal
53// feedback-procgen-deep-time-causality-fantasy-
54// included Substrate-C foundation"
55// sil_target: SIL1
56// asil_target: QM
57// dal_target: NONE
58// evidence: [Q14_fixed_point_deterministic,
59// sealed_8_event_kind_enum,
60// decay_model_classical]
61// hazard_register: [bug-tape-event-overlap-causality-violation,
62// bug-tape-decay-rate-underflow]
63// residual_risk: "Procgen substrate; surfaces in game-world."
64// verdict: NOT_YET_EVALUATED
65
66import "nx_syscalls.nx"
67import "nx_hal.nx"
68import "nx_tier.nx"
69const NX_MAGIC_11025: i64 = 11025
70const NX_MAGIC_10000: i64 = 10000
71const NX_MAGIC_1100: i64 = 1100
72const NX_MAGIC_7000: i64 = 7000
73const NX_MAGIC_1000000: i64 = 1000000
74const NX_MAGIC_3505: i64 = 3505
75const NX_MAGIC_3495: i64 = 3495
76const NX_MAGIC_22000: i64 = 22000
77const NX_MAGIC_19000: i64 = 19000
78const NX_MAGIC_20000: i64 = 20000
79const NX_MAGIC_16000: i64 = 16000
80const NX_MAGIC_17000: i64 = 17000
81const NX_MAGIC_5000: i64 = 5000
82const NX_MAGIC_2000: i64 = 2000
83const NX_MAGIC_12750: i64 = 12750
84const NX_MAGIC_12850: i64 = 12850
85
86// ===== Q14 ==========================================================
87const NX_WE_Q: nx_int = 16384
88
89// Event record stride.
90const NX_WE_STRIDE: nx_int = 8
91
92// Field offsets.
93const NX_WE_OFF_KIND: nx_int = 0
94const NX_WE_OFF_TIME: nx_int = 1
95const NX_WE_OFF_CX: nx_int = 2
96const NX_WE_OFF_CY: nx_int = 3
97const NX_WE_OFF_RADIUS: nx_int = 4
98const NX_WE_OFF_INTENSITY: nx_int = 5
99const NX_WE_OFF_DECAY_AGE: nx_int = 6
100const NX_WE_OFF_META: nx_int = 7
101
102// ===== Event kinds (sealed enum) ===================================
103// Order chosen so natural events come first, terraform (god/wizard)
104// events later. IDs reserved additively per cardinal rule 13.
105const NX_WORLD_EVENT_IMPACT: nx_int = 0 // comet / asteroid hit
106const NX_WORLD_EVENT_VOLCANIC: nx_int = 1 // eruption builds cone + caldera
107const NX_WORLD_EVENT_EARTHQUAKE: nx_int = 2 // fault uplift (bidirectional displacement)
108const NX_WORLD_EVENT_FLOOD: nx_int = 3 // sediment deposit in low areas
109const NX_WORLD_EVENT_TERRAFORM_RAISE: nx_int = 4 // god/wizard radial uplift
110const NX_WORLD_EVENT_TERRAFORM_LOWER: nx_int = 5 // god/wizard radial depression
111const NX_WORLD_EVENT_TERRAFORM_SCORCH: nx_int = 6 // biome shift to desert (no height change)
112const NX_WORLD_EVENT_TERRAFORM_BLOOM: nx_int = 7 // biome shift to verdant (no height change)
113
114const NX_WORLD_EVENT_KIND_COUNT: nx_int = 8
115
116// ===== Validity predicate ==========================================
117func nx_world_event_kind_is_valid(k: nx_int) -> nx_int {
118 if k == NX_WORLD_EVENT_IMPACT { return 1 }
119 if k == NX_WORLD_EVENT_VOLCANIC { return 1 }
120 if k == NX_WORLD_EVENT_EARTHQUAKE { return 1 }
121 if k == NX_WORLD_EVENT_FLOOD { return 1 }
122 if k == NX_WORLD_EVENT_TERRAFORM_RAISE { return 1 }
123 if k == NX_WORLD_EVENT_TERRAFORM_LOWER { return 1 }
124 if k == NX_WORLD_EVENT_TERRAFORM_SCORCH { return 1 }
125 if k == NX_WORLD_EVENT_TERRAFORM_BLOOM { return 1 }
126 return 0
127}
128
129// ===== Event record write helper ===================================
130// Caller-convenience function: fills an 8-field record buffer with
131// the supplied fields. Caller can also write fields directly.
132func nx_world_event_write(
133 rec: *i64,
134 kind: nx_int,
135 time_q14: nx_int,
136 cx_q14: nx_int,
137 cy_q14: nx_int,
138 radius_q14: nx_int,
139 intensity_q14_m: nx_int,
140 decay_age_q14: nx_int,
141 meta: nx_int
142) {
143 rec[NX_WE_OFF_KIND] = kind
144 rec[NX_WE_OFF_TIME] = time_q14
145 rec[NX_WE_OFF_CX] = cx_q14
146 rec[NX_WE_OFF_CY] = cy_q14
147 rec[NX_WE_OFF_RADIUS] = radius_q14
148 rec[NX_WE_OFF_INTENSITY] = intensity_q14_m
149 rec[NX_WE_OFF_DECAY_AGE] = decay_age_q14
150 rec[NX_WE_OFF_META] = meta
151}
152
153// ===== Decay factor based on event age =============================
154// Returns Q14 factor in [0, Q]: 1 for a just-happened event, 0 for
155// events older than decay_age, linear in between. Permanent features
156// (very large decay_age) effectively stay at factor 1 for the
157// duration of any playthrough.
158//
159// decay_factor(t_now, t_event, decay_age) =
160// clamp((decay_age - (t_now - t_event)) / decay_age, 0, 1)
161//
162// Future-events (t_event > t_now) get factor 0 (haven't happened
163// yet) -- supports a god/wizard scheduling an event for the future
164// and the world only seeing it once time advances.
165func nx_world_event_decay_factor_q14(
166 current_time_q14: nx_int,
167 event_time_q14: nx_int,
168 decay_age_q14: nx_int
169) -> nx_int {
170 if decay_age_q14 <= 0 { return 0 }
171 let age: nx_int = current_time_q14 - event_time_q14
172 if age < 0 { return 0 } // future event
173 if age >= decay_age_q14 { return 0 } // fully eroded
174 return (decay_age_q14 - age) * NX_WE_Q / decay_age_q14
175}
176
177// ===== Impact shape ================================================
178// Parabolic depression with raised rim (~5% of intensity at the
179// rim, fading outward). Models a fresh impact crater.
180//
181// Inside radius * 0.9: full parabolic depression.
182// Between radius * 0.9 and radius * 1.05: positive rim (ejecta).
183// Outside radius * 1.05: 0.
184func _we_impact_shape(
185 cx: nx_int, cy: nx_int, px: nx_int, py: nx_int,
186 radius: nx_int, intensity: nx_int
187) -> nx_int {
188 let dx: nx_int = px - cx
189 let dy: nx_int = py - cy
190 let d2: nx_int = dx * dx + dy * dy
191 let r2: nx_int = radius * radius
192
193 // Inner bowl: 0..(0.9r)^2 = 0..(0.81 r2)
194 let inner_r2: nx_int = 81 * r2 / 100
195 // Rim band: (0.9r)^2 .. (1.05r)^2 = 0.81r2 .. 1.1025r2
196 let rim_r2: nx_int = NX_MAGIC_11025 * r2 / NX_MAGIC_10000
197
198 if d2 >= rim_r2 { return 0 }
199 if d2 < inner_r2 {
200 // Parabolic depression: -intensity * (1 - d2/inner_r2).
201 return 0 - intensity * (inner_r2 - d2) / inner_r2
202 }
203 // Rim band: positive bump (small fraction of intensity).
204 // Peak at boundary inner_r2; tapers to 0 at rim_r2.
205 let rim_width: nx_int = rim_r2 - inner_r2
206 let rim_pos: nx_int = rim_r2 - d2 // [0, rim_width]
207 let rim_peak: nx_int = intensity / 20 // ~5% of intensity
208 return rim_peak * rim_pos / rim_width
209}
210
211// ===== Volcanic eruption shape =====================================
212// Parabolic dome + small caldera in centre. Models a built-up shield
213// volcano with collapsed crater at the summit.
214//
215// Inside radius * 0.15: caldera (small negative dip at the summit).
216// Inside radius: full parabolic dome.
217// Outside radius: 0.
218func _we_volcanic_shape(
219 cx: nx_int, cy: nx_int, px: nx_int, py: nx_int,
220 radius: nx_int, intensity: nx_int
221) -> nx_int {
222 let dx: nx_int = px - cx
223 let dy: nx_int = py - cy
224 let d2: nx_int = dx * dx + dy * dy
225 let r2: nx_int = radius * radius
226 if d2 >= r2 { return 0 }
227
228 // Caldera radius = 15% of full radius.
229 let caldera_r2: nx_int = 225 * r2 / NX_MAGIC_10000 // (0.15 r)^2
230
231 // Dome part: intensity * (1 - d2/r2).
232 let dome: nx_int = intensity * (r2 - d2) / r2
233
234 if d2 >= caldera_r2 { return dome }
235
236 // Caldera dip: subtract a small fraction at the very top.
237 let caldera_pos: nx_int = caldera_r2 - d2 // [0, caldera_r2]
238 let caldera_max: nx_int = intensity / 10 // ~10% depth of caldera
239 let caldera_dip: nx_int = caldera_max * caldera_pos / caldera_r2
240 return dome - caldera_dip
241}
242
243// ===== Generic radial dome/depression ==============================
244// Pure parabolic, no rim, no caldera. Used by both TERRAFORM_RAISE
245// (positive intensity) and TERRAFORM_LOWER (the caller-visible
246// "lower" semantics is encoded by passing positive intensity and
247// the dispatch negates).
248func _we_radial_parabolic(
249 cx: nx_int, cy: nx_int, px: nx_int, py: nx_int,
250 radius: nx_int, intensity: nx_int
251) -> nx_int {
252 let dx: nx_int = px - cx
253 let dy: nx_int = py - cy
254 let d2: nx_int = dx * dx + dy * dy
255 let r2: nx_int = radius * radius
256 if d2 >= r2 { return 0 }
257 return intensity * (r2 - d2) / r2
258}
259
260// ===== Earthquake fault shape ======================================
261// Linear fault-line deformation. One side rises (upthrust), the
262// other side drops (downthrust). Meta encodes the fault orientation
263// (0 = N-S fault axis; 1 = E-W). Magnitude = peak upthrust height;
264// downthrust is symmetric negative. Half-width 1/4 of radius gives
265// a fault-zone width comparable to ridges.
266func _we_earthquake_shape(
267 cx: nx_int, cy: nx_int, px: nx_int, py: nx_int,
268 radius: nx_int, intensity: nx_int, meta: nx_int
269) -> nx_int {
270 var d_along: nx_int = 0
271 var d_perp: nx_int = 0
272 if meta == 0 {
273 // N-S fault: rises/falls as a function of perpendicular X.
274 d_along = py - cy
275 d_perp = px - cx
276 } else {
277 // E-W fault: rises/falls as a function of perpendicular Y.
278 d_along = px - cx
279 d_perp = py - cy
280 }
281 var along_abs: nx_int = d_along
282 if along_abs < 0 { along_abs = 0 - along_abs }
283 if along_abs >= radius { return 0 }
284 let half_width: nx_int = radius / 4
285 var perp_abs: nx_int = d_perp
286 if perp_abs < 0 { perp_abs = 0 - perp_abs }
287 if perp_abs >= half_width { return 0 }
288
289 // Length taper smooths toward ends.
290 let length_taper: nx_int = (radius - along_abs) * NX_WE_Q / radius
291 // Cross-fault profile: linear ramp from -intensity (one side) to
292 // +intensity (other side), with sign of d_perp.
293 let sign_perp: nx_int = d_perp
294 // Magnitude along the fault zone -- closer to fault line = larger
295 // delta (linear in |perp| / half_width).
296 var amp: nx_int = perp_abs * intensity / half_width
297 if sign_perp < 0 { amp = 0 - amp }
298 return amp * length_taper / NX_WE_Q
299}
300
301// ===== Flood shape =================================================
302// Sediment deposit in low-lying areas + slight smoothing. Returns a
303// SMALL positive delta in flood zones (sediment built up). Larger
304// effect at the centre (worst-affected); zero at the edge. Caller
305// composes with their existing heightmap; the v2 follow-up adds a
306// water-level flag for actual standing water.
307func _we_flood_shape(
308 cx: nx_int, cy: nx_int, px: nx_int, py: nx_int,
309 radius: nx_int, intensity: nx_int
310) -> nx_int {
311 let dx: nx_int = px - cx
312 let dy: nx_int = py - cy
313 let d2: nx_int = dx * dx + dy * dy
314 let r2: nx_int = radius * radius
315 if d2 >= r2 { return 0 }
316 // Gentle deposit: 30% of intensity peak (most of the flood effect
317 // is biome shift to wetland, not height change).
318 let peak: nx_int = intensity * 3 / 10
319 return peak * (r2 - d2) / r2
320}
321
322// ===== Scorch / Bloom shape (biome shift, no height change) ========
323// These events shift biome but don't permanently change heightmap.
324// Returns 0 for height contribution. Callers that want the biome
325// shift use nx_world_event_biome_at() instead.
326func _we_biome_shift_zero_height() -> nx_int {
327 return 0
328}
329
330// ===== Biome-shift accessor for SCORCH + BLOOM =====================
331// Caller passes the default biome at (px, py); if the query is inside
332// a SCORCH zone, returns the scorched biome (default 8 = DESERT
333// interpretation); if inside BLOOM, returns the verdant biome. Meta
334// encodes the target biome ID; if meta = 0, uses defaults.
335//
336// All other event kinds return the default biome unchanged.
337func nx_world_event_biome_at(
338 rec: *i64,
339 px_q14: nx_int,
340 py_q14: nx_int,
341 current_time_q14: nx_int,
342 default_biome: nx_int
343) -> nx_int {
344 let kind: nx_int = rec[NX_WE_OFF_KIND]
345 if kind != NX_WORLD_EVENT_TERRAFORM_SCORCH {
346 if kind != NX_WORLD_EVENT_TERRAFORM_BLOOM {
347 return default_biome
348 }
349 }
350 let t_event: nx_int = rec[NX_WE_OFF_TIME]
351 let decay_age: nx_int = rec[NX_WE_OFF_DECAY_AGE]
352 let decay: nx_int = nx_world_event_decay_factor_q14(
353 current_time_q14, t_event, decay_age)
354 if decay <= 0 { return default_biome }
355
356 let cx: nx_int = rec[NX_WE_OFF_CX]
357 let cy: nx_int = rec[NX_WE_OFF_CY]
358 let r: nx_int = rec[NX_WE_OFF_RADIUS]
359 let meta: nx_int = rec[NX_WE_OFF_META]
360
361 // In-zone test.
362 let dx: nx_int = px_q14 - cx
363 let dy: nx_int = py_q14 - cy
364 let d2: nx_int = dx * dx + dy * dy
365 let r2: nx_int = r * r
366 if d2 >= r2 { return default_biome }
367
368 // Target biome from meta; if meta == 0, use sensible defaults.
369 if kind == NX_WORLD_EVENT_TERRAFORM_SCORCH {
370 if meta != 0 { return meta }
371 return 8 // DESERT (matches nx_biome_classifier convention)
372 }
373 // BLOOM
374 if meta != 0 { return meta }
375 return 5 // TEMPERATE_FOREST
376}
377
378// ===== Public: contribution of one event at a query point ==========
379// rec: 8-i64 event record.
380// px, py: query point in world coords (Q14).
381// current_time_q14: current simulation time.
382//
383// Returns Q14 height delta (signed). Decay factor is applied
384// internally; old or future events contribute 0.
385func nx_world_event_contribution(
386 rec: *i64,
387 px_q14: nx_int,
388 py_q14: nx_int,
389 current_time_q14: nx_int
390) -> nx_int {
391 let kind: nx_int = rec[NX_WE_OFF_KIND]
392 let t_event: nx_int = rec[NX_WE_OFF_TIME]
393 let cx: nx_int = rec[NX_WE_OFF_CX]
394 let cy: nx_int = rec[NX_WE_OFF_CY]
395 let r: nx_int = rec[NX_WE_OFF_RADIUS]
396 let i: nx_int = rec[NX_WE_OFF_INTENSITY]
397 let decay_age: nx_int = rec[NX_WE_OFF_DECAY_AGE]
398
399 let decay: nx_int = nx_world_event_decay_factor_q14(
400 current_time_q14, t_event, decay_age)
401 if decay <= 0 { return 0 }
402
403 let meta: nx_int = rec[NX_WE_OFF_META]
404 var shape: nx_int = 0
405 if kind == NX_WORLD_EVENT_IMPACT {
406 shape = _we_impact_shape(cx, cy, px_q14, py_q14, r, i)
407 }
408 if kind == NX_WORLD_EVENT_VOLCANIC {
409 shape = _we_volcanic_shape(cx, cy, px_q14, py_q14, r, i)
410 }
411 if kind == NX_WORLD_EVENT_TERRAFORM_RAISE {
412 shape = _we_radial_parabolic(cx, cy, px_q14, py_q14, r, i)
413 }
414 if kind == NX_WORLD_EVENT_TERRAFORM_LOWER {
415 shape = 0 - _we_radial_parabolic(cx, cy, px_q14, py_q14, r, i)
416 }
417 if kind == NX_WORLD_EVENT_EARTHQUAKE {
418 shape = _we_earthquake_shape(cx, cy, px_q14, py_q14, r, i, meta)
419 }
420 if kind == NX_WORLD_EVENT_FLOOD {
421 shape = _we_flood_shape(cx, cy, px_q14, py_q14, r, i)
422 }
423 // SCORCH + BLOOM are biome-shift only; no height contribution.
424 // Use nx_world_event_biome_at() for the biome lookup.
425
426 // Apply decay factor: shape * decay / Q.
427 return shape * decay / NX_WE_Q
428}
429
430// ===== Public: sum contributions of an event list ==================
431// events: pointer to N * 8 i64. Iterates and sums per-event
432// contributions at (px, py) at current_time.
433func nx_world_event_sum_contribution(
434 events: *i64,
435 n_events: nx_int,
436 px_q14: nx_int,
437 py_q14: nx_int,
438 current_time_q14: nx_int
439) -> nx_int {
440 var sum: nx_int = 0
441 var i: nx_int = 0
442 while i < n_events {
443 let rec: *i64 = (events as i64 + i * NX_WE_STRIDE * NX_SIZEOF_NX_INT) as *i64
444 sum = sum + nx_world_event_contribution(rec, px_q14, py_q14, current_time_q14)
445 i = i + 1
446 }
447 return sum
448}
449
450// ===== Self-test ====================================================
451func main() -> i64 {
452 let q: nx_int = NX_WE_Q
453
454 // T1: Kind validity predicate.
455 if nx_world_event_kind_is_valid(NX_WORLD_EVENT_IMPACT) != 1 { return nx_hal_exit(1) }
456 if nx_world_event_kind_is_valid(NX_WORLD_EVENT_VOLCANIC) != 1 { return nx_hal_exit(2) }
457 if nx_world_event_kind_is_valid(NX_WORLD_EVENT_TERRAFORM_RAISE) != 1 { return nx_hal_exit(3) }
458 if nx_world_event_kind_is_valid(NX_WORLD_EVENT_TERRAFORM_LOWER) != 1 { return nx_hal_exit(4) }
459 if nx_world_event_kind_is_valid(NX_WORLD_EVENT_TERRAFORM_BLOOM) != 1 { return nx_hal_exit(5) }
460 if nx_world_event_kind_is_valid(99) != 0 { return nx_hal_exit(6) }
461
462 // T2: Decay factor -- just-happened event = Q, half-lifetime = Q/2,
463 // fully-eroded = 0, future event = 0.
464 if nx_world_event_decay_factor_q14(100, 100, 1000) != q { return nx_hal_exit(10) }
465 if nx_world_event_decay_factor_q14(600, 100, 1000) != q / 2 { return nx_hal_exit(11) }
466 if nx_world_event_decay_factor_q14(NX_MAGIC_1100, 100, 1000) != 0 { return nx_hal_exit(12) }
467 if nx_world_event_decay_factor_q14(50, 100, 1000) != 0 { return nx_hal_exit(13) }
468 if nx_world_event_decay_factor_q14(100, 100, 0) != 0 { return nx_hal_exit(14) } // zero decay_age refused
469
470 // T3: Impact event at centre = -intensity * decay (after decay).
471 // Just-happened impact (t_event = 0, current_time = 0,
472 // decay_age = very large) -> full intensity, no decay erosion.
473 let rec: *i64 = (sys_mmap(NX_WE_STRIDE * NX_SIZEOF_NX_INT)) as *i64
474 nx_world_event_write(rec, NX_WORLD_EVENT_IMPACT, 0, 100 * q, 200 * q, 50 * q, NX_MAGIC_7000, NX_MAGIC_1000000, 0)
475 // At exact centre, contribution should be -7000 (impact bowl) * 1.0 decay.
476 let c_centre: nx_int = nx_world_event_contribution(rec, 100 * q, 200 * q, 0)
477 if c_centre != (0 - NX_MAGIC_7000) { return nx_hal_exit(20) }
478
479 // T4: Same impact, but query OUTSIDE rim -> 0.
480 let c_far: nx_int = nx_world_event_contribution(rec, 100 * q + 100 * q, 200 * q, 0)
481 if c_far != 0 { return nx_hal_exit(30) }
482
483 // T5: Same impact, FUTURE query time -> 0 (event hasn't happened
484 // yet from the perspective of the simulation).
485 nx_world_event_write(rec, NX_WORLD_EVENT_IMPACT, 1000, 100 * q, 200 * q, 50 * q, NX_MAGIC_7000, NX_MAGIC_1000000, 0)
486 let c_future: nx_int = nx_world_event_contribution(rec, 100 * q, 200 * q, 500)
487 if c_future != 0 { return nx_hal_exit(40) }
488 // After event happens at t=1000, query at t=1000 -> full effect.
489 let c_now: nx_int = nx_world_event_contribution(rec, 100 * q, 200 * q, 1000)
490 if c_now != (0 - NX_MAGIC_7000) { return nx_hal_exit(41) }
491
492 // T6: Decay over time. Decay age = 1000. Event at t=0; query at
493 // t=500 -> half decay -> half intensity.
494 nx_world_event_write(rec, NX_WORLD_EVENT_IMPACT, 0, 0, 0, 50 * q, NX_MAGIC_7000, 1000, 0)
495 let c_half: nx_int = nx_world_event_contribution(rec, 0, 0, 500)
496 // Expected -7000 * 0.5 = -3500 (allow +/- 5).
497 if c_half < 0 - NX_MAGIC_3505 { return nx_hal_exit(50) }
498 if c_half > 0 - NX_MAGIC_3495 { return nx_hal_exit(51) }
499
500 // T7: Volcanic event -- at the summit there's a caldera dip but
501 // overall the contribution should be strongly POSITIVE (the cone
502 // is much taller than the caldera is deep). At centre = dome -
503 // caldera_dip. Dome = intensity, caldera_dip = intensity/10.
504 // So summit contribution = intensity * 0.9 = 19800 for intensity
505 // 22000.
506 nx_world_event_write(rec, NX_WORLD_EVENT_VOLCANIC, 0, 0, 0, 100 * q, NX_MAGIC_22000, NX_MAGIC_1000000, 0)
507 let v_centre: nx_int = nx_world_event_contribution(rec, 0, 0, 0)
508 if v_centre < NX_MAGIC_19000 { return nx_hal_exit(60) }
509 if v_centre > NX_MAGIC_20000 { return nx_hal_exit(61) }
510 // Mid-flank (radius/2) -> dome = intensity * 0.75 = 16500.
511 let v_flank: nx_int = nx_world_event_contribution(rec, 50 * q, 0, 0)
512 if v_flank < NX_MAGIC_16000 { return nx_hal_exit(70) }
513 if v_flank > NX_MAGIC_17000 { return nx_hal_exit(71) }
514
515 // T8: TERRAFORM_RAISE -- wizard pulls land up. Pure parabolic
516 // dome, no caldera. At centre = full intensity.
517 nx_world_event_write(rec, NX_WORLD_EVENT_TERRAFORM_RAISE, 0, 0, 0, 50 * q, NX_MAGIC_5000, NX_MAGIC_1000000, 0)
518 let r_centre: nx_int = nx_world_event_contribution(rec, 0, 0, 0)
519 if r_centre != NX_MAGIC_5000 { return nx_hal_exit(80) }
520
521 // T9: TERRAFORM_LOWER -- wizard digs land down. Pure negative
522 // parabolic.
523 nx_world_event_write(rec, NX_WORLD_EVENT_TERRAFORM_LOWER, 0, 0, 0, 50 * q, NX_MAGIC_5000, NX_MAGIC_1000000, 0)
524 let lo_centre: nx_int = nx_world_event_contribution(rec, 0, 0, 0)
525 if lo_centre != (0 - NX_MAGIC_5000) { return nx_hal_exit(90) }
526
527 // T10: EARTHQUAKE -- N-S fault (meta=0). Symmetric bidirectional
528 // displacement: positive on one side, negative on the other. At
529 // the fault line itself (perp = 0) -> 0; off to one side at half
530 // the half-width -> non-trivial positive; mirrored side -> mirrored
531 // negative.
532 // radius = 50 * q -> half-width = radius / 4 = 12.5 * q.
533 // Query at perp = +6 * q (about half of half-width), along = 0:
534 // perp_abs/half_width = 6/12.5 ~ 0.48 -> amp = 0.48 * intensity.
535 // length_taper = 1.0. Expected ~ 0.48 * intensity = ~960.
536 nx_world_event_write(rec, NX_WORLD_EVENT_EARTHQUAKE, 0, 0, 0, 50 * q, NX_MAGIC_2000, NX_MAGIC_1000000, 0)
537 let eq_pos: nx_int = nx_world_event_contribution(rec, 6 * q, 0, 0)
538 if eq_pos < 900 { return nx_hal_exit(100) }
539 if eq_pos > 1020 { return nx_hal_exit(101) }
540 let eq_neg: nx_int = nx_world_event_contribution(rec, 0 - (6 * q), 0, 0)
541 if eq_neg > 0 - 900 { return nx_hal_exit(102) }
542 if eq_neg < 0 - 1020 { return nx_hal_exit(103) }
543 // On the fault line itself -> 0.
544 let eq_fault: nx_int = nx_world_event_contribution(rec, 0, 0, 0)
545 if eq_fault != 0 { return nx_hal_exit(104) }
546 // Beyond fault length -> 0.
547 let eq_far: nx_int = nx_world_event_contribution(rec, 6 * q, 100 * q, 0)
548 if eq_far != 0 { return nx_hal_exit(105) }
549
550 // T11: FLOOD -- gentle positive deposit centred at impact location.
551 // Peak = intensity * 0.3. intensity = 1000 -> peak ~ 300 at centre.
552 nx_world_event_write(rec, NX_WORLD_EVENT_FLOOD, 0, 0, 0, 50 * q, 1000, NX_MAGIC_1000000, 0)
553 let fl_centre: nx_int = nx_world_event_contribution(rec, 0, 0, 0)
554 if fl_centre < 290 { return nx_hal_exit(110) }
555 if fl_centre > 310 { return nx_hal_exit(111) }
556 // Outside radius -> 0.
557 let fl_far: nx_int = nx_world_event_contribution(rec, 100 * q, 0, 0)
558 if fl_far != 0 { return nx_hal_exit(112) }
559
560 // T12: SCORCH / BLOOM contribute 0 to height.
561 nx_world_event_write(rec, NX_WORLD_EVENT_TERRAFORM_SCORCH, 0, 0, 0, 50 * q, 0, NX_MAGIC_1000000, 0)
562 if nx_world_event_contribution(rec, 0, 0, 0) != 0 { return nx_hal_exit(120) }
563 nx_world_event_write(rec, NX_WORLD_EVENT_TERRAFORM_BLOOM, 0, 0, 0, 50 * q, 0, NX_MAGIC_1000000, 0)
564 if nx_world_event_contribution(rec, 0, 0, 0) != 0 { return nx_hal_exit(121) }
565
566 // T13: SCORCH biome-shift -- inside zone returns scorched-desert id
567 // (default 8 when meta=0); outside returns default biome.
568 nx_world_event_write(rec, NX_WORLD_EVENT_TERRAFORM_SCORCH, 0, 0, 0, 50 * q, 0, NX_MAGIC_1000000, 0)
569 let sb_in: nx_int = nx_world_event_biome_at(rec, 0, 0, 0, 5)
570 if sb_in != 8 { return nx_hal_exit(130) }
571 let sb_out: nx_int = nx_world_event_biome_at(rec, 100 * q, 0, 0, 5)
572 if sb_out != 5 { return nx_hal_exit(131) }
573 // SCORCH with explicit meta=11 -> returns 11 inside zone.
574 nx_world_event_write(rec, NX_WORLD_EVENT_TERRAFORM_SCORCH, 0, 0, 0, 50 * q, 0, NX_MAGIC_1000000, 11)
575 let sb_meta: nx_int = nx_world_event_biome_at(rec, 0, 0, 0, 5)
576 if sb_meta != 11 { return nx_hal_exit(132) }
577
578 // T14: BLOOM biome-shift -- inside zone returns verdant id (default
579 // 5 when meta=0); decayed event returns default biome.
580 nx_world_event_write(rec, NX_WORLD_EVENT_TERRAFORM_BLOOM, 0, 0, 0, 50 * q, 0, NX_MAGIC_1000000, 0)
581 let bb_in: nx_int = nx_world_event_biome_at(rec, 0, 0, 0, 8)
582 if bb_in != 5 { return nx_hal_exit(140) }
583 // Future event -> not yet happened -> default biome.
584 nx_world_event_write(rec, NX_WORLD_EVENT_TERRAFORM_BLOOM, 1000, 0, 0, 50 * q, 0, NX_MAGIC_1000000, 0)
585 let bb_future: nx_int = nx_world_event_biome_at(rec, 0, 0, 500, 8)
586 if bb_future != 8 { return nx_hal_exit(141) }
587 // Non-biome-shifting kinds return default biome unchanged.
588 nx_world_event_write(rec, NX_WORLD_EVENT_IMPACT, 0, 0, 0, 50 * q, NX_MAGIC_7000, NX_MAGIC_1000000, 0)
589 let bb_other: nx_int = nx_world_event_biome_at(rec, 0, 0, 0, 8)
590 if bb_other != 8 { return nx_hal_exit(142) }
591
592 // T11: Event-list sum. Two events at same place: an impact then
593 // a volcanic eruption directly in the crater. Net at centre:
594 // -7000 (impact bowl) + 19800 (volcano summit) = 12800 ish.
595 let events: *i64 = (sys_mmap(2 * NX_WE_STRIDE * NX_SIZEOF_NX_INT)) as *i64
596 nx_world_event_write(events, NX_WORLD_EVENT_IMPACT, 0, 0, 0, 100 * q, NX_MAGIC_7000, NX_MAGIC_1000000, 0)
597 nx_world_event_write((events as i64 + NX_WE_STRIDE * NX_SIZEOF_NX_INT) as *i64,
598 NX_WORLD_EVENT_VOLCANIC, 1, 0, 0, 100 * q, NX_MAGIC_22000, NX_MAGIC_1000000, 0)
599 let s: nx_int = nx_world_event_sum_contribution(events, 2, 0, 0, 100)
600 // -7000 + 19800 = 12800. Allow +/- 50.
601 if s < NX_MAGIC_12750 { return nx_hal_exit(110) }
602 if s > NX_MAGIC_12850 { return nx_hal_exit(111) }
603
604 return 0
605}