451 lines
16 KiB
C#
451 lines
16 KiB
C#
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<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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <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 || 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Advance dialogue state for the next interaction
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <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;
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
}
|