- Drop Evidence stats and Open Lore; keep lens as a quiet eyebrow - Wrap stake/Cast/Legacy under the portrait; stack Legacy as lines - End glyph hover immediately without panel-dwell keep-alive - Re-apply dossier reason/traits/history on exit so the tip does not lag
301 lines
7.5 KiB
C#
301 lines
7.5 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";
|
|
/// <summary>Only used if an exit was deferred; glyph leave finishes immediately.</summary>
|
|
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;
|
|
}
|
|
|
|
/// <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;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Harness: force-complete hover exit as if the pointer left every glyph.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <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;
|
|
}
|
|
}
|