@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.
- package/CHANGELOG.md +16 -0
- package/dist/Game/Object.d.ts +1 -0
- package/dist/Game/Object.js +10 -0
- package/dist/Game/Object.js.map +1 -1
- package/dist/Gui/Gui.js +37 -4
- package/dist/Gui/Gui.js.map +1 -1
- package/dist/RpgClientEngine.d.ts +18 -9
- package/dist/RpgClientEngine.js +40 -13
- package/dist/RpgClientEngine.js.map +1 -1
- package/dist/components/character-hitbox.d.ts +13 -0
- package/dist/components/character-hitbox.js +38 -0
- package/dist/components/character-hitbox.js.map +1 -0
- package/dist/components/character-hitbox.spec.d.ts +1 -0
- package/dist/components/character.ce.js +179 -36
- package/dist/components/character.ce.js.map +1 -1
- package/dist/components/gui/mobile/index.d.ts +51 -2
- package/dist/components/gui/mobile/index.js +12 -4
- package/dist/components/gui/mobile/index.js.map +1 -1
- package/dist/components/gui/mobile/index.spec.d.ts +1 -0
- package/dist/components/gui/mobile/mobile.ce.js +303 -55
- package/dist/components/gui/mobile/mobile.ce.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/services/cameraFollow.d.ts +51 -0
- package/dist/services/cameraFollow.js +134 -0
- package/dist/services/cameraFollow.js.map +1 -0
- package/dist/services/cameraFollow.spec.d.ts +1 -0
- package/dist/services/standalone.d.ts +6 -1
- package/dist/services/standalone.js +35 -17
- package/dist/services/standalone.js.map +1 -1
- package/dist/utils/syncHitbox.d.ts +1 -0
- package/dist/utils/syncHitbox.js +69 -0
- package/dist/utils/syncHitbox.js.map +1 -0
- package/dist/utils/syncHitbox.spec.d.ts +1 -0
- package/package.json +5 -5
- package/src/Game/Object.spec.ts +44 -4
- package/src/Game/Object.ts +19 -0
- package/src/Gui/Gui.spec.ts +19 -0
- package/src/Gui/Gui.ts +50 -11
- package/src/RpgClientEngine.ts +56 -24
- package/src/components/character-hitbox.spec.ts +33 -0
- package/src/components/character-hitbox.ts +72 -0
- package/src/components/character.ce +207 -48
- package/src/components/gui/mobile/index.spec.ts +94 -0
- package/src/components/gui/mobile/index.ts +74 -6
- package/src/components/gui/mobile/mobile.ce +347 -65
- package/src/index.ts +12 -0
- package/src/services/cameraFollow.spec.ts +220 -0
- package/src/services/cameraFollow.ts +222 -0
- package/src/services/standalone.spec.ts +47 -0
- package/src/services/standalone.ts +53 -20
- package/src/utils/syncHitbox.spec.ts +79 -0
- package/src/utils/syncHitbox.ts +104 -0
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
x={smoothX}
|
|
3
3
|
y={smoothY}
|
|
4
4
|
zIndex={z}
|
|
5
|
-
viewportFollow={shouldFollowCamera}
|
|
6
5
|
controls
|
|
7
6
|
onBeforeDestroy
|
|
8
7
|
visible
|
|
@@ -24,12 +23,12 @@
|
|
|
24
23
|
<Particle emit={emitParticleTrigger} settings={particleSettings} zIndex={1000} name={particleName} />
|
|
25
24
|
<Container>
|
|
26
25
|
@for (graphicObj of renderedGraphics) {
|
|
27
|
-
<Container scale={
|
|
26
|
+
<Container scale={graphicContainerScale(graphicObj)}>
|
|
28
27
|
<Sprite
|
|
29
28
|
sheet={sheet(graphicObj)}
|
|
30
29
|
direction
|
|
31
30
|
tint
|
|
32
|
-
hitbox
|
|
31
|
+
hitbox={graphicHitbox(graphicObj)}
|
|
33
32
|
shadowCaster={shadowCaster(graphicObj)}
|
|
34
33
|
flash={flashConfig}
|
|
35
34
|
/>
|
|
@@ -76,6 +75,8 @@
|
|
|
76
75
|
import { Particle } from "@canvasengine/presets";
|
|
77
76
|
import { GameEngineToken, ModulesToken, shouldRenderLightingShadows } from "@rpgjs/common";
|
|
78
77
|
import { RpgClientEngine } from "../RpgClientEngine";
|
|
78
|
+
import { applyCameraFollow, clearCameraFollowPlugins, ownsCameraFollowRevision } from "../services/cameraFollow";
|
|
79
|
+
import { resolveScaledHitboxAnchor, scaleHitboxForGraphicDisplay, toPositiveNumber } from "./character-hitbox";
|
|
79
80
|
import { inject } from "../core/inject";
|
|
80
81
|
import { Direction, Animation } from "@rpgjs/common";
|
|
81
82
|
import { normalizeEventComponent } from "../Game/EventComponentResolver";
|
|
@@ -360,9 +361,43 @@
|
|
|
360
361
|
const canControls = () => isMe() && getCanMoveValue(sprite)
|
|
361
362
|
const keyboardControls = client.globalConfig.keyboardControls;
|
|
362
363
|
const activeDirectionKeys = new Map();
|
|
364
|
+
const activeControlDirections = new Map();
|
|
365
|
+
const activeControlDirectionReleaseTimers = new Map();
|
|
366
|
+
const CONTROL_DIRECTION_RELEASE_GRACE_MS = 160;
|
|
367
|
+
|
|
368
|
+
const hasHeldDirection = () =>
|
|
369
|
+
activeDirectionKeys.size > 0 || activeControlDirections.size > 0;
|
|
370
|
+
|
|
371
|
+
const publishMobileDebug = (type, payload = {}) => {
|
|
372
|
+
const target = typeof window !== "undefined" ? window : globalThis;
|
|
373
|
+
if (!target.__RPGJS_MOBILE_DEBUG__) return;
|
|
374
|
+
const event = {
|
|
375
|
+
source: "character",
|
|
376
|
+
type,
|
|
377
|
+
time: Date.now(),
|
|
378
|
+
isCurrentPlayer: isCurrentPlayer(),
|
|
379
|
+
animationName: animationName(),
|
|
380
|
+
realAnimationName: realAnimationName?.(),
|
|
381
|
+
activeKeyboardDirections: Array.from(activeDirectionKeys.values()),
|
|
382
|
+
activeControlDirections: Array.from(activeControlDirections.values()),
|
|
383
|
+
heldDirection: resolveHeldDirection(),
|
|
384
|
+
canControls: canControls(),
|
|
385
|
+
x: x(),
|
|
386
|
+
y: y(),
|
|
387
|
+
...payload
|
|
388
|
+
};
|
|
389
|
+
target.__RPGJS_MOBILE_DEBUG_EVENTS__ = [
|
|
390
|
+
...(target.__RPGJS_MOBILE_DEBUG_EVENTS__ || []).slice(-199),
|
|
391
|
+
event
|
|
392
|
+
];
|
|
393
|
+
target.__RPGJS_MOBILE_DEBUG_LAST__ = event;
|
|
394
|
+
};
|
|
363
395
|
|
|
364
396
|
const resolveHeldDirection = () => {
|
|
365
|
-
const directions =
|
|
397
|
+
const directions = [
|
|
398
|
+
...Array.from(activeDirectionKeys.values()),
|
|
399
|
+
...Array.from(activeControlDirections.values()),
|
|
400
|
+
];
|
|
366
401
|
return directions[directions.length - 1];
|
|
367
402
|
};
|
|
368
403
|
|
|
@@ -408,27 +443,77 @@
|
|
|
408
443
|
);
|
|
409
444
|
|
|
410
445
|
const playPredictedWalkAnimation = () => {
|
|
411
|
-
if (sprite.animationFixed)
|
|
412
|
-
|
|
446
|
+
if (sprite.animationFixed) {
|
|
447
|
+
publishMobileDebug("walk:skip", { reason: "animationFixed" });
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (sprite.animationIsPlaying && sprite.animationIsPlaying()) {
|
|
451
|
+
publishMobileDebug("walk:skip", { reason: "animationIsPlaying" });
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
413
454
|
realAnimationName.set('walk');
|
|
455
|
+
publishMobileDebug("walk:set", { reason: "predicted" });
|
|
414
456
|
};
|
|
415
457
|
|
|
416
458
|
const resumeHeldDirectionWalkAnimation = () => {
|
|
417
|
-
if (!isCurrentPlayer())
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
if (
|
|
459
|
+
if (!isCurrentPlayer()) {
|
|
460
|
+
publishMobileDebug("walk:resume-fail", { reason: "notCurrentPlayer" });
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
if (!hasHeldDirection()) {
|
|
464
|
+
publishMobileDebug("walk:resume-fail", { reason: "noHeldDirection" });
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
if (!canControls()) {
|
|
468
|
+
publishMobileDebug("walk:resume-fail", { reason: "cannotControl" });
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
if (sprite.animationFixed) {
|
|
472
|
+
publishMobileDebug("walk:resume-fail", { reason: "animationFixed" });
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
if (sprite.animationIsPlaying && sprite.animationIsPlaying()) {
|
|
476
|
+
publishMobileDebug("walk:resume-fail", { reason: "animationIsPlaying" });
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
422
479
|
realAnimationName.set('walk');
|
|
480
|
+
publishMobileDebug("walk:set", { reason: "resumeHeldDirection" });
|
|
423
481
|
return true;
|
|
424
482
|
};
|
|
425
483
|
|
|
426
484
|
const processMovementInput = (input) => {
|
|
427
485
|
if (!canControls()) return;
|
|
486
|
+
publishMobileDebug("movement:input", { input });
|
|
428
487
|
client.processInput({ input });
|
|
429
488
|
playPredictedWalkAnimation();
|
|
430
489
|
};
|
|
431
490
|
|
|
491
|
+
const activateControlDirection = (name, directionInput) => {
|
|
492
|
+
const releaseTimer = activeControlDirectionReleaseTimers.get(name);
|
|
493
|
+
if (releaseTimer) {
|
|
494
|
+
clearTimeout(releaseTimer);
|
|
495
|
+
activeControlDirectionReleaseTimers.delete(name);
|
|
496
|
+
}
|
|
497
|
+
activeControlDirections.delete(name);
|
|
498
|
+
activeControlDirections.set(name, directionInput);
|
|
499
|
+
publishMobileDebug("control:activate", { name, directionInput });
|
|
500
|
+
resumeHeldDirectionWalkAnimation();
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
const releaseControlDirection = (name) => {
|
|
504
|
+
const previousTimer = activeControlDirectionReleaseTimers.get(name);
|
|
505
|
+
if (previousTimer) clearTimeout(previousTimer);
|
|
506
|
+
activeControlDirectionReleaseTimers.set(name, setTimeout(() => {
|
|
507
|
+
activeControlDirectionReleaseTimers.delete(name);
|
|
508
|
+
activeControlDirections.delete(name);
|
|
509
|
+
publishMobileDebug("control:release", { name });
|
|
510
|
+
if (!hasHeldDirection() && animationName() === 'stand') {
|
|
511
|
+
realAnimationName.set('stand');
|
|
512
|
+
publishMobileDebug("stand:set", { reason: "controlRelease" });
|
|
513
|
+
}
|
|
514
|
+
}, CONTROL_DIRECTION_RELEASE_GRACE_MS));
|
|
515
|
+
};
|
|
516
|
+
|
|
432
517
|
const processDashInput = () => {
|
|
433
518
|
if (!canControls()) return;
|
|
434
519
|
client.processDash({
|
|
@@ -474,29 +559,45 @@
|
|
|
474
559
|
repeat: true,
|
|
475
560
|
bind: keyboardControls.down,
|
|
476
561
|
keyDown() {
|
|
562
|
+
activateControlDirection('down', Direction.Down)
|
|
477
563
|
processMovementInput(Direction.Down)
|
|
478
564
|
},
|
|
565
|
+
keyUp() {
|
|
566
|
+
releaseControlDirection('down')
|
|
567
|
+
},
|
|
479
568
|
},
|
|
480
569
|
up: {
|
|
481
570
|
repeat: true,
|
|
482
571
|
bind: keyboardControls.up,
|
|
483
572
|
keyDown() {
|
|
573
|
+
activateControlDirection('up', Direction.Up)
|
|
484
574
|
processMovementInput(Direction.Up)
|
|
485
575
|
},
|
|
576
|
+
keyUp() {
|
|
577
|
+
releaseControlDirection('up')
|
|
578
|
+
},
|
|
486
579
|
},
|
|
487
580
|
left: {
|
|
488
581
|
repeat: true,
|
|
489
582
|
bind: keyboardControls.left,
|
|
490
583
|
keyDown() {
|
|
584
|
+
activateControlDirection('left', Direction.Left)
|
|
491
585
|
processMovementInput(Direction.Left)
|
|
492
586
|
},
|
|
587
|
+
keyUp() {
|
|
588
|
+
releaseControlDirection('left')
|
|
589
|
+
},
|
|
493
590
|
},
|
|
494
591
|
right: {
|
|
495
592
|
repeat: true,
|
|
496
593
|
bind: keyboardControls.right,
|
|
497
594
|
keyDown() {
|
|
595
|
+
activateControlDirection('right', Direction.Right)
|
|
498
596
|
processMovementInput(Direction.Right)
|
|
499
597
|
},
|
|
598
|
+
keyUp() {
|
|
599
|
+
releaseControlDirection('right')
|
|
600
|
+
},
|
|
500
601
|
},
|
|
501
602
|
action: {
|
|
502
603
|
bind: getKeyboardControlBind(keyboardControls.action),
|
|
@@ -512,6 +613,14 @@
|
|
|
512
613
|
processDashInput()
|
|
513
614
|
},
|
|
514
615
|
},
|
|
616
|
+
back: {
|
|
617
|
+
bind: keyboardControls.escape,
|
|
618
|
+
keyDown() {
|
|
619
|
+
if (canControls()) {
|
|
620
|
+
client.processAction({ action: 'escape' })
|
|
621
|
+
}
|
|
622
|
+
},
|
|
623
|
+
},
|
|
515
624
|
escape: {
|
|
516
625
|
bind: keyboardControls.escape,
|
|
517
626
|
keyDown() {
|
|
@@ -520,6 +629,21 @@
|
|
|
520
629
|
}
|
|
521
630
|
},
|
|
522
631
|
},
|
|
632
|
+
joystick: {
|
|
633
|
+
enabled: true,
|
|
634
|
+
directionMapping: {
|
|
635
|
+
top: 'up',
|
|
636
|
+
bottom: 'down',
|
|
637
|
+
left: 'left',
|
|
638
|
+
right: 'right',
|
|
639
|
+
top_left: ['up', 'left'],
|
|
640
|
+
top_right: ['up', 'right'],
|
|
641
|
+
bottom_left: ['down', 'left'],
|
|
642
|
+
bottom_right: ['down', 'right']
|
|
643
|
+
},
|
|
644
|
+
moveInterval: 50,
|
|
645
|
+
threshold: 0.1
|
|
646
|
+
},
|
|
523
647
|
gamepad: {
|
|
524
648
|
enabled: true
|
|
525
649
|
}
|
|
@@ -562,7 +686,7 @@
|
|
|
562
686
|
}
|
|
563
687
|
|
|
564
688
|
const graphicScale = (graphicObject) => {
|
|
565
|
-
const scale = graphicObject?.
|
|
689
|
+
const scale = graphicObject?.scale;
|
|
566
690
|
if (Array.isArray(scale)) return scale;
|
|
567
691
|
if (typeof scale === 'number') return [scale, scale];
|
|
568
692
|
if (scale && typeof scale === 'object') {
|
|
@@ -573,12 +697,34 @@
|
|
|
573
697
|
return undefined;
|
|
574
698
|
}
|
|
575
699
|
|
|
700
|
+
const graphicContainerScale = (graphicObject) => {
|
|
701
|
+
const scale = graphicObject?.displayScale;
|
|
702
|
+
if (Array.isArray(scale)) return scale;
|
|
703
|
+
if (typeof scale === 'number') return [scale, scale];
|
|
704
|
+
if (scale && typeof scale === 'object') {
|
|
705
|
+
const x = typeof scale.x === 'number' ? scale.x : 1;
|
|
706
|
+
const y = typeof scale.y === 'number' ? scale.y : x;
|
|
707
|
+
return [x, y];
|
|
708
|
+
}
|
|
709
|
+
return [1, 1];
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const multiplyScale = (left, right) => {
|
|
713
|
+
const a = normalizePair(left);
|
|
714
|
+
const b = normalizePair(right);
|
|
715
|
+
return [a[0] * b[0], a[1] * b[1]];
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const graphicHitbox = (graphicObject) => {
|
|
719
|
+
const box = hitbox();
|
|
720
|
+
const scale = graphicEffectiveScale(graphicObject);
|
|
721
|
+
return scaleHitboxForGraphicDisplay(box, scale);
|
|
722
|
+
}
|
|
723
|
+
|
|
576
724
|
const shadowCaster = (graphicObject) => {
|
|
577
725
|
const box = hitbox();
|
|
578
726
|
const bounds = graphicBounds();
|
|
579
|
-
const
|
|
580
|
-
const scaleY = Array.isArray(scale) && typeof scale[1] === 'number' ? Math.abs(scale[1]) : 1;
|
|
581
|
-
const height = Math.max(bounds?.height ?? box?.h ?? 32, box?.h ?? 32) * scaleY;
|
|
727
|
+
const height = Math.max(bounds?.height ?? box?.h ?? 32, box?.h ?? 32);
|
|
582
728
|
|
|
583
729
|
return {
|
|
584
730
|
enabled: shadowsEnabled,
|
|
@@ -599,11 +745,6 @@
|
|
|
599
745
|
const imageDimensions = signal({});
|
|
600
746
|
const loadingImageDimensions = new Set();
|
|
601
747
|
|
|
602
|
-
const toPositiveNumber = (value) => {
|
|
603
|
-
const number = typeof value === 'number' ? value : parseFloat(value);
|
|
604
|
-
return Number.isFinite(number) && number > 0 ? number : undefined;
|
|
605
|
-
};
|
|
606
|
-
|
|
607
748
|
const toFiniteNumber = (value, fallback = 0) => {
|
|
608
749
|
const number = typeof value === 'number' ? value : parseFloat(value);
|
|
609
750
|
return Number.isFinite(number) ? number : fallback;
|
|
@@ -703,6 +844,13 @@
|
|
|
703
844
|
return frame?.[prop] ?? textureOptions?.[prop] ?? graphicObject?.[prop];
|
|
704
845
|
};
|
|
705
846
|
|
|
847
|
+
const graphicEffectiveScale = (graphicObject) => {
|
|
848
|
+
const textureOptions = resolveTextureOptions(graphicObject);
|
|
849
|
+
const frame = resolveFirstAnimationFrame(textureOptions);
|
|
850
|
+
const spriteScale = normalizePair(optionValue('scale', frame, textureOptions, graphicObject) ?? graphicScale(graphicObject));
|
|
851
|
+
return multiplyScale(spriteScale, graphicContainerScale(graphicObject));
|
|
852
|
+
};
|
|
853
|
+
|
|
706
854
|
const resolveFrameSize = (textureOptions, dimensions) => {
|
|
707
855
|
const framesWidth = toPositiveNumber(textureOptions?.framesWidth) ?? 1;
|
|
708
856
|
const framesHeight = toPositiveNumber(textureOptions?.framesHeight) ?? 1;
|
|
@@ -723,31 +871,6 @@
|
|
|
723
871
|
};
|
|
724
872
|
};
|
|
725
873
|
|
|
726
|
-
const resolveHitboxAnchor = (spriteWidth, spriteHeight, realSize, box) => {
|
|
727
|
-
if (!spriteWidth || !spriteHeight || !box) {
|
|
728
|
-
return [0, 0];
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
const heightOfSprite = typeof realSize === 'number' ? realSize : realSize?.height;
|
|
732
|
-
const resolvedHeight = toPositiveNumber(heightOfSprite) ?? spriteHeight;
|
|
733
|
-
const gap = Math.max(0, (spriteHeight - resolvedHeight) / 2);
|
|
734
|
-
const hitboxTopLeftX = clampRatio((spriteWidth - box.w) / 2 / spriteWidth);
|
|
735
|
-
const hitboxTopLeftY = clampRatio((spriteHeight - box.h - gap) / spriteHeight);
|
|
736
|
-
const hitboxCenterX = clampRatio(hitboxTopLeftX + box.w / 2 / spriteWidth);
|
|
737
|
-
const hitboxCenterY = clampRatio(hitboxTopLeftY + box.h / 2 / spriteHeight);
|
|
738
|
-
const footY = clampRatio((spriteHeight - gap) / spriteHeight);
|
|
739
|
-
|
|
740
|
-
switch (box.anchorMode ?? 'top-left') {
|
|
741
|
-
case 'center':
|
|
742
|
-
return [hitboxCenterX, hitboxCenterY];
|
|
743
|
-
case 'foot':
|
|
744
|
-
return [hitboxCenterX, footY];
|
|
745
|
-
case 'top-left':
|
|
746
|
-
default:
|
|
747
|
-
return [hitboxTopLeftX, hitboxTopLeftY];
|
|
748
|
-
}
|
|
749
|
-
};
|
|
750
|
-
|
|
751
874
|
const loadImageDimensions = (image) => {
|
|
752
875
|
const source = resolveImageSource(image);
|
|
753
876
|
if (!source || imageDimensions()[source] || loadingImageDimensions.has(source)) {
|
|
@@ -827,14 +950,15 @@
|
|
|
827
950
|
return;
|
|
828
951
|
}
|
|
829
952
|
|
|
953
|
+
const scale = graphicEffectiveScale(graphicObject);
|
|
830
954
|
const explicitAnchor = normalizeAnchor(optionValue('anchor', frame, textureOptions, graphicObject));
|
|
831
|
-
const anchor = explicitAnchor ??
|
|
955
|
+
const anchor = explicitAnchor ?? resolveScaledHitboxAnchor(
|
|
832
956
|
spriteWidth,
|
|
833
957
|
spriteHeight,
|
|
834
958
|
optionValue('spriteRealSize', frame, textureOptions, graphicObject),
|
|
835
|
-
box
|
|
959
|
+
box,
|
|
960
|
+
scale
|
|
836
961
|
);
|
|
837
|
-
const scale = normalizePair(optionValue('scale', frame, textureOptions, graphicObject) ?? graphicScale(graphicObject));
|
|
838
962
|
const x = toFiniteNumber(optionValue('x', frame, textureOptions, graphicObject), 0);
|
|
839
963
|
const y = toFiniteNumber(optionValue('y', frame, textureOptions, graphicObject), 0);
|
|
840
964
|
const leftEdge = -anchor[0] * spriteWidth * scale[0];
|
|
@@ -1051,6 +1175,8 @@
|
|
|
1051
1175
|
removeTransitionSubscription?.unsubscribe();
|
|
1052
1176
|
animationMovementSubscription.unsubscribe();
|
|
1053
1177
|
resumeWalkSubscriptions.forEach(subscription => subscription.unsubscribe());
|
|
1178
|
+
activeControlDirectionReleaseTimers.forEach(timer => clearTimeout(timer));
|
|
1179
|
+
activeControlDirectionReleaseTimers.clear();
|
|
1054
1180
|
xSubscription.unsubscribe();
|
|
1055
1181
|
ySubscription.unsubscribe();
|
|
1056
1182
|
await lastValueFrom(hooks.callHooks("client-sprite-onDestroy", sprite))
|
|
@@ -1058,6 +1184,7 @@
|
|
|
1058
1184
|
}
|
|
1059
1185
|
|
|
1060
1186
|
mount((element) => {
|
|
1187
|
+
let appliedCameraFollowRevision = null;
|
|
1061
1188
|
if (typeof document !== 'undefined') {
|
|
1062
1189
|
document.addEventListener('keydown', handleNativeActionWhileMoving);
|
|
1063
1190
|
document.addEventListener('keyup', handleNativeActionWhileMoving);
|
|
@@ -1069,6 +1196,38 @@
|
|
|
1069
1196
|
client.setKeyboardControls(element.directives.controls)
|
|
1070
1197
|
}
|
|
1071
1198
|
})
|
|
1199
|
+
|
|
1200
|
+
effect(() => {
|
|
1201
|
+
const followRevision = client.cameraFollowRevision();
|
|
1202
|
+
if (!shouldFollowCamera()) {
|
|
1203
|
+
appliedCameraFollowRevision = null;
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
const smoothMove = client.cameraFollowSmoothMove;
|
|
1208
|
+
const viewport = element.props.context?.viewport;
|
|
1209
|
+
const target = element.componentInstance;
|
|
1210
|
+
if (!viewport || !target) {
|
|
1211
|
+
appliedCameraFollowRevision = null;
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
const appliedCameraFollow = applyCameraFollow({
|
|
1216
|
+
viewport,
|
|
1217
|
+
target,
|
|
1218
|
+
smoothMove,
|
|
1219
|
+
followRevision,
|
|
1220
|
+
isCurrentRevision: (revision) => client.cameraFollowRevision() === revision,
|
|
1221
|
+
shouldFollowCamera
|
|
1222
|
+
});
|
|
1223
|
+
appliedCameraFollowRevision = appliedCameraFollow ? followRevision : null;
|
|
1224
|
+
})
|
|
1225
|
+
|
|
1226
|
+
return () => {
|
|
1227
|
+
if (ownsCameraFollowRevision(appliedCameraFollowRevision, client.cameraFollowRevision())) {
|
|
1228
|
+
clearCameraFollowPlugins(element.props.context?.viewport);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1072
1231
|
})
|
|
1073
1232
|
|
|
1074
1233
|
/**
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from "vitest";
|
|
2
|
+
import { signal } from "canvasengine";
|
|
3
|
+
|
|
4
|
+
const fakeEngine = {
|
|
5
|
+
controlsReady: signal(true),
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
vi.mock("../../../core/inject", () => ({
|
|
9
|
+
inject: vi.fn(() => fakeEngine),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
describe("withMobile", () => {
|
|
13
|
+
test("registers the mobile GUI with default options", async () => {
|
|
14
|
+
const { withMobile } = await import("./index");
|
|
15
|
+
|
|
16
|
+
const module = withMobile();
|
|
17
|
+
const gui = module.gui[0];
|
|
18
|
+
|
|
19
|
+
expect(gui.id).toBe("mobile-gui");
|
|
20
|
+
expect(gui.autoDisplay).toBe(true);
|
|
21
|
+
expect(gui.data).toEqual({});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("passes options to the GUI and resolves enabled dependencies", async () => {
|
|
25
|
+
const { withMobile } = await import("./index");
|
|
26
|
+
const CustomJoystick = vi.fn();
|
|
27
|
+
const CustomActionButton = vi.fn();
|
|
28
|
+
|
|
29
|
+
const module = withMobile({
|
|
30
|
+
id: "touch-controls",
|
|
31
|
+
enabled: "always",
|
|
32
|
+
layout: {
|
|
33
|
+
joystickSide: "right",
|
|
34
|
+
gap: 18,
|
|
35
|
+
buttonsMargin: 44,
|
|
36
|
+
joystickMargin: [24, 60, 24, 24],
|
|
37
|
+
},
|
|
38
|
+
components: {
|
|
39
|
+
joystick: CustomJoystick,
|
|
40
|
+
buttons: {
|
|
41
|
+
action: CustomActionButton,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
joystick: {
|
|
45
|
+
outerColor: "#111111",
|
|
46
|
+
innerColor: "#eeeeee",
|
|
47
|
+
scale: 0.8,
|
|
48
|
+
outerScale: { x: 0.8, y: 0.8 },
|
|
49
|
+
innerScale: { x: 0.9, y: 0.9 },
|
|
50
|
+
moveInterval: 40,
|
|
51
|
+
threshold: 0.2,
|
|
52
|
+
},
|
|
53
|
+
buttons: {
|
|
54
|
+
action: {
|
|
55
|
+
enabled: true,
|
|
56
|
+
width: 72,
|
|
57
|
+
},
|
|
58
|
+
back: false,
|
|
59
|
+
dash: true,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
const gui = module.gui[0];
|
|
63
|
+
const dependencies = gui.dependencies();
|
|
64
|
+
|
|
65
|
+
expect(gui.id).toBe("touch-controls");
|
|
66
|
+
expect(gui.data.layout?.joystickSide).toBe("right");
|
|
67
|
+
expect(gui.data.components?.joystick).toBe(CustomJoystick);
|
|
68
|
+
expect(gui.data.components?.buttons?.action).toBe(CustomActionButton);
|
|
69
|
+
expect(gui.data.buttons?.action).toMatchObject({
|
|
70
|
+
enabled: true,
|
|
71
|
+
width: 72,
|
|
72
|
+
});
|
|
73
|
+
expect(gui.data.buttons?.dash).toBe(true);
|
|
74
|
+
expect(gui.data.joystick).toMatchObject({
|
|
75
|
+
outerColor: "#111111",
|
|
76
|
+
innerColor: "#eeeeee",
|
|
77
|
+
scale: 0.8,
|
|
78
|
+
outerScale: { x: 0.8, y: 0.8 },
|
|
79
|
+
innerScale: { x: 0.9, y: 0.9 },
|
|
80
|
+
moveInterval: 40,
|
|
81
|
+
threshold: 0.2,
|
|
82
|
+
});
|
|
83
|
+
expect(dependencies[0]()).toBe(true);
|
|
84
|
+
expect(dependencies[1]()).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("keeps the auto detector unresolved when mobile is disabled", async () => {
|
|
88
|
+
const { withMobile } = await import("./index");
|
|
89
|
+
|
|
90
|
+
const dependencies = withMobile({ enabled: "never" }).gui[0].dependencies();
|
|
91
|
+
|
|
92
|
+
expect(dependencies[0]()).toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -1,24 +1,92 @@
|
|
|
1
1
|
import { inject } from "../../../core/inject";
|
|
2
2
|
import { RpgClientEngine } from "../../../RpgClientEngine";
|
|
3
3
|
import MobileGui from "./mobile.ce";
|
|
4
|
-
import {
|
|
4
|
+
import { computed } from "canvasengine";
|
|
5
|
+
import type { ButtonProps, ComponentFunction, JoystickSettings } from "canvasengine";
|
|
6
|
+
|
|
7
|
+
export type MobileGuiEnabled = "auto" | "always" | "never" | (() => boolean);
|
|
8
|
+
export type MobileGuiJoystickSide = "left" | "right";
|
|
9
|
+
export type MobileGuiMargin = number | [number, number, number, number];
|
|
10
|
+
|
|
11
|
+
export interface MobileGuiLayoutOptions {
|
|
12
|
+
joystickSide?: MobileGuiJoystickSide;
|
|
13
|
+
margin?: MobileGuiMargin;
|
|
14
|
+
buttonsMargin?: MobileGuiMargin;
|
|
15
|
+
joystickMargin?: MobileGuiMargin;
|
|
16
|
+
gap?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface MobileJoystickComponentProps extends JoystickSettings {
|
|
20
|
+
defaultProps: JoystickSettings;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface MobileButtonComponentProps extends ButtonProps {
|
|
24
|
+
controlName: "action" | "back" | "dash";
|
|
25
|
+
defaultProps: ButtonProps;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface MobileGuiComponentsOptions {
|
|
29
|
+
joystick?: ComponentFunction<MobileJoystickComponentProps>;
|
|
30
|
+
buttons?: {
|
|
31
|
+
action?: ComponentFunction<MobileButtonComponentProps>;
|
|
32
|
+
back?: ComponentFunction<MobileButtonComponentProps>;
|
|
33
|
+
dash?: ComponentFunction<MobileButtonComponentProps>;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface MobileGuiButtonOptions extends Partial<ButtonProps> {
|
|
38
|
+
enabled?: boolean;
|
|
39
|
+
component?: ComponentFunction<MobileButtonComponentProps>;
|
|
40
|
+
container?: Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface MobileGuiJoystickOptions extends Partial<JoystickSettings> {
|
|
44
|
+
component?: ComponentFunction<MobileJoystickComponentProps>;
|
|
45
|
+
moveInterval?: number;
|
|
46
|
+
threshold?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface MobileGuiOptions {
|
|
50
|
+
id?: string;
|
|
51
|
+
enabled?: MobileGuiEnabled;
|
|
52
|
+
layout?: MobileGuiLayoutOptions;
|
|
53
|
+
components?: MobileGuiComponentsOptions;
|
|
54
|
+
joystick?: false | MobileGuiJoystickOptions;
|
|
55
|
+
buttons?: {
|
|
56
|
+
action?: boolean | MobileGuiButtonOptions;
|
|
57
|
+
back?: boolean | MobileGuiButtonOptions;
|
|
58
|
+
dash?: boolean | MobileGuiButtonOptions;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
5
61
|
|
|
6
62
|
function isMobile() {
|
|
63
|
+
if (typeof navigator === "undefined") return false;
|
|
7
64
|
return /Android|iPhone|iPad|iPod|Windows Phone|webOS|BlackBerry/i.test(navigator.userAgent);
|
|
8
|
-
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resolveEnabled(enabled: MobileGuiEnabled = "auto") {
|
|
68
|
+
if (enabled === "always") return true;
|
|
69
|
+
if (enabled === "never") return false;
|
|
70
|
+
if (typeof enabled === "function") return enabled();
|
|
71
|
+
return isMobile();
|
|
72
|
+
}
|
|
9
73
|
|
|
10
|
-
export const withMobile = () => (
|
|
74
|
+
export const withMobile = (options: MobileGuiOptions = {}) => (
|
|
11
75
|
{
|
|
12
76
|
gui: [
|
|
13
77
|
{
|
|
14
|
-
id: 'mobile-gui',
|
|
78
|
+
id: options.id ?? 'mobile-gui',
|
|
15
79
|
component: MobileGui,
|
|
16
80
|
autoDisplay: true,
|
|
81
|
+
data: options,
|
|
17
82
|
dependencies: () => {
|
|
18
83
|
const engine = inject(RpgClientEngine);
|
|
19
|
-
return [
|
|
84
|
+
return [
|
|
85
|
+
computed(() => resolveEnabled(options.enabled) || undefined),
|
|
86
|
+
engine.controlsReady
|
|
87
|
+
]
|
|
20
88
|
}
|
|
21
89
|
}
|
|
22
90
|
]
|
|
23
91
|
}
|
|
24
|
-
)
|
|
92
|
+
)
|