diff --git a/IdleSpectator/ActivityLog.cs b/IdleSpectator/ActivityLog.cs index 52fa2b4..9289c97 100644 --- a/IdleSpectator/ActivityLog.cs +++ b/IdleSpectator/ActivityLog.cs @@ -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); diff --git a/IdleSpectator/ActivityStatusProse.cs b/IdleSpectator/ActivityStatusProse.cs index f7a9fd0..62f05e1 100644 --- a/IdleSpectator/ActivityStatusProse.cs +++ b/IdleSpectator/ActivityStatusProse.cs @@ -13,10 +13,10 @@ public static class ActivityStatusProse new Dictionary(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"), }; diff --git a/IdleSpectator/AgentHarness.cs b/IdleSpectator/AgentHarness.cs index 7a813a0..917c9ef 100644 --- a/IdleSpectator/AgentHarness.cs +++ b/IdleSpectator/AgentHarness.cs @@ -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; + /// + /// When true, relationship interest feeds still Emit while + /// (harness verification of live Harmony patches). + /// + public static bool ForceRelationshipFeeds; + /// /// When true, InterestDirector / SpeciesDiscovery stay frozen during harness commands. /// director_run 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; } @@ -2381,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 : "?")}"); } @@ -2681,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"; @@ -2744,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"; @@ -2756,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"; @@ -2764,7 +2858,7 @@ 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"; @@ -3590,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(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(); @@ -6154,6 +6283,7 @@ public static class AgentHarness long focusId = MoveCamera._focus_unit.getID(); if (focusId != 0) { + _activitySubjectId = focusId; return focusId; } } @@ -6163,6 +6293,11 @@ public static class AgentHarness // ignore } + if (_activitySubjectId != 0) + { + return _activitySubjectId; + } + return WatchCaption.CurrentUnitId; } diff --git a/IdleSpectator/Events/EventReason.cs b/IdleSpectator/Events/EventReason.cs index f6f7442..efe32fb 100644 --- a/IdleSpectator/Events/EventReason.cs +++ b/IdleSpectator/Events/EventReason.cs @@ -215,6 +215,173 @@ public static class EventReason return gained ? an + " gains " + id : an + " loses " + id; } + /// + /// Plot labels are phase-aware: start templates stay progressive; finish verbs fire on complete. + /// + 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); diff --git a/IdleSpectator/Events/Feeds/PlotInterestFeed.cs b/IdleSpectator/Events/Feeds/PlotInterestFeed.cs index 506a76f..ddd26bb 100644 --- a/IdleSpectator/Events/Feeds/PlotInterestFeed.cs +++ b/IdleSpectator/Events/Feeds/PlotInterestFeed.cs @@ -28,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, diff --git a/IdleSpectator/Events/Feeds/RelationshipInterestFeed.cs b/IdleSpectator/Events/Feeds/RelationshipInterestFeed.cs index 45e68f4..28ef84b 100644 --- a/IdleSpectator/Events/Feeds/RelationshipInterestFeed.cs +++ b/IdleSpectator/Events/Feeds/RelationshipInterestFeed.cs @@ -5,9 +5,15 @@ namespace IdleSpectator; /// Registers relationship lifecycle interest candidates under the ownership contract. 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; } @@ -35,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"; @@ -65,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; } @@ -85,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", @@ -97,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) diff --git a/IdleSpectator/Events/Patches/ActorHappinessPatches.cs b/IdleSpectator/Events/Patches/ActorHappinessPatches.cs index 97e818d..d50b5e8 100644 --- a/IdleSpectator/Events/Patches/ActorHappinessPatches.cs +++ b/IdleSpectator/Events/Patches/ActorHappinessPatches.cs @@ -1,10 +1,11 @@ +using ai.behaviours; using HarmonyLib; namespace IdleSpectator; /// /// 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. /// [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"); + } + + /// + /// 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. + /// + [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(); + } } diff --git a/IdleSpectator/Events/Patches/RelationshipEventPatches.cs b/IdleSpectator/Events/Patches/RelationshipEventPatches.cs index 5670a69..6567d9d 100644 --- a/IdleSpectator/Events/Patches/RelationshipEventPatches.cs +++ b/IdleSpectator/Events/Patches/RelationshipEventPatches.cs @@ -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) + /// + /// Surviving partners clear dead lovers by field assignment in + /// , which bypasses . + /// Emit clear_lover for the living partner before those fields are nulled. + /// + [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 + /// + /// Real parent/child link. Vanilla addChild attaches avatar components (Dragon, Boat), not kids. + /// + [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); } + /// + /// Seeking only: BehFindLover returns Continue both when still searching and after + /// . Success is covered by set_lover / love status. + /// [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)); } diff --git a/IdleSpectator/HappinessEventRouter.cs b/IdleSpectator/HappinessEventRouter.cs index b19d391..99282d4 100644 --- a/IdleSpectator/HappinessEventRouter.cs +++ b/IdleSpectator/HappinessEventRouter.cs @@ -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); } /// Record that the game refused to apply happiness (psychopath/egg/no emotions). @@ -206,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) { @@ -239,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)) @@ -449,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; @@ -495,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); diff --git a/IdleSpectator/HarnessScenarios.cs b/IdleSpectator/HarnessScenarios.cs index ce04112..e2a5ddb 100644 --- a/IdleSpectator/HarnessScenarios.cs +++ b/IdleSpectator/HarnessScenarios.cs @@ -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 FamilyTriggerSmoke() + { + return new List + { + 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 ActivityHappinessAudit() { return new List @@ -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"), @@ -1205,6 +1240,7 @@ internal static class HarnessScenarios 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"), diff --git a/IdleSpectator/UnitDossier.cs b/IdleSpectator/UnitDossier.cs index f9d86ab..0504296 100644 --- a/IdleSpectator/UnitDossier.cs +++ b/IdleSpectator/UnitDossier.cs @@ -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); } + /// + /// Job sits on the orange reason row: alone when there is no watch beat, otherwise + /// {reason} · {job}. + /// + 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) diff --git a/IdleSpectator/WatchCaption.cs b/IdleSpectator/WatchCaption.cs index 7ceca8c..67e1c07 100644 --- a/IdleSpectator/WatchCaption.cs +++ b/IdleSpectator/WatchCaption.cs @@ -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) { diff --git a/IdleSpectator/event-catalog.json b/IdleSpectator/event-catalog.json index 76d8892..9bb8eb9 100644 --- a/IdleSpectator/event-catalog.json +++ b/IdleSpectator/event-catalog.json @@ -2,7 +2,7 @@ "modVersion": "0.25.1", "title": "IdleSpectator event catalog", "updated": "2026-07-16", - "note": "Authored IdleSpectator event catalog. Score ladder (EventStrength): 95-100 epic world (war/kingdom fall/major disaster); 88-94 major spectacle; 78-87 kingdom politics / peak grief; 70-77 life milestones (notice band); 55-69 notable beats; 40-54 warm social; 28-39 soft daily Signal; \u226425 B-only FX. decisions.action = infinitive after \"decides to\"; other domains use label/variants. scoring-model.json remains global formula (margins/multipliers).", + "note": "Authored IdleSpectator event catalog. Score ladder (EventStrength): 95-100 epic world (war/kingdom fall/major disaster); 88-94 major spectacle; 78-87 kingdom politics / peak grief; 70-77 life milestones (notice band); 55-69 notable beats; 40-54 warm social; 28-39 soft daily Signal; ≤25 B-only FX. decisions.action = infinitive after \"decides to\"; other domains use label/variants. scoring-model.json remains global formula (margins/multipliers).", "decisions": [ { "id": "find_lover", @@ -256,12 +256,12 @@ "relationRole": "None", "category": "Emotion", "variantsWithRelated": [ - "gets caught", - "is caught" + "gets caught in the act", + "is caught red-handed" ], "variantsWithoutRelated": [ - "gets caught", - "is caught" + "gets caught in the act", + "is caught red-handed" ] }, { @@ -292,12 +292,12 @@ "relationRole": "None", "category": "Daily", "variantsWithRelated": [ - "finishes a meal", - "eats and feels better" + "eats and feels better", + "enjoys a meal" ], "variantsWithoutRelated": [ - "finishes a meal", - "eats and feels better" + "eats and feels better", + "enjoys a meal" ], "statusOverlapId": "just_ate" }, @@ -713,12 +713,12 @@ "relationRole": "None", "category": "Daily", "variantsWithRelated": [ - "finishes playing", - "has fun playing" + "finishes playing around", + "has fun as a child" ], "variantsWithoutRelated": [ - "finishes playing", - "has fun playing" + "finishes playing around", + "has fun as a child" ] }, { @@ -749,12 +749,12 @@ "relationRole": "None", "category": "Daily", "variantsWithRelated": [ - "bursts out laughing", - "shares a laugh" + "finishes laughing", + "had a good laugh" ], "variantsWithoutRelated": [ - "bursts out laughing", - "shares a laugh" + "finishes laughing", + "had a good laugh" ], "statusOverlapId": "laughing" }, @@ -787,12 +787,12 @@ "relationRole": "None", "category": "Daily", "variantsWithRelated": [ - "lets out a curse", - "swears loudly" + "finishes swearing", + "lets out a string of curses" ], "variantsWithoutRelated": [ - "lets out a curse", - "swears loudly" + "finishes swearing", + "lets out a string of curses" ], "statusOverlapId": "swearing" }, @@ -974,12 +974,12 @@ "relationRole": "None", "category": "Emotion", "variantsWithRelated": [ - "feels the divine touch", - "is touched by the divine" + "is cleansed by divine light", + "has bad traits burned away by the divine" ], "variantsWithoutRelated": [ - "feels the divine touch", - "is touched by the divine" + "is cleansed by divine light", + "has bad traits burned away by the divine" ] }, { @@ -2451,812 +2451,812 @@ "strength": 72, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "wise", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "whirlwind", "strength": 72, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "weightless", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "weak", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "veteran", "strength": 58, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "venomous", "strength": 72, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "unlucky", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "ugly", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "tumor_infection", "strength": 72, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "tough", "strength": 58, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "titan_lungs", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "tiny", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "thorns", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "thief", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "super_health", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "sunblessed", "strength": 58, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "stupid", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "strong_minded", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "strong", "strength": 58, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "soft_skin", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "slow", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "skin_burns", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "short_sighted", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "shiny", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "scar_of_divinity", "strength": 72, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "savage", "strength": 72, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "regeneration", "strength": 58, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "pyromaniac", "strength": 72, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "psychopath", "strength": 72, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "poisonous", "strength": 72, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "poison_immune", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "plague", "strength": 72, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "peaceful", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "paranoid", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "pacifist", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "nightchild", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "mute", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "mush_spores", "strength": 72, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "moonchild", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "miracle_born", "strength": 72, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "miracle_bearer", "strength": 72, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "miner", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "metamorphed", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "mega_heartbeat", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "mageslayer", "strength": 58, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "madness", "strength": 72, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "lustful", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "lucky", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "long_liver", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "light_lamp", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "kingslayer", "strength": 58, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "infertile", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "infected", "strength": 72, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "immune", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "immortal", "strength": 72, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "hotheaded", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "honest", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "heliophobia", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "heart_of_wizard", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "healing_aura", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "hard_skin", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "greedy", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "golden_tooth", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "gluttonous", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "giant", "strength": 72, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "genius", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "freeze_proof", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "fragile_health", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "flower_prints", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "flesh_eater", "strength": 72, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "fire_proof", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "fire_blood", "strength": 72, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "fertile", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "fat", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "fast", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "eyepatch", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "evil", "strength": 72, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "energized", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "eagle_eyed", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "dragonslayer", "strength": 58, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "dodge", "strength": 58, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "desire_harp", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "desire_golden_egg", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "desire_computer", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "desire_alien_mold", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "deflect_projectile", "strength": 58, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "deceitful", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "death_nuke", "strength": 72, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "death_mark", "strength": 72, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "death_bomb", "strength": 72, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "dash", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "crippled", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "content", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "contagious", "strength": 72, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "cold_aura", "strength": 72, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "clumsy", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "clone", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "chosen_one", "strength": 72, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "burning_feet", "strength": 72, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "bubble_defense", "strength": 58, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "boosted_vitality", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "bomberman", "strength": 72, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "boat", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "bloodlust", "strength": 72, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "block", "strength": 58, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "blessed", "strength": 58, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "battle_reflexes", "strength": 58, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "backstep", "strength": 58, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "attractive", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "arcane_reflexes", "strength": 58, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "ambitious", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "agile", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "acid_touch", "strength": 72, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "acid_proof", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" }, { "id": "acid_blood", "strength": 38, "category": "Trait", "camera": true, - "label": "Trait: {id} ({a})" + "label": "{a} gains {id}" } ], "books": [ diff --git a/IdleSpectator/mod.json b/IdleSpectator/mod.json index dbce99e..fe97a85 100644 --- a/IdleSpectator/mod.json +++ b/IdleSpectator/mod.json @@ -1,7 +1,7 @@ { "name": "IdleSpectator", "author": "dazed", - "version": "0.25.1", - "description": "AFK Idle Spectator (I) + Lore (L). Event strengths/prose in event-catalog.json.", + "version": "0.25.11", + "description": "AFK Idle Spectator (I) + Lore (L). Event trigger audit Phases 4-8 + job reason row.", "GUID": "com.dazed.idlespectator" } diff --git a/docs/event-prose-audit.md b/docs/event-prose-audit.md index 30394ea..343dc2a 100644 --- a/docs/event-prose-audit.md +++ b/docs/event-prose-audit.md @@ -37,7 +37,7 @@ 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) @@ -67,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 4–7 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). diff --git a/docs/event-trigger-audit.tsv b/docs/event-trigger-audit.tsv new file mode 100644 index 0000000..34b2ec1 --- /dev/null +++ b/docs/event-trigger-audit.tsv @@ -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