using System.Collections.Generic; using UnityEngine; namespace IdleSpectator; /// Cached enrichment for top-K finalists only. public sealed class InterestMetadataSnapshot { public long UnitId; public float CharacterSignificance; public bool Favorite; public bool King; public bool Leader; public int Kills; public int Level; public float Renown; public string SpeciesId = ""; public string CityKey = ""; public string KingdomKey = ""; public ActivityBand Band = ActivityBand.Cold; public float CapturedAt; } /// /// Urgency-class mapping and within-class score breakdown. /// public static class InterestScoring { private static readonly Dictionary MetaCache = new Dictionary(64); private const float MetaCacheTtl = 2f; private const int EnrichTopKLimit = 8; public static void ClearCache() { MetaCache.Clear(); } public static InterestTier UrgencyForHappiness(string effectId, Actor subject) { string id = effectId ?? ""; bool notable = IsNotable(subject); if (id == "death_child" || id == "death_lover") { return notable ? InterestTier.Story : InterestTier.Action; } if (id == "death_best_friend" || id == "death_family_member") { return InterestTier.Action; } if (id == "got_saved" || id == "just_born" || id == "just_hatched" || id == "become_king" || id == "become_city_leader" || id == "become_clan_chief" || id == "just_found_house" || id == "new_home") { return InterestTier.Action; } return InterestTier.Curiosity; } public static float EventStrengthForHappiness(string effectId) { string id = effectId ?? ""; switch (id) { case "death_child": return 95f; case "death_lover": return 88f; case "death_best_friend": return 78f; case "death_family_member": return 76f; case "just_born": case "just_hatched": case "had_child": case "just_had_child": return 70f; case "become_king": case "become_city_leader": case "become_clan_chief": return 72f; case "just_found_house": case "new_home": return 55f; default: return 40f; } } public static InterestTier UrgencyForWorldLog(string assetId) { return WorldLogInterestTable.TryGetTier(assetId, out InterestTier tier) ? tier : InterestTier.Ambient; } public static void EnrichTopK(List candidates) { if (candidates == null || candidates.Count == 0) { return; } candidates.Sort(CompareByUrgencyThenScore); int n = Mathf.Min(EnrichTopKLimit, candidates.Count); float now = Time.unscaledTime; for (int i = 0; i < n; i++) { InterestCandidate c = candidates[i]; if (c?.FollowUnit == null) { continue; } InterestMetadataSnapshot snap = GetOrBuildMeta(c.FollowUnit, now); ApplyMeta(c, snap); RecalcTotal(c); } } public static void ScoreCheap(InterestCandidate c) { if (c == null) { return; } RecalcTotal(c); } public static void RecalcTotal(InterestCandidate c) { if (c == null) { return; } float urgencyWeight = 1f + (int)c.Urgency * 0.15f; if (c.LeadKind == InterestLeadKind.EventLed) { c.TotalScore = c.EventStrength * urgencyWeight + c.CharacterSignificance * 0.35f + c.VisualConfidence * 20f + c.Novelty * 5f; c.ScoreDetail = $"evt={c.EventStrength:0.#} char={c.CharacterSignificance:0.#} vis={c.VisualConfidence:0.##} nov={c.Novelty:0.##}"; } else { c.TotalScore = c.CharacterSignificance * urgencyWeight + c.EventStrength * 0.25f + c.VisualConfidence * 12f + c.Novelty * 5f; c.ScoreDetail = $"char={c.CharacterSignificance:0.#} evt={c.EventStrength:0.#} vis={c.VisualConfidence:0.##} nov={c.Novelty:0.##}"; } } public static InterestMetadataSnapshot GetOrBuildMeta(Actor actor, float now) { if (actor == null) { return null; } long id = 0; try { id = actor.getID(); } catch { return null; } if (MetaCache.TryGetValue(id, out InterestMetadataSnapshot cached) && now - cached.CapturedAt < MetaCacheTtl) { return cached; } var snap = new InterestMetadataSnapshot { UnitId = id, CapturedAt = now, Favorite = SafeBool(() => actor.isFavorite()), King = SafeBool(() => actor.isKing()), Leader = SafeBool(() => actor.isCityLeader()), Kills = actor.data != null ? actor.data.kills : 0, Level = SafeLevel(actor), Renown = SafeFloat(() => actor.renown), SpeciesId = actor.asset != null ? actor.asset.id : "", CityKey = actor.city != null ? (actor.city.name ?? "") : "", KingdomKey = actor.kingdom != null ? (actor.kingdom.name ?? "") : "", Band = ActivityInterestTable.Classify(actor) }; float sig = 0f; if (snap.Favorite) { sig += 22f; } if (snap.King) { sig += 18f; } else if (snap.Leader) { sig += 14f; } sig += Mathf.Min(8f, snap.Renown / 120f); sig += Mathf.Min(12f, snap.Kills * 0.35f); sig += Mathf.Min(10f, snap.Level * 0.4f); snap.CharacterSignificance = sig; MetaCache[id] = snap; return snap; } private static void ApplyMeta(InterestCandidate c, InterestMetadataSnapshot snap) { if (c == null || snap == null) { return; } c.CharacterSignificance = Mathf.Max(c.CharacterSignificance, snap.CharacterSignificance); if (string.IsNullOrEmpty(c.SpeciesId)) { c.SpeciesId = snap.SpeciesId; } if (string.IsNullOrEmpty(c.CityKey)) { c.CityKey = snap.CityKey; } if (string.IsNullOrEmpty(c.KingdomKey)) { c.KingdomKey = snap.KingdomKey; } if (snap.Band >= ActivityBand.Warm) { c.VisualConfidence = Mathf.Max(c.VisualConfidence, 0.65f); } if (snap.Band == ActivityBand.Hot) { c.VisualConfidence = Mathf.Max(c.VisualConfidence, 0.85f); } } public static int CompareByUrgencyThenScore(InterestCandidate a, InterestCandidate b) { if (ReferenceEquals(a, b)) { return 0; } if (a == null) { return 1; } if (b == null) { return -1; } int urg = ((int)b.Urgency).CompareTo((int)a.Urgency); if (urg != 0) { return urg; } int score = b.TotalScore.CompareTo(a.TotalScore); if (score != 0) { return score; } return b.LastSeenAt.CompareTo(a.LastSeenAt); } public static bool IsNotable(Actor actor) { if (actor == null) { return false; } try { if (actor.isFavorite() || actor.isKing() || actor.isCityLeader()) { return true; } } catch { // ignore } return false; } private static bool SafeBool(System.Func fn) { try { return fn(); } catch { return false; } } private static float SafeFloat(System.Func fn) { try { return fn(); } catch { return 0f; } } private static int SafeLevel(Actor actor) { try { return actor.data != null ? actor.data.level : 0; } catch { return 0; } } }