@rpgjs/client 5.0.0-beta.7 → 5.0.0-beta.9

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 (158) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/Game/AnimationManager.js.map +1 -1
  3. package/dist/Game/Event.js.map +1 -1
  4. package/dist/Game/Map.d.ts +9 -1
  5. package/dist/Game/Map.js +63 -5
  6. package/dist/Game/Map.js.map +1 -1
  7. package/dist/Game/Object.d.ts +17 -9
  8. package/dist/Game/Object.js +1 -12
  9. package/dist/Game/Object.js.map +1 -1
  10. package/dist/Game/Player.js.map +1 -1
  11. package/dist/Gui/Gui.d.ts +17 -4
  12. package/dist/Gui/Gui.js +64 -34
  13. package/dist/Gui/Gui.js.map +1 -1
  14. package/dist/Gui/Gui.spec.d.ts +1 -0
  15. package/dist/Gui/NotificationManager.js.map +1 -1
  16. package/dist/Resource.js +1 -1
  17. package/dist/Resource.js.map +1 -1
  18. package/dist/RpgClient.d.ts +35 -2
  19. package/dist/RpgClientEngine.d.ts +41 -5
  20. package/dist/RpgClientEngine.js +50 -5
  21. package/dist/RpgClientEngine.js.map +1 -1
  22. package/dist/Sound.js.map +1 -1
  23. package/dist/_virtual/{_@oxc-project_runtime@0.128.0 → _@oxc-project_runtime@0.130.0}/helpers/decorate.js +1 -1
  24. package/dist/_virtual/{_@oxc-project_runtime@0.128.0 → _@oxc-project_runtime@0.130.0}/helpers/decorateMetadata.js +1 -1
  25. package/dist/components/animations/animation.ce.js.map +1 -1
  26. package/dist/components/animations/hit.ce.js.map +1 -1
  27. package/dist/components/animations/index.js.map +1 -1
  28. package/dist/components/character.ce.js +259 -5
  29. package/dist/components/character.ce.js.map +1 -1
  30. package/dist/components/dynamics/bar.ce.js +96 -0
  31. package/dist/components/dynamics/bar.ce.js.map +1 -0
  32. package/dist/components/dynamics/image.ce.js +23 -0
  33. package/dist/components/dynamics/image.ce.js.map +1 -0
  34. package/dist/components/dynamics/parse-value.d.ts +3 -0
  35. package/dist/components/dynamics/parse-value.js +51 -35
  36. package/dist/components/dynamics/parse-value.js.map +1 -1
  37. package/dist/components/dynamics/parse-value.spec.d.ts +1 -0
  38. package/dist/components/dynamics/shape-utils.d.ts +16 -0
  39. package/dist/components/dynamics/shape-utils.js +73 -0
  40. package/dist/components/dynamics/shape-utils.js.map +1 -0
  41. package/dist/components/dynamics/shape-utils.spec.d.ts +1 -0
  42. package/dist/components/dynamics/shape.ce.js +83 -0
  43. package/dist/components/dynamics/shape.ce.js.map +1 -0
  44. package/dist/components/dynamics/text.ce.js +28 -41
  45. package/dist/components/dynamics/text.ce.js.map +1 -1
  46. package/dist/components/gui/box.ce.js.map +1 -1
  47. package/dist/components/gui/dialogbox/index.ce.js +3 -3
  48. package/dist/components/gui/dialogbox/index.ce.js.map +1 -1
  49. package/dist/components/gui/gameover.ce.js +1 -1
  50. package/dist/components/gui/gameover.ce.js.map +1 -1
  51. package/dist/components/gui/hud/hud.ce.js +1 -1
  52. package/dist/components/gui/hud/hud.ce.js.map +1 -1
  53. package/dist/components/gui/menu/equip-menu.ce.js.map +1 -1
  54. package/dist/components/gui/menu/exit-menu.ce.js.map +1 -1
  55. package/dist/components/gui/menu/items-menu.ce.js.map +1 -1
  56. package/dist/components/gui/menu/main-menu.ce.js.map +1 -1
  57. package/dist/components/gui/menu/options-menu.ce.js.map +1 -1
  58. package/dist/components/gui/menu/skills-menu.ce.js.map +1 -1
  59. package/dist/components/gui/mobile/index.js.map +1 -1
  60. package/dist/components/gui/mobile/mobile.ce.js.map +1 -1
  61. package/dist/components/gui/notification/notification.ce.js.map +1 -1
  62. package/dist/components/gui/save-load.ce.js.map +1 -1
  63. package/dist/components/gui/shop/shop.ce.js +1 -1
  64. package/dist/components/gui/shop/shop.ce.js.map +1 -1
  65. package/dist/components/gui/title-screen.ce.js +2 -2
  66. package/dist/components/gui/title-screen.ce.js.map +1 -1
  67. package/dist/components/player-components-utils.d.ts +67 -0
  68. package/dist/components/player-components-utils.js +162 -0
  69. package/dist/components/player-components-utils.js.map +1 -0
  70. package/dist/components/player-components-utils.spec.d.ts +1 -0
  71. package/dist/components/player-components.ce.js +188 -0
  72. package/dist/components/player-components.ce.js.map +1 -0
  73. package/dist/components/prebuilt/hp-bar.ce.js.map +1 -1
  74. package/dist/components/prebuilt/light-halo.ce.js.map +1 -1
  75. package/dist/components/scenes/canvas.ce.js +147 -4
  76. package/dist/components/scenes/canvas.ce.js.map +1 -1
  77. package/dist/components/scenes/draw-map.ce.js +2 -8
  78. package/dist/components/scenes/draw-map.ce.js.map +1 -1
  79. package/dist/components/scenes/event-layer.ce.js.map +1 -1
  80. package/dist/core/inject.js +1 -1
  81. package/dist/core/inject.js.map +1 -1
  82. package/dist/core/setup.js +1 -1
  83. package/dist/core/setup.js.map +1 -1
  84. package/dist/decorators/spritesheet.d.ts +1 -0
  85. package/dist/decorators/spritesheet.js +11 -0
  86. package/dist/decorators/spritesheet.js.map +1 -0
  87. package/dist/index.d.ts +1 -0
  88. package/dist/index.js +3 -2
  89. package/dist/module.js +4 -1
  90. package/dist/module.js.map +1 -1
  91. package/dist/node_modules/.pnpm/{@signe_di@2.10.0 → @signe_di@3.0.1}/node_modules/@signe/di/dist/index.js +1 -1
  92. package/dist/node_modules/.pnpm/@signe_di@3.0.1/node_modules/@signe/di/dist/index.js.map +1 -0
  93. package/dist/node_modules/.pnpm/{@signe_reactive@2.9.2 → @signe_reactive@3.0.1}/node_modules/@signe/reactive/dist/index.js +15 -3
  94. package/dist/node_modules/.pnpm/@signe_reactive@3.0.1/node_modules/@signe/reactive/dist/index.js.map +1 -0
  95. package/dist/node_modules/.pnpm/@signe_room@3.0.1/node_modules/@signe/room/dist/chunk-EUXUH3YW.js +13 -0
  96. package/dist/node_modules/.pnpm/@signe_room@3.0.1/node_modules/@signe/room/dist/chunk-EUXUH3YW.js.map +1 -0
  97. package/dist/node_modules/.pnpm/{@signe_room@2.10.0 → @signe_room@3.0.1}/node_modules/@signe/room/dist/index.js +124 -39
  98. package/dist/node_modules/.pnpm/@signe_room@3.0.1/node_modules/@signe/room/dist/index.js.map +1 -0
  99. package/dist/node_modules/.pnpm/{@signe_sync@2.10.0 → @signe_sync@3.0.1}/node_modules/@signe/sync/dist/client/index.js +1 -1
  100. package/dist/node_modules/.pnpm/@signe_sync@3.0.1/node_modules/@signe/sync/dist/client/index.js.map +1 -0
  101. package/dist/node_modules/.pnpm/{@signe_sync@2.10.0 → @signe_sync@3.0.1}/node_modules/@signe/sync/dist/index.js +36 -13
  102. package/dist/node_modules/.pnpm/@signe_sync@3.0.1/node_modules/@signe/sync/dist/index.js.map +1 -0
  103. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-HAC622V3.js.map +1 -1
  104. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-S74YV6PU.js.map +1 -1
  105. package/dist/node_modules/.pnpm/zod@3.24.2/node_modules/zod/lib/index.js.map +1 -1
  106. package/dist/presets/animation.js.map +1 -1
  107. package/dist/presets/faceset.js.map +1 -1
  108. package/dist/presets/icon.js.map +1 -1
  109. package/dist/presets/index.js.map +1 -1
  110. package/dist/presets/lpc.js.map +1 -1
  111. package/dist/presets/rmspritesheet.js.map +1 -1
  112. package/dist/services/AbstractSocket.js.map +1 -1
  113. package/dist/services/keyboardControls.js.map +1 -1
  114. package/dist/services/loadMap.d.ts +6 -0
  115. package/dist/services/loadMap.js +1 -1
  116. package/dist/services/loadMap.js.map +1 -1
  117. package/dist/services/mmorpg.js +7 -3
  118. package/dist/services/mmorpg.js.map +1 -1
  119. package/dist/services/save.js.map +1 -1
  120. package/dist/services/standalone.js +1 -1
  121. package/dist/services/standalone.js.map +1 -1
  122. package/dist/utils/getEntityProp.js.map +1 -1
  123. package/package.json +10 -10
  124. package/src/Game/Map.ts +91 -2
  125. package/src/Game/Object.ts +22 -35
  126. package/src/Gui/Gui.spec.ts +273 -0
  127. package/src/Gui/Gui.ts +105 -50
  128. package/src/Resource.ts +1 -2
  129. package/src/RpgClient.ts +36 -2
  130. package/src/RpgClientEngine.ts +74 -11
  131. package/src/components/character.ce +318 -9
  132. package/src/components/dynamics/bar.ce +87 -0
  133. package/src/components/dynamics/image.ce +20 -0
  134. package/src/components/dynamics/parse-value.spec.ts +41 -0
  135. package/src/components/dynamics/parse-value.ts +102 -37
  136. package/src/components/dynamics/shape-utils.spec.ts +46 -0
  137. package/src/components/dynamics/shape-utils.ts +61 -0
  138. package/src/components/dynamics/shape.ce +89 -0
  139. package/src/components/dynamics/text.ce +34 -149
  140. package/src/components/player-components-utils.spec.ts +109 -0
  141. package/src/components/player-components-utils.ts +205 -0
  142. package/src/components/player-components.ce +221 -0
  143. package/src/components/scenes/canvas.ce +165 -6
  144. package/src/components/scenes/draw-map.ce +2 -15
  145. package/src/components/scenes/event-layer.ce +1 -2
  146. package/src/core/setup.ts +2 -2
  147. package/src/decorators/spritesheet.ts +8 -0
  148. package/src/index.ts +1 -0
  149. package/src/module.ts +5 -1
  150. package/src/services/loadMap.ts +2 -0
  151. package/src/services/mmorpg.ts +8 -2
  152. package/dist/node_modules/.pnpm/@signe_di@2.10.0/node_modules/@signe/di/dist/index.js.map +0 -1
  153. package/dist/node_modules/.pnpm/@signe_reactive@2.10.0/node_modules/@signe/reactive/dist/index.js +0 -45
  154. package/dist/node_modules/.pnpm/@signe_reactive@2.10.0/node_modules/@signe/reactive/dist/index.js.map +0 -1
  155. package/dist/node_modules/.pnpm/@signe_reactive@2.9.2/node_modules/@signe/reactive/dist/index.js.map +0 -1
  156. package/dist/node_modules/.pnpm/@signe_room@2.10.0/node_modules/@signe/room/dist/index.js.map +0 -1
  157. package/dist/node_modules/.pnpm/@signe_sync@2.10.0/node_modules/@signe/sync/dist/client/index.js.map +0 -1
  158. package/dist/node_modules/.pnpm/@signe_sync@2.10.0/node_modules/@signe/sync/dist/index.js.map +0 -1
@@ -0,0 +1,109 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import {
3
+ computeBlockPosition,
4
+ computeBlockSize,
5
+ estimateComponentSize,
6
+ getComponentProps
7
+ } from "./player-components-utils";
8
+
9
+ const graphic = {
10
+ left: -8,
11
+ top: -24,
12
+ right: 40,
13
+ bottom: 48,
14
+ width: 48,
15
+ height: 72,
16
+ centerX: 16,
17
+ centerY: 12
18
+ };
19
+
20
+ describe("player component layout utilities", () => {
21
+ test("bottom layout uses the hitbox as positioning rectangle", () => {
22
+ const rowMetrics = [
23
+ {
24
+ cells: [{ width: 32, height: 32 }],
25
+ width: 32,
26
+ height: 32
27
+ }
28
+ ];
29
+
30
+ const size = computeBlockSize({
31
+ position: "bottom",
32
+ rowMetrics,
33
+ graphic,
34
+ hitbox: { w: 32, h: 32 }
35
+ });
36
+
37
+ expect(size).toEqual({ width: 32, height: 32 });
38
+ expect(
39
+ computeBlockPosition({
40
+ position: "bottom",
41
+ size,
42
+ graphic,
43
+ hitbox: { w: 32, h: 32 }
44
+ })
45
+ ).toEqual({ x: 0, y: 0 });
46
+ });
47
+
48
+ test("bottom layout centers smaller content in the hitbox rectangle", () => {
49
+ const size = computeBlockSize({
50
+ position: "bottom",
51
+ rowMetrics: [
52
+ {
53
+ cells: [{ width: 16, height: 8 }],
54
+ width: 16,
55
+ height: 8
56
+ }
57
+ ],
58
+ graphic,
59
+ hitbox: { w: 32, h: 32 }
60
+ });
61
+
62
+ expect(size).toEqual({ width: 32, height: 32 });
63
+ expect(
64
+ computeBlockPosition({
65
+ position: "bottom",
66
+ size,
67
+ graphic,
68
+ hitbox: { w: 32, h: 32 }
69
+ })
70
+ ).toEqual({ x: 0, y: 0 });
71
+ });
72
+
73
+ test("bottom margins move from the hitbox-centered position", () => {
74
+ const position = computeBlockPosition({
75
+ position: "bottom",
76
+ size: { width: 32, height: 32 },
77
+ layout: { marginLeft: 3, marginRight: 1, marginBottom: 16, marginTop: 4 },
78
+ graphic,
79
+ hitbox: { w: 32, h: 32 }
80
+ });
81
+
82
+ expect(position).toEqual({ x: 2, y: 12 });
83
+ });
84
+
85
+ test("top layout remains anchored above graphic bounds", () => {
86
+ const position = computeBlockPosition({
87
+ position: "top",
88
+ size: { width: 50, height: 12 },
89
+ graphic,
90
+ hitbox: { w: 32, h: 32 }
91
+ });
92
+
93
+ expect(position).toEqual({ x: -9, y: -36 });
94
+ });
95
+
96
+ test("estimates bar labels, shape aliases, circles, lines and polygons", () => {
97
+ expect(estimateComponentSize({ type: "hpBar", style: { width: 60, height: 6 }, text: "{$percent}%" })).toEqual({ width: 60, height: 18 });
98
+ expect(estimateComponentSize({ type: "shape", value: { type: "rect", width: 32, height: 24 } })).toEqual({ width: 32, height: 24 });
99
+ expect(estimateComponentSize({ type: "shape", value: { type: "circle", radius: 7 } })).toEqual({ width: 14, height: 14 });
100
+ expect(estimateComponentSize({ type: "shape", value: { type: "line", x1: -4, y1: 2, x2: 6, y2: 2 } })).toEqual({ width: 10, height: 1 });
101
+ expect(estimateComponentSize({ type: "shape", value: { type: "polygon", points: [-5, 2, 15, 2, 5, 12] } })).toEqual({ width: 20, height: 10 });
102
+ });
103
+
104
+ test("legacy hp and sp bars receive default fill colors", () => {
105
+ expect(getComponentProps({ type: "hpBar" }).style.fillColor).toBe("#ef4444");
106
+ expect(getComponentProps({ type: "spBar" }).style.fillColor).toBe("#3b82f6");
107
+ expect(getComponentProps({ type: "hpBar", style: { fillColor: "#111111" } }).style.fillColor).toBe("#111111");
108
+ });
109
+ });
@@ -0,0 +1,205 @@
1
+ export const DEFAULT_HP_BAR_STYLE = { fillColor: '#ef4444' };
2
+ export const DEFAULT_SP_BAR_STYLE = { fillColor: '#3b82f6' };
3
+
4
+ const DEFAULT_CELL_HEIGHT = 16;
5
+ const DEFAULT_CELL_WIDTH = 32;
6
+
7
+ type NumberResolver = (value: any, fallback?: number) => number;
8
+
9
+ const defaultToNumber: NumberResolver = (value, fallback = 0) => {
10
+ const num = typeof value === 'number' ? value : parseFloat(value);
11
+ return Number.isFinite(num) ? num : fallback;
12
+ };
13
+
14
+ export function getPointBounds(points: any[] = [], toNumber: NumberResolver = defaultToNumber) {
15
+ if (!Array.isArray(points) || points.length < 2) {
16
+ return { width: 1, height: 1 };
17
+ }
18
+
19
+ let minX = Infinity;
20
+ let minY = Infinity;
21
+ let maxX = -Infinity;
22
+ let maxY = -Infinity;
23
+
24
+ for (let i = 0; i < points.length; i += 2) {
25
+ const x = toNumber(points[i], 0);
26
+ const y = toNumber(points[i + 1], 0);
27
+ minX = Math.min(minX, x);
28
+ minY = Math.min(minY, y);
29
+ maxX = Math.max(maxX, x);
30
+ maxY = Math.max(maxY, y);
31
+ }
32
+
33
+ return {
34
+ width: Math.max(1, maxX - minX),
35
+ height: Math.max(1, maxY - minY)
36
+ };
37
+ }
38
+
39
+ export function getComponentId(definition: any) {
40
+ if (!definition) return undefined;
41
+ if (definition.id) return definition.id;
42
+ if (definition.type === 'text') return 'rpg:text';
43
+ if (definition.type === 'hpBar') return 'rpg:hpBar';
44
+ if (definition.type === 'spBar') return 'rpg:spBar';
45
+ if (definition.type === 'bar') return 'rpg:bar';
46
+ if (definition.type === 'shape') return 'rpg:shape';
47
+ if (definition.type === 'image') return 'rpg:image';
48
+ if (definition.type === 'tile') return 'rpg:tile';
49
+ return definition.type;
50
+ }
51
+
52
+ export function getComponentProps(definition: any) {
53
+ if (definition.props) return definition.props;
54
+
55
+ if (definition.type === 'text') {
56
+ return { value: definition.value, style: definition.style };
57
+ }
58
+ if (definition.type === 'hpBar') {
59
+ return { current: '{hp}', max: '{param.maxHp}', style: { ...DEFAULT_HP_BAR_STYLE, ...definition.style }, text: definition.text };
60
+ }
61
+ if (definition.type === 'spBar') {
62
+ return { current: '{sp}', max: '{param.maxSp}', style: { ...DEFAULT_SP_BAR_STYLE, ...definition.style }, text: definition.text };
63
+ }
64
+ if (definition.type === 'bar') {
65
+ return { current: `{${definition.current}}`, max: `{${definition.max}}`, style: definition.style, text: definition.text };
66
+ }
67
+ if (definition.type === 'shape') {
68
+ return definition.value;
69
+ }
70
+ if (definition.type === 'image' || definition.type === 'tile') {
71
+ return { value: definition.value };
72
+ }
73
+
74
+ return {};
75
+ }
76
+
77
+ export function estimateComponentSize(
78
+ definition: any,
79
+ {
80
+ toNumber = defaultToNumber,
81
+ estimateTextWidth = (value: any) => String(value ?? '').length * 8
82
+ }: {
83
+ toNumber?: NumberResolver;
84
+ estimateTextWidth?: (value: any, style?: any) => number;
85
+ } = {}
86
+ ) {
87
+ const props = getComponentProps(definition);
88
+ const style = props?.style ?? definition?.style ?? {};
89
+
90
+ if (definition?.type === 'text' || definition?.id === 'rpg:text') {
91
+ return {
92
+ width: estimateTextWidth(props.value ?? definition.value, style),
93
+ height: toNumber(style.fontSize, 12)
94
+ };
95
+ }
96
+
97
+ if (definition?.type === 'hpBar' || definition?.type === 'spBar' || definition?.type === 'bar' || definition?.id === 'rpg:hpBar' || definition?.id === 'rpg:spBar' || definition?.id === 'rpg:bar') {
98
+ const barHeight = toNumber(style.height, 8);
99
+ const labelHeight = props.text != null && props.text !== '' ? toNumber(style.fontSize, 10) + 2 : 0;
100
+ return {
101
+ width: toNumber(style.width, 50),
102
+ height: barHeight + labelHeight
103
+ };
104
+ }
105
+
106
+ if (definition?.type === 'shape' || definition?.id === 'rpg:shape') {
107
+ const shape = props ?? {};
108
+ if (shape.type === 'circle') {
109
+ const radius = toNumber(shape.radius, 8);
110
+ return { width: radius * 2, height: radius * 2 };
111
+ }
112
+ if (shape.type === 'line') {
113
+ return {
114
+ width: Math.max(1, Math.abs(toNumber(shape.x2, 16) - toNumber(shape.x1, 0))),
115
+ height: Math.max(1, Math.abs(toNumber(shape.y2, 0) - toNumber(shape.y1, 0)))
116
+ };
117
+ }
118
+ if (shape.type === 'polygon') {
119
+ return getPointBounds(shape.points, toNumber);
120
+ }
121
+ return {
122
+ width: toNumber(shape.width, 16),
123
+ height: toNumber(shape.height, 16)
124
+ };
125
+ }
126
+
127
+ return {
128
+ width: toNumber(definition?.props?.width, DEFAULT_CELL_WIDTH),
129
+ height: toNumber(definition?.props?.height, DEFAULT_CELL_HEIGHT)
130
+ };
131
+ }
132
+
133
+ export function computeBlockSize({
134
+ position,
135
+ layout = {},
136
+ rowMetrics,
137
+ gap = { row: 0, column: 0 },
138
+ graphic,
139
+ hitbox
140
+ }: {
141
+ position: string;
142
+ layout?: any;
143
+ rowMetrics: Array<{ width: number; height: number; cells: any[] }>;
144
+ gap?: { row: number; column: number };
145
+ graphic: { width: number; height: number };
146
+ hitbox: { w: number; h: number };
147
+ }) {
148
+ const rowGapTotal = Math.max(0, rowMetrics.length - 1) * gap.row;
149
+ const maxColumns = rowMetrics.reduce((max, row) => Math.max(max, row.cells.length), 0);
150
+ const columnGapTotal = Math.max(0, maxColumns - 1) * gap.column;
151
+ const contentWidth = rowMetrics.reduce((max, row) => Math.max(max, row.width), 0) + columnGapTotal;
152
+ const contentHeight = rowMetrics.reduce((sum, row) => sum + row.height, 0) + rowGapTotal;
153
+ const width = layout.width ?? Math.max(contentWidth, position === 'bottom' ? hitbox.w : position === 'top' || position === 'center' ? graphic.width : contentWidth);
154
+ const height = layout.height ?? Math.max(contentHeight, position === 'bottom' ? hitbox.h : position === 'left' || position === 'right' || position === 'center' ? graphic.height : contentHeight);
155
+
156
+ return { width, height };
157
+ }
158
+
159
+ export function computeBlockPosition({
160
+ position,
161
+ size,
162
+ layout = {},
163
+ graphic,
164
+ hitbox
165
+ }: {
166
+ position: string;
167
+ size: { width: number; height: number };
168
+ layout?: any;
169
+ graphic: { left: number; top: number; right: number; centerX: number; centerY: number };
170
+ hitbox: { w: number; h: number };
171
+ }) {
172
+ const marginLeft = layout.marginLeft ?? 0;
173
+ const marginRight = layout.marginRight ?? 0;
174
+ const marginTop = layout.marginTop ?? 0;
175
+ const marginBottom = layout.marginBottom ?? 0;
176
+
177
+ switch (position) {
178
+ case 'bottom':
179
+ return {
180
+ x: (hitbox.w / 2) - (size.width / 2) + marginLeft - marginRight,
181
+ y: (hitbox.h / 2) - (size.height / 2) + marginBottom - marginTop
182
+ };
183
+ case 'center':
184
+ return {
185
+ x: graphic.centerX - (size.width / 2) + marginLeft - marginRight,
186
+ y: graphic.centerY - (size.height / 2) + marginTop - marginBottom
187
+ };
188
+ case 'left':
189
+ return {
190
+ x: graphic.left - size.width + marginLeft - marginRight,
191
+ y: graphic.centerY - (size.height / 2) + marginTop - marginBottom
192
+ };
193
+ case 'right':
194
+ return {
195
+ x: graphic.right + marginLeft - marginRight,
196
+ y: graphic.centerY - (size.height / 2) + marginTop - marginBottom
197
+ };
198
+ case 'top':
199
+ default:
200
+ return {
201
+ x: graphic.centerX - (size.width / 2) + marginLeft - marginRight,
202
+ y: graphic.top - size.height + marginTop - marginBottom
203
+ };
204
+ }
205
+ }
@@ -0,0 +1,221 @@
1
+ <Container
2
+ x={blockPosition().x}
3
+ y={blockPosition().y}
4
+ width={blockSize().width}
5
+ height={blockSize().height}
6
+ minWidth={blockSize().width}
7
+ minHeight={blockSize().height}
8
+ display="flex"
9
+ flexDirection="column"
10
+ justifyContent="center"
11
+ alignItems="center"
12
+ rowGap={gap().row}
13
+ >
14
+ @for (row of renderedRows) {
15
+ <Container
16
+ width="100%"
17
+ height={row.height}
18
+ display="flex"
19
+ flexDirection="row"
20
+ justifyContent="center"
21
+ alignItems="center"
22
+ columnGap={gap().column}
23
+ >
24
+ @for (entry of row.entries) {
25
+ <Container
26
+ width={entry.width}
27
+ height={entry.height}
28
+ display="flex"
29
+ justifyContent="center"
30
+ alignItems="center"
31
+ >
32
+ <entry.component object ...entry.props />
33
+ </Container>
34
+ }
35
+ </Container>
36
+ }
37
+ </Container>
38
+
39
+ <script>
40
+ import { computed } from "canvasengine";
41
+ import { RpgClientEngine } from "../RpgClientEngine";
42
+ import { inject } from "../core/inject";
43
+ import { resolveDynamicProps, resolveDynamicValue } from "./dynamics/parse-value";
44
+ import {
45
+ computeBlockPosition,
46
+ computeBlockSize,
47
+ estimateComponentSize,
48
+ getComponentId,
49
+ getComponentProps
50
+ } from "./player-components-utils";
51
+
52
+ const { object, position, graphicBounds } = defineProps({
53
+ position: {
54
+ default: 'top'
55
+ }
56
+ });
57
+
58
+ const client = inject(RpgClientEngine);
59
+ const warnedComponents = new Set();
60
+
61
+ const readPosition = computed(() => position?.() ?? 'top');
62
+
63
+ const componentSource = computed(() => {
64
+ switch (readPosition()) {
65
+ case 'bottom':
66
+ return object.componentsBottom?.();
67
+ case 'center':
68
+ return object.componentsCenter?.();
69
+ case 'left':
70
+ return object.componentsLeft?.();
71
+ case 'right':
72
+ return object.componentsRight?.();
73
+ case 'top':
74
+ default:
75
+ return object.componentsTop?.();
76
+ }
77
+ });
78
+
79
+ const componentData = computed(() => {
80
+ const value = componentSource();
81
+ if (!value) {
82
+ return { components: [], layout: {} };
83
+ }
84
+
85
+ if (typeof value !== 'string') {
86
+ return value;
87
+ }
88
+
89
+ try {
90
+ return JSON.parse(value);
91
+ } catch (error) {
92
+ if (typeof process === 'undefined' || process.env?.NODE_ENV !== 'production') {
93
+ console.warn('[RPGJS] Invalid server sprite component payload', error);
94
+ }
95
+ return { components: [], layout: {} };
96
+ }
97
+ });
98
+
99
+ const layout = computed(() => componentData()?.layout ?? {});
100
+ const rows = computed(() => componentData()?.components ?? []);
101
+ const hitbox = object.hitbox;
102
+
103
+ const toNumber = (value, fallback = 0) => {
104
+ const resolved = resolveDynamicValue(value, object);
105
+ const num = typeof resolved === 'number' ? resolved : parseFloat(resolved);
106
+ return Number.isFinite(num) ? num : fallback;
107
+ };
108
+
109
+ const estimateTextWidth = (value, style = {}) => {
110
+ const text = String(resolveDynamicValue(value ?? '', object) ?? '');
111
+ const fontSize = toNumber(style.fontSize, 12);
112
+ return Math.max(1, Math.ceil(text.length * fontSize * 0.6));
113
+ };
114
+
115
+ const rowMetrics = computed(() => {
116
+ return rows().map((row) => {
117
+ const cells = row.map((definition) => {
118
+ const intrinsic = estimateComponentSize(definition, { toNumber, estimateTextWidth });
119
+ return {
120
+ definition,
121
+ width: intrinsic.width,
122
+ height: intrinsic.height
123
+ };
124
+ });
125
+
126
+ return {
127
+ cells,
128
+ width: cells.reduce((sum, cell) => sum + cell.width, 0),
129
+ height: cells.reduce((max, cell) => Math.max(max, cell.height), 0)
130
+ };
131
+ });
132
+ });
133
+
134
+ const gap = computed(() => ({
135
+ row: toNumber(layout().rowGap ?? layout().gap, 0),
136
+ column: toNumber(layout().columnGap ?? layout().gap, 0)
137
+ }));
138
+
139
+ const fallbackBounds = computed(() => {
140
+ const box = hitbox();
141
+ const width = box?.w ?? 0;
142
+ const height = box?.h ?? 0;
143
+
144
+ return {
145
+ left: 0,
146
+ top: 0,
147
+ right: width,
148
+ bottom: height,
149
+ width,
150
+ height,
151
+ centerX: width / 2,
152
+ centerY: height / 2
153
+ };
154
+ });
155
+
156
+ const bounds = computed(() => {
157
+ const resolvedBounds = typeof graphicBounds === 'function' ? graphicBounds() : undefined;
158
+ return resolvedBounds ?? fallbackBounds();
159
+ });
160
+
161
+ const blockSize = computed(() => {
162
+ const box = hitbox() ?? { w: 0, h: 0 };
163
+ const graphic = bounds();
164
+ return computeBlockSize({
165
+ position: readPosition(),
166
+ layout: layout(),
167
+ rowMetrics: rowMetrics(),
168
+ gap: gap(),
169
+ graphic,
170
+ hitbox: box
171
+ });
172
+ });
173
+
174
+ const blockPosition = computed(() => {
175
+ const box = hitbox() ?? { w: 0, h: 0 };
176
+ const graphic = bounds();
177
+ return computeBlockPosition({
178
+ position: readPosition(),
179
+ size: blockSize(),
180
+ layout: layout(),
181
+ graphic,
182
+ hitbox: box
183
+ });
184
+ });
185
+
186
+ const warnMissingComponent = (id) => {
187
+ if (!id || warnedComponents.has(id)) return;
188
+ if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production') return;
189
+ warnedComponents.add(id);
190
+ console.warn(`[RPGJS] Server sprite component "${id}" is not registered on the client.`);
191
+ };
192
+
193
+ const renderedRows = computed(() => {
194
+ return rowMetrics().map((row) => {
195
+ const entries = [];
196
+
197
+ row.cells.forEach((cell) => {
198
+ const definition = cell.definition;
199
+ const id = getComponentId(definition);
200
+ const component = client.getSpriteComponent(id);
201
+
202
+ if (!component) {
203
+ warnMissingComponent(id);
204
+ return;
205
+ }
206
+
207
+ entries.push({
208
+ component,
209
+ props: resolveDynamicProps(getComponentProps(definition), object),
210
+ width: cell.width,
211
+ height: cell.height
212
+ });
213
+ });
214
+
215
+ return {
216
+ height: row.height,
217
+ entries
218
+ };
219
+ });
220
+ });
221
+ </script>