@zakmandhro/bunti 0.2.1 → 0.4.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 (121) hide show
  1. package/README.md +6 -5
  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 +1 -1
  10. package/dist/components/Button.js +2 -2
  11. package/dist/components/Card.d.ts +1 -1
  12. package/dist/components/Card.js +3 -3
  13. package/dist/components/Header.d.ts +1 -1
  14. package/dist/components/Header.js +2 -2
  15. package/dist/components/Input.d.ts +2 -2
  16. package/dist/components/Input.js +1 -1
  17. package/dist/components/Link.d.ts +2 -2
  18. package/dist/components/Link.js +2 -2
  19. package/dist/components/Modal.d.ts +1 -1
  20. package/dist/components/Modal.js +2 -2
  21. package/dist/components/Progress.d.ts +2 -2
  22. package/dist/components/Progress.js +2 -2
  23. package/dist/components/Spinner.d.ts +2 -2
  24. package/dist/components/index.d.ts +9 -9
  25. package/dist/components/index.js +9 -9
  26. package/dist/data/nf-glyphs.d.ts +2 -2
  27. package/dist/demo-registry.d.ts.map +1 -1
  28. package/dist/demo-registry.js +6 -1
  29. package/dist/demo-registry.js.map +1 -1
  30. package/dist/demos/2048.js +559 -0
  31. package/dist/demos/2048.ts +1 -1
  32. package/dist/demos/animation.js +177 -0
  33. package/dist/demos/animation.ts +1 -1
  34. package/dist/demos/demo-layout.js +85 -0
  35. package/dist/demos/demo-layout.ts +6 -1
  36. package/dist/demos/engine.js +244 -0
  37. package/dist/demos/interaction.js +74 -0
  38. package/dist/demos/interaction.ts +1 -1
  39. package/dist/demos/login.js +175 -0
  40. package/dist/demos/login.ts +1 -1
  41. package/dist/demos/mission-control.js +427 -0
  42. package/dist/demos/platformer-physics.js +122 -0
  43. package/dist/demos/platformer-physics.ts +284 -0
  44. package/dist/demos/platformer.js +519 -0
  45. package/dist/demos/platformer.ts +761 -0
  46. package/dist/demos/showcase.js +181 -0
  47. package/dist/demos/showcase.ts +1 -1
  48. package/dist/detect.d.ts +24 -7
  49. package/dist/detect.d.ts.map +1 -1
  50. package/dist/detect.js +73 -12
  51. package/dist/detect.js.map +1 -1
  52. package/dist/diagnostics.d.ts +8 -2
  53. package/dist/diagnostics.d.ts.map +1 -1
  54. package/dist/diagnostics.js +11 -0
  55. package/dist/diagnostics.js.map +1 -1
  56. package/dist/dsl/context.d.ts +2 -2
  57. package/dist/dsl/context.d.ts.map +1 -1
  58. package/dist/dsl/context.js +14 -14
  59. package/dist/dsl/context.js.map +1 -1
  60. package/dist/dsl/hooks.d.ts +2 -2
  61. package/dist/dsl/hooks.js +1 -1
  62. package/dist/dsl/interaction.d.ts +3 -3
  63. package/dist/dsl/layers.d.ts +2 -2
  64. package/dist/dsl/motion.d.ts +2 -2
  65. package/dist/dsl/motion.js +2 -2
  66. package/dist/dsl/render.d.ts +2 -2
  67. package/dist/dsl/render.d.ts.map +1 -1
  68. package/dist/dsl/render.js +4 -8
  69. package/dist/dsl/render.js.map +1 -1
  70. package/dist/dsl/types.d.ts +11 -10
  71. package/dist/dsl/types.d.ts.map +1 -1
  72. package/dist/dsl/types.js.map +1 -1
  73. package/dist/dsl.d.ts +4 -4
  74. package/dist/dsl.js +3 -3
  75. package/dist/easing.d.ts +1 -1
  76. package/dist/geometry.d.ts +1 -1
  77. package/dist/geometry.js +1 -1
  78. package/dist/icons-full.d.ts +2 -2
  79. package/dist/icons-full.js +3 -3
  80. package/dist/icons.d.ts +4 -4
  81. package/dist/icons.js +3 -3
  82. package/dist/index.d.ts +45 -36
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +29 -24
  85. package/dist/index.js.map +1 -1
  86. package/dist/layout.d.ts +18 -6
  87. package/dist/layout.d.ts.map +1 -1
  88. package/dist/layout.js +15 -6
  89. package/dist/layout.js.map +1 -1
  90. package/dist/pixels.d.ts +51 -0
  91. package/dist/pixels.d.ts.map +1 -0
  92. package/dist/pixels.js +82 -0
  93. package/dist/pixels.js.map +1 -0
  94. package/dist/render.d.ts +1 -1
  95. package/dist/render.d.ts.map +1 -1
  96. package/dist/render.js +51 -11
  97. package/dist/render.js.map +1 -1
  98. package/dist/state.d.ts +4 -4
  99. package/dist/state.js +1 -1
  100. package/dist/theme.d.ts +2 -2
  101. package/dist/theme.js +1 -1
  102. package/dist/themes/catppuccin-mocha.d.ts +1 -1
  103. package/dist/themes/catppuccin-mocha.js +1 -1
  104. package/dist/themes/dracula.d.ts +1 -1
  105. package/dist/themes/dracula.js +1 -1
  106. package/dist/themes/github-light.d.ts +1 -1
  107. package/dist/themes/github-light.js +1 -1
  108. package/dist/themes/index.d.ts +7 -7
  109. package/dist/themes/index.js +6 -6
  110. package/dist/themes/nord.d.ts +1 -1
  111. package/dist/themes/nord.js +1 -1
  112. package/dist/themes/one-dark-pro.d.ts +1 -1
  113. package/dist/themes/one-dark-pro.js +1 -1
  114. package/dist/themes/tokyo-night.d.ts +1 -1
  115. package/dist/themes/tokyo-night.js +1 -1
  116. package/dist/utils.d.ts +6 -1
  117. package/dist/utils.d.ts.map +1 -1
  118. package/dist/utils.js +68 -3
  119. package/dist/utils.js.map +1 -1
  120. package/llms.txt +4 -2
  121. package/package.json +8 -4
@@ -0,0 +1,122 @@
1
+ export const PHYSICS = {
2
+ gravity: 42,
3
+ moveSpeed: 19,
4
+ accel: 70,
5
+ decel: 110,
6
+ jumpVel: 17.5,
7
+ jumpCutVel: 6,
8
+ coyoteMs: 80,
9
+ jumpBufferMs: 120,
10
+ maxFall: 30,
11
+ halfW: 1.5,
12
+ minX: 2,
13
+ maxX: 214
14
+ };
15
+ export function createPlayer(x, y) {
16
+ return {
17
+ x,
18
+ y,
19
+ vx: 0,
20
+ vy: 0,
21
+ grounded: true,
22
+ facing: 1,
23
+ lastGroundedAt: 0,
24
+ jumpBufferedAt: Number.NEGATIVE_INFINITY,
25
+ coyoteSpent: false,
26
+ justJumped: false,
27
+ landedImpact: 0
28
+ };
29
+ }
30
+ export function approach(current, target, maxDelta) {
31
+ if (current < target)
32
+ return Math.min(current + maxDelta, target);
33
+ return Math.max(current - maxDelta, target);
34
+ }
35
+ export function overlapsX(p, x, halfW) {
36
+ return x + halfW > p.x && x - halfW < p.x + p.w;
37
+ }
38
+ export function supportAt(platforms, x, halfW, y) {
39
+ for (const p of platforms) {
40
+ if (Math.abs(y - p.top) < 0.000001 && overlapsX(p, x, halfW))
41
+ return p;
42
+ }
43
+ return null;
44
+ }
45
+ export function landingPlatform(platforms, x, halfW, prevY, nextY) {
46
+ let best = null;
47
+ for (const p of platforms) {
48
+ if (!overlapsX(p, x, halfW))
49
+ continue;
50
+ if (prevY >= p.top && nextY <= p.top) {
51
+ if (best === null || p.top > best.top)
52
+ best = p;
53
+ }
54
+ }
55
+ return best;
56
+ }
57
+ export function stepPlayer(sim, input, dtMs, now, platforms, cfg = PHYSICS) {
58
+ const dt = dtMs / 1000;
59
+ sim.justJumped = false;
60
+ sim.landedImpact = 0;
61
+ const dir = (input.right ? 1 : 0) - (input.left ? 1 : 0);
62
+ if (dir !== 0)
63
+ sim.facing = dir;
64
+ const rate = dir !== 0 ? cfg.accel : cfg.decel;
65
+ sim.vx = approach(sim.vx, dir * cfg.moveSpeed, rate * dt);
66
+ sim.x = Math.min(cfg.maxX, Math.max(cfg.minX, sim.x + sim.vx * dt));
67
+ if (input.jumpPressed)
68
+ sim.jumpBufferedAt = now;
69
+ const buffered = now - sim.jumpBufferedAt <= cfg.jumpBufferMs;
70
+ const canJump = sim.grounded || !sim.coyoteSpent && now - sim.lastGroundedAt <= cfg.coyoteMs;
71
+ if (buffered && canJump) {
72
+ sim.vy = cfg.jumpVel;
73
+ sim.grounded = false;
74
+ sim.coyoteSpent = true;
75
+ sim.jumpBufferedAt = Number.NEGATIVE_INFINITY;
76
+ sim.justJumped = true;
77
+ }
78
+ if (!input.jumpHeld && sim.vy > cfg.jumpCutVel)
79
+ sim.vy = cfg.jumpCutVel;
80
+ if (sim.grounded) {
81
+ if (supportAt(platforms, sim.x, cfg.halfW, sim.y)) {
82
+ sim.lastGroundedAt = now;
83
+ sim.coyoteSpent = false;
84
+ } else {
85
+ sim.grounded = false;
86
+ }
87
+ }
88
+ if (!sim.grounded) {
89
+ const prevY = sim.y;
90
+ sim.vy = Math.max(sim.vy - cfg.gravity * dt, -cfg.maxFall);
91
+ sim.y += sim.vy * dt;
92
+ if (sim.vy <= 0) {
93
+ const hit = landingPlatform(platforms, sim.x, cfg.halfW, prevY, sim.y);
94
+ if (hit) {
95
+ sim.landedImpact = -sim.vy;
96
+ sim.y = hit.top;
97
+ sim.vy = 0;
98
+ sim.grounded = true;
99
+ sim.coyoteSpent = false;
100
+ sim.lastGroundedAt = now;
101
+ }
102
+ }
103
+ }
104
+ }
105
+ export const MOVE_HOLD_MS = 230;
106
+ export const JUMP_HOLD_MS = 140;
107
+ export function createHeldKeys() {
108
+ return {
109
+ left: Number.NEGATIVE_INFINITY,
110
+ right: Number.NEGATIVE_INFINITY,
111
+ jump: Number.NEGATIVE_INFINITY
112
+ };
113
+ }
114
+ export function noteHeld(held, action, now) {
115
+ held[action] = now;
116
+ }
117
+ export function clearHeld(held, action) {
118
+ held[action] = Number.NEGATIVE_INFINITY;
119
+ }
120
+ export function isHeldActive(held, action, now, windowMs = action === "jump" ? JUMP_HOLD_MS : MOVE_HOLD_MS) {
121
+ return now - held[action] <= windowMs;
122
+ }
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Platformer demo physics — pure, engine-free helpers for demo/platformer.ts,
3
+ * unit-tested with a fake clock in tests/platformer.test.ts.
4
+ *
5
+ * This is deliberately demo-side code: Bunti ships rendering primitives, not
6
+ * game APIs. Everything here is plain math over plain objects.
7
+ *
8
+ * Units and conventions:
9
+ * - x is in terminal columns, y is in rows ABOVE the ground line (up = +).
10
+ * - Velocities are cells/second; timestamps and durations are milliseconds.
11
+ * - `now` is always passed in (never read from Date.now) so tests can drive
12
+ * the clock, and stepPlayer mutates the sim in place so the render loop
13
+ * allocates nothing per frame.
14
+ */
15
+
16
+ /** One solid, one-sided surface: land on `top` when falling across it. */
17
+ export interface Platform {
18
+ /** Left edge, in columns. */
19
+ x: number;
20
+ /** Width, in columns. */
21
+ w: number;
22
+ /** Surface height in rows above the ground line (ground segments use 0). */
23
+ top: number;
24
+ }
25
+
26
+ /** Game-feel constants for stepPlayer. */
27
+ export interface PhysicsConfig {
28
+ /** Downward acceleration, rows/s^2. */
29
+ gravity: number;
30
+ /** Target horizontal run speed, cells/s. */
31
+ moveSpeed: number;
32
+ /** Horizontal acceleration toward the input direction, cells/s^2. */
33
+ accel: number;
34
+ /** Horizontal deceleration with no input, cells/s^2. */
35
+ decel: number;
36
+ /** Initial upward jump velocity, rows/s. */
37
+ jumpVel: number;
38
+ /** Rising speed cap applied while the jump key is NOT held (variable jump height). */
39
+ jumpCutVel: number;
40
+ /** Grace window after walking off a ledge in which a jump still works. */
41
+ coyoteMs: number;
42
+ /** A jump pressed this long before landing still fires on touchdown. */
43
+ jumpBufferMs: number;
44
+ /** Terminal falling speed, rows/s. */
45
+ maxFall: number;
46
+ /** Player half-width for platform overlap tests, in columns. */
47
+ halfW: number;
48
+ /** World clamp, in columns. */
49
+ minX: number;
50
+ maxX: number;
51
+ }
52
+
53
+ export const PHYSICS: PhysicsConfig = {
54
+ gravity: 42,
55
+ moveSpeed: 19,
56
+ accel: 70,
57
+ decel: 110,
58
+ jumpVel: 17.5,
59
+ jumpCutVel: 6,
60
+ coyoteMs: 80,
61
+ jumpBufferMs: 120,
62
+ maxFall: 30,
63
+ halfW: 1.5,
64
+ minX: 2,
65
+ maxX: 214,
66
+ };
67
+
68
+ /** Per-frame input snapshot fed to stepPlayer. */
69
+ export interface SimInput {
70
+ left: boolean;
71
+ right: boolean;
72
+ /** True on the frame a jump key was pressed (edge, not level). */
73
+ jumpPressed: boolean;
74
+ /** True while the jump key is considered held (level). */
75
+ jumpHeld: boolean;
76
+ }
77
+
78
+ /** The player simulation state. Mutated in place by stepPlayer. */
79
+ export interface PlayerSim {
80
+ x: number;
81
+ y: number;
82
+ vx: number;
83
+ vy: number;
84
+ grounded: boolean;
85
+ /** Last direction of travel (1 right, -1 left) — render-side flavor. */
86
+ facing: 1 | -1;
87
+ /** Timestamp of the most recent grounded frame (drives coyote time). */
88
+ lastGroundedAt: number;
89
+ /** Timestamp of the most recent jump press (drives jump buffering). */
90
+ jumpBufferedAt: number;
91
+ /** True once the current airtime has consumed its coyote jump. */
92
+ coyoteSpent: boolean;
93
+ /** Set by stepPlayer: this step fired a jump. */
94
+ justJumped: boolean;
95
+ /** Set by stepPlayer: downward impact speed (rows/s) if this step landed. */
96
+ landedImpact: number;
97
+ }
98
+
99
+ /** A fresh sim standing at (x, y). */
100
+ export function createPlayer(x: number, y: number): PlayerSim {
101
+ return {
102
+ x,
103
+ y,
104
+ vx: 0,
105
+ vy: 0,
106
+ grounded: true,
107
+ facing: 1,
108
+ lastGroundedAt: 0,
109
+ jumpBufferedAt: Number.NEGATIVE_INFINITY,
110
+ coyoteSpent: false,
111
+ justJumped: false,
112
+ landedImpact: 0,
113
+ };
114
+ }
115
+
116
+ /** Moves `current` toward `target` by at most `maxDelta` (no overshoot). */
117
+ export function approach(
118
+ current: number,
119
+ target: number,
120
+ maxDelta: number,
121
+ ): number {
122
+ if (current < target) return Math.min(current + maxDelta, target);
123
+ return Math.max(current - maxDelta, target);
124
+ }
125
+
126
+ /** True when a player centered at `x` horizontally overlaps the platform. */
127
+ export function overlapsX(p: Platform, x: number, halfW: number): boolean {
128
+ return x + halfW > p.x && x - halfW < p.x + p.w;
129
+ }
130
+
131
+ /**
132
+ * The platform whose surface the player is standing on (y within epsilon of
133
+ * a top it overlaps), or null when unsupported — walking off a ledge.
134
+ */
135
+ export function supportAt(
136
+ platforms: readonly Platform[],
137
+ x: number,
138
+ halfW: number,
139
+ y: number,
140
+ ): Platform | null {
141
+ for (const p of platforms) {
142
+ if (Math.abs(y - p.top) < 1e-6 && overlapsX(p, x, halfW)) return p;
143
+ }
144
+ return null;
145
+ }
146
+
147
+ /**
148
+ * Swept AABB landing test for one integration step: of all platforms the
149
+ * player overlaps whose surface the feet crossed while falling from prevY
150
+ * to nextY, returns the highest (so a fast fall can't tunnel through a
151
+ * stack of tops). Null when nothing was crossed. Callers gate on vy <= 0 —
152
+ * platforms are one-sided and jumping up through them is allowed.
153
+ */
154
+ export function landingPlatform(
155
+ platforms: readonly Platform[],
156
+ x: number,
157
+ halfW: number,
158
+ prevY: number,
159
+ nextY: number,
160
+ ): Platform | null {
161
+ let best: Platform | null = null;
162
+ for (const p of platforms) {
163
+ if (!overlapsX(p, x, halfW)) continue;
164
+ if (prevY >= p.top && nextY <= p.top) {
165
+ if (best === null || p.top > best.top) best = p;
166
+ }
167
+ }
168
+ return best;
169
+ }
170
+
171
+ /**
172
+ * Advances the sim by dtMs. Semi-implicit Euler for gravity, buffered +
173
+ * coyote jumps, variable jump height via a rising-speed cap while the jump
174
+ * key is not held, and swept landing resolution. Mutates `sim`; per-step
175
+ * results are reported through sim.justJumped / sim.landedImpact.
176
+ */
177
+ export function stepPlayer(
178
+ sim: PlayerSim,
179
+ input: SimInput,
180
+ dtMs: number,
181
+ now: number,
182
+ platforms: readonly Platform[],
183
+ cfg: PhysicsConfig = PHYSICS,
184
+ ): void {
185
+ const dt = dtMs / 1000;
186
+ sim.justJumped = false;
187
+ sim.landedImpact = 0;
188
+
189
+ // --- Horizontal: accelerate toward the input direction, clamp to world.
190
+ const dir = (input.right ? 1 : 0) - (input.left ? 1 : 0);
191
+ if (dir !== 0) sim.facing = dir as 1 | -1;
192
+ const rate = dir !== 0 ? cfg.accel : cfg.decel;
193
+ sim.vx = approach(sim.vx, dir * cfg.moveSpeed, rate * dt);
194
+ sim.x = Math.min(cfg.maxX, Math.max(cfg.minX, sim.x + sim.vx * dt));
195
+
196
+ // --- Jump: buffered presses + coyote window.
197
+ if (input.jumpPressed) sim.jumpBufferedAt = now;
198
+ const buffered = now - sim.jumpBufferedAt <= cfg.jumpBufferMs;
199
+ const canJump =
200
+ sim.grounded ||
201
+ (!sim.coyoteSpent && now - sim.lastGroundedAt <= cfg.coyoteMs);
202
+ if (buffered && canJump) {
203
+ sim.vy = cfg.jumpVel;
204
+ sim.grounded = false;
205
+ sim.coyoteSpent = true;
206
+ sim.jumpBufferedAt = Number.NEGATIVE_INFINITY;
207
+ sim.justJumped = true;
208
+ }
209
+
210
+ // Variable jump height: while rising with the jump key released, cap the
211
+ // climb — taps hop, holds soar.
212
+ if (!input.jumpHeld && sim.vy > cfg.jumpCutVel) sim.vy = cfg.jumpCutVel;
213
+
214
+ // --- Vertical: support check while grounded, integrate + land while not.
215
+ if (sim.grounded) {
216
+ if (supportAt(platforms, sim.x, cfg.halfW, sim.y)) {
217
+ sim.lastGroundedAt = now;
218
+ sim.coyoteSpent = false;
219
+ } else {
220
+ sim.grounded = false; // walked off a ledge; coyote clock is running
221
+ }
222
+ }
223
+ if (!sim.grounded) {
224
+ const prevY = sim.y;
225
+ sim.vy = Math.max(sim.vy - cfg.gravity * dt, -cfg.maxFall);
226
+ sim.y += sim.vy * dt;
227
+ if (sim.vy <= 0) {
228
+ const hit = landingPlatform(platforms, sim.x, cfg.halfW, prevY, sim.y);
229
+ if (hit) {
230
+ sim.landedImpact = -sim.vy;
231
+ sim.y = hit.top;
232
+ sim.vy = 0;
233
+ sim.grounded = true;
234
+ sim.coyoteSpent = false;
235
+ sim.lastGroundedAt = now;
236
+ }
237
+ }
238
+ }
239
+ }
240
+
241
+ // --- Held-key tracking ------------------------------------------------------
242
+
243
+ /**
244
+ * Demo-side held-key windows. Legacy terminals report no key releases and
245
+ * pause ~200-300ms before auto-repeat kicks in, so the engine's 150ms
246
+ * ctx.isKeyHeld window can blink off mid-hold. Movement gets a wider grace
247
+ * window; the jump key gets a tighter one so variable jump height still
248
+ * reads on a quick tap.
249
+ */
250
+ export const MOVE_HOLD_MS = 230;
251
+ export const JUMP_HOLD_MS = 140;
252
+
253
+ export type HeldAction = 'left' | 'right' | 'jump';
254
+
255
+ /** Last-seen press/repeat timestamps per action. */
256
+ export type HeldKeys = Record<HeldAction, number>;
257
+
258
+ export function createHeldKeys(): HeldKeys {
259
+ return {
260
+ left: Number.NEGATIVE_INFINITY,
261
+ right: Number.NEGATIVE_INFINITY,
262
+ jump: Number.NEGATIVE_INFINITY,
263
+ };
264
+ }
265
+
266
+ /** Records a press/repeat of `action` at `now`. */
267
+ export function noteHeld(held: HeldKeys, action: HeldAction, now: number) {
268
+ held[action] = now;
269
+ }
270
+
271
+ /** Clears `action` immediately (a real or synthetic key release). */
272
+ export function clearHeld(held: HeldKeys, action: HeldAction) {
273
+ held[action] = Number.NEGATIVE_INFINITY;
274
+ }
275
+
276
+ /** True while `action`'s last press is inside its hold window. */
277
+ export function isHeldActive(
278
+ held: HeldKeys,
279
+ action: HeldAction,
280
+ now: number,
281
+ windowMs = action === 'jump' ? JUMP_HOLD_MS : MOVE_HOLD_MS,
282
+ ): boolean {
283
+ return now - held[action] <= windowMs;
284
+ }