using System; using UnityEngine; namespace IdleSpectator; /// /// Owns Dossier/Saga tab selection, hover preview, and camera read-pause lease. /// public static class LifeSagaViewController { public const string ReadPauseOwner = "saga_hover"; /// Only used if an exit was deferred; glyph leave finishes immediately. public const float HoverExitGraceSeconds = 0.05f; public enum Tab { Dossier = 0, Saga = 1 } private static Tab _selectedTab = Tab.Dossier; private static long _persistentSagaId; private static bool _persistentPinned; private static long _hoverPreviewId; private static Tab _tabBeforeHover = Tab.Dossier; private static float _hoverExitAt = -1f; private static bool _pauseHeld; private static bool _loreReturnToSaga; private static long _loreReturnSagaId; public static Tab SelectedTab => _selectedTab; public static long PersistentSagaId => _persistentSagaId; public static long HoverPreviewId => _hoverPreviewId; public static bool IsHoverPreview => _hoverPreviewId != 0; public static Tab EffectiveTab => _hoverPreviewId != 0 ? Tab.Saga : _selectedTab; public static long EffectiveSagaId { get { if (_hoverPreviewId != 0) { return _hoverPreviewId; } return ResolvePersistentSagaId(); } } public static void SelectTab(Tab tab) { // Explicit tab click wins over hover preview restore. if (_hoverPreviewId != 0 || _pauseHeld) { CancelPreviewAndPause(); _tabBeforeHover = tab; } _selectedTab = tab; if (tab == Tab.Saga) { long watch = WatchedMcId(); if (watch != 0) { _persistentSagaId = watch; _persistentPinned = true; } else if (!IsValidMc(_persistentSagaId) || !_persistentPinned) { _persistentSagaId = 0; _persistentPinned = false; } } WatchCaption.RequestRelayout(); } public static void SetPersistentSaga(long unitId) { if (unitId == 0 || !LifeSagaRoster.IsMc(unitId)) { return; } _persistentSagaId = unitId; _persistentPinned = true; } /// Stash Saga return target before opening Lore from the Saga panel. public static void StashLoreReturn(long unitId) { _loreReturnToSaga = true; _loreReturnSagaId = unitId; } /// /// After Lore closes, restore Saga tab + subject when a stash is present. /// Returns true when a restore was applied. /// public static bool TryRestoreAfterLore() { if (!_loreReturnToSaga) { return false; } long id = _loreReturnSagaId; _loreReturnToSaga = false; _loreReturnSagaId = 0; CancelPreviewAndPause(); _selectedTab = Tab.Saga; _tabBeforeHover = Tab.Saga; if (IsValidMc(id)) { _persistentSagaId = id; _persistentPinned = true; } else { long watch = WatchedMcId(); if (watch != 0) { _persistentSagaId = watch; _persistentPinned = true; } else { _persistentSagaId = 0; _persistentPinned = false; } } WatchCaption.RequestRelayout(); return true; } public static void BeginHoverPreview(long unitId) { if (unitId == 0) { return; } _hoverExitAt = -1f; if (_hoverPreviewId == 0) { _tabBeforeHover = _selectedTab; } _hoverPreviewId = unitId; if (!_pauseHeld) { InterestDirector.AcquireReadPause(ReadPauseOwner); _pauseHeld = true; } WatchCaption.RequestRelayout(); } public static void EndHoverPreview() { if (_hoverPreviewId == 0 && !_pauseHeld) { return; } // Relayout / glyph→glyph can fire Exit while the pointer is still on a glyph. // Keep the preview in that case; otherwise restore Dossier immediately. if (LifeSagaRail.IsPointerOverGlyph()) { _hoverExitAt = -1f; return; } FinishHoverExit(); } public static void Tick() { if (_hoverPreviewId == 0) { return; } // Actively hovering (no exit armed) - keep preview until EndHoverPreview. if (_hoverExitAt < 0f) { return; } // Deferred exit was armed; cancel it if the pointer landed on another glyph. if (LifeSagaRail.IsPointerOverGlyph()) { _hoverExitAt = -1f; return; } if (Time.unscaledTime < _hoverExitAt) { return; } FinishHoverExit(); } public static void CancelPreviewAndPause() { _hoverExitAt = -1f; _hoverPreviewId = 0; if (_pauseHeld) { InterestDirector.ReleaseReadPause(ReadPauseOwner); _pauseHeld = false; } } public static void Clear() { CancelPreviewAndPause(); _selectedTab = Tab.Dossier; _persistentSagaId = 0; _persistentPinned = false; _tabBeforeHover = Tab.Dossier; _loreReturnToSaga = false; _loreReturnSagaId = 0; } /// /// Harness: force-complete hover exit as if the pointer left every glyph. /// public static void HarnessTickExitAwayFromChrome() { if (_hoverPreviewId == 0 && !_pauseHeld) { return; } FinishHoverExit(); } public static LifeSagaViewModel BuildEffectivePresentation() { long id = EffectiveSagaId; return id != 0 ? LifeSagaPresentation.Build(id) : new LifeSagaViewModel(); } private static void FinishHoverExit() { _hoverExitAt = -1f; _hoverPreviewId = 0; _selectedTab = _tabBeforeHover; if (_pauseHeld) { InterestDirector.ReleaseReadPause(ReadPauseOwner); _pauseHeld = false; } WatchCaption.RequestRelayout(); } /// /// Watched MC wins when on roster. Otherwise only an explicitly pinned alive MC. /// Never falls back to first-roster / stale last-active filler. /// private static long ResolvePersistentSagaId() { long watch = WatchedMcId(); if (watch != 0) { _persistentSagaId = watch; _persistentPinned = true; return watch; } if (_persistentPinned && IsValidMc(_persistentSagaId)) { return _persistentSagaId; } _persistentSagaId = 0; _persistentPinned = false; return 0; } private static bool IsValidMc(long unitId) { return unitId != 0 && LifeSagaRoster.IsMc(unitId) && EventFeedUtil.FindAliveById(unitId) != null; } private static long WatchedMcId() { long watchId = EventFeedUtil.SafeId( MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null); return watchId != 0 && LifeSagaRoster.IsMc(watchId) ? watchId : 0; } }