@rpgjs/client 5.0.0-beta.20 → 5.0.0-beta.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/Game/Object.d.ts +1 -0
  3. package/dist/Game/Object.js +10 -0
  4. package/dist/Game/Object.js.map +1 -1
  5. package/dist/Gui/Gui.js +37 -4
  6. package/dist/Gui/Gui.js.map +1 -1
  7. package/dist/RpgClientEngine.d.ts +18 -9
  8. package/dist/RpgClientEngine.js +40 -13
  9. package/dist/RpgClientEngine.js.map +1 -1
  10. package/dist/components/character-hitbox.d.ts +13 -0
  11. package/dist/components/character-hitbox.js +38 -0
  12. package/dist/components/character-hitbox.js.map +1 -0
  13. package/dist/components/character-hitbox.spec.d.ts +1 -0
  14. package/dist/components/character.ce.js +179 -36
  15. package/dist/components/character.ce.js.map +1 -1
  16. package/dist/components/gui/mobile/index.d.ts +51 -2
  17. package/dist/components/gui/mobile/index.js +12 -4
  18. package/dist/components/gui/mobile/index.js.map +1 -1
  19. package/dist/components/gui/mobile/index.spec.d.ts +1 -0
  20. package/dist/components/gui/mobile/mobile.ce.js +303 -55
  21. package/dist/components/gui/mobile/mobile.ce.js.map +1 -1
  22. package/dist/index.d.ts +1 -0
  23. package/dist/services/cameraFollow.d.ts +51 -0
  24. package/dist/services/cameraFollow.js +134 -0
  25. package/dist/services/cameraFollow.js.map +1 -0
  26. package/dist/services/cameraFollow.spec.d.ts +1 -0
  27. package/dist/services/standalone.d.ts +6 -1
  28. package/dist/services/standalone.js +35 -17
  29. package/dist/services/standalone.js.map +1 -1
  30. package/dist/utils/syncHitbox.d.ts +1 -0
  31. package/dist/utils/syncHitbox.js +69 -0
  32. package/dist/utils/syncHitbox.js.map +1 -0
  33. package/dist/utils/syncHitbox.spec.d.ts +1 -0
  34. package/package.json +5 -5
  35. package/src/Game/Object.spec.ts +44 -4
  36. package/src/Game/Object.ts +19 -0
  37. package/src/Gui/Gui.spec.ts +19 -0
  38. package/src/Gui/Gui.ts +50 -11
  39. package/src/RpgClientEngine.ts +56 -24
  40. package/src/components/character-hitbox.spec.ts +33 -0
  41. package/src/components/character-hitbox.ts +72 -0
  42. package/src/components/character.ce +207 -48
  43. package/src/components/gui/mobile/index.spec.ts +94 -0
  44. package/src/components/gui/mobile/index.ts +74 -6
  45. package/src/components/gui/mobile/mobile.ce +347 -65
  46. package/src/index.ts +12 -0
  47. package/src/services/cameraFollow.spec.ts +220 -0
  48. package/src/services/cameraFollow.ts +222 -0
  49. package/src/services/standalone.spec.ts +47 -0
  50. package/src/services/standalone.ts +53 -20
  51. package/src/utils/syncHitbox.spec.ts +79 -0
  52. package/src/utils/syncHitbox.ts +104 -0
@@ -0,0 +1,13 @@
1
+ export type CharacterHitbox = {
2
+ w: number;
3
+ h: number;
4
+ anchorMode?: "top-left" | "center" | "foot";
5
+ };
6
+ export declare const toPositiveNumber: (value: unknown) => number | undefined;
7
+ export declare const scaleHitboxForGraphicDisplay: (box: CharacterHitbox | null | undefined, scale: [number, number]) => CharacterHitbox | null;
8
+ export declare const resolveHitboxAnchor: (spriteWidth: number | undefined, spriteHeight: number | undefined, realSize: number | {
9
+ height?: number;
10
+ } | undefined, box: CharacterHitbox | null | undefined) => [number, number];
11
+ export declare const resolveScaledHitboxAnchor: (spriteWidth: number | undefined, spriteHeight: number | undefined, realSize: number | {
12
+ height?: number;
13
+ } | undefined, box: CharacterHitbox | null | undefined, scale: [number, number]) => [number, number];
@@ -0,0 +1,38 @@
1
+ //#region src/components/character-hitbox.ts
2
+ var toPositiveNumber = (value) => {
3
+ const number = typeof value === "number" ? value : parseFloat(String(value));
4
+ return Number.isFinite(number) && number > 0 ? number : void 0;
5
+ };
6
+ var clampRatio = (value) => Math.min(1, Math.max(0, value));
7
+ var scaleHitboxForGraphicDisplay = (box, scale) => {
8
+ if (!box) return null;
9
+ const scaleX = Math.abs(toPositiveNumber(scale[0]) ?? 1);
10
+ const scaleY = Math.abs(toPositiveNumber(scale[1]) ?? scaleX);
11
+ return {
12
+ ...box,
13
+ w: box.w / scaleX,
14
+ h: box.h / scaleY
15
+ };
16
+ };
17
+ var resolveHitboxAnchor = (spriteWidth, spriteHeight, realSize, box) => {
18
+ if (!spriteWidth || !spriteHeight || !box) return [0, 0];
19
+ const resolvedHeight = toPositiveNumber(typeof realSize === "number" ? realSize : realSize?.height) ?? spriteHeight;
20
+ const gap = Math.max(0, (spriteHeight - resolvedHeight) / 2);
21
+ const hitboxTopLeftX = clampRatio((spriteWidth - box.w) / 2 / spriteWidth);
22
+ const hitboxTopLeftY = clampRatio((spriteHeight - box.h - gap) / spriteHeight);
23
+ const hitboxCenterX = clampRatio(hitboxTopLeftX + box.w / 2 / spriteWidth);
24
+ const hitboxCenterY = clampRatio(hitboxTopLeftY + box.h / 2 / spriteHeight);
25
+ const footY = clampRatio((spriteHeight - gap) / spriteHeight);
26
+ switch (box.anchorMode ?? "top-left") {
27
+ case "center": return [hitboxCenterX, hitboxCenterY];
28
+ case "foot": return [hitboxCenterX, footY];
29
+ default: return [hitboxTopLeftX, hitboxTopLeftY];
30
+ }
31
+ };
32
+ var resolveScaledHitboxAnchor = (spriteWidth, spriteHeight, realSize, box, scale) => {
33
+ return resolveHitboxAnchor(spriteWidth, spriteHeight, realSize, scaleHitboxForGraphicDisplay(box, scale));
34
+ };
35
+ //#endregion
36
+ export { resolveScaledHitboxAnchor, scaleHitboxForGraphicDisplay, toPositiveNumber };
37
+
38
+ //# sourceMappingURL=character-hitbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"character-hitbox.js","names":[],"sources":["../../src/components/character-hitbox.ts"],"sourcesContent":["export type CharacterHitbox = {\n w: number;\n h: number;\n anchorMode?: \"top-left\" | \"center\" | \"foot\";\n};\n\nexport const toPositiveNumber = (value: unknown): number | undefined => {\n const number = typeof value === \"number\" ? value : parseFloat(String(value));\n return Number.isFinite(number) && number > 0 ? number : undefined;\n};\n\nconst clampRatio = (value: number): number => Math.min(1, Math.max(0, value));\n\nexport const scaleHitboxForGraphicDisplay = (\n box: CharacterHitbox | null | undefined,\n scale: [number, number],\n): CharacterHitbox | null => {\n if (!box) return null;\n const scaleX = Math.abs(toPositiveNumber(scale[0]) ?? 1);\n const scaleY = Math.abs(toPositiveNumber(scale[1]) ?? scaleX);\n\n return {\n ...box,\n w: box.w / scaleX,\n h: box.h / scaleY,\n };\n};\n\nexport const resolveHitboxAnchor = (\n spriteWidth: number | undefined,\n spriteHeight: number | undefined,\n realSize: number | { height?: number } | undefined,\n box: CharacterHitbox | null | undefined,\n): [number, number] => {\n if (!spriteWidth || !spriteHeight || !box) {\n return [0, 0];\n }\n\n const heightOfSprite = typeof realSize === \"number\" ? realSize : realSize?.height;\n const resolvedHeight = toPositiveNumber(heightOfSprite) ?? spriteHeight;\n const gap = Math.max(0, (spriteHeight - resolvedHeight) / 2);\n const hitboxTopLeftX = clampRatio((spriteWidth - box.w) / 2 / spriteWidth);\n const hitboxTopLeftY = clampRatio((spriteHeight - box.h - gap) / spriteHeight);\n const hitboxCenterX = clampRatio(hitboxTopLeftX + box.w / 2 / spriteWidth);\n const hitboxCenterY = clampRatio(hitboxTopLeftY + box.h / 2 / spriteHeight);\n const footY = clampRatio((spriteHeight - gap) / spriteHeight);\n\n switch (box.anchorMode ?? \"top-left\") {\n case \"center\":\n return [hitboxCenterX, hitboxCenterY];\n case \"foot\":\n return [hitboxCenterX, footY];\n case \"top-left\":\n default:\n return [hitboxTopLeftX, hitboxTopLeftY];\n }\n};\n\nexport const resolveScaledHitboxAnchor = (\n spriteWidth: number | undefined,\n spriteHeight: number | undefined,\n realSize: number | { height?: number } | undefined,\n box: CharacterHitbox | null | undefined,\n scale: [number, number],\n): [number, number] => {\n return resolveHitboxAnchor(\n spriteWidth,\n spriteHeight,\n realSize,\n scaleHitboxForGraphicDisplay(box, scale),\n );\n};\n"],"mappings":";AAMA,IAAa,oBAAoB,UAAuC;CACtE,MAAM,SAAS,OAAO,UAAU,WAAW,QAAQ,WAAW,OAAO,KAAK,CAAC;CAC3E,OAAO,OAAO,SAAS,MAAM,KAAK,SAAS,IAAI,SAAS,KAAA;AAC1D;AAEA,IAAM,cAAc,UAA0B,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,KAAK,CAAC;AAE5E,IAAa,gCACX,KACA,UAC2B;CAC3B,IAAI,CAAC,KAAK,OAAO;CACjB,MAAM,SAAS,KAAK,IAAI,iBAAiB,MAAM,EAAE,KAAK,CAAC;CACvD,MAAM,SAAS,KAAK,IAAI,iBAAiB,MAAM,EAAE,KAAK,MAAM;CAE5D,OAAO;EACL,GAAG;EACH,GAAG,IAAI,IAAI;EACX,GAAG,IAAI,IAAI;CACb;AACF;AAEA,IAAa,uBACX,aACA,cACA,UACA,QACqB;CACrB,IAAI,CAAC,eAAe,CAAC,gBAAgB,CAAC,KACpC,OAAO,CAAC,GAAG,CAAC;CAId,MAAM,iBAAiB,iBADA,OAAO,aAAa,WAAW,WAAW,UAAU,MACrB,KAAK;CAC3D,MAAM,MAAM,KAAK,IAAI,IAAI,eAAe,kBAAkB,CAAC;CAC3D,MAAM,iBAAiB,YAAY,cAAc,IAAI,KAAK,IAAI,WAAW;CACzE,MAAM,iBAAiB,YAAY,eAAe,IAAI,IAAI,OAAO,YAAY;CAC7E,MAAM,gBAAgB,WAAW,iBAAiB,IAAI,IAAI,IAAI,WAAW;CACzE,MAAM,gBAAgB,WAAW,iBAAiB,IAAI,IAAI,IAAI,YAAY;CAC1E,MAAM,QAAQ,YAAY,eAAe,OAAO,YAAY;CAE5D,QAAQ,IAAI,cAAc,YAA1B;EACE,KAAK,UACH,OAAO,CAAC,eAAe,aAAa;EACtC,KAAK,QACH,OAAO,CAAC,eAAe,KAAK;EAE9B,SACE,OAAO,CAAC,gBAAgB,cAAc;CAC1C;AACF;AAEA,IAAa,6BACX,aACA,cACA,UACA,KACA,UACqB;CACrB,OAAO,oBACL,aACA,cACA,UACA,6BAA6B,KAAK,KAAK,CACzC;AACF"}
@@ -0,0 +1 @@
1
+ export {};
@@ -4,6 +4,8 @@ import { getCanMoveValue } from "../utils/readPropValue.js";
4
4
  import { RpgGui } from "../Gui/Gui.js";
5
5
  import { normalizeEventComponent } from "../Game/EventComponentResolver.js";
6
6
  import { RpgClientEngine } from "../RpgClientEngine.js";
7
+ import { applyCameraFollow, clearCameraFollowPlugins, ownsCameraFollowRevision } from "../services/cameraFollow.js";
8
+ import { resolveScaledHitboxAnchor, scaleHitboxForGraphicDisplay, toPositiveNumber } from "./character-hitbox.js";
7
9
  import __ce_component$1 from "./player-components.ce.js";
8
10
  import __ce_component$2 from "./interaction-components.ce.js";
9
11
  import { Container, Sprite, animatedSignal, computed, cond, effect, h, loop, mount, on, signal, tick, useDefineEmits, useDefineProps, useProps } from "canvasengine";
@@ -118,8 +120,33 @@ function component($$props) {
118
120
  const canControls = () => isMe() && getCanMoveValue(sprite);
119
121
  const keyboardControls = client.globalConfig.keyboardControls;
120
122
  const activeDirectionKeys = /* @__PURE__ */ new Map();
123
+ const activeControlDirections = /* @__PURE__ */ new Map();
124
+ const activeControlDirectionReleaseTimers = /* @__PURE__ */ new Map();
125
+ const CONTROL_DIRECTION_RELEASE_GRACE_MS = 160;
126
+ const hasHeldDirection = () => activeDirectionKeys.size > 0 || activeControlDirections.size > 0;
127
+ const publishMobileDebug = (type, payload = {}) => {
128
+ const target = typeof window !== "undefined" ? window : globalThis;
129
+ if (!target.__RPGJS_MOBILE_DEBUG__) return;
130
+ const event = {
131
+ source: "character",
132
+ type,
133
+ time: Date.now(),
134
+ isCurrentPlayer: isCurrentPlayer(),
135
+ animationName: animationName(),
136
+ realAnimationName: realAnimationName?.(),
137
+ activeKeyboardDirections: Array.from(activeDirectionKeys.values()),
138
+ activeControlDirections: Array.from(activeControlDirections.values()),
139
+ heldDirection: resolveHeldDirection(),
140
+ canControls: canControls(),
141
+ x: x(),
142
+ y: y(),
143
+ ...payload
144
+ };
145
+ target.__RPGJS_MOBILE_DEBUG_EVENTS__ = [...(target.__RPGJS_MOBILE_DEBUG_EVENTS__ || []).slice(-199), event];
146
+ target.__RPGJS_MOBILE_DEBUG_LAST__ = event;
147
+ };
121
148
  const resolveHeldDirection = () => {
122
- const directions = Array.from(activeDirectionKeys.values());
149
+ const directions = [...Array.from(activeDirectionKeys.values()), ...Array.from(activeControlDirections.values())];
123
150
  return directions[directions.length - 1];
124
151
  };
125
152
  const resolveSpriteDirection = () => {
@@ -163,24 +190,75 @@ function component($$props) {
163
190
  };
164
191
  const resolveCurrentActionInput = () => withCurrentDirection(resolveKeyboardActionInput(keyboardControls.action, client, sprite));
165
192
  const playPredictedWalkAnimation = () => {
166
- if (sprite.animationFixed) return;
167
- if (sprite.animationIsPlaying && sprite.animationIsPlaying()) return;
193
+ if (sprite.animationFixed) {
194
+ publishMobileDebug("walk:skip", { reason: "animationFixed" });
195
+ return;
196
+ }
197
+ if (sprite.animationIsPlaying && sprite.animationIsPlaying()) {
198
+ publishMobileDebug("walk:skip", { reason: "animationIsPlaying" });
199
+ return;
200
+ }
168
201
  realAnimationName.set("walk");
202
+ publishMobileDebug("walk:set", { reason: "predicted" });
169
203
  };
170
204
  const resumeHeldDirectionWalkAnimation = () => {
171
- if (!isCurrentPlayer()) return false;
172
- if (activeDirectionKeys.size === 0) return false;
173
- if (!canControls()) return false;
174
- if (sprite.animationFixed) return false;
175
- if (sprite.animationIsPlaying && sprite.animationIsPlaying()) return false;
205
+ if (!isCurrentPlayer()) {
206
+ publishMobileDebug("walk:resume-fail", { reason: "notCurrentPlayer" });
207
+ return false;
208
+ }
209
+ if (!hasHeldDirection()) {
210
+ publishMobileDebug("walk:resume-fail", { reason: "noHeldDirection" });
211
+ return false;
212
+ }
213
+ if (!canControls()) {
214
+ publishMobileDebug("walk:resume-fail", { reason: "cannotControl" });
215
+ return false;
216
+ }
217
+ if (sprite.animationFixed) {
218
+ publishMobileDebug("walk:resume-fail", { reason: "animationFixed" });
219
+ return false;
220
+ }
221
+ if (sprite.animationIsPlaying && sprite.animationIsPlaying()) {
222
+ publishMobileDebug("walk:resume-fail", { reason: "animationIsPlaying" });
223
+ return false;
224
+ }
176
225
  realAnimationName.set("walk");
226
+ publishMobileDebug("walk:set", { reason: "resumeHeldDirection" });
177
227
  return true;
178
228
  };
179
229
  const processMovementInput = (input) => {
180
230
  if (!canControls()) return;
231
+ publishMobileDebug("movement:input", { input });
181
232
  client.processInput({ input });
182
233
  playPredictedWalkAnimation();
183
234
  };
235
+ const activateControlDirection = (name, directionInput) => {
236
+ const releaseTimer = activeControlDirectionReleaseTimers.get(name);
237
+ if (releaseTimer) {
238
+ clearTimeout(releaseTimer);
239
+ activeControlDirectionReleaseTimers.delete(name);
240
+ }
241
+ activeControlDirections.delete(name);
242
+ activeControlDirections.set(name, directionInput);
243
+ publishMobileDebug("control:activate", {
244
+ name,
245
+ directionInput
246
+ });
247
+ resumeHeldDirectionWalkAnimation();
248
+ };
249
+ const releaseControlDirection = (name) => {
250
+ const previousTimer = activeControlDirectionReleaseTimers.get(name);
251
+ if (previousTimer) clearTimeout(previousTimer);
252
+ activeControlDirectionReleaseTimers.set(name, setTimeout(() => {
253
+ activeControlDirectionReleaseTimers.delete(name);
254
+ activeControlDirections.delete(name);
255
+ publishMobileDebug("control:release", { name });
256
+ if (!hasHeldDirection() && animationName() === "stand") {
257
+ realAnimationName.set("stand");
258
+ publishMobileDebug("stand:set", { reason: "controlRelease" });
259
+ }
260
+ }, CONTROL_DIRECTION_RELEASE_GRACE_MS));
261
+ };
184
262
  const processDashInput = () => {
185
263
  if (!canControls()) return;
186
264
  client.processDash({ direction: directionToDashVector(resolveSpriteDirection()) });
@@ -213,28 +291,44 @@ function component($$props) {
213
291
  repeat: true,
214
292
  bind: keyboardControls.down,
215
293
  keyDown() {
294
+ activateControlDirection("down", Direction.Down);
216
295
  processMovementInput(Direction.Down);
296
+ },
297
+ keyUp() {
298
+ releaseControlDirection("down");
217
299
  }
218
300
  },
219
301
  up: {
220
302
  repeat: true,
221
303
  bind: keyboardControls.up,
222
304
  keyDown() {
305
+ activateControlDirection("up", Direction.Up);
223
306
  processMovementInput(Direction.Up);
307
+ },
308
+ keyUp() {
309
+ releaseControlDirection("up");
224
310
  }
225
311
  },
226
312
  left: {
227
313
  repeat: true,
228
314
  bind: keyboardControls.left,
229
315
  keyDown() {
316
+ activateControlDirection("left", Direction.Left);
230
317
  processMovementInput(Direction.Left);
318
+ },
319
+ keyUp() {
320
+ releaseControlDirection("left");
231
321
  }
232
322
  },
233
323
  right: {
234
324
  repeat: true,
235
325
  bind: keyboardControls.right,
236
326
  keyDown() {
327
+ activateControlDirection("right", Direction.Right);
237
328
  processMovementInput(Direction.Right);
329
+ },
330
+ keyUp() {
331
+ releaseControlDirection("right");
238
332
  }
239
333
  },
240
334
  action: {
@@ -249,12 +343,33 @@ function component($$props) {
249
343
  processDashInput();
250
344
  }
251
345
  },
346
+ back: {
347
+ bind: keyboardControls.escape,
348
+ keyDown() {
349
+ if (canControls()) client.processAction({ action: "escape" });
350
+ }
351
+ },
252
352
  escape: {
253
353
  bind: keyboardControls.escape,
254
354
  keyDown() {
255
355
  if (canControls()) client.processAction({ action: "escape" });
256
356
  }
257
357
  },
358
+ joystick: {
359
+ enabled: true,
360
+ directionMapping: {
361
+ top: "up",
362
+ bottom: "down",
363
+ left: "left",
364
+ right: "right",
365
+ top_left: ["up", "left"],
366
+ top_right: ["up", "right"],
367
+ bottom_left: ["down", "left"],
368
+ bottom_right: ["down", "right"]
369
+ },
370
+ moveInterval: 50,
371
+ threshold: .1
372
+ },
258
373
  gamepad: { enabled: true }
259
374
  };
260
375
  const smoothX = animatedSignal(x(), { duration: isMe() ? 0 : 0 });
@@ -281,20 +396,36 @@ function component($$props) {
281
396
  };
282
397
  };
283
398
  const graphicScale = (graphicObject) => {
284
- const scale = graphicObject?.displayScale ?? graphicObject?.scale;
399
+ const scale = graphicObject?.scale;
400
+ if (Array.isArray(scale)) return scale;
401
+ if (typeof scale === "number") return [scale, scale];
402
+ if (scale && typeof scale === "object") {
403
+ const x = typeof scale.x === "number" ? scale.x : 1;
404
+ return [x, typeof scale.y === "number" ? scale.y : x];
405
+ }
406
+ };
407
+ const graphicContainerScale = (graphicObject) => {
408
+ const scale = graphicObject?.displayScale;
285
409
  if (Array.isArray(scale)) return scale;
286
410
  if (typeof scale === "number") return [scale, scale];
287
411
  if (scale && typeof scale === "object") {
288
412
  const x = typeof scale.x === "number" ? scale.x : 1;
289
413
  return [x, typeof scale.y === "number" ? scale.y : x];
290
414
  }
415
+ return [1, 1];
416
+ };
417
+ const multiplyScale = (left, right) => {
418
+ const a = normalizePair(left);
419
+ const b = normalizePair(right);
420
+ return [a[0] * b[0], a[1] * b[1]];
421
+ };
422
+ const graphicHitbox = (graphicObject) => {
423
+ return scaleHitboxForGraphicDisplay(hitbox(), graphicEffectiveScale(graphicObject));
291
424
  };
292
425
  const shadowCaster = (graphicObject) => {
293
426
  const box = hitbox();
294
427
  const bounds = graphicBounds();
295
- const scale = graphicScale(graphicObject);
296
- const scaleY = Array.isArray(scale) && typeof scale[1] === "number" ? Math.abs(scale[1]) : 1;
297
- const height = Math.max(bounds?.height ?? box?.h ?? 32, box?.h ?? 32) * scaleY;
428
+ const height = Math.max(bounds?.height ?? box?.h ?? 32, box?.h ?? 32);
298
429
  return {
299
430
  enabled: shadowsEnabled,
300
431
  height,
@@ -318,10 +449,6 @@ function component($$props) {
318
449
  };
319
450
  const imageDimensions = signal({});
320
451
  const loadingImageDimensions = /* @__PURE__ */ new Set();
321
- const toPositiveNumber = (value) => {
322
- const number = typeof value === "number" ? value : parseFloat(value);
323
- return Number.isFinite(number) && number > 0 ? number : void 0;
324
- };
325
452
  const toFiniteNumber = (value, fallback = 0) => {
326
453
  const number = typeof value === "number" ? value : parseFloat(value);
327
454
  return Number.isFinite(number) ? number : fallback;
@@ -394,6 +521,10 @@ function component($$props) {
394
521
  const optionValue = (prop, frame, textureOptions, graphicObject) => {
395
522
  return frame?.[prop] ?? textureOptions?.[prop] ?? graphicObject?.[prop];
396
523
  };
524
+ const graphicEffectiveScale = (graphicObject) => {
525
+ const textureOptions = resolveTextureOptions(graphicObject);
526
+ return multiplyScale(normalizePair(optionValue("scale", resolveFirstAnimationFrame(textureOptions), textureOptions, graphicObject) ?? graphicScale(graphicObject)), graphicContainerScale(graphicObject));
527
+ };
397
528
  const resolveFrameSize = (textureOptions, dimensions) => {
398
529
  const framesWidth = toPositiveNumber(textureOptions?.framesWidth) ?? 1;
399
530
  const framesHeight = toPositiveNumber(textureOptions?.framesHeight) ?? 1;
@@ -406,21 +537,6 @@ function component($$props) {
406
537
  height: toPositiveNumber(textureOptions?.rectHeight) ?? toPositiveNumber(textureOptions?.spriteHeight) ?? (fullHeight ? fullHeight / framesHeight : void 0)
407
538
  };
408
539
  };
409
- const resolveHitboxAnchor = (spriteWidth, spriteHeight, realSize, box) => {
410
- if (!spriteWidth || !spriteHeight || !box) return [0, 0];
411
- const resolvedHeight = toPositiveNumber(typeof realSize === "number" ? realSize : realSize?.height) ?? spriteHeight;
412
- const gap = Math.max(0, (spriteHeight - resolvedHeight) / 2);
413
- const hitboxTopLeftX = clampRatio((spriteWidth - box.w) / 2 / spriteWidth);
414
- const hitboxTopLeftY = clampRatio((spriteHeight - box.h - gap) / spriteHeight);
415
- const hitboxCenterX = clampRatio(hitboxTopLeftX + box.w / 2 / spriteWidth);
416
- const hitboxCenterY = clampRatio(hitboxTopLeftY + box.h / 2 / spriteHeight);
417
- const footY = clampRatio((spriteHeight - gap) / spriteHeight);
418
- switch (box.anchorMode ?? "top-left") {
419
- case "center": return [hitboxCenterX, hitboxCenterY];
420
- case "foot": return [hitboxCenterX, footY];
421
- default: return [hitboxTopLeftX, hitboxTopLeftY];
422
- }
423
- };
424
540
  const loadImageDimensions = (image) => {
425
541
  const source = resolveImageSource(image);
426
542
  if (!source || imageDimensions()[source] || loadingImageDimensions.has(source)) return;
@@ -482,8 +598,8 @@ function component($$props) {
482
598
  const spriteWidth = size.width ?? box?.w;
483
599
  const spriteHeight = size.height ?? box?.h;
484
600
  if (!spriteWidth || !spriteHeight) return;
485
- const anchor = normalizeAnchor(optionValue("anchor", frame, textureOptions, graphicObject)) ?? resolveHitboxAnchor(spriteWidth, spriteHeight, optionValue("spriteRealSize", frame, textureOptions, graphicObject), box);
486
- const scale = normalizePair(optionValue("scale", frame, textureOptions, graphicObject) ?? graphicScale(graphicObject));
601
+ const scale = graphicEffectiveScale(graphicObject);
602
+ const anchor = normalizeAnchor(optionValue("anchor", frame, textureOptions, graphicObject)) ?? resolveScaledHitboxAnchor(spriteWidth, spriteHeight, optionValue("spriteRealSize", frame, textureOptions, graphicObject), box, scale);
487
603
  const x = toFiniteNumber(optionValue("x", frame, textureOptions, graphicObject), 0);
488
604
  const y = toFiniteNumber(optionValue("y", frame, textureOptions, graphicObject), 0);
489
605
  const leftEdge = -anchor[0] * spriteWidth * scale[0];
@@ -628,12 +744,15 @@ function component($$props) {
628
744
  removeTransitionSubscription?.unsubscribe();
629
745
  animationMovementSubscription.unsubscribe();
630
746
  resumeWalkSubscriptions.forEach((subscription) => subscription.unsubscribe());
747
+ activeControlDirectionReleaseTimers.forEach((timer) => clearTimeout(timer));
748
+ activeControlDirectionReleaseTimers.clear();
631
749
  xSubscription.unsubscribe();
632
750
  ySubscription.unsubscribe();
633
751
  await lastValueFrom(hooks.callHooks("client-sprite-onDestroy", sprite));
634
752
  await lastValueFrom(hooks.callHooks("client-sceneMap-onRemoveSprite", client.sceneMap, sprite));
635
753
  };
636
754
  mount((element) => {
755
+ let appliedCameraFollowRevision = null;
637
756
  if (typeof document !== "undefined") {
638
757
  document.addEventListener("keydown", handleNativeActionWhileMoving);
639
758
  document.addEventListener("keyup", handleNativeActionWhileMoving);
@@ -643,6 +762,31 @@ function component($$props) {
643
762
  effect(() => {
644
763
  if (isCurrentPlayer()) client.setKeyboardControls(element.directives.controls);
645
764
  });
765
+ effect(() => {
766
+ const followRevision = client.cameraFollowRevision();
767
+ if (!shouldFollowCamera()) {
768
+ appliedCameraFollowRevision = null;
769
+ return;
770
+ }
771
+ const smoothMove = client.cameraFollowSmoothMove;
772
+ const viewport = element.props.context?.viewport;
773
+ const target = element.componentInstance;
774
+ if (!viewport || !target) {
775
+ appliedCameraFollowRevision = null;
776
+ return;
777
+ }
778
+ appliedCameraFollowRevision = applyCameraFollow({
779
+ viewport,
780
+ target,
781
+ smoothMove,
782
+ followRevision,
783
+ isCurrentRevision: (revision) => client.cameraFollowRevision() === revision,
784
+ shouldFollowCamera
785
+ }) ? followRevision : null;
786
+ });
787
+ return () => {
788
+ if (ownsCameraFollowRevision(appliedCameraFollowRevision, client.cameraFollowRevision())) clearCameraFollowPlugins(element.props.context?.viewport);
789
+ };
646
790
  });
647
791
  const normalizeOpenId = (value) => {
648
792
  const resolved = typeof value === "function" ? value() : value;
@@ -664,7 +808,6 @@ function component($$props) {
664
808
  x: smoothX,
665
809
  y: smoothY,
666
810
  zIndex: z,
667
- viewportFollow: shouldFollowCamera,
668
811
  controls,
669
812
  onBeforeDestroy,
670
813
  visible,
@@ -696,11 +839,11 @@ function component($$props) {
696
839
  zIndex: 1e3,
697
840
  name: particleName
698
841
  }),
699
- h(Container, null, [loop(renderedGraphics, (graphicObj) => h(Container, { scale: computed(() => graphicScale(graphicObj)) }, h(Sprite, {
842
+ h(Container, null, [loop(renderedGraphics, (graphicObj) => h(Container, { scale: computed(() => graphicContainerScale(graphicObj)) }, h(Sprite, {
700
843
  sheet: computed(() => sheet(graphicObj)),
701
844
  direction,
702
845
  tint,
703
- hitbox,
846
+ hitbox: computed(() => graphicHitbox(graphicObj)),
704
847
  shadowCaster: computed(() => shadowCaster(graphicObj)),
705
848
  flash: flashConfig
706
849
  }))), loop(resolvedEventComponents, (eventComponent) => h(Container, { dependencies: eventComponent.dependencies }, h(eventComponent.component, eventComponent.props)))]),