star-sdk-cli 0.1.17 → 0.1.18

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 +299 -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 };
89
+
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
+ }
91
93
 
92
- // Gameplay tap handler (single handler, state-based)
93
- canvas.addEventListener('pointerdown', () => {
94
- if (state === 'playing') { jump(); }
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
 
@@ -146,21 +156,24 @@ Star.audio.play('coin'); // Works on mobile, desktop, everywhere
146
156
 
147
157
  ### Coordinate Handling
148
158
 
159
+ \`g.tap\` and \`g.pointer\` are already in canvas-space coordinates. No conversion needed:
160
+
149
161
  \`\`\`javascript
150
- canvas.onclick = (e) => {
151
- const point = ctx.toStagePoint(e); // Correct coordinates
152
- console.log(point.x, point.y);
153
- };
162
+ g.loop((dt) => {
163
+ if (g.tap) {
164
+ console.log(g.tap.x, g.tap.y); // Already canvas-space
165
+ }
166
+ });
154
167
  \`\`\`
155
168
 
156
169
  ## Don't Do This
157
170
 
158
171
  - **Don't** create canvas manually - use \`Star.game()\`
159
- - **Don't** use \`setInterval\` for game loops - use \`ctx.loop()\`
172
+ - **Don't** use \`setInterval\` for game loops - use \`g.loop()\`
160
173
  - **Don't** destructure Star - use \`Star.audio\`, \`Star.leaderboard\`, etc.
161
174
  - **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.
175
+ - **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.
176
+ - **Don't** register multiple event handlers for different game states \u2014 use ONE \`if (g.tap)\` block with if/else for priority.
164
177
 
165
178
  ## Audio Presets (Full List)
166
179
 
@@ -176,102 +189,120 @@ Only these 17 presets exist:
176
189
  import Star from 'star-sdk';
177
190
  Star.init({ gameId: '<gameId from .starrc>' }); // run: npx star-sdk init
178
191
 
179
- Star.game(ctx => {
180
- const { canvas, width, height, ctx: c, ui, on } = ctx;
192
+ Star.game(g => {
193
+ const { width, height, ctx } = g;
181
194
  let score = 0;
182
- let state = 'playing';
195
+ let state = 'menu';
183
196
  let playerY = height / 2;
184
197
  let obstacles = [];
198
+ let spawnTimer = 0;
185
199
 
186
- Star.audio.preload({
187
- jump: 'jump',
188
- coin: 'coin',
189
- hurt: 'hurt'
190
- });
200
+ Star.audio.preload({ jump: 'jump', coin: 'coin', hurt: 'hurt' });
191
201
 
192
- // Spawn obstacles
193
- setInterval(() => {
194
- if (state === 'playing') {
195
- obstacles.push({ x: width, y: Math.random() * height, passed: false });
196
- }
197
- }, 2000);
202
+ const lbBtn = { x: width / 2 - 120, y: height / 2 + 20, w: 240, h: 50 };
203
+ const restartBtn = { x: width / 2 - 120, y: height / 2 + 90, w: 240, h: 50 };
198
204
 
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
- });
205
+ function inRect(pt, r) {
206
+ return pt.x >= r.x && pt.x <= r.x + r.w && pt.y >= r.y && pt.y <= r.y + r.h;
207
+ }
206
208
 
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());
209
+ function drawButton(text, r) {
210
+ ctx.fillStyle = '#7c3aed';
211
+ ctx.fillRect(r.x, r.y, r.w, r.h);
212
+ ctx.fillStyle = '#fff';
213
+ ctx.font = 'bold 16px sans-serif';
214
+ ctx.textAlign = 'center';
215
+ ctx.fillText(text, r.x + r.w / 2, r.y + r.h / 2 + 6);
216
+ ctx.textAlign = 'left';
217
+ }
210
218
 
211
219
  function startGame() {
212
220
  state = 'playing';
213
221
  score = 0;
214
222
  playerY = height / 2;
215
223
  obstacles = [];
216
- ui.render(''); // Clear game-over UI
224
+ spawnTimer = 0;
217
225
  }
218
226
 
219
227
  function endGame() {
220
228
  state = 'gameover';
221
229
  Star.audio.play('hurt');
222
230
  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
231
  }
237
232
 
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');
233
+ g.loop((dt) => {
234
+ // --- Input (polling) ---
235
+ if (g.tap) {
236
+ if (state === 'menu') {
237
+ startGame();
238
+ } else if (state === 'playing') {
239
+ playerY -= 50;
240
+ Star.audio.play('jump');
241
+ } else if (state === 'gameover') {
242
+ if (inRect(g.tap, lbBtn)) {
243
+ Star.leaderboard.show();
244
+ } else if (inRect(g.tap, restartBtn)) {
245
+ startGame();
246
+ }
252
247
  }
253
- if (Math.abs(obs.x - 50) < 20 && Math.abs(obs.y - playerY) < 30) {
254
- endGame();
248
+ }
249
+
250
+ // --- Update ---
251
+ if (state === 'playing') {
252
+ spawnTimer += dt;
253
+ if (spawnTimer > 2) {
254
+ obstacles.push({ x: width, y: Math.random() * height, passed: false });
255
+ spawnTimer = 0;
255
256
  }
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
- });
257
+ obstacles.forEach(obs => {
258
+ obs.x -= 200 * dt;
259
+ if (!obs.passed && obs.x < 50) {
260
+ obs.passed = true;
261
+ score += 10;
262
+ Star.audio.play('coin');
263
+ }
264
+ if (Math.abs(obs.x - 50) < 20 && Math.abs(obs.y - playerY) < 30) {
265
+ endGame();
266
+ }
267
+ });
268
+ obstacles = obstacles.filter(o => o.x > -20);
269
+ }
270
+
271
+ // --- Draw ---
272
+ ctx.fillStyle = '#111827';
273
+ ctx.fillRect(0, 0, width, height);
274
+
275
+ if (state === 'menu') {
276
+ ctx.fillStyle = '#fff';
277
+ ctx.font = 'bold 32px sans-serif';
278
+ ctx.textAlign = 'center';
279
+ ctx.fillText('TAP TO START', width / 2, height / 2);
280
+ ctx.textAlign = 'left';
281
+ } else if (state === 'playing' || state === 'gameover') {
282
+ ctx.fillStyle = '#3b82f6';
283
+ ctx.beginPath();
284
+ ctx.arc(50, playerY, 15, 0, Math.PI * 2);
285
+ ctx.fill();
286
+ ctx.fillStyle = '#a855f7';
287
+ obstacles.forEach(obs => ctx.fillRect(obs.x - 10, obs.y - 25, 20, 50));
288
+ ctx.fillStyle = '#fff';
289
+ ctx.font = '24px sans-serif';
290
+ ctx.fillText(\`Score: \${score}\`, 20, 40);
291
+ }
270
292
 
271
- // Draw score
272
- c.fillStyle = '#fff';
273
- c.font = '24px sans-serif';
274
- c.fillText(\`Score: \${score}\`, 20, 40);
293
+ if (state === 'gameover') {
294
+ ctx.fillStyle = 'rgba(0,0,0,0.6)';
295
+ ctx.fillRect(0, 0, width, height);
296
+ ctx.fillStyle = '#fff';
297
+ ctx.font = 'bold 32px sans-serif';
298
+ ctx.textAlign = 'center';
299
+ ctx.fillText('GAME OVER', width / 2, height / 2 - 40);
300
+ ctx.font = 'bold 48px sans-serif';
301
+ ctx.fillText(score, width / 2, height / 2);
302
+ ctx.textAlign = 'left';
303
+ drawButton('VIEW LEADERBOARD', lbBtn);
304
+ drawButton('PLAY AGAIN', restartBtn);
305
+ }
275
306
  });
276
307
  });
277
308
  \`\`\`
@@ -463,40 +494,26 @@ Import \`game\` and wrap your code in it. The \`game\` function handles DOM read
463
494
  \`\`\`ts
464
495
  import { game } from 'star-canvas';
465
496
 
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
- \`);
497
+ game((g) => {
498
+ const { ctx, width, height } = g;
499
+ let score = 0;
489
500
 
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
- });
501
+ // Game loop \u2014 input, update, draw
502
+ g.loop((dt) => {
503
+ // Input: check g.tap each frame (null if no tap)
504
+ if (g.tap) {
505
+ score++;
506
+ }
494
507
 
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);
508
+ // Draw
509
+ ctx.fillStyle = '#0f172a';
510
+ ctx.fillRect(0, 0, width, height);
511
+ ctx.fillStyle = '#3b82f6';
512
+ ctx.font = 'bold 32px sans-serif';
513
+ ctx.textAlign = 'center';
514
+ ctx.fillText(\`Score: \${score}\`, width / 2, height / 2);
515
+ ctx.fillText('TAP ANYWHERE', width / 2, height / 2 + 40);
516
+ ctx.textAlign = 'left';
500
517
  });
501
518
  });
502
519
  \`\`\`
@@ -524,9 +541,8 @@ The 2D drawing context. Its transform is already scaled for DPR. You **always dr
524
541
 
525
542
  The \`<canvas>\` element itself.
526
543
 
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.
544
+ - For drawing, use \`ctx\` (the 2D context).
545
+ - For input, use \`g.tap\` / \`g.pointer\` / \`g.released\` in the game loop (see Input Polling below).
530
546
 
531
547
  ### \`width: number\` (getter)
532
548
 
@@ -561,7 +577,60 @@ A safe manager for your HTML overlay, stacked on top of the canvas.
561
577
  - \`ui.el(selector)\`: Scoped \`querySelector\` for the UI root.
562
578
  - \`ui.all(selector)\`: Scoped \`querySelectorAll\` for the UI root.
563
579
 
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.
580
+ ### Input Polling: \`tap\`, \`pointer\`, \`released\`
581
+
582
+ **The standard way to handle input.** Read these in your game loop \u2014 no event handlers needed.
583
+
584
+ \`\`\`ts
585
+ g.loop((dt) => {
586
+ if (g.tap) {
587
+ // Tap/click happened this frame. g.tap.x, g.tap.y are canvas-space.
588
+ }
589
+ if (g.pointer.down) {
590
+ // Pointer is held. g.pointer.x, g.pointer.y track current position.
591
+ }
592
+ if (g.released) {
593
+ // Pointer released this frame.
594
+ }
595
+ });
596
+ \`\`\`
597
+
598
+ - **\`g.tap\`** \u2014 \`{ x, y, time } | null\`. Non-null on the frame a pointerdown occurs. Cleared next frame.
599
+ - **\`g.pointer\`** \u2014 \`{ x, y, down }\`. Always available. Tracks current pointer position and held state.
600
+ - **\`g.released\`** \u2014 \`{ x, y, time } | null\`. Non-null on the frame a pointerup occurs. Cleared next frame.
601
+ - **\`g.taps\`** \u2014 \`Array<{ x, y, id, time }>\`. All taps this frame (multi-touch).
602
+ - **\`g.pointers\`** \u2014 \`Array<{ x, y, id, down }>\`. All active pointers (multi-touch).
603
+
604
+ **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.
605
+
606
+ **Button priority pattern:**
607
+ \`\`\`ts
608
+ if (g.tap) {
609
+ if (state === 'gameover' && inRect(g.tap, leaderboardBtn)) {
610
+ leaderboard.show(); // Checked first \u2014 highest priority
611
+ } else if (state === 'gameover') {
612
+ startGame(); // Generic tap \u2014 lower priority
613
+ } else if (state === 'playing') {
614
+ jump();
615
+ }
616
+ }
617
+ \`\`\`
618
+
619
+ **Hold-to-fire with cooldown:**
620
+ \`\`\`ts
621
+ let fireCooldown = 0;
622
+ const FIRE_RATE = 0.15; // seconds between shots
623
+
624
+ g.loop((dt) => {
625
+ fireCooldown -= dt;
626
+ if (g.pointer.down && fireCooldown <= 0) {
627
+ spawnBullet();
628
+ fireCooldown = FIRE_RATE; // Slow shotgun: 0.8, fast minigun: 0.05
629
+ }
630
+ });
631
+ \`\`\`
632
+
633
+ Taps on interactive UI elements (from \`on()\` or native \`<button>\`) are automatically suppressed from \`g.tap\`.
565
634
 
566
635
  ### Cursor Management
567
636
 
@@ -706,7 +775,8 @@ For puzzle games, card games, match-3, mobile-style games - use portrait preset.
706
775
  \`\`\`ts
707
776
  import { game } from 'star-canvas';
708
777
 
709
- game(({ ctx, width, height, loop, canvas, toStagePoint }) => {
778
+ game((g) => {
779
+ const { ctx, width, height } = g;
710
780
  // width = 360, height = 640 (always, with letterboxing)
711
781
  const cellSize = 40;
712
782
  const gridCols = 8;
@@ -717,12 +787,17 @@ game(({ ctx, width, height, loop, canvas, toStagePoint }) => {
717
787
  const gridX = (width - gridWidth) / 2;
718
788
  const gridY = 80;
719
789
 
720
- canvas.addEventListener('pointerdown', (e) => {
721
- const { x, y } = toStagePoint(e);
722
- // Handle tap on grid...
723
- });
790
+ g.loop((dt) => {
791
+ // Input
792
+ if (g.tap) {
793
+ const col = Math.floor((g.tap.x - gridX) / cellSize);
794
+ const row = Math.floor((g.tap.y - gridY) / cellSize);
795
+ if (col >= 0 && col < gridCols && row >= 0 && row < gridRows) {
796
+ // Handle tap on grid cell...
797
+ }
798
+ }
724
799
 
725
- loop((dt) => {
800
+ // Draw
726
801
  ctx.fillStyle = '#111827';
727
802
  ctx.fillRect(0, 0, width, height);
728
803
 
@@ -747,16 +822,19 @@ For games that need different dimensions (e.g., pixel art at 320\xD7180).
747
822
  \`\`\`ts
748
823
  import { game } from 'star-canvas';
749
824
 
750
- game(({ ctx, width, height, loop, toStagePoint, canvas }) => {
825
+ game((g) => {
826
+ const { ctx, width, height } = g;
751
827
  // Custom 320\xD7180 resolution (retro pixel art style)
752
828
  const player = { x: 160, y: 90 }; // Center
753
829
 
754
- canvas.addEventListener('pointerdown', (e) => {
755
- const { x, y } = toStagePoint(e);
756
- console.log('Tapped at:', x, y); // Always 0-320, 0-180
757
- });
830
+ g.loop((dt) => {
831
+ // Input \u2014 g.tap coords are already in canvas-space (0-320, 0-180)
832
+ if (g.tap) {
833
+ player.x = g.tap.x;
834
+ player.y = g.tap.y;
835
+ }
758
836
 
759
- loop((dt) => {
837
+ // Draw
760
838
  ctx.fillStyle = '#111827';
761
839
  ctx.fillRect(0, 0, width, height);
762
840
 
@@ -766,9 +844,9 @@ game(({ ctx, width, height, loop, toStagePoint, canvas }) => {
766
844
  }, { width: 320, height: 180 }); // Custom resolution with letterboxing
767
845
  \`\`\`
768
846
 
769
- ### Recipe 5: Complex Game with Canvas + UI + Events (like FLOW)
847
+ ### Recipe 5: Complex Game with Canvas + Leaderboard
770
848
 
771
- **Key pattern:** Canvas for gameplay input, DOM buttons for UI. Never draw buttons on canvas.
849
+ **Key pattern:** All input via \`g.tap\` in the loop. if/else for priority \u2014 check buttons first.
772
850
 
773
851
  \`\`\`ts
774
852
  import { game } from 'star-canvas';
@@ -776,77 +854,75 @@ import { createLeaderboard } from 'star-leaderboard';
776
854
 
777
855
  const leaderboard = createLeaderboard({ gameId: '<gameId from .starrc>' });
778
856
 
779
- game(({ ctx, width, height, loop, ui, on, canvas, toStagePoint }) => {
857
+ game((g) => {
858
+ const { ctx, width, height } = g;
780
859
  let score = 0;
781
860
  let state = 'menu';
782
861
 
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());
862
+ const lbBtn = { x: width / 2 - 120, y: height / 2 + 20, w: 240, h: 50 };
863
+ const restartBtn = { x: width / 2 - 120, y: height / 2 + 90, w: 240, h: 50 };
795
864
 
796
- // 3. Render UI \u2014 elements targeted by on() are automatically interactive
797
- let lastState = null;
798
- let lastScore = -1;
799
-
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
- }
865
+ function inRect(pt, r) {
866
+ return pt.x >= r.x && pt.x <= r.x + r.w && pt.y >= r.y && pt.y <= r.y + r.h;
831
867
  }
832
868
 
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();
869
+ function drawButton(text, r, color) {
870
+ ctx.fillStyle = color || '#7c3aed';
871
+ ctx.fillRect(r.x, r.y, r.w, r.h);
872
+ ctx.fillStyle = '#fff';
873
+ ctx.font = 'bold 16px sans-serif';
874
+ ctx.textAlign = 'center';
875
+ ctx.fillText(text, r.x + r.w / 2, r.y + r.h / 2 + 6);
842
876
  }
843
877
 
878
+ function startGame() { state = 'playing'; score = 0; }
879
+
844
880
  function endGame() {
845
881
  state = 'gameover';
846
- // Submit score to leaderboard
847
882
  leaderboard.submit(score);
848
- updateUI();
849
883
  }
884
+
885
+ g.loop((dt) => {
886
+ // --- Input (polling) ---
887
+ if (g.tap) {
888
+ if (state === 'gameover') {
889
+ if (inRect(g.tap, lbBtn)) leaderboard.show();
890
+ else if (inRect(g.tap, restartBtn)) startGame();
891
+ } else if (state === 'menu') {
892
+ startGame();
893
+ } else if (state === 'playing') {
894
+ // ... (player jump/action logic) ...
895
+ }
896
+ }
897
+
898
+ // --- Draw ---
899
+ ctx.fillStyle = '#111827';
900
+ ctx.fillRect(0, 0, width, height);
901
+
902
+ if (state === 'menu') {
903
+ ctx.fillStyle = '#fff';
904
+ ctx.font = 'bold 48px sans-serif';
905
+ ctx.textAlign = 'center';
906
+ ctx.fillText('FLOW', width / 2, height / 2 - 20);
907
+ ctx.font = '24px sans-serif';
908
+ ctx.fillText('TAP TO START', width / 2, height / 2 + 30);
909
+ } else if (state === 'playing') {
910
+ ctx.fillStyle = '#fff';
911
+ ctx.font = 'bold 48px sans-serif';
912
+ ctx.textAlign = 'center';
913
+ ctx.fillText(String(score), width / 2, 60);
914
+ } else if (state === 'gameover') {
915
+ ctx.fillStyle = '#fff';
916
+ ctx.font = '24px sans-serif';
917
+ ctx.textAlign = 'center';
918
+ ctx.fillText('GAME OVER', width / 2, height / 2 - 60);
919
+ ctx.font = 'bold 48px sans-serif';
920
+ ctx.fillText(String(score), width / 2, height / 2 - 10);
921
+ drawButton('VIEW LEADERBOARD', lbBtn, '#7c3aed');
922
+ drawButton('PLAY AGAIN', restartBtn, '#374151');
923
+ }
924
+ ctx.textAlign = 'left';
925
+ });
850
926
  });
851
927
  \`\`\`
852
928
 
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.18",
4
4
  "description": "CLI for Star SDK — register games, install AI docs, and deploy to Star hosting",
5
5
  "type": "module",
6
6
  "bin": {