star-sdk-cli 0.1.1 → 0.1.2
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 +1690 -4
- package/package.json +2 -1
package/dist/cli.mjs
CHANGED
|
@@ -3,6 +3,1628 @@
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import * as fs from "fs";
|
|
5
5
|
import * as path from "path";
|
|
6
|
+
import * as os from "os";
|
|
7
|
+
|
|
8
|
+
// src/skills.ts
|
|
9
|
+
var SKILL_CONTENT = `---
|
|
10
|
+
name: star-sdk
|
|
11
|
+
description: Star SDK for browser game development. Use when building games, adding audio/sound effects, canvas rendering, leaderboards, or multiplayer. Covers Star.game(), Star.audio, Star.leaderboard, and Star.multiplayer APIs.
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Star SDK
|
|
15
|
+
|
|
16
|
+
Browser game development SDK with audio, canvas, leaderboards, and multiplayer.
|
|
17
|
+
|
|
18
|
+
## Import
|
|
19
|
+
|
|
20
|
+
\`\`\`javascript
|
|
21
|
+
import Star from 'star-sdk';
|
|
22
|
+
\`\`\`
|
|
23
|
+
|
|
24
|
+
**CRITICAL:** Always use \`import Star from 'star-sdk'\` - not destructured imports.
|
|
25
|
+
|
|
26
|
+
## API Overview
|
|
27
|
+
|
|
28
|
+
| API | Use When | Docs |
|
|
29
|
+
|-----|----------|------|
|
|
30
|
+
| \`Star.game()\` | Game loop, canvas, UI, input | [canvas.md](./canvas.md) |
|
|
31
|
+
| \`Star.audio\` | Sound effects, music | [audio.md](./audio.md) |
|
|
32
|
+
| \`Star.leaderboard\` | Scores, rankings | [leaderboard.md](./leaderboard.md) |
|
|
33
|
+
| \`Star.multiplayer\` | Real-time multiplayer | [multiplayer.md](./multiplayer.md) |
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
\`\`\`javascript
|
|
38
|
+
import Star from 'star-sdk';
|
|
39
|
+
|
|
40
|
+
Star.game(ctx => {
|
|
41
|
+
const { canvas, width, height, ctx: c } = ctx;
|
|
42
|
+
let score = 0;
|
|
43
|
+
|
|
44
|
+
// Preload audio
|
|
45
|
+
Star.audio.preload({ coin: 'coin', jump: 'jump' });
|
|
46
|
+
|
|
47
|
+
// Game loop
|
|
48
|
+
ctx.loop((dt) => {
|
|
49
|
+
c.fillStyle = '#1a1a2e';
|
|
50
|
+
c.fillRect(0, 0, width, height);
|
|
51
|
+
c.fillStyle = '#fff';
|
|
52
|
+
c.font = '24px sans-serif';
|
|
53
|
+
c.fillText(\`Score: \${score}\`, 20, 40);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Input
|
|
57
|
+
canvas.onclick = () => {
|
|
58
|
+
score += 10;
|
|
59
|
+
Star.audio.play('coin');
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
\`\`\`
|
|
63
|
+
|
|
64
|
+
## Common Patterns
|
|
65
|
+
|
|
66
|
+
### Game Over -> Submit Score -> Show Leaderboard
|
|
67
|
+
|
|
68
|
+
\`\`\`javascript
|
|
69
|
+
function gameOver(finalScore) {
|
|
70
|
+
Star.leaderboard.submit(finalScore);
|
|
71
|
+
Star.leaderboard.show();
|
|
72
|
+
}
|
|
73
|
+
\`\`\`
|
|
74
|
+
|
|
75
|
+
### Mobile Audio (IMPORTANT)
|
|
76
|
+
|
|
77
|
+
Audio on mobile requires user interaction first. Star.audio handles unlocking automatically - just make sure the first \`play()\` call happens after a user interaction (click/tap).
|
|
78
|
+
|
|
79
|
+
### Coordinate Handling
|
|
80
|
+
|
|
81
|
+
\`\`\`javascript
|
|
82
|
+
canvas.onclick = (e) => {
|
|
83
|
+
const point = ctx.toStagePoint(e); // Correct coordinates
|
|
84
|
+
console.log(point.x, point.y);
|
|
85
|
+
};
|
|
86
|
+
\`\`\`
|
|
87
|
+
|
|
88
|
+
## Don't Do This
|
|
89
|
+
|
|
90
|
+
- **Don't** create canvas manually - use \`Star.game()\`
|
|
91
|
+
- **Don't** use \`setInterval\` for game loops - use \`ctx.loop()\`
|
|
92
|
+
- **Don't** destructure Star - use \`Star.audio\`, \`Star.leaderboard\`, etc.
|
|
93
|
+
- **Don't** invent audio preset names - only 17 exist (see audio.md)
|
|
94
|
+
|
|
95
|
+
## Audio Presets (Full List)
|
|
96
|
+
|
|
97
|
+
Only these 17 presets exist:
|
|
98
|
+
- UI: \`beep\`, \`click\`, \`select\`, \`error\`, \`success\`
|
|
99
|
+
- Actions: \`jump\`, \`swoosh\`, \`shoot\`, \`laser\`, \`explosion\`
|
|
100
|
+
- Combat: \`hit\`, \`hurt\`
|
|
101
|
+
- Collection: \`coin\`, \`pickup\`, \`bonus\`, \`unlock\`, \`powerup\`
|
|
102
|
+
|
|
103
|
+
## Full Game Example
|
|
104
|
+
|
|
105
|
+
\`\`\`javascript
|
|
106
|
+
import Star from 'star-sdk';
|
|
107
|
+
|
|
108
|
+
Star.game(ctx => {
|
|
109
|
+
const { canvas, width, height, ctx: c } = ctx;
|
|
110
|
+
let score = 0;
|
|
111
|
+
let gameOver = false;
|
|
112
|
+
let playerY = height / 2;
|
|
113
|
+
let obstacles = [];
|
|
114
|
+
|
|
115
|
+
Star.audio.preload({
|
|
116
|
+
jump: 'jump',
|
|
117
|
+
coin: 'coin',
|
|
118
|
+
hurt: 'hurt'
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Spawn obstacles
|
|
122
|
+
setInterval(() => {
|
|
123
|
+
if (!gameOver) {
|
|
124
|
+
obstacles.push({ x: width, y: Math.random() * height, passed: false });
|
|
125
|
+
}
|
|
126
|
+
}, 2000);
|
|
127
|
+
|
|
128
|
+
ctx.loop((dt) => {
|
|
129
|
+
if (gameOver) return;
|
|
130
|
+
|
|
131
|
+
// Clear
|
|
132
|
+
c.fillStyle = '#1a1a2e';
|
|
133
|
+
c.fillRect(0, 0, width, height);
|
|
134
|
+
|
|
135
|
+
// Update obstacles
|
|
136
|
+
obstacles.forEach(obs => {
|
|
137
|
+
obs.x -= 200 * dt;
|
|
138
|
+
|
|
139
|
+
// Score when passed
|
|
140
|
+
if (!obs.passed && obs.x < 50) {
|
|
141
|
+
obs.passed = true;
|
|
142
|
+
score += 10;
|
|
143
|
+
Star.audio.play('coin');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Collision
|
|
147
|
+
if (Math.abs(obs.x - 50) < 20 && Math.abs(obs.y - playerY) < 30) {
|
|
148
|
+
gameOver = true;
|
|
149
|
+
Star.audio.play('hurt');
|
|
150
|
+
Star.leaderboard.submit(score);
|
|
151
|
+
Star.leaderboard.show();
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Remove off-screen
|
|
156
|
+
obstacles = obstacles.filter(o => o.x > -20);
|
|
157
|
+
|
|
158
|
+
// Draw player
|
|
159
|
+
c.fillStyle = '#e94560';
|
|
160
|
+
c.beginPath();
|
|
161
|
+
c.arc(50, playerY, 15, 0, Math.PI * 2);
|
|
162
|
+
c.fill();
|
|
163
|
+
|
|
164
|
+
// Draw obstacles
|
|
165
|
+
c.fillStyle = '#0f4c75';
|
|
166
|
+
obstacles.forEach(obs => {
|
|
167
|
+
c.fillRect(obs.x - 10, obs.y - 25, 20, 50);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Draw score
|
|
171
|
+
c.fillStyle = '#fff';
|
|
172
|
+
c.font = '24px sans-serif';
|
|
173
|
+
c.fillText(\`Score: \${score}\`, 20, 40);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Jump on click/tap
|
|
177
|
+
canvas.onclick = () => {
|
|
178
|
+
if (!gameOver) {
|
|
179
|
+
playerY -= 50;
|
|
180
|
+
Star.audio.play('jump');
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
\`\`\`
|
|
185
|
+
|
|
186
|
+
For detailed API documentation, see the linked files above.
|
|
187
|
+
`;
|
|
188
|
+
var AUDIO_DOCS = `**Installation**
|
|
189
|
+
|
|
190
|
+
First, add the package to your project:
|
|
191
|
+
\` \` \`bash
|
|
192
|
+
yarn add star-audio
|
|
193
|
+
\` \` \`
|
|
194
|
+
|
|
195
|
+
### Star Audio SDK
|
|
196
|
+
|
|
197
|
+
**Mobile-first, bulletproof audio for web games.** Works on iOS/Android out of the box. Missing files won't crash your game.
|
|
198
|
+
|
|
199
|
+
**Import:**
|
|
200
|
+
\` \` \`javascript
|
|
201
|
+
import createAudio from 'star-audio';
|
|
202
|
+
const audio = createAudio();
|
|
203
|
+
\` \` \`
|
|
204
|
+
|
|
205
|
+
**CRITICAL:** Import in JavaScript - don't add \`<script src="/star-sdk/audio.js">\` tags.
|
|
206
|
+
|
|
207
|
+
**Why Star Audio?**
|
|
208
|
+
- \u2705 **Mobile-first** - Works on iOS/Android, handles audio unlock automatically, plays even in silent mode
|
|
209
|
+
- \u2705 **Never throws** - Missing audio files? Game keeps running with clear warnings
|
|
210
|
+
- \u2705 **Zero-config** - Works on first play, no setup needed
|
|
211
|
+
- \u2705 **No try/catch needed** - Fire-and-forget API, perfect for AI-generated games
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
**Quick Start:**
|
|
216
|
+
|
|
217
|
+
\` \` \`javascript
|
|
218
|
+
const audio = createAudio();
|
|
219
|
+
|
|
220
|
+
// CRITICAL: Preload presets before playing them
|
|
221
|
+
// You can set per-sound volumes in preload
|
|
222
|
+
audio.preload({
|
|
223
|
+
jump: { synth: 'jump', volume: 0.5 }, // Quieter jump
|
|
224
|
+
shoot: { synth: 'shoot', volume: 0.8 }, // Loud shoot
|
|
225
|
+
coin: { synth: 'coin', volume: 0.6 },
|
|
226
|
+
explosion: { synth: 'explosion', volume: 1.0 }
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Now play them - volumes are already set
|
|
230
|
+
audio.play('jump');
|
|
231
|
+
audio.play('shoot');
|
|
232
|
+
\` \` \`
|
|
233
|
+
|
|
234
|
+
**ONLY THESE 17 PRESETS EXIST - DO NOT INVENT NAMES:**
|
|
235
|
+
- UI: \`beep\`, \`click\`, \`select\`, \`error\`, \`success\`
|
|
236
|
+
- Actions: \`jump\`, \`swoosh\`, \`shoot\`, \`laser\`, \`explosion\`
|
|
237
|
+
- Combat: \`hit\`, \`hurt\`
|
|
238
|
+
- Collection: \`coin\`, \`pickup\`, \`bonus\`, \`unlock\`, \`powerup\`
|
|
239
|
+
|
|
240
|
+
**If you need a sound that's not in this list, use custom synth or generate audio.**
|
|
241
|
+
|
|
242
|
+
**CRITICAL:** Preload all presets before use. Set volumes in preload:
|
|
243
|
+
\` \` \`javascript
|
|
244
|
+
audio.preload({
|
|
245
|
+
jump: { synth: 'jump', volume: 0.5 }, // With custom volume
|
|
246
|
+
coin: 'coin', // Default volume
|
|
247
|
+
explosion: 'explosion' // For crashes/impacts
|
|
248
|
+
});
|
|
249
|
+
\` \` \`
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
**Custom synth (advanced):**
|
|
254
|
+
|
|
255
|
+
\` \` \`javascript
|
|
256
|
+
audio.preload({
|
|
257
|
+
'sfx.charge': {
|
|
258
|
+
waveform: 'triangle',
|
|
259
|
+
frequency: [200, 300, 450, 650, 900], // Rising charge-up
|
|
260
|
+
duration: 0.40,
|
|
261
|
+
volume: 0.38
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
\` \` \`
|
|
265
|
+
|
|
266
|
+
**Make sounds feel good:**
|
|
267
|
+
- **Use frequency arrays** - Sweeps/arpeggios are more satisfying than single tones
|
|
268
|
+
- **Rising = positive** - Ascending pitches for rewards (coin, jump, powerup)
|
|
269
|
+
- **Descending = impact** - Falling pitches for actions (shoot, hurt, explosion)
|
|
270
|
+
- **More notes = richer** - 3-6 frequencies sound fuller than 1-2
|
|
271
|
+
- **Musical intervals** - Use harmonious ratios (octaves, fifths, major chords)
|
|
272
|
+
|
|
273
|
+
**Waveform choice:**
|
|
274
|
+
- \`sine\` - Pure, pleasant (UI, bells, rewards)
|
|
275
|
+
- \`triangle\` - Warm, full (jumps, explosions, success)
|
|
276
|
+
- \`square\` - Retro, characterful (powerups, beeps, chiptune)
|
|
277
|
+
- \`sawtooth\` - Harsh, aggressive (lasers, damage, errors)
|
|
278
|
+
|
|
279
|
+
**Frequency guide:**
|
|
280
|
+
- High (800-2000 Hz): Bright, attention-grabbing (UI, coins)
|
|
281
|
+
- Mid (200-800 Hz): Game actions (jumps, shoots)
|
|
282
|
+
- Low (30-200 Hz): Impacts, bass (explosions, rumbles)
|
|
283
|
+
- Arrays: 3-4 notes for melodies, 6+ for noise-like effects
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
**Audio files:**
|
|
288
|
+
|
|
289
|
+
\` \` \`javascript
|
|
290
|
+
audio.preload({
|
|
291
|
+
'sfx.boom': 'assets/boom.mp3',
|
|
292
|
+
'bgm.theme': 'assets/music.mp3'
|
|
293
|
+
});
|
|
294
|
+
audio.play('sfx.boom');
|
|
295
|
+
audio.music.crossfadeTo('bgm.theme', { duration: 1.5 });
|
|
296
|
+
\` \` \`
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
**Controls:**
|
|
301
|
+
|
|
302
|
+
\` \` \`javascript
|
|
303
|
+
audio.setMusicVolume(0.5);
|
|
304
|
+
audio.setSfxVolume(0.8);
|
|
305
|
+
audio.toggleMute();
|
|
306
|
+
\` \` \``;
|
|
307
|
+
var CANVAS_DOCS = `**Installation**
|
|
308
|
+
|
|
309
|
+
First, add the package to your project:
|
|
310
|
+
|
|
311
|
+
\`\`\`bash
|
|
312
|
+
yarn add star-dom
|
|
313
|
+
\`\`\`
|
|
314
|
+
|
|
315
|
+
### Star DOM SDK
|
|
316
|
+
|
|
317
|
+
Use the **Star DOM SDK** to initialize games reliably.
|
|
318
|
+
It prevents the most common bugs:
|
|
319
|
+
|
|
320
|
+
- \u2705 No "cannot read addEventListener of null"
|
|
321
|
+
- \u2705 No canvas sizing/DPR/blur issues
|
|
322
|
+
- \u2705 No accidentally wiping the canvas with \`innerHTML\`
|
|
323
|
+
- \u2705 Games work identically on ALL devices (fixed 16:9 with letterboxing)
|
|
324
|
+
|
|
325
|
+
-----
|
|
326
|
+
|
|
327
|
+
### Fixed 16:9 Resolution
|
|
328
|
+
|
|
329
|
+
**Default: 640\xD7360 (landscape) or 360\xD7640 (portrait).** Games work identically on every device.
|
|
330
|
+
|
|
331
|
+
The SDK uses letterboxing to maintain the exact game area. This means:
|
|
332
|
+
- Positions like \`x: 320, y: 180\` always mean the exact center
|
|
333
|
+
- Two objects at \`x: 100\` and \`x: 540\` are always the same distance apart
|
|
334
|
+
- No "works on my screen, breaks on mobile" bugs
|
|
335
|
+
|
|
336
|
+
\`\`\`ts
|
|
337
|
+
// These values work identically on ALL devices:
|
|
338
|
+
const player = { x: 320, y: 300 }; // Center-bottom area
|
|
339
|
+
const enemy = { x: 600, y: 50 }; // Top-right area
|
|
340
|
+
const playerSize = 32; // Always 32px
|
|
341
|
+
const speed = 200; // Always 200px/sec
|
|
342
|
+
\`\`\`
|
|
343
|
+
|
|
344
|
+
-----
|
|
345
|
+
|
|
346
|
+
### Golden Path (How to Use)
|
|
347
|
+
|
|
348
|
+
Import \`game\` and wrap your code in it. The \`game\` function handles DOM readiness, creates a canvas and a UI overlay, and gives you a safe context to build.
|
|
349
|
+
|
|
350
|
+
\`\`\`ts
|
|
351
|
+
import { game } from 'star-canvas';
|
|
352
|
+
|
|
353
|
+
game(({ ctx, width, height, on, loop, ui, canvas }) => {
|
|
354
|
+
// ctx: The 2D canvas context
|
|
355
|
+
// width, height: The logical size (CSS pixels) - READ-ONLY
|
|
356
|
+
// on: Safe, delegated event listener
|
|
357
|
+
// loop: Stable game loop (with dt)
|
|
358
|
+
// ui: Safe overlay for HTML
|
|
359
|
+
// canvas: The <canvas> element
|
|
360
|
+
|
|
361
|
+
// 1. Draw on the canvas
|
|
362
|
+
loop((dt) => {
|
|
363
|
+
ctx.clearRect(0, 0, width, height);
|
|
364
|
+
ctx.fillStyle = '#22d3ee'; // cyan-400
|
|
365
|
+
ctx.fillRect(width / 2 - 25, height / 2 - 25, 50, 50);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// 2. Render HTML to the safe UI overlay
|
|
369
|
+
// UI is interactive by default (scroll, buttons work)
|
|
370
|
+
// Adding canvas.addEventListener makes UI click-through automatically
|
|
371
|
+
ui.render(\`
|
|
372
|
+
<div class="absolute top-4 left-4 text-white">
|
|
373
|
+
<button id="start-btn" class="px-4 py-2 bg-blue-500 rounded pointer-events-auto">
|
|
374
|
+
Click Me
|
|
375
|
+
</button>
|
|
376
|
+
</div>
|
|
377
|
+
\`);
|
|
378
|
+
|
|
379
|
+
// 3. Listen for button clicks
|
|
380
|
+
on('click', '#start-btn', () => {
|
|
381
|
+
console.log('Button clicked!');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// 4. For canvas games: listen for taps on canvas
|
|
385
|
+
// This automatically makes UI click-through (taps pass through to canvas)
|
|
386
|
+
// Buttons with pointer-events-auto still work
|
|
387
|
+
canvas.addEventListener('pointerdown', (e) => {
|
|
388
|
+
console.log('Canvas/screen tapped!', e);
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
\`\`\`
|
|
392
|
+
|
|
393
|
+
> **CRITICAL:** Always import the SDK in your JavaScript/TypeScript.
|
|
394
|
+
> **Do not** add a \`<script src="/star-sdk/dom.js">\` tag in HTML.
|
|
395
|
+
>
|
|
396
|
+
> **Recommended Import:**
|
|
397
|
+
>
|
|
398
|
+
> \`\`\`ts
|
|
399
|
+
> import { game } from 'star-canvas';
|
|
400
|
+
> \`\`\`
|
|
401
|
+
|
|
402
|
+
-----
|
|
403
|
+
|
|
404
|
+
## Core API: \`game(setup, options?)\`
|
|
405
|
+
|
|
406
|
+
The \`setup\` function receives one argument: a \`GameContext\` object with the following properties:
|
|
407
|
+
|
|
408
|
+
### \`ctx: CanvasRenderingContext2D\`
|
|
409
|
+
|
|
410
|
+
The 2D drawing context. Its transform is already scaled for DPR. You **always draw in logical CSS pixels**.
|
|
411
|
+
|
|
412
|
+
### \`canvas: HTMLCanvasElement\`
|
|
413
|
+
|
|
414
|
+
The \`<canvas>\` element itself.
|
|
415
|
+
|
|
416
|
+
- **Use this for gameplay input listeners** (e.g., \`pointerdown\`, \`pointermove\`).
|
|
417
|
+
|
|
418
|
+
### \`width: number\` (getter)
|
|
419
|
+
|
|
420
|
+
### \`height: number\` (getter)
|
|
421
|
+
|
|
422
|
+
The logical CSS pixel width and height of the stage. **Use these for all game logic and drawing.** They are getters, so they are always up-to-date.
|
|
423
|
+
|
|
424
|
+
### \`on(type, selector, handler, options?)\`
|
|
425
|
+
|
|
426
|
+
Attaches a **delegated event listener** to the document.
|
|
427
|
+
|
|
428
|
+
- \u2705 **Use this for UI elements** (buttons, menus) inside your \`ui.render()\` HTML.
|
|
429
|
+
- \u2705 Survives \`ui.render()\` calls.
|
|
430
|
+
- Returns an \`off()\` function to unsubscribe.
|
|
431
|
+
|
|
432
|
+
### \`loop(tick)\`
|
|
433
|
+
|
|
434
|
+
Starts a \`requestAnimationFrame\` loop.
|
|
435
|
+
|
|
436
|
+
- \`tick\` function receives \`(dt, now)\`, where \`dt\` is **delta time in seconds**.
|
|
437
|
+
- **ALWAYS** multiply movement by \`dt\` (e.g., \`player.x += speed * dt\`).
|
|
438
|
+
- Returns \`{ start(), stop(), running }\`. The loop starts automatically.
|
|
439
|
+
|
|
440
|
+
### \`ui: GameUI\`
|
|
441
|
+
|
|
442
|
+
A safe manager for your HTML overlay, stacked on top of the canvas.
|
|
443
|
+
|
|
444
|
+
- \`ui.root\`: The \`<div>\` element for your UI. It is **interactive by default** (standard HTML behavior - scroll, buttons work).
|
|
445
|
+
- \`ui.render(html: string)\`: **Use this** to set your UI. It's safe and won't destroy the canvas.
|
|
446
|
+
- Automatically skips updates if HTML is unchanged (safe to call in loop for static content)
|
|
447
|
+
- For best performance with dynamic content (score), only call when values actually change
|
|
448
|
+
- \`ui.el(selector)\`: Scoped \`querySelector\` for the UI root.
|
|
449
|
+
- \`ui.all(selector)\`: Scoped \`querySelectorAll\` for the UI root.
|
|
450
|
+
|
|
451
|
+
**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.
|
|
452
|
+
|
|
453
|
+
### Cursor Management
|
|
454
|
+
|
|
455
|
+
**CRITICAL:** Choose cursor based on how players interact. Update cursor when state changes (e.g., menu \u2192 playing \u2192 gameover).
|
|
456
|
+
|
|
457
|
+
\`\`\`ts
|
|
458
|
+
// MOUSE-BASED GAMES (click/point-and-click/puzzle/clicker/strategy/constellation)
|
|
459
|
+
if (state === 'playing') canvas.style.cursor = 'pointer'; // Show where to click
|
|
460
|
+
if (state === 'menu' || state === 'gameover') canvas.style.cursor = 'pointer'; // Keep visible
|
|
461
|
+
|
|
462
|
+
// PRECISION AIMING (shooter/drawing/building)
|
|
463
|
+
if (state === 'playing') canvas.style.cursor = 'crosshair';
|
|
464
|
+
if (state === 'menu' || state === 'gameover') canvas.style.cursor = 'auto';
|
|
465
|
+
|
|
466
|
+
// KEYBOARD/TOUCH ONLY (platformer/WASD/rhythm/endless runner)
|
|
467
|
+
if (state === 'playing') canvas.style.cursor = 'none'; // Hide (doesn't matter)
|
|
468
|
+
if (state === 'menu' || state === 'gameover') canvas.style.cursor = 'auto'; // Show for menus!
|
|
469
|
+
\`\`\`
|
|
470
|
+
|
|
471
|
+
**Decision:** Does player click on game objects? \u2192 \`'pointer'\` | Aim precisely? \u2192 \`'crosshair'\` | WASD/touch only? \u2192 \`'none'\` during play, \`'auto'\` for menus
|
|
472
|
+
|
|
473
|
+
### \`toStagePoint(event)\`
|
|
474
|
+
|
|
475
|
+
Converts \`MouseEvent\` or \`PointerEvent\` client coordinates to the stage's logical coordinates.
|
|
476
|
+
|
|
477
|
+
- **USE THIS** for all canvas pointer input.
|
|
478
|
+
|
|
479
|
+
### \`createDrag()\`
|
|
480
|
+
|
|
481
|
+
Creates a drag state helper that handles coordinate conversion and offset tracking automatically.
|
|
482
|
+
|
|
483
|
+
\`\`\`ts
|
|
484
|
+
const drag = createDrag();
|
|
485
|
+
|
|
486
|
+
canvas.addEventListener('pointerdown', (e) => {
|
|
487
|
+
canvas.setPointerCapture(e.pointerId); // IMPORTANT: Capture for reliable drags
|
|
488
|
+
const { x, y } = drag.point(e); // Convert coordinates
|
|
489
|
+
const hit = pieces.find(p => /* hit test */);
|
|
490
|
+
if (hit) drag.grab(e, hit); // Start drag with offset
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
canvas.addEventListener('pointermove', (e) => drag.move(e)); // Updates position
|
|
494
|
+
canvas.addEventListener('pointerup', () => {
|
|
495
|
+
const dropped = drag.release(); // Returns dropped object (or null)
|
|
496
|
+
});
|
|
497
|
+
\`\`\`
|
|
498
|
+
|
|
499
|
+
**API:**
|
|
500
|
+
- \`point(e)\` - Pure coordinate conversion, no side effects
|
|
501
|
+
- \`grab(e, obj)\` - Start dragging an object, computing offset from cursor
|
|
502
|
+
- \`move(e)\` - Update dragged object's position
|
|
503
|
+
- \`release()\` - End drag, returns dropped object or null
|
|
504
|
+
- \`dragging\` - The currently dragged object (or null)
|
|
505
|
+
|
|
506
|
+
### \`GameOptions\` (optional)
|
|
507
|
+
|
|
508
|
+
Pass an options object as the second argument to \`game()\`:
|
|
509
|
+
|
|
510
|
+
- \`preset?: 'landscape' | 'portrait' | 'responsive'\`: Game orientation preset.
|
|
511
|
+
- \`'landscape'\` (default): 640\xD7360 - for platformers, shooters, racing
|
|
512
|
+
- \`'portrait'\`: 360\xD7640 - for puzzle, cards, match-3, mobile-style
|
|
513
|
+
- \`'responsive'\`: Fills container, no fixed dimensions (legacy - gameplay varies by device)
|
|
514
|
+
- \`width?: number\`: Override width (default: 640 for landscape, 360 for portrait)
|
|
515
|
+
- \`height?: number\`: Override height (default: 360 for landscape, 640 for portrait)
|
|
516
|
+
- \`fit?: 'contain' | 'cover' | 'stretch'\`: How game fits container (default: \`'contain'\` with letterboxing)
|
|
517
|
+
- \`pixelRatio?: 'device' | number\`: (default: \`'device'\`)
|
|
518
|
+
- \`maxPixelRatio?: number\`: (default: \`2\`)
|
|
519
|
+
- \`preventContextMenu?: boolean\`: Prevent right-click context menu on canvas (default: \`true\`)
|
|
520
|
+
|
|
521
|
+
**Default behavior:** Fixed 640\xD7360 (16:9) with letterboxing. Games work identically on all devices.
|
|
522
|
+
|
|
523
|
+
-----
|
|
524
|
+
|
|
525
|
+
## Recipes
|
|
526
|
+
|
|
527
|
+
### Recipe 1: UI-Only Game (e.g., Clicker)
|
|
528
|
+
|
|
529
|
+
Use \`game\`, \`on\`, and \`ui\`.
|
|
530
|
+
|
|
531
|
+
\`\`\`ts
|
|
532
|
+
import { game } from 'star-canvas';
|
|
533
|
+
|
|
534
|
+
game(({ on, ui }) => {
|
|
535
|
+
let score = 0;
|
|
536
|
+
|
|
537
|
+
function render() {
|
|
538
|
+
// UI is interactive by default - buttons, scroll, forms all work
|
|
539
|
+
ui.render(\`
|
|
540
|
+
<div class="min-h-[100dvh] grid place-items-center bg-purple-900 text-white">
|
|
541
|
+
<div class="text-center space-y-4">
|
|
542
|
+
<h1 class="text-4xl font-bold">Score: \\\${score}</h1>
|
|
543
|
+
<button id="clickBtn" class="px-8 py-4 rounded-xl bg-cyan-400 text-slate-900 font-bold">
|
|
544
|
+
Click Me!
|
|
545
|
+
</button>
|
|
546
|
+
</div>
|
|
547
|
+
</div>
|
|
548
|
+
\`);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Button clicks work by default
|
|
552
|
+
on('click', '#clickBtn', () => {
|
|
553
|
+
score++;
|
|
554
|
+
render();
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
render();
|
|
558
|
+
});
|
|
559
|
+
\`\`\`
|
|
560
|
+
|
|
561
|
+
### Recipe 2: Canvas Game (Landscape)
|
|
562
|
+
|
|
563
|
+
Default pattern - fixed 640\xD7360 resolution. Games work identically on all devices.
|
|
564
|
+
|
|
565
|
+
\`\`\`ts
|
|
566
|
+
import { game } from 'star-canvas';
|
|
567
|
+
|
|
568
|
+
game(({ ctx, width, height, loop }) => {
|
|
569
|
+
// width = 640, height = 360 (always, with letterboxing)
|
|
570
|
+
const playerSize = 32;
|
|
571
|
+
const speed = 200; // 200px per second
|
|
572
|
+
|
|
573
|
+
const player = { x: 64, y: 180 }; // Fixed positions work everywhere
|
|
574
|
+
|
|
575
|
+
loop((dt) => {
|
|
576
|
+
player.x += speed * dt;
|
|
577
|
+
if (player.x > width) player.x = -playerSize;
|
|
578
|
+
|
|
579
|
+
ctx.fillStyle = '#0f172a';
|
|
580
|
+
ctx.fillRect(0, 0, width, height);
|
|
581
|
+
|
|
582
|
+
ctx.fillStyle = '#22d3ee';
|
|
583
|
+
ctx.fillRect(player.x, player.y - playerSize/2, playerSize, playerSize);
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
// Default: 640\xD7360 landscape with letterboxing
|
|
587
|
+
\`\`\`
|
|
588
|
+
|
|
589
|
+
### Recipe 3: Canvas Game (Portrait)
|
|
590
|
+
|
|
591
|
+
For puzzle games, card games, match-3, mobile-style games - use portrait preset.
|
|
592
|
+
|
|
593
|
+
\`\`\`ts
|
|
594
|
+
import { game } from 'star-canvas';
|
|
595
|
+
|
|
596
|
+
game(({ ctx, width, height, loop, canvas, toStagePoint }) => {
|
|
597
|
+
// width = 360, height = 640 (always, with letterboxing)
|
|
598
|
+
const cellSize = 40;
|
|
599
|
+
const gridCols = 8;
|
|
600
|
+
const gridRows = 12;
|
|
601
|
+
|
|
602
|
+
// Center the grid
|
|
603
|
+
const gridWidth = gridCols * cellSize;
|
|
604
|
+
const gridX = (width - gridWidth) / 2;
|
|
605
|
+
const gridY = 80;
|
|
606
|
+
|
|
607
|
+
canvas.addEventListener('pointerdown', (e) => {
|
|
608
|
+
const { x, y } = toStagePoint(e);
|
|
609
|
+
// Handle tap on grid...
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
loop((dt) => {
|
|
613
|
+
ctx.fillStyle = '#1e1b4b';
|
|
614
|
+
ctx.fillRect(0, 0, width, height);
|
|
615
|
+
|
|
616
|
+
ctx.strokeStyle = '#4338ca';
|
|
617
|
+
for (let row = 0; row < gridRows; row++) {
|
|
618
|
+
for (let col = 0; col < gridCols; col++) {
|
|
619
|
+
ctx.strokeRect(
|
|
620
|
+
gridX + col * cellSize,
|
|
621
|
+
gridY + row * cellSize,
|
|
622
|
+
cellSize, cellSize
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
}, { preset: 'portrait' }); // 360\xD7640 portrait with letterboxing
|
|
628
|
+
\`\`\`
|
|
629
|
+
|
|
630
|
+
### Recipe 4: Custom Resolution
|
|
631
|
+
|
|
632
|
+
For games that need different dimensions (e.g., pixel art at 320\xD7180).
|
|
633
|
+
|
|
634
|
+
\`\`\`ts
|
|
635
|
+
import { game } from 'star-canvas';
|
|
636
|
+
|
|
637
|
+
game(({ ctx, width, height, loop, toStagePoint, canvas }) => {
|
|
638
|
+
// Custom 320\xD7180 resolution (retro pixel art style)
|
|
639
|
+
const player = { x: 160, y: 90 }; // Center
|
|
640
|
+
|
|
641
|
+
canvas.addEventListener('pointerdown', (e) => {
|
|
642
|
+
const { x, y } = toStagePoint(e);
|
|
643
|
+
console.log('Tapped at:', x, y); // Always 0-320, 0-180
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
loop((dt) => {
|
|
647
|
+
ctx.fillStyle = '#0f172a';
|
|
648
|
+
ctx.fillRect(0, 0, width, height);
|
|
649
|
+
|
|
650
|
+
ctx.fillStyle = '#22d3ee';
|
|
651
|
+
ctx.fillRect(player.x - 8, player.y - 8, 16, 16);
|
|
652
|
+
});
|
|
653
|
+
}, { width: 320, height: 180 }); // Custom resolution with letterboxing
|
|
654
|
+
\`\`\`
|
|
655
|
+
|
|
656
|
+
### Recipe 5: Complex Game with Canvas + UI + Events (like FLOW)
|
|
657
|
+
|
|
658
|
+
\`\`\`ts
|
|
659
|
+
import { game } from 'star-canvas';
|
|
660
|
+
import { createLeaderboard } from '/star-sdk/v1/leaderboard.js';
|
|
661
|
+
|
|
662
|
+
const leaderboard = createLeaderboard();
|
|
663
|
+
|
|
664
|
+
game(({ ctx, width, height, loop, ui, on, canvas, toStagePoint }) => {
|
|
665
|
+
let score = 0;
|
|
666
|
+
let state = 'menu';
|
|
667
|
+
|
|
668
|
+
function handleTap() {
|
|
669
|
+
if (state === 'menu' || state === 'gameover') {
|
|
670
|
+
startGame();
|
|
671
|
+
} else if (state === 'playing') {
|
|
672
|
+
// ... (player float logic) ...
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// 1. Listen for screen taps - this makes UI click-through automatically
|
|
677
|
+
canvas.addEventListener('pointerdown', handleTap);
|
|
678
|
+
|
|
679
|
+
// 2. Listen for button clicks - buttons need pointer-events-auto
|
|
680
|
+
on('click', '#leaderboard-btn', (e) => {
|
|
681
|
+
e.stopPropagation();
|
|
682
|
+
leaderboard.show();
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
// 3. Render UI - buttons need pointer-events-auto to intercept clicks
|
|
686
|
+
let lastState = null;
|
|
687
|
+
let lastScore = -1;
|
|
688
|
+
|
|
689
|
+
function updateUI() {
|
|
690
|
+
// CRITICAL: Only render when state/score changes, NOT every frame
|
|
691
|
+
// Calling ui.render() in the loop breaks buttons (DOM recreation)
|
|
692
|
+
if (state === lastState && score === lastScore) return;
|
|
693
|
+
lastState = state;
|
|
694
|
+
lastScore = score;
|
|
695
|
+
|
|
696
|
+
if (state === 'menu') {
|
|
697
|
+
ui.render(\`
|
|
698
|
+
<div class="h-full flex flex-col items-center justify-center text-white">
|
|
699
|
+
<h1 class="text-6xl font-bold mb-4">FLOW</h1>
|
|
700
|
+
<div class="text-2xl animate-pulse">TAP TO START</div>
|
|
701
|
+
</div>\`);
|
|
702
|
+
} else if (state === 'playing') {
|
|
703
|
+
ui.render(\`
|
|
704
|
+
<div class="absolute top-8 left-1/2 -translate-x-1/2 text-white">
|
|
705
|
+
<div class="text-5xl font-bold">\\\${score}</div>
|
|
706
|
+
</div>\`);
|
|
707
|
+
} else if (state === 'gameover') {
|
|
708
|
+
ui.render(\`
|
|
709
|
+
<div class="h-full flex flex-col items-center justify-center text-white">
|
|
710
|
+
<div class="text-3xl mb-4">GAME OVER</div>
|
|
711
|
+
<div class="text-6xl mb-4">\\\${score}</div>
|
|
712
|
+
<button id="leaderboard-btn" class="px-6 py-3 mb-4 bg-purple-600 rounded-lg pointer-events-auto">
|
|
713
|
+
VIEW LEADERBOARD
|
|
714
|
+
</button>
|
|
715
|
+
<div class="text-xl animate-pulse">TAP TO RESTART</div>
|
|
716
|
+
</div>\`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// 4. Call updateUI when state changes (NOT every frame)
|
|
721
|
+
updateUI();
|
|
722
|
+
|
|
723
|
+
// Update when state transitions happen
|
|
724
|
+
function startGame() {
|
|
725
|
+
state = 'playing';
|
|
726
|
+
score = 0;
|
|
727
|
+
updateUI();
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function endGame() {
|
|
731
|
+
state = 'gameover';
|
|
732
|
+
// Submit score to leaderboard
|
|
733
|
+
leaderboard.submit(score);
|
|
734
|
+
updateUI();
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
\`\`\`
|
|
738
|
+
|
|
739
|
+
### Recipe 6: Safe Canvas Transforms (scoped)
|
|
740
|
+
|
|
741
|
+
When applying temporary transforms (translate, rotate, scale), use \`scoped()\` to automatically restore the context state:
|
|
742
|
+
|
|
743
|
+
\`\`\`ts
|
|
744
|
+
import { game } from 'star-canvas';
|
|
745
|
+
|
|
746
|
+
game(({ ctx, scoped, loop }) => {
|
|
747
|
+
const cards = [
|
|
748
|
+
{ x: 100, y: 100, angle: 0.1, visible: true },
|
|
749
|
+
{ x: 200, y: 150, angle: -0.2, visible: true },
|
|
750
|
+
];
|
|
751
|
+
|
|
752
|
+
function drawCard(card) {
|
|
753
|
+
scoped(() => {
|
|
754
|
+
ctx.translate(card.x, card.y);
|
|
755
|
+
ctx.rotate(card.angle);
|
|
756
|
+
if (!card.visible) return; // Safe! restore() still happens
|
|
757
|
+
ctx.fillStyle = '#3b82f6';
|
|
758
|
+
ctx.fillRect(-40, -60, 80, 120);
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
loop(() => {
|
|
763
|
+
ctx.clearRect(0, 0, 800, 600);
|
|
764
|
+
cards.forEach(drawCard);
|
|
765
|
+
});
|
|
766
|
+
});
|
|
767
|
+
\`\`\`
|
|
768
|
+
|
|
769
|
+
**Why use \`scoped()\`:** Prevents transform stack corruption from early returns, exceptions, or forgetting \`ctx.restore()\`. The context is always restored, even if the function exits early.
|
|
770
|
+
|
|
771
|
+
### Recipe 7: Drag and Drop with createDrag() (RECOMMENDED)
|
|
772
|
+
|
|
773
|
+
Use the \`createDrag()\` helper - it handles coordinate conversion and offset tracking automatically.
|
|
774
|
+
|
|
775
|
+
\`\`\`ts
|
|
776
|
+
import { game } from 'star-canvas';
|
|
777
|
+
|
|
778
|
+
game(({ ctx, width, height, loop, canvas, createDrag }) => {
|
|
779
|
+
// Size relative to height for consistency
|
|
780
|
+
const pieceSize = height * 0.15;
|
|
781
|
+
|
|
782
|
+
const pieces = [
|
|
783
|
+
{ x: width * 0.2, y: height * 0.3, color: '#ef4444' },
|
|
784
|
+
{ x: width * 0.4, y: height * 0.4, color: '#22c55e' },
|
|
785
|
+
{ x: width * 0.6, y: height * 0.3, color: '#3b82f6' },
|
|
786
|
+
];
|
|
787
|
+
|
|
788
|
+
// Create drag helper - handles coordinate conversion automatically
|
|
789
|
+
const drag = createDrag();
|
|
790
|
+
|
|
791
|
+
function hitTest(x, y) {
|
|
792
|
+
for (let i = pieces.length - 1; i >= 0; i--) {
|
|
793
|
+
const p = pieces[i];
|
|
794
|
+
if (x >= p.x && x < p.x + pieceSize && y >= p.y && y < p.y + pieceSize) {
|
|
795
|
+
return p;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
canvas.addEventListener('pointerdown', (e) => {
|
|
802
|
+
canvas.setPointerCapture(e.pointerId); // IMPORTANT: Ensures drag works outside canvas
|
|
803
|
+
const { x, y } = drag.point(e); // Convert coordinates
|
|
804
|
+
const hit = hitTest(x, y);
|
|
805
|
+
if (hit) {
|
|
806
|
+
drag.grab(e, hit); // Start drag with offset from cursor
|
|
807
|
+
canvas.style.cursor = 'grabbing';
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
canvas.addEventListener('pointermove', (e) => {
|
|
812
|
+
drag.move(e); // Updates grabbed object position
|
|
813
|
+
if (!drag.dragging) {
|
|
814
|
+
const { x, y } = drag.point(e);
|
|
815
|
+
canvas.style.cursor = hitTest(x, y) ? 'grab' : 'default';
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
canvas.addEventListener('pointerup', () => {
|
|
820
|
+
const dropped = drag.release(); // Returns dropped object (or null)
|
|
821
|
+
if (dropped) {
|
|
822
|
+
console.log('Dropped:', dropped);
|
|
823
|
+
}
|
|
824
|
+
canvas.style.cursor = 'default';
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
loop(() => {
|
|
828
|
+
ctx.fillStyle = '#1e293b';
|
|
829
|
+
ctx.fillRect(0, 0, width, height);
|
|
830
|
+
|
|
831
|
+
for (const p of pieces) {
|
|
832
|
+
ctx.fillStyle = drag.dragging === p ? '#fbbf24' : p.color;
|
|
833
|
+
ctx.fillRect(p.x, p.y, pieceSize, pieceSize);
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
});
|
|
837
|
+
\`\`\`
|
|
838
|
+
|
|
839
|
+
**CRITICAL: Always use \`setPointerCapture()\`** - This ensures drags work even when the pointer moves outside the canvas. Without it, fast drags can leave objects stuck mid-drag.
|
|
840
|
+
|
|
841
|
+
### Recipe 8: Drag and Drop (Manual Pattern)
|
|
842
|
+
|
|
843
|
+
If you need more control, here's the manual approach with \`toStagePoint()\`.
|
|
844
|
+
|
|
845
|
+
\`\`\`ts
|
|
846
|
+
import { game } from 'star-canvas';
|
|
847
|
+
|
|
848
|
+
game(({ ctx, width, height, loop, canvas, toStagePoint }) => {
|
|
849
|
+
const pieceSize = height * 0.15;
|
|
850
|
+
const pieces = [
|
|
851
|
+
{ x: width * 0.2, y: height * 0.3, color: '#ef4444' },
|
|
852
|
+
{ x: width * 0.4, y: height * 0.4, color: '#22c55e' },
|
|
853
|
+
];
|
|
854
|
+
|
|
855
|
+
// Manual drag state
|
|
856
|
+
let dragging = null;
|
|
857
|
+
let dragOffsetX = 0;
|
|
858
|
+
let dragOffsetY = 0;
|
|
859
|
+
|
|
860
|
+
function hitTest(px, py) {
|
|
861
|
+
for (let i = pieces.length - 1; i >= 0; i--) {
|
|
862
|
+
const p = pieces[i];
|
|
863
|
+
if (px >= p.x && px < p.x + pieceSize && py >= p.y && py < p.y + pieceSize) {
|
|
864
|
+
return p;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
return null;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
canvas.addEventListener('pointerdown', (e) => {
|
|
871
|
+
canvas.setPointerCapture(e.pointerId); // Ensures drag works outside canvas
|
|
872
|
+
const { x, y } = toStagePoint(e); // CRITICAL: Convert coordinates!
|
|
873
|
+
const hit = hitTest(x, y);
|
|
874
|
+
if (hit) {
|
|
875
|
+
dragging = hit;
|
|
876
|
+
dragOffsetX = x - hit.x; // Store offset
|
|
877
|
+
dragOffsetY = y - hit.y;
|
|
878
|
+
canvas.style.cursor = 'grabbing';
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
canvas.addEventListener('pointermove', (e) => {
|
|
883
|
+
const { x, y } = toStagePoint(e); // CRITICAL: Convert here too!
|
|
884
|
+
if (dragging) {
|
|
885
|
+
dragging.x = x - dragOffsetX;
|
|
886
|
+
dragging.y = y - dragOffsetY;
|
|
887
|
+
} else {
|
|
888
|
+
canvas.style.cursor = hitTest(x, y) ? 'grab' : 'default';
|
|
889
|
+
}
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
canvas.addEventListener('pointerup', () => {
|
|
893
|
+
dragging = null;
|
|
894
|
+
canvas.style.cursor = 'default';
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
loop(() => {
|
|
898
|
+
ctx.fillStyle = '#1e293b';
|
|
899
|
+
ctx.fillRect(0, 0, width, height);
|
|
900
|
+
|
|
901
|
+
for (const p of pieces) {
|
|
902
|
+
ctx.fillStyle = dragging === p ? '#fbbf24' : p.color;
|
|
903
|
+
ctx.fillRect(p.x, p.y, pieceSize, pieceSize);
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
});
|
|
907
|
+
\`\`\`
|
|
908
|
+
|
|
909
|
+
**Common Drag-Drop Mistakes:**
|
|
910
|
+
|
|
911
|
+
1. \u274C Forgetting \`toStagePoint()\` in pointermove \u2192 \`createDrag()\` fixes this
|
|
912
|
+
2. \u274C No drag offset (piece "jumps" to cursor) \u2192 \`createDrag()\` fixes this
|
|
913
|
+
3. \u274C Using \`e.clientX/clientY\` directly \u2192 \`createDrag()\` fixes this
|
|
914
|
+
4. \u274C Not clearing state on pointerup \u2192 \`createDrag()\` fixes this
|
|
915
|
+
5. \u274C Missing \`setPointerCapture()\` (drags break outside canvas) \u2192 **You must add this!**
|
|
916
|
+
|
|
917
|
+
**Recommendation:** Use \`createDrag()\` + \`setPointerCapture()\` for bulletproof drag-and-drop.`;
|
|
918
|
+
var LEADERBOARD_DOCS = `**Installation**
|
|
919
|
+
|
|
920
|
+
\`\`\`bash
|
|
921
|
+
yarn add star-leaderboard
|
|
922
|
+
\`\`\`
|
|
923
|
+
|
|
924
|
+
### Star Leaderboard SDK
|
|
925
|
+
|
|
926
|
+
**Simple leaderboards for Star games.** Submit scores, show rankings, share results. Never crashes your game.
|
|
927
|
+
|
|
928
|
+
**Import:**
|
|
929
|
+
\`\`\`javascript
|
|
930
|
+
import { createLeaderboard } from 'star-leaderboard';
|
|
931
|
+
const leaderboard = createLeaderboard();
|
|
932
|
+
\`\`\`
|
|
933
|
+
|
|
934
|
+
**CRITICAL:** Import in JavaScript - don't add \`<script>\` tags.
|
|
935
|
+
|
|
936
|
+
---
|
|
937
|
+
|
|
938
|
+
**Quick Start:**
|
|
939
|
+
|
|
940
|
+
\`\`\`javascript
|
|
941
|
+
import { createLeaderboard } from 'star-leaderboard';
|
|
942
|
+
import { game } from '/star-sdk/v1/dom.js';
|
|
943
|
+
|
|
944
|
+
const leaderboard = createLeaderboard();
|
|
945
|
+
|
|
946
|
+
game(({ ctx, width, height, loop, ui, on, canvas }) => {
|
|
947
|
+
let score = 0;
|
|
948
|
+
let state = 'playing';
|
|
949
|
+
|
|
950
|
+
function endGame() {
|
|
951
|
+
state = 'gameover';
|
|
952
|
+
// Submit score and show leaderboard
|
|
953
|
+
leaderboard.submit(score);
|
|
954
|
+
leaderboard.show();
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Game logic...
|
|
958
|
+
});
|
|
959
|
+
\`\`\`
|
|
960
|
+
|
|
961
|
+
**That's it!** The SDK handles:
|
|
962
|
+
- Score submission (works for guests and logged-in users)
|
|
963
|
+
- Platform leaderboard UI (modal with rankings)
|
|
964
|
+
- Weekly/all-time timeframes
|
|
965
|
+
- AI-detected scoring (score/time/moves - higher or lower is better)
|
|
966
|
+
|
|
967
|
+
---
|
|
968
|
+
|
|
969
|
+
**API Reference:**
|
|
970
|
+
|
|
971
|
+
**Core Methods:**
|
|
972
|
+
\`\`\`javascript
|
|
973
|
+
leaderboard.submit(score) // Submit score, returns Promise<{ success, rank, scoreId }>
|
|
974
|
+
leaderboard.show() // Show platform leaderboard UI
|
|
975
|
+
leaderboard.getScores(options) // Fetch scores for custom UI
|
|
976
|
+
leaderboard.share(options) // Generate shareable link
|
|
977
|
+
\`\`\`
|
|
978
|
+
|
|
979
|
+
**Properties:**
|
|
980
|
+
\`\`\`javascript
|
|
981
|
+
leaderboard.ready // true when SDK is initialized
|
|
982
|
+
leaderboard.gameId // Current game ID (auto-detected on platform)
|
|
983
|
+
\`\`\`
|
|
984
|
+
|
|
985
|
+
**Aliases (for discoverability):**
|
|
986
|
+
\`\`\`javascript
|
|
987
|
+
leaderboard.submitScore(score) // Same as submit()
|
|
988
|
+
leaderboard.showLeaderboard() // Same as show()
|
|
989
|
+
\`\`\`
|
|
990
|
+
|
|
991
|
+
---
|
|
992
|
+
|
|
993
|
+
**Patterns:**
|
|
994
|
+
|
|
995
|
+
### Pattern 1: Submit and Show (Most Common)
|
|
996
|
+
|
|
997
|
+
\`\`\`javascript
|
|
998
|
+
import { createLeaderboard } from 'star-leaderboard';
|
|
999
|
+
|
|
1000
|
+
const leaderboard = createLeaderboard();
|
|
1001
|
+
|
|
1002
|
+
function gameOver(finalScore) {
|
|
1003
|
+
// Fire and forget - simplest approach
|
|
1004
|
+
leaderboard.submit(finalScore);
|
|
1005
|
+
leaderboard.show();
|
|
1006
|
+
}
|
|
1007
|
+
\`\`\`
|
|
1008
|
+
|
|
1009
|
+
### Pattern 2: With Rank Feedback
|
|
1010
|
+
|
|
1011
|
+
\`\`\`javascript
|
|
1012
|
+
import { createLeaderboard } from 'star-leaderboard';
|
|
1013
|
+
|
|
1014
|
+
const leaderboard = createLeaderboard();
|
|
1015
|
+
|
|
1016
|
+
async function gameOver(finalScore) {
|
|
1017
|
+
const { success, rank } = await leaderboard.submit(finalScore);
|
|
1018
|
+
|
|
1019
|
+
if (success && rank) {
|
|
1020
|
+
console.log(\`You ranked #\${rank}!\`);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
leaderboard.show();
|
|
1024
|
+
}
|
|
1025
|
+
\`\`\`
|
|
1026
|
+
|
|
1027
|
+
### Pattern 3: Leaderboard Button
|
|
1028
|
+
|
|
1029
|
+
\`\`\`javascript
|
|
1030
|
+
import { createLeaderboard } from 'star-leaderboard';
|
|
1031
|
+
import { game } from '/star-sdk/v1/dom.js';
|
|
1032
|
+
|
|
1033
|
+
const leaderboard = createLeaderboard();
|
|
1034
|
+
|
|
1035
|
+
game(({ ui, on }) => {
|
|
1036
|
+
ui.render(\`
|
|
1037
|
+
<button id="lb-btn" class="pointer-events-auto">
|
|
1038
|
+
View Leaderboard
|
|
1039
|
+
</button>
|
|
1040
|
+
\`);
|
|
1041
|
+
|
|
1042
|
+
on('click', '#lb-btn', (e) => {
|
|
1043
|
+
e.stopPropagation();
|
|
1044
|
+
leaderboard.show();
|
|
1045
|
+
});
|
|
1046
|
+
});
|
|
1047
|
+
\`\`\`
|
|
1048
|
+
|
|
1049
|
+
### Pattern 4: Custom Leaderboard UI
|
|
1050
|
+
|
|
1051
|
+
\`\`\`javascript
|
|
1052
|
+
import { createLeaderboard } from 'star-leaderboard';
|
|
1053
|
+
|
|
1054
|
+
const leaderboard = createLeaderboard();
|
|
1055
|
+
|
|
1056
|
+
async function showCustomLeaderboard() {
|
|
1057
|
+
const { scores, you, config } = await leaderboard.getScores({
|
|
1058
|
+
timeframe: 'weekly', // or 'all_time'
|
|
1059
|
+
limit: 10
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
// Render your own UI
|
|
1063
|
+
scores.forEach(entry => {
|
|
1064
|
+
console.log(\`#\${entry.rank} \${entry.playerName}: \${entry.score}\`);
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
if (you) {
|
|
1068
|
+
console.log(\`Your rank: #\${you.rank}\`);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
\`\`\`
|
|
1072
|
+
|
|
1073
|
+
### Pattern 5: Full Game Example
|
|
1074
|
+
|
|
1075
|
+
\`\`\`javascript
|
|
1076
|
+
import { createLeaderboard } from 'star-leaderboard';
|
|
1077
|
+
import { game } from '/star-sdk/v1/dom.js';
|
|
1078
|
+
|
|
1079
|
+
const leaderboard = createLeaderboard();
|
|
1080
|
+
|
|
1081
|
+
game(({ ctx, width, height, loop, ui, on, canvas }) => {
|
|
1082
|
+
let score = 0;
|
|
1083
|
+
let state = 'menu';
|
|
1084
|
+
|
|
1085
|
+
function startGame() {
|
|
1086
|
+
state = 'playing';
|
|
1087
|
+
score = 0;
|
|
1088
|
+
updateUI();
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function endGame() {
|
|
1092
|
+
state = 'gameover';
|
|
1093
|
+
leaderboard.submit(score);
|
|
1094
|
+
updateUI();
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// UI with leaderboard button
|
|
1098
|
+
function updateUI() {
|
|
1099
|
+
if (state === 'gameover') {
|
|
1100
|
+
ui.render(\`
|
|
1101
|
+
<div class="h-full flex flex-col items-center justify-center text-white">
|
|
1102
|
+
<div class="text-3xl mb-4">GAME OVER</div>
|
|
1103
|
+
<div class="text-6xl mb-4">\\\${score}</div>
|
|
1104
|
+
<button id="lb-btn" class="px-6 py-3 mb-4 bg-purple-600 rounded-lg pointer-events-auto">
|
|
1105
|
+
VIEW LEADERBOARD
|
|
1106
|
+
</button>
|
|
1107
|
+
<div class="text-xl animate-pulse">TAP TO RESTART</div>
|
|
1108
|
+
</div>
|
|
1109
|
+
\`);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
on('click', '#lb-btn', (e) => {
|
|
1114
|
+
e.stopPropagation();
|
|
1115
|
+
leaderboard.show();
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
canvas.addEventListener('pointerdown', () => {
|
|
1119
|
+
if (state === 'menu' || state === 'gameover') startGame();
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
loop((dt) => {
|
|
1123
|
+
// Game logic...
|
|
1124
|
+
});
|
|
1125
|
+
});
|
|
1126
|
+
\`\`\`
|
|
1127
|
+
|
|
1128
|
+
---
|
|
1129
|
+
|
|
1130
|
+
**Options:**
|
|
1131
|
+
|
|
1132
|
+
\`\`\`javascript
|
|
1133
|
+
// Default - auto-detects gameId on Star platform
|
|
1134
|
+
const leaderboard = createLeaderboard();
|
|
1135
|
+
|
|
1136
|
+
// Standalone use (outside Star platform)
|
|
1137
|
+
const leaderboard = createLeaderboard({
|
|
1138
|
+
gameId: 'your-game-uuid',
|
|
1139
|
+
apiBase: 'https://buildwithstar.com'
|
|
1140
|
+
});
|
|
1141
|
+
\`\`\`
|
|
1142
|
+
|
|
1143
|
+
---
|
|
1144
|
+
|
|
1145
|
+
**getScores Options:**
|
|
1146
|
+
|
|
1147
|
+
\`\`\`javascript
|
|
1148
|
+
const data = await leaderboard.getScores({
|
|
1149
|
+
timeframe: 'weekly', // 'weekly' (default) or 'all_time'
|
|
1150
|
+
limit: 10 // Number of scores (default: 10)
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
// Returns:
|
|
1154
|
+
{
|
|
1155
|
+
scores: [{ id, playerName, score, rank, submittedAt }],
|
|
1156
|
+
config: { sort: 'DESC', valueType: 'score' },
|
|
1157
|
+
timeframe: 'weekly',
|
|
1158
|
+
you: { ... } | null, // Your score if outside top scores
|
|
1159
|
+
weekResetTime: 1234567890 // Unix ms when weekly resets
|
|
1160
|
+
}
|
|
1161
|
+
\`\`\`
|
|
1162
|
+
|
|
1163
|
+
---
|
|
1164
|
+
|
|
1165
|
+
**Tips:**
|
|
1166
|
+
|
|
1167
|
+
1. **Call \`submit()\` before \`show()\`** - Ensures your score appears immediately in the leaderboard.
|
|
1168
|
+
|
|
1169
|
+
2. **Fire and forget is fine** - \`submit()\` returns a Promise but you don't need to await it.
|
|
1170
|
+
|
|
1171
|
+
3. **Use \`show()\` for platform UI** - It's the easiest way. Use \`getScores()\` only if you need custom rendering.
|
|
1172
|
+
|
|
1173
|
+
4. **Don't store leaderboard state** - Just call the SDK methods when needed. The platform handles caching.
|
|
1174
|
+
|
|
1175
|
+
5. **Works for guests** - Guests get a generated name like "Guest1234". They can sign in later to claim scores.`;
|
|
1176
|
+
var MULTIPLAYER_DOCS = `**Installation**
|
|
1177
|
+
|
|
1178
|
+
\`\`\`bash
|
|
1179
|
+
yarn add star-multiplayer
|
|
1180
|
+
\`\`\`
|
|
1181
|
+
|
|
1182
|
+
### Star Multiplayer SDK
|
|
1183
|
+
|
|
1184
|
+
**Real-time multiplayer for Star games.** Host-authoritative state sync with automatic room management.
|
|
1185
|
+
|
|
1186
|
+
**Import:**
|
|
1187
|
+
\`\`\`javascript
|
|
1188
|
+
import { createMultiplayer } from 'star-multiplayer';
|
|
1189
|
+
const mp = createMultiplayer();
|
|
1190
|
+
\`\`\`
|
|
1191
|
+
|
|
1192
|
+
**CRITICAL:** Import in JavaScript - don't add \`<script>\` tags.
|
|
1193
|
+
|
|
1194
|
+
**Features:**
|
|
1195
|
+
- **Host-authoritative** - One player runs game logic, others receive state
|
|
1196
|
+
- **Room-based** - Create/join rooms with 6-character invite codes
|
|
1197
|
+
- **Auto host migration** - If host leaves, another player becomes host
|
|
1198
|
+
- **Built-in lobby UI** - No need to build your own
|
|
1199
|
+
- **Works in iframes** - Uses postMessage relay to parent window
|
|
1200
|
+
|
|
1201
|
+
---
|
|
1202
|
+
|
|
1203
|
+
**Quick Start:**
|
|
1204
|
+
|
|
1205
|
+
\`\`\`javascript
|
|
1206
|
+
import { createMultiplayer } from 'star-multiplayer';
|
|
1207
|
+
import { game } from '/star-sdk/v1/dom.js';
|
|
1208
|
+
|
|
1209
|
+
const mp = createMultiplayer();
|
|
1210
|
+
|
|
1211
|
+
game(async ({ ctx, width, height, loop, canvas }) => {
|
|
1212
|
+
// 1. Start - auto-finds or creates room, starts immediately
|
|
1213
|
+
await mp.start();
|
|
1214
|
+
|
|
1215
|
+
// 2. Game state
|
|
1216
|
+
let state = { players: {}, ball: { x: width/2, y: height/2, vx: 200, vy: 150 } };
|
|
1217
|
+
|
|
1218
|
+
// 3. Receive state updates - update shared objects, keep local player prediction
|
|
1219
|
+
mp.onState((s) => {
|
|
1220
|
+
state.ball = s.ball;
|
|
1221
|
+
for (const [id, p] of Object.entries(s.players)) {
|
|
1222
|
+
if (id !== mp.localPlayerId) {
|
|
1223
|
+
state.players[id] = p;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
// 4. Handle inputs (runs locally for prediction, on host for authoritative state)
|
|
1229
|
+
mp.onInput((playerId, input) => {
|
|
1230
|
+
if (!state.players[playerId]) {
|
|
1231
|
+
state.players[playerId] = { y: height/2 };
|
|
1232
|
+
}
|
|
1233
|
+
state.players[playerId].y = input.y;
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
// 5. Game loop
|
|
1237
|
+
loop((dt) => {
|
|
1238
|
+
// Host: run physics + broadcast state (auto-throttled to 20Hz)
|
|
1239
|
+
mp.hostTick(dt, () => {
|
|
1240
|
+
state.ball.x += state.ball.vx * dt;
|
|
1241
|
+
state.ball.y += state.ball.vy * dt;
|
|
1242
|
+
if (state.ball.y < 0 || state.ball.y > height) state.ball.vy *= -1;
|
|
1243
|
+
return state;
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
// Everyone: render with interpolation for smooth movement
|
|
1247
|
+
const renderState = mp.getInterpolatedState(dt, state);
|
|
1248
|
+
ctx.fillStyle = '#0f172a';
|
|
1249
|
+
ctx.fillRect(0, 0, width, height);
|
|
1250
|
+
for (const p of Object.values(renderState.players)) {
|
|
1251
|
+
ctx.fillStyle = '#22d3ee';
|
|
1252
|
+
ctx.fillRect(20, p.y - 40, 10, 80);
|
|
1253
|
+
}
|
|
1254
|
+
ctx.beginPath();
|
|
1255
|
+
ctx.arc(renderState.ball.x, renderState.ball.y, 10, 0, Math.PI * 2);
|
|
1256
|
+
ctx.fill();
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
// 6. Input - works for EVERYONE (applied locally for instant feedback)
|
|
1260
|
+
canvas.onpointermove = (e) => {
|
|
1261
|
+
const rect = canvas.getBoundingClientRect();
|
|
1262
|
+
mp.input({ y: ((e.clientY - rect.top) / rect.height) * height });
|
|
1263
|
+
};
|
|
1264
|
+
});
|
|
1265
|
+
\`\`\`
|
|
1266
|
+
|
|
1267
|
+
**That's it!** The SDK handles:
|
|
1268
|
+
- Auto-matchmaking (finds open room or creates one)
|
|
1269
|
+
- URL detection (\`?room=CODE\` \u2192 joins specific room)
|
|
1270
|
+
- Throttling state broadcasts to 20Hz
|
|
1271
|
+
- Routing host's inputs to their own handler
|
|
1272
|
+
|
|
1273
|
+
---
|
|
1274
|
+
|
|
1275
|
+
**Architecture:**
|
|
1276
|
+
|
|
1277
|
+
\`\`\`
|
|
1278
|
+
Host (Player 1) Server Client (Player 2)
|
|
1279
|
+
\u2502 \u2502 \u2502
|
|
1280
|
+
\u2502 \u2500\u2500 state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2500\u2500 state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25B6\u2502
|
|
1281
|
+
\u2502 \u2502 \u2502
|
|
1282
|
+
\u2502\u25C0\u2500\u2500 input \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2500\u2500 input \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502
|
|
1283
|
+
\u2502 \u2502 \u2502
|
|
1284
|
+
\u2502 (runs game logic) \u2502 (dumb relay) \u2502 (renders state)
|
|
1285
|
+
\`\`\`
|
|
1286
|
+
|
|
1287
|
+
---
|
|
1288
|
+
|
|
1289
|
+
**API Reference:**
|
|
1290
|
+
|
|
1291
|
+
**State (read-only):**
|
|
1292
|
+
\`\`\`javascript
|
|
1293
|
+
mp.isHost // true if you're the host
|
|
1294
|
+
mp.isReady // true if game has started
|
|
1295
|
+
mp.players // Array of { id: string }
|
|
1296
|
+
mp.localPlayerId // Your player ID
|
|
1297
|
+
mp.roomCode // Current room code
|
|
1298
|
+
\`\`\`
|
|
1299
|
+
|
|
1300
|
+
**Setup:**
|
|
1301
|
+
\`\`\`javascript
|
|
1302
|
+
await mp.start(); // Uses all defaults - auto-matchmaking, 8 players
|
|
1303
|
+
|
|
1304
|
+
// Or customize:
|
|
1305
|
+
await mp.start({
|
|
1306
|
+
maxPlayers: 8, // default: 8 (max players per room)
|
|
1307
|
+
mode: 'auto', // default: 'auto' - finds/creates rooms automatically
|
|
1308
|
+
// 'private' - shows lobby with shareable code
|
|
1309
|
+
showLobby: true, // default: true (only used in 'private' mode)
|
|
1310
|
+
tickRate: 50, // default: 50ms (20Hz state broadcasts)
|
|
1311
|
+
prediction: true // default: true (client-side prediction for instant input)
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
// Examples:
|
|
1315
|
+
await mp.start({ maxPlayers: 2 }); // 1v1 game
|
|
1316
|
+
await mp.start({ maxPlayers: 100 }); // Battle royale
|
|
1317
|
+
await mp.start({ mode: 'private' }); // Friends-only with invite code
|
|
1318
|
+
await mp.start({ mode: 'private', maxPlayers: 4 }); // Small private game
|
|
1319
|
+
\`\`\`
|
|
1320
|
+
|
|
1321
|
+
**Sync:**
|
|
1322
|
+
\`\`\`javascript
|
|
1323
|
+
mp.onState((state) => { ... }) // Receive state (returns unsubscribe)
|
|
1324
|
+
mp.onInput((playerId, input) => { ... }) // Handle continuous input (position, aim)
|
|
1325
|
+
mp.hostTick(dt, () => state) // Run on host, broadcast returned state
|
|
1326
|
+
mp.input(data) // Send continuous input (position, aim)
|
|
1327
|
+
mp.getInterpolatedState(dt, state) // Get smoothly interpolated state for rendering
|
|
1328
|
+
\`\`\`
|
|
1329
|
+
|
|
1330
|
+
**Discrete Events (shooting, jumping, using items):**
|
|
1331
|
+
\`\`\`javascript
|
|
1332
|
+
mp.event(type, data) // Send event - guaranteed delivery
|
|
1333
|
+
mp.onEvent((playerId, type, data) => { }) // Handle events (HOST ONLY, authoritative)
|
|
1334
|
+
\`\`\`
|
|
1335
|
+
|
|
1336
|
+
**Player Updates:**
|
|
1337
|
+
\`\`\`javascript
|
|
1338
|
+
mp.onLocalPlayerUpdate((player, prevPlayer) => { ... }) // Local player changed
|
|
1339
|
+
mp.onPlayerUpdate(playerId, (player, prevPlayer) => { ... }) // Specific player changed
|
|
1340
|
+
\`\`\`
|
|
1341
|
+
|
|
1342
|
+
**Lifecycle Events:**
|
|
1343
|
+
\`\`\`javascript
|
|
1344
|
+
mp.onPlayers((players) => { ... }) // Player list changed
|
|
1345
|
+
mp.onPlayerJoin((player) => { ... }) // Another player joined (use to init their state)
|
|
1346
|
+
mp.onPlayerLeave((player) => { ... }) // A player left
|
|
1347
|
+
mp.onHost((isHost) => { ... }) // Host status changed (for host migration)
|
|
1348
|
+
mp.onError((message) => { ... }) // Error occurred
|
|
1349
|
+
\`\`\`
|
|
1350
|
+
|
|
1351
|
+
**Cleanup:**
|
|
1352
|
+
\`\`\`javascript
|
|
1353
|
+
mp.leave() // Leave room
|
|
1354
|
+
mp.destroy() // Full cleanup
|
|
1355
|
+
\`\`\`
|
|
1356
|
+
|
|
1357
|
+
---
|
|
1358
|
+
|
|
1359
|
+
**Tips:**
|
|
1360
|
+
|
|
1361
|
+
1. **Use \`hostTick()\`** - It handles host detection and throttling. Don't use \`setInterval\`.
|
|
1362
|
+
|
|
1363
|
+
2. **\`input()\` works on host** - Host's \`input()\` calls their \`onInput\` handler directly. Same code for everyone.
|
|
1364
|
+
|
|
1365
|
+
3. **Handle host migration** - If host leaves, \`onHost(true)\` fires on new host. They should start running physics.
|
|
1366
|
+
|
|
1367
|
+
4. **State design** - Only sync what changes: positions, scores, game objects. Not constants or textures.
|
|
1368
|
+
|
|
1369
|
+
5. **Auto mode is default** - Players join instantly without friction. Use \`mode: 'private'\` only if you need invite codes.
|
|
1370
|
+
|
|
1371
|
+
6. **Scale with maxPlayers** - Set \`maxPlayers: 2\` for 1v1, \`maxPlayers: 50\` for larger games. Default is 8.
|
|
1372
|
+
|
|
1373
|
+
7. **Built-in prediction** - Your \`onInput\` handler runs immediately for local player input, eliminating input lag. Use \`mp.getInterpolatedState(dt, state)\` for smooth rendering of all players.
|
|
1374
|
+
|
|
1375
|
+
8. **State pattern** - In \`onState\`, update shared objects (ball, score) but don't overwrite local player (keep your prediction). The SDK handles interpolation for other players.
|
|
1376
|
+
|
|
1377
|
+
9. **Player count** - Use \`mp.players.length\` for player count in UI. Don't use \`Object.keys(state.players).length\` - that only counts players who've sent input.
|
|
1378
|
+
|
|
1379
|
+
10. **Use \`mp.event()\` for discrete actions** - \`mp.input()\` is for continuous state (position, aim). Use \`mp.event('shoot', data)\` for discrete actions (shooting, jumping) - SDK guarantees delivery.
|
|
1380
|
+
|
|
1381
|
+
---
|
|
1382
|
+
|
|
1383
|
+
**Where Code Runs (Important!):**
|
|
1384
|
+
|
|
1385
|
+
| Code Type | Where | Why |
|
|
1386
|
+
|-----------|-------|-----|
|
|
1387
|
+
| Physics, collision | Host (\`hostTick\`) | Single source of truth |
|
|
1388
|
+
| Hit detection, damage | Host (\`onInput\`) | Prevents cheating |
|
|
1389
|
+
| State changes (health, score) | Host | Authoritative |
|
|
1390
|
+
| UI updates, messages | **Everyone** (\`onState\` or \`loop\`) | Each player sees their own |
|
|
1391
|
+
| Visual/sound effects | **Everyone** | Local feedback |
|
|
1392
|
+
| Input sending | **Everyone** (\`mp.input()\`) | All players send inputs |
|
|
1393
|
+
|
|
1394
|
+
**Golden rule:**
|
|
1395
|
+
- **CHANGES state** \u2192 host only
|
|
1396
|
+
- **REACTS to state** \u2192 everyone locally
|
|
1397
|
+
|
|
1398
|
+
\`\`\`javascript
|
|
1399
|
+
// \u274C WRONG - UI in host-only code
|
|
1400
|
+
mp.hostTick(dt, () => {
|
|
1401
|
+
if (player.health <= 0) showDeathMessage(); // Only host sees!
|
|
1402
|
+
return state;
|
|
1403
|
+
});
|
|
1404
|
+
|
|
1405
|
+
// \u2705 RIGHT - React to state on every client
|
|
1406
|
+
mp.onState((state) => {
|
|
1407
|
+
const wasAlive = localPlayer.health > 0;
|
|
1408
|
+
localPlayer.health = state.players[mp.localPlayerId].health;
|
|
1409
|
+
if (wasAlive && localPlayer.health <= 0) {
|
|
1410
|
+
showDeathMessage(); // Everyone sees their own death!
|
|
1411
|
+
}
|
|
1412
|
+
});
|
|
1413
|
+
\`\`\`
|
|
1414
|
+
|
|
1415
|
+
---
|
|
1416
|
+
|
|
1417
|
+
**Common Patterns:**
|
|
1418
|
+
|
|
1419
|
+
### Player Initialization (Important!)
|
|
1420
|
+
|
|
1421
|
+
Initialize player state when they join, not when they first send input:
|
|
1422
|
+
|
|
1423
|
+
\`\`\`javascript
|
|
1424
|
+
// \u274C WRONG - Players invisible until they move
|
|
1425
|
+
mp.onInput((playerId, input) => {
|
|
1426
|
+
if (!state.players[playerId]) {
|
|
1427
|
+
state.players[playerId] = { x: 0, y: 0, health: 100 }; // Too late!
|
|
1428
|
+
}
|
|
1429
|
+
});
|
|
1430
|
+
|
|
1431
|
+
// \u2705 RIGHT - Initialize on join
|
|
1432
|
+
mp.onPlayerJoin((player) => {
|
|
1433
|
+
state.players[player.id] = {
|
|
1434
|
+
x: Math.random() * width,
|
|
1435
|
+
y: Math.random() * height,
|
|
1436
|
+
health: 100
|
|
1437
|
+
};
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
mp.onPlayerLeave((player) => {
|
|
1441
|
+
delete state.players[player.id];
|
|
1442
|
+
});
|
|
1443
|
+
\`\`\`
|
|
1444
|
+
|
|
1445
|
+
**Why this matters:** Without \`onPlayerJoin\`, a spectating player (not sending input) would be invisible to everyone.
|
|
1446
|
+
|
|
1447
|
+
### Position Sync (3D/Complex Games)
|
|
1448
|
+
|
|
1449
|
+
Clients send position, host tracks all players:
|
|
1450
|
+
|
|
1451
|
+
\`\`\`javascript
|
|
1452
|
+
// Everyone: Send position every frame
|
|
1453
|
+
mp.input({
|
|
1454
|
+
pos: [localPlayer.x, localPlayer.y, localPlayer.z],
|
|
1455
|
+
yaw: localPlayer.yaw
|
|
1456
|
+
});
|
|
1457
|
+
|
|
1458
|
+
// Host: Track in onInput
|
|
1459
|
+
const remotePlayers = {};
|
|
1460
|
+
mp.onInput((playerId, input) => {
|
|
1461
|
+
if (!remotePlayers[playerId]) remotePlayers[playerId] = { health: 100 };
|
|
1462
|
+
if (input.pos) {
|
|
1463
|
+
remotePlayers[playerId].pos = input.pos;
|
|
1464
|
+
remotePlayers[playerId].yaw = input.yaw;
|
|
1465
|
+
}
|
|
1466
|
+
});
|
|
1467
|
+
\`\`\`
|
|
1468
|
+
|
|
1469
|
+
### Actions (Shooting, Jumping, Items) - Use \`mp.event()\`
|
|
1470
|
+
|
|
1471
|
+
**For discrete actions, use \`mp.event()\` instead of \`mp.input()\`.** The SDK guarantees delivery, deduplication, and preserves each event's data.
|
|
1472
|
+
|
|
1473
|
+
**The Pattern:**
|
|
1474
|
+
|
|
1475
|
+
\`\`\`javascript
|
|
1476
|
+
// \u274C WRONG - mp.input() loses rapid discrete actions (30Hz throttling)
|
|
1477
|
+
mp.input({ shoot: true }); // 3 rapid clicks = 1 shot!
|
|
1478
|
+
|
|
1479
|
+
// \u2705 RIGHT - Two parts: cosmetic (instant) + authoritative (host)
|
|
1480
|
+
|
|
1481
|
+
// PART 1: At the trigger point (EVERYONE runs this)
|
|
1482
|
+
if (shooting && canShoot(localPlayer)) {
|
|
1483
|
+
// Cosmetic feedback - instant, local only
|
|
1484
|
+
audio.play('shoot');
|
|
1485
|
+
particles.muzzleFlash(localPlayer.pos);
|
|
1486
|
+
|
|
1487
|
+
// Send event to host for authoritative processing
|
|
1488
|
+
mp.event('shoot', {
|
|
1489
|
+
origin: [x, y, z],
|
|
1490
|
+
dir: [dx, dy, dz]
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// PART 2: Authoritative handler (HOST ONLY processes this)
|
|
1495
|
+
mp.onEvent((playerId, type, data) => {
|
|
1496
|
+
if (type === 'shoot') {
|
|
1497
|
+
// This creates the REAL bullet in game state
|
|
1498
|
+
state.bullets.push({
|
|
1499
|
+
owner: playerId,
|
|
1500
|
+
pos: data.origin,
|
|
1501
|
+
dir: data.dir
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
});
|
|
1505
|
+
\`\`\`
|
|
1506
|
+
|
|
1507
|
+
**Why two parts?**
|
|
1508
|
+
|
|
1509
|
+
| Part | Who runs it | What it does |
|
|
1510
|
+
|------|-------------|--------------|
|
|
1511
|
+
| Cosmetic (at call site) | Everyone | Instant feedback: sound, particles, animations |
|
|
1512
|
+
| Authoritative (\`onEvent\`) | Host only | State changes: spawn bullets, apply damage |
|
|
1513
|
+
|
|
1514
|
+
**This separation is intentional:**
|
|
1515
|
+
- **Cosmetic:** Your own shots feel instant (no network latency)
|
|
1516
|
+
- **Authoritative:** Game state is consistent (host is source of truth)
|
|
1517
|
+
- **Other players' bullets:** Appear in state broadcast, ~50ms later (acceptable for casual games)
|
|
1518
|
+
|
|
1519
|
+
**Hearing other players' shots (optional):**
|
|
1520
|
+
\`\`\`javascript
|
|
1521
|
+
const seenBullets = new Set();
|
|
1522
|
+
mp.onState((state) => {
|
|
1523
|
+
for (const bullet of state.bullets || []) {
|
|
1524
|
+
if (!seenBullets.has(bullet.id) && bullet.owner !== mp.localPlayerId) {
|
|
1525
|
+
audio.play('shoot'); // Hear remote player's shot
|
|
1526
|
+
seenBullets.add(bullet.id);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
});
|
|
1530
|
+
\`\`\`
|
|
1531
|
+
|
|
1532
|
+
**When to use which:**
|
|
1533
|
+
| Action Type | Method | Example |
|
|
1534
|
+
|-------------|--------|---------|
|
|
1535
|
+
| Continuous state | \`mp.input()\` | Position, aim direction, movement |
|
|
1536
|
+
| Discrete action | \`mp.event()\` | Shooting, jumping, using items, emotes |
|
|
1537
|
+
|
|
1538
|
+
### Health (Host-Authoritative)
|
|
1539
|
+
|
|
1540
|
+
Host owns health. Clients accept and react:
|
|
1541
|
+
|
|
1542
|
+
\`\`\`javascript
|
|
1543
|
+
// \u274C WRONG - Client sends health (cheatable!)
|
|
1544
|
+
mp.input({ health: localPlayer.health });
|
|
1545
|
+
|
|
1546
|
+
// \u2705 RIGHT - Host applies damage
|
|
1547
|
+
mp.onInput((playerId, input) => {
|
|
1548
|
+
if (hit.playerId) remotePlayers[hit.playerId].health -= 25;
|
|
1549
|
+
});
|
|
1550
|
+
|
|
1551
|
+
// \u2705 RIGHT - Client accepts AND reacts
|
|
1552
|
+
mp.onState((state) => {
|
|
1553
|
+
const myHealth = state.players[mp.localPlayerId]?.health;
|
|
1554
|
+
if (myHealth !== undefined) {
|
|
1555
|
+
if (localPlayer.health > 0 && myHealth <= 0) showDeathMessage();
|
|
1556
|
+
localPlayer.health = myHealth;
|
|
1557
|
+
}
|
|
1558
|
+
});
|
|
1559
|
+
\`\`\`
|
|
1560
|
+
|
|
1561
|
+
### Reacting to Player State Changes (Death, Score, etc.)
|
|
1562
|
+
|
|
1563
|
+
Use \`onLocalPlayerUpdate\` to react when your player's state changes - the SDK tracks previous state for you:
|
|
1564
|
+
|
|
1565
|
+
\`\`\`javascript
|
|
1566
|
+
mp.onLocalPlayerUpdate((player, prevPlayer) => {
|
|
1567
|
+
// Detect death
|
|
1568
|
+
if (prevPlayer.health > 0 && player.health <= 0) {
|
|
1569
|
+
showDeathScreen();
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
// Detect respawn
|
|
1573
|
+
if (prevPlayer.health <= 0 && player.health > 0) {
|
|
1574
|
+
hideDeathScreen();
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// Detect score increase
|
|
1578
|
+
if (player.score > (prevPlayer.score || 0)) {
|
|
1579
|
+
showScorePopup(player.score - prevPlayer.score);
|
|
1580
|
+
}
|
|
1581
|
+
});
|
|
1582
|
+
\`\`\`
|
|
1583
|
+
|
|
1584
|
+
For remote player effects (death animations, etc.):
|
|
1585
|
+
|
|
1586
|
+
\`\`\`javascript
|
|
1587
|
+
// Subscribe to each remote player's updates
|
|
1588
|
+
for (const p of mp.players) {
|
|
1589
|
+
if (p.id !== mp.localPlayerId) {
|
|
1590
|
+
mp.onPlayerUpdate(p.id, (player, prev) => {
|
|
1591
|
+
if (prev.health > 0 && player.health <= 0) {
|
|
1592
|
+
playDeathAnimation(p.id);
|
|
1593
|
+
}
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
\`\`\`
|
|
1598
|
+
|
|
1599
|
+
---
|
|
1600
|
+
|
|
1601
|
+
**Anti-Patterns:**
|
|
1602
|
+
|
|
1603
|
+
### Using \`mp.input()\` for Discrete Actions
|
|
1604
|
+
|
|
1605
|
+
**Problem**: Using \`mp.input({ shoot: true })\` for shooting, jumping, or any discrete action.
|
|
1606
|
+
|
|
1607
|
+
**Why it breaks**: \`mp.input()\` is throttled to 30Hz. Multiple rapid clicks collapse into a single value, losing inputs.
|
|
1608
|
+
|
|
1609
|
+
\`\`\`javascript
|
|
1610
|
+
// \u274C BAD - mp.input() loses rapid discrete actions
|
|
1611
|
+
mp.input({ shoot: true }); // 3 rapid clicks = 1 shot!
|
|
1612
|
+
|
|
1613
|
+
// \u274C ALSO BAD - counters in mp.input() still have issues
|
|
1614
|
+
mp.input({ shootCount: count }); // Bursts all shots at once, loses timing
|
|
1615
|
+
|
|
1616
|
+
// \u2705 GOOD - Use mp.event() for discrete actions
|
|
1617
|
+
mp.event('shoot', { origin, dir }); // Each event preserved with its data
|
|
1618
|
+
\`\`\`
|
|
1619
|
+
|
|
1620
|
+
**The rule is simple:**
|
|
1621
|
+
|
|
1622
|
+
| Action Type | Method | Why |
|
|
1623
|
+
|-------------|--------|-----|
|
|
1624
|
+
| **Continuous** (position, aim) | \`mp.input()\` | Last value is correct, throttling is fine |
|
|
1625
|
+
| **Discrete** (shoot, jump, use item) | \`mp.event()\` | Each action must be delivered with its data |`;
|
|
1626
|
+
|
|
1627
|
+
// src/cli.ts
|
|
6
1628
|
var VERSION = "0.1.0";
|
|
7
1629
|
var API_BASE = process.env.STAR_API_BASE || "https://buildwithstar.com";
|
|
8
1630
|
var CONFIG_FILE = ".starrc";
|
|
@@ -34,10 +1656,13 @@ ${colors.bright}Star SDK CLI${colors.reset} v${VERSION}
|
|
|
34
1656
|
|
|
35
1657
|
${colors.dim}Commands:${colors.reset}
|
|
36
1658
|
${colors.cyan}init <name>${colors.reset} Register a new game
|
|
1659
|
+
${colors.cyan}install${colors.reset} Install Star SDK skill for Claude Code
|
|
1660
|
+
${colors.cyan}docs [topic]${colors.reset} Print API documentation (topics: audio, canvas, leaderboard, multiplayer)
|
|
37
1661
|
${colors.cyan}whoami${colors.reset} Show current configuration
|
|
38
1662
|
|
|
39
1663
|
${colors.dim}Options:${colors.reset}
|
|
40
1664
|
--email <email> Email for higher rate limits (optional)
|
|
1665
|
+
--project Install skill to project instead of personal (~/.claude)
|
|
41
1666
|
--help Show this help message
|
|
42
1667
|
--version Show version
|
|
43
1668
|
|
|
@@ -45,11 +1670,12 @@ ${colors.dim}Examples:${colors.reset}
|
|
|
45
1670
|
${colors.dim}# Register a new game${colors.reset}
|
|
46
1671
|
npx star-sdk init "My Awesome Game"
|
|
47
1672
|
|
|
48
|
-
${colors.dim}#
|
|
49
|
-
npx star-sdk
|
|
1673
|
+
${colors.dim}# Install SDK skill for Claude Code${colors.reset}
|
|
1674
|
+
npx star-sdk install
|
|
50
1675
|
|
|
51
|
-
${colors.dim}#
|
|
52
|
-
|
|
1676
|
+
${colors.dim}# Print API documentation${colors.reset}
|
|
1677
|
+
npx star-sdk docs
|
|
1678
|
+
npx star-sdk docs audio
|
|
53
1679
|
|
|
54
1680
|
${colors.dim}Learn more:${colors.reset} https://buildwithstar.com/docs/sdk
|
|
55
1681
|
`);
|
|
@@ -130,6 +1756,14 @@ async function initCommand(name, email) {
|
|
|
130
1756
|
log(` ${colors.cyan}import Star from 'star-sdk';${colors.reset}`);
|
|
131
1757
|
log(` ${colors.cyan}Star.leaderboard.submit(1500);${colors.reset}`);
|
|
132
1758
|
log("");
|
|
1759
|
+
if (process.env.CLAUDE_SESSION_ID) {
|
|
1760
|
+
info(`Claude Code detected! Run ${colors.cyan}npx star-sdk install${colors.reset} to enable auto-discovery.`);
|
|
1761
|
+
log("");
|
|
1762
|
+
}
|
|
1763
|
+
log(`${colors.dim}Documentation:${colors.reset}`);
|
|
1764
|
+
log(` ${colors.dim}Full docs:${colors.reset} ${colors.cyan}https://buildwithstar.com/docs/sdk${colors.reset}`);
|
|
1765
|
+
log(` ${colors.dim}CLI docs:${colors.reset} ${colors.cyan}npx star-sdk docs${colors.reset}`);
|
|
1766
|
+
log("");
|
|
133
1767
|
} catch (err) {
|
|
134
1768
|
if (err.code === "ECONNREFUSED" || err.code === "ENOTFOUND") {
|
|
135
1769
|
error("Could not connect to Star API. Check your internet connection.");
|
|
@@ -160,6 +1794,50 @@ function whoamiCommand() {
|
|
|
160
1794
|
}
|
|
161
1795
|
log("");
|
|
162
1796
|
}
|
|
1797
|
+
function installCommand(scope = "personal") {
|
|
1798
|
+
const skillDir = scope === "personal" ? path.join(os.homedir(), ".claude", "skills", "star-sdk") : path.join(process.cwd(), ".claude", "skills", "star-sdk");
|
|
1799
|
+
try {
|
|
1800
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
1801
|
+
fs.writeFileSync(path.join(skillDir, "SKILL.md"), SKILL_CONTENT);
|
|
1802
|
+
fs.writeFileSync(path.join(skillDir, "audio.md"), AUDIO_DOCS);
|
|
1803
|
+
fs.writeFileSync(path.join(skillDir, "canvas.md"), CANVAS_DOCS);
|
|
1804
|
+
fs.writeFileSync(path.join(skillDir, "leaderboard.md"), LEADERBOARD_DOCS);
|
|
1805
|
+
fs.writeFileSync(path.join(skillDir, "multiplayer.md"), MULTIPLAYER_DOCS);
|
|
1806
|
+
log("");
|
|
1807
|
+
success(`Star SDK skill installed to ${skillDir}`);
|
|
1808
|
+
log("");
|
|
1809
|
+
log(`${colors.dim}Claude Code will now auto-discover the Star SDK when you work on games.${colors.reset}`);
|
|
1810
|
+
log("");
|
|
1811
|
+
log(`${colors.dim}Installed files:${colors.reset}`);
|
|
1812
|
+
log(` SKILL.md ${colors.dim}Main skill entry point${colors.reset}`);
|
|
1813
|
+
log(` audio.md ${colors.dim}Star.audio API docs${colors.reset}`);
|
|
1814
|
+
log(` canvas.md ${colors.dim}Star.game() API docs${colors.reset}`);
|
|
1815
|
+
log(` leaderboard.md ${colors.dim}Star.leaderboard API docs${colors.reset}`);
|
|
1816
|
+
log(` multiplayer.md ${colors.dim}Star.multiplayer API docs${colors.reset}`);
|
|
1817
|
+
log("");
|
|
1818
|
+
} catch (err) {
|
|
1819
|
+
error(`Failed to install skill: ${err.message}`);
|
|
1820
|
+
process.exit(1);
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
function docsCommand(topic) {
|
|
1824
|
+
const docs = {
|
|
1825
|
+
audio: AUDIO_DOCS,
|
|
1826
|
+
canvas: CANVAS_DOCS,
|
|
1827
|
+
leaderboard: LEADERBOARD_DOCS,
|
|
1828
|
+
multiplayer: MULTIPLAYER_DOCS
|
|
1829
|
+
};
|
|
1830
|
+
if (!topic) {
|
|
1831
|
+
console.log(SKILL_CONTENT);
|
|
1832
|
+
} else if (docs[topic]) {
|
|
1833
|
+
console.log(docs[topic]);
|
|
1834
|
+
} else {
|
|
1835
|
+
error(`Unknown topic: ${topic}`);
|
|
1836
|
+
log("");
|
|
1837
|
+
log("Available topics: audio, canvas, leaderboard, multiplayer");
|
|
1838
|
+
process.exit(1);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
163
1841
|
function parseArgs(args) {
|
|
164
1842
|
const flags = {};
|
|
165
1843
|
const positional = [];
|
|
@@ -199,6 +1877,14 @@ async function main() {
|
|
|
199
1877
|
case "init":
|
|
200
1878
|
await initCommand(positional[0], email);
|
|
201
1879
|
break;
|
|
1880
|
+
case "install":
|
|
1881
|
+
installCommand(flags.project ? "project" : "personal");
|
|
1882
|
+
break;
|
|
1883
|
+
case "docs":
|
|
1884
|
+
case "skill":
|
|
1885
|
+
case "prompt":
|
|
1886
|
+
docsCommand(positional[0]);
|
|
1887
|
+
break;
|
|
202
1888
|
case "whoami":
|
|
203
1889
|
whoamiCommand();
|
|
204
1890
|
break;
|