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.
This commit is contained in:
parent
6c6a7cc286
commit
3fe12d6ff0
7 changed files with 190 additions and 16 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")));
|
||||
}
|
||||
|
||||
/// <summary>True when a decision id should be allowed to own the idle camera.</summary>
|
||||
public static bool IsCameraWorthy(string id) => IsKingdomDecision(id);
|
||||
|
||||
private static void Ensure() =>
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True when a drained New species tip would be allowed to take the camera.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Birth/spawn endowment traits that are still worth a camera beat.
|
||||
/// Routine species defaults (poisonous frog, bloodlust beetle, …) are not.
|
||||
/// </summary>
|
||||
private static readonly HashSet<string> LegendaryBirthTraits = new HashSet<string>(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);
|
||||
}
|
||||
|
||||
/// <summary>True while traits are still being stamped onto a brand-new unit.</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue