diff --git a/.gitignore b/.gitignore
index 95a3b9f6..f716d48c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,7 +17,11 @@ export_presets.cfg
.godot/
-# Docs specic ignores
+# CSharp Dev
+*.DotSettings
+*.csproj.old
+
+# Docs specific ignores
#/docs/
node_modules
cache
diff --git a/PhantomCamera.csproj b/PhantomCamera.csproj
new file mode 100644
index 00000000..30e9280b
--- /dev/null
+++ b/PhantomCamera.csproj
@@ -0,0 +1,12 @@
+
+
+ net8.0
+ net7.0
+ net8.0
+ true
+ 11.0
+
+
+
+
+
\ No newline at end of file
diff --git a/PhantomCamera.sln b/PhantomCamera.sln
new file mode 100644
index 00000000..4a0d21cc
--- /dev/null
+++ b/PhantomCamera.sln
@@ -0,0 +1,19 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 2012
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhantomCamera", "PhantomCamera.csproj", "{74ABEBAD-177B-4436-87F5-58340D7F4EE5}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ ExportDebug|Any CPU = ExportDebug|Any CPU
+ ExportRelease|Any CPU = ExportRelease|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {74ABEBAD-177B-4436-87F5-58340D7F4EE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {74ABEBAD-177B-4436-87F5-58340D7F4EE5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {74ABEBAD-177B-4436-87F5-58340D7F4EE5}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU
+ {74ABEBAD-177B-4436-87F5-58340D7F4EE5}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU
+ {74ABEBAD-177B-4436-87F5-58340D7F4EE5}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU
+ {74ABEBAD-177B-4436-87F5-58340D7F4EE5}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU
+ EndGlobalSection
+EndGlobal
diff --git a/addons/phantom_camera/csharp/PCam3D.cs b/addons/phantom_camera/csharp/PCam3D.cs
new file mode 100644
index 00000000..d387cb47
--- /dev/null
+++ b/addons/phantom_camera/csharp/PCam3D.cs
@@ -0,0 +1,188 @@
+using Godot;
+
+namespace PhantomCamera;
+
+public class PCam3D
+{
+ public delegate void PCam3DEventHandler();
+ public delegate void PCam3DTweenInterruptedEventHandler(Variant pCam3D);
+
+ public event PCam3DEventHandler BecameActive;
+ public event PCam3DEventHandler BecameInactive;
+ public event PCam3DEventHandler FollowTargetChanged;
+ public event PCam3DEventHandler LookAtTargetChanged;
+ public event PCam3DEventHandler DeadZoneChanged;
+ public event PCam3DEventHandler TweenStarted;
+ public event PCam3DEventHandler IsTweening;
+ public event PCam3DEventHandler TweenCompleted;
+ public event PCam3DTweenInterruptedEventHandler TweenInterrupted;
+
+ private readonly GodotObject _godotObject;
+
+ private readonly Callable _callableBecameActive;
+ private readonly Callable _callableBecameInactive;
+ private readonly Callable _callableFollowTargetChanged;
+ private readonly Callable _callableLookAtTargetChanged;
+ private readonly Callable _callableDeadZoneChanged;
+ private readonly Callable _callableTweenStarted;
+ private readonly Callable _callableIsTweening;
+ private readonly Callable _callableTweenCompleted;
+ private readonly Callable _callableTweenInterrupted;
+
+ public FollowMode FollowMode => _godotObject.Call(MethodName.GetFollowMode).As();
+
+ public LookAtMode LookAtMode => _godotObject.Call(MethodName.GetLookAtMode).As();
+
+ public bool IsActive => _godotObject.Call(MethodName.IsActive).As();
+
+ public int Priority
+ {
+ get => _godotObject.Call(MethodName.GetPriority).As();
+ set => _godotObject.Call(MethodName.SetPriority, value);
+ }
+
+ public Vector3 ThirdPersonRotation
+ {
+ get => _godotObject.Call(MethodName.GetThirdPersonRotation).As();
+ set => _godotObject.Call(MethodName.SetThirdPersonRotation, value);
+ }
+
+ public Vector3 ThirdPersonRotationDegrees
+ {
+ get => _godotObject.Call(MethodName.GetThirdPersonRotationDegrees).As();
+ set => _godotObject.Call(MethodName.SetThirdPersonRotationDegrees, value);
+ }
+
+ public Quaternion ThirdPersonQuaternion
+ {
+ get => _godotObject.Call(MethodName.GetThirdPersonQuaternion).As();
+ set => _godotObject.Call(MethodName.SetThirdPersonQuaternion, value);
+ }
+
+ public float SpringLength
+ {
+ get => _godotObject.Call(MethodName.GetSpringLength).As();
+ set => _godotObject.Call(MethodName.SetSpringLength, value);
+ }
+
+ public static PCam3D FromScript(string path) => GD.Load(path).AsPCam3D();
+
+ public PCam3D(GodotObject godotObject)
+ {
+ _godotObject = godotObject;
+
+ _callableBecameActive = Callable.From(OnBecameActive);
+ _callableBecameInactive = Callable.From(OnBecameInactive);
+ _callableFollowTargetChanged = Callable.From(OnFollowTargetChanged);
+ _callableLookAtTargetChanged = Callable.From(OnLookAtTargetChanged);
+ _callableDeadZoneChanged = Callable.From(OnDeadZoneChanged);
+ _callableTweenStarted = Callable.From(OnTweenStarted);
+ _callableIsTweening = Callable.From(OnIsTweening);
+ _callableTweenCompleted = Callable.From(OnTweenCompleted);
+ _callableTweenInterrupted = Callable.From(OnTweenInterrupted);
+
+ _godotObject.Connect(SignalName.BecameActive, _callableBecameActive);
+ _godotObject.Connect(SignalName.BecameInactive, _callableBecameInactive);
+ _godotObject.Connect(SignalName.FollowTargetChanged, _callableFollowTargetChanged);
+ _godotObject.Connect(SignalName.LookAtTargetChanged, _callableLookAtTargetChanged);
+ _godotObject.Connect(SignalName.DeadZoneChanged, _callableDeadZoneChanged);
+ _godotObject.Connect(SignalName.TweenStarted, _callableTweenStarted);
+ _godotObject.Connect(SignalName.IsTweening, _callableIsTweening);
+ _godotObject.Connect(SignalName.TweenCompleted, _callableTweenCompleted);
+ _godotObject.Connect(SignalName.TweenInterrupted, _callableTweenInterrupted);
+ }
+
+ ~PCam3D()
+ {
+ _godotObject.Disconnect(SignalName.BecameActive, _callableBecameActive);
+ _godotObject.Disconnect(SignalName.BecameInactive, _callableBecameInactive);
+ _godotObject.Disconnect(SignalName.FollowTargetChanged, _callableFollowTargetChanged);
+ _godotObject.Disconnect(SignalName.LookAtTargetChanged, _callableLookAtTargetChanged);
+ _godotObject.Disconnect(SignalName.DeadZoneChanged, _callableDeadZoneChanged);
+ _godotObject.Disconnect(SignalName.TweenStarted, _callableTweenStarted);
+ _godotObject.Disconnect(SignalName.IsTweening, _callableIsTweening);
+ _godotObject.Disconnect(SignalName.TweenCompleted, _callableTweenCompleted);
+ _godotObject.Disconnect(SignalName.TweenInterrupted, _callableTweenInterrupted);
+ }
+
+ protected virtual void OnBecameActive()
+ {
+ BecameActive?.Invoke();
+ }
+
+ protected virtual void OnBecameInactive()
+ {
+ BecameInactive?.Invoke();
+ }
+
+ protected virtual void OnFollowTargetChanged()
+ {
+ FollowTargetChanged?.Invoke();
+ }
+
+ protected virtual void OnLookAtTargetChanged()
+ {
+ LookAtTargetChanged?.Invoke();
+ }
+
+ protected virtual void OnDeadZoneChanged()
+ {
+ DeadZoneChanged?.Invoke();
+ }
+
+ protected virtual void OnTweenStarted()
+ {
+ TweenStarted?.Invoke();
+ }
+
+ protected virtual void OnIsTweening()
+ {
+ IsTweening?.Invoke();
+ }
+
+ protected virtual void OnTweenInterrupted(Variant pCam3D)
+ {
+ TweenInterrupted?.Invoke(pCam3D);
+ }
+
+ protected virtual void OnTweenCompleted()
+ {
+ TweenCompleted?.Invoke();
+ }
+
+
+ public static class MethodName
+ {
+ public const string GetFollowMode = "get_follow_mode";
+ public const string GetLookAtMode = "get_look_at_mode";
+ public const string IsActive = "is_active";
+
+ public const string GetPriority = "get_priority";
+ public const string SetPriority = "set_priority";
+
+ public const string GetThirdPersonRotation = "get_third_person_rotation";
+ public const string SetThirdPersonRotation = "set_third_person_rotation";
+
+ public const string GetThirdPersonRotationDegrees = "get_third_person_rotation_degrees";
+ public const string SetThirdPersonRotationDegrees = "set_third_person_rotation_degrees";
+
+ public const string GetThirdPersonQuaternion = "get_third_person_quaternion";
+ public const string SetThirdPersonQuaternion = "set_third_person_quaternion";
+
+ public const string GetSpringLength = "get_spring_length";
+ public const string SetSpringLength = "set_spring_length";
+ }
+
+ public static class SignalName
+ {
+ public const string BecameActive = "became_active";
+ public const string BecameInactive = "became_inactive";
+ public const string FollowTargetChanged = "follow_target_changed";
+ public const string LookAtTargetChanged = "look_at_target_changed";
+ public const string DeadZoneChanged = "dead_zone_changed";
+ public const string TweenStarted = "tween_started";
+ public const string IsTweening = "is_tweening";
+ public const string TweenInterrupted = "tween_interrupted";
+ public const string TweenCompleted = "tween_completed";
+ }
+}
\ No newline at end of file
diff --git a/addons/phantom_camera/csharp/PhantomCamera.cs b/addons/phantom_camera/csharp/PhantomCamera.cs
new file mode 100644
index 00000000..3ea3a960
--- /dev/null
+++ b/addons/phantom_camera/csharp/PhantomCamera.cs
@@ -0,0 +1,35 @@
+using Godot;
+
+namespace PhantomCamera;
+
+public static class GodotExtension
+{
+ public static PCam3D AsPCam3D(this GodotObject godotObject)
+ {
+ return new PCam3D(godotObject);
+ }
+
+ public static PCam3D AsPCam3D(this GDScript godotScript)
+ {
+ return new PCam3D(godotScript.New().AsGodotObject());
+ }
+}
+
+public enum FollowMode
+{
+ None,
+ Glued,
+ Simple,
+ Group,
+ Path,
+ Framed,
+ ThirdPerson
+}
+
+public enum LookAtMode
+{
+ None,
+ Mimic,
+ Simple,
+ Group
+}
\ No newline at end of file
diff --git a/addons/phantom_camera/examples/textures/3D/checker_pattern_dark.png.import b/addons/phantom_camera/examples/textures/3D/checker_pattern_dark.png.import
index 98915e8d..cfcc34e5 100644
--- a/addons/phantom_camera/examples/textures/3D/checker_pattern_dark.png.import
+++ b/addons/phantom_camera/examples/textures/3D/checker_pattern_dark.png.import
@@ -4,16 +4,15 @@ importer="texture"
type="CompressedTexture2D"
uid="uid://c7ja4woxol8yc"
path.bptc="res://.godot/imported/checker_pattern_dark.png-70cedad2d3abf4ad6166d6614eefa7fb.bptc.ctex"
-path.astc="res://.godot/imported/checker_pattern_dark.png-70cedad2d3abf4ad6166d6614eefa7fb.astc.ctex"
metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
+"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
[deps]
source_file="res://addons/phantom_camera/examples/textures/3D/checker_pattern_dark.png"
-dest_files=["res://.godot/imported/checker_pattern_dark.png-70cedad2d3abf4ad6166d6614eefa7fb.bptc.ctex", "res://.godot/imported/checker_pattern_dark.png-70cedad2d3abf4ad6166d6614eefa7fb.astc.ctex"]
+dest_files=["res://.godot/imported/checker_pattern_dark.png-70cedad2d3abf4ad6166d6614eefa7fb.bptc.ctex"]
[params]
diff --git a/addons/phantom_camera/icons/phantom_camera_gizmo.svg.import b/addons/phantom_camera/icons/phantom_camera_gizmo.svg.import
index e89d99c5..7b49608d 100644
--- a/addons/phantom_camera/icons/phantom_camera_gizmo.svg.import
+++ b/addons/phantom_camera/icons/phantom_camera_gizmo.svg.import
@@ -4,16 +4,15 @@ importer="texture"
type="CompressedTexture2D"
uid="uid://e36npe2rbxyg"
path.s3tc="res://.godot/imported/phantom_camera_gizmo.svg-ba1aacb9b1c5f4ef401d3bd3697a542b.s3tc.ctex"
-path.etc2="res://.godot/imported/phantom_camera_gizmo.svg-ba1aacb9b1c5f4ef401d3bd3697a542b.etc2.ctex"
metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
+"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
[deps]
source_file="res://addons/phantom_camera/icons/phantom_camera_gizmo.svg"
-dest_files=["res://.godot/imported/phantom_camera_gizmo.svg-ba1aacb9b1c5f4ef401d3bd3697a542b.s3tc.ctex", "res://.godot/imported/phantom_camera_gizmo.svg-ba1aacb9b1c5f4ef401d3bd3697a542b.etc2.ctex"]
+dest_files=["res://.godot/imported/phantom_camera_gizmo.svg-ba1aacb9b1c5f4ef401d3bd3697a542b.s3tc.ctex"]
[params]
diff --git a/addons/phantom_camera/scripts/PhantomCamera.cs b/addons/phantom_camera/scripts/PhantomCamera.cs
new file mode 100644
index 00000000..2da8606f
--- /dev/null
+++ b/addons/phantom_camera/scripts/PhantomCamera.cs
@@ -0,0 +1,28 @@
+using Godot;
+using PhantomCamera.Cameras;
+using PhantomCamera.Hosts;
+
+namespace PhantomCamera;
+
+public static class PhantomCameraExtension
+{
+ public static PhantomCamera3D AsPhantomCamera3D(this Node3D node3D)
+ {
+ return new PhantomCamera3D(node3D);
+ }
+
+ public static PhantomCamera2D AsPhantomCamera2D(this Node2D node2D)
+ {
+ return new PhantomCamera2D(node2D);
+ }
+
+ public static PhantomCameraHost AsPhantomCameraHost(this Node node)
+ {
+ return new PhantomCameraHost(node);
+ }
+}
+
+
+
+
+
diff --git a/addons/phantom_camera/scripts/managers/PhantomCameraManager.cs b/addons/phantom_camera/scripts/managers/PhantomCameraManager.cs
new file mode 100644
index 00000000..9d41647e
--- /dev/null
+++ b/addons/phantom_camera/scripts/managers/PhantomCameraManager.cs
@@ -0,0 +1,34 @@
+using System.Linq;
+using Godot;
+using PhantomCamera.Cameras;
+using PhantomCamera.Hosts;
+
+#nullable enable
+
+namespace PhantomCamera.Managers;
+
+public static class PhantomCameraManager
+{
+ private static GodotObject? _instance;
+
+ public static GodotObject Instance => _instance ??= Engine.GetSingleton("PhantomCameraManager");
+
+ public static PhantomCamera2D[] PhantomCamera2Ds =>
+ Instance.Call(MethodName.GetPhantomCamera2Ds).AsGodotArray()
+ .Select(node => new PhantomCamera2D(node)).ToArray();
+
+ public static PhantomCamera3D[] PhantomCamera3Ds =>
+ Instance.Call(MethodName.GetPhantomCamera3Ds).AsGodotArray()
+ .Select(node => new PhantomCamera3D(node)).ToArray();
+
+ public static PhantomCameraHost[] PhantomCameraHosts =>
+ Instance.Call(MethodName.GetPhantomCameraHosts).AsGodotArray()
+ .Select(node => new PhantomCameraHost(node)).ToArray();
+
+ public static class MethodName
+ {
+ public const string GetPhantomCamera2Ds = "get_phantom_camera_2ds";
+ public const string GetPhantomCamera3Ds = "get_phantom_camera_3ds";
+ public const string GetPhantomCameraHosts = "get_phantom_camera_hosts";
+ }
+}
\ No newline at end of file
diff --git a/addons/phantom_camera/scripts/managers/phantom_camera_manager.gd b/addons/phantom_camera/scripts/managers/phantom_camera_manager.gd
index 8a53e059..999df238 100644
--- a/addons/phantom_camera/scripts/managers/phantom_camera_manager.gd
+++ b/addons/phantom_camera/scripts/managers/phantom_camera_manager.gd
@@ -18,6 +18,9 @@ var phantom_camera_3ds: Array: ## Note: To support disable_3d export templates f
return _phantom_camera_3d_list
var _phantom_camera_3d_list: Array ## Note: To support disable_3d export templates for 2D projects, this is purposely not strongly typed.
+func _ready():
+ if not Engine.has_singleton(PHANTOM_CAMERA_CONSTS.PCAM_MANAGER_NODE_NAME):
+ Engine.register_singleton(PHANTOM_CAMERA_CONSTS.PCAM_MANAGER_NODE_NAME, self)
func _enter_tree():
Engine.physics_jitter_fix = 0
diff --git a/addons/phantom_camera/scripts/phantom_camera/PhantomCamera.cs b/addons/phantom_camera/scripts/phantom_camera/PhantomCamera.cs
new file mode 100644
index 00000000..d6b1b820
--- /dev/null
+++ b/addons/phantom_camera/scripts/phantom_camera/PhantomCamera.cs
@@ -0,0 +1,239 @@
+using Godot;
+using PhantomCamera.Hosts;
+using PhantomCamera.Resources;
+
+#nullable enable
+
+namespace PhantomCamera.Cameras;
+
+public enum FollowMode
+{
+ None,
+ Glued,
+ Simple,
+ Group,
+ Path,
+ Framed,
+ ThirdPerson
+}
+
+public enum LookAtMode
+{
+ None,
+ Mimic,
+ Simple,
+ Group
+}
+
+public enum InactiveUpdateMode
+{
+ Always,
+ Never
+}
+
+public abstract class PhantomCamera
+{
+ protected readonly GodotObject Node;
+
+ public delegate void BecameActiveEventHandler();
+ public delegate void BecameInactiveEventHandler();
+ public delegate void FollowTargetChangedEventHandler();
+ public delegate void DeadZoneChangedEventHandler();
+ public delegate void TweenStartedEventHandler();
+ public delegate void IsTweeningEventHandler();
+ public delegate void TweenCompletedEventHandler();
+
+ public event BecameActiveEventHandler? BecameActive;
+ public event BecameInactiveEventHandler? BecameInactive;
+ public event FollowTargetChangedEventHandler? FollowTargetChanged;
+ public event DeadZoneChangedEventHandler? DeadZoneChanged;
+ public event TweenStartedEventHandler? TweenStarted;
+ public event IsTweeningEventHandler? IsTweening;
+ public event TweenCompletedEventHandler? TweenCompleted;
+
+ private readonly Callable _callableBecameActive;
+ private readonly Callable _callableBecameInactive;
+ private readonly Callable _callableFollowTargetChanged;
+ private readonly Callable _callableDeadZoneChanged;
+ private readonly Callable _callableTweenStarted;
+ private readonly Callable _callableIsTweening;
+ private readonly Callable _callableTweenCompleted;
+
+ public int Priority
+ {
+ get => (int)Node.Call(MethodName.GetPriority);
+ set => Node.Call(MethodName.SetPriority, value);
+ }
+
+ public FollowMode FollowMode => (FollowMode)(int)Node.Call(MethodName.GetFollowMode);
+
+ public bool IsActive => (bool)Node.Call(MethodName.IsActive);
+
+ public PhantomCameraHost PhantomCameraHostOwner
+ {
+ get => new((Node)Node.Call(MethodName.GetPCamHostOwner));
+ set => Node.Call(MethodName.SetPCamHostOwner, value.Node);
+ }
+
+ public bool FollowDamping
+ {
+ get => (bool)Node.Call(MethodName.GetFollowDamping);
+ set => Node.Call(MethodName.SetFollowDamping, value);
+ }
+
+ public float DeadZoneWidth
+ {
+ get => (float)Node.Get(PropertyName.DeadZoneWidth);
+ set => Node.Set(PropertyName.DeadZoneWidth, value);
+ }
+
+ public float DeadZoneHeight
+ {
+ get => (float)Node.Get(PropertyName.DeadZoneHeight);
+ set => Node.Set(PropertyName.DeadZoneHeight, value);
+ }
+
+ public PhantomCameraTween TweenResource
+ {
+ get => new((Resource)Node.Call(MethodName.GetTweenResource));
+ set => Node.Call(MethodName.SetTweenResource, (GodotObject)value.Resource);
+ }
+
+ public bool TweenSkip
+ {
+ get => (bool)Node.Call(MethodName.GetTweenSkip);
+ set => Node.Call(MethodName.SetTweenSkip, value);
+ }
+
+ public float TweenDuration
+ {
+ get => (float)Node.Call(MethodName.GetTweenDuration);
+ set => Node.Call(MethodName.SetTweenDuration, value);
+ }
+
+ public TransitionType TweenTransition
+ {
+ get => (TransitionType)(int)Node.Call(MethodName.GetTweenTransition);
+ set => Node.Call(MethodName.GetTweenTransition, (int)value);
+ }
+
+ public EaseType TweenEase
+ {
+ get => (EaseType)(int)Node.Call(MethodName.GetTweenEase);
+ set => Node.Call(MethodName.GetTweenEase, (int)value);
+ }
+
+ public bool TweenOnLoad
+ {
+ get => (bool)Node.Call(MethodName.GetTweenOnLoad);
+ set => Node.Call(MethodName.SetTweenOnLoad, value);
+ }
+
+ public InactiveUpdateMode InactiveUpdateMode
+ {
+ get => (InactiveUpdateMode)(int)Node.Call(MethodName.GetInactiveUpdateMode);
+ set => Node.Call(MethodName.SetInactiveUpdateMode, (int)value);
+ }
+
+ protected PhantomCamera(GodotObject phantomCameraNode)
+ {
+ Node = phantomCameraNode;
+
+ _callableBecameActive = Callable.From(() => BecameActive?.Invoke());
+ _callableBecameInactive = Callable.From(() => BecameInactive?.Invoke());
+ _callableFollowTargetChanged = Callable.From(() => FollowTargetChanged?.Invoke());
+ _callableDeadZoneChanged = Callable.From(() => DeadZoneChanged?.Invoke());
+ _callableTweenStarted = Callable.From(() => TweenStarted?.Invoke());
+ _callableIsTweening = Callable.From(() => IsTweening?.Invoke());
+ _callableTweenCompleted = Callable.From(() => TweenCompleted?.Invoke());
+
+ Node.Connect(SignalName.BecameActive, _callableBecameActive);
+ Node.Connect(SignalName.BecameInactive, _callableBecameInactive);
+ Node.Connect(SignalName.FollowTargetChanged, _callableFollowTargetChanged);
+ Node.Connect(SignalName.DeadZoneChanged, _callableDeadZoneChanged);
+ Node.Connect(SignalName.TweenStarted, _callableTweenStarted);
+ Node.Connect(SignalName.IsTweening, _callableIsTweening);
+ Node.Connect(SignalName.TweenCompleted, _callableTweenCompleted);
+ }
+
+ ~PhantomCamera()
+ {
+ Node.Disconnect(SignalName.BecameActive, _callableBecameActive);
+ Node.Disconnect(SignalName.BecameInactive, _callableBecameInactive);
+ Node.Disconnect(SignalName.FollowTargetChanged, _callableFollowTargetChanged);
+ Node.Disconnect(SignalName.DeadZoneChanged, _callableDeadZoneChanged);
+ Node.Disconnect(SignalName.TweenStarted, _callableTweenStarted);
+ Node.Disconnect(SignalName.IsTweening, _callableIsTweening);
+ Node.Disconnect(SignalName.TweenCompleted, _callableTweenCompleted);
+ }
+
+ public static class MethodName
+ {
+ public const string GetFollowMode = "get_follow_mode";
+ public const string IsActive = "is_active";
+
+ public const string GetPriority = "get_priority";
+ public const string SetPriority = "set_priority";
+
+ public const string GetPCamHostOwner = "get_pcam_host_owner";
+ public const string SetPCamHostOwner = "set_pcam_host_owner";
+
+ public const string GetFollowTarget = "get_follow_target";
+ public const string SetFollowTarget = "set_follow_target";
+
+ public const string GetFollowTargets = "get_follow_targets";
+ public const string SetFollowTargets = "set_follow_targets";
+
+ public const string GetFollowPath = "get_follow_path";
+ public const string SetFollowPath = "set_follow_path";
+
+ public const string GetFollowOffset = "get_follow_offset";
+ public const string SetFollowOffset = "set_follow_offset";
+
+ public const string GetFollowDamping = "get_follow_damping";
+ public const string SetFollowDamping = "set_follow_damping";
+
+ public const string GetFollowDampingValue = "get_follow_damping_value";
+ public const string SetFollowDampingValue = "set_follow_damping_value";
+
+ public const string GetTweenResource = "get_tween_resource";
+ public const string SetTweenResource = "set_tween_resource";
+
+ public const string GetTweenSkip = "get_tween_skip";
+ public const string SetTweenSkip = "set_tween_skip";
+
+ public const string GetTweenDuration = "get_tween_duration";
+ public const string SetTweenDuration = "set_tween_duration";
+
+ public const string GetTweenTransition = "get_tween_transition";
+ public const string SetTweenTransition = "set_tween_transition";
+
+ public const string GetTweenEase = "get_tween_ease";
+ public const string SetTweenEase = "set_tween_ease";
+
+ public const string GetTweenOnLoad = "get_tween_on_load";
+ public const string SetTweenOnLoad = "set_tween_on_load";
+
+ public const string GetInactiveUpdateMode = "get_inactive_update_mode";
+ public const string SetInactiveUpdateMode = "set_inactive_update_mode";
+ }
+
+ public static class PropertyName
+ {
+ public const string DeadZoneWidth = "dead_zone_width";
+ public const string DeadZoneHeight = "dead_zone_height";
+ }
+
+ public static class SignalName
+ {
+ public const string BecameActive = "became_active";
+ public const string BecameInactive = "became_inactive";
+ public const string FollowTargetChanged = "follow_target_changed";
+ public const string LookAtTargetChanged = "look_at_target_changed";
+ public const string DeadZoneChanged = "dead_zone_changed";
+ public const string TweenStarted = "tween_started";
+ public const string IsTweening = "is_tweening";
+ public const string TweenCompleted = "tween_completed";
+ public const string TweenInterrupted = "tween_interrupted";
+ }
+}
\ No newline at end of file
diff --git a/addons/phantom_camera/scripts/phantom_camera/PhantomCamera2D.cs b/addons/phantom_camera/scripts/phantom_camera/PhantomCamera2D.cs
new file mode 100644
index 00000000..820578aa
--- /dev/null
+++ b/addons/phantom_camera/scripts/phantom_camera/PhantomCamera2D.cs
@@ -0,0 +1,239 @@
+using System.Linq;
+using Godot;
+
+#nullable enable
+
+namespace PhantomCamera.Cameras;
+
+public class PhantomCamera2D : PhantomCamera
+{
+ public Node2D Node2D => (Node2D)Node;
+
+ public delegate void TweenInterruptedEventHandler(Node2D pCam);
+
+ public event TweenInterruptedEventHandler? TweenInterrupted;
+
+ private readonly Callable _callableTweenInterrupted;
+
+ public Node2D FollowTarget
+ {
+ get => (Node2D)Node2D.Call(PhantomCamera.MethodName.GetFollowTarget);
+ set => Node2D.Call(PhantomCamera.MethodName.SetFollowTarget, value);
+ }
+
+ public Node2D[] FollowTargets
+ {
+ get => Node2D.Call(PhantomCamera.MethodName.GetFollowTargets).AsGodotArray().ToArray();
+ set => Node2D.Call(PhantomCamera.MethodName.SetFollowTargets, value);
+ }
+
+ public Path2D FollowPath
+ {
+ get => (Path2D)Node2D.Call(PhantomCamera.MethodName.GetFollowPath);
+ set => Node2D.Call(PhantomCamera.MethodName.SetFollowPath, value);
+ }
+
+ public Vector2 FollowOffset
+ {
+ get => (Vector2)Node2D.Call(PhantomCamera.MethodName.GetFollowOffset);
+ set => Node2D.Call(PhantomCamera.MethodName.SetFollowOffset, value);
+ }
+
+ public Vector2 FollowDampingValue
+ {
+ get => (Vector2)Node2D.Call(PhantomCamera.MethodName.GetFollowDampingValue);
+ set => Node2D.Call(PhantomCamera.MethodName.SetFollowDampingValue, value);
+ }
+
+ public Vector2 Zoom
+ {
+ get => (Vector2)Node.Call(MethodName.GetZoom);
+ set => Node.Call(MethodName.SetZoom, value);
+ }
+
+ public bool SnapToPixel
+ {
+ get => (bool)Node.Call(MethodName.GetSnapToPixel);
+ set => Node.Call(MethodName.SetSnapToPixel, value);
+ }
+
+ public int LimitLeft
+ {
+ get => (int)Node.Call(MethodName.GetLimitLeft);
+ set => Node.Call(MethodName.SetLimitLeft, value);
+ }
+
+ public int LimitTop
+ {
+ get => (int)Node.Call(MethodName.GetLimitTop);
+ set => Node.Call(MethodName.SetLimitTop, value);
+ }
+
+ public int LimitRight
+ {
+ get => (int)Node.Call(MethodName.GetLimitRight);
+ set => Node.Call(MethodName.SetLimitRight, value);
+ }
+
+ public int LimitBottom
+ {
+ get => (int)Node.Call(MethodName.GetLimitBottom);
+ set => Node.Call(MethodName.SetLimitBottom, value);
+ }
+
+ public Vector4I LimitMargin
+ {
+ get => (Vector4I)Node.Call(MethodName.GetLimitMargin);
+ set => Node.Call(MethodName.SetLimitMargin, value);
+ }
+
+ public bool AutoZoom
+ {
+ get => (bool)Node2D.Call(MethodName.GetAutoZoom);
+ set => Node2D.Call(MethodName.SetAutoZoom, value);
+ }
+
+ public float AutoZoomMin
+ {
+ get => (float)Node2D.Call(MethodName.GetAutoZoomMin);
+ set => Node2D.Call(MethodName.SetAutoZoomMin, value);
+ }
+
+ public float AutoZoomMax
+ {
+ get => (float)Node2D.Call(MethodName.GetAutoZoomMax);
+ set => Node2D.Call(MethodName.SetAutoZoomMax, value);
+ }
+
+ public Vector4 AutoZoomMargin
+ {
+ get => (Vector4)Node2D.Call(MethodName.GetAutoZoomMargin);
+ set => Node2D.Call(MethodName.SetAutoZoomMargin, value);
+ }
+
+ public bool DrawLimits
+ {
+ get => (bool)Node2D.Get(PropertyName.DrawLimits);
+ set => Node2D.Set(PropertyName.DrawLimits, value);
+ }
+
+ public static PhantomCamera2D FromScript(string path) => new(GD.Load(path).New().AsGodotObject());
+ public static PhantomCamera2D FromScript(GDScript script) => new(script.New().AsGodotObject());
+
+ public PhantomCamera2D(GodotObject phantomCameraNode) : base(phantomCameraNode)
+ {
+ _callableTweenInterrupted = Callable.From(pCam => TweenInterrupted?.Invoke(pCam));
+ Node.Connect(SignalName.TweenInterrupted, _callableTweenInterrupted);
+ }
+
+ ~PhantomCamera2D()
+ {
+ Node.Disconnect(SignalName.TweenInterrupted, _callableTweenInterrupted);
+ }
+
+ public void SetLimitTarget(TileMap tileMap)
+ {
+ Node.Call(MethodName.SetLimitTarget, tileMap.GetPath());
+ }
+
+ public void SetLimitTarget(TileMapLayer tileMapLayer)
+ {
+ Node.Call(MethodName.SetLimitTarget, tileMapLayer.GetPath());
+ }
+
+ public void SetLimitTarget(CollisionShape2D shape2D)
+ {
+ Node.Call(MethodName.SetLimitTarget, shape2D.GetPath());
+ }
+
+ public LimitTargetQueryResult? GetLimitTarget()
+ {
+ var result = (NodePath)Node.Call(MethodName.GetLimitTarget);
+ return result.IsEmpty ? null : new LimitTargetQueryResult(Node2D.GetNode(result));
+ }
+
+ public void SetLimit(Side side, int value)
+ {
+ Node.Call(MethodName.SetLimit, (int)side, value);
+ }
+
+ public int GetLimit(Side side)
+ {
+ return (int)Node.Call(MethodName.GetLimit, (int)side);
+ }
+
+ public new static class MethodName
+ {
+ public const string GetZoom = "get_zoom";
+ public const string SetZoom = "set_zoom";
+
+ public const string GetSnapToPixel = "get_snap_to_pixel";
+ public const string SetSnapToPixel = "set_snap_to_pixel";
+
+ public const string GetLimit = "get_limit";
+ public const string SetLimit = "set_limit";
+
+ public const string GetLimitLeft = "get_limit_left";
+ public const string SetLimitLeft = "set_limit_left";
+
+ public const string GetLimitTop = "get_limit_top";
+ public const string SetLimitTop = "set_limit_top";
+
+ public const string GetLimitRight = "get_limit_right";
+ public const string SetLimitRight = "set_limit_right";
+
+ public const string GetLimitBottom = "get_limit_bottom";
+ public const string SetLimitBottom = "set_limit_bottom";
+
+ public const string GetLimitTarget = "get_limit_target";
+ public const string SetLimitTarget = "set_limit_target";
+
+ public const string GetLimitMargin = "get_limit_margin";
+ public const string SetLimitMargin = "set_limit_margin";
+
+ public const string GetAutoZoom = "get_auto_zoom";
+ public const string SetAutoZoom = "set_auto_zoom";
+
+ public const string GetAutoZoomMin = "get_auto_zoom_min";
+ public const string SetAutoZoomMin = "set_auto_zoom_min";
+
+ public const string GetAutoZoomMax = "get_auto_zoom_max";
+ public const string SetAutoZoomMax = "set_auto_zoom_max";
+
+ public const string GetAutoZoomMargin = "get_auto_zoom_margin";
+ public const string SetAutoZoomMargin = "set_auto_zoom_margin";
+ }
+
+ public new static class PropertyName
+ {
+ public const string DrawLimits = "draw_limits";
+ }
+}
+
+public class LimitTargetQueryResult
+{
+ private readonly GodotObject _obj;
+
+ public bool IsTileMap => _obj.IsClass("TileMap");
+
+ public bool IsTileMapLayer => _obj.IsClass("TileMapLayer");
+
+ public bool IsCollisionShape2D => _obj.IsClass("CollisionShape2D");
+
+ public LimitTargetQueryResult(GodotObject godotObject) => _obj = godotObject;
+
+ public TileMap? AsTileMap()
+ {
+ return IsTileMap ? (TileMap)_obj : null;
+ }
+
+ public TileMapLayer? AsTileMapLayer()
+ {
+ return IsTileMapLayer ? (TileMapLayer)_obj : null;
+ }
+
+ public CollisionShape2D? AsCollisionShape2D()
+ {
+ return IsCollisionShape2D ? (CollisionShape2D)_obj : null;
+ }
+}
\ No newline at end of file
diff --git a/addons/phantom_camera/scripts/phantom_camera/PhantomCamera3D.cs b/addons/phantom_camera/scripts/phantom_camera/PhantomCamera3D.cs
new file mode 100644
index 00000000..8502c6bf
--- /dev/null
+++ b/addons/phantom_camera/scripts/phantom_camera/PhantomCamera3D.cs
@@ -0,0 +1,347 @@
+using System.Linq;
+using Godot;
+using PhantomCamera.Resources;
+
+#nullable enable
+
+namespace PhantomCamera.Cameras;
+
+public class PhantomCamera3D : PhantomCamera
+{
+ public Node3D Node3D => (Node3D)Node;
+
+ public delegate void LookAtTargetChangedEventHandler();
+ public delegate void TweenInterruptedEventHandler(Node3D pCam);
+
+ public event LookAtTargetChangedEventHandler? LookAtTargetChanged;
+ public event TweenInterruptedEventHandler? TweenInterrupted;
+
+ private readonly Callable _callableLookAtTargetChanged;
+ private readonly Callable _callableTweenInterrupted;
+
+ public Node3D FollowTarget
+ {
+ get => (Node3D)Node3D.Call(PhantomCamera.MethodName.GetFollowTarget);
+ set => Node3D.Call(PhantomCamera.MethodName.SetFollowTarget, value);
+ }
+
+ public Node3D[] FollowTargets
+ {
+ get => Node3D.Call(PhantomCamera.MethodName.GetFollowTargets).AsGodotArray().ToArray();
+ set => Node3D.Call(PhantomCamera.MethodName.SetFollowTargets, value);
+ }
+
+ public Path3D FollowPath
+ {
+ get => (Path3D)Node3D.Call(PhantomCamera.MethodName.GetFollowPath);
+ set => Node3D.Call(PhantomCamera.MethodName.SetFollowPath, value);
+ }
+
+ public Vector3 FollowOffset
+ {
+ get => (Vector3)Node3D.Call(PhantomCamera.MethodName.GetFollowOffset);
+ set => Node3D.Call(PhantomCamera.MethodName.SetFollowOffset, value);
+ }
+
+ public Vector3 FollowDampingValue
+ {
+ get => (Vector3)Node3D.Call(PhantomCamera.MethodName.GetFollowDampingValue);
+ set => Node3D.Call(PhantomCamera.MethodName.SetFollowDampingValue, value);
+ }
+
+ public LookAtMode LookAtMode => (LookAtMode)(int)Node.Call(MethodName.GetLookAtMode);
+
+ public Camera3DResource Camera3DResource
+ {
+ get => new((Resource)Node.Call(MethodName.GetCamera3DResource));
+ set => Node.Call(MethodName.SetCamera3DResource, value.Resource);
+ }
+
+ public Vector3 ThirdPersonRotation
+ {
+ get => (Vector3)Node.Call(MethodName.GetThirdPersonRotation);
+ set => Node.Call(MethodName.SetThirdPersonRotation, value);
+ }
+
+ public Vector3 ThirdPersonRotationDegrees
+ {
+ get => (Vector3)Node.Call(MethodName.GetThirdPersonRotationDegrees);
+ set => Node.Call(MethodName.SetThirdPersonRotationDegrees, value);
+ }
+
+ public Quaternion ThirdPersonQuaternion
+ {
+ get => (Quaternion)Node.Call(MethodName.GetThirdPersonQuaternion);
+ set => Node.Call(MethodName.SetThirdPersonQuaternion, value);
+ }
+
+ public float SpringLength
+ {
+ get => (float)Node.Call(MethodName.GetSpringLength);
+ set => Node.Call(MethodName.SetSpringLength, value);
+ }
+
+ public float FollowDistance
+ {
+ get => (float)Node3D.Call(MethodName.GetFollowDistance);
+ set => Node3D.Call(MethodName.SetFollowDistance, value);
+ }
+
+ public bool AutoFollowDistance
+ {
+ get => (bool)Node3D.Call(MethodName.GetAutoFollowDistance);
+ set => Node3D.Call(MethodName.SetAutoFollowDistance, value);
+ }
+
+ public float AutoFollowDistanceMin
+ {
+ get => (float)Node3D.Call(MethodName.GetAutoFollowDistanceMin);
+ set => Node3D.Call(MethodName.SetAutoFollowDistanceMin, value);
+ }
+
+ public float AutoFollowDistanceMax
+ {
+ get => (float)Node3D.Call(MethodName.GetAutoFollowDistanceMax);
+ set => Node3D.Call(MethodName.SetAutoFollowDistanceMax, value);
+ }
+
+ public float AutoFollowDistanceDivisor
+ {
+ get => (float)Node3D.Call(MethodName.GetAutoFollowDistanceDivisor);
+ set => Node3D.Call(MethodName.SetAutoFollowDistanceDivisor, value);
+ }
+
+ public Node3D LookAtTarget
+ {
+ get => (Node3D)Node3D.Call(MethodName.GetLookAtTarget);
+ set => Node3D.Call(MethodName.SetLookAtTarget, value);
+ }
+
+ public Node3D[] LookAtTargets
+ {
+ get => Node3D.Call(MethodName.GetLookAtTargets).AsGodotArray().ToArray();
+ set => Node3D.Call(MethodName.SetLookAtTargets, value);
+ }
+
+ public Vector2 ViewportPosition
+ {
+ get => (Vector2)Node3D.Call(MethodName.GetViewportPosition);
+ set => Node3D.Call(MethodName.SetViewportPosition, value);
+ }
+
+ public int CollisionMask
+ {
+ get => (int)Node3D.Call(MethodName.GetCollisionMask);
+ set => Node3D.Call(MethodName.SetCollisionMask, value);
+ }
+
+ public Shape3D Shape
+ {
+ get => (Shape3D)Node3D.Call(MethodName.GetShape);
+ set => Node3D.Call(MethodName.SetShape, value);
+ }
+
+ public float Margin
+ {
+ get => (float)Node3D.Call(MethodName.GetMargin);
+ set => Node3D.Call(MethodName.SetMargin, value);
+ }
+
+ public Vector3 LookAtOffset
+ {
+ get => (Vector3)Node3D.Call(MethodName.GetLookAtOffset);
+ set => Node3D.Call(MethodName.SetLookAtOffset, value);
+ }
+
+ public bool LookAtDamping
+ {
+ get => (bool)Node3D.Call(MethodName.GetLookAtDamping);
+ set => Node3D.Call(MethodName.SetLookAtDamping, value);
+ }
+
+ public float LookAtDampingValue
+ {
+ get => (float)Node3D.Call(MethodName.GetLookAtDampingValue);
+ set => Node3D.Call(MethodName.SetLookAtDampingValue, value);
+ }
+
+ public int CullMask
+ {
+ get => (int)Node.Call(MethodName.GetCullMask);
+ set => Node.Call(MethodName.SetCullMask, value);
+ }
+
+ public float HOffset
+ {
+ get => (float)Node.Call(MethodName.GetHOffset);
+ set => Node.Call(MethodName.SetHOffset, value);
+ }
+
+ public float VOffset
+ {
+ get => (float)Node.Call(MethodName.GetVOffset);
+ set => Node.Call(MethodName.SetVOffset, value);
+ }
+
+ public ProjectionType Projection
+ {
+ get => (ProjectionType)(int)Node.Call(MethodName.GetProjection);
+ set => Node.Call(MethodName.SetProjection, (int)value);
+ }
+
+ public float Fov
+ {
+ get => (float)Node.Call(MethodName.GetFov);
+ set => Node.Call(MethodName.SetFov, value);
+ }
+
+ public float Size
+ {
+ get => (float)Node.Call(MethodName.GetSize);
+ set => Node.Call(MethodName.SetSize, value);
+ }
+
+ public Vector2 FrustumOffset
+ {
+ get => (Vector2)Node.Call(MethodName.GetFrustumOffset);
+ set => Node.Call(MethodName.SetFrustumOffset, value);
+ }
+
+ public float Far
+ {
+ get => (float)Node.Call(MethodName.GetFar);
+ set => Node.Call(MethodName.SetFar, value);
+ }
+
+ public float Near
+ {
+ get => (float)Node.Call(MethodName.GetNear);
+ set => Node.Call(MethodName.SetNear, value);
+ }
+
+ public Environment Environment
+ {
+ get => (Environment)Node.Call(MethodName.GetEnvironment);
+ set => Node.Call(MethodName.SetEnvironment, value);
+ }
+
+ public CameraAttributes Attributes
+ {
+ get => (CameraAttributes)Node.Call(MethodName.GetAttributes);
+ set => Node.Call(MethodName.SetAttributes, value);
+ }
+
+
+ public static PhantomCamera3D FromScript(string path) => new(GD.Load(path).New().AsGodotObject());
+ public static PhantomCamera3D FromScript(GDScript script) => new(script.New().AsGodotObject());
+
+ public PhantomCamera3D(GodotObject phantomCamera3DNode) : base(phantomCamera3DNode)
+ {
+ _callableLookAtTargetChanged = Callable.From(() => LookAtTargetChanged?.Invoke());
+ _callableTweenInterrupted = Callable.From(pCam => TweenInterrupted?.Invoke(pCam));
+
+ Node.Connect(SignalName.LookAtTargetChanged, _callableLookAtTargetChanged);
+ Node.Connect(SignalName.TweenInterrupted, _callableTweenInterrupted);
+ }
+
+ ~PhantomCamera3D()
+ {
+ Node.Disconnect(SignalName.LookAtTargetChanged, _callableLookAtTargetChanged);
+ Node.Disconnect(SignalName.TweenInterrupted, _callableTweenInterrupted);
+ }
+
+ public new static class MethodName
+ {
+ public const string GetLookAtMode = "get_look_at_mode";
+
+ public const string GetCamera3DResource = "get_camera_3d_resource";
+ public const string SetCamera3DResource = "set_camera_3d_resource";
+
+ public const string GetThirdPersonRotation = "get_third_person_rotation";
+ public const string SetThirdPersonRotation = "set_third_person_rotation";
+
+ public const string GetThirdPersonRotationDegrees = "get_third_person_rotation_degrees";
+ public const string SetThirdPersonRotationDegrees = "set_third_person_rotation_degrees";
+
+ public const string GetThirdPersonQuaternion = "get_third_person_quaternion";
+ public const string SetThirdPersonQuaternion = "set_third_person_quaternion";
+
+ public const string GetSpringLength = "get_spring_length";
+ public const string SetSpringLength = "set_spring_length";
+
+ public const string GetFollowDistance = "get_follow_distance";
+ public const string SetFollowDistance = "set_follow_distance";
+
+ public const string GetAutoFollowDistance = "get_auto_follow_distance";
+ public const string SetAutoFollowDistance = "set_auto_follow_distance";
+
+ public const string GetAutoFollowDistanceMin = "get_auto_follow_distance_min";
+ public const string SetAutoFollowDistanceMin = "set_auto_follow_distance_min";
+
+ public const string GetAutoFollowDistanceMax = "get_auto_follow_distance_max";
+ public const string SetAutoFollowDistanceMax = "set_auto_follow_distance_max";
+
+ public const string GetAutoFollowDistanceDivisor = "get_auto_follow_distance_divisor";
+ public const string SetAutoFollowDistanceDivisor = "set_auto_follow_distance_divisor";
+
+ public const string GetLookAtTarget = "get_look_at_target";
+ public const string SetLookAtTarget = "set_look_at_target";
+
+ public const string GetLookAtTargets = "get_look_at_targets";
+ public const string SetLookAtTargets = "set_look_at_targets";
+
+ public const string GetViewportPosition = "get_viewport_position";
+ public const string SetViewportPosition = "set_viewport_position";
+
+ public const string GetCollisionMask = "get_collision_mask";
+ public const string SetCollisionMask = "set_collision_mask";
+
+ public const string GetShape = "get_shape";
+ public const string SetShape = "set_shape";
+
+ public const string GetMargin = "get_margin";
+ public const string SetMargin = "set_margin";
+
+ public const string GetLookAtOffset = "get_look_at_offset";
+ public const string SetLookAtOffset = "set_look_at_offset";
+
+ public const string GetLookAtDamping = "get_look_at_damping";
+ public const string SetLookAtDamping = "set_look_at_damping";
+
+ public const string GetLookAtDampingValue = "get_look_at_damping_value";
+ public const string SetLookAtDampingValue = "set_look_at_damping_value";
+
+ public const string GetCullMask = "get_cull_mask";
+ public const string SetCullMask = "set_cull_mask";
+
+ public const string GetHOffset = "get_h_offset";
+ public const string SetHOffset = "set_h_offset";
+
+ public const string GetVOffset = "get_v_offset";
+ public const string SetVOffset = "set_v_offset";
+
+ public const string GetProjection = "get_projection";
+ public const string SetProjection = "set_projection";
+
+ public const string GetFov = "get_fov";
+ public const string SetFov = "set_fov";
+
+ public const string GetSize = "get_size";
+ public const string SetSize = "set_size";
+
+ public const string GetFrustumOffset = "get_frustum_offset";
+ public const string SetFrustumOffset = "set_frustum_offset";
+
+ public const string GetFar = "get_far";
+ public const string SetFar = "set_far";
+
+ public const string GetNear = "get_near";
+ public const string SetNear = "set_near";
+
+ public const string GetEnvironment = "get_environment";
+ public const string SetEnvironment = "set_environment";
+
+ public const string GetAttributes = "get_attributes";
+ public const string SetAttributes = "set_attributes";
+ }
+}
\ No newline at end of file
diff --git a/addons/phantom_camera/scripts/phantom_camera_host/PhantomCameraHost.cs b/addons/phantom_camera/scripts/phantom_camera_host/PhantomCameraHost.cs
new file mode 100644
index 00000000..34cf663b
--- /dev/null
+++ b/addons/phantom_camera/scripts/phantom_camera_host/PhantomCameraHost.cs
@@ -0,0 +1,75 @@
+using Godot;
+using PhantomCamera.Cameras;
+
+#nullable enable
+
+namespace PhantomCamera.Hosts;
+
+public enum InterpolationMode
+{
+ Auto,
+ Idle,
+ Physics
+}
+
+public class PhantomCameraHost
+{
+ public Node Node { get; }
+
+ // TODO: For Godot 4.3
+ // public InterpolationMode InterpolationMode
+ // {
+ // get => (InterpolationMode)(int)Node.Call(MethodName.GetInterpolationMode);
+ // set => Node.Call(MethodName.SetInterpolationMode, (int)value);
+ // }
+
+ public Camera2D? Camera2D => (Camera2D?)Node.Get(PropertyName.Camera2D);
+
+ public Camera3D? Camera3D => (Camera3D?)Node.Get(PropertyName.Camera3D);
+
+ public bool TriggerPhantomCameraTween => (bool)Node.Call(MethodName.GetTriggerPhantomCameraTween);
+
+ public PhantomCameraHost(Node node) => Node = node;
+
+ public ActivePhantomCameraQueryResult? GetActivePhantomCamera()
+ {
+ var result = Node.Call(MethodName.GetActivePhantomCamera);
+ return result.VariantType == Variant.Type.Nil ? null : new ActivePhantomCameraQueryResult(result.AsGodotObject());
+ }
+
+ public static class PropertyName
+ {
+ public const string Camera2D = "camera_2d";
+ public const string Camera3D = "camera_3d";
+ }
+
+ public static class MethodName
+ {
+ public const string GetActivePhantomCamera = "get_active_pcam";
+ public const string GetTriggerPhantomCameraTween = "get_trigger_pcam_tween";
+
+ public const string GetInterpolationMode = "get_interpolation_mode";
+ public const string SetInterpolationMode = "set_interpolation_mode";
+ }
+}
+
+public class ActivePhantomCameraQueryResult
+{
+ private readonly GodotObject _obj;
+
+ public bool Is2D => _obj.IsClass("Node2D");
+
+ public bool Is3D => _obj.IsClass("Node3D");
+
+ public ActivePhantomCameraQueryResult(GodotObject godotObject) => _obj = godotObject;
+
+ public PhantomCamera2D? AsPhantomCamera2D()
+ {
+ return Is2D ? new PhantomCamera2D(_obj) : null;
+ }
+
+ public PhantomCamera3D? AsPhantomCamera3D()
+ {
+ return Is3D ? new PhantomCamera3D(_obj) : null;
+ }
+}
\ No newline at end of file
diff --git a/addons/phantom_camera/scripts/resources/Camera3DResource.cs b/addons/phantom_camera/scripts/resources/Camera3DResource.cs
new file mode 100644
index 00000000..0d5dd399
--- /dev/null
+++ b/addons/phantom_camera/scripts/resources/Camera3DResource.cs
@@ -0,0 +1,122 @@
+using Godot;
+
+namespace PhantomCamera.Resources;
+
+public enum ProjectionType
+{
+ Perspective,
+ Orthogonal,
+ Frustum
+}
+
+public class Camera3DResource
+{
+ public readonly Resource Resource;
+
+ public const float MinOffset = 0;
+ public const float MaxOffset = 1;
+
+ public const float MinFov = 1;
+ public const float MaxFov = 179;
+
+ public const float MinSize = 0.001f;
+ public const float MaxSize = 100;
+
+ public const float MinNear = 0.001f;
+ public const float MaxNear = 10;
+
+ public const float MinFar = 0.01f;
+ public const float MaxFar = 4000;
+
+ public int CullMask
+ {
+ get => (int)Resource.Call(MethodName.GetCullMask);
+ set => Resource.Call(MethodName.SetCullMask, value);
+ }
+
+ public float HOffset
+ {
+ get => (float)Resource.Call(MethodName.GetHOffset);
+ set => Resource.Call(MethodName.SetHOffset, Mathf.Clamp(value, MinOffset, MaxOffset));
+ }
+
+ public float VOffset
+ {
+ get => (float)Resource.Call(MethodName.GetVOffset);
+ set => Resource.Call(MethodName.SetVOffset, Mathf.Clamp(value, MinOffset, MaxOffset));
+ }
+
+ public ProjectionType Projection
+ {
+ get => (ProjectionType)(int)Resource.Call(MethodName.GetProjection);
+ set => Resource.Call(MethodName.SetProjection, (int)value);
+ }
+
+ public float Fov
+ {
+ get => (float)Resource.Call(MethodName.GetFov);
+ set => Resource.Call(MethodName.SetFov, Mathf.Clamp(value, MinFov, MaxFov));
+ }
+
+ public float Size
+ {
+ get => (float)Resource.Call(MethodName.GetSize);
+ set => Resource.Call(MethodName.SetSize, Mathf.Clamp(value, MinSize, MaxSize));
+ }
+
+ public Vector2 FrustumOffset
+ {
+ get => (Vector2)Resource.Call(MethodName.GetFrustumOffset);
+ set => Resource.Call(MethodName.SetFrustumOffset, value);
+ }
+
+ public float Near
+ {
+ get => (float)Resource.Call(MethodName.GetNear);
+ set => Resource.Call(MethodName.SetNear, Mathf.Clamp(value, MinNear, MaxNear));
+ }
+
+ public float Far
+ {
+ get => (float)Resource.Call(MethodName.GetFar);
+ set => Resource.Call(MethodName.SetFar, Mathf.Clamp(value, MinFar, MaxFar));
+ }
+
+ public Camera3DResource(Resource resource) => Resource = resource;
+
+ public void SetCullMaskValue(int layerNumber, bool value)
+ {
+ Resource.Call(MethodName.SetCullMaskValue, layerNumber, value);
+ }
+
+ public static class MethodName
+ {
+ public const string GetCullMask = "get_cull_mask";
+ public const string SetCullMask = "set_cull_mask";
+ public const string SetCullMaskValue = "set_cull_mask_value";
+
+ public const string GetHOffset = "get_h_offset";
+ public const string SetHOffset = "set_h_offset";
+
+ public const string GetVOffset = "get_v_offset";
+ public const string SetVOffset = "set_v_offset";
+
+ public const string GetProjection = "get_projection";
+ public const string SetProjection = "set_projection";
+
+ public const string GetFov = "get_fov";
+ public const string SetFov = "set_fov";
+
+ public const string GetSize = "get_size";
+ public const string SetSize = "set_size";
+
+ public const string GetFrustumOffset = "get_frustum_offset";
+ public const string SetFrustumOffset = "set_frustum_offset";
+
+ public const string GetNear = "get_near";
+ public const string SetNear = "set_near";
+
+ public const string GetFar = "get_far";
+ public const string SetFar = "set_far";
+ }
+}
\ No newline at end of file
diff --git a/addons/phantom_camera/scripts/resources/PhantomCameraTween.cs b/addons/phantom_camera/scripts/resources/PhantomCameraTween.cs
new file mode 100644
index 00000000..b596f0a8
--- /dev/null
+++ b/addons/phantom_camera/scripts/resources/PhantomCameraTween.cs
@@ -0,0 +1,58 @@
+using Godot;
+
+namespace PhantomCamera.Resources;
+
+public enum TransitionType
+{
+ Linear,
+ Sine,
+ Quintic,
+ Quartic,
+ Quadratic,
+ Exponential,
+ Elastic,
+ Cubic,
+ Circ,
+ Bounce,
+ Back
+}
+
+public enum EaseType
+{
+ In,
+ Out,
+ InOut,
+ OutIn
+}
+
+public class PhantomCameraTween
+{
+ public Resource Resource { get; }
+
+ public float Duration
+ {
+ get => (float)Resource.Get(PropertyName.Duration);
+ set => Resource.Set(PropertyName.Duration, value);
+ }
+
+ public TransitionType Transition
+ {
+ get => (TransitionType)(int)Resource.Get(PropertyName.Transition);
+ set => Resource.Set(PropertyName.Transition, (int)value);
+ }
+
+ public EaseType Ease
+ {
+ get => (EaseType)(int)Resource.Get(PropertyName.Ease);
+ set => Resource.Set(PropertyName.Ease, (int)value);
+ }
+
+ public PhantomCameraTween(Resource tweenResource) => Resource = tweenResource;
+
+ public static class PropertyName
+ {
+ public const string Duration = "durartion";
+ public const string Transition = "transition";
+ public const string Ease = "ease";
+ }
+}
\ No newline at end of file
diff --git a/dev_scenes/3d/dev_scene_3d.tscn b/dev_scenes/3d/dev_scene_3d.tscn
index 36a7af77..f350e28e 100644
--- a/dev_scenes/3d/dev_scene_3d.tscn
+++ b/dev_scenes/3d/dev_scene_3d.tscn
@@ -17,6 +17,27 @@ fog_density = 0.0392
adjustment_enabled = true
adjustment_contrast = 1.19
+<<<<<<< HEAD
+[sub_resource type="Resource" id="Resource_sj6ok"]
+script = ExtResource("6_pmc8r")
+duration = 0.6
+transition = 2
+ease = 2
+
+[sub_resource type="Resource" id="Resource_a3u85"]
+script = ExtResource("7_fioii")
+cull_mask = 1048575
+h_offset = 0.0
+v_offset = 0.0
+projection = 0
+fov = 75.0
+size = 1.0
+frustum_offset = Vector2(0, 0)
+near = 0.05
+far = 4000.0
+
+=======
+>>>>>>> main
[sub_resource type="Resource" id="Resource_6c6yi"]
script = ExtResource("6_pmc8r")
duration = 1.0
@@ -33,6 +54,9 @@ fov = 75.0
size = 1.0
frustum_offset = Vector2(0, 0)
near = 0.05
+<<<<<<< HEAD
+far = 4000.0
+=======
far = 2000.0
[sub_resource type="Resource" id="Resource_sj6ok"]
@@ -52,6 +76,7 @@ size = 1.0
frustum_offset = Vector2(0, 0)
near = 0.05
far = 2000.0
+>>>>>>> main
[node name="Node3D" type="Node3D"]
script = ExtResource("1_gnrfx")
diff --git a/dev_scenes/3d/dev_scene_csharp_3d.tscn b/dev_scenes/3d/dev_scene_csharp_3d.tscn
new file mode 100644
index 00000000..7b44bb82
--- /dev/null
+++ b/dev_scenes/3d/dev_scene_csharp_3d.tscn
@@ -0,0 +1,81 @@
+[gd_scene load_steps=13 format=3 uid="uid://brb8fl27ofqhv"]
+
+[ext_resource type="Script" path="res://dev_scenes/3d/scripts/DevSceneCSharp3D.cs" id="1_fd2f4"]
+[ext_resource type="Script" path="res://addons/phantom_camera/scripts/phantom_camera_host/phantom_camera_host.gd" id="2_m8s1w"]
+[ext_resource type="Script" path="res://addons/phantom_camera/scripts/phantom_camera/phantom_camera_3d.gd" id="3_xni2u"]
+[ext_resource type="Script" path="res://addons/phantom_camera/scripts/resources/tween_resource.gd" id="4_c7bav"]
+[ext_resource type="Script" path="res://addons/phantom_camera/scripts/resources/camera_3d_resource.gd" id="5_jea0a"]
+
+[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_vuocs"]
+sky_horizon_color = Color(0.64625, 0.65575, 0.67075, 1)
+ground_horizon_color = Color(0.64625, 0.65575, 0.67075, 1)
+
+[sub_resource type="Sky" id="Sky_evu1o"]
+sky_material = SubResource("ProceduralSkyMaterial_vuocs")
+
+[sub_resource type="Environment" id="Environment_5sile"]
+background_mode = 2
+sky = SubResource("Sky_evu1o")
+tonemap_mode = 2
+glow_enabled = true
+
+[sub_resource type="CapsuleMesh" id="CapsuleMesh_vcumb"]
+
+[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_04sci"]
+
+[sub_resource type="Resource" id="Resource_nsh7j"]
+script = ExtResource("4_c7bav")
+duration = 1.0
+transition = 0
+ease = 2
+
+[sub_resource type="Resource" id="Resource_ujhhy"]
+script = ExtResource("5_jea0a")
+cull_mask = 1048575
+h_offset = 0.0
+v_offset = 0.0
+projection = 0
+fov = 75.0
+size = 1.0
+frustum_offset = Vector2(0, 0)
+near = 0.05
+far = 4000.0
+
+[node name="Node3D" type="Node3D"]
+script = ExtResource("1_fd2f4")
+
+[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
+transform = Transform3D(-0.866023, -0.433016, 0.250001, 0, 0.499998, 0.866027, -0.500003, 0.749999, -0.43301, 0, 0, 0)
+shadow_enabled = true
+
+[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
+environment = SubResource("Environment_5sile")
+
+[node name="Camera3D" type="Camera3D" parent="."]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.25252, 4)
+
+[node name="PhantomCameraHost" type="Node" parent="Camera3D"]
+script = ExtResource("2_m8s1w")
+
+[node name="CSGBox3D" type="CSGBox3D" parent="."]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.514567, 0)
+use_collision = true
+size = Vector3(10, 1, 10)
+
+[node name="Player" type="CharacterBody3D" parent="."]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.25252, 0)
+
+[node name="MeshInstance3D" type="MeshInstance3D" parent="Player"]
+mesh = SubResource("CapsuleMesh_vcumb")
+
+[node name="CollisionShape3D" type="CollisionShape3D" parent="Player"]
+shape = SubResource("CapsuleShape3D_04sci")
+
+[node name="PlayerCam" type="Node3D" parent="Player" node_paths=PackedStringArray("follow_target")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 4)
+script = ExtResource("3_xni2u")
+follow_mode = 6
+follow_target = NodePath("..")
+tween_resource = SubResource("Resource_nsh7j")
+camera_3d_resource = SubResource("Resource_ujhhy")
+follow_offset = Vector3(0, 1, 3)
diff --git a/dev_scenes/3d/scripts/DevSceneCSharp3D.cs b/dev_scenes/3d/scripts/DevSceneCSharp3D.cs
new file mode 100644
index 00000000..a191968c
--- /dev/null
+++ b/dev_scenes/3d/scripts/DevSceneCSharp3D.cs
@@ -0,0 +1,14 @@
+using Godot;
+using PhantomCamera;
+
+public partial class DevSceneCSharp3D : Node3D
+{
+ public override void _Ready()
+ {
+ var pCam = GetNode("Player/PlayerCam").AsPhantomCamera3D();
+
+ GD.Print(pCam.Node3D);
+
+
+ }
+}
diff --git a/tests/scenes/test_runner.tscn b/tests/scenes/test_runner.tscn
new file mode 100644
index 00000000..c17a6eff
--- /dev/null
+++ b/tests/scenes/test_runner.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://bu4xjlkrknuo5"]
+
+[ext_resource type="Script" path="res://tests/scripts/test_runner.gd" id="1_m0lqx"]
+
+[node name="TestRunner" type="Node"]
+script = ExtResource("1_m0lqx")
diff --git a/tests/scenes/test_scene_2d.tscn b/tests/scenes/test_scene_2d.tscn
new file mode 100644
index 00000000..f2bd1424
--- /dev/null
+++ b/tests/scenes/test_scene_2d.tscn
@@ -0,0 +1,37 @@
+[gd_scene load_steps=7 format=3 uid="uid://bx61t0ytiwtwm"]
+
+[ext_resource type="Script" path="res://addons/phantom_camera/scripts/phantom_camera_host/phantom_camera_host.gd" id="1_qr8fr"]
+[ext_resource type="Script" path="res://addons/phantom_camera/scripts/phantom_camera/phantom_camera_2d.gd" id="2_53jhm"]
+[ext_resource type="Script" path="res://addons/phantom_camera/scripts/resources/tween_resource.gd" id="3_aqfsy"]
+
+[sub_resource type="Resource" id="Resource_f0onm"]
+script = ExtResource("3_aqfsy")
+duration = 1.0
+transition = 0
+ease = 2
+
+[sub_resource type="TileSet" id="TileSet_lpbxs"]
+
+[sub_resource type="RectangleShape2D" id="RectangleShape2D_4v04e"]
+
+[node name="TestScene2D" type="Node2D"]
+
+[node name="Camera2D" type="Camera2D" parent="."]
+
+[node name="PhantomCameraHost" type="Node" parent="Camera2D"]
+script = ExtResource("1_qr8fr")
+
+[node name="PhantomCamera2D" type="Node2D" parent="."]
+script = ExtResource("2_53jhm")
+tween_resource = SubResource("Resource_f0onm")
+
+[node name="TileMap" type="TileMap" parent="."]
+tile_set = SubResource("TileSet_lpbxs")
+format = 2
+
+[node name="Area2D" type="Area2D" parent="."]
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D"]
+shape = SubResource("RectangleShape2D_4v04e")
+
+[node name="Marker2D" type="Marker2D" parent="."]
diff --git a/tests/scenes/test_scene_3d.tscn b/tests/scenes/test_scene_3d.tscn
new file mode 100644
index 00000000..d654f4a9
--- /dev/null
+++ b/tests/scenes/test_scene_3d.tscn
@@ -0,0 +1,36 @@
+[gd_scene load_steps=7 format=3 uid="uid://bry2ltsrujg02"]
+
+[ext_resource type="Script" path="res://addons/phantom_camera/scripts/phantom_camera_host/phantom_camera_host.gd" id="1_egs8c"]
+[ext_resource type="Script" path="res://addons/phantom_camera/scripts/phantom_camera/phantom_camera_3d.gd" id="2_fln48"]
+[ext_resource type="Script" path="res://addons/phantom_camera/scripts/resources/tween_resource.gd" id="3_ir72h"]
+[ext_resource type="Script" path="res://addons/phantom_camera/scripts/resources/camera_3d_resource.gd" id="4_w22nl"]
+
+[sub_resource type="Resource" id="Resource_om1dn"]
+script = ExtResource("3_ir72h")
+duration = 1.0
+transition = 0
+ease = 2
+
+[sub_resource type="Resource" id="Resource_omnwe"]
+script = ExtResource("4_w22nl")
+cull_mask = 1048575
+h_offset = 0.0
+v_offset = 0.0
+projection = 0
+fov = 75.0
+size = 1.0
+frustum_offset = Vector2(0, 0)
+near = 0.05
+far = 4000.0
+
+[node name="TestScene3D" type="Node3D"]
+
+[node name="Camera3D" type="Camera3D" parent="."]
+
+[node name="PhantomCameraHost" type="Node" parent="Camera3D"]
+script = ExtResource("1_egs8c")
+
+[node name="PhantomCamera3D" type="Node3D" parent="."]
+script = ExtResource("2_fln48")
+tween_resource = SubResource("Resource_om1dn")
+camera_3d_resource = SubResource("Resource_omnwe")
diff --git a/tests/scripts/TestPhantomCameraWrapper.cs b/tests/scripts/TestPhantomCameraWrapper.cs
new file mode 100644
index 00000000..537dcca6
--- /dev/null
+++ b/tests/scripts/TestPhantomCameraWrapper.cs
@@ -0,0 +1,186 @@
+using System.Diagnostics;
+using Godot;
+using PhantomCamera;
+using PhantomCamera.Cameras;
+using PhantomCamera.Managers;
+using PhantomCamera.Resources;
+
+namespace PhantomCameraTests;
+
+public partial class TestPhantomCameraWrapper: Node
+{
+ private PackedScene _scene2d;
+
+ private PackedScene _scene3d;
+
+ public override void _Ready()
+ {
+ _scene2d = GD.Load("res://tests/scenes/test_scene_2d.tscn");
+ _scene3d = GD.Load("res://tests/scenes/test_scene_3d.tscn");
+ }
+
+ public void Test()
+ {
+ Test2D();
+ Test3D();
+ GD.Print("PhantomCameraWrapper tests complete");
+ }
+
+ private void Test2D()
+ {
+ var testScene = _scene2d.Instantiate();
+ AddChild(testScene);
+
+ // PhantomCameraManager tests
+ Debug.Assert(PhantomCameraManager.Instance != null);
+ Debug.Assert(PhantomCameraManager.PhantomCamera3Ds.Length == 0);
+ Debug.Assert(PhantomCameraManager.PhantomCamera2Ds.Length == 1);
+ Debug.Assert(PhantomCameraManager.PhantomCameraHosts.Length == 1);
+
+ // PhantomCameraHost tests
+ var cameraHost = testScene.GetNode("Camera2D/PhantomCameraHost").AsPhantomCameraHost();
+ Debug.Assert(cameraHost.Node != null);
+ Debug.Assert(cameraHost.Camera2D != null);
+ Debug.Assert(cameraHost.Camera3D == null);
+ Debug.Assert(cameraHost.TriggerPhantomCameraTween);
+
+ var cameraQuery = cameraHost.GetActivePhantomCamera();
+ Debug.Assert(cameraQuery != null);
+ Debug.Assert(cameraQuery.Is2D);
+ Debug.Assert(!cameraQuery.Is3D);
+ Debug.Assert(cameraQuery.AsPhantomCamera3D() == null);
+
+ // PhantomCamera shared tests
+ var camera = cameraQuery.AsPhantomCamera2D();
+ Debug.Assert(camera != null);
+ Debug.Assert(camera.Node2D != null);
+
+ Debug.Assert(camera.FollowMode == FollowMode.None);
+ Debug.Assert(camera.IsActive);
+
+ var priority = camera.Priority;
+ camera.Priority += 10;
+ Debug.Assert(camera.Priority == priority + 10);
+
+ var tweenOnLoad = camera.TweenOnLoad;
+ camera.TweenOnLoad = !camera.TweenOnLoad;
+ Debug.Assert(camera.TweenOnLoad != tweenOnLoad);
+
+ Debug.Assert(camera.InactiveUpdateMode == InactiveUpdateMode.Always);
+ camera.InactiveUpdateMode = InactiveUpdateMode.Never;
+ Debug.Assert(camera.InactiveUpdateMode == InactiveUpdateMode.Never);
+
+ // TweenResource tests
+ var tweenResource = camera.TweenResource;
+ Debug.Assert(tweenResource.Resource != null);
+
+ var tweenDuration = tweenResource.Duration;
+ tweenResource.Duration += 1.0f;
+ Debug.Assert((tweenResource.Duration - (tweenDuration + 1.0f)) <= float.Epsilon);
+
+ tweenResource.Ease = EaseType.Out;
+ Debug.Assert(tweenResource.Ease == EaseType.Out);
+
+ tweenResource.Transition = TransitionType.Sine;
+ Debug.Assert(tweenResource.Transition == TransitionType.Sine);
+
+ var tweenResourceScript = GD.Load("res://addons/phantom_camera/scripts/resources/tween_resource.gd");
+ var newTweenResource = new PhantomCameraTween(tweenResourceScript.New().As())
+ {
+ Duration = 1.5f,
+ Ease = EaseType.In,
+ Transition = TransitionType.Cubic
+ };
+ camera.TweenResource = newTweenResource;
+
+ Debug.Assert((camera.TweenResource.Duration - 1.5f) <= float.Epsilon);
+ Debug.Assert(camera.TweenResource.Ease == EaseType.In);
+ Debug.Assert(camera.TweenResource.Transition == TransitionType.Cubic);
+
+ // PhantomCamera2D tests
+ camera.Zoom = new Vector2(2, 2);
+ Debug.Assert(camera.Zoom.Equals(new Vector2(2, 2)));
+
+ var snapToPixel = camera.SnapToPixel;
+ camera.SnapToPixel = !camera.SnapToPixel;
+ Debug.Assert(camera.SnapToPixel != snapToPixel);
+
+ camera.LimitLeft = 2;
+ camera.LimitTop = 3;
+ camera.LimitRight = 4;
+ camera.LimitBottom = 5;
+ Debug.Assert(camera.LimitLeft == camera.GetLimit(Side.Left));
+ Debug.Assert(camera.LimitTop == camera.GetLimit(Side.Top));
+ Debug.Assert(camera.LimitRight == camera.GetLimit(Side.Right));
+ Debug.Assert(camera.LimitBottom == camera.GetLimit(Side.Bottom));
+
+ camera.SetLimit(Side.Left, 5);
+ camera.SetLimit(Side.Top, 4);
+ camera.SetLimit(Side.Right, 3);
+ camera.SetLimit(Side.Bottom, 2);
+ Debug.Assert(camera.LimitLeft == camera.GetLimit(Side.Left));
+ Debug.Assert(camera.LimitTop == camera.GetLimit(Side.Top));
+ Debug.Assert(camera.LimitRight == camera.GetLimit(Side.Right));
+ Debug.Assert(camera.LimitBottom == camera.GetLimit(Side.Bottom));
+
+ Debug.Assert(camera.GetLimitTarget() == null);
+
+ var tileMap = testScene.GetNode("TileMap");
+ camera.SetLimitTarget(tileMap);
+ var limitTarget = camera.GetLimitTarget();
+ Debug.Assert(limitTarget != null);
+ Debug.Assert(limitTarget.IsTileMap);
+ Debug.Assert(limitTarget.AsTileMap() != null);
+
+ var tileMapLayer = testScene.GetNode("TileMapLayer");
+ camera.SetLimitTarget(tileMapLayer);
+ limitTarget = camera.GetLimitTarget();
+ Debug.Assert(limitTarget != null);
+ Debug.Assert(limitTarget.IsTileMapLayer);
+ Debug.Assert(limitTarget.AsTileMapLayer() != null);
+
+ var shape2D = testScene.GetNode("Area2D/CollisionShape2D");
+ camera.SetLimitTarget(shape2D);
+ limitTarget = camera.GetLimitTarget();
+ Debug.Assert(limitTarget != null);
+ Debug.Assert(limitTarget.IsCollisionShape2D);
+ Debug.Assert(limitTarget.AsCollisionShape2D() != null);
+
+ // TODO: test LimitMargin
+
+ // TODO: test signals
+
+ RemoveChild(testScene);
+ }
+
+ private void Test3D()
+ {
+ var testScene = _scene3d.Instantiate();
+ AddChild(testScene);
+
+ // PhantomCameraManager Tests
+ Debug.Assert(PhantomCameraManager.Instance != null);
+ Debug.Assert(PhantomCameraManager.PhantomCamera2Ds.Length == 0);
+ Debug.Assert(PhantomCameraManager.PhantomCamera3Ds.Length == 1);
+ Debug.Assert(PhantomCameraManager.PhantomCameraHosts.Length == 1);
+
+ // PhantomCameraHost Tests
+ var cameraHost = testScene.GetNode("Camera3D/PhantomCameraHost").AsPhantomCameraHost();
+ Debug.Assert(cameraHost.Node != null);
+ Debug.Assert(cameraHost.Camera2D == null);
+ Debug.Assert(cameraHost.Camera3D != null);
+ Debug.Assert(cameraHost.TriggerPhantomCameraTween);
+
+ var cameraQuery = cameraHost.GetActivePhantomCamera();
+ Debug.Assert(cameraQuery != null);
+ Debug.Assert(!cameraQuery.Is2D);
+ Debug.Assert(cameraQuery.Is3D);
+ Debug.Assert(cameraQuery.AsPhantomCamera2D() == null);
+
+ // PhantomCamera3D Tests
+ var camera = cameraQuery.AsPhantomCamera3D();
+ Debug.Assert(camera != null);
+
+ RemoveChild(testScene);
+ }
+}
\ No newline at end of file
diff --git a/tests/scripts/test_phantom_camera.gd b/tests/scripts/test_phantom_camera.gd
new file mode 100644
index 00000000..e5f101f7
--- /dev/null
+++ b/tests/scripts/test_phantom_camera.gd
@@ -0,0 +1,7 @@
+extends Node
+
+@onready var scene2d = load("res://tests/scenes/test_scene_2d.tscn")
+@onready var scene3d = load("res://tests/scenes/test_scene_3d.tscn")
+
+func Test() -> void:
+ print("No GDScript tests implemented")
diff --git a/tests/scripts/test_runner.gd b/tests/scripts/test_runner.gd
new file mode 100644
index 00000000..fa4b5795
--- /dev/null
+++ b/tests/scripts/test_runner.gd
@@ -0,0 +1,15 @@
+extends Node
+
+var test_scripts: Array[Script] = [
+ load("res://tests/scripts/test_phantom_camera.gd"),
+ load("res://tests/scripts/TestPhantomCameraWrapper.cs"),
+]
+
+func _ready() -> void:
+ for script: Script in test_scripts:
+ var test: Object = script.new()
+ if test.has_method("Test"):
+ add_child(test)
+ test.Test()
+ remove_child(test)
+ get_tree().quit()