@rpgjs/client 5.0.0-beta.22 → 5.0.0-beta.24

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 (49) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/Game/Object.d.ts +6 -0
  3. package/dist/Game/Object.js +58 -2
  4. package/dist/Game/Object.js.map +1 -1
  5. package/dist/RpgClientEngine.d.ts +2 -0
  6. package/dist/RpgClientEngine.js +15 -6
  7. package/dist/RpgClientEngine.js.map +1 -1
  8. package/dist/components/character-hitbox.d.ts +13 -0
  9. package/dist/components/character-hitbox.js +38 -0
  10. package/dist/components/character-hitbox.js.map +1 -0
  11. package/dist/components/character-hitbox.spec.d.ts +1 -0
  12. package/dist/components/character.ce.js +38 -29
  13. package/dist/components/character.ce.js.map +1 -1
  14. package/dist/components/gui/dialogbox/index.ce.js +31 -2
  15. package/dist/components/gui/dialogbox/index.ce.js.map +1 -1
  16. package/dist/components/gui/hud/hud.ce.js +2 -2
  17. package/dist/components/gui/hud/hud.ce.js.map +1 -1
  18. package/dist/components/gui/menu/items-menu.ce.js +27 -4
  19. package/dist/components/gui/menu/items-menu.ce.js.map +1 -1
  20. package/dist/components/gui/menu/main-menu.ce.js +210 -155
  21. package/dist/components/gui/menu/main-menu.ce.js.map +1 -1
  22. package/dist/components/gui/save-load.ce.js +33 -4
  23. package/dist/components/gui/save-load.ce.js.map +1 -1
  24. package/dist/components/gui/shop/shop.ce.js +47 -4
  25. package/dist/components/gui/shop/shop.ce.js.map +1 -1
  26. package/dist/services/standalone.d.ts +6 -1
  27. package/dist/services/standalone.js +35 -17
  28. package/dist/services/standalone.js.map +1 -1
  29. package/dist/utils/syncHitbox.d.ts +1 -0
  30. package/dist/utils/syncHitbox.js +69 -0
  31. package/dist/utils/syncHitbox.js.map +1 -0
  32. package/dist/utils/syncHitbox.spec.d.ts +1 -0
  33. package/package.json +4 -4
  34. package/src/Game/Object.spec.ts +70 -6
  35. package/src/Game/Object.ts +90 -2
  36. package/src/RpgClientEngine.ts +19 -6
  37. package/src/components/character-hitbox.spec.ts +33 -0
  38. package/src/components/character-hitbox.ts +72 -0
  39. package/src/components/character.ce +52 -45
  40. package/src/components/gui/dialogbox/index.ce +56 -40
  41. package/src/components/gui/hud/hud.ce +2 -2
  42. package/src/components/gui/menu/items-menu.ce +12 -2
  43. package/src/components/gui/menu/main-menu.ce +37 -6
  44. package/src/components/gui/save-load.ce +48 -32
  45. package/src/components/gui/shop/shop.ce +28 -3
  46. package/src/services/standalone.spec.ts +47 -0
  47. package/src/services/standalone.ts +53 -20
  48. package/src/utils/syncHitbox.spec.ts +79 -0
  49. package/src/utils/syncHitbox.ts +104 -0
@@ -23,12 +23,12 @@
23
23
  <Particle emit={emitParticleTrigger} settings={particleSettings} zIndex={1000} name={particleName} />
24
24
  <Container>
25
25
  @for (graphicObj of renderedGraphics) {
26
- <Container scale={graphicScale(graphicObj)}>
26
+ <Container scale={graphicContainerScale(graphicObj)}>
27
27
  <Sprite
28
28
  sheet={sheet(graphicObj)}
29
29
  direction
30
30
  tint
31
- hitbox
31
+ hitbox={graphicHitbox(graphicObj)}
32
32
  shadowCaster={shadowCaster(graphicObj)}
33
33
  flash={flashConfig}
34
34
  />
@@ -76,6 +76,7 @@
76
76
  import { GameEngineToken, ModulesToken, shouldRenderLightingShadows } from "@rpgjs/common";
77
77
  import { RpgClientEngine } from "../RpgClientEngine";
78
78
  import { applyCameraFollow, clearCameraFollowPlugins, ownsCameraFollowRevision } from "../services/cameraFollow";
79
+ import { resolveScaledHitboxAnchor, scaleHitboxForGraphicDisplay, toPositiveNumber } from "./character-hitbox";
79
80
  import { inject } from "../core/inject";
80
81
  import { Direction, Animation } from "@rpgjs/common";
81
82
  import { normalizeEventComponent } from "../Game/EventComponentResolver";
@@ -520,6 +521,16 @@
520
521
  });
521
522
  };
522
523
 
524
+ let lastEscapeActionAt = 0;
525
+ const processEscapeInput = () => {
526
+ const now = Date.now();
527
+ if (now - lastEscapeActionAt < 50) return;
528
+ lastEscapeActionAt = now;
529
+ if (canControls()) {
530
+ client.processAction({ action: 'escape' })
531
+ }
532
+ };
533
+
523
534
  const actionBind = () => getKeyboardControlBind(keyboardControls.action);
524
535
  const keyboardEventId = (event) => `${event.keyCode}:${event.code}:${event.key}`;
525
536
 
@@ -615,17 +626,13 @@
615
626
  back: {
616
627
  bind: keyboardControls.escape,
617
628
  keyDown() {
618
- if (canControls()) {
619
- client.processAction({ action: 'escape' })
620
- }
629
+ processEscapeInput()
621
630
  },
622
631
  },
623
632
  escape: {
624
633
  bind: keyboardControls.escape,
625
634
  keyDown() {
626
- if (canControls()) {
627
- client.processAction({ action: 'escape' })
628
- }
635
+ processEscapeInput()
629
636
  },
630
637
  },
631
638
  joystick: {
@@ -685,7 +692,7 @@
685
692
  }
686
693
 
687
694
  const graphicScale = (graphicObject) => {
688
- const scale = graphicObject?.displayScale ?? graphicObject?.scale;
695
+ const scale = graphicObject?.scale;
689
696
  if (Array.isArray(scale)) return scale;
690
697
  if (typeof scale === 'number') return [scale, scale];
691
698
  if (scale && typeof scale === 'object') {
@@ -696,12 +703,34 @@
696
703
  return undefined;
697
704
  }
698
705
 
706
+ const graphicContainerScale = (graphicObject) => {
707
+ const scale = graphicObject?.displayScale;
708
+ if (Array.isArray(scale)) return scale;
709
+ if (typeof scale === 'number') return [scale, scale];
710
+ if (scale && typeof scale === 'object') {
711
+ const x = typeof scale.x === 'number' ? scale.x : 1;
712
+ const y = typeof scale.y === 'number' ? scale.y : x;
713
+ return [x, y];
714
+ }
715
+ return [1, 1];
716
+ }
717
+
718
+ const multiplyScale = (left, right) => {
719
+ const a = normalizePair(left);
720
+ const b = normalizePair(right);
721
+ return [a[0] * b[0], a[1] * b[1]];
722
+ }
723
+
724
+ const graphicHitbox = (graphicObject) => {
725
+ const box = hitbox();
726
+ const scale = graphicEffectiveScale(graphicObject);
727
+ return scaleHitboxForGraphicDisplay(box, scale);
728
+ }
729
+
699
730
  const shadowCaster = (graphicObject) => {
700
731
  const box = hitbox();
701
732
  const bounds = graphicBounds();
702
- const scale = graphicScale(graphicObject);
703
- const scaleY = Array.isArray(scale) && typeof scale[1] === 'number' ? Math.abs(scale[1]) : 1;
704
- const height = Math.max(bounds?.height ?? box?.h ?? 32, box?.h ?? 32) * scaleY;
733
+ const height = Math.max(bounds?.height ?? box?.h ?? 32, box?.h ?? 32);
705
734
 
706
735
  return {
707
736
  enabled: shadowsEnabled,
@@ -722,11 +751,6 @@
722
751
  const imageDimensions = signal({});
723
752
  const loadingImageDimensions = new Set();
724
753
 
725
- const toPositiveNumber = (value) => {
726
- const number = typeof value === 'number' ? value : parseFloat(value);
727
- return Number.isFinite(number) && number > 0 ? number : undefined;
728
- };
729
-
730
754
  const toFiniteNumber = (value, fallback = 0) => {
731
755
  const number = typeof value === 'number' ? value : parseFloat(value);
732
756
  return Number.isFinite(number) ? number : fallback;
@@ -826,6 +850,13 @@
826
850
  return frame?.[prop] ?? textureOptions?.[prop] ?? graphicObject?.[prop];
827
851
  };
828
852
 
853
+ const graphicEffectiveScale = (graphicObject) => {
854
+ const textureOptions = resolveTextureOptions(graphicObject);
855
+ const frame = resolveFirstAnimationFrame(textureOptions);
856
+ const spriteScale = normalizePair(optionValue('scale', frame, textureOptions, graphicObject) ?? graphicScale(graphicObject));
857
+ return multiplyScale(spriteScale, graphicContainerScale(graphicObject));
858
+ };
859
+
829
860
  const resolveFrameSize = (textureOptions, dimensions) => {
830
861
  const framesWidth = toPositiveNumber(textureOptions?.framesWidth) ?? 1;
831
862
  const framesHeight = toPositiveNumber(textureOptions?.framesHeight) ?? 1;
@@ -846,31 +877,6 @@
846
877
  };
847
878
  };
848
879
 
849
- const resolveHitboxAnchor = (spriteWidth, spriteHeight, realSize, box) => {
850
- if (!spriteWidth || !spriteHeight || !box) {
851
- return [0, 0];
852
- }
853
-
854
- const heightOfSprite = typeof realSize === 'number' ? realSize : realSize?.height;
855
- const resolvedHeight = toPositiveNumber(heightOfSprite) ?? spriteHeight;
856
- const gap = Math.max(0, (spriteHeight - resolvedHeight) / 2);
857
- const hitboxTopLeftX = clampRatio((spriteWidth - box.w) / 2 / spriteWidth);
858
- const hitboxTopLeftY = clampRatio((spriteHeight - box.h - gap) / spriteHeight);
859
- const hitboxCenterX = clampRatio(hitboxTopLeftX + box.w / 2 / spriteWidth);
860
- const hitboxCenterY = clampRatio(hitboxTopLeftY + box.h / 2 / spriteHeight);
861
- const footY = clampRatio((spriteHeight - gap) / spriteHeight);
862
-
863
- switch (box.anchorMode ?? 'top-left') {
864
- case 'center':
865
- return [hitboxCenterX, hitboxCenterY];
866
- case 'foot':
867
- return [hitboxCenterX, footY];
868
- case 'top-left':
869
- default:
870
- return [hitboxTopLeftX, hitboxTopLeftY];
871
- }
872
- };
873
-
874
880
  const loadImageDimensions = (image) => {
875
881
  const source = resolveImageSource(image);
876
882
  if (!source || imageDimensions()[source] || loadingImageDimensions.has(source)) {
@@ -950,14 +956,15 @@
950
956
  return;
951
957
  }
952
958
 
959
+ const scale = graphicEffectiveScale(graphicObject);
953
960
  const explicitAnchor = normalizeAnchor(optionValue('anchor', frame, textureOptions, graphicObject));
954
- const anchor = explicitAnchor ?? resolveHitboxAnchor(
961
+ const anchor = explicitAnchor ?? resolveScaledHitboxAnchor(
955
962
  spriteWidth,
956
963
  spriteHeight,
957
964
  optionValue('spriteRealSize', frame, textureOptions, graphicObject),
958
- box
965
+ box,
966
+ scale
959
967
  );
960
- const scale = normalizePair(optionValue('scale', frame, textureOptions, graphicObject) ?? graphicScale(graphicObject));
961
968
  const x = toFiniteNumber(optionValue('x', frame, textureOptions, graphicObject), 0);
962
969
  const y = toFiniteNumber(optionValue('y', frame, textureOptions, graphicObject), 0);
963
970
  const leftEdge = -anchor[0] * spriteWidth * scale[0];
@@ -1,49 +1,52 @@
1
1
  <DOMContainer width="100%" height="100%" controls={dialogControls}>
2
- <div
3
- class="rpg-ui-dialog-container"
4
- data-position={dialogPosition()}
5
- data-full-width={isFullWidth() ? "true" : "false"}
6
- data-has-face={hasFace() ? "true" : "false"}
7
- >
8
- <div class="rpg-ui-dialog rpg-anim-fade-in">
9
- <div class="rpg-ui-dialog-body">
10
- <div>
11
- @if (speakerName()) {
12
- <div class="rpg-ui-dialog-speaker">{speakerName()}</div>
13
- }
14
- <div class="rpg-ui-dialog-content">
15
- {displayMessage()}
2
+ <div class="rpg-ui-dialog-layer" click={closeDialogFromPointer}>
3
+ <button class="rpg-ui-close-button" type="button" title="Close" aria-label="Close dialog" click={closeDialogFromPointer}>x</button>
4
+ <div
5
+ class="rpg-ui-dialog-container"
6
+ data-position={dialogPosition()}
7
+ data-full-width={isFullWidth() ? "true" : "false"}
8
+ data-has-face={hasFace() ? "true" : "false"}
9
+ >
10
+ <div class="rpg-ui-dialog rpg-anim-fade-in" click={stopEvent}>
11
+ <div class="rpg-ui-dialog-body">
12
+ <div>
13
+ @if (speakerName()) {
14
+ <div class="rpg-ui-dialog-speaker">{speakerName()}</div>
15
+ }
16
+ <div class="rpg-ui-dialog-content">
17
+ {displayMessage()}
18
+ </div>
19
+ @if (hasChoices()) {
20
+ <Navigation tabindex={selectedItem} controls={controls}>
21
+ <div class="rpg-ui-dialog-choices">
22
+ @for ((choice,index) of dialogChoices()) {
23
+ <div
24
+ class="rpg-ui-dialog-choice"
25
+ class={{active: selectedItem() === index}}
26
+ tabindex={index}
27
+ data-choice-index={index}
28
+ click={selectChoice(index)}
29
+ >{{ choice.text }}</div>
30
+ }
31
+ </div>
32
+ </Navigation>
33
+ }
16
34
  </div>
17
- @if (hasChoices()) {
18
- <Navigation tabindex={selectedItem} controls={controls}>
19
- <div class="rpg-ui-dialog-choices">
20
- @for ((choice,index) of dialogChoices()) {
21
- <div
22
- class="rpg-ui-dialog-choice"
23
- class={{active: selectedItem() === index}}
24
- tabindex={index}
25
- data-choice-index={index}
26
- click={selectChoice(index)}
27
- >{{ choice.text }}</div>
28
- }
29
- </div>
30
- </Navigation>
35
+ @if (hasFace()) {
36
+ <div class="rpg-ui-dialog-face">
37
+ <DOMSprite
38
+ sheet={faceSheet(dialogFace())}
39
+ width="100%"
40
+ height="100%"
41
+ objectFit="contain"
42
+ />
43
+ </div>
31
44
  }
32
45
  </div>
33
- @if (hasFace()) {
34
- <div class="rpg-ui-dialog-face">
35
- <DOMSprite
36
- sheet={faceSheet(dialogFace())}
37
- width="100%"
38
- height="100%"
39
- objectFit="contain"
40
- />
41
- </div>
46
+ @if (showIndicator) {
47
+ <div class="rpg-ui-dialog-indicator"></div>
42
48
  }
43
49
  </div>
44
- @if (showIndicator) {
45
- <div class="rpg-ui-dialog-indicator"></div>
46
- }
47
50
  </div>
48
51
  </div>
49
52
  </DOMContainer>
@@ -160,6 +163,19 @@
160
163
  if (onFinish) onFinish(value, normalizeOpenId(guiOpenId));
161
164
  }
162
165
 
166
+ const stopEvent = (event) => {
167
+ event?.stopPropagation?.();
168
+ };
169
+
170
+ const closeDialog = () => {
171
+ _onFinish();
172
+ };
173
+
174
+ const closeDialogFromPointer = (event) => {
175
+ stopEvent(event);
176
+ closeDialog();
177
+ };
178
+
163
179
  const onSelect = (index) => {
164
180
  _onFinish(index);
165
181
  };
@@ -5,8 +5,8 @@
5
5
  <div class="rpg-avatar-face">
6
6
  <DOMSprite
7
7
  sheet={faceSheet(face())}
8
- width={64}
9
- height={64}
8
+ width="100%"
9
+ height="100%"
10
10
  objectFit="contain"
11
11
  />
12
12
  </div>
@@ -63,8 +63,9 @@
63
63
  </div>
64
64
  </div>
65
65
  @if (confirmOpen) {
66
- <div class="rpg-ui-menu-confirm">
67
- <div class="rpg-ui-menu-confirm-card">
66
+ <div class="rpg-ui-menu-confirm" click={cancelConfirmFromPointer}>
67
+ <button class="rpg-ui-close-button" type="button" title="Close" aria-label="Close confirmation" click={cancelConfirmFromPointer}>x</button>
68
+ <div class="rpg-ui-menu-confirm-card" click={stopEvent}>
68
69
  <div class="rpg-ui-menu-confirm-title">{t("rpg.menu.use")} {confirmItem()?.name}?</div>
69
70
  <Navigation tabindex={selectedConfirm} controls={confirmControls}>
70
71
  <div class="rpg-ui-menu-confirm-actions">
@@ -197,6 +198,15 @@
197
198
  });
198
199
  };
199
200
 
201
+ const stopEvent = (event) => {
202
+ event?.stopPropagation?.();
203
+ };
204
+
205
+ const cancelConfirmFromPointer = (event) => {
206
+ stopEvent(event);
207
+ cancelConfirm();
208
+ };
209
+
200
210
  function confirmSelect(index) {
201
211
  return function() {
202
212
  selectedConfirm.set(index);
@@ -1,6 +1,8 @@
1
1
  <DOMContainer width="100%" height="100%" controls={menuControls}>
2
- <div class="rpg-ui-main-menu rpg-anim-fade-in">
3
- <div class="rpg-ui-main-menu-layout">
2
+ <div class="rpg-ui-main-menu rpg-anim-fade-in" click={closeMenuFromPointer}>
3
+ <div class="rpg-ui-main-menu-backdrop"></div>
4
+ <button class="rpg-ui-close-button" type="button" title="Close" aria-label="Close menu" click={closeMenuFromPointer}>x</button>
5
+ <div class="rpg-ui-main-menu-layout" click={stopEvent}>
4
6
  <div class="rpg-ui-main-menu-left rpg-ui-menu rpg-ui-panel">
5
7
  <div class="rpg-ui-menu-header">{t("rpg.menu.title")}</div>
6
8
  <div class="rpg-ui-main-menu-list">
@@ -77,8 +79,8 @@
77
79
  </div>
78
80
  @if (saveOverlay) {
79
81
  <div class="rpg-ui-main-menu-overlay">
80
- <div class="rpg-ui-main-menu-overlay-backdrop"></div>
81
- <div class="rpg-ui-main-menu-overlay-content">
82
+ <div class="rpg-ui-main-menu-overlay-backdrop" click={closeSaveOverlayFromPointer}></div>
83
+ <div class="rpg-ui-main-menu-overlay-content" click={stopEvent}>
82
84
  <SaveLoadComponent
83
85
  data={saveLoadData}
84
86
  onFinish={closeSaveOverlay}
@@ -202,6 +204,28 @@
202
204
  });
203
205
  };
204
206
 
207
+ const stopEvent = (event) => {
208
+ event?.stopPropagation?.();
209
+ };
210
+
211
+ const closeSaveOverlayFromPointer = (event) => {
212
+ stopEvent(event);
213
+ closeSaveOverlay();
214
+ };
215
+
216
+ const closeMenu = () => {
217
+ if (saveOverlay()) {
218
+ closeSaveOverlay();
219
+ return;
220
+ }
221
+ onFinish();
222
+ };
223
+
224
+ const closeMenuFromPointer = (event) => {
225
+ stopEvent(event);
226
+ closeMenu();
227
+ };
228
+
205
229
  const confirmExit = () => {
206
230
  if (onInteraction) onInteraction("exit");
207
231
  };
@@ -285,11 +309,18 @@
285
309
  });
286
310
 
287
311
  mount((element) => {
312
+ const inputLockToken = {};
313
+ const previousStopProcessingInput = engine.stopProcessingInput;
314
+ const previousGamePause = engine.gamePause();
315
+ engine.__rpgMainMenuInputLock = inputLockToken;
316
+ engine.stopProcessingInput = true;
288
317
  engine.gamePause.set(true);
289
318
  return () => {
290
319
  delay(() => {
291
- engine.stopProcessingInput = false;
292
- engine.gamePause.set(false);
320
+ if (engine.__rpgMainMenuInputLock !== inputLockToken) return;
321
+ engine.__rpgMainMenuInputLock = undefined;
322
+ engine.stopProcessingInput = previousStopProcessingInput;
323
+ engine.gamePause.set(previousGamePause);
293
324
  });
294
325
  }
295
326
  });
@@ -1,35 +1,38 @@
1
1
  <DOMContainer width="100%" height="100%">
2
- <div class="rpg-ui-save-load rpg-anim-fade-in">
3
- <div class="rpg-ui-save-load-header">
4
- <div class="rpg-ui-save-load-title">{title()}</div>
5
- <div class="rpg-ui-save-load-subtitle">{subtitle()}</div>
6
- </div>
7
- <Navigation tabindex={selectedSlot} controls={controls}>
8
- <div class="rpg-ui-save-load-list">
9
- @for ((item,displayIndex) of displaySlots) {
10
- <div
11
- class="rpg-ui-save-load-slot"
12
- class={{active: selectedSlot() === displayIndex}}
13
- tabindex={displayIndex}
14
- data-slot-index={displayIndex}
15
- click={selectSlot(displayIndex)}
16
- >
17
- <div class="rpg-ui-save-load-slot-index">{item.label}</div>
18
- @if (item.slot) {
19
- <div class="rpg-ui-save-load-slot-meta">
20
- <div class="rpg-ui-save-load-slot-line">{t("rpg.save.level")}: {item.slot.level ?? "-"}</div>
21
- <div class="rpg-ui-save-load-slot-line">{t("rpg.save.exp")}: {item.slot.exp ?? "-"}</div>
22
- <div class="rpg-ui-save-load-slot-line">{t("rpg.save.map")}: {item.slot.map ?? "-"}</div>
23
- <div class="rpg-ui-save-load-slot-line">{t("rpg.save.date")}: {item.slot.date ?? "-"}</div>
24
- </div>
25
- }
26
- @else {
27
- <div class="rpg-ui-save-load-slot-empty">{t("rpg.save.empty-slot")}</div>
28
- }
29
- </div>
30
- }
2
+ <div class="rpg-ui-save-load-layer" click={closeSaveLoadFromPointer}>
3
+ <button class="rpg-ui-close-button" type="button" title="Close" aria-label="Close save menu" click={closeSaveLoadFromPointer}>x</button>
4
+ <div class="rpg-ui-save-load rpg-anim-fade-in" click={stopEvent}>
5
+ <div class="rpg-ui-save-load-header">
6
+ <div class="rpg-ui-save-load-title">{title()}</div>
7
+ <div class="rpg-ui-save-load-subtitle">{subtitle()}</div>
31
8
  </div>
32
- </Navigation>
9
+ <Navigation tabindex={selectedSlot} controls={controls}>
10
+ <div class="rpg-ui-save-load-list">
11
+ @for ((item,displayIndex) of displaySlots) {
12
+ <div
13
+ class="rpg-ui-save-load-slot"
14
+ class={{active: selectedSlot() === displayIndex}}
15
+ tabindex={displayIndex}
16
+ data-slot-index={displayIndex}
17
+ click={selectSlot(displayIndex)}
18
+ >
19
+ <div class="rpg-ui-save-load-slot-index">{item.label}</div>
20
+ @if (item.slot) {
21
+ <div class="rpg-ui-save-load-slot-meta">
22
+ <div class="rpg-ui-save-load-slot-line">{t("rpg.save.level")}: {item.slot.level ?? "-"}</div>
23
+ <div class="rpg-ui-save-load-slot-line">{t("rpg.save.exp")}: {item.slot.exp ?? "-"}</div>
24
+ <div class="rpg-ui-save-load-slot-line">{t("rpg.save.map")}: {item.slot.map ?? "-"}</div>
25
+ <div class="rpg-ui-save-load-slot-line">{t("rpg.save.date")}: {item.slot.date ?? "-"}</div>
26
+ </div>
27
+ }
28
+ @else {
29
+ <div class="rpg-ui-save-load-slot-empty">{t("rpg.save.empty-slot")}</div>
30
+ }
31
+ </div>
32
+ }
33
+ </div>
34
+ </Navigation>
35
+ </div>
33
36
  </div>
34
37
  </DOMContainer>
35
38
 
@@ -169,6 +172,20 @@
169
172
  }
170
173
  }
171
174
 
175
+ const stopEvent = (event) => {
176
+ event?.stopPropagation?.();
177
+ };
178
+
179
+ const closeSaveLoad = () => {
180
+ if (onFinish) onFinish();
181
+ gui.hide(PrebuiltGui.Save);
182
+ };
183
+
184
+ const closeSaveLoadFromPointer = (event) => {
185
+ stopEvent(event);
186
+ closeSaveLoad();
187
+ };
188
+
172
189
  const controls = signal({
173
190
  up: {
174
191
  repeat: true,
@@ -195,8 +212,7 @@
195
212
  escape: {
196
213
  bind: keyboardControls.escape,
197
214
  keyDown() {
198
- if (onFinish) onFinish();
199
- gui.hide(PrebuiltGui.Save);
215
+ closeSaveLoad();
200
216
  }
201
217
  },
202
218
  gamepad: {
@@ -1,5 +1,7 @@
1
1
  <DOMContainer width="100%" height="100%">
2
- <div class="rpg-shop-container rpg-anim-fade-in">
2
+ <div class="rpg-shop-layer" click={closeShopFromPointer}>
3
+ <button class="rpg-ui-close-button" type="button" title="Close" aria-label="Close shop" click={closeShopFromPointer}>x</button>
4
+ <div class="rpg-shop-container rpg-anim-fade-in" click={stopEvent}>
3
5
  <div class="rpg-shop-header">
4
6
  <div class="rpg-shop-merchant">
5
7
  <div>
@@ -143,8 +145,8 @@
143
145
  </div>
144
146
  }
145
147
  @if (quantityDialogOpen) {
146
- <div class="rpg-shop-modal">
147
- <div class="rpg-shop-modal-card">
148
+ <div class="rpg-shop-modal" click={closeQuantityDialogFromPointer}>
149
+ <div class="rpg-shop-modal-card" click={stopEvent}>
148
150
  <div class="rpg-shop-modal-title">{{ actionLabel }}</div>
149
151
  <div class="rpg-shop-modal-item">{{ currentItem()?.name || "" }}</div>
150
152
  @if (currentItem()?.quantity !== undefined) {
@@ -174,6 +176,7 @@
174
176
  </div>
175
177
  </div>
176
178
  </div>
179
+ </div>
177
180
  </DOMContainer>
178
181
 
179
182
  <script>
@@ -351,6 +354,28 @@
351
354
  }
352
355
  }
353
356
 
357
+ const stopEvent = (event) => {
358
+ event?.stopPropagation?.()
359
+ }
360
+
361
+ const closeQuantityDialogFromPointer = (event) => {
362
+ stopEvent(event)
363
+ quantityDialogOpen.set(false)
364
+ }
365
+
366
+ const closeShop = () => {
367
+ if (quantityDialogOpen()) {
368
+ quantityDialogOpen.set(false)
369
+ return
370
+ }
371
+ onFinish()
372
+ }
373
+
374
+ const closeShopFromPointer = (event) => {
375
+ stopEvent(event)
376
+ closeShop()
377
+ }
378
+
354
379
  function confirmTrade() {
355
380
  return function() {
356
381
  const item = currentItem()
@@ -51,4 +51,51 @@ describe("standalone websocket bridge", () => {
51
51
  expect(standaloneProvider.useFactory(context).mode).toBe("standalone");
52
52
  expect(mmorpgProvider.useFactory(context).mode).toBe("mmorpg");
53
53
  });
54
+
55
+ test("reconnects standalone rooms with updated room and query", async () => {
56
+ const connects: Array<{ roomId: string; url: string; sessionId: string }> = [];
57
+ class Server {
58
+ subRoom = {
59
+ onStart: vi.fn(),
60
+ };
61
+
62
+ constructor(public room: any) {}
63
+
64
+ async onStart() {}
65
+
66
+ async onConnect(conn: any, ctx: any) {
67
+ connects.push({
68
+ roomId: this.room.id,
69
+ url: ctx.request.url,
70
+ sessionId: conn.sessionId,
71
+ });
72
+ }
73
+ }
74
+
75
+ const context = new Context();
76
+ const standaloneProvider = provideRpg(Server).find(
77
+ (provider: any) => provider.provide === WebSocketToken,
78
+ ) as any;
79
+ const socket = standaloneProvider.useFactory(context);
80
+
81
+ await socket.connection();
82
+ socket.updateProperties({
83
+ room: "map-center-map",
84
+ query: { transferToken: "token-1" },
85
+ });
86
+ await socket.reconnect();
87
+
88
+ expect(connects).toEqual([
89
+ expect.objectContaining({
90
+ roomId: "lobby-1",
91
+ sessionId: "player-client-id",
92
+ }),
93
+ expect.objectContaining({
94
+ roomId: "map-center-map",
95
+ sessionId: "player-client-id",
96
+ }),
97
+ ]);
98
+ expect(connects[1].url).toContain("transferToken=token-1");
99
+ expect(socket.getServer().room.id).toBe("map-center-map");
100
+ });
54
101
  });