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"
}