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

831 lines
33 KiB
C#
Raw Normal View History

Lifecycle System Refactor & Logging Centralization (#56) ## ManagedBehaviour System Refactor - **Sealed `Awake()`** to prevent override mistakes that break singleton registration - **Added `OnManagedAwake()`** for early initialization (fires during registration) - **Renamed lifecycle hook:** `OnManagedAwake()` → `OnManagedStart()` (fires after boot, mirrors Unity's Awake→Start) - **40 files migrated** to new pattern (2 core, 38 components) - Eliminated all fragile `private new void Awake()` patterns - Zero breaking changes - backward compatible ## Centralized Logging System - **Automatic tagging** via `CallerMemberName` and `CallerFilePath` - logs auto-tagged as `[ClassName][MethodName] message` - **Unified API:** Single `Logging.Debug/Info/Warning/Error()` replaces custom `LogDebugMessage()` implementations - **~90 logging call sites** migrated across 10 files - **10 redundant helper methods** removed - All logs broadcast via `Logging.OnLogEntryAdded` event for real-time monitoring ## Custom Log Console (Editor Window) - **Persistent filter popups** for multi-selection (classes, methods, log levels) - windows stay open during selection - **Search** across class names, methods, and message content - **Time range filter** with MinMaxSlider - **Export** filtered logs to timestamped `.txt` files - **Right-click context menu** for quick filtering and copy actions - **Visual improvements:** White text, alternating row backgrounds, color-coded log levels - **Multiple instances** supported for simultaneous system monitoring - Open via `AppleHills > Custom Log Console` Co-authored-by: Michal Pikulski <michal@foolhardyhorizons.com> Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com> Reviewed-on: https://homelab.tailf7f81b.ts.net/tschesky/AppleHillsProduction/pulls/56
2025-11-11 08:48:29 +00:00
using System.Collections.Generic;
using Core;
Refactor interactions, introduce template-method lifecycle management, work on save-load system (#51) # Lifecycle Management & Save System Revamp ## Overview Complete overhaul of game lifecycle management, interactable system, and save/load architecture. Introduces centralized `ManagedBehaviour` base class for consistent initialization ordering and lifecycle hooks across all systems. ## Core Architecture ### New Lifecycle System - **`LifecycleManager`**: Centralized coordinator for all managed objects - **`ManagedBehaviour`**: Base class replacing ad-hoc initialization patterns - `OnManagedAwake()`: Priority-based initialization (0-100, lower = earlier) - `OnSceneReady()`: Scene-specific setup after managers ready - Replaces `BootCompletionService` (deleted) - **Priority groups**: Infrastructure (0-20) → Game Systems (30-50) → Data (60-80) → UI/Gameplay (90-100) - **Editor support**: `EditorLifecycleBootstrap` ensures lifecycle works in editor mode ### Unified SaveID System - Consistent format: `{ParentName}_{ComponentType}` - Auto-registration via `AutoRegisterForSave = true` - New `DebugSaveIds` editor tool for inspection ## Save/Load Improvements ### Enhanced State Management - **Extended SaveLoadData**: Unlocked minigames, card collection states, combination items, slot occupancy - **Async loading**: `ApplyCardCollectionState()` waits for card definitions before restoring - **New `SaveablePlayableDirector`**: Timeline sequences save/restore playback state - **Fixed race conditions**: Proper initialization ordering prevents data corruption ## Interactable & Pickup System - Migrated to `OnManagedAwake()` for consistent initialization - Template method pattern for state restoration (`RestoreInteractionState()`) - Fixed combination item save/load bugs (items in slots vs. follower hand) - Dynamic spawning support for combined items on load - **Breaking**: `Interactable.Awake()` now sealed, use `OnManagedAwake()` instead ## UI System Changes - **AlbumViewPage** and **BoosterNotificationDot**: Migrated to `ManagedBehaviour` - **Fixed menu persistence bug**: Menus no longer reappear after scene transitions - **Pause Menu**: Now reacts to all scene loads (not just first scene) - **Orientation Enforcer**: Enforces per-scene via `SceneManagementService` - **Loading Screen**: Integrated with new lifecycle ## ⚠️ Breaking Changes 1. **`BootCompletionService` removed** → Use `ManagedBehaviour.OnManagedAwake()` with priority 2. **`Interactable.Awake()` sealed** → Override `OnManagedAwake()` instead 3. **SaveID format changed** → Now `{ParentName}_{ComponentType}` consistently 4. **MonoBehaviours needing init ordering** → Must inherit from `ManagedBehaviour` Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com> Co-authored-by: Michal Pikulski <michal@foolhardyhorizons.com> Reviewed-on: https://homelab.tailf7f81b.ts.net/tschesky/AppleHillsProduction/pulls/51
2025-11-07 15:38:31 +00:00
using Core.Lifecycle;
using Interactions;
using UnityEngine;
using PuzzleS;
2025-10-29 17:01:02 +01:00
using UnityEngine.Audio;
namespace Dialogue
{
[AddComponentMenu("AppleHills/Dialogue/Dialogue Component")]
2025-10-29 17:01:02 +01:00
[RequireComponent(typeof(AppleAudioSource))]
Refactor interactions, introduce template-method lifecycle management, work on save-load system (#51) # Lifecycle Management & Save System Revamp ## Overview Complete overhaul of game lifecycle management, interactable system, and save/load architecture. Introduces centralized `ManagedBehaviour` base class for consistent initialization ordering and lifecycle hooks across all systems. ## Core Architecture ### New Lifecycle System - **`LifecycleManager`**: Centralized coordinator for all managed objects - **`ManagedBehaviour`**: Base class replacing ad-hoc initialization patterns - `OnManagedAwake()`: Priority-based initialization (0-100, lower = earlier) - `OnSceneReady()`: Scene-specific setup after managers ready - Replaces `BootCompletionService` (deleted) - **Priority groups**: Infrastructure (0-20) → Game Systems (30-50) → Data (60-80) → UI/Gameplay (90-100) - **Editor support**: `EditorLifecycleBootstrap` ensures lifecycle works in editor mode ### Unified SaveID System - Consistent format: `{ParentName}_{ComponentType}` - Auto-registration via `AutoRegisterForSave = true` - New `DebugSaveIds` editor tool for inspection ## Save/Load Improvements ### Enhanced State Management - **Extended SaveLoadData**: Unlocked minigames, card collection states, combination items, slot occupancy - **Async loading**: `ApplyCardCollectionState()` waits for card definitions before restoring - **New `SaveablePlayableDirector`**: Timeline sequences save/restore playback state - **Fixed race conditions**: Proper initialization ordering prevents data corruption ## Interactable & Pickup System - Migrated to `OnManagedAwake()` for consistent initialization - Template method pattern for state restoration (`RestoreInteractionState()`) - Fixed combination item save/load bugs (items in slots vs. follower hand) - Dynamic spawning support for combined items on load - **Breaking**: `Interactable.Awake()` now sealed, use `OnManagedAwake()` instead ## UI System Changes - **AlbumViewPage** and **BoosterNotificationDot**: Migrated to `ManagedBehaviour` - **Fixed menu persistence bug**: Menus no longer reappear after scene transitions - **Pause Menu**: Now reacts to all scene loads (not just first scene) - **Orientation Enforcer**: Enforces per-scene via `SceneManagementService` - **Loading Screen**: Integrated with new lifecycle ## ⚠️ Breaking Changes 1. **`BootCompletionService` removed** → Use `ManagedBehaviour.OnManagedAwake()` with priority 2. **`Interactable.Awake()` sealed** → Override `OnManagedAwake()` instead 3. **SaveID format changed** → Now `{ParentName}_{ComponentType}` consistently 4. **MonoBehaviours needing init ordering** → Must inherit from `ManagedBehaviour` Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com> Co-authored-by: Michal Pikulski <michal@foolhardyhorizons.com> Reviewed-on: https://homelab.tailf7f81b.ts.net/tschesky/AppleHillsProduction/pulls/51
2025-11-07 15:38:31 +00:00
public class DialogueComponent : ManagedBehaviour
{
[SerializeField] private RuntimeDialogueGraph dialogueGraph;
private RuntimeDialogueNode currentNode;
private int currentLineIndex;
private bool initialized = false;
private SpeechBubble speechBubble;
2025-10-29 17:01:02 +01:00
private AppleAudioSource appleAudioSource;
// Flag to track when a condition has been met but dialogue hasn't advanced yet
private bool _conditionSatisfiedPendingAdvance = false;
// Track the current slot state for WaitOnSlot nodes
private ItemSlotState _currentSlotState = ItemSlotState.None;
private PickupItemData _lastSlottedItem;
// Properties
public bool IsActive { get; private set; }
public bool IsCompleted { get; private set; }
public string CurrentSpeakerName => dialogueGraph?.speakerName;
Refactor interactions, introduce template-method lifecycle management, work on save-load system (#51) # Lifecycle Management & Save System Revamp ## Overview Complete overhaul of game lifecycle management, interactable system, and save/load architecture. Introduces centralized `ManagedBehaviour` base class for consistent initialization ordering and lifecycle hooks across all systems. ## Core Architecture ### New Lifecycle System - **`LifecycleManager`**: Centralized coordinator for all managed objects - **`ManagedBehaviour`**: Base class replacing ad-hoc initialization patterns - `OnManagedAwake()`: Priority-based initialization (0-100, lower = earlier) - `OnSceneReady()`: Scene-specific setup after managers ready - Replaces `BootCompletionService` (deleted) - **Priority groups**: Infrastructure (0-20) → Game Systems (30-50) → Data (60-80) → UI/Gameplay (90-100) - **Editor support**: `EditorLifecycleBootstrap` ensures lifecycle works in editor mode ### Unified SaveID System - Consistent format: `{ParentName}_{ComponentType}` - Auto-registration via `AutoRegisterForSave = true` - New `DebugSaveIds` editor tool for inspection ## Save/Load Improvements ### Enhanced State Management - **Extended SaveLoadData**: Unlocked minigames, card collection states, combination items, slot occupancy - **Async loading**: `ApplyCardCollectionState()` waits for card definitions before restoring - **New `SaveablePlayableDirector`**: Timeline sequences save/restore playback state - **Fixed race conditions**: Proper initialization ordering prevents data corruption ## Interactable & Pickup System - Migrated to `OnManagedAwake()` for consistent initialization - Template method pattern for state restoration (`RestoreInteractionState()`) - Fixed combination item save/load bugs (items in slots vs. follower hand) - Dynamic spawning support for combined items on load - **Breaking**: `Interactable.Awake()` now sealed, use `OnManagedAwake()` instead ## UI System Changes - **AlbumViewPage** and **BoosterNotificationDot**: Migrated to `ManagedBehaviour` - **Fixed menu persistence bug**: Menus no longer reappear after scene transitions - **Pause Menu**: Now reacts to all scene loads (not just first scene) - **Orientation Enforcer**: Enforces per-scene via `SceneManagementService` - **Loading Screen**: Integrated with new lifecycle ## ⚠️ Breaking Changes 1. **`BootCompletionService` removed** → Use `ManagedBehaviour.OnManagedAwake()` with priority 2. **`Interactable.Awake()` sealed** → Override `OnManagedAwake()` instead 3. **SaveID format changed** → Now `{ParentName}_{ComponentType}` consistently 4. **MonoBehaviours needing init ordering** → Must inherit from `ManagedBehaviour` Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com> Co-authored-by: Michal Pikulski <michal@foolhardyhorizons.com> Reviewed-on: https://homelab.tailf7f81b.ts.net/tschesky/AppleHillsProduction/pulls/51
2025-11-07 15:38:31 +00:00
public override int ManagedAwakePriority => 150; // Dialogue systems
Lifecycle System Refactor & Logging Centralization (#56) ## ManagedBehaviour System Refactor - **Sealed `Awake()`** to prevent override mistakes that break singleton registration - **Added `OnManagedAwake()`** for early initialization (fires during registration) - **Renamed lifecycle hook:** `OnManagedAwake()` → `OnManagedStart()` (fires after boot, mirrors Unity's Awake→Start) - **40 files migrated** to new pattern (2 core, 38 components) - Eliminated all fragile `private new void Awake()` patterns - Zero breaking changes - backward compatible ## Centralized Logging System - **Automatic tagging** via `CallerMemberName` and `CallerFilePath` - logs auto-tagged as `[ClassName][MethodName] message` - **Unified API:** Single `Logging.Debug/Info/Warning/Error()` replaces custom `LogDebugMessage()` implementations - **~90 logging call sites** migrated across 10 files - **10 redundant helper methods** removed - All logs broadcast via `Logging.OnLogEntryAdded` event for real-time monitoring ## Custom Log Console (Editor Window) - **Persistent filter popups** for multi-selection (classes, methods, log levels) - windows stay open during selection - **Search** across class names, methods, and message content - **Time range filter** with MinMaxSlider - **Export** filtered logs to timestamped `.txt` files - **Right-click context menu** for quick filtering and copy actions - **Visual improvements:** White text, alternating row backgrounds, color-coded log levels - **Multiple instances** supported for simultaneous system monitoring - Open via `AppleHills > Custom Log Console` Co-authored-by: Michal Pikulski <michal@foolhardyhorizons.com> Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com> Reviewed-on: https://homelab.tailf7f81b.ts.net/tschesky/AppleHillsProduction/pulls/56
2025-11-11 08:48:29 +00:00
internal override void OnManagedStart()
{
2025-10-17 15:20:29 +02:00
// Get required components
2025-10-29 17:01:02 +01:00
appleAudioSource = GetComponent<AppleAudioSource>();
2025-10-17 15:20:29 +02:00
speechBubble = GetComponentInChildren<SpeechBubble>();
if (speechBubble == null)
{
Debug.LogError("SpeechBubble component is missing on Dialogue Component");
}
Refactoring of the interaction system and preliminary integration of save/load functionality across the game. (#44) ### Interactables Architecture Refactor - Converted composition to inheritance, moved from component-based to class-based interactables. No more requirement for chain of "Interactable -> Item" etc. - Created `InteractableBase` abstract base class with common functionality that replaces the old component - Specialized child classes: `Pickup`, `ItemSlot`, `LevelSwitch`, `MinigameSwitch`, `CombinationItem`, `OneClickInteraction` are now children classes - Light updates to the interactable inspector, moved some things arround, added collapsible inspector sections in the UI for better editor experience ### State Machine Integration - Custom `AppleMachine` inheritong from Pixelplacement's StateMachine which implements our own interface for saving, easy place for future improvements - Replaced all previous StateMachines by `AppleMachine` - Custom `AppleState` extends from default `State`. Added serialization, split state logic into "EnterState", "RestoreState", "ExitState" allowing for separate logic when triggering in-game vs loading game - Restores directly to target state without triggering transitional logic - Migration tool converts existing instances ### Prefab Organization - Saved changes from scenes into prefabs - Cleaned up duplicated components, confusing prefabs hierarchies - Created prefab variants where possible - Consolidated Environment prefabs and moved them out of Placeholders subfolder into main Environment folder - Organized item prefabs from PrefabsPLACEHOLDER into proper Items folder - Updated prefab references - All scene references updated to new locations - Removed placeholder files from Characters, Levels, UI, and Minigames folders ### Scene Updates - Quarry scene with major updates - Saved multiple working versions (Quarry, Quarry_Fixed, Quarry_OLD) - Added proper lighting data - Updated all interactable components to new architecture ### Minor editor tools - New tool for testing cards from an editor window (no in-scene object required) - Updated Interactable Inspector - New debug option to opt in-and-out of the save/load system - Tooling for easier migration Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com> Reviewed-on: https://homelab.tailf7f81b.ts.net/tschesky/AppleHillsProduction/pulls/44
2025-11-03 10:12:51 +00:00
var interactable = GetComponent<InteractableBase>();
if (interactable != null)
{
interactable.characterArrived.AddListener(OnCharacterArrived);
}
// Update bubble visibility based on whether we have lines
if (speechBubble != null)
{
speechBubble.UpdatePromptVisibility(HasAnyLines());
}
2025-10-27 16:06:47 +01:00
// Register for global events
PuzzleManager.Instance.OnStepCompleted += OnAnyPuzzleStepCompleted;
ItemManager.Instance.OnItemPickedUp += OnAnyItemPickedUp;
ItemManager.Instance.OnCorrectItemSlotted += OnAnyItemSlotted;
ItemManager.Instance.OnIncorrectItemSlotted += OnAnyIncorrectItemSlotted;
ItemManager.Instance.OnForbiddenItemSlotted += OnAnyForbiddenItemSlotted;
ItemManager.Instance.OnItemSlotCleared += OnAnyItemSlotCleared;
ItemManager.Instance.OnItemsCombined += OnAnyItemsCombined;
}
private void OnCharacterArrived()
{
if (speechBubble == null || !HasAnyLines()) return;
// Advance the dialogue state to move to the next content
AdvanceDialogueState();
// Check if we have DialogueContent available (prioritizing the new content system)
DialogueContent content = GetCurrentDialogueContent();
if (content != null)
{
// Display the content with the method that handles both text and images
// and pass whether there are more lines available for prompt display
speechBubble.DisplayDialogueContent(content, HasAnyLines());
2025-10-17 15:20:29 +02:00
// Play audio if available
PlayDialogueAudio(content.Audio);
// Log the content type for debugging
Logging.Debug($"Displaying content type: {content.ContentType} - {(content.ContentType == DialogueContentType.Text ? content.Text : content.Image?.name)}");
}
else
{
// Fall back to legacy text-only method if no DialogueContent is available
string line = GetCurrentDialogueLine();
speechBubble.DisplayDialogueLine(line, HasAnyLines());
// Log for debugging
Logging.Debug($"Displaying legacy text: {line}");
}
}
2025-10-17 15:20:29 +02:00
/// <summary>
/// Play the audio clip for the current dialogue content
/// </summary>
/// <param name="clip">Audio clip to play</param>
2025-10-29 17:01:02 +01:00
private void PlayDialogueAudio(AudioResource clip)
2025-10-17 15:20:29 +02:00
{
// Stop any currently playing audio
2025-10-29 17:01:02 +01:00
if (appleAudioSource.audioSource.isPlaying)
2025-10-17 15:20:29 +02:00
{
2025-10-29 17:01:02 +01:00
appleAudioSource.Stop();
2025-10-17 15:20:29 +02:00
}
// Play the new clip if it exists
if (clip != null)
{
2025-10-29 17:01:02 +01:00
appleAudioSource.audioSource.resource = clip;
2025-10-30 14:17:47 +01:00
appleAudioSource.Play(1);
2025-10-17 15:20:29 +02:00
Logging.Debug($"Playing dialogue audio: {clip.name}");
}
}
/// <summary>
/// Get the current dialogue content (text or image)
/// </summary>
/// <returns>DialogueContent or null if only legacy text content is available</returns>
private DialogueContent GetCurrentDialogueContent()
{
// Initialize if needed
if (!initialized)
{
StartDialogue();
}
if (!IsActive || IsCompleted || currentNode == null)
return null;
// Check if we have DialogueContent available
if (currentNode.dialogueContent != null && currentNode.dialogueContent.Count > 0)
{
// For WaitOnSlot nodes, use the appropriate content based on slot state
if (currentNode.nodeType == RuntimeDialogueNodeType.WaitOnSlot)
{
// Choose the appropriate content collection based on the current slot state
List<DialogueContent> contentForState = currentNode.dialogueContent; // Default content
switch (_currentSlotState)
{
case ItemSlotState.Incorrect:
// Use incorrect item content if available
if (currentNode.incorrectItemContent != null && currentNode.incorrectItemContent.Count > 0)
contentForState = currentNode.incorrectItemContent;
break;
case ItemSlotState.Forbidden:
// Use forbidden item content if available
if (currentNode.forbiddenItemContent != null && currentNode.forbiddenItemContent.Count > 0)
contentForState = currentNode.forbiddenItemContent;
break;
}
// If we have content for this state, return the current one
if (contentForState != null && contentForState.Count > 0)
{
// Make sure index is within bounds
int index = Mathf.Clamp(currentLineIndex, 0, contentForState.Count - 1);
return contentForState[index];
}
return null; // No content for this slot state
}
else
{
// For other node types, use the default dialogueContent
if (currentLineIndex >= 0 && currentLineIndex < currentNode.dialogueContent.Count)
{
return currentNode.dialogueContent[currentLineIndex];
}
}
}
// No DialogueContent available, will fall back to legacy text handling
return null;
}
Lifecycle System Refactor & Logging Centralization (#56) ## ManagedBehaviour System Refactor - **Sealed `Awake()`** to prevent override mistakes that break singleton registration - **Added `OnManagedAwake()`** for early initialization (fires during registration) - **Renamed lifecycle hook:** `OnManagedAwake()` → `OnManagedStart()` (fires after boot, mirrors Unity's Awake→Start) - **40 files migrated** to new pattern (2 core, 38 components) - Eliminated all fragile `private new void Awake()` patterns - Zero breaking changes - backward compatible ## Centralized Logging System - **Automatic tagging** via `CallerMemberName` and `CallerFilePath` - logs auto-tagged as `[ClassName][MethodName] message` - **Unified API:** Single `Logging.Debug/Info/Warning/Error()` replaces custom `LogDebugMessage()` implementations - **~90 logging call sites** migrated across 10 files - **10 redundant helper methods** removed - All logs broadcast via `Logging.OnLogEntryAdded` event for real-time monitoring ## Custom Log Console (Editor Window) - **Persistent filter popups** for multi-selection (classes, methods, log levels) - windows stay open during selection - **Search** across class names, methods, and message content - **Time range filter** with MinMaxSlider - **Export** filtered logs to timestamped `.txt` files - **Right-click context menu** for quick filtering and copy actions - **Visual improvements:** White text, alternating row backgrounds, color-coded log levels - **Multiple instances** supported for simultaneous system monitoring - Open via `AppleHills > Custom Log Console` Co-authored-by: Michal Pikulski <michal@foolhardyhorizons.com> Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com> Reviewed-on: https://homelab.tailf7f81b.ts.net/tschesky/AppleHillsProduction/pulls/56
2025-11-11 08:48:29 +00:00
protected override void OnDestroy()
{
Lifecycle System Refactor & Logging Centralization (#56) ## ManagedBehaviour System Refactor - **Sealed `Awake()`** to prevent override mistakes that break singleton registration - **Added `OnManagedAwake()`** for early initialization (fires during registration) - **Renamed lifecycle hook:** `OnManagedAwake()` → `OnManagedStart()` (fires after boot, mirrors Unity's Awake→Start) - **40 files migrated** to new pattern (2 core, 38 components) - Eliminated all fragile `private new void Awake()` patterns - Zero breaking changes - backward compatible ## Centralized Logging System - **Automatic tagging** via `CallerMemberName` and `CallerFilePath` - logs auto-tagged as `[ClassName][MethodName] message` - **Unified API:** Single `Logging.Debug/Info/Warning/Error()` replaces custom `LogDebugMessage()` implementations - **~90 logging call sites** migrated across 10 files - **10 redundant helper methods** removed - All logs broadcast via `Logging.OnLogEntryAdded` event for real-time monitoring ## Custom Log Console (Editor Window) - **Persistent filter popups** for multi-selection (classes, methods, log levels) - windows stay open during selection - **Search** across class names, methods, and message content - **Time range filter** with MinMaxSlider - **Export** filtered logs to timestamped `.txt` files - **Right-click context menu** for quick filtering and copy actions - **Visual improvements:** White text, alternating row backgrounds, color-coded log levels - **Multiple instances** supported for simultaneous system monitoring - Open via `AppleHills > Custom Log Console` Co-authored-by: Michal Pikulski <michal@foolhardyhorizons.com> Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com> Reviewed-on: https://homelab.tailf7f81b.ts.net/tschesky/AppleHillsProduction/pulls/56
2025-11-11 08:48:29 +00:00
base.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;
}
// First check if we have any dialogueContent to process
bool hasDialogueContent = currentNode.dialogueContent != null && currentNode.dialogueContent.Count > 0;
if (hasDialogueContent)
{
// If we have dialogueContent and there are more entries, advance to the next one
if (currentLineIndex < currentNode.dialogueContent.Count - 1)
{
currentLineIndex++;
return;
}
// If we should loop through content, reset the index
if (currentNode.loopThroughLines && currentNode.dialogueContent.Count > 0)
{
currentLineIndex = 0;
return;
}
}
else
{
// Fall back to legacy dialogueLines
// If we have more lines in the current node, advance to the next line
if (currentLineIndex < currentNode.dialogueLines.Count - 1)
{
currentLineIndex++;
return;
}
// If we should loop through lines, reset the index
if (currentNode.loopThroughLines && currentNode.dialogueLines.Count > 0)
{
currentLineIndex = 0;
return;
}
}
// If we're at a node that doesn't have a next node, we're done
if (string.IsNullOrEmpty(currentNode.nextNodeID))
{
IsActive = false;
IsCompleted = true;
return;
}
// Move to the next node only if no conditions to wait for
if (!IsWaitingForCondition())
{
MoveToNextNode();
}
}
private void MoveToNextNode()
{
Logging.Debug("MoveToNextNode");
// If there's no next node, complete the dialogue
if (string.IsNullOrEmpty(currentNode.nextNodeID))
{
IsActive = false;
IsCompleted = true;
return;
}
// Move to the next node
currentNode = dialogueGraph.GetNodeByID(currentNode.nextNodeID);
currentLineIndex = 0;
// Process the new node
ProcessCurrentNode();
}
private void ProcessCurrentNode()
{
if (currentNode == null)
{
Debug.LogError("DialogueComponent: Current node is null!");
return;
}
// Handle different node types
switch (currentNode.nodeType)
{
case RuntimeDialogueNodeType.Dialogue:
// Regular dialogue node, nothing special to do
break;
case RuntimeDialogueNodeType.WaitOnPuzzleStep:
// Check if the puzzle step is already completed
if (IsPuzzleStepComplete(currentNode.puzzleStepID))
{
// If it's already complete, move past this node automatically
MoveToNextNode();
}
break;
case RuntimeDialogueNodeType.WaitOnPickup:
// Check if the item is already picked up
if (IsItemPickedUp(currentNode.pickupItemID))
{
// If it's already picked up, move past this node automatically
MoveToNextNode();
}
break;
case RuntimeDialogueNodeType.WaitOnSlot:
// Check if the item is already slotted
if (IsItemSlotted(currentNode.slotItemID))
{
// If it's already slotted, move past this node automatically
MoveToNextNode();
}
break;
case RuntimeDialogueNodeType.WaitOnCombination:
// Check if the result item is already created through combination
if (IsResultItemCreated(currentNode.combinationResultItemID))
{
// If it's already created, move past this node automatically
MoveToNextNode();
}
break;
case RuntimeDialogueNodeType.End:
// End node, complete the dialogue
IsActive = false;
IsCompleted = true;
break;
default:
Debug.LogError($"DialogueComponent: Unknown node type {currentNode.nodeType}");
break;
}
}
// Global event handlers
private void OnAnyPuzzleStepCompleted(PuzzleStepSO step)
{
2025-10-27 16:06:47 +01:00
// Initialize if needed
if (!initialized)
{
StartDialogue();
}
// Only react if we're active and waiting on a puzzle step
if (!IsActive || IsCompleted || currentNode == null ||
currentNode.nodeType != RuntimeDialogueNodeType.WaitOnPuzzleStep)
return;
// Check if this is the step we're waiting for
if (step.stepId == currentNode.puzzleStepID)
{
2025-10-27 16:06:47 +01:00
// Set the flag that condition is satisfied
_conditionSatisfiedPendingAdvance = true;
2025-10-27 16:06:47 +01:00
UpdateDialogueVisibilityOnItemEvent();
}
}
private void OnAnyItemPickedUp(PickupItemData item)
{
2025-10-27 16:06:47 +01:00
// Initialize if needed
if (!initialized)
{
StartDialogue();
}
// Only react if we're active and waiting on an item pickup
if (!IsActive || IsCompleted || currentNode == null ||
currentNode.nodeType != RuntimeDialogueNodeType.WaitOnPickup)
return;
// Check if this is the item we're waiting for
if (item.itemId == currentNode.pickupItemID)
{
2025-10-27 16:06:47 +01:00
// Set the flag that condition is satisfied
_conditionSatisfiedPendingAdvance = true;
2025-10-27 16:06:47 +01:00
UpdateDialogueVisibilityOnItemEvent();
}
}
private void OnAnyItemSlotted(PickupItemData slotDefinition, PickupItemData slottedItem)
{
Logging.Debug("[DialogueComponent] OnAnyItemSlotted");
2025-10-27 16:06:47 +01:00
// Initialize if needed
if (!initialized)
{
StartDialogue();
}
// Only react if we're active and waiting on a slot
if (!IsActive || IsCompleted || currentNode == null ||
currentNode.nodeType != RuntimeDialogueNodeType.WaitOnSlot)
return;
// Check if this is the slot we're waiting for
if (slotDefinition.itemId == currentNode.slotItemID)
{
2025-10-27 16:06:47 +01:00
// Set the flag that condition is satisfied
_conditionSatisfiedPendingAdvance = true;
2025-10-27 16:06:47 +01:00
UpdateDialogueVisibilityOnItemEvent();
}
}
private void OnAnyItemsCombined(PickupItemData itemA, PickupItemData itemB, PickupItemData resultItem)
{
2025-10-27 16:06:47 +01:00
// Initialize if needed
if (!initialized)
{
StartDialogue();
}
// Only react if we're active and waiting on a combination
if (!IsActive || IsCompleted || currentNode == null ||
currentNode.nodeType != RuntimeDialogueNodeType.WaitOnCombination)
return;
// Check if this is the result item we're waiting for
if (resultItem.itemId == currentNode.combinationResultItemID)
{
2025-10-27 16:06:47 +01:00
// Set the flag that condition is satisfied
_conditionSatisfiedPendingAdvance = true;
2025-10-27 16:06:47 +01:00
UpdateDialogueVisibilityOnItemEvent();
}
}
private void OnAnyIncorrectItemSlotted(PickupItemData slotDefinition, PickupItemData slottedItem)
{
2025-10-27 16:06:47 +01:00
// Initialize if needed
if (!initialized)
{
StartDialogue();
}
// Update the slot state for displaying the correct dialogue lines
if (!IsActive || IsCompleted || currentNode == null)
return;
// Only update state if we're actively waiting on this slot
if (currentNode.nodeType == RuntimeDialogueNodeType.WaitOnSlot &&
slotDefinition.itemId == currentNode.slotItemID)
{
_currentSlotState = ItemSlotState.Incorrect;
_lastSlottedItem = slottedItem;
2025-10-27 16:06:47 +01:00
UpdateDialogueVisibilityOnItemEvent();
}
}
private void OnAnyForbiddenItemSlotted(PickupItemData slotDefinition, PickupItemData slottedItem)
{
2025-10-27 16:06:47 +01:00
// Initialize if needed
if (!initialized)
{
StartDialogue();
}
// Update the slot state for displaying the correct dialogue lines
if (!IsActive || IsCompleted || currentNode == null)
return;
// Only update state if we're actively waiting on this slot
if (currentNode.nodeType == RuntimeDialogueNodeType.WaitOnSlot &&
slotDefinition.itemId == currentNode.slotItemID)
{
_currentSlotState = ItemSlotState.Forbidden;
_lastSlottedItem = slottedItem;
2025-10-27 16:06:47 +01:00
UpdateDialogueVisibilityOnItemEvent();
}
}
private void OnAnyItemSlotCleared(PickupItemData removedItem)
{
2025-10-27 16:06:47 +01:00
// Initialize if needed
if (!initialized)
{
StartDialogue();
}
// Update the slot state when an item is removed
if (!IsActive || IsCompleted || currentNode == null)
return;
// Reset slot state if we were tracking this item
if (_lastSlottedItem != null && _lastSlottedItem == removedItem)
{
_currentSlotState = ItemSlotState.None;
_lastSlottedItem = null;
2025-10-27 16:06:47 +01:00
UpdateDialogueVisibilityOnItemEvent();
}
}
private void UpdateDialogueVisibilityOnItemEvent()
{
// If auto-play is enabled, immediately display dialogue
if (currentNode.shouldAutoPlay)
{
OnCharacterArrived();
}
else
{
// Manual mode: just update bubble visibility to show interaction prompt
if (speechBubble != null)
{
speechBubble.UpdatePromptVisibility(HasAnyLines());
}
}
}
// Helper methods
private bool IsWaitingForCondition()
{
if (currentNode == null) return false;
switch (currentNode.nodeType)
{
case RuntimeDialogueNodeType.WaitOnPuzzleStep:
return !IsPuzzleStepComplete(currentNode.puzzleStepID);
case RuntimeDialogueNodeType.WaitOnPickup:
return !IsItemPickedUp(currentNode.pickupItemID);
case RuntimeDialogueNodeType.WaitOnSlot:
return !IsItemSlotted(currentNode.slotItemID);
case RuntimeDialogueNodeType.WaitOnCombination:
return !IsResultItemCreated(currentNode.combinationResultItemID);
default:
return false;
}
}
private bool IsPuzzleStepComplete(string stepID)
{
return PuzzleManager.Instance != null &&
PuzzleManager.Instance.IsPuzzleStepCompleted(stepID);
}
private bool IsItemPickedUp(string itemID)
{
if (ItemManager.Instance == null) return false;
// Check all pickups for the given ID
foreach (var pickup in ItemManager.Instance.Pickups)
{
Refactoring of the interaction system and preliminary integration of save/load functionality across the game. (#44) ### Interactables Architecture Refactor - Converted composition to inheritance, moved from component-based to class-based interactables. No more requirement for chain of "Interactable -> Item" etc. - Created `InteractableBase` abstract base class with common functionality that replaces the old component - Specialized child classes: `Pickup`, `ItemSlot`, `LevelSwitch`, `MinigameSwitch`, `CombinationItem`, `OneClickInteraction` are now children classes - Light updates to the interactable inspector, moved some things arround, added collapsible inspector sections in the UI for better editor experience ### State Machine Integration - Custom `AppleMachine` inheritong from Pixelplacement's StateMachine which implements our own interface for saving, easy place for future improvements - Replaced all previous StateMachines by `AppleMachine` - Custom `AppleState` extends from default `State`. Added serialization, split state logic into "EnterState", "RestoreState", "ExitState" allowing for separate logic when triggering in-game vs loading game - Restores directly to target state without triggering transitional logic - Migration tool converts existing instances ### Prefab Organization - Saved changes from scenes into prefabs - Cleaned up duplicated components, confusing prefabs hierarchies - Created prefab variants where possible - Consolidated Environment prefabs and moved them out of Placeholders subfolder into main Environment folder - Organized item prefabs from PrefabsPLACEHOLDER into proper Items folder - Updated prefab references - All scene references updated to new locations - Removed placeholder files from Characters, Levels, UI, and Minigames folders ### Scene Updates - Quarry scene with major updates - Saved multiple working versions (Quarry, Quarry_Fixed, Quarry_OLD) - Added proper lighting data - Updated all interactable components to new architecture ### Minor editor tools - New tool for testing cards from an editor window (no in-scene object required) - Updated Interactable Inspector - New debug option to opt in-and-out of the save/load system - Tooling for easier migration Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com> Reviewed-on: https://homelab.tailf7f81b.ts.net/tschesky/AppleHillsProduction/pulls/44
2025-11-03 10:12:51 +00:00
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 or content
RuntimeDialogueNode nextNode = dialogueGraph.GetNodeByID(currentNode.nextNodeID);
return nextNode != null &&
((nextNode.dialogueLines != null && nextNode.dialogueLines.Count > 0) ||
(nextNode.dialogueContent != null && nextNode.dialogueContent.Count > 0) ||
nextNode.nodeType != RuntimeDialogueNodeType.End);
}
// For WaitOnSlot nodes, check for lines or content based on current slot state
if (currentNode.nodeType == RuntimeDialogueNodeType.WaitOnSlot)
{
// First check for DialogueContent
if (currentNode.dialogueContent != null && currentNode.dialogueContent.Count > 0)
{
// Choose the appropriate content collection based on the current slot state
List<DialogueContent> contentForState = currentNode.dialogueContent;
switch (_currentSlotState)
{
case ItemSlotState.Incorrect:
if (currentNode.incorrectItemContent != null && currentNode.incorrectItemContent.Count > 0)
contentForState = currentNode.incorrectItemContent;
break;
case ItemSlotState.Forbidden:
if (currentNode.forbiddenItemContent != null && currentNode.forbiddenItemContent.Count > 0)
contentForState = currentNode.forbiddenItemContent;
break;
}
if (contentForState.Count > 0)
{
if (currentLineIndex < contentForState.Count - 1 || currentNode.loopThroughLines)
{
return true;
}
}
}
// Fall back to legacy text lines
List<string> linesForState = currentNode.dialogueLines;
switch (_currentSlotState)
{
case ItemSlotState.Incorrect:
if (currentNode.incorrectItemLines != null && currentNode.incorrectItemLines.Count > 0)
linesForState = currentNode.incorrectItemLines;
break;
case ItemSlotState.Forbidden:
if (currentNode.forbiddenItemLines != null && currentNode.forbiddenItemLines.Count > 0)
linesForState = currentNode.forbiddenItemLines;
break;
}
if (linesForState != null && linesForState.Count > 0)
{
if (currentLineIndex < linesForState.Count - 1 || currentNode.loopThroughLines)
{
return true;
}
}
}
// For other node types, check for DialogueContent first, then fall back to legacy text
else
{
// Check for DialogueContent
if (currentNode.dialogueContent != null && currentNode.dialogueContent.Count > 0)
{
if (currentLineIndex < currentNode.dialogueContent.Count - 1 || currentNode.loopThroughLines)
{
return true;
}
// If we're at the end of content but not waiting for a condition and have a next node
if (!IsWaitingForCondition() && !string.IsNullOrEmpty(currentNode.nextNodeID))
{
RuntimeDialogueNode nextNode = dialogueGraph.GetNodeByID(currentNode.nextNodeID);
return nextNode != null &&
((nextNode.dialogueContent != null && nextNode.dialogueContent.Count > 0) ||
(nextNode.dialogueLines != null && nextNode.dialogueLines.Count > 0) ||
nextNode.nodeType != RuntimeDialogueNodeType.End);
}
}
// Fall back to legacy text lines
if (currentNode.dialogueLines != null && currentNode.dialogueLines.Count > 0)
{
if (currentLineIndex < currentNode.dialogueLines.Count - 1 || currentNode.loopThroughLines)
{
return true;
}
// If we're at the end of lines but not waiting for a condition and have a next node
if (!IsWaitingForCondition() && !string.IsNullOrEmpty(currentNode.nextNodeID))
{
RuntimeDialogueNode nextNode = dialogueGraph.GetNodeByID(currentNode.nextNodeID);
return nextNode != null &&
((nextNode.dialogueContent != null && nextNode.dialogueContent.Count > 0) ||
(nextNode.dialogueLines != null && nextNode.dialogueLines.Count > 0) ||
nextNode.nodeType != RuntimeDialogueNodeType.End);
}
}
}
return false;
}
// Editor functionality
public void SetDialogueGraph(RuntimeDialogueGraph graph)
{
dialogueGraph = graph;
}
}
}