@rpgjs/client 5.0.0-alpha.2 → 5.0.0-alpha.20

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 (163) hide show
  1. package/dist/Game/AnimationManager.d.ts +8 -0
  2. package/dist/Game/Map.d.ts +7 -1
  3. package/dist/Gui/Gui.d.ts +128 -5
  4. package/dist/RpgClient.d.ts +217 -59
  5. package/dist/RpgClientEngine.d.ts +345 -6
  6. package/dist/Sound.d.ts +199 -0
  7. package/dist/components/animations/index.d.ts +4 -0
  8. package/dist/components/dynamics/parse-value.d.ts +1 -0
  9. package/dist/components/gui/index.d.ts +3 -3
  10. package/dist/components/index.d.ts +3 -1
  11. package/dist/components/prebuilt/index.d.ts +18 -0
  12. package/dist/index.d.ts +4 -1
  13. package/dist/index.js +9 -4
  14. package/dist/index.js.map +1 -1
  15. package/dist/index10.js +149 -4
  16. package/dist/index10.js.map +1 -1
  17. package/dist/index11.js +21 -7
  18. package/dist/index11.js.map +1 -1
  19. package/dist/index12.js +6 -4
  20. package/dist/index12.js.map +1 -1
  21. package/dist/index13.js +11 -14
  22. package/dist/index13.js.map +1 -1
  23. package/dist/index14.js +8 -40
  24. package/dist/index14.js.map +1 -1
  25. package/dist/index15.js +187 -180
  26. package/dist/index15.js.map +1 -1
  27. package/dist/index16.js +104 -7
  28. package/dist/index16.js.map +1 -1
  29. package/dist/index17.js +82 -372
  30. package/dist/index17.js.map +1 -1
  31. package/dist/index18.js +361 -26
  32. package/dist/index18.js.map +1 -1
  33. package/dist/index19.js +46 -20
  34. package/dist/index19.js.map +1 -1
  35. package/dist/index2.js +683 -32
  36. package/dist/index2.js.map +1 -1
  37. package/dist/index20.js +5 -2417
  38. package/dist/index20.js.map +1 -1
  39. package/dist/index21.js +383 -97
  40. package/dist/index21.js.map +1 -1
  41. package/dist/index22.js +41 -104
  42. package/dist/index22.js.map +1 -1
  43. package/dist/index23.js +21 -67
  44. package/dist/index23.js.map +1 -1
  45. package/dist/index24.js +2632 -20
  46. package/dist/index24.js.map +1 -1
  47. package/dist/index25.js +107 -34
  48. package/dist/index25.js.map +1 -1
  49. package/dist/index26.js +69 -3
  50. package/dist/index26.js.map +1 -1
  51. package/dist/index27.js +17 -318
  52. package/dist/index27.js.map +1 -1
  53. package/dist/index28.js +24 -22
  54. package/dist/index28.js.map +1 -1
  55. package/dist/index29.js +92 -8
  56. package/dist/index29.js.map +1 -1
  57. package/dist/index3.js +68 -8
  58. package/dist/index3.js.map +1 -1
  59. package/dist/index30.js +37 -7
  60. package/dist/index30.js.map +1 -1
  61. package/dist/index31.js +18 -168
  62. package/dist/index31.js.map +1 -1
  63. package/dist/index32.js +3 -499
  64. package/dist/index32.js.map +1 -1
  65. package/dist/index33.js +332 -9
  66. package/dist/index33.js.map +1 -1
  67. package/dist/index34.js +24 -4400
  68. package/dist/index34.js.map +1 -1
  69. package/dist/index35.js +6 -311
  70. package/dist/index35.js.map +1 -1
  71. package/dist/index36.js +8 -88
  72. package/dist/index36.js.map +1 -1
  73. package/dist/index37.js +182 -56
  74. package/dist/index37.js.map +1 -1
  75. package/dist/index38.js +500 -16
  76. package/dist/index38.js.map +1 -1
  77. package/dist/index39.js +10 -18
  78. package/dist/index39.js.map +1 -1
  79. package/dist/index4.js +23 -5
  80. package/dist/index4.js.map +1 -1
  81. package/dist/index40.js +7 -0
  82. package/dist/index40.js.map +1 -0
  83. package/dist/index41.js +3690 -0
  84. package/dist/index41.js.map +1 -0
  85. package/dist/index42.js +77 -0
  86. package/dist/index42.js.map +1 -0
  87. package/dist/index43.js +6 -0
  88. package/dist/index43.js.map +1 -0
  89. package/dist/index44.js +20 -0
  90. package/dist/index44.js.map +1 -0
  91. package/dist/index45.js +146 -0
  92. package/dist/index45.js.map +1 -0
  93. package/dist/index46.js +12 -0
  94. package/dist/index46.js.map +1 -0
  95. package/dist/index47.js +113 -0
  96. package/dist/index47.js.map +1 -0
  97. package/dist/index48.js +136 -0
  98. package/dist/index48.js.map +1 -0
  99. package/dist/index49.js +137 -0
  100. package/dist/index49.js.map +1 -0
  101. package/dist/index5.js +2 -1
  102. package/dist/index5.js.map +1 -1
  103. package/dist/index50.js +112 -0
  104. package/dist/index50.js.map +1 -0
  105. package/dist/index51.js +141 -0
  106. package/dist/index51.js.map +1 -0
  107. package/dist/index52.js +9 -0
  108. package/dist/index52.js.map +1 -0
  109. package/dist/index53.js +54 -0
  110. package/dist/index53.js.map +1 -0
  111. package/dist/index6.js +1 -1
  112. package/dist/index6.js.map +1 -1
  113. package/dist/index7.js +11 -3
  114. package/dist/index7.js.map +1 -1
  115. package/dist/index8.js +68 -7
  116. package/dist/index8.js.map +1 -1
  117. package/dist/index9.js +230 -15
  118. package/dist/index9.js.map +1 -1
  119. package/dist/presets/animation.d.ts +31 -0
  120. package/dist/presets/faceset.d.ts +30 -0
  121. package/dist/presets/index.d.ts +103 -0
  122. package/dist/presets/lpc.d.ts +89 -0
  123. package/dist/services/loadMap.d.ts +123 -2
  124. package/dist/services/mmorpg.d.ts +9 -4
  125. package/dist/services/standalone.d.ts +51 -2
  126. package/package.json +22 -18
  127. package/src/Game/{EffectManager.ts → AnimationManager.ts} +3 -2
  128. package/src/Game/Map.ts +20 -2
  129. package/src/Game/Object.ts +163 -9
  130. package/src/Gui/Gui.ts +300 -17
  131. package/src/RpgClient.ts +222 -58
  132. package/src/RpgClientEngine.ts +804 -36
  133. package/src/Sound.ts +253 -0
  134. package/src/components/{effects → animations}/animation.ce +3 -6
  135. package/src/components/{effects → animations}/index.ts +1 -1
  136. package/src/components/character.ce +165 -37
  137. package/src/components/dynamics/parse-value.ts +80 -0
  138. package/src/components/dynamics/text.ce +183 -0
  139. package/src/components/gui/box.ce +17 -0
  140. package/src/components/gui/dialogbox/index.ce +73 -35
  141. package/src/components/gui/dialogbox/selection.ce +16 -1
  142. package/src/components/gui/index.ts +3 -4
  143. package/src/components/index.ts +5 -1
  144. package/src/components/prebuilt/hp-bar.ce +255 -0
  145. package/src/components/prebuilt/index.ts +21 -0
  146. package/src/components/scenes/draw-map.ce +6 -23
  147. package/src/components/scenes/event-layer.ce +9 -3
  148. package/src/core/setup.ts +2 -0
  149. package/src/index.ts +5 -2
  150. package/src/module.ts +72 -6
  151. package/src/presets/animation.ts +46 -0
  152. package/src/presets/faceset.ts +60 -0
  153. package/src/presets/index.ts +7 -1
  154. package/src/presets/lpc.ts +108 -0
  155. package/src/services/loadMap.ts +132 -3
  156. package/src/services/mmorpg.ts +27 -5
  157. package/src/services/standalone.ts +68 -6
  158. package/tsconfig.json +1 -1
  159. package/vite.config.ts +1 -1
  160. package/dist/Game/EffectManager.d.ts +0 -5
  161. package/dist/components/effects/index.d.ts +0 -4
  162. package/src/components/scenes/element-map.ce +0 -23
  163. /package/src/components/{effects → animations}/hit.ce +0 -0
@@ -0,0 +1,183 @@
1
+ <Text text={@parseDynamicValue(@component.@value, @object)} ...getComponentStyle(component) />
2
+
3
+ <script>
4
+ import { computed } from "canvasengine";
5
+ import { parseDynamicValue } from "./parse-value";
6
+
7
+ const { object } = defineProps();
8
+ const component = object._component;
9
+
10
+ /**
11
+ * Parses a numeric style value that can be a number or a string
12
+ *
13
+ * If the value is a string, it may contain dynamic references like {hp}
14
+ * which need to be parsed using parseDynamicValue. If it's a number,
15
+ * it's returned as-is wrapped in a computed.
16
+ *
17
+ * @param value - Numeric value (number or string)
18
+ * @param object - Object to resolve dynamic references from
19
+ * @returns Computed signal with the numeric value
20
+ */
21
+ const parseNumericStyleValue = (value, object) => {
22
+ if (value === undefined || value === null) {
23
+ return undefined;
24
+ }
25
+
26
+ if (typeof value === 'number') {
27
+ return value;
28
+ }
29
+
30
+ if (typeof value === 'string') {
31
+ // Check if it contains dynamic references
32
+ if (value.includes('{')) {
33
+ // Parse dynamic value and convert to number
34
+ const parsed = parseDynamicValue(value, object);
35
+ return computed(() => {
36
+ const str = parsed();
37
+ const num = parseFloat(str);
38
+ return isNaN(num) ? 0 : num;
39
+ });
40
+ } else {
41
+ // Simple string number, convert directly
42
+ const num = parseFloat(value);
43
+ return isNaN(num) ? undefined : num;
44
+ }
45
+ }
46
+
47
+ return value;
48
+ };
49
+
50
+ /**
51
+ * Maps component style properties to Canvas Engine Text component props
52
+ *
53
+ * Converts TextComponentOptions from the server to the format expected
54
+ * by the Canvas Engine Text component. Supports all text styling properties
55
+ * including fill, fontSize, fontFamily, fontStyle, fontWeight, stroke,
56
+ * opacity, wordWrap, and align. Also supports dynamic values (number | string)
57
+ * for numeric properties like fontSize and opacity.
58
+ *
59
+ * @param component - Component definition with style property
60
+ * @returns Object with Text component props
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * // Component with style
65
+ * const component = {
66
+ * style: {
67
+ * fill: '#000000',
68
+ * fontSize: 20,
69
+ * fontFamily: 'Arial',
70
+ * fontWeight: 'bold'
71
+ * }
72
+ * };
73
+ *
74
+ * const props = getComponentStyle(component);
75
+ * // Returns: { color: '#000000', size: 20, fontFamily: 'Arial', style: { fontWeight: 'bold' } }
76
+ *
77
+ * // Component with dynamic fontSize
78
+ * const component2 = {
79
+ * style: {
80
+ * fill: '#000000',
81
+ * fontSize: '{hp}', // Will be resolved from object.hp
82
+ * opacity: '0.8'
83
+ * }
84
+ * };
85
+ * ```
86
+ */
87
+ const getComponentStyle = (component) => {
88
+ if (!component.style) {
89
+ return {};
90
+ }
91
+
92
+ const style = component.style;
93
+ const result = {};
94
+
95
+ // Map fill to color (shortcut property)
96
+ // fill can be a string (hex color) or a dynamic string
97
+ if (style.fill !== undefined) {
98
+ if (typeof style.fill === 'string' && style.fill.includes('{')) {
99
+ result.color = parseDynamicValue(style.fill, object);
100
+ } else {
101
+ result.color = style.fill;
102
+ }
103
+ }
104
+
105
+ // Map fontSize to size (shortcut property)
106
+ // fontSize can be number or string (with dynamic references)
107
+ if (style.fontSize !== undefined) {
108
+ const fontSizeValue = parseNumericStyleValue(style.fontSize, object);
109
+ if (fontSizeValue !== undefined) {
110
+ result.size = fontSizeValue;
111
+ }
112
+ }
113
+
114
+ // Map fontFamily (shortcut property)
115
+ if (style.fontFamily !== undefined) {
116
+ if (typeof style.fontFamily === 'string' && style.fontFamily.includes('{')) {
117
+ result.fontFamily = parseDynamicValue(style.fontFamily, object);
118
+ } else {
119
+ result.fontFamily = style.fontFamily;
120
+ }
121
+ }
122
+
123
+ // Build style object for PixiJS Text properties
124
+ const textStyle = {};
125
+
126
+ // Font style properties
127
+ if (style.fontStyle !== undefined) {
128
+ if (typeof style.fontStyle === 'string' && style.fontStyle.includes('{')) {
129
+ textStyle.fontStyle = parseDynamicValue(style.fontStyle, object);
130
+ } else {
131
+ textStyle.fontStyle = style.fontStyle;
132
+ }
133
+ }
134
+
135
+ if (style.fontWeight !== undefined) {
136
+ if (typeof style.fontWeight === 'string' && style.fontWeight.includes('{')) {
137
+ textStyle.fontWeight = parseDynamicValue(style.fontWeight, object);
138
+ } else if (typeof style.fontWeight === 'number') {
139
+ textStyle.fontWeight = style.fontWeight;
140
+ } else {
141
+ textStyle.fontWeight = style.fontWeight;
142
+ }
143
+ }
144
+
145
+ // Stroke properties
146
+ if (style.stroke !== undefined) {
147
+ if (typeof style.stroke === 'string' && style.stroke.includes('{')) {
148
+ textStyle.stroke = parseDynamicValue(style.stroke, object);
149
+ } else {
150
+ textStyle.stroke = style.stroke;
151
+ }
152
+ }
153
+
154
+ // Opacity (can be number or string)
155
+ if (style.opacity !== undefined) {
156
+ const opacityValue = parseNumericStyleValue(style.opacity, object);
157
+ if (opacityValue !== undefined) {
158
+ textStyle.opacity = opacityValue;
159
+ }
160
+ }
161
+
162
+ // Word wrap
163
+ if (style.wordWrap !== undefined) {
164
+ textStyle.wordWrap = style.wordWrap;
165
+ }
166
+
167
+ // Text alignment
168
+ if (style.align !== undefined) {
169
+ if (typeof style.align === 'string' && style.align.includes('{')) {
170
+ textStyle.align = parseDynamicValue(style.align, object);
171
+ } else {
172
+ textStyle.align = style.align;
173
+ }
174
+ }
175
+
176
+ // Only add style prop if there are style properties
177
+ if (Object.keys(textStyle).length > 0) {
178
+ result.style = textStyle;
179
+ }
180
+
181
+ return result;
182
+ }
183
+ </script>
@@ -0,0 +1,17 @@
1
+ <Container positionType="absolute" top={top} left={left}>
2
+ <Container
3
+ anchor={[0.5, 0.5]}
4
+ >
5
+ <Rect width height color={_color} />
6
+ <Container attach={child}></Container>
7
+ </Container>
8
+ </Container>
9
+
10
+ <script>
11
+ import { RpgClientEngine, inject } from "../../index";
12
+
13
+ const { width, height, children, color, top, left } = defineProps();
14
+ const engine = inject(RpgClientEngine)
15
+ const child = children[0]
16
+ const _color = computed(() => engine.globalConfig.gui?.windowColor || color?.() || "#1a1a2e")
17
+ </script>
@@ -3,41 +3,38 @@
3
3
  ref="dialogbox"
4
4
  scale={{ x: scaleX }}
5
5
  anchor={[0.5, 0.5]}
6
- width={700}
6
+ width={widthBox}
7
7
  height
8
8
  controls
9
- positionType="absolute"
10
- bottom={10}
9
+ ...positionBox()
11
10
  >
12
- <Rect width={700} height={250} color="#1a1a2e" />
13
- <Rect
14
- x={0}
15
- y={0}
16
- width={700}
17
- height
18
- color="#1a1a2e"
19
- alpha={0.9}
20
- borderRadius={10}
21
- border
22
- shadow
23
- />
11
+ <Rect
12
+ width={widthBox}
13
+ height
14
+ color={@dialogboxStyles.@backgroundColor}
15
+ alpha={@dialogboxStyles.@backgroundOpacity} />
24
16
  <Container
25
17
  flexDirection="row"
26
- width={700}
18
+ width={widthBox}
27
19
  height
28
20
  alpha={contentOpacity}
29
21
  >
30
- <Container flexDirection="column">
31
- <Text
32
- text
33
- color="#fff"
34
- fontSize={18}
35
- margin={40}
36
- typewriter
37
- style={textStyle}
38
- />
39
- @if (visibleSelection) {
40
- <Selection selectedIndex={0} items={choices} onSelect />
22
+ <Container flexDirection="row">
23
+ <Container flexDirection="column">
24
+ <Text
25
+ text
26
+ color="#fff"
27
+ fontSize={18}
28
+ margin
29
+ typewriter
30
+ style={textStyle}
31
+ />
32
+ @if (visibleSelection) {
33
+ <Selection selectedIndex={0} items={choices} onSelect />
34
+ }
35
+ </Container>
36
+ @if (face) {
37
+ <Sprite sheet={@faceSheet(@face.@id, @face.@expression)} />
41
38
  }
42
39
  </Container>
43
40
  </Container>
@@ -59,22 +56,36 @@
59
56
 
60
57
  import { inject } from "../../../core/inject";
61
58
  import { RpgClientEngine } from "../../../RpgClientEngine";
59
+ import BoxComponent from "../box.ce";
62
60
 
63
61
  const {
64
62
  message,
65
63
  choices: _choices,
66
64
  onFinish,
67
- onInteraction
65
+ onInteraction,
66
+ face,
67
+ position,
68
+ typewriterEffect,
69
+ autoClose
68
70
  } = defineProps();
69
-
71
+
70
72
  const client = inject(RpgClientEngine);
71
73
  const keyboardControls = client.globalConfig.keyboardControls;
72
74
 
75
+ const dialogboxStyles = client.globalConfig.box.styles ?? {
76
+ backgroundColor: "#1a1a2e",
77
+ backgroundOpacity: 0.9,
78
+ }
79
+ const dialogBoxTypewriterSound = client.globalConfig?.box?.sounds?.typewriter
80
+
81
+ const sounds = client.sounds;
82
+
73
83
  client.stopProcessingInput = true;
74
84
  let isDestroyed = false;
75
85
 
76
86
  const texts = [message()]
77
- const height = signal(250);
87
+ const height = signal(256);
88
+ const margin = signal(40);
78
89
  const isTextCompleted = signal(false);
79
90
 
80
91
  const drawSpeaker = (g) => {
@@ -100,6 +111,20 @@
100
111
  duration: 500,
101
112
  });
102
113
 
114
+ const positionBox = computed(() => {
115
+ if (position() === 'bottom') {
116
+ return { positionType: 'absolute', bottom: 10 };
117
+ }
118
+ else if (position() === 'top') {
119
+ return { positionType: 'absolute', top: 10 };
120
+ }
121
+ return {};
122
+ });
123
+
124
+ const widthBox = computed(() => {
125
+ return 700;
126
+ });
127
+
103
128
  scaleX.set(1);
104
129
  contentOpacity.set(1);
105
130
 
@@ -112,6 +137,13 @@
112
137
  return typeof current === "string" ? current : current.text;
113
138
  });
114
139
 
140
+ const faceSheet = (graphicId, animationName) => {
141
+ return {
142
+ definition: client.getSpriteSheet(graphicId),
143
+ playing: animationName,
144
+ };
145
+ }
146
+
115
147
  const choices = computed(() => {
116
148
  //const current = currentText();
117
149
  //return typeof current === "string" ? null : current.choices;
@@ -122,21 +154,27 @@
122
154
 
123
155
  const triggerSkip = trigger();
124
156
 
125
- const typewriter = {
157
+ const typewriter = typewriterEffect() ? {
126
158
  speed: 0.3,
127
159
  skip: triggerSkip,
160
+ sound: {
161
+ src: sounds.get(dialogBoxTypewriterSound)?.src
162
+ },
128
163
  onComplete: () => {
129
164
  isTextCompleted.set(true);
165
+ if (autoClose()) {
166
+ setTimeout(() => {
167
+ onFinish();
168
+ }, 1000);
169
+ }
130
170
  }
131
- }
171
+ } : null;
132
172
 
133
173
  const textStyle = {
134
174
  wordWrap: true,
135
- wordWrapWidth: 700 - 256 - 80
175
+ wordWrapWidth: widthBox() - margin() * 2 - (face ? 256 : 0)
136
176
  }
137
177
 
138
- const face = signal({ x: 0, y: 0, width: 256, height: 256 });
139
-
140
178
  mount((element) => {
141
179
  const [dialogbox] = element.props.children
142
180
  return () => {
@@ -5,7 +5,7 @@
5
5
  </Container>
6
6
 
7
7
  <script>
8
- import { signal, computed, mount } from "canvasengine";
8
+ import { signal, computed, mount, Howl } from "canvasengine";
9
9
  import ItemMenu from "./itemMenu.ce";
10
10
  import { RpgClientEngine } from "../../../RpgClientEngine";
11
11
  import { inject } from "../../../core/inject";
@@ -19,6 +19,18 @@
19
19
 
20
20
  const client = inject(RpgClientEngine);
21
21
  const keyboardControls = client.globalConfig.keyboardControls;
22
+ const sounds = client.sounds;
23
+ const dialogBoxCursorSound = client.globalConfig?.box?.sounds?.cursorMove
24
+ const dialogBoxCursorSelectSound = client.globalConfig?.box?.sounds?.cursorSelect
25
+
26
+
27
+ const playDialogBoxSound = (soundId) => {
28
+ if (!soundId) return;
29
+ const sound = new Howl.Howl({
30
+ src: [sounds.get(soundId)?.src]
31
+ })
32
+ sound.play()
33
+ }
22
34
 
23
35
  const selected = (index) => {
24
36
  return computed(() => {
@@ -36,6 +48,7 @@
36
48
  down: {
37
49
  bind: keyboardControls.down,
38
50
  keyDown() {
51
+ playDialogBoxSound(dialogBoxCursorSound);
39
52
  selectedIndex.update((currentIndex) => {
40
53
  if (wrapAround) {
41
54
  return (currentIndex + 1) % items().length;
@@ -48,6 +61,7 @@
48
61
  up: {
49
62
  bind: keyboardControls.up,
50
63
  keyDown() {
64
+ playDialogBoxSound(dialogBoxCursorSound);
51
65
  selectedIndex.update((currentIndex) => {
52
66
  if (wrapAround) {
53
67
  return (currentIndex - 1 + items().length) % items().length;
@@ -61,6 +75,7 @@
61
75
  bind: keyboardControls.action,
62
76
  keyDown() {
63
77
  onSelect?.(selectedIndex());
78
+ playDialogBoxSound(dialogBoxCursorSelectSound);
64
79
  },
65
80
  },
66
81
  });
@@ -1,5 +1,4 @@
1
- import Dialogbox from "./dialogbox/index.ce";
1
+ import DialogboxComponent from "./dialogbox/index.ce";
2
+ import BoxComponent from "./box.ce";
2
3
 
3
- export const PrebuiltGui = {
4
- Dialogbox
5
- }
4
+ export { DialogboxComponent, BoxComponent }
@@ -1,3 +1,7 @@
1
1
  import EventLayerComponent from "./scenes/event-layer.ce";
2
+ import CharacterComponent from "./character.ce";
2
3
 
3
- export { EventLayerComponent }
4
+ // Prebuilt sprite components
5
+ export { HpBar } from "./prebuilt";
6
+
7
+ export { EventLayerComponent, CharacterComponent }
@@ -0,0 +1,255 @@
1
+ <!--
2
+ HP Bar Component
3
+
4
+ A beautiful, animated health bar component for displaying player HP above sprites.
5
+ Features a gradient color based on HP level, smooth animations, and modern styling.
6
+
7
+ ## Design
8
+
9
+ The bar changes color dynamically based on HP percentage:
10
+ - Green (#4ade80) when HP > 60% - Healthy state
11
+ - Yellow (#facc15) when HP 30-60% - Caution state
12
+ - Orange (#fb923c) when HP 15-30% - Danger state
13
+ - Red (#ef4444) when HP < 15% - Critical state
14
+
15
+ @example
16
+ ```ts
17
+ import HpBar from './hp-bar.ce';
18
+
19
+ // In module configuration
20
+ export default defineModule<RpgClient>({
21
+ sprite: {
22
+ componentsInFront: [HpBar]
23
+ }
24
+ })
25
+ ```
26
+ -->
27
+
28
+ <Container x={position.@x} y={position.@y}>
29
+ <!-- Background shadow for depth effect -->
30
+ <Graphics draw={drawShadow} x={1} y={1} />
31
+
32
+ <!-- Main background -->
33
+ <Graphics draw={drawBackground} />
34
+
35
+ <!-- HP fill bar -->
36
+ <Graphics draw={drawFill} />
37
+
38
+ <!-- Highlight overlay for 3D effect -->
39
+ <Graphics draw={drawHighlight} />
40
+
41
+ <!-- Border frame -->
42
+ <Graphics draw={drawBorder} />
43
+ </Container>
44
+
45
+ <script>
46
+ import { computed, animatedSignal, effect } from "canvasengine";
47
+
48
+ const { object } = defineProps();
49
+
50
+ // ====================
51
+ // Configuration
52
+ // ====================
53
+
54
+ /** Total width of the HP bar in pixels */
55
+ const barWidth = 50;
56
+
57
+ /** Total height of the HP bar in pixels */
58
+ const barHeight = 8;
59
+
60
+ /** Border radius for rounded corners */
61
+ const borderRadius = 4;
62
+
63
+ /** Inner border radius for the fill bar */
64
+ const innerRadius = 3;
65
+
66
+ /** Padding between background and fill */
67
+ const padding = 1;
68
+
69
+ /** Background color (dark theme) */
70
+ const bgColor = 0x16213e;
71
+
72
+ /** Shadow color */
73
+ const shadowColor = 0x000000;
74
+
75
+ /** Border color */
76
+ const borderColor = 0x4a5568;
77
+
78
+ // ====================
79
+ // Calculated dimensions
80
+ // ====================
81
+
82
+ /** Maximum fill width */
83
+ const maxFillWidth = barWidth - (padding * 2);
84
+
85
+ /** Fill height */
86
+ const fillHeight = barHeight - (padding * 2);
87
+
88
+ /** Highlight height (half of fill) */
89
+ const highlightHeight = Math.floor(fillHeight / 2);
90
+
91
+ // ====================
92
+ // Reactive HP values
93
+ // ====================
94
+
95
+ /** Gets hitbox dimensions for positioning */
96
+ const hitbox = object.hitbox;
97
+
98
+ /**
99
+ * Gets the current HP value from the player object
100
+ * Uses hpSignal which is synchronized from the server
101
+ */
102
+ const currentHp = computed(() => {
103
+ return object.hpSignal?.() ?? 0;
104
+ });
105
+
106
+ /**
107
+ * Gets the maximum HP value from player parameters
108
+ * Reads from _param.maxHp which contains calculated stats
109
+ */
110
+ const maxHp = computed(() => {
111
+ const params = object._param?.() ?? {};
112
+ return params.maxHp ?? 100;
113
+ });
114
+
115
+ /**
116
+ * Calculates HP percentage (0 to 1)
117
+ */
118
+ const hpPercent = computed(() => {
119
+ const max = maxHp();
120
+ if (max <= 0) return 0;
121
+ const percent = currentHp() / max;
122
+ return Math.max(0, Math.min(1, percent));
123
+ });
124
+
125
+ // ====================
126
+ // Animated values
127
+ // ====================
128
+
129
+ /**
130
+ * Animated percentage for smooth bar transitions
131
+ */
132
+ const animatedPercent = animatedSignal(hpPercent(), {
133
+ duration: 300,
134
+ easing: 'easeOutCubic'
135
+ });
136
+
137
+ // Update animated value when HP changes
138
+ effect(() => {
139
+ const newPercent = hpPercent();
140
+ animatedPercent.set(newPercent);
141
+ });
142
+
143
+ // ====================
144
+ // Visual calculations
145
+ // ====================
146
+
147
+ /**
148
+ * Position of the bar relative to the sprite
149
+ */
150
+ const position = computed(() => ({
151
+ x: (hitbox().w / 2) - (barWidth / 2),
152
+ y: -barHeight - 8
153
+ }));
154
+
155
+ /**
156
+ * Current width of the HP fill based on animated percentage
157
+ */
158
+ const fillWidth = computed(() => {
159
+ const percent = animatedPercent();
160
+ const width = maxFillWidth * percent;
161
+ // Ensure minimum visible width when HP > 0
162
+ if (percent > 0 && width < innerRadius * 2) {
163
+ return innerRadius * 2;
164
+ }
165
+ return Math.max(0, width);
166
+ });
167
+
168
+ /**
169
+ * Determines HP bar color based on current HP percentage
170
+ * Returns hex color number for PixiJS
171
+ *
172
+ * ## Color Thresholds
173
+ * - Green (0x4ade80): HP > 60% - Healthy
174
+ * - Yellow (0xfacc15): HP 30-60% - Caution
175
+ * - Orange (0xfb923c): HP 15-30% - Danger
176
+ * - Red (0xef4444): HP < 15% - Critical
177
+ */
178
+ const hpColorHex = computed(() => {
179
+ const percent = hpPercent();
180
+
181
+ if (percent > 0.6) {
182
+ return 0x4ade80; // Green - healthy
183
+ } else if (percent > 0.3) {
184
+ return 0xfacc15; // Yellow - caution
185
+ } else if (percent > 0.15) {
186
+ return 0xfb923c; // Orange - danger
187
+ } else {
188
+ return 0xef4444; // Red - critical
189
+ }
190
+ });
191
+
192
+ // ====================
193
+ // Drawing functions
194
+ // ====================
195
+
196
+ /**
197
+ * Draws the shadow behind the HP bar for depth effect
198
+ */
199
+ const drawShadow = (g) => {
200
+ g.roundRect(0, 0, barWidth, barHeight, borderRadius);
201
+ g.fill({ color: shadowColor, alpha: 0.3 });
202
+ };
203
+
204
+ /**
205
+ * Draws the main background of the HP bar
206
+ */
207
+ const drawBackground = (g) => {
208
+ g.roundRect(0, 0, barWidth, barHeight, borderRadius);
209
+ g.fill({ color: bgColor, alpha: 0.9 });
210
+ };
211
+
212
+ /**
213
+ * Draws the HP fill bar with dynamic color
214
+ */
215
+ const drawFill = (g) => {
216
+ const width = fillWidth();
217
+ if (width > 0) {
218
+ g.roundRect(padding, padding, width, fillHeight, innerRadius);
219
+ g.fill({ color: hpColorHex() });
220
+ }
221
+ };
222
+
223
+ /**
224
+ * Draws the highlight overlay for 3D effect
225
+ */
226
+ const drawHighlight = (g) => {
227
+ const width = fillWidth();
228
+ if (width > 0) {
229
+ g.roundRect(padding, padding, width, highlightHeight, innerRadius);
230
+ g.fill({ color: 0xffffff, alpha: 0.25 });
231
+ }
232
+ };
233
+
234
+ /**
235
+ * Draws the border frame around the HP bar
236
+ *
237
+ * Uses PixiJS Graphics API to create a rounded rectangle stroke
238
+ * that serves as a visual border for the bar.
239
+ *
240
+ * @param g - PixiJS Graphics object
241
+ *
242
+ * @example
243
+ * ```html
244
+ * <Graphics draw={drawBorder} />
245
+ * ```
246
+ */
247
+ const drawBorder = (g) => {
248
+ g.roundRect(0, 0, barWidth, barHeight, borderRadius);
249
+ g.stroke({
250
+ color: borderColor,
251
+ width: 1,
252
+ alpha: 0.7
253
+ });
254
+ };
255
+ </script>