From 199adee6442b6c02f7fe323bf5667be91ceb9743 Mon Sep 17 00:00:00 2001 From: DazedAnon Date: Wed, 15 Jul 2026 20:09:08 -0500 Subject: [PATCH] Build Inventories --- IdleSpectator/ActivityAssetCatalog.cs | 79 ++++- IdleSpectator/ActivityHappinessHarness.cs | 23 +- IdleSpectator/ActivityStatusHarness.cs | 56 +++- IdleSpectator/AgentHarness.cs | 92 ++++++ IdleSpectator/DisasterInterestCatalog.cs | 91 ++++++ IdleSpectator/DisasterWarHarness.cs | 139 +++++++++ IdleSpectator/HappinessEventCatalog.cs | 68 +++++ IdleSpectator/HappinessModels.cs | 1 + IdleSpectator/HarnessScenarios.cs | 58 ++++ IdleSpectator/InterestFeeds.cs | 37 +-- IdleSpectator/InterestScoring.cs | 29 +- IdleSpectator/StatusInterestCatalog.cs | 146 ++++++++++ IdleSpectator/WarInterestFeed.cs | 336 ++++++++++++++++++++++ IdleSpectator/WarTypeInterestCatalog.cs | 92 ++++++ IdleSpectator/WorldLogEventCatalog.cs | 190 ++++++++++++ IdleSpectator/WorldLogEventHarness.cs | 216 ++++++++++++++ IdleSpectator/WorldLogInterestTable.cs | 122 +------- IdleSpectator/WorldLogPatches.cs | 12 +- IdleSpectator/mod.json | 2 +- scripts/harness-run.sh | 1 + 20 files changed, 1598 insertions(+), 192 deletions(-) create mode 100644 IdleSpectator/DisasterInterestCatalog.cs create mode 100644 IdleSpectator/DisasterWarHarness.cs create mode 100644 IdleSpectator/StatusInterestCatalog.cs create mode 100644 IdleSpectator/WarInterestFeed.cs create mode 100644 IdleSpectator/WarTypeInterestCatalog.cs create mode 100644 IdleSpectator/WorldLogEventCatalog.cs create mode 100644 IdleSpectator/WorldLogEventHarness.cs diff --git a/IdleSpectator/ActivityAssetCatalog.cs b/IdleSpectator/ActivityAssetCatalog.cs index a4f7b10..276d5ba 100644 --- a/IdleSpectator/ActivityAssetCatalog.cs +++ b/IdleSpectator/ActivityAssetCatalog.cs @@ -204,11 +204,82 @@ public static class ActivityAssetCatalog } public static List EnumerateLiveTaskIds() + { + return EnumerateLibraryIds("tasks_actor"); + } + + public static List EnumerateLiveWorldLogIds() + { + return EnumerateLibraryIds("world_log_library"); + } + + public static WorldLogAsset TryGetWorldLogAsset(string assetId) + { + if (string.IsNullOrEmpty(assetId)) + { + return null; + } + + try + { + return AssetManager.world_log_library?.get(assetId.Trim()); + } + catch + { + return null; + } + } + + public static List EnumerateLiveDisasterIds() + { + return EnumerateLibraryIds("disasters"); + } + + public static DisasterAsset TryGetDisasterAsset(string disasterId) + { + if (string.IsNullOrEmpty(disasterId)) + { + return null; + } + + try + { + return AssetManager.disasters?.get(disasterId.Trim()); + } + catch + { + return null; + } + } + + public static List EnumerateLiveWarTypeIds() + { + return EnumerateLibraryIds("war_types_library"); + } + + public static WarTypeAsset TryGetWarTypeAsset(string warTypeId) + { + if (string.IsNullOrEmpty(warTypeId)) + { + return null; + } + + try + { + return AssetManager.war_types_library?.get(warTypeId.Trim()); + } + catch + { + return null; + } + } + + private static List EnumerateLibraryIds(string assetManagerField) { var ids = new List(); try { - object lib = typeof(AssetManager).GetField("tasks_actor")?.GetValue(null); + object lib = typeof(AssetManager).GetField(assetManagerField)?.GetValue(null); object listObj = lib?.GetType().GetField("list")?.GetValue(lib) ?? lib?.GetType().GetProperty("list")?.GetValue(lib, null); System.Collections.IList list = listObj as System.Collections.IList; @@ -219,9 +290,9 @@ public static class ActivityAssetCatalog for (int i = 0; i < list.Count; i++) { - object task = list[i]; - object idObj = task?.GetType().GetField("id")?.GetValue(task) - ?? task?.GetType().GetProperty("id")?.GetValue(task, null); + object asset = list[i]; + object idObj = asset?.GetType().GetField("id")?.GetValue(asset) + ?? asset?.GetType().GetProperty("id")?.GetValue(asset, null); string id = idObj as string; if (!string.IsNullOrEmpty(id)) { diff --git a/IdleSpectator/ActivityHappinessHarness.cs b/IdleSpectator/ActivityHappinessHarness.cs index 17563e2..3536779 100644 --- a/IdleSpectator/ActivityHappinessHarness.cs +++ b/IdleSpectator/ActivityHappinessHarness.cs @@ -18,6 +18,7 @@ public sealed class ActivityHappinessAuditResult public int OrphanAuthored; public int MissingPolicy; public int RequiredContextFailures; + public int MissingEventStrength; } /// @@ -41,9 +42,10 @@ public static class ActivityHappinessHarness int orphanAuthored = 0; int missingPolicy = 0; int requiredContextFailures = 0; + int missingEventStrength = 0; var tsv = new StringBuilder(); tsv.AppendLine( - "id\tvalue\tpsychopath\tpresentation\tlife\tcontext\toverlap\tclause\tauthored\ticon\tlocale_leak\tnotes"); + "id\tvalue\tpsychopath\tpresentation\tlife\tcontext\toverlap\tevent_strength\tclause\tauthored\ticon\tlocale_leak\tnotes"); var liveSet = new HashSet(StringComparer.OrdinalIgnoreCase); for (int i = 0; i < liveIds.Count; i++) @@ -58,6 +60,11 @@ public static class ActivityHappinessHarness missingAuthored++; } + if (hasAuthored && entry.EventStrength <= 0f) + { + missingEventStrength++; + } + string clauseWith = ActivityHappinessProse.Clause(entry, "Ava", out bool usedRelated, out _); string clauseWithout = ActivityHappinessProse.Clause(entry, "", out _, out _); if (string.IsNullOrEmpty(clauseWith) || string.IsNullOrEmpty(clauseWithout)) @@ -126,6 +133,10 @@ public static class ActivityHappinessHarness { notes = "missing_authored"; } + else if (entry.EventStrength <= 0f) + { + notes = "missing_event_strength"; + } else if (!iconOk) { notes = "missing_icon"; @@ -151,6 +162,7 @@ public static class ActivityHappinessHarness .Append(entry.Life).Append('\t') .Append(entry.Context).Append('\t') .Append(entry.Overlap).Append('\t') + .Append(entry.EventStrength.ToString("0.#")).Append('\t') .Append(Escape(clauseWith)).Append('\t') .Append(hasAuthored ? "1" : "0").Append('\t') .Append(iconOk ? "1" : "0").Append('\t') @@ -164,7 +176,7 @@ public static class ActivityHappinessHarness { orphanAuthored++; tsv.Append(id).Append('\t') - .Append("\t\t\t\t\t\t\t1\t0\t0\torphan_authored").AppendLine(); + .Append("\t\t\t\t\t\t\t0\t\t1\t0\t0\torphan_authored").AppendLine(); } } @@ -189,7 +201,8 @@ public static class ActivityHappinessHarness && emptyPhrase == 0 && orphanAuthored == 0 && missingPolicy == 0 - && requiredContextFailures == 0; + && requiredContextFailures == 0 + && missingEventStrength == 0; return new ActivityHappinessAuditResult { @@ -202,10 +215,12 @@ public static class ActivityHappinessHarness OrphanAuthored = orphanAuthored, MissingPolicy = missingPolicy, RequiredContextFailures = requiredContextFailures, + MissingEventStrength = missingEventStrength, Detail = $"total={liveIds.Count} missingAuthored={missingAuthored} missingIcon={missingIcon} " + $"localeLeaks={localeLeaks} emptyPhrase={emptyPhrase} orphanAuthored={orphanAuthored} " - + $"missingPolicy={missingPolicy} requiredContextFailures={requiredContextFailures}" + + $"missingPolicy={missingPolicy} requiredContextFailures={requiredContextFailures} " + + $"missingEventStrength={missingEventStrength}" }; } diff --git a/IdleSpectator/ActivityStatusHarness.cs b/IdleSpectator/ActivityStatusHarness.cs index a579316..b15605b 100644 --- a/IdleSpectator/ActivityStatusHarness.cs +++ b/IdleSpectator/ActivityStatusHarness.cs @@ -16,10 +16,12 @@ public sealed class ActivityStatusAuditResult public int LocaleLeaks; public int EmptyPhrase; public int OrphanAuthored; + public int MissingInterest; + public int OrphanInterest; } /// -/// Exhaustive live-library audit for status Activity prose and icons. +/// Exhaustive live-library audit for status Activity prose, icons, and interest policy. /// public static class ActivityStatusHarness { @@ -37,11 +39,19 @@ public static class ActivityStatusHarness int localeLeaks = 0; int emptyPhrase = 0; int orphanAuthored = 0; + int missingInterest = 0; + int orphanInterest = 0; var tsv = new StringBuilder(); tsv.AppendLine( - "id\tgain\tloss\tauthored\ticon\tlocale_leak\tduration\tallow_timer_reset\tnotes"); + "id\tgain\tloss\tauthored\tinterest\tevent_strength\tcreates_interest\ticon\tlocale_leak\tduration\tallow_timer_reset\tnotes"); var liveSet = new HashSet(StringComparer.OrdinalIgnoreCase); + var interestAuthored = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (string id in StatusInterestCatalog.AuthoredIds) + { + interestAuthored.Add(id); + } + for (int i = 0; i < liveIds.Count; i++) { string id = liveIds[i]; @@ -55,6 +65,13 @@ public static class ActivityStatusHarness missingAuthored++; } + StatusInterestEntry interest = StatusInterestCatalog.GetOrFallback(id); + bool hasInterest = StatusInterestCatalog.HasAuthored(id) && !interest.IsFallback; + if (!hasInterest) + { + missingInterest++; + } + if (string.IsNullOrEmpty(gain) || string.IsNullOrEmpty(loss)) { emptyPhrase++; @@ -90,11 +107,15 @@ public static class ActivityStatusHarness // ignore } - string notes = ""; + string notes = "ok"; if (!hasAuthored) { notes = "missing_authored"; } + else if (!hasInterest) + { + notes = "missing_interest"; + } else if (!iconOk) { notes = "missing_icon"; @@ -107,22 +128,20 @@ public static class ActivityStatusHarness { notes = "empty_phrase"; } - else - { - notes = "ok"; - } tsv.Append(id).Append('\t') .Append(Escape(gain)).Append('\t') .Append(Escape(loss)).Append('\t') .Append(hasAuthored ? "1" : "0").Append('\t') + .Append(hasInterest ? "1" : "0").Append('\t') + .Append(interest.EventStrength.ToString("0.#")).Append('\t') + .Append(interest.CreatesInterest ? "1" : "0").Append('\t') .Append(iconOk ? "1" : "0").Append('\t') .Append(leak ? "1" : "0").Append('\t') .Append(duration).Append('\t') .Append(allowReset).Append('\t') .Append(notes).AppendLine(); - // Smoke the milestone formatting + keys. string gainKey = ActivityStatusProse.TaskKey(id, true); string lossKey = ActivityStatusProse.TaskKey(id, false); if (string.IsNullOrEmpty(gainKey) || string.IsNullOrEmpty(lossKey)) @@ -136,9 +155,16 @@ public static class ActivityStatusHarness if (!liveSet.Contains(id)) { orphanAuthored++; - tsv.Append(id).Append('\t') - .Append('\t').Append('\t') - .Append("1\t0\t0\t\t\torphan_authored").AppendLine(); + tsv.Append(id).Append("\t\t\t1\t0\t0\t0\t0\t0\t\t\torphan_authored").AppendLine(); + } + } + + foreach (string id in interestAuthored) + { + if (!liveSet.Contains(id)) + { + orphanInterest++; + tsv.Append(id).Append("\t\t\t0\t1\t0\t0\t0\t0\t\t\torphan_interest").AppendLine(); } } @@ -163,7 +189,9 @@ public static class ActivityStatusHarness && missingIcon == 0 && localeLeaks == 0 && emptyPhrase == 0 - && orphanAuthored == 0; + && orphanAuthored == 0 + && missingInterest == 0 + && orphanInterest == 0; return new ActivityStatusAuditResult { @@ -174,10 +202,12 @@ public static class ActivityStatusHarness LocaleLeaks = localeLeaks, EmptyPhrase = emptyPhrase, OrphanAuthored = orphanAuthored, + MissingInterest = missingInterest, + OrphanInterest = orphanInterest, Detail = $"total={liveIds.Count} missingAuthored={missingAuthored} missingIcon={missingIcon} " + $"localeLeaks={localeLeaks} emptyPhrase={emptyPhrase} orphanAuthored={orphanAuthored} " - + $"passed={passed}" + + $"missingInterest={missingInterest} orphanInterest={orphanInterest} passed={passed}" }; } diff --git a/IdleSpectator/AgentHarness.cs b/IdleSpectator/AgentHarness.cs index 2fd64f4..765436c 100644 --- a/IdleSpectator/AgentHarness.cs +++ b/IdleSpectator/AgentHarness.cs @@ -1797,6 +1797,10 @@ public static class AgentHarness DoTriggerInterest(cmd); break; + case "world_log_feed": + DoWorldLogFeed(cmd); + break; + case "interest_inject": DoInterestInject(cmd); break; @@ -2447,6 +2451,77 @@ public static class AgentHarness detail: $"triggered label={tipLabel} evt={tierEvt:0.#} forced={forceCamera}"); } + /// + /// Feed a live WorldLog asset id through InterestFeeds as if WorldLog fired (coverage of authored catalog). + /// + private static void DoWorldLogFeed(HarnessCommand cmd) + { + if (!WorldReady()) + { + _cmdFail++; + Emit(cmd, ok: false, detail: "world_not_ready"); + return; + } + + string assetId = string.IsNullOrEmpty(cmd.asset) ? (cmd.value ?? "") : cmd.asset; + if (string.IsNullOrEmpty(assetId) || assetId == "auto") + { + assetId = "disaster_tornado"; + } + + if (!WorldLogEventCatalog.HasAuthored(assetId)) + { + _cmdFail++; + Emit(cmd, ok: false, detail: "not_authored:" + assetId); + return; + } + + WorldLogEventEntry entry = WorldLogEventCatalog.GetOrFallback(assetId); + Actor follow = ResolveUnit(null) ?? WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f); + if (follow == null && entry.CreatesInterest) + { + _cmdFail++; + Emit(cmd, ok: false, detail: "no unit for world_log_feed"); + return; + } + + if (!entry.CreatesInterest) + { + _cmdOk++; + Emit(cmd, ok: true, detail: $"skipped_no_interest id={assetId}"); + return; + } + + string kingdom = follow.kingdom != null ? (follow.kingdom.name ?? "Alpha") : "Alpha"; + string key = "worldlog:" + assetId + ":" + kingdom; + var candidate = new InterestCandidate + { + Key = key, + LeadKind = InterestLeadKind.EventLed, + Category = entry.Category, + Source = "worldlog", + EventStrength = entry.EventStrength, + VisualConfidence = 0.7f, + Position = follow.current_position, + FollowUnit = follow, + SubjectId = follow.getID(), + Label = entry.MakeLabel(kingdom, "Beta"), + AssetId = assetId, + KingdomKey = kingdom, + SpeciesId = follow.asset != null ? follow.asset.id : "", + CreatedAt = Time.unscaledTime, + LastSeenAt = Time.unscaledTime, + ExpiresAt = Time.unscaledTime + 40f, + MinWatch = entry.MinWatch, + MaxWatch = entry.MaxWatch, + Completion = InterestCompletionKind.FixedDwell + }; + InterestScoring.ScoreCheap(candidate); + InterestRegistry.Upsert(candidate); + _cmdOk++; + Emit(cmd, ok: true, detail: $"fed id={assetId} key={key} evt={entry.EventStrength:0.#}"); + } + private static void DoInterestInject(HarnessCommand cmd) { if (!WorldReady()) @@ -3970,6 +4045,23 @@ public static class AgentHarness detail = audit.Detail; break; } + case "world_log_audit": + { + WorldLogEventAuditResult audit = WorldLogEventHarness.RunAudit(HarnessDir()); + pass = audit.Passed; + detail = audit.Detail; + break; + } + case "disaster_war_inventory": + case "disaster_war_audit": + { + DisasterWarAuditResult audit = DisasterWarHarness.RunAudit(HarnessDir()); + // Keep inventory dump for debugging alongside audit. + WorldLogEventHarness.DumpDisasterWarInventory(HarnessDir()); + pass = audit.Passed; + detail = audit.Detail; + break; + } case "activity_happiness_count": { int want = ParseCountExpect(cmd, defaultValue: 1); diff --git a/IdleSpectator/DisasterInterestCatalog.cs b/IdleSpectator/DisasterInterestCatalog.cs new file mode 100644 index 0000000..bacb2c7 --- /dev/null +++ b/IdleSpectator/DisasterInterestCatalog.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; + +namespace IdleSpectator; + +public sealed class DisasterInterestEntry +{ + public string Id = ""; + public float EventStrength = 90f; + public string Category = "Disaster"; + public string WorldLogId = ""; + public bool CreatesInterest = true; + public bool IsFallback; +} + +/// Authored interest overlay for every live disasters library asset. +public static class DisasterInterestCatalog +{ + private static readonly Dictionary Entries = + new Dictionary(StringComparer.OrdinalIgnoreCase); + + static DisasterInterestCatalog() + { + Add("tornado", 95f, "disaster_tornado"); + Add("heatwave", 88f, "disaster_heatwave"); + Add("small_meteorite", 95f, "disaster_meteorite"); + Add("small_earthquake", 95f, "disaster_earthquake"); + Add("hellspawn", 96f, "disaster_hellspawn"); + Add("ice_ones_awoken", 95f, "disaster_ice_ones"); + Add("sudden_snowman", 90f, "disaster_sudden_snowman"); + Add("garden_surprise", 90f, "disaster_garden_surprise"); + Add("dragon_from_farlands", 98f, "disaster_dragon_from_farlands"); + Add("ash_bandits", 92f, "disaster_bandits"); + Add("alien_invasion", 97f, "disaster_alien_invasion"); + Add("biomass", 96f, "disaster_biomass"); + Add("tumor", 96f, "disaster_tumor"); + Add("wild_mage", 94f, "disaster_evil_mage"); + Add("underground_necromancer", 95f, "disaster_underground_necromancer"); + Add("mad_thoughts", 90f, "disaster_mad_thoughts"); + Add("greg_abominations", 96f, "disaster_greg_abominations"); + } + + private static void Add(string id, float strength, string worldLogId) + { + Entries[id] = new DisasterInterestEntry + { + Id = id, + EventStrength = strength, + WorldLogId = worldLogId, + CreatesInterest = true, + Category = "Disaster" + }; + } + + public static IEnumerable AuthoredIds => Entries.Keys; + + public static bool HasAuthored(string disasterId) + { + return !string.IsNullOrEmpty(disasterId) && Entries.ContainsKey(disasterId.Trim()); + } + + public static bool TryGet(string disasterId, out DisasterInterestEntry entry) + { + entry = null; + if (string.IsNullOrEmpty(disasterId)) + { + return false; + } + + return Entries.TryGetValue(disasterId.Trim(), out entry); + } + + public static DisasterInterestEntry GetOrFallback(string disasterId) + { + if (TryGet(disasterId, out DisasterInterestEntry entry)) + { + return entry; + } + + string id = string.IsNullOrEmpty(disasterId) ? "unknown" : disasterId.Trim(); + return new DisasterInterestEntry + { + Id = id, + EventStrength = 90f, + Category = "Disaster", + WorldLogId = "", + CreatesInterest = true, + IsFallback = true + }; + } +} diff --git a/IdleSpectator/DisasterWarHarness.cs b/IdleSpectator/DisasterWarHarness.cs new file mode 100644 index 0000000..a63887e --- /dev/null +++ b/IdleSpectator/DisasterWarHarness.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace IdleSpectator; + +public sealed class DisasterWarAuditResult +{ + public bool Passed; + public string Detail = ""; + public int DisasterTotal; + public int WarTypeTotal; + public int MissingDisaster; + public int OrphanDisaster; + public int MissingWarType; + public int OrphanWarType; +} + +/// Live disasters + war_types library coverage audit. +public static class DisasterWarHarness +{ + public static DisasterWarAuditResult RunAudit(string outputDirectory) + { + List disasters = ActivityAssetCatalog.EnumerateLiveDisasterIds(); + List warTypes = ActivityAssetCatalog.EnumerateLiveWarTypeIds(); + + var authoredDisasters = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (string id in DisasterInterestCatalog.AuthoredIds) + { + authoredDisasters.Add(id); + } + + var authoredWars = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (string id in WarTypeInterestCatalog.AuthoredIds) + { + authoredWars.Add(id); + } + + int missingDisaster = 0; + int orphanDisaster = 0; + int missingWarType = 0; + int orphanWarType = 0; + var tsv = new StringBuilder(); + tsv.AppendLine("kind\tid\tauthored\tevent_strength\tworld_log_id\tnotes"); + + var liveDisasters = new HashSet(StringComparer.OrdinalIgnoreCase); + for (int i = 0; i < disasters.Count; i++) + { + string id = disasters[i]; + liveDisasters.Add(id); + DisasterInterestEntry entry = DisasterInterestCatalog.GetOrFallback(id); + bool has = DisasterInterestCatalog.HasAuthored(id) && !entry.IsFallback; + if (!has) + { + missingDisaster++; + } + + tsv.Append("disaster\t").Append(id).Append('\t') + .Append(has ? "1" : "0").Append('\t') + .Append(entry.EventStrength.ToString("0.#")).Append('\t') + .Append(entry.WorldLogId ?? "").Append('\t') + .Append(has ? "ok" : "missing_authored").AppendLine(); + } + + foreach (string id in authoredDisasters) + { + if (!liveDisasters.Contains(id)) + { + orphanDisaster++; + tsv.Append("disaster\t").Append(id).Append("\t1\t0\t\torphan_authored").AppendLine(); + } + } + + var liveWars = new HashSet(StringComparer.OrdinalIgnoreCase); + for (int i = 0; i < warTypes.Count; i++) + { + string id = warTypes[i]; + liveWars.Add(id); + WarTypeInterestEntry entry = WarTypeInterestCatalog.GetOrFallback(id); + bool has = WarTypeInterestCatalog.HasAuthored(id) && !entry.IsFallback; + if (!has) + { + missingWarType++; + } + + tsv.Append("war_type\t").Append(id).Append('\t') + .Append(has ? "1" : "0").Append('\t') + .Append(entry.EventStrength.ToString("0.#")).Append('\t') + .Append('\t') + .Append(has ? "ok" : "missing_authored").AppendLine(); + } + + foreach (string id in authoredWars) + { + if (!liveWars.Contains(id)) + { + orphanWarType++; + tsv.Append("war_type\t").Append(id).Append("\t1\t0\t\torphan_authored").AppendLine(); + } + } + + try + { + if (!string.IsNullOrEmpty(outputDirectory)) + { + Directory.CreateDirectory(outputDirectory); + File.WriteAllText( + Path.Combine(outputDirectory, "disaster-war-audit.tsv"), + tsv.ToString()); + } + } + catch + { + // ignore + } + + bool passed = disasters.Count > 0 + && warTypes.Count > 0 + && missingDisaster == 0 + && orphanDisaster == 0 + && missingWarType == 0 + && orphanWarType == 0; + + return new DisasterWarAuditResult + { + Passed = passed, + DisasterTotal = disasters.Count, + WarTypeTotal = warTypes.Count, + MissingDisaster = missingDisaster, + OrphanDisaster = orphanDisaster, + MissingWarType = missingWarType, + OrphanWarType = orphanWarType, + Detail = + $"disasters={disasters.Count} missingDisaster={missingDisaster} orphanDisaster={orphanDisaster} " + + $"war_types={warTypes.Count} missingWarType={missingWarType} orphanWarType={orphanWarType}" + }; + } +} diff --git a/IdleSpectator/HappinessEventCatalog.cs b/IdleSpectator/HappinessEventCatalog.cs index 2728b5a..cf45f5d 100644 --- a/IdleSpectator/HappinessEventCatalog.cs +++ b/IdleSpectator/HappinessEventCatalog.cs @@ -12,6 +12,7 @@ public static class HappinessEventCatalog ["death_family_member"] = new HappinessCatalogEntry { Id = "death_family_member", + EventStrength = 76f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.RequiresRelatedActor, Life = HappinessLifePolicy.ProjectToLife, @@ -24,6 +25,7 @@ public static class HappinessEventCatalog ["death_lover"] = new HappinessCatalogEntry { Id = "death_lover", + EventStrength = 88f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.RequiresRelatedActor, Life = HappinessLifePolicy.ProjectToLife, @@ -36,6 +38,7 @@ public static class HappinessEventCatalog ["death_child"] = new HappinessCatalogEntry { Id = "death_child", + EventStrength = 95f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.RequiresRelatedActor, Life = HappinessLifePolicy.ProjectToLife, @@ -48,6 +51,7 @@ public static class HappinessEventCatalog ["death_best_friend"] = new HappinessCatalogEntry { Id = "death_best_friend", + EventStrength = 78f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.RequiresRelatedActor, Life = HappinessLifePolicy.ProjectToLife, @@ -60,6 +64,7 @@ public static class HappinessEventCatalog ["got_robbed"] = new HappinessCatalogEntry { Id = "got_robbed", + EventStrength = 50f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.RelatedOptional, Life = HappinessLifePolicy.ActivityOnly, @@ -72,6 +77,7 @@ public static class HappinessEventCatalog ["got_poked"] = new HappinessCatalogEntry { Id = "got_poked", + EventStrength = 30f, Presentation = HappinessPresentationTier.Ambient, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -84,6 +90,7 @@ public static class HappinessEventCatalog ["lost_fight"] = new HappinessCatalogEntry { Id = "lost_fight", + EventStrength = 55f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -96,6 +103,7 @@ public static class HappinessEventCatalog ["got_caught"] = new HappinessCatalogEntry { Id = "got_caught", + EventStrength = 48f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -108,6 +116,7 @@ public static class HappinessEventCatalog ["paid_tax"] = new HappinessCatalogEntry { Id = "paid_tax", + EventStrength = 30f, Presentation = HappinessPresentationTier.Ambient, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -120,6 +129,7 @@ public static class HappinessEventCatalog ["just_ate"] = new HappinessCatalogEntry { Id = "just_ate", + EventStrength = 30f, Presentation = HappinessPresentationTier.Ambient, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -133,6 +143,7 @@ public static class HappinessEventCatalog ["just_received_gift"] = new HappinessCatalogEntry { Id = "just_received_gift", + EventStrength = 50f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.RelatedOptional, Life = HappinessLifePolicy.ActivityOnly, @@ -145,6 +156,7 @@ public static class HappinessEventCatalog ["just_gave_gift"] = new HappinessCatalogEntry { Id = "just_gave_gift", + EventStrength = 50f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.RelatedOptional, Life = HappinessLifePolicy.ActivityOnly, @@ -157,6 +169,7 @@ public static class HappinessEventCatalog ["just_pooped"] = new HappinessCatalogEntry { Id = "just_pooped", + EventStrength = 30f, Presentation = HappinessPresentationTier.Ambient, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -169,6 +182,7 @@ public static class HappinessEventCatalog ["just_slept"] = new HappinessCatalogEntry { Id = "just_slept", + EventStrength = 30f, Presentation = HappinessPresentationTier.Ambient, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -181,6 +195,7 @@ public static class HappinessEventCatalog ["had_bad_dream"] = new HappinessCatalogEntry { Id = "had_bad_dream", + EventStrength = 30f, Presentation = HappinessPresentationTier.Ambient, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -194,6 +209,7 @@ public static class HappinessEventCatalog ["had_good_dream"] = new HappinessCatalogEntry { Id = "had_good_dream", + EventStrength = 30f, Presentation = HappinessPresentationTier.Ambient, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -207,6 +223,7 @@ public static class HappinessEventCatalog ["had_nightmare"] = new HappinessCatalogEntry { Id = "had_nightmare", + EventStrength = 30f, Presentation = HappinessPresentationTier.Ambient, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -220,6 +237,7 @@ public static class HappinessEventCatalog ["slept_outside"] = new HappinessCatalogEntry { Id = "slept_outside", + EventStrength = 30f, Presentation = HappinessPresentationTier.Ambient, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -232,6 +250,7 @@ public static class HappinessEventCatalog ["just_kissed"] = new HappinessCatalogEntry { Id = "just_kissed", + EventStrength = 50f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.RelatedOptional, Life = HappinessLifePolicy.ActivityOnly, @@ -244,6 +263,7 @@ public static class HappinessEventCatalog ["just_killed"] = new HappinessCatalogEntry { Id = "just_killed", + EventStrength = 45f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -257,6 +277,7 @@ public static class HappinessEventCatalog ["become_king"] = new HappinessCatalogEntry { Id = "become_king", + EventStrength = 72f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ProjectToLife, @@ -269,6 +290,7 @@ public static class HappinessEventCatalog ["become_leader"] = new HappinessCatalogEntry { Id = "become_leader", + EventStrength = 65f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ProjectToLife, @@ -281,6 +303,7 @@ public static class HappinessEventCatalog ["just_won_war"] = new HappinessCatalogEntry { Id = "just_won_war", + EventStrength = 35f, Presentation = HappinessPresentationTier.Aggregate, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -293,6 +316,7 @@ public static class HappinessEventCatalog ["just_made_peace"] = new HappinessCatalogEntry { Id = "just_made_peace", + EventStrength = 35f, Presentation = HappinessPresentationTier.Aggregate, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -305,6 +329,7 @@ public static class HappinessEventCatalog ["just_lost_war"] = new HappinessCatalogEntry { Id = "just_lost_war", + EventStrength = 35f, Presentation = HappinessPresentationTier.Aggregate, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -317,6 +342,7 @@ public static class HappinessEventCatalog ["was_conquered"] = new HappinessCatalogEntry { Id = "was_conquered", + EventStrength = 35f, Presentation = HappinessPresentationTier.Aggregate, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -329,6 +355,7 @@ public static class HappinessEventCatalog ["kingdom_fell_apart"] = new HappinessCatalogEntry { Id = "kingdom_fell_apart", + EventStrength = 35f, Presentation = HappinessPresentationTier.Aggregate, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -341,6 +368,7 @@ public static class HappinessEventCatalog ["just_started_war"] = new HappinessCatalogEntry { Id = "just_started_war", + EventStrength = 80f, Presentation = HappinessPresentationTier.Aggregate, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -353,6 +381,7 @@ public static class HappinessEventCatalog ["just_rebelled"] = new HappinessCatalogEntry { Id = "just_rebelled", + EventStrength = 35f, Presentation = HappinessPresentationTier.Aggregate, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -365,6 +394,7 @@ public static class HappinessEventCatalog ["fallen_in_love"] = new HappinessCatalogEntry { Id = "fallen_in_love", + EventStrength = 50f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.RelatedOptional, Life = HappinessLifePolicy.ActivityOnly, @@ -379,6 +409,7 @@ public static class HappinessEventCatalog ["just_had_child"] = new HappinessCatalogEntry { Id = "just_had_child", + EventStrength = 70f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.RelatedOptional, Life = HappinessLifePolicy.ProjectToLife, @@ -391,6 +422,7 @@ public static class HappinessEventCatalog ["just_read_book"] = new HappinessCatalogEntry { Id = "just_read_book", + EventStrength = 30f, Presentation = HappinessPresentationTier.Ambient, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -403,6 +435,7 @@ public static class HappinessEventCatalog ["just_played"] = new HappinessCatalogEntry { Id = "just_played", + EventStrength = 30f, Presentation = HappinessPresentationTier.Ambient, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -415,6 +448,7 @@ public static class HappinessEventCatalog ["just_talked"] = new HappinessCatalogEntry { Id = "just_talked", + EventStrength = 30f, Presentation = HappinessPresentationTier.Ambient, Context = HappinessContextRequirement.RelatedOptional, Life = HappinessLifePolicy.ActivityOnly, @@ -427,6 +461,7 @@ public static class HappinessEventCatalog ["just_laughed"] = new HappinessCatalogEntry { Id = "just_laughed", + EventStrength = 30f, Presentation = HappinessPresentationTier.Ambient, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -440,6 +475,7 @@ public static class HappinessEventCatalog ["just_sang"] = new HappinessCatalogEntry { Id = "just_sang", + EventStrength = 30f, Presentation = HappinessPresentationTier.Ambient, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -453,6 +489,7 @@ public static class HappinessEventCatalog ["just_swore"] = new HappinessCatalogEntry { Id = "just_swore", + EventStrength = 30f, Presentation = HappinessPresentationTier.Ambient, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -466,6 +503,7 @@ public static class HappinessEventCatalog ["just_cried"] = new HappinessCatalogEntry { Id = "just_cried", + EventStrength = 30f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -479,6 +517,7 @@ public static class HappinessEventCatalog ["just_talked_gossip"] = new HappinessCatalogEntry { Id = "just_talked_gossip", + EventStrength = 30f, Presentation = HappinessPresentationTier.Ambient, Context = HappinessContextRequirement.RelatedOptional, Life = HappinessLifePolicy.ActivityOnly, @@ -491,6 +530,7 @@ public static class HappinessEventCatalog ["just_surprised"] = new HappinessCatalogEntry { Id = "just_surprised", + EventStrength = 30f, Presentation = HappinessPresentationTier.Ambient, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -504,6 +544,7 @@ public static class HappinessEventCatalog ["just_born"] = new HappinessCatalogEntry { Id = "just_born", + EventStrength = 70f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ProjectToLife, @@ -516,6 +557,7 @@ public static class HappinessEventCatalog ["just_magnetised"] = new HappinessCatalogEntry { Id = "just_magnetised", + EventStrength = 45f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -529,6 +571,7 @@ public static class HappinessEventCatalog ["just_forced_power"] = new HappinessCatalogEntry { Id = "just_forced_power", + EventStrength = 45f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -541,6 +584,7 @@ public static class HappinessEventCatalog ["just_possessed"] = new HappinessCatalogEntry { Id = "just_possessed", + EventStrength = 45f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -554,6 +598,7 @@ public static class HappinessEventCatalog ["strange_urge"] = new HappinessCatalogEntry { Id = "strange_urge", + EventStrength = 45f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -567,6 +612,7 @@ public static class HappinessEventCatalog ["just_had_tantrum"] = new HappinessCatalogEntry { Id = "just_had_tantrum", + EventStrength = 45f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -580,6 +626,7 @@ public static class HappinessEventCatalog ["just_felt_the_divine"] = new HappinessCatalogEntry { Id = "just_felt_the_divine", + EventStrength = 45f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -592,6 +639,7 @@ public static class HappinessEventCatalog ["just_enchanted"] = new HappinessCatalogEntry { Id = "just_enchanted", + EventStrength = 45f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -605,6 +653,7 @@ public static class HappinessEventCatalog ["just_inspired"] = new HappinessCatalogEntry { Id = "just_inspired", + EventStrength = 45f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -618,6 +667,7 @@ public static class HappinessEventCatalog ["wrote_book"] = new HappinessCatalogEntry { Id = "wrote_book", + EventStrength = 65f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ProjectToLife, @@ -630,6 +680,7 @@ public static class HappinessEventCatalog ["just_became_adult"] = new HappinessCatalogEntry { Id = "just_became_adult", + EventStrength = 65f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ProjectToLife, @@ -642,6 +693,7 @@ public static class HappinessEventCatalog ["just_got_out_of_egg"] = new HappinessCatalogEntry { Id = "just_got_out_of_egg", + EventStrength = 65f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ProjectToLife, @@ -654,6 +706,7 @@ public static class HappinessEventCatalog ["just_finished_plot"] = new HappinessCatalogEntry { Id = "just_finished_plot", + EventStrength = 65f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ProjectToLife, @@ -666,6 +719,7 @@ public static class HappinessEventCatalog ["just_found_house"] = new HappinessCatalogEntry { Id = "just_found_house", + EventStrength = 55f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ProjectToLife, @@ -678,6 +732,7 @@ public static class HappinessEventCatalog ["just_lost_house"] = new HappinessCatalogEntry { Id = "just_lost_house", + EventStrength = 65f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ProjectToLife, @@ -690,6 +745,7 @@ public static class HappinessEventCatalog ["just_made_friend"] = new HappinessCatalogEntry { Id = "just_made_friend", + EventStrength = 50f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.RelatedOptional, Life = HappinessLifePolicy.ActivityOnly, @@ -703,6 +759,7 @@ public static class HappinessEventCatalog ["just_injured"] = new HappinessCatalogEntry { Id = "just_injured", + EventStrength = 65f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ProjectToLife, @@ -715,6 +772,7 @@ public static class HappinessEventCatalog ["just_cursed"] = new HappinessCatalogEntry { Id = "just_cursed", + EventStrength = 45f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -728,6 +786,7 @@ public static class HappinessEventCatalog ["starving"] = new HappinessCatalogEntry { Id = "starving", + EventStrength = 30f, Presentation = HappinessPresentationTier.Ambient, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -741,6 +800,7 @@ public static class HappinessEventCatalog ["conquered_city"] = new HappinessCatalogEntry { Id = "conquered_city", + EventStrength = 75f, Presentation = HappinessPresentationTier.Aggregate, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -753,6 +813,7 @@ public static class HappinessEventCatalog ["destroyed_city"] = new HappinessCatalogEntry { Id = "destroyed_city", + EventStrength = 35f, Presentation = HappinessPresentationTier.Aggregate, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -765,6 +826,7 @@ public static class HappinessEventCatalog ["lost_crown"] = new HappinessCatalogEntry { Id = "lost_crown", + EventStrength = 65f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ProjectToLife, @@ -777,6 +839,7 @@ public static class HappinessEventCatalog ["razed_capital"] = new HappinessCatalogEntry { Id = "razed_capital", + EventStrength = 35f, Presentation = HappinessPresentationTier.Aggregate, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -789,6 +852,7 @@ public static class HappinessEventCatalog ["lost_capital"] = new HappinessCatalogEntry { Id = "lost_capital", + EventStrength = 35f, Presentation = HappinessPresentationTier.Aggregate, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -801,6 +865,7 @@ public static class HappinessEventCatalog ["razed_city"] = new HappinessCatalogEntry { Id = "razed_city", + EventStrength = 35f, Presentation = HappinessPresentationTier.Aggregate, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -813,6 +878,7 @@ public static class HappinessEventCatalog ["lost_city"] = new HappinessCatalogEntry { Id = "lost_city", + EventStrength = 72f, Presentation = HappinessPresentationTier.Aggregate, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ActivityOnly, @@ -825,6 +891,7 @@ public static class HappinessEventCatalog ["become_alpha"] = new HappinessCatalogEntry { Id = "become_alpha", + EventStrength = 65f, Presentation = HappinessPresentationTier.Signal, Context = HappinessContextRequirement.None, Life = HappinessLifePolicy.ProjectToLife, @@ -872,6 +939,7 @@ public static class HappinessEventCatalog InterestCategory = "Unknown", VariantsWithRelated = new[] { "feels a change in happiness" }, VariantsWithoutRelated = new[] { "feels a change in happiness" }, + EventStrength = 40f, IsFallback = true }; } diff --git a/IdleSpectator/HappinessModels.cs b/IdleSpectator/HappinessModels.cs index 8d243f1..dcdf90d 100644 --- a/IdleSpectator/HappinessModels.cs +++ b/IdleSpectator/HappinessModels.cs @@ -66,6 +66,7 @@ public sealed class HappinessCatalogEntry public HappinessOverlapPolicy Overlap = HappinessOverlapPolicy.Independent; public HappinessRelationRole RelationRole = HappinessRelationRole.None; public string InterestCategory = ""; + public float EventStrength = 40f; public string CanonicalMilestoneKey = ""; public string StatusOverlapId = ""; public string[] VariantsWithRelated = Array.Empty(); diff --git a/IdleSpectator/HarnessScenarios.cs b/IdleSpectator/HarnessScenarios.cs index 4693546..30ed302 100644 --- a/IdleSpectator/HarnessScenarios.cs +++ b/IdleSpectator/HarnessScenarios.cs @@ -48,6 +48,13 @@ internal static class HarnessScenarios return Discovery(); case "world_log": return WorldLog(); + case "world_log_audit": + return WorldLogAudit(); + case "disaster_war_audit": + case "disaster_war_inventory": + return DisasterWarAudit(); + case "event_coverage": + return EventCoverage(); case "chronicle": case "chronicle_smoke": return ChronicleSmoke(); @@ -146,6 +153,7 @@ internal static class HarnessScenarios Nested("reg_action_priority", "director_action_priority"), Nested("reg_discovery", "discovery"), Nested("reg_worldlog", "world_log"), + Nested("reg_event_coverage", "event_coverage"), Nested("reg_chronicle", "chronicle_smoke"), Nested("reg_activity", "activity_idle_smoke"), Nested("reg_activity_status", "activity_status_smoke"), @@ -1069,6 +1077,56 @@ internal static class HarnessScenarios }; } + private static List WorldLogAudit() + { + return new List + { + Step("wla0", "wait_world"), + Step("wla1", "assert", expect: "world_log_audit"), + Step("wla2", "snapshot") + }; + } + + private static List DisasterWarAudit() + { + return new List + { + Step("dwa0", "wait_world"), + Step("dwa1", "assert", expect: "disaster_war_audit"), + Step("dwa2", "snapshot") + }; + } + + private static List EventCoverage() + { + return new List + { + Step("ec0", "dismiss_windows"), + Step("ec1", "wait_world"), + Step("ec2", "set_setting", expect: "enabled", value: "true"), + Step("ec3", "pick_unit", asset: "auto"), + Step("ec4", "spectator", value: "off"), + Step("ec5", "spectator", value: "on"), + Step("ec6", "focus", asset: "auto"), + + // Full authored coverage gates + Step("ec10", "assert", expect: "world_log_audit"), + Step("ec11", "assert", expect: "disaster_war_audit"), + Step("ec12", "assert", expect: "activity_status_audit"), + Step("ec13", "assert", expect: "activity_happiness_audit"), + + // Previously silent-dropped WorldLog disaster id must enter the registry + Step("ec20", "world_log_feed", asset: "disaster_tornado"), + Step("ec21", "assert", expect: "interest_has_key", value: "worldlog:disaster_tornado"), + Step("ec22", "world_log_feed", asset: "disaster_alien_invasion"), + Step("ec23", "assert", expect: "interest_has_key", value: "worldlog:disaster_alien_invasion"), + + Step("ec90", "assert", expect: "health"), + Step("ec91", "assert", expect: "no_bad"), + Step("ec99", "snapshot") + }; + } + private static List ActivityRateSample() { return new List diff --git a/IdleSpectator/InterestFeeds.cs b/IdleSpectator/InterestFeeds.cs index ca46dc8..d8c4c8e 100644 --- a/IdleSpectator/InterestFeeds.cs +++ b/IdleSpectator/InterestFeeds.cs @@ -92,11 +92,13 @@ public static class InterestFeeds return; } - if (!WorldLogInterestTable.TryGetEventStrength(message.asset_id, out float evtSeed)) + WorldLogEventEntry entry = WorldLogEventCatalog.GetOrFallback(message.asset_id); + if (!entry.CreatesInterest) { return; } + float evtSeed = entry.EventStrength; Vector3 position = message.getLocation(); Actor unit = null; if (message.hasFollowLocation()) @@ -109,32 +111,32 @@ public static class InterestFeeds return; } - string assetId = message.asset_id ?? "worldlog"; + string assetId = message.asset_id ?? entry.Id ?? "worldlog"; string kingdom = message.special1 ?? ""; string key = "worldlog:" + assetId + ":" + kingdom; float boost = PeekCivicBoost(kingdom, assetId); - bool politics = evtSeed >= 90f; + string category = string.IsNullOrEmpty(entry.Category) ? "WorldLog" : entry.Category; var candidate = new InterestCandidate { Key = key, LeadKind = InterestLeadKind.EventLed, - Category = politics ? "Politics" : "Settlement", + Category = category, Source = "worldlog", EventStrength = evtSeed + boost, VisualConfidence = unit != null ? 0.7f : 0.4f, Position = unit != null ? unit.current_position : position, FollowUnit = unit, SubjectId = SafeId(unit), - Label = WorldLogInterestTable.MakeLabel(message), + Label = entry.MakeLabel(message), AssetId = assetId, KingdomKey = kingdom, SpeciesId = unit?.asset != null ? unit.asset.id : "", CreatedAt = Time.unscaledTime, LastSeenAt = Time.unscaledTime, ExpiresAt = Time.unscaledTime + 40f, - MinWatch = politics ? 8f : 6f, - MaxWatch = politics ? 35f : 28f, + MinWatch = entry.MinWatch, + MaxWatch = entry.MaxWatch, Completion = InterestCompletionKind.FixedDwell }; InterestScoring.ScoreCheap(candidate); @@ -342,19 +344,14 @@ public static class InterestFeeds return; } - // Visible spectacle statuses amplify / extend scenes; crying extends grief. - bool spectacle = statusId == "burning" - || statusId == "possessed" - || statusId == "crying" - || statusId == "frozen" - || statusId == "shocked"; - if (!spectacle) + StatusInterestEntry entry = StatusInterestCatalog.GetOrFallback(statusId); + if (!entry.CreatesInterest) { return; } long id = SafeId(actor); - if (statusId == "crying") + if (entry.ExtendsGrief || statusId == "crying") { // Extend existing grief candidate rather than spawning a second scene. string griefPrefix = "happiness:death_"; @@ -362,20 +359,21 @@ public static class InterestFeeds return; } + string phrase = ActivityStatusProse.Phrase(statusId, gained: true, out _); string key = "status:" + statusId + ":" + id; var candidate = new InterestCandidate { Key = key, LeadKind = InterestLeadKind.EventLed, - Category = "StatusTransformation", + Category = string.IsNullOrEmpty(entry.Category) ? "StatusTransformation" : entry.Category, Source = "status", - EventStrength = 55f, + EventStrength = entry.EventStrength, VisualConfidence = 0.85f, Position = actor.current_position, FollowUnit = actor, SubjectId = id, StatusId = statusId, - Label = SafeName(actor) + " · " + statusId, + Label = SafeName(actor) + " · " + phrase, AssetId = actor.asset != null ? actor.asset.id : "", SpeciesId = actor.asset != null ? actor.asset.id : "", CreatedAt = Time.unscaledTime, @@ -659,6 +657,9 @@ public static class InterestFeeds RegisterDirect(battle); } + // Ongoing wars from WarManager (complements WorldLog war start/end). + WarInterestFeed.Tick(); + // Character-led vignette seeds (not sole one-best monopoly: register best + a few notables). WorldActivityScanner.RegisterTopCharacterCandidates(max: 4); } diff --git a/IdleSpectator/InterestScoring.cs b/IdleSpectator/InterestScoring.cs index 9e5cdf4..206a4f1 100644 --- a/IdleSpectator/InterestScoring.cs +++ b/IdleSpectator/InterestScoring.cs @@ -39,37 +39,12 @@ public static class InterestScoring 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; - } + return HappinessEventCatalog.GetOrFallback(effectId).EventStrength; } public static float EventStrengthForWorldLog(string assetId) { - return WorldLogInterestTable.TryGetEventStrength(assetId, out float seed) ? seed : 0f; + return WorldLogEventCatalog.GetOrFallback(assetId).EventStrength; } public static bool IsFillScore(float totalScore) diff --git a/IdleSpectator/StatusInterestCatalog.cs b/IdleSpectator/StatusInterestCatalog.cs new file mode 100644 index 0000000..962705e --- /dev/null +++ b/IdleSpectator/StatusInterestCatalog.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; + +namespace IdleSpectator; + +public sealed class StatusInterestEntry +{ + public string Id = ""; + public float EventStrength = 40f; + public string Category = "Status"; + public bool CreatesInterest; + public bool ExtendsGrief; + public bool IsFallback; +} + +/// +/// Authored interest policy for every live status asset. Prose stays in ActivityStatusProse. +/// +public static class StatusInterestCatalog +{ + private static readonly Dictionary Entries = + new Dictionary(StringComparer.OrdinalIgnoreCase); + + static StatusInterestCatalog() + { + // Spectacle / camera-worthy transformations + Add("burning", 70f, "StatusTransformation", true); + Add("possessed", 72f, "StatusTransformation", true); + Add("possessed_follower", 60f, "StatusTransformation", true); + Add("frozen", 65f, "StatusTransformation", true); + Add("poisoned", 58f, "StatusTransformation", true); + Add("drowning", 68f, "StatusTransformation", true); + Add("stunned", 50f, "StatusTransformation", true); + Add("rage", 62f, "StatusTransformation", true); + Add("angry", 48f, "StatusTransformation", true); + Add("cursed", 60f, "StatusTransformation", true); + 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("magnetized", 55f, "StatusTransformation", true); + Add("invincible", 56f, "StatusTransformation", true); + Add("powerup", 54f, "StatusTransformation", true); + Add("enchanted", 52f, "StatusTransformation", true); + Add("ash_fever", 50f, "StatusTransformation", true); + Add("starving", 48f, "StatusTransformation", true); + Add("surprised", 42f, "StatusTransformation", true); + Add("confused", 40f, "StatusTransformation", true); + Add("strange_urge", 45f, "StatusTransformation", true); + Add("inspired", 42f, "Emotion", true); + Add("motivated", 40f, "Emotion", true); + Add("fell_in_love", 55f, "Relationship", true); + Add("pregnant", 58f, "LifeChapter", true); + Add("pregnant_parthenogenesis", 58f, "LifeChapter", true); + Add("crying", 50f, "Grief", true, extendsGrief: true); + + // Authored but do not create interest candidates (cooldowns / ambient / brief FX) + AddNoInterest("afterglow"); + AddNoInterest("being_suspicious"); + AddNoInterest("budding"); + AddNoInterest("caffeinated"); + AddNoInterest("cough"); + AddNoInterest("dash"); + AddNoInterest("dodge"); + AddNoInterest("festive_spirit"); + AddNoInterest("flicked"); + AddNoInterest("had_bad_dream"); + AddNoInterest("had_good_dream"); + AddNoInterest("had_nightmare"); + AddNoInterest("handsome_migrant"); + AddNoInterest("just_ate"); + AddNoInterest("laughing"); + AddNoInterest("on_guard"); + AddNoInterest("recovery_combat_action"); + AddNoInterest("recovery_plot"); + AddNoInterest("recovery_social"); + AddNoInterest("recovery_spell"); + AddNoInterest("shield"); + AddNoInterest("singing"); + AddNoInterest("sleeping"); + AddNoInterest("slowness"); + AddNoInterest("spell_boost"); + AddNoInterest("spell_silence"); + AddNoInterest("swearing"); + AddNoInterest("taking_roots"); + AddNoInterest("uprooting"); + } + + private static void Add( + string id, + float strength, + string category, + bool createsInterest, + bool extendsGrief = false) + { + Entries[id] = new StatusInterestEntry + { + Id = id, + EventStrength = strength, + Category = category, + CreatesInterest = createsInterest, + ExtendsGrief = extendsGrief + }; + } + + private static void AddNoInterest(string id) + { + Add(id, 20f, "StatusAmbient", false); + } + + public static IEnumerable AuthoredIds => Entries.Keys; + + public static bool HasAuthored(string statusId) + { + return !string.IsNullOrEmpty(statusId) && Entries.ContainsKey(statusId.Trim()); + } + + public static bool TryGet(string statusId, out StatusInterestEntry entry) + { + entry = null; + if (string.IsNullOrEmpty(statusId)) + { + return false; + } + + return Entries.TryGetValue(statusId.Trim(), out entry); + } + + public static StatusInterestEntry GetOrFallback(string statusId) + { + if (TryGet(statusId, out StatusInterestEntry entry)) + { + return entry; + } + + string id = string.IsNullOrEmpty(statusId) ? "unknown" : statusId.Trim(); + return new StatusInterestEntry + { + Id = id, + EventStrength = 40f, + Category = "Status", + CreatesInterest = false, + IsFallback = true + }; + } +} diff --git a/IdleSpectator/WarInterestFeed.cs b/IdleSpectator/WarInterestFeed.cs new file mode 100644 index 0000000..fafe87e --- /dev/null +++ b/IdleSpectator/WarInterestFeed.cs @@ -0,0 +1,336 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using UnityEngine; + +namespace IdleSpectator; + +/// +/// Polls live WarManager state and registers ongoing war interest candidates. +/// +public static class WarInterestFeed +{ + public static void Tick() + { + if (AgentHarness.Busy) + { + return; + } + + object warsManager = ResolveWarsManager(); + if (warsManager == null) + { + return; + } + + IEnumerable active = InvokeEnumerable(warsManager, "getActiveWars") + ?? InvokeEnumerable(warsManager, "getWars"); + if (active == null) + { + return; + } + + int registered = 0; + foreach (object war in active) + { + if (war == null || registered >= 6) + { + break; + } + + if (TryRegisterWar(war)) + { + registered++; + } + } + } + + private static bool TryRegisterWar(object war) + { + string warTypeId = ReadWarTypeId(war); + WarTypeInterestEntry entry = WarTypeInterestCatalog.GetOrFallback(warTypeId); + if (!entry.CreatesInterest) + { + return false; + } + + object attacker = ReadPropOrField(war, "main_attacker") ?? Invoke(war, "get_main_attacker"); + object defender = ReadPropOrField(war, "main_defender") ?? Invoke(war, "get_main_defender"); + string attackerName = ReadMetaName(attacker); + string defenderName = ReadMetaName(defender); + string labelPair = string.IsNullOrEmpty(defenderName) + ? attackerName + : attackerName + " vs " + defenderName; + if (string.IsNullOrEmpty(labelPair)) + { + labelPair = ReadString(war, "name") ?? warTypeId ?? "war"; + } + + Vector3 position = Vector3.zero; + Actor follow = null; + if (!TryLocateWar(attacker, defender, out position, out follow)) + { + return false; + } + + string key = "war:" + (ReadString(war, "id") ?? labelPair) + ":" + (warTypeId ?? "normal"); + string label = WarTypeInterestCatalog.MakeLabel(entry, labelPair); + var candidate = new InterestCandidate + { + Key = key, + LeadKind = InterestLeadKind.EventLed, + Category = entry.Category, + Source = "war", + EventStrength = entry.EventStrength, + VisualConfidence = follow != null ? 0.65f : 0.35f, + Position = position, + FollowUnit = follow, + SubjectId = follow != null ? SafeActorId(follow) : 0, + Label = label, + AssetId = warTypeId ?? "war", + KingdomKey = attackerName, + CreatedAt = Time.unscaledTime, + LastSeenAt = Time.unscaledTime, + ExpiresAt = Time.unscaledTime + 25f, + MinWatch = 6f, + MaxWatch = 30f, + Completion = InterestCompletionKind.FixedDwell + }; + InterestScoring.ScoreCheap(candidate); + InterestRegistry.Upsert(candidate); + return true; + } + + private static bool TryLocateWar(object attacker, object defender, out Vector3 position, out Actor follow) + { + position = Vector3.zero; + follow = null; + if (TryKingdomAnchor(attacker, out position, out follow)) + { + return true; + } + + if (TryKingdomAnchor(defender, out position, out follow)) + { + return true; + } + + // Fall back to any living unit. + try + { + if (World.world?.units != null) + { + foreach (Actor actor in World.world.units) + { + if (actor == null || !actor.isAlive()) + { + continue; + } + + follow = actor; + position = actor.current_position; + return true; + } + } + } + catch + { + // ignore + } + + return false; + } + + private static bool TryKingdomAnchor(object kingdom, out Vector3 position, out Actor follow) + { + position = Vector3.zero; + follow = null; + if (kingdom == null) + { + return false; + } + + try + { + object capital = ReadPropOrField(kingdom, "capital") ?? Invoke(kingdom, "getCapital"); + if (capital != null) + { + object posObj = ReadPropOrField(capital, "current_position") + ?? ReadPropOrField(capital, "city_center"); + if (posObj is Vector3 v && v != Vector3.zero) + { + position = v; + } + else if (posObj is Vector2 v2) + { + position = new Vector3(v2.x, v2.y, 0f); + } + } + + object king = ReadPropOrField(kingdom, "king"); + if (king is Actor actorKing && actorKing.isAlive()) + { + follow = actorKing; + position = actorKing.current_position; + return true; + } + + if (position != Vector3.zero) + { + return true; + } + } + catch + { + // ignore + } + + return false; + } + + private static object ResolveWarsManager() + { + try + { + object world = World.world; + if (world == null) + { + return null; + } + + object wars = ReadPropOrField(world, "wars"); + if (wars != null) + { + return wars; + } + + return typeof(AssetManager).Assembly.GetType("WarManager"); + } + catch + { + return null; + } + } + + private static string ReadWarTypeId(object war) + { + try + { + object warType = ReadPropOrField(war, "war_type") ?? Invoke(war, "get_war_type"); + if (warType == null) + { + return "normal"; + } + + if (warType is string s) + { + return s; + } + + string id = ReadString(warType, "id"); + return string.IsNullOrEmpty(id) ? "normal" : id; + } + catch + { + return "normal"; + } + } + + private static string ReadMetaName(object meta) + { + if (meta == null) + { + return ""; + } + + try + { + object name = Invoke(meta, "getName") ?? ReadPropOrField(meta, "name") ?? ReadString(meta, "name"); + return name as string ?? ""; + } + catch + { + return ""; + } + } + + private static IEnumerable InvokeEnumerable(object target, string methodName) + { + try + { + MethodInfo method = target.GetType().GetMethod(methodName, Type.EmptyTypes); + object result = method?.Invoke(target, null); + return result as IEnumerable; + } + catch + { + return null; + } + } + + private static object Invoke(object target, string methodName) + { + if (target == null || string.IsNullOrEmpty(methodName)) + { + return null; + } + + try + { + MethodInfo method = target.GetType().GetMethod(methodName, Type.EmptyTypes); + return method?.Invoke(target, null); + } + catch + { + return null; + } + } + + private static object ReadPropOrField(object target, string name) + { + if (target == null || string.IsNullOrEmpty(name)) + { + return null; + } + + try + { + Type type = target.GetType(); + PropertyInfo prop = type.GetProperty(name); + if (prop != null) + { + return prop.GetValue(target, null); + } + + FieldInfo field = type.GetField(name); + return field?.GetValue(target); + } + catch + { + return null; + } + } + + private static string ReadString(object target, string name) + { + return ReadPropOrField(target, name) as string; + } + + private static long SafeActorId(Actor actor) + { + if (actor == null) + { + return 0; + } + + try + { + return actor.getID(); + } + catch + { + return 0; + } + } +} diff --git a/IdleSpectator/WarTypeInterestCatalog.cs b/IdleSpectator/WarTypeInterestCatalog.cs new file mode 100644 index 0000000..e5a509a --- /dev/null +++ b/IdleSpectator/WarTypeInterestCatalog.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; + +namespace IdleSpectator; + +public sealed class WarTypeInterestEntry +{ + public string Id = ""; + public float EventStrength = 85f; + public string Category = "Politics"; + public string LabelTemplate = "War ({id})"; + public bool CreatesInterest = true; + public bool IsFallback; +} + +/// Authored interest overlay for every live war_types_library asset. +public static class WarTypeInterestCatalog +{ + private static readonly Dictionary Entries = + new Dictionary(StringComparer.OrdinalIgnoreCase); + + static WarTypeInterestCatalog() + { + Add("normal", 90f, "War: {a}"); + Add("spite", 88f, "Spite war: {a}"); + Add("inspire", 86f, "Inspired war: {a}"); + Add("rebellion", 92f, "Rebellion: {a}"); + Add("whisper_of_war", 84f, "Whisper of war: {a}"); + } + + private static void Add(string id, float strength, string labelTemplate) + { + Entries[id] = new WarTypeInterestEntry + { + Id = id, + EventStrength = strength, + Category = "Politics", + LabelTemplate = labelTemplate, + CreatesInterest = true + }; + } + + public static IEnumerable AuthoredIds => Entries.Keys; + + public static bool HasAuthored(string warTypeId) + { + return !string.IsNullOrEmpty(warTypeId) && Entries.ContainsKey(warTypeId.Trim()); + } + + public static bool TryGet(string warTypeId, out WarTypeInterestEntry entry) + { + entry = null; + if (string.IsNullOrEmpty(warTypeId)) + { + return false; + } + + return Entries.TryGetValue(warTypeId.Trim(), out entry); + } + + public static WarTypeInterestEntry GetOrFallback(string warTypeId) + { + if (TryGet(warTypeId, out WarTypeInterestEntry entry)) + { + return entry; + } + + string id = string.IsNullOrEmpty(warTypeId) ? "unknown" : warTypeId.Trim(); + return new WarTypeInterestEntry + { + Id = id, + EventStrength = 85f, + Category = "Politics", + LabelTemplate = "War ({id}): {a}", + CreatesInterest = true, + IsFallback = true + }; + } + + public static string MakeLabel(WarTypeInterestEntry entry, string special1) + { + if (entry == null) + { + return "War"; + } + + string template = string.IsNullOrEmpty(entry.LabelTemplate) ? "War ({id})" : entry.LabelTemplate; + return template + .Replace("{id}", entry.Id ?? "") + .Replace("{a}", special1 ?? ""); + } +} diff --git a/IdleSpectator/WorldLogEventCatalog.cs b/IdleSpectator/WorldLogEventCatalog.cs new file mode 100644 index 0000000..756e7e7 --- /dev/null +++ b/IdleSpectator/WorldLogEventCatalog.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections.Generic; + +namespace IdleSpectator; + +public sealed class WorldLogEventEntry +{ + public string Id = ""; + public float EventStrength = 40f; + public string Category = "WorldLog"; + public bool CreatesInterest = true; + public bool ChronicleEligible; + public float MinWatch = 6f; + public float MaxWatch = 28f; + public string LabelTemplate = "{id}"; + public bool IsFallback; + + public string MakeLabel(string special1, string special2) + { + string a = special1 ?? ""; + string b = special2 ?? ""; + string template = string.IsNullOrEmpty(LabelTemplate) ? "{id}" : LabelTemplate; + return template + .Replace("{id}", Id ?? "") + .Replace("{a}", a) + .Replace("{b}", b) + .Replace("{special1}", a) + .Replace("{special2}", b); + } + + public string MakeLabel(WorldLogMessage message) + { + if (message == null) + { + return MakeLabel("", ""); + } + + return MakeLabel(message.special1, message.special2); + } +} + +/// +/// Authored policy for every live WorldLog asset. Game library is inventory; this is presentation/scoring only. +/// +public static class WorldLogEventCatalog +{ + private static readonly Dictionary Entries = + new Dictionary(StringComparer.OrdinalIgnoreCase); + + static WorldLogEventCatalog() + { + // Kings + Add("king_new", 78f, "Politics", true, "New king: {b} ({a})", 6f, 28f); + Add("king_left", 65f, "Politics", true, "King left: {b} ({a})", 6f, 28f); + Add("king_fled_capital", 70f, "Politics", true, "King fled capital: {b}", 6f, 28f); + Add("king_fled_city", 68f, "Politics", true, "King fled city: {b}", 6f, 28f); + Add("king_dead", 80f, "Politics", true, "King died: {b} ({a})", 6f, 28f); + Add("king_killed", 85f, "Politics", true, "King killed: {b} ({a})", 6f, 28f); + + // Favorites + Add("favorite_dead", 82f, "Politics", true, "Favorite fell: {a}", 6f, 28f); + Add("favorite_killed", 85f, "Politics", true, "Favorite fell: {a}", 6f, 28f); + + // Cities / kingdoms / clans / wars / alliances + Add("city_new", 72f, "Settlement", true, "New city: {a}", 6f, 28f); + Add("log_city_revolted", 90f, "Politics", true, "Revolt: {a}", 8f, 35f); + Add("city_destroyed", 92f, "Settlement", true, "City destroyed: {a}", 8f, 35f); + Add("diplomacy_war_ended", 72f, "Politics", true, "War ended: {a}", 6f, 28f); + Add("diplomacy_war_started", 100f, "Politics", true, "War: {a} vs {b}", 8f, 35f); + Add("total_war_started", 100f, "Politics", true, "Total war: {a}", 8f, 35f); + Add("alliance_new", 70f, "Politics", true, "Alliance: {a}", 6f, 28f); + Add("alliance_dissolved", 68f, "Politics", true, "Alliance dissolved: {a}", 6f, 28f); + Add("kingdom_new", 75f, "Settlement", true, "New kingdom: {a}", 6f, 28f); + Add("kingdom_destroyed", 100f, "Politics", true, "Kingdom fell: {a}", 8f, 35f); + Add("kingdom_shattered", 98f, "Politics", true, "Kingdom shattered: {a}", 8f, 35f); + Add("kingdom_fractured", 95f, "Politics", true, "Kingdom fractured: {a}", 8f, 35f); + Add("kingdom_royal_clan_new", 74f, "Politics", true, "Royal clan: {a}", 6f, 28f); + Add("kingdom_royal_clan_changed", 72f, "Politics", true, "Royal clan: {a}", 6f, 28f); + Add("kingdom_royal_clan_dead", 76f, "Politics", true, "Royal clan ended: {a}", 6f, 28f); + + // Disasters (WorldLog ids; linked to disasters library by naming) + Add("disaster_tornado", 95f, "Disaster", true, "Tornado: {a}", 8f, 35f); + Add("disaster_meteorite", 95f, "Disaster", true, "Meteorite: {a}", 8f, 35f); + Add("disaster_hellspawn", 96f, "Disaster", true, "Hellspawn: {a}", 8f, 35f); + Add("disaster_earthquake", 95f, "Disaster", true, "Earthquake: {a}", 8f, 35f); + Add("disaster_greg_abominations", 96f, "Disaster", true, "Greg abominations: {a}", 8f, 35f); + Add("disaster_ice_ones", 95f, "Disaster", true, "Ice ones awaken: {a}", 8f, 35f); + Add("disaster_sudden_snowman", 90f, "Disaster", true, "Sudden snowman: {a}", 8f, 35f); + Add("disaster_garden_surprise", 90f, "Disaster", true, "Garden surprise: {a}", 8f, 35f); + Add("disaster_dragon_from_farlands", 98f, "Disaster", true, "Dragon from the farlands: {a}", 8f, 35f); + Add("disaster_bandits", 92f, "Disaster", true, "Bandit raid: {a}", 8f, 35f); + Add("disaster_alien_invasion", 97f, "Disaster", true, "Alien invasion: {a}", 8f, 35f); + Add("disaster_biomass", 96f, "Disaster", true, "Biomass outbreak: {a}", 8f, 35f); + Add("disaster_tumor", 96f, "Disaster", true, "Tumor outbreak: {a}", 8f, 35f); + Add("disaster_heatwave", 88f, "Disaster", true, "Heatwave: {a}", 8f, 35f); + Add("disaster_evil_mage", 94f, "Disaster", true, "Evil mage: {a}", 8f, 35f); + Add("disaster_underground_necromancer", 95f, "Disaster", true, "Underground necromancer: {a}", 8f, 35f); + Add("disaster_mad_thoughts", 90f, "Disaster", true, "Mad thoughts: {a}", 8f, 35f); + + // Harness / internal - authored but never owns camera interest + Add("auto_tester", 10f, "Harness", false, "Auto tester: {a}", 2f, 6f); + } + + private static void Add( + string id, + float strength, + string category, + bool createsInterest, + string labelTemplate, + float minWatch, + float maxWatch) + { + Entries[id] = new WorldLogEventEntry + { + Id = id, + EventStrength = strength, + Category = category, + CreatesInterest = createsInterest, + ChronicleEligible = createsInterest && strength >= 65f, + LabelTemplate = labelTemplate, + MinWatch = minWatch, + MaxWatch = maxWatch + }; + } + + public static IEnumerable AuthoredIds => Entries.Keys; + + public static bool HasAuthored(string assetId) + { + return !string.IsNullOrEmpty(assetId) && Entries.ContainsKey(assetId.Trim()); + } + + public static bool TryGet(string assetId, out WorldLogEventEntry entry) + { + entry = null; + if (string.IsNullOrEmpty(assetId)) + { + return false; + } + + return Entries.TryGetValue(assetId.Trim(), out entry); + } + + public static WorldLogEventEntry GetOrFallback(string assetId) + { + if (TryGet(assetId, out WorldLogEventEntry entry)) + { + return entry; + } + + string id = string.IsNullOrEmpty(assetId) ? "unknown" : assetId.Trim(); + float strength = 40f; + string category = "WorldLog"; + if (id.IndexOf("disaster", StringComparison.OrdinalIgnoreCase) >= 0) + { + strength = 95f; + category = "Disaster"; + } + + return new WorldLogEventEntry + { + Id = id, + EventStrength = strength, + Category = category, + CreatesInterest = true, + ChronicleEligible = strength >= 65f, + LabelTemplate = "{id}: {a}", + MinWatch = strength >= 90f ? 8f : 6f, + MaxWatch = strength >= 90f ? 35f : 28f, + IsFallback = true + }; + } + + public static bool TryGetEventStrength(string assetId, out float eventStrength) + { + WorldLogEventEntry entry = GetOrFallback(assetId); + eventStrength = entry.EventStrength; + return !string.IsNullOrEmpty(assetId); + } + + public static string MakeLabel(WorldLogMessage message) + { + if (message == null) + { + return "event"; + } + + return GetOrFallback(message.asset_id).MakeLabel(message); + } +} diff --git a/IdleSpectator/WorldLogEventHarness.cs b/IdleSpectator/WorldLogEventHarness.cs new file mode 100644 index 0000000..2a42e63 --- /dev/null +++ b/IdleSpectator/WorldLogEventHarness.cs @@ -0,0 +1,216 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text; +using UnityEngine; + +namespace IdleSpectator; + +public sealed class WorldLogEventAuditResult +{ + public bool Passed; + public string Detail = ""; + public int Total; + public int MissingAuthored; + public int OrphanAuthored; + public int EmptyLabel; +} + +/// +/// Exhaustive live world_log_library audit for authored WorldLog event catalog coverage. +/// +public static class WorldLogEventHarness +{ + public static WorldLogEventAuditResult RunAudit(string outputDirectory) + { + List liveIds = ActivityAssetCatalog.EnumerateLiveWorldLogIds(); + var authored = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (string id in WorldLogEventCatalog.AuthoredIds) + { + authored.Add(id); + } + + int missingAuthored = 0; + int orphanAuthored = 0; + int emptyLabel = 0; + var tsv = new StringBuilder(); + tsv.AppendLine( + "id\tgroup\tlocale\tpath_icon\tdisaster_link\tauthored\tevent_strength\tcategory\tcreates_interest\tlabel_sample\tnotes"); + + var liveSet = new HashSet(StringComparer.OrdinalIgnoreCase); + for (int i = 0; i < liveIds.Count; i++) + { + string id = liveIds[i]; + liveSet.Add(id); + WorldLogAsset asset = ActivityAssetCatalog.TryGetWorldLogAsset(id); + bool hasAuthored = WorldLogEventCatalog.HasAuthored(id); + WorldLogEventEntry entry = WorldLogEventCatalog.GetOrFallback(id); + if (!hasAuthored || entry.IsFallback) + { + missingAuthored++; + } + + string group = ReadStringField(asset, "group_id") + ?? ReadStringField(asset, "history_group") + ?? ReadStringField(asset, "group") + ?? ""; + string locale = ""; + try + { + locale = asset != null ? (asset.getLocaleID() ?? "") : ""; + } + catch + { + locale = ReadStringField(asset, "path_locale") ?? ""; + } + + string pathIcon = ReadStringField(asset, "path_icon") ?? ""; + string disasterLink = ReadStringField(asset, "disaster_id") + ?? ReadStringField(asset, "linked_disaster") + ?? ""; + if (string.IsNullOrEmpty(disasterLink) && id.IndexOf("disaster", StringComparison.OrdinalIgnoreCase) >= 0) + { + disasterLink = "id_hint"; + } + + string labelSample = entry.MakeLabel("Alpha", "Beta"); + if (string.IsNullOrEmpty(labelSample)) + { + emptyLabel++; + } + + string notes = "ok"; + if (!hasAuthored || entry.IsFallback) + { + notes = "missing_authored"; + } + else if (string.IsNullOrEmpty(labelSample)) + { + notes = "empty_label"; + } + + tsv.Append(id).Append('\t') + .Append(Escape(group)).Append('\t') + .Append(Escape(locale)).Append('\t') + .Append(Escape(pathIcon)).Append('\t') + .Append(Escape(disasterLink)).Append('\t') + .Append(hasAuthored ? "1" : "0").Append('\t') + .Append(entry.EventStrength.ToString("0.#")).Append('\t') + .Append(Escape(entry.Category)).Append('\t') + .Append(entry.CreatesInterest ? "1" : "0").Append('\t') + .Append(Escape(labelSample)).Append('\t') + .Append(notes).AppendLine(); + } + + foreach (string id in authored) + { + if (!liveSet.Contains(id)) + { + orphanAuthored++; + tsv.Append(id).Append("\t\t\t\t\t1\t0\t\t0\t\torphan_authored").AppendLine(); + } + } + + try + { + if (!string.IsNullOrEmpty(outputDirectory)) + { + Directory.CreateDirectory(outputDirectory); + File.WriteAllText( + Path.Combine(outputDirectory, "world-log-event-audit.tsv"), + tsv.ToString()); + } + } + catch + { + // Disk may be unavailable in some harness contexts. + } + + bool passed = missingAuthored == 0 && orphanAuthored == 0 && emptyLabel == 0; + return new WorldLogEventAuditResult + { + Passed = passed, + Total = liveIds.Count, + MissingAuthored = missingAuthored, + OrphanAuthored = orphanAuthored, + EmptyLabel = emptyLabel, + Detail = + $"total={liveIds.Count} missingAuthored={missingAuthored} orphanAuthored={orphanAuthored} " + + $"emptyLabel={emptyLabel}" + }; + } + + public static string DumpDisasterWarInventory(string outputDirectory) + { + var tsv = new StringBuilder(); + tsv.AppendLine("kind\tid\tnotes"); + List disasters = ActivityAssetCatalog.EnumerateLiveDisasterIds(); + for (int i = 0; i < disasters.Count; i++) + { + tsv.Append("disaster\t").Append(disasters[i]).Append("\tok").AppendLine(); + } + + List warTypes = ActivityAssetCatalog.EnumerateLiveWarTypeIds(); + for (int i = 0; i < warTypes.Count; i++) + { + tsv.Append("war_type\t").Append(warTypes[i]).Append("\tok").AppendLine(); + } + + try + { + if (!string.IsNullOrEmpty(outputDirectory)) + { + Directory.CreateDirectory(outputDirectory); + File.WriteAllText( + Path.Combine(outputDirectory, "disaster-war-inventory.tsv"), + tsv.ToString()); + } + } + catch + { + // ignore + } + + return $"disasters={disasters.Count} war_types={warTypes.Count}"; + } + + private static string ReadStringField(object asset, string fieldName) + { + if (asset == null || string.IsNullOrEmpty(fieldName)) + { + return null; + } + + try + { + FieldInfo field = asset.GetType().GetField(fieldName); + if (field != null) + { + return field.GetValue(asset) as string; + } + + PropertyInfo prop = asset.GetType().GetProperty(fieldName); + if (prop != null) + { + return prop.GetValue(asset, null) as string; + } + } + catch + { + // ignore + } + + return null; + } + + private static string Escape(string value) + { + if (string.IsNullOrEmpty(value)) + { + return ""; + } + + return value.Replace('\t', ' ').Replace('\n', ' ').Replace('\r', ' '); + } +} diff --git a/IdleSpectator/WorldLogInterestTable.cs b/IdleSpectator/WorldLogInterestTable.cs index e1af40b..b08df8a 100644 --- a/IdleSpectator/WorldLogInterestTable.cs +++ b/IdleSpectator/WorldLogInterestTable.cs @@ -1,133 +1,21 @@ using System.Collections.Generic; -using UnityEngine; namespace IdleSpectator; /// -/// WorldLog asset ids → EventStrength seeds (score-native; no InterestTier). +/// Compatibility facade over . Prefer the catalog directly. /// public static class WorldLogInterestTable { - private static readonly Dictionary EventSeeds = new Dictionary - { - // World-shaping - ["diplomacy_war_started"] = 100f, - ["total_war_started"] = 100f, - ["kingdom_destroyed"] = 100f, - ["kingdom_shattered"] = 98f, - ["kingdom_fractured"] = 95f, - ["city_destroyed"] = 92f, - ["log_city_revolted"] = 90f, - - // Leadership / politics / favorites / founding - ["kingdom_new"] = 75f, - ["city_new"] = 72f, - ["king_new"] = 78f, - ["king_dead"] = 80f, - ["king_killed"] = 85f, - ["king_fled_capital"] = 70f, - ["king_fled_city"] = 68f, - ["king_left"] = 65f, - ["alliance_new"] = 70f, - ["alliance_dissolved"] = 68f, - ["diplomacy_war_ended"] = 72f, - ["favorite_dead"] = 82f, - ["favorite_killed"] = 85f, - ["kingdom_royal_clan_new"] = 74f, - ["kingdom_royal_clan_changed"] = 72f, - ["kingdom_royal_clan_dead"] = 76f, - - // Biosphere footnotes - ["race_dead"] = 40f - }; - public static bool TryGetEventStrength(string assetId, out float eventStrength) { - if (string.IsNullOrEmpty(assetId)) - { - eventStrength = 0f; - return false; - } - - if (EventSeeds.TryGetValue(assetId, out eventStrength)) - { - return true; - } - - if (assetId.Contains("disaster") || assetId.StartsWith("worldlog_disaster")) - { - eventStrength = 95f; - return true; - } - - eventStrength = 0f; - return false; + return WorldLogEventCatalog.TryGetEventStrength(assetId, out eventStrength); } public static string MakeLabel(WorldLogMessage message) { - string id = message.asset_id ?? "event"; - string a = message.special1; - string b = message.special2; - - switch (id) - { - case "diplomacy_war_started": - return $"War: {a} vs {b}"; - case "total_war_started": - return $"Total war: {a}"; - case "kingdom_destroyed": - return $"Kingdom fell: {a}"; - case "kingdom_shattered": - return $"Kingdom shattered: {a}"; - case "kingdom_fractured": - return $"Kingdom fractured: {a}"; - case "city_destroyed": - return $"City destroyed: {a}"; - case "log_city_revolted": - return $"Revolt: {a}"; - case "kingdom_new": - return $"New kingdom: {a}"; - case "city_new": - return $"New city: {a}"; - case "king_new": - return $"New king: {b} ({a})"; - case "king_dead": - return $"King died: {b} ({a})"; - case "king_killed": - return $"King killed: {b} ({a})"; - case "king_fled_capital": - return $"King fled capital: {b}"; - case "king_fled_city": - return $"King fled city: {b}"; - case "alliance_new": - return $"Alliance: {a}"; - case "alliance_dissolved": - return $"Alliance dissolved: {a}"; - case "diplomacy_war_ended": - return $"War ended: {a}"; - case "kingdom_royal_clan_new": - case "kingdom_royal_clan_changed": - return $"Royal clan: {a}"; - case "kingdom_royal_clan_dead": - return $"Royal clan ended: {a}"; - case "race_dead": - return $"Species extinct: {a}"; - case "favorite_dead": - case "favorite_killed": - return $"Favorite fell: {a}"; - default: - if (!string.IsNullOrEmpty(a) && !string.IsNullOrEmpty(b)) - { - return $"{id}: {a} / {b}"; - } - - if (!string.IsNullOrEmpty(a)) - { - return $"{id}: {a}"; - } - - return id; - } + return WorldLogEventCatalog.MakeLabel(message); } + + public static IEnumerable AuthoredIds => WorldLogEventCatalog.AuthoredIds; } diff --git a/IdleSpectator/WorldLogPatches.cs b/IdleSpectator/WorldLogPatches.cs index 00d74ee..6608d2f 100644 --- a/IdleSpectator/WorldLogPatches.cs +++ b/IdleSpectator/WorldLogPatches.cs @@ -14,17 +14,13 @@ public static class WorldLogMessageAddPatch return; } - if (!WorldLogInterestTable.TryGetEventStrength(pMessage.asset_id, out float evtSeed)) + WorldLogEventEntry entry = WorldLogEventCatalog.GetOrFallback(pMessage.asset_id); + // Chronicle notable world events only (authored ChronicleEligible / story-strength+). + if (!entry.ChronicleEligible) { return; } - // Chronicle notable world events (story-strength and above); skip footnotes. - if (evtSeed < 65f) - { - return; - } - - Chronicle.NoteWorldLog(pMessage, WorldLogInterestTable.MakeLabel(pMessage)); + Chronicle.NoteWorldLog(pMessage, entry.MakeLabel(pMessage)); } } diff --git a/IdleSpectator/mod.json b/IdleSpectator/mod.json index c45dcce..f2d6d8e 100644 --- a/IdleSpectator/mod.json +++ b/IdleSpectator/mod.json @@ -1,7 +1,7 @@ { "name": "IdleSpectator", "author": "dazed", - "version": "0.16.8", + "version": "0.16.10", "description": "AFK Idle Spectator (I) + Lore (L). Event-centered scene director + happiness Activity/Life logging.", "GUID": "com.dazed.idlespectator" } diff --git a/scripts/harness-run.sh b/scripts/harness-run.sh index d732db8..0ebd665 100755 --- a/scripts/harness-run.sh +++ b/scripts/harness-run.sh @@ -37,6 +37,7 @@ REGRESSION_SCENARIOS=( director_action_priority discovery world_log + event_coverage chronicle_smoke activity_idle_smoke activity_status_smoke