@bountyboard/arcade-sdk 1.2.0 → 1.4.0

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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  BBArcade
3
- } from "./chunk-KDBBR532.js";
3
+ } from "./chunk-PYW5F45R.js";
4
4
 
5
5
  // src/multiplayer.ts
6
6
  var WELCOME_TIMEOUT_MS = 1e4;
@@ -42,6 +42,46 @@ interface BBArcadeRewardedAdsConfig {
42
42
  interface BBArcadeConfig {
43
43
  rewardedAds?: BBArcadeRewardedAdsConfig;
44
44
  }
45
+ /**
46
+ * What is backing `window.localStorage` right now.
47
+ * - `native` — a real Storage works here; the SDK changed nothing.
48
+ * - `cloud` — the shim is installed and syncs to the player's Bounty Board save.
49
+ * - `memory` — the shim is installed but nothing can persist (guest, standalone,
50
+ * off-host); reads and writes work for the session and are then discarded.
51
+ */
52
+ type BBArcadeStorageMode = 'native' | 'cloud' | 'memory';
53
+ /**
54
+ * localStorage compatibility for hosted builds. Bounty-Board-hosted uploads run
55
+ * in an opaque-origin sandbox where `localStorage` THROWS rather than returning
56
+ * empty, which breaks engine exports (GameMaker `ini_*`, Godot, Unity) that
57
+ * assume synchronous storage. The shim replaces it with a Storage-shaped object
58
+ * backed by the SDK's own cloud save, so those builds persist per player and
59
+ * across devices with no engine changes.
60
+ *
61
+ * The script-tag build installs it automatically at load. Module consumers keep
62
+ * a side-effect-free import and call `install()` themselves before boot.
63
+ */
64
+ interface BBArcadeStorage {
65
+ /**
66
+ * Install the shim if this origin has no working localStorage, and return the
67
+ * resulting mode. Idempotent, and a no-op when native storage works. Call it
68
+ * BEFORE the engine/game script runs.
69
+ */
70
+ install(): BBArcadeStorageMode;
71
+ /**
72
+ * Resolves once the cloud read has landed (or settled as unpersistable).
73
+ * `getItem` is synchronous but the cloud read is not, so a game that reads
74
+ * saved progress during boot should await this first.
75
+ */
76
+ ready(): Promise<BBArcadeStorageMode>;
77
+ /**
78
+ * Force pending writes out now instead of waiting for the debounce. Rejects
79
+ * with the same BBArcadeError codes as save().
80
+ */
81
+ flush(): Promise<void>;
82
+ /** The current backing mode. */
83
+ readonly mode: BBArcadeStorageMode;
84
+ }
45
85
  /**
46
86
  * The logged-in player's public display identity, as delivered by the Bounty
47
87
  * Board host. Display name + avatar ONLY — the host never sends ids, emails,
@@ -402,6 +442,11 @@ interface BBArcadeSDK {
402
442
  * 'error' after the same 15s request timeout when no host answers).
403
443
  */
404
444
  load(): Promise<string | null>;
445
+ /**
446
+ * localStorage compatibility for hosted builds — the escape hatch for engine
447
+ * exports that can't be rewired to call save()/load(). See BBArcadeStorage.
448
+ */
449
+ storage: BBArcadeStorage;
405
450
  /**
406
451
  * The logged-in player's display identity for in-game UI. Resolves
407
452
  * { name, avatarUrl } once the host's config handshake completes (or after
@@ -436,4 +481,4 @@ interface BBArcadeSDK {
436
481
  version: number;
437
482
  }
438
483
 
439
- export type { BBArcadeSDK as B, BBArcadeConfig as a, BBArcadeError as b, BBArcadeErrorCode as c, BBArcadeLockToHostOptions as d, BBArcadePlayer as e, BBArcadePrepareRewardedAdOptions as f, BBArcadePreparedRewardedAd as g, BBArcadeRewardedAdOptions as h, BBArcadeRewardedAdPreparation as i, BBArcadeRewardedAdPrepareFailure as j, BBArcadeRewardedAdResult as k, BBArcadeRewardedAdStatus as l, BBArcadeRewardedAdsConfig as m, BBArcadeRewardedBreakOptions as n, BBArcadeMpJoinOptions as o, BBArcadeMpRoom as p, BBArcadeMultiplayer as q, BBArcadeMpPlayer as r, BBArcadeMpResult as s, BBArcadeMpRoomEvents as t };
484
+ export type { BBArcadeSDK as B, BBArcadeConfig as a, BBArcadeError as b, BBArcadeErrorCode as c, BBArcadeLockToHostOptions as d, BBArcadePlayer as e, BBArcadePrepareRewardedAdOptions as f, BBArcadePreparedRewardedAd as g, BBArcadeRewardedAdOptions as h, BBArcadeRewardedAdPreparation as i, BBArcadeRewardedAdPrepareFailure as j, BBArcadeRewardedAdResult as k, BBArcadeRewardedAdStatus as l, BBArcadeRewardedAdsConfig as m, BBArcadeRewardedBreakOptions as n, BBArcadeStorage as o, BBArcadeStorageMode as p, BBArcadeMpJoinOptions as q, BBArcadeMpRoom as r, BBArcadeMultiplayer as s, BBArcadeMpPlayer as t, BBArcadeMpResult as u, BBArcadeMpRoomEvents as v };
@@ -42,6 +42,46 @@ interface BBArcadeRewardedAdsConfig {
42
42
  interface BBArcadeConfig {
43
43
  rewardedAds?: BBArcadeRewardedAdsConfig;
44
44
  }
45
+ /**
46
+ * What is backing `window.localStorage` right now.
47
+ * - `native` — a real Storage works here; the SDK changed nothing.
48
+ * - `cloud` — the shim is installed and syncs to the player's Bounty Board save.
49
+ * - `memory` — the shim is installed but nothing can persist (guest, standalone,
50
+ * off-host); reads and writes work for the session and are then discarded.
51
+ */
52
+ type BBArcadeStorageMode = 'native' | 'cloud' | 'memory';
53
+ /**
54
+ * localStorage compatibility for hosted builds. Bounty-Board-hosted uploads run
55
+ * in an opaque-origin sandbox where `localStorage` THROWS rather than returning
56
+ * empty, which breaks engine exports (GameMaker `ini_*`, Godot, Unity) that
57
+ * assume synchronous storage. The shim replaces it with a Storage-shaped object
58
+ * backed by the SDK's own cloud save, so those builds persist per player and
59
+ * across devices with no engine changes.
60
+ *
61
+ * The script-tag build installs it automatically at load. Module consumers keep
62
+ * a side-effect-free import and call `install()` themselves before boot.
63
+ */
64
+ interface BBArcadeStorage {
65
+ /**
66
+ * Install the shim if this origin has no working localStorage, and return the
67
+ * resulting mode. Idempotent, and a no-op when native storage works. Call it
68
+ * BEFORE the engine/game script runs.
69
+ */
70
+ install(): BBArcadeStorageMode;
71
+ /**
72
+ * Resolves once the cloud read has landed (or settled as unpersistable).
73
+ * `getItem` is synchronous but the cloud read is not, so a game that reads
74
+ * saved progress during boot should await this first.
75
+ */
76
+ ready(): Promise<BBArcadeStorageMode>;
77
+ /**
78
+ * Force pending writes out now instead of waiting for the debounce. Rejects
79
+ * with the same BBArcadeError codes as save().
80
+ */
81
+ flush(): Promise<void>;
82
+ /** The current backing mode. */
83
+ readonly mode: BBArcadeStorageMode;
84
+ }
45
85
  /**
46
86
  * The logged-in player's public display identity, as delivered by the Bounty
47
87
  * Board host. Display name + avatar ONLY — the host never sends ids, emails,
@@ -402,6 +442,11 @@ interface BBArcadeSDK {
402
442
  * 'error' after the same 15s request timeout when no host answers).
403
443
  */
404
444
  load(): Promise<string | null>;
445
+ /**
446
+ * localStorage compatibility for hosted builds — the escape hatch for engine
447
+ * exports that can't be rewired to call save()/load(). See BBArcadeStorage.
448
+ */
449
+ storage: BBArcadeStorage;
405
450
  /**
406
451
  * The logged-in player's display identity for in-game UI. Resolves
407
452
  * { name, avatarUrl } once the host's config handshake completes (or after
@@ -436,4 +481,4 @@ interface BBArcadeSDK {
436
481
  version: number;
437
482
  }
438
483
 
439
- export type { BBArcadeSDK as B, BBArcadeConfig as a, BBArcadeError as b, BBArcadeErrorCode as c, BBArcadeLockToHostOptions as d, BBArcadePlayer as e, BBArcadePrepareRewardedAdOptions as f, BBArcadePreparedRewardedAd as g, BBArcadeRewardedAdOptions as h, BBArcadeRewardedAdPreparation as i, BBArcadeRewardedAdPrepareFailure as j, BBArcadeRewardedAdResult as k, BBArcadeRewardedAdStatus as l, BBArcadeRewardedAdsConfig as m, BBArcadeRewardedBreakOptions as n, BBArcadeMpJoinOptions as o, BBArcadeMpRoom as p, BBArcadeMultiplayer as q, BBArcadeMpPlayer as r, BBArcadeMpResult as s, BBArcadeMpRoomEvents as t };
484
+ export type { BBArcadeSDK as B, BBArcadeConfig as a, BBArcadeError as b, BBArcadeErrorCode as c, BBArcadeLockToHostOptions as d, BBArcadePlayer as e, BBArcadePrepareRewardedAdOptions as f, BBArcadePreparedRewardedAd as g, BBArcadeRewardedAdOptions as h, BBArcadeRewardedAdPreparation as i, BBArcadeRewardedAdPrepareFailure as j, BBArcadeRewardedAdResult as k, BBArcadeRewardedAdStatus as l, BBArcadeRewardedAdsConfig as m, BBArcadeRewardedBreakOptions as n, BBArcadeStorage as o, BBArcadeStorageMode as p, BBArcadeMpJoinOptions as q, BBArcadeMpRoom as r, BBArcadeMultiplayer as s, BBArcadeMpPlayer as t, BBArcadeMpResult as u, BBArcadeMpRoomEvents as v };
@@ -7,7 +7,7 @@ adapter contract; it does not relay traffic or run a second simulation.
7
7
 
8
8
  External authorities are reviewed and enabled per game. Registration is not
9
9
  self-service. Contact Bounty Board before implementing the adapter so the game
10
- slug, endpoints, room-code rules, ticket keys, and result model can be agreed
10
+ slug, endpoints, room code rules, ticket keys, and result model can be agreed
11
11
  for staging and production.
12
12
 
13
13
  ## What stays authoritative
@@ -23,7 +23,7 @@ Your server remains the only source of truth. It must:
23
23
  - keep endless rooms endless instead of inventing a terminal result.
24
24
 
25
25
  The SDK handles ticket acquisition, WebSocket connection, reconnection, and a
26
- small room-message envelope. It never grants a client authority over identity,
26
+ small room message envelope. It never grants a client authority over identity,
27
27
  state, roles, or results.
28
28
 
29
29
  ## Registration information
@@ -32,9 +32,9 @@ Provide Bounty Board with the following for each environment:
32
32
 
33
33
  - the exact Arcade game slug;
34
34
  - one fixed `wss:` endpoint dedicated to authenticated SDK rooms;
35
- - the room-code format and maximum length your server accepts;
35
+ - the room code format and maximum length your server accepts;
36
36
  - whether missing room codes may create rooms;
37
- - any validated, game-defined `joinData` fields;
37
+ - any validated, game defined `joinData` fields;
38
38
  - whether the game has finite matches or endless sessions; and
39
39
  - an operational contact for key rotation or incident response.
40
40
 
@@ -42,9 +42,9 @@ The registered endpoint must not contain userinfo, query parameters, or a
42
42
  fragment. Use `ws:` only for loopback development. Staging and production need
43
43
  different endpoints and different secrets.
44
44
 
45
- Bounty Board provisions a dedicated ticket-verification secret for the game
45
+ Bounty Board provisions a dedicated ticket verification secret for the game
46
46
  and environment. Store it only on servers. Do not put it in the game bundle,
47
- browser storage, logs, analytics, crash reports, or client-visible environment
47
+ browser storage, logs, analytics, crash reports, or client visible environment
48
48
  variables. Do not reuse credentials from another game or room service.
49
49
 
50
50
  ## Connection flow
@@ -100,7 +100,7 @@ Before accepting a connection, verify all of the following:
100
100
  - `sub` is non-empty; and
101
101
  - required claim types and lengths are valid.
102
102
 
103
- Treat `sub` as an opaque game-scoped identity. Never attempt to map it to a
103
+ Treat `sub` as an opaque game scoped identity. Never attempt to map it to a
104
104
  Bounty Board account or correlate it with another game. `name` and `avatarUrl`
105
105
  are display data, not authorization data. `avatarUrl` may be `null` for any
106
106
  player.
@@ -129,12 +129,12 @@ if (!room.trySend({ type: 'move', x: 1, y: 0 })) {
129
129
  }
130
130
  ```
131
131
 
132
- Script-tag builds use `BBArcade.multiplayer.joinRoom(...)`.
132
+ Script tag builds use `BBArcade.multiplayer.joinRoom(...)`.
133
133
 
134
134
  `joinData` is an optional JSON object capped at 1 KiB. Validate every field,
135
135
  reject unknown or oversized values, and never accept credentials or identity
136
136
  claims from it. If your server distinguishes create from join, use a documented
137
- game-defined intent field and rate-limit room creation. That field is still
137
+ game defined intent field and rate limit room creation. That field is still
138
138
  untrusted and grants no role or permission by itself.
139
139
 
140
140
  ## WebSocket URL
@@ -147,7 +147,7 @@ wss://multiplayer.example.com/bountyboard?ticket=<encoded>&join=<encoded-json>
147
147
 
148
148
  The `join` parameter is omitted when no `joinData` was supplied. Keep this path
149
149
  separate from any anonymous or legacy socket endpoint so a guessed room code
150
- cannot cross the authentication boundary. Redact or disable request-URI logging
150
+ cannot cross the authentication boundary. Redact or disable request URI logging
151
151
  on this route because the short-lived ticket is in the query string.
152
152
 
153
153
  ## SDK room envelope
@@ -174,7 +174,7 @@ Send authoritative state snapshots as:
174
174
  }
175
175
  ```
176
176
 
177
- Send game-defined transient events without changing their payload:
177
+ Send game defined transient events without changing their payload:
178
178
 
179
179
  ```json
180
180
  { "t": "event", "data": { "type": "round_started", "round": 2 } }
@@ -204,23 +204,23 @@ Reject an invalid ticket before `welcome` with an error and close the socket:
204
204
  ```
205
205
 
206
206
  Use `rejected` for invalid admission or `joinData`. After admission, error codes
207
- remain game-defined strings and are surfaced through `room.on('error', ...)`.
207
+ remain game defined strings and are surfaced through `room.on('error', ...)`.
208
208
 
209
209
  ## Reconnection
210
210
 
211
211
  The SDK automatically makes a small number of reconnect attempts. It requests
212
212
  a fresh ticket for the same room and reuses the original `joinData`. Rebind the
213
213
  seat using the verified `(ticket.sub, ticket.roomId)` pair. Never trust a
214
- client-supplied player id or reconnect token as the proof of identity.
214
+ client supplied player id or reconnect token as the proof of identity.
215
215
 
216
- Send the latest complete viewer-safe state in the new `welcome` frame. If your
216
+ Send the latest complete viewer safe state in the new `welcome` frame. If your
217
217
  client uses prediction, include the last processed input sequence in that state
218
218
  so it can discard acknowledged inputs. Clear held controls while the socket is
219
219
  absent.
220
220
 
221
221
  ## Finite matches and endless rooms
222
222
 
223
- Only a server-authoritative finite game may emit `match_end`:
223
+ Only a server authoritative finite game may emit `match_end`:
224
224
 
225
225
  ```json
226
226
  {
@@ -239,7 +239,7 @@ Only a server-authoritative finite game may emit `match_end`:
239
239
 
240
240
  The client receives this as `room.on('end', ({ results }) => ...)`. Persistent
241
241
  Bounty Board results, when enabled, use a separately provisioned
242
- server-to-server reporting credential. Never accept a client-originated result
242
+ server to server reporting credential. Never accept a client originated result
243
243
  or reporting credential.
244
244
 
245
245
  Endless rooms must not emit `match_end`. Death, respawn, a round transition, or
@@ -248,18 +248,18 @@ a player leaving is not automatically a terminal match result.
248
248
  ## Production checklist
249
249
 
250
250
  - Use an isolated staging endpoint and secrets before production.
251
- - Confirm tampered, expired, wrong-slug, and wrong-room tickets fail closed.
252
- - Test both guest and logged-in opaque subjects.
251
+ - Confirm tampered, expired, wrong slug, and wrong room tickets fail closed.
252
+ - Test both guest and logged in opaque subjects.
253
253
  - Reject malformed, unknown, or oversized `joinData`.
254
254
  - Send `welcome` only after ticket and admission validation succeeds.
255
255
  - Verify every snapshot is safe for its specific viewer.
256
- - Test disconnect, held-input cleanup, fresh-ticket reconnect, and `leave()`.
257
- - Rate-limit upgrades, room creation, joins, and inputs.
256
+ - Test disconnect, held input cleanup, fresh ticket reconnect, and `leave()`.
257
+ - Rate limit upgrades, room creation, joins, and inputs.
258
258
  - Redact tickets and join payloads from access logs and error telemetry.
259
259
  - Rotate the dedicated ticket secret without reusing another environment's key.
260
260
  - Emit final results only from a real authoritative terminal condition.
261
261
  - Keep the game playable when multiplayer is unsupported or temporarily down.
262
262
 
263
263
  For SDK integration basics, see https://www.bountyboard.gg/arcade/sdk. For the
264
- agent-readable API contract, see
264
+ agent readable API contract, see
265
265
  https://www.bountyboard.gg/arcade/sdk/llms.txt.
@@ -1,17 +1,17 @@
1
1
  # Bounty Board game design playbook (the "design brain")
2
2
 
3
3
  What actually performs on the Bounty Board arcade, distilled from operating it. Read this
4
- BEFORE designing a new game or porting one in — the SDK wiring is the easy part; these choices
4
+ BEFORE designing a new game or porting one in. The SDK wiring is the easy part; these choices
5
5
  decide whether the game earns plays, retention, and revenue.
6
6
 
7
- > Draft co-owned with our partner studios — challenge anything here with data.
7
+ > Draft co-owned with our partner studios. Challenge anything here with data.
8
8
 
9
9
  ## Session shape
10
10
 
11
- - **Target a 60–180 second core loop.** Arcade traffic arrives mid-browse; games that deliver a
11
+ - **Target a 60 to 180 second core loop.** Arcade traffic arrives mid browse; games that deliver a
12
12
  complete emotional arc (start → tension → payoff) in under three minutes get replays, and
13
- replays drive feed ranking. Longer-form games need checkpointed sessions via cloud saves.
14
- - **Time-to-first-input under 5 seconds.** Call `gameLoadingFinished()` honestly; players who
13
+ replays drive feed ranking. Longer form games need checkpointed sessions via cloud saves.
14
+ - **Time to first input under 5 seconds.** Call `gameLoadingFinished()` honestly; players who
15
15
  bounce on a spinner never come back. Defer heavy assets past the first playable moment.
16
16
  - **Instant restart.** Death → new run should be ONE input and under a second. Restart friction
17
17
  is the top killer of "one more run".
@@ -19,31 +19,31 @@ decide whether the game earns plays, retention, and revenue.
19
19
  ## Score design (this is leaderboard design)
20
20
 
21
21
  - **Scores must be integers with a meaningful gradient.** A good score curve separates a casual
22
- run from a great one by 10–100x, not 2x — that's what makes a board worth climbing.
22
+ run from a great one by 10 to 100x, not 2x. That's what makes a board worth climbing.
23
23
  - **Skill ceiling over grind ceiling.** If score scales with time played rather than skill, the
24
- board saturates and goes stale. Cap or decay pure-survival scoring; reward risk.
25
- - **Design the "one point short" feeling.** Near-miss visibility (show the player's best and the
24
+ board saturates and goes stale. Cap or decay pure survival scoring; reward risk.
25
+ - **Design the "one point short" feeling.** Near miss visibility (show the player's best and the
26
26
  next board rank in-game via your own UI) measurably lifts replays.
27
- - **Daily mode**: if your game has procedural content, ship a shared-seed daily run and submit
27
+ - **Daily mode**: if your game has procedural content, ship a shared seed daily run and submit
28
28
  it with `{ mode: 'daily' }`. Daily boards reset at midnight UTC and are the strongest
29
- retention surface on the platform — everyone plays the SAME level, so the board is fair chat.
29
+ retention surface on the platform. Everyone plays the SAME level, so the board is fair chat.
30
30
 
31
- ## Multiplayer design (for room-based games)
31
+ ## Multiplayer design (for room based games)
32
32
 
33
- - **Latency-tolerant mechanics win.** Positional games at 10–20Hz snapshots with client
33
+ - **Latency tolerant mechanics win.** Positional games at 10 to 20Hz snapshots with client
34
34
  interpolation feel great for chase/tag/social deduction; twitch duels don't. Design around
35
- prediction-friendly movement (momentum, grid steps) rather than instant-hit actions.
35
+ prediction friendly movement (momentum, grid steps) rather than instant hit actions.
36
36
  - **Information asymmetry is a server feature.** The room server sends each player only what
37
37
  they may know (hiders invisible to the seeker). Lean into designs where hidden information IS
38
- the game — it's cheat-proof by construction here.
39
- - **2-minute rounds, drop-in lobbies.** Rooms fill from friends sharing codes; short rounds
40
- forgive mid-round joins as spectators and keep groups cycling.
38
+ the game. It's cheat proof by construction here.
39
+ - **2-minute rounds, drop in lobbies.** Rooms fill from friends sharing codes; short rounds
40
+ forgive mid round joins as spectators and keep groups cycling.
41
41
  - **Send inputs, not outcomes.** If your design needs the client to decide who got tagged, the
42
- design is wrong — move the rule server-side.
42
+ design is wrong. Move the rule server-side.
43
43
 
44
44
  ## Monetization etiquette (rewarded ads)
45
45
 
46
- - **Ads are a player's trade, never a toll.** Best-performing placements: revive ("continue this
46
+ - **Ads are a player's trade, never a toll.** Best performing placements: revive ("continue this
47
47
  run?"), doubler ("2x this run's coins"), cosmetic unlock. Never gate core progression.
48
48
  - **One organic placement beats three pushy ones.** Interrupting flow trains players to leave;
49
49
  prepare one rewarded placement at a natural fail state and offer it once.
@@ -54,13 +54,13 @@ decide whether the game earns plays, retention, and revenue.
54
54
 
55
55
  ## Platform fit
56
56
 
57
- - **Mobile-first inputs.** Most arcade sessions are touch. One-thumb controls, generous hit
58
- targets, no hover dependence, portrait-friendly if possible. Keyboard is the enhancement.
59
- - **Performance budget: 60fps on a mid-range phone.** Cap DPR, pool objects, avoid layout
57
+ - **Mobile first inputs.** Most arcade sessions are touch. One thumb controls, generous hit
58
+ targets, no hover dependence, portrait friendly if possible. Keyboard is the enhancement.
59
+ - **Performance budget: 60fps on a mid range phone.** Cap DPR, pool objects, avoid layout
60
60
  thrash. Players don't report jank, they just leave.
61
- - **Own your standalone build.** The same bundle must run off-platform (the SDK no-ops). Don't
62
- fork builds; feature-detect through the SDK's own fallbacks.
63
- - **Cloud saves make your game feel native.** Load on boot, save on checkpoint/game-over, and
61
+ - **Own your standalone build.** The same bundle must run off platform (the SDK no-ops). Don't
62
+ fork builds; feature detect through the SDK's own fallbacks.
63
+ - **Cloud saves make your game feel native.** Load on boot, save on checkpoint/game over, and
64
64
  greet returning players with their progress (pair with `getPlayer()` for the name). Hosted
65
65
  builds have no localStorage, so wire this early, not as a retrofit.
66
66
 
@@ -71,5 +71,5 @@ decide whether the game earns plays, retention, and revenue.
71
71
  - [ ] The score of a great run embarrasses the score of a lucky run
72
72
  - [ ] Daily mode if content is procedural
73
73
  - [ ] Rewarded placement is a trade the player initiates
74
- - [ ] Playable one-thumb on a phone at 60fps
74
+ - [ ] Playable one thumb on a phone at 60fps
75
75
  - [ ] Boots and plays with the SDK fully offline