@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.
Files changed (66) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/Game/AnimationManager.d.ts +1 -0
  3. package/dist/Game/AnimationManager.js +3 -0
  4. package/dist/Game/AnimationManager.js.map +1 -1
  5. package/dist/Game/ClientVisuals.d.ts +61 -0
  6. package/dist/Game/ClientVisuals.js +96 -0
  7. package/dist/Game/ClientVisuals.js.map +1 -0
  8. package/dist/Game/ClientVisuals.spec.d.ts +1 -0
  9. package/dist/Game/EventComponentResolver.d.ts +16 -0
  10. package/dist/Game/EventComponentResolver.js +52 -0
  11. package/dist/Game/EventComponentResolver.js.map +1 -0
  12. package/dist/Game/EventComponentResolver.spec.d.ts +1 -0
  13. package/dist/Game/Map.js +9 -0
  14. package/dist/Game/Map.js.map +1 -1
  15. package/dist/Game/Object.js +2 -2
  16. package/dist/Game/Object.js.map +1 -1
  17. package/dist/Game/Object.spec.d.ts +1 -0
  18. package/dist/Game/ProjectileManager.d.ts +11 -2
  19. package/dist/Game/ProjectileManager.js +19 -2
  20. package/dist/Game/ProjectileManager.js.map +1 -1
  21. package/dist/RpgClient.d.ts +64 -0
  22. package/dist/RpgClientEngine.d.ts +57 -0
  23. package/dist/RpgClientEngine.js +110 -14
  24. package/dist/RpgClientEngine.js.map +1 -1
  25. package/dist/components/animations/fx.ce.js +58 -0
  26. package/dist/components/animations/fx.ce.js.map +1 -0
  27. package/dist/components/animations/index.d.ts +1 -0
  28. package/dist/components/animations/index.js +3 -1
  29. package/dist/components/animations/index.js.map +1 -1
  30. package/dist/components/character.ce.js +111 -13
  31. package/dist/components/character.ce.js.map +1 -1
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.js +3 -2
  34. package/dist/module.js +7 -0
  35. package/dist/module.js.map +1 -1
  36. package/dist/services/actionInput.d.ts +3 -1
  37. package/dist/services/actionInput.js +33 -1
  38. package/dist/services/actionInput.js.map +1 -1
  39. package/dist/services/standalone.d.ts +3 -1
  40. package/dist/services/standalone.js +31 -13
  41. package/dist/services/standalone.js.map +1 -1
  42. package/dist/utils/mapId.d.ts +1 -0
  43. package/dist/utils/mapId.js +6 -0
  44. package/dist/utils/mapId.js.map +1 -0
  45. package/package.json +3 -3
  46. package/src/Game/AnimationManager.ts +4 -0
  47. package/src/Game/ClientVisuals.spec.ts +56 -0
  48. package/src/Game/ClientVisuals.ts +184 -0
  49. package/src/Game/EventComponentResolver.spec.ts +84 -0
  50. package/src/Game/EventComponentResolver.ts +74 -0
  51. package/src/Game/Map.ts +10 -0
  52. package/src/Game/Object.spec.ts +46 -0
  53. package/src/Game/Object.ts +2 -2
  54. package/src/Game/ProjectileManager.spec.ts +111 -0
  55. package/src/Game/ProjectileManager.ts +24 -2
  56. package/src/RpgClient.ts +68 -0
  57. package/src/RpgClientEngine.ts +130 -16
  58. package/src/components/animations/fx.ce +101 -0
  59. package/src/components/animations/index.ts +4 -2
  60. package/src/components/character.ce +154 -11
  61. package/src/index.ts +1 -0
  62. package/src/module.ts +11 -0
  63. package/src/services/actionInput.spec.ts +54 -0
  64. package/src/services/actionInput.ts +68 -1
  65. package/src/services/standalone.ts +39 -10
  66. 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 graphicsSignals) {
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 { getKeyboardControlBind, resolveKeyboardActionInput } from "../services/actionInput";
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
- if (canControls()) client.processInput({ input: Direction.Down })
434
+ processMovementInput(Direction.Down)
325
435
  },
326
436
  },
327
437
  up: {
328
438
  repeat: true,
329
439
  bind: keyboardControls.up,
330
440
  keyDown() {
331
- if (canControls()) client.processInput({ input: Direction.Up })
441
+ processMovementInput(Direction.Up)
332
442
  },
333
443
  },
334
444
  left: {
335
445
  repeat: true,
336
446
  bind: keyboardControls.left,
337
447
  keyDown() {
338
- if (canControls()) client.processInput({ input: Direction.Left })
448
+ processMovementInput(Direction.Left)
339
449
  },
340
450
  },
341
451
  right: {
342
452
  repeat: true,
343
453
  bind: keyboardControls.right,
344
454
  keyDown() {
345
- if (canControls()) client.processInput({ input: Direction.Right })
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(resolveKeyboardActionInput(keyboardControls.action, client, sprite))
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
- realAnimationName.set(curr);
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 (!movementAnimations.includes(curr)) {
912
+ else if (!isMovementAnimation) {
789
913
  realAnimationName.set(curr);
790
914
  }
791
- if (!isMoving && sprite.animationIsPlaying && sprite.animationIsPlaying()) {
792
- if (movementAnimations.includes(curr)) {
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 { RpgActionInput, RpgActionName } from "@rpgjs/common";
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 pendingOn: Array<{ event: string; callback: (data: any) => void }> = [];
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.pendingOn.forEach(({ event, callback }) => this.socket.addEventListener(event, callback));
64
- this.pendingOn = [];
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
- if (!this.socket) {
77
- this.pendingOn.push({ event: "message", callback: handler });
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
- if (!this.socket) return;
85
- this.socket.removeEventListener(event, callback);
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
  }
@@ -0,0 +1,2 @@
1
+ export const normalizeRoomMapId = (mapId: string | undefined): string | undefined =>
2
+ typeof mapId === "string" ? mapId.replace(/^map-/, "") : undefined;