worldbox-observer-mod/IdleSpectator/Story/LifeSagaViewController.cs
DazedAnon 12e2ba0d01 feat(narrative): refine camera and narrative handling for civilized terminology
- Implement checks for civilized terminology in camera focus events
- Update event handling to suppress inappropriate pack language for civilized characters
- Enhance narrative presentation to differentiate between family and pack language
- Introduce new scenarios to validate significance of beats and relationships
- Improve documentation to clarify narrative structure and event significance
2026-07-24 14:06:23 -05:00

203 lines
5.2 KiB
C#

using System;
using UnityEngine;
namespace IdleSpectator;
/// <summary>
/// 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).
/// </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;
/// <summary>
/// Compatibility enum for harness asserts. AFK chrome never pins Saga;
/// <see cref="EffectiveTab"/> is Saga only while a hover preview is active.
/// </summary>
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;
/// <summary>Saga while hovering a rail glyph; otherwise Dossier (caption body).</summary>
public static Tab EffectiveTab =>
_hoverPreviewId != 0 ? Tab.Saga : Tab.Dossier;
public static long EffectiveSagaId => _hoverPreviewId;
/// <summary>
/// Harness/compat: Dossier cancels hover. Saga begins hover preview of the watched MC
/// when on the roster (no persistent pin / empty pick chrome).
/// </summary>
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();
}
/// <summary>No-op: Prefer click no longer pins a persistent Saga subject.</summary>
public static void SetPersistentSaga(long unitId)
{
_ = unitId;
}
/// <summary>No-op: Lore return no longer restores a Saga tab.</summary>
public static void StashLoreReturn(long unitId)
{
_ = unitId;
}
/// <summary>No-op: Lore close leaves caption chrome as-is.</summary>
public static bool TryRestoreAfterLore()
{
return false;
}
public static void BeginHoverPreview(long unitId)
{
if (unitId == 0)
{
return;
}
_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;
}
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();
}
/// <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;
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;
}
}