@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,262 +1,262 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Building one on-screen control (button, tap region, joystick or dpad) and
|
|
3
|
-
* wiring its touch and mouse handling, plus drag-to-move while the player
|
|
4
|
-
* edits the layout.
|
|
5
|
-
*/
|
|
6
|
-
import type { VirtualDef } from '../input/types';
|
|
7
|
-
import { placeAt, requireElement } from './dom';
|
|
8
|
-
import { declaredSize, isStickType } from './layout-geometry';
|
|
9
|
-
import type { ListenerScope } from './listener-scope';
|
|
10
|
-
import { ACCENT_COLOR } from './styles';
|
|
11
|
-
|
|
12
|
-
/** What a control reports. */
|
|
13
|
-
export interface VirtualControlCallbacks {
|
|
14
|
-
/** A button went down. */
|
|
15
|
-
onButtonDown(id: string): void;
|
|
16
|
-
/** A button came up. */
|
|
17
|
-
onButtonUp(id: string): void;
|
|
18
|
-
/** A stick moved (-1..1 per axis; 0, 0 on release). */
|
|
19
|
-
onStick(id: string, outX: number, outY: number): void;
|
|
20
|
-
/** A tap region was tapped. */
|
|
21
|
-
onTap(): void;
|
|
22
|
-
/** The player dropped a dragged control: its top-left corner as % of the viewport. */
|
|
23
|
-
onDragged(id: string, spot: { x: number; y: number }): void;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** What controls need from the overlay. */
|
|
27
|
-
export interface VirtualControlEnvironment {
|
|
28
|
-
readonly callbacks: VirtualControlCallbacks;
|
|
29
|
-
/** Window listeners, removed when the controls are rebuilt. */
|
|
30
|
-
readonly scope: ListenerScope;
|
|
31
|
-
/** Whether the player is editing the layout (controls drag instead of acting). */
|
|
32
|
-
isEditing(): boolean;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** A dpad snaps past this deflection. */
|
|
36
|
-
const DPAD_THRESHOLD = 0.4;
|
|
37
|
-
|
|
38
|
-
const RADIUS_BY_SHAPE = { circle: '50%', pill: '999px', square: '0', round: '16px' } as const;
|
|
39
|
-
|
|
40
|
-
/** The first touch of a touch event. */
|
|
41
|
-
function firstTouch(event: TouchEvent): Touch | undefined {
|
|
42
|
-
return event.changedTouches[0];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function bindButton(id: string, element: HTMLElement, environment: VirtualControlEnvironment): void {
|
|
46
|
-
const { callbacks } = environment;
|
|
47
|
-
const isTapRegion = element.classList.contains('vc-transparent');
|
|
48
|
-
const press = (event: Event): void => {
|
|
49
|
-
if (environment.isEditing()) {
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
event.preventDefault();
|
|
53
|
-
element.classList.add('pressed');
|
|
54
|
-
callbacks.onButtonDown(id);
|
|
55
|
-
if (isTapRegion) {
|
|
56
|
-
callbacks.onTap();
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
const release = (event: Event): void => {
|
|
60
|
-
event.preventDefault();
|
|
61
|
-
element.classList.remove('pressed');
|
|
62
|
-
callbacks.onButtonUp(id);
|
|
63
|
-
};
|
|
64
|
-
element.addEventListener('touchstart', press, { passive: false });
|
|
65
|
-
element.addEventListener('touchend', release, { passive: false });
|
|
66
|
-
element.addEventListener('touchcancel', release, { passive: false });
|
|
67
|
-
element.addEventListener('mousedown', press);
|
|
68
|
-
environment.scope.listenWindow('mouseup', release);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function bindStick(id: string, wrapper: HTMLElement, knob: HTMLElement, definition: VirtualDef, environment: VirtualControlEnvironment): void {
|
|
72
|
-
const isDpad = definition.type === 'dpad';
|
|
73
|
-
const axisMode = definition.axis ?? 'both';
|
|
74
|
-
const { callbacks } = environment;
|
|
75
|
-
let activeTouchId = -1;
|
|
76
|
-
let centreX = 0;
|
|
77
|
-
let centreY = 0;
|
|
78
|
-
let travelX = 1;
|
|
79
|
-
let travelY = 1;
|
|
80
|
-
const measure = (): void => {
|
|
81
|
-
const bounds = wrapper.getBoundingClientRect();
|
|
82
|
-
centreX = bounds.left + bounds.width / 2;
|
|
83
|
-
centreY = bounds.top + bounds.height / 2;
|
|
84
|
-
const knobRadius = knob.getBoundingClientRect().width / 2 || Math.min(bounds.width, bounds.height) * 0.25;
|
|
85
|
-
travelX = Math.max(1, bounds.width / 2 - knobRadius);
|
|
86
|
-
travelY = Math.max(1, bounds.height / 2 - knobRadius);
|
|
87
|
-
};
|
|
88
|
-
const moveTo = (pointerX: number, pointerY: number): void => {
|
|
89
|
-
let dx = axisMode === 'y' ? 0 : pointerX - centreX;
|
|
90
|
-
let dy = axisMode === 'x' ? 0 : pointerY - centreY;
|
|
91
|
-
if (axisMode === 'both') {
|
|
92
|
-
const limit = Math.min(travelX, travelY);
|
|
93
|
-
const distance = Math.hypot(dx, dy);
|
|
94
|
-
if (distance > limit) {
|
|
95
|
-
dx = (dx * limit) / distance;
|
|
96
|
-
dy = (dy * limit) / distance;
|
|
97
|
-
}
|
|
98
|
-
} else {
|
|
99
|
-
dx = Math.max(-travelX, Math.min(travelX, dx));
|
|
100
|
-
dy = Math.max(-travelY, Math.min(travelY, dy));
|
|
101
|
-
}
|
|
102
|
-
let outX = dx / travelX;
|
|
103
|
-
let outY = dy / travelY;
|
|
104
|
-
if (isDpad) {
|
|
105
|
-
outX = Math.abs(outX) > DPAD_THRESHOLD ? Math.sign(outX) : 0;
|
|
106
|
-
outY = Math.abs(outY) > DPAD_THRESHOLD ? Math.sign(outY) : 0;
|
|
107
|
-
}
|
|
108
|
-
knob.style.transform = `translate(${String(dx)}px, ${String(dy)}px)`;
|
|
109
|
-
callbacks.onStick(id, outX, outY);
|
|
110
|
-
};
|
|
111
|
-
const release = (): void => {
|
|
112
|
-
activeTouchId = -1;
|
|
113
|
-
knob.style.transform = 'translate(0,0)';
|
|
114
|
-
callbacks.onStick(id, 0, 0);
|
|
115
|
-
};
|
|
116
|
-
wrapper.addEventListener('touchstart', (event) => {
|
|
117
|
-
const touch = firstTouch(event);
|
|
118
|
-
if (environment.isEditing() || touch === undefined) {
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
event.preventDefault();
|
|
122
|
-
measure();
|
|
123
|
-
activeTouchId = touch.identifier;
|
|
124
|
-
moveTo(touch.clientX, touch.clientY);
|
|
125
|
-
}, { passive: false });
|
|
126
|
-
wrapper.addEventListener('touchmove', (event) => {
|
|
127
|
-
event.preventDefault();
|
|
128
|
-
for (const touch of event.changedTouches) {
|
|
129
|
-
if (touch.identifier === activeTouchId) {
|
|
130
|
-
moveTo(touch.clientX, touch.clientY);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}, { passive: false });
|
|
134
|
-
wrapper.addEventListener('touchend', release, { passive: false });
|
|
135
|
-
wrapper.addEventListener('touchcancel', release, { passive: false });
|
|
136
|
-
// Mouse drag (desktop testing).
|
|
137
|
-
let isMouseHeld = false;
|
|
138
|
-
wrapper.addEventListener('mousedown', (event) => {
|
|
139
|
-
if (environment.isEditing()) {
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
measure();
|
|
143
|
-
isMouseHeld = true;
|
|
144
|
-
moveTo(event.clientX, event.clientY);
|
|
145
|
-
});
|
|
146
|
-
environment.scope.listenWindow('mousemove', (event) => {
|
|
147
|
-
if (isMouseHeld) {
|
|
148
|
-
moveTo(event.clientX, event.clientY);
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
environment.scope.listenWindow('mouseup', () => {
|
|
152
|
-
if (isMouseHeld) {
|
|
153
|
-
isMouseHeld = false;
|
|
154
|
-
release();
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/** Drag-to-move while editing: the control keeps its grab point under the finger and stays fully on-screen. */
|
|
160
|
-
function bindEditDrag(id: string, element: HTMLElement, environment: VirtualControlEnvironment): void {
|
|
161
|
-
let isDragging = false;
|
|
162
|
-
let grabX = 0;
|
|
163
|
-
let grabY = 0;
|
|
164
|
-
let droppedSpot: { x: number; y: number } | null = null;
|
|
165
|
-
const start = (pointerX: number, pointerY: number): void => {
|
|
166
|
-
if (!environment.isEditing()) {
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
isDragging = true;
|
|
170
|
-
const bounds = element.getBoundingClientRect();
|
|
171
|
-
grabX = pointerX - bounds.left;
|
|
172
|
-
grabY = pointerY - bounds.top;
|
|
173
|
-
droppedSpot = null;
|
|
174
|
-
};
|
|
175
|
-
const move = (pointerX: number, pointerY: number): void => {
|
|
176
|
-
if (!isDragging) {
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
const viewportWidth = window.innerWidth;
|
|
180
|
-
const viewportHeight = window.innerHeight;
|
|
181
|
-
const left = Math.max(0, Math.min(viewportWidth - element.offsetWidth, pointerX - grabX));
|
|
182
|
-
const top = Math.max(0, Math.min(viewportHeight - element.offsetHeight, pointerY - grabY));
|
|
183
|
-
placeAt(element, left, top);
|
|
184
|
-
droppedSpot = { x: (left / viewportWidth) * 100, y: (top / viewportHeight) * 100 };
|
|
185
|
-
};
|
|
186
|
-
const end = (): void => {
|
|
187
|
-
if (!isDragging) {
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
isDragging = false;
|
|
191
|
-
if (droppedSpot !== null) {
|
|
192
|
-
environment.callbacks.onDragged(id, droppedSpot);
|
|
193
|
-
}
|
|
194
|
-
};
|
|
195
|
-
element.addEventListener('mousedown', (event) => {
|
|
196
|
-
start(event.clientX, event.clientY);
|
|
197
|
-
});
|
|
198
|
-
element.addEventListener('touchstart', (event) => {
|
|
199
|
-
const touch = firstTouch(event);
|
|
200
|
-
if (touch !== undefined) {
|
|
201
|
-
start(touch.clientX, touch.clientY);
|
|
202
|
-
}
|
|
203
|
-
}, { passive: true });
|
|
204
|
-
environment.scope.listenWindow('mousemove', (event) => {
|
|
205
|
-
move(event.clientX, event.clientY);
|
|
206
|
-
});
|
|
207
|
-
environment.scope.listenWindow('touchmove', (event) => {
|
|
208
|
-
const touch = firstTouch(event);
|
|
209
|
-
if (isDragging && touch !== undefined) {
|
|
210
|
-
move(touch.clientX, touch.clientY);
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
environment.scope.listenWindow('mouseup', end);
|
|
214
|
-
environment.scope.listenWindow('touchend', end);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/** Build one on-screen control element, wired up. */
|
|
218
|
-
export function createVirtualControl(definition: VirtualDef, environment: VirtualControlEnvironment): HTMLElement {
|
|
219
|
-
const isStick = isStickType(definition.type);
|
|
220
|
-
const { w: width, h: height } = declaredSize(definition);
|
|
221
|
-
const wrapper = document.createElement('div');
|
|
222
|
-
wrapper.className = `vc vc-${definition.type}`;
|
|
223
|
-
wrapper.dataset['controlId'] = definition.id;
|
|
224
|
-
// The v1 expando tests and tools read the control id from.
|
|
225
|
-
Reflect.set(wrapper, '__id', definition.id);
|
|
226
|
-
wrapper.style.width = `${String(width)}px`;
|
|
227
|
-
wrapper.style.height = `${String(height)}px`;
|
|
228
|
-
if (definition.opacity !== undefined) {
|
|
229
|
-
wrapper.style.opacity = String(definition.opacity);
|
|
230
|
-
}
|
|
231
|
-
wrapper.style.setProperty('--vc-color', definition.color ?? ACCENT_COLOR);
|
|
232
|
-
if (definition.bg !== undefined) {
|
|
233
|
-
wrapper.style.setProperty('--vc-bg', definition.bg);
|
|
234
|
-
}
|
|
235
|
-
if (definition.text !== undefined) {
|
|
236
|
-
wrapper.style.color = definition.text;
|
|
237
|
-
}
|
|
238
|
-
const radius = RADIUS_BY_SHAPE[definition.shape ?? (isStick ? 'circle' : 'round')];
|
|
239
|
-
wrapper.style.borderRadius = radius;
|
|
240
|
-
if (isStick) {
|
|
241
|
-
wrapper.innerHTML = '<div class="base"></div><div class="knob"></div>';
|
|
242
|
-
requireElement(wrapper, '.base', HTMLElement).style.borderRadius = radius;
|
|
243
|
-
// The knob stays a circle sized to the short edge (a wide pill stick gets a
|
|
244
|
-
// round knob that slides). Single-axis sticks get a knob that mostly fills
|
|
245
|
-
// the short edge; 2D sticks a smaller one with room to travel.
|
|
246
|
-
const knob = requireElement(wrapper, '.knob', HTMLElement);
|
|
247
|
-
const isSingleAxis = definition.axis === 'x' || definition.axis === 'y';
|
|
248
|
-
const knobDiameter = Math.round(Math.min(width, height) * (isSingleAxis ? 0.92 : 0.5));
|
|
249
|
-
knob.style.width = `${String(knobDiameter)}px`;
|
|
250
|
-
knob.style.height = `${String(knobDiameter)}px`;
|
|
251
|
-
bindStick(definition.id, wrapper, knob, definition, environment);
|
|
252
|
-
} else {
|
|
253
|
-
// The game's own markup (an SVG icon, an entity in a label), inserted as given.
|
|
254
|
-
wrapper.innerHTML = definition.html ?? `<span>${definition.label ?? ''}</span>`;
|
|
255
|
-
if (definition.type === 'tap') {
|
|
256
|
-
wrapper.classList.add('vc-transparent');
|
|
257
|
-
}
|
|
258
|
-
bindButton(definition.id, wrapper, environment);
|
|
259
|
-
}
|
|
260
|
-
bindEditDrag(definition.id, wrapper, environment);
|
|
261
|
-
return wrapper;
|
|
262
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Building one on-screen control (button, tap region, joystick or dpad) and
|
|
3
|
+
* wiring its touch and mouse handling, plus drag-to-move while the player
|
|
4
|
+
* edits the layout.
|
|
5
|
+
*/
|
|
6
|
+
import type { VirtualDef } from '../input/types';
|
|
7
|
+
import { placeAt, requireElement } from './dom';
|
|
8
|
+
import { declaredSize, isStickType } from './layout-geometry';
|
|
9
|
+
import type { ListenerScope } from './listener-scope';
|
|
10
|
+
import { ACCENT_COLOR } from './styles';
|
|
11
|
+
|
|
12
|
+
/** What a control reports. */
|
|
13
|
+
export interface VirtualControlCallbacks {
|
|
14
|
+
/** A button went down. */
|
|
15
|
+
onButtonDown(id: string): void;
|
|
16
|
+
/** A button came up. */
|
|
17
|
+
onButtonUp(id: string): void;
|
|
18
|
+
/** A stick moved (-1..1 per axis; 0, 0 on release). */
|
|
19
|
+
onStick(id: string, outX: number, outY: number): void;
|
|
20
|
+
/** A tap region was tapped. */
|
|
21
|
+
onTap(): void;
|
|
22
|
+
/** The player dropped a dragged control: its top-left corner as % of the viewport. */
|
|
23
|
+
onDragged(id: string, spot: { x: number; y: number }): void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** What controls need from the overlay. */
|
|
27
|
+
export interface VirtualControlEnvironment {
|
|
28
|
+
readonly callbacks: VirtualControlCallbacks;
|
|
29
|
+
/** Window listeners, removed when the controls are rebuilt. */
|
|
30
|
+
readonly scope: ListenerScope;
|
|
31
|
+
/** Whether the player is editing the layout (controls drag instead of acting). */
|
|
32
|
+
isEditing(): boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A dpad snaps past this deflection. */
|
|
36
|
+
const DPAD_THRESHOLD = 0.4;
|
|
37
|
+
|
|
38
|
+
const RADIUS_BY_SHAPE = { circle: '50%', pill: '999px', square: '0', round: '16px' } as const;
|
|
39
|
+
|
|
40
|
+
/** The first touch of a touch event. */
|
|
41
|
+
function firstTouch(event: TouchEvent): Touch | undefined {
|
|
42
|
+
return event.changedTouches[0];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function bindButton(id: string, element: HTMLElement, environment: VirtualControlEnvironment): void {
|
|
46
|
+
const { callbacks } = environment;
|
|
47
|
+
const isTapRegion = element.classList.contains('vc-transparent');
|
|
48
|
+
const press = (event: Event): void => {
|
|
49
|
+
if (environment.isEditing()) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
event.preventDefault();
|
|
53
|
+
element.classList.add('pressed');
|
|
54
|
+
callbacks.onButtonDown(id);
|
|
55
|
+
if (isTapRegion) {
|
|
56
|
+
callbacks.onTap();
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const release = (event: Event): void => {
|
|
60
|
+
event.preventDefault();
|
|
61
|
+
element.classList.remove('pressed');
|
|
62
|
+
callbacks.onButtonUp(id);
|
|
63
|
+
};
|
|
64
|
+
element.addEventListener('touchstart', press, { passive: false });
|
|
65
|
+
element.addEventListener('touchend', release, { passive: false });
|
|
66
|
+
element.addEventListener('touchcancel', release, { passive: false });
|
|
67
|
+
element.addEventListener('mousedown', press);
|
|
68
|
+
environment.scope.listenWindow('mouseup', release);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function bindStick(id: string, wrapper: HTMLElement, knob: HTMLElement, definition: VirtualDef, environment: VirtualControlEnvironment): void {
|
|
72
|
+
const isDpad = definition.type === 'dpad';
|
|
73
|
+
const axisMode = definition.axis ?? 'both';
|
|
74
|
+
const { callbacks } = environment;
|
|
75
|
+
let activeTouchId = -1;
|
|
76
|
+
let centreX = 0;
|
|
77
|
+
let centreY = 0;
|
|
78
|
+
let travelX = 1;
|
|
79
|
+
let travelY = 1;
|
|
80
|
+
const measure = (): void => {
|
|
81
|
+
const bounds = wrapper.getBoundingClientRect();
|
|
82
|
+
centreX = bounds.left + bounds.width / 2;
|
|
83
|
+
centreY = bounds.top + bounds.height / 2;
|
|
84
|
+
const knobRadius = knob.getBoundingClientRect().width / 2 || Math.min(bounds.width, bounds.height) * 0.25;
|
|
85
|
+
travelX = Math.max(1, bounds.width / 2 - knobRadius);
|
|
86
|
+
travelY = Math.max(1, bounds.height / 2 - knobRadius);
|
|
87
|
+
};
|
|
88
|
+
const moveTo = (pointerX: number, pointerY: number): void => {
|
|
89
|
+
let dx = axisMode === 'y' ? 0 : pointerX - centreX;
|
|
90
|
+
let dy = axisMode === 'x' ? 0 : pointerY - centreY;
|
|
91
|
+
if (axisMode === 'both') {
|
|
92
|
+
const limit = Math.min(travelX, travelY);
|
|
93
|
+
const distance = Math.hypot(dx, dy);
|
|
94
|
+
if (distance > limit) {
|
|
95
|
+
dx = (dx * limit) / distance;
|
|
96
|
+
dy = (dy * limit) / distance;
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
dx = Math.max(-travelX, Math.min(travelX, dx));
|
|
100
|
+
dy = Math.max(-travelY, Math.min(travelY, dy));
|
|
101
|
+
}
|
|
102
|
+
let outX = dx / travelX;
|
|
103
|
+
let outY = dy / travelY;
|
|
104
|
+
if (isDpad) {
|
|
105
|
+
outX = Math.abs(outX) > DPAD_THRESHOLD ? Math.sign(outX) : 0;
|
|
106
|
+
outY = Math.abs(outY) > DPAD_THRESHOLD ? Math.sign(outY) : 0;
|
|
107
|
+
}
|
|
108
|
+
knob.style.transform = `translate(${String(dx)}px, ${String(dy)}px)`;
|
|
109
|
+
callbacks.onStick(id, outX, outY);
|
|
110
|
+
};
|
|
111
|
+
const release = (): void => {
|
|
112
|
+
activeTouchId = -1;
|
|
113
|
+
knob.style.transform = 'translate(0,0)';
|
|
114
|
+
callbacks.onStick(id, 0, 0);
|
|
115
|
+
};
|
|
116
|
+
wrapper.addEventListener('touchstart', (event) => {
|
|
117
|
+
const touch = firstTouch(event);
|
|
118
|
+
if (environment.isEditing() || touch === undefined) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
event.preventDefault();
|
|
122
|
+
measure();
|
|
123
|
+
activeTouchId = touch.identifier;
|
|
124
|
+
moveTo(touch.clientX, touch.clientY);
|
|
125
|
+
}, { passive: false });
|
|
126
|
+
wrapper.addEventListener('touchmove', (event) => {
|
|
127
|
+
event.preventDefault();
|
|
128
|
+
for (const touch of event.changedTouches) {
|
|
129
|
+
if (touch.identifier === activeTouchId) {
|
|
130
|
+
moveTo(touch.clientX, touch.clientY);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}, { passive: false });
|
|
134
|
+
wrapper.addEventListener('touchend', release, { passive: false });
|
|
135
|
+
wrapper.addEventListener('touchcancel', release, { passive: false });
|
|
136
|
+
// Mouse drag (desktop testing).
|
|
137
|
+
let isMouseHeld = false;
|
|
138
|
+
wrapper.addEventListener('mousedown', (event) => {
|
|
139
|
+
if (environment.isEditing()) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
measure();
|
|
143
|
+
isMouseHeld = true;
|
|
144
|
+
moveTo(event.clientX, event.clientY);
|
|
145
|
+
});
|
|
146
|
+
environment.scope.listenWindow('mousemove', (event) => {
|
|
147
|
+
if (isMouseHeld) {
|
|
148
|
+
moveTo(event.clientX, event.clientY);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
environment.scope.listenWindow('mouseup', () => {
|
|
152
|
+
if (isMouseHeld) {
|
|
153
|
+
isMouseHeld = false;
|
|
154
|
+
release();
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Drag-to-move while editing: the control keeps its grab point under the finger and stays fully on-screen. */
|
|
160
|
+
function bindEditDrag(id: string, element: HTMLElement, environment: VirtualControlEnvironment): void {
|
|
161
|
+
let isDragging = false;
|
|
162
|
+
let grabX = 0;
|
|
163
|
+
let grabY = 0;
|
|
164
|
+
let droppedSpot: { x: number; y: number } | null = null;
|
|
165
|
+
const start = (pointerX: number, pointerY: number): void => {
|
|
166
|
+
if (!environment.isEditing()) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
isDragging = true;
|
|
170
|
+
const bounds = element.getBoundingClientRect();
|
|
171
|
+
grabX = pointerX - bounds.left;
|
|
172
|
+
grabY = pointerY - bounds.top;
|
|
173
|
+
droppedSpot = null;
|
|
174
|
+
};
|
|
175
|
+
const move = (pointerX: number, pointerY: number): void => {
|
|
176
|
+
if (!isDragging) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const viewportWidth = window.innerWidth;
|
|
180
|
+
const viewportHeight = window.innerHeight;
|
|
181
|
+
const left = Math.max(0, Math.min(viewportWidth - element.offsetWidth, pointerX - grabX));
|
|
182
|
+
const top = Math.max(0, Math.min(viewportHeight - element.offsetHeight, pointerY - grabY));
|
|
183
|
+
placeAt(element, left, top);
|
|
184
|
+
droppedSpot = { x: (left / viewportWidth) * 100, y: (top / viewportHeight) * 100 };
|
|
185
|
+
};
|
|
186
|
+
const end = (): void => {
|
|
187
|
+
if (!isDragging) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
isDragging = false;
|
|
191
|
+
if (droppedSpot !== null) {
|
|
192
|
+
environment.callbacks.onDragged(id, droppedSpot);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
element.addEventListener('mousedown', (event) => {
|
|
196
|
+
start(event.clientX, event.clientY);
|
|
197
|
+
});
|
|
198
|
+
element.addEventListener('touchstart', (event) => {
|
|
199
|
+
const touch = firstTouch(event);
|
|
200
|
+
if (touch !== undefined) {
|
|
201
|
+
start(touch.clientX, touch.clientY);
|
|
202
|
+
}
|
|
203
|
+
}, { passive: true });
|
|
204
|
+
environment.scope.listenWindow('mousemove', (event) => {
|
|
205
|
+
move(event.clientX, event.clientY);
|
|
206
|
+
});
|
|
207
|
+
environment.scope.listenWindow('touchmove', (event) => {
|
|
208
|
+
const touch = firstTouch(event);
|
|
209
|
+
if (isDragging && touch !== undefined) {
|
|
210
|
+
move(touch.clientX, touch.clientY);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
environment.scope.listenWindow('mouseup', end);
|
|
214
|
+
environment.scope.listenWindow('touchend', end);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Build one on-screen control element, wired up. */
|
|
218
|
+
export function createVirtualControl(definition: VirtualDef, environment: VirtualControlEnvironment): HTMLElement {
|
|
219
|
+
const isStick = isStickType(definition.type);
|
|
220
|
+
const { w: width, h: height } = declaredSize(definition);
|
|
221
|
+
const wrapper = document.createElement('div');
|
|
222
|
+
wrapper.className = `vc vc-${definition.type}`;
|
|
223
|
+
wrapper.dataset['controlId'] = definition.id;
|
|
224
|
+
// The v1 expando tests and tools read the control id from.
|
|
225
|
+
Reflect.set(wrapper, '__id', definition.id);
|
|
226
|
+
wrapper.style.width = `${String(width)}px`;
|
|
227
|
+
wrapper.style.height = `${String(height)}px`;
|
|
228
|
+
if (definition.opacity !== undefined) {
|
|
229
|
+
wrapper.style.opacity = String(definition.opacity);
|
|
230
|
+
}
|
|
231
|
+
wrapper.style.setProperty('--vc-color', definition.color ?? ACCENT_COLOR);
|
|
232
|
+
if (definition.bg !== undefined) {
|
|
233
|
+
wrapper.style.setProperty('--vc-bg', definition.bg);
|
|
234
|
+
}
|
|
235
|
+
if (definition.text !== undefined) {
|
|
236
|
+
wrapper.style.color = definition.text;
|
|
237
|
+
}
|
|
238
|
+
const radius = RADIUS_BY_SHAPE[definition.shape ?? (isStick ? 'circle' : 'round')];
|
|
239
|
+
wrapper.style.borderRadius = radius;
|
|
240
|
+
if (isStick) {
|
|
241
|
+
wrapper.innerHTML = '<div class="base"></div><div class="knob"></div>';
|
|
242
|
+
requireElement(wrapper, '.base', HTMLElement).style.borderRadius = radius;
|
|
243
|
+
// The knob stays a circle sized to the short edge (a wide pill stick gets a
|
|
244
|
+
// round knob that slides). Single-axis sticks get a knob that mostly fills
|
|
245
|
+
// the short edge; 2D sticks a smaller one with room to travel.
|
|
246
|
+
const knob = requireElement(wrapper, '.knob', HTMLElement);
|
|
247
|
+
const isSingleAxis = definition.axis === 'x' || definition.axis === 'y';
|
|
248
|
+
const knobDiameter = Math.round(Math.min(width, height) * (isSingleAxis ? 0.92 : 0.5));
|
|
249
|
+
knob.style.width = `${String(knobDiameter)}px`;
|
|
250
|
+
knob.style.height = `${String(knobDiameter)}px`;
|
|
251
|
+
bindStick(definition.id, wrapper, knob, definition, environment);
|
|
252
|
+
} else {
|
|
253
|
+
// The game's own markup (an SVG icon, an entity in a label), inserted as given.
|
|
254
|
+
wrapper.innerHTML = definition.html ?? `<span>${definition.label ?? ''}</span>`;
|
|
255
|
+
if (definition.type === 'tap') {
|
|
256
|
+
wrapper.classList.add('vc-transparent');
|
|
257
|
+
}
|
|
258
|
+
bindButton(definition.id, wrapper, environment);
|
|
259
|
+
}
|
|
260
|
+
bindEditDrag(definition.id, wrapper, environment);
|
|
261
|
+
return wrapper;
|
|
262
|
+
}
|
package/src/race.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `@diffenderfer-games/hub/race`: the race forwarder (`hub.race`), for games
|
|
3
|
-
* that want only this namespace.
|
|
4
|
-
*
|
|
5
|
-
* @packageDocumentation
|
|
6
|
-
*/
|
|
7
|
-
export { raceForwarder as race } from './realtime/race-forwarder';
|
|
8
|
-
export type * from './realtime/race-types';
|
|
9
|
-
export type { RaceHistory, RaceRecordPlayer, RaceResultPlayer, RaceStatusEvent } from './realtime/wire-types';
|
|
10
|
-
export type { RaceOutcome, RaceParams, RaceRecord, RaceResult, RaceState, RaceStats, RaceStatValue, Racer, RacerResult, RacerState, RoomInfo } from '@diffenderfer-games/hub-contract';
|
|
1
|
+
/**
|
|
2
|
+
* `@diffenderfer-games/hub/race`: the race forwarder (`hub.race`), for games
|
|
3
|
+
* that want only this namespace.
|
|
4
|
+
*
|
|
5
|
+
* @packageDocumentation
|
|
6
|
+
*/
|
|
7
|
+
export { raceForwarder as race } from './realtime/race-forwarder';
|
|
8
|
+
export type * from './realtime/race-types';
|
|
9
|
+
export type { RaceHistory, RaceRecordPlayer, RaceResultPlayer, RaceStatusEvent } from './realtime/wire-types';
|
|
10
|
+
export type { RaceOutcome, RaceParams, RaceRecord, RaceResult, RaceState, RaceStats, RaceStatValue, Racer, RacerResult, RacerState, RoomInfo } from '@diffenderfer-games/hub-contract';
|