@zakmandhro/bunti 0.2.0 → 0.3.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 (127) hide show
  1. package/README.md +16 -10
  2. package/dist/cli.d.ts +2 -2
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +33 -9
  5. package/dist/cli.js.map +1 -1
  6. package/dist/colors.d.ts +2 -2
  7. package/dist/colors.js +2 -2
  8. package/dist/components/Box.d.ts +1 -1
  9. package/dist/components/Button.d.ts +6 -3
  10. package/dist/components/Button.d.ts.map +1 -1
  11. package/dist/components/Button.js +12 -3
  12. package/dist/components/Button.js.map +1 -1
  13. package/dist/components/Card.d.ts +1 -1
  14. package/dist/components/Card.js +3 -3
  15. package/dist/components/Header.d.ts +1 -1
  16. package/dist/components/Header.js +2 -2
  17. package/dist/components/Input.d.ts +2 -2
  18. package/dist/components/Input.d.ts.map +1 -1
  19. package/dist/components/Input.js +5 -1
  20. package/dist/components/Input.js.map +1 -1
  21. package/dist/components/Link.d.ts +2 -2
  22. package/dist/components/Link.d.ts.map +1 -1
  23. package/dist/components/Link.js +12 -3
  24. package/dist/components/Link.js.map +1 -1
  25. package/dist/components/Modal.d.ts +1 -1
  26. package/dist/components/Modal.d.ts.map +1 -1
  27. package/dist/components/Modal.js +11 -6
  28. package/dist/components/Modal.js.map +1 -1
  29. package/dist/components/Progress.d.ts +2 -2
  30. package/dist/components/Progress.js +2 -2
  31. package/dist/components/Spinner.d.ts +2 -2
  32. package/dist/components/index.d.ts +9 -9
  33. package/dist/components/index.js +9 -9
  34. package/dist/data/nf-glyphs.d.ts +2 -2
  35. package/dist/demo-registry.d.ts.map +1 -1
  36. package/dist/demo-registry.js +6 -1
  37. package/dist/demo-registry.js.map +1 -1
  38. package/dist/demos/2048.js +559 -0
  39. package/dist/demos/2048.ts +1 -1
  40. package/dist/demos/animation.js +177 -0
  41. package/dist/demos/animation.ts +1 -1
  42. package/dist/demos/demo-layout.js +85 -0
  43. package/dist/demos/demo-layout.ts +6 -1
  44. package/dist/demos/engine.js +244 -0
  45. package/dist/demos/interaction.js +74 -0
  46. package/dist/demos/interaction.ts +1 -1
  47. package/dist/demos/login.js +175 -0
  48. package/dist/demos/login.ts +1 -1
  49. package/dist/demos/mission-control.js +427 -0
  50. package/dist/demos/platformer-physics.js +122 -0
  51. package/dist/demos/platformer-physics.ts +284 -0
  52. package/dist/demos/platformer.js +519 -0
  53. package/dist/demos/platformer.ts +761 -0
  54. package/dist/demos/showcase.js +181 -0
  55. package/dist/demos/showcase.ts +1 -1
  56. package/dist/diagnostics.d.ts +2 -2
  57. package/dist/dsl/context.d.ts +2 -2
  58. package/dist/dsl/context.d.ts.map +1 -1
  59. package/dist/dsl/context.js +122 -27
  60. package/dist/dsl/context.js.map +1 -1
  61. package/dist/dsl/hooks.d.ts +2 -2
  62. package/dist/dsl/hooks.js +1 -1
  63. package/dist/dsl/interaction.d.ts +19 -4
  64. package/dist/dsl/interaction.d.ts.map +1 -1
  65. package/dist/dsl/interaction.js +54 -5
  66. package/dist/dsl/interaction.js.map +1 -1
  67. package/dist/dsl/layers.d.ts +2 -2
  68. package/dist/dsl/motion.d.ts +2 -2
  69. package/dist/dsl/motion.js +2 -2
  70. package/dist/dsl/render.d.ts +2 -2
  71. package/dist/dsl/render.d.ts.map +1 -1
  72. package/dist/dsl/render.js +4 -8
  73. package/dist/dsl/render.js.map +1 -1
  74. package/dist/dsl/types.d.ts +58 -12
  75. package/dist/dsl/types.d.ts.map +1 -1
  76. package/dist/dsl/types.js.map +1 -1
  77. package/dist/dsl.d.ts +4 -4
  78. package/dist/dsl.js +3 -3
  79. package/dist/easing.d.ts +1 -1
  80. package/dist/geometry.d.ts +1 -1
  81. package/dist/geometry.js +1 -1
  82. package/dist/icons-full.d.ts +2 -2
  83. package/dist/icons-full.js +3 -3
  84. package/dist/icons.d.ts +4 -4
  85. package/dist/icons.js +3 -3
  86. package/dist/index.d.ts +44 -34
  87. package/dist/index.d.ts.map +1 -1
  88. package/dist/index.js +29 -24
  89. package/dist/index.js.map +1 -1
  90. package/dist/layout.d.ts +61 -8
  91. package/dist/layout.d.ts.map +1 -1
  92. package/dist/layout.js +77 -21
  93. package/dist/layout.js.map +1 -1
  94. package/dist/pixels.d.ts +51 -0
  95. package/dist/pixels.d.ts.map +1 -0
  96. package/dist/pixels.js +82 -0
  97. package/dist/pixels.js.map +1 -0
  98. package/dist/render.d.ts +1 -1
  99. package/dist/render.d.ts.map +1 -1
  100. package/dist/render.js +44 -10
  101. package/dist/render.js.map +1 -1
  102. package/dist/state.d.ts +22 -3
  103. package/dist/state.d.ts.map +1 -1
  104. package/dist/state.js +1 -1
  105. package/dist/state.js.map +1 -1
  106. package/dist/theme.d.ts +2 -2
  107. package/dist/theme.js +1 -1
  108. package/dist/themes/catppuccin-mocha.d.ts +1 -1
  109. package/dist/themes/catppuccin-mocha.js +1 -1
  110. package/dist/themes/dracula.d.ts +1 -1
  111. package/dist/themes/dracula.js +1 -1
  112. package/dist/themes/github-light.d.ts +1 -1
  113. package/dist/themes/github-light.js +1 -1
  114. package/dist/themes/index.d.ts +7 -7
  115. package/dist/themes/index.js +6 -6
  116. package/dist/themes/nord.d.ts +1 -1
  117. package/dist/themes/nord.js +1 -1
  118. package/dist/themes/one-dark-pro.d.ts +1 -1
  119. package/dist/themes/one-dark-pro.js +1 -1
  120. package/dist/themes/tokyo-night.d.ts +1 -1
  121. package/dist/themes/tokyo-night.js +1 -1
  122. package/dist/utils.d.ts +6 -1
  123. package/dist/utils.d.ts.map +1 -1
  124. package/dist/utils.js +68 -3
  125. package/dist/utils.js.map +1 -1
  126. package/llms.txt +4 -2
  127. package/package.json +8 -4
@@ -1,10 +1,10 @@
1
- export * from './Box';
2
- export * from './Button';
3
- export * from './Card';
4
- export * from './Header';
5
- export * from './Input';
6
- export * from './Link';
7
- export * from './Modal';
8
- export * from './Progress';
9
- export * from './Spinner';
1
+ export * from './Box.js';
2
+ export * from './Button.js';
3
+ export * from './Card.js';
4
+ export * from './Header.js';
5
+ export * from './Input.js';
6
+ export * from './Link.js';
7
+ export * from './Modal.js';
8
+ export * from './Progress.js';
9
+ export * from './Spinner.js';
10
10
  //# sourceMappingURL=index.js.map
@@ -4,8 +4,8 @@
4
4
  * Source: Nerd Fonts v3.4.0 glyphnames.json
5
5
  * Regenerate: bun scripts/gen-icons.ts
6
6
  */
7
- import type { IconName } from './nf-names';
8
- export type { IconName } from './nf-names';
7
+ import type { IconName } from './nf-names.js';
8
+ export type { IconName } from './nf-names.js';
9
9
  export declare const NF_GLYPHS: Record<IconName, string>;
10
10
  export declare const NF_VERSION = "v3.4.0";
11
11
  //# sourceMappingURL=nf-glyphs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"demo-registry.d.ts","sourceRoot":"","sources":["../src/demo-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,SAAS;IACxB,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,mEAAmE;IACnE,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,sCAAsC;AACtC,eAAO,MAAM,YAAY,EAAE,SAAS,EAqCnC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,YAAY,UAAqB,CAAC;AAE/C,+CAA+C;AAC/C,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAE5D"}
1
+ {"version":3,"file":"demo-registry.d.ts","sourceRoot":"","sources":["../src/demo-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,SAAS;IACxB,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,mEAAmE;IACnE,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,sCAAsC;AACtC,eAAO,MAAM,YAAY,EAAE,SAAS,EA2CnC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,YAAY,UAA8C,CAAC;AAExE,+CAA+C;AAC/C,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAE5D"}
@@ -19,6 +19,11 @@ export const PUBLIC_DEMOS = [
19
19
  file: '2048.ts',
20
20
  description: 'The classic game - keyboard input, merge animations',
21
21
  },
22
+ {
23
+ name: 'platformer',
24
+ file: 'platformer.ts',
25
+ description: 'STARHOP - 60fps side-scroller, pixel sprites, parallax, coyote jumps',
26
+ },
22
27
  {
23
28
  name: 'animation',
24
29
  file: 'animation.ts',
@@ -49,7 +54,7 @@ export const PUBLIC_DEMOS = [
49
54
  * Non-demo helper modules that public demos import; copied alongside them
50
55
  * into dist/demos/ by scripts/build-demos.ts.
51
56
  */
52
- export const DEMO_HELPERS = ['demo-layout.ts'];
57
+ export const DEMO_HELPERS = ['demo-layout.ts', 'platformer-physics.ts'];
53
58
  /** Looks up a public demo by registry name. */
54
59
  export function findDemo(name) {
55
60
  return PUBLIC_DEMOS.find((d) => d.name === name);
@@ -1 +1 @@
1
- {"version":3,"file":"demo-registry.js","sourceRoot":"","sources":["../src/demo-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAWH,sCAAsC;AACtC,MAAM,CAAC,MAAM,YAAY,GAAgB;IACvC;QACE,IAAI,EAAE,iBAAiB;QACvB,IAAI,EAAE,oBAAoB;QAC1B,WAAW,EACT,kEAAkE;KACrE;IACD;QACE,IAAI,EAAE,MAAM;QACZ,IAAI,EAAE,SAAS;QACf,WAAW,EAAE,qDAAqD;KACnE;IACD;QACE,IAAI,EAAE,WAAW;QACjB,IAAI,EAAE,cAAc;QACpB,WAAW,EAAE,oDAAoD;KAClE;IACD;QACE,IAAI,EAAE,aAAa;QACnB,IAAI,EAAE,gBAAgB;QACtB,WAAW,EAAE,sDAAsD;KACpE;IACD;QACE,IAAI,EAAE,OAAO;QACb,IAAI,EAAE,UAAU;QAChB,WAAW,EAAE,8CAA8C;KAC5D;IACD;QACE,IAAI,EAAE,QAAQ;QACd,IAAI,EAAE,WAAW;QACjB,WAAW,EAAE,gDAAgD;KAC9D;IACD;QACE,IAAI,EAAE,UAAU;QAChB,IAAI,EAAE,aAAa;QACnB,WAAW,EAAE,0DAA0D;KACxE;CACF,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,gBAAgB,CAAC,CAAC;AAE/C,+CAA+C;AAC/C,MAAM,UAAU,QAAQ,CAAC,IAAY;IACnC,OAAO,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;AACnD,CAAC"}
1
+ {"version":3,"file":"demo-registry.js","sourceRoot":"","sources":["../src/demo-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAWH,sCAAsC;AACtC,MAAM,CAAC,MAAM,YAAY,GAAgB;IACvC;QACE,IAAI,EAAE,iBAAiB;QACvB,IAAI,EAAE,oBAAoB;QAC1B,WAAW,EACT,kEAAkE;KACrE;IACD;QACE,IAAI,EAAE,MAAM;QACZ,IAAI,EAAE,SAAS;QACf,WAAW,EAAE,qDAAqD;KACnE;IACD;QACE,IAAI,EAAE,YAAY;QAClB,IAAI,EAAE,eAAe;QACrB,WAAW,EACT,sEAAsE;KACzE;IACD;QACE,IAAI,EAAE,WAAW;QACjB,IAAI,EAAE,cAAc;QACpB,WAAW,EAAE,oDAAoD;KAClE;IACD;QACE,IAAI,EAAE,aAAa;QACnB,IAAI,EAAE,gBAAgB;QACtB,WAAW,EAAE,sDAAsD;KACpE;IACD;QACE,IAAI,EAAE,OAAO;QACb,IAAI,EAAE,UAAU;QAChB,WAAW,EAAE,8CAA8C;KAC5D;IACD;QACE,IAAI,EAAE,QAAQ;QACd,IAAI,EAAE,WAAW;QACjB,WAAW,EAAE,gDAAgD;KAC9D;IACD;QACE,IAAI,EAAE,UAAU;QAChB,IAAI,EAAE,aAAa;QACnB,WAAW,EAAE,0DAA0D;KACxE;CACF,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,gBAAgB,EAAE,uBAAuB,CAAC,CAAC;AAExE,+CAA+C;AAC/C,MAAM,UAAU,QAAQ,CAAC,IAAY;IACnC,OAAO,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;AACnD,CAAC"}
@@ -0,0 +1,559 @@
1
+ import { Box, Card, Modal } from "../components/index.js";
2
+ import {
3
+ clamp01,
4
+ easeInOutCubic,
5
+ easeOutCubic,
6
+ visibleWidth
7
+ } from "../index.js";
8
+ import { demo } from "./demo-layout.js";
9
+ function initGame() {
10
+ const grid = [
11
+ [0, 0, 0, 0],
12
+ [0, 0, 0, 0],
13
+ [0, 0, 0, 0],
14
+ [0, 0, 0, 0]
15
+ ];
16
+ spawnTile(grid);
17
+ spawnTile(grid);
18
+ return grid;
19
+ }
20
+ function spawnTile(grid) {
21
+ const empties = [];
22
+ for (let r = 0;r < 4; r++) {
23
+ for (let c = 0;c < 4; c++) {
24
+ if (grid[r][c] === 0) {
25
+ empties.push({ r, c });
26
+ }
27
+ }
28
+ }
29
+ if (empties.length > 0) {
30
+ const { r, c } = empties[Math.floor(Math.random() * empties.length)];
31
+ grid[r][c] = Math.random() < 0.9 ? 2 : 4;
32
+ }
33
+ }
34
+ function transpose(grid) {
35
+ const result = [
36
+ [0, 0, 0, 0],
37
+ [0, 0, 0, 0],
38
+ [0, 0, 0, 0],
39
+ [0, 0, 0, 0]
40
+ ];
41
+ for (let r = 0;r < 4; r++) {
42
+ for (let c = 0;c < 4; c++) {
43
+ result[c][r] = grid[r][c];
44
+ }
45
+ }
46
+ return result;
47
+ }
48
+ function moveRowLeft(row) {
49
+ const newRow = [0, 0, 0, 0];
50
+ const moves = [];
51
+ const mergedInThisTurn = [false, false, false, false];
52
+ let writeCol = 0;
53
+ let score = 0;
54
+ for (let col = 0;col < 4; col++) {
55
+ const val = row[col];
56
+ if (val === 0)
57
+ continue;
58
+ if (writeCol > 0 && newRow[writeCol - 1] === val && !mergedInThisTurn[writeCol - 1]) {
59
+ newRow[writeCol - 1] = val * 2;
60
+ score += val * 2;
61
+ mergedInThisTurn[writeCol - 1] = true;
62
+ for (const m of moves) {
63
+ if (m.to === writeCol - 1) {
64
+ m.merged = true;
65
+ }
66
+ }
67
+ moves.push({ from: col, to: writeCol - 1, merged: true, val });
68
+ } else {
69
+ newRow[writeCol] = val;
70
+ moves.push({ from: col, to: writeCol, merged: false, val });
71
+ writeCol++;
72
+ }
73
+ }
74
+ return { newRow, moves, score };
75
+ }
76
+ function slideLeft(grid) {
77
+ const next = [];
78
+ let scoreGained = 0;
79
+ const moves = [];
80
+ for (let r = 0;r < 4; r++) {
81
+ const { newRow, moves: rowMoves, score } = moveRowLeft(grid[r]);
82
+ next.push(newRow);
83
+ scoreGained += score;
84
+ for (const rm of rowMoves) {
85
+ moves.push({
86
+ fromRow: r,
87
+ fromCol: rm.from,
88
+ toRow: r,
89
+ toCol: rm.to,
90
+ value: rm.val,
91
+ merged: rm.merged
92
+ });
93
+ }
94
+ }
95
+ return { next, scoreGained, moves };
96
+ }
97
+ function slideRight(grid) {
98
+ const next = [];
99
+ let scoreGained = 0;
100
+ const moves = [];
101
+ for (let r = 0;r < 4; r++) {
102
+ const reversed = [...grid[r]].reverse();
103
+ const { newRow, moves: rowMoves, score } = moveRowLeft(reversed);
104
+ next.push([...newRow].reverse());
105
+ scoreGained += score;
106
+ for (const rm of rowMoves) {
107
+ const fromCol = 3 - rm.from;
108
+ const toCol = 3 - rm.to;
109
+ moves.push({
110
+ fromRow: r,
111
+ fromCol,
112
+ toRow: r,
113
+ toCol,
114
+ value: rm.val,
115
+ merged: rm.merged
116
+ });
117
+ }
118
+ }
119
+ return { next, scoreGained, moves };
120
+ }
121
+ function slideUp(grid) {
122
+ const transposed = transpose(grid);
123
+ const { next: slid, scoreGained, moves: slidMoves } = slideLeft(transposed);
124
+ const moves = slidMoves.map((m) => ({
125
+ fromRow: m.fromCol,
126
+ fromCol: m.fromRow,
127
+ toRow: m.toCol,
128
+ toCol: m.toRow,
129
+ value: m.value,
130
+ merged: m.merged
131
+ }));
132
+ return { next: transpose(slid), scoreGained, moves };
133
+ }
134
+ function slideDown(grid) {
135
+ const transposed = transpose(grid);
136
+ const { next: slid, scoreGained, moves: slidMoves } = slideRight(transposed);
137
+ const moves = slidMoves.map((m) => ({
138
+ fromRow: m.fromCol,
139
+ fromCol: m.fromRow,
140
+ toRow: m.toCol,
141
+ toCol: m.toRow,
142
+ value: m.value,
143
+ merged: m.merged
144
+ }));
145
+ return { next: transpose(slid), scoreGained, moves };
146
+ }
147
+ function gridChanged(g1, g2) {
148
+ for (let r = 0;r < 4; r++) {
149
+ for (let c = 0;c < 4; c++) {
150
+ if (g1[r][c] !== g2[r][c])
151
+ return true;
152
+ }
153
+ }
154
+ return false;
155
+ }
156
+ function getMaxTile(grid) {
157
+ let max = 0;
158
+ for (let r = 0;r < 4; r++) {
159
+ for (let c = 0;c < 4; c++) {
160
+ max = Math.max(max, grid[r][c]);
161
+ }
162
+ }
163
+ return max;
164
+ }
165
+ function findPeakMerge(moves, next, currentMaxTile) {
166
+ let peak = null;
167
+ const seenTargets = new Set;
168
+ for (const move of moves) {
169
+ if (!move.merged)
170
+ continue;
171
+ const key = `${move.toRow}:${move.toCol}`;
172
+ if (seenTargets.has(key))
173
+ continue;
174
+ seenTargets.add(key);
175
+ const value = next[move.toRow][move.toCol];
176
+ if (value <= currentMaxTile)
177
+ continue;
178
+ if (peak && value <= peak.value)
179
+ continue;
180
+ peak = {
181
+ row: move.toRow,
182
+ col: move.toCol,
183
+ value
184
+ };
185
+ }
186
+ return peak;
187
+ }
188
+ function isGameOver(grid) {
189
+ for (let r = 0;r < 4; r++) {
190
+ for (let c = 0;c < 4; c++) {
191
+ if (grid[r][c] === 0)
192
+ return false;
193
+ }
194
+ }
195
+ for (let r = 0;r < 4; r++) {
196
+ for (let c = 0;c < 3; c++) {
197
+ if (grid[r][c] === grid[r][c + 1])
198
+ return false;
199
+ }
200
+ }
201
+ for (let r = 0;r < 3; r++) {
202
+ for (let c = 0;c < 4; c++) {
203
+ if (grid[r][c] === grid[r + 1][c])
204
+ return false;
205
+ }
206
+ }
207
+ return true;
208
+ }
209
+ function centerText(text, width) {
210
+ const padTotal = width - text.length;
211
+ if (padTotal <= 0)
212
+ return text.substring(0, width);
213
+ const padLeft = Math.floor(padTotal / 2);
214
+ const padRight = padTotal - padLeft;
215
+ return " ".repeat(padLeft) + text + " ".repeat(padRight);
216
+ }
217
+ function mixRGB(from, to, amount) {
218
+ const t = clamp01(amount);
219
+ return {
220
+ r: Math.round(from.r + (to.r - from.r) * t),
221
+ g: Math.round(from.g + (to.g - from.g) * t),
222
+ b: Math.round(from.b + (to.b - from.b) * t)
223
+ };
224
+ }
225
+ function getTileColors(val) {
226
+ switch (val) {
227
+ case 2:
228
+ return { bg: { r: 238, g: 228, b: 218 }, fg: { r: 119, g: 110, b: 101 } };
229
+ case 4:
230
+ return { bg: { r: 237, g: 224, b: 200 }, fg: { r: 119, g: 110, b: 101 } };
231
+ case 8:
232
+ return { bg: { r: 242, g: 177, b: 121 }, fg: { r: 249, g: 246, b: 242 } };
233
+ case 16:
234
+ return { bg: { r: 245, g: 149, b: 99 }, fg: { r: 249, g: 246, b: 242 } };
235
+ case 32:
236
+ return { bg: { r: 246, g: 124, b: 95 }, fg: { r: 249, g: 246, b: 242 } };
237
+ case 64:
238
+ return { bg: { r: 246, g: 94, b: 59 }, fg: { r: 249, g: 246, b: 242 } };
239
+ case 128:
240
+ return { bg: { r: 237, g: 207, b: 114 }, fg: { r: 249, g: 246, b: 242 } };
241
+ case 256:
242
+ return { bg: { r: 237, g: 204, b: 97 }, fg: { r: 249, g: 246, b: 242 } };
243
+ case 512:
244
+ return { bg: { r: 156, g: 206, b: 56 }, fg: { r: 249, g: 246, b: 242 } };
245
+ case 1024:
246
+ return { bg: { r: 70, g: 180, b: 233 }, fg: { r: 249, g: 246, b: 242 } };
247
+ case 2048:
248
+ return { bg: { r: 164, g: 75, b: 227 }, fg: { r: 249, g: 246, b: 242 } };
249
+ default:
250
+ if (val > 2048) {
251
+ return {
252
+ bg: { r: 224, g: 36, b: 171 },
253
+ fg: { r: 255, g: 255, b: 255 }
254
+ };
255
+ }
256
+ return { bg: { r: 44, g: 47, b: 56 }, fg: { r: 80, g: 85, b: 95 } };
257
+ }
258
+ }
259
+ const SLIDE_ANIMATION_DURATION = 220;
260
+ const PEAK_MERGE_DURATION = 380;
261
+ const GRID_SIZE = 4;
262
+ const TILE_W = 8;
263
+ const TILE_H = 3;
264
+ const TILE_GAP_X = 2;
265
+ const TILE_GAP_Y = 1;
266
+ const TILE_STEP_X = TILE_W + TILE_GAP_X;
267
+ const TILE_STEP_Y = TILE_H + TILE_GAP_Y;
268
+ const BOARD_W = GRID_SIZE * TILE_W + (GRID_SIZE - 1) * TILE_GAP_X + 2;
269
+ const BOARD_H = GRID_SIZE * TILE_H + (GRID_SIZE - 1) * TILE_GAP_Y + 2;
270
+ function drawPeakMergeCelebration(ctx, celebration) {
271
+ if (!celebration)
272
+ return;
273
+ const age = Date.now() - celebration.startedAt;
274
+ if (age < 0 || age > PEAK_MERGE_DURATION)
275
+ return;
276
+ const progress = clamp01(age / PEAK_MERGE_DURATION);
277
+ const glow = 1 - easeOutCubic(progress);
278
+ const flash = Math.sin(progress * Math.PI) * (1 - progress * 0.35);
279
+ const tileX = ctx.offsetX + 1 + celebration.col * TILE_STEP_X;
280
+ const tileY = ctx.offsetY + 1 + celebration.row * TILE_STEP_Y;
281
+ const { bg: tileBg, fg: tileFg } = getTileColors(celebration.value);
282
+ const glowColor = mixRGB({ r: 62, g: 52, b: 30 }, { r: 255, g: 226, b: 86 }, glow);
283
+ const numberColor = mixRGB(tileFg, { r: 255, g: 255, b: 255 }, flash);
284
+ const tileColor = mixRGB(tileBg, { r: 255, g: 226, b: 86 }, flash * 0.22);
285
+ ctx.layer(8, (fx) => {
286
+ fx.rect(tileX - 1, tileY - 1, TILE_W + 2, TILE_H + 2, { bg: glowColor });
287
+ fx.rect(tileX, tileY, TILE_W, TILE_H, { bg: tileColor });
288
+ fx.blit(tileX, tileY + 1, fx.color.bold(centerText(celebration.value.toString(), TILE_W)), {
289
+ fg: numberColor,
290
+ bg: tileColor,
291
+ bold: true
292
+ });
293
+ });
294
+ }
295
+ demo("Bunti Retro Arcade: 2048", (ctx, bounds) => {
296
+ const {
297
+ color,
298
+ gradient,
299
+ lastKey,
300
+ usePersistentState,
301
+ useState,
302
+ wallpaper
303
+ } = ctx;
304
+ const [grid, setGrid] = useState("2048_grid", null);
305
+ const [score, setScore] = useState("2048_score", 0);
306
+ const [highScore, setHighScore] = usePersistentState("2048_highscore", 0);
307
+ const [gameOver, setGameOver] = useState("2048_gameover", false);
308
+ const [win, setWin] = useState("2048_win", false);
309
+ const [keepPlaying, setKeepPlaying] = useState("2048_keepplaying", false);
310
+ const [activeMoves, setActiveMoves] = useState("2048_activemoves", []);
311
+ const [slideStart, setSlideStart] = useState("2048_slidestart", 0);
312
+ const [maxTile, setMaxTile] = useState("2048_maxtile", 0);
313
+ const [peakMerge, setPeakMerge] = useState("2048_peakmerge", null);
314
+ const resetGame = () => {
315
+ const g = initGame();
316
+ setGrid(g);
317
+ setScore(0);
318
+ setGameOver(false);
319
+ setWin(false);
320
+ setKeepPlaying(false);
321
+ setActiveMoves([]);
322
+ setSlideStart(0);
323
+ setMaxTile(getMaxTile(g));
324
+ setPeakMerge(null);
325
+ };
326
+ if (grid === null) {
327
+ resetGame();
328
+ return;
329
+ }
330
+ if (lastKey) {
331
+ let moved = false;
332
+ let next = [];
333
+ let scoreGained = 0;
334
+ let computedMoves = [];
335
+ if (win && !keepPlaying) {
336
+ if (lastKey === "enter" || lastKey === "c") {
337
+ setKeepPlaying(true);
338
+ setWin(false);
339
+ }
340
+ } else if (!gameOver) {
341
+ if (lastKey === "up" || lastKey === "w") {
342
+ const res = slideUp(grid);
343
+ next = res.next;
344
+ scoreGained = res.scoreGained;
345
+ computedMoves = res.moves;
346
+ moved = gridChanged(grid, next);
347
+ } else if (lastKey === "down" || lastKey === "s") {
348
+ const res = slideDown(grid);
349
+ next = res.next;
350
+ scoreGained = res.scoreGained;
351
+ computedMoves = res.moves;
352
+ moved = gridChanged(grid, next);
353
+ } else if (lastKey === "left" || lastKey === "a") {
354
+ const res = slideLeft(grid);
355
+ next = res.next;
356
+ scoreGained = res.scoreGained;
357
+ computedMoves = res.moves;
358
+ moved = gridChanged(grid, next);
359
+ } else if (lastKey === "right" || lastKey === "d") {
360
+ const res = slideRight(grid);
361
+ next = res.next;
362
+ scoreGained = res.scoreGained;
363
+ computedMoves = res.moves;
364
+ moved = gridChanged(grid, next);
365
+ }
366
+ if (moved) {
367
+ const now = Date.now();
368
+ const peak = findPeakMerge(computedMoves, next, maxTile);
369
+ setActiveMoves(computedMoves);
370
+ setSlideStart(now);
371
+ spawnTile(next);
372
+ setGrid(next);
373
+ setMaxTile(Math.max(maxTile, getMaxTile(next)));
374
+ if (peak) {
375
+ setPeakMerge({
376
+ ...peak,
377
+ startedAt: now + SLIDE_ANIMATION_DURATION
378
+ });
379
+ }
380
+ const newScore = score + scoreGained;
381
+ setScore(newScore);
382
+ if (newScore > highScore) {
383
+ setHighScore(newScore);
384
+ }
385
+ if (!win && !keepPlaying) {
386
+ for (let r = 0;r < 4; r++) {
387
+ for (let c = 0;c < 4; c++) {
388
+ if (next[r][c] === 2048) {
389
+ setWin(true);
390
+ }
391
+ }
392
+ }
393
+ }
394
+ if (isGameOver(next)) {
395
+ setGameOver(true);
396
+ }
397
+ }
398
+ }
399
+ if (lastKey === "r") {
400
+ resetGame();
401
+ }
402
+ }
403
+ wallpaper(gradient({
404
+ colors: [
405
+ { r: 7, g: 10, b: 18 },
406
+ { r: 15, g: 28, b: 46 },
407
+ { r: 33, g: 15, b: 49 },
408
+ { r: 8, g: 11, b: 20 }
409
+ ],
410
+ direction: "vertical",
411
+ steps: 24
412
+ }));
413
+ const W = bounds.w;
414
+ const H = bounds.h;
415
+ const boardW = BOARD_W;
416
+ const boardH = BOARD_H;
417
+ const boardX = bounds.x + Math.max(0, Math.floor((W - boardW) / 2));
418
+ const totalNeededH = 3 + boardH;
419
+ const extraH = Math.max(0, H - totalNeededH);
420
+ const topPadding = Math.floor(extraH / 2);
421
+ const scoreboardY = bounds.y + topPadding;
422
+ const boardY = scoreboardY + 3;
423
+ Card(ctx, {
424
+ x: boardX,
425
+ y: scoreboardY,
426
+ width: boardW,
427
+ height: 3,
428
+ border: "none",
429
+ padding: [0, 0]
430
+ }, (sub) => {
431
+ const titleStr = color.yellow(color.bold(` \uDB81\uDCD3 2048 `));
432
+ const scoreStr = color.white(`SCORE: ${color.green(color.bold(score.toString()))} | BEST: ${color.yellow(color.bold(highScore.toString()))}`);
433
+ const titleW = visibleWidth(titleStr);
434
+ const scoreW = visibleWidth(scoreStr);
435
+ const spaceCount = Math.max(0, boardW - titleW - scoreW);
436
+ sub.text(`${titleStr}${" ".repeat(spaceCount)}${scoreStr}`);
437
+ });
438
+ Box(ctx, {
439
+ x: boardX,
440
+ y: boardY,
441
+ width: boardW,
442
+ height: boardH,
443
+ border: "none",
444
+ padding: [0, 0]
445
+ }, (sub) => {
446
+ const elapsed = Date.now() - slideStart;
447
+ const isAnimating = elapsed < SLIDE_ANIMATION_DURATION && activeMoves.length > 0;
448
+ for (let r = 0;r < GRID_SIZE; r++) {
449
+ for (let c = 0;c < GRID_SIZE; c++) {
450
+ const relX = 1 + c * TILE_STEP_X;
451
+ const relY = 1 + r * TILE_STEP_Y;
452
+ const absX = sub.offsetX + relX;
453
+ const absY = sub.offsetY + relY;
454
+ const { bg: bgCol } = getTileColors(0);
455
+ sub.rect(absX, absY, TILE_W, TILE_H, { bg: bgCol });
456
+ }
457
+ }
458
+ if (isAnimating) {
459
+ const progress = clamp01(elapsed / SLIDE_ANIMATION_DURATION);
460
+ const eased = easeInOutCubic(progress);
461
+ for (const m of activeMoves) {
462
+ const startX = 1 + m.fromCol * TILE_STEP_X;
463
+ const startY = 1 + m.fromRow * TILE_STEP_Y;
464
+ const endX = 1 + m.toCol * TILE_STEP_X;
465
+ const endY = 1 + m.toRow * TILE_STEP_Y;
466
+ const currX = startX + eased * (endX - startX);
467
+ const currY = startY + eased * (endY - startY);
468
+ const absX = Math.round(sub.offsetX + currX);
469
+ const absY = Math.round(sub.offsetY + currY);
470
+ const { bg: bgCol, fg: fgCol } = getTileColors(m.value);
471
+ sub.rect(absX, absY, TILE_W, TILE_H, { bg: bgCol });
472
+ const valStr = m.value.toString();
473
+ const centered = color.bold(centerText(valStr, TILE_W));
474
+ sub.blit(absX, absY + 1, centered, {
475
+ fg: fgCol,
476
+ bg: bgCol,
477
+ bold: true
478
+ });
479
+ }
480
+ } else {
481
+ for (let r = 0;r < GRID_SIZE; r++) {
482
+ for (let c = 0;c < GRID_SIZE; c++) {
483
+ const val = grid[r][c];
484
+ if (val === 0)
485
+ continue;
486
+ const relX = 1 + c * TILE_STEP_X;
487
+ const relY = 1 + r * TILE_STEP_Y;
488
+ const absX = sub.offsetX + relX;
489
+ const absY = sub.offsetY + relY;
490
+ const { bg: bgCol, fg: fgCol } = getTileColors(val);
491
+ sub.rect(absX, absY, TILE_W, TILE_H, { bg: bgCol });
492
+ const valStr = val.toString();
493
+ const centered = color.bold(centerText(valStr, TILE_W));
494
+ sub.blit(absX, absY + 1, centered, {
495
+ fg: fgCol,
496
+ bg: bgCol,
497
+ bold: true
498
+ });
499
+ }
500
+ }
501
+ }
502
+ drawPeakMergeCelebration(sub, peakMerge);
503
+ if (gameOver) {
504
+ sub.layer(10, (overlay) => {
505
+ Modal(overlay, {
506
+ width: 27,
507
+ height: 7,
508
+ border: "none",
509
+ bgColor: { r: 35, g: 0, b: 0 },
510
+ align: "center"
511
+ }, (modalSub) => {
512
+ modalSub.text(`
513
+ `);
514
+ modalSub.text(color.red(color.bold(`GAME OVER
515
+ `)));
516
+ modalSub.text(color.dim(`Final Score: ${score}
517
+
518
+ `));
519
+ modalSub.text(color.yellow("[R] To Restart"));
520
+ });
521
+ });
522
+ } else if (win && !keepPlaying) {
523
+ sub.layer(10, (overlay) => {
524
+ Modal(overlay, {
525
+ width: 27,
526
+ height: 7,
527
+ border: "none",
528
+ bgColor: { r: 35, g: 35, b: 0 },
529
+ align: "center"
530
+ }, (modalSub) => {
531
+ modalSub.text(`
532
+ `);
533
+ modalSub.text(color.fg("gold", color.bold(`YOU REACHED 2048!
534
+ `)));
535
+ modalSub.text(color.dim(`Magnificent job!
536
+
537
+ `));
538
+ modalSub.text(color.green("[Enter] Keep Playing"));
539
+ });
540
+ });
541
+ }
542
+ });
543
+ Box(ctx, {
544
+ x: boardX,
545
+ y: boardY + boardH,
546
+ width: boardW,
547
+ height: 3,
548
+ border: "none",
549
+ padding: [0, 0],
550
+ align: "center",
551
+ valign: "middle"
552
+ }, (sub) => {
553
+ sub.text(color.dim(`\uDB81\uDF32 ${color.bold("W,A,S,D")} or ${color.bold("ARROWS")} to slide
554
+ ${color.bold("R")} restart | ${color.bold("Q")} quit`));
555
+ });
556
+ }, {
557
+ fps: 60,
558
+ mouse: false
559
+ });
@@ -6,7 +6,7 @@ import {
6
6
  visibleWidth,
7
7
  } from '../index.js';
8
8
  import type { RGB } from '../state.js';
9
- import { demo } from './demo-layout';
9
+ import { demo } from './demo-layout.js';
10
10
 
11
11
  // --- Game Types ---
12
12
  type Grid = number[][];