star-sdk-cli 0.1.4 → 0.1.6
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 +109 -472
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -61,6 +61,17 @@ Star.game(ctx => {
|
|
|
61
61
|
});
|
|
62
62
|
\`\`\`
|
|
63
63
|
|
|
64
|
+
## Initialization (Required for Leaderboards)
|
|
65
|
+
|
|
66
|
+
If using leaderboards outside the Star platform (local dev, self-hosted), initialize with your game ID:
|
|
67
|
+
|
|
68
|
+
\`\`\`javascript
|
|
69
|
+
import Star from 'star-sdk';
|
|
70
|
+
|
|
71
|
+
// Get your gameId from .starrc (created by: npx star-sdk init "Game Name")
|
|
72
|
+
Star.init({ gameId: 'your-game-id-here' });
|
|
73
|
+
\`\`\`
|
|
74
|
+
|
|
64
75
|
## Common Patterns
|
|
65
76
|
|
|
66
77
|
### Game Over -> Submit Score -> Show Leaderboard
|
|
@@ -110,6 +121,9 @@ Only these 17 presets exist:
|
|
|
110
121
|
\`\`\`javascript
|
|
111
122
|
import Star from 'star-sdk';
|
|
112
123
|
|
|
124
|
+
// Initialize for leaderboard support (get gameId from .starrc)
|
|
125
|
+
Star.init({ gameId: 'your-game-id' });
|
|
126
|
+
|
|
113
127
|
Star.game(ctx => {
|
|
114
128
|
const { canvas, width, height, ctx: c } = ctx;
|
|
115
129
|
let score = 0;
|
|
@@ -314,7 +328,7 @@ var CANVAS_DOCS = `**Installation**
|
|
|
314
328
|
First, add the package to your project:
|
|
315
329
|
|
|
316
330
|
\`\`\`bash
|
|
317
|
-
yarn add star-
|
|
331
|
+
yarn add star-canvas
|
|
318
332
|
\`\`\`
|
|
319
333
|
|
|
320
334
|
### Star DOM SDK
|
|
@@ -662,7 +676,7 @@ game(({ ctx, width, height, loop, toStagePoint, canvas }) => {
|
|
|
662
676
|
|
|
663
677
|
\`\`\`ts
|
|
664
678
|
import { game } from 'star-canvas';
|
|
665
|
-
import { createLeaderboard } from '
|
|
679
|
+
import { createLeaderboard } from 'star-leaderboard';
|
|
666
680
|
|
|
667
681
|
const leaderboard = createLeaderboard();
|
|
668
682
|
|
|
@@ -919,7 +933,86 @@ game(({ ctx, width, height, loop, canvas, toStagePoint }) => {
|
|
|
919
933
|
4. \u274C Not clearing state on pointerup \u2192 \`createDrag()\` fixes this
|
|
920
934
|
5. \u274C Missing \`setPointerCapture()\` (drags break outside canvas) \u2192 **You must add this!**
|
|
921
935
|
|
|
922
|
-
**Recommendation:** Use \`createDrag()\` + \`setPointerCapture()\` for bulletproof drag-and-drop
|
|
936
|
+
**Recommendation:** Use \`createDrag()\` + \`setPointerCapture()\` for bulletproof drag-and-drop.
|
|
937
|
+
|
|
938
|
+
### Recipe 9: Image Backgrounds
|
|
939
|
+
|
|
940
|
+
Two patterns for backgrounds: **full-canvas** (unique scenes) or **tileable patterns** (repeating textures).
|
|
941
|
+
|
|
942
|
+
**Full-canvas background (scaled to fit):**
|
|
943
|
+
|
|
944
|
+
\`\`\`ts
|
|
945
|
+
import { game } from 'star-canvas';
|
|
946
|
+
|
|
947
|
+
game(({ ctx, width, height, loop }) => {
|
|
948
|
+
const bg = new Image();
|
|
949
|
+
bg.src = 'https://example.com/background.png'; // Use generated asset URL
|
|
950
|
+
|
|
951
|
+
const player = { x: 320, y: 300 };
|
|
952
|
+
|
|
953
|
+
loop((dt) => {
|
|
954
|
+
// Draw background scaled to canvas (no tiling)
|
|
955
|
+
if (bg.complete) {
|
|
956
|
+
ctx.drawImage(bg, 0, 0, width, height);
|
|
957
|
+
} else {
|
|
958
|
+
ctx.fillStyle = '#1e293b'; // Fallback color while loading
|
|
959
|
+
ctx.fillRect(0, 0, width, height);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// Draw game objects on top
|
|
963
|
+
ctx.fillStyle = '#22d3ee';
|
|
964
|
+
ctx.fillRect(player.x - 16, player.y - 16, 32, 32);
|
|
965
|
+
});
|
|
966
|
+
});
|
|
967
|
+
\`\`\`
|
|
968
|
+
|
|
969
|
+
**Tileable pattern background (repeating texture):**
|
|
970
|
+
|
|
971
|
+
\`\`\`ts
|
|
972
|
+
import { game } from 'star-canvas';
|
|
973
|
+
|
|
974
|
+
game(({ ctx, width, height, loop }) => {
|
|
975
|
+
const tile = new Image();
|
|
976
|
+
tile.src = 'https://example.com/grass_tile.png'; // Use generated asset URL (seamlessTile: true)
|
|
977
|
+
|
|
978
|
+
let pattern = null;
|
|
979
|
+
tile.onload = () => {
|
|
980
|
+
pattern = ctx.createPattern(tile, 'repeat');
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
const player = { x: 320, y: 300 };
|
|
984
|
+
|
|
985
|
+
loop((dt) => {
|
|
986
|
+
// Draw tiled background
|
|
987
|
+
if (pattern) {
|
|
988
|
+
ctx.fillStyle = pattern;
|
|
989
|
+
ctx.fillRect(0, 0, width, height);
|
|
990
|
+
} else {
|
|
991
|
+
ctx.fillStyle = '#22c55e'; // Fallback color while loading
|
|
992
|
+
ctx.fillRect(0, 0, width, height);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Draw game objects on top
|
|
996
|
+
ctx.fillStyle = '#3b82f6';
|
|
997
|
+
ctx.fillRect(player.x - 16, player.y - 16, 32, 32);
|
|
998
|
+
});
|
|
999
|
+
});
|
|
1000
|
+
\`\`\`
|
|
1001
|
+
|
|
1002
|
+
**When to use which:**
|
|
1003
|
+
|
|
1004
|
+
| Type | Size | Use Case | Generation Settings |
|
|
1005
|
+
|------|------|----------|---------------------|
|
|
1006
|
+
| Full-canvas | 1024\xD71024 | Unique scenes, landscapes, detailed environments | \`model: "gemini"\`, no \`seamlessTile\` |
|
|
1007
|
+
| Tileable (all directions) | 256\xD7256 or 512\xD7512 | Grass, water, brick, abstract patterns | \`model: "gemini"\`, \`seamlessTile: "both"\` |
|
|
1008
|
+
| Horizontal tiling | 256\xD7512 or similar | Side-scroller parallax layers, horizon lines | \`model: "gemini"\`, \`seamlessTile: "horizontal"\` |
|
|
1009
|
+
| Vertical tiling | 512\xD7256 or similar | Vertical scroller backgrounds | \`model: "gemini"\`, \`seamlessTile: "vertical"\` |
|
|
1010
|
+
|
|
1011
|
+
**Common mistakes:**
|
|
1012
|
+
- \u274C Generating a detailed scene and expecting it to tile \u2192 Use \`seamlessTile\` only for patterns
|
|
1013
|
+
- \u274C Using \`drawImage()\` without size params \u2192 Image won't scale to canvas
|
|
1014
|
+
- \u274C Not handling image load state \u2192 Blank canvas until loaded
|
|
1015
|
+
- \u274C Using \`seamlessTile: "both"\` when you only need one direction \u2192 AI has better success with single-axis tiling`;
|
|
923
1016
|
var LEADERBOARD_DOCS = `**Installation**
|
|
924
1017
|
|
|
925
1018
|
\`\`\`bash
|
|
@@ -944,7 +1037,7 @@ const leaderboard = createLeaderboard();
|
|
|
944
1037
|
|
|
945
1038
|
\`\`\`javascript
|
|
946
1039
|
import { createLeaderboard } from 'star-leaderboard';
|
|
947
|
-
import { game } from '
|
|
1040
|
+
import { game } from 'star-canvas';
|
|
948
1041
|
|
|
949
1042
|
const leaderboard = createLeaderboard();
|
|
950
1043
|
|
|
@@ -1033,7 +1126,7 @@ async function gameOver(finalScore) {
|
|
|
1033
1126
|
|
|
1034
1127
|
\`\`\`javascript
|
|
1035
1128
|
import { createLeaderboard } from 'star-leaderboard';
|
|
1036
|
-
import { game } from '
|
|
1129
|
+
import { game } from 'star-canvas';
|
|
1037
1130
|
|
|
1038
1131
|
const leaderboard = createLeaderboard();
|
|
1039
1132
|
|
|
@@ -1079,7 +1172,7 @@ async function showCustomLeaderboard() {
|
|
|
1079
1172
|
|
|
1080
1173
|
\`\`\`javascript
|
|
1081
1174
|
import { createLeaderboard } from 'star-leaderboard';
|
|
1082
|
-
import { game } from '
|
|
1175
|
+
import { game } from 'star-canvas';
|
|
1083
1176
|
|
|
1084
1177
|
const leaderboard = createLeaderboard();
|
|
1085
1178
|
|
|
@@ -1178,456 +1271,6 @@ const data = await leaderboard.getScores({
|
|
|
1178
1271
|
4. **Don't store leaderboard state** - Just call the SDK methods when needed. The platform handles caching.
|
|
1179
1272
|
|
|
1180
1273
|
5. **Works for guests** - Guests get a generated name like "Guest1234". They can sign in later to claim scores.`;
|
|
1181
|
-
var MULTIPLAYER_DOCS = `**Installation**
|
|
1182
|
-
|
|
1183
|
-
\`\`\`bash
|
|
1184
|
-
yarn add star-multiplayer
|
|
1185
|
-
\`\`\`
|
|
1186
|
-
|
|
1187
|
-
### Star Multiplayer SDK
|
|
1188
|
-
|
|
1189
|
-
**Real-time multiplayer for Star games.** Host-authoritative state sync with automatic room management.
|
|
1190
|
-
|
|
1191
|
-
**Import:**
|
|
1192
|
-
\`\`\`javascript
|
|
1193
|
-
import { createMultiplayer } from 'star-multiplayer';
|
|
1194
|
-
const mp = createMultiplayer();
|
|
1195
|
-
\`\`\`
|
|
1196
|
-
|
|
1197
|
-
**CRITICAL:** Import in JavaScript - don't add \`<script>\` tags.
|
|
1198
|
-
|
|
1199
|
-
**Features:**
|
|
1200
|
-
- **Host-authoritative** - One player runs game logic, others receive state
|
|
1201
|
-
- **Room-based** - Create/join rooms with 6-character invite codes
|
|
1202
|
-
- **Auto host migration** - If host leaves, another player becomes host
|
|
1203
|
-
- **Built-in lobby UI** - No need to build your own
|
|
1204
|
-
- **Works in iframes** - Uses postMessage relay to parent window
|
|
1205
|
-
|
|
1206
|
-
---
|
|
1207
|
-
|
|
1208
|
-
**Quick Start:**
|
|
1209
|
-
|
|
1210
|
-
\`\`\`javascript
|
|
1211
|
-
import { createMultiplayer } from 'star-multiplayer';
|
|
1212
|
-
import { game } from '/star-sdk/v1/dom.js';
|
|
1213
|
-
|
|
1214
|
-
const mp = createMultiplayer();
|
|
1215
|
-
|
|
1216
|
-
game(async ({ ctx, width, height, loop, canvas }) => {
|
|
1217
|
-
// 1. Start - auto-finds or creates room, starts immediately
|
|
1218
|
-
await mp.start();
|
|
1219
|
-
|
|
1220
|
-
// 2. Game state
|
|
1221
|
-
let state = { players: {}, ball: { x: width/2, y: height/2, vx: 200, vy: 150 } };
|
|
1222
|
-
|
|
1223
|
-
// 3. Receive state updates - update shared objects, keep local player prediction
|
|
1224
|
-
mp.onState((s) => {
|
|
1225
|
-
state.ball = s.ball;
|
|
1226
|
-
for (const [id, p] of Object.entries(s.players)) {
|
|
1227
|
-
if (id !== mp.localPlayerId) {
|
|
1228
|
-
state.players[id] = p;
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
});
|
|
1232
|
-
|
|
1233
|
-
// 4. Handle inputs (runs locally for prediction, on host for authoritative state)
|
|
1234
|
-
mp.onInput((playerId, input) => {
|
|
1235
|
-
if (!state.players[playerId]) {
|
|
1236
|
-
state.players[playerId] = { y: height/2 };
|
|
1237
|
-
}
|
|
1238
|
-
state.players[playerId].y = input.y;
|
|
1239
|
-
});
|
|
1240
|
-
|
|
1241
|
-
// 5. Game loop
|
|
1242
|
-
loop((dt) => {
|
|
1243
|
-
// Host: run physics + broadcast state (auto-throttled to 20Hz)
|
|
1244
|
-
mp.hostTick(dt, () => {
|
|
1245
|
-
state.ball.x += state.ball.vx * dt;
|
|
1246
|
-
state.ball.y += state.ball.vy * dt;
|
|
1247
|
-
if (state.ball.y < 0 || state.ball.y > height) state.ball.vy *= -1;
|
|
1248
|
-
return state;
|
|
1249
|
-
});
|
|
1250
|
-
|
|
1251
|
-
// Everyone: render with interpolation for smooth movement
|
|
1252
|
-
const renderState = mp.getInterpolatedState(dt, state);
|
|
1253
|
-
ctx.fillStyle = '#0f172a';
|
|
1254
|
-
ctx.fillRect(0, 0, width, height);
|
|
1255
|
-
for (const p of Object.values(renderState.players)) {
|
|
1256
|
-
ctx.fillStyle = '#22d3ee';
|
|
1257
|
-
ctx.fillRect(20, p.y - 40, 10, 80);
|
|
1258
|
-
}
|
|
1259
|
-
ctx.beginPath();
|
|
1260
|
-
ctx.arc(renderState.ball.x, renderState.ball.y, 10, 0, Math.PI * 2);
|
|
1261
|
-
ctx.fill();
|
|
1262
|
-
});
|
|
1263
|
-
|
|
1264
|
-
// 6. Input - works for EVERYONE (applied locally for instant feedback)
|
|
1265
|
-
canvas.onpointermove = (e) => {
|
|
1266
|
-
const rect = canvas.getBoundingClientRect();
|
|
1267
|
-
mp.input({ y: ((e.clientY - rect.top) / rect.height) * height });
|
|
1268
|
-
};
|
|
1269
|
-
});
|
|
1270
|
-
\`\`\`
|
|
1271
|
-
|
|
1272
|
-
**That's it!** The SDK handles:
|
|
1273
|
-
- Auto-matchmaking (finds open room or creates one)
|
|
1274
|
-
- URL detection (\`?room=CODE\` \u2192 joins specific room)
|
|
1275
|
-
- Throttling state broadcasts to 20Hz
|
|
1276
|
-
- Routing host's inputs to their own handler
|
|
1277
|
-
|
|
1278
|
-
---
|
|
1279
|
-
|
|
1280
|
-
**Architecture:**
|
|
1281
|
-
|
|
1282
|
-
\`\`\`
|
|
1283
|
-
Host (Player 1) Server Client (Player 2)
|
|
1284
|
-
\u2502 \u2502 \u2502
|
|
1285
|
-
\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
|
|
1286
|
-
\u2502 \u2502 \u2502
|
|
1287
|
-
\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
|
|
1288
|
-
\u2502 \u2502 \u2502
|
|
1289
|
-
\u2502 (runs game logic) \u2502 (dumb relay) \u2502 (renders state)
|
|
1290
|
-
\`\`\`
|
|
1291
|
-
|
|
1292
|
-
---
|
|
1293
|
-
|
|
1294
|
-
**API Reference:**
|
|
1295
|
-
|
|
1296
|
-
**State (read-only):**
|
|
1297
|
-
\`\`\`javascript
|
|
1298
|
-
mp.isHost // true if you're the host
|
|
1299
|
-
mp.isReady // true if game has started
|
|
1300
|
-
mp.players // Array of { id: string }
|
|
1301
|
-
mp.localPlayerId // Your player ID
|
|
1302
|
-
mp.roomCode // Current room code
|
|
1303
|
-
\`\`\`
|
|
1304
|
-
|
|
1305
|
-
**Setup:**
|
|
1306
|
-
\`\`\`javascript
|
|
1307
|
-
await mp.start(); // Uses all defaults - auto-matchmaking, 8 players
|
|
1308
|
-
|
|
1309
|
-
// Or customize:
|
|
1310
|
-
await mp.start({
|
|
1311
|
-
maxPlayers: 8, // default: 8 (max players per room)
|
|
1312
|
-
mode: 'auto', // default: 'auto' - finds/creates rooms automatically
|
|
1313
|
-
// 'private' - shows lobby with shareable code
|
|
1314
|
-
showLobby: true, // default: true (only used in 'private' mode)
|
|
1315
|
-
tickRate: 50, // default: 50ms (20Hz state broadcasts)
|
|
1316
|
-
prediction: true // default: true (client-side prediction for instant input)
|
|
1317
|
-
});
|
|
1318
|
-
|
|
1319
|
-
// Examples:
|
|
1320
|
-
await mp.start({ maxPlayers: 2 }); // 1v1 game
|
|
1321
|
-
await mp.start({ maxPlayers: 100 }); // Battle royale
|
|
1322
|
-
await mp.start({ mode: 'private' }); // Friends-only with invite code
|
|
1323
|
-
await mp.start({ mode: 'private', maxPlayers: 4 }); // Small private game
|
|
1324
|
-
\`\`\`
|
|
1325
|
-
|
|
1326
|
-
**Sync:**
|
|
1327
|
-
\`\`\`javascript
|
|
1328
|
-
mp.onState((state) => { ... }) // Receive state (returns unsubscribe)
|
|
1329
|
-
mp.onInput((playerId, input) => { ... }) // Handle continuous input (position, aim)
|
|
1330
|
-
mp.hostTick(dt, () => state) // Run on host, broadcast returned state
|
|
1331
|
-
mp.input(data) // Send continuous input (position, aim)
|
|
1332
|
-
mp.getInterpolatedState(dt, state) // Get smoothly interpolated state for rendering
|
|
1333
|
-
\`\`\`
|
|
1334
|
-
|
|
1335
|
-
**Discrete Events (shooting, jumping, using items):**
|
|
1336
|
-
\`\`\`javascript
|
|
1337
|
-
mp.event(type, data) // Send event - guaranteed delivery
|
|
1338
|
-
mp.onEvent((playerId, type, data) => { }) // Handle events (HOST ONLY, authoritative)
|
|
1339
|
-
\`\`\`
|
|
1340
|
-
|
|
1341
|
-
**Player Updates:**
|
|
1342
|
-
\`\`\`javascript
|
|
1343
|
-
mp.onLocalPlayerUpdate((player, prevPlayer) => { ... }) // Local player changed
|
|
1344
|
-
mp.onPlayerUpdate(playerId, (player, prevPlayer) => { ... }) // Specific player changed
|
|
1345
|
-
\`\`\`
|
|
1346
|
-
|
|
1347
|
-
**Lifecycle Events:**
|
|
1348
|
-
\`\`\`javascript
|
|
1349
|
-
mp.onPlayers((players) => { ... }) // Player list changed
|
|
1350
|
-
mp.onPlayerJoin((player) => { ... }) // Another player joined (use to init their state)
|
|
1351
|
-
mp.onPlayerLeave((player) => { ... }) // A player left
|
|
1352
|
-
mp.onHost((isHost) => { ... }) // Host status changed (for host migration)
|
|
1353
|
-
mp.onError((message) => { ... }) // Error occurred
|
|
1354
|
-
\`\`\`
|
|
1355
|
-
|
|
1356
|
-
**Cleanup:**
|
|
1357
|
-
\`\`\`javascript
|
|
1358
|
-
mp.leave() // Leave room
|
|
1359
|
-
mp.destroy() // Full cleanup
|
|
1360
|
-
\`\`\`
|
|
1361
|
-
|
|
1362
|
-
---
|
|
1363
|
-
|
|
1364
|
-
**Tips:**
|
|
1365
|
-
|
|
1366
|
-
1. **Use \`hostTick()\`** - It handles host detection and throttling. Don't use \`setInterval\`.
|
|
1367
|
-
|
|
1368
|
-
2. **\`input()\` works on host** - Host's \`input()\` calls their \`onInput\` handler directly. Same code for everyone.
|
|
1369
|
-
|
|
1370
|
-
3. **Handle host migration** - If host leaves, \`onHost(true)\` fires on new host. They should start running physics.
|
|
1371
|
-
|
|
1372
|
-
4. **State design** - Only sync what changes: positions, scores, game objects. Not constants or textures.
|
|
1373
|
-
|
|
1374
|
-
5. **Auto mode is default** - Players join instantly without friction. Use \`mode: 'private'\` only if you need invite codes.
|
|
1375
|
-
|
|
1376
|
-
6. **Scale with maxPlayers** - Set \`maxPlayers: 2\` for 1v1, \`maxPlayers: 50\` for larger games. Default is 8.
|
|
1377
|
-
|
|
1378
|
-
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.
|
|
1379
|
-
|
|
1380
|
-
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.
|
|
1381
|
-
|
|
1382
|
-
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.
|
|
1383
|
-
|
|
1384
|
-
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.
|
|
1385
|
-
|
|
1386
|
-
---
|
|
1387
|
-
|
|
1388
|
-
**Where Code Runs (Important!):**
|
|
1389
|
-
|
|
1390
|
-
| Code Type | Where | Why |
|
|
1391
|
-
|-----------|-------|-----|
|
|
1392
|
-
| Physics, collision | Host (\`hostTick\`) | Single source of truth |
|
|
1393
|
-
| Hit detection, damage | Host (\`onInput\`) | Prevents cheating |
|
|
1394
|
-
| State changes (health, score) | Host | Authoritative |
|
|
1395
|
-
| UI updates, messages | **Everyone** (\`onState\` or \`loop\`) | Each player sees their own |
|
|
1396
|
-
| Visual/sound effects | **Everyone** | Local feedback |
|
|
1397
|
-
| Input sending | **Everyone** (\`mp.input()\`) | All players send inputs |
|
|
1398
|
-
|
|
1399
|
-
**Golden rule:**
|
|
1400
|
-
- **CHANGES state** \u2192 host only
|
|
1401
|
-
- **REACTS to state** \u2192 everyone locally
|
|
1402
|
-
|
|
1403
|
-
\`\`\`javascript
|
|
1404
|
-
// \u274C WRONG - UI in host-only code
|
|
1405
|
-
mp.hostTick(dt, () => {
|
|
1406
|
-
if (player.health <= 0) showDeathMessage(); // Only host sees!
|
|
1407
|
-
return state;
|
|
1408
|
-
});
|
|
1409
|
-
|
|
1410
|
-
// \u2705 RIGHT - React to state on every client
|
|
1411
|
-
mp.onState((state) => {
|
|
1412
|
-
const wasAlive = localPlayer.health > 0;
|
|
1413
|
-
localPlayer.health = state.players[mp.localPlayerId].health;
|
|
1414
|
-
if (wasAlive && localPlayer.health <= 0) {
|
|
1415
|
-
showDeathMessage(); // Everyone sees their own death!
|
|
1416
|
-
}
|
|
1417
|
-
});
|
|
1418
|
-
\`\`\`
|
|
1419
|
-
|
|
1420
|
-
---
|
|
1421
|
-
|
|
1422
|
-
**Common Patterns:**
|
|
1423
|
-
|
|
1424
|
-
### Player Initialization (Important!)
|
|
1425
|
-
|
|
1426
|
-
Initialize player state when they join, not when they first send input:
|
|
1427
|
-
|
|
1428
|
-
\`\`\`javascript
|
|
1429
|
-
// \u274C WRONG - Players invisible until they move
|
|
1430
|
-
mp.onInput((playerId, input) => {
|
|
1431
|
-
if (!state.players[playerId]) {
|
|
1432
|
-
state.players[playerId] = { x: 0, y: 0, health: 100 }; // Too late!
|
|
1433
|
-
}
|
|
1434
|
-
});
|
|
1435
|
-
|
|
1436
|
-
// \u2705 RIGHT - Initialize on join
|
|
1437
|
-
mp.onPlayerJoin((player) => {
|
|
1438
|
-
state.players[player.id] = {
|
|
1439
|
-
x: Math.random() * width,
|
|
1440
|
-
y: Math.random() * height,
|
|
1441
|
-
health: 100
|
|
1442
|
-
};
|
|
1443
|
-
});
|
|
1444
|
-
|
|
1445
|
-
mp.onPlayerLeave((player) => {
|
|
1446
|
-
delete state.players[player.id];
|
|
1447
|
-
});
|
|
1448
|
-
\`\`\`
|
|
1449
|
-
|
|
1450
|
-
**Why this matters:** Without \`onPlayerJoin\`, a spectating player (not sending input) would be invisible to everyone.
|
|
1451
|
-
|
|
1452
|
-
### Position Sync (3D/Complex Games)
|
|
1453
|
-
|
|
1454
|
-
Clients send position, host tracks all players:
|
|
1455
|
-
|
|
1456
|
-
\`\`\`javascript
|
|
1457
|
-
// Everyone: Send position every frame
|
|
1458
|
-
mp.input({
|
|
1459
|
-
pos: [localPlayer.x, localPlayer.y, localPlayer.z],
|
|
1460
|
-
yaw: localPlayer.yaw
|
|
1461
|
-
});
|
|
1462
|
-
|
|
1463
|
-
// Host: Track in onInput
|
|
1464
|
-
const remotePlayers = {};
|
|
1465
|
-
mp.onInput((playerId, input) => {
|
|
1466
|
-
if (!remotePlayers[playerId]) remotePlayers[playerId] = { health: 100 };
|
|
1467
|
-
if (input.pos) {
|
|
1468
|
-
remotePlayers[playerId].pos = input.pos;
|
|
1469
|
-
remotePlayers[playerId].yaw = input.yaw;
|
|
1470
|
-
}
|
|
1471
|
-
});
|
|
1472
|
-
\`\`\`
|
|
1473
|
-
|
|
1474
|
-
### Actions (Shooting, Jumping, Items) - Use \`mp.event()\`
|
|
1475
|
-
|
|
1476
|
-
**For discrete actions, use \`mp.event()\` instead of \`mp.input()\`.** The SDK guarantees delivery, deduplication, and preserves each event's data.
|
|
1477
|
-
|
|
1478
|
-
**The Pattern:**
|
|
1479
|
-
|
|
1480
|
-
\`\`\`javascript
|
|
1481
|
-
// \u274C WRONG - mp.input() loses rapid discrete actions (30Hz throttling)
|
|
1482
|
-
mp.input({ shoot: true }); // 3 rapid clicks = 1 shot!
|
|
1483
|
-
|
|
1484
|
-
// \u2705 RIGHT - Two parts: cosmetic (instant) + authoritative (host)
|
|
1485
|
-
|
|
1486
|
-
// PART 1: At the trigger point (EVERYONE runs this)
|
|
1487
|
-
if (shooting && canShoot(localPlayer)) {
|
|
1488
|
-
// Cosmetic feedback - instant, local only
|
|
1489
|
-
audio.play('shoot');
|
|
1490
|
-
particles.muzzleFlash(localPlayer.pos);
|
|
1491
|
-
|
|
1492
|
-
// Send event to host for authoritative processing
|
|
1493
|
-
mp.event('shoot', {
|
|
1494
|
-
origin: [x, y, z],
|
|
1495
|
-
dir: [dx, dy, dz]
|
|
1496
|
-
});
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
// PART 2: Authoritative handler (HOST ONLY processes this)
|
|
1500
|
-
mp.onEvent((playerId, type, data) => {
|
|
1501
|
-
if (type === 'shoot') {
|
|
1502
|
-
// This creates the REAL bullet in game state
|
|
1503
|
-
state.bullets.push({
|
|
1504
|
-
owner: playerId,
|
|
1505
|
-
pos: data.origin,
|
|
1506
|
-
dir: data.dir
|
|
1507
|
-
});
|
|
1508
|
-
}
|
|
1509
|
-
});
|
|
1510
|
-
\`\`\`
|
|
1511
|
-
|
|
1512
|
-
**Why two parts?**
|
|
1513
|
-
|
|
1514
|
-
| Part | Who runs it | What it does |
|
|
1515
|
-
|------|-------------|--------------|
|
|
1516
|
-
| Cosmetic (at call site) | Everyone | Instant feedback: sound, particles, animations |
|
|
1517
|
-
| Authoritative (\`onEvent\`) | Host only | State changes: spawn bullets, apply damage |
|
|
1518
|
-
|
|
1519
|
-
**This separation is intentional:**
|
|
1520
|
-
- **Cosmetic:** Your own shots feel instant (no network latency)
|
|
1521
|
-
- **Authoritative:** Game state is consistent (host is source of truth)
|
|
1522
|
-
- **Other players' bullets:** Appear in state broadcast, ~50ms later (acceptable for casual games)
|
|
1523
|
-
|
|
1524
|
-
**Hearing other players' shots (optional):**
|
|
1525
|
-
\`\`\`javascript
|
|
1526
|
-
const seenBullets = new Set();
|
|
1527
|
-
mp.onState((state) => {
|
|
1528
|
-
for (const bullet of state.bullets || []) {
|
|
1529
|
-
if (!seenBullets.has(bullet.id) && bullet.owner !== mp.localPlayerId) {
|
|
1530
|
-
audio.play('shoot'); // Hear remote player's shot
|
|
1531
|
-
seenBullets.add(bullet.id);
|
|
1532
|
-
}
|
|
1533
|
-
}
|
|
1534
|
-
});
|
|
1535
|
-
\`\`\`
|
|
1536
|
-
|
|
1537
|
-
**When to use which:**
|
|
1538
|
-
| Action Type | Method | Example |
|
|
1539
|
-
|-------------|--------|---------|
|
|
1540
|
-
| Continuous state | \`mp.input()\` | Position, aim direction, movement |
|
|
1541
|
-
| Discrete action | \`mp.event()\` | Shooting, jumping, using items, emotes |
|
|
1542
|
-
|
|
1543
|
-
### Health (Host-Authoritative)
|
|
1544
|
-
|
|
1545
|
-
Host owns health. Clients accept and react:
|
|
1546
|
-
|
|
1547
|
-
\`\`\`javascript
|
|
1548
|
-
// \u274C WRONG - Client sends health (cheatable!)
|
|
1549
|
-
mp.input({ health: localPlayer.health });
|
|
1550
|
-
|
|
1551
|
-
// \u2705 RIGHT - Host applies damage
|
|
1552
|
-
mp.onInput((playerId, input) => {
|
|
1553
|
-
if (hit.playerId) remotePlayers[hit.playerId].health -= 25;
|
|
1554
|
-
});
|
|
1555
|
-
|
|
1556
|
-
// \u2705 RIGHT - Client accepts AND reacts
|
|
1557
|
-
mp.onState((state) => {
|
|
1558
|
-
const myHealth = state.players[mp.localPlayerId]?.health;
|
|
1559
|
-
if (myHealth !== undefined) {
|
|
1560
|
-
if (localPlayer.health > 0 && myHealth <= 0) showDeathMessage();
|
|
1561
|
-
localPlayer.health = myHealth;
|
|
1562
|
-
}
|
|
1563
|
-
});
|
|
1564
|
-
\`\`\`
|
|
1565
|
-
|
|
1566
|
-
### Reacting to Player State Changes (Death, Score, etc.)
|
|
1567
|
-
|
|
1568
|
-
Use \`onLocalPlayerUpdate\` to react when your player's state changes - the SDK tracks previous state for you:
|
|
1569
|
-
|
|
1570
|
-
\`\`\`javascript
|
|
1571
|
-
mp.onLocalPlayerUpdate((player, prevPlayer) => {
|
|
1572
|
-
// Detect death
|
|
1573
|
-
if (prevPlayer.health > 0 && player.health <= 0) {
|
|
1574
|
-
showDeathScreen();
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
// Detect respawn
|
|
1578
|
-
if (prevPlayer.health <= 0 && player.health > 0) {
|
|
1579
|
-
hideDeathScreen();
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
|
-
// Detect score increase
|
|
1583
|
-
if (player.score > (prevPlayer.score || 0)) {
|
|
1584
|
-
showScorePopup(player.score - prevPlayer.score);
|
|
1585
|
-
}
|
|
1586
|
-
});
|
|
1587
|
-
\`\`\`
|
|
1588
|
-
|
|
1589
|
-
For remote player effects (death animations, etc.):
|
|
1590
|
-
|
|
1591
|
-
\`\`\`javascript
|
|
1592
|
-
// Subscribe to each remote player's updates
|
|
1593
|
-
for (const p of mp.players) {
|
|
1594
|
-
if (p.id !== mp.localPlayerId) {
|
|
1595
|
-
mp.onPlayerUpdate(p.id, (player, prev) => {
|
|
1596
|
-
if (prev.health > 0 && player.health <= 0) {
|
|
1597
|
-
playDeathAnimation(p.id);
|
|
1598
|
-
}
|
|
1599
|
-
});
|
|
1600
|
-
}
|
|
1601
|
-
}
|
|
1602
|
-
\`\`\`
|
|
1603
|
-
|
|
1604
|
-
---
|
|
1605
|
-
|
|
1606
|
-
**Anti-Patterns:**
|
|
1607
|
-
|
|
1608
|
-
### Using \`mp.input()\` for Discrete Actions
|
|
1609
|
-
|
|
1610
|
-
**Problem**: Using \`mp.input({ shoot: true })\` for shooting, jumping, or any discrete action.
|
|
1611
|
-
|
|
1612
|
-
**Why it breaks**: \`mp.input()\` is throttled to 30Hz. Multiple rapid clicks collapse into a single value, losing inputs.
|
|
1613
|
-
|
|
1614
|
-
\`\`\`javascript
|
|
1615
|
-
// \u274C BAD - mp.input() loses rapid discrete actions
|
|
1616
|
-
mp.input({ shoot: true }); // 3 rapid clicks = 1 shot!
|
|
1617
|
-
|
|
1618
|
-
// \u274C ALSO BAD - counters in mp.input() still have issues
|
|
1619
|
-
mp.input({ shootCount: count }); // Bursts all shots at once, loses timing
|
|
1620
|
-
|
|
1621
|
-
// \u2705 GOOD - Use mp.event() for discrete actions
|
|
1622
|
-
mp.event('shoot', { origin, dir }); // Each event preserved with its data
|
|
1623
|
-
\`\`\`
|
|
1624
|
-
|
|
1625
|
-
**The rule is simple:**
|
|
1626
|
-
|
|
1627
|
-
| Action Type | Method | Why |
|
|
1628
|
-
|-------------|--------|-----|
|
|
1629
|
-
| **Continuous** (position, aim) | \`mp.input()\` | Last value is correct, throttling is fine |
|
|
1630
|
-
| **Discrete** (shoot, jump, use item) | \`mp.event()\` | Each action must be delivered with its data |`;
|
|
1631
1274
|
|
|
1632
1275
|
// src/cli.ts
|
|
1633
1276
|
var VERSION = "0.1.0";
|
|
@@ -1767,11 +1410,14 @@ async function initCommand(name, email) {
|
|
|
1767
1410
|
log(`${colors.dim}Config saved to${colors.reset} ${colors.bright}.starrc${colors.reset}`);
|
|
1768
1411
|
log("");
|
|
1769
1412
|
log(`${colors.dim}Next steps:${colors.reset}`);
|
|
1770
|
-
log(` 1.
|
|
1771
|
-
log(` 2.
|
|
1413
|
+
log(` 1. Install: ${colors.cyan}npm install star-sdk${colors.reset}`);
|
|
1414
|
+
log(` 2. Import Star SDK in your game`);
|
|
1415
|
+
log(` 3. Initialize with your game ID`);
|
|
1416
|
+
log(` 4. Use Star.leaderboard.submit(score) to submit scores`);
|
|
1772
1417
|
log("");
|
|
1773
1418
|
log(`${colors.dim}Example:${colors.reset}`);
|
|
1774
1419
|
log(` ${colors.cyan}import Star from 'star-sdk';${colors.reset}`);
|
|
1420
|
+
log(` ${colors.cyan}Star.init({ gameId: '${data.gameId}' });${colors.reset}`);
|
|
1775
1421
|
log(` ${colors.cyan}Star.leaderboard.submit(1500);${colors.reset}`);
|
|
1776
1422
|
log("");
|
|
1777
1423
|
log(`${colors.dim}Using an AI coding agent?${colors.reset}`);
|
|
@@ -1813,7 +1459,7 @@ function whoamiCommand() {
|
|
|
1813
1459
|
function transformForStdout(content) {
|
|
1814
1460
|
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(
|
|
1815
1461
|
/For detailed API documentation, see the linked files above\./,
|
|
1816
|
-
"For detailed API docs: `npx star-sdk docs <topic>` where topic is audio, canvas,
|
|
1462
|
+
"For detailed API docs: `npx star-sdk docs <topic>` where topic is audio, canvas, or leaderboard."
|
|
1817
1463
|
);
|
|
1818
1464
|
}
|
|
1819
1465
|
function getAllDocsContent() {
|
|
@@ -1835,13 +1481,7 @@ ${CANVAS_DOCS}
|
|
|
1835
1481
|
|
|
1836
1482
|
# Leaderboard API
|
|
1837
1483
|
|
|
1838
|
-
${LEADERBOARD_DOCS}
|
|
1839
|
-
|
|
1840
|
-
---
|
|
1841
|
-
|
|
1842
|
-
# Multiplayer API
|
|
1843
|
-
|
|
1844
|
-
${MULTIPLAYER_DOCS}`;
|
|
1484
|
+
${LEADERBOARD_DOCS}`;
|
|
1845
1485
|
}
|
|
1846
1486
|
function installClaudeCode(scope) {
|
|
1847
1487
|
const skillDir = scope === "global" ? path.join(os.homedir(), ".claude", "skills", "star-sdk") : path.join(process.cwd(), ".claude", "skills", "star-sdk");
|
|
@@ -1850,7 +1490,6 @@ function installClaudeCode(scope) {
|
|
|
1850
1490
|
fs.writeFileSync(path.join(skillDir, "audio.md"), AUDIO_DOCS);
|
|
1851
1491
|
fs.writeFileSync(path.join(skillDir, "canvas.md"), CANVAS_DOCS);
|
|
1852
1492
|
fs.writeFileSync(path.join(skillDir, "leaderboard.md"), LEADERBOARD_DOCS);
|
|
1853
|
-
fs.writeFileSync(path.join(skillDir, "multiplayer.md"), MULTIPLAYER_DOCS);
|
|
1854
1493
|
log("");
|
|
1855
1494
|
success(`Star SDK skill installed to ${skillDir}`);
|
|
1856
1495
|
log("");
|
|
@@ -1861,7 +1500,6 @@ function installClaudeCode(scope) {
|
|
|
1861
1500
|
log(` audio.md ${colors.dim}Star.audio API docs${colors.reset}`);
|
|
1862
1501
|
log(` canvas.md ${colors.dim}Star.game() API docs${colors.reset}`);
|
|
1863
1502
|
log(` leaderboard.md ${colors.dim}Star.leaderboard API docs${colors.reset}`);
|
|
1864
|
-
log(` multiplayer.md ${colors.dim}Star.multiplayer API docs${colors.reset}`);
|
|
1865
1503
|
log("");
|
|
1866
1504
|
}
|
|
1867
1505
|
function installCodex(scope) {
|
|
@@ -1892,7 +1530,7 @@ function installCursor() {
|
|
|
1892
1530
|
const rulesDir = path.join(process.cwd(), ".cursor", "rules");
|
|
1893
1531
|
const filePath = path.join(rulesDir, "star-sdk.mdc");
|
|
1894
1532
|
const content = `---
|
|
1895
|
-
description: Star SDK for browser game development with audio, canvas,
|
|
1533
|
+
description: Star SDK for browser game development with audio, canvas, and leaderboards
|
|
1896
1534
|
globs: ["**/*.js", "**/*.ts", "**/*.jsx", "**/*.tsx", "**/*.html"]
|
|
1897
1535
|
alwaysApply: false
|
|
1898
1536
|
---
|
|
@@ -1976,8 +1614,7 @@ function docsCommand(topic) {
|
|
|
1976
1614
|
skill: SKILL_CONTENT,
|
|
1977
1615
|
audio: AUDIO_DOCS,
|
|
1978
1616
|
canvas: CANVAS_DOCS,
|
|
1979
|
-
leaderboard: LEADERBOARD_DOCS
|
|
1980
|
-
multiplayer: MULTIPLAYER_DOCS
|
|
1617
|
+
leaderboard: LEADERBOARD_DOCS
|
|
1981
1618
|
};
|
|
1982
1619
|
if (!topic) {
|
|
1983
1620
|
console.log(transformForStdout(SKILL_CONTENT));
|
|
@@ -1986,7 +1623,7 @@ function docsCommand(topic) {
|
|
|
1986
1623
|
} else {
|
|
1987
1624
|
error(`Unknown topic: ${topic}`);
|
|
1988
1625
|
log("");
|
|
1989
|
-
log("Available topics: skill, audio, canvas, leaderboard
|
|
1626
|
+
log("Available topics: skill, audio, canvas, leaderboard");
|
|
1990
1627
|
process.exit(1);
|
|
1991
1628
|
}
|
|
1992
1629
|
}
|