pixel-daylight-switch-gsvegetable 1.3.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. package/README.ja.md +207 -0
  2. package/README.md +134 -360
  3. package/README.zh-CN.md +207 -0
  4. package/app.js +664 -267
  5. package/package.json +19 -4
package/app.js CHANGED
@@ -9,6 +9,13 @@
9
9
  */
10
10
  (function () {
11
11
  "use strict";
12
+ const reportDiagnostic = (title, detail) => {
13
+ try {
14
+ if (typeof window !== 'undefined' && window.__PDS_DIAG__ && typeof window.__PDS_DIAG__.show === 'function') {
15
+ window.__PDS_DIAG__.show(title, detail);
16
+ }
17
+ } catch (_) {}
18
+ };
12
19
  const CORE_W = 192;
13
20
  const CORE_H = 112;
14
21
  const HORIZON = 82;
@@ -96,6 +103,42 @@ const MATS = [
96
103
  ['MARK', I0, I0, C3, C3],
97
104
  ['ZZZ', U3, U3, U3, U3],
98
105
  ['WING', A2, A2, A1, I2],
106
+ ['SNOW', C3, A3, U2, I3],
107
+ ['SNOW_SH', U2, U1, U0, I2],
108
+ ['SNOW_DK', U1, U0, I3, I1],
109
+ ['SEA', U1, U0, I3, I1],
110
+ ['SEA_DK', U0, I3, I2, I0],
111
+ ['SEA_LT', U2, A1, U0, I2],
112
+ ['SEA_HI', U3, A3, U2, I3],
113
+ ['FOAM', C3, A3, U2, I3],
114
+ ['ROCK', U0, I3, I1, I0],
115
+ ['LANTERN', A1, A2, A3, A3],
116
+ ['LANTERN_HI', A2, A3, A3, A3],
117
+ ['BAND', A1, A1, I3, I1],
118
+ ['PLASTER', C2, A2, U1, I2],
119
+ ['PLASTER_SH', C1, A1, U0, I1],
120
+ ['CONCRETE', U2, U1, I3, I1],
121
+ ['CONCRETE_SH', U1, U0, I2, I0],
122
+ ['ASPHALT', U0, I3, I2, I1],
123
+ ['ASPHALT_LT', U1, U0, I3, I1],
124
+ ['ASPHALT_DK', I3, I2, I1, I0],
125
+ ['LINE', C3, A3, U2, I3],
126
+ ['GLASS_SKY', U2, A2, I3, I1],
127
+ ['GLASS_SKY_HI', U3, A3, U1, I2],
128
+ ['NOREN', I3, I2, I1, I0],
129
+ ['CAT', A2, A1, I3, I1],
130
+ ['CAT_DK', A0, A0, I2, I0],
131
+ ['AUR_HI', G3, G3, G3, G3],
132
+ ['AUR_MID', G2, G2, G2, G2],
133
+ ['AUR_LO', G1, G1, G1, G1],
134
+ ['AUR_ICE', U3, U3, U3, U3],
135
+ ['SAND_LT', C2, A2, U1, I3],
136
+ ['SOFA', G1, G0, I2, I0],
137
+ ['SOFA_LT', G2, G1, I3, I1],
138
+ ['SOFA_DK', G0, G0, I1, I0],
139
+ ['RUG', U1, U0, I3, I1],
140
+ ['RUG_LT', U2, U1, I3, I2],
141
+ ['SHALLOW', G3, A2, U1, I2],
99
142
  ];
100
143
  const M = Object.fromEntries(MATS.map((row, i) => [row[0], i]));
101
144
  const KEYS = [
@@ -148,7 +191,7 @@ function labTone(lab, L) {
148
191
  }
149
192
  return oklabToLinear(L, 0, 0).map((v) => Math.round(toGamma(clamp(v)) * 255));
150
193
  }
151
- const CHARS = { H: 'HAIR', S: 'SKIN', E: 'HAIR', T: 'SHIRT', D: 'SHIRT_DK', P: 'PANTS', B: 'BOOT', W: 'BOOK', K: 'BOOK_DK' };
194
+ const CHARS = { H: 'HAIR', S: 'SKIN', E: 'HAIR', T: 'SHIRT', D: 'SHIRT_DK', P: 'PANTS', B: 'BOOT', W: 'BOOK', K: 'BOOK_DK', C: 'CAT', Q: 'CAT_DK', L: 'LANTERN', F: 'FOAM' };
152
195
  const makeSprite = (rows) => { const list = []; rows.forEach((row, y) => { [...row].forEach((ch, x) => { if (ch !== '.') list.push([x, y, M[CHARS[ch]], ch === 'E']); }); }); return { list, w: rows[0].length, h: rows.length }; };
153
196
  const HEAD = ['..HHH..', '.HHHHH.', '.HHSSS.', '.HSSES.', '..SSSS.'];
154
197
  const HEAD_BLINK = ['..HHH..', '.HHHHH.', '.HHSSS.', '.HSSSS.', '..SSSS.'];
@@ -315,268 +358,568 @@ function mount(root) {
315
358
  rect(156, 87, 5, 2, M.STONE); rect(157, 86, 3, 1, M.STONE_LT); rect(124, 95, 4, 2, M.STONE); rect(125, 94, 2, 1, M.STONE_LT);
316
359
  };
317
360
 
318
- // Additional handcrafted pixel environments. Coordinates share the 192×112 world grid.
319
- const paintJapaneseStreet = () => {
320
- rect(0, 0, CORE_W, 74, 0);
321
- rect(0, 74, CORE_W, 8, M.STONE); rect(0, 82, CORE_W, 30, M.PATH_DK);
322
- for (let y=76;y<82;y+=2) rect(0,y,CORE_W,1,M.STONE_LT);
323
- // shopfronts and narrow Japanese buildings
324
- rect(0,24,53,51,M.WALL); rect(0,20,55,5,M.ROOF);
325
- rect(4,30,45,3,M.WOOD_DK); rect(5,34,43,23,M.GLASS);
326
- rect(4,58,46,17,M.WOOD_DK); rect(7,60,40,13,M.WOOD);
327
- for(let x=10;x<47;x+=8){rect(x,60,1,13,M.WOOD_DK);}
328
- rect(8,27,39,5,M.ROOF_HI); rect(8,36,39,2,M.WOOD_DK);
329
- // noren curtain
330
- for(let x=10;x<44;x+=7){rect(x,38,5,9,M.CLOUD);rect(x+1,45,3,2,M.CLOUD_SH);}
331
- rect(68,16,42,59,M.WALL_SH); rect(70,19,38,5,M.ROOF);
332
- for(let x=74;x<106;x+=12){rect(x,30,8,13,M.GLASS);rect(x+1,31,6,2,M.GLASS_HI);rect(x+3,30,1,13,M.WOOD_DK);}
333
- rect(72,51,34,22,M.WOOD_DK); rect(75,54,28,19,M.WOOD);
334
- rect(78,57,22,9,M.GLASS); rect(79,58,20,2,M.GLASS_HI);
335
- // right-side ramen shop
336
- rect(132,29,60,46,M.WALL); rect(130,24,62,6,M.ROOF);
337
- rect(135,34,54,4,M.WOOD_DK); rect(138,39,48,3,M.WOOD_LT);
338
- rect(139,44,44,19,M.GLASS); rect(141,46,40,14,M.WOOD_DK);
339
- rect(143,48,36,10,M.GLASS); rect(144,49,34,2,M.GLASS_HI);
340
- rect(135,63,54,12,M.WOOD_DK); rect(139,65,46,8,M.WOOD);
341
- // lanterns, signs, street poles and wires
342
- rect(15,18,2,8,M.WOOD_DK); rect(12,17,8,1,M.WOOD_DK); rect(13,18,6,6,M.LIT); rect(14,19,4,4,M.LIT_HI);
343
- rect(150,15,2,9,M.WOOD_DK); rect(146,14,10,1,M.WOOD_DK); rect(147,15,8,7,M.LIT); rect(148,16,6,4,M.LIT_HI);
344
- rect(117,10,2,70,M.WOOD_DK); rect(108,12,22,2,M.WOOD_DK); rect(111,14,1,5,M.WOOD_DK);
345
- for(let x=0;x<CORE_W;x+=2){ if(x%4===0) put(x,12+Math.round(Math.sin(x*.035)*2),M.WOOD_DK); }
346
- rect(118,78,2,8,M.WOOD_DK); rect(115,76,8,2,M.WOOD_LT);
347
- // road markings and drain
348
- for(let x=14;x<CORE_W;x+=36){rect(x,94,14,2,M.STONE_LT);rect(x+4,95,7,1,M.PATH);}
349
- rect(24,80,12,2,M.WOOD_DK); for(let x=26;x<35;x+=3) put(x,80,M.STONE_LT);
350
- // vending machine and bicycle silhouette
351
- rect(57,51,8,24,M.WOOD_DK); rect(58,52,6,18,M.CLOUD); rect(59,54,4,6,M.GLASS_HI); rect(59,63,4,2,M.LIT);
352
- for(let x of [84,101]){for(let a=0;a<360;a+=30){const rad=a*Math.PI/180;put(Math.round(x+4*Math.cos(rad)),Math.round(78+4*Math.sin(rad)),M.WOOD_DK);} }
353
- rect(88,75,10,1,M.WOOD_DK); rect(91,72,4,4,M.WOOD_DK); rect(95,72,4,1,M.WOOD_DK);
354
- // Second-floor balconies, tiled roof edges, shop sign typography and alley depth
355
- rect(2,13,49,5,M.WOOD_DK); rect(4,14,45,2,M.ROOF_LT);
356
- for(let x=6;x<49;x+=7){rect(x,8,4,5,M.WALL_SH);rect(x+1,9,2,3,M.GLASS);put(x+2,10,M.GLASS_HI);}
357
- rect(3,47,48,2,M.WOOD_DK); for(let x=5;x<50;x+=5) rect(x,48,1,5,M.WOOD_LT);
358
- rect(6,53,44,2,M.WOOD_DK); rect(10,24,34,7,M.WOOD_DK);
359
- for(let x=12;x<43;x+=4){put(x,26,M.LIT_HI);put(x+1,27,M.LIT);}
360
- // Ramen shop noren, menu board and noren tassels
361
- rect(143,30,43,7,M.WOOD_DK); rect(146,31,37,4,M.LIT_DK);
362
- for(let x=148;x<181;x+=6){rect(x,39,4,7,M.CLOUD);put(x+1,46,M.CLOUD_SH);}
363
- rect(184,38,5,18,M.WOOD_DK); for(let y=40;y<54;y+=3) put(186,y,M.LIT_HI);
364
- // Rooftop water tank, satellite dish and dense utility cables
365
- rect(82,7,13,7,M.STONE); rect(84,5,9,2,M.STONE_LT); rect(85,9,7,3,M.WOOD_DK);
366
- rect(101,9,2,5,M.WOOD_DK); rect(98,8,8,1,M.WOOD_DK); rect(100,6,4,2,M.STONE_LT);
367
- for(let x=0;x<CORE_W;x+=1){const sag=Math.round(10+Math.sin(x/CORE_W*Math.PI)*5);if(x%3===0)put(x,sag,M.WOOD_DK);if(x%11===0)put(x,sag+1,M.WOOD_DK);}
368
- // Street utility boxes, bollards, storm drain and manhole
369
- rect(4,76,8,8,M.WOOD_DK);rect(5,77,6,5,M.STONE);for(let y=78;y<82;y+=2)rect(6,y,4,1,M.WOOD_DK);
370
- for(let x=4;x<CORE_W;x+=48){rect(x,83,2,8,M.STONE);put(x,83,M.STONE_LT);}
371
- rect(148,91,16,5,M.WOOD_DK);rect(150,92,12,2,M.STONE);for(let x=151;x<162;x+=3)put(x,93,M.WOOD_DK);
372
- // More road perspective and wet-pavement glints
373
- for(let y=99;y<111;y+=4){for(let x=(y*3)%17;x<CORE_W;x+=29)rect(x,y,5+(y%3),1,M.STONE);}
374
- for(let x=18;x<CORE_W;x+=31){put(x,88,M.CLOUD_SH);put(x+1,89,M.CLOUD_SH);}
375
- // Planters and small street tree
376
- rect(26,69,13,5,M.WOOD_DK);rect(28,67,9,2,M.STONE);for(let x=29;x<37;x+=2){put(x,65-(x%3),M.LEAF2);put(x+1,67,M.LEAF1);}
377
- rect(187,64,5,11,M.WOOD_DK);rect(188,61,3,4,M.LEAF1);put(187,62,M.LEAF2);put(191,63,M.LEAF2);
378
- };
379
- const paintApartment = () => {
380
- rect(0,0,CORE_W,78,M.WALL); rect(0,78,CORE_W,34,M.PLANK);
381
- for(let y=4;y<76;y+=12) rect(0,y,CORE_W,1,M.WALL_SH);
382
- // large city window
383
- rect(12,12,68,52,M.WOOD_DK); rect(15,15,62,46,M.GLASS);
384
- rect(44,15,2,46,M.WOOD_DK); rect(15,37,62,2,M.WOOD_DK);
385
- // distant city skyline through glass
386
- rect(17,42,58,18,M.CLOUD_SH);
387
- rect(20,32,8,28,M.STONE); rect(31,25,10,35,M.STONE_LT); rect(49,35,9,25,M.STONE); rect(63,29,10,31,M.STONE_LT);
388
- for(let x=21;x<28;x+=3) for(let y=36;y<57;y+=6) put(x,y,M.LIT);
389
- for(let x=33;x<40;x+=3) for(let y=29;y<57;y+=6) put(x,y,M.LIT);
390
- for(let x=51;x<57;x+=3) for(let y=39;y<57;y+=6) put(x,y,M.LIT);
391
- for(let x=65;x<72;x+=3) for(let y=33;y<57;y+=6) put(x,y,M.LIT);
392
- // curtains
393
- rect(9,10,5,57,M.WOOD); rect(78,10,5,57,M.WOOD);
394
- for(let y=12;y<65;y+=4){put(10,y,M.WOOD_LT);put(80,y,M.WOOD_LT);}
395
- // sofa and coffee table
396
- rect(91,59,63,8,M.WOOD_DK); rect(94,54,57,12,M.SHIRT_DK); rect(97,51,50,10,M.SHIRT);
397
- rect(89,63,6,12,M.WOOD_DK); rect(151,63,6,12,M.WOOD_DK);
398
- rect(105,76,42,4,M.WOOD_DK); rect(108,80,3,8,M.WOOD); rect(141,80,3,8,M.WOOD);
399
- rect(112,72,27,4,M.WOOD_LT); rect(119,69,12,3,M.BOOK);
400
- // floor lamp, framed art and indoor plant
401
- rect(169,25,2,47,M.WOOD_DK); rect(164,21,12,5,M.LIT); rect(166,22,8,2,M.LIT_HI);
402
- rect(160,72,20,2,M.WOOD_DK); rect(162,68,16,4,M.WOOD);
403
- rect(23,69,19,2,M.WOOD_DK); rect(25,53,15,14,M.WOOD_DK); rect(27,55,11,10,M.CLOUD);
404
- rect(26,84,11,10,M.WOOD_DK); rect(24,92,15,3,M.WOOD);
405
- for(let i=0;i<5;i++){put(31+i*2,83-i*2,M.LEAF2);put(29-i,85-i*2,M.LEAF1);}
406
- // rug and small details
407
- rect(78,91,89,10,M.SHIRT_DK); rect(82,93,81,6,M.SHIRT);
408
- rect(12,103,168,1,M.WOOD_DK);
409
- // Architectural trim and ceiling coving
410
- rect(0,5,CORE_W,2,M.WOOD_DK);rect(0,7,CORE_W,1,M.WOOD_LT);
411
- rect(86,12,2,47,M.WOOD_DK);rect(88,12,1,47,M.WOOD_LT);
412
- // Gallery wall: framed print, clock and floating shelf
413
- rect(96,12,22,20,M.WOOD_DK);rect(98,14,18,16,M.CLOUD_SH);
414
- rect(100,16,14,12,M.HILL_FAR);rect(100,24,14,4,M.HILL_NEAR);
415
- rect(103,18,3,5,M.LEAF1);put(106,17,M.LEAF2);put(109,19,M.LEAF2);
416
- rect(126,16,12,12,M.WOOD_DK);rect(128,18,8,8,M.CLOUD);
417
- put(132,21,M.WOOD_DK);put(132,23,M.WOOD_DK);
418
- rect(94,34,48,2,M.WOOD_DK);rect(99,36,4,2,M.WOOD_LT);rect(132,36,7,2,M.WOOD_LT);
419
- // Media console and television
420
- rect(91,43,42,8,M.WOOD_DK);rect(94,45,36,5,M.WOOD);
421
- rect(103,29,25,14,M.WOOD_DK);rect(105,31,21,10,M.GLASS);rect(107,33,17,6,M.HILL_FAR);
422
- put(115,36,M.LIT_HI);rect(108,43,15,1,M.WOOD_LT);
423
- // Cushions, seams, throw blanket and side table on sofa
424
- rect(99,54,11,7,M.SHIRT_DK);rect(101,55,7,4,M.SHIRT);
425
- rect(134,54,12,7,M.SHIRT_DK);rect(136,55,8,4,M.SHIRT);
426
- for(let x=99;x<149;x+=7)put(x,64,M.SHIRT_DK);
427
- rect(154,60,12,12,M.WOOD_DK);rect(156,62,8,7,M.WOOD);
428
- rect(158,59,4,2,M.CLOUD);put(159,58,M.LIT_HI);
429
- // Bookshelf with books and a small speaker
430
- rect(4,28,19,45,M.WOOD_DK);rect(6,31,15,2,M.WOOD_LT);rect(6,48,15,2,M.WOOD_LT);
431
- for(let x=7;x<20;x+=4){rect(x,34,2,12,M.BOOK);rect(x+2,36,1,10,M.BOOK_DK);}
432
- rect(7,52,5,10,M.WOOD);rect(13,54,6,8,M.BOOK);
433
- rect(17,66,5,7,M.WOOD_DK);rect(18,67,3,4,M.GLASS);
434
- // Kitchen counter, backsplash and hanging pendant
435
- rect(4,78,53,3,M.WOOD_DK);rect(7,81,47,10,M.WOOD);
436
- rect(9,73,44,5,M.STONE);rect(12,74,12,3,M.CLOUD);
437
- rect(39,75,10,3,M.WOOD_DK);rect(42,71,4,4,M.GLASS);
438
- rect(34,10,2,9,M.WOOD_DK);rect(30,18,10,3,M.LIT);rect(32,19,6,2,M.LIT_HI);
439
- // Floorboards, rug border and woven rug pattern
440
- for(let y=84;y<111;y+=5)rect(0,y,CORE_W,1,M.WOOD_DK);
441
- for(let x=10;x<CORE_W;x+=24)rect(x,84,1,28,M.WOOD_DK);
442
- rect(78,89,89,2,M.WOOD_DK);rect(78,101,89,2,M.WOOD_DK);
443
- for(let x=86;x<161;x+=8){put(x,95,M.SHIRT_DK);put(x+3,97,M.SHIRT_DK);}
444
- // Plant leaves, pot highlights and lamp pull chain
445
- rect(26,94,11,2,M.BRICK);rect(28,96,7,3,M.BRICK_LT);
446
- for(let i=0;i<7;i++){put(30+i*2,82-i%3*2,M.LEAF3);put(28+i*2,86-i%4*2,M.LEAF2);}
447
- rect(170,27,1,28,M.WOOD_LT);put(171,55,M.WOOD_DK);
361
+
362
+ // ===== Scene toolkit: shared by the four non-countryside scenes =====
363
+ // Every painter fills the WHOLE world buffer (-ox..bw-ox, -oy..bh-oy), not just the 192x112 core,
364
+ // so wide windows never show raw sky at the edges. Dynamic things (lights, people, water, smoke) are
365
+ // redrawn each frame on top of the static base in drawSceneLive().
366
+ const hash2 = (a, b) => { let t = Math.imul(a | 0, 374761393) ^ Math.imul(b | 0, 668265263); t = Math.imul(t ^ (t >>> 13), 1274126177); return ((t ^ (t >>> 16)) >>> 0) / 4294967296; };
367
+ const dth = (x, y, t) => t * 16 > bayerAt(x, y);
368
+ const strip = (y0, y1, m) => { for (let y = y0; y < y1; y++) for (let x = -ox; x < bw - ox; x++) put(x, y, m); };
369
+ const disc = (cx, cy, r, m) => { for (let y = Math.floor(cy - r); y <= cy + r; y++) for (let x = Math.floor(cx - r); x <= cx + r; x++) if ((x - cx) * (x - cx) + (y - cy) * (y - cy) <= r * r + 0.3) put(x, y, m); };
370
+ const line = (x0, y0, x1, y1, m) => { const n = Math.max(Math.abs(x1 - x0), Math.abs(y1 - y0), 1); for (let i = 0; i <= n; i++) put(Math.round(mix(x0, x1, i / n)), Math.round(mix(y0, y1, i / n)), m); };
371
+ const sunPos = (h) => { if (h <= 5.2 || h >= 19) return null; const a = ((h - 6) / 12) * Math.PI; return { x: Math.round(96 - Math.cos(a) * 102), y: Math.round(HORIZON - 2 - Math.sin(a) * 70) }; };
372
+ const moonPos = (h) => { const mh = h >= 17.6 ? h : h + 24; if (mh <= 17.6 || mh >= 30.4) return null; const a = ((mh - 18) / 12) * Math.PI; return { x: Math.round(96 - Math.cos(a) * 102), y: Math.round(HORIZON - 2 - Math.sin(a) * 66) }; };
373
+ const nightF = (h) => clamp(smoothstep(18.2, 20.2, h) + (1 - smoothstep(4.6, 6.2, h)));
374
+ const lampAmt = (h) => (h >= 12 ? smoothstep(17.4, 19.0, h) : 1 - smoothstep(5.2, 6.8, h));
375
+ const winFrac = (h) => { if (h >= 12) return 0.9 * smoothstep(17.2, 20.4, h) * (1 - 0.35 * smoothstep(23, 24, h)); if (h < 5) return 0.58 - h * 0.1; return Math.max(0, (0.08 + 0.32 * smoothstep(5, 6.3, h)) * (1 - smoothstep(6.8, 8.3, h))); };
376
+ // windows that light up one by one as evening falls
377
+ const wins = [];
378
+ const addWin = (x, y, w, h, o = {}) => wins.push({ x, y, w, h, seed: o.seed !== undefined ? o.seed : hash2(x * 7 + 3, y * 13 + 1), mull: o.mull || 0, always: !!o.always, tiny: !!o.tiny });
379
+ const drawWins = (h, level, boost = 1) => {
380
+ for (const w of wins) {
381
+ const on = w.always ? lampAmt(h) > 0.4 : w.seed < level; if (!on) continue;
382
+ if (w.tiny) { put(w.x, w.y, M.LIT); emit(w.x, w.y, 70 * boost, 2); continue; }
383
+ const flick = calm ? 1 : 0.93 + 0.07 * Math.sin(anim * 2.1 + w.seed * 40);
384
+ for (let j = 0; j < w.h; j++) for (let i = 0; i < w.w; i++) {
385
+ const ring = i === 0 || j === 0 || i === w.w - 1 || j === w.h - 1; const low = j === w.h - 1 && w.h > 3;
386
+ let m = low ? M.LIT_DK : ring ? M.LIT : M.LIT_HI; if (w.mull && i === (w.w >> 1) && w.w > 4) m = M.WOOD_DK;
387
+ put(w.x + i, w.y + j, m);
388
+ if (!ring && ((i + j) & 1) === 0) emit(w.x + i, w.y + j, 62 * boost * flick, 2);
389
+ }
390
+ }
448
391
  };
392
+ // warm light pool: switches nearby non-sky pixels to the "lit" tone table
393
+ const pool = (cx, cy, rx, ry, L) => { if (L <= 0.01) return; for (let y = Math.max(Math.floor(cy - ry), -oy); y <= cy + ry; y++) for (let x = Math.floor(cx - rx); x <= cx + rx; x++) { const px = x + ox; const py = y + oy; if (px < 0 || py < 0 || px >= bw || py >= bh) continue; const i = py * bw + px; const m = matBuf[i]; if (m === 0 || m === M.SIL || m === M.LIT || m === M.LIT_HI || m === M.LIT_DK || m === M.LANTERN || m === M.LANTERN_HI) continue; const d = Math.hypot((x - cx) / rx, (y - cy) / ry); if ((1 - d) * L * 1.5 * 16 > bayerAt(x, y)) varBuf[i] = 2; } };
394
+ // shell glow without bloom (the shell reads this as "the room is lit")
395
+ const homeLight = (x, y, weight) => { if (weight <= 0) return; const c = sources[0]; const px = x + ox; const py = y + oy; c.w += weight; c.x += px * weight; c.y += py * weight; };
396
+ const isSeaMat = (m) => m === M.SEA || m === M.SEA_DK || m === M.SEA_LT || m === M.SEA_HI;
397
+ const fx = { smoke: [], t: 0 };
398
+ // ===== Scene: seaside (lighthouse + pier + village across the bay) =====
399
+ const SEAL = { YH: 60, SH: 98, LX: 182, T: 38, BASE: 76, DECK: 90 };
449
400
  const paintSeaside = () => {
450
- // ocean band and distant islands
451
- rect(0,48,CORE_W,36,M.CLOUD); rect(0,82,CORE_W,30,M.PATH);
452
- for(let y=52;y<82;y+=5){for(let x=(y%2)*7;x<CORE_W;x+=24){rect(x,y,8+(x%3),1,M.CLOUD_SH);}}
453
- rect(0,46,48,5,M.HILL_FAR); rect(142,43,50,7,M.HILL_NEAR);
454
- // sandy beach and shoreline
455
- rect(0,83,CORE_W,29,M.PATH); for(let y=84;y<112;y+=5) for(let x=(y%3)*4;x<CORE_W;x+=19) put(x,y,M.STONE_LT);
456
- for(let x=0;x<CORE_W;x++){const shore=82+Math.round(Math.sin(x*.055)*2);put(x,shore,M.CLOUD_SH);if(x%3===0)put(x,shore+1,M.STONE_LT);}
457
- // lighthouse on rocky point
458
- rect(145,34,31,9,M.STONE); rect(151,16,18,27,M.WALL); rect(149,14,22,4,M.WOOD_DK);
459
- rect(153,19,14,3,M.WOOD_LT); rect(155,23,10,17,M.WALL_SH);
460
- rect(155,27,10,7,M.GLASS); rect(157,28,6,4,M.LIT_HI);
461
- rect(154,10,12,4,M.ROOF); rect(157,7,6,3,M.LIT); rect(159,3,2,4,M.LIT_HI);
462
- rect(144,41,34,3,M.STONE_LT); rect(148,44,28,2,M.STONE);
463
- // sailboat
464
- rect(49,56,2,18,M.WOOD_DK); rect(51,58,13,12,M.CLOUD); rect(39,70,31,3,M.WOOD_DK);
465
- rect(42,73,25,2,M.WOOD); rect(52,60,1,9,M.WOOD_DK);
466
- // beach umbrella, towel and rocks
467
- rect(25,77,2,18,M.WOOD_DK); rect(17,73,18,4,M.ROOF); rect(20,70,12,3,M.ROOF_LT);
468
- rect(8,96,28,5,M.SHIRT); rect(11,98,22,1,M.SHIRT_DK);
469
- paintBush(101,91,10,5,440); paintBush(186,87,12,6,441);
470
- rect(115,101,19,5,M.STONE); rect(117,99,15,2,M.STONE_LT);
471
- // small palm at edge
472
- for(let y=52;y<82;y++){const x=16+Math.round((y-52)*.18);put(x,y,M.BARK);}
473
- for(const [dx,dy] of [[-13,-4],[-9,-9],[-3,-12],[5,-11],[12,-6],[15,-1]]) for(let i=0;i<7;i++) put(16+dx+Math.round(i*dx*.08),52+dy+Math.round(i*.35),M.LEAF2);
474
- // Palm fronds with layered highlights and a textured trunk
475
- for(let y=55;y<82;y+=3){put(16+Math.round((y-52)*.18)-1,y,M.BARK_LT);put(16+Math.round((y-52)*.18)+1,y,M.BARK);}
476
- for(let x=0;x<CORE_W;x+=1){const wave=83+Math.round(Math.sin(x*.11)*2);if(x%2===0)put(x,wave,M.CLOUD);if(x%7===0)put(x,wave+2,M.CLOUD_SH);}
477
- // Multiple ocean bands, foam streaks and distant wave crests
478
- for(let y=53;y<80;y+=3){for(let x=(y*7)%21;x<CORE_W;x+=27){rect(x,y,5+(x%5),1,M.CLOUD_SH);if(y%2===0)put(x+2,y-1,M.CLOUD);}}
479
- for(let x=5;x<CORE_W;x+=17){put(x,61+(x%5),M.CLOUD);put(x+1,62+(x%5),M.CLOUD_SH);}
480
- // Lighthouse masonry bands, windows, balcony rail and lantern room
481
- for(let y=20;y<41;y+=5){rect(152,y,16,1,M.WOOD_LT);for(let x=153+(y%3);x<168;x+=6)put(x,y+1,M.STONE_LT);}
482
- rect(150,12,20,2,M.WOOD_DK);rect(152,9,16,3,M.GLASS);rect(154,10,12,1,M.LIT_HI);
483
- rect(149,7,22,2,M.WOOD_DK);rect(151,5,18,2,M.ROOF_LT);
484
- rect(150,17,20,1,M.WOOD_DK);for(let x=151;x<170;x+=3)put(x,16,M.WOOD_LT);
485
- rect(154,29,12,7,M.GLASS);rect(156,30,8,5,M.LIT);put(159,31,M.LIT_HI);
486
- rect(153,38,14,2,M.WOOD_DK);rect(155,40,10,3,M.STONE);
487
- // Rocky headland and a winding footpath to the lighthouse
488
- rect(137,43,43,4,M.STONE);rect(141,47,34,3,M.STONE_LT);
489
- for(let y=49;y<84;y+=3){const x=147-Math.floor((y-49)*.35);rect(x,y,26,2,y%2?M.STONE:M.HILL_NEAR);}
490
- for(let i=0;i<8;i++){put(139+i*4,47+i%3,M.CLOUD_SH);put(175-i*3,50+i%4,M.STONE_LT);}
491
- // Wooden pier, posts, rope and mooring rings
492
- rect(63,82,58,3,M.WOOD_DK);rect(66,83,52,1,M.WOOD_LT);
493
- for(let x=68;x<119;x+=8){rect(x,85,2,10,M.WOOD_DK);put(x,86,M.WOOD_LT);}
494
- rect(64,80,2,2,M.WOOD_DK);rect(118,80,2,2,M.WOOD_DK);
495
- // Boat cabin, rigging, mast flag and hull planks
496
- rect(43,68,26,2,M.WOOD_DK);rect(46,65,19,3,M.WOOD);rect(49,62,12,3,M.WOOD_DK);
497
- rect(51,58,2,13,M.WOOD_DK);rect(53,58,1,12,M.WOOD_LT);
498
- for(let x=45;x<67;x+=5)put(x,71,M.WOOD_LT);
499
- rect(53,55,7,2,M.CLOUD);put(60,55,M.ROOF);
500
- // Beach details: deck chair, shell, driftwood and dune grass
501
- rect(6,87,18,2,M.WOOD_DK);rect(8,89,2,7,M.WOOD_DK);rect(21,89,2,7,M.WOOD_DK);
502
- rect(9,88,12,3,M.SHIRT);rect(12,91,7,2,M.SHIRT_DK);
503
- put(42,98,M.CLOUD);put(43,97,M.CLOUD_SH);put(44,98,M.CLOUD);
504
- rect(128,102,15,2,M.WOOD_DK);put(130,100,M.WOOD_LT);put(137,99,M.WOOD_LT);
505
- for(let x=0;x<CORE_W;x+=13){put(x,87+(x%4),M.LEAF1);put(x+2,85+(x%5),M.LEAF2);}
506
- // Reflected sunlight on wet sand
507
- for(let y=88;y<109;y+=6){for(let x=(y*5)%23;x<CORE_W;x+=31)rect(x,y,7,1,M.LIT_DK);}
401
+ wins.length = 0; const { YH, SH, LX, T, BASE, DECK } = SEAL; const xa = -ox; const xb = bw - ox;
402
+ // sea: pale haze at the horizon, deeper water in the middle, lighter shallows near the shore
403
+ for (let y = YH; y <= SH + 10; y++) {
404
+ const t = clamp((y - YH) / (SH - YH));
405
+ for (let x = xa; x < xb; x++) {
406
+ const n = noiseA(x * 0.05 + 9, y * 0.45 + 2);
407
+ let m;
408
+ if (t < 0.16) m = dth(x, y, (0.16 - t) * 6.5) ? M.SEA_LT : M.SEA;
409
+ else if (t < 0.6) m = dth(x, y, (n - 0.3) * 1.7 + t * 0.35) ? M.SEA_DK : M.SEA;
410
+ else m = dth(x, y, (t - 0.6) * 1.9 + (n - 0.5) * 0.4) ? M.SEA_LT : M.SEA;
411
+ if (t > 0.78 && dth(x, y, (t - 0.78) * 3.8 + (n - 0.5) * 0.3)) m = M.SHALLOW;
412
+ put(x, y, m);
413
+ }
414
+ }
415
+ for (let x = xa; x < xb; x += 2) put(x, YH, M.SEA_HI);
416
+ // far hills + village across the bay (left) and a low island (centre)
417
+ const farTop = (x) => Math.round(YH - 10 + noiseA(x * 0.05 + 2, 3.5) * 3 + Math.pow(clamp((x - 6) / 64), 1.6) * 10);
418
+ for (let x = xa; x < 76; x++) { const top = farTop(x); for (let y = top; y < YH; y++) put(x, y, y === top || dth(x, y, 0.3) ? M.HILL_NEAR : M.HILL_FAR); }
419
+ for (const cx of [4, 22, 40]) for (let i = 0; i < 3; i++) {
420
+ const x = cx + i * 4 + ((hash2(cx, i) * 2) | 0); const top = farTop(x) + 2; const hh = 2 + ((hash2(x, 9) * 2) | 0);
421
+ rect(x, top - hh, 3, hh, M.CONCRETE_SH); put(x, top - hh - 1, M.ROOF); put(x + 1, top - hh - 2, M.ROOF); put(x + 1, top - hh - 1, M.ROOF); put(x + 2, top - hh - 1, M.ROOF); if (hash2(x, 4) < 0.85) addWin(x + 1, top - 1, 1, 1, { tiny: true });
422
+ }
423
+ for (let x = 98; x < 134; x++) { const top = YH - 1 - Math.round(3.4 * Math.sin(clamp((x - 98) / 36) * Math.PI) + noiseA(x * 0.1, 8) * 1.2); for (let y = top; y < YH; y++) put(x, y, y === top ? M.HILL_NEAR : M.HILL_FAR); }
424
+ // rocky headland with the lighthouse (right): left-lit, dithered three-tone rock with strata
425
+ const cliffTop = (x) => Math.round((x < 150 ? 98 - 22 * smoothstep(118, 150, x) : BASE) + (x > 204 ? (x - 204) * 0.4 : 0) + (noiseB(x * 0.2, 1) - 0.5) * 2.4);
426
+ for (let x = 118; x < xb; x++) {
427
+ const top = cliffTop(x);
428
+ for (let y = top; y <= SH + 8; y++) {
429
+ const depth = y - top; const strata = Math.sin((y + noiseA(x * 0.05, 3) * 7) * 1.25) * 0.09;
430
+ const v = 0.62 - (x - 118) * 0.0045 - depth * 0.014 + (noiseB(x * 0.17 + 4, y * 0.3) - 0.5) * 0.5 + strata + (bayerAt(x, y) / 16 - 0.5) * 0.2 + (x < 150 ? (150 - x) * 0.004 : 0);
431
+ let m = v > 0.6 ? M.STONE_LT : v > 0.3 ? M.STONE : M.ROCK;
432
+ if (depth < 2 && x >= 150) m = (x + y) % 3 === 0 ? M.GRASS_LT : M.GRASS;
433
+ else if (depth >= 2 && depth < 4 && x >= 150 && dth(x, y, 0.45)) m = M.GRASS_DK;
434
+ else if (depth < 1) m = M.STONE_LT;
435
+ put(x, y, m);
436
+ }
437
+ }
438
+ for (let i = 0; i < 18; i++) { const x = 152 + ((hash2(i, 1) * 46) | 0); put(x, BASE - 1, M.GRASS_LT); put(x, BASE - 2, M.GRASS_LT); if (i % 4 === 0) put(x + 1, BASE - 3, M.FLOWER_C); }
439
+ // keeper's cottage
440
+ const CX0 = 153; const CB = BASE;
441
+ rect(CX0, CB - 9, 17, 9, M.WALL); rect(CX0 + 14, CB - 9, 3, 9, M.WALL_SH); rect(CX0, CB - 9, 17, 1, M.WALL_SH);
442
+ for (let y = CB - 17; y < CB - 9; y++) { const hw = Math.round(9.8 - (CB - 10 - y) * 1.1); for (let x = CX0 + 8 - hw; x <= CX0 + 8 + hw; x++) put(x, y, (y + x) % 3 === 0 ? M.ROOF_LT : M.ROOF); }
443
+ rect(CX0 - 2, CB - 10, 21, 1, M.ROOF_LT); rect(CX0 + 11, CB - 22, 3, 7, M.BRICK); rect(CX0 + 10, CB - 23, 5, 1, M.STONE_LT);
444
+ rect(CX0 + 3, CB - 6, 4, 6, M.WOOD_DK); put(CX0 + 6, CB - 3, M.WOOD_LT); rect(CX0 + 9, CB - 7, 4, 3, M.GLASS); put(CX0 + 9, CB - 7, M.GLASS_HI); addWin(CX0 + 9, CB - 7, 4, 3, {});
445
+ // lighthouse tower
446
+ const half = (y) => 5.5 + ((y - T) / (BASE - T)) * 3;
447
+ for (let y = T; y < BASE; y++) {
448
+ const hw = Math.round(half(y));
449
+ for (let x = LX - hw; x <= LX + hw; x++) {
450
+ const t = (x - (LX - hw)) / (2 * hw); let m = M.WALL;
451
+ if (t > 0.78 || (t > 0.58 && dth(x, y, 0.55))) m = M.WALL_SH;
452
+ if ((y >= T + 9 && y < T + 15) || (y >= T + 25 && y < T + 31)) m = t > 0.62 ? M.WOOD : M.BAND;
453
+ put(x, y, m);
454
+ }
455
+ }
456
+ rect(LX - 2, BASE - 7, 5, 7, M.WOOD_DK); put(LX + 1, BASE - 4, M.WOOD_LT); rect(LX - 1, T + 19, 3, 4, M.GLASS); put(LX - 1, T + 19, M.GLASS_HI); rect(LX - 1, T + 3, 3, 3, M.GLASS); put(LX - 1, T + 3, M.GLASS_HI);
457
+ rect(LX - 9, T - 3, 19, 3, M.WOOD_DK); rect(LX - 9, T - 3, 19, 1, M.WOOD_LT); for (let x = LX - 9; x <= LX + 9; x += 3) rect(x, T - 6, 1, 3, M.WOOD_DK); rect(LX - 9, T - 7, 19, 1, M.WOOD_DK);
458
+ rect(LX - 4, T - 15, 9, 8, M.GLASS); put(LX - 3, T - 14, M.GLASS_HI); put(LX - 3, T - 13, M.GLASS_HI); for (const dx of [-4, 0, 4]) rect(LX + dx, T - 15, 1, 8, M.WOOD_DK); rect(LX - 5, T - 8, 11, 1, M.WOOD_DK); rect(LX - 5, T - 16, 11, 1, M.WOOD_DK);
459
+ for (let y = T - 22; y < T - 16; y++) { const hw = Math.round(1.6 + (y - (T - 22)) * 1.1); for (let x = LX - hw; x <= LX + hw; x++) put(x, y, x < LX - hw + 2 && y > T - 22 ? M.ROOF_HI : M.ROOF); }
460
+ put(LX, T - 24, M.WOOD_DK); put(LX, T - 23, M.WOOD_DK); put(LX, T - 25, M.LANTERN_HI);
461
+ // pier (left): plank deck, piles standing in the water, a lantern post at the end
462
+ for (let x = xa; x < 106; x++) { put(x, DECK, (x % 6 === 0) ? M.WOOD : M.WOOD_LT); put(x, DECK + 1, M.WOOD); put(x, DECK + 2, M.WOOD_DK); }
463
+ for (let x = xa + 3; x < 104; x += 12) { rect(x, DECK + 3, 2, 7, M.WOOD_DK); put(x, DECK + 3, M.WOOD); rect(x - 1, DECK + 9, 4, 1, M.SEA_HI); }
464
+ for (let x = xa + 9; x < 100; x += 12) line(x, DECK + 3, x + 5, DECK + 7, M.WOOD_DK);
465
+ rect(103, DECK - 9, 2, 9, M.WOOD_DK); rect(102, DECK - 12, 4, 3, M.LANTERN); put(102, DECK - 13, M.WOOD_DK); put(105, DECK - 13, M.WOOD_DK); rect(102, DECK - 14, 4, 1, M.WOOD_DK);
466
+ // beach: tan sand, a thin wet band at the water, a few light grains
467
+ for (let y = SH + 2; y < bh - oy; y++) {
468
+ for (let x = xa; x < xb; x++) {
469
+ const d = y - (SH + 2); const n = noiseA(x * 0.08 + 3, y * 0.25) + noiseB(x * 0.4, y * 0.6) * 0.3;
470
+ let m = M.PATH; if (d < 3) m = dth(x, y, 0.8 - d * 0.25) ? M.PATH_DK : M.PATH; else if (n > 0.98) m = M.SAND_LT; else if (n < 0.3 && dth(x, y, 0.2)) m = M.PATH_DK;
471
+ put(x, y, m);
472
+ }
473
+ }
474
+ const rr = seeded(612); for (let i = 0; i < 26; i++) { const x = xa + ((rr() * bw) | 0); const y = SH + 6 + ((rr() * (bh - oy - SH - 6)) | 0); put(x, y, rr() < 0.6 ? M.SAND_LT : M.STONE_LT); }
475
+ // row boat on the sand
476
+ for (let y = 103; y < 110; y++) { const t = (y - 103) / 6; const l = Math.round(22 + t * 5); const r = Math.round(58 - t * 6); for (let x = l; x <= r; x++) put(x, y, y === 103 ? M.WOOD_LT : x < l + 2 || x > r - 2 ? M.WOOD_DK : (y + x) % 5 === 0 ? M.WOOD_LT : M.WOOD); }
477
+ rect(26, 101, 28, 1, M.WOOD_DK); put(26, 100, M.WOOD_DK); put(53, 100, M.WOOD_DK); rect(36, 104, 8, 1, M.WOOD_LT); rect(50, 98, 1, 7, M.WOOD_LT); rect(49, 97, 3, 1, M.WOOD_LT);
478
+ paintBush(8, 112, 9, 3, 951); paintBush(196, 113, 12, 3, 952); paintBush(112, 115, 8, 2, 953);
479
+ for (let x = xa; x < xb; x += 9) { const y = 106 + (hash2(x, 5) * 7 | 0); put(x, y, M.GRASS_DK); put(x, y - 1, M.GRASS); put(x + 1, y - 2, M.GRASS_LT); put(x - 1, y - 2, M.GRASS_LT); }
480
+ };
481
+ const seaPerson = { x: 84, face: 1, step: 0, bite: 0, nextBite: 6, bob: 0 };
482
+ const walkTo = (p, want, speed, dt) => { const d = want - p.x; if (Math.abs(d) < 0.6) { p.x = want; p.step = 0; return false; } const mv = Math.min(Math.abs(d), speed * dt); p.x += Math.sign(d) * mv; p.face = Math.sign(d); p.step += mv * 0.28; return true; };
483
+ const liveSeaside = (h, dt) => {
484
+ const { YH, SH, LX, T, DECK } = SEAL; const LY = T - 11; const xa = -ox; const xb = bw - ox; const lamp = lampAmt(h); const t = anim;
485
+ // wave crests
486
+ for (let y = YH + 2; y < SH - 1; y++) {
487
+ if (y & 1) continue; const tt = (y - YH) / (SH - YH); const cnt = 3 + Math.round(tt * 4);
488
+ for (let i = 0; i < cnt; i++) {
489
+ const sp = (0.7 + tt * 4.5) * (hash2(y, i + 70) > 0.5 ? 1 : -1); const span = bw + 30;
490
+ const x = Math.round((((hash2(y, i + 50) * span + t * sp) % span) + span) % span) + xa - 15; const len = 2 + Math.floor(tt * 4) + ((hash2(y, i + 9) * 3) | 0);
491
+ if (Math.sin(t * 0.8 + hash2(y, i + 3) * 6.28) < -0.25) continue;
492
+ for (let k2 = 0; k2 < len; k2++) { const xx = x + k2; if (isSeaMat(get(xx, y))) put(xx, y, k2 === 0 || k2 === len - 1 ? M.SEA_LT : M.SEA_HI); }
493
+ }
494
+ }
495
+ // sun / moon glitter on the water
496
+ const sp = sunPos(h); const mp = moonPos(h); const lit = sp && sp.y < YH - 3 ? { x: sp.x, m: M.LIT_HI } : mp && mp.y < YH - 3 ? { x: mp.x, m: M.STAR } : null;
497
+ if (lit) for (let y = YH + 2; y < SH - 1; y++) { const tt = (y - YH) / (SH - YH); const spread = 1.5 + tt * 8; const n = 1 + Math.round(tt * 3); for (let i = 0; i < n; i++) { const r = hash2(y * 5 + i, Math.floor(t * 2.5 + i * 3)); const x = lit.x + Math.round((r - 0.5) * 2 * spread); if (r > 0.25 && isSeaMat(get(x, y))) { put(x, y, lit.m); if (r > 0.7 && isSeaMat(get(x + 1, y))) put(x + 1, y, lit.m); } } }
498
+ // distant village lights
499
+ drawWins(h, winFrac(h) * 1.1);
500
+ for (const w of wins) if (w.tiny && w.seed < winFrac(h) * 1.1 && ((Math.floor(t * 1.5 + w.seed * 9) & 1) === 0)) put(w.x, YH + 1 + (w.seed > 0.5 ? 1 : 0), M.SEA_HI);
501
+ // sailing boat
502
+ const span = 330; const bx = Math.round((((t * 2.4 + 60) % span) + span) % span) - 70 + xa + 10; const by = YH + 5 + Math.round(Math.sin(t * 1.2));
503
+ if (bx > xa - 14 && bx < xb + 14) {
504
+ rect(bx, by, 9, 1, M.WOOD_DK); rect(bx + 1, by + 1, 7, 1, M.WOOD_DK); rect(bx + 1, by, 7, 1, M.WOOD_LT); line(bx + 4, by - 1, bx + 4, by - 11, M.WOOD_DK);
505
+ for (let j = 0; j < 9; j++) { const w2 = Math.round(j * 0.45); for (let i = 1; i <= w2 + 1; i++) put(bx + 4 - i, by - 2 - j, M.CLOUD); }
506
+ for (let j = 0; j < 6; j++) { const w2 = Math.round(j * 0.35); for (let i = 1; i <= w2 + 1; i++) put(bx + 5 + i - 1, by - 2 - j, j > 3 ? M.CLOUD : M.CLOUD_SH); }
507
+ if (lamp > 0.1) { put(bx + 4, by - 12, M.LANTERN_HI); emit(bx + 4, by - 12, 110 * lamp, 2); put(bx + 4, by + 3, M.SEA_HI); }
508
+ }
509
+ // fisherman on the pier
510
+ const away = h >= 22.6 || h < 7.4; const P = seaPerson;
511
+ const moving = calm ? ((P.x = away ? -40 : 84), false) : walkTo(P, away ? -40 : 84, 14, dt);
512
+ if (P.x > xa - 8) {
513
+ if (moving) blit(SPR.walk[Math.floor(P.step) % 4], Math.round(P.x) - 3, DECK - 12, P.face < 0);
514
+ else {
515
+ P.nextBite -= calm ? 0 : dt; if (P.nextBite <= 0) { P.bite = 1.1; P.nextBite = 7 + hash2(Math.floor(t), 4) * 6; } P.bite = Math.max(0, P.bite - dt);
516
+ const spr = P.bite > 0.5 ? SPR.seatUp : (!calm && Math.sin(t * 0.9) > 0.97 ? SPR.seatBlink : SPR.seat); const sx = 90; const sy = DECK - 9;
517
+ blit(spr, sx, sy, true);
518
+ const hx = sx + 6; const hy = sy + 5; const tipx = hx + 12; const tipy = hy - 8 + (P.bite > 0.5 ? 2 : 0);
519
+ line(hx, hy, tipx, tipy, M.WOOD_LT);
520
+ const bobY = DECK + 4 + Math.round(calm ? 0 : Math.sin(t * 1.6)) + (P.bite > 0 ? 1 : 0); const bobX = tipx + 7;
521
+ for (let y = tipy; y < bobY; y++) if (((y - tipy) & 1) === 0 || y > bobY - 3) put(Math.round(mix(tipx, bobX, (y - tipy) / (bobY - tipy))), y, M.STAR_DIM);
522
+ put(bobX, bobY, M.LANTERN_HI); put(bobX, bobY + 1, M.ROOF_LT);
523
+ if (P.bite > 0.7 && P.bite < 1.0) { put(bobX - 1, bobY - 1, M.FOAM); put(bobX + 1, bobY - 2, M.FOAM); put(bobX + 2, bobY - 1, M.FOAM); }
524
+ }
525
+ }
526
+ // pier lantern
527
+ const pl = lamp > 0.05; rect(103, DECK - 11, 2, 2, pl ? M.LANTERN_HI : M.LANTERN); if (pl) { emit(103, DECK - 11, 150 * lamp, 2); emit(104, DECK - 10, 150 * lamp, 2); pool(104, DECK + 1, 12, 5, lamp * 0.7); }
528
+ // lighthouse lamp + sweeping beam
529
+ if (lamp > 0.02) {
530
+ for (let j = 0; j < 8; j++) for (let i = 0; i < 9; i++) { const xx = LX - 4 + i; const mm = get(xx, T - 15 + j); if (mm === M.GLASS || mm === M.GLASS_HI) put(xx, T - 15 + j, j > 5 ? M.LIT : M.LIT_HI); }
531
+ for (let j = 0; j < 8; j += 1) for (let i = 1; i < 8; i += 2) emit(LX - 4 + i, T - 15 + j, 230 * lamp, 0);
532
+ const th = t * 0.55; const bx2 = Math.cos(th); const LYb = T - 11; const dir = bx2 >= 0 ? 1 : -1; const ab = Math.abs(bx2);
533
+ if (ab > 0.1) { const len = 150 * ab; for (let d = 6; d < len; d += 1) { const hh = 0.8 + d * 0.05 * ab; const fade = (1 - d / len) * (0.5 + 0.5 * lamp); for (let dy = -Math.floor(hh); dy <= hh; dy++) { const x = LX + dir * d; const y = LYb + dy + Math.round(d * 0.02); if (dth(x, y, fade * 2.2 * (1 - Math.abs(dy) / (hh + 1.5)))) { const mm = get(x, y); if (mm === 0 || isSeaMat(mm) || mm === M.STAR || mm === M.STAR_DIM) put(x, y, d < len * 0.25 ? M.LIT_HI : M.LIT); } } } }
534
+ if (ab < 0.45) { const f = 1 - ab / 0.45; disc(LX, LYb, 2 + Math.round(f * 2), M.LIT_HI); for (let k2 = 0; k2 < 18; k2++) { const a = k2 * 0.349; const r2 = 6 + f * 6; if (dth(Math.round(LX + Math.cos(a) * r2), Math.round(LYb + Math.sin(a) * r2), f)) putSky(Math.round(LX + Math.cos(a) * r2), Math.round(LYb + Math.sin(a) * r2), M.LIT); } }
535
+ homeLight(LX, LYb, 20000 * lamp);
536
+ }
537
+ // surf at the shoreline
538
+ const wash = 1.6 * Math.sin(t * 0.7) + 0.8 * Math.sin(t * 1.9 + 1);
539
+ for (let x = xa; x < xb; x++) { if (x > 116 && get(x, SH - 1) === M.ROCK) continue; const sy2 = Math.round(SH + wash + Math.sin(x * 0.09 + t * 0.6) * 1.2); const m0 = get(x, sy2); if (m0 === M.WOOD_DK || m0 === M.ROCK || m0 === M.STONE || m0 === M.STONE_LT) continue; put(x, sy2, M.FOAM); if (((x + Math.floor(t * 2)) & 1) === 0) put(x, sy2 + 1, M.SEA_HI); put(x, sy2 + 2, M.PATH_DK); }
540
+ };
541
+
542
+ // ===== Scene: mountain cabin (snow, pines, chopping wood, aurora at night) =====
543
+ const pineTree = (cx, base, h, tiers, snowy) => {
544
+ rect(cx - 1, base - 7, 3, 7, M.BARK); put(cx - 1, base - 7, M.BARK_LT); put(cx - 1, base - 5, M.BARK_LT);
545
+ const span = h - 6; const tierH = Math.round((span / tiers) * 1.9);
546
+ for (let i = 0; i < tiers; i++) {
547
+ const top = base - h + Math.round((i * span) / tiers); const maxW = 2 + Math.round(((i + 1) * h * 0.34) / tiers);
548
+ for (let y = top; y < Math.min(top + tierH, base - 5); y++) {
549
+ const t = (y - top) / tierH; const hw = Math.round(0.6 + t * (maxW - 0.6));
550
+ for (let x = cx - hw; x <= cx + hw; x++) {
551
+ const rel = (x - cx) / Math.max(hw, 1);
552
+ if (t > 0.9 && ((x + i) & 1) && Math.abs(rel) < 0.85) continue;
553
+ const v = 0.5 - rel * 0.45 + (bayerAt(x, y) / 16 - 0.5) * 0.36 + (noiseB(x * 0.5 + i, y * 0.4) - 0.5) * 0.3;
554
+ let m = v > 0.72 ? M.LEAF2 : v > 0.4 ? M.LEAF1 : M.LEAF0;
555
+ if (snowy) { const edge = hw - Math.abs(x - cx); if (edge < 1.6 && t < 0.85 && t > 0.05) m = rel < 0 ? M.SNOW : M.SNOW_SH; else if (t < 0.28 && edge < 3.4 && dth(x, y, 0.5)) m = rel < 0 ? M.SNOW : M.SNOW_SH; }
556
+ put(x, y, m);
557
+ }
558
+ }
559
+ }
508
560
  };
509
561
  const paintMountainCabin = () => {
510
- rect(0,0,CORE_W,112,0);
511
- // layered mountain silhouettes
512
- for(let x=0;x<CORE_W;x++){
513
- const a=35+Math.round(Math.sin(x*.045)*9+Math.sin(x*.11)*4);
514
- const b=50+Math.round(Math.sin(x*.035+1.7)*8+Math.sin(x*.08)*3);
515
- for(let y=a;y<68;y++) put(x,y,M.HILL_FAR);
516
- for(let y=b;y<77;y++) put(x,y,M.HILL_NEAR);
517
- }
518
- // snow caps
519
- for(let x=0;x<CORE_W;x++){const peak=35+Math.round(Math.sin(x*.045)*9+Math.sin(x*.11)*4);if(x%2===0){put(x,peak,M.CLOUD);put(x,peak+1,M.CLOUD_SH);}}
520
- rect(0,77,CORE_W,35,M.GRASS_DK);
521
- for(let y=82;y<112;y+=7) for(let x=(y%2)*5;x<CORE_W;x+=18) put(x,y,M.STONE_LT);
522
- // pines
523
- const pine=(cx,base,h)=>{rect(cx-1,base-h,3,h,M.BARK);for(let j=0;j<4;j++){const yy=base-h+j*Math.floor(h/4);const w=3+j*3;for(let y=yy;y<yy+Math.floor(h/4)+2;y++)for(let x=cx-w;x<=cx+w;x++)if(Math.abs(x-cx)<=w-(y-yy)*.45)put(x,y,j%2?M.LEAF1:M.LEAF0);}};
524
- pine(18,91,37);pine(34,88,28);pine(178,94,42);pine(160,89,27);pine(116,86,22);
525
- // wooden cabin
526
- rect(66,55,62,35,M.WALL); for(let y=60;y<88;y+=5)rect(66,y,62,1,M.WOOD);
527
- for(let y=34;y<57;y++){const t=(y-34)/23;for(let x=Math.round(58+t*8);x<=136-Math.round(t*8);x++)put(x,y,M.ROOF);}
528
- rect(57,53,80,4,M.WOOD_DK); rect(88,67,17,23,M.WOOD_DK);rect(90,69,13,21,M.WOOD);
529
- rect(71,65,13,12,M.GLASS);rect(73,67,9,8,M.LIT);rect(77,65,2,12,M.WOOD_DK);
530
- rect(111,64,12,13,M.GLASS);rect(112,65,10,3,M.GLASS_HI);
531
- rect(87,90,20,3,M.STONE);rect(83,93,28,2,M.STONE_LT);
532
- // chimney and smoke
533
- rect(115,34,8,17,M.BRICK);rect(113,32,12,3,M.STONE_LT);
534
- // Jagged distant peaks with layered snow shelves and exposed rock
535
- for(let x=0;x<CORE_W;x+=1){const peak=35+Math.round(Math.sin(x*.045)*9+Math.sin(x*.11)*4);if(x%3===0){put(x,peak+3,M.CLOUD);put(x+1,peak+5,M.CLOUD_SH);}}
536
- for(let x=0;x<CORE_W;x+=17){const y=44+(x%5);rect(x,y,5,2,M.HILL_FAR);put(x+2,y-1,M.CLOUD);}
537
- // Pine boughs, trunks and snow caught on branches
538
- const snowPine=(cx,base,h)=>{for(let j=0;j<5;j++){const yy=base-h+j*Math.floor(h/5);const w=3+j*3;for(let x=cx-w;x<=cx+w;x++){if(Math.abs(x-cx)<=w){put(x,yy+1,M.LEAF2);if((x+yy)%3===0)put(x,yy+2,M.CLOUD_SH);}}}rect(cx,base-h,1,h,M.WOOD_DK);};
539
- snowPine(48,91,30);snowPine(145,92,34);snowPine(190,90,29);snowPine(8,94,26);
540
- // Cabin log courses, corner joinery and stone foundation
541
- for(let y=58;y<89;y+=4){rect(67,y,60,1,M.WOOD_DK);for(let x=70+(y%3);x<124;x+=13)put(x,y+1,M.WOOD_LT);}
542
- rect(63,87,68,5,M.STONE);for(let x=66;x<128;x+=8)put(x,89,M.STONE_LT);
543
- // Porch roof, posts, railings and steps
544
- rect(78,51,48,3,M.WOOD_DK);rect(82,48,40,3,M.ROOF_LT);
545
- rect(81,53,2,35,M.WOOD_DK);rect(123,53,2,35,M.WOOD_DK);
546
- rect(80,78,45,2,M.WOOD_LT);for(let x=84;x<123;x+=7)rect(x,72,1,8,M.WOOD_DK);
547
- rect(88,91,18,2,M.WOOD_DK);rect(86,93,23,2,M.STONE_LT);rect(83,95,29,2,M.STONE);
548
- // Window shutters, warm panes and window boxes
549
- rect(69,63,16,15,M.WOOD_DK);rect(71,65,12,11,M.GLASS);
550
- rect(73,67,8,7,M.LIT);rect(77,65,2,11,M.WOOD_DK);
551
- rect(67,62,3,17,M.WOOD);rect(84,62,3,17,M.WOOD);
552
- rect(69,79,16,2,M.WOOD_DK);for(let x=71;x<84;x+=3)put(x,78,M.LEAF2);
553
- rect(108,61,17,17,M.WOOD_DK);rect(110,63,13,13,M.GLASS);
554
- rect(112,65,9,9,M.LIT_DK);rect(116,64,2,12,M.WOOD_DK);
555
- // Firewood stack, lantern, mailbox and snow-covered stones
556
- rect(133,82,18,5,M.WOOD_DK);for(let y=83;y<87;y+=2)for(let x=135+(y%3);x<149;x+=6)rect(x,y,5,1,M.WOOD);
557
- rect(154,76,2,12,M.WOOD_DK);rect(151,74,8,3,M.LIT);put(155,75,M.LIT_HI);
558
- rect(55,84,7,5,M.WOOD_DK);rect(56,82,5,3,M.BRICK_LT);put(58,81,M.LIT_HI);
559
- // Snowdrifts, tracks and scattered blue-white snow sparkle
560
- for(let x=0;x<CORE_W;x+=1){const drift=96+Math.round(Math.sin(x*.07)*2+Math.sin(x*.19));if(x%2===0)put(x,drift,M.CLOUD);if(x%5===0)put(x,drift+1,M.CLOUD_SH);}
561
- for(let i=0;i<7;i++){rect(112+i*3,99+i%2*2,2,1,M.CLOUD_SH);put(114+i*3,97+i%3,M.CLOUD);}
562
- for(let x=3;x<CORE_W;x+=29){put(x,107,M.CLOUD);put(x+1,106,M.CLOUD_SH);}
563
- };
564
- const scene = ['countryside','japanese-street','apartment','seaside','mountain-cabin'].includes(dataset.scene) ? (dataset.scene || 'countryside') : 'countryside';
565
- let sceneAnim = 0;
566
- const drawSceneResident = (dt) => {
567
- if (!calm) sceneAnim += dt;
568
- if (scene === 'apartment') {
569
- const pose = Math.sin(sceneAnim * 0.6) > 0.85 ? SPR.seatBlink : SPR.seat;
570
- blit(pose, 128, 61);
571
- return;
562
+ wins.length = 0; const xa = -ox; const xb = bw - ox; const yb = bh - oy;
563
+ // ---- far range: left-lit rock faces, snow above the snow line ----
564
+ const tent = (x, cx, hgt, s) => Math.max(0, hgt - Math.abs(x - cx) * s);
565
+ const far = (x) => Math.round(70 - Math.max(tent(x, 40, 44, 1.0), tent(x, 156, 29, 0.92), tent(x, 96, 17, 0.9), tent(x, -6, 34, 1.0), tent(x, 214, 30, 0.95)) + (noiseB(x * 0.33, 2) - 0.5) * 5);
566
+ for (let x = xa; x < xb; x++) {
567
+ const top = far(x); const lit = far(x + 2) - far(x - 2) < 0;
568
+ for (let y = top; y < 80; y++) {
569
+ const snowLine = 52 + (noiseA(x * 0.09, 6) - 0.5) * 9 + (bayerAt(x, y) / 16 - 0.5) * 3.5; const streak = noiseB(x * 0.55, y * 0.12) > 0.68;
570
+ let m; if (y < snowLine) m = lit ? M.SNOW : M.SNOW_SH; else m = lit ? (streak ? M.HILL_NEAR : M.HILL_FAR) : (streak ? M.ROCK : M.HILL_NEAR);
571
+ put(x, y, m);
572
+ }
573
+ }
574
+ // ---- forested foothills ----
575
+ const hill = (x) => Math.round(64 + noiseA(x * 0.035 + 11, 4) * 10 - 2 * Math.sin(x * 0.05));
576
+ for (let x = xa; x < xb; x++) { const top = hill(x); for (let y = top; y < 84; y++) { const d = y - top; put(x, y, d < 1 ? M.LEAF0 : dth(x, y, 0.4 - d * 0.03) ? M.SNOW_SH : (noiseB(x * 0.6, y * 0.7) > 0.55 ? M.TREE_FAR : M.LEAF0)); } }
577
+ for (let x = xa + 2; x < xb; x += 3 + ((hash2(x, 5) * 3) | 0)) { const top = hill(x); const hh = 4 + ((hash2(x, 7) * 4) | 0); for (let j = 0; j < hh; j++) { const hw = Math.floor(j / 3) + (j > 0 ? 0 : 0); for (let i = -hw; i <= hw; i++) put(x + i, top - hh + j + 1, (i < 0 && j % 3 === 0) ? M.SNOW_SH : M.LEAF0); } }
578
+ // ---- snow field with soft dune shading ----
579
+ for (let y = 78; y < yb; y++) for (let x = xa; x < xb; x++) {
580
+ const n = noiseA(x * 0.045 + 3, y * 0.16 + 1) + (noiseB(x * 0.3, y * 0.5) - 0.5) * 0.2 + (bayerAt(x, y) / 16 - 0.5) * 0.16;
581
+ const dune = Math.sin(x * 0.07 + y * 0.18) * 0.12;
582
+ put(x, y, n + dune > 0.72 ? M.SNOW_SH : (y < 81 && dth(x, y, 0.5)) ? M.SNOW_SH : M.SNOW);
572
583
  }
573
- const x = scene === 'japanese-street' ? 104 + Math.sin(sceneAnim * 0.18) * 20 : scene === 'seaside' ? 104 : 91;
574
- const y = scene === 'seaside' ? 82 : 88;
575
- const moving = !calm && scene !== 'seaside';
576
- const sprite = moving ? SPR.walk[Math.floor(sceneAnim * 3) % 4] : SPR.stand;
577
- blit(sprite, Math.round(x)-3, y-12, Math.sin(sceneAnim * 0.18) < 0);
584
+ for (let i = 0; i < 40; i++) { const x = xa + ((hash2(i, 31) * bw) | 0); const y = 84 + ((hash2(i, 32) * (yb - 84)) | 0); put(x, y, M.SNOW_SH); put(x + 1, y, M.SNOW_SH); }
585
+ // ---- mid pines ----
586
+ pineTree(52, 86, 34, 5, true); pineTree(62, 83, 24, 4, true); pineTree(138, 84, 26, 4, true); pineTree(122, 82, 18, 3, true);
587
+ // ---- cabin (front gable) ----
588
+ const CX = 90; const AP = 36; const EY = 62; const SLOPE = 0.76;
589
+ // external stone chimney on the right wall
590
+ for (let y = 33; y < 88; y++) for (let x = 116; x < 124; x++) { const k2 = ((x + (((y >> 1) & 1) * 2)) % 4) < 2; put(x, y, x > 120 ? M.STONE : k2 ? M.STONE_LT : M.STONE); if (y % 2 === 0 && x === 116) put(x, y, M.STONE); }
591
+ rect(115, 31, 10, 3, M.STONE_LT); rect(115, 33, 10, 1, M.STONE); rect(118, 29, 4, 2, M.ROCK);
592
+ // gable wall (vertical planks), main log wall
593
+ for (let y = AP; y < EY; y++) { const hw = (y - AP) / SLOPE; for (let x = Math.round(CX - hw); x <= Math.round(CX + hw); x++) put(x, y, (x % 4 === 0) ? M.WOOD_DK : (x % 4 === 1 ? M.WOOD_LT : M.WOOD)); }
594
+ for (let y = EY; y < 88; y++) for (let x = 66; x < 115; x++) { const row = (y - EY) % 4; let m = row === 0 ? M.WOOD_LT : row === 3 ? M.WOOD_DK : M.WOOD; if (x > 109 && dth(x, y, 0.5)) m = row === 0 ? M.WOOD : M.WOOD_DK; put(x, y, m); }
595
+ for (let y = EY; y < 88; y += 4) { for (const cx2 of [66, 67, 112, 113]) { put(cx2, y, M.WOOD_LT); put(cx2, y + 1, M.BARK); } }
596
+ rect(64, 87, 52, 3, M.STONE); for (let x = 64; x < 116; x += 6) put(x, 87, M.STONE_LT); rect(64, 89, 52, 1, M.ROCK);
597
+ // attic round window
598
+ disc(CX, 50, 3, M.WOOD_DK); disc(CX, 50, 2, M.GLASS); put(CX - 1, 49, M.GLASS_HI);
599
+ // roof rakes with thick snow on top (snow overhang gets a blue underside)
600
+ for (let y = AP - 3; y < EY + 4; y++) {
601
+ const hw = (y - AP) / SLOPE; const l = Math.round(CX - hw); const r = Math.round(CX + hw);
602
+ for (let i = 0; i < 3; i++) { put(l - i + (y >= EY ? 0 : 0), y, M.WOOD_DK); put(r + i, y, M.WOOD_DK); }
603
+ for (let i = 3; i < 7; i++) { const mL = i === 6 ? M.SNOW_SH : (i === 3 && y > AP + 8 ? M.SNOW_SH : M.SNOW); put(l - i + 2, y - 1, mL); put(r + i - 2, y - 1, i === 6 || i === 3 ? M.SNOW_SH : M.SNOW); }
604
+ }
605
+ for (let x = 56; x <= 124; x++) { if ((x * 5) % 7 < 4) put(x, EY + 4, M.SNOW); }
606
+ rect(57, EY + 3, 68, 1, M.WOOD_DK); rect(CX - 3, AP - 4, 7, 4, M.SNOW); put(CX - 1, AP - 5, M.SNOW); put(CX, AP - 5, M.SNOW);
607
+ // porch canopy and posts
608
+ rect(77, 69, 26, 3, M.WOOD_DK); rect(76, 66, 28, 3, M.SNOW); rect(78, 68, 24, 1, M.SNOW_SH); rect(79, 72, 2, 16, M.WOOD_DK); rect(99, 72, 2, 16, M.WOOD_DK);
609
+ // door
610
+ rect(84, 73, 12, 15, M.WOOD_DK); rect(85, 74, 10, 14, M.WOOD); for (let x = 87; x < 95; x += 3) rect(x, 74, 1, 14, M.WOOD_DK); rect(85, 74, 10, 4, M.GLASS); rect(89, 74, 1, 4, M.WOOD_DK); put(93, 81, M.LANTERN_HI);
611
+ addWin(85, 74, 4, 4, { always: true }); addWin(90, 74, 5, 4, { always: true });
612
+ // side windows with shutters + flower boxes
613
+ const winAt = (x0) => { rect(x0 - 2, 71, 14, 13, M.WOOD_DK); rect(x0, 73, 10, 9, M.GLASS); rect(x0, 73, 10, 1, M.GLASS_HI); put(x0 + 1, 74, M.GLASS_HI); rect(x0 + 4, 73, 2, 9, M.WOOD_DK); rect(x0 - 3, 72, 2, 11, M.WOOD); rect(x0 + 11, 72, 2, 11, M.WOOD); rect(x0 - 1, 84, 12, 2, M.WOOD_DK); for (let x = x0; x < x0 + 10; x += 2) { put(x, 83, M.SNOW); } addWin(x0, 73, 10, 9, { always: true, mull: 1 }); };
614
+ winAt(68); winAt(102);
615
+ // lantern on the porch
616
+ rect(81, 74, 1, 2, M.WOOD_DK); rect(80, 76, 3, 4, M.LANTERN); rect(80, 76, 3, 1, M.WOOD_DK); rect(80, 80, 3, 1, M.WOOD_DK);
617
+ // steps
618
+ rect(81, 88, 18, 2, M.STONE_LT); rect(79, 90, 22, 2, M.STONE); rect(78, 92, 24, 1, M.ROCK);
619
+ // snow drifts against the cabin
620
+ for (let x = 62; x < 120; x++) { const dr = 90 + Math.round(Math.sin(x * 0.3) * 1.2); put(x, dr, M.SNOW); if (x % 3 === 0) put(x, dr + 1, M.SNOW_SH); }
621
+ // ---- woodpile, chopping block, footprints ----
622
+ rect(150, 86, 24, 3, M.SNOW_SH);
623
+ for (let r = 0; r < 4; r++) for (let c = 0; c < 8; c++) { const x = 151 + c * 3 + (r & 1); const y = 77 + r * 3; if (x > 172) continue; rect(x, y, 3, 3, M.WOOD); put(x + 1, y + 1, (c + r) % 2 ? M.WOOD_LT : M.BARK_LT); put(x, y, M.WOOD_DK); put(x + 2, y + 2, M.WOOD_DK); }
624
+ rect(149, 74, 26, 3, M.SNOW); rect(150, 77, 24, 1, M.SNOW_SH); put(149, 77, M.SNOW_SH); put(174, 77, M.SNOW_SH);
625
+ rect(140, 82, 8, 6, M.WOOD); rect(140, 82, 8, 2, M.WOOD_LT); put(141, 83, M.WOOD); put(144, 83, M.WOOD_DK); rect(147, 83, 1, 5, M.WOOD_DK); rect(139, 81, 10, 1, M.SNOW); rect(140, 88, 8, 1, M.SNOW_SH);
626
+ for (let i = 0; i < 12; i++) { const x = 98 + i * 3 + (i & 1) * 2; const y = 95 + (i % 3 === 1 ? 1 : 0) - Math.floor(i / 6); put(x, y, M.SNOW_DK); put(x + 1, y + (i & 1), M.SNOW_DK); }
627
+ // ---- foreground pines ----
628
+ pineTree(24, 100, 66, 7, true); pineTree(179, 98, 58, 7, true); pineTree(196, 104, 40, 5, true);
629
+ const rr = seeded(77); for (let i = 0; i < 14; i++) { const x = xa + ((rr() * bw) | 0); const y = 100 + ((rr() * (yb - 100)) | 0); rect(x, y, 3, 1, M.SNOW_SH); }
630
+ };
631
+ const cabinP = { x: 132, inside: false, door: 0, face: 1, step: 0, cycle: 0, split: -1 };
632
+ const liveCabin = (h, dt) => {
633
+ const t = anim; const lamp = lampAmt(h); const xa = -ox; const xb = bw - ox; const P = cabinP;
634
+ // aurora curtains
635
+ const an = nightF(h);
636
+ if (an > 0.5) {
637
+ const amp = smoothstep(0.5, 0.95, an);
638
+ for (let x = xa; x < xb; x++) {
639
+ const base = 27 + Math.sin(x * 0.045 + t * 0.22) * 7 + Math.sin(x * 0.11 - t * 0.35) * 3; const len = 17 + 11 * Math.sin(x * 0.03 + t * 0.13 + 1.3) + noiseB(x * 0.15, t * 0.25) * 10;
640
+ const ray = 0.55 + 0.45 * Math.sin(x * 1.6 + noiseA(x * 0.2, t * 0.2) * 6);
641
+ for (let y = Math.floor(base - len); y <= base; y++) { const f = (y - (base - len)) / len; const pr = amp * Math.pow(f, 1.4) * 1.7 * ray; if (dth(x, y, pr)) putSky(x, y, f > 0.86 ? M.AUR_HI : f > 0.45 ? M.AUR_MID : M.AUR_LO); }
642
+ }
643
+ }
644
+ // chimney smoke
645
+ if (!calm) { fx.t += dt * 3.2; while (fx.t >= 1) { fx.t -= 1; fx.smoke.push({ x: 119 + Math.random() * 2, y: 28, t: 0, seed: Math.random() * 10 }); } }
646
+ for (let i = fx.smoke.length - 1; i >= 0; i--) { const s = fx.smoke[i]; s.t += dt; s.y -= dt * (6.5 - s.t * 0.7); s.x += dt * (2.4 + Math.sin(s.t * 1.7 + s.seed) * 2.4); if (s.t > 4.6 || s.y < -4) { fx.smoke.splice(i, 1); continue; } const size = 1 + Math.floor(s.t * 0.75); for (let dy = 0; dy < size; dy++) for (let dx = 0; dx < size; dx++) { const x = Math.round(s.x) + dx; const y = Math.round(s.y) + dy; if (bayerAt(x, y) < 16 - s.t * 2.9) put(x, y, s.t > 2.4 ? M.SMOKE_DK : M.SMOKE); } }
647
+ if (calm) for (let i = 0; i < 5; i++) rect(119 + i * 2, 24 - i * 4, 1 + (i >> 1), 2, M.SMOKE);
648
+ // windows, lantern, light on the snow
649
+ drawWins(h, 0, 1.3);
650
+ if (lamp > 0.05) { rect(80, 76, 3, 4, M.LANTERN_HI); for (let y = 77; y < 79; y++) for (let x = 80; x < 83; x++) emit(x, y, 200 * lamp, 0); pool(90, 92, 30, 7, lamp * 0.95); pool(73, 88, 10, 6, lamp * 0.6); pool(107, 88, 10, 6, lamp * 0.6); pool(81, 82, 8, 6, lamp * 0.7); homeLight(90, 78, 16000 * lamp); }
651
+ // daytime shadows on the snow (cabin + pines), opposite the sun
652
+ const sp = sunPos(h);
653
+ if (sp && sp.y < 70 && h > 6.4 && h < 17.4) {
654
+ const off = clamp((96 - sp.x) / 80, -1.3, 1.3); const sh = (cx, base, rx, ry) => { for (let y = base - ry; y <= base + ry; y++) for (let x = Math.floor(cx - rx); x <= cx + rx; x++) { if ((((x - cx) / rx) ** 2 + ((y - base) / ry) ** 2) > 1) continue; if (get(x, y) === M.SNOW) put(x, y, M.SNOW_SH); } };
655
+ sh(24 + off * 30, 101, 30, 3); sh(179 + off * 24, 99, 26, 3); sh(52 + off * 16, 87, 14, 2); sh(90 + off * 22, 92, 36, 3);
656
+ }
657
+ // the woodcutter
658
+ const outdoors = h >= 8.4 && h < 17.6; const wantX = outdoors ? 132 : 90;
659
+ if (calm) { P.inside = !outdoors; P.x = wantX; }
660
+ else {
661
+ P.door = Math.max(0, P.door - dt);
662
+ if (P.inside && outdoors) { P.inside = false; P.x = 90; P.door = 0.5; }
663
+ if (!P.inside) { const d = wantX - P.x; if (Math.abs(d) > 0.6) { const mv = Math.min(Math.abs(d), 16 * dt); P.x += Math.sign(d) * mv; P.face = Math.sign(d); P.step += mv * 0.28; } else { P.x = wantX; P.step = 0; if (!outdoors) { P.inside = true; P.door = 0.5; } } }
664
+ }
665
+ if (P.door > 0) { rect(85, 74, 10, 14, M.SIL); rect(85, 74, 2, 14, M.WOOD_LT); }
666
+ if (!P.inside) {
667
+ const base = 88; const x0 = Math.round(P.x) - 3; const moving = Math.abs(wantX - P.x) > 0.6;
668
+ if (moving) blit(SPR.walk[Math.floor(P.step) % 4], x0, base - 12, P.face < 0);
669
+ else if (outdoors) {
670
+ P.cycle = calm ? 1.2 : (P.cycle + dt) % 4.4; const c = P.cycle; const bx = 140; const by = 82;
671
+ let spr = SPR.stand; let axe = 0;
672
+ if (c > 2.5 && c < 2.95) { spr = SPR.up; axe = 1; } else if (c >= 2.95 && c < 3.25) { spr = SPR.crouch; axe = 2; } else if (c >= 3.25 && c < 3.6) { spr = SPR.stand; axe = 3; } else if (!calm && Math.sin(t * 0.9) > 0.93) spr = SPR.blink;
673
+ blit(spr, x0, base - 12, false);
674
+ if (axe === 1) { rect(x0 + 3, base - 18, 1, 6, M.WOOD_DK); rect(x0 + 3, base - 19, 3, 2, M.STONE_LT); }
675
+ else if (axe === 2) { line(x0 + 6, base - 7, x0 + 10, base - 5, M.WOOD_DK); rect(x0 + 10, base - 7, 2, 3, M.STONE_LT); }
676
+ else if (axe === 3) { rect(x0 + 7, base - 9, 1, 6, M.WOOD_DK); }
677
+ const sp2 = c > 2.95 && c < 3.9 ? (c - 2.95) : -1;
678
+ if (sp2 < 0) { rect(bx + 1, by - 3, 6, 3, M.WOOD_LT); rect(bx + 1, by - 3, 6, 1, M.WOOD); put(bx + 2, by - 2, M.WOOD_DK); }
679
+ else { const e = Math.min(1, sp2 / 0.9); rect(bx + 1 - Math.round(e * 3), by - 3 + Math.round(e * e * 5), 3, 3, M.WOOD); rect(bx + 4 + Math.round(e * 3), by - 3 + Math.round(e * e * 5), 3, 3, M.WOOD_LT); if (sp2 < 0.25) { put(bx - 1, by - 5, M.SNOW); put(bx + 8, by - 6, M.SNOW); put(bx + 3, by - 7, M.SNOW); } }
680
+ }
681
+ }
682
+ };
683
+
684
+ // ===== Scene: Japanese street (machiya, konbini, ramen shop, utility poles, Fuji) =====
685
+ const STR = { GY: 82, P1: 69, P2: 171 };
686
+ const paintJapaneseStreet = () => {
687
+ wins.length = 0; const { GY, P1, P2 } = STR; const xa = -ox; const xb = bw - ox; const yb = bh - oy;
688
+ // ---- far layer: low hills, Mt. Fuji, far town ----
689
+ const fuji = (x) => Math.round(34 + Math.max(0, Math.abs(x - 150) - 5) * 0.5 + (noiseB(x * 0.3, 3) - 0.5) * 1.2);
690
+ for (let x = xa; x < xb; x++) { const top = fuji(x); if (top < 72) for (let y = top; y < 76; y++) { const sn = y < top + 8 + (noiseA(x * 0.25, 1) * 4) - (x > 150 ? 2 : 0); put(x, y, sn ? (x <= 150 ? M.SNOW : M.SNOW_SH) : (x <= 150 ? M.HILL_FAR : M.HILL_NEAR)); } }
691
+ for (let x = xa; x < xb; x++) { const top = Math.round(60 + noiseA(x * 0.04 + 5, 2) * 7); for (let y = top; y < 76; y++) put(x, y, dth(x, y, 0.3) && y > top ? M.HILL_FAR : M.HILL_NEAR); }
692
+ for (let x = xa; x < xb; x += 5 + ((hash2(x, 2) * 5) | 0)) { const w = 4 + ((hash2(x, 3) * 4) | 0); const hh = 4 + ((hash2(x, 4) * 8) | 0); rect(x, 70 - hh, w, hh + 6, M.CONCRETE_SH); if (hash2(x, 6) < 0.55) addWin(x + 1, 72 - hh, 1, 1, { tiny: true }); }
693
+ // ---- A: machiya (left) ----
694
+ const AX0 = 6; const AX1 = 58;
695
+ rect(AX0, 40, AX1 - AX0, GY - 40, M.PLASTER); rect(AX1 - 4, 40, 4, GY - 40, M.PLASTER_SH);
696
+ for (let y = 41; y < 58; y++) for (let x = AX0; x < AX1; x++) if (y < 44 && dth(x, y, (44 - y) * 0.3)) put(x, y, M.PLASTER_SH);
697
+ // lattice windows on the second floor
698
+ for (const wx of [12, 38]) { rect(wx - 1, 44, 17, 13, M.WOOD_DK); rect(wx, 45, 15, 11, M.GLASS_SKY); rect(wx, 45, 15, 1, M.GLASS_SKY_HI); for (let x = wx + 1; x < wx + 15; x += 2) rect(x, 45, 1, 11, M.WOOD_LT); rect(wx, 50, 15, 1, M.WOOD_DK); addWin(wx, 45, 15, 11, {}); }
699
+ rect(AX0 - 1, 57, AX1 - AX0 + 2, 2, M.WOOD_DK);
700
+ // hip roof (kawara) with ridge and end ornaments
701
+ for (let y = 26; y < 41; y++) {
702
+ const r0 = y - 26; const l = Math.round(15 - r0 * 0.9); const r = Math.round(49 + r0 * 0.9);
703
+ for (let x = l; x <= r; x++) { const even = r0 % 2 === 0; let m = even ? M.ROOF_LT : M.ROOF; if (even && (x + ((r0 >> 1) & 1) * 2) % 4 === 0) m = M.ROOF; if (even && x - l < 6 + r0 && dth(x, y, 0.45)) m = M.ROOF_HI; put(x, y, m); }
704
+ }
705
+ rect(14, 25, 36, 1, M.ROOF_HI); rect(12, 23, 3, 3, M.ROOF_LT); rect(49, 23, 3, 3, M.ROOF_LT); put(13, 22, M.ROOF_HI); put(50, 22, M.ROOF_HI); rect(2, 41, 60, 1, M.WOOD_DK);
706
+ // lower eave
707
+ for (let y = 59; y < 63; y++) { const r0 = y - 59; for (let x = AX0 - 3 - r0; x < AX1 + 3 + r0; x++) put(x, y, r0 === 3 ? M.ROOF : (r0 % 2 === 0 ? M.ROOF_LT : M.ROOF)); } rect(AX0 - 6, 63, AX1 - AX0 + 12, 1, M.WOOD_DK);
708
+ // ground floor: wooden lattice, sliding door, noren, lantern
709
+ rect(AX0, 64, AX1 - AX0, GY - 64, M.WOOD); for (let x = AX0; x < AX1; x += 3) rect(x, 64, 1, GY - 64, M.WOOD_LT); for (let y = 64; y < GY; y += 9) rect(AX0, y, AX1 - AX0, 1, M.WOOD_DK);
710
+ rect(20, 66, 24, 16, M.WOOD_DK); rect(21, 67, 22, 15, M.GLASS_SKY); for (let x = 21; x < 43; x += 5) rect(x, 67, 1, 15, M.WOOD_DK); rect(21, 74, 22, 1, M.WOOD_DK); addWin(21, 67, 22, 7, { mull: 1 });
711
+ rect(19, 63, 26, 2, M.WOOD_DK); for (let x = 21; x < 43; x += 6) { rect(x, 65, 5, 9, M.NOREN); rect(x + 1, 67, 3, 1, M.CLOUD); rect(x + 2, 66, 1, 4, M.CLOUD); }
712
+ rect(11, 66, 1, 4, M.WOOD_DK); rect(8, 70, 7, 8, M.LANTERN); rect(8, 70, 7, 1, M.WOOD_DK); rect(8, 77, 7, 1, M.WOOD_DK); rect(10, 72, 3, 3, M.LANTERN_HI);
713
+ rect(AX1 - 12, 74, 8, 8, M.BRICK); rect(AX1 - 13, 73, 10, 1, M.BRICK_LT); for (let i = 0; i < 4; i++) { put(AX1 - 11 + i * 2, 72, M.LEAF2); put(AX1 - 12 + i * 2, 71, M.LEAF1); }
714
+ // ---- B: narrow alley with vending machines ----
715
+ rect(58, 30, 20, GY - 30, M.ASPHALT_DK); for (let y = 30; y < GY; y++) for (let x = 58; x < 78; x++) if (dth(x, y, 0.1 + (y - 30) * 0.006)) put(x, y, M.CONCRETE_SH);
716
+ const vend = (x0) => { rect(x0, 62, 8, 20, M.CONCRETE_SH); rect(x0 + 1, 63, 6, 11, M.GLASS); for (let r = 0; r < 3; r++) for (let c = 0; c < 3; c++) put(x0 + 1 + c * 2, 64 + r * 3, [M.LANTERN, M.SOFA_LT, M.CLOUD, M.BOOK_DK][(r + c * 2) % 4]); rect(x0 + 1, 76, 6, 2, M.ROOF); rect(x0 + 2, 79, 4, 2, M.WOOD_DK); };
717
+ vend(60); vend(69);
718
+ // ---- C: modern apartment + konbini ----
719
+ const CX0 = 78; const CX1 = 124; const CT = 20;
720
+ rect(CX0, CT, CX1 - CX0, GY - CT, M.CONCRETE); rect(CX1 - 4, CT, 4, GY - CT, M.CONCRETE_SH);
721
+ rect(CX0 - 1, CT - 2, CX1 - CX0 + 2, 3, M.CONCRETE_SH); rect(CX0 - 1, CT - 2, CX1 - CX0 + 2, 1, M.PLASTER);
722
+ for (const fy of [28, 44, 60]) { rect(CX0, fy, CX1 - CX0, 1, M.CONCRETE_SH); rect(CX0, fy + 1, CX1 - CX0, 1, M.PLASTER); }
723
+ for (const fy of [31, 47]) for (const wx of [82, 97, 112]) {
724
+ rect(wx - 1, fy, 12, 9, M.CONCRETE_SH); rect(wx, fy + 1, 10, 7, M.GLASS_SKY); rect(wx, fy + 1, 10, 1, M.GLASS_SKY_HI); rect(wx + 4, fy + 1, 1, 7, M.CONCRETE_SH); addWin(wx, fy + 1, 10, 7, { mull: 1 });
725
+ rect(wx - 2, fy + 9, 14, 1, M.WOOD_DK); for (let x = wx - 2; x < wx + 12; x += 2) put(x, fy + 8, M.WOOD_DK);
726
+ if (hash2(wx, fy) < 0.55) { rect(wx + 1, fy + 5, 1, 4, M.CLOUD); rect(wx + 3, fy + 4, 1, 5, M.LANTERN); rect(wx + 5, fy + 5, 1, 4, M.SOFA_LT); put(wx, fy + 4, M.WOOD_DK); rect(wx, fy + 4, 7, 1, M.WOOD_DK); }
727
+ }
728
+ rect(CX0 + 3, 52, 6, 3, M.CONCRETE_SH); rect(CX0 + 3, 52, 6, 1, M.PLASTER);
729
+ // roof-top water tank + antenna
730
+ rect(104, 11, 10, 9, M.CONCRETE_SH); rect(103, 10, 12, 2, M.CONCRETE); rect(106, 18, 1, 2, M.WOOD_DK); rect(111, 18, 1, 2, M.WOOD_DK); line(88, CT - 3, 88, 8, M.WOOD_DK); rect(85, 11, 7, 1, M.WOOD_DK); rect(86, 14, 5, 1, M.WOOD_DK);
731
+ // konbini storefront
732
+ rect(CX0 + 2, 62, CX1 - CX0 - 4, 20, M.CONCRETE_SH); rect(CX0 + 4, 66, 34, 15, M.GLASS_SKY); rect(CX0 + 4, 66, 34, 1, M.GLASS_SKY_HI);
733
+ for (let x = CX0 + 6; x < CX0 + 36; x += 5) { rect(x, 71, 4, 1, M.WOOD_LT); rect(x, 75, 4, 1, M.WOOD_LT); put(x + 1, 69, M.LANTERN); put(x + 2, 73, M.SOFA_LT); put(x, 77, M.CLOUD); }
734
+ rect(CX0 + 22, 66, 1, 15, M.CONCRETE_SH); rect(CX0 + 4, 80, 34, 1, M.CONCRETE_SH);
735
+ rect(CX0 + 2, 61, CX1 - CX0 - 4, 5, M.LANTERN); rect(CX0 + 2, 61, CX1 - CX0 - 4, 1, M.WOOD_DK); for (let x = CX0 + 5; x < CX1 - 7; x += 4) { rect(x, 62, 2, 3, M.NOREN); put(x + 2, 63, M.CLOUD); }
736
+ addWin(CX0 + 4, 67, 34, 14, { always: true, mull: 1 });
737
+ rect(CX0 + 42, 66, 2, 14, M.WOOD_DK); rect(CX0 + 40, 74, 6, 6, M.LANTERN); // standing shop sign
738
+ // ---- D: ramen-ya (right) ----
739
+ const DX0 = 126; const DX1 = xb;
740
+ rect(DX0, 30, 56, 14, M.PLASTER); rect(DX0 + 52, 30, 4, 14, M.PLASTER_SH);
741
+ rect(DX0 + 6, 33, 14, 9, M.WOOD_DK); rect(DX0 + 7, 34, 12, 7, M.GLASS_SKY); rect(DX0 + 13, 34, 1, 7, M.WOOD_DK); addWin(DX0 + 7, 34, 12, 7, { mull: 1 });
742
+ rect(DX0 + 36, 33, 14, 9, M.WOOD_DK); rect(DX0 + 37, 34, 12, 7, M.GLASS_SKY); rect(DX0 + 43, 34, 1, 7, M.WOOD_DK); addWin(DX0 + 37, 34, 12, 7, { mull: 1 });
743
+ for (let y = 22; y < 31; y++) { const r0 = y - 22; for (let x = DX0 - 3 + r0 * 1; x < DX0 + 59 - r0; x++) { const even = r0 % 2 === 0; let m = even ? M.ROOF_LT : M.ROOF; if (even && (x + ((r0 >> 1) & 1) * 2) % 4 === 0) m = M.ROOF; put(x, y, m); } }
744
+ rect(DX0 + 6, 21, 44, 1, M.ROOF_HI);
745
+ rect(DX0 - 4, 44, 66, 8, M.ROOF); for (let x = DX0 - 4; x < DX0 + 62; x += 4) { put(x, 45, M.ROOF_LT); put(x + 2, 47, M.ROOF_LT); put(x, 49, M.ROOF_LT); } rect(DX0 - 4, 52, 66, 1, M.WOOD_DK);
746
+ rect(DX0, 53, 56, GY - 53, M.WOOD); rect(DX0 + 52, 53, 4, GY - 53, M.WOOD_DK); for (let x = DX0; x < DX0 + 56; x += 4) rect(x, 53, 1, GY - 53, M.WOOD_LT);
747
+ rect(DX0 + 22, 56, 20, 26, M.WOOD_DK); rect(DX0 + 23, 57, 18, 25, M.GLASS_SKY); rect(DX0 + 32, 57, 1, 25, M.WOOD_DK); rect(DX0 + 23, 66, 18, 1, M.WOOD_DK); addWin(DX0 + 23, 57, 18, 9, { mull: 1 });
748
+ rect(DX0 + 4, 58, 14, 14, M.WOOD_DK); rect(DX0 + 5, 59, 12, 12, M.GLASS_SKY); rect(DX0 + 5, 59, 12, 1, M.GLASS_SKY_HI); addWin(DX0 + 5, 59, 12, 12, { mull: 1 }); rect(DX0 + 4, 72, 14, 2, M.WOOD_DK);
749
+ rect(DX0 + 20, 53, 24, 3, M.NOREN); for (let x = DX0 + 22; x < DX0 + 43; x += 6) { rect(x, 56, 5, 9, M.NOREN); rect(x + 2, 58, 1, 5, M.CLOUD); rect(x + 1, 60, 3, 1, M.CLOUD); }
750
+ rect(DX0 + 45, 62, 6, 12, M.WOOD_DK); rect(DX0 + 46, 63, 4, 10, M.LANTERN); put(DX0 + 47, 65, M.NOREN); put(DX0 + 48, 67, M.NOREN); put(DX0 + 47, 69, M.NOREN);
751
+ rect(DX0 - 2, 56, 1, 4, M.WOOD_DK); rect(DX0 - 5, 60, 7, 9, M.LANTERN); rect(DX0 - 5, 60, 7, 1, M.WOOD_DK); rect(DX0 - 5, 68, 7, 1, M.WOOD_DK); rect(DX0 - 3, 62, 3, 4, M.LANTERN_HI); put(DX0 - 2, 63, M.NOREN);
752
+ rect(DX0 + 40, 20, 3, 3, M.CONCRETE_SH); rect(DX0 + 39, 19, 5, 1, M.CONCRETE);
753
+ // ---- ground: sidewalk, curb, road ----
754
+ for (let y = GY; y < yb; y++) for (let x = xa; x < xb; x++) {
755
+ let m;
756
+ if (y < GY + 7) { m = ((x + (y & 1) * 3) % 6 === 0 && y !== GY) ? M.STONE : (y === GY ? M.STONE : M.STONE_LT); if (y > GY + 1 && dth(x, y, 0.1)) m = M.STONE; }
757
+ else if (y < GY + 9) m = y === GY + 7 ? M.STONE_LT : M.STONE;
758
+ else { const n = noiseA(x * 0.05 + 2, y * 0.3) + noiseB(x * 0.35, y * 0.5) * 0.3; m = M.ASPHALT; if (n > 0.95 && dth(x, y, 0.6)) m = M.ASPHALT_LT; else if (n < 0.35 && dth(x, y, 0.4)) m = M.ASPHALT_DK; }
759
+ put(x, y, m);
760
+ }
761
+ for (let x = xa; x < xb; x += 24) rect(x, 101, 12, 1, M.LINE); for (let x = xa + 4; x < xb; x += 24) rect(x, 102, 8, 1, M.LINE);
762
+ disc(146, 103, 4, M.ASPHALT_DK); disc(146, 103, 3, M.ASPHALT_LT); for (let x = 143; x < 150; x += 2) put(x, 103, M.ASPHALT_DK); rect(144, 102, 5, 1, M.ASPHALT_DK);
763
+ for (let x = 156; x < 188; x += 7) { rect(x, 92, 4, 1, M.LINE); rect(x, 94, 4, 1, M.LINE); }
764
+ for (let i = 0; i < 6; i++) { const x = xa + ((hash2(i, 41) * bw) | 0); const y = 106 + ((hash2(i, 42) * 8) | 0); rect(x, y, 8, 2, M.ASPHALT_LT); rect(x + 1, y + 2, 6, 1, M.ASPHALT_DK); }
765
+ rect(30, 107, 14, 1, M.LINE); rect(30, 109, 14, 1, M.LINE); rect(76, 108, 14, 1, M.LINE); rect(76, 110, 14, 1, M.LINE);
766
+ // parked kei van
767
+ { const x0 = 90; const y0 = 91;
768
+ rect(x0 + 1, y0 + 13, 31, 1, M.ASPHALT_DK);
769
+ rect(x0 + 2, y0 + 3, 29, 8, M.WALL); rect(x0 + 27, y0 + 3, 4, 8, M.WALL_SH); rect(x0 + 5, y0, 22, 4, M.WALL); rect(x0 + 5, y0, 22, 1, M.PLASTER);
770
+ rect(x0 + 6, y0 + 1, 8, 3, M.GLASS_SKY); put(x0 + 6, y0 + 1, M.GLASS_SKY_HI); put(x0 + 7, y0 + 1, M.GLASS_SKY_HI); rect(x0 + 16, y0 + 1, 9, 3, M.GLASS_SKY); rect(x0 + 15, y0 + 1, 1, 3, M.WALL_SH);
771
+ rect(x0 + 2, y0 + 8, 29, 1, M.BAND); rect(x0 + 2, y0 + 10, 29, 1, M.WALL_SH); rect(x0, y0 + 9, 3, 2, M.STONE); put(x0 + 2, y0 + 5, M.LANTERN); put(x0 + 2, y0 + 6, M.LANTERN); put(x0 + 30, y0 + 5, M.BRICK_LT);
772
+ for (const wx of [x0 + 8, x0 + 24]) { disc(wx, y0 + 11, 3, M.ROOF); disc(wx, y0 + 11, 1, M.STONE_LT); }
773
+ rect(x0 + 18, y0 + 4, 1, 5, M.WALL_SH); put(x0 + 17, y0 + 6, M.STONE_LT); }
774
+ // ---- utility poles + street lamp ----
775
+ for (const px0 of [P1, P2]) {
776
+ rect(px0, 10, 2, GY + 6 - 10, M.WOOD_DK); rect(px0, 10, 1, GY + 6 - 10, M.WOOD); rect(px0 - 7, 14, 16, 2, M.WOOD_DK); rect(px0 - 5, 21, 12, 2, M.WOOD_DK);
777
+ for (const dx of [-6, -2, 3, 7]) { put(px0 + dx, 13, M.STONE_LT); } for (const dx of [-4, 5]) put(px0 + dx, 20, M.STONE_LT);
778
+ rect(px0 + 2, 28, 5, 8, M.CONCRETE_SH); rect(px0 + 2, 28, 5, 1, M.CONCRETE); rect(px0 + 2, 36, 5, 1, M.WOOD_DK);
779
+ for (let y = 44; y < GY; y += 7) rect(px0 - 1, y, 4, 1, M.WOOD_LT);
780
+ }
781
+ rect(P2 - 12, 31, 12, 1, M.WOOD_DK); rect(P2 - 15, 32, 5, 2, M.CONCRETE_SH); rect(P2 - 14, 34, 3, 1, M.LANTERN);
782
+ const wire = (xs, ys, xe, ye, sag) => { for (let x = xs; x <= xe; x++) { const u = (x - xs) / (xe - xs); put(x, Math.round(mix(ys, ye, u) + sag * 4 * u * (1 - u)), M.NOREN); } };
783
+ for (const [dx0, y0] of [[-6, 13], [-2, 13], [3, 13], [7, 13], [-4, 20], [5, 20]]) { wire(xa, y0 + 1, P1 + dx0, y0, 3); wire(P1 + dx0, y0, P2 + dx0, y0, 7); wire(P2 + dx0, y0, xb, y0 + 2, 3); }
784
+ // ---- potted plants, signboard, bicycle ----
785
+ rect(100, 78, 6, 4, M.BRICK); rect(99, 77, 8, 1, M.BRICK_LT); put(101, 75, M.LEAF2); put(103, 74, M.LEAF3); put(104, 76, M.LEAF2); put(102, 76, M.LEAF1);
786
+ for (const bx of [53, 82]) { /* bike wheels */ }
787
+ const bike = (bx) => { for (const wx of [bx, bx + 11]) for (let a = 0; a < 16; a++) put(Math.round(wx + 3.5 * Math.cos(a * 0.3927)), Math.round(GY + 4 + 3.5 * Math.sin(a * 0.3927)), M.ROOF); line(bx, GY + 4, bx + 5, GY - 1, M.WOOD_DK); line(bx + 5, GY - 1, bx + 11, GY + 4, M.WOOD_DK); line(bx + 5, GY - 1, bx + 4, GY + 4, M.WOOD_DK); line(bx + 4, GY + 4, bx, GY + 4, M.WOOD_DK); rect(bx + 4, GY - 3, 3, 1, M.WOOD_DK); rect(bx + 10, GY - 3, 3, 1, M.WOOD_DK); line(bx + 11, GY + 4, bx + 10, GY - 3, M.WOOD_DK); rect(bx + 11, GY - 1, 4, 3, M.ROOF_LT); };
788
+ bike(4);
789
+ };
790
+ const catSit = makeSprite(['C.C...', 'CCCC..', 'CCCCC.', '.CCCCC', '.CCCCC', '.CC.CC']);
791
+ const catSleep = makeSprite(['......', '......', '.CCCC.', 'CCCCCC', 'CCCCCC']);
792
+ const walkerP = { x: 24, face: 1, step: 0, wait: 2.5, goal: 150 };
793
+ const liveStreet = (h, dt) => {
794
+ const { GY, P1, P2 } = STR; const t = anim; const lamp = lampAmt(h); const xa = -ox; const xb = bw - ox;
795
+ // windows light up one by one; shop windows (always) follow the lamp curve
796
+ drawWins(h, winFrac(h), 0.9);
797
+ // ramen steam
798
+ if (!calm) { fx.t += dt * (lamp > 0.3 ? 3 : 1.2); while (fx.t >= 1) { fx.t -= 1; fx.smoke.push({ x: 167 + Math.random() * 2, y: 18, t: 0, seed: Math.random() * 10 }); } }
799
+ for (let i = fx.smoke.length - 1; i >= 0; i--) { const s = fx.smoke[i]; s.t += dt; s.y -= dt * (6 - s.t); s.x += dt * (1.6 + Math.sin(s.t * 1.9 + s.seed) * 2.2); if (s.t > 4.2 || s.y < -4) { fx.smoke.splice(i, 1); continue; } const size = 1 + Math.floor(s.t * 0.8); for (let dy = 0; dy < size; dy++) for (let dx = 0; dx < size; dx++) { const x = Math.round(s.x) + dx; const y = Math.round(s.y) + dy; if (bayerAt(x, y) < 15 - s.t * 3) put(x, y, s.t > 2.3 ? M.SMOKE_DK : M.SMOKE); } }
800
+ // street lamp, lanterns and light pools
801
+ const lampOn = lamp > 0.05;
802
+ if (lampOn) {
803
+ rect(P2 - 14, 34, 3, 1, M.LANTERN_HI); emit(P2 - 13, 34, 190 * lamp, 2); pool(P2 - 13, 92, 20, 8, lamp * 0.8);
804
+ pool(100, 90, 34, 9, lamp * 0.95); pool(100, 80, 26, 12, lamp * 0.5); pool(30, 84, 24, 9, lamp * 0.7); pool(160, 88, 26, 9, lamp * 0.8); pool(70, 86, 10, 8, lamp * 0.6);
805
+ for (const [lx, ly, lw, lh] of [[8, 70, 7, 8], [121, 60, 7, 9], [171, 62, 6, 12]]) for (let j = 1; j < lh - 1; j++) for (let i = 1; i < lw - 1; i += 2) emit(lx + i, ly + j, 130 * lamp, 2);
806
+ homeLight(100, 74, 26000 * lamp);
807
+ }
808
+ // vending machines glow
809
+ if (lampOn) for (const vx of [60, 69]) { rect(vx + 1, 63, 6, 11, M.LIT); for (let r = 0; r < 3; r++) for (let c = 0; c < 3; c++) put(vx + 1 + c * 2, 64 + r * 3, [M.LANTERN_HI, M.LIT_HI, M.CLOUD, M.LIT_DK][(r + c * 2) % 4]); for (let j = 0; j < 11; j += 2) emit(vx + 3, 63 + j, 90 * lamp, 2); }
810
+ // cat on the curb (sleeps at night)
811
+ const asleep = h >= 22.5 || h < 6.5; const cx0 = 112; const cy0 = GY - 1;
812
+ if (asleep) blit(catSleep, cx0, cy0 - 4); else { blit(catSit, cx0, cy0 - 5); const tail = calm ? 0 : Math.round(Math.sin(t * 2.4)); put(cx0 + 6, cy0 - 1 + tail, M.CAT); put(cx0 + 7, cy0 - 2 + tail, M.CAT_DK); }
813
+ // passer-by
814
+ const W = walkerP; const away = h >= 23.4 || h < 6.4;
815
+ if (calm) { W.x = 40; }
816
+ else if (!away) {
817
+ if (W.wait > 0) { W.wait -= dt; W.step = 0; }
818
+ else { const d = W.goal - W.x; if (Math.abs(d) < 0.8) { W.wait = 1.8 + Math.random() * 2.5; W.goal = [18, 46, 96, 122, 150, 184][(Math.random() * 6) | 0]; } else { const mv = Math.min(Math.abs(d), 12 * dt); W.x += Math.sign(d) * mv; W.face = Math.sign(d); W.step += mv * 0.28; } }
819
+ }
820
+ if (!away || calm) { const moving = !calm && W.wait <= 0; blit(moving ? SPR.walk[Math.floor(W.step) % 4] : (!calm && Math.sin(t * 0.8) > 0.93 ? SPR.blink : SPR.stand), Math.round(W.x) - 3, GY + 4 - 12, W.face < 0); }
578
821
  };
579
822
 
823
+ // ===== Scene: apartment (city window, sofa, cat on the bench, lamps) =====
824
+ const APT = { FL: 80, WX0: 17, WX1: 73, WY0: 12, WY1: 61 };
825
+ const roomLamp = (h) => (h >= 12 ? smoothstep(17.6, 19.2, h) * (1 - smoothstep(23.2, 23.5, h)) : smoothstep(5.2, 5.6, h) * (1 - smoothstep(6.8, 7.4, h)));
826
+ const catBench = makeSprite(['..C.C...', '.CCCCC..', 'CCCCCCC.', 'CCCCCCCC', 'QQQQQQQQ']);
827
+ const catBenchB = makeSprite(['..C.C...', '.CCCCC..', 'CCCCCCC.', 'CCCCCCCC', 'CQQQQQQC']);
828
+ const paintApartment = () => {
829
+ wins.length = 0; const { FL, WX0, WX1, WY0, WY1 } = APT; const xa = -ox; const xb = bw - ox; const yb = bh - oy;
830
+ // ---- walls: soft vertical stripes, shadow under the ceiling, wainscot ----
831
+ for (let y = -oy; y < FL; y++) for (let x = xa; x < xb; x++) {
832
+ let m = M.WALL; if ((x + 3) % 12 === 0) m = M.WALL_SH; if (y < 6 && dth(x, y, (6 - y) * 0.16)) m = M.WALL_SH; if (y > 66 && y < FL - 2) m = M.PLASTER; if (y === 66) m = M.WALL_SH; if (y > 66 && (x + 3) % 12 === 0) m = M.PLASTER_SH; put(x, y, m);
833
+ }
834
+ for (let x = xa; x < xb; x++) { put(x, FL - 2, M.WOOD_LT); put(x, FL - 1, M.WOOD_DK); }
835
+ // ---- floor planks (pale oak: tan by day, amber at golden hour) ----
836
+ for (let y = FL; y < yb; y++) for (let x = xa; x < xb; x++) {
837
+ const row = Math.floor((y - FL) / 5); const seam = (y - FL) % 5 === 0; const vs = (x + Math.floor((((Math.sin(row * 12.9898) * 43758.5453) % 1) + 1) % 1 * 38)) % 38 === 0;
838
+ let m = M.PLANK; if (seam || vs) m = M.PATH_DK; else if (((x * 3 + row * 11) % 19) < 2) m = M.SAND_LT; else if (y < FL + 4 && dth(x, y, 0.5)) m = M.PATH_DK; put(x, y, m);
839
+ }
840
+ // ---- window: sky shows through the glass; skyline layers on top ----
841
+ rect(WX0 - 3, WY0 - 3, WX1 - WX0 + 6, WY1 - WY0 + 6, M.WOOD_DK); rect(WX0, WY0, WX1 - WX0, WY1 - WY0, 0);
842
+ for (let x = WX0; x < WX1; x += 4 + ((hash2(x, 21) * 4) | 0)) { const w = 4 + ((hash2(x, 22) * 4) | 0); const hh = 7 + ((hash2(x, 23) * 12) | 0); rect(x, WY1 - hh, Math.min(w, WX1 - x), hh, M.HILL_FAR); }
843
+ for (let x = WX0 + 1; x < WX1; ) { const w = 7 + ((hash2(x, 24) * 5) | 0); const hh = 12 + ((hash2(x, 25) * 17) | 0); const ww = Math.min(w, WX1 - x); rect(x, WY1 - hh, ww, hh, M.CONCRETE_SH); rect(x, WY1 - hh, ww, 1, M.CONCRETE); for (let wy = WY1 - hh + 3; wy < WY1 - 2; wy += 4) for (let wx = x + 2; wx < x + ww - 1; wx += 3) { put(wx, wy, M.GLASS); if (hash2(wx * 3, wy * 5) < 0.75) addWin(wx, wy, 1, 1, { tiny: true }); } x += ww + 1; }
844
+ for (let y = 20; y < WY1; y++) { put(63, y, M.HILL_NEAR); if (y % 6 === 0 && y > 26) { rect(63 - Math.floor((y - 20) / 6), y, 1 + 2 * Math.floor((y - 20) / 6) + 1, 1, M.HILL_NEAR); } }
845
+ rect(WX0 + 17, WY0, 2, WY1 - WY0, M.WOOD_DK); rect(WX0 + 37, WY0, 2, WY1 - WY0, M.WOOD_DK); rect(WX0, WY0 + 20, WX1 - WX0, 2, M.WOOD_DK); rect(WX0, WY0, WX1 - WX0, 1, M.WOOD_LT);
846
+ rect(WX0 - 5, WY1 + 2, WX1 - WX0 + 10, 3, M.WOOD_LT); rect(WX0 - 5, WY1 + 5, WX1 - WX0 + 10, 1, M.WOOD_DK);
847
+ // curtains with folds and tie-backs
848
+ for (const cx of [5, 76]) { for (let y = 8; y < 68; y++) for (let x = cx; x < cx + 8; x++) put(x, y, (x - cx) % 3 === 1 ? M.SHIRT_DK : M.SHIRT); rect(cx, 44, 8, 2, M.STONE_LT); rect(cx - 1, 7, 10, 2, M.WOOD_DK); for (let x = cx; x < cx + 8; x += 2) put(x, 67, M.SHIRT_DK); }
849
+ // window bench + cushions
850
+ rect(16, 70, 62, 3, M.WOOD_LT); rect(16, 73, 62, 8, M.WOOD); rect(16, 73, 62, 1, M.WOOD_DK); rect(16, 80, 62, 1, M.WOOD_DK); for (const dx of [22, 44, 66]) { rect(dx, 75, 8, 4, M.WOOD_DK); rect(dx + 3, 76, 2, 1, M.WOOD_LT); } rect(18, 81, 3, 4, M.WOOD_DK); rect(73, 81, 3, 4, M.WOOD_DK);
851
+ rect(20, 65, 12, 5, M.SOFA_LT); rect(20, 65, 12, 1, M.SOFA); rect(21, 66, 10, 1, M.SOFA_LT); rect(22, 69, 8, 1, M.SOFA_DK);
852
+ rect(62, 64, 12, 6, M.SOFA); rect(62, 64, 12, 1, M.SOFA_LT); rect(62, 69, 12, 1, M.SOFA_DK);
853
+ paintBush(41, 62, 4, 3, 1202); rect(37, 66, 9, 4, M.BRICK); rect(36, 65, 11, 1, M.BRICK_LT);
854
+ // ---- plant between window and sofa ----
855
+ paintBush(91, 64, 7, 9, 1201); rect(86, 76, 11, 9, M.BRICK); rect(85, 75, 13, 2, M.BRICK_LT); rect(86, 84, 11, 1, M.WOOD_DK); line(91, 76, 91, 68, M.STEM);
856
+ // ---- pendant, frames, clock, shelf ----
857
+ rect(130, -oy, 1, 16 + oy, M.WOOD_DK); for (let y = 16; y < 23; y++) { const hw = 2 + Math.round((y - 16) * 0.9); for (let x = 130 - hw; x <= 130 + hw; x++) put(x, y, y === 16 ? M.WOOD_DK : M.LANTERN); } rect(128, 22, 5, 1, M.WOOD_DK);
858
+ rect(106, 22, 17, 15, M.WOOD_DK); rect(108, 24, 13, 11, M.CLOUD_SH); for (let y = 0; y < 6; y++) for (let x = -y; x <= y; x++) put(114 + x, 28 + y, y < 2 ? M.SNOW : M.HILL_FAR); rect(108, 31, 13, 4, M.HILL_NEAR); disc(118, 26, 1, M.LANTERN); put(114, 28, M.SNOW);
859
+ rect(137, 28, 11, 9, M.WOOD_DK); rect(139, 30, 7, 5, M.CLOUD); rect(140, 33, 5, 2, M.LEAF1); put(142, 31, M.LEAF2);
860
+ disc(158, 28, 6, M.WOOD_DK); disc(158, 28, 5, M.WALL); put(158, 24, M.WOOD_DK); put(158, 32, M.WOOD_DK); put(154, 28, M.WOOD_DK); put(162, 28, M.WOOD_DK); line(158, 28, 158, 25, M.WOOD_DK); line(158, 28, 161, 29, M.WOOD_DK);
861
+ rect(104, 44, 58, 2, M.WOOD_DK); rect(104, 43, 58, 1, M.WOOD_LT);
862
+ for (let x = 108; x < 124; x += 3) { const hh = 6 + ((hash2(x, 71) * 4) | 0); rect(x, 43 - hh, 2, hh, [M.BOOK, M.BOOK_DK, M.LEAF1, M.WOOD_LT, M.ROOF_LT][(x / 3) % 5 | 0]); }
863
+ rect(138, 36, 7, 7, M.BRICK); rect(137, 35, 9, 1, M.BRICK_LT); paintBush(141, 33, 4, 3, 1203);
864
+ rect(150, 38, 8, 5, M.BOOK_DK); rect(151, 39, 6, 1, M.BOOK);
865
+ // ---- sofa ----
866
+ rect(103, 54, 68, 16, M.SOFA); rect(103, 54, 68, 1, M.SOFA_LT); rect(103, 55, 68, 1, M.SOFA_LT); for (let x = 103; x < 171; x += 11) rect(x, 56, 1, 13, M.SOFA_DK);
867
+ rect(103, 69, 68, 12, M.SOFA_LT); rect(103, 69, 68, 1, M.SOFA); rect(103, 79, 68, 2, M.SOFA_DK); for (let x = 103; x < 171; x += 22) rect(x + 22, 70, 1, 9, M.SOFA);
868
+ rect(97, 58, 9, 24, M.SOFA); rect(97, 58, 9, 2, M.SOFA_LT); rect(98, 58, 1, 22, M.SOFA_LT); rect(97, 80, 9, 2, M.SOFA_DK); rect(168, 58, 9, 24, M.SOFA); rect(168, 58, 9, 2, M.SOFA_LT); rect(168, 80, 9, 2, M.SOFA_DK); rect(175, 60, 2, 22, M.SOFA_DK);
869
+ rect(100, 82, 2, 3, M.WOOD_DK); rect(172, 82, 2, 3, M.WOOD_DK);
870
+ rect(108, 58, 12, 11, M.SHIRT); rect(108, 58, 12, 1, M.SHIRT); rect(108, 68, 12, 1, M.SHIRT_DK); put(109, 59, M.LANTERN_HI); put(114, 63, M.SHIRT_DK); put(113, 62, M.SHIRT_DK);
871
+ rect(151, 58, 12, 11, M.CLOUD_SH); rect(151, 58, 12, 1, M.CLOUD); rect(151, 68, 12, 1, M.U2 !== undefined ? M.STAR_DIM : M.CLOUD_SH); for (let y = 60; y < 68; y += 3) rect(152, y, 10, 1, M.STAR_DIM);
872
+ // ---- rug and coffee table ----
873
+ for (let y = 87; y < 109; y++) { const t = (y - 87) / 21; const l = Math.round(98 - t * 6); const r = Math.round(180 + t * 6); for (let x = l; x <= r; x++) { const edge = x < l + 2 || x > r - 2 || y < 89 || y > 106; put(x, y, edge ? M.RUG_LT : (((x + y) % 8 === 0) || ((x - y + 400) % 8 === 0)) ? M.RUG_LT : M.RUG); } }
874
+ for (let x = 102; x < 176; x += 4) { put(x, 87, M.RUG); put(x, 108, M.RUG); }
875
+ rect(110, 93, 44, 4, M.WOOD_LT); rect(110, 97, 44, 1, M.WOOD_DK); rect(113, 98, 3, 8, M.WOOD_DK); rect(148, 98, 3, 8, M.WOOD_DK); rect(112, 108, 40, 1, M.RUG);
876
+ rect(137, 90, 10, 2, M.BOOK_DK); rect(138, 88, 8, 2, M.BOOK); rect(139, 87, 6, 1, M.BOOK_DK); put(121, 91, M.CLOUD); rect(120, 91, 4, 2, M.CLOUD); put(124, 92, M.CLOUD); rect(120, 92, 4, 1, M.CLOUD_SH);
877
+ // ---- floor lamp ----
878
+ rect(186, 46, 2, 40, M.WOOD_DK); rect(182, 84, 10, 2, M.WOOD_DK); for (let y = 36; y < 46; y++) { const hw = 3 + Math.round((y - 36) * 0.5); for (let x = 187 - hw; x <= 187 + hw; x++) put(x, y, M.LANTERN); } rect(183, 36, 8, 1, M.WOOD_DK); rect(182, 45, 10, 1, M.WOOD_DK);
879
+ // ---- baseboard heater + wall socket + small details ----
880
+ rect(180, 66, 2, 2, M.WOOD_DK);
881
+ };
882
+ const aptP = { blink: 2, stretch: 0, tc: 0, mug: [], ac: 3 };
883
+ const liveApartment = (h, dt) => {
884
+ const { FL, WX0, WX1, WY0, WY1 } = APT; const t = anim; const room = roomLamp(h); const night = nightF(h); const xa = -ox; const xb = bw - ox;
885
+ // window: lit city windows, tower beacon, a plane
886
+ drawWins(h, clamp(winFrac(h) * 1.25 + (night > 0.5 ? 0.2 : 0)));
887
+ if (night > 0.35) { const blink = calm ? true : Math.sin(t * 2.2) > 0; if (blink) { put(63, 19, M.LANTERN_HI); emit(63, 19, 150, 2); } else put(63, 19, M.LANTERN); const px0 = Math.round(WX0 + ((t * 4) % 120) - 20); if (!calm && px0 > WX0 && px0 < WX1 - 1 && Math.floor(t * 3) % 2 === 0) { put(px0, WY0 + 8, M.LIT_HI); } }
888
+ // sunbeam from the window across the floor
889
+ const sp = sunPos(h);
890
+ if (sp && sp.y < 62 && h > 6.8 && h < 17.4) {
891
+ const skew = (sp.x - 96) * 0.55; const k0 = clamp(1 - sp.y / 62, 0.35, 1);
892
+ for (let y = FL + 2; y < 112; y++) { const u = (y - FL) / 30; for (let x = Math.round(WX0 + 4 + skew * u); x < Math.round(WX1 - 4 + skew * u); x++) { if (x < 12) continue; const m = get(x, y); if ((m === M.PLANK || m === M.SAND_LT || m === M.PATH_DK) && dth(x, y, 0.8 * k0 * (1 - u * 0.35))) put(x, y, M.WALL); } }
893
+ }
894
+ // lamps
895
+ const floorLamp = room; const pend = room * (h >= 12 || h < 7 ? 1 : 0);
896
+ if (floorLamp > 0.05) {
897
+ for (let y = 37; y < 45; y++) { const hw = 3 + Math.round((y - 36) * 0.5); for (let x = 187 - hw + 1; x <= 187 + hw - 1; x++) put(x, y, y < 40 ? M.LANTERN_HI : M.LANTERN); }
898
+ for (let y = 37; y < 45; y += 2) for (let x = 183; x < 192; x += 2) emit(x, y, 210 * floorLamp, 0);
899
+ pool(187, 60, 21, 20, floorLamp * 0.75); pool(182, 90, 20, 8, floorLamp * 0.6); homeLight(187, 42, 14000 * floorLamp);
900
+ }
901
+ if (pend > 0.05) { for (let y = 17; y < 23; y++) { const hw = 2 + Math.round((y - 16) * 0.9); for (let x = 130 - hw + 1; x <= 130 + hw - 1; x++) put(x, y, M.LANTERN_HI); } for (let y = 17; y < 22; y += 2) for (let x = 127; x < 134; x += 2) emit(x, y, 190 * pend, 0); pool(130, 24, 26, 17, pend * 0.65); pool(132, 92, 24, 9, pend * 0.7); homeLight(130, 20, 11000 * pend); }
902
+ // cat on the window bench (breathes; sleeps through the night)
903
+ const cb = calm ? false : Math.sin(t * 1.3) > 0.2; blit(cb ? catBenchB : catBench, 46, 65); if (!calm && Math.sin(t * 0.5) > 0.55) put(54, 66 + (Math.sin(t * 3) > 0 ? 1 : 0), M.CAT_DK);
904
+ if (night > 0.2 || (h > 9 && h < 14)) { /* zzz */ }
905
+ // mug steam
906
+ if (!calm) { aptP.tc += dt * 2.2; while (aptP.tc >= 1) { aptP.tc -= 1; aptP.mug.push({ x: 122 + Math.random() * 1.5, y: 90, t: 0, seed: Math.random() * 7 }); } }
907
+ for (let i = aptP.mug.length - 1; i >= 0; i--) { const s = aptP.mug[i]; s.t += dt; s.y -= dt * 4.5; s.x += Math.sin(s.t * 3 + s.seed) * dt * 3; if (s.t > 1.7) { aptP.mug.splice(i, 1); continue; } if (bayerAt(Math.round(s.x), Math.round(s.y)) < 15 - s.t * 7) put(Math.round(s.x), Math.round(s.y), M.SMOKE); }
908
+ // reader on the sofa (away while asleep)
909
+ const present = !(h >= 23.4 || h < 6.4);
910
+ if (present) {
911
+ aptP.blink -= calm ? 0 : dt; const c = calm ? 0 : (t % 6.2) / 6.2; let spr = SPR.bust;
912
+ if (c > 0.16 && c < 0.24) spr = SPR.bustHalf; else if (c >= 0.24 && c < 0.46) spr = SPR.bustUp; else if (c >= 0.46 && c < 0.54) spr = SPR.bustHalf; else if (!calm && Math.sin(t * 0.7) > 0.93) spr = SPR.bustBlink;
913
+ blit(spr, 131, 49); rect(122, 57, 26, 12, M.SHIRT_DK); rect(122, 57, 26, 1, M.SHIRT); for (let x = 124; x < 146; x += 5) put(x, 62, M.SHIRT); rect(131, 57 + (spr === SPR.bustUp ? 2 : 0), 8, 5, M.BOOK); rect(135, 57 + (spr === SPR.bustUp ? 2 : 0), 1, 5, M.BOOK_DK);
914
+ if (room > 0.3 && spr !== SPR.bustUp) { for (let x = 132; x < 139; x += 2) emit(x, 59, 40 * room, 2); }
915
+ }
916
+ };
917
+
918
+ const scene = ['countryside', 'japanese-street', 'apartment', 'seaside', 'mountain-cabin'].includes(dataset.scene) ? dataset.scene : 'countryside';
919
+ const LIVE = { seaside: liveSeaside, 'mountain-cabin': liveCabin, 'japanese-street': liveStreet, apartment: liveApartment };
920
+ const drawSceneLive = (h, dt) => { const fn = LIVE[scene]; if (fn) fn(h, dt); };
921
+
922
+
580
923
  const buildBase = () => {
581
924
  matBuf.fill(0); if (scene === 'countryside') { paintGround(); paintFence(); paintBush(22, 78, 16, 8, 61); paintBush(176, 80, 14, 7, 62); paintBush(-6, 81, 11, 6, 63); paintBush(204, 81, 10, 5, 64); paintTrunk(); paintHouse(); paintFlowers(96, 126, 85, 92, 26, 91); paintFlowers(14, 38, 86, 96, 16, 92); paintFlowers(150, 205, 90, 106, 24, 93); paintFlowers(-14, 14, 88, 104, 12, 95); paintFlowers(40, 62, 90, 104, 12, 94); buildCanopy(); } else { canopy = []; if (scene === 'japanese-street') paintJapaneseStreet(); else if (scene === 'apartment') paintApartment(); else if (scene === 'seaside') paintSeaside(); else paintMountainCabin(); } baseBuf.set(matBuf);
582
925
  const r = seeded(808); stars = [];
@@ -695,19 +1038,60 @@ function mount(root) {
695
1038
  for (const c of sources) { c.w = 0; c.x = 0; c.y = 0; }
696
1039
  lampFlick = calm ? 1 : 0.95 + 0.05 * Math.sin(anim * 13) * Math.sin(anim * 5.3);
697
1040
  if (!calm) { birds.next -= dt; if (birds.t >= 0) { birds.t += dt; if (birds.t > 9) birds.t = -1; } else if (birds.next <= 0) { birds.t = 0; birds.next = 12 + Math.random() * 14; birds.y = 10 + Math.random() * 24; birds.dir = Math.random() < 0.5 ? 1 : -1; } shoot.next -= dt; if (shoot.t >= 0) { shoot.t += dt; if (shoot.t > 0.6) shoot.t = -1; } else if (shoot.next <= 0) { shoot.t = 0; shoot.next = 9 + Math.random() * 16; shoot.x = 10 + Math.random() * 130; shoot.y = 2 + Math.random() * 20; } }
698
- drawSky(h); if (scene === 'countryside') { const plan = stepResident(dt, h, hurry); drawShadows(h); const state = drawWindow(h); drawDoor(); drawLights(state, h); drawResident(plan, h); drawCanopy(); drawParticles(h, dt); } else { drawSceneResident(dt); drawParticles(h, dt); }
1041
+ drawSky(h); if (scene === 'countryside') { const plan = stepResident(dt, h, hurry); drawShadows(h); const state = drawWindow(h); drawDoor(); drawLights(state, h); drawResident(plan, h); drawCanopy(); drawParticles(h, dt); } else { drawSceneLive(h, dt); drawParticles(h, dt); }
699
1042
  pickKeys(h); const ta = table[keyA]; const tb = table[keyB]; const sa = skyKeys[keyA]; const sb = skyKeys[keyB]; const p = keyP * 16; const n = bw * bh;
700
1043
  for (let i = 0; i < n; i++) { const m = matBuf[i]; const useB = thrA[i] < p; const idx = m === 0 ? (useB ? sb[i] : sa[i]) : (useB ? tb : ta)[varBuf[i] * NM + m]; pixels[i] = palette32[idx]; }
701
1044
  };
702
1045
  const GRID_W = 72; let GRID_H = 44; let glowRaw = new Float32Array(1); let glowNearBuf = new Float32Array(1); let glowFarBuf = new Float32Array(1); let glowTemp = new Float32Array(1);
703
1046
  const boxBlur = (src, dst, w, h, r) => { const size = r * 2 + 1; for (let y = 0; y < h; y++) { const row = y * w * 4; for (let c = 0; c < 3; c++) { let sum = 0; for (let x = -r; x <= r; x++) sum += src[row + clamp(x, 0, w - 1) * 4 + c]; for (let x = 0; x < w; x++) { dst[row + x * 4 + c] = sum / size; sum += src[row + clamp(x + r + 1, 0, w - 1) * 4 + c] - src[row + clamp(x - r, 0, w - 1) * 4 + c]; } } } for (let x = 0; x < w; x++) { for (let c = 0; c < 3; c++) { let sum = 0; for (let y = -r; y <= r; y++) sum += dst[clamp(y, 0, h - 1) * w * 4 + x * 4 + c]; for (let y = 0; y < h; y++) { src[y * w * 4 + x * 4 + c] = sum / size; sum += dst[clamp(y + r + 1, 0, h - 1) * w * 4 + x * 4 + c] - dst[clamp(y - r, 0, h - 1) * w * 4 + x * 4 + c]; } } } };
704
1047
  let gl = null;
705
- try { gl = canvas.getContext('webgl2', { alpha: true, premultipliedAlpha: true, antialias: false, depth: false, stencil: false, powerPreference: 'high-performance' }); } catch { gl = null; }
1048
+ let webglContextError = '';
1049
+ try {
1050
+ gl = canvas.getContext('webgl2', { alpha: true, premultipliedAlpha: true, antialias: false, depth: false, stencil: false, powerPreference: 'high-performance' });
1051
+ if (!gl) webglContextError = 'canvas.getContext("webgl2") returned null. WebGL2 may be unavailable, disabled, or blocked by the device WebView.';
1052
+ } catch (error) {
1053
+ webglContextError = error && (error.stack || error.message) || String(error);
1054
+ gl = null;
1055
+ }
706
1056
  let program = null; const loc = {}; let vao = null; let quad = null; let texWorld = null; let texNear = null;
707
- const compile = (type, source) => { const s = gl.createShader(type); gl.shaderSource(s, source); gl.compileShader(s); if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s) || 'shader'); return s; };
1057
+ const compile = (type, source) => {
1058
+ const label = type === gl.VERTEX_SHADER ? 'Vertex Shader' : 'Fragment Shader';
1059
+ const shader = gl.createShader(type);
1060
+ if (!shader) throw new Error(label + ': gl.createShader returned null');
1061
+ gl.shaderSource(shader, source);
1062
+ gl.compileShader(shader);
1063
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
1064
+ const log = gl.getShaderInfoLog(shader) || 'No shader compiler log was provided.';
1065
+ gl.deleteShader(shader);
1066
+ throw new Error(label + ' compilation failed:\\n' + log);
1067
+ }
1068
+ return shader;
1069
+ };
708
1070
  const setupGl = () => {
709
- if (!gl) return false;
710
- try { program = gl.createProgram(); const v = compile(gl.VERTEX_SHADER, VERT); const f = compile(gl.FRAGMENT_SHADER, FRAG); gl.attachShader(program, v); gl.attachShader(program, f); gl.bindAttribLocation(program, 0, 'aPos'); gl.linkProgram(program); gl.deleteShader(v); gl.deleteShader(f); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(program) || 'link'); } catch (error) { console.warn('cannot create shader', error); return false; }
1071
+ if (!gl) {
1072
+ reportDiagnostic('WebGL2 Failed', webglContextError || 'WebGL2 context could not be created.');
1073
+ return false;
1074
+ }
1075
+ try {
1076
+ program = gl.createProgram();
1077
+ if (!program) throw new Error('gl.createProgram returned null');
1078
+ const v = compile(gl.VERTEX_SHADER, VERT);
1079
+ const f = compile(gl.FRAGMENT_SHADER, FRAG);
1080
+ gl.attachShader(program, v);
1081
+ gl.attachShader(program, f);
1082
+ gl.bindAttribLocation(program, 0, 'aPos');
1083
+ gl.linkProgram(program);
1084
+ gl.deleteShader(v);
1085
+ gl.deleteShader(f);
1086
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
1087
+ throw new Error('Shader program link failed:\\n' + (gl.getProgramInfoLog(program) || 'No program link log was provided.'));
1088
+ }
1089
+ } catch (error) {
1090
+ const detail = error && (error.stack || error.message) || String(error);
1091
+ console.warn('cannot create shader', error);
1092
+ reportDiagnostic('WebGL2 Failed', detail);
1093
+ return false;
1094
+ }
711
1095
  gl.useProgram(program);
712
1096
  for (const name of ['uWorld', 'uGlowNear', 'uRes', 'uPill', 'uOrigin', 'uWorldSize', 'uGlowSize', 'uLens', 'uBezel', 'uK', 'uNight', 'uLift', 'uSeam', 'uCalm', 'uRefraction', 'uDispersion', 'uShine', 'uShellDay', 'uShellNight', 'uInk', 'uLight', 'uHiNight', 'uLoNight', 'uSkyDay', 'uHomeCol', 'uSkyCol', 'uHomePos', 'uSkyPos']) { loc[name] = gl.getUniformLocation(program, name); }
713
1097
  vao = gl.createVertexArray(); gl.bindVertexArray(vao); quad = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, quad); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW); gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
@@ -716,7 +1100,17 @@ function mount(root) {
716
1100
  texWorld = makeTex(); texNear = makeTex(); gl.uniform1i(loc.uWorld, 0); gl.uniform1i(loc.uGlowNear, 1); gl.disable(gl.BLEND); return true;
717
1101
  };
718
1102
  let ready = setupGl(); let lost = !ready && !!gl && gl.isContextLost(); let ctx2d = null; let lowCanvas = null;
719
- const useCanvas2d = () => { const fresh = canvas.cloneNode(false); canvas.replaceWith(fresh); canvas = fresh; ctx2d = canvas.getContext('2d'); };
1103
+ const useCanvas2d = () => {
1104
+ const fresh = canvas.cloneNode(false);
1105
+ canvas.replaceWith(fresh);
1106
+ canvas = fresh;
1107
+ try { ctx2d = canvas.getContext('2d'); }
1108
+ catch (error) {
1109
+ reportDiagnostic('Canvas 2D Failed', error && (error.stack || error.message) || String(error));
1110
+ return;
1111
+ }
1112
+ if (!ctx2d) reportDiagnostic('Canvas 2D Failed', 'canvas.getContext("2d") returned null after WebGL2 initialization failed.');
1113
+ };
720
1114
  if (!ready && !lost) useCanvas2d();
721
1115
  const shape = { W: 1, H: 1, cssW: 1, cssH: 1, dpr: 1, pillW: 1, pillH: 1, bezel: 1, winW: 1, winH: 1, r0: 1, travel: 1, travelCss: 1, ox: 0, oy: 0 };
722
1116
  let renderScale = 1; let fullK = 1; let thumbSeam = 1;
@@ -753,12 +1147,14 @@ function mount(root) {
753
1147
  const clockOf = () => { if (branch === 'am') return mix(36, amStart, x); if (x >= 1) return nightClock; return CLOCK_DAY + (CLOCK_NIGHT - CLOCK_DAY) * x; };
754
1148
  const beginTravel = () => { if (x <= 0.0005) branch = 'pm'; else if (x >= 0.9995) { branch = 'am'; amStart = nightClock; } };
755
1149
  const setStatus = (text) => { if (statusEl.textContent !== text) statusEl.textContent = text; };
756
- const phaseName = (h) => (h < 5 || h >= 20 ? '夜' : h < 7 ? '夜明け' : h < 10 ? '朝' : h < 16.5 ? '昼' : '夕方');
1150
+ const locale = (() => { const el = root.closest('[lang]') || root.ownerDocument.documentElement; const value = (el.getAttribute('lang') || 'en').toLowerCase(); return value.startsWith('ja') ? 'ja' : value.startsWith('zh') ? 'zh-CN' : 'en'; })();
1151
+ const phaseNames = locale === 'ja' ? ['夜','夜明け','朝','昼','夕方'] : locale === 'zh-CN' ? ['夜晚','黎明','早晨','白天','傍晚'] : ['Night','Dawn','Morning','Day','Evening'];
1152
+ const phaseName = (h) => h < 5 || h >= 20 ? phaseNames[0] : h < 7 ? phaseNames[1] : h < 10 ? phaseNames[2] : h < 16.5 ? phaseNames[3] : phaseNames[4];
757
1153
  const commit = (value) => {
758
1154
  const night = value >= 1; x = night ? 1 : 0; velocity = 0; moving = false;
759
1155
  if (night) { nightClock = branch === 'am' ? amStart : CLOCK_NIGHT; branch = 'pm'; } else { branch = 'pm'; }
760
1156
  if (checked !== night) { checked = night; button.setAttribute('aria-checked', String(night)); root.dispatchEvent(new CustomEvent('daylight:change', { bubbles: true, composed: true, detail: { night } })); }
761
- setStatus(night ? '夜' : '昼');
1157
+ setStatus(night ? (locale === 'ja' ? '夜' : locale === 'zh-CN' ? '夜晚' : 'Night') : (locale === 'ja' ? '昼' : locale === 'zh-CN' ? '白天' : 'Day'));
762
1158
  };
763
1159
  const drawGl = (night) => {
764
1160
  gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); gl.useProgram(program); gl.bindVertexArray(vao);
@@ -824,7 +1220,7 @@ function mount(root) {
824
1220
  on(canvas, 'webglcontextrestored', () => { if (ctx2d) return; clearTimeout(restoreTimer); lost = false; ready = setupGl(); if (!ready) useCanvas2d(); redrawAll(); });
825
1221
  readPalette(); measure(); if (lost) waitForRestore(); res.x = 131; res.inside = false;
826
1222
  renderWorld(clockOf(), 0, 0); if (ready) uploadWorld(); if (startNight) { commit(1); dirty = true; } wake();
827
- return () => { stopDrawing(); clearTimeout(restoreTimer); visible = false; io.disconnect(); ro.disconnect(); for (const off of cleanups) off(); if (gl && ready) { gl.deleteProgram(program); gl.deleteBuffer(quad); gl.deleteVertexArray(vao); gl.deleteTexture(texWorld); gl.deleteTexture(texNear); } };
1223
+ return () => { stopDrawing(); clearTimeout(restoreTimer); visible = false; io.disconnect(); ro.disconnect(); for (const off of cleanups) off(); if (gl && ready) { gl.deleteProgram(program); gl.deleteBuffer(quad); gl.deleteVertexArray(vao); gl.deleteTexture(texWorld); gl.deleteTexture(texNear); } };
828
1224
  }
829
1225
  const SELECTOR = '[data-ld-part="toggle-daylight"]';
830
1226
  const STYLE_ID = 'ld-toggle-daylight-style';
@@ -900,7 +1296,7 @@ function ensureMarkup(root) {
900
1296
  if (!root.querySelector('.scene')) root.appendChild(make('canvas', 'scene', { 'aria-hidden': 'true' }));
901
1297
  if (!root.querySelector('.hit')) {
902
1298
  const lang = ((root.closest('[lang]') || doc.documentElement).getAttribute('lang') || '').toLowerCase();
903
- const label = root.getAttribute('data-label') || (lang.indexOf('zh') === 0 ? '昼夜开关' : 'Day/night switch');
1299
+ const label = root.getAttribute('data-label') || (lang.indexOf('ja') === 0 ? '昼夜スイッチ' : lang.indexOf('zh') === 0 ? '昼夜开关' : 'Day/night switch');
904
1300
  root.appendChild(make('button', 'hit', { type: 'button', role: 'switch', 'aria-checked': 'false', 'aria-label': label }));
905
1301
  }
906
1302
  if (!root.querySelector('[data-status]')) {
@@ -920,6 +1316,7 @@ function mountOne(root) {
920
1316
  return destroy;
921
1317
  } catch (err) {
922
1318
  console.error('[pixel-daylight-switch]', err);
1319
+ reportDiagnostic('App Initialization Failed', err && (err.stack || err.message) || String(err));
923
1320
  return null;
924
1321
  }
925
1322
  }
@@ -935,4 +1332,4 @@ if (typeof document !== 'undefined') {
935
1332
  }
936
1333
  if (typeof window !== 'undefined') window.PixelDaylightSwitch = api;
937
1334
  if (typeof module !== 'undefined' && module.exports) module.exports = api;
938
- })();
1335
+ })();