diff --git a/Runtime/Cast/Event.meta b/Runtime/Cast/Event.meta
new file mode 100644
index 00000000..ff7f0028
--- /dev/null
+++ b/Runtime/Cast/Event.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 5d682a641a14eb0498704d848f9f27dc
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Runtime/Cast/Event/Proxy.meta b/Runtime/Cast/Event/Proxy.meta
new file mode 100644
index 00000000..4c3d9f87
--- /dev/null
+++ b/Runtime/Cast/Event/Proxy.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 09f268040a7b0cf46827a884dd279303
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Runtime/Cast/Event/Proxy/PointsCastEventProxyEmitter.cs b/Runtime/Cast/Event/Proxy/PointsCastEventProxyEmitter.cs
new file mode 100644
index 00000000..9ab022f2
--- /dev/null
+++ b/Runtime/Cast/Event/Proxy/PointsCastEventProxyEmitter.cs
@@ -0,0 +1,73 @@
+namespace Zinnia.Cast.Event.Proxy
+{
+ using System;
+ using UnityEngine;
+ using UnityEngine.Events;
+ using Zinnia.Event.Proxy;
+ using Zinnia.Extension;
+
+ ///
+ /// Emits a with a payload whenever is called.
+ ///
+ public class PointsCastEventProxyEmitter : RestrictableSingleEventProxyEmitter
+ {
+ ///
+ /// The types of that can be used for the rule source.
+ ///
+ public enum RuleSourceType
+ {
+ ///
+ /// Use the actual hit as the source for the rule.
+ ///
+ Collider,
+ ///
+ /// Use the parent hit as the target for the rule.
+ ///
+ Rigidbody
+ }
+
+ [Tooltip("The source GameObject to apply to the RestrictableSingleEventProxyEmitter.ReceiveValidity.")]
+ [SerializeField]
+ private RuleSourceType ruleSource;
+ ///
+ /// The source to apply to the .
+ ///
+ public RuleSourceType RuleSource
+ {
+ get
+ {
+ return ruleSource;
+ }
+ set
+ {
+ ruleSource = value;
+ }
+ }
+
+ ///
+ /// Defines the event with the specified state.
+ ///
+ [Serializable]
+ public class UnityEvent : UnityEvent { }
+
+ ///
+ protected override object GetTargetToCheck()
+ {
+ if (Payload == null || Payload.HitData == null)
+ {
+ return null;
+ }
+
+ RaycastHit hitData = (RaycastHit)Payload.HitData;
+ switch (RuleSource)
+ {
+ case RuleSourceType.Collider:
+ return hitData.collider.gameObject;
+ case RuleSourceType.Rigidbody:
+ return hitData.collider.GetContainingTransform().gameObject;
+ }
+
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Runtime/Cast/Event/Proxy/PointsCastEventProxyEmitter.cs.meta b/Runtime/Cast/Event/Proxy/PointsCastEventProxyEmitter.cs.meta
new file mode 100644
index 00000000..557d20e4
--- /dev/null
+++ b/Runtime/Cast/Event/Proxy/PointsCastEventProxyEmitter.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: cb9ca9e2bfd53be4c8822920af53c734
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Tests/Editor/Cast/Event.meta b/Tests/Editor/Cast/Event.meta
new file mode 100644
index 00000000..d428f585
--- /dev/null
+++ b/Tests/Editor/Cast/Event.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: cdd13003b42bd2f4e8519d6a1c552eda
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Tests/Editor/Cast/Event/Proxy.meta b/Tests/Editor/Cast/Event/Proxy.meta
new file mode 100644
index 00000000..bcbe737f
--- /dev/null
+++ b/Tests/Editor/Cast/Event/Proxy.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: a72c05e97cc1f33418c3272fa4d6b350
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Tests/Editor/Cast/Event/Proxy/PointsCastEventProxyEmitterTest.cs b/Tests/Editor/Cast/Event/Proxy/PointsCastEventProxyEmitterTest.cs
new file mode 100644
index 00000000..25874a49
--- /dev/null
+++ b/Tests/Editor/Cast/Event/Proxy/PointsCastEventProxyEmitterTest.cs
@@ -0,0 +1,181 @@
+using Zinnia.Cast;
+using Zinnia.Cast.Event.Proxy;
+using Zinnia.Data.Collection.List;
+using Zinnia.Rule;
+
+namespace Test.Zinnia.Cast.Event.Proxy
+{
+ using NUnit.Framework;
+ using Test.Zinnia.Utility.Helper;
+ using Test.Zinnia.Utility.Mock;
+ using UnityEngine;
+
+ public class PointsCastEventProxyEmitterTest
+ {
+ private GameObject containingObject;
+ private PointsCastEventProxyEmitter subject;
+
+ [SetUp]
+ public void SetUp()
+ {
+ containingObject = new GameObject("PointsCastEventProxyEmitterTest");
+ subject = containingObject.AddComponent();
+ }
+
+ [TearDown]
+ public void TearDown()
+ {
+ Object.DestroyImmediate(containingObject);
+ }
+
+ [Test]
+ public void Receive()
+ {
+ UnityEventListenerMock emittedMock = new UnityEventListenerMock();
+ subject.Emitted.AddListener(emittedMock.Listen);
+
+ PointsCast.EventData data = new PointsCast.EventData();
+ data.HitData = RaycastHitHelper.GetRaycastHit();
+ data.IsValid = true;
+
+ Assert.IsNull(subject.Payload);
+ Assert.IsFalse(emittedMock.Received);
+
+ subject.Receive(data);
+
+ Assert.AreEqual(data, subject.Payload);
+ Assert.IsTrue(emittedMock.Received);
+ }
+
+ [Test]
+ public void ReceiveWithRuleRestrictions()
+ {
+ UnityEventListenerMock emittedMock = new UnityEventListenerMock();
+ subject.Emitted.AddListener(emittedMock.Listen);
+
+ GameObject validBlockerParent = new GameObject("PointsCastEventProxyEmitterTest_validParent");
+ validBlockerParent.AddComponent().isKinematic = true;
+ GameObject validBlockerChild = GameObject.CreatePrimitive(PrimitiveType.Cube);
+ validBlockerChild.name = "PointsCastEventProxyEmitterTest_validChild";
+ validBlockerChild.transform.SetParent(validBlockerParent.transform);
+ validBlockerParent.transform.position = Vector3.left + Vector3.forward;
+
+ GameObject invalidBlockerParent = new GameObject("PointsCastEventProxyEmitterTest_invalidParent");
+ invalidBlockerParent.AddComponent().isKinematic = true;
+ GameObject invalidBlockerChild = GameObject.CreatePrimitive(PrimitiveType.Cube);
+ invalidBlockerChild.name = "PointsCastEventProxyEmitterTest_invalidChild";
+ invalidBlockerChild.transform.SetParent(invalidBlockerParent.transform);
+ invalidBlockerParent.transform.position = Vector3.right + Vector3.forward;
+
+ ListContainsRule rule = subject.gameObject.AddComponent();
+ UnityObjectObservableList objects = containingObject.AddComponent();
+ rule.Objects = objects;
+
+ objects.Add(validBlockerChild);
+ subject.ReceiveValidity = new RuleContainer
+ {
+ Interface = rule
+ };
+
+ subject.RuleSource = PointsCastEventProxyEmitter.RuleSourceType.Collider;
+
+ PointsCast.EventData data = new PointsCast.EventData();
+ data.HitData = RaycastHitHelper.GetRaycastHit(validBlockerParent, false, Vector3.left, Vector3.forward);
+ data.IsValid = true;
+
+ Assert.IsNull(subject.Payload);
+ Assert.IsFalse(emittedMock.Received);
+
+ subject.Receive(data);
+
+ Assert.AreEqual(data, subject.Payload);
+ Assert.IsTrue(emittedMock.Received);
+
+ subject.Payload = null;
+ emittedMock.Reset();
+
+ subject.RuleSource = PointsCastEventProxyEmitter.RuleSourceType.Rigidbody;
+
+ subject.Receive(data);
+
+ Assert.IsNull(subject.Payload);
+ Assert.IsFalse(emittedMock.Received);
+
+ subject.Payload = null;
+ emittedMock.Reset();
+
+ objects.Add(validBlockerParent);
+
+ subject.Receive(data);
+
+ Assert.AreEqual(data, subject.Payload);
+ Assert.IsTrue(emittedMock.Received);
+
+ subject.Payload = null;
+ emittedMock.Reset();
+
+ subject.RuleSource = PointsCastEventProxyEmitter.RuleSourceType.Collider;
+
+ data.HitData = RaycastHitHelper.GetRaycastHit(invalidBlockerParent, false, Vector3.right, Vector3.forward);
+ data.IsValid = true;
+
+ subject.Receive(data);
+
+ Assert.IsNull(subject.Payload);
+ Assert.IsFalse(emittedMock.Received);
+
+ subject.Payload = null;
+ emittedMock.Reset();
+
+ subject.RuleSource = PointsCastEventProxyEmitter.RuleSourceType.Rigidbody;
+
+ subject.Receive(data);
+
+ Assert.IsNull(subject.Payload);
+ Assert.IsFalse(emittedMock.Received);
+
+ Object.DestroyImmediate(validBlockerParent);
+ Object.DestroyImmediate(invalidBlockerParent);
+ }
+
+ [Test]
+ public void ReceiveInactiveGameObject()
+ {
+ UnityEventListenerMock emittedMock = new UnityEventListenerMock();
+ subject.Emitted.AddListener(emittedMock.Listen);
+ PointsCast.EventData data = new PointsCast.EventData();
+ data.HitData = RaycastHitHelper.GetRaycastHit();
+ data.IsValid = true;
+
+ subject.gameObject.SetActive(false);
+
+ Assert.IsNull(subject.Payload);
+ Assert.IsFalse(emittedMock.Received);
+
+ subject.Receive(data);
+
+ Assert.IsNull(subject.Payload);
+ Assert.IsFalse(emittedMock.Received);
+ }
+
+ [Test]
+ public void ReceiveInactiveComponent()
+ {
+ UnityEventListenerMock emittedMock = new UnityEventListenerMock();
+ subject.Emitted.AddListener(emittedMock.Listen);
+ PointsCast.EventData data = new PointsCast.EventData();
+ data.HitData = RaycastHitHelper.GetRaycastHit();
+ data.IsValid = true;
+
+ subject.enabled = false;
+
+ Assert.IsNull(subject.Payload);
+ Assert.IsFalse(emittedMock.Received);
+
+ subject.Receive(data);
+
+ Assert.IsNull(subject.Payload);
+ Assert.IsFalse(emittedMock.Received);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tests/Editor/Cast/Event/Proxy/PointsCastEventProxyEmitterTest.cs.meta b/Tests/Editor/Cast/Event/Proxy/PointsCastEventProxyEmitterTest.cs.meta
new file mode 100644
index 00000000..6a5dcba8
--- /dev/null
+++ b/Tests/Editor/Cast/Event/Proxy/PointsCastEventProxyEmitterTest.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 64c8628533928b14bb31164a1f676771
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant: