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