star-sdk-cli 0.1.5 → 0.1.7
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 +7 -467
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -8,12 +8,12 @@ import * as os from "os";
|
|
|
8
8
|
// src/skills.ts
|
|
9
9
|
var SKILL_CONTENT = `---
|
|
10
10
|
name: star-sdk
|
|
11
|
-
description: Use when you need a game that works on mobile without audio issues, has leaderboards without building a backend, or needs
|
|
11
|
+
description: Use when you need a game that works on mobile without audio issues, has leaderboards without building a backend, or needs canvas rendering without DPR/sizing bugs. Handles iOS Safari audio unlocking, DPR scaling, and coordinate conversion automatically.
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
# Star SDK
|
|
15
15
|
|
|
16
|
-
Browser game development SDK with audio, canvas,
|
|
16
|
+
Browser game development SDK with audio, canvas, and leaderboards.
|
|
17
17
|
|
|
18
18
|
## Import
|
|
19
19
|
|
|
@@ -30,7 +30,6 @@ import Star from 'star-sdk';
|
|
|
30
30
|
| \`Star.game()\` | Game loop, canvas, UI, input | [canvas.md](./canvas.md) |
|
|
31
31
|
| \`Star.audio\` | Sound effects, music | [audio.md](./audio.md) |
|
|
32
32
|
| \`Star.leaderboard\` | Scores, rankings | [leaderboard.md](./leaderboard.md) |
|
|
33
|
-
| \`Star.multiplayer\` | Real-time multiplayer | [multiplayer.md](./multiplayer.md) |
|
|
34
33
|
|
|
35
34
|
## Quick Start
|
|
36
35
|
|
|
@@ -1271,456 +1270,6 @@ const data = await leaderboard.getScores({
|
|
|
1271
1270
|
4. **Don't store leaderboard state** - Just call the SDK methods when needed. The platform handles caching.
|
|
1272
1271
|
|
|
1273
1272
|
5. **Works for guests** - Guests get a generated name like "Guest1234". They can sign in later to claim scores.`;
|
|
1274
|
-
var MULTIPLAYER_DOCS = `**Installation**
|
|
1275
|
-
|
|
1276
|
-
\`\`\`bash
|
|
1277
|
-
yarn add star-multiplayer
|
|
1278
|
-
\`\`\`
|
|
1279
|
-
|
|
1280
|
-
### Star Multiplayer SDK
|
|
1281
|
-
|
|
1282
|
-
**Real-time multiplayer for Star games.** Host-authoritative state sync with automatic room management.
|
|
1283
|
-
|
|
1284
|
-
**Import:**
|
|
1285
|
-
\`\`\`javascript
|
|
1286
|
-
import { createMultiplayer } from 'star-multiplayer';
|
|
1287
|
-
const mp = createMultiplayer();
|
|
1288
|
-
\`\`\`
|
|
1289
|
-
|
|
1290
|
-
**CRITICAL:** Import in JavaScript - don't add \`<script>\` tags.
|
|
1291
|
-
|
|
1292
|
-
**Features:**
|
|
1293
|
-
- **Host-authoritative** - One player runs game logic, others receive state
|
|
1294
|
-
- **Room-based** - Create/join rooms with 6-character invite codes
|
|
1295
|
-
- **Auto host migration** - If host leaves, another player becomes host
|
|
1296
|
-
- **Built-in lobby UI** - No need to build your own
|
|
1297
|
-
- **Works in iframes** - Uses postMessage relay to parent window
|
|
1298
|
-
|
|
1299
|
-
---
|
|
1300
|
-
|
|
1301
|
-
**Quick Start:**
|
|
1302
|
-
|
|
1303
|
-
\`\`\`javascript
|
|
1304
|
-
import { createMultiplayer } from 'star-multiplayer';
|
|
1305
|
-
import { game } from 'star-canvas';
|
|
1306
|
-
|
|
1307
|
-
const mp = createMultiplayer();
|
|
1308
|
-
|
|
1309
|
-
game(async ({ ctx, width, height, loop, canvas }) => {
|
|
1310
|
-
// 1. Start - auto-finds or creates room, starts immediately
|
|
1311
|
-
await mp.start();
|
|
1312
|
-
|
|
1313
|
-
// 2. Game state
|
|
1314
|
-
let state = { players: {}, ball: { x: width/2, y: height/2, vx: 200, vy: 150 } };
|
|
1315
|
-
|
|
1316
|
-
// 3. Receive state updates - update shared objects, keep local player prediction
|
|
1317
|
-
mp.onState((s) => {
|
|
1318
|
-
state.ball = s.ball;
|
|
1319
|
-
for (const [id, p] of Object.entries(s.players)) {
|
|
1320
|
-
if (id !== mp.localPlayerId) {
|
|
1321
|
-
state.players[id] = p;
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
|
-
});
|
|
1325
|
-
|
|
1326
|
-
// 4. Handle inputs (runs locally for prediction, on host for authoritative state)
|
|
1327
|
-
mp.onInput((playerId, input) => {
|
|
1328
|
-
if (!state.players[playerId]) {
|
|
1329
|
-
state.players[playerId] = { y: height/2 };
|
|
1330
|
-
}
|
|
1331
|
-
state.players[playerId].y = input.y;
|
|
1332
|
-
});
|
|
1333
|
-
|
|
1334
|
-
// 5. Game loop
|
|
1335
|
-
loop((dt) => {
|
|
1336
|
-
// Host: run physics + broadcast state (auto-throttled to 20Hz)
|
|
1337
|
-
mp.hostTick(dt, () => {
|
|
1338
|
-
state.ball.x += state.ball.vx * dt;
|
|
1339
|
-
state.ball.y += state.ball.vy * dt;
|
|
1340
|
-
if (state.ball.y < 0 || state.ball.y > height) state.ball.vy *= -1;
|
|
1341
|
-
return state;
|
|
1342
|
-
});
|
|
1343
|
-
|
|
1344
|
-
// Everyone: render with interpolation for smooth movement
|
|
1345
|
-
const renderState = mp.getInterpolatedState(dt, state);
|
|
1346
|
-
ctx.fillStyle = '#0f172a';
|
|
1347
|
-
ctx.fillRect(0, 0, width, height);
|
|
1348
|
-
for (const p of Object.values(renderState.players)) {
|
|
1349
|
-
ctx.fillStyle = '#22d3ee';
|
|
1350
|
-
ctx.fillRect(20, p.y - 40, 10, 80);
|
|
1351
|
-
}
|
|
1352
|
-
ctx.beginPath();
|
|
1353
|
-
ctx.arc(renderState.ball.x, renderState.ball.y, 10, 0, Math.PI * 2);
|
|
1354
|
-
ctx.fill();
|
|
1355
|
-
});
|
|
1356
|
-
|
|
1357
|
-
// 6. Input - works for EVERYONE (applied locally for instant feedback)
|
|
1358
|
-
canvas.onpointermove = (e) => {
|
|
1359
|
-
const rect = canvas.getBoundingClientRect();
|
|
1360
|
-
mp.input({ y: ((e.clientY - rect.top) / rect.height) * height });
|
|
1361
|
-
};
|
|
1362
|
-
});
|
|
1363
|
-
\`\`\`
|
|
1364
|
-
|
|
1365
|
-
**That's it!** The SDK handles:
|
|
1366
|
-
- Auto-matchmaking (finds open room or creates one)
|
|
1367
|
-
- URL detection (\`?room=CODE\` \u2192 joins specific room)
|
|
1368
|
-
- Throttling state broadcasts to 20Hz
|
|
1369
|
-
- Routing host's inputs to their own handler
|
|
1370
|
-
|
|
1371
|
-
---
|
|
1372
|
-
|
|
1373
|
-
**Architecture:**
|
|
1374
|
-
|
|
1375
|
-
\`\`\`
|
|
1376
|
-
Host (Player 1) Server Client (Player 2)
|
|
1377
|
-
\u2502 \u2502 \u2502
|
|
1378
|
-
\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
|
|
1379
|
-
\u2502 \u2502 \u2502
|
|
1380
|
-
\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
|
|
1381
|
-
\u2502 \u2502 \u2502
|
|
1382
|
-
\u2502 (runs game logic) \u2502 (dumb relay) \u2502 (renders state)
|
|
1383
|
-
\`\`\`
|
|
1384
|
-
|
|
1385
|
-
---
|
|
1386
|
-
|
|
1387
|
-
**API Reference:**
|
|
1388
|
-
|
|
1389
|
-
**State (read-only):**
|
|
1390
|
-
\`\`\`javascript
|
|
1391
|
-
mp.isHost // true if you're the host
|
|
1392
|
-
mp.isReady // true if game has started
|
|
1393
|
-
mp.players // Array of { id: string }
|
|
1394
|
-
mp.localPlayerId // Your player ID
|
|
1395
|
-
mp.roomCode // Current room code
|
|
1396
|
-
\`\`\`
|
|
1397
|
-
|
|
1398
|
-
**Setup:**
|
|
1399
|
-
\`\`\`javascript
|
|
1400
|
-
await mp.start(); // Uses all defaults - auto-matchmaking, 8 players
|
|
1401
|
-
|
|
1402
|
-
// Or customize:
|
|
1403
|
-
await mp.start({
|
|
1404
|
-
maxPlayers: 8, // default: 8 (max players per room)
|
|
1405
|
-
mode: 'auto', // default: 'auto' - finds/creates rooms automatically
|
|
1406
|
-
// 'private' - shows lobby with shareable code
|
|
1407
|
-
showLobby: true, // default: true (only used in 'private' mode)
|
|
1408
|
-
tickRate: 50, // default: 50ms (20Hz state broadcasts)
|
|
1409
|
-
prediction: true // default: true (client-side prediction for instant input)
|
|
1410
|
-
});
|
|
1411
|
-
|
|
1412
|
-
// Examples:
|
|
1413
|
-
await mp.start({ maxPlayers: 2 }); // 1v1 game
|
|
1414
|
-
await mp.start({ maxPlayers: 100 }); // Battle royale
|
|
1415
|
-
await mp.start({ mode: 'private' }); // Friends-only with invite code
|
|
1416
|
-
await mp.start({ mode: 'private', maxPlayers: 4 }); // Small private game
|
|
1417
|
-
\`\`\`
|
|
1418
|
-
|
|
1419
|
-
**Sync:**
|
|
1420
|
-
\`\`\`javascript
|
|
1421
|
-
mp.onState((state) => { ... }) // Receive state (returns unsubscribe)
|
|
1422
|
-
mp.onInput((playerId, input) => { ... }) // Handle continuous input (position, aim)
|
|
1423
|
-
mp.hostTick(dt, () => state) // Run on host, broadcast returned state
|
|
1424
|
-
mp.input(data) // Send continuous input (position, aim)
|
|
1425
|
-
mp.getInterpolatedState(dt, state) // Get smoothly interpolated state for rendering
|
|
1426
|
-
\`\`\`
|
|
1427
|
-
|
|
1428
|
-
**Discrete Events (shooting, jumping, using items):**
|
|
1429
|
-
\`\`\`javascript
|
|
1430
|
-
mp.event(type, data) // Send event - guaranteed delivery
|
|
1431
|
-
mp.onEvent((playerId, type, data) => { }) // Handle events (HOST ONLY, authoritative)
|
|
1432
|
-
\`\`\`
|
|
1433
|
-
|
|
1434
|
-
**Player Updates:**
|
|
1435
|
-
\`\`\`javascript
|
|
1436
|
-
mp.onLocalPlayerUpdate((player, prevPlayer) => { ... }) // Local player changed
|
|
1437
|
-
mp.onPlayerUpdate(playerId, (player, prevPlayer) => { ... }) // Specific player changed
|
|
1438
|
-
\`\`\`
|
|
1439
|
-
|
|
1440
|
-
**Lifecycle Events:**
|
|
1441
|
-
\`\`\`javascript
|
|
1442
|
-
mp.onPlayers((players) => { ... }) // Player list changed
|
|
1443
|
-
mp.onPlayerJoin((player) => { ... }) // Another player joined (use to init their state)
|
|
1444
|
-
mp.onPlayerLeave((player) => { ... }) // A player left
|
|
1445
|
-
mp.onHost((isHost) => { ... }) // Host status changed (for host migration)
|
|
1446
|
-
mp.onError((message) => { ... }) // Error occurred
|
|
1447
|
-
\`\`\`
|
|
1448
|
-
|
|
1449
|
-
**Cleanup:**
|
|
1450
|
-
\`\`\`javascript
|
|
1451
|
-
mp.leave() // Leave room
|
|
1452
|
-
mp.destroy() // Full cleanup
|
|
1453
|
-
\`\`\`
|
|
1454
|
-
|
|
1455
|
-
---
|
|
1456
|
-
|
|
1457
|
-
**Tips:**
|
|
1458
|
-
|
|
1459
|
-
1. **Use \`hostTick()\`** - It handles host detection and throttling. Don't use \`setInterval\`.
|
|
1460
|
-
|
|
1461
|
-
2. **\`input()\` works on host** - Host's \`input()\` calls their \`onInput\` handler directly. Same code for everyone.
|
|
1462
|
-
|
|
1463
|
-
3. **Handle host migration** - If host leaves, \`onHost(true)\` fires on new host. They should start running physics.
|
|
1464
|
-
|
|
1465
|
-
4. **State design** - Only sync what changes: positions, scores, game objects. Not constants or textures.
|
|
1466
|
-
|
|
1467
|
-
5. **Auto mode is default** - Players join instantly without friction. Use \`mode: 'private'\` only if you need invite codes.
|
|
1468
|
-
|
|
1469
|
-
6. **Scale with maxPlayers** - Set \`maxPlayers: 2\` for 1v1, \`maxPlayers: 50\` for larger games. Default is 8.
|
|
1470
|
-
|
|
1471
|
-
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.
|
|
1472
|
-
|
|
1473
|
-
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.
|
|
1474
|
-
|
|
1475
|
-
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.
|
|
1476
|
-
|
|
1477
|
-
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.
|
|
1478
|
-
|
|
1479
|
-
---
|
|
1480
|
-
|
|
1481
|
-
**Where Code Runs (Important!):**
|
|
1482
|
-
|
|
1483
|
-
| Code Type | Where | Why |
|
|
1484
|
-
|-----------|-------|-----|
|
|
1485
|
-
| Physics, collision | Host (\`hostTick\`) | Single source of truth |
|
|
1486
|
-
| Hit detection, damage | Host (\`onInput\`) | Prevents cheating |
|
|
1487
|
-
| State changes (health, score) | Host | Authoritative |
|
|
1488
|
-
| UI updates, messages | **Everyone** (\`onState\` or \`loop\`) | Each player sees their own |
|
|
1489
|
-
| Visual/sound effects | **Everyone** | Local feedback |
|
|
1490
|
-
| Input sending | **Everyone** (\`mp.input()\`) | All players send inputs |
|
|
1491
|
-
|
|
1492
|
-
**Golden rule:**
|
|
1493
|
-
- **CHANGES state** \u2192 host only
|
|
1494
|
-
- **REACTS to state** \u2192 everyone locally
|
|
1495
|
-
|
|
1496
|
-
\`\`\`javascript
|
|
1497
|
-
// \u274C WRONG - UI in host-only code
|
|
1498
|
-
mp.hostTick(dt, () => {
|
|
1499
|
-
if (player.health <= 0) showDeathMessage(); // Only host sees!
|
|
1500
|
-
return state;
|
|
1501
|
-
});
|
|
1502
|
-
|
|
1503
|
-
// \u2705 RIGHT - React to state on every client
|
|
1504
|
-
mp.onState((state) => {
|
|
1505
|
-
const wasAlive = localPlayer.health > 0;
|
|
1506
|
-
localPlayer.health = state.players[mp.localPlayerId].health;
|
|
1507
|
-
if (wasAlive && localPlayer.health <= 0) {
|
|
1508
|
-
showDeathMessage(); // Everyone sees their own death!
|
|
1509
|
-
}
|
|
1510
|
-
});
|
|
1511
|
-
\`\`\`
|
|
1512
|
-
|
|
1513
|
-
---
|
|
1514
|
-
|
|
1515
|
-
**Common Patterns:**
|
|
1516
|
-
|
|
1517
|
-
### Player Initialization (Important!)
|
|
1518
|
-
|
|
1519
|
-
Initialize player state when they join, not when they first send input:
|
|
1520
|
-
|
|
1521
|
-
\`\`\`javascript
|
|
1522
|
-
// \u274C WRONG - Players invisible until they move
|
|
1523
|
-
mp.onInput((playerId, input) => {
|
|
1524
|
-
if (!state.players[playerId]) {
|
|
1525
|
-
state.players[playerId] = { x: 0, y: 0, health: 100 }; // Too late!
|
|
1526
|
-
}
|
|
1527
|
-
});
|
|
1528
|
-
|
|
1529
|
-
// \u2705 RIGHT - Initialize on join
|
|
1530
|
-
mp.onPlayerJoin((player) => {
|
|
1531
|
-
state.players[player.id] = {
|
|
1532
|
-
x: Math.random() * width,
|
|
1533
|
-
y: Math.random() * height,
|
|
1534
|
-
health: 100
|
|
1535
|
-
};
|
|
1536
|
-
});
|
|
1537
|
-
|
|
1538
|
-
mp.onPlayerLeave((player) => {
|
|
1539
|
-
delete state.players[player.id];
|
|
1540
|
-
});
|
|
1541
|
-
\`\`\`
|
|
1542
|
-
|
|
1543
|
-
**Why this matters:** Without \`onPlayerJoin\`, a spectating player (not sending input) would be invisible to everyone.
|
|
1544
|
-
|
|
1545
|
-
### Position Sync (3D/Complex Games)
|
|
1546
|
-
|
|
1547
|
-
Clients send position, host tracks all players:
|
|
1548
|
-
|
|
1549
|
-
\`\`\`javascript
|
|
1550
|
-
// Everyone: Send position every frame
|
|
1551
|
-
mp.input({
|
|
1552
|
-
pos: [localPlayer.x, localPlayer.y, localPlayer.z],
|
|
1553
|
-
yaw: localPlayer.yaw
|
|
1554
|
-
});
|
|
1555
|
-
|
|
1556
|
-
// Host: Track in onInput
|
|
1557
|
-
const remotePlayers = {};
|
|
1558
|
-
mp.onInput((playerId, input) => {
|
|
1559
|
-
if (!remotePlayers[playerId]) remotePlayers[playerId] = { health: 100 };
|
|
1560
|
-
if (input.pos) {
|
|
1561
|
-
remotePlayers[playerId].pos = input.pos;
|
|
1562
|
-
remotePlayers[playerId].yaw = input.yaw;
|
|
1563
|
-
}
|
|
1564
|
-
});
|
|
1565
|
-
\`\`\`
|
|
1566
|
-
|
|
1567
|
-
### Actions (Shooting, Jumping, Items) - Use \`mp.event()\`
|
|
1568
|
-
|
|
1569
|
-
**For discrete actions, use \`mp.event()\` instead of \`mp.input()\`.** The SDK guarantees delivery, deduplication, and preserves each event's data.
|
|
1570
|
-
|
|
1571
|
-
**The Pattern:**
|
|
1572
|
-
|
|
1573
|
-
\`\`\`javascript
|
|
1574
|
-
// \u274C WRONG - mp.input() loses rapid discrete actions (30Hz throttling)
|
|
1575
|
-
mp.input({ shoot: true }); // 3 rapid clicks = 1 shot!
|
|
1576
|
-
|
|
1577
|
-
// \u2705 RIGHT - Two parts: cosmetic (instant) + authoritative (host)
|
|
1578
|
-
|
|
1579
|
-
// PART 1: At the trigger point (EVERYONE runs this)
|
|
1580
|
-
if (shooting && canShoot(localPlayer)) {
|
|
1581
|
-
// Cosmetic feedback - instant, local only
|
|
1582
|
-
audio.play('shoot');
|
|
1583
|
-
particles.muzzleFlash(localPlayer.pos);
|
|
1584
|
-
|
|
1585
|
-
// Send event to host for authoritative processing
|
|
1586
|
-
mp.event('shoot', {
|
|
1587
|
-
origin: [x, y, z],
|
|
1588
|
-
dir: [dx, dy, dz]
|
|
1589
|
-
});
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
|
-
// PART 2: Authoritative handler (HOST ONLY processes this)
|
|
1593
|
-
mp.onEvent((playerId, type, data) => {
|
|
1594
|
-
if (type === 'shoot') {
|
|
1595
|
-
// This creates the REAL bullet in game state
|
|
1596
|
-
state.bullets.push({
|
|
1597
|
-
owner: playerId,
|
|
1598
|
-
pos: data.origin,
|
|
1599
|
-
dir: data.dir
|
|
1600
|
-
});
|
|
1601
|
-
}
|
|
1602
|
-
});
|
|
1603
|
-
\`\`\`
|
|
1604
|
-
|
|
1605
|
-
**Why two parts?**
|
|
1606
|
-
|
|
1607
|
-
| Part | Who runs it | What it does |
|
|
1608
|
-
|------|-------------|--------------|
|
|
1609
|
-
| Cosmetic (at call site) | Everyone | Instant feedback: sound, particles, animations |
|
|
1610
|
-
| Authoritative (\`onEvent\`) | Host only | State changes: spawn bullets, apply damage |
|
|
1611
|
-
|
|
1612
|
-
**This separation is intentional:**
|
|
1613
|
-
- **Cosmetic:** Your own shots feel instant (no network latency)
|
|
1614
|
-
- **Authoritative:** Game state is consistent (host is source of truth)
|
|
1615
|
-
- **Other players' bullets:** Appear in state broadcast, ~50ms later (acceptable for casual games)
|
|
1616
|
-
|
|
1617
|
-
**Hearing other players' shots (optional):**
|
|
1618
|
-
\`\`\`javascript
|
|
1619
|
-
const seenBullets = new Set();
|
|
1620
|
-
mp.onState((state) => {
|
|
1621
|
-
for (const bullet of state.bullets || []) {
|
|
1622
|
-
if (!seenBullets.has(bullet.id) && bullet.owner !== mp.localPlayerId) {
|
|
1623
|
-
audio.play('shoot'); // Hear remote player's shot
|
|
1624
|
-
seenBullets.add(bullet.id);
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
});
|
|
1628
|
-
\`\`\`
|
|
1629
|
-
|
|
1630
|
-
**When to use which:**
|
|
1631
|
-
| Action Type | Method | Example |
|
|
1632
|
-
|-------------|--------|---------|
|
|
1633
|
-
| Continuous state | \`mp.input()\` | Position, aim direction, movement |
|
|
1634
|
-
| Discrete action | \`mp.event()\` | Shooting, jumping, using items, emotes |
|
|
1635
|
-
|
|
1636
|
-
### Health (Host-Authoritative)
|
|
1637
|
-
|
|
1638
|
-
Host owns health. Clients accept and react:
|
|
1639
|
-
|
|
1640
|
-
\`\`\`javascript
|
|
1641
|
-
// \u274C WRONG - Client sends health (cheatable!)
|
|
1642
|
-
mp.input({ health: localPlayer.health });
|
|
1643
|
-
|
|
1644
|
-
// \u2705 RIGHT - Host applies damage
|
|
1645
|
-
mp.onInput((playerId, input) => {
|
|
1646
|
-
if (hit.playerId) remotePlayers[hit.playerId].health -= 25;
|
|
1647
|
-
});
|
|
1648
|
-
|
|
1649
|
-
// \u2705 RIGHT - Client accepts AND reacts
|
|
1650
|
-
mp.onState((state) => {
|
|
1651
|
-
const myHealth = state.players[mp.localPlayerId]?.health;
|
|
1652
|
-
if (myHealth !== undefined) {
|
|
1653
|
-
if (localPlayer.health > 0 && myHealth <= 0) showDeathMessage();
|
|
1654
|
-
localPlayer.health = myHealth;
|
|
1655
|
-
}
|
|
1656
|
-
});
|
|
1657
|
-
\`\`\`
|
|
1658
|
-
|
|
1659
|
-
### Reacting to Player State Changes (Death, Score, etc.)
|
|
1660
|
-
|
|
1661
|
-
Use \`onLocalPlayerUpdate\` to react when your player's state changes - the SDK tracks previous state for you:
|
|
1662
|
-
|
|
1663
|
-
\`\`\`javascript
|
|
1664
|
-
mp.onLocalPlayerUpdate((player, prevPlayer) => {
|
|
1665
|
-
// Detect death
|
|
1666
|
-
if (prevPlayer.health > 0 && player.health <= 0) {
|
|
1667
|
-
showDeathScreen();
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
|
-
// Detect respawn
|
|
1671
|
-
if (prevPlayer.health <= 0 && player.health > 0) {
|
|
1672
|
-
hideDeathScreen();
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
// Detect score increase
|
|
1676
|
-
if (player.score > (prevPlayer.score || 0)) {
|
|
1677
|
-
showScorePopup(player.score - prevPlayer.score);
|
|
1678
|
-
}
|
|
1679
|
-
});
|
|
1680
|
-
\`\`\`
|
|
1681
|
-
|
|
1682
|
-
For remote player effects (death animations, etc.):
|
|
1683
|
-
|
|
1684
|
-
\`\`\`javascript
|
|
1685
|
-
// Subscribe to each remote player's updates
|
|
1686
|
-
for (const p of mp.players) {
|
|
1687
|
-
if (p.id !== mp.localPlayerId) {
|
|
1688
|
-
mp.onPlayerUpdate(p.id, (player, prev) => {
|
|
1689
|
-
if (prev.health > 0 && player.health <= 0) {
|
|
1690
|
-
playDeathAnimation(p.id);
|
|
1691
|
-
}
|
|
1692
|
-
});
|
|
1693
|
-
}
|
|
1694
|
-
}
|
|
1695
|
-
\`\`\`
|
|
1696
|
-
|
|
1697
|
-
---
|
|
1698
|
-
|
|
1699
|
-
**Anti-Patterns:**
|
|
1700
|
-
|
|
1701
|
-
### Using \`mp.input()\` for Discrete Actions
|
|
1702
|
-
|
|
1703
|
-
**Problem**: Using \`mp.input({ shoot: true })\` for shooting, jumping, or any discrete action.
|
|
1704
|
-
|
|
1705
|
-
**Why it breaks**: \`mp.input()\` is throttled to 30Hz. Multiple rapid clicks collapse into a single value, losing inputs.
|
|
1706
|
-
|
|
1707
|
-
\`\`\`javascript
|
|
1708
|
-
// \u274C BAD - mp.input() loses rapid discrete actions
|
|
1709
|
-
mp.input({ shoot: true }); // 3 rapid clicks = 1 shot!
|
|
1710
|
-
|
|
1711
|
-
// \u274C ALSO BAD - counters in mp.input() still have issues
|
|
1712
|
-
mp.input({ shootCount: count }); // Bursts all shots at once, loses timing
|
|
1713
|
-
|
|
1714
|
-
// \u2705 GOOD - Use mp.event() for discrete actions
|
|
1715
|
-
mp.event('shoot', { origin, dir }); // Each event preserved with its data
|
|
1716
|
-
\`\`\`
|
|
1717
|
-
|
|
1718
|
-
**The rule is simple:**
|
|
1719
|
-
|
|
1720
|
-
| Action Type | Method | Why |
|
|
1721
|
-
|-------------|--------|-----|
|
|
1722
|
-
| **Continuous** (position, aim) | \`mp.input()\` | Last value is correct, throttling is fine |
|
|
1723
|
-
| **Discrete** (shoot, jump, use item) | \`mp.event()\` | Each action must be delivered with its data |`;
|
|
1724
1273
|
|
|
1725
1274
|
// src/cli.ts
|
|
1726
1275
|
var VERSION = "0.1.0";
|
|
@@ -1909,7 +1458,7 @@ function whoamiCommand() {
|
|
|
1909
1458
|
function transformForStdout(content) {
|
|
1910
1459
|
return content.replace(/\[(\w+)\.md\]\(\.\/\1\.md\)/g, "$1 (`npx star-sdk docs $1`)").replace(/see (\w+)\.md/g, "run `npx star-sdk docs $1`").replace(
|
|
1911
1460
|
/For detailed API documentation, see the linked files above\./,
|
|
1912
|
-
"For detailed API docs: `npx star-sdk docs <topic>` where topic is audio, canvas,
|
|
1461
|
+
"For detailed API docs: `npx star-sdk docs <topic>` where topic is audio, canvas, or leaderboard."
|
|
1913
1462
|
);
|
|
1914
1463
|
}
|
|
1915
1464
|
function getAllDocsContent() {
|
|
@@ -1931,13 +1480,7 @@ ${CANVAS_DOCS}
|
|
|
1931
1480
|
|
|
1932
1481
|
# Leaderboard API
|
|
1933
1482
|
|
|
1934
|
-
${LEADERBOARD_DOCS}
|
|
1935
|
-
|
|
1936
|
-
---
|
|
1937
|
-
|
|
1938
|
-
# Multiplayer API
|
|
1939
|
-
|
|
1940
|
-
${MULTIPLAYER_DOCS}`;
|
|
1483
|
+
${LEADERBOARD_DOCS}`;
|
|
1941
1484
|
}
|
|
1942
1485
|
function installClaudeCode(scope) {
|
|
1943
1486
|
const skillDir = scope === "global" ? path.join(os.homedir(), ".claude", "skills", "star-sdk") : path.join(process.cwd(), ".claude", "skills", "star-sdk");
|
|
@@ -1946,7 +1489,6 @@ function installClaudeCode(scope) {
|
|
|
1946
1489
|
fs.writeFileSync(path.join(skillDir, "audio.md"), AUDIO_DOCS);
|
|
1947
1490
|
fs.writeFileSync(path.join(skillDir, "canvas.md"), CANVAS_DOCS);
|
|
1948
1491
|
fs.writeFileSync(path.join(skillDir, "leaderboard.md"), LEADERBOARD_DOCS);
|
|
1949
|
-
fs.writeFileSync(path.join(skillDir, "multiplayer.md"), MULTIPLAYER_DOCS);
|
|
1950
1492
|
log("");
|
|
1951
1493
|
success(`Star SDK skill installed to ${skillDir}`);
|
|
1952
1494
|
log("");
|
|
@@ -1957,7 +1499,6 @@ function installClaudeCode(scope) {
|
|
|
1957
1499
|
log(` audio.md ${colors.dim}Star.audio API docs${colors.reset}`);
|
|
1958
1500
|
log(` canvas.md ${colors.dim}Star.game() API docs${colors.reset}`);
|
|
1959
1501
|
log(` leaderboard.md ${colors.dim}Star.leaderboard API docs${colors.reset}`);
|
|
1960
|
-
log(` multiplayer.md ${colors.dim}Star.multiplayer API docs${colors.reset}`);
|
|
1961
1502
|
log("");
|
|
1962
1503
|
}
|
|
1963
1504
|
function installCodex(scope) {
|
|
@@ -1988,7 +1529,7 @@ function installCursor() {
|
|
|
1988
1529
|
const rulesDir = path.join(process.cwd(), ".cursor", "rules");
|
|
1989
1530
|
const filePath = path.join(rulesDir, "star-sdk.mdc");
|
|
1990
1531
|
const content = `---
|
|
1991
|
-
description: Star SDK for browser game development with audio, canvas,
|
|
1532
|
+
description: Star SDK for browser game development with audio, canvas, and leaderboards
|
|
1992
1533
|
globs: ["**/*.js", "**/*.ts", "**/*.jsx", "**/*.tsx", "**/*.html"]
|
|
1993
1534
|
alwaysApply: false
|
|
1994
1535
|
---
|
|
@@ -2072,8 +1613,7 @@ function docsCommand(topic) {
|
|
|
2072
1613
|
skill: SKILL_CONTENT,
|
|
2073
1614
|
audio: AUDIO_DOCS,
|
|
2074
1615
|
canvas: CANVAS_DOCS,
|
|
2075
|
-
leaderboard: LEADERBOARD_DOCS
|
|
2076
|
-
multiplayer: MULTIPLAYER_DOCS
|
|
1616
|
+
leaderboard: LEADERBOARD_DOCS
|
|
2077
1617
|
};
|
|
2078
1618
|
if (!topic) {
|
|
2079
1619
|
console.log(transformForStdout(SKILL_CONTENT));
|
|
@@ -2082,7 +1622,7 @@ function docsCommand(topic) {
|
|
|
2082
1622
|
} else {
|
|
2083
1623
|
error(`Unknown topic: ${topic}`);
|
|
2084
1624
|
log("");
|
|
2085
|
-
log("Available topics: skill, audio, canvas, leaderboard
|
|
1625
|
+
log("Available topics: skill, audio, canvas, leaderboard");
|
|
2086
1626
|
process.exit(1);
|
|
2087
1627
|
}
|
|
2088
1628
|
}
|