using System; using System.Collections; using System.Collections.Generic; using PixelCrushers; using PixelCrushers.DialogueSystem; using Sirenix.OdinInspector; using Sirenix.Utilities; using UnityEngine; using UnityEngine.Audio; using Random = UnityEngine.Random; namespace Beyond // Ensure this namespace matches your project structure { // Forward declare Player if it's in a different namespace or assembly, // or ensure 'using Beyond.PlayerNamespace;' if applicable. // Assuming Player class might be in the same 'Beyond' namespace for simplicity here. // If Player class is defined elsewhere, you might need: // using YourPlayerNamespace; public class BarkManager : Saver { // --- Inner Classes (Bark, BarkEntry, SaveData) remain the same --- [Serializable] public class Bark { [Tooltip("Conversation to get bark content from.")] [ConversationPopup(false)] public string barkConversation = string.Empty; [Tooltip("Direct text to bark if Conversation is empty.")] public string barkText; [Tooltip("Audio clip for the bark.")] public AudioClip clip; } [Serializable] public class BarkEntry { [Tooltip("If true, this entire entry can only be triggered once via PlayBark(int) or PlayBark(string). BarkPlayer ignores this for sequential playback.")] public bool playOnce = true; [Tooltip("Delay before the bark starts playing (applies to the first bark triggered externally or random barks).")] public float delayToStart = 0f; [Tooltip("Optional custom audio mixer group for barks in this entry.")] public AudioMixerGroup customOutput; [Tooltip("The list of barks for this entry.")] public Bark[] barks; } [ListDrawerSettings(DraggableItems = true, Expanded = false, ShowIndexLabels = true, ShowPaging = false, ShowItemCount = true, HideRemoveButton = true)] public BarkEntry[] m_barks; [Serializable] public class SaveData { public bool[] wasEntryPlayed; } public SaveData m_saveData = new SaveData(); private Queue m_barkQueue = new Queue(); private struct QueuedBarkRequest { public int entryIndex; public Transform target; // Can be null if default (Player) is intended } public AudioSource m_audioSource; private Dictionary m_conversationToEntry = new Dictionary(); private static BarkManager s_instance; public static BarkManager Instance => s_instance; public bool IsPlaying => m_audioSource != null && m_audioSource.isPlaying; // --- Core Methods --- public override void Awake() { base.Awake(); if (s_instance != null && s_instance != this) { Debug.LogError("BarkManager instance already exists! Destroying this duplicate.", this); Destroy(gameObject); } else { s_instance = this; // Optional: DontDestroyOnLoad(gameObject); } SetupAudioSource(); } private void Start() { SetupAudioSource(); InitializeSaveData(); BuildConversationLookup(); } private void SetupAudioSource() { if (m_audioSource == null) m_audioSource = GetComponent(); if (m_audioSource == null) { m_audioSource = gameObject.AddComponent(); m_audioSource.playOnAwake = false; m_audioSource.spatialBlend = 1.0f; } } private void InitializeSaveData() { if (m_saveData.wasEntryPlayed == null || m_saveData.wasEntryPlayed.Length != m_barks.Length) { m_saveData.wasEntryPlayed = new bool[m_barks.Length]; } } private void BuildConversationLookup() { m_conversationToEntry.Clear(); for (int i = 0; i < m_barks.Length; i++) { var entry = m_barks[i]; if (entry.barks != null) { foreach (var bark in entry.barks) { if (!string.IsNullOrWhiteSpace(bark.barkConversation)) { if (!m_conversationToEntry.ContainsKey(bark.barkConversation)) { m_conversationToEntry[bark.barkConversation] = i; } } } } } } public void PlayBark(int num) // <<< METHOD RESTORED HERE { // This simply queues the request with a null target. // The Update loop will handle dequeuing, checking playOnce, // resolving the null target to Player.Instance, and playing. PlayBarkFromQueue(num, null); } /// /// Plays a bark from the specified entry. Uses Player.Instance.transform for text barks if barkTarget is null. /// /// Index of the BarkEntry. /// Transform for positioning Dialogue Manager barks. If null, attempts to use Player.Instance.transform. /// Index of the specific bark within the entry. -1 for random. /// The AudioClip that will be played (or null if none/delayed). public AudioClip PlayBark(int entryIndex, Transform barkTarget = null, int specificBarkIndex = -1) { if (entryIndex < 0 || entryIndex >= m_barks.Length) { Debug.LogError($"BarkManager: Invalid entryIndex {entryIndex}.", this); return null; } // --- Determine the target for text barks --- Transform targetForText = barkTarget; if (targetForText == null) { if (Player.Instance != null) // Check if Player singleton exists { targetForText = Player.Instance.transform; } else { // Player.Instance is null, text bark won't have a specific target unless one was provided. // Audio will still play. Log warning maybe? // Debug.LogWarning($"BarkManager: barkTarget is null and Player.Instance is null. Text bark for entry {entryIndex} will not be shown.", this); } } // --- End Target Determination --- var barkEntry = m_barks[entryIndex]; // Check playOnce only for random barks if (specificBarkIndex == -1 && barkEntry.playOnce && m_saveData.wasEntryPlayed != null && m_saveData.wasEntryPlayed[entryIndex]) { return null; } if (barkEntry.barks == null || barkEntry.barks.Length == 0) { Debug.LogWarning($"BarkManager: BarkEntry {entryIndex} has no barks defined.", this); return null; } AudioClip clipToPlay = null; if (specificBarkIndex != -1) // Specific bark (e.g., BarkPlayer) - play immediately { // Pass the determined targetForText clipToPlay = PlayBarkImmediately(barkEntry, targetForText, specificBarkIndex); } else // Random bark { if (barkEntry.delayToStart > 0f && !IsPlaying) { // Pass the determined targetForText to the coroutine StartCoroutine(PlayBarkWithDelayCoroutine(barkEntry, targetForText, specificBarkIndex)); } else { // Pass the determined targetForText clipToPlay = PlayBarkImmediately(barkEntry, targetForText, specificBarkIndex); } // Mark entry as played if playOnce (only for random) if (barkEntry.playOnce && m_saveData.wasEntryPlayed != null) { m_saveData.wasEntryPlayed[entryIndex] = true; } } return clipToPlay; } /// /// Enqueues a request to play a random bark from the entry associated with the conversation. /// Uses Player.Instance.transform for text barks if barkTarget is null. /// public void PlayBark(string conversation, Transform barkTarget = null) { if (m_conversationToEntry.TryGetValue(conversation, out int entryIndex)) { // Determine target *before* queuing, store null if Player isn't available/needed Transform targetForQueue = barkTarget; if (targetForQueue == null && Player.Instance != null) { targetForQueue = Player.Instance.transform; } // Note: If Player.Instance becomes available *later* when the queue processes, // the Update loop logic will handle checking for it again. // So, we can actually queue null and let Update resolve it. m_barkQueue.Enqueue(new QueuedBarkRequest { entryIndex = entryIndex, target = barkTarget }); // Queue the original target (null if default) } else { Debug.LogError($"BarkManager: Conversation '{conversation}' not found in any BarkEntry.", this); } } /// /// Enqueues a request to play a random bark from the specified entry index. /// Uses Player.Instance.transform for text barks if barkTarget is null. /// public void PlayBarkFromQueue(int entryIndex, Transform barkTarget = null) { // Queue the original target (null if default intended). Update loop will resolve Player.Instance if needed. m_barkQueue.Enqueue(new QueuedBarkRequest { entryIndex = entryIndex, target = barkTarget }); } private IEnumerator PlayBarkWithDelayCoroutine(BarkEntry barkEntry, Transform targetForText, int specificBarkIndex) { // Note: Player.Instance could become valid/invalid during this delay. // Re-check target just before playing? Or rely on the target passed in? // Let's stick with the target determined when the coroutine was started for consistency. yield return new WaitForSecondsRealtime(barkEntry.delayToStart); PlayBarkImmediately(barkEntry, targetForText, specificBarkIndex); } /// /// Internal method to immediately play audio and show text bark. Uses the provided targetForText. /// private AudioClip PlayBarkImmediately(BarkEntry barkEntry, Transform targetForText, int specificBarkIndex = -1) { if (barkEntry.barks == null || barkEntry.barks.Length == 0) return null; Bark barkToPlay = null; int chosenIndex = specificBarkIndex; if (chosenIndex < 0 || chosenIndex >= barkEntry.barks.Length) { if (barkEntry.barks.Length > 0) chosenIndex = Random.Range(0, barkEntry.barks.Length); else return null; } barkToPlay = barkEntry.barks[chosenIndex]; // --- Audio Playback --- AudioClip clipToPlay = null; if (barkToPlay.clip != null) { if (m_audioSource == null) Debug.LogError("BarkManager: m_audioSource is null! Cannot play audio.", this); else { m_audioSource.outputAudioMixerGroup = barkEntry.customOutput != null ? barkEntry.customOutput : null; m_audioSource.PlayOneShot(barkToPlay.clip); clipToPlay = barkToPlay.clip; } } // --- Text Bark --- // Use the resolved targetForText passed into this method if (targetForText != null) { if (!string.IsNullOrWhiteSpace(barkToPlay.barkConversation)) { DialogueManager.Bark(barkToPlay.barkConversation, targetForText); } else if (!string.IsNullOrEmpty(barkToPlay.barkText)) { DialogueManager.BarkString(barkToPlay.barkText, targetForText); } } // If targetForText is null here, no text bark is shown. return clipToPlay; } // --- Utility & Save/Load --- (Remain the same) public bool CanBarkBeStillPlayed(int entryIndex) { if (entryIndex < 0 || entryIndex >= m_barks.Length) return false; if (m_saveData.wasEntryPlayed == null || entryIndex >= m_saveData.wasEntryPlayed.Length) return true; return !m_barks[entryIndex].playOnce || !m_saveData.wasEntryPlayed[entryIndex]; } public int GetBarkCountInEntry(int entryIndex) { if (entryIndex >= 0 && entryIndex < m_barks.Length && m_barks[entryIndex].barks != null) { return m_barks[entryIndex].barks.Length; } return 0; } public override string RecordData() { if (m_saveData.wasEntryPlayed == null || m_saveData.wasEntryPlayed.Length != m_barks.Length) { bool[] oldData = m_saveData.wasEntryPlayed; m_saveData.wasEntryPlayed = new bool[m_barks.Length]; if (oldData != null) Array.Copy(oldData, m_saveData.wasEntryPlayed, Math.Min(oldData.Length, m_barks.Length)); } return SaveSystem.Serialize(m_saveData); } public override void ApplyData(string s) { if (string.IsNullOrEmpty(s)) return; var loadedData = SaveSystem.Deserialize(s); if (loadedData != null) { if (loadedData.wasEntryPlayed != null && loadedData.wasEntryPlayed.Length == m_barks.Length) { m_saveData = loadedData; } else { Debug.LogWarning($"BarkManager: Loaded 'wasEntryPlayed' data length mismatch. Discarding loaded state.", this); InitializeSaveData(); } } else Debug.LogError("BarkManager: Failed to deserialize save data.", this); } private void Update() { // Process the queue for externally triggered RANDOM barks if (m_barkQueue.Count > 0 && !IsPlaying) { QueuedBarkRequest request = m_barkQueue.Dequeue(); // --- Resolve target just before playing from queue --- Transform targetForText = request.target; // Use the target stored in the queue request first if (targetForText == null) // If no specific target was queued... { if (Player.Instance != null) // Check for Player *now* { targetForText = Player.Instance.transform; } // else: targetForText remains null, text bark won't show } // --- End Target Resolution --- // Call the main PlayBark method, passing the resolved target and -1 for random PlayBark(request.entryIndex, targetForText, -1); } } } }