Compare commits

...

5 commits

56 changed files with 8766 additions and 2317 deletions

View file

@ -19,6 +19,7 @@ public sealed class ActivityHappinessAuditResult
public int MissingPolicy;
public int RequiredContextFailures;
public int MissingEventStrength;
public int ProseRegressions;
}
/// <summary>
@ -43,6 +44,7 @@ public static class ActivityHappinessHarness
int missingPolicy = 0;
int requiredContextFailures = 0;
int missingEventStrength = 0;
int proseRegressions = 0;
var tsv = new StringBuilder();
tsv.AppendLine(
"id\tvalue\tpsychopath\tpresentation\tlife\tcontext\toverlap\tevent_strength\tclause\tauthored\ticon\tlocale_leak\tnotes");
@ -195,6 +197,49 @@ public static class ActivityHappinessHarness
// Disk may be unavailable in some harness contexts.
}
// Prose vs call-site regressions (Layer B honesty).
proseRegressions += CountProseRegression(
"just_born",
mustContain: new[] { "appears in the world", "brought into existence" },
mustNotContain: new[] { "born into the world", "first breath" });
proseRegressions += CountProseRegression(
"just_kissed",
mustContain: new[] { "mates", "intimacy" },
mustNotContain: new[] { "kiss" });
proseRegressions += CountProseRegression(
"just_got_out_of_egg",
mustContain: new[] { "hatches from an egg" },
mustNotContain: Array.Empty<string>());
// Camera demotion: only true FX / bookkeeping stay Ambient (Layer B).
// Interesting life/social beats are Signal; director ranks by EventStrength.
string[] ambientRequired =
{
"got_poked", "got_caught", "paid_tax", "just_ate", "just_pooped"
};
for (int i = 0; i < ambientRequired.Length; i++)
{
HappinessCatalogEntry e = EventCatalog.Happiness.GetOrFallback(ambientRequired[i]);
if (e.Presentation == HappinessPresentationTier.Signal)
{
proseRegressions++;
}
}
string[] signalRequired =
{
"just_made_friend", "just_injured", "just_found_house", "got_robbed",
"fallen_in_love", "become_alpha", "just_had_child"
};
for (int i = 0; i < signalRequired.Length; i++)
{
HappinessCatalogEntry e = EventCatalog.Happiness.GetOrFallback(signalRequired[i]);
if (e.Presentation != HappinessPresentationTier.Signal)
{
proseRegressions++;
}
}
bool passed = missingAuthored == 0
&& missingIcon == 0
&& localeLeaks == 0
@ -202,7 +247,8 @@ public static class ActivityHappinessHarness
&& orphanAuthored == 0
&& missingPolicy == 0
&& requiredContextFailures == 0
&& missingEventStrength == 0;
&& missingEventStrength == 0
&& proseRegressions == 0;
return new ActivityHappinessAuditResult
{
@ -216,14 +262,45 @@ public static class ActivityHappinessHarness
MissingPolicy = missingPolicy,
RequiredContextFailures = requiredContextFailures,
MissingEventStrength = missingEventStrength,
ProseRegressions = proseRegressions,
Detail =
$"total={liveIds.Count} missingAuthored={missingAuthored} missingIcon={missingIcon} "
+ $"localeLeaks={localeLeaks} emptyPhrase={emptyPhrase} orphanAuthored={orphanAuthored} "
+ $"missingPolicy={missingPolicy} requiredContextFailures={requiredContextFailures} "
+ $"missingEventStrength={missingEventStrength}"
+ $"missingEventStrength={missingEventStrength} proseRegressions={proseRegressions}"
};
}
private static int CountProseRegression(string id, string[] mustContain, string[] mustNotContain)
{
HappinessCatalogEntry entry = EventCatalog.Happiness.GetOrFallback(id);
if (entry == null || entry.IsFallback)
{
return 1;
}
string joined = string.Join(" ", entry.VariantsWithoutRelated ?? Array.Empty<string>())
+ " "
+ string.Join(" ", entry.VariantsWithRelated ?? Array.Empty<string>());
for (int i = 0; mustContain != null && i < mustContain.Length; i++)
{
if (joined.IndexOf(mustContain[i], StringComparison.OrdinalIgnoreCase) < 0)
{
return 1;
}
}
for (int i = 0; mustNotContain != null && i < mustNotContain.Length; i++)
{
if (joined.IndexOf(mustNotContain[i], StringComparison.OrdinalIgnoreCase) >= 0)
{
return 1;
}
}
return 0;
}
private static bool LooksLikeLocaleLeak(string text, string id)
{
if (string.IsNullOrEmpty(text))

View file

@ -745,19 +745,9 @@ public static class ActivityLog
amount != 0 ? amount : HappinessGameApi.ResolveAmount(effectId),
HappinessSourceHook.Harness,
related: null,
role: HappinessRelationRole.None,
forceRing: true);
if (published != null && !string.IsNullOrEmpty(relatedName))
{
published.RelatedName = relatedName;
HappinessCatalogEntry entry = EventCatalog.Happiness.GetOrFallback(effectId);
string clause = ActivityHappinessProse.Clause(entry, relatedName, out bool usedRelated, out _);
published.PlainClause = clause;
published.RichClause = usedRelated
? clause.Replace(relatedName, ActivityProse.ColorName(relatedName))
: clause;
}
role: HappinessContextBag.RoleForDeathEffect(effectId.Trim()),
forceRing: true,
relatedNameOverride: relatedName);
if (published != null)
{
InterestFeeds.IngestForHarness(published);

View file

@ -184,6 +184,38 @@ public static class ActivityStatusHarness
// Artifact write is best-effort.
}
int cameraDemotionFailures = 0;
// Brief FX / cooldowns must stay B. Story-readable statuses must stay A.
string[] bOnlyStatus =
{
"afterglow", "cough", "dash", "dodge", "flicked", "just_ate", "on_guard",
"recovery_combat_action", "recovery_plot", "recovery_social", "recovery_spell",
"shield", "slowness", "spell_boost", "spell_silence", "egg"
};
for (int i = 0; i < bOnlyStatus.Length; i++)
{
StatusInterestEntry e = EventCatalog.Status.GetOrFallback(bOnlyStatus[i]);
if (e.CreatesInterest)
{
cameraDemotionFailures++;
}
}
string[] aStatus =
{
"drowning", "burning", "frozen", "poisoned", "possessed", "cursed",
"pregnant", "crying", "rage", "stunned", "soul_harvested", "tantrum",
"fell_in_love", "starving", "angry", "sleeping", "inspired", "ash_fever"
};
for (int i = 0; i < aStatus.Length; i++)
{
StatusInterestEntry e = EventCatalog.Status.GetOrFallback(aStatus[i]);
if (!e.CreatesInterest)
{
cameraDemotionFailures++;
}
}
bool passed = liveIds.Count > 20
&& missingAuthored == 0
&& missingIcon == 0
@ -191,7 +223,8 @@ public static class ActivityStatusHarness
&& emptyPhrase == 0
&& orphanAuthored == 0
&& missingInterest == 0
&& orphanInterest == 0;
&& orphanInterest == 0
&& cameraDemotionFailures == 0;
return new ActivityStatusAuditResult
{
@ -207,7 +240,8 @@ public static class ActivityStatusHarness
Detail =
$"total={liveIds.Count} missingAuthored={missingAuthored} missingIcon={missingIcon} "
+ $"localeLeaks={localeLeaks} emptyPhrase={emptyPhrase} orphanAuthored={orphanAuthored} "
+ $"missingInterest={missingInterest} orphanInterest={orphanInterest} passed={passed}"
+ $"missingInterest={missingInterest} orphanInterest={orphanInterest} "
+ $"cameraDemotionFailures={cameraDemotionFailures} passed={passed}"
};
}

View file

@ -13,10 +13,10 @@ public static class ActivityStatusProse
new Dictionary<string, (string, string)>(StringComparer.OrdinalIgnoreCase)
{
["afterglow"] = ("Basks in an afterglow", "Lets the afterglow fade"),
["angry"] = ("Flies into a rage", "Cools off"),
["angry"] = ("Flies into a fury", "Cools off"),
["ash_fever"] = ("Catches ash fever", "Recovers from ash fever"),
["being_suspicious"] = ("Starts acting suspicious", "Stops looking so suspicious"),
["budding"] = ("Begins budding", "Finishes budding"),
["budding"] = ("Begins budding", "Splits off a bud offspring"),
["burning"] = ("Catches fire", "Stops burning"),
["caffeinated"] = ("Gets a caffeine rush", "Comes down from caffeine"),
["confused"] = ("Becomes confused", "Regains clarity"),
@ -45,10 +45,10 @@ public static class ActivityStatusProse
["on_guard"] = ("Goes on guard", "Drops guard"),
["poisoned"] = ("Is poisoned", "Purges the poison"),
["possessed"] = ("Is possessed", "Breaks free of possession"),
["possessed_follower"] = ("Falls under a possessed follower's sway", "Escapes the follower's sway"),
["possessed_follower"] = ("Is converted into a possessed follower", "Breaks free of follower possession"),
["powerup"] = ("Powers up", "Loses the power-up"),
["pregnant"] = ("Becomes pregnant", "Is no longer pregnant"),
["pregnant_parthenogenesis"] = ("Becomes pregnant by parthenogenesis", "Ends the parthenogenic pregnancy"),
["pregnant"] = ("Becomes pregnant", "Gives birth"),
["pregnant_parthenogenesis"] = ("Becomes pregnant by parthenogenesis", "Gives birth alone"),
["rage"] = ("Enters a rage", "Leaves the rage behind"),
["recovery_combat_action"] = ("Needs a moment after combat", "Is ready for combat again"),
["recovery_plot"] = ("Needs a moment after a plot", "Is ready to plot again"),
@ -66,9 +66,9 @@ public static class ActivityStatusProse
["stunned"] = ("Is stunned", "Shakes off the stun"),
["surprised"] = ("Is surprised", "Recovers from surprise"),
["swearing"] = ("Starts swearing", "Stops swearing"),
["taking_roots"] = ("Starts taking root", "Pulls free of the roots"),
["taking_roots"] = ("Starts taking root", "Finishes rooting and sprouts offspring"),
["tantrum"] = ("Throws a tantrum", "Calms down from the tantrum"),
["uprooting"] = ("Starts uprooting", "Finishes uprooting"),
["uprooting"] = ("Emerges as a young plant", "Settles after emerging"),
["voices_in_my_head"] = ("Hears voices", "The voices fall quiet"),
};

View file

@ -28,6 +28,7 @@ public static class AgentHarness
private static Actor _lastSpawned;
private static string _lastSpawnedAssetId = "";
private static Actor _happinessPartner;
private static long _activitySubjectId;
private static bool _directorRunActive;
private static string _rememberedFocusKey = "";
private static string LastScreenshotPath = "";
@ -56,6 +57,12 @@ public static class AgentHarness
public static bool Busy => Queue.Count > 0 || _waitUntil > 0f;
/// <summary>
/// When true, relationship interest feeds still Emit while <see cref="Busy"/>
/// (harness verification of live Harmony patches).
/// </summary>
public static bool ForceRelationshipFeeds;
/// <summary>
/// When true, InterestDirector / SpeciesDiscovery stay frozen during harness commands.
/// <c>director_run</c> temporarily clears this so dwell/interrupt/discovery can execute.
@ -828,18 +835,24 @@ public static class AgentHarness
case "happiness_force_note":
{
long id = WatchCaption.CurrentUnitId;
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
if (id == 0 && focus != null)
long id = 0;
try
{
try
if (focus != null)
{
id = focus.getID();
}
catch
{
id = 0;
}
}
catch
{
id = 0;
}
// Prefer live focus over a stale dossier CurrentUnitId.
if (id == 0)
{
id = WatchCaption.CurrentUnitId;
}
string effectId = !string.IsNullOrEmpty(cmd.value) ? cmd.value.Trim() : "just_ate";
@ -1031,6 +1044,50 @@ public static class AgentHarness
break;
}
case "relationship_set_parent":
{
Actor child = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : _lastSpawned;
Actor parent = _happinessPartner;
bool ok = false;
string detail;
ForceRelationshipFeeds = true;
try
{
if (child != null && parent != null && child.isAlive() && parent.isAlive()
&& child != parent)
{
child.setParent1(parent);
ok = true;
detail = $"parent={SafeName(parent)} child={SafeName(child)}";
}
else
{
detail = "need focus child + remembered parent";
}
}
catch (Exception ex)
{
detail = "exception: " + ex.Message;
ok = false;
}
finally
{
ForceRelationshipFeeds = false;
}
if (ok)
{
_cmdOk++;
}
else
{
_cmdFail++;
}
Emit(cmd, ok, detail: detail);
break;
}
case "happiness_bond_friends":
{
Actor a = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
@ -1070,21 +1127,45 @@ public static class AgentHarness
case "happiness_kill_partner":
{
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
Actor partner = _happinessPartner;
bool ok = false;
string err = "";
try
{
if (partner != null && partner.isAlive())
{
partner.die(true, AttackType.Divine);
ok = !partner.isAlive();
}
}
catch (Exception ex)
{
err = ex.Message ?? "die_exception";
}
bool ok = false;
try
{
ok = partner != null && !partner.isAlive();
}
catch
{
ok = false;
}
// Keep the surviving focus so grief asserts resolve the right subject.
try
{
if (focus != null && focus.isAlive())
{
CameraDirector.FocusUnit(focus);
_activitySubjectId = focus.getID();
}
}
catch
{
// ignore
}
if (ok)
{
_cmdOk++;
@ -1095,7 +1176,11 @@ public static class AgentHarness
}
WatchCaption.ForceRefreshHistory();
Emit(cmd, ok, detail: $"killed={SafeName(partner)} ok={ok}");
Emit(
cmd,
ok,
detail: $"killed={SafeName(partner)} ok={ok}"
+ (string.IsNullOrEmpty(err) ? "" : " err=" + err));
break;
}
@ -1855,6 +1940,7 @@ public static class AgentHarness
case "scoring_reload":
{
bool ok = InterestScoringConfig.Reload();
bool catalogOk = EventCatalogConfig.Reload();
if (ok)
{
_cmdOk++;
@ -1868,11 +1954,33 @@ public static class AgentHarness
cmd,
ok: ok,
detail: ok
? $"loaded v={InterestScoringConfig.LoadedVersion} path={InterestScoringConfig.LoadedPath}"
? $"loaded v={InterestScoringConfig.LoadedVersion} path={InterestScoringConfig.LoadedPath} "
+ $"eventCatalog={catalogOk} decisions={EventCatalogConfig.DecisionCount}"
: "scoring_reload_failed");
break;
}
case "event_catalog_reload":
{
bool ok = EventCatalogConfig.Reload();
if (ok)
{
_cmdOk++;
}
else
{
_cmdFail++;
}
Emit(
cmd,
ok: ok,
detail: ok
? $"loaded v={EventCatalogConfig.LoadedVersion} path={EventCatalogConfig.LoadedPath}"
: "event_catalog_reload_failed");
break;
}
case "interest_variety_seed":
{
// value = "events:chars" e.g. "2:10"
@ -2358,6 +2466,15 @@ public static class AgentHarness
}
CameraDirector.FocusUnit(unit);
try
{
_activitySubjectId = unit.getID();
}
catch
{
_activitySubjectId = 0;
}
_cmdOk++;
Emit(cmd, ok: true, detail: $"unit={SafeName(unit)} asset={(unit.asset != null ? unit.asset.id : "?")}");
}
@ -2658,7 +2775,7 @@ public static class AgentHarness
entry = EventCatalog.Plot.GetOrFallback(id);
key = "plot:" + entry.Id + ":new:" + EventFeedUtil.SafeId(unit);
label = entry.MakeLabel(EventFeedUtil.SafeName(unit), "new");
label = EventReason.Plot(unit, entry.LabelTemplate, "new", entry.Id);
strength = entry.EventStrength;
category = entry.Category;
source = "plot";
@ -2721,7 +2838,7 @@ public static class AgentHarness
entry = EventCatalog.Trait.GetOrFallback(id);
key = "trait:" + entry.Id + ":gains:" + EventFeedUtil.SafeId(unit);
label = EventFeedUtil.SafeName(unit) + " gains " + entry.Id;
label = EventReason.Trait(unit, entry.Id, gained: true);
strength = entry.EventStrength;
category = entry.Category;
source = "trait";
@ -2733,7 +2850,7 @@ public static class AgentHarness
}
key = "meta:" + id + ":" + EventFeedUtil.SafeId(unit);
label = "Meta: " + id + " (" + EventFeedUtil.SafeName(unit) + ")";
label = EventReason.MetaNew(unit, id);
strength = 74f;
category = "Politics";
source = "meta";
@ -2741,16 +2858,16 @@ public static class AgentHarness
case "boat":
id = "boat_unload";
key = "boat:boat_unload:" + EventFeedUtil.SafeId(unit);
label = "Boat unloads: " + EventFeedUtil.SafeName(unit);
label = EventReason.Boat(unit, "unload");
strength = 72f;
category = "Travel";
source = "boat";
break;
case "building":
id = "building_damage";
key = "building:building_damage:" + EventFeedUtil.SafeId(unit);
label = "Damages a building: " + EventFeedUtil.SafeName(unit);
strength = 70f;
id = "building_destroy";
key = "building:destroy:harness:" + EventFeedUtil.SafeId(unit);
label = EventReason.BuildingFalls("harness_hall", unit);
strength = 88f;
category = "Spectacle";
source = "building";
break;
@ -3567,6 +3684,41 @@ public static class AgentHarness
detail = $"want='{want}' found='{found}' pending={InterestRegistry.PendingCount}";
break;
}
case "interest_label_contains":
{
string want = (cmd.value ?? cmd.expect ?? "").Trim();
bool have = false;
string found = "";
if (!string.IsNullOrEmpty(want))
{
string currentLabel = InterestDirector.CurrentLabel ?? "";
if (currentLabel.IndexOf(want, StringComparison.OrdinalIgnoreCase) >= 0)
{
have = true;
found = currentLabel;
}
else
{
var pending = new System.Collections.Generic.List<InterestCandidate>(64);
InterestRegistry.CopyPending(pending);
for (int i = 0; i < pending.Count; i++)
{
InterestCandidate c = pending[i];
string label = c?.Label ?? "";
if (label.IndexOf(want, StringComparison.OrdinalIgnoreCase) >= 0)
{
have = true;
found = label;
break;
}
}
}
}
pass = have;
detail = $"want='{want}' found='{found}' pending={InterestRegistry.PendingCount}";
break;
}
case "interest_no_key":
{
string want = (cmd.value ?? cmd.expect ?? "").Trim();
@ -5744,8 +5896,14 @@ public static class AgentHarness
private static void DoSnapshot(HarnessCommand cmd)
{
string drops = InterestDropLog.FormatRecent(6);
string snap =
$"idle={SpectatorMode.Active} focus={MoveCamera.hasFocusUnit()} powerBar={CanvasMain.isBottomBarShowing()} unit={FocusLabel()} tip={CameraDirector.LastWatchLabel} tipAsset={CameraDirector.LastWatchAssetId} bad={StateProbe.BadEventCount} score={InterestDirector.CurrentScoreLabel} discoveryPending={SpeciesDiscovery.PendingCount} caption={WatchCaption.LastHeadline} chronicle={Chronicle.Count}";
$"idle={SpectatorMode.Active} focus={MoveCamera.hasFocusUnit()} powerBar={CanvasMain.isBottomBarShowing()} unit={FocusLabel()} tip={CameraDirector.LastWatchLabel} tipAsset={CameraDirector.LastWatchAssetId} bad={StateProbe.BadEventCount} score={InterestDirector.CurrentScoreLabel} discoveryPending={SpeciesDiscovery.PendingCount} caption={WatchCaption.LastHeadline} chronicle={Chronicle.Count} drops={InterestDropLog.Count}";
if (!string.IsNullOrEmpty(drops))
{
snap += " recentDrop=[" + drops + "]";
}
_cmdOk++;
Emit(cmd, ok: true, detail: snap);
LogService.LogInfo("[IdleSpectator][SNAP] " + snap);
@ -6125,6 +6283,7 @@ public static class AgentHarness
long focusId = MoveCamera._focus_unit.getID();
if (focusId != 0)
{
_activitySubjectId = focusId;
return focusId;
}
}
@ -6134,6 +6293,11 @@ public static class AgentHarness
// ignore
}
if (_activitySubjectId != 0)
{
return _activitySubjectId;
}
return WatchCaption.CurrentUnitId;
}

View file

@ -230,16 +230,58 @@ public static class DomainEventHarness
CatalogCoverageAudit.WriteReport(outputDirectory, "domain_event_audit.tsv", tsv.ToString());
// Camera decisions must have authored ActionPhrase + EventStrength (event-catalog.json).
int decisionProseMissing = 0;
int decisionStrengthMissing = 0;
int decisionProseTotal = 0;
List<string> liveDecisions = ActivityAssetCatalog.EnumerateLiveDecisionIds();
for (int i = 0; i < liveDecisions.Count; i++)
{
string id = liveDecisions[i];
if (!EventCatalog.Decision.IsCameraWorthy(id))
{
continue;
}
decisionProseTotal++;
DiscreteEventEntry entry = EventCatalog.Decision.GetOrFallback(id);
if (!EventCatalog.Decision.TryGetAuthoredActionPhrase(id, out _))
{
decisionProseMissing++;
tsv.Append("decision_prose\t").Append(id).Append("\t0\t1\t\tmissing_action_phrase").AppendLine();
}
else if (entry.EventStrength <= 0f)
{
decisionStrengthMissing++;
tsv.Append("decision_prose\t").Append(id).Append("\t0\t1\t")
.Append(entry.EventStrength.ToString("0.#"))
.Append("\tmissing_strength").AppendLine();
}
else
{
string phrase = EventCatalog.Decision.ActionPhraseFor(id);
tsv.Append("decision_prose\t").Append(id).Append("\t1\t1\t")
.Append(entry.EventStrength.ToString("0.#")).Append('\t')
.Append(phrase).AppendLine();
}
}
CatalogCoverageAudit.WriteReport(outputDirectory, "domain_event_audit.tsv", tsv.ToString());
bool passed = plots.Passed && eras.Passed && books.Passed && traits.Passed
&& spells.Passed && powers.Passed && decisions.Passed && items.Passed
&& subspTraits.Passed && genes.Passed && phenotypes.Passed
&& worldLaws.Passed && biomes.Passed
&& relMissing == 0 && relTotal > 0;
&& relMissing == 0 && relTotal > 0
&& decisionProseMissing == 0 && decisionStrengthMissing == 0
&& decisionProseTotal > 0;
string detail =
$"{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}; 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

@ -0,0 +1,726 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using NeoModLoader.services;
namespace IdleSpectator;
/// <summary>One decision row from event-catalog.json (action phrase + EventStrength).</summary>
public sealed class DecisionCatalogRow
{
public string Id = "";
public string Action = "";
public float Strength;
}
/// <summary>Library dial defaults from event-catalog.json <c>libraries</c>.</summary>
public sealed class LibraryCatalogDefaults
{
public float AmbientStrength = 40f;
public float SignalStrength = 80f;
public string Category = "Event";
public string Label = "{a} · {id}";
public bool AmbientCreatesInterest = true;
public readonly HashSet<string> SignalIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
}
/// <summary>
/// Loads event-catalog.json — SoT for per-event strength/prose overlays.
/// <c>action</c> is decision-only (clause after "decides to"); other domains use <c>label</c> / happiness variants.
/// </summary>
public static class EventCatalogConfig
{
public const string FileName = "event-catalog.json";
public const float DefaultDecisionStrength = 86f;
private static readonly Dictionary<string, DecisionCatalogRow> Decisions =
new Dictionary<string, DecisionCatalogRow>(StringComparer.OrdinalIgnoreCase);
private static readonly Dictionary<string, float> SpeciesBonuses =
new Dictionary<string, float>(StringComparer.OrdinalIgnoreCase);
private static readonly Dictionary<string, float> CharacterBonuses =
new Dictionary<string, float>(StringComparer.OrdinalIgnoreCase);
private static readonly Dictionary<string, HappinessCatalogEntry> Happiness =
new Dictionary<string, HappinessCatalogEntry>(StringComparer.OrdinalIgnoreCase);
private static readonly Dictionary<string, StatusInterestEntry> Status =
new Dictionary<string, StatusInterestEntry>(StringComparer.OrdinalIgnoreCase);
private static readonly Dictionary<string, WorldLogEventEntry> WorldLog =
new Dictionary<string, WorldLogEventEntry>(StringComparer.OrdinalIgnoreCase);
private static readonly Dictionary<string, DiscreteEventEntry> Plots =
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
private static readonly Dictionary<string, DiscreteEventEntry> Relationship =
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
private static readonly Dictionary<string, DiscreteEventEntry> Traits =
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
private static readonly Dictionary<string, DiscreteEventEntry> Books =
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
private static readonly Dictionary<string, DiscreteEventEntry> Eras =
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
private static readonly Dictionary<string, DisasterInterestEntry> Disasters =
new Dictionary<string, DisasterInterestEntry>(StringComparer.OrdinalIgnoreCase);
private static readonly Dictionary<string, WarTypeInterestEntry> WarTypes =
new Dictionary<string, WarTypeInterestEntry>(StringComparer.OrdinalIgnoreCase);
private static readonly Dictionary<string, LibraryCatalogDefaults> Libraries =
new Dictionary<string, LibraryCatalogDefaults>(StringComparer.OrdinalIgnoreCase);
private static string _loadedPath = "";
private static string _loadedVersion = "";
private static bool _loadedFromFile;
private static int _generation;
public static bool LoadedFromFile => _loadedFromFile;
public static string LoadedPath => _loadedPath;
public static string LoadedVersion => _loadedVersion;
public static int Generation => _generation;
public static IEnumerable<string> DecisionIds => Decisions.Keys;
public static int DecisionCount => Decisions.Count;
public static int SpeciesCount => SpeciesBonuses.Count;
public static int CharacterCount => CharacterBonuses.Count;
public static int HappinessCount => Happiness.Count;
public static int StatusCount => Status.Count;
public static int WorldLogCount => WorldLog.Count;
public static int PlotCount => Plots.Count;
public static int RelationshipCount => Relationship.Count;
public static int TraitCount => Traits.Count;
public static int BookCount => Books.Count;
public static int EraCount => Eras.Count;
public static int DisasterCount => Disasters.Count;
public static int WarTypeCount => WarTypes.Count;
public static int LibraryCount => Libraries.Count;
public static void LoadFromModFolder(string modFolder)
{
ClearAll();
_loadedFromFile = false;
_loadedPath = "";
_loadedVersion = "";
if (string.IsNullOrEmpty(modFolder))
{
LogService.LogInfo("[IdleSpectator] event-catalog.json: no mod folder, using C# fallbacks");
BumpAndInvalidate();
return;
}
string path = Path.Combine(modFolder, FileName);
if (!File.Exists(path))
{
LogService.LogInfo($"[IdleSpectator] event-catalog.json missing at {path}, using C# fallbacks");
BumpAndInvalidate();
return;
}
try
{
string json = File.ReadAllText(path);
if (!TryParseDocument(json, out string version))
{
LogService.LogInfo("[IdleSpectator] event-catalog.json parse failed, using C# fallbacks");
ClearAll();
BumpAndInvalidate();
return;
}
_loadedFromFile = Happiness.Count > 0 || Decisions.Count > 0 || Status.Count > 0;
_loadedPath = path;
_loadedVersion = version ?? "";
BumpAndInvalidate();
LogService.LogInfo(
$"[IdleSpectator] event-catalog.json loaded v={_loadedVersion} "
+ $"happiness={Happiness.Count} status={Status.Count} worldLog={WorldLog.Count} "
+ $"plots={Plots.Count} relationship={Relationship.Count} traits={Traits.Count} "
+ $"books={Books.Count} eras={Eras.Count} disasters={Disasters.Count} "
+ $"warTypes={WarTypes.Count} decisions={Decisions.Count} libraries={Libraries.Count} "
+ $"species={SpeciesBonuses.Count} character={CharacterBonuses.Count}");
}
catch (Exception ex)
{
ClearAll();
LogService.LogInfo($"[IdleSpectator] event-catalog.json failed ({ex.Message}), using C# fallbacks");
BumpAndInvalidate();
}
}
private static void ClearAll()
{
Decisions.Clear();
SpeciesBonuses.Clear();
CharacterBonuses.Clear();
Happiness.Clear();
Status.Clear();
WorldLog.Clear();
Plots.Clear();
Relationship.Clear();
Traits.Clear();
Books.Clear();
Eras.Clear();
Disasters.Clear();
WarTypes.Clear();
Libraries.Clear();
}
private static void BumpAndInvalidate()
{
_generation++;
EventCatalog.InvalidateAllOverlays();
}
internal static bool TryParseDocument(string json, out string version)
{
version = "";
if (string.IsNullOrEmpty(json))
{
return false;
}
version = ExtractJsonString(json, "modVersion") ?? "";
if (TryExtractArray(json, "decisions", out string a)) IngestDecisionObjects(a);
if (TryExtractArray(json, "species", out a)) IngestBonusObjects(a, SpeciesBonuses);
if (TryExtractArray(json, "character", out a)) IngestBonusObjects(a, CharacterBonuses);
if (TryExtractArray(json, "happiness", out a)) IngestHappiness(a);
if (TryExtractArray(json, "status", out a)) IngestStatus(a);
if (TryExtractArray(json, "worldLog", out a)) IngestWorldLog(a);
if (TryExtractArray(json, "plots", out a)) IngestDiscrete(a, Plots);
if (TryExtractArray(json, "relationship", out a)) IngestDiscrete(a, Relationship);
if (TryExtractArray(json, "traits", out a)) IngestDiscrete(a, Traits);
if (TryExtractArray(json, "books", out a)) IngestDiscrete(a, Books);
if (TryExtractArray(json, "eras", out a)) IngestDiscrete(a, Eras);
if (TryExtractArray(json, "disasters", out a)) IngestDisasters(a);
if (TryExtractArray(json, "warTypes", out a)) IngestWarTypes(a);
IngestLibraries(json);
return Happiness.Count > 0 || Decisions.Count > 0 || Status.Count > 0 || Plots.Count > 0;
}
public static int FillHappiness(Dictionary<string, HappinessCatalogEntry> into)
{
into.Clear();
foreach (KeyValuePair<string, HappinessCatalogEntry> kv in Happiness)
{
into[kv.Key] = kv.Value;
}
return into.Count;
}
public static int FillStatus(Dictionary<string, StatusInterestEntry> into)
{
into.Clear();
foreach (KeyValuePair<string, StatusInterestEntry> kv in Status)
{
into[kv.Key] = kv.Value;
}
return into.Count;
}
public static int FillWorldLog(Dictionary<string, WorldLogEventEntry> into)
{
into.Clear();
foreach (KeyValuePair<string, WorldLogEventEntry> kv in WorldLog)
{
into[kv.Key] = kv.Value;
}
return into.Count;
}
public static int FillPlots(Dictionary<string, DiscreteEventEntry> into) => FillDiscrete(Plots, into);
public static int FillRelationship(Dictionary<string, DiscreteEventEntry> into) => FillDiscrete(Relationship, into);
public static int FillTraits(Dictionary<string, DiscreteEventEntry> into) => FillDiscrete(Traits, into);
public static int FillBooks(Dictionary<string, DiscreteEventEntry> into) => FillDiscrete(Books, into);
public static int FillEras(Dictionary<string, DiscreteEventEntry> into) => FillDiscrete(Eras, into);
public static int FillDisasters(Dictionary<string, DisasterInterestEntry> into)
{
into.Clear();
foreach (KeyValuePair<string, DisasterInterestEntry> kv in Disasters)
{
into[kv.Key] = kv.Value;
}
return into.Count;
}
public static int FillWarTypes(Dictionary<string, WarTypeInterestEntry> into)
{
into.Clear();
foreach (KeyValuePair<string, WarTypeInterestEntry> kv in WarTypes)
{
into[kv.Key] = kv.Value;
}
return into.Count;
}
private static int FillDiscrete(
Dictionary<string, DiscreteEventEntry> src,
Dictionary<string, DiscreteEventEntry> into)
{
into.Clear();
foreach (KeyValuePair<string, DiscreteEventEntry> kv in src)
{
into[kv.Key] = kv.Value;
}
return into.Count;
}
public static bool TryGetLibraryDefaults(string key, out LibraryCatalogDefaults defaults)
{
defaults = null;
if (string.IsNullOrEmpty(key))
{
return false;
}
return Libraries.TryGetValue(key.Trim(), out defaults) && defaults != null;
}
private static void IngestHappiness(string arrayJson)
{
foreach (string obj in EnumerateObjects(arrayJson))
{
string id = ExtractJsonString(obj, "id");
if (string.IsNullOrEmpty(id)) continue;
var entry = new HappinessCatalogEntry
{
Id = id.Trim(),
EventStrength = ExtractJsonFloat(obj, "strength", 40f),
Presentation = ParseEnum(obj, "presentation", HappinessPresentationTier.Signal),
Context = ParseEnum(obj, "context", HappinessContextRequirement.None),
Life = ParseEnum(obj, "life", HappinessLifePolicy.ActivityOnly),
Overlap = ParseEnum(obj, "overlap", HappinessOverlapPolicy.Independent),
RelationRole = ParseEnum(obj, "relationRole", HappinessRelationRole.None),
InterestCategory = ExtractJsonString(obj, "category") ?? "",
CanonicalMilestoneKey = ExtractJsonString(obj, "canonicalMilestoneKey") ?? "",
StatusOverlapId = ExtractJsonString(obj, "statusOverlapId") ?? "",
VariantsWithRelated = ExtractStringArray(obj, "variantsWithRelated"),
VariantsWithoutRelated = ExtractStringArray(obj, "variantsWithoutRelated")
};
Happiness[entry.Id] = entry;
}
}
private static void IngestStatus(string arrayJson)
{
foreach (string obj in EnumerateObjects(arrayJson))
{
string id = ExtractJsonString(obj, "id");
if (string.IsNullOrEmpty(id)) continue;
Status[id.Trim()] = new StatusInterestEntry
{
Id = id.Trim(),
EventStrength = ExtractJsonFloat(obj, "strength", 40f),
Category = ExtractJsonString(obj, "category") ?? "Status",
CreatesInterest = ExtractJsonBool(obj, "camera", false),
ExtendsGrief = ExtractJsonBool(obj, "extendsGrief", false)
};
}
}
private static void IngestWorldLog(string arrayJson)
{
foreach (string obj in EnumerateObjects(arrayJson))
{
string id = ExtractJsonString(obj, "id");
if (string.IsNullOrEmpty(id)) continue;
bool camera = ExtractJsonBool(obj, "camera", true);
float strength = ExtractJsonFloat(obj, "strength", 40f);
bool chronicle = ExtractJsonBool(obj, "chronicle", camera && strength >= 65f);
WorldLog[id.Trim()] = new WorldLogEventEntry
{
Id = id.Trim(),
EventStrength = strength,
Category = ExtractJsonString(obj, "category") ?? "WorldLog",
CreatesInterest = camera,
ChronicleEligible = chronicle,
LabelTemplate = ExtractJsonString(obj, "label") ?? "{id}: {a}",
MinWatch = ExtractJsonFloat(obj, "minWatch", 6f),
MaxWatch = ExtractJsonFloat(obj, "maxWatch", 28f)
};
}
}
private static void IngestDiscrete(string arrayJson, Dictionary<string, DiscreteEventEntry> into)
{
foreach (string obj in EnumerateObjects(arrayJson))
{
string id = ExtractJsonString(obj, "id");
if (string.IsNullOrEmpty(id)) continue;
into[id.Trim()] = new DiscreteEventEntry
{
Id = id.Trim(),
EventStrength = ExtractJsonFloat(obj, "strength", 40f),
Category = ExtractJsonString(obj, "category") ?? "Event",
CreatesInterest = ExtractJsonBool(obj, "camera", true),
LabelTemplate = ExtractJsonString(obj, "label") ?? "{a} · {id}",
ActionPhrase = ExtractJsonString(obj, "action") ?? ""
};
}
}
private static void IngestDisasters(string arrayJson)
{
foreach (string obj in EnumerateObjects(arrayJson))
{
string id = ExtractJsonString(obj, "id");
if (string.IsNullOrEmpty(id)) continue;
Disasters[id.Trim()] = new DisasterInterestEntry
{
Id = id.Trim(),
EventStrength = ExtractJsonFloat(obj, "strength", 90f),
Category = ExtractJsonString(obj, "category") ?? "Disaster",
CreatesInterest = ExtractJsonBool(obj, "camera", true),
WorldLogId = ExtractJsonString(obj, "worldLogId") ?? ""
};
}
}
private static void IngestWarTypes(string arrayJson)
{
foreach (string obj in EnumerateObjects(arrayJson))
{
string id = ExtractJsonString(obj, "id");
if (string.IsNullOrEmpty(id)) continue;
WarTypes[id.Trim()] = new WarTypeInterestEntry
{
Id = id.Trim(),
EventStrength = ExtractJsonFloat(obj, "strength", 85f),
Category = ExtractJsonString(obj, "category") ?? "Politics",
CreatesInterest = ExtractJsonBool(obj, "camera", true),
LabelTemplate = ExtractJsonString(obj, "label") ?? "War ({id})"
};
}
}
private static void IngestLibraries(string json)
{
if (!TryExtractObject(json, "libraries", out string libsJson))
{
return;
}
string[] keys =
{
"spell", "power", "item", "gene", "phenotype", "worldLaw", "subspeciesTrait", "biome"
};
for (int i = 0; i < keys.Length; i++)
{
string key = keys[i];
if (!TryExtractObject(libsJson, key, out string obj))
{
continue;
}
var d = new LibraryCatalogDefaults
{
AmbientStrength = ExtractJsonFloat(obj, "ambientStrength", 40f),
SignalStrength = ExtractJsonFloat(obj, "signalStrength", 80f),
Category = ExtractJsonString(obj, "category") ?? "Event",
Label = ExtractJsonString(obj, "label") ?? "{a} · {id}",
AmbientCreatesInterest = ExtractJsonBool(obj, "ambientCreatesInterest", true)
};
string[] ids = ExtractStringArray(obj, "signalIds");
for (int j = 0; j < ids.Length; j++)
{
if (!string.IsNullOrEmpty(ids[j]))
{
d.SignalIds.Add(ids[j]);
}
}
Libraries[key] = d;
}
}
private static int IngestDecisionObjects(string arrayJson)
{
int n = 0;
foreach (string obj in EnumerateObjects(arrayJson))
{
string id = ExtractJsonString(obj, "id");
string action = ExtractJsonString(obj, "action");
if (string.IsNullOrEmpty(id) || string.IsNullOrEmpty(action)) continue;
float strength = ExtractJsonFloat(obj, "strength", DefaultDecisionStrength);
Decisions[id.Trim()] = new DecisionCatalogRow
{
Id = id.Trim(),
Action = action,
Strength = strength > 0f ? strength : DefaultDecisionStrength
};
n++;
}
return n;
}
private static int IngestBonusObjects(string arrayJson, Dictionary<string, float> into)
{
int n = 0;
foreach (string obj in EnumerateObjects(arrayJson))
{
string id = ExtractJsonString(obj, "id");
if (string.IsNullOrEmpty(id)) continue;
into[id.Trim()] = ExtractJsonFloat(obj, "bonus", 0f);
n++;
}
return n;
}
private static TEnum ParseEnum<TEnum>(string json, string key, TEnum fallback) where TEnum : struct
{
string raw = ExtractJsonString(json, key);
if (string.IsNullOrEmpty(raw)) return fallback;
return Enum.TryParse(raw, true, out TEnum value) ? value : fallback;
}
private static string[] ExtractStringArray(string json, string key)
{
if (!TryExtractArray(json, key, out string arr) || string.IsNullOrEmpty(arr))
{
return Array.Empty<string>();
}
var list = new List<string>();
int i = 0;
while (i < arr.Length)
{
int q = arr.IndexOf('"', i);
if (q < 0) break;
int end = q + 1;
var sb = new StringBuilder();
while (end < arr.Length)
{
char c = arr[end++];
if (c == '\\' && end < arr.Length) { sb.Append(arr[end++]); continue; }
if (c == '"') break;
sb.Append(c);
}
list.Add(sb.ToString());
i = end;
}
return list.ToArray();
}
private static bool ExtractJsonBool(string json, string key, bool fallback)
{
string needle = "\"" + key + "\"";
int keyAt = json.IndexOf(needle, StringComparison.Ordinal);
if (keyAt < 0) return fallback;
int colon = json.IndexOf(':', keyAt + needle.Length);
if (colon < 0) return fallback;
int i = colon + 1;
while (i < json.Length && char.IsWhiteSpace(json[i])) i++;
if (i + 4 <= json.Length && string.Compare(json, i, "true", 0, 4, StringComparison.OrdinalIgnoreCase) == 0)
return true;
if (i + 5 <= json.Length && string.Compare(json, i, "false", 0, 5, StringComparison.OrdinalIgnoreCase) == 0)
return false;
return fallback;
}
private static IEnumerable<string> EnumerateObjects(string arrayJson)
{
if (string.IsNullOrEmpty(arrayJson)) yield break;
int i = 0;
while (i < arrayJson.Length)
{
int start = arrayJson.IndexOf('{', i);
if (start < 0) yield break;
int depth = 0;
bool inString = false;
bool escape = false;
for (int j = start; j < arrayJson.Length; j++)
{
char c = arrayJson[j];
if (inString)
{
if (escape) { escape = false; continue; }
if (c == '\\') { escape = true; continue; }
if (c == '"') inString = false;
continue;
}
if (c == '"') { inString = true; continue; }
if (c == '{') depth++;
else if (c == '}')
{
depth--;
if (depth == 0)
{
yield return arrayJson.Substring(start, j - start + 1);
i = j + 1;
break;
}
}
}
if (depth != 0) yield break;
}
}
private static string ExtractJsonString(string json, string key)
{
string needle = "\"" + key + "\"";
int keyAt = json.IndexOf(needle, StringComparison.Ordinal);
if (keyAt < 0) return null;
int colon = json.IndexOf(':', keyAt + needle.Length);
if (colon < 0) return null;
int i = colon + 1;
while (i < json.Length && char.IsWhiteSpace(json[i])) i++;
if (i >= json.Length || json[i] != '"') return null;
i++;
var sb = new StringBuilder();
while (i < json.Length)
{
char c = json[i++];
if (c == '\\' && i < json.Length) { sb.Append(json[i++]); continue; }
if (c == '"') break;
sb.Append(c);
}
return sb.ToString();
}
private static float ExtractJsonFloat(string json, string key, float fallback)
{
string needle = "\"" + key + "\"";
int keyAt = json.IndexOf(needle, StringComparison.Ordinal);
if (keyAt < 0) return fallback;
int colon = json.IndexOf(':', keyAt + needle.Length);
if (colon < 0) return fallback;
int i = colon + 1;
while (i < json.Length && char.IsWhiteSpace(json[i])) i++;
int start = i;
while (i < json.Length && (char.IsDigit(json[i]) || json[i] == '.' || json[i] == '-' || json[i] == '+'))
i++;
if (i <= start) return fallback;
string token = json.Substring(start, i - start);
return float.TryParse(token, NumberStyles.Float, CultureInfo.InvariantCulture, out float value)
? value : fallback;
}
private static bool TryExtractArray(string json, string key, out string arrayJson)
{
arrayJson = null;
string keyColon = "\"" + key + "\"";
int searchFrom = 0;
int keyAt = -1;
while (true)
{
int at = json.IndexOf(keyColon, searchFrom, StringComparison.Ordinal);
if (at < 0) break;
int after = at + keyColon.Length;
while (after < json.Length && char.IsWhiteSpace(json[after])) after++;
if (after < json.Length && json[after] == ':') { keyAt = at; break; }
searchFrom = at + 1;
}
if (keyAt < 0) return false;
int bracket = json.IndexOf('[', keyAt + keyColon.Length);
if (bracket < 0) return false;
int depth = 0;
bool inString = false;
bool escape = false;
for (int i = bracket; i < json.Length; i++)
{
char c = json[i];
if (inString)
{
if (escape) { escape = false; continue; }
if (c == '\\') { escape = true; continue; }
if (c == '"') inString = false;
continue;
}
if (c == '"') { inString = true; continue; }
if (c == '[') depth++;
else if (c == ']')
{
depth--;
if (depth == 0)
{
arrayJson = json.Substring(bracket, i - bracket + 1);
return true;
}
}
}
return false;
}
private static bool TryExtractObject(string json, string key, out string objectJson)
{
objectJson = null;
string keyColon = "\"" + key + "\"";
int searchFrom = 0;
int keyAt = -1;
while (true)
{
int at = json.IndexOf(keyColon, searchFrom, StringComparison.Ordinal);
if (at < 0) break;
int after = at + keyColon.Length;
while (after < json.Length && char.IsWhiteSpace(json[after])) after++;
if (after < json.Length && json[after] == ':') { keyAt = at; break; }
searchFrom = at + 1;
}
if (keyAt < 0) return false;
int brace = json.IndexOf('{', keyAt + keyColon.Length);
if (brace < 0) return false;
int depth = 0;
bool inString = false;
bool escape = false;
for (int i = brace; i < json.Length; i++)
{
char c = json[i];
if (inString)
{
if (escape) { escape = false; continue; }
if (c == '\\') { escape = true; continue; }
if (c == '"') inString = false;
continue;
}
if (c == '"') { inString = true; continue; }
if (c == '{') depth++;
else if (c == '}')
{
depth--;
if (depth == 0)
{
objectJson = json.Substring(brace, i - brace + 1);
return true;
}
}
}
return false;
}
public static bool TryGetDecision(string id, out DecisionCatalogRow row)
{
row = null;
if (string.IsNullOrEmpty(id)) return false;
return Decisions.TryGetValue(id.Trim(), out row) && row != null;
}
public static bool TryGetDecisionAction(string id, out string action)
{
action = "";
if (!TryGetDecision(id, out DecisionCatalogRow row)) return false;
action = row.Action ?? "";
return !string.IsNullOrEmpty(action);
}
public static float DecisionStrengthOrDefault(string id, float fallback = DefaultDecisionStrength)
{
if (TryGetDecision(id, out DecisionCatalogRow row) && row.Strength > 0f) return row.Strength;
return fallback;
}
public static float SpeciesBonus(string speciesId)
{
if (string.IsNullOrEmpty(speciesId)) return 0f;
return SpeciesBonuses.TryGetValue(speciesId.Trim(), out float bonus) ? bonus : 0f;
}
public static float CharacterBonus(string roleId, float scoringModelFallback)
{
if (string.IsNullOrEmpty(roleId)) return scoringModelFallback;
if (CharacterBonuses.TryGetValue(roleId.Trim(), out float bonus)) return bonus;
return scoringModelFallback;
}
public static bool Reload()
{
string folder = ModClass.ModFolder;
if (string.IsNullOrEmpty(folder)) return false;
LoadFromModFolder(folder);
return _loadedFromFile;
}
}

View file

@ -3,67 +3,66 @@ using System.Collections.Generic;
namespace IdleSpectator;
/// <summary>Authored interest overlay for every live book_types asset.</summary>
/// <summary>Book dials from <c>event-catalog.json</c> <c>books</c>.</summary>
public static partial class EventCatalog
{
public static class Book
{
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
static Book()
{
Add("family_story", 52f, "Culture", "{a} writes a family story");
Add("love_story", 58f, "Culture", "{a} writes a love story");
Add("friendship_story", 50f, "Culture", "{a} writes a friendship story");
Add("bad_story_about_king", 62f, "Culture", "{a} writes a scandalous book");
Add("fable", 48f, "Culture", "{a} writes a fable");
Add("warfare_manual", 60f, "Culture", "{a} writes a warfare manual");
Add("economy_manual", 48f, "Culture", "{a} writes an economy manual");
Add("stewardship_manual", 48f, "Culture", "{a} writes a stewardship manual");
Add("diplomacy_manual", 55f, "Culture", "{a} writes a diplomacy manual");
Add("mathbook", 45f, "Culture", "{a} writes a math book");
Add("biology_book", 45f, "Culture", "{a} writes a biology book");
Add("history_book", 55f, "Culture", "{a} writes a history book");
}
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
private static void Add(
string id,
float strength,
string category,
string label)
{
Entries[id] = new DiscreteEventEntry
private static int _appliedGeneration = -1;
public static void InvalidateOverlays()
{
Id = id,
EventStrength = strength,
Category = category,
CreatesInterest = true,
LabelTemplate = label
};
}
public static IEnumerable<string> AuthoredIds => Entries.Keys;
public static bool HasAuthored(string id) =>
!string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim());
public static DiscreteEventEntry GetOrFallback(string id)
{
if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry))
{
return entry;
Entries.Clear();
_appliedGeneration = -1;
}
string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim();
return new DiscreteEventEntry
private static void Ensure()
{
Id = key,
EventStrength = 45f,
Category = "Culture",
LabelTemplate = "{a} writes a book",
IsFallback = true
};
if (_appliedGeneration == EventCatalogConfig.Generation)
{
return;
}
_appliedGeneration = EventCatalogConfig.Generation;
EventCatalogConfig.FillBooks(Entries);
}
public static IEnumerable<string> AuthoredIds
{
get
{
Ensure();
return Entries.Keys;
}
}
public static bool HasAuthored(string id)
{
Ensure();
return !string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim());
}
public static DiscreteEventEntry GetOrFallback(string id)
{
Ensure();
if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry))
{
return entry;
}
string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim();
return new DiscreteEventEntry
{
Id = key,
EventStrength = 45f,
Category = "Culture",
LabelTemplate = "{a} writes a book",
CreatesInterest = true,
IsFallback = true
};
}
}
}
}

View file

@ -0,0 +1,406 @@
using System;
using System.Collections.Generic;
namespace IdleSpectator;
/// <summary>
/// Decision interest dials + authored action phrases.
/// <para>
/// <b>Source of truth:</b> <c>event-catalog.json</c> (<c>decisions</c> rows: action + strength).
/// <see cref="BuiltinActionPhrases"/> is the offline fallback when that file is missing.
/// Live ids still seed via <see cref="LiveLibraryInterest"/>; camera Signal uses
/// <see cref="IsCameraWorthy"/>; missing ActionPhrase falls back to a generic humanize.
/// </para>
/// </summary>
public static partial class EventCatalog
{
public static class Decision
{
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
private static int _appliedOverlayGeneration = -1;
/// <summary>
/// Offline fallback infinitive clauses when event-catalog.json is missing a row.
/// Prefer editing <c>event-catalog.json</c> instead.
/// </summary>
private static readonly Dictionary<string, string> BuiltinActionPhrases =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
// Social / crime
["find_lover"] = "find a lover",
["try_to_steal_money"] = "try to steal money",
["banish_unruly_clan_members"] = "banish unruly clan members",
["kill_unruly_clan_members"] = "kill unruly clan members",
// Plots
["try_new_plot"] = "try a new plot",
// King kingdom policy
["king_change_kingdom_culture"] = "change the kingdom's culture",
["king_change_kingdom_language"] = "change the kingdom's language",
["king_change_kingdom_religion"] = "change the kingdom's religion",
// City leader policy
["leader_change_city_culture"] = "change the city's culture",
["leader_change_city_language"] = "change the city's language",
["leader_change_city_religion"] = "change the city's religion",
// Army / warrior (keep train-with-dummy authored for if we ever Signal it)
["warrior_army_follow_leader"] = "follow their leader",
["warrior_train_with_dummy"] = "train with a dummy",
["warrior_try_join_army_group"] = "try to join an army",
};
/// <summary>Drop cached rows so the next Ensure re-applies JSON overlays.</summary>
public static void InvalidateOverlays()
{
Entries.Clear();
_appliedOverlayGeneration = -1;
}
private static bool IsInterestingDecision(string id)
{
if (string.IsNullOrEmpty(id))
{
return false;
}
string s = id.ToLowerInvariant();
// Pure AI ticks / daily fluff - not story beats.
if (s.Contains("idle")
|| s.Contains("walking")
|| s.Contains("check")
|| s.Contains("wait")
|| s.Contains("sleep")
|| s.Contains("random")
|| s.Contains("play")
|| s.Contains("jump")
|| s.Contains("flip")
|| s.Contains("diet_")
|| s.Contains("move")
|| s.Contains("swim")
|| s.Contains("follow_parent")
|| s.Contains("follow_desire")
|| s.Contains("reflection")
|| s.Contains("poop"))
{
return false;
}
return HasToken(s, "war")
|| s.Contains("rebellion")
|| s.Contains("revolt")
|| s.Contains("alliance")
|| s.Contains("coup")
|| s.Contains("betray")
|| s.Contains("declare")
|| s.Contains("invade")
|| s.Contains("siege")
|| s.Contains("found")
|| s.Contains("city_foundation")
|| s.Contains("army")
|| s.Contains("lover")
|| s.Contains("plot")
|| s.Contains("marry")
|| s.Contains("banish")
|| s.Contains("steal")
|| s.Contains("kill_unruly")
|| HasToken(s, "king")
|| HasToken(s, "leader")
|| s.Contains("clan")
|| s.Contains("religion")
|| s.Contains("culture")
|| s.Contains("baby_make")
|| s.Contains("have_child")
|| s.Contains("birth");
}
/// <summary>Underscore-token match so <c>war</c> does not hit <c>warrior</c>.</summary>
private static bool HasToken(string haystack, string token)
{
if (string.IsNullOrEmpty(haystack) || string.IsNullOrEmpty(token))
{
return false;
}
if (haystack == token)
{
return true;
}
return haystack.StartsWith(token + "_", StringComparison.Ordinal)
|| haystack.EndsWith("_" + token, StringComparison.Ordinal)
|| haystack.Contains("_" + token + "_");
}
/// <summary>True when a decision id should be allowed to own the idle camera.</summary>
public static bool IsCameraWorthy(string id) => IsInterestingDecision(id);
/// <summary>Authored action-phrase ids (JSON catalog builtin fallback).</summary>
public static IEnumerable<string> AuthoredActionPhraseIds
{
get
{
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (string id in EventCatalogConfig.DecisionIds)
{
if (seen.Add(id))
{
yield return id;
}
}
foreach (string id in BuiltinActionPhrases.Keys)
{
if (seen.Add(id))
{
yield return id;
}
}
}
}
public static bool TryGetAuthoredActionPhrase(string id, out string actionPhrase)
{
actionPhrase = "";
if (string.IsNullOrEmpty(id))
{
return false;
}
string key = id.Trim();
if (EventCatalogConfig.TryGetDecisionAction(key, out actionPhrase))
{
return true;
}
return BuiltinActionPhrases.TryGetValue(key, out actionPhrase)
&& !string.IsNullOrEmpty(actionPhrase);
}
/// <summary>
/// Infinitive clause for <c>decides to …</c>. Prefers event-catalog.json / builtin,
/// else a generic humanize of the asset id.
/// </summary>
public static string ActionPhraseFor(string decisionId)
{
Ensure();
if (string.IsNullOrEmpty(decisionId))
{
return "";
}
string key = decisionId.Trim();
if (Entries.TryGetValue(key, out DiscreteEventEntry entry)
&& !string.IsNullOrEmpty(entry.ActionPhrase))
{
return entry.ActionPhrase;
}
if (TryGetAuthoredActionPhrase(key, out string authored))
{
return authored;
}
return FallbackHumanize(key);
}
private static void Ensure()
{
if (_appliedOverlayGeneration != EventCatalogConfig.Generation)
{
Entries.Clear();
_appliedOverlayGeneration = EventCatalogConfig.Generation;
}
LiveLibraryInterest.EnsureSeeded(
Entries,
ActivityAssetCatalog.EnumerateLiveDecisionIds,
"Politics",
"{a} decides to act",
ambientStrength: 40f,
signalIds: null,
signalStrength: EventCatalogConfig.DefaultDecisionStrength,
signalPredicate: IsInterestingDecision,
ambientCreatesInterest: false);
// Apply authored prose + per-id strength (JSON wins over builtin).
foreach (string id in AuthoredActionPhraseIds)
{
if (!TryGetAuthoredActionPhrase(id, out string action) || string.IsNullOrEmpty(action))
{
continue;
}
float strength = EventCatalogConfig.DecisionStrengthOrDefault(
id,
IsInterestingDecision(id) ? EventCatalogConfig.DefaultDecisionStrength : 40f);
string label = "{a} decides to " + action;
if (Entries.TryGetValue(id, out DiscreteEventEntry existing))
{
existing.ActionPhrase = action;
existing.LabelTemplate = label;
if (EventCatalogConfig.TryGetDecision(id, out _))
{
existing.EventStrength = strength;
}
continue;
}
Entries[id] = new DiscreteEventEntry
{
Id = id,
EventStrength = strength,
Category = "Politics",
CreatesInterest = IsInterestingDecision(id),
ActionPhrase = action,
LabelTemplate = label
};
}
}
public static IEnumerable<string> AuthoredIds
{
get
{
Ensure();
return Entries.Keys;
}
}
public static DiscreteEventEntry GetOrFallback(string id)
{
Ensure();
DiscreteEventEntry entry = LiveLibraryInterest.Lookup(
Entries, id, "Politics", "{a} decides to act", 35f);
if (!entry.IsFallback && string.IsNullOrEmpty(entry.ActionPhrase))
{
// Attach fallback humanize so callers can still read a clause.
entry.ActionPhrase = FallbackHumanize(entry.Id);
if (!string.IsNullOrEmpty(entry.ActionPhrase))
{
entry.LabelTemplate = "{a} decides to " + entry.ActionPhrase;
}
}
return entry;
}
/// <summary>
/// Generic id → infinitive clause when no authored row exists.
/// Prefer adding a row to <c>event-catalog.json</c> instead of relying on this.
/// </summary>
public static string FallbackHumanize(string decisionId)
{
if (string.IsNullOrEmpty(decisionId))
{
return "";
}
string raw = decisionId.Trim().Replace('-', '_').ToLowerInvariant();
if (raw.StartsWith("type_", StringComparison.Ordinal))
{
raw = raw.Substring(5);
}
string[] parts = raw.Split(new[] { '_' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 0)
{
return "";
}
int start = 0;
while (start < parts.Length
&& (parts[start] == "child"
|| parts[start] == "baby"
|| parts[start] == "adult"
|| parts[start] == "warrior"
|| parts[start] == "city"
|| parts[start] == "actor"
|| parts[start] == "animal"
|| parts[start] == "herd"
|| parts[start] == "civ"
|| parts[start] == "mob"
|| parts[start] == "king"
|| parts[start] == "leader"))
{
start++;
}
if (start >= parts.Length)
{
start = 0;
}
var words = new List<string>(parts.Length - start);
for (int i = start; i < parts.Length; i++)
{
words.Add(parts[i]);
}
if (words.Count == 0)
{
return "";
}
if (words.Count >= 3
&& words[0] == "change"
&& (words[1] == "kingdom" || words[1] == "city" || words[1] == "clan"))
{
return "change the " + words[1] + "'s " + string.Join(" ", words.GetRange(2, words.Count - 2));
}
if (words[0] == "diet" && words.Count >= 2)
{
words[0] = "eat";
}
if (words.Count >= 2 && words[0] == "try" && words[1] == "new")
{
words.Insert(1, "a");
return string.Join(" ", words);
}
if (words.Count == 2 && words[0] == "find")
{
return "find a " + words[1];
}
if (words[0] == "join" && words.Count >= 2 && words[1] == "army")
{
return "join an army";
}
if (words[0] == "try" && words.Count >= 2 && words[1] != "to")
{
words.Insert(1, "to");
}
if (words.Count >= 4
&& words[0] == "try"
&& words[1] == "to"
&& words[2] == "join"
&& words[3] == "army")
{
return "try to join an army";
}
if (words.Count >= 3 && words[0] == "train" && words[1] == "with")
{
return "train with a " + string.Join(" ", words.GetRange(2, words.Count - 2));
}
if (words.Count >= 2
&& words[words.Count - 1] == "leader"
&& (words[0] == "follow" || (words[0] == "army" && words[1] == "follow")))
{
return "follow their leader";
}
return string.Join(" ", words);
}
}
}

View file

@ -13,82 +13,77 @@ public sealed class DisasterInterestEntry
public bool IsFallback;
}
/// <summary>Authored interest overlay for every live disasters library asset.</summary>
/// <summary>Disaster dials from <c>event-catalog.json</c> <c>disasters</c>.</summary>
public static partial class EventCatalog
{
public static class Disaster
{
private static readonly Dictionary<string, DisasterInterestEntry> Entries =
new Dictionary<string, DisasterInterestEntry>(StringComparer.OrdinalIgnoreCase);
static Disaster()
{
Add("tornado", 95f, "disaster_tornado");
Add("heatwave", 88f, "disaster_heatwave");
Add("small_meteorite", 95f, "disaster_meteorite");
Add("small_earthquake", 95f, "disaster_earthquake");
Add("hellspawn", 96f, "disaster_hellspawn");
Add("ice_ones_awoken", 95f, "disaster_ice_ones");
Add("sudden_snowman", 90f, "disaster_sudden_snowman");
Add("garden_surprise", 90f, "disaster_garden_surprise");
Add("dragon_from_farlands", 98f, "disaster_dragon_from_farlands");
Add("ash_bandits", 92f, "disaster_bandits");
Add("alien_invasion", 97f, "disaster_alien_invasion");
Add("biomass", 96f, "disaster_biomass");
Add("tumor", 96f, "disaster_tumor");
Add("wild_mage", 94f, "disaster_evil_mage");
Add("underground_necromancer", 95f, "disaster_underground_necromancer");
Add("mad_thoughts", 90f, "disaster_mad_thoughts");
Add("greg_abominations", 96f, "disaster_greg_abominations");
}
private static readonly Dictionary<string, DisasterInterestEntry> Entries =
new Dictionary<string, DisasterInterestEntry>(StringComparer.OrdinalIgnoreCase);
private static void Add(string id, float strength, string worldLogId)
{
Entries[id] = new DisasterInterestEntry
private static int _appliedGeneration = -1;
public static void InvalidateOverlays()
{
Id = id,
EventStrength = strength,
WorldLogId = worldLogId,
CreatesInterest = true,
Category = "Disaster"
};
}
public static IEnumerable<string> AuthoredIds => Entries.Keys;
public static bool HasAuthored(string disasterId)
{
return !string.IsNullOrEmpty(disasterId) && Entries.ContainsKey(disasterId.Trim());
}
public static bool TryGet(string disasterId, out DisasterInterestEntry entry)
{
entry = null;
if (string.IsNullOrEmpty(disasterId))
{
return false;
Entries.Clear();
_appliedGeneration = -1;
}
return Entries.TryGetValue(disasterId.Trim(), out entry);
}
public static DisasterInterestEntry GetOrFallback(string disasterId)
{
if (TryGet(disasterId, out DisasterInterestEntry entry))
private static void Ensure()
{
return entry;
if (_appliedGeneration == EventCatalogConfig.Generation)
{
return;
}
_appliedGeneration = EventCatalogConfig.Generation;
EventCatalogConfig.FillDisasters(Entries);
}
string id = string.IsNullOrEmpty(disasterId) ? "unknown" : disasterId.Trim();
return new DisasterInterestEntry
public static IEnumerable<string> AuthoredIds
{
Id = id,
EventStrength = 90f,
Category = "Disaster",
WorldLogId = "",
CreatesInterest = true,
IsFallback = true
};
get
{
Ensure();
return Entries.Keys;
}
}
public static bool HasAuthored(string disasterId)
{
Ensure();
return !string.IsNullOrEmpty(disasterId) && Entries.ContainsKey(disasterId.Trim());
}
public static bool TryGet(string disasterId, out DisasterInterestEntry entry)
{
Ensure();
entry = null;
if (string.IsNullOrEmpty(disasterId))
{
return false;
}
return Entries.TryGetValue(disasterId.Trim(), out entry);
}
public static DisasterInterestEntry GetOrFallback(string disasterId)
{
if (TryGet(disasterId, out DisasterInterestEntry entry))
{
return entry;
}
string id = string.IsNullOrEmpty(disasterId) ? "unknown" : disasterId.Trim();
return new DisasterInterestEntry
{
Id = id,
EventStrength = 90f,
Category = "Disaster",
WorldLogId = "",
CreatesInterest = true,
IsFallback = true
};
}
}
}
}

View file

@ -3,66 +3,66 @@ using System.Collections.Generic;
namespace IdleSpectator;
/// <summary>Authored interest overlay for every live era_library asset.</summary>
/// <summary>Era dials from <c>event-catalog.json</c> <c>eras</c>.</summary>
public static partial class EventCatalog
{
public static class Era
{
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
static Era()
{
Add("age_hope", 78f, "World", "Age of Hope");
Add("age_sun", 80f, "World", "Age of Sun");
Add("age_dark", 88f, "World", "Age of Dark");
Add("age_tears", 86f, "World", "Age of Tears");
Add("age_moon", 82f, "World", "Age of Moon");
Add("age_chaos", 92f, "World", "Age of Chaos");
Add("age_wonders", 84f, "World", "Age of Wonders");
Add("age_ice", 87f, "World", "Age of Ice");
Add("age_ash", 90f, "World", "Age of Ash");
Add("age_despair", 91f, "World", "Age of Despair");
Add("age_unknown", 70f, "World", "Unknown age");
}
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
private static void Add(
string id,
float strength,
string category,
string label)
{
Entries[id] = new DiscreteEventEntry
private static int _appliedGeneration = -1;
public static void InvalidateOverlays()
{
Id = id,
EventStrength = strength,
Category = category,
CreatesInterest = true,
LabelTemplate = label
};
}
public static IEnumerable<string> AuthoredIds => Entries.Keys;
public static bool HasAuthored(string id) =>
!string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim());
public static DiscreteEventEntry GetOrFallback(string id)
{
if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry))
{
return entry;
Entries.Clear();
_appliedGeneration = -1;
}
string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim();
return new DiscreteEventEntry
private static void Ensure()
{
Id = key,
EventStrength = 70f,
Category = "World",
LabelTemplate = "Era ({id})",
IsFallback = true
};
if (_appliedGeneration == EventCatalogConfig.Generation)
{
return;
}
_appliedGeneration = EventCatalogConfig.Generation;
EventCatalogConfig.FillEras(Entries);
}
public static IEnumerable<string> AuthoredIds
{
get
{
Ensure();
return Entries.Keys;
}
}
public static bool HasAuthored(string id)
{
Ensure();
return !string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim());
}
public static DiscreteEventEntry GetOrFallback(string id)
{
Ensure();
if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry))
{
return entry;
}
string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim();
return new DiscreteEventEntry
{
Id = key,
EventStrength = 70f,
Category = "World",
LabelTemplate = "Era ({id})",
CreatesInterest = true,
IsFallback = true
};
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -3,416 +3,519 @@ using System.Collections.Generic;
namespace IdleSpectator;
/// <summary>Live AssetManager library event dials (powers, genes, …).</summary>
/// <summary>
/// Live AssetManager library event dials (powers, genes, …).
/// Strengths/labels from <c>event-catalog.json</c> <c>libraries</c>; signal predicates stay in C#.
/// </summary>
public static partial class EventCatalog
{
private static LibraryCatalogDefaults LibOr(
string key,
float ambient,
float signal,
string category,
string label,
bool ambientCreatesInterest)
{
if (EventCatalogConfig.TryGetLibraryDefaults(key, out LibraryCatalogDefaults d) && d != null)
{
return d;
}
return new LibraryCatalogDefaults
{
AmbientStrength = ambient,
SignalStrength = signal,
Category = category,
Label = label,
AmbientCreatesInterest = ambientCreatesInterest
};
}
public static class Spell
{
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
private static readonly HashSet<string> SignalIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"summon_lightning", "summon_tornado", "cast_curse", "cast_fire",
"cast_blood_rain", "summon_meteor", "teleport"
};
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
private static void Ensure() =>
LiveLibraryInterest.EnsureSeeded(
Entries,
ActivityAssetCatalog.EnumerateLiveSpellIds,
"Spectacle",
"{a} casts {id}",
ambientStrength: 48f,
SignalIds,
signalStrength: 88f,
ambientCreatesInterest: false);
private static int _appliedGeneration = -1;
public static IEnumerable<string> AuthoredIds
{
get
public static void InvalidateOverlays()
{
Entries.Clear();
_appliedGeneration = -1;
}
private static void Ensure()
{
if (_appliedGeneration == EventCatalogConfig.Generation && Entries.Count > 0)
{
return;
}
Entries.Clear();
_appliedGeneration = EventCatalogConfig.Generation;
LibraryCatalogDefaults d = LibOr("spell", 48f, 88f, "Spectacle", "{a} casts {id}", true);
HashSet<string> signals = d.SignalIds.Count > 0
? d.SignalIds
: new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"summon_lightning", "summon_tornado", "cast_curse", "cast_fire",
"cast_blood_rain", "summon_meteor", "teleport"
};
LiveLibraryInterest.EnsureSeeded(
Entries,
ActivityAssetCatalog.EnumerateLiveSpellIds,
d.Category,
d.Label,
d.AmbientStrength,
signals,
d.SignalStrength,
ambientCreatesInterest: d.AmbientCreatesInterest);
}
public static IEnumerable<string> AuthoredIds
{
get
{
Ensure();
return Entries.Keys;
}
}
public static DiscreteEventEntry GetOrFallback(string id)
{
Ensure();
return Entries.Keys;
LibraryCatalogDefaults d = LibOr("spell", 48f, 88f, "Spectacle", "{a} casts {id}", true);
return LiveLibraryInterest.Lookup(Entries, id, d.Category, d.Label, 55f);
}
}
public static DiscreteEventEntry GetOrFallback(string id)
{
Ensure();
return LiveLibraryInterest.Lookup(Entries, id, "Spectacle", "{a} casts {id}", 55f);
}
}
/// <summary>God powers library - mostly Ambient; Signal for disaster/spectacle ids.</summary>
public static class Power
{
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
private static bool IsSpectaclePower(string id)
{
if (string.IsNullOrEmpty(id))
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
private static int _appliedGeneration = -1;
public static void InvalidateOverlays()
{
return false;
Entries.Clear();
_appliedGeneration = -1;
}
string s = id.ToLowerInvariant();
return s.Contains("meteor")
|| s.Contains("earthquake")
|| s.Contains("tornado")
|| s.Contains("volcano")
|| s.Contains("nuke")
|| s.Contains("bomb")
|| s.Contains("rain_blood")
|| s.Contains("hell")
|| s.Contains("lightning")
|| s.Contains("storm")
|| s.Contains("acid")
|| s.Contains("fire")
|| s.Contains("disaster");
}
private static bool IsSpectaclePower(string id)
{
if (string.IsNullOrEmpty(id))
{
return false;
}
private static void Ensure() =>
LiveLibraryInterest.EnsureSeeded(
Entries,
ActivityAssetCatalog.EnumerateLivePowerIds,
"Spectacle",
"God power: {id}",
ambientStrength: 28f,
signalIds: null,
signalStrength: 92f,
signalPredicate: IsSpectaclePower,
ambientCreatesInterest: false);
string s = id.ToLowerInvariant();
return s.Contains("meteor")
|| s.Contains("earthquake")
|| s.Contains("tornado")
|| s.Contains("volcano")
|| s.Contains("nuke")
|| s.Contains("bomb")
|| s.Contains("rain_blood")
|| s.Contains("hell")
|| s.Contains("lightning")
|| s.Contains("storm")
|| s.Contains("acid")
|| s.Contains("fire")
|| s.Contains("disaster");
}
public static IEnumerable<string> AuthoredIds
{
get
private static void Ensure()
{
if (_appliedGeneration == EventCatalogConfig.Generation && Entries.Count > 0)
{
return;
}
Entries.Clear();
_appliedGeneration = EventCatalogConfig.Generation;
LibraryCatalogDefaults d = LibOr("power", 28f, 92f, "Spectacle", "God power: {id}", false);
LiveLibraryInterest.EnsureSeeded(
Entries,
ActivityAssetCatalog.EnumerateLivePowerIds,
d.Category,
d.Label,
d.AmbientStrength,
signalIds: null,
d.SignalStrength,
signalPredicate: IsSpectaclePower,
ambientCreatesInterest: d.AmbientCreatesInterest);
}
public static IEnumerable<string> AuthoredIds
{
get
{
Ensure();
return Entries.Keys;
}
}
public static DiscreteEventEntry GetOrFallback(string id)
{
Ensure();
return Entries.Keys;
LibraryCatalogDefaults d = LibOr("power", 28f, 92f, "Spectacle", "God power: {id}", false);
return LiveLibraryInterest.Lookup(Entries, id, d.Category, d.Label, 30f);
}
}
public static DiscreteEventEntry GetOrFallback(string id)
{
Ensure();
return LiveLibraryInterest.Lookup(Entries, id, "Spectacle", "God power: {id}", 30f);
}
}
/// <summary>AI decisions - Signal for war/rebellion/kingdom-affecting; Ambient otherwise.</summary>
public static class Decision
{
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
private static bool IsKingdomDecision(string id)
{
if (string.IsNullOrEmpty(id))
{
return false;
}
string s = id.ToLowerInvariant();
// Routine AI ticks often include "king" in the name - those are not camera events.
if (s.Contains("idle")
|| s.Contains("walking")
|| s.Contains("check")
|| s.Contains("wait")
|| s.Contains("sleep"))
{
return false;
}
return s.Contains("war")
|| s.Contains("rebellion")
|| s.Contains("revolt")
|| s.Contains("alliance")
|| s.Contains("coup")
|| s.Contains("betray")
|| s.Contains("declare")
|| s.Contains("invade")
|| s.Contains("siege")
|| s.Contains("found")
|| s.Contains("city_foundation")
|| (s.Contains("king") && (s.Contains("war") || s.Contains("city") || s.Contains("rebellion")));
}
/// <summary>True when a decision id should be allowed to own the idle camera.</summary>
public static bool IsCameraWorthy(string id) => IsKingdomDecision(id);
private static void Ensure() =>
LiveLibraryInterest.EnsureSeeded(
Entries,
ActivityAssetCatalog.EnumerateLiveDecisionIds,
"Politics",
"{a} decides {id}",
ambientStrength: 32f,
signalIds: null,
signalStrength: 86f,
signalPredicate: IsKingdomDecision,
ambientCreatesInterest: false);
public static IEnumerable<string> AuthoredIds
{
get
{
Ensure();
return Entries.Keys;
}
}
public static DiscreteEventEntry GetOrFallback(string id)
{
Ensure();
return LiveLibraryInterest.Lookup(Entries, id, "Politics", "{a} decides {id}", 35f);
}
}
/// <summary>Items library - Signal for legendary/wonder subset.</summary>
public static class Item
{
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
private static bool IsLegendaryItem(string id)
{
if (string.IsNullOrEmpty(id))
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
private static int _appliedGeneration = -1;
public static void InvalidateOverlays()
{
return false;
Entries.Clear();
_appliedGeneration = -1;
}
string s = id.ToLowerInvariant();
return s.Contains("legendary")
|| s.Contains("mythic")
|| s.Contains("artifact")
|| s.Contains("relic")
|| s.Contains("divine")
|| s.Contains("demon")
|| s.Contains("dragon")
|| s.Contains("nuke")
|| s.Contains("excalibur")
|| s.Contains("wonder");
}
private static bool IsLegendaryItem(string id)
{
if (string.IsNullOrEmpty(id))
{
return false;
}
private static void Ensure() =>
LiveLibraryInterest.EnsureSeeded(
Entries,
ActivityAssetCatalog.EnumerateLiveItemIds,
"Item",
"{a} forges {id}",
ambientStrength: 36f,
signalIds: null,
signalStrength: 80f,
signalPredicate: IsLegendaryItem,
ambientCreatesInterest: false);
string s = id.ToLowerInvariant();
return s.Contains("legendary")
|| s.Contains("mythic")
|| s.Contains("artifact")
|| s.Contains("relic")
|| s.Contains("divine")
|| s.Contains("demon")
|| s.Contains("dragon")
|| s.Contains("nuke")
|| s.Contains("excalibur")
|| s.Contains("wonder");
}
public static IEnumerable<string> AuthoredIds
{
get
private static void Ensure()
{
if (_appliedGeneration == EventCatalogConfig.Generation && Entries.Count > 0)
{
return;
}
Entries.Clear();
_appliedGeneration = EventCatalogConfig.Generation;
LibraryCatalogDefaults d = LibOr("item", 36f, 80f, "Item", "{a} forges {id}", true);
LiveLibraryInterest.EnsureSeeded(
Entries,
ActivityAssetCatalog.EnumerateLiveItemIds,
d.Category,
d.Label,
d.AmbientStrength,
signalIds: null,
d.SignalStrength,
signalPredicate: IsLegendaryItem,
ambientCreatesInterest: d.AmbientCreatesInterest);
}
public static IEnumerable<string> AuthoredIds
{
get
{
Ensure();
return Entries.Keys;
}
}
public static DiscreteEventEntry GetOrFallback(string id)
{
Ensure();
return Entries.Keys;
LibraryCatalogDefaults d = LibOr("item", 36f, 80f, "Item", "{a} forges {id}", true);
return LiveLibraryInterest.Lookup(Entries, id, d.Category, d.Label, 40f);
}
}
public static DiscreteEventEntry GetOrFallback(string id)
{
Ensure();
return LiveLibraryInterest.Lookup(Entries, id, "Item", "{a} forges {id}", 40f);
}
}
/// <summary>Subspecies traits - Signal for dramatic; Ambient fill-capped.</summary>
public static class SubspeciesTrait
{
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
private static bool IsDramatic(string id)
{
if (string.IsNullOrEmpty(id))
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
private static int _appliedGeneration = -1;
public static void InvalidateOverlays()
{
return false;
Entries.Clear();
_appliedGeneration = -1;
}
string s = id.ToLowerInvariant();
return s.Contains("immortal")
|| s.Contains("giant")
|| s.Contains("tiny")
|| s.Contains("fire")
|| s.Contains("frost")
|| s.Contains("poison")
|| s.Contains("plague")
|| s.Contains("magic")
|| s.Contains("divine")
|| s.Contains("demon")
|| s.Contains("undead")
|| s.Contains("dragon")
|| s.Contains("evolve")
|| s.Contains("mutate");
}
private static bool IsDramatic(string id)
{
if (string.IsNullOrEmpty(id))
{
return false;
}
private static void Ensure() =>
LiveLibraryInterest.EnsureSeeded(
Entries,
ActivityAssetCatalog.EnumerateLiveSubspeciesTraitIds,
"Evolution",
"{a} evolves {id}",
ambientStrength: 34f,
signalIds: null,
signalStrength: 78f,
signalPredicate: IsDramatic);
string s = id.ToLowerInvariant();
return s.Contains("immortal")
|| s.Contains("giant")
|| s.Contains("tiny")
|| s.Contains("fire")
|| s.Contains("frost")
|| s.Contains("poison")
|| s.Contains("plague")
|| s.Contains("magic")
|| s.Contains("divine")
|| s.Contains("demon")
|| s.Contains("undead")
|| s.Contains("dragon")
|| s.Contains("evolve")
|| s.Contains("mutate");
}
public static IEnumerable<string> AuthoredIds
{
get
private static void Ensure()
{
if (_appliedGeneration == EventCatalogConfig.Generation && Entries.Count > 0)
{
return;
}
Entries.Clear();
_appliedGeneration = EventCatalogConfig.Generation;
LibraryCatalogDefaults d = LibOr("subspeciesTrait", 34f, 78f, "Evolution", "{a} evolves {id}", true);
LiveLibraryInterest.EnsureSeeded(
Entries,
ActivityAssetCatalog.EnumerateLiveSubspeciesTraitIds,
d.Category,
d.Label,
d.AmbientStrength,
signalIds: null,
d.SignalStrength,
signalPredicate: IsDramatic,
ambientCreatesInterest: d.AmbientCreatesInterest);
}
public static IEnumerable<string> AuthoredIds
{
get
{
Ensure();
return Entries.Keys;
}
}
public static DiscreteEventEntry GetOrFallback(string id)
{
Ensure();
return Entries.Keys;
LibraryCatalogDefaults d = LibOr("subspeciesTrait", 34f, 78f, "Evolution", "{a} evolves {id}", true);
return LiveLibraryInterest.Lookup(Entries, id, d.Category, d.Label, 36f);
}
}
public static DiscreteEventEntry GetOrFallback(string id)
{
Ensure();
return LiveLibraryInterest.Lookup(Entries, id, "Evolution", "{a} evolves {id}", 36f);
}
}
/// <summary>Genes - Ambient default.</summary>
public static class Gene
{
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
private static void Ensure() =>
LiveLibraryInterest.EnsureSeeded(
Entries,
ActivityAssetCatalog.EnumerateLiveGeneIds,
"Genetics",
"{a} gains gene {id}",
ambientStrength: 30f,
signalIds: null,
signalStrength: 70f,
signalPredicate: id => id != null && (id.Contains("warfare") || id.Contains("magic") || id.Contains("mutation")),
ambientCreatesInterest: false);
public static IEnumerable<string> AuthoredIds
{
get
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
private static int _appliedGeneration = -1;
public static void InvalidateOverlays()
{
Entries.Clear();
_appliedGeneration = -1;
}
private static void Ensure()
{
if (_appliedGeneration == EventCatalogConfig.Generation && Entries.Count > 0)
{
return;
}
Entries.Clear();
_appliedGeneration = EventCatalogConfig.Generation;
LibraryCatalogDefaults d = LibOr("gene", 30f, 70f, "Genetics", "{a} gains gene {id}", true);
LiveLibraryInterest.EnsureSeeded(
Entries,
ActivityAssetCatalog.EnumerateLiveGeneIds,
d.Category,
d.Label,
d.AmbientStrength,
signalIds: null,
d.SignalStrength,
signalPredicate: id => id != null && (id.Contains("warfare") || id.Contains("magic") || id.Contains("mutation")),
ambientCreatesInterest: d.AmbientCreatesInterest);
}
public static IEnumerable<string> AuthoredIds
{
get
{
Ensure();
return Entries.Keys;
}
}
public static DiscreteEventEntry GetOrFallback(string id)
{
Ensure();
return Entries.Keys;
LibraryCatalogDefaults d = LibOr("gene", 30f, 70f, "Genetics", "{a} gains gene {id}", true);
return LiveLibraryInterest.Lookup(Entries, id, d.Category, d.Label, 30f);
}
}
public static DiscreteEventEntry GetOrFallback(string id)
{
Ensure();
return LiveLibraryInterest.Lookup(Entries, id, "Genetics", "{a} gains gene {id}", 30f);
}
}
/// <summary>Phenotypes - Ambient default.</summary>
public static class Phenotype
{
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
private static void Ensure() =>
LiveLibraryInterest.EnsureSeeded(
Entries,
ActivityAssetCatalog.EnumerateLivePhenotypeIds,
"Genetics",
"{a} shows {id}",
ambientStrength: 28f,
signalIds: null,
signalStrength: 60f,
ambientCreatesInterest: false);
public static IEnumerable<string> AuthoredIds
{
get
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
private static int _appliedGeneration = -1;
public static void InvalidateOverlays()
{
Entries.Clear();
_appliedGeneration = -1;
}
private static void Ensure()
{
if (_appliedGeneration == EventCatalogConfig.Generation && Entries.Count > 0)
{
return;
}
Entries.Clear();
_appliedGeneration = EventCatalogConfig.Generation;
LibraryCatalogDefaults d = LibOr("phenotype", 28f, 60f, "Genetics", "{a} shows {id}", true);
LiveLibraryInterest.EnsureSeeded(
Entries,
ActivityAssetCatalog.EnumerateLivePhenotypeIds,
d.Category,
d.Label,
d.AmbientStrength,
signalIds: null,
d.SignalStrength,
ambientCreatesInterest: d.AmbientCreatesInterest);
}
public static IEnumerable<string> AuthoredIds
{
get
{
Ensure();
return Entries.Keys;
}
}
public static DiscreteEventEntry GetOrFallback(string id)
{
Ensure();
return Entries.Keys;
LibraryCatalogDefaults d = LibOr("phenotype", 28f, 60f, "Genetics", "{a} shows {id}", true);
return LiveLibraryInterest.Lookup(Entries, id, d.Category, d.Label, 28f);
}
}
public static DiscreteEventEntry GetOrFallback(string id)
{
Ensure();
return LiveLibraryInterest.Lookup(Entries, id, "Genetics", "{a} shows {id}", 28f);
}
}
/// <summary>World laws - Ambient / location-only style.</summary>
public static class WorldLaw
{
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
private static void Ensure() =>
LiveLibraryInterest.EnsureSeeded(
Entries,
ActivityAssetCatalog.EnumerateLiveWorldLawIds,
"WorldLaw",
"Law changed: {id}",
ambientStrength: 40f,
signalIds: null,
signalStrength: 70f,
signalPredicate: id => id != null && (id.Contains("war") || id.Contains("death") || id.Contains("plague")),
ambientCreatesInterest: false);
public static IEnumerable<string> AuthoredIds
{
get
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
private static int _appliedGeneration = -1;
public static void InvalidateOverlays()
{
Entries.Clear();
_appliedGeneration = -1;
}
private static void Ensure()
{
if (_appliedGeneration == EventCatalogConfig.Generation && Entries.Count > 0)
{
return;
}
Entries.Clear();
_appliedGeneration = EventCatalogConfig.Generation;
LibraryCatalogDefaults d = LibOr("worldLaw", 40f, 70f, "WorldLaw", "Law changed: {id}", true);
LiveLibraryInterest.EnsureSeeded(
Entries,
ActivityAssetCatalog.EnumerateLiveWorldLawIds,
d.Category,
d.Label,
d.AmbientStrength,
signalIds: null,
d.SignalStrength,
signalPredicate: id => id != null && (id.Contains("war") || id.Contains("death") || id.Contains("plague")),
ambientCreatesInterest: d.AmbientCreatesInterest);
}
public static IEnumerable<string> AuthoredIds
{
get
{
Ensure();
return Entries.Keys;
}
}
public static DiscreteEventEntry GetOrFallback(string id)
{
Ensure();
return Entries.Keys;
LibraryCatalogDefaults d = LibOr("worldLaw", 40f, 70f, "WorldLaw", "Law changed: {id}", true);
return LiveLibraryInterest.Lookup(Entries, id, d.Category, d.Label, 40f);
}
}
public static DiscreteEventEntry GetOrFallback(string id)
{
Ensure();
return LiveLibraryInterest.Lookup(Entries, id, "WorldLaw", "Law changed: {id}", 40f);
}
}
/// <summary>Biomes - Ambient.</summary>
public static class Biome
{
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
private static void Ensure() =>
LiveLibraryInterest.EnsureSeeded(
Entries,
ActivityAssetCatalog.EnumerateLiveBiomeIds,
"Biome",
"Biome shifts: {id}",
ambientStrength: 26f,
signalIds: null,
signalStrength: 50f,
ambientCreatesInterest: false);
public static IEnumerable<string> AuthoredIds
{
get
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
private static int _appliedGeneration = -1;
public static void InvalidateOverlays()
{
Entries.Clear();
_appliedGeneration = -1;
}
private static void Ensure()
{
if (_appliedGeneration == EventCatalogConfig.Generation && Entries.Count > 0)
{
return;
}
Entries.Clear();
_appliedGeneration = EventCatalogConfig.Generation;
LibraryCatalogDefaults d = LibOr("biome", 26f, 50f, "Biome", "Biome shifts: {id}", false);
LiveLibraryInterest.EnsureSeeded(
Entries,
ActivityAssetCatalog.EnumerateLiveBiomeIds,
d.Category,
d.Label,
d.AmbientStrength,
signalIds: null,
d.SignalStrength,
ambientCreatesInterest: d.AmbientCreatesInterest);
}
public static IEnumerable<string> AuthoredIds
{
get
{
Ensure();
return Entries.Keys;
}
}
public static DiscreteEventEntry GetOrFallback(string id)
{
Ensure();
return Entries.Keys;
LibraryCatalogDefaults d = LibOr("biome", 26f, 50f, "Biome", "Biome shifts: {id}", false);
return LiveLibraryInterest.Lookup(Entries, id, d.Category, d.Label, 26f);
}
}
public static DiscreteEventEntry GetOrFallback(string id)
{
Ensure();
return LiveLibraryInterest.Lookup(Entries, id, "Biome", "Biome shifts: {id}", 26f);
}
}
}

View file

@ -3,83 +3,66 @@ using System.Collections.Generic;
namespace IdleSpectator;
/// <summary>Authored interest overlay for every live plots_library asset.</summary>
/// <summary>Plot dials from <c>event-catalog.json</c> <c>plots</c>.</summary>
public static partial class EventCatalog
{
public static class Plot
{
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
static Plot()
{
Add("rebellion", 92f, "Politics", "{a} is plotting a rebellion");
Add("new_war", 94f, "Politics", "{a} is plotting a war");
Add("alliance_create", 80f, "Politics", "{a} is plotting an alliance");
Add("alliance_join", 72f, "Politics", "{a} is joining an alliance");
Add("alliance_destroy", 86f, "Politics", "{a} is breaking an alliance");
Add("attacker_stop_war", 78f, "Politics", "{a} is ending a war");
Add("new_book", 55f, "Culture", "{a} is writing a book");
Add("new_language", 62f, "Culture", "{a} is forging a language");
Add("new_religion", 70f, "Culture", "{a} is founding a religion");
Add("new_culture", 68f, "Culture", "{a} is founding a culture");
Add("clan_ascension", 82f, "Politics", "{a} is ascending a clan");
Add("culture_divide", 75f, "Culture", "{a} splits a culture");
Add("religion_schism", 84f, "Culture", "{a} causes a religion schism");
Add("language_divergence", 60f, "Culture", "{a} splits a language");
Add("summon_meteor_rain", 96f, "Spectacle", "{a} summons a meteor rain");
Add("summon_earthquake", 95f, "Spectacle", "{a} summons an earthquake");
Add("summon_thunderstorm", 93f, "Spectacle", "{a} summons a thunderstorm");
Add("summon_stormfront", 93f, "Spectacle", "{a} summons a stormfront");
Add("summon_hellstorm", 97f, "Spectacle", "{a} summons a hellstorm");
Add("summon_demons", 96f, "Spectacle", "{a} summons demons");
Add("summon_angles", 96f, "Spectacle", "{a} summons angels");
Add("summon_skeletons", 92f, "Spectacle", "{a} summons skeletons");
Add("summon_living_plants", 90f, "Spectacle", "{a} summons living plants");
Add("big_cast_coffee", 58f, "Spectacle", "{a} performs a coffee ritual");
Add("big_cast_bubble_shield", 64f, "Spectacle", "{a} casts a bubble shield");
Add("big_cast_madness", 80f, "Spectacle", "{a} casts madness");
Add("big_cast_slowness", 70f, "Spectacle", "{a} casts slowness");
Add("cause_rebellion", 90f, "Politics", "{a} causes a rebellion");
}
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
private static void Add(
string id,
float strength,
string category,
string label)
{
Entries[id] = new DiscreteEventEntry
private static int _appliedGeneration = -1;
public static void InvalidateOverlays()
{
Id = id,
EventStrength = strength,
Category = category,
CreatesInterest = true,
LabelTemplate = label
};
}
public static IEnumerable<string> AuthoredIds => Entries.Keys;
public static bool HasAuthored(string id) =>
!string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim());
public static DiscreteEventEntry GetOrFallback(string id)
{
if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry))
{
return entry;
Entries.Clear();
_appliedGeneration = -1;
}
string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim();
return new DiscreteEventEntry
private static void Ensure()
{
Id = key,
EventStrength = 55f,
Category = "Plot",
LabelTemplate = "{a} · {id}",
IsFallback = true
};
if (_appliedGeneration == EventCatalogConfig.Generation)
{
return;
}
_appliedGeneration = EventCatalogConfig.Generation;
EventCatalogConfig.FillPlots(Entries);
}
public static IEnumerable<string> AuthoredIds
{
get
{
Ensure();
return Entries.Keys;
}
}
public static bool HasAuthored(string id)
{
Ensure();
return !string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim());
}
public static DiscreteEventEntry GetOrFallback(string id)
{
Ensure();
if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry))
{
return entry;
}
string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim();
return new DiscreteEventEntry
{
Id = key,
EventStrength = 55f,
Category = "Plot",
LabelTemplate = "{a} · {id}",
CreatesInterest = false,
IsFallback = true
};
}
}
}
}

View file

@ -3,67 +3,66 @@ using System.Collections.Generic;
namespace IdleSpectator;
/// <summary>Authored discrete relationship lifecycle events (not a live asset library).</summary>
/// <summary>Relationship dials from <c>event-catalog.json</c> <c>relationship</c>.</summary>
public static partial class EventCatalog
{
public static class Relationship
{
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
static Relationship()
{
Add("set_lover", 72f, "Relationship", "{a} became lovers with {b}");
Add("clear_lover", 55f, "Relationship", "{a} parted from a lover");
Add("add_child", 68f, "Relationship", "{a} gains a child, {b}");
Add("new_family", 70f, "Relationship", "{a} starts a new family");
Add("family_removed", 50f, "Relationship", "{a}'s family ends");
Add("find_lover", 45f, "Relationship", "{a} is seeking a lover");
Add("family_group_new", 60f, "Relationship", "{a} forms a family pack");
Add("family_group_join", 36f, "Relationship", "{a} joins a family pack", createsInterest: false);
Add("family_group_leave", 36f, "Relationship", "{a} leaves a family pack", createsInterest: false);
Add("baby_created", 70f, "Relationship", "{a} is created as a baby");
}
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
private static void Add(
string id,
float strength,
string category,
string label,
bool createsInterest = true)
{
Entries[id] = new DiscreteEventEntry
private static int _appliedGeneration = -1;
public static void InvalidateOverlays()
{
Id = id,
EventStrength = strength,
Category = category,
CreatesInterest = createsInterest,
LabelTemplate = label
};
}
public static IEnumerable<string> AuthoredIds => Entries.Keys;
public static bool HasAuthored(string id) =>
!string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim());
public static DiscreteEventEntry GetOrFallback(string id)
{
if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry))
{
return entry;
Entries.Clear();
_appliedGeneration = -1;
}
string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim();
return new DiscreteEventEntry
private static void Ensure()
{
Id = key,
EventStrength = 40f,
Category = "Relationship",
LabelTemplate = "{a} · {id}",
CreatesInterest = false,
IsFallback = true
};
if (_appliedGeneration == EventCatalogConfig.Generation)
{
return;
}
_appliedGeneration = EventCatalogConfig.Generation;
EventCatalogConfig.FillRelationship(Entries);
}
public static IEnumerable<string> AuthoredIds
{
get
{
Ensure();
return Entries.Keys;
}
}
public static bool HasAuthored(string id)
{
Ensure();
return !string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim());
}
public static DiscreteEventEntry GetOrFallback(string id)
{
Ensure();
if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry))
{
return entry;
}
string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim();
return new DiscreteEventEntry
{
Id = key,
EventStrength = 40f,
Category = "Relationship",
LabelTemplate = "{a} · {id}",
CreatesInterest = false,
IsFallback = true
};
}
}
}
}

View file

@ -13,139 +13,76 @@ public sealed class StatusInterestEntry
public bool IsFallback;
}
/// <summary>
/// Authored interest policy for every live status asset. Prose stays in ActivityStatusProse.
/// </summary>
/// <summary>Status interest dials from <c>event-catalog.json</c> <c>status</c>.</summary>
public static partial class EventCatalog
{
public static class Status
{
private static readonly Dictionary<string, StatusInterestEntry> Entries =
new Dictionary<string, StatusInterestEntry>(StringComparer.OrdinalIgnoreCase);
static Status()
{
// Spectacle / camera-worthy transformations
Add("burning", 70f, "StatusTransformation", true);
Add("possessed", 72f, "StatusTransformation", true);
Add("possessed_follower", 60f, "StatusTransformation", true);
Add("frozen", 65f, "StatusTransformation", true);
Add("poisoned", 58f, "StatusTransformation", true);
Add("drowning", 50f, "StatusTransformation", true);
Add("stunned", 50f, "StatusTransformation", true);
Add("rage", 62f, "StatusTransformation", true);
Add("angry", 48f, "StatusTransformation", true);
Add("cursed", 60f, "StatusTransformation", true);
Add("soul_harvested", 75f, "StatusTransformation", true);
Add("voices_in_my_head", 55f, "StatusTransformation", true);
Add("tantrum", 52f, "StatusTransformation", true);
Add("egg", 40f, "StatusTransformation", false);
Add("magnetized", 55f, "StatusTransformation", true);
Add("invincible", 40f, "StatusTransformation", false);
Add("powerup", 54f, "StatusTransformation", true);
Add("enchanted", 52f, "StatusTransformation", true);
Add("ash_fever", 50f, "StatusTransformation", true);
Add("starving", 48f, "StatusTransformation", true);
Add("surprised", 42f, "StatusTransformation", true);
Add("confused", 40f, "StatusTransformation", true);
Add("strange_urge", 45f, "StatusTransformation", true);
Add("inspired", 42f, "Emotion", true);
Add("motivated", 40f, "Emotion", true);
Add("fell_in_love", 42f, "Relationship", false);
Add("pregnant", 58f, "LifeChapter", true);
Add("pregnant_parthenogenesis", 58f, "LifeChapter", true);
Add("crying", 50f, "Grief", true, extendsGrief: true);
private static readonly Dictionary<string, StatusInterestEntry> Entries =
new Dictionary<string, StatusInterestEntry>(StringComparer.OrdinalIgnoreCase);
// Authored Ambient - never owns camera (task chip / enrichment only).
Add("festive_spirit", 28f, "StatusAmbient", false);
Add("laughing", 26f, "StatusAmbient", false);
Add("singing", 26f, "StatusAmbient", false);
Add("sleeping", 22f, "StatusAmbient", false);
Add("handsome_migrant", 30f, "StatusAmbient", false);
private static int _appliedGeneration = -1;
// Authored but do not create interest candidates (cooldowns / ambient / brief FX)
AddNoInterest("afterglow");
AddNoInterest("being_suspicious");
AddNoInterest("budding");
AddNoInterest("caffeinated");
AddNoInterest("cough");
AddNoInterest("dash");
AddNoInterest("dodge");
AddNoInterest("flicked");
AddNoInterest("had_bad_dream");
AddNoInterest("had_good_dream");
AddNoInterest("had_nightmare");
AddNoInterest("just_ate");
AddNoInterest("on_guard");
AddNoInterest("recovery_combat_action");
AddNoInterest("recovery_plot");
AddNoInterest("recovery_social");
AddNoInterest("recovery_spell");
AddNoInterest("shield");
AddNoInterest("slowness");
AddNoInterest("spell_boost");
AddNoInterest("spell_silence");
AddNoInterest("swearing");
AddNoInterest("taking_roots");
AddNoInterest("uprooting");
}
private static void Add(
string id,
float strength,
string category,
bool createsInterest,
bool extendsGrief = false)
{
Entries[id] = new StatusInterestEntry
public static void InvalidateOverlays()
{
Id = id,
EventStrength = strength,
Category = category,
CreatesInterest = createsInterest,
ExtendsGrief = extendsGrief
};
}
private static void AddNoInterest(string id)
{
Add(id, 20f, "StatusAmbient", false);
}
public static IEnumerable<string> AuthoredIds => Entries.Keys;
public static bool HasAuthored(string statusId)
{
return !string.IsNullOrEmpty(statusId) && Entries.ContainsKey(statusId.Trim());
}
public static bool TryGet(string statusId, out StatusInterestEntry entry)
{
entry = null;
if (string.IsNullOrEmpty(statusId))
{
return false;
Entries.Clear();
_appliedGeneration = -1;
}
return Entries.TryGetValue(statusId.Trim(), out entry);
}
public static StatusInterestEntry GetOrFallback(string statusId)
{
if (TryGet(statusId, out StatusInterestEntry entry))
private static void Ensure()
{
return entry;
if (_appliedGeneration == EventCatalogConfig.Generation)
{
return;
}
_appliedGeneration = EventCatalogConfig.Generation;
EventCatalogConfig.FillStatus(Entries);
}
string id = string.IsNullOrEmpty(statusId) ? "unknown" : statusId.Trim();
return new StatusInterestEntry
public static IEnumerable<string> AuthoredIds
{
Id = id,
EventStrength = 40f,
Category = "Status",
CreatesInterest = false,
IsFallback = true
};
get
{
Ensure();
return Entries.Keys;
}
}
public static bool HasAuthored(string statusId)
{
Ensure();
return !string.IsNullOrEmpty(statusId) && Entries.ContainsKey(statusId.Trim());
}
public static bool TryGet(string statusId, out StatusInterestEntry entry)
{
Ensure();
entry = null;
if (string.IsNullOrEmpty(statusId))
{
return false;
}
return Entries.TryGetValue(statusId.Trim(), out entry);
}
public static StatusInterestEntry GetOrFallback(string statusId)
{
if (TryGet(statusId, out StatusInterestEntry entry))
{
return entry;
}
string id = string.IsNullOrEmpty(statusId) ? "unknown" : statusId.Trim();
return new StatusInterestEntry
{
Id = id,
EventStrength = 40f,
Category = "Status",
CreatesInterest = false,
IsFallback = true
};
}
}
}
}

View file

@ -3,167 +3,66 @@ using System.Collections.Generic;
namespace IdleSpectator;
/// <summary>Authored interest overlay for every live traits library asset.</summary>
/// <summary>Trait dials from <c>event-catalog.json</c> <c>traits</c>.</summary>
public static partial class EventCatalog
{
public static class Trait
{
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
static Trait()
{
Add("zombie", 82f);
Add("wise", 42f);
Add("whirlwind", 82f);
Add("weightless", 42f);
Add("weak", 42f);
Add("veteran", 42f);
Add("venomous", 82f);
Add("unlucky", 42f);
Add("ugly", 42f);
Add("tumor_infection", 82f);
Add("tough", 42f);
Add("titan_lungs", 42f);
Add("tiny", 42f);
Add("thorns", 42f);
Add("thief", 42f);
Add("super_health", 42f);
Add("sunblessed", 42f);
Add("stupid", 42f);
Add("strong_minded", 42f);
Add("strong", 42f);
Add("soft_skin", 42f);
Add("slow", 42f);
Add("skin_burns", 42f);
Add("short_sighted", 42f);
Add("shiny", 42f);
Add("scar_of_divinity", 82f);
Add("savage", 82f);
Add("regeneration", 42f);
Add("pyromaniac", 82f);
Add("psychopath", 82f);
Add("poisonous", 82f);
Add("poison_immune", 42f);
Add("plague", 82f);
Add("peaceful", 42f);
Add("paranoid", 42f);
Add("pacifist", 42f);
Add("nightchild", 42f);
Add("mute", 42f);
Add("mush_spores", 82f);
Add("moonchild", 42f);
Add("miracle_born", 82f);
Add("miracle_bearer", 82f);
Add("miner", 42f);
Add("metamorphed", 42f);
Add("mega_heartbeat", 42f);
Add("mageslayer", 82f);
Add("madness", 82f);
Add("lustful", 42f);
Add("lucky", 42f);
Add("long_liver", 42f);
Add("light_lamp", 42f);
Add("kingslayer", 82f);
Add("infertile", 42f);
Add("infected", 82f);
Add("immune", 42f);
Add("immortal", 82f);
Add("hotheaded", 42f);
Add("honest", 42f);
Add("heliophobia", 42f);
Add("heart_of_wizard", 42f);
Add("healing_aura", 42f);
Add("hard_skin", 42f);
Add("greedy", 42f);
Add("golden_tooth", 42f);
Add("gluttonous", 42f);
Add("giant", 82f);
Add("genius", 42f);
Add("freeze_proof", 42f);
Add("fragile_health", 42f);
Add("flower_prints", 42f);
Add("flesh_eater", 82f);
Add("fire_proof", 42f);
Add("fire_blood", 82f);
Add("fertile", 42f);
Add("fat", 42f);
Add("fast", 42f);
Add("eyepatch", 42f);
Add("evil", 82f);
Add("energized", 42f);
Add("eagle_eyed", 42f);
Add("dragonslayer", 82f);
Add("dodge", 42f);
Add("desire_harp", 42f);
Add("desire_golden_egg", 42f);
Add("desire_computer", 42f);
Add("desire_alien_mold", 42f);
Add("deflect_projectile", 42f);
Add("deceitful", 42f);
Add("death_nuke", 82f);
Add("death_mark", 82f);
Add("death_bomb", 82f);
Add("dash", 42f);
Add("crippled", 42f);
Add("content", 42f);
Add("contagious", 82f);
Add("cold_aura", 82f);
Add("clumsy", 42f);
Add("clone", 42f);
Add("chosen_one", 82f);
Add("burning_feet", 82f);
Add("bubble_defense", 42f);
Add("boosted_vitality", 42f);
Add("bomberman", 82f);
Add("boat", 42f);
Add("bloodlust", 82f);
Add("block", 42f);
Add("blessed", 42f);
Add("battle_reflexes", 42f);
Add("backstep", 42f);
Add("attractive", 42f);
Add("arcane_reflexes", 42f);
Add("ambitious", 42f);
Add("agile", 42f);
Add("acid_touch", 82f);
Add("acid_proof", 42f);
Add("acid_blood", 42f);
}
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
private static void Add(string id, float strength)
{
Entries[id] = new DiscreteEventEntry
private static int _appliedGeneration = -1;
public static void InvalidateOverlays()
{
Id = id,
EventStrength = strength,
Category = "Trait",
CreatesInterest = true,
LabelTemplate = "Trait: {id} ({a})"
};
}
public static IEnumerable<string> AuthoredIds => Entries.Keys;
public static bool HasAuthored(string id) =>
!string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim());
public static DiscreteEventEntry GetOrFallback(string id)
{
if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry))
{
return entry;
Entries.Clear();
_appliedGeneration = -1;
}
string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim();
return new DiscreteEventEntry
private static void Ensure()
{
Id = key,
EventStrength = 40f,
Category = "Trait",
LabelTemplate = "Trait: {id} ({a})",
IsFallback = true
};
if (_appliedGeneration == EventCatalogConfig.Generation)
{
return;
}
_appliedGeneration = EventCatalogConfig.Generation;
EventCatalogConfig.FillTraits(Entries);
}
public static IEnumerable<string> AuthoredIds
{
get
{
Ensure();
return Entries.Keys;
}
}
public static bool HasAuthored(string id)
{
Ensure();
return !string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim());
}
public static DiscreteEventEntry GetOrFallback(string id)
{
Ensure();
if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry))
{
return entry;
}
string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim();
return new DiscreteEventEntry
{
Id = key,
EventStrength = 40f,
Category = "Trait",
LabelTemplate = "Trait: {id} ({a})",
CreatesInterest = true,
IsFallback = true
};
}
}
}
}

View file

@ -13,83 +13,90 @@ public sealed class WarTypeInterestEntry
public bool IsFallback;
}
/// <summary>Authored interest overlay for every live war_types_library asset.</summary>
/// <summary>War type dials from <c>event-catalog.json</c> <c>warTypes</c>.</summary>
public static partial class EventCatalog
{
public static class WarType
{
private static readonly Dictionary<string, WarTypeInterestEntry> Entries =
new Dictionary<string, WarTypeInterestEntry>(StringComparer.OrdinalIgnoreCase);
static WarType()
{
Add("normal", 90f, "War: {a}");
Add("spite", 88f, "Spite war: {a}");
Add("inspire", 86f, "Inspired war: {a}");
Add("rebellion", 92f, "Rebellion: {a}");
Add("whisper_of_war", 84f, "Whisper of war: {a}");
}
private static readonly Dictionary<string, WarTypeInterestEntry> Entries =
new Dictionary<string, WarTypeInterestEntry>(StringComparer.OrdinalIgnoreCase);
private static void Add(string id, float strength, string labelTemplate)
{
Entries[id] = new WarTypeInterestEntry
private static int _appliedGeneration = -1;
public static void InvalidateOverlays()
{
Id = id,
EventStrength = strength,
Category = "Politics",
LabelTemplate = labelTemplate,
CreatesInterest = true
};
}
public static IEnumerable<string> AuthoredIds => Entries.Keys;
public static bool HasAuthored(string warTypeId)
{
return !string.IsNullOrEmpty(warTypeId) && Entries.ContainsKey(warTypeId.Trim());
}
public static bool TryGet(string warTypeId, out WarTypeInterestEntry entry)
{
entry = null;
if (string.IsNullOrEmpty(warTypeId))
{
return false;
Entries.Clear();
_appliedGeneration = -1;
}
return Entries.TryGetValue(warTypeId.Trim(), out entry);
}
public static WarTypeInterestEntry GetOrFallback(string warTypeId)
{
if (TryGet(warTypeId, out WarTypeInterestEntry entry))
private static void Ensure()
{
return entry;
if (_appliedGeneration == EventCatalogConfig.Generation)
{
return;
}
_appliedGeneration = EventCatalogConfig.Generation;
EventCatalogConfig.FillWarTypes(Entries);
}
string id = string.IsNullOrEmpty(warTypeId) ? "unknown" : warTypeId.Trim();
return new WarTypeInterestEntry
public static IEnumerable<string> AuthoredIds
{
Id = id,
EventStrength = 85f,
Category = "Politics",
LabelTemplate = "War ({id}): {a}",
CreatesInterest = true,
IsFallback = true
};
}
public static string MakeLabel(WarTypeInterestEntry entry, string special1)
{
if (entry == null)
{
return "War";
get
{
Ensure();
return Entries.Keys;
}
}
string template = string.IsNullOrEmpty(entry.LabelTemplate) ? "War ({id})" : entry.LabelTemplate;
return template
.Replace("{id}", entry.Id ?? "")
.Replace("{a}", special1 ?? "");
public static bool HasAuthored(string warTypeId)
{
Ensure();
return !string.IsNullOrEmpty(warTypeId) && Entries.ContainsKey(warTypeId.Trim());
}
public static bool TryGet(string warTypeId, out WarTypeInterestEntry entry)
{
Ensure();
entry = null;
if (string.IsNullOrEmpty(warTypeId))
{
return false;
}
return Entries.TryGetValue(warTypeId.Trim(), out entry);
}
public static WarTypeInterestEntry GetOrFallback(string warTypeId)
{
if (TryGet(warTypeId, out WarTypeInterestEntry entry))
{
return entry;
}
string id = string.IsNullOrEmpty(warTypeId) ? "unknown" : warTypeId.Trim();
return new WarTypeInterestEntry
{
Id = id,
EventStrength = 85f,
Category = "Politics",
LabelTemplate = "War ({id}): {a}",
CreatesInterest = true,
IsFallback = true
};
}
public static string MakeLabel(WarTypeInterestEntry entry, string special1)
{
if (entry == null)
{
return "War";
}
string template = string.IsNullOrEmpty(entry.LabelTemplate) ? "War ({id})" : entry.LabelTemplate;
return template
.Replace("{id}", entry.Id ?? "")
.Replace("{a}", special1 ?? "");
}
}
}
}

View file

@ -39,155 +39,105 @@ public sealed class WorldLogEventEntry
}
}
/// <summary>
/// Authored policy for every live WorldLog asset. Game library is inventory; this is presentation/scoring only.
/// </summary>
/// <summary>WorldLog dials from <c>event-catalog.json</c> <c>worldLog</c>.</summary>
public static partial class EventCatalog
{
public static class WorldLog
{
private static readonly Dictionary<string, WorldLogEventEntry> Entries =
new Dictionary<string, WorldLogEventEntry>(StringComparer.OrdinalIgnoreCase);
static WorldLog()
{
// Kings
Add("king_new", 78f, "Politics", true, "New king: {b} ({a})", 6f, 28f);
Add("king_left", 65f, "Politics", true, "King left: {b} ({a})", 6f, 28f);
Add("king_fled_capital", 70f, "Politics", true, "King fled capital: {b}", 6f, 28f);
Add("king_fled_city", 68f, "Politics", true, "King fled city: {b}", 6f, 28f);
Add("king_dead", 80f, "Politics", true, "King died: {b} ({a})", 6f, 28f);
Add("king_killed", 85f, "Politics", true, "King killed: {b} ({a})", 6f, 28f);
private static readonly Dictionary<string, WorldLogEventEntry> Entries =
new Dictionary<string, WorldLogEventEntry>(StringComparer.OrdinalIgnoreCase);
// Favorites
Add("favorite_dead", 82f, "Politics", true, "Favorite fell: {a}", 6f, 28f);
Add("favorite_killed", 85f, "Politics", true, "Favorite fell: {a}", 6f, 28f);
private static int _appliedGeneration = -1;
// Cities / kingdoms / clans / wars / alliances
Add("city_new", 72f, "Settlement", true, "New city: {a}", 6f, 28f);
Add("log_city_revolted", 90f, "Politics", true, "Revolt: {a}", 8f, 35f);
Add("city_destroyed", 92f, "Settlement", true, "City destroyed: {a}", 8f, 35f);
Add("diplomacy_war_ended", 72f, "Politics", true, "War ended: {a}", 6f, 28f);
Add("diplomacy_war_started", 100f, "Politics", true, "War: {a} vs {b}", 8f, 35f);
Add("total_war_started", 100f, "Politics", true, "Total war: {a}", 8f, 35f);
Add("alliance_new", 70f, "Politics", true, "Alliance: {a}", 6f, 28f);
Add("alliance_dissolved", 68f, "Politics", true, "Alliance dissolved: {a}", 6f, 28f);
Add("kingdom_new", 75f, "Settlement", true, "New kingdom: {a}", 6f, 28f);
Add("kingdom_destroyed", 100f, "Politics", true, "Kingdom fell: {a}", 8f, 35f);
Add("kingdom_shattered", 98f, "Politics", true, "Kingdom shattered: {a}", 8f, 35f);
Add("kingdom_fractured", 95f, "Politics", true, "Kingdom fractured: {a}", 8f, 35f);
Add("kingdom_royal_clan_new", 74f, "Politics", true, "Royal clan: {a}", 6f, 28f);
Add("kingdom_royal_clan_changed", 72f, "Politics", true, "Royal clan: {a}", 6f, 28f);
Add("kingdom_royal_clan_dead", 76f, "Politics", true, "Royal clan ended: {a}", 6f, 28f);
// Disasters (WorldLog ids; linked to disasters library by naming)
Add("disaster_tornado", 95f, "Disaster", true, "Tornado: {a}", 8f, 35f);
Add("disaster_meteorite", 95f, "Disaster", true, "Meteorite: {a}", 8f, 35f);
Add("disaster_hellspawn", 96f, "Disaster", true, "Hellspawn: {a}", 8f, 35f);
Add("disaster_earthquake", 95f, "Disaster", true, "Earthquake: {a}", 8f, 35f);
Add("disaster_greg_abominations", 96f, "Disaster", true, "Greg abominations: {a}", 8f, 35f);
Add("disaster_ice_ones", 95f, "Disaster", true, "Ice ones awaken: {a}", 8f, 35f);
Add("disaster_sudden_snowman", 90f, "Disaster", true, "Sudden snowman: {a}", 8f, 35f);
Add("disaster_garden_surprise", 90f, "Disaster", true, "Garden surprise: {a}", 8f, 35f);
Add("disaster_dragon_from_farlands", 98f, "Disaster", true, "Dragon from the farlands: {a}", 8f, 35f);
Add("disaster_bandits", 92f, "Disaster", true, "Bandit raid: {a}", 8f, 35f);
Add("disaster_alien_invasion", 97f, "Disaster", true, "Alien invasion: {a}", 8f, 35f);
Add("disaster_biomass", 96f, "Disaster", true, "Biomass outbreak: {a}", 8f, 35f);
Add("disaster_tumor", 96f, "Disaster", true, "Tumor outbreak: {a}", 8f, 35f);
Add("disaster_heatwave", 88f, "Disaster", true, "Heatwave: {a}", 8f, 35f);
Add("disaster_evil_mage", 94f, "Disaster", true, "Evil mage: {a}", 8f, 35f);
Add("disaster_underground_necromancer", 95f, "Disaster", true, "Underground necromancer: {a}", 8f, 35f);
Add("disaster_mad_thoughts", 90f, "Disaster", true, "Mad thoughts: {a}", 8f, 35f);
// Harness / internal - authored but never owns camera interest
Add("auto_tester", 10f, "Harness", false, "Auto tester: {a}", 2f, 6f);
}
private static void Add(
string id,
float strength,
string category,
bool createsInterest,
string labelTemplate,
float minWatch,
float maxWatch)
{
Entries[id] = new WorldLogEventEntry
public static void InvalidateOverlays()
{
Id = id,
EventStrength = strength,
Category = category,
CreatesInterest = createsInterest,
ChronicleEligible = createsInterest && strength >= 65f,
LabelTemplate = labelTemplate,
MinWatch = minWatch,
MaxWatch = maxWatch
};
}
public static IEnumerable<string> AuthoredIds => Entries.Keys;
public static bool HasAuthored(string assetId)
{
return !string.IsNullOrEmpty(assetId) && Entries.ContainsKey(assetId.Trim());
}
public static bool TryGet(string assetId, out WorldLogEventEntry entry)
{
entry = null;
if (string.IsNullOrEmpty(assetId))
{
return false;
Entries.Clear();
_appliedGeneration = -1;
}
return Entries.TryGetValue(assetId.Trim(), out entry);
}
public static WorldLogEventEntry GetOrFallback(string assetId)
{
if (TryGet(assetId, out WorldLogEventEntry entry))
private static void Ensure()
{
return entry;
if (_appliedGeneration == EventCatalogConfig.Generation)
{
return;
}
_appliedGeneration = EventCatalogConfig.Generation;
EventCatalogConfig.FillWorldLog(Entries);
}
string id = string.IsNullOrEmpty(assetId) ? "unknown" : assetId.Trim();
float strength = 40f;
string category = "WorldLog";
if (id.IndexOf("disaster", StringComparison.OrdinalIgnoreCase) >= 0)
public static IEnumerable<string> AuthoredIds
{
strength = 95f;
category = "Disaster";
get
{
Ensure();
return Entries.Keys;
}
}
return new WorldLogEventEntry
public static bool HasAuthored(string assetId)
{
Id = id,
EventStrength = strength,
Category = category,
CreatesInterest = true,
ChronicleEligible = strength >= 65f,
LabelTemplate = "{id}: {a}",
MinWatch = strength >= 90f ? 8f : 6f,
MaxWatch = strength >= 90f ? 35f : 28f,
IsFallback = true
};
}
public static bool TryGetEventStrength(string assetId, out float eventStrength)
{
WorldLogEventEntry entry = GetOrFallback(assetId);
eventStrength = entry.EventStrength;
return !string.IsNullOrEmpty(assetId);
}
public static string MakeLabel(WorldLogMessage message)
{
if (message == null)
{
return "event";
Ensure();
return !string.IsNullOrEmpty(assetId) && Entries.ContainsKey(assetId.Trim());
}
return GetOrFallback(message.asset_id).MakeLabel(message);
public static bool TryGet(string assetId, out WorldLogEventEntry entry)
{
Ensure();
entry = null;
if (string.IsNullOrEmpty(assetId))
{
return false;
}
return Entries.TryGetValue(assetId.Trim(), out entry);
}
public static WorldLogEventEntry GetOrFallback(string assetId)
{
if (TryGet(assetId, out WorldLogEventEntry entry))
{
return entry;
}
string id = string.IsNullOrEmpty(assetId) ? "unknown" : assetId.Trim();
float strength = 40f;
string category = "WorldLog";
if (id.IndexOf("disaster", StringComparison.OrdinalIgnoreCase) >= 0)
{
strength = 95f;
category = "Disaster";
}
return new WorldLogEventEntry
{
Id = id,
EventStrength = strength,
Category = category,
CreatesInterest = true,
ChronicleEligible = strength >= 65f,
LabelTemplate = "{id}: {a}",
MinWatch = strength >= 90f ? 8f : 6f,
MaxWatch = strength >= 90f ? 35f : 28f,
IsFallback = true
};
}
public static bool TryGetEventStrength(string assetId, out float eventStrength)
{
WorldLogEventEntry entry = GetOrFallback(assetId);
eventStrength = entry.EventStrength;
return !string.IsNullOrEmpty(assetId);
}
public static string MakeLabel(WorldLogMessage message)
{
if (message == null)
{
return "event";
}
return GetOrFallback(message.asset_id).MakeLabel(message);
}
}
}
}

View file

@ -11,6 +11,11 @@ public sealed class DiscreteEventEntry
public string Category = "Event";
public bool CreatesInterest = true;
public string LabelTemplate = "{a}";
/// <summary>
/// Optional infinitive clause for decision-style reasons
/// (e.g. <c>change the kingdom's culture</c> → <c>{a} decides to …</c>).
/// </summary>
public string ActionPhrase = "";
public bool IsFallback;
public string MakeLabel(Actor a, Actor b = null)

View file

@ -2,8 +2,12 @@ namespace IdleSpectator;
/// <summary>
/// IdleSpectator event inventory (one type, split across partial files).
///
/// Layer B (ticker): Activity / Life prose for live ids - completeness.
/// Layer A (story camera): only CameraWorthy beats register interest and cut the camera.
///
/// Call sites: EventCatalog.Status / .Happiness / .Combat / …
/// Director only ranks; feeds register from these dials.
/// Director only ranks A candidates; it does not author inventory.
///
/// Domain files under Events/Catalogs/:
/// EventCatalog.Status.cs
@ -16,11 +20,56 @@ namespace IdleSpectator;
/// EventCatalog.Relationship.cs
/// EventCatalog.Disaster.cs
/// EventCatalog.WarType.cs
/// EventCatalog.Libraries.cs (Spell, Power, Decision, Item, SubspeciesTrait, Gene, Phenotype, WorldLaw, Biome)
/// EventCatalog.Libraries.cs
/// Combat policy lives in this file.
/// </summary>
public static partial class EventCatalog
{
/// <summary>Drop cached catalog rows so the next Ensure reloads from event-catalog.json.</summary>
public static void InvalidateAllOverlays()
{
Decision.InvalidateOverlays();
Happiness.InvalidateOverlays();
Status.InvalidateOverlays();
WorldLog.InvalidateOverlays();
Plot.InvalidateOverlays();
Relationship.InvalidateOverlays();
Trait.InvalidateOverlays();
Book.InvalidateOverlays();
Era.InvalidateOverlays();
Disaster.InvalidateOverlays();
WarType.InvalidateOverlays();
Spell.InvalidateOverlays();
Power.InvalidateOverlays();
Item.InvalidateOverlays();
Gene.InvalidateOverlays();
Phenotype.InvalidateOverlays();
WorldLaw.InvalidateOverlays();
SubspeciesTrait.InvalidateOverlays();
Biome.InvalidateOverlays();
}
/// <summary>
/// Layer A gate for happiness: Signal may own the camera; Ambient/Aggregate are B-only.
/// </summary>
public static bool IsCameraWorthy(HappinessPresentationTier presentation) =>
presentation == HappinessPresentationTier.Signal;
/// <summary>Layer A gate for discrete / status / worldlog rows.</summary>
public static bool IsCameraWorthy(bool createsInterest) => createsInterest;
public static bool IsCameraWorthy(DiscreteEventEntry entry) =>
entry != null && entry.CreatesInterest && !entry.IsFallback;
public static bool IsCameraWorthy(StatusInterestEntry entry) =>
entry != null && entry.CreatesInterest && !entry.IsFallback;
public static bool IsCameraWorthy(WorldLogEventEntry entry) =>
entry != null && entry.CreatesInterest && !entry.IsFallback;
public static bool IsCameraWorthy(HappinessCatalogEntry entry) =>
entry != null && IsCameraWorthy(entry.Presentation);
/// <summary>Combat camera policy (activity + scanner share one key / dials).</summary>
public static class Combat
{

View file

@ -50,6 +50,11 @@ public static class EventReason
return NameOrSomeone(a) + " is seeking a lover";
}
public static string BecomeAlpha(Actor a)
{
return NameOrSomeone(a) + " becomes the family alpha";
}
public static string Hatch(Actor a)
{
return NameOrSomeone(a) + " hatches from an egg";
@ -210,6 +215,173 @@ public static class EventReason
return gained ? an + " gains " + id : an + " loses " + id;
}
/// <summary>
/// Plot labels are phase-aware: start templates stay progressive; finish verbs fire on complete.
/// </summary>
public static string Plot(Actor a, string template, string phase, string plotId = "")
{
string filled = Apply(
string.IsNullOrEmpty(template) ? "{a} is plotting" : template,
a,
id: plotId);
string p = (phase ?? "active").Trim().ToLowerInvariant();
switch (p)
{
case "complete":
case "finished":
case "finish":
return RewritePlotProgressive(filled, "finishes ");
case "cancel":
case "cancelled":
case "canceled":
return RewritePlotAbandon(filled);
case "leave":
return NameOrSomeone(a) + " leaves a plot";
case "join":
return filled;
case "new":
case "active":
default:
return RewritePlotStart(filled);
}
}
private static string RewritePlotStart(string label)
{
if (string.IsNullOrEmpty(label))
{
return label;
}
// Finish-style verbs on start → "plots to …"
string[] finishVerbs =
{
"summons ", "casts ", "splits ", "causes ", "performs ",
};
foreach (string verb in finishVerbs)
{
int idx = IndexOfWordPhrase(label, verb);
if (idx < 0)
{
continue;
}
string infinitive = InfinitiveFromFinishVerb(verb.TrimEnd());
return label.Substring(0, idx) + "plots to " + infinitive + label.Substring(idx + verb.Length);
}
return label;
}
private static string RewritePlotProgressive(string label, string replacementPrefix)
{
if (string.IsNullOrEmpty(label))
{
return label;
}
string[] progressives =
{
"is plotting ", "is joining ", "is breaking ", "is ending ",
"is writing ", "is forging ", "is founding ", "is ascending ",
};
foreach (string progressive in progressives)
{
int idx = IndexOfWordPhrase(label, progressive);
if (idx >= 0)
{
// "is plotting " → "plotting " after dropping "is "
return label.Substring(0, idx)
+ replacementPrefix
+ progressive.Substring(3)
+ label.Substring(idx + progressive.Length);
}
}
// Already a finish verb ("summons", "casts", …) - correct for complete.
return label;
}
private static string RewritePlotAbandon(string label)
{
if (string.IsNullOrEmpty(label))
{
return label;
}
string progressive = RewritePlotProgressive(label, "abandons ");
if (progressive != label)
{
return progressive;
}
string[] finishVerbs =
{
"summons ", "casts ", "splits ", "causes ", "performs ",
};
foreach (string verb in finishVerbs)
{
int idx = IndexOfWordPhrase(label, verb);
if (idx < 0)
{
continue;
}
string gerund = GerundFromFinishVerb(verb.TrimEnd());
return label.Substring(0, idx) + "abandons " + gerund + label.Substring(idx + verb.Length);
}
return label;
}
private static string InfinitiveFromFinishVerb(string verb)
{
switch (verb)
{
case "summons":
return "summon ";
case "casts":
return "cast ";
case "splits":
return "split ";
case "causes":
return "cause ";
case "performs":
return "perform ";
default:
return verb + " ";
}
}
private static string GerundFromFinishVerb(string verb)
{
switch (verb)
{
case "summons":
return "summoning ";
case "casts":
return "casting ";
case "splits":
return "splitting ";
case "causes":
return "causing ";
case "performs":
return "performing ";
default:
return verb + " ";
}
}
private static int IndexOfWordPhrase(string haystack, string phrase)
{
if (string.IsNullOrEmpty(haystack) || string.IsNullOrEmpty(phrase))
{
return -1;
}
return haystack.IndexOf(phrase, StringComparison.OrdinalIgnoreCase);
}
public static string Boat(Actor a, string phase)
{
string an = NameOrSomeone(a);
@ -258,7 +430,7 @@ public static class EventReason
case "item":
return an + " forges " + id;
case "decision":
return an + " decides " + id;
return Decision(a, assetId);
case "gene":
return an + " gains gene " + id;
case "phenotype":
@ -272,6 +444,18 @@ public static class EventReason
}
}
public static string Decision(Actor a, string decisionId)
{
string an = NameOrSomeone(a);
string action = EventCatalog.Decision.ActionPhraseFor(decisionId);
if (string.IsNullOrEmpty(action))
{
return an + " makes a decision";
}
return an + " decides to " + action;
}
/// <summary>
/// Apply a sentence template with {a}/{b}/{id}. Prefer typed helpers for new copy.
/// </summary>

View file

@ -13,8 +13,9 @@ public static class BookInterestFeed
}
DiscreteEventEntry entry = EventCatalog.Book.GetOrFallback(bookTypeId);
if (!entry.CreatesInterest)
if (!EventCatalog.IsCameraWorthy(entry))
{
InterestDropLog.Record("createsInterest=false", "book");
return;
}

View file

@ -41,8 +41,15 @@ public static class DeferredInterestFeed
public static void EmitPower(string powerId, Vector3 position, Actor nearUnit)
{
DiscreteEventEntry entry = EventCatalog.Power.GetOrFallback(powerId);
if (!entry.CreatesInterest || AgentHarness.Busy)
if (AgentHarness.Busy)
{
InterestDropLog.Record("busy", "power");
return;
}
if (!EventCatalog.IsCameraWorthy(entry))
{
InterestDropLog.Record("createsInterest=false", "power:" + (powerId ?? ""));
return;
}
@ -110,8 +117,9 @@ public static class DeferredInterestFeed
}
DiscreteEventEntry entry = EventCatalog.WorldLaw.GetOrFallback(lawId);
if (!entry.CreatesInterest)
if (!EventCatalog.IsCameraWorthy(entry))
{
InterestDropLog.Record("createsInterest=false", entry.Id ?? "");
return;
}
@ -135,8 +143,9 @@ public static class DeferredInterestFeed
}
DiscreteEventEntry entry = EventCatalog.Biome.GetOrFallback(biomeId);
if (!entry.CreatesInterest)
if (!EventCatalog.IsCameraWorthy(entry))
{
InterestDropLog.Record("createsInterest=false", entry.Id ?? "");
return;
}
@ -166,8 +175,9 @@ public static class DeferredInterestFeed
}
DiscreteEventEntry entry = lookup(assetId);
if (!entry.CreatesInterest)
if (!EventCatalog.IsCameraWorthy(entry))
{
InterestDropLog.Record("createsInterest=false", entry.Id ?? "");
return;
}

View file

@ -24,6 +24,7 @@ public static class InterestFeeds
HappinessDrain.Clear();
_lastScannerAt = -999f;
CivicBoostUntil.Clear();
InterestDropLog.Clear();
}
public static ulong HappinessCursor => _happinessCursor;
@ -94,8 +95,11 @@ public static class InterestFeeds
}
WorldLogEventEntry entry = EventCatalog.WorldLog.GetOrFallback(message.asset_id);
if (!entry.CreatesInterest)
if (!EventCatalog.IsCameraWorthy(entry))
{
InterestDropLog.Record(
"createsInterest=false",
"worldlog:" + (message.asset_id ?? ""));
return;
}
@ -370,7 +374,7 @@ public static class InterestFeeds
}
// Egg timer end is the reliable hatch beat. Happiness just_got_out_of_egg is often
// suppressed (no emotions / still flagged egg), so status loss owns the camera reason.
// suppressed (no emotions), so status loss owns the camera reason.
if (!gained && string.Equals(statusId.Trim(), "egg", StringComparison.OrdinalIgnoreCase))
{
EmitHatch(actor, "status_egg_loss");
@ -379,12 +383,14 @@ public static class InterestFeeds
if (!gained)
{
InterestDropLog.Record("status_loss_ignored", statusId);
return;
}
StatusInterestEntry entry = EventCatalog.Status.GetOrFallback(statusId);
if (!entry.CreatesInterest)
if (!EventCatalog.IsCameraWorthy(entry))
{
InterestDropLog.Record("createsInterest=false", "status:" + statusId);
return;
}
@ -473,6 +479,67 @@ public static class InterestFeeds
private static string HatchKey(long subjectId) => "hatch:" + subjectId;
private static string AlphaKey(long subjectId) => "alpha:" + subjectId;
/// <summary>
/// Family alpha succession. Shared key with happiness <c>become_alpha</c> so emotion and
/// emotion-less paths merge into one camera beat.
/// </summary>
public static void EmitAlpha(Actor actor, string source)
{
if (actor == null || !actor.isAlive())
{
return;
}
if (AgentHarness.Busy)
{
return;
}
long id = SafeId(actor);
if (id == 0)
{
return;
}
DiscreteEventEntry entry = EventCatalog.Relationship.GetOrFallback("become_alpha");
if (!EventCatalog.IsCameraWorthy(entry))
{
InterestDropLog.Record("createsInterest=false", "become_alpha");
return;
}
float strength = Mathf.Max(
entry.EventStrength,
InterestScoring.EventStrengthForHappiness("become_alpha"));
var candidate = new InterestCandidate
{
Key = AlphaKey(id),
LeadKind = InterestLeadKind.EventLed,
Category = string.IsNullOrEmpty(entry.Category) ? "LifeChapter" : entry.Category,
Source = string.IsNullOrEmpty(source) ? "alpha" : source,
EventStrength = strength,
VisualConfidence = 0.8f,
Position = actor.current_position,
FollowUnit = actor,
SubjectId = id,
Label = EventReason.BecomeAlpha(actor),
AssetId = SafeAsset(actor),
SpeciesId = SafeAsset(actor),
HappinessEffectId = "become_alpha",
CreatedAt = Time.unscaledTime,
LastSeenAt = Time.unscaledTime,
ExpiresAt = Time.unscaledTime + 25f,
MinWatch = 5f,
MaxWatch = 22f,
Completion = InterestCompletionKind.FixedDwell
};
candidate.ParticipantIds.Add(id);
InterestScoring.ScoreCheap(candidate);
EventFeedUtil.RegisterCandidate(candidate);
}
public static void OnChronicleMilestone(Actor subject, string milestoneKey, Actor related, string label)
{
if (subject == null || !subject.isAlive() || string.IsNullOrEmpty(milestoneKey))
@ -535,20 +602,22 @@ public static class InterestFeeds
private static void IngestHappiness(HappinessOccurrence occ)
{
if (occ == null || !occ.Applied || occ.SuppressedByPsychopath)
if (occ == null)
{
return;
}
// Harness scenarios inject via SourceHook.Harness; ignore live world noise while Busy.
if (!occ.Applied || occ.SuppressedByPsychopath)
{
InterestDropLog.Record(
occ.SuppressedByPsychopath ? "suppressed" : "not_applied",
occ.EffectId ?? "");
return;
}
if (AgentHarness.Busy && occ.SourceHook != HappinessSourceHook.Harness)
{
return;
}
if (occ.Presentation == HappinessPresentationTier.Ambient)
{
// Enrichment only - never owns the camera.
InterestDropLog.Record("busy", occ.EffectId ?? "");
return;
}
@ -578,6 +647,14 @@ public static class InterestFeeds
StampCivicBoost(kingdom, occ.EffectId, 25f + Mathf.Min(40f, occ.TotalAffectedCount));
}
InterestDropLog.Record("aggregate", occ.EffectId ?? "");
return;
}
if (!EventCatalog.IsCameraWorthy(occ.Presentation))
{
// Layer B only - never owns the camera.
InterestDropLog.Record("ambient", occ.EffectId ?? "");
return;
}
@ -598,12 +675,23 @@ public static class InterestFeeds
bool grief = occ.EffectId != null && occ.EffectId.StartsWith("death_");
float strength = InterestScoring.EventStrengthForHappiness(occ.EffectId);
bool hatchMoment = IsFreshLifeMoment(occ.EffectId);
bool alphaMoment = IsAlphaMoment(occ.EffectId);
// Hatch shares a key with status egg-loss so both paths merge into one camera beat.
string key = hatchMoment
? HatchKey(occ.SubjectId)
: ("happiness:" + occ.EffectId + ":" + occ.SubjectId + ":" + occ.RelatedId
+ ":" + CorrBucket(occ.CorrelationKey));
// Hatch / alpha share keys with status-loss / Family.setAlpha so paths merge.
string key;
if (hatchMoment)
{
key = HatchKey(occ.SubjectId);
}
else if (alphaMoment)
{
key = AlphaKey(occ.SubjectId);
}
else
{
key = "happiness:" + occ.EffectId + ":" + occ.SubjectId + ":" + occ.RelatedId
+ ":" + CorrBucket(occ.CorrelationKey);
}
// Hatch: short queue TTL, but on-camera floor comes from minCameraDwell.
float ttl = hatchMoment ? 7f : 35f;
@ -614,11 +702,23 @@ public static class InterestFeeds
strength = Mathf.Max(strength, 88f);
}
if (alphaMoment)
{
strength = Mathf.Max(strength, EventCatalog.Relationship.GetOrFallback("become_alpha").EventStrength);
ttl = 25f;
minWatch = 5f;
maxWatch = 22f;
}
string label;
if (hatchMoment)
{
label = EventReason.Hatch(subject);
}
else if (alphaMoment)
{
label = EventReason.BecomeAlpha(subject);
}
else if (!string.IsNullOrEmpty(occ.PlainClause))
{
label = occ.SubjectName + " " + occ.PlainClause;
@ -633,7 +733,9 @@ public static class InterestFeeds
Key = key,
LeadKind = InterestLeadKind.EventLed,
Category = grief ? "FamilyEmotion" : MapHappinessCategory(cat),
Source = hatchMoment ? "happiness_hatch" : "happiness",
Source = hatchMoment
? "happiness_hatch"
: (alphaMoment ? "happiness_alpha" : "happiness"),
EventStrength = strength,
VisualConfidence = 0.75f,
Position = occ.Position != Vector3.zero ? occ.Position : subject.current_position,
@ -871,6 +973,12 @@ public static class InterestFeeds
|| string.Equals(id, "just_hatched", StringComparison.OrdinalIgnoreCase);
}
private static bool IsAlphaMoment(string effectId)
{
return !string.IsNullOrEmpty(effectId)
&& string.Equals(effectId.Trim(), "become_alpha", StringComparison.OrdinalIgnoreCase);
}
private static string MapHappinessCategory(string cat)
{
switch ((cat ?? "").ToLowerInvariant())

View file

@ -13,8 +13,9 @@ public static class MetaInterestFeed
}
DiscreteEventEntry entry = EventCatalog.Era.GetOrFallback(eraId);
if (!entry.CreatesInterest)
if (!EventCatalog.IsCameraWorthy(entry))
{
InterestDropLog.Record("createsInterest=false", "meta");
return;
}

View file

@ -15,8 +15,9 @@ public static class PlotInterestFeed
}
DiscreteEventEntry entry = EventCatalog.Plot.GetOrFallback(plotAssetId);
if (!entry.CreatesInterest)
if (!EventCatalog.IsCameraWorthy(entry))
{
InterestDropLog.Record("createsInterest=false", "plot");
return;
}
@ -27,7 +28,7 @@ public static class PlotInterestFeed
return;
}
string label = EventReason.Apply(entry.LabelTemplate, follow, id: entry.Id);
string label = EventReason.Plot(follow, entry.LabelTemplate, phase, entry.Id);
string key = "plot:" + entry.Id + ":" + (phase ?? "active") + ":" + EventFeedUtil.SafeId(follow);
EventFeedUtil.Register(
key,

View file

@ -5,16 +5,23 @@ namespace IdleSpectator;
/// <summary>Registers relationship lifecycle interest candidates under the ownership contract.</summary>
public static class RelationshipInterestFeed
{
private const float FoundingMergeSeconds = 0.35f;
private static float _foundingAt = -999f;
private static long _foundingActorId;
public static void Emit(string eventId, Actor subject, Actor related, string labelOverride = null)
{
if (AgentHarness.Busy || subject == null || !subject.isAlive())
if ((AgentHarness.Busy && !AgentHarness.ForceRelationshipFeeds)
|| subject == null
|| !subject.isAlive())
{
return;
}
DiscreteEventEntry entry = EventCatalog.Relationship.GetOrFallback(eventId);
if (!entry.CreatesInterest)
if (!EventCatalog.IsCameraWorthy(entry))
{
InterestDropLog.Record("createsInterest=false", "relationship:" + (eventId ?? ""));
return;
}
@ -34,6 +41,29 @@ public static class RelationshipInterestFeed
? labelOverride
: TypedLabel(eventId, subject, relatedAlive);
// BehFamilyGroupNew always calls newFamily first - keep family_group_new on but
// share the founding interest key so one orange reason wins for that beat.
if (eventId == "family_group_new" && IsRecentFounding(subject))
{
string foundingKey = FoundingKey(subject);
if (InterestRegistry.TryGet(foundingKey, out _))
{
return;
}
EventFeedUtil.Register(
foundingKey,
"relationship",
entry.Category,
label,
entry.Id,
entry.EventStrength,
subject.current_position,
subject,
related: relatedAlive);
return;
}
if (claimsPeer && relatedAlive == null)
{
string soloKey = "rel:" + entry.Id + ":" + EventFeedUtil.SafeId(subject) + ":solo";
@ -64,7 +94,7 @@ public static class RelationshipInterestFeed
public static void EmitFamily(string eventId, object family, Actor anchor)
{
if (AgentHarness.Busy)
if (AgentHarness.Busy && !AgentHarness.ForceRelationshipFeeds)
{
return;
}
@ -84,6 +114,12 @@ public static class RelationshipInterestFeed
Actor related = ActorRelation.FirstFamilyPeer(follow);
string label = TypedLabel(eventId, follow, related);
string key = "rel:" + entry.Id + ":" + EventFeedUtil.SafeId(follow);
if (eventId == "new_family")
{
NoteFounding(follow);
key = FoundingKey(follow);
}
EventFeedUtil.Register(
key,
"relationship",
@ -96,6 +132,31 @@ public static class RelationshipInterestFeed
related: related);
}
private static void NoteFounding(Actor anchor)
{
_foundingAt = Time.unscaledTime;
_foundingActorId = EventFeedUtil.SafeId(anchor);
}
private static bool IsRecentFounding(Actor subject)
{
if (subject == null)
{
return false;
}
long id = EventFeedUtil.SafeId(subject);
if (id == 0 || id != _foundingActorId)
{
return false;
}
return Time.unscaledTime - _foundingAt <= FoundingMergeSeconds;
}
private static string FoundingKey(Actor anchor) =>
"rel:new_family:" + EventFeedUtil.SafeId(anchor);
private static string TypedLabel(string eventId, Actor subject, Actor related)
{
switch (eventId)
@ -106,6 +167,8 @@ public static class RelationshipInterestFeed
return EventReason.LoverParted(subject);
case "find_lover":
return EventReason.SeekingLover(subject);
case "become_alpha":
return EventReason.BecomeAlpha(subject);
case "add_child":
return EventReason.NewChild(subject, related);
case "baby_created":

View file

@ -60,8 +60,9 @@ public static class WarInterestFeed
{
string warTypeId = ReadWarTypeId(war);
WarTypeInterestEntry entry = EventCatalog.WarType.GetOrFallback(warTypeId);
if (!entry.CreatesInterest)
if (entry == null || !EventCatalog.IsCameraWorthy(entry.CreatesInterest))
{
InterestDropLog.Record("createsInterest=false", "war:" + (warTypeId ?? ""));
return false;
}

View file

@ -0,0 +1,108 @@
using System;
using System.Collections.Generic;
using System.Text;
using NeoModLoader.services;
namespace IdleSpectator;
/// <summary>
/// Ring buffer of why a beat stayed Layer B or failed to take / keep Layer A camera.
/// </summary>
public static class InterestDropLog
{
public const int Capacity = 48;
private static readonly string[] Buffer = new string[Capacity];
private static int _write;
private static int _count;
private static float _lastPlayerLogAt = -999f;
private const float PlayerLogInterval = 8f;
public static int Count => _count;
public static void Clear()
{
_write = 0;
_count = 0;
for (int i = 0; i < Capacity; i++)
{
Buffer[i] = null;
}
}
public static void Record(string reason, string detail = "")
{
if (string.IsNullOrEmpty(reason))
{
return;
}
string line = string.IsNullOrEmpty(detail)
? reason
: (reason + ": " + detail);
Buffer[_write] = line;
_write = (_write + 1) % Capacity;
if (_count < Capacity)
{
_count++;
}
float now = UnityEngine.Time.unscaledTime;
if (now - _lastPlayerLogAt >= PlayerLogInterval)
{
_lastPlayerLogAt = now;
try
{
LogService.LogInfo("[IdleSpectator][DROP] " + line);
}
catch
{
// ignore log failures
}
}
}
/// <summary>Newest-first snapshot for harness / diagnostics.</summary>
public static List<string> Snapshot(int max = 12)
{
var list = new List<string>(Math.Min(max, _count));
if (_count == 0 || max <= 0)
{
return list;
}
int n = Math.Min(max, _count);
for (int i = 0; i < n; i++)
{
int idx = (_write - 1 - i + Capacity) % Capacity;
if (!string.IsNullOrEmpty(Buffer[idx]))
{
list.Add(Buffer[idx]);
}
}
return list;
}
public static string FormatRecent(int max = 8)
{
List<string> lines = Snapshot(max);
if (lines.Count == 0)
{
return "";
}
var sb = new StringBuilder();
for (int i = 0; i < lines.Count; i++)
{
if (i > 0)
{
sb.Append(" | ");
}
sb.Append(lines[i]);
}
return sb.ToString();
}
}

View file

@ -1,10 +1,11 @@
using ai.behaviours;
using HarmonyLib;
namespace IdleSpectator;
/// <summary>
/// Authoritative happiness hooks. Universal changeHappiness covers every effect id;
/// death/birth patches supply related-unit context the history struct lacks.
/// apply-site prefixes supply related-unit context the history struct lacks.
/// </summary>
[HarmonyPatch]
internal static class ActorHappinessPatches
@ -85,30 +86,49 @@ internal static class ActorHappinessPatches
[HarmonyPrefix]
public static void PrefixLovers(Actor __instance, Actor pTarget)
{
// Context for status fell_in_love → changeHappiness("fallen_in_love") during the body.
// Do not NoteCanonicalMilestone here - that would merge the ring before it writes.
HappinessContextBag.SetPair(__instance, pTarget, HappinessRelationRole.Lover, "fallen_in_love");
}
/// <summary>
/// Live path: status fell_in_love action_on_receive → changeHappiness("fallen_in_love").
/// Bridge still publishes for units that suppress changeHappiness (no emotions / egg),
/// and ensures related names when the status path is skipped. Self-merge within 0.35s
/// collapses the double for emotion units.
/// </summary>
[HarmonyPatch(typeof(Actor), nameof(Actor.becomeLoversWith))]
[HarmonyPostfix]
[HarmonyPriority(Priority.First)]
public static void PostfixLovers(Actor __instance, Actor pTarget)
{
try
{
if (__instance != null)
{
HappinessEventRouter.NoteCanonicalMilestone(__instance.getID(), "milestone_lover");
}
if (pTarget != null)
{
HappinessEventRouter.NoteCanonicalMilestone(pTarget.getID(), "milestone_lover");
}
BridgeFallenInLove(__instance, pTarget);
BridgeFallenInLove(pTarget, __instance);
}
catch
finally
{
// ignore
HappinessContextBag.ClearRelated();
}
}
[HarmonyPatch(typeof(Actor), nameof(Actor.becomeLoversWith))]
[HarmonyPostfix]
public static void PostfixLovers()
private static void BridgeFallenInLove(Actor subject, Actor related)
{
HappinessContextBag.ClearRelated();
if (subject == null || !subject.isAlive())
{
return;
}
Actor relatedAlive = related != null && related.isAlive() ? related : null;
int amount = HappinessGameApi.ResolveAmount("fallen_in_love", 0);
HappinessEventRouter.PublishApplied(
subject,
"fallen_in_love",
amount,
HappinessSourceHook.ChangeHappiness,
relatedAlive,
HappinessRelationRole.Lover);
}
[HarmonyPatch(typeof(Actor), nameof(Actor.setBestFriend))]
@ -140,4 +160,88 @@ internal static class ActorHappinessPatches
{
HappinessContextBag.ClearRelated();
}
[HarmonyPatch(typeof(Actor), nameof(Actor.newKillAction))]
[HarmonyPrefix]
public static void PrefixKillAction(Actor __instance, Actor pDeadUnit)
{
HappinessContextBag.SetRelated(pDeadUnit, HappinessRelationRole.Victim, "just_killed");
}
[HarmonyPatch(typeof(Actor), nameof(Actor.newKillAction))]
[HarmonyPostfix]
public static void PostfixKillAction()
{
HappinessContextBag.ClearRelated();
}
[HarmonyPatch(typeof(Actor), nameof(Actor.stealActionFrom))]
[HarmonyPrefix]
public static void PrefixSteal(Actor __instance, Actor pTarget)
{
// Victim gets got_robbed; related = thief.
HappinessContextBag.SetRelated(__instance, HappinessRelationRole.Robber, "got_robbed");
}
[HarmonyPatch(typeof(Actor), nameof(Actor.stealActionFrom))]
[HarmonyPostfix]
public static void PostfixSteal()
{
HappinessContextBag.ClearRelated();
}
[HarmonyPatch(typeof(BehCheckForBabiesFromSexualReproduction), nameof(BehCheckForBabiesFromSexualReproduction.execute))]
[HarmonyPrefix]
public static void PrefixKissed(Actor pActor)
{
Actor lover = null;
try
{
if (pActor != null && pActor.hasLover())
{
lover = pActor.lover;
}
}
catch
{
lover = null;
}
HappinessContextBag.SetPair(pActor, lover, HappinessRelationRole.Lover, "just_kissed");
}
[HarmonyPatch(typeof(BehCheckForBabiesFromSexualReproduction), nameof(BehCheckForBabiesFromSexualReproduction.execute))]
[HarmonyPostfix]
public static void PostfixKissed()
{
HappinessContextBag.ClearRelated();
}
[HarmonyPatch(typeof(BehFinishTalk), "finishTalk")]
[HarmonyPrefix]
public static void PrefixFinishTalk(Actor pActor, Actor pTarget)
{
HappinessContextBag.SetPair(pActor, pTarget, HappinessRelationRole.ConversationPartner, "just_talked");
}
[HarmonyPatch(typeof(BehFinishTalk), "finishTalk")]
[HarmonyPostfix]
public static void PostfixFinishTalk()
{
HappinessContextBag.ClearRelated();
}
[HarmonyPatch(typeof(BehFinishTalk), "makeGift")]
[HarmonyPrefix]
public static void PrefixMakeGift(Actor pActor, Actor pTarget)
{
HappinessContextBag.SetPair(pActor, pTarget, HappinessRelationRole.GiftPartner, "just_gave_gift");
}
[HarmonyPatch(typeof(BehFinishTalk), "makeGift")]
[HarmonyPostfix]
public static void PostfixMakeGift()
{
HappinessContextBag.ClearRelated();
}
}

View file

@ -35,12 +35,9 @@ public static class BoatEventPatches
[HarmonyPostfix]
public static void PostfixLoad(Actor pActor, BehResult __result)
{
if (pActor == null || !pActor.isAlive() || __result == BehResult.Stop)
{
return;
}
EmitBoat("boat_load", 55f, pActor, EventReason.Boat(pActor, "load"));
// Loading is routine transport churn - unload/trade stay on Layer A.
_ = pActor;
_ = __result;
}
private static void EmitBoat(string eventId, float strength, Actor actor, string label)

View file

@ -12,23 +12,9 @@ public static class BuildingEventPatches
[HarmonyPostfix]
public static void PostfixDamageBuilding(Actor pActor, BehResult __result)
{
if (pActor == null || !pActor.isAlive() || __result == BehResult.Stop)
{
return;
}
Building target = null;
try
{
target = pActor.beh_building_target;
}
catch
{
target = null;
}
string label = EventReason.BuildingDamage(pActor, target);
Emit("building_damage", 70f, pActor, label);
// Routine siege chips are Layer B noise. Camera waits for consume/destroy.
_ = pActor;
_ = __result;
}
[HarmonyPatch(typeof(BehConsumeTargetBuilding), nameof(BehConsumeTargetBuilding.execute))]

View file

@ -62,6 +62,29 @@ public static class MetaEventPatches
EmitNew("new_city", "Politics", 74f, __result, "city");
}
[HarmonyPatch(typeof(KingdomManager), nameof(KingdomManager.makeNewCivKingdom))]
[HarmonyPostfix]
public static void PostfixNewCivKingdom(Kingdom __result, Actor pActor)
{
if (__result == null)
{
return;
}
Actor anchor = pActor != null && pActor.isAlive() ? pActor : TryReadActor(__result);
if (anchor == null)
{
return;
}
MetaInterestFeed.EmitMeta(
"new_kingdom",
"Politics",
82f,
anchor,
EventReason.MetaNew(anchor, "kingdom"));
}
private static void EmitNew(
string eventId,
string category,

View file

@ -74,6 +74,32 @@ public static class PlotEventPatches
PlotInterestFeed.Emit(__state, __instance, "leave");
}
[HarmonyPatch(typeof(Plot), nameof(Plot.finishPlot))]
[HarmonyPostfix]
public static void PostfixFinishPlot(Plot __instance, PlotState pState, Actor pActor)
{
if (__instance == null || pState != PlotState.Finished)
{
return;
}
// Prefer finishing actor; fall back to plot author inside EmitFromPlot.
if (pActor != null && pActor.isAlive())
{
string assetId = ReadPlotAssetId(__instance);
if (string.IsNullOrEmpty(assetId))
{
PlotInterestFeed.EmitFromPlot(__instance, "complete");
return;
}
PlotInterestFeed.Emit(assetId, pActor, "complete");
return;
}
PlotInterestFeed.EmitFromPlot(__instance, "complete");
}
private static string ReadPlotAssetId(object plot)
{
try

View file

@ -26,87 +26,96 @@ public static class RelationshipEventPatches
RelationshipInterestFeed.Emit("set_lover", __instance, pActor);
}
[HarmonyPatch(typeof(Actor), nameof(Actor.addChild))]
[HarmonyPostfix]
public static void PostfixAddChild(Actor __instance, object pObject)
/// <summary>
/// Surviving partners clear dead lovers by field assignment in
/// <see cref="MapBox.checkEventUnitsDestroy"/>, which bypasses <see cref="Actor.setLover"/>.
/// Emit clear_lover for the living partner before those fields are nulled.
/// </summary>
[HarmonyPatch(typeof(MapBox), "checkEventUnitsDestroy")]
[HarmonyPrefix]
public static void PrefixDeadLoverCleanup(MapBox __instance)
{
Actor child = ResolveRelatedActor(pObject);
if (__instance == null || child == null)
{
return;
}
RelationshipInterestFeed.Emit("add_child", __instance, child);
}
[HarmonyPatch(typeof(Actor), "addChildSimple")]
[HarmonyPostfix]
public static void PostfixAddChildSimple(Actor __instance, object pObject)
{
Actor child = ResolveRelatedActor(pObject);
if (__instance == null || child == null)
{
return;
}
RelationshipInterestFeed.Emit("add_child", __instance, child);
}
private static Actor ResolveRelatedActor(object raw)
{
if (raw == null)
{
return null;
}
if (raw is Actor actor)
{
return actor;
}
try
{
if (raw is BaseSimObject sim && sim.isActor())
if (__instance == null || __instance.units == null || !__instance.units.event_destroy)
{
return sim.a;
return;
}
foreach (Actor unit in __instance.units)
{
try
{
if (unit == null || !unit.isAlive() || !unit.hasLover())
{
continue;
}
Actor lover = unit.lover;
if (lover == null)
{
continue;
}
bool loverAlive = false;
try
{
loverAlive = lover.isAlive();
}
catch
{
loverAlive = false;
}
if (loverAlive)
{
continue;
}
RelationshipInterestFeed.Emit("clear_lover", unit, null);
}
catch
{
// never break vanilla destroy cleanup
}
}
}
catch
{
// fall through
// never break vanilla destroy cleanup
}
}
try
/// <summary>
/// Real parent/child link. Vanilla <c>addChild</c> attaches avatar components (Dragon, Boat), not kids.
/// </summary>
[HarmonyPatch(typeof(Actor), nameof(Actor.setParent1))]
[HarmonyPostfix]
public static void PostfixSetParent1(Actor __instance, Actor pParentActor, bool pIncreaseChildren)
{
EmitAddChild(pParentActor, __instance, pIncreaseChildren);
}
[HarmonyPatch(typeof(Actor), nameof(Actor.setParent2))]
[HarmonyPostfix]
public static void PostfixSetParent2(Actor __instance, Actor pActor, bool pIncreaseChildren)
{
EmitAddChild(pActor, __instance, pIncreaseChildren);
}
private static void EmitAddChild(Actor parent, Actor child, bool increaseChildren)
{
if (!increaseChildren || parent == null || child == null)
{
var type = raw.GetType();
foreach (string name in new[] { "actor", "Actor", "a", "parent" })
{
var prop = type.GetProperty(name);
if (prop != null && typeof(Actor).IsAssignableFrom(prop.PropertyType))
{
return prop.GetValue(raw, null) as Actor;
}
var field = type.GetField(name);
if (field != null && typeof(Actor).IsAssignableFrom(field.FieldType))
{
return field.GetValue(raw) as Actor;
}
}
var getActor = type.GetMethod("getActor", Type.EmptyTypes)
?? type.GetMethod("GetActor", Type.EmptyTypes);
if (getActor != null && typeof(Actor).IsAssignableFrom(getActor.ReturnType))
{
return getActor.Invoke(raw, null) as Actor;
}
return;
}
catch
if (!parent.isAlive() || !child.isAlive())
{
// ignore
return;
}
return null;
RelationshipInterestFeed.Emit("add_child", parent, child);
}
[HarmonyPatch(typeof(FamilyManager), nameof(FamilyManager.newFamily))]
@ -136,6 +145,10 @@ public static class RelationshipEventPatches
RelationshipInterestFeed.Emit("baby_created", __result, null);
}
/// <summary>
/// Seeking only: BehFindLover returns Continue both when still searching and after
/// <see cref="Actor.becomeLoversWith"/>. Success is covered by set_lover / love status.
/// </summary>
[HarmonyPatch(typeof(BehFindLover), nameof(BehFindLover.execute))]
[HarmonyPostfix]
public static void PostfixFindLover(Actor pActor, BehResult __result)
@ -145,6 +158,11 @@ public static class RelationshipEventPatches
return;
}
if (pActor.hasLover())
{
return;
}
RelationshipInterestFeed.Emit("find_lover", pActor, null);
}
@ -157,6 +175,7 @@ public static class RelationshipEventPatches
return;
}
// BehFamilyGroupNew always calls FamilyManager.newFamily first - share that founding key.
RelationshipInterestFeed.Emit("family_group_new", pActor, ActorRelation.ResolvePackRelated(pActor));
}
@ -183,4 +202,20 @@ public static class RelationshipEventPatches
RelationshipInterestFeed.Emit("family_group_leave", pActor, ActorRelation.ResolvePackRelated(pActor));
}
/// <summary>
/// Alpha succession is happiness-gated by emotions in vanilla. Patch the family API so
/// animals without emotions still get a Layer A / Activity-capable beat.
/// </summary>
[HarmonyPatch(typeof(Family), "setAlpha")]
[HarmonyPostfix]
public static void PostfixSetAlpha(Actor pActor, bool pNew)
{
if (!pNew || pActor == null || !pActor.isAlive())
{
return;
}
InterestFeeds.EmitAlpha(pActor, "family_set_alpha");
}
}

View file

@ -113,17 +113,13 @@ public static class TraitEventPatches
}
DiscreteEventEntry entry = EventCatalog.Trait.GetOrFallback(traitId);
if (!entry.CreatesInterest)
{
return;
}
// Quiet-world Ambient traits must not steal the camera.
if (entry.EventStrength < InterestScoringConfig.W.noticeScoreMin)
if (!EventCatalog.IsCameraWorthy(entry))
{
InterestDropLog.Record("createsInterest=false", "trait:" + (traitId ?? ""));
return;
}
// Quiet traits still register; low EventStrength lets the director bury them.
// Spawn/birth endowment: only legendary rarities.
if (gained && IsSpawnTraitWindow(actor) && !LegendaryBirthTraits.Contains(entry.Id))
{

View file

@ -161,7 +161,8 @@ public static class HappinessEventRouter
HappinessSourceHook hook,
Actor related = null,
HappinessRelationRole role = HappinessRelationRole.None,
bool forceRing = false)
bool forceRing = false,
string relatedNameOverride = null)
{
return PublishCore(
subject,
@ -172,7 +173,8 @@ public static class HappinessEventRouter
role,
applied: true,
suppressedByPsychopath: false,
forceRing);
forceRing,
relatedNameOverride);
}
/// <summary>Record that the game refused to apply happiness (psychopath/egg/no emotions).</summary>
@ -183,6 +185,7 @@ public static class HappinessEventRouter
_suppressedSinceClear++;
}
InterestDropLog.Record("no_emotions_or_egg", effectId ?? "");
var occ = BuildOccurrence(
subject,
effectId,
@ -205,7 +208,8 @@ public static class HappinessEventRouter
HappinessRelationRole role,
bool applied,
bool suppressedByPsychopath,
bool forceRing)
bool forceRing,
string relatedNameOverride = null)
{
if (subject == null || string.IsNullOrEmpty(effectId) || !applied)
{
@ -238,7 +242,15 @@ public static class HappinessEventRouter
}
HappinessOccurrence occ = BuildOccurrence(
subject, effectId, amount, hook, related, role, applied, suppressedByPsychopath);
subject,
effectId,
amount,
hook,
related,
role,
applied,
suppressedByPsychopath,
relatedNameOverride);
FillProse(occ, entry);
if (ShouldMerge(occ, entry))
@ -448,7 +460,8 @@ public static class HappinessEventRouter
Actor related,
HappinessRelationRole role,
bool applied,
bool suppressedByPsychopath)
bool suppressedByPsychopath,
string relatedNameOverride = null)
{
HappinessCatalogEntry entry = EventCatalog.Happiness.GetOrFallback(effectId);
long subjectId = 0;
@ -494,6 +507,11 @@ public static class HappinessEventRouter
}
}
if (string.IsNullOrEmpty(relatedName) && !string.IsNullOrEmpty(relatedNameOverride))
{
relatedName = relatedNameOverride.Trim();
}
StampWorldDate(out double worldTime, out string dateLabel);
string corr = subjectId + "|" + effectId + "|" + relatedId + "|" + Math.Floor(worldTime);

View file

@ -62,6 +62,8 @@ internal static class HarnessScenarios
return DomainEventAudit();
case "event_tracking":
return EventTracking();
case "family_trigger_smoke":
return FamilyTriggerSmoke();
case "mutation_discovery":
return MutationDiscovery();
case "chronicle":
@ -172,6 +174,37 @@ internal static class HarnessScenarios
};
}
private static List<HarnessCommand> FamilyTriggerSmoke()
{
return new List<HarnessCommand>
{
Step("ft0", "wait_world"),
Step("ft1", "dismiss_windows"),
Step("ft2", "spectator", value: "off"),
// Bridge: becomeLoversWith publishes happiness fallen_in_love
Step("ft10", "spawn", asset: "human"),
Step("ft11", "happiness_remember_partner"),
Step("ft12", "spawn", asset: "human"),
Step("ft13", "focus", asset: "auto"),
Step("ft14", "activity_clear"),
Step("ft15", "happiness_reset"),
Step("ft16", "happiness_bond_lovers"),
Step("ft17", "assert", expect: "activity_happiness_count", value: "2", label: "min"),
Step("ft18", "assert", expect: "activity_log_contains", value: "love"),
// Retarget: setParent1 emits rel:add_child
Step("ft20", "spawn", asset: "human"),
Step("ft21", "happiness_remember_partner"),
Step("ft22", "spawn", asset: "human"),
Step("ft23", "focus", asset: "auto"),
Step("ft24", "relationship_set_parent"),
Step("ft25", "assert", expect: "interest_has_key", value: "rel:add_child"),
Step("ft99", "snapshot"),
};
}
private static List<HarnessCommand> ActivityHappinessAudit()
{
return new List<HarnessCommand>
@ -202,7 +235,7 @@ internal static class HarnessScenarios
Step("hp11", "assert", expect: "activity_happiness_task_id", value: "happiness:become_king"),
Step("hp12", "assert", expect: "activity_happiness_ring_count", value: "1", label: "exact"),
Step("hp13", "assert", expect: "activity_happiness_life_count", value: "1", label: "min"),
Step("hp14", "assert", expect: "activity_log_contains", value: "throne"),
Step("hp14", "assert", expect: "activity_log_contains", value: "ruler"),
// Ambient cooldown: second meal within window should not add another ring write
Step("hp20", "happiness_reset"),
@ -226,6 +259,8 @@ internal static class HarnessScenarios
Step("hp44", "activity_clear"),
Step("hp45", "happiness_reset"),
Step("hp46", "happiness_bond_lovers"),
Step("hp46b", "assert", expect: "activity_happiness_count", value: "2", label: "min"),
Step("hp46c", "assert", expect: "activity_log_contains", value: "love"),
Step("hp47", "wait", value: "0.5"),
Step("hp48", "happiness_kill_partner"),
Step("hp49", "wait", value: "0.5"),
@ -1197,12 +1232,21 @@ internal static class HarnessScenarios
Step("et20", "domain_feed", asset: "relationship", value: "set_lover"),
Step("et21", "assert", expect: "interest_has_key", value: "rel:set_lover"),
Step("et21a", "domain_feed", asset: "relationship", value: "become_alpha"),
Step("et21b", "assert", expect: "interest_has_key", value: "rel:become_alpha"),
Step("et21c", "interest_force_session", asset: "human",
label: "{a} becomes the family alpha", tier: "Action", expect: "alpha_reason",
value: "lead=event;evt=72"),
Step("et21d", "assert", expect: "dossier_contains", value: "family alpha"),
Step("et22", "domain_feed", asset: "plot", value: "summon_meteor_rain"),
Step("et23", "assert", expect: "interest_has_key", value: "plot:summon_meteor_rain"),
Step("et23b", "assert", expect: "interest_label_contains", value: "plots to summon"),
Step("et24", "domain_feed", asset: "era", value: "age_chaos"),
Step("et25", "assert", expect: "interest_has_key", value: "era:age_chaos"),
Step("et26", "domain_feed", asset: "meta", value: "new_city"),
Step("et27", "assert", expect: "interest_has_key", value: "meta:new_city"),
Step("et27b", "domain_feed", asset: "meta", value: "new_kingdom"),
Step("et27c", "assert", expect: "interest_has_key", value: "meta:new_kingdom"),
Step("et28", "domain_feed", asset: "book", value: "bad_story_about_king"),
Step("et29", "assert", expect: "interest_has_key", value: "book:bad_story_about_king"),
Step("et30", "domain_feed", asset: "trait", value: "immortal"),
@ -1217,6 +1261,16 @@ internal static class HarnessScenarios
Step("et43", "assert", expect: "interest_has_key", value: "item:"),
Step("et44", "domain_feed", asset: "decision", value: "declare_war"),
Step("et45", "assert", expect: "interest_has_key", value: "decision:"),
Step("et45b", "interest_force_session", asset: "human",
label: "{a} decides to declare war", tier: "Action", expect: "decision_prose",
value: "lead=event;evt=86"),
Step("et45c", "assert", expect: "dossier_contains", value: "decides to declare war"),
Step("et45d", "assert", expect: "dossier_not_contains", value: "decides Declare"),
Step("et45e", "interest_force_session", asset: "human",
label: "{a} decides to change the kingdom's culture", tier: "Action", expect: "king_decision_prose",
value: "lead=event;evt=86"),
Step("et45f", "assert", expect: "dossier_contains", value: "decides to change the kingdom's culture"),
Step("et45g", "assert", expect: "dossier_not_contains", value: "decides to king"),
Step("et46", "domain_feed", asset: "power", value: "lightning"),
Step("et47", "assert", expect: "interest_has_key", value: "power:"),
Step("et48", "domain_feed", asset: "subspecies_trait", value: "fire_blood"),

View file

@ -552,12 +552,14 @@ public static class InterestDirector
// Queued too long - miss the beat.
if (now - c.CreatedAt > 5.5f)
{
InterestDropLog.Record("stale", c.HappinessEffectId + " queue");
return true;
}
Actor unit = c.FollowUnit;
if (unit == null || !unit.isAlive())
{
InterestDropLog.Record("stale", c.HappinessEffectId + " dead");
return true;
}
@ -569,6 +571,7 @@ public static class InterestDirector
{
if (!unit.isBaby() && unit.getAge() > 1)
{
InterestDropLog.Record("stale", "just_born grown");
return true;
}
}
@ -876,11 +879,17 @@ public static class InterestDirector
if (_current != null
&& (_current.Completion == InterestCompletionKind.CombatActive
|| _current.Completion == InterestCompletionKind.StatusPhase
|| _current.Completion == InterestCompletionKind.HappinessGrief))
|| _current.Completion == InterestCompletionKind.HappinessGrief
|| IsStickyStoryScene(_current)))
{
float graceCur = _current.TotalScore;
float graceNext = candidate.TotalScore;
return graceNext >= graceCur + InterestScoringConfig.W.cutInMargin
float graceMargin = Mathf.Max(
InterestScoringConfig.W.cutInMargin,
InterestScoringConfig.W.stickyCutInMargin > 0f
? InterestScoringConfig.W.stickyCutInMargin
: 50f);
return graceNext >= graceCur + graceMargin
&& IsWorthWatchingNow(candidate, now, selectingDuringGrace: true);
}
@ -899,6 +908,10 @@ public static class InterestDirector
bool nextFill = InterestScoring.IsFillScore(nextScore);
bool curAmbient = IsAmbientShot(_current);
bool curFill = curAmbient || InterestScoring.IsFillScore(curScore);
bool sticky = IsStickyStoryScene(_current);
float cutMargin = sticky
? Mathf.Max(w.cutInMargin, w.stickyCutInMargin > 0f ? w.stickyCutInMargin : 50f)
: w.cutInMargin;
// Ambient fill: any real event may take the camera immediately (no min dwell).
if (curAmbient && !nextFill && !IsAmbientShot(candidate))
@ -906,12 +919,25 @@ public static class InterestDirector
return true;
}
// Instant score-margin cut - no settle grace, no MinDwell block.
if (nextScore >= curScore + w.cutInMargin)
// Same-arc refresh (combat/hatch/grief keys) may replace without full margin.
if (sticky && IsSameStoryArc(_current, candidate) && nextScore >= curScore - w.rotateSlack)
{
return true;
}
// Instant score-margin cut - no settle grace, no MinDwell block.
if (nextScore >= curScore + cutMargin)
{
return true;
}
if (sticky && nextScore < curScore + cutMargin)
{
InterestDropLog.Record(
"below_margin",
$"sticky cur={curScore:0.#} next={nextScore:0.#} need={cutMargin:0.#}");
}
// Fill never cuts a protected non-fill session without margin (already failed above).
if (nextFill && !curFill && protectedScene)
{
@ -924,8 +950,8 @@ public static class InterestDirector
return false;
}
// Protected EventLed hold: only margin cut-in (handled above).
if (protectedScene && !curAmbient)
// Protected / sticky EventLed hold: only margin or same-arc (handled above).
if ((protectedScene || sticky) && !curAmbient)
{
return false;
}
@ -941,6 +967,89 @@ public static class InterestDirector
&& nextScore >= curScore - w.rotateSlack;
}
private static bool IsStickyStoryScene(InterestCandidate c)
{
if (c == null)
{
return false;
}
if (c.Completion == InterestCompletionKind.CombatActive
|| c.Completion == InterestCompletionKind.StatusPhase
|| c.Completion == InterestCompletionKind.HappinessGrief)
{
return true;
}
// Hatch FixedDwell is a story beat - hold against weak peers.
if (!string.IsNullOrEmpty(c.HappinessEffectId)
&& (string.Equals(c.HappinessEffectId, "just_got_out_of_egg", StringComparison.OrdinalIgnoreCase)
|| string.Equals(c.HappinessEffectId, "just_hatched", StringComparison.OrdinalIgnoreCase)))
{
return true;
}
if (!string.IsNullOrEmpty(c.Key)
&& (c.Key.StartsWith("combat:", StringComparison.Ordinal)
|| c.Key.StartsWith("hatch:", StringComparison.Ordinal)))
{
return true;
}
return false;
}
private static bool IsSameStoryArc(InterestCandidate current, InterestCandidate next)
{
if (current == null || next == null)
{
return false;
}
if (!string.IsNullOrEmpty(current.Key)
&& !string.IsNullOrEmpty(next.Key)
&& string.Equals(current.Key, next.Key, StringComparison.Ordinal))
{
return true;
}
// Combat / hatch share key prefixes per subject.
if (!string.IsNullOrEmpty(current.Key) && !string.IsNullOrEmpty(next.Key))
{
if (SameKeyPrefix(current.Key, next.Key, "combat:")
|| SameKeyPrefix(current.Key, next.Key, "hatch:"))
{
return true;
}
}
if (current.Completion == InterestCompletionKind.HappinessGrief
&& next.Completion == InterestCompletionKind.HappinessGrief
&& current.SubjectId != 0
&& current.SubjectId == next.SubjectId)
{
return true;
}
return false;
}
private static bool SameKeyPrefix(string a, string b, string prefix)
{
if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b) || string.IsNullOrEmpty(prefix))
{
return false;
}
if (!a.StartsWith(prefix, StringComparison.Ordinal)
|| !b.StartsWith(prefix, StringComparison.Ordinal))
{
return false;
}
return string.Equals(a, b, StringComparison.Ordinal);
}
/// <summary>
/// Still-worth-watching filter: drop stale queued moments; keep live sticky conditions
/// and fresh FixedDwell; during grace, brand-new fires are always eligible.

View file

@ -183,27 +183,63 @@ public static class InterestScoring
float sig = 0f;
if (snap.Favorite)
{
sig += w.metaFavorite;
sig += EventCatalogConfig.CharacterBonus("favorite", w.metaFavorite);
}
if (snap.King)
{
sig += w.metaKing;
sig += EventCatalogConfig.CharacterBonus("king", w.metaKing);
}
else if (snap.Leader)
{
sig += w.metaLeader;
sig += EventCatalogConfig.CharacterBonus("leader", w.metaLeader);
}
float renownDiv = w.metaRenownDivisor > 0f ? w.metaRenownDivisor : 120f;
sig += Mathf.Min(w.metaRenownCap, snap.Renown / renownDiv);
sig += Mathf.Min(w.metaKillsCap, snap.Kills * w.metaKillsFactor);
sig += Mathf.Min(w.metaLevelCap, snap.Level * w.metaLevelFactor);
sig += SpeciesRarityBonus(snap.SpeciesId, w);
sig += EventCatalogConfig.SpeciesBonus(snap.SpeciesId);
snap.CharacterSignificance = sig;
MetaCache[id] = snap;
return snap;
}
private static float SpeciesRarityBonus(string speciesId, ScoringWeights w)
{
if (string.IsNullOrEmpty(speciesId) || w == null)
{
return 0f;
}
try
{
Dictionary<string, int> counts = WorldActivityScanner.CountSpeciesPopulations();
if (counts == null || !counts.TryGetValue(speciesId, out int pop) || pop <= 0)
{
return 0f;
}
if (pop == 1)
{
return w.speciesSingletonBonus;
}
int rareMax = w.speciesRareMaxPop > 0 ? w.speciesRareMaxPop : 5;
if (pop <= rareMax)
{
return w.speciesRareBonus;
}
}
catch
{
// ignore
}
return 0f;
}
private static void ApplyMeta(InterestCandidate c, InterestMetadataSnapshot snap)
{
if (c == null || snap == null)

View file

@ -28,6 +28,8 @@ public class ScoringWeights
// Director policy (score-native; replaces InterestTier preemption).
public float cutInMargin = 35f;
/// <summary>Extra margin required to interrupt sticky A scenes (combat/grief/status/hatch).</summary>
public float stickyCutInMargin = 50f;
public float rotateSlack = 12f;
public float fillScoreMax = 55f;
public float noticeScoreMin = 70f;
@ -77,6 +79,11 @@ public class ScoringWeights
public float metaKillsCap = 12f;
public float metaLevelFactor = 0.4f;
public float metaLevelCap = 10f;
/// <summary>Bonus when the follow unit is the only living member of its species.</summary>
public float speciesSingletonBonus = 14f;
/// <summary>Bonus when species population is between 2 and speciesRareMaxPop inclusive.</summary>
public float speciesRareBonus = 8f;
public int speciesRareMaxPop = 5;
public int notableKills = 40;
public float notableRenown = 200f;

View file

@ -55,6 +55,7 @@ public class ModClass : MonoBehaviour, IMod, IConfigurable, ILocalizable
ModFolder = pModDecl.FolderPath;
_config = ModSettings.Load(pModDecl);
InterestScoringConfig.LoadFromModFolder(ModFolder);
EventCatalogConfig.LoadFromModFolder(ModFolder);
_harmony = new Harmony(pModDecl.UID);
ApplyHarmonyPatches(pModDecl.Name);

View file

@ -106,6 +106,9 @@ public static class MutationDiscoveryHarness
"ArmyManager.newArmy",
"AllianceManager.newAlliance",
"CityManager.newCity",
"KingdomManager.makeNewCivKingdom",
"Family.setAlpha",
"Plot.finishPlot",
"ItemManager.generateItem",
"ItemManager.newItem",
"SubspeciesManager.newSpecies",

View file

@ -119,7 +119,7 @@ public sealed class UnitDossier
d.Headline = string.IsNullOrEmpty(d.SpeciesId)
? d.Name
: $"{d.Name} ({d.SpeciesId})";
d.ReasonLine = BuildStoryBeat(d, watchLabel, actor);
d.ReasonLine = ComposeReasonWithJob(BuildStoryBeat(d, watchLabel, actor), d.JobLabel);
d.DetailLine = BuildDetailLine(d);
d.CaptionText = JoinCaption(d.Headline, d.ReasonLine, d.DetailLine);
return d;
@ -136,6 +136,32 @@ public sealed class UnitDossier
return ActivityProse.HumanizeJobPublic(jobId);
}
/// <summary>
/// Job sits on the orange reason row: alone when there is no watch beat, otherwise
/// <c>{reason} · {job}</c>.
/// </summary>
public static string ComposeReasonWithJob(string reason, string job)
{
string r = (reason ?? "").Trim();
string j = (job ?? "").Trim();
if (string.IsNullOrEmpty(j))
{
return r;
}
if (string.IsNullOrEmpty(r))
{
return j;
}
if (r.IndexOf(j, StringComparison.OrdinalIgnoreCase) >= 0)
{
return r;
}
return r + " · " + j;
}
public bool ContainsIgnoreCase(string needle)
{
if (string.IsNullOrEmpty(needle))
@ -147,6 +173,7 @@ public sealed class UnitDossier
|| (Headline ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|| (ReasonLine ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|| (DetailLine ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|| (JobLabel ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|| (Name ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|| (SpeciesId ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|| (TaskText ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0)
@ -284,12 +311,14 @@ public sealed class UnitDossier
// Ownership: never let the reason name a living stranger (not subject/related).
if (ReasonNamesStranger(beat, scene))
{
InterestDropLog.Record("identity_filter", "stranger:" + beat);
return "";
}
// Ownership: reject beats that lead with someone else's proper name.
if (LeadsWithForeignName(beat, scene))
{
InterestDropLog.Record("identity_filter", "foreign:" + beat);
return "";
}
@ -494,9 +523,22 @@ public sealed class UnitDossier
}
}
// EventReason sentences are dossier-ready - keep names; only drop identity crumbs.
// EventReason / Name-verb sentences are dossier-ready.
if (LooksLikeEventSentence(s)
|| (!string.IsNullOrEmpty(d?.Name) && StartsWithSubjectVerbPhrase(s, d.Name)))
{
if (IsWeakFlavorBeat(s, d))
{
InterestDropLog.Record("identity_filter", "weak:" + s);
return "";
}
return CleanBeat(s);
}
if (IsIdentityWhy(s, d) || IsWeakFlavorBeat(s, d))
{
InterestDropLog.Record("identity_filter", s);
return "";
}

View file

@ -647,7 +647,9 @@ public static class WatchCaption
return;
}
string next = UnitDossier.OwnedEventReason(actor, _current);
string next = UnitDossier.ComposeReasonWithJob(
UnitDossier.OwnedEventReason(actor, _current),
_current.JobLabel);
string prev = _current.ReasonLine ?? "";
if (next == prev)
{

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{
"name": "IdleSpectator",
"author": "dazed",
"version": "0.21.3",
"description": "AFK Idle Spectator (I) + Lore (L). Event-centered scene director + happiness Activity/Life logging.",
"version": "0.25.11",
"description": "AFK Idle Spectator (I) + Lore (L). Event trigger audit Phases 4-8 + job reason row.",
"GUID": "com.dazed.idlespectator"
}

View file

@ -11,6 +11,7 @@
"noveltyMultiplier": 4,
"cutInMargin": 35,
"stickyCutInMargin": 50,
"rotateSlack": 12,
"fillScoreMax": 55,
"noticeScoreMin": 70,
@ -58,6 +59,9 @@
"metaKillsCap": 12,
"metaLevelFactor": 0.4,
"metaLevelCap": 10,
"speciesSingletonBonus": 14,
"speciesRareBonus": 8,
"speciesRareMaxPop": 5,
"notableKills": 40,
"notableRenown": 200,

View file

@ -1,21 +1,21 @@
# Event audit (post-expand)
# Event audit (A/B layers)
Inventory policy after the full gap expand.
Camera interrupt is score-margin only (`cutInMargin`); no Signal/Ambient ownership gate.
**Layer B (ticker):** Activity / Life coverage for live library ids - honest prose, completeness.
**Layer A (story camera):** only `EventCatalog.IsCameraWorthy` beats register interest and cut the camera.
**Single inventory:** [`EventCatalog`](../IdleSpectator/Events/EventCatalog.cs) (partials under `Events/Catalogs/`).
Nested domains: `Status`, `Happiness`, `WorldLog`, `Trait`, `Combat`, `Spell`, …
Feeds/patches register from those dials; `InterestDirector` only ranks candidates.
Feeds/patches register A from those dials; `InterestDirector` only ranks candidates.
## Dials
| Dial | Role |
|------|------|
| `CreatesInterest` | Whether a live id may register at all |
| `EventStrength` | Base contribution to `TotalScore` (interrupt via +`cutInMargin`) |
| `Completion` | Hold-until-complete kind |
| `MaxWatch` | Hard cap (class defaults in `scoring-model.json`) |
| `EventReason` | Orange dossier sentence |
| Dial | Layer | Role |
|------|-------|------|
| Happiness `Presentation` | A if Signal | Ambient/Aggregate = B-only |
| `CreatesInterest` | A gate | Discrete / status / WorldLog / libraries |
| `EventStrength` | A rank | Base `TotalScore` (interrupt via margin) |
| `Completion` | A hold | Hold-until-complete kind |
| `MaxWatch` | A hold | Hard cap (`scoring-model.json`) |
| `EventReason` / Label | A display | Orange dossier sentence |
## MaxWatch class defaults
@ -27,56 +27,48 @@ Feeds/patches register from those dials; `InterestDirector` only ranks candidate
| Combat | 60 | `maxWatchCombat` |
| Epic world | 50 | `maxWatchEpic` |
Peer rotate / soft cut on EventLed floors at `minCameraDwell` (15s). Score-margin interrupts bypass that floor.
Ambient / Character fill is exempt - events may take the camera immediately.
Peer rotate / soft cut on EventLed floors at `minCameraDwell` (15s).
Score-margin interrupts bypass that floor; sticky A scenes use `stickyCutInMargin`.
Ambient / Character fill is exempt - A events may take the camera immediately.
## Sources (wired)
| Source | Primary owner | Reason factory | Notes |
|--------|---------------|----------------|-------|
| WorldLog | `InterestFeeds.OnWorldLogMessage` | catalog / EventReason | Strength from `EventCatalog.WorldLog` |
| Happiness | `IngestHappiness` | `EventCatalog.Happiness` + EventReason | Prose must match game call sites - see [event-prose-audit.md](event-prose-audit.md) |
| Status | `OnStatusChange` | EventReason.Status | `drowning` CreatesInterest=true, strength ~50; notable via CharacterSignificance |
| Scanner combat | WorldActivityScanner | EventReason.Fight / Battle | Completion CombatActive; dials `EventCatalog.Combat` |
| War | WarEventPatches + WarInterestFeed.Tick | war type labels | |
| Plot | PlotEventPatches + PlotInterestFeed.Tick | `EventCatalog.Plot` | |
| Relationship | RelationshipEventPatches + happiness lovers | EventReason.* | |
| Trait | TraitEventPatches | EventReason.Trait | Spawn window filters routine traits |
| Building | BuildingEventPatches | EventReason.Building* | Eat/damage/falls |
| Boat | BoatEventPatches | EventReason.Boat | |
| Book | BookEventPatches | `EventCatalog.Book` sentences | |
| Era / Meta | MetaEventPatches | EventReason.MetaNew / era labels | |
| Spell | CombatActionLibrary.tryToCastSpell | EventReason.Library | Ambient CreatesInterest=false except spectacle ids |
| Power | PlayerControl.clickedFinal | EventReason.HumanizeId | Spectacle ids create interest |
| Decision | setDecisionCooldown | EventReason.Library | IsCameraWorthy filter |
| Item | ItemManager | EventReason.Library | Legendary CreatesInterest |
| Gene | Subspecies.mutateFrom | EventReason.Library | Ambient CreatesInterest=false |
| Phenotype | generatePhenotypeAndShade | EventReason.Library | |
| World law | WorldLaws.enable / toggle | EventReason.HumanizeId | Location-only |
| Biome | catalog ready | EventReason.HumanizeId | Ambient CreatesInterest=false; emit via power terraform path |
| Subspecies trait | SubspeciesManager biome hooks | EventReason.Library | |
| WorldLog | `InterestFeeds.OnWorldLogMessage` | catalog / EventReason | A if CreatesInterest |
| Happiness | `IngestHappiness` | catalog + EventReason | Signal = A; see [event-prose-audit.md](event-prose-audit.md) |
| Status | `OnStatusChange` | EventReason.Status | Gain A if CreatesInterest; egg **loss** → hatch A |
| Scanner combat | WorldActivityScanner | EventReason.Fight / Battle | `EventCatalog.Combat` |
| War / Plot / Relationship / Trait / Building / Boat / Book / Era / Kingdom | Feeds + patches | EventReason / catalog | A via CreatesInterest; alpha via Family.setAlpha |
| Libraries | Deferred feeds | EventReason.Library | Mostly B; spectacle ids A |
| Character fill | InterestDirector.TryCharacterFill | empty Label | Only when no pending EventLed |
## Ambient statuses (intentional off)
## Layer A policy
`festive_spirit`, `laughing`, `singing`, `sleeping`, `handsome_migrant` - CreatesInterest=false.
Interesting beats register as interest candidates (`Signal` / `CreatesInterest`).
Director ranks by `EventStrength` + scoring - do not hard-gate interesting life off Layer A.
## Same-moment ownership
Happiness Ambient (B) only: `got_poked`, `got_caught`, `paid_tax`, `just_ate`, `just_pooped`.
Happiness Aggregates stay civic mass (B unit camera).
Prefer typed feeds over WorldLog when both fire (relationship / status / combat).
Registry Upsert merges same key and refreshes `LastSeenAt` for sticky combat/status/grief.
Status B: brief FX/cooldowns + egg gain only.
Noise still skipped at patch (not dial): building_damage chips, boat_load.
## Full surface
See [world-event-inventory.md](world-event-inventory.md) for live library counts, gaps, and refresh commands.
## Interrupt / hold / grace
1. Instant cut when `next.TotalScore >= current + cutInMargin` (no settle; ignores min dwell).
2. Ambient fill: any EventLed may take the camera immediately (no `minCameraDwell`).
3. Else hold while `InterestCompletion.IsActive` and under MaxWatch (moments floor to `minCameraDwell`; combat uses class 60s).
4. Soft peer rotates on EventLed only after `minCameraDwell` (15s).
1. Instant cut when `next.TotalScore >= current + cutInMargin` (sticky: `stickyCutInMargin`).
2. Ambient fill: any A EventLed may take the camera immediately.
3. Else hold while `InterestCompletion.IsActive` and under MaxWatch.
4. Soft peer rotates on EventLed only after `minCameraDwell` (15s), never during protected sticky hold without margin.
5. Quiet grace 6s: still-worth-watching filter, then Character fill with empty reason.
6. Drops recorded in `InterestDropLog`.
## Mutation gap triage
`mutation_discovery` refreshes `.harness/mutation_gaps.tsv`.
Open rows are unexplained Wire candidates only.
False positives (clear/load/UI/FX/getters) and covered-by-primary paths are filtered or listed in `AlreadyWired`.
Library fields are inventory-complete via catalogs / activity / WorldLog (`mutation_libraries.tsv` notes).

View file

@ -37,7 +37,16 @@ Scope: Happiness (all authored), Relationship (all), Status/WorldLog/Plot/etc. s
## Still clean (sample)
Grief deaths, `fallen_in_love`, gifts, house find/lose, `just_got_out_of_egg`, `just_became_adult`, WorldLog `king_new` template roles, Status `egg` / `pregnant` / `drowning` prose.
Grief deaths, `fallen_in_love`, gifts, house find/lose, `just_got_out_of_egg`, `just_became_adult`, WorldLog `king_new` template roles, Status `egg` / `drowning` prose.
## A/B camera policy (2026-07-16)
Interesting = interest candidate. Director ranks by strength.
Happiness Ambient only for true FX: poke, caught stub, tax, ate, pooped.
Status CreatesInterest false only for brief FX/cooldowns + egg gain.
Gate helper: `EventCatalog.IsCameraWorthy`. Drops: `InterestDropLog`.
## Hatch camera path (fixed)
@ -58,4 +67,53 @@ Sources: `EventReason.*`, happiness catalog `PlainClause`, or discrete catalog t
Character fill / ambient may show empty reason.
Not every Activity line is a camera event (Ambient happiness, status gains with `CreatesInterest=false`, status losses except egg).
## Happiness trigger pass (2026-07-16)
Full 67-id call-site audit vs decompile + BehaviourTask / StatusLibrary / City civic force.
| Id | Finding | Keep-on fix |
|----|---------|-------------|
| `just_laughed` / `just_swore` | Fire at **end** of emotion tasks | Prose → finishes laughing / swearing |
| `just_ate` | Fires on `consumeFoodResource` | Prose → eats / enjoys a meal |
| `just_felt_the_divine` | Divine light clears bad traits | Prose → cleansed by divine light |
| `just_played` | Child play task end | Prose → finishes playing around |
| `got_caught` | Library asset, **no caller** | Stay Ambient; honest prose |
| `got_robbed` / gifts / kiss / talk / kill | Related optional but bag missing | Context bags on steal/gift/kiss/talk/kill |
| `fallen_in_love` | Status `fell_in_love` receive + bridge | Keep bridge for no-emotions |
| Civic aggregates / BehAddHappiness | Live via forceUnits / Beh* | Marked accurate |
Worksheet: `docs/event-trigger-audit.tsv` (happiness domain done).
## Status trigger pass (2026-07-16)
Full 58-id audit vs `StatusLibrary` + apply sites. Universal Activity hook:
`ActorStatusPatches``addNewStatusEffect` / `Status.finish` (egg loss also `InterestFeeds.EmitHatch`).
| Id | Finding | Keep-on fix |
|----|---------|-------------|
| `angry` | `addAggro`, not the `rage` combat buff | Prose → flies into a fury |
| `possessed_follower` | Possession talk converts **target** | Prose → converted into possessed follower |
| `pregnant` / `pregnant_parthenogenesis` | Finish → `BabyMaker` birth | Loss prose → gives birth (/ alone) |
| `taking_roots` | Finish → vegetative offspring | Loss prose → sprouts offspring |
| `budding` | Finish → bud offspring | Loss prose → splits off a bud offspring |
| `uprooting` | Plant babies emerging | Prose → emerges as a young plant |
| Dreams / `inspired` / combat FX | Sleep pot, finish→happiness, CombatActionLibrary | Already accurate |
Worksheet: `docs/event-trigger-audit.tsv` (status domain done).
## WorldLog / plot / trait pass (2026-07-16)
Phases 47 worksheet complete (all audit rows `done`).
| Area | Finding | Keep-on fix |
|------|---------|-------------|
| WorldLog `{a}`/`{b}` | `special1`/`special2` match game replacers (kingdom/king, war kingdoms) | Accurate |
| Disasters | Policy rows; camera via paired `disaster_*` WorldLog | Accurate (no DisasterManager patch) |
| Wars / eras | `WarManager` + `WarInterestFeed`; `WorldAgeManager.startNextAge` | Accurate |
| Plots | Same label for new/complete/cancel | `EventReason.Plot` phase rewrite |
| Traits | JSON said `Trait: {id} ({a})`; live was `gains`/`loses` | Catalog labels → `{a} gains {id}` |
| Boat/meta harness | Crumb strings ≠ `EventReason` | Harness uses `EventReason.Boat` / `MetaNew` |
| Building / combat | Already `EventReason.BuildingEat` / `Fight` | Accurate |
Worksheet: `docs/event-trigger-audit.tsv` (all domains done).

View file

@ -1,14 +1,23 @@
# Event reasons (camera + orange dossier)
## Contract
## A/B layers
| Layer | What | Gate |
|-------|------|------|
| **B ticker** | Activity / Life / chronicle prose for live ids | Always may write when the game fires |
| **A story camera** | Interest candidates, orange reason, camera cuts | `EventCatalog.IsCameraWorthy` only |
Happiness: `Presentation.Signal` = A; `Ambient` / `Aggregate` = B-only.
Discrete / status / WorldLog: `CreatesInterest` = A gate.
`InterestDirector` only ranks A candidates. It does not author inventory.
Registered **events** own the idle camera.
`EventStrength` / `TotalScore` ranks who wins.
Character fill runs only when the pending **EventLed** queue is empty, and uses an empty Label (task chip only).
Orange dossier reason = the owning events `Label` while the scene is active (`InterestDirector.TryGetOwnedReasonCandidate`).
Orange dossier reason = the owning A events `Label` while the scene is active (`InterestDirector.TryGetOwnedReasonCandidate`).
Quiet grace / inactive dwell clears the reason even if focus has not switched yet.
Drops (ambient, createsInterest=false, identity filter, below margin, …) go to `InterestDropLog`.
## EventReason
[`IdleSpectator/Events/EventReason.cs`](../IdleSpectator/Events/EventReason.cs) is the sole Label factory for camera candidates.
@ -19,21 +28,26 @@ Feeds call typed helpers (`Fight`, `BuildingEat`, `SeekingLover`, …) or `Apply
| Path | Role |
|------|------|
| `Events/EventCatalog.cs` (+ `Catalogs/EventCatalog.*.cs`) | **One inventory** - nested domains (`Status`, `Combat`, …) |
| `Events/Feeds/` | Build + register candidates from catalog dials |
| `Events/Patches/` | Harmony hooks → feeds |
| `Events/EventCatalog.cs` (+ `Catalogs/EventCatalog.*.cs`) | **One inventory** - nested domains + `IsCameraWorthy` |
| `Events/Feeds/` | Build + register **A** candidates from catalog dials |
| `Events/Patches/` | Harmony hooks → feeds / Activity |
| `Events/EventReason.cs` | Orange reason sentences |
| `Events/EventFeedUtil.cs` | Shared Register path |
| `Events/InterestDropLog.cs` | Why a beat stayed B or missed A |
| `Events/DiscreteEventEntry.cs` | Shared catalog row shape |
`InterestDirector` only ranks registered candidates for the camera - it does not author event inventory.
Rules:
- Sentence form with names: `{a} is fighting {b}`, `{a} is eating a beehive`, `{a} is seeking a lover`.
- Sentence form with names: `{a} is fighting {b}`, `{a} is eating a beehive`, `{a} decides to change the kingdom's culture`.
Decision orange reasons use the `action` field (infinitive after "decides to").
Other domains use `label` or happiness `variantsWithRelated` / `variantsWithoutRelated`.
All of that - plus per-id `strength` - lives in [`event-catalog.json`](../IdleSpectator/event-catalog.json).
`EventReason.Decision` only formats `{name} decides to {phrase}`.
Species/character score overlays live in the same JSON; global formula stays in `scoring-model.json`.
- Set `RelatedUnit` when the sentence names a second party.
- Never invent stranger subjects for unit-led events.
- Never use engine jargon that misstates the action (`Consumes a building` → eating/foraging via `BehConsumeTargetBuilding`).
- Never use engine jargon that misstates the action.
## Removed crumbs
@ -44,19 +58,20 @@ Do not author these as orange reasons:
- identity titles (`King of X`, `Leader of X`, `Lone beetle`)
- `Consumes a building` / `Damages a building: {a}` crumb form
## Strength dial (no ownership enum)
## Strength dial (Layer A only)
`InterestOwnership` / `OwnsCamera` were removed.
Every registration is EventLed and may own the camera.
`CreatesInterest` gates whether an id registers; `EventStrength` ranks.
Instant cut-in when `next.TotalScore >= current + cutInMargin` (see `scoring-model.json`).
`CreatesInterest` / Signal gates whether an id registers for the camera; `EventStrength` ranks among A.
Instant cut-in when `next.TotalScore >= current + cutInMargin` (sticky scenes use `stickyCutInMargin`).
See `scoring-model.json`.
## Dossier display
`UnitDossier.EventLabelBeat` keeps the authored sentence.
It only rejects identity/weak crumbs and scrubs living stranger names (`ReasonNamesStranger` / `LeadsWithForeignName`).
`UnitDossier.EventLabelBeat` keeps EventReason / `Name verb …` sentences.
It rejects identity/weak crumbs and scrubs living stranger names (`ReasonNamesStranger` / `LeadsWithForeignName`).
Active EventLed A scenes should not silently blank a valid Label.
## See also
- [`event-audit.md`](event-audit.md) - full source inventory, MaxWatch classes, drowning policy
- [`event-audit.md`](event-audit.md) - sources, MaxWatch, A demotion set
- [`event-prose-audit.md`](event-prose-audit.md) - prose vs game call sites
- [`scoring-model.md`](scoring-model.md) - score weights

View file

@ -0,0 +1,381 @@
domain id strength camera game_call_site verdict fix_action done
decisions find_lover 62 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
decisions try_to_steal_money 68 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
decisions banish_unruly_clan_members 76 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
decisions kill_unruly_clan_members 82 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
decisions try_new_plot 78 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
decisions king_change_kingdom_culture 84 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
decisions king_change_kingdom_language 82 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
decisions king_change_kingdom_religion 84 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
decisions leader_change_city_culture 74 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
decisions leader_change_city_language 72 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
decisions leader_change_city_religion 74 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
decisions warrior_army_follow_leader 58 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
decisions warrior_train_with_dummy 42 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
decisions warrior_try_join_army_group 64 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
happiness death_family_member 78 Signal Actor accurate done
happiness death_lover 90 Signal Actor accurate done
happiness death_child 96 Signal Actor accurate done
happiness death_best_friend 80 Signal Actor accurate done
happiness got_robbed 56 Signal Actor bridge Context bag / bridge for related names done
happiness got_poked 30 Ambient Actor accurate done
happiness lost_fight 58 Signal Actor accurate done
happiness got_caught 20 Ambient HappinessLibrary asset only - no live caller (Ambient stub) prose_fix Updated catalog prose to match call timing/action done
happiness paid_tax 30 Ambient Actor accurate done
happiness just_ate 30 Ambient Actor prose_fix Updated catalog prose to match call timing/action done
happiness just_received_gift 52 Signal BehFinishTalk bridge Context bag / bridge for related names done
happiness just_gave_gift 52 Signal BehFinishTalk bridge Context bag / bridge for related names done
happiness just_pooped 30 Ambient Actor accurate done
happiness just_slept 30 Signal StatusLibrary accurate done
happiness had_bad_dream 40 Signal StatusLibrary accurate done
happiness had_good_dream 38 Signal StatusLibrary accurate done
happiness had_nightmare 44 Signal StatusLibrary accurate done
happiness slept_outside 34 Signal BehTrySleep accurate done
happiness just_kissed 62 Signal BehCheckForBabiesFromSexualReproduction bridge Context bag / bridge for related names done
happiness just_killed 70 Signal Actor bridge Context bag / bridge for related names done
happiness become_king 80 Signal Kingdom accurate done
happiness become_leader 72 Signal City accurate done
happiness just_won_war 52 Aggregate Actor accurate done
happiness just_made_peace 50 Aggregate Actor accurate done
happiness just_lost_war 55 Aggregate Actor accurate done
happiness was_conquered 58 Aggregate City.joinAnotherKingdom → forceUnits changeHappiness accurate done
happiness kingdom_fell_apart 60 Aggregate City.makeOwnKingdom(pFellApart) → forceUnits accurate done
happiness just_started_war 78 Aggregate WarManager accurate done
happiness just_rebelled 58 Aggregate City.makeOwnKingdom/joinAnotherKingdom → forceUnits accurate done
happiness fallen_in_love 74 Signal StatusLibrary fell_in_love action_on_receive (+ becomeLoversWith bridge) bridge Status receive + no-emotions PublishApplied bridge done
happiness just_had_child 76 Signal Actor accurate done
happiness just_read_book 34 Signal BehFinishReading accurate done
happiness just_played 32 Signal BehActorChangeHappiness(child play tasks) prose_fix Updated catalog prose to match call timing/action done
happiness just_talked 34 Signal BehFinishTalk bridge Context bag / bridge for related names done
happiness just_laughed 34 Signal BehAddHappiness(happy_laughing end) prose_fix Updated catalog prose to match call timing/action done
happiness just_sang 34 Signal BehAddHappiness(singing end) accurate done
happiness just_swore 32 Signal BehAddHappiness(swearing end) prose_fix Updated catalog prose to match call timing/action done
happiness just_cried 42 Signal BehAddHappiness(crying end) accurate done
happiness just_talked_gossip 32 Signal BehFinishTalk bridge Context bag / bridge for related names done
happiness just_surprised 36 Signal StatusLibrary accurate done
happiness just_born 68 Signal Actor accurate done
happiness just_magnetised 48 Signal StatusLibrary accurate done
happiness just_forced_power 58 Signal MapBox accurate done
happiness just_possessed 62 Signal StatusLibrary accurate done
happiness strange_urge 46 Signal StatusLibrary accurate done
happiness just_had_tantrum 50 Signal StatusLibrary accurate done
happiness just_felt_the_divine 56 Signal PowerLibrary prose_fix Updated catalog prose to match call timing/action done
happiness just_enchanted 54 Signal StatusLibrary accurate done
happiness just_inspired 52 Signal StatusLibrary accurate done
happiness wrote_book 62 Signal BookManager accurate done
happiness just_became_adult 70 Signal Actor accurate done
happiness just_got_out_of_egg 72 Signal Actor accurate done
happiness just_finished_plot 70 Signal Plot accurate done
happiness just_found_house 58 Signal BehFindHouse accurate done
happiness just_lost_house 68 Signal Actor accurate done
happiness just_made_friend 58 Signal Actor accurate done
happiness just_injured 60 Signal Actor accurate done
happiness just_cursed 60 Signal StatusLibrary accurate done
happiness starving 50 Signal StatusLibrary accurate done
happiness conquered_city 70 Aggregate Kingdom accurate done
happiness destroyed_city 62 Aggregate Kingdom accurate done
happiness lost_crown 72 Signal Kingdom accurate done
happiness razed_capital 68 Aggregate Kingdom accurate done
happiness lost_capital 64 Aggregate Kingdom accurate done
happiness razed_city 60 Aggregate Kingdom accurate done
happiness lost_city 66 Aggregate Kingdom accurate done
happiness become_alpha 74 Signal Family accurate done
status afterglow 20 False BehCheckForBabiesFromSexualReproduction accurate done
status cough 20 False StatusLibrary / addNewStatusEffect accurate done
status dash 20 False CombatActionLibrary accurate done
status dodge 20 False CombatActionLibrary accurate done
status flicked 20 False God power / finger flick accurate done
status just_ate 20 False Actor consume / StatusLibrary accurate done
status on_guard 20 False StatusLibrary / addNewStatusEffect accurate done
status recovery_combat_action 20 False CombatActionLibrary cooldown accurate done
status recovery_plot 20 False Plot cooldown accurate done
status recovery_social 20 False Social cooldown accurate done
status recovery_spell 20 False Spell cooldown accurate done
status shield 20 False Combat / spell shield accurate done
status slowness 20 False StatusLibrary / addNewStatusEffect accurate done
status spell_boost 20 False Spell cast accurate done
status spell_silence 20 False Spell cast accurate done
status burning 72 True Fire / tile / combat accurate done
status possessed 74 True Possession convert / god power accurate done
status possessed_follower 60 True ControllableUnit.checkTalk convert target prose_fix Target becomes follower; not under sway done
status frozen 65 True Cold / freeze powers accurate done
status poisoned 58 True Poison attacks / tiles accurate done
status drowning 50 True Water drowning check accurate done
status stunned 50 True Combat stun / makeStunned accurate done
status rage 62 True Combat rage buff accurate done
status angry 48 True Actor.addAggro prose_fix Aggro anger; do not conflate with rage done
status cursed 60 True Curse powers / traits accurate done
status soul_harvested 78 True Soul harvest action accurate done
status voices_in_my_head 55 True StatusLibrary / addNewStatusEffect accurate done
status tantrum 52 True StatusLibrary; finish→just_had_tantrum accurate done
status magnetized 55 True Magnet powers accurate done
status invincible 50 True Invincibility powers accurate done
status powerup 54 True Powerup effects accurate done
status enchanted 52 True Enchant powers accurate done
status ash_fever 50 True Ash fever effect accurate done
status starving 56 True Hunger / nutrition accurate done
status surprised 42 True getSurprised / tryToGetSurprised accurate done
status confused 40 True StatusLibrary / addNewStatusEffect accurate done
status strange_urge 45 True StatusLibrary / addNewStatusEffect accurate done
status inspired 42 True StatusLibrary; finish→just_inspired accurate done
status motivated 40 True StatusLibrary / addNewStatusEffect accurate done
status fell_in_love 74 True becomeLoversWith / StatusLibrary receive accurate done
status pregnant 72 True Sexual reproduction; finish→makeBabyFromPregnancy prose_fix Loss = gives birth done
status pregnant_parthenogenesis 72 True BehCheckParthenogenesisReproduction; finish→baby prose_fix Loss = gives birth alone done
status crying 58 True Emotion tasks / StatusLibrary accurate done
status festive_spirit 36 True Festival / StatusLibrary accurate done
status laughing 34 True Emotion tasks / StatusLibrary accurate done
status singing 34 True Emotion tasks / StatusLibrary accurate done
status sleeping 32 True Sleep tasks; finish→dream pot accurate done
status handsome_migrant 44 True Migration status accurate done
status being_suspicious 40 True Steal / suspicious tasks accurate done
status budding 56 True BehCheckBuddingReproduction; finish→makeBabyViaBudding prose_fix Loss = bud offspring done
status caffeinated 36 True Consume caffeine accurate done
status had_bad_dream 40 True Sleep action_finish dream pot accurate done
status had_good_dream 38 True Sleep action_finish dream pot accurate done
status had_nightmare 48 True Sleep action_finish dream pot accurate done
status swearing 34 True Emotion tasks / possession swear accurate done
status taking_roots 54 True BehCheckVegetativeReproduction; finish→vegetative baby prose_fix Loss = sprouts offspring, not pull free done
status uprooting 54 True BabyMaker plant birth prose_fix Plant emerging/maturing, not generic uproot done
status egg 40 False Egg form; loss→InterestFeeds.EmitHatch accurate done
worldLog king_new 80 True WorldLogMessage.add; special1=kingdom special2=king accurate {b}=king {a}=kingdom matches kingReplacer done
worldLog king_left 72 True WorldLogMessage.add; special1=kingdom special2=king accurate {b}=king {a}=kingdom matches kingReplacer done
worldLog king_fled_capital 70 True WorldLogMessage.add; special1=kingdom special2=king accurate {b}=king {a}=kingdom matches kingReplacer done
worldLog king_fled_city 68 True WorldLogMessage.add; special1=kingdom special2=king accurate {b}=king {a}=kingdom matches kingReplacer done
worldLog king_dead 84 True WorldLogMessage.add; special1=kingdom special2=king accurate {b}=king {a}=kingdom matches kingReplacer done
worldLog king_killed 88 True WorldLogMessage.add; special1=kingdom special2=king accurate {b}=king {a}=kingdom matches kingReplacer done
worldLog favorite_dead 82 True WorldLogMessage.add accurate done
worldLog favorite_killed 85 True WorldLogMessage.add accurate done
worldLog city_new 72 True WorldLogMessage.add accurate done
worldLog log_city_revolted 90 True WorldLogMessage.add accurate done
worldLog city_destroyed 92 True WorldLogMessage.add accurate done
worldLog diplomacy_war_ended 72 True WorldLogMessage.add accurate done
worldLog diplomacy_war_started 100 True WorldLogMessage.add; special1/2=kingdoms accurate Also WarManager.newWar feed (dual path, distinct keys) done
worldLog total_war_started 100 True WorldLogMessage.add accurate done
worldLog alliance_new 70 True WorldLogMessage.add accurate done
worldLog alliance_dissolved 68 True WorldLogMessage.add accurate done
worldLog kingdom_new 75 True WorldLogMessage.add accurate done
worldLog kingdom_destroyed 100 True WorldLogMessage.add accurate done
worldLog kingdom_shattered 98 True WorldLogMessage.add accurate done
worldLog kingdom_fractured 95 True WorldLogMessage.add accurate done
worldLog kingdom_royal_clan_new 74 True WorldLogMessage.add accurate done
worldLog kingdom_royal_clan_changed 72 True WorldLogMessage.add accurate done
worldLog kingdom_royal_clan_dead 76 True WorldLogMessage.add accurate done
worldLog disaster_tornado 95 True WorldLogMessage.add (disaster→world_log) accurate done
worldLog disaster_meteorite 95 True WorldLogMessage.add (disaster→world_log) accurate done
worldLog disaster_hellspawn 96 True WorldLogMessage.add (disaster→world_log) accurate done
worldLog disaster_earthquake 95 True WorldLogMessage.add (disaster→world_log) accurate done
worldLog disaster_greg_abominations 96 True WorldLogMessage.add (disaster→world_log) accurate done
worldLog disaster_ice_ones 95 True WorldLogMessage.add (disaster→world_log) accurate done
worldLog disaster_sudden_snowman 90 True WorldLogMessage.add (disaster→world_log) accurate done
worldLog disaster_garden_surprise 90 True WorldLogMessage.add (disaster→world_log) accurate done
worldLog disaster_dragon_from_farlands 98 True WorldLogMessage.add (disaster→world_log) accurate done
worldLog disaster_bandits 92 True WorldLogMessage.add (disaster→world_log) accurate done
worldLog disaster_alien_invasion 97 True WorldLogMessage.add (disaster→world_log) accurate done
worldLog disaster_biomass 96 True WorldLogMessage.add (disaster→world_log) accurate done
worldLog disaster_tumor 96 True WorldLogMessage.add (disaster→world_log) accurate done
worldLog disaster_heatwave 88 True WorldLogMessage.add (disaster→world_log) accurate done
worldLog disaster_evil_mage 94 True WorldLogMessage.add (disaster→world_log) accurate done
worldLog disaster_underground_necromancer 95 True WorldLogMessage.add (disaster→world_log) accurate done
worldLog disaster_mad_thoughts 90 True WorldLogMessage.add (disaster→world_log) accurate done
worldLog auto_tester 10 False WorldLogMessage.add accurate CreatesInterest=false harness-only done
plots rebellion 92 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
plots new_war 94 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
plots alliance_create 80 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
plots alliance_join 68 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
plots alliance_destroy 86 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
plots attacker_stop_war 78 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
plots new_book 52 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
plots new_language 60 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
plots new_religion 70 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
plots new_culture 68 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
plots clan_ascension 82 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
plots culture_divide 75 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
plots religion_schism 84 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
plots language_divergence 58 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
plots summon_meteor_rain 96 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
plots summon_earthquake 95 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
plots summon_thunderstorm 93 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
plots summon_stormfront 93 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
plots summon_hellstorm 97 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
plots summon_demons 96 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
plots summon_angles 96 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
plots summon_skeletons 92 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
plots summon_living_plants 90 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
plots big_cast_coffee 48 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
plots big_cast_bubble_shield 64 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
plots big_cast_madness 80 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
plots big_cast_slowness 70 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
plots cause_rebellion 90 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
relationship set_lover 78 True Actor.setLover accurate done
relationship clear_lover 60 True setLover(null)+MapBox dead-lover cleanup hook_retarget done
relationship add_child 74 True Actor.setParent1/setParent2 hook_retarget done
relationship new_family 72 True FamilyManager.newFamily accurate done
relationship family_removed 58 True FamilyManager.removeObject accurate Confirmed patch + EventReason prose done
relationship find_lover 52 True BehFindLover Continue && !hasLover gate done
relationship become_alpha 74 True Family.setAlpha accurate done
relationship family_group_new 58 True BehFamilyGroupNew; shares new_family key merge done
relationship family_group_join 36 True BehFamilyGroupJoin accurate Confirmed patch + EventReason prose done
relationship family_group_leave 36 True BehFamilyGroupLeave accurate Confirmed patch + EventReason prose done
relationship baby_created 72 True ActorManager.createBabyActorFromData accurate done
traits zombie 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits wise 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits whirlwind 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits weightless 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits weak 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits veteran 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits venomous 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits unlucky 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits ugly 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits tumor_infection 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits tough 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits titan_lungs 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits tiny 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits thorns 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits thief 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits super_health 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits sunblessed 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits stupid 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits strong_minded 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits strong 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits soft_skin 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits slow 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits skin_burns 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits short_sighted 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits shiny 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits scar_of_divinity 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits savage 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits regeneration 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits pyromaniac 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits psychopath 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits poisonous 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits poison_immune 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits plague 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits peaceful 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits paranoid 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits pacifist 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits nightchild 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits mute 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits mush_spores 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits moonchild 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits miracle_born 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits miracle_bearer 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits miner 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits metamorphed 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits mega_heartbeat 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits mageslayer 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits madness 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits lustful 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits lucky 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits long_liver 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits light_lamp 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits kingslayer 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits infertile 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits infected 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits immune 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits immortal 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits hotheaded 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits honest 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits heliophobia 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits heart_of_wizard 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits healing_aura 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits hard_skin 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits greedy 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits golden_tooth 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits gluttonous 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits giant 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits genius 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits freeze_proof 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits fragile_health 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits flower_prints 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits flesh_eater 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits fire_proof 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits fire_blood 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits fertile 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits fat 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits fast 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits eyepatch 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits evil 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits energized 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits eagle_eyed 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits dragonslayer 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits dodge 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits desire_harp 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits desire_golden_egg 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits desire_computer 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits desire_alien_mold 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits deflect_projectile 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits deceitful 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits death_nuke 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits death_mark 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits death_bomb 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits dash 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits crippled 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits content 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits contagious 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits cold_aura 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits clumsy 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits clone 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits chosen_one 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits burning_feet 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits bubble_defense 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits boosted_vitality 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits bomberman 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits boat 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits bloodlust 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits block 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits blessed 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits battle_reflexes 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits backstep 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits attractive 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits arcane_reflexes 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits ambitious 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits agile 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits acid_touch 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits acid_proof 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
traits acid_blood 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
books family_story 52 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
books love_story 56 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
books friendship_story 50 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
books bad_story_about_king 64 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
books fable 48 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
books warfare_manual 58 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
books economy_manual 48 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
books stewardship_manual 48 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
books diplomacy_manual 55 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
books mathbook 45 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
books biology_book 45 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
books history_book 55 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
eras age_hope 78 True WorldAgeManager.startNextAge → MetaInterestFeed.EmitEra accurate done
eras age_sun 80 True WorldAgeManager.startNextAge → MetaInterestFeed.EmitEra accurate done
eras age_dark 88 True WorldAgeManager.startNextAge → MetaInterestFeed.EmitEra accurate done
eras age_tears 86 True WorldAgeManager.startNextAge → MetaInterestFeed.EmitEra accurate done
eras age_moon 82 True WorldAgeManager.startNextAge → MetaInterestFeed.EmitEra accurate done
eras age_chaos 92 True WorldAgeManager.startNextAge → MetaInterestFeed.EmitEra accurate done
eras age_wonders 84 True WorldAgeManager.startNextAge → MetaInterestFeed.EmitEra accurate done
eras age_ice 87 True WorldAgeManager.startNextAge → MetaInterestFeed.EmitEra accurate done
eras age_ash 90 True WorldAgeManager.startNextAge → MetaInterestFeed.EmitEra accurate done
eras age_despair 91 True WorldAgeManager.startNextAge → MetaInterestFeed.EmitEra accurate done
eras age_unknown 70 True WorldAgeManager.startNextAge → MetaInterestFeed.EmitEra accurate done
disasters tornado 95 True Policy→WorldLog disaster_tornado; no DisasterManager patch accurate Camera via paired worldLog row done
disasters heatwave 88 True Policy→WorldLog disaster_heatwave; no DisasterManager patch accurate Camera via paired worldLog row done
disasters small_meteorite 95 True Policy→WorldLog disaster_meteorite; no DisasterManager patch accurate Camera via paired worldLog row done
disasters small_earthquake 95 True Policy→WorldLog disaster_earthquake; no DisasterManager patch accurate Camera via paired worldLog row done
disasters hellspawn 96 True Policy→WorldLog disaster_hellspawn; no DisasterManager patch accurate Camera via paired worldLog row done
disasters ice_ones_awoken 95 True Policy→WorldLog disaster_ice_ones; no DisasterManager patch accurate Camera via paired worldLog row done
disasters sudden_snowman 90 True Policy→WorldLog disaster_sudden_snowman; no DisasterManager patch accurate Camera via paired worldLog row done
disasters garden_surprise 90 True Policy→WorldLog disaster_garden_surprise; no DisasterManager patch accurate Camera via paired worldLog row done
disasters dragon_from_farlands 98 True Policy→WorldLog disaster_dragon_from_farlands; no DisasterManager patch accurate Camera via paired worldLog row done
disasters ash_bandits 92 True Policy→WorldLog disaster_bandits; no DisasterManager patch accurate Camera via paired worldLog row done
disasters alien_invasion 97 True Policy→WorldLog disaster_alien_invasion; no DisasterManager patch accurate Camera via paired worldLog row done
disasters biomass 96 True Policy→WorldLog disaster_biomass; no DisasterManager patch accurate Camera via paired worldLog row done
disasters tumor 96 True Policy→WorldLog disaster_tumor; no DisasterManager patch accurate Camera via paired worldLog row done
disasters wild_mage 94 True Policy→WorldLog disaster_evil_mage; no DisasterManager patch accurate Camera via paired worldLog row done
disasters underground_necromancer 95 True Policy→WorldLog disaster_underground_necromancer; no DisasterManager patch accurate Camera via paired worldLog row done
disasters mad_thoughts 90 True Policy→WorldLog disaster_mad_thoughts; no DisasterManager patch accurate Camera via paired worldLog row done
disasters greg_abominations 96 True Policy→WorldLog disaster_greg_abominations; no DisasterManager patch accurate Camera via paired worldLog row done
warTypes normal 90 True WarManager.newWar/endWar + WarInterestFeed.Tick accurate Live label often Attacker vs Defender done
warTypes spite 88 True WarManager.newWar/endWar + WarInterestFeed.Tick accurate Live label often Attacker vs Defender done
warTypes inspire 86 True WarManager.newWar/endWar + WarInterestFeed.Tick accurate Live label often Attacker vs Defender done
warTypes rebellion 92 True WarManager.newWar/endWar + WarInterestFeed.Tick accurate Live label often Attacker vs Defender done
warTypes whisper_of_war 84 True WarManager.newWar/endWar + WarInterestFeed.Tick accurate Live label often Attacker vs Defender done
1 domain id strength camera game_call_site verdict fix_action done
2 decisions find_lover 62 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
3 decisions try_to_steal_money 68 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
4 decisions banish_unruly_clan_members 76 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
5 decisions kill_unruly_clan_members 82 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
6 decisions try_new_plot 78 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
7 decisions king_change_kingdom_culture 84 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
8 decisions king_change_kingdom_language 82 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
9 decisions king_change_kingdom_religion 84 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
10 decisions leader_change_city_culture 74 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
11 decisions leader_change_city_language 72 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
12 decisions leader_change_city_religion 74 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
13 decisions warrior_army_follow_leader 58 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
14 decisions warrior_train_with_dummy 42 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
15 decisions warrior_try_join_army_group 64 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
16 happiness death_family_member 78 Signal Actor accurate done
17 happiness death_lover 90 Signal Actor accurate done
18 happiness death_child 96 Signal Actor accurate done
19 happiness death_best_friend 80 Signal Actor accurate done
20 happiness got_robbed 56 Signal Actor bridge Context bag / bridge for related names done
21 happiness got_poked 30 Ambient Actor accurate done
22 happiness lost_fight 58 Signal Actor accurate done
23 happiness got_caught 20 Ambient HappinessLibrary asset only - no live caller (Ambient stub) prose_fix Updated catalog prose to match call timing/action done
24 happiness paid_tax 30 Ambient Actor accurate done
25 happiness just_ate 30 Ambient Actor prose_fix Updated catalog prose to match call timing/action done
26 happiness just_received_gift 52 Signal BehFinishTalk bridge Context bag / bridge for related names done
27 happiness just_gave_gift 52 Signal BehFinishTalk bridge Context bag / bridge for related names done
28 happiness just_pooped 30 Ambient Actor accurate done
29 happiness just_slept 30 Signal StatusLibrary accurate done
30 happiness had_bad_dream 40 Signal StatusLibrary accurate done
31 happiness had_good_dream 38 Signal StatusLibrary accurate done
32 happiness had_nightmare 44 Signal StatusLibrary accurate done
33 happiness slept_outside 34 Signal BehTrySleep accurate done
34 happiness just_kissed 62 Signal BehCheckForBabiesFromSexualReproduction bridge Context bag / bridge for related names done
35 happiness just_killed 70 Signal Actor bridge Context bag / bridge for related names done
36 happiness become_king 80 Signal Kingdom accurate done
37 happiness become_leader 72 Signal City accurate done
38 happiness just_won_war 52 Aggregate Actor accurate done
39 happiness just_made_peace 50 Aggregate Actor accurate done
40 happiness just_lost_war 55 Aggregate Actor accurate done
41 happiness was_conquered 58 Aggregate City.joinAnotherKingdom → forceUnits changeHappiness accurate done
42 happiness kingdom_fell_apart 60 Aggregate City.makeOwnKingdom(pFellApart) → forceUnits accurate done
43 happiness just_started_war 78 Aggregate WarManager accurate done
44 happiness just_rebelled 58 Aggregate City.makeOwnKingdom/joinAnotherKingdom → forceUnits accurate done
45 happiness fallen_in_love 74 Signal StatusLibrary fell_in_love action_on_receive (+ becomeLoversWith bridge) bridge Status receive + no-emotions PublishApplied bridge done
46 happiness just_had_child 76 Signal Actor accurate done
47 happiness just_read_book 34 Signal BehFinishReading accurate done
48 happiness just_played 32 Signal BehActorChangeHappiness(child play tasks) prose_fix Updated catalog prose to match call timing/action done
49 happiness just_talked 34 Signal BehFinishTalk bridge Context bag / bridge for related names done
50 happiness just_laughed 34 Signal BehAddHappiness(happy_laughing end) prose_fix Updated catalog prose to match call timing/action done
51 happiness just_sang 34 Signal BehAddHappiness(singing end) accurate done
52 happiness just_swore 32 Signal BehAddHappiness(swearing end) prose_fix Updated catalog prose to match call timing/action done
53 happiness just_cried 42 Signal BehAddHappiness(crying end) accurate done
54 happiness just_talked_gossip 32 Signal BehFinishTalk bridge Context bag / bridge for related names done
55 happiness just_surprised 36 Signal StatusLibrary accurate done
56 happiness just_born 68 Signal Actor accurate done
57 happiness just_magnetised 48 Signal StatusLibrary accurate done
58 happiness just_forced_power 58 Signal MapBox accurate done
59 happiness just_possessed 62 Signal StatusLibrary accurate done
60 happiness strange_urge 46 Signal StatusLibrary accurate done
61 happiness just_had_tantrum 50 Signal StatusLibrary accurate done
62 happiness just_felt_the_divine 56 Signal PowerLibrary prose_fix Updated catalog prose to match call timing/action done
63 happiness just_enchanted 54 Signal StatusLibrary accurate done
64 happiness just_inspired 52 Signal StatusLibrary accurate done
65 happiness wrote_book 62 Signal BookManager accurate done
66 happiness just_became_adult 70 Signal Actor accurate done
67 happiness just_got_out_of_egg 72 Signal Actor accurate done
68 happiness just_finished_plot 70 Signal Plot accurate done
69 happiness just_found_house 58 Signal BehFindHouse accurate done
70 happiness just_lost_house 68 Signal Actor accurate done
71 happiness just_made_friend 58 Signal Actor accurate done
72 happiness just_injured 60 Signal Actor accurate done
73 happiness just_cursed 60 Signal StatusLibrary accurate done
74 happiness starving 50 Signal StatusLibrary accurate done
75 happiness conquered_city 70 Aggregate Kingdom accurate done
76 happiness destroyed_city 62 Aggregate Kingdom accurate done
77 happiness lost_crown 72 Signal Kingdom accurate done
78 happiness razed_capital 68 Aggregate Kingdom accurate done
79 happiness lost_capital 64 Aggregate Kingdom accurate done
80 happiness razed_city 60 Aggregate Kingdom accurate done
81 happiness lost_city 66 Aggregate Kingdom accurate done
82 happiness become_alpha 74 Signal Family accurate done
83 status afterglow 20 False BehCheckForBabiesFromSexualReproduction accurate done
84 status cough 20 False StatusLibrary / addNewStatusEffect accurate done
85 status dash 20 False CombatActionLibrary accurate done
86 status dodge 20 False CombatActionLibrary accurate done
87 status flicked 20 False God power / finger flick accurate done
88 status just_ate 20 False Actor consume / StatusLibrary accurate done
89 status on_guard 20 False StatusLibrary / addNewStatusEffect accurate done
90 status recovery_combat_action 20 False CombatActionLibrary cooldown accurate done
91 status recovery_plot 20 False Plot cooldown accurate done
92 status recovery_social 20 False Social cooldown accurate done
93 status recovery_spell 20 False Spell cooldown accurate done
94 status shield 20 False Combat / spell shield accurate done
95 status slowness 20 False StatusLibrary / addNewStatusEffect accurate done
96 status spell_boost 20 False Spell cast accurate done
97 status spell_silence 20 False Spell cast accurate done
98 status burning 72 True Fire / tile / combat accurate done
99 status possessed 74 True Possession convert / god power accurate done
100 status possessed_follower 60 True ControllableUnit.checkTalk convert target prose_fix Target becomes follower; not under sway done
101 status frozen 65 True Cold / freeze powers accurate done
102 status poisoned 58 True Poison attacks / tiles accurate done
103 status drowning 50 True Water drowning check accurate done
104 status stunned 50 True Combat stun / makeStunned accurate done
105 status rage 62 True Combat rage buff accurate done
106 status angry 48 True Actor.addAggro prose_fix Aggro anger; do not conflate with rage done
107 status cursed 60 True Curse powers / traits accurate done
108 status soul_harvested 78 True Soul harvest action accurate done
109 status voices_in_my_head 55 True StatusLibrary / addNewStatusEffect accurate done
110 status tantrum 52 True StatusLibrary; finish→just_had_tantrum accurate done
111 status magnetized 55 True Magnet powers accurate done
112 status invincible 50 True Invincibility powers accurate done
113 status powerup 54 True Powerup effects accurate done
114 status enchanted 52 True Enchant powers accurate done
115 status ash_fever 50 True Ash fever effect accurate done
116 status starving 56 True Hunger / nutrition accurate done
117 status surprised 42 True getSurprised / tryToGetSurprised accurate done
118 status confused 40 True StatusLibrary / addNewStatusEffect accurate done
119 status strange_urge 45 True StatusLibrary / addNewStatusEffect accurate done
120 status inspired 42 True StatusLibrary; finish→just_inspired accurate done
121 status motivated 40 True StatusLibrary / addNewStatusEffect accurate done
122 status fell_in_love 74 True becomeLoversWith / StatusLibrary receive accurate done
123 status pregnant 72 True Sexual reproduction; finish→makeBabyFromPregnancy prose_fix Loss = gives birth done
124 status pregnant_parthenogenesis 72 True BehCheckParthenogenesisReproduction; finish→baby prose_fix Loss = gives birth alone done
125 status crying 58 True Emotion tasks / StatusLibrary accurate done
126 status festive_spirit 36 True Festival / StatusLibrary accurate done
127 status laughing 34 True Emotion tasks / StatusLibrary accurate done
128 status singing 34 True Emotion tasks / StatusLibrary accurate done
129 status sleeping 32 True Sleep tasks; finish→dream pot accurate done
130 status handsome_migrant 44 True Migration status accurate done
131 status being_suspicious 40 True Steal / suspicious tasks accurate done
132 status budding 56 True BehCheckBuddingReproduction; finish→makeBabyViaBudding prose_fix Loss = bud offspring done
133 status caffeinated 36 True Consume caffeine accurate done
134 status had_bad_dream 40 True Sleep action_finish dream pot accurate done
135 status had_good_dream 38 True Sleep action_finish dream pot accurate done
136 status had_nightmare 48 True Sleep action_finish dream pot accurate done
137 status swearing 34 True Emotion tasks / possession swear accurate done
138 status taking_roots 54 True BehCheckVegetativeReproduction; finish→vegetative baby prose_fix Loss = sprouts offspring, not pull free done
139 status uprooting 54 True BabyMaker plant birth prose_fix Plant emerging/maturing, not generic uproot done
140 status egg 40 False Egg form; loss→InterestFeeds.EmitHatch accurate done
141 worldLog king_new 80 True WorldLogMessage.add; special1=kingdom special2=king accurate {b}=king {a}=kingdom matches kingReplacer done
142 worldLog king_left 72 True WorldLogMessage.add; special1=kingdom special2=king accurate {b}=king {a}=kingdom matches kingReplacer done
143 worldLog king_fled_capital 70 True WorldLogMessage.add; special1=kingdom special2=king accurate {b}=king {a}=kingdom matches kingReplacer done
144 worldLog king_fled_city 68 True WorldLogMessage.add; special1=kingdom special2=king accurate {b}=king {a}=kingdom matches kingReplacer done
145 worldLog king_dead 84 True WorldLogMessage.add; special1=kingdom special2=king accurate {b}=king {a}=kingdom matches kingReplacer done
146 worldLog king_killed 88 True WorldLogMessage.add; special1=kingdom special2=king accurate {b}=king {a}=kingdom matches kingReplacer done
147 worldLog favorite_dead 82 True WorldLogMessage.add accurate done
148 worldLog favorite_killed 85 True WorldLogMessage.add accurate done
149 worldLog city_new 72 True WorldLogMessage.add accurate done
150 worldLog log_city_revolted 90 True WorldLogMessage.add accurate done
151 worldLog city_destroyed 92 True WorldLogMessage.add accurate done
152 worldLog diplomacy_war_ended 72 True WorldLogMessage.add accurate done
153 worldLog diplomacy_war_started 100 True WorldLogMessage.add; special1/2=kingdoms accurate Also WarManager.newWar feed (dual path, distinct keys) done
154 worldLog total_war_started 100 True WorldLogMessage.add accurate done
155 worldLog alliance_new 70 True WorldLogMessage.add accurate done
156 worldLog alliance_dissolved 68 True WorldLogMessage.add accurate done
157 worldLog kingdom_new 75 True WorldLogMessage.add accurate done
158 worldLog kingdom_destroyed 100 True WorldLogMessage.add accurate done
159 worldLog kingdom_shattered 98 True WorldLogMessage.add accurate done
160 worldLog kingdom_fractured 95 True WorldLogMessage.add accurate done
161 worldLog kingdom_royal_clan_new 74 True WorldLogMessage.add accurate done
162 worldLog kingdom_royal_clan_changed 72 True WorldLogMessage.add accurate done
163 worldLog kingdom_royal_clan_dead 76 True WorldLogMessage.add accurate done
164 worldLog disaster_tornado 95 True WorldLogMessage.add (disaster→world_log) accurate done
165 worldLog disaster_meteorite 95 True WorldLogMessage.add (disaster→world_log) accurate done
166 worldLog disaster_hellspawn 96 True WorldLogMessage.add (disaster→world_log) accurate done
167 worldLog disaster_earthquake 95 True WorldLogMessage.add (disaster→world_log) accurate done
168 worldLog disaster_greg_abominations 96 True WorldLogMessage.add (disaster→world_log) accurate done
169 worldLog disaster_ice_ones 95 True WorldLogMessage.add (disaster→world_log) accurate done
170 worldLog disaster_sudden_snowman 90 True WorldLogMessage.add (disaster→world_log) accurate done
171 worldLog disaster_garden_surprise 90 True WorldLogMessage.add (disaster→world_log) accurate done
172 worldLog disaster_dragon_from_farlands 98 True WorldLogMessage.add (disaster→world_log) accurate done
173 worldLog disaster_bandits 92 True WorldLogMessage.add (disaster→world_log) accurate done
174 worldLog disaster_alien_invasion 97 True WorldLogMessage.add (disaster→world_log) accurate done
175 worldLog disaster_biomass 96 True WorldLogMessage.add (disaster→world_log) accurate done
176 worldLog disaster_tumor 96 True WorldLogMessage.add (disaster→world_log) accurate done
177 worldLog disaster_heatwave 88 True WorldLogMessage.add (disaster→world_log) accurate done
178 worldLog disaster_evil_mage 94 True WorldLogMessage.add (disaster→world_log) accurate done
179 worldLog disaster_underground_necromancer 95 True WorldLogMessage.add (disaster→world_log) accurate done
180 worldLog disaster_mad_thoughts 90 True WorldLogMessage.add (disaster→world_log) accurate done
181 worldLog auto_tester 10 False WorldLogMessage.add accurate CreatesInterest=false harness-only done
182 plots rebellion 92 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
183 plots new_war 94 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
184 plots alliance_create 80 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
185 plots alliance_join 68 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
186 plots alliance_destroy 86 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
187 plots attacker_stop_war 78 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
188 plots new_book 52 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
189 plots new_language 60 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
190 plots new_religion 70 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
191 plots new_culture 68 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
192 plots clan_ascension 82 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
193 plots culture_divide 75 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
194 plots religion_schism 84 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
195 plots language_divergence 58 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
196 plots summon_meteor_rain 96 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
197 plots summon_earthquake 95 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
198 plots summon_thunderstorm 93 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
199 plots summon_stormfront 93 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
200 plots summon_hellstorm 97 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
201 plots summon_demons 96 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
202 plots summon_angles 96 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
203 plots summon_skeletons 92 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
204 plots summon_living_plants 90 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
205 plots big_cast_coffee 48 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
206 plots big_cast_bubble_shield 64 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
207 plots big_cast_madness 80 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
208 plots big_cast_slowness 70 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
209 plots cause_rebellion 90 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
210 relationship set_lover 78 True Actor.setLover accurate done
211 relationship clear_lover 60 True setLover(null)+MapBox dead-lover cleanup hook_retarget done
212 relationship add_child 74 True Actor.setParent1/setParent2 hook_retarget done
213 relationship new_family 72 True FamilyManager.newFamily accurate done
214 relationship family_removed 58 True FamilyManager.removeObject accurate Confirmed patch + EventReason prose done
215 relationship find_lover 52 True BehFindLover Continue && !hasLover gate done
216 relationship become_alpha 74 True Family.setAlpha accurate done
217 relationship family_group_new 58 True BehFamilyGroupNew; shares new_family key merge done
218 relationship family_group_join 36 True BehFamilyGroupJoin accurate Confirmed patch + EventReason prose done
219 relationship family_group_leave 36 True BehFamilyGroupLeave accurate Confirmed patch + EventReason prose done
220 relationship baby_created 72 True ActorManager.createBabyActorFromData accurate done
221 traits zombie 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
222 traits wise 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
223 traits whirlwind 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
224 traits weightless 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
225 traits weak 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
226 traits veteran 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
227 traits venomous 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
228 traits unlucky 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
229 traits ugly 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
230 traits tumor_infection 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
231 traits tough 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
232 traits titan_lungs 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
233 traits tiny 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
234 traits thorns 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
235 traits thief 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
236 traits super_health 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
237 traits sunblessed 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
238 traits stupid 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
239 traits strong_minded 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
240 traits strong 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
241 traits soft_skin 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
242 traits slow 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
243 traits skin_burns 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
244 traits short_sighted 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
245 traits shiny 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
246 traits scar_of_divinity 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
247 traits savage 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
248 traits regeneration 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
249 traits pyromaniac 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
250 traits psychopath 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
251 traits poisonous 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
252 traits poison_immune 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
253 traits plague 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
254 traits peaceful 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
255 traits paranoid 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
256 traits pacifist 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
257 traits nightchild 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
258 traits mute 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
259 traits mush_spores 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
260 traits moonchild 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
261 traits miracle_born 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
262 traits miracle_bearer 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
263 traits miner 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
264 traits metamorphed 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
265 traits mega_heartbeat 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
266 traits mageslayer 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
267 traits madness 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
268 traits lustful 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
269 traits lucky 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
270 traits long_liver 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
271 traits light_lamp 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
272 traits kingslayer 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
273 traits infertile 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
274 traits infected 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
275 traits immune 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
276 traits immortal 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
277 traits hotheaded 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
278 traits honest 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
279 traits heliophobia 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
280 traits heart_of_wizard 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
281 traits healing_aura 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
282 traits hard_skin 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
283 traits greedy 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
284 traits golden_tooth 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
285 traits gluttonous 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
286 traits giant 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
287 traits genius 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
288 traits freeze_proof 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
289 traits fragile_health 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
290 traits flower_prints 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
291 traits flesh_eater 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
292 traits fire_proof 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
293 traits fire_blood 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
294 traits fertile 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
295 traits fat 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
296 traits fast 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
297 traits eyepatch 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
298 traits evil 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
299 traits energized 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
300 traits eagle_eyed 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
301 traits dragonslayer 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
302 traits dodge 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
303 traits desire_harp 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
304 traits desire_golden_egg 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
305 traits desire_computer 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
306 traits desire_alien_mold 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
307 traits deflect_projectile 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
308 traits deceitful 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
309 traits death_nuke 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
310 traits death_mark 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
311 traits death_bomb 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
312 traits dash 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
313 traits crippled 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
314 traits content 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
315 traits contagious 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
316 traits cold_aura 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
317 traits clumsy 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
318 traits clone 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
319 traits chosen_one 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
320 traits burning_feet 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
321 traits bubble_defense 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
322 traits boosted_vitality 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
323 traits bomberman 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
324 traits boat 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
325 traits bloodlust 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
326 traits block 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
327 traits blessed 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
328 traits battle_reflexes 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
329 traits backstep 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
330 traits attractive 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
331 traits arcane_reflexes 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
332 traits ambitious 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
333 traits agile 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
334 traits acid_touch 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
335 traits acid_proof 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
336 traits acid_blood 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
337 books family_story 52 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
338 books love_story 56 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
339 books friendship_story 50 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
340 books bad_story_about_king 64 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
341 books fable 48 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
342 books warfare_manual 58 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
343 books economy_manual 48 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
344 books stewardship_manual 48 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
345 books diplomacy_manual 55 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
346 books mathbook 45 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
347 books biology_book 45 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
348 books history_book 55 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
349 eras age_hope 78 True WorldAgeManager.startNextAge → MetaInterestFeed.EmitEra accurate done
350 eras age_sun 80 True WorldAgeManager.startNextAge → MetaInterestFeed.EmitEra accurate done
351 eras age_dark 88 True WorldAgeManager.startNextAge → MetaInterestFeed.EmitEra accurate done
352 eras age_tears 86 True WorldAgeManager.startNextAge → MetaInterestFeed.EmitEra accurate done
353 eras age_moon 82 True WorldAgeManager.startNextAge → MetaInterestFeed.EmitEra accurate done
354 eras age_chaos 92 True WorldAgeManager.startNextAge → MetaInterestFeed.EmitEra accurate done
355 eras age_wonders 84 True WorldAgeManager.startNextAge → MetaInterestFeed.EmitEra accurate done
356 eras age_ice 87 True WorldAgeManager.startNextAge → MetaInterestFeed.EmitEra accurate done
357 eras age_ash 90 True WorldAgeManager.startNextAge → MetaInterestFeed.EmitEra accurate done
358 eras age_despair 91 True WorldAgeManager.startNextAge → MetaInterestFeed.EmitEra accurate done
359 eras age_unknown 70 True WorldAgeManager.startNextAge → MetaInterestFeed.EmitEra accurate done
360 disasters tornado 95 True Policy→WorldLog disaster_tornado; no DisasterManager patch accurate Camera via paired worldLog row done
361 disasters heatwave 88 True Policy→WorldLog disaster_heatwave; no DisasterManager patch accurate Camera via paired worldLog row done
362 disasters small_meteorite 95 True Policy→WorldLog disaster_meteorite; no DisasterManager patch accurate Camera via paired worldLog row done
363 disasters small_earthquake 95 True Policy→WorldLog disaster_earthquake; no DisasterManager patch accurate Camera via paired worldLog row done
364 disasters hellspawn 96 True Policy→WorldLog disaster_hellspawn; no DisasterManager patch accurate Camera via paired worldLog row done
365 disasters ice_ones_awoken 95 True Policy→WorldLog disaster_ice_ones; no DisasterManager patch accurate Camera via paired worldLog row done
366 disasters sudden_snowman 90 True Policy→WorldLog disaster_sudden_snowman; no DisasterManager patch accurate Camera via paired worldLog row done
367 disasters garden_surprise 90 True Policy→WorldLog disaster_garden_surprise; no DisasterManager patch accurate Camera via paired worldLog row done
368 disasters dragon_from_farlands 98 True Policy→WorldLog disaster_dragon_from_farlands; no DisasterManager patch accurate Camera via paired worldLog row done
369 disasters ash_bandits 92 True Policy→WorldLog disaster_bandits; no DisasterManager patch accurate Camera via paired worldLog row done
370 disasters alien_invasion 97 True Policy→WorldLog disaster_alien_invasion; no DisasterManager patch accurate Camera via paired worldLog row done
371 disasters biomass 96 True Policy→WorldLog disaster_biomass; no DisasterManager patch accurate Camera via paired worldLog row done
372 disasters tumor 96 True Policy→WorldLog disaster_tumor; no DisasterManager patch accurate Camera via paired worldLog row done
373 disasters wild_mage 94 True Policy→WorldLog disaster_evil_mage; no DisasterManager patch accurate Camera via paired worldLog row done
374 disasters underground_necromancer 95 True Policy→WorldLog disaster_underground_necromancer; no DisasterManager patch accurate Camera via paired worldLog row done
375 disasters mad_thoughts 90 True Policy→WorldLog disaster_mad_thoughts; no DisasterManager patch accurate Camera via paired worldLog row done
376 disasters greg_abominations 96 True Policy→WorldLog disaster_greg_abominations; no DisasterManager patch accurate Camera via paired worldLog row done
377 warTypes normal 90 True WarManager.newWar/endWar + WarInterestFeed.Tick accurate Live label often Attacker vs Defender done
378 warTypes spite 88 True WarManager.newWar/endWar + WarInterestFeed.Tick accurate Live label often Attacker vs Defender done
379 warTypes inspire 86 True WarManager.newWar/endWar + WarInterestFeed.Tick accurate Live label often Attacker vs Defender done
380 warTypes rebellion 92 True WarManager.newWar/endWar + WarInterestFeed.Tick accurate Live label often Attacker vs Defender done
381 warTypes whisper_of_war 84 True WarManager.newWar/endWar + WarInterestFeed.Tick accurate Live label often Attacker vs Defender done

View file

@ -1,9 +1,17 @@
# Scoring model
**Source of truth (game loads this):** [`IdleSpectator/scoring-model.json`](../IdleSpectator/scoring-model.json)
**Global formula:** [`IdleSpectator/scoring-model.json`](../IdleSpectator/scoring-model.json)
**Per-event inventory (strength + prose):** [`IdleSpectator/event-catalog.json`](../IdleSpectator/event-catalog.json)
- `scoring-model.json` - margins, multipliers, rarity caps
- `event-catalog.json` - every authored event id (`happiness`, `status`, `worldLog`, `plots`, `decisions`, …)
- Decisions use `action` (clause after "decides to …")
- Most other domains use `label`
- Happiness uses `variantsWithRelated` / `variantsWithoutRelated`
- Optional `species` / `character` bonuses
**Visual tracker (open beside chat):** [scoring-model canvas](/home/dazed/.cursor/projects/home-dazed-Projects-worldbox-mods-idle-mode/canvases/scoring-model.canvas.tsx)
Edit the `weights` block in the JSON to change live scoring.
The canvas charts and calculator mirror those weights.
After changing the JSON, refresh the canvas `W` snapshot so the visual stays accurate.
Edit the `weights` block in scoring-model.json to change live scoring formula knobs.
Edit event-catalog.json to change what each event is worth and what it says.

View file

@ -0,0 +1,91 @@
# World event inventory (full surface)
Source of truth for what IdleSpectator covers across the **whole** game.
Refresh via live libraries + `mutation_discovery` / domain audits under `.harness/`.
Audit date: 2026-07-16.
## Policy
**If it is interesting, it is an interest candidate.**
`CreatesInterest` / Happiness `Signal` registers Layer A.
`EventStrength` + scoring decide who gets the spotlight.
Only true brief FX, cooldowns, civic Aggregates, and god-tool / biome spam stay B-only.
## How to read this
| Layer | Meaning |
|-------|---------|
| **B** | Activity / Life / chronicle prose may fire; no interest candidate |
| **A** | Registers interest; director ranks by points |
| **Gap** | Game fires a lifecycle beat; we have no reliable A path |
## Live libraries (enumerated)
| Library | ~Count | Owner |
|---------|-------:|-------|
| happiness_library | 67 | EventCatalog.Happiness |
| status | 58 | EventCatalog.Status |
| world_log_library | 41 | EventCatalog.WorldLog |
| disasters | 17 | via WorldLog |
| war_types_library | 5 | EventCatalog.WarType |
| traits | 116 | EventCatalog.Trait (all CI; strength ranks) |
| plots_library | 28 | EventCatalog.Plot |
| era_library | 11 | EventCatalog.Era |
| book_types | 12 | EventCatalog.Book (all A) |
| decisions / powers / spells / items | 127 / 339 / 12 / 111 | EventCatalog.Libraries |
| subspecies_traits / genes / phenotypes | 204 / 47 / 52 | Libraries |
| buildings | 618 | patches (consume/destroy A; damage skipped) |
| actor_library | 322 | SpeciesDiscovery / scoring |
| culture/religion/clan/language traits | 78 / 40 / 29 / 26 | **no event catalog yet** |
## Layer A sizes (policy)
| Domain | Authored | Layer A |
|--------|--------:|--------:|
| Happiness | 67 | ~49 Signal (5 Ambient FX; 13 Aggregate civic) |
| Status | 58 | ~42 CreatesInterest (FX/cooldowns + egg gain stay B) |
| WorldLog | 41 | ~40 |
| Relationship | 11 | all interesting discrete (seeker/pack at low strength) |
| Plot / Era / War / Book | 28 / 11 / 5 / 12 | all A |
| Trait | 116 | all CI (no noticeScoreMin hard drop) |
| Spell / Item / Gene / Phenotype / WorldLaw / SubspeciesTrait | live | ambient A at low strength; Signal bumps spectacle |
| Power / Decision / Biome | live | Signal predicate only (tools / idle AI / terrain) |
| Building / Boat | none | destroy/consume/trade/unload; damage/load skipped |
## Wired hooks (major)
Happiness, status (+ egg-loss hatch), WorldLog, wars, plots (new/cancel/join/leave/finish),
meta genesis (culture/religion/language/clan/army/alliance/city/kingdom),
books, traits, deferred libraries, boats (trade/unload), buildings (consume/destroy),
relationship (lover/child/family/baby/alpha), Family.setAlpha, SpeciesDiscovery.
## Known gaps / thin areas
| Gap | Why it matters | Status |
|-----|----------------|--------|
| Culture/religion/clan/language trait gain | Meta drama libraries exist | **Open** |
| Building complete / wonder finish | Settlement spectacle | **Open** |
| Alliance/kingdom destroy managers | End beats if WorldLog thin | **Open** |
| Era/earthquake poll (ongoing) | Age change is wired; poll not | **Open** |
## Stay B (true noise)
- Happiness: `got_poked`, `got_caught`, `paid_tax`, `just_ate`, `just_pooped` + all Aggregates
- Status: dash/dodge/shield/recovery_*/spell_*/cough/flicked/just_ate/on_guard/afterglow/slowness + egg gain
- Libraries: Power ambient (god paint tools), Decision ambient (idle AI; interesting decisions are Signal), Biome ambient
- Patches: `building_damage`, `boat_load`
## Scoring notes
`TotalScore = EventStrength + scale + CharacterSignificance * charWeight + visual + novelty`.
CharacterSignificance includes favorite/king/leader/renown/kills/level and species rarity.
## Refresh commands
```bash
./scripts/harness-run.sh --no-launch mutation_discovery
./scripts/harness-run.sh --no-launch domain_event_audit
./scripts/harness-run.sh --no-launch activity_happiness_audit
```

View file

@ -0,0 +1,843 @@
#!/usr/bin/env python3
"""Export IdleSpectator/event-catalog.json from EventCatalog C# sources.
Parses authored catalog partials under IdleSpectator/Events/Catalogs/ and merges
with existing decisions/species/character overlays from event-catalog.json
(JSON decisions strengths/actions win).
Usage (from repo root):
python3 scripts/export-event-catalog-from-cs.py
python3 scripts/export-event-catalog-from-cs.py --out IdleSpectator/event-catalog.json
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
from typing import Any
REPO = Path(__file__).resolve().parents[1]
CATALOGS = REPO / "IdleSpectator" / "Events" / "Catalogs"
DEFAULT_OUT = REPO / "IdleSpectator" / "event-catalog.json"
DEFAULT_EXISTING = DEFAULT_OUT
MOD_VERSION = "0.25.0"
UPDATED = "2026-07-16"
NOTE = (
"Authored IdleSpectator event catalog exported from EventCatalog C# sources "
"(happiness, status, worldLog, plots, relationship, traits, books, eras, "
"disasters, warTypes, libraries defaults). decisions/species/character merge "
"from prior JSON (JSON wins). Loaded by EventCatalogConfig; scoring-model.json "
"remains the global formula."
)
# ---------------------------------------------------------------------------
# C# text helpers
# ---------------------------------------------------------------------------
def strip_csharp_comments(text: str) -> str:
"""Remove // and /* */ comments outside of string literals."""
out: list[str] = []
i = 0
n = len(text)
while i < n:
c = text[i]
if c == '"':
j = i + 1
while j < n:
if text[j] == "\\":
j += 2
continue
if text[j] == '"':
j += 1
break
j += 1
out.append(text[i:j])
i = j
continue
if c == "/" and i + 1 < n:
nxt = text[i + 1]
if nxt == "/":
while i < n and text[i] not in "\r\n":
i += 1
continue
if nxt == "*":
i += 2
while i + 1 < n and not (text[i] == "*" and text[i + 1] == "/"):
i += 1
i = min(n, i + 2)
continue
out.append(c)
i += 1
return "".join(out)
def enum_leaf(expr: str) -> str:
"""HappinessPresentationTier.Signal -> Signal."""
s = expr.strip().rstrip(",")
if "." in s:
return s.rsplit(".", 1)[-1]
return s
def parse_csharp_string(token: str) -> str:
token = token.strip()
if len(token) >= 2 and token[0] == '"' and token[-1] == '"':
inner = token[1:-1]
return (
inner.replace(r"\\", "\0")
.replace(r"\"", '"')
.replace(r"\n", "\n")
.replace(r"\t", "\t")
.replace("\0", "\\")
)
return token
def parse_number(token: str) -> float | int:
t = token.strip().rstrip("fFdDmM")
if re.fullmatch(r"-?\d+", t):
return int(t)
return float(t)
def parse_bool(token: str) -> bool:
t = token.strip().lower()
if t == "true":
return True
if t == "false":
return False
raise ValueError(f"not a bool: {token!r}")
def split_top_level_args(arg_text: str) -> list[str]:
"""Split comma-separated C# call args, respecting strings/parens/braces."""
args: list[str] = []
buf: list[str] = []
depth_paren = depth_brace = depth_bracket = 0
in_string = False
escape = False
i = 0
while i < len(arg_text):
c = arg_text[i]
if in_string:
buf.append(c)
if escape:
escape = False
elif c == "\\":
escape = True
elif c == '"':
in_string = False
i += 1
continue
if c == '"':
in_string = True
buf.append(c)
i += 1
continue
if c == "(":
depth_paren += 1
buf.append(c)
elif c == ")":
depth_paren -= 1
buf.append(c)
elif c == "{":
depth_brace += 1
buf.append(c)
elif c == "}":
depth_brace -= 1
buf.append(c)
elif c == "[":
depth_bracket += 1
buf.append(c)
elif c == "]":
depth_bracket -= 1
buf.append(c)
elif (
c == ","
and depth_paren == 0
and depth_brace == 0
and depth_bracket == 0
):
args.append("".join(buf).strip())
buf = []
else:
buf.append(c)
i += 1
tail = "".join(buf).strip()
if tail:
args.append(tail)
return args
def parse_call_args(arg_text: str) -> tuple[list[Any], dict[str, Any]]:
"""Parse positional + named (name: value) C# call arguments."""
positional: list[Any] = []
named: dict[str, Any] = {}
for raw in split_top_level_args(arg_text):
if not raw:
continue
m = re.match(r"^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.*)$", raw, re.S)
if m and not raw.lstrip().startswith('"'):
name, val = m.group(1), m.group(2).strip()
named[name] = coerce_literal(val)
else:
positional.append(coerce_literal(raw))
return positional, named
def is_seed_call(arg_text: str) -> bool:
"""True when the first arg is a string literal (seed), not a method signature."""
parts = split_top_level_args(arg_text)
return bool(parts) and parts[0].lstrip().startswith('"')
def coerce_literal(token: str) -> Any:
t = token.strip()
if not t:
return t
if t.startswith('"'):
return parse_csharp_string(t)
low = t.lower()
if low in ("true", "false"):
return low == "true"
if re.fullmatch(r"-?\d+(\.\d+)?[fFdDmM]?", t):
return parse_number(t)
if t.startswith("new[]") or t.startswith("new string[]"):
return parse_string_array(t)
return t
def parse_string_array(expr: str) -> list[str]:
m = re.search(r"\{(.*)\}", expr, re.S)
if not m:
return []
inner = m.group(1).strip()
if not inner:
return []
return [parse_csharp_string(p) for p in split_top_level_args(inner) if p.strip()]
def find_method_calls(text: str, method: str) -> list[tuple[int, str]]:
"""Return (index, arg_text) for method(...); calls, balanced parens."""
results: list[tuple[int, str]] = []
pattern = re.compile(rf"\b{re.escape(method)}\s*\(")
for m in pattern.finditer(text):
start = m.end()
depth = 1
i = start
in_string = False
escape = False
while i < len(text) and depth > 0:
c = text[i]
if in_string:
if escape:
escape = False
elif c == "\\":
escape = True
elif c == '"':
in_string = False
else:
if c == '"':
in_string = True
elif c == "(":
depth += 1
elif c == ")":
depth -= 1
if depth == 0:
results.append((m.start(), text[start:i]))
break
i += 1
return results
def read_catalog(name: str) -> str:
path = CATALOGS / name
if not path.is_file():
raise FileNotFoundError(path)
return strip_csharp_comments(path.read_text(encoding="utf-8"))
# ---------------------------------------------------------------------------
# Section parsers
# ---------------------------------------------------------------------------
_FAILURES: list[str] = []
def fail(msg: str) -> None:
_FAILURES.append(msg)
print(f"PARSE FAIL: {msg}", file=sys.stderr)
def parse_happiness(text: str) -> list[dict[str, Any]]:
entries: list[dict[str, Any]] = []
# Match ["id"] = new HappinessCatalogEntry { ... },
pattern = re.compile(
r'\["([^"]+)"\]\s*=\s*new\s+HappinessCatalogEntry\s*\{',
re.M,
)
for m in pattern.finditer(text):
entry_id = m.group(1)
body_start = m.end()
depth = 1
i = body_start
in_string = False
escape = False
while i < len(text) and depth > 0:
c = text[i]
if in_string:
if escape:
escape = False
elif c == "\\":
escape = True
elif c == '"':
in_string = False
else:
if c == '"':
in_string = True
elif c == "{":
depth += 1
elif c == "}":
depth -= 1
if depth == 0:
break
i += 1
if depth != 0:
fail(f"happiness: unclosed block for {entry_id}")
continue
body = text[body_start:i]
try:
entries.append(parse_happiness_body(entry_id, body))
except Exception as ex: # noqa: BLE001
fail(f"happiness[{entry_id}]: {ex}")
return entries
def parse_happiness_body(entry_id: str, body: str) -> dict[str, Any]:
props: dict[str, str] = {}
# Property = value; (value may be new[] { ... })
prop_re = re.compile(
r"(\w+)\s*=\s*"
r"("
r"new\s*(?:string\s*)?\[\s*\]\s*\{(?:[^{}]|\{[^{}]*\})*\}"
r'|"[^"\\]*(?:\\.[^"\\]*)*"'
r"|[^,\n]+"
r")\s*,?",
re.M,
)
for m in prop_re.finditer(body):
props[m.group(1)] = m.group(2).strip().rstrip(",")
def req(name: str) -> str:
if name not in props:
raise ValueError(f"missing {name}")
return props[name]
strength = parse_number(req("EventStrength"))
row: dict[str, Any] = {
"id": parse_csharp_string(props["Id"]) if "Id" in props else entry_id,
"strength": strength,
"presentation": enum_leaf(req("Presentation")),
"context": enum_leaf(req("Context")),
"life": enum_leaf(req("Life")),
"overlap": enum_leaf(req("Overlap")),
"relationRole": enum_leaf(req("RelationRole")),
"category": parse_csharp_string(req("InterestCategory"))
if req("InterestCategory").startswith('"')
else req("InterestCategory").strip('"'),
"variantsWithRelated": parse_string_array(req("VariantsWithRelated"))
if "VariantsWithRelated" in props
else [],
"variantsWithoutRelated": parse_string_array(req("VariantsWithoutRelated"))
if "VariantsWithoutRelated" in props
else [],
}
if "CanonicalMilestoneKey" in props:
key = props["CanonicalMilestoneKey"]
val = parse_csharp_string(key) if key.startswith('"') else key
if val:
row["canonicalMilestoneKey"] = val
if "StatusOverlapId" in props:
key = props["StatusOverlapId"]
val = parse_csharp_string(key) if key.startswith('"') else key
if val:
row["statusOverlapId"] = val
return row
def parse_status(text: str) -> list[dict[str, Any]]:
entries: list[dict[str, Any]] = []
for _, args in find_method_calls(text, "AddNoInterest"):
if not is_seed_call(args):
continue
pos, _ = parse_call_args(args)
if not pos:
fail(f"status AddNoInterest: empty args ({args!r})")
continue
entries.append(
{
"id": pos[0],
"strength": 20,
"category": "StatusAmbient",
"camera": False,
"extendsGrief": False,
}
)
for _, args in find_method_calls(text, "Add"):
if not is_seed_call(args):
continue
pos, named = parse_call_args(args)
if len(pos) < 4:
continue
try:
entry = {
"id": pos[0],
"strength": _num(pos[1]),
"category": pos[2],
"camera": bool(pos[3]),
"extendsGrief": bool(named.get("extendsGrief", False)),
}
entries.append(entry)
except Exception as ex: # noqa: BLE001
fail(f"status Add({args[:60]}…): {ex}")
return entries
def parse_world_log(text: str) -> list[dict[str, Any]]:
entries: list[dict[str, Any]] = []
for _, args in find_method_calls(text, "Add"):
if not is_seed_call(args):
continue
pos, _ = parse_call_args(args)
# Seed calls: id, strength, category, createsInterest, label, minWatch, maxWatch
if len(pos) < 7:
fail(f"worldLog Add: expected 7 args, got {len(pos)} ({args[:60]}…)")
continue
try:
strength = float(pos[1])
camera = bool(pos[3])
chronicle = camera and strength >= 65.0
row: dict[str, Any] = {
"id": pos[0],
"strength": _num(pos[1]),
"category": pos[2],
"camera": camera,
"label": pos[4],
"minWatch": _num(pos[5]),
"maxWatch": _num(pos[6]),
"chronicle": chronicle,
}
entries.append(row)
except Exception as ex: # noqa: BLE001
fail(f"worldLog Add({args[:60]}…): {ex}")
return entries
def parse_simple_add(
text: str,
*,
default_category: str | None = None,
default_label: str | None = None,
default_camera: bool = True,
min_pos: int = 2,
style: str = "id_strength_category_label",
) -> list[dict[str, Any]]:
"""Parse Add(...) seed calls for DiscreteEventEntry-style catalogs."""
entries: list[dict[str, Any]] = []
for _, args in find_method_calls(text, "Add"):
if not is_seed_call(args):
continue
pos, named = parse_call_args(args)
if len(pos) < min_pos:
continue
try:
if style == "id_strength_only":
# Trait: Add(id, strength) - apply defaults
if len(pos) != 2 or not isinstance(pos[0], str):
continue
row = {
"id": pos[0],
"strength": _num(pos[1]),
"category": default_category or "Trait",
"camera": default_camera,
"label": default_label or "Trait: {id} ({a})",
}
elif style == "id_strength_category_label":
# Plot/Book/Era/Relationship: Add(id, strength, category, label [, createsInterest])
if len(pos) < 4:
continue
camera = bool(named.get("createsInterest", pos[4] if len(pos) > 4 else default_camera))
row = {
"id": pos[0],
"strength": _num(pos[1]),
"category": pos[2],
"camera": camera,
"label": pos[3],
}
elif style == "disaster":
# Add(id, strength, worldLogId)
if len(pos) < 3:
continue
row = {
"id": pos[0],
"strength": _num(pos[1]),
"category": "Disaster",
"camera": True,
"worldLogId": pos[2],
}
elif style == "war_type":
# Add(id, strength, labelTemplate)
if len(pos) < 3:
continue
row = {
"id": pos[0],
"strength": _num(pos[1]),
"category": "Politics",
"camera": True,
"label": pos[2],
}
else:
fail(f"unknown add style {style}")
continue
entries.append(row)
except Exception as ex: # noqa: BLE001
fail(f"{style} Add({args[:60]}…): {ex}")
return entries
def _num(v: Any) -> int | float:
if isinstance(v, float) and v.is_integer():
return int(v)
return v
def parse_libraries(text: str) -> dict[str, Any]:
"""Emit defaults per library class; include signalIds only when HashSet listed."""
# Class blocks: public static class Spell { ... }
class_re = re.compile(
r"public\s+static\s+class\s+(\w+)\s*\{",
re.M,
)
classes = list(class_re.finditer(text))
wanted_order = [
("Spell", "spell"),
("Power", "power"),
("Item", "item"),
("Gene", "gene"),
("Phenotype", "phenotype"),
("WorldLaw", "worldLaw"),
("SubspeciesTrait", "subspeciesTrait"),
("Biome", "biome"),
]
by_cs: dict[str, dict[str, Any]] = {}
for idx, m in enumerate(classes):
name = m.group(1)
if name not in {cs for cs, _ in wanted_order}:
continue
start = m.end()
end = classes[idx + 1].start() if idx + 1 < len(classes) else len(text)
block = text[start:end]
defaults = extract_ensure_seeded_defaults(name, block)
if defaults is None:
fail(f"libraries[{name}]: could not parse EnsureSeeded defaults")
continue
by_cs[name] = defaults
libs: dict[str, Any] = {}
for cs_name, json_key in wanted_order:
if cs_name in by_cs:
libs[json_key] = by_cs[cs_name]
return libs
def extract_ensure_seeded_defaults(name: str, block: str) -> dict[str, Any] | None:
# Find EnsureSeeded( ... ) call args
calls = find_method_calls(block, "EnsureSeeded")
if not calls:
# LiveLibraryInterest.EnsureSeeded via expression body
calls = find_method_calls(block, "LiveLibraryInterest.EnsureSeeded")
if not calls:
return None
_, arg_text = calls[0]
pos, named = parse_call_args(arg_text)
# Signature (positional):
# Entries, enumerate, category, label, ambientStrength, signalIds, signalStrength,
# [signalPredicate], ambientCreatesInterest
# Named kwargs also used: ambientStrength, signalIds, signalStrength,
# signalPredicate, ambientCreatesInterest
category = None
label = None
ambient_strength = named.get("ambientStrength")
signal_strength = named.get("signalStrength")
ambient_creates = named.get("ambientCreatesInterest")
signal_ids_expr = named.get("signalIds")
# Positional layout from LiveLibraryInterest.EnsureSeeded - read that file if needed.
# From usage:
# Entries, Enumerate..., "Category", "label", ambientStrength: N, SignalIds|null,
# signalStrength: N, [signalPredicate: ...], ambientCreatesInterest: bool
if len(pos) >= 4:
category = pos[2] if isinstance(pos[2], str) else category
label = pos[3] if isinstance(pos[3], str) else label
if ambient_strength is None and len(pos) >= 5 and isinstance(pos[4], (int, float)):
ambient_strength = pos[4]
# signalIds may be positional after ambientStrength
if signal_ids_expr is None and len(pos) >= 6:
signal_ids_expr = pos[5]
if signal_strength is None:
# often named; sometimes after signalIds
for p in pos[6:]:
if isinstance(p, (int, float)):
signal_strength = p
break
if ambient_creates is None:
for p in reversed(pos):
if isinstance(p, bool):
ambient_creates = p
break
# Also pull named ambientStrength etc. from the raw call via regex for robustness
am = re.search(r"ambientStrength\s*:\s*([\d.]+)f?", block)
if am and ambient_strength is None:
ambient_strength = parse_number(am.group(1))
sm = re.search(r"signalStrength\s*:\s*([\d.]+)f?", block)
if sm and signal_strength is None:
signal_strength = parse_number(sm.group(1))
acm = re.search(r"ambientCreatesInterest\s*:\s*(true|false)", block)
if acm and ambient_creates is None:
ambient_creates = acm.group(1) == "true"
# Category / label from EnsureSeeded positional strings
cat_m = re.search(
r"EnsureSeeded\s*\(\s*Entries\s*,\s*[^,]+,\s*\"([^\"]+)\"\s*,\s*\"([^\"]*)\"",
block,
)
if cat_m:
category = cat_m.group(1)
label = cat_m.group(2)
if category is None or label is None or ambient_strength is None or signal_strength is None:
return None
if ambient_creates is None:
ambient_creates = True
out: dict[str, Any] = {
"ambientStrength": _num(ambient_strength),
"signalStrength": _num(signal_strength),
"category": category,
"label": label,
"ambientCreatesInterest": bool(ambient_creates),
}
# signalIds HashSet initializer if present
hs = re.search(
r"HashSet\s*<\s*string\s*>\s+\w+\s*=\s*new\s+HashSet\s*<\s*string\s*>\s*"
r"(?:\([^)]*\))?\s*\{([^}]*)\}",
block,
re.S,
)
if hs:
ids = []
for sm in re.finditer(r'"([^"]+)"', hs.group(1)):
ids.append(sm.group(1))
if ids:
out["signalIds"] = ids
return out
def load_existing_overlays(path: Path) -> dict[str, Any]:
if not path.is_file():
return {"decisions": [], "species": [], "character": []}
data = json.loads(path.read_text(encoding="utf-8"))
return {
"decisions": data.get("decisions") or [],
"species": data.get("species") or [],
"character": data.get("character") or [],
}
def merge_decisions(
existing: list[dict[str, Any]],
decision_cs: str,
) -> list[dict[str, Any]]:
"""Keep JSON decisions (strength/action win). Fill gaps from BuiltinActionPhrases."""
by_id: dict[str, dict[str, Any]] = {}
# Builtin phrases from C#
m = re.search(
r"BuiltinActionPhrases\s*=\s*new\s+Dictionary[^\{]*\{(.*)\};\s*\n\s*///",
decision_cs,
re.S,
)
if not m:
# fallback: grab until closing of static field
m = re.search(
r"BuiltinActionPhrases\s*=\s*new\s+Dictionary[^\{]*\{(.*?)^\s*\};",
decision_cs,
re.S | re.M,
)
builtins: dict[str, str] = {}
if m:
for km in re.finditer(r'\["([^"]+)"\]\s*=\s*"([^"]*)"', m.group(1)):
builtins[km.group(1)] = km.group(2)
else:
fail("decisions: could not parse BuiltinActionPhrases")
for row in existing:
rid = row.get("id")
if not rid:
continue
by_id[rid] = {
"id": rid,
"action": row.get("action") or builtins.get(rid, ""),
"strength": row.get("strength", 86),
}
# Do not invent strengths for builtin-only ids not in JSON (user: JSON wins / keep from existing)
# Optionally add builtin ids missing from JSON with no strength? User said keep from existing JSON.
# So only existing decisions list.
return list(by_id.values()) if by_id else [
{"id": k, "action": v, "strength": 86} for k, v in builtins.items()
]
def build_document(existing_path: Path) -> dict[str, Any]:
overlays = load_existing_overlays(existing_path)
happiness = parse_happiness(read_catalog("EventCatalog.Happiness.cs"))
status = parse_status(read_catalog("EventCatalog.Status.cs"))
world_log = parse_world_log(read_catalog("EventCatalog.WorldLog.cs"))
plots = parse_simple_add(
read_catalog("EventCatalog.Plot.cs"),
style="id_strength_category_label",
min_pos=4,
)
relationship = parse_simple_add(
read_catalog("EventCatalog.Relationship.cs"),
style="id_strength_category_label",
min_pos=4,
)
traits = parse_simple_add(
read_catalog("EventCatalog.Trait.cs"),
style="id_strength_only",
default_category="Trait",
default_label="Trait: {id} ({a})",
default_camera=True,
min_pos=2,
)
books = parse_simple_add(
read_catalog("EventCatalog.Book.cs"),
style="id_strength_category_label",
min_pos=4,
)
eras = parse_simple_add(
read_catalog("EventCatalog.Era.cs"),
style="id_strength_category_label",
min_pos=4,
)
disasters = parse_simple_add(
read_catalog("EventCatalog.Disaster.cs"),
style="disaster",
min_pos=3,
)
war_types = parse_simple_add(
read_catalog("EventCatalog.WarType.cs"),
style="war_type",
min_pos=3,
)
libraries = parse_libraries(read_catalog("EventCatalog.Libraries.cs"))
decision_cs = read_catalog("EventCatalog.Decision.cs")
decisions = merge_decisions(overlays["decisions"], decision_cs)
doc: dict[str, Any] = {
"modVersion": MOD_VERSION,
"title": "IdleSpectator event catalog",
"updated": UPDATED,
"note": NOTE,
"decisions": decisions,
"species": overlays["species"],
"character": overlays["character"],
"happiness": happiness,
"status": status,
"worldLog": world_log,
"plots": plots,
"relationship": relationship,
"traits": traits,
"books": books,
"eras": eras,
"disasters": disasters,
"warTypes": war_types,
"libraries": libraries,
}
return doc
def print_counts(doc: dict[str, Any]) -> None:
sections = [
"decisions",
"species",
"character",
"happiness",
"status",
"worldLog",
"plots",
"relationship",
"traits",
"books",
"eras",
"disasters",
"warTypes",
]
print("Counts:")
for key in sections:
val = doc.get(key)
n = len(val) if isinstance(val, list) else 0
print(f" {key}: {n}")
libs = doc.get("libraries") or {}
print(f" libraries: {len(libs)} keys ({', '.join(libs.keys())})")
for k, v in libs.items():
sig = v.get("signalIds")
extra = f", signalIds={len(sig)}" if isinstance(sig, list) else ""
print(
f" {k}: ambient={v.get('ambientStrength')} signal={v.get('signalStrength')} "
f"category={v.get('category')}{extra}"
)
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument(
"--out",
type=Path,
default=DEFAULT_OUT,
help=f"Output JSON path (default: {DEFAULT_OUT})",
)
ap.add_argument(
"--existing",
type=Path,
default=DEFAULT_EXISTING,
help="Existing event-catalog.json to merge decisions/species/character from",
)
args = ap.parse_args()
doc = build_document(args.existing)
args.out.parent.mkdir(parents=True, exist_ok=True)
args.out.write_text(
json.dumps(doc, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
print(f"Wrote {args.out}")
print_counts(doc)
if _FAILURES:
print(f"\nParse failures ({len(_FAILURES)}):", file=sys.stderr)
for f in _FAILURES:
print(f" - {f}", file=sys.stderr)
return 1
print("\nParse failures: none")
return 0
if __name__ == "__main__":
sys.exit(main())