using System; using System.Collections; using System.Collections.Generic; using Core; using Core.Lifecycle; using Interactions; using UnityEngine; using PuzzleS; using UnityEngine.Audio; namespace Dialogue { [AddComponentMenu("AppleHills/Dialogue/Dialogue Component")] [RequireComponent(typeof(AppleAudioSource))] public class DialogueComponent : ManagedBehaviour { [SerializeField] private RuntimeDialogueGraph dialogueGraph; private RuntimeDialogueNode currentNode; private int currentLineIndex; private bool initialized = false; private SpeechBubble speechBubble; private AppleAudioSource appleAudioSource; // Flag to track when a condition has been met but dialogue hasn't advanced yet private bool _conditionSatisfiedPendingAdvance = false; // Track the current slot state for WaitOnSlot nodes private ItemSlotState _currentSlotState = ItemSlotState.None; private PickupItemData _lastSlottedItem; // Properties public bool IsActive { get; private set; } public bool IsCompleted { get; private set; } public string CurrentSpeakerName => dialogueGraph?.speakerName; public override int ManagedAwakePriority => 150; // Dialogue systems protected override void OnManagedAwake() { // Get required components appleAudioSource = GetComponent(); speechBubble = GetComponentInChildren(); if (speechBubble == null) { Debug.LogError("SpeechBubble component is missing on Dialogue Component"); } 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()); } // Register for global events PuzzleManager.Instance.OnStepCompleted += OnAnyPuzzleStepCompleted; ItemManager.Instance.OnItemPickedUp += OnAnyItemPickedUp; ItemManager.Instance.OnCorrectItemSlotted += OnAnyItemSlotted; ItemManager.Instance.OnIncorrectItemSlotted += OnAnyIncorrectItemSlotted; ItemManager.Instance.OnForbiddenItemSlotted += OnAnyForbiddenItemSlotted; ItemManager.Instance.OnItemSlotCleared += OnAnyItemSlotCleared; ItemManager.Instance.OnItemsCombined += OnAnyItemsCombined; } private void OnCharacterArrived() { if (speechBubble == null || !HasAnyLines()) return; // Advance the dialogue state to move to the next content AdvanceDialogueState(); // Check if we have DialogueContent available (prioritizing the new content system) DialogueContent content = GetCurrentDialogueContent(); if (content != null) { // Display the content with the method that handles both text and images // and pass whether there are more lines available for prompt display speechBubble.DisplayDialogueContent(content, HasAnyLines()); // Play audio if available PlayDialogueAudio(content.Audio); // Log the content type for debugging Logging.Debug($"Displaying content type: {content.ContentType} - {(content.ContentType == DialogueContentType.Text ? content.Text : content.Image?.name)}"); } else { // Fall back to legacy text-only method if no DialogueContent is available string line = GetCurrentDialogueLine(); speechBubble.DisplayDialogueLine(line, HasAnyLines()); // Log for debugging Logging.Debug($"Displaying legacy text: {line}"); } } /// /// Play the audio clip for the current dialogue content /// /// Audio clip to play private void PlayDialogueAudio(AudioResource clip) { // Stop any currently playing audio if (appleAudioSource.audioSource.isPlaying) { appleAudioSource.Stop(); } // Play the new clip if it exists if (clip != null) { appleAudioSource.audioSource.resource = clip; appleAudioSource.Play(1); Logging.Debug($"Playing dialogue audio: {clip.name}"); } } /// /// Get the current dialogue content (text or image) /// /// DialogueContent or null if only legacy text content is available private DialogueContent GetCurrentDialogueContent() { // Initialize if needed if (!initialized) { StartDialogue(); } if (!IsActive || IsCompleted || currentNode == null) return null; // Check if we have DialogueContent available if (currentNode.dialogueContent != null && currentNode.dialogueContent.Count > 0) { // For WaitOnSlot nodes, use the appropriate content based on slot state if (currentNode.nodeType == RuntimeDialogueNodeType.WaitOnSlot) { // Choose the appropriate content collection based on the current slot state List contentForState = currentNode.dialogueContent; // Default content switch (_currentSlotState) { case ItemSlotState.Incorrect: // Use incorrect item content if available if (currentNode.incorrectItemContent != null && currentNode.incorrectItemContent.Count > 0) contentForState = currentNode.incorrectItemContent; break; case ItemSlotState.Forbidden: // Use forbidden item content if available if (currentNode.forbiddenItemContent != null && currentNode.forbiddenItemContent.Count > 0) contentForState = currentNode.forbiddenItemContent; break; } // If we have content for this state, return the current one if (contentForState != null && contentForState.Count > 0) { // Make sure index is within bounds int index = Mathf.Clamp(currentLineIndex, 0, contentForState.Count - 1); return contentForState[index]; } return null; // No content for this slot state } else { // For other node types, use the default dialogueContent if (currentLineIndex >= 0 && currentLineIndex < currentNode.dialogueContent.Count) { return currentNode.dialogueContent[currentLineIndex]; } } } // No DialogueContent available, will fall back to legacy text handling return null; } 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; ItemManager.Instance.OnIncorrectItemSlotted -= OnAnyIncorrectItemSlotted; ItemManager.Instance.OnForbiddenItemSlotted -= OnAnyForbiddenItemSlotted; ItemManager.Instance.OnItemSlotCleared -= OnAnyItemSlotCleared; ItemManager.Instance.OnItemsCombined -= OnAnyItemsCombined; } } /// /// 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) return string.Empty; // For WaitOnSlot nodes, use the appropriate line type based on slot state if (currentNode.nodeType == RuntimeDialogueNodeType.WaitOnSlot) { // Choose the appropriate line collection based on the current slot state List linesForState = currentNode.dialogueLines; // Default lines switch (_currentSlotState) { case ItemSlotState.Incorrect: // Use incorrect item lines if available, otherwise fall back to default lines if (currentNode.incorrectItemLines != null && currentNode.incorrectItemLines.Count > 0) linesForState = currentNode.incorrectItemLines; break; case ItemSlotState.Forbidden: // Use forbidden item lines if available, otherwise fall back to default lines if (currentNode.forbiddenItemLines != null && currentNode.forbiddenItemLines.Count > 0) linesForState = currentNode.forbiddenItemLines; break; // For None or Correct state, use the default lines default: linesForState = currentNode.dialogueLines; break; } // If we have lines for this state, return the current one if (linesForState != null && linesForState.Count > 0) { // Make sure index is within bounds int index = Mathf.Clamp(currentLineIndex, 0, linesForState.Count - 1); return linesForState[index]; } } // For other node types, use the default dialogueLines if (currentNode.dialogueLines == 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 currentLine; } /// /// Advance dialogue state for the next interaction /// private void AdvanceDialogueState() { if (!IsActive || IsCompleted || currentNode == null) return; // If the condition was satisfied earlier, move to the next node immediately if (_conditionSatisfiedPendingAdvance) { _conditionSatisfiedPendingAdvance = false; // Reset flag MoveToNextNode(); return; } // First check if we have any dialogueContent to process bool hasDialogueContent = currentNode.dialogueContent != null && currentNode.dialogueContent.Count > 0; if (hasDialogueContent) { // If we have dialogueContent and there are more entries, advance to the next one if (currentLineIndex < currentNode.dialogueContent.Count - 1) { currentLineIndex++; return; } // If we should loop through content, reset the index if (currentNode.loopThroughLines && currentNode.dialogueContent.Count > 0) { currentLineIndex = 0; return; } } else { // Fall back to legacy dialogueLines // 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() { Logging.Debug("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.WaitOnCombination: // Check if the result item is already created through combination if (IsResultItemCreated(currentNode.combinationResultItemID)) { // If it's already created, 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) { // Initialize if needed if (!initialized) { StartDialogue(); } // 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) { // Set the flag that condition is satisfied _conditionSatisfiedPendingAdvance = true; UpdateDialogueVisibilityOnItemEvent(); } } private void OnAnyItemPickedUp(PickupItemData item) { // Initialize if needed if (!initialized) { StartDialogue(); } // 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) { // Set the flag that condition is satisfied _conditionSatisfiedPendingAdvance = true; UpdateDialogueVisibilityOnItemEvent(); } } private void OnAnyItemSlotted(PickupItemData slotDefinition, PickupItemData slottedItem) { Logging.Debug("[DialogueComponent] OnAnyItemSlotted"); // Initialize if needed if (!initialized) { StartDialogue(); } // 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) { // Set the flag that condition is satisfied _conditionSatisfiedPendingAdvance = true; UpdateDialogueVisibilityOnItemEvent(); } } private void OnAnyItemsCombined(PickupItemData itemA, PickupItemData itemB, PickupItemData resultItem) { // Initialize if needed if (!initialized) { StartDialogue(); } // Only react if we're active and waiting on a combination if (!IsActive || IsCompleted || currentNode == null || currentNode.nodeType != RuntimeDialogueNodeType.WaitOnCombination) return; // Check if this is the result item we're waiting for if (resultItem.itemId == currentNode.combinationResultItemID) { // Set the flag that condition is satisfied _conditionSatisfiedPendingAdvance = true; UpdateDialogueVisibilityOnItemEvent(); } } private void OnAnyIncorrectItemSlotted(PickupItemData slotDefinition, PickupItemData slottedItem) { // Initialize if needed if (!initialized) { StartDialogue(); } // Update the slot state for displaying the correct dialogue lines if (!IsActive || IsCompleted || currentNode == null) return; // Only update state if we're actively waiting on this slot if (currentNode.nodeType == RuntimeDialogueNodeType.WaitOnSlot && slotDefinition.itemId == currentNode.slotItemID) { _currentSlotState = ItemSlotState.Incorrect; _lastSlottedItem = slottedItem; UpdateDialogueVisibilityOnItemEvent(); } } private void OnAnyForbiddenItemSlotted(PickupItemData slotDefinition, PickupItemData slottedItem) { // Initialize if needed if (!initialized) { StartDialogue(); } // Update the slot state for displaying the correct dialogue lines if (!IsActive || IsCompleted || currentNode == null) return; // Only update state if we're actively waiting on this slot if (currentNode.nodeType == RuntimeDialogueNodeType.WaitOnSlot && slotDefinition.itemId == currentNode.slotItemID) { _currentSlotState = ItemSlotState.Forbidden; _lastSlottedItem = slottedItem; UpdateDialogueVisibilityOnItemEvent(); } } private void OnAnyItemSlotCleared(PickupItemData removedItem) { // Initialize if needed if (!initialized) { StartDialogue(); } // Update the slot state when an item is removed if (!IsActive || IsCompleted || currentNode == null) return; // Reset slot state if we were tracking this item if (_lastSlottedItem != null && _lastSlottedItem == removedItem) { _currentSlotState = ItemSlotState.None; _lastSlottedItem = null; UpdateDialogueVisibilityOnItemEvent(); } } private void UpdateDialogueVisibilityOnItemEvent() { // If auto-play is enabled, immediately display dialogue if (currentNode.shouldAutoPlay) { OnCharacterArrived(); } else { // Manual mode: just update bubble visibility to show interaction prompt 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); case RuntimeDialogueNodeType.WaitOnCombination: return !IsResultItemCreated(currentNode.combinationResultItemID); 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; } private bool IsResultItemCreated(string resultItemId) { if (ItemManager.Instance == null) return false; // Use the ItemManager's tracking of items created through combination return ItemManager.Instance.WasItemCreatedThroughCombination(resultItemId); } /// /// 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; // Special case: if condition has been satisfied but not yet advanced, we should show lines if (_conditionSatisfiedPendingAdvance && !string.IsNullOrEmpty(currentNode.nextNodeID)) { // Check if the next node would have lines or content RuntimeDialogueNode nextNode = dialogueGraph.GetNodeByID(currentNode.nextNodeID); return nextNode != null && ((nextNode.dialogueLines != null && nextNode.dialogueLines.Count > 0) || (nextNode.dialogueContent != null && nextNode.dialogueContent.Count > 0) || nextNode.nodeType != RuntimeDialogueNodeType.End); } // For WaitOnSlot nodes, check for lines or content based on current slot state if (currentNode.nodeType == RuntimeDialogueNodeType.WaitOnSlot) { // First check for DialogueContent if (currentNode.dialogueContent != null && currentNode.dialogueContent.Count > 0) { // Choose the appropriate content collection based on the current slot state List contentForState = currentNode.dialogueContent; switch (_currentSlotState) { case ItemSlotState.Incorrect: if (currentNode.incorrectItemContent != null && currentNode.incorrectItemContent.Count > 0) contentForState = currentNode.incorrectItemContent; break; case ItemSlotState.Forbidden: if (currentNode.forbiddenItemContent != null && currentNode.forbiddenItemContent.Count > 0) contentForState = currentNode.forbiddenItemContent; break; } if (contentForState.Count > 0) { if (currentLineIndex < contentForState.Count - 1 || currentNode.loopThroughLines) { return true; } } } // Fall back to legacy text lines List linesForState = currentNode.dialogueLines; switch (_currentSlotState) { case ItemSlotState.Incorrect: if (currentNode.incorrectItemLines != null && currentNode.incorrectItemLines.Count > 0) linesForState = currentNode.incorrectItemLines; break; case ItemSlotState.Forbidden: if (currentNode.forbiddenItemLines != null && currentNode.forbiddenItemLines.Count > 0) linesForState = currentNode.forbiddenItemLines; break; } if (linesForState != null && linesForState.Count > 0) { if (currentLineIndex < linesForState.Count - 1 || currentNode.loopThroughLines) { return true; } } } // For other node types, check for DialogueContent first, then fall back to legacy text else { // Check for DialogueContent if (currentNode.dialogueContent != null && currentNode.dialogueContent.Count > 0) { if (currentLineIndex < currentNode.dialogueContent.Count - 1 || currentNode.loopThroughLines) { return true; } // If we're at the end of content but not waiting for a condition and have a next node if (!IsWaitingForCondition() && !string.IsNullOrEmpty(currentNode.nextNodeID)) { RuntimeDialogueNode nextNode = dialogueGraph.GetNodeByID(currentNode.nextNodeID); return nextNode != null && ((nextNode.dialogueContent != null && nextNode.dialogueContent.Count > 0) || (nextNode.dialogueLines != null && nextNode.dialogueLines.Count > 0) || nextNode.nodeType != RuntimeDialogueNodeType.End); } } // Fall back to legacy text lines if (currentNode.dialogueLines != null && currentNode.dialogueLines.Count > 0) { 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)) { RuntimeDialogueNode nextNode = dialogueGraph.GetNodeByID(currentNode.nextNodeID); return nextNode != null && ((nextNode.dialogueContent != null && nextNode.dialogueContent.Count > 0) || (nextNode.dialogueLines != null && nextNode.dialogueLines.Count > 0) || nextNode.nodeType != RuntimeDialogueNodeType.End); } } } return false; } // Editor functionality public void SetDialogueGraph(RuntimeDialogueGraph graph) { dialogueGraph = graph; } } }