@flare-engine/leaderboards 0.4.6 → 0.4.8
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/index.d.mts +5 -11
- package/dist/index.d.mts.map +1 -1
- package/dist/index.d.ts +5 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
//#region src/provider.d.ts
|
|
2
1
|
/** Opaque leaderboard identifier — adapter-defined (e.g., "daily_high_score"). */
|
|
3
2
|
type LeaderboardId = string;
|
|
4
3
|
/** A single leaderboard row. */
|
|
@@ -24,20 +23,16 @@ interface LeaderboardsProvider {
|
|
|
24
23
|
* Default no-op provider. Submissions resolve silently; queries return empty
|
|
25
24
|
* arrays and `null` rank.
|
|
26
25
|
*/
|
|
27
|
-
declare const NoopLeaderboardsProvider: LeaderboardsProvider;
|
|
28
|
-
//#endregion
|
|
29
|
-
//#region src/context.d.ts
|
|
26
|
+
export declare const NoopLeaderboardsProvider: LeaderboardsProvider;
|
|
30
27
|
/**
|
|
31
28
|
* React context carrying the active `LeaderboardsProvider`. Defaults to
|
|
32
29
|
* `NoopLeaderboardsProvider` so `useLeaderboards()` never throws on a bare
|
|
33
30
|
* tree — consumers wrap their app in a `<LeaderboardsContext.Provider>` with
|
|
34
31
|
* a real adapter when they need one.
|
|
35
32
|
*/
|
|
36
|
-
declare const LeaderboardsContext: import("react").Context<LeaderboardsProvider>;
|
|
33
|
+
export declare const LeaderboardsContext: import("react").Context<LeaderboardsProvider>;
|
|
37
34
|
/** Hook returning the current `LeaderboardsProvider`. */
|
|
38
|
-
declare function useLeaderboards(): LeaderboardsProvider;
|
|
39
|
-
//#endregion
|
|
40
|
-
//#region src/memory.d.ts
|
|
35
|
+
export declare function useLeaderboards(): LeaderboardsProvider;
|
|
41
36
|
/** A competitor row used to pre-populate an in-memory board (see {@link MemoryLeaderboardsOptions.seed}). */
|
|
42
37
|
interface SeedScore {
|
|
43
38
|
playerId: string;
|
|
@@ -75,7 +70,6 @@ interface MemoryLeaderboardsOptions {
|
|
|
75
70
|
* - `getPlayerRank(id)` returns the local player's 1-based rank under the same
|
|
76
71
|
* ordering, or `null` when the local player has no entry on that board.
|
|
77
72
|
*/
|
|
78
|
-
declare function createMemoryLeaderboards(opts?: MemoryLeaderboardsOptions): LeaderboardsProvider;
|
|
79
|
-
|
|
80
|
-
export { type LeaderboardId, LeaderboardsContext, type LeaderboardsProvider, type MemoryLeaderboardsOptions, NoopLeaderboardsProvider, type ScoreEntry, type SeedScore, createMemoryLeaderboards, useLeaderboards };
|
|
73
|
+
export declare function createMemoryLeaderboards(opts?: MemoryLeaderboardsOptions): LeaderboardsProvider;
|
|
74
|
+
export type { LeaderboardId, LeaderboardsProvider, MemoryLeaderboardsOptions, ScoreEntry, SeedScore };
|
|
81
75
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/provider.ts","../src/context.ts","../src/memory.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/provider.ts","../src/context.ts","../src/memory.ts"],"mappings":";KACY;;UAGK;EACf;EACA;EACA;EACA;EACA,OAAO;;;;;;;;UASQ;EACf,YAAY,IAAI,eAAe,eAAe,OAAO,0BAA0B;EAC/E,QAAQ,IAAI,eAAe,YAAY,iBAAiB;EACxD,cAAc,IAAI,gBAAgB;;;;;;qBAOvB,0BAA0B;;;;;;;qBCnB1B,qCAAmB,QAAA;;wBAGhB,mBAAmB;;UCTlB;EACf;EACA;EACA;EACA,OAAO;;;UAIQ;;EAEf;;EAEA;;EAEA,OAAO,SAAS,OAAO,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;wBA+CjC,yBAAyB,OAAO,4BAA4B"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
//#region src/provider.d.ts
|
|
2
1
|
/** Opaque leaderboard identifier — adapter-defined (e.g., "daily_high_score"). */
|
|
3
2
|
type LeaderboardId = string;
|
|
4
3
|
/** A single leaderboard row. */
|
|
@@ -24,20 +23,16 @@ interface LeaderboardsProvider {
|
|
|
24
23
|
* Default no-op provider. Submissions resolve silently; queries return empty
|
|
25
24
|
* arrays and `null` rank.
|
|
26
25
|
*/
|
|
27
|
-
declare const NoopLeaderboardsProvider: LeaderboardsProvider;
|
|
28
|
-
//#endregion
|
|
29
|
-
//#region src/context.d.ts
|
|
26
|
+
export declare const NoopLeaderboardsProvider: LeaderboardsProvider;
|
|
30
27
|
/**
|
|
31
28
|
* React context carrying the active `LeaderboardsProvider`. Defaults to
|
|
32
29
|
* `NoopLeaderboardsProvider` so `useLeaderboards()` never throws on a bare
|
|
33
30
|
* tree — consumers wrap their app in a `<LeaderboardsContext.Provider>` with
|
|
34
31
|
* a real adapter when they need one.
|
|
35
32
|
*/
|
|
36
|
-
declare const LeaderboardsContext: import("react").Context<LeaderboardsProvider>;
|
|
33
|
+
export declare const LeaderboardsContext: import("react").Context<LeaderboardsProvider>;
|
|
37
34
|
/** Hook returning the current `LeaderboardsProvider`. */
|
|
38
|
-
declare function useLeaderboards(): LeaderboardsProvider;
|
|
39
|
-
//#endregion
|
|
40
|
-
//#region src/memory.d.ts
|
|
35
|
+
export declare function useLeaderboards(): LeaderboardsProvider;
|
|
41
36
|
/** A competitor row used to pre-populate an in-memory board (see {@link MemoryLeaderboardsOptions.seed}). */
|
|
42
37
|
interface SeedScore {
|
|
43
38
|
playerId: string;
|
|
@@ -75,7 +70,6 @@ interface MemoryLeaderboardsOptions {
|
|
|
75
70
|
* - `getPlayerRank(id)` returns the local player's 1-based rank under the same
|
|
76
71
|
* ordering, or `null` when the local player has no entry on that board.
|
|
77
72
|
*/
|
|
78
|
-
declare function createMemoryLeaderboards(opts?: MemoryLeaderboardsOptions): LeaderboardsProvider;
|
|
79
|
-
|
|
80
|
-
export { type LeaderboardId, LeaderboardsContext, type LeaderboardsProvider, type MemoryLeaderboardsOptions, NoopLeaderboardsProvider, type ScoreEntry, type SeedScore, createMemoryLeaderboards, useLeaderboards };
|
|
73
|
+
export declare function createMemoryLeaderboards(opts?: MemoryLeaderboardsOptions): LeaderboardsProvider;
|
|
74
|
+
export type { LeaderboardId, LeaderboardsProvider, MemoryLeaderboardsOptions, ScoreEntry, SeedScore };
|
|
81
75
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/provider.ts","../src/context.ts","../src/memory.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/provider.ts","../src/context.ts","../src/memory.ts"],"mappings":";KACY;;UAGK;EACf;EACA;EACA;EACA;EACA,OAAO;;;;;;;;UASQ;EACf,YAAY,IAAI,eAAe,eAAe,OAAO,0BAA0B;EAC/E,QAAQ,IAAI,eAAe,YAAY,iBAAiB;EACxD,cAAc,IAAI,gBAAgB;;;;;;qBAOvB,0BAA0B;;;;;;;qBCnB1B,qCAAmB,QAAA;;wBAGhB,mBAAmB;;UCTlB;EACf;EACA;EACA;EACA,OAAO;;;UAIQ;;EAEf;;EAEA;;EAEA,OAAO,SAAS,OAAO,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;wBA+CjC,yBAAyB,OAAO,4BAA4B"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
let react = require("react");
|
|
3
|
-
|
|
3
|
+
// src/provider.ts
|
|
4
4
|
/**
|
|
5
5
|
* Default no-op provider. Submissions resolve silently; queries return empty
|
|
6
6
|
* arrays and `null` rank.
|
|
@@ -14,8 +14,7 @@ const NoopLeaderboardsProvider = {
|
|
|
14
14
|
return null;
|
|
15
15
|
}
|
|
16
16
|
};
|
|
17
|
-
|
|
18
|
-
//#region src/context.ts
|
|
17
|
+
// src/context.ts
|
|
19
18
|
/**
|
|
20
19
|
* React context carrying the active `LeaderboardsProvider`. Defaults to
|
|
21
20
|
* `NoopLeaderboardsProvider` so `useLeaderboards()` never throws on a bare
|
|
@@ -27,8 +26,7 @@ const LeaderboardsContext = (0, react.createContext)(NoopLeaderboardsProvider);
|
|
|
27
26
|
function useLeaderboards() {
|
|
28
27
|
return (0, react.useContext)(LeaderboardsContext);
|
|
29
28
|
}
|
|
30
|
-
|
|
31
|
-
//#region src/memory.ts
|
|
29
|
+
// src/memory.ts
|
|
32
30
|
/** Sort a board DESCENDING by score (ties by insertion order) and assign sequential 1-based ranks. */
|
|
33
31
|
function ranked(entries) {
|
|
34
32
|
return entries.slice().sort((a, b) => b.score - a.score || a.order - b.order).map((e, i) => ({
|
|
@@ -109,7 +107,6 @@ function createMemoryLeaderboards(opts) {
|
|
|
109
107
|
}
|
|
110
108
|
};
|
|
111
109
|
}
|
|
112
|
-
//#endregion
|
|
113
110
|
exports.LeaderboardsContext = LeaderboardsContext;
|
|
114
111
|
exports.NoopLeaderboardsProvider = NoopLeaderboardsProvider;
|
|
115
112
|
exports.createMemoryLeaderboards = createMemoryLeaderboards;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/provider.ts","../src/context.ts","../src/memory.ts"],"sourcesContent":["/** Opaque leaderboard identifier — adapter-defined (e.g., \"daily_high_score\"). */\nexport type LeaderboardId = string;\n\n/** A single leaderboard row. */\nexport interface ScoreEntry {\n playerId: string;\n displayName: string;\n score: number;\n rank: number;\n meta?: Record<string, unknown>;\n}\n\n/**\n * Leaderboard provider interface. Consumers supply a concrete adapter (Game\n * Center, Google Play Games, Steam, custom backend) at the app boundary.\n * 0.1.0 ships the interface + a no-op default so downstream packages can\n * depend on it without forcing a platform choice.\n */\nexport interface LeaderboardsProvider {\n submitScore(id: LeaderboardId, score: number, meta?: Record<string, unknown>): Promise<void>;\n getTopN(id: LeaderboardId, n: number): Promise<readonly ScoreEntry[]>;\n getPlayerRank(id: LeaderboardId): Promise<number | null>;\n}\n\n/**\n * Default no-op provider. Submissions resolve silently; queries return empty\n * arrays and `null` rank.\n */\nexport const NoopLeaderboardsProvider: LeaderboardsProvider = {\n async submitScore(): Promise<void> {\n // no-op\n },\n async getTopN(): Promise<readonly ScoreEntry[]> {\n return [];\n },\n async getPlayerRank(): Promise<number | null> {\n return null;\n },\n};\n","import { createContext, useContext } from 'react';\nimport { type LeaderboardsProvider, NoopLeaderboardsProvider } from './provider.js';\n\n/**\n * React context carrying the active `LeaderboardsProvider`. Defaults to\n * `NoopLeaderboardsProvider` so `useLeaderboards()` never throws on a bare\n * tree — consumers wrap their app in a `<LeaderboardsContext.Provider>` with\n * a real adapter when they need one.\n */\nexport const LeaderboardsContext = createContext<LeaderboardsProvider>(NoopLeaderboardsProvider);\n\n/** Hook returning the current `LeaderboardsProvider`. */\nexport function useLeaderboards(): LeaderboardsProvider {\n return useContext(LeaderboardsContext);\n}\n","import type { LeaderboardId, LeaderboardsProvider, ScoreEntry } from './provider.js';\n\n/** A competitor row used to pre-populate an in-memory board (see {@link MemoryLeaderboardsOptions.seed}). */\nexport interface SeedScore {\n playerId: string;\n displayName: string;\n score: number;\n meta?: Record<string, unknown>;\n}\n\n/** Options for {@link createMemoryLeaderboards}. */\nexport interface MemoryLeaderboardsOptions {\n /** Identity all local submissions record under. Defaults to `'local'`. */\n playerId?: string;\n /** Display name paired with `playerId`. Defaults to `'Player'`. */\n displayName?: string;\n /** Per-board competitor rows, pre-populated so queries are meaningful without a backend. */\n seed?: Readonly<Record<LeaderboardId, readonly SeedScore[]>>;\n}\n\ninterface InternalEntry {\n playerId: string;\n displayName: string;\n score: number;\n meta?: Record<string, unknown>;\n /** Monotonic insertion order; earlier submissions break score ties in their favour. */\n order: number;\n}\n\n/** Sort a board DESCENDING by score (ties by insertion order) and assign sequential 1-based ranks. */\nfunction ranked(entries: readonly InternalEntry[]): ScoreEntry[] {\n return entries\n .slice()\n .sort((a, b) => b.score - a.score || a.order - b.order)\n .map((e, i) => ({\n playerId: e.playerId,\n displayName: e.displayName,\n score: e.score,\n rank: i + 1,\n meta: e.meta,\n }));\n}\n\n/**\n * Create an in-memory {@link LeaderboardsProvider} — a backend-free adapter for\n * local development, tests, and single-player games.\n *\n * Semantics (the adapter-author contract):\n * - The provider interface's `submitScore(id, score, meta?)` carries no\n * playerId, so every submission records under one local identity:\n * `opts.playerId` (default `'local'`) / `opts.displayName` (default\n * `'Player'`).\n * - Boards are per-id in-memory lists. `opts.seed` pre-populates competitor\n * entries so `getTopN` / `getPlayerRank` are meaningful without a backend.\n * - `submitScore` keeps the local player's BEST (max) score per board: the\n * first submission inserts an entry; a later HIGHER score replaces both the\n * score AND the meta; a lower-or-equal score is ignored. Each entry carries a\n * monotonic insertion counter for stable tie-breaking.\n * - `getTopN(id, n)` ranks the board DESCENDING by score, ties broken by\n * insertion order (earlier submission ranks higher). Ranks are sequential\n * 1-based with no sharing. `n <= 0` and unknown boards return `[]`.\n * - `getPlayerRank(id)` returns the local player's 1-based rank under the same\n * ordering, or `null` when the local player has no entry on that board.\n */\nexport function createMemoryLeaderboards(opts?: MemoryLeaderboardsOptions): LeaderboardsProvider {\n const localId = opts?.playerId ?? 'local';\n const localName = opts?.displayName ?? 'Player';\n const boards = new Map<LeaderboardId, InternalEntry[]>();\n let nextOrder = 0;\n\n if (opts?.seed !== undefined) {\n for (const [id, rows] of Object.entries(opts.seed)) {\n const board: InternalEntry[] = rows.map((row) => ({\n playerId: row.playerId,\n displayName: row.displayName,\n score: row.score,\n meta: row.meta,\n order: nextOrder++,\n }));\n boards.set(id, board);\n }\n }\n\n return {\n async submitScore(id, score, meta): Promise<void> {\n let board = boards.get(id);\n if (board === undefined) {\n board = [];\n boards.set(id, board);\n }\n const existing = board.find((e) => e.playerId === localId);\n if (existing === undefined) {\n board.push({\n playerId: localId,\n displayName: localName,\n score,\n meta,\n order: nextOrder++,\n });\n } else if (score > existing.score) {\n existing.score = score;\n existing.meta = meta;\n }\n },\n\n async getTopN(id, n): Promise<readonly ScoreEntry[]> {\n if (n <= 0) {\n return [];\n }\n const board = boards.get(id);\n if (board === undefined) {\n return [];\n }\n return ranked(board).slice(0, n);\n },\n\n async getPlayerRank(id): Promise<number | null> {\n const board = boards.get(id);\n if (board === undefined) {\n return null;\n }\n const found = ranked(board).find((e) => e.playerId === localId);\n return found === undefined ? null : found.rank;\n },\n };\n}\n"],"mappings":";;;;;;;AA4BA,MAAa,2BAAiD;CAC5D,MAAM,cAA6B,CAEnC;CACA,MAAM,UAA0C;EAC9C,OAAO,CAAC;CACV;CACA,MAAM,gBAAwC;EAC5C,OAAO;CACT;AACF
|
|
1
|
+
{"version":3,"file":"index.js","names":["createContext","useContext"],"sources":["../src/provider.ts","../src/context.ts","../src/memory.ts"],"sourcesContent":["/** Opaque leaderboard identifier — adapter-defined (e.g., \"daily_high_score\"). */\nexport type LeaderboardId = string;\n\n/** A single leaderboard row. */\nexport interface ScoreEntry {\n playerId: string;\n displayName: string;\n score: number;\n rank: number;\n meta?: Record<string, unknown>;\n}\n\n/**\n * Leaderboard provider interface. Consumers supply a concrete adapter (Game\n * Center, Google Play Games, Steam, custom backend) at the app boundary.\n * 0.1.0 ships the interface + a no-op default so downstream packages can\n * depend on it without forcing a platform choice.\n */\nexport interface LeaderboardsProvider {\n submitScore(id: LeaderboardId, score: number, meta?: Record<string, unknown>): Promise<void>;\n getTopN(id: LeaderboardId, n: number): Promise<readonly ScoreEntry[]>;\n getPlayerRank(id: LeaderboardId): Promise<number | null>;\n}\n\n/**\n * Default no-op provider. Submissions resolve silently; queries return empty\n * arrays and `null` rank.\n */\nexport const NoopLeaderboardsProvider: LeaderboardsProvider = {\n async submitScore(): Promise<void> {\n // no-op\n },\n async getTopN(): Promise<readonly ScoreEntry[]> {\n return [];\n },\n async getPlayerRank(): Promise<number | null> {\n return null;\n },\n};\n","import { createContext, useContext } from 'react';\nimport { type LeaderboardsProvider, NoopLeaderboardsProvider } from './provider.js';\n\n/**\n * React context carrying the active `LeaderboardsProvider`. Defaults to\n * `NoopLeaderboardsProvider` so `useLeaderboards()` never throws on a bare\n * tree — consumers wrap their app in a `<LeaderboardsContext.Provider>` with\n * a real adapter when they need one.\n */\nexport const LeaderboardsContext = createContext<LeaderboardsProvider>(NoopLeaderboardsProvider);\n\n/** Hook returning the current `LeaderboardsProvider`. */\nexport function useLeaderboards(): LeaderboardsProvider {\n return useContext(LeaderboardsContext);\n}\n","import type { LeaderboardId, LeaderboardsProvider, ScoreEntry } from './provider.js';\n\n/** A competitor row used to pre-populate an in-memory board (see {@link MemoryLeaderboardsOptions.seed}). */\nexport interface SeedScore {\n playerId: string;\n displayName: string;\n score: number;\n meta?: Record<string, unknown>;\n}\n\n/** Options for {@link createMemoryLeaderboards}. */\nexport interface MemoryLeaderboardsOptions {\n /** Identity all local submissions record under. Defaults to `'local'`. */\n playerId?: string;\n /** Display name paired with `playerId`. Defaults to `'Player'`. */\n displayName?: string;\n /** Per-board competitor rows, pre-populated so queries are meaningful without a backend. */\n seed?: Readonly<Record<LeaderboardId, readonly SeedScore[]>>;\n}\n\ninterface InternalEntry {\n playerId: string;\n displayName: string;\n score: number;\n meta?: Record<string, unknown>;\n /** Monotonic insertion order; earlier submissions break score ties in their favour. */\n order: number;\n}\n\n/** Sort a board DESCENDING by score (ties by insertion order) and assign sequential 1-based ranks. */\nfunction ranked(entries: readonly InternalEntry[]): ScoreEntry[] {\n return entries\n .slice()\n .sort((a, b) => b.score - a.score || a.order - b.order)\n .map((e, i) => ({\n playerId: e.playerId,\n displayName: e.displayName,\n score: e.score,\n rank: i + 1,\n meta: e.meta,\n }));\n}\n\n/**\n * Create an in-memory {@link LeaderboardsProvider} — a backend-free adapter for\n * local development, tests, and single-player games.\n *\n * Semantics (the adapter-author contract):\n * - The provider interface's `submitScore(id, score, meta?)` carries no\n * playerId, so every submission records under one local identity:\n * `opts.playerId` (default `'local'`) / `opts.displayName` (default\n * `'Player'`).\n * - Boards are per-id in-memory lists. `opts.seed` pre-populates competitor\n * entries so `getTopN` / `getPlayerRank` are meaningful without a backend.\n * - `submitScore` keeps the local player's BEST (max) score per board: the\n * first submission inserts an entry; a later HIGHER score replaces both the\n * score AND the meta; a lower-or-equal score is ignored. Each entry carries a\n * monotonic insertion counter for stable tie-breaking.\n * - `getTopN(id, n)` ranks the board DESCENDING by score, ties broken by\n * insertion order (earlier submission ranks higher). Ranks are sequential\n * 1-based with no sharing. `n <= 0` and unknown boards return `[]`.\n * - `getPlayerRank(id)` returns the local player's 1-based rank under the same\n * ordering, or `null` when the local player has no entry on that board.\n */\nexport function createMemoryLeaderboards(opts?: MemoryLeaderboardsOptions): LeaderboardsProvider {\n const localId = opts?.playerId ?? 'local';\n const localName = opts?.displayName ?? 'Player';\n const boards = new Map<LeaderboardId, InternalEntry[]>();\n let nextOrder = 0;\n\n if (opts?.seed !== undefined) {\n for (const [id, rows] of Object.entries(opts.seed)) {\n const board: InternalEntry[] = rows.map((row) => ({\n playerId: row.playerId,\n displayName: row.displayName,\n score: row.score,\n meta: row.meta,\n order: nextOrder++,\n }));\n boards.set(id, board);\n }\n }\n\n return {\n async submitScore(id, score, meta): Promise<void> {\n let board = boards.get(id);\n if (board === undefined) {\n board = [];\n boards.set(id, board);\n }\n const existing = board.find((e) => e.playerId === localId);\n if (existing === undefined) {\n board.push({\n playerId: localId,\n displayName: localName,\n score,\n meta,\n order: nextOrder++,\n });\n } else if (score > existing.score) {\n existing.score = score;\n existing.meta = meta;\n }\n },\n\n async getTopN(id, n): Promise<readonly ScoreEntry[]> {\n if (n <= 0) {\n return [];\n }\n const board = boards.get(id);\n if (board === undefined) {\n return [];\n }\n return ranked(board).slice(0, n);\n },\n\n async getPlayerRank(id): Promise<number | null> {\n const board = boards.get(id);\n if (board === undefined) {\n return null;\n }\n const found = ranked(board).find((e) => e.playerId === localId);\n return found === undefined ? null : found.rank;\n },\n };\n}\n"],"mappings":";;;;;;;AA4BA,MAAa,2BAAiD;CAC5D,MAAM,cAA6B,CAEnC;CACA,MAAM,UAA0C;EAC9C,OAAO,CAAC;CACV;CACA,MAAM,gBAAwC;EAC5C,OAAO;CACT;AACF;;;;;;;;AC7BA,MAAa,uBAAA,GAAsBA,MAAAA,cAAAA,CAAoC,wBAAwB;;AAG/F,SAAgB,kBAAwC;CACtD,QAAA,GAAOC,MAAAA,WAAAA,CAAW,mBAAmB;AACvC;;;ACgBA,SAAS,OAAO,SAAiD;CAC/D,OAAO,QACJ,MAAM,CAAC,CACP,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,CAAC,CACtD,KAAK,GAAG,OAAO;EACd,UAAU,EAAE;EACZ,aAAa,EAAE;EACf,OAAO,EAAE;EACT,MAAM,IAAI;EACV,MAAM,EAAE;CACV,EAAE;AACN;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,yBAAyB,MAAwD;CAC/F,MAAM,UAAU,MAAM,YAAY;CAClC,MAAM,YAAY,MAAM,eAAe;CACvC,MAAM,yBAAS,IAAI,IAAoC;CACvD,IAAI,YAAY;CAEhB,IAAI,MAAM,SAAS,KAAA,GACjB,KAAK,MAAM,CAAC,IAAI,SAAS,OAAO,QAAQ,KAAK,IAAI,GAAG;EAClD,MAAM,QAAyB,KAAK,KAAK,SAAS;GAChD,UAAU,IAAI;GACd,aAAa,IAAI;GACjB,OAAO,IAAI;GACX,MAAM,IAAI;GACV,OAAO;EACT,EAAE;EACF,OAAO,IAAI,IAAI,KAAK;CACtB;CAGF,OAAO;EACL,MAAM,YAAY,IAAI,OAAO,MAAqB;GAChD,IAAI,QAAQ,OAAO,IAAI,EAAE;GACzB,IAAI,UAAU,KAAA,GAAW;IACvB,QAAQ,CAAC;IACT,OAAO,IAAI,IAAI,KAAK;GACtB;GACA,MAAM,WAAW,MAAM,MAAM,MAAM,EAAE,aAAa,OAAO;GACzD,IAAI,aAAa,KAAA,GACf,MAAM,KAAK;IACT,UAAU;IACV,aAAa;IACb;IACA;IACA,OAAO;GACT,CAAC;QACI,IAAI,QAAQ,SAAS,OAAO;IACjC,SAAS,QAAQ;IACjB,SAAS,OAAO;GAClB;EACF;EAEA,MAAM,QAAQ,IAAI,GAAmC;GACnD,IAAI,KAAK,GACP,OAAO,CAAC;GAEV,MAAM,QAAQ,OAAO,IAAI,EAAE;GAC3B,IAAI,UAAU,KAAA,GACZ,OAAO,CAAC;GAEV,OAAO,OAAO,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC;EACjC;EAEA,MAAM,cAAc,IAA4B;GAC9C,MAAM,QAAQ,OAAO,IAAI,EAAE;GAC3B,IAAI,UAAU,KAAA,GACZ,OAAO;GAET,MAAM,QAAQ,OAAO,KAAK,CAAC,CAAC,MAAM,MAAM,EAAE,aAAa,OAAO;GAC9D,OAAO,UAAU,KAAA,IAAY,OAAO,MAAM;EAC5C;CACF;AACF"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createContext, useContext } from "react";
|
|
2
|
-
|
|
2
|
+
// src/provider.ts
|
|
3
3
|
/**
|
|
4
4
|
* Default no-op provider. Submissions resolve silently; queries return empty
|
|
5
5
|
* arrays and `null` rank.
|
|
@@ -13,8 +13,7 @@ const NoopLeaderboardsProvider = {
|
|
|
13
13
|
return null;
|
|
14
14
|
}
|
|
15
15
|
};
|
|
16
|
-
|
|
17
|
-
//#region src/context.ts
|
|
16
|
+
// src/context.ts
|
|
18
17
|
/**
|
|
19
18
|
* React context carrying the active `LeaderboardsProvider`. Defaults to
|
|
20
19
|
* `NoopLeaderboardsProvider` so `useLeaderboards()` never throws on a bare
|
|
@@ -26,8 +25,7 @@ const LeaderboardsContext = createContext(NoopLeaderboardsProvider);
|
|
|
26
25
|
function useLeaderboards() {
|
|
27
26
|
return useContext(LeaderboardsContext);
|
|
28
27
|
}
|
|
29
|
-
|
|
30
|
-
//#region src/memory.ts
|
|
28
|
+
// src/memory.ts
|
|
31
29
|
/** Sort a board DESCENDING by score (ties by insertion order) and assign sequential 1-based ranks. */
|
|
32
30
|
function ranked(entries) {
|
|
33
31
|
return entries.slice().sort((a, b) => b.score - a.score || a.order - b.order).map((e, i) => ({
|
|
@@ -108,7 +106,6 @@ function createMemoryLeaderboards(opts) {
|
|
|
108
106
|
}
|
|
109
107
|
};
|
|
110
108
|
}
|
|
111
|
-
//#endregion
|
|
112
109
|
export { LeaderboardsContext, NoopLeaderboardsProvider, createMemoryLeaderboards, useLeaderboards };
|
|
113
110
|
|
|
114
111
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/provider.ts","../src/context.ts","../src/memory.ts"],"sourcesContent":["/** Opaque leaderboard identifier — adapter-defined (e.g., \"daily_high_score\"). */\nexport type LeaderboardId = string;\n\n/** A single leaderboard row. */\nexport interface ScoreEntry {\n playerId: string;\n displayName: string;\n score: number;\n rank: number;\n meta?: Record<string, unknown>;\n}\n\n/**\n * Leaderboard provider interface. Consumers supply a concrete adapter (Game\n * Center, Google Play Games, Steam, custom backend) at the app boundary.\n * 0.1.0 ships the interface + a no-op default so downstream packages can\n * depend on it without forcing a platform choice.\n */\nexport interface LeaderboardsProvider {\n submitScore(id: LeaderboardId, score: number, meta?: Record<string, unknown>): Promise<void>;\n getTopN(id: LeaderboardId, n: number): Promise<readonly ScoreEntry[]>;\n getPlayerRank(id: LeaderboardId): Promise<number | null>;\n}\n\n/**\n * Default no-op provider. Submissions resolve silently; queries return empty\n * arrays and `null` rank.\n */\nexport const NoopLeaderboardsProvider: LeaderboardsProvider = {\n async submitScore(): Promise<void> {\n // no-op\n },\n async getTopN(): Promise<readonly ScoreEntry[]> {\n return [];\n },\n async getPlayerRank(): Promise<number | null> {\n return null;\n },\n};\n","import { createContext, useContext } from 'react';\nimport { type LeaderboardsProvider, NoopLeaderboardsProvider } from './provider.js';\n\n/**\n * React context carrying the active `LeaderboardsProvider`. Defaults to\n * `NoopLeaderboardsProvider` so `useLeaderboards()` never throws on a bare\n * tree — consumers wrap their app in a `<LeaderboardsContext.Provider>` with\n * a real adapter when they need one.\n */\nexport const LeaderboardsContext = createContext<LeaderboardsProvider>(NoopLeaderboardsProvider);\n\n/** Hook returning the current `LeaderboardsProvider`. */\nexport function useLeaderboards(): LeaderboardsProvider {\n return useContext(LeaderboardsContext);\n}\n","import type { LeaderboardId, LeaderboardsProvider, ScoreEntry } from './provider.js';\n\n/** A competitor row used to pre-populate an in-memory board (see {@link MemoryLeaderboardsOptions.seed}). */\nexport interface SeedScore {\n playerId: string;\n displayName: string;\n score: number;\n meta?: Record<string, unknown>;\n}\n\n/** Options for {@link createMemoryLeaderboards}. */\nexport interface MemoryLeaderboardsOptions {\n /** Identity all local submissions record under. Defaults to `'local'`. */\n playerId?: string;\n /** Display name paired with `playerId`. Defaults to `'Player'`. */\n displayName?: string;\n /** Per-board competitor rows, pre-populated so queries are meaningful without a backend. */\n seed?: Readonly<Record<LeaderboardId, readonly SeedScore[]>>;\n}\n\ninterface InternalEntry {\n playerId: string;\n displayName: string;\n score: number;\n meta?: Record<string, unknown>;\n /** Monotonic insertion order; earlier submissions break score ties in their favour. */\n order: number;\n}\n\n/** Sort a board DESCENDING by score (ties by insertion order) and assign sequential 1-based ranks. */\nfunction ranked(entries: readonly InternalEntry[]): ScoreEntry[] {\n return entries\n .slice()\n .sort((a, b) => b.score - a.score || a.order - b.order)\n .map((e, i) => ({\n playerId: e.playerId,\n displayName: e.displayName,\n score: e.score,\n rank: i + 1,\n meta: e.meta,\n }));\n}\n\n/**\n * Create an in-memory {@link LeaderboardsProvider} — a backend-free adapter for\n * local development, tests, and single-player games.\n *\n * Semantics (the adapter-author contract):\n * - The provider interface's `submitScore(id, score, meta?)` carries no\n * playerId, so every submission records under one local identity:\n * `opts.playerId` (default `'local'`) / `opts.displayName` (default\n * `'Player'`).\n * - Boards are per-id in-memory lists. `opts.seed` pre-populates competitor\n * entries so `getTopN` / `getPlayerRank` are meaningful without a backend.\n * - `submitScore` keeps the local player's BEST (max) score per board: the\n * first submission inserts an entry; a later HIGHER score replaces both the\n * score AND the meta; a lower-or-equal score is ignored. Each entry carries a\n * monotonic insertion counter for stable tie-breaking.\n * - `getTopN(id, n)` ranks the board DESCENDING by score, ties broken by\n * insertion order (earlier submission ranks higher). Ranks are sequential\n * 1-based with no sharing. `n <= 0` and unknown boards return `[]`.\n * - `getPlayerRank(id)` returns the local player's 1-based rank under the same\n * ordering, or `null` when the local player has no entry on that board.\n */\nexport function createMemoryLeaderboards(opts?: MemoryLeaderboardsOptions): LeaderboardsProvider {\n const localId = opts?.playerId ?? 'local';\n const localName = opts?.displayName ?? 'Player';\n const boards = new Map<LeaderboardId, InternalEntry[]>();\n let nextOrder = 0;\n\n if (opts?.seed !== undefined) {\n for (const [id, rows] of Object.entries(opts.seed)) {\n const board: InternalEntry[] = rows.map((row) => ({\n playerId: row.playerId,\n displayName: row.displayName,\n score: row.score,\n meta: row.meta,\n order: nextOrder++,\n }));\n boards.set(id, board);\n }\n }\n\n return {\n async submitScore(id, score, meta): Promise<void> {\n let board = boards.get(id);\n if (board === undefined) {\n board = [];\n boards.set(id, board);\n }\n const existing = board.find((e) => e.playerId === localId);\n if (existing === undefined) {\n board.push({\n playerId: localId,\n displayName: localName,\n score,\n meta,\n order: nextOrder++,\n });\n } else if (score > existing.score) {\n existing.score = score;\n existing.meta = meta;\n }\n },\n\n async getTopN(id, n): Promise<readonly ScoreEntry[]> {\n if (n <= 0) {\n return [];\n }\n const board = boards.get(id);\n if (board === undefined) {\n return [];\n }\n return ranked(board).slice(0, n);\n },\n\n async getPlayerRank(id): Promise<number | null> {\n const board = boards.get(id);\n if (board === undefined) {\n return null;\n }\n const found = ranked(board).find((e) => e.playerId === localId);\n return found === undefined ? null : found.rank;\n },\n };\n}\n"],"mappings":";;;;;;AA4BA,MAAa,2BAAiD;CAC5D,MAAM,cAA6B,CAEnC;CACA,MAAM,UAA0C;EAC9C,OAAO,CAAC;CACV;CACA,MAAM,gBAAwC;EAC5C,OAAO;CACT;AACF
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/provider.ts","../src/context.ts","../src/memory.ts"],"sourcesContent":["/** Opaque leaderboard identifier — adapter-defined (e.g., \"daily_high_score\"). */\nexport type LeaderboardId = string;\n\n/** A single leaderboard row. */\nexport interface ScoreEntry {\n playerId: string;\n displayName: string;\n score: number;\n rank: number;\n meta?: Record<string, unknown>;\n}\n\n/**\n * Leaderboard provider interface. Consumers supply a concrete adapter (Game\n * Center, Google Play Games, Steam, custom backend) at the app boundary.\n * 0.1.0 ships the interface + a no-op default so downstream packages can\n * depend on it without forcing a platform choice.\n */\nexport interface LeaderboardsProvider {\n submitScore(id: LeaderboardId, score: number, meta?: Record<string, unknown>): Promise<void>;\n getTopN(id: LeaderboardId, n: number): Promise<readonly ScoreEntry[]>;\n getPlayerRank(id: LeaderboardId): Promise<number | null>;\n}\n\n/**\n * Default no-op provider. Submissions resolve silently; queries return empty\n * arrays and `null` rank.\n */\nexport const NoopLeaderboardsProvider: LeaderboardsProvider = {\n async submitScore(): Promise<void> {\n // no-op\n },\n async getTopN(): Promise<readonly ScoreEntry[]> {\n return [];\n },\n async getPlayerRank(): Promise<number | null> {\n return null;\n },\n};\n","import { createContext, useContext } from 'react';\nimport { type LeaderboardsProvider, NoopLeaderboardsProvider } from './provider.js';\n\n/**\n * React context carrying the active `LeaderboardsProvider`. Defaults to\n * `NoopLeaderboardsProvider` so `useLeaderboards()` never throws on a bare\n * tree — consumers wrap their app in a `<LeaderboardsContext.Provider>` with\n * a real adapter when they need one.\n */\nexport const LeaderboardsContext = createContext<LeaderboardsProvider>(NoopLeaderboardsProvider);\n\n/** Hook returning the current `LeaderboardsProvider`. */\nexport function useLeaderboards(): LeaderboardsProvider {\n return useContext(LeaderboardsContext);\n}\n","import type { LeaderboardId, LeaderboardsProvider, ScoreEntry } from './provider.js';\n\n/** A competitor row used to pre-populate an in-memory board (see {@link MemoryLeaderboardsOptions.seed}). */\nexport interface SeedScore {\n playerId: string;\n displayName: string;\n score: number;\n meta?: Record<string, unknown>;\n}\n\n/** Options for {@link createMemoryLeaderboards}. */\nexport interface MemoryLeaderboardsOptions {\n /** Identity all local submissions record under. Defaults to `'local'`. */\n playerId?: string;\n /** Display name paired with `playerId`. Defaults to `'Player'`. */\n displayName?: string;\n /** Per-board competitor rows, pre-populated so queries are meaningful without a backend. */\n seed?: Readonly<Record<LeaderboardId, readonly SeedScore[]>>;\n}\n\ninterface InternalEntry {\n playerId: string;\n displayName: string;\n score: number;\n meta?: Record<string, unknown>;\n /** Monotonic insertion order; earlier submissions break score ties in their favour. */\n order: number;\n}\n\n/** Sort a board DESCENDING by score (ties by insertion order) and assign sequential 1-based ranks. */\nfunction ranked(entries: readonly InternalEntry[]): ScoreEntry[] {\n return entries\n .slice()\n .sort((a, b) => b.score - a.score || a.order - b.order)\n .map((e, i) => ({\n playerId: e.playerId,\n displayName: e.displayName,\n score: e.score,\n rank: i + 1,\n meta: e.meta,\n }));\n}\n\n/**\n * Create an in-memory {@link LeaderboardsProvider} — a backend-free adapter for\n * local development, tests, and single-player games.\n *\n * Semantics (the adapter-author contract):\n * - The provider interface's `submitScore(id, score, meta?)` carries no\n * playerId, so every submission records under one local identity:\n * `opts.playerId` (default `'local'`) / `opts.displayName` (default\n * `'Player'`).\n * - Boards are per-id in-memory lists. `opts.seed` pre-populates competitor\n * entries so `getTopN` / `getPlayerRank` are meaningful without a backend.\n * - `submitScore` keeps the local player's BEST (max) score per board: the\n * first submission inserts an entry; a later HIGHER score replaces both the\n * score AND the meta; a lower-or-equal score is ignored. Each entry carries a\n * monotonic insertion counter for stable tie-breaking.\n * - `getTopN(id, n)` ranks the board DESCENDING by score, ties broken by\n * insertion order (earlier submission ranks higher). Ranks are sequential\n * 1-based with no sharing. `n <= 0` and unknown boards return `[]`.\n * - `getPlayerRank(id)` returns the local player's 1-based rank under the same\n * ordering, or `null` when the local player has no entry on that board.\n */\nexport function createMemoryLeaderboards(opts?: MemoryLeaderboardsOptions): LeaderboardsProvider {\n const localId = opts?.playerId ?? 'local';\n const localName = opts?.displayName ?? 'Player';\n const boards = new Map<LeaderboardId, InternalEntry[]>();\n let nextOrder = 0;\n\n if (opts?.seed !== undefined) {\n for (const [id, rows] of Object.entries(opts.seed)) {\n const board: InternalEntry[] = rows.map((row) => ({\n playerId: row.playerId,\n displayName: row.displayName,\n score: row.score,\n meta: row.meta,\n order: nextOrder++,\n }));\n boards.set(id, board);\n }\n }\n\n return {\n async submitScore(id, score, meta): Promise<void> {\n let board = boards.get(id);\n if (board === undefined) {\n board = [];\n boards.set(id, board);\n }\n const existing = board.find((e) => e.playerId === localId);\n if (existing === undefined) {\n board.push({\n playerId: localId,\n displayName: localName,\n score,\n meta,\n order: nextOrder++,\n });\n } else if (score > existing.score) {\n existing.score = score;\n existing.meta = meta;\n }\n },\n\n async getTopN(id, n): Promise<readonly ScoreEntry[]> {\n if (n <= 0) {\n return [];\n }\n const board = boards.get(id);\n if (board === undefined) {\n return [];\n }\n return ranked(board).slice(0, n);\n },\n\n async getPlayerRank(id): Promise<number | null> {\n const board = boards.get(id);\n if (board === undefined) {\n return null;\n }\n const found = ranked(board).find((e) => e.playerId === localId);\n return found === undefined ? null : found.rank;\n },\n };\n}\n"],"mappings":";;;;;;AA4BA,MAAa,2BAAiD;CAC5D,MAAM,cAA6B,CAEnC;CACA,MAAM,UAA0C;EAC9C,OAAO,CAAC;CACV;CACA,MAAM,gBAAwC;EAC5C,OAAO;CACT;AACF;;;;;;;;AC7BA,MAAa,sBAAsB,cAAoC,wBAAwB;;AAG/F,SAAgB,kBAAwC;CACtD,OAAO,WAAW,mBAAmB;AACvC;;;ACgBA,SAAS,OAAO,SAAiD;CAC/D,OAAO,QACJ,MAAM,CAAC,CACP,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,CAAC,CACtD,KAAK,GAAG,OAAO;EACd,UAAU,EAAE;EACZ,aAAa,EAAE;EACf,OAAO,EAAE;EACT,MAAM,IAAI;EACV,MAAM,EAAE;CACV,EAAE;AACN;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,yBAAyB,MAAwD;CAC/F,MAAM,UAAU,MAAM,YAAY;CAClC,MAAM,YAAY,MAAM,eAAe;CACvC,MAAM,yBAAS,IAAI,IAAoC;CACvD,IAAI,YAAY;CAEhB,IAAI,MAAM,SAAS,KAAA,GACjB,KAAK,MAAM,CAAC,IAAI,SAAS,OAAO,QAAQ,KAAK,IAAI,GAAG;EAClD,MAAM,QAAyB,KAAK,KAAK,SAAS;GAChD,UAAU,IAAI;GACd,aAAa,IAAI;GACjB,OAAO,IAAI;GACX,MAAM,IAAI;GACV,OAAO;EACT,EAAE;EACF,OAAO,IAAI,IAAI,KAAK;CACtB;CAGF,OAAO;EACL,MAAM,YAAY,IAAI,OAAO,MAAqB;GAChD,IAAI,QAAQ,OAAO,IAAI,EAAE;GACzB,IAAI,UAAU,KAAA,GAAW;IACvB,QAAQ,CAAC;IACT,OAAO,IAAI,IAAI,KAAK;GACtB;GACA,MAAM,WAAW,MAAM,MAAM,MAAM,EAAE,aAAa,OAAO;GACzD,IAAI,aAAa,KAAA,GACf,MAAM,KAAK;IACT,UAAU;IACV,aAAa;IACb;IACA;IACA,OAAO;GACT,CAAC;QACI,IAAI,QAAQ,SAAS,OAAO;IACjC,SAAS,QAAQ;IACjB,SAAS,OAAO;GAClB;EACF;EAEA,MAAM,QAAQ,IAAI,GAAmC;GACnD,IAAI,KAAK,GACP,OAAO,CAAC;GAEV,MAAM,QAAQ,OAAO,IAAI,EAAE;GAC3B,IAAI,UAAU,KAAA,GACZ,OAAO,CAAC;GAEV,OAAO,OAAO,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC;EACjC;EAEA,MAAM,cAAc,IAA4B;GAC9C,MAAM,QAAQ,OAAO,IAAI,EAAE;GAC3B,IAAI,UAAU,KAAA,GACZ,OAAO;GAET,MAAM,QAAQ,OAAO,KAAK,CAAC,CAAC,MAAM,MAAM,EAAE,aAAa,OAAO;GAC9D,OAAO,UAAU,KAAA,IAAY,OAAO,MAAM;EAC5C;CACF;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flare-engine/leaderboards",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.8",
|
|
4
4
|
"description": "Leaderboard provider interface (consumer-supplied adapter)",
|
|
5
5
|
"keywords": ["flare-engine", "leaderboards", "provider", "interface"],
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"typecheck": "tsc --noEmit"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@types/react": "^19.
|
|
40
|
+
"@types/react": "^19.3.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"react": ">=19.0.0"
|