From 3fe12d6ff088c99d3ee80903cb03c6aa7f73844c Mon Sep 17 00:00:00 2001 From: DazedAnon Date: Wed, 15 Jul 2026 23:10:30 -0500 Subject: [PATCH] Refactor decision event handling in IdleSpectator to filter camera-worthy events more effectively. Introduce new logic to exclude routine AI ticks and enhance the identification of significant kingdom-affecting decisions. Update event strength handling for birth moments and adjust status interest catalog to prevent non-essential traits from owning the camera. Increment version in mod.json to 0.19.3 to reflect these changes. --- IdleSpectator/DeferredEventPatches.cs | 4 +- IdleSpectator/DeferredLibraryCatalogs.cs | 19 +++++- IdleSpectator/InterestDirector.cs | 52 +++++++++++++++ IdleSpectator/InterestFeeds.cs | 30 ++++++++- IdleSpectator/StatusInterestCatalog.cs | 16 ++--- IdleSpectator/TraitEventPatches.cs | 83 ++++++++++++++++++++++++ IdleSpectator/mod.json | 2 +- 7 files changed, 190 insertions(+), 16 deletions(-) diff --git a/IdleSpectator/DeferredEventPatches.cs b/IdleSpectator/DeferredEventPatches.cs index 8f4b9dc..50c7fee 100644 --- a/IdleSpectator/DeferredEventPatches.cs +++ b/IdleSpectator/DeferredEventPatches.cs @@ -82,8 +82,8 @@ public static class DeferredEventPatches } DiscreteEventEntry entry = DecisionInterestCatalog.GetOrFallback(id); - // High-frequency hook: only emit stronger decisions (strength dial, not Signal gate). - if (entry.EventStrength < InterestScoringConfig.W.noticeScoreMin) + // High-frequency hook: only kingdom-affecting decisions, never idle AI ticks. + if (!DecisionInterestCatalog.IsCameraWorthy(id)) { return; } diff --git a/IdleSpectator/DeferredLibraryCatalogs.cs b/IdleSpectator/DeferredLibraryCatalogs.cs index 6292741..d7a6c78 100644 --- a/IdleSpectator/DeferredLibraryCatalogs.cs +++ b/IdleSpectator/DeferredLibraryCatalogs.cs @@ -111,18 +111,33 @@ public static class DecisionInterestCatalog } string s = id.ToLowerInvariant(); + // Routine AI ticks often include "king" in the name - those are not camera events. + if (s.Contains("idle") + || s.Contains("walking") + || s.Contains("check") + || s.Contains("wait") + || s.Contains("sleep")) + { + return false; + } + return s.Contains("war") || s.Contains("rebellion") || s.Contains("revolt") || s.Contains("alliance") - || s.Contains("king") || s.Contains("coup") || s.Contains("betray") || s.Contains("declare") || s.Contains("invade") - || s.Contains("siege"); + || s.Contains("siege") + || s.Contains("found") + || s.Contains("city_foundation") + || (s.Contains("king") && (s.Contains("war") || s.Contains("city") || s.Contains("rebellion"))); } + /// True when a decision id should be allowed to own the idle camera. + public static bool IsCameraWorthy(string id) => IsKingdomDecision(id); + private static void Ensure() => LiveLibraryInterest.EnsureSeeded( Entries, diff --git a/IdleSpectator/InterestDirector.cs b/IdleSpectator/InterestDirector.cs index 3eb5b2e..5b4dacc 100644 --- a/IdleSpectator/InterestDirector.cs +++ b/IdleSpectator/InterestDirector.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using NeoModLoader.services; using UnityEngine; @@ -486,6 +487,14 @@ public static class InterestDirector if (c == null || !CanSwitchTo(c, onCurrent, sinceSwitch)) { PendingScratch.RemoveAt(i); + continue; + } + + // Drop stale birth/hatch shots - by then the unit is already running around. + if (IsStaleFreshLifeMoment(c, now)) + { + InterestRegistry.Remove(c.Key); + PendingScratch.RemoveAt(i); } } @@ -498,6 +507,49 @@ public static class InterestDirector return InterestVariety.Pick(PendingScratch, _current); } + private static bool IsStaleFreshLifeMoment(InterestCandidate c, float now) + { + if (c == null || string.IsNullOrEmpty(c.HappinessEffectId)) + { + return false; + } + + string id = c.HappinessEffectId; + if (!string.Equals(id, "just_born", StringComparison.OrdinalIgnoreCase) + && !string.Equals(id, "just_got_out_of_egg", StringComparison.OrdinalIgnoreCase) + && !string.Equals(id, "just_hatched", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + // Real-time freshness: if the shot waited in queue more than ~5s, skip it. + if (now - c.CreatedAt > 5.5f) + { + return true; + } + + Actor unit = c.FollowUnit; + if (unit == null || !unit.isAlive()) + { + return true; + } + + try + { + // Already past the newborn beat (playing/moving as a grown unit). + if (!unit.isBaby() && unit.getAge() > 1) + { + return true; + } + } + catch + { + // ignore age API mismatches + } + + return false; + } + /// /// True when a drained New species tip would be allowed to take the camera. /// diff --git a/IdleSpectator/InterestFeeds.cs b/IdleSpectator/InterestFeeds.cs index 3a4772d..4662d62 100644 --- a/IdleSpectator/InterestFeeds.cs +++ b/IdleSpectator/InterestFeeds.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using UnityEngine; @@ -535,6 +536,16 @@ public static class InterestFeeds + ":" + CorrBucket(occ.CorrelationKey); bool grief = occ.EffectId != null && occ.EffectId.StartsWith("death_"); float strength = InterestScoring.EventStrengthForHappiness(occ.EffectId); + bool birthMoment = IsFreshLifeMoment(occ.EffectId); + + // Birth/hatch only matter while still fresh - do not queue behind a long drama reel. + float ttl = birthMoment ? 7f : 35f; + float minWatch = birthMoment ? 2f : 4f; + float maxWatch = birthMoment ? 5.5f : 28f; + if (birthMoment) + { + strength = Mathf.Max(strength, 88f); + } var candidate = new InterestCandidate { @@ -559,9 +570,9 @@ public static class InterestFeeds CorrelationKey = occ.CorrelationKey, CreatedAt = Time.unscaledTime, LastSeenAt = Time.unscaledTime, - ExpiresAt = Time.unscaledTime + 35f, - MinWatch = 4f, - MaxWatch = 28f, + ExpiresAt = Time.unscaledTime + ttl, + MinWatch = minWatch, + MaxWatch = maxWatch, Completion = grief ? InterestCompletionKind.HappinessGrief : InterestCompletionKind.FixedDwell @@ -768,6 +779,19 @@ public static class InterestFeeds } } + private static bool IsFreshLifeMoment(string effectId) + { + if (string.IsNullOrEmpty(effectId)) + { + return false; + } + + string id = effectId.Trim(); + return string.Equals(id, "just_born", StringComparison.OrdinalIgnoreCase) + || string.Equals(id, "just_got_out_of_egg", StringComparison.OrdinalIgnoreCase) + || string.Equals(id, "just_hatched", StringComparison.OrdinalIgnoreCase); + } + private static string MapHappinessCategory(string cat) { switch ((cat ?? "").ToLowerInvariant()) diff --git a/IdleSpectator/StatusInterestCatalog.cs b/IdleSpectator/StatusInterestCatalog.cs index 8be9c34..e832255 100644 --- a/IdleSpectator/StatusInterestCatalog.cs +++ b/IdleSpectator/StatusInterestCatalog.cs @@ -30,7 +30,7 @@ public static class StatusInterestCatalog Add("possessed_follower", 60f, "StatusTransformation", true); Add("frozen", 65f, "StatusTransformation", true); Add("poisoned", 58f, "StatusTransformation", true); - Add("drowning", 68f, "StatusTransformation", true); + Add("drowning", 40f, "StatusTransformation", false); Add("stunned", 50f, "StatusTransformation", true); Add("rage", 62f, "StatusTransformation", true); Add("angry", 48f, "StatusTransformation", true); @@ -38,7 +38,7 @@ public static class StatusInterestCatalog Add("soul_harvested", 75f, "StatusTransformation", true); Add("voices_in_my_head", 55f, "StatusTransformation", true); Add("tantrum", 52f, "StatusTransformation", true); - Add("egg", 58f, "StatusTransformation", true); + Add("egg", 40f, "StatusTransformation", false); Add("magnetized", 55f, "StatusTransformation", true); Add("invincible", 56f, "StatusTransformation", true); Add("powerup", 54f, "StatusTransformation", true); @@ -55,12 +55,12 @@ public static class StatusInterestCatalog Add("pregnant_parthenogenesis", 58f, "LifeChapter", true); Add("crying", 50f, "Grief", true, InterestOwnership.Signal, extendsGrief: true); - // Authored Ambient interest (tracked, fill-only) - Add("festive_spirit", 28f, "StatusAmbient", true, InterestOwnership.Ambient); - Add("laughing", 26f, "StatusAmbient", true, InterestOwnership.Ambient); - Add("singing", 26f, "StatusAmbient", true, InterestOwnership.Ambient); - Add("sleeping", 22f, "StatusAmbient", true, InterestOwnership.Ambient); - Add("handsome_migrant", 30f, "StatusAmbient", true, InterestOwnership.Ambient); + // Authored Ambient - never owns camera (task chip / enrichment only). + Add("festive_spirit", 28f, "StatusAmbient", false, InterestOwnership.Ambient); + Add("laughing", 26f, "StatusAmbient", false, InterestOwnership.Ambient); + Add("singing", 26f, "StatusAmbient", false, InterestOwnership.Ambient); + Add("sleeping", 22f, "StatusAmbient", false, InterestOwnership.Ambient); + Add("handsome_migrant", 30f, "StatusAmbient", false, InterestOwnership.Ambient); // Authored but do not create interest candidates (cooldowns / ambient / brief FX) AddNoInterest("afterglow"); diff --git a/IdleSpectator/TraitEventPatches.cs b/IdleSpectator/TraitEventPatches.cs index 77d2a93..0326dce 100644 --- a/IdleSpectator/TraitEventPatches.cs +++ b/IdleSpectator/TraitEventPatches.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using HarmonyLib; using UnityEngine; @@ -8,6 +9,24 @@ namespace IdleSpectator; [HarmonyPatch] public static class TraitEventPatches { + /// + /// Birth/spawn endowment traits that are still worth a camera beat. + /// Routine species defaults (poisonous frog, bloodlust beetle, …) are not. + /// + private static readonly HashSet LegendaryBirthTraits = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "immortal", + "chosen_one", + "miracle_born", + "miracle_bearer", + "scar_of_divinity", + "giant", + "whirlwind", + "death_nuke", + "death_bomb", + "bomberman" + }; + [HarmonyPatch(typeof(Actor), nameof(Actor.addTrait), new Type[] { typeof(string), typeof(bool) })] [HarmonyPostfix] public static void PostfixAddTraitString(Actor __instance, string pTraitID, bool __result) @@ -99,10 +118,27 @@ public static class TraitEventPatches return; } + // Quiet-world Ambient traits must not steal the camera. + if (entry.EventStrength < InterestScoringConfig.W.noticeScoreMin) + { + return; + } + + // Spawn/birth endowment: only legendary rarities. + if (gained && IsSpawnTraitWindow(actor) && !LegendaryBirthTraits.Contains(entry.Id)) + { + return; + } + float strength = entry.EventStrength; if (!gained) { strength = Mathf.Min(strength, 45f); + // Trait loss is quieter unless it was dramatic. + if (entry.EventStrength < InterestScoringConfig.W.hotScoreMin) + { + return; + } } string label = EventReason.Trait(actor, entry.Id, gained); @@ -119,4 +155,51 @@ public static class TraitEventPatches actor.current_position, actor); } + + /// True while traits are still being stamped onto a brand-new unit. + private static bool IsSpawnTraitWindow(Actor actor) + { + if (actor == null) + { + return true; + } + + try + { + if (actor.isBaby()) + { + return true; + } + } + catch + { + // ignore + } + + try + { + double created = actor.data != null ? actor.data.created_time : 0; + if (created <= 0) + { + return false; + } + + double now = 0; + if (World.world != null) + { + now = World.world.getCurWorldTime(); + } + else if (MapBox.instance != null) + { + now = MapBox.instance.getCurWorldTime(); + } + + // Within ~2 world-years of creation = spawn endowment, not a mid-life gain. + return now > 0 && (now - created) < 2.0; + } + catch + { + return false; + } + } } diff --git a/IdleSpectator/mod.json b/IdleSpectator/mod.json index d9e022b..6e81e74 100644 --- a/IdleSpectator/mod.json +++ b/IdleSpectator/mod.json @@ -1,7 +1,7 @@ { "name": "IdleSpectator", "author": "dazed", - "version": "0.19.2", + "version": "0.19.3", "description": "AFK Idle Spectator (I) + Lore (L). Event-centered scene director + happiness Activity/Life logging.", "GUID": "com.dazed.idlespectator" }