// ============================== // HintManager.cs (fallback to nearest lower stage) // ============================== using System; using System.Collections.Generic; using System.Threading; using BriarQueen.Framework.Coordinators.Events; using BriarQueen.Framework.Events.Gameplay; using BriarQueen.Framework.Events.Progression; using BriarQueen.Framework.Managers.Hints.Data; using BriarQueen.Framework.Managers.Levels.Data; using Cysharp.Threading.Tasks; using UnityEngine; using VContainer; namespace BriarQueen.Framework.Managers.Hints { public class HintManager : IDisposable, IManager { private readonly EventCoordinator _eventCoordinator; private readonly Dictionary _hints = new(); private CancellationTokenSource _activeHintCts; private BaseLevel _currentLevel; private CancellationTokenSource _levelCts; public bool Initialized { get; private set; } [Inject] public HintManager(EventCoordinator eventCoordinator) { _eventCoordinator = eventCoordinator; } public IReadOnlyDictionary Hints => _hints; public void Dispose() { _eventCoordinator.Unsubscribe(OnLevelChanged); _eventCoordinator.Unsubscribe(OnHintRequested); CancelAndDispose(ref _activeHintCts); CancelAndDispose(ref _levelCts); _currentLevel = null; _hints.Clear(); } public void Initialize() { _eventCoordinator.Subscribe(OnLevelChanged); _eventCoordinator.Subscribe(OnHintRequested); Initialized = true; } private void OnLevelChanged(LevelChangedEvent evt) { CancelAndDispose(ref _activeHintCts); CancelAndDispose(ref _levelCts); _levelCts = new CancellationTokenSource(); _hints.Clear(); _currentLevel = evt?.Level; if (_currentLevel?.Hints == null) return; foreach (var kvp in _currentLevel.Hints) _hints[kvp.Key] = kvp.Value; } private void OnHintRequested(RequestHintEvent evt) { RequestHint().Forget(); } /// /// Plays the hint for the current hint stage. /// If no hint exists at that stage, it falls back to the nearest lower stage that has a hint. /// public async UniTask RequestHint() { if (_currentLevel == null || _hints.Count == 0) return; var stage = Mathf.Max(0, _currentLevel.CurrentLevelHintStage); if (!TryGetHintForStageOrFallback(stage, out var resolvedStage, out var hint) || hint == null) // Optional: later show a “No hints available” toast return; // Cancel any in-progress hint playback (spam-proof). CancelAndDispose(ref _activeHintCts); _activeHintCts = new CancellationTokenSource(); var levelToken = _levelCts?.Token ?? CancellationToken.None; using var linked = CancellationTokenSource.CreateLinkedTokenSource(levelToken, _activeHintCts.Token); try { await hint.Activate().AttachExternalCancellation(linked.Token); } catch (OperationCanceledException) { // Fine: new request or level changed. } catch (Exception ex) { Debug.LogWarning( $"[HintManager] Hint activation threw at requested stage {stage} (resolved {resolvedStage}): {ex}"); } } private bool TryGetHintForStageOrFallback(int requestedStage, out int resolvedStage, out BaseHint hint) { // Exact stage if (_hints.TryGetValue(requestedStage, out hint) && hint != null) { resolvedStage = requestedStage; return true; } // ✅ Fallback: nearest lower stage that exists // (Designers can skip stages; hint still works.) var best = -1; // If you prefer performance, you can cache/sort keys once per level. foreach (var k in _hints.Keys) if (k <= requestedStage && k > best) best = k; if (best >= 0 && _hints.TryGetValue(best, out hint) && hint != null) { resolvedStage = best; return true; } resolvedStage = -1; hint = null; return false; } private static void CancelAndDispose(ref CancellationTokenSource cts) { if (cts == null) return; try { cts.Cancel(); } catch { /* ignore */ } cts.Dispose(); cts = null; } } }