@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.
- package/AGENTS.md +12 -3
- package/CHANGELOG.md +48 -1
- package/README.md +56 -9
- package/dist/{chunk-KDBBR532.js → chunk-PYW5F45R.js} +334 -2
- package/dist/chunk-PYW5F45R.js.map +1 -0
- package/dist/global-sdk.d.ts +47 -0
- package/dist/global.js +343 -10
- package/dist/index.cjs +333 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/multiplayer.cjs +342 -10
- package/dist/multiplayer.cjs.map +1 -1
- package/dist/multiplayer.d.cts +2 -2
- package/dist/multiplayer.d.ts +2 -2
- package/dist/multiplayer.js +1 -1
- package/dist/{types-dtsNXYZW.d.cts → types-B5bqWnHH.d.cts} +46 -1
- package/dist/{types-dtsNXYZW.d.ts → types-B5bqWnHH.d.ts} +46 -1
- package/docs/external-authoritative-servers.md +21 -21
- package/docs/game-design-playbook.md +25 -25
- package/docs/llms.txt +257 -127
- package/docs/relay-rooms.md +22 -22
- package/package.json +2 -2
- package/dist/chunk-KDBBR532.js.map +0 -1
package/dist/multiplayer.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
35
|
+
- the room code format and maximum length your server accepts;
|
|
36
36
|
- whether missing room codes may create rooms;
|
|
37
|
-
- any validated, game
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
214
|
+
client supplied player id or reconnect token as the proof of identity.
|
|
215
215
|
|
|
216
|
-
Send the latest complete viewer
|
|
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
|
|
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
|
|
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
|
|
252
|
-
- Test both guest and logged
|
|
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
|
|
257
|
-
- Rate
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
14
|
-
- **Time
|
|
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
|
|
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
|
|
25
|
-
- **Design the "one point short" feeling.** Near
|
|
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
|
|
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
|
|
29
|
+
retention surface on the platform. Everyone plays the SAME level, so the board is fair chat.
|
|
30
30
|
|
|
31
|
-
## Multiplayer design (for room
|
|
31
|
+
## Multiplayer design (for room based games)
|
|
32
32
|
|
|
33
|
-
- **Latency
|
|
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
|
|
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
|
|
39
|
-
- **2-minute rounds, drop
|
|
40
|
-
forgive mid
|
|
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
|
|
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
|
|
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
|
|
58
|
-
targets, no hover dependence, portrait
|
|
59
|
-
- **Performance budget: 60fps on a mid
|
|
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
|
|
62
|
-
fork builds; feature
|
|
63
|
-
- **Cloud saves make your game feel native.** Load on boot, save on checkpoint/game
|
|
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
|
|
74
|
+
- [ ] Playable one thumb on a phone at 60fps
|
|
75
75
|
- [ ] Boots and plays with the SDK fully offline
|