keeperboard 1.0.3 → 2.0.0
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/README.md +197 -296
- package/dist/index.d.mts +337 -111
- package/dist/index.d.ts +337 -111
- package/dist/index.js +453 -49
- package/dist/index.mjs +448 -48
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,301 +1,229 @@
|
|
|
1
1
|
# KeeperBoard SDK
|
|
2
2
|
|
|
3
|
-
TypeScript client
|
|
4
|
-
|
|
5
|
-
Works with Phaser.js, vanilla JavaScript, and any TypeScript/JavaScript game running in the browser.
|
|
3
|
+
TypeScript client for [KeeperBoard](https://keeperboard.vercel.app) leaderboard-as-a-service. Works in browsers and Node.js.
|
|
6
4
|
|
|
7
5
|
## Installation
|
|
8
6
|
|
|
9
7
|
```bash
|
|
10
|
-
npm install keeperboard
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
### Alternative: Copy source directly
|
|
14
|
-
|
|
15
|
-
If you prefer not to use npm, copy the `src/` folder into your project:
|
|
16
|
-
|
|
17
|
-
```typescript
|
|
18
|
-
import { KeeperBoardClient, PlayerIdentity } from './keeperboard/index';
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## Quick Start
|
|
22
|
-
|
|
23
|
-
### 1. Get your API key
|
|
24
|
-
|
|
25
|
-
1. Sign up at your KeeperBoard dashboard
|
|
26
|
-
2. Create a game
|
|
27
|
-
3. Create an environment (dev/prod)
|
|
28
|
-
4. Generate an API key for that environment
|
|
29
|
-
|
|
30
|
-
### 2. Initialize the client
|
|
31
|
-
|
|
32
|
-
```typescript
|
|
33
|
-
import { KeeperBoardClient, PlayerIdentity } from 'keeperboard-sdk';
|
|
34
|
-
|
|
35
|
-
// Create the API client
|
|
36
|
-
const client = new KeeperBoardClient({
|
|
37
|
-
apiUrl: 'https://keeperboard.vercel.app',
|
|
38
|
-
apiKey: 'kb_prod_your_api_key_here',
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
// Helper for persistent player identity
|
|
42
|
-
const identity = new PlayerIdentity();
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
### 3. Submit a score
|
|
46
|
-
|
|
47
|
-
```typescript
|
|
48
|
-
const playerGuid = identity.getOrCreatePlayerGuid();
|
|
49
|
-
|
|
50
|
-
// Submit to default leaderboard
|
|
51
|
-
const result = await client.submitScore(playerGuid, 'PlayerName', 1500);
|
|
52
|
-
|
|
53
|
-
console.log(`Rank: #${result.rank}`);
|
|
54
|
-
console.log(`New high score: ${result.is_new_high_score}`);
|
|
8
|
+
npm install keeperboard
|
|
55
9
|
```
|
|
56
10
|
|
|
57
|
-
|
|
11
|
+
## Quick Start (15 lines)
|
|
58
12
|
|
|
59
13
|
```typescript
|
|
60
|
-
|
|
61
|
-
const leaderboard = await client.getLeaderboard();
|
|
14
|
+
import { KeeperBoardSession } from 'keeperboard';
|
|
62
15
|
|
|
63
|
-
|
|
64
|
-
|
|
16
|
+
const session = new KeeperBoardSession({
|
|
17
|
+
apiKey: 'kb_dev_your_api_key',
|
|
18
|
+
leaderboard: 'main',
|
|
19
|
+
cache: { ttlMs: 30000 }, // Optional: 30s cache
|
|
20
|
+
retry: { maxAgeMs: 86400000 }, // Optional: 24h retry queue
|
|
65
21
|
});
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
### 5. Show player's rank (even if not in top 10)
|
|
69
|
-
|
|
70
|
-
```typescript
|
|
71
|
-
const player = await client.getPlayerRank(playerGuid);
|
|
72
22
|
|
|
73
|
-
|
|
74
|
-
|
|
23
|
+
// Submit a score
|
|
24
|
+
const result = await session.submitScore(1500);
|
|
25
|
+
if (result.success) {
|
|
26
|
+
console.log(`Rank #${result.rank}, New high: ${result.isNewHighScore}`);
|
|
75
27
|
}
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
## API Reference
|
|
79
|
-
|
|
80
|
-
### KeeperBoardClient
|
|
81
28
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
apiUrl: string, // Your KeeperBoard API URL
|
|
87
|
-
apiKey: string, // API key from dashboard
|
|
29
|
+
// Get leaderboard with player's rank
|
|
30
|
+
const snapshot = await session.getSnapshot({ limit: 10 });
|
|
31
|
+
snapshot.entries.forEach(e => {
|
|
32
|
+
console.log(`#${e.rank} ${e.playerName}: ${e.score}`, e.isCurrentPlayer ? '(you)' : '');
|
|
88
33
|
});
|
|
89
34
|
```
|
|
90
35
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
---
|
|
94
|
-
|
|
95
|
-
### Score Submission
|
|
36
|
+
## Two API Layers
|
|
96
37
|
|
|
97
|
-
|
|
38
|
+
| Layer | Use case | Identity | Cache | Retry |
|
|
39
|
+
|-------|----------|----------|-------|-------|
|
|
40
|
+
| **KeeperBoardSession** | Browser games | Auto-managed | Built-in | Built-in |
|
|
41
|
+
| **KeeperBoardClient** | Server-side, advanced | Manual | No | No |
|
|
98
42
|
|
|
99
|
-
|
|
43
|
+
Most browser games should use `KeeperBoardSession`. Use `KeeperBoardClient` for server-side code or when you need full control.
|
|
100
44
|
|
|
101
|
-
|
|
102
|
-
const result = await client.submitScore('player-uuid', 'PlayerName', 2500);
|
|
103
|
-
// Returns: { id, player_guid, player_name, score, rank, is_new_high_score }
|
|
104
|
-
```
|
|
45
|
+
---
|
|
105
46
|
|
|
106
|
-
|
|
47
|
+
## KeeperBoardSession API
|
|
107
48
|
|
|
108
|
-
|
|
49
|
+
### Constructor
|
|
109
50
|
|
|
110
51
|
```typescript
|
|
111
|
-
|
|
52
|
+
const session = new KeeperBoardSession({
|
|
53
|
+
apiKey: 'kb_dev_xxx', // Required
|
|
54
|
+
leaderboard: 'main', // Required - session is bound to one board
|
|
55
|
+
defaultPlayerName: 'ANON', // Optional (default: 'ANON')
|
|
56
|
+
identity: { keyPrefix: 'app_' }, // Optional localStorage prefix
|
|
57
|
+
cache: { ttlMs: 30000 }, // Optional TTL cache for getSnapshot()
|
|
58
|
+
retry: { maxAgeMs: 86400000 }, // Optional retry queue for failed submissions
|
|
59
|
+
});
|
|
112
60
|
```
|
|
113
61
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
Submit with optional metadata.
|
|
62
|
+
### Identity (auto-managed)
|
|
117
63
|
|
|
118
64
|
```typescript
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
2500,
|
|
123
|
-
'Weekly Best',
|
|
124
|
-
{ level: 10, character: 'warrior' }
|
|
125
|
-
);
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
---
|
|
129
|
-
|
|
130
|
-
### Leaderboard
|
|
65
|
+
session.getPlayerGuid(); // Get or create persistent GUID
|
|
66
|
+
session.getPlayerName(); // Get stored name or default
|
|
67
|
+
session.setPlayerName(name); // Store name locally (doesn't update server)
|
|
131
68
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
```typescript
|
|
137
|
-
const lb = await client.getLeaderboard();
|
|
138
|
-
// Returns: { entries, total_count, reset_schedule }
|
|
69
|
+
// Validate a name (pure function)
|
|
70
|
+
const validated = session.validateName(' Ace Pilot! ');
|
|
71
|
+
// Returns 'ACEPILOT' or null if invalid
|
|
139
72
|
```
|
|
140
73
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
Get a specific leaderboard by name.
|
|
74
|
+
### Core Methods
|
|
144
75
|
|
|
145
76
|
```typescript
|
|
146
|
-
|
|
77
|
+
// Submit score (identity auto-injected)
|
|
78
|
+
const result = await session.submitScore(1500, { level: 5 });
|
|
79
|
+
// Returns: { success: true, rank: 3, isNewHighScore: true }
|
|
80
|
+
// or: { success: false, error: 'Network error' }
|
|
81
|
+
|
|
82
|
+
// Get snapshot (leaderboard + player rank combined)
|
|
83
|
+
const snapshot = await session.getSnapshot({ limit: 10 });
|
|
84
|
+
// Returns: {
|
|
85
|
+
// entries: [{ rank, playerGuid, playerName, score, isCurrentPlayer }],
|
|
86
|
+
// totalCount: 150,
|
|
87
|
+
// playerRank: { rank: 42, score: 1200, ... } | null // Only if outside top N
|
|
88
|
+
// }
|
|
89
|
+
|
|
90
|
+
// Update player name on server
|
|
91
|
+
const success = await session.updatePlayerName('MAVERICK');
|
|
147
92
|
```
|
|
148
93
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
Get with a custom limit (max 100).
|
|
94
|
+
### Retry Queue
|
|
152
95
|
|
|
153
96
|
```typescript
|
|
154
|
-
|
|
97
|
+
// Check for pending scores from previous failed submissions
|
|
98
|
+
if (session.hasPendingScore()) {
|
|
99
|
+
await session.retryPendingScore();
|
|
100
|
+
}
|
|
155
101
|
```
|
|
156
102
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
Get with pagination.
|
|
103
|
+
### Cache
|
|
160
104
|
|
|
161
105
|
```typescript
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
---
|
|
167
|
-
|
|
168
|
-
### Leaderboard Versions (Time-Based)
|
|
169
|
-
|
|
170
|
-
For leaderboards with reset schedules (daily/weekly/monthly), you can query historical versions.
|
|
171
|
-
|
|
172
|
-
#### `getLeaderboardVersion(name, version)`
|
|
173
|
-
|
|
174
|
-
Get a specific version of a time-based leaderboard.
|
|
106
|
+
// Pre-fetch in background (e.g., on menu load)
|
|
107
|
+
session.prefetch();
|
|
175
108
|
|
|
176
|
-
|
|
177
|
-
// Get last week's scores (version 3)
|
|
178
|
-
const lastWeek = await client.getLeaderboardVersion('Weekly Best', 3);
|
|
109
|
+
// getSnapshot() automatically uses cache when fresh
|
|
179
110
|
```
|
|
180
111
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
Get historical version with pagination.
|
|
112
|
+
### Escape Hatch
|
|
184
113
|
|
|
185
114
|
```typescript
|
|
186
|
-
|
|
115
|
+
// Access underlying client for advanced operations
|
|
116
|
+
const client = session.getClient();
|
|
117
|
+
await client.claimScore({ playerGuid: '...', playerName: '...' });
|
|
187
118
|
```
|
|
188
119
|
|
|
189
120
|
---
|
|
190
121
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
#### `getPlayerRank(playerGuid)`
|
|
194
|
-
|
|
195
|
-
Get a player's rank and score. Returns `null` if player has no score.
|
|
122
|
+
## KeeperBoardClient API
|
|
196
123
|
|
|
197
|
-
|
|
198
|
-
const player = await client.getPlayerRank('player-uuid');
|
|
199
|
-
|
|
200
|
-
if (player) {
|
|
201
|
-
console.log(`Rank: #${player.rank}, Score: ${player.score}`);
|
|
202
|
-
}
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
#### `getPlayerRank(playerGuid, leaderboard)`
|
|
124
|
+
Low-level client with options-object methods and camelCase responses.
|
|
206
125
|
|
|
207
|
-
|
|
126
|
+
### Constructor
|
|
208
127
|
|
|
209
128
|
```typescript
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
Update a player's display name.
|
|
216
|
-
|
|
217
|
-
```typescript
|
|
218
|
-
await client.updatePlayerName('player-uuid', 'NewPlayerName');
|
|
129
|
+
const client = new KeeperBoardClient({
|
|
130
|
+
apiKey: 'kb_dev_xxx',
|
|
131
|
+
defaultLeaderboard: 'main', // Optional - used when leaderboard not specified
|
|
132
|
+
});
|
|
219
133
|
```
|
|
220
134
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
### Claim (for migrated scores)
|
|
224
|
-
|
|
225
|
-
#### `claimScore(playerGuid, playerName)`
|
|
226
|
-
|
|
227
|
-
Claim a migrated score by matching player name. Used when scores were imported without player GUIDs.
|
|
135
|
+
### Methods
|
|
228
136
|
|
|
229
137
|
```typescript
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
138
|
+
// Submit score
|
|
139
|
+
const result = await client.submitScore({
|
|
140
|
+
playerGuid: 'abc-123',
|
|
141
|
+
playerName: 'ACE',
|
|
142
|
+
score: 1500,
|
|
143
|
+
metadata: { level: 5 }, // Optional
|
|
144
|
+
leaderboard: 'weekly', // Optional - overrides defaultLeaderboard
|
|
145
|
+
});
|
|
146
|
+
// Returns: ScoreResult { id, playerGuid, playerName, score, rank, isNewHighScore }
|
|
147
|
+
|
|
148
|
+
// Get leaderboard
|
|
149
|
+
const lb = await client.getLeaderboard({
|
|
150
|
+
leaderboard: 'main', // Optional
|
|
151
|
+
limit: 25, // Optional (default 10, max 100)
|
|
152
|
+
offset: 0, // Optional pagination
|
|
153
|
+
version: 3, // Optional - for time-based boards
|
|
154
|
+
});
|
|
155
|
+
// Returns: LeaderboardResult { entries, totalCount, resetSchedule, version?, ... }
|
|
235
156
|
|
|
236
|
-
|
|
157
|
+
// Get player rank
|
|
158
|
+
const player = await client.getPlayerRank({
|
|
159
|
+
playerGuid: 'abc-123',
|
|
160
|
+
leaderboard: 'main', // Optional
|
|
161
|
+
});
|
|
162
|
+
// Returns: PlayerResult | null
|
|
237
163
|
|
|
238
|
-
|
|
164
|
+
// Update player name
|
|
165
|
+
const updated = await client.updatePlayerName({
|
|
166
|
+
playerGuid: 'abc-123',
|
|
167
|
+
newName: 'MAVERICK',
|
|
168
|
+
leaderboard: 'main', // Optional
|
|
169
|
+
});
|
|
239
170
|
|
|
240
|
-
|
|
171
|
+
// Claim migrated score (for imported data without GUIDs)
|
|
172
|
+
const claim = await client.claimScore({
|
|
173
|
+
playerGuid: 'abc-123',
|
|
174
|
+
playerName: 'OldPlayer',
|
|
175
|
+
leaderboard: 'main', // Optional
|
|
176
|
+
});
|
|
241
177
|
|
|
242
|
-
|
|
178
|
+
// Health check (no auth required)
|
|
243
179
|
const health = await client.healthCheck();
|
|
244
|
-
console.log(`API Version: ${health.version}`);
|
|
245
180
|
```
|
|
246
181
|
|
|
247
182
|
---
|
|
248
183
|
|
|
249
|
-
|
|
184
|
+
## Name Validation
|
|
250
185
|
|
|
251
|
-
|
|
186
|
+
Standalone function for validating player names:
|
|
252
187
|
|
|
253
188
|
```typescript
|
|
254
|
-
|
|
255
|
-
|
|
189
|
+
import { validateName } from 'keeperboard';
|
|
190
|
+
|
|
191
|
+
validateName(' Ace Pilot! '); // 'ACEPILOT'
|
|
192
|
+
validateName('x'); // null (too short)
|
|
193
|
+
validateName('verylongname123456'); // 'VERYLONGNAME' (truncated to 12)
|
|
194
|
+
|
|
195
|
+
// Custom options
|
|
196
|
+
validateName('hello', {
|
|
197
|
+
minLength: 3,
|
|
198
|
+
maxLength: 8,
|
|
199
|
+
uppercase: false,
|
|
200
|
+
allowedPattern: /[^a-z]/g,
|
|
256
201
|
});
|
|
257
|
-
|
|
258
|
-
// Get or create a persistent player GUID
|
|
259
|
-
const guid = identity.getOrCreatePlayerGuid();
|
|
260
|
-
|
|
261
|
-
// Store/retrieve player name
|
|
262
|
-
identity.setPlayerName('PlayerName');
|
|
263
|
-
const name = identity.getPlayerName();
|
|
264
|
-
|
|
265
|
-
// Check if identity exists
|
|
266
|
-
if (identity.hasIdentity()) {
|
|
267
|
-
// returning player
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Clear identity (e.g., for "Sign Out")
|
|
271
|
-
identity.clear();
|
|
272
202
|
```
|
|
273
203
|
|
|
274
204
|
---
|
|
275
205
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
All methods throw `KeeperBoardError` on failure:
|
|
206
|
+
## Error Handling
|
|
279
207
|
|
|
280
208
|
```typescript
|
|
281
|
-
import { KeeperBoardError } from 'keeperboard
|
|
209
|
+
import { KeeperBoardError } from 'keeperboard';
|
|
282
210
|
|
|
283
211
|
try {
|
|
284
|
-
await client.submitScore(
|
|
212
|
+
await client.submitScore({ ... });
|
|
285
213
|
} catch (error) {
|
|
286
214
|
if (error instanceof KeeperBoardError) {
|
|
287
|
-
console.error(`Error [${error.code}]: ${error.message}`);
|
|
288
|
-
|
|
289
215
|
switch (error.code) {
|
|
290
216
|
case 'INVALID_API_KEY':
|
|
291
|
-
|
|
217
|
+
console.error('Check your API key');
|
|
292
218
|
break;
|
|
293
219
|
case 'NOT_FOUND':
|
|
294
|
-
|
|
220
|
+
console.error('Leaderboard not found');
|
|
295
221
|
break;
|
|
296
222
|
case 'INVALID_REQUEST':
|
|
297
|
-
|
|
223
|
+
console.error('Bad request:', error.message);
|
|
298
224
|
break;
|
|
225
|
+
default:
|
|
226
|
+
console.error('API error:', error.message);
|
|
299
227
|
}
|
|
300
228
|
}
|
|
301
229
|
}
|
|
@@ -303,89 +231,79 @@ try {
|
|
|
303
231
|
|
|
304
232
|
---
|
|
305
233
|
|
|
306
|
-
##
|
|
307
|
-
|
|
308
|
-
If your game has multiple leaderboards (e.g., per-level or per-mode):
|
|
234
|
+
## Phaser.js Integration
|
|
309
235
|
|
|
310
236
|
```typescript
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
237
|
+
import { KeeperBoardSession } from 'keeperboard';
|
|
238
|
+
|
|
239
|
+
// Initialize once at game start
|
|
240
|
+
const leaderboard = new KeeperBoardSession({
|
|
241
|
+
apiKey: import.meta.env.VITE_KEEPERBOARD_API_KEY,
|
|
242
|
+
leaderboard: 'main',
|
|
243
|
+
cache: { ttlMs: 30000 },
|
|
244
|
+
retry: { maxAgeMs: 86400000 },
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// In BootScene - prefetch and retry
|
|
248
|
+
class BootScene extends Phaser.Scene {
|
|
249
|
+
async create() {
|
|
250
|
+
leaderboard.prefetch();
|
|
251
|
+
await leaderboard.retryPendingScore();
|
|
252
|
+
this.scene.start('MenuScene');
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// In GameOverScene
|
|
257
|
+
class GameOverScene extends Phaser.Scene {
|
|
258
|
+
async create() {
|
|
259
|
+
const result = await leaderboard.submitScore(this.score);
|
|
260
|
+
if (result.success) {
|
|
261
|
+
this.showRank(result.rank, result.isNewHighScore);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const snapshot = await leaderboard.getSnapshot({ limit: 10 });
|
|
265
|
+
this.displayLeaderboard(snapshot.entries);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
319
268
|
```
|
|
320
269
|
|
|
321
270
|
---
|
|
322
271
|
|
|
323
|
-
##
|
|
272
|
+
## Utilities
|
|
273
|
+
|
|
274
|
+
### PlayerIdentity
|
|
275
|
+
|
|
276
|
+
Standalone helper for localStorage identity management:
|
|
324
277
|
|
|
325
278
|
```typescript
|
|
326
|
-
import {
|
|
279
|
+
import { PlayerIdentity } from 'keeperboard';
|
|
327
280
|
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
281
|
+
const identity = new PlayerIdentity({ keyPrefix: 'myapp_' });
|
|
282
|
+
const guid = identity.getOrCreatePlayerGuid();
|
|
283
|
+
identity.setPlayerName('ACE');
|
|
284
|
+
```
|
|
332
285
|
|
|
333
|
-
|
|
286
|
+
### Cache
|
|
334
287
|
|
|
335
|
-
|
|
336
|
-
private score: number = 0;
|
|
288
|
+
Generic TTL cache with deduplication:
|
|
337
289
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}
|
|
290
|
+
```typescript
|
|
291
|
+
import { Cache } from 'keeperboard';
|
|
341
292
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
293
|
+
const cache = new Cache<Data>(30000); // 30s TTL
|
|
294
|
+
const data = await cache.getOrFetch(() => fetchData());
|
|
295
|
+
```
|
|
345
296
|
|
|
346
|
-
|
|
347
|
-
const result = await client.submitScore(playerGuid, playerName, this.score);
|
|
297
|
+
### RetryQueue
|
|
348
298
|
|
|
349
|
-
|
|
350
|
-
this.add
|
|
351
|
-
.text(400, 200, `Your Rank: #${result.rank}`, { fontSize: '32px' })
|
|
352
|
-
.setOrigin(0.5);
|
|
299
|
+
localStorage-based retry for failed operations:
|
|
353
300
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
.text(400, 250, 'NEW HIGH SCORE!', { fontSize: '24px', color: '#ffff00' })
|
|
357
|
-
.setOrigin(0.5);
|
|
358
|
-
}
|
|
301
|
+
```typescript
|
|
302
|
+
import { RetryQueue } from 'keeperboard';
|
|
359
303
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
leaderboard.entries.forEach((entry, index) => {
|
|
364
|
-
const isMe = entry.player_guid === playerGuid;
|
|
365
|
-
const color = isMe ? '#00ff00' : '#ffffff';
|
|
366
|
-
|
|
367
|
-
this.add
|
|
368
|
-
.text(
|
|
369
|
-
400,
|
|
370
|
-
350 + index * 30,
|
|
371
|
-
`#${entry.rank} ${entry.player_name}: ${entry.score}`,
|
|
372
|
-
{ fontSize: '18px', color }
|
|
373
|
-
)
|
|
374
|
-
.setOrigin(0.5);
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
// Show player's rank if not in top 10
|
|
378
|
-
const player = await client.getPlayerRank(playerGuid);
|
|
379
|
-
if (player && player.rank > 10) {
|
|
380
|
-
this.add
|
|
381
|
-
.text(400, 660, `... #${player.rank} ${playerName}: ${player.score}`, {
|
|
382
|
-
fontSize: '18px',
|
|
383
|
-
color: '#00ff00',
|
|
384
|
-
})
|
|
385
|
-
.setOrigin(0.5);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
304
|
+
const queue = new RetryQueue('myapp_retry', 86400000); // 24h max age
|
|
305
|
+
queue.save(1500, { level: 5 });
|
|
306
|
+
const pending = queue.get(); // { score: 1500, metadata: {...} } or null
|
|
389
307
|
```
|
|
390
308
|
|
|
391
309
|
---
|
|
@@ -396,34 +314,17 @@ class GameOverScene extends Phaser.Scene {
|
|
|
396
314
|
# Install dependencies
|
|
397
315
|
npm install
|
|
398
316
|
|
|
317
|
+
# Run tests (requires local KeeperBoard server + Supabase)
|
|
318
|
+
npm test
|
|
319
|
+
|
|
399
320
|
# Type check
|
|
400
321
|
npm run typecheck
|
|
401
322
|
|
|
402
323
|
# Build
|
|
403
324
|
npm run build
|
|
404
|
-
|
|
405
|
-
# Run tests (requires .env with Supabase credentials)
|
|
406
|
-
npm test
|
|
407
|
-
|
|
408
|
-
# Clean
|
|
409
|
-
npm run clean
|
|
410
325
|
```
|
|
411
326
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
Copy `.env.example` to `.env` and fill in your Supabase credentials:
|
|
415
|
-
|
|
416
|
-
```bash
|
|
417
|
-
cp .env.example .env
|
|
418
|
-
```
|
|
419
|
-
|
|
420
|
-
Then run:
|
|
421
|
-
|
|
422
|
-
```bash
|
|
423
|
-
npm test
|
|
424
|
-
```
|
|
425
|
-
|
|
426
|
-
---
|
|
327
|
+
See [MIGRATION.md](./MIGRATION.md) for upgrading from v1.x.
|
|
427
328
|
|
|
428
329
|
## License
|
|
429
330
|
|