@rpgjs/client 5.0.0-beta.12 → 5.0.0-beta.14
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 +18 -0
- package/dist/Game/Object.d.ts +2 -0
- package/dist/Game/Object.js +20 -6
- package/dist/Game/Object.js.map +1 -1
- package/dist/Gui/Gui.d.ts +3 -2
- package/dist/Gui/Gui.js +18 -6
- package/dist/Gui/Gui.js.map +1 -1
- package/dist/RpgClient.d.ts +21 -1
- package/dist/RpgClientEngine.d.ts +20 -2
- package/dist/RpgClientEngine.js +182 -17
- package/dist/RpgClientEngine.js.map +1 -1
- package/dist/components/character.ce.js +84 -9
- package/dist/components/character.ce.js.map +1 -1
- package/dist/components/gui/dialogbox/index.ce.js +27 -12
- package/dist/components/gui/dialogbox/index.ce.js.map +1 -1
- package/dist/components/gui/gameover.ce.js +4 -3
- package/dist/components/gui/gameover.ce.js.map +1 -1
- package/dist/components/gui/menu/equip-menu.ce.js +9 -8
- package/dist/components/gui/menu/equip-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/exit-menu.ce.js +7 -5
- package/dist/components/gui/menu/exit-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/items-menu.ce.js +8 -7
- package/dist/components/gui/menu/items-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/main-menu.ce.js +12 -11
- package/dist/components/gui/menu/main-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/options-menu.ce.js +7 -5
- package/dist/components/gui/menu/options-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/skills-menu.ce.js +4 -2
- package/dist/components/gui/menu/skills-menu.ce.js.map +1 -1
- package/dist/components/gui/notification/notification.ce.js +4 -1
- package/dist/components/gui/notification/notification.ce.js.map +1 -1
- package/dist/components/gui/save-load.ce.js +10 -9
- package/dist/components/gui/save-load.ce.js.map +1 -1
- package/dist/components/gui/shop/shop.ce.js +17 -16
- package/dist/components/gui/shop/shop.ce.js.map +1 -1
- package/dist/components/gui/title-screen.ce.js +4 -3
- package/dist/components/gui/title-screen.ce.js.map +1 -1
- package/dist/components/interaction-components.ce.js +20 -0
- package/dist/components/interaction-components.ce.js.map +1 -0
- package/dist/components/scenes/canvas.ce.js +66 -33
- package/dist/components/scenes/canvas.ce.js.map +1 -1
- package/dist/components/scenes/draw-map.ce.js +18 -13
- package/dist/components/scenes/draw-map.ce.js.map +1 -1
- package/dist/components/scenes/event-layer.ce.js +42 -3
- package/dist/components/scenes/event-layer.ce.js.map +1 -1
- package/dist/i18n.d.ts +55 -0
- package/dist/i18n.js +60 -0
- package/dist/i18n.js.map +1 -0
- package/dist/i18n.spec.d.ts +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -1
- package/dist/module.js +23 -3
- package/dist/module.js.map +1 -1
- package/dist/services/interactions.d.ts +159 -0
- package/dist/services/interactions.js +460 -0
- package/dist/services/interactions.js.map +1 -0
- package/dist/services/interactions.spec.d.ts +1 -0
- package/dist/services/keyboardControls.d.ts +1 -0
- package/dist/services/keyboardControls.js +1 -0
- package/dist/services/keyboardControls.js.map +1 -1
- package/dist/services/loadMap.d.ts +3 -0
- package/dist/services/loadMap.js.map +1 -1
- package/package.json +4 -4
- package/src/Game/Object.spec.ts +14 -1
- package/src/Game/Object.ts +34 -10
- package/src/Gui/Gui.spec.ts +67 -0
- package/src/Gui/Gui.ts +24 -7
- package/src/RpgClient.ts +28 -1
- package/src/RpgClientEngine.ts +254 -29
- package/src/components/character.ce +92 -9
- package/src/components/gui/dialogbox/index.ce +35 -14
- package/src/components/gui/gameover.ce +4 -3
- package/src/components/gui/menu/equip-menu.ce +9 -8
- package/src/components/gui/menu/exit-menu.ce +4 -3
- package/src/components/gui/menu/items-menu.ce +8 -7
- package/src/components/gui/menu/main-menu.ce +12 -11
- package/src/components/gui/menu/options-menu.ce +4 -3
- package/src/components/gui/menu/skills-menu.ce +2 -1
- package/src/components/gui/notification/notification.ce +7 -1
- package/src/components/gui/save-load.ce +11 -10
- package/src/components/gui/shop/shop.ce +17 -16
- package/src/components/gui/title-screen.ce +4 -3
- package/src/components/interaction-components.ce +23 -0
- package/src/components/scenes/canvas.ce +68 -31
- package/src/components/scenes/draw-map.ce +16 -5
- package/src/components/scenes/event-layer.ce +54 -2
- package/src/i18n.spec.ts +39 -0
- package/src/i18n.ts +59 -0
- package/src/index.ts +2 -0
- package/src/module.ts +32 -10
- package/src/services/interactions.spec.ts +175 -0
- package/src/services/interactions.ts +722 -0
- package/src/services/keyboardControls.ts +2 -1
- package/src/services/loadMap.ts +3 -1
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
height={engine.height}
|
|
34
34
|
>
|
|
35
35
|
@if (gui.display) {
|
|
36
|
-
<gui.component data={gui.data} dependencies={gui.dependencies} onFinish={(data) => {
|
|
37
|
-
onGuiFinish(gui, data)
|
|
36
|
+
<gui.component data={gui.data} dependencies={gui.dependencies} guiOpenId={gui.openId} onFinish={(data, guiOpenId) => {
|
|
37
|
+
onGuiFinish(gui, data, guiOpenId)
|
|
38
38
|
}} onInteraction={(name, data) => {
|
|
39
39
|
onGuiInteraction(gui, name, data)
|
|
40
40
|
}} />
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
import { inject } from "../../core/inject";
|
|
49
49
|
import { RpgClientEngine } from "../../RpgClientEngine";
|
|
50
50
|
import { RpgGui } from "../../Gui/Gui";
|
|
51
|
-
import { delay } from "@rpgjs/common";
|
|
52
51
|
import { NightAmbiant, SpriteShadows } from '@canvasengine/presets'
|
|
52
|
+
import { shouldRenderLightingShadows } from "@rpgjs/common";
|
|
53
53
|
|
|
54
54
|
const engine = inject(RpgClientEngine);
|
|
55
55
|
const SceneMap = engine.sceneMapComponent;
|
|
@@ -76,10 +76,16 @@
|
|
|
76
76
|
}
|
|
77
77
|
})
|
|
78
78
|
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
const normalizeOpenId = (value) => {
|
|
80
|
+
const resolved = typeof value === "function" ? value() : value
|
|
81
|
+
return typeof resolved === "string" && resolved.length > 0 ? resolved : undefined
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const onGuiFinish = (gui, data, guiOpenId) => {
|
|
85
|
+
const completedOpenId = normalizeOpenId(guiOpenId)
|
|
86
|
+
const currentOpenId = normalizeOpenId(gui.openId)
|
|
87
|
+
if (completedOpenId && currentOpenId && completedOpenId !== currentOpenId) return
|
|
88
|
+
guiService.guiClose(gui.name, data, completedOpenId ?? currentOpenId)
|
|
83
89
|
}
|
|
84
90
|
|
|
85
91
|
const onGuiInteraction = (gui, name, data) => {
|
|
@@ -94,12 +100,20 @@
|
|
|
94
100
|
const NIGHT_SPOT_MIN_INTENSITY = 1
|
|
95
101
|
const SHADOW_SPOT_RADIUS_SCALE = 12
|
|
96
102
|
const SHADOW_SPOT_MIN_RADIUS = 480
|
|
97
|
-
const SHADOW_SPOT_MIN_INTENSITY = 1.35
|
|
98
103
|
|
|
104
|
+
const toFiniteNumber = (value, fallback = null) => {
|
|
105
|
+
const number = Number(value)
|
|
106
|
+
return Number.isFinite(number) ? number : fallback
|
|
107
|
+
}
|
|
108
|
+
const clampNumber = (value, min, max) => Math.max(min, Math.min(max, value))
|
|
99
109
|
const nightSpotRadius = (radius) => Math.max(radius * NIGHT_SPOT_RADIUS_SCALE, NIGHT_SPOT_MIN_RADIUS)
|
|
100
110
|
const shadowSpotRadius = (radius) => Math.max(radius * SHADOW_SPOT_RADIUS_SCALE, SHADOW_SPOT_MIN_RADIUS)
|
|
101
111
|
const nightSpotIntensity = (intensity, fallback) => Math.max(intensity ?? fallback, NIGHT_SPOT_MIN_INTENSITY)
|
|
102
|
-
const
|
|
112
|
+
const shadowLightIntensity = (intensity, fallback = 1) => {
|
|
113
|
+
const value = Number(intensity ?? fallback)
|
|
114
|
+
return Number.isFinite(value) ? Math.max(0, value) : fallback
|
|
115
|
+
}
|
|
116
|
+
const shadowSpotIntensity = (intensity) => shadowLightIntensity(intensity, 1)
|
|
103
117
|
|
|
104
118
|
const lightingAmbient = computed(() => {
|
|
105
119
|
const state = lighting?.()
|
|
@@ -123,11 +137,6 @@
|
|
|
123
137
|
}
|
|
124
138
|
})
|
|
125
139
|
})
|
|
126
|
-
const hasLightSpots = computed(() => {
|
|
127
|
-
const state = lighting?.()
|
|
128
|
-
return (state?.spots?.length ?? 0) > 0
|
|
129
|
-
})
|
|
130
|
-
|
|
131
140
|
const lightingDarkness = computed(() => {
|
|
132
141
|
const darkness = lightingAmbient().darkness
|
|
133
142
|
return typeof darkness === "number" ? darkness : 0
|
|
@@ -156,18 +165,55 @@
|
|
|
156
165
|
const scale = Number(data?.params?.scale ?? 1) || 1
|
|
157
166
|
const mapWidth = width * scale
|
|
158
167
|
const mapHeight = height * scale
|
|
168
|
+
const projectionBase = Math.max(1, mapWidth, mapHeight)
|
|
159
169
|
|
|
160
170
|
return {
|
|
161
|
-
x: -
|
|
162
|
-
y: -
|
|
171
|
+
x: -projectionBase * 24,
|
|
172
|
+
y: -projectionBase * 24,
|
|
163
173
|
z: 520,
|
|
164
|
-
radius:
|
|
174
|
+
radius: projectionBase * 160,
|
|
165
175
|
intensity: 0.85,
|
|
166
176
|
shadowWeight: lightingDarkness() > 0 ? 2.2 : 1,
|
|
167
177
|
enabled: true,
|
|
168
178
|
}
|
|
169
179
|
}
|
|
170
180
|
|
|
181
|
+
const normalizeSunDirection = (sun) => {
|
|
182
|
+
const x = toFiniteNumber(sun?.x, null)
|
|
183
|
+
const y = toFiniteNumber(sun?.y, null)
|
|
184
|
+
if (x !== null && y !== null && Math.hypot(x, y) > 0.001) {
|
|
185
|
+
return { x, y }
|
|
186
|
+
}
|
|
187
|
+
return { x: -0.45, y: -1 }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const defaultSunAmbientLight = () => {
|
|
191
|
+
const state = lighting?.()
|
|
192
|
+
if (!state?.sun || state.sun.enabled === false) return null
|
|
193
|
+
|
|
194
|
+
const defaultSun = defaultSunLight()
|
|
195
|
+
const sun = {
|
|
196
|
+
...defaultSun,
|
|
197
|
+
...state.sun,
|
|
198
|
+
intensity: shadowLightIntensity(state.sun.intensity, defaultSun.intensity),
|
|
199
|
+
shadowWeight: state.sun.shadowWeight ?? defaultSun.shadowWeight,
|
|
200
|
+
}
|
|
201
|
+
if (sun.intensity <= 0) return null
|
|
202
|
+
|
|
203
|
+
const direction = normalizeSunDirection(sun)
|
|
204
|
+
const shadowWeight = clampNumber(toFiniteNumber(sun.shadowWeight, lightingDarkness() > 0 ? 1.35 : 1) ?? 1, 0, 4)
|
|
205
|
+
const length = clampNumber(30 + sun.intensity * 38 * Math.max(0.75, shadowWeight), 30, 86)
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
x: direction.x,
|
|
209
|
+
y: direction.y,
|
|
210
|
+
z: toFiniteNumber(sun.z, 520) ?? 520,
|
|
211
|
+
intensity: clampNumber(sun.intensity, 0, 2),
|
|
212
|
+
shadowWeight,
|
|
213
|
+
length,
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
171
217
|
const shadowState = computed(() => {
|
|
172
218
|
const state = lighting?.()
|
|
173
219
|
return state?.shadows ?? null
|
|
@@ -175,12 +221,6 @@
|
|
|
175
221
|
|
|
176
222
|
const shadowLights = computed(() => {
|
|
177
223
|
const state = lighting?.()
|
|
178
|
-
const defaultSun = defaultSunLight()
|
|
179
|
-
const sun = {
|
|
180
|
-
...defaultSun,
|
|
181
|
-
...(state?.sun ?? {}),
|
|
182
|
-
shadowWeight: state?.sun?.shadowWeight ?? defaultSun.shadowWeight,
|
|
183
|
-
}
|
|
184
224
|
const spotLights = (state?.spots ?? []).map((spot) => {
|
|
185
225
|
const radius = spot.radius ?? 180
|
|
186
226
|
return {
|
|
@@ -189,15 +229,12 @@
|
|
|
189
229
|
z: 170,
|
|
190
230
|
radius: shadowSpotRadius(radius),
|
|
191
231
|
intensity: shadowSpotIntensity(spot.intensity),
|
|
192
|
-
shadowWeight:
|
|
232
|
+
shadowWeight: 1,
|
|
193
233
|
enabled: true,
|
|
194
234
|
}
|
|
195
235
|
})
|
|
196
236
|
|
|
197
|
-
return
|
|
198
|
-
...((sun.enabled === false || sun.intensity <= 0) ? [] : [sun]),
|
|
199
|
-
...spotLights,
|
|
200
|
-
]
|
|
237
|
+
return spotLights
|
|
201
238
|
})
|
|
202
239
|
|
|
203
240
|
const shadowAmbientLight = computed(() => {
|
|
@@ -205,12 +242,12 @@
|
|
|
205
242
|
if (shadows?.ambientLight === null || shadows?.ambientLight?.enabled === false) {
|
|
206
243
|
return null
|
|
207
244
|
}
|
|
208
|
-
return shadows?.ambientLight ??
|
|
245
|
+
return shadows?.ambientLight ?? defaultSunAmbientLight()
|
|
209
246
|
})
|
|
210
247
|
|
|
211
248
|
const shadowEnabled = computed(() => {
|
|
212
|
-
const
|
|
213
|
-
return Boolean((
|
|
249
|
+
const state = lighting?.()
|
|
250
|
+
return Boolean(shouldRenderLightingShadows(state) && (shadowLights().length > 0 || shadowAmbientLight()))
|
|
214
251
|
})
|
|
215
252
|
|
|
216
253
|
const shadowMode = computed(() => shadowState()?.mode ?? "strongest")
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
<Container
|
|
2
|
-
|
|
1
|
+
<Container shake={shakeConfig} freeze={engine.gamePause}>
|
|
2
|
+
@if (backgroundMusic()) {
|
|
3
|
+
<Container sound={backgroundMusic()} />
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
@if (backgroundAmbientSound()) {
|
|
7
|
+
<Container sound={backgroundAmbientSound()} />
|
|
8
|
+
}
|
|
3
9
|
|
|
4
10
|
<Container>
|
|
5
11
|
@if (map() && sceneComponent()) {
|
|
@@ -42,10 +48,15 @@
|
|
|
42
48
|
const projectiles = engine.projectiles.current
|
|
43
49
|
const map = engine.sceneMap?.data
|
|
44
50
|
const sceneComponent = computed(() => map()?.component)
|
|
45
|
-
const mapParams = map()?.params
|
|
46
51
|
const weather = engine.sceneMap.weather
|
|
47
|
-
const backgroundMusic =
|
|
48
|
-
|
|
52
|
+
const backgroundMusic = computed(() => {
|
|
53
|
+
const src = map()?.params?.backgroundMusic
|
|
54
|
+
return src ? { src, autoplay: true, loop: true } : undefined
|
|
55
|
+
})
|
|
56
|
+
const backgroundAmbientSound = computed(() => {
|
|
57
|
+
const src = map()?.params?.backgroundAmbientSound
|
|
58
|
+
return src ? { src, autoplay: true, loop: true } : undefined
|
|
59
|
+
})
|
|
49
60
|
|
|
50
61
|
const shakeConfig = {
|
|
51
62
|
trigger: engine.mapShakeTrigger,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<Container sortableChildren={true}>
|
|
1
|
+
<Container sortableChildren={true} onBeforeDestroy={detachPixiChildren}>
|
|
2
2
|
@for ((event,id) of events) {
|
|
3
3
|
<Character id={id} object={event} />
|
|
4
4
|
}
|
|
@@ -13,14 +13,66 @@
|
|
|
13
13
|
</Container>
|
|
14
14
|
|
|
15
15
|
<script>
|
|
16
|
+
import { effect, mount } from "canvasengine";
|
|
16
17
|
import { inject } from "../../core/inject";
|
|
17
18
|
import { RpgClientEngine } from "../../RpgClientEngine";
|
|
18
19
|
import Character from "../character.ce";
|
|
19
20
|
import LightHalo from "../prebuilt/light-halo.ce";
|
|
20
21
|
|
|
21
22
|
const engine = inject(RpgClientEngine);
|
|
22
|
-
const { children } = defineProps(
|
|
23
|
+
const { children, pixiChildren } = defineProps({
|
|
24
|
+
pixiChildren: {
|
|
25
|
+
default: []
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
const readValue = (value) => typeof value === "function" ? value() : value
|
|
29
|
+
let rootContainer = null
|
|
30
|
+
let mountedPixiChildren = []
|
|
23
31
|
|
|
24
32
|
const players = engine.sceneMap.players
|
|
25
33
|
const events = engine.sceneMap.events
|
|
34
|
+
|
|
35
|
+
const getPixiChildren = () => {
|
|
36
|
+
const value = readValue(pixiChildren)
|
|
37
|
+
return Array.isArray(value) ? value.filter(Boolean) : []
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const syncPixiChildren = () => {
|
|
41
|
+
if (!rootContainer) return
|
|
42
|
+
const nextPixiChildren = getPixiChildren()
|
|
43
|
+
mountedPixiChildren.forEach((child) => {
|
|
44
|
+
if (!nextPixiChildren.includes(child) && child.parent === rootContainer) {
|
|
45
|
+
rootContainer.removeChild(child)
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
nextPixiChildren.forEach((child) => {
|
|
49
|
+
if (child.parent === rootContainer) return
|
|
50
|
+
if (child.parent) {
|
|
51
|
+
child.parent.removeChild(child)
|
|
52
|
+
}
|
|
53
|
+
rootContainer.addChild(child)
|
|
54
|
+
})
|
|
55
|
+
mountedPixiChildren = nextPixiChildren
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const detachPixiChildren = () => {
|
|
59
|
+
if (!rootContainer) return
|
|
60
|
+
mountedPixiChildren.forEach((child) => {
|
|
61
|
+
if (child.parent === rootContainer) {
|
|
62
|
+
rootContainer.removeChild(child)
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
mountedPixiChildren = []
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
effect(() => {
|
|
69
|
+
readValue(pixiChildren)
|
|
70
|
+
syncPixiChildren()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
mount((element) => {
|
|
74
|
+
rootContainer = element.componentInstance
|
|
75
|
+
syncPixiChildren()
|
|
76
|
+
return detachPixiChildren
|
|
77
|
+
})
|
|
26
78
|
</script>
|
package/src/i18n.spec.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from "vitest";
|
|
4
|
+
import { Context, injector } from "@signe/di";
|
|
5
|
+
import { getOrCreateI18nService } from "@rpgjs/common";
|
|
6
|
+
import { provideClientModules } from "./module";
|
|
7
|
+
import { provideI18n } from "./i18n";
|
|
8
|
+
|
|
9
|
+
describe("client i18n", () => {
|
|
10
|
+
test("merges client module translations with game overrides", async () => {
|
|
11
|
+
const context = new Context();
|
|
12
|
+
|
|
13
|
+
await injector(context, [
|
|
14
|
+
provideClientModules([
|
|
15
|
+
{
|
|
16
|
+
i18n: {
|
|
17
|
+
fr: {
|
|
18
|
+
"menu.title": "Titre du module",
|
|
19
|
+
"menu.module-only": "Module",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
]),
|
|
24
|
+
provideI18n({
|
|
25
|
+
defaultLocale: "fr",
|
|
26
|
+
messages: {
|
|
27
|
+
fr: {
|
|
28
|
+
"menu.title": "Titre du jeu",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
}),
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const service = getOrCreateI18nService(context);
|
|
35
|
+
|
|
36
|
+
expect(service.t("menu.title", undefined, "fr")).toBe("Titre du jeu");
|
|
37
|
+
expect(service.t("menu.module-only", undefined, "fr")).toBe("Module");
|
|
38
|
+
});
|
|
39
|
+
});
|
package/src/i18n.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createI18nProvider, type I18nConfig } from "@rpgjs/common";
|
|
2
|
+
|
|
3
|
+
export const RpgClientBuiltinI18n = {
|
|
4
|
+
en: {
|
|
5
|
+
"rpg.menu.title": "Menu",
|
|
6
|
+
"rpg.menu.status": "Status",
|
|
7
|
+
"rpg.menu.level": "Level",
|
|
8
|
+
"rpg.menu.gold": "Gold",
|
|
9
|
+
"rpg.menu.parameters": "Parameters",
|
|
10
|
+
"rpg.menu.items": "Items",
|
|
11
|
+
"rpg.menu.skills": "Skills",
|
|
12
|
+
"rpg.menu.equip": "Equip",
|
|
13
|
+
"rpg.menu.options": "Options",
|
|
14
|
+
"rpg.menu.save": "Save",
|
|
15
|
+
"rpg.menu.exit": "Exit",
|
|
16
|
+
"rpg.menu.weapons": "Weapons",
|
|
17
|
+
"rpg.menu.armor": "Armor",
|
|
18
|
+
"rpg.menu.use": "Use",
|
|
19
|
+
"rpg.menu.cancel": "Cancel",
|
|
20
|
+
"rpg.menu.unequip": "Unequip",
|
|
21
|
+
"rpg.menu.remove-equipment": "Remove the current equipment",
|
|
22
|
+
"rpg.menu.empty": "Empty",
|
|
23
|
+
"rpg.menu.leave-game": "Leave the game?",
|
|
24
|
+
"rpg.menu.exit-help": "Press Action to confirm or Escape to go back.",
|
|
25
|
+
"rpg.menu.options-help": "Configure your preferences here.",
|
|
26
|
+
"rpg.save.title": "Save Game",
|
|
27
|
+
"rpg.save.subtitle": "Choose a slot to overwrite or create.",
|
|
28
|
+
"rpg.load.title": "Load Game",
|
|
29
|
+
"rpg.load.subtitle": "Select a slot to load your progress.",
|
|
30
|
+
"rpg.save.auto": "Auto Save",
|
|
31
|
+
"rpg.save.slot": "Slot {index}",
|
|
32
|
+
"rpg.save.empty-slot": "Empty Slot",
|
|
33
|
+
"rpg.save.level": "Level",
|
|
34
|
+
"rpg.save.exp": "Exp",
|
|
35
|
+
"rpg.save.map": "Map",
|
|
36
|
+
"rpg.save.date": "Date",
|
|
37
|
+
"rpg.title.default": "RPG",
|
|
38
|
+
"rpg.title.start": "Start",
|
|
39
|
+
"rpg.title.load": "Load",
|
|
40
|
+
"rpg.gameover.title": "Game Over",
|
|
41
|
+
"rpg.gameover.title-screen": "Title Screen",
|
|
42
|
+
"rpg.gameover.load-game": "Load Game",
|
|
43
|
+
"rpg.shop.default-message": "Welcome to my shop!",
|
|
44
|
+
"rpg.shop.choose-action": "Choose an action",
|
|
45
|
+
"rpg.shop.buy": "Buy",
|
|
46
|
+
"rpg.shop.sell": "Sell",
|
|
47
|
+
"rpg.shop.back": "Back",
|
|
48
|
+
"rpg.shop.equipped": "Equipped",
|
|
49
|
+
"rpg.shop.already-equipped": "Already equipped",
|
|
50
|
+
"rpg.shop.qty": "Qty",
|
|
51
|
+
"rpg.shop.available": "Available",
|
|
52
|
+
"rpg.shop.quantity": "Quantity",
|
|
53
|
+
"rpg.shop.total": "Total",
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export function provideI18n(config: I18nConfig = {}) {
|
|
58
|
+
return createI18nProvider(config);
|
|
59
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ export * from "./core/inject";
|
|
|
8
8
|
export * from "./services/loadMap";
|
|
9
9
|
export * from "./services/actionInput";
|
|
10
10
|
export * from "./services/pointerContext";
|
|
11
|
+
export * from "./services/interactions";
|
|
11
12
|
export * from "./module";
|
|
12
13
|
export * from "./Gui/Gui";
|
|
13
14
|
export * from "./components/gui";
|
|
@@ -30,3 +31,4 @@ export * from "./Game/ProjectileManager";
|
|
|
30
31
|
export * from "./Game/ClientVisuals";
|
|
31
32
|
export { withMobile } from "./components/gui/mobile";
|
|
32
33
|
export * from "./services/AbstractSocket";
|
|
34
|
+
export * from "./i18n";
|
package/src/module.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { findModules, provideModules } from "@rpgjs/common";
|
|
1
|
+
import { findModules, provideModules, registerI18nMessages } from "@rpgjs/common";
|
|
2
2
|
import { FactoryProvider } from "@signe/di";
|
|
3
3
|
import { RpgClientEngine } from "./RpgClientEngine";
|
|
4
4
|
import { RpgClient } from "./RpgClient";
|
|
@@ -67,6 +67,9 @@ export function provideClientModules(modules: RpgClientModule[]): FactoryProvide
|
|
|
67
67
|
if ('client' in module) {
|
|
68
68
|
module = module.client as any;
|
|
69
69
|
}
|
|
70
|
+
if (module.i18n) {
|
|
71
|
+
registerI18nMessages(context, module.i18n, "client-module", 10);
|
|
72
|
+
}
|
|
70
73
|
if (module.spritesheets) {
|
|
71
74
|
const spritesheets = [...module.spritesheets];
|
|
72
75
|
module.spritesheets = {
|
|
@@ -176,6 +179,25 @@ export function provideClientModules(modules: RpgClientModule[]): FactoryProvide
|
|
|
176
179
|
},
|
|
177
180
|
};
|
|
178
181
|
}
|
|
182
|
+
if (module.interactions) {
|
|
183
|
+
const interactions = module.interactions;
|
|
184
|
+
module.interactions = {
|
|
185
|
+
...interactions,
|
|
186
|
+
load: (engine: RpgClientEngine) => {
|
|
187
|
+
if (typeof interactions === "function") {
|
|
188
|
+
interactions(engine);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
interactions.load?.(engine);
|
|
192
|
+
interactions.setup?.(engine);
|
|
193
|
+
if (Array.isArray(interactions.use)) {
|
|
194
|
+
interactions.use.forEach(([matcher, behavior]) => {
|
|
195
|
+
engine.interactions.use(matcher, behavior);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
179
201
|
if (module.transitions) {
|
|
180
202
|
const transitions = [...module.transitions];
|
|
181
203
|
module.transitions = {
|
|
@@ -243,15 +265,15 @@ export function provideGlobalConfig(config: any) {
|
|
|
243
265
|
}
|
|
244
266
|
|
|
245
267
|
export function provideClientGlobalConfig(config: any = {}) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
268
|
+
config.keyboardControls = {
|
|
269
|
+
up: 'up',
|
|
270
|
+
down: 'down',
|
|
271
|
+
left: 'left',
|
|
272
|
+
right: 'right',
|
|
273
|
+
action: 'space',
|
|
274
|
+
dash: 'shift',
|
|
275
|
+
escape: 'escape',
|
|
276
|
+
...(config.keyboardControls ?? {}),
|
|
255
277
|
}
|
|
256
278
|
return provideGlobalConfig(config)
|
|
257
279
|
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
dragToTile,
|
|
4
|
+
hoverPopover,
|
|
5
|
+
RpgClientInteractions,
|
|
6
|
+
selectable,
|
|
7
|
+
} from "./interactions";
|
|
8
|
+
import { createClientPointerContext } from "./pointerContext";
|
|
9
|
+
|
|
10
|
+
function createClient() {
|
|
11
|
+
const client = {
|
|
12
|
+
pointer: createClientPointerContext(),
|
|
13
|
+
processAction: vi.fn(),
|
|
14
|
+
sceneMap: {
|
|
15
|
+
tileWidth: 16,
|
|
16
|
+
tileHeight: 16,
|
|
17
|
+
},
|
|
18
|
+
} as any;
|
|
19
|
+
client.interactions = new RpgClientInteractions(client);
|
|
20
|
+
return client;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("RpgClientInteractions", () => {
|
|
24
|
+
test("renders registered components with sprite state and bounds", () => {
|
|
25
|
+
const client = createClient();
|
|
26
|
+
const Popover = () => null;
|
|
27
|
+
const sprite = { id: "event-1", name: "Guard" };
|
|
28
|
+
|
|
29
|
+
client.interactions.use("Guard", hoverPopover(Popover, { label: "Talk" }));
|
|
30
|
+
client.interactions.handle(sprite, "pointerover", {
|
|
31
|
+
bounds: {
|
|
32
|
+
graphic: { left: 1, top: 2, right: 11, bottom: 22, width: 10, height: 20, centerX: 6, centerY: 12 } as any,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const entries = client.interactions.getRenderedComponents(sprite, {
|
|
37
|
+
graphic: { left: 1, top: 2, right: 11, bottom: 22, width: 10, height: 20, centerX: 6, centerY: 12 } as any,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(entries).toHaveLength(1);
|
|
41
|
+
expect(entries[0].component).toBe(Popover);
|
|
42
|
+
expect(entries[0].props.label).toBe("Talk");
|
|
43
|
+
expect(entries[0].props.state.hovered).toBe(true);
|
|
44
|
+
expect(entries[0].props.bounds.centerX).toBe(6);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("keeps clicks client-only unless a behavior sends an action", () => {
|
|
48
|
+
const client = createClient();
|
|
49
|
+
const sprite = { id: "event-1", name: "Guard" };
|
|
50
|
+
|
|
51
|
+
client.interactions.use("Guard", selectable());
|
|
52
|
+
client.interactions.handle(sprite, "click");
|
|
53
|
+
|
|
54
|
+
expect(client.interactions.getState(sprite).selected).toBe(true);
|
|
55
|
+
expect(client.processAction).not.toHaveBeenCalled();
|
|
56
|
+
|
|
57
|
+
client.interactions.use("Guard", {
|
|
58
|
+
click(ctx) {
|
|
59
|
+
ctx.action("guard:talk", { eventId: ctx.target.id });
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
client.interactions.handle(sprite, "click");
|
|
63
|
+
|
|
64
|
+
expect(client.processAction).toHaveBeenCalledWith("guard:talk", { eventId: "event-1" });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("uses behavior hit tests before changing hover state", () => {
|
|
68
|
+
const client = createClient();
|
|
69
|
+
const sprite = { id: "event-1", name: "Tree" };
|
|
70
|
+
|
|
71
|
+
client.pointer.update({ x: 0, y: 0 }, { x: 40, y: 40 });
|
|
72
|
+
client.interactions.use("Tree", {
|
|
73
|
+
cursor: "pointer",
|
|
74
|
+
hitTest(ctx) {
|
|
75
|
+
return ctx.bounds("hitbox").contains(ctx.pointer.world());
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
client.interactions.handle(sprite, "pointerover", {
|
|
80
|
+
bounds: {
|
|
81
|
+
hitbox: { left: 0, top: 0, right: 16, bottom: 16, width: 16, height: 16, centerX: 8, centerY: 8 } as any,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(client.interactions.getState(sprite).hovered).toBe(false);
|
|
86
|
+
expect(client.interactions.cursorFor(sprite, {
|
|
87
|
+
hitbox: { left: 0, top: 0, right: 16, bottom: 16, width: 16, height: 16, centerX: 8, centerY: 8 } as any,
|
|
88
|
+
})).toBeUndefined();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("exposes handler bounds in world coordinates", () => {
|
|
92
|
+
const client = createClient();
|
|
93
|
+
const sprite = { id: "crate-1", name: "Crate", x: () => 100, y: () => 80 };
|
|
94
|
+
|
|
95
|
+
client.pointer.update({ x: 0, y: 0 }, { x: 112, y: 92 });
|
|
96
|
+
client.interactions.use("Crate", {
|
|
97
|
+
component: () => null,
|
|
98
|
+
cursor: "grab",
|
|
99
|
+
hitTest(ctx) {
|
|
100
|
+
return ctx.bounds("hitbox").contains(ctx.pointer.world());
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
client.interactions.handle(sprite, "pointerover", {
|
|
105
|
+
bounds: {
|
|
106
|
+
hitbox: { left: 0, top: 0, right: 16, bottom: 16, width: 16, height: 16, centerX: 8, centerY: 8 } as any,
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(client.interactions.getState(sprite).hovered).toBe(true);
|
|
111
|
+
expect(client.interactions.cursorFor(sprite, {
|
|
112
|
+
hitbox: { left: 0, top: 0, right: 16, bottom: 16, width: 16, height: 16, centerX: 8, centerY: 8 } as any,
|
|
113
|
+
})).toBe("grab");
|
|
114
|
+
|
|
115
|
+
const [entry] = client.interactions.getRenderedComponents(sprite, {
|
|
116
|
+
hitbox: { left: 0, top: 0, right: 16, bottom: 16, width: 16, height: 16, centerX: 8, centerY: 8 } as any,
|
|
117
|
+
});
|
|
118
|
+
expect(entry?.props.bounds.centerX).toBe(8);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("updates hit-tested hover while moving inside an already hovered sprite", () => {
|
|
122
|
+
const client = createClient();
|
|
123
|
+
const sprite = { id: "crate-1", name: "Crate", x: () => 100, y: () => 80 };
|
|
124
|
+
|
|
125
|
+
client.interactions.use("Crate", {
|
|
126
|
+
hitTest(ctx) {
|
|
127
|
+
return ctx.bounds("hitbox").contains(ctx.pointer.world());
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
client.pointer.update({ x: 0, y: 0 }, { x: 140, y: 120 });
|
|
132
|
+
client.interactions.handle(sprite, "pointerover", {
|
|
133
|
+
bounds: {
|
|
134
|
+
hitbox: { left: 0, top: 0, right: 16, bottom: 16, width: 16, height: 16, centerX: 8, centerY: 8 } as any,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
expect(client.interactions.getState(sprite).hovered).toBe(false);
|
|
138
|
+
|
|
139
|
+
client.pointer.update({ x: 0, y: 0 }, { x: 112, y: 92 });
|
|
140
|
+
client.interactions.handle(sprite, "pointermove", {
|
|
141
|
+
bounds: {
|
|
142
|
+
hitbox: { left: 0, top: 0, right: 16, bottom: 16, width: 16, height: 16, centerX: 8, centerY: 8 } as any,
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(client.interactions.getState(sprite).hovered).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("runs drag lifecycle and resolves pointer tile on drop", () => {
|
|
150
|
+
const client = createClient();
|
|
151
|
+
const sprite = { id: "crate-1", name: "Crate" };
|
|
152
|
+
|
|
153
|
+
client.interactions.use("Crate", dragToTile({ action: "crate:move" }));
|
|
154
|
+
client.pointer.update({ x: 0, y: 0 }, { x: 18, y: 35 });
|
|
155
|
+
client.interactions.handle(sprite, "pointerdown");
|
|
156
|
+
|
|
157
|
+
expect(client.interactions.getState(sprite).dragging).toBe(true);
|
|
158
|
+
|
|
159
|
+
client.pointer.update({ x: 0, y: 0 }, { x: 33, y: 47 });
|
|
160
|
+
client.interactions.handlePointerUp();
|
|
161
|
+
|
|
162
|
+
expect(client.interactions.getState(sprite).dragging).toBe(false);
|
|
163
|
+
expect(client.processAction).toHaveBeenCalledWith("crate:move", {
|
|
164
|
+
eventId: "crate-1",
|
|
165
|
+
position: {
|
|
166
|
+
x: 2,
|
|
167
|
+
y: 2,
|
|
168
|
+
worldX: 32,
|
|
169
|
+
worldY: 32,
|
|
170
|
+
width: 16,
|
|
171
|
+
height: 16,
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
});
|