code wiki / (root) / nx_day_night_cycle.nx

nx_day_night_cycle.nx source

↩ module page · 189 lines · 8044 B

1// nx_day_night_cycle.nx -- diurnal lighting + sun position model. 2// 3// Per honest gap roadmap 2026-05-16: world needs a day/night loop so 4// gameplay has dawn/dusk dramatic moments, hostile mobs spawn at 5// night, and atmosphere primitives have meaningful sun position to 6// drive sky color. 7// 8// MODEL: 9// - 1 game day = N ticks (caller-configurable; default 24000 ticks 10// for Minecraft-compatible 20 minutes at 20 tps). 11// - Sub-phases: 12// * DAWN ticks [0, day/8) sun rising; sky reddening 13// * MORNING ticks [day/8, day*3/8) sun climbing 14// * NOON ticks [day*3/8, day*5/8) sun overhead 15// * AFTERNOON ticks [day*5/8, day*7/8) sun descending 16// * DUSK ticks [day*7/8, day*9/16) sun setting; sky reddening 17// * NIGHT ticks [day*9/16, day) moon up; hostile spawn enabled 18// (Actually for simplicity: day_frac in [0, Q]; mid-day = Q/2; 19// midnight = 0 / Q. Sub-phases inferred from day_frac.) 20// 21// PUBLIC APIs: 22// nx_day_frac_q14(tick, ticks_per_day) -> Q14 [0, Q] 23// nx_sun_zenith_q14(day_frac) -> Q14 (0 = overhead, Q = horizon) 24// nx_ambient_light_q14(day_frac) -> Q14 [0, Q] 25// nx_phase(day_frac) -> NX_PHASE_* 26// nx_is_night(day_frac) -> 0/1 27// 28// genealogy_id: solar_day_canon + minecraft_day_night 29// lineage_id: nx_day_night_cycle_q14_v1 30 31// nx_safety_envelope: 32// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 33// sil_target: SIL1 34// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 35// verdict: NOT_YET_EVALUATED 36 37import "nx_syscalls.nx" 38import "nx_tier.nx" 39const NX_MAGIC_24000: i64 = 24000 40const NX_MAGIC_12000: i64 = 12000 41const NX_MAGIC_48000: i64 = 48000 42 43const NX_DN_Q: nx_int = 16384 44 45// ===== Sealed-enum phases ========================================== 46const NX_PHASE_DAWN: nx_int = 0 47const NX_PHASE_MORNING: nx_int = 1 48const NX_PHASE_NOON: nx_int = 2 49const NX_PHASE_AFTERNOON: nx_int = 3 50const NX_PHASE_DUSK: nx_int = 4 51const NX_PHASE_NIGHT: nx_int = 5 52const NX_PHASE_COUNT: nx_int = 6 53 54func nx_phase_is_valid(p: nx_int) -> nx_int { 55 if p >= 0 { if p < NX_PHASE_COUNT { return 1 } } 56 return 0 57} 58 59// ===== Default day length =========================================== 60// 20 tps * 60 sec * 20 min = 24000 ticks (Minecraft default). 61const NX_DN_DEFAULT_TICKS_PER_DAY: nx_int = 24000 62 63// ===== Day fraction ================================================= 64// Returns the current point in the day as a Q14 fraction [0, Q]. 65// 0 = midnight; Q/2 = noon; Q = next midnight. 66func nx_day_frac_q14(tick: nx_int, ticks_per_day: nx_int) -> nx_int { 67 var ttl: nx_int = ticks_per_day 68 if ttl <= 0 { ttl = NX_DN_DEFAULT_TICKS_PER_DAY } 69 var t: nx_int = tick % ttl 70 if t < 0 { t = t + ttl } 71 return (t * NX_DN_Q) / ttl 72} 73 74// ===== Phase classifier ============================================ 75// Maps day_frac to one of 6 phases. 76// night [0, 0.20Q) 77// dawn [0.20Q, 0.30Q) 78// morning [0.30Q, 0.45Q) 79// noon [0.45Q, 0.55Q) 80// afternoon[0.55Q, 0.70Q) 81// dusk [0.70Q, 0.80Q) 82// night [0.80Q, Q) 83func nx_phase(day_frac_q14: nx_int) -> nx_int { 84 let q: nx_int = NX_DN_Q 85 if day_frac_q14 < (q * 20) / 100 { return NX_PHASE_NIGHT } 86 if day_frac_q14 < (q * 30) / 100 { return NX_PHASE_DAWN } 87 if day_frac_q14 < (q * 45) / 100 { return NX_PHASE_MORNING } 88 if day_frac_q14 < (q * 55) / 100 { return NX_PHASE_NOON } 89 if day_frac_q14 < (q * 70) / 100 { return NX_PHASE_AFTERNOON } 90 if day_frac_q14 < (q * 80) / 100 { return NX_PHASE_DUSK } 91 return NX_PHASE_NIGHT 92} 93 94// Is-night predicate (for hostile-mob spawn gating). 95func nx_is_night(day_frac_q14: nx_int) -> nx_int { 96 if nx_phase(day_frac_q14) == NX_PHASE_NIGHT { return 1 } 97 return 0 98} 99 100// ===== Sun zenith angle ============================================ 101// Returns Q14 with 0 = directly overhead, Q = at the horizon, beyond 102// Q = below horizon (night). 103// 104// Sinusoidal model: zenith = |cos(2*pi * day_frac)| approximately. 105// At day_frac=0 (midnight): zenith = Q (below horizon). 106// At day_frac=Q/2 (noon): zenith = 0 (overhead). 107// We use a piecewise-linear approximation: 108// day_frac in [0, 0.25Q]: zenith from Q to Q/2 109// day_frac in [0.25Q, 0.5Q]: zenith from Q/2 to 0 110// day_frac in [0.5Q, 0.75Q]: zenith from 0 to Q/2 111// day_frac in [0.75Q, Q]: zenith from Q/2 to Q 112func nx_sun_zenith_q14(day_frac_q14: nx_int) -> nx_int { 113 let q: nx_int = NX_DN_Q 114 if day_frac_q14 <= q / 4 { 115 let f: nx_int = (day_frac_q14 * q) / (q / 4) 116 return q - (f * (q / 2)) / q 117 } 118 if day_frac_q14 <= q / 2 { 119 let f: nx_int = ((day_frac_q14 - q / 4) * q) / (q / 4) 120 return (q / 2) - (f * (q / 2)) / q 121 } 122 if day_frac_q14 <= (q * 3) / 4 { 123 let f: nx_int = ((day_frac_q14 - q / 2) * q) / (q / 4) 124 return (f * (q / 2)) / q 125 } 126 let f: nx_int = ((day_frac_q14 - (q * 3) / 4) * q) / (q / 4) 127 return (q / 2) + (f * (q / 2)) / q 128} 129 130// ===== Ambient light =============================================== 131// Q14 [0, Q] inverse of zenith but more dramatic. 132// noon (day_frac = Q/2): ambient = Q 133// dawn/dusk: ambient = Q/2 134// midnight: ambient = Q/16 (moonlight floor) 135func nx_ambient_light_q14(day_frac_q14: nx_int) -> nx_int { 136 let q: nx_int = NX_DN_Q 137 let zenith: nx_int = nx_sun_zenith_q14(day_frac_q14) 138 // ambient = clamp(Q - zenith, Q/16, Q) 139 var light: nx_int = q - zenith 140 let floor_q: nx_int = q / 16 141 if light < floor_q { light = floor_q } 142 if light > q { light = q } 143 return light 144} 145 146// ===== Self-test ==================================================== 147func main() -> i64 { 148 let q: nx_int = NX_DN_Q 149 150 // T1: Day frac wraps modulo ticks_per_day. 151 if nx_day_frac_q14(0, NX_MAGIC_24000) != 0 { return __syscall(93, 1, 0, 0, 0, 0, 0) } 152 if nx_day_frac_q14(NX_MAGIC_12000, NX_MAGIC_24000) != q / 2 { return __syscall(93, 2, 0, 0, 0, 0, 0) } 153 if nx_day_frac_q14(NX_MAGIC_48000, NX_MAGIC_24000) != 0 { return __syscall(93, 3, 0, 0, 0, 0, 0) } // 2 days 154 // Default day length (24000). 155 if nx_day_frac_q14(0, 0) != 0 { return __syscall(93, 4, 0, 0, 0, 0, 0) } 156 157 // T2: Phase classification. 158 if nx_phase(0) != NX_PHASE_NIGHT { return __syscall(93, 10, 0, 0, 0, 0, 0) } 159 if nx_phase(q / 2) != NX_PHASE_NOON { return __syscall(93, 11, 0, 0, 0, 0, 0) } 160 if nx_phase((q * 25) / 100) != NX_PHASE_DAWN { return __syscall(93, 12, 0, 0, 0, 0, 0) } 161 if nx_phase((q * 75) / 100) != NX_PHASE_DUSK { return __syscall(93, 13, 0, 0, 0, 0, 0) } 162 if nx_phase(q - 100) != NX_PHASE_NIGHT { return __syscall(93, 14, 0, 0, 0, 0, 0) } 163 164 // T3: is_night predicate. 165 if nx_is_night(0) != 1 { return __syscall(93, 20, 0, 0, 0, 0, 0) } 166 if nx_is_night(q / 2) != 0 { return __syscall(93, 21, 0, 0, 0, 0, 0) } 167 if nx_is_night((q * 90) / 100) != 1 { return __syscall(93, 22, 0, 0, 0, 0, 0) } 168 169 // T4: Sun zenith: midnight = Q (below horizon); noon = 0 (overhead). 170 if nx_sun_zenith_q14(0) != q { return __syscall(93, 30, 0, 0, 0, 0, 0) } 171 if nx_sun_zenith_q14(q / 2) != 0 { return __syscall(93, 31, 0, 0, 0, 0, 0) } 172 // Quarter day (dawn): zenith = Q/2. 173 if nx_sun_zenith_q14(q / 4) != q / 2 { return __syscall(93, 32, 0, 0, 0, 0, 0) } 174 175 // T5: Ambient light: night floor; noon max. 176 let l_noon: nx_int = nx_ambient_light_q14(q / 2) 177 if l_noon != q { return __syscall(93, 40, 0, 0, 0, 0, 0) } 178 let l_night: nx_int = nx_ambient_light_q14(0) 179 if l_night != q / 16 { return __syscall(93, 41, 0, 0, 0, 0, 0) } 180 // Dawn: about Q/2. 181 let l_dawn: nx_int = nx_ambient_light_q14(q / 4) 182 if l_dawn != q / 2 { return __syscall(93, 42, 0, 0, 0, 0, 0) } 183 184 // T6: Phase validity predicate. 185 if nx_phase_is_valid(NX_PHASE_NOON) != 1 { return __syscall(93, 50, 0, 0, 0, 0, 0) } 186 if nx_phase_is_valid(99) != 0 { return __syscall(93, 51, 0, 0, 0, 0, 0) } 187 188 return 0 189}