@codexstar/pi-pompom 1.3.0 → 1.5.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 (2) hide show
  1. package/extensions/pompom.ts +226 -82
  2. package/package.json +1 -1
@@ -145,13 +145,67 @@ function fbm(x: number, y: number): number {
145
145
  Math.sin(x * 30 - time) * Math.cos(y * 30) * 0.02;
146
146
  }
147
147
 
148
- function getWeatherAndTime() {
148
+ type Weather = "clear" | "cloudy" | "rain" | "snow" | "storm";
149
+ type TimeOfDay = "dawn" | "morning" | "day" | "sunset" | "dusk" | "night";
150
+
151
+ function getTimeOfDay(): TimeOfDay {
149
152
  const h = new Date().getHours();
153
+ if (h >= 5 && h < 7) return "dawn";
154
+ if (h >= 7 && h < 10) return "morning";
155
+ if (h >= 10 && h < 16) return "day";
156
+ if (h >= 16 && h < 18) return "sunset";
157
+ if (h >= 18 && h < 20) return "dusk";
158
+ return "night";
159
+ }
160
+
161
+ function getWeather(): Weather {
162
+ // Cycle weather based on minute within each hour for variety
163
+ const m = new Date().getMinutes();
164
+ if (m < 15) return "clear";
165
+ if (m < 25) return "cloudy";
166
+ if (m < 35) return "rain";
167
+ if (m < 40) return "storm";
168
+ if (m < 45) return "snow";
169
+ if (m < 55) return "cloudy";
170
+ return "clear";
171
+ }
172
+
173
+ function getWeatherAndTime() {
174
+ const tod = getTimeOfDay();
175
+ const weather = getWeather();
150
176
  let rTop = 0, gTop = 0, bTop = 0, rBot = 0, gBot = 0, bBot = 0;
151
- if (h >= 6 && h < 17) { rTop = 40; gTop = 120; bTop = 255; rBot = 180; gBot = 220; bBot = 255; }
152
- else if (h >= 17 && h < 19) { rTop = 140; gTop = 50; bTop = 120; rBot = 255; gBot = 160; bBot = 100; }
153
- else { rTop = 10; gTop = 10; bTop = 20; rBot = 25; gBot = 20; bBot = 40; }
154
- return { rTop, gTop, bTop, rBot, gBot, bBot, isNight: h >= 19 || h < 6 };
177
+
178
+ // Sky gradients per time of day
179
+ if (tod === "dawn") {
180
+ rTop = 40; gTop = 30; bTop = 80; rBot = 255; gBot = 140; bBot = 80;
181
+ } else if (tod === "morning") {
182
+ rTop = 60; gTop = 140; bTop = 255; rBot = 200; gBot = 220; bBot = 255;
183
+ } else if (tod === "day") {
184
+ rTop = 40; gTop = 120; bTop = 255; rBot = 180; gBot = 220; bBot = 255;
185
+ } else if (tod === "sunset") {
186
+ rTop = 140; gTop = 50; bTop = 120; rBot = 255; gBot = 120; bBot = 60;
187
+ } else if (tod === "dusk") {
188
+ rTop = 50; gTop = 20; bTop = 80; rBot = 120; gBot = 60; bBot = 80;
189
+ } else { // night
190
+ rTop = 5; gTop = 5; bTop = 15; rBot = 15; gBot = 10; bBot = 30;
191
+ }
192
+
193
+ // Weather tinting — overcast dims the sky, storm darkens further
194
+ if (weather === "cloudy") {
195
+ rTop = Math.floor(rTop * 0.7 + 40); gTop = Math.floor(gTop * 0.7 + 40); bTop = Math.floor(bTop * 0.7 + 40);
196
+ rBot = Math.floor(rBot * 0.7 + 40); gBot = Math.floor(gBot * 0.7 + 40); bBot = Math.floor(bBot * 0.7 + 40);
197
+ } else if (weather === "rain") {
198
+ rTop = Math.floor(rTop * 0.5 + 30); gTop = Math.floor(gTop * 0.5 + 30); bTop = Math.floor(bTop * 0.5 + 40);
199
+ rBot = Math.floor(rBot * 0.5 + 30); gBot = Math.floor(gBot * 0.5 + 30); bBot = Math.floor(bBot * 0.5 + 40);
200
+ } else if (weather === "storm") {
201
+ rTop = Math.floor(rTop * 0.3 + 15); gTop = Math.floor(gTop * 0.3 + 15); bTop = Math.floor(bTop * 0.3 + 20);
202
+ rBot = Math.floor(rBot * 0.3 + 20); gBot = Math.floor(gBot * 0.3 + 20); bBot = Math.floor(bBot * 0.3 + 25);
203
+ } else if (weather === "snow") {
204
+ rTop = Math.floor(rTop * 0.6 + 60); gTop = Math.floor(gTop * 0.6 + 60); bTop = Math.floor(bTop * 0.6 + 70);
205
+ rBot = Math.floor(rBot * 0.6 + 60); gBot = Math.floor(gBot * 0.6 + 60); bBot = Math.floor(bBot * 0.6 + 70);
206
+ }
207
+
208
+ return { rTop, gTop, bTop, rBot, gBot, bBot, isNight: tod === "night" || tod === "dusk", weather, timeOfDay: tod };
155
209
  }
156
210
 
157
211
  function getObjHit(px: number, py: number, objects: RenderObj[]) {
@@ -222,74 +276,113 @@ function shadeObject(hit: ReturnType<typeof getObjHit>, px: number, py: number,
222
276
  const spot = Math.sin(hitNx * 10) * Math.cos(hitNy * 8);
223
277
  if (spot > 0.6) { r = Math.max(0, r - 40); g = Math.max(0, g - 20); b = Math.max(0, b - 10); }
224
278
  }
225
- if (hitObj.id === "body") {
226
- let bdx = px - hitObj.x, bdy = py - hitObj.y;
227
- if (isFlipping) {
228
- const s = Math.sin(-flipPhase), c = Math.cos(-flipPhase);
229
- const nx = bdx * c - bdy * s, ny = bdx * s + bdy * c;
230
- bdx = nx; bdy = ny;
231
- }
232
- // Blush
233
- const blx1 = bdx + 0.16, bly1 = bdy - 0.04;
234
- const blx2 = bdx - 0.16, bly2 = bdy - 0.04;
235
- const blush = Math.exp(-(blx1 * blx1 + bly1 * bly1) * 80) + Math.exp(-(blx2 * blx2 + bly2 * bly2) * 80);
236
- if (!isSleeping) {
237
- r = r * (1 - blush) + 255 * blush; g = g * (1 - blush) + 80 * blush; b = b * (1 - blush) + 100 * blush;
238
- }
239
- // Eyes
240
- const isTired = (energy < 20 || hunger < 30) && !isSleeping;
241
- const eyeOpen = isSleeping ? 0.05 : (isTired ? 0.4 : 1.0) - blinkFade;
242
- const ex1 = bdx - lookX * 0.08 + 0.1, ey1 = bdy - lookY * 0.05 + 0.02;
243
- const ex2 = bdx - lookX * 0.08 - 0.1, ey2 = bdy - lookY * 0.05 + 0.02;
244
-
245
- if (isSleeping || currentState === "singing") {
246
- if (isSleeping) {
247
- if ((Math.abs(ey1) < 0.01 && Math.abs(ex1) < 0.05) || (Math.abs(ey2) < 0.01 && Math.abs(ex2) < 0.05)) { r = 30; g = 20; b = 30; }
248
- } else {
249
- if ((Math.abs(ey1 + ex1 * ex1 * 15) < 0.015 && Math.abs(ex1) < 0.06 && ey1 > -ex1 * ex1 * 15) ||
250
- (Math.abs(ey2 + ex2 * ex2 * 15) < 0.015 && Math.abs(ex2) < 0.06 && ey2 > -ex2 * ex2 * 15)) { r = 30; g = 20; b = 30; }
251
- }
252
- } else {
253
- const eDist1 = ex1 * ex1 + (ey1 * ey1) / (eyeOpen * eyeOpen + 0.001);
254
- const eDist2 = ex2 * ex2 + (ey2 * ey2) / (eyeOpen * eyeOpen + 0.001);
255
- if (eDist1 < 0.004 || eDist2 < 0.004) {
256
- r = 15; g = 10; b = 20;
257
- if (ey1 > 0 || ey2 > 0) { r = 50; g = 180; b = 100; }
258
- if ((ex1 + 0.012) ** 2 + (ey1 + 0.012) ** 2 < 0.0008 || (ex2 + 0.012) ** 2 + (ey2 + 0.012) ** 2 < 0.0008) {
259
- if (!isTired) { r = 255; g = 255; b = 255; }
260
- } else if ((ex1 - 0.015) ** 2 + (ey1 - 0.015) ** 2 < 0.0005 || (ex2 - 0.015) ** 2 + (ey2 - 0.015) ** 2 < 0.0005) {
261
- if (!isTired) { r = 255; g = 255; b = 255; }
262
- }
263
- }
264
- }
265
- // Nose
266
- const nnx = bdx - lookX * 0.06, nny = bdy - lookY * 0.05 - 0.02;
267
- if (nnx * nnx * 1.5 + nny * nny < 0.0006 && !isSleeping) { r = 30; g = 20; b = 30; }
268
- // Mouth
269
- if (!isSleeping && !hasBall) {
270
- const mx = bdx - lookX * 0.06, my = bdy - lookY * 0.05 - 0.06;
271
- if ((Math.abs(my - (mx - 0.025) ** 2 * 20 + 0.01) < 0.01 && mx > 0 && mx < 0.05) ||
272
- (Math.abs(my - (mx + 0.025) ** 2 * 20 + 0.01) < 0.01 && mx < 0 && mx > -0.05)) {
273
- r = 50; g = 30; b = 40;
274
- }
275
- if (currentState === "excited" || currentState === "singing" || speechTimer > 0 || isTalking) {
276
- const mouthOpen = (speechTimer > 0 || currentState === "singing" || isTalking)
277
- ? (isTalking ? talkAudioLevel * 0.04 + 0.005 : Math.abs(Math.sin(time * 12)) * 0.025)
278
- : 0.015;
279
- if (mx * mx + (my + 0.01) ** 2 < mouthOpen && my < -0.01) {
280
- r = 240; g = 80; b = 100;
281
- if (my < -0.025) { r = 255; g = 120; b = 140; }
282
- }
283
- }
284
- }
279
+ let isOnFace = false;
280
+ if (hitObj.id === "body") {
281
+ let bdx = px - hitObj.x, bdy = py - hitObj.y;
282
+ if (isFlipping) {
283
+ const s = Math.sin(-flipPhase), c = Math.cos(-flipPhase);
284
+ const nx = bdx * c - bdy * s, ny = bdx * s + bdy * c;
285
+ bdx = nx; bdy = ny;
286
+ }
287
+
288
+ // ── Face plate: bright cream area so features pop ──
289
+ const faceR = Math.sqrt(bdx * bdx + bdy * bdy);
290
+ if (faceR < 0.22) {
291
+ isOnFace = true;
292
+ const faceMix = Math.max(0, 1.0 - faceR / 0.22);
293
+ r = Math.floor(r * (1 - faceMix * 0.6) + 255 * faceMix * 0.6);
294
+ g = Math.floor(g * (1 - faceMix * 0.6) + 252 * faceMix * 0.6);
295
+ b = Math.floor(b * (1 - faceMix * 0.6) + 248 * faceMix * 0.6);
296
+ }
297
+
298
+ // ── Blush: big rosy cheeks ──
299
+ const blx1 = bdx + 0.15, bly1 = bdy - 0.05;
300
+ const blx2 = bdx - 0.15, bly2 = bdy - 0.05;
301
+ const blush = Math.exp(-(blx1 * blx1 + bly1 * bly1) * 40) + Math.exp(-(blx2 * blx2 + bly2 * bly2) * 40);
302
+ if (!isSleeping) {
303
+ r = Math.floor(r * (1 - blush) + 255 * blush);
304
+ g = Math.floor(g * (1 - blush) + 70 * blush);
305
+ b = Math.floor(b * (1 - blush) + 90 * blush);
306
+ }
307
+
308
+ // ── Eyes: kawaii style white sclera colored iris dark pupil highlight ──
309
+ const isTired = (energy < 20 || hunger < 30) && !isSleeping;
310
+ const eyeOpen = isSleeping ? 0.05 : (isTired ? 0.4 : 1.0) - blinkFade;
311
+ const ex1 = bdx - lookX * 0.08 + 0.11, ey1 = bdy - lookY * 0.05 + 0.02;
312
+ const ex2 = bdx - lookX * 0.08 - 0.11, ey2 = bdy - lookY * 0.05 + 0.02;
313
+
314
+ if (isSleeping || currentState === "singing") {
315
+ // Closed eyes horizontal lines
316
+ if (isSleeping) {
317
+ if ((Math.abs(ey1) < 0.012 && Math.abs(ex1) < 0.06) || (Math.abs(ey2) < 0.012 && Math.abs(ex2) < 0.06)) { r = 40; g = 30; b = 50; }
318
+ } else {
319
+ // Happy squint arcs
320
+ if ((Math.abs(ey1 + ex1 * ex1 * 12) < 0.018 && Math.abs(ex1) < 0.07 && ey1 > -ex1 * ex1 * 12) ||
321
+ (Math.abs(ey2 + ex2 * ex2 * 12) < 0.018 && Math.abs(ex2) < 0.07 && ey2 > -ex2 * ex2 * 12)) { r = 40; g = 30; b = 50; }
322
+ }
285
323
  } else {
286
- if (hitObj.id === "earL" || hitObj.id === "earR") {
287
- if (hitU > -0.3 && hitU < 0.3 && hitV > -0.5 && hitV < 0.5) { r = 255; g = 130; b = 160; }
288
- }
324
+ const eDist1 = ex1 * ex1 + (ey1 * ey1) / (eyeOpen * eyeOpen + 0.001);
325
+ const eDist2 = ex2 * ex2 + (ey2 * ey2) / (eyeOpen * eyeOpen + 0.001);
326
+
327
+ // Layer 1: White sclera (outermost)
328
+ if (eDist1 < 0.009 || eDist2 < 0.009) {
329
+ r = 250; g = 250; b = 255;
330
+
331
+ // Layer 2: Colored iris
332
+ if (eDist1 < 0.005 || eDist2 < 0.005) {
333
+ r = 40; g = 130; b = 90; // teal-green iris
334
+ // Lower iris lighter
335
+ if (ey1 > 0.01 || ey2 > 0.01) { r = 60; g = 170; b = 110; }
336
+
337
+ // Layer 3: Dark pupil
338
+ if (eDist1 < 0.002 || eDist2 < 0.002) {
339
+ r = 10; g = 10; b = 15;
340
+ }
341
+ }
342
+
343
+ // Big highlight (upper-left) — spans ~2 chars
344
+ if ((ex1 + 0.02) ** 2 + (ey1 + 0.02) ** 2 < 0.0015 || (ex2 + 0.02) ** 2 + (ey2 + 0.02) ** 2 < 0.0015) {
345
+ if (!isTired) { r = 255; g = 255; b = 255; }
346
+ }
347
+ // Small secondary highlight (lower-right)
348
+ if ((ex1 - 0.02) ** 2 + (ey1 - 0.02) ** 2 < 0.0006 || (ex2 - 0.02) ** 2 + (ey2 - 0.02) ** 2 < 0.0006) {
349
+ if (!isTired) { r = 230; g = 240; b = 255; }
350
+ }
351
+ }
352
+ }
353
+
354
+ // ── Nose: small dark oval ──
355
+ const nnx = bdx - lookX * 0.06, nny = bdy - lookY * 0.05 - 0.03;
356
+ if (nnx * nnx * 1.2 + nny * nny < 0.001 && !isSleeping) { r = 40; g = 30; b = 40; }
357
+
358
+ // ── Mouth: clear smile arc ──
359
+ if (!isSleeping && !hasBall) {
360
+ const mx = bdx - lookX * 0.06, my = bdy - lookY * 0.05 - 0.07;
361
+ // Smile curves
362
+ if ((Math.abs(my - (mx - 0.03) ** 2 * 15 + 0.012) < 0.013 && mx > 0 && mx < 0.06) ||
363
+ (Math.abs(my - (mx + 0.03) ** 2 * 15 + 0.012) < 0.013 && mx < 0 && mx > -0.06)) {
364
+ r = 50; g = 30; b = 40;
365
+ }
366
+ // Open mouth when excited/talking
367
+ if (currentState === "excited" || currentState === "singing" || currentState === "dance" || speechTimer > 0 || isTalking) {
368
+ const mouthOpen = (speechTimer > 0 || currentState === "singing" || isTalking)
369
+ ? (isTalking ? talkAudioLevel * 0.04 + 0.008 : Math.abs(Math.sin(time * 12)) * 0.03)
370
+ : 0.02;
371
+ if (mx * mx + (my + 0.012) ** 2 < mouthOpen && my < -0.01) {
372
+ r = 230; g = 70; b = 90;
373
+ if (my < -0.03) { r = 255; g = 110; b = 130; }
374
+ }
375
+ }
289
376
  }
290
- if (hitNz < 0.25) {
291
- r *= 0.6; g *= 0.6; b *= 0.6;
377
+ } else {
378
+ if (hitObj.id === "earL" || hitObj.id === "earR") {
379
+ if (hitU > -0.3 && hitU < 0.3 && hitV > -0.5 && hitV < 0.5) { r = 255; g = 130; b = 160; }
292
380
  }
381
+ }
382
+ // Dark outline — but NOT on the face area (preserves feature contrast)
383
+ if (hitNz < 0.25 && !isOnFace) {
384
+ r = Math.floor(r * 0.6); g = Math.floor(g * 0.6); b = Math.floor(b * 0.6);
385
+ }
293
386
  } else if (hitObj.mat === 2) {
294
387
  r = Math.max(0, th.r - 20); g = Math.max(0, th.g - 15); b = Math.max(0, th.b - 10);
295
388
  if (hitNy > 0.5) { r = 255; g = 180; b = 190; }
@@ -407,11 +500,44 @@ function getPixel(px: number, py: number, objects: RenderObj[], skyColors: Retur
407
500
  let bgR = Math.floor(skyColors.rTop * (1 - grad) + skyColors.rBot * grad);
408
501
  let bgG = Math.floor(skyColors.gTop * (1 - grad) + skyColors.gBot * grad);
409
502
  let bgB = Math.floor(skyColors.bTop * (1 - grad) + skyColors.bBot * grad);
503
+
504
+ // Stars at night / dusk — twinkling via time modulation
410
505
  if (skyColors.isNight) {
411
- const star = Math.sin(px * 80) * Math.cos(py * 80);
412
- if (star > 0.99) { bgR = 255; bgG = 255; bgB = 255; }
413
- else if (star > 0.97) { bgR += 50; bgG += 50; bgB += 50; }
506
+ const star = Math.sin(px * 80 + 1.3) * Math.cos(py * 80 + 0.7);
507
+ const twinkle = Math.sin(time * 3 + px * 20 + py * 30) * 0.5 + 0.5;
508
+ if (star > 0.98) { const b = Math.floor(180 + twinkle * 75); bgR = b; bgG = b; bgB = b; }
509
+ else if (star > 0.96) { const b = Math.floor(40 + twinkle * 40); bgR += b; bgG += b; bgB += b; }
510
+ }
511
+
512
+ // Clouds — wispy noise shapes in upper sky
513
+ const w = (skyColors as any).weather as Weather | undefined;
514
+ if (w === "cloudy" || w === "rain" || w === "storm" || w === "snow") {
515
+ const cloudDensity = w === "storm" ? 0.7 : w === "rain" ? 0.5 : w === "snow" ? 0.4 : 0.3;
516
+ const cn = Math.sin(px * 8 + time * 0.3) * Math.cos(py * 12 - time * 0.2) +
517
+ Math.sin(px * 16 + py * 6) * 0.5;
518
+ if (cn > 1.0 - cloudDensity && py < 0.2) {
519
+ const blend = Math.min(1.0, (cn - (1.0 - cloudDensity)) * 4);
520
+ const cr = w === "storm" ? 50 : 180, cg = w === "storm" ? 50 : 185, cb = w === "storm" ? 55 : 195;
521
+ bgR = Math.floor(bgR * (1 - blend) + cr * blend);
522
+ bgG = Math.floor(bgG * (1 - blend) + cg * blend);
523
+ bgB = Math.floor(bgB * (1 - blend) + cb * blend);
524
+ }
525
+ }
526
+
527
+ // Sunset/dawn glow at horizon
528
+ const tod = (skyColors as any).timeOfDay as TimeOfDay | undefined;
529
+ if (tod === "sunset" || tod === "dawn") {
530
+ const horizonGlow = Math.exp(-(py - 0.4) * (py - 0.4) * 20);
531
+ if (tod === "sunset") {
532
+ bgR = Math.min(255, Math.floor(bgR + horizonGlow * 80));
533
+ bgG = Math.min(255, Math.floor(bgG + horizonGlow * 30));
534
+ } else {
535
+ bgR = Math.min(255, Math.floor(bgR + horizonGlow * 50));
536
+ bgG = Math.min(255, Math.floor(bgG + horizonGlow * 40));
537
+ bgB = Math.min(255, Math.floor(bgB + horizonGlow * 30));
538
+ }
414
539
  }
540
+
415
541
  return [bgR, bgG, bgB];
416
542
  }
417
543
 
@@ -486,14 +612,21 @@ function updatePhysics(dt: number) {
486
612
  ffZ = posZ + Math.sin(time * 0.9) * 0.4;
487
613
 
488
614
  // Weather particles
489
- const isRaining = new Date().getMinutes() % 10 < 3 && !getWeatherAndTime().isNight;
490
- if (isRaining && Math.random() < 0.3) {
491
- const effectDim = Math.max(40, Math.min(W, H * 4));
492
- const scale = 2.0 / effectDim;
493
- particles.push({
494
- x: (Math.random() - 0.5) * W * scale, y: -H * scale,
495
- vx: 0.1, vy: 2.0 + Math.random(), char: "|", r: 150, g: 200, b: 255, life: 1.0, type: "rain",
496
- });
615
+ const weather = getWeather();
616
+ const effectDim = Math.max(40, Math.min(W, H * 4));
617
+ const wScale = 2.0 / effectDim;
618
+ if (weather === "rain" && Math.random() < 0.4) {
619
+ particles.push({ x: (Math.random() - 0.5) * W * wScale, y: -H * wScale, vx: 0.15, vy: 2.5 + Math.random(), char: "|", r: 150, g: 200, b: 255, life: 1.0, type: "rain" });
620
+ }
621
+ if (weather === "storm" && Math.random() < 0.6) {
622
+ particles.push({ x: (Math.random() - 0.5) * W * wScale, y: -H * wScale, vx: 0.4 + Math.random() * 0.3, vy: 3.0 + Math.random() * 2, char: "/", r: 180, g: 200, b: 255, life: 0.8, type: "rain" });
623
+ // Occasional lightning flash (brief bright particle)
624
+ if (Math.random() < 0.005) {
625
+ particles.push({ x: (Math.random() - 0.5) * W * wScale * 0.5, y: -H * wScale * 0.5, vx: 0, vy: 0, char: "#", r: 255, g: 255, b: 255, life: 0.1, type: "lightning" });
626
+ }
627
+ }
628
+ if (weather === "snow" && Math.random() < 0.2) {
629
+ particles.push({ x: (Math.random() - 0.5) * W * wScale, y: -H * wScale, vx: (Math.random() - 0.5) * 0.3, vy: 0.4 + Math.random() * 0.3, char: ".", r: 240, g: 245, b: 255, life: 3.0, type: "snow" });
497
630
  }
498
631
 
499
632
  // Ball physics
@@ -639,6 +772,8 @@ function updatePhysics(dt: number) {
639
772
  if (p.type === "z") p.x += Math.sin(p.y * 4.0) * 0.005;
640
773
  if (p.type === "note") p.x += Math.sin(p.y * 6.0) * 0.01;
641
774
  if (p.type === "rain" && p.y > 0.6) { p.type = "splash"; p.char = "."; p.vy = -0.5; p.vx = (Math.random() - 0.5) * 0.5; p.life = 0.2; }
775
+ if (p.type === "snow") { p.vx += Math.sin(time * 2 + p.x * 5) * 0.01; if (p.y > 0.55) { p.life = 0; } }
776
+ if (p.type === "lightning") { p.life -= dt * 8; }
642
777
  p.life -= dt * 0.8;
643
778
  if (p.life <= 0) particles.splice(i, 1);
644
779
  }
@@ -798,7 +933,16 @@ export function renderPompom(width: number, audioLevel: number, dt: number): str
798
933
  else if (currentState === "peek") stateMsg = "Pompom is peeking back in... hi!";
799
934
  else if (currentState === "offscreen") stateMsg = "Pompom wandered off... they'll be back";
800
935
  else if (isTalking) stateMsg = "Pompom is listening to you speak";
801
- else stateMsg = "Pompom is vibing. Pet, feed, or play!";
936
+ else {
937
+ const w = getWeather(), tod = getTimeOfDay();
938
+ if (w === "storm") stateMsg = "Pompom hides from the thunder!";
939
+ else if (w === "rain") stateMsg = "Pompom watches the rain fall";
940
+ else if (w === "snow") stateMsg = "Pompom catches snowflakes!";
941
+ else if (tod === "dawn") stateMsg = "Pompom watches the sunrise";
942
+ else if (tod === "sunset") stateMsg = "Pompom enjoys the sunset";
943
+ else if (tod === "night") stateMsg = "Pompom stargazes under the night sky";
944
+ else stateMsg = "Pompom is vibing. Pet, feed, or play!";
945
+ }
802
946
 
803
947
  // Build status: "─ ⌥ w·Wake p·Pet ... │ State ───" exactly W visible chars
804
948
  const shortcuts: [string, string][] = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codexstar/pi-pompom",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "A 3D raymarched virtual pet companion for Pi CLI",
5
5
  "type": "module",
6
6
  "author": "codexstar69 <engazedigital@gmail.com>",