nx_wasm_breeders.nx source
↩ module page · 783 lines · 36197 B
1// nx_wasm_breeders.nx -- NISHI BREEDERS, SOVEREIGN. The whole game -- world, characters, animation,
2// genetics, combat -- is NishiLang emitting pixels into a packed-RGB framebuffer. Nothing outside this
3// file draws anything. The browser's only job is to copy the framebuffer to a canvas and hand back key
4// codes; that shim is the single interop boundary and it holds no game logic.
5//
6// BASE-RELATIVE by construction: every address is `base + offset`, so the identical code runs natively
7// (base = mmap) and in wasm (base = 0). The native run renders the same frames to PNG, which is how the
8// wasm build is verified without a browser -- the established "verify wasm by native PNG" loop.
9//
10// Genetics are the real thing, ported from nx_game_genetics.nx: diploid alleles, Mendel's segregation
11// and independent assortment, incomplete dominance on coat, codominant ABO affinity, recessive wings.
12// license_tier: ORIGINAL
13import "nx_syscalls.nx"
14import "nx_input_abstract.nx"
15const O_MAGIC_374761393: i64 = 374761393
16const O_MAGIC_668265263: i64 = 668265263
17const O_MAGIC_1442695041: i64 = 1442695041
18const O_MAGIC_4611686018427387903: i64 = 4611686018427387903
19const O_MAGIC_1274126177: i64 = 1274126177
20const O_MAGIC_1024: i64 = 1024
21const O_MAGIC_3072: i64 = 3072
22const O_MAGIC_1048576: i64 = 1048576
23const O_MAGIC_88172645463325252: i64 = 88172645463325252
24const O_MAGIC_99999: i64 = 99999
25const O_MAGIC_2654435761: i64 = 2654435761
26const O_MAGIC_1013904223: i64 = 1013904223
27const O_MAGIC_12345: i64 = 12345
28const O_MAGIC_7919: i64 = 7919
29const O_MAGIC_30000: i64 = 30000
30const O_MAGIC_1600: i64 = 1600
31const O_MAGIC_999999999: i64 = 999999999
32const O_MAGIC_3000: i64 = 3000
33const O_MAGIC_1400: i64 = 1400
34const O_MAGIC_9973: i64 = 9973
35const O_MAGIC_20260727: i64 = 20260727
36
37const W: i64 = 480
38const H: i64 = 300
39const O_FB: i64 = 0 // framebuffer W*H i64 packed R|G<<8|B<<16
40const O_ST: i64 = W*H*8 // state block
41const O_WLD: i64 = O_ST + 64*8 // tile map MW*MH i64
42const MW: i64 = 40
43const MH: i64 = 26
44const TS: i64 = 32
45const O_WILD: i64 = O_WLD + MW*MH*8 // wilds: 6 rows x 12 i64
46const NWILD: i64 = 6
47const WSTR: i64 = 12
48const O_PARTY: i64 = O_WILD + NWILD*WSTR*8 // party: 8 rows x 6 i64
49const PSTR: i64 = 6
50const O_INPUT: i64 = O_PARTY + 8*PSTR*8 // nx_input_abstract arena (64 words == IA_WORDS, gate-checked)
51const WORDS: i64 = O_INPUT/8 + 64 + 64
52
53// state slots
54const S_PX:i64=0; const S_PY:i64=1; const S_T:i64=2; const S_HP:i64=3; const S_NP:i64=4
55const S_SEED:i64=5; const S_OVER:i64=6; const S_FACE:i64=7; const S_WALK:i64=8
56const S_MSG:i64=9; const S_MSGT:i64=10; const S_BORN:i64=11; const S_TGT:i64=12
57const S_SHAKE:i64=13; const S_FLASH:i64=14; const S_CHARM:i64=15
58// wild slots
59const V_X:i64=0; const V_Y:i64=1; const V_G:i64=2; const V_POLY:i64=3; const V_HP:i64=4
60const V_ALIVE:i64=5; const V_HX:i64=6; const V_HY:i64=7; const V_LVL:i64=8; const V_FACE:i64=9
61const V_WALK:i64=10; const V_PH:i64=11
62// tiles
63const T_GRASS:i64=0; const T_TREE:i64=1; const T_WATER:i64=2; const T_ROCK:i64=3; const T_DEN:i64=4
64
65func st(base: i64) -> *i64 { return (base + O_ST) as *i64 }
66func fb(base: i64) -> *i64 { return (base + O_FB) as *i64 }
67func wld(base: i64) -> *i64 { return (base + O_WLD) as *i64 }
68func wildp(base: i64, i: i64) -> *i64 { return (base + O_WILD + i*WSTR*8) as *i64 }
69func party(base: i64, i: i64) -> *i64 { return (base + O_PARTY + i*PSTR*8) as *i64 }
70func inp(base: i64) -> *i64 { return (base + O_INPUT) as *i64 }
71
72// ---------- integer helpers (no float anywhere: identical result on x86 and wasm) ----------
73func iabs(v: i64) -> i64 { if v < 0 { return 0 - v } return v }
74func clamp(v: i64, lo: i64, hi: i64) -> i64 { if v<lo {return lo} if v>hi {return hi} return v }
75func imin(a: i64, b: i64) -> i64 { if a<b {return a} return b }
76func isqrt(n: i64) -> i64 {
77 if n <= 0 { return 0 }
78 var x: i64 = n
79 var y: i64 = (x+1)/2
80 var g: i64 = 0
81 while g < 32 { if y < x { x = y; y = (x + n/x)/2 } g = g + 1 }
82 return x
83}
84// hash -> 0..1023
85func hsh(x: i64, y: i64, s: i64) -> i64 {
86 var n: i64 = x*O_MAGIC_374761393 + y*O_MAGIC_668265263 + s*O_MAGIC_1442695041
87 n = n & O_MAGIC_4611686018427387903
88 n = (n ^ (n >> 13)) * O_MAGIC_1274126177
89 n = n & O_MAGIC_4611686018427387903
90 return (n ^ (n >> 16)) % O_MAGIC_1024
91}
92// smooth value noise, fixed point 0..1023
93func vnz(x: i64, y: i64, s: i64, sc: i64) -> i64 {
94 let ix: i64 = x/sc
95 let iy: i64 = y/sc
96 let tx: i64 = ((x % sc)+sc)%sc * O_MAGIC_1024 / sc
97 let ty: i64 = ((y % sc)+sc)%sc * O_MAGIC_1024 / sc
98 let sx: i64 = tx*tx*(O_MAGIC_3072-2*tx)/O_MAGIC_1048576/O_MAGIC_1024*O_MAGIC_1024/O_MAGIC_1024
99 let n00: i64 = hsh(ix,iy,s)
100 let n10: i64 = hsh(ix+1,iy,s)
101 let n01: i64 = hsh(ix,iy+1,s)
102 let n11: i64 = hsh(ix+1,iy+1,s)
103 let a: i64 = n00 + (n10-n00)*tx/O_MAGIC_1024
104 let b: i64 = n01 + (n11-n01)*tx/O_MAGIC_1024
105 return a + (b-a)*ty/O_MAGIC_1024
106}
107
108// ---------- genetics (Mendel, ported exactly) ----------
109const G_HORN:i64=0; const G_WING:i64=1; const G_COAT:i64=2; const G_ELEM:i64=3; const G_TAIL:i64=4
110func al0(g: i64, t: i64) -> i64 { return (g >> (t*4)) & 3 }
111func al1(g: i64, t: i64) -> i64 { return (g >> (t*4+2)) & 3 }
112func gput(g: i64, t: i64, a: i64, b: i64) -> i64 {
113 var v: i64 = g
114 var m: i64 = 15 << (t*4)
115 v = v - (v & m)
116 v = v | ((a & 3) << (t*4))
117 v = v | ((b & 3) << (t*4+2))
118 return v
119}
120func phen(g: i64, t: i64) -> i64 {
121 let a: i64 = al0(g,t)
122 let b: i64 = al1(g,t)
123 if t == G_COAT { return a + b } // incomplete dominance: white/rose/crimson
124 if t == G_ELEM { // codominant ABO
125 var hA: i64 = 0
126 var hB: i64 = 0
127 if a==1 { hA=1 } if b==1 { hA=1 }
128 if a==2 { hB=1 } if b==2 { hB=1 }
129 if hA==1 { if hB==1 { return 3 } return 1 }
130 if hB==1 { return 2 }
131 return 0
132 }
133 if a==1 { return 1 } if b==1 { return 1 }
134 return 0
135}
136func rng(base: i64) -> i64 { // xorshift on the state seed
137 let s: *i64 = st(base)
138 var x: i64 = s[S_SEED]
139 x = x ^ (x << 13); x = x ^ (x >> 7); x = x ^ (x << 17)
140 x = x & O_MAGIC_4611686018427387903
141 if x == 0 { x = O_MAGIC_88172645463325252 }
142 s[S_SEED] = x
143 return x
144}
145func gamete(base: i64, g: i64, t: i64) -> i64 { // Mendel's 1st law
146 if (rng(base) & 1) == 1 { return al1(g,t) }
147 return al0(g,t)
148}
149func gcross(base: i64, ga: i64, gb: i64) -> i64 { // Mendel's 2nd law
150 var c: i64 = 0
151 var t: i64 = 0
152 while t < 5 { c = gput(c, t, gamete(base,ga,t), gamete(base,gb,t)); t = t + 1 }
153 return c
154}
155func founder_g(seed: i64) -> i64 {
156 var g: i64 = 0
157 var t: i64 = 0
158 while t < 5 {
159 var hi: i64 = 2
160 if t == G_ELEM { hi = 3 }
161 g = gput(g, t, hsh(seed,t,7)%hi, hsh(seed,t,11)%hi)
162 t = t + 1
163 }
164 g = gput(g, G_COAT, 0, 1) // founders heterozygous at the prize loci so
165 g = gput(g, G_WING, 1, 0) // 1:2:1 keeps segregating and ww stays reachable
166 return g
167}
168// palette from genome
169func hair_r(g: i64) -> i64 { let c: i64=phen(g,G_COAT); if c==2 {return 196} if c==1 {return 236} return 240 }
170func hair_g(g: i64) -> i64 { let c: i64=phen(g,G_COAT); if c==2 {return 44} if c==1 {return 152} return 236 }
171func hair_b(g: i64) -> i64 { let c: i64=phen(g,G_COAT); if c==2 {return 62} if c==1 {return 176} return 244 }
172func eye_r(g: i64) -> i64 { let e: i64=phen(g,G_ELEM); if e==1 {return 255} if e==2 {return 92} if e==3 {return 214} return 130 }
173func eye_g(g: i64) -> i64 { let e: i64=phen(g,G_ELEM); if e==1 {return 152} if e==2 {return 172} if e==3 {return 128} return 196 }
174func eye_b(g: i64) -> i64 { let e: i64=phen(g,G_ELEM); if e==1 {return 64} if e==2 {return 255} if e==3 {return 255} return 180 }
175
176// ---------- raster primitives (every pixel this game draws goes through these) ----------
177func pack(r: i64, g: i64, b: i64) -> i64 {
178 return (clamp(r,0,255)) | (clamp(g,0,255) << 8) | (clamp(b,0,255) << 16)
179}
180func px(base: i64, x: i64, y: i64, c: i64) -> i64 {
181 if x<0 {return 0} if y<0 {return 0} if x>=W {return 0} if y>=H {return 0}
182 let f: *i64 = fb(base); f[y*W+x] = c; return 0
183}
184func rect(base: i64, x0: i64, y0: i64, x1: i64, y1: i64, c: i64) -> i64 {
185 var y: i64 = clamp(y0,0,H-1)
186 let ye: i64 = clamp(y1,0,H-1)
187 let xs: i64 = clamp(x0,0,W-1)
188 let xe: i64 = clamp(x1,0,W-1)
189 let f: *i64 = fb(base)
190 while y <= ye { var x: i64 = xs; while x <= xe { f[y*W+x]=c; x=x+1 } y=y+1 }
191 return 0
192}
193// shaded ellipse: light from upper-left, so bodies read as volumes not flat cut-outs
194func ell(base: i64, cx: i64, cy: i64, rx: i64, ry: i64, r: i64, g: i64, b: i64) -> i64 {
195 if rx<=0 {return 0} if ry<=0 {return 0}
196 var y: i64 = 0-ry
197 while y <= ry {
198 var x: i64 = 0-rx
199 while x <= rx {
200 if x*x*1000/(rx*rx) + y*y*1000/(ry*ry) <= 1000 {
201 var lit: i64 = 100 + (0-x)*34/rx + (0-y)*34/ry
202 lit = clamp(lit,62,150)
203 px(base, cx+x, cy+y, pack(r*lit/100, g*lit/100, b*lit/100))
204 }
205 x = x + 1
206 }
207 y = y + 1
208 }
209 return 0
210}
211// sweep a tapered limb: overlapping discs from (x0,y0) to (x1,y1), radius r0 -> r1. One continuous
212// mass, so a bent knee reads as a bend instead of a seam between two shapes.
213func limb(base: i64, x0: i64, y0: i64, x1: i64, y1: i64, r0: i64, r1: i64, r: i64, g: i64, b: i64) -> i64 {
214 var i: i64 = 0
215 let n: i64 = 14
216 while i <= n {
217 let px2: i64 = x0 + (x1-x0)*i/n
218 let py2: i64 = y0 + (y1-y0)*i/n
219 let rr: i64 = r0 + (r1-r0)*i/n
220 ell(base, px2, py2, rr, rr, r, g, b)
221 i = i + 1
222 }
223 return 0
224}
225func tri(base: i64, x0:i64,y0:i64,x1:i64,y1:i64,x2:i64,y2:i64, c: i64) -> i64 {
226 var ylo: i64 = y0; if y1<ylo {ylo=y1} if y2<ylo {ylo=y2}
227 var yhi: i64 = y0; if y1>yhi {yhi=y1} if y2>yhi {yhi=y2}
228 var y: i64 = clamp(ylo,0,H-1)
229 let ye: i64 = clamp(yhi,0,H-1)
230 while y <= ye {
231 var xlo: i64 = O_MAGIC_99999
232 var xhi: i64 = 0-O_MAGIC_99999
233 // edge scan
234 if (y0<=y) != (y1<=y) { let xa: i64 = x0 + (x1-x0)*(y-y0)/(y1-y0+1); if xa<xlo {xlo=xa} if xa>xhi {xhi=xa} }
235 if (y1<=y) != (y2<=y) { let xb: i64 = x1 + (x2-x1)*(y-y1)/(y2-y1+1); if xb<xlo {xlo=xb} if xb>xhi {xhi=xb} }
236 if (y2<=y) != (y0<=y) { let xc: i64 = x2 + (x0-x2)*(y-y2)/(y0-y2+1); if xc<xlo {xlo=xc} if xc>xhi {xhi=xc} }
237 if xhi >= xlo { var x: i64 = clamp(xlo,0,W-1); let xe: i64 = clamp(xhi,0,W-1)
238 while x <= xe { px(base,x,y,c); x=x+1 } }
239 y = y + 1
240 }
241 return 0
242}
243
244// ---------- world ----------
245func tile(base: i64, tx: i64, ty: i64) -> i64 {
246 if tx<0 {return T_ROCK} if ty<0 {return T_ROCK}
247 if tx>=MW {return T_ROCK} if ty>=MH {return T_ROCK}
248 let m: *i64 = wld(base); return m[ty*MW+tx]
249}
250func solidt(t: i64) -> i64 { if t==T_TREE {return 1} if t==T_WATER {return 1} if t==T_ROCK {return 1} return 0 }
251func blocked(base: i64, wx: i64, wy: i64) -> i64 {
252 let r: i64 = 8
253 if solidt(tile(base,(wx-r)/TS,(wy-r)/TS))==1 {return 1}
254 if solidt(tile(base,(wx+r)/TS,(wy-r)/TS))==1 {return 1}
255 if solidt(tile(base,(wx-r)/TS,(wy+r)/TS))==1 {return 1}
256 if solidt(tile(base,(wx+r)/TS,(wy+r)/TS))==1 {return 1}
257 return 0
258}
259func genworld(base: i64, seed: i64) -> i64 {
260 let m: *i64 = wld(base)
261 var y: i64 = 0
262 while y < MH {
263 var x: i64 = 0
264 while x < MW {
265 let e: i64 = vnz(x*16,y*16,seed,144)*55/100 + vnz(x*16,y*16,seed+31,72)*30/100 + vnz(x*16,y*16,seed+61,32)*15/100
266 // ★forest density sampled on an ANISOTROPIC lattice with a second decorrelating octave.
267 // Sampling x and y with the SAME stride/scale made the lattice align and the woodland came
268 // out in vertical COLUMNS -- visible in the render, invisible to the tile-mix gate (the
269 // ratios were correct, the ARRANGEMENT was not). A mix can be right and still look wrong.
270 let f: i64 = vnz(x*16,y*23,seed+97,52)*60/100 + vnz(x*29,y*11,seed+131,31)*40/100
271 var t: i64 = T_GRASS
272 if e < 340 { t = T_WATER }
273 if e >= 340 { if e > 800 { t = T_ROCK } }
274 if t == T_GRASS { if f > 600 { t = T_TREE } }
275 if x<1 { t=T_ROCK } if y<1 { t=T_ROCK }
276 if x>=MW-1 { t=T_ROCK } if y>=MH-1 { t=T_ROCK }
277 m[y*MW+x] = t
278 x = x + 1
279 }
280 y = y + 1
281 }
282 var cy: i64 = 2
283 while cy < 8 { var cx: i64 = 2; while cx < 9 { m[cy*MW+cx]=T_GRASS; cx=cx+1 } cy=cy+1 }
284 m[4*MW+5] = T_DEN
285 return 0
286}
287
288// ---------- CHARACTER: anime proportions, drawn entirely in NishiLang ----------
289// head = 1/5 of stature (the anime canon in nx_scene_contract); widths are the measured fractions
290// biacromial .23 / waist .15 / hip .19 / thigh .10 of stature.
291func drawgirl(base: i64, cx: i64, cy: i64, h: i64, g: i64, t: i64, face: i64, walk: i64) -> i64 {
292 let HD: i64 = h*205/1000
293 let sr: i64 = 232 - (g%3)*8
294 let sg2: i64 = 196 - (g%5)*6
295 let sb: i64 = 178 - (g%7)*4
296 let hr: i64 = hair_r(g)
297 let hg: i64 = hair_g(g)
298 let hb: i64 = hair_b(g)
299 let er: i64 = eye_r(g)
300 let eg: i64 = eye_g(g)
301 let eb: i64 = eye_b(g)
302 let shW: i64 = h*115/1000
303 let wsW: i64 = h*75/1000
304 let hpW: i64 = h*105/1000
305 let shY: i64 = cy - h*700/1000
306 let wsY: i64 = cy - h*500/1000
307 let hpY: i64 = cy - h*420/1000
308 let hdY: i64 = cy - h*855/1000
309 // walk cycle: legs swing, body bobs -- integer sine via a small table
310 var sw: i64 = 0
311 if walk == 1 { sw = ((t*11) % 62) - 31 } // triangle wave, cheap and deterministic
312 let bob: i64 = 0
313 // shadow
314 ell(base, cx, cy, h*115/1000, h*28/1000, 12, 12, 20)
315 // wings (recessive)
316 if phen(g,G_WING) == 0 {
317 ell(base, cx - h*90/1000, cy - h*620/1000, h*48/1000, h*90/1000, 206, 214, 250)
318 ell(base, cx + h*90/1000, cy - h*620/1000, h*48/1000, h*90/1000, 206, 214, 250)
319 }
320 // ★WALK CYCLE: thigh + calf with a knee, striding out of phase. A single straight ellipse per leg
321 // could not read as walking however fast it slid; the knee is what makes a stride legible.
322 let legTop: i64 = cy - h*400/1000
323 let kneeY: i64 = cy - h*200/1000
324 var s1: i64 = sw*h/700
325 var s2: i64 = 0 - s1
326 // ★ONE CONTINUOUS TAPERED LIMB PER LEG. Drawing thigh and calf as two ellipses with different
327 // shading left a visible SEAM, and at 92px the far leg + near leg + skirt hem + feet stacked into
328 // horizontal BANDS -- the figure stopped reading as a body. A limb is swept as overlapping discs
329 // from hip to ankle, radius tapering, so the knee is a BEND and never a joint line.
330 // ⚠STANCE: hpW/4 apart with r=h*30/1000 put the two limbs 2.4px apart with 2.8px radii at 92px --
331 // they overlapped into ONE COLUMN. Legs need a visible gap: widen the stance, slim the limb.
332 let stance: i64 = hpW*48/100
333 limb(base, cx - stance, legTop, cx - stance + s1, cy - h*14/1000, h*24/1000, h*13/1000,
334 sr*84/100, sg2*84/100, sb*84/100)
335 limb(base, cx + stance, legTop, cx + stance + s2, cy - h*14/1000, h*25/1000, h*14/1000, sr, sg2, sb)
336 ell(base, cx - stance + s1, cy - h*10/1000, h*17/1000, h*10/1000, 58, 44, 62)
337 ell(base, cx + stance + s2, cy - h*10/1000, h*18/1000, h*10/1000, 62, 48, 68)
338 // torso: shoulders -> waist -> hips
339 ell(base, cx, shY + h*60/1000, shW, h*90/1000, sr, sg2, sb)
340 ell(base, cx, wsY, wsW, h*70/1000, sr, sg2, sb)
341 ell(base, cx, hpY, hpW, h*72/1000, sr, sg2, sb)
342 // outfit: bodice + skirt, tinted off the genome so each girl is dressed and distinct
343 let oc_r: i64 = 60 + (g*13)%90
344 let oc_g: i64 = 52 + (g*29)%80
345 let oc_b: i64 = 110 + (g*47)%80
346 ell(base, cx, shY + h*80/1000, shW*94/100, h*80/1000, oc_r, oc_g, oc_b)
347 ell(base, cx, wsY + h*10/1000, wsW*104/100, h*54/1000, oc_r, oc_g, oc_b)
348 ell(base, cx, hpY + h*36/1000, hpW*122/100, h*54/1000, oc_r*8/10, oc_g*8/10, oc_b*8/10)
349 // arms
350 ell(base, cx - shW - h*8/1000 + s2/2, shY + h*100/1000, h*16/1000, h*88/1000, sr*9/10, sg2*9/10, sb*9/10)
351 ell(base, cx + shW + h*8/1000 + s1/2, shY + h*100/1000, h*16/1000, h*88/1000, sr, sg2, sb)
352 // neck
353 ell(base, cx, hdY + HD*52/100, h*17/1000, h*26/1000, sr*88/100, sg2*88/100, sb*88/100)
354 // ★hair BACK MASS, silhouette varies by hairstyle allele so companions are distinguishable at a
355 // glance rather than being one shape in different colours.
356 let hs: i64 = g % 4
357 if hs == 0 { // long fall
358 ell(base, cx, hdY + HD*30/100, HD*66/100, HD*96/100, hr*70/100, hg*70/100, hb*70/100)
359 }
360 if hs == 1 { // twin tails
361 ell(base, cx, hdY + HD*6/100, HD*58/100, HD*70/100, hr*70/100, hg*70/100, hb*70/100)
362 ell(base, cx - HD*72/100, hdY + HD*46/100, HD*22/100, HD*54/100, hr*76/100, hg*76/100, hb*76/100)
363 ell(base, cx + HD*72/100, hdY + HD*46/100, HD*22/100, HD*54/100, hr*76/100, hg*76/100, hb*76/100)
364 }
365 if hs == 2 { // bob
366 ell(base, cx, hdY + HD*12/100, HD*62/100, HD*62/100, hr*70/100, hg*70/100, hb*70/100)
367 }
368 if hs == 3 { // voluminous
369 ell(base, cx, hdY + HD*8/100, HD*74/100, HD*76/100, hr*68/100, hg*68/100, hb*68/100)
370 ell(base, cx - HD*54/100, hdY + HD*30/100, HD*26/100, HD*36/100, hr*74/100, hg*74/100, hb*74/100)
371 ell(base, cx + HD*54/100, hdY + HD*30/100, HD*26/100, HD*36/100, hr*74/100, hg*74/100, hb*74/100)
372 }
373 // head: cranium + a narrower jaw below it, so the face tapers to a chin instead of being a ball
374 ell(base, cx, hdY - HD*6/100, HD*48/100, HD*46/100, sr, sg2, sb)
375 ell(base, cx, hdY + HD*26/100, HD*34/100, HD*30/100, sr, sg2, sb)
376 // horns (simple dominance)
377 if phen(g,G_HORN) == 1 {
378 tri(base, cx-HD*30/100, hdY-HD*30/100, cx-HD*14/100, hdY-HD*95/100, cx-HD*2/100, hdY-HD*30/100, pack(238,230,206))
379 tri(base, cx+HD*30/100, hdY-HD*30/100, cx+HD*14/100, hdY-HD*95/100, cx+HD*2/100, hdY-HD*30/100, pack(238,230,206))
380 }
381 // eyes: sclera, iris, pupil, highlight -- large, anime
382 let ew: i64 = HD*19/100
383 let eh: i64 = HD*23/100
384 ell(base, cx-HD*24/100, hdY+HD*8/100, ew, eh, 253, 251, 255)
385 ell(base, cx+HD*24/100, hdY+HD*8/100, ew, eh, 253, 251, 255)
386 ell(base, cx-HD*24/100, hdY+HD*10/100, ew*76/100, eh*78/100, er, eg, eb)
387 ell(base, cx+HD*24/100, hdY+HD*10/100, ew*76/100, eh*78/100, er, eg, eb)
388 ell(base, cx-HD*24/100, hdY+HD*12/100, ew*34/100, eh*42/100, 24, 16, 30)
389 ell(base, cx+HD*24/100, hdY+HD*12/100, ew*34/100, eh*42/100, 24, 16, 30)
390 px(base, cx-HD*30/100, hdY+HD*2/100, pack(255,255,255))
391 px(base, cx+HD*18/100, hdY+HD*2/100, pack(255,255,255))
392 ell(base, cx-HD*30/100, hdY+HD*1/100, ew*24/100, eh*22/100, 255,255,255)
393 ell(base, cx+HD*18/100, hdY+HD*1/100, ew*24/100, eh*22/100, 255,255,255)
394 // upper lash line: the single strongest cue that reads a face at small scale
395 rect(base, cx-HD*24/100-ew, hdY+HD*8/100-eh, cx-HD*24/100+ew, hdY+HD*8/100-eh+1, pack(42,24,44))
396 rect(base, cx+HD*24/100-ew, hdY+HD*8/100-eh, cx+HD*24/100+ew, hdY+HD*8/100-eh+1, pack(42,24,44))
397 // hair front fringe
398 ell(base, cx, hdY - HD*36/100, HD*54/100, HD*26/100, hr, hg, hb)
399 ell(base, cx - HD*46/100, hdY - HD*4/100, HD*16/100, HD*40/100, hr, hg, hb)
400 ell(base, cx + HD*46/100, hdY - HD*4/100, HD*16/100, HD*40/100, hr, hg, hb)
401 // affinity marks (codominant: AB shows BOTH)
402 let e2: i64 = phen(g,G_ELEM)
403 if e2 == 1 { ell(base, cx-shW*55/100, shY+h*20/1000, h*10/1000, h*10/1000, 255,150,60) }
404 if e2 == 2 { ell(base, cx+shW*55/100, shY+h*20/1000, h*10/1000, h*10/1000, 90,180,255) }
405 if e2 == 3 { ell(base, cx-shW*55/100, shY+h*20/1000, h*10/1000, h*10/1000, 255,150,60)
406 ell(base, cx+shW*55/100, shY+h*20/1000, h*10/1000, h*10/1000, 90,180,255) }
407 return 0
408}
409
410// ---------- init / tick / render : the exported game ----------
411func init_impl(base: i64, seed: i64) -> i64 {
412 let s: *i64 = st(base)
413 var k: i64 = 0
414 while k < 64 { s[k] = 0; k = k + 1 }
415 s[S_SEED] = seed*O_MAGIC_2654435761 + O_MAGIC_1013904223
416 if s[S_SEED] == 0 { s[S_SEED] = O_MAGIC_12345 }
417 ia_init(inp(base))
418 s[S_PX] = 5*TS + TS/2
419 s[S_PY] = 6*TS + TS/2
420 s[S_HP] = 100
421 s[S_FACE] = 1
422 s[S_CHARM] = 3
423 genworld(base, seed)
424 var i: i64 = 0
425 while i < NWILD {
426 let v: *i64 = wildp(base, i)
427 // place on open ground, away from the den
428 var tries: i64 = 0
429 var wx: i64 = 0
430 var wy: i64 = 0
431 var ok: i64 = 0
432 while tries < 200 {
433 if ok == 0 {
434 wx = (2 + hsh(i,tries,seed)%(MW-5))*TS + TS/2
435 wy = (2 + hsh(tries,i,seed+5)%(MH-5))*TS + TS/2
436 if blocked(base,wx,wy) == 0 { if iabs(wx-s[S_PX])+iabs(wy-s[S_PY]) > 5*TS { ok = 1 } }
437 }
438 tries = tries + 1
439 }
440 v[V_X]=wx; v[V_Y]=wy; v[V_HX]=wx; v[V_HY]=wy
441 v[V_G]=founder_g(seed+i*O_MAGIC_7919)
442 v[V_POLY]=hsh(i,i,seed)%256
443 v[V_HP]=100; v[V_ALIVE]=1; v[V_FACE]=1; v[V_WALK]=0
444 v[V_LVL]=2
445 if i > 0 { v[V_LVL] = 2 + hsh(i,3,seed)%4 }
446 v[V_PH] = hsh(i,7,seed)%628
447 i = i + 1
448 }
449 return 0
450}
451// dir: 0 up 1 down 2 left 3 right 4 interact 5 breed
452func tick_impl(base: i64, dir: i64) -> i64 {
453 let s: *i64 = st(base)
454 s[S_T] = s[S_T] + 1
455 if s[S_SHAKE] > 0 { s[S_SHAKE] = s[S_SHAKE] - 1 }
456 if s[S_FLASH] > 0 { s[S_FLASH] = s[S_FLASH] - 1 }
457 if s[S_MSGT] > 0 { s[S_MSGT] = s[S_MSGT] - 1 }
458 if s[S_OVER] != 0 { return s[S_OVER] }
459 // player move
460 var dx: i64 = 0
461 var dy: i64 = 0
462 if dir == 0 { dy = 0-1 }
463 if dir == 1 { dy = 1 }
464 if dir == 2 { dx = 0-1; s[S_FACE] = 0-1 }
465 if dir == 3 { dx = 1; s[S_FACE] = 1 }
466 s[S_WALK] = 0
467 if dx != 0 { if blocked(base, s[S_PX]+dx*6, s[S_PY]) == 0 { s[S_PX]=s[S_PX]+dx*6; s[S_WALK]=1 } }
468 if dy != 0 { if blocked(base, s[S_PX], s[S_PY]+dy*6) == 0 { s[S_PY]=s[S_PY]+dy*6; s[S_WALK]=1 } }
469 // interact
470 if dir == 4 { do_interact(base) }
471 if dir == 5 { do_breed(base) }
472 // wilds roam + notice
473 var i: i64 = 0
474 while i < NWILD {
475 let v: *i64 = wildp(base,i)
476 if v[V_ALIVE] == 1 {
477 let ddx: i64 = s[S_PX]-v[V_X]
478 let ddy: i64 = s[S_PY]-v[V_Y]
479 let d2: i64 = ddx*ddx + ddy*ddy
480 v[V_WALK] = 0
481 if d2 < O_MAGIC_30000 { if d2 > O_MAGIC_1600 { // notices you, closes in
482 var mx: i64 = 0
483 var my: i64 = 0
484 if ddx > 2 { mx = 3 } if ddx < 0-2 { mx = 0-3 }
485 if ddy > 2 { my = 3 } if ddy < 0-2 { my = 0-3 }
486 if blocked(base, v[V_X]+mx, v[V_Y]) == 0 { v[V_X]=v[V_X]+mx; v[V_WALK]=1 }
487 if blocked(base, v[V_X], v[V_Y]+my) == 0 { v[V_Y]=v[V_Y]+my; v[V_WALK]=1 }
488 if mx > 0 { v[V_FACE]=1 } if mx < 0 { v[V_FACE]=0-1 }
489 } }
490 if d2 >= O_MAGIC_30000 { // wanders near home
491 v[V_PH] = (v[V_PH] + 7) % 628
492 var wx2: i64 = v[V_HX] + ((v[V_PH]%157)-78)/2
493 var wy2: i64 = v[V_HY] + ((v[V_PH]%97)-48)/2
494 var mx2: i64 = 0
495 var my2: i64 = 0
496 if wx2 > v[V_X]+2 { mx2 = 2 } if wx2 < v[V_X]-2 { mx2 = 0-2 }
497 if wy2 > v[V_Y]+2 { my2 = 2 } if wy2 < v[V_Y]-2 { my2 = 0-2 }
498 if blocked(base, v[V_X]+mx2, v[V_Y]) == 0 { v[V_X]=v[V_X]+mx2; if mx2!=0 {v[V_WALK]=1} }
499 if blocked(base, v[V_X], v[V_Y]+my2) == 0 { v[V_Y]=v[V_Y]+my2; if my2!=0 {v[V_WALK]=1} }
500 }
501 }
502 i = i + 1
503 }
504 return s[S_OVER]
505}
506func nearest_wild(base: i64) -> i64 {
507 let s: *i64 = st(base)
508 var best: i64 = 0-1
509 var bd: i64 = O_MAGIC_999999999
510 var i: i64 = 0
511 while i < NWILD {
512 let v: *i64 = wildp(base,i)
513 if v[V_ALIVE] == 1 {
514 let dx: i64 = v[V_X]-s[S_PX]
515 let dy: i64 = v[V_Y]-s[S_PY]
516 let d: i64 = dx*dx+dy*dy
517 if d < bd { bd = d; best = i }
518 }
519 i = i + 1
520 }
521 if bd > O_MAGIC_3000 { return 0-1 }
522 return best
523}
524func do_interact(base: i64) -> i64 {
525 let s: *i64 = st(base)
526 let idx: i64 = nearest_wild(base)
527 if idx < 0 { s[S_MSG]=1; s[S_MSGT]=90; return 0 } // nothing here
528 let v: *i64 = wildp(base,idx)
529 // duel: her level bites, your party helps. Same shape as nx_breeder_arena.
530 var php: i64 = 62 + s[S_NP]*14
531 let pmax: i64 = php
532 var whp: i64 = v[V_LVL]*17 + v[V_HP]*45/100
533 var guard: i64 = 0
534 while guard < 60 {
535 if php > 0 { if whp > 0 {
536 whp = whp - (6 + (rng(base)%9))
537 if whp > 0 { php = php - (3 + v[V_LVL]*2 + (rng(base)%4)) }
538 } }
539 guard = guard + 1
540 }
541 s[S_SHAKE] = 6
542 if php <= 0 {
543 s[S_HP] = s[S_HP] - 22
544 s[S_FLASH] = 8
545 v[V_HP] = 100
546 s[S_MSG]=2; s[S_MSGT]=110
547 if s[S_HP] <= 0 { s[S_OVER] = 0-1 }
548 return 0
549 }
550 let margin: i64 = php*1000/pmax
551 var bar: i64 = 350
552 let bsel: i64 = v[V_G] % 6
553 if bsel==1 { bar=450 } if bsel==2 { bar=400 } if bsel==3 { bar=500 }
554 if bsel==4 { bar=300 } if bsel==5 { bar=400 }
555 v[V_HP] = clamp(v[V_HP]*margin/O_MAGIC_1400, 6, 100)
556 if margin >= bar {
557 v[V_ALIVE] = 0
558 if s[S_NP] < 8 {
559 let p: *i64 = party(base, s[S_NP])
560 p[0]=v[V_G]; p[1]=v[V_POLY]; p[2]=v[V_LVL]; p[3]=1
561 s[S_NP] = s[S_NP] + 1
562 }
563 s[S_MSG]=3; s[S_MSGT]=140
564 }
565 if margin < bar { s[S_MSG]=4; s[S_MSGT]=110 }
566 return 0
567}
568func do_breed(base: i64) -> i64 {
569 let s: *i64 = st(base)
570 if s[S_NP] < 2 { s[S_MSG]=5; s[S_MSGT]=100; return 0 }
571 // must stand at the den
572 let dx: i64 = iabs(s[S_PX] - (5*TS+TS/2))
573 let dy: i64 = iabs(s[S_PY] - (4*TS+TS/2))
574 if dx+dy > TS { s[S_MSG]=6; s[S_MSGT]=100; return 0 }
575 let a: *i64 = party(base,0)
576 let b: *i64 = party(base,1)
577 let kid: i64 = gcross(base, a[0], b[0])
578 if s[S_NP] < 8 {
579 let p: *i64 = party(base, s[S_NP])
580 p[0]=kid; p[1]=(a[1]+b[1])/2; p[2]=1; p[3]=1
581 s[S_NP] = s[S_NP] + 1
582 }
583 s[S_BORN] = s[S_BORN] + 1
584 s[S_MSG]=7; s[S_MSGT]=170
585 if phen(kid,G_COAT)==2 { s[S_OVER]=1 }
586 if phen(kid,G_WING)==0 { s[S_OVER]=1 }
587 return 0
588}
589
590// ---------- render: the entire frame, drawn in NishiLang ----------
591func render_impl(base: i64) -> i64 {
592 let s: *i64 = st(base)
593 // camera follows the player, clamped to the world
594 var camx: i64 = s[S_PX] - W/2
595 var camy: i64 = s[S_PY] - H/2
596 camx = clamp(camx, 0, MW*TS - W)
597 camy = clamp(camy, 0, MH*TS - H)
598 if s[S_SHAKE] > 0 { camx = camx + (s[S_SHAKE]%3) - 1; camy = camy + (s[S_SHAKE]%2) }
599 // terrain
600 var ty: i64 = camy/TS
601 while ty <= (camy+H)/TS {
602 var tx: i64 = camx/TS
603 while tx <= (camx+W)/TS {
604 let t: i64 = tile(base,tx,ty)
605 let sx: i64 = tx*TS - camx
606 let sy: i64 = ty*TS - camy
607 let n: i64 = vnz(tx*16,ty*16,s[S_SEED]%O_MAGIC_9973+29,96)
608 // ★per-pixel ground from continuous world-space noise. Flat per-tile fills made the map
609 // read as a checkerboard -- the tile grid was the first thing the eye caught.
610 if t != T_DEN {
611 var qy: i64 = 0
612 while qy < TS {
613 var qx: i64 = 0
614 while qx < TS {
615 let wx2: i64 = (tx*TS+qx)
616 let wy2: i64 = (ty*TS+qy)
617 let g2: i64 = vnz(wx2*4, wy2*4, s[S_SEED]%O_MAGIC_9973+29, 220)
618 var cc: i64 = 0
619 if t == T_WATER {
620 let wv: i64 = vnz(wx2*4+s[S_T]*3, wy2*4, 71, 90)
621 cc = pack(18+wv/40, 52+wv/22, 96+wv/16)
622 }
623 if t == T_ROCK { cc = pack(54+g2/26, 50+g2/28, 66+g2/26) }
624 if t == T_GRASS { cc = pack(26+g2/44, 50+g2/22, 32+g2/44) }
625 if t == T_TREE { cc = pack(22+g2/50, 44+g2/26, 28+g2/50) }
626 px(base, sx+qx, sy+qy, cc)
627 qx = qx + 1
628 }
629 qy = qy + 1
630 }
631 }
632 if t == T_DEN {
633 rect(base,sx,sy,sx+TS-1,sy+TS-1, pack(44,92,62))
634 rect(base,sx+3,sy+3,sx+TS-4,sy+4, pack(150,240,190))
635 rect(base,sx+3,sy+TS-5,sx+TS-4,sy+TS-4, pack(150,240,190))
636 }
637 tx = tx + 1
638 }
639 ty = ty + 1
640 }
641 // ★DEPTH PASS 1: trees whose base sits ABOVE the player draw BEHIND her.
642 // (Pass 2 after the actors draws the nearer trees in FRONT, so you walk into the treeline
643 // instead of sliding over it. One flat pass made every character float on top of the world.)
644 var ty2: i64 = camy/TS
645 while ty2 <= (camy+H)/TS {
646 var tx2: i64 = camx/TS
647 while tx2 <= (camx+W)/TS {
648 var draw_now: i64 = 0
649 if tile(base,tx2,ty2) == T_TREE { if ty2*TS + TS <= s[S_PY] { draw_now = 1 } }
650 if draw_now == 1 {
651 let jx: i64 = (hsh(tx2,ty2,83)%TS) - TS/2
652 let jy: i64 = (hsh(tx2,ty2,89)%(TS/2)) - TS/4
653 let sc: i64 = 70 + hsh(tx2,ty2,101)%50
654 let cx: i64 = tx2*TS - camx + TS/2 + jx/2
655 let cy: i64 = ty2*TS - camy + TS - 4 + jy/2
656 ell(base, cx, cy, TS*30/100*sc/100, TS*9/100*sc/100, 10,10,16)
657 rect(base, cx-2, cy-TS*34/100*sc/100, cx+2, cy, pack(72,50,32))
658 ell(base, cx, cy-TS*58/100*sc/100, TS*40/100*sc/100, TS*36/100*sc/100, 74, 140, 72)
659 }
660 tx2 = tx2 + 1
661 }
662 ty2 = ty2 + 1
663 }
664 // wilds
665 var i: i64 = 0
666 while i < NWILD {
667 let v: *i64 = wildp(base,i)
668 if v[V_ALIVE] == 1 {
669 let sx: i64 = v[V_X]-camx
670 let sy: i64 = v[V_Y]-camy
671 if sx > 0-60 { if sx < W+60 { if sy > 0-90 { if sy < H+90 {
672 drawgirl(base, sx, sy, 88, v[V_G], s[S_T], v[V_FACE], v[V_WALK])
673 // health pip when hurt
674 if v[V_HP] < 100 {
675 rect(base, sx-14, sy+4, sx+14, sy+6, pack(30,14,20))
676 rect(base, sx-14, sy+4, sx-14+28*v[V_HP]/100, sy+6, pack(230,110,130))
677 }
678 } } } }
679 }
680 i = i + 1
681 }
682 // party trailing behind you
683 var p: i64 = 0
684 while p < s[S_NP] {
685 if p < 3 {
686 let pp: *i64 = party(base,p)
687 let ox: i64 = s[S_PX]-camx - (p+1)*16*s[S_FACE]
688 drawgirl(base, ox, s[S_PY]-camy+4, 74, pp[0], s[S_T]+p*9, s[S_FACE], s[S_WALK])
689 }
690 p = p + 1
691 }
692 // player
693 drawgirl(base, s[S_PX]-camx, s[S_PY]-camy, 92, 0, s[S_T], s[S_FACE], s[S_WALK])
694 // ★DEPTH PASS 2: trees whose base is BELOW the player redraw OVER her -- she walks into the treeline
695 var fy3: i64 = camy/TS
696 while fy3 <= (camy+H)/TS {
697 var fx3: i64 = camx/TS
698 while fx3 <= (camx+W)/TS {
699 var front: i64 = 0
700 if tile(base,fx3,fy3) == T_TREE { if fy3*TS + TS > s[S_PY] { front = 1 } }
701 if front == 1 {
702 let jx3: i64 = (hsh(fx3,fy3,83)%TS) - TS/2
703 let jy3: i64 = (hsh(fx3,fy3,89)%(TS/2)) - TS/4
704 let sc3: i64 = 70 + hsh(fx3,fy3,101)%50
705 let cx3: i64 = fx3*TS - camx + TS/2 + jx3/2
706 let cy3: i64 = fy3*TS - camy + TS - 4 + jy3/2
707 ell(base, cx3, cy3, TS*30/100*sc3/100, TS*9/100*sc3/100, 10,10,16)
708 rect(base, cx3-2, cy3-TS*34/100*sc3/100, cx3+2, cy3, pack(72,50,32))
709 ell(base, cx3, cy3-TS*58/100*sc3/100, TS*40/100*sc3/100, TS*36/100*sc3/100, 74, 140, 72)
710 }
711 fx3 = fx3 + 1
712 }
713 fy3 = fy3 + 1
714 }
715 // hurt flash
716 if s[S_FLASH] > 0 {
717 var fy: i64 = 0
718 while fy < H { var fx2: i64 = 0
719 while fx2 < W {
720 let f2: *i64 = fb(base)
721 let c: i64 = f2[fy*W+fx2]
722 f2[fy*W+fx2] = pack((c&255)*7/10+70, ((c>>8)&255)*6/10, ((c>>16)&255)*6/10)
723 fx2 = fx2 + 3 }
724 fy = fy + 1 }
725 }
726 // HUD: vitality bar, party count, births
727 rect(base, 6, 6, 6+104, 6+10, pack(18,14,30))
728 rect(base, 7, 7, 7+clamp(s[S_HP],0,100), 6+9, pack(224,86,110))
729 var q: i64 = 0
730 while q < s[S_NP] { // one pip per bonded companion
731 rect(base, 122+q*9, 7, 122+q*9+6, 13, pack(255,200,110))
732 q = q + 1
733 }
734 var bq: i64 = 0
735 while bq < s[S_BORN] {
736 rect(base, 122+bq*9, 16, 122+bq*9+6, 20, pack(160,240,190))
737 bq = bq + 1
738 }
739 // banner on win/lose
740 if s[S_OVER] == 1 { rect(base, W/2-96, H/2-14, W/2+96, H/2+14, pack(24,70,44))
741 rect(base, W/2-92, H/2-10, W/2+92, H/2-8, pack(150,240,190)) }
742 if s[S_OVER] == 0-1 { rect(base, W/2-96, H/2-14, W/2+96, H/2+14, pack(70,20,30))
743 rect(base, W/2-92, H/2-10, W/2+92, H/2-8, pack(255,140,150)) }
744 return 0
745}
746
747// input-driven tick: ONE dir derived from the device-agnostic action state. One-shot actions come
748// from the PRESSED edge (interact cannot repeat-fire while held); movement comes from HELD, so the
749// old per-frame tick semantics are preserved exactly -- tick_impl itself is untouched and the gate's
750// golden checksums stay valid.
751func step_impl(base: i64) -> i64 {
752 let q: *i64 = inp(base)
753 let held: i64 = ia_held(q)
754 let pressed: i64 = ia_frame(q)
755 var dir: i64 = 9
756 if (held & IA_UP) != 0 { dir = 0 }
757 if (held & IA_DOWN) != 0 { dir = 1 }
758 if (held & IA_LEFT) != 0 { dir = 2 }
759 if (held & IA_RIGHT) != 0 { dir = 3 }
760 if (pressed & IA_A) != 0 { dir = 4 }
761 if (pressed & IA_B) != 0 { dir = 5 }
762 return tick_impl(base, dir)
763}
764
765// ---------- the exported wasm surface (base = 0 in linear memory) ----------
766func init() -> i64 { return init_impl(0, O_MAGIC_20260727) }
767func init_seed(sd: i64) -> i64 { return init_impl(0, sd) }
768func tick(dir: i64) -> i64 { return tick_impl(0, dir) }
769func render() -> i64 { return render_impl(0) }
770func fb_off() -> i64 { return O_FB }
771func ww() -> i64 { return W }
772func hh() -> i64 { return H }
773func hp() -> i64 { let s: *i64 = st(0); return s[S_HP] }
774func party_n() -> i64 { let s: *i64 = st(0); return s[S_NP] }
775func born() -> i64 { let s: *i64 = st(0); return s[S_BORN] }
776func over() -> i64 { let s: *i64 = st(0); return s[S_OVER] }
777func msg() -> i64 { let s: *i64 = st(0); if s[S_MSGT] > 0 { return s[S_MSG] } return 0 }
778// device doors (nx_input_abstract): the shim forwards RAW events; meaning lives sovereign-side.
779func key(code: i64, down: i64) -> i64 { return ia_key(inp(0), code, down) }
780func padb(btn: i64, down: i64) -> i64 { return ia_pad(inp(0), btn, down) }
781func paxis(idx: i64, milli: i64) -> i64 { return ia_axis(inp(0), idx, milli) }
782func touch(tx: i64, ty: i64, down: i64) -> i64 { return ia_touch(inp(0), tx, ty, W, H, down) }
783func step() -> i64 { return step_impl(0) }