Files
AppleHillsProduction/Assets/Scripts/Dialogue/DialogueComponent.cs

628 lines
25 KiB
C#
Raw Normal View History

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;
// 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;
// Event for UI updates if needed
public event Action<string> 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;
ItemManager.Instance.OnIncorrectItemSlotted += OnAnyIncorrectItemSlotted;
ItemManager.Instance.OnForbiddenItemSlotted += OnAnyForbiddenItemSlotted;
ItemManager.Instance.OnItemSlotCleared += OnAnyItemSlotCleared;
ItemManager.Instance.OnItemsCombined += OnAnyItemsCombined;
}
speechBubble = GetComponentInChildren<SpeechBubble>();
if (speechBubble == null)
{
Debug.LogError("SpeechBubble component is missing on Dialogue Component");
}
// Auto-start the dialogue
// StartDialogue();
var interactable = GetComponent<Interactable>();
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;
ItemManager.Instance.OnIncorrectItemSlotted -= OnAnyIncorrectItemSlotted;
ItemManager.Instance.OnForbiddenItemSlotted -= OnAnyForbiddenItemSlotted;
ItemManager.Instance.OnItemSlotCleared -= OnAnyItemSlotCleared;
ItemManager.Instance.OnItemsCombined -= OnAnyItemsCombined;
}
}
/// <summary>
/// Start the dialogue from the beginning
/// </summary>
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());
}
}
/// <summary>
/// Get the current dialogue line and advance to the next line or node if appropriate
/// Each call represents one interaction with the NPC
/// </summary>
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<string> 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;
}
/// <summary>
/// Advance dialogue state for the next interaction
/// </summary>
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;
}
// 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()
{
Debug.Log("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)
{
// 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)
{
// Instead of immediately moving to the next node, set the flag
_conditionSatisfiedPendingAdvance = true;
// Update bubble visibility after state change to show interaction prompt
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)
{
// Instead of immediately moving to the next node, set the flag
_conditionSatisfiedPendingAdvance = true;
// Update bubble visibility after state change to show interaction prompt
if (speechBubble != null)
{
speechBubble.UpdatePromptVisibility(HasAnyLines());
}
}
}
private void OnAnyItemSlotted(PickupItemData slotDefinition, PickupItemData slottedItem)
{
Debug.Log("[DialogueComponent] OnAnyItemSlotted");
// 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)
{
// Instead of immediately moving to the next node, set the flag
_conditionSatisfiedPendingAdvance = true;
// Update bubble visibility after state change to show interaction prompt
if (speechBubble != null)
{
speechBubble.UpdatePromptVisibility(HasAnyLines());
}
}
}
private void OnAnyItemsCombined(PickupItemData itemA, PickupItemData itemB, PickupItemData resultItem)
{
// 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)
{
// Instead of immediately moving to the next node, set the flag
_conditionSatisfiedPendingAdvance = true;
// Update bubble visibility after state change to show interaction prompt
if (speechBubble != null)
{
speechBubble.UpdatePromptVisibility(HasAnyLines());
}
}
}
private void OnAnyIncorrectItemSlotted(PickupItemData slotDefinition, PickupItemData slottedItem)
{
// 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;
// Trigger dialogue update
if (speechBubble != null)
{
speechBubble.UpdatePromptVisibility(HasAnyLines());
}
}
}
private void OnAnyForbiddenItemSlotted(PickupItemData slotDefinition, PickupItemData slottedItem)
{
// 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;
// Trigger dialogue update
if (speechBubble != null)
{
speechBubble.UpdatePromptVisibility(HasAnyLines());
}
}
}
private void OnAnyItemSlotCleared(PickupItemData removedItem)
{
// 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;
// Trigger dialogue update
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);
}
/// <summary>
/// Checks if the dialogue component has any lines available to serve
/// </summary>
/// <returns>True if there are lines available, false otherwise</returns>
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
RuntimeDialogueNode nextNode = dialogueGraph.GetNodeByID(currentNode.nextNodeID);
return nextNode != null && (nextNode.dialogueLines.Count > 0 || nextNode.nodeType != RuntimeDialogueNodeType.End);
}
// For WaitOnSlot nodes, check for lines based on current slot state
if (currentNode.nodeType == RuntimeDialogueNodeType.WaitOnSlot)
{
// Choose the appropriate line collection based on the current slot state
List<string> 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;
}
// Check if we have any lines for the current state
if (linesForState != null && linesForState.Count > 0)
{
// If we're not at the end of the lines or we loop through them
if (currentLineIndex < linesForState.Count - 1 || currentNode.loopThroughLines)
{
return true;
}
}
}
// For other node types, use the standard check
else 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);
}
}
return false;
}
// Editor functionality
public void SetDialogueGraph(RuntimeDialogueGraph graph)
{
dialogueGraph = graph;
}
}
}