star-sdk-cli 0.1.17 → 0.1.19

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/dist/cli.mjs +320 -223
  2. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -54,27 +54,26 @@ Star.init({ gameId: '<paste gameId from .starrc>' });
54
54
  import Star from 'star-sdk';
55
55
  Star.init({ gameId: '<gameId from .starrc>' }); // run: npx star-sdk init
56
56
 
57
- Star.game(ctx => {
58
- const { canvas, width, height, ctx: c } = ctx;
57
+ Star.game(g => {
58
+ const { width, height, ctx } = g;
59
59
  let score = 0;
60
60
 
61
- // Preload audio
62
61
  Star.audio.preload({ coin: 'coin', jump: 'jump' });
63
62
 
64
- // Game loop
65
- ctx.loop((dt) => {
66
- c.fillStyle = '#111827';
67
- c.fillRect(0, 0, width, height);
68
- c.fillStyle = '#fff';
69
- c.font = '24px sans-serif';
70
- c.fillText(\`Score: \${score}\`, 20, 40);
71
- });
63
+ g.loop((dt) => {
64
+ // Input (polling \u2014 check once per frame)
65
+ if (g.tap) {
66
+ score += 10;
67
+ Star.audio.play('coin');
68
+ }
72
69
 
73
- // Input
74
- canvas.onclick = () => {
75
- score += 10;
76
- Star.audio.play('coin');
77
- };
70
+ // Draw
71
+ ctx.fillStyle = '#111827';
72
+ ctx.fillRect(0, 0, width, height);
73
+ ctx.fillStyle = '#fff';
74
+ ctx.font = '24px sans-serif';
75
+ ctx.fillText(\`Score: \${score}\`, 20, 40);
76
+ });
78
77
  });
79
78
  \`\`\`
80
79
 
@@ -82,33 +81,44 @@ Star.game(ctx => {
82
81
 
83
82
  ### Game Over Screen with Leaderboard
84
83
 
85
- Use DOM buttons (via \`ui.render()\` + \`on()\`) for all interactive UI \u2014 never draw buttons on canvas:
84
+ Use \`g.tap\` in the loop with if/else for priority \u2014 check buttons first, then general tap:
86
85
 
87
86
  \`\`\`javascript
88
- // UI button handler (register once \u2014 survives ui.render calls)
89
- ctx.on('click', '#lb-btn', () => Star.leaderboard.show());
90
- ctx.on('click', '#restart-btn', () => startGame());
87
+ const lbBtn = { x: 200, y: 260, w: 240, h: 50 };
88
+ const restartBtn = { x: 200, y: 330, w: 240, h: 50 };
91
89
 
92
- // Gameplay tap handler (single handler, state-based)
93
- canvas.addEventListener('pointerdown', () => {
94
- if (state === 'playing') { jump(); }
90
+ function inRect(tap, r) {
91
+ return tap.x >= r.x && tap.x <= r.x + r.w && tap.y >= r.y && tap.y <= r.y + r.h;
92
+ }
93
+
94
+ g.loop((dt) => {
95
+ // Input
96
+ if (g.tap) {
97
+ if (state === 'gameover') {
98
+ if (inRect(g.tap, lbBtn)) {
99
+ Star.leaderboard.show();
100
+ } else if (inRect(g.tap, restartBtn)) {
101
+ startGame();
102
+ }
103
+ } else if (state === 'playing') {
104
+ jump();
105
+ } else if (state === 'menu') {
106
+ startGame();
107
+ }
108
+ }
109
+
110
+ // Update & Draw ...
111
+
112
+ if (state === 'gameover') {
113
+ // Draw buttons on canvas
114
+ drawButton(ctx, 'VIEW LEADERBOARD', lbBtn);
115
+ drawButton(ctx, 'PLAY AGAIN', restartBtn);
116
+ }
95
117
  });
96
118
 
97
119
  function endGame() {
98
120
  state = 'gameover';
99
121
  Star.leaderboard.submit(score);
100
- ctx.ui.render(\`
101
- <div class="h-full flex flex-col items-center justify-center text-white">
102
- <div class="text-3xl font-bold mb-2">GAME OVER</div>
103
- <div class="text-6xl font-bold mb-6">\${score}</div>
104
- <button id="lb-btn" class="px-6 py-3 mb-4 bg-purple-600 rounded-lg font-bold">
105
- VIEW LEADERBOARD
106
- </button>
107
- <button id="restart-btn" class="px-6 py-3 bg-gray-700 rounded-lg">
108
- PLAY AGAIN
109
- </button>
110
- </div>
111
- \`);
112
122
  }
113
123
  \`\`\`
114
124
 
@@ -144,23 +154,38 @@ Star.audio.preload({ coin: 'coin', jump: 'jump' });
144
154
  Star.audio.play('coin'); // Works on mobile, desktop, everywhere
145
155
  \`\`\`
146
156
 
157
+ ### Hover Effects for Canvas Buttons
158
+
159
+ \`g.pointer\` tracks position every frame \u2014 use it for hover states:
160
+
161
+ \`\`\`javascript
162
+ g.loop((dt) => {
163
+ const hoverLb = state === 'gameover' && inRect(g.pointer, lbBtn);
164
+ drawButton('VIEW LEADERBOARD', lbBtn, hoverLb ? '#9061f9' : '#7c3aed');
165
+ g.canvas.style.cursor = hoverLb ? 'pointer' : 'default';
166
+ });
167
+ \`\`\`
168
+
147
169
  ### Coordinate Handling
148
170
 
171
+ \`g.tap\` and \`g.pointer\` are already in canvas-space coordinates. No conversion needed:
172
+
149
173
  \`\`\`javascript
150
- canvas.onclick = (e) => {
151
- const point = ctx.toStagePoint(e); // Correct coordinates
152
- console.log(point.x, point.y);
153
- };
174
+ g.loop((dt) => {
175
+ if (g.tap) {
176
+ console.log(g.tap.x, g.tap.y); // Already canvas-space
177
+ }
178
+ });
154
179
  \`\`\`
155
180
 
156
181
  ## Don't Do This
157
182
 
158
183
  - **Don't** create canvas manually - use \`Star.game()\`
159
- - **Don't** use \`setInterval\` for game loops - use \`ctx.loop()\`
184
+ - **Don't** use \`setInterval\` for game loops - use \`g.loop()\`
160
185
  - **Don't** destructure Star - use \`Star.audio\`, \`Star.leaderboard\`, etc.
161
186
  - **Don't** invent audio preset names - only 17 exist (see audio.md)
162
- - **Don't** draw buttons on canvas (fillRect + hit-test) \u2014 use \`ui.render()\` with HTML \`<button>\` elements + \`on()\` for clicks. Canvas-drawn "buttons" conflict with tap handlers and have no hover/focus states.
163
- - **Don't** register multiple \`canvas.addEventListener('pointerdown', ...)\` handlers \u2014 use ONE handler with state-based logic. Use \`on()\` for UI button clicks.
187
+ - **Don't** use \`canvas.addEventListener('pointerdown', ...)\` for input \u2014 use \`g.tap\` / \`g.pointer\` / \`g.released\` in the game loop. Polling prevents conflicting handler bugs.
188
+ - **Don't** register multiple event handlers for different game states \u2014 use ONE \`if (g.tap)\` block with if/else for priority.
164
189
 
165
190
  ## Audio Presets (Full List)
166
191
 
@@ -176,102 +201,120 @@ Only these 17 presets exist:
176
201
  import Star from 'star-sdk';
177
202
  Star.init({ gameId: '<gameId from .starrc>' }); // run: npx star-sdk init
178
203
 
179
- Star.game(ctx => {
180
- const { canvas, width, height, ctx: c, ui, on } = ctx;
204
+ Star.game(g => {
205
+ const { width, height, ctx } = g;
181
206
  let score = 0;
182
- let state = 'playing';
207
+ let state = 'menu';
183
208
  let playerY = height / 2;
184
209
  let obstacles = [];
210
+ let spawnTimer = 0;
185
211
 
186
- Star.audio.preload({
187
- jump: 'jump',
188
- coin: 'coin',
189
- hurt: 'hurt'
190
- });
212
+ Star.audio.preload({ jump: 'jump', coin: 'coin', hurt: 'hurt' });
191
213
 
192
- // Spawn obstacles
193
- setInterval(() => {
194
- if (state === 'playing') {
195
- obstacles.push({ x: width, y: Math.random() * height, passed: false });
196
- }
197
- }, 2000);
214
+ const lbBtn = { x: width / 2 - 120, y: height / 2 + 20, w: 240, h: 50 };
215
+ const restartBtn = { x: width / 2 - 120, y: height / 2 + 90, w: 240, h: 50 };
198
216
 
199
- // Gameplay input \u2014 ONE handler, state-based
200
- canvas.addEventListener('pointerdown', () => {
201
- if (state === 'playing') {
202
- playerY -= 50;
203
- Star.audio.play('jump');
204
- }
205
- });
217
+ function inRect(pt, r) {
218
+ return pt.x >= r.x && pt.x <= r.x + r.w && pt.y >= r.y && pt.y <= r.y + r.h;
219
+ }
206
220
 
207
- // UI button clicks \u2014 use on() for DOM buttons, not canvas hit-testing
208
- on('click', '#lb-btn', () => Star.leaderboard.show());
209
- on('click', '#restart-btn', () => startGame());
221
+ function drawButton(text, r) {
222
+ ctx.fillStyle = '#7c3aed';
223
+ ctx.fillRect(r.x, r.y, r.w, r.h);
224
+ ctx.fillStyle = '#fff';
225
+ ctx.font = 'bold 16px sans-serif';
226
+ ctx.textAlign = 'center';
227
+ ctx.fillText(text, r.x + r.w / 2, r.y + r.h / 2 + 6);
228
+ ctx.textAlign = 'left';
229
+ }
210
230
 
211
231
  function startGame() {
212
232
  state = 'playing';
213
233
  score = 0;
214
234
  playerY = height / 2;
215
235
  obstacles = [];
216
- ui.render(''); // Clear game-over UI
236
+ spawnTimer = 0;
217
237
  }
218
238
 
219
239
  function endGame() {
220
240
  state = 'gameover';
221
241
  Star.audio.play('hurt');
222
242
  Star.leaderboard.submit(score);
223
- // Game-over UI with DOM buttons (not canvas-drawn)
224
- ui.render(\`
225
- <div class="h-full flex flex-col items-center justify-center text-white">
226
- <div class="text-3xl font-bold mb-2">GAME OVER</div>
227
- <div class="text-6xl font-bold mb-6">\${score}</div>
228
- <button id="lb-btn" class="px-6 py-3 mb-4 bg-purple-600 rounded-lg font-bold">
229
- VIEW LEADERBOARD
230
- </button>
231
- <button id="restart-btn" class="px-6 py-3 bg-gray-700 rounded-lg">
232
- PLAY AGAIN
233
- </button>
234
- </div>
235
- \`);
236
243
  }
237
244
 
238
- ctx.loop((dt) => {
239
- if (state !== 'playing') return;
240
-
241
- // Clear
242
- c.fillStyle = '#111827';
243
- c.fillRect(0, 0, width, height);
244
-
245
- // Update obstacles
246
- obstacles.forEach(obs => {
247
- obs.x -= 200 * dt;
248
- if (!obs.passed && obs.x < 50) {
249
- obs.passed = true;
250
- score += 10;
251
- Star.audio.play('coin');
245
+ g.loop((dt) => {
246
+ // --- Input (polling) ---
247
+ if (g.tap) {
248
+ if (state === 'menu') {
249
+ startGame();
250
+ } else if (state === 'playing') {
251
+ playerY -= 50;
252
+ Star.audio.play('jump');
253
+ } else if (state === 'gameover') {
254
+ if (inRect(g.tap, lbBtn)) {
255
+ Star.leaderboard.show();
256
+ } else if (inRect(g.tap, restartBtn)) {
257
+ startGame();
258
+ }
252
259
  }
253
- if (Math.abs(obs.x - 50) < 20 && Math.abs(obs.y - playerY) < 30) {
254
- endGame();
260
+ }
261
+
262
+ // --- Update ---
263
+ if (state === 'playing') {
264
+ spawnTimer += dt;
265
+ if (spawnTimer > 2) {
266
+ obstacles.push({ x: width, y: Math.random() * height, passed: false });
267
+ spawnTimer = 0;
255
268
  }
256
- });
257
- obstacles = obstacles.filter(o => o.x > -20);
258
-
259
- // Draw player
260
- c.fillStyle = '#3b82f6';
261
- c.beginPath();
262
- c.arc(50, playerY, 15, 0, Math.PI * 2);
263
- c.fill();
264
-
265
- // Draw obstacles
266
- c.fillStyle = '#a855f7';
267
- obstacles.forEach(obs => {
268
- c.fillRect(obs.x - 10, obs.y - 25, 20, 50);
269
- });
269
+ obstacles.forEach(obs => {
270
+ obs.x -= 200 * dt;
271
+ if (!obs.passed && obs.x < 50) {
272
+ obs.passed = true;
273
+ score += 10;
274
+ Star.audio.play('coin');
275
+ }
276
+ if (Math.abs(obs.x - 50) < 20 && Math.abs(obs.y - playerY) < 30) {
277
+ endGame();
278
+ }
279
+ });
280
+ obstacles = obstacles.filter(o => o.x > -20);
281
+ }
270
282
 
271
- // Draw score
272
- c.fillStyle = '#fff';
273
- c.font = '24px sans-serif';
274
- c.fillText(\`Score: \${score}\`, 20, 40);
283
+ // --- Draw ---
284
+ ctx.fillStyle = '#111827';
285
+ ctx.fillRect(0, 0, width, height);
286
+
287
+ if (state === 'menu') {
288
+ ctx.fillStyle = '#fff';
289
+ ctx.font = 'bold 32px sans-serif';
290
+ ctx.textAlign = 'center';
291
+ ctx.fillText('TAP TO START', width / 2, height / 2);
292
+ ctx.textAlign = 'left';
293
+ } else if (state === 'playing' || state === 'gameover') {
294
+ ctx.fillStyle = '#3b82f6';
295
+ ctx.beginPath();
296
+ ctx.arc(50, playerY, 15, 0, Math.PI * 2);
297
+ ctx.fill();
298
+ ctx.fillStyle = '#a855f7';
299
+ obstacles.forEach(obs => ctx.fillRect(obs.x - 10, obs.y - 25, 20, 50));
300
+ ctx.fillStyle = '#fff';
301
+ ctx.font = '24px sans-serif';
302
+ ctx.fillText(\`Score: \${score}\`, 20, 40);
303
+ }
304
+
305
+ if (state === 'gameover') {
306
+ ctx.fillStyle = 'rgba(0,0,0,0.6)';
307
+ ctx.fillRect(0, 0, width, height);
308
+ ctx.fillStyle = '#fff';
309
+ ctx.font = 'bold 32px sans-serif';
310
+ ctx.textAlign = 'center';
311
+ ctx.fillText('GAME OVER', width / 2, height / 2 - 40);
312
+ ctx.font = 'bold 48px sans-serif';
313
+ ctx.fillText(score, width / 2, height / 2);
314
+ ctx.textAlign = 'left';
315
+ drawButton('VIEW LEADERBOARD', lbBtn);
316
+ drawButton('PLAY AGAIN', restartBtn);
317
+ }
275
318
  });
276
319
  });
277
320
  \`\`\`
@@ -463,40 +506,26 @@ Import \`game\` and wrap your code in it. The \`game\` function handles DOM read
463
506
  \`\`\`ts
464
507
  import { game } from 'star-canvas';
465
508
 
466
- game(({ ctx, width, height, on, loop, ui, canvas }) => {
467
- // ctx: The 2D canvas context
468
- // width, height: The logical size (CSS pixels) - READ-ONLY
469
- // on: Safe, delegated event listener
470
- // loop: Stable game loop (with dt)
471
- // ui: Safe overlay for HTML
472
- // canvas: The <canvas> element
473
-
474
- // 1. Draw on the canvas
475
- loop((dt) => {
476
- ctx.clearRect(0, 0, width, height);
477
- ctx.fillStyle = '#3b82f6'; // blue-500
478
- ctx.fillRect(width / 2 - 25, height / 2 - 25, 50, 50);
479
- });
480
-
481
- // 2. Render HTML to the safe UI overlay
482
- ui.render(\`
483
- <div class="absolute top-4 left-4 text-white">
484
- <button id="start-btn" class="px-4 py-2 bg-blue-500 rounded">
485
- Click Me
486
- </button>
487
- </div>
488
- \`);
509
+ game((g) => {
510
+ const { ctx, width, height } = g;
511
+ let score = 0;
489
512
 
490
- // 3. Listen for button clicks \u2014 on() auto-enables pointer-events for the target
491
- on('click', '#start-btn', () => {
492
- console.log('Button clicked!');
493
- });
513
+ // Game loop \u2014 input, update, draw
514
+ g.loop((dt) => {
515
+ // Input: check g.tap each frame (null if no tap)
516
+ if (g.tap) {
517
+ score++;
518
+ }
494
519
 
495
- // 4. Gameplay taps \u2014 use canvas.addEventListener for game mechanics only
496
- // For buttons/menus, use ui.render() + on() above
497
- // The SDK auto-suppresses this handler when a UI button is clicked
498
- canvas.addEventListener('pointerdown', (e) => {
499
- console.log('Gameplay tap!', e);
520
+ // Draw
521
+ ctx.fillStyle = '#0f172a';
522
+ ctx.fillRect(0, 0, width, height);
523
+ ctx.fillStyle = '#3b82f6';
524
+ ctx.font = 'bold 32px sans-serif';
525
+ ctx.textAlign = 'center';
526
+ ctx.fillText(\`Score: \${score}\`, width / 2, height / 2);
527
+ ctx.fillText('TAP ANYWHERE', width / 2, height / 2 + 40);
528
+ ctx.textAlign = 'left';
500
529
  });
501
530
  });
502
531
  \`\`\`
@@ -524,9 +553,8 @@ The 2D drawing context. Its transform is already scaled for DPR. You **always dr
524
553
 
525
554
  The \`<canvas>\` element itself.
526
555
 
527
- - **Use this for gameplay input** (e.g., \`pointerdown\` for tap-to-jump, \`pointermove\` for aiming).
528
- - **For buttons and menus, use \`ui.render()\` + \`on()\` instead** \u2014 HTML buttons get hover states, touch targets, accessibility, and never conflict with gameplay handlers.
529
- - Use ONE \`addEventListener\` handler with state-based logic. Multiple pointerdown handlers cause ordering bugs.
556
+ - For drawing, use \`ctx\` (the 2D context).
557
+ - For input, use \`g.tap\` / \`g.pointer\` / \`g.released\` in the game loop (see Input Polling below).
530
558
 
531
559
  ### \`width: number\` (getter)
532
560
 
@@ -561,7 +589,69 @@ A safe manager for your HTML overlay, stacked on top of the canvas.
561
589
  - \`ui.el(selector)\`: Scoped \`querySelector\` for the UI root.
562
590
  - \`ui.all(selector)\`: Scoped \`querySelectorAll\` for the UI root.
563
591
 
564
- **Auto-detection:** When you add \`canvas.addEventListener('pointerdown', ...)\`, the SDK automatically makes UI click-through so taps reach the canvas. Elements targeted by \`on()\` are automatically interactive \u2014 no extra CSS classes needed. Native \`<button>\` and \`<a>\` elements are also always interactive.
592
+ ### Input Polling: \`tap\`, \`pointer\`, \`released\`
593
+
594
+ **The standard way to handle input.** Read these in your game loop \u2014 no event handlers needed.
595
+
596
+ \`\`\`ts
597
+ g.loop((dt) => {
598
+ if (g.tap) {
599
+ // Tap/click happened this frame. g.tap.x, g.tap.y are canvas-space.
600
+ }
601
+ if (g.pointer.down) {
602
+ // Pointer is held. g.pointer.x, g.pointer.y track current position.
603
+ }
604
+ if (g.released) {
605
+ // Pointer released this frame.
606
+ }
607
+ });
608
+ \`\`\`
609
+
610
+ - **\`g.tap\`** \u2014 \`{ x, y, time } | null\`. Non-null on the frame a pointerdown occurs. Cleared next frame.
611
+ - **\`g.pointer\`** \u2014 \`{ x, y, down }\`. Always available. Tracks current pointer position and held state.
612
+ - **\`g.released\`** \u2014 \`{ x, y, time } | null\`. Non-null on the frame a pointerup occurs. Cleared next frame.
613
+ - **\`g.taps\`** \u2014 \`Array<{ x, y, id, time }>\`. All taps this frame (multi-touch).
614
+ - **\`g.pointers\`** \u2014 \`Array<{ x, y, id, down }>\`. All active pointers (multi-touch).
615
+
616
+ **Why polling?** One code path with if/else for priority. No conflicting event handlers, no registration order bugs. This is how Unity, Godot, and every game engine handles input.
617
+
618
+ **Button priority pattern:**
619
+ \`\`\`ts
620
+ if (g.tap) {
621
+ if (state === 'gameover' && inRect(g.tap, leaderboardBtn)) {
622
+ leaderboard.show(); // Checked first \u2014 highest priority
623
+ } else if (state === 'gameover') {
624
+ startGame(); // Generic tap \u2014 lower priority
625
+ } else if (state === 'playing') {
626
+ jump();
627
+ }
628
+ }
629
+ \`\`\`
630
+
631
+ **Hold-to-fire with cooldown:**
632
+ \`\`\`ts
633
+ let fireCooldown = 0;
634
+ const FIRE_RATE = 0.15; // seconds between shots
635
+
636
+ g.loop((dt) => {
637
+ fireCooldown -= dt;
638
+ if (g.pointer.down && fireCooldown <= 0) {
639
+ spawnBullet();
640
+ fireCooldown = FIRE_RATE; // Slow shotgun: 0.8, fast minigun: 0.05
641
+ }
642
+ });
643
+ \`\`\`
644
+
645
+ **Hover effects for canvas buttons:**
646
+ \`\`\`ts
647
+ g.loop((dt) => {
648
+ const hoverLb = state === 'gameover' && inRect(g.pointer, lbBtn);
649
+ drawButton('VIEW LEADERBOARD', lbBtn, hoverLb ? '#9061f9' : '#7c3aed');
650
+ g.canvas.style.cursor = hoverLb ? 'pointer' : 'default';
651
+ });
652
+ \`\`\`
653
+
654
+ Taps on interactive UI elements (from \`on()\` or native \`<button>\`) are automatically suppressed from \`g.tap\`.
565
655
 
566
656
  ### Cursor Management
567
657
 
@@ -706,7 +796,8 @@ For puzzle games, card games, match-3, mobile-style games - use portrait preset.
706
796
  \`\`\`ts
707
797
  import { game } from 'star-canvas';
708
798
 
709
- game(({ ctx, width, height, loop, canvas, toStagePoint }) => {
799
+ game((g) => {
800
+ const { ctx, width, height } = g;
710
801
  // width = 360, height = 640 (always, with letterboxing)
711
802
  const cellSize = 40;
712
803
  const gridCols = 8;
@@ -717,12 +808,17 @@ game(({ ctx, width, height, loop, canvas, toStagePoint }) => {
717
808
  const gridX = (width - gridWidth) / 2;
718
809
  const gridY = 80;
719
810
 
720
- canvas.addEventListener('pointerdown', (e) => {
721
- const { x, y } = toStagePoint(e);
722
- // Handle tap on grid...
723
- });
811
+ g.loop((dt) => {
812
+ // Input
813
+ if (g.tap) {
814
+ const col = Math.floor((g.tap.x - gridX) / cellSize);
815
+ const row = Math.floor((g.tap.y - gridY) / cellSize);
816
+ if (col >= 0 && col < gridCols && row >= 0 && row < gridRows) {
817
+ // Handle tap on grid cell...
818
+ }
819
+ }
724
820
 
725
- loop((dt) => {
821
+ // Draw
726
822
  ctx.fillStyle = '#111827';
727
823
  ctx.fillRect(0, 0, width, height);
728
824
 
@@ -747,16 +843,19 @@ For games that need different dimensions (e.g., pixel art at 320\xD7180).
747
843
  \`\`\`ts
748
844
  import { game } from 'star-canvas';
749
845
 
750
- game(({ ctx, width, height, loop, toStagePoint, canvas }) => {
846
+ game((g) => {
847
+ const { ctx, width, height } = g;
751
848
  // Custom 320\xD7180 resolution (retro pixel art style)
752
849
  const player = { x: 160, y: 90 }; // Center
753
850
 
754
- canvas.addEventListener('pointerdown', (e) => {
755
- const { x, y } = toStagePoint(e);
756
- console.log('Tapped at:', x, y); // Always 0-320, 0-180
757
- });
851
+ g.loop((dt) => {
852
+ // Input \u2014 g.tap coords are already in canvas-space (0-320, 0-180)
853
+ if (g.tap) {
854
+ player.x = g.tap.x;
855
+ player.y = g.tap.y;
856
+ }
758
857
 
759
- loop((dt) => {
858
+ // Draw
760
859
  ctx.fillStyle = '#111827';
761
860
  ctx.fillRect(0, 0, width, height);
762
861
 
@@ -766,9 +865,9 @@ game(({ ctx, width, height, loop, toStagePoint, canvas }) => {
766
865
  }, { width: 320, height: 180 }); // Custom resolution with letterboxing
767
866
  \`\`\`
768
867
 
769
- ### Recipe 5: Complex Game with Canvas + UI + Events (like FLOW)
868
+ ### Recipe 5: Complex Game with Canvas + Leaderboard
770
869
 
771
- **Key pattern:** Canvas for gameplay input, DOM buttons for UI. Never draw buttons on canvas.
870
+ **Key pattern:** All input via \`g.tap\` in the loop. if/else for priority \u2014 check buttons first.
772
871
 
773
872
  \`\`\`ts
774
873
  import { game } from 'star-canvas';
@@ -776,77 +875,75 @@ import { createLeaderboard } from 'star-leaderboard';
776
875
 
777
876
  const leaderboard = createLeaderboard({ gameId: '<gameId from .starrc>' });
778
877
 
779
- game(({ ctx, width, height, loop, ui, on, canvas, toStagePoint }) => {
878
+ game((g) => {
879
+ const { ctx, width, height } = g;
780
880
  let score = 0;
781
881
  let state = 'menu';
782
882
 
783
- // 1. ONE gameplay tap handler \u2014 state-based logic, no button hit-testing
784
- canvas.addEventListener('pointerdown', () => {
785
- if (state === 'menu') startGame();
786
- else if (state === 'playing') {
787
- // ... (player float logic) ...
788
- }
789
- });
790
-
791
- // 2. DOM button clicks \u2014 on() auto-enables pointer-events
792
- // The SDK suppresses the canvas handler when a button is clicked
793
- on('click', '#leaderboard-btn', () => leaderboard.show());
794
- on('click', '#restart-btn', () => startGame());
795
-
796
- // 3. Render UI \u2014 elements targeted by on() are automatically interactive
797
- let lastState = null;
798
- let lastScore = -1;
883
+ const lbBtn = { x: width / 2 - 120, y: height / 2 + 20, w: 240, h: 50 };
884
+ const restartBtn = { x: width / 2 - 120, y: height / 2 + 90, w: 240, h: 50 };
799
885
 
800
- function updateUI() {
801
- // CRITICAL: Only render when state/score changes, NOT every frame
802
- // Calling ui.render() in the loop breaks buttons (DOM recreation)
803
- if (state === lastState && score === lastScore) return;
804
- lastState = state;
805
- lastScore = score;
806
-
807
- if (state === 'menu') {
808
- ui.render(\`
809
- <div class="h-full flex flex-col items-center justify-center text-white">
810
- <h1 class="text-6xl font-bold mb-4">FLOW</h1>
811
- <div class="text-2xl animate-pulse">TAP TO START</div>
812
- </div>\`);
813
- } else if (state === 'playing') {
814
- ui.render(\`
815
- <div class="absolute top-8 left-1/2 -translate-x-1/2 text-white">
816
- <div class="text-5xl font-bold">\\\${score}</div>
817
- </div>\`);
818
- } else if (state === 'gameover') {
819
- ui.render(\`
820
- <div class="h-full flex flex-col items-center justify-center text-white">
821
- <div class="text-3xl mb-4">GAME OVER</div>
822
- <div class="text-6xl mb-4">\\\${score}</div>
823
- <button id="leaderboard-btn" class="px-6 py-3 mb-4 bg-gradient-to-r from-blue-600 to-purple-600 rounded-xl font-bold shadow-lg shadow-blue-500/20">
824
- VIEW LEADERBOARD
825
- </button>
826
- <button id="restart-btn" class="px-6 py-3 bg-gray-700 rounded-xl font-bold">
827
- PLAY AGAIN
828
- </button>
829
- </div>\`);
830
- }
886
+ function inRect(pt, r) {
887
+ return pt.x >= r.x && pt.x <= r.x + r.w && pt.y >= r.y && pt.y <= r.y + r.h;
831
888
  }
832
889
 
833
- // 4. Call updateUI when state changes (NOT every frame)
834
- updateUI();
835
-
836
- // Update when state transitions happen
837
- function startGame() {
838
- state = 'playing';
839
- score = 0;
840
- ui.render(''); // Clear game-over buttons
841
- updateUI();
890
+ function drawButton(text, r, color) {
891
+ ctx.fillStyle = color || '#7c3aed';
892
+ ctx.fillRect(r.x, r.y, r.w, r.h);
893
+ ctx.fillStyle = '#fff';
894
+ ctx.font = 'bold 16px sans-serif';
895
+ ctx.textAlign = 'center';
896
+ ctx.fillText(text, r.x + r.w / 2, r.y + r.h / 2 + 6);
842
897
  }
843
898
 
899
+ function startGame() { state = 'playing'; score = 0; }
900
+
844
901
  function endGame() {
845
902
  state = 'gameover';
846
- // Submit score to leaderboard
847
903
  leaderboard.submit(score);
848
- updateUI();
849
904
  }
905
+
906
+ g.loop((dt) => {
907
+ // --- Input (polling) ---
908
+ if (g.tap) {
909
+ if (state === 'gameover') {
910
+ if (inRect(g.tap, lbBtn)) leaderboard.show();
911
+ else if (inRect(g.tap, restartBtn)) startGame();
912
+ } else if (state === 'menu') {
913
+ startGame();
914
+ } else if (state === 'playing') {
915
+ // ... (player jump/action logic) ...
916
+ }
917
+ }
918
+
919
+ // --- Draw ---
920
+ ctx.fillStyle = '#111827';
921
+ ctx.fillRect(0, 0, width, height);
922
+
923
+ if (state === 'menu') {
924
+ ctx.fillStyle = '#fff';
925
+ ctx.font = 'bold 48px sans-serif';
926
+ ctx.textAlign = 'center';
927
+ ctx.fillText('FLOW', width / 2, height / 2 - 20);
928
+ ctx.font = '24px sans-serif';
929
+ ctx.fillText('TAP TO START', width / 2, height / 2 + 30);
930
+ } else if (state === 'playing') {
931
+ ctx.fillStyle = '#fff';
932
+ ctx.font = 'bold 48px sans-serif';
933
+ ctx.textAlign = 'center';
934
+ ctx.fillText(String(score), width / 2, 60);
935
+ } else if (state === 'gameover') {
936
+ ctx.fillStyle = '#fff';
937
+ ctx.font = '24px sans-serif';
938
+ ctx.textAlign = 'center';
939
+ ctx.fillText('GAME OVER', width / 2, height / 2 - 60);
940
+ ctx.font = 'bold 48px sans-serif';
941
+ ctx.fillText(String(score), width / 2, height / 2 - 10);
942
+ drawButton('VIEW LEADERBOARD', lbBtn, '#7c3aed');
943
+ drawButton('PLAY AGAIN', restartBtn, '#374151');
944
+ }
945
+ ctx.textAlign = 'left';
946
+ });
850
947
  });
851
948
  \`\`\`
852
949
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "star-sdk-cli",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "CLI for Star SDK — register games, install AI docs, and deploy to Star hosting",
5
5
  "type": "module",
6
6
  "bin": {