@rpgjs/client 5.0.0-beta.23 → 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.
@@ -29,12 +29,49 @@ type ConfigurableTrigger<T> = Omit<Trigger<T>, "start"> & {
29
29
  start(config?: T): Promise<void>;
30
30
  };
31
31
 
32
+ const toFiniteScale = (value: unknown, fallback: number): number => {
33
+ if (typeof value === "number" && Number.isFinite(value)) return value;
34
+ if (typeof value === "string" && value.trim()) {
35
+ const parsed = Number(value);
36
+ return Number.isFinite(parsed) ? parsed : fallback;
37
+ }
38
+ return fallback;
39
+ };
40
+
41
+ const normalizeGraphicScale = (value: unknown, fallback: [number, number] = [1, 1]): [number, number] => {
42
+ if (typeof value === "number" || typeof value === "string") {
43
+ const scale = toFiniteScale(value, fallback[0]);
44
+ return [scale, scale];
45
+ }
46
+ if (Array.isArray(value)) {
47
+ const x = toFiniteScale(value[0], fallback[0]);
48
+ const y = toFiniteScale(value[1] ?? value[0], x);
49
+ return [x, y];
50
+ }
51
+ if (value && typeof value === "object") {
52
+ const scale = value as { x?: unknown; y?: unknown };
53
+ const x = toFiniteScale(scale.x, fallback[0]);
54
+ const y = toFiniteScale(scale.y ?? scale.x, x);
55
+ return [x, y];
56
+ }
57
+ return fallback;
58
+ };
59
+
60
+ export const multiplyGraphicDisplayScale = (
61
+ baseScale: unknown,
62
+ instanceScale: unknown,
63
+ ): [number, number] => {
64
+ const base = normalizeGraphicScale(baseScale);
65
+ const instance = normalizeGraphicScale(instanceScale);
66
+ return [base[0] * instance[0], base[1] * instance[1]];
67
+ };
68
+
32
69
  export const withGraphicDisplayScale = (spritesheet: any, scale: unknown): any => {
33
70
  if (!spritesheet || typeof spritesheet !== "object") return spritesheet;
34
71
  if (scale === undefined || scale === null) return spritesheet;
35
72
  return {
36
73
  ...spritesheet,
37
- displayScale: scale,
74
+ displayScale: multiplyGraphicDisplayScale(spritesheet.displayScale, scale),
38
75
  };
39
76
  };
40
77
 
@@ -47,6 +84,24 @@ export const appendFramePayload = (current: unknown, items: unknown): Frame[] =>
47
84
  return currentFrames.concat(nextFrames);
48
85
  };
49
86
 
87
+ export const getFrameTimestamp = (frame: Frame): number => {
88
+ const timestamp = Number(frame.ts);
89
+ return Number.isFinite(timestamp) ? timestamp : 0;
90
+ };
91
+
92
+ export const mergeFreshFramePayload = (
93
+ current: unknown,
94
+ items: unknown,
95
+ lastAppliedFrameTs = 0,
96
+ ): Frame[] => {
97
+ return appendFramePayload(current, items)
98
+ .filter((frame) => {
99
+ const timestamp = getFrameTimestamp(frame);
100
+ return timestamp === 0 || timestamp > lastAppliedFrameTs;
101
+ })
102
+ .sort((a, b) => getFrameTimestamp(a) - getFrameTimestamp(b));
103
+ };
104
+
50
105
  export abstract class RpgClientObject extends RpgCommonPlayer {
51
106
  abstract _type: string;
52
107
  emitParticleTrigger = trigger();
@@ -57,6 +112,7 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
57
112
  frames: Frame[] = [];
58
113
  graphicsSignals = signal<any[]>([]);
59
114
  flashTrigger: ConfigurableTrigger<FlashTriggerOptions> = trigger<FlashTriggerOptions>();
115
+ private lastAppliedFrameTs = 0;
60
116
  private animationRestoreState?: {
61
117
  animationName: string;
62
118
  graphics: any[];
@@ -70,7 +126,11 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
70
126
  this._frames.observable.subscribe(({ items }) => {
71
127
  if (!this.id) return;
72
128
  //if (this.id == this.engine.playerIdSignal()!) return;
73
- this.frames = appendFramePayload(this.frames, items);
129
+ this.frames = mergeFreshFramePayload(
130
+ this.frames,
131
+ items,
132
+ this.lastAppliedFrameTs,
133
+ );
74
134
  });
75
135
 
76
136
  const graphics$ = this.graphics.observable.pipe(map(({ items }) => items));
@@ -102,16 +162,25 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
102
162
  const frame = this.frames.shift();
103
163
  if (frame) {
104
164
  if (typeof frame.x !== "number" || typeof frame.y !== "number") return;
165
+ const frameTs = this.getFrameTimestamp(frame);
166
+ if (frameTs > 0 && frameTs <= this.lastAppliedFrameTs) return;
105
167
  engine.scene.setBodyPosition(
106
168
  this.id,
107
169
  frame.x,
108
170
  frame.y,
109
171
  "top-left"
110
172
  );
173
+ if (frameTs > this.lastAppliedFrameTs) {
174
+ this.lastAppliedFrameTs = frameTs;
175
+ }
111
176
  }
112
177
  });
113
178
  }
114
179
 
180
+ private getFrameTimestamp(frame: Frame): number {
181
+ return getFrameTimestamp(frame);
182
+ }
183
+
115
184
  /**
116
185
  * Access the shared client hook registry.
117
186
  *
@@ -521,6 +521,16 @@
521
521
  });
522
522
  };
523
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
+
524
534
  const actionBind = () => getKeyboardControlBind(keyboardControls.action);
525
535
  const keyboardEventId = (event) => `${event.keyCode}:${event.code}:${event.key}`;
526
536
 
@@ -616,17 +626,13 @@
616
626
  back: {
617
627
  bind: keyboardControls.escape,
618
628
  keyDown() {
619
- if (canControls()) {
620
- client.processAction({ action: 'escape' })
621
- }
629
+ processEscapeInput()
622
630
  },
623
631
  },
624
632
  escape: {
625
633
  bind: keyboardControls.escape,
626
634
  keyDown() {
627
- if (canControls()) {
628
- client.processAction({ action: 'escape' })
629
- }
635
+ processEscapeInput()
630
636
  },
631
637
  },
632
638
  joystick: {
@@ -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()