From f4be23e23de4976415f827b691117f913ec32480 Mon Sep 17 00:00:00 2001 From: DazedAnon Date: Thu, 16 Jul 2026 13:49:16 -0500 Subject: [PATCH] Refine event handling in IdleSpectator by introducing new scoring logic for species rarity bonuses and enhancing event catalog entries for relationship and status events. Update presentation tiers for happiness events to ensure accurate representation in gameplay. Increment version in mod.json to 0.24.0 to reflect these changes. --- IdleSpectator/ActivityHappinessHarness.cs | 20 +++- IdleSpectator/ActivityStatusHarness.cs | 9 +- IdleSpectator/AgentHarness.cs | 8 +- .../Events/Catalogs/EventCatalog.Book.cs | 106 +++++++++--------- .../Events/Catalogs/EventCatalog.Happiness.cs | 60 +++++----- .../Events/Catalogs/EventCatalog.Libraries.cs | 32 ++++-- .../Catalogs/EventCatalog.Relationship.cs | 6 +- .../Events/Catalogs/EventCatalog.Status.cs | 65 ++++++----- IdleSpectator/Events/EventReason.cs | 5 + IdleSpectator/Events/Feeds/InterestFeeds.cs | 104 ++++++++++++++++- .../Events/Feeds/RelationshipInterestFeed.cs | 2 + .../Events/Patches/BoatEventPatches.cs | 9 +- .../Events/Patches/BuildingEventPatches.cs | 20 +--- .../Events/Patches/MetaEventPatches.cs | 23 ++++ .../Events/Patches/PlotEventPatches.cs | 26 +++++ .../Patches/RelationshipEventPatches.cs | 16 +++ .../Events/Patches/TraitEventPatches.cs | 7 +- IdleSpectator/HarnessScenarios.cs | 8 ++ IdleSpectator/InterestScoring.cs | 35 ++++++ IdleSpectator/InterestScoringConfig.cs | 5 + IdleSpectator/MutationDiscoveryHarness.cs | 3 + IdleSpectator/mod.json | 4 +- IdleSpectator/scoring-model.json | 3 + docs/event-audit.md | 18 ++- docs/event-prose-audit.md | 7 +- docs/world-event-inventory.md | 91 +++++++++++++++ 26 files changed, 513 insertions(+), 179 deletions(-) create mode 100644 docs/world-event-inventory.md diff --git a/IdleSpectator/ActivityHappinessHarness.cs b/IdleSpectator/ActivityHappinessHarness.cs index ec9ca84..3de7814 100644 --- a/IdleSpectator/ActivityHappinessHarness.cs +++ b/IdleSpectator/ActivityHappinessHarness.cs @@ -211,11 +211,11 @@ public static class ActivityHappinessHarness mustContain: new[] { "hatches from an egg" }, mustNotContain: Array.Empty()); - // Camera demotion: ticker crumbs must stay Ambient (Layer B only). + // Camera demotion: only true FX / bookkeeping stay Ambient (Layer B). + // Interesting life/social beats are Signal; director ranks by EventStrength. string[] ambientRequired = { - "just_received_gift", "just_gave_gift", "just_cried", "just_inspired", - "got_robbed", "lost_fight", "just_injured", "just_made_friend" + "got_poked", "got_caught", "paid_tax", "just_ate", "just_pooped" }; for (int i = 0; i < ambientRequired.Length; i++) { @@ -226,6 +226,20 @@ public static class ActivityHappinessHarness } } + string[] signalRequired = + { + "just_made_friend", "just_injured", "just_found_house", "got_robbed", + "fallen_in_love", "become_alpha", "just_had_child" + }; + for (int i = 0; i < signalRequired.Length; i++) + { + HappinessCatalogEntry e = EventCatalog.Happiness.GetOrFallback(signalRequired[i]); + if (e.Presentation != HappinessPresentationTier.Signal) + { + proseRegressions++; + } + } + bool passed = missingAuthored == 0 && missingIcon == 0 && localeLeaks == 0 diff --git a/IdleSpectator/ActivityStatusHarness.cs b/IdleSpectator/ActivityStatusHarness.cs index 1ab3577..13b7936 100644 --- a/IdleSpectator/ActivityStatusHarness.cs +++ b/IdleSpectator/ActivityStatusHarness.cs @@ -185,10 +185,12 @@ public static class ActivityStatusHarness } int cameraDemotionFailures = 0; + // Brief FX / cooldowns must stay B. Story-readable statuses must stay A. string[] bOnlyStatus = { - "surprised", "confused", "motivated", "inspired", "magnetized", - "starving", "ash_fever", "powerup", "voices_in_my_head", "angry" + "afterglow", "cough", "dash", "dodge", "flicked", "just_ate", "on_guard", + "recovery_combat_action", "recovery_plot", "recovery_social", "recovery_spell", + "shield", "slowness", "spell_boost", "spell_silence", "egg" }; for (int i = 0; i < bOnlyStatus.Length; i++) { @@ -202,7 +204,8 @@ public static class ActivityStatusHarness string[] aStatus = { "drowning", "burning", "frozen", "poisoned", "possessed", "cursed", - "pregnant", "crying", "rage", "stunned", "soul_harvested", "tantrum" + "pregnant", "crying", "rage", "stunned", "soul_harvested", "tantrum", + "fell_in_love", "starving", "angry", "sleeping", "inspired", "ash_fever" }; for (int i = 0; i < aStatus.Length; i++) { diff --git a/IdleSpectator/AgentHarness.cs b/IdleSpectator/AgentHarness.cs index f276c62..2a36b95 100644 --- a/IdleSpectator/AgentHarness.cs +++ b/IdleSpectator/AgentHarness.cs @@ -2747,10 +2747,10 @@ public static class AgentHarness source = "boat"; break; case "building": - id = "building_damage"; - key = "building:building_damage:" + EventFeedUtil.SafeId(unit); - label = "Damages a building: " + EventFeedUtil.SafeName(unit); - strength = 70f; + id = "building_destroy"; + key = "building:destroy:harness:" + EventFeedUtil.SafeId(unit); + label = EventReason.BuildingFalls("harness_hall", unit); + strength = 88f; category = "Spectacle"; source = "building"; break; diff --git a/IdleSpectator/Events/Catalogs/EventCatalog.Book.cs b/IdleSpectator/Events/Catalogs/EventCatalog.Book.cs index d077d5a..1b15e43 100644 --- a/IdleSpectator/Events/Catalogs/EventCatalog.Book.cs +++ b/IdleSpectator/Events/Catalogs/EventCatalog.Book.cs @@ -7,63 +7,65 @@ namespace IdleSpectator; public static partial class EventCatalog { public static class Book -{ - private static readonly Dictionary Entries = - new Dictionary(StringComparer.OrdinalIgnoreCase); - - static Book() { - Add("family_story", 52f, "Culture", "{a} writes a family story"); - Add("love_story", 58f, "Culture", "{a} writes a love story"); - Add("friendship_story", 50f, "Culture", "{a} writes a friendship story"); - Add("bad_story_about_king", 62f, "Culture", "{a} writes a scandalous book"); - Add("fable", 48f, "Culture", "{a} writes a fable"); - Add("warfare_manual", 60f, "Culture", "{a} writes a warfare manual"); - Add("economy_manual", 48f, "Culture", "{a} writes an economy manual"); - Add("stewardship_manual", 48f, "Culture", "{a} writes a stewardship manual"); - Add("diplomacy_manual", 55f, "Culture", "{a} writes a diplomacy manual"); - Add("mathbook", 45f, "Culture", "{a} writes a math book"); - Add("biology_book", 45f, "Culture", "{a} writes a biology book"); - Add("history_book", 55f, "Culture", "{a} writes a history book"); - } + private static readonly Dictionary Entries = + new Dictionary(StringComparer.OrdinalIgnoreCase); - private static void Add( - string id, - float strength, - string category, - string label) - { - Entries[id] = new DiscreteEventEntry + static Book() { - Id = id, - EventStrength = strength, - Category = category, - CreatesInterest = true, - LabelTemplate = label - }; - } - - public static IEnumerable AuthoredIds => Entries.Keys; - - public static bool HasAuthored(string id) => - !string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim()); - - public static DiscreteEventEntry GetOrFallback(string id) - { - if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry)) - { - return entry; + // All authored books are Layer A; EventStrength ranks them. + Add("family_story", 52f, "Culture", "{a} writes a family story"); + Add("love_story", 58f, "Culture", "{a} writes a love story"); + Add("friendship_story", 50f, "Culture", "{a} writes a friendship story"); + Add("bad_story_about_king", 62f, "Culture", "{a} writes a scandalous book"); + Add("fable", 48f, "Culture", "{a} writes a fable"); + Add("warfare_manual", 60f, "Culture", "{a} writes a warfare manual"); + Add("economy_manual", 48f, "Culture", "{a} writes an economy manual"); + Add("stewardship_manual", 48f, "Culture", "{a} writes a stewardship manual"); + Add("diplomacy_manual", 55f, "Culture", "{a} writes a diplomacy manual"); + Add("mathbook", 45f, "Culture", "{a} writes a math book"); + Add("biology_book", 45f, "Culture", "{a} writes a biology book"); + Add("history_book", 55f, "Culture", "{a} writes a history book"); } - string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim(); - return new DiscreteEventEntry + private static void Add( + string id, + float strength, + string category, + string label) { - Id = key, - EventStrength = 45f, - Category = "Culture", - LabelTemplate = "{a} writes a book", - IsFallback = true - }; + Entries[id] = new DiscreteEventEntry + { + Id = id, + EventStrength = strength, + Category = category, + CreatesInterest = true, + LabelTemplate = label + }; + } + + public static IEnumerable AuthoredIds => Entries.Keys; + + public static bool HasAuthored(string id) => + !string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim()); + + public static DiscreteEventEntry GetOrFallback(string id) + { + if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry)) + { + return entry; + } + + string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim(); + return new DiscreteEventEntry + { + Id = key, + EventStrength = 45f, + Category = "Culture", + LabelTemplate = "{a} writes a book", + CreatesInterest = true, + IsFallback = true + }; + } } } -} diff --git a/IdleSpectator/Events/Catalogs/EventCatalog.Happiness.cs b/IdleSpectator/Events/Catalogs/EventCatalog.Happiness.cs index 4c98347..c926608 100644 --- a/IdleSpectator/Events/Catalogs/EventCatalog.Happiness.cs +++ b/IdleSpectator/Events/Catalogs/EventCatalog.Happiness.cs @@ -67,7 +67,7 @@ public static partial class EventCatalog { Id = "got_robbed", EventStrength = 50f, - Presentation = HappinessPresentationTier.Ambient, + Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.RelatedOptional, Life = HappinessLifePolicy.ActivityOnly, Overlap = HappinessOverlapPolicy.Independent, @@ -93,7 +93,7 @@ public static partial class EventCatalog { Id = "lost_fight", EventStrength = 55f, - Presentation = HappinessPresentationTier.Ambient, + Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, Overlap = HappinessOverlapPolicy.Independent, @@ -147,7 +147,7 @@ public static partial class EventCatalog { Id = "just_received_gift", EventStrength = 50f, - Presentation = HappinessPresentationTier.Ambient, + Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.RelatedOptional, Life = HappinessLifePolicy.ActivityOnly, Overlap = HappinessOverlapPolicy.Independent, @@ -160,7 +160,7 @@ public static partial class EventCatalog { Id = "just_gave_gift", EventStrength = 50f, - Presentation = HappinessPresentationTier.Ambient, + Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.RelatedOptional, Life = HappinessLifePolicy.ActivityOnly, Overlap = HappinessOverlapPolicy.Independent, @@ -186,7 +186,7 @@ public static partial class EventCatalog { Id = "just_slept", EventStrength = 30f, - Presentation = HappinessPresentationTier.Ambient, + Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, Overlap = HappinessOverlapPolicy.Independent, @@ -199,7 +199,7 @@ public static partial class EventCatalog { Id = "had_bad_dream", EventStrength = 30f, - Presentation = HappinessPresentationTier.Ambient, + Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, Overlap = HappinessOverlapPolicy.Continuation, @@ -213,7 +213,7 @@ public static partial class EventCatalog { Id = "had_good_dream", EventStrength = 30f, - Presentation = HappinessPresentationTier.Ambient, + Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, Overlap = HappinessOverlapPolicy.Continuation, @@ -227,7 +227,7 @@ public static partial class EventCatalog { Id = "had_nightmare", EventStrength = 30f, - Presentation = HappinessPresentationTier.Ambient, + Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, Overlap = HappinessOverlapPolicy.Continuation, @@ -241,7 +241,7 @@ public static partial class EventCatalog { Id = "slept_outside", EventStrength = 30f, - Presentation = HappinessPresentationTier.Ambient, + Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, Overlap = HappinessOverlapPolicy.Independent, @@ -429,7 +429,7 @@ public static partial class EventCatalog { Id = "just_read_book", EventStrength = 30f, - Presentation = HappinessPresentationTier.Ambient, + Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, Overlap = HappinessOverlapPolicy.Independent, @@ -442,7 +442,7 @@ public static partial class EventCatalog { Id = "just_played", EventStrength = 30f, - Presentation = HappinessPresentationTier.Ambient, + Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, Overlap = HappinessOverlapPolicy.Independent, @@ -455,7 +455,7 @@ public static partial class EventCatalog { Id = "just_talked", EventStrength = 30f, - Presentation = HappinessPresentationTier.Ambient, + Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.RelatedOptional, Life = HappinessLifePolicy.ActivityOnly, Overlap = HappinessOverlapPolicy.Independent, @@ -468,7 +468,7 @@ public static partial class EventCatalog { Id = "just_laughed", EventStrength = 30f, - Presentation = HappinessPresentationTier.Ambient, + Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, Overlap = HappinessOverlapPolicy.Continuation, @@ -482,7 +482,7 @@ public static partial class EventCatalog { Id = "just_sang", EventStrength = 30f, - Presentation = HappinessPresentationTier.Ambient, + Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, Overlap = HappinessOverlapPolicy.Continuation, @@ -496,7 +496,7 @@ public static partial class EventCatalog { Id = "just_swore", EventStrength = 30f, - Presentation = HappinessPresentationTier.Ambient, + Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, Overlap = HappinessOverlapPolicy.Continuation, @@ -510,7 +510,7 @@ public static partial class EventCatalog { Id = "just_cried", EventStrength = 30f, - Presentation = HappinessPresentationTier.Ambient, + Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, Overlap = HappinessOverlapPolicy.Continuation, @@ -524,7 +524,7 @@ public static partial class EventCatalog { Id = "just_talked_gossip", EventStrength = 30f, - Presentation = HappinessPresentationTier.Ambient, + Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.RelatedOptional, Life = HappinessLifePolicy.ActivityOnly, Overlap = HappinessOverlapPolicy.Independent, @@ -537,7 +537,7 @@ public static partial class EventCatalog { Id = "just_surprised", EventStrength = 30f, - Presentation = HappinessPresentationTier.Ambient, + Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, Overlap = HappinessOverlapPolicy.Continuation, @@ -566,7 +566,7 @@ public static partial class EventCatalog // Game id uses British spelling; status asset is magnetized. Id = "just_magnetised", EventStrength = 45f, - Presentation = HappinessPresentationTier.Ambient, + Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, Overlap = HappinessOverlapPolicy.Continuation, @@ -580,7 +580,7 @@ public static partial class EventCatalog { Id = "just_forced_power", EventStrength = 45f, - Presentation = HappinessPresentationTier.Ambient, + Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, Overlap = HappinessOverlapPolicy.Independent, @@ -607,7 +607,7 @@ public static partial class EventCatalog { Id = "strange_urge", EventStrength = 45f, - Presentation = HappinessPresentationTier.Ambient, + Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, Overlap = HappinessOverlapPolicy.Continuation, @@ -621,7 +621,7 @@ public static partial class EventCatalog { Id = "just_had_tantrum", EventStrength = 45f, - Presentation = HappinessPresentationTier.Ambient, + Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, Overlap = HappinessOverlapPolicy.Continuation, @@ -635,7 +635,7 @@ public static partial class EventCatalog { Id = "just_felt_the_divine", EventStrength = 45f, - Presentation = HappinessPresentationTier.Ambient, + Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, Overlap = HappinessOverlapPolicy.Independent, @@ -648,7 +648,7 @@ public static partial class EventCatalog { Id = "just_enchanted", EventStrength = 45f, - Presentation = HappinessPresentationTier.Ambient, + Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, Overlap = HappinessOverlapPolicy.Continuation, @@ -662,7 +662,7 @@ public static partial class EventCatalog { Id = "just_inspired", EventStrength = 45f, - Presentation = HappinessPresentationTier.Ambient, + Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, Overlap = HappinessOverlapPolicy.Continuation, @@ -729,7 +729,7 @@ public static partial class EventCatalog { Id = "just_found_house", EventStrength = 55f, - Presentation = HappinessPresentationTier.Ambient, + Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ProjectToLife, Overlap = HappinessOverlapPolicy.Independent, @@ -742,7 +742,7 @@ public static partial class EventCatalog { Id = "just_lost_house", EventStrength = 65f, - Presentation = HappinessPresentationTier.Ambient, + Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ProjectToLife, Overlap = HappinessOverlapPolicy.Independent, @@ -755,7 +755,7 @@ public static partial class EventCatalog { Id = "just_made_friend", EventStrength = 50f, - Presentation = HappinessPresentationTier.Ambient, + Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.RelatedOptional, Life = HappinessLifePolicy.ActivityOnly, Overlap = HappinessOverlapPolicy.CanonicalMilestone, @@ -769,7 +769,7 @@ public static partial class EventCatalog { Id = "just_injured", EventStrength = 65f, - Presentation = HappinessPresentationTier.Ambient, + Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ProjectToLife, Overlap = HappinessOverlapPolicy.Independent, @@ -796,7 +796,7 @@ public static partial class EventCatalog { Id = "starving", EventStrength = 30f, - Presentation = HappinessPresentationTier.Ambient, + Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, Overlap = HappinessOverlapPolicy.Continuation, diff --git a/IdleSpectator/Events/Catalogs/EventCatalog.Libraries.cs b/IdleSpectator/Events/Catalogs/EventCatalog.Libraries.cs index c701d75..8242671 100644 --- a/IdleSpectator/Events/Catalogs/EventCatalog.Libraries.cs +++ b/IdleSpectator/Events/Catalogs/EventCatalog.Libraries.cs @@ -26,7 +26,7 @@ public static partial class EventCatalog ambientStrength: 48f, SignalIds, signalStrength: 88f, - ambientCreatesInterest: false); + ambientCreatesInterest: true); public static IEnumerable AuthoredIds { @@ -115,12 +115,13 @@ public static partial class EventCatalog } string s = id.ToLowerInvariant(); - // Routine AI ticks often include "king" in the name - those are not camera events. + // Pure AI ticks - not story beats. if (s.Contains("idle") || s.Contains("walking") || s.Contains("check") || s.Contains("wait") - || s.Contains("sleep")) + || s.Contains("sleep") + || s.Contains("random")) { return false; } @@ -136,7 +137,17 @@ public static partial class EventCatalog || s.Contains("siege") || s.Contains("found") || s.Contains("city_foundation") - || (s.Contains("king") && (s.Contains("war") || s.Contains("city") || s.Contains("rebellion"))); + || s.Contains("army") + || s.Contains("lover") + || s.Contains("plot") + || s.Contains("marry") + || s.Contains("baby") + || s.Contains("child") + || s.Contains("king") + || s.Contains("leader") + || s.Contains("clan") + || s.Contains("religion") + || s.Contains("culture"); } /// True when a decision id should be allowed to own the idle camera. @@ -148,7 +159,7 @@ public static partial class EventCatalog ActivityAssetCatalog.EnumerateLiveDecisionIds, "Politics", "{a} decides {id}", - ambientStrength: 32f, + ambientStrength: 40f, signalIds: null, signalStrength: 86f, signalPredicate: IsKingdomDecision, @@ -206,7 +217,7 @@ public static partial class EventCatalog signalIds: null, signalStrength: 80f, signalPredicate: IsLegendaryItem, - ambientCreatesInterest: false); + ambientCreatesInterest: true); public static IEnumerable AuthoredIds { @@ -263,7 +274,8 @@ public static partial class EventCatalog ambientStrength: 34f, signalIds: null, signalStrength: 78f, - signalPredicate: IsDramatic); + signalPredicate: IsDramatic, + ambientCreatesInterest: true); public static IEnumerable AuthoredIds { @@ -297,7 +309,7 @@ public static partial class EventCatalog signalIds: null, signalStrength: 70f, signalPredicate: id => id != null && (id.Contains("warfare") || id.Contains("magic") || id.Contains("mutation")), - ambientCreatesInterest: false); + ambientCreatesInterest: true); public static IEnumerable AuthoredIds { @@ -330,7 +342,7 @@ public static partial class EventCatalog ambientStrength: 28f, signalIds: null, signalStrength: 60f, - ambientCreatesInterest: false); + ambientCreatesInterest: true); public static IEnumerable AuthoredIds { @@ -364,7 +376,7 @@ public static partial class EventCatalog signalIds: null, signalStrength: 70f, signalPredicate: id => id != null && (id.Contains("war") || id.Contains("death") || id.Contains("plague")), - ambientCreatesInterest: false); + ambientCreatesInterest: true); public static IEnumerable AuthoredIds { diff --git a/IdleSpectator/Events/Catalogs/EventCatalog.Relationship.cs b/IdleSpectator/Events/Catalogs/EventCatalog.Relationship.cs index 8d2dbab..012fac1 100644 --- a/IdleSpectator/Events/Catalogs/EventCatalog.Relationship.cs +++ b/IdleSpectator/Events/Catalogs/EventCatalog.Relationship.cs @@ -18,10 +18,12 @@ public static partial class EventCatalog Add("add_child", 68f, "Relationship", "{a} gains a child, {b}"); Add("new_family", 70f, "Relationship", "{a} starts a new family"); Add("family_removed", 50f, "Relationship", "{a}'s family ends"); + // Seeking / pack churn are still candidates; low strength lets the director bury them. Add("find_lover", 45f, "Relationship", "{a} is seeking a lover"); + Add("become_alpha", 72f, "LifeChapter", "{a} becomes the family alpha"); Add("family_group_new", 60f, "Relationship", "{a} forms a family pack"); - Add("family_group_join", 36f, "Relationship", "{a} joins a family pack", createsInterest: false); - Add("family_group_leave", 36f, "Relationship", "{a} leaves a family pack", createsInterest: false); + Add("family_group_join", 36f, "Relationship", "{a} joins a family pack"); + Add("family_group_leave", 36f, "Relationship", "{a} leaves a family pack"); Add("baby_created", 70f, "Relationship", "{a} is created as a baby"); } diff --git a/IdleSpectator/Events/Catalogs/EventCatalog.Status.cs b/IdleSpectator/Events/Catalogs/EventCatalog.Status.cs index b4f782c..2ce2f86 100644 --- a/IdleSpectator/Events/Catalogs/EventCatalog.Status.cs +++ b/IdleSpectator/Events/Catalogs/EventCatalog.Status.cs @@ -15,6 +15,7 @@ public sealed class StatusInterestEntry /// /// Authored interest policy for every live status asset. Prose stays in ActivityStatusProse. +/// Default: story-readable statuses are Layer A. Only brief FX / cooldowns stay B-only. /// public static partial class EventCatalog { @@ -25,7 +26,7 @@ public static partial class EventCatalog static Status() { - // Spectacle / camera-worthy transformations + // Spectacles / crisis Add("burning", 70f, "StatusTransformation", true); Add("possessed", 72f, "StatusTransformation", true); Add("possessed_follower", 60f, "StatusTransformation", true); @@ -34,47 +35,52 @@ public static partial class EventCatalog Add("drowning", 50f, "StatusTransformation", true); Add("stunned", 50f, "StatusTransformation", true); Add("rage", 62f, "StatusTransformation", true); - Add("angry", 48f, "StatusAmbient", false); + Add("angry", 48f, "StatusAmbient", true); Add("cursed", 60f, "StatusTransformation", true); Add("soul_harvested", 75f, "StatusTransformation", true); - Add("voices_in_my_head", 55f, "StatusAmbient", false); + Add("voices_in_my_head", 55f, "StatusAmbient", true); Add("tantrum", 52f, "StatusTransformation", true); - Add("egg", 40f, "StatusTransformation", false); - Add("magnetized", 55f, "StatusAmbient", false); - Add("invincible", 40f, "StatusTransformation", false); - Add("powerup", 54f, "StatusAmbient", false); + Add("magnetized", 55f, "StatusAmbient", true); + Add("invincible", 50f, "StatusTransformation", true); + Add("powerup", 54f, "StatusAmbient", true); Add("enchanted", 52f, "StatusTransformation", true); - Add("ash_fever", 50f, "StatusAmbient", false); - Add("starving", 48f, "StatusAmbient", false); - Add("surprised", 42f, "StatusAmbient", false); - Add("confused", 40f, "StatusAmbient", false); - Add("strange_urge", 45f, "StatusAmbient", false); - Add("inspired", 42f, "Emotion", false); - Add("motivated", 40f, "Emotion", false); - Add("fell_in_love", 42f, "Relationship", false); + Add("ash_fever", 50f, "StatusAmbient", true); + Add("starving", 52f, "StatusAmbient", true); + Add("surprised", 42f, "StatusAmbient", true); + Add("confused", 40f, "StatusAmbient", true); + Add("strange_urge", 45f, "StatusAmbient", true); + Add("inspired", 42f, "Emotion", true); + Add("motivated", 40f, "Emotion", true); + Add("fell_in_love", 58f, "Relationship", true); Add("pregnant", 58f, "LifeChapter", true); Add("pregnant_parthenogenesis", 58f, "LifeChapter", true); Add("crying", 50f, "Grief", true, extendsGrief: true); - // Authored Ambient - never owns camera (task chip / enrichment only). - Add("festive_spirit", 28f, "StatusAmbient", false); - Add("laughing", 26f, "StatusAmbient", false); - Add("singing", 26f, "StatusAmbient", false); - Add("sleeping", 22f, "StatusAmbient", false); - Add("handsome_migrant", 30f, "StatusAmbient", false); + // Life / social readable beats (lower strength; still camera-eligible) + Add("festive_spirit", 36f, "StatusAmbient", true); + Add("laughing", 34f, "StatusAmbient", true); + Add("singing", 34f, "StatusAmbient", true); + Add("sleeping", 32f, "StatusAmbient", true); + Add("handsome_migrant", 44f, "StatusAmbient", true); + Add("being_suspicious", 40f, "StatusAmbient", true); + Add("budding", 48f, "LifeChapter", true); + Add("caffeinated", 36f, "StatusAmbient", true); + Add("had_bad_dream", 40f, "Emotion", true); + Add("had_good_dream", 38f, "Emotion", true); + Add("had_nightmare", 44f, "Emotion", true); + Add("swearing", 34f, "StatusAmbient", true); + Add("taking_roots", 46f, "LifeChapter", true); + Add("uprooting", 46f, "LifeChapter", true); - // Authored but do not create interest candidates (cooldowns / ambient / brief FX) + // Egg gain is becoming an egg (not a camera beat). Loss → EmitHatch. + Add("egg", 40f, "StatusTransformation", false); + + // Brief FX / combat cooldowns - ticker only (Layer B). AddNoInterest("afterglow"); - AddNoInterest("being_suspicious"); - AddNoInterest("budding"); - AddNoInterest("caffeinated"); AddNoInterest("cough"); AddNoInterest("dash"); AddNoInterest("dodge"); AddNoInterest("flicked"); - AddNoInterest("had_bad_dream"); - AddNoInterest("had_good_dream"); - AddNoInterest("had_nightmare"); AddNoInterest("just_ate"); AddNoInterest("on_guard"); AddNoInterest("recovery_combat_action"); @@ -85,9 +91,6 @@ public static partial class EventCatalog AddNoInterest("slowness"); AddNoInterest("spell_boost"); AddNoInterest("spell_silence"); - AddNoInterest("swearing"); - AddNoInterest("taking_roots"); - AddNoInterest("uprooting"); } private static void Add( diff --git a/IdleSpectator/Events/EventReason.cs b/IdleSpectator/Events/EventReason.cs index f9edb07..2857de3 100644 --- a/IdleSpectator/Events/EventReason.cs +++ b/IdleSpectator/Events/EventReason.cs @@ -50,6 +50,11 @@ public static class EventReason return NameOrSomeone(a) + " is seeking a lover"; } + public static string BecomeAlpha(Actor a) + { + return NameOrSomeone(a) + " becomes the family alpha"; + } + public static string Hatch(Actor a) { return NameOrSomeone(a) + " hatches from an egg"; diff --git a/IdleSpectator/Events/Feeds/InterestFeeds.cs b/IdleSpectator/Events/Feeds/InterestFeeds.cs index ab75c2c..248af5d 100644 --- a/IdleSpectator/Events/Feeds/InterestFeeds.cs +++ b/IdleSpectator/Events/Feeds/InterestFeeds.cs @@ -479,6 +479,67 @@ public static class InterestFeeds private static string HatchKey(long subjectId) => "hatch:" + subjectId; + private static string AlphaKey(long subjectId) => "alpha:" + subjectId; + + /// + /// Family alpha succession. Shared key with happiness become_alpha so emotion and + /// emotion-less paths merge into one camera beat. + /// + public static void EmitAlpha(Actor actor, string source) + { + if (actor == null || !actor.isAlive()) + { + return; + } + + if (AgentHarness.Busy) + { + return; + } + + long id = SafeId(actor); + if (id == 0) + { + return; + } + + DiscreteEventEntry entry = EventCatalog.Relationship.GetOrFallback("become_alpha"); + if (!EventCatalog.IsCameraWorthy(entry)) + { + InterestDropLog.Record("createsInterest=false", "become_alpha"); + return; + } + + float strength = Mathf.Max( + entry.EventStrength, + InterestScoring.EventStrengthForHappiness("become_alpha")); + var candidate = new InterestCandidate + { + Key = AlphaKey(id), + LeadKind = InterestLeadKind.EventLed, + Category = string.IsNullOrEmpty(entry.Category) ? "LifeChapter" : entry.Category, + Source = string.IsNullOrEmpty(source) ? "alpha" : source, + EventStrength = strength, + VisualConfidence = 0.8f, + Position = actor.current_position, + FollowUnit = actor, + SubjectId = id, + Label = EventReason.BecomeAlpha(actor), + AssetId = SafeAsset(actor), + SpeciesId = SafeAsset(actor), + HappinessEffectId = "become_alpha", + CreatedAt = Time.unscaledTime, + LastSeenAt = Time.unscaledTime, + ExpiresAt = Time.unscaledTime + 25f, + MinWatch = 5f, + MaxWatch = 22f, + Completion = InterestCompletionKind.FixedDwell + }; + candidate.ParticipantIds.Add(id); + InterestScoring.ScoreCheap(candidate); + EventFeedUtil.RegisterCandidate(candidate); + } + public static void OnChronicleMilestone(Actor subject, string milestoneKey, Actor related, string label) { if (subject == null || !subject.isAlive() || string.IsNullOrEmpty(milestoneKey)) @@ -614,12 +675,23 @@ public static class InterestFeeds bool grief = occ.EffectId != null && occ.EffectId.StartsWith("death_"); float strength = InterestScoring.EventStrengthForHappiness(occ.EffectId); bool hatchMoment = IsFreshLifeMoment(occ.EffectId); + bool alphaMoment = IsAlphaMoment(occ.EffectId); - // Hatch shares a key with status egg-loss so both paths merge into one camera beat. - string key = hatchMoment - ? HatchKey(occ.SubjectId) - : ("happiness:" + occ.EffectId + ":" + occ.SubjectId + ":" + occ.RelatedId - + ":" + CorrBucket(occ.CorrelationKey)); + // Hatch / alpha share keys with status-loss / Family.setAlpha so paths merge. + string key; + if (hatchMoment) + { + key = HatchKey(occ.SubjectId); + } + else if (alphaMoment) + { + key = AlphaKey(occ.SubjectId); + } + else + { + key = "happiness:" + occ.EffectId + ":" + occ.SubjectId + ":" + occ.RelatedId + + ":" + CorrBucket(occ.CorrelationKey); + } // Hatch: short queue TTL, but on-camera floor comes from minCameraDwell. float ttl = hatchMoment ? 7f : 35f; @@ -630,11 +702,23 @@ public static class InterestFeeds strength = Mathf.Max(strength, 88f); } + if (alphaMoment) + { + strength = Mathf.Max(strength, EventCatalog.Relationship.GetOrFallback("become_alpha").EventStrength); + ttl = 25f; + minWatch = 5f; + maxWatch = 22f; + } + string label; if (hatchMoment) { label = EventReason.Hatch(subject); } + else if (alphaMoment) + { + label = EventReason.BecomeAlpha(subject); + } else if (!string.IsNullOrEmpty(occ.PlainClause)) { label = occ.SubjectName + " " + occ.PlainClause; @@ -649,7 +733,9 @@ public static class InterestFeeds Key = key, LeadKind = InterestLeadKind.EventLed, Category = grief ? "FamilyEmotion" : MapHappinessCategory(cat), - Source = hatchMoment ? "happiness_hatch" : "happiness", + Source = hatchMoment + ? "happiness_hatch" + : (alphaMoment ? "happiness_alpha" : "happiness"), EventStrength = strength, VisualConfidence = 0.75f, Position = occ.Position != Vector3.zero ? occ.Position : subject.current_position, @@ -887,6 +973,12 @@ public static class InterestFeeds || string.Equals(id, "just_hatched", StringComparison.OrdinalIgnoreCase); } + private static bool IsAlphaMoment(string effectId) + { + return !string.IsNullOrEmpty(effectId) + && string.Equals(effectId.Trim(), "become_alpha", StringComparison.OrdinalIgnoreCase); + } + private static string MapHappinessCategory(string cat) { switch ((cat ?? "").ToLowerInvariant()) diff --git a/IdleSpectator/Events/Feeds/RelationshipInterestFeed.cs b/IdleSpectator/Events/Feeds/RelationshipInterestFeed.cs index 70adf77..45e68f4 100644 --- a/IdleSpectator/Events/Feeds/RelationshipInterestFeed.cs +++ b/IdleSpectator/Events/Feeds/RelationshipInterestFeed.cs @@ -107,6 +107,8 @@ public static class RelationshipInterestFeed return EventReason.LoverParted(subject); case "find_lover": return EventReason.SeekingLover(subject); + case "become_alpha": + return EventReason.BecomeAlpha(subject); case "add_child": return EventReason.NewChild(subject, related); case "baby_created": diff --git a/IdleSpectator/Events/Patches/BoatEventPatches.cs b/IdleSpectator/Events/Patches/BoatEventPatches.cs index 1b8e6b6..07a509f 100644 --- a/IdleSpectator/Events/Patches/BoatEventPatches.cs +++ b/IdleSpectator/Events/Patches/BoatEventPatches.cs @@ -35,12 +35,9 @@ public static class BoatEventPatches [HarmonyPostfix] public static void PostfixLoad(Actor pActor, BehResult __result) { - if (pActor == null || !pActor.isAlive() || __result == BehResult.Stop) - { - return; - } - - EmitBoat("boat_load", 55f, pActor, EventReason.Boat(pActor, "load")); + // Loading is routine transport churn - unload/trade stay on Layer A. + _ = pActor; + _ = __result; } private static void EmitBoat(string eventId, float strength, Actor actor, string label) diff --git a/IdleSpectator/Events/Patches/BuildingEventPatches.cs b/IdleSpectator/Events/Patches/BuildingEventPatches.cs index c785bab..78f0c7f 100644 --- a/IdleSpectator/Events/Patches/BuildingEventPatches.cs +++ b/IdleSpectator/Events/Patches/BuildingEventPatches.cs @@ -12,23 +12,9 @@ public static class BuildingEventPatches [HarmonyPostfix] public static void PostfixDamageBuilding(Actor pActor, BehResult __result) { - if (pActor == null || !pActor.isAlive() || __result == BehResult.Stop) - { - return; - } - - Building target = null; - try - { - target = pActor.beh_building_target; - } - catch - { - target = null; - } - - string label = EventReason.BuildingDamage(pActor, target); - Emit("building_damage", 70f, pActor, label); + // Routine siege chips are Layer B noise. Camera waits for consume/destroy. + _ = pActor; + _ = __result; } [HarmonyPatch(typeof(BehConsumeTargetBuilding), nameof(BehConsumeTargetBuilding.execute))] diff --git a/IdleSpectator/Events/Patches/MetaEventPatches.cs b/IdleSpectator/Events/Patches/MetaEventPatches.cs index cda1078..4d06e35 100644 --- a/IdleSpectator/Events/Patches/MetaEventPatches.cs +++ b/IdleSpectator/Events/Patches/MetaEventPatches.cs @@ -62,6 +62,29 @@ public static class MetaEventPatches EmitNew("new_city", "Politics", 74f, __result, "city"); } + [HarmonyPatch(typeof(KingdomManager), nameof(KingdomManager.makeNewCivKingdom))] + [HarmonyPostfix] + public static void PostfixNewCivKingdom(Kingdom __result, Actor pActor) + { + if (__result == null) + { + return; + } + + Actor anchor = pActor != null && pActor.isAlive() ? pActor : TryReadActor(__result); + if (anchor == null) + { + return; + } + + MetaInterestFeed.EmitMeta( + "new_kingdom", + "Politics", + 82f, + anchor, + EventReason.MetaNew(anchor, "kingdom")); + } + private static void EmitNew( string eventId, string category, diff --git a/IdleSpectator/Events/Patches/PlotEventPatches.cs b/IdleSpectator/Events/Patches/PlotEventPatches.cs index 933305f..5e02199 100644 --- a/IdleSpectator/Events/Patches/PlotEventPatches.cs +++ b/IdleSpectator/Events/Patches/PlotEventPatches.cs @@ -74,6 +74,32 @@ public static class PlotEventPatches PlotInterestFeed.Emit(__state, __instance, "leave"); } + [HarmonyPatch(typeof(Plot), nameof(Plot.finishPlot))] + [HarmonyPostfix] + public static void PostfixFinishPlot(Plot __instance, PlotState pState, Actor pActor) + { + if (__instance == null || pState != PlotState.Finished) + { + return; + } + + // Prefer finishing actor; fall back to plot author inside EmitFromPlot. + if (pActor != null && pActor.isAlive()) + { + string assetId = ReadPlotAssetId(__instance); + if (string.IsNullOrEmpty(assetId)) + { + PlotInterestFeed.EmitFromPlot(__instance, "complete"); + return; + } + + PlotInterestFeed.Emit(assetId, pActor, "complete"); + return; + } + + PlotInterestFeed.EmitFromPlot(__instance, "complete"); + } + private static string ReadPlotAssetId(object plot) { try diff --git a/IdleSpectator/Events/Patches/RelationshipEventPatches.cs b/IdleSpectator/Events/Patches/RelationshipEventPatches.cs index 1c091c9..5670a69 100644 --- a/IdleSpectator/Events/Patches/RelationshipEventPatches.cs +++ b/IdleSpectator/Events/Patches/RelationshipEventPatches.cs @@ -183,4 +183,20 @@ public static class RelationshipEventPatches RelationshipInterestFeed.Emit("family_group_leave", pActor, ActorRelation.ResolvePackRelated(pActor)); } + + /// + /// Alpha succession is happiness-gated by emotions in vanilla. Patch the family API so + /// animals without emotions still get a Layer A / Activity-capable beat. + /// + [HarmonyPatch(typeof(Family), "setAlpha")] + [HarmonyPostfix] + public static void PostfixSetAlpha(Actor pActor, bool pNew) + { + if (!pNew || pActor == null || !pActor.isAlive()) + { + return; + } + + InterestFeeds.EmitAlpha(pActor, "family_set_alpha"); + } } diff --git a/IdleSpectator/Events/Patches/TraitEventPatches.cs b/IdleSpectator/Events/Patches/TraitEventPatches.cs index 3d565bd..0d752b4 100644 --- a/IdleSpectator/Events/Patches/TraitEventPatches.cs +++ b/IdleSpectator/Events/Patches/TraitEventPatches.cs @@ -119,12 +119,7 @@ public static class TraitEventPatches return; } - // Quiet-world Ambient traits must not steal the camera. - if (entry.EventStrength < InterestScoringConfig.W.noticeScoreMin) - { - return; - } - + // Quiet traits still register; low EventStrength lets the director bury them. // Spawn/birth endowment: only legendary rarities. if (gained && IsSpawnTraitWindow(actor) && !LegendaryBirthTraits.Contains(entry.Id)) { diff --git a/IdleSpectator/HarnessScenarios.cs b/IdleSpectator/HarnessScenarios.cs index b9ed5bc..b845966 100644 --- a/IdleSpectator/HarnessScenarios.cs +++ b/IdleSpectator/HarnessScenarios.cs @@ -1197,12 +1197,20 @@ internal static class HarnessScenarios Step("et20", "domain_feed", asset: "relationship", value: "set_lover"), Step("et21", "assert", expect: "interest_has_key", value: "rel:set_lover"), + Step("et21a", "domain_feed", asset: "relationship", value: "become_alpha"), + Step("et21b", "assert", expect: "interest_has_key", value: "rel:become_alpha"), + Step("et21c", "interest_force_session", asset: "human", + label: "{a} becomes the family alpha", tier: "Action", expect: "alpha_reason", + value: "lead=event;evt=72"), + Step("et21d", "assert", expect: "dossier_contains", value: "family alpha"), Step("et22", "domain_feed", asset: "plot", value: "summon_meteor_rain"), Step("et23", "assert", expect: "interest_has_key", value: "plot:summon_meteor_rain"), Step("et24", "domain_feed", asset: "era", value: "age_chaos"), Step("et25", "assert", expect: "interest_has_key", value: "era:age_chaos"), Step("et26", "domain_feed", asset: "meta", value: "new_city"), Step("et27", "assert", expect: "interest_has_key", value: "meta:new_city"), + Step("et27b", "domain_feed", asset: "meta", value: "new_kingdom"), + Step("et27c", "assert", expect: "interest_has_key", value: "meta:new_kingdom"), Step("et28", "domain_feed", asset: "book", value: "bad_story_about_king"), Step("et29", "assert", expect: "interest_has_key", value: "book:bad_story_about_king"), Step("et30", "domain_feed", asset: "trait", value: "immortal"), diff --git a/IdleSpectator/InterestScoring.cs b/IdleSpectator/InterestScoring.cs index b119ea7..aaa4737 100644 --- a/IdleSpectator/InterestScoring.cs +++ b/IdleSpectator/InterestScoring.cs @@ -199,11 +199,46 @@ public static class InterestScoring sig += Mathf.Min(w.metaRenownCap, snap.Renown / renownDiv); sig += Mathf.Min(w.metaKillsCap, snap.Kills * w.metaKillsFactor); sig += Mathf.Min(w.metaLevelCap, snap.Level * w.metaLevelFactor); + sig += SpeciesRarityBonus(snap.SpeciesId, w); snap.CharacterSignificance = sig; MetaCache[id] = snap; return snap; } + private static float SpeciesRarityBonus(string speciesId, ScoringWeights w) + { + if (string.IsNullOrEmpty(speciesId) || w == null) + { + return 0f; + } + + try + { + Dictionary counts = WorldActivityScanner.CountSpeciesPopulations(); + if (counts == null || !counts.TryGetValue(speciesId, out int pop) || pop <= 0) + { + return 0f; + } + + if (pop == 1) + { + return w.speciesSingletonBonus; + } + + int rareMax = w.speciesRareMaxPop > 0 ? w.speciesRareMaxPop : 5; + if (pop <= rareMax) + { + return w.speciesRareBonus; + } + } + catch + { + // ignore + } + + return 0f; + } + private static void ApplyMeta(InterestCandidate c, InterestMetadataSnapshot snap) { if (c == null || snap == null) diff --git a/IdleSpectator/InterestScoringConfig.cs b/IdleSpectator/InterestScoringConfig.cs index f252e39..1b642c9 100644 --- a/IdleSpectator/InterestScoringConfig.cs +++ b/IdleSpectator/InterestScoringConfig.cs @@ -79,6 +79,11 @@ public class ScoringWeights public float metaKillsCap = 12f; public float metaLevelFactor = 0.4f; public float metaLevelCap = 10f; + /// Bonus when the follow unit is the only living member of its species. + public float speciesSingletonBonus = 14f; + /// Bonus when species population is between 2 and speciesRareMaxPop inclusive. + public float speciesRareBonus = 8f; + public int speciesRareMaxPop = 5; public int notableKills = 40; public float notableRenown = 200f; diff --git a/IdleSpectator/MutationDiscoveryHarness.cs b/IdleSpectator/MutationDiscoveryHarness.cs index ba80a44..e9c6f43 100644 --- a/IdleSpectator/MutationDiscoveryHarness.cs +++ b/IdleSpectator/MutationDiscoveryHarness.cs @@ -106,6 +106,9 @@ public static class MutationDiscoveryHarness "ArmyManager.newArmy", "AllianceManager.newAlliance", "CityManager.newCity", + "KingdomManager.makeNewCivKingdom", + "Family.setAlpha", + "Plot.finishPlot", "ItemManager.generateItem", "ItemManager.newItem", "SubspeciesManager.newSpecies", diff --git a/IdleSpectator/mod.json b/IdleSpectator/mod.json index 48cfc90..0d2d362 100644 --- a/IdleSpectator/mod.json +++ b/IdleSpectator/mod.json @@ -1,7 +1,7 @@ { "name": "IdleSpectator", "author": "dazed", - "version": "0.22.0", - "description": "AFK Idle Spectator (I) + Lore (L). Layer B ticker + Layer A story camera.", + "version": "0.24.0", + "description": "AFK Idle Spectator (I) + Lore (L). Interesting beats are candidates; director ranks.", "GUID": "com.dazed.idlespectator" } diff --git a/IdleSpectator/scoring-model.json b/IdleSpectator/scoring-model.json index 0aa2c6e..bef2ca4 100644 --- a/IdleSpectator/scoring-model.json +++ b/IdleSpectator/scoring-model.json @@ -59,6 +59,9 @@ "metaKillsCap": 12, "metaLevelFactor": 0.4, "metaLevelCap": 10, + "speciesSingletonBonus": 14, + "speciesRareBonus": 8, + "speciesRareMaxPop": 5, "notableKills": 40, "notableRenown": 200, diff --git a/docs/event-audit.md b/docs/event-audit.md index f48df8b..776e1ad 100644 --- a/docs/event-audit.md +++ b/docs/event-audit.md @@ -39,15 +39,25 @@ Ambient / Character fill is exempt - A events may take the camera immediately. | Happiness | `IngestHappiness` | catalog + EventReason | Signal = A; see [event-prose-audit.md](event-prose-audit.md) | | Status | `OnStatusChange` | EventReason.Status | Gain A if CreatesInterest; egg **loss** → hatch A | | Scanner combat | WorldActivityScanner | EventReason.Fight / Battle | `EventCatalog.Combat` | -| War / Plot / Relationship / Trait / Building / Boat / Book / Era | Feeds + patches | EventReason / catalog | A via CreatesInterest | +| War / Plot / Relationship / Trait / Building / Boat / Book / Era / Kingdom | Feeds + patches | EventReason / catalog | A via CreatesInterest; alpha via Family.setAlpha | | Libraries | Deferred feeds | EventReason.Library | Mostly B; spectacle ids A | | Character fill | InterestDirector.TryCharacterFill | empty Label | Only when no pending EventLed | -## Layer A demotion (this pass) +## Layer A policy -Happiness Signal → Ambient (B ticker only): gifts, cry, inspired, enchanted, divine, magnetised, strange_urge, tantrum, forced_power, lost_fight, got_robbed, house find/lose, made friend, injured. +Interesting beats register as interest candidates (`Signal` / `CreatesInterest`). +Director ranks by `EventStrength` + scoring - do not hard-gate interesting life off Layer A. -Status CreatesInterest → false (Activity remains): surprised, confused, motivated, inspired, magnetized, starving, ash_fever, powerup, voices_in_my_head, angry. +Happiness Ambient (B) only: `got_poked`, `got_caught`, `paid_tax`, `just_ate`, `just_pooped`. +Happiness Aggregates stay civic mass (B unit camera). + +Status B: brief FX/cooldowns + egg gain only. + +Noise still skipped at patch (not dial): building_damage chips, boat_load. + +## Full surface + +See [world-event-inventory.md](world-event-inventory.md) for live library counts, gaps, and refresh commands. ## Interrupt / hold / grace diff --git a/docs/event-prose-audit.md b/docs/event-prose-audit.md index f8fd858..30394ea 100644 --- a/docs/event-prose-audit.md +++ b/docs/event-prose-audit.md @@ -39,11 +39,12 @@ Scope: Happiness (all authored), Relationship (all), Status/WorldLog/Plot/etc. s Grief deaths, `fallen_in_love`, gifts, house find/lose, `just_got_out_of_egg`, `just_became_adult`, WorldLog `king_new` template roles, Status `egg` / `pregnant` / `drowning` prose. -## A/B camera demotion (2026-07-16) +## A/B camera policy (2026-07-16) -Happiness Signal → Ambient (ticker only): gifts, cry, inspired, enchanted, divine, magnetised, strange_urge, tantrum happiness, forced_power, lost_fight, got_robbed, house find/lose, made friend, injured. +Interesting = interest candidate. Director ranks by strength. -Status CreatesInterest → false (Activity remains): surprised, confused, motivated, inspired, magnetized, starving, ash_fever, powerup, voices_in_my_head, angry. +Happiness Ambient only for true FX: poke, caught stub, tax, ate, pooped. +Status CreatesInterest false only for brief FX/cooldowns + egg gain. Gate helper: `EventCatalog.IsCameraWorthy`. Drops: `InterestDropLog`. diff --git a/docs/world-event-inventory.md b/docs/world-event-inventory.md new file mode 100644 index 0000000..98ba16c --- /dev/null +++ b/docs/world-event-inventory.md @@ -0,0 +1,91 @@ +# World event inventory (full surface) + +Source of truth for what IdleSpectator covers across the **whole** game. +Refresh via live libraries + `mutation_discovery` / domain audits under `.harness/`. + +Audit date: 2026-07-16. + +## Policy + +**If it is interesting, it is an interest candidate.** +`CreatesInterest` / Happiness `Signal` registers Layer A. +`EventStrength` + scoring decide who gets the spotlight. +Only true brief FX, cooldowns, civic Aggregates, and god-tool / biome spam stay B-only. + +## How to read this + +| Layer | Meaning | +|-------|---------| +| **B** | Activity / Life / chronicle prose may fire; no interest candidate | +| **A** | Registers interest; director ranks by points | +| **Gap** | Game fires a lifecycle beat; we have no reliable A path | + +## Live libraries (enumerated) + +| Library | ~Count | Owner | +|---------|-------:|-------| +| happiness_library | 67 | EventCatalog.Happiness | +| status | 58 | EventCatalog.Status | +| world_log_library | 41 | EventCatalog.WorldLog | +| disasters | 17 | via WorldLog | +| war_types_library | 5 | EventCatalog.WarType | +| traits | 116 | EventCatalog.Trait (all CI; strength ranks) | +| plots_library | 28 | EventCatalog.Plot | +| era_library | 11 | EventCatalog.Era | +| book_types | 12 | EventCatalog.Book (all A) | +| decisions / powers / spells / items | 127 / 339 / 12 / 111 | EventCatalog.Libraries | +| subspecies_traits / genes / phenotypes | 204 / 47 / 52 | Libraries | +| buildings | 618 | patches (consume/destroy A; damage skipped) | +| actor_library | 322 | SpeciesDiscovery / scoring | +| culture/religion/clan/language traits | 78 / 40 / 29 / 26 | **no event catalog yet** | + +## Layer A sizes (policy) + +| Domain | Authored | Layer A | +|--------|--------:|--------:| +| Happiness | 67 | ~49 Signal (5 Ambient FX; 13 Aggregate civic) | +| Status | 58 | ~42 CreatesInterest (FX/cooldowns + egg gain stay B) | +| WorldLog | 41 | ~40 | +| Relationship | 11 | all interesting discrete (seeker/pack at low strength) | +| Plot / Era / War / Book | 28 / 11 / 5 / 12 | all A | +| Trait | 116 | all CI (no noticeScoreMin hard drop) | +| Spell / Item / Gene / Phenotype / WorldLaw / SubspeciesTrait | live | ambient A at low strength; Signal bumps spectacle | +| Power / Decision / Biome | live | Signal predicate only (tools / idle AI / terrain) | +| Building / Boat | none | destroy/consume/trade/unload; damage/load skipped | + +## Wired hooks (major) + +Happiness, status (+ egg-loss hatch), WorldLog, wars, plots (new/cancel/join/leave/finish), +meta genesis (culture/religion/language/clan/army/alliance/city/kingdom), +books, traits, deferred libraries, boats (trade/unload), buildings (consume/destroy), +relationship (lover/child/family/baby/alpha), Family.setAlpha, SpeciesDiscovery. + +## Known gaps / thin areas + +| Gap | Why it matters | Status | +|-----|----------------|--------| +| Culture/religion/clan/language trait gain | Meta drama libraries exist | **Open** | +| Building complete / wonder finish | Settlement spectacle | **Open** | +| Alliance/kingdom destroy managers | End beats if WorldLog thin | **Open** | +| Era/earthquake poll (ongoing) | Age change is wired; poll not | **Open** | + +## Stay B (true noise) + +- Happiness: `got_poked`, `got_caught`, `paid_tax`, `just_ate`, `just_pooped` + all Aggregates +- Status: dash/dodge/shield/recovery_*/spell_*/cough/flicked/just_ate/on_guard/afterglow/slowness + egg gain +- Libraries: Power ambient (god paint tools), Decision ambient (idle AI; interesting decisions are Signal), Biome ambient +- Patches: `building_damage`, `boat_load` + +## Scoring notes + +`TotalScore = EventStrength + scale + CharacterSignificance * charWeight + visual + novelty`. + +CharacterSignificance includes favorite/king/leader/renown/kills/level and species rarity. + +## Refresh commands + +```bash +./scripts/harness-run.sh --no-launch mutation_discovery +./scripts/harness-run.sh --no-launch domain_event_audit +./scripts/harness-run.sh --no-launch activity_happiness_audit +```