code wiki / (root) / nx_world_event.nx

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}