@rpgjs/client 5.0.0-beta.11 → 5.0.0-beta.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/dist/Game/AnimationManager.d.ts +1 -0
- package/dist/Game/AnimationManager.js +3 -0
- package/dist/Game/AnimationManager.js.map +1 -1
- package/dist/Game/ClientVisuals.d.ts +61 -0
- package/dist/Game/ClientVisuals.js +96 -0
- package/dist/Game/ClientVisuals.js.map +1 -0
- package/dist/Game/ClientVisuals.spec.d.ts +1 -0
- package/dist/Game/EventComponentResolver.d.ts +16 -0
- package/dist/Game/EventComponentResolver.js +52 -0
- package/dist/Game/EventComponentResolver.js.map +1 -0
- package/dist/Game/EventComponentResolver.spec.d.ts +1 -0
- package/dist/Game/Map.js +9 -0
- package/dist/Game/Map.js.map +1 -1
- package/dist/Game/Object.js +2 -2
- package/dist/Game/Object.js.map +1 -1
- package/dist/Game/Object.spec.d.ts +1 -0
- package/dist/Game/ProjectileManager.d.ts +11 -2
- package/dist/Game/ProjectileManager.js +19 -2
- package/dist/Game/ProjectileManager.js.map +1 -1
- package/dist/RpgClient.d.ts +64 -0
- package/dist/RpgClientEngine.d.ts +57 -0
- package/dist/RpgClientEngine.js +110 -14
- package/dist/RpgClientEngine.js.map +1 -1
- package/dist/components/animations/fx.ce.js +58 -0
- package/dist/components/animations/fx.ce.js.map +1 -0
- package/dist/components/animations/index.d.ts +1 -0
- package/dist/components/animations/index.js +3 -1
- package/dist/components/animations/index.js.map +1 -1
- package/dist/components/character.ce.js +111 -13
- package/dist/components/character.ce.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -2
- package/dist/module.js +7 -0
- package/dist/module.js.map +1 -1
- package/dist/services/actionInput.d.ts +3 -1
- package/dist/services/actionInput.js +33 -1
- package/dist/services/actionInput.js.map +1 -1
- package/dist/services/standalone.d.ts +3 -1
- package/dist/services/standalone.js +31 -13
- package/dist/services/standalone.js.map +1 -1
- package/dist/utils/mapId.d.ts +1 -0
- package/dist/utils/mapId.js +6 -0
- package/dist/utils/mapId.js.map +1 -0
- package/package.json +3 -3
- package/src/Game/AnimationManager.ts +4 -0
- package/src/Game/ClientVisuals.spec.ts +56 -0
- package/src/Game/ClientVisuals.ts +184 -0
- package/src/Game/EventComponentResolver.spec.ts +84 -0
- package/src/Game/EventComponentResolver.ts +74 -0
- package/src/Game/Map.ts +10 -0
- package/src/Game/Object.spec.ts +46 -0
- package/src/Game/Object.ts +2 -2
- package/src/Game/ProjectileManager.spec.ts +111 -0
- package/src/Game/ProjectileManager.ts +24 -2
- package/src/RpgClient.ts +68 -0
- package/src/RpgClientEngine.ts +130 -16
- package/src/components/animations/fx.ce +101 -0
- package/src/components/animations/index.ts +4 -2
- package/src/components/character.ce +154 -11
- package/src/index.ts +1 -0
- package/src/module.ts +11 -0
- package/src/services/actionInput.spec.ts +54 -0
- package/src/services/actionInput.ts +68 -1
- package/src/services/standalone.ts +39 -10
- package/src/utils/mapId.ts +2 -0
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<PlayerComponents object={sprite} position="left" graphicBounds />
|
|
9
9
|
<Particle emit={emitParticleTrigger} settings={particleSettings} zIndex={1000} name={particleName} />
|
|
10
10
|
<Container>
|
|
11
|
-
@for (graphicObj of
|
|
11
|
+
@for (graphicObj of renderedGraphics) {
|
|
12
12
|
<Container scale={graphicScale(graphicObj)}>
|
|
13
13
|
<Sprite
|
|
14
14
|
sheet={sheet(graphicObj)}
|
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
/>
|
|
21
21
|
</Container>
|
|
22
22
|
}
|
|
23
|
+
@for (eventComponent of resolvedEventComponents) {
|
|
24
|
+
<Container dependencies={eventComponent.dependencies}>
|
|
25
|
+
<eventComponent.component ...eventComponent.props />
|
|
26
|
+
</Container>
|
|
27
|
+
}
|
|
23
28
|
</Container>
|
|
24
29
|
<PlayerComponents object={sprite} position="center" graphicBounds />
|
|
25
30
|
<PlayerComponents object={sprite} position="right" graphicBounds />
|
|
@@ -52,11 +57,17 @@
|
|
|
52
57
|
import { RpgClientEngine } from "../RpgClientEngine";
|
|
53
58
|
import { inject } from "../core/inject";
|
|
54
59
|
import { Direction, Animation } from "@rpgjs/common";
|
|
60
|
+
import { normalizeEventComponent } from "../Game/EventComponentResolver";
|
|
55
61
|
import Hit from "./effects/hit.ce";
|
|
56
62
|
import PlayerComponents from "./player-components.ce";
|
|
57
63
|
import { RpgGui } from "../Gui/Gui";
|
|
58
64
|
import { getCanMoveValue } from "../utils/readPropValue";
|
|
59
|
-
import {
|
|
65
|
+
import {
|
|
66
|
+
getKeyboardControlBind,
|
|
67
|
+
keyboardEventMatchesBind,
|
|
68
|
+
resolveKeyboardActionInput,
|
|
69
|
+
resolveKeyboardDirectionInput,
|
|
70
|
+
} from "../services/actionInput";
|
|
60
71
|
|
|
61
72
|
const { object, id } = defineProps();
|
|
62
73
|
const sprite = object();
|
|
@@ -213,6 +224,18 @@
|
|
|
213
224
|
const normalizedComponentsInFront = computed(() => {
|
|
214
225
|
return normalizeComponents(componentsInFront());
|
|
215
226
|
});
|
|
227
|
+
|
|
228
|
+
const isEventSprite = () => {
|
|
229
|
+
return typeof sprite?.isEvent === 'function'
|
|
230
|
+
? sprite.isEvent()
|
|
231
|
+
: sprite?._type === 'event';
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const resolvedEventComponents = computed(() => {
|
|
235
|
+
if (!isEventSprite()) return [];
|
|
236
|
+
const eventComponent = normalizeEventComponent(client.resolveEventComponent(sprite), sprite);
|
|
237
|
+
return eventComponent ? [eventComponent] : [];
|
|
238
|
+
});
|
|
216
239
|
|
|
217
240
|
/**
|
|
218
241
|
* Determine if the camera should follow this sprite
|
|
@@ -263,6 +286,12 @@
|
|
|
263
286
|
flashTrigger
|
|
264
287
|
} = sprite;
|
|
265
288
|
|
|
289
|
+
const renderedGraphics = computed(() => {
|
|
290
|
+
const eventComponent = resolvedEventComponents()[0];
|
|
291
|
+
if (eventComponent && !eventComponent.renderGraphic) return [];
|
|
292
|
+
return graphicsSignals();
|
|
293
|
+
});
|
|
294
|
+
|
|
266
295
|
/**
|
|
267
296
|
* Flash configuration signals for dynamic options
|
|
268
297
|
* These signals are updated when the flash trigger is activated with options
|
|
@@ -308,6 +337,87 @@
|
|
|
308
337
|
|
|
309
338
|
const canControls = () => isMe() && getCanMoveValue(sprite)
|
|
310
339
|
const keyboardControls = client.globalConfig.keyboardControls;
|
|
340
|
+
const activeDirectionKeys = new Map();
|
|
341
|
+
|
|
342
|
+
const resolveHeldDirection = () => {
|
|
343
|
+
const directions = Array.from(activeDirectionKeys.values());
|
|
344
|
+
return directions[directions.length - 1];
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const resolveSpriteDirection = () => {
|
|
348
|
+
const heldDirection = resolveHeldDirection();
|
|
349
|
+
if (heldDirection) return heldDirection;
|
|
350
|
+
if (typeof sprite.getDirection === 'function') return sprite.getDirection();
|
|
351
|
+
if (typeof sprite.direction === 'function') return sprite.direction();
|
|
352
|
+
return direction();
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const withCurrentDirection = (payload) => {
|
|
356
|
+
if (payload.action !== 'action') return payload;
|
|
357
|
+
const data = payload.data && typeof payload.data === 'object'
|
|
358
|
+
? { ...payload.data }
|
|
359
|
+
: {};
|
|
360
|
+
return {
|
|
361
|
+
...payload,
|
|
362
|
+
data: {
|
|
363
|
+
...data,
|
|
364
|
+
direction: data.direction ?? resolveSpriteDirection(),
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const resolveCurrentActionInput = () =>
|
|
370
|
+
withCurrentDirection(
|
|
371
|
+
resolveKeyboardActionInput(keyboardControls.action, client, sprite)
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
const playPredictedWalkAnimation = () => {
|
|
375
|
+
if (sprite.animationFixed) return;
|
|
376
|
+
if (sprite.animationIsPlaying && sprite.animationIsPlaying()) return;
|
|
377
|
+
realAnimationName.set('walk');
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const resumeHeldDirectionWalkAnimation = () => {
|
|
381
|
+
if (!isCurrentPlayer()) return false;
|
|
382
|
+
if (activeDirectionKeys.size === 0) return false;
|
|
383
|
+
if (!canControls()) return false;
|
|
384
|
+
if (sprite.animationFixed) return false;
|
|
385
|
+
if (sprite.animationIsPlaying && sprite.animationIsPlaying()) return false;
|
|
386
|
+
realAnimationName.set('walk');
|
|
387
|
+
return true;
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const processMovementInput = (input) => {
|
|
391
|
+
if (!canControls()) return;
|
|
392
|
+
client.processInput({ input });
|
|
393
|
+
playPredictedWalkAnimation();
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const actionBind = () => getKeyboardControlBind(keyboardControls.action);
|
|
397
|
+
const keyboardEventId = (event) => `${event.keyCode}:${event.code}:${event.key}`;
|
|
398
|
+
|
|
399
|
+
const handleNativeActionWhileMoving = (event) => {
|
|
400
|
+
const inputDirection = resolveKeyboardDirectionInput(event, keyboardControls);
|
|
401
|
+
if (inputDirection) {
|
|
402
|
+
const keyId = keyboardEventId(event);
|
|
403
|
+
if (event.type === 'keydown') {
|
|
404
|
+
activeDirectionKeys.delete(keyId);
|
|
405
|
+
activeDirectionKeys.set(keyId, inputDirection);
|
|
406
|
+
resumeHeldDirectionWalkAnimation();
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
activeDirectionKeys.delete(keyId);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (!isCurrentPlayer()) return;
|
|
414
|
+
if (event.type !== 'keydown' || event.repeat) return;
|
|
415
|
+
if (activeDirectionKeys.size === 0) return;
|
|
416
|
+
if (!keyboardEventMatchesBind(event, actionBind())) return;
|
|
417
|
+
if (!canControls()) return;
|
|
418
|
+
|
|
419
|
+
client.processAction(resolveCurrentActionInput());
|
|
420
|
+
};
|
|
311
421
|
|
|
312
422
|
const visible = computed(() => {
|
|
313
423
|
if (sprite.isEvent()) {
|
|
@@ -321,35 +431,35 @@
|
|
|
321
431
|
repeat: true,
|
|
322
432
|
bind: keyboardControls.down,
|
|
323
433
|
keyDown() {
|
|
324
|
-
|
|
434
|
+
processMovementInput(Direction.Down)
|
|
325
435
|
},
|
|
326
436
|
},
|
|
327
437
|
up: {
|
|
328
438
|
repeat: true,
|
|
329
439
|
bind: keyboardControls.up,
|
|
330
440
|
keyDown() {
|
|
331
|
-
|
|
441
|
+
processMovementInput(Direction.Up)
|
|
332
442
|
},
|
|
333
443
|
},
|
|
334
444
|
left: {
|
|
335
445
|
repeat: true,
|
|
336
446
|
bind: keyboardControls.left,
|
|
337
447
|
keyDown() {
|
|
338
|
-
|
|
448
|
+
processMovementInput(Direction.Left)
|
|
339
449
|
},
|
|
340
450
|
},
|
|
341
451
|
right: {
|
|
342
452
|
repeat: true,
|
|
343
453
|
bind: keyboardControls.right,
|
|
344
454
|
keyDown() {
|
|
345
|
-
|
|
455
|
+
processMovementInput(Direction.Right)
|
|
346
456
|
},
|
|
347
457
|
},
|
|
348
458
|
action: {
|
|
349
459
|
bind: getKeyboardControlBind(keyboardControls.action),
|
|
350
460
|
keyDown() {
|
|
351
461
|
if (canControls()) {
|
|
352
|
-
client.processAction(
|
|
462
|
+
client.processAction(resolveCurrentActionInput())
|
|
353
463
|
}
|
|
354
464
|
},
|
|
355
465
|
},
|
|
@@ -648,6 +758,10 @@
|
|
|
648
758
|
const graphicBounds = computed(() => {
|
|
649
759
|
const box = hitbox();
|
|
650
760
|
const fallback = hitboxBounds();
|
|
761
|
+
const customEventComponent = resolvedEventComponents()[0];
|
|
762
|
+
if (customEventComponent && !customEventComponent.renderGraphic) {
|
|
763
|
+
return fallback;
|
|
764
|
+
}
|
|
651
765
|
const dimensions = imageDimensions();
|
|
652
766
|
const graphics = graphicsSignals();
|
|
653
767
|
let bounds = null;
|
|
@@ -779,17 +893,27 @@
|
|
|
779
893
|
});
|
|
780
894
|
|
|
781
895
|
const animationMovementSubscription = combineLatest([animationChange$, moving$]).subscribe(([[prev, curr], isMoving]) => {
|
|
896
|
+
const isMovementAnimation = movementAnimations.includes(curr);
|
|
897
|
+
const isTemporaryAnimationPlaying =
|
|
898
|
+
sprite.animationIsPlaying && sprite.animationIsPlaying();
|
|
899
|
+
|
|
900
|
+
if (sprite.animationFixed && isMovementAnimation && isTemporaryAnimationPlaying) {
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
|
|
782
904
|
if (curr == 'stand' && !isMoving) {
|
|
783
|
-
|
|
905
|
+
if (!resumeHeldDirectionWalkAnimation()) {
|
|
906
|
+
realAnimationName.set(curr);
|
|
907
|
+
}
|
|
784
908
|
}
|
|
785
909
|
else if (curr == 'walk' && isMoving) {
|
|
786
910
|
realAnimationName.set(curr);
|
|
787
911
|
}
|
|
788
|
-
else if (!
|
|
912
|
+
else if (!isMovementAnimation) {
|
|
789
913
|
realAnimationName.set(curr);
|
|
790
914
|
}
|
|
791
|
-
if (!isMoving &&
|
|
792
|
-
if (
|
|
915
|
+
if (!isMoving && isTemporaryAnimationPlaying) {
|
|
916
|
+
if (isMovementAnimation) {
|
|
793
917
|
if (typeof sprite.resetAnimationState === 'function') {
|
|
794
918
|
sprite.resetAnimationState();
|
|
795
919
|
}
|
|
@@ -797,6 +921,16 @@
|
|
|
797
921
|
}
|
|
798
922
|
});
|
|
799
923
|
|
|
924
|
+
const resumeWalkSubscriptions = [
|
|
925
|
+
sprite._canMove,
|
|
926
|
+
sprite._animationFixed,
|
|
927
|
+
sprite.animationIsPlaying,
|
|
928
|
+
]
|
|
929
|
+
.filter(signal => signal?.observable)
|
|
930
|
+
.map(signal => signal.observable.subscribe(() => {
|
|
931
|
+
resumeHeldDirectionWalkAnimation();
|
|
932
|
+
}));
|
|
933
|
+
|
|
800
934
|
/**
|
|
801
935
|
* Cleanup subscriptions and call hooks before sprite destruction.
|
|
802
936
|
*
|
|
@@ -834,8 +968,13 @@
|
|
|
834
968
|
const onBeforeDestroy = async () => {
|
|
835
969
|
await runBeforeRemove();
|
|
836
970
|
await waitForTemporaryAnimationEnd();
|
|
971
|
+
if (typeof document !== 'undefined') {
|
|
972
|
+
document.removeEventListener('keydown', handleNativeActionWhileMoving);
|
|
973
|
+
document.removeEventListener('keyup', handleNativeActionWhileMoving);
|
|
974
|
+
}
|
|
837
975
|
removeTransitionSubscription?.unsubscribe();
|
|
838
976
|
animationMovementSubscription.unsubscribe();
|
|
977
|
+
resumeWalkSubscriptions.forEach(subscription => subscription.unsubscribe());
|
|
839
978
|
xSubscription.unsubscribe();
|
|
840
979
|
ySubscription.unsubscribe();
|
|
841
980
|
await lastValueFrom(hooks.callHooks("client-sprite-onDestroy", sprite))
|
|
@@ -843,6 +982,10 @@
|
|
|
843
982
|
}
|
|
844
983
|
|
|
845
984
|
mount((element) => {
|
|
985
|
+
if (typeof document !== 'undefined') {
|
|
986
|
+
document.addEventListener('keydown', handleNativeActionWhileMoving);
|
|
987
|
+
document.addEventListener('keyup', handleNativeActionWhileMoving);
|
|
988
|
+
}
|
|
846
989
|
hooks.callHooks("client-sprite-onAdd", sprite).subscribe()
|
|
847
990
|
hooks.callHooks("client-sceneMap-onAddSprite", client.sceneMap, sprite).subscribe()
|
|
848
991
|
effect(() => {
|
package/src/index.ts
CHANGED
|
@@ -27,5 +27,6 @@ export { RpgClientObject } from "./Game/Object";
|
|
|
27
27
|
export { RpgClientPlayer } from "./Game/Player";
|
|
28
28
|
export { RpgClientEvent } from "./Game/Event";
|
|
29
29
|
export * from "./Game/ProjectileManager";
|
|
30
|
+
export * from "./Game/ClientVisuals";
|
|
30
31
|
export { withMobile } from "./components/gui/mobile";
|
|
31
32
|
export * from "./services/AbstractSocket";
|
package/src/module.ts
CHANGED
|
@@ -155,6 +155,14 @@ export function provideClientModules(modules: RpgClientModule[]): FactoryProvide
|
|
|
155
155
|
},
|
|
156
156
|
};
|
|
157
157
|
}
|
|
158
|
+
if (module.clientVisuals) {
|
|
159
|
+
const clientVisuals = { ...module.clientVisuals };
|
|
160
|
+
module.clientVisuals = {
|
|
161
|
+
load: (engine: RpgClientEngine) => {
|
|
162
|
+
engine.registerClientVisuals(clientVisuals);
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
158
166
|
if (module.projectiles) {
|
|
159
167
|
const projectiles = { ...module.projectiles };
|
|
160
168
|
module.projectiles = {
|
|
@@ -213,6 +221,9 @@ export function provideClientModules(modules: RpgClientModule[]): FactoryProvide
|
|
|
213
221
|
engine.registerSpriteComponent(id, component);
|
|
214
222
|
});
|
|
215
223
|
}
|
|
224
|
+
if (sprite.eventComponent) {
|
|
225
|
+
engine.addEventComponentResolver(sprite.eventComponent);
|
|
226
|
+
}
|
|
216
227
|
},
|
|
217
228
|
};
|
|
218
229
|
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { describe, expect, test } from "vitest";
|
|
2
2
|
import {
|
|
3
3
|
getKeyboardControlBind,
|
|
4
|
+
keyboardEventMatchesBind,
|
|
4
5
|
normalizeActionInput,
|
|
5
6
|
resolveKeyboardActionInput,
|
|
7
|
+
resolveKeyboardDirectionInput,
|
|
6
8
|
} from "./actionInput";
|
|
7
9
|
|
|
10
|
+
const keyboardEvent = (values: Partial<KeyboardEvent>) =>
|
|
11
|
+
values as KeyboardEvent;
|
|
12
|
+
|
|
8
13
|
describe("normalizeActionInput", () => {
|
|
9
14
|
test("keeps simple actions compatible", () => {
|
|
10
15
|
expect(normalizeActionInput("action")).toEqual({
|
|
@@ -98,4 +103,53 @@ describe("keyboard action controls", () => {
|
|
|
98
103
|
action: "projectile:shoot",
|
|
99
104
|
});
|
|
100
105
|
});
|
|
106
|
+
|
|
107
|
+
test("matches keyboard events against string, numeric, and array binds", () => {
|
|
108
|
+
expect(
|
|
109
|
+
keyboardEventMatchesBind(
|
|
110
|
+
keyboardEvent({ key: " ", code: "Space", keyCode: 32 }),
|
|
111
|
+
"space"
|
|
112
|
+
)
|
|
113
|
+
).toBe(true);
|
|
114
|
+
expect(
|
|
115
|
+
keyboardEventMatchesBind(
|
|
116
|
+
keyboardEvent({ key: "ArrowUp", code: "ArrowUp", keyCode: 38 }),
|
|
117
|
+
"up"
|
|
118
|
+
)
|
|
119
|
+
).toBe(true);
|
|
120
|
+
expect(
|
|
121
|
+
keyboardEventMatchesBind(
|
|
122
|
+
keyboardEvent({ key: "x", code: "KeyX", keyCode: 88 }),
|
|
123
|
+
["space", "x"]
|
|
124
|
+
)
|
|
125
|
+
).toBe(true);
|
|
126
|
+
expect(
|
|
127
|
+
keyboardEventMatchesBind(
|
|
128
|
+
keyboardEvent({ key: "Escape", code: "Escape", keyCode: 27 }),
|
|
129
|
+
27
|
|
130
|
+
)
|
|
131
|
+
).toBe(true);
|
|
132
|
+
expect(
|
|
133
|
+
keyboardEventMatchesBind(
|
|
134
|
+
keyboardEvent({ key: "a", code: "KeyA", keyCode: 65 }),
|
|
135
|
+
"space"
|
|
136
|
+
)
|
|
137
|
+
).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("resolves directional keyboard controls from a native keyboard event", () => {
|
|
141
|
+
const controls = {
|
|
142
|
+
up: "up",
|
|
143
|
+
down: "down",
|
|
144
|
+
left: "left",
|
|
145
|
+
right: "right",
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
expect(
|
|
149
|
+
resolveKeyboardDirectionInput(
|
|
150
|
+
keyboardEvent({ key: "ArrowRight", code: "ArrowRight", keyCode: 39 }),
|
|
151
|
+
controls
|
|
152
|
+
)
|
|
153
|
+
).toBe("right");
|
|
154
|
+
});
|
|
101
155
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { Direction, type RpgActionInput, type RpgActionName } from "@rpgjs/common";
|
|
2
2
|
|
|
3
3
|
export type KeyboardActionDataResolver<TClient = any, TSprite = any> = (
|
|
4
4
|
client: TClient,
|
|
@@ -32,6 +32,53 @@ export function getKeyboardControlBind(control: any): any {
|
|
|
32
32
|
return isKeyboardActionConfig(control) ? control.bind : control;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
const KEY_CODE_NAMES: Record<number, string> = {
|
|
36
|
+
32: "space",
|
|
37
|
+
27: "escape",
|
|
38
|
+
37: "left",
|
|
39
|
+
38: "up",
|
|
40
|
+
39: "right",
|
|
41
|
+
40: "down",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const normalizeKeyboardName = (value: unknown): string | undefined => {
|
|
45
|
+
if (typeof value !== "string") return undefined;
|
|
46
|
+
const normalized = value.toLowerCase();
|
|
47
|
+
if (
|
|
48
|
+
normalized === " " ||
|
|
49
|
+
normalized === "spacebar" ||
|
|
50
|
+
normalized === "space"
|
|
51
|
+
) {
|
|
52
|
+
return "space";
|
|
53
|
+
}
|
|
54
|
+
if (normalized.startsWith("arrow")) {
|
|
55
|
+
return normalized.slice("arrow".length);
|
|
56
|
+
}
|
|
57
|
+
return normalized;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export function keyboardEventMatchesBind(
|
|
61
|
+
event: KeyboardEvent,
|
|
62
|
+
bind: any
|
|
63
|
+
): boolean {
|
|
64
|
+
if (Array.isArray(bind)) {
|
|
65
|
+
return bind.some(item => keyboardEventMatchesBind(event, item));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (typeof bind === "number") {
|
|
69
|
+
return event.keyCode === bind;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const expected = normalizeKeyboardName(bind);
|
|
73
|
+
if (!expected) return false;
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
normalizeKeyboardName(event.key) === expected ||
|
|
77
|
+
normalizeKeyboardName(event.code) === expected ||
|
|
78
|
+
KEY_CODE_NAMES[event.keyCode] === expected
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
35
82
|
export function resolveKeyboardActionInput(
|
|
36
83
|
control: any,
|
|
37
84
|
client: any,
|
|
@@ -51,3 +98,23 @@ export function resolveKeyboardActionInput(
|
|
|
51
98
|
? { action }
|
|
52
99
|
: { action, data };
|
|
53
100
|
}
|
|
101
|
+
|
|
102
|
+
export function resolveKeyboardDirectionInput(
|
|
103
|
+
event: KeyboardEvent,
|
|
104
|
+
keyboardControls: any
|
|
105
|
+
): Direction | undefined {
|
|
106
|
+
const directions: Array<[any, Direction]> = [
|
|
107
|
+
[keyboardControls?.up, Direction.Up],
|
|
108
|
+
[keyboardControls?.down, Direction.Down],
|
|
109
|
+
[keyboardControls?.left, Direction.Left],
|
|
110
|
+
[keyboardControls?.right, Direction.Right],
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
for (const [control, direction] of directions) {
|
|
114
|
+
if (keyboardEventMatchesBind(event, getKeyboardControlBind(control))) {
|
|
115
|
+
return direction;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
@@ -19,7 +19,12 @@ interface StandaloneOptions {
|
|
|
19
19
|
class BridgeWebsocket extends AbstractWebsocket {
|
|
20
20
|
private room: ServerIo;
|
|
21
21
|
private socket: ClientIo;
|
|
22
|
-
private
|
|
22
|
+
private socketRoom?: ServerIo;
|
|
23
|
+
private listeners: Array<{
|
|
24
|
+
event: string;
|
|
25
|
+
callback: (data: any) => void;
|
|
26
|
+
handler: (event: any) => void;
|
|
27
|
+
}> = [];
|
|
23
28
|
private rooms = {
|
|
24
29
|
partyFn: async (roomId: string) => {
|
|
25
30
|
this.room = new ServerIo(roomId, this.rooms);
|
|
@@ -49,6 +54,7 @@ class BridgeWebsocket extends AbstractWebsocket {
|
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
private async _connection(listeners?: (data: any) => void) {
|
|
57
|
+
this.detachCurrentSocket();
|
|
52
58
|
this.serverInstance = this.context.get('server')
|
|
53
59
|
this.socket = new ClientIo(this.serverInstance, 'player-client-id');
|
|
54
60
|
const url = new URL('http://localhost')
|
|
@@ -60,29 +66,42 @@ class BridgeWebsocket extends AbstractWebsocket {
|
|
|
60
66
|
})
|
|
61
67
|
listeners?.(this.socket)
|
|
62
68
|
this.room.clients.set(this.socket.id, this.socket);
|
|
63
|
-
this.
|
|
64
|
-
this.
|
|
69
|
+
this.socketRoom = this.room;
|
|
70
|
+
this.listeners.forEach(({ handler }) => {
|
|
71
|
+
this.socket.addEventListener("message", handler);
|
|
72
|
+
});
|
|
65
73
|
await this.serverInstance.onConnect(this.socket.conn as any, { request } as any);
|
|
66
74
|
return this.socket
|
|
67
75
|
}
|
|
68
76
|
|
|
69
77
|
on(key: string, callback: (data: any) => void) {
|
|
78
|
+
if (
|
|
79
|
+
this.listeners.some(
|
|
80
|
+
(listener) => listener.event === key && listener.callback === callback
|
|
81
|
+
)
|
|
82
|
+
) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
70
85
|
const handler = (event) => {
|
|
71
86
|
const object = normalizeStandaloneMessage(event);
|
|
72
87
|
if (object.type === key) {
|
|
73
88
|
callback(object.value);
|
|
74
89
|
}
|
|
75
90
|
};
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
this.socket.addEventListener("message", handler);
|
|
91
|
+
this.listeners.push({ event: key, callback, handler });
|
|
92
|
+
this.socket?.addEventListener("message", handler);
|
|
81
93
|
}
|
|
82
94
|
|
|
83
95
|
off(event: string, callback: (data: any) => void) {
|
|
84
|
-
|
|
85
|
-
this.
|
|
96
|
+
const remaining: typeof this.listeners = [];
|
|
97
|
+
for (const listener of this.listeners) {
|
|
98
|
+
if (listener.event === event && listener.callback === callback) {
|
|
99
|
+
this.socket?.removeEventListener("message", listener.handler);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
remaining.push(listener);
|
|
103
|
+
}
|
|
104
|
+
this.listeners = remaining;
|
|
86
105
|
}
|
|
87
106
|
|
|
88
107
|
emit(event: string, data: any) {
|
|
@@ -135,6 +154,16 @@ class BridgeWebsocket extends AbstractWebsocket {
|
|
|
135
154
|
})
|
|
136
155
|
}
|
|
137
156
|
|
|
157
|
+
private detachCurrentSocket() {
|
|
158
|
+
if (!this.socket) return;
|
|
159
|
+
this.listeners.forEach(({ handler }) => {
|
|
160
|
+
this.socket.removeEventListener("message", handler);
|
|
161
|
+
});
|
|
162
|
+
this.socketRoom?.clients?.delete?.(this.socket.id);
|
|
163
|
+
this.socket = undefined as any;
|
|
164
|
+
this.socketRoom = undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
138
167
|
getServer() {
|
|
139
168
|
return this.serverInstance
|
|
140
169
|
}
|