@modelstatus/cli 0.1.39 → 0.1.40

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelstatus/cli",
3
- "version": "0.1.39",
3
+ "version": "0.1.40",
4
4
  "description": "Track which AI models you use, where, and never get surprised by a retirement. Free offline model-health for any repo (mm status), browser sign-in for cloud inventory + alerts.",
5
5
  "keywords": [
6
6
  "llm",
@@ -29,16 +29,38 @@ const G_ASCII = {
29
29
  };
30
30
  export const GAME_GLYPH = ASCII ? G_ASCII : G_UNICODE;
31
31
 
32
+ // Animation frames cycled by state.frame / entity fields so the board feels alive:
33
+ // barrels spin, the player bobs while walking/climbing + opens up airborne, the
34
+ // princess pulses a heart. ASCII variants stay pure-ASCII for the MM_ASCII path.
35
+ const PLAYER_FRAMES = ASCII ? ["P", "p"] : ["☻", "☺"];
36
+ const BARREL_FRAMES = ASCII ? ["o", "O", "0"] : ["◐", "◓", "◑", "◒"];
37
+ const PRINCESS_FRAMES = ASCII ? ["V", "v"] : ["♥", "♡"];
38
+ export const GAME_FRAMES = {
39
+ player: PLAYER_FRAMES, barrel: BARREL_FRAMES, princess: PRINCESS_FRAMES,
40
+ dk: [GAME_GLYPH.dk], girder: [GAME_GLYPH.girder], ladder: [GAME_GLYPH.ladder],
41
+ };
42
+
43
+ /** Animated player glyph from motion state + the global frame counter. */
44
+ function playerFrame(state) {
45
+ const p = state.jumpman || state.player;
46
+ const f = state.frame || 0;
47
+ if (!p) return PLAYER_FRAMES[0];
48
+ if (p.onLadder) return PLAYER_FRAMES[(f >> 2) & 1]; // climb bob
49
+ if (!p.onGround) return PLAYER_FRAMES[1]; // airborne
50
+ if (Math.abs(p.vx || 0) > 30) return PLAYER_FRAMES[(f >> 2) & 1]; // walk bob
51
+ return PLAYER_FRAMES[0]; // standing
52
+ }
53
+
32
54
  // Entity color keys (resolved against the C palette by the wrapper; kept here as
33
55
  // literal hex so the renderer stays dependency-free + the colorize output is
34
56
  // directly usable by tests without importing the design system).
35
57
  export const GAME_COLORS = {
36
- jumpman: "#22d3ee", // C.ACCENT
37
- barrel: "#ea580c", // retiring-orange
38
- girder: "#243042", // C.BORDER
39
- ladder: "#8b98a5", // C.FG_DIM
40
- dk: "#dc2626", // retired-red
41
- princess: "#a78bfa", // violet
58
+ jumpman: "#ef4444", // Mario red — pops off the board
59
+ barrel: "#f97316", // bright orange
60
+ girder: "#e0698c", // classic DK rose girders
61
+ ladder: "#38bdf8", // sky-blue ladders (cool contrast vs the warm board)
62
+ dk: "#c2630a", // gorilla amber-brown
63
+ princess: "#f472b6", // pink
42
64
  empty: "#5b6673", // C.FG_FAINT
43
65
  };
44
66
 
@@ -67,11 +89,12 @@ function buildGrid(state, glyph) {
67
89
  for (const l of ladders) {
68
90
  for (let y = l.yTop; y <= l.yBottom; y++) { put(l.col, y, glyph.ladder, "ladder"); put(l.col + 1, y, glyph.ladder, "ladder"); }
69
91
  }
70
- // goal + actors
71
- put(princess.x, princess.y, glyph.princess, "princess");
92
+ // goal + actors (animated: princess pulse, barrel spin, player bob)
93
+ const f = state.frame || 0;
94
+ put(princess.x, princess.y, PRINCESS_FRAMES[(f >> 4) & 1], "princess");
72
95
  put(donkeyKong.x, donkeyKong.y, glyph.dk, "dk");
73
- for (const b of barrels) put(b.x, b.y, glyph.barrel, "barrel");
74
- put(jumpman.x, jumpman.y, glyph.jumpman, "jumpman");
96
+ for (const b of barrels) put(b.x, b.y, BARREL_FRAMES[(b.spin >> 1) % BARREL_FRAMES.length], "barrel");
97
+ put(jumpman.x, jumpman.y, playerFrame(state), "jumpman");
75
98
  return grid;
76
99
  }
77
100
 
@@ -51,8 +51,14 @@ const COL = {
51
51
  green: packHex("#16a34a"),
52
52
  amber: packHex("#d97706"),
53
53
  violet: packHex("#a78bfa"),
54
+ pink: packHex("#f472b6"),
55
+ gold: packHex("#fbbf24"),
54
56
  };
55
57
 
58
+ // Win-celebration sparkles + a color-cycle palette (the flare on a level clear).
59
+ const WIN_SPARK = ASCII ? ["*", "+", ".", "'", "o"] : ["✦", "✧", "✺", "·", "♥"];
60
+ const WIN_PAL = [COL.pink, COL.gold, COL.accent, COL.green, COL.red, COL.violet];
61
+
56
62
  const fmtNum = (n) => (n == null ? "0" : Number(n).toLocaleString("en-US"));
57
63
 
58
64
  /**
@@ -213,7 +219,9 @@ export function runGame({ width, height, level = 1, scanStore = null, onExit, _i
213
219
  bb.blit(cells, 0, HUD_ROWS, cells.width, cells.height);
214
220
  // Respawn blink: hide the player glyph on alternate frames during invuln.
215
221
  maybeBlinkPlayer(bb, game, curW);
216
- drawBanner(bb, game, curW, curH);
222
+ // Level-clear gets the full celebration; everything else the plain banner.
223
+ if (game.status === "levelclear") drawWinFlare(bb, game, curW, curH);
224
+ else drawBanner(bb, game, curW, curH);
217
225
  drawKeybar(bb, curW, HUD_ROWS + curH);
218
226
  const s = bb.render();
219
227
  if (s) term.write(s);
@@ -323,6 +331,36 @@ function drawBanner(bb, game, w, boardH) {
323
331
  }
324
332
  }
325
333
 
334
+ /** Win celebration: a drifting confetti field + a color-cycling heart banner +
335
+ * the bonus tally. Drawn over the board during the levelclear window (the loop is
336
+ * actively ticking, so animating here is fine — it's not idle). All deterministic
337
+ * off game.frame (no RNG), so it's stable + cheap (the diff renderer only emits
338
+ * the cells that changed). */
339
+ function drawWinFlare(bb, game, w, boardH) {
340
+ const f = game.frame || 0;
341
+ // Confetti: a deterministic, gently-falling sparkle field across the board.
342
+ const count = Math.min(48, Math.max(16, Math.floor((w * boardH) / 10)));
343
+ for (let i = 0; i < count; i++) {
344
+ const col = (i * 17 + (i % 5)) % w;
345
+ const row = (i * 7 + (f >> 1)) % boardH; // drifts downward each frame
346
+ const ch = WIN_SPARK[(i + (f >> 2)) % WIN_SPARK.length];
347
+ const c = WIN_PAL[(i + (f >> 3)) % WIN_PAL.length];
348
+ bb.setCell(col, HUD_ROWS + row, ch.charCodeAt(0), c);
349
+ }
350
+ // Flashing centered banner with hearts.
351
+ const text = `${G.spark} ${G.heart} YOU SAVED HER! ${G.heart} ${G.spark}`;
352
+ const brow = HUD_ROWS + Math.floor(boardH / 2);
353
+ const bx = Math.max(0, Math.floor((w - text.length) / 2));
354
+ for (let cx = 0; cx < w; cx++) bb.setCell(cx, brow, 32, -1); // clear row for legibility
355
+ bb.setText(bx, brow, clip(text, w - bx), WIN_PAL[(f >> 2) % WIN_PAL.length]);
356
+ // Bonus tally sub-line.
357
+ const sub = `+100 bonus ${fmtNum(game.bonus || 0)} ${ASCII ? "->" : "→"} next level`;
358
+ const sy = Math.min(HUD_ROWS + boardH - 1, brow + 1);
359
+ const sx = Math.max(0, Math.floor((w - sub.length) / 2));
360
+ for (let cx = 0; cx < w; cx++) bb.setCell(cx, sy, 32, -1);
361
+ bb.setText(sx, sy, clip(sub, w - sx), COL.strong);
362
+ }
363
+
326
364
  /** Bottom row: in-game keybar. */
327
365
  function drawKeybar(bb, w, y) {
328
366
  const bar = ASCII
@@ -335,3 +373,6 @@ function clip(s, w) {
335
373
  if (w <= 0) return "";
336
374
  return s.length > w ? s.slice(0, w) : s;
337
375
  }
376
+
377
+ // Exposed for focused render tests (the win celebration + banners).
378
+ export const _internals = { drawWinFlare, drawBanner, drawStats, HUD_ROWS, KEY_ROW };