Create a simple dialogue authoring system, tied into our items (#10)
- Editor dialogue graph - Asset importer for processing the graph into runtime data - DialogueComponent that steers the dialogue interactions - DialogueCanbas with a scalable speech bubble to display everything - Brief README overview of the system Co-authored-by: AlexanderT <alexander@foolhardyhorizons.com> Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com> Reviewed-on: #10
This commit is contained in:
263
Assets/Scripts/Core/ItemManager.cs
Normal file
263
Assets/Scripts/Core/ItemManager.cs
Normal file
@@ -0,0 +1,263 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Interactions;
|
||||
|
||||
namespace Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Central registry for pickups and item slots.
|
||||
/// Mirrors the singleton pattern used by PuzzleManager.
|
||||
/// </summary>
|
||||
public class ItemManager : MonoBehaviour
|
||||
{
|
||||
private static ItemManager _instance;
|
||||
private static bool _isQuitting;
|
||||
|
||||
public static ItemManager Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_instance == null && Application.isPlaying && !_isQuitting)
|
||||
{
|
||||
_instance = FindAnyObjectByType<ItemManager>();
|
||||
if (_instance == null)
|
||||
{
|
||||
var go = new GameObject("ItemManager");
|
||||
_instance = go.AddComponent<ItemManager>();
|
||||
// DontDestroyOnLoad(go);
|
||||
}
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly HashSet<Pickup> _pickups = new HashSet<Pickup>();
|
||||
private readonly HashSet<ItemSlot> _itemSlots = new HashSet<ItemSlot>();
|
||||
private readonly HashSet<string> _itemsCreatedThroughCombination = new HashSet<string>();
|
||||
|
||||
// Central events forwarded from registered pickups/slots
|
||||
// Broadcasts when any registered pickup was picked up (passes the picked item data)
|
||||
public event Action<PickupItemData> OnItemPickedUp;
|
||||
|
||||
// Broadcasts when any registered ItemSlot reports a correct item slotted
|
||||
// Args: slot's itemData (the slot definition), then the slotted item data
|
||||
public event Action<PickupItemData, PickupItemData> OnCorrectItemSlotted;
|
||||
|
||||
// Broadcasts when any registered ItemSlot reports an incorrect item slotted
|
||||
// Args: slot's itemData (the slot definition), then the slotted item data
|
||||
public event Action<PickupItemData, PickupItemData> OnIncorrectItemSlotted;
|
||||
|
||||
// Broadcasts when any registered ItemSlot reports a forbidden item slotted
|
||||
// Args: slot's itemData (the slot definition), then the slotted item data
|
||||
public event Action<PickupItemData, PickupItemData> OnForbiddenItemSlotted;
|
||||
|
||||
// Broadcasts when any registered ItemSlot is cleared (item removed)
|
||||
// Args: the item data that was removed
|
||||
public event Action<PickupItemData> OnItemSlotCleared;
|
||||
|
||||
// Broadcasts when any two items are successfully combined
|
||||
// Args: first item data, second item data, result item data
|
||||
public event Action<PickupItemData, PickupItemData, PickupItemData> OnItemsCombined;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
_instance = this;
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
// Subscribe to scene load completed so we can clear registrations when scenes change
|
||||
// Access Instance directly to ensure the service is initialized and we get the event hookup.
|
||||
SceneManagerService.Instance.SceneLoadCompleted += OnSceneLoadCompleted;
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
// Unsubscribe from SceneManagerService
|
||||
if (SceneManagerService.Instance != null)
|
||||
SceneManagerService.Instance.SceneLoadCompleted -= OnSceneLoadCompleted;
|
||||
|
||||
// Ensure we clean up any subscriptions from registered items when the manager is destroyed
|
||||
ClearAllRegistrations();
|
||||
}
|
||||
|
||||
void OnApplicationQuit()
|
||||
{
|
||||
_isQuitting = true;
|
||||
}
|
||||
|
||||
private void OnSceneLoadCompleted(string sceneName)
|
||||
{
|
||||
// Clear all registrations when a new scene is loaded, so no stale references persist
|
||||
ClearAllRegistrations();
|
||||
}
|
||||
|
||||
// Handler that forwards pickup events
|
||||
private void Pickup_OnItemPickedUp(PickupItemData data)
|
||||
{
|
||||
OnItemPickedUp?.Invoke(data);
|
||||
}
|
||||
|
||||
// Handler that forwards correct-slot events
|
||||
private void ItemSlot_OnCorrectItemSlotted(PickupItemData slotData, PickupItemData slottedItem)
|
||||
{
|
||||
OnCorrectItemSlotted?.Invoke(slotData, slottedItem);
|
||||
}
|
||||
|
||||
// Handler that forwards incorrect-slot events
|
||||
private void ItemSlot_OnIncorrectItemSlotted(PickupItemData slotData, PickupItemData slottedItem)
|
||||
{
|
||||
OnIncorrectItemSlotted?.Invoke(slotData, slottedItem);
|
||||
}
|
||||
|
||||
// Handler that forwards forbidden-slot events
|
||||
private void ItemSlot_OnForbiddenItemSlotted(PickupItemData slotData, PickupItemData slottedItem)
|
||||
{
|
||||
OnForbiddenItemSlotted?.Invoke(slotData, slottedItem);
|
||||
}
|
||||
|
||||
// Handler that forwards item combination events
|
||||
private void Pickup_OnItemsCombined(PickupItemData itemA, PickupItemData itemB, PickupItemData resultItem)
|
||||
{
|
||||
// Track the created item
|
||||
if (resultItem != null)
|
||||
{
|
||||
_itemsCreatedThroughCombination.Add(resultItem.itemId);
|
||||
}
|
||||
|
||||
OnItemsCombined?.Invoke(itemA, itemB, resultItem);
|
||||
}
|
||||
|
||||
// Handler that forwards slot-removed events
|
||||
private void ItemSlot_OnItemSlotRemoved(PickupItemData removedItem)
|
||||
{
|
||||
OnItemSlotCleared?.Invoke(removedItem);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribe all pickup/slot event handlers and clear registries and manager events.
|
||||
/// </summary>
|
||||
private void ClearAllRegistrations()
|
||||
{
|
||||
// Unsubscribe pickup handlers
|
||||
var pickupsCopy = new List<Pickup>(_pickups);
|
||||
foreach (var p in pickupsCopy)
|
||||
{
|
||||
if (p != null)
|
||||
{
|
||||
p.OnItemPickedUp -= Pickup_OnItemPickedUp;
|
||||
p.OnItemsCombined -= Pickup_OnItemsCombined;
|
||||
}
|
||||
}
|
||||
_pickups.Clear();
|
||||
|
||||
// Unsubscribe slot handlers
|
||||
var slotsCopy = new List<ItemSlot>(_itemSlots);
|
||||
foreach (var s in slotsCopy)
|
||||
{
|
||||
if (s != null)
|
||||
{
|
||||
s.OnCorrectItemSlotted -= ItemSlot_OnCorrectItemSlotted;
|
||||
s.OnIncorrectItemSlotted -= ItemSlot_OnIncorrectItemSlotted;
|
||||
s.OnForbiddenItemSlotted -= ItemSlot_OnForbiddenItemSlotted;
|
||||
s.OnItemSlotRemoved -= ItemSlot_OnItemSlotRemoved;
|
||||
}
|
||||
}
|
||||
_itemSlots.Clear();
|
||||
|
||||
// Clear item tracking
|
||||
_itemsCreatedThroughCombination.Clear();
|
||||
|
||||
// Clear manager-level event subscribers
|
||||
OnItemPickedUp = null;
|
||||
OnCorrectItemSlotted = null;
|
||||
OnIncorrectItemSlotted = null;
|
||||
OnForbiddenItemSlotted = null;
|
||||
OnItemSlotCleared = null;
|
||||
OnItemsCombined = null;
|
||||
}
|
||||
|
||||
public void RegisterPickup(Pickup pickup)
|
||||
{
|
||||
if (pickup == null) return;
|
||||
// only subscribe if newly added to avoid duplicate subscriptions
|
||||
if (_pickups.Add(pickup))
|
||||
{
|
||||
pickup.OnItemPickedUp += Pickup_OnItemPickedUp;
|
||||
pickup.OnItemsCombined += Pickup_OnItemsCombined;
|
||||
}
|
||||
}
|
||||
|
||||
public void UnregisterPickup(Pickup pickup)
|
||||
{
|
||||
if (pickup == null) return;
|
||||
if (_pickups.Remove(pickup))
|
||||
{
|
||||
pickup.OnItemPickedUp -= Pickup_OnItemPickedUp;
|
||||
pickup.OnItemsCombined -= Pickup_OnItemsCombined;
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterItemSlot(ItemSlot slot)
|
||||
{
|
||||
if (slot == null) return;
|
||||
if (_itemSlots.Add(slot))
|
||||
{
|
||||
// Subscribe to all slot events
|
||||
slot.OnCorrectItemSlotted += ItemSlot_OnCorrectItemSlotted;
|
||||
slot.OnIncorrectItemSlotted += ItemSlot_OnIncorrectItemSlotted;
|
||||
slot.OnForbiddenItemSlotted += ItemSlot_OnForbiddenItemSlotted;
|
||||
slot.OnItemSlotRemoved += ItemSlot_OnItemSlotRemoved;
|
||||
}
|
||||
}
|
||||
|
||||
public void UnregisterItemSlot(ItemSlot slot)
|
||||
{
|
||||
if (slot == null) return;
|
||||
if (_itemSlots.Remove(slot))
|
||||
{
|
||||
// Unsubscribe from all slot events
|
||||
slot.OnCorrectItemSlotted -= ItemSlot_OnCorrectItemSlotted;
|
||||
slot.OnIncorrectItemSlotted -= ItemSlot_OnIncorrectItemSlotted;
|
||||
slot.OnForbiddenItemSlotted -= ItemSlot_OnForbiddenItemSlotted;
|
||||
slot.OnItemSlotRemoved -= ItemSlot_OnItemSlotRemoved;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a specific item has been created through item combination.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The ID of the item to check.</param>
|
||||
/// <returns>True if the item has been created through combination, false otherwise.</returns>
|
||||
public bool WasItemCreatedThroughCombination(string itemId)
|
||||
{
|
||||
return !string.IsNullOrEmpty(itemId) && _itemsCreatedThroughCombination.Contains(itemId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current slot state for the given item data by searching registered slots.
|
||||
/// If the item is currently slotted in a slot, returns that slot's state; otherwise returns ItemSlotState.None.
|
||||
/// </summary>
|
||||
public ItemSlotState GetSlotStatusForItem(PickupItemData itemData)
|
||||
{
|
||||
if (itemData == null) return ItemSlotState.None;
|
||||
|
||||
foreach (var slot in _itemSlots)
|
||||
{
|
||||
var slottedObj = slot.GetSlottedObject();
|
||||
if (slottedObj == null) continue;
|
||||
var pickup = slottedObj.GetComponent<Pickup>();
|
||||
if (pickup == null) continue;
|
||||
if (pickup.itemData == itemData)
|
||||
{
|
||||
return slot.CurrentSlottedState;
|
||||
}
|
||||
}
|
||||
return ItemSlotState.None;
|
||||
}
|
||||
|
||||
public IEnumerable<Pickup> Pickups => _pickups;
|
||||
public IEnumerable<ItemSlot> ItemSlots => _itemSlots;
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/Core/ItemManager.cs.meta
Normal file
3
Assets/Scripts/Core/ItemManager.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a78fe78378e6426da43710f6d0ae84ba
|
||||
timeCreated: 1758888493
|
||||
3
Assets/Scripts/Dialogue.meta
Normal file
3
Assets/Scripts/Dialogue.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e592597a12a7498dbb5336395d7db00c
|
||||
timeCreated: 1758871403
|
||||
627
Assets/Scripts/Dialogue/DialogueComponent.cs
Normal file
627
Assets/Scripts/Dialogue/DialogueComponent.cs
Normal file
@@ -0,0 +1,627 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/Dialogue/DialogueComponent.cs.meta
Normal file
3
Assets/Scripts/Dialogue/DialogueComponent.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 25bbad45f1fa4183b30ad76c62256fd6
|
||||
timeCreated: 1758891211
|
||||
55
Assets/Scripts/Dialogue/RuntimeDialogueGraph.cs
Normal file
55
Assets/Scripts/Dialogue/RuntimeDialogueGraph.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Dialogue
|
||||
{
|
||||
[Serializable]
|
||||
public enum RuntimeDialogueNodeType
|
||||
{
|
||||
Dialogue,
|
||||
WaitOnPuzzleStep,
|
||||
WaitOnPickup,
|
||||
WaitOnSlot,
|
||||
WaitOnCombination,
|
||||
End
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class RuntimeDialogueGraph : ScriptableObject
|
||||
{
|
||||
public string entryNodeID;
|
||||
public string speakerName;
|
||||
public List<RuntimeDialogueNode> allNodes = new List<RuntimeDialogueNode>();
|
||||
|
||||
// Helper method to find a node by ID
|
||||
public RuntimeDialogueNode GetNodeByID(string id)
|
||||
{
|
||||
return allNodes.Find(n => n.nodeID == id);
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class RuntimeDialogueNode
|
||||
{
|
||||
public string nodeID;
|
||||
public RuntimeDialogueNodeType nodeType;
|
||||
public string nextNodeID;
|
||||
|
||||
// Basic dialogue
|
||||
public List<string> dialogueLines = new List<string>();
|
||||
public bool loopThroughLines;
|
||||
|
||||
// Conditional nodes
|
||||
public string puzzleStepID; // For WaitOnPuzzleStep
|
||||
public string pickupItemID; // For WaitOnPickup
|
||||
public string slotItemID; // For WaitOnSlot
|
||||
public string combinationResultItemID; // For WaitOnCombination
|
||||
|
||||
// For WaitOnSlot - different responses
|
||||
public List<string> incorrectItemLines = new List<string>();
|
||||
public bool loopThroughIncorrectLines;
|
||||
public List<string> forbiddenItemLines = new List<string>();
|
||||
public bool loopThroughForbiddenLines;
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/Dialogue/RuntimeDialogueGraph.cs.meta
Normal file
3
Assets/Scripts/Dialogue/RuntimeDialogueGraph.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3c3be3596532450a923c31dfe0ed4aa9
|
||||
timeCreated: 1758871423
|
||||
263
Assets/Scripts/Dialogue/SpeechBubble.cs
Normal file
263
Assets/Scripts/Dialogue/SpeechBubble.cs
Normal file
@@ -0,0 +1,263 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Dialogue
|
||||
{
|
||||
/// <summary>
|
||||
/// Display mode for the speech bubble text
|
||||
/// </summary>
|
||||
public enum TextDisplayMode
|
||||
{
|
||||
Instant, // Display all text at once
|
||||
Typewriter // Display text one character at a time
|
||||
}
|
||||
|
||||
[AddComponentMenu("Apple Hills/Dialogue/Speech Bubble")]
|
||||
public class SpeechBubble : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private TextMeshProUGUI textDisplay;
|
||||
[SerializeField] private TextDisplayMode displayMode = TextDisplayMode.Typewriter;
|
||||
[SerializeField] private float typewriterSpeed = 0.05f; // Time between characters in seconds
|
||||
[SerializeField] private AudioSource typingSoundSource;
|
||||
[SerializeField] private float typingSoundFrequency = 3; // Play sound every X characters
|
||||
[SerializeField] private bool useRichText = true; // Whether to respect rich text tags
|
||||
[SerializeField] private float dialogueDisplayTime = 1.5f; // Time in seconds to display dialogue before showing prompt
|
||||
[SerializeField] private string dialoguePromptText = ". . ."; // Text to show as a prompt for available dialogue
|
||||
|
||||
private Coroutine typewriterCoroutine;
|
||||
private Coroutine promptUpdateCoroutine;
|
||||
private string currentFullText = string.Empty;
|
||||
private bool isVisible = false;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show the speech bubble
|
||||
/// </summary>
|
||||
public void Show()
|
||||
{
|
||||
gameObject.SetActive(true);
|
||||
isVisible = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hide the speech bubble
|
||||
/// </summary>
|
||||
public void Hide()
|
||||
{
|
||||
gameObject.SetActive(false);
|
||||
isVisible = false;
|
||||
|
||||
// Stop any ongoing typewriter effect
|
||||
if (typewriterCoroutine != null)
|
||||
{
|
||||
StopCoroutine(typewriterCoroutine);
|
||||
typewriterCoroutine = null;
|
||||
}
|
||||
|
||||
// Stop any prompt update coroutine
|
||||
if (promptUpdateCoroutine != null)
|
||||
{
|
||||
StopCoroutine(promptUpdateCoroutine);
|
||||
promptUpdateCoroutine = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggle visibility of the speech bubble
|
||||
/// </summary>
|
||||
public void Toggle()
|
||||
{
|
||||
if (isVisible)
|
||||
Hide();
|
||||
else
|
||||
Show();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the text to display in the speech bubble
|
||||
/// </summary>
|
||||
/// <param name="text">Text to display</param>
|
||||
public void SetText(string text)
|
||||
{
|
||||
if (textDisplay == null)
|
||||
{
|
||||
Debug.LogError("SpeechBubble: TextMeshProUGUI component is not assigned!");
|
||||
return;
|
||||
}
|
||||
|
||||
currentFullText = text;
|
||||
|
||||
// Stop any existing typewriter effect
|
||||
if (typewriterCoroutine != null)
|
||||
{
|
||||
StopCoroutine(typewriterCoroutine);
|
||||
typewriterCoroutine = null;
|
||||
}
|
||||
|
||||
// Display text based on the selected mode
|
||||
if (displayMode == TextDisplayMode.Instant)
|
||||
{
|
||||
textDisplay.text = text;
|
||||
}
|
||||
else // Typewriter mode
|
||||
{
|
||||
textDisplay.text = string.Empty; // Clear the text initially
|
||||
typewriterCoroutine = StartCoroutine(TypewriterEffect(text));
|
||||
}
|
||||
|
||||
// Make sure the bubble is visible when setting text
|
||||
if (!isVisible)
|
||||
Show();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display a dialogue line and handle prompt visibility afterward
|
||||
/// </summary>
|
||||
/// <param name="line">The dialogue line to display</param>
|
||||
/// <param name="hasMoreDialogue">Whether there are more dialogue lines available</param>
|
||||
public void DisplayDialogueLine(string line, bool hasMoreDialogue)
|
||||
{
|
||||
// Cancel any existing prompt update
|
||||
if (promptUpdateCoroutine != null)
|
||||
{
|
||||
StopCoroutine(promptUpdateCoroutine);
|
||||
promptUpdateCoroutine = null;
|
||||
}
|
||||
|
||||
// Display the dialogue line
|
||||
if (!string.IsNullOrEmpty(line))
|
||||
{
|
||||
SetText(line);
|
||||
|
||||
// After a delay, update the prompt visibility
|
||||
promptUpdateCoroutine = StartCoroutine(UpdatePromptAfterDelay(hasMoreDialogue));
|
||||
}
|
||||
else
|
||||
{
|
||||
// If no line to display, update prompt visibility immediately
|
||||
UpdatePromptVisibility(hasMoreDialogue);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the speech bubble to either show a prompt or hide based on dialogue availability
|
||||
/// </summary>
|
||||
/// <param name="hasDialogueAvailable">Whether dialogue is available</param>
|
||||
public void UpdatePromptVisibility(bool hasDialogueAvailable)
|
||||
{
|
||||
if (hasDialogueAvailable)
|
||||
{
|
||||
Show();
|
||||
SetText(dialoguePromptText);
|
||||
}
|
||||
else
|
||||
{
|
||||
Hide();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Coroutine to update the prompt visibility after a delay
|
||||
/// </summary>
|
||||
private IEnumerator UpdatePromptAfterDelay(bool hasMoreDialogue)
|
||||
{
|
||||
// Wait for the configured display time
|
||||
yield return new WaitForSeconds(dialogueDisplayTime);
|
||||
|
||||
// Update the prompt visibility
|
||||
UpdatePromptVisibility(hasMoreDialogue);
|
||||
|
||||
promptUpdateCoroutine = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Change the display mode
|
||||
/// </summary>
|
||||
/// <param name="mode">New display mode</param>
|
||||
public void SetDisplayMode(TextDisplayMode mode)
|
||||
{
|
||||
displayMode = mode;
|
||||
|
||||
// If we're changing modes while text is displayed, refresh it
|
||||
if (!string.IsNullOrEmpty(currentFullText))
|
||||
{
|
||||
SetText(currentFullText);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Skip the typewriter effect and show the full text immediately
|
||||
/// </summary>
|
||||
public void SkipTypewriter()
|
||||
{
|
||||
if (typewriterCoroutine != null)
|
||||
{
|
||||
StopCoroutine(typewriterCoroutine);
|
||||
typewriterCoroutine = null;
|
||||
textDisplay.text = currentFullText;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the speed of the typewriter effect
|
||||
/// </summary>
|
||||
/// <param name="charactersPerSecond">Characters per second</param>
|
||||
public void SetTypewriterSpeed(float charactersPerSecond)
|
||||
{
|
||||
if (charactersPerSecond <= 0)
|
||||
{
|
||||
Debug.LogError("SpeechBubble: Typewriter speed must be greater than 0!");
|
||||
return;
|
||||
}
|
||||
|
||||
typewriterSpeed = 1f / charactersPerSecond;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Coroutine that gradually reveals text one character at a time
|
||||
/// </summary>
|
||||
private IEnumerator TypewriterEffect(string text)
|
||||
{
|
||||
int visibleCount = 0;
|
||||
int characterCount = 0;
|
||||
|
||||
while (visibleCount < text.Length)
|
||||
{
|
||||
// Skip rich text tags if enabled
|
||||
if (useRichText && visibleCount < text.Length && text[visibleCount] == '<')
|
||||
{
|
||||
// Find the end of the tag
|
||||
int tagEnd = text.IndexOf('>', visibleCount);
|
||||
if (tagEnd != -1)
|
||||
{
|
||||
// Include the entire tag at once
|
||||
visibleCount = tagEnd + 1;
|
||||
textDisplay.text = text.Substring(0, visibleCount);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Reveal the next character
|
||||
visibleCount++;
|
||||
characterCount++;
|
||||
textDisplay.text = text.Substring(0, visibleCount);
|
||||
|
||||
// Play typing sound at specified frequency
|
||||
if (typingSoundSource != null && characterCount % typingSoundFrequency == 0)
|
||||
{
|
||||
typingSoundSource.Play();
|
||||
}
|
||||
|
||||
yield return new WaitForSeconds(typewriterSpeed);
|
||||
}
|
||||
|
||||
typewriterCoroutine = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/Dialogue/SpeechBubble.cs.meta
Normal file
3
Assets/Scripts/Dialogue/SpeechBubble.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cb3605ae81a54d2689504e0cd456ac27
|
||||
timeCreated: 1758973942
|
||||
1
Assets/Scripts/Dialogue/SpeechBubbleController.cs
Normal file
1
Assets/Scripts/Dialogue/SpeechBubbleController.cs
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
3
Assets/Scripts/Dialogue/SpeechBubbleController.cs.meta
Normal file
3
Assets/Scripts/Dialogue/SpeechBubbleController.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 877344c7a0014922bc3a2a469e03792d
|
||||
timeCreated: 1759050622
|
||||
@@ -25,7 +25,7 @@ namespace Interactions
|
||||
public UnityEvent interactionInterrupted;
|
||||
public UnityEvent characterArrived;
|
||||
public UnityEvent<bool> interactionComplete;
|
||||
|
||||
|
||||
// Helpers for managing interaction state
|
||||
private bool _interactionInProgress;
|
||||
private PlayerTouchController _playerRef;
|
||||
|
||||
@@ -1,20 +1,51 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using System; // for Action<T>
|
||||
using Core; // register with ItemManager
|
||||
|
||||
namespace Interactions
|
||||
{
|
||||
// New enum describing possible states for the slotted item
|
||||
public enum ItemSlotState
|
||||
{
|
||||
None,
|
||||
Correct,
|
||||
Incorrect,
|
||||
Forbidden
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interaction requirement that allows slotting, swapping, or picking up items in a slot.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Interactable))]
|
||||
public class ItemSlot : Pickup
|
||||
{
|
||||
// Tracks the current state of the slotted item
|
||||
private ItemSlotState _currentState = ItemSlotState.None;
|
||||
|
||||
/// <summary>
|
||||
/// Read-only access to the current slotted item state.
|
||||
/// </summary>
|
||||
public ItemSlotState CurrentSlottedState => _currentState;
|
||||
|
||||
public UnityEvent onItemSlotted;
|
||||
public UnityEvent onItemSlotRemoved;
|
||||
// Native C# event alternative for code-only subscribers
|
||||
public event Action<PickupItemData> OnItemSlotRemoved;
|
||||
|
||||
public UnityEvent onCorrectItemSlotted;
|
||||
// Native C# event alternative to the UnityEvent for code-only subscribers
|
||||
public event Action<PickupItemData, PickupItemData> OnCorrectItemSlotted;
|
||||
|
||||
public UnityEvent onIncorrectItemSlotted;
|
||||
// Native C# event alternative for code-only subscribers
|
||||
public event Action<PickupItemData, PickupItemData> OnIncorrectItemSlotted;
|
||||
|
||||
public UnityEvent onForbiddenItemSlotted;
|
||||
// Native C# event alternative for code-only subscribers
|
||||
public event Action<PickupItemData, PickupItemData> OnForbiddenItemSlotted;
|
||||
|
||||
private PickupItemData _currentlySlottedItemData;
|
||||
public SpriteRenderer slottedItemRenderer;
|
||||
private GameObject _currentlySlottedItemObject = null;
|
||||
@@ -35,6 +66,8 @@ namespace Interactions
|
||||
|
||||
protected override void OnCharacterArrived()
|
||||
{
|
||||
Debug.Log("[ItemSlot] OnCharacterArrived");
|
||||
|
||||
var heldItemData = FollowerController.CurrentlyHeldItemData;
|
||||
var heldItemObj = FollowerController.GetHeldPickupObject();
|
||||
var config = GameManager.Instance.GetSlotItemConfig(itemData);
|
||||
@@ -48,6 +81,8 @@ namespace Interactions
|
||||
{
|
||||
DebugUIMessage.Show("Can't place that here.", Color.red);
|
||||
onForbiddenItemSlotted?.Invoke();
|
||||
OnForbiddenItemSlotted?.Invoke(itemData, heldItemData);
|
||||
_currentState = ItemSlotState.Forbidden;
|
||||
Interactable.BroadcastInteractionComplete(false);
|
||||
return;
|
||||
}
|
||||
@@ -62,6 +97,8 @@ namespace Interactions
|
||||
{
|
||||
FollowerController.TryPickupItem(_currentlySlottedItemObject, _currentlySlottedItemData, false);
|
||||
onItemSlotRemoved?.Invoke();
|
||||
OnItemSlotRemoved?.Invoke(_currentlySlottedItemData);
|
||||
_currentState = ItemSlotState.None;
|
||||
SlotItem(heldItemObj, heldItemData, _currentlySlottedItemObject == null);
|
||||
return;
|
||||
}
|
||||
@@ -105,10 +142,22 @@ namespace Interactions
|
||||
|
||||
public void SlotItem(GameObject itemToSlot, PickupItemData itemToSlotData, bool clearFollowerHeldItem = true)
|
||||
{
|
||||
// Cache the previous item data before clearing, needed for OnItemSlotRemoved event
|
||||
var previousItemData = _currentlySlottedItemData;
|
||||
bool wasSlotCleared = _currentlySlottedItemObject != null && itemToSlot == null;
|
||||
|
||||
if (itemToSlot == null)
|
||||
{
|
||||
_currentlySlottedItemObject = null;
|
||||
_currentlySlottedItemData = null;
|
||||
// Clear state when no item is slotted
|
||||
_currentState = ItemSlotState.None;
|
||||
|
||||
// Fire native event for slot clearing
|
||||
if (wasSlotCleared)
|
||||
{
|
||||
OnItemSlotRemoved?.Invoke(previousItemData);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -117,6 +166,7 @@ namespace Interactions
|
||||
SetSlottedObject(itemToSlot);
|
||||
_currentlySlottedItemData = itemToSlotData;
|
||||
}
|
||||
|
||||
if (clearFollowerHeldItem)
|
||||
{
|
||||
FollowerController.ClearHeldItem();
|
||||
@@ -133,6 +183,8 @@ namespace Interactions
|
||||
{
|
||||
DebugUIMessage.Show("You correctly slotted " + itemToSlotData.itemName + " into: " + itemData.itemName, Color.green);
|
||||
onCorrectItemSlotted?.Invoke();
|
||||
OnCorrectItemSlotted?.Invoke(itemData, _currentlySlottedItemData);
|
||||
_currentState = ItemSlotState.Correct;
|
||||
}
|
||||
|
||||
Interactable.BroadcastInteractionComplete(true);
|
||||
@@ -143,9 +195,22 @@ namespace Interactions
|
||||
{
|
||||
DebugUIMessage.Show("I'm not sure this works.", Color.yellow);
|
||||
onIncorrectItemSlotted?.Invoke();
|
||||
OnIncorrectItemSlotted?.Invoke(itemData, _currentlySlottedItemData);
|
||||
_currentState = ItemSlotState.Incorrect;
|
||||
}
|
||||
Interactable.BroadcastInteractionComplete(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Register with ItemManager when enabled
|
||||
void Start()
|
||||
{
|
||||
ItemManager.Instance?.RegisterItemSlot(this);
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
ItemManager.Instance?.UnregisterItemSlot(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using Input;
|
||||
using UnityEngine;
|
||||
using System; // added for Action<T>
|
||||
using Core; // register with ItemManager
|
||||
|
||||
namespace Interactions
|
||||
{
|
||||
@@ -12,6 +14,15 @@ namespace Interactions
|
||||
private PlayerTouchController _playerRef;
|
||||
protected FollowerController FollowerController;
|
||||
|
||||
// Track if the item has been picked up
|
||||
public bool isPickedUp { get; private set; }
|
||||
|
||||
// Event: invoked when the item was picked up successfully
|
||||
public event Action<PickupItemData> OnItemPickedUp;
|
||||
|
||||
// Event: invoked when this item is successfully combined with another
|
||||
public event Action<PickupItemData, PickupItemData, PickupItemData> OnItemsCombined;
|
||||
|
||||
/// <summary>
|
||||
/// Unity Awake callback. Sets up icon, interactable, and event handlers.
|
||||
/// </summary>
|
||||
@@ -30,6 +41,14 @@ namespace Interactions
|
||||
ApplyItemData();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register with ItemManager on Start
|
||||
/// </summary>
|
||||
void Start()
|
||||
{
|
||||
ItemManager.Instance?.RegisterPickup(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unity OnDestroy callback. Cleans up event handlers.
|
||||
/// </summary>
|
||||
@@ -40,6 +59,9 @@ namespace Interactions
|
||||
Interactable.interactionStarted.RemoveListener(OnInteractionStarted);
|
||||
Interactable.characterArrived.RemoveListener(OnCharacterArrived);
|
||||
}
|
||||
|
||||
// Unregister from ItemManager
|
||||
ItemManager.Instance?.UnregisterPickup(this);
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
@@ -81,15 +103,48 @@ namespace Interactions
|
||||
|
||||
protected virtual void OnCharacterArrived()
|
||||
{
|
||||
Debug.Log("[Pickup] OnCharacterArrived");
|
||||
|
||||
var combinationResult = FollowerController.TryCombineItems(this, out var combinationResultItem);
|
||||
if (combinationResultItem != null)
|
||||
{
|
||||
Interactable.BroadcastInteractionComplete(true);
|
||||
|
||||
// Fire the combination event when items are successfully combined
|
||||
if (combinationResult == FollowerController.CombinationResult.Successful)
|
||||
{
|
||||
var resultPickup = combinationResultItem.GetComponent<Pickup>();
|
||||
if (resultPickup != null && resultPickup.itemData != null)
|
||||
{
|
||||
// Get the combined item data
|
||||
var resultItemData = resultPickup.itemData;
|
||||
var heldItem = FollowerController.GetHeldPickupObject();
|
||||
|
||||
if (heldItem != null)
|
||||
{
|
||||
var heldPickup = heldItem.GetComponent<Pickup>();
|
||||
if (heldPickup != null && heldPickup.itemData != null)
|
||||
{
|
||||
// Trigger the combination event
|
||||
OnItemsCombined?.Invoke(itemData, heldPickup.itemData, resultItemData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
FollowerController?.TryPickupItem(gameObject, itemData);
|
||||
Interactable.BroadcastInteractionComplete(combinationResult == FollowerController.CombinationResult.NotApplicable);
|
||||
bool wasPickedUp = (combinationResult == FollowerController.CombinationResult.NotApplicable);
|
||||
Interactable.BroadcastInteractionComplete(wasPickedUp);
|
||||
|
||||
// Update pickup state and invoke event when the item was picked up successfully
|
||||
if (wasPickedUp)
|
||||
{
|
||||
isPickedUp = true;
|
||||
OnItemPickedUp?.Invoke(itemData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,58 @@
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
|
||||
[CreateAssetMenu(fileName = "PickupItemData", menuName = "Game/Pickup Item Data")]
|
||||
public class PickupItemData : ScriptableObject
|
||||
{
|
||||
[SerializeField] private string _itemId;
|
||||
|
||||
public string itemName;
|
||||
[TextArea]
|
||||
public string description;
|
||||
public Sprite mapSprite;
|
||||
|
||||
// Read-only property for itemId
|
||||
public string itemId => _itemId;
|
||||
|
||||
// Auto-generate ID on creation or validation
|
||||
void OnValidate()
|
||||
{
|
||||
// Only generate if empty
|
||||
if (string.IsNullOrEmpty(_itemId))
|
||||
{
|
||||
_itemId = GenerateItemId();
|
||||
|
||||
#if UNITY_EDITOR
|
||||
// Mark the asset as dirty to ensure the ID is saved
|
||||
UnityEditor.EditorUtility.SetDirty(this);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private string GenerateItemId()
|
||||
{
|
||||
// Use asset name as the basis for the ID to keep it somewhat readable
|
||||
string baseName = name.Replace(" ", "").ToLowerInvariant();
|
||||
|
||||
// Add a unique suffix based on a GUID
|
||||
string uniqueSuffix = Guid.NewGuid().ToString().Substring(0, 8);
|
||||
|
||||
return $"{baseName}_{uniqueSuffix}";
|
||||
}
|
||||
|
||||
// Method to manually regenerate ID if needed (for editor scripts)
|
||||
public void RegenerateId()
|
||||
{
|
||||
_itemId = GenerateItemId();
|
||||
}
|
||||
|
||||
public static bool AreEquivalent(PickupItemData a, PickupItemData b)
|
||||
{
|
||||
if (ReferenceEquals(a, b)) return true;
|
||||
if (a is null || b is null) return false;
|
||||
// First compare by itemId if available
|
||||
if (!string.IsNullOrEmpty(a.itemId) && !string.IsNullOrEmpty(b.itemId))
|
||||
return a.itemId == b.itemId;
|
||||
|
||||
// Compare by itemName as a fallback
|
||||
return a.itemName == b.itemName;
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ namespace PuzzleS
|
||||
if (success)
|
||||
{
|
||||
Debug.Log($"[Puzzles] Step interacted: {stepData?.stepId} on {gameObject.name}");
|
||||
PuzzleManager.Instance?.OnStepCompleted(stepData);
|
||||
PuzzleManager.Instance?.MarkPuzzleStepCompleted(stepData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,196 +1,219 @@
|
||||
using UnityEngine;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using PuzzleS;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
/// <summary>
|
||||
/// Manages puzzle step registration, dependency management, and step completion for the puzzle system.
|
||||
/// </summary>
|
||||
public class PuzzleManager : MonoBehaviour
|
||||
namespace PuzzleS
|
||||
{
|
||||
private static PuzzleManager _instance;
|
||||
private static bool _isQuitting = false;
|
||||
|
||||
/// <summary>
|
||||
/// Singleton instance of the PuzzleManager.
|
||||
/// Manages puzzle step registration, dependency management, and step completion for the puzzle system.
|
||||
/// </summary>
|
||||
public static PuzzleManager Instance
|
||||
public class PuzzleManager : MonoBehaviour
|
||||
{
|
||||
get
|
||||
private static PuzzleManager _instance;
|
||||
private static bool _isQuitting;
|
||||
|
||||
/// <summary>
|
||||
/// Singleton instance of the PuzzleManager.
|
||||
/// </summary>
|
||||
public static PuzzleManager Instance
|
||||
{
|
||||
if (_instance == null && Application.isPlaying && !_isQuitting)
|
||||
get
|
||||
{
|
||||
_instance = FindAnyObjectByType<PuzzleManager>();
|
||||
if (_instance == null)
|
||||
if (_instance == null && Application.isPlaying && !_isQuitting)
|
||||
{
|
||||
var go = new GameObject("PuzzleManager");
|
||||
_instance = go.AddComponent<PuzzleManager>();
|
||||
// DontDestroyOnLoad(go);
|
||||
_instance = FindAnyObjectByType<PuzzleManager>();
|
||||
if (_instance == null)
|
||||
{
|
||||
var go = new GameObject("PuzzleManager");
|
||||
_instance = go.AddComponent<PuzzleManager>();
|
||||
// DontDestroyOnLoad(go);
|
||||
}
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
// Events to notify about step lifecycle
|
||||
public event Action<PuzzleStepSO> OnStepCompleted;
|
||||
public event Action<PuzzleStepSO> OnStepUnlocked;
|
||||
|
||||
private HashSet<PuzzleStepSO> _completedSteps = new HashSet<PuzzleStepSO>();
|
||||
private HashSet<PuzzleStepSO> _unlockedSteps = new HashSet<PuzzleStepSO>();
|
||||
// Registration for ObjectiveStepBehaviour
|
||||
private Dictionary<PuzzleStepSO, ObjectiveStepBehaviour> _stepBehaviours = new Dictionary<PuzzleStepSO, ObjectiveStepBehaviour>();
|
||||
// Runtime dependency graph
|
||||
private Dictionary<PuzzleStepSO, List<PuzzleStepSO>> _runtimeDependencies = new Dictionary<PuzzleStepSO, List<PuzzleStepSO>>();
|
||||
|
||||
void Awake()
|
||||
{
|
||||
_instance = this;
|
||||
// DontDestroyOnLoad(gameObject);
|
||||
SceneManager.sceneLoaded += OnSceneLoaded;
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
SceneManager.sceneLoaded -= OnSceneLoaded;
|
||||
}
|
||||
|
||||
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
|
||||
{
|
||||
BuildRuntimeDependencies();
|
||||
UnlockInitialSteps();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a step behaviour with the manager.
|
||||
/// </summary>
|
||||
/// <param name="behaviour">The step behaviour to register.</param>
|
||||
public void RegisterStepBehaviour(ObjectiveStepBehaviour behaviour)
|
||||
{
|
||||
if (behaviour?.stepData == null) return;
|
||||
if (!_stepBehaviours.ContainsKey(behaviour.stepData))
|
||||
{
|
||||
_stepBehaviours.Add(behaviour.stepData, behaviour);
|
||||
Debug.Log($"[Puzzles] Registered step: {behaviour.stepData.stepId} on {behaviour.gameObject.name}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters a step behaviour from the manager.
|
||||
/// </summary>
|
||||
/// <param name="behaviour">The step behaviour to unregister.</param>
|
||||
public void UnregisterStepBehaviour(ObjectiveStepBehaviour behaviour)
|
||||
{
|
||||
if (behaviour?.stepData == null) return;
|
||||
_stepBehaviours.Remove(behaviour.stepData);
|
||||
Debug.Log($"[Puzzles] Unregistered step: {behaviour.stepData.stepId} on {behaviour.gameObject.name}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the runtime dependency graph for all registered steps.
|
||||
/// </summary>
|
||||
private void BuildRuntimeDependencies()
|
||||
{
|
||||
_runtimeDependencies = PuzzleGraphUtility.BuildDependencyGraph(_stepBehaviours.Keys);
|
||||
foreach (var step in _runtimeDependencies.Keys)
|
||||
{
|
||||
foreach (var dep in _runtimeDependencies[step])
|
||||
{
|
||||
Debug.Log($"[Puzzles] Step {step.stepId} depends on {dep.stepId}");
|
||||
}
|
||||
}
|
||||
return _instance;
|
||||
Debug.Log($"[Puzzles] Runtime dependencies built. Total steps: {_stepBehaviours.Count}");
|
||||
}
|
||||
}
|
||||
|
||||
private HashSet<PuzzleStepSO> completedSteps = new HashSet<PuzzleStepSO>();
|
||||
private HashSet<PuzzleStepSO> unlockedSteps = new HashSet<PuzzleStepSO>();
|
||||
|
||||
// Registration for ObjectiveStepBehaviour
|
||||
private Dictionary<PuzzleStepSO, ObjectiveStepBehaviour> stepBehaviours = new Dictionary<PuzzleStepSO, ObjectiveStepBehaviour>();
|
||||
|
||||
// Runtime dependency graph
|
||||
private Dictionary<PuzzleStepSO, List<PuzzleStepSO>> runtimeDependencies = new Dictionary<PuzzleStepSO, List<PuzzleStepSO>>();
|
||||
|
||||
void Awake()
|
||||
{
|
||||
_instance = this;
|
||||
// DontDestroyOnLoad(gameObject);
|
||||
SceneManager.sceneLoaded += OnSceneLoaded;
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
SceneManager.sceneLoaded -= OnSceneLoaded;
|
||||
}
|
||||
|
||||
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
|
||||
{
|
||||
BuildRuntimeDependencies();
|
||||
UnlockInitialSteps();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a step behaviour with the manager.
|
||||
/// </summary>
|
||||
/// <param name="behaviour">The step behaviour to register.</param>
|
||||
public void RegisterStepBehaviour(ObjectiveStepBehaviour behaviour)
|
||||
{
|
||||
if (behaviour?.stepData == null) return;
|
||||
if (!stepBehaviours.ContainsKey(behaviour.stepData))
|
||||
/// <summary>
|
||||
/// Unlocks all initial steps (those with no dependencies).
|
||||
/// </summary>
|
||||
private void UnlockInitialSteps()
|
||||
{
|
||||
stepBehaviours.Add(behaviour.stepData, behaviour);
|
||||
Debug.Log($"[Puzzles] Registered step: {behaviour.stepData.stepId} on {behaviour.gameObject.name}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters a step behaviour from the manager.
|
||||
/// </summary>
|
||||
/// <param name="behaviour">The step behaviour to unregister.</param>
|
||||
public void UnregisterStepBehaviour(ObjectiveStepBehaviour behaviour)
|
||||
{
|
||||
if (behaviour?.stepData == null) return;
|
||||
stepBehaviours.Remove(behaviour.stepData);
|
||||
Debug.Log($"[Puzzles] Unregistered step: {behaviour.stepData.stepId} on {behaviour.gameObject.name}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the runtime dependency graph for all registered steps.
|
||||
/// </summary>
|
||||
private void BuildRuntimeDependencies()
|
||||
{
|
||||
runtimeDependencies = PuzzleGraphUtility.BuildDependencyGraph(stepBehaviours.Keys);
|
||||
foreach (var step in runtimeDependencies.Keys)
|
||||
{
|
||||
foreach (var dep in runtimeDependencies[step])
|
||||
var initialSteps = PuzzleGraphUtility.FindInitialSteps(_runtimeDependencies);
|
||||
foreach (var step in initialSteps)
|
||||
{
|
||||
Debug.Log($"[Puzzles] Step {step.stepId} depends on {dep.stepId}");
|
||||
Debug.Log($"[Puzzles] Initial step unlocked: {step.stepId}");
|
||||
UnlockStep(step);
|
||||
}
|
||||
}
|
||||
Debug.Log($"[Puzzles] Runtime dependencies built. Total steps: {stepBehaviours.Count}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unlocks all initial steps (those with no dependencies).
|
||||
/// </summary>
|
||||
private void UnlockInitialSteps()
|
||||
{
|
||||
var initialSteps = PuzzleGraphUtility.FindInitialSteps(runtimeDependencies);
|
||||
foreach (var step in initialSteps)
|
||||
/// <summary>
|
||||
/// Called when a step is completed. Unlocks dependent steps if their dependencies are met.
|
||||
/// </summary>
|
||||
/// <param name="step">The completed step.</param>
|
||||
public void MarkPuzzleStepCompleted(PuzzleStepSO step)
|
||||
{
|
||||
Debug.Log($"[Puzzles] Initial step unlocked: {step.stepId}");
|
||||
UnlockStep(step);
|
||||
}
|
||||
}
|
||||
if (_completedSteps.Contains(step)) return;
|
||||
_completedSteps.Add(step);
|
||||
Debug.Log($"[Puzzles] Step completed: {step.stepId}");
|
||||
|
||||
/// <summary>
|
||||
/// Called when a step is completed. Unlocks dependent steps if their dependencies are met.
|
||||
/// </summary>
|
||||
/// <param name="step">The completed step.</param>
|
||||
public void OnStepCompleted(PuzzleStepSO step)
|
||||
{
|
||||
if (completedSteps.Contains(step)) return;
|
||||
completedSteps.Add(step);
|
||||
Debug.Log($"[Puzzles] Step completed: {step.stepId}");
|
||||
foreach (var unlock in step.unlocks)
|
||||
{
|
||||
if (AreRuntimeDependenciesMet(unlock))
|
||||
// Broadcast completion
|
||||
OnStepCompleted?.Invoke(step);
|
||||
|
||||
foreach (var unlock in step.unlocks)
|
||||
{
|
||||
Debug.Log($"[Puzzles] Unlocking step {unlock.stepId} after completing {step.stepId}");
|
||||
UnlockStep(unlock);
|
||||
if (AreRuntimeDependenciesMet(unlock))
|
||||
{
|
||||
Debug.Log($"[Puzzles] Unlocking step {unlock.stepId} after completing {step.stepId}");
|
||||
UnlockStep(unlock);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"[Puzzles] Step {unlock.stepId} not unlocked yet, waiting for other dependencies");
|
||||
}
|
||||
}
|
||||
else
|
||||
CheckPuzzleCompletion();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if all dependencies for a step are met.
|
||||
/// </summary>
|
||||
/// <param name="step">The step to check.</param>
|
||||
/// <returns>True if all dependencies are met, false otherwise.</returns>
|
||||
private bool AreRuntimeDependenciesMet(PuzzleStepSO step)
|
||||
{
|
||||
if (!_runtimeDependencies.ContainsKey(step) || _runtimeDependencies[step].Count == 0) return true;
|
||||
foreach (var dep in _runtimeDependencies[step])
|
||||
{
|
||||
Debug.Log($"[Puzzles] Step {unlock.stepId} not unlocked yet, waiting for other dependencies");
|
||||
if (!_completedSteps.Contains(dep)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unlocks a specific step and notifies its behaviour.
|
||||
/// </summary>
|
||||
/// <param name="step">The step to unlock.</param>
|
||||
private void UnlockStep(PuzzleStepSO step)
|
||||
{
|
||||
if (_unlockedSteps.Contains(step)) return;
|
||||
_unlockedSteps.Add(step);
|
||||
if (_stepBehaviours.TryGetValue(step, out var behaviour))
|
||||
{
|
||||
behaviour.UnlockStep();
|
||||
}
|
||||
Debug.Log($"[Puzzles] Step unlocked: {step.stepId}");
|
||||
|
||||
// Broadcast unlock
|
||||
OnStepUnlocked?.Invoke(step);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the puzzle is complete (all steps finished).
|
||||
/// </summary>
|
||||
private void CheckPuzzleCompletion()
|
||||
{
|
||||
if (_completedSteps.Count == _stepBehaviours.Count)
|
||||
{
|
||||
Debug.Log("[Puzzles] Puzzle complete! All steps finished.");
|
||||
// TODO: Fire puzzle complete event or trigger outcome logic
|
||||
}
|
||||
}
|
||||
CheckPuzzleCompletion();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if all dependencies for a step are met.
|
||||
/// </summary>
|
||||
/// <param name="step">The step to check.</param>
|
||||
/// <returns>True if all dependencies are met, false otherwise.</returns>
|
||||
private bool AreRuntimeDependenciesMet(PuzzleStepSO step)
|
||||
{
|
||||
if (!runtimeDependencies.ContainsKey(step) || runtimeDependencies[step].Count == 0) return true;
|
||||
foreach (var dep in runtimeDependencies[step])
|
||||
/// <summary>
|
||||
/// Returns whether a step is already unlocked.
|
||||
/// </summary>
|
||||
public bool IsStepUnlocked(PuzzleStepSO step)
|
||||
{
|
||||
if (!completedSteps.Contains(dep)) return false;
|
||||
BuildRuntimeDependencies();
|
||||
UnlockInitialSteps();
|
||||
return _unlockedSteps.Contains(step);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unlocks a specific step and notifies its behaviour.
|
||||
/// </summary>
|
||||
/// <param name="step">The step to unlock.</param>
|
||||
private void UnlockStep(PuzzleStepSO step)
|
||||
{
|
||||
if (unlockedSteps.Contains(step)) return;
|
||||
unlockedSteps.Add(step);
|
||||
if (stepBehaviours.TryGetValue(step, out var behaviour))
|
||||
/// <summary>
|
||||
/// Checks if a puzzle step with the specified ID has been completed
|
||||
/// </summary>
|
||||
/// <param name="stepId">The ID of the puzzle step to check</param>
|
||||
/// <returns>True if the step has been completed, false otherwise</returns>
|
||||
public bool IsPuzzleStepCompleted(string stepId)
|
||||
{
|
||||
behaviour.UnlockStep();
|
||||
return _completedSteps.Any(step => step.stepId == stepId);
|
||||
}
|
||||
Debug.Log($"[Puzzles] Step unlocked: {step.stepId}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the puzzle is complete (all steps finished).
|
||||
/// </summary>
|
||||
private void CheckPuzzleCompletion()
|
||||
{
|
||||
if (completedSteps.Count == stepBehaviours.Count)
|
||||
void OnApplicationQuit()
|
||||
{
|
||||
Debug.Log("[Puzzles] Puzzle complete! All steps finished.");
|
||||
// TODO: Fire puzzle complete event or trigger outcome logic
|
||||
_isQuitting = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether a step is already unlocked.
|
||||
/// </summary>
|
||||
public bool IsStepUnlocked(PuzzleStepSO step)
|
||||
{
|
||||
BuildRuntimeDependencies();
|
||||
UnlockInitialSteps();
|
||||
return unlockedSteps.Contains(step);
|
||||
}
|
||||
|
||||
void OnApplicationQuit()
|
||||
{
|
||||
_isQuitting = true;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user