@rpgjs/client 5.0.0-beta.6 → 5.0.0-beta.8

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 (147) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/Game/AnimationManager.d.ts +2 -2
  3. package/dist/Game/AnimationManager.js +18 -9
  4. package/dist/Game/AnimationManager.js.map +1 -1
  5. package/dist/Game/AnimationManager.spec.d.ts +1 -0
  6. package/dist/Game/Map.d.ts +7 -9
  7. package/dist/Game/Map.js +5 -4
  8. package/dist/Game/Map.js.map +1 -1
  9. package/dist/Game/Object.d.ts +44 -20
  10. package/dist/Game/Object.js +28 -14
  11. package/dist/Game/Object.js.map +1 -1
  12. package/dist/Gui/Gui.d.ts +19 -6
  13. package/dist/Gui/Gui.js +64 -34
  14. package/dist/Gui/Gui.js.map +1 -1
  15. package/dist/Gui/Gui.spec.d.ts +1 -0
  16. package/dist/Gui/NotificationManager.d.ts +1 -1
  17. package/dist/Gui/NotificationManager.js.map +1 -1
  18. package/dist/Resource.js +1 -1
  19. package/dist/Resource.js.map +1 -1
  20. package/dist/RpgClient.d.ts +57 -2
  21. package/dist/RpgClientEngine.d.ts +55 -16
  22. package/dist/RpgClientEngine.js +60 -5
  23. package/dist/RpgClientEngine.js.map +1 -1
  24. package/dist/Sound.js.map +1 -1
  25. package/dist/_virtual/{_@oxc-project_runtime@0.127.0 → _@oxc-project_runtime@0.128.0}/helpers/decorate.js +1 -1
  26. package/dist/_virtual/{_@oxc-project_runtime@0.127.0 → _@oxc-project_runtime@0.128.0}/helpers/decorateMetadata.js +1 -1
  27. package/dist/components/animations/animation.ce.js.map +1 -1
  28. package/dist/components/animations/hit.ce.js.map +1 -1
  29. package/dist/components/character.ce.js +280 -3
  30. package/dist/components/character.ce.js.map +1 -1
  31. package/dist/components/dynamics/bar.ce.js +96 -0
  32. package/dist/components/dynamics/bar.ce.js.map +1 -0
  33. package/dist/components/dynamics/image.ce.js +23 -0
  34. package/dist/components/dynamics/image.ce.js.map +1 -0
  35. package/dist/components/dynamics/parse-value.d.ts +4 -1
  36. package/dist/components/dynamics/parse-value.js +51 -35
  37. package/dist/components/dynamics/parse-value.js.map +1 -1
  38. package/dist/components/dynamics/parse-value.spec.d.ts +1 -0
  39. package/dist/components/dynamics/shape-utils.d.ts +16 -0
  40. package/dist/components/dynamics/shape-utils.js +73 -0
  41. package/dist/components/dynamics/shape-utils.js.map +1 -0
  42. package/dist/components/dynamics/shape-utils.spec.d.ts +1 -0
  43. package/dist/components/dynamics/shape.ce.js +83 -0
  44. package/dist/components/dynamics/shape.ce.js.map +1 -0
  45. package/dist/components/dynamics/text.ce.js +28 -41
  46. package/dist/components/dynamics/text.ce.js.map +1 -1
  47. package/dist/components/gui/box.ce.js.map +1 -1
  48. package/dist/components/gui/dialogbox/index.ce.js.map +1 -1
  49. package/dist/components/gui/gameover.ce.js.map +1 -1
  50. package/dist/components/gui/hud/hud.ce.js.map +1 -1
  51. package/dist/components/gui/menu/equip-menu.ce.js.map +1 -1
  52. package/dist/components/gui/menu/exit-menu.ce.js.map +1 -1
  53. package/dist/components/gui/menu/items-menu.ce.js.map +1 -1
  54. package/dist/components/gui/menu/main-menu.ce.js.map +1 -1
  55. package/dist/components/gui/menu/options-menu.ce.js.map +1 -1
  56. package/dist/components/gui/menu/skills-menu.ce.js.map +1 -1
  57. package/dist/components/gui/mobile/index.d.ts +1 -1
  58. package/dist/components/gui/mobile/index.js.map +1 -1
  59. package/dist/components/gui/mobile/mobile.ce.js.map +1 -1
  60. package/dist/components/gui/notification/notification.ce.js.map +1 -1
  61. package/dist/components/gui/save-load.ce.js.map +1 -1
  62. package/dist/components/gui/shop/shop.ce.js.map +1 -1
  63. package/dist/components/gui/title-screen.ce.js.map +1 -1
  64. package/dist/components/player-components-utils.d.ts +67 -0
  65. package/dist/components/player-components-utils.js +162 -0
  66. package/dist/components/player-components-utils.js.map +1 -0
  67. package/dist/components/player-components-utils.spec.d.ts +1 -0
  68. package/dist/components/player-components.ce.js +188 -0
  69. package/dist/components/player-components.ce.js.map +1 -0
  70. package/dist/components/prebuilt/hp-bar.ce.js.map +1 -1
  71. package/dist/components/prebuilt/light-halo.ce.js.map +1 -1
  72. package/dist/components/scenes/canvas.ce.js.map +1 -1
  73. package/dist/components/scenes/draw-map.ce.js.map +1 -1
  74. package/dist/components/scenes/event-layer.ce.js.map +1 -1
  75. package/dist/core/inject.js +1 -1
  76. package/dist/core/inject.js.map +1 -1
  77. package/dist/core/setup.js +1 -1
  78. package/dist/core/setup.js.map +1 -1
  79. package/dist/index.js +1 -1
  80. package/dist/module.js +4 -1
  81. package/dist/module.js.map +1 -1
  82. package/dist/node_modules/.pnpm/{@signe_di@2.9.0 → @signe_di@2.10.0}/node_modules/@signe/di/dist/index.js +7 -117
  83. package/dist/node_modules/.pnpm/@signe_di@2.10.0/node_modules/@signe/di/dist/index.js.map +1 -0
  84. package/dist/node_modules/.pnpm/@signe_reactive@2.10.0/node_modules/@signe/reactive/dist/index.js +239 -0
  85. package/dist/node_modules/.pnpm/@signe_reactive@2.10.0/node_modules/@signe/reactive/dist/index.js.map +1 -0
  86. package/dist/node_modules/.pnpm/@signe_room@2.10.0/node_modules/@signe/room/dist/index.js +611 -0
  87. package/dist/node_modules/.pnpm/@signe_room@2.10.0/node_modules/@signe/room/dist/index.js.map +1 -0
  88. package/dist/node_modules/.pnpm/@signe_sync@2.10.0/node_modules/@signe/sync/dist/client/index.js +44 -0
  89. package/dist/node_modules/.pnpm/@signe_sync@2.10.0/node_modules/@signe/sync/dist/client/index.js.map +1 -0
  90. package/dist/node_modules/.pnpm/{@signe_sync@2.9.0 → @signe_sync@2.10.0}/node_modules/@signe/sync/dist/index.js +29 -136
  91. package/dist/node_modules/.pnpm/@signe_sync@2.10.0/node_modules/@signe/sync/dist/index.js.map +1 -0
  92. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-HAC622V3.js.map +1 -1
  93. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-S74YV6PU.js.map +1 -1
  94. package/dist/node_modules/.pnpm/zod@3.24.2/node_modules/zod/lib/index.js.map +1 -1
  95. package/dist/presets/animation.js.map +1 -1
  96. package/dist/presets/faceset.js.map +1 -1
  97. package/dist/presets/icon.js.map +1 -1
  98. package/dist/presets/lpc.js.map +1 -1
  99. package/dist/presets/rmspritesheet.js.map +1 -1
  100. package/dist/services/AbstractSocket.js.map +1 -1
  101. package/dist/services/keyboardControls.js.map +1 -1
  102. package/dist/services/loadMap.d.ts +6 -0
  103. package/dist/services/loadMap.js +1 -1
  104. package/dist/services/loadMap.js.map +1 -1
  105. package/dist/services/mmorpg.js +1 -1
  106. package/dist/services/mmorpg.js.map +1 -1
  107. package/dist/services/save.js.map +1 -1
  108. package/dist/services/standalone.js +1 -1
  109. package/dist/services/standalone.js.map +1 -1
  110. package/dist/utils/getEntityProp.js.map +1 -1
  111. package/package.json +8 -8
  112. package/src/Game/AnimationManager.spec.ts +30 -0
  113. package/src/Game/AnimationManager.ts +22 -10
  114. package/src/Game/Map.ts +12 -2
  115. package/src/Game/Object.ts +68 -43
  116. package/src/Gui/Gui.spec.ts +273 -0
  117. package/src/Gui/Gui.ts +105 -50
  118. package/src/Resource.ts +1 -2
  119. package/src/RpgClient.ts +63 -2
  120. package/src/RpgClientEngine.ts +82 -12
  121. package/src/components/character.ce +353 -1
  122. package/src/components/dynamics/bar.ce +87 -0
  123. package/src/components/dynamics/image.ce +20 -0
  124. package/src/components/dynamics/parse-value.spec.ts +41 -0
  125. package/src/components/dynamics/parse-value.ts +102 -37
  126. package/src/components/dynamics/shape-utils.spec.ts +46 -0
  127. package/src/components/dynamics/shape-utils.ts +61 -0
  128. package/src/components/dynamics/shape.ce +89 -0
  129. package/src/components/dynamics/text.ce +34 -149
  130. package/src/components/player-components-utils.spec.ts +109 -0
  131. package/src/components/player-components-utils.ts +205 -0
  132. package/src/components/player-components.ce +221 -0
  133. package/src/core/setup.ts +2 -2
  134. package/src/module.ts +5 -1
  135. package/src/services/loadMap.ts +2 -0
  136. package/dist/node_modules/.pnpm/@signe_di@2.9.0/node_modules/@signe/di/dist/index.js.map +0 -1
  137. package/dist/node_modules/.pnpm/@signe_reactive@2.9.0/node_modules/@signe/reactive/dist/index.js +0 -463
  138. package/dist/node_modules/.pnpm/@signe_reactive@2.9.0/node_modules/@signe/reactive/dist/index.js.map +0 -1
  139. package/dist/node_modules/.pnpm/@signe_room@2.9.0/node_modules/@signe/room/dist/index.js +0 -2191
  140. package/dist/node_modules/.pnpm/@signe_room@2.9.0/node_modules/@signe/room/dist/index.js.map +0 -1
  141. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/chunk-7QVYU63E.js +0 -10
  142. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/chunk-7QVYU63E.js.map +0 -1
  143. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/client/index.js +0 -91
  144. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/client/index.js.map +0 -1
  145. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/index.js.map +0 -1
  146. package/dist/node_modules/.pnpm/dset@3.1.4/node_modules/dset/dist/index.js +0 -14
  147. package/dist/node_modules/.pnpm/dset@3.1.4/node_modules/dset/dist/index.js.map +0 -1
@@ -4,6 +4,8 @@
4
4
  <compConfig.component object ...compConfig.props />
5
5
  </Container>
6
6
  }
7
+ <PlayerComponents object position="bottom" graphicBounds />
8
+ <PlayerComponents object position="left" graphicBounds />
7
9
  <Particle emit={emitParticleTrigger} settings={particleSettings} zIndex={1000} name={particleName} />
8
10
  <Container>
9
11
  @for (graphicObj of graphicsSignals) {
@@ -17,6 +19,9 @@
17
19
  />
18
20
  }
19
21
  </Container>
22
+ <PlayerComponents object position="center" graphicBounds />
23
+ <PlayerComponents object position="right" graphicBounds />
24
+ <PlayerComponents object position="top" graphicBounds />
20
25
  @for (compConfig of normalizedComponentsInFront) {
21
26
  <Container dependencies={compConfig.dependencies}>
22
27
  <compConfig.component object ...compConfig.props />
@@ -37,13 +42,14 @@
37
42
 
38
43
  <script>
39
44
  import { signal, effect, mount, computed, tick, animatedSignal, on } from "canvasengine";
45
+ import { Assets } from "pixi.js";
40
46
 
41
47
  import { lastValueFrom, combineLatest, pairwise, filter, map, startWith } from "rxjs";
42
48
  import { Particle } from "@canvasengine/presets";
43
49
  import { GameEngineToken, ModulesToken } from "@rpgjs/common";
44
50
  import { RpgClientEngine } from "../RpgClientEngine";
45
51
  import { inject } from "../core/inject";
46
- import { Direction } from "@rpgjs/common";
52
+ import { Direction, Animation } from "@rpgjs/common";
47
53
  import Hit from "./effects/hit.ce";
48
54
  import PlayerComponents from "./player-components.ce";
49
55
  import { RpgGui } from "../Gui/Gui";
@@ -390,6 +396,280 @@
390
396
  return undefined;
391
397
  }
392
398
 
399
+ const imageDimensions = signal({});
400
+ const loadingImageDimensions = new Set();
401
+
402
+ const toPositiveNumber = (value) => {
403
+ const number = typeof value === 'number' ? value : parseFloat(value);
404
+ return Number.isFinite(number) && number > 0 ? number : undefined;
405
+ };
406
+
407
+ const toFiniteNumber = (value, fallback = 0) => {
408
+ const number = typeof value === 'number' ? value : parseFloat(value);
409
+ return Number.isFinite(number) ? number : fallback;
410
+ };
411
+
412
+ const clampRatio = (value) => Math.min(1, Math.max(0, value));
413
+
414
+ const normalizePair = (value, fallback = [1, 1]) => {
415
+ if (Array.isArray(value)) {
416
+ const x = toFiniteNumber(value[0], fallback[0]);
417
+ const y = toFiniteNumber(value[1] ?? value[0], x);
418
+ return [x, y];
419
+ }
420
+ if (typeof value === 'number') {
421
+ return [value, value];
422
+ }
423
+ if (value && typeof value === 'object') {
424
+ const x = toFiniteNumber(value.x, fallback[0]);
425
+ const y = toFiniteNumber(value.y ?? value.x, x);
426
+ return [x, y];
427
+ }
428
+ return fallback;
429
+ };
430
+
431
+ const normalizeAnchor = (value) => {
432
+ if (!Array.isArray(value)) return undefined;
433
+ const [x, y] = normalizePair(value, [0, 0]);
434
+ return [clampRatio(x), clampRatio(y)];
435
+ };
436
+
437
+ const resolveImageSource = (image) => {
438
+ if (typeof image === 'string') return image;
439
+ if (typeof image?.default === 'string') return image.default;
440
+ return undefined;
441
+ };
442
+
443
+ const parentTextureOptions = (graphicObject) => {
444
+ const props = [
445
+ 'width',
446
+ 'height',
447
+ 'framesHeight',
448
+ 'framesWidth',
449
+ 'rectWidth',
450
+ 'rectHeight',
451
+ 'offset',
452
+ 'image',
453
+ 'sound',
454
+ 'spriteRealSize',
455
+ 'scale',
456
+ 'anchor',
457
+ 'pivot',
458
+ 'x',
459
+ 'y',
460
+ 'opacity'
461
+ ];
462
+
463
+ return props.reduce((options, prop) => {
464
+ if (graphicObject?.[prop] !== undefined) {
465
+ options[prop] = graphicObject[prop];
466
+ }
467
+ return options;
468
+ }, {});
469
+ };
470
+
471
+ const resolveTextureOptions = (graphicObject) => {
472
+ const textures = graphicObject?.textures ?? {};
473
+ const texture =
474
+ textures[realAnimationName()] ??
475
+ textures[Animation.Stand] ??
476
+ Object.values(textures)[0] ??
477
+ {};
478
+
479
+ return {
480
+ ...parentTextureOptions(graphicObject),
481
+ ...texture
482
+ };
483
+ };
484
+
485
+ const resolveFirstAnimationFrame = (textureOptions) => {
486
+ const animations = textureOptions?.animations;
487
+ if (!animations) return {};
488
+
489
+ try {
490
+ const frames = typeof animations === 'function'
491
+ ? animations({ direction: direction() })
492
+ : animations;
493
+ if (!Array.isArray(frames)) return {};
494
+ const firstGroup = frames[0];
495
+ return Array.isArray(firstGroup) ? firstGroup[0] ?? {} : firstGroup ?? {};
496
+ }
497
+ catch {
498
+ return {};
499
+ }
500
+ };
501
+
502
+ const optionValue = (prop, frame, textureOptions, graphicObject) => {
503
+ return frame?.[prop] ?? textureOptions?.[prop] ?? graphicObject?.[prop];
504
+ };
505
+
506
+ const resolveFrameSize = (textureOptions, dimensions) => {
507
+ const framesWidth = toPositiveNumber(textureOptions?.framesWidth) ?? 1;
508
+ const framesHeight = toPositiveNumber(textureOptions?.framesHeight) ?? 1;
509
+ const imageSource = resolveImageSource(textureOptions?.image);
510
+ const loadedSize = imageSource ? dimensions[imageSource] : undefined;
511
+ const fullWidth = toPositiveNumber(textureOptions?.width) ?? loadedSize?.width;
512
+ const fullHeight = toPositiveNumber(textureOptions?.height) ?? loadedSize?.height;
513
+ const width = toPositiveNumber(textureOptions?.rectWidth) ??
514
+ toPositiveNumber(textureOptions?.spriteWidth) ??
515
+ (fullWidth ? fullWidth / framesWidth : undefined);
516
+ const height = toPositiveNumber(textureOptions?.rectHeight) ??
517
+ toPositiveNumber(textureOptions?.spriteHeight) ??
518
+ (fullHeight ? fullHeight / framesHeight : undefined);
519
+
520
+ return {
521
+ width,
522
+ height
523
+ };
524
+ };
525
+
526
+ const resolveHitboxAnchor = (spriteWidth, spriteHeight, realSize, box) => {
527
+ if (!spriteWidth || !spriteHeight || !box) {
528
+ return [0, 0];
529
+ }
530
+
531
+ const heightOfSprite = typeof realSize === 'number' ? realSize : realSize?.height;
532
+ const resolvedHeight = toPositiveNumber(heightOfSprite) ?? spriteHeight;
533
+ const gap = Math.max(0, (spriteHeight - resolvedHeight) / 2);
534
+ const hitboxTopLeftX = clampRatio((spriteWidth - box.w) / 2 / spriteWidth);
535
+ const hitboxTopLeftY = clampRatio((spriteHeight - box.h - gap) / spriteHeight);
536
+ const hitboxCenterX = clampRatio(hitboxTopLeftX + box.w / 2 / spriteWidth);
537
+ const hitboxCenterY = clampRatio(hitboxTopLeftY + box.h / 2 / spriteHeight);
538
+ const footY = clampRatio((spriteHeight - gap) / spriteHeight);
539
+
540
+ switch (box.anchorMode ?? 'top-left') {
541
+ case 'center':
542
+ return [hitboxCenterX, hitboxCenterY];
543
+ case 'foot':
544
+ return [hitboxCenterX, footY];
545
+ case 'top-left':
546
+ default:
547
+ return [hitboxTopLeftX, hitboxTopLeftY];
548
+ }
549
+ };
550
+
551
+ const loadImageDimensions = (image) => {
552
+ const source = resolveImageSource(image);
553
+ if (!source || imageDimensions()[source] || loadingImageDimensions.has(source)) {
554
+ return;
555
+ }
556
+
557
+ loadingImageDimensions.add(source);
558
+ Assets.load(source)
559
+ .then((texture) => {
560
+ const width = toPositiveNumber(texture?.width);
561
+ const height = toPositiveNumber(texture?.height);
562
+ if (!width || !height) return;
563
+
564
+ imageDimensions.update((dimensions) => ({
565
+ ...dimensions,
566
+ [source]: { width, height }
567
+ }));
568
+ })
569
+ .catch(() => {})
570
+ .finally(() => {
571
+ loadingImageDimensions.delete(source);
572
+ });
573
+ };
574
+
575
+ effect(() => {
576
+ const sources = new Set();
577
+
578
+ graphicsSignals().forEach((graphicObject) => {
579
+ const baseImage = resolveImageSource(graphicObject?.image);
580
+ if (baseImage) sources.add(baseImage);
581
+
582
+ Object.values(graphicObject?.textures ?? {}).forEach((textureOptions) => {
583
+ const image = resolveImageSource(textureOptions?.image ?? graphicObject?.image);
584
+ if (image) sources.add(image);
585
+ });
586
+ });
587
+
588
+ sources.forEach((source) => loadImageDimensions(source));
589
+ });
590
+
591
+ const hitboxBounds = computed(() => {
592
+ const box = hitbox();
593
+ const width = box?.w ?? 0;
594
+ const height = box?.h ?? 0;
595
+
596
+ return {
597
+ left: 0,
598
+ top: 0,
599
+ right: width,
600
+ bottom: height,
601
+ width,
602
+ height,
603
+ centerX: width / 2,
604
+ centerY: height / 2
605
+ };
606
+ });
607
+
608
+ const graphicBounds = computed(() => {
609
+ const box = hitbox();
610
+ const fallback = hitboxBounds();
611
+ const dimensions = imageDimensions();
612
+ const graphics = graphicsSignals();
613
+ let bounds = null;
614
+
615
+ graphics.forEach((graphicObject) => {
616
+ const textureOptions = resolveTextureOptions(graphicObject);
617
+ const frame = resolveFirstAnimationFrame(textureOptions);
618
+ const size = resolveFrameSize(textureOptions, dimensions);
619
+ const spriteWidth = size.width ?? box?.w;
620
+ const spriteHeight = size.height ?? box?.h;
621
+
622
+ if (!spriteWidth || !spriteHeight) {
623
+ return;
624
+ }
625
+
626
+ const explicitAnchor = normalizeAnchor(optionValue('anchor', frame, textureOptions, graphicObject));
627
+ const anchor = explicitAnchor ?? resolveHitboxAnchor(
628
+ spriteWidth,
629
+ spriteHeight,
630
+ optionValue('spriteRealSize', frame, textureOptions, graphicObject),
631
+ box
632
+ );
633
+ const scale = normalizePair(optionValue('scale', frame, textureOptions, graphicObject) ?? graphicScale(graphicObject));
634
+ const x = toFiniteNumber(optionValue('x', frame, textureOptions, graphicObject), 0);
635
+ const y = toFiniteNumber(optionValue('y', frame, textureOptions, graphicObject), 0);
636
+ const leftEdge = -anchor[0] * spriteWidth * scale[0];
637
+ const rightEdge = (1 - anchor[0]) * spriteWidth * scale[0];
638
+ const topEdge = -anchor[1] * spriteHeight * scale[1];
639
+ const bottomEdge = (1 - anchor[1]) * spriteHeight * scale[1];
640
+ const graphic = {
641
+ left: x + Math.min(leftEdge, rightEdge),
642
+ top: y + Math.min(topEdge, bottomEdge),
643
+ right: x + Math.max(leftEdge, rightEdge),
644
+ bottom: y + Math.max(topEdge, bottomEdge)
645
+ };
646
+
647
+ bounds = bounds
648
+ ? {
649
+ left: Math.min(bounds.left, graphic.left),
650
+ top: Math.min(bounds.top, graphic.top),
651
+ right: Math.max(bounds.right, graphic.right),
652
+ bottom: Math.max(bounds.bottom, graphic.bottom)
653
+ }
654
+ : graphic;
655
+ });
656
+
657
+ if (!bounds) {
658
+ return fallback;
659
+ }
660
+
661
+ const width = bounds.right - bounds.left;
662
+ const height = bounds.bottom - bounds.top;
663
+
664
+ return {
665
+ ...bounds,
666
+ width,
667
+ height,
668
+ centerX: bounds.left + width / 2,
669
+ centerY: bounds.top + height / 2
670
+ };
671
+ });
672
+
393
673
  // Combine animation change detection with movement state from smoothX/smoothY
394
674
  const movementAnimations = ['walk', 'stand'];
395
675
  const epsilon = 0; // movement threshold to consider the easing still running
@@ -413,6 +693,51 @@
413
693
  filter(([prev, curr]) => prev !== curr)
414
694
  );
415
695
 
696
+ let beforeRemovePromise = null;
697
+ let beforeRemoveTransitionValue = null;
698
+ const resolveRemoveContext = () => {
699
+ if (!object._removeTransition) return null;
700
+ const value = object._removeTransition();
701
+ if (!value || typeof value !== 'string') return null;
702
+ try {
703
+ const context = JSON.parse(value);
704
+ if (!context || typeof context !== 'object' || !context.active) return null;
705
+ context.__transitionValue = value;
706
+ return context;
707
+ }
708
+ catch {
709
+ return null;
710
+ }
711
+ };
712
+
713
+ const withTimeout = (promise, timeoutMs = 0) => {
714
+ if (!timeoutMs || timeoutMs <= 0) return promise;
715
+ return Promise.race([
716
+ promise,
717
+ new Promise((resolve) => setTimeout(resolve, timeoutMs)),
718
+ ]);
719
+ };
720
+
721
+ const runBeforeRemove = () => {
722
+ const context = resolveRemoveContext();
723
+ if (!context) return Promise.resolve();
724
+ if (beforeRemovePromise && beforeRemoveTransitionValue === context.__transitionValue) {
725
+ return beforeRemovePromise;
726
+ }
727
+ beforeRemoveTransitionValue = context.__transitionValue;
728
+ beforeRemovePromise = withTimeout(
729
+ lastValueFrom(hooks.callHooks("client-sprite-onBeforeRemove", object, context)),
730
+ context.timeoutMs
731
+ );
732
+ return beforeRemovePromise;
733
+ };
734
+
735
+ const removeTransitionSubscription = object._removeTransition?.observable?.subscribe(() => {
736
+ if (resolveRemoveContext()) {
737
+ runBeforeRemove();
738
+ }
739
+ });
740
+
416
741
  const animationMovementSubscription = combineLatest([animationChange$, moving$]).subscribe(([[prev, curr], isMoving]) => {
417
742
  if (curr == 'stand' && !isMoving) {
418
743
  realAnimationName.set(curr);
@@ -442,7 +767,34 @@
442
767
  * @example
443
768
  * await onBeforeDestroy();
444
769
  */
770
+ const waitForTemporaryAnimationEnd = (maxDuration = 1200) => {
771
+ if (!object.animationIsPlaying || !object.animationIsPlaying()) {
772
+ return Promise.resolve();
773
+ }
774
+
775
+ return new Promise((resolve) => {
776
+ let finished = false;
777
+ let timeout;
778
+ let subscription;
779
+ const finish = () => {
780
+ if (finished) return;
781
+ finished = true;
782
+ clearTimeout(timeout);
783
+ subscription?.unsubscribe();
784
+ resolve();
785
+ };
786
+ timeout = setTimeout(finish, maxDuration);
787
+ subscription = object.animationIsPlaying.observable.subscribe((isPlaying) => {
788
+ if (!isPlaying) finish();
789
+ });
790
+ if (finished) subscription.unsubscribe();
791
+ });
792
+ };
793
+
445
794
  const onBeforeDestroy = async () => {
795
+ await runBeforeRemove();
796
+ await waitForTemporaryAnimationEnd();
797
+ removeTransitionSubscription?.unsubscribe();
446
798
  animationMovementSubscription.unsubscribe();
447
799
  xSubscription.unsubscribe();
448
800
  ySubscription.unsubscribe();
@@ -0,0 +1,87 @@
1
+ <Container width={width} height={containerHeight} minWidth={width} minHeight={containerHeight}>
2
+ <Graphics width={width} height={containerHeight} draw={drawBar} />
3
+ @if (hasLabel) {
4
+ <Text text={labelText} x={labelPosition.x} y={labelPosition.y} size={labelSize} color={labelColor} />
5
+ }
6
+ </Container>
7
+
8
+ <script>
9
+ import { computed } from "canvasengine";
10
+ import { resolveDynamicValue } from "./parse-value";
11
+
12
+ const { object, current, max, style, text } = defineProps();
13
+
14
+ const read = (prop, fallback) => prop ? prop() : fallback;
15
+
16
+ const toNumber = (value, fallback = 0) => {
17
+ const resolved = resolveDynamicValue(value, object);
18
+ const num = typeof resolved === 'number' ? resolved : parseFloat(resolved);
19
+ return Number.isFinite(num) ? num : fallback;
20
+ };
21
+
22
+ const toColor = (value, fallback) => {
23
+ const resolved = resolveDynamicValue(value, object);
24
+ if (typeof resolved === 'number') return resolved;
25
+ if (typeof resolved === 'string' && resolved.startsWith('#')) {
26
+ return parseInt(resolved.slice(1), 16);
27
+ }
28
+ return resolved ?? fallback;
29
+ };
30
+
31
+ const config = computed(() => read(style, {}) ?? {});
32
+ const width = computed(() => toNumber(config().width, 50));
33
+ const height = computed(() => toNumber(config().height, 8));
34
+ const borderRadius = computed(() => toNumber(config().borderRadius, 3));
35
+ const borderWidth = computed(() => toNumber(config().borderWidth, 1));
36
+ const backgroundColor = computed(() => toColor(config().bgColor, 0x16213e));
37
+ const fillColor = computed(() => toColor(config().fillColor, 0x4ade80));
38
+ const borderColor = computed(() => toColor(config().borderColor, 0x4a5568));
39
+ const opacity = computed(() => Math.max(0, Math.min(1, toNumber(config().opacity, 1))));
40
+
41
+ const currentValue = computed(() => toNumber(read(current, 0), 0));
42
+ const maxValue = computed(() => Math.max(0, toNumber(read(max, 0), 0)));
43
+ const percent = computed(() => {
44
+ const max = maxValue();
45
+ if (max <= 0) return 0;
46
+ return Math.max(0, Math.min(1, currentValue() / max));
47
+ });
48
+
49
+ const fillWidth = computed(() => Math.max(0, width() * percent()));
50
+ const labelTemplate = computed(() => read(text, null));
51
+ const labelText = computed(() => {
52
+ const template = labelTemplate();
53
+ if (template == null || template === '') return '';
54
+
55
+ const value = String(template)
56
+ .replace(/\{\$current\}/g, String(currentValue()))
57
+ .replace(/\{\$max\}/g, String(maxValue()))
58
+ .replace(/\{\$percent\}/g, String(Math.round(percent() * 100)));
59
+
60
+ return String(resolveDynamicValue(value, object) ?? '');
61
+ });
62
+ const labelSize = computed(() => toNumber(config().fontSize, 10));
63
+ const hasLabel = computed(() => labelText().length > 0);
64
+ const labelOffset = computed(() => hasLabel() ? labelSize() + 2 : 0);
65
+ const containerHeight = computed(() => labelOffset() + height());
66
+ const labelColor = computed(() => toColor(config().textColor, 0xffffff));
67
+ const labelPosition = computed(() => ({
68
+ x: 0,
69
+ y: 0
70
+ }));
71
+
72
+ const drawBar = (g) => {
73
+ g.roundRect(0, labelOffset(), width(), height(), borderRadius());
74
+ g.fill({ color: backgroundColor(), alpha: opacity() });
75
+
76
+ const currentWidth = fillWidth();
77
+ if (currentWidth > 0) {
78
+ g.roundRect(0, labelOffset(), currentWidth, height(), borderRadius());
79
+ g.fill({ color: fillColor(), alpha: opacity() });
80
+ }
81
+
82
+ const strokeWidth = borderWidth();
83
+ if (strokeWidth <= 0) return;
84
+ g.roundRect(0, labelOffset(), width(), height(), borderRadius());
85
+ g.stroke({ color: borderColor(), width: strokeWidth, alpha: opacity() });
86
+ };
87
+ </script>
@@ -0,0 +1,20 @@
1
+ <Sprite sheet={sheet} />
2
+
3
+ <script>
4
+ import { computed } from "canvasengine";
5
+ import { RpgClientEngine } from "../../RpgClientEngine";
6
+ import { inject } from "../../core/inject";
7
+ import { resolveDynamicValue } from "./parse-value";
8
+
9
+ const { object, value } = defineProps();
10
+ const client = inject(RpgClientEngine);
11
+
12
+ const sheet = computed(() => {
13
+ const id = resolveDynamicValue(value?.(), object);
14
+ if (!id) return null;
15
+ return {
16
+ definition: client.getSpriteSheet(id),
17
+ playing: 'default'
18
+ };
19
+ });
20
+ </script>
@@ -0,0 +1,41 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { signal } from "canvasengine";
3
+ import { resolveDynamicProps, resolveDynamicValue } from "./parse-value";
4
+
5
+ describe("dynamic component values", () => {
6
+ test("resolves player properties and keeps bar placeholders for the bar renderer", () => {
7
+ const object = {
8
+ name: signal("Alex"),
9
+ hpSignal: signal(100),
10
+ _param: signal({ maxHp: 120 })
11
+ };
12
+
13
+ expect(resolveDynamicValue("HP: {hp}/{param.maxHp} {name} {$current}", object)).toBe("HP: 100/120 Alex {$current}");
14
+ });
15
+
16
+ test("keeps resolved props reactive", () => {
17
+ const object = {
18
+ name: signal("Alex"),
19
+ hpSignal: signal(100),
20
+ _param: signal({ maxHp: 120 })
21
+ };
22
+ const props: any = resolveDynamicProps({
23
+ value: "HP: {hp} {name}",
24
+ text: "{$current}/{$max} {name}",
25
+ style: {
26
+ width: "{hp}"
27
+ }
28
+ }, object);
29
+
30
+ expect(props.value()).toBe("HP: 100 Alex");
31
+ expect(props.text()).toBe("{$current}/{$max} Alex");
32
+ expect(props.style()).toEqual({ width: "100" });
33
+
34
+ object.hpSignal.set(10);
35
+ object.name.set("Sam");
36
+
37
+ expect(props.value()).toBe("HP: 10 Sam");
38
+ expect(props.text()).toBe("{$current}/{$max} Sam");
39
+ expect(props.style()).toEqual({ width: "10" });
40
+ });
41
+ });