@rpgjs/client 5.0.0-beta.20 → 5.0.0-beta.23
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/CHANGELOG.md +16 -0
- package/dist/Game/Object.d.ts +1 -0
- package/dist/Game/Object.js +10 -0
- package/dist/Game/Object.js.map +1 -1
- package/dist/Gui/Gui.js +37 -4
- package/dist/Gui/Gui.js.map +1 -1
- package/dist/RpgClientEngine.d.ts +18 -9
- package/dist/RpgClientEngine.js +40 -13
- package/dist/RpgClientEngine.js.map +1 -1
- package/dist/components/character-hitbox.d.ts +13 -0
- package/dist/components/character-hitbox.js +38 -0
- package/dist/components/character-hitbox.js.map +1 -0
- package/dist/components/character-hitbox.spec.d.ts +1 -0
- package/dist/components/character.ce.js +179 -36
- package/dist/components/character.ce.js.map +1 -1
- package/dist/components/gui/mobile/index.d.ts +51 -2
- package/dist/components/gui/mobile/index.js +12 -4
- package/dist/components/gui/mobile/index.js.map +1 -1
- package/dist/components/gui/mobile/index.spec.d.ts +1 -0
- package/dist/components/gui/mobile/mobile.ce.js +303 -55
- package/dist/components/gui/mobile/mobile.ce.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/services/cameraFollow.d.ts +51 -0
- package/dist/services/cameraFollow.js +134 -0
- package/dist/services/cameraFollow.js.map +1 -0
- package/dist/services/cameraFollow.spec.d.ts +1 -0
- package/dist/services/standalone.d.ts +6 -1
- package/dist/services/standalone.js +35 -17
- package/dist/services/standalone.js.map +1 -1
- package/dist/utils/syncHitbox.d.ts +1 -0
- package/dist/utils/syncHitbox.js +69 -0
- package/dist/utils/syncHitbox.js.map +1 -0
- package/dist/utils/syncHitbox.spec.d.ts +1 -0
- package/package.json +5 -5
- package/src/Game/Object.spec.ts +44 -4
- package/src/Game/Object.ts +19 -0
- package/src/Gui/Gui.spec.ts +19 -0
- package/src/Gui/Gui.ts +50 -11
- package/src/RpgClientEngine.ts +56 -24
- package/src/components/character-hitbox.spec.ts +33 -0
- package/src/components/character-hitbox.ts +72 -0
- package/src/components/character.ce +207 -48
- package/src/components/gui/mobile/index.spec.ts +94 -0
- package/src/components/gui/mobile/index.ts +74 -6
- package/src/components/gui/mobile/mobile.ce +347 -65
- package/src/index.ts +12 -0
- package/src/services/cameraFollow.spec.ts +220 -0
- package/src/services/cameraFollow.ts +222 -0
- package/src/services/standalone.spec.ts +47 -0
- package/src/services/standalone.ts +53 -20
- package/src/utils/syncHitbox.spec.ts +79 -0
- package/src/utils/syncHitbox.ts +104 -0
|
@@ -1,80 +1,362 @@
|
|
|
1
|
-
<Container justifyContent="space-between" alignItems="flex-end" width="100%" height="100%">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
},
|
|
22
|
-
text: {
|
|
23
|
-
fontSize: 24,
|
|
24
|
-
fontFamily: "Arial Bold",
|
|
25
|
-
color: "#ffffff"
|
|
26
|
-
}
|
|
27
|
-
}}
|
|
28
|
-
/>
|
|
29
|
-
|
|
30
|
-
<Button
|
|
31
|
-
text="B"
|
|
32
|
-
shape="circle"
|
|
33
|
-
width={70}
|
|
34
|
-
height={70}
|
|
35
|
-
controls={controlsInstance}
|
|
36
|
-
controlName="back"
|
|
37
|
-
style={{
|
|
38
|
-
backgroundColor: {
|
|
39
|
-
normal: "#e74c3c",
|
|
40
|
-
hover: "#c0392b",
|
|
41
|
-
pressed: "#a93226",
|
|
42
|
-
disabled: "#7f8c8d"
|
|
43
|
-
},
|
|
44
|
-
text: {
|
|
45
|
-
fontSize: 24,
|
|
46
|
-
fontFamily: "Arial Bold",
|
|
47
|
-
color: "#ffffff"
|
|
48
|
-
}
|
|
49
|
-
}}
|
|
50
|
-
/>
|
|
1
|
+
<Container flexDirection={rootDirection()} justifyContent="space-between" alignItems="flex-end" width="100%" height="100%" onBeforeDestroy={releaseJoystickControl}>
|
|
2
|
+
<Container justifyContent="flex-end" alignItems="flex-end" gap={layoutGap()} margin={buttonsMargin()}>
|
|
3
|
+
<Container display="flex" flexDirection="row" alignItems="flex-end" gap={layoutGap()}>
|
|
4
|
+
@if (showDashButton()) {
|
|
5
|
+
<Container ...buttonContainerProps("dash")>
|
|
6
|
+
<dashButtonComponent() ...buttonProps("dash") />
|
|
7
|
+
</Container>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@if (showActionButton()) {
|
|
11
|
+
<Container ...buttonContainerProps("action")>
|
|
12
|
+
<actionButtonComponent() ...buttonProps("action") />
|
|
13
|
+
</Container>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@if (showBackButton()) {
|
|
17
|
+
<Container ...buttonContainerProps("back")>
|
|
18
|
+
<backButtonComponent() ...buttonProps("back") />
|
|
19
|
+
</Container>
|
|
20
|
+
}
|
|
51
21
|
|
|
52
22
|
</Container>
|
|
53
23
|
</Container>
|
|
54
|
-
|
|
55
|
-
<Container margin={
|
|
56
|
-
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
outerColor="#34495e"
|
|
60
|
-
innerColor="#3498db"
|
|
61
|
-
/>
|
|
62
|
-
</Container>
|
|
24
|
+
|
|
25
|
+
<Container margin={joystickMargin()} alignItems="flex-end">
|
|
26
|
+
@if (showJoystick()) {
|
|
27
|
+
<joystickComponent() ...joystickProps() />
|
|
28
|
+
}
|
|
63
29
|
</Container>
|
|
64
30
|
</Container>
|
|
65
31
|
|
|
66
32
|
<script>
|
|
67
|
-
import { signal, mount } from 'canvasengine'
|
|
68
|
-
import { Button } from 'canvasengine'
|
|
33
|
+
import { Button, Joystick, signal, mount, computed } from 'canvasengine'
|
|
69
34
|
import { inject } from '../../../core/inject'
|
|
70
|
-
import { RpgClientEngine } from '../../../RpgClientEngine'
|
|
71
|
-
import { Direction } from '@rpgjs/common'
|
|
72
|
-
|
|
73
35
|
|
|
36
|
+
const { data } = defineProps()
|
|
74
37
|
const controlsInstance = signal(null)
|
|
38
|
+
const options = computed(() => data?.() || {})
|
|
39
|
+
const layout = computed(() => options().layout || {})
|
|
40
|
+
const components = computed(() => options().components || {})
|
|
41
|
+
const buttons = computed(() => options().buttons || {})
|
|
42
|
+
const joystick = computed(() => options().joystick)
|
|
43
|
+
|
|
44
|
+
const readButtonOption = (name) => buttons()[name]
|
|
45
|
+
const buttonEnabled = (name, defaultValue) => {
|
|
46
|
+
const option = readButtonOption(name)
|
|
47
|
+
if (typeof option === "boolean") return option
|
|
48
|
+
return option?.enabled ?? defaultValue
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const showActionButton = computed(() => buttonEnabled("action", true))
|
|
52
|
+
const showBackButton = computed(() => buttonEnabled("back", true))
|
|
53
|
+
const showDashButton = computed(() => buttonEnabled("dash", false))
|
|
54
|
+
const showJoystick = computed(() => joystick() !== false)
|
|
55
|
+
const joystickSide = computed(() => layout().joystickSide || "right")
|
|
56
|
+
const rootDirection = computed(() => joystickSide() === "left" ? "row-reverse" : "row")
|
|
57
|
+
const layoutGap = computed(() => layout().gap ?? 14)
|
|
58
|
+
const buttonsMargin = computed(() => layout().buttonsMargin ?? layout().margin ?? 50)
|
|
59
|
+
const joystickMargin = computed(() => (
|
|
60
|
+
layout().joystickMargin
|
|
61
|
+
?? layout().margin
|
|
62
|
+
?? (joystickSide() === "right" ? [30, 54, 30, 30] : [30, 30, 30, 54])
|
|
63
|
+
))
|
|
64
|
+
|
|
65
|
+
function readObjectOption(option) {
|
|
66
|
+
return typeof option === "object" && option ? option : {}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function resolveButtonComponent(name) {
|
|
70
|
+
const option = readObjectOption(readButtonOption(name))
|
|
71
|
+
return option.component || components().buttons?.[name] || Button
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const joystickComponent = computed(() => joystick()?.component || components().joystick || Joystick)
|
|
75
|
+
const actionButtonComponent = computed(() => resolveButtonComponent("action"))
|
|
76
|
+
const backButtonComponent = computed(() => resolveButtonComponent("back"))
|
|
77
|
+
const dashButtonComponent = computed(() => resolveButtonComponent("dash"))
|
|
78
|
+
let activeJoystickControl = null
|
|
79
|
+
let activeJoystickPower = 0
|
|
80
|
+
let joystickRepeatTimer = null
|
|
81
|
+
|
|
82
|
+
const baseButtonTextStyle = {
|
|
83
|
+
fontSize: 24,
|
|
84
|
+
fontFamily: "Arial Bold",
|
|
85
|
+
color: "#ffffff"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const actionButtonStyle = {
|
|
89
|
+
backgroundColor: {
|
|
90
|
+
normal: "#f7fbff",
|
|
91
|
+
hover: "#ffffff",
|
|
92
|
+
pressed: "#8fd4ff",
|
|
93
|
+
disabled: "#7f8c8d"
|
|
94
|
+
},
|
|
95
|
+
text: {
|
|
96
|
+
...baseButtonTextStyle,
|
|
97
|
+
color: "#14253a"
|
|
98
|
+
},
|
|
99
|
+
border: {
|
|
100
|
+
color: "#ffffff",
|
|
101
|
+
width: 2
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const backButtonStyle = {
|
|
106
|
+
backgroundColor: {
|
|
107
|
+
normal: "#243041",
|
|
108
|
+
hover: "#2f4059",
|
|
109
|
+
pressed: "#152132",
|
|
110
|
+
disabled: "#7f8c8d"
|
|
111
|
+
},
|
|
112
|
+
text: baseButtonTextStyle,
|
|
113
|
+
border: {
|
|
114
|
+
color: "#ffffff",
|
|
115
|
+
width: 1
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const dashButtonStyle = {
|
|
120
|
+
backgroundColor: {
|
|
121
|
+
normal: "#243041",
|
|
122
|
+
hover: "#2f4059",
|
|
123
|
+
pressed: "#152132",
|
|
124
|
+
disabled: "#7f8c8d"
|
|
125
|
+
},
|
|
126
|
+
text: {
|
|
127
|
+
...baseButtonTextStyle,
|
|
128
|
+
fontSize: 20
|
|
129
|
+
},
|
|
130
|
+
border: {
|
|
131
|
+
color: "#ffffff",
|
|
132
|
+
width: 1
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const defaultButtonProps = {
|
|
137
|
+
action: {
|
|
138
|
+
shape: "circle",
|
|
139
|
+
text: "A",
|
|
140
|
+
width: 68,
|
|
141
|
+
height: 68,
|
|
142
|
+
controls: controlsInstance,
|
|
143
|
+
controlName: "action",
|
|
144
|
+
style: actionButtonStyle,
|
|
145
|
+
alpha: 0.84
|
|
146
|
+
},
|
|
147
|
+
back: {
|
|
148
|
+
shape: "circle",
|
|
149
|
+
text: "B",
|
|
150
|
+
width: 56,
|
|
151
|
+
height: 56,
|
|
152
|
+
controls: controlsInstance,
|
|
153
|
+
controlName: "back",
|
|
154
|
+
style: backButtonStyle,
|
|
155
|
+
alpha: 0.78
|
|
156
|
+
},
|
|
157
|
+
dash: {
|
|
158
|
+
shape: "circle",
|
|
159
|
+
text: "R",
|
|
160
|
+
width: 56,
|
|
161
|
+
height: 56,
|
|
162
|
+
controls: controlsInstance,
|
|
163
|
+
controlName: "dash",
|
|
164
|
+
style: dashButtonStyle,
|
|
165
|
+
alpha: 0.78
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const buttonProps = (name) => {
|
|
170
|
+
const option = readObjectOption(readButtonOption(name))
|
|
171
|
+
const { enabled, component, container, ...props } = option
|
|
172
|
+
const defaultProps = defaultButtonProps[name]
|
|
173
|
+
return {
|
|
174
|
+
...defaultProps,
|
|
175
|
+
...props,
|
|
176
|
+
defaultProps
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const buttonContainerProps = (name) => {
|
|
181
|
+
const props = buttonProps(name)
|
|
182
|
+
return {
|
|
183
|
+
width: props.width,
|
|
184
|
+
height: props.height,
|
|
185
|
+
...readObjectOption(readButtonOption(name)).container
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const joystickProps = () => {
|
|
190
|
+
const joystickOptions = readObjectOption(joystick())
|
|
191
|
+
const { component, moveInterval, threshold, ...props } = joystickOptions
|
|
192
|
+
const defaultProps = {
|
|
193
|
+
outerColor: "#d7e7ff",
|
|
194
|
+
innerColor: "#ffffff",
|
|
195
|
+
scale: 0.82,
|
|
196
|
+
outerScale: { x: 0.86, y: 0.86 },
|
|
197
|
+
innerScale: { x: 0.88, y: 0.88 },
|
|
198
|
+
onStart: onJoystickStart,
|
|
199
|
+
onChange: onJoystickChange,
|
|
200
|
+
onEnd: onJoystickEnd
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
...defaultProps,
|
|
204
|
+
...props,
|
|
205
|
+
defaultProps
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const applyJoystickOptions = (control) => {
|
|
210
|
+
const joystickOptions = joystick()
|
|
211
|
+
if (!control || !joystickOptions || joystickOptions === false) return
|
|
212
|
+
control.joystick?.updateJoystickConfig?.({
|
|
213
|
+
...control.joystick?.getJoystickConfig?.(),
|
|
214
|
+
...(typeof joystickOptions.moveInterval === "number" ? { moveInterval: joystickOptions.moveInterval } : {}),
|
|
215
|
+
...(typeof joystickOptions.threshold === "number" ? { threshold: joystickOptions.threshold } : {})
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const getJoystickThreshold = () => {
|
|
220
|
+
const joystickOptions = joystick()
|
|
221
|
+
if (joystickOptions && joystickOptions !== false && typeof joystickOptions.threshold === "number") {
|
|
222
|
+
return joystickOptions.threshold
|
|
223
|
+
}
|
|
224
|
+
return controlsInstance()?.joystick?.getJoystickConfig?.()?.threshold ?? 0.1
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const getJoystickMoveInterval = () => {
|
|
228
|
+
const joystickOptions = joystick()
|
|
229
|
+
if (joystickOptions && joystickOptions !== false && typeof joystickOptions.moveInterval === "number") {
|
|
230
|
+
return joystickOptions.moveInterval
|
|
231
|
+
}
|
|
232
|
+
return controlsInstance()?.joystick?.getJoystickConfig?.()?.moveInterval ?? 50
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const controlForAxis = (negativeControl, positiveControl, negativeAngle, positiveAngle, angle) => {
|
|
236
|
+
const distanceToNegative = Math.abs(angle - negativeAngle)
|
|
237
|
+
const distanceToPositive = Math.abs(angle - positiveAngle)
|
|
238
|
+
return distanceToNegative <= distanceToPositive ? negativeControl : positiveControl
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const resolveJoystickControl = (event) => {
|
|
242
|
+
const direction = event?.direction
|
|
243
|
+
if (direction === "top") return "up"
|
|
244
|
+
if (direction === "bottom") return "down"
|
|
245
|
+
if (direction === "left") return "left"
|
|
246
|
+
if (direction === "right") return "right"
|
|
247
|
+
|
|
248
|
+
const angle = Number(event?.angle)
|
|
249
|
+
if (!Number.isFinite(angle)) {
|
|
250
|
+
if (direction === "top_left") return "left"
|
|
251
|
+
if (direction === "top_right") return "right"
|
|
252
|
+
if (direction === "bottom_left") return "left"
|
|
253
|
+
if (direction === "bottom_right") return "right"
|
|
254
|
+
return null
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (direction === "top_left") return controlForAxis("up", "left", 90, 180, angle)
|
|
258
|
+
if (direction === "top_right") return controlForAxis("right", "up", 0, 90, angle)
|
|
259
|
+
if (direction === "bottom_left") return controlForAxis("left", "down", 180, 270, angle)
|
|
260
|
+
if (direction === "bottom_right") return controlForAxis("right", "down", 360, 270, angle)
|
|
261
|
+
return null
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const applyJoystickControl = (name, isDown, power = activeJoystickPower) => {
|
|
265
|
+
const control = controlsInstance()
|
|
266
|
+
const joystickControls = control?.joystick
|
|
267
|
+
if (joystickControls?.applyControl) {
|
|
268
|
+
return joystickControls.applyControl(name, isDown, { power })
|
|
269
|
+
}
|
|
270
|
+
const keyboard = control?.keyboard
|
|
271
|
+
if (keyboard?.applyControl) return keyboard.applyControl(name, isDown)
|
|
272
|
+
return control?.applyControl?.(name, isDown, { power })
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const stopJoystickRepeat = () => {
|
|
276
|
+
if (!joystickRepeatTimer) return
|
|
277
|
+
clearInterval(joystickRepeatTimer)
|
|
278
|
+
joystickRepeatTimer = null
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const startJoystickRepeat = () => {
|
|
282
|
+
if (!activeJoystickControl || joystickRepeatTimer) return
|
|
283
|
+
joystickRepeatTimer = setInterval(() => {
|
|
284
|
+
if (!activeJoystickControl) return
|
|
285
|
+
applyJoystickControl(activeJoystickControl, true)
|
|
286
|
+
}, getJoystickMoveInterval())
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const releaseJoystickControl = () => {
|
|
290
|
+
stopJoystickRepeat()
|
|
291
|
+
if (!activeJoystickControl) return
|
|
292
|
+
const previous = activeJoystickControl
|
|
293
|
+
activeJoystickControl = null
|
|
294
|
+
activeJoystickPower = 0
|
|
295
|
+
applyJoystickControl(previous, false, 0)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const activateJoystickControl = (controlName, power = 0) => {
|
|
299
|
+
if (!controlName) {
|
|
300
|
+
releaseJoystickControl()
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
activeJoystickPower = power
|
|
304
|
+
if (activeJoystickControl === controlName) {
|
|
305
|
+
startJoystickRepeat()
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
releaseJoystickControl()
|
|
309
|
+
activeJoystickControl = controlName
|
|
310
|
+
applyJoystickControl(controlName, true, power)
|
|
311
|
+
startJoystickRepeat()
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const publishMobileDebug = (type, payload = {}) => {
|
|
315
|
+
const target = typeof window !== "undefined" ? window : globalThis
|
|
316
|
+
if (!target.__RPGJS_MOBILE_DEBUG__) return
|
|
317
|
+
const event = {
|
|
318
|
+
source: "mobile-gui",
|
|
319
|
+
type,
|
|
320
|
+
time: Date.now(),
|
|
321
|
+
...payload
|
|
322
|
+
}
|
|
323
|
+
target.__RPGJS_MOBILE_DEBUG_EVENTS__ = [
|
|
324
|
+
...(target.__RPGJS_MOBILE_DEBUG_EVENTS__ || []).slice(-199),
|
|
325
|
+
event
|
|
326
|
+
]
|
|
327
|
+
target.__RPGJS_MOBILE_DEBUG_LAST__ = event
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const onJoystickStart = () => {
|
|
331
|
+
publishMobileDebug("joystick:start", {
|
|
332
|
+
config: controlsInstance()?.joystick?.getJoystickConfig?.()
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const onJoystickChange = (event) => {
|
|
337
|
+
const threshold = getJoystickThreshold()
|
|
338
|
+
const controlName = event?.power >= threshold ? resolveJoystickControl(event) : null
|
|
339
|
+
publishMobileDebug("joystick:change", {
|
|
340
|
+
direction: event?.direction,
|
|
341
|
+
power: event?.power,
|
|
342
|
+
angle: event?.angle,
|
|
343
|
+
threshold,
|
|
344
|
+
controlName,
|
|
345
|
+
config: controlsInstance()?.joystick?.getJoystickConfig?.()
|
|
346
|
+
})
|
|
347
|
+
activateJoystickControl(controlName, event?.power ?? 0)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const onJoystickEnd = () => {
|
|
351
|
+
publishMobileDebug("joystick:end", {
|
|
352
|
+
controlName: activeJoystickControl
|
|
353
|
+
})
|
|
354
|
+
releaseJoystickControl()
|
|
355
|
+
}
|
|
75
356
|
|
|
76
357
|
mount((element) => {
|
|
77
358
|
const control = inject('KeyboardControls')
|
|
78
359
|
controlsInstance.set(control)
|
|
360
|
+
applyJoystickOptions(control)
|
|
79
361
|
})
|
|
80
|
-
</script>
|
|
362
|
+
</script>
|
package/src/index.ts
CHANGED
|
@@ -30,5 +30,17 @@ export { RpgClientEvent } from "./Game/Event";
|
|
|
30
30
|
export * from "./Game/ProjectileManager";
|
|
31
31
|
export * from "./Game/ClientVisuals";
|
|
32
32
|
export { withMobile } from "./components/gui/mobile";
|
|
33
|
+
export type {
|
|
34
|
+
MobileButtonComponentProps,
|
|
35
|
+
MobileGuiButtonOptions,
|
|
36
|
+
MobileGuiComponentsOptions,
|
|
37
|
+
MobileGuiEnabled,
|
|
38
|
+
MobileGuiJoystickOptions,
|
|
39
|
+
MobileGuiJoystickSide,
|
|
40
|
+
MobileGuiLayoutOptions,
|
|
41
|
+
MobileGuiMargin,
|
|
42
|
+
MobileGuiOptions,
|
|
43
|
+
MobileJoystickComponentProps,
|
|
44
|
+
} from "./components/gui/mobile";
|
|
33
45
|
export * from "./services/AbstractSocket";
|
|
34
46
|
export * from "./i18n";
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
applyCameraFollow,
|
|
4
|
+
cameraFollowAnimationOptions,
|
|
5
|
+
cameraFollowOptions,
|
|
6
|
+
ownsCameraFollowRevision,
|
|
7
|
+
} from "./cameraFollow";
|
|
8
|
+
|
|
9
|
+
const createViewport = () => ({
|
|
10
|
+
animate: vi.fn(),
|
|
11
|
+
follow: vi.fn(),
|
|
12
|
+
plugins: {
|
|
13
|
+
remove: vi.fn(),
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("camera follow", () => {
|
|
18
|
+
it("normalizes transition options from smoothMove", () => {
|
|
19
|
+
expect(cameraFollowAnimationOptions(true)).toEqual({
|
|
20
|
+
time: 1000,
|
|
21
|
+
ease: "easeInOutSine",
|
|
22
|
+
});
|
|
23
|
+
expect(cameraFollowAnimationOptions({ time: 450, ease: "easeInOutQuad" })).toEqual({
|
|
24
|
+
time: 450,
|
|
25
|
+
ease: "easeInOutQuad",
|
|
26
|
+
});
|
|
27
|
+
expect(cameraFollowAnimationOptions({ time: 450, ease: "easeInOutQuadd" } as any)).toEqual({
|
|
28
|
+
time: 450,
|
|
29
|
+
ease: "easeInOutSine",
|
|
30
|
+
});
|
|
31
|
+
expect(cameraFollowAnimationOptions(false)).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("normalizes continuous follow options", () => {
|
|
35
|
+
expect(cameraFollowOptions({ speed: 12, acceleration: 0.2, radius: 80 })).toEqual({
|
|
36
|
+
speed: 12,
|
|
37
|
+
acceleration: 0.2,
|
|
38
|
+
radius: 80,
|
|
39
|
+
});
|
|
40
|
+
expect(cameraFollowOptions({ speed: -4, acceleration: null, radius: null })).toEqual({
|
|
41
|
+
speed: 0,
|
|
42
|
+
acceleration: null,
|
|
43
|
+
radius: null,
|
|
44
|
+
});
|
|
45
|
+
expect(cameraFollowOptions(true)).toBeUndefined();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("animates to the target then follows it", () => {
|
|
49
|
+
const viewport = createViewport();
|
|
50
|
+
const target = { x: 120, y: 240 };
|
|
51
|
+
|
|
52
|
+
applyCameraFollow({
|
|
53
|
+
viewport,
|
|
54
|
+
target,
|
|
55
|
+
smoothMove: { time: 800, ease: "easeInOutQuad", speed: 10, radius: 32 },
|
|
56
|
+
followRevision: 2,
|
|
57
|
+
isCurrentRevision: (revision) => revision === 2,
|
|
58
|
+
shouldFollowCamera: () => true,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(viewport.plugins.remove).toHaveBeenCalledWith("animate");
|
|
62
|
+
expect(viewport.plugins.remove).toHaveBeenCalledWith("follow");
|
|
63
|
+
expect(viewport.animate).toHaveBeenCalledWith(
|
|
64
|
+
expect.objectContaining({
|
|
65
|
+
position: { x: 120, y: 240 },
|
|
66
|
+
time: 800,
|
|
67
|
+
ease: "easeInOutQuad",
|
|
68
|
+
})
|
|
69
|
+
);
|
|
70
|
+
expect(viewport.follow).not.toHaveBeenCalled();
|
|
71
|
+
|
|
72
|
+
const animateOptions = viewport.animate.mock.calls[0][0];
|
|
73
|
+
animateOptions.callbackOnComplete();
|
|
74
|
+
|
|
75
|
+
const [followTarget, followOptions] = viewport.follow.mock.calls[0];
|
|
76
|
+
expect(followTarget).not.toBe(target);
|
|
77
|
+
expect(followTarget.x).toBe(120);
|
|
78
|
+
expect(followTarget.y).toBe(240);
|
|
79
|
+
expect(followOptions).toEqual({
|
|
80
|
+
speed: 10,
|
|
81
|
+
radius: 32,
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("follows instantly when smoothMove is disabled", () => {
|
|
86
|
+
const viewport = createViewport();
|
|
87
|
+
const target = { x: 120, y: 240 };
|
|
88
|
+
|
|
89
|
+
applyCameraFollow({
|
|
90
|
+
viewport,
|
|
91
|
+
target,
|
|
92
|
+
smoothMove: false,
|
|
93
|
+
followRevision: 1,
|
|
94
|
+
isCurrentRevision: () => true,
|
|
95
|
+
shouldFollowCamera: () => true,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(viewport.animate).not.toHaveBeenCalled();
|
|
99
|
+
const [followTarget] = viewport.follow.mock.calls[0];
|
|
100
|
+
expect(followTarget).not.toBe(target);
|
|
101
|
+
expect(followTarget.x).toBe(120);
|
|
102
|
+
expect(followTarget.y).toBe(240);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("does not follow after animation if another camera command superseded it", () => {
|
|
106
|
+
const viewport = createViewport();
|
|
107
|
+
|
|
108
|
+
applyCameraFollow({
|
|
109
|
+
viewport,
|
|
110
|
+
target: { x: 120, y: 240 },
|
|
111
|
+
smoothMove: { time: 800, ease: "easeInOutQuad" },
|
|
112
|
+
followRevision: 2,
|
|
113
|
+
isCurrentRevision: (revision) => revision === 3,
|
|
114
|
+
shouldFollowCamera: () => true,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const animateOptions = viewport.animate.mock.calls[0][0];
|
|
118
|
+
animateOptions.callbackOnComplete();
|
|
119
|
+
|
|
120
|
+
expect(viewport.follow).not.toHaveBeenCalled();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("does not follow a target whose position cannot be read", () => {
|
|
124
|
+
const viewport = createViewport();
|
|
125
|
+
const target = {
|
|
126
|
+
get x(): number {
|
|
127
|
+
throw new Error("target destroyed");
|
|
128
|
+
},
|
|
129
|
+
get y() {
|
|
130
|
+
return 240;
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
applyCameraFollow({
|
|
135
|
+
viewport,
|
|
136
|
+
target,
|
|
137
|
+
smoothMove: false,
|
|
138
|
+
followRevision: 1,
|
|
139
|
+
isCurrentRevision: () => true,
|
|
140
|
+
shouldFollowCamera: () => true,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(viewport.plugins.remove).toHaveBeenCalledWith("animate");
|
|
144
|
+
expect(viewport.plugins.remove).toHaveBeenCalledWith("follow");
|
|
145
|
+
expect(viewport.animate).not.toHaveBeenCalled();
|
|
146
|
+
expect(viewport.follow).not.toHaveBeenCalled();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("keeps the follow target readable when the source target is destroyed", () => {
|
|
150
|
+
const viewport = createViewport();
|
|
151
|
+
let position: { x: number; y: number } | null = { x: 120, y: 240 };
|
|
152
|
+
const target = {
|
|
153
|
+
get destroyed() {
|
|
154
|
+
return position === null;
|
|
155
|
+
},
|
|
156
|
+
get x() {
|
|
157
|
+
if (!position) throw new Error("target destroyed");
|
|
158
|
+
return position.x;
|
|
159
|
+
},
|
|
160
|
+
get y() {
|
|
161
|
+
if (!position) throw new Error("target destroyed");
|
|
162
|
+
return position.y;
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
applyCameraFollow({
|
|
167
|
+
viewport,
|
|
168
|
+
target,
|
|
169
|
+
smoothMove: false,
|
|
170
|
+
followRevision: 1,
|
|
171
|
+
isCurrentRevision: () => true,
|
|
172
|
+
shouldFollowCamera: () => true,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const [followTarget] = viewport.follow.mock.calls[0];
|
|
176
|
+
expect(followTarget.x).toBe(120);
|
|
177
|
+
expect(followTarget.y).toBe(240);
|
|
178
|
+
|
|
179
|
+
position = { x: 160, y: 280 };
|
|
180
|
+
expect(followTarget.x).toBe(160);
|
|
181
|
+
expect(followTarget.y).toBe(280);
|
|
182
|
+
|
|
183
|
+
position = null;
|
|
184
|
+
expect(followTarget.x).toBe(160);
|
|
185
|
+
expect(followTarget.y).toBe(280);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("does not follow after animation if the target was destroyed", () => {
|
|
189
|
+
const viewport = createViewport();
|
|
190
|
+
let destroyed = false;
|
|
191
|
+
const target = {
|
|
192
|
+
get destroyed() {
|
|
193
|
+
return destroyed;
|
|
194
|
+
},
|
|
195
|
+
x: 120,
|
|
196
|
+
y: 240,
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
applyCameraFollow({
|
|
200
|
+
viewport,
|
|
201
|
+
target,
|
|
202
|
+
smoothMove: { time: 800, ease: "easeInOutQuad" },
|
|
203
|
+
followRevision: 2,
|
|
204
|
+
isCurrentRevision: (revision) => revision === 2,
|
|
205
|
+
shouldFollowCamera: () => true,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
destroyed = true;
|
|
209
|
+
const animateOptions = viewport.animate.mock.calls[0][0];
|
|
210
|
+
animateOptions.callbackOnComplete();
|
|
211
|
+
|
|
212
|
+
expect(viewport.follow).not.toHaveBeenCalled();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("only lets the sprite that owns the active follow revision clear plugins", () => {
|
|
216
|
+
expect(ownsCameraFollowRevision(2, 2)).toBe(true);
|
|
217
|
+
expect(ownsCameraFollowRevision(2, 3)).toBe(false);
|
|
218
|
+
expect(ownsCameraFollowRevision(null, 2)).toBe(false);
|
|
219
|
+
});
|
|
220
|
+
});
|