castle-web-sdk 0.4.5 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +321 -0
- package/dist/runtime.js +22 -14
- package/package.json +3 -4
- package/src/castle.ts +0 -25
- package/src/commands.ts +0 -136
- package/src/context.ts +0 -30
- package/src/errors.ts +0 -32
- package/src/leaderboard.ts +0 -348
- package/src/passes.ts +0 -95
- package/src/runtime.ts +0 -451
- package/src/storage.ts +0 -427
- package/src/time.ts +0 -202
- package/src/transport.ts +0 -169
- package/src/types.ts +0 -7
- package/src/user.ts +0 -58
package/README.md
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# Castle Web SDK Reference
|
|
2
|
+
|
|
3
|
+
`castle-web-sdk` lets a deck use services provided by the Castle
|
|
4
|
+
platform — for example, a deck can save per-player data, post and read
|
|
5
|
+
scores on a leaderboard, or get a server-synced time for daily content.
|
|
6
|
+
|
|
7
|
+
Comes with `castle-web init`. Import what you need from
|
|
8
|
+
`castle-web-sdk`:
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
import { setup, initCard, Storage, Leaderboard } from "castle-web-sdk";
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Contents
|
|
15
|
+
|
|
16
|
+
- [Storage](#storage)
|
|
17
|
+
- [SharedStorage](#sharedstorage)
|
|
18
|
+
- [Leaderboard](#leaderboard)
|
|
19
|
+
- [Time](#time)
|
|
20
|
+
- [User](#user)
|
|
21
|
+
- [Pass](#pass)
|
|
22
|
+
- [Setup](#setup)
|
|
23
|
+
- [CastleError](#castleerror)
|
|
24
|
+
|
|
25
|
+
## Storage
|
|
26
|
+
|
|
27
|
+
`Storage` saves data for the current player. Nobody else can read it.
|
|
28
|
+
Use it for save files, settings, progress.
|
|
29
|
+
|
|
30
|
+
### `Storage.get<T>(key): Promise<T | null>`
|
|
31
|
+
|
|
32
|
+
Returns the value at `key`, or `null` if not set.
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
const level = (await Storage.get("level")) ?? 1;
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### `Storage.set(key, value)`
|
|
39
|
+
|
|
40
|
+
Sets `key` to `value`. `value` must be something that can convert to
|
|
41
|
+
JSON (`null`, booleans, finite numbers, strings, arrays, plain
|
|
42
|
+
objects). The next `get(key)` returns the new value immediately. Writes
|
|
43
|
+
save in the background.
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
Storage.set("level", 7);
|
|
47
|
+
Storage.set("settings", { sound: true, music: false });
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### `Storage.remove(key)`
|
|
51
|
+
|
|
52
|
+
Removes `key`.
|
|
53
|
+
|
|
54
|
+
## SharedStorage
|
|
55
|
+
|
|
56
|
+
`SharedStorage` saves data that other players can read. Values must
|
|
57
|
+
be something that can convert to JSON, same as `Storage`.
|
|
58
|
+
|
|
59
|
+
Scopes:
|
|
60
|
+
|
|
61
|
+
- `'deck'` — one shared bucket for the whole deck. Any player can read
|
|
62
|
+
or write.
|
|
63
|
+
- `'user'` — a per-player public bucket. Any player can read; only the
|
|
64
|
+
owning player can write.
|
|
65
|
+
|
|
66
|
+
### `SharedStorage.get(scope, key): Promise<T | null>`
|
|
67
|
+
|
|
68
|
+
Reads a shared value. For `'user'`, omit the user id to read the
|
|
69
|
+
current player's bucket, or pass one to read someone else's:
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
const worldHighScore = await SharedStorage.get("deck", "highScore");
|
|
73
|
+
const myColor = await SharedStorage.get("user", "color");
|
|
74
|
+
const theirColor = await SharedStorage.get("user", otherUserId, "color");
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### `SharedStorage.set(scope, key, value)`
|
|
78
|
+
|
|
79
|
+
Writes a shared value. `'user'` writes always go to the current
|
|
80
|
+
player's bucket. Writes save in the background.
|
|
81
|
+
|
|
82
|
+
```js
|
|
83
|
+
SharedStorage.set("deck", "highScore", 9001);
|
|
84
|
+
SharedStorage.set("user", "color", "red");
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### `SharedStorage.remove(scope, key)`
|
|
88
|
+
|
|
89
|
+
Removes a shared value.
|
|
90
|
+
|
|
91
|
+
## Leaderboard
|
|
92
|
+
|
|
93
|
+
`Leaderboard` ranks players by a numeric score, per deck and per
|
|
94
|
+
variable name. Pick a variable name for each leaderboard the deck has
|
|
95
|
+
(e.g. `'score'`, `'time'`).
|
|
96
|
+
|
|
97
|
+
### `Leaderboard.write(variable, score, options?)`
|
|
98
|
+
|
|
99
|
+
Submits a score. Only the player's best score for that `variable` (and
|
|
100
|
+
scope) is kept. In the editor it does nothing — safe to call from
|
|
101
|
+
gameplay code unconditionally.
|
|
102
|
+
|
|
103
|
+
`options` is `{ scope?: string }`. By default the score goes to the
|
|
104
|
+
deck's global leaderboard for `variable`; pass `scope` to write to a
|
|
105
|
+
separate leaderboard (for example a daily one, or a custom id):
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
Leaderboard.write("score", 1200);
|
|
109
|
+
|
|
110
|
+
// A daily leaderboard: scope by the current Castle day so each day gets
|
|
111
|
+
// its own board (see Time.getServerDate).
|
|
112
|
+
const { daysSinceCastleEpoch } = await Time.getServerDate();
|
|
113
|
+
Leaderboard.write("score", 1200, { scope: `daily-${daysSinceCastleEpoch}` });
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### `Leaderboard.fetch(variable, type, options?): Promise<LeaderboardData>`
|
|
117
|
+
|
|
118
|
+
Fetches the leaderboard for `variable`. `type` is `'high'` (highest
|
|
119
|
+
first) or `'low'` (lowest first). `options.scope` works the same as in
|
|
120
|
+
`write`.
|
|
121
|
+
|
|
122
|
+
If the player has written a score this session, a `fetch` reflects
|
|
123
|
+
**their own** new score right away — you can `write` then `fetch` and
|
|
124
|
+
show the result without waiting. Other players' recent scores still
|
|
125
|
+
appear on their own normal timing.
|
|
126
|
+
|
|
127
|
+
The returned `LeaderboardData` has:
|
|
128
|
+
|
|
129
|
+
- `list` — array of entries, each `{ place, value, username, userId? }`.
|
|
130
|
+
- `playerRank` — the current player's place on the board (if they have
|
|
131
|
+
a score).
|
|
132
|
+
- `playerValue` — the current player's score (if they have one).
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
const data = await Leaderboard.fetch("score", "high");
|
|
136
|
+
for (const entry of data.list) {
|
|
137
|
+
console.log(`${entry.place}. ${entry.username} — ${entry.value}`);
|
|
138
|
+
}
|
|
139
|
+
if (data.playerRank) {
|
|
140
|
+
console.log(`you are #${data.playerRank} with ${data.playerValue}`);
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Time
|
|
145
|
+
|
|
146
|
+
### `Time.getServerTime(): Promise<number>`
|
|
147
|
+
|
|
148
|
+
Returns the current server time as a Unix timestamp in seconds.
|
|
149
|
+
|
|
150
|
+
```js
|
|
151
|
+
const now = await Time.getServerTime();
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### `Time.getServerDate(timezone?): Promise<CastleDateParts>`
|
|
155
|
+
|
|
156
|
+
Returns the current server time broken into date parts. `timezone` is
|
|
157
|
+
`'Castle'` (default; Castle's server timezone, same for every player)
|
|
158
|
+
or `'player'` (the player's local timezone).
|
|
159
|
+
|
|
160
|
+
The returned `CastleDateParts` has:
|
|
161
|
+
|
|
162
|
+
- `sec`, `min`, `hour` — time of day.
|
|
163
|
+
- `day` (1-31), `month` (1-12), `year` — date.
|
|
164
|
+
- `wday` — day of the week (1-7, Sunday = 1).
|
|
165
|
+
- `yday` — day of the year (1-366).
|
|
166
|
+
- `daysSinceCastleEpoch` — a day number that increments every day.
|
|
167
|
+
Use it for daily content.
|
|
168
|
+
|
|
169
|
+
```js
|
|
170
|
+
const date = await Time.getServerDate("player");
|
|
171
|
+
const dailyPuzzle = (date.daysSinceCastleEpoch % 30) + 1;
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## User
|
|
175
|
+
|
|
176
|
+
### `User.getCurrent(): Promise<CastleUser>`
|
|
177
|
+
|
|
178
|
+
Returns the signed-in player. Throws `CastleError`
|
|
179
|
+
(`LOGIN_REQUIRED`) when nobody is signed in.
|
|
180
|
+
|
|
181
|
+
The returned `CastleUser` has `userId`, `username`, and `isActive`.
|
|
182
|
+
|
|
183
|
+
```js
|
|
184
|
+
const me = await User.getCurrent();
|
|
185
|
+
greet(me.username);
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Pass
|
|
189
|
+
|
|
190
|
+
A pass is something a creator sells to players for Castle bricks (the
|
|
191
|
+
in-app currency): buy it once, own it for good. Use one to gate part of a
|
|
192
|
+
deck behind a purchase — bonus levels, a cosmetic, supporting the
|
|
193
|
+
creator. Set up the pass (name, art, price) on Castle; a deck refers to
|
|
194
|
+
it by id.
|
|
195
|
+
|
|
196
|
+
### `Pass.has(passId): Promise<boolean>`
|
|
197
|
+
|
|
198
|
+
Returns `true` if the current player owns the pass. No UI, nothing
|
|
199
|
+
charged — use it to gate content.
|
|
200
|
+
|
|
201
|
+
```js
|
|
202
|
+
if (await Pass.has(bonusLevelsPassId)) {
|
|
203
|
+
showBonusLevels();
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### `Pass.offer(passId): Promise<PassOfferResult>`
|
|
208
|
+
|
|
209
|
+
Presents the pass for the player to buy; resolves when they're done.
|
|
210
|
+
Bricks cost real money, so this only works in the Castle mobile app —
|
|
211
|
+
elsewhere (the website, the dev server) it resolves `unavailable`.
|
|
212
|
+
`PassOfferResult` has a `status`:
|
|
213
|
+
|
|
214
|
+
- `'purchased'` — just bought it; grant access.
|
|
215
|
+
- `'alreadyOwned'` — already had it (not charged); grant access.
|
|
216
|
+
- `'cancelled'` — dismissed without buying.
|
|
217
|
+
- `'unavailable'` — can't buy here (e.g. the website).
|
|
218
|
+
|
|
219
|
+
```js
|
|
220
|
+
const { status } = await Pass.offer(bonusLevelsPassId);
|
|
221
|
+
if (status === "purchased" || status === "alreadyOwned") {
|
|
222
|
+
showBonusLevels();
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Setup
|
|
227
|
+
|
|
228
|
+
Startup, editor-mode check, and a file-write call for editor UI.
|
|
229
|
+
|
|
230
|
+
### `setup()`
|
|
231
|
+
|
|
232
|
+
Call this once at the start of the deck, before any other SDK call.
|
|
233
|
+
`setup()` initializes the SDK so the rest of the API is usable and
|
|
234
|
+
mounts the centered 5:7 card shell around whatever the deck renders
|
|
235
|
+
into `#root` (when the deck is being played standalone in a browser).
|
|
236
|
+
While running locally with `castle-web serve`, it also forwards
|
|
237
|
+
`console` output to the CLI and reloads the page when `castle-web
|
|
238
|
+
restart` runs.
|
|
239
|
+
|
|
240
|
+
```js
|
|
241
|
+
import { setup } from "castle-web-sdk";
|
|
242
|
+
|
|
243
|
+
setup();
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### `initCard(): HTMLDivElement`
|
|
247
|
+
|
|
248
|
+
Use this when the deck draws into a `<canvas>` (or anything else)
|
|
249
|
+
rather than into the React tree at `#root`. Returns a centered,
|
|
250
|
+
viewport-sized `<div>` with the standard Castle 5:7 card aspect ratio.
|
|
251
|
+
The div resizes itself when the window resizes.
|
|
252
|
+
|
|
253
|
+
```js
|
|
254
|
+
import { setup, initCard } from "castle-web-sdk";
|
|
255
|
+
|
|
256
|
+
setup();
|
|
257
|
+
const card = initCard();
|
|
258
|
+
|
|
259
|
+
const canvas = document.createElement("canvas");
|
|
260
|
+
canvas.style.cssText = "width: 100%; height: 100%; display: block;";
|
|
261
|
+
card.appendChild(canvas);
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
If the deck mounts a React tree into `#root` instead, you don't need
|
|
265
|
+
`initCard()` — `setup()` already wraps `#root`'s children in a card.
|
|
266
|
+
|
|
267
|
+
### `CARD_RATIO`
|
|
268
|
+
|
|
269
|
+
The card aspect ratio (`5 / 7`). Use this if you need to size something
|
|
270
|
+
to match the card.
|
|
271
|
+
|
|
272
|
+
### `isEdit(): boolean`
|
|
273
|
+
|
|
274
|
+
`true` when the deck is being edited, `false` when it's being played.
|
|
275
|
+
Use this to show editor UI only in edit mode.
|
|
276
|
+
|
|
277
|
+
```js
|
|
278
|
+
import { isEdit, setup } from "castle-web-sdk";
|
|
279
|
+
|
|
280
|
+
setup();
|
|
281
|
+
if (isEdit()) {
|
|
282
|
+
mountEditor();
|
|
283
|
+
} else {
|
|
284
|
+
startGame();
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### `writeFile(path, contents): Promise<void>`
|
|
289
|
+
|
|
290
|
+
Writes a file in the deck directory. `path` is relative to the deck
|
|
291
|
+
root, `contents` is a string. Use this from editor UI to save scenes,
|
|
292
|
+
drawings, or generated source.
|
|
293
|
+
|
|
294
|
+
```js
|
|
295
|
+
import { writeFile } from "castle-web-sdk";
|
|
296
|
+
|
|
297
|
+
await writeFile("scenes/main.scene", JSON.stringify(scene, null, 2));
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Only works while editing locally with `castle-web serve`. Calls from a
|
|
301
|
+
published deck fail.
|
|
302
|
+
|
|
303
|
+
## CastleError
|
|
304
|
+
|
|
305
|
+
Every error the SDK throws is a `CastleError`. Check `code` to tell
|
|
306
|
+
the kinds apart.
|
|
307
|
+
|
|
308
|
+
Common codes:
|
|
309
|
+
|
|
310
|
+
- `LOGIN_REQUIRED` — the player needs to be signed in.
|
|
311
|
+
- `MISSING_DECK_ID` — the deck hasn't been saved to Castle yet, so it
|
|
312
|
+
has no id.
|
|
313
|
+
- `CASTLE_STORAGE_SERIALIZE_FAILED` — value isn't plain JSON (e.g. a
|
|
314
|
+
class instance, a function, a non-finite number, or a cycle).
|
|
315
|
+
- `INVALID_LEADERBOARD_VARIABLE`, `INVALID_LEADERBOARD_SCORE`,
|
|
316
|
+
`INVALID_LEADERBOARD_TYPE` — bad argument to a `Leaderboard` call.
|
|
317
|
+
- `UNSUPPORTED_TIMEZONE` — `Time.getServerDate` got a zone other than
|
|
318
|
+
`'Castle'` or `'player'`.
|
|
319
|
+
- `CASTLE_HOST_UNAVAILABLE` — the Castle host (the app or website running
|
|
320
|
+
the deck) didn't handle the request — e.g. it timed out or wasn't
|
|
321
|
+
reachable. Usually transient; retry or surface a gentle error.
|
package/dist/runtime.js
CHANGED
|
@@ -250,26 +250,34 @@ async function captureWithHtml2Canvas(target) {
|
|
|
250
250
|
return null;
|
|
251
251
|
}
|
|
252
252
|
}
|
|
253
|
+
function cropCanvasToCard(card, canvas) {
|
|
254
|
+
const cardRect = card.getBoundingClientRect();
|
|
255
|
+
const c = document.createElement("canvas");
|
|
256
|
+
c.width = cardRect.width * devicePixelRatio;
|
|
257
|
+
c.height = cardRect.height * devicePixelRatio;
|
|
258
|
+
const ctx = c.getContext("2d");
|
|
259
|
+
const canvasRect = canvas.getBoundingClientRect();
|
|
260
|
+
const dx = (canvasRect.left - cardRect.left) * devicePixelRatio;
|
|
261
|
+
const dy = (canvasRect.top - cardRect.top) * devicePixelRatio;
|
|
262
|
+
ctx.drawImage(canvas, dx, dy, canvasRect.width * devicePixelRatio, canvasRect.height * devicePixelRatio);
|
|
263
|
+
return c.toDataURL("image/png");
|
|
264
|
+
}
|
|
253
265
|
async function captureScreenshot() {
|
|
254
|
-
const card = document.
|
|
255
|
-
|
|
266
|
+
const card = document.querySelector("#castle-card, [data-castle-card]");
|
|
267
|
+
if (card) {
|
|
268
|
+
const cardCanvas = card.querySelector("canvas");
|
|
269
|
+
if (cardCanvas)
|
|
270
|
+
return cropCanvasToCard(card, cardCanvas);
|
|
271
|
+
const cropped = await captureWithHtml2Canvas(card);
|
|
272
|
+
if (cropped)
|
|
273
|
+
return cropped;
|
|
274
|
+
}
|
|
256
275
|
if (document.body?.dataset.castleScreenshotTarget === "viewport") {
|
|
257
276
|
const viewportCapture = await captureWithHtml2Canvas(document.body);
|
|
258
277
|
if (viewportCapture)
|
|
259
278
|
return viewportCapture;
|
|
260
279
|
}
|
|
261
|
-
|
|
262
|
-
const cardRect = card.getBoundingClientRect();
|
|
263
|
-
const c = document.createElement("canvas");
|
|
264
|
-
c.width = cardRect.width * devicePixelRatio;
|
|
265
|
-
c.height = cardRect.height * devicePixelRatio;
|
|
266
|
-
const ctx = c.getContext("2d");
|
|
267
|
-
const canvasRect = canvas.getBoundingClientRect();
|
|
268
|
-
const dx = (canvasRect.left - cardRect.left) * devicePixelRatio;
|
|
269
|
-
const dy = (canvasRect.top - cardRect.top) * devicePixelRatio;
|
|
270
|
-
ctx.drawImage(canvas, dx, dy, canvasRect.width * devicePixelRatio, canvasRect.height * devicePixelRatio);
|
|
271
|
-
return c.toDataURL("image/png");
|
|
272
|
-
}
|
|
280
|
+
const canvas = document.querySelector("canvas");
|
|
273
281
|
if (canvas)
|
|
274
282
|
return canvas.toDataURL("image/png");
|
|
275
283
|
return captureWithHtml2Canvas(card || document.body);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "castle-web-sdk",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/castle.js",
|
|
6
6
|
"types": "dist/castle.d.ts",
|
|
@@ -13,10 +13,9 @@
|
|
|
13
13
|
"//host": "host.{js,d.ts} is the host-side executor — deliberately NOT exported and NOT packaged. It is vendored into host repos via scripts/copy-host-module.mjs; decks must never receive it.",
|
|
14
14
|
"files": [
|
|
15
15
|
"dist",
|
|
16
|
-
"
|
|
16
|
+
"README.md",
|
|
17
17
|
"!dist/host.js",
|
|
18
|
-
"!dist/host.d.ts"
|
|
19
|
-
"!src/host.ts"
|
|
18
|
+
"!dist/host.d.ts"
|
|
20
19
|
],
|
|
21
20
|
"scripts": {
|
|
22
21
|
"build": "rm -rf dist && tsc",
|
package/src/castle.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
// Castle Web SDK
|
|
2
|
-
|
|
3
|
-
export { isEdit } from "./context";
|
|
4
|
-
export { CastleError } from "./errors";
|
|
5
|
-
export { Leaderboard } from "./leaderboard";
|
|
6
|
-
export type {
|
|
7
|
-
LeaderboardData,
|
|
8
|
-
LeaderboardEntry,
|
|
9
|
-
LeaderboardOptions,
|
|
10
|
-
LeaderboardScope,
|
|
11
|
-
LeaderboardSort,
|
|
12
|
-
} from "./leaderboard";
|
|
13
|
-
export { Pass } from "./passes";
|
|
14
|
-
export type {
|
|
15
|
-
CastlePassApi,
|
|
16
|
-
PassOfferResult,
|
|
17
|
-
PassOfferStatus,
|
|
18
|
-
} from "./passes";
|
|
19
|
-
export { CARD_RATIO, initCard, onBeforeRestart, setup, writeFile } from "./runtime";
|
|
20
|
-
export { SharedStorage, Storage } from "./storage";
|
|
21
|
-
export { Time } from "./time";
|
|
22
|
-
export type { CastleClockZone, CastleDateParts, CastleTimeApi } from "./time";
|
|
23
|
-
export type { Json } from "./types";
|
|
24
|
-
export { User } from "./user";
|
|
25
|
-
export type { CastleUser, CastleUserApi } from "./user";
|
package/src/commands.ts
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
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
|
-
|
|
7
|
-
import type { Json } from "./types";
|
|
8
|
-
|
|
9
|
-
// Marker + protocol version. Doubles as a discriminator so host messages can't
|
|
10
|
-
// be confused with embed.js `castlexyz:` strings or RN console messages.
|
|
11
|
-
export const CASTLE_SDK_PROTOCOL = 1;
|
|
12
|
-
|
|
13
|
-
export type StorageBlob = Record<string, string>;
|
|
14
|
-
export type SharedScope = "deck" | "user";
|
|
15
|
-
|
|
16
|
-
export interface StorageUpdate {
|
|
17
|
-
key: string;
|
|
18
|
-
value: string | null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// The raw GraphQL leaderboard shape the host returns; the SDK normalizes it
|
|
22
|
-
// (and computes playerRank from `currentUserId`) into the public LeaderboardData.
|
|
23
|
-
export interface RawLeaderboardEntry {
|
|
24
|
-
place?: string | number | null;
|
|
25
|
-
score?: string | number | null;
|
|
26
|
-
user?: { userId?: string | null; username?: string | null } | null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface RawLeaderboard {
|
|
30
|
-
list?: RawLeaderboardEntry[] | null;
|
|
31
|
-
yourScore?: { score?: string | number | null } | null;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export type PassOfferStatus =
|
|
35
|
-
| "purchased"
|
|
36
|
-
| "alreadyOwned"
|
|
37
|
-
| "cancelled"
|
|
38
|
-
| "unavailable";
|
|
39
|
-
|
|
40
|
-
export interface PassOfferResult {
|
|
41
|
-
status: PassOfferStatus;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface CommandParams {
|
|
45
|
-
"deckStorage.load": Record<string, never>;
|
|
46
|
-
"deckStorage.update": { updates: StorageUpdate[] };
|
|
47
|
-
"sharedDeckStorage.load": {
|
|
48
|
-
scope: SharedScope;
|
|
49
|
-
userId?: string | null;
|
|
50
|
-
keys: string[];
|
|
51
|
-
};
|
|
52
|
-
"sharedDeckStorage.update": { scope: SharedScope; updates: StorageUpdate[] };
|
|
53
|
-
"leaderboard.fetch": {
|
|
54
|
-
variable: string;
|
|
55
|
-
type: "high" | "low";
|
|
56
|
-
scope?: string | null;
|
|
57
|
-
// When present, the deck has a freshly-written score for this
|
|
58
|
-
// variable+scope that hasn't settled server-side yet. The host routes
|
|
59
|
-
// through the leaderboardV2 mutation, which writes this score and returns
|
|
60
|
-
// the post-write leaderboard atomically, so the player's own score shows
|
|
61
|
-
// up immediately. Absent → a plain read of the settled leaderboard.
|
|
62
|
-
score?: number | null;
|
|
63
|
-
};
|
|
64
|
-
"leaderboard.save": { variable: string; score: number; scope?: string | null };
|
|
65
|
-
"user.getCurrent": Record<string, never>;
|
|
66
|
-
"time.getServerTime": Record<string, never>;
|
|
67
|
-
"pass.has": { passId: string };
|
|
68
|
-
// Platform/interactive command — dispatched to the host's platformHandler,
|
|
69
|
-
// not graphqlFetch. Unlike the data commands above, this one can stay open
|
|
70
|
-
// for a long time (the player interacting with a native sheet), so the
|
|
71
|
-
// deck-side transport gives it no timeout.
|
|
72
|
-
"pass.offer": { passId: string };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export interface CommandResult {
|
|
76
|
-
"deckStorage.load": { blob: StorageBlob };
|
|
77
|
-
"deckStorage.update": { blob: StorageBlob };
|
|
78
|
-
"sharedDeckStorage.load": { blob: StorageBlob };
|
|
79
|
-
"sharedDeckStorage.update": { ok: true };
|
|
80
|
-
"leaderboard.fetch": {
|
|
81
|
-
leaderboard: RawLeaderboard;
|
|
82
|
-
currentUserId: string | null;
|
|
83
|
-
};
|
|
84
|
-
"leaderboard.save": { ok: true };
|
|
85
|
-
"user.getCurrent": { user: { userId: string; username: string } | null };
|
|
86
|
-
"time.getServerTime": {
|
|
87
|
-
timestamp: number;
|
|
88
|
-
timezoneOffset: number;
|
|
89
|
-
castleEpochData: Json;
|
|
90
|
-
};
|
|
91
|
-
"pass.has": { hasPass: boolean };
|
|
92
|
-
"pass.offer": PassOfferResult;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export type CommandName = keyof CommandParams;
|
|
96
|
-
|
|
97
|
-
// NB: the runtime command allowlist (COMMAND_NAMES / isCommandName) lives in
|
|
98
|
-
// host.ts, not here, so that host.ts can keep ALL of its imports type-only and
|
|
99
|
-
// compile to a single self-contained file (zero runtime imports) that vendors
|
|
100
|
-
// cleanly into Node-ESM / webpack / Metro hosts in other repos.
|
|
101
|
-
|
|
102
|
-
// Serializable error that survives the postMessage boundary. The deck-side
|
|
103
|
-
// rebuilds a real CastleError from it, preserving `code` so decks can branch.
|
|
104
|
-
export interface SerializedCommandError {
|
|
105
|
-
code: string;
|
|
106
|
-
message: string;
|
|
107
|
-
command?: string;
|
|
108
|
-
extensions?: Record<string, unknown>;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export interface CommandRequestEnvelope {
|
|
112
|
-
castleSdk: typeof CASTLE_SDK_PROTOCOL;
|
|
113
|
-
requestId: string;
|
|
114
|
-
command: CommandName;
|
|
115
|
-
params: unknown;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export interface CommandResponseEnvelope {
|
|
119
|
-
castleSdk: typeof CASTLE_SDK_PROTOCOL;
|
|
120
|
-
requestId: string;
|
|
121
|
-
ok: boolean;
|
|
122
|
-
data?: unknown;
|
|
123
|
-
error?: SerializedCommandError;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export function isResponseEnvelope(
|
|
127
|
-
value: unknown,
|
|
128
|
-
): value is CommandResponseEnvelope {
|
|
129
|
-
if (typeof value !== "object" || value === null) return false;
|
|
130
|
-
const record = value as Record<string, unknown>;
|
|
131
|
-
return (
|
|
132
|
-
record.castleSdk === CASTLE_SDK_PROTOCOL &&
|
|
133
|
-
typeof record.requestId === "string" &&
|
|
134
|
-
typeof record.ok === "boolean"
|
|
135
|
-
);
|
|
136
|
-
}
|
package/src/context.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
// Which outer runtime is hosting the deck. Set by each host alongside the
|
|
2
|
-
// (now non-secret) CastleEmbed flags; used by transport.ts to pick a channel.
|
|
3
|
-
export type CastleHost = "web" | "mobile" | "dev";
|
|
4
|
-
|
|
5
|
-
export interface CastleEmbed {
|
|
6
|
-
edit?: boolean;
|
|
7
|
-
feed?: boolean;
|
|
8
|
-
host?: CastleHost;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
declare global {
|
|
12
|
-
interface Window {
|
|
13
|
-
CastleEmbed?: CastleEmbed;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function getCastleEmbed(): CastleEmbed | undefined {
|
|
18
|
-
return typeof window === "undefined" ? undefined : window.CastleEmbed;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function isEdit(): boolean {
|
|
22
|
-
try {
|
|
23
|
-
const params = new URLSearchParams(window.location.search);
|
|
24
|
-
const override = params.get("edit");
|
|
25
|
-
if (override === "0" || override === "false") return false;
|
|
26
|
-
} catch {
|
|
27
|
-
// ignore -- window.location may be unavailable
|
|
28
|
-
}
|
|
29
|
-
return !!getCastleEmbed()?.edit;
|
|
30
|
-
}
|
package/src/errors.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
export interface GraphqlErrorPayload {
|
|
2
|
-
message?: string;
|
|
3
|
-
extensions?: Record<string, unknown>;
|
|
4
|
-
path?: Array<string | number>;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
interface CastleErrorInput {
|
|
8
|
-
code: string;
|
|
9
|
-
message: string;
|
|
10
|
-
operation?: string;
|
|
11
|
-
status?: number;
|
|
12
|
-
extensions?: Record<string, unknown>;
|
|
13
|
-
errors?: GraphqlErrorPayload[];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export class CastleError extends Error {
|
|
17
|
-
code: string;
|
|
18
|
-
operation?: string;
|
|
19
|
-
status?: number;
|
|
20
|
-
extensions?: Record<string, unknown>;
|
|
21
|
-
errors?: GraphqlErrorPayload[];
|
|
22
|
-
|
|
23
|
-
constructor(input: CastleErrorInput) {
|
|
24
|
-
super(input.message);
|
|
25
|
-
this.name = "CastleError";
|
|
26
|
-
this.code = input.code;
|
|
27
|
-
this.operation = input.operation;
|
|
28
|
-
this.status = input.status;
|
|
29
|
-
this.extensions = input.extensions;
|
|
30
|
-
this.errors = input.errors;
|
|
31
|
-
}
|
|
32
|
-
}
|