@diffenderfer-games/hub 2.0.0-next.1 → 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 +160 -6
- package/README.md +27 -15
- 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 +71 -10
- 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 +1 -39
- 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 +39 -3
- 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 +201 -92
- package/src/core/errors.ts +15 -2
- package/src/core/game-data.ts +11 -9
- package/src/core/hub-economy.ts +47 -10
- package/src/core/hub-progress.ts +94 -30
- package/src/core/hub.ts +228 -7
- 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/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 +66 -23
- 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 +4 -49
- 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 +14 -1
- package/src/core/rest/rest-client.ts +14 -3
- 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 +6 -0
- 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/version/client-version.gen.ts +2 -2
- package/src/daily/daily-system.ts +127 -12
- package/src/daily/page-daily.ts +2 -2
- package/src/daily/types.ts +49 -11
- package/src/doctor/checks.ts +12 -4
- package/src/doctor/cli.ts +5 -2
- package/src/doctor/hub-version-fetch.ts +43 -43
- package/src/doctor/world-check.ts +61 -0
- package/src/index.ts +20 -2
- package/src/input/focus-ring.ts +13 -0
- package/src/input/input-registry.ts +9 -2
- package/src/input/input-system.ts +14 -1
- package/src/input/modality.ts +122 -0
- package/src/input/nav-state.ts +13 -4
- package/src/input/navigator.ts +11 -0
- package/src/input/zero-config.ts +1 -14
- package/src/input.ts +4 -0
- package/src/mp.ts +1 -1
- package/src/overlay/overlay.ts +30 -4
- package/src/overlay/styles.ts +4 -3
- package/src/realtime/bridge.ts +5 -0
- package/src/realtime/multiplayer-forwarder.ts +13 -1
- package/src/realtime/multiplayer-types.ts +15 -1
- package/src/realtime/room-types.ts +28 -2
- package/src/realtime/rooms-forwarder.ts +27 -2
- package/src/realtime/runtime-api.ts +33 -8
- package/src/realtime/social-forwarder.ts +5 -0
- package/src/realtime/social-types.ts +8 -0
- package/src/realtime/types.ts +1 -0
- package/src/realtime/wire-types.ts +7 -0
- package/src/realtime/world-types.ts +20 -0
- package/src/rooms.ts +1 -1
- package/src/social.ts +1 -0
- package/src/turns-kit/check-rules.ts +281 -0
- package/src/turns-rules.ts +12 -0
- package/src/turns.ts +14 -0
package/src/index.ts
CHANGED
|
@@ -13,10 +13,11 @@
|
|
|
13
13
|
* {@link createHub} for others. Every call rejects with a {@link HubError}
|
|
14
14
|
* when it fails, except the fire-and-forget analytics (`recordPlay`,
|
|
15
15
|
* `heartbeat`, `event`). When the hub can't be reached, reads come from an
|
|
16
|
-
* offline cache and writes are queued until it is back
|
|
16
|
+
* offline cache and writes are queued in an outbox until it is back; games
|
|
17
|
+
* queue their own operations with `hub.offline` (docs: "Offline and replay").
|
|
17
18
|
*
|
|
18
19
|
* The page-wide namespaces hang off `hub` as lazy getters (`hub.input`,
|
|
19
|
-
* `hub.daily`, `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
|
|
20
21
|
* its own entry (`@diffenderfer-games/hub/input`, ...) for games that want
|
|
21
22
|
* only that part.
|
|
22
23
|
*
|
|
@@ -27,6 +28,7 @@ export {
|
|
|
27
28
|
ContentRejectedError,
|
|
28
29
|
HubError,
|
|
29
30
|
OfflineError,
|
|
31
|
+
SAVE_UNKNOWN_CODE,
|
|
30
32
|
UNSUPPORTED_BY_HUB,
|
|
31
33
|
type HubErrorCode,
|
|
32
34
|
type HubErrorDetails,
|
|
@@ -35,7 +37,23 @@ export type { SyncConflict, SyncResult } from './core/connection';
|
|
|
35
37
|
export type { AuthResponse, ProfileUpdate, StatResult } from './core/hub-progress';
|
|
36
38
|
export type { GrantBundle, TradeBundle, TradeId, TradeListOptions, TradeNoteOptions } from './core/hub-economy';
|
|
37
39
|
export type { HubMenuControl } from './core/menu-control';
|
|
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';
|
|
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 { isNewlyDue, readYourTurnBuzz, saveYourTurnBuzz, YOUR_TURN_BUZZ, YOUR_TURN_BUZZ_KEY } from './core/turns/your-turn-buzz';
|
|
50
|
+
export { TURN_CHAT_EVENT, TURN_EVENT, TURN_POLL_MS, TURN_QUIET_MS, TURN_RESYNC_EVENT, TurnGame, type TurnGameEvents, type TurnGameOptions, type TurnMoveEvent, type TurnOutcome } from './core/turns/turn-game';
|
|
51
|
+
export type { OpenTurnGame, TurnChatLine, TurnDeadline, TurnGameView } from '@diffenderfer-games/hub-contract';
|
|
38
52
|
export type { QueueOp, QueueOpInput, QueuedScoreOptions, ProfilePatch } from './core/offline/queue';
|
|
53
|
+
export { OfflineApi } from './core/offline/offline-api';
|
|
54
|
+
export { OUTBOX_FULL_CODE, type OfflineEventDetails, type OfflineEventName, type OfflineListener, type OfflineOp } from './core/offline/outbox';
|
|
55
|
+
export type { OfflineHandler, OfflineKindDefinition, ReplayContext } from './core/offline/outbox-kinds';
|
|
56
|
+
export { OUTBOX_MAX_CHARS, OUTBOX_MAX_ENTRIES } from './core/offline/outbox-store';
|
|
39
57
|
export type { BufferedEvent } from './core/offline/events';
|
|
40
58
|
export type { EndpointFields, EndpointName, EndpointReply } from './core/rest/endpoint-types';
|
|
41
59
|
export { CLIENT_VERSION_HEADER } from './core/rest/rest-client';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hub's focus ring: a two-tone box-shadow (a dark gap, then a light ring)
|
|
3
|
+
* that follows the element's own border-radius and shows on light and dark
|
|
4
|
+
* buttons alike. Pages tune it with CSS variables:
|
|
5
|
+
* `--hub-focus-color` (the ring), `--hub-focus-inner` (the gap),
|
|
6
|
+
* `--hub-focus-width` and `--hub-focus-gap`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const GAP = 'var(--hub-focus-gap, 2px)';
|
|
10
|
+
const WIDTH = 'var(--hub-focus-width, 3px)';
|
|
11
|
+
|
|
12
|
+
/** The ring as a `box-shadow` value. */
|
|
13
|
+
export const FOCUS_RING_SHADOW = `0 0 0 ${GAP} var(--hub-focus-inner, rgba(13, 15, 20, 0.85)), 0 0 0 calc(${GAP} + ${WIDTH}) var(--hub-focus-color, #fff)`;
|
|
@@ -132,13 +132,20 @@ export class InputRegistry {
|
|
|
132
132
|
return { value: raw, raw, isDown: this.wentDown(name), isUp: this.wentUp(name) };
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
/**
|
|
135
|
+
/**
|
|
136
|
+
* A named axis (or an ad-hoc negative/positive pair) as -1..1: its `x`, or
|
|
137
|
+
* its `y` when it declares only `y` (a vertical one-axis control, like a
|
|
138
|
+
* paddle).
|
|
139
|
+
*/
|
|
136
140
|
axis(name: string, positive?: string): number {
|
|
137
141
|
if (positive !== undefined) {
|
|
138
142
|
return clampUnit(this.value(positive)) - clampUnit(this.value(name));
|
|
139
143
|
}
|
|
140
144
|
const axis = this.axes.get(name);
|
|
141
|
-
|
|
145
|
+
if (axis === undefined) {
|
|
146
|
+
return this.value(name);
|
|
147
|
+
}
|
|
148
|
+
return readAxisSide(axis.x ?? axis.y, (input) => this.value(input));
|
|
142
149
|
}
|
|
143
150
|
|
|
144
151
|
/** A named axis pair as a vector clamped to the unit circle. */
|
|
@@ -13,10 +13,12 @@ import { RepeatingEdges, RisingEdges } from './edges';
|
|
|
13
13
|
import { isAnyPadButtonPressed, PAD_BUTTON } from './gamepad';
|
|
14
14
|
import { InputEventHub } from './input-events';
|
|
15
15
|
import { InputRegistry } from './input-registry';
|
|
16
|
+
import { trackInputModality } from './modality';
|
|
17
|
+
import type { ModalityTracker } from './modality';
|
|
16
18
|
import { parseLegacyControls } from './legacy-controls';
|
|
17
19
|
import type { LegacyControls } from './legacy-controls';
|
|
18
20
|
import { Navigator } from './navigator';
|
|
19
|
-
import type { NavigatorContext } from './navigator';
|
|
21
|
+
import type { AutoNavigateTarget, NavigatorContext } from './navigator';
|
|
20
22
|
import { feedDeviceState, OnScreenControls } from './on-screen-controls';
|
|
21
23
|
import type { PageInputControl } from './page-control';
|
|
22
24
|
import { describeBinding, PlayerRemaps } from './player-remaps';
|
|
@@ -56,6 +58,7 @@ export class InputSystem implements ControlsPanelHost, PageInputControl {
|
|
|
56
58
|
private activeSource: InputSource = 'keyboard';
|
|
57
59
|
private isPageOwner: boolean;
|
|
58
60
|
private pageOwner: PageInputControl | null = null;
|
|
61
|
+
private readonly modality: ModalityTracker | null = typeof document === 'undefined' ? null : trackInputModality();
|
|
59
62
|
|
|
60
63
|
constructor(host: InputHost, options: InputSystemOptions = {}) {
|
|
61
64
|
this.host = host;
|
|
@@ -283,6 +286,15 @@ export class InputSystem implements ControlsPanelHost, PageInputControl {
|
|
|
283
286
|
this.navigator.setAutoNavigate(Navigator.queryFor(selector), options?.back ?? null);
|
|
284
287
|
}
|
|
285
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
|
+
|
|
286
298
|
/** Stop navigating the game's own menu. */
|
|
287
299
|
clearAutoNavigate(): void {
|
|
288
300
|
this.navigator.clearAutoNavigate(this.controls.existingOverlay);
|
|
@@ -352,6 +364,7 @@ export class InputSystem implements ControlsPanelHost, PageInputControl {
|
|
|
352
364
|
this.controls.isGamepadConnected = isConnected;
|
|
353
365
|
if (hasActivity) {
|
|
354
366
|
this.setSource('gamepad');
|
|
367
|
+
this.modality?.set('keys');
|
|
355
368
|
}
|
|
356
369
|
if (this.isPageOwner) {
|
|
357
370
|
this.driveFrame();
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which kind of input the player used last, published as `data-hub-input` on
|
|
3
|
+
* `<html>` so focus styles show only to keyboard and gamepad players: a tap or
|
|
4
|
+
* click never leaves a ring behind, and touch devices show none at all. The
|
|
5
|
+
* attribute is the shared state, so every bundle's copy of the client agrees.
|
|
6
|
+
*/
|
|
7
|
+
import { isTypingTarget } from './device-state';
|
|
8
|
+
import { FOCUS_RING_SHADOW } from './focus-ring';
|
|
9
|
+
|
|
10
|
+
/** `keys`: keyboard or gamepad. `pointer`: mouse or pen. `touch`: a finger. */
|
|
11
|
+
export type InputModality = 'keys' | 'pointer' | 'touch';
|
|
12
|
+
|
|
13
|
+
/** The `<html>` attribute that holds the modality. */
|
|
14
|
+
export const MODALITY_ATTRIBUTE = 'data-hub-input';
|
|
15
|
+
|
|
16
|
+
const STYLE_ID = 'hub-focus-style';
|
|
17
|
+
const MODIFIER_KEYS = new Set(['Shift', 'Control', 'Alt', 'Meta', 'AltGraph', 'CapsLock']);
|
|
18
|
+
const MODALITIES: readonly InputModality[] = ['keys', 'pointer', 'touch'];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The page focus styles: the gamepad highlight (`gp-focus`) for keyboard and
|
|
22
|
+
* gamepad players only, and no browser focus ring on buttons after a tap.
|
|
23
|
+
*/
|
|
24
|
+
const PAGE_FOCUS_CSS = `html[${MODALITY_ATTRIBUTE}="keys"] .gp-focus{outline:none !important;box-shadow:${FOCUS_RING_SHADOW} !important;}
|
|
25
|
+
html[${MODALITY_ATTRIBUTE}="touch"] :is(button,a,summary,[role="button"],[tabindex]):focus{outline:none;}`;
|
|
26
|
+
|
|
27
|
+
/** The modality a pointer event's `pointerType` implies. */
|
|
28
|
+
export function modalityOfPointer(pointerType: string): InputModality {
|
|
29
|
+
return pointerType === 'touch' ? 'touch' : 'pointer';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The modality a key press implies, or `null` when it says nothing (a lone modifier, typing in a field). */
|
|
33
|
+
export function modalityOfKey(key: string, isTyping: boolean): InputModality | null {
|
|
34
|
+
if (MODIFIER_KEYS.has(key)) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
// Typing (an on-screen keyboard fires keydown too) is not navigating; Tab always is.
|
|
38
|
+
return isTyping && key !== 'Tab' ? null : 'keys';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Where the modality is stored: `<html>`, or a fake in tests. */
|
|
42
|
+
export interface ModalityRoot {
|
|
43
|
+
getAttribute(name: string): string | null;
|
|
44
|
+
setAttribute(name: string, value: string): void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Reads and writes the page's modality attribute. */
|
|
48
|
+
export class ModalityTracker {
|
|
49
|
+
private readonly root: ModalityRoot;
|
|
50
|
+
|
|
51
|
+
/** Start at `initial` unless another copy already set the attribute. */
|
|
52
|
+
constructor(root: ModalityRoot, initial: InputModality) {
|
|
53
|
+
this.root = root;
|
|
54
|
+
if (this.readAttribute() === null) {
|
|
55
|
+
this.set(initial);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The last modality used. */
|
|
60
|
+
get current(): InputModality {
|
|
61
|
+
return this.readAttribute() ?? 'pointer';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Whether focus should show: the last input was a keyboard or gamepad. */
|
|
65
|
+
get showsFocus(): boolean {
|
|
66
|
+
return this.current === 'keys';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Record the modality the player just used. */
|
|
70
|
+
set(modality: InputModality): void {
|
|
71
|
+
if (this.readAttribute() !== modality) {
|
|
72
|
+
this.root.setAttribute(MODALITY_ATTRIBUTE, modality);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** A key went down (`isTyping`: in a text field). */
|
|
77
|
+
noteKey(key: string, isTyping: boolean): void {
|
|
78
|
+
const modality = modalityOfKey(key, isTyping);
|
|
79
|
+
if (modality !== null) {
|
|
80
|
+
this.set(modality);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** A pointer went down. */
|
|
85
|
+
notePointer(pointerType: string): void {
|
|
86
|
+
this.set(modalityOfPointer(pointerType));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private readAttribute(): InputModality | null {
|
|
90
|
+
const value = this.root.getAttribute(MODALITY_ATTRIBUTE);
|
|
91
|
+
return MODALITIES.find((modality) => modality === value) ?? null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let pageTracker: ModalityTracker | null = null;
|
|
96
|
+
|
|
97
|
+
/** Track the page's modality (once per bundle) and add the focus styles (once per page). */
|
|
98
|
+
export function trackInputModality(): ModalityTracker {
|
|
99
|
+
if (pageTracker !== null) {
|
|
100
|
+
return pageTracker;
|
|
101
|
+
}
|
|
102
|
+
const root = document.documentElement;
|
|
103
|
+
const isTouchDevice = typeof matchMedia === 'function' && matchMedia('(hover: none) and (pointer: coarse)').matches;
|
|
104
|
+
const isFirstCopy = root.getAttribute(MODALITY_ATTRIBUTE) === null;
|
|
105
|
+
const tracker = new ModalityTracker(root, isTouchDevice ? 'touch' : 'pointer');
|
|
106
|
+
pageTracker = tracker;
|
|
107
|
+
if (isFirstCopy) {
|
|
108
|
+
document.addEventListener('keydown', (event) => {
|
|
109
|
+
tracker.noteKey(event.key, isTypingTarget(event.target));
|
|
110
|
+
}, true);
|
|
111
|
+
document.addEventListener('pointerdown', (event) => {
|
|
112
|
+
tracker.notePointer(event.pointerType);
|
|
113
|
+
}, true);
|
|
114
|
+
}
|
|
115
|
+
if (document.getElementById(STYLE_ID) === null) {
|
|
116
|
+
const style = document.createElement('style');
|
|
117
|
+
style.id = STYLE_ID;
|
|
118
|
+
style.textContent = PAGE_FOCUS_CSS;
|
|
119
|
+
document.head.appendChild(style);
|
|
120
|
+
}
|
|
121
|
+
return tracker;
|
|
122
|
+
}
|
package/src/input/nav-state.ts
CHANGED
|
@@ -9,8 +9,8 @@ import type { NavBlurEvent, NavElement, NavElementKind, NavFocusEvent, NavTarget
|
|
|
9
9
|
|
|
10
10
|
/** What a navigable list draws with (the overlay). */
|
|
11
11
|
export interface NavHighlighter {
|
|
12
|
-
/** Ring the highlighted element. */
|
|
13
|
-
showHighlight(bounds: Rect): void;
|
|
12
|
+
/** Ring the highlighted element, following its corner radius (a CSS `border-radius`, or empty). */
|
|
13
|
+
showHighlight(bounds: Rect, radius: string): void;
|
|
14
14
|
/** Remove the ring. */
|
|
15
15
|
hideHighlight(): void;
|
|
16
16
|
/** Open the on-screen keyboard for a text field. */
|
|
@@ -50,6 +50,11 @@ function boundsOf(element: NavElement): Rect {
|
|
|
50
50
|
return element.el === undefined ? { x: 0, y: 0, width: 0, height: 0 } : rectOf(element.el);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/** A navigable element's CSS corner radius, so the ring can follow it (empty for a non-DOM element). */
|
|
54
|
+
function radiusOf(element: NavElement): string {
|
|
55
|
+
return element.el === undefined ? '' : getComputedStyle(element.el).borderRadius;
|
|
56
|
+
}
|
|
57
|
+
|
|
53
58
|
/** The text field a navigable element edits, if it is one. */
|
|
54
59
|
function textFieldOf(element: NavElement): HTMLInputElement | HTMLTextAreaElement | null {
|
|
55
60
|
return element.el instanceof HTMLInputElement || element.el instanceof HTMLTextAreaElement ? element.el : null;
|
|
@@ -148,16 +153,20 @@ export class NavState {
|
|
|
148
153
|
this.position(highlighter);
|
|
149
154
|
}
|
|
150
155
|
|
|
151
|
-
/** Move the ring to the highlighted element (or hide it). */
|
|
156
|
+
/** Move the ring to the highlighted element (or hide it), following keyboard focus (Tab) within the list. */
|
|
152
157
|
position(highlighter: NavHighlighter | null): void {
|
|
153
158
|
if (highlighter === null) {
|
|
154
159
|
return;
|
|
155
160
|
}
|
|
161
|
+
const focused = this.elements.findIndex((element) => element.el !== undefined && element.el === document.activeElement);
|
|
162
|
+
if (focused >= 0) {
|
|
163
|
+
this.index = focused;
|
|
164
|
+
}
|
|
156
165
|
const current = this.elements[this.index];
|
|
157
166
|
if (current === undefined) {
|
|
158
167
|
highlighter.hideHighlight();
|
|
159
168
|
} else {
|
|
160
|
-
highlighter.showHighlight(boundsOf(current));
|
|
169
|
+
highlighter.showHighlight(boundsOf(current), radiusOf(current));
|
|
161
170
|
}
|
|
162
171
|
}
|
|
163
172
|
|
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/zero-config.ts
CHANGED
|
@@ -14,9 +14,8 @@ import { isTypingTarget } from './device-state';
|
|
|
14
14
|
|
|
15
15
|
/** Synthetic keys stay pressed past half deflection. */
|
|
16
16
|
const SYNTH_DEADZONE = 0.5;
|
|
17
|
+
/** The catalog tile highlight; its style (keyboard and gamepad players only) is in `modality.ts`. */
|
|
17
18
|
const FOCUS_CLASS = 'gp-focus';
|
|
18
|
-
const FOCUS_STYLE_ID = 'gp-focus-style';
|
|
19
|
-
const FOCUS_CSS = '.gp-focus{outline:3px solid #ff5f3b !important;outline-offset:2px;border-radius:4px;}';
|
|
20
19
|
|
|
21
20
|
/** `KeyboardEvent.code` → its `key` and legacy `keyCode`. */
|
|
22
21
|
const KEY_FOR_CODE = new Map<string, readonly [string, number]>([
|
|
@@ -51,17 +50,6 @@ function dispatchKey(type: 'keydown' | 'keyup', code: string): void {
|
|
|
51
50
|
target.dispatchEvent(event);
|
|
52
51
|
}
|
|
53
52
|
|
|
54
|
-
/** Add the highlight style to the page once. */
|
|
55
|
-
function ensureFocusStyle(): void {
|
|
56
|
-
if (document.getElementById(FOCUS_STYLE_ID) !== null) {
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
const style = document.createElement('style');
|
|
60
|
-
style.id = FOCUS_STYLE_ID;
|
|
61
|
-
style.textContent = FOCUS_CSS;
|
|
62
|
-
document.head.appendChild(style);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
53
|
/** The keys each pad holds down, by pad index. */
|
|
66
54
|
const heldCodes = new Map<number, Set<string>>();
|
|
67
55
|
|
|
@@ -168,7 +156,6 @@ export function runZeroConfigFrame(frame: ZeroConfigFrame): void {
|
|
|
168
156
|
if (frame.gamepads.length === 0) {
|
|
169
157
|
return;
|
|
170
158
|
}
|
|
171
|
-
ensureFocusStyle();
|
|
172
159
|
if (frame.slug === null) {
|
|
173
160
|
navigateCatalog(frame);
|
|
174
161
|
} else {
|
package/src/input.ts
CHANGED
|
@@ -9,9 +9,13 @@ export { InputSystem } from './input/input-system';
|
|
|
9
9
|
export type { InputSystemOptions } from './input/input-system';
|
|
10
10
|
export { getInput, INPUT_SINGLETON_API, isInputSystem, parseInputOverrides } from './input/page-input';
|
|
11
11
|
export type { InputOptions } from './input/page-input';
|
|
12
|
+
export { FOCUS_RING_SHADOW } from './input/focus-ring';
|
|
13
|
+
export { MODALITY_ATTRIBUTE, trackInputModality } from './input/modality';
|
|
14
|
+
export type { InputModality } from './input/modality';
|
|
12
15
|
export { isPageInputControl } from './input/page-control';
|
|
13
16
|
export type { PageInputControl } from './input/page-control';
|
|
14
17
|
export type * from './input/types';
|
|
18
|
+
export type { AutoNavigateTarget } from './input/navigator';
|
|
15
19
|
export { Overlay } from './overlay/overlay';
|
|
16
20
|
export { layoutControls } from './overlay/layout';
|
|
17
21
|
export type { LayoutOptions } from './overlay/layout';
|
package/src/mp.ts
CHANGED
|
@@ -6,5 +6,5 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { multiplayerForwarder as mp } from './realtime/multiplayer-forwarder';
|
|
8
8
|
export type * from './realtime/multiplayer-types';
|
|
9
|
-
export type { InviteCreated } from './realtime/wire-types';
|
|
9
|
+
export type { InviteCreated, InviteLink } from './realtime/wire-types';
|
|
10
10
|
export type { Invite, JoinInfo, Launch, LaunchTicket, Party } from '@diffenderfer-games/hub-contract';
|
package/src/overlay/overlay.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { NavHighlighter } from '../input/nav-state';
|
|
8
8
|
import type { InputConfig, InputOverrides, Rect, VirtualDef } from '../input/types';
|
|
9
|
+
import { trackInputModality } from '../input/modality';
|
|
9
10
|
import { readField } from '../realtime/cross-bundle';
|
|
10
11
|
import { ControlsPanel } from './controls-panel';
|
|
11
12
|
import type { ControlsPanelHost } from './controls-panel';
|
|
@@ -122,9 +123,15 @@ export class Overlay implements NavHighlighter {
|
|
|
122
123
|
}
|
|
123
124
|
}
|
|
124
125
|
|
|
125
|
-
/**
|
|
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
|
+
*/
|
|
126
132
|
setInsets(insets: Partial<Insets>): void {
|
|
127
133
|
this.gameInsets = { ...this.gameInsets, ...insets };
|
|
134
|
+
publishInsets(this.gameInsets);
|
|
128
135
|
this.layout();
|
|
129
136
|
}
|
|
130
137
|
|
|
@@ -160,9 +167,10 @@ export class Overlay implements NavHighlighter {
|
|
|
160
167
|
}
|
|
161
168
|
}
|
|
162
169
|
|
|
163
|
-
/** Ring the highlighted element. */
|
|
164
|
-
showHighlight(bounds: Rect): void {
|
|
165
|
-
this.ring.hidden =
|
|
170
|
+
/** Ring the highlighted element, for keyboard and gamepad players only (never after a tap or click). */
|
|
171
|
+
showHighlight(bounds: Rect, radius: string): void {
|
|
172
|
+
this.ring.hidden = !trackInputModality().showsFocus;
|
|
173
|
+
this.ring.style.borderRadius = radius;
|
|
166
174
|
this.ring.style.left = `${String(bounds.x)}px`;
|
|
167
175
|
this.ring.style.top = `${String(bounds.y)}px`;
|
|
168
176
|
this.ring.style.width = `${String(bounds.width)}px`;
|
|
@@ -206,3 +214,21 @@ export class Overlay implements NavHighlighter {
|
|
|
206
214
|
return { top: pixels(style.paddingTop), right: pixels(style.paddingRight), bottom: pixels(style.paddingBottom), left: pixels(style.paddingLeft) };
|
|
207
215
|
}
|
|
208
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
|
+
}
|
package/src/overlay/styles.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The overlay's styles (inside its shadow root, so games can't restyle it).
|
|
3
3
|
*/
|
|
4
|
+
import { FOCUS_RING_SHADOW } from '../input/focus-ring';
|
|
4
5
|
|
|
5
6
|
/** The hub accent colour. */
|
|
6
7
|
export const ACCENT_COLOR = '#ff5f3b';
|
|
@@ -28,8 +29,8 @@ export const OVERLAY_CSS = `
|
|
|
28
29
|
:host(.editing) .edit-bar .btn { margin: 0; }
|
|
29
30
|
.safe-probe { position: fixed; left: 0; top: 0; width: 0; height: 0; visibility: hidden; pointer-events: none;
|
|
30
31
|
padding: env(safe-area-inset-top, 0px) env(safe-area-inset-right, 0px) env(safe-area-inset-bottom, 0px) env(safe-area-inset-left, 0px); }
|
|
31
|
-
.ring { position: fixed; pointer-events: none; z-index: 2147483200;
|
|
32
|
-
|
|
32
|
+
.ring { position: fixed; pointer-events: none; z-index: 2147483200; box-shadow: ${FOCUS_RING_SHADOW};
|
|
33
|
+
transition: left .08s, top .08s, width .08s, height .08s; }
|
|
33
34
|
.ring[hidden] { display: none; }
|
|
34
35
|
.kb { position: fixed; left: 0; right: 0; bottom: 0; z-index: 2147483400; background: #0d0f14ee; padding: 10px;
|
|
35
36
|
pointer-events: auto; border-top: 1px solid #2a3142; }
|
|
@@ -38,7 +39,7 @@ export const OVERLAY_CSS = `
|
|
|
38
39
|
.kb .k { flex: 1; min-width: 0; max-width: 64px; height: 46px; border-radius: 8px; border: 1px solid #2a3142;
|
|
39
40
|
background: #11141c; color: #e8e6df; font: 16px monospace; cursor: pointer; }
|
|
40
41
|
.kb .k.space { max-width: 280px; } .kb .k.wide { max-width: 90px; }
|
|
41
|
-
.kb .k.kfocus {
|
|
42
|
+
.kb .k.kfocus { box-shadow: ${FOCUS_RING_SHADOW}; }
|
|
42
43
|
.panel { position: fixed; inset: 0; z-index: 2147483500; display: grid; place-items: center; background: rgba(0,0,0,.6); pointer-events: auto;
|
|
43
44
|
font-family: 'Space Mono', ui-monospace, monospace; }
|
|
44
45
|
.panel-card { background: #0d0f14; color: #e8e6df; border: 1px solid #2a3142; border-radius: 12px; padding: 18px;
|
package/src/realtime/bridge.ts
CHANGED
|
@@ -117,5 +117,10 @@ export async function rethrowAsHubError<Result>(pending: Promise<Result>): Promi
|
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
/** The rejection for a feature the page's social runtime is too old to have (version 3 additions). */
|
|
121
|
+
export function outdatedRuntime(feature: string): Error {
|
|
122
|
+
return hubLink().createOfflineError(`${feature} needs the newest hub. Reload the page to update.`);
|
|
123
|
+
}
|
|
124
|
+
|
|
120
125
|
/** The page's one bridge (per client bundle). */
|
|
121
126
|
export const pageBridge = new RealtimeBridge();
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* forwarded to the social runtime.
|
|
4
4
|
*/
|
|
5
5
|
import { getUiStack } from '../uistack/page-stack';
|
|
6
|
-
import { pageBridge } from './bridge';
|
|
6
|
+
import { outdatedRuntime, pageBridge } from './bridge';
|
|
7
7
|
import { hubLink } from './hub-link';
|
|
8
8
|
import { latestState } from './latest-state';
|
|
9
9
|
import type { HubMpApi } from './multiplayer-types';
|
|
@@ -25,4 +25,16 @@ export const multiplayerForwarder: HubMpApi = {
|
|
|
25
25
|
ticket: () => hubLink().getGameServerTicket(),
|
|
26
26
|
setJoinInfo: (partyId, joinInfo) => pageBridge.call((realtime) => realtime.mp.setJoinInfo(partyId, joinInfo)),
|
|
27
27
|
party: () => pageBridge.current()?.mp.party() ?? null,
|
|
28
|
+
inviteLink: () => pageBridge.call((realtime) => {
|
|
29
|
+
if (realtime.mp.inviteLink === undefined) {
|
|
30
|
+
throw outdatedRuntime('Invite links');
|
|
31
|
+
}
|
|
32
|
+
return realtime.mp.inviteLink();
|
|
33
|
+
}),
|
|
34
|
+
revokeInviteLink: (token) => pageBridge.call((realtime) => {
|
|
35
|
+
if (realtime.mp.revokeInviteLink === undefined) {
|
|
36
|
+
throw outdatedRuntime('Invite links');
|
|
37
|
+
}
|
|
38
|
+
return realtime.mp.revokeInviteLink(token);
|
|
39
|
+
}),
|
|
28
40
|
};
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* launches, the leave guard and own-server tickets.
|
|
4
4
|
*/
|
|
5
5
|
import type { Invite, JoinInfo, Launch, Party } from '@diffenderfer-games/hub-contract';
|
|
6
|
+
import type { InviteLink } from './wire-types';
|
|
6
7
|
|
|
7
8
|
/** `hub.mp.onJoinInfo` payload: the party leader published where to connect. */
|
|
8
9
|
export interface JoinInfoEvent {
|
|
@@ -30,8 +31,21 @@ export interface BusyOptions {
|
|
|
30
31
|
label?: string;
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
/** What feature version 3 added to `hub.mp`: invite links. */
|
|
35
|
+
export interface InviteLinkMethods {
|
|
36
|
+
/**
|
|
37
|
+
* A shareable link to the room I'm in (my live one when it exists): anyone
|
|
38
|
+
* who opens it joins while a seat is free, otherwise watches. It expires
|
|
39
|
+
* after 24 h. Claimed accounts only; rejects with `HubError`
|
|
40
|
+
* (`not_in_room`, `too_many_links`, `rate_limited`) or `OfflineError`.
|
|
41
|
+
*/
|
|
42
|
+
inviteLink(): Promise<InviteLink>;
|
|
43
|
+
/** Revoke a link I made, or one into a room I host. */
|
|
44
|
+
revokeInviteLink(token: string): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
|
|
33
47
|
/** `hub.mp`: the multiplayer lifecycle (docs/multiplayer.md §7). */
|
|
34
|
-
export interface HubMpApi {
|
|
48
|
+
export interface HubMpApi extends InviteLinkMethods {
|
|
35
49
|
/** Invite, join and watch arrivals. Call at boot; launches that arrived earlier are buffered. */
|
|
36
50
|
onLaunch(listener: (launch: Launch) => void): () => void;
|
|
37
51
|
/** The party leader published join info (e.g. a room code). */
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* multiplayer for static games, relayed (never refereed) by the hub.
|
|
4
4
|
*/
|
|
5
5
|
import type { Json, RoomInfo } from '@diffenderfer-games/hub-contract';
|
|
6
|
-
import type { RoomChatLine, RoomMessage } from './wire-types';
|
|
6
|
+
import type { RoomChatLine, RoomMessage, RoomSeat, RoomSeatChange } from './wire-types';
|
|
7
7
|
|
|
8
8
|
/** The shared room state (a JSON object, at most 32 KB). */
|
|
9
9
|
export type RoomState = Record<string, Json>;
|
|
@@ -20,6 +20,13 @@ export interface RoomCreateOptions {
|
|
|
20
20
|
maxPlayers?: number;
|
|
21
21
|
/** `host` (default): guests' unaddressed sends reach the host only. `relay`: everyone hears every send. */
|
|
22
22
|
model?: 'host' | 'relay';
|
|
23
|
+
/**
|
|
24
|
+
* When the host role moves on mid-game: `grace` (default) once the host's
|
|
25
|
+
* seat is freed (left, or 3 minutes away), so a host holding hidden state
|
|
26
|
+
* can come back; `drop` as soon as the host drops, for games whose truth is
|
|
27
|
+
* all in `setState`.
|
|
28
|
+
*/
|
|
29
|
+
hostMigration?: 'grace' | 'drop';
|
|
23
30
|
/** Seat this party in the room. */
|
|
24
31
|
partyId?: string;
|
|
25
32
|
}
|
|
@@ -52,6 +59,13 @@ export interface RoomEvents {
|
|
|
52
59
|
ended: { room: RoomInfo; results?: Json };
|
|
53
60
|
/** A player left mid-game (or their reconnect grace ran out). */
|
|
54
61
|
abandoned: { userId: number; reason?: string };
|
|
62
|
+
/**
|
|
63
|
+
* A seat changed: `away` (dropped; held until `graceEndsAt`, so hand it to
|
|
64
|
+
* your computer player), `back` (reclaimed: give control back) or `gone`
|
|
65
|
+
* (freed: left, kicked or the grace ran out). The game never ends on its
|
|
66
|
+
* own because of this; `info.hostId` may have moved to another player.
|
|
67
|
+
*/
|
|
68
|
+
seat: RoomSeatChange;
|
|
55
69
|
}
|
|
56
70
|
|
|
57
71
|
/** Who I am in a room. */
|
|
@@ -84,6 +98,8 @@ export interface HubRoom {
|
|
|
84
98
|
readonly state: RoomState;
|
|
85
99
|
/** Who I am in this room. */
|
|
86
100
|
readonly me: RoomSelf;
|
|
101
|
+
/** The seats as last reported; `connected: false` while a seat is away. */
|
|
102
|
+
seats(): RoomSeat[];
|
|
87
103
|
/** Subscribe to a room event. Returns the unsubscribe function. */
|
|
88
104
|
on<Name extends keyof RoomEvents>(event: Name, listener: (payload: RoomEvents[Name]) => void): () => void;
|
|
89
105
|
/** Mark my seat ready (or not). */
|
|
@@ -114,8 +130,18 @@ export interface RoomListOptions {
|
|
|
114
130
|
mode?: string;
|
|
115
131
|
}
|
|
116
132
|
|
|
133
|
+
/** What feature version 3 added to `hub.rooms`. */
|
|
134
|
+
export interface RoomReclaimMethods {
|
|
135
|
+
/**
|
|
136
|
+
* Take back the seat I still hold in this game (after a reload, from
|
|
137
|
+
* another tab): the room, or `null` when I hold none. The hub also does
|
|
138
|
+
* this on its own at boot, as a `join` launch with `via: 'rejoin'`.
|
|
139
|
+
*/
|
|
140
|
+
reclaim(): Promise<HubRoom | null>;
|
|
141
|
+
}
|
|
142
|
+
|
|
117
143
|
/** `hub.rooms`: hub relay rooms (static games, no server). */
|
|
118
|
-
export interface HubRoomsApi {
|
|
144
|
+
export interface HubRoomsApi extends RoomReclaimMethods {
|
|
119
145
|
/** Create a room and take its first seat. */
|
|
120
146
|
create(options?: RoomCreateOptions): Promise<HubRoom>;
|
|
121
147
|
/** Join a room by its 5-character code or its id. */
|