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.
- package/dist/cli.mjs +320 -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
|
-
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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 \`
|
|
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**
|
|
163
|
-
- **Don't** register multiple
|
|
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(
|
|
180
|
-
const {
|
|
204
|
+
Star.game(g => {
|
|
205
|
+
const { width, height, ctx } = g;
|
|
181
206
|
let score = 0;
|
|
182
|
-
let state = '
|
|
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
|
-
|
|
193
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
254
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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((
|
|
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
|
-
\`);
|
|
509
|
+
game((g) => {
|
|
510
|
+
const { ctx, width, height } = g;
|
|
511
|
+
let score = 0;
|
|
489
512
|
|
|
490
|
-
//
|
|
491
|
-
|
|
492
|
-
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
-
-
|
|
528
|
-
-
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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
|
-
|
|
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 +
|
|
868
|
+
### Recipe 5: Complex Game with Canvas + Leaderboard
|
|
770
869
|
|
|
771
|
-
**Key pattern:**
|
|
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((
|
|
878
|
+
game((g) => {
|
|
879
|
+
const { ctx, width, height } = g;
|
|
780
880
|
let score = 0;
|
|
781
881
|
let state = 'menu';
|
|
782
882
|
|
|
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());
|
|
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
|
|
801
|
-
|
|
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
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
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
|
|