using UnityEngine; namespace IdleSpectator; /// Evaluates whether an interest scene is still "live" for protection. 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; } } }