@diffenderfer-games/hub 2.0.0-next.0 → 2.0.0-next.1
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 +9 -0
- package/LICENSE +21 -21
- package/README.md +191 -139
- package/dist/doctor/cli.mjs +49 -19
- package/dist/doctor/cli.mjs.map +1 -1
- package/dist/shared/client-version.gen.js +1 -1
- package/dist/shared/daily.js +1 -1
- package/dist/shared/globals.js +8 -3
- package/dist/shared/globals.js.map +1 -1
- package/dist/shared/index.d.ts +1 -1
- package/package.json +89 -89
- package/src/auto.ts +39 -39
- package/src/core/connection.ts +299 -299
- package/src/core/errors.ts +147 -147
- package/src/core/game-data.ts +38 -38
- package/src/core/hub-economy.ts +179 -179
- package/src/core/hub-progress.ts +205 -205
- package/src/core/hub.ts +131 -131
- package/src/core/menu-control.ts +61 -61
- package/src/core/offline/events.ts +50 -50
- package/src/core/offline/favorites.ts +21 -21
- package/src/core/offline/local-store.ts +261 -261
- package/src/core/offline/queue.ts +171 -171
- package/src/core/page.ts +74 -74
- package/src/core/rest/endpoint-types.ts +22 -22
- package/src/core/rest/rest-client.ts +152 -152
- package/src/core/storage/json-guards.ts +74 -74
- package/src/core/storage/key-value.ts +66 -66
- package/src/core/ticket.ts +43 -43
- package/src/core/types.ts +134 -134
- package/src/core/version/client-version.gen.ts +1 -1
- package/src/core/version/hub-version.ts +62 -62
- package/src/core/version/semver.ts +97 -97
- package/src/daily/daily-system.ts +161 -161
- package/src/daily/page-daily.ts +36 -36
- package/src/daily/types.ts +67 -67
- package/src/daily.ts +10 -10
- package/src/doctor/checks.ts +78 -76
- package/src/doctor/cli.ts +98 -106
- package/src/doctor/hub-version-fetch.ts +43 -0
- package/src/doctor/tsconfig.json +12 -12
- package/src/globals.ts +115 -110
- package/src/index.ts +50 -50
- package/src/input/device-state.ts +146 -146
- package/src/input/edges.ts +55 -55
- package/src/input/gamepad.ts +61 -61
- package/src/input/input-events.ts +32 -32
- package/src/input/input-registry.ts +164 -164
- package/src/input/input-system.ts +393 -393
- package/src/input/legacy-controls.ts +64 -64
- package/src/input/nav-state.ts +191 -191
- package/src/input/navigator.ts +171 -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 +177 -177
- package/src/input.ts +22 -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 +208 -208
- package/src/overlay/styles.ts +9 -9
- package/src/overlay/virtual-controls.ts +262 -262
- package/src/race.ts +10 -10
- package/src/realtime/bridge.ts +121 -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 +28 -28
- package/src/realtime/multiplayer-types.ts +52 -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 +125 -125
- package/src/realtime/rooms-forwarder.ts +79 -79
- package/src/realtime/runtime-api.ts +49 -49
- package/src/realtime/runtime-opener.ts +61 -61
- package/src/realtime/social-forwarder.ts +72 -72
- package/src/realtime/social-types.ts +78 -78
- package/src/realtime/types.ts +53 -53
- package/src/realtime/wire-types.ts +72 -72
- package/src/rooms.ts +10 -10
- package/src/social.ts +18 -18
- 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
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The multiplayer lifecycle as games see it through `hub.mp`: invites,
|
|
3
|
-
* launches, the leave guard and own-server tickets.
|
|
4
|
-
*/
|
|
5
|
-
import type { Invite, JoinInfo, Launch, Party } from '@diffenderfer-games/hub-contract';
|
|
6
|
-
|
|
7
|
-
/** `hub.mp.onJoinInfo` payload: the party leader published where to connect. */
|
|
8
|
-
export interface JoinInfoEvent {
|
|
9
|
-
/** The party it is for. */
|
|
10
|
-
partyId: string;
|
|
11
|
-
/** Opaque connection details (a room code, a server address…). */
|
|
12
|
-
info: JoinInfo;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** What `hub.mp.invite` resolves with. */
|
|
16
|
-
export interface InviteResult {
|
|
17
|
-
/** The invite as sent. */
|
|
18
|
-
invite: Invite;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** Options for `hub.mp.invite`. */
|
|
22
|
-
export interface InviteOptions {
|
|
23
|
-
/** The game mode to invite to. */
|
|
24
|
-
mode?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Options for `hub.mp.setBusy`. */
|
|
28
|
-
export interface BusyOptions {
|
|
29
|
-
/** What the leave-confirmation calls the match ("a duel"). */
|
|
30
|
-
label?: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** `hub.mp`: the multiplayer lifecycle (docs/multiplayer.md §7). */
|
|
34
|
-
export interface HubMpApi {
|
|
35
|
-
/** Invite, join and watch arrivals. Call at boot; launches that arrived earlier are buffered. */
|
|
36
|
-
onLaunch(listener: (launch: Launch) => void): () => void;
|
|
37
|
-
/** The party leader published join info (e.g. a room code). */
|
|
38
|
-
onJoinInfo(listener: (event: JoinInfoEvent) => void): () => void;
|
|
39
|
-
/** Invite a friend to this game. Rejects with `HubError` or `OfflineError`. */
|
|
40
|
-
invite(userId: number, options?: InviteOptions): Promise<InviteResult>;
|
|
41
|
-
/**
|
|
42
|
-
* A live match is in progress: the hub confirms before navigating away and
|
|
43
|
-
* overlay events carry `canPause: false`.
|
|
44
|
-
*/
|
|
45
|
-
setBusy(isBusy: boolean, options?: BusyOptions): void;
|
|
46
|
-
/** A 60-second identity ticket for your own game server. */
|
|
47
|
-
ticket(): Promise<string>;
|
|
48
|
-
/** Host side, own-server games: tell the party where to connect. */
|
|
49
|
-
setJoinInfo(partyId: string, joinInfo: JoinInfo): Promise<void>;
|
|
50
|
-
/** My current party, or `null` (also `null` until the runtime is up). */
|
|
51
|
-
party(): Party | null;
|
|
52
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* The multiplayer lifecycle as games see it through `hub.mp`: invites,
|
|
3
|
+
* launches, the leave guard and own-server tickets.
|
|
4
|
+
*/
|
|
5
|
+
import type { Invite, JoinInfo, Launch, Party } from '@diffenderfer-games/hub-contract';
|
|
6
|
+
|
|
7
|
+
/** `hub.mp.onJoinInfo` payload: the party leader published where to connect. */
|
|
8
|
+
export interface JoinInfoEvent {
|
|
9
|
+
/** The party it is for. */
|
|
10
|
+
partyId: string;
|
|
11
|
+
/** Opaque connection details (a room code, a server address…). */
|
|
12
|
+
info: JoinInfo;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** What `hub.mp.invite` resolves with. */
|
|
16
|
+
export interface InviteResult {
|
|
17
|
+
/** The invite as sent. */
|
|
18
|
+
invite: Invite;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Options for `hub.mp.invite`. */
|
|
22
|
+
export interface InviteOptions {
|
|
23
|
+
/** The game mode to invite to. */
|
|
24
|
+
mode?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Options for `hub.mp.setBusy`. */
|
|
28
|
+
export interface BusyOptions {
|
|
29
|
+
/** What the leave-confirmation calls the match ("a duel"). */
|
|
30
|
+
label?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** `hub.mp`: the multiplayer lifecycle (docs/multiplayer.md §7). */
|
|
34
|
+
export interface HubMpApi {
|
|
35
|
+
/** Invite, join and watch arrivals. Call at boot; launches that arrived earlier are buffered. */
|
|
36
|
+
onLaunch(listener: (launch: Launch) => void): () => void;
|
|
37
|
+
/** The party leader published join info (e.g. a room code). */
|
|
38
|
+
onJoinInfo(listener: (event: JoinInfoEvent) => void): () => void;
|
|
39
|
+
/** Invite a friend to this game. Rejects with `HubError` or `OfflineError`. */
|
|
40
|
+
invite(userId: number, options?: InviteOptions): Promise<InviteResult>;
|
|
41
|
+
/**
|
|
42
|
+
* A live match is in progress: the hub confirms before navigating away and
|
|
43
|
+
* overlay events carry `canPause: false`.
|
|
44
|
+
*/
|
|
45
|
+
setBusy(isBusy: boolean, options?: BusyOptions): void;
|
|
46
|
+
/** A 60-second identity ticket for your own game server. */
|
|
47
|
+
ticket(): Promise<string>;
|
|
48
|
+
/** Host side, own-server games: tell the party where to connect. */
|
|
49
|
+
setJoinInfo(partyId: string, joinInfo: JoinInfo): Promise<void>;
|
|
50
|
+
/** My current party, or `null` (also `null` until the runtime is up). */
|
|
51
|
+
party(): Party | null;
|
|
52
|
+
}
|
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Finding the social runtime's realtime API on the page. The runtime owns the
|
|
3
|
-
* `rt` singleton (plan §4.4): it registers on `window.__DG__.rt`, and older
|
|
4
|
-
* runtimes only set the v1 name `window.__HUB_RT__`.
|
|
5
|
-
*/
|
|
6
|
-
import { LEGACY_GLOBAL_NAMES } from '../globals';
|
|
7
|
-
import { hasMethods, isObject, readField } from './cross-bundle';
|
|
8
|
-
import type { RealtimeApi } from './runtime-api';
|
|
9
|
-
import { REALTIME_SINGLETON_API } from './runtime-api';
|
|
10
|
-
|
|
11
|
-
/** Whether `candidate` looks like a {@link RealtimeApi} (any bundle's). */
|
|
12
|
-
// eslint-disable-next-line no-restricted-syntax -- trust boundary: the runtime is another bundle
|
|
13
|
-
export function isRealtimeApi(candidate: unknown): candidate is RealtimeApi {
|
|
14
|
-
return typeof readField(candidate, 'version') === 'number'
|
|
15
|
-
&& hasMethods(readField(candidate, 'presence'), ['set', 'clear'])
|
|
16
|
-
&& hasMethods(readField(candidate, 'social'), ['openPlayer', 'friends', 'counts', 'chatMode', 'on'])
|
|
17
|
-
&& hasMethods(readField(candidate, 'mp'), ['onLaunch', 'onJoinInfo', 'invite', 'setBusy', 'party'])
|
|
18
|
-
&& hasMethods(readField(candidate, 'rooms'), ['create', 'join', 'list'])
|
|
19
|
-
&& hasMethods(readField(candidate, 'notify'), ['setQuiet']);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
let hasWarnedAboutApi = false;
|
|
23
|
-
|
|
24
|
-
/** Warn once when the runtime registered a different shape number than this client knows. */
|
|
25
|
-
function warnOnApiMismatch(registeredApi: number): void {
|
|
26
|
-
if (registeredApi === REALTIME_SINGLETON_API || hasWarnedAboutApi) {
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
hasWarnedAboutApi = true;
|
|
30
|
-
// eslint-disable-next-line no-console -- the plan's one-time warning to the game developer, with the fix
|
|
31
|
-
console.warn(`[hub] this page's social runtime speaks realtime api ${String(registeredApi)}, this client api ${String(REALTIME_SINGLETON_API)}. Fix: npm update @diffenderfer-games/hub`);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** The page's realtime API if the runtime has loaded, else `null`. */
|
|
35
|
-
export function findRealtimeApi(): RealtimeApi | null {
|
|
36
|
-
if (typeof window === 'undefined') {
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
const registered = window.__DG__?.rt;
|
|
40
|
-
if (registered !== undefined && isRealtimeApi(registered.instance)) {
|
|
41
|
-
warnOnApiMismatch(registered.api);
|
|
42
|
-
return registered.instance;
|
|
43
|
-
}
|
|
44
|
-
const legacy = window[LEGACY_GLOBAL_NAMES.rt];
|
|
45
|
-
return isRealtimeApi(legacy) ? legacy : null;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Whether the hub injected this page (`window.__HUB__`); without it the runtime can never arrive. */
|
|
49
|
-
export function isHubInjectedPage(): boolean {
|
|
50
|
-
return typeof window !== 'undefined' && isObject(readField(window, '__HUB__'));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Why the social runtime failed to load on this page (set by the menu), or `''`. */
|
|
54
|
-
export function readRuntimeFailure(): string {
|
|
55
|
-
if (typeof window === 'undefined') {
|
|
56
|
-
return '';
|
|
57
|
-
}
|
|
58
|
-
const failure = readField(window, '__HUB_RT_ERROR__');
|
|
59
|
-
return typeof failure === 'string' ? failure.slice(0, 120) : '';
|
|
60
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Finding the social runtime's realtime API on the page. The runtime owns the
|
|
3
|
+
* `rt` singleton (plan §4.4): it registers on `window.__DG__.rt`, and older
|
|
4
|
+
* runtimes only set the v1 name `window.__HUB_RT__`.
|
|
5
|
+
*/
|
|
6
|
+
import { LEGACY_GLOBAL_NAMES } from '../globals';
|
|
7
|
+
import { hasMethods, isObject, readField } from './cross-bundle';
|
|
8
|
+
import type { RealtimeApi } from './runtime-api';
|
|
9
|
+
import { REALTIME_SINGLETON_API } from './runtime-api';
|
|
10
|
+
|
|
11
|
+
/** Whether `candidate` looks like a {@link RealtimeApi} (any bundle's). */
|
|
12
|
+
// eslint-disable-next-line no-restricted-syntax -- trust boundary: the runtime is another bundle
|
|
13
|
+
export function isRealtimeApi(candidate: unknown): candidate is RealtimeApi {
|
|
14
|
+
return typeof readField(candidate, 'version') === 'number'
|
|
15
|
+
&& hasMethods(readField(candidate, 'presence'), ['set', 'clear'])
|
|
16
|
+
&& hasMethods(readField(candidate, 'social'), ['openPlayer', 'friends', 'counts', 'chatMode', 'on'])
|
|
17
|
+
&& hasMethods(readField(candidate, 'mp'), ['onLaunch', 'onJoinInfo', 'invite', 'setBusy', 'party'])
|
|
18
|
+
&& hasMethods(readField(candidate, 'rooms'), ['create', 'join', 'list'])
|
|
19
|
+
&& hasMethods(readField(candidate, 'notify'), ['setQuiet']);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let hasWarnedAboutApi = false;
|
|
23
|
+
|
|
24
|
+
/** Warn once when the runtime registered a different shape number than this client knows. */
|
|
25
|
+
function warnOnApiMismatch(registeredApi: number): void {
|
|
26
|
+
if (registeredApi === REALTIME_SINGLETON_API || hasWarnedAboutApi) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
hasWarnedAboutApi = true;
|
|
30
|
+
// eslint-disable-next-line no-console -- the plan's one-time warning to the game developer, with the fix
|
|
31
|
+
console.warn(`[hub] this page's social runtime speaks realtime api ${String(registeredApi)}, this client api ${String(REALTIME_SINGLETON_API)}. Fix: npm update @diffenderfer-games/hub`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The page's realtime API if the runtime has loaded, else `null`. */
|
|
35
|
+
export function findRealtimeApi(): RealtimeApi | null {
|
|
36
|
+
if (typeof window === 'undefined') {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const registered = window.__DG__?.rt;
|
|
40
|
+
if (registered !== undefined && isRealtimeApi(registered.instance)) {
|
|
41
|
+
warnOnApiMismatch(registered.api);
|
|
42
|
+
return registered.instance;
|
|
43
|
+
}
|
|
44
|
+
const legacy = window[LEGACY_GLOBAL_NAMES.rt];
|
|
45
|
+
return isRealtimeApi(legacy) ? legacy : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Whether the hub injected this page (`window.__HUB__`); without it the runtime can never arrive. */
|
|
49
|
+
export function isHubInjectedPage(): boolean {
|
|
50
|
+
return typeof window !== 'undefined' && isObject(readField(window, '__HUB__'));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Why the social runtime failed to load on this page (set by the menu), or `''`. */
|
|
54
|
+
export function readRuntimeFailure(): string {
|
|
55
|
+
if (typeof window === 'undefined') {
|
|
56
|
+
return '';
|
|
57
|
+
}
|
|
58
|
+
const failure = readField(window, '__HUB_RT_ERROR__');
|
|
59
|
+
return typeof failure === 'string' ? failure.slice(0, 120) : '';
|
|
60
|
+
}
|
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `hub.race`: races, forwarded to the social runtime. An old cached runtime
|
|
3
|
-
* (feature version 1) has no races, which reads as "not available" rather
|
|
4
|
-
* than a crash.
|
|
5
|
-
*/
|
|
6
|
-
import { pageBridge } from './bridge';
|
|
7
|
-
import { hubLink } from './hub-link';
|
|
8
|
-
import type { HubRaceApi, RaceDefinition } from './race-types';
|
|
9
|
-
import type { RealtimeApi } from './runtime-api';
|
|
10
|
-
import { createRuntimeOpener } from './runtime-opener';
|
|
11
|
-
|
|
12
|
-
const openWithRuntime = createRuntimeOpener(pageBridge);
|
|
13
|
-
|
|
14
|
-
let raceDefinition: RaceDefinition | null = null;
|
|
15
|
-
pageBridge.onReady((realtime) => {
|
|
16
|
-
if (raceDefinition !== null) {
|
|
17
|
-
realtime.race?.define(raceDefinition);
|
|
18
|
-
}
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
/** The runtime's race section, or an `OfflineError` for a runtime too old to race. */
|
|
22
|
-
function requireRaces(realtime: RealtimeApi): HubRaceApi {
|
|
23
|
-
if (realtime.race === undefined) {
|
|
24
|
-
throw hubLink().createOfflineError('Races need the latest hub — reload the page.');
|
|
25
|
-
}
|
|
26
|
-
return realtime.race;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** `hub.race`: races. */
|
|
30
|
-
export const raceForwarder: HubRaceApi = {
|
|
31
|
-
define(definition) {
|
|
32
|
-
raceDefinition = definition;
|
|
33
|
-
pageBridge.current()?.race?.define(definition);
|
|
34
|
-
},
|
|
35
|
-
create: (options) => pageBridge.call((realtime) => requireRaces(realtime).create(options)),
|
|
36
|
-
join: (code, options) => pageBridge.call((realtime) => requireRaces(realtime).join(code, options)),
|
|
37
|
-
list: () => pageBridge.call((realtime) => requireRaces(realtime).list()),
|
|
38
|
-
browse: () => {
|
|
39
|
-
openWithRuntime((realtime) => {
|
|
40
|
-
realtime.race?.browse();
|
|
41
|
-
});
|
|
42
|
-
},
|
|
43
|
-
openHistory: (options) => {
|
|
44
|
-
openWithRuntime((realtime) => {
|
|
45
|
-
realtime.race?.openHistory(options);
|
|
46
|
-
});
|
|
47
|
-
},
|
|
48
|
-
history: (query) => pageBridge.call((realtime) => requireRaces(realtime).history(query)),
|
|
49
|
-
status: (stats, progress) => {
|
|
50
|
-
pageBridge.current()?.race?.status(stats, progress);
|
|
51
|
-
},
|
|
52
|
-
finish: (stats) => pageBridge.call((realtime) => requireRaces(realtime).finish(stats)),
|
|
53
|
-
lose: (stats) => pageBridge.call((realtime) => requireRaces(realtime).lose(stats)),
|
|
54
|
-
forfeit: () => pageBridge.call((realtime) => requireRaces(realtime).forfeit()),
|
|
55
|
-
leave: () => pageBridge.call((realtime) => requireRaces(realtime).leave()),
|
|
56
|
-
rematch: (params) => pageBridge.call((realtime) => requireRaces(realtime).rematch(params)),
|
|
57
|
-
current: () => pageBridge.current()?.race?.current() ?? null,
|
|
58
|
-
on: (event, listener) => pageBridge.subscribe((realtime) => realtime.race?.on(event, listener) ?? (() => undefined)),
|
|
59
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* `hub.race`: races, forwarded to the social runtime. An old cached runtime
|
|
3
|
+
* (feature version 1) has no races, which reads as "not available" rather
|
|
4
|
+
* than a crash.
|
|
5
|
+
*/
|
|
6
|
+
import { pageBridge } from './bridge';
|
|
7
|
+
import { hubLink } from './hub-link';
|
|
8
|
+
import type { HubRaceApi, RaceDefinition } from './race-types';
|
|
9
|
+
import type { RealtimeApi } from './runtime-api';
|
|
10
|
+
import { createRuntimeOpener } from './runtime-opener';
|
|
11
|
+
|
|
12
|
+
const openWithRuntime = createRuntimeOpener(pageBridge);
|
|
13
|
+
|
|
14
|
+
let raceDefinition: RaceDefinition | null = null;
|
|
15
|
+
pageBridge.onReady((realtime) => {
|
|
16
|
+
if (raceDefinition !== null) {
|
|
17
|
+
realtime.race?.define(raceDefinition);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
/** The runtime's race section, or an `OfflineError` for a runtime too old to race. */
|
|
22
|
+
function requireRaces(realtime: RealtimeApi): HubRaceApi {
|
|
23
|
+
if (realtime.race === undefined) {
|
|
24
|
+
throw hubLink().createOfflineError('Races need the latest hub — reload the page.');
|
|
25
|
+
}
|
|
26
|
+
return realtime.race;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** `hub.race`: races. */
|
|
30
|
+
export const raceForwarder: HubRaceApi = {
|
|
31
|
+
define(definition) {
|
|
32
|
+
raceDefinition = definition;
|
|
33
|
+
pageBridge.current()?.race?.define(definition);
|
|
34
|
+
},
|
|
35
|
+
create: (options) => pageBridge.call((realtime) => requireRaces(realtime).create(options)),
|
|
36
|
+
join: (code, options) => pageBridge.call((realtime) => requireRaces(realtime).join(code, options)),
|
|
37
|
+
list: () => pageBridge.call((realtime) => requireRaces(realtime).list()),
|
|
38
|
+
browse: () => {
|
|
39
|
+
openWithRuntime((realtime) => {
|
|
40
|
+
realtime.race?.browse();
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
openHistory: (options) => {
|
|
44
|
+
openWithRuntime((realtime) => {
|
|
45
|
+
realtime.race?.openHistory(options);
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
history: (query) => pageBridge.call((realtime) => requireRaces(realtime).history(query)),
|
|
49
|
+
status: (stats, progress) => {
|
|
50
|
+
pageBridge.current()?.race?.status(stats, progress);
|
|
51
|
+
},
|
|
52
|
+
finish: (stats) => pageBridge.call((realtime) => requireRaces(realtime).finish(stats)),
|
|
53
|
+
lose: (stats) => pageBridge.call((realtime) => requireRaces(realtime).lose(stats)),
|
|
54
|
+
forfeit: () => pageBridge.call((realtime) => requireRaces(realtime).forfeit()),
|
|
55
|
+
leave: () => pageBridge.call((realtime) => requireRaces(realtime).leave()),
|
|
56
|
+
rematch: (params) => pageBridge.call((realtime) => requireRaces(realtime).rematch(params)),
|
|
57
|
+
current: () => pageBridge.current()?.race?.current() ?? null,
|
|
58
|
+
on: (event, listener) => pageBridge.subscribe((realtime) => realtime.race?.on(event, listener) ?? (() => undefined)),
|
|
59
|
+
};
|
|
@@ -1,132 +1,132 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Races as games see them through `hub.race` (docs/plans/race.md): a hub
|
|
3
|
-
* room with a referee. Everyone gets the same puzzle (shared seed and
|
|
4
|
-
* params), live status, and the first valid finish wins.
|
|
5
|
-
*/
|
|
6
|
-
import type { RaceParams, RaceResult, RaceStats, RoomInfo } from '@diffenderfer-games/hub-contract';
|
|
7
|
-
import type { RaceHistory, RaceStatusEvent } from './wire-types';
|
|
8
|
-
import type { JoinOptions } from './room-types';
|
|
9
|
-
|
|
10
|
-
/** What a race game's `start` receives: build the puzzle from `seed` and `params`. */
|
|
11
|
-
export interface RaceStart {
|
|
12
|
-
/** The race room. */
|
|
13
|
-
roomId: string;
|
|
14
|
-
/** 1 for the first race in the room, +1 per rematch. */
|
|
15
|
-
round: number;
|
|
16
|
-
/** The shared seed, revealed at the countdown. */
|
|
17
|
-
seed: number;
|
|
18
|
-
/** The creator's choices. */
|
|
19
|
-
params: RaceParams;
|
|
20
|
-
/** The go, on THIS page's clock (`Date.now()` ms). The hub draws the 3-2-1. */
|
|
21
|
-
startAt: number;
|
|
22
|
-
/** Racer user ids. */
|
|
23
|
-
players: number[];
|
|
24
|
-
/** Watching: don't let a spectator play. */
|
|
25
|
-
spectator: boolean;
|
|
26
|
-
/** A re-join after a reload or reconnect (the race was already running). */
|
|
27
|
-
resumed: boolean;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Options for `hub.race.create`. */
|
|
31
|
-
export interface RaceCreateOptions {
|
|
32
|
-
/** The race params (defaults from the manifest). */
|
|
33
|
-
params?: RaceParams;
|
|
34
|
-
/** Public (default true): shows in Open races and can alert friends who follow the game. */
|
|
35
|
-
public?: boolean;
|
|
36
|
-
/** Seats (default the manifest's minimum). The race starts when they're all filled. */
|
|
37
|
-
maxPlayers?: number;
|
|
38
|
-
/** Race this party. */
|
|
39
|
-
partyId?: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Where the hub's race HUD sits. */
|
|
43
|
-
export type RaceHudPlace = 'top' | 'bottom' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
44
|
-
|
|
45
|
-
/** `hub.race.define`: how the hub drives a race game. */
|
|
46
|
-
export interface RaceDefinition {
|
|
47
|
-
/** The countdown began: build the puzzle from `seed` and `params` now and let the player move at `startAt`. */
|
|
48
|
-
start(race: RaceStart): void;
|
|
49
|
-
/** The race was decided (stop input; the hub shows the result card). */
|
|
50
|
-
end?(result: RaceResult): void;
|
|
51
|
-
/** The player left the race screen (Done, cancelled, the room closed): go back to your menu. */
|
|
52
|
-
exit?(): void;
|
|
53
|
-
/** The hub's live race HUD (default on, top). `false` if you draw your own from `hub.race.on('status')`. */
|
|
54
|
-
hud?: false | { place?: RaceHudPlace };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Events `hub.race.on` delivers, name → payload. */
|
|
58
|
-
export interface RaceEvents {
|
|
59
|
-
/** The race room changed (roster, phase, statuses folded in). */
|
|
60
|
-
room: RoomInfo;
|
|
61
|
-
/** The countdown began. */
|
|
62
|
-
start: RaceStart;
|
|
63
|
-
/** Another racer's live status. */
|
|
64
|
-
status: RaceStatusEvent;
|
|
65
|
-
/** The race was decided. */
|
|
66
|
-
result: RaceResult;
|
|
67
|
-
/** The race room is gone for this page (left, closed, kicked). */
|
|
68
|
-
closed: { reason: string };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Filters for the race history. */
|
|
72
|
-
export interface RaceHistoryQuery {
|
|
73
|
-
/** A game slug (default: every game). */
|
|
74
|
-
game?: string;
|
|
75
|
-
/** Only races with this player. */
|
|
76
|
-
with?: number;
|
|
77
|
-
/** Only races older than this race id (paging). */
|
|
78
|
-
before?: number;
|
|
79
|
-
/** Page size, 1 to 50. */
|
|
80
|
-
limit?: number;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** Options for `hub.race.openHistory`. */
|
|
84
|
-
export interface RaceHistoryViewOptions {
|
|
85
|
-
/** This game (default), another slug, or `null` for every game. */
|
|
86
|
-
game?: string | null;
|
|
87
|
-
/** Only races with this player. */
|
|
88
|
-
with?: number;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* `hub.race`: races (docs/multiplayer.md "Races"). Declare `game.race` in
|
|
93
|
-
* package.json, call `define` at boot, put a Race button on your menu that
|
|
94
|
-
* calls `create({ params })` (or `browse()`), and report `status`, `finish`
|
|
95
|
-
* and `lose`. The hub does the matching, the waiting card, invites, the
|
|
96
|
-
* countdown, the live HUD, the referee, the result card and the history.
|
|
97
|
-
*/
|
|
98
|
-
export interface HubRaceApi {
|
|
99
|
-
/** Register how the hub starts and ends your race. Call once at boot (works before the runtime loads). */
|
|
100
|
-
define(definition: RaceDefinition): void;
|
|
101
|
-
/** Create a race (you're seated; the hub shows the waiting card). Rejects with `HubError` or `OfflineError`. */
|
|
102
|
-
create(options?: RaceCreateOptions): Promise<RoomInfo>;
|
|
103
|
-
/** Join a race by its 5-character code (or watch: `{ spectate: true }`). */
|
|
104
|
-
join(code: string, options?: JoinOptions): Promise<RoomInfo>;
|
|
105
|
-
/** Public races in this game waiting for racers. */
|
|
106
|
-
list(): Promise<RoomInfo[]>;
|
|
107
|
-
/** Open the hub's "Open races" sheet (join one, or type a code). */
|
|
108
|
-
browse(): void;
|
|
109
|
-
/** Open the hub's race history (this game by default). */
|
|
110
|
-
openHistory(options?: RaceHistoryViewOptions): void;
|
|
111
|
-
/** The signed-in player's races. */
|
|
112
|
-
history(query?: RaceHistoryQuery): Promise<RaceHistory>;
|
|
113
|
-
/** Live stats for the HUD (and optional 0..1 progress). Throttled and coalesced: call it on every change. */
|
|
114
|
-
status(stats: RaceStats, progress?: number): void;
|
|
115
|
-
/** I finished: resolves with the result if I won, `null` if someone was first. */
|
|
116
|
-
finish(stats?: RaceStats): Promise<RaceResult | null>;
|
|
117
|
-
/** I'm out (game over, no lives): the others race on, the last one in wins. */
|
|
118
|
-
lose(stats?: RaceStats): Promise<void>;
|
|
119
|
-
/** Give up. */
|
|
120
|
-
forfeit(): Promise<void>;
|
|
121
|
-
/** Leave the race room (a forfeit if it's running). */
|
|
122
|
-
leave(): Promise<void>;
|
|
123
|
-
/** Ask for another round after a race. */
|
|
124
|
-
rematch(params?: RaceParams): Promise<void>;
|
|
125
|
-
/** The race room this page is in (its `RoomInfo` has `race`), or `null`. */
|
|
126
|
-
current(): RoomInfo | null;
|
|
127
|
-
/** Subscribe to race events; works before the runtime loads. Returns the unsubscribe function. */
|
|
128
|
-
on<Name extends keyof RaceEvents>(event: Name, listener: (payload: RaceEvents[Name]) => void): () => void;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** What the runtime's `race` section implements (the same surface `hub.race` forwards). */
|
|
132
|
-
export type RaceRuntimeApi = HubRaceApi;
|
|
1
|
+
/**
|
|
2
|
+
* Races as games see them through `hub.race` (docs/plans/race.md): a hub
|
|
3
|
+
* room with a referee. Everyone gets the same puzzle (shared seed and
|
|
4
|
+
* params), live status, and the first valid finish wins.
|
|
5
|
+
*/
|
|
6
|
+
import type { RaceParams, RaceResult, RaceStats, RoomInfo } from '@diffenderfer-games/hub-contract';
|
|
7
|
+
import type { RaceHistory, RaceStatusEvent } from './wire-types';
|
|
8
|
+
import type { JoinOptions } from './room-types';
|
|
9
|
+
|
|
10
|
+
/** What a race game's `start` receives: build the puzzle from `seed` and `params`. */
|
|
11
|
+
export interface RaceStart {
|
|
12
|
+
/** The race room. */
|
|
13
|
+
roomId: string;
|
|
14
|
+
/** 1 for the first race in the room, +1 per rematch. */
|
|
15
|
+
round: number;
|
|
16
|
+
/** The shared seed, revealed at the countdown. */
|
|
17
|
+
seed: number;
|
|
18
|
+
/** The creator's choices. */
|
|
19
|
+
params: RaceParams;
|
|
20
|
+
/** The go, on THIS page's clock (`Date.now()` ms). The hub draws the 3-2-1. */
|
|
21
|
+
startAt: number;
|
|
22
|
+
/** Racer user ids. */
|
|
23
|
+
players: number[];
|
|
24
|
+
/** Watching: don't let a spectator play. */
|
|
25
|
+
spectator: boolean;
|
|
26
|
+
/** A re-join after a reload or reconnect (the race was already running). */
|
|
27
|
+
resumed: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Options for `hub.race.create`. */
|
|
31
|
+
export interface RaceCreateOptions {
|
|
32
|
+
/** The race params (defaults from the manifest). */
|
|
33
|
+
params?: RaceParams;
|
|
34
|
+
/** Public (default true): shows in Open races and can alert friends who follow the game. */
|
|
35
|
+
public?: boolean;
|
|
36
|
+
/** Seats (default the manifest's minimum). The race starts when they're all filled. */
|
|
37
|
+
maxPlayers?: number;
|
|
38
|
+
/** Race this party. */
|
|
39
|
+
partyId?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Where the hub's race HUD sits. */
|
|
43
|
+
export type RaceHudPlace = 'top' | 'bottom' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
44
|
+
|
|
45
|
+
/** `hub.race.define`: how the hub drives a race game. */
|
|
46
|
+
export interface RaceDefinition {
|
|
47
|
+
/** The countdown began: build the puzzle from `seed` and `params` now and let the player move at `startAt`. */
|
|
48
|
+
start(race: RaceStart): void;
|
|
49
|
+
/** The race was decided (stop input; the hub shows the result card). */
|
|
50
|
+
end?(result: RaceResult): void;
|
|
51
|
+
/** The player left the race screen (Done, cancelled, the room closed): go back to your menu. */
|
|
52
|
+
exit?(): void;
|
|
53
|
+
/** The hub's live race HUD (default on, top). `false` if you draw your own from `hub.race.on('status')`. */
|
|
54
|
+
hud?: false | { place?: RaceHudPlace };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Events `hub.race.on` delivers, name → payload. */
|
|
58
|
+
export interface RaceEvents {
|
|
59
|
+
/** The race room changed (roster, phase, statuses folded in). */
|
|
60
|
+
room: RoomInfo;
|
|
61
|
+
/** The countdown began. */
|
|
62
|
+
start: RaceStart;
|
|
63
|
+
/** Another racer's live status. */
|
|
64
|
+
status: RaceStatusEvent;
|
|
65
|
+
/** The race was decided. */
|
|
66
|
+
result: RaceResult;
|
|
67
|
+
/** The race room is gone for this page (left, closed, kicked). */
|
|
68
|
+
closed: { reason: string };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Filters for the race history. */
|
|
72
|
+
export interface RaceHistoryQuery {
|
|
73
|
+
/** A game slug (default: every game). */
|
|
74
|
+
game?: string;
|
|
75
|
+
/** Only races with this player. */
|
|
76
|
+
with?: number;
|
|
77
|
+
/** Only races older than this race id (paging). */
|
|
78
|
+
before?: number;
|
|
79
|
+
/** Page size, 1 to 50. */
|
|
80
|
+
limit?: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Options for `hub.race.openHistory`. */
|
|
84
|
+
export interface RaceHistoryViewOptions {
|
|
85
|
+
/** This game (default), another slug, or `null` for every game. */
|
|
86
|
+
game?: string | null;
|
|
87
|
+
/** Only races with this player. */
|
|
88
|
+
with?: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* `hub.race`: races (docs/multiplayer.md "Races"). Declare `game.race` in
|
|
93
|
+
* package.json, call `define` at boot, put a Race button on your menu that
|
|
94
|
+
* calls `create({ params })` (or `browse()`), and report `status`, `finish`
|
|
95
|
+
* and `lose`. The hub does the matching, the waiting card, invites, the
|
|
96
|
+
* countdown, the live HUD, the referee, the result card and the history.
|
|
97
|
+
*/
|
|
98
|
+
export interface HubRaceApi {
|
|
99
|
+
/** Register how the hub starts and ends your race. Call once at boot (works before the runtime loads). */
|
|
100
|
+
define(definition: RaceDefinition): void;
|
|
101
|
+
/** Create a race (you're seated; the hub shows the waiting card). Rejects with `HubError` or `OfflineError`. */
|
|
102
|
+
create(options?: RaceCreateOptions): Promise<RoomInfo>;
|
|
103
|
+
/** Join a race by its 5-character code (or watch: `{ spectate: true }`). */
|
|
104
|
+
join(code: string, options?: JoinOptions): Promise<RoomInfo>;
|
|
105
|
+
/** Public races in this game waiting for racers. */
|
|
106
|
+
list(): Promise<RoomInfo[]>;
|
|
107
|
+
/** Open the hub's "Open races" sheet (join one, or type a code). */
|
|
108
|
+
browse(): void;
|
|
109
|
+
/** Open the hub's race history (this game by default). */
|
|
110
|
+
openHistory(options?: RaceHistoryViewOptions): void;
|
|
111
|
+
/** The signed-in player's races. */
|
|
112
|
+
history(query?: RaceHistoryQuery): Promise<RaceHistory>;
|
|
113
|
+
/** Live stats for the HUD (and optional 0..1 progress). Throttled and coalesced: call it on every change. */
|
|
114
|
+
status(stats: RaceStats, progress?: number): void;
|
|
115
|
+
/** I finished: resolves with the result if I won, `null` if someone was first. */
|
|
116
|
+
finish(stats?: RaceStats): Promise<RaceResult | null>;
|
|
117
|
+
/** I'm out (game over, no lives): the others race on, the last one in wins. */
|
|
118
|
+
lose(stats?: RaceStats): Promise<void>;
|
|
119
|
+
/** Give up. */
|
|
120
|
+
forfeit(): Promise<void>;
|
|
121
|
+
/** Leave the race room (a forfeit if it's running). */
|
|
122
|
+
leave(): Promise<void>;
|
|
123
|
+
/** Ask for another round after a race. */
|
|
124
|
+
rematch(params?: RaceParams): Promise<void>;
|
|
125
|
+
/** The race room this page is in (its `RoomInfo` has `race`), or `null`. */
|
|
126
|
+
current(): RoomInfo | null;
|
|
127
|
+
/** Subscribe to race events; works before the runtime loads. Returns the unsubscribe function. */
|
|
128
|
+
on<Name extends keyof RaceEvents>(event: Name, listener: (payload: RaceEvents[Name]) => void): () => void;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** What the runtime's `race` section implements (the same surface `hub.race` forwards). */
|
|
132
|
+
export type RaceRuntimeApi = HubRaceApi;
|