star-sdk-cli 0.1.13 → 0.1.15

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 +162 -52
  2. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -80,7 +80,39 @@ Star.game(ctx => {
80
80
 
81
81
  ## Common Patterns
82
82
 
83
- ### Game Over -> Submit Score -> Show Leaderboard
83
+ ### Game Over Screen with Leaderboard
84
+
85
+ Use DOM buttons (via \`ui.render()\` + \`on()\`) for all interactive UI \u2014 never draw buttons on canvas:
86
+
87
+ \`\`\`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());
91
+
92
+ // Gameplay tap handler (single handler, state-based)
93
+ canvas.addEventListener('pointerdown', () => {
94
+ if (state === 'playing') { jump(); }
95
+ });
96
+
97
+ function endGame() {
98
+ state = 'gameover';
99
+ 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
+ }
113
+ \`\`\`
114
+
115
+ ### Submit Score Only (No UI)
84
116
 
85
117
  \`\`\`javascript
86
118
  function gameOver(finalScore) {
@@ -127,6 +159,8 @@ canvas.onclick = (e) => {
127
159
  - **Don't** use \`setInterval\` for game loops - use \`ctx.loop()\`
128
160
  - **Don't** destructure Star - use \`Star.audio\`, \`Star.leaderboard\`, etc.
129
161
  - **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.
130
164
 
131
165
  ## Audio Presets (Full List)
132
166
 
@@ -143,9 +177,9 @@ import Star from 'star-sdk';
143
177
  Star.init({ gameId: '<gameId from .starrc>' }); // run: npx star-sdk init
144
178
 
145
179
  Star.game(ctx => {
146
- const { canvas, width, height, ctx: c } = ctx;
180
+ const { canvas, width, height, ctx: c, ui, on } = ctx;
147
181
  let score = 0;
148
- let gameOver = false;
182
+ let state = 'playing';
149
183
  let playerY = height / 2;
150
184
  let obstacles = [];
151
185
 
@@ -157,13 +191,52 @@ Star.game(ctx => {
157
191
 
158
192
  // Spawn obstacles
159
193
  setInterval(() => {
160
- if (!gameOver) {
194
+ if (state === 'playing') {
161
195
  obstacles.push({ x: width, y: Math.random() * height, passed: false });
162
196
  }
163
197
  }, 2000);
164
198
 
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
+ });
206
+
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());
210
+
211
+ function startGame() {
212
+ state = 'playing';
213
+ score = 0;
214
+ playerY = height / 2;
215
+ obstacles = [];
216
+ ui.render(''); // Clear game-over UI
217
+ }
218
+
219
+ function endGame() {
220
+ state = 'gameover';
221
+ Star.audio.play('hurt');
222
+ 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
+ }
237
+
165
238
  ctx.loop((dt) => {
166
- if (gameOver) return;
239
+ if (state !== 'playing') return;
167
240
 
168
241
  // Clear
169
242
  c.fillStyle = '#111827';
@@ -172,24 +245,15 @@ Star.game(ctx => {
172
245
  // Update obstacles
173
246
  obstacles.forEach(obs => {
174
247
  obs.x -= 200 * dt;
175
-
176
- // Score when passed
177
248
  if (!obs.passed && obs.x < 50) {
178
249
  obs.passed = true;
179
250
  score += 10;
180
251
  Star.audio.play('coin');
181
252
  }
182
-
183
- // Collision
184
253
  if (Math.abs(obs.x - 50) < 20 && Math.abs(obs.y - playerY) < 30) {
185
- gameOver = true;
186
- Star.audio.play('hurt');
187
- Star.leaderboard.submit(score);
188
- Star.leaderboard.show();
254
+ endGame();
189
255
  }
190
256
  });
191
-
192
- // Remove off-screen
193
257
  obstacles = obstacles.filter(o => o.x > -20);
194
258
 
195
259
  // Draw player
@@ -209,14 +273,6 @@ Star.game(ctx => {
209
273
  c.font = '24px sans-serif';
210
274
  c.fillText(\`Score: \${score}\`, 20, 40);
211
275
  });
212
-
213
- // Jump on click/tap
214
- canvas.onclick = () => {
215
- if (!gameOver) {
216
- playerY -= 50;
217
- Star.audio.play('jump');
218
- }
219
- };
220
276
  });
221
277
  \`\`\`
222
278
 
@@ -423,26 +479,24 @@ game(({ ctx, width, height, on, loop, ui, canvas }) => {
423
479
  });
424
480
 
425
481
  // 2. Render HTML to the safe UI overlay
426
- // UI is interactive by default (scroll, buttons work)
427
- // Adding canvas.addEventListener makes UI click-through automatically
428
482
  ui.render(\`
429
483
  <div class="absolute top-4 left-4 text-white">
430
- <button id="start-btn" class="px-4 py-2 bg-blue-500 rounded pointer-events-auto">
484
+ <button id="start-btn" class="px-4 py-2 bg-blue-500 rounded">
431
485
  Click Me
432
486
  </button>
433
487
  </div>
434
488
  \`);
435
489
 
436
- // 3. Listen for button clicks
490
+ // 3. Listen for button clicks \u2014 on() auto-enables pointer-events for the target
437
491
  on('click', '#start-btn', () => {
438
492
  console.log('Button clicked!');
439
493
  });
440
494
 
441
- // 4. For canvas games: listen for taps on canvas
442
- // This automatically makes UI click-through (taps pass through to canvas)
443
- // Buttons with pointer-events-auto still work
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
444
498
  canvas.addEventListener('pointerdown', (e) => {
445
- console.log('Canvas/screen tapped!', e);
499
+ console.log('Gameplay tap!', e);
446
500
  });
447
501
  });
448
502
  \`\`\`
@@ -470,7 +524,9 @@ The 2D drawing context. Its transform is already scaled for DPR. You **always dr
470
524
 
471
525
  The \`<canvas>\` element itself.
472
526
 
473
- - **Use this for gameplay input listeners** (e.g., \`pointerdown\`, \`pointermove\`).
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.
474
530
 
475
531
  ### \`width: number\` (getter)
476
532
 
@@ -505,7 +561,7 @@ A safe manager for your HTML overlay, stacked on top of the canvas.
505
561
  - \`ui.el(selector)\`: Scoped \`querySelector\` for the UI root.
506
562
  - \`ui.all(selector)\`: Scoped \`querySelectorAll\` for the UI root.
507
563
 
508
- **Auto-detection:** When you add \`canvas.addEventListener('pointerdown', ...)\`, the SDK automatically makes UI click-through so taps reach the canvas. Buttons with \`pointer-events-auto\` still work.
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.
509
565
 
510
566
  ### Cursor Management
511
567
 
@@ -712,6 +768,8 @@ game(({ ctx, width, height, loop, toStagePoint, canvas }) => {
712
768
 
713
769
  ### Recipe 5: Complex Game with Canvas + UI + Events (like FLOW)
714
770
 
771
+ **Key pattern:** Canvas for gameplay input, DOM buttons for UI. Never draw buttons on canvas.
772
+
715
773
  \`\`\`ts
716
774
  import { game } from 'star-canvas';
717
775
  import { createLeaderboard } from 'star-leaderboard';
@@ -722,24 +780,20 @@ game(({ ctx, width, height, loop, ui, on, canvas, toStagePoint }) => {
722
780
  let score = 0;
723
781
  let state = 'menu';
724
782
 
725
- function handleTap() {
726
- if (state === 'menu' || state === 'gameover') {
727
- startGame();
728
- } else if (state === 'playing') {
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') {
729
787
  // ... (player float logic) ...
730
788
  }
731
- }
732
-
733
- // 1. Listen for screen taps - this makes UI click-through automatically
734
- canvas.addEventListener('pointerdown', handleTap);
735
-
736
- // 2. Listen for button clicks - buttons need pointer-events-auto
737
- on('click', '#leaderboard-btn', (e) => {
738
- e.stopPropagation();
739
- leaderboard.show();
740
789
  });
741
790
 
742
- // 3. Render UI - buttons need pointer-events-auto to intercept clicks
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
743
797
  let lastState = null;
744
798
  let lastScore = -1;
745
799
 
@@ -766,10 +820,12 @@ game(({ ctx, width, height, loop, ui, on, canvas, toStagePoint }) => {
766
820
  <div class="h-full flex flex-col items-center justify-center text-white">
767
821
  <div class="text-3xl mb-4">GAME OVER</div>
768
822
  <div class="text-6xl mb-4">\\\${score}</div>
769
- <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 pointer-events-auto">
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">
770
824
  VIEW LEADERBOARD
771
825
  </button>
772
- <div class="text-xl animate-pulse">TAP TO RESTART</div>
826
+ <button id="restart-btn" class="px-6 py-3 bg-gray-700 rounded-xl font-bold">
827
+ PLAY AGAIN
828
+ </button>
773
829
  </div>\`);
774
830
  }
775
831
  }
@@ -781,6 +837,7 @@ game(({ ctx, width, height, loop, ui, on, canvas, toStagePoint }) => {
781
837
  function startGame() {
782
838
  state = 'playing';
783
839
  score = 0;
840
+ ui.render(''); // Clear game-over buttons
784
841
  updateUI();
785
842
  }
786
843
 
@@ -1180,7 +1237,7 @@ const leaderboard = createLeaderboard({ gameId: '<gameId from .starrc>' });
1180
1237
 
1181
1238
  game(({ ui, on }) => {
1182
1239
  ui.render(\`
1183
- <button id="lb-btn" class="px-6 py-3 bg-gradient-to-r from-blue-600 to-purple-600 rounded-xl font-bold text-white shadow-lg shadow-blue-500/20 pointer-events-auto">
1240
+ <button id="lb-btn" class="px-6 py-3 bg-gradient-to-r from-blue-600 to-purple-600 rounded-xl font-bold text-white shadow-lg shadow-blue-500/20">
1184
1241
  View Leaderboard
1185
1242
  </button>
1186
1243
  \`);
@@ -1247,7 +1304,7 @@ game(({ ctx, width, height, loop, ui, on, canvas }) => {
1247
1304
  <div class="h-full flex flex-col items-center justify-center text-white">
1248
1305
  <div class="text-3xl mb-4">GAME OVER</div>
1249
1306
  <div class="text-6xl mb-4">\\\${score}</div>
1250
- <button id="lb-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 pointer-events-auto">
1307
+ <button id="lb-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">
1251
1308
  VIEW LEADERBOARD
1252
1309
  </button>
1253
1310
  <div class="text-xl animate-pulse">TAP TO RESTART</div>
@@ -1358,6 +1415,52 @@ function error(message) {
1358
1415
  function info(message) {
1359
1416
  console.log(`${colors.blue}\u2139${colors.reset} ${message}`);
1360
1417
  }
1418
+ var STAR_PACKAGES = ["star-sdk", "star-canvas", "star-audio", "star-leaderboard", "star-multiplayer"];
1419
+ function resolveStarSdkVersion(deployDir) {
1420
+ const candidates = [
1421
+ path.join(deployDir, "node_modules", "star-sdk", "package.json"),
1422
+ path.join(process.cwd(), "node_modules", "star-sdk", "package.json")
1423
+ ];
1424
+ for (const p of candidates) {
1425
+ try {
1426
+ if (fs.existsSync(p)) {
1427
+ const pkg = JSON.parse(fs.readFileSync(p, "utf-8"));
1428
+ if (pkg.version) return pkg.version;
1429
+ }
1430
+ } catch {
1431
+ }
1432
+ }
1433
+ return null;
1434
+ }
1435
+ function injectImportMapIfNeeded(html, deployDir) {
1436
+ if (/<script\s[^>]*type\s*=\s*["']importmap["'][^>]*>/i.test(html)) {
1437
+ return html;
1438
+ }
1439
+ const bareImportPattern = /\bfrom\s+['"](?:star-sdk|star-canvas|star-audio|star-leaderboard|star-multiplayer)['"]/;
1440
+ if (!bareImportPattern.test(html)) {
1441
+ return html;
1442
+ }
1443
+ const version = resolveStarSdkVersion(deployDir);
1444
+ const suffix = version ? `@${version}` : "";
1445
+ const imports = {};
1446
+ for (const pkg of STAR_PACKAGES) {
1447
+ imports[pkg] = `https://esm.sh/${pkg}${suffix}`;
1448
+ }
1449
+ const importmapTag = `<script type="importmap">
1450
+ ${JSON.stringify({ imports }, null, 2)}
1451
+ </script>
1452
+ `;
1453
+ const moduleScriptMatch = html.match(/<script\s[^>]*type\s*=\s*["']module["'][^>]*>/i);
1454
+ if (moduleScriptMatch && moduleScriptMatch.index !== void 0) {
1455
+ return html.slice(0, moduleScriptMatch.index) + importmapTag + html.slice(moduleScriptMatch.index);
1456
+ }
1457
+ const headMatch = html.match(/<head[^>]*>/i);
1458
+ if (headMatch && headMatch.index !== void 0) {
1459
+ const insertPos = headMatch.index + headMatch[0].length;
1460
+ return html.slice(0, insertPos) + "\n" + importmapTag + html.slice(insertPos);
1461
+ }
1462
+ return html;
1463
+ }
1361
1464
  function showHelp() {
1362
1465
  log(`
1363
1466
  ${colors.bright}Star SDK CLI${colors.reset} v${VERSION}
@@ -1555,6 +1658,12 @@ async function deployCommand(dirPath) {
1555
1658
  }
1556
1659
  log(`Deploying ${colors.bright}${config.name}${colors.reset} from ${colors.dim}${deployDir}${colors.reset}`);
1557
1660
  try {
1661
+ const originalHtml = fs.readFileSync(indexPath, "utf-8");
1662
+ const processedHtml = injectImportMapIfNeeded(originalHtml, deployDir);
1663
+ const htmlModified = processedHtml !== originalHtml;
1664
+ if (htmlModified) {
1665
+ info("Injected importmap for bare imports (star-sdk \u2192 esm.sh)");
1666
+ }
1558
1667
  const archiver = (await import("archiver")).default;
1559
1668
  const zipBuffer = await new Promise((resolve2, reject) => {
1560
1669
  const chunks = [];
@@ -1564,9 +1673,10 @@ async function deployCommand(dirPath) {
1564
1673
  archive.on("error", reject);
1565
1674
  archive.glob("**/*", {
1566
1675
  cwd: deployDir,
1567
- ignore: ["node_modules/**", ".starrc", ".git/**", ".DS_Store"],
1676
+ ignore: ["node_modules/**", ".starrc", ".git/**", ".DS_Store", "index.html"],
1568
1677
  dot: false
1569
1678
  });
1679
+ archive.append(processedHtml, { name: "index.html" });
1570
1680
  archive.finalize();
1571
1681
  });
1572
1682
  log(` ${colors.dim}Uploading ${(zipBuffer.length / 1024).toFixed(1)} KB...${colors.reset}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "star-sdk-cli",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "CLI for Star SDK — register games, install AI docs, and deploy to Star hosting",
5
5
  "type": "module",
6
6
  "bin": {