- Add Dossier/Saga tabs, Design A panel, and hover preview with read-pause - Persist observed saga facts across roster churn; pin subject without stale fallback - Restore Saga after Open Lore; hide redundant Kind · Climax spine under matching tips - Densify dossier header/chips and keep the cast rail as stable top chrome - Extend harness coverage for hover restore, empty pick, Evidence, and Lore return
317 lines
7.8 KiB
C#
317 lines
7.8 KiB
C#
using System;
|
|
using UnityEngine;
|
|
|
|
namespace IdleSpectator;
|
|
|
|
/// <summary>
|
|
/// Owns Dossier/Saga tab selection, hover preview, and camera read-pause lease.
|
|
/// </summary>
|
|
public static class LifeSagaViewController
|
|
{
|
|
public const string ReadPauseOwner = "saga_hover";
|
|
public const float HoverExitGraceSeconds = 0.35f;
|
|
|
|
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;
|
|
}
|
|
|
|
/// <summary>Stash Saga return target before opening Lore from the Saga panel.</summary>
|
|
public static void StashLoreReturn(long unitId)
|
|
{
|
|
_loreReturnToSaga = true;
|
|
_loreReturnSagaId = unitId;
|
|
}
|
|
|
|
/// <summary>
|
|
/// After Lore closes, restore Saga tab + subject when a stash is present.
|
|
/// Returns true when a restore was applied.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
_hoverExitAt = Time.unscaledTime + HoverExitGraceSeconds;
|
|
}
|
|
|
|
public static void Tick()
|
|
{
|
|
if (_hoverPreviewId == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Keep preview alive while the pointer stays on the dossier/rail chrome.
|
|
if (WatchCaption.IsPointerOverPanel() || LifeSagaRail.IsPointerOverRail())
|
|
{
|
|
_hoverExitAt = -1f;
|
|
return;
|
|
}
|
|
|
|
// Pointer left chrome after a panel dwell cleared the timer - re-arm grace.
|
|
if (_hoverExitAt < 0f)
|
|
{
|
|
_hoverExitAt = Time.unscaledTime + HoverExitGraceSeconds;
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Harness: simulate Tick clearing the exit timer while the pointer dwells on the panel.
|
|
/// </summary>
|
|
public static void HarnessSimulatePanelDwell()
|
|
{
|
|
if (_hoverPreviewId == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_hoverExitAt = -1f;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Harness: run one hover Tick as if the pointer is off rail and panel chrome.
|
|
/// </summary>
|
|
public static void HarnessTickExitAwayFromChrome()
|
|
{
|
|
if (_hoverPreviewId == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_hoverExitAt < 0f)
|
|
{
|
|
_hoverExitAt = Time.unscaledTime + HoverExitGraceSeconds;
|
|
return;
|
|
}
|
|
|
|
if (Time.unscaledTime < _hoverExitAt)
|
|
{
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Watched MC wins when on roster. Otherwise only an explicitly pinned alive MC.
|
|
/// Never falls back to first-roster / stale last-active filler.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|