@diffenderfer-games/hub 2.0.0-next.0 → 2.0.0-next.10
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/CHANGELOG.md +163 -0
- package/LICENSE +21 -21
- package/README.md +203 -139
- package/dist/auto.d.ts +8 -8
- package/dist/auto.js +3 -3
- package/dist/avatar-pixi.d.ts +165 -0
- package/dist/avatar-pixi.d.ts.map +1 -0
- package/dist/avatar-pixi.js +299 -0
- package/dist/avatar-pixi.js.map +1 -0
- package/dist/avatar-svg.d.ts +4 -0
- package/dist/avatar-svg.js +5 -0
- package/dist/avatar-three.d.ts +69 -0
- package/dist/avatar-three.d.ts.map +1 -0
- package/dist/avatar-three.js +49 -0
- package/dist/avatar-three.js.map +1 -0
- package/dist/avatar.d.ts +7 -0
- package/dist/avatar.js +500 -0
- package/dist/avatar.js.map +1 -0
- package/dist/daily.d.ts +2 -2
- package/dist/doctor/cli.mjs +119 -28
- package/dist/doctor/cli.mjs.map +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/index.js +3 -3
- package/dist/input.d.ts +2 -2
- package/dist/input.js +2 -2
- package/dist/mp.d.ts +3 -3
- package/dist/race.d.ts +1 -1
- package/dist/rooms.d.ts +3 -3
- package/dist/rooms.js +1 -1
- package/dist/shared/animator.d.ts +104 -0
- package/dist/shared/animator.d.ts.map +1 -0
- package/dist/shared/art.js +1946 -0
- package/dist/shared/art.js.map +1 -0
- package/dist/shared/atlas.d.ts +138 -0
- package/dist/shared/atlas.d.ts.map +1 -0
- package/dist/shared/avatar.d.ts +123 -0
- package/dist/shared/avatar.d.ts.map +1 -0
- package/dist/shared/bridge.js +18 -4
- package/dist/shared/bridge.js.map +1 -1
- package/dist/shared/client-version.gen.js +1 -1
- package/dist/shared/daily.d.ts +145 -19
- package/dist/shared/daily.d.ts.map +1 -1
- package/dist/shared/daily.js +138 -25
- package/dist/shared/daily.js.map +1 -1
- package/dist/shared/defineProperty.js +39 -0
- package/dist/shared/describe.d.ts +32 -0
- package/dist/shared/describe.d.ts.map +1 -0
- package/dist/shared/describe.js +109 -0
- package/dist/shared/describe.js.map +1 -0
- package/dist/shared/globals.js +9 -42
- package/dist/shared/globals.js.map +1 -1
- package/dist/shared/index.d.ts +1122 -46
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/input.d.ts +74 -14
- package/dist/shared/input.d.ts.map +1 -1
- package/dist/shared/input.js +179 -25
- package/dist/shared/input.js.map +1 -1
- package/dist/shared/json-guards.js +48 -0
- package/dist/shared/json-guards.js.map +1 -0
- package/dist/shared/latest-state.js +1 -1
- package/dist/shared/live-canvas.js +62 -0
- package/dist/shared/live-canvas.js.map +1 -0
- package/dist/shared/mp.js +10 -2
- package/dist/shared/mp.js.map +1 -1
- package/dist/shared/multiplayer-types.d.ts +15 -2
- package/dist/shared/multiplayer-types.d.ts.map +1 -1
- package/dist/shared/overlays.js +800 -0
- package/dist/shared/overlays.js.map +1 -0
- package/dist/shared/page-stack.js +2 -1
- package/dist/shared/page-stack.js.map +1 -1
- package/dist/shared/pose-key.js +249 -0
- package/dist/shared/pose-key.js.map +1 -0
- package/dist/shared/race-types.d.ts +1 -1
- package/dist/shared/race.js +1 -1
- package/dist/shared/room-types.d.ts +28 -3
- package/dist/shared/room-types.d.ts.map +1 -1
- package/dist/shared/rooms.d.ts.map +1 -1
- package/dist/shared/rooms.js +23 -4
- package/dist/shared/rooms.js.map +1 -1
- package/dist/shared/runtime-opener.js +1 -1
- package/dist/shared/scene.d.ts +90 -0
- package/dist/shared/scene.d.ts.map +1 -0
- package/dist/shared/social.d.ts +90 -9
- package/dist/shared/social.d.ts.map +1 -1
- package/dist/shared/social.js +6 -1
- package/dist/shared/social.js.map +1 -1
- package/dist/shared/src.js +4563 -667
- package/dist/shared/src.js.map +1 -1
- package/dist/shared/svg-markup.js +2354 -0
- package/dist/shared/svg-markup.js.map +1 -0
- package/dist/shared/svg.js +27 -0
- package/dist/shared/svg.js.map +1 -0
- package/dist/shared/types.d.ts +174 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/wire-types.d.ts +6 -2
- package/dist/shared/wire-types.d.ts.map +1 -1
- package/dist/social.d.ts +5 -5
- package/dist/social.js +2 -2
- package/dist/turns-rules.d.ts +2 -0
- package/dist/turns-rules.js +1 -0
- package/dist/turns.d.ts +61 -0
- package/dist/turns.d.ts.map +1 -0
- package/dist/turns.js +183 -0
- package/dist/turns.js.map +1 -0
- package/package.json +125 -89
- package/src/auto.ts +39 -39
- package/src/avatar/adapters/pixi-avatar.ts +73 -0
- package/src/avatar/adapters/pixi-crowd.ts +329 -0
- package/src/avatar/adapters/pixi-types.ts +84 -0
- package/src/avatar/adapters/three.ts +126 -0
- package/src/avatar/animator/animator.ts +285 -0
- package/src/avatar/animator/reactions.ts +44 -0
- package/src/avatar/animator/schedule.ts +73 -0
- package/src/avatar/art/beard.ts +49 -0
- package/src/avatar/art/glasses.ts +67 -0
- package/src/avatar/art/hair.ts +254 -0
- package/src/avatar/art/headwear.ts +75 -0
- package/src/avatar/art/index.ts +21 -0
- package/src/avatar/art/shape.ts +87 -0
- package/src/avatar/art-store.ts +36 -0
- package/src/avatar/atlas.ts +246 -0
- package/src/avatar/catalog-view.ts +14 -0
- package/src/avatar/colour.ts +109 -0
- package/src/avatar/describe.ts +79 -0
- package/src/avatar/face.ts +187 -0
- package/src/avatar/geom.ts +173 -0
- package/src/avatar/layout.ts +47 -0
- package/src/avatar/look-like.ts +52 -0
- package/src/avatar/outputs/bitmap.ts +75 -0
- package/src/avatar/outputs/canvas-paint.ts +177 -0
- package/src/avatar/outputs/canvas.ts +62 -0
- package/src/avatar/outputs/custom-element.ts +140 -0
- package/src/avatar/outputs/element.ts +58 -0
- package/src/avatar/outputs/live-canvas.ts +57 -0
- package/src/avatar/outputs/overlay-svg.ts +22 -0
- package/src/avatar/outputs/pose-key.ts +30 -0
- package/src/avatar/outputs/svg-markup.ts +113 -0
- package/src/avatar/outputs/svg.ts +41 -0
- package/src/avatar/overlays.ts +128 -0
- package/src/avatar/render/body.ts +116 -0
- package/src/avatar/render/colours.ts +81 -0
- package/src/avatar/render/compose.ts +320 -0
- package/src/avatar/render/context.ts +117 -0
- package/src/avatar/render/eyes.ts +78 -0
- package/src/avatar/render/eyewear.ts +118 -0
- package/src/avatar/render/frame.ts +73 -0
- package/src/avatar/render/head.ts +81 -0
- package/src/avatar/render/mouth.ts +86 -0
- package/src/avatar/render/props.ts +91 -0
- package/src/avatar/render/types.ts +93 -0
- package/src/avatar/render/walker-parts.ts +121 -0
- package/src/avatar/render/walker.ts +199 -0
- package/src/avatar/scene.ts +119 -0
- package/src/avatar-pixi.ts +20 -0
- package/src/avatar-svg.ts +21 -0
- package/src/avatar-three.ts +18 -0
- package/src/avatar.ts +63 -0
- package/src/core/avatar/avatar-cache.ts +95 -0
- package/src/core/avatar/avatar-ref.ts +34 -0
- package/src/core/avatar/avatar-types.ts +43 -0
- package/src/core/avatar/hub-avatar.ts +308 -0
- package/src/core/connection.ts +408 -299
- package/src/core/errors.ts +160 -147
- package/src/core/game-data.ts +40 -38
- package/src/core/hub-economy.ts +216 -179
- package/src/core/hub-progress.ts +269 -205
- package/src/core/hub.ts +352 -131
- package/src/core/menu-control.ts +61 -61
- package/src/core/offline/cloud-saves.ts +62 -0
- package/src/core/offline/events.ts +50 -50
- package/src/core/offline/favorites.ts +21 -21
- package/src/core/offline/hub-kinds/avatar-kind.ts +42 -0
- package/src/core/offline/hub-kinds/daily-kind.ts +68 -0
- package/src/core/offline/hub-kinds/economy-kinds.ts +86 -0
- package/src/core/offline/hub-kinds/game-fields.ts +2 -0
- package/src/core/offline/hub-kinds/index.ts +52 -0
- package/src/core/offline/hub-kinds/legacy-ops.ts +37 -0
- package/src/core/offline/hub-kinds/progress-kinds.ts +165 -0
- package/src/core/offline/hub-kinds/resume-kind.ts +108 -0
- package/src/core/offline/hub-kinds/saved-kind.ts +93 -0
- package/src/core/offline/kind-adapters.ts +48 -0
- package/src/core/offline/local-inventory.ts +64 -0
- package/src/core/offline/local-store.ts +304 -261
- package/src/core/offline/offline-api.ts +65 -0
- package/src/core/offline/outbox-entry.ts +59 -0
- package/src/core/offline/outbox-kinds.ts +78 -0
- package/src/core/offline/outbox-merge.ts +39 -0
- package/src/core/offline/outbox-replayer.ts +141 -0
- package/src/core/offline/outbox-store.ts +85 -0
- package/src/core/offline/outbox.ts +232 -0
- package/src/core/offline/queue.ts +126 -171
- package/src/core/offline/replay-lease.ts +87 -0
- package/src/core/offline/replay-policy.ts +52 -0
- package/src/core/page-account.ts +121 -0
- package/src/core/page.ts +87 -74
- package/src/core/rest/endpoint-types.ts +22 -22
- package/src/core/rest/rest-client.ts +163 -152
- package/src/core/rest/routes.gen.ts +144 -88
- package/src/core/resume/hub-resume.ts +286 -0
- package/src/core/resume/local-resume.ts +130 -0
- package/src/core/resume/resume-list.ts +65 -0
- package/src/core/resume/resume-request.ts +52 -0
- package/src/core/resume/resume-types.ts +64 -0
- package/src/core/saved/daily-saves.ts +196 -0
- package/src/core/saved/daily-seed.ts +16 -0
- package/src/core/saved/hub-saved.ts +221 -0
- package/src/core/saved/local-saved.ts +122 -0
- package/src/core/saved/saved-list.ts +129 -0
- package/src/core/saved/saved-open.ts +104 -0
- package/src/core/saved/saved-order.ts +49 -0
- package/src/core/saved/saved-types.ts +24 -0
- package/src/core/storage/json-guards.ts +80 -74
- package/src/core/storage/key-value.ts +66 -66
- package/src/core/ticket.ts +43 -43
- package/src/core/turns/hub-turns.ts +161 -0
- package/src/core/turns/turn-game.ts +510 -0
- package/src/core/turns/your-turn-buzz.ts +69 -0
- package/src/core/types.ts +134 -134
- package/src/core/version/client-version.gen.ts +2 -2
- package/src/core/version/hub-version.ts +62 -62
- package/src/core/version/semver.ts +97 -97
- package/src/daily/daily-system.ts +276 -161
- package/src/daily/page-daily.ts +36 -36
- package/src/daily/types.ts +105 -67
- package/src/daily.ts +10 -10
- package/src/doctor/checks.ts +86 -76
- package/src/doctor/cli.ts +101 -106
- package/src/doctor/hub-version-fetch.ts +43 -0
- package/src/doctor/tsconfig.json +12 -12
- package/src/doctor/world-check.ts +61 -0
- package/src/globals.ts +115 -110
- package/src/index.ts +68 -50
- package/src/input/device-state.ts +146 -146
- package/src/input/edges.ts +55 -55
- package/src/input/focus-ring.ts +13 -0
- package/src/input/gamepad.ts +61 -61
- package/src/input/input-events.ts +32 -32
- package/src/input/input-registry.ts +171 -164
- package/src/input/input-system.ts +406 -393
- package/src/input/legacy-controls.ts +64 -64
- package/src/input/modality.ts +122 -0
- package/src/input/nav-state.ts +200 -191
- package/src/input/navigator.ts +182 -171
- package/src/input/on-screen-controls.ts +101 -101
- package/src/input/page-control.ts +43 -43
- package/src/input/page-input.ts +193 -193
- package/src/input/player-remaps.ts +77 -77
- package/src/input/source-values.ts +116 -116
- package/src/input/spatial.ts +47 -47
- package/src/input/types.ts +331 -331
- package/src/input/zero-config.ts +164 -177
- package/src/input.ts +26 -22
- package/src/mp.ts +10 -10
- package/src/overlay/controls-panel.ts +163 -163
- package/src/overlay/dom.ts +46 -46
- package/src/overlay/layout-clusters.ts +162 -162
- package/src/overlay/layout-geometry.ts +136 -136
- package/src/overlay/layout.ts +203 -203
- package/src/overlay/listener-scope.ts +23 -23
- package/src/overlay/on-screen-keyboard.ts +137 -137
- package/src/overlay/overlay.ts +234 -208
- package/src/overlay/styles.ts +13 -12
- package/src/overlay/virtual-controls.ts +262 -262
- package/src/race.ts +10 -10
- package/src/realtime/bridge.ts +126 -121
- package/src/realtime/cross-bundle.ts +29 -29
- package/src/realtime/fallback-notice.ts +48 -48
- package/src/realtime/hub-link.ts +65 -65
- package/src/realtime/latest-state.ts +33 -33
- package/src/realtime/multiplayer-forwarder.ts +40 -28
- package/src/realtime/multiplayer-types.ts +66 -52
- package/src/realtime/page-runtime.ts +60 -60
- package/src/realtime/race-forwarder.ts +59 -59
- package/src/realtime/race-types.ts +132 -132
- package/src/realtime/room-types.ts +151 -125
- package/src/realtime/rooms-forwarder.ts +104 -79
- package/src/realtime/runtime-api.ts +74 -49
- package/src/realtime/runtime-opener.ts +61 -61
- package/src/realtime/social-forwarder.ts +77 -72
- package/src/realtime/social-types.ts +86 -78
- package/src/realtime/types.ts +54 -53
- package/src/realtime/wire-types.ts +79 -72
- package/src/realtime/world-types.ts +20 -0
- package/src/rooms.ts +10 -10
- package/src/social.ts +19 -18
- package/src/turns-kit/check-rules.ts +281 -0
- package/src/turns-rules.ts +12 -0
- package/src/turns.ts +14 -0
- package/src/types.ts +13 -13
- package/src/uistack/page-stack.ts +45 -45
- package/src/uistack/play-suspension.ts +82 -82
- package/src/uistack/types.ts +76 -76
- package/src/uistack/ui-stack.ts +144 -144
package/src/core/hub-progress.ts
CHANGED
|
@@ -1,205 +1,269 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Accounts and the player's progress in this game: sign-in, the profile,
|
|
3
|
-
* saves, stats and leaderboards. Reads fall back to the offline cache; writes
|
|
4
|
-
* apply to the cache at once and are queued while offline. Method names are
|
|
5
|
-
* the v1 client's, unchanged.
|
|
6
|
-
*/
|
|
7
|
-
import type { Board, Json, Stats } from '@diffenderfer-games/hub-contract';
|
|
8
|
-
import { HubConnection } from './connection';
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*/
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
this.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
|
|
149
|
-
return this.
|
|
150
|
-
this.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
(
|
|
159
|
-
(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Accounts and the player's progress in this game: sign-in, the profile,
|
|
3
|
+
* saves, stats and leaderboards. Reads fall back to the offline cache; writes
|
|
4
|
+
* apply to the cache at once and are queued while offline. Method names are
|
|
5
|
+
* the v1 client's, unchanged.
|
|
6
|
+
*/
|
|
7
|
+
import type { Board, Json, Stats } from '@diffenderfer-games/hub-contract';
|
|
8
|
+
import { HubConnection } from './connection';
|
|
9
|
+
import { HubError, SAVE_UNKNOWN_CODE } from './errors';
|
|
10
|
+
import { toGameData, toStoredJson, type GamePayload } from './game-data';
|
|
11
|
+
import { PROGRESS_KINDS, type ProfilePayload, type ScorePayload } from './offline/hub-kinds';
|
|
12
|
+
import { pageAccount, toPageUser } from './page-account';
|
|
13
|
+
import { isRecord } from './storage/json-guards';
|
|
14
|
+
import type { LeaderboardResult, MeResponse, Profile, Save, SaveMeta, SubmitScoreOptions, SubmitScoreResult, User } from './types';
|
|
15
|
+
|
|
16
|
+
/** A signed-in user, as the auth calls answer. */
|
|
17
|
+
export interface AuthResponse {
|
|
18
|
+
/** The signed-in user. */
|
|
19
|
+
readonly user: User;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A stat's new value, as the stat calls answer. */
|
|
23
|
+
export interface StatResult {
|
|
24
|
+
/** The stat key. */
|
|
25
|
+
readonly key: string;
|
|
26
|
+
/** Its value after the update. */
|
|
27
|
+
readonly value: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A profile change with the game's own data typed as `TData`. */
|
|
31
|
+
export interface ProfileUpdate<TData extends GamePayload = Json> {
|
|
32
|
+
/** Shown to other players; `null` clears it. */
|
|
33
|
+
readonly displayName?: string | null;
|
|
34
|
+
/** The game's own profile data. */
|
|
35
|
+
readonly data?: TData;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A profile with the game's own data typed as `TData`. */
|
|
39
|
+
function typedProfile<TData>(profile: Profile | null): Profile<TData> | null {
|
|
40
|
+
return profile === null ? null : { ...profile, data: toGameData<TData>(profile.data) };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A save with the game's own data typed as `TData`. */
|
|
44
|
+
function typedSave<TData>(save: Save | null): Save<TData> | null {
|
|
45
|
+
return save === null ? null : { ...save, data: toGameData<TData>(save.data) };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Accounts, profile, saves, stats and leaderboards. */
|
|
49
|
+
export class HubProgress extends HubConnection {
|
|
50
|
+
/** Who the page is signed in as, as the hub's events and this client's sign-ins tell it. */
|
|
51
|
+
protected readonly account = pageAccount();
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Your user id, or `null` when nobody is signed in (also before an offline
|
|
55
|
+
* first visit has an account). Synchronous, and it follows the account as
|
|
56
|
+
* the hub changes it (a new guest's account made by the menu, sign-in,
|
|
57
|
+
* sign-out) with no `hub.me()` needed. Compare it with a seat's `userId`
|
|
58
|
+
* to find yours.
|
|
59
|
+
*/
|
|
60
|
+
userId(): number | null {
|
|
61
|
+
const heard = this.account.user;
|
|
62
|
+
const id = heard === undefined ? this.store.getUser()?.id ?? null : heard?.id ?? null;
|
|
63
|
+
// 0 is an offline local guest: no account on the hub yet.
|
|
64
|
+
return id === 0 ? null : id;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The current user (or `null`) and the games they have a profile in. */
|
|
68
|
+
me(): Promise<MeResponse> {
|
|
69
|
+
return this.withFallback(async () => {
|
|
70
|
+
const reply = await this.rest.call('getCurrentUser');
|
|
71
|
+
this.account.note(reply.user === null ? null : toPageUser(reply.user));
|
|
72
|
+
return reply;
|
|
73
|
+
}, () => {
|
|
74
|
+
const profile = this.store.getProfile();
|
|
75
|
+
return { user: this.store.getUser(), profiles: profile === null ? [] : [{ game: profile.game, displayName: profile.displayName }] };
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Make sure a session exists, creating a guest (a random name, no
|
|
81
|
+
* password) when the player isn't signed in, so progress saves without a
|
|
82
|
+
* sign-in. Idempotent. Offline, a local guest is used.
|
|
83
|
+
*/
|
|
84
|
+
ensureGuest(): Promise<AuthResponse> {
|
|
85
|
+
return this.withFallback<AuthResponse>(async () => this.signedIn(await this.rest.call('ensureGuestSession')), () => {
|
|
86
|
+
const known = this.store.getUser();
|
|
87
|
+
if (known !== null) {
|
|
88
|
+
return { user: known };
|
|
89
|
+
}
|
|
90
|
+
return this.signInLocally({ id: 0, username: 'Guest-local', guest: true });
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Turn the current guest into a registered account (the same identity, so
|
|
96
|
+
* all progress carries over). Rejects with 409 when already registered or
|
|
97
|
+
* the name is taken; the player should `login` instead.
|
|
98
|
+
*/
|
|
99
|
+
claim(username: string, password: string): Promise<AuthResponse> {
|
|
100
|
+
return this.withFallback<AuthResponse>(async () => this.signedIn(await this.rest.call('claimGuestAccount', { username, password })), () => this.signInLocally({ id: 0, username, guest: false }));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Create an account and sign in. (Offline: a local user is stored.) */
|
|
104
|
+
signup(username: string, password: string): Promise<AuthResponse> {
|
|
105
|
+
return this.withFallback<AuthResponse>(async () => this.signedIn(await this.rest.call('signUpWithPassword', { username, password })), () => this.signInLocally({ id: 0, username }));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Sign in. (Offline: a local user is stored so signed-in flows work.) */
|
|
109
|
+
login(username: string, password: string): Promise<AuthResponse> {
|
|
110
|
+
return this.withFallback<AuthResponse>(async () => this.signedIn(await this.rest.call('logInWithPassword', { username, password })), () => this.signInLocally({ id: 0, username }));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Sign out (ends the session shared by every game). */
|
|
114
|
+
logout(): Promise<{ ok: true }> {
|
|
115
|
+
return this.withFallback(async () => {
|
|
116
|
+
const reply = await this.rest.call('logOut');
|
|
117
|
+
this.signedOut();
|
|
118
|
+
return reply;
|
|
119
|
+
}, () => {
|
|
120
|
+
this.signedOut();
|
|
121
|
+
return { ok: true } as const;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private signInLocally(user: User): AuthResponse {
|
|
126
|
+
this.remember(user);
|
|
127
|
+
return { user };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Remember who the hub signed in, so the cache's notes about the hub's copies are read for the right player. */
|
|
131
|
+
private signedIn(reply: AuthResponse): AuthResponse {
|
|
132
|
+
this.remember(reply.user);
|
|
133
|
+
return reply;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The cache and the page (`hub.userId()`, `hub.avatar`) learn who is signed in. */
|
|
137
|
+
private remember(user: User): void {
|
|
138
|
+
this.store.setUser(user);
|
|
139
|
+
this.account.note(toPageUser(user));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private signedOut(): void {
|
|
143
|
+
this.store.clearUser();
|
|
144
|
+
this.account.note(null);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** This game's profile for the current user, or `profile: null`. `TData` is the game's own data type. */
|
|
148
|
+
getProfile<TData = Json>(): Promise<{ profile: Profile<TData> | null }> {
|
|
149
|
+
return this.withFallback(
|
|
150
|
+
async () => ({ profile: typedProfile<TData>((await this.rest.call('getGameProfile', this.gameFields())).profile) }),
|
|
151
|
+
() => ({ profile: typedProfile<TData>(this.store.getProfile()) }),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Create or update this game's profile for the current user. */
|
|
156
|
+
setProfile<TData extends GamePayload = Json>(patch: ProfileUpdate<TData>): Promise<{ profile: Profile<TData> }> {
|
|
157
|
+
const queued: ProfilePayload = {
|
|
158
|
+
...(patch.displayName === undefined ? {} : { displayName: patch.displayName }),
|
|
159
|
+
...(patch.data === undefined ? {} : { data: toStoredJson(patch.data) }),
|
|
160
|
+
};
|
|
161
|
+
return this.mutateOffline(PROGRESS_KINDS.profile, queued, () => {
|
|
162
|
+
const stored = this.store.setProfile(this.slug ?? 'app', patch.displayName ?? null, queued.data ?? {});
|
|
163
|
+
return { profile: { ...stored, data: toGameData<TData>(stored.data) } };
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Metadata for every save slot (no payloads), newest first. */
|
|
168
|
+
listSaves(): Promise<{ saves: SaveMeta[] }> {
|
|
169
|
+
return this.withFallback(() => this.rest.call('listSaves', this.gameFields()), () => ({ saves: this.store.listSaves() }));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* One slot's contents, or `save: null` when the slot is empty. `TData` is
|
|
174
|
+
* the game's own save type. Offline it answers from the device's copy, but
|
|
175
|
+
* only when the device knows the hub's copy of the slot; otherwise (a fresh
|
|
176
|
+
* device or cleared browser) it rejects with a {@link HubError} whose
|
|
177
|
+
* `code` is `save_unknown` (`isOffline` is `true`), never a false "empty".
|
|
178
|
+
*/
|
|
179
|
+
getSave<TData = Json>(slot: string): Promise<{ save: Save<TData> | null }> {
|
|
180
|
+
return this.withFallback(
|
|
181
|
+
async () => {
|
|
182
|
+
const { save } = await this.rest.call('getSave', { ...this.gameFields(), slot });
|
|
183
|
+
if (this.outbox.count() === 0) {
|
|
184
|
+
this.store.cacheCloudSave(slot, save);
|
|
185
|
+
}
|
|
186
|
+
return { save: typedSave<TData>(save) };
|
|
187
|
+
},
|
|
188
|
+
() => ({ save: typedSave<TData>(this.offlineSave(slot)) }),
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The device's copy of `slot`, when it can be trusted offline (see {@link getSave}). */
|
|
193
|
+
private offlineSave(slot: string): Save | null {
|
|
194
|
+
// Trusted: a slot whose hub copy the device has seen, a slot it wrote itself and hasn't sent
|
|
195
|
+
// yet, or any slot of a player signed in only on this device (offline, never reached the hub).
|
|
196
|
+
const isLocalOnly = this.store.getUser()?.id === 0;
|
|
197
|
+
const hasQueuedWrite = this.outbox.pending().some((op) => op.kind === PROGRESS_KINDS.save && isRecord(op.payload) && op.payload.slot === slot);
|
|
198
|
+
if (!isLocalOnly && !hasQueuedWrite && this.store.cloudSave(slot) === undefined) {
|
|
199
|
+
throw new HubError(`Hub: offline, and this device hasn't seen save "${slot}" yet.`, undefined, { code: SAVE_UNKNOWN_CODE });
|
|
200
|
+
}
|
|
201
|
+
return this.store.getSave(slot);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Create or overwrite a save slot. `saveData` is any JSON up to 64 KB. */
|
|
205
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- kept so v1 calls such as putSave<MySave>(...) still compile
|
|
206
|
+
putSave<TData extends GamePayload = Json>(slot: string, saveData: TData, label?: string): Promise<{ ok: true; updatedAt: number }> {
|
|
207
|
+
const stored = toStoredJson(saveData);
|
|
208
|
+
return this.mutateOffline(PROGRESS_KINDS.save, { slot, data: stored, label: label ?? null }, () => ({ ok: true as const, updatedAt: this.store.putSave(slot, stored, label ?? null) }));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Delete a save slot. */
|
|
212
|
+
deleteSave(slot: string): Promise<{ ok: true }> {
|
|
213
|
+
return this.mutateOffline(PROGRESS_KINDS.save, { slot, deleted: true }, () => {
|
|
214
|
+
this.store.deleteSave(slot);
|
|
215
|
+
return { ok: true as const };
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Numeric stats for the current user, or another player's via `userId`. */
|
|
220
|
+
getStats(userId?: number): Promise<{ stats: Stats }> {
|
|
221
|
+
return this.withFallback(
|
|
222
|
+
() => this.rest.call('getGameStats', { ...this.gameFields(), ...(userId === undefined ? {} : { user: userId }) }),
|
|
223
|
+
() => ({ stats: this.store.getStats() }),
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Overwrite a stat. */
|
|
228
|
+
setStat(key: string, value: number): Promise<StatResult> {
|
|
229
|
+
return this.mutateOffline(PROGRESS_KINDS.stat, { key, mode: 'set', value }, () => ({ key, value: this.store.updateStat(key, value, 'set') }));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Add to a stat (creating it at `amount` when absent). */
|
|
233
|
+
incrStat(key: string, amount = 1): Promise<StatResult> {
|
|
234
|
+
return this.mutateOffline(PROGRESS_KINDS.stat, { key, mode: 'incr', value: amount }, () => ({ key, value: this.store.updateStat(key, amount, 'incr') }));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Keep the larger of the stored and the given value. */
|
|
238
|
+
maxStat(key: string, value: number): Promise<StatResult> {
|
|
239
|
+
return this.mutateOffline(PROGRESS_KINDS.stat, { key, mode: 'max', value }, () => ({ key, value: this.store.updateStat(key, value, 'max') }));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Submit a score. The board is created on first use; only a player's best score sticks. */
|
|
243
|
+
submitScore<TMeta extends GamePayload = Json>(boardKey: string, score: number, opts: SubmitScoreOptions<TMeta> = {}): Promise<SubmitScoreResult> {
|
|
244
|
+
const queued: ScorePayload['opts'] = {
|
|
245
|
+
...(opts.title === undefined ? {} : { title: opts.title }),
|
|
246
|
+
...(opts.sort === undefined ? {} : { sort: opts.sort }),
|
|
247
|
+
...(opts.meta === undefined ? {} : { meta: toStoredJson(opts.meta) }),
|
|
248
|
+
};
|
|
249
|
+
return this.mutateOffline(PROGRESS_KINDS.score, { key: boardKey, score, opts: queued }, () => ({ ...this.store.submitScore(boardKey, score, queued), rank: 1 }));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** One board's ranked entries (top `limit`, default 50). Offline: only the player's own best. */
|
|
253
|
+
getLeaderboard(boardKey: string, limit = 50): Promise<LeaderboardResult> {
|
|
254
|
+
return this.withFallback(() => this.rest.call('getLeaderboard', { ...this.gameFields(), key: boardKey, limit }), () => {
|
|
255
|
+
const local = this.store.getBoard(boardKey);
|
|
256
|
+
if (local === null) {
|
|
257
|
+
return { board: { key: boardKey, title: boardKey, sortDir: 'desc' }, entries: [] };
|
|
258
|
+
}
|
|
259
|
+
const user = this.store.getUser();
|
|
260
|
+
return { board: local.board, entries: [{ rank: 1, userId: user?.id ?? 0, username: user?.username ?? 'you', score: local.best, meta: null }] };
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** The boards this game owns (keys and titles, no entries). */
|
|
265
|
+
listLeaderboards(): Promise<{ boards: Board[] }> {
|
|
266
|
+
return this.withFallback(() => this.rest.call('listGameLeaderboards', this.gameFields()), () => ({ boards: this.store.listBoards() }));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
}
|