using System; using UnityEngine; namespace IdleSpectator; /// /// Owns hover preview and camera read-pause lease for the life-saga rail. /// AFK chrome is always character beat caption (Identity + Beat + Context); /// the saga panel is hover depth only (no Dossier/Saga tabs). /// 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; /// /// Compatibility enum for harness asserts. AFK chrome never pins Saga; /// is Saga only while a hover preview is active. /// public enum Tab { Dossier = 0, Saga = 1 } private static long _hoverPreviewId; private static float _hoverExitAt = -1f; private static bool _pauseHeld; public static Tab SelectedTab => Tab.Dossier; public static long PersistentSagaId => 0; public static long HoverPreviewId => _hoverPreviewId; public static bool IsHoverPreview => _hoverPreviewId != 0; /// Saga while hovering a rail glyph; otherwise Dossier (caption body). public static Tab EffectiveTab => _hoverPreviewId != 0 ? Tab.Saga : Tab.Dossier; public static long EffectiveSagaId => _hoverPreviewId; /// /// Harness/compat: Dossier cancels hover. Saga begins hover preview of the watched MC /// when on the roster (no persistent pin / empty pick chrome). /// public static void SelectTab(Tab tab) { if (tab == Tab.Dossier) { CancelPreviewAndPause(); WatchCaption.RequestRelayout(); return; } long watch = WatchedMcId(); if (watch != 0) { BeginHoverPreview(watch); return; } CancelPreviewAndPause(); WatchCaption.RequestRelayout(); } /// No-op: Prefer click no longer pins a persistent Saga subject. public static void SetPersistentSaga(long unitId) { _ = unitId; } /// No-op: Lore return no longer restores a Saga tab. public static void StashLoreReturn(long unitId) { _ = unitId; } /// No-op: Lore close leaves caption chrome as-is. public static bool TryRestoreAfterLore() { return false; } public static void BeginHoverPreview(long unitId) { if (unitId == 0) { return; } if (_hoverPreviewId == 0) { // Capture the already-painted dossier typography before hover changes ownership // of the header. Every glyph in this hover session uses that same ceiling. WatchCaption.BeginSagaHoverSession(); } _hoverExitAt = -1f; _hoverPreviewId = unitId; WatchCaption.PrimeSagaHoverHeader(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 caption body immediately. if (LifeSagaRail.IsPointerOverGlyph()) { _hoverExitAt = -1f; return; } // PointerExit is delivered before PointerEnter when scrubbing directly from one // glyph to another. Keep Saga ownership alive across that event handoff so the // dossier cannot paint one intermediate header frame. _hoverExitAt = Time.unscaledTime + HoverExitGraceSeconds; } 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() { bool hadPreview = _hoverPreviewId != 0; _hoverExitAt = -1f; _hoverPreviewId = 0; if (hadPreview) { WatchCaption.EndSagaHoverSession(); } if (_pauseHeld) { InterestDirector.ReleaseReadPause(ReadPauseOwner); _pauseHeld = false; } } public static void Clear() { CancelPreviewAndPause(); } /// /// 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; WatchCaption.EndSagaHoverSession(); if (_pauseHeld) { InterestDirector.ReleaseReadPause(ReadPauseOwner); _pauseHeld = false; } WatchCaption.RequestRelayout(); } private static long WatchedMcId() { long watchId = EventFeedUtil.SafeId( MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null); return watchId != 0 && LifeSagaRoster.IsMc(watchId) ? watchId : 0; } }