worldbox-observer-mod/IdleSpectator/InterestCompletion.cs

210 lines
5.3 KiB
C#

using UnityEngine;
namespace IdleSpectator;
/// <summary>Evaluates whether an interest scene is still "live" for protection.</summary>
public static class InterestCompletion
{
public static bool IsActive(InterestCandidate candidate, float sessionStartedAt, float now)
{
if (candidate == null)
{
return false;
}
if (candidate.ForceActive)
{
return true;
}
if (!candidate.HasValidPosition)
{
return false;
}
float age = now - sessionStartedAt;
if (age >= candidate.MaxWatch)
{
return false;
}
switch (candidate.Completion)
{
case InterestCompletionKind.Manual:
return age < candidate.MaxWatch;
case InterestCompletionKind.FixedDwell:
// Hold the authored MaxWatch window (director floors this to minCameraDwell).
return age < Mathf.Max(candidate.MinWatch, candidate.MaxWatch);
case InterestCompletionKind.CombatActive:
return CombatStillActive(candidate);
case InterestCompletionKind.ActivityActive:
return ActivityStillActive(candidate);
case InterestCompletionKind.HappinessGrief:
return GriefStillActive(candidate);
case InterestCompletionKind.StatusPhase:
return StatusStillActive(candidate);
case InterestCompletionKind.CharacterVignette:
return VignetteStillActive(candidate, age);
default:
return age < candidate.MinWatch;
}
}
private static bool CombatStillActive(InterestCandidate c)
{
Actor unit = c.FollowUnit;
if (unit == null || !unit.isAlive())
{
return false;
}
float now = Time.unscaledTime;
bool hot = false;
try
{
if (unit.has_attack_target)
{
hot = true;
}
else if (unit.hasTask() && unit.ai?.task != null
&& (unit.ai.task.in_combat || unit.ai.task.is_fireman))
{
hot = true;
}
else if (RelatedStillFightingUs(c, unit))
{
hot = true;
}
}
catch
{
// ignore
}
if (!hot && c.AssetId == "live_battle")
{
InterestEvent battle = WorldActivityScanner.FindHottestBattle();
hot = battle != null;
}
if (hot)
{
// Refresh so brief attack_target gaps do not clear reason / open quiet grace.
c.LastSeenAt = now;
return true;
}
// Hysteresis: WorldBox clears attack_target between swings.
const float combatHysteresisSeconds = 8f;
return now - c.LastSeenAt < combatHysteresisSeconds;
}
private static bool RelatedStillFightingUs(InterestCandidate c, Actor unit)
{
Actor foe = c.RelatedUnit;
if (foe == null || !foe.isAlive())
{
return false;
}
try
{
if (!foe.has_attack_target || foe.attack_target == null || !foe.attack_target.isAlive())
{
return false;
}
if (!foe.attack_target.isActor())
{
return false;
}
return foe.attack_target.a == unit;
}
catch
{
return false;
}
}
private static bool ActivityStillActive(InterestCandidate c)
{
Actor unit = c.FollowUnit;
if (unit == null || !unit.isAlive())
{
return false;
}
ActivityBand band = ActivityInterestTable.Classify(unit);
return band >= ActivityBand.Warm || unit.has_attack_target;
}
private static bool GriefStillActive(InterestCandidate c)
{
Actor survivor = c.FollowUnit;
if (survivor == null || !survivor.isAlive())
{
return false;
}
if (!string.IsNullOrEmpty(c.StatusId) && HasStatus(survivor, c.StatusId))
{
return true;
}
// Short grief window after the happiness signal.
return Time.unscaledTime - c.LastSeenAt < 8f;
}
private static bool StatusStillActive(InterestCandidate c)
{
Actor unit = c.FollowUnit;
if (unit == null || !unit.isAlive() || string.IsNullOrEmpty(c.StatusId))
{
return false;
}
return HasStatus(unit, c.StatusId);
}
private static bool VignetteStillActive(InterestCandidate c, float age)
{
if (age < c.MinWatch)
{
return true;
}
Actor unit = c.FollowUnit;
if (unit == null || !unit.isAlive())
{
return false;
}
// Stay while still warm; cold ends after min watch.
return !WorldActivityScanner.IsFocusActivityCold(unit);
}
private static bool HasStatus(Actor actor, string statusId)
{
if (actor == null || string.IsNullOrEmpty(statusId))
{
return false;
}
try
{
return actor.hasStatus(statusId);
}
catch
{
return false;
}
}
}