castle-web-sdk 0.4.30 → 0.4.32
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 +73 -0
- package/dist/castle.d.ts +2 -0
- package/dist/castle.js +1 -0
- package/dist/commands.d.ts +26 -0
- package/dist/portal.d.ts +3 -2
- package/dist/portal.js +22 -0
- package/dist/snapshots.d.ts +12 -0
- package/dist/snapshots.js +128 -0
- package/dist/transport.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@ import { setup, initCard, Store, Leaderboard } from "castle-web-sdk";
|
|
|
22
22
|
- [Time](#time)
|
|
23
23
|
- [User](#user)
|
|
24
24
|
- [Pass](#pass)
|
|
25
|
+
- [Snapshot](#snapshot)
|
|
25
26
|
- [Portal](#portal)
|
|
26
27
|
- [Haptics](#haptics)
|
|
27
28
|
- [Lifecycle](#lifecycle)
|
|
@@ -499,6 +500,53 @@ if (status === "purchased" || status === "alreadyOwned") {
|
|
|
499
500
|
}
|
|
500
501
|
```
|
|
501
502
|
|
|
503
|
+
## Snapshot
|
|
504
|
+
|
|
505
|
+
A snapshot is a picture of the deck's current frame that a player collects
|
|
506
|
+
into their Castle inventory, free or for bricks. It is the same item a
|
|
507
|
+
classic deck makes with its "offer snapshot" rule. The player can also take
|
|
508
|
+
one any time with the camera button in the feed; the deck is not involved in
|
|
509
|
+
that one.
|
|
510
|
+
|
|
511
|
+
### `Snapshot.offer(options?): Promise<SnapshotOfferResult>`
|
|
512
|
+
|
|
513
|
+
Captures the current frame and shows Castle's collect card over it; resolves
|
|
514
|
+
when the player collects or closes it. `options`:
|
|
515
|
+
|
|
516
|
+
- `title`, `description` — shown on the card. With no title the card reads
|
|
517
|
+
"A snapshot of @creator's deck".
|
|
518
|
+
- `price` — bricks. `0` or absent is free; otherwise a whole number from 5 to
|
|
519
|
+
9995. Anything else throws `INVALID_ARGUMENT`.
|
|
520
|
+
|
|
521
|
+
Collecting works only in the Castle mobile app; the website shows "collect in
|
|
522
|
+
the app" and the dev server a short notice, and both resolve `unavailable`.
|
|
523
|
+
`SnapshotOfferResult` has a `status`:
|
|
524
|
+
|
|
525
|
+
- `'collected'` — the player collected it.
|
|
526
|
+
- `'cancelled'` — the player closed the card.
|
|
527
|
+
- `'unavailable'` — can't collect here, or another snapshot card is already
|
|
528
|
+
open (a second `offer` while one is open gets this and the first carries on).
|
|
529
|
+
|
|
530
|
+
```js
|
|
531
|
+
const { status } = await Snapshot.offer({
|
|
532
|
+
title: "Level 3 cleared",
|
|
533
|
+
description: `${score} points`,
|
|
534
|
+
});
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
### `Snapshot.onTaken(handler): () => void`
|
|
538
|
+
|
|
539
|
+
Registers a function called just before an offer's capture, the twin of the
|
|
540
|
+
classic "snapshot taken" trigger. Change what's on screen in it — hide the
|
|
541
|
+
HUD, pose the character — and the capture shows the next painted frame.
|
|
542
|
+
Returns a function that unregisters it. The camera button never calls it.
|
|
543
|
+
|
|
544
|
+
```js
|
|
545
|
+
Snapshot.onTaken(() => {
|
|
546
|
+
hud.hidden = true;
|
|
547
|
+
});
|
|
548
|
+
```
|
|
549
|
+
|
|
502
550
|
## Portal
|
|
503
551
|
|
|
504
552
|
A portal sends the player from this deck to another Castle deck,
|
|
@@ -543,6 +591,31 @@ link.addEventListener("pointerenter", () => {
|
|
|
543
591
|
});
|
|
544
592
|
```
|
|
545
593
|
|
|
594
|
+
### `Portal.info(deckId): Promise<PortalInfoResult>`
|
|
595
|
+
|
|
596
|
+
Where a portal leads: the destination deck's title, creator and cover, for a
|
|
597
|
+
sign or label on the portal. A deck can't reach Castle's API or load remote
|
|
598
|
+
images itself, so this is how it shows another deck's details. It needs a
|
|
599
|
+
Castle login, which the app and a signed-in website visitor have; the cloud
|
|
600
|
+
editor's preview and a signed-out visitor get `unavailable`, so keep your own
|
|
601
|
+
label as the fallback. `PortalInfoResult` has a `status`:
|
|
602
|
+
|
|
603
|
+
- `'ok'` — also has `deckId`, `title`, `creator` (`{ userId, username }` or
|
|
604
|
+
`null`), and `coverUrl`, a `data:` url of the deck's cover or `null`.
|
|
605
|
+
- `'broken'` — the portal wouldn't work for this player: the deck doesn't
|
|
606
|
+
exist, has portals turned off, or isn't visible to them.
|
|
607
|
+
- `'unavailable'` — the host couldn't look it up.
|
|
608
|
+
|
|
609
|
+
```js
|
|
610
|
+
const info = await Portal.info(doorway.deckId);
|
|
611
|
+
if (info.status === "ok") {
|
|
612
|
+
sign.textContent = `${info.title} by @${info.creator?.username ?? "?"}`;
|
|
613
|
+
if (info.coverUrl) sign.style.backgroundImage = `url(${info.coverUrl})`;
|
|
614
|
+
}
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
Answers are kept per deck for the session, so asking again is cheap.
|
|
618
|
+
|
|
546
619
|
## Haptics
|
|
547
620
|
|
|
548
621
|
`Haptics` plays a short device vibration ("buzz") for tactile feedback —
|
package/dist/castle.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export { registerParams } from "./paramsRegistry";
|
|
|
18
18
|
export type { TunedParam, TunedParamValue } from "./paramsRegistry";
|
|
19
19
|
export { Pass } from "./passes";
|
|
20
20
|
export type { CastlePassApi, PassOfferResult, PassOfferStatus, } from "./passes";
|
|
21
|
+
export { Snapshot } from "./snapshots";
|
|
22
|
+
export type { CastleSnapshotApi, SnapshotOfferOptions, SnapshotOfferResult, SnapshotOfferStatus, } from "./snapshots";
|
|
21
23
|
export { Portal } from "./portal";
|
|
22
24
|
export type { CastlePortalApi, PortalOpenResult, PortalOpenStatus, PortalPrefetchResult, PortalPrefetchStatus, } from "./portal";
|
|
23
25
|
export { CARD_RATIO, deleteFile, fileUrl, initCard, onBeforeRestart, onFilesChanged, onSaveReloadState, openFile, renameFile, requestReload, setup, takeReloadState, writeFileOnce, } from "./runtime";
|
package/dist/castle.js
CHANGED
|
@@ -10,6 +10,7 @@ export { Multiplayer } from "./multiplayer";
|
|
|
10
10
|
export { MAX_CLIENT_FRAME_BYTES, MAX_PLAYERS_CAP } from "./multiplayerProtocol";
|
|
11
11
|
export { registerParams } from "./paramsRegistry";
|
|
12
12
|
export { Pass } from "./passes";
|
|
13
|
+
export { Snapshot } from "./snapshots";
|
|
13
14
|
export { Portal } from "./portal";
|
|
14
15
|
export { CARD_RATIO, deleteFile, fileUrl, initCard, onBeforeRestart, onFilesChanged, onSaveReloadState, openFile, renameFile, requestReload, setup, takeReloadState, writeFileOnce, } from "./runtime";
|
|
15
16
|
export { flushSaves, hasPendingSave, onSaveState, writeFile, } from "./saveQueue";
|
package/dist/commands.d.ts
CHANGED
|
@@ -48,11 +48,27 @@ export type PassOfferStatus = "purchased" | "alreadyOwned" | "cancelled" | "unav
|
|
|
48
48
|
export interface PassOfferResult {
|
|
49
49
|
status: PassOfferStatus;
|
|
50
50
|
}
|
|
51
|
+
export type SnapshotOfferStatus = "collected" | "cancelled" | "unavailable";
|
|
52
|
+
export interface SnapshotOfferResult {
|
|
53
|
+
status: SnapshotOfferStatus;
|
|
54
|
+
}
|
|
51
55
|
export type PortalOpenStatus = "navigating" | "unavailable";
|
|
52
56
|
export interface PortalOpenResult {
|
|
53
57
|
status: PortalOpenStatus;
|
|
54
58
|
}
|
|
55
59
|
export type PortalPrefetchStatus = "prefetching" | "rejected" | "unavailable";
|
|
60
|
+
export type PortalInfoResult = {
|
|
61
|
+
status: "ok";
|
|
62
|
+
deckId: string;
|
|
63
|
+
title: string;
|
|
64
|
+
creator: {
|
|
65
|
+
userId: string;
|
|
66
|
+
username: string;
|
|
67
|
+
} | null;
|
|
68
|
+
coverUrl: string | null;
|
|
69
|
+
} | {
|
|
70
|
+
status: "broken" | "unavailable";
|
|
71
|
+
};
|
|
56
72
|
export interface PortalPrefetchResult {
|
|
57
73
|
status: PortalPrefetchStatus;
|
|
58
74
|
}
|
|
@@ -171,12 +187,20 @@ export interface CommandParams {
|
|
|
171
187
|
"pass.offer": {
|
|
172
188
|
passId: string;
|
|
173
189
|
};
|
|
190
|
+
"snapshot.offer": {
|
|
191
|
+
title: string;
|
|
192
|
+
description: string;
|
|
193
|
+
bricksPrice: number;
|
|
194
|
+
};
|
|
174
195
|
"portal.open": {
|
|
175
196
|
targetDeckId: string;
|
|
176
197
|
};
|
|
177
198
|
"portal.prefetch": {
|
|
178
199
|
targetDeckId: string;
|
|
179
200
|
};
|
|
201
|
+
"portal.info": {
|
|
202
|
+
targetDeckId: string;
|
|
203
|
+
};
|
|
180
204
|
"haptics.play": {
|
|
181
205
|
style: HapticStyle;
|
|
182
206
|
};
|
|
@@ -294,8 +318,10 @@ export interface CommandResult {
|
|
|
294
318
|
hasPass: boolean;
|
|
295
319
|
};
|
|
296
320
|
"pass.offer": PassOfferResult;
|
|
321
|
+
"snapshot.offer": SnapshotOfferResult;
|
|
297
322
|
"portal.open": PortalOpenResult;
|
|
298
323
|
"portal.prefetch": PortalPrefetchResult;
|
|
324
|
+
"portal.info": PortalInfoResult;
|
|
299
325
|
"haptics.play": HapticsResult;
|
|
300
326
|
"multiplayer.getSession": MultiplayerGetSessionResult;
|
|
301
327
|
"characters.open": CharacterAdmission;
|
package/dist/portal.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { PortalOpenResult, PortalPrefetchResult } from "./commands";
|
|
2
|
-
export type { PortalOpenResult, PortalOpenStatus, PortalPrefetchResult, PortalPrefetchStatus, } from "./commands";
|
|
1
|
+
import type { PortalInfoResult, PortalOpenResult, PortalPrefetchResult } from "./commands";
|
|
2
|
+
export type { PortalInfoResult, PortalOpenResult, PortalOpenStatus, PortalPrefetchResult, PortalPrefetchStatus, } from "./commands";
|
|
3
3
|
export interface CastlePortalApi {
|
|
4
4
|
open(deckId: string): Promise<PortalOpenResult>;
|
|
5
5
|
prefetch(deckId: string): Promise<PortalPrefetchResult>;
|
|
6
|
+
info(deckId: string): Promise<PortalInfoResult>;
|
|
6
7
|
}
|
|
7
8
|
export declare const Portal: CastlePortalApi;
|
package/dist/portal.js
CHANGED
|
@@ -7,11 +7,16 @@
|
|
|
7
7
|
// Navigation is effectively fire-and-forget: once the host accepts it, the
|
|
8
8
|
// source deck is torn down as the feed moves to the target, so a deck should
|
|
9
9
|
// not rely on code running after a successful `open`.
|
|
10
|
+
//
|
|
11
|
+
// `info` is a plain read of where a portal leads: the target's title, creator
|
|
12
|
+
// and cover, for a sign on the portal. It works on every host that has a
|
|
13
|
+
// Castle login.
|
|
10
14
|
import { CastleError } from "./errors";
|
|
11
15
|
import { hostRequest } from "./transport";
|
|
12
16
|
export const Portal = {
|
|
13
17
|
open,
|
|
14
18
|
prefetch,
|
|
19
|
+
info,
|
|
15
20
|
};
|
|
16
21
|
async function open(deckId) {
|
|
17
22
|
if (typeof deckId !== "string" || deckId.length === 0) {
|
|
@@ -33,3 +38,20 @@ async function prefetch(deckId) {
|
|
|
33
38
|
}
|
|
34
39
|
return hostRequest("portal.prefetch", { targetDeckId: deckId });
|
|
35
40
|
}
|
|
41
|
+
async function info(deckId) {
|
|
42
|
+
if (typeof deckId !== "string" || deckId.length === 0) {
|
|
43
|
+
throw new CastleError({
|
|
44
|
+
code: "INVALID_ARGUMENT",
|
|
45
|
+
message: "Portal.info requires a deckId.",
|
|
46
|
+
operation: "Portal.info",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
return await hostRequest("portal.info", { targetDeckId: deckId });
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// No login, an old host, or a failed lookup all read the same to a deck:
|
|
54
|
+
// there is nothing to show, so it keeps whatever label it had.
|
|
55
|
+
return { status: "unavailable" };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SnapshotOfferResult } from "./commands";
|
|
2
|
+
export type { SnapshotOfferResult, SnapshotOfferStatus } from "./commands";
|
|
3
|
+
export interface SnapshotOfferOptions {
|
|
4
|
+
title?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
price?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface CastleSnapshotApi {
|
|
9
|
+
offer(options?: SnapshotOfferOptions): Promise<SnapshotOfferResult>;
|
|
10
|
+
onTaken(handler: () => void): () => void;
|
|
11
|
+
}
|
|
12
|
+
export declare const Snapshot: CastleSnapshotApi;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Snapshot: offer the player a collectible picture of the deck's current frame,
|
|
2
|
+
// the same item a classic deck makes with its "offer snapshot" rule.
|
|
3
|
+
//
|
|
4
|
+
// The host captures the frame and shows its own collect card; the deck gets one
|
|
5
|
+
// result back. `onTaken` is the twin of the classic "snapshot taken" trigger: it
|
|
6
|
+
// runs just before an offer's capture, so the deck can pose the frame. The
|
|
7
|
+
// camera button in the feed takes a snapshot without the deck, and never calls it.
|
|
8
|
+
//
|
|
9
|
+
// - mobile app : the collect card over the captured frame, then the confirm sheet
|
|
10
|
+
// - web player : "collect this snapshot in the app", returns `unavailable`
|
|
11
|
+
// - dev CLI : a small notice, returns `unavailable`
|
|
12
|
+
import { CastleError } from "./errors";
|
|
13
|
+
import { getCommandChannel, hostRequest } from "./transport";
|
|
14
|
+
export const Snapshot = {
|
|
15
|
+
offer,
|
|
16
|
+
onTaken,
|
|
17
|
+
};
|
|
18
|
+
const MIN_BRICKS_PRICE = 5;
|
|
19
|
+
const MAX_BRICKS_PRICE = 9995;
|
|
20
|
+
const takenHandlers = new Set();
|
|
21
|
+
// An offer is open until its result comes back. A second call meanwhile is
|
|
22
|
+
// ignored: it resolves `unavailable` and the open card goes on.
|
|
23
|
+
let offerOpen = false;
|
|
24
|
+
function onTaken(handler) {
|
|
25
|
+
takenHandlers.add(handler);
|
|
26
|
+
return () => {
|
|
27
|
+
takenHandlers.delete(handler);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
async function offer(options = {}) {
|
|
31
|
+
const title = options.title ?? "";
|
|
32
|
+
const description = options.description ?? "";
|
|
33
|
+
const price = options.price ?? 0;
|
|
34
|
+
if (typeof title !== "string" || typeof description !== "string") {
|
|
35
|
+
throw new CastleError({
|
|
36
|
+
code: "INVALID_ARGUMENT",
|
|
37
|
+
message: "Snapshot.offer title and description must be strings.",
|
|
38
|
+
operation: "Snapshot.offer",
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
if (!Number.isInteger(price) ||
|
|
42
|
+
(price !== 0 && (price < MIN_BRICKS_PRICE || price > MAX_BRICKS_PRICE))) {
|
|
43
|
+
throw new CastleError({
|
|
44
|
+
code: "INVALID_ARGUMENT",
|
|
45
|
+
message: `Snapshot.offer price must be 0 or a whole number of bricks from ${MIN_BRICKS_PRICE} to ${MAX_BRICKS_PRICE}.`,
|
|
46
|
+
operation: "Snapshot.offer",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (offerOpen) {
|
|
50
|
+
return { status: "unavailable" };
|
|
51
|
+
}
|
|
52
|
+
offerOpen = true;
|
|
53
|
+
try {
|
|
54
|
+
// Give the deck its "snapshot taken" moment, then let a frame paint so the
|
|
55
|
+
// capture shows whatever the handlers changed.
|
|
56
|
+
for (const handler of [...takenHandlers]) {
|
|
57
|
+
try {
|
|
58
|
+
handler();
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
console.warn("Castle Snapshot.onTaken handler threw.", error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
await nextPaintedFrame();
|
|
65
|
+
const result = await hostRequest("snapshot.offer", {
|
|
66
|
+
title,
|
|
67
|
+
description,
|
|
68
|
+
bricksPrice: price,
|
|
69
|
+
});
|
|
70
|
+
// The dev server has no collect card, so say why nothing happened.
|
|
71
|
+
if (result.status === "unavailable" && getCommandChannel() === "local") {
|
|
72
|
+
showDevUnavailableNotice();
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
offerOpen = false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Two animation frames: the first runs after the handlers' changes are queued,
|
|
81
|
+
// the second after that frame has been painted.
|
|
82
|
+
function nextPaintedFrame() {
|
|
83
|
+
if (typeof requestAnimationFrame === "undefined") {
|
|
84
|
+
return Promise.resolve();
|
|
85
|
+
}
|
|
86
|
+
return new Promise((resolve) => {
|
|
87
|
+
requestAnimationFrame(() => {
|
|
88
|
+
requestAnimationFrame(() => resolve());
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
let devNoticeEl = null;
|
|
93
|
+
let devNoticeTimer = null;
|
|
94
|
+
function showDevUnavailableNotice() {
|
|
95
|
+
if (typeof document === "undefined") {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (!devNoticeEl) {
|
|
99
|
+
devNoticeEl = document.createElement("div");
|
|
100
|
+
devNoticeEl.textContent = "Snapshots can only be collected in the Castle app.";
|
|
101
|
+
devNoticeEl.style.cssText = [
|
|
102
|
+
"position:fixed",
|
|
103
|
+
"left:50%",
|
|
104
|
+
"bottom:24px",
|
|
105
|
+
"transform:translateX(-50%)",
|
|
106
|
+
"max-width:80vw",
|
|
107
|
+
"padding:10px 16px",
|
|
108
|
+
"border-radius:8px",
|
|
109
|
+
"background:rgba(0,0,0,0.82)",
|
|
110
|
+
"color:#fff",
|
|
111
|
+
"font:500 13px/1.4 system-ui,sans-serif",
|
|
112
|
+
"text-align:center",
|
|
113
|
+
"z-index:2147483647",
|
|
114
|
+
"pointer-events:none",
|
|
115
|
+
"transition:opacity 0.3s ease",
|
|
116
|
+
].join(";");
|
|
117
|
+
document.body.appendChild(devNoticeEl);
|
|
118
|
+
}
|
|
119
|
+
devNoticeEl.style.opacity = "1";
|
|
120
|
+
if (devNoticeTimer) {
|
|
121
|
+
clearTimeout(devNoticeTimer);
|
|
122
|
+
}
|
|
123
|
+
devNoticeTimer = setTimeout(() => {
|
|
124
|
+
if (devNoticeEl) {
|
|
125
|
+
devNoticeEl.style.opacity = "0";
|
|
126
|
+
}
|
|
127
|
+
}, 3200);
|
|
128
|
+
}
|
package/dist/transport.js
CHANGED