@diffenderfer-games/hub 2.0.0-next.4 → 2.0.0-next.6
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 +54 -11
- package/README.md +3 -6
- package/dist/auto.d.ts +5 -5
- package/dist/auto.js +2 -2
- package/dist/avatar-pixi.d.ts +147 -0
- package/dist/avatar-pixi.d.ts.map +1 -0
- package/dist/avatar-pixi.js +280 -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 +487 -0
- package/dist/avatar.js.map +1 -0
- package/dist/daily.d.ts +2 -2
- package/dist/doctor/cli.mjs +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +2 -2
- package/dist/input.d.ts +2 -2
- package/dist/shared/animator.d.ts +104 -0
- package/dist/shared/animator.d.ts.map +1 -0
- package/dist/shared/art.js +1804 -0
- package/dist/shared/art.js.map +1 -0
- package/dist/shared/atlas.d.ts +134 -0
- package/dist/shared/atlas.d.ts.map +1 -0
- package/dist/shared/avatar.d.ts +117 -0
- package/dist/shared/avatar.d.ts.map +1 -0
- package/dist/shared/bridge.js +13 -5
- package/dist/shared/bridge.js.map +1 -1
- package/dist/shared/client-version.gen.js +1 -1
- package/dist/shared/daily.d.ts +129 -18
- package/dist/shared/daily.d.ts.map +1 -1
- package/dist/shared/daily.js +120 -26
- 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 +101 -0
- package/dist/shared/describe.js.map +1 -0
- package/dist/shared/globals.js +1 -39
- package/dist/shared/globals.js.map +1 -1
- package/dist/shared/index.d.ts +537 -17
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/input.d.ts +31 -13
- package/dist/shared/input.d.ts.map +1 -1
- package/dist/shared/input.js +40 -2
- 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/live-canvas.js +62 -0
- package/dist/shared/live-canvas.js.map +1 -0
- package/dist/shared/mp.js +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.js +1 -1
- package/dist/shared/rooms.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 +55 -4
- package/dist/shared/social.d.ts.map +1 -1
- package/dist/shared/src.js +2217 -199
- package/dist/shared/src.js.map +1 -1
- package/dist/shared/svg-markup.js +1860 -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 +166 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/social.d.ts +2 -2
- package/dist/social.js +2 -2
- package/dist/turns.d.ts +39 -0
- package/dist/turns.d.ts.map +1 -0
- package/dist/turns.js +117 -0
- package/dist/turns.js.map +1 -0
- package/package.json +33 -3
- package/src/avatar/adapters/pixi-avatar.ts +73 -0
- package/src/avatar/adapters/pixi-crowd.ts +302 -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 +9 -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 +68 -0
- package/src/avatar/art-store.ts +36 -0
- package/src/avatar/atlas.ts +236 -0
- package/src/avatar/catalog-view.ts +14 -0
- package/src/avatar/colour.ts +109 -0
- package/src/avatar/describe.ts +76 -0
- package/src/avatar/face.ts +187 -0
- package/src/avatar/geom.ts +168 -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 +258 -0
- package/src/avatar/render/context.ts +117 -0
- package/src/avatar/render/eyes.ts +78 -0
- package/src/avatar/render/eyewear.ts +56 -0
- package/src/avatar/render/frame.ts +60 -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 +84 -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 +62 -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 +9 -0
- package/src/core/errors.ts +15 -2
- package/src/core/hub-progress.ts +269 -202
- package/src/core/hub.ts +174 -6
- package/src/core/offline/cloud-saves.ts +62 -0
- package/src/core/offline/hub-kinds/avatar-kind.ts +42 -0
- package/src/core/offline/hub-kinds/index.ts +14 -3
- package/src/core/offline/hub-kinds/progress-kinds.ts +21 -4
- package/src/core/offline/hub-kinds/saved-kind.ts +93 -0
- package/src/core/offline/local-store.ts +46 -1
- package/src/core/page-account.ts +121 -0
- package/src/core/rest/routes.gen.ts +31 -1
- package/src/core/resume/hub-resume.ts +39 -5
- package/src/core/resume/resume-types.ts +2 -2
- 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/turns/hub-turns.ts +140 -0
- package/src/core/turns/turn-game.ts +388 -0
- package/src/core/version/client-version.gen.ts +2 -2
- package/src/daily/daily-system.ts +112 -13
- package/src/daily/page-daily.ts +2 -2
- package/src/daily/types.ts +46 -9
- package/src/doctor/hub-version-fetch.ts +43 -43
- package/src/index.ts +11 -1
- package/src/input/input-system.ts +10 -1
- package/src/input/navigator.ts +11 -0
- package/src/input.ts +1 -0
- package/src/overlay/overlay.ts +25 -1
- package/src/realtime/runtime-api.ts +14 -3
- package/src/social.ts +1 -0
- package/src/turns-kit/check-rules.ts +182 -0
- package/src/turns.ts +13 -0
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* registered `play` on. Deep links (`/<slug>/?daily=YYYY-MM-DD`) auto-start.
|
|
10
10
|
*/
|
|
11
11
|
import { readField } from '../realtime/cross-bundle';
|
|
12
|
-
import type {
|
|
12
|
+
import type { SavedEntry } from '../core/saved/saved-types';
|
|
13
|
+
import type { DailyAll, DailyChallenge, DailyCompleteResponse, DailyCompletionMeta, DailyDefinition, DailyHost, DailySaveInput, DailyStartResponse, DailyStatus } from './types';
|
|
13
14
|
|
|
14
15
|
const DAY_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
15
16
|
|
|
@@ -39,20 +40,32 @@ function messageOf(error: unknown): string {
|
|
|
39
40
|
return error instanceof Error ? error.message : String(error);
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
/** The challenge a start or resume answered. */
|
|
44
|
+
function challengeOf(started: DailyStartResponse): DailyChallenge {
|
|
45
|
+
return {
|
|
46
|
+
day: started.day, seed: started.seed, token: started.token, alreadyCompleted: started.alreadyCompleted,
|
|
47
|
+
instanceId: started.instanceId, saved: started.saved, savedVersion: started.savedVersion, resumed: started.resumed,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
42
51
|
/** Daily challenges (`hub.daily`). */
|
|
43
52
|
export class DailySystem {
|
|
44
53
|
/**
|
|
45
54
|
* The challenge in progress: set when a day starts; cleared the moment the
|
|
46
55
|
* game calls `complete` (whatever the hub answers, and before it answers)
|
|
47
|
-
* or `cancel
|
|
56
|
+
* or `cancel` (or `discard`). A game's win handler checks it:
|
|
48
57
|
* `if (hub.daily.active) hub.daily.complete(…)`. While it is set the page
|
|
49
|
-
* is in a daily run, so `hub.resume.save` saves
|
|
50
|
-
* player leaves a daily unfinished must call
|
|
58
|
+
* is in a daily run, so `hub.resume.save` saves to the day instead of the
|
|
59
|
+
* game's run: a game whose player leaves a daily unfinished must call
|
|
60
|
+
* `cancel()`.
|
|
51
61
|
*/
|
|
52
62
|
active: DailyChallenge | null = null;
|
|
53
63
|
private readonly host: DailyHost;
|
|
54
64
|
private play: DailyDefinition['play'] | null = null;
|
|
55
65
|
private pendingDay: string | null = null;
|
|
66
|
+
private pendingInstance: string | null = null;
|
|
67
|
+
/** A start on its way to the hub, so a resume of the same day waits for it instead of starting it twice. */
|
|
68
|
+
private inFlight: Promise<void> | null = null;
|
|
56
69
|
private isRelaying = false;
|
|
57
70
|
|
|
58
71
|
constructor(host: DailyHost) {
|
|
@@ -84,9 +97,13 @@ export class DailySystem {
|
|
|
84
97
|
define(definition: DailyDefinition): void {
|
|
85
98
|
this.play = definition.play;
|
|
86
99
|
const day = readDeepLinkDay() ?? this.pendingDay;
|
|
100
|
+
const instanceId = this.pendingInstance;
|
|
87
101
|
this.pendingDay = null;
|
|
102
|
+
this.pendingInstance = null;
|
|
88
103
|
if (day !== null) {
|
|
89
104
|
void this.startAndPlay(day);
|
|
105
|
+
} else if (instanceId !== null) {
|
|
106
|
+
void this.resume(instanceId);
|
|
90
107
|
}
|
|
91
108
|
}
|
|
92
109
|
|
|
@@ -103,17 +120,91 @@ export class DailySystem {
|
|
|
103
120
|
this.relayStart(day);
|
|
104
121
|
return;
|
|
105
122
|
}
|
|
123
|
+
const starting = (async (): Promise<void> => {
|
|
124
|
+
try {
|
|
125
|
+
this.begin(play, challengeOf(await this.host.start(day)));
|
|
126
|
+
} catch (error) {
|
|
127
|
+
this.host.emit('hub:daily-error', { day, error: messageOf(error) });
|
|
128
|
+
}
|
|
129
|
+
})();
|
|
130
|
+
this.inFlight = starting;
|
|
131
|
+
await starting;
|
|
132
|
+
if (this.inFlight === starting) {
|
|
133
|
+
this.inFlight = null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Pick up a day in progress by its saved game's id (from `hub.saved`):
|
|
139
|
+
* a fresh token, then the game's `play` with `challenge.saved`. Finishing
|
|
140
|
+
* it any time later gives that day's credit. A no-op while that day is
|
|
141
|
+
* already being played.
|
|
142
|
+
*/
|
|
143
|
+
async resume(instanceId: string): Promise<void> {
|
|
144
|
+
// A start may be on its way for the same day (the `?daily=` link of the same open): let it land first.
|
|
145
|
+
await this.inFlight;
|
|
146
|
+
if (this.active?.instanceId === instanceId) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const play = this.play;
|
|
150
|
+
if (play === null) {
|
|
151
|
+
this.pendingInstance = instanceId;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
106
154
|
try {
|
|
107
|
-
|
|
108
|
-
const challenge: DailyChallenge = { day: started.day, seed: started.seed, token: started.token, alreadyCompleted: started.alreadyCompleted };
|
|
109
|
-
this.active = challenge;
|
|
110
|
-
this.host.emit('hub:daily-started', { day: started.day, seed: started.seed, alreadyCompleted: started.alreadyCompleted });
|
|
111
|
-
play({ ...challenge });
|
|
155
|
+
this.begin(play, challengeOf(await this.host.resume(instanceId)));
|
|
112
156
|
} catch (error) {
|
|
113
|
-
this.host.emit('hub:daily-error', { day, error: messageOf(error) });
|
|
157
|
+
this.host.emit('hub:daily-error', { day: '', error: messageOf(error) });
|
|
114
158
|
}
|
|
115
159
|
}
|
|
116
160
|
|
|
161
|
+
/** Make `challenge` the active day and hand it to the game. */
|
|
162
|
+
private begin(play: DailyDefinition['play'], challenge: DailyChallenge): void {
|
|
163
|
+
this.active = challenge;
|
|
164
|
+
this.host.emit('hub:daily-started', { day: challenge.day, seed: challenge.seed, alreadyCompleted: challenge.alreadyCompleted === true, resumed: challenge.resumed === true });
|
|
165
|
+
if (challenge.resumed === true) {
|
|
166
|
+
this.host.emit('hub:daily-resumed', { day: challenge.day, seed: challenge.seed, alreadyCompleted: false, resumed: true });
|
|
167
|
+
}
|
|
168
|
+
play({ ...challenge });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Save the active day's state, so it can be picked up later (a reload,
|
|
173
|
+
* another device, next week) and still give that day's credit. Kept on
|
|
174
|
+
* the device at once and sent after a short quiet spell (works offline).
|
|
175
|
+
* `hub.resume.save` during a daily lands here too. Returns `false` with
|
|
176
|
+
* no daily active, or for a replay of a completed day (nothing to save).
|
|
177
|
+
*/
|
|
178
|
+
save(input: DailySaveInput): boolean {
|
|
179
|
+
const challenge = this.active;
|
|
180
|
+
if (challenge === null || challenge.instanceId === null) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
return this.host.save(challenge, input);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** This game's days in progress (the calendar marks them, the game can list them). */
|
|
187
|
+
inProgress(): Promise<SavedEntry[]> {
|
|
188
|
+
return this.host.inProgress();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Give up the active day: its progress is discarded and nothing is
|
|
193
|
+
* recorded. The day can be started again fresh while it is in the 30-day
|
|
194
|
+
* window. (The hub's lists ask the player first; a game asks for itself.)
|
|
195
|
+
*/
|
|
196
|
+
async discard(): Promise<void> {
|
|
197
|
+
const challenge = this.active;
|
|
198
|
+
if (challenge === null) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
this.active = null;
|
|
202
|
+
if (challenge.instanceId !== null) {
|
|
203
|
+
await this.host.discard(challenge);
|
|
204
|
+
}
|
|
205
|
+
this.host.emit('hub:daily-discarded', { day: challenge.day, discarded: true });
|
|
206
|
+
}
|
|
207
|
+
|
|
117
208
|
/**
|
|
118
209
|
* Record completion of the active challenge with optional metadata.
|
|
119
210
|
* Resolves with the new completion count and rank, or `null` when no
|
|
@@ -157,12 +248,20 @@ export class DailySystem {
|
|
|
157
248
|
}
|
|
158
249
|
|
|
159
250
|
/**
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
251
|
+
* Pause the active day: the player left it unfinished (quit, picked
|
|
252
|
+
* another level, started a normal game). Nothing is recorded, and the day
|
|
253
|
+
* stays in progress in "Your games" and the calendar, to finish later
|
|
254
|
+
* (its last save is sent now). Until then the page counts as in a daily,
|
|
255
|
+
* so `hub.resume.save` saves to the day: call `cancel()` on every path
|
|
256
|
+
* that leaves a daily without `complete`. To give a day up, `discard()`.
|
|
163
257
|
*/
|
|
164
258
|
cancel(): void {
|
|
259
|
+
const challenge = this.active;
|
|
165
260
|
this.active = null;
|
|
261
|
+
if (challenge !== null) {
|
|
262
|
+
void this.host.flush().catch(() => undefined);
|
|
263
|
+
this.host.emit('hub:daily-paused', { day: challenge.day, paused: true });
|
|
264
|
+
}
|
|
166
265
|
}
|
|
167
266
|
|
|
168
267
|
/** This game's window and the player's completions. */
|
package/src/daily/page-daily.ts
CHANGED
|
@@ -8,8 +8,8 @@ import { hasMethods } from '../realtime/cross-bundle';
|
|
|
8
8
|
import { DailySystem } from './daily-system';
|
|
9
9
|
import type { DailyHost } from './types';
|
|
10
10
|
|
|
11
|
-
/** The cross-bundle shape number of the `daily` singleton. Bump only on an incompatible change. */
|
|
12
|
-
export const DAILY_SINGLETON_API =
|
|
11
|
+
/** The cross-bundle shape number of the `daily` singleton (2 added save, resume, discard and inProgress). Bump only on an incompatible change. */
|
|
12
|
+
export const DAILY_SINGLETON_API = 2;
|
|
13
13
|
|
|
14
14
|
/** Whether `candidate` is some bundle's daily system (v1 copies included). */
|
|
15
15
|
// eslint-disable-next-line no-restricted-syntax -- trust boundary: the instance may come from another bundle
|
package/src/daily/types.ts
CHANGED
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
* answers (typed from the contract's daily endpoints).
|
|
4
4
|
*/
|
|
5
5
|
import type { dailyEndpoints, Json } from '@diffenderfer-games/hub-contract';
|
|
6
|
+
import type { GamePayload } from '../core/game-data';
|
|
7
|
+
import type { SavedEntry } from '../core/saved/saved-types';
|
|
6
8
|
import type { SchemaOutput } from '../realtime/wire-types';
|
|
7
9
|
|
|
8
10
|
/** The response type of a daily endpoint. */
|
|
9
11
|
type DailyResponse<Name extends keyof typeof dailyEndpoints> = SchemaOutput<(typeof dailyEndpoints)[Name]['cfg']['response']>;
|
|
10
12
|
|
|
11
|
-
/** This game's daily window
|
|
13
|
+
/** This game's daily window, the player's completions and days in progress (`GET /games/:slug/daily`). */
|
|
12
14
|
export type DailyStatus = DailyResponse<'getGameDailyChallenges'>;
|
|
13
15
|
|
|
14
16
|
/** Every daily game and the global standings (`GET /daily`). */
|
|
@@ -17,7 +19,7 @@ export type DailyAll = DailyResponse<'listDailyChallengesForAllGames'>;
|
|
|
17
19
|
/** One game's row in {@link DailyAll}. */
|
|
18
20
|
export type DailyGameStatus = DailyAll['games'][number];
|
|
19
21
|
|
|
20
|
-
/** The answer to starting a day: its seed
|
|
22
|
+
/** The answer to starting (or resuming) a day: its seed, the signed token to present on completion, and its saved game. */
|
|
21
23
|
export type DailyStartResponse = DailyResponse<'startDailyChallenge'>;
|
|
22
24
|
|
|
23
25
|
/** The answer to completing a day: the player's new completion count and rank. */
|
|
@@ -25,26 +27,49 @@ export type DailyCompleteResponse = DailyResponse<'completeDailyChallenge'>;
|
|
|
25
27
|
|
|
26
28
|
/** The challenge a game's `play` receives: set up and start exactly this day (derive the level from `seed`). */
|
|
27
29
|
export interface DailyChallenge {
|
|
28
|
-
/** The day, `YYYY-MM-DD
|
|
30
|
+
/** The day, `YYYY-MM-DD` (a UTC day key). */
|
|
29
31
|
day: string;
|
|
30
32
|
/** Deterministic per game and day. */
|
|
31
33
|
seed: number;
|
|
32
|
-
/** The signed token `complete` presents. */
|
|
34
|
+
/** The signed token `complete` presents (empty for a day started offline: it is fetched when the hub is back). */
|
|
33
35
|
token: string;
|
|
34
|
-
/** The player already completed this day (a replay). */
|
|
36
|
+
/** The player already completed this day (a replay for fun: nothing is saved or recorded). */
|
|
35
37
|
alreadyCompleted?: boolean;
|
|
38
|
+
/** The day's saved game; `null` for a replay of a completed day. */
|
|
39
|
+
instanceId: string | null;
|
|
40
|
+
/**
|
|
41
|
+
* The state last saved for this day (`hub.daily.save`, or `hub.resume.save`
|
|
42
|
+
* during the daily), or `null` when the day starts fresh. A game that
|
|
43
|
+
* saves restores from it: `if (challenge.saved) startFrom(challenge.saved)`.
|
|
44
|
+
*/
|
|
45
|
+
saved: GamePayload | null;
|
|
46
|
+
/** The game's state version of `saved` (0 when none). */
|
|
47
|
+
savedVersion: number;
|
|
48
|
+
/** The day was already in progress and was picked up again. */
|
|
49
|
+
resumed?: boolean;
|
|
36
50
|
}
|
|
37
51
|
|
|
38
52
|
/** What `hub.daily.define` takes. */
|
|
39
53
|
export interface DailyDefinition {
|
|
40
|
-
/** Start this challenge now. */
|
|
54
|
+
/** Start this challenge now (restore `challenge.saved` when it has one). */
|
|
41
55
|
play: (challenge: DailyChallenge) => void;
|
|
42
56
|
}
|
|
43
57
|
|
|
44
58
|
/** Metadata a game records with a completion (`{ score, difficulty, timeMs, … }`). */
|
|
45
59
|
export type DailyCompletionMeta = Record<string, Json>;
|
|
46
60
|
|
|
47
|
-
/**
|
|
61
|
+
/** A daily state save (the same shape as `hub.resume.save`). */
|
|
62
|
+
export interface DailySaveInput {
|
|
63
|
+
/** Everything needed to rebuild the day's run (up to 512 KB as JSON). */
|
|
64
|
+
readonly state: GamePayload;
|
|
65
|
+
/** A short line ("34 cards left") and an optional 0 to 1 fraction. */
|
|
66
|
+
readonly progress: { readonly label: string; readonly fraction?: number | null };
|
|
67
|
+
readonly title?: string | null;
|
|
68
|
+
/** The game's state version (default 0). */
|
|
69
|
+
readonly version?: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** What the daily system needs from the hub client: the REST calls, the device copy and its window events. */
|
|
48
73
|
export interface DailyHost {
|
|
49
74
|
/** This page's game slug. */
|
|
50
75
|
readonly slug: string | null;
|
|
@@ -52,17 +77,29 @@ export interface DailyHost {
|
|
|
52
77
|
status(): Promise<DailyStatus>;
|
|
53
78
|
/** `GET /daily`. */
|
|
54
79
|
all(): Promise<DailyAll>;
|
|
55
|
-
/** `POST /games/:slug/daily/:day/start
|
|
80
|
+
/** Start (or pick up) a day: `POST /games/:slug/daily/:day/start`, or a device-only start while offline. */
|
|
56
81
|
start(day: string): Promise<DailyStartResponse>;
|
|
82
|
+
/** Pick a day in progress up again by its saved game's id. */
|
|
83
|
+
resume(instanceId: string): Promise<DailyStartResponse>;
|
|
57
84
|
/** `POST /games/:slug/daily/:day/complete`; `null` when it was queued offline, to replay when the hub is back. */
|
|
58
85
|
complete(day: string, token: string, meta: DailyCompletionMeta): Promise<DailyCompleteResponse | null>;
|
|
86
|
+
/** Keep the day's state on the device at once and send it soon (works offline); `false` when it can't be stored. */
|
|
87
|
+
save(challenge: DailyChallenge, input: DailySaveInput): boolean;
|
|
88
|
+
/** Send a waiting save now. */
|
|
89
|
+
flush(): Promise<void>;
|
|
90
|
+
/** Give the day up: discard its saved game (works offline). */
|
|
91
|
+
discard(challenge: DailyChallenge): Promise<void>;
|
|
92
|
+
/** This game's days in progress (the saved games of kind daily). */
|
|
93
|
+
inProgress(): Promise<SavedEntry[]>;
|
|
59
94
|
/** Fire a `hub:*` window event (the menu listens). */
|
|
60
95
|
emit(name: string, detail: DailyEventDetail): void;
|
|
61
96
|
}
|
|
62
97
|
|
|
63
98
|
/** The detail of the `hub:daily-*` window events. */
|
|
64
99
|
export type DailyEventDetail =
|
|
65
|
-
| { day: string; seed: number; alreadyCompleted: boolean }
|
|
100
|
+
| { day: string; seed: number; alreadyCompleted: boolean; resumed?: boolean }
|
|
66
101
|
| { day: string; count: number; rank: number | null; meta: DailyCompletionMeta }
|
|
67
102
|
| { day: string; queued: true; meta: DailyCompletionMeta }
|
|
103
|
+
| { day: string; paused: true }
|
|
104
|
+
| { day: string; discarded: true }
|
|
68
105
|
| { day: string; error: string };
|
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reading the hub's `GET /_api/version` for `hub-doctor`, with a timeout so a
|
|
3
|
-
* hung or unreachable hub fails a game's CI quickly instead of stalling it.
|
|
4
|
-
*/
|
|
5
|
-
import { HubVersion } from '@diffenderfer-games/hub-contract';
|
|
6
|
-
|
|
7
|
-
/** How long the doctor waits for the hub by default, ms. */
|
|
8
|
-
export const DEFAULT_HUB_TIMEOUT_MS = 10_000;
|
|
9
|
-
|
|
10
|
-
/** The hub could not be asked: down, slow, or answering with an error. */
|
|
11
|
-
export interface HubUnreachable {
|
|
12
|
-
/** The URL that was asked. */
|
|
13
|
-
readonly unreachableUrl: string;
|
|
14
|
-
/** Why, for the message (`timed out after 10000 ms`, `HTTP 502`, a network error). */
|
|
15
|
-
readonly reason: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** Whether a version lookup failed. */
|
|
19
|
-
export function isHubUnreachable(lookup: HubVersion | null | HubUnreachable): lookup is HubUnreachable {
|
|
20
|
-
return lookup !== null && 'unreachableUrl' in lookup;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* The hub's version: `null` for a hub from before versioning (404), a
|
|
25
|
-
* {@link HubUnreachable} when it can't be read within `timeoutMs`.
|
|
26
|
-
*/
|
|
27
|
-
export async function fetchHubVersion(hubUrl: string, timeoutMs: number = DEFAULT_HUB_TIMEOUT_MS): Promise<HubVersion | null | HubUnreachable> {
|
|
28
|
-
const url = new URL('/_api/version', hubUrl).toString();
|
|
29
|
-
try {
|
|
30
|
-
const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
31
|
-
if (response.status === 404) {
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
if (!response.ok) {
|
|
35
|
-
return { unreachableUrl: url, reason: `HTTP ${String(response.status)}` };
|
|
36
|
-
}
|
|
37
|
-
return HubVersion.parse(await response.json());
|
|
38
|
-
} catch (failure) {
|
|
39
|
-
const isTimeout = failure instanceof Error && (failure.name === 'TimeoutError' || failure.name === 'AbortError');
|
|
40
|
-
const reason = isTimeout ? `timed out after ${String(timeoutMs)} ms` : failure instanceof Error ? failure.message : String(failure);
|
|
41
|
-
return { unreachableUrl: url, reason };
|
|
42
|
-
}
|
|
43
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Reading the hub's `GET /_api/version` for `hub-doctor`, with a timeout so a
|
|
3
|
+
* hung or unreachable hub fails a game's CI quickly instead of stalling it.
|
|
4
|
+
*/
|
|
5
|
+
import { HubVersion } from '@diffenderfer-games/hub-contract';
|
|
6
|
+
|
|
7
|
+
/** How long the doctor waits for the hub by default, ms. */
|
|
8
|
+
export const DEFAULT_HUB_TIMEOUT_MS = 10_000;
|
|
9
|
+
|
|
10
|
+
/** The hub could not be asked: down, slow, or answering with an error. */
|
|
11
|
+
export interface HubUnreachable {
|
|
12
|
+
/** The URL that was asked. */
|
|
13
|
+
readonly unreachableUrl: string;
|
|
14
|
+
/** Why, for the message (`timed out after 10000 ms`, `HTTP 502`, a network error). */
|
|
15
|
+
readonly reason: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Whether a version lookup failed. */
|
|
19
|
+
export function isHubUnreachable(lookup: HubVersion | null | HubUnreachable): lookup is HubUnreachable {
|
|
20
|
+
return lookup !== null && 'unreachableUrl' in lookup;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The hub's version: `null` for a hub from before versioning (404), a
|
|
25
|
+
* {@link HubUnreachable} when it can't be read within `timeoutMs`.
|
|
26
|
+
*/
|
|
27
|
+
export async function fetchHubVersion(hubUrl: string, timeoutMs: number = DEFAULT_HUB_TIMEOUT_MS): Promise<HubVersion | null | HubUnreachable> {
|
|
28
|
+
const url = new URL('/_api/version', hubUrl).toString();
|
|
29
|
+
try {
|
|
30
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
31
|
+
if (response.status === 404) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
return { unreachableUrl: url, reason: `HTTP ${String(response.status)}` };
|
|
36
|
+
}
|
|
37
|
+
return HubVersion.parse(await response.json());
|
|
38
|
+
} catch (failure) {
|
|
39
|
+
const isTimeout = failure instanceof Error && (failure.name === 'TimeoutError' || failure.name === 'AbortError');
|
|
40
|
+
const reason = isTimeout ? `timed out after ${String(timeoutMs)} ms` : failure instanceof Error ? failure.message : String(failure);
|
|
41
|
+
return { unreachableUrl: url, reason };
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* queue their own operations with `hub.offline` (docs: "Offline and replay").
|
|
18
18
|
*
|
|
19
19
|
* The page-wide namespaces hang off `hub` as lazy getters (`hub.input`,
|
|
20
|
-
* `hub.daily`, `hub.resume`, `hub.social`, `hub.mp`, `hub.rooms`, `hub.race`), and each has
|
|
20
|
+
* `hub.daily`, `hub.resume`, `hub.saved`, `hub.social`, `hub.mp`, `hub.rooms`, `hub.race`), and each has
|
|
21
21
|
* its own entry (`@diffenderfer-games/hub/input`, ...) for games that want
|
|
22
22
|
* only that part.
|
|
23
23
|
*
|
|
@@ -28,6 +28,7 @@ export {
|
|
|
28
28
|
ContentRejectedError,
|
|
29
29
|
HubError,
|
|
30
30
|
OfflineError,
|
|
31
|
+
SAVE_UNKNOWN_CODE,
|
|
31
32
|
UNSUPPORTED_BY_HUB,
|
|
32
33
|
type HubErrorCode,
|
|
33
34
|
type HubErrorDetails,
|
|
@@ -37,7 +38,16 @@ export type { AuthResponse, ProfileUpdate, StatResult } from './core/hub-progres
|
|
|
37
38
|
export type { GrantBundle, TradeBundle, TradeId, TradeListOptions, TradeNoteOptions } from './core/hub-economy';
|
|
38
39
|
export type { HubMenuControl } from './core/menu-control';
|
|
39
40
|
export { HubResume, RESUME_DEBOUNCE_MS, RESUME_MAX_STATE_BYTES } from './core/resume/hub-resume';
|
|
41
|
+
export { HubAvatar, type AvatarHost, type AvatarModule } from './core/avatar/hub-avatar';
|
|
42
|
+
export type { AvatarChange, AvatarRef, DesignerResult, OpenDesignerOptions, RuntimeAvatarApi } from './core/avatar/avatar-types';
|
|
43
|
+
export type { CarriesLook } from './core/avatar/avatar-ref';
|
|
40
44
|
export type { ResumeBlocker, ResumeEntry, ResumeLoadOptions, ResumeProgress, ResumeRun, ResumeSaveInput } from './core/resume/resume-types';
|
|
45
|
+
export { HubSaved, SAVED_CHANGE_QUIET_MS, type SavedChangeListener, type SavedOpenDetail, type SavedOpenHandler } from './core/saved/hub-saved';
|
|
46
|
+
export { SAVED_CHANGE_EVENT, SAVED_KIND_PARAM, SAVED_OPEN_EVENT, SAVED_OPEN_PARAM, savedGameUrl } from './core/saved/saved-open';
|
|
47
|
+
export type * from './core/saved/saved-types';
|
|
48
|
+
export { HubTurns, TURN_LINK_PARAM, type TurnCreateOptions } from './core/turns/hub-turns';
|
|
49
|
+
export { TURN_CHAT_EVENT, TURN_EVENT, TURN_POLL_MS, TURN_RESYNC_EVENT, TurnGame, type TurnGameEvents, type TurnMoveEvent, type TurnOutcome } from './core/turns/turn-game';
|
|
50
|
+
export type { OpenTurnGame, TurnChatLine, TurnDeadline, TurnGameView } from '@diffenderfer-games/hub-contract';
|
|
41
51
|
export type { QueueOp, QueueOpInput, QueuedScoreOptions, ProfilePatch } from './core/offline/queue';
|
|
42
52
|
export { OfflineApi } from './core/offline/offline-api';
|
|
43
53
|
export { OUTBOX_FULL_CODE, type OfflineEventDetails, type OfflineEventName, type OfflineListener, type OfflineOp } from './core/offline/outbox';
|
|
@@ -18,7 +18,7 @@ import type { ModalityTracker } from './modality';
|
|
|
18
18
|
import { parseLegacyControls } from './legacy-controls';
|
|
19
19
|
import type { LegacyControls } from './legacy-controls';
|
|
20
20
|
import { Navigator } from './navigator';
|
|
21
|
-
import type { NavigatorContext } from './navigator';
|
|
21
|
+
import type { AutoNavigateTarget, NavigatorContext } from './navigator';
|
|
22
22
|
import { feedDeviceState, OnScreenControls } from './on-screen-controls';
|
|
23
23
|
import type { PageInputControl } from './page-control';
|
|
24
24
|
import { describeBinding, PlayerRemaps } from './player-remaps';
|
|
@@ -286,6 +286,15 @@ export class InputSystem implements ControlsPanelHost, PageInputControl {
|
|
|
286
286
|
this.navigator.setAutoNavigate(Navigator.queryFor(selector), options?.back ?? null);
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
/**
|
|
290
|
+
* The game's current `autoNavigate` target (its query and back action), or
|
|
291
|
+
* `null`. Hub UI that takes the gamepad for a while (a dialog) reads it,
|
|
292
|
+
* and gives it back with `autoNavigate(target.query, { back })` when done.
|
|
293
|
+
*/
|
|
294
|
+
autoNavigateTarget(): AutoNavigateTarget | null {
|
|
295
|
+
return this.navigator.autoNavigateTarget();
|
|
296
|
+
}
|
|
297
|
+
|
|
289
298
|
/** Stop navigating the game's own menu. */
|
|
290
299
|
clearAutoNavigate(): void {
|
|
291
300
|
this.navigator.clearAutoNavigate(this.controls.existingOverlay);
|
package/src/input/navigator.ts
CHANGED
|
@@ -40,6 +40,12 @@ function visibleMatches(selector: string): HTMLElement[] {
|
|
|
40
40
|
return [...document.querySelectorAll<HTMLElement>(selector)].filter((element) => element.getClientRects().length > 0);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/** What `autoNavigate` was last given: the targets' query and the back action. */
|
|
44
|
+
export interface AutoNavigateTarget {
|
|
45
|
+
readonly query: () => NavTarget[];
|
|
46
|
+
readonly back: (() => void) | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
43
49
|
/** A navigable group with its highlight state. */
|
|
44
50
|
interface GroupNavigation {
|
|
45
51
|
readonly definition: NavigableDef;
|
|
@@ -81,6 +87,11 @@ export class Navigator {
|
|
|
81
87
|
this.autoNavigateState = null;
|
|
82
88
|
}
|
|
83
89
|
|
|
90
|
+
/** The current auto-navigate target and back action, or `null` when none is set. */
|
|
91
|
+
autoNavigateTarget(): AutoNavigateTarget | null {
|
|
92
|
+
return this.autoNavigateQuery === null ? null : { query: this.autoNavigateQuery, back: this.autoNavigateBack };
|
|
93
|
+
}
|
|
94
|
+
|
|
84
95
|
/** Make a CSS selector's visible matches the auto-navigate targets. */
|
|
85
96
|
static queryFor(selector: string | (() => NavTarget[])): () => NavTarget[] {
|
|
86
97
|
return typeof selector === 'string' ? () => visibleMatches(selector) : selector;
|
package/src/input.ts
CHANGED
|
@@ -15,6 +15,7 @@ export type { InputModality } from './input/modality';
|
|
|
15
15
|
export { isPageInputControl } from './input/page-control';
|
|
16
16
|
export type { PageInputControl } from './input/page-control';
|
|
17
17
|
export type * from './input/types';
|
|
18
|
+
export type { AutoNavigateTarget } from './input/navigator';
|
|
18
19
|
export { Overlay } from './overlay/overlay';
|
|
19
20
|
export { layoutControls } from './overlay/layout';
|
|
20
21
|
export type { LayoutOptions } from './overlay/layout';
|
package/src/overlay/overlay.ts
CHANGED
|
@@ -123,9 +123,15 @@ export class Overlay implements NavHighlighter {
|
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
/**
|
|
126
|
+
/**
|
|
127
|
+
* Game insets (px) auto-placed controls keep clear of, e.g. a top HUD bar.
|
|
128
|
+
* Merged with earlier calls. Also published on the page as
|
|
129
|
+
* `--hub-inset-top` (and right, bottom, left), so the hub's toasts sit
|
|
130
|
+
* below a top HUD too.
|
|
131
|
+
*/
|
|
127
132
|
setInsets(insets: Partial<Insets>): void {
|
|
128
133
|
this.gameInsets = { ...this.gameInsets, ...insets };
|
|
134
|
+
publishInsets(this.gameInsets);
|
|
129
135
|
this.layout();
|
|
130
136
|
}
|
|
131
137
|
|
|
@@ -208,3 +214,21 @@ export class Overlay implements NavHighlighter {
|
|
|
208
214
|
return { top: pixels(style.paddingTop), right: pixels(style.paddingRight), bottom: pixels(style.paddingBottom), left: pixels(style.paddingLeft) };
|
|
209
215
|
}
|
|
210
216
|
}
|
|
217
|
+
|
|
218
|
+
/** Every inset side. */
|
|
219
|
+
const INSET_SIDES: readonly (keyof Insets)[] = ['top', 'right', 'bottom', 'left'];
|
|
220
|
+
|
|
221
|
+
/** The page-wide CSS variables other hub UI (the runtime's toasts) keeps clear of. */
|
|
222
|
+
function publishInsets(insets: Partial<Insets>): void {
|
|
223
|
+
if (typeof document === 'undefined') {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
for (const side of INSET_SIDES) {
|
|
227
|
+
const px = insets[side];
|
|
228
|
+
if (px === undefined) {
|
|
229
|
+
document.documentElement.style.removeProperty(`--hub-inset-${side}`);
|
|
230
|
+
} else {
|
|
231
|
+
document.documentElement.style.setProperty(`--hub-inset-${side}`, `${String(Math.max(0, Math.round(px)))}px`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* the page, and that `hub.presence`, `hub.social`, `hub.mp`, `hub.rooms`,
|
|
4
4
|
* `hub.notify` and `hub.race` forward to.
|
|
5
5
|
*/
|
|
6
|
+
import type { RuntimeAvatarApi } from '../core/avatar/avatar-types';
|
|
6
7
|
import type { HubMpApi, InviteLinkMethods } from './multiplayer-types';
|
|
7
8
|
import type { HubRaceApi } from './race-types';
|
|
8
9
|
import type { HubRoomsApi, RoomReclaimMethods } from './room-types';
|
|
@@ -11,10 +12,10 @@ import type { HubNotifyApi, HubPresenceApi, HubSocialApi, PlayOnlineMethods } fr
|
|
|
11
12
|
/**
|
|
12
13
|
* The realtime feature version the runtime reports in {@link RealtimeApi.version}.
|
|
13
14
|
* 2 added races; 3 added invite links and taking back a held seat; 4 added
|
|
14
|
-
* "Play online" (tournaments)
|
|
15
|
-
* it (or by the optional method).
|
|
15
|
+
* "Play online" (tournaments); 5 added avatars (the designer and `ev:avatar`).
|
|
16
|
+
* A client feature-detects newer additions with it (or by the optional method).
|
|
16
17
|
*/
|
|
17
|
-
export const REALTIME_FEATURE_VERSION =
|
|
18
|
+
export const REALTIME_FEATURE_VERSION = 5;
|
|
18
19
|
|
|
19
20
|
/** `hub.social` as a runtime installs it: version 4's `openPlayOnline` is missing on older runtimes. */
|
|
20
21
|
export type RuntimeSocialApi = Omit<HubSocialApi, keyof PlayOnlineMethods> & Partial<PlayOnlineMethods>;
|
|
@@ -35,6 +36,14 @@ export const REALTIME_SINGLETON_API = 1;
|
|
|
35
36
|
/** The window event the runtime fires once it has installed itself. */
|
|
36
37
|
export const REALTIME_READY_EVENT = 'hub:rt-ready';
|
|
37
38
|
|
|
39
|
+
/**
|
|
40
|
+
* The window event the runtime fires each time its socket says hello (it
|
|
41
|
+
* connected, or connected again after a drop), with `{ user: { id, look } }`
|
|
42
|
+
* from the hello snapshot. `hub.avatar` and `hub.userId()` follow it; changes
|
|
43
|
+
* sent while the socket was down are fetched again.
|
|
44
|
+
*/
|
|
45
|
+
export const SESSION_EVENT = 'hub:session';
|
|
46
|
+
|
|
38
47
|
/** The window event the menu fires when the runtime failed to load (its reason is on `window.__HUB_RT_ERROR__`). */
|
|
39
48
|
export const REALTIME_FAILED_EVENT = 'hub:rt-failed';
|
|
40
49
|
|
|
@@ -57,4 +66,6 @@ export interface RealtimeApi {
|
|
|
57
66
|
notify: HubNotifyApi;
|
|
58
67
|
/** Races (feature version 2 and later). */
|
|
59
68
|
race?: HubRaceApi;
|
|
69
|
+
/** Avatars: the designer and live changes (feature version 5 and later). */
|
|
70
|
+
avatar?: RuntimeAvatarApi;
|
|
60
71
|
}
|