castle-web-sdk 0.4.46 → 0.4.48

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 CHANGED
@@ -26,12 +26,15 @@ import { setup, initCard, Store, Leaderboard } from "castle-web-sdk";
26
26
  - [Pass](#pass)
27
27
  - [Snapshot](#snapshot)
28
28
  - [Portal](#portal)
29
+ - [Social](#social)
30
+ - [Party](#party)
29
31
  - [Hub](#hub)
30
32
  - [Decks](#decks)
31
33
  - [Clipboard](#clipboard)
32
34
  - [Haptics](#haptics)
33
35
  - [Agent](#agent)
34
36
  - [Lifecycle](#lifecycle)
37
+ - [Display](#display)
35
38
  - [Multiplayer](#multiplayer)
36
39
  - [Setup](#setup)
37
40
  - [Saving files](#writefilepath-contents-promisevoid)
@@ -65,6 +68,18 @@ reconnects temporary socket drops automatically. `onStateChange` reports
65
68
  `acquiring`, `connecting`, `connected`, `reconnecting`, and `closed`; its optional
66
69
  metadata distinguishes a server `bye`, a replaced server version, an explicit
67
70
  `leave`, and connection failures.
71
+
72
+ `connection.serverBundleId` and `connection.serverBundleSha256` identify the server publication running the connected session. These read-only diagnostics are `null` before a live connection, while disconnected, in solo mode, or when the host does not provide them. The ID identifies a publication; the SHA-256 identifies its code bytes, so two publications can share a hash. Read the values again after a reconnect or a destination join. They describe the running server, not the client bundle or the deck’s newest save.
73
+
74
+ Experimental: `Multiplayer.join({ mode: "public", pongTimeoutMs: 10_000 })`
75
+ reconnects if a keepalive ping receives no pong within the given milliseconds.
76
+ Omitting the option (or passing `0`) preserves the existing keepalive behavior.
77
+ The deadline starts at a ping, normally sent every 25 seconds; it is not a
78
+ deadline for game messages. A deadline callback delayed by more than one second (for example during page
79
+ suspension) sends one fresh ping and waits a full deadline before reconnecting. Only one
80
+ such extension is allowed without a pong, so repeated timer throttling cannot
81
+ postpone recovery indefinitely.
82
+
68
83
  During local development, reloading a Play panel restores that panel's session
69
84
  when its dev server is still running. A separate Play panel joins separately.
70
85
 
@@ -147,10 +162,34 @@ export default {
147
162
 
148
163
  `session.storage.deck` is shared by everyone playing.
149
164
  `session.storage.user(userId)` is one player's public data.
165
+ `await session.storage.playthrough(userId)` opens that player's generation-bound
166
+ game state. Its `resetGeneration` is present only after an administrator has
167
+ reset that player's progress for this deck. It is a monotonic reset marker,
168
+ separate from the handle's `generation`, which fences playthrough operations.
169
+ Deck-scope storage writes are not fenced by either generation; a server using
170
+ `session.storage.deck` must discard its own stale queued writes and cached
171
+ per-player values when it observes a newer reset marker.
150
172
  `session.storage.board(name)` is a leaderboard. The root and its buckets also
151
173
  support the [atomic save API](#atomic-saves). Each scope has the same
152
174
  `get`, `set`, `remove`, `increment` and `list` as `Store`.
153
175
 
176
+ `session.storage.town(userId)` lets a door deck's server read and write one
177
+ signed-in player's rows in its source town's deck scope, for things such as
178
+ hats bought in town. It has `get`, `set`, `remove` and `increment`; quota counts
179
+ against the town. Every key is one of the town's per-player namespaces
180
+ (`tokens`, `spent`, `own`, `hat`, `chirp`) followed by the user id: for user
181
+ `7`, `own:7:tophat` and `hat:7` work; `own:tophat:7` and `quay:7` do not. The
182
+ rest of the town's rows are its own. The player must be live in this session
183
+ (a player who joined a few seconds ago can briefly get
184
+ `CAULDRON_HUB_NOT_IN_SESSION`, so retry later rather than giving up); in a house, only the owner's rows are
185
+ accessible. Calls reject with `CAULDRON_HUB_NO_TOWN` without a source town
186
+ (including a town calling it), `CAULDRON_HUB_NOT_IN_SESSION` for an absent
187
+ player, or `CAULDRON_HUB_NOT_OWNER` for someone other than the house's owner.
188
+ Invalid key segments reject with `CAULDRON_STORAGE_INVALID_ARGUMENT`.
189
+ During dev serve, keys live under `town/` in the dev deck's own deck scope,
190
+ so testing a door never changes a real town. An unpublished deck still needs
191
+ a published deck id for storage.
192
+
154
193
  Two differences follow from a session server not being a player:
155
194
 
156
195
  - **It owns `deck` scope.** Once a deck has a published server, clients read
@@ -173,8 +212,16 @@ kit's contract). `save-deck` publishes the block's titles and result shapes with
173
212
  the server bundle, and Castle pins them to every session of the place. Those
174
213
  sessions have `session.hub`; every other session has `session.hub === null`.
175
214
 
215
+ `session.hub.owner` is the owner's user id string on an owner door instance,
216
+ passed by the platform. It is absent on every other hub session, including the
217
+ dev serve.
218
+
176
219
  - `session.hub.place`, `.title` and `.result` are the place and the result shape
177
220
  as published (`result` is `null` for a place that reports none, such as a town).
221
+ - `session.hub.reconnectWindowMs`, when present, is Castle's pinned hub-place
222
+ reconnect grace in milliseconds, inclusive at the deadline. It is absent on
223
+ sessions without a pinned grace and older hosts. Kits use it to match visit retention to the
224
+ transport window; deck code does not choose it.
178
225
  - `session.hub.arrival(playerId)` is how a player came in when Castle placed them
179
226
  through a hub door: `{ fromDeckId, doorId }` (`doorId` is `null` when the door
180
227
  names none), else `null`. It is Castle's word, never a client's, and it is also
@@ -199,6 +246,46 @@ sessions have `session.hub`; every other session has `session.hub === null`.
199
246
  id the kit draws, `color` `'#rrggbb'`, `rarity` 0-9), or clears it with `null`;
200
247
  the latest wins. Castle takes it only from a curated hub place. Resolves
201
248
  `{ wearable }`; `readResults` returns it as `players[userId].wearable`.
249
+ - `session.hub.unlock(playerId, itemId)` gives a signed-in player one of the
250
+ deck's declared earned items (castle.json `hub.unlocks`: `{ id, class, shape,
251
+ name, deed, color }`, at most 8, `class` one of `badge`, `pin`, `belt`, `sash`,
252
+ `held`). v1.70 also permits up to 6 home items separately: `furniture` or
253
+ `deed`, with `piece` in place of `shape` (the same lowercase id format).
254
+ Each id is unique across both lists. Castle takes it only from a curated hub
255
+ place and only for an id the saved deck declares; a first wearable unlock
256
+ is also shown. Resolves
257
+ `{ unlocked: true, isNew, earned }`, where `earned` is what the player now
258
+ shows (`{ deckId, id, class, shape, name, deed, color }`) or `null`, including
259
+ for home grants. A home grant never changes what the avatar shows; a home
260
+ item appears only in the owned list returned by `readUnlocks`.
261
+ `readResults` returns the shown one as `players[userId].earned`, absent when
262
+ none.
263
+ - `session.hub.wearEarned(playerId, { deckId, id } | null)` chooses which owned
264
+ earned item a player shows, or none. Castle takes it only from the town.
265
+ Home items are refused with `CAULDRON_HUB_EARNED_NOT_WEARABLE`.
266
+ Resolves `{ earned }`.
267
+ - `session.hub.readUnlocks(userIds)` reads the earned items players here own:
268
+ `{ players: { [userId]: { owned: [...] } } }`, including home items with
269
+ their `class` and `piece`. `MultiplayerHubEarnedItem` and
270
+ `MultiplayerHubEarnedClass` describe wearable items only;
271
+ `MultiplayerHubHomeItem` and `MultiplayerHubHomeClass` describe home items.
272
+ Only the owned list uses the union of both item types; unlock/wear receipts
273
+ and arrival results keep the wearable-only type.
274
+ - These three reject with `CAULDRON_HUB_UNAVAILABLE` on a runtime without them.
275
+ - `session.hub.friendsAmong(playerId, userIds)` resolves the subset of candidate
276
+ user ids that are this connected player's friends (`Promise<string[]>`).
277
+ The platform checks mutual follows, age, blocks, the hub gate and the player's
278
+ recent client friends ask. It uses `hubFriendsAmong` or the older runtime's
279
+ storage broker and rejects on platform errors. The hub-game kit wraps it with
280
+ decimal-id filtering, a 100-candidate cap, caching and a silent `[]` fallback.
281
+ - `session.hub.relationsAmong(playerId, userIds)` makes the same single host read
282
+ and resolves `{ friends, follows, followedBy }`, each limited to asked string
283
+ ids. Missing lists are empty on older hosts. Mutual friends keep the chat age
284
+ and block rules; follows in either direction require no block, with no age rule.
285
+ These lists belong to the viewer only and must never go to another client.
286
+ Optional third argument `{ strict: true }` rejects malformed raw lists before
287
+ normalization and adds `validated: true` to a successful result. Without it,
288
+ the existing response shape and older-host behavior remain unchanged.
202
289
  - `session.hub.hiddenPair(playerIdA, playerIdB)` is true when a block hides these
203
290
  two players from each other (either one blocked the other). Never send either
204
291
  one the other's pose, avatar or name. The block lists themselves never reach
@@ -567,6 +654,61 @@ claim can fail with writer loss while `committed === true` and its original
567
654
  receipt; it never takes ownership again. Transport ambiguity keeps the outcome
568
655
  unknown until receipt lookup or identical replay settles it.
569
656
 
657
+ ### Conditional writes and recovery (gated)
658
+
659
+ `Store.conditionalWrite(input)` and `session.storage.conditionalWrite(input)`
660
+ use the same request and receipt as `transaction`. Read with `snapshot`, then
661
+ pass its `access` and each target's `revision` as `expectedAccess` and
662
+ `expectedRevision`. Use `null` to create an absent target. A revision belongs
663
+ to a **prefix**, so changing any key in it changes the revision. For an
664
+ independent value, give it a separate, non-overlapping prefix.
665
+
666
+ This additive API is off by default. The platform must enable conditional
667
+ storage for the deck after its database index and host support are ready.
668
+ Older hosts and the dev memory store do not support it. Keep a dependent
669
+ feature off when the API is unavailable; never fall back to ordinary writes.
670
+ Existing `get`, `set` and save methods keep their behavior.
671
+
672
+ One call changes up to eight targets, with up to eight explicit key changes
673
+ per target, across the same deck and at most two players. Every target needs
674
+ its expected revision. A read-only `check` can use `expectedRevision: null`
675
+ to require absence without creating that target. All changes commit together. Clients may address their
676
+ own user/private targets and writable deck targets; only the deck server can
677
+ condition on another player's user target. Servers cannot access private data.
678
+ Existing quota, byte and rate allowances still apply. A prefix replacement
679
+ also removes its previous rows, subject to those allowances.
680
+
681
+ On a revision conflict, `SaveError.code` is
682
+ `CAULDRON_STORAGE_REVISION_CONFLICT`, `committed` is `false`, and `current`
683
+ contains the current values and revisions read at the failed comparison.
684
+ If that full snapshot exceeds the byte allowance, the call fails with
685
+ `CAULDRON_STORAGE_TOO_LARGE`; no changes commit. Keep the player's proposed
686
+ change and reload before offering another choice.
687
+
688
+ Use a unique `operationId` for each action. Retrying the **identical request**
689
+ after a lost reply returns its original committed receipt, even if values
690
+ have since changed. Reusing the ID with different input is refused. Failed
691
+ comparisons do not create receipts. Receipts have no automatic expiry and
692
+ are retained beyond 24 hours; an explicit data/account reset may remove them.
693
+ Never reuse old operation IDs or access tokens after a reset.
694
+
695
+ `recentOperations({ limit?, cursor? })`, on both root APIs, discovers the
696
+ caller's committed operation IDs after a restart. It returns
697
+ `{ operations: [{ operationId, committedAt, receipt }], cursor }`, newest
698
+ first. `limit` defaults to 16 and is at most 32; pass the returned cursor for
699
+ the next page. This lists existing save receipts too. Client and server
700
+ histories are separate; all layers of the same deck share the server history.
701
+ Use a smaller limit if the reply exceeds the byte allowance. A history page
702
+ is discovery, not a snapshot of current values: reload with `snapshot` before
703
+ building the next change. `operation(id)` still looks up a known ID.
704
+
705
+ Pairing can atomically create a project and reserve both players' recovery
706
+ entries. The game server must still validate consent, membership and editing
707
+ leases. A market sale can atomically update stock, the daily cap and the
708
+ purse, once **every** writer of those values uses the managed targets.
709
+ Durable game recovery entries belong in those targets; recent operation
710
+ receipts alone do not define permanent project ownership or sale policy.
711
+
570
712
  ### Save coordinator
571
713
 
572
714
  `createSaveCoordinator`, from `castle-web-sdk` or `castle-web-sdk/server`,
@@ -895,7 +1037,7 @@ itself (ones Castle runs across decks), on the serve only.
895
1037
 
896
1038
  The player's platform avatar: one per account, worn in every hub place
897
1039
  (cauldron-game-server `docs/HUB.md`, decision 4). In v1 it is an index into the
898
- hub-game kit's character templates; there is no editor and no cosmetics. The
1040
+ hub-game kit's character templates, with an optional colour choice. The
899
1041
  hub-game kit is the caller: it dresses the player and offers the choice in its
900
1042
  picker. Other players' avatars are not read here; a place's server learns them
901
1043
  from ghost.
@@ -907,7 +1049,7 @@ every account has (`chosen: false`). Anonymous accounts have one too.
907
1049
  `{ status: "unavailable", avatar: null }` for a signed-out visitor; a host from
908
1050
  before avatars rejects with `UNKNOWN_COMMAND`. Either way the deck picks its own.
909
1051
 
910
- ### `Avatar.set(avatar): Promise<AvatarResult>`
1052
+ ### `Avatar.set(avatar, options?: { color?: number }): Promise<AvatarResult>`
911
1053
 
912
1054
  Makes `avatar` the account's from now on, everywhere. Only a curated hub place
913
1055
  (a deck an enabled hub door leads into or out of) may: anywhere else, and for a
@@ -915,6 +1057,15 @@ signed-out visitor, the answer is `unavailable` and nothing changes. Ghost
915
1057
  refuses an index outside its template set (`INVALID_ARGUMENT`) and more than 30
916
1058
  choices a minute (`RATE_LIMIT_EXCEEDED`).
917
1059
 
1060
+ A host that supports avatar colours includes `color` in `AvatarResult`; the
1061
+ field is absent from older hosts. `Avatar.colors === true` marks an SDK whose
1062
+ `Avatar.set` carries `{ color }`; the hub-game kit requires both the host field
1063
+ and this marker to offer platform colours. Only after that support is known, pass
1064
+ `{ color }` to choose a non-negative integer colour for the current avatar.
1065
+ The host accepts 0–255; ghost holds the offered range. Changing character
1066
+ without options clears its colour. Omitting colour sends the existing command
1067
+ and mutation unchanged. The read query will ask for colour once ghost serves it.
1068
+
918
1069
  ## Pass
919
1070
 
920
1071
  A pass is something a creator sells to players for Castle bricks (the
@@ -1008,14 +1159,111 @@ Snapshot.onTaken(() => {
1008
1159
  });
1009
1160
  ```
1010
1161
 
1162
+ ## Social
1163
+
1164
+ `Social` gives a Cauldron deck the signed-in player's mutual Castle follows.
1165
+ The host supplies the player and current deck from trusted play context; a deck
1166
+ cannot ask about another account or deck. A host from before Social rejects with
1167
+ `CastleError` code `UNKNOWN_COMMAND`.
1168
+
1169
+ - `Social.friends()` returns at most 20 mutual follows as
1170
+ `{ userId, username, photoUrl? }`. A one-way follow is not a friend here.
1171
+ - `Social.presence()` returns the mutual friends playing this deck now as
1172
+ `{ userId, username, photoUrl?, sessionId, routeId?, slot?, updatedAt }`.
1173
+ Castle's now-playing privacy and multiplayer gates apply. It fails closed to
1174
+ `[]` when presence is gated or unavailable.
1175
+
1176
+ Presence has no event in v1. Poll `Social.presence()` when the game needs to
1177
+ refresh its list; do not treat `updatedAt` as a durable offline record.
1178
+
1179
+ ```js
1180
+ import { Social } from "castle-web-sdk";
1181
+
1182
+ const friends = await Social.friends();
1183
+ const playingHere = await Social.presence();
1184
+ ```
1185
+
1186
+ ## Party
1187
+
1188
+ `Party` creates and drives Castle parties with the signed-in player's account.
1189
+ Castle still enforces membership, roles, age and party permissions; refusals are
1190
+ `CastleError`s carrying the server's code. Party and user-id arrays are bounded
1191
+ to 32 users.
1192
+
1193
+ - `Party.current()` returns the party this play was launched with as
1194
+ `{ partyId, name, status, hasUnreadMessages }`, or `null`. The party id comes
1195
+ only from trusted host context, and no request is made when the play has none.
1196
+ - `Party.create(userIds)` creates a party containing those users and the
1197
+ signed-in player. It never sets an initial deck. Creating one does not change
1198
+ `Party.current()` or the launch party used by
1199
+ `Multiplayer.join({ mode: "party" })`.
1200
+ - `Party.invite(partyId, userIds)` invites more users.
1201
+ - `Party.respond(partyId, accept)` accepts or rejects an invitation.
1202
+ - `Party.members(partyId)` returns
1203
+ `{ userId, username, photoUrl?, status, role }` rows. `role` is `member`,
1204
+ `host` or `owner`.
1205
+ - `Party.leave(partyId)` leaves the party.
1206
+ - `Party.messages(partyId, cursor?)` returns
1207
+ `{ messages, cursor }`. Messages are newest first; pass the cursor to load
1208
+ **older** history (the opposite direction from `Hub.chat(after)`). Each message
1209
+ has `{ id, fromUserId, fromUsername, body, text, createdAt }`. `body` is the
1210
+ Castle chat JSON; `text` is non-null only when that body is plain text with no
1211
+ tags, links or embeds.
1212
+ - `Party.say(partyId, text)` sends plain text to the party. The host trims it
1213
+ and caps it at 1,000 characters, then returns the stored `PartyMessage`.
1214
+
1215
+ ```js
1216
+ import { Party } from "castle-web-sdk";
1217
+
1218
+ const party = await Party.current();
1219
+ if (party) {
1220
+ const page = await Party.messages(party.partyId);
1221
+ await Party.say(party.partyId, "Meet by the gate!");
1222
+ }
1223
+ ```
1224
+
1011
1225
  ## Hub
1012
1226
 
1227
+ Hub contract v1.95 adds optional invites. A place declaring `hub.invite: true`
1228
+ can call `hub.invite(playerId)` on its server to offer **Invite a friend**.
1229
+ The hub kit asks `Hub.invite()` for a link; `Hub.invite({ crew: true })` makes
1230
+ a crew link for up to five invitees. Links expire within 24 hours and default
1231
+ to one invitee. Hosts without invite support return `unavailable`.
1232
+
1233
+ The trusted host resolves the link after sign-in, retains its token, and requests
1234
+ the inviter's layer when it has room. An invite can also route through the
1235
+ existing `Hub.inviteArrival()` result `{ status: 'route', doorId }` to the
1236
+ inviter's house or active run behind a curated door that allows late joins.
1237
+ Contract v1.113 active-run routing requires root `hub.bringFriend: true`
1238
+ in addition to `invite: true` on both the town and destination. The destination
1239
+ also declares `hub.bringFriend: true` so its saved bundle and fresh pin retain
1240
+ the invite capability;
1241
+ it admits only to the matching live run while the inviter is present. A full,
1242
+ ended or unavailable run is refused rather than recreated. The kit calls
1243
+ `Hub.inviteArrival()` after joining that destination. Ghost verifies arrival
1244
+ before creating mutual follows among the eligible
1245
+ over-13 members of that link. Under-13 arrivals create no follows. The arrival
1246
+ surface shows an authored now-friends line and `Hub.undoInvite(userId)` removes
1247
+ both follow directions for that pair. No player-entered message travels with a link.
1248
+ These operations retain the existing hub access gates. Link creation and arrival
1249
+ completion wait up to 16 seconds when the server presence roster is still catching
1250
+ up with the game connection.
1251
+
1013
1252
  `Hub` moves a player between hub places: every deck built on the hub-game kit is
1014
1253
  a place, and a door is a row in Castle's curated door table. A deck names only a
1015
1254
  door; the host supplies the current place, the session it joined there and the
1016
1255
  party, and calls Castle with the player's own account. Hosts without hub support
1017
1256
  answer `unavailable`; older hosts reject with `CastleError` code `UNKNOWN_COMMAND`.
1018
1257
 
1258
+ `Hub.signIn({ reason })` asks the host to open its native sign-in flow for an
1259
+ anonymous player. `reason` is the short explanation shown to the player (at most
1260
+ 200 characters and 512 UTF-8 bytes). It resolves to
1261
+ `{ status: 'signedIn', userId? }`, `{ status: 'dismissed' }`, or
1262
+ `{ status: 'unavailable' }`. `userId` is present only when the host can return
1263
+ the signed-in account id. A new host without native sign-in support returns
1264
+ `unavailable`; an older host that does not know this command rejects with
1265
+ `UNKNOWN_COMMAND`.
1266
+
1019
1267
  - `Hub.doors()` reads the current place's doors and who is inside them:
1020
1268
  `{ status: "ok", doors }` or `{ status: "unavailable", doors: null }`. A row has
1021
1269
  `doorId`, `launcherKind` (`door`, `table`, `cabinet`, `gate`), `placementKind`
@@ -1444,6 +1692,13 @@ startGame(saved ?? newGame());
1444
1692
  Lifecycle.ready();
1445
1693
  ```
1446
1694
 
1695
+ ## Display
1696
+
1697
+ `getDisplay()` returns a `DeckDisplay`. Its optional `orientation` field is the
1698
+ deck's preferred display orientation: `landscape`, `portrait`, or `any`.
1699
+ Missing or unrecognized preferences mean `any`; read the value as
1700
+ `display.orientation ?? "any"`.
1701
+
1447
1702
  ## Setup
1448
1703
 
1449
1704
  Startup, editor-mode check, and a file-write call for editor UI.
@@ -1625,3 +1880,22 @@ Common codes:
1625
1880
  that lacks it.
1626
1881
  - `CAULDRON_STORAGE_STALE_GENERATION` — the handle belongs to an older reset
1627
1882
  generation; reopen explicitly rather than replaying work into the new one.
1883
+
1884
+ ### Your games: hub hearts
1885
+
1886
+ `Hub.hearts()` returns `{ status: 'ok', hearts: string[] }` for the signed-in
1887
+ account. `Hub.setHearts(doorIds)` replaces that list with at most eight distinct
1888
+ door ids, oldest first. The host supplies the current deck id; only curated hub
1889
+ places may save hearts. Guests and unavailable hosts return
1890
+ `{ status: 'unavailable', hearts: [] }`; older hosts throw `UNKNOWN_COMMAND`.
1891
+ Invalid lists throw `INVALID_ARGUMENT`, and other host failures use `CastleError`.
1892
+ The dev serve keeps hearts per account in memory until it stops.
1893
+
1894
+ Server hub analytics (hub-game contract v1.93): `session.hub.track(playerId,
1895
+ { kind, subject?, value?, result?, flags? })` sends a numeric event through the
1896
+ server's authenticated platform channel. It returns `Promise<boolean>`: `true`
1897
+ acknowledges queueing, while unsupported hosts and refused delivery return `false`.
1898
+ It never throws. Only opted-in hub bundles are recorded; Ghost stamps the player
1899
+ and town from the session and door pin and excludes the town creator's own plays.
1900
+ The hub-game kit exposes `hub.track(playerId, eventName, properties)` only on
1901
+ places declaring `hub.analytics: true`; see its contract for the numeric vocabulary.
package/dist/avatar.d.ts CHANGED
@@ -1,12 +1,16 @@
1
1
  import type { AvatarResult } from "./commands";
2
2
  export type { AvatarResult } from "./commands";
3
3
  export interface CastleAvatarApi {
4
+ /** Present only in an SDK whose Avatar.set carries { color }; the hub-game kit offers colours only when it is here. */
5
+ readonly colors?: true;
4
6
  /**
5
7
  * The player's avatar: their choice, or the default every account has.
6
8
  * `unavailable` for a signed-out visitor, or on a host without avatars.
7
9
  */
8
10
  get(): Promise<AvatarResult>;
9
- /** Choose the avatar this player wears everywhere from now on. */
10
- set(avatar: number): Promise<AvatarResult>;
11
+ /** Choose the avatar, and optionally its colour, worn everywhere. Requires a host with colours for options.color. */
12
+ set(avatar: number, options?: {
13
+ color?: number;
14
+ }): Promise<AvatarResult>;
11
15
  }
12
16
  export declare const Avatar: CastleAvatarApi;
package/dist/avatar.js CHANGED
@@ -1,17 +1,17 @@
1
1
  // Avatar — the player's platform avatar (cauldron-game-server docs/HUB.md,
2
2
  // decision 4): one per account, worn in every hub place. v1 is an index into the
3
- // hub-game kit's character templates; there is no editor and no cosmetics.
3
+ // hub-game kit's character templates, with optional colour choices.
4
4
  //
5
5
  // The hub-game kit is the one caller: it reads the avatar to dress the player
6
6
  // and offers the choice in its picker. Other players' avatars are not read
7
7
  // here; a place's server learns them from ghost.
8
8
  import { CastleError } from "./errors";
9
9
  import { hostRequest } from "./transport";
10
- export const Avatar = { get, set };
10
+ export const Avatar = { get, set, colors: true };
11
11
  async function get() {
12
12
  return hostRequest("avatar.get", {});
13
13
  }
14
- async function set(avatar) {
14
+ async function set(avatar, options) {
15
15
  if (typeof avatar !== "number" || !Number.isInteger(avatar) || avatar < 0) {
16
16
  throw new CastleError({
17
17
  code: "INVALID_ARGUMENT",
@@ -19,5 +19,13 @@ async function set(avatar) {
19
19
  operation: "Avatar.set",
20
20
  });
21
21
  }
22
- return hostRequest("avatar.set", { avatar });
22
+ const color = options?.color;
23
+ if (color !== undefined && (!Number.isInteger(color) || color < 0)) {
24
+ throw new CastleError({
25
+ code: "INVALID_ARGUMENT",
26
+ message: "Avatar.set requires a non-negative integer colour.",
27
+ operation: "Avatar.set",
28
+ });
29
+ }
30
+ return hostRequest("avatar.set", color === undefined ? { avatar } : { avatar, color });
23
31
  }
package/dist/castle.d.ts CHANGED
@@ -13,7 +13,11 @@ export { deckPrefixOf, IMPORTS_DIR, IMPORTS_PREFIX, isImportedFile, resolveDeckF
13
13
  export { Haptics } from "./haptics";
14
14
  export type { CastleHapticsApi, HapticsResult, HapticsStatus, HapticStyle } from "./haptics";
15
15
  export { Hub } from "./hub";
16
- export type { CastleHubApi, HubBackResult, HubCancelResult, HubCancelStatus, HubChatMessage, HubChatResult, HubDoor, HubDoorsResult, HubFriend, HubFriendsResult, HubGoParams, HubGoResult, HubHandoffResult, HubHandoffStatus, HubJumpResult, HubPlacementKind, HubReportResult, HubSayResult, } from "./hub";
16
+ export type { CastleHubApi, HubBackResult, HubCancelResult, HubCancelStatus, HubChatMessage, HubChatResult, HubDoor, HubDoorsResult, HubFriend, HubFriendsResult, HubInviteResult, HubInviteArrivalResult, HubHeartsResult, HubGoParams, HubSignInParams, HubSignInResult, HubGoResult, HubHandoffResult, HubHandoffStatus, HubJumpResult, HubPlacementKind, HubReportResult, HubSayResult, } from "./hub";
17
+ export { Social } from "./social";
18
+ export type { CastleSocialApi, SocialFriend, SocialPresence } from "./social";
19
+ export { Party } from "./party";
20
+ export type { CastlePartyApi, PartyMember, PartyMessage, PartyMessagesResult, PartyStatus, PartySummary, } from "./party";
17
21
  export { Leaderboard } from "./leaderboard";
18
22
  export type { LeaderboardData, LeaderboardEntry, LeaderboardOptions, LeaderboardScope, LeaderboardShowOptions, LeaderboardShowResult, LeaderboardShowStatus, LeaderboardSort, } from "./leaderboard";
19
23
  export { Lifecycle } from "./lifecycle";
@@ -35,7 +39,7 @@ export type { CastlePortalApi, PortalOpenResult, PortalOpenStatus, PortalPrefetc
35
39
  export { CARD_RATIO, deleteFile, fileUrl, initCard, onBeforeRestart, onFilesChanged, onSaveReloadState, openFile, renameFile, requestReload, setup, takeReloadState, writeFileOnce, } from "./runtime";
36
40
  export type { FileChange, FilesChangedEvent } from "./runtime";
37
41
  export { cardUnit, fitStage, fovForAspect, getDisplay } from "./display";
38
- export type { DeckDisplay, DeckLayout } from "./display";
42
+ export type { DeckDisplay, DeckLayout, DeckOrientation } from "./display";
39
43
  export { flushSaves, hasPendingSave, onSaveState, writeFile, } from "./saveQueue";
40
44
  export type { SaveState } from "./saveQueue";
41
45
  export { SharedStorage, Storage } from "./storage";
@@ -49,4 +53,4 @@ export type { CastleUser, CastleUserApi } from "./user";
49
53
  export { createSaveCoordinator } from "./saveCoordinator";
50
54
  export type { CoordinatedChange, CoordinatedSave, SaveCoordinator, SaveStatus, } from "./saveCoordinator";
51
55
  export { SaveError } from "./save";
52
- export type { SaveApi, SaveBucketApi, SaveBucketRead, SaveChange, SaveCheck, SaveClaimed, SaveClaimInput, SaveErrorEnvelope, SaveInitial, SaveInitializeInput, SaveOpened, SaveOperation, SaveReadTarget, SaveReceipt, SaveRevision, SaveRows, SaveScope, SaveSelection, SaveSnapshot, SaveTarget, SaveTransactionInput, SaveWriter, } from "./save";
56
+ export type { SaveApi, SaveBucketApi, SaveBucketRead, SaveChange, SaveCheck, SaveConditionalCheck, SaveConditionalWriteInput, SaveClaimed, SaveClaimInput, SaveErrorEnvelope, SaveInitial, SaveInitializeInput, SaveOpened, SaveOperation, SaveReadTarget, SaveReceipt, SaveRecentOperationsInput, SaveRecentOperationsPage, SaveRevision, SaveRows, SaveScope, SaveSelection, SaveSnapshot, SaveTarget, SaveTransactionInput, SaveWriter, } from "./save";
package/dist/castle.js CHANGED
@@ -9,6 +9,8 @@ export { experiment, experiments, experimentsReady } from "./experiments";
9
9
  export { deckPrefixOf, IMPORTS_DIR, IMPORTS_PREFIX, isImportedFile, resolveDeckFile, } from "./files";
10
10
  export { Haptics } from "./haptics";
11
11
  export { Hub } from "./hub";
12
+ export { Social } from "./social";
13
+ export { Party } from "./party";
12
14
  export { Leaderboard } from "./leaderboard";
13
15
  export { Lifecycle } from "./lifecycle";
14
16
  export { Multiplayer } from "./multiplayer";