@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.
- package/dist/Game/AnimationManager.d.ts +8 -0
- package/dist/Game/Map.d.ts +7 -1
- package/dist/Gui/Gui.d.ts +128 -5
- package/dist/RpgClient.d.ts +217 -59
- package/dist/RpgClientEngine.d.ts +345 -6
- package/dist/Sound.d.ts +199 -0
- package/dist/components/animations/index.d.ts +4 -0
- package/dist/components/dynamics/parse-value.d.ts +1 -0
- package/dist/components/gui/index.d.ts +3 -3
- package/dist/components/index.d.ts +3 -1
- package/dist/components/prebuilt/index.d.ts +18 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +9 -4
- package/dist/index.js.map +1 -1
- package/dist/index10.js +149 -4
- package/dist/index10.js.map +1 -1
- package/dist/index11.js +21 -7
- package/dist/index11.js.map +1 -1
- package/dist/index12.js +6 -4
- package/dist/index12.js.map +1 -1
- package/dist/index13.js +11 -14
- package/dist/index13.js.map +1 -1
- package/dist/index14.js +8 -40
- package/dist/index14.js.map +1 -1
- package/dist/index15.js +187 -180
- package/dist/index15.js.map +1 -1
- package/dist/index16.js +104 -7
- package/dist/index16.js.map +1 -1
- package/dist/index17.js +82 -372
- package/dist/index17.js.map +1 -1
- package/dist/index18.js +361 -26
- package/dist/index18.js.map +1 -1
- package/dist/index19.js +46 -20
- package/dist/index19.js.map +1 -1
- package/dist/index2.js +683 -32
- package/dist/index2.js.map +1 -1
- package/dist/index20.js +5 -2417
- package/dist/index20.js.map +1 -1
- package/dist/index21.js +383 -97
- package/dist/index21.js.map +1 -1
- package/dist/index22.js +41 -104
- package/dist/index22.js.map +1 -1
- package/dist/index23.js +21 -67
- package/dist/index23.js.map +1 -1
- package/dist/index24.js +2632 -20
- package/dist/index24.js.map +1 -1
- package/dist/index25.js +107 -34
- package/dist/index25.js.map +1 -1
- package/dist/index26.js +69 -3
- package/dist/index26.js.map +1 -1
- package/dist/index27.js +17 -318
- package/dist/index27.js.map +1 -1
- package/dist/index28.js +24 -22
- package/dist/index28.js.map +1 -1
- package/dist/index29.js +92 -8
- package/dist/index29.js.map +1 -1
- package/dist/index3.js +68 -8
- package/dist/index3.js.map +1 -1
- package/dist/index30.js +37 -7
- package/dist/index30.js.map +1 -1
- package/dist/index31.js +18 -168
- package/dist/index31.js.map +1 -1
- package/dist/index32.js +3 -499
- package/dist/index32.js.map +1 -1
- package/dist/index33.js +332 -9
- package/dist/index33.js.map +1 -1
- package/dist/index34.js +24 -4400
- package/dist/index34.js.map +1 -1
- package/dist/index35.js +6 -311
- package/dist/index35.js.map +1 -1
- package/dist/index36.js +8 -88
- package/dist/index36.js.map +1 -1
- package/dist/index37.js +182 -56
- package/dist/index37.js.map +1 -1
- package/dist/index38.js +500 -16
- package/dist/index38.js.map +1 -1
- package/dist/index39.js +10 -18
- package/dist/index39.js.map +1 -1
- package/dist/index4.js +23 -5
- package/dist/index4.js.map +1 -1
- package/dist/index40.js +7 -0
- package/dist/index40.js.map +1 -0
- package/dist/index41.js +3690 -0
- package/dist/index41.js.map +1 -0
- package/dist/index42.js +77 -0
- package/dist/index42.js.map +1 -0
- package/dist/index43.js +6 -0
- package/dist/index43.js.map +1 -0
- package/dist/index44.js +20 -0
- package/dist/index44.js.map +1 -0
- package/dist/index45.js +146 -0
- package/dist/index45.js.map +1 -0
- package/dist/index46.js +12 -0
- package/dist/index46.js.map +1 -0
- package/dist/index47.js +113 -0
- package/dist/index47.js.map +1 -0
- package/dist/index48.js +136 -0
- package/dist/index48.js.map +1 -0
- package/dist/index49.js +137 -0
- package/dist/index49.js.map +1 -0
- package/dist/index5.js +2 -1
- package/dist/index5.js.map +1 -1
- package/dist/index50.js +112 -0
- package/dist/index50.js.map +1 -0
- package/dist/index51.js +141 -0
- package/dist/index51.js.map +1 -0
- package/dist/index52.js +9 -0
- package/dist/index52.js.map +1 -0
- package/dist/index53.js +54 -0
- package/dist/index53.js.map +1 -0
- package/dist/index6.js +1 -1
- package/dist/index6.js.map +1 -1
- package/dist/index7.js +11 -3
- package/dist/index7.js.map +1 -1
- package/dist/index8.js +68 -7
- package/dist/index8.js.map +1 -1
- package/dist/index9.js +230 -15
- package/dist/index9.js.map +1 -1
- package/dist/presets/animation.d.ts +31 -0
- package/dist/presets/faceset.d.ts +30 -0
- package/dist/presets/index.d.ts +103 -0
- package/dist/presets/lpc.d.ts +89 -0
- package/dist/services/loadMap.d.ts +123 -2
- package/dist/services/mmorpg.d.ts +9 -4
- package/dist/services/standalone.d.ts +51 -2
- package/package.json +22 -18
- package/src/Game/{EffectManager.ts → AnimationManager.ts} +3 -2
- package/src/Game/Map.ts +20 -2
- package/src/Game/Object.ts +163 -9
- package/src/Gui/Gui.ts +300 -17
- package/src/RpgClient.ts +222 -58
- package/src/RpgClientEngine.ts +804 -36
- package/src/Sound.ts +253 -0
- package/src/components/{effects → animations}/animation.ce +3 -6
- package/src/components/{effects → animations}/index.ts +1 -1
- package/src/components/character.ce +165 -37
- package/src/components/dynamics/parse-value.ts +80 -0
- package/src/components/dynamics/text.ce +183 -0
- package/src/components/gui/box.ce +17 -0
- package/src/components/gui/dialogbox/index.ce +73 -35
- package/src/components/gui/dialogbox/selection.ce +16 -1
- package/src/components/gui/index.ts +3 -4
- package/src/components/index.ts +5 -1
- package/src/components/prebuilt/hp-bar.ce +255 -0
- package/src/components/prebuilt/index.ts +21 -0
- package/src/components/scenes/draw-map.ce +6 -23
- package/src/components/scenes/event-layer.ce +9 -3
- package/src/core/setup.ts +2 -0
- package/src/index.ts +5 -2
- package/src/module.ts +72 -6
- package/src/presets/animation.ts +46 -0
- package/src/presets/faceset.ts +60 -0
- package/src/presets/index.ts +7 -1
- package/src/presets/lpc.ts +108 -0
- package/src/services/loadMap.ts +132 -3
- package/src/services/mmorpg.ts +27 -5
- package/src/services/standalone.ts +68 -6
- package/tsconfig.json +1 -1
- package/vite.config.ts +1 -1
- package/dist/Game/EffectManager.d.ts +0 -5
- package/dist/components/effects/index.d.ts +0 -4
- package/src/components/scenes/element-map.ce +0 -23
- /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={
|
|
6
|
+
width={widthBox}
|
|
7
7
|
height
|
|
8
8
|
controls
|
|
9
|
-
|
|
10
|
-
bottom={10}
|
|
9
|
+
...positionBox()
|
|
11
10
|
>
|
|
12
|
-
<Rect
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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={
|
|
18
|
+
width={widthBox}
|
|
27
19
|
height
|
|
28
20
|
alpha={contentOpacity}
|
|
29
21
|
>
|
|
30
|
-
<Container flexDirection="
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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(
|
|
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:
|
|
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
|
});
|
package/src/components/index.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
import EventLayerComponent from "./scenes/event-layer.ce";
|
|
2
|
+
import CharacterComponent from "./character.ce";
|
|
2
3
|
|
|
3
|
-
|
|
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>
|