// --- THIS SCRIPT CAN BE PLACED ANYWHERE IN YOUR ASSETS FOLDER --- using UnityEngine; using System.Collections.Generic; using Sirenix.OdinInspector; using PixelCrushers.DialogueSystem; // Required for [ConversationPopup] attribute using PixelCrushers.QuestMachine; using Invector.vCharacterController.vActions; // Required for the vTriggerGenericAction reference using UnityEngine.Events; namespace Beyond { [AddComponentMenu("Beyond/Quests/Generic Quest Trigger")] [RequireComponent(typeof(vTriggerGenericAction))] // IMPORTANT: Ensures the Invector trigger is always on the same GameObject public class GenericQuestTrigger : MonoBehaviour { #region --- ODIN-POWERED INSPECTOR --- [Title("Invector Trigger Link")] [InfoBox("This component links into the vTriggerGenericAction below and adds quest/dialogue functionality to its events.")] [Required("A vTriggerGenericAction component is required on this GameObject.")] [OnInspectorInit("FindTriggerReference")] // Odin attribute to auto-fill the reference on first view public vTriggerGenericAction invectorTrigger; [Title("Quest and Dialogue Actions")] [ListDrawerSettings(Expanded = true, DraggableItems = true, NumberOfItemsPerPage = 10)] public List actions = new List(); #endregion private GameObject _lastInteractor; // This is a helper method for the Odin attribute [OnInspectorInit] private void FindTriggerReference() { if (invectorTrigger == null) { invectorTrigger = GetComponent(); } } #region --- CORE LOGIC (Lifecycle & Event Handlers) --- private void Start() { if (invectorTrigger == null) { Debug.LogError("GenericQuestTrigger requires a vTriggerGenericAction component, but none was found. Disabling component.", this); this.enabled = false; return; } // Subscribe our methods to the events on the Invector component invectorTrigger.OnPressActionInput.AddListener(() => HandleEvent(TriggerEventType.OnPressActionInput, _lastInteractor)); invectorTrigger.OnStartAnimation.AddListener(() => HandleEvent(TriggerEventType.OnStartAnimation, _lastInteractor)); invectorTrigger.OnEndAnimation.AddListener(() => HandleEvent(TriggerEventType.OnEndAnimation, _lastInteractor)); invectorTrigger.OnPlayerEnter.AddListener(HandlePlayerEnter); invectorTrigger.OnPlayerExit.AddListener(HandlePlayerExit); invectorTrigger.OnValidate.AddListener((interactor) => HandleEvent(TriggerEventType.OnValidate, interactor)); invectorTrigger.OnInvalidate.AddListener((interactor) => HandleEvent(TriggerEventType.OnInvalidate, interactor)); if (DialogueManager.instance != null) { DialogueManager.instance.conversationEnded += OnConversationEnded; } } private void OnDisable() { if (DialogueManager.instance != null) { DialogueManager.instance.conversationEnded -= OnConversationEnded; } // It's good practice to unsubscribe from events when disabled/destroyed to prevent memory leaks if (invectorTrigger != null) { invectorTrigger.OnPressActionInput.RemoveListener(() => HandleEvent(TriggerEventType.OnPressActionInput, _lastInteractor)); invectorTrigger.OnStartAnimation.RemoveListener(() => HandleEvent(TriggerEventType.OnStartAnimation, _lastInteractor)); invectorTrigger.OnEndAnimation.RemoveListener(() => HandleEvent(TriggerEventType.OnEndAnimation, _lastInteractor)); invectorTrigger.OnPlayerEnter.RemoveListener(HandlePlayerEnter); invectorTrigger.OnPlayerExit.RemoveListener(HandlePlayerExit); invectorTrigger.OnValidate.RemoveListener((interactor) => HandleEvent(TriggerEventType.OnValidate, interactor)); invectorTrigger.OnInvalidate.RemoveListener((interactor) => HandleEvent(TriggerEventType.OnInvalidate, interactor)); } } private void HandlePlayerEnter(GameObject interactor) { _lastInteractor = interactor; HandleEvent(TriggerEventType.OnPlayerEnter, interactor); } private void HandlePlayerExit(GameObject interactor) { HandleEvent(TriggerEventType.OnPlayerExit, interactor); _lastInteractor = null; } private void OnConversationEnded(Transform actor) { if (_lastInteractor != null && actor.gameObject == _lastInteractor) { HandleEvent(TriggerEventType.OnConversationEnd, actor.gameObject); } } private void HandleEvent(TriggerEventType eventType, GameObject interactor) { foreach (var action in actions) { if (action.triggerEvent == eventType) { // Check if the optional Dialogue System condition is met if (action.condition == null || action.condition.IsTrue(interactor?.transform)) { PerformAction(action, interactor); } } } } private void PerformAction(TriggeredAction action, GameObject interactor) { var actorTransform = interactor?.transform; switch (action.actionType) { case ActionType.StartConversation: if (!string.IsNullOrEmpty(action.conversation)) { Transform conversant = action.conversant != null ? action.conversant : this.transform; DialogueManager.StartConversation(action.conversation, actorTransform, conversant); } break; case ActionType.ExecuteLua: if (!string.IsNullOrEmpty(action.luaCode)) { Lua.Run(action.luaCode, true); } break; case ActionType.SendQuestMachineMessage: if (!string.IsNullOrEmpty(action.questMachineMessage)) { QuestMachineMessages.SendCompositeMessage(this, action.questMachineMessage); } break; case ActionType.SetQuestState: if (!string.IsNullOrEmpty(action.questID)) { QuestMachine.SetQuestState(action.questID, action.questState); } break; case ActionType.SetQuestNodeState: if (!string.IsNullOrEmpty(action.questID) && !string.IsNullOrEmpty(action.questNodeID)) { QuestMachine.SetQuestNodeState(action.questID, action.questNodeID, action.questNodeState); } break; case ActionType.InvokeUnityEvent: action.onExecute?.Invoke(); break; } } #endregion } #region --- Action Definitions (Enums and Class) --- public enum TriggerEventType { OnPlayerEnter, OnPlayerExit, OnValidate, OnInvalidate, OnPressActionInput, OnStartAnimation, OnEndAnimation, OnConversationEnd } public enum ActionType { StartConversation, ExecuteLua, SendQuestMachineMessage, SetQuestState, SetQuestNodeState, InvokeUnityEvent } [System.Serializable] public class TriggeredAction { [HorizontalGroup("Top", 120)] [BoxGroup("Top/Trigger", showLabel: false)] [EnumToggleButtons, HideLabel] public TriggerEventType triggerEvent; [BoxGroup("Top/Action", showLabel: false)] [EnumToggleButtons, HideLabel] public ActionType actionType; [BoxGroup("Settings")] [Tooltip("Optional Dialogue System condition that must be true for this action to fire.")] [DrawWithUnity] // <-- Tells Odin to use the Pixel Crushers drawer for this field. public Condition condition; [BoxGroup("Settings")] [ShowIf("actionType", ActionType.StartConversation)] [ConversationPopup(false)] // <-- The Dialogue System attribute that creates the dropdown. [DrawWithUnity] // <-- The Odin attribute that allows the above attribute to work. public string conversation; [BoxGroup("Settings")] [ShowIf("actionType", ActionType.StartConversation)] [Tooltip("The other participant in the conversation. If unassigned, this trigger object will be the conversant.")] public Transform conversant; [BoxGroup("Settings")] [ShowIf("actionType", ActionType.ExecuteLua)] [TextArea(2, 5)] public string luaCode; [BoxGroup("Settings")] [ShowIf("actionType", ActionType.SendQuestMachineMessage)] public string questMachineMessage; [BoxGroup("Settings")] [ShowIf("@this.actionType == ActionType.SetQuestState || this.actionType == ActionType.SetQuestNodeState")] [DrawWithUnity] // <-- Tells Odin to use the Quest Machine drawer for the quest ID field. public string questID; [BoxGroup("Settings")] [ShowIf("actionType", ActionType.SetQuestState)] public PixelCrushers.QuestMachine.QuestState questState; [BoxGroup("Settings")] [ShowIf("actionType", ActionType.SetQuestNodeState)] [DrawWithUnity] // <-- Tells Odin to use the Quest Machine drawer for the node ID field. public string questNodeID; [BoxGroup("Settings")] [ShowIf("actionType", ActionType.SetQuestNodeState)] public PixelCrushers.QuestMachine.QuestNodeState questNodeState; [BoxGroup("Settings")] [ShowIf("actionType", ActionType.InvokeUnityEvent)] public UnityEvent onExecute; } #endregion }