@zylem/game-lib 0.6.3 → 0.6.4

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 (74) hide show
  1. package/dist/actions.d.ts +5 -5
  2. package/dist/actions.js +196 -32
  3. package/dist/actions.js.map +1 -1
  4. package/dist/behavior/jumper-2d.d.ts +114 -0
  5. package/dist/behavior/jumper-2d.js +711 -0
  6. package/dist/behavior/jumper-2d.js.map +1 -0
  7. package/dist/behavior/platformer-3d.d.ts +14 -14
  8. package/dist/behavior/platformer-3d.js +347 -104
  9. package/dist/behavior/platformer-3d.js.map +1 -1
  10. package/dist/behavior/ricochet-2d.d.ts +4 -3
  11. package/dist/behavior/ricochet-2d.js +53 -22
  12. package/dist/behavior/ricochet-2d.js.map +1 -1
  13. package/dist/behavior/ricochet-3d.d.ts +117 -0
  14. package/dist/behavior/ricochet-3d.js +443 -0
  15. package/dist/behavior/ricochet-3d.js.map +1 -0
  16. package/dist/behavior/screen-visibility.d.ts +79 -0
  17. package/dist/behavior/screen-visibility.js +358 -0
  18. package/dist/behavior/screen-visibility.js.map +1 -0
  19. package/dist/behavior/screen-wrap.d.ts +4 -3
  20. package/dist/behavior/screen-wrap.js +100 -49
  21. package/dist/behavior/screen-wrap.js.map +1 -1
  22. package/dist/behavior/shooter-2d.d.ts +79 -0
  23. package/dist/behavior/shooter-2d.js +180 -0
  24. package/dist/behavior/shooter-2d.js.map +1 -0
  25. package/dist/behavior/thruster.d.ts +5 -4
  26. package/dist/behavior/thruster.js +133 -75
  27. package/dist/behavior/thruster.js.map +1 -1
  28. package/dist/behavior/top-down-movement.d.ts +56 -0
  29. package/dist/behavior/top-down-movement.js +125 -0
  30. package/dist/behavior/top-down-movement.js.map +1 -0
  31. package/dist/behavior/world-boundary-2d.d.ts +4 -3
  32. package/dist/behavior/world-boundary-2d.js +90 -36
  33. package/dist/behavior/world-boundary-2d.js.map +1 -1
  34. package/dist/behavior/world-boundary-3d.d.ts +76 -0
  35. package/dist/behavior/world-boundary-3d.js +274 -0
  36. package/dist/behavior/world-boundary-3d.js.map +1 -0
  37. package/dist/{behavior-descriptor-BWNWmIjv.d.ts → behavior-descriptor-BXnVR8Ki.d.ts} +22 -5
  38. package/dist/{blueprints-BWGz8fII.d.ts → blueprints-DmbK2dki.d.ts} +2 -2
  39. package/dist/camera-4XO5gbQH.d.ts +905 -0
  40. package/dist/camera.d.ts +1 -1
  41. package/dist/camera.js +876 -289
  42. package/dist/camera.js.map +1 -1
  43. package/dist/{composition-DrzFrbqI.d.ts → composition-BASvMKrW.d.ts} +1 -1
  44. package/dist/{core-DAkskq6Y.d.ts → core-CARRaS55.d.ts} +57 -14
  45. package/dist/core.d.ts +9 -8
  46. package/dist/core.js +4519 -1255
  47. package/dist/core.js.map +1 -1
  48. package/dist/{entities-DC9ce_vx.d.ts → entities-ChFirVL9.d.ts} +22 -28
  49. package/dist/entities.d.ts +4 -4
  50. package/dist/entities.js +1231 -314
  51. package/dist/entities.js.map +1 -1
  52. package/dist/{entity-BpbZqg19.d.ts → entity-vj-HTjzU.d.ts} +80 -11
  53. package/dist/{global-change-Dc8uCKi2.d.ts → global-change-2JvMaz44.d.ts} +1 -1
  54. package/dist/main.d.ts +718 -19
  55. package/dist/main.js +12129 -5959
  56. package/dist/main.js.map +1 -1
  57. package/dist/physics-pose-DCc4oE44.d.ts +25 -0
  58. package/dist/physics-protocol-BDD3P5W2.d.ts +200 -0
  59. package/dist/physics-worker.d.ts +21 -0
  60. package/dist/physics-worker.js +306 -0
  61. package/dist/physics-worker.js.map +1 -0
  62. package/dist/physics.d.ts +205 -0
  63. package/dist/physics.js +577 -0
  64. package/dist/physics.js.map +1 -0
  65. package/dist/{stage-types-BFsm3qsZ.d.ts → stage-types-C19IhuzA.d.ts} +253 -89
  66. package/dist/stage.d.ts +9 -8
  67. package/dist/stage.js +3782 -1041
  68. package/dist/stage.js.map +1 -1
  69. package/dist/sync-state-machine-CZyspBpj.d.ts +16 -0
  70. package/dist/{thruster-DhRaJnoL.d.ts → thruster-23lzoPZd.d.ts} +16 -8
  71. package/dist/world-DfgxoNMt.d.ts +105 -0
  72. package/package.json +25 -1
  73. package/dist/camera-B5e4c78l.d.ts +0 -468
  74. package/dist/world-Be5m1XC1.d.ts +0 -31
package/dist/camera.js CHANGED
@@ -1,8 +1,8 @@
1
1
  // src/lib/camera/camera.ts
2
- import { Vector2 as Vector24, Vector3 as Vector34 } from "three";
2
+ import { Vector2 as Vector23, Vector3 as Vector37 } from "three";
3
3
 
4
4
  // src/lib/camera/zylem-camera.ts
5
- import { PerspectiveCamera as PerspectiveCamera2, Vector3 as Vector33, Object3D as Object3D2, OrthographicCamera } from "three";
5
+ import { PerspectiveCamera as PerspectiveCamera2, Vector3 as Vector36, OrthographicCamera, WebGLRenderTarget as WebGLRenderTarget2, LinearFilter } from "three";
6
6
 
7
7
  // src/lib/camera/perspective.ts
8
8
  var Perspectives = {
@@ -10,145 +10,12 @@ var Perspectives = {
10
10
  ThirdPerson: "third-person",
11
11
  Isometric: "isometric",
12
12
  Flat2D: "flat-2d",
13
- Fixed2D: "fixed-2d"
14
- };
15
-
16
- // src/lib/camera/third-person.ts
17
- import { Vector3 } from "three";
18
- var ThirdPersonCamera = class {
19
- distance;
20
- screenResolution = null;
21
- renderer = null;
22
- scene = null;
23
- cameraRef = null;
24
- /** Padding multiplier when framing multiple targets. Higher = more zoom out. */
25
- paddingFactor = 1.5;
26
- /** Minimum camera distance when multi-framing (prevents extreme zoom-in). */
27
- minDistance = 5;
28
- /** Lerp factor for camera position smoothing. */
29
- lerpFactor = 0.1;
30
- constructor() {
31
- this.distance = new Vector3(0, 5, 8);
32
- }
33
- /**
34
- * Setup the third person camera controller
35
- */
36
- setup(params) {
37
- const { screenResolution, renderer, scene, camera } = params;
38
- this.screenResolution = screenResolution;
39
- this.renderer = renderer;
40
- this.scene = scene;
41
- this.cameraRef = camera;
42
- }
43
- /**
44
- * Update the third person camera.
45
- * Handles 0, 1, and multi-target scenarios.
46
- */
47
- update(delta) {
48
- if (!this.cameraRef) return;
49
- const targets = this.cameraRef.targets;
50
- if (targets.length === 0) {
51
- this.cameraRef.camera.lookAt(new Vector3(0, 0, 0));
52
- return;
53
- }
54
- if (targets.length === 1) {
55
- this.updateSingleTarget(targets[0]);
56
- return;
57
- }
58
- this.updateMultiTarget(targets);
59
- }
60
- /**
61
- * Classic single-target follow: lerp to target position + offset, lookAt target.
62
- */
63
- updateSingleTarget(target) {
64
- const useTarget = target.group?.position || new Vector3(0, 0, 0);
65
- const desiredCameraPosition = useTarget.clone().add(this.distance);
66
- this.cameraRef.camera.position.lerp(desiredCameraPosition, this.lerpFactor);
67
- this.cameraRef.camera.lookAt(useTarget);
68
- }
69
- /**
70
- * Multi-target framing: compute centroid, measure spread, zoom out to fit all.
71
- */
72
- updateMultiTarget(targets) {
73
- const centroid = new Vector3();
74
- for (const t of targets) {
75
- centroid.add(t.group.position);
76
- }
77
- centroid.divideScalar(targets.length);
78
- let maxDistFromCentroid = 0;
79
- for (const t of targets) {
80
- const dist = centroid.distanceTo(t.group.position);
81
- if (dist > maxDistFromCentroid) {
82
- maxDistFromCentroid = dist;
83
- }
84
- }
85
- const dynamicDistance = Math.max(maxDistFromCentroid * this.paddingFactor, this.minDistance);
86
- const offsetDirection = this.distance.clone().normalize();
87
- const desiredCameraPosition = centroid.clone().add(
88
- offsetDirection.multiplyScalar(dynamicDistance)
89
- );
90
- const baseLen = this.distance.length();
91
- if (baseLen > 0) {
92
- const heightRatio = this.distance.y / baseLen;
93
- desiredCameraPosition.y = centroid.y + dynamicDistance * heightRatio;
94
- }
95
- this.cameraRef.camera.position.lerp(desiredCameraPosition, this.lerpFactor);
96
- this.cameraRef.camera.lookAt(centroid);
97
- }
98
- /**
99
- * Handle resize events
100
- */
101
- resize(width, height) {
102
- if (this.screenResolution) {
103
- this.screenResolution.set(width, height);
104
- }
105
- }
106
- /**
107
- * Set the distance offset from the target
108
- */
109
- setDistance(distance) {
110
- this.distance = distance;
111
- }
112
- };
113
-
114
- // src/lib/camera/fixed-2d.ts
115
- var Fixed2DCamera = class {
116
- screenResolution = null;
117
- renderer = null;
118
- scene = null;
119
- cameraRef = null;
120
- constructor() {
121
- }
122
- /**
123
- * Setup the fixed 2D camera controller
124
- */
125
- setup(params) {
126
- const { screenResolution, renderer, scene, camera } = params;
127
- this.screenResolution = screenResolution;
128
- this.renderer = renderer;
129
- this.scene = scene;
130
- this.cameraRef = camera;
131
- this.cameraRef.camera.position.set(0, 0, 10);
132
- this.cameraRef.camera.lookAt(0, 0, 0);
133
- }
134
- /**
135
- * Update the fixed 2D camera
136
- * Fixed cameras don't need to update position/rotation automatically
137
- */
138
- update(delta) {
139
- }
140
- /**
141
- * Handle resize events for 2D camera
142
- */
143
- resize(width, height) {
144
- if (this.screenResolution) {
145
- this.screenResolution.set(width, height);
146
- }
147
- }
13
+ Fixed2D: "fixed-2d",
14
+ TopDown: "top-down"
148
15
  };
149
16
 
150
17
  // src/lib/camera/camera-debug-delegate.ts
151
- import { Vector3 as Vector32 } from "three";
18
+ import { Vector3 } from "three";
152
19
  import { OrbitControls } from "three/addons/controls/OrbitControls.js";
153
20
  var CameraOrbitController = class {
154
21
  camera;
@@ -157,7 +24,7 @@ var CameraOrbitController = class {
157
24
  sceneRef = null;
158
25
  orbitControls = null;
159
26
  orbitTarget = null;
160
- orbitTargetWorldPos = new Vector32();
27
+ orbitTargetWorldPos = new Vector3();
161
28
  debugDelegate = null;
162
29
  debugUnsubscribe = null;
163
30
  debugStateSnapshot = { enabled: false, selected: [] };
@@ -404,7 +271,7 @@ var CameraOrbitController = class {
404
271
  return;
405
272
  }
406
273
  this.savedCameraLocalPosition = this.camera.position.clone();
407
- const worldPos = new Vector32();
274
+ const worldPos = new Vector3();
408
275
  this.camera.getWorldPosition(worldPos);
409
276
  this.cameraRig.remove(this.camera);
410
277
  if (this.sceneRef) {
@@ -432,7 +299,7 @@ var CameraOrbitController = class {
432
299
  };
433
300
 
434
301
  // src/lib/camera/renderer-manager.ts
435
- import { Vector2 as Vector22, WebGLRenderer as WebGLRenderer2 } from "three";
302
+ import { Vector2, WebGLRenderer as WebGLRenderer2 } from "three";
436
303
  import { WebGPURenderer } from "three/webgpu";
437
304
  import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
438
305
 
@@ -564,8 +431,9 @@ var RendererManager = class {
564
431
  _isWebGPU = false;
565
432
  _initialized = false;
566
433
  _sceneRef = null;
434
+ _lastAnimationTimestamp = null;
567
435
  constructor(screenResolution, rendererType = "webgl") {
568
- this.screenResolution = screenResolution || new Vector22(window.innerWidth, window.innerHeight);
436
+ this.screenResolution = screenResolution || new Vector2(window.innerWidth, window.innerHeight);
569
437
  this.rendererType = rendererType;
570
438
  }
571
439
  /**
@@ -647,14 +515,18 @@ var RendererManager = class {
647
515
  * Start the render loop. Calls the provided callback each frame.
648
516
  */
649
517
  startRenderLoop(onFrame) {
650
- this.renderer.setAnimationLoop((delta) => {
651
- onFrame(delta || 0);
518
+ this._lastAnimationTimestamp = null;
519
+ this.renderer.setAnimationLoop((timestamp) => {
520
+ const deltaSeconds = this._lastAnimationTimestamp === null ? 0 : Math.max(0, (timestamp - this._lastAnimationTimestamp) / 1e3);
521
+ this._lastAnimationTimestamp = timestamp;
522
+ onFrame(deltaSeconds);
652
523
  });
653
524
  }
654
525
  /**
655
526
  * Stop the render loop.
656
527
  */
657
528
  stopRenderLoop() {
529
+ this._lastAnimationTimestamp = null;
658
530
  try {
659
531
  this.renderer.setAnimationLoop(null);
660
532
  } catch {
@@ -683,6 +555,25 @@ var RendererManager = class {
683
555
  this.composer.render(0);
684
556
  }
685
557
  }
558
+ /**
559
+ * Render a camera to its offscreen render target (WebGL only).
560
+ * Bypasses the EffectComposer since post-processing is not needed
561
+ * for render-to-texture output.
562
+ *
563
+ * The camera must have a non-null renderTarget.
564
+ */
565
+ renderCameraToTarget(scene, camera) {
566
+ if (!camera.renderTarget) return;
567
+ if (this.renderer instanceof WebGLRenderer2) {
568
+ const prevTarget = this.renderer.getRenderTarget();
569
+ this.renderer.setRenderTarget(camera.renderTarget);
570
+ this.renderer.clear();
571
+ this.renderer.render(scene, camera.camera);
572
+ this.renderer.setRenderTarget(prevTarget);
573
+ } else {
574
+ console.warn("RendererManager: Render-to-texture is not yet supported for WebGPU");
575
+ }
576
+ }
686
577
  /**
687
578
  * Render a scene from multiple cameras, each with their own viewport.
688
579
  * Cameras are rendered in order (first = bottom layer, last = top layer).
@@ -753,8 +644,540 @@ var RendererManager = class {
753
644
  }
754
645
  };
755
646
 
647
+ // src/lib/camera/smoothing.ts
648
+ import { Vector3 as Vector32, Quaternion as Quaternion2, MathUtils } from "three";
649
+ function defaultPose() {
650
+ return {
651
+ position: new Vector32(0, 0, 10),
652
+ rotation: new Quaternion2(),
653
+ fov: 75,
654
+ zoom: 1,
655
+ near: 0.1,
656
+ far: 1e3
657
+ };
658
+ }
659
+ function clonePose(pose) {
660
+ return {
661
+ position: pose.position.clone(),
662
+ rotation: pose.rotation.clone(),
663
+ fov: pose.fov,
664
+ zoom: pose.zoom,
665
+ near: pose.near,
666
+ far: pose.far,
667
+ lookAt: pose.lookAt?.clone()
668
+ };
669
+ }
670
+ function applyDelta(pose, delta) {
671
+ const result = clonePose(pose);
672
+ if (delta.position) {
673
+ result.position.add(delta.position);
674
+ }
675
+ if (delta.rotation) {
676
+ result.rotation.multiply(delta.rotation);
677
+ }
678
+ if (delta.fov != null && result.fov != null) {
679
+ result.fov += delta.fov;
680
+ }
681
+ if (delta.zoom != null && result.zoom != null) {
682
+ result.zoom += delta.zoom;
683
+ }
684
+ return result;
685
+ }
686
+ function smoothPose(current, target, damping, dt) {
687
+ const t = 1 - Math.pow(1 - MathUtils.clamp(damping, 0, 1), dt * 60);
688
+ const result = clonePose(current);
689
+ result.position.lerp(target.position, t);
690
+ result.rotation.slerp(target.rotation, t);
691
+ if (target.fov != null && result.fov != null) {
692
+ result.fov = MathUtils.lerp(result.fov, target.fov, t);
693
+ }
694
+ if (target.zoom != null && result.zoom != null) {
695
+ result.zoom = MathUtils.lerp(result.zoom, target.zoom, t);
696
+ }
697
+ if (target.near != null) {
698
+ result.near = target.near;
699
+ }
700
+ if (target.far != null) {
701
+ result.far = target.far;
702
+ }
703
+ result.lookAt = target.lookAt?.clone();
704
+ return result;
705
+ }
706
+
707
+ // src/lib/camera/camera-pipeline.ts
708
+ var CameraPipeline = class {
709
+ /** Active perspective (exactly one at a time). */
710
+ perspective = null;
711
+ /** Keyed behaviors, sorted by priority each frame. */
712
+ behaviors = /* @__PURE__ */ new Map();
713
+ /** Active transient actions. Expired actions are auto-removed. */
714
+ actions = [];
715
+ /** The desired pose after perspective + behaviors + actions. */
716
+ _desiredPose = defaultPose();
717
+ /** The smoothed final pose committed to the Three.js camera. */
718
+ _finalPose = defaultPose();
719
+ /** Whether the pipeline has run at least once (prevents lerp from origin on first frame). */
720
+ _initialized = false;
721
+ /** Smoothing factor: 0 = no movement, 1 = instant snap. */
722
+ damping = 0.15;
723
+ constructor(perspective) {
724
+ if (perspective) {
725
+ this.perspective = perspective;
726
+ if (perspective.defaults?.damping != null) {
727
+ this.damping = perspective.defaults.damping;
728
+ }
729
+ }
730
+ }
731
+ /**
732
+ * Run the full pipeline for one frame.
733
+ * Returns the final pose that should be committed to the Three.js camera.
734
+ */
735
+ run(ctx) {
736
+ let pose = this.perspective ? this.perspective.getBasePose(ctx) : defaultPose();
737
+ const sorted = this.getSortedBehaviors();
738
+ for (const behavior of sorted) {
739
+ if (behavior.enabled === false) continue;
740
+ pose = behavior.update(ctx, pose);
741
+ }
742
+ this._desiredPose = clonePose(pose);
743
+ for (let i = this.actions.length - 1; i >= 0; i--) {
744
+ const action = this.actions[i];
745
+ const delta = action.update(ctx);
746
+ pose = applyDelta(pose, delta);
747
+ if (action.isDone(ctx)) {
748
+ this.actions.splice(i, 1);
749
+ }
750
+ }
751
+ if (!this._initialized) {
752
+ this._finalPose = clonePose(pose);
753
+ this._initialized = true;
754
+ } else {
755
+ this._finalPose = smoothPose(this._finalPose, pose, this.damping, ctx.dt);
756
+ }
757
+ return this._finalPose;
758
+ }
759
+ // ─── Behavior management ───────────────────────────────────────────────
760
+ /**
761
+ * Add or replace a behavior by key (idempotent).
762
+ * Calls onDetach on the old behavior and onAttach on the new one.
763
+ */
764
+ addBehavior(key, behavior, ctx) {
765
+ const existing = this.behaviors.get(key);
766
+ if (existing?.onDetach && ctx) {
767
+ existing.onDetach(ctx);
768
+ }
769
+ this.behaviors.set(key, behavior);
770
+ if (behavior.onAttach && ctx) {
771
+ behavior.onAttach(ctx);
772
+ }
773
+ }
774
+ /**
775
+ * Remove a behavior by key. Calls onDetach if a context is provided.
776
+ */
777
+ removeBehavior(key, ctx) {
778
+ const existing = this.behaviors.get(key);
779
+ if (!existing) return false;
780
+ if (existing.onDetach && ctx) {
781
+ existing.onDetach(ctx);
782
+ }
783
+ return this.behaviors.delete(key);
784
+ }
785
+ /**
786
+ * Check whether a behavior with the given key exists.
787
+ */
788
+ hasBehavior(key) {
789
+ return this.behaviors.has(key);
790
+ }
791
+ // ─── Action management ─────────────────────────────────────────────────
792
+ /**
793
+ * Add a transient action. Actions self-expire via isDone().
794
+ */
795
+ addAction(action) {
796
+ this.actions.push(action);
797
+ }
798
+ // ─── State inspection ──────────────────────────────────────────────────
799
+ /**
800
+ * Return a debug snapshot of the pipeline state.
801
+ */
802
+ getState() {
803
+ return {
804
+ perspectiveId: this.perspective?.id ?? null,
805
+ desiredPose: this._desiredPose ? clonePose(this._desiredPose) : null,
806
+ finalPose: this._finalPose ? clonePose(this._finalPose) : null,
807
+ activeBehaviors: Array.from(this.behaviors.keys()),
808
+ activeActionCount: this.actions.length
809
+ };
810
+ }
811
+ /**
812
+ * Set a new perspective. Resets the pipeline initialization flag so the
813
+ * first frame with the new perspective snaps instead of lerping from the old pose.
814
+ */
815
+ setPerspective(perspective) {
816
+ this.perspective = perspective;
817
+ this._initialized = false;
818
+ if (perspective.defaults?.damping != null) {
819
+ this.damping = perspective.defaults.damping;
820
+ }
821
+ }
822
+ // ─── Internal helpers ──────────────────────────────────────────────────
823
+ getSortedBehaviors() {
824
+ return Array.from(this.behaviors.values()).sort(
825
+ (a, b) => (a.priority ?? 0) - (b.priority ?? 0)
826
+ );
827
+ }
828
+ };
829
+
830
+ // src/lib/camera/perspectives/third-person-perspective.ts
831
+ import { Vector3 as Vector33, Quaternion as Quaternion3 } from "three";
832
+ var DEFAULTS = {
833
+ distance: 8,
834
+ height: 5,
835
+ shoulderOffset: 0,
836
+ targetKey: "primary",
837
+ fov: 75,
838
+ paddingFactor: 1.5,
839
+ minDistance: 5
840
+ };
841
+ var ThirdPersonPerspective = class {
842
+ id = "third-person";
843
+ defaults = { damping: 0.15 };
844
+ opts;
845
+ initialPosition;
846
+ initialLookAt;
847
+ constructor(options) {
848
+ const { initialPosition, initialLookAt, ...rest } = options ?? {};
849
+ this.opts = { ...DEFAULTS, ...rest };
850
+ this.initialPosition = initialPosition;
851
+ this.initialLookAt = initialLookAt;
852
+ }
853
+ getBasePose(ctx) {
854
+ const targetKeys = Object.keys(ctx.targets);
855
+ const primary = ctx.targets[this.opts.targetKey];
856
+ if (targetKeys.length === 0 || !primary) {
857
+ return this.staticPose();
858
+ }
859
+ if (targetKeys.length === 1) {
860
+ return this.singleTargetPose(primary.position);
861
+ }
862
+ return this.multiTargetPose(ctx);
863
+ }
864
+ /**
865
+ * No targets: use the user-specified initial position if available,
866
+ * otherwise fall back to a default pose behind the origin.
867
+ */
868
+ staticPose() {
869
+ if (this.initialPosition) {
870
+ return {
871
+ position: this.initialPosition.clone(),
872
+ rotation: new Quaternion3(),
873
+ fov: this.opts.fov,
874
+ zoom: 1,
875
+ near: 0.1,
876
+ far: 1e3,
877
+ lookAt: this.initialLookAt?.clone() ?? new Vector33(0, 0, 0)
878
+ };
879
+ }
880
+ const position = new Vector33(
881
+ this.opts.shoulderOffset,
882
+ this.opts.height,
883
+ this.opts.distance
884
+ );
885
+ return {
886
+ position,
887
+ rotation: new Quaternion3(),
888
+ fov: this.opts.fov,
889
+ zoom: 1,
890
+ near: 0.1,
891
+ far: 1e3,
892
+ lookAt: new Vector33(0, 0, 0)
893
+ };
894
+ }
895
+ /**
896
+ * Single target: position = target + offset, lookAt = target.
897
+ */
898
+ singleTargetPose(targetPos) {
899
+ const position = new Vector33(
900
+ targetPos.x + this.opts.shoulderOffset,
901
+ targetPos.y + this.opts.height,
902
+ targetPos.z + this.opts.distance
903
+ );
904
+ return {
905
+ position,
906
+ rotation: new Quaternion3(),
907
+ fov: this.opts.fov,
908
+ zoom: 1,
909
+ near: 0.1,
910
+ far: 1e3,
911
+ lookAt: targetPos.clone()
912
+ };
913
+ }
914
+ /**
915
+ * Multi-target: compute centroid and adjust distance to frame all targets.
916
+ */
917
+ multiTargetPose(ctx) {
918
+ const targets = Object.values(ctx.targets);
919
+ const centroid = new Vector33();
920
+ for (const t of targets) {
921
+ centroid.add(t.position);
922
+ }
923
+ centroid.divideScalar(targets.length);
924
+ let maxDist = 0;
925
+ for (const t of targets) {
926
+ const d = centroid.distanceTo(t.position);
927
+ if (d > maxDist) maxDist = d;
928
+ }
929
+ const dynamicDistance = Math.max(
930
+ maxDist * this.opts.paddingFactor,
931
+ this.opts.minDistance
932
+ );
933
+ const baseOffset = new Vector33(
934
+ this.opts.shoulderOffset,
935
+ this.opts.height,
936
+ this.opts.distance
937
+ );
938
+ const baseLen = baseOffset.length();
939
+ const dir = baseLen > 0 ? baseOffset.clone().normalize() : new Vector33(0, 0.5, 1).normalize();
940
+ const position = centroid.clone().add(dir.multiplyScalar(dynamicDistance));
941
+ if (baseLen > 0) {
942
+ const heightRatio = this.opts.height / baseLen;
943
+ position.y = centroid.y + dynamicDistance * heightRatio;
944
+ }
945
+ return {
946
+ position,
947
+ rotation: new Quaternion3(),
948
+ fov: this.opts.fov,
949
+ zoom: 1,
950
+ near: 0.1,
951
+ far: 1e3,
952
+ lookAt: centroid.clone()
953
+ };
954
+ }
955
+ };
956
+
957
+ // src/lib/camera/perspectives/fixed-2d-perspective.ts
958
+ import { Vector3 as Vector34, Quaternion as Quaternion4 } from "three";
959
+ var DEFAULTS2 = {
960
+ position: { x: 0, y: 0, z: 10 },
961
+ zoom: 10
962
+ };
963
+ var Fixed2DPerspective = class {
964
+ id = "fixed-2d";
965
+ defaults = { damping: 1 };
966
+ opts;
967
+ constructor(options) {
968
+ this.opts = { ...DEFAULTS2, ...options };
969
+ }
970
+ getBasePose(_ctx) {
971
+ return {
972
+ position: new Vector34(
973
+ this.opts.position.x,
974
+ this.opts.position.y,
975
+ this.opts.position.z
976
+ ),
977
+ rotation: new Quaternion4(),
978
+ zoom: this.opts.zoom,
979
+ near: 1,
980
+ far: 1e3,
981
+ lookAt: new Vector34(this.opts.position.x, this.opts.position.y, 0)
982
+ };
983
+ }
984
+ };
985
+
986
+ // src/lib/camera/perspectives/first-person-perspective.ts
987
+ import { Vector3 as Vector35, Quaternion as Quaternion5, Euler, MathUtils as MathUtils2 } from "three";
988
+ var DEFAULTS3 = {
989
+ eyeHeight: 1.7,
990
+ defaultFov: 75,
991
+ pitchLimit: Math.PI / 2 - 0.01,
992
+ lookAtLerpSpeed: 5,
993
+ fovLerpSpeed: 8,
994
+ targetKey: "primary"
995
+ };
996
+ var FirstPersonPerspective = class {
997
+ id = "first-person";
998
+ defaults = { damping: 1 };
999
+ opts;
1000
+ /** Fallback position when no target entity is attached. Mutate directly for manual movement. */
1001
+ initialPosition;
1002
+ initialLookAt;
1003
+ // --- Look state ---
1004
+ _yaw = 0;
1005
+ _pitch = 0;
1006
+ // --- FOV zoom state ---
1007
+ _currentFov;
1008
+ _targetFov;
1009
+ // --- Look-at target state ---
1010
+ _lookAtTarget = null;
1011
+ _lookAtLerpSpeed;
1012
+ _currentRotation = new Quaternion5();
1013
+ _rotationInitialized = false;
1014
+ constructor(options) {
1015
+ const { initialPosition, initialLookAt, ...rest } = options ?? {};
1016
+ this.opts = { ...DEFAULTS3, ...rest };
1017
+ this.initialPosition = initialPosition;
1018
+ this.initialLookAt = initialLookAt;
1019
+ this._currentFov = this.opts.defaultFov;
1020
+ this._targetFov = this.opts.defaultFov;
1021
+ this._lookAtLerpSpeed = this.opts.lookAtLerpSpeed;
1022
+ if (initialPosition && initialLookAt) {
1023
+ this.deriveYawPitchFromLookAt(initialPosition, initialLookAt);
1024
+ }
1025
+ }
1026
+ // --- Public API (called by game code / FPS behavior) ---
1027
+ /** Accumulate yaw and pitch deltas. Pitch is clamped to the configured limit. */
1028
+ look(deltaYaw, deltaPitch) {
1029
+ this._yaw += deltaYaw;
1030
+ this._pitch = MathUtils2.clamp(
1031
+ this._pitch + deltaPitch,
1032
+ -this.opts.pitchLimit,
1033
+ this.opts.pitchLimit
1034
+ );
1035
+ }
1036
+ /** Set absolute yaw and pitch. Pitch is clamped to the configured limit. */
1037
+ setLook(yaw, pitch) {
1038
+ this._yaw = yaw;
1039
+ this._pitch = MathUtils2.clamp(pitch, -this.opts.pitchLimit, this.opts.pitchLimit);
1040
+ }
1041
+ /** Current yaw in radians. */
1042
+ get yaw() {
1043
+ return this._yaw;
1044
+ }
1045
+ /** Current pitch in radians. */
1046
+ get pitch() {
1047
+ return this._pitch;
1048
+ }
1049
+ /** Set the target FOV for a smooth zoom transition (e.g. sniper scope). */
1050
+ zoom(fov) {
1051
+ this._targetFov = fov;
1052
+ }
1053
+ /** Return to the default FOV. */
1054
+ resetZoom() {
1055
+ this._targetFov = this.opts.defaultFov;
1056
+ }
1057
+ /** Current field of view. */
1058
+ get currentFov() {
1059
+ return this._currentFov;
1060
+ }
1061
+ /**
1062
+ * Enable smooth look-at toward a world position.
1063
+ * The camera will slerp from the current rotation toward the look-at direction.
1064
+ */
1065
+ lookAt(target, lerpSpeed) {
1066
+ this._lookAtTarget = target;
1067
+ if (lerpSpeed != null) {
1068
+ this._lookAtLerpSpeed = lerpSpeed;
1069
+ }
1070
+ }
1071
+ /** Disable look-at and return to manual yaw/pitch control. */
1072
+ clearLookAt() {
1073
+ if (this._lookAtTarget) {
1074
+ this.deriveYawPitchFromQuaternion(this._currentRotation);
1075
+ }
1076
+ this._lookAtTarget = null;
1077
+ }
1078
+ // --- CameraPerspective interface ---
1079
+ getBasePose(ctx) {
1080
+ const position = this.computePosition(ctx);
1081
+ const rotation = this.computeRotation(position, ctx.dt);
1082
+ this._currentFov = this.lerpFov(ctx.dt);
1083
+ return {
1084
+ position,
1085
+ rotation,
1086
+ fov: this._currentFov,
1087
+ near: 0.1,
1088
+ far: 1e3
1089
+ };
1090
+ }
1091
+ // --- Private helpers ---
1092
+ computePosition(ctx) {
1093
+ const target = ctx.targets[this.opts.targetKey];
1094
+ if (target) {
1095
+ return new Vector35(
1096
+ target.position.x,
1097
+ target.position.y + this.opts.eyeHeight,
1098
+ target.position.z
1099
+ );
1100
+ }
1101
+ if (this.initialPosition) {
1102
+ return this.initialPosition.clone();
1103
+ }
1104
+ return new Vector35(0, this.opts.eyeHeight, 0);
1105
+ }
1106
+ computeRotation(eyePosition, dt) {
1107
+ const yawPitchQuat = new Quaternion5().setFromEuler(
1108
+ new Euler(this._pitch, this._yaw, 0, "YXZ")
1109
+ );
1110
+ if (!this._rotationInitialized) {
1111
+ this._currentRotation.copy(yawPitchQuat);
1112
+ this._rotationInitialized = true;
1113
+ }
1114
+ if (this._lookAtTarget) {
1115
+ const dir = _vec3.copy(this._lookAtTarget).sub(eyePosition);
1116
+ if (dir.lengthSq() > 1e-4) {
1117
+ dir.normalize();
1118
+ const desiredYaw = Math.atan2(-dir.x, -dir.z);
1119
+ const desiredPitch = Math.asin(MathUtils2.clamp(dir.y, -1, 1));
1120
+ const desiredQuat = _quat.setFromEuler(
1121
+ _euler.set(desiredPitch, desiredYaw, 0, "YXZ")
1122
+ );
1123
+ const t = 1 - Math.pow(1 - Math.min(this._lookAtLerpSpeed * dt, 1), 1);
1124
+ this._currentRotation.slerp(desiredQuat, MathUtils2.clamp(t, 0, 1));
1125
+ return this._currentRotation.clone();
1126
+ }
1127
+ }
1128
+ this._currentRotation.copy(yawPitchQuat);
1129
+ return yawPitchQuat;
1130
+ }
1131
+ lerpFov(dt) {
1132
+ if (Math.abs(this._currentFov - this._targetFov) < 0.01) {
1133
+ return this._targetFov;
1134
+ }
1135
+ const t = 1 - Math.pow(1 - Math.min(this.opts.fovLerpSpeed * dt, 1), 1);
1136
+ return MathUtils2.lerp(this._currentFov, this._targetFov, MathUtils2.clamp(t, 0, 1));
1137
+ }
1138
+ deriveYawPitchFromLookAt(from, to) {
1139
+ const dir = _vec3.copy(to).sub(from).normalize();
1140
+ this._yaw = Math.atan2(-dir.x, -dir.z);
1141
+ this._pitch = Math.asin(MathUtils2.clamp(dir.y, -1, 1));
1142
+ }
1143
+ deriveYawPitchFromQuaternion(q) {
1144
+ const euler = _euler.setFromQuaternion(q, "YXZ");
1145
+ this._yaw = euler.y;
1146
+ this._pitch = MathUtils2.clamp(euler.x, -this.opts.pitchLimit, this.opts.pitchLimit);
1147
+ }
1148
+ };
1149
+ var _vec3 = new Vector35();
1150
+ var _quat = new Quaternion5();
1151
+ var _euler = new Euler();
1152
+
1153
+ // src/lib/camera/perspectives/index.ts
1154
+ function createPerspective(type, options) {
1155
+ switch (type) {
1156
+ case "third-person":
1157
+ return new ThirdPersonPerspective(options);
1158
+ case "first-person":
1159
+ return new FirstPersonPerspective(options);
1160
+ case "isometric":
1161
+ return new ThirdPersonPerspective({
1162
+ distance: 10,
1163
+ height: 10,
1164
+ ...options
1165
+ });
1166
+ case "flat-2d":
1167
+ return new Fixed2DPerspective(options);
1168
+ case "fixed-2d":
1169
+ return new Fixed2DPerspective(options);
1170
+ default:
1171
+ return new ThirdPersonPerspective(options);
1172
+ }
1173
+ }
1174
+
756
1175
  // src/lib/camera/zylem-camera.ts
757
1176
  var ZylemCamera = class {
1177
+ /**
1178
+ * @deprecated No longer used. Kept as null for backward compatibility
1179
+ * with code that checks `camera.cameraRig` (e.g. scene graph insertion).
1180
+ */
758
1181
  cameraRig = null;
759
1182
  camera;
760
1183
  screenResolution;
@@ -762,7 +1185,7 @@ var ZylemCamera = class {
762
1185
  frustumSize = 10;
763
1186
  rendererType;
764
1187
  sceneRef = null;
765
- /** Name for camera manager lookup */
1188
+ /** Name for camera manager lookup. */
766
1189
  name = "";
767
1190
  /**
768
1191
  * Viewport in normalized coordinates (0-1).
@@ -771,11 +1194,21 @@ var ZylemCamera = class {
771
1194
  viewport = { ...DEFAULT_VIEWPORT };
772
1195
  /**
773
1196
  * Multiple targets for the camera to follow/frame.
774
- * Replaces the old single `target` property.
775
1197
  */
776
1198
  targets = [];
777
1199
  /**
778
- * @deprecated Use `targets` array instead. This getter/setter is kept for backward compatibility.
1200
+ * The camera pose pipeline.
1201
+ * Exposed so CameraWrapper can delegate addBehavior/addAction/getState.
1202
+ */
1203
+ pipeline;
1204
+ /**
1205
+ * Offscreen render target for render-to-texture (RTT) cameras.
1206
+ * When set, the camera renders to this target instead of the screen viewport.
1207
+ * Created via createRenderTarget() or automatically by setCameraFeed().
1208
+ */
1209
+ renderTarget = null;
1210
+ /**
1211
+ * @deprecated Use `targets` array instead. Kept for backward compatibility.
779
1212
  */
780
1213
  get target() {
781
1214
  return this.targets.length > 0 ? this.targets[0] : null;
@@ -791,13 +1224,15 @@ var ZylemCamera = class {
791
1224
  this.targets = [];
792
1225
  }
793
1226
  }
794
- // Perspective controller delegation
795
- perspectiveController = null;
796
1227
  // Orbit controls
797
1228
  orbitController = null;
798
1229
  _useOrbitalControls = false;
799
- /** Reference to the shared renderer manager (set during setup) */
1230
+ /** When true, debug-mode orbital controls are not attached to this camera. */
1231
+ _skipDebugOrbit = false;
1232
+ /** Reference to the shared renderer manager (set during setup). */
800
1233
  _rendererManager = null;
1234
+ /** Elapsed time tracker for CameraContext. */
1235
+ _elapsedTime = 0;
801
1236
  constructor(perspective, screenResolution, frustumSize = 10, rendererType = "webgl") {
802
1237
  this._perspective = perspective;
803
1238
  this.screenResolution = screenResolution;
@@ -805,20 +1240,13 @@ var ZylemCamera = class {
805
1240
  this.rendererType = rendererType;
806
1241
  const aspectRatio = screenResolution.x / screenResolution.y;
807
1242
  this.camera = this.createCameraForPerspective(aspectRatio);
808
- if (this.needsRig()) {
809
- this.cameraRig = new Object3D2();
810
- this.cameraRig.position.set(0, 3, 10);
811
- this.cameraRig.add(this.camera);
812
- this.camera.lookAt(new Vector33(0, 2, 0));
813
- } else {
814
- this.camera.position.set(0, 0, 10);
815
- this.camera.lookAt(new Vector33(0, 0, 0));
816
- }
817
- this.initializePerspectiveController();
1243
+ this.camera.position.set(0, 0, 10);
1244
+ this.camera.lookAt(new Vector36(0, 0, 0));
1245
+ const perspectiveImpl = createPerspective(perspective);
1246
+ this.pipeline = new CameraPipeline(perspectiveImpl);
818
1247
  }
819
1248
  /**
820
1249
  * Setup the camera with a scene and renderer manager.
821
- * The renderer manager provides shared rendering infrastructure.
822
1250
  */
823
1251
  async setup(scene, rendererManager) {
824
1252
  this.sceneRef = scene;
@@ -828,19 +1256,12 @@ var ZylemCamera = class {
828
1256
  if (this._rendererManager && !this._rendererManager.initialized) {
829
1257
  await this._rendererManager.initRenderer();
830
1258
  }
831
- if (this.perspectiveController && this._rendererManager) {
832
- this.perspectiveController.setup({
833
- screenResolution: this.screenResolution,
834
- renderer: this._rendererManager.renderer,
835
- scene,
836
- camera: this
837
- });
838
- }
839
1259
  if (this._rendererManager) {
840
1260
  this.orbitController = new CameraOrbitController(
841
1261
  this.camera,
842
1262
  this._rendererManager.renderer.domElement,
843
- this.cameraRig
1263
+ null
1264
+ // no camera rig
844
1265
  );
845
1266
  this.orbitController.setScene(scene);
846
1267
  if (this._useOrbitalControls) {
@@ -868,17 +1289,24 @@ var ZylemCamera = class {
868
1289
  await this.setup(scene, this._rendererManager);
869
1290
  }
870
1291
  /**
871
- * Update camera controllers (called each frame).
872
- * Does NOT render -- rendering is handled by RendererManager.
1292
+ * Update the camera each frame.
1293
+ *
1294
+ * When orbit/debug controls are active, the pipeline is skipped and
1295
+ * orbit controls manage the camera directly. Otherwise, the pipeline
1296
+ * runs: Perspective -> Behaviors -> Actions -> Smoothing -> Commit.
873
1297
  */
874
1298
  update(delta) {
875
1299
  this.orbitController?.update();
876
- if (this.perspectiveController && !this.isDebugModeActive() && !this._useOrbitalControls) {
877
- this.perspectiveController.update(delta);
1300
+ if (this.isDebugModeActive() || this._useOrbitalControls) {
1301
+ return;
878
1302
  }
1303
+ this._elapsedTime += delta;
1304
+ const ctx = this.buildContext(delta);
1305
+ const finalPose = this.pipeline.run(ctx);
1306
+ this.commitPose(finalPose);
879
1307
  }
880
1308
  /**
881
- * Check if debug mode is active (orbit controls taking over camera)
1309
+ * Check if debug mode is active (orbit controls taking over camera).
882
1310
  */
883
1311
  isDebugModeActive() {
884
1312
  return this.orbitController?.isActive ?? false;
@@ -903,6 +1331,7 @@ var ZylemCamera = class {
903
1331
  get useOrbitalControls() {
904
1332
  return this._useOrbitalControls;
905
1333
  }
1334
+ // ─── Target management ──────────────────────────────────────────────────
906
1335
  /**
907
1336
  * Add a target entity for the camera to follow/frame.
908
1337
  */
@@ -926,23 +1355,12 @@ var ZylemCamera = class {
926
1355
  clearTargets() {
927
1356
  this.targets = [];
928
1357
  }
1358
+ // ─── Viewport & resize ──────────────────────────────────────────────────
929
1359
  /**
930
- * Dispose camera resources (not the renderer -- that's managed by RendererManager).
931
- */
932
- destroy() {
933
- try {
934
- this.orbitController?.dispose();
935
- } catch {
936
- }
937
- this.sceneRef = null;
938
- this.targets = [];
939
- this._rendererManager = null;
940
- }
941
- /**
942
- * Attach a delegate to react to debug state changes.
1360
+ * Set the viewport for this camera (normalized 0-1 coordinates).
943
1361
  */
944
- setDebugDelegate(delegate) {
945
- this.orbitController?.setDebugDelegate(delegate);
1362
+ setViewport(x, y, width, height) {
1363
+ this.viewport = { x, y, width, height };
946
1364
  }
947
1365
  /**
948
1366
  * Resize camera projection.
@@ -953,110 +1371,87 @@ var ZylemCamera = class {
953
1371
  this.camera.aspect = width / height;
954
1372
  this.camera.updateProjectionMatrix();
955
1373
  }
956
- if (this.perspectiveController) {
957
- this.perspectiveController.resize(width, height);
1374
+ if (this.camera instanceof OrthographicCamera) {
1375
+ const aspect = width / height;
1376
+ this.camera.left = this.frustumSize * aspect / -2;
1377
+ this.camera.right = this.frustumSize * aspect / 2;
1378
+ this.camera.top = this.frustumSize / 2;
1379
+ this.camera.bottom = this.frustumSize / -2;
1380
+ this.camera.updateProjectionMatrix();
958
1381
  }
959
1382
  }
1383
+ // ─── Render-to-texture ─────────────────────────────────────────────────
960
1384
  /**
961
- * Set the viewport for this camera (normalized 0-1 coordinates).
1385
+ * Create an offscreen render target for this camera.
1386
+ * When a render target is present, CameraManager renders this camera
1387
+ * to the target instead of the screen viewport.
1388
+ *
1389
+ * @param width Texture width in pixels (default 512)
1390
+ * @param height Texture height in pixels (default 512)
962
1391
  */
963
- setViewport(x, y, width, height) {
964
- this.viewport = { x, y, width, height };
1392
+ createRenderTarget(width = 512, height = 512) {
1393
+ if (this.renderTarget) {
1394
+ this.renderTarget.dispose();
1395
+ }
1396
+ this.renderTarget = new WebGLRenderTarget2(width, height, {
1397
+ minFilter: LinearFilter,
1398
+ magFilter: LinearFilter
1399
+ });
1400
+ return this.renderTarget;
965
1401
  }
966
1402
  /**
967
- * Create camera based on perspective type
1403
+ * Get the texture from the render target (for applying to a mesh material).
1404
+ * Returns null if no render target has been created.
968
1405
  */
969
- createCameraForPerspective(aspectRatio) {
970
- switch (this._perspective) {
971
- case Perspectives.ThirdPerson:
972
- return this.createThirdPersonCamera(aspectRatio);
973
- case Perspectives.FirstPerson:
974
- return this.createFirstPersonCamera(aspectRatio);
975
- case Perspectives.Isometric:
976
- return this.createIsometricCamera(aspectRatio);
977
- case Perspectives.Flat2D:
978
- return this.createFlat2DCamera(aspectRatio);
979
- case Perspectives.Fixed2D:
980
- return this.createFixed2DCamera(aspectRatio);
981
- default:
982
- return this.createThirdPersonCamera(aspectRatio);
983
- }
1406
+ getRenderTexture() {
1407
+ return this.renderTarget?.texture ?? null;
984
1408
  }
1409
+ // ─── Lifecycle ──────────────────────────────────────────────────────────
985
1410
  /**
986
- * Initialize perspective-specific controller
1411
+ * Dispose camera resources.
987
1412
  */
988
- initializePerspectiveController() {
989
- switch (this._perspective) {
990
- case Perspectives.ThirdPerson:
991
- this.perspectiveController = new ThirdPersonCamera();
992
- break;
993
- case Perspectives.Fixed2D:
994
- this.perspectiveController = new Fixed2DCamera();
995
- break;
996
- default:
997
- this.perspectiveController = new ThirdPersonCamera();
1413
+ destroy() {
1414
+ try {
1415
+ this.orbitController?.dispose();
1416
+ } catch {
998
1417
  }
1418
+ try {
1419
+ this.renderTarget?.dispose();
1420
+ } catch {
1421
+ }
1422
+ this.renderTarget = null;
1423
+ this.sceneRef = null;
1424
+ this.targets = [];
1425
+ this._rendererManager = null;
999
1426
  }
1000
- createThirdPersonCamera(aspectRatio) {
1001
- return new PerspectiveCamera2(75, aspectRatio, 0.1, 1e3);
1002
- }
1003
- createFirstPersonCamera(aspectRatio) {
1004
- return new PerspectiveCamera2(75, aspectRatio, 0.1, 1e3);
1005
- }
1006
- createIsometricCamera(aspectRatio) {
1007
- return new OrthographicCamera(
1008
- this.frustumSize * aspectRatio / -2,
1009
- this.frustumSize * aspectRatio / 2,
1010
- this.frustumSize / 2,
1011
- this.frustumSize / -2,
1012
- 1,
1013
- 1e3
1014
- );
1015
- }
1016
- createFlat2DCamera(aspectRatio) {
1017
- return new OrthographicCamera(
1018
- this.frustumSize * aspectRatio / -2,
1019
- this.frustumSize * aspectRatio / 2,
1020
- this.frustumSize / 2,
1021
- this.frustumSize / -2,
1022
- 1,
1023
- 1e3
1024
- );
1025
- }
1026
- createFixed2DCamera(aspectRatio) {
1027
- return this.createFlat2DCamera(aspectRatio);
1427
+ // ─── Debug delegate ─────────────────────────────────────────────────────
1428
+ /**
1429
+ * Attach a delegate to react to debug state changes.
1430
+ * Skipped when _skipDebugOrbit is true so the pipeline always runs.
1431
+ */
1432
+ setDebugDelegate(delegate) {
1433
+ if (this._skipDebugOrbit) return;
1434
+ this.orbitController?.setDebugDelegate(delegate);
1028
1435
  }
1029
- // Movement methods
1030
- moveCamera(position) {
1436
+ // ─── Movement helpers (backward compat) ─────────────────────────────────
1437
+ /**
1438
+ * Directly set the camera position.
1439
+ */
1440
+ move(position) {
1031
1441
  if (this._perspective === Perspectives.Flat2D || this._perspective === Perspectives.Fixed2D) {
1032
1442
  this.frustumSize = position.z;
1033
1443
  }
1034
- if (this.cameraRig) {
1035
- this.cameraRig.position.set(position.x, position.y, position.z);
1036
- } else {
1037
- this.camera.position.set(position.x, position.y, position.z);
1038
- }
1039
- }
1040
- move(position) {
1041
- this.moveCamera(position);
1042
- }
1043
- rotate(pitch, yaw, roll) {
1044
- if (this.cameraRig) {
1045
- this.cameraRig.rotateX(pitch);
1046
- this.cameraRig.rotateY(yaw);
1047
- this.cameraRig.rotateZ(roll);
1048
- } else {
1049
- this.camera.rotateX(pitch);
1050
- this.camera.rotateY(yaw);
1051
- this.camera.rotateZ(roll);
1052
- }
1444
+ this.camera.position.set(position.x, position.y, position.z);
1053
1445
  }
1054
1446
  /**
1055
- * Check if this perspective type needs a camera rig
1447
+ * Apply incremental rotation to the camera.
1056
1448
  */
1057
- needsRig() {
1058
- return this._perspective === Perspectives.ThirdPerson;
1449
+ rotate(pitch, yaw, roll) {
1450
+ this.camera.rotateX(pitch);
1451
+ this.camera.rotateY(yaw);
1452
+ this.camera.rotateZ(roll);
1059
1453
  }
1454
+ // ─── Renderer manager access ────────────────────────────────────────────
1060
1455
  /**
1061
1456
  * Get the DOM element for the renderer.
1062
1457
  * @deprecated Access via RendererManager instead.
@@ -1080,24 +1475,108 @@ var ZylemCamera = class {
1080
1475
  this._rendererManager = manager;
1081
1476
  }
1082
1477
  // ─── Legacy compatibility methods ────────────────────────────────────────
1083
- /**
1084
- * @deprecated Renderer is now owned by RendererManager
1085
- */
1478
+ /** @deprecated Renderer is now owned by RendererManager */
1086
1479
  get renderer() {
1087
1480
  return this._rendererManager?.renderer;
1088
1481
  }
1089
- /**
1090
- * @deprecated Composer is now owned by RendererManager
1091
- */
1482
+ /** @deprecated Composer is now owned by RendererManager */
1092
1483
  get composer() {
1093
1484
  return this._rendererManager?.composer;
1094
1485
  }
1095
- /**
1096
- * @deprecated Use RendererManager.setPixelRatio() instead
1097
- */
1486
+ /** @deprecated Use RendererManager.setPixelRatio() instead */
1098
1487
  setPixelRatio(dpr) {
1099
1488
  this._rendererManager?.setPixelRatio(dpr);
1100
1489
  }
1490
+ // ─── Private helpers ────────────────────────────────────────────────────
1491
+ /**
1492
+ * Build a CameraContext from current ZylemCamera state.
1493
+ * Converts StageEntity[] targets into Record<string, TransformLike>.
1494
+ */
1495
+ buildContext(delta) {
1496
+ const targets = {};
1497
+ for (let i = 0; i < this.targets.length; i++) {
1498
+ const entity = this.targets[i];
1499
+ const key = i === 0 ? "primary" : `target_${i}`;
1500
+ if (entity.group) {
1501
+ targets[key] = {
1502
+ position: entity.group.position,
1503
+ rotation: entity.group.quaternion
1504
+ };
1505
+ }
1506
+ }
1507
+ return {
1508
+ dt: delta,
1509
+ time: this._elapsedTime,
1510
+ viewport: {
1511
+ width: this.screenResolution.x,
1512
+ height: this.screenResolution.y,
1513
+ aspect: this.screenResolution.x / this.screenResolution.y
1514
+ },
1515
+ targets
1516
+ };
1517
+ }
1518
+ /**
1519
+ * Apply the final pipeline pose to the Three.js camera.
1520
+ */
1521
+ commitPose(pose) {
1522
+ this.camera.position.copy(pose.position);
1523
+ if (pose.lookAt) {
1524
+ this.camera.lookAt(pose.lookAt);
1525
+ } else {
1526
+ this.camera.quaternion.copy(pose.rotation);
1527
+ }
1528
+ if (this.camera instanceof PerspectiveCamera2) {
1529
+ if (pose.fov != null) this.camera.fov = pose.fov;
1530
+ if (pose.near != null) this.camera.near = pose.near;
1531
+ if (pose.far != null) this.camera.far = pose.far;
1532
+ this.camera.updateProjectionMatrix();
1533
+ }
1534
+ if (this.camera instanceof OrthographicCamera) {
1535
+ if (pose.zoom != null) {
1536
+ const aspect = this.screenResolution.x / this.screenResolution.y;
1537
+ const size = pose.zoom;
1538
+ this.camera.left = -size * aspect / 2;
1539
+ this.camera.right = size * aspect / 2;
1540
+ this.camera.top = size / 2;
1541
+ this.camera.bottom = -size / 2;
1542
+ }
1543
+ if (pose.near != null) this.camera.near = pose.near;
1544
+ if (pose.far != null) this.camera.far = pose.far;
1545
+ this.camera.updateProjectionMatrix();
1546
+ }
1547
+ }
1548
+ /**
1549
+ * Create a Three.js camera based on perspective type.
1550
+ */
1551
+ createCameraForPerspective(aspectRatio) {
1552
+ switch (this._perspective) {
1553
+ case Perspectives.ThirdPerson:
1554
+ return new PerspectiveCamera2(75, aspectRatio, 0.1, 1e3);
1555
+ case Perspectives.FirstPerson:
1556
+ return new PerspectiveCamera2(75, aspectRatio, 0.1, 1e3);
1557
+ case Perspectives.Isometric:
1558
+ return new OrthographicCamera(
1559
+ this.frustumSize * aspectRatio / -2,
1560
+ this.frustumSize * aspectRatio / 2,
1561
+ this.frustumSize / 2,
1562
+ this.frustumSize / -2,
1563
+ 1,
1564
+ 1e3
1565
+ );
1566
+ case Perspectives.Flat2D:
1567
+ case Perspectives.Fixed2D:
1568
+ return new OrthographicCamera(
1569
+ this.frustumSize * aspectRatio / -2,
1570
+ this.frustumSize * aspectRatio / 2,
1571
+ this.frustumSize / 2,
1572
+ this.frustumSize / -2,
1573
+ 1,
1574
+ 1e3
1575
+ );
1576
+ default:
1577
+ return new PerspectiveCamera2(75, aspectRatio, 0.1, 1e3);
1578
+ }
1579
+ }
1101
1580
  };
1102
1581
 
1103
1582
  // src/lib/camera/camera.ts
@@ -1151,9 +1630,73 @@ var CameraWrapper = class {
1151
1630
  setViewport(x, y, width, height) {
1152
1631
  this.cameraRef.setViewport(x, y, width, height);
1153
1632
  }
1633
+ // ─── Pipeline: Behaviors ────────────────────────────────────────────────
1634
+ /**
1635
+ * Add or replace a behavior by key (idempotent).
1636
+ * Behaviors modify the desired camera pose each frame.
1637
+ *
1638
+ * @param key Unique key for this behavior (used for replacement/removal).
1639
+ * @param behavior The CameraBehavior implementation.
1640
+ */
1641
+ addBehavior(key, behavior) {
1642
+ this.cameraRef.pipeline.addBehavior(key, behavior);
1643
+ }
1644
+ /**
1645
+ * Remove a behavior by key.
1646
+ */
1647
+ removeBehavior(key) {
1648
+ return this.cameraRef.pipeline.removeBehavior(key);
1649
+ }
1650
+ // ─── Pipeline: Actions ──────────────────────────────────────────────────
1651
+ /**
1652
+ * Add a transient action (screenshake, recoil, etc.).
1653
+ * Actions apply additive deltas and self-expire when isDone() returns true.
1654
+ */
1655
+ addAction(action) {
1656
+ this.cameraRef.pipeline.addAction(action);
1657
+ }
1658
+ // ─── Pipeline: Perspective ──────────────────────────────────────────────
1659
+ /**
1660
+ * Switch the camera's active perspective at runtime.
1661
+ * The first frame after switching snaps to the new pose (no lerp).
1662
+ *
1663
+ * @param type Perspective type string (e.g. Perspectives.ThirdPerson).
1664
+ * @param options Perspective-specific options (distance, height, zoom, etc.).
1665
+ */
1666
+ setPerspective(type, options) {
1667
+ this.cameraRef.pipeline.setPerspective(createPerspective(type, options));
1668
+ }
1669
+ /**
1670
+ * Retrieve the active perspective instance, cast to the desired type.
1671
+ * Useful for calling perspective-specific methods (e.g. FirstPersonPerspective.look()).
1672
+ *
1673
+ * @example
1674
+ * const fps = camera.getPerspective<FirstPersonPerspective>();
1675
+ * fps.look(dx, dy);
1676
+ */
1677
+ getPerspective() {
1678
+ return this.cameraRef.pipeline.perspective;
1679
+ }
1680
+ // ─── Pipeline: Debug state ──────────────────────────────────────────────
1681
+ /**
1682
+ * Return a debug snapshot of the camera pipeline state.
1683
+ * Includes: active perspective, desired/final pose, behavior keys, action count.
1684
+ */
1685
+ getState() {
1686
+ return this.cameraRef.pipeline.getState();
1687
+ }
1688
+ // ─── Render-to-texture ─────────────────────────────────────────────────
1689
+ /**
1690
+ * Get the offscreen render texture for this camera.
1691
+ * Returns null if the camera was not created with renderToTexture.
1692
+ * Use with setCameraFeed() or apply directly to a mesh material.
1693
+ */
1694
+ getRenderTexture() {
1695
+ return this.cameraRef.getRenderTexture();
1696
+ }
1154
1697
  };
1155
1698
  function createCamera(options) {
1156
- const screenResolution = options.screenResolution || new Vector24(window.innerWidth, window.innerHeight);
1699
+ const screenResolution = options.screenResolution || new Vector23(window.innerWidth, window.innerHeight);
1157
1700
  let frustumSize = 10;
1158
1701
  if (options.perspective === "fixed-2d") {
1159
1702
  frustumSize = options.zoom || 10;
@@ -1167,21 +1710,49 @@ function createCamera(options) {
1167
1710
  if (options.name) {
1168
1711
  zylemCamera.name = options.name;
1169
1712
  }
1170
- const position = options.position ? options.position instanceof Vector34 ? options.position : new Vector34(options.position.x, options.position.y, options.position.z) : new Vector34(0, 0, 0);
1171
- const target = options.target ? options.target instanceof Vector34 ? options.target : new Vector34(options.target.x, options.target.y, options.target.z) : new Vector34(0, 0, 0);
1713
+ const position = options.position ? options.position instanceof Vector37 ? options.position : new Vector37(options.position.x, options.position.y, options.position.z) : new Vector37(0, 0, 0);
1714
+ const target = options.target ? options.target instanceof Vector37 ? options.target : new Vector37(options.target.x, options.target.y, options.target.z) : new Vector37(0, 0, 0);
1172
1715
  zylemCamera.move(position);
1173
1716
  zylemCamera.camera.lookAt(target);
1717
+ const perspType = options.perspective || "third-person";
1718
+ if (perspType === "fixed-2d" || perspType === "flat-2d") {
1719
+ zylemCamera.pipeline.setPerspective(
1720
+ createPerspective(perspType, { zoom: frustumSize })
1721
+ );
1722
+ } else {
1723
+ zylemCamera.pipeline.setPerspective(
1724
+ createPerspective(perspType, {
1725
+ initialPosition: position.clone(),
1726
+ initialLookAt: target.clone()
1727
+ })
1728
+ );
1729
+ }
1174
1730
  if (options.viewport) {
1175
1731
  zylemCamera.viewport = { ...options.viewport };
1176
1732
  }
1177
1733
  if (options.useOrbitalControls) {
1178
1734
  zylemCamera._useOrbitalControls = true;
1179
1735
  }
1736
+ if (options.skipDebugOrbit) {
1737
+ zylemCamera._skipDebugOrbit = true;
1738
+ }
1739
+ if (options.damping != null) {
1740
+ zylemCamera.pipeline.damping = options.damping;
1741
+ }
1742
+ if (options.behaviors) {
1743
+ for (const [key, behavior] of Object.entries(options.behaviors)) {
1744
+ zylemCamera.pipeline.addBehavior(key, behavior);
1745
+ }
1746
+ }
1747
+ if (options.renderToTexture) {
1748
+ const { width = 512, height = 512 } = options.renderToTexture;
1749
+ zylemCamera.createRenderTarget(width, height);
1750
+ }
1180
1751
  return new CameraWrapper(zylemCamera);
1181
1752
  }
1182
1753
 
1183
1754
  // src/lib/camera/camera-manager.ts
1184
- import { Vector2 as Vector25 } from "three";
1755
+ import { Vector2 as Vector24 } from "three";
1185
1756
  var CameraManager = class {
1186
1757
  /** Named camera registry */
1187
1758
  cameras = /* @__PURE__ */ new Map();
@@ -1344,17 +1915,33 @@ var CameraManager = class {
1344
1915
  }
1345
1916
  /**
1346
1917
  * Render all active cameras through the renderer manager.
1918
+ * RTT cameras (those with a renderTarget) are rendered first to their
1919
+ * offscreen textures, then viewport cameras are rendered to the screen.
1347
1920
  */
1348
1921
  render(scene) {
1349
1922
  if (!this._rendererManager || this._activeCameras.length === 0) return;
1350
- this._rendererManager.renderCameras(scene, this._activeCameras);
1923
+ const rttCameras = [];
1924
+ const viewportCameras = [];
1925
+ for (const cam of this._activeCameras) {
1926
+ if (cam.renderTarget) {
1927
+ rttCameras.push(cam);
1928
+ } else {
1929
+ viewportCameras.push(cam);
1930
+ }
1931
+ }
1932
+ for (const cam of rttCameras) {
1933
+ this._rendererManager.renderCameraToTarget(scene, cam);
1934
+ }
1935
+ if (viewportCameras.length > 0) {
1936
+ this._rendererManager.renderCameras(scene, viewportCameras);
1937
+ }
1351
1938
  }
1352
1939
  /**
1353
1940
  * Create a default third-person camera if no cameras have been added.
1354
1941
  */
1355
1942
  ensureDefaultCamera() {
1356
1943
  if (this.cameras.size === 0 || this._activeCameras.length === 0) {
1357
- const screenRes = this._rendererManager?.screenResolution || new Vector25(window.innerWidth, window.innerHeight);
1944
+ const screenRes = this._rendererManager?.screenResolution || new Vector24(window.innerWidth, window.innerHeight);
1358
1945
  const defaultCam = new ZylemCamera(Perspectives.ThirdPerson, screenRes);
1359
1946
  this.addCamera(defaultCam, "default");
1360
1947
  return defaultCam;