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.
- package/dist/cli.mjs +299 -223
- 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(
|
|
58
|
-
const {
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
if (
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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 \`
|
|
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**
|
|
163
|
-
- **Don't** register multiple
|
|
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(
|
|
180
|
-
const {
|
|
192
|
+
Star.game(g => {
|
|
193
|
+
const { width, height, ctx } = g;
|
|
181
194
|
let score = 0;
|
|
182
|
-
let state = '
|
|
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
|
-
|
|
193
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
254
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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((
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
//
|
|
491
|
-
|
|
492
|
-
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
-
-
|
|
528
|
-
-
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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
|
-
|
|
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 +
|
|
847
|
+
### Recipe 5: Complex Game with Canvas + Leaderboard
|
|
770
848
|
|
|
771
|
-
**Key pattern:**
|
|
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((
|
|
857
|
+
game((g) => {
|
|
858
|
+
const { ctx, width, height } = g;
|
|
780
859
|
let score = 0;
|
|
781
860
|
let state = 'menu';
|
|
782
861
|
|
|
783
|
-
|
|
784
|
-
|
|
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
|
-
|
|
797
|
-
|
|
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
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
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
|
|