castle-web-sdk 0.4.4 → 0.4.5
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/castle.d.ts +2 -0
- package/dist/castle.js +1 -0
- package/dist/commands.d.ts +117 -0
- package/dist/commands.js +16 -0
- package/dist/context.d.ts +2 -19
- package/dist/context.js +0 -71
- package/dist/leaderboard.js +69 -114
- package/dist/passes.d.ts +7 -0
- package/dist/passes.js +84 -0
- package/dist/runtime.d.ts +2 -0
- package/dist/runtime.js +67 -1
- package/dist/storage.d.ts +1 -2
- package/dist/storage.js +82 -198
- package/dist/time.js +5 -16
- package/dist/transport.d.ts +16 -0
- package/dist/transport.js +126 -0
- package/dist/user.js +6 -23
- package/package.json +8 -4
- package/src/castle.ts +6 -0
- package/src/commands.ts +136 -0
- package/src/context.ts +4 -98
- package/src/leaderboard.ts +78 -186
- package/src/passes.ts +95 -0
- package/src/runtime.ts +80 -1
- package/src/storage.ts +106 -315
- package/src/time.ts +5 -29
- package/src/transport.ts +169 -0
- package/src/user.ts +6 -39
- package/dist/auth.d.ts +0 -8
- package/dist/auth.js +0 -52
- package/dist/graphql.d.ts +0 -15
- package/dist/graphql.js +0 -120
- package/src/auth.ts +0 -64
- package/src/graphql.ts +0 -182
package/dist/castle.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ export { isEdit } from "./context";
|
|
|
2
2
|
export { CastleError } from "./errors";
|
|
3
3
|
export { Leaderboard } from "./leaderboard";
|
|
4
4
|
export type { LeaderboardData, LeaderboardEntry, LeaderboardOptions, LeaderboardScope, LeaderboardSort, } from "./leaderboard";
|
|
5
|
+
export { Pass } from "./passes";
|
|
6
|
+
export type { CastlePassApi, PassOfferResult, PassOfferStatus, } from "./passes";
|
|
5
7
|
export { CARD_RATIO, initCard, onBeforeRestart, setup, writeFile } from "./runtime";
|
|
6
8
|
export { SharedStorage, Storage } from "./storage";
|
|
7
9
|
export { Time } from "./time";
|
package/dist/castle.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
export { isEdit } from "./context";
|
|
3
3
|
export { CastleError } from "./errors";
|
|
4
4
|
export { Leaderboard } from "./leaderboard";
|
|
5
|
+
export { Pass } from "./passes";
|
|
5
6
|
export { CARD_RATIO, initCard, onBeforeRestart, setup, writeFile } from "./runtime";
|
|
6
7
|
export { SharedStorage, Storage } from "./storage";
|
|
7
8
|
export { Time } from "./time";
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { Json } from "./types";
|
|
2
|
+
export declare const CASTLE_SDK_PROTOCOL = 1;
|
|
3
|
+
export type StorageBlob = Record<string, string>;
|
|
4
|
+
export type SharedScope = "deck" | "user";
|
|
5
|
+
export interface StorageUpdate {
|
|
6
|
+
key: string;
|
|
7
|
+
value: string | null;
|
|
8
|
+
}
|
|
9
|
+
export interface RawLeaderboardEntry {
|
|
10
|
+
place?: string | number | null;
|
|
11
|
+
score?: string | number | null;
|
|
12
|
+
user?: {
|
|
13
|
+
userId?: string | null;
|
|
14
|
+
username?: string | null;
|
|
15
|
+
} | null;
|
|
16
|
+
}
|
|
17
|
+
export interface RawLeaderboard {
|
|
18
|
+
list?: RawLeaderboardEntry[] | null;
|
|
19
|
+
yourScore?: {
|
|
20
|
+
score?: string | number | null;
|
|
21
|
+
} | null;
|
|
22
|
+
}
|
|
23
|
+
export type PassOfferStatus = "purchased" | "alreadyOwned" | "cancelled" | "unavailable";
|
|
24
|
+
export interface PassOfferResult {
|
|
25
|
+
status: PassOfferStatus;
|
|
26
|
+
}
|
|
27
|
+
export interface CommandParams {
|
|
28
|
+
"deckStorage.load": Record<string, never>;
|
|
29
|
+
"deckStorage.update": {
|
|
30
|
+
updates: StorageUpdate[];
|
|
31
|
+
};
|
|
32
|
+
"sharedDeckStorage.load": {
|
|
33
|
+
scope: SharedScope;
|
|
34
|
+
userId?: string | null;
|
|
35
|
+
keys: string[];
|
|
36
|
+
};
|
|
37
|
+
"sharedDeckStorage.update": {
|
|
38
|
+
scope: SharedScope;
|
|
39
|
+
updates: StorageUpdate[];
|
|
40
|
+
};
|
|
41
|
+
"leaderboard.fetch": {
|
|
42
|
+
variable: string;
|
|
43
|
+
type: "high" | "low";
|
|
44
|
+
scope?: string | null;
|
|
45
|
+
score?: number | null;
|
|
46
|
+
};
|
|
47
|
+
"leaderboard.save": {
|
|
48
|
+
variable: string;
|
|
49
|
+
score: number;
|
|
50
|
+
scope?: string | null;
|
|
51
|
+
};
|
|
52
|
+
"user.getCurrent": Record<string, never>;
|
|
53
|
+
"time.getServerTime": Record<string, never>;
|
|
54
|
+
"pass.has": {
|
|
55
|
+
passId: string;
|
|
56
|
+
};
|
|
57
|
+
"pass.offer": {
|
|
58
|
+
passId: string;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export interface CommandResult {
|
|
62
|
+
"deckStorage.load": {
|
|
63
|
+
blob: StorageBlob;
|
|
64
|
+
};
|
|
65
|
+
"deckStorage.update": {
|
|
66
|
+
blob: StorageBlob;
|
|
67
|
+
};
|
|
68
|
+
"sharedDeckStorage.load": {
|
|
69
|
+
blob: StorageBlob;
|
|
70
|
+
};
|
|
71
|
+
"sharedDeckStorage.update": {
|
|
72
|
+
ok: true;
|
|
73
|
+
};
|
|
74
|
+
"leaderboard.fetch": {
|
|
75
|
+
leaderboard: RawLeaderboard;
|
|
76
|
+
currentUserId: string | null;
|
|
77
|
+
};
|
|
78
|
+
"leaderboard.save": {
|
|
79
|
+
ok: true;
|
|
80
|
+
};
|
|
81
|
+
"user.getCurrent": {
|
|
82
|
+
user: {
|
|
83
|
+
userId: string;
|
|
84
|
+
username: string;
|
|
85
|
+
} | null;
|
|
86
|
+
};
|
|
87
|
+
"time.getServerTime": {
|
|
88
|
+
timestamp: number;
|
|
89
|
+
timezoneOffset: number;
|
|
90
|
+
castleEpochData: Json;
|
|
91
|
+
};
|
|
92
|
+
"pass.has": {
|
|
93
|
+
hasPass: boolean;
|
|
94
|
+
};
|
|
95
|
+
"pass.offer": PassOfferResult;
|
|
96
|
+
}
|
|
97
|
+
export type CommandName = keyof CommandParams;
|
|
98
|
+
export interface SerializedCommandError {
|
|
99
|
+
code: string;
|
|
100
|
+
message: string;
|
|
101
|
+
command?: string;
|
|
102
|
+
extensions?: Record<string, unknown>;
|
|
103
|
+
}
|
|
104
|
+
export interface CommandRequestEnvelope {
|
|
105
|
+
castleSdk: typeof CASTLE_SDK_PROTOCOL;
|
|
106
|
+
requestId: string;
|
|
107
|
+
command: CommandName;
|
|
108
|
+
params: unknown;
|
|
109
|
+
}
|
|
110
|
+
export interface CommandResponseEnvelope {
|
|
111
|
+
castleSdk: typeof CASTLE_SDK_PROTOCOL;
|
|
112
|
+
requestId: string;
|
|
113
|
+
ok: boolean;
|
|
114
|
+
data?: unknown;
|
|
115
|
+
error?: SerializedCommandError;
|
|
116
|
+
}
|
|
117
|
+
export declare function isResponseEnvelope(value: unknown): value is CommandResponseEnvelope;
|
package/dist/commands.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// The SDK↔host command contract. This is the single source of truth for the
|
|
2
|
+
// wire protocol shared by the deck-side command-poster (`transport.ts`) and the
|
|
3
|
+
// host-side executor (`host.ts`). It carries ONLY names and types — no GraphQL
|
|
4
|
+
// query strings and no auth — so importing it into a deck bundle reveals
|
|
5
|
+
// nothing privileged.
|
|
6
|
+
// Marker + protocol version. Doubles as a discriminator so host messages can't
|
|
7
|
+
// be confused with embed.js `castlexyz:` strings or RN console messages.
|
|
8
|
+
export const CASTLE_SDK_PROTOCOL = 1;
|
|
9
|
+
export function isResponseEnvelope(value) {
|
|
10
|
+
if (typeof value !== "object" || value === null)
|
|
11
|
+
return false;
|
|
12
|
+
const record = value;
|
|
13
|
+
return (record.castleSdk === CASTLE_SDK_PROTOCOL &&
|
|
14
|
+
typeof record.requestId === "string" &&
|
|
15
|
+
typeof record.ok === "boolean");
|
|
16
|
+
}
|
package/dist/context.d.ts
CHANGED
|
@@ -1,21 +1,8 @@
|
|
|
1
|
-
export
|
|
2
|
-
deckId?: string | null;
|
|
3
|
-
cardId?: string | null;
|
|
4
|
-
sessionId?: string | null;
|
|
5
|
-
}
|
|
6
|
-
interface CastleEmbedAuth {
|
|
7
|
-
token?: string | null;
|
|
8
|
-
userId?: string | null;
|
|
9
|
-
}
|
|
1
|
+
export type CastleHost = "web" | "mobile" | "dev";
|
|
10
2
|
export interface CastleEmbed {
|
|
11
3
|
edit?: boolean;
|
|
12
4
|
feed?: boolean;
|
|
13
|
-
|
|
14
|
-
deck?: CastleDeckContext;
|
|
15
|
-
deckId?: string;
|
|
16
|
-
cardId?: string;
|
|
17
|
-
sessionId?: string | null;
|
|
18
|
-
graphqlEndpoint?: string;
|
|
5
|
+
host?: CastleHost;
|
|
19
6
|
}
|
|
20
7
|
declare global {
|
|
21
8
|
interface Window {
|
|
@@ -24,7 +11,3 @@ declare global {
|
|
|
24
11
|
}
|
|
25
12
|
export declare function getCastleEmbed(): CastleEmbed | undefined;
|
|
26
13
|
export declare function isEdit(): boolean;
|
|
27
|
-
export declare function configureDeckContext(context: CastleDeckContext): void;
|
|
28
|
-
export declare function getDeckContext(): Promise<CastleDeckContext>;
|
|
29
|
-
export declare function requireDeckId(operation?: string): Promise<string>;
|
|
30
|
-
export {};
|
package/dist/context.js
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
import { CastleError } from "./errors";
|
|
2
|
-
let configuredDeckContext = {};
|
|
3
|
-
let cachedDeckContext = null;
|
|
4
|
-
let cachedDeckContextPromise = null;
|
|
5
1
|
export function getCastleEmbed() {
|
|
6
2
|
return typeof window === "undefined" ? undefined : window.CastleEmbed;
|
|
7
3
|
}
|
|
@@ -17,70 +13,3 @@ export function isEdit() {
|
|
|
17
13
|
}
|
|
18
14
|
return !!getCastleEmbed()?.edit;
|
|
19
15
|
}
|
|
20
|
-
export function configureDeckContext(context) {
|
|
21
|
-
configuredDeckContext = cleanDeckContext(context);
|
|
22
|
-
cachedDeckContext = null;
|
|
23
|
-
cachedDeckContextPromise = null;
|
|
24
|
-
}
|
|
25
|
-
export async function getDeckContext() {
|
|
26
|
-
if (cachedDeckContext)
|
|
27
|
-
return cachedDeckContext;
|
|
28
|
-
cachedDeckContextPromise ??= resolveDeckContext();
|
|
29
|
-
return cachedDeckContextPromise;
|
|
30
|
-
}
|
|
31
|
-
export async function requireDeckId(operation = "Castle API request") {
|
|
32
|
-
const context = await getDeckContext();
|
|
33
|
-
if (!context.deckId) {
|
|
34
|
-
throw new CastleError({
|
|
35
|
-
code: "MISSING_DECK_ID",
|
|
36
|
-
message: "This Castle API needs a deck id.",
|
|
37
|
-
operation,
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
return context.deckId;
|
|
41
|
-
}
|
|
42
|
-
async function resolveDeckContext() {
|
|
43
|
-
const embedded = embeddedDeckContext();
|
|
44
|
-
const local = await localDeckContext();
|
|
45
|
-
const context = mergeDeckContexts(configuredDeckContext, embedded, local);
|
|
46
|
-
cachedDeckContext = context;
|
|
47
|
-
return context;
|
|
48
|
-
}
|
|
49
|
-
function embeddedDeckContext() {
|
|
50
|
-
const embed = getCastleEmbed();
|
|
51
|
-
return cleanDeckContext({
|
|
52
|
-
deckId: embed?.deck?.deckId ?? embed?.deckId,
|
|
53
|
-
cardId: embed?.deck?.cardId ?? embed?.cardId,
|
|
54
|
-
sessionId: embed?.deck?.sessionId ?? embed?.sessionId,
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
async function localDeckContext() {
|
|
58
|
-
try {
|
|
59
|
-
const res = await fetch("/__castle/context");
|
|
60
|
-
if (!res.ok)
|
|
61
|
-
return {};
|
|
62
|
-
return cleanDeckContext((await res.json()));
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
return {};
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
function cleanDeckContext(context) {
|
|
69
|
-
return {
|
|
70
|
-
deckId: stringOrNull(context.deckId),
|
|
71
|
-
cardId: stringOrNull(context.cardId),
|
|
72
|
-
sessionId: stringOrNull(context.sessionId),
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
function mergeDeckContexts(...contexts) {
|
|
76
|
-
return cleanDeckContext({
|
|
77
|
-
deckId: contexts.find((context) => context.deckId)?.deckId,
|
|
78
|
-
cardId: contexts.find((context) => context.cardId)?.cardId,
|
|
79
|
-
sessionId: contexts.find((context) => context.sessionId)?.sessionId,
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
function stringOrNull(value) {
|
|
83
|
-
if (typeof value !== "string")
|
|
84
|
-
return null;
|
|
85
|
-
return value.length > 0 ? value : null;
|
|
86
|
-
}
|
package/dist/leaderboard.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { isEdit, requireDeckId } from "./context";
|
|
1
|
+
import { isEdit } from "./context";
|
|
3
2
|
import { CastleError } from "./errors";
|
|
4
|
-
import {
|
|
3
|
+
import { hostRequest } from "./transport";
|
|
5
4
|
const LEADERBOARD_FLUSH_INTERVAL_MS = 5000;
|
|
5
|
+
// Host stamps deckId, so one deck session = one set of leaderboards; key by
|
|
6
|
+
// variable+scope only and keep the best high/low score per key until flush.
|
|
6
7
|
const leaderboardWrites = new Map();
|
|
7
8
|
let leaderboardFlushTimer = null;
|
|
8
9
|
let leaderboardFlushPromise = null;
|
|
@@ -18,32 +19,78 @@ export const Leaderboard = {
|
|
|
18
19
|
function writeLeaderboard(variable, score, options) {
|
|
19
20
|
if (isEdit())
|
|
20
21
|
return;
|
|
21
|
-
|
|
22
|
+
try {
|
|
23
|
+
bufferLeaderboardWrite(variable, score, options);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
reportLeaderboardError(error);
|
|
27
|
+
}
|
|
22
28
|
}
|
|
23
29
|
async function fetchLeaderboardData(variable, type, options) {
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
assertLeaderboardVariable(variable, "Leaderboard.fetch");
|
|
31
|
+
assertLeaderboardType(type, "Leaderboard.fetch");
|
|
32
|
+
const scope = leaderboardScope(options);
|
|
33
|
+
// If the deck has written a score for this variable+scope this session, send
|
|
34
|
+
// it so the host writes-and-reads via leaderboardV2 and the player's own
|
|
35
|
+
// score shows up immediately (mirrors getLeaderboard in
|
|
36
|
+
// core/src/leaderboards.cpp — presence of a buffered score, not dirtiness,
|
|
37
|
+
// gates the write-through). Otherwise a plain read of the settled board.
|
|
38
|
+
const record = leaderboardWrites.get(leaderboardWriteKey(variable, scope));
|
|
39
|
+
const score = record
|
|
40
|
+
? type === "high"
|
|
41
|
+
? record.highScore
|
|
42
|
+
: record.lowScore
|
|
43
|
+
: null;
|
|
44
|
+
const { leaderboard, currentUserId } = await hostRequest("leaderboard.fetch", {
|
|
45
|
+
variable,
|
|
46
|
+
type,
|
|
47
|
+
scope,
|
|
48
|
+
...(score === null ? {} : { score }),
|
|
49
|
+
});
|
|
50
|
+
if (record && score !== null) {
|
|
51
|
+
clearLeaderboardDirtyAfterFetch(record, type, score);
|
|
52
|
+
}
|
|
53
|
+
return normalizeLeaderboard(leaderboard, currentUserId);
|
|
54
|
+
}
|
|
55
|
+
// After a write-through fetch, clear the dirty flag for the side we just sent
|
|
56
|
+
// (so the periodic flush won't re-send it via saveVariableToLeaderboard). If
|
|
57
|
+
// the other side's buffered value matches what we sent (the common single-score
|
|
58
|
+
// case where high == low), clear it too. The equality guards skip clearing if a
|
|
59
|
+
// concurrent write bumped the buffered score while the fetch was in flight —
|
|
60
|
+
// that newer score still needs flushing. Mirrors leaderboards.cpp.
|
|
61
|
+
function clearLeaderboardDirtyAfterFetch(record, type, sentScore) {
|
|
62
|
+
if (type === "high") {
|
|
63
|
+
if (record.highScore === sentScore) {
|
|
64
|
+
record.isHighDirty = false;
|
|
65
|
+
if (record.lowScore === sentScore)
|
|
66
|
+
record.isLowDirty = false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
if (record.lowScore === sentScore) {
|
|
71
|
+
record.isLowDirty = false;
|
|
72
|
+
if (record.highScore === sentScore)
|
|
73
|
+
record.isHighDirty = false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
26
76
|
}
|
|
27
|
-
|
|
77
|
+
function bufferLeaderboardWrite(variable, score, options) {
|
|
28
78
|
assertLeaderboardVariable(variable, "Leaderboard.write");
|
|
29
79
|
assertLeaderboardScore(score, "Leaderboard.write");
|
|
30
|
-
const deckId = await requireDeckId("Leaderboard.write");
|
|
31
|
-
await requireAuthToken("Leaderboard.write");
|
|
32
80
|
const scope = leaderboardScope(options);
|
|
33
|
-
const key = leaderboardWriteKey(
|
|
81
|
+
const key = leaderboardWriteKey(variable, scope);
|
|
34
82
|
const record = leaderboardWrites.get(key);
|
|
35
83
|
if (record) {
|
|
36
84
|
updatePendingLeaderboardWrite(record, score);
|
|
37
85
|
}
|
|
38
86
|
else {
|
|
39
|
-
leaderboardWrites.set(key, newPendingLeaderboardWrite(
|
|
87
|
+
leaderboardWrites.set(key, newPendingLeaderboardWrite(variable, scope, score));
|
|
40
88
|
}
|
|
41
89
|
ensureLeaderboardUnloadFlush();
|
|
42
90
|
scheduleLeaderboardFlush();
|
|
43
91
|
}
|
|
44
|
-
function newPendingLeaderboardWrite(
|
|
92
|
+
function newPendingLeaderboardWrite(variable, scope, score) {
|
|
45
93
|
return {
|
|
46
|
-
deckId,
|
|
47
94
|
variable,
|
|
48
95
|
scope,
|
|
49
96
|
highScore: score,
|
|
@@ -89,12 +136,8 @@ async function flushLeaderboardWrites() {
|
|
|
89
136
|
return leaderboardFlushPromise;
|
|
90
137
|
}
|
|
91
138
|
async function flushLeaderboardWritesOnce() {
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
return;
|
|
95
|
-
const token = await requireAuthToken("Leaderboard.write");
|
|
96
|
-
for (const job of jobs) {
|
|
97
|
-
await saveLeaderboardScore(job.record, job.score, token);
|
|
139
|
+
for (const job of leaderboardWriteJobs()) {
|
|
140
|
+
await saveLeaderboardScore(job.record, job.score);
|
|
98
141
|
markLeaderboardWriteClean(job);
|
|
99
142
|
}
|
|
100
143
|
}
|
|
@@ -131,104 +174,13 @@ function hasDirtyLeaderboardWrites() {
|
|
|
131
174
|
}
|
|
132
175
|
return false;
|
|
133
176
|
}
|
|
134
|
-
async function
|
|
135
|
-
|
|
136
|
-
assertLeaderboardType(type, operation);
|
|
137
|
-
const [deckId, token, auth] = await Promise.all([
|
|
138
|
-
requireDeckId(operation),
|
|
139
|
-
requireAuthToken(operation),
|
|
140
|
-
getAuth(),
|
|
141
|
-
]);
|
|
142
|
-
return {
|
|
143
|
-
deckId,
|
|
144
|
-
variable,
|
|
145
|
-
type,
|
|
146
|
-
scope: leaderboardScope(options),
|
|
147
|
-
token,
|
|
148
|
-
userId: auth.userId ?? null,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
async function fetchLeaderboard(request) {
|
|
152
|
-
const data = await graphqlRequest(`
|
|
153
|
-
query CastleLeaderboard(
|
|
154
|
-
$deckId: ID!
|
|
155
|
-
$variable: String!
|
|
156
|
-
$type: LeaderboardType!
|
|
157
|
-
$filter: LeaderboardFilter!
|
|
158
|
-
$includeFollowList: Boolean
|
|
159
|
-
$includeParties: Boolean
|
|
160
|
-
$scope: String
|
|
161
|
-
) {
|
|
162
|
-
leaderboard(
|
|
163
|
-
deckId: $deckId
|
|
164
|
-
variable: $variable
|
|
165
|
-
type: $type
|
|
166
|
-
filter: $filter
|
|
167
|
-
includeFollowList: $includeFollowList
|
|
168
|
-
includeParties: $includeParties
|
|
169
|
-
scope: $scope
|
|
170
|
-
) ${leaderboardFields()}
|
|
171
|
-
}
|
|
172
|
-
`, leaderboardVariables(request), {
|
|
173
|
-
operation: "CastleLeaderboard",
|
|
174
|
-
token: request.token,
|
|
175
|
-
requireAuth: true,
|
|
176
|
-
});
|
|
177
|
-
return data.leaderboard;
|
|
178
|
-
}
|
|
179
|
-
async function saveLeaderboardScore(record, score, token) {
|
|
180
|
-
await graphqlRequest(`
|
|
181
|
-
mutation CastleSaveVariableToLeaderboard(
|
|
182
|
-
$deckId: ID!
|
|
183
|
-
$variable: String!
|
|
184
|
-
$score: Float!
|
|
185
|
-
$scope: String
|
|
186
|
-
) {
|
|
187
|
-
saveVariableToLeaderboard(
|
|
188
|
-
deckId: $deckId
|
|
189
|
-
variable: $variable
|
|
190
|
-
score: $score
|
|
191
|
-
scope: $scope
|
|
192
|
-
)
|
|
193
|
-
}
|
|
194
|
-
`, {
|
|
195
|
-
deckId: record.deckId,
|
|
177
|
+
async function saveLeaderboardScore(record, score) {
|
|
178
|
+
await hostRequest("leaderboard.save", {
|
|
196
179
|
variable: record.variable,
|
|
197
180
|
score,
|
|
198
181
|
scope: record.scope,
|
|
199
|
-
}, {
|
|
200
|
-
operation: "CastleSaveVariableToLeaderboard",
|
|
201
|
-
token,
|
|
202
|
-
requireAuth: true,
|
|
203
182
|
});
|
|
204
183
|
}
|
|
205
|
-
function leaderboardVariables(request) {
|
|
206
|
-
return {
|
|
207
|
-
deckId: request.deckId,
|
|
208
|
-
variable: request.variable,
|
|
209
|
-
type: request.type,
|
|
210
|
-
filter: "dedupUsers",
|
|
211
|
-
includeFollowList: false,
|
|
212
|
-
includeParties: false,
|
|
213
|
-
scope: request.scope,
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
function leaderboardFields() {
|
|
217
|
-
return `{
|
|
218
|
-
yourScore { score }
|
|
219
|
-
list {
|
|
220
|
-
place
|
|
221
|
-
score
|
|
222
|
-
user {
|
|
223
|
-
userId
|
|
224
|
-
username
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}`;
|
|
228
|
-
}
|
|
229
|
-
function leaderboardScope(options) {
|
|
230
|
-
return options.scope ?? null;
|
|
231
|
-
}
|
|
232
184
|
function normalizeLeaderboard(leaderboard, userId) {
|
|
233
185
|
const list = (leaderboard.list ?? []).map(normalizeLeaderboardEntry);
|
|
234
186
|
const playerRank = playerRankFromList(list, userId);
|
|
@@ -261,8 +213,11 @@ function scoreNumber(value) {
|
|
|
261
213
|
const parsed = Number.parseFloat(value);
|
|
262
214
|
return Number.isFinite(parsed) ? parsed : undefined;
|
|
263
215
|
}
|
|
264
|
-
function leaderboardWriteKey(
|
|
265
|
-
return `${
|
|
216
|
+
function leaderboardWriteKey(variable, scope) {
|
|
217
|
+
return `${variable}${scope ? `::${scope}` : ""}`;
|
|
218
|
+
}
|
|
219
|
+
function leaderboardScope(options) {
|
|
220
|
+
return options.scope ?? null;
|
|
266
221
|
}
|
|
267
222
|
function assertLeaderboardVariable(variable, operation) {
|
|
268
223
|
if (variable.trim().length > 0)
|
package/dist/passes.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PassOfferResult } from "./commands";
|
|
2
|
+
export type { PassOfferResult, PassOfferStatus } from "./commands";
|
|
3
|
+
export interface CastlePassApi {
|
|
4
|
+
has(passId: string): Promise<boolean>;
|
|
5
|
+
offer(passId: string): Promise<PassOfferResult>;
|
|
6
|
+
}
|
|
7
|
+
export declare const Pass: CastlePassApi;
|
package/dist/passes.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Pass — a deck-facing capability for selling a creator "pass" to the player.
|
|
2
|
+
//
|
|
3
|
+
// The deck stays capability-AGNOSTIC: it just offers a pass to the player and
|
|
4
|
+
// gets back one normalized outcome regardless of platform. The host decides what
|
|
5
|
+
// UI to show and whether a real transaction can happen:
|
|
6
|
+
// - mobile app : renders the native bricks purchase sheet over the deck
|
|
7
|
+
// - web player : shows an "open in the app" upsell, returns `unavailable`
|
|
8
|
+
// - dev CLI : no host UI surface, so the SDK shows a minimal in-page
|
|
9
|
+
// notice itself (kept deliberately tiny), returns `unavailable`
|
|
10
|
+
// There is no capability check for the deck to make — every platform returns a
|
|
11
|
+
// PassOfferResult, so a single code path handles them all.
|
|
12
|
+
import { CastleError } from "./errors";
|
|
13
|
+
import { getCommandChannel, hostRequest } from "./transport";
|
|
14
|
+
export const Pass = {
|
|
15
|
+
has,
|
|
16
|
+
offer,
|
|
17
|
+
};
|
|
18
|
+
// Pure read — does the current player already own this pass? No UI, every
|
|
19
|
+
// platform answers it the same way (a GraphQL query). Use it to gate content or
|
|
20
|
+
// to decide whether to bother calling `offer`.
|
|
21
|
+
async function has(passId) {
|
|
22
|
+
if (typeof passId !== "string" || passId.length === 0) {
|
|
23
|
+
throw new CastleError({
|
|
24
|
+
code: "INVALID_ARGUMENT",
|
|
25
|
+
message: "Pass.has requires a passId.",
|
|
26
|
+
operation: "Pass.has",
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
const { hasPass } = await hostRequest("pass.has", { passId });
|
|
30
|
+
return hasPass;
|
|
31
|
+
}
|
|
32
|
+
async function offer(passId) {
|
|
33
|
+
if (typeof passId !== "string" || passId.length === 0) {
|
|
34
|
+
throw new CastleError({
|
|
35
|
+
code: "INVALID_ARGUMENT",
|
|
36
|
+
message: "Pass.offer requires a passId.",
|
|
37
|
+
operation: "Pass.offer",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const result = await hostRequest("pass.offer", { passId });
|
|
41
|
+
// On the dev server there's no host chrome to explain why nothing happened,
|
|
42
|
+
// so surface a small built-in notice. The mobile/web hosts render their own
|
|
43
|
+
// UI, so the SDK stays silent there.
|
|
44
|
+
if (result.status === "unavailable" && getCommandChannel() === "local") {
|
|
45
|
+
showDevUnavailableNotice();
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
let devNoticeEl = null;
|
|
50
|
+
let devNoticeTimer = null;
|
|
51
|
+
// Minimal, dependency-free toast. Dev-only affordance — not the place for a
|
|
52
|
+
// designed purchase UI.
|
|
53
|
+
function showDevUnavailableNotice() {
|
|
54
|
+
if (typeof document === "undefined")
|
|
55
|
+
return;
|
|
56
|
+
if (!devNoticeEl) {
|
|
57
|
+
devNoticeEl = document.createElement("div");
|
|
58
|
+
devNoticeEl.textContent = "Passes can only be purchased in the Castle app.";
|
|
59
|
+
devNoticeEl.style.cssText = [
|
|
60
|
+
"position:fixed",
|
|
61
|
+
"left:50%",
|
|
62
|
+
"bottom:24px",
|
|
63
|
+
"transform:translateX(-50%)",
|
|
64
|
+
"max-width:80vw",
|
|
65
|
+
"padding:10px 16px",
|
|
66
|
+
"border-radius:8px",
|
|
67
|
+
"background:rgba(0,0,0,0.82)",
|
|
68
|
+
"color:#fff",
|
|
69
|
+
"font:500 13px/1.4 system-ui,sans-serif",
|
|
70
|
+
"text-align:center",
|
|
71
|
+
"z-index:2147483647",
|
|
72
|
+
"pointer-events:none",
|
|
73
|
+
"transition:opacity 0.3s ease",
|
|
74
|
+
].join(";");
|
|
75
|
+
document.body.appendChild(devNoticeEl);
|
|
76
|
+
}
|
|
77
|
+
devNoticeEl.style.opacity = "1";
|
|
78
|
+
if (devNoticeTimer)
|
|
79
|
+
clearTimeout(devNoticeTimer);
|
|
80
|
+
devNoticeTimer = setTimeout(() => {
|
|
81
|
+
if (devNoticeEl)
|
|
82
|
+
devNoticeEl.style.opacity = "0";
|
|
83
|
+
}, 3200);
|
|
84
|
+
}
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CommandName, type CommandParams, type CommandResponseEnvelope } from "./commands";
|
|
1
2
|
export declare const CARD_RATIO: number;
|
|
2
3
|
interface LocalResponse {
|
|
3
4
|
type: string;
|
|
@@ -9,5 +10,6 @@ interface LocalResponse {
|
|
|
9
10
|
export declare function setup(): void;
|
|
10
11
|
export declare function writeFile(path: string, contents: string): Promise<LocalResponse>;
|
|
11
12
|
export declare function initCard(): HTMLDivElement;
|
|
13
|
+
export declare function sendLocalCommand<C extends CommandName>(command: C, params: CommandParams[C]): Promise<CommandResponseEnvelope>;
|
|
12
14
|
export declare function onBeforeRestart(hook: () => void | Promise<void>): () => void;
|
|
13
15
|
export {};
|