@rpgjs/client 5.0.0-beta.23 → 5.0.0-beta.25

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 (47) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/Game/Object.d.ts +5 -0
  3. package/dist/Game/Object.js +48 -2
  4. package/dist/Game/Object.js.map +1 -1
  5. package/dist/Gui/Gui.js +5 -0
  6. package/dist/Gui/Gui.js.map +1 -1
  7. package/dist/components/character.ce.js +9 -2
  8. package/dist/components/character.ce.js.map +1 -1
  9. package/dist/components/gui/dialogbox/index.ce.js +54 -4
  10. package/dist/components/gui/dialogbox/index.ce.js.map +1 -1
  11. package/dist/components/gui/hud/hud.ce.js +2 -2
  12. package/dist/components/gui/hud/hud.ce.js.map +1 -1
  13. package/dist/components/gui/index.d.ts +3 -1
  14. package/dist/components/gui/index.js +2 -0
  15. package/dist/components/gui/input-field.ce.js +123 -0
  16. package/dist/components/gui/input-field.ce.js.map +1 -0
  17. package/dist/components/gui/input.ce.js +40 -0
  18. package/dist/components/gui/input.ce.js.map +1 -0
  19. package/dist/components/gui/menu/items-menu.ce.js +27 -4
  20. package/dist/components/gui/menu/items-menu.ce.js.map +1 -1
  21. package/dist/components/gui/menu/main-menu.ce.js +210 -155
  22. package/dist/components/gui/menu/main-menu.ce.js.map +1 -1
  23. package/dist/components/gui/save-load.ce.js +33 -4
  24. package/dist/components/gui/save-load.ce.js.map +1 -1
  25. package/dist/components/gui/shop/shop.ce.js +47 -4
  26. package/dist/components/gui/shop/shop.ce.js.map +1 -1
  27. package/dist/i18n.d.ts +11 -0
  28. package/dist/i18n.js +11 -0
  29. package/dist/i18n.js.map +1 -1
  30. package/dist/index.js +13 -11
  31. package/package.json +4 -4
  32. package/src/Game/Object.spec.ts +26 -2
  33. package/src/Game/Object.ts +71 -2
  34. package/src/Gui/Gui.spec.ts +5 -0
  35. package/src/Gui/Gui.ts +5 -1
  36. package/src/components/character.ce +12 -6
  37. package/src/components/gui/dialogbox/index.ce +82 -41
  38. package/src/components/gui/hud/hud.ce +2 -2
  39. package/src/components/gui/index.ts +5 -1
  40. package/src/components/gui/input-field.ce +106 -0
  41. package/src/components/gui/input.ce +26 -0
  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/i18n.ts +11 -0
  47. package/vite.config.ts +5 -2
@@ -1,49 +1,61 @@
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
+ }
34
+ @if (hasInput()) {
35
+ <InputField
36
+ data={dialogInput}
37
+ layout="dialog"
38
+ showMessage={false}
39
+ submit={submitInput}
40
+ cancel={cancelInput}
41
+ />
42
+ }
16
43
  </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>
44
+ @if (hasFace()) {
45
+ <div class="rpg-ui-dialog-face">
46
+ <DOMSprite
47
+ sheet={faceSheet(dialogFace())}
48
+ width="100%"
49
+ height="100%"
50
+ objectFit="contain"
51
+ />
52
+ </div>
31
53
  }
32
54
  </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>
55
+ @if (showIndicator) {
56
+ <div class="rpg-ui-dialog-indicator"></div>
42
57
  }
43
58
  </div>
44
- @if (showIndicator) {
45
- <div class="rpg-ui-dialog-indicator"></div>
46
- }
47
59
  </div>
48
60
  </div>
49
61
  </DOMContainer>
@@ -54,6 +66,7 @@
54
66
  import { RpgClientEngine } from "../../../RpgClientEngine";
55
67
  import { delay } from "@rpgjs/common";
56
68
  import { getKeyboardControlBind } from "../../../services/actionInput";
69
+ import InputField from "../input-field.ce";
57
70
 
58
71
  const engine = inject(RpgClientEngine);
59
72
  const currentPlayer = engine.scene.currentPlayer
@@ -79,6 +92,8 @@
79
92
  const position = computed(() => dialogData().position);
80
93
  const typewriterEffect = computed(() => dialogData().typewriterEffect);
81
94
  const fullWidth = computed(() => dialogData().fullWidth || false);
95
+ const dialogInput = computed(() => dialogData().input || null);
96
+ const hasInput = computed(() => !!dialogInput());
82
97
 
83
98
  const resolveProp = (value) => typeof value === "function" ? value() : value;
84
99
  const normalizeOpenId = (value) => {
@@ -146,7 +161,7 @@
146
161
 
147
162
 
148
163
  const hasChoices = computed(() => dialogChoices().length > 0);
149
- const showIndicator = computed(() => !hasChoices() && !isTyping());
164
+ const showIndicator = computed(() => !hasChoices() && !hasInput() && !isTyping());
150
165
  const nav = createTabindexNavigator(selectedItem, { count: () => dialogChoices().length }, 'wrap');
151
166
 
152
167
  function selectChoice(index) {
@@ -160,10 +175,35 @@
160
175
  if (onFinish) onFinish(value, normalizeOpenId(guiOpenId));
161
176
  }
162
177
 
178
+ const stopEvent = (event) => {
179
+ event?.stopPropagation?.();
180
+ };
181
+
182
+ const closeDialog = () => {
183
+ if (hasInput()) {
184
+ onInteraction?.("cancel");
185
+ return;
186
+ }
187
+ _onFinish();
188
+ };
189
+
190
+ const closeDialogFromPointer = (event) => {
191
+ stopEvent(event);
192
+ closeDialog();
193
+ };
194
+
163
195
  const onSelect = (index) => {
164
196
  _onFinish(index);
165
197
  };
166
198
 
199
+ const submitInput = (payload) => {
200
+ onInteraction?.("submit", payload);
201
+ };
202
+
203
+ const cancelInput = () => {
204
+ onInteraction?.("cancel");
205
+ };
206
+
167
207
  const canAcceptAction = () => Date.now() - openedAt >= ACTION_OPEN_GUARD_MS;
168
208
 
169
209
  const controls = signal({
@@ -215,6 +255,7 @@
215
255
  finishTyping();
216
256
  return;
217
257
  }
258
+ if (hasInput()) return;
218
259
  if (hasChoices()) return;
219
260
  _onFinish();
220
261
  }
@@ -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>
@@ -12,6 +12,8 @@ import ExitMenuComponent from "./menu/exit-menu.ce";
12
12
  import NotificationComponent from "./notification/notification.ce";
13
13
  import TitleScreenComponent from "./title-screen.ce";
14
14
  import GameoverComponent from "./gameover.ce";
15
+ import InputComponent from "./input.ce";
16
+ import InputFieldComponent from "./input-field.ce";
15
17
 
16
18
  export {
17
19
  DialogboxComponent,
@@ -27,5 +29,7 @@ export {
27
29
  ExitMenuComponent,
28
30
  NotificationComponent,
29
31
  TitleScreenComponent,
30
- GameoverComponent
32
+ GameoverComponent,
33
+ InputComponent,
34
+ InputFieldComponent
31
35
  }
@@ -0,0 +1,106 @@
1
+ <form
2
+ class="rpg-ui-input-form"
3
+ data-layout={layout()}
4
+ submit={submitForm}
5
+ keydown={handleKeydown}
6
+ >
7
+ @if (showMessage()) {
8
+ <label class="rpg-ui-input-message" for={fieldId()}>{formData().message}</label>
9
+ }
10
+ @if (formData().control === "textarea") {
11
+ <textarea
12
+ id={fieldId()}
13
+ class="rpg-ui-input rpg-ui-input-dialog-field"
14
+ name="value"
15
+ value={value}
16
+ placeholder={formData().placeholder || ""}
17
+ rows={formData().rows || 4}
18
+ minlength={formData().minLength}
19
+ maxlength={formData().maxLength}
20
+ aria-required={formData().required ? "true" : "false"}
21
+ ></textarea>
22
+ }
23
+ @else {
24
+ <input
25
+ id={fieldId()}
26
+ class="rpg-ui-input rpg-ui-input-dialog-field"
27
+ name="value"
28
+ type={formData().type || "text"}
29
+ value={value}
30
+ placeholder={formData().placeholder || ""}
31
+ min={formData().min}
32
+ max={formData().max}
33
+ step={formData().step}
34
+ minlength={formData().minLength}
35
+ maxlength={formData().maxLength}
36
+ aria-required={formData().required ? "true" : "false"}
37
+ />
38
+ }
39
+ @if (errorText()) {
40
+ <div class="rpg-ui-input-error" role="alert">{errorText()}</div>
41
+ }
42
+ <div class="rpg-ui-input-actions">
43
+ @if (showCancelButton()) {
44
+ <button class="rpg-ui-btn rpg-ui-input-button rpg-ui-input-button-cancel" type="button" click={cancelForm}>
45
+ {formData().cancelText || t("rpg.input.cancel")}
46
+ </button>
47
+ }
48
+ <button
49
+ class="rpg-ui-btn rpg-ui-input-button rpg-ui-input-button-confirm"
50
+ data-variant="primary"
51
+ type="submit"
52
+ >
53
+ {formData().confirmText || t("rpg.input.confirm")}
54
+ </button>
55
+ </div>
56
+ </form>
57
+
58
+ <script>
59
+ import { computed, effect, signal } from "canvasengine";
60
+ import { inject } from "../../core/inject";
61
+ import { RpgClientEngine } from "../../RpgClientEngine";
62
+
63
+ const engine = inject(RpgClientEngine);
64
+ const { t } = engine.i18n();
65
+ const { data, layout, showMessage } = defineProps({
66
+ layout: "standalone",
67
+ showMessage: true,
68
+ });
69
+ const { submit, cancel } = defineEmits();
70
+ const formData = computed(() => data() || {});
71
+ const value = signal(String(formData().defaultValue ?? ""));
72
+ const fieldId = computed(() => `rpg-input-value-${layout()}`);
73
+ const errorText = computed(() => formData().errorKey
74
+ ? t(formData().errorKey, formData().errorParams || {})
75
+ : ""
76
+ );
77
+ const showCancelButton = computed(() => formData().cancelButton !== false);
78
+ let focused = false;
79
+
80
+ function submitForm(event, collectedData) {
81
+ const submittedValue = collectedData && Object.prototype.hasOwnProperty.call(collectedData, "value")
82
+ ? collectedData.value
83
+ : event?.currentTarget?.elements?.value?.value ?? value();
84
+ value.set(String(submittedValue ?? ""));
85
+ submit({ value: submittedValue ?? "" });
86
+ }
87
+
88
+ function cancelForm() {
89
+ cancel();
90
+ }
91
+
92
+ function handleKeydown(event) {
93
+ if (event?.key !== "Escape") return;
94
+ event.preventDefault?.();
95
+ event.stopPropagation?.();
96
+ cancelForm();
97
+ }
98
+
99
+ effect(() => {
100
+ if (focused) return;
101
+ focused = true;
102
+ setTimeout(() => {
103
+ document.getElementById(fieldId())?.focus?.();
104
+ });
105
+ });
106
+ </script>
@@ -0,0 +1,26 @@
1
+ <DOMContainer width="100%" height="100%">
2
+ <div class="rpg-ui-input-layer">
3
+ <div class="rpg-ui-input-dialog rpg-ui-window rpg-anim-fade-in">
4
+ @if (title()) {
5
+ <div class="rpg-ui-window-title">{title()}</div>
6
+ }
7
+ <InputField data={data} layout="standalone" submit={submit} cancel={cancel} />
8
+ </div>
9
+ </div>
10
+ </DOMContainer>
11
+
12
+ <script>
13
+ import { computed } from "canvasengine";
14
+ import InputField from "./input-field.ce";
15
+
16
+ const { data, onInteraction } = defineProps();
17
+ const title = computed(() => data().title || "");
18
+
19
+ function submit(payload) {
20
+ onInteraction?.("submit", payload);
21
+ }
22
+
23
+ function cancel() {
24
+ onInteraction?.("cancel");
25
+ }
26
+ </script>
@@ -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()
package/src/i18n.ts CHANGED
@@ -40,6 +40,17 @@ export const RpgClientBuiltinI18n = {
40
40
  "rpg.gameover.title": "Game Over",
41
41
  "rpg.gameover.title-screen": "Title Screen",
42
42
  "rpg.gameover.load-game": "Load Game",
43
+ "rpg.input.confirm": "Confirm",
44
+ "rpg.input.cancel": "Cancel",
45
+ "rpg.input.error.invalid": "Invalid value.",
46
+ "rpg.input.error.required": "A value is required.",
47
+ "rpg.input.error.number": "Enter a valid number.",
48
+ "rpg.input.error.min": "The value must be at least {min}.",
49
+ "rpg.input.error.max": "The value must be at most {max}.",
50
+ "rpg.input.error.step": "The value must use increments of {step}.",
51
+ "rpg.input.error.min-length": "Enter at least {minLength} characters.",
52
+ "rpg.input.error.max-length": "Enter no more than {maxLength} characters.",
53
+ "rpg.input.error.email": "Enter a valid email address.",
43
54
  "rpg.shop.default-message": "Welcome to my shop!",
44
55
  "rpg.shop.choose-action": "Choose an action",
45
56
  "rpg.shop.buy": "Buy",
package/vite.config.ts CHANGED
@@ -11,7 +11,10 @@ export default defineConfig({
11
11
  canvasengine(),
12
12
  dts({
13
13
  include: ['src/**/*.ts'],
14
- outDir: 'dist'
14
+ outDirs: 'dist',
15
+ afterDiagnostic(diagnostics) {
16
+ if (diagnostics.length > 0) throw new Error(`Declaration generation failed with ${diagnostics.length} TypeScript diagnostic(s)`)
17
+ }
15
18
  })
16
19
  ],
17
20
  build: {
@@ -33,4 +36,4 @@ export default defineConfig({
33
36
  }
34
37
  },
35
38
  },
36
- })
39
+ })