@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
@@ -0,0 +1,722 @@
1
+ import { signal } from "canvasengine";
2
+ import type { RpgActionInput, RpgActionName } from "@rpgjs/common";
3
+ import type { RpgClientEngine } from "../RpgClientEngine";
4
+
5
+ export type RpgInteractionEventName =
6
+ | "pointerenter"
7
+ | "pointerleave"
8
+ | "pointerover"
9
+ | "pointerout"
10
+ | "pointerdown"
11
+ | "pointerup"
12
+ | "pointermove"
13
+ | "click"
14
+ | "dragstart"
15
+ | "dragmove"
16
+ | "drop"
17
+ | "cancel";
18
+
19
+ export type RpgInteractionPosition = {
20
+ x: number;
21
+ y: number;
22
+ };
23
+
24
+ export type RpgInteractionBounds = {
25
+ left: number;
26
+ top: number;
27
+ right: number;
28
+ bottom: number;
29
+ width: number;
30
+ height: number;
31
+ centerX: number;
32
+ centerY: number;
33
+ contains(point: RpgInteractionPosition | null | undefined): boolean;
34
+ };
35
+
36
+ export type RpgInteractionBoundsSet = {
37
+ bounds?: RpgInteractionBounds;
38
+ hitbox?: RpgInteractionBounds;
39
+ graphic?: RpgInteractionBounds;
40
+ [key: string]: RpgInteractionBounds | undefined;
41
+ };
42
+
43
+ export type RpgInteractionTile = {
44
+ x: number;
45
+ y: number;
46
+ worldX: number;
47
+ worldY: number;
48
+ width: number;
49
+ height: number;
50
+ };
51
+
52
+ export type RpgInteractionState = {
53
+ hovered: boolean;
54
+ pressed: boolean;
55
+ selected: boolean;
56
+ dragging: boolean;
57
+ data: Record<string, any>;
58
+ overlays: Record<string, RpgInteractionOverlay>;
59
+ };
60
+
61
+ export type RpgInteractionOverlay = {
62
+ component: any;
63
+ props?: Record<string, any>;
64
+ };
65
+
66
+ export type RpgInteractionMatcher =
67
+ | string
68
+ | ((ctx: RpgInteractionMatcherContext) => boolean);
69
+
70
+ export type RpgInteractionMatcherContext = {
71
+ client: RpgClientEngine;
72
+ target: any;
73
+ sprite: any;
74
+ };
75
+
76
+ export type RpgInteractionHandler = (ctx: RpgInteractionContext) => void;
77
+
78
+ export type RpgInteractionBehavior = {
79
+ component?: any;
80
+ props?: Record<string, any> | ((ctx: RpgInteractionContext) => Record<string, any>);
81
+ dependencies?: any[] | ((ctx: RpgInteractionContext) => any[]);
82
+ cursor?: string | ((ctx: RpgInteractionContext) => string | undefined);
83
+ hitTest?: (ctx: RpgInteractionContext) => boolean;
84
+ pointerenter?: RpgInteractionHandler;
85
+ pointerleave?: RpgInteractionHandler;
86
+ pointerover?: RpgInteractionHandler;
87
+ pointerout?: RpgInteractionHandler;
88
+ pointerdown?: RpgInteractionHandler;
89
+ pointerup?: RpgInteractionHandler;
90
+ pointermove?: RpgInteractionHandler;
91
+ click?: RpgInteractionHandler;
92
+ dragstart?: RpgInteractionHandler;
93
+ dragmove?: RpgInteractionHandler;
94
+ drop?: RpgInteractionHandler;
95
+ cancel?: RpgInteractionHandler;
96
+ };
97
+
98
+ export type RpgInteractionComponentEntry = {
99
+ component: any;
100
+ props: Record<string, any>;
101
+ dependencies: any[];
102
+ };
103
+
104
+ export type RpgInteractionContext = {
105
+ client: RpgClientEngine;
106
+ target: any;
107
+ sprite: any;
108
+ event?: any;
109
+ behavior: RpgInteractionBehavior;
110
+ behaviorId: string;
111
+ pointer: {
112
+ screen(): RpgInteractionPosition | null;
113
+ world(): RpgInteractionPosition | null;
114
+ tile(): RpgInteractionTile | null;
115
+ };
116
+ bounds(kind?: string): RpgInteractionBounds;
117
+ state: {
118
+ value(): RpgInteractionState;
119
+ get<T = any>(key: string): T | undefined;
120
+ set(key: string, value: any): void;
121
+ patch(patch: Partial<Omit<RpgInteractionState, "data" | "overlays">>): void;
122
+ };
123
+ overlay: {
124
+ render(component: any, props?: Record<string, any>): void;
125
+ update(props?: Record<string, any>): void;
126
+ clear(): void;
127
+ };
128
+ select(selected?: boolean): void;
129
+ action(action: RpgActionName, data?: any): void;
130
+ action(input: RpgActionInput): void;
131
+ cancel(): void;
132
+ };
133
+
134
+ type InteractionRegistration = {
135
+ id: string;
136
+ matcher: RpgInteractionMatcher;
137
+ behavior: RpgInteractionBehavior;
138
+ };
139
+
140
+ type InteractionEventInput = {
141
+ event?: any;
142
+ bounds?: RpgInteractionBoundsSet;
143
+ };
144
+
145
+ type ActiveDrag = {
146
+ sprite: any;
147
+ behavior: RpgInteractionBehavior;
148
+ behaviorId: string;
149
+ bounds?: RpgInteractionBoundsSet;
150
+ cancelled: boolean;
151
+ };
152
+
153
+ const DEFAULT_STATE: RpgInteractionState = {
154
+ hovered: false,
155
+ pressed: false,
156
+ selected: false,
157
+ dragging: false,
158
+ data: {},
159
+ overlays: {},
160
+ };
161
+
162
+ function readValue(value: any): any {
163
+ return typeof value === "function" ? value() : value;
164
+ }
165
+
166
+ function createBounds(value: any): RpgInteractionBounds {
167
+ const left = Number(value?.left ?? value?.x ?? 0);
168
+ const top = Number(value?.top ?? value?.y ?? 0);
169
+ const width = Number(value?.width ?? value?.w ?? 0);
170
+ const height = Number(value?.height ?? value?.h ?? 0);
171
+ const right = Number(value?.right ?? left + width);
172
+ const bottom = Number(value?.bottom ?? top + height);
173
+ const resolvedWidth = Number.isFinite(width) && width > 0 ? width : Math.max(0, right - left);
174
+ const resolvedHeight = Number.isFinite(height) && height > 0 ? height : Math.max(0, bottom - top);
175
+
176
+ return {
177
+ left,
178
+ top,
179
+ right,
180
+ bottom,
181
+ width: resolvedWidth,
182
+ height: resolvedHeight,
183
+ centerX: Number(value?.centerX ?? left + resolvedWidth / 2),
184
+ centerY: Number(value?.centerY ?? top + resolvedHeight / 2),
185
+ contains(point) {
186
+ if (!point) return false;
187
+ return point.x >= left && point.x <= right && point.y >= top && point.y <= bottom;
188
+ },
189
+ };
190
+ }
191
+
192
+ function normalizeBounds(bounds?: RpgInteractionBoundsSet): RpgInteractionBoundsSet {
193
+ if (!bounds) return {};
194
+
195
+ return Object.entries(bounds).reduce<RpgInteractionBoundsSet>((next, [key, value]) => {
196
+ if (value) next[key] = createBounds(value);
197
+ return next;
198
+ }, {});
199
+ }
200
+
201
+ function readNumber(value: any, fallback = 0): number {
202
+ const resolved = readValue(value);
203
+ const number = Number(resolved);
204
+ return Number.isFinite(number) ? number : fallback;
205
+ }
206
+
207
+ function offsetBounds(bounds: RpgInteractionBounds, x: number, y: number): RpgInteractionBounds {
208
+ return createBounds({
209
+ left: bounds.left + x,
210
+ top: bounds.top + y,
211
+ right: bounds.right + x,
212
+ bottom: bounds.bottom + y,
213
+ width: bounds.width,
214
+ height: bounds.height,
215
+ centerX: bounds.centerX + x,
216
+ centerY: bounds.centerY + y,
217
+ });
218
+ }
219
+
220
+ function toWorldBounds(sprite: any, bounds: RpgInteractionBoundsSet): RpgInteractionBoundsSet {
221
+ const x = readNumber(sprite?.x);
222
+ const y = readNumber(sprite?.y);
223
+
224
+ return Object.entries(bounds).reduce<RpgInteractionBoundsSet>((next, [key, value]) => {
225
+ if (value) next[key] = offsetBounds(value, x, y);
226
+ return next;
227
+ }, {});
228
+ }
229
+
230
+ function normalizeBehavior(behavior: RpgInteractionBehavior | any): RpgInteractionBehavior {
231
+ if (typeof behavior === "function") {
232
+ return { component: behavior };
233
+ }
234
+ return behavior ?? {};
235
+ }
236
+
237
+ export class RpgClientInteractions {
238
+ private registrations = signal<InteractionRegistration[]>([]);
239
+ private states = signal<Record<string, RpgInteractionState>>({});
240
+ private activeDrag?: ActiveDrag;
241
+ private nextId = 0;
242
+
243
+ constructor(private client: RpgClientEngine) {}
244
+
245
+ use(matcher: RpgInteractionMatcher, behavior: RpgInteractionBehavior | any): () => void {
246
+ const registration = {
247
+ id: `interaction:${++this.nextId}`,
248
+ matcher,
249
+ behavior: normalizeBehavior(behavior),
250
+ };
251
+
252
+ this.registrations.update((registrations) => [...registrations, registration]);
253
+
254
+ return () => {
255
+ this.registrations.update((registrations) =>
256
+ registrations.filter((entry) => entry.id !== registration.id)
257
+ );
258
+ };
259
+ }
260
+
261
+ getState(sprite: any): RpgInteractionState {
262
+ const id = this.getSpriteId(sprite);
263
+ return {
264
+ ...DEFAULT_STATE,
265
+ ...(id ? this.states()[id] : undefined),
266
+ data: id ? { ...(this.states()[id]?.data ?? {}) } : {},
267
+ overlays: id ? { ...(this.states()[id]?.overlays ?? {}) } : {},
268
+ };
269
+ }
270
+
271
+ getRenderedComponents(sprite: any, bounds?: RpgInteractionBoundsSet): RpgInteractionComponentEntry[] {
272
+ const normalizedBounds = normalizeBounds(bounds);
273
+ const matched = this.getMatches(sprite);
274
+ const state = this.getState(sprite);
275
+ const entries: RpgInteractionComponentEntry[] = [];
276
+
277
+ matched.forEach((registration) => {
278
+ if (!registration.behavior.component) return;
279
+ const ctx = this.createContext(sprite, registration, { bounds: normalizedBounds });
280
+ entries.push({
281
+ component: registration.behavior.component,
282
+ props: {
283
+ ...this.defaultComponentProps(sprite, state, normalizedBounds),
284
+ ...this.resolveProps(registration.behavior.props, ctx),
285
+ },
286
+ dependencies: this.resolveDependencies(registration.behavior.dependencies, ctx),
287
+ });
288
+ });
289
+
290
+ Object.entries(state.overlays).forEach(([id, overlay]) => {
291
+ entries.push({
292
+ component: overlay.component,
293
+ props: {
294
+ ...this.defaultComponentProps(sprite, state, normalizedBounds),
295
+ ...(overlay.props ?? {}),
296
+ overlayId: id,
297
+ },
298
+ dependencies: [],
299
+ });
300
+ });
301
+
302
+ return entries;
303
+ }
304
+
305
+ cursorFor(sprite: any, bounds?: RpgInteractionBoundsSet): string | undefined {
306
+ this.states();
307
+ for (const registration of this.getMatches(sprite)) {
308
+ const cursor = registration.behavior.cursor;
309
+ if (!cursor) continue;
310
+ const ctx = this.createContext(sprite, registration, { bounds: normalizeBounds(bounds) });
311
+ if (!this.passesHitTest(registration.behavior, ctx, "pointermove")) continue;
312
+ const resolved = typeof cursor === "function" ? cursor(ctx) : cursor;
313
+ if (resolved) return resolved;
314
+ }
315
+ return undefined;
316
+ }
317
+
318
+ handle(sprite: any, type: RpgInteractionEventName, input: InteractionEventInput = {}): void {
319
+ const matches = this.getMatches(sprite);
320
+ const bounds = normalizeBounds(input.bounds);
321
+ const entries = matches
322
+ .map((registration) => ({
323
+ registration,
324
+ ctx: this.createContext(sprite, registration, {
325
+ event: input.event,
326
+ bounds,
327
+ }),
328
+ }))
329
+ .filter(({ registration, ctx }) =>
330
+ this.passesHitTest(registration.behavior, ctx, type)
331
+ );
332
+
333
+ if (type === "pointerover" || type === "pointerenter") {
334
+ if (entries.length > 0) {
335
+ this.patchState(sprite, { hovered: true });
336
+ }
337
+ }
338
+ if (type === "pointerout" || type === "pointerleave") {
339
+ if (matches.length > 0) {
340
+ this.patchState(sprite, { hovered: false, pressed: false });
341
+ }
342
+ }
343
+ if (type === "pointerdown") {
344
+ if (entries.length > 0) {
345
+ this.patchState(sprite, { pressed: true });
346
+ }
347
+ }
348
+ if (type === "pointermove" && matches.length > 0) {
349
+ this.patchState(sprite, { hovered: entries.length > 0 });
350
+ }
351
+ if (type === "pointerup") {
352
+ if (matches.length > 0) {
353
+ this.patchState(sprite, { pressed: false });
354
+ }
355
+ }
356
+
357
+ entries.forEach(({ registration, ctx }) => {
358
+ this.callHandler(registration.behavior, type, ctx);
359
+
360
+ if (type === "pointerdown" && this.isDraggable(registration.behavior)) {
361
+ this.activeDrag = {
362
+ sprite,
363
+ behavior: registration.behavior,
364
+ behaviorId: registration.id,
365
+ bounds,
366
+ cancelled: false,
367
+ };
368
+ this.patchState(sprite, { dragging: true });
369
+ this.callHandler(registration.behavior, "dragstart", ctx);
370
+ }
371
+ });
372
+ }
373
+
374
+ handlePointerMove(event?: any): void {
375
+ if (!this.activeDrag) return;
376
+ const drag = this.activeDrag;
377
+ const registration = {
378
+ id: drag.behaviorId,
379
+ matcher: "*",
380
+ behavior: drag.behavior,
381
+ };
382
+ const ctx = this.createContext(drag.sprite, registration, {
383
+ event,
384
+ bounds: drag.bounds,
385
+ });
386
+ this.callHandler(drag.behavior, "dragmove", ctx);
387
+ }
388
+
389
+ handlePointerUp(event?: any): void {
390
+ if (!this.activeDrag) return;
391
+ const drag = this.activeDrag;
392
+ this.activeDrag = undefined;
393
+ this.patchState(drag.sprite, { dragging: false, pressed: false });
394
+
395
+ const registration = {
396
+ id: drag.behaviorId,
397
+ matcher: "*",
398
+ behavior: drag.behavior,
399
+ };
400
+ const ctx = this.createContext(drag.sprite, registration, {
401
+ event,
402
+ bounds: drag.bounds,
403
+ });
404
+
405
+ this.callHandler(drag.behavior, drag.cancelled ? "cancel" : "drop", ctx);
406
+ }
407
+
408
+ cancelDrag(event?: any): void {
409
+ if (!this.activeDrag) return;
410
+ this.activeDrag.cancelled = true;
411
+ this.handlePointerUp(event);
412
+ }
413
+
414
+ private getMatches(sprite: any): InteractionRegistration[] {
415
+ return this.registrations().filter((registration) =>
416
+ this.matches(registration.matcher, sprite)
417
+ );
418
+ }
419
+
420
+ private matches(matcher: RpgInteractionMatcher, sprite: any): boolean {
421
+ if (typeof matcher === "function") {
422
+ return !!matcher({ client: this.client, target: sprite, sprite });
423
+ }
424
+
425
+ if (matcher === "*") return true;
426
+
427
+ const candidates = [
428
+ readValue(sprite?.id),
429
+ readValue(sprite?.name),
430
+ readValue(sprite?._name),
431
+ readValue(sprite?.type),
432
+ readValue(sprite?._type),
433
+ sprite?.constructor?.name,
434
+ ].filter(Boolean);
435
+
436
+ return candidates.includes(matcher);
437
+ }
438
+
439
+ private createContext(
440
+ sprite: any,
441
+ registration: Pick<InteractionRegistration, "id" | "behavior">,
442
+ input: InteractionEventInput = {},
443
+ ): RpgInteractionContext {
444
+ const bounds = toWorldBounds(sprite, normalizeBounds(input.bounds));
445
+
446
+ const ctx = {
447
+ client: this.client,
448
+ target: sprite,
449
+ sprite,
450
+ event: input.event,
451
+ behavior: registration.behavior,
452
+ behaviorId: registration.id,
453
+ pointer: {
454
+ screen: () => this.client.pointer.screen(),
455
+ world: () => this.client.pointer.world(),
456
+ tile: () => this.getPointerTile(),
457
+ },
458
+ bounds: (kind = "bounds") =>
459
+ bounds[kind] ?? bounds.graphic ?? bounds.hitbox ?? bounds.bounds ?? createBounds({}),
460
+ state: {
461
+ value: () => this.getState(sprite),
462
+ get: <T = any>(key: string) => this.getState(sprite).data[key] as T | undefined,
463
+ set: (key: string, value: any) => this.patchStateData(sprite, { [key]: value }),
464
+ patch: (patch: Partial<Omit<RpgInteractionState, "data" | "overlays">>) =>
465
+ this.patchState(sprite, patch),
466
+ },
467
+ overlay: {
468
+ render: (component: any, props?: Record<string, any>) =>
469
+ this.patchOverlay(sprite, registration.id, { component, props }),
470
+ update: (props?: Record<string, any>) =>
471
+ this.updateOverlay(sprite, registration.id, props),
472
+ clear: () => this.clearOverlay(sprite, registration.id),
473
+ },
474
+ select: (selected = true) => this.patchState(sprite, { selected }),
475
+ action: (action: RpgActionName | RpgActionInput, data?: any) =>
476
+ this.client.processAction(action as any, data),
477
+ cancel: () => {
478
+ const activeDrag = this.activeDrag;
479
+ if (activeDrag && activeDrag.sprite === sprite) {
480
+ activeDrag.cancelled = true;
481
+ }
482
+ },
483
+ };
484
+
485
+ return ctx;
486
+ }
487
+
488
+ private callHandler(
489
+ behavior: RpgInteractionBehavior,
490
+ type: RpgInteractionEventName,
491
+ ctx: RpgInteractionContext,
492
+ ): void {
493
+ const handler = behavior[type];
494
+ handler?.(ctx);
495
+
496
+ if (type === "pointerover") {
497
+ behavior.pointerenter?.(ctx);
498
+ }
499
+ if (type === "pointerout") {
500
+ behavior.pointerleave?.(ctx);
501
+ }
502
+ }
503
+
504
+ private passesHitTest(
505
+ behavior: RpgInteractionBehavior,
506
+ ctx: RpgInteractionContext,
507
+ type: RpgInteractionEventName,
508
+ ): boolean {
509
+ if (!behavior.hitTest) return true;
510
+ if (type === "pointerout" || type === "pointerleave" || type === "cancel" || type === "drop") {
511
+ return true;
512
+ }
513
+ return behavior.hitTest(ctx);
514
+ }
515
+
516
+ private isDraggable(behavior: RpgInteractionBehavior): boolean {
517
+ return !!(behavior.dragstart || behavior.dragmove || behavior.drop || behavior.cancel);
518
+ }
519
+
520
+ private defaultComponentProps(
521
+ sprite: any,
522
+ state: RpgInteractionState,
523
+ bounds: RpgInteractionBoundsSet,
524
+ ): Record<string, any> {
525
+ return {
526
+ target: sprite,
527
+ sprite,
528
+ state,
529
+ bounds: bounds.bounds ?? bounds.graphic ?? bounds.hitbox ?? createBounds({}),
530
+ hitboxBounds: bounds.hitbox,
531
+ graphicBounds: bounds.graphic,
532
+ pointer: this.client.pointer,
533
+ client: this.client,
534
+ };
535
+ }
536
+
537
+ private resolveProps(
538
+ props: RpgInteractionBehavior["props"],
539
+ ctx: RpgInteractionContext,
540
+ ): Record<string, any> {
541
+ if (!props) return {};
542
+ return typeof props === "function" ? props(ctx) : props;
543
+ }
544
+
545
+ private resolveDependencies(
546
+ dependencies: RpgInteractionBehavior["dependencies"],
547
+ ctx: RpgInteractionContext,
548
+ ): any[] {
549
+ if (!dependencies) return [];
550
+ return typeof dependencies === "function" ? dependencies(ctx) : dependencies;
551
+ }
552
+
553
+ private getSpriteId(sprite: any): string | undefined {
554
+ const id = readValue(sprite?.id);
555
+ return id == null ? undefined : String(id);
556
+ }
557
+
558
+ private patchState(
559
+ sprite: any,
560
+ patch: Partial<Omit<RpgInteractionState, "data" | "overlays">>,
561
+ ): void {
562
+ const id = this.getSpriteId(sprite);
563
+ if (!id) return;
564
+ this.states.update((states) => ({
565
+ ...states,
566
+ [id]: {
567
+ ...DEFAULT_STATE,
568
+ ...(states[id] ?? {}),
569
+ ...patch,
570
+ data: states[id]?.data ?? {},
571
+ overlays: states[id]?.overlays ?? {},
572
+ },
573
+ }));
574
+ }
575
+
576
+ private patchStateData(sprite: any, patch: Record<string, any>): void {
577
+ const id = this.getSpriteId(sprite);
578
+ if (!id) return;
579
+ const current = this.getState(sprite);
580
+ this.states.update((states) => ({
581
+ ...states,
582
+ [id]: {
583
+ ...current,
584
+ data: {
585
+ ...current.data,
586
+ ...patch,
587
+ },
588
+ },
589
+ }));
590
+ }
591
+
592
+ private patchOverlay(sprite: any, id: string, overlay: RpgInteractionOverlay): void {
593
+ const spriteId = this.getSpriteId(sprite);
594
+ if (!spriteId) return;
595
+ const current = this.getState(sprite);
596
+ this.states.update((states) => ({
597
+ ...states,
598
+ [spriteId]: {
599
+ ...current,
600
+ overlays: {
601
+ ...current.overlays,
602
+ [id]: overlay,
603
+ },
604
+ },
605
+ }));
606
+ }
607
+
608
+ private updateOverlay(sprite: any, id: string, props?: Record<string, any>): void {
609
+ const current = this.getState(sprite);
610
+ const overlay = current.overlays[id];
611
+ if (!overlay) return;
612
+ this.patchOverlay(sprite, id, {
613
+ ...overlay,
614
+ props: {
615
+ ...(overlay.props ?? {}),
616
+ ...(props ?? {}),
617
+ },
618
+ });
619
+ }
620
+
621
+ private clearOverlay(sprite: any, id: string): void {
622
+ const spriteId = this.getSpriteId(sprite);
623
+ if (!spriteId) return;
624
+ const current = this.getState(sprite);
625
+ const { [id]: _removed, ...overlays } = current.overlays;
626
+ this.states.update((states) => ({
627
+ ...states,
628
+ [spriteId]: {
629
+ ...current,
630
+ overlays,
631
+ },
632
+ }));
633
+ }
634
+
635
+ private getPointerTile(): RpgInteractionTile | null {
636
+ const world = this.client.pointer.world();
637
+ if (!world) return null;
638
+
639
+ const map = (this.client as any).sceneMap;
640
+ const width = Number(map?.tileWidth ?? 32);
641
+ const height = Number(map?.tileHeight ?? 32);
642
+ const tileX = Math.floor(world.x / width);
643
+ const tileY = Math.floor(world.y / height);
644
+
645
+ return {
646
+ x: tileX,
647
+ y: tileY,
648
+ worldX: tileX * width,
649
+ worldY: tileY * height,
650
+ width,
651
+ height,
652
+ };
653
+ }
654
+ }
655
+
656
+ export function hoverPopover(component: any, props?: Record<string, any>): RpgInteractionBehavior {
657
+ return {
658
+ component,
659
+ props,
660
+ cursor: "pointer",
661
+ };
662
+ }
663
+
664
+ export function selectable(options: {
665
+ cursor?: string;
666
+ onSelect?: RpgInteractionHandler;
667
+ } = {}): RpgInteractionBehavior {
668
+ return {
669
+ cursor: options.cursor ?? "pointer",
670
+ click(ctx) {
671
+ ctx.select();
672
+ options.onSelect?.(ctx);
673
+ },
674
+ };
675
+ }
676
+
677
+ export function draggable(options: {
678
+ cursor?: string;
679
+ start?: RpgInteractionHandler;
680
+ move?: RpgInteractionHandler;
681
+ drop?: RpgInteractionHandler;
682
+ cancel?: RpgInteractionHandler;
683
+ } = {}): RpgInteractionBehavior {
684
+ return {
685
+ cursor: options.cursor ?? "grab",
686
+ dragstart(ctx) {
687
+ ctx.state.patch({ dragging: true });
688
+ options.start?.(ctx);
689
+ },
690
+ dragmove: options.move,
691
+ drop(ctx) {
692
+ ctx.state.patch({ dragging: false, pressed: false });
693
+ options.drop?.(ctx);
694
+ },
695
+ cancel(ctx) {
696
+ ctx.state.patch({ dragging: false, pressed: false });
697
+ options.cancel?.(ctx);
698
+ },
699
+ };
700
+ }
701
+
702
+ export function dragToTile(options: {
703
+ action?: RpgActionName;
704
+ data?: (ctx: RpgInteractionContext) => any;
705
+ onDrop?: RpgInteractionHandler;
706
+ cursor?: string;
707
+ }): RpgInteractionBehavior {
708
+ return draggable({
709
+ cursor: options.cursor,
710
+ drop(ctx) {
711
+ if (options.onDrop) {
712
+ options.onDrop(ctx);
713
+ return;
714
+ }
715
+ if (!options.action) return;
716
+ ctx.action(options.action, options.data ? options.data(ctx) : {
717
+ eventId: ctx.target.id,
718
+ position: ctx.pointer.tile(),
719
+ });
720
+ },
721
+ });
722
+ }