@rpgjs/client 5.0.0-beta.11 → 5.0.0-beta.13

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 (133) hide show
  1. package/CHANGELOG.md +19 -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.d.ts +2 -0
  16. package/dist/Game/Object.js +22 -8
  17. package/dist/Game/Object.js.map +1 -1
  18. package/dist/Game/Object.spec.d.ts +1 -0
  19. package/dist/Game/ProjectileManager.d.ts +11 -2
  20. package/dist/Game/ProjectileManager.js +19 -2
  21. package/dist/Game/ProjectileManager.js.map +1 -1
  22. package/dist/Gui/Gui.d.ts +3 -2
  23. package/dist/Gui/Gui.js +18 -6
  24. package/dist/Gui/Gui.js.map +1 -1
  25. package/dist/RpgClient.d.ts +85 -1
  26. package/dist/RpgClientEngine.d.ts +77 -2
  27. package/dist/RpgClientEngine.js +290 -31
  28. package/dist/RpgClientEngine.js.map +1 -1
  29. package/dist/components/animations/fx.ce.js +58 -0
  30. package/dist/components/animations/fx.ce.js.map +1 -0
  31. package/dist/components/animations/index.d.ts +1 -0
  32. package/dist/components/animations/index.js +3 -1
  33. package/dist/components/animations/index.js.map +1 -1
  34. package/dist/components/character.ce.js +192 -19
  35. package/dist/components/character.ce.js.map +1 -1
  36. package/dist/components/gui/dialogbox/index.ce.js +27 -12
  37. package/dist/components/gui/dialogbox/index.ce.js.map +1 -1
  38. package/dist/components/gui/gameover.ce.js +4 -3
  39. package/dist/components/gui/gameover.ce.js.map +1 -1
  40. package/dist/components/gui/menu/equip-menu.ce.js +9 -8
  41. package/dist/components/gui/menu/equip-menu.ce.js.map +1 -1
  42. package/dist/components/gui/menu/exit-menu.ce.js +7 -5
  43. package/dist/components/gui/menu/exit-menu.ce.js.map +1 -1
  44. package/dist/components/gui/menu/items-menu.ce.js +8 -7
  45. package/dist/components/gui/menu/items-menu.ce.js.map +1 -1
  46. package/dist/components/gui/menu/main-menu.ce.js +12 -11
  47. package/dist/components/gui/menu/main-menu.ce.js.map +1 -1
  48. package/dist/components/gui/menu/options-menu.ce.js +7 -5
  49. package/dist/components/gui/menu/options-menu.ce.js.map +1 -1
  50. package/dist/components/gui/menu/skills-menu.ce.js +4 -2
  51. package/dist/components/gui/menu/skills-menu.ce.js.map +1 -1
  52. package/dist/components/gui/notification/notification.ce.js +4 -1
  53. package/dist/components/gui/notification/notification.ce.js.map +1 -1
  54. package/dist/components/gui/save-load.ce.js +10 -9
  55. package/dist/components/gui/save-load.ce.js.map +1 -1
  56. package/dist/components/gui/shop/shop.ce.js +17 -16
  57. package/dist/components/gui/shop/shop.ce.js.map +1 -1
  58. package/dist/components/gui/title-screen.ce.js +4 -3
  59. package/dist/components/gui/title-screen.ce.js.map +1 -1
  60. package/dist/components/interaction-components.ce.js +20 -0
  61. package/dist/components/interaction-components.ce.js.map +1 -0
  62. package/dist/components/scenes/canvas.ce.js +12 -7
  63. package/dist/components/scenes/canvas.ce.js.map +1 -1
  64. package/dist/components/scenes/draw-map.ce.js +18 -13
  65. package/dist/components/scenes/draw-map.ce.js.map +1 -1
  66. package/dist/i18n.d.ts +55 -0
  67. package/dist/i18n.js +60 -0
  68. package/dist/i18n.js.map +1 -0
  69. package/dist/i18n.spec.d.ts +1 -0
  70. package/dist/index.d.ts +3 -0
  71. package/dist/index.js +5 -2
  72. package/dist/module.js +30 -3
  73. package/dist/module.js.map +1 -1
  74. package/dist/services/actionInput.d.ts +3 -1
  75. package/dist/services/actionInput.js +33 -1
  76. package/dist/services/actionInput.js.map +1 -1
  77. package/dist/services/interactions.d.ts +159 -0
  78. package/dist/services/interactions.js +460 -0
  79. package/dist/services/interactions.js.map +1 -0
  80. package/dist/services/interactions.spec.d.ts +1 -0
  81. package/dist/services/keyboardControls.d.ts +1 -0
  82. package/dist/services/keyboardControls.js +1 -0
  83. package/dist/services/keyboardControls.js.map +1 -1
  84. package/dist/services/standalone.d.ts +3 -1
  85. package/dist/services/standalone.js +31 -13
  86. package/dist/services/standalone.js.map +1 -1
  87. package/dist/utils/mapId.d.ts +1 -0
  88. package/dist/utils/mapId.js +6 -0
  89. package/dist/utils/mapId.js.map +1 -0
  90. package/package.json +4 -4
  91. package/src/Game/AnimationManager.ts +4 -0
  92. package/src/Game/ClientVisuals.spec.ts +56 -0
  93. package/src/Game/ClientVisuals.ts +184 -0
  94. package/src/Game/EventComponentResolver.spec.ts +84 -0
  95. package/src/Game/EventComponentResolver.ts +74 -0
  96. package/src/Game/Map.ts +10 -0
  97. package/src/Game/Object.spec.ts +59 -0
  98. package/src/Game/Object.ts +36 -12
  99. package/src/Game/ProjectileManager.spec.ts +111 -0
  100. package/src/Game/ProjectileManager.ts +24 -2
  101. package/src/Gui/Gui.spec.ts +67 -0
  102. package/src/Gui/Gui.ts +24 -7
  103. package/src/RpgClient.ts +96 -1
  104. package/src/RpgClientEngine.ts +378 -45
  105. package/src/components/animations/fx.ce +101 -0
  106. package/src/components/animations/index.ts +4 -2
  107. package/src/components/character.ce +243 -17
  108. package/src/components/gui/dialogbox/index.ce +35 -14
  109. package/src/components/gui/gameover.ce +4 -3
  110. package/src/components/gui/menu/equip-menu.ce +9 -8
  111. package/src/components/gui/menu/exit-menu.ce +4 -3
  112. package/src/components/gui/menu/items-menu.ce +8 -7
  113. package/src/components/gui/menu/main-menu.ce +12 -11
  114. package/src/components/gui/menu/options-menu.ce +4 -3
  115. package/src/components/gui/menu/skills-menu.ce +2 -1
  116. package/src/components/gui/notification/notification.ce +7 -1
  117. package/src/components/gui/save-load.ce +11 -10
  118. package/src/components/gui/shop/shop.ce +17 -16
  119. package/src/components/gui/title-screen.ce +4 -3
  120. package/src/components/interaction-components.ce +23 -0
  121. package/src/components/scenes/canvas.ce +12 -7
  122. package/src/components/scenes/draw-map.ce +16 -5
  123. package/src/i18n.spec.ts +39 -0
  124. package/src/i18n.ts +59 -0
  125. package/src/index.ts +3 -0
  126. package/src/module.ts +43 -10
  127. package/src/services/actionInput.spec.ts +54 -0
  128. package/src/services/actionInput.ts +68 -1
  129. package/src/services/interactions.spec.ts +175 -0
  130. package/src/services/interactions.ts +722 -0
  131. package/src/services/keyboardControls.ts +2 -1
  132. package/src/services/standalone.ts +39 -10
  133. package/src/utils/mapId.ts +2 -0
@@ -0,0 +1,101 @@
1
+ <Fx
2
+ name={name}
3
+ preset={preset}
4
+ trigger={trigger}
5
+ autostart={autostart}
6
+ loop={loop}
7
+ enabled={enabled}
8
+ x={x}
9
+ y={y}
10
+ rotation={rotation}
11
+ scale={scale}
12
+ alpha={alpha}
13
+ timeScale={timeScale}
14
+ maxParticles={maxParticles}
15
+ preload={preload}
16
+ missingTexture={missingTexture}
17
+ zIndex={zIndex}
18
+ onStart={onStart}
19
+ onComplete={finish}
20
+ onParticleSpawn={onParticleSpawn}
21
+ />
22
+
23
+ <script>
24
+ import { tick } from "canvasengine";
25
+ import { Fx } from "@canvasengine/presets";
26
+
27
+ const {
28
+ name,
29
+ preset,
30
+ trigger,
31
+ onFinish,
32
+ onStart,
33
+ onComplete,
34
+ onParticleSpawn,
35
+ displayDuration,
36
+ duration,
37
+ autostart,
38
+ loop,
39
+ enabled,
40
+ x,
41
+ y,
42
+ rotation,
43
+ scale,
44
+ alpha,
45
+ timeScale,
46
+ maxParticles,
47
+ preload,
48
+ missingTexture,
49
+ zIndex,
50
+ } = defineProps({
51
+ autostart: {
52
+ default: true,
53
+ },
54
+ loop: {
55
+ default: false,
56
+ },
57
+ enabled: {
58
+ default: true,
59
+ },
60
+ rotation: {
61
+ default: 0,
62
+ },
63
+ scale: {
64
+ default: 1,
65
+ },
66
+ alpha: {
67
+ default: 1,
68
+ },
69
+ timeScale: {
70
+ default: 1,
71
+ },
72
+ maxParticles: {
73
+ default: 600,
74
+ },
75
+ preload: {
76
+ default: true,
77
+ },
78
+ missingTexture: {
79
+ default: "shape",
80
+ },
81
+ });
82
+
83
+ let elapsedTime = 0;
84
+ let finished = false;
85
+
86
+ function finish(instance) {
87
+ if (finished) return;
88
+ finished = true;
89
+ onComplete?.(instance);
90
+ onFinish?.(instance);
91
+ }
92
+
93
+ tick(({ deltaTime }) => {
94
+ const maxDuration = displayDuration?.() ?? (loop() ? duration?.() : undefined);
95
+ if (!maxDuration || finished) return;
96
+ elapsedTime += deltaTime;
97
+ if (elapsedTime >= maxDuration) {
98
+ finish();
99
+ }
100
+ });
101
+ </script>
@@ -1,7 +1,9 @@
1
1
  import Hit from "./hit.ce";
2
2
  import Animation from "./animation.ce";
3
+ import Fx from "./fx.ce";
3
4
 
4
5
  export const PrebuiltComponentAnimations = {
5
6
  Hit,
6
- Animation
7
- }
7
+ Animation,
8
+ Fx
9
+ }
@@ -1,4 +1,19 @@
1
- <Container x={smoothX} y={smoothY} zIndex={z} viewportFollow={shouldFollowCamera} controls onBeforeDestroy visible>
1
+ <Container
2
+ x={smoothX}
3
+ y={smoothY}
4
+ zIndex={z}
5
+ viewportFollow={shouldFollowCamera}
6
+ controls
7
+ onBeforeDestroy
8
+ visible
9
+ cursor={interactionCursor}
10
+ pointerover={interactionPointerOver}
11
+ pointerout={interactionPointerOut}
12
+ pointerdown={interactionPointerDown}
13
+ pointerup={interactionPointerUp}
14
+ pointermove={interactionPointerMove}
15
+ click={interactionClick}
16
+ >
2
17
  @for (compConfig of normalizedComponentsBehind) {
3
18
  <Container>
4
19
  <compConfig.component object={sprite} ...compConfig.props />
@@ -8,7 +23,7 @@
8
23
  <PlayerComponents object={sprite} position="left" graphicBounds />
9
24
  <Particle emit={emitParticleTrigger} settings={particleSettings} zIndex={1000} name={particleName} />
10
25
  <Container>
11
- @for (graphicObj of graphicsSignals) {
26
+ @for (graphicObj of renderedGraphics) {
12
27
  <Container scale={graphicScale(graphicObj)}>
13
28
  <Sprite
14
29
  sheet={sheet(graphicObj)}
@@ -20,6 +35,11 @@
20
35
  />
21
36
  </Container>
22
37
  }
38
+ @for (eventComponent of resolvedEventComponents) {
39
+ <Container dependencies={eventComponent.dependencies}>
40
+ <eventComponent.component ...eventComponent.props />
41
+ </Container>
42
+ }
23
43
  </Container>
24
44
  <PlayerComponents object={sprite} position="center" graphicBounds />
25
45
  <PlayerComponents object={sprite} position="right" graphicBounds />
@@ -29,11 +49,17 @@
29
49
  <compConfig.component object={sprite} ...compConfig.props />
30
50
  </Container>
31
51
  }
52
+ <InteractionComponents
53
+ object={sprite}
54
+ bounds={graphicBounds}
55
+ hitboxBounds={hitboxBounds}
56
+ graphicBounds={graphicBounds}
57
+ />
32
58
  @for (attachedGui of attachedGuis) {
33
59
  @if (shouldDisplayAttachedGui) {
34
60
  <Container>
35
- <attachedGui.component ...attachedGui.data() dependencies={attachedGui.dependencies} object={sprite} onFinish={(data) => {
36
- onAttachedGuiFinish(attachedGui, data)
61
+ <attachedGui.component ...attachedGui.data() dependencies={attachedGui.dependencies} object={sprite} guiOpenId={attachedGui.openId} onFinish={(data, guiOpenId) => {
62
+ onAttachedGuiFinish(attachedGui, data, guiOpenId)
37
63
  }} onInteraction={(name, data) => {
38
64
  onAttachedGuiInteraction(attachedGui, name, data)
39
65
  }} />
@@ -52,11 +78,18 @@
52
78
  import { RpgClientEngine } from "../RpgClientEngine";
53
79
  import { inject } from "../core/inject";
54
80
  import { Direction, Animation } from "@rpgjs/common";
81
+ import { normalizeEventComponent } from "../Game/EventComponentResolver";
55
82
  import Hit from "./effects/hit.ce";
56
83
  import PlayerComponents from "./player-components.ce";
84
+ import InteractionComponents from "./interaction-components.ce";
57
85
  import { RpgGui } from "../Gui/Gui";
58
86
  import { getCanMoveValue } from "../utils/readPropValue";
59
- import { getKeyboardControlBind, resolveKeyboardActionInput } from "../services/actionInput";
87
+ import {
88
+ getKeyboardControlBind,
89
+ keyboardEventMatchesBind,
90
+ resolveKeyboardActionInput,
91
+ resolveKeyboardDirectionInput,
92
+ } from "../services/actionInput";
60
93
 
61
94
  const { object, id } = defineProps();
62
95
  const sprite = object();
@@ -213,6 +246,18 @@
213
246
  const normalizedComponentsInFront = computed(() => {
214
247
  return normalizeComponents(componentsInFront());
215
248
  });
249
+
250
+ const isEventSprite = () => {
251
+ return typeof sprite?.isEvent === 'function'
252
+ ? sprite.isEvent()
253
+ : sprite?._type === 'event';
254
+ };
255
+
256
+ const resolvedEventComponents = computed(() => {
257
+ if (!isEventSprite()) return [];
258
+ const eventComponent = normalizeEventComponent(client.resolveEventComponent(sprite), sprite);
259
+ return eventComponent ? [eventComponent] : [];
260
+ });
216
261
 
217
262
  /**
218
263
  * Determine if the camera should follow this sprite
@@ -263,6 +308,12 @@
263
308
  flashTrigger
264
309
  } = sprite;
265
310
 
311
+ const renderedGraphics = computed(() => {
312
+ const eventComponent = resolvedEventComponents()[0];
313
+ if (eventComponent && !eventComponent.renderGraphic) return [];
314
+ return graphicsSignals();
315
+ });
316
+
266
317
  /**
267
318
  * Flash configuration signals for dynamic options
268
319
  * These signals are updated when the flash trigger is activated with options
@@ -308,6 +359,108 @@
308
359
 
309
360
  const canControls = () => isMe() && getCanMoveValue(sprite)
310
361
  const keyboardControls = client.globalConfig.keyboardControls;
362
+ const activeDirectionKeys = new Map();
363
+
364
+ const resolveHeldDirection = () => {
365
+ const directions = Array.from(activeDirectionKeys.values());
366
+ return directions[directions.length - 1];
367
+ };
368
+
369
+ const resolveSpriteDirection = () => {
370
+ const heldDirection = resolveHeldDirection();
371
+ if (heldDirection) return heldDirection;
372
+ if (typeof sprite.getDirection === 'function') return sprite.getDirection();
373
+ if (typeof sprite.direction === 'function') return sprite.direction();
374
+ return direction();
375
+ };
376
+
377
+ const directionToDashVector = (currentDirection) => {
378
+ switch (currentDirection) {
379
+ case Direction.Left:
380
+ return { x: -1, y: 0 };
381
+ case Direction.Right:
382
+ return { x: 1, y: 0 };
383
+ case Direction.Up:
384
+ return { x: 0, y: -1 };
385
+ case Direction.Down:
386
+ default:
387
+ return { x: 0, y: 1 };
388
+ }
389
+ };
390
+
391
+ const withCurrentDirection = (payload) => {
392
+ if (payload.action !== 'action') return payload;
393
+ const data = payload.data && typeof payload.data === 'object'
394
+ ? { ...payload.data }
395
+ : {};
396
+ return {
397
+ ...payload,
398
+ data: {
399
+ ...data,
400
+ direction: data.direction ?? resolveSpriteDirection(),
401
+ },
402
+ };
403
+ };
404
+
405
+ const resolveCurrentActionInput = () =>
406
+ withCurrentDirection(
407
+ resolveKeyboardActionInput(keyboardControls.action, client, sprite)
408
+ );
409
+
410
+ const playPredictedWalkAnimation = () => {
411
+ if (sprite.animationFixed) return;
412
+ if (sprite.animationIsPlaying && sprite.animationIsPlaying()) return;
413
+ realAnimationName.set('walk');
414
+ };
415
+
416
+ const resumeHeldDirectionWalkAnimation = () => {
417
+ if (!isCurrentPlayer()) return false;
418
+ if (activeDirectionKeys.size === 0) return false;
419
+ if (!canControls()) return false;
420
+ if (sprite.animationFixed) return false;
421
+ if (sprite.animationIsPlaying && sprite.animationIsPlaying()) return false;
422
+ realAnimationName.set('walk');
423
+ return true;
424
+ };
425
+
426
+ const processMovementInput = (input) => {
427
+ if (!canControls()) return;
428
+ client.processInput({ input });
429
+ playPredictedWalkAnimation();
430
+ };
431
+
432
+ const processDashInput = () => {
433
+ if (!canControls()) return;
434
+ client.processDash({
435
+ direction: directionToDashVector(resolveSpriteDirection()),
436
+ });
437
+ };
438
+
439
+ const actionBind = () => getKeyboardControlBind(keyboardControls.action);
440
+ const keyboardEventId = (event) => `${event.keyCode}:${event.code}:${event.key}`;
441
+
442
+ const handleNativeActionWhileMoving = (event) => {
443
+ const inputDirection = resolveKeyboardDirectionInput(event, keyboardControls);
444
+ if (inputDirection) {
445
+ const keyId = keyboardEventId(event);
446
+ if (event.type === 'keydown') {
447
+ activeDirectionKeys.delete(keyId);
448
+ activeDirectionKeys.set(keyId, inputDirection);
449
+ resumeHeldDirectionWalkAnimation();
450
+ }
451
+ else {
452
+ activeDirectionKeys.delete(keyId);
453
+ }
454
+ }
455
+
456
+ if (!isCurrentPlayer()) return;
457
+ if (event.type !== 'keydown' || event.repeat) return;
458
+ if (activeDirectionKeys.size === 0) return;
459
+ if (!keyboardEventMatchesBind(event, actionBind())) return;
460
+ if (!canControls()) return;
461
+
462
+ client.processAction(resolveCurrentActionInput());
463
+ };
311
464
 
312
465
  const visible = computed(() => {
313
466
  if (sprite.isEvent()) {
@@ -321,38 +474,44 @@
321
474
  repeat: true,
322
475
  bind: keyboardControls.down,
323
476
  keyDown() {
324
- if (canControls()) client.processInput({ input: Direction.Down })
477
+ processMovementInput(Direction.Down)
325
478
  },
326
479
  },
327
480
  up: {
328
481
  repeat: true,
329
482
  bind: keyboardControls.up,
330
483
  keyDown() {
331
- if (canControls()) client.processInput({ input: Direction.Up })
484
+ processMovementInput(Direction.Up)
332
485
  },
333
486
  },
334
487
  left: {
335
488
  repeat: true,
336
489
  bind: keyboardControls.left,
337
490
  keyDown() {
338
- if (canControls()) client.processInput({ input: Direction.Left })
491
+ processMovementInput(Direction.Left)
339
492
  },
340
493
  },
341
494
  right: {
342
495
  repeat: true,
343
496
  bind: keyboardControls.right,
344
497
  keyDown() {
345
- if (canControls()) client.processInput({ input: Direction.Right })
498
+ processMovementInput(Direction.Right)
346
499
  },
347
500
  },
348
501
  action: {
349
502
  bind: getKeyboardControlBind(keyboardControls.action),
350
503
  keyDown() {
351
504
  if (canControls()) {
352
- client.processAction(resolveKeyboardActionInput(keyboardControls.action, client, sprite))
505
+ client.processAction(resolveCurrentActionInput())
353
506
  }
354
507
  },
355
508
  },
509
+ dash: {
510
+ bind: keyboardControls.dash,
511
+ keyDown() {
512
+ processDashInput()
513
+ },
514
+ },
356
515
  escape: {
357
516
  bind: keyboardControls.escape,
358
517
  keyDown() {
@@ -402,7 +561,7 @@
402
561
  }
403
562
 
404
563
  const graphicScale = (graphicObject) => {
405
- const scale = graphicObject?.scale;
564
+ const scale = graphicObject?.displayScale ?? graphicObject?.scale;
406
565
  if (Array.isArray(scale)) return scale;
407
566
  if (typeof scale === 'number') return [scale, scale];
408
567
  if (scale && typeof scale === 'object') {
@@ -648,6 +807,10 @@
648
807
  const graphicBounds = computed(() => {
649
808
  const box = hitbox();
650
809
  const fallback = hitboxBounds();
810
+ const customEventComponent = resolvedEventComponents()[0];
811
+ if (customEventComponent && !customEventComponent.renderGraphic) {
812
+ return fallback;
813
+ }
651
814
  const dimensions = imageDimensions();
652
815
  const graphics = graphicsSignals();
653
816
  let bounds = null;
@@ -710,6 +873,31 @@
710
873
  };
711
874
  });
712
875
 
876
+ const interactionBounds = () => ({
877
+ bounds: graphicBounds(),
878
+ hitbox: hitboxBounds(),
879
+ graphic: graphicBounds()
880
+ });
881
+
882
+ const interactionCursor = computed(() =>
883
+ client.interactions.cursorFor(sprite, interactionBounds())
884
+ );
885
+
886
+ const handleInteraction = (type) => (event) => {
887
+ client.updatePointerFromInteractionEvent(event);
888
+ client.interactions.handle(sprite, type, {
889
+ event,
890
+ bounds: interactionBounds()
891
+ });
892
+ };
893
+
894
+ const interactionPointerOver = handleInteraction('pointerover');
895
+ const interactionPointerOut = handleInteraction('pointerout');
896
+ const interactionPointerDown = handleInteraction('pointerdown');
897
+ const interactionPointerUp = handleInteraction('pointerup');
898
+ const interactionPointerMove = handleInteraction('pointermove');
899
+ const interactionClick = handleInteraction('click');
900
+
713
901
  // Combine animation change detection with movement state from smoothX/smoothY
714
902
  const movementAnimations = ['walk', 'stand'];
715
903
  const epsilon = 0; // movement threshold to consider the easing still running
@@ -779,17 +967,28 @@
779
967
  });
780
968
 
781
969
  const animationMovementSubscription = combineLatest([animationChange$, moving$]).subscribe(([[prev, curr], isMoving]) => {
782
- if (curr == 'stand' && !isMoving) {
970
+ const isMovementAnimation = movementAnimations.includes(curr);
971
+ const isTemporaryAnimationPlaying =
972
+ sprite.animationIsPlaying && sprite.animationIsPlaying();
973
+
974
+ if (sprite.animationFixed && isMovementAnimation) {
783
975
  realAnimationName.set(curr);
976
+ return;
977
+ }
978
+
979
+ if (curr == 'stand' && !isMoving) {
980
+ if (!resumeHeldDirectionWalkAnimation()) {
981
+ realAnimationName.set(curr);
982
+ }
784
983
  }
785
984
  else if (curr == 'walk' && isMoving) {
786
985
  realAnimationName.set(curr);
787
986
  }
788
- else if (!movementAnimations.includes(curr)) {
987
+ else if (!isMovementAnimation) {
789
988
  realAnimationName.set(curr);
790
989
  }
791
- if (!isMoving && sprite.animationIsPlaying && sprite.animationIsPlaying()) {
792
- if (movementAnimations.includes(curr)) {
990
+ if (!isMoving && isTemporaryAnimationPlaying) {
991
+ if (isMovementAnimation) {
793
992
  if (typeof sprite.resetAnimationState === 'function') {
794
993
  sprite.resetAnimationState();
795
994
  }
@@ -797,6 +996,16 @@
797
996
  }
798
997
  });
799
998
 
999
+ const resumeWalkSubscriptions = [
1000
+ sprite._canMove,
1001
+ sprite._animationFixed,
1002
+ sprite.animationIsPlaying,
1003
+ ]
1004
+ .filter(signal => signal?.observable)
1005
+ .map(signal => signal.observable.subscribe(() => {
1006
+ resumeHeldDirectionWalkAnimation();
1007
+ }));
1008
+
800
1009
  /**
801
1010
  * Cleanup subscriptions and call hooks before sprite destruction.
802
1011
  *
@@ -834,8 +1043,13 @@
834
1043
  const onBeforeDestroy = async () => {
835
1044
  await runBeforeRemove();
836
1045
  await waitForTemporaryAnimationEnd();
1046
+ if (typeof document !== 'undefined') {
1047
+ document.removeEventListener('keydown', handleNativeActionWhileMoving);
1048
+ document.removeEventListener('keyup', handleNativeActionWhileMoving);
1049
+ }
837
1050
  removeTransitionSubscription?.unsubscribe();
838
1051
  animationMovementSubscription.unsubscribe();
1052
+ resumeWalkSubscriptions.forEach(subscription => subscription.unsubscribe());
839
1053
  xSubscription.unsubscribe();
840
1054
  ySubscription.unsubscribe();
841
1055
  await lastValueFrom(hooks.callHooks("client-sprite-onDestroy", sprite))
@@ -843,6 +1057,10 @@
843
1057
  }
844
1058
 
845
1059
  mount((element) => {
1060
+ if (typeof document !== 'undefined') {
1061
+ document.addEventListener('keydown', handleNativeActionWhileMoving);
1062
+ document.addEventListener('keyup', handleNativeActionWhileMoving);
1063
+ }
846
1064
  hooks.callHooks("client-sprite-onAdd", sprite).subscribe()
847
1065
  hooks.callHooks("client-sceneMap-onAddSprite", client.sceneMap, sprite).subscribe()
848
1066
  effect(() => {
@@ -858,8 +1076,16 @@
858
1076
  * @param gui - The GUI instance
859
1077
  * @param data - Data passed from the GUI component
860
1078
  */
861
- const onAttachedGuiFinish = (gui, data) => {
862
- guiService.guiClose(gui.name, data);
1079
+ const normalizeOpenId = (value) => {
1080
+ const resolved = typeof value === "function" ? value() : value;
1081
+ return typeof resolved === "string" && resolved.length > 0 ? resolved : undefined;
1082
+ };
1083
+
1084
+ const onAttachedGuiFinish = (gui, data, guiOpenId) => {
1085
+ const completedOpenId = normalizeOpenId(guiOpenId);
1086
+ const currentOpenId = normalizeOpenId(gui.openId);
1087
+ if (completedOpenId && currentOpenId && completedOpenId !== currentOpenId) return;
1088
+ guiService.guiClose(gui.name, data, completedOpenId ?? currentOpenId);
863
1089
  };
864
1090
 
865
1091
  /**
@@ -17,7 +17,7 @@
17
17
  @if (hasChoices()) {
18
18
  <Navigation tabindex={selectedItem} controls={controls}>
19
19
  <div class="rpg-ui-dialog-choices">
20
- @for ((choice,index) of choices) {
20
+ @for ((choice,index) of dialogChoices()) {
21
21
  <div
22
22
  class="rpg-ui-dialog-choice"
23
23
  class={{active: selectedItem() === index}}
@@ -62,27 +62,38 @@
62
62
  engine.stopProcessingInput = true;
63
63
 
64
64
  const selectedItem = signal(0)
65
- let isDestroyed = false;
65
+ const ACTION_OPEN_GUARD_MS = 150;
66
+ const openedAt = Date.now();
66
67
 
67
68
  const {
68
69
  data,
69
70
  onFinish,
70
- onInteraction
71
+ onInteraction,
72
+ guiOpenId
71
73
  } = defineProps();
72
74
 
73
- const { message, choices, face, speaker, position, typewriterEffect, autoClose } = data();
74
- const fullWidth = computed(() => data().fullWidth || false);
75
+ const dialogData = computed(() => data() || {});
76
+ const dialogChoices = computed(() => Array.isArray(dialogData().choices) ? dialogData().choices : []);
77
+ const message = computed(() => dialogData().message);
78
+ const speaker = computed(() => dialogData().speaker);
79
+ const position = computed(() => dialogData().position);
80
+ const typewriterEffect = computed(() => dialogData().typewriterEffect);
81
+ const fullWidth = computed(() => dialogData().fullWidth || false);
75
82
 
76
83
  const resolveProp = (value) => typeof value === "function" ? value() : value;
84
+ const normalizeOpenId = (value) => {
85
+ const resolved = resolveProp(value);
86
+ return typeof resolved === "string" && resolved.length > 0 ? resolved : undefined;
87
+ };
77
88
 
78
89
  const speakerName = computed(() => {
79
- const value = resolveProp(speaker);
90
+ const value = resolveProp(speaker());
80
91
  return value ? String(value) : "";
81
92
  });
82
93
 
83
- const dialogPosition = computed(() => resolveProp(position) || "bottom");
94
+ const dialogPosition = computed(() => resolveProp(position()) || "bottom");
84
95
  const isFullWidth = computed(() => resolveProp(fullWidth) !== false);
85
- const dialogFace = computed(() => resolveProp(face));
96
+ const dialogFace = computed(() => resolveProp(dialogData().face));
86
97
  const hasFace = computed(() => {
87
98
  const value = dialogFace();
88
99
  return !!(value && value.id);
@@ -123,9 +134,9 @@
123
134
  };
124
135
 
125
136
  effect(() => {
126
- const text = resolveProp(message) || "";
137
+ const text = resolveProp(message()) || "";
127
138
  fullMessage.set(text);
128
- const useTypewriter = resolveProp(typewriterEffect) !== false;
139
+ const useTypewriter = resolveProp(typewriterEffect()) !== false;
129
140
  if (!useTypewriter) {
130
141
  finishTyping();
131
142
  return;
@@ -134,9 +145,9 @@
134
145
  });
135
146
 
136
147
 
137
- const hasChoices = computed(() => choices.length > 0);
148
+ const hasChoices = computed(() => dialogChoices().length > 0);
138
149
  const showIndicator = computed(() => !hasChoices() && !isTyping());
139
- const nav = createTabindexNavigator(selectedItem, { count: () => choices.length }, 'wrap');
150
+ const nav = createTabindexNavigator(selectedItem, { count: () => dialogChoices().length }, 'wrap');
140
151
 
141
152
  function selectChoice(index) {
142
153
  return function() {
@@ -146,13 +157,15 @@
146
157
  }
147
158
 
148
159
  function _onFinish(value) {
149
- if (onFinish) onFinish(value);
160
+ if (onFinish) onFinish(value, normalizeOpenId(guiOpenId));
150
161
  }
151
162
 
152
163
  const onSelect = (index) => {
153
164
  _onFinish(index);
154
165
  };
155
166
 
167
+ const canAcceptAction = () => Date.now() - openedAt >= ACTION_OPEN_GUARD_MS;
168
+
156
169
  const controls = signal({
157
170
  up: {
158
171
  repeat: true,
@@ -175,6 +188,9 @@
175
188
  action: {
176
189
  bind: getKeyboardControlBind(keyboardControls.action),
177
190
  keyDown() {
191
+ if (!canAcceptAction()) {
192
+ return;
193
+ }
178
194
  if (isTyping()) {
179
195
  finishTyping();
180
196
  return;
@@ -192,6 +208,9 @@
192
208
  action: {
193
209
  bind: getKeyboardControlBind(keyboardControls.action),
194
210
  keyDown() {
211
+ if (!canAcceptAction()) {
212
+ return;
213
+ }
195
214
  if (isTyping()) {
196
215
  finishTyping();
197
216
  return;
@@ -203,6 +222,9 @@
203
222
  })
204
223
 
205
224
  const faceSheet = (faceValue) => {
225
+ if (!faceValue || !faceValue.id) {
226
+ return undefined;
227
+ }
206
228
  return {
207
229
  definition: engine.getSpriteSheet(faceValue.id),
208
230
  playing: faceValue.expression || "default",
@@ -211,7 +233,6 @@
211
233
 
212
234
  mount((element) => {
213
235
  return () => {
214
- isDestroyed = true;
215
236
  // Wait destroy is finished before start processing input
216
237
  delay(() => {
217
238
  engine.stopProcessingInput = false;
@@ -34,6 +34,7 @@
34
34
  import { getKeyboardControlBind } from "../../services/actionInput";
35
35
 
36
36
  const engine = inject(RpgClientEngine);
37
+ const { t } = engine.i18n();
37
38
  const guiService = inject(RpgGui);
38
39
  const keyboardControls = engine.globalConfig.keyboardControls;
39
40
 
@@ -50,12 +51,12 @@
50
51
  });
51
52
 
52
53
  const defaultEntries = [
53
- { id: "title", label: "Title Screen" },
54
- { id: "load", label: "Load Game" }
54
+ { id: "title", label: t("rpg.gameover.title-screen") },
55
+ { id: "load", label: t("rpg.gameover.load-game") }
55
56
  ];
56
57
 
57
58
  const resolveProp = (value) => typeof value === "function" ? value() : value;
58
- const titleText = computed(() => resolveProp(title) || "Game Over");
59
+ const titleText = computed(() => resolveProp(title) || t("rpg.gameover.title"));
59
60
  const subtitleText = computed(() => resolveProp(subtitle) || "");
60
61
  const localActionsEnabled = computed(() => resolveProp(localActions) === true);
61
62