using System.Collections; using System.Collections.Generic; using System.Linq; using PixelCrushers; // For Saver using Sirenix.OdinInspector; // For ReadOnly/Button attributes (Optional: remove if not using Odin) using UnityEngine; using Random = UnityEngine.Random; namespace Beyond // Ensure this namespace matches your project { [RequireComponent(typeof(Collider))] public class BarkPlayer : Saver { #region Inspector Fields [Header("Bark Configuration")] [Tooltip("The indices of the BarkEntries in BarkManager's list to play sequentially.")] public int[] barkManagerEntryIndices = new int[0]; [Tooltip("Delay in seconds after one bark's audio finishes before starting the next bark WITHIN THE SAME entry sequence.")] [Min(0f)] public float delayBetweenBarks = 0.5f; [Tooltip("Delay in seconds after ALL barks in one entry sequence finish before starting the NEXT entry sequence.")] [Min(0f)] public float delayBetweenEntries = 5.0f; [Tooltip("Play barks within each entry in a random order (shuffled when the entry starts).")] public bool shuffleOrder = false; [Header("Trigger Settings")] [Tooltip("Layers that can activate this bark trigger.")] public LayerMask triggeringLayers; [Header("Activation")] [Tooltip("If true, the BarkPlayer will be active automatically when the game starts or when loaded without existing save data for it.")] public bool startAutomaticallyOnLoad = false; // Runtime state fields remain private but visible for debugging if needed [Header("Runtime State (Read Only)")] [SerializeField, ReadOnly] // Use Sirenix ReadOnly or remove if not using Odin private bool _runtime_IsStarted = false; // Tracks the current operational state [SerializeField, ReadOnly] private int currentEntryArrayIndex = 0; // Index for barkManagerEntryIndices array [SerializeField, ReadOnly] private int nextBarkSequenceIndex = 0; // Index for playbackOrder (within current entry) [SerializeField, ReadOnly] private bool isTriggeringObjectInside = false; // Is a valid object currently inside? #endregion #region Internal Variables private List playbackOrder; // Stores the bark order for the CURRENTLY active entry private bool isInitialized = false; private Collider triggerCollider; private BarkManager barkManager; // Cached reference private Coroutine currentPlaybackCoroutine = null; private Transform currentTriggererTransform = null; // Transform of the object inside #endregion #region Save Data Structure [System.Serializable] public class BarkPlayerData { public bool savedIsStarted; // Tracks the saved state from _runtime_IsStarted public int savedCurrentEntryArrayIndex; public int savedNextBarkSequenceIndex; public bool wasShuffled; // Tracks shuffle setting at time of save } private BarkPlayerData m_saveData = new BarkPlayerData(); #endregion #region Unity Lifecycle Methods public override void Awake() { // Set initial state based on Inspector setting *before* base.Awake potentially calls ApplyData // If save data exists, ApplyData will overwrite this. _runtime_IsStarted = startAutomaticallyOnLoad; base.Awake(); // This might call ApplyData via Save System // --- Component & Initial Setup --- triggerCollider = GetComponent(); if (!triggerCollider.isTrigger) { Debug.LogWarning($"BarkPlayer on {gameObject.name}: Collider was not set to 'Is Trigger'. Forcing it.", this); triggerCollider.isTrigger = true; } // --- Default LayerMask --- if (triggeringLayers.value == 0) // LayerMask is empty/unassigned { int playerLayer = LayerMask.NameToLayer("Player"); if (playerLayer != -1) { triggeringLayers = LayerMask.GetMask("Player"); // Default to Player layer Debug.LogWarning($"BarkPlayer on {gameObject.name}: Triggering Layers not set in inspector, defaulting to 'Player' layer.", this); } else { Debug.LogError($"BarkPlayer ({gameObject.name}): Triggering Layers is not set in the inspector, and the default 'Player' layer was not found. This trigger will likely not activate.", this); } } // Final state of _runtime_IsStarted reflects Inspector default OR loaded save data. } private void Start() { // --- Get BarkManager Instance --- barkManager = BarkManager.Instance; if (barkManager == null) { Debug.LogError($"BarkPlayer ({gameObject.name}): Could not find BarkManager instance! Disabling component.", this); enabled = false; return; } // --- Apply Loaded Indices --- // Ensure array index is valid even if array size changed since saving currentEntryArrayIndex = Mathf.Clamp(m_saveData.savedCurrentEntryArrayIndex, 0, Mathf.Max(0, barkManagerEntryIndices.Length - 1)); // nextBarkSequenceIndex will be applied when PrepareEntrySequence is called // --- Validate Entry Indices --- if (barkManagerEntryIndices == null || barkManagerEntryIndices.Length == 0) { Debug.LogWarning($"BarkPlayer ({gameObject.name}): No Bark Manager Entry Indices provided. Disabling.", this); enabled = false; return; } for (int i = 0; i < barkManagerEntryIndices.Length; i++) { if (barkManagerEntryIndices[i] < 0 || barkManagerEntryIndices[i] >= barkManager.m_barks.Length) { Debug.LogError($"BarkPlayer ({gameObject.name}): Invalid Bark Manager Entry Index {barkManagerEntryIndices[i]} at array position {i}. Max index is {barkManager.m_barks.Length - 1}. Disabling.", this); enabled = false; return; } } isInitialized = true; // Check if we should start playing immediately after initialization // (e.g., if loaded state is 'started' and player is already inside trigger) CheckForAutoStartIfInside(); } private void OnDrawGizmos() { Collider col = GetComponent(); if (col != null) { Gizmos.color = isTriggeringObjectInside ? new Color(1f, 0.5f, 0.5f, 0.4f) : new Color(0.5f, 1f, 0.5f, 0.3f); // Red when active if (col is BoxCollider box) Gizmos.DrawCube(transform.TransformPoint(box.center), Vector3.Scale(box.size, transform.lossyScale)); else if (col is SphereCollider sphere) Gizmos.DrawSphere(transform.TransformPoint(sphere.center), sphere.radius * MaxComponent(transform.lossyScale)); // Add other collider types if needed } } #endregion #region Trigger Handling private void OnTriggerEnter(Collider other) { // Ignore if not initialized, not enabled, or runtime state is not 'Started' if (!isInitialized || !enabled || !_runtime_IsStarted) return; // --- Layer Check --- int otherLayer = other.gameObject.layer; if ((triggeringLayers.value & (1 << otherLayer)) == 0) { return; // Layer not in mask, ignore this trigger event } // --- Check if already processing an object --- // This prevents multiple triggering objects from interfering. Only the first one in matters. if (isTriggeringObjectInside) return; // --- Valid Trigger --- Debug.Log($"BarkPlayer ({gameObject.name}): Triggering object '{other.name}' entered.", this); isTriggeringObjectInside = true; currentTriggererTransform = other.transform; // Store the transform for barking // --- Start Playback Coroutine if not already running --- if (currentPlaybackCoroutine == null) { // Apply potentially loaded/saved array index before starting loop currentEntryArrayIndex = Mathf.Clamp(m_saveData.savedCurrentEntryArrayIndex, 0, Mathf.Max(0, barkManagerEntryIndices.Length - 1)); // nextBarkSequenceIndex will be set by PrepareEntrySequence inside the coroutine currentPlaybackCoroutine = StartCoroutine(ContinuousPlaybackLoop()); } } private void OnTriggerExit(Collider other) { if (!isInitialized || !enabled) return; // --- Layer Check --- int otherLayer = other.gameObject.layer; if ((triggeringLayers.value & (1 << otherLayer)) == 0) { return; // Ignore exit events from non-triggering layers } // --- Check if the *tracked* object is the one exiting --- if (other.transform == currentTriggererTransform) { Debug.Log($"BarkPlayer ({gameObject.name}): Triggering object '{other.name}' exited.", this); isTriggeringObjectInside = false; currentTriggererTransform = null; // Clear the stored transform // --- Stop the Playback Coroutine --- if (currentPlaybackCoroutine != null) { StopCoroutine(currentPlaybackCoroutine); currentPlaybackCoroutine = null; Debug.Log($"BarkPlayer ({gameObject.name}): Stopping playback loop due to exit.", this); // Optional: Immediately stop BarkManager audio? // if (barkManager != null && barkManager.IsPlaying) barkManager.m_audioSource.Stop(); } } // Else: Some other object on a triggering layer exited, but the primary one is still inside. Do nothing. } #endregion #region Playback Logic /// /// Sets up the playbackOrder list for a specific BarkManager entry index. /// Resets or applies saved nextBarkSequenceIndex. /// /// The index within BarkManager's m_barks array. /// True if preparation was successful (entry has barks), false otherwise. private bool PrepareEntrySequence(int entryIndexToPrepare) { if (barkManager == null) return false; int barkCount = barkManager.GetBarkCountInEntry(entryIndexToPrepare); if (barkCount <= 0) { Debug.LogWarning($"BarkPlayer ({gameObject.name}): BarkEntry {entryIndexToPrepare} (from array index {currentEntryArrayIndex}) has no barks. Skipping entry.", this); playbackOrder = new List(); // Ensure list is empty nextBarkSequenceIndex = 0; return false; // Indicate nothing to play for this entry } // Generate sequence order (sequential or shuffled) playbackOrder = Enumerable.Range(0, barkCount).ToList(); if (shuffleOrder) { // Simple Fisher-Yates shuffle for (int i = playbackOrder.Count - 1; i > 0; i--) { int j = Random.Range(0, i + 1); // Tuple swap is concise (playbackOrder[i], playbackOrder[j]) = (playbackOrder[j], playbackOrder[i]); } // Debug.Log($"BarkPlayer ({gameObject.name}): Shuffled order for entry {entryIndexToPrepare}.", this); } // Reset sequence progress. Apply saved index ONLY if the loaded array index matches the current one. if (currentEntryArrayIndex == m_saveData.savedCurrentEntryArrayIndex) { nextBarkSequenceIndex = Mathf.Clamp(m_saveData.savedNextBarkSequenceIndex, 0, playbackOrder.Count); // Debug.Log($"BarkPlayer ({gameObject.name}): Resuming entry {entryIndexToPrepare} at bark index {nextBarkSequenceIndex}.", this); } else { nextBarkSequenceIndex = 0; // Start new entry from beginning if array index differs from saved } return true; // Preparation successful } /// /// Main coroutine managing the playback loop through entries and barks. /// private IEnumerator ContinuousPlaybackLoop() { Debug.Log($"BarkPlayer ({gameObject.name}): Starting playback loop.", this); // Initial check for valid configuration if (barkManagerEntryIndices == null || barkManagerEntryIndices.Length == 0) { Debug.LogError($"BarkPlayer ({gameObject.name}): Cannot start loop, entry indices array is empty or null.", this); currentPlaybackCoroutine = null; // Ensure coroutine reference is cleared yield break; } // Outer loop: Continues as long as a valid triggering object is inside while (isTriggeringObjectInside && currentTriggererTransform != null) { // --- Step 1: Prepare for Current Entry --- // Safety check/loop wrap for the array index if (currentEntryArrayIndex >= barkManagerEntryIndices.Length) { currentEntryArrayIndex = 0; if (barkManagerEntryIndices.Length == 0) { // Should be caught earlier, but double-check Debug.LogError($"BarkPlayer ({gameObject.name}): Entry indices array became empty during loop?", this); yield break; } } int currentManagerEntryIndex = barkManagerEntryIndices[currentEntryArrayIndex]; // Debug.Log($"BarkPlayer ({gameObject.name}): Preparing Entry Index: {currentManagerEntryIndex} (Array Pos: {currentEntryArrayIndex}).", this); // Prepare the sequence (generate playbackOrder, set nextBarkSequenceIndex) bool canPlayEntry = PrepareEntrySequence(currentManagerEntryIndex); if (!canPlayEntry) // Skip this entry if it has no barks { // Debug.Log($"BarkPlayer ({gameObject.name}): Skipping empty BarkEntry {currentManagerEntryIndex}.", this); currentEntryArrayIndex++; // Move immediately to the next entry index yield return null; // Wait a frame before checking the next entry in the outer loop continue; // Go to next iteration of the outer while loop (skips delays below) } // --- Step 2: Inner Loop - Play Barks within the Current Entry --- while (nextBarkSequenceIndex < playbackOrder.Count && isTriggeringObjectInside && currentTriggererTransform != null) { // Wait for BarkManager's AudioSource if it's busy while (barkManager.IsPlaying && isTriggeringObjectInside) { yield return null; // Wait a frame } // Re-check condition after waiting, player might have left if (!isTriggeringObjectInside || currentTriggererTransform == null) break; // Play the specific bark for this step in the sequence int barkIndexToPlay = playbackOrder[nextBarkSequenceIndex]; Debug.Log($"BarkPlayer ({gameObject.name}): Playing Bark {nextBarkSequenceIndex + 1}/{playbackOrder.Count} (Actual Index: {barkIndexToPlay}) from Entry {currentManagerEntryIndex}.", this); // Play using the specific index, passing the triggerer's transform AudioClip playedClip = barkManager.PlayBark(currentManagerEntryIndex, currentTriggererTransform, barkIndexToPlay); nextBarkSequenceIndex++; // Increment progress within this entry's sequence // Wait for the bark's duration plus the inter-bark delay float waitTime = delayBetweenBarks; if (playedClip != null) { waitTime += Mathf.Max(0f, playedClip.length); } // Ensure non-negative length // Perform the wait, checking continuously if the player leaves if (waitTime > 0) { float timer = 0f; while (timer < waitTime && isTriggeringObjectInside && currentTriggererTransform != null) { timer += Time.deltaTime; yield return null; // Wait one frame } } else { // Minimum one frame wait even if no delay/clip length yield return null; } // If player left during wait, break inner loop if (!isTriggeringObjectInside || currentTriggererTransform == null) break; } // --- End of Inner Loop (Barks within Entry) --- // --- Step 3: Transition to Next Entry (if player didn't exit) --- if (isTriggeringObjectInside && currentTriggererTransform != null) { // Current entry sequence finished normally Debug.Log($"BarkPlayer ({gameObject.name}): Finished sequence for Entry {currentManagerEntryIndex}.", this); currentEntryArrayIndex++; // Move to the next entry index in the array // Check for wrapping / end of all entries in the array if (currentEntryArrayIndex >= barkManagerEntryIndices.Length) { Debug.Log($"BarkPlayer ({gameObject.name}): Finished all entries in the array. Looping back.", this); currentEntryArrayIndex = 0; // Loop back to the start of the array } // Wait for the specified delay BETWEEN entries if (delayBetweenEntries > 0) { Debug.Log($"BarkPlayer ({gameObject.name}): Waiting {delayBetweenEntries}s before next entry.", this); float timer = 0f; while (timer < delayBetweenEntries && isTriggeringObjectInside && currentTriggererTransform != null) { timer += Time.deltaTime; yield return null; // Wait one frame } // If player left during delay, break outer loop if (!isTriggeringObjectInside || currentTriggererTransform == null) break; } // The outer loop will then continue, preparing the next entry } } // --- End of Outer Loop (isTriggeringObjectInside) --- Debug.Log($"BarkPlayer ({gameObject.name}): Playback loop finished (Triggering object left or component stopped).", this); // Clear coroutine reference *if* this coroutine instance is the one finishing if (currentPlaybackCoroutine != null && (!isTriggeringObjectInside || currentTriggererTransform == null)) { currentPlaybackCoroutine = null; } } #endregion #region Public Control Methods /// /// Enables the BarkPlayer's runtime operation. It will start playing sequences when triggered. /// If a triggering object is already inside, playback may start immediately. /// [Button] // Example for Odin Inspector button public void StartPlayer() { if (!_runtime_IsStarted) { Debug.Log($"BarkPlayer ({gameObject.name}): Player Runtime Started.", this); _runtime_IsStarted = true; // If already initialized, check if we need to immediately start playback if (isInitialized) { CheckForAutoStartIfInside(); } } } /// /// Disables the BarkPlayer's runtime operation. Stops any current playback and ignores future triggers. /// [Button] // Example for Odin Inspector button public void StopPlayer() { if (_runtime_IsStarted) { Debug.Log($"BarkPlayer ({gameObject.name}): Player Runtime Stopped.", this); _runtime_IsStarted = false; // Stop any currently running playback coroutine if (currentPlaybackCoroutine != null) { StopCoroutine(currentPlaybackCoroutine); currentPlaybackCoroutine = null; // Clear the reference Debug.Log($"BarkPlayer ({gameObject.name}): Stopping active playback loop.", this); // Optional: Immediately stop BarkManager audio? // if (barkManager != null && barkManager.IsPlaying) barkManager.m_audioSource.Stop(); } } } /// /// Checks if the player is currently inside the trigger and starts the playback loop /// if the BarkPlayer is started (_runtime_IsStarted=true) but the loop isn't currently running. /// Useful after initialization (in Start) or after calling StartPlayer manually. /// private void CheckForAutoStartIfInside() { if (_runtime_IsStarted && isTriggeringObjectInside && currentTriggererTransform != null && currentPlaybackCoroutine == null && isInitialized) { Debug.Log($"BarkPlayer ({gameObject.name}): Triggering object already inside and player is started. Starting playback loop.", this); // Ensure indices are correctly set from save data before starting currentEntryArrayIndex = Mathf.Clamp(m_saveData.savedCurrentEntryArrayIndex, 0, Mathf.Max(0, barkManagerEntryIndices.Length - 1)); currentPlaybackCoroutine = StartCoroutine(ContinuousPlaybackLoop()); } } #endregion #region Save System Integration (Pixel Crushers Saver) public override string RecordData() { // Update save data container with current runtime state m_saveData.savedIsStarted = this._runtime_IsStarted; m_saveData.savedCurrentEntryArrayIndex = this.currentEntryArrayIndex; m_saveData.savedNextBarkSequenceIndex = this.nextBarkSequenceIndex; m_saveData.wasShuffled = this.shuffleOrder; // Record shuffle setting at time of save // Serialize the save data container return SaveSystem.Serialize(m_saveData); } public override void ApplyData(string s) { // Handle case where there is no save data for this component if (string.IsNullOrEmpty(s)) { // No save data - runtime state keeps the value set by startAutomaticallyOnLoad in Awake _runtime_IsStarted = startAutomaticallyOnLoad; // Reset progress indices if no save data m_saveData.savedCurrentEntryArrayIndex = 0; m_saveData.savedNextBarkSequenceIndex = 0; Debug.Log($"BarkPlayer ({gameObject.name}): No save data found, using startAutomaticallyOnLoad ({startAutomaticallyOnLoad}).", this); return; } // Deserialize the saved data string var loadedData = SaveSystem.Deserialize(s); if (loadedData != null) { m_saveData = loadedData; // Store the loaded data // Apply the loaded runtime state this._runtime_IsStarted = m_saveData.savedIsStarted; // Indices will be applied in Start() / PrepareEntrySequence() using m_saveData Debug.Log($"BarkPlayer ({gameObject.name}): Applied loaded data. Started={_runtime_IsStarted}, EntryIdx={m_saveData.savedCurrentEntryArrayIndex}, BarkIdx={m_saveData.savedNextBarkSequenceIndex}.", this); } else { Debug.LogError($"BarkPlayer ({gameObject.name}): Failed to deserialize save data. Using defaults.", this); // Fallback: use inspector setting if deserialize failed this._runtime_IsStarted = startAutomaticallyOnLoad; m_saveData.savedCurrentEntryArrayIndex = 0; m_saveData.savedNextBarkSequenceIndex = 0; } // After applying data, ensure consistency: if loaded state is 'stopped', stop coroutine if (!this._runtime_IsStarted && currentPlaybackCoroutine != null) { StopCoroutine(currentPlaybackCoroutine); currentPlaybackCoroutine = null; } // The CheckForAutoStartIfInside() call in Start() will handle restarting the // coroutine if the loaded state is 'started' and the player is inside the trigger. } #endregion #region Helper Methods /// Helper to find the largest component of a Vector3 (for Gizmos). private float MaxComponent(Vector3 v) => Mathf.Max(Mathf.Max(v.x, v.y), v.z); #endregion } }