using System.Collections.Generic; using UnityEngine; namespace IdleSpectator; public enum InterestLeadKind { EventLed = 0, CharacterLed = 1 } public enum InterestCompletionKind { /// One-shot narrative dwell (WorldLog, harness tips). FixedDwell = 0, /// Combat / hunt / fire while participants stay live. CombatActive = 1, /// Owning AI task / beat still live. ActivityActive = 2, /// Grief / family emotion with survivor + optional crying. HappinessGrief = 3, /// Status phase still present on subject. StatusPhase = 4, /// Character vignette until max watch or cold activity. CharacterVignette = 5, /// Harness-forced session; active until released or max cap. Manual = 6, /// Sticky war front while the war / kingdoms remain resolvable. WarFront = 7, /// Sticky plot cell while the plot / plotters remain resolvable. PlotActive = 8, /// Sticky family pack while members / alpha remain resolvable. FamilyPack = 9, /// Sticky status outbreak while clustered carriers remain resolvable. StatusOutbreak = 10, /// Sticky while Earthquake.isQuakeActive() (ongoing shake poll). EarthquakeActive = 11 } /// /// Durable interest scene candidate. is the sole ranking signal; /// typed fields (lead, completion, combat) gate interrupt policy. /// Multi-actor sticky scoreboard lives on . /// public sealed class InterestCandidate { public string Key = ""; public InterestLeadKind LeadKind = InterestLeadKind.EventLed; public string Category = ""; public string Source = ""; public float EventStrength; public float CharacterSignificance; public float Novelty = 1f; public float VisualConfidence = 0.5f; public float TotalScore; /// Editorial role assigned by the narrative catalog at registry intake. public NarrativeFunction NarrativeFunction; public bool HasNarrativeFunction; public string NarrativeFunctionSource = ""; /// Exact semantic development represented by this candidate, when durable. public string NarrativeDevelopmentId = ""; /// Complete typed orange-reason input. Final English is rendered at presentation. public NarrativePresentationModel NarrativePresentation; /// Live fighters / participants in a cluster (battles, multi-unit events). public int ParticipantCount; /// Sticky multi-actor scoreboard (combat first; war/plot/family later). public readonly LiveSceneStickyState Sticky = new LiveSceneStickyState(); /// Notable (king/favorite/leader/high renown) participants in the cluster. public int NotableParticipantCount; public Vector3 Position; public Actor FollowUnit; public Actor RelatedUnit; public long SubjectId; public long RelatedId; /// /// Durable 1v1 ownership: first Follow stays camera owner while both pair ids live. /// Prevents Duel A↔B thrash across maintain ticks and registry upserts. /// public long PairOwnerId; public long PairPartnerId; /// /// Collective Mass/Battle camera owner: best fighter across both sticky sides. /// Held across attack-target gaps so maintain ticks cannot thrash the follow. /// public long TheaterLeadId; /// Unscaled time the theater lead was last a live combat participant. public float TheaterLeadLastCombatAt = -999f; public string Label = ""; public string AssetId = ""; public string SpeciesId = ""; public string Verb = ""; public string CityKey = ""; public string KingdomKey = ""; public string RegionKey = ""; public string HappinessEffectId = ""; public string StatusId = ""; public string CorrelationKey = ""; public float CreatedAt; public float LastSeenAt; public float ExpiresAt; public float MinWatch = 1.5f; public float MaxWatch = 45f; public InterestCompletionKind Completion = InterestCompletionKind.FixedDwell; public bool Resumable = true; public bool ForceActive; public bool Selected; public string ScoreDetail = ""; public readonly List ParticipantIds = new List(4); // Compat forwarders - existing combat maintain/harness code. public int CombatPeakParticipants { get => Sticky.PeakParticipants; set => Sticky.PeakParticipants = value; } public EnsembleFrame CombatSideFrame { get => Sticky.Frame; set => Sticky.Frame = value; } public string CombatSideAKey { get => Sticky.SideAKey; set => Sticky.SideAKey = value; } public string CombatSideADisplay { get => Sticky.SideADisplay; set => Sticky.SideADisplay = value; } public string CombatSideAKingdom { get => Sticky.SideAKingdom; set => Sticky.SideAKingdom = value; } public int CombatSideACount { get => Sticky.SideACount; set => Sticky.SideACount = value; } public string CombatSideBKey { get => Sticky.SideBKey; set => Sticky.SideBKey = value; } public string CombatSideBDisplay { get => Sticky.SideBDisplay; set => Sticky.SideBDisplay = value; } public string CombatSideBKingdom { get => Sticky.SideBKingdom; set => Sticky.SideBKingdom = value; } public int CombatSideBCount { get => Sticky.SideBCount; set => Sticky.SideBCount = value; } public List CombatSideAIds => Sticky.SideAIds; public List CombatSideBIds => Sticky.SideBIds; public bool HasFollowUnit => FollowUnit != null && FollowUnit.isAlive(); public bool HasValidPosition => HasFollowUnit || (Position != Vector3.zero && !float.IsNaN(Position.x)); public bool HasStickyCombatSides => Sticky.HasOpposingSides; public void ClearCombatSticky() { // Keep TheaterLeadId across sticky rebuilds / tip reframes so Mass focus // does not hop when camps briefly lose opposing-side identity. Sticky.Clear(); } /// /// Lock durable 1v1 owner/partner ids (no-op when owner missing). /// First living partner sticks across attack_target swaps. Replace only when the /// locked partner is confirmed dead and the owner is no longer fighting /// (or ). Lookup misses never unlock the partner. /// public void StampCombatPair(Actor owner, Actor partner, bool replacePartner = false) { long oid = EventFeedUtil.SafeId(owner); if (oid == 0) { return; } if (PairOwnerId == 0) { PairOwnerId = oid; } long pid = EventFeedUtil.SafeId(partner); if (pid == 0 || pid == PairOwnerId) { return; } if (PairPartnerId == 0 || replacePartner) { PairPartnerId = pid; return; } if (PairPartnerId == pid) { return; } // Prefer FindUnitById: FindAliveById null means dead OR miss - misses must not thrash. Actor locked = EventFeedUtil.FindUnitById(PairPartnerId); bool lockedAlive = false; bool lockedKnown = locked != null; try { lockedAlive = locked != null && locked.isAlive(); } catch { // Destroyed Unity object - treat as miss (keep lock). lockedKnown = false; lockedAlive = false; } if (lockedAlive) { return; } if (!lockedKnown) { // Unknown / unloaded / destroyed - keep the first partner id. return; } // Confirmed dead. While the owner is still swinging (spectacle 1vN / scrap mop), // do not chain-adopt the next attack_target into the durable lock. Actor ownerActor = null; try { ownerActor = owner != null && owner.isAlive() ? owner : EventFeedUtil.FindAliveById(PairOwnerId); } catch { ownerActor = EventFeedUtil.FindAliveById(PairOwnerId); } if (ownerActor != null && LiveEnsemble.IsCombatParticipant(ownerActor)) { return; } PairPartnerId = pid; } public bool HasCombatPairLock => PairOwnerId != 0; public void StampTheaterLead(Actor lead) { long id = EventFeedUtil.SafeId(lead); if (id == 0) { return; } // New lead must not inherit the previous unit's attack-gap grace. if (TheaterLeadId != 0 && TheaterLeadId != id) { TheaterLeadLastCombatAt = -999f; } TheaterLeadId = id; } public void ClearTheaterLead() { TheaterLeadId = 0; TheaterLeadLastCombatAt = -999f; } public InterestEvent ToInterestEvent() { return new InterestEvent { Score = TotalScore, Position = Position, FollowUnit = FollowUnit, RelatedUnit = RelatedUnit, Label = Label, CreatedAt = CreatedAt, AssetId = AssetId, ParticipantCount = ParticipantCount, NotableParticipantCount = NotableParticipantCount, CharacterSignificance = CharacterSignificance }; } public InterestCandidate CloneShallow() { var copy = new InterestCandidate { Key = Key, LeadKind = LeadKind, Category = Category, Source = Source, EventStrength = EventStrength, CharacterSignificance = CharacterSignificance, Novelty = Novelty, VisualConfidence = VisualConfidence, TotalScore = TotalScore, NarrativeFunction = NarrativeFunction, HasNarrativeFunction = HasNarrativeFunction, NarrativeFunctionSource = NarrativeFunctionSource, NarrativeDevelopmentId = NarrativeDevelopmentId, NarrativePresentation = NarrativePresentation?.Clone(), ParticipantCount = ParticipantCount, NotableParticipantCount = NotableParticipantCount, Position = Position, FollowUnit = FollowUnit, RelatedUnit = RelatedUnit, SubjectId = SubjectId, RelatedId = RelatedId, PairOwnerId = PairOwnerId, PairPartnerId = PairPartnerId, TheaterLeadId = TheaterLeadId, TheaterLeadLastCombatAt = TheaterLeadLastCombatAt, Label = Label, AssetId = AssetId, SpeciesId = SpeciesId, Verb = Verb, CityKey = CityKey, KingdomKey = KingdomKey, RegionKey = RegionKey, HappinessEffectId = HappinessEffectId, StatusId = StatusId, CorrelationKey = CorrelationKey, CreatedAt = CreatedAt, LastSeenAt = LastSeenAt, ExpiresAt = ExpiresAt, MinWatch = MinWatch, MaxWatch = MaxWatch, Completion = Completion, Resumable = Resumable, ForceActive = ForceActive, Selected = Selected, ScoreDetail = ScoreDetail }; Sticky.CopyTo(copy.Sticky); for (int i = 0; i < ParticipantIds.Count; i++) { copy.ParticipantIds.Add(ParticipantIds[i]); } return copy; } }