@rpgjs/client 5.0.0-beta.1 → 5.0.0-beta.10

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 (200) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/LICENSE +19 -0
  3. package/dist/Game/AnimationManager.d.ts +1 -1
  4. package/dist/Game/AnimationManager.js +18 -9
  5. package/dist/Game/AnimationManager.js.map +1 -1
  6. package/dist/Game/AnimationManager.spec.d.ts +1 -0
  7. package/dist/Game/Event.js.map +1 -1
  8. package/dist/Game/Map.d.ts +9 -1
  9. package/dist/Game/Map.js +63 -5
  10. package/dist/Game/Map.js.map +1 -1
  11. package/dist/Game/Object.d.ts +47 -15
  12. package/dist/Game/Object.js +82 -38
  13. package/dist/Game/Object.js.map +1 -1
  14. package/dist/Game/Player.js.map +1 -1
  15. package/dist/Gui/Gui.d.ts +17 -4
  16. package/dist/Gui/Gui.js +78 -48
  17. package/dist/Gui/Gui.js.map +1 -1
  18. package/dist/Gui/Gui.spec.d.ts +1 -0
  19. package/dist/Gui/NotificationManager.js.map +1 -1
  20. package/dist/Resource.js +1 -1
  21. package/dist/Resource.js.map +1 -1
  22. package/dist/RpgClient.d.ts +57 -2
  23. package/dist/RpgClientEngine.d.ts +61 -6
  24. package/dist/RpgClientEngine.js +122 -14
  25. package/dist/RpgClientEngine.js.map +1 -1
  26. package/dist/Sound.js.map +1 -1
  27. package/dist/_virtual/{_@oxc-project_runtime@0.122.0 → _@oxc-project_runtime@0.130.0}/helpers/decorate.js +1 -1
  28. package/dist/_virtual/{_@oxc-project_runtime@0.122.0 → _@oxc-project_runtime@0.130.0}/helpers/decorateMetadata.js +1 -1
  29. package/dist/components/animations/animation.ce.js +4 -5
  30. package/dist/components/animations/animation.ce.js.map +1 -1
  31. package/dist/components/animations/hit.ce.js +19 -25
  32. package/dist/components/animations/hit.ce.js.map +1 -1
  33. package/dist/components/animations/index.js +4 -4
  34. package/dist/components/animations/index.js.map +1 -1
  35. package/dist/components/character.ce.js +406 -226
  36. package/dist/components/character.ce.js.map +1 -1
  37. package/dist/components/dynamics/bar.ce.js +96 -0
  38. package/dist/components/dynamics/bar.ce.js.map +1 -0
  39. package/dist/components/dynamics/image.ce.js +23 -0
  40. package/dist/components/dynamics/image.ce.js.map +1 -0
  41. package/dist/components/dynamics/parse-value.d.ts +3 -0
  42. package/dist/components/dynamics/parse-value.js +54 -35
  43. package/dist/components/dynamics/parse-value.js.map +1 -1
  44. package/dist/components/dynamics/parse-value.spec.d.ts +1 -0
  45. package/dist/components/dynamics/shape-utils.d.ts +16 -0
  46. package/dist/components/dynamics/shape-utils.js +73 -0
  47. package/dist/components/dynamics/shape-utils.js.map +1 -0
  48. package/dist/components/dynamics/shape-utils.spec.d.ts +1 -0
  49. package/dist/components/dynamics/shape.ce.js +83 -0
  50. package/dist/components/dynamics/shape.ce.js.map +1 -0
  51. package/dist/components/dynamics/text.ce.js +33 -56
  52. package/dist/components/dynamics/text.ce.js.map +1 -1
  53. package/dist/components/gui/box.ce.js +6 -8
  54. package/dist/components/gui/box.ce.js.map +1 -1
  55. package/dist/components/gui/dialogbox/index.ce.js +53 -60
  56. package/dist/components/gui/dialogbox/index.ce.js.map +1 -1
  57. package/dist/components/gui/gameover.ce.js +39 -63
  58. package/dist/components/gui/gameover.ce.js.map +1 -1
  59. package/dist/components/gui/hud/hud.ce.js +21 -30
  60. package/dist/components/gui/hud/hud.ce.js.map +1 -1
  61. package/dist/components/gui/menu/equip-menu.ce.js +110 -164
  62. package/dist/components/gui/menu/equip-menu.ce.js.map +1 -1
  63. package/dist/components/gui/menu/exit-menu.ce.js +6 -5
  64. package/dist/components/gui/menu/exit-menu.ce.js.map +1 -1
  65. package/dist/components/gui/menu/items-menu.ce.js +49 -67
  66. package/dist/components/gui/menu/items-menu.ce.js.map +1 -1
  67. package/dist/components/gui/menu/main-menu.ce.js +72 -90
  68. package/dist/components/gui/menu/main-menu.ce.js.map +1 -1
  69. package/dist/components/gui/menu/options-menu.ce.js +5 -4
  70. package/dist/components/gui/menu/options-menu.ce.js.map +1 -1
  71. package/dist/components/gui/menu/skills-menu.ce.js +12 -17
  72. package/dist/components/gui/menu/skills-menu.ce.js.map +1 -1
  73. package/dist/components/gui/mobile/index.js +2 -2
  74. package/dist/components/gui/mobile/index.js.map +1 -1
  75. package/dist/components/gui/mobile/mobile.ce.js +5 -4
  76. package/dist/components/gui/mobile/mobile.ce.js.map +1 -1
  77. package/dist/components/gui/notification/notification.ce.js +22 -24
  78. package/dist/components/gui/notification/notification.ce.js.map +1 -1
  79. package/dist/components/gui/save-load.ce.js +70 -248
  80. package/dist/components/gui/save-load.ce.js.map +1 -1
  81. package/dist/components/gui/shop/shop.ce.js +87 -125
  82. package/dist/components/gui/shop/shop.ce.js.map +1 -1
  83. package/dist/components/gui/title-screen.ce.js +42 -68
  84. package/dist/components/gui/title-screen.ce.js.map +1 -1
  85. package/dist/components/player-components-utils.d.ts +67 -0
  86. package/dist/components/player-components-utils.js +162 -0
  87. package/dist/components/player-components-utils.js.map +1 -0
  88. package/dist/components/player-components-utils.spec.d.ts +1 -0
  89. package/dist/components/player-components.ce.js +188 -0
  90. package/dist/components/player-components.ce.js.map +1 -0
  91. package/dist/components/prebuilt/hp-bar.ce.js +41 -44
  92. package/dist/components/prebuilt/hp-bar.ce.js.map +1 -1
  93. package/dist/components/prebuilt/light-halo.ce.js +35 -59
  94. package/dist/components/prebuilt/light-halo.ce.js.map +1 -1
  95. package/dist/components/scenes/canvas.ce.js +157 -21
  96. package/dist/components/scenes/canvas.ce.js.map +1 -1
  97. package/dist/components/scenes/draw-map.ce.js +19 -29
  98. package/dist/components/scenes/draw-map.ce.js.map +1 -1
  99. package/dist/components/scenes/event-layer.ce.js +9 -8
  100. package/dist/components/scenes/event-layer.ce.js.map +1 -1
  101. package/dist/core/inject.js +1 -1
  102. package/dist/core/inject.js.map +1 -1
  103. package/dist/core/setup.js +1 -1
  104. package/dist/core/setup.js.map +1 -1
  105. package/dist/decorators/spritesheet.d.ts +1 -0
  106. package/dist/decorators/spritesheet.js +11 -0
  107. package/dist/decorators/spritesheet.js.map +1 -0
  108. package/dist/index.d.ts +1 -0
  109. package/dist/index.js +21 -20
  110. package/dist/module.js +4 -1
  111. package/dist/module.js.map +1 -1
  112. package/dist/node_modules/.pnpm/{@signe_di@2.9.0 → @signe_di@3.0.1}/node_modules/@signe/di/dist/index.js +7 -117
  113. package/dist/node_modules/.pnpm/@signe_di@3.0.1/node_modules/@signe/di/dist/index.js.map +1 -0
  114. package/dist/node_modules/.pnpm/@signe_reactive@3.0.1/node_modules/@signe/reactive/dist/index.js +239 -0
  115. package/dist/node_modules/.pnpm/@signe_reactive@3.0.1/node_modules/@signe/reactive/dist/index.js.map +1 -0
  116. package/dist/node_modules/.pnpm/@signe_room@3.0.1/node_modules/@signe/room/dist/chunk-EUXUH3YW.js +13 -0
  117. package/dist/node_modules/.pnpm/@signe_room@3.0.1/node_modules/@signe/room/dist/chunk-EUXUH3YW.js.map +1 -0
  118. package/dist/node_modules/.pnpm/@signe_room@3.0.1/node_modules/@signe/room/dist/index.js +696 -0
  119. package/dist/node_modules/.pnpm/@signe_room@3.0.1/node_modules/@signe/room/dist/index.js.map +1 -0
  120. package/dist/node_modules/.pnpm/@signe_sync@3.0.1/node_modules/@signe/sync/dist/client/index.js +44 -0
  121. package/dist/node_modules/.pnpm/@signe_sync@3.0.1/node_modules/@signe/sync/dist/client/index.js.map +1 -0
  122. package/dist/node_modules/.pnpm/{@signe_sync@2.9.0 → @signe_sync@3.0.1}/node_modules/@signe/sync/dist/index.js +57 -141
  123. package/dist/node_modules/.pnpm/@signe_sync@3.0.1/node_modules/@signe/sync/dist/index.js.map +1 -0
  124. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-HAC622V3.js.map +1 -1
  125. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-S74YV6PU.js.map +1 -1
  126. package/dist/node_modules/.pnpm/zod@3.24.2/node_modules/zod/lib/index.js +27 -27
  127. package/dist/node_modules/.pnpm/zod@3.24.2/node_modules/zod/lib/index.js.map +1 -1
  128. package/dist/presets/animation.js.map +1 -1
  129. package/dist/presets/faceset.js.map +1 -1
  130. package/dist/presets/icon.js.map +1 -1
  131. package/dist/presets/index.js.map +1 -1
  132. package/dist/presets/lpc.js.map +1 -1
  133. package/dist/presets/rmspritesheet.js.map +1 -1
  134. package/dist/services/AbstractSocket.js.map +1 -1
  135. package/dist/services/keyboardControls.js.map +1 -1
  136. package/dist/services/loadMap.d.ts +6 -0
  137. package/dist/services/loadMap.js +1 -1
  138. package/dist/services/loadMap.js.map +1 -1
  139. package/dist/services/mmorpg.js +9 -4
  140. package/dist/services/mmorpg.js.map +1 -1
  141. package/dist/services/save.js.map +1 -1
  142. package/dist/services/save.spec.d.ts +1 -0
  143. package/dist/services/standalone.js +1 -1
  144. package/dist/services/standalone.js.map +1 -1
  145. package/dist/utils/getEntityProp.js +4 -3
  146. package/dist/utils/getEntityProp.js.map +1 -1
  147. package/dist/utils/getEntityProp.spec.d.ts +1 -0
  148. package/dist/utils/readPropValue.d.ts +2 -0
  149. package/dist/utils/readPropValue.js +13 -0
  150. package/dist/utils/readPropValue.js.map +1 -0
  151. package/package.json +13 -14
  152. package/src/Game/AnimationManager.spec.ts +30 -0
  153. package/src/Game/AnimationManager.ts +22 -10
  154. package/src/Game/Map.ts +91 -2
  155. package/src/Game/Object.ts +148 -69
  156. package/src/Gui/Gui.spec.ts +273 -0
  157. package/src/Gui/Gui.ts +105 -50
  158. package/src/Resource.ts +1 -2
  159. package/src/RpgClient.ts +63 -2
  160. package/src/RpgClientEngine.ts +173 -25
  161. package/src/components/character.ce +422 -15
  162. package/src/components/dynamics/bar.ce +87 -0
  163. package/src/components/dynamics/image.ce +20 -0
  164. package/src/components/dynamics/parse-value.spec.ts +83 -0
  165. package/src/components/dynamics/parse-value.ts +111 -37
  166. package/src/components/dynamics/shape-utils.spec.ts +46 -0
  167. package/src/components/dynamics/shape-utils.ts +61 -0
  168. package/src/components/dynamics/shape.ce +89 -0
  169. package/src/components/dynamics/text.ce +34 -149
  170. package/src/components/gui/dialogbox/index.ce +15 -6
  171. package/src/components/player-components-utils.spec.ts +109 -0
  172. package/src/components/player-components-utils.ts +205 -0
  173. package/src/components/player-components.ce +221 -0
  174. package/src/components/scenes/canvas.ce +165 -6
  175. package/src/components/scenes/draw-map.ce +2 -15
  176. package/src/components/scenes/event-layer.ce +1 -2
  177. package/src/core/setup.ts +2 -2
  178. package/src/decorators/spritesheet.ts +8 -0
  179. package/src/index.ts +1 -0
  180. package/src/module.ts +5 -1
  181. package/src/services/loadMap.ts +2 -0
  182. package/src/services/mmorpg.ts +8 -2
  183. package/src/services/save.spec.ts +127 -0
  184. package/src/utils/getEntityProp.spec.ts +96 -0
  185. package/src/utils/getEntityProp.ts +4 -3
  186. package/src/utils/readPropValue.ts +16 -0
  187. package/dist/node_modules/.pnpm/@signe_di@2.9.0/node_modules/@signe/di/dist/index.js.map +0 -1
  188. package/dist/node_modules/.pnpm/@signe_reactive@2.8.3/node_modules/@signe/reactive/dist/index.js +0 -457
  189. package/dist/node_modules/.pnpm/@signe_reactive@2.8.3/node_modules/@signe/reactive/dist/index.js.map +0 -1
  190. package/dist/node_modules/.pnpm/@signe_reactive@2.9.0/node_modules/@signe/reactive/dist/index.js +0 -463
  191. package/dist/node_modules/.pnpm/@signe_reactive@2.9.0/node_modules/@signe/reactive/dist/index.js.map +0 -1
  192. package/dist/node_modules/.pnpm/@signe_room@2.9.0/node_modules/@signe/room/dist/index.js +0 -2191
  193. package/dist/node_modules/.pnpm/@signe_room@2.9.0/node_modules/@signe/room/dist/index.js.map +0 -1
  194. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/chunk-7QVYU63E.js +0 -10
  195. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/chunk-7QVYU63E.js.map +0 -1
  196. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/client/index.js +0 -91
  197. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/client/index.js.map +0 -1
  198. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/index.js.map +0 -1
  199. package/dist/node_modules/.pnpm/dset@3.1.4/node_modules/dset/dist/index.js +0 -14
  200. package/dist/node_modules/.pnpm/dset@3.1.4/node_modules/dset/dist/index.js.map +0 -1
@@ -4,18 +4,26 @@
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) {
10
- <Sprite
11
- sheet={sheet(graphicObj)}
12
- direction
13
- tint
14
- hitbox
15
- flash={flashConfig}
16
- />
12
+ <Container scale={graphicScale(graphicObj)}>
13
+ <Sprite
14
+ sheet={sheet(graphicObj)}
15
+ direction
16
+ tint
17
+ hitbox
18
+ shadowCaster={shadowCaster(graphicObj)}
19
+ flash={flashConfig}
20
+ />
21
+ </Container>
17
22
  }
18
23
  </Container>
24
+ <PlayerComponents object position="center" graphicBounds />
25
+ <PlayerComponents object position="right" graphicBounds />
26
+ <PlayerComponents object position="top" graphicBounds />
19
27
  @for (compConfig of normalizedComponentsInFront) {
20
28
  <Container dependencies={compConfig.dependencies}>
21
29
  <compConfig.component object ...compConfig.props />
@@ -36,16 +44,18 @@
36
44
 
37
45
  <script>
38
46
  import { signal, effect, mount, computed, tick, animatedSignal, on } from "canvasengine";
47
+ import { Assets } from "pixi.js";
39
48
 
40
49
  import { lastValueFrom, combineLatest, pairwise, filter, map, startWith } from "rxjs";
41
50
  import { Particle } from "@canvasengine/presets";
42
51
  import { GameEngineToken, ModulesToken } from "@rpgjs/common";
43
52
  import { RpgClientEngine } from "../RpgClientEngine";
44
53
  import { inject } from "../core/inject";
45
- import { Direction } from "@rpgjs/common";
54
+ import { Direction, Animation } from "@rpgjs/common";
46
55
  import Hit from "./effects/hit.ce";
47
56
  import PlayerComponents from "./player-components.ce";
48
57
  import { RpgGui } from "../Gui/Gui";
58
+ import { getCanMoveValue } from "../utils/readPropValue";
49
59
 
50
60
  const { object, id } = defineProps();
51
61
 
@@ -54,10 +64,22 @@
54
64
  const guiService = inject(RpgGui);
55
65
 
56
66
  const spritesheets = client.spritesheets;
57
- const playerId = client.playerId;
58
67
  const componentsBehind = client.spriteComponentsBehind;
59
68
  const componentsInFront = client.spriteComponentsInFront;
60
- const isMe = computed(() => id() === playerId);
69
+ const readProp = (value) => typeof value === 'function' ? value() : value;
70
+ const isCurrentPlayer = () => {
71
+ const playerId = client.playerIdSignal();
72
+ const currentPlayer = playerId ? client.sceneMap?.players?.()?.[playerId] : undefined;
73
+ return readProp(id) === playerId
74
+ || readProp(object?.id) === playerId
75
+ || object === currentPlayer
76
+ || object === client.sceneMap?.getCurrentPlayer?.();
77
+ };
78
+ const isMe = computed(isCurrentPlayer);
79
+ const shadowsEnabled = computed(() => {
80
+ const lighting = client.sceneMap?.lighting?.();
81
+ return Boolean(lighting?.shadows?.enabled || (lighting?.spots?.length ?? 0) > 0);
82
+ });
61
83
 
62
84
  /**
63
85
  * Normalize a single sprite component configuration
@@ -282,7 +304,7 @@
282
304
 
283
305
  const particleSettings = client.particleSettings;
284
306
 
285
- const canControls = () => isMe() && object.canMove()
307
+ const canControls = () => isMe() && getCanMoveValue(object)
286
308
  const keyboardControls = client.globalConfig.keyboardControls;
287
309
 
288
310
  const visible = computed(() => {
@@ -292,7 +314,7 @@
292
314
  return isConnected()
293
315
  });
294
316
 
295
- const controls = signal({
317
+ const controls = {
296
318
  down: {
297
319
  repeat: true,
298
320
  bind: keyboardControls.down,
@@ -340,7 +362,7 @@
340
362
  gamepad: {
341
363
  enabled: true
342
364
  }
343
- });
365
+ };
344
366
 
345
367
  const smoothX = animatedSignal(x(), {
346
368
  duration: isMe() ? 0 : 0
@@ -377,6 +399,315 @@
377
399
  };
378
400
  }
379
401
 
402
+ const graphicScale = (graphicObject) => {
403
+ const scale = graphicObject?.scale;
404
+ if (Array.isArray(scale)) return scale;
405
+ if (typeof scale === 'number') return [scale, scale];
406
+ if (scale && typeof scale === 'object') {
407
+ const x = typeof scale.x === 'number' ? scale.x : 1;
408
+ const y = typeof scale.y === 'number' ? scale.y : x;
409
+ return [x, y];
410
+ }
411
+ return undefined;
412
+ }
413
+
414
+ const shadowCaster = (graphicObject) => {
415
+ const box = hitbox();
416
+ const bounds = graphicBounds();
417
+ const scale = graphicScale(graphicObject);
418
+ const scaleY = Array.isArray(scale) && typeof scale[1] === 'number' ? Math.abs(scale[1]) : 1;
419
+ const height = Math.max(bounds?.height ?? box?.h ?? 32, box?.h ?? 32) * scaleY;
420
+
421
+ return {
422
+ enabled: shadowsEnabled,
423
+ height,
424
+ footAnchor: { x: 0.5, y: 1 },
425
+ footOffset: { x: 0, y: 2 },
426
+ alpha: 0.5,
427
+ blur: 3.5,
428
+ gradientPower: 2,
429
+ hardness: 0.42,
430
+ minLength: Math.max(6, (box?.h ?? 32) * 0.25),
431
+ maxLength: Math.max(90, height * 1.8),
432
+ contactAlpha: 0.3,
433
+ contactScale: 0.3,
434
+ };
435
+ }
436
+
437
+ const imageDimensions = signal({});
438
+ const loadingImageDimensions = new Set();
439
+
440
+ const toPositiveNumber = (value) => {
441
+ const number = typeof value === 'number' ? value : parseFloat(value);
442
+ return Number.isFinite(number) && number > 0 ? number : undefined;
443
+ };
444
+
445
+ const toFiniteNumber = (value, fallback = 0) => {
446
+ const number = typeof value === 'number' ? value : parseFloat(value);
447
+ return Number.isFinite(number) ? number : fallback;
448
+ };
449
+
450
+ const clampRatio = (value) => Math.min(1, Math.max(0, value));
451
+
452
+ const normalizePair = (value, fallback = [1, 1]) => {
453
+ if (Array.isArray(value)) {
454
+ const x = toFiniteNumber(value[0], fallback[0]);
455
+ const y = toFiniteNumber(value[1] ?? value[0], x);
456
+ return [x, y];
457
+ }
458
+ if (typeof value === 'number') {
459
+ return [value, value];
460
+ }
461
+ if (value && typeof value === 'object') {
462
+ const x = toFiniteNumber(value.x, fallback[0]);
463
+ const y = toFiniteNumber(value.y ?? value.x, x);
464
+ return [x, y];
465
+ }
466
+ return fallback;
467
+ };
468
+
469
+ const normalizeAnchor = (value) => {
470
+ if (!Array.isArray(value)) return undefined;
471
+ const [x, y] = normalizePair(value, [0, 0]);
472
+ return [clampRatio(x), clampRatio(y)];
473
+ };
474
+
475
+ const resolveImageSource = (image) => {
476
+ if (typeof image === 'string') return image;
477
+ if (typeof image?.default === 'string') return image.default;
478
+ return undefined;
479
+ };
480
+
481
+ const parentTextureOptions = (graphicObject) => {
482
+ const props = [
483
+ 'width',
484
+ 'height',
485
+ 'framesHeight',
486
+ 'framesWidth',
487
+ 'rectWidth',
488
+ 'rectHeight',
489
+ 'offset',
490
+ 'image',
491
+ 'sound',
492
+ 'spriteRealSize',
493
+ 'scale',
494
+ 'anchor',
495
+ 'pivot',
496
+ 'x',
497
+ 'y',
498
+ 'opacity'
499
+ ];
500
+
501
+ return props.reduce((options, prop) => {
502
+ if (graphicObject?.[prop] !== undefined) {
503
+ options[prop] = graphicObject[prop];
504
+ }
505
+ return options;
506
+ }, {});
507
+ };
508
+
509
+ const resolveTextureOptions = (graphicObject) => {
510
+ const textures = graphicObject?.textures ?? {};
511
+ const texture =
512
+ textures[realAnimationName()] ??
513
+ textures[Animation.Stand] ??
514
+ Object.values(textures)[0] ??
515
+ {};
516
+
517
+ return {
518
+ ...parentTextureOptions(graphicObject),
519
+ ...texture
520
+ };
521
+ };
522
+
523
+ const resolveFirstAnimationFrame = (textureOptions) => {
524
+ const animations = textureOptions?.animations;
525
+ if (!animations) return {};
526
+
527
+ try {
528
+ const frames = typeof animations === 'function'
529
+ ? animations({ direction: direction() })
530
+ : animations;
531
+ if (!Array.isArray(frames)) return {};
532
+ const firstGroup = frames[0];
533
+ return Array.isArray(firstGroup) ? firstGroup[0] ?? {} : firstGroup ?? {};
534
+ }
535
+ catch {
536
+ return {};
537
+ }
538
+ };
539
+
540
+ const optionValue = (prop, frame, textureOptions, graphicObject) => {
541
+ return frame?.[prop] ?? textureOptions?.[prop] ?? graphicObject?.[prop];
542
+ };
543
+
544
+ const resolveFrameSize = (textureOptions, dimensions) => {
545
+ const framesWidth = toPositiveNumber(textureOptions?.framesWidth) ?? 1;
546
+ const framesHeight = toPositiveNumber(textureOptions?.framesHeight) ?? 1;
547
+ const imageSource = resolveImageSource(textureOptions?.image);
548
+ const loadedSize = imageSource ? dimensions[imageSource] : undefined;
549
+ const fullWidth = toPositiveNumber(textureOptions?.width) ?? loadedSize?.width;
550
+ const fullHeight = toPositiveNumber(textureOptions?.height) ?? loadedSize?.height;
551
+ const width = toPositiveNumber(textureOptions?.rectWidth) ??
552
+ toPositiveNumber(textureOptions?.spriteWidth) ??
553
+ (fullWidth ? fullWidth / framesWidth : undefined);
554
+ const height = toPositiveNumber(textureOptions?.rectHeight) ??
555
+ toPositiveNumber(textureOptions?.spriteHeight) ??
556
+ (fullHeight ? fullHeight / framesHeight : undefined);
557
+
558
+ return {
559
+ width,
560
+ height
561
+ };
562
+ };
563
+
564
+ const resolveHitboxAnchor = (spriteWidth, spriteHeight, realSize, box) => {
565
+ if (!spriteWidth || !spriteHeight || !box) {
566
+ return [0, 0];
567
+ }
568
+
569
+ const heightOfSprite = typeof realSize === 'number' ? realSize : realSize?.height;
570
+ const resolvedHeight = toPositiveNumber(heightOfSprite) ?? spriteHeight;
571
+ const gap = Math.max(0, (spriteHeight - resolvedHeight) / 2);
572
+ const hitboxTopLeftX = clampRatio((spriteWidth - box.w) / 2 / spriteWidth);
573
+ const hitboxTopLeftY = clampRatio((spriteHeight - box.h - gap) / spriteHeight);
574
+ const hitboxCenterX = clampRatio(hitboxTopLeftX + box.w / 2 / spriteWidth);
575
+ const hitboxCenterY = clampRatio(hitboxTopLeftY + box.h / 2 / spriteHeight);
576
+ const footY = clampRatio((spriteHeight - gap) / spriteHeight);
577
+
578
+ switch (box.anchorMode ?? 'top-left') {
579
+ case 'center':
580
+ return [hitboxCenterX, hitboxCenterY];
581
+ case 'foot':
582
+ return [hitboxCenterX, footY];
583
+ case 'top-left':
584
+ default:
585
+ return [hitboxTopLeftX, hitboxTopLeftY];
586
+ }
587
+ };
588
+
589
+ const loadImageDimensions = (image) => {
590
+ const source = resolveImageSource(image);
591
+ if (!source || imageDimensions()[source] || loadingImageDimensions.has(source)) {
592
+ return;
593
+ }
594
+
595
+ loadingImageDimensions.add(source);
596
+ Assets.load(source)
597
+ .then((texture) => {
598
+ const width = toPositiveNumber(texture?.width);
599
+ const height = toPositiveNumber(texture?.height);
600
+ if (!width || !height) return;
601
+
602
+ imageDimensions.update((dimensions) => ({
603
+ ...dimensions,
604
+ [source]: { width, height }
605
+ }));
606
+ })
607
+ .catch(() => {})
608
+ .finally(() => {
609
+ loadingImageDimensions.delete(source);
610
+ });
611
+ };
612
+
613
+ effect(() => {
614
+ const sources = new Set();
615
+
616
+ graphicsSignals().forEach((graphicObject) => {
617
+ const baseImage = resolveImageSource(graphicObject?.image);
618
+ if (baseImage) sources.add(baseImage);
619
+
620
+ Object.values(graphicObject?.textures ?? {}).forEach((textureOptions) => {
621
+ const image = resolveImageSource(textureOptions?.image ?? graphicObject?.image);
622
+ if (image) sources.add(image);
623
+ });
624
+ });
625
+
626
+ sources.forEach((source) => loadImageDimensions(source));
627
+ });
628
+
629
+ const hitboxBounds = computed(() => {
630
+ const box = hitbox();
631
+ const width = box?.w ?? 0;
632
+ const height = box?.h ?? 0;
633
+
634
+ return {
635
+ left: 0,
636
+ top: 0,
637
+ right: width,
638
+ bottom: height,
639
+ width,
640
+ height,
641
+ centerX: width / 2,
642
+ centerY: height / 2
643
+ };
644
+ });
645
+
646
+ const graphicBounds = computed(() => {
647
+ const box = hitbox();
648
+ const fallback = hitboxBounds();
649
+ const dimensions = imageDimensions();
650
+ const graphics = graphicsSignals();
651
+ let bounds = null;
652
+
653
+ graphics.forEach((graphicObject) => {
654
+ const textureOptions = resolveTextureOptions(graphicObject);
655
+ const frame = resolveFirstAnimationFrame(textureOptions);
656
+ const size = resolveFrameSize(textureOptions, dimensions);
657
+ const spriteWidth = size.width ?? box?.w;
658
+ const spriteHeight = size.height ?? box?.h;
659
+
660
+ if (!spriteWidth || !spriteHeight) {
661
+ return;
662
+ }
663
+
664
+ const explicitAnchor = normalizeAnchor(optionValue('anchor', frame, textureOptions, graphicObject));
665
+ const anchor = explicitAnchor ?? resolveHitboxAnchor(
666
+ spriteWidth,
667
+ spriteHeight,
668
+ optionValue('spriteRealSize', frame, textureOptions, graphicObject),
669
+ box
670
+ );
671
+ const scale = normalizePair(optionValue('scale', frame, textureOptions, graphicObject) ?? graphicScale(graphicObject));
672
+ const x = toFiniteNumber(optionValue('x', frame, textureOptions, graphicObject), 0);
673
+ const y = toFiniteNumber(optionValue('y', frame, textureOptions, graphicObject), 0);
674
+ const leftEdge = -anchor[0] * spriteWidth * scale[0];
675
+ const rightEdge = (1 - anchor[0]) * spriteWidth * scale[0];
676
+ const topEdge = -anchor[1] * spriteHeight * scale[1];
677
+ const bottomEdge = (1 - anchor[1]) * spriteHeight * scale[1];
678
+ const graphic = {
679
+ left: x + Math.min(leftEdge, rightEdge),
680
+ top: y + Math.min(topEdge, bottomEdge),
681
+ right: x + Math.max(leftEdge, rightEdge),
682
+ bottom: y + Math.max(topEdge, bottomEdge)
683
+ };
684
+
685
+ bounds = bounds
686
+ ? {
687
+ left: Math.min(bounds.left, graphic.left),
688
+ top: Math.min(bounds.top, graphic.top),
689
+ right: Math.max(bounds.right, graphic.right),
690
+ bottom: Math.max(bounds.bottom, graphic.bottom)
691
+ }
692
+ : graphic;
693
+ });
694
+
695
+ if (!bounds) {
696
+ return fallback;
697
+ }
698
+
699
+ const width = bounds.right - bounds.left;
700
+ const height = bounds.bottom - bounds.top;
701
+
702
+ return {
703
+ ...bounds,
704
+ width,
705
+ height,
706
+ centerX: bounds.left + width / 2,
707
+ centerY: bounds.top + height / 2
708
+ };
709
+ });
710
+
380
711
  // Combine animation change detection with movement state from smoothX/smoothY
381
712
  const movementAnimations = ['walk', 'stand'];
382
713
  const epsilon = 0; // movement threshold to consider the easing still running
@@ -400,6 +731,51 @@
400
731
  filter(([prev, curr]) => prev !== curr)
401
732
  );
402
733
 
734
+ let beforeRemovePromise = null;
735
+ let beforeRemoveTransitionValue = null;
736
+ const resolveRemoveContext = () => {
737
+ if (!object._removeTransition) return null;
738
+ const value = object._removeTransition();
739
+ if (!value || typeof value !== 'string') return null;
740
+ try {
741
+ const context = JSON.parse(value);
742
+ if (!context || typeof context !== 'object' || !context.active) return null;
743
+ context.__transitionValue = value;
744
+ return context;
745
+ }
746
+ catch {
747
+ return null;
748
+ }
749
+ };
750
+
751
+ const withTimeout = (promise, timeoutMs = 0) => {
752
+ if (!timeoutMs || timeoutMs <= 0) return promise;
753
+ return Promise.race([
754
+ promise,
755
+ new Promise((resolve) => setTimeout(resolve, timeoutMs)),
756
+ ]);
757
+ };
758
+
759
+ const runBeforeRemove = () => {
760
+ const context = resolveRemoveContext();
761
+ if (!context) return Promise.resolve();
762
+ if (beforeRemovePromise && beforeRemoveTransitionValue === context.__transitionValue) {
763
+ return beforeRemovePromise;
764
+ }
765
+ beforeRemoveTransitionValue = context.__transitionValue;
766
+ beforeRemovePromise = withTimeout(
767
+ lastValueFrom(hooks.callHooks("client-sprite-onBeforeRemove", object, context)),
768
+ context.timeoutMs
769
+ );
770
+ return beforeRemovePromise;
771
+ };
772
+
773
+ const removeTransitionSubscription = object._removeTransition?.observable?.subscribe(() => {
774
+ if (resolveRemoveContext()) {
775
+ runBeforeRemove();
776
+ }
777
+ });
778
+
403
779
  const animationMovementSubscription = combineLatest([animationChange$, moving$]).subscribe(([[prev, curr], isMoving]) => {
404
780
  if (curr == 'stand' && !isMoving) {
405
781
  realAnimationName.set(curr);
@@ -429,7 +805,34 @@
429
805
  * @example
430
806
  * await onBeforeDestroy();
431
807
  */
808
+ const waitForTemporaryAnimationEnd = (maxDuration = 1200) => {
809
+ if (!object.animationIsPlaying || !object.animationIsPlaying()) {
810
+ return Promise.resolve();
811
+ }
812
+
813
+ return new Promise((resolve) => {
814
+ let finished = false;
815
+ let timeout;
816
+ let subscription;
817
+ const finish = () => {
818
+ if (finished) return;
819
+ finished = true;
820
+ clearTimeout(timeout);
821
+ subscription?.unsubscribe();
822
+ resolve();
823
+ };
824
+ timeout = setTimeout(finish, maxDuration);
825
+ subscription = object.animationIsPlaying.observable.subscribe((isPlaying) => {
826
+ if (!isPlaying) finish();
827
+ });
828
+ if (finished) subscription.unsubscribe();
829
+ });
830
+ };
831
+
432
832
  const onBeforeDestroy = async () => {
833
+ await runBeforeRemove();
834
+ await waitForTemporaryAnimationEnd();
835
+ removeTransitionSubscription?.unsubscribe();
433
836
  animationMovementSubscription.unsubscribe();
434
837
  xSubscription.unsubscribe();
435
838
  ySubscription.unsubscribe();
@@ -440,7 +843,11 @@
440
843
  mount((element) => {
441
844
  hooks.callHooks("client-sprite-onAdd", object).subscribe()
442
845
  hooks.callHooks("client-sceneMap-onAddSprite", client.sceneMap, object).subscribe()
443
- if (isMe()) client.setKeyboardControls(element.directives.controls)
846
+ effect(() => {
847
+ if (isCurrentPlayer()) {
848
+ client.setKeyboardControls(element.directives.controls)
849
+ }
850
+ })
444
851
  })
445
852
 
446
853
  /**
@@ -467,4 +874,4 @@
467
874
  tick(() => {
468
875
  hooks.callHooks("client-sprite-onUpdate").subscribe()
469
876
  })
470
- </script>
877
+ </script>
@@ -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>