using System; using System.Collections; using System.Collections.Generic; using Core; using Interactions; using UnityEngine; using PuzzleS; namespace Dialogue { [AddComponentMenu("Apple Hills/Dialogue/Dialogue Component")] public class DialogueComponent : MonoBehaviour { [SerializeField] private RuntimeDialogueGraph dialogueGraph; private RuntimeDialogueNode currentNode; private int currentLineIndex; private bool initialized = false; private SpeechBubble speechBubble; // Properties public bool IsActive { get; private set; } public bool IsCompleted { get; private set; } public string CurrentSpeakerName => dialogueGraph?.speakerName; // Event for UI updates if needed public event Action OnDialogueChanged; private void Start() { // Register for global events if (PuzzleManager.Instance != null) PuzzleManager.Instance.OnStepCompleted += OnAnyPuzzleStepCompleted; if (ItemManager.Instance != null) { ItemManager.Instance.OnItemPickedUp += OnAnyItemPickedUp; ItemManager.Instance.OnCorrectItemSlotted += OnAnyItemSlotted; } speechBubble = GetComponentInChildren(); if (speechBubble == null) { Debug.LogError("SpeechBubble component is missing on Dialogue Component"); } // Auto-start the dialogue // StartDialogue(); var interactable = GetComponent(); if (interactable != null) { interactable.characterArrived.AddListener(OnCharacterArrived); } // Update bubble visibility based on whether we have lines if (speechBubble != null) { speechBubble.UpdatePromptVisibility(HasAnyLines()); } } private void OnCharacterArrived() { if (speechBubble == null || ! HasAnyLines()) return; AdvanceDialogueState(); // Get the current dialogue line string line = GetCurrentDialogueLine(); // Display the line with the new method that handles timed updates speechBubble.DisplayDialogueLine(line, HasAnyLines()); // Advance dialogue state for next interaction } private void OnDestroy() { // Unregister from events if (PuzzleManager.Instance != null) PuzzleManager.Instance.OnStepCompleted -= OnAnyPuzzleStepCompleted; if (ItemManager.Instance != null) { ItemManager.Instance.OnItemPickedUp -= OnAnyItemPickedUp; ItemManager.Instance.OnCorrectItemSlotted -= OnAnyItemSlotted; } } /// /// Start the dialogue from the beginning /// public void StartDialogue() { if (dialogueGraph == null) { Debug.LogError("DialogueComponent: No dialogue graph assigned!"); return; } // Reset state IsActive = true; IsCompleted = false; currentLineIndex = 0; initialized = true; // Set to entry node currentNode = dialogueGraph.GetNodeByID(dialogueGraph.entryNodeID); // Process the node ProcessCurrentNode(); // Update bubble visibility based on whether we have lines if (speechBubble != null) { speechBubble.UpdatePromptVisibility(HasAnyLines()); } } /// /// Get the current dialogue line and advance to the next line or node if appropriate /// Each call represents one interaction with the NPC /// public string GetCurrentDialogueLine() { // Initialize if needed if (!initialized) { StartDialogue(); } if (!IsActive || IsCompleted || currentNode == null || currentNode.dialogueLines.Count == 0) return string.Empty; // Get current line string currentLine = string.Empty; if (currentLineIndex >= 0 && currentLineIndex < currentNode.dialogueLines.Count) { currentLine = currentNode.dialogueLines[currentLineIndex]; } // Return the current line return currentLine; } /// /// Advance dialogue state for the next interaction /// private void AdvanceDialogueState() { if (!IsActive || IsCompleted || currentNode == null) return; // If we have more lines in the current node, advance to the next line if (currentLineIndex < currentNode.dialogueLines.Count - 1) { currentLineIndex++; return; } // If we should loop through lines, reset the index if (currentNode.loopThroughLines && currentNode.dialogueLines.Count > 0) { currentLineIndex = 0; return; } // If we're at a node that doesn't have a next node, we're done if (string.IsNullOrEmpty(currentNode.nextNodeID)) { IsActive = false; IsCompleted = true; return; } // Move to the next node only if no conditions to wait for if (!IsWaitingForCondition()) { MoveToNextNode(); } } private void MoveToNextNode() { // If there's no next node, complete the dialogue if (string.IsNullOrEmpty(currentNode.nextNodeID)) { IsActive = false; IsCompleted = true; return; } // Move to the next node currentNode = dialogueGraph.GetNodeByID(currentNode.nextNodeID); currentLineIndex = 0; // Process the new node ProcessCurrentNode(); } private void ProcessCurrentNode() { if (currentNode == null) { Debug.LogError("DialogueComponent: Current node is null!"); return; } // Handle different node types switch (currentNode.nodeType) { case RuntimeDialogueNodeType.Dialogue: // Regular dialogue node, nothing special to do break; case RuntimeDialogueNodeType.WaitOnPuzzleStep: // Check if the puzzle step is already completed if (IsPuzzleStepComplete(currentNode.puzzleStepID)) { // If it's already complete, move past this node automatically MoveToNextNode(); } break; case RuntimeDialogueNodeType.WaitOnPickup: // Check if the item is already picked up if (IsItemPickedUp(currentNode.pickupItemID)) { // If it's already picked up, move past this node automatically MoveToNextNode(); } break; case RuntimeDialogueNodeType.WaitOnSlot: // Check if the item is already slotted if (IsItemSlotted(currentNode.slotItemID)) { // If it's already slotted, move past this node automatically MoveToNextNode(); } break; case RuntimeDialogueNodeType.End: // End node, complete the dialogue IsActive = false; IsCompleted = true; break; default: Debug.LogError($"DialogueComponent: Unknown node type {currentNode.nodeType}"); break; } } // Global event handlers private void OnAnyPuzzleStepCompleted(PuzzleStepSO step) { // Only react if we're active and waiting on a puzzle step if (!IsActive || IsCompleted || currentNode == null || currentNode.nodeType != RuntimeDialogueNodeType.WaitOnPuzzleStep) return; // Check if this is the step we're waiting for if (step.stepId == currentNode.puzzleStepID) { // Move to next node automatically when condition is met MoveToNextNode(); // Notify any listeners about the dialogue change string line = GetCurrentDialogueLine(); OnDialogueChanged?.Invoke(line); // Update bubble visibility after state change if (speechBubble != null) { speechBubble.UpdatePromptVisibility(HasAnyLines()); } } } private void OnAnyItemPickedUp(PickupItemData item) { // Only react if we're active and waiting on an item pickup if (!IsActive || IsCompleted || currentNode == null || currentNode.nodeType != RuntimeDialogueNodeType.WaitOnPickup) return; // Check if this is the item we're waiting for if (item.itemId == currentNode.pickupItemID) { // Move to next node automatically when condition is met MoveToNextNode(); // Notify any listeners about the dialogue change string line = GetCurrentDialogueLine(); OnDialogueChanged?.Invoke(line); // Update bubble visibility after state change if (speechBubble != null) { speechBubble.UpdatePromptVisibility(HasAnyLines()); } } // Always check if any dialogue was unblocked by this pickup if (speechBubble != null) { speechBubble.UpdatePromptVisibility(HasAnyLines()); } } private void OnAnyItemSlotted(PickupItemData slotDefinition, PickupItemData slottedItem) { // Only react if we're active and waiting on a slot if (!IsActive || IsCompleted || currentNode == null || currentNode.nodeType != RuntimeDialogueNodeType.WaitOnSlot) return; // Check if this is the slot we're waiting for if (slotDefinition.itemId == currentNode.slotItemID) { // Move to next node automatically when condition is met MoveToNextNode(); // Notify any listeners about the dialogue change string line = GetCurrentDialogueLine(); OnDialogueChanged?.Invoke(line); // Update bubble visibility after state change if (speechBubble != null) { speechBubble.UpdatePromptVisibility(HasAnyLines()); } } // Always check if any dialogue was unblocked by this slotting if (speechBubble != null) { speechBubble.UpdatePromptVisibility(HasAnyLines()); } } // Helper methods private bool IsWaitingForCondition() { if (currentNode == null) return false; switch (currentNode.nodeType) { case RuntimeDialogueNodeType.WaitOnPuzzleStep: return !IsPuzzleStepComplete(currentNode.puzzleStepID); case RuntimeDialogueNodeType.WaitOnPickup: return !IsItemPickedUp(currentNode.pickupItemID); case RuntimeDialogueNodeType.WaitOnSlot: return !IsItemSlotted(currentNode.slotItemID); default: return false; } } private bool IsPuzzleStepComplete(string stepID) { return PuzzleManager.Instance != null && PuzzleManager.Instance.IsPuzzleStepCompleted(stepID); } private bool IsItemPickedUp(string itemID) { if (ItemManager.Instance == null) return false; // Check all pickups for the given ID foreach (var pickup in ItemManager.Instance.Pickups) { if (pickup.isPickedUp && pickup.itemData != null && pickup.itemData.itemId == itemID) { return true; } } return false; } private bool IsItemSlotted(string slotID) { if (ItemManager.Instance == null) return false; // Check if any slot with this ID has the correct item foreach (var slot in ItemManager.Instance.ItemSlots) { if (slot.itemData != null && slot.itemData.itemId == slotID && slot.CurrentSlottedState == ItemSlotState.Correct) { return true; } } return false; } /// /// Checks if the dialogue component has any lines available to serve /// /// True if there are lines available, false otherwise public bool HasAnyLines() { if (!initialized) { // If not initialized yet but has a dialogue graph, it will have lines when initialized return dialogueGraph != null; } // No lines if dialogue is not active or is completed if (!IsActive || IsCompleted || currentNode == null) return false; // Check if the current node has any lines if (currentNode.dialogueLines.Count > 0) { // If we're not at the end of the lines or we loop through them if (currentLineIndex < currentNode.dialogueLines.Count - 1 || currentNode.loopThroughLines) { return true; } // If we're at the end of lines but not waiting for a condition and have a next node if (!IsWaitingForCondition() && !string.IsNullOrEmpty(currentNode.nextNodeID)) { // We need to check if the next node would have lines RuntimeDialogueNode nextNode = dialogueGraph.GetNodeByID(currentNode.nextNodeID); return nextNode != null && (nextNode.dialogueLines.Count > 0 || nextNode.nodeType != RuntimeDialogueNodeType.End); } } // Special case for conditional nodes waiting on conditions // if (IsWaitingForCondition()) // { // return currentNode.dialogueLines.Count > 0; // } return false; } // Editor functionality public void SetDialogueGraph(RuntimeDialogueGraph graph) { dialogueGraph = graph; } } }