worldbox-observer-mod/IdleSpectator/InterestDirector.cs
2026-07-14 13:20:49 -05:00

211 lines
6.3 KiB
C#

using NeoModLoader.services;
using UnityEngine;
namespace IdleSpectator;
/// <summary>
/// Picks the next interesting target using dwell, cooldown, and tier interrupts.
/// </summary>
public static class InterestDirector
{
public const float MinDwellSeconds = 10f;
public const float CuriosityDwellSeconds = 6f;
public const float SwitchCooldownSeconds = 5f;
public const float HighTierInterruptAfterSeconds = 3f;
public const float QuietWorldAmbientAfterSeconds = 8f;
public const float AmbientRotateSeconds = 20f;
public const float ActionPollSeconds = 0.75f;
private static InterestEvent _current;
private static float _currentStartedAt = -999f;
private static float _lastSwitchAt = -999f;
private static float _lastInterestingAt = -999f;
private static float _lastAmbientAt = -999f;
private static float _lastActionPollAt = -999f;
private static float _enabledAt = -999f;
private static bool _prevCameraDrag;
private const float DragExitGraceSeconds = 2.5f;
public static void OnSpectatorEnabled()
{
InterestCollector.Clear();
_current = null;
float now = Time.unscaledTime;
_currentStartedAt = now;
_lastSwitchAt = now;
_lastInterestingAt = now;
_lastAmbientAt = -999f;
_lastActionPollAt = -999f;
_enabledAt = now;
_prevCameraDrag = MoveCamera.camera_drag_run;
TryAmbient(force: true);
}
public static void OnSpectatorDisabled()
{
_current = null;
InterestCollector.Clear();
}
public static void Update()
{
if (!SpectatorMode.Active || !Config.game_loaded || World.world == null)
{
return;
}
bool dragging = MoveCamera.camera_drag_run;
bool pastGrace = Time.unscaledTime - _enabledAt >= DragExitGraceSeconds;
if (pastGrace && dragging && !_prevCameraDrag)
{
_prevCameraDrag = dragging;
SpectatorMode.SetActive(false);
WorldTip.showNowTop("Idle Spectator paused (camera drag)", pTranslate: false);
return;
}
_prevCameraDrag = dragging;
float now = Time.unscaledTime;
float onCurrent = now - _currentStartedAt;
float sinceSwitch = now - _lastSwitchAt;
// Periodically surface live battles into the queue (Action tier).
if (now - _lastActionPollAt >= ActionPollSeconds)
{
_lastActionPollAt = now;
InterestEvent battle = WorldActivityScanner.FindHottestBattle();
if (battle != null)
{
InterestCollector.EnqueueDirect(battle);
}
}
if (InterestCollector.TryGetBest(out InterestEvent candidate))
{
if (candidate.HasValidPosition && CanSwitchTo(candidate, onCurrent, sinceSwitch))
{
InterestCollector.Remove(candidate);
if (candidate.Tier >= InterestTier.Action)
{
_lastInterestingAt = now;
}
SwitchTo(candidate, now);
onCurrent = 0f;
sinceSwitch = 0f;
}
else if (!candidate.HasValidPosition)
{
InterestCollector.Remove(candidate);
}
else if (candidate.Tier >= InterestTier.Action)
{
_lastInterestingAt = now;
}
}
bool quiet = now - _lastInterestingAt >= QuietWorldAmbientAfterSeconds;
bool dwellDone = _current == null || onCurrent >= DwellFor(_current);
bool ambientDue = now - _lastAmbientAt >= AmbientRotateSeconds;
if (_current == null && sinceSwitch >= 0.5f)
{
TryAmbient(force: true);
}
else if (quiet && dwellDone && ambientDue && sinceSwitch >= SwitchCooldownSeconds)
{
TryAmbient(force: false);
}
}
private static float DwellFor(InterestEvent current)
{
if (current == null)
{
return MinDwellSeconds;
}
return current.Tier <= InterestTier.Curiosity ? CuriosityDwellSeconds : MinDwellSeconds;
}
private static bool CanSwitchTo(InterestEvent candidate, float onCurrent, float sinceSwitch)
{
if (_current == null)
{
return true;
}
// Curiosity is a footnote: never interrupt Action/Story/Epic, and rarely replace another curiosity.
if (candidate.Tier == InterestTier.Curiosity)
{
if (_current.Tier >= InterestTier.Action)
{
return false;
}
if (_current.Tier == InterestTier.Curiosity && onCurrent < AmbientRotateSeconds)
{
return false;
}
if (_current.Tier == InterestTier.Story)
{
return false;
}
}
if (candidate.Tier > _current.Tier && onCurrent >= HighTierInterruptAfterSeconds)
{
return true;
}
return onCurrent >= DwellFor(_current) && sinceSwitch >= SwitchCooldownSeconds;
}
private static void SwitchTo(InterestEvent next, float now)
{
_current = next;
_currentStartedAt = now;
_lastSwitchAt = now;
if (next.Tier >= InterestTier.Action)
{
_lastInterestingAt = now;
}
CameraDirector.Watch(next);
}
private static void TryAmbient(bool force)
{
float now = Time.unscaledTime;
_lastAmbientAt = now;
InterestEvent ambient = WorldActivityScanner.FindBestLiveTarget();
if (ambient == null)
{
return;
}
if (!force && _current != null && _current.Tier >= InterestTier.Story)
{
return;
}
if (!force && now - _lastSwitchAt < SwitchCooldownSeconds && _current != null)
{
return;
}
// Avoid re-following the exact same unit every ambient tick.
if (!force
&& _current != null
&& _current.FollowUnit != null
&& ambient.FollowUnit != null
&& _current.FollowUnit == ambient.FollowUnit
&& ambient.Tier <= InterestTier.Ambient)
{
return;
}
SwitchTo(ambient, now);
LogService.LogInfo("[IdleSpectator] Ambient: " + ambient.Label);
}
}