Implement building completion events and enhance earthquake handling.

- Update building event patches to include completion notifications
- Introduce earthquake event handling for ongoing seismic activity
- Modify event catalog to reflect changes in ambient interest settings
- Add new harness scenarios for library asset label validation
- Increment version to 0.28.17 in mod.json
This commit is contained in:
DazedAnon 2026-07-17 17:13:43 -05:00
parent 8a4314be6f
commit 03729664be
22 changed files with 1459 additions and 86 deletions

View file

@ -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<string> { "kind\tid\tdisplay\tsample_label" };
IReadOnlyList<string> kinds = LibraryAssetNames.AllKinds;
for (int k = 0; k < kinds.Count; k++)
{
string kind = kinds[k];
List<string> 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<string>
{
"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;

View file

@ -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 };

View file

@ -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++)

View file

@ -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<string> 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<string> 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<string> 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<string> fails)
{
InterestDropLog.Clear();

View file

@ -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<string> signals = d.SignalIds.Count > 0
? d.SignalIds
: new HashSet<string>(StringComparer.OrdinalIgnoreCase)
@ -70,9 +70,41 @@ public static partial class EventCatalog
d.AmbientStrength,
signals,
d.SignalStrength,
signalPredicate: IsSpectacleSpell,
ambientCreatesInterest: d.AmbientCreatesInterest);
}
/// <summary>
/// Spectacle casts own the camera; brief FX / utility (silence, shield, cure, grass) stay B.
/// </summary>
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<string> 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<string> 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);
}
}

View file

@ -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 ?? "");
}

View file

@ -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";
}
/// <summary>Civ construction finished - temple/statue spectacle vs ordinary house.</summary>
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) ?? "");
}

View file

@ -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,

View file

@ -95,4 +95,66 @@ public static class MetaInterestFeed
follow.current_position,
follow);
}
/// <summary>
/// Ongoing earthquake while <see cref="Earthquake.isQuakeActive"/> - covers god-tool quakes
/// and disaster-spawned quakes (WorldLog may also fire for the latter).
/// </summary>
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);
}
}

View file

@ -1,9 +1,10 @@
using ai.behaviours;
using HarmonyLib;
using UnityEngine;
namespace IdleSpectator;
/// <summary>Building damage/destroy/eat interest patches from mutation discovery.</summary>
/// <summary>Building damage/destroy/eat/complete interest patches from mutation discovery.</summary>
[HarmonyPatch]
public static class BuildingEventPatches
{
@ -49,6 +50,58 @@ public static class BuildingEventPatches
_ = __instance;
}
/// <summary>
/// Civ construction finished (houses, temples, statues).
/// Nature/tree completes stay silent - not settlement spectacle.
/// </summary>
[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;
}
}
}

View file

@ -13,6 +13,22 @@ public static class MetaEventPatches
MetaInterestFeed.EmitEra(ReadCurrentEraId());
}
/// <summary>
/// Ongoing quake hold. Age change is already covered by <see cref="PostfixStartNextAge"/>
/// (called from <c>WorldAgeManager.update</c> when progress completes).
/// </summary>
[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)

View file

@ -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<HarnessCommand> LibraryAssetLabelsOk()
{
return new List<HarnessCommand>
{
Step("lal0", "wait_world"),
Step("lal1", "assert", expect: "library_asset_labels_ok"),
Step("lal2", "snapshot")
};
}
private static List<HarnessCommand> EventTracking()
{
return new List<HarnessCommand>

View file

@ -32,7 +32,9 @@ public enum InterestCompletionKind
/// <summary>Sticky family pack while members / alpha remain resolvable.</summary>
FamilyPack = 9,
/// <summary>Sticky status outbreak while clustered carriers remain resolvable.</summary>
StatusOutbreak = 10
StatusOutbreak = 10,
/// <summary>Sticky while <c>Earthquake.isQuakeActive()</c> (ongoing shake poll).</summary>
EarthquakeActive = 11
}
/// <summary>

View file

@ -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;

View file

@ -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:

View file

@ -0,0 +1,314 @@
using System;
using System.Collections.Generic;
using System.Reflection;
namespace IdleSpectator;
/// <summary>
/// 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.
/// </summary>
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<string> AllKinds => LibraryKinds;
/// <summary>Spectator-facing name for a library asset id (localized when available).</summary>
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<string> 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<string>();
}
}
/// <summary>
/// Item watch verb from live asset shape: equipment/weapons/armor gain; forge-y ids forge; else gain.
/// </summary>
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;
}
}

View file

@ -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')

View file

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

View file

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

View file

@ -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
```

View file

@ -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
1 library_or_hook live_count catalog_mode missing_ids camera_a wire_status tip_quality priority notes
2 happiness_library 67 authored 0 49_Signal wired authored_prose P3 Ambient5 Aggregate13; audit PASS
3 status 58 authored 0 42 wired authored_prose P3 16 CreatesInterest=false FX; audit PASS
4 world_log_library 41 authored 0 40 wired authored_labels P3 auto_tester B; audit PASS
5 plots_library 28 authored 0 28 wired authored_labels P3 domain_event_audit PASS
6 era_library 11 authored 0 11 wired authored_labels P3 age change via WorldAgeManager; era_manager field null
7 book_types 12 authored 0 12 wired authored_labels P3
8 traits 116 authored 0 116 wired authored_labels P3
9 disasters 17 authored 0 via_worldlog wired authored_labels P3 disaster_war_audit PASS
10 war_types_library 5 authored 0 dials wired authored_labels P3
11 relationship 11 authored_discrete 0 11 wired authored_labels P3 no AssetManager lib
12 decisions_library 127 live+dial 0 12 wired 12_action_phrases P1 try_new_plot and warrior_train_with_dummy authored non-camera
13 spells 12 live+dial 0 12 wired raw_id_in_label P1 7 signalIds@86; 5 ambient@44
14 powers 339 live+dial 0 23 wired raw_id_in_label P1 ambient CI=false
15 items 111 live+dial 0 111 wired raw_id_forges_verb P1 ambient A; forge verb weak
16 genes 47 live+dial 0 47 wired raw_id_in_label P1/P2 ambient A spam risk
17 phenotypes 52 live+dial 0 52 wired raw_id_in_label P1/P2 ambient A spam risk
18 world_laws_library 49 live+dial 0 49 wired raw_id_in_label P1/P2 ambient A
19 subspecies_traits 204 live+dial 0 204 wired raw_id_in_label P1/P2 ambient A
20 biome_library 29 live+dial 0 0 wired raw_id_template P3 ambient CI=false
21 culture_traits 78 live+dial 0 78 wired raw_id_in_label P1 TSV audited; not fail-gated
22 religion_traits 40 live+dial 0 40 wired raw_id_in_label P1 TSV audited; not fail-gated
23 clan_traits 29 live+dial 0 29 wired raw_id_in_label P1 TSV audited; not fail-gated
24 language_traits 26 live+dial 0 26 wired raw_id_in_label P1 TSV audited; not fail-gated
25 buildings 618 patch_only n/a consume_and_civ_complete partial EventReason.BuildingEat_BuildingComplete P3 civ complete/wonder A; destroy demoted B
26 boats n/a patch_only n/a trade_unload wired EventReason.Boat P3 load skipped
27 combat n/a csharp_policy n/a pair_keys wired EventReason.Fight P3
28 architecture_library 53 none n/a 0 covered_by_building_activity n/a P3
29 city_build_orders 3 none n/a 0 covered_by_building_activity n/a P3
30 kingdoms_traits 5 none n/a 0 covered_by_trait_meta_feeds n/a P3
31 items_modifiers 59 none n/a 0 covered_by_item_feed n/a P3
32 combat_action_library 11 none n/a 0 covered_by_spell_combat_feeds n/a P2 only if distinct beats
33 loyalty_library 29 none n/a 0 covered_by_worldlog_happiness n/a P3
34 opinion_library 24 none n/a 0 covered_by_worldlog_happiness n/a P3
35 knowledge_library 11 none n/a 0 covered_by_worldlog_happiness n/a P3
36 communication_library 10 none n/a 0 covered_by_worldlog_happiness n/a P3
37 story_library 1 none n/a 0 covered_by_worldlog_happiness n/a P3
38 citizen_job_library 13 none n/a 0 covered_by_activity_dossier n/a P3
39 professions 5 none n/a 0 covered_by_activity_dossier n/a P3
40 personalities 5 none n/a 0 covered_by_activity_dossier n/a P3
41 tasks_actor 217 none n/a 0 activity_taxonomy n/a P3 unmappedTasks=0
42 actor_library 322 scoring_overlay n/a 0 species_discovery n/a P3 taxonomy PASS
43 plot_category_library 9 none n/a 0 category_only n/a P3
44 AllianceManager.removeObject n/a worldlog 0 via_alliance_dissolved worldlog_covers authored_worldlog P3 not mutation gap
45 KingdomManager.removeObject n/a worldlog 0 via_kingdom_destroyed worldlog_covers authored_worldlog P3 not mutation gap
46 building_complete_wonder n/a patch n/a civ_complete Building.completeConstruction EventReason.BuildingComplete P3 temple/statue spectacle strength
47 World.era_manager_poll n/a wired n/a era_change WorldAgeManager.startNextAge EventCatalog.Era P3 update→startNextAge already emits
48 World.earthquake_manager_poll n/a wired n/a quake_sticky Earthquake.startQuake EarthquakeActive P3 holds while isQuakeActive
49 combat_action_library 11 covered_by_spell_combat n/a 0 CombatActionLibrary.tryToCastSpell n/a P3 no separate catalog
50 story_knowledge_opinion n/a covered_by_worldlog_happiness n/a 0 n/a n/a P3 Wave3 closed
51 personalities_professions 5+5 covered_by_activity_dossier n/a 0 n/a n/a P3 Wave3 closed
52 mutation_gaps 0 harness 0 n/a gaps_empty n/a P3 managers=57 libraryAssets=2893

View file

@ -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.