@rpgjs/client 5.0.0-beta.11 → 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.
Files changed (133) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/Game/AnimationManager.d.ts +1 -0
  3. package/dist/Game/AnimationManager.js +3 -0
  4. package/dist/Game/AnimationManager.js.map +1 -1
  5. package/dist/Game/ClientVisuals.d.ts +61 -0
  6. package/dist/Game/ClientVisuals.js +96 -0
  7. package/dist/Game/ClientVisuals.js.map +1 -0
  8. package/dist/Game/ClientVisuals.spec.d.ts +1 -0
  9. package/dist/Game/EventComponentResolver.d.ts +16 -0
  10. package/dist/Game/EventComponentResolver.js +52 -0
  11. package/dist/Game/EventComponentResolver.js.map +1 -0
  12. package/dist/Game/EventComponentResolver.spec.d.ts +1 -0
  13. package/dist/Game/Map.js +9 -0
  14. package/dist/Game/Map.js.map +1 -1
  15. package/dist/Game/Object.d.ts +2 -0
  16. package/dist/Game/Object.js +22 -8
  17. package/dist/Game/Object.js.map +1 -1
  18. package/dist/Game/Object.spec.d.ts +1 -0
  19. package/dist/Game/ProjectileManager.d.ts +11 -2
  20. package/dist/Game/ProjectileManager.js +19 -2
  21. package/dist/Game/ProjectileManager.js.map +1 -1
  22. package/dist/Gui/Gui.d.ts +3 -2
  23. package/dist/Gui/Gui.js +18 -6
  24. package/dist/Gui/Gui.js.map +1 -1
  25. package/dist/RpgClient.d.ts +85 -1
  26. package/dist/RpgClientEngine.d.ts +77 -2
  27. package/dist/RpgClientEngine.js +290 -31
  28. package/dist/RpgClientEngine.js.map +1 -1
  29. package/dist/components/animations/fx.ce.js +58 -0
  30. package/dist/components/animations/fx.ce.js.map +1 -0
  31. package/dist/components/animations/index.d.ts +1 -0
  32. package/dist/components/animations/index.js +3 -1
  33. package/dist/components/animations/index.js.map +1 -1
  34. package/dist/components/character.ce.js +192 -19
  35. package/dist/components/character.ce.js.map +1 -1
  36. package/dist/components/gui/dialogbox/index.ce.js +27 -12
  37. package/dist/components/gui/dialogbox/index.ce.js.map +1 -1
  38. package/dist/components/gui/gameover.ce.js +4 -3
  39. package/dist/components/gui/gameover.ce.js.map +1 -1
  40. package/dist/components/gui/menu/equip-menu.ce.js +9 -8
  41. package/dist/components/gui/menu/equip-menu.ce.js.map +1 -1
  42. package/dist/components/gui/menu/exit-menu.ce.js +7 -5
  43. package/dist/components/gui/menu/exit-menu.ce.js.map +1 -1
  44. package/dist/components/gui/menu/items-menu.ce.js +8 -7
  45. package/dist/components/gui/menu/items-menu.ce.js.map +1 -1
  46. package/dist/components/gui/menu/main-menu.ce.js +12 -11
  47. package/dist/components/gui/menu/main-menu.ce.js.map +1 -1
  48. package/dist/components/gui/menu/options-menu.ce.js +7 -5
  49. package/dist/components/gui/menu/options-menu.ce.js.map +1 -1
  50. package/dist/components/gui/menu/skills-menu.ce.js +4 -2
  51. package/dist/components/gui/menu/skills-menu.ce.js.map +1 -1
  52. package/dist/components/gui/notification/notification.ce.js +4 -1
  53. package/dist/components/gui/notification/notification.ce.js.map +1 -1
  54. package/dist/components/gui/save-load.ce.js +10 -9
  55. package/dist/components/gui/save-load.ce.js.map +1 -1
  56. package/dist/components/gui/shop/shop.ce.js +17 -16
  57. package/dist/components/gui/shop/shop.ce.js.map +1 -1
  58. package/dist/components/gui/title-screen.ce.js +4 -3
  59. package/dist/components/gui/title-screen.ce.js.map +1 -1
  60. package/dist/components/interaction-components.ce.js +20 -0
  61. package/dist/components/interaction-components.ce.js.map +1 -0
  62. package/dist/components/scenes/canvas.ce.js +12 -7
  63. package/dist/components/scenes/canvas.ce.js.map +1 -1
  64. package/dist/components/scenes/draw-map.ce.js +18 -13
  65. package/dist/components/scenes/draw-map.ce.js.map +1 -1
  66. package/dist/i18n.d.ts +55 -0
  67. package/dist/i18n.js +60 -0
  68. package/dist/i18n.js.map +1 -0
  69. package/dist/i18n.spec.d.ts +1 -0
  70. package/dist/index.d.ts +3 -0
  71. package/dist/index.js +5 -2
  72. package/dist/module.js +30 -3
  73. package/dist/module.js.map +1 -1
  74. package/dist/services/actionInput.d.ts +3 -1
  75. package/dist/services/actionInput.js +33 -1
  76. package/dist/services/actionInput.js.map +1 -1
  77. package/dist/services/interactions.d.ts +159 -0
  78. package/dist/services/interactions.js +460 -0
  79. package/dist/services/interactions.js.map +1 -0
  80. package/dist/services/interactions.spec.d.ts +1 -0
  81. package/dist/services/keyboardControls.d.ts +1 -0
  82. package/dist/services/keyboardControls.js +1 -0
  83. package/dist/services/keyboardControls.js.map +1 -1
  84. package/dist/services/standalone.d.ts +3 -1
  85. package/dist/services/standalone.js +31 -13
  86. package/dist/services/standalone.js.map +1 -1
  87. package/dist/utils/mapId.d.ts +1 -0
  88. package/dist/utils/mapId.js +6 -0
  89. package/dist/utils/mapId.js.map +1 -0
  90. package/package.json +4 -4
  91. package/src/Game/AnimationManager.ts +4 -0
  92. package/src/Game/ClientVisuals.spec.ts +56 -0
  93. package/src/Game/ClientVisuals.ts +184 -0
  94. package/src/Game/EventComponentResolver.spec.ts +84 -0
  95. package/src/Game/EventComponentResolver.ts +74 -0
  96. package/src/Game/Map.ts +10 -0
  97. package/src/Game/Object.spec.ts +59 -0
  98. package/src/Game/Object.ts +36 -12
  99. package/src/Game/ProjectileManager.spec.ts +111 -0
  100. package/src/Game/ProjectileManager.ts +24 -2
  101. package/src/Gui/Gui.spec.ts +67 -0
  102. package/src/Gui/Gui.ts +24 -7
  103. package/src/RpgClient.ts +96 -1
  104. package/src/RpgClientEngine.ts +378 -45
  105. package/src/components/animations/fx.ce +101 -0
  106. package/src/components/animations/index.ts +4 -2
  107. package/src/components/character.ce +243 -17
  108. package/src/components/gui/dialogbox/index.ce +35 -14
  109. package/src/components/gui/gameover.ce +4 -3
  110. package/src/components/gui/menu/equip-menu.ce +9 -8
  111. package/src/components/gui/menu/exit-menu.ce +4 -3
  112. package/src/components/gui/menu/items-menu.ce +8 -7
  113. package/src/components/gui/menu/main-menu.ce +12 -11
  114. package/src/components/gui/menu/options-menu.ce +4 -3
  115. package/src/components/gui/menu/skills-menu.ce +2 -1
  116. package/src/components/gui/notification/notification.ce +7 -1
  117. package/src/components/gui/save-load.ce +11 -10
  118. package/src/components/gui/shop/shop.ce +17 -16
  119. package/src/components/gui/title-screen.ce +4 -3
  120. package/src/components/interaction-components.ce +23 -0
  121. package/src/components/scenes/canvas.ce +12 -7
  122. package/src/components/scenes/draw-map.ce +16 -5
  123. package/src/i18n.spec.ts +39 -0
  124. package/src/i18n.ts +59 -0
  125. package/src/index.ts +3 -0
  126. package/src/module.ts +43 -10
  127. package/src/services/actionInput.spec.ts +54 -0
  128. package/src/services/actionInput.ts +68 -1
  129. package/src/services/interactions.spec.ts +175 -0
  130. package/src/services/interactions.ts +722 -0
  131. package/src/services/keyboardControls.ts +2 -1
  132. package/src/services/standalone.ts +39 -10
  133. package/src/utils/mapId.ts +2 -0
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";
@@ -27,5 +28,7 @@ export { RpgClientObject } from "./Game/Object";
27
28
  export { RpgClientPlayer } from "./Game/Player";
28
29
  export { RpgClientEvent } from "./Game/Event";
29
30
  export * from "./Game/ProjectileManager";
31
+ export * from "./Game/ClientVisuals";
30
32
  export { withMobile } from "./components/gui/mobile";
31
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 = {
@@ -155,6 +158,14 @@ export function provideClientModules(modules: RpgClientModule[]): FactoryProvide
155
158
  },
156
159
  };
157
160
  }
161
+ if (module.clientVisuals) {
162
+ const clientVisuals = { ...module.clientVisuals };
163
+ module.clientVisuals = {
164
+ load: (engine: RpgClientEngine) => {
165
+ engine.registerClientVisuals(clientVisuals);
166
+ },
167
+ };
168
+ }
158
169
  if (module.projectiles) {
159
170
  const projectiles = { ...module.projectiles };
160
171
  module.projectiles = {
@@ -168,6 +179,25 @@ export function provideClientModules(modules: RpgClientModule[]): FactoryProvide
168
179
  },
169
180
  };
170
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
+ }
171
201
  if (module.transitions) {
172
202
  const transitions = [...module.transitions];
173
203
  module.transitions = {
@@ -213,6 +243,9 @@ export function provideClientModules(modules: RpgClientModule[]): FactoryProvide
213
243
  engine.registerSpriteComponent(id, component);
214
244
  });
215
245
  }
246
+ if (sprite.eventComponent) {
247
+ engine.addEventComponentResolver(sprite.eventComponent);
248
+ }
216
249
  },
217
250
  };
218
251
  }
@@ -232,15 +265,15 @@ export function provideGlobalConfig(config: any) {
232
265
  }
233
266
 
234
267
  export function provideClientGlobalConfig(config: any = {}) {
235
- if (!config.keyboardControls) {
236
- config.keyboardControls = {
237
- up: 'up',
238
- down: 'down',
239
- left: 'left',
240
- right: 'right',
241
- action: 'space',
242
- escape: 'escape'
243
- }
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 ?? {}),
244
277
  }
245
278
  return provideGlobalConfig(config)
246
279
  }
@@ -1,10 +1,15 @@
1
1
  import { describe, expect, test } from "vitest";
2
2
  import {
3
3
  getKeyboardControlBind,
4
+ keyboardEventMatchesBind,
4
5
  normalizeActionInput,
5
6
  resolveKeyboardActionInput,
7
+ resolveKeyboardDirectionInput,
6
8
  } from "./actionInput";
7
9
 
10
+ const keyboardEvent = (values: Partial<KeyboardEvent>) =>
11
+ values as KeyboardEvent;
12
+
8
13
  describe("normalizeActionInput", () => {
9
14
  test("keeps simple actions compatible", () => {
10
15
  expect(normalizeActionInput("action")).toEqual({
@@ -98,4 +103,53 @@ describe("keyboard action controls", () => {
98
103
  action: "projectile:shoot",
99
104
  });
100
105
  });
106
+
107
+ test("matches keyboard events against string, numeric, and array binds", () => {
108
+ expect(
109
+ keyboardEventMatchesBind(
110
+ keyboardEvent({ key: " ", code: "Space", keyCode: 32 }),
111
+ "space"
112
+ )
113
+ ).toBe(true);
114
+ expect(
115
+ keyboardEventMatchesBind(
116
+ keyboardEvent({ key: "ArrowUp", code: "ArrowUp", keyCode: 38 }),
117
+ "up"
118
+ )
119
+ ).toBe(true);
120
+ expect(
121
+ keyboardEventMatchesBind(
122
+ keyboardEvent({ key: "x", code: "KeyX", keyCode: 88 }),
123
+ ["space", "x"]
124
+ )
125
+ ).toBe(true);
126
+ expect(
127
+ keyboardEventMatchesBind(
128
+ keyboardEvent({ key: "Escape", code: "Escape", keyCode: 27 }),
129
+ 27
130
+ )
131
+ ).toBe(true);
132
+ expect(
133
+ keyboardEventMatchesBind(
134
+ keyboardEvent({ key: "a", code: "KeyA", keyCode: 65 }),
135
+ "space"
136
+ )
137
+ ).toBe(false);
138
+ });
139
+
140
+ test("resolves directional keyboard controls from a native keyboard event", () => {
141
+ const controls = {
142
+ up: "up",
143
+ down: "down",
144
+ left: "left",
145
+ right: "right",
146
+ };
147
+
148
+ expect(
149
+ resolveKeyboardDirectionInput(
150
+ keyboardEvent({ key: "ArrowRight", code: "ArrowRight", keyCode: 39 }),
151
+ controls
152
+ )
153
+ ).toBe("right");
154
+ });
101
155
  });
@@ -1,4 +1,4 @@
1
- import type { RpgActionInput, RpgActionName } from "@rpgjs/common";
1
+ import { Direction, type RpgActionInput, type RpgActionName } from "@rpgjs/common";
2
2
 
3
3
  export type KeyboardActionDataResolver<TClient = any, TSprite = any> = (
4
4
  client: TClient,
@@ -32,6 +32,53 @@ export function getKeyboardControlBind(control: any): any {
32
32
  return isKeyboardActionConfig(control) ? control.bind : control;
33
33
  }
34
34
 
35
+ const KEY_CODE_NAMES: Record<number, string> = {
36
+ 32: "space",
37
+ 27: "escape",
38
+ 37: "left",
39
+ 38: "up",
40
+ 39: "right",
41
+ 40: "down",
42
+ };
43
+
44
+ const normalizeKeyboardName = (value: unknown): string | undefined => {
45
+ if (typeof value !== "string") return undefined;
46
+ const normalized = value.toLowerCase();
47
+ if (
48
+ normalized === " " ||
49
+ normalized === "spacebar" ||
50
+ normalized === "space"
51
+ ) {
52
+ return "space";
53
+ }
54
+ if (normalized.startsWith("arrow")) {
55
+ return normalized.slice("arrow".length);
56
+ }
57
+ return normalized;
58
+ };
59
+
60
+ export function keyboardEventMatchesBind(
61
+ event: KeyboardEvent,
62
+ bind: any
63
+ ): boolean {
64
+ if (Array.isArray(bind)) {
65
+ return bind.some(item => keyboardEventMatchesBind(event, item));
66
+ }
67
+
68
+ if (typeof bind === "number") {
69
+ return event.keyCode === bind;
70
+ }
71
+
72
+ const expected = normalizeKeyboardName(bind);
73
+ if (!expected) return false;
74
+
75
+ return (
76
+ normalizeKeyboardName(event.key) === expected ||
77
+ normalizeKeyboardName(event.code) === expected ||
78
+ KEY_CODE_NAMES[event.keyCode] === expected
79
+ );
80
+ }
81
+
35
82
  export function resolveKeyboardActionInput(
36
83
  control: any,
37
84
  client: any,
@@ -51,3 +98,23 @@ export function resolveKeyboardActionInput(
51
98
  ? { action }
52
99
  : { action, data };
53
100
  }
101
+
102
+ export function resolveKeyboardDirectionInput(
103
+ event: KeyboardEvent,
104
+ keyboardControls: any
105
+ ): Direction | undefined {
106
+ const directions: Array<[any, Direction]> = [
107
+ [keyboardControls?.up, Direction.Up],
108
+ [keyboardControls?.down, Direction.Down],
109
+ [keyboardControls?.left, Direction.Left],
110
+ [keyboardControls?.right, Direction.Right],
111
+ ];
112
+
113
+ for (const [control, direction] of directions) {
114
+ if (keyboardEventMatchesBind(event, getKeyboardControlBind(control))) {
115
+ return direction;
116
+ }
117
+ }
118
+
119
+ return undefined;
120
+ }
@@ -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
+ });