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.
- package/dist/cli.mjs +162 -52
- 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
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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.
|
|
442
|
-
//
|
|
443
|
-
//
|
|
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('
|
|
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
|
|
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.
|
|
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
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
<
|
|
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
|
|
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
|
|
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}`);
|