@rpgjs/client 5.0.0-beta.12 → 5.0.0-beta.13
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 +10 -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 +180 -17
- package/dist/RpgClientEngine.js.map +1 -1
- package/dist/components/character.ce.js +82 -7
- 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 +12 -7
- 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/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/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 +248 -29
- package/src/components/character.ce +90 -7
- 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 +12 -7
- package/src/components/scenes/draw-map.ce +16 -5
- 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
|
@@ -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,7 +48,6 @@
|
|
|
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'
|
|
53
52
|
|
|
54
53
|
const engine = inject(RpgClientEngine);
|
|
@@ -76,10 +75,16 @@
|
|
|
76
75
|
}
|
|
77
76
|
})
|
|
78
77
|
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
const normalizeOpenId = (value) => {
|
|
79
|
+
const resolved = typeof value === "function" ? value() : value
|
|
80
|
+
return typeof resolved === "string" && resolved.length > 0 ? resolved : undefined
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const onGuiFinish = (gui, data, guiOpenId) => {
|
|
84
|
+
const completedOpenId = normalizeOpenId(guiOpenId)
|
|
85
|
+
const currentOpenId = normalizeOpenId(gui.openId)
|
|
86
|
+
if (completedOpenId && currentOpenId && completedOpenId !== currentOpenId) return
|
|
87
|
+
guiService.guiClose(gui.name, data, completedOpenId ?? currentOpenId)
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
const onGuiInteraction = (gui, name, data) => {
|
|
@@ -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,
|
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
|
+
});
|