@diffenderfer-games/hub 2.0.0-next.0 → 2.0.0-next.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +163 -0
- package/LICENSE +21 -21
- package/README.md +203 -139
- package/dist/auto.d.ts +8 -8
- package/dist/auto.js +3 -3
- package/dist/avatar-pixi.d.ts +165 -0
- package/dist/avatar-pixi.d.ts.map +1 -0
- package/dist/avatar-pixi.js +299 -0
- package/dist/avatar-pixi.js.map +1 -0
- package/dist/avatar-svg.d.ts +4 -0
- package/dist/avatar-svg.js +5 -0
- package/dist/avatar-three.d.ts +69 -0
- package/dist/avatar-three.d.ts.map +1 -0
- package/dist/avatar-three.js +49 -0
- package/dist/avatar-three.js.map +1 -0
- package/dist/avatar.d.ts +7 -0
- package/dist/avatar.js +500 -0
- package/dist/avatar.js.map +1 -0
- package/dist/daily.d.ts +2 -2
- package/dist/doctor/cli.mjs +119 -28
- package/dist/doctor/cli.mjs.map +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/index.js +3 -3
- package/dist/input.d.ts +2 -2
- package/dist/input.js +2 -2
- package/dist/mp.d.ts +3 -3
- package/dist/race.d.ts +1 -1
- package/dist/rooms.d.ts +3 -3
- package/dist/rooms.js +1 -1
- package/dist/shared/animator.d.ts +104 -0
- package/dist/shared/animator.d.ts.map +1 -0
- package/dist/shared/art.js +1946 -0
- package/dist/shared/art.js.map +1 -0
- package/dist/shared/atlas.d.ts +138 -0
- package/dist/shared/atlas.d.ts.map +1 -0
- package/dist/shared/avatar.d.ts +123 -0
- package/dist/shared/avatar.d.ts.map +1 -0
- package/dist/shared/bridge.js +18 -4
- package/dist/shared/bridge.js.map +1 -1
- package/dist/shared/client-version.gen.js +1 -1
- package/dist/shared/daily.d.ts +145 -19
- package/dist/shared/daily.d.ts.map +1 -1
- package/dist/shared/daily.js +138 -25
- package/dist/shared/daily.js.map +1 -1
- package/dist/shared/defineProperty.js +39 -0
- package/dist/shared/describe.d.ts +32 -0
- package/dist/shared/describe.d.ts.map +1 -0
- package/dist/shared/describe.js +109 -0
- package/dist/shared/describe.js.map +1 -0
- package/dist/shared/globals.js +9 -42
- package/dist/shared/globals.js.map +1 -1
- package/dist/shared/index.d.ts +1122 -46
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/input.d.ts +74 -14
- package/dist/shared/input.d.ts.map +1 -1
- package/dist/shared/input.js +179 -25
- package/dist/shared/input.js.map +1 -1
- package/dist/shared/json-guards.js +48 -0
- package/dist/shared/json-guards.js.map +1 -0
- package/dist/shared/latest-state.js +1 -1
- package/dist/shared/live-canvas.js +62 -0
- package/dist/shared/live-canvas.js.map +1 -0
- package/dist/shared/mp.js +10 -2
- package/dist/shared/mp.js.map +1 -1
- package/dist/shared/multiplayer-types.d.ts +15 -2
- package/dist/shared/multiplayer-types.d.ts.map +1 -1
- package/dist/shared/overlays.js +800 -0
- package/dist/shared/overlays.js.map +1 -0
- package/dist/shared/page-stack.js +2 -1
- package/dist/shared/page-stack.js.map +1 -1
- package/dist/shared/pose-key.js +249 -0
- package/dist/shared/pose-key.js.map +1 -0
- package/dist/shared/race-types.d.ts +1 -1
- package/dist/shared/race.js +1 -1
- package/dist/shared/room-types.d.ts +28 -3
- package/dist/shared/room-types.d.ts.map +1 -1
- package/dist/shared/rooms.d.ts.map +1 -1
- package/dist/shared/rooms.js +23 -4
- package/dist/shared/rooms.js.map +1 -1
- package/dist/shared/runtime-opener.js +1 -1
- package/dist/shared/scene.d.ts +90 -0
- package/dist/shared/scene.d.ts.map +1 -0
- package/dist/shared/social.d.ts +90 -9
- package/dist/shared/social.d.ts.map +1 -1
- package/dist/shared/social.js +6 -1
- package/dist/shared/social.js.map +1 -1
- package/dist/shared/src.js +4563 -667
- package/dist/shared/src.js.map +1 -1
- package/dist/shared/svg-markup.js +2354 -0
- package/dist/shared/svg-markup.js.map +1 -0
- package/dist/shared/svg.js +27 -0
- package/dist/shared/svg.js.map +1 -0
- package/dist/shared/types.d.ts +174 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/wire-types.d.ts +6 -2
- package/dist/shared/wire-types.d.ts.map +1 -1
- package/dist/social.d.ts +5 -5
- package/dist/social.js +2 -2
- package/dist/turns-rules.d.ts +2 -0
- package/dist/turns-rules.js +1 -0
- package/dist/turns.d.ts +61 -0
- package/dist/turns.d.ts.map +1 -0
- package/dist/turns.js +183 -0
- package/dist/turns.js.map +1 -0
- package/package.json +125 -89
- package/src/auto.ts +39 -39
- package/src/avatar/adapters/pixi-avatar.ts +73 -0
- package/src/avatar/adapters/pixi-crowd.ts +329 -0
- package/src/avatar/adapters/pixi-types.ts +84 -0
- package/src/avatar/adapters/three.ts +126 -0
- package/src/avatar/animator/animator.ts +285 -0
- package/src/avatar/animator/reactions.ts +44 -0
- package/src/avatar/animator/schedule.ts +73 -0
- package/src/avatar/art/beard.ts +49 -0
- package/src/avatar/art/glasses.ts +67 -0
- package/src/avatar/art/hair.ts +254 -0
- package/src/avatar/art/headwear.ts +75 -0
- package/src/avatar/art/index.ts +21 -0
- package/src/avatar/art/shape.ts +87 -0
- package/src/avatar/art-store.ts +36 -0
- package/src/avatar/atlas.ts +246 -0
- package/src/avatar/catalog-view.ts +14 -0
- package/src/avatar/colour.ts +109 -0
- package/src/avatar/describe.ts +79 -0
- package/src/avatar/face.ts +187 -0
- package/src/avatar/geom.ts +173 -0
- package/src/avatar/layout.ts +47 -0
- package/src/avatar/look-like.ts +52 -0
- package/src/avatar/outputs/bitmap.ts +75 -0
- package/src/avatar/outputs/canvas-paint.ts +177 -0
- package/src/avatar/outputs/canvas.ts +62 -0
- package/src/avatar/outputs/custom-element.ts +140 -0
- package/src/avatar/outputs/element.ts +58 -0
- package/src/avatar/outputs/live-canvas.ts +57 -0
- package/src/avatar/outputs/overlay-svg.ts +22 -0
- package/src/avatar/outputs/pose-key.ts +30 -0
- package/src/avatar/outputs/svg-markup.ts +113 -0
- package/src/avatar/outputs/svg.ts +41 -0
- package/src/avatar/overlays.ts +128 -0
- package/src/avatar/render/body.ts +116 -0
- package/src/avatar/render/colours.ts +81 -0
- package/src/avatar/render/compose.ts +320 -0
- package/src/avatar/render/context.ts +117 -0
- package/src/avatar/render/eyes.ts +78 -0
- package/src/avatar/render/eyewear.ts +118 -0
- package/src/avatar/render/frame.ts +73 -0
- package/src/avatar/render/head.ts +81 -0
- package/src/avatar/render/mouth.ts +86 -0
- package/src/avatar/render/props.ts +91 -0
- package/src/avatar/render/types.ts +93 -0
- package/src/avatar/render/walker-parts.ts +121 -0
- package/src/avatar/render/walker.ts +199 -0
- package/src/avatar/scene.ts +119 -0
- package/src/avatar-pixi.ts +20 -0
- package/src/avatar-svg.ts +21 -0
- package/src/avatar-three.ts +18 -0
- package/src/avatar.ts +63 -0
- package/src/core/avatar/avatar-cache.ts +95 -0
- package/src/core/avatar/avatar-ref.ts +34 -0
- package/src/core/avatar/avatar-types.ts +43 -0
- package/src/core/avatar/hub-avatar.ts +308 -0
- package/src/core/connection.ts +408 -299
- package/src/core/errors.ts +160 -147
- package/src/core/game-data.ts +40 -38
- package/src/core/hub-economy.ts +216 -179
- package/src/core/hub-progress.ts +269 -205
- package/src/core/hub.ts +352 -131
- package/src/core/menu-control.ts +61 -61
- package/src/core/offline/cloud-saves.ts +62 -0
- package/src/core/offline/events.ts +50 -50
- package/src/core/offline/favorites.ts +21 -21
- package/src/core/offline/hub-kinds/avatar-kind.ts +42 -0
- package/src/core/offline/hub-kinds/daily-kind.ts +68 -0
- package/src/core/offline/hub-kinds/economy-kinds.ts +86 -0
- package/src/core/offline/hub-kinds/game-fields.ts +2 -0
- package/src/core/offline/hub-kinds/index.ts +52 -0
- package/src/core/offline/hub-kinds/legacy-ops.ts +37 -0
- package/src/core/offline/hub-kinds/progress-kinds.ts +165 -0
- package/src/core/offline/hub-kinds/resume-kind.ts +108 -0
- package/src/core/offline/hub-kinds/saved-kind.ts +93 -0
- package/src/core/offline/kind-adapters.ts +48 -0
- package/src/core/offline/local-inventory.ts +64 -0
- package/src/core/offline/local-store.ts +304 -261
- package/src/core/offline/offline-api.ts +65 -0
- package/src/core/offline/outbox-entry.ts +59 -0
- package/src/core/offline/outbox-kinds.ts +78 -0
- package/src/core/offline/outbox-merge.ts +39 -0
- package/src/core/offline/outbox-replayer.ts +141 -0
- package/src/core/offline/outbox-store.ts +85 -0
- package/src/core/offline/outbox.ts +232 -0
- package/src/core/offline/queue.ts +126 -171
- package/src/core/offline/replay-lease.ts +87 -0
- package/src/core/offline/replay-policy.ts +52 -0
- package/src/core/page-account.ts +121 -0
- package/src/core/page.ts +87 -74
- package/src/core/rest/endpoint-types.ts +22 -22
- package/src/core/rest/rest-client.ts +163 -152
- package/src/core/rest/routes.gen.ts +144 -88
- package/src/core/resume/hub-resume.ts +286 -0
- package/src/core/resume/local-resume.ts +130 -0
- package/src/core/resume/resume-list.ts +65 -0
- package/src/core/resume/resume-request.ts +52 -0
- package/src/core/resume/resume-types.ts +64 -0
- package/src/core/saved/daily-saves.ts +196 -0
- package/src/core/saved/daily-seed.ts +16 -0
- package/src/core/saved/hub-saved.ts +221 -0
- package/src/core/saved/local-saved.ts +122 -0
- package/src/core/saved/saved-list.ts +129 -0
- package/src/core/saved/saved-open.ts +104 -0
- package/src/core/saved/saved-order.ts +49 -0
- package/src/core/saved/saved-types.ts +24 -0
- package/src/core/storage/json-guards.ts +80 -74
- package/src/core/storage/key-value.ts +66 -66
- package/src/core/ticket.ts +43 -43
- package/src/core/turns/hub-turns.ts +161 -0
- package/src/core/turns/turn-game.ts +510 -0
- package/src/core/turns/your-turn-buzz.ts +69 -0
- package/src/core/types.ts +134 -134
- package/src/core/version/client-version.gen.ts +2 -2
- package/src/core/version/hub-version.ts +62 -62
- package/src/core/version/semver.ts +97 -97
- package/src/daily/daily-system.ts +276 -161
- package/src/daily/page-daily.ts +36 -36
- package/src/daily/types.ts +105 -67
- package/src/daily.ts +10 -10
- package/src/doctor/checks.ts +86 -76
- package/src/doctor/cli.ts +101 -106
- package/src/doctor/hub-version-fetch.ts +43 -0
- package/src/doctor/tsconfig.json +12 -12
- package/src/doctor/world-check.ts +61 -0
- package/src/globals.ts +115 -110
- package/src/index.ts +68 -50
- package/src/input/device-state.ts +146 -146
- package/src/input/edges.ts +55 -55
- package/src/input/focus-ring.ts +13 -0
- package/src/input/gamepad.ts +61 -61
- package/src/input/input-events.ts +32 -32
- package/src/input/input-registry.ts +171 -164
- package/src/input/input-system.ts +406 -393
- package/src/input/legacy-controls.ts +64 -64
- package/src/input/modality.ts +122 -0
- package/src/input/nav-state.ts +200 -191
- package/src/input/navigator.ts +182 -171
- package/src/input/on-screen-controls.ts +101 -101
- package/src/input/page-control.ts +43 -43
- package/src/input/page-input.ts +193 -193
- package/src/input/player-remaps.ts +77 -77
- package/src/input/source-values.ts +116 -116
- package/src/input/spatial.ts +47 -47
- package/src/input/types.ts +331 -331
- package/src/input/zero-config.ts +164 -177
- package/src/input.ts +26 -22
- package/src/mp.ts +10 -10
- package/src/overlay/controls-panel.ts +163 -163
- package/src/overlay/dom.ts +46 -46
- package/src/overlay/layout-clusters.ts +162 -162
- package/src/overlay/layout-geometry.ts +136 -136
- package/src/overlay/layout.ts +203 -203
- package/src/overlay/listener-scope.ts +23 -23
- package/src/overlay/on-screen-keyboard.ts +137 -137
- package/src/overlay/overlay.ts +234 -208
- package/src/overlay/styles.ts +13 -12
- package/src/overlay/virtual-controls.ts +262 -262
- package/src/race.ts +10 -10
- package/src/realtime/bridge.ts +126 -121
- package/src/realtime/cross-bundle.ts +29 -29
- package/src/realtime/fallback-notice.ts +48 -48
- package/src/realtime/hub-link.ts +65 -65
- package/src/realtime/latest-state.ts +33 -33
- package/src/realtime/multiplayer-forwarder.ts +40 -28
- package/src/realtime/multiplayer-types.ts +66 -52
- package/src/realtime/page-runtime.ts +60 -60
- package/src/realtime/race-forwarder.ts +59 -59
- package/src/realtime/race-types.ts +132 -132
- package/src/realtime/room-types.ts +151 -125
- package/src/realtime/rooms-forwarder.ts +104 -79
- package/src/realtime/runtime-api.ts +74 -49
- package/src/realtime/runtime-opener.ts +61 -61
- package/src/realtime/social-forwarder.ts +77 -72
- package/src/realtime/social-types.ts +86 -78
- package/src/realtime/types.ts +54 -53
- package/src/realtime/wire-types.ts +79 -72
- package/src/realtime/world-types.ts +20 -0
- package/src/rooms.ts +10 -10
- package/src/social.ts +19 -18
- package/src/turns-kit/check-rules.ts +281 -0
- package/src/turns-rules.ts +12 -0
- package/src/turns.ts +14 -0
- package/src/types.ts +13 -13
- package/src/uistack/page-stack.ts +45 -45
- package/src/uistack/play-suspension.ts +82 -82
- package/src/uistack/types.ts +76 -76
- package/src/uistack/ui-stack.ts +144 -144
|
@@ -1,137 +1,137 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The on-screen keyboard for text fields, usable by touch, mouse or gamepad
|
|
3
|
-
* (the stick or d-pad moves between keys, A presses, B or Start closes).
|
|
4
|
-
*/
|
|
5
|
-
import { RepeatingEdges, RisingEdges } from '../input/edges';
|
|
6
|
-
import { isAnyPadButtonPressed, PAD_BUTTON, readGamepads } from '../input/gamepad';
|
|
7
|
-
import type { Direction } from '../input/gamepad';
|
|
8
|
-
import { findNearestInDirection, rectOf } from '../input/spatial';
|
|
9
|
-
import { findElements } from './dom';
|
|
10
|
-
|
|
11
|
-
const LETTER_ROWS = ['1234567890', 'qwertyuiop', 'asdfghjkl', 'zxcvbnm'];
|
|
12
|
-
const STICK_THRESHOLD = 0.5;
|
|
13
|
-
|
|
14
|
-
/** One key's markup. */
|
|
15
|
-
function keyHtml(label: string, key: string, extraClass = ''): string {
|
|
16
|
-
return `<button class="k ${extraClass}" data-k="${key}">${label}</button>`;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const KEYBOARD_HTML = `<div class="kb-rows">${LETTER_ROWS.map((row) => `<div class="kb-row">${Array.from(row).map((character) => keyHtml(character, character)).join('')}</div>`).join('')}`
|
|
20
|
-
+ `<div class="kb-row">${keyHtml('⇧', 'shift', 'wide')}${keyHtml('space', ' ', 'space')}${keyHtml('⌫', 'back', 'wide')}${keyHtml('↵', 'enter', 'wide')}${keyHtml('✕', 'close', 'wide')}</div></div>`;
|
|
21
|
-
|
|
22
|
-
/** The direction the pads point, from the summed stick and d-pad. */
|
|
23
|
-
function heldDirection(pads: readonly Gamepad[]): Direction | null {
|
|
24
|
-
let horizontal = 0;
|
|
25
|
-
let vertical = 0;
|
|
26
|
-
for (const pad of pads) {
|
|
27
|
-
horizontal += (pad.axes[0] ?? 0) - Number(pad.buttons[PAD_BUTTON.LEFT]?.pressed === true) + Number(pad.buttons[PAD_BUTTON.RIGHT]?.pressed === true);
|
|
28
|
-
vertical += (pad.axes[1] ?? 0) - Number(pad.buttons[PAD_BUTTON.UP]?.pressed === true) + Number(pad.buttons[PAD_BUTTON.DOWN]?.pressed === true);
|
|
29
|
-
}
|
|
30
|
-
if (Math.abs(horizontal) <= STICK_THRESHOLD && Math.abs(vertical) <= STICK_THRESHOLD) {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
if (Math.abs(horizontal) > Math.abs(vertical)) {
|
|
34
|
-
return horizontal > 0 ? 'right' : 'left';
|
|
35
|
-
}
|
|
36
|
-
return vertical > 0 ? 'down' : 'up';
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** The on-screen keyboard (one at a time, inside the overlay's shadow root). */
|
|
40
|
-
export class OnScreenKeyboard {
|
|
41
|
-
private readonly root: ShadowRoot;
|
|
42
|
-
private panel: HTMLDivElement | null = null;
|
|
43
|
-
private target: HTMLInputElement | HTMLTextAreaElement | null = null;
|
|
44
|
-
private keys: HTMLElement[] = [];
|
|
45
|
-
private focusedIndex = 0;
|
|
46
|
-
private isShifted = false;
|
|
47
|
-
private readonly repeats = new RepeatingEdges();
|
|
48
|
-
private readonly presses = new RisingEdges();
|
|
49
|
-
|
|
50
|
-
constructor(root: ShadowRoot) {
|
|
51
|
-
this.root = root;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Whether the keyboard is showing. */
|
|
55
|
-
get isOpen(): boolean {
|
|
56
|
-
return this.panel !== null;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Open it for `target` (replacing any open keyboard). */
|
|
60
|
-
open(target: HTMLInputElement | HTMLTextAreaElement | null): void {
|
|
61
|
-
this.close();
|
|
62
|
-
this.target = target;
|
|
63
|
-
this.isShifted = false;
|
|
64
|
-
this.focusedIndex = 0;
|
|
65
|
-
const panel = document.createElement('div');
|
|
66
|
-
panel.className = 'kb';
|
|
67
|
-
panel.innerHTML = KEYBOARD_HTML;
|
|
68
|
-
this.root.appendChild(panel);
|
|
69
|
-
this.panel = panel;
|
|
70
|
-
this.keys = findElements(panel, '.k', HTMLElement);
|
|
71
|
-
for (const key of this.keys) {
|
|
72
|
-
const press = (event: Event): void => {
|
|
73
|
-
event.preventDefault();
|
|
74
|
-
this.press(key.dataset['k'] ?? '');
|
|
75
|
-
};
|
|
76
|
-
key.addEventListener('mousedown', press);
|
|
77
|
-
key.addEventListener('touchstart', press, { passive: false });
|
|
78
|
-
}
|
|
79
|
-
this.highlightFocusedKey();
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** Close it. */
|
|
83
|
-
close(): void {
|
|
84
|
-
this.panel?.remove();
|
|
85
|
-
this.panel = null;
|
|
86
|
-
this.target = null;
|
|
87
|
-
this.keys = [];
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** Called every frame while open: gamepad navigation. */
|
|
91
|
-
pollGamepads(): void {
|
|
92
|
-
if (this.panel === null) {
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
const pads = readGamepads();
|
|
96
|
-
const direction = heldDirection(pads);
|
|
97
|
-
if (this.repeats.fires('move', direction !== null) && direction !== null) {
|
|
98
|
-
this.moveFocus(direction);
|
|
99
|
-
}
|
|
100
|
-
if (this.presses.rose('act', isAnyPadButtonPressed(pads, PAD_BUTTON.A))) {
|
|
101
|
-
this.press(this.keys[this.focusedIndex]?.dataset['k'] ?? '');
|
|
102
|
-
}
|
|
103
|
-
const isClosing = isAnyPadButtonPressed(pads, PAD_BUTTON.B) || isAnyPadButtonPressed(pads, PAD_BUTTON.START);
|
|
104
|
-
if (this.presses.rose('close', isClosing)) {
|
|
105
|
-
this.close();
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
private press(key: string): void {
|
|
110
|
-
if (key === 'close' || key === 'enter') {
|
|
111
|
-
this.close();
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
if (key === 'shift') {
|
|
115
|
-
this.isShifted = !this.isShifted;
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
const target = this.target;
|
|
119
|
-
if (target === null || key === '') {
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
target.value = key === 'back' ? target.value.slice(0, -1) : target.value + (this.isShifted ? key.toUpperCase() : key);
|
|
123
|
-
target.dispatchEvent(new Event('input', { bubbles: true }));
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
private moveFocus(direction: Direction): void {
|
|
127
|
-
const next = findNearestInDirection(this.keys.map(rectOf), this.focusedIndex, direction);
|
|
128
|
-
if (next >= 0) {
|
|
129
|
-
this.focusedIndex = next;
|
|
130
|
-
this.highlightFocusedKey();
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
private highlightFocusedKey(): void {
|
|
135
|
-
this.keys.forEach((key, index) => key.classList.toggle('kfocus', index === this.focusedIndex));
|
|
136
|
-
}
|
|
137
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* The on-screen keyboard for text fields, usable by touch, mouse or gamepad
|
|
3
|
+
* (the stick or d-pad moves between keys, A presses, B or Start closes).
|
|
4
|
+
*/
|
|
5
|
+
import { RepeatingEdges, RisingEdges } from '../input/edges';
|
|
6
|
+
import { isAnyPadButtonPressed, PAD_BUTTON, readGamepads } from '../input/gamepad';
|
|
7
|
+
import type { Direction } from '../input/gamepad';
|
|
8
|
+
import { findNearestInDirection, rectOf } from '../input/spatial';
|
|
9
|
+
import { findElements } from './dom';
|
|
10
|
+
|
|
11
|
+
const LETTER_ROWS = ['1234567890', 'qwertyuiop', 'asdfghjkl', 'zxcvbnm'];
|
|
12
|
+
const STICK_THRESHOLD = 0.5;
|
|
13
|
+
|
|
14
|
+
/** One key's markup. */
|
|
15
|
+
function keyHtml(label: string, key: string, extraClass = ''): string {
|
|
16
|
+
return `<button class="k ${extraClass}" data-k="${key}">${label}</button>`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const KEYBOARD_HTML = `<div class="kb-rows">${LETTER_ROWS.map((row) => `<div class="kb-row">${Array.from(row).map((character) => keyHtml(character, character)).join('')}</div>`).join('')}`
|
|
20
|
+
+ `<div class="kb-row">${keyHtml('⇧', 'shift', 'wide')}${keyHtml('space', ' ', 'space')}${keyHtml('⌫', 'back', 'wide')}${keyHtml('↵', 'enter', 'wide')}${keyHtml('✕', 'close', 'wide')}</div></div>`;
|
|
21
|
+
|
|
22
|
+
/** The direction the pads point, from the summed stick and d-pad. */
|
|
23
|
+
function heldDirection(pads: readonly Gamepad[]): Direction | null {
|
|
24
|
+
let horizontal = 0;
|
|
25
|
+
let vertical = 0;
|
|
26
|
+
for (const pad of pads) {
|
|
27
|
+
horizontal += (pad.axes[0] ?? 0) - Number(pad.buttons[PAD_BUTTON.LEFT]?.pressed === true) + Number(pad.buttons[PAD_BUTTON.RIGHT]?.pressed === true);
|
|
28
|
+
vertical += (pad.axes[1] ?? 0) - Number(pad.buttons[PAD_BUTTON.UP]?.pressed === true) + Number(pad.buttons[PAD_BUTTON.DOWN]?.pressed === true);
|
|
29
|
+
}
|
|
30
|
+
if (Math.abs(horizontal) <= STICK_THRESHOLD && Math.abs(vertical) <= STICK_THRESHOLD) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
if (Math.abs(horizontal) > Math.abs(vertical)) {
|
|
34
|
+
return horizontal > 0 ? 'right' : 'left';
|
|
35
|
+
}
|
|
36
|
+
return vertical > 0 ? 'down' : 'up';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The on-screen keyboard (one at a time, inside the overlay's shadow root). */
|
|
40
|
+
export class OnScreenKeyboard {
|
|
41
|
+
private readonly root: ShadowRoot;
|
|
42
|
+
private panel: HTMLDivElement | null = null;
|
|
43
|
+
private target: HTMLInputElement | HTMLTextAreaElement | null = null;
|
|
44
|
+
private keys: HTMLElement[] = [];
|
|
45
|
+
private focusedIndex = 0;
|
|
46
|
+
private isShifted = false;
|
|
47
|
+
private readonly repeats = new RepeatingEdges();
|
|
48
|
+
private readonly presses = new RisingEdges();
|
|
49
|
+
|
|
50
|
+
constructor(root: ShadowRoot) {
|
|
51
|
+
this.root = root;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Whether the keyboard is showing. */
|
|
55
|
+
get isOpen(): boolean {
|
|
56
|
+
return this.panel !== null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Open it for `target` (replacing any open keyboard). */
|
|
60
|
+
open(target: HTMLInputElement | HTMLTextAreaElement | null): void {
|
|
61
|
+
this.close();
|
|
62
|
+
this.target = target;
|
|
63
|
+
this.isShifted = false;
|
|
64
|
+
this.focusedIndex = 0;
|
|
65
|
+
const panel = document.createElement('div');
|
|
66
|
+
panel.className = 'kb';
|
|
67
|
+
panel.innerHTML = KEYBOARD_HTML;
|
|
68
|
+
this.root.appendChild(panel);
|
|
69
|
+
this.panel = panel;
|
|
70
|
+
this.keys = findElements(panel, '.k', HTMLElement);
|
|
71
|
+
for (const key of this.keys) {
|
|
72
|
+
const press = (event: Event): void => {
|
|
73
|
+
event.preventDefault();
|
|
74
|
+
this.press(key.dataset['k'] ?? '');
|
|
75
|
+
};
|
|
76
|
+
key.addEventListener('mousedown', press);
|
|
77
|
+
key.addEventListener('touchstart', press, { passive: false });
|
|
78
|
+
}
|
|
79
|
+
this.highlightFocusedKey();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Close it. */
|
|
83
|
+
close(): void {
|
|
84
|
+
this.panel?.remove();
|
|
85
|
+
this.panel = null;
|
|
86
|
+
this.target = null;
|
|
87
|
+
this.keys = [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Called every frame while open: gamepad navigation. */
|
|
91
|
+
pollGamepads(): void {
|
|
92
|
+
if (this.panel === null) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const pads = readGamepads();
|
|
96
|
+
const direction = heldDirection(pads);
|
|
97
|
+
if (this.repeats.fires('move', direction !== null) && direction !== null) {
|
|
98
|
+
this.moveFocus(direction);
|
|
99
|
+
}
|
|
100
|
+
if (this.presses.rose('act', isAnyPadButtonPressed(pads, PAD_BUTTON.A))) {
|
|
101
|
+
this.press(this.keys[this.focusedIndex]?.dataset['k'] ?? '');
|
|
102
|
+
}
|
|
103
|
+
const isClosing = isAnyPadButtonPressed(pads, PAD_BUTTON.B) || isAnyPadButtonPressed(pads, PAD_BUTTON.START);
|
|
104
|
+
if (this.presses.rose('close', isClosing)) {
|
|
105
|
+
this.close();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private press(key: string): void {
|
|
110
|
+
if (key === 'close' || key === 'enter') {
|
|
111
|
+
this.close();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (key === 'shift') {
|
|
115
|
+
this.isShifted = !this.isShifted;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const target = this.target;
|
|
119
|
+
if (target === null || key === '') {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
target.value = key === 'back' ? target.value.slice(0, -1) : target.value + (this.isShifted ? key.toUpperCase() : key);
|
|
123
|
+
target.dispatchEvent(new Event('input', { bubbles: true }));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private moveFocus(direction: Direction): void {
|
|
127
|
+
const next = findNearestInDirection(this.keys.map(rectOf), this.focusedIndex, direction);
|
|
128
|
+
if (next >= 0) {
|
|
129
|
+
this.focusedIndex = next;
|
|
130
|
+
this.highlightFocusedKey();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private highlightFocusedKey(): void {
|
|
135
|
+
this.keys.forEach((key, index) => key.classList.toggle('kfocus', index === this.focusedIndex));
|
|
136
|
+
}
|
|
137
|
+
}
|