diff --git a/IdleSpectator/AgentHarness.cs b/IdleSpectator/AgentHarness.cs index cffc6c5..a404d49 100644 --- a/IdleSpectator/AgentHarness.cs +++ b/IdleSpectator/AgentHarness.cs @@ -3753,11 +3753,11 @@ public static class AgentHarness source = "boat"; break; case "building": - id = "building_destroy"; - key = "building:destroy:harness:" + EventFeedUtil.SafeId(unit); - label = EventReason.BuildingFalls("harness_hall", unit); - strength = 88f; - category = "Spectacle"; + id = "building_complete"; + key = "building:complete:harness:" + EventFeedUtil.SafeId(unit); + label = EventReason.BuildingComplete(unit, "temple_human", spectacle: true); + strength = 86f; + category = "Settlement"; source = "building"; break; case "spell": @@ -7041,6 +7041,205 @@ public static class AgentHarness + $"familyBad={familyBad} siblingBad={siblingBad} sample='{(ids.Count > 0 ? ActivityAssetCatalog.FormatCitizenJobIdLabel(ids[0]) : "")}'"; break; } + case "library_asset_labels_ok": + { + // Every live library id used by EventReason.Library must resolve to a tip + // without snake_case, and item tips must not use "forges" except forge-y ids. + int total = 0; + int empty = 0; + int underscored = 0; + int forgeBad = 0; + var lines = new List { "kind\tid\tdisplay\tsample_label" }; + IReadOnlyList kinds = LibraryAssetNames.AllKinds; + for (int k = 0; k < kinds.Count; k++) + { + string kind = kinds[k]; + List ids = LibraryAssetNames.EnumerateLiveIds(kind); + for (int i = 0; i < ids.Count; i++) + { + string id = ids[i]; + if (string.IsNullOrEmpty(id)) + { + continue; + } + + total++; + string display = LibraryAssetNames.DisplayName(kind, id); + string sample = EventReason.Library(null, kind, id); + lines.Add(kind + "\t" + id + "\t" + display + "\t" + sample); + if (string.IsNullOrEmpty(display)) + { + empty++; + } + + if (!string.IsNullOrEmpty(display) && display.IndexOf('_') >= 0) + { + underscored++; + } + + if (kind == "item") + { + bool forgey = id.IndexOf("forge", StringComparison.OrdinalIgnoreCase) >= 0 + || id.IndexOf("smith", StringComparison.OrdinalIgnoreCase) >= 0 + || id.IndexOf("anvil", StringComparison.OrdinalIgnoreCase) >= 0; + string verb = LibraryAssetNames.ItemVerb(id); + if (!forgey + && string.Equals(verb, "forges", StringComparison.OrdinalIgnoreCase)) + { + forgeBad++; + } + } + } + } + + try + { + System.IO.File.WriteAllLines( + System.IO.Path.Combine(HarnessDir(), "library-asset-labels.tsv"), + lines); + } + catch + { + // diagnostic dump only + } + + pass = total > 0 && empty == 0 && underscored == 0 && forgeBad == 0; + detail = + $"assets={total} empty={empty} underscored={underscored} forgeBad={forgeBad} " + + $"kinds={kinds.Count}"; + break; + } + case "library_ambient_policy_ok": + { + // Signal-only libraries: ambient dials must be B; phenotypes all B; + // spectacle spells stay A; utility spells stay B. + int dialBad = 0; + int phenotypeCamera = 0; + int spellFxCamera = 0; + int spellSpectacleMissing = 0; + int signalCamera = 0; + var lines = new List + { + "domain\tid\tcreates_interest\tstrength\tnotes" + }; + + string[] signalOnlyKeys = + { + "spell", "power", "item", "gene", "phenotype", "worldLaw", "subspeciesTrait", + "biome", "cultureTrait", "religionTrait", "clanTrait", "languageTrait" + }; + for (int i = 0; i < signalOnlyKeys.Length; i++) + { + string key = signalOnlyKeys[i]; + if (EventCatalogConfig.TryGetLibraryDefaults(key, out LibraryCatalogDefaults dial) + && dial != null + && dial.AmbientCreatesInterest) + { + dialBad++; + lines.Add(key + "\t*\t\t\tambientCreatesInterest_true"); + } + } + + foreach (string id in EventCatalog.Phenotype.AuthoredIds) + { + DiscreteEventEntry e = EventCatalog.Phenotype.GetOrFallback(id); + lines.Add("phenotypes\t" + id + "\t" + e.CreatesInterest + "\t" + + e.EventStrength.ToString("0.#") + "\t"); + if (e.CreatesInterest) + { + phenotypeCamera++; + } + } + + string[] fxSpells = + { + "cast_silence", "cast_grass_seeds", "spawn_vegetation", "spawn_skeleton", + "cast_shield", "cast_cure" + }; + for (int i = 0; i < fxSpells.Length; i++) + { + DiscreteEventEntry e = EventCatalog.Spell.GetOrFallback(fxSpells[i]); + lines.Add("spells\t" + fxSpells[i] + "\t" + e.CreatesInterest + "\t" + + e.EventStrength.ToString("0.#") + "\tfx"); + if (e.CreatesInterest) + { + spellFxCamera++; + } + } + + string[] spectacleSpells = + { + "summon_lightning", "summon_tornado", "cast_curse", "cast_fire", + "cast_blood_rain", "teleport" + }; + for (int i = 0; i < spectacleSpells.Length; i++) + { + DiscreteEventEntry e = EventCatalog.Spell.GetOrFallback(spectacleSpells[i]); + lines.Add("spells\t" + spectacleSpells[i] + "\t" + e.CreatesInterest + "\t" + + e.EventStrength.ToString("0.#") + "\tspectacle"); + if (!e.CreatesInterest) + { + spellSpectacleMissing++; + } + else + { + signalCamera++; + } + } + + // Spot-check Signal classes still have camera rows after demote. + int geneSignal = 0; + foreach (string id in EventCatalog.Gene.AuthoredIds) + { + if (EventCatalog.Gene.GetOrFallback(id).CreatesInterest) + { + geneSignal++; + } + } + + int lawSignal = 0; + foreach (string id in EventCatalog.WorldLaw.AuthoredIds) + { + if (EventCatalog.WorldLaw.GetOrFallback(id).CreatesInterest) + { + lawSignal++; + } + } + + int itemSignal = 0; + foreach (string id in EventCatalog.Item.AuthoredIds) + { + if (EventCatalog.Item.GetOrFallback(id).CreatesInterest) + { + itemSignal++; + } + } + + try + { + System.IO.File.WriteAllLines( + System.IO.Path.Combine(HarnessDir(), "library-ambient-policy.tsv"), + lines); + } + catch + { + // diagnostic dump only + } + + pass = dialBad == 0 + && phenotypeCamera == 0 + && spellFxCamera == 0 + && spellSpectacleMissing == 0 + && geneSignal > 0 + && lawSignal > 0 + && itemSignal > 0 + && signalCamera == spectacleSpells.Length; + detail = + $"dialBad={dialBad} phenotypeCamera={phenotypeCamera} spellFxCamera={spellFxCamera} " + + $"spellSpectacleMissing={spellSpectacleMissing} geneSignal={geneSignal} " + + $"lawSignal={lawSignal} itemSignal={itemSignal} spectacleOk={signalCamera}"; + break; + } case "dossier_not_contains": { string needle = string.IsNullOrEmpty(cmd.value) ? cmd.label : cmd.value; diff --git a/IdleSpectator/CatalogCoverageAudit.cs b/IdleSpectator/CatalogCoverageAudit.cs index 9d59110..98d6d9e 100644 --- a/IdleSpectator/CatalogCoverageAudit.cs +++ b/IdleSpectator/CatalogCoverageAudit.cs @@ -316,6 +316,8 @@ public static class DomainEventHarness && spells.Passed && powers.Passed && decisions.Passed && items.Passed && subspTraits.Passed && genes.Passed && phenotypes.Passed && worldLaws.Passed && biomes.Passed + && cultureTraits.Passed && religionTraits.Passed + && clanTraits.Passed && languageTraits.Passed && relMissing == 0 && relTotal > 0 && decisionProseMissing == 0 && decisionStrengthMissing == 0 && decisionProseTotal > 0; @@ -323,7 +325,10 @@ public static class DomainEventHarness $"{plots.Detail}; {eras.Detail}; {books.Detail}; {traits.Detail}; " + $"{spells.Detail}; {powers.Detail}; {decisions.Detail}; {items.Detail}; " + $"{subspTraits.Detail}; {genes.Detail}; {phenotypes.Detail}; " - + $"{worldLaws.Detail}; {biomes.Detail}; relationship: authored={relTotal} missing={relMissing}; " + + $"{worldLaws.Detail}; {biomes.Detail}; " + + $"{cultureTraits.Detail}; {religionTraits.Detail}; " + + $"{clanTraits.Detail}; {languageTraits.Detail}; " + + $"relationship: authored={relTotal} missing={relMissing}; " + $"decision_prose: camera={decisionProseTotal} missing={decisionProseMissing} " + $"strengthMissing={decisionStrengthMissing} catalog={EventCatalogConfig.LoadedFromFile}"; return new DomainEventAuditResult { Passed = passed, Detail = detail }; diff --git a/IdleSpectator/EventCatalogConfig.cs b/IdleSpectator/EventCatalogConfig.cs index db65f8a..99dc2d2 100644 --- a/IdleSpectator/EventCatalogConfig.cs +++ b/IdleSpectator/EventCatalogConfig.cs @@ -399,7 +399,8 @@ public static class EventCatalogConfig string[] keys = { - "spell", "power", "item", "gene", "phenotype", "worldLaw", "subspeciesTrait", "biome" + "spell", "power", "item", "gene", "phenotype", "worldLaw", "subspeciesTrait", "biome", + "cultureTrait", "religionTrait", "clanTrait", "languageTrait" }; for (int i = 0; i < keys.Length; i++) { @@ -415,7 +416,8 @@ public static class EventCatalogConfig SignalStrength = ExtractJsonFloat(obj, "signalStrength", 80f), Category = ExtractJsonString(obj, "category") ?? "Event", Label = ExtractJsonString(obj, "label") ?? "{a} · {id}", - AmbientCreatesInterest = ExtractJsonBool(obj, "ambientCreatesInterest", true) + // Large libraries default Signal-only; JSON must opt into ambient A. + AmbientCreatesInterest = ExtractJsonBool(obj, "ambientCreatesInterest", false) }; string[] ids = ExtractStringArray(obj, "signalIds"); for (int j = 0; j < ids.Length; j++) diff --git a/IdleSpectator/EventLivePipelinesHarness.cs b/IdleSpectator/EventLivePipelinesHarness.cs index f268951..6d5b158 100644 --- a/IdleSpectator/EventLivePipelinesHarness.cs +++ b/IdleSpectator/EventLivePipelinesHarness.cs @@ -46,9 +46,11 @@ public static class EventLivePipelinesHarness RunDisastersViaWorldLog(unit, fails); RunWar(unit, fails); RunEra(fails); + RunEarthquake(unit, fails); RunBook(unit, fails); RunPlot(unit, fails); RunBuildingDestroy(unit, fails); + RunBuildingComplete(unit, fails); RunBoatSmoke(unit, fails); RunCombatSmoke(unit, fails); RunMeta(unit, fails); @@ -579,6 +581,69 @@ public static class EventLivePipelinesHarness } } + private static void RunEarthquake(Actor unit, List fails) + { + InterestDropLog.Clear(); + InterestRegistry.ForceExpireContaining("earthquake:", Time.unscaledTime); + try + { + WorldTile tile = unit != null ? unit.current_tile : null; + if (tile == null) + { + try + { + tile = World.world?.tiles_list?.GetRandom(); + } + catch + { + tile = null; + } + } + + if (tile == null) + { + EventLiveCoverageLedger.Claim( + "earthquake", + "earthquake", + "unsupported", + "Earthquake.startQuake", + "no_tile"); + return; + } + + Earthquake.startQuake(tile, EarthquakeType.SmallDisaster); + if (InterestRegistry.CountKeysContaining("earthquake:") > 0) + { + EventLiveCoverageLedger.Claim( + "earthquake", + "earthquake", + "ok", + "Earthquake.startQuake", + "busy_bypass=ForceLiveEventFeeds;active=" + Earthquake.isQuakeActive()); + } + else + { + EventLiveCoverageLedger.Claim( + "earthquake", + "earthquake", + "fail", + "Earthquake.startQuake", + "no_a_interest"); + fails.Add("earthquake:earthquake no interest key"); + } + } + catch (Exception ex) + { + fails.Add("earthquake:earthquake " + ex.Message); + EventLiveCoverageLedger.Claim( + "earthquake", + "earthquake", + "fail", + "Earthquake.startQuake", + ex.Message); + } + } + private static void RunBook(Actor unit, List fails) { InterestDropLog.Clear(); @@ -1494,6 +1559,208 @@ public static class EventLivePipelinesHarness // Domains without AuthoredIds rows in ledger seed - combat/boat/building claimed in Run*Smoke. } + private static void RunBuildingComplete(Actor unit, List fails) + { + InterestDropLog.Clear(); + InterestRegistry.ForceExpireContaining("building:complete", Time.unscaledTime); + try + { + Building building = FindCivBuildingNear(unit) ?? TrySpawnCivBuilding(unit); + if (building == null) + { + EventLiveCoverageLedger.Claim( + "building", + "building_complete", + "unsupported", + "Building.completeConstruction", + "no_civ_building"); + return; + } + + MethodInfo complete = AccessTools.Method(typeof(Building), "completeConstruction"); + if (complete == null) + { + EventLiveCoverageLedger.Claim( + "building", + "building_complete", + "unsupported", + "Building.completeConstruction", + "no_method"); + return; + } + + try + { + if (unit != null && building.current_tile != null) + { + AccessTools.Method(typeof(Actor), "setCurrentTile", new[] { typeof(WorldTile) }) + ?.Invoke(unit, new object[] { building.current_tile }); + unit.current_position = building.current_position; + } + } + catch + { + // ignore + } + + complete.Invoke(building, null); + if (InterestRegistry.CountKeysContaining("building:complete") > 0) + { + EventLiveCoverageLedger.Claim( + "building", + "building_complete", + "ok", + "Building.completeConstruction", + "busy_bypass=ForceLiveEventFeeds"); + } + else + { + EventLiveCoverageLedger.Claim( + "building", + "building_complete", + "fail", + "Building.completeConstruction", + "no_a_interest_after_complete"); + fails.Add("building:building_complete no interest key"); + } + } + catch (Exception ex) + { + fails.Add("building:building_complete " + ex.Message); + EventLiveCoverageLedger.Claim( + "building", + "building_complete", + "fail", + "Building.completeConstruction", + ex.Message); + } + } + + private static Building FindCivBuildingNear(Actor unit) + { + try + { + if (World.world?.buildings == null) + { + return null; + } + + Building best = null; + float bestDist = float.MaxValue; + Vector3 origin = unit != null ? unit.current_position : Vector3.zero; + foreach (Building b in World.world.buildings) + { + try + { + if (b == null || !b.isAlive() || b.asset == null + || !LibraryAssetNames.IsCivBuilding(b.asset)) + { + continue; + } + + Vector3 pos = Vector3.zero; + try + { + if (b.current_tile != null) + { + pos = b.current_tile.posV3; + } + } + catch + { + pos = Vector3.zero; + } + + float d = (pos - origin).sqrMagnitude; + bool spectacle = LibraryAssetNames.IsSpectacleBuilding(b.asset.id, b.asset); + // Prefer temples/statues when scoring distance ties. + float score = d - (spectacle ? 1000f : 0f); + if (score < bestDist) + { + bestDist = score; + best = b; + } + } + catch + { + // ignore + } + } + + return best; + } + catch + { + return null; + } + } + + private static Building TrySpawnCivBuilding(Actor unit) + { + try + { + WorldTile tile = unit != null ? unit.current_tile : null; + if (tile == null || World.world?.buildings == null) + { + return null; + } + + MethodInfo add = AccessTools.Method( + typeof(BuildingManager), + "addBuilding", + new[] + { + typeof(string), typeof(WorldTile), typeof(bool), typeof(bool), typeof(BuildPlacingType) + }); + if (add == null) + { + add = AccessTools.Method(typeof(BuildingManager), "addBuilding"); + } + + if (add == null) + { + return null; + } + + string[] candidates = { "statue", "temple_human", "bonfire", "house_human_1", "house" }; + for (int i = 0; i < candidates.Length; i++) + { + try + { + BuildingAsset asset = AssetManager.buildings?.get(candidates[i]); + if (asset == null || !LibraryAssetNames.IsCivBuilding(asset)) + { + // Still try known spectacle ids even if type check fails early. + if (candidates[i] != "statue" && !candidates[i].StartsWith("temple_", StringComparison.Ordinal)) + { + continue; + } + } + + object built = add.GetParameters().Length >= 5 + ? add.Invoke( + World.world.buildings, + new object[] { candidates[i], tile, false, false, BuildPlacingType.New }) + : add.Invoke(World.world.buildings, new object[] { candidates[i], tile }); + if (built is Building b && b.isAlive()) + { + return b; + } + } + catch + { + // try next + } + } + } + catch + { + // ignore + } + + return null; + } + private static void RunBuildingDestroy(Actor unit, List fails) { InterestDropLog.Clear(); diff --git a/IdleSpectator/Events/Catalogs/EventCatalog.Libraries.cs b/IdleSpectator/Events/Catalogs/EventCatalog.Libraries.cs index 9d8c24c..1ed41de 100644 --- a/IdleSpectator/Events/Catalogs/EventCatalog.Libraries.cs +++ b/IdleSpectator/Events/Catalogs/EventCatalog.Libraries.cs @@ -54,7 +54,7 @@ public static partial class EventCatalog Entries.Clear(); _appliedGeneration = EventCatalogConfig.Generation; - LibraryCatalogDefaults d = LibOr("spell", 48f, 88f, "Spectacle", "{a} casts {id}", true); + LibraryCatalogDefaults d = LibOr("spell", 48f, 88f, "Spectacle", "{a} casts {id}", false); HashSet signals = d.SignalIds.Count > 0 ? d.SignalIds : new HashSet(StringComparer.OrdinalIgnoreCase) @@ -70,9 +70,41 @@ public static partial class EventCatalog d.AmbientStrength, signals, d.SignalStrength, + signalPredicate: IsSpectacleSpell, ambientCreatesInterest: d.AmbientCreatesInterest); } + /// + /// Spectacle casts own the camera; brief FX / utility (silence, shield, cure, grass) stay B. + /// + private static bool IsSpectacleSpell(string id) + { + if (string.IsNullOrEmpty(id)) + { + return false; + } + + string s = id.ToLowerInvariant(); + if (s.Contains("silence") + || s.Contains("shield") + || s.Contains("cure") + || s.Contains("grass") + || s.Contains("vegetation") + || s.Contains("skeleton")) + { + return false; + } + + return s.StartsWith("summon_", StringComparison.Ordinal) + || s.Contains("meteor") + || s.Contains("tornado") + || s.Contains("lightning") + || s.Contains("teleport") + || s.Contains("curse") + || s.Contains("blood_rain") + || s.Contains("cast_fire"); + } + public static IEnumerable AuthoredIds { get @@ -85,7 +117,7 @@ public static partial class EventCatalog public static DiscreteEventEntry GetOrFallback(string id) { Ensure(); - LibraryCatalogDefaults d = LibOr("spell", 48f, 88f, "Spectacle", "{a} casts {id}", true); + LibraryCatalogDefaults d = LibOr("spell", 48f, 88f, "Spectacle", "{a} casts {id}", false); return LiveLibraryInterest.Lookup(Entries, id, d.Category, d.Label, 55f); } } @@ -186,8 +218,11 @@ public static partial class EventCatalog } string s = id.ToLowerInvariant(); + // Top-tier materials from live items library (mythril/adamantine) plus named relics. return s.Contains("legendary") || s.Contains("mythic") + || s.Contains("mythril") + || s.Contains("adamantine") || s.Contains("artifact") || s.Contains("relic") || s.Contains("divine") @@ -207,7 +242,7 @@ public static partial class EventCatalog Entries.Clear(); _appliedGeneration = EventCatalogConfig.Generation; - LibraryCatalogDefaults d = LibOr("item", 36f, 80f, "Item", "{a} forges {id}", true); + LibraryCatalogDefaults d = LibOr("item", 36f, 80f, "Item", "{a} gains {id}", false); LiveLibraryInterest.EnsureSeeded( Entries, ActivityAssetCatalog.EnumerateLiveItemIds, @@ -232,7 +267,7 @@ public static partial class EventCatalog public static DiscreteEventEntry GetOrFallback(string id) { Ensure(); - LibraryCatalogDefaults d = LibOr("item", 36f, 80f, "Item", "{a} forges {id}", true); + LibraryCatalogDefaults d = LibOr("item", 36f, 80f, "Item", "{a} gains {id}", false); return LiveLibraryInterest.Lookup(Entries, id, d.Category, d.Label, 40f); } } @@ -283,7 +318,7 @@ public static partial class EventCatalog Entries.Clear(); _appliedGeneration = EventCatalogConfig.Generation; - LibraryCatalogDefaults d = LibOr("subspeciesTrait", 34f, 78f, "Evolution", "{a} evolves {id}", true); + LibraryCatalogDefaults d = LibOr("subspeciesTrait", 34f, 78f, "Evolution", "{a} evolves {id}", false); LiveLibraryInterest.EnsureSeeded( Entries, ActivityAssetCatalog.EnumerateLiveSubspeciesTraitIds, @@ -308,7 +343,7 @@ public static partial class EventCatalog public static DiscreteEventEntry GetOrFallback(string id) { Ensure(); - LibraryCatalogDefaults d = LibOr("subspeciesTrait", 34f, 78f, "Evolution", "{a} evolves {id}", true); + LibraryCatalogDefaults d = LibOr("subspeciesTrait", 34f, 78f, "Evolution", "{a} evolves {id}", false); return LiveLibraryInterest.Lookup(Entries, id, d.Category, d.Label, 36f); } } @@ -335,7 +370,7 @@ public static partial class EventCatalog Entries.Clear(); _appliedGeneration = EventCatalogConfig.Generation; - LibraryCatalogDefaults d = LibOr("gene", 30f, 70f, "Genetics", "{a} gains gene {id}", true); + LibraryCatalogDefaults d = LibOr("gene", 30f, 70f, "Genetics", "{a} gains gene {id}", false); LiveLibraryInterest.EnsureSeeded( Entries, ActivityAssetCatalog.EnumerateLiveGeneIds, @@ -360,7 +395,7 @@ public static partial class EventCatalog public static DiscreteEventEntry GetOrFallback(string id) { Ensure(); - LibraryCatalogDefaults d = LibOr("gene", 30f, 70f, "Genetics", "{a} gains gene {id}", true); + LibraryCatalogDefaults d = LibOr("gene", 30f, 70f, "Genetics", "{a} gains gene {id}", false); return LiveLibraryInterest.Lookup(Entries, id, d.Category, d.Label, 30f); } } @@ -387,7 +422,8 @@ public static partial class EventCatalog Entries.Clear(); _appliedGeneration = EventCatalogConfig.Generation; - LibraryCatalogDefaults d = LibOr("phenotype", 28f, 60f, "Genetics", "{a} shows {id}", true); + // Skin/shade phenotypes are ambient noise - no Signal predicate, all B. + LibraryCatalogDefaults d = LibOr("phenotype", 28f, 60f, "Genetics", "{a} shows {id}", false); LiveLibraryInterest.EnsureSeeded( Entries, ActivityAssetCatalog.EnumerateLivePhenotypeIds, @@ -411,7 +447,7 @@ public static partial class EventCatalog public static DiscreteEventEntry GetOrFallback(string id) { Ensure(); - LibraryCatalogDefaults d = LibOr("phenotype", 28f, 60f, "Genetics", "{a} shows {id}", true); + LibraryCatalogDefaults d = LibOr("phenotype", 28f, 60f, "Genetics", "{a} shows {id}", false); return LiveLibraryInterest.Lookup(Entries, id, d.Category, d.Label, 28f); } } @@ -438,7 +474,7 @@ public static partial class EventCatalog Entries.Clear(); _appliedGeneration = EventCatalogConfig.Generation; - LibraryCatalogDefaults d = LibOr("worldLaw", 40f, 70f, "WorldLaw", "Law changed: {id}", true); + LibraryCatalogDefaults d = LibOr("worldLaw", 40f, 70f, "WorldLaw", "Law changed: {id}", false); LiveLibraryInterest.EnsureSeeded( Entries, ActivityAssetCatalog.EnumerateLiveWorldLawIds, @@ -447,10 +483,27 @@ public static partial class EventCatalog d.AmbientStrength, signalIds: null, d.SignalStrength, - signalPredicate: id => id != null && (id.Contains("war") || id.Contains("death") || id.Contains("plague")), + signalPredicate: IsDramaticWorldLaw, ambientCreatesInterest: d.AmbientCreatesInterest); } + private static bool IsDramaticWorldLaw(string id) + { + if (string.IsNullOrEmpty(id)) + { + return false; + } + + string s = id.ToLowerInvariant(); + return s.Contains("war") + || s.Contains("death") + || s.Contains("plague") + || s.Contains("hunger") + || s.Contains("rebell") + || s.Contains("disaster") + || s.Contains("angry"); + } + public static IEnumerable AuthoredIds { get @@ -463,7 +516,7 @@ public static partial class EventCatalog public static DiscreteEventEntry GetOrFallback(string id) { Ensure(); - LibraryCatalogDefaults d = LibOr("worldLaw", 40f, 70f, "WorldLaw", "Law changed: {id}", true); + LibraryCatalogDefaults d = LibOr("worldLaw", 40f, 70f, "WorldLaw", "Law changed: {id}", false); return LiveLibraryInterest.Lookup(Entries, id, d.Category, d.Label, 40f); } } @@ -568,7 +621,7 @@ public static partial class EventCatalog Entries.Clear(); _appliedGeneration = EventCatalogConfig.Generation; - LibraryCatalogDefaults d = LibOr("cultureTrait", 34f, 74f, "Culture", "{a} culture gains {id}", true); + LibraryCatalogDefaults d = LibOr("cultureTrait", 34f, 74f, "Culture", "{a} culture gains {id}", false); LiveLibraryInterest.EnsureSeeded( Entries, ActivityAssetCatalog.EnumerateLiveCultureTraitIds, @@ -593,7 +646,7 @@ public static partial class EventCatalog public static DiscreteEventEntry GetOrFallback(string id) { Ensure(); - LibraryCatalogDefaults d = LibOr("cultureTrait", 34f, 74f, "Culture", "{a} culture gains {id}", true); + LibraryCatalogDefaults d = LibOr("cultureTrait", 34f, 74f, "Culture", "{a} culture gains {id}", false); return LiveLibraryInterest.Lookup(Entries, id, d.Category, d.Label, 34f); } } @@ -620,7 +673,7 @@ public static partial class EventCatalog Entries.Clear(); _appliedGeneration = EventCatalogConfig.Generation; - LibraryCatalogDefaults d = LibOr("religionTrait", 34f, 74f, "Religion", "{a} faith gains {id}", true); + LibraryCatalogDefaults d = LibOr("religionTrait", 34f, 74f, "Religion", "{a} faith gains {id}", false); LiveLibraryInterest.EnsureSeeded( Entries, ActivityAssetCatalog.EnumerateLiveReligionTraitIds, @@ -645,7 +698,7 @@ public static partial class EventCatalog public static DiscreteEventEntry GetOrFallback(string id) { Ensure(); - LibraryCatalogDefaults d = LibOr("religionTrait", 34f, 74f, "Religion", "{a} faith gains {id}", true); + LibraryCatalogDefaults d = LibOr("religionTrait", 34f, 74f, "Religion", "{a} faith gains {id}", false); return LiveLibraryInterest.Lookup(Entries, id, d.Category, d.Label, 34f); } } @@ -672,7 +725,7 @@ public static partial class EventCatalog Entries.Clear(); _appliedGeneration = EventCatalogConfig.Generation; - LibraryCatalogDefaults d = LibOr("clanTrait", 32f, 72f, "Clan", "{a} clan gains {id}", true); + LibraryCatalogDefaults d = LibOr("clanTrait", 32f, 72f, "Clan", "{a} clan gains {id}", false); LiveLibraryInterest.EnsureSeeded( Entries, ActivityAssetCatalog.EnumerateLiveClanTraitIds, @@ -697,7 +750,7 @@ public static partial class EventCatalog public static DiscreteEventEntry GetOrFallback(string id) { Ensure(); - LibraryCatalogDefaults d = LibOr("clanTrait", 32f, 72f, "Clan", "{a} clan gains {id}", true); + LibraryCatalogDefaults d = LibOr("clanTrait", 32f, 72f, "Clan", "{a} clan gains {id}", false); return LiveLibraryInterest.Lookup(Entries, id, d.Category, d.Label, 32f); } } @@ -724,7 +777,7 @@ public static partial class EventCatalog Entries.Clear(); _appliedGeneration = EventCatalogConfig.Generation; - LibraryCatalogDefaults d = LibOr("languageTrait", 30f, 68f, "Language", "{a} tongue gains {id}", true); + LibraryCatalogDefaults d = LibOr("languageTrait", 30f, 68f, "Language", "{a} tongue gains {id}", false); LiveLibraryInterest.EnsureSeeded( Entries, ActivityAssetCatalog.EnumerateLiveLanguageTraitIds, @@ -749,7 +802,7 @@ public static partial class EventCatalog public static DiscreteEventEntry GetOrFallback(string id) { Ensure(); - LibraryCatalogDefaults d = LibOr("languageTrait", 30f, 68f, "Language", "{a} tongue gains {id}", true); + LibraryCatalogDefaults d = LibOr("languageTrait", 30f, 68f, "Language", "{a} tongue gains {id}", false); return LiveLibraryInterest.Lookup(Entries, id, d.Category, d.Label, 30f); } } diff --git a/IdleSpectator/Events/DiscreteEventEntry.cs b/IdleSpectator/Events/DiscreteEventEntry.cs index 486dc94..2ccc01e 100644 --- a/IdleSpectator/Events/DiscreteEventEntry.cs +++ b/IdleSpectator/Events/DiscreteEventEntry.cs @@ -26,8 +26,9 @@ public sealed class DiscreteEventEntry public string MakeLabel(string a, string b) { string template = string.IsNullOrEmpty(LabelTemplate) ? "{id}" : LabelTemplate; + string displayId = string.IsNullOrEmpty(Id) ? "" : EventReason.HumanizeId(Id); return template - .Replace("{id}", Id ?? "") + .Replace("{id}", displayId) .Replace("{a}", a ?? "") .Replace("{b}", b ?? ""); } diff --git a/IdleSpectator/Events/EventReason.cs b/IdleSpectator/Events/EventReason.cs index a915279..7829774 100644 --- a/IdleSpectator/Events/EventReason.cs +++ b/IdleSpectator/Events/EventReason.cs @@ -814,43 +814,102 @@ public static class EventReason public static string Library(Actor a, string kind, string assetId) { - string an = NameOrSomeone(a); - string id = HumanizeId(assetId); - string k = HumanizeId(kind); + string k = (kind ?? "").Trim().ToLowerInvariant(); + string id = LibraryAssetNames.DisplayName(k, assetId); if (string.IsNullOrEmpty(id)) { - return an + " triggers " + (string.IsNullOrEmpty(k) ? "an event" : k); + id = HumanizeId(assetId); } - switch ((kind ?? "").Trim().ToLowerInvariant()) + bool hasActor = a != null && a.isAlive(); + string an = hasActor ? NameOrSomeone(a) : ""; + + if (string.IsNullOrEmpty(id)) + { + if (!hasActor) + { + return string.IsNullOrEmpty(k) ? "An event" : HumanizeId(k); + } + + return an + " triggers " + (string.IsNullOrEmpty(k) ? "an event" : HumanizeId(k)); + } + + switch (k) { case "spell": - return an + " casts " + id; + return hasActor ? an + " casts " + id : id + " is cast"; case "item": - return an + " forges " + id; + return hasActor + ? an + " " + LibraryAssetNames.ItemVerb(assetId) + " " + id + : id + " is gained"; case "decision": return Decision(a, assetId); case "gene": - return an + " gains gene " + id; + return hasActor ? an + " gains gene " + id : "Gene " + id; case "phenotype": - return an + " shows phenotype " + id; + return hasActor ? an + " shows " + id : "Phenotype " + id; case "power": - return an + " channels " + id; + return hasActor ? an + " channels " + id : "God power: " + id; + case "worldlaw": + case "world_law": + return "Law changed: " + id; + case "biome": + return "Biome shifts: " + id; case "subspecies_trait": - return an + " gains " + id; + return hasActor ? an + " evolves " + id : id + " evolves"; case "culture_trait": - return an + "'s culture gains " + id; + return hasActor ? an + "'s culture gains " + id : "Culture gains " + id; case "religion_trait": - return an + "'s faith gains " + id; + return hasActor ? an + "'s faith gains " + id : "Faith gains " + id; case "clan_trait": - return an + "'s clan gains " + id; + return hasActor ? an + "'s clan gains " + id : "Clan gains " + id; case "language_trait": - return an + "'s language gains " + id; + return hasActor ? an + "'s language gains " + id : "Language gains " + id; default: - return an + " · " + id; + return hasActor ? an + " · " + id : id; } } + public static string Earthquake(Actor near) + { + if (near != null && near.isAlive()) + { + string n = Name(near); + if (!string.IsNullOrEmpty(n)) + { + return n + " is caught in an earthquake"; + } + } + + return "Earthquake shakes the land"; + } + + /// Civ construction finished - temple/statue spectacle vs ordinary house. + public static string BuildingComplete(Actor near, string buildingId, bool spectacle) + { + string id = LibraryAssetNames.DisplayName("building", buildingId); + if (string.IsNullOrEmpty(id)) + { + id = HumanizeId(buildingId); + } + + string what = string.IsNullOrEmpty(id) + ? (spectacle ? "a wonder" : "a building") + : (spectacle ? "wonder " + id : id); + + if (near != null && near.isAlive()) + { + string n = Name(near); + if (!string.IsNullOrEmpty(n)) + { + return n + " finishes " + what; + } + } + + return spectacle ? "Wonder finished: " + (string.IsNullOrEmpty(id) ? "structure" : id) + : "Building finished: " + (string.IsNullOrEmpty(id) ? "structure" : id); + } + public static string Decision(Actor a, string decisionId) { string an = NameOrSomeone(a); @@ -869,8 +928,10 @@ public static class EventReason public static string Apply(string template, Actor a, Actor b = null, string id = "") { string t = string.IsNullOrEmpty(template) ? "{a}" : template; + string displayId = string.IsNullOrEmpty(id) ? "" : HumanizeId(id); + // Prefer humanized id in catalog templates so `{id}` never dumps snake_case. return t - .Replace("{id}", id ?? "") + .Replace("{id}", displayId) .Replace("{a}", Name(a) ?? "") .Replace("{b}", Name(b) ?? ""); } diff --git a/IdleSpectator/Events/Feeds/DeferredInterestFeed.cs b/IdleSpectator/Events/Feeds/DeferredInterestFeed.cs index eddc6e8..e353aed 100644 --- a/IdleSpectator/Events/Feeds/DeferredInterestFeed.cs +++ b/IdleSpectator/Events/Feeds/DeferredInterestFeed.cs @@ -68,7 +68,7 @@ public static class DeferredInterestFeed "power:" + entry.Id, "power", entry.Category, - EventReason.HumanizeId(entry.Id), + EventReason.Library(null, "power", entry.Id), entry.Id, entry.EventStrength, position, @@ -127,7 +127,7 @@ public static class DeferredInterestFeed "worldlaw:" + entry.Id, "worldlaw", entry.Category, - EventReason.HumanizeId(entry.Id), + EventReason.Library(null, "worldlaw", entry.Id), entry.Id, entry.EventStrength, position, @@ -197,7 +197,7 @@ public static class DeferredInterestFeed "biome:" + entry.Id, "biome", entry.Category, - EventReason.HumanizeId(entry.Id), + EventReason.Library(null, "biome", entry.Id), entry.Id, entry.EventStrength, position, diff --git a/IdleSpectator/Events/Feeds/MetaInterestFeed.cs b/IdleSpectator/Events/Feeds/MetaInterestFeed.cs index d18a6c9..824ecb4 100644 --- a/IdleSpectator/Events/Feeds/MetaInterestFeed.cs +++ b/IdleSpectator/Events/Feeds/MetaInterestFeed.cs @@ -95,4 +95,66 @@ public static class MetaInterestFeed follow.current_position, follow); } + + /// + /// Ongoing earthquake while - covers god-tool quakes + /// and disaster-spawned quakes (WorldLog may also fire for the latter). + /// + public static void EmitEarthquake(WorldTile tile) + { + if (!AgentHarness.LiveFeedsAllowed) + { + return; + } + + Vector3 pos = Vector3.zero; + try + { + if (tile != null) + { + pos = tile.posV3; + } + } + catch + { + pos = Vector3.zero; + } + + if (pos == Vector3.zero) + { + try + { + if (Camera.main != null) + { + Vector3 cam = Camera.main.transform.position; + pos = new Vector3(cam.x, cam.y, 0f); + } + } + catch + { + pos = new Vector3(1f, 1f, 0f); + } + } + + if (pos == Vector3.zero) + { + pos = new Vector3(1f, 1f, 0f); + } + + Actor follow = WorldActivityScanner.FindNearestAliveUnit(pos, 80f); + string label = EventReason.Earthquake(follow); + EventFeedUtil.Register( + "earthquake:active", + "earthquake", + "Disaster", + label, + "earthquake", + 95f, + pos, + follow, + locationOnly: follow == null, + minWatch: 8f, + maxWatch: 45f, + completion: InterestCompletionKind.EarthquakeActive); + } } diff --git a/IdleSpectator/Events/Patches/BuildingEventPatches.cs b/IdleSpectator/Events/Patches/BuildingEventPatches.cs index fa89e5b..95dc68b 100644 --- a/IdleSpectator/Events/Patches/BuildingEventPatches.cs +++ b/IdleSpectator/Events/Patches/BuildingEventPatches.cs @@ -1,9 +1,10 @@ using ai.behaviours; using HarmonyLib; +using UnityEngine; namespace IdleSpectator; -/// Building damage/destroy/eat interest patches from mutation discovery. +/// Building damage/destroy/eat/complete interest patches from mutation discovery. [HarmonyPatch] public static class BuildingEventPatches { @@ -49,6 +50,58 @@ public static class BuildingEventPatches _ = __instance; } + /// + /// Civ construction finished (houses, temples, statues). + /// Nature/tree completes stay silent - not settlement spectacle. + /// + [HarmonyPatch(typeof(Building), nameof(Building.completeConstruction))] + [HarmonyPostfix] + public static void PostfixCompleteConstruction(Building __instance) + { + if (!AgentHarness.LiveFeedsAllowed || __instance?.asset == null) + { + return; + } + + if (!LibraryAssetNames.IsCivBuilding(__instance.asset)) + { + return; + } + + string buildingId = ""; + try + { + buildingId = __instance.asset.id ?? ""; + } + catch + { + buildingId = ""; + } + + bool spectacle = LibraryAssetNames.IsSpectacleBuilding(buildingId, __instance.asset); + float strength = spectacle ? 86f : 72f; + Vector3 pos = BuildingWorldPos(__instance); + Actor follow = WorldActivityScanner.FindNearestAliveUnit(pos, 48f); + string label = EventReason.BuildingComplete(follow, buildingId, spectacle); + string key = "building:complete:" + + (string.IsNullOrEmpty(buildingId) ? "unknown" : buildingId) + + ":" + + EventFeedUtil.SafeId(follow); + + EventFeedUtil.Register( + key, + "building", + "Settlement", + label, + string.IsNullOrEmpty(buildingId) ? "building_complete" : buildingId, + strength, + pos, + follow, + locationOnly: follow == null, + minWatch: 6f, + maxWatch: spectacle ? 28f : 18f); + } + private static void Emit(string eventId, float strength, Actor actor, string label) { if (!AgentHarness.LiveFeedsAllowed) @@ -67,4 +120,28 @@ public static class BuildingEventPatches actor.current_position, actor); } + + private static Vector3 BuildingWorldPos(Building building) + { + try + { + if (building.current_tile != null) + { + return building.current_tile.posV3; + } + } + catch + { + // fall through + } + + try + { + return building.current_position; + } + catch + { + return Vector3.zero; + } + } } diff --git a/IdleSpectator/Events/Patches/MetaEventPatches.cs b/IdleSpectator/Events/Patches/MetaEventPatches.cs index 4d06e35..50c1265 100644 --- a/IdleSpectator/Events/Patches/MetaEventPatches.cs +++ b/IdleSpectator/Events/Patches/MetaEventPatches.cs @@ -13,6 +13,22 @@ public static class MetaEventPatches MetaInterestFeed.EmitEra(ReadCurrentEraId()); } + /// + /// Ongoing quake hold. Age change is already covered by + /// (called from WorldAgeManager.update when progress completes). + /// + [HarmonyPatch(typeof(Earthquake), nameof(Earthquake.startQuake))] + [HarmonyPostfix] + public static void PostfixStartQuake(WorldTile pTile) + { + if (!Earthquake.isQuakeActive()) + { + return; + } + + MetaInterestFeed.EmitEarthquake(pTile); + } + [HarmonyPatch(typeof(CultureManager), nameof(CultureManager.newCulture))] [HarmonyPostfix] public static void PostfixNewCulture(Culture __result) diff --git a/IdleSpectator/HarnessScenarios.cs b/IdleSpectator/HarnessScenarios.cs index 79aca40..8603373 100644 --- a/IdleSpectator/HarnessScenarios.cs +++ b/IdleSpectator/HarnessScenarios.cs @@ -107,6 +107,8 @@ internal static class HarnessScenarios return EventSuite(); case "domain_event_audit": return DomainEventAudit(); + case "library_asset_labels_ok": + return LibraryAssetLabelsOk(); case "event_tracking": return EventTracking(); case "family_trigger_smoke": @@ -1938,10 +1940,22 @@ internal static class HarnessScenarios { Step("dea0", "wait_world"), Step("dea1", "assert", expect: "domain_event_audit"), + Step("dea1b", "assert", expect: "library_asset_labels_ok"), + Step("dea1c", "assert", expect: "library_ambient_policy_ok"), Step("dea2", "snapshot") }; } + private static List LibraryAssetLabelsOk() + { + return new List + { + Step("lal0", "wait_world"), + Step("lal1", "assert", expect: "library_asset_labels_ok"), + Step("lal2", "snapshot") + }; + } + private static List EventTracking() { return new List diff --git a/IdleSpectator/InterestCandidate.cs b/IdleSpectator/InterestCandidate.cs index 27f3542..1957c26 100644 --- a/IdleSpectator/InterestCandidate.cs +++ b/IdleSpectator/InterestCandidate.cs @@ -32,7 +32,9 @@ public enum InterestCompletionKind /// Sticky family pack while members / alpha remain resolvable. FamilyPack = 9, /// Sticky status outbreak while clustered carriers remain resolvable. - StatusOutbreak = 10 + StatusOutbreak = 10, + /// Sticky while Earthquake.isQuakeActive() (ongoing shake poll). + EarthquakeActive = 11 } /// diff --git a/IdleSpectator/InterestCompletion.cs b/IdleSpectator/InterestCompletion.cs index a77c09a..b7fe819 100644 --- a/IdleSpectator/InterestCompletion.cs +++ b/IdleSpectator/InterestCompletion.cs @@ -54,6 +54,9 @@ public static class InterestCompletion case InterestCompletionKind.StatusOutbreak: return StatusOutbreakStillActive(candidate); + case InterestCompletionKind.EarthquakeActive: + return EarthquakeStillActive(candidate); + case InterestCompletionKind.ActivityActive: return ActivityStillActive(candidate); @@ -71,6 +74,19 @@ public static class InterestCompletion } } + private static bool EarthquakeStillActive(InterestCandidate c) + { + _ = c; + try + { + return Earthquake.isQuakeActive(); + } + catch + { + return false; + } + } + private static bool CombatStillActive(InterestCandidate c) { Actor unit = c.FollowUnit; diff --git a/IdleSpectator/InterestDirector.cs b/IdleSpectator/InterestDirector.cs index 589e78a..dae3262 100644 --- a/IdleSpectator/InterestDirector.cs +++ b/IdleSpectator/InterestDirector.cs @@ -3045,6 +3045,7 @@ public static class InterestDirector case InterestCompletionKind.CombatActive: case InterestCompletionKind.WarFront: case InterestCompletionKind.PlotActive: + case InterestCompletionKind.EarthquakeActive: classCap = w.maxWatchCombat; break; case InterestCompletionKind.FamilyPack: diff --git a/IdleSpectator/LibraryAssetNames.cs b/IdleSpectator/LibraryAssetNames.cs new file mode 100644 index 0000000..2f7dbc9 --- /dev/null +++ b/IdleSpectator/LibraryAssetNames.cs @@ -0,0 +1,314 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace IdleSpectator; + +/// +/// Localized / humanized display names for live AssetManager library ids. +/// Used by library event Labels so watch tips never show raw snake_case when the game has a name. +/// +public static class LibraryAssetNames +{ + private static readonly string[] LibraryKinds = + { + "spell", "power", "item", "gene", "phenotype", "worldlaw", "subspecies_trait", + "culture_trait", "religion_trait", "clan_trait", "language_trait", "biome", "building" + }; + + public static IReadOnlyList AllKinds => LibraryKinds; + + /// Spectator-facing name for a library asset id (localized when available). + public static string DisplayName(string kind, string assetId) + { + if (string.IsNullOrEmpty(assetId)) + { + return ""; + } + + object asset = TryGetAsset(kind, assetId); + string localized = TryLocalizedName(asset); + if (!string.IsNullOrEmpty(localized)) + { + return localized; + } + + return EventReason.HumanizeId(assetId); + } + + public static object TryGetAsset(string kind, string assetId) + { + if (string.IsNullOrEmpty(kind) || string.IsNullOrEmpty(assetId)) + { + return null; + } + + string id = assetId.Trim(); + string k = kind.Trim().ToLowerInvariant(); + try + { + switch (k) + { + case "spell": + return AssetManager.spells?.get(id); + case "power": + return AssetManager.powers?.get(id); + case "item": + return AssetManager.items?.get(id); + case "gene": + return AssetManager.gene_library?.get(id); + case "phenotype": + return AssetManager.phenotype_library?.get(id); + case "worldlaw": + case "world_law": + return AssetManager.world_laws_library?.get(id); + case "subspecies_trait": + return AssetManager.subspecies_traits?.get(id); + case "culture_trait": + return AssetManager.culture_traits?.get(id); + case "religion_trait": + return AssetManager.religion_traits?.get(id); + case "clan_trait": + return AssetManager.clan_traits?.get(id); + case "language_trait": + return AssetManager.language_traits?.get(id); + case "biome": + return AssetManager.biome_library?.get(id); + case "building": + return AssetManager.buildings?.get(id); + case "trait": + return AssetManager.traits?.get(id); + default: + return null; + } + } + catch + { + return null; + } + } + + public static List EnumerateLiveIds(string kind) + { + string k = (kind ?? "").Trim().ToLowerInvariant(); + switch (k) + { + case "spell": + return ActivityAssetCatalog.EnumerateLiveSpellIds(); + case "power": + return ActivityAssetCatalog.EnumerateLivePowerIds(); + case "item": + return ActivityAssetCatalog.EnumerateLiveItemIds(); + case "gene": + return ActivityAssetCatalog.EnumerateLiveGeneIds(); + case "phenotype": + return ActivityAssetCatalog.EnumerateLivePhenotypeIds(); + case "worldlaw": + case "world_law": + return ActivityAssetCatalog.EnumerateLiveWorldLawIds(); + case "subspecies_trait": + return ActivityAssetCatalog.EnumerateLiveSubspeciesTraitIds(); + case "culture_trait": + return ActivityAssetCatalog.EnumerateLiveCultureTraitIds(); + case "religion_trait": + return ActivityAssetCatalog.EnumerateLiveReligionTraitIds(); + case "clan_trait": + return ActivityAssetCatalog.EnumerateLiveClanTraitIds(); + case "language_trait": + return ActivityAssetCatalog.EnumerateLiveLanguageTraitIds(); + case "biome": + return ActivityAssetCatalog.EnumerateLiveBiomeIds(); + case "building": + return ActivityAssetCatalog.EnumerateLiveBuildingIds(); + default: + return new List(); + } + } + + /// + /// Item watch verb from live asset shape: equipment/weapons/armor gain; forge-y ids forge; else gain. + /// + public static string ItemVerb(string itemId) + { + if (string.IsNullOrEmpty(itemId)) + { + return "gains"; + } + + string s = itemId.ToLowerInvariant(); + if (s.Contains("forge") || s.Contains("smith") || s.Contains("anvil")) + { + return "forges"; + } + + try + { + ItemAsset item = AssetManager.items?.get(itemId.Trim()); + if (item != null) + { + string subtype = item.equipment_subtype ?? ""; + string material = item.material ?? ""; + if (!string.IsNullOrEmpty(subtype) + || item.equipment_value > 0 + || s.StartsWith("armor_", StringComparison.Ordinal) + || s.StartsWith("weapon_", StringComparison.Ordinal) + || s.StartsWith("helmet_", StringComparison.Ordinal) + || s.StartsWith("boots_", StringComparison.Ordinal) + || s.StartsWith("ring_", StringComparison.Ordinal) + || s.StartsWith("amulet_", StringComparison.Ordinal)) + { + return "gains"; + } + + _ = material; + } + } + catch + { + // fall through + } + + return "gains"; + } + + public static bool IsSpectacleBuilding(string buildingId, BuildingAsset asset) + { + string id = (buildingId ?? "").ToLowerInvariant(); + if (id.StartsWith("temple_", StringComparison.Ordinal) + || id == "statue" + || id.Contains("statue") + || id.Contains("monument") + || id.Contains("wonder")) + { + return true; + } + + try + { + string group = asset != null ? (asset.group ?? "") : ""; + if (group.IndexOf("temple", StringComparison.OrdinalIgnoreCase) >= 0 + || group.IndexOf("wonder", StringComparison.OrdinalIgnoreCase) >= 0) + { + return true; + } + } + catch + { + // ignore + } + + return false; + } + + public static bool IsCivBuilding(BuildingAsset asset) + { + if (asset == null) + { + return false; + } + + try + { + return asset.building_type == BuildingType.Building_Civ; + } + catch + { + try + { + string type = asset.type ?? ""; + return type.IndexOf("civ", StringComparison.OrdinalIgnoreCase) >= 0; + } + catch + { + return false; + } + } + } + + private static string TryLocalizedName(object asset) + { + if (asset == null) + { + return ""; + } + + try + { + MethodInfo translated = asset.GetType().GetMethod( + "getTranslatedName", + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (translated != null && translated.GetParameters().Length == 0) + { + string name = translated.Invoke(asset, null) as string; + if (IsUsableDisplay(name, asset)) + { + return ActivityAssetCatalog.TitleCaseWords(name.Trim()); + } + } + } + catch + { + // fall through + } + + try + { + MethodInfo locale = asset.GetType().GetMethod( + "getLocaleID", + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (locale != null && locale.GetParameters().Length == 0) + { + string localeId = locale.Invoke(asset, null) as string; + if (!string.IsNullOrEmpty(localeId)) + { + string text = LocalizedTextManager.getText(localeId); + if (IsUsableDisplay(text, asset) && !string.Equals(text, localeId, StringComparison.Ordinal)) + { + return ActivityAssetCatalog.TitleCaseWords(text.Trim()); + } + } + } + } + catch + { + // fall through + } + + return ""; + } + + private static bool IsUsableDisplay(string name, object asset) + { + if (string.IsNullOrEmpty(name)) + { + return false; + } + + string trimmed = name.Trim(); + if (trimmed.Length == 0 || trimmed.IndexOf('_') >= 0) + { + return false; + } + + try + { + PropertyInfo idProp = asset.GetType().GetProperty("id"); + FieldInfo idField = asset.GetType().GetField("id"); + string id = idProp != null + ? idProp.GetValue(asset) as string + : idField != null + ? idField.GetValue(asset) as string + : null; + if (!string.IsNullOrEmpty(id) && string.Equals(trimmed, id, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + catch + { + // keep name + } + + return true; + } +} diff --git a/IdleSpectator/MutationDiscoveryHarness.cs b/IdleSpectator/MutationDiscoveryHarness.cs index e9c6f43..8a5e145 100644 --- a/IdleSpectator/MutationDiscoveryHarness.cs +++ b/IdleSpectator/MutationDiscoveryHarness.cs @@ -99,6 +99,8 @@ public static class MutationDiscoveryHarness "BookManager.generateNewBook", "BookManager.burnBook", "WorldAgeManager.startNextAge", + "Earthquake.startQuake", + "Earthquake.spawnQuake", "CultureManager.newCulture", "ReligionManager.newReligion", "LanguageManager.newLanguage", @@ -118,6 +120,7 @@ public static class MutationDiscoveryHarness "BehDealDamageToTargetBuilding.execute", "BehConsumeTargetBuilding.execute", "Building.startDestroyBuilding", + "Building.completeConstruction", "BehBoatTransportUnloadUnits.execute", "BehBoatMakeTrade.execute", "BehBoatTransportDoLoading.execute", @@ -454,7 +457,7 @@ public static class MutationDiscoveryHarness || name == "units" || name == "subspecies" || name == "plots" - // Meta genesis Harmony covers new*; ongoing poll is optional fill. + // Meta genesis Harmony covers new*; era/quake have dedicated wires. || name == "alliances" || name == "clans" || name == "cultures" @@ -467,7 +470,9 @@ public static class MutationDiscoveryHarness || name == "kingdoms" || name == "items" || name == "buildings" - || name == "diplomacy"; + || name == "diplomacy" + || name == "era_manager" + || name == "earthquake_manager"; managersTsv.Append("World").Append('\t') .Append(name).Append('\t') .Append("0").Append('\t') diff --git a/IdleSpectator/event-catalog.json b/IdleSpectator/event-catalog.json index c37c48e..4ac1359 100644 --- a/IdleSpectator/event-catalog.json +++ b/IdleSpectator/event-catalog.json @@ -3606,7 +3606,7 @@ "signalStrength": 86, "category": "Spectacle", "label": "{a} casts {id}", - "ambientCreatesInterest": true, + "ambientCreatesInterest": false, "signalIds": [ "summon_lightning", "summon_tornado", @@ -3628,36 +3628,36 @@ "ambientStrength": 34, "signalStrength": 76, "category": "Item", - "label": "{a} forges {id}", - "ambientCreatesInterest": true + "label": "{a} gains {id}", + "ambientCreatesInterest": false }, "gene": { "ambientStrength": 28, "signalStrength": 64, "category": "Genetics", "label": "{a} gains gene {id}", - "ambientCreatesInterest": true + "ambientCreatesInterest": false }, "phenotype": { "ambientStrength": 26, "signalStrength": 55, "category": "Genetics", "label": "{a} shows {id}", - "ambientCreatesInterest": true + "ambientCreatesInterest": false }, "worldLaw": { "ambientStrength": 36, "signalStrength": 68, "category": "WorldLaw", "label": "Law changed: {id}", - "ambientCreatesInterest": true + "ambientCreatesInterest": false }, "subspeciesTrait": { "ambientStrength": 32, "signalStrength": 72, "category": "Evolution", "label": "{a} evolves {id}", - "ambientCreatesInterest": true + "ambientCreatesInterest": false }, "biome": { "ambientStrength": 22, @@ -3671,28 +3671,28 @@ "signalStrength": 74, "category": "Culture", "label": "{a} culture gains {id}", - "ambientCreatesInterest": true + "ambientCreatesInterest": false }, "religionTrait": { "ambientStrength": 34, "signalStrength": 74, "category": "Religion", "label": "{a} faith gains {id}", - "ambientCreatesInterest": true + "ambientCreatesInterest": false }, "clanTrait": { "ambientStrength": 32, "signalStrength": 72, "category": "Clan", "label": "{a} clan gains {id}", - "ambientCreatesInterest": true + "ambientCreatesInterest": false }, "languageTrait": { "ambientStrength": 30, "signalStrength": 68, "category": "Language", "label": "{a} tongue gains {id}", - "ambientCreatesInterest": true + "ambientCreatesInterest": false } } } diff --git a/IdleSpectator/mod.json b/IdleSpectator/mod.json index 77d4b07..d31f687 100644 --- a/IdleSpectator/mod.json +++ b/IdleSpectator/mod.json @@ -1,7 +1,7 @@ { "name": "IdleSpectator", "author": "dazed", - "version": "0.28.13", + "version": "0.28.17", "description": "AFK Idle Spectator (I) + Lore (L). Killer button retargets dossier to the killer.", "GUID": "com.dazed.idlespectator" } diff --git a/docs/event-catalog-discovery.md b/docs/event-catalog-discovery.md new file mode 100644 index 0000000..09270c2 --- /dev/null +++ b/docs/event-catalog-discovery.md @@ -0,0 +1,212 @@ +# Event catalog discovery + +Live gap map of AssetManager libraries, mutation hooks, and IdleSpectator event-catalog coverage. +Expansion of [`event-catalog.json`](../IdleSpectator/event-catalog.json) should cite this ledger, not guessed id lists. + +Audit date: 2026-07-17. +WorldBox harness refresh: `mutation_discovery`, `domain_event_audit`, `activity_happiness_audit`, `activity_status_audit`, `world_log_audit`, `disaster_war_audit`, `activity_taxonomy_audit` (all PASS). +Artifacts: `IdleSpectator/.harness/` (especially `mutation_*.tsv`, `domain_event_audit.tsv`). +Machine worksheet: [`event-catalog-discovery.tsv`](event-catalog-discovery.tsv). + +## Verdict + +**Id inventory is complete for every EventCatalog domain.** +Authored 1:1 libraries match live counts with zero missing/orphan ids. +Live+dial libraries are fully seeded. +`mutation_gaps.tsv` is empty (`gaps=0`). + +The feeling of “lots of events/tips we are not tracking” is mostly **tip quality and Layer A noise**, not missing catalog rows: + +1. Library Labels still interpolate raw `{id}` (`culture gains xenophobic`, `forges armor_leather`). +2. Large ambient-A surfaces (items, genes, phenotypes, subspecies traits, meta traits, world laws) own the camera with generic templates. +3. Building **complete / wonder finish** still has no Layer A wire (destroy/consume only). +4. Meta-trait domains are live-seeded and TSV-audited but **not fail-gated** by `domain_event_audit`. + +Tips here means watch tip / orange reason Labels from interest feeds, not dossier chip hover tooltips. + +## How to read priorities + +| Priority | Meaning | +|----------|---------| +| P0 | Missing wire or authored sync that drops a real lifecycle beat | +| P1 | Tip/prose quality for already-seeded high-visibility classes | +| P2 | New surfaces or Signal tuning after P0/P1 | +| P3 | Intentional Layer B / covered-by another feed | + +## Authored 1:1 domains + +Live audits: `missingAuthored=0` / `orphan=0` for all closed libraries. + +| Domain | Live | Authored | Catalog mode | Camera policy (approx) | Priority | +|--------|-----:|---------:|--------------|------------------------|----------| +| happiness | 67 | 67 | authored | Signal 49 / Aggregate 13 / Ambient 5 | P3 sync done | +| status | 58 | 58 | authored | CreatesInterest 42; B FX 16 | P3 sync done | +| worldLog | 41 | 41 | authored | CreatesInterest 40 (`auto_tester` B) | P3 sync done | +| plots | 28 | 28 | authored | all A | P3 sync done | +| eras | 11 | 11 | authored | all A | P3 sync done | +| books | 12 | 12 | authored | all A | P3 sync done | +| traits | 116 | 116 | authored | all A | P3 sync done | +| disasters | 17 | 17 | authored | via WorldLog pairs | P3 sync done | +| warTypes | 5 | 5 | authored | dials | P3 sync done | +| relationship | n/a | 11 | authored discrete | all A | P3 sync done | + +Happiness Ambient (stay B): `got_poked`, `got_caught`, `paid_tax`, `just_ate`, `just_pooped`. +Status B: recovery_*, egg gain, spell_silence/boost, on_guard, just_ate, afterglow, slowness, cough, shield, dodge, dash, flicked. + +## Live + dial domains + +Seeded via `LiveLibraryInterest.EnsureSeeded` + JSON `libraries.*` dials. +`authored=1` in `domain_event_audit.tsv` means live-seeded entry exists, not a per-id JSON prose row. + +| Domain | Live | Camera A | Dial label template | Notes | Priority | +|--------|-----:|---------:|---------------------|-------|----------| +| decisions | 127 | 12 | `{a} decides to {action}` | 12 camera ids have action phrases; 2 JSON overlays are non-camera (`try_new_plot`, `warrior_train_with_dummy`) | P1 only if new interesting decisions appear | +| spells | 12 | 12 | `{a} casts {id}` | 7 signalIds @86; 5 ambient A @44 | P1 display names; P2 signal set | +| powers | 339 | 23 | `God power: {id}` | ambient CI=false; spectacle predicate | P1 display names | +| items | 111 | 111 | `{a} forges {id}` | ambient A; “forges” weak for armor/non-craft | P1 tip quality | +| genes | 47 | 47 | `{a} gains gene {id}` | ambient A | P1 names; P2 demote ambient spam | +| phenotypes | 52 | 52 | `{a} shows {id}` | ambient A | P1 names; P2 demote ambient spam | +| world_laws | 49 | 49 | `Law changed: {id}` | ambient A | P1 names | +| subspecies_traits | 204 | 204 | `{a} evolves {id}` | ambient A | P1 names; P2 Signal heuristics | +| biomes | 29 | 0 | `Biome shifts: {id}` | ambient CI=false | P3 stay B | +| culture_traits | 78 | 78 | `{a} culture gains {id}` | ~14 dramatic @74; rest ambient @34 | P1 names; hygiene gate | +| religion_traits | 40 | 40 | `{a} faith gains {id}` | dramatic heuristic | P1 names; hygiene gate | +| clan_traits | 29 | 29 | `{a} clan gains {id}` | dramatic heuristic | P1 names; hygiene gate | +| language_traits | 26 | 26 | `{a} tongue gains {id}` | dramatic heuristic | P1 names; hygiene gate | + +Decision camera set (all have authored action phrases): +`try_to_steal_money`, `kill_unruly_clan_members`, `banish_unruly_clan_members`, `find_lover`, +`king_change_kingdom_{language,culture,religion}`, `leader_change_city_{language,culture,religion}`, +`warrior_try_join_army_group`, `warrior_army_follow_leader`. + +Spell signalIds (strength 86): `summon_lightning`, `summon_tornado`, `cast_curse`, `cast_fire`, `cast_blood_rain`, `summon_meteor`, `teleport`. +Ambient-A spells @44: `cast_silence`, `cast_grass_seeds`, `spawn_vegetation`, `spawn_skeleton`, `cast_shield`, `cast_cure`. + +## Patch-only / no catalog rows + +| Surface | Live / notes | Wire status | Priority | +|---------|--------------|-------------|----------| +| buildings | 618 | consume A; ambient destroy demoted B; **no complete/wonder A** | P0 complete/wonder | +| boats | patch | trade/unload A; load B | P3 | +| combat | policy in `EventCatalog.Combat` | scanner + fight feeds | P3 | +| architecture / city_build_orders | 53 / 3 | covered_by_building_activity | P3 | +| kingdoms_traits | 5 | covered_by_trait_meta_feeds | P3 | +| items_modifiers | 59 | covered_by_item_feed | P3 | +| combat_action_library | 11 | covered_by_spell_combat_feeds | P2 only if distinct beats | +| loyalty / opinion / knowledge / communication / story | 29/24/11/10/1 | covered_by_worldlog_happiness | P3 | +| citizen_job / professions / personalities | 13/5/5 | Activity/dossier labels | P3 | +| tasks_actor | 217 | Activity taxonomy (unmappedTasks=0) | P3 | +| actor_library | 322 | SpeciesDiscovery / voice (taxonomy PASS) | P3 | +| plot_category_library | 9 | categories only | P3 | + +WorldLog already covers end beats that inventory once flagged as open wire work: +`alliance_dissolved`, `kingdom_destroyed`, `kingdom_shattered`, `kingdom_fractured`, `city_destroyed`. +Manager `removeObject` is intentionally not event-worthy in `MutationDiscoveryHarness` because those WorldLog rows exist. + +## Mutation discovery + +| Metric | Value | +|--------|------:| +| managers | 57 | +| managerMethods | 189 | +| actorMethods | 256 | +| beh | 93 | +| libraries | 39 | +| libraryAssets | 2893 | +| gaps | 0 | + +`World.era_manager` / `World.earthquake_manager` report `null` on this build (unwired PollState, not gap rows). +Age change remains wired via `WorldAgeManager.startNextAge`. +Ongoing era/earthquake poll is still optional fill (P2), not a missing id catalog row. + +Unwired ManagerEnd / Beh rows exist in TSVs but are filtered out of `mutation_gaps` by design (Beh → activity path; `removeObject` → WorldLog). + +## Tip quality gap (main expansion lever) + +`EventReason` / discrete `MakeLabel` replace `{id}` with the raw asset id. +Status/trait Activity and dossier paths already use locale/`getTranslatedName` helpers (`LiveEnsemble.StatusDisplayName`, trait display names). +Library event Labels do not. + +Examples of weak watch tips today: + +- `{a} culture gains xenophobic` instead of a localized culture-trait name +- `{a} forges armor_leather` (wrong verb + raw id) +- `{a} gains gene warfare_3` +- `God power: atomic_bomb` (readable but not localized) + +Recommended class fix: resolve `{id}` through live asset display names inside Label builders for library domains (same pattern as status), not thousands of JSON rows. + +## Audit hygiene + +`DomainEventHarness` diffs culture/religion/clan/language traits into `domain_event_audit.tsv` but omits those diffs from `Passed`. +If meta traits are first-class EventCatalog domains (they are), the pass condition should include them. + +## Expansion waves + +### Wave 0 - done by this audit + +Authored 1:1 sync and live seed inventory: no missing ids. + +### Wave 1 (first implementation PR) - tip quality + building complete + +Status (2026-07-17): **implemented** in mod `0.28.14`. + +1. `LibraryAssetNames` + `EventReason.Library` resolve live display names (localized when available, else humanized). +2. Item verb is `gains` by default; `forges` only for forge/smith/anvil ids. Catalog template `{a} gains {id}`. +3. `Building.completeConstruction` wires Layer A for `Building_Civ` (temple/statue spectacle strength bump). +4. `domain_event_audit` fail-gates culture/religion/clan/language trait diffs; `library_asset_labels_ok` dumps `.harness/library-asset-labels.tsv`. + +### Wave 2 - Signal / ambient A tuning + +Status (2026-07-17): **implemented** in mod `0.28.16`. + +1. `ambientCreatesInterest=false` for spell/item/gene/phenotype/worldLaw/subspecies/meta trait dials (Signal predicates / signalIds own Layer A). +2. Spells: `IsSpectacleSpell` keeps summon/meteor/tornado/lightning/teleport/curse/blood/fire as A; silence/shield/cure/grass/vegetation/skeleton stay B. +3. World laws: dramatic class includes hunger/rebellion/disaster/angry (plus war/death/plague). +4. Phenotypes: all B (no Signal predicate). +5. Items: Signal for mythril/adamantine (+ named relic tokens); leather/iron ambient B. +6. JSON loader now ingests culture/religion/clan/language trait dials; default ambient CI is false. +7. Gate: `library_ambient_policy_ok` (nested in `domain_event_audit`). + +Era change was already wired (`WorldAgeManager.update` → `startNextAge`). +Earthquake ongoing hold: **implemented** (`Earthquake.startQuake` → `EarthquakeActive` sticky) in `0.28.17`. + +### Wave 3 - new surfaces only if playtests demand + +Status (2026-07-17): **triaged closed** - no new EventCatalog domains. + +| Surface | Live | Decision | +|---------|------|----------| +| combat_action_library (11) | melee/range/spell/dodge/… | Covered by combat scanner + `tryToCastSpell` → spell catalog | +| story / knowledge / communication / loyalty / opinion | small libs | Covered by WorldLog / happiness civic paths | +| personalities / professions | 5 / 5 | Activity/dossier labels only | +| architecture / city_build_orders | build helpers | Building complete / consume path | + +Do not mass-author powers/items/buildings into JSON. + +## First implementation PR recommendation + +**Title focus:** Improve library event tip labels and wire building completion. + +**Scope:** + +- Display-name resolution for library Labels (class fix) +- Item label template improvement +- Building complete / wonder Layer A path + harness gate +- `domain_event_audit` pass includes culture/religion/clan/language trait diffs +- Refresh this doc + [`world-event-inventory.md`](world-event-inventory.md) counts + +**Out of scope for that PR:** mass JSON rows; scoring-model changes; dossier chip tooltips. + +## Refresh commands + +```bash +./scripts/worldbox-ctl.sh status +./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 +./scripts/harness-run.sh --no-launch activity_status_audit +./scripts/harness-run.sh --no-launch world_log_audit +./scripts/harness-run.sh --no-launch disaster_war_audit +./scripts/harness-run.sh --no-launch activity_taxonomy_audit +``` diff --git a/docs/event-catalog-discovery.tsv b/docs/event-catalog-discovery.tsv new file mode 100644 index 0000000..cc9e652 --- /dev/null +++ b/docs/event-catalog-discovery.tsv @@ -0,0 +1,52 @@ +library_or_hook live_count catalog_mode missing_ids camera_a wire_status tip_quality priority notes +happiness_library 67 authored 0 49_Signal wired authored_prose P3 Ambient5 Aggregate13; audit PASS +status 58 authored 0 42 wired authored_prose P3 16 CreatesInterest=false FX; audit PASS +world_log_library 41 authored 0 40 wired authored_labels P3 auto_tester B; audit PASS +plots_library 28 authored 0 28 wired authored_labels P3 domain_event_audit PASS +era_library 11 authored 0 11 wired authored_labels P3 age change via WorldAgeManager; era_manager field null +book_types 12 authored 0 12 wired authored_labels P3 +traits 116 authored 0 116 wired authored_labels P3 +disasters 17 authored 0 via_worldlog wired authored_labels P3 disaster_war_audit PASS +war_types_library 5 authored 0 dials wired authored_labels P3 +relationship 11 authored_discrete 0 11 wired authored_labels P3 no AssetManager lib +decisions_library 127 live+dial 0 12 wired 12_action_phrases P1 try_new_plot and warrior_train_with_dummy authored non-camera +spells 12 live+dial 0 12 wired raw_id_in_label P1 7 signalIds@86; 5 ambient@44 +powers 339 live+dial 0 23 wired raw_id_in_label P1 ambient CI=false +items 111 live+dial 0 111 wired raw_id_forges_verb P1 ambient A; forge verb weak +genes 47 live+dial 0 47 wired raw_id_in_label P1/P2 ambient A spam risk +phenotypes 52 live+dial 0 52 wired raw_id_in_label P1/P2 ambient A spam risk +world_laws_library 49 live+dial 0 49 wired raw_id_in_label P1/P2 ambient A +subspecies_traits 204 live+dial 0 204 wired raw_id_in_label P1/P2 ambient A +biome_library 29 live+dial 0 0 wired raw_id_template P3 ambient CI=false +culture_traits 78 live+dial 0 78 wired raw_id_in_label P1 TSV audited; not fail-gated +religion_traits 40 live+dial 0 40 wired raw_id_in_label P1 TSV audited; not fail-gated +clan_traits 29 live+dial 0 29 wired raw_id_in_label P1 TSV audited; not fail-gated +language_traits 26 live+dial 0 26 wired raw_id_in_label P1 TSV audited; not fail-gated +buildings 618 patch_only n/a consume_and_civ_complete partial EventReason.BuildingEat_BuildingComplete P3 civ complete/wonder A; destroy demoted B +boats n/a patch_only n/a trade_unload wired EventReason.Boat P3 load skipped +combat n/a csharp_policy n/a pair_keys wired EventReason.Fight P3 +architecture_library 53 none n/a 0 covered_by_building_activity n/a P3 +city_build_orders 3 none n/a 0 covered_by_building_activity n/a P3 +kingdoms_traits 5 none n/a 0 covered_by_trait_meta_feeds n/a P3 +items_modifiers 59 none n/a 0 covered_by_item_feed n/a P3 +combat_action_library 11 none n/a 0 covered_by_spell_combat_feeds n/a P2 only if distinct beats +loyalty_library 29 none n/a 0 covered_by_worldlog_happiness n/a P3 +opinion_library 24 none n/a 0 covered_by_worldlog_happiness n/a P3 +knowledge_library 11 none n/a 0 covered_by_worldlog_happiness n/a P3 +communication_library 10 none n/a 0 covered_by_worldlog_happiness n/a P3 +story_library 1 none n/a 0 covered_by_worldlog_happiness n/a P3 +citizen_job_library 13 none n/a 0 covered_by_activity_dossier n/a P3 +professions 5 none n/a 0 covered_by_activity_dossier n/a P3 +personalities 5 none n/a 0 covered_by_activity_dossier n/a P3 +tasks_actor 217 none n/a 0 activity_taxonomy n/a P3 unmappedTasks=0 +actor_library 322 scoring_overlay n/a 0 species_discovery n/a P3 taxonomy PASS +plot_category_library 9 none n/a 0 category_only n/a P3 +AllianceManager.removeObject n/a worldlog 0 via_alliance_dissolved worldlog_covers authored_worldlog P3 not mutation gap +KingdomManager.removeObject n/a worldlog 0 via_kingdom_destroyed worldlog_covers authored_worldlog P3 not mutation gap +building_complete_wonder n/a patch n/a civ_complete Building.completeConstruction EventReason.BuildingComplete P3 temple/statue spectacle strength +World.era_manager_poll n/a wired n/a era_change WorldAgeManager.startNextAge EventCatalog.Era P3 update→startNextAge already emits +World.earthquake_manager_poll n/a wired n/a quake_sticky Earthquake.startQuake EarthquakeActive P3 holds while isQuakeActive +combat_action_library 11 covered_by_spell_combat n/a 0 CombatActionLibrary.tryToCastSpell n/a P3 no separate catalog +story_knowledge_opinion n/a covered_by_worldlog_happiness n/a 0 n/a n/a P3 Wave3 closed +personalities_professions 5+5 covered_by_activity_dossier n/a 0 n/a n/a P3 Wave3 closed +mutation_gaps 0 harness 0 n/a gaps_empty n/a P3 managers=57 libraryAssets=2893 diff --git a/docs/world-event-inventory.md b/docs/world-event-inventory.md index 86838ab..8af9164 100644 --- a/docs/world-event-inventory.md +++ b/docs/world-event-inventory.md @@ -2,8 +2,9 @@ Source of truth for what IdleSpectator covers across the **whole** game. Refresh via live libraries + `mutation_discovery` / domain audits under `.harness/`. +Full gap map and expansion waves: [`event-catalog-discovery.md`](event-catalog-discovery.md). -Audit date: 2026-07-16. +Audit date: 2026-07-17. ## Policy @@ -24,57 +25,65 @@ Only true brief FX, cooldowns, civic Aggregates, and god-tool / biome spam stay | Library | ~Count | Owner | |---------|-------:|-------| -| happiness_library | 67 | EventCatalog.Happiness | -| status | 58 | EventCatalog.Status | -| world_log_library | 41 | EventCatalog.WorldLog | +| happiness_library | 67 | EventCatalog.Happiness (1:1 authored) | +| status | 58 | EventCatalog.Status (1:1 authored) | +| world_log_library | 41 | EventCatalog.WorldLog (1:1 authored) | | 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 | +| decisions / powers / spells / items | 127 / 339 / 12 / 111 | EventCatalog.Libraries / Decision | | subspecies_traits / genes / phenotypes | 204 / 47 / 52 | Libraries | -| buildings | 618 | patches (consume/destroy A; damage skipped) | +| world_laws / biomes | 49 / 29 | Libraries | +| culture / religion / clan / language traits | 78 / 40 / 29 / 26 | EventCatalog.Libraries (live+dial) | +| buildings | 618 | patches (consume A; damage/ambient destroy skipped) | | actor_library | 322 | SpeciesDiscovery / scoring | -| culture/religion/clan/language traits | 78 / 40 / 29 / 26 | **no event catalog yet** | +| tasks_actor | 217 | Activity taxonomy | ## Layer A sizes (policy) -| Domain | Authored | Layer A | -|--------|--------:|--------:| +| Domain | Authored / seeded | 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) | +| Relationship | 11 | all interesting discrete | | 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) | +| Trait | 116 | all CI | +| Decision | 127 live / 14 prose overlays | 12 camera-interesting | +| Spell / Item / Gene / Phenotype / WorldLaw / SubspeciesTrait | live | Signal-only A (ambient CI=false); phenotypes all B | +| Culture / Religion / Clan / LanguageTrait | live dials | Signal-only via dramatic heuristic | +| Power / Biome | live | Signal predicate only / ambient CI=false | | 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. +books, traits, deferred libraries, boats (trade/unload), buildings (consume; civ complete/wonder; ambient destroy demoted), +relationship (lover/child/family/baby/alpha), Family.setAlpha, SpeciesDiscovery, +meta trait gain (`MetaObjectWithTraits.addTrait` pipelines), +earthquake sticky (`Earthquake.startQuake` while active). ## Known gaps / thin areas | Gap | Why it matters | Status | |-----|----------------|--------| -| Culture/religion/clan/language trait gain | Meta drama libraries exist | **Live** (MetaObjectWithTraits.addTrait in pipelines) | -| 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** | +| Building complete / wonder finish | Settlement spectacle | **Live** (`Building.completeConstruction`, civ only) | +| Library tip Labels use raw `{id}` | Watch tips look untracked / ugly | **Live** (`LibraryAssetNames` + humanize) | +| Large ambient-A libraries (items/genes/phenotypes/subspecies/meta) | Camera noise with weak prose | **Live** (Signal-only A; `library_ambient_policy_ok`) | +| Meta-trait diffs not fail-gated in `domain_event_audit` | Regressions easy to miss | **Live** (fail-gated + `library_asset_labels_ok`) | +| Era/earthquake ongoing poll | Age via `startNextAge`; quake sticky via `Earthquake.startQuake` | **Live** | +| Alliance/kingdom destroy managers | End beats covered by WorldLog (`alliance_dissolved`, `kingdom_destroyed`, …) | **Closed via WorldLog** | ## 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` +- Patches: `building_damage`, ambient building destroy, `boat_load` ## Scoring notes @@ -88,4 +97,9 @@ CharacterSignificance includes favorite/king/leader/renown/kills/level and speci ./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 +./scripts/harness-run.sh --no-launch activity_status_audit +./scripts/harness-run.sh --no-launch world_log_audit +./scripts/harness-run.sh --no-launch disaster_war_audit ``` + +See also [`event-catalog-discovery.md`](event-catalog-discovery.md) for the prioritized expansion waves.