using UnityEngine; /// /// Place this on an NPC, Item, or Trigger Zone to easily connect it to the Quest System! /// public class QuestInteractable : MonoBehaviour, IInteractable { [Header("Quest Progression")] [Tooltip("What action does this object trigger?")] public ObjectiveType objectiveType; [Tooltip("The unique ID that matches the QuestObjective (e.g., 'wolf', 'ancient_relic')")] public string targetID; [Tooltip("How much to progress the objective (usually 1)")] public int progressAmount = 1; [Header("Interaction Settings")] [Tooltip("Can the player walk up and press E on this object?")] public bool isInteractable = false; [Tooltip("If true, the player can ONLY interact with this if they currently have the matching quest objective active!")] public bool requireActiveObjective = false; [Tooltip("The action verb for the prompt (e.g., 'Open', 'Collect', 'Steal')")] public string interactAction = "Interact"; [Tooltip("The secondary text to display (e.g. the name of the item or 'Press E')")] public string interactTargetName = "[ E ]"; [Header("Destination/Trigger Settings")] [Tooltip("Check this if walking into a Trigger Collider should automatically complete the objective.")] public bool completeOnTriggerEnter = false; [Tooltip("A visual marker GameObject to enable while active, and disable when completed.")] public GameObject destinationMarker; [Tooltip("A TMPro text element to show the distance counting down as the player gets closer.")] public TMPro.TextMeshProUGUI distanceText; [Header("Dynamic Marker Height")] [Tooltip("If true, the marker will physically raise into the sky when far away, and lower to the ground when close.")] public bool scaleMarkerHeight = true; public float maxDistanceForHeight = 50f; public float minHeight = 1.5f; public float maxHeight = 15f; [Tooltip("How far from the center the marker should hit its lowest point. If you have a large Box Collider, set this to half its width (e.g., 3m) so the marker bottoms out right as you touch the edge!")] public float arrivalDistanceOffset = 3f; [Header("Item Settings")] [Tooltip("Check this if this is an item that should destroy itself when collected/triggered")] public bool destroyOnTrigger; private bool _hasBeenTriggered = false; private Transform _playerTransform; private void Start() { // Find player once at start for distance tracking GameObject playerObj = GameObject.FindGameObjectWithTag("Player"); if (playerObj != null) { _playerTransform = playerObj.transform; } } private void Update() { // Update distance countdown if marker is active and we haven't triggered if (completeOnTriggerEnter && !_hasBeenTriggered && destinationMarker != null && destinationMarker.activeInHierarchy) { // Fallback: If the 'Player' tag is on a stationary object or missing, track the Main Camera! // The Main Camera is guaranteed to move with you, so the distance will always update correctly. if (_playerTransform == null || _playerTransform.position == Vector3.zero) { if (Camera.main != null) _playerTransform = Camera.main.transform; } if (_playerTransform != null) { float dist = Vector3.Distance(_playerTransform.position, transform.position); if (distanceText != null) { distanceText.text = $"{Mathf.RoundToInt(dist)}m"; } if (scaleMarkerHeight) { // Subtract the offset so it bottoms out at the edge of the collider instead of the exact center float effectiveDist = Mathf.Max(0, dist - arrivalDistanceOffset); // Calculate a 0 to 1 value (0 = at the edge, 1 = max distance away) float t = Mathf.Clamp01(effectiveDist / maxDistanceForHeight); // Lerp between our min and max height clamps float targetY = Mathf.Lerp(minHeight, maxHeight, t); Vector3 localPos = destinationMarker.transform.localPosition; localPos.y = targetY; destinationMarker.transform.localPosition = localPos; } } } } private void OnTriggerEnter(Collider other) { if (!completeOnTriggerEnter || _hasBeenTriggered) return; // Ensure it's the player that walked into the trigger if (other.CompareTag("Player") || other.transform.root.CompareTag("Player") || other.GetComponentInParent() != null) { TriggerQuestProgress(); } } // --- IInteractable Implementation --- public string GetInteractText() { return interactAction; } public string GetSecondaryInteractText() { return interactTargetName; } public bool CanInteract() { if (!isInteractable || _hasBeenTriggered) return false; // If this option is checked, we ask the QuestManager if this is our current objective! if (requireActiveObjective) { if (QuestManager.Instance == null) return false; return QuestManager.Instance.IsObjectiveActive(objectiveType, targetID); } return true; } public void Interact(GameObject player) { TriggerQuestProgress(); } // ------------------------------------ /// /// Call this from your existing interaction scripts, or a Unity Event when picking up an item! /// public void TriggerQuestProgress() { if (_hasBeenTriggered) return; bool wasSuccessful = false; if (QuestManager.Instance != null) { wasSuccessful = QuestManager.Instance.ProgressObjective(objectiveType, targetID, progressAmount); } // If the objective wasn't active (or was wrong type), we ignore it so they can try again later if (!wasSuccessful) return; _hasBeenTriggered = true; if (destinationMarker != null) { destinationMarker.SetActive(false); } if (destroyOnTrigger) { Destroy(gameObject); } } }