Compare commits
5 commits
1c00a7980a
...
04731c2c06
| Author | SHA1 | Date | |
|---|---|---|---|
| 04731c2c06 | |||
| 2d1d915156 | |||
| cc0d44ae2e | |||
| f4be23e23d | |||
| 2c1f80cdb4 |
56 changed files with 8766 additions and 2317 deletions
|
|
@ -19,6 +19,7 @@ public sealed class ActivityHappinessAuditResult
|
|||
public int MissingPolicy;
|
||||
public int RequiredContextFailures;
|
||||
public int MissingEventStrength;
|
||||
public int ProseRegressions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -43,6 +44,7 @@ public static class ActivityHappinessHarness
|
|||
int missingPolicy = 0;
|
||||
int requiredContextFailures = 0;
|
||||
int missingEventStrength = 0;
|
||||
int proseRegressions = 0;
|
||||
var tsv = new StringBuilder();
|
||||
tsv.AppendLine(
|
||||
"id\tvalue\tpsychopath\tpresentation\tlife\tcontext\toverlap\tevent_strength\tclause\tauthored\ticon\tlocale_leak\tnotes");
|
||||
|
|
@ -195,6 +197,49 @@ public static class ActivityHappinessHarness
|
|||
// Disk may be unavailable in some harness contexts.
|
||||
}
|
||||
|
||||
// Prose vs call-site regressions (Layer B honesty).
|
||||
proseRegressions += CountProseRegression(
|
||||
"just_born",
|
||||
mustContain: new[] { "appears in the world", "brought into existence" },
|
||||
mustNotContain: new[] { "born into the world", "first breath" });
|
||||
proseRegressions += CountProseRegression(
|
||||
"just_kissed",
|
||||
mustContain: new[] { "mates", "intimacy" },
|
||||
mustNotContain: new[] { "kiss" });
|
||||
proseRegressions += CountProseRegression(
|
||||
"just_got_out_of_egg",
|
||||
mustContain: new[] { "hatches from an egg" },
|
||||
mustNotContain: Array.Empty<string>());
|
||||
|
||||
// Camera demotion: only true FX / bookkeeping stay Ambient (Layer B).
|
||||
// Interesting life/social beats are Signal; director ranks by EventStrength.
|
||||
string[] ambientRequired =
|
||||
{
|
||||
"got_poked", "got_caught", "paid_tax", "just_ate", "just_pooped"
|
||||
};
|
||||
for (int i = 0; i < ambientRequired.Length; i++)
|
||||
{
|
||||
HappinessCatalogEntry e = EventCatalog.Happiness.GetOrFallback(ambientRequired[i]);
|
||||
if (e.Presentation == HappinessPresentationTier.Signal)
|
||||
{
|
||||
proseRegressions++;
|
||||
}
|
||||
}
|
||||
|
||||
string[] signalRequired =
|
||||
{
|
||||
"just_made_friend", "just_injured", "just_found_house", "got_robbed",
|
||||
"fallen_in_love", "become_alpha", "just_had_child"
|
||||
};
|
||||
for (int i = 0; i < signalRequired.Length; i++)
|
||||
{
|
||||
HappinessCatalogEntry e = EventCatalog.Happiness.GetOrFallback(signalRequired[i]);
|
||||
if (e.Presentation != HappinessPresentationTier.Signal)
|
||||
{
|
||||
proseRegressions++;
|
||||
}
|
||||
}
|
||||
|
||||
bool passed = missingAuthored == 0
|
||||
&& missingIcon == 0
|
||||
&& localeLeaks == 0
|
||||
|
|
@ -202,7 +247,8 @@ public static class ActivityHappinessHarness
|
|||
&& orphanAuthored == 0
|
||||
&& missingPolicy == 0
|
||||
&& requiredContextFailures == 0
|
||||
&& missingEventStrength == 0;
|
||||
&& missingEventStrength == 0
|
||||
&& proseRegressions == 0;
|
||||
|
||||
return new ActivityHappinessAuditResult
|
||||
{
|
||||
|
|
@ -216,14 +262,45 @@ public static class ActivityHappinessHarness
|
|||
MissingPolicy = missingPolicy,
|
||||
RequiredContextFailures = requiredContextFailures,
|
||||
MissingEventStrength = missingEventStrength,
|
||||
ProseRegressions = proseRegressions,
|
||||
Detail =
|
||||
$"total={liveIds.Count} missingAuthored={missingAuthored} missingIcon={missingIcon} "
|
||||
+ $"localeLeaks={localeLeaks} emptyPhrase={emptyPhrase} orphanAuthored={orphanAuthored} "
|
||||
+ $"missingPolicy={missingPolicy} requiredContextFailures={requiredContextFailures} "
|
||||
+ $"missingEventStrength={missingEventStrength}"
|
||||
+ $"missingEventStrength={missingEventStrength} proseRegressions={proseRegressions}"
|
||||
};
|
||||
}
|
||||
|
||||
private static int CountProseRegression(string id, string[] mustContain, string[] mustNotContain)
|
||||
{
|
||||
HappinessCatalogEntry entry = EventCatalog.Happiness.GetOrFallback(id);
|
||||
if (entry == null || entry.IsFallback)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
string joined = string.Join(" ", entry.VariantsWithoutRelated ?? Array.Empty<string>())
|
||||
+ " "
|
||||
+ string.Join(" ", entry.VariantsWithRelated ?? Array.Empty<string>());
|
||||
for (int i = 0; mustContain != null && i < mustContain.Length; i++)
|
||||
{
|
||||
if (joined.IndexOf(mustContain[i], StringComparison.OrdinalIgnoreCase) < 0)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; mustNotContain != null && i < mustNotContain.Length; i++)
|
||||
{
|
||||
if (joined.IndexOf(mustNotContain[i], StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static bool LooksLikeLocaleLeak(string text, string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -184,6 +184,38 @@ public static class ActivityStatusHarness
|
|||
// Artifact write is best-effort.
|
||||
}
|
||||
|
||||
int cameraDemotionFailures = 0;
|
||||
// Brief FX / cooldowns must stay B. Story-readable statuses must stay A.
|
||||
string[] bOnlyStatus =
|
||||
{
|
||||
"afterglow", "cough", "dash", "dodge", "flicked", "just_ate", "on_guard",
|
||||
"recovery_combat_action", "recovery_plot", "recovery_social", "recovery_spell",
|
||||
"shield", "slowness", "spell_boost", "spell_silence", "egg"
|
||||
};
|
||||
for (int i = 0; i < bOnlyStatus.Length; i++)
|
||||
{
|
||||
StatusInterestEntry e = EventCatalog.Status.GetOrFallback(bOnlyStatus[i]);
|
||||
if (e.CreatesInterest)
|
||||
{
|
||||
cameraDemotionFailures++;
|
||||
}
|
||||
}
|
||||
|
||||
string[] aStatus =
|
||||
{
|
||||
"drowning", "burning", "frozen", "poisoned", "possessed", "cursed",
|
||||
"pregnant", "crying", "rage", "stunned", "soul_harvested", "tantrum",
|
||||
"fell_in_love", "starving", "angry", "sleeping", "inspired", "ash_fever"
|
||||
};
|
||||
for (int i = 0; i < aStatus.Length; i++)
|
||||
{
|
||||
StatusInterestEntry e = EventCatalog.Status.GetOrFallback(aStatus[i]);
|
||||
if (!e.CreatesInterest)
|
||||
{
|
||||
cameraDemotionFailures++;
|
||||
}
|
||||
}
|
||||
|
||||
bool passed = liveIds.Count > 20
|
||||
&& missingAuthored == 0
|
||||
&& missingIcon == 0
|
||||
|
|
@ -191,7 +223,8 @@ public static class ActivityStatusHarness
|
|||
&& emptyPhrase == 0
|
||||
&& orphanAuthored == 0
|
||||
&& missingInterest == 0
|
||||
&& orphanInterest == 0;
|
||||
&& orphanInterest == 0
|
||||
&& cameraDemotionFailures == 0;
|
||||
|
||||
return new ActivityStatusAuditResult
|
||||
{
|
||||
|
|
@ -207,7 +240,8 @@ public static class ActivityStatusHarness
|
|||
Detail =
|
||||
$"total={liveIds.Count} missingAuthored={missingAuthored} missingIcon={missingIcon} "
|
||||
+ $"localeLeaks={localeLeaks} emptyPhrase={emptyPhrase} orphanAuthored={orphanAuthored} "
|
||||
+ $"missingInterest={missingInterest} orphanInterest={orphanInterest} passed={passed}"
|
||||
+ $"missingInterest={missingInterest} orphanInterest={orphanInterest} "
|
||||
+ $"cameraDemotionFailures={cameraDemotionFailures} passed={passed}"
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ public static class ActivityStatusProse
|
|||
new Dictionary<string, (string, string)>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["afterglow"] = ("Basks in an afterglow", "Lets the afterglow fade"),
|
||||
["angry"] = ("Flies into a rage", "Cools off"),
|
||||
["angry"] = ("Flies into a fury", "Cools off"),
|
||||
["ash_fever"] = ("Catches ash fever", "Recovers from ash fever"),
|
||||
["being_suspicious"] = ("Starts acting suspicious", "Stops looking so suspicious"),
|
||||
["budding"] = ("Begins budding", "Finishes budding"),
|
||||
["budding"] = ("Begins budding", "Splits off a bud offspring"),
|
||||
["burning"] = ("Catches fire", "Stops burning"),
|
||||
["caffeinated"] = ("Gets a caffeine rush", "Comes down from caffeine"),
|
||||
["confused"] = ("Becomes confused", "Regains clarity"),
|
||||
|
|
@ -45,10 +45,10 @@ public static class ActivityStatusProse
|
|||
["on_guard"] = ("Goes on guard", "Drops guard"),
|
||||
["poisoned"] = ("Is poisoned", "Purges the poison"),
|
||||
["possessed"] = ("Is possessed", "Breaks free of possession"),
|
||||
["possessed_follower"] = ("Falls under a possessed follower's sway", "Escapes the follower's sway"),
|
||||
["possessed_follower"] = ("Is converted into a possessed follower", "Breaks free of follower possession"),
|
||||
["powerup"] = ("Powers up", "Loses the power-up"),
|
||||
["pregnant"] = ("Becomes pregnant", "Is no longer pregnant"),
|
||||
["pregnant_parthenogenesis"] = ("Becomes pregnant by parthenogenesis", "Ends the parthenogenic pregnancy"),
|
||||
["pregnant"] = ("Becomes pregnant", "Gives birth"),
|
||||
["pregnant_parthenogenesis"] = ("Becomes pregnant by parthenogenesis", "Gives birth alone"),
|
||||
["rage"] = ("Enters a rage", "Leaves the rage behind"),
|
||||
["recovery_combat_action"] = ("Needs a moment after combat", "Is ready for combat again"),
|
||||
["recovery_plot"] = ("Needs a moment after a plot", "Is ready to plot again"),
|
||||
|
|
@ -66,9 +66,9 @@ public static class ActivityStatusProse
|
|||
["stunned"] = ("Is stunned", "Shakes off the stun"),
|
||||
["surprised"] = ("Is surprised", "Recovers from surprise"),
|
||||
["swearing"] = ("Starts swearing", "Stops swearing"),
|
||||
["taking_roots"] = ("Starts taking root", "Pulls free of the roots"),
|
||||
["taking_roots"] = ("Starts taking root", "Finishes rooting and sprouts offspring"),
|
||||
["tantrum"] = ("Throws a tantrum", "Calms down from the tantrum"),
|
||||
["uprooting"] = ("Starts uprooting", "Finishes uprooting"),
|
||||
["uprooting"] = ("Emerges as a young plant", "Settles after emerging"),
|
||||
["voices_in_my_head"] = ("Hears voices", "The voices fall quiet"),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ public static class AgentHarness
|
|||
private static Actor _lastSpawned;
|
||||
private static string _lastSpawnedAssetId = "";
|
||||
private static Actor _happinessPartner;
|
||||
private static long _activitySubjectId;
|
||||
private static bool _directorRunActive;
|
||||
private static string _rememberedFocusKey = "";
|
||||
private static string LastScreenshotPath = "";
|
||||
|
|
@ -56,6 +57,12 @@ public static class AgentHarness
|
|||
|
||||
public static bool Busy => Queue.Count > 0 || _waitUntil > 0f;
|
||||
|
||||
/// <summary>
|
||||
/// When true, relationship interest feeds still Emit while <see cref="Busy"/>
|
||||
/// (harness verification of live Harmony patches).
|
||||
/// </summary>
|
||||
public static bool ForceRelationshipFeeds;
|
||||
|
||||
/// <summary>
|
||||
/// When true, InterestDirector / SpeciesDiscovery stay frozen during harness commands.
|
||||
/// <c>director_run</c> temporarily clears this so dwell/interrupt/discovery can execute.
|
||||
|
|
@ -828,18 +835,24 @@ public static class AgentHarness
|
|||
|
||||
case "happiness_force_note":
|
||||
{
|
||||
long id = WatchCaption.CurrentUnitId;
|
||||
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
|
||||
if (id == 0 && focus != null)
|
||||
long id = 0;
|
||||
try
|
||||
{
|
||||
try
|
||||
if (focus != null)
|
||||
{
|
||||
id = focus.getID();
|
||||
}
|
||||
catch
|
||||
{
|
||||
id = 0;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
id = 0;
|
||||
}
|
||||
|
||||
// Prefer live focus over a stale dossier CurrentUnitId.
|
||||
if (id == 0)
|
||||
{
|
||||
id = WatchCaption.CurrentUnitId;
|
||||
}
|
||||
|
||||
string effectId = !string.IsNullOrEmpty(cmd.value) ? cmd.value.Trim() : "just_ate";
|
||||
|
|
@ -1031,6 +1044,50 @@ public static class AgentHarness
|
|||
break;
|
||||
}
|
||||
|
||||
case "relationship_set_parent":
|
||||
{
|
||||
Actor child = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : _lastSpawned;
|
||||
Actor parent = _happinessPartner;
|
||||
bool ok = false;
|
||||
string detail;
|
||||
ForceRelationshipFeeds = true;
|
||||
try
|
||||
{
|
||||
if (child != null && parent != null && child.isAlive() && parent.isAlive()
|
||||
&& child != parent)
|
||||
{
|
||||
child.setParent1(parent);
|
||||
ok = true;
|
||||
detail = $"parent={SafeName(parent)} child={SafeName(child)}";
|
||||
}
|
||||
else
|
||||
{
|
||||
detail = "need focus child + remembered parent";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
detail = "exception: " + ex.Message;
|
||||
ok = false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ForceRelationshipFeeds = false;
|
||||
}
|
||||
|
||||
if (ok)
|
||||
{
|
||||
_cmdOk++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_cmdFail++;
|
||||
}
|
||||
|
||||
Emit(cmd, ok, detail: detail);
|
||||
break;
|
||||
}
|
||||
|
||||
case "happiness_bond_friends":
|
||||
{
|
||||
Actor a = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
|
||||
|
|
@ -1070,21 +1127,45 @@ public static class AgentHarness
|
|||
|
||||
case "happiness_kill_partner":
|
||||
{
|
||||
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
|
||||
Actor partner = _happinessPartner;
|
||||
bool ok = false;
|
||||
string err = "";
|
||||
try
|
||||
{
|
||||
if (partner != null && partner.isAlive())
|
||||
{
|
||||
partner.die(true, AttackType.Divine);
|
||||
ok = !partner.isAlive();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
err = ex.Message ?? "die_exception";
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
try
|
||||
{
|
||||
ok = partner != null && !partner.isAlive();
|
||||
}
|
||||
catch
|
||||
{
|
||||
ok = false;
|
||||
}
|
||||
|
||||
// Keep the surviving focus so grief asserts resolve the right subject.
|
||||
try
|
||||
{
|
||||
if (focus != null && focus.isAlive())
|
||||
{
|
||||
CameraDirector.FocusUnit(focus);
|
||||
_activitySubjectId = focus.getID();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
|
||||
if (ok)
|
||||
{
|
||||
_cmdOk++;
|
||||
|
|
@ -1095,7 +1176,11 @@ public static class AgentHarness
|
|||
}
|
||||
|
||||
WatchCaption.ForceRefreshHistory();
|
||||
Emit(cmd, ok, detail: $"killed={SafeName(partner)} ok={ok}");
|
||||
Emit(
|
||||
cmd,
|
||||
ok,
|
||||
detail: $"killed={SafeName(partner)} ok={ok}"
|
||||
+ (string.IsNullOrEmpty(err) ? "" : " err=" + err));
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -1855,6 +1940,7 @@ public static class AgentHarness
|
|||
case "scoring_reload":
|
||||
{
|
||||
bool ok = InterestScoringConfig.Reload();
|
||||
bool catalogOk = EventCatalogConfig.Reload();
|
||||
if (ok)
|
||||
{
|
||||
_cmdOk++;
|
||||
|
|
@ -1868,11 +1954,33 @@ public static class AgentHarness
|
|||
cmd,
|
||||
ok: ok,
|
||||
detail: ok
|
||||
? $"loaded v={InterestScoringConfig.LoadedVersion} path={InterestScoringConfig.LoadedPath}"
|
||||
? $"loaded v={InterestScoringConfig.LoadedVersion} path={InterestScoringConfig.LoadedPath} "
|
||||
+ $"eventCatalog={catalogOk} decisions={EventCatalogConfig.DecisionCount}"
|
||||
: "scoring_reload_failed");
|
||||
break;
|
||||
}
|
||||
|
||||
case "event_catalog_reload":
|
||||
{
|
||||
bool ok = EventCatalogConfig.Reload();
|
||||
if (ok)
|
||||
{
|
||||
_cmdOk++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_cmdFail++;
|
||||
}
|
||||
|
||||
Emit(
|
||||
cmd,
|
||||
ok: ok,
|
||||
detail: ok
|
||||
? $"loaded v={EventCatalogConfig.LoadedVersion} path={EventCatalogConfig.LoadedPath}"
|
||||
: "event_catalog_reload_failed");
|
||||
break;
|
||||
}
|
||||
|
||||
case "interest_variety_seed":
|
||||
{
|
||||
// value = "events:chars" e.g. "2:10"
|
||||
|
|
@ -2358,6 +2466,15 @@ public static class AgentHarness
|
|||
}
|
||||
|
||||
CameraDirector.FocusUnit(unit);
|
||||
try
|
||||
{
|
||||
_activitySubjectId = unit.getID();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_activitySubjectId = 0;
|
||||
}
|
||||
|
||||
_cmdOk++;
|
||||
Emit(cmd, ok: true, detail: $"unit={SafeName(unit)} asset={(unit.asset != null ? unit.asset.id : "?")}");
|
||||
}
|
||||
|
|
@ -2658,7 +2775,7 @@ public static class AgentHarness
|
|||
|
||||
entry = EventCatalog.Plot.GetOrFallback(id);
|
||||
key = "plot:" + entry.Id + ":new:" + EventFeedUtil.SafeId(unit);
|
||||
label = entry.MakeLabel(EventFeedUtil.SafeName(unit), "new");
|
||||
label = EventReason.Plot(unit, entry.LabelTemplate, "new", entry.Id);
|
||||
strength = entry.EventStrength;
|
||||
category = entry.Category;
|
||||
source = "plot";
|
||||
|
|
@ -2721,7 +2838,7 @@ public static class AgentHarness
|
|||
|
||||
entry = EventCatalog.Trait.GetOrFallback(id);
|
||||
key = "trait:" + entry.Id + ":gains:" + EventFeedUtil.SafeId(unit);
|
||||
label = EventFeedUtil.SafeName(unit) + " gains " + entry.Id;
|
||||
label = EventReason.Trait(unit, entry.Id, gained: true);
|
||||
strength = entry.EventStrength;
|
||||
category = entry.Category;
|
||||
source = "trait";
|
||||
|
|
@ -2733,7 +2850,7 @@ public static class AgentHarness
|
|||
}
|
||||
|
||||
key = "meta:" + id + ":" + EventFeedUtil.SafeId(unit);
|
||||
label = "Meta: " + id + " (" + EventFeedUtil.SafeName(unit) + ")";
|
||||
label = EventReason.MetaNew(unit, id);
|
||||
strength = 74f;
|
||||
category = "Politics";
|
||||
source = "meta";
|
||||
|
|
@ -2741,16 +2858,16 @@ public static class AgentHarness
|
|||
case "boat":
|
||||
id = "boat_unload";
|
||||
key = "boat:boat_unload:" + EventFeedUtil.SafeId(unit);
|
||||
label = "Boat unloads: " + EventFeedUtil.SafeName(unit);
|
||||
label = EventReason.Boat(unit, "unload");
|
||||
strength = 72f;
|
||||
category = "Travel";
|
||||
source = "boat";
|
||||
break;
|
||||
case "building":
|
||||
id = "building_damage";
|
||||
key = "building:building_damage:" + EventFeedUtil.SafeId(unit);
|
||||
label = "Damages a building: " + EventFeedUtil.SafeName(unit);
|
||||
strength = 70f;
|
||||
id = "building_destroy";
|
||||
key = "building:destroy:harness:" + EventFeedUtil.SafeId(unit);
|
||||
label = EventReason.BuildingFalls("harness_hall", unit);
|
||||
strength = 88f;
|
||||
category = "Spectacle";
|
||||
source = "building";
|
||||
break;
|
||||
|
|
@ -3567,6 +3684,41 @@ public static class AgentHarness
|
|||
detail = $"want='{want}' found='{found}' pending={InterestRegistry.PendingCount}";
|
||||
break;
|
||||
}
|
||||
case "interest_label_contains":
|
||||
{
|
||||
string want = (cmd.value ?? cmd.expect ?? "").Trim();
|
||||
bool have = false;
|
||||
string found = "";
|
||||
if (!string.IsNullOrEmpty(want))
|
||||
{
|
||||
string currentLabel = InterestDirector.CurrentLabel ?? "";
|
||||
if (currentLabel.IndexOf(want, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
have = true;
|
||||
found = currentLabel;
|
||||
}
|
||||
else
|
||||
{
|
||||
var pending = new System.Collections.Generic.List<InterestCandidate>(64);
|
||||
InterestRegistry.CopyPending(pending);
|
||||
for (int i = 0; i < pending.Count; i++)
|
||||
{
|
||||
InterestCandidate c = pending[i];
|
||||
string label = c?.Label ?? "";
|
||||
if (label.IndexOf(want, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
have = true;
|
||||
found = label;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pass = have;
|
||||
detail = $"want='{want}' found='{found}' pending={InterestRegistry.PendingCount}";
|
||||
break;
|
||||
}
|
||||
case "interest_no_key":
|
||||
{
|
||||
string want = (cmd.value ?? cmd.expect ?? "").Trim();
|
||||
|
|
@ -5744,8 +5896,14 @@ public static class AgentHarness
|
|||
|
||||
private static void DoSnapshot(HarnessCommand cmd)
|
||||
{
|
||||
string drops = InterestDropLog.FormatRecent(6);
|
||||
string snap =
|
||||
$"idle={SpectatorMode.Active} focus={MoveCamera.hasFocusUnit()} powerBar={CanvasMain.isBottomBarShowing()} unit={FocusLabel()} tip={CameraDirector.LastWatchLabel} tipAsset={CameraDirector.LastWatchAssetId} bad={StateProbe.BadEventCount} score={InterestDirector.CurrentScoreLabel} discoveryPending={SpeciesDiscovery.PendingCount} caption={WatchCaption.LastHeadline} chronicle={Chronicle.Count}";
|
||||
$"idle={SpectatorMode.Active} focus={MoveCamera.hasFocusUnit()} powerBar={CanvasMain.isBottomBarShowing()} unit={FocusLabel()} tip={CameraDirector.LastWatchLabel} tipAsset={CameraDirector.LastWatchAssetId} bad={StateProbe.BadEventCount} score={InterestDirector.CurrentScoreLabel} discoveryPending={SpeciesDiscovery.PendingCount} caption={WatchCaption.LastHeadline} chronicle={Chronicle.Count} drops={InterestDropLog.Count}";
|
||||
if (!string.IsNullOrEmpty(drops))
|
||||
{
|
||||
snap += " recentDrop=[" + drops + "]";
|
||||
}
|
||||
|
||||
_cmdOk++;
|
||||
Emit(cmd, ok: true, detail: snap);
|
||||
LogService.LogInfo("[IdleSpectator][SNAP] " + snap);
|
||||
|
|
@ -6125,6 +6283,7 @@ public static class AgentHarness
|
|||
long focusId = MoveCamera._focus_unit.getID();
|
||||
if (focusId != 0)
|
||||
{
|
||||
_activitySubjectId = focusId;
|
||||
return focusId;
|
||||
}
|
||||
}
|
||||
|
|
@ -6134,6 +6293,11 @@ public static class AgentHarness
|
|||
// ignore
|
||||
}
|
||||
|
||||
if (_activitySubjectId != 0)
|
||||
{
|
||||
return _activitySubjectId;
|
||||
}
|
||||
|
||||
return WatchCaption.CurrentUnitId;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -230,16 +230,58 @@ public static class DomainEventHarness
|
|||
|
||||
CatalogCoverageAudit.WriteReport(outputDirectory, "domain_event_audit.tsv", tsv.ToString());
|
||||
|
||||
// Camera decisions must have authored ActionPhrase + EventStrength (event-catalog.json).
|
||||
int decisionProseMissing = 0;
|
||||
int decisionStrengthMissing = 0;
|
||||
int decisionProseTotal = 0;
|
||||
List<string> liveDecisions = ActivityAssetCatalog.EnumerateLiveDecisionIds();
|
||||
for (int i = 0; i < liveDecisions.Count; i++)
|
||||
{
|
||||
string id = liveDecisions[i];
|
||||
if (!EventCatalog.Decision.IsCameraWorthy(id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
decisionProseTotal++;
|
||||
DiscreteEventEntry entry = EventCatalog.Decision.GetOrFallback(id);
|
||||
if (!EventCatalog.Decision.TryGetAuthoredActionPhrase(id, out _))
|
||||
{
|
||||
decisionProseMissing++;
|
||||
tsv.Append("decision_prose\t").Append(id).Append("\t0\t1\t\tmissing_action_phrase").AppendLine();
|
||||
}
|
||||
else if (entry.EventStrength <= 0f)
|
||||
{
|
||||
decisionStrengthMissing++;
|
||||
tsv.Append("decision_prose\t").Append(id).Append("\t0\t1\t")
|
||||
.Append(entry.EventStrength.ToString("0.#"))
|
||||
.Append("\tmissing_strength").AppendLine();
|
||||
}
|
||||
else
|
||||
{
|
||||
string phrase = EventCatalog.Decision.ActionPhraseFor(id);
|
||||
tsv.Append("decision_prose\t").Append(id).Append("\t1\t1\t")
|
||||
.Append(entry.EventStrength.ToString("0.#")).Append('\t')
|
||||
.Append(phrase).AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
CatalogCoverageAudit.WriteReport(outputDirectory, "domain_event_audit.tsv", tsv.ToString());
|
||||
|
||||
bool passed = plots.Passed && eras.Passed && books.Passed && traits.Passed
|
||||
&& spells.Passed && powers.Passed && decisions.Passed && items.Passed
|
||||
&& subspTraits.Passed && genes.Passed && phenotypes.Passed
|
||||
&& worldLaws.Passed && biomes.Passed
|
||||
&& relMissing == 0 && relTotal > 0;
|
||||
&& relMissing == 0 && relTotal > 0
|
||||
&& decisionProseMissing == 0 && decisionStrengthMissing == 0
|
||||
&& decisionProseTotal > 0;
|
||||
string detail =
|
||||
$"{plots.Detail}; {eras.Detail}; {books.Detail}; {traits.Detail}; "
|
||||
+ $"{spells.Detail}; {powers.Detail}; {decisions.Detail}; {items.Detail}; "
|
||||
+ $"{subspTraits.Detail}; {genes.Detail}; {phenotypes.Detail}; "
|
||||
+ $"{worldLaws.Detail}; {biomes.Detail}; relationship: authored={relTotal} missing={relMissing}";
|
||||
+ $"{worldLaws.Detail}; {biomes.Detail}; relationship: authored={relTotal} missing={relMissing}; "
|
||||
+ $"decision_prose: camera={decisionProseTotal} missing={decisionProseMissing} "
|
||||
+ $"strengthMissing={decisionStrengthMissing} catalog={EventCatalogConfig.LoadedFromFile}";
|
||||
return new DomainEventAuditResult { Passed = passed, Detail = detail };
|
||||
}
|
||||
|
||||
|
|
|
|||
726
IdleSpectator/EventCatalogConfig.cs
Normal file
726
IdleSpectator/EventCatalogConfig.cs
Normal file
|
|
@ -0,0 +1,726 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using NeoModLoader.services;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>One decision row from event-catalog.json (action phrase + EventStrength).</summary>
|
||||
public sealed class DecisionCatalogRow
|
||||
{
|
||||
public string Id = "";
|
||||
public string Action = "";
|
||||
public float Strength;
|
||||
}
|
||||
|
||||
/// <summary>Library dial defaults from event-catalog.json <c>libraries</c>.</summary>
|
||||
public sealed class LibraryCatalogDefaults
|
||||
{
|
||||
public float AmbientStrength = 40f;
|
||||
public float SignalStrength = 80f;
|
||||
public string Category = "Event";
|
||||
public string Label = "{a} · {id}";
|
||||
public bool AmbientCreatesInterest = true;
|
||||
public readonly HashSet<string> SignalIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads event-catalog.json — SoT for per-event strength/prose overlays.
|
||||
/// <c>action</c> is decision-only (clause after "decides to"); other domains use <c>label</c> / happiness variants.
|
||||
/// </summary>
|
||||
public static class EventCatalogConfig
|
||||
{
|
||||
public const string FileName = "event-catalog.json";
|
||||
public const float DefaultDecisionStrength = 86f;
|
||||
|
||||
private static readonly Dictionary<string, DecisionCatalogRow> Decisions =
|
||||
new Dictionary<string, DecisionCatalogRow>(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly Dictionary<string, float> SpeciesBonuses =
|
||||
new Dictionary<string, float>(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly Dictionary<string, float> CharacterBonuses =
|
||||
new Dictionary<string, float>(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly Dictionary<string, HappinessCatalogEntry> Happiness =
|
||||
new Dictionary<string, HappinessCatalogEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly Dictionary<string, StatusInterestEntry> Status =
|
||||
new Dictionary<string, StatusInterestEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly Dictionary<string, WorldLogEventEntry> WorldLog =
|
||||
new Dictionary<string, WorldLogEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Plots =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Relationship =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Traits =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Books =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Eras =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly Dictionary<string, DisasterInterestEntry> Disasters =
|
||||
new Dictionary<string, DisasterInterestEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly Dictionary<string, WarTypeInterestEntry> WarTypes =
|
||||
new Dictionary<string, WarTypeInterestEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly Dictionary<string, LibraryCatalogDefaults> Libraries =
|
||||
new Dictionary<string, LibraryCatalogDefaults>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static string _loadedPath = "";
|
||||
private static string _loadedVersion = "";
|
||||
private static bool _loadedFromFile;
|
||||
private static int _generation;
|
||||
|
||||
public static bool LoadedFromFile => _loadedFromFile;
|
||||
public static string LoadedPath => _loadedPath;
|
||||
public static string LoadedVersion => _loadedVersion;
|
||||
public static int Generation => _generation;
|
||||
|
||||
public static IEnumerable<string> DecisionIds => Decisions.Keys;
|
||||
public static int DecisionCount => Decisions.Count;
|
||||
public static int SpeciesCount => SpeciesBonuses.Count;
|
||||
public static int CharacterCount => CharacterBonuses.Count;
|
||||
public static int HappinessCount => Happiness.Count;
|
||||
public static int StatusCount => Status.Count;
|
||||
public static int WorldLogCount => WorldLog.Count;
|
||||
public static int PlotCount => Plots.Count;
|
||||
public static int RelationshipCount => Relationship.Count;
|
||||
public static int TraitCount => Traits.Count;
|
||||
public static int BookCount => Books.Count;
|
||||
public static int EraCount => Eras.Count;
|
||||
public static int DisasterCount => Disasters.Count;
|
||||
public static int WarTypeCount => WarTypes.Count;
|
||||
public static int LibraryCount => Libraries.Count;
|
||||
|
||||
public static void LoadFromModFolder(string modFolder)
|
||||
{
|
||||
ClearAll();
|
||||
_loadedFromFile = false;
|
||||
_loadedPath = "";
|
||||
_loadedVersion = "";
|
||||
|
||||
if (string.IsNullOrEmpty(modFolder))
|
||||
{
|
||||
LogService.LogInfo("[IdleSpectator] event-catalog.json: no mod folder, using C# fallbacks");
|
||||
BumpAndInvalidate();
|
||||
return;
|
||||
}
|
||||
|
||||
string path = Path.Combine(modFolder, FileName);
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
LogService.LogInfo($"[IdleSpectator] event-catalog.json missing at {path}, using C# fallbacks");
|
||||
BumpAndInvalidate();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string json = File.ReadAllText(path);
|
||||
if (!TryParseDocument(json, out string version))
|
||||
{
|
||||
LogService.LogInfo("[IdleSpectator] event-catalog.json parse failed, using C# fallbacks");
|
||||
ClearAll();
|
||||
BumpAndInvalidate();
|
||||
return;
|
||||
}
|
||||
|
||||
_loadedFromFile = Happiness.Count > 0 || Decisions.Count > 0 || Status.Count > 0;
|
||||
_loadedPath = path;
|
||||
_loadedVersion = version ?? "";
|
||||
BumpAndInvalidate();
|
||||
LogService.LogInfo(
|
||||
$"[IdleSpectator] event-catalog.json loaded v={_loadedVersion} "
|
||||
+ $"happiness={Happiness.Count} status={Status.Count} worldLog={WorldLog.Count} "
|
||||
+ $"plots={Plots.Count} relationship={Relationship.Count} traits={Traits.Count} "
|
||||
+ $"books={Books.Count} eras={Eras.Count} disasters={Disasters.Count} "
|
||||
+ $"warTypes={WarTypes.Count} decisions={Decisions.Count} libraries={Libraries.Count} "
|
||||
+ $"species={SpeciesBonuses.Count} character={CharacterBonuses.Count}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ClearAll();
|
||||
LogService.LogInfo($"[IdleSpectator] event-catalog.json failed ({ex.Message}), using C# fallbacks");
|
||||
BumpAndInvalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ClearAll()
|
||||
{
|
||||
Decisions.Clear();
|
||||
SpeciesBonuses.Clear();
|
||||
CharacterBonuses.Clear();
|
||||
Happiness.Clear();
|
||||
Status.Clear();
|
||||
WorldLog.Clear();
|
||||
Plots.Clear();
|
||||
Relationship.Clear();
|
||||
Traits.Clear();
|
||||
Books.Clear();
|
||||
Eras.Clear();
|
||||
Disasters.Clear();
|
||||
WarTypes.Clear();
|
||||
Libraries.Clear();
|
||||
}
|
||||
|
||||
private static void BumpAndInvalidate()
|
||||
{
|
||||
_generation++;
|
||||
EventCatalog.InvalidateAllOverlays();
|
||||
}
|
||||
|
||||
internal static bool TryParseDocument(string json, out string version)
|
||||
{
|
||||
version = "";
|
||||
if (string.IsNullOrEmpty(json))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
version = ExtractJsonString(json, "modVersion") ?? "";
|
||||
if (TryExtractArray(json, "decisions", out string a)) IngestDecisionObjects(a);
|
||||
if (TryExtractArray(json, "species", out a)) IngestBonusObjects(a, SpeciesBonuses);
|
||||
if (TryExtractArray(json, "character", out a)) IngestBonusObjects(a, CharacterBonuses);
|
||||
if (TryExtractArray(json, "happiness", out a)) IngestHappiness(a);
|
||||
if (TryExtractArray(json, "status", out a)) IngestStatus(a);
|
||||
if (TryExtractArray(json, "worldLog", out a)) IngestWorldLog(a);
|
||||
if (TryExtractArray(json, "plots", out a)) IngestDiscrete(a, Plots);
|
||||
if (TryExtractArray(json, "relationship", out a)) IngestDiscrete(a, Relationship);
|
||||
if (TryExtractArray(json, "traits", out a)) IngestDiscrete(a, Traits);
|
||||
if (TryExtractArray(json, "books", out a)) IngestDiscrete(a, Books);
|
||||
if (TryExtractArray(json, "eras", out a)) IngestDiscrete(a, Eras);
|
||||
if (TryExtractArray(json, "disasters", out a)) IngestDisasters(a);
|
||||
if (TryExtractArray(json, "warTypes", out a)) IngestWarTypes(a);
|
||||
IngestLibraries(json);
|
||||
|
||||
return Happiness.Count > 0 || Decisions.Count > 0 || Status.Count > 0 || Plots.Count > 0;
|
||||
}
|
||||
|
||||
public static int FillHappiness(Dictionary<string, HappinessCatalogEntry> into)
|
||||
{
|
||||
into.Clear();
|
||||
foreach (KeyValuePair<string, HappinessCatalogEntry> kv in Happiness)
|
||||
{
|
||||
into[kv.Key] = kv.Value;
|
||||
}
|
||||
return into.Count;
|
||||
}
|
||||
|
||||
public static int FillStatus(Dictionary<string, StatusInterestEntry> into)
|
||||
{
|
||||
into.Clear();
|
||||
foreach (KeyValuePair<string, StatusInterestEntry> kv in Status)
|
||||
{
|
||||
into[kv.Key] = kv.Value;
|
||||
}
|
||||
return into.Count;
|
||||
}
|
||||
|
||||
public static int FillWorldLog(Dictionary<string, WorldLogEventEntry> into)
|
||||
{
|
||||
into.Clear();
|
||||
foreach (KeyValuePair<string, WorldLogEventEntry> kv in WorldLog)
|
||||
{
|
||||
into[kv.Key] = kv.Value;
|
||||
}
|
||||
return into.Count;
|
||||
}
|
||||
|
||||
public static int FillPlots(Dictionary<string, DiscreteEventEntry> into) => FillDiscrete(Plots, into);
|
||||
public static int FillRelationship(Dictionary<string, DiscreteEventEntry> into) => FillDiscrete(Relationship, into);
|
||||
public static int FillTraits(Dictionary<string, DiscreteEventEntry> into) => FillDiscrete(Traits, into);
|
||||
public static int FillBooks(Dictionary<string, DiscreteEventEntry> into) => FillDiscrete(Books, into);
|
||||
public static int FillEras(Dictionary<string, DiscreteEventEntry> into) => FillDiscrete(Eras, into);
|
||||
|
||||
public static int FillDisasters(Dictionary<string, DisasterInterestEntry> into)
|
||||
{
|
||||
into.Clear();
|
||||
foreach (KeyValuePair<string, DisasterInterestEntry> kv in Disasters)
|
||||
{
|
||||
into[kv.Key] = kv.Value;
|
||||
}
|
||||
return into.Count;
|
||||
}
|
||||
|
||||
public static int FillWarTypes(Dictionary<string, WarTypeInterestEntry> into)
|
||||
{
|
||||
into.Clear();
|
||||
foreach (KeyValuePair<string, WarTypeInterestEntry> kv in WarTypes)
|
||||
{
|
||||
into[kv.Key] = kv.Value;
|
||||
}
|
||||
return into.Count;
|
||||
}
|
||||
|
||||
private static int FillDiscrete(
|
||||
Dictionary<string, DiscreteEventEntry> src,
|
||||
Dictionary<string, DiscreteEventEntry> into)
|
||||
{
|
||||
into.Clear();
|
||||
foreach (KeyValuePair<string, DiscreteEventEntry> kv in src)
|
||||
{
|
||||
into[kv.Key] = kv.Value;
|
||||
}
|
||||
return into.Count;
|
||||
}
|
||||
|
||||
public static bool TryGetLibraryDefaults(string key, out LibraryCatalogDefaults defaults)
|
||||
{
|
||||
defaults = null;
|
||||
if (string.IsNullOrEmpty(key))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return Libraries.TryGetValue(key.Trim(), out defaults) && defaults != null;
|
||||
}
|
||||
|
||||
private static void IngestHappiness(string arrayJson)
|
||||
{
|
||||
foreach (string obj in EnumerateObjects(arrayJson))
|
||||
{
|
||||
string id = ExtractJsonString(obj, "id");
|
||||
if (string.IsNullOrEmpty(id)) continue;
|
||||
var entry = new HappinessCatalogEntry
|
||||
{
|
||||
Id = id.Trim(),
|
||||
EventStrength = ExtractJsonFloat(obj, "strength", 40f),
|
||||
Presentation = ParseEnum(obj, "presentation", HappinessPresentationTier.Signal),
|
||||
Context = ParseEnum(obj, "context", HappinessContextRequirement.None),
|
||||
Life = ParseEnum(obj, "life", HappinessLifePolicy.ActivityOnly),
|
||||
Overlap = ParseEnum(obj, "overlap", HappinessOverlapPolicy.Independent),
|
||||
RelationRole = ParseEnum(obj, "relationRole", HappinessRelationRole.None),
|
||||
InterestCategory = ExtractJsonString(obj, "category") ?? "",
|
||||
CanonicalMilestoneKey = ExtractJsonString(obj, "canonicalMilestoneKey") ?? "",
|
||||
StatusOverlapId = ExtractJsonString(obj, "statusOverlapId") ?? "",
|
||||
VariantsWithRelated = ExtractStringArray(obj, "variantsWithRelated"),
|
||||
VariantsWithoutRelated = ExtractStringArray(obj, "variantsWithoutRelated")
|
||||
};
|
||||
Happiness[entry.Id] = entry;
|
||||
}
|
||||
}
|
||||
|
||||
private static void IngestStatus(string arrayJson)
|
||||
{
|
||||
foreach (string obj in EnumerateObjects(arrayJson))
|
||||
{
|
||||
string id = ExtractJsonString(obj, "id");
|
||||
if (string.IsNullOrEmpty(id)) continue;
|
||||
Status[id.Trim()] = new StatusInterestEntry
|
||||
{
|
||||
Id = id.Trim(),
|
||||
EventStrength = ExtractJsonFloat(obj, "strength", 40f),
|
||||
Category = ExtractJsonString(obj, "category") ?? "Status",
|
||||
CreatesInterest = ExtractJsonBool(obj, "camera", false),
|
||||
ExtendsGrief = ExtractJsonBool(obj, "extendsGrief", false)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static void IngestWorldLog(string arrayJson)
|
||||
{
|
||||
foreach (string obj in EnumerateObjects(arrayJson))
|
||||
{
|
||||
string id = ExtractJsonString(obj, "id");
|
||||
if (string.IsNullOrEmpty(id)) continue;
|
||||
bool camera = ExtractJsonBool(obj, "camera", true);
|
||||
float strength = ExtractJsonFloat(obj, "strength", 40f);
|
||||
bool chronicle = ExtractJsonBool(obj, "chronicle", camera && strength >= 65f);
|
||||
WorldLog[id.Trim()] = new WorldLogEventEntry
|
||||
{
|
||||
Id = id.Trim(),
|
||||
EventStrength = strength,
|
||||
Category = ExtractJsonString(obj, "category") ?? "WorldLog",
|
||||
CreatesInterest = camera,
|
||||
ChronicleEligible = chronicle,
|
||||
LabelTemplate = ExtractJsonString(obj, "label") ?? "{id}: {a}",
|
||||
MinWatch = ExtractJsonFloat(obj, "minWatch", 6f),
|
||||
MaxWatch = ExtractJsonFloat(obj, "maxWatch", 28f)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static void IngestDiscrete(string arrayJson, Dictionary<string, DiscreteEventEntry> into)
|
||||
{
|
||||
foreach (string obj in EnumerateObjects(arrayJson))
|
||||
{
|
||||
string id = ExtractJsonString(obj, "id");
|
||||
if (string.IsNullOrEmpty(id)) continue;
|
||||
into[id.Trim()] = new DiscreteEventEntry
|
||||
{
|
||||
Id = id.Trim(),
|
||||
EventStrength = ExtractJsonFloat(obj, "strength", 40f),
|
||||
Category = ExtractJsonString(obj, "category") ?? "Event",
|
||||
CreatesInterest = ExtractJsonBool(obj, "camera", true),
|
||||
LabelTemplate = ExtractJsonString(obj, "label") ?? "{a} · {id}",
|
||||
ActionPhrase = ExtractJsonString(obj, "action") ?? ""
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static void IngestDisasters(string arrayJson)
|
||||
{
|
||||
foreach (string obj in EnumerateObjects(arrayJson))
|
||||
{
|
||||
string id = ExtractJsonString(obj, "id");
|
||||
if (string.IsNullOrEmpty(id)) continue;
|
||||
Disasters[id.Trim()] = new DisasterInterestEntry
|
||||
{
|
||||
Id = id.Trim(),
|
||||
EventStrength = ExtractJsonFloat(obj, "strength", 90f),
|
||||
Category = ExtractJsonString(obj, "category") ?? "Disaster",
|
||||
CreatesInterest = ExtractJsonBool(obj, "camera", true),
|
||||
WorldLogId = ExtractJsonString(obj, "worldLogId") ?? ""
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static void IngestWarTypes(string arrayJson)
|
||||
{
|
||||
foreach (string obj in EnumerateObjects(arrayJson))
|
||||
{
|
||||
string id = ExtractJsonString(obj, "id");
|
||||
if (string.IsNullOrEmpty(id)) continue;
|
||||
WarTypes[id.Trim()] = new WarTypeInterestEntry
|
||||
{
|
||||
Id = id.Trim(),
|
||||
EventStrength = ExtractJsonFloat(obj, "strength", 85f),
|
||||
Category = ExtractJsonString(obj, "category") ?? "Politics",
|
||||
CreatesInterest = ExtractJsonBool(obj, "camera", true),
|
||||
LabelTemplate = ExtractJsonString(obj, "label") ?? "War ({id})"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static void IngestLibraries(string json)
|
||||
{
|
||||
if (!TryExtractObject(json, "libraries", out string libsJson))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string[] keys =
|
||||
{
|
||||
"spell", "power", "item", "gene", "phenotype", "worldLaw", "subspeciesTrait", "biome"
|
||||
};
|
||||
for (int i = 0; i < keys.Length; i++)
|
||||
{
|
||||
string key = keys[i];
|
||||
if (!TryExtractObject(libsJson, key, out string obj))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var d = new LibraryCatalogDefaults
|
||||
{
|
||||
AmbientStrength = ExtractJsonFloat(obj, "ambientStrength", 40f),
|
||||
SignalStrength = ExtractJsonFloat(obj, "signalStrength", 80f),
|
||||
Category = ExtractJsonString(obj, "category") ?? "Event",
|
||||
Label = ExtractJsonString(obj, "label") ?? "{a} · {id}",
|
||||
AmbientCreatesInterest = ExtractJsonBool(obj, "ambientCreatesInterest", true)
|
||||
};
|
||||
string[] ids = ExtractStringArray(obj, "signalIds");
|
||||
for (int j = 0; j < ids.Length; j++)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(ids[j]))
|
||||
{
|
||||
d.SignalIds.Add(ids[j]);
|
||||
}
|
||||
}
|
||||
Libraries[key] = d;
|
||||
}
|
||||
}
|
||||
|
||||
private static int IngestDecisionObjects(string arrayJson)
|
||||
{
|
||||
int n = 0;
|
||||
foreach (string obj in EnumerateObjects(arrayJson))
|
||||
{
|
||||
string id = ExtractJsonString(obj, "id");
|
||||
string action = ExtractJsonString(obj, "action");
|
||||
if (string.IsNullOrEmpty(id) || string.IsNullOrEmpty(action)) continue;
|
||||
float strength = ExtractJsonFloat(obj, "strength", DefaultDecisionStrength);
|
||||
Decisions[id.Trim()] = new DecisionCatalogRow
|
||||
{
|
||||
Id = id.Trim(),
|
||||
Action = action,
|
||||
Strength = strength > 0f ? strength : DefaultDecisionStrength
|
||||
};
|
||||
n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
private static int IngestBonusObjects(string arrayJson, Dictionary<string, float> into)
|
||||
{
|
||||
int n = 0;
|
||||
foreach (string obj in EnumerateObjects(arrayJson))
|
||||
{
|
||||
string id = ExtractJsonString(obj, "id");
|
||||
if (string.IsNullOrEmpty(id)) continue;
|
||||
into[id.Trim()] = ExtractJsonFloat(obj, "bonus", 0f);
|
||||
n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
private static TEnum ParseEnum<TEnum>(string json, string key, TEnum fallback) where TEnum : struct
|
||||
{
|
||||
string raw = ExtractJsonString(json, key);
|
||||
if (string.IsNullOrEmpty(raw)) return fallback;
|
||||
return Enum.TryParse(raw, true, out TEnum value) ? value : fallback;
|
||||
}
|
||||
|
||||
private static string[] ExtractStringArray(string json, string key)
|
||||
{
|
||||
if (!TryExtractArray(json, key, out string arr) || string.IsNullOrEmpty(arr))
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
var list = new List<string>();
|
||||
int i = 0;
|
||||
while (i < arr.Length)
|
||||
{
|
||||
int q = arr.IndexOf('"', i);
|
||||
if (q < 0) break;
|
||||
int end = q + 1;
|
||||
var sb = new StringBuilder();
|
||||
while (end < arr.Length)
|
||||
{
|
||||
char c = arr[end++];
|
||||
if (c == '\\' && end < arr.Length) { sb.Append(arr[end++]); continue; }
|
||||
if (c == '"') break;
|
||||
sb.Append(c);
|
||||
}
|
||||
list.Add(sb.ToString());
|
||||
i = end;
|
||||
}
|
||||
return list.ToArray();
|
||||
}
|
||||
|
||||
private static bool ExtractJsonBool(string json, string key, bool fallback)
|
||||
{
|
||||
string needle = "\"" + key + "\"";
|
||||
int keyAt = json.IndexOf(needle, StringComparison.Ordinal);
|
||||
if (keyAt < 0) return fallback;
|
||||
int colon = json.IndexOf(':', keyAt + needle.Length);
|
||||
if (colon < 0) return fallback;
|
||||
int i = colon + 1;
|
||||
while (i < json.Length && char.IsWhiteSpace(json[i])) i++;
|
||||
if (i + 4 <= json.Length && string.Compare(json, i, "true", 0, 4, StringComparison.OrdinalIgnoreCase) == 0)
|
||||
return true;
|
||||
if (i + 5 <= json.Length && string.Compare(json, i, "false", 0, 5, StringComparison.OrdinalIgnoreCase) == 0)
|
||||
return false;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateObjects(string arrayJson)
|
||||
{
|
||||
if (string.IsNullOrEmpty(arrayJson)) yield break;
|
||||
int i = 0;
|
||||
while (i < arrayJson.Length)
|
||||
{
|
||||
int start = arrayJson.IndexOf('{', i);
|
||||
if (start < 0) yield break;
|
||||
int depth = 0;
|
||||
bool inString = false;
|
||||
bool escape = false;
|
||||
for (int j = start; j < arrayJson.Length; j++)
|
||||
{
|
||||
char c = arrayJson[j];
|
||||
if (inString)
|
||||
{
|
||||
if (escape) { escape = false; continue; }
|
||||
if (c == '\\') { escape = true; continue; }
|
||||
if (c == '"') inString = false;
|
||||
continue;
|
||||
}
|
||||
if (c == '"') { inString = true; continue; }
|
||||
if (c == '{') depth++;
|
||||
else if (c == '}')
|
||||
{
|
||||
depth--;
|
||||
if (depth == 0)
|
||||
{
|
||||
yield return arrayJson.Substring(start, j - start + 1);
|
||||
i = j + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (depth != 0) yield break;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ExtractJsonString(string json, string key)
|
||||
{
|
||||
string needle = "\"" + key + "\"";
|
||||
int keyAt = json.IndexOf(needle, StringComparison.Ordinal);
|
||||
if (keyAt < 0) return null;
|
||||
int colon = json.IndexOf(':', keyAt + needle.Length);
|
||||
if (colon < 0) return null;
|
||||
int i = colon + 1;
|
||||
while (i < json.Length && char.IsWhiteSpace(json[i])) i++;
|
||||
if (i >= json.Length || json[i] != '"') return null;
|
||||
i++;
|
||||
var sb = new StringBuilder();
|
||||
while (i < json.Length)
|
||||
{
|
||||
char c = json[i++];
|
||||
if (c == '\\' && i < json.Length) { sb.Append(json[i++]); continue; }
|
||||
if (c == '"') break;
|
||||
sb.Append(c);
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static float ExtractJsonFloat(string json, string key, float fallback)
|
||||
{
|
||||
string needle = "\"" + key + "\"";
|
||||
int keyAt = json.IndexOf(needle, StringComparison.Ordinal);
|
||||
if (keyAt < 0) return fallback;
|
||||
int colon = json.IndexOf(':', keyAt + needle.Length);
|
||||
if (colon < 0) return fallback;
|
||||
int i = colon + 1;
|
||||
while (i < json.Length && char.IsWhiteSpace(json[i])) i++;
|
||||
int start = i;
|
||||
while (i < json.Length && (char.IsDigit(json[i]) || json[i] == '.' || json[i] == '-' || json[i] == '+'))
|
||||
i++;
|
||||
if (i <= start) return fallback;
|
||||
string token = json.Substring(start, i - start);
|
||||
return float.TryParse(token, NumberStyles.Float, CultureInfo.InvariantCulture, out float value)
|
||||
? value : fallback;
|
||||
}
|
||||
|
||||
private static bool TryExtractArray(string json, string key, out string arrayJson)
|
||||
{
|
||||
arrayJson = null;
|
||||
string keyColon = "\"" + key + "\"";
|
||||
int searchFrom = 0;
|
||||
int keyAt = -1;
|
||||
while (true)
|
||||
{
|
||||
int at = json.IndexOf(keyColon, searchFrom, StringComparison.Ordinal);
|
||||
if (at < 0) break;
|
||||
int after = at + keyColon.Length;
|
||||
while (after < json.Length && char.IsWhiteSpace(json[after])) after++;
|
||||
if (after < json.Length && json[after] == ':') { keyAt = at; break; }
|
||||
searchFrom = at + 1;
|
||||
}
|
||||
if (keyAt < 0) return false;
|
||||
int bracket = json.IndexOf('[', keyAt + keyColon.Length);
|
||||
if (bracket < 0) return false;
|
||||
int depth = 0;
|
||||
bool inString = false;
|
||||
bool escape = false;
|
||||
for (int i = bracket; i < json.Length; i++)
|
||||
{
|
||||
char c = json[i];
|
||||
if (inString)
|
||||
{
|
||||
if (escape) { escape = false; continue; }
|
||||
if (c == '\\') { escape = true; continue; }
|
||||
if (c == '"') inString = false;
|
||||
continue;
|
||||
}
|
||||
if (c == '"') { inString = true; continue; }
|
||||
if (c == '[') depth++;
|
||||
else if (c == ']')
|
||||
{
|
||||
depth--;
|
||||
if (depth == 0)
|
||||
{
|
||||
arrayJson = json.Substring(bracket, i - bracket + 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryExtractObject(string json, string key, out string objectJson)
|
||||
{
|
||||
objectJson = null;
|
||||
string keyColon = "\"" + key + "\"";
|
||||
int searchFrom = 0;
|
||||
int keyAt = -1;
|
||||
while (true)
|
||||
{
|
||||
int at = json.IndexOf(keyColon, searchFrom, StringComparison.Ordinal);
|
||||
if (at < 0) break;
|
||||
int after = at + keyColon.Length;
|
||||
while (after < json.Length && char.IsWhiteSpace(json[after])) after++;
|
||||
if (after < json.Length && json[after] == ':') { keyAt = at; break; }
|
||||
searchFrom = at + 1;
|
||||
}
|
||||
if (keyAt < 0) return false;
|
||||
int brace = json.IndexOf('{', keyAt + keyColon.Length);
|
||||
if (brace < 0) return false;
|
||||
int depth = 0;
|
||||
bool inString = false;
|
||||
bool escape = false;
|
||||
for (int i = brace; i < json.Length; i++)
|
||||
{
|
||||
char c = json[i];
|
||||
if (inString)
|
||||
{
|
||||
if (escape) { escape = false; continue; }
|
||||
if (c == '\\') { escape = true; continue; }
|
||||
if (c == '"') inString = false;
|
||||
continue;
|
||||
}
|
||||
if (c == '"') { inString = true; continue; }
|
||||
if (c == '{') depth++;
|
||||
else if (c == '}')
|
||||
{
|
||||
depth--;
|
||||
if (depth == 0)
|
||||
{
|
||||
objectJson = json.Substring(brace, i - brace + 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool TryGetDecision(string id, out DecisionCatalogRow row)
|
||||
{
|
||||
row = null;
|
||||
if (string.IsNullOrEmpty(id)) return false;
|
||||
return Decisions.TryGetValue(id.Trim(), out row) && row != null;
|
||||
}
|
||||
|
||||
public static bool TryGetDecisionAction(string id, out string action)
|
||||
{
|
||||
action = "";
|
||||
if (!TryGetDecision(id, out DecisionCatalogRow row)) return false;
|
||||
action = row.Action ?? "";
|
||||
return !string.IsNullOrEmpty(action);
|
||||
}
|
||||
|
||||
public static float DecisionStrengthOrDefault(string id, float fallback = DefaultDecisionStrength)
|
||||
{
|
||||
if (TryGetDecision(id, out DecisionCatalogRow row) && row.Strength > 0f) return row.Strength;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
public static float SpeciesBonus(string speciesId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(speciesId)) return 0f;
|
||||
return SpeciesBonuses.TryGetValue(speciesId.Trim(), out float bonus) ? bonus : 0f;
|
||||
}
|
||||
|
||||
public static float CharacterBonus(string roleId, float scoringModelFallback)
|
||||
{
|
||||
if (string.IsNullOrEmpty(roleId)) return scoringModelFallback;
|
||||
if (CharacterBonuses.TryGetValue(roleId.Trim(), out float bonus)) return bonus;
|
||||
return scoringModelFallback;
|
||||
}
|
||||
|
||||
public static bool Reload()
|
||||
{
|
||||
string folder = ModClass.ModFolder;
|
||||
if (string.IsNullOrEmpty(folder)) return false;
|
||||
LoadFromModFolder(folder);
|
||||
return _loadedFromFile;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,67 +3,66 @@ using System.Collections.Generic;
|
|||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>Authored interest overlay for every live book_types asset.</summary>
|
||||
/// <summary>Book dials from <c>event-catalog.json</c> <c>books</c>.</summary>
|
||||
public static partial class EventCatalog
|
||||
{
|
||||
public static class Book
|
||||
{
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
static Book()
|
||||
{
|
||||
Add("family_story", 52f, "Culture", "{a} writes a family story");
|
||||
Add("love_story", 58f, "Culture", "{a} writes a love story");
|
||||
Add("friendship_story", 50f, "Culture", "{a} writes a friendship story");
|
||||
Add("bad_story_about_king", 62f, "Culture", "{a} writes a scandalous book");
|
||||
Add("fable", 48f, "Culture", "{a} writes a fable");
|
||||
Add("warfare_manual", 60f, "Culture", "{a} writes a warfare manual");
|
||||
Add("economy_manual", 48f, "Culture", "{a} writes an economy manual");
|
||||
Add("stewardship_manual", 48f, "Culture", "{a} writes a stewardship manual");
|
||||
Add("diplomacy_manual", 55f, "Culture", "{a} writes a diplomacy manual");
|
||||
Add("mathbook", 45f, "Culture", "{a} writes a math book");
|
||||
Add("biology_book", 45f, "Culture", "{a} writes a biology book");
|
||||
Add("history_book", 55f, "Culture", "{a} writes a history book");
|
||||
}
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static void Add(
|
||||
string id,
|
||||
float strength,
|
||||
string category,
|
||||
string label)
|
||||
{
|
||||
Entries[id] = new DiscreteEventEntry
|
||||
private static int _appliedGeneration = -1;
|
||||
|
||||
public static void InvalidateOverlays()
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = strength,
|
||||
Category = category,
|
||||
CreatesInterest = true,
|
||||
LabelTemplate = label
|
||||
};
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds => Entries.Keys;
|
||||
|
||||
public static bool HasAuthored(string id) =>
|
||||
!string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim());
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry))
|
||||
{
|
||||
return entry;
|
||||
Entries.Clear();
|
||||
_appliedGeneration = -1;
|
||||
}
|
||||
|
||||
string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim();
|
||||
return new DiscreteEventEntry
|
||||
private static void Ensure()
|
||||
{
|
||||
Id = key,
|
||||
EventStrength = 45f,
|
||||
Category = "Culture",
|
||||
LabelTemplate = "{a} writes a book",
|
||||
IsFallback = true
|
||||
};
|
||||
if (_appliedGeneration == EventCatalogConfig.Generation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_appliedGeneration = EventCatalogConfig.Generation;
|
||||
EventCatalogConfig.FillBooks(Entries);
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool HasAuthored(string id)
|
||||
{
|
||||
Ensure();
|
||||
return !string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim());
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry))
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
|
||||
string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim();
|
||||
return new DiscreteEventEntry
|
||||
{
|
||||
Id = key,
|
||||
EventStrength = 45f,
|
||||
Category = "Culture",
|
||||
LabelTemplate = "{a} writes a book",
|
||||
CreatesInterest = true,
|
||||
IsFallback = true
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
406
IdleSpectator/Events/Catalogs/EventCatalog.Decision.cs
Normal file
406
IdleSpectator/Events/Catalogs/EventCatalog.Decision.cs
Normal file
|
|
@ -0,0 +1,406 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>
|
||||
/// Decision interest dials + authored action phrases.
|
||||
/// <para>
|
||||
/// <b>Source of truth:</b> <c>event-catalog.json</c> (<c>decisions</c> rows: action + strength).
|
||||
/// <see cref="BuiltinActionPhrases"/> is the offline fallback when that file is missing.
|
||||
/// Live ids still seed via <see cref="LiveLibraryInterest"/>; camera Signal uses
|
||||
/// <see cref="IsCameraWorthy"/>; missing ActionPhrase falls back to a generic humanize.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static partial class EventCatalog
|
||||
{
|
||||
public static class Decision
|
||||
{
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static int _appliedOverlayGeneration = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Offline fallback infinitive clauses when event-catalog.json is missing a row.
|
||||
/// Prefer editing <c>event-catalog.json</c> instead.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<string, string> BuiltinActionPhrases =
|
||||
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
// Social / crime
|
||||
["find_lover"] = "find a lover",
|
||||
["try_to_steal_money"] = "try to steal money",
|
||||
["banish_unruly_clan_members"] = "banish unruly clan members",
|
||||
["kill_unruly_clan_members"] = "kill unruly clan members",
|
||||
|
||||
// Plots
|
||||
["try_new_plot"] = "try a new plot",
|
||||
|
||||
// King kingdom policy
|
||||
["king_change_kingdom_culture"] = "change the kingdom's culture",
|
||||
["king_change_kingdom_language"] = "change the kingdom's language",
|
||||
["king_change_kingdom_religion"] = "change the kingdom's religion",
|
||||
|
||||
// City leader policy
|
||||
["leader_change_city_culture"] = "change the city's culture",
|
||||
["leader_change_city_language"] = "change the city's language",
|
||||
["leader_change_city_religion"] = "change the city's religion",
|
||||
|
||||
// Army / warrior (keep train-with-dummy authored for if we ever Signal it)
|
||||
["warrior_army_follow_leader"] = "follow their leader",
|
||||
["warrior_train_with_dummy"] = "train with a dummy",
|
||||
["warrior_try_join_army_group"] = "try to join an army",
|
||||
};
|
||||
|
||||
/// <summary>Drop cached rows so the next Ensure re-applies JSON overlays.</summary>
|
||||
public static void InvalidateOverlays()
|
||||
{
|
||||
Entries.Clear();
|
||||
_appliedOverlayGeneration = -1;
|
||||
}
|
||||
|
||||
private static bool IsInterestingDecision(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string s = id.ToLowerInvariant();
|
||||
// Pure AI ticks / daily fluff - not story beats.
|
||||
if (s.Contains("idle")
|
||||
|| s.Contains("walking")
|
||||
|| s.Contains("check")
|
||||
|| s.Contains("wait")
|
||||
|| s.Contains("sleep")
|
||||
|| s.Contains("random")
|
||||
|| s.Contains("play")
|
||||
|| s.Contains("jump")
|
||||
|| s.Contains("flip")
|
||||
|| s.Contains("diet_")
|
||||
|| s.Contains("move")
|
||||
|| s.Contains("swim")
|
||||
|| s.Contains("follow_parent")
|
||||
|| s.Contains("follow_desire")
|
||||
|| s.Contains("reflection")
|
||||
|| s.Contains("poop"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return HasToken(s, "war")
|
||||
|| s.Contains("rebellion")
|
||||
|| s.Contains("revolt")
|
||||
|| s.Contains("alliance")
|
||||
|| s.Contains("coup")
|
||||
|| s.Contains("betray")
|
||||
|| s.Contains("declare")
|
||||
|| s.Contains("invade")
|
||||
|| s.Contains("siege")
|
||||
|| s.Contains("found")
|
||||
|| s.Contains("city_foundation")
|
||||
|| s.Contains("army")
|
||||
|| s.Contains("lover")
|
||||
|| s.Contains("plot")
|
||||
|| s.Contains("marry")
|
||||
|| s.Contains("banish")
|
||||
|| s.Contains("steal")
|
||||
|| s.Contains("kill_unruly")
|
||||
|| HasToken(s, "king")
|
||||
|| HasToken(s, "leader")
|
||||
|| s.Contains("clan")
|
||||
|| s.Contains("religion")
|
||||
|| s.Contains("culture")
|
||||
|| s.Contains("baby_make")
|
||||
|| s.Contains("have_child")
|
||||
|| s.Contains("birth");
|
||||
}
|
||||
|
||||
/// <summary>Underscore-token match so <c>war</c> does not hit <c>warrior</c>.</summary>
|
||||
private static bool HasToken(string haystack, string token)
|
||||
{
|
||||
if (string.IsNullOrEmpty(haystack) || string.IsNullOrEmpty(token))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (haystack == token)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return haystack.StartsWith(token + "_", StringComparison.Ordinal)
|
||||
|| haystack.EndsWith("_" + token, StringComparison.Ordinal)
|
||||
|| haystack.Contains("_" + token + "_");
|
||||
}
|
||||
|
||||
/// <summary>True when a decision id should be allowed to own the idle camera.</summary>
|
||||
public static bool IsCameraWorthy(string id) => IsInterestingDecision(id);
|
||||
|
||||
/// <summary>Authored action-phrase ids (JSON catalog ∪ builtin fallback).</summary>
|
||||
public static IEnumerable<string> AuthoredActionPhraseIds
|
||||
{
|
||||
get
|
||||
{
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (string id in EventCatalogConfig.DecisionIds)
|
||||
{
|
||||
if (seen.Add(id))
|
||||
{
|
||||
yield return id;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string id in BuiltinActionPhrases.Keys)
|
||||
{
|
||||
if (seen.Add(id))
|
||||
{
|
||||
yield return id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryGetAuthoredActionPhrase(string id, out string actionPhrase)
|
||||
{
|
||||
actionPhrase = "";
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string key = id.Trim();
|
||||
if (EventCatalogConfig.TryGetDecisionAction(key, out actionPhrase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return BuiltinActionPhrases.TryGetValue(key, out actionPhrase)
|
||||
&& !string.IsNullOrEmpty(actionPhrase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Infinitive clause for <c>decides to …</c>. Prefers event-catalog.json / builtin,
|
||||
/// else a generic humanize of the asset id.
|
||||
/// </summary>
|
||||
public static string ActionPhraseFor(string decisionId)
|
||||
{
|
||||
Ensure();
|
||||
if (string.IsNullOrEmpty(decisionId))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
string key = decisionId.Trim();
|
||||
if (Entries.TryGetValue(key, out DiscreteEventEntry entry)
|
||||
&& !string.IsNullOrEmpty(entry.ActionPhrase))
|
||||
{
|
||||
return entry.ActionPhrase;
|
||||
}
|
||||
|
||||
if (TryGetAuthoredActionPhrase(key, out string authored))
|
||||
{
|
||||
return authored;
|
||||
}
|
||||
|
||||
return FallbackHumanize(key);
|
||||
}
|
||||
|
||||
private static void Ensure()
|
||||
{
|
||||
if (_appliedOverlayGeneration != EventCatalogConfig.Generation)
|
||||
{
|
||||
Entries.Clear();
|
||||
_appliedOverlayGeneration = EventCatalogConfig.Generation;
|
||||
}
|
||||
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLiveDecisionIds,
|
||||
"Politics",
|
||||
"{a} decides to act",
|
||||
ambientStrength: 40f,
|
||||
signalIds: null,
|
||||
signalStrength: EventCatalogConfig.DefaultDecisionStrength,
|
||||
signalPredicate: IsInterestingDecision,
|
||||
ambientCreatesInterest: false);
|
||||
|
||||
// Apply authored prose + per-id strength (JSON wins over builtin).
|
||||
foreach (string id in AuthoredActionPhraseIds)
|
||||
{
|
||||
if (!TryGetAuthoredActionPhrase(id, out string action) || string.IsNullOrEmpty(action))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float strength = EventCatalogConfig.DecisionStrengthOrDefault(
|
||||
id,
|
||||
IsInterestingDecision(id) ? EventCatalogConfig.DefaultDecisionStrength : 40f);
|
||||
string label = "{a} decides to " + action;
|
||||
if (Entries.TryGetValue(id, out DiscreteEventEntry existing))
|
||||
{
|
||||
existing.ActionPhrase = action;
|
||||
existing.LabelTemplate = label;
|
||||
if (EventCatalogConfig.TryGetDecision(id, out _))
|
||||
{
|
||||
existing.EventStrength = strength;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
Entries[id] = new DiscreteEventEntry
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = strength,
|
||||
Category = "Politics",
|
||||
CreatesInterest = IsInterestingDecision(id),
|
||||
ActionPhrase = action,
|
||||
LabelTemplate = label
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
DiscreteEventEntry entry = LiveLibraryInterest.Lookup(
|
||||
Entries, id, "Politics", "{a} decides to act", 35f);
|
||||
if (!entry.IsFallback && string.IsNullOrEmpty(entry.ActionPhrase))
|
||||
{
|
||||
// Attach fallback humanize so callers can still read a clause.
|
||||
entry.ActionPhrase = FallbackHumanize(entry.Id);
|
||||
if (!string.IsNullOrEmpty(entry.ActionPhrase))
|
||||
{
|
||||
entry.LabelTemplate = "{a} decides to " + entry.ActionPhrase;
|
||||
}
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic id → infinitive clause when no authored row exists.
|
||||
/// Prefer adding a row to <c>event-catalog.json</c> instead of relying on this.
|
||||
/// </summary>
|
||||
public static string FallbackHumanize(string decisionId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(decisionId))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
string raw = decisionId.Trim().Replace('-', '_').ToLowerInvariant();
|
||||
if (raw.StartsWith("type_", StringComparison.Ordinal))
|
||||
{
|
||||
raw = raw.Substring(5);
|
||||
}
|
||||
|
||||
string[] parts = raw.Split(new[] { '_' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length == 0)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
int start = 0;
|
||||
while (start < parts.Length
|
||||
&& (parts[start] == "child"
|
||||
|| parts[start] == "baby"
|
||||
|| parts[start] == "adult"
|
||||
|| parts[start] == "warrior"
|
||||
|| parts[start] == "city"
|
||||
|| parts[start] == "actor"
|
||||
|| parts[start] == "animal"
|
||||
|| parts[start] == "herd"
|
||||
|| parts[start] == "civ"
|
||||
|| parts[start] == "mob"
|
||||
|| parts[start] == "king"
|
||||
|| parts[start] == "leader"))
|
||||
{
|
||||
start++;
|
||||
}
|
||||
|
||||
if (start >= parts.Length)
|
||||
{
|
||||
start = 0;
|
||||
}
|
||||
|
||||
var words = new List<string>(parts.Length - start);
|
||||
for (int i = start; i < parts.Length; i++)
|
||||
{
|
||||
words.Add(parts[i]);
|
||||
}
|
||||
|
||||
if (words.Count == 0)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
if (words.Count >= 3
|
||||
&& words[0] == "change"
|
||||
&& (words[1] == "kingdom" || words[1] == "city" || words[1] == "clan"))
|
||||
{
|
||||
return "change the " + words[1] + "'s " + string.Join(" ", words.GetRange(2, words.Count - 2));
|
||||
}
|
||||
|
||||
if (words[0] == "diet" && words.Count >= 2)
|
||||
{
|
||||
words[0] = "eat";
|
||||
}
|
||||
|
||||
if (words.Count >= 2 && words[0] == "try" && words[1] == "new")
|
||||
{
|
||||
words.Insert(1, "a");
|
||||
return string.Join(" ", words);
|
||||
}
|
||||
|
||||
if (words.Count == 2 && words[0] == "find")
|
||||
{
|
||||
return "find a " + words[1];
|
||||
}
|
||||
|
||||
if (words[0] == "join" && words.Count >= 2 && words[1] == "army")
|
||||
{
|
||||
return "join an army";
|
||||
}
|
||||
|
||||
if (words[0] == "try" && words.Count >= 2 && words[1] != "to")
|
||||
{
|
||||
words.Insert(1, "to");
|
||||
}
|
||||
|
||||
if (words.Count >= 4
|
||||
&& words[0] == "try"
|
||||
&& words[1] == "to"
|
||||
&& words[2] == "join"
|
||||
&& words[3] == "army")
|
||||
{
|
||||
return "try to join an army";
|
||||
}
|
||||
|
||||
if (words.Count >= 3 && words[0] == "train" && words[1] == "with")
|
||||
{
|
||||
return "train with a " + string.Join(" ", words.GetRange(2, words.Count - 2));
|
||||
}
|
||||
|
||||
if (words.Count >= 2
|
||||
&& words[words.Count - 1] == "leader"
|
||||
&& (words[0] == "follow" || (words[0] == "army" && words[1] == "follow")))
|
||||
{
|
||||
return "follow their leader";
|
||||
}
|
||||
|
||||
return string.Join(" ", words);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,82 +13,77 @@ public sealed class DisasterInterestEntry
|
|||
public bool IsFallback;
|
||||
}
|
||||
|
||||
/// <summary>Authored interest overlay for every live disasters library asset.</summary>
|
||||
/// <summary>Disaster dials from <c>event-catalog.json</c> <c>disasters</c>.</summary>
|
||||
public static partial class EventCatalog
|
||||
{
|
||||
public static class Disaster
|
||||
{
|
||||
private static readonly Dictionary<string, DisasterInterestEntry> Entries =
|
||||
new Dictionary<string, DisasterInterestEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
static Disaster()
|
||||
{
|
||||
Add("tornado", 95f, "disaster_tornado");
|
||||
Add("heatwave", 88f, "disaster_heatwave");
|
||||
Add("small_meteorite", 95f, "disaster_meteorite");
|
||||
Add("small_earthquake", 95f, "disaster_earthquake");
|
||||
Add("hellspawn", 96f, "disaster_hellspawn");
|
||||
Add("ice_ones_awoken", 95f, "disaster_ice_ones");
|
||||
Add("sudden_snowman", 90f, "disaster_sudden_snowman");
|
||||
Add("garden_surprise", 90f, "disaster_garden_surprise");
|
||||
Add("dragon_from_farlands", 98f, "disaster_dragon_from_farlands");
|
||||
Add("ash_bandits", 92f, "disaster_bandits");
|
||||
Add("alien_invasion", 97f, "disaster_alien_invasion");
|
||||
Add("biomass", 96f, "disaster_biomass");
|
||||
Add("tumor", 96f, "disaster_tumor");
|
||||
Add("wild_mage", 94f, "disaster_evil_mage");
|
||||
Add("underground_necromancer", 95f, "disaster_underground_necromancer");
|
||||
Add("mad_thoughts", 90f, "disaster_mad_thoughts");
|
||||
Add("greg_abominations", 96f, "disaster_greg_abominations");
|
||||
}
|
||||
private static readonly Dictionary<string, DisasterInterestEntry> Entries =
|
||||
new Dictionary<string, DisasterInterestEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static void Add(string id, float strength, string worldLogId)
|
||||
{
|
||||
Entries[id] = new DisasterInterestEntry
|
||||
private static int _appliedGeneration = -1;
|
||||
|
||||
public static void InvalidateOverlays()
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = strength,
|
||||
WorldLogId = worldLogId,
|
||||
CreatesInterest = true,
|
||||
Category = "Disaster"
|
||||
};
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds => Entries.Keys;
|
||||
|
||||
public static bool HasAuthored(string disasterId)
|
||||
{
|
||||
return !string.IsNullOrEmpty(disasterId) && Entries.ContainsKey(disasterId.Trim());
|
||||
}
|
||||
|
||||
public static bool TryGet(string disasterId, out DisasterInterestEntry entry)
|
||||
{
|
||||
entry = null;
|
||||
if (string.IsNullOrEmpty(disasterId))
|
||||
{
|
||||
return false;
|
||||
Entries.Clear();
|
||||
_appliedGeneration = -1;
|
||||
}
|
||||
|
||||
return Entries.TryGetValue(disasterId.Trim(), out entry);
|
||||
}
|
||||
|
||||
public static DisasterInterestEntry GetOrFallback(string disasterId)
|
||||
{
|
||||
if (TryGet(disasterId, out DisasterInterestEntry entry))
|
||||
private static void Ensure()
|
||||
{
|
||||
return entry;
|
||||
if (_appliedGeneration == EventCatalogConfig.Generation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_appliedGeneration = EventCatalogConfig.Generation;
|
||||
EventCatalogConfig.FillDisasters(Entries);
|
||||
}
|
||||
|
||||
string id = string.IsNullOrEmpty(disasterId) ? "unknown" : disasterId.Trim();
|
||||
return new DisasterInterestEntry
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = 90f,
|
||||
Category = "Disaster",
|
||||
WorldLogId = "",
|
||||
CreatesInterest = true,
|
||||
IsFallback = true
|
||||
};
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool HasAuthored(string disasterId)
|
||||
{
|
||||
Ensure();
|
||||
return !string.IsNullOrEmpty(disasterId) && Entries.ContainsKey(disasterId.Trim());
|
||||
}
|
||||
|
||||
public static bool TryGet(string disasterId, out DisasterInterestEntry entry)
|
||||
{
|
||||
Ensure();
|
||||
entry = null;
|
||||
if (string.IsNullOrEmpty(disasterId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Entries.TryGetValue(disasterId.Trim(), out entry);
|
||||
}
|
||||
|
||||
public static DisasterInterestEntry GetOrFallback(string disasterId)
|
||||
{
|
||||
if (TryGet(disasterId, out DisasterInterestEntry entry))
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
|
||||
string id = string.IsNullOrEmpty(disasterId) ? "unknown" : disasterId.Trim();
|
||||
return new DisasterInterestEntry
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = 90f,
|
||||
Category = "Disaster",
|
||||
WorldLogId = "",
|
||||
CreatesInterest = true,
|
||||
IsFallback = true
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,66 +3,66 @@ using System.Collections.Generic;
|
|||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>Authored interest overlay for every live era_library asset.</summary>
|
||||
/// <summary>Era dials from <c>event-catalog.json</c> <c>eras</c>.</summary>
|
||||
public static partial class EventCatalog
|
||||
{
|
||||
public static class Era
|
||||
{
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
static Era()
|
||||
{
|
||||
Add("age_hope", 78f, "World", "Age of Hope");
|
||||
Add("age_sun", 80f, "World", "Age of Sun");
|
||||
Add("age_dark", 88f, "World", "Age of Dark");
|
||||
Add("age_tears", 86f, "World", "Age of Tears");
|
||||
Add("age_moon", 82f, "World", "Age of Moon");
|
||||
Add("age_chaos", 92f, "World", "Age of Chaos");
|
||||
Add("age_wonders", 84f, "World", "Age of Wonders");
|
||||
Add("age_ice", 87f, "World", "Age of Ice");
|
||||
Add("age_ash", 90f, "World", "Age of Ash");
|
||||
Add("age_despair", 91f, "World", "Age of Despair");
|
||||
Add("age_unknown", 70f, "World", "Unknown age");
|
||||
}
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static void Add(
|
||||
string id,
|
||||
float strength,
|
||||
string category,
|
||||
string label)
|
||||
{
|
||||
Entries[id] = new DiscreteEventEntry
|
||||
private static int _appliedGeneration = -1;
|
||||
|
||||
public static void InvalidateOverlays()
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = strength,
|
||||
Category = category,
|
||||
CreatesInterest = true,
|
||||
LabelTemplate = label
|
||||
};
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds => Entries.Keys;
|
||||
|
||||
public static bool HasAuthored(string id) =>
|
||||
!string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim());
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry))
|
||||
{
|
||||
return entry;
|
||||
Entries.Clear();
|
||||
_appliedGeneration = -1;
|
||||
}
|
||||
|
||||
string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim();
|
||||
return new DiscreteEventEntry
|
||||
private static void Ensure()
|
||||
{
|
||||
Id = key,
|
||||
EventStrength = 70f,
|
||||
Category = "World",
|
||||
LabelTemplate = "Era ({id})",
|
||||
IsFallback = true
|
||||
};
|
||||
if (_appliedGeneration == EventCatalogConfig.Generation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_appliedGeneration = EventCatalogConfig.Generation;
|
||||
EventCatalogConfig.FillEras(Entries);
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool HasAuthored(string id)
|
||||
{
|
||||
Ensure();
|
||||
return !string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim());
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry))
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
|
||||
string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim();
|
||||
return new DiscreteEventEntry
|
||||
{
|
||||
Id = key,
|
||||
EventStrength = 70f,
|
||||
Category = "World",
|
||||
LabelTemplate = "Era ({id})",
|
||||
CreatesInterest = true,
|
||||
IsFallback = true
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -3,416 +3,519 @@ using System.Collections.Generic;
|
|||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>Live AssetManager library event dials (powers, genes, …).</summary>
|
||||
/// <summary>
|
||||
/// Live AssetManager library event dials (powers, genes, …).
|
||||
/// Strengths/labels from <c>event-catalog.json</c> <c>libraries</c>; signal predicates stay in C#.
|
||||
/// </summary>
|
||||
public static partial class EventCatalog
|
||||
{
|
||||
private static LibraryCatalogDefaults LibOr(
|
||||
string key,
|
||||
float ambient,
|
||||
float signal,
|
||||
string category,
|
||||
string label,
|
||||
bool ambientCreatesInterest)
|
||||
{
|
||||
if (EventCatalogConfig.TryGetLibraryDefaults(key, out LibraryCatalogDefaults d) && d != null)
|
||||
{
|
||||
return d;
|
||||
}
|
||||
|
||||
return new LibraryCatalogDefaults
|
||||
{
|
||||
AmbientStrength = ambient,
|
||||
SignalStrength = signal,
|
||||
Category = category,
|
||||
Label = label,
|
||||
AmbientCreatesInterest = ambientCreatesInterest
|
||||
};
|
||||
}
|
||||
|
||||
public static class Spell
|
||||
{
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static readonly HashSet<string> SignalIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"summon_lightning", "summon_tornado", "cast_curse", "cast_fire",
|
||||
"cast_blood_rain", "summon_meteor", "teleport"
|
||||
};
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static void Ensure() =>
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLiveSpellIds,
|
||||
"Spectacle",
|
||||
"{a} casts {id}",
|
||||
ambientStrength: 48f,
|
||||
SignalIds,
|
||||
signalStrength: 88f,
|
||||
ambientCreatesInterest: false);
|
||||
private static int _appliedGeneration = -1;
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
public static void InvalidateOverlays()
|
||||
{
|
||||
Entries.Clear();
|
||||
_appliedGeneration = -1;
|
||||
}
|
||||
|
||||
private static void Ensure()
|
||||
{
|
||||
if (_appliedGeneration == EventCatalogConfig.Generation && Entries.Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Entries.Clear();
|
||||
_appliedGeneration = EventCatalogConfig.Generation;
|
||||
LibraryCatalogDefaults d = LibOr("spell", 48f, 88f, "Spectacle", "{a} casts {id}", true);
|
||||
HashSet<string> signals = d.SignalIds.Count > 0
|
||||
? d.SignalIds
|
||||
: new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"summon_lightning", "summon_tornado", "cast_curse", "cast_fire",
|
||||
"cast_blood_rain", "summon_meteor", "teleport"
|
||||
};
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLiveSpellIds,
|
||||
d.Category,
|
||||
d.Label,
|
||||
d.AmbientStrength,
|
||||
signals,
|
||||
d.SignalStrength,
|
||||
ambientCreatesInterest: d.AmbientCreatesInterest);
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
LibraryCatalogDefaults d = LibOr("spell", 48f, 88f, "Spectacle", "{a} casts {id}", true);
|
||||
return LiveLibraryInterest.Lookup(Entries, id, d.Category, d.Label, 55f);
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
return LiveLibraryInterest.Lookup(Entries, id, "Spectacle", "{a} casts {id}", 55f);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>God powers library - mostly Ambient; Signal for disaster/spectacle ids.</summary>
|
||||
public static class Power
|
||||
{
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsSpectaclePower(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static int _appliedGeneration = -1;
|
||||
|
||||
public static void InvalidateOverlays()
|
||||
{
|
||||
return false;
|
||||
Entries.Clear();
|
||||
_appliedGeneration = -1;
|
||||
}
|
||||
|
||||
string s = id.ToLowerInvariant();
|
||||
return s.Contains("meteor")
|
||||
|| s.Contains("earthquake")
|
||||
|| s.Contains("tornado")
|
||||
|| s.Contains("volcano")
|
||||
|| s.Contains("nuke")
|
||||
|| s.Contains("bomb")
|
||||
|| s.Contains("rain_blood")
|
||||
|| s.Contains("hell")
|
||||
|| s.Contains("lightning")
|
||||
|| s.Contains("storm")
|
||||
|| s.Contains("acid")
|
||||
|| s.Contains("fire")
|
||||
|| s.Contains("disaster");
|
||||
}
|
||||
private static bool IsSpectaclePower(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void Ensure() =>
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLivePowerIds,
|
||||
"Spectacle",
|
||||
"God power: {id}",
|
||||
ambientStrength: 28f,
|
||||
signalIds: null,
|
||||
signalStrength: 92f,
|
||||
signalPredicate: IsSpectaclePower,
|
||||
ambientCreatesInterest: false);
|
||||
string s = id.ToLowerInvariant();
|
||||
return s.Contains("meteor")
|
||||
|| s.Contains("earthquake")
|
||||
|| s.Contains("tornado")
|
||||
|| s.Contains("volcano")
|
||||
|| s.Contains("nuke")
|
||||
|| s.Contains("bomb")
|
||||
|| s.Contains("rain_blood")
|
||||
|| s.Contains("hell")
|
||||
|| s.Contains("lightning")
|
||||
|| s.Contains("storm")
|
||||
|| s.Contains("acid")
|
||||
|| s.Contains("fire")
|
||||
|| s.Contains("disaster");
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
private static void Ensure()
|
||||
{
|
||||
if (_appliedGeneration == EventCatalogConfig.Generation && Entries.Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Entries.Clear();
|
||||
_appliedGeneration = EventCatalogConfig.Generation;
|
||||
LibraryCatalogDefaults d = LibOr("power", 28f, 92f, "Spectacle", "God power: {id}", false);
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLivePowerIds,
|
||||
d.Category,
|
||||
d.Label,
|
||||
d.AmbientStrength,
|
||||
signalIds: null,
|
||||
d.SignalStrength,
|
||||
signalPredicate: IsSpectaclePower,
|
||||
ambientCreatesInterest: d.AmbientCreatesInterest);
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
LibraryCatalogDefaults d = LibOr("power", 28f, 92f, "Spectacle", "God power: {id}", false);
|
||||
return LiveLibraryInterest.Lookup(Entries, id, d.Category, d.Label, 30f);
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
return LiveLibraryInterest.Lookup(Entries, id, "Spectacle", "God power: {id}", 30f);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>AI decisions - Signal for war/rebellion/kingdom-affecting; Ambient otherwise.</summary>
|
||||
public static class Decision
|
||||
{
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsKingdomDecision(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string s = id.ToLowerInvariant();
|
||||
// Routine AI ticks often include "king" in the name - those are not camera events.
|
||||
if (s.Contains("idle")
|
||||
|| s.Contains("walking")
|
||||
|| s.Contains("check")
|
||||
|| s.Contains("wait")
|
||||
|| s.Contains("sleep"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return s.Contains("war")
|
||||
|| s.Contains("rebellion")
|
||||
|| s.Contains("revolt")
|
||||
|| s.Contains("alliance")
|
||||
|| s.Contains("coup")
|
||||
|| s.Contains("betray")
|
||||
|| s.Contains("declare")
|
||||
|| s.Contains("invade")
|
||||
|| s.Contains("siege")
|
||||
|| s.Contains("found")
|
||||
|| s.Contains("city_foundation")
|
||||
|| (s.Contains("king") && (s.Contains("war") || s.Contains("city") || s.Contains("rebellion")));
|
||||
}
|
||||
|
||||
/// <summary>True when a decision id should be allowed to own the idle camera.</summary>
|
||||
public static bool IsCameraWorthy(string id) => IsKingdomDecision(id);
|
||||
|
||||
private static void Ensure() =>
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLiveDecisionIds,
|
||||
"Politics",
|
||||
"{a} decides {id}",
|
||||
ambientStrength: 32f,
|
||||
signalIds: null,
|
||||
signalStrength: 86f,
|
||||
signalPredicate: IsKingdomDecision,
|
||||
ambientCreatesInterest: false);
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
return LiveLibraryInterest.Lookup(Entries, id, "Politics", "{a} decides {id}", 35f);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Items library - Signal for legendary/wonder subset.</summary>
|
||||
public static class Item
|
||||
{
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsLegendaryItem(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static int _appliedGeneration = -1;
|
||||
|
||||
public static void InvalidateOverlays()
|
||||
{
|
||||
return false;
|
||||
Entries.Clear();
|
||||
_appliedGeneration = -1;
|
||||
}
|
||||
|
||||
string s = id.ToLowerInvariant();
|
||||
return s.Contains("legendary")
|
||||
|| s.Contains("mythic")
|
||||
|| s.Contains("artifact")
|
||||
|| s.Contains("relic")
|
||||
|| s.Contains("divine")
|
||||
|| s.Contains("demon")
|
||||
|| s.Contains("dragon")
|
||||
|| s.Contains("nuke")
|
||||
|| s.Contains("excalibur")
|
||||
|| s.Contains("wonder");
|
||||
}
|
||||
private static bool IsLegendaryItem(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void Ensure() =>
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLiveItemIds,
|
||||
"Item",
|
||||
"{a} forges {id}",
|
||||
ambientStrength: 36f,
|
||||
signalIds: null,
|
||||
signalStrength: 80f,
|
||||
signalPredicate: IsLegendaryItem,
|
||||
ambientCreatesInterest: false);
|
||||
string s = id.ToLowerInvariant();
|
||||
return s.Contains("legendary")
|
||||
|| s.Contains("mythic")
|
||||
|| s.Contains("artifact")
|
||||
|| s.Contains("relic")
|
||||
|| s.Contains("divine")
|
||||
|| s.Contains("demon")
|
||||
|| s.Contains("dragon")
|
||||
|| s.Contains("nuke")
|
||||
|| s.Contains("excalibur")
|
||||
|| s.Contains("wonder");
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
private static void Ensure()
|
||||
{
|
||||
if (_appliedGeneration == EventCatalogConfig.Generation && Entries.Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Entries.Clear();
|
||||
_appliedGeneration = EventCatalogConfig.Generation;
|
||||
LibraryCatalogDefaults d = LibOr("item", 36f, 80f, "Item", "{a} forges {id}", true);
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLiveItemIds,
|
||||
d.Category,
|
||||
d.Label,
|
||||
d.AmbientStrength,
|
||||
signalIds: null,
|
||||
d.SignalStrength,
|
||||
signalPredicate: IsLegendaryItem,
|
||||
ambientCreatesInterest: d.AmbientCreatesInterest);
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
LibraryCatalogDefaults d = LibOr("item", 36f, 80f, "Item", "{a} forges {id}", true);
|
||||
return LiveLibraryInterest.Lookup(Entries, id, d.Category, d.Label, 40f);
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
return LiveLibraryInterest.Lookup(Entries, id, "Item", "{a} forges {id}", 40f);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Subspecies traits - Signal for dramatic; Ambient fill-capped.</summary>
|
||||
public static class SubspeciesTrait
|
||||
{
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsDramatic(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static int _appliedGeneration = -1;
|
||||
|
||||
public static void InvalidateOverlays()
|
||||
{
|
||||
return false;
|
||||
Entries.Clear();
|
||||
_appliedGeneration = -1;
|
||||
}
|
||||
|
||||
string s = id.ToLowerInvariant();
|
||||
return s.Contains("immortal")
|
||||
|| s.Contains("giant")
|
||||
|| s.Contains("tiny")
|
||||
|| s.Contains("fire")
|
||||
|| s.Contains("frost")
|
||||
|| s.Contains("poison")
|
||||
|| s.Contains("plague")
|
||||
|| s.Contains("magic")
|
||||
|| s.Contains("divine")
|
||||
|| s.Contains("demon")
|
||||
|| s.Contains("undead")
|
||||
|| s.Contains("dragon")
|
||||
|| s.Contains("evolve")
|
||||
|| s.Contains("mutate");
|
||||
}
|
||||
private static bool IsDramatic(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void Ensure() =>
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLiveSubspeciesTraitIds,
|
||||
"Evolution",
|
||||
"{a} evolves {id}",
|
||||
ambientStrength: 34f,
|
||||
signalIds: null,
|
||||
signalStrength: 78f,
|
||||
signalPredicate: IsDramatic);
|
||||
string s = id.ToLowerInvariant();
|
||||
return s.Contains("immortal")
|
||||
|| s.Contains("giant")
|
||||
|| s.Contains("tiny")
|
||||
|| s.Contains("fire")
|
||||
|| s.Contains("frost")
|
||||
|| s.Contains("poison")
|
||||
|| s.Contains("plague")
|
||||
|| s.Contains("magic")
|
||||
|| s.Contains("divine")
|
||||
|| s.Contains("demon")
|
||||
|| s.Contains("undead")
|
||||
|| s.Contains("dragon")
|
||||
|| s.Contains("evolve")
|
||||
|| s.Contains("mutate");
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
private static void Ensure()
|
||||
{
|
||||
if (_appliedGeneration == EventCatalogConfig.Generation && Entries.Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Entries.Clear();
|
||||
_appliedGeneration = EventCatalogConfig.Generation;
|
||||
LibraryCatalogDefaults d = LibOr("subspeciesTrait", 34f, 78f, "Evolution", "{a} evolves {id}", true);
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLiveSubspeciesTraitIds,
|
||||
d.Category,
|
||||
d.Label,
|
||||
d.AmbientStrength,
|
||||
signalIds: null,
|
||||
d.SignalStrength,
|
||||
signalPredicate: IsDramatic,
|
||||
ambientCreatesInterest: d.AmbientCreatesInterest);
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
LibraryCatalogDefaults d = LibOr("subspeciesTrait", 34f, 78f, "Evolution", "{a} evolves {id}", true);
|
||||
return LiveLibraryInterest.Lookup(Entries, id, d.Category, d.Label, 36f);
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
return LiveLibraryInterest.Lookup(Entries, id, "Evolution", "{a} evolves {id}", 36f);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Genes - Ambient default.</summary>
|
||||
public static class Gene
|
||||
{
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static void Ensure() =>
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLiveGeneIds,
|
||||
"Genetics",
|
||||
"{a} gains gene {id}",
|
||||
ambientStrength: 30f,
|
||||
signalIds: null,
|
||||
signalStrength: 70f,
|
||||
signalPredicate: id => id != null && (id.Contains("warfare") || id.Contains("magic") || id.Contains("mutation")),
|
||||
ambientCreatesInterest: false);
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static int _appliedGeneration = -1;
|
||||
|
||||
public static void InvalidateOverlays()
|
||||
{
|
||||
Entries.Clear();
|
||||
_appliedGeneration = -1;
|
||||
}
|
||||
|
||||
private static void Ensure()
|
||||
{
|
||||
if (_appliedGeneration == EventCatalogConfig.Generation && Entries.Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Entries.Clear();
|
||||
_appliedGeneration = EventCatalogConfig.Generation;
|
||||
LibraryCatalogDefaults d = LibOr("gene", 30f, 70f, "Genetics", "{a} gains gene {id}", true);
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLiveGeneIds,
|
||||
d.Category,
|
||||
d.Label,
|
||||
d.AmbientStrength,
|
||||
signalIds: null,
|
||||
d.SignalStrength,
|
||||
signalPredicate: id => id != null && (id.Contains("warfare") || id.Contains("magic") || id.Contains("mutation")),
|
||||
ambientCreatesInterest: d.AmbientCreatesInterest);
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
LibraryCatalogDefaults d = LibOr("gene", 30f, 70f, "Genetics", "{a} gains gene {id}", true);
|
||||
return LiveLibraryInterest.Lookup(Entries, id, d.Category, d.Label, 30f);
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
return LiveLibraryInterest.Lookup(Entries, id, "Genetics", "{a} gains gene {id}", 30f);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Phenotypes - Ambient default.</summary>
|
||||
public static class Phenotype
|
||||
{
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static void Ensure() =>
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLivePhenotypeIds,
|
||||
"Genetics",
|
||||
"{a} shows {id}",
|
||||
ambientStrength: 28f,
|
||||
signalIds: null,
|
||||
signalStrength: 60f,
|
||||
ambientCreatesInterest: false);
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static int _appliedGeneration = -1;
|
||||
|
||||
public static void InvalidateOverlays()
|
||||
{
|
||||
Entries.Clear();
|
||||
_appliedGeneration = -1;
|
||||
}
|
||||
|
||||
private static void Ensure()
|
||||
{
|
||||
if (_appliedGeneration == EventCatalogConfig.Generation && Entries.Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Entries.Clear();
|
||||
_appliedGeneration = EventCatalogConfig.Generation;
|
||||
LibraryCatalogDefaults d = LibOr("phenotype", 28f, 60f, "Genetics", "{a} shows {id}", true);
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLivePhenotypeIds,
|
||||
d.Category,
|
||||
d.Label,
|
||||
d.AmbientStrength,
|
||||
signalIds: null,
|
||||
d.SignalStrength,
|
||||
ambientCreatesInterest: d.AmbientCreatesInterest);
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
LibraryCatalogDefaults d = LibOr("phenotype", 28f, 60f, "Genetics", "{a} shows {id}", true);
|
||||
return LiveLibraryInterest.Lookup(Entries, id, d.Category, d.Label, 28f);
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
return LiveLibraryInterest.Lookup(Entries, id, "Genetics", "{a} shows {id}", 28f);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>World laws - Ambient / location-only style.</summary>
|
||||
public static class WorldLaw
|
||||
{
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static void Ensure() =>
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLiveWorldLawIds,
|
||||
"WorldLaw",
|
||||
"Law changed: {id}",
|
||||
ambientStrength: 40f,
|
||||
signalIds: null,
|
||||
signalStrength: 70f,
|
||||
signalPredicate: id => id != null && (id.Contains("war") || id.Contains("death") || id.Contains("plague")),
|
||||
ambientCreatesInterest: false);
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static int _appliedGeneration = -1;
|
||||
|
||||
public static void InvalidateOverlays()
|
||||
{
|
||||
Entries.Clear();
|
||||
_appliedGeneration = -1;
|
||||
}
|
||||
|
||||
private static void Ensure()
|
||||
{
|
||||
if (_appliedGeneration == EventCatalogConfig.Generation && Entries.Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Entries.Clear();
|
||||
_appliedGeneration = EventCatalogConfig.Generation;
|
||||
LibraryCatalogDefaults d = LibOr("worldLaw", 40f, 70f, "WorldLaw", "Law changed: {id}", true);
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLiveWorldLawIds,
|
||||
d.Category,
|
||||
d.Label,
|
||||
d.AmbientStrength,
|
||||
signalIds: null,
|
||||
d.SignalStrength,
|
||||
signalPredicate: id => id != null && (id.Contains("war") || id.Contains("death") || id.Contains("plague")),
|
||||
ambientCreatesInterest: d.AmbientCreatesInterest);
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
LibraryCatalogDefaults d = LibOr("worldLaw", 40f, 70f, "WorldLaw", "Law changed: {id}", true);
|
||||
return LiveLibraryInterest.Lookup(Entries, id, d.Category, d.Label, 40f);
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
return LiveLibraryInterest.Lookup(Entries, id, "WorldLaw", "Law changed: {id}", 40f);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Biomes - Ambient.</summary>
|
||||
public static class Biome
|
||||
{
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static void Ensure() =>
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLiveBiomeIds,
|
||||
"Biome",
|
||||
"Biome shifts: {id}",
|
||||
ambientStrength: 26f,
|
||||
signalIds: null,
|
||||
signalStrength: 50f,
|
||||
ambientCreatesInterest: false);
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static int _appliedGeneration = -1;
|
||||
|
||||
public static void InvalidateOverlays()
|
||||
{
|
||||
Entries.Clear();
|
||||
_appliedGeneration = -1;
|
||||
}
|
||||
|
||||
private static void Ensure()
|
||||
{
|
||||
if (_appliedGeneration == EventCatalogConfig.Generation && Entries.Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Entries.Clear();
|
||||
_appliedGeneration = EventCatalogConfig.Generation;
|
||||
LibraryCatalogDefaults d = LibOr("biome", 26f, 50f, "Biome", "Biome shifts: {id}", false);
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLiveBiomeIds,
|
||||
d.Category,
|
||||
d.Label,
|
||||
d.AmbientStrength,
|
||||
signalIds: null,
|
||||
d.SignalStrength,
|
||||
ambientCreatesInterest: d.AmbientCreatesInterest);
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
LibraryCatalogDefaults d = LibOr("biome", 26f, 50f, "Biome", "Biome shifts: {id}", false);
|
||||
return LiveLibraryInterest.Lookup(Entries, id, d.Category, d.Label, 26f);
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
return LiveLibraryInterest.Lookup(Entries, id, "Biome", "Biome shifts: {id}", 26f);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,83 +3,66 @@ using System.Collections.Generic;
|
|||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>Authored interest overlay for every live plots_library asset.</summary>
|
||||
/// <summary>Plot dials from <c>event-catalog.json</c> <c>plots</c>.</summary>
|
||||
public static partial class EventCatalog
|
||||
{
|
||||
public static class Plot
|
||||
{
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
static Plot()
|
||||
{
|
||||
Add("rebellion", 92f, "Politics", "{a} is plotting a rebellion");
|
||||
Add("new_war", 94f, "Politics", "{a} is plotting a war");
|
||||
Add("alliance_create", 80f, "Politics", "{a} is plotting an alliance");
|
||||
Add("alliance_join", 72f, "Politics", "{a} is joining an alliance");
|
||||
Add("alliance_destroy", 86f, "Politics", "{a} is breaking an alliance");
|
||||
Add("attacker_stop_war", 78f, "Politics", "{a} is ending a war");
|
||||
Add("new_book", 55f, "Culture", "{a} is writing a book");
|
||||
Add("new_language", 62f, "Culture", "{a} is forging a language");
|
||||
Add("new_religion", 70f, "Culture", "{a} is founding a religion");
|
||||
Add("new_culture", 68f, "Culture", "{a} is founding a culture");
|
||||
Add("clan_ascension", 82f, "Politics", "{a} is ascending a clan");
|
||||
Add("culture_divide", 75f, "Culture", "{a} splits a culture");
|
||||
Add("religion_schism", 84f, "Culture", "{a} causes a religion schism");
|
||||
Add("language_divergence", 60f, "Culture", "{a} splits a language");
|
||||
Add("summon_meteor_rain", 96f, "Spectacle", "{a} summons a meteor rain");
|
||||
Add("summon_earthquake", 95f, "Spectacle", "{a} summons an earthquake");
|
||||
Add("summon_thunderstorm", 93f, "Spectacle", "{a} summons a thunderstorm");
|
||||
Add("summon_stormfront", 93f, "Spectacle", "{a} summons a stormfront");
|
||||
Add("summon_hellstorm", 97f, "Spectacle", "{a} summons a hellstorm");
|
||||
Add("summon_demons", 96f, "Spectacle", "{a} summons demons");
|
||||
Add("summon_angles", 96f, "Spectacle", "{a} summons angels");
|
||||
Add("summon_skeletons", 92f, "Spectacle", "{a} summons skeletons");
|
||||
Add("summon_living_plants", 90f, "Spectacle", "{a} summons living plants");
|
||||
Add("big_cast_coffee", 58f, "Spectacle", "{a} performs a coffee ritual");
|
||||
Add("big_cast_bubble_shield", 64f, "Spectacle", "{a} casts a bubble shield");
|
||||
Add("big_cast_madness", 80f, "Spectacle", "{a} casts madness");
|
||||
Add("big_cast_slowness", 70f, "Spectacle", "{a} casts slowness");
|
||||
Add("cause_rebellion", 90f, "Politics", "{a} causes a rebellion");
|
||||
}
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static void Add(
|
||||
string id,
|
||||
float strength,
|
||||
string category,
|
||||
string label)
|
||||
{
|
||||
Entries[id] = new DiscreteEventEntry
|
||||
private static int _appliedGeneration = -1;
|
||||
|
||||
public static void InvalidateOverlays()
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = strength,
|
||||
Category = category,
|
||||
CreatesInterest = true,
|
||||
LabelTemplate = label
|
||||
};
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds => Entries.Keys;
|
||||
|
||||
public static bool HasAuthored(string id) =>
|
||||
!string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim());
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry))
|
||||
{
|
||||
return entry;
|
||||
Entries.Clear();
|
||||
_appliedGeneration = -1;
|
||||
}
|
||||
|
||||
string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim();
|
||||
return new DiscreteEventEntry
|
||||
private static void Ensure()
|
||||
{
|
||||
Id = key,
|
||||
EventStrength = 55f,
|
||||
Category = "Plot",
|
||||
LabelTemplate = "{a} · {id}",
|
||||
IsFallback = true
|
||||
};
|
||||
if (_appliedGeneration == EventCatalogConfig.Generation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_appliedGeneration = EventCatalogConfig.Generation;
|
||||
EventCatalogConfig.FillPlots(Entries);
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool HasAuthored(string id)
|
||||
{
|
||||
Ensure();
|
||||
return !string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim());
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry))
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
|
||||
string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim();
|
||||
return new DiscreteEventEntry
|
||||
{
|
||||
Id = key,
|
||||
EventStrength = 55f,
|
||||
Category = "Plot",
|
||||
LabelTemplate = "{a} · {id}",
|
||||
CreatesInterest = false,
|
||||
IsFallback = true
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,67 +3,66 @@ using System.Collections.Generic;
|
|||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>Authored discrete relationship lifecycle events (not a live asset library).</summary>
|
||||
/// <summary>Relationship dials from <c>event-catalog.json</c> <c>relationship</c>.</summary>
|
||||
public static partial class EventCatalog
|
||||
{
|
||||
public static class Relationship
|
||||
{
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
static Relationship()
|
||||
{
|
||||
Add("set_lover", 72f, "Relationship", "{a} became lovers with {b}");
|
||||
Add("clear_lover", 55f, "Relationship", "{a} parted from a lover");
|
||||
Add("add_child", 68f, "Relationship", "{a} gains a child, {b}");
|
||||
Add("new_family", 70f, "Relationship", "{a} starts a new family");
|
||||
Add("family_removed", 50f, "Relationship", "{a}'s family ends");
|
||||
Add("find_lover", 45f, "Relationship", "{a} is seeking a lover");
|
||||
Add("family_group_new", 60f, "Relationship", "{a} forms a family pack");
|
||||
Add("family_group_join", 36f, "Relationship", "{a} joins a family pack", createsInterest: false);
|
||||
Add("family_group_leave", 36f, "Relationship", "{a} leaves a family pack", createsInterest: false);
|
||||
Add("baby_created", 70f, "Relationship", "{a} is created as a baby");
|
||||
}
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static void Add(
|
||||
string id,
|
||||
float strength,
|
||||
string category,
|
||||
string label,
|
||||
bool createsInterest = true)
|
||||
{
|
||||
Entries[id] = new DiscreteEventEntry
|
||||
private static int _appliedGeneration = -1;
|
||||
|
||||
public static void InvalidateOverlays()
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = strength,
|
||||
Category = category,
|
||||
CreatesInterest = createsInterest,
|
||||
LabelTemplate = label
|
||||
};
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds => Entries.Keys;
|
||||
|
||||
public static bool HasAuthored(string id) =>
|
||||
!string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim());
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry))
|
||||
{
|
||||
return entry;
|
||||
Entries.Clear();
|
||||
_appliedGeneration = -1;
|
||||
}
|
||||
|
||||
string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim();
|
||||
return new DiscreteEventEntry
|
||||
private static void Ensure()
|
||||
{
|
||||
Id = key,
|
||||
EventStrength = 40f,
|
||||
Category = "Relationship",
|
||||
LabelTemplate = "{a} · {id}",
|
||||
CreatesInterest = false,
|
||||
IsFallback = true
|
||||
};
|
||||
if (_appliedGeneration == EventCatalogConfig.Generation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_appliedGeneration = EventCatalogConfig.Generation;
|
||||
EventCatalogConfig.FillRelationship(Entries);
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool HasAuthored(string id)
|
||||
{
|
||||
Ensure();
|
||||
return !string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim());
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry))
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
|
||||
string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim();
|
||||
return new DiscreteEventEntry
|
||||
{
|
||||
Id = key,
|
||||
EventStrength = 40f,
|
||||
Category = "Relationship",
|
||||
LabelTemplate = "{a} · {id}",
|
||||
CreatesInterest = false,
|
||||
IsFallback = true
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,139 +13,76 @@ public sealed class StatusInterestEntry
|
|||
public bool IsFallback;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Authored interest policy for every live status asset. Prose stays in ActivityStatusProse.
|
||||
/// </summary>
|
||||
/// <summary>Status interest dials from <c>event-catalog.json</c> <c>status</c>.</summary>
|
||||
public static partial class EventCatalog
|
||||
{
|
||||
public static class Status
|
||||
{
|
||||
private static readonly Dictionary<string, StatusInterestEntry> Entries =
|
||||
new Dictionary<string, StatusInterestEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
static Status()
|
||||
{
|
||||
// Spectacle / camera-worthy transformations
|
||||
Add("burning", 70f, "StatusTransformation", true);
|
||||
Add("possessed", 72f, "StatusTransformation", true);
|
||||
Add("possessed_follower", 60f, "StatusTransformation", true);
|
||||
Add("frozen", 65f, "StatusTransformation", true);
|
||||
Add("poisoned", 58f, "StatusTransformation", true);
|
||||
Add("drowning", 50f, "StatusTransformation", true);
|
||||
Add("stunned", 50f, "StatusTransformation", true);
|
||||
Add("rage", 62f, "StatusTransformation", true);
|
||||
Add("angry", 48f, "StatusTransformation", true);
|
||||
Add("cursed", 60f, "StatusTransformation", true);
|
||||
Add("soul_harvested", 75f, "StatusTransformation", true);
|
||||
Add("voices_in_my_head", 55f, "StatusTransformation", true);
|
||||
Add("tantrum", 52f, "StatusTransformation", true);
|
||||
Add("egg", 40f, "StatusTransformation", false);
|
||||
Add("magnetized", 55f, "StatusTransformation", true);
|
||||
Add("invincible", 40f, "StatusTransformation", false);
|
||||
Add("powerup", 54f, "StatusTransformation", true);
|
||||
Add("enchanted", 52f, "StatusTransformation", true);
|
||||
Add("ash_fever", 50f, "StatusTransformation", true);
|
||||
Add("starving", 48f, "StatusTransformation", true);
|
||||
Add("surprised", 42f, "StatusTransformation", true);
|
||||
Add("confused", 40f, "StatusTransformation", true);
|
||||
Add("strange_urge", 45f, "StatusTransformation", true);
|
||||
Add("inspired", 42f, "Emotion", true);
|
||||
Add("motivated", 40f, "Emotion", true);
|
||||
Add("fell_in_love", 42f, "Relationship", false);
|
||||
Add("pregnant", 58f, "LifeChapter", true);
|
||||
Add("pregnant_parthenogenesis", 58f, "LifeChapter", true);
|
||||
Add("crying", 50f, "Grief", true, extendsGrief: true);
|
||||
private static readonly Dictionary<string, StatusInterestEntry> Entries =
|
||||
new Dictionary<string, StatusInterestEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Authored Ambient - never owns camera (task chip / enrichment only).
|
||||
Add("festive_spirit", 28f, "StatusAmbient", false);
|
||||
Add("laughing", 26f, "StatusAmbient", false);
|
||||
Add("singing", 26f, "StatusAmbient", false);
|
||||
Add("sleeping", 22f, "StatusAmbient", false);
|
||||
Add("handsome_migrant", 30f, "StatusAmbient", false);
|
||||
private static int _appliedGeneration = -1;
|
||||
|
||||
// Authored but do not create interest candidates (cooldowns / ambient / brief FX)
|
||||
AddNoInterest("afterglow");
|
||||
AddNoInterest("being_suspicious");
|
||||
AddNoInterest("budding");
|
||||
AddNoInterest("caffeinated");
|
||||
AddNoInterest("cough");
|
||||
AddNoInterest("dash");
|
||||
AddNoInterest("dodge");
|
||||
AddNoInterest("flicked");
|
||||
AddNoInterest("had_bad_dream");
|
||||
AddNoInterest("had_good_dream");
|
||||
AddNoInterest("had_nightmare");
|
||||
AddNoInterest("just_ate");
|
||||
AddNoInterest("on_guard");
|
||||
AddNoInterest("recovery_combat_action");
|
||||
AddNoInterest("recovery_plot");
|
||||
AddNoInterest("recovery_social");
|
||||
AddNoInterest("recovery_spell");
|
||||
AddNoInterest("shield");
|
||||
AddNoInterest("slowness");
|
||||
AddNoInterest("spell_boost");
|
||||
AddNoInterest("spell_silence");
|
||||
AddNoInterest("swearing");
|
||||
AddNoInterest("taking_roots");
|
||||
AddNoInterest("uprooting");
|
||||
}
|
||||
|
||||
private static void Add(
|
||||
string id,
|
||||
float strength,
|
||||
string category,
|
||||
bool createsInterest,
|
||||
bool extendsGrief = false)
|
||||
{
|
||||
Entries[id] = new StatusInterestEntry
|
||||
public static void InvalidateOverlays()
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = strength,
|
||||
Category = category,
|
||||
CreatesInterest = createsInterest,
|
||||
ExtendsGrief = extendsGrief
|
||||
};
|
||||
}
|
||||
|
||||
private static void AddNoInterest(string id)
|
||||
{
|
||||
Add(id, 20f, "StatusAmbient", false);
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds => Entries.Keys;
|
||||
|
||||
public static bool HasAuthored(string statusId)
|
||||
{
|
||||
return !string.IsNullOrEmpty(statusId) && Entries.ContainsKey(statusId.Trim());
|
||||
}
|
||||
|
||||
public static bool TryGet(string statusId, out StatusInterestEntry entry)
|
||||
{
|
||||
entry = null;
|
||||
if (string.IsNullOrEmpty(statusId))
|
||||
{
|
||||
return false;
|
||||
Entries.Clear();
|
||||
_appliedGeneration = -1;
|
||||
}
|
||||
|
||||
return Entries.TryGetValue(statusId.Trim(), out entry);
|
||||
}
|
||||
|
||||
public static StatusInterestEntry GetOrFallback(string statusId)
|
||||
{
|
||||
if (TryGet(statusId, out StatusInterestEntry entry))
|
||||
private static void Ensure()
|
||||
{
|
||||
return entry;
|
||||
if (_appliedGeneration == EventCatalogConfig.Generation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_appliedGeneration = EventCatalogConfig.Generation;
|
||||
EventCatalogConfig.FillStatus(Entries);
|
||||
}
|
||||
|
||||
string id = string.IsNullOrEmpty(statusId) ? "unknown" : statusId.Trim();
|
||||
return new StatusInterestEntry
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = 40f,
|
||||
Category = "Status",
|
||||
CreatesInterest = false,
|
||||
IsFallback = true
|
||||
};
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool HasAuthored(string statusId)
|
||||
{
|
||||
Ensure();
|
||||
return !string.IsNullOrEmpty(statusId) && Entries.ContainsKey(statusId.Trim());
|
||||
}
|
||||
|
||||
public static bool TryGet(string statusId, out StatusInterestEntry entry)
|
||||
{
|
||||
Ensure();
|
||||
entry = null;
|
||||
if (string.IsNullOrEmpty(statusId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Entries.TryGetValue(statusId.Trim(), out entry);
|
||||
}
|
||||
|
||||
public static StatusInterestEntry GetOrFallback(string statusId)
|
||||
{
|
||||
if (TryGet(statusId, out StatusInterestEntry entry))
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
|
||||
string id = string.IsNullOrEmpty(statusId) ? "unknown" : statusId.Trim();
|
||||
return new StatusInterestEntry
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = 40f,
|
||||
Category = "Status",
|
||||
CreatesInterest = false,
|
||||
IsFallback = true
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,167 +3,66 @@ using System.Collections.Generic;
|
|||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>Authored interest overlay for every live traits library asset.</summary>
|
||||
/// <summary>Trait dials from <c>event-catalog.json</c> <c>traits</c>.</summary>
|
||||
public static partial class EventCatalog
|
||||
{
|
||||
public static class Trait
|
||||
{
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
static Trait()
|
||||
{
|
||||
Add("zombie", 82f);
|
||||
Add("wise", 42f);
|
||||
Add("whirlwind", 82f);
|
||||
Add("weightless", 42f);
|
||||
Add("weak", 42f);
|
||||
Add("veteran", 42f);
|
||||
Add("venomous", 82f);
|
||||
Add("unlucky", 42f);
|
||||
Add("ugly", 42f);
|
||||
Add("tumor_infection", 82f);
|
||||
Add("tough", 42f);
|
||||
Add("titan_lungs", 42f);
|
||||
Add("tiny", 42f);
|
||||
Add("thorns", 42f);
|
||||
Add("thief", 42f);
|
||||
Add("super_health", 42f);
|
||||
Add("sunblessed", 42f);
|
||||
Add("stupid", 42f);
|
||||
Add("strong_minded", 42f);
|
||||
Add("strong", 42f);
|
||||
Add("soft_skin", 42f);
|
||||
Add("slow", 42f);
|
||||
Add("skin_burns", 42f);
|
||||
Add("short_sighted", 42f);
|
||||
Add("shiny", 42f);
|
||||
Add("scar_of_divinity", 82f);
|
||||
Add("savage", 82f);
|
||||
Add("regeneration", 42f);
|
||||
Add("pyromaniac", 82f);
|
||||
Add("psychopath", 82f);
|
||||
Add("poisonous", 82f);
|
||||
Add("poison_immune", 42f);
|
||||
Add("plague", 82f);
|
||||
Add("peaceful", 42f);
|
||||
Add("paranoid", 42f);
|
||||
Add("pacifist", 42f);
|
||||
Add("nightchild", 42f);
|
||||
Add("mute", 42f);
|
||||
Add("mush_spores", 82f);
|
||||
Add("moonchild", 42f);
|
||||
Add("miracle_born", 82f);
|
||||
Add("miracle_bearer", 82f);
|
||||
Add("miner", 42f);
|
||||
Add("metamorphed", 42f);
|
||||
Add("mega_heartbeat", 42f);
|
||||
Add("mageslayer", 82f);
|
||||
Add("madness", 82f);
|
||||
Add("lustful", 42f);
|
||||
Add("lucky", 42f);
|
||||
Add("long_liver", 42f);
|
||||
Add("light_lamp", 42f);
|
||||
Add("kingslayer", 82f);
|
||||
Add("infertile", 42f);
|
||||
Add("infected", 82f);
|
||||
Add("immune", 42f);
|
||||
Add("immortal", 82f);
|
||||
Add("hotheaded", 42f);
|
||||
Add("honest", 42f);
|
||||
Add("heliophobia", 42f);
|
||||
Add("heart_of_wizard", 42f);
|
||||
Add("healing_aura", 42f);
|
||||
Add("hard_skin", 42f);
|
||||
Add("greedy", 42f);
|
||||
Add("golden_tooth", 42f);
|
||||
Add("gluttonous", 42f);
|
||||
Add("giant", 82f);
|
||||
Add("genius", 42f);
|
||||
Add("freeze_proof", 42f);
|
||||
Add("fragile_health", 42f);
|
||||
Add("flower_prints", 42f);
|
||||
Add("flesh_eater", 82f);
|
||||
Add("fire_proof", 42f);
|
||||
Add("fire_blood", 82f);
|
||||
Add("fertile", 42f);
|
||||
Add("fat", 42f);
|
||||
Add("fast", 42f);
|
||||
Add("eyepatch", 42f);
|
||||
Add("evil", 82f);
|
||||
Add("energized", 42f);
|
||||
Add("eagle_eyed", 42f);
|
||||
Add("dragonslayer", 82f);
|
||||
Add("dodge", 42f);
|
||||
Add("desire_harp", 42f);
|
||||
Add("desire_golden_egg", 42f);
|
||||
Add("desire_computer", 42f);
|
||||
Add("desire_alien_mold", 42f);
|
||||
Add("deflect_projectile", 42f);
|
||||
Add("deceitful", 42f);
|
||||
Add("death_nuke", 82f);
|
||||
Add("death_mark", 82f);
|
||||
Add("death_bomb", 82f);
|
||||
Add("dash", 42f);
|
||||
Add("crippled", 42f);
|
||||
Add("content", 42f);
|
||||
Add("contagious", 82f);
|
||||
Add("cold_aura", 82f);
|
||||
Add("clumsy", 42f);
|
||||
Add("clone", 42f);
|
||||
Add("chosen_one", 82f);
|
||||
Add("burning_feet", 82f);
|
||||
Add("bubble_defense", 42f);
|
||||
Add("boosted_vitality", 42f);
|
||||
Add("bomberman", 82f);
|
||||
Add("boat", 42f);
|
||||
Add("bloodlust", 82f);
|
||||
Add("block", 42f);
|
||||
Add("blessed", 42f);
|
||||
Add("battle_reflexes", 42f);
|
||||
Add("backstep", 42f);
|
||||
Add("attractive", 42f);
|
||||
Add("arcane_reflexes", 42f);
|
||||
Add("ambitious", 42f);
|
||||
Add("agile", 42f);
|
||||
Add("acid_touch", 82f);
|
||||
Add("acid_proof", 42f);
|
||||
Add("acid_blood", 42f);
|
||||
}
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static void Add(string id, float strength)
|
||||
{
|
||||
Entries[id] = new DiscreteEventEntry
|
||||
private static int _appliedGeneration = -1;
|
||||
|
||||
public static void InvalidateOverlays()
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = strength,
|
||||
Category = "Trait",
|
||||
CreatesInterest = true,
|
||||
LabelTemplate = "Trait: {id} ({a})"
|
||||
};
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds => Entries.Keys;
|
||||
|
||||
public static bool HasAuthored(string id) =>
|
||||
!string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim());
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry))
|
||||
{
|
||||
return entry;
|
||||
Entries.Clear();
|
||||
_appliedGeneration = -1;
|
||||
}
|
||||
|
||||
string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim();
|
||||
return new DiscreteEventEntry
|
||||
private static void Ensure()
|
||||
{
|
||||
Id = key,
|
||||
EventStrength = 40f,
|
||||
Category = "Trait",
|
||||
LabelTemplate = "Trait: {id} ({a})",
|
||||
IsFallback = true
|
||||
};
|
||||
if (_appliedGeneration == EventCatalogConfig.Generation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_appliedGeneration = EventCatalogConfig.Generation;
|
||||
EventCatalogConfig.FillTraits(Entries);
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool HasAuthored(string id)
|
||||
{
|
||||
Ensure();
|
||||
return !string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim());
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry))
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
|
||||
string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim();
|
||||
return new DiscreteEventEntry
|
||||
{
|
||||
Id = key,
|
||||
EventStrength = 40f,
|
||||
Category = "Trait",
|
||||
LabelTemplate = "Trait: {id} ({a})",
|
||||
CreatesInterest = true,
|
||||
IsFallback = true
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,83 +13,90 @@ public sealed class WarTypeInterestEntry
|
|||
public bool IsFallback;
|
||||
}
|
||||
|
||||
/// <summary>Authored interest overlay for every live war_types_library asset.</summary>
|
||||
/// <summary>War type dials from <c>event-catalog.json</c> <c>warTypes</c>.</summary>
|
||||
public static partial class EventCatalog
|
||||
{
|
||||
public static class WarType
|
||||
{
|
||||
private static readonly Dictionary<string, WarTypeInterestEntry> Entries =
|
||||
new Dictionary<string, WarTypeInterestEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
static WarType()
|
||||
{
|
||||
Add("normal", 90f, "War: {a}");
|
||||
Add("spite", 88f, "Spite war: {a}");
|
||||
Add("inspire", 86f, "Inspired war: {a}");
|
||||
Add("rebellion", 92f, "Rebellion: {a}");
|
||||
Add("whisper_of_war", 84f, "Whisper of war: {a}");
|
||||
}
|
||||
private static readonly Dictionary<string, WarTypeInterestEntry> Entries =
|
||||
new Dictionary<string, WarTypeInterestEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static void Add(string id, float strength, string labelTemplate)
|
||||
{
|
||||
Entries[id] = new WarTypeInterestEntry
|
||||
private static int _appliedGeneration = -1;
|
||||
|
||||
public static void InvalidateOverlays()
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = strength,
|
||||
Category = "Politics",
|
||||
LabelTemplate = labelTemplate,
|
||||
CreatesInterest = true
|
||||
};
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds => Entries.Keys;
|
||||
|
||||
public static bool HasAuthored(string warTypeId)
|
||||
{
|
||||
return !string.IsNullOrEmpty(warTypeId) && Entries.ContainsKey(warTypeId.Trim());
|
||||
}
|
||||
|
||||
public static bool TryGet(string warTypeId, out WarTypeInterestEntry entry)
|
||||
{
|
||||
entry = null;
|
||||
if (string.IsNullOrEmpty(warTypeId))
|
||||
{
|
||||
return false;
|
||||
Entries.Clear();
|
||||
_appliedGeneration = -1;
|
||||
}
|
||||
|
||||
return Entries.TryGetValue(warTypeId.Trim(), out entry);
|
||||
}
|
||||
|
||||
public static WarTypeInterestEntry GetOrFallback(string warTypeId)
|
||||
{
|
||||
if (TryGet(warTypeId, out WarTypeInterestEntry entry))
|
||||
private static void Ensure()
|
||||
{
|
||||
return entry;
|
||||
if (_appliedGeneration == EventCatalogConfig.Generation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_appliedGeneration = EventCatalogConfig.Generation;
|
||||
EventCatalogConfig.FillWarTypes(Entries);
|
||||
}
|
||||
|
||||
string id = string.IsNullOrEmpty(warTypeId) ? "unknown" : warTypeId.Trim();
|
||||
return new WarTypeInterestEntry
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = 85f,
|
||||
Category = "Politics",
|
||||
LabelTemplate = "War ({id}): {a}",
|
||||
CreatesInterest = true,
|
||||
IsFallback = true
|
||||
};
|
||||
}
|
||||
|
||||
public static string MakeLabel(WarTypeInterestEntry entry, string special1)
|
||||
{
|
||||
if (entry == null)
|
||||
{
|
||||
return "War";
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
string template = string.IsNullOrEmpty(entry.LabelTemplate) ? "War ({id})" : entry.LabelTemplate;
|
||||
return template
|
||||
.Replace("{id}", entry.Id ?? "")
|
||||
.Replace("{a}", special1 ?? "");
|
||||
public static bool HasAuthored(string warTypeId)
|
||||
{
|
||||
Ensure();
|
||||
return !string.IsNullOrEmpty(warTypeId) && Entries.ContainsKey(warTypeId.Trim());
|
||||
}
|
||||
|
||||
public static bool TryGet(string warTypeId, out WarTypeInterestEntry entry)
|
||||
{
|
||||
Ensure();
|
||||
entry = null;
|
||||
if (string.IsNullOrEmpty(warTypeId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Entries.TryGetValue(warTypeId.Trim(), out entry);
|
||||
}
|
||||
|
||||
public static WarTypeInterestEntry GetOrFallback(string warTypeId)
|
||||
{
|
||||
if (TryGet(warTypeId, out WarTypeInterestEntry entry))
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
|
||||
string id = string.IsNullOrEmpty(warTypeId) ? "unknown" : warTypeId.Trim();
|
||||
return new WarTypeInterestEntry
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = 85f,
|
||||
Category = "Politics",
|
||||
LabelTemplate = "War ({id}): {a}",
|
||||
CreatesInterest = true,
|
||||
IsFallback = true
|
||||
};
|
||||
}
|
||||
|
||||
public static string MakeLabel(WarTypeInterestEntry entry, string special1)
|
||||
{
|
||||
if (entry == null)
|
||||
{
|
||||
return "War";
|
||||
}
|
||||
|
||||
string template = string.IsNullOrEmpty(entry.LabelTemplate) ? "War ({id})" : entry.LabelTemplate;
|
||||
return template
|
||||
.Replace("{id}", entry.Id ?? "")
|
||||
.Replace("{a}", special1 ?? "");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,155 +39,105 @@ public sealed class WorldLogEventEntry
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Authored policy for every live WorldLog asset. Game library is inventory; this is presentation/scoring only.
|
||||
/// </summary>
|
||||
/// <summary>WorldLog dials from <c>event-catalog.json</c> <c>worldLog</c>.</summary>
|
||||
public static partial class EventCatalog
|
||||
{
|
||||
public static class WorldLog
|
||||
{
|
||||
private static readonly Dictionary<string, WorldLogEventEntry> Entries =
|
||||
new Dictionary<string, WorldLogEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
static WorldLog()
|
||||
{
|
||||
// Kings
|
||||
Add("king_new", 78f, "Politics", true, "New king: {b} ({a})", 6f, 28f);
|
||||
Add("king_left", 65f, "Politics", true, "King left: {b} ({a})", 6f, 28f);
|
||||
Add("king_fled_capital", 70f, "Politics", true, "King fled capital: {b}", 6f, 28f);
|
||||
Add("king_fled_city", 68f, "Politics", true, "King fled city: {b}", 6f, 28f);
|
||||
Add("king_dead", 80f, "Politics", true, "King died: {b} ({a})", 6f, 28f);
|
||||
Add("king_killed", 85f, "Politics", true, "King killed: {b} ({a})", 6f, 28f);
|
||||
private static readonly Dictionary<string, WorldLogEventEntry> Entries =
|
||||
new Dictionary<string, WorldLogEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Favorites
|
||||
Add("favorite_dead", 82f, "Politics", true, "Favorite fell: {a}", 6f, 28f);
|
||||
Add("favorite_killed", 85f, "Politics", true, "Favorite fell: {a}", 6f, 28f);
|
||||
private static int _appliedGeneration = -1;
|
||||
|
||||
// Cities / kingdoms / clans / wars / alliances
|
||||
Add("city_new", 72f, "Settlement", true, "New city: {a}", 6f, 28f);
|
||||
Add("log_city_revolted", 90f, "Politics", true, "Revolt: {a}", 8f, 35f);
|
||||
Add("city_destroyed", 92f, "Settlement", true, "City destroyed: {a}", 8f, 35f);
|
||||
Add("diplomacy_war_ended", 72f, "Politics", true, "War ended: {a}", 6f, 28f);
|
||||
Add("diplomacy_war_started", 100f, "Politics", true, "War: {a} vs {b}", 8f, 35f);
|
||||
Add("total_war_started", 100f, "Politics", true, "Total war: {a}", 8f, 35f);
|
||||
Add("alliance_new", 70f, "Politics", true, "Alliance: {a}", 6f, 28f);
|
||||
Add("alliance_dissolved", 68f, "Politics", true, "Alliance dissolved: {a}", 6f, 28f);
|
||||
Add("kingdom_new", 75f, "Settlement", true, "New kingdom: {a}", 6f, 28f);
|
||||
Add("kingdom_destroyed", 100f, "Politics", true, "Kingdom fell: {a}", 8f, 35f);
|
||||
Add("kingdom_shattered", 98f, "Politics", true, "Kingdom shattered: {a}", 8f, 35f);
|
||||
Add("kingdom_fractured", 95f, "Politics", true, "Kingdom fractured: {a}", 8f, 35f);
|
||||
Add("kingdom_royal_clan_new", 74f, "Politics", true, "Royal clan: {a}", 6f, 28f);
|
||||
Add("kingdom_royal_clan_changed", 72f, "Politics", true, "Royal clan: {a}", 6f, 28f);
|
||||
Add("kingdom_royal_clan_dead", 76f, "Politics", true, "Royal clan ended: {a}", 6f, 28f);
|
||||
|
||||
// Disasters (WorldLog ids; linked to disasters library by naming)
|
||||
Add("disaster_tornado", 95f, "Disaster", true, "Tornado: {a}", 8f, 35f);
|
||||
Add("disaster_meteorite", 95f, "Disaster", true, "Meteorite: {a}", 8f, 35f);
|
||||
Add("disaster_hellspawn", 96f, "Disaster", true, "Hellspawn: {a}", 8f, 35f);
|
||||
Add("disaster_earthquake", 95f, "Disaster", true, "Earthquake: {a}", 8f, 35f);
|
||||
Add("disaster_greg_abominations", 96f, "Disaster", true, "Greg abominations: {a}", 8f, 35f);
|
||||
Add("disaster_ice_ones", 95f, "Disaster", true, "Ice ones awaken: {a}", 8f, 35f);
|
||||
Add("disaster_sudden_snowman", 90f, "Disaster", true, "Sudden snowman: {a}", 8f, 35f);
|
||||
Add("disaster_garden_surprise", 90f, "Disaster", true, "Garden surprise: {a}", 8f, 35f);
|
||||
Add("disaster_dragon_from_farlands", 98f, "Disaster", true, "Dragon from the farlands: {a}", 8f, 35f);
|
||||
Add("disaster_bandits", 92f, "Disaster", true, "Bandit raid: {a}", 8f, 35f);
|
||||
Add("disaster_alien_invasion", 97f, "Disaster", true, "Alien invasion: {a}", 8f, 35f);
|
||||
Add("disaster_biomass", 96f, "Disaster", true, "Biomass outbreak: {a}", 8f, 35f);
|
||||
Add("disaster_tumor", 96f, "Disaster", true, "Tumor outbreak: {a}", 8f, 35f);
|
||||
Add("disaster_heatwave", 88f, "Disaster", true, "Heatwave: {a}", 8f, 35f);
|
||||
Add("disaster_evil_mage", 94f, "Disaster", true, "Evil mage: {a}", 8f, 35f);
|
||||
Add("disaster_underground_necromancer", 95f, "Disaster", true, "Underground necromancer: {a}", 8f, 35f);
|
||||
Add("disaster_mad_thoughts", 90f, "Disaster", true, "Mad thoughts: {a}", 8f, 35f);
|
||||
|
||||
// Harness / internal - authored but never owns camera interest
|
||||
Add("auto_tester", 10f, "Harness", false, "Auto tester: {a}", 2f, 6f);
|
||||
}
|
||||
|
||||
private static void Add(
|
||||
string id,
|
||||
float strength,
|
||||
string category,
|
||||
bool createsInterest,
|
||||
string labelTemplate,
|
||||
float minWatch,
|
||||
float maxWatch)
|
||||
{
|
||||
Entries[id] = new WorldLogEventEntry
|
||||
public static void InvalidateOverlays()
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = strength,
|
||||
Category = category,
|
||||
CreatesInterest = createsInterest,
|
||||
ChronicleEligible = createsInterest && strength >= 65f,
|
||||
LabelTemplate = labelTemplate,
|
||||
MinWatch = minWatch,
|
||||
MaxWatch = maxWatch
|
||||
};
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds => Entries.Keys;
|
||||
|
||||
public static bool HasAuthored(string assetId)
|
||||
{
|
||||
return !string.IsNullOrEmpty(assetId) && Entries.ContainsKey(assetId.Trim());
|
||||
}
|
||||
|
||||
public static bool TryGet(string assetId, out WorldLogEventEntry entry)
|
||||
{
|
||||
entry = null;
|
||||
if (string.IsNullOrEmpty(assetId))
|
||||
{
|
||||
return false;
|
||||
Entries.Clear();
|
||||
_appliedGeneration = -1;
|
||||
}
|
||||
|
||||
return Entries.TryGetValue(assetId.Trim(), out entry);
|
||||
}
|
||||
|
||||
public static WorldLogEventEntry GetOrFallback(string assetId)
|
||||
{
|
||||
if (TryGet(assetId, out WorldLogEventEntry entry))
|
||||
private static void Ensure()
|
||||
{
|
||||
return entry;
|
||||
if (_appliedGeneration == EventCatalogConfig.Generation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_appliedGeneration = EventCatalogConfig.Generation;
|
||||
EventCatalogConfig.FillWorldLog(Entries);
|
||||
}
|
||||
|
||||
string id = string.IsNullOrEmpty(assetId) ? "unknown" : assetId.Trim();
|
||||
float strength = 40f;
|
||||
string category = "WorldLog";
|
||||
if (id.IndexOf("disaster", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
strength = 95f;
|
||||
category = "Disaster";
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
return new WorldLogEventEntry
|
||||
public static bool HasAuthored(string assetId)
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = strength,
|
||||
Category = category,
|
||||
CreatesInterest = true,
|
||||
ChronicleEligible = strength >= 65f,
|
||||
LabelTemplate = "{id}: {a}",
|
||||
MinWatch = strength >= 90f ? 8f : 6f,
|
||||
MaxWatch = strength >= 90f ? 35f : 28f,
|
||||
IsFallback = true
|
||||
};
|
||||
}
|
||||
|
||||
public static bool TryGetEventStrength(string assetId, out float eventStrength)
|
||||
{
|
||||
WorldLogEventEntry entry = GetOrFallback(assetId);
|
||||
eventStrength = entry.EventStrength;
|
||||
return !string.IsNullOrEmpty(assetId);
|
||||
}
|
||||
|
||||
public static string MakeLabel(WorldLogMessage message)
|
||||
{
|
||||
if (message == null)
|
||||
{
|
||||
return "event";
|
||||
Ensure();
|
||||
return !string.IsNullOrEmpty(assetId) && Entries.ContainsKey(assetId.Trim());
|
||||
}
|
||||
|
||||
return GetOrFallback(message.asset_id).MakeLabel(message);
|
||||
public static bool TryGet(string assetId, out WorldLogEventEntry entry)
|
||||
{
|
||||
Ensure();
|
||||
entry = null;
|
||||
if (string.IsNullOrEmpty(assetId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Entries.TryGetValue(assetId.Trim(), out entry);
|
||||
}
|
||||
|
||||
public static WorldLogEventEntry GetOrFallback(string assetId)
|
||||
{
|
||||
if (TryGet(assetId, out WorldLogEventEntry entry))
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
|
||||
string id = string.IsNullOrEmpty(assetId) ? "unknown" : assetId.Trim();
|
||||
float strength = 40f;
|
||||
string category = "WorldLog";
|
||||
if (id.IndexOf("disaster", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
strength = 95f;
|
||||
category = "Disaster";
|
||||
}
|
||||
|
||||
return new WorldLogEventEntry
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = strength,
|
||||
Category = category,
|
||||
CreatesInterest = true,
|
||||
ChronicleEligible = strength >= 65f,
|
||||
LabelTemplate = "{id}: {a}",
|
||||
MinWatch = strength >= 90f ? 8f : 6f,
|
||||
MaxWatch = strength >= 90f ? 35f : 28f,
|
||||
IsFallback = true
|
||||
};
|
||||
}
|
||||
|
||||
public static bool TryGetEventStrength(string assetId, out float eventStrength)
|
||||
{
|
||||
WorldLogEventEntry entry = GetOrFallback(assetId);
|
||||
eventStrength = entry.EventStrength;
|
||||
return !string.IsNullOrEmpty(assetId);
|
||||
}
|
||||
|
||||
public static string MakeLabel(WorldLogMessage message)
|
||||
{
|
||||
if (message == null)
|
||||
{
|
||||
return "event";
|
||||
}
|
||||
|
||||
return GetOrFallback(message.asset_id).MakeLabel(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,11 @@ public sealed class DiscreteEventEntry
|
|||
public string Category = "Event";
|
||||
public bool CreatesInterest = true;
|
||||
public string LabelTemplate = "{a}";
|
||||
/// <summary>
|
||||
/// Optional infinitive clause for decision-style reasons
|
||||
/// (e.g. <c>change the kingdom's culture</c> → <c>{a} decides to …</c>).
|
||||
/// </summary>
|
||||
public string ActionPhrase = "";
|
||||
public bool IsFallback;
|
||||
|
||||
public string MakeLabel(Actor a, Actor b = null)
|
||||
|
|
|
|||
|
|
@ -2,8 +2,12 @@ namespace IdleSpectator;
|
|||
|
||||
/// <summary>
|
||||
/// IdleSpectator event inventory (one type, split across partial files).
|
||||
///
|
||||
/// Layer B (ticker): Activity / Life prose for live ids - completeness.
|
||||
/// Layer A (story camera): only CameraWorthy beats register interest and cut the camera.
|
||||
///
|
||||
/// Call sites: EventCatalog.Status / .Happiness / .Combat / …
|
||||
/// Director only ranks; feeds register from these dials.
|
||||
/// Director only ranks A candidates; it does not author inventory.
|
||||
///
|
||||
/// Domain files under Events/Catalogs/:
|
||||
/// EventCatalog.Status.cs
|
||||
|
|
@ -16,11 +20,56 @@ namespace IdleSpectator;
|
|||
/// EventCatalog.Relationship.cs
|
||||
/// EventCatalog.Disaster.cs
|
||||
/// EventCatalog.WarType.cs
|
||||
/// EventCatalog.Libraries.cs (Spell, Power, Decision, Item, SubspeciesTrait, Gene, Phenotype, WorldLaw, Biome)
|
||||
/// EventCatalog.Libraries.cs
|
||||
/// Combat policy lives in this file.
|
||||
/// </summary>
|
||||
public static partial class EventCatalog
|
||||
{
|
||||
/// <summary>Drop cached catalog rows so the next Ensure reloads from event-catalog.json.</summary>
|
||||
public static void InvalidateAllOverlays()
|
||||
{
|
||||
Decision.InvalidateOverlays();
|
||||
Happiness.InvalidateOverlays();
|
||||
Status.InvalidateOverlays();
|
||||
WorldLog.InvalidateOverlays();
|
||||
Plot.InvalidateOverlays();
|
||||
Relationship.InvalidateOverlays();
|
||||
Trait.InvalidateOverlays();
|
||||
Book.InvalidateOverlays();
|
||||
Era.InvalidateOverlays();
|
||||
Disaster.InvalidateOverlays();
|
||||
WarType.InvalidateOverlays();
|
||||
Spell.InvalidateOverlays();
|
||||
Power.InvalidateOverlays();
|
||||
Item.InvalidateOverlays();
|
||||
Gene.InvalidateOverlays();
|
||||
Phenotype.InvalidateOverlays();
|
||||
WorldLaw.InvalidateOverlays();
|
||||
SubspeciesTrait.InvalidateOverlays();
|
||||
Biome.InvalidateOverlays();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Layer A gate for happiness: Signal may own the camera; Ambient/Aggregate are B-only.
|
||||
/// </summary>
|
||||
public static bool IsCameraWorthy(HappinessPresentationTier presentation) =>
|
||||
presentation == HappinessPresentationTier.Signal;
|
||||
|
||||
/// <summary>Layer A gate for discrete / status / worldlog rows.</summary>
|
||||
public static bool IsCameraWorthy(bool createsInterest) => createsInterest;
|
||||
|
||||
public static bool IsCameraWorthy(DiscreteEventEntry entry) =>
|
||||
entry != null && entry.CreatesInterest && !entry.IsFallback;
|
||||
|
||||
public static bool IsCameraWorthy(StatusInterestEntry entry) =>
|
||||
entry != null && entry.CreatesInterest && !entry.IsFallback;
|
||||
|
||||
public static bool IsCameraWorthy(WorldLogEventEntry entry) =>
|
||||
entry != null && entry.CreatesInterest && !entry.IsFallback;
|
||||
|
||||
public static bool IsCameraWorthy(HappinessCatalogEntry entry) =>
|
||||
entry != null && IsCameraWorthy(entry.Presentation);
|
||||
|
||||
/// <summary>Combat camera policy (activity + scanner share one key / dials).</summary>
|
||||
public static class Combat
|
||||
{
|
||||
|
|
|
|||
|
|
@ -50,6 +50,11 @@ public static class EventReason
|
|||
return NameOrSomeone(a) + " is seeking a lover";
|
||||
}
|
||||
|
||||
public static string BecomeAlpha(Actor a)
|
||||
{
|
||||
return NameOrSomeone(a) + " becomes the family alpha";
|
||||
}
|
||||
|
||||
public static string Hatch(Actor a)
|
||||
{
|
||||
return NameOrSomeone(a) + " hatches from an egg";
|
||||
|
|
@ -210,6 +215,173 @@ public static class EventReason
|
|||
return gained ? an + " gains " + id : an + " loses " + id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plot labels are phase-aware: start templates stay progressive; finish verbs fire on complete.
|
||||
/// </summary>
|
||||
public static string Plot(Actor a, string template, string phase, string plotId = "")
|
||||
{
|
||||
string filled = Apply(
|
||||
string.IsNullOrEmpty(template) ? "{a} is plotting" : template,
|
||||
a,
|
||||
id: plotId);
|
||||
string p = (phase ?? "active").Trim().ToLowerInvariant();
|
||||
switch (p)
|
||||
{
|
||||
case "complete":
|
||||
case "finished":
|
||||
case "finish":
|
||||
return RewritePlotProgressive(filled, "finishes ");
|
||||
case "cancel":
|
||||
case "cancelled":
|
||||
case "canceled":
|
||||
return RewritePlotAbandon(filled);
|
||||
case "leave":
|
||||
return NameOrSomeone(a) + " leaves a plot";
|
||||
case "join":
|
||||
return filled;
|
||||
case "new":
|
||||
case "active":
|
||||
default:
|
||||
return RewritePlotStart(filled);
|
||||
}
|
||||
}
|
||||
|
||||
private static string RewritePlotStart(string label)
|
||||
{
|
||||
if (string.IsNullOrEmpty(label))
|
||||
{
|
||||
return label;
|
||||
}
|
||||
|
||||
// Finish-style verbs on start → "plots to …"
|
||||
string[] finishVerbs =
|
||||
{
|
||||
"summons ", "casts ", "splits ", "causes ", "performs ",
|
||||
};
|
||||
foreach (string verb in finishVerbs)
|
||||
{
|
||||
int idx = IndexOfWordPhrase(label, verb);
|
||||
if (idx < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string infinitive = InfinitiveFromFinishVerb(verb.TrimEnd());
|
||||
return label.Substring(0, idx) + "plots to " + infinitive + label.Substring(idx + verb.Length);
|
||||
}
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
private static string RewritePlotProgressive(string label, string replacementPrefix)
|
||||
{
|
||||
if (string.IsNullOrEmpty(label))
|
||||
{
|
||||
return label;
|
||||
}
|
||||
|
||||
string[] progressives =
|
||||
{
|
||||
"is plotting ", "is joining ", "is breaking ", "is ending ",
|
||||
"is writing ", "is forging ", "is founding ", "is ascending ",
|
||||
};
|
||||
foreach (string progressive in progressives)
|
||||
{
|
||||
int idx = IndexOfWordPhrase(label, progressive);
|
||||
if (idx >= 0)
|
||||
{
|
||||
// "is plotting " → "plotting " after dropping "is "
|
||||
return label.Substring(0, idx)
|
||||
+ replacementPrefix
|
||||
+ progressive.Substring(3)
|
||||
+ label.Substring(idx + progressive.Length);
|
||||
}
|
||||
}
|
||||
|
||||
// Already a finish verb ("summons", "casts", …) - correct for complete.
|
||||
return label;
|
||||
}
|
||||
|
||||
private static string RewritePlotAbandon(string label)
|
||||
{
|
||||
if (string.IsNullOrEmpty(label))
|
||||
{
|
||||
return label;
|
||||
}
|
||||
|
||||
string progressive = RewritePlotProgressive(label, "abandons ");
|
||||
if (progressive != label)
|
||||
{
|
||||
return progressive;
|
||||
}
|
||||
|
||||
string[] finishVerbs =
|
||||
{
|
||||
"summons ", "casts ", "splits ", "causes ", "performs ",
|
||||
};
|
||||
foreach (string verb in finishVerbs)
|
||||
{
|
||||
int idx = IndexOfWordPhrase(label, verb);
|
||||
if (idx < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string gerund = GerundFromFinishVerb(verb.TrimEnd());
|
||||
return label.Substring(0, idx) + "abandons " + gerund + label.Substring(idx + verb.Length);
|
||||
}
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
private static string InfinitiveFromFinishVerb(string verb)
|
||||
{
|
||||
switch (verb)
|
||||
{
|
||||
case "summons":
|
||||
return "summon ";
|
||||
case "casts":
|
||||
return "cast ";
|
||||
case "splits":
|
||||
return "split ";
|
||||
case "causes":
|
||||
return "cause ";
|
||||
case "performs":
|
||||
return "perform ";
|
||||
default:
|
||||
return verb + " ";
|
||||
}
|
||||
}
|
||||
|
||||
private static string GerundFromFinishVerb(string verb)
|
||||
{
|
||||
switch (verb)
|
||||
{
|
||||
case "summons":
|
||||
return "summoning ";
|
||||
case "casts":
|
||||
return "casting ";
|
||||
case "splits":
|
||||
return "splitting ";
|
||||
case "causes":
|
||||
return "causing ";
|
||||
case "performs":
|
||||
return "performing ";
|
||||
default:
|
||||
return verb + " ";
|
||||
}
|
||||
}
|
||||
|
||||
private static int IndexOfWordPhrase(string haystack, string phrase)
|
||||
{
|
||||
if (string.IsNullOrEmpty(haystack) || string.IsNullOrEmpty(phrase))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
return haystack.IndexOf(phrase, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static string Boat(Actor a, string phase)
|
||||
{
|
||||
string an = NameOrSomeone(a);
|
||||
|
|
@ -258,7 +430,7 @@ public static class EventReason
|
|||
case "item":
|
||||
return an + " forges " + id;
|
||||
case "decision":
|
||||
return an + " decides " + id;
|
||||
return Decision(a, assetId);
|
||||
case "gene":
|
||||
return an + " gains gene " + id;
|
||||
case "phenotype":
|
||||
|
|
@ -272,6 +444,18 @@ public static class EventReason
|
|||
}
|
||||
}
|
||||
|
||||
public static string Decision(Actor a, string decisionId)
|
||||
{
|
||||
string an = NameOrSomeone(a);
|
||||
string action = EventCatalog.Decision.ActionPhraseFor(decisionId);
|
||||
if (string.IsNullOrEmpty(action))
|
||||
{
|
||||
return an + " makes a decision";
|
||||
}
|
||||
|
||||
return an + " decides to " + action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply a sentence template with {a}/{b}/{id}. Prefer typed helpers for new copy.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -13,8 +13,9 @@ public static class BookInterestFeed
|
|||
}
|
||||
|
||||
DiscreteEventEntry entry = EventCatalog.Book.GetOrFallback(bookTypeId);
|
||||
if (!entry.CreatesInterest)
|
||||
if (!EventCatalog.IsCameraWorthy(entry))
|
||||
{
|
||||
InterestDropLog.Record("createsInterest=false", "book");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,8 +41,15 @@ public static class DeferredInterestFeed
|
|||
public static void EmitPower(string powerId, Vector3 position, Actor nearUnit)
|
||||
{
|
||||
DiscreteEventEntry entry = EventCatalog.Power.GetOrFallback(powerId);
|
||||
if (!entry.CreatesInterest || AgentHarness.Busy)
|
||||
if (AgentHarness.Busy)
|
||||
{
|
||||
InterestDropLog.Record("busy", "power");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EventCatalog.IsCameraWorthy(entry))
|
||||
{
|
||||
InterestDropLog.Record("createsInterest=false", "power:" + (powerId ?? ""));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -110,8 +117,9 @@ public static class DeferredInterestFeed
|
|||
}
|
||||
|
||||
DiscreteEventEntry entry = EventCatalog.WorldLaw.GetOrFallback(lawId);
|
||||
if (!entry.CreatesInterest)
|
||||
if (!EventCatalog.IsCameraWorthy(entry))
|
||||
{
|
||||
InterestDropLog.Record("createsInterest=false", entry.Id ?? "");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -135,8 +143,9 @@ public static class DeferredInterestFeed
|
|||
}
|
||||
|
||||
DiscreteEventEntry entry = EventCatalog.Biome.GetOrFallback(biomeId);
|
||||
if (!entry.CreatesInterest)
|
||||
if (!EventCatalog.IsCameraWorthy(entry))
|
||||
{
|
||||
InterestDropLog.Record("createsInterest=false", entry.Id ?? "");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -166,8 +175,9 @@ public static class DeferredInterestFeed
|
|||
}
|
||||
|
||||
DiscreteEventEntry entry = lookup(assetId);
|
||||
if (!entry.CreatesInterest)
|
||||
if (!EventCatalog.IsCameraWorthy(entry))
|
||||
{
|
||||
InterestDropLog.Record("createsInterest=false", entry.Id ?? "");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ public static class InterestFeeds
|
|||
HappinessDrain.Clear();
|
||||
_lastScannerAt = -999f;
|
||||
CivicBoostUntil.Clear();
|
||||
InterestDropLog.Clear();
|
||||
}
|
||||
|
||||
public static ulong HappinessCursor => _happinessCursor;
|
||||
|
|
@ -94,8 +95,11 @@ public static class InterestFeeds
|
|||
}
|
||||
|
||||
WorldLogEventEntry entry = EventCatalog.WorldLog.GetOrFallback(message.asset_id);
|
||||
if (!entry.CreatesInterest)
|
||||
if (!EventCatalog.IsCameraWorthy(entry))
|
||||
{
|
||||
InterestDropLog.Record(
|
||||
"createsInterest=false",
|
||||
"worldlog:" + (message.asset_id ?? ""));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -370,7 +374,7 @@ public static class InterestFeeds
|
|||
}
|
||||
|
||||
// Egg timer end is the reliable hatch beat. Happiness just_got_out_of_egg is often
|
||||
// suppressed (no emotions / still flagged egg), so status loss owns the camera reason.
|
||||
// suppressed (no emotions), so status loss owns the camera reason.
|
||||
if (!gained && string.Equals(statusId.Trim(), "egg", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
EmitHatch(actor, "status_egg_loss");
|
||||
|
|
@ -379,12 +383,14 @@ public static class InterestFeeds
|
|||
|
||||
if (!gained)
|
||||
{
|
||||
InterestDropLog.Record("status_loss_ignored", statusId);
|
||||
return;
|
||||
}
|
||||
|
||||
StatusInterestEntry entry = EventCatalog.Status.GetOrFallback(statusId);
|
||||
if (!entry.CreatesInterest)
|
||||
if (!EventCatalog.IsCameraWorthy(entry))
|
||||
{
|
||||
InterestDropLog.Record("createsInterest=false", "status:" + statusId);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -473,6 +479,67 @@ public static class InterestFeeds
|
|||
|
||||
private static string HatchKey(long subjectId) => "hatch:" + subjectId;
|
||||
|
||||
private static string AlphaKey(long subjectId) => "alpha:" + subjectId;
|
||||
|
||||
/// <summary>
|
||||
/// Family alpha succession. Shared key with happiness <c>become_alpha</c> so emotion and
|
||||
/// emotion-less paths merge into one camera beat.
|
||||
/// </summary>
|
||||
public static void EmitAlpha(Actor actor, string source)
|
||||
{
|
||||
if (actor == null || !actor.isAlive())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (AgentHarness.Busy)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
long id = SafeId(actor);
|
||||
if (id == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DiscreteEventEntry entry = EventCatalog.Relationship.GetOrFallback("become_alpha");
|
||||
if (!EventCatalog.IsCameraWorthy(entry))
|
||||
{
|
||||
InterestDropLog.Record("createsInterest=false", "become_alpha");
|
||||
return;
|
||||
}
|
||||
|
||||
float strength = Mathf.Max(
|
||||
entry.EventStrength,
|
||||
InterestScoring.EventStrengthForHappiness("become_alpha"));
|
||||
var candidate = new InterestCandidate
|
||||
{
|
||||
Key = AlphaKey(id),
|
||||
LeadKind = InterestLeadKind.EventLed,
|
||||
Category = string.IsNullOrEmpty(entry.Category) ? "LifeChapter" : entry.Category,
|
||||
Source = string.IsNullOrEmpty(source) ? "alpha" : source,
|
||||
EventStrength = strength,
|
||||
VisualConfidence = 0.8f,
|
||||
Position = actor.current_position,
|
||||
FollowUnit = actor,
|
||||
SubjectId = id,
|
||||
Label = EventReason.BecomeAlpha(actor),
|
||||
AssetId = SafeAsset(actor),
|
||||
SpeciesId = SafeAsset(actor),
|
||||
HappinessEffectId = "become_alpha",
|
||||
CreatedAt = Time.unscaledTime,
|
||||
LastSeenAt = Time.unscaledTime,
|
||||
ExpiresAt = Time.unscaledTime + 25f,
|
||||
MinWatch = 5f,
|
||||
MaxWatch = 22f,
|
||||
Completion = InterestCompletionKind.FixedDwell
|
||||
};
|
||||
candidate.ParticipantIds.Add(id);
|
||||
InterestScoring.ScoreCheap(candidate);
|
||||
EventFeedUtil.RegisterCandidate(candidate);
|
||||
}
|
||||
|
||||
public static void OnChronicleMilestone(Actor subject, string milestoneKey, Actor related, string label)
|
||||
{
|
||||
if (subject == null || !subject.isAlive() || string.IsNullOrEmpty(milestoneKey))
|
||||
|
|
@ -535,20 +602,22 @@ public static class InterestFeeds
|
|||
|
||||
private static void IngestHappiness(HappinessOccurrence occ)
|
||||
{
|
||||
if (occ == null || !occ.Applied || occ.SuppressedByPsychopath)
|
||||
if (occ == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Harness scenarios inject via SourceHook.Harness; ignore live world noise while Busy.
|
||||
if (!occ.Applied || occ.SuppressedByPsychopath)
|
||||
{
|
||||
InterestDropLog.Record(
|
||||
occ.SuppressedByPsychopath ? "suppressed" : "not_applied",
|
||||
occ.EffectId ?? "");
|
||||
return;
|
||||
}
|
||||
|
||||
if (AgentHarness.Busy && occ.SourceHook != HappinessSourceHook.Harness)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (occ.Presentation == HappinessPresentationTier.Ambient)
|
||||
{
|
||||
// Enrichment only - never owns the camera.
|
||||
InterestDropLog.Record("busy", occ.EffectId ?? "");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -578,6 +647,14 @@ public static class InterestFeeds
|
|||
StampCivicBoost(kingdom, occ.EffectId, 25f + Mathf.Min(40f, occ.TotalAffectedCount));
|
||||
}
|
||||
|
||||
InterestDropLog.Record("aggregate", occ.EffectId ?? "");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EventCatalog.IsCameraWorthy(occ.Presentation))
|
||||
{
|
||||
// Layer B only - never owns the camera.
|
||||
InterestDropLog.Record("ambient", occ.EffectId ?? "");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -598,12 +675,23 @@ public static class InterestFeeds
|
|||
bool grief = occ.EffectId != null && occ.EffectId.StartsWith("death_");
|
||||
float strength = InterestScoring.EventStrengthForHappiness(occ.EffectId);
|
||||
bool hatchMoment = IsFreshLifeMoment(occ.EffectId);
|
||||
bool alphaMoment = IsAlphaMoment(occ.EffectId);
|
||||
|
||||
// Hatch shares a key with status egg-loss so both paths merge into one camera beat.
|
||||
string key = hatchMoment
|
||||
? HatchKey(occ.SubjectId)
|
||||
: ("happiness:" + occ.EffectId + ":" + occ.SubjectId + ":" + occ.RelatedId
|
||||
+ ":" + CorrBucket(occ.CorrelationKey));
|
||||
// Hatch / alpha share keys with status-loss / Family.setAlpha so paths merge.
|
||||
string key;
|
||||
if (hatchMoment)
|
||||
{
|
||||
key = HatchKey(occ.SubjectId);
|
||||
}
|
||||
else if (alphaMoment)
|
||||
{
|
||||
key = AlphaKey(occ.SubjectId);
|
||||
}
|
||||
else
|
||||
{
|
||||
key = "happiness:" + occ.EffectId + ":" + occ.SubjectId + ":" + occ.RelatedId
|
||||
+ ":" + CorrBucket(occ.CorrelationKey);
|
||||
}
|
||||
|
||||
// Hatch: short queue TTL, but on-camera floor comes from minCameraDwell.
|
||||
float ttl = hatchMoment ? 7f : 35f;
|
||||
|
|
@ -614,11 +702,23 @@ public static class InterestFeeds
|
|||
strength = Mathf.Max(strength, 88f);
|
||||
}
|
||||
|
||||
if (alphaMoment)
|
||||
{
|
||||
strength = Mathf.Max(strength, EventCatalog.Relationship.GetOrFallback("become_alpha").EventStrength);
|
||||
ttl = 25f;
|
||||
minWatch = 5f;
|
||||
maxWatch = 22f;
|
||||
}
|
||||
|
||||
string label;
|
||||
if (hatchMoment)
|
||||
{
|
||||
label = EventReason.Hatch(subject);
|
||||
}
|
||||
else if (alphaMoment)
|
||||
{
|
||||
label = EventReason.BecomeAlpha(subject);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(occ.PlainClause))
|
||||
{
|
||||
label = occ.SubjectName + " " + occ.PlainClause;
|
||||
|
|
@ -633,7 +733,9 @@ public static class InterestFeeds
|
|||
Key = key,
|
||||
LeadKind = InterestLeadKind.EventLed,
|
||||
Category = grief ? "FamilyEmotion" : MapHappinessCategory(cat),
|
||||
Source = hatchMoment ? "happiness_hatch" : "happiness",
|
||||
Source = hatchMoment
|
||||
? "happiness_hatch"
|
||||
: (alphaMoment ? "happiness_alpha" : "happiness"),
|
||||
EventStrength = strength,
|
||||
VisualConfidence = 0.75f,
|
||||
Position = occ.Position != Vector3.zero ? occ.Position : subject.current_position,
|
||||
|
|
@ -871,6 +973,12 @@ public static class InterestFeeds
|
|||
|| string.Equals(id, "just_hatched", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool IsAlphaMoment(string effectId)
|
||||
{
|
||||
return !string.IsNullOrEmpty(effectId)
|
||||
&& string.Equals(effectId.Trim(), "become_alpha", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string MapHappinessCategory(string cat)
|
||||
{
|
||||
switch ((cat ?? "").ToLowerInvariant())
|
||||
|
|
|
|||
|
|
@ -13,8 +13,9 @@ public static class MetaInterestFeed
|
|||
}
|
||||
|
||||
DiscreteEventEntry entry = EventCatalog.Era.GetOrFallback(eraId);
|
||||
if (!entry.CreatesInterest)
|
||||
if (!EventCatalog.IsCameraWorthy(entry))
|
||||
{
|
||||
InterestDropLog.Record("createsInterest=false", "meta");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,8 +15,9 @@ public static class PlotInterestFeed
|
|||
}
|
||||
|
||||
DiscreteEventEntry entry = EventCatalog.Plot.GetOrFallback(plotAssetId);
|
||||
if (!entry.CreatesInterest)
|
||||
if (!EventCatalog.IsCameraWorthy(entry))
|
||||
{
|
||||
InterestDropLog.Record("createsInterest=false", "plot");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -27,7 +28,7 @@ public static class PlotInterestFeed
|
|||
return;
|
||||
}
|
||||
|
||||
string label = EventReason.Apply(entry.LabelTemplate, follow, id: entry.Id);
|
||||
string label = EventReason.Plot(follow, entry.LabelTemplate, phase, entry.Id);
|
||||
string key = "plot:" + entry.Id + ":" + (phase ?? "active") + ":" + EventFeedUtil.SafeId(follow);
|
||||
EventFeedUtil.Register(
|
||||
key,
|
||||
|
|
|
|||
|
|
@ -5,16 +5,23 @@ namespace IdleSpectator;
|
|||
/// <summary>Registers relationship lifecycle interest candidates under the ownership contract.</summary>
|
||||
public static class RelationshipInterestFeed
|
||||
{
|
||||
private const float FoundingMergeSeconds = 0.35f;
|
||||
private static float _foundingAt = -999f;
|
||||
private static long _foundingActorId;
|
||||
|
||||
public static void Emit(string eventId, Actor subject, Actor related, string labelOverride = null)
|
||||
{
|
||||
if (AgentHarness.Busy || subject == null || !subject.isAlive())
|
||||
if ((AgentHarness.Busy && !AgentHarness.ForceRelationshipFeeds)
|
||||
|| subject == null
|
||||
|| !subject.isAlive())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DiscreteEventEntry entry = EventCatalog.Relationship.GetOrFallback(eventId);
|
||||
if (!entry.CreatesInterest)
|
||||
if (!EventCatalog.IsCameraWorthy(entry))
|
||||
{
|
||||
InterestDropLog.Record("createsInterest=false", "relationship:" + (eventId ?? ""));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -34,6 +41,29 @@ public static class RelationshipInterestFeed
|
|||
? labelOverride
|
||||
: TypedLabel(eventId, subject, relatedAlive);
|
||||
|
||||
// BehFamilyGroupNew always calls newFamily first - keep family_group_new on but
|
||||
// share the founding interest key so one orange reason wins for that beat.
|
||||
if (eventId == "family_group_new" && IsRecentFounding(subject))
|
||||
{
|
||||
string foundingKey = FoundingKey(subject);
|
||||
if (InterestRegistry.TryGet(foundingKey, out _))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EventFeedUtil.Register(
|
||||
foundingKey,
|
||||
"relationship",
|
||||
entry.Category,
|
||||
label,
|
||||
entry.Id,
|
||||
entry.EventStrength,
|
||||
subject.current_position,
|
||||
subject,
|
||||
related: relatedAlive);
|
||||
return;
|
||||
}
|
||||
|
||||
if (claimsPeer && relatedAlive == null)
|
||||
{
|
||||
string soloKey = "rel:" + entry.Id + ":" + EventFeedUtil.SafeId(subject) + ":solo";
|
||||
|
|
@ -64,7 +94,7 @@ public static class RelationshipInterestFeed
|
|||
|
||||
public static void EmitFamily(string eventId, object family, Actor anchor)
|
||||
{
|
||||
if (AgentHarness.Busy)
|
||||
if (AgentHarness.Busy && !AgentHarness.ForceRelationshipFeeds)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -84,6 +114,12 @@ public static class RelationshipInterestFeed
|
|||
Actor related = ActorRelation.FirstFamilyPeer(follow);
|
||||
string label = TypedLabel(eventId, follow, related);
|
||||
string key = "rel:" + entry.Id + ":" + EventFeedUtil.SafeId(follow);
|
||||
if (eventId == "new_family")
|
||||
{
|
||||
NoteFounding(follow);
|
||||
key = FoundingKey(follow);
|
||||
}
|
||||
|
||||
EventFeedUtil.Register(
|
||||
key,
|
||||
"relationship",
|
||||
|
|
@ -96,6 +132,31 @@ public static class RelationshipInterestFeed
|
|||
related: related);
|
||||
}
|
||||
|
||||
private static void NoteFounding(Actor anchor)
|
||||
{
|
||||
_foundingAt = Time.unscaledTime;
|
||||
_foundingActorId = EventFeedUtil.SafeId(anchor);
|
||||
}
|
||||
|
||||
private static bool IsRecentFounding(Actor subject)
|
||||
{
|
||||
if (subject == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
long id = EventFeedUtil.SafeId(subject);
|
||||
if (id == 0 || id != _foundingActorId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Time.unscaledTime - _foundingAt <= FoundingMergeSeconds;
|
||||
}
|
||||
|
||||
private static string FoundingKey(Actor anchor) =>
|
||||
"rel:new_family:" + EventFeedUtil.SafeId(anchor);
|
||||
|
||||
private static string TypedLabel(string eventId, Actor subject, Actor related)
|
||||
{
|
||||
switch (eventId)
|
||||
|
|
@ -106,6 +167,8 @@ public static class RelationshipInterestFeed
|
|||
return EventReason.LoverParted(subject);
|
||||
case "find_lover":
|
||||
return EventReason.SeekingLover(subject);
|
||||
case "become_alpha":
|
||||
return EventReason.BecomeAlpha(subject);
|
||||
case "add_child":
|
||||
return EventReason.NewChild(subject, related);
|
||||
case "baby_created":
|
||||
|
|
|
|||
|
|
@ -60,8 +60,9 @@ public static class WarInterestFeed
|
|||
{
|
||||
string warTypeId = ReadWarTypeId(war);
|
||||
WarTypeInterestEntry entry = EventCatalog.WarType.GetOrFallback(warTypeId);
|
||||
if (!entry.CreatesInterest)
|
||||
if (entry == null || !EventCatalog.IsCameraWorthy(entry.CreatesInterest))
|
||||
{
|
||||
InterestDropLog.Record("createsInterest=false", "war:" + (warTypeId ?? ""));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
108
IdleSpectator/Events/InterestDropLog.cs
Normal file
108
IdleSpectator/Events/InterestDropLog.cs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using NeoModLoader.services;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>
|
||||
/// Ring buffer of why a beat stayed Layer B or failed to take / keep Layer A camera.
|
||||
/// </summary>
|
||||
public static class InterestDropLog
|
||||
{
|
||||
public const int Capacity = 48;
|
||||
|
||||
private static readonly string[] Buffer = new string[Capacity];
|
||||
private static int _write;
|
||||
private static int _count;
|
||||
private static float _lastPlayerLogAt = -999f;
|
||||
private const float PlayerLogInterval = 8f;
|
||||
|
||||
public static int Count => _count;
|
||||
|
||||
public static void Clear()
|
||||
{
|
||||
_write = 0;
|
||||
_count = 0;
|
||||
for (int i = 0; i < Capacity; i++)
|
||||
{
|
||||
Buffer[i] = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Record(string reason, string detail = "")
|
||||
{
|
||||
if (string.IsNullOrEmpty(reason))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string line = string.IsNullOrEmpty(detail)
|
||||
? reason
|
||||
: (reason + ": " + detail);
|
||||
Buffer[_write] = line;
|
||||
_write = (_write + 1) % Capacity;
|
||||
if (_count < Capacity)
|
||||
{
|
||||
_count++;
|
||||
}
|
||||
|
||||
float now = UnityEngine.Time.unscaledTime;
|
||||
if (now - _lastPlayerLogAt >= PlayerLogInterval)
|
||||
{
|
||||
_lastPlayerLogAt = now;
|
||||
try
|
||||
{
|
||||
LogService.LogInfo("[IdleSpectator][DROP] " + line);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore log failures
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Newest-first snapshot for harness / diagnostics.</summary>
|
||||
public static List<string> Snapshot(int max = 12)
|
||||
{
|
||||
var list = new List<string>(Math.Min(max, _count));
|
||||
if (_count == 0 || max <= 0)
|
||||
{
|
||||
return list;
|
||||
}
|
||||
|
||||
int n = Math.Min(max, _count);
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
int idx = (_write - 1 - i + Capacity) % Capacity;
|
||||
if (!string.IsNullOrEmpty(Buffer[idx]))
|
||||
{
|
||||
list.Add(Buffer[idx]);
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public static string FormatRecent(int max = 8)
|
||||
{
|
||||
List<string> lines = Snapshot(max);
|
||||
if (lines.Count == 0)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
for (int i = 0; i < lines.Count; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
sb.Append(" | ");
|
||||
}
|
||||
|
||||
sb.Append(lines[i]);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
using ai.behaviours;
|
||||
using HarmonyLib;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>
|
||||
/// Authoritative happiness hooks. Universal changeHappiness covers every effect id;
|
||||
/// death/birth patches supply related-unit context the history struct lacks.
|
||||
/// apply-site prefixes supply related-unit context the history struct lacks.
|
||||
/// </summary>
|
||||
[HarmonyPatch]
|
||||
internal static class ActorHappinessPatches
|
||||
|
|
@ -85,30 +86,49 @@ internal static class ActorHappinessPatches
|
|||
[HarmonyPrefix]
|
||||
public static void PrefixLovers(Actor __instance, Actor pTarget)
|
||||
{
|
||||
// Context for status fell_in_love → changeHappiness("fallen_in_love") during the body.
|
||||
// Do not NoteCanonicalMilestone here - that would merge the ring before it writes.
|
||||
HappinessContextBag.SetPair(__instance, pTarget, HappinessRelationRole.Lover, "fallen_in_love");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Live path: status fell_in_love action_on_receive → changeHappiness("fallen_in_love").
|
||||
/// Bridge still publishes for units that suppress changeHappiness (no emotions / egg),
|
||||
/// and ensures related names when the status path is skipped. Self-merge within 0.35s
|
||||
/// collapses the double for emotion units.
|
||||
/// </summary>
|
||||
[HarmonyPatch(typeof(Actor), nameof(Actor.becomeLoversWith))]
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPriority(Priority.First)]
|
||||
public static void PostfixLovers(Actor __instance, Actor pTarget)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (__instance != null)
|
||||
{
|
||||
HappinessEventRouter.NoteCanonicalMilestone(__instance.getID(), "milestone_lover");
|
||||
}
|
||||
|
||||
if (pTarget != null)
|
||||
{
|
||||
HappinessEventRouter.NoteCanonicalMilestone(pTarget.getID(), "milestone_lover");
|
||||
}
|
||||
BridgeFallenInLove(__instance, pTarget);
|
||||
BridgeFallenInLove(pTarget, __instance);
|
||||
}
|
||||
catch
|
||||
finally
|
||||
{
|
||||
// ignore
|
||||
HappinessContextBag.ClearRelated();
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Actor), nameof(Actor.becomeLoversWith))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixLovers()
|
||||
private static void BridgeFallenInLove(Actor subject, Actor related)
|
||||
{
|
||||
HappinessContextBag.ClearRelated();
|
||||
if (subject == null || !subject.isAlive())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Actor relatedAlive = related != null && related.isAlive() ? related : null;
|
||||
int amount = HappinessGameApi.ResolveAmount("fallen_in_love", 0);
|
||||
HappinessEventRouter.PublishApplied(
|
||||
subject,
|
||||
"fallen_in_love",
|
||||
amount,
|
||||
HappinessSourceHook.ChangeHappiness,
|
||||
relatedAlive,
|
||||
HappinessRelationRole.Lover);
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Actor), nameof(Actor.setBestFriend))]
|
||||
|
|
@ -140,4 +160,88 @@ internal static class ActorHappinessPatches
|
|||
{
|
||||
HappinessContextBag.ClearRelated();
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Actor), nameof(Actor.newKillAction))]
|
||||
[HarmonyPrefix]
|
||||
public static void PrefixKillAction(Actor __instance, Actor pDeadUnit)
|
||||
{
|
||||
HappinessContextBag.SetRelated(pDeadUnit, HappinessRelationRole.Victim, "just_killed");
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Actor), nameof(Actor.newKillAction))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixKillAction()
|
||||
{
|
||||
HappinessContextBag.ClearRelated();
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Actor), nameof(Actor.stealActionFrom))]
|
||||
[HarmonyPrefix]
|
||||
public static void PrefixSteal(Actor __instance, Actor pTarget)
|
||||
{
|
||||
// Victim gets got_robbed; related = thief.
|
||||
HappinessContextBag.SetRelated(__instance, HappinessRelationRole.Robber, "got_robbed");
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Actor), nameof(Actor.stealActionFrom))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixSteal()
|
||||
{
|
||||
HappinessContextBag.ClearRelated();
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(BehCheckForBabiesFromSexualReproduction), nameof(BehCheckForBabiesFromSexualReproduction.execute))]
|
||||
[HarmonyPrefix]
|
||||
public static void PrefixKissed(Actor pActor)
|
||||
{
|
||||
Actor lover = null;
|
||||
try
|
||||
{
|
||||
if (pActor != null && pActor.hasLover())
|
||||
{
|
||||
lover = pActor.lover;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
lover = null;
|
||||
}
|
||||
|
||||
HappinessContextBag.SetPair(pActor, lover, HappinessRelationRole.Lover, "just_kissed");
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(BehCheckForBabiesFromSexualReproduction), nameof(BehCheckForBabiesFromSexualReproduction.execute))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixKissed()
|
||||
{
|
||||
HappinessContextBag.ClearRelated();
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(BehFinishTalk), "finishTalk")]
|
||||
[HarmonyPrefix]
|
||||
public static void PrefixFinishTalk(Actor pActor, Actor pTarget)
|
||||
{
|
||||
HappinessContextBag.SetPair(pActor, pTarget, HappinessRelationRole.ConversationPartner, "just_talked");
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(BehFinishTalk), "finishTalk")]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixFinishTalk()
|
||||
{
|
||||
HappinessContextBag.ClearRelated();
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(BehFinishTalk), "makeGift")]
|
||||
[HarmonyPrefix]
|
||||
public static void PrefixMakeGift(Actor pActor, Actor pTarget)
|
||||
{
|
||||
HappinessContextBag.SetPair(pActor, pTarget, HappinessRelationRole.GiftPartner, "just_gave_gift");
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(BehFinishTalk), "makeGift")]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixMakeGift()
|
||||
{
|
||||
HappinessContextBag.ClearRelated();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,12 +35,9 @@ public static class BoatEventPatches
|
|||
[HarmonyPostfix]
|
||||
public static void PostfixLoad(Actor pActor, BehResult __result)
|
||||
{
|
||||
if (pActor == null || !pActor.isAlive() || __result == BehResult.Stop)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EmitBoat("boat_load", 55f, pActor, EventReason.Boat(pActor, "load"));
|
||||
// Loading is routine transport churn - unload/trade stay on Layer A.
|
||||
_ = pActor;
|
||||
_ = __result;
|
||||
}
|
||||
|
||||
private static void EmitBoat(string eventId, float strength, Actor actor, string label)
|
||||
|
|
|
|||
|
|
@ -12,23 +12,9 @@ public static class BuildingEventPatches
|
|||
[HarmonyPostfix]
|
||||
public static void PostfixDamageBuilding(Actor pActor, BehResult __result)
|
||||
{
|
||||
if (pActor == null || !pActor.isAlive() || __result == BehResult.Stop)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Building target = null;
|
||||
try
|
||||
{
|
||||
target = pActor.beh_building_target;
|
||||
}
|
||||
catch
|
||||
{
|
||||
target = null;
|
||||
}
|
||||
|
||||
string label = EventReason.BuildingDamage(pActor, target);
|
||||
Emit("building_damage", 70f, pActor, label);
|
||||
// Routine siege chips are Layer B noise. Camera waits for consume/destroy.
|
||||
_ = pActor;
|
||||
_ = __result;
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(BehConsumeTargetBuilding), nameof(BehConsumeTargetBuilding.execute))]
|
||||
|
|
|
|||
|
|
@ -62,6 +62,29 @@ public static class MetaEventPatches
|
|||
EmitNew("new_city", "Politics", 74f, __result, "city");
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(KingdomManager), nameof(KingdomManager.makeNewCivKingdom))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixNewCivKingdom(Kingdom __result, Actor pActor)
|
||||
{
|
||||
if (__result == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Actor anchor = pActor != null && pActor.isAlive() ? pActor : TryReadActor(__result);
|
||||
if (anchor == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MetaInterestFeed.EmitMeta(
|
||||
"new_kingdom",
|
||||
"Politics",
|
||||
82f,
|
||||
anchor,
|
||||
EventReason.MetaNew(anchor, "kingdom"));
|
||||
}
|
||||
|
||||
private static void EmitNew(
|
||||
string eventId,
|
||||
string category,
|
||||
|
|
|
|||
|
|
@ -74,6 +74,32 @@ public static class PlotEventPatches
|
|||
PlotInterestFeed.Emit(__state, __instance, "leave");
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Plot), nameof(Plot.finishPlot))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixFinishPlot(Plot __instance, PlotState pState, Actor pActor)
|
||||
{
|
||||
if (__instance == null || pState != PlotState.Finished)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefer finishing actor; fall back to plot author inside EmitFromPlot.
|
||||
if (pActor != null && pActor.isAlive())
|
||||
{
|
||||
string assetId = ReadPlotAssetId(__instance);
|
||||
if (string.IsNullOrEmpty(assetId))
|
||||
{
|
||||
PlotInterestFeed.EmitFromPlot(__instance, "complete");
|
||||
return;
|
||||
}
|
||||
|
||||
PlotInterestFeed.Emit(assetId, pActor, "complete");
|
||||
return;
|
||||
}
|
||||
|
||||
PlotInterestFeed.EmitFromPlot(__instance, "complete");
|
||||
}
|
||||
|
||||
private static string ReadPlotAssetId(object plot)
|
||||
{
|
||||
try
|
||||
|
|
|
|||
|
|
@ -26,87 +26,96 @@ public static class RelationshipEventPatches
|
|||
RelationshipInterestFeed.Emit("set_lover", __instance, pActor);
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Actor), nameof(Actor.addChild))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixAddChild(Actor __instance, object pObject)
|
||||
/// <summary>
|
||||
/// Surviving partners clear dead lovers by field assignment in
|
||||
/// <see cref="MapBox.checkEventUnitsDestroy"/>, which bypasses <see cref="Actor.setLover"/>.
|
||||
/// Emit clear_lover for the living partner before those fields are nulled.
|
||||
/// </summary>
|
||||
[HarmonyPatch(typeof(MapBox), "checkEventUnitsDestroy")]
|
||||
[HarmonyPrefix]
|
||||
public static void PrefixDeadLoverCleanup(MapBox __instance)
|
||||
{
|
||||
Actor child = ResolveRelatedActor(pObject);
|
||||
if (__instance == null || child == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RelationshipInterestFeed.Emit("add_child", __instance, child);
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Actor), "addChildSimple")]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixAddChildSimple(Actor __instance, object pObject)
|
||||
{
|
||||
Actor child = ResolveRelatedActor(pObject);
|
||||
if (__instance == null || child == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RelationshipInterestFeed.Emit("add_child", __instance, child);
|
||||
}
|
||||
|
||||
private static Actor ResolveRelatedActor(object raw)
|
||||
{
|
||||
if (raw == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (raw is Actor actor)
|
||||
{
|
||||
return actor;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (raw is BaseSimObject sim && sim.isActor())
|
||||
if (__instance == null || __instance.units == null || !__instance.units.event_destroy)
|
||||
{
|
||||
return sim.a;
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (Actor unit in __instance.units)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (unit == null || !unit.isAlive() || !unit.hasLover())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Actor lover = unit.lover;
|
||||
if (lover == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bool loverAlive = false;
|
||||
try
|
||||
{
|
||||
loverAlive = lover.isAlive();
|
||||
}
|
||||
catch
|
||||
{
|
||||
loverAlive = false;
|
||||
}
|
||||
|
||||
if (loverAlive)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
RelationshipInterestFeed.Emit("clear_lover", unit, null);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// never break vanilla destroy cleanup
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// fall through
|
||||
// never break vanilla destroy cleanup
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
/// <summary>
|
||||
/// Real parent/child link. Vanilla <c>addChild</c> attaches avatar components (Dragon, Boat), not kids.
|
||||
/// </summary>
|
||||
[HarmonyPatch(typeof(Actor), nameof(Actor.setParent1))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixSetParent1(Actor __instance, Actor pParentActor, bool pIncreaseChildren)
|
||||
{
|
||||
EmitAddChild(pParentActor, __instance, pIncreaseChildren);
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Actor), nameof(Actor.setParent2))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixSetParent2(Actor __instance, Actor pActor, bool pIncreaseChildren)
|
||||
{
|
||||
EmitAddChild(pActor, __instance, pIncreaseChildren);
|
||||
}
|
||||
|
||||
private static void EmitAddChild(Actor parent, Actor child, bool increaseChildren)
|
||||
{
|
||||
if (!increaseChildren || parent == null || child == null)
|
||||
{
|
||||
var type = raw.GetType();
|
||||
foreach (string name in new[] { "actor", "Actor", "a", "parent" })
|
||||
{
|
||||
var prop = type.GetProperty(name);
|
||||
if (prop != null && typeof(Actor).IsAssignableFrom(prop.PropertyType))
|
||||
{
|
||||
return prop.GetValue(raw, null) as Actor;
|
||||
}
|
||||
|
||||
var field = type.GetField(name);
|
||||
if (field != null && typeof(Actor).IsAssignableFrom(field.FieldType))
|
||||
{
|
||||
return field.GetValue(raw) as Actor;
|
||||
}
|
||||
}
|
||||
|
||||
var getActor = type.GetMethod("getActor", Type.EmptyTypes)
|
||||
?? type.GetMethod("GetActor", Type.EmptyTypes);
|
||||
if (getActor != null && typeof(Actor).IsAssignableFrom(getActor.ReturnType))
|
||||
{
|
||||
return getActor.Invoke(raw, null) as Actor;
|
||||
}
|
||||
return;
|
||||
}
|
||||
catch
|
||||
|
||||
if (!parent.isAlive() || !child.isAlive())
|
||||
{
|
||||
// ignore
|
||||
return;
|
||||
}
|
||||
|
||||
return null;
|
||||
RelationshipInterestFeed.Emit("add_child", parent, child);
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(FamilyManager), nameof(FamilyManager.newFamily))]
|
||||
|
|
@ -136,6 +145,10 @@ public static class RelationshipEventPatches
|
|||
RelationshipInterestFeed.Emit("baby_created", __result, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seeking only: BehFindLover returns Continue both when still searching and after
|
||||
/// <see cref="Actor.becomeLoversWith"/>. Success is covered by set_lover / love status.
|
||||
/// </summary>
|
||||
[HarmonyPatch(typeof(BehFindLover), nameof(BehFindLover.execute))]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixFindLover(Actor pActor, BehResult __result)
|
||||
|
|
@ -145,6 +158,11 @@ public static class RelationshipEventPatches
|
|||
return;
|
||||
}
|
||||
|
||||
if (pActor.hasLover())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RelationshipInterestFeed.Emit("find_lover", pActor, null);
|
||||
}
|
||||
|
||||
|
|
@ -157,6 +175,7 @@ public static class RelationshipEventPatches
|
|||
return;
|
||||
}
|
||||
|
||||
// BehFamilyGroupNew always calls FamilyManager.newFamily first - share that founding key.
|
||||
RelationshipInterestFeed.Emit("family_group_new", pActor, ActorRelation.ResolvePackRelated(pActor));
|
||||
}
|
||||
|
||||
|
|
@ -183,4 +202,20 @@ public static class RelationshipEventPatches
|
|||
|
||||
RelationshipInterestFeed.Emit("family_group_leave", pActor, ActorRelation.ResolvePackRelated(pActor));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Alpha succession is happiness-gated by emotions in vanilla. Patch the family API so
|
||||
/// animals without emotions still get a Layer A / Activity-capable beat.
|
||||
/// </summary>
|
||||
[HarmonyPatch(typeof(Family), "setAlpha")]
|
||||
[HarmonyPostfix]
|
||||
public static void PostfixSetAlpha(Actor pActor, bool pNew)
|
||||
{
|
||||
if (!pNew || pActor == null || !pActor.isAlive())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
InterestFeeds.EmitAlpha(pActor, "family_set_alpha");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,17 +113,13 @@ public static class TraitEventPatches
|
|||
}
|
||||
|
||||
DiscreteEventEntry entry = EventCatalog.Trait.GetOrFallback(traitId);
|
||||
if (!entry.CreatesInterest)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Quiet-world Ambient traits must not steal the camera.
|
||||
if (entry.EventStrength < InterestScoringConfig.W.noticeScoreMin)
|
||||
if (!EventCatalog.IsCameraWorthy(entry))
|
||||
{
|
||||
InterestDropLog.Record("createsInterest=false", "trait:" + (traitId ?? ""));
|
||||
return;
|
||||
}
|
||||
|
||||
// Quiet traits still register; low EventStrength lets the director bury them.
|
||||
// Spawn/birth endowment: only legendary rarities.
|
||||
if (gained && IsSpawnTraitWindow(actor) && !LegendaryBirthTraits.Contains(entry.Id))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -161,7 +161,8 @@ public static class HappinessEventRouter
|
|||
HappinessSourceHook hook,
|
||||
Actor related = null,
|
||||
HappinessRelationRole role = HappinessRelationRole.None,
|
||||
bool forceRing = false)
|
||||
bool forceRing = false,
|
||||
string relatedNameOverride = null)
|
||||
{
|
||||
return PublishCore(
|
||||
subject,
|
||||
|
|
@ -172,7 +173,8 @@ public static class HappinessEventRouter
|
|||
role,
|
||||
applied: true,
|
||||
suppressedByPsychopath: false,
|
||||
forceRing);
|
||||
forceRing,
|
||||
relatedNameOverride);
|
||||
}
|
||||
|
||||
/// <summary>Record that the game refused to apply happiness (psychopath/egg/no emotions).</summary>
|
||||
|
|
@ -183,6 +185,7 @@ public static class HappinessEventRouter
|
|||
_suppressedSinceClear++;
|
||||
}
|
||||
|
||||
InterestDropLog.Record("no_emotions_or_egg", effectId ?? "");
|
||||
var occ = BuildOccurrence(
|
||||
subject,
|
||||
effectId,
|
||||
|
|
@ -205,7 +208,8 @@ public static class HappinessEventRouter
|
|||
HappinessRelationRole role,
|
||||
bool applied,
|
||||
bool suppressedByPsychopath,
|
||||
bool forceRing)
|
||||
bool forceRing,
|
||||
string relatedNameOverride = null)
|
||||
{
|
||||
if (subject == null || string.IsNullOrEmpty(effectId) || !applied)
|
||||
{
|
||||
|
|
@ -238,7 +242,15 @@ public static class HappinessEventRouter
|
|||
}
|
||||
|
||||
HappinessOccurrence occ = BuildOccurrence(
|
||||
subject, effectId, amount, hook, related, role, applied, suppressedByPsychopath);
|
||||
subject,
|
||||
effectId,
|
||||
amount,
|
||||
hook,
|
||||
related,
|
||||
role,
|
||||
applied,
|
||||
suppressedByPsychopath,
|
||||
relatedNameOverride);
|
||||
FillProse(occ, entry);
|
||||
|
||||
if (ShouldMerge(occ, entry))
|
||||
|
|
@ -448,7 +460,8 @@ public static class HappinessEventRouter
|
|||
Actor related,
|
||||
HappinessRelationRole role,
|
||||
bool applied,
|
||||
bool suppressedByPsychopath)
|
||||
bool suppressedByPsychopath,
|
||||
string relatedNameOverride = null)
|
||||
{
|
||||
HappinessCatalogEntry entry = EventCatalog.Happiness.GetOrFallback(effectId);
|
||||
long subjectId = 0;
|
||||
|
|
@ -494,6 +507,11 @@ public static class HappinessEventRouter
|
|||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(relatedName) && !string.IsNullOrEmpty(relatedNameOverride))
|
||||
{
|
||||
relatedName = relatedNameOverride.Trim();
|
||||
}
|
||||
|
||||
StampWorldDate(out double worldTime, out string dateLabel);
|
||||
string corr = subjectId + "|" + effectId + "|" + relatedId + "|" + Math.Floor(worldTime);
|
||||
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@ internal static class HarnessScenarios
|
|||
return DomainEventAudit();
|
||||
case "event_tracking":
|
||||
return EventTracking();
|
||||
case "family_trigger_smoke":
|
||||
return FamilyTriggerSmoke();
|
||||
case "mutation_discovery":
|
||||
return MutationDiscovery();
|
||||
case "chronicle":
|
||||
|
|
@ -172,6 +174,37 @@ internal static class HarnessScenarios
|
|||
};
|
||||
}
|
||||
|
||||
private static List<HarnessCommand> FamilyTriggerSmoke()
|
||||
{
|
||||
return new List<HarnessCommand>
|
||||
{
|
||||
Step("ft0", "wait_world"),
|
||||
Step("ft1", "dismiss_windows"),
|
||||
Step("ft2", "spectator", value: "off"),
|
||||
|
||||
// Bridge: becomeLoversWith publishes happiness fallen_in_love
|
||||
Step("ft10", "spawn", asset: "human"),
|
||||
Step("ft11", "happiness_remember_partner"),
|
||||
Step("ft12", "spawn", asset: "human"),
|
||||
Step("ft13", "focus", asset: "auto"),
|
||||
Step("ft14", "activity_clear"),
|
||||
Step("ft15", "happiness_reset"),
|
||||
Step("ft16", "happiness_bond_lovers"),
|
||||
Step("ft17", "assert", expect: "activity_happiness_count", value: "2", label: "min"),
|
||||
Step("ft18", "assert", expect: "activity_log_contains", value: "love"),
|
||||
|
||||
// Retarget: setParent1 emits rel:add_child
|
||||
Step("ft20", "spawn", asset: "human"),
|
||||
Step("ft21", "happiness_remember_partner"),
|
||||
Step("ft22", "spawn", asset: "human"),
|
||||
Step("ft23", "focus", asset: "auto"),
|
||||
Step("ft24", "relationship_set_parent"),
|
||||
Step("ft25", "assert", expect: "interest_has_key", value: "rel:add_child"),
|
||||
|
||||
Step("ft99", "snapshot"),
|
||||
};
|
||||
}
|
||||
|
||||
private static List<HarnessCommand> ActivityHappinessAudit()
|
||||
{
|
||||
return new List<HarnessCommand>
|
||||
|
|
@ -202,7 +235,7 @@ internal static class HarnessScenarios
|
|||
Step("hp11", "assert", expect: "activity_happiness_task_id", value: "happiness:become_king"),
|
||||
Step("hp12", "assert", expect: "activity_happiness_ring_count", value: "1", label: "exact"),
|
||||
Step("hp13", "assert", expect: "activity_happiness_life_count", value: "1", label: "min"),
|
||||
Step("hp14", "assert", expect: "activity_log_contains", value: "throne"),
|
||||
Step("hp14", "assert", expect: "activity_log_contains", value: "ruler"),
|
||||
|
||||
// Ambient cooldown: second meal within window should not add another ring write
|
||||
Step("hp20", "happiness_reset"),
|
||||
|
|
@ -226,6 +259,8 @@ internal static class HarnessScenarios
|
|||
Step("hp44", "activity_clear"),
|
||||
Step("hp45", "happiness_reset"),
|
||||
Step("hp46", "happiness_bond_lovers"),
|
||||
Step("hp46b", "assert", expect: "activity_happiness_count", value: "2", label: "min"),
|
||||
Step("hp46c", "assert", expect: "activity_log_contains", value: "love"),
|
||||
Step("hp47", "wait", value: "0.5"),
|
||||
Step("hp48", "happiness_kill_partner"),
|
||||
Step("hp49", "wait", value: "0.5"),
|
||||
|
|
@ -1197,12 +1232,21 @@ internal static class HarnessScenarios
|
|||
|
||||
Step("et20", "domain_feed", asset: "relationship", value: "set_lover"),
|
||||
Step("et21", "assert", expect: "interest_has_key", value: "rel:set_lover"),
|
||||
Step("et21a", "domain_feed", asset: "relationship", value: "become_alpha"),
|
||||
Step("et21b", "assert", expect: "interest_has_key", value: "rel:become_alpha"),
|
||||
Step("et21c", "interest_force_session", asset: "human",
|
||||
label: "{a} becomes the family alpha", tier: "Action", expect: "alpha_reason",
|
||||
value: "lead=event;evt=72"),
|
||||
Step("et21d", "assert", expect: "dossier_contains", value: "family alpha"),
|
||||
Step("et22", "domain_feed", asset: "plot", value: "summon_meteor_rain"),
|
||||
Step("et23", "assert", expect: "interest_has_key", value: "plot:summon_meteor_rain"),
|
||||
Step("et23b", "assert", expect: "interest_label_contains", value: "plots to summon"),
|
||||
Step("et24", "domain_feed", asset: "era", value: "age_chaos"),
|
||||
Step("et25", "assert", expect: "interest_has_key", value: "era:age_chaos"),
|
||||
Step("et26", "domain_feed", asset: "meta", value: "new_city"),
|
||||
Step("et27", "assert", expect: "interest_has_key", value: "meta:new_city"),
|
||||
Step("et27b", "domain_feed", asset: "meta", value: "new_kingdom"),
|
||||
Step("et27c", "assert", expect: "interest_has_key", value: "meta:new_kingdom"),
|
||||
Step("et28", "domain_feed", asset: "book", value: "bad_story_about_king"),
|
||||
Step("et29", "assert", expect: "interest_has_key", value: "book:bad_story_about_king"),
|
||||
Step("et30", "domain_feed", asset: "trait", value: "immortal"),
|
||||
|
|
@ -1217,6 +1261,16 @@ internal static class HarnessScenarios
|
|||
Step("et43", "assert", expect: "interest_has_key", value: "item:"),
|
||||
Step("et44", "domain_feed", asset: "decision", value: "declare_war"),
|
||||
Step("et45", "assert", expect: "interest_has_key", value: "decision:"),
|
||||
Step("et45b", "interest_force_session", asset: "human",
|
||||
label: "{a} decides to declare war", tier: "Action", expect: "decision_prose",
|
||||
value: "lead=event;evt=86"),
|
||||
Step("et45c", "assert", expect: "dossier_contains", value: "decides to declare war"),
|
||||
Step("et45d", "assert", expect: "dossier_not_contains", value: "decides Declare"),
|
||||
Step("et45e", "interest_force_session", asset: "human",
|
||||
label: "{a} decides to change the kingdom's culture", tier: "Action", expect: "king_decision_prose",
|
||||
value: "lead=event;evt=86"),
|
||||
Step("et45f", "assert", expect: "dossier_contains", value: "decides to change the kingdom's culture"),
|
||||
Step("et45g", "assert", expect: "dossier_not_contains", value: "decides to king"),
|
||||
Step("et46", "domain_feed", asset: "power", value: "lightning"),
|
||||
Step("et47", "assert", expect: "interest_has_key", value: "power:"),
|
||||
Step("et48", "domain_feed", asset: "subspecies_trait", value: "fire_blood"),
|
||||
|
|
|
|||
|
|
@ -552,12 +552,14 @@ public static class InterestDirector
|
|||
// Queued too long - miss the beat.
|
||||
if (now - c.CreatedAt > 5.5f)
|
||||
{
|
||||
InterestDropLog.Record("stale", c.HappinessEffectId + " queue");
|
||||
return true;
|
||||
}
|
||||
|
||||
Actor unit = c.FollowUnit;
|
||||
if (unit == null || !unit.isAlive())
|
||||
{
|
||||
InterestDropLog.Record("stale", c.HappinessEffectId + " dead");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -569,6 +571,7 @@ public static class InterestDirector
|
|||
{
|
||||
if (!unit.isBaby() && unit.getAge() > 1)
|
||||
{
|
||||
InterestDropLog.Record("stale", "just_born grown");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -876,11 +879,17 @@ public static class InterestDirector
|
|||
if (_current != null
|
||||
&& (_current.Completion == InterestCompletionKind.CombatActive
|
||||
|| _current.Completion == InterestCompletionKind.StatusPhase
|
||||
|| _current.Completion == InterestCompletionKind.HappinessGrief))
|
||||
|| _current.Completion == InterestCompletionKind.HappinessGrief
|
||||
|| IsStickyStoryScene(_current)))
|
||||
{
|
||||
float graceCur = _current.TotalScore;
|
||||
float graceNext = candidate.TotalScore;
|
||||
return graceNext >= graceCur + InterestScoringConfig.W.cutInMargin
|
||||
float graceMargin = Mathf.Max(
|
||||
InterestScoringConfig.W.cutInMargin,
|
||||
InterestScoringConfig.W.stickyCutInMargin > 0f
|
||||
? InterestScoringConfig.W.stickyCutInMargin
|
||||
: 50f);
|
||||
return graceNext >= graceCur + graceMargin
|
||||
&& IsWorthWatchingNow(candidate, now, selectingDuringGrace: true);
|
||||
}
|
||||
|
||||
|
|
@ -899,6 +908,10 @@ public static class InterestDirector
|
|||
bool nextFill = InterestScoring.IsFillScore(nextScore);
|
||||
bool curAmbient = IsAmbientShot(_current);
|
||||
bool curFill = curAmbient || InterestScoring.IsFillScore(curScore);
|
||||
bool sticky = IsStickyStoryScene(_current);
|
||||
float cutMargin = sticky
|
||||
? Mathf.Max(w.cutInMargin, w.stickyCutInMargin > 0f ? w.stickyCutInMargin : 50f)
|
||||
: w.cutInMargin;
|
||||
|
||||
// Ambient fill: any real event may take the camera immediately (no min dwell).
|
||||
if (curAmbient && !nextFill && !IsAmbientShot(candidate))
|
||||
|
|
@ -906,12 +919,25 @@ public static class InterestDirector
|
|||
return true;
|
||||
}
|
||||
|
||||
// Instant score-margin cut - no settle grace, no MinDwell block.
|
||||
if (nextScore >= curScore + w.cutInMargin)
|
||||
// Same-arc refresh (combat/hatch/grief keys) may replace without full margin.
|
||||
if (sticky && IsSameStoryArc(_current, candidate) && nextScore >= curScore - w.rotateSlack)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Instant score-margin cut - no settle grace, no MinDwell block.
|
||||
if (nextScore >= curScore + cutMargin)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (sticky && nextScore < curScore + cutMargin)
|
||||
{
|
||||
InterestDropLog.Record(
|
||||
"below_margin",
|
||||
$"sticky cur={curScore:0.#} next={nextScore:0.#} need={cutMargin:0.#}");
|
||||
}
|
||||
|
||||
// Fill never cuts a protected non-fill session without margin (already failed above).
|
||||
if (nextFill && !curFill && protectedScene)
|
||||
{
|
||||
|
|
@ -924,8 +950,8 @@ public static class InterestDirector
|
|||
return false;
|
||||
}
|
||||
|
||||
// Protected EventLed hold: only margin cut-in (handled above).
|
||||
if (protectedScene && !curAmbient)
|
||||
// Protected / sticky EventLed hold: only margin or same-arc (handled above).
|
||||
if ((protectedScene || sticky) && !curAmbient)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -941,6 +967,89 @@ public static class InterestDirector
|
|||
&& nextScore >= curScore - w.rotateSlack;
|
||||
}
|
||||
|
||||
private static bool IsStickyStoryScene(InterestCandidate c)
|
||||
{
|
||||
if (c == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (c.Completion == InterestCompletionKind.CombatActive
|
||||
|| c.Completion == InterestCompletionKind.StatusPhase
|
||||
|| c.Completion == InterestCompletionKind.HappinessGrief)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Hatch FixedDwell is a story beat - hold against weak peers.
|
||||
if (!string.IsNullOrEmpty(c.HappinessEffectId)
|
||||
&& (string.Equals(c.HappinessEffectId, "just_got_out_of_egg", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(c.HappinessEffectId, "just_hatched", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(c.Key)
|
||||
&& (c.Key.StartsWith("combat:", StringComparison.Ordinal)
|
||||
|| c.Key.StartsWith("hatch:", StringComparison.Ordinal)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsSameStoryArc(InterestCandidate current, InterestCandidate next)
|
||||
{
|
||||
if (current == null || next == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(current.Key)
|
||||
&& !string.IsNullOrEmpty(next.Key)
|
||||
&& string.Equals(current.Key, next.Key, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Combat / hatch share key prefixes per subject.
|
||||
if (!string.IsNullOrEmpty(current.Key) && !string.IsNullOrEmpty(next.Key))
|
||||
{
|
||||
if (SameKeyPrefix(current.Key, next.Key, "combat:")
|
||||
|| SameKeyPrefix(current.Key, next.Key, "hatch:"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (current.Completion == InterestCompletionKind.HappinessGrief
|
||||
&& next.Completion == InterestCompletionKind.HappinessGrief
|
||||
&& current.SubjectId != 0
|
||||
&& current.SubjectId == next.SubjectId)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool SameKeyPrefix(string a, string b, string prefix)
|
||||
{
|
||||
if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b) || string.IsNullOrEmpty(prefix))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!a.StartsWith(prefix, StringComparison.Ordinal)
|
||||
|| !b.StartsWith(prefix, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return string.Equals(a, b, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Still-worth-watching filter: drop stale queued moments; keep live sticky conditions
|
||||
/// and fresh FixedDwell; during grace, brand-new fires are always eligible.
|
||||
|
|
|
|||
|
|
@ -183,27 +183,63 @@ public static class InterestScoring
|
|||
float sig = 0f;
|
||||
if (snap.Favorite)
|
||||
{
|
||||
sig += w.metaFavorite;
|
||||
sig += EventCatalogConfig.CharacterBonus("favorite", w.metaFavorite);
|
||||
}
|
||||
|
||||
if (snap.King)
|
||||
{
|
||||
sig += w.metaKing;
|
||||
sig += EventCatalogConfig.CharacterBonus("king", w.metaKing);
|
||||
}
|
||||
else if (snap.Leader)
|
||||
{
|
||||
sig += w.metaLeader;
|
||||
sig += EventCatalogConfig.CharacterBonus("leader", w.metaLeader);
|
||||
}
|
||||
|
||||
float renownDiv = w.metaRenownDivisor > 0f ? w.metaRenownDivisor : 120f;
|
||||
sig += Mathf.Min(w.metaRenownCap, snap.Renown / renownDiv);
|
||||
sig += Mathf.Min(w.metaKillsCap, snap.Kills * w.metaKillsFactor);
|
||||
sig += Mathf.Min(w.metaLevelCap, snap.Level * w.metaLevelFactor);
|
||||
sig += SpeciesRarityBonus(snap.SpeciesId, w);
|
||||
sig += EventCatalogConfig.SpeciesBonus(snap.SpeciesId);
|
||||
snap.CharacterSignificance = sig;
|
||||
MetaCache[id] = snap;
|
||||
return snap;
|
||||
}
|
||||
|
||||
private static float SpeciesRarityBonus(string speciesId, ScoringWeights w)
|
||||
{
|
||||
if (string.IsNullOrEmpty(speciesId) || w == null)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Dictionary<string, int> counts = WorldActivityScanner.CountSpeciesPopulations();
|
||||
if (counts == null || !counts.TryGetValue(speciesId, out int pop) || pop <= 0)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
if (pop == 1)
|
||||
{
|
||||
return w.speciesSingletonBonus;
|
||||
}
|
||||
|
||||
int rareMax = w.speciesRareMaxPop > 0 ? w.speciesRareMaxPop : 5;
|
||||
if (pop <= rareMax)
|
||||
{
|
||||
return w.speciesRareBonus;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
|
||||
return 0f;
|
||||
}
|
||||
|
||||
private static void ApplyMeta(InterestCandidate c, InterestMetadataSnapshot snap)
|
||||
{
|
||||
if (c == null || snap == null)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ public class ScoringWeights
|
|||
|
||||
// Director policy (score-native; replaces InterestTier preemption).
|
||||
public float cutInMargin = 35f;
|
||||
/// <summary>Extra margin required to interrupt sticky A scenes (combat/grief/status/hatch).</summary>
|
||||
public float stickyCutInMargin = 50f;
|
||||
public float rotateSlack = 12f;
|
||||
public float fillScoreMax = 55f;
|
||||
public float noticeScoreMin = 70f;
|
||||
|
|
@ -77,6 +79,11 @@ public class ScoringWeights
|
|||
public float metaKillsCap = 12f;
|
||||
public float metaLevelFactor = 0.4f;
|
||||
public float metaLevelCap = 10f;
|
||||
/// <summary>Bonus when the follow unit is the only living member of its species.</summary>
|
||||
public float speciesSingletonBonus = 14f;
|
||||
/// <summary>Bonus when species population is between 2 and speciesRareMaxPop inclusive.</summary>
|
||||
public float speciesRareBonus = 8f;
|
||||
public int speciesRareMaxPop = 5;
|
||||
|
||||
public int notableKills = 40;
|
||||
public float notableRenown = 200f;
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ public class ModClass : MonoBehaviour, IMod, IConfigurable, ILocalizable
|
|||
ModFolder = pModDecl.FolderPath;
|
||||
_config = ModSettings.Load(pModDecl);
|
||||
InterestScoringConfig.LoadFromModFolder(ModFolder);
|
||||
EventCatalogConfig.LoadFromModFolder(ModFolder);
|
||||
|
||||
_harmony = new Harmony(pModDecl.UID);
|
||||
ApplyHarmonyPatches(pModDecl.Name);
|
||||
|
|
|
|||
|
|
@ -106,6 +106,9 @@ public static class MutationDiscoveryHarness
|
|||
"ArmyManager.newArmy",
|
||||
"AllianceManager.newAlliance",
|
||||
"CityManager.newCity",
|
||||
"KingdomManager.makeNewCivKingdom",
|
||||
"Family.setAlpha",
|
||||
"Plot.finishPlot",
|
||||
"ItemManager.generateItem",
|
||||
"ItemManager.newItem",
|
||||
"SubspeciesManager.newSpecies",
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ public sealed class UnitDossier
|
|||
d.Headline = string.IsNullOrEmpty(d.SpeciesId)
|
||||
? d.Name
|
||||
: $"{d.Name} ({d.SpeciesId})";
|
||||
d.ReasonLine = BuildStoryBeat(d, watchLabel, actor);
|
||||
d.ReasonLine = ComposeReasonWithJob(BuildStoryBeat(d, watchLabel, actor), d.JobLabel);
|
||||
d.DetailLine = BuildDetailLine(d);
|
||||
d.CaptionText = JoinCaption(d.Headline, d.ReasonLine, d.DetailLine);
|
||||
return d;
|
||||
|
|
@ -136,6 +136,32 @@ public sealed class UnitDossier
|
|||
return ActivityProse.HumanizeJobPublic(jobId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Job sits on the orange reason row: alone when there is no watch beat, otherwise
|
||||
/// <c>{reason} · {job}</c>.
|
||||
/// </summary>
|
||||
public static string ComposeReasonWithJob(string reason, string job)
|
||||
{
|
||||
string r = (reason ?? "").Trim();
|
||||
string j = (job ?? "").Trim();
|
||||
if (string.IsNullOrEmpty(j))
|
||||
{
|
||||
return r;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(r))
|
||||
{
|
||||
return j;
|
||||
}
|
||||
|
||||
if (r.IndexOf(j, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
return r;
|
||||
}
|
||||
|
||||
return r + " · " + j;
|
||||
}
|
||||
|
||||
public bool ContainsIgnoreCase(string needle)
|
||||
{
|
||||
if (string.IsNullOrEmpty(needle))
|
||||
|
|
@ -147,6 +173,7 @@ public sealed class UnitDossier
|
|||
|| (Headline ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| (ReasonLine ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| (DetailLine ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| (JobLabel ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| (Name ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| (SpeciesId ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| (TaskText ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
|
|
@ -284,12 +311,14 @@ public sealed class UnitDossier
|
|||
// Ownership: never let the reason name a living stranger (not subject/related).
|
||||
if (ReasonNamesStranger(beat, scene))
|
||||
{
|
||||
InterestDropLog.Record("identity_filter", "stranger:" + beat);
|
||||
return "";
|
||||
}
|
||||
|
||||
// Ownership: reject beats that lead with someone else's proper name.
|
||||
if (LeadsWithForeignName(beat, scene))
|
||||
{
|
||||
InterestDropLog.Record("identity_filter", "foreign:" + beat);
|
||||
return "";
|
||||
}
|
||||
|
||||
|
|
@ -494,9 +523,22 @@ public sealed class UnitDossier
|
|||
}
|
||||
}
|
||||
|
||||
// EventReason sentences are dossier-ready - keep names; only drop identity crumbs.
|
||||
// EventReason / Name-verb sentences are dossier-ready.
|
||||
if (LooksLikeEventSentence(s)
|
||||
|| (!string.IsNullOrEmpty(d?.Name) && StartsWithSubjectVerbPhrase(s, d.Name)))
|
||||
{
|
||||
if (IsWeakFlavorBeat(s, d))
|
||||
{
|
||||
InterestDropLog.Record("identity_filter", "weak:" + s);
|
||||
return "";
|
||||
}
|
||||
|
||||
return CleanBeat(s);
|
||||
}
|
||||
|
||||
if (IsIdentityWhy(s, d) || IsWeakFlavorBeat(s, d))
|
||||
{
|
||||
InterestDropLog.Record("identity_filter", s);
|
||||
return "";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
3652
IdleSpectator/event-catalog.json
Normal file
3652
IdleSpectator/event-catalog.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "IdleSpectator",
|
||||
"author": "dazed",
|
||||
"version": "0.21.3",
|
||||
"description": "AFK Idle Spectator (I) + Lore (L). Event-centered scene director + happiness Activity/Life logging.",
|
||||
"version": "0.25.11",
|
||||
"description": "AFK Idle Spectator (I) + Lore (L). Event trigger audit Phases 4-8 + job reason row.",
|
||||
"GUID": "com.dazed.idlespectator"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
"noveltyMultiplier": 4,
|
||||
|
||||
"cutInMargin": 35,
|
||||
"stickyCutInMargin": 50,
|
||||
"rotateSlack": 12,
|
||||
"fillScoreMax": 55,
|
||||
"noticeScoreMin": 70,
|
||||
|
|
@ -58,6 +59,9 @@
|
|||
"metaKillsCap": 12,
|
||||
"metaLevelFactor": 0.4,
|
||||
"metaLevelCap": 10,
|
||||
"speciesSingletonBonus": 14,
|
||||
"speciesRareBonus": 8,
|
||||
"speciesRareMaxPop": 5,
|
||||
|
||||
"notableKills": 40,
|
||||
"notableRenown": 200,
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
# Event audit (post-expand)
|
||||
# Event audit (A/B layers)
|
||||
|
||||
Inventory policy after the full gap expand.
|
||||
Camera interrupt is score-margin only (`cutInMargin`); no Signal/Ambient ownership gate.
|
||||
**Layer B (ticker):** Activity / Life coverage for live library ids - honest prose, completeness.
|
||||
**Layer A (story camera):** only `EventCatalog.IsCameraWorthy` beats register interest and cut the camera.
|
||||
|
||||
**Single inventory:** [`EventCatalog`](../IdleSpectator/Events/EventCatalog.cs) (partials under `Events/Catalogs/`).
|
||||
Nested domains: `Status`, `Happiness`, `WorldLog`, `Trait`, `Combat`, `Spell`, …
|
||||
Feeds/patches register from those dials; `InterestDirector` only ranks candidates.
|
||||
Feeds/patches register A from those dials; `InterestDirector` only ranks candidates.
|
||||
|
||||
## Dials
|
||||
|
||||
| Dial | Role |
|
||||
|------|------|
|
||||
| `CreatesInterest` | Whether a live id may register at all |
|
||||
| `EventStrength` | Base contribution to `TotalScore` (interrupt via +`cutInMargin`) |
|
||||
| `Completion` | Hold-until-complete kind |
|
||||
| `MaxWatch` | Hard cap (class defaults in `scoring-model.json`) |
|
||||
| `EventReason` | Orange dossier sentence |
|
||||
| Dial | Layer | Role |
|
||||
|------|-------|------|
|
||||
| Happiness `Presentation` | A if Signal | Ambient/Aggregate = B-only |
|
||||
| `CreatesInterest` | A gate | Discrete / status / WorldLog / libraries |
|
||||
| `EventStrength` | A rank | Base `TotalScore` (interrupt via margin) |
|
||||
| `Completion` | A hold | Hold-until-complete kind |
|
||||
| `MaxWatch` | A hold | Hard cap (`scoring-model.json`) |
|
||||
| `EventReason` / Label | A display | Orange dossier sentence |
|
||||
|
||||
## MaxWatch class defaults
|
||||
|
||||
|
|
@ -27,56 +27,48 @@ Feeds/patches register from those dials; `InterestDirector` only ranks candidate
|
|||
| Combat | 60 | `maxWatchCombat` |
|
||||
| Epic world | 50 | `maxWatchEpic` |
|
||||
|
||||
Peer rotate / soft cut on EventLed floors at `minCameraDwell` (15s). Score-margin interrupts bypass that floor.
|
||||
Ambient / Character fill is exempt - events may take the camera immediately.
|
||||
Peer rotate / soft cut on EventLed floors at `minCameraDwell` (15s).
|
||||
Score-margin interrupts bypass that floor; sticky A scenes use `stickyCutInMargin`.
|
||||
Ambient / Character fill is exempt - A events may take the camera immediately.
|
||||
|
||||
## Sources (wired)
|
||||
|
||||
| Source | Primary owner | Reason factory | Notes |
|
||||
|--------|---------------|----------------|-------|
|
||||
| WorldLog | `InterestFeeds.OnWorldLogMessage` | catalog / EventReason | Strength from `EventCatalog.WorldLog` |
|
||||
| Happiness | `IngestHappiness` | `EventCatalog.Happiness` + EventReason | Prose must match game call sites - see [event-prose-audit.md](event-prose-audit.md) |
|
||||
| Status | `OnStatusChange` | EventReason.Status | `drowning` CreatesInterest=true, strength ~50; notable via CharacterSignificance |
|
||||
| Scanner combat | WorldActivityScanner | EventReason.Fight / Battle | Completion CombatActive; dials `EventCatalog.Combat` |
|
||||
| War | WarEventPatches + WarInterestFeed.Tick | war type labels | |
|
||||
| Plot | PlotEventPatches + PlotInterestFeed.Tick | `EventCatalog.Plot` | |
|
||||
| Relationship | RelationshipEventPatches + happiness lovers | EventReason.* | |
|
||||
| Trait | TraitEventPatches | EventReason.Trait | Spawn window filters routine traits |
|
||||
| Building | BuildingEventPatches | EventReason.Building* | Eat/damage/falls |
|
||||
| Boat | BoatEventPatches | EventReason.Boat | |
|
||||
| Book | BookEventPatches | `EventCatalog.Book` sentences | |
|
||||
| Era / Meta | MetaEventPatches | EventReason.MetaNew / era labels | |
|
||||
| Spell | CombatActionLibrary.tryToCastSpell | EventReason.Library | Ambient CreatesInterest=false except spectacle ids |
|
||||
| Power | PlayerControl.clickedFinal | EventReason.HumanizeId | Spectacle ids create interest |
|
||||
| Decision | setDecisionCooldown | EventReason.Library | IsCameraWorthy filter |
|
||||
| Item | ItemManager | EventReason.Library | Legendary CreatesInterest |
|
||||
| Gene | Subspecies.mutateFrom | EventReason.Library | Ambient CreatesInterest=false |
|
||||
| Phenotype | generatePhenotypeAndShade | EventReason.Library | |
|
||||
| World law | WorldLaws.enable / toggle | EventReason.HumanizeId | Location-only |
|
||||
| Biome | catalog ready | EventReason.HumanizeId | Ambient CreatesInterest=false; emit via power terraform path |
|
||||
| Subspecies trait | SubspeciesManager biome hooks | EventReason.Library | |
|
||||
| WorldLog | `InterestFeeds.OnWorldLogMessage` | catalog / EventReason | A if CreatesInterest |
|
||||
| Happiness | `IngestHappiness` | catalog + EventReason | Signal = A; see [event-prose-audit.md](event-prose-audit.md) |
|
||||
| Status | `OnStatusChange` | EventReason.Status | Gain A if CreatesInterest; egg **loss** → hatch A |
|
||||
| Scanner combat | WorldActivityScanner | EventReason.Fight / Battle | `EventCatalog.Combat` |
|
||||
| War / Plot / Relationship / Trait / Building / Boat / Book / Era / Kingdom | Feeds + patches | EventReason / catalog | A via CreatesInterest; alpha via Family.setAlpha |
|
||||
| Libraries | Deferred feeds | EventReason.Library | Mostly B; spectacle ids A |
|
||||
| Character fill | InterestDirector.TryCharacterFill | empty Label | Only when no pending EventLed |
|
||||
|
||||
## Ambient statuses (intentional off)
|
||||
## Layer A policy
|
||||
|
||||
`festive_spirit`, `laughing`, `singing`, `sleeping`, `handsome_migrant` - CreatesInterest=false.
|
||||
Interesting beats register as interest candidates (`Signal` / `CreatesInterest`).
|
||||
Director ranks by `EventStrength` + scoring - do not hard-gate interesting life off Layer A.
|
||||
|
||||
## Same-moment ownership
|
||||
Happiness Ambient (B) only: `got_poked`, `got_caught`, `paid_tax`, `just_ate`, `just_pooped`.
|
||||
Happiness Aggregates stay civic mass (B unit camera).
|
||||
|
||||
Prefer typed feeds over WorldLog when both fire (relationship / status / combat).
|
||||
Registry Upsert merges same key and refreshes `LastSeenAt` for sticky combat/status/grief.
|
||||
Status B: brief FX/cooldowns + egg gain only.
|
||||
|
||||
Noise still skipped at patch (not dial): building_damage chips, boat_load.
|
||||
|
||||
## Full surface
|
||||
|
||||
See [world-event-inventory.md](world-event-inventory.md) for live library counts, gaps, and refresh commands.
|
||||
|
||||
## Interrupt / hold / grace
|
||||
|
||||
1. Instant cut when `next.TotalScore >= current + cutInMargin` (no settle; ignores min dwell).
|
||||
2. Ambient fill: any EventLed may take the camera immediately (no `minCameraDwell`).
|
||||
3. Else hold while `InterestCompletion.IsActive` and under MaxWatch (moments floor to `minCameraDwell`; combat uses class 60s).
|
||||
4. Soft peer rotates on EventLed only after `minCameraDwell` (15s).
|
||||
1. Instant cut when `next.TotalScore >= current + cutInMargin` (sticky: `stickyCutInMargin`).
|
||||
2. Ambient fill: any A EventLed may take the camera immediately.
|
||||
3. Else hold while `InterestCompletion.IsActive` and under MaxWatch.
|
||||
4. Soft peer rotates on EventLed only after `minCameraDwell` (15s), never during protected sticky hold without margin.
|
||||
5. Quiet grace 6s: still-worth-watching filter, then Character fill with empty reason.
|
||||
6. Drops recorded in `InterestDropLog`.
|
||||
|
||||
## Mutation gap triage
|
||||
|
||||
`mutation_discovery` refreshes `.harness/mutation_gaps.tsv`.
|
||||
Open rows are unexplained Wire candidates only.
|
||||
False positives (clear/load/UI/FX/getters) and covered-by-primary paths are filtered or listed in `AlreadyWired`.
|
||||
Library fields are inventory-complete via catalogs / activity / WorldLog (`mutation_libraries.tsv` notes).
|
||||
|
|
|
|||
|
|
@ -37,7 +37,16 @@ Scope: Happiness (all authored), Relationship (all), Status/WorldLog/Plot/etc. s
|
|||
|
||||
## Still clean (sample)
|
||||
|
||||
Grief deaths, `fallen_in_love`, gifts, house find/lose, `just_got_out_of_egg`, `just_became_adult`, WorldLog `king_new` template roles, Status `egg` / `pregnant` / `drowning` prose.
|
||||
Grief deaths, `fallen_in_love`, gifts, house find/lose, `just_got_out_of_egg`, `just_became_adult`, WorldLog `king_new` template roles, Status `egg` / `drowning` prose.
|
||||
|
||||
## A/B camera policy (2026-07-16)
|
||||
|
||||
Interesting = interest candidate. Director ranks by strength.
|
||||
|
||||
Happiness Ambient only for true FX: poke, caught stub, tax, ate, pooped.
|
||||
Status CreatesInterest false only for brief FX/cooldowns + egg gain.
|
||||
|
||||
Gate helper: `EventCatalog.IsCameraWorthy`. Drops: `InterestDropLog`.
|
||||
|
||||
## Hatch camera path (fixed)
|
||||
|
||||
|
|
@ -58,4 +67,53 @@ Sources: `EventReason.*`, happiness catalog `PlainClause`, or discrete catalog t
|
|||
Character fill / ambient may show empty reason.
|
||||
Not every Activity line is a camera event (Ambient happiness, status gains with `CreatesInterest=false`, status losses except egg).
|
||||
|
||||
## Happiness trigger pass (2026-07-16)
|
||||
|
||||
Full 67-id call-site audit vs decompile + BehaviourTask / StatusLibrary / City civic force.
|
||||
|
||||
| Id | Finding | Keep-on fix |
|
||||
|----|---------|-------------|
|
||||
| `just_laughed` / `just_swore` | Fire at **end** of emotion tasks | Prose → finishes laughing / swearing |
|
||||
| `just_ate` | Fires on `consumeFoodResource` | Prose → eats / enjoys a meal |
|
||||
| `just_felt_the_divine` | Divine light clears bad traits | Prose → cleansed by divine light |
|
||||
| `just_played` | Child play task end | Prose → finishes playing around |
|
||||
| `got_caught` | Library asset, **no caller** | Stay Ambient; honest prose |
|
||||
| `got_robbed` / gifts / kiss / talk / kill | Related optional but bag missing | Context bags on steal/gift/kiss/talk/kill |
|
||||
| `fallen_in_love` | Status `fell_in_love` receive + bridge | Keep bridge for no-emotions |
|
||||
| Civic aggregates / BehAddHappiness | Live via forceUnits / Beh* | Marked accurate |
|
||||
|
||||
Worksheet: `docs/event-trigger-audit.tsv` (happiness domain done).
|
||||
|
||||
## Status trigger pass (2026-07-16)
|
||||
|
||||
Full 58-id audit vs `StatusLibrary` + apply sites. Universal Activity hook:
|
||||
`ActorStatusPatches` → `addNewStatusEffect` / `Status.finish` (egg loss also `InterestFeeds.EmitHatch`).
|
||||
|
||||
| Id | Finding | Keep-on fix |
|
||||
|----|---------|-------------|
|
||||
| `angry` | `addAggro`, not the `rage` combat buff | Prose → flies into a fury |
|
||||
| `possessed_follower` | Possession talk converts **target** | Prose → converted into possessed follower |
|
||||
| `pregnant` / `pregnant_parthenogenesis` | Finish → `BabyMaker` birth | Loss prose → gives birth (/ alone) |
|
||||
| `taking_roots` | Finish → vegetative offspring | Loss prose → sprouts offspring |
|
||||
| `budding` | Finish → bud offspring | Loss prose → splits off a bud offspring |
|
||||
| `uprooting` | Plant babies emerging | Prose → emerges as a young plant |
|
||||
| Dreams / `inspired` / combat FX | Sleep pot, finish→happiness, CombatActionLibrary | Already accurate |
|
||||
|
||||
Worksheet: `docs/event-trigger-audit.tsv` (status domain done).
|
||||
|
||||
## WorldLog / plot / trait pass (2026-07-16)
|
||||
|
||||
Phases 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).
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,23 @@
|
|||
# Event reasons (camera + orange dossier)
|
||||
|
||||
## Contract
|
||||
## A/B layers
|
||||
|
||||
| Layer | What | Gate |
|
||||
|-------|------|------|
|
||||
| **B ticker** | Activity / Life / chronicle prose for live ids | Always may write when the game fires |
|
||||
| **A story camera** | Interest candidates, orange reason, camera cuts | `EventCatalog.IsCameraWorthy` only |
|
||||
|
||||
Happiness: `Presentation.Signal` = A; `Ambient` / `Aggregate` = B-only.
|
||||
Discrete / status / WorldLog: `CreatesInterest` = A gate.
|
||||
`InterestDirector` only ranks A candidates. It does not author inventory.
|
||||
|
||||
Registered **events** own the idle camera.
|
||||
`EventStrength` / `TotalScore` ranks who wins.
|
||||
Character fill runs only when the pending **EventLed** queue is empty, and uses an empty Label (task chip only).
|
||||
|
||||
Orange dossier reason = the owning event’s `Label` while the scene is active (`InterestDirector.TryGetOwnedReasonCandidate`).
|
||||
Orange dossier reason = the owning A event’s `Label` while the scene is active (`InterestDirector.TryGetOwnedReasonCandidate`).
|
||||
Quiet grace / inactive dwell clears the reason even if focus has not switched yet.
|
||||
|
||||
Drops (ambient, createsInterest=false, identity filter, below margin, …) go to `InterestDropLog`.
|
||||
|
||||
## EventReason
|
||||
|
||||
[`IdleSpectator/Events/EventReason.cs`](../IdleSpectator/Events/EventReason.cs) is the sole Label factory for camera candidates.
|
||||
|
|
@ -19,21 +28,26 @@ Feeds call typed helpers (`Fight`, `BuildingEat`, `SeekingLover`, …) or `Apply
|
|||
|
||||
| Path | Role |
|
||||
|------|------|
|
||||
| `Events/EventCatalog.cs` (+ `Catalogs/EventCatalog.*.cs`) | **One inventory** - nested domains (`Status`, `Combat`, …) |
|
||||
| `Events/Feeds/` | Build + register candidates from catalog dials |
|
||||
| `Events/Patches/` | Harmony hooks → feeds |
|
||||
| `Events/EventCatalog.cs` (+ `Catalogs/EventCatalog.*.cs`) | **One inventory** - nested domains + `IsCameraWorthy` |
|
||||
| `Events/Feeds/` | Build + register **A** candidates from catalog dials |
|
||||
| `Events/Patches/` | Harmony hooks → feeds / Activity |
|
||||
| `Events/EventReason.cs` | Orange reason sentences |
|
||||
| `Events/EventFeedUtil.cs` | Shared Register path |
|
||||
| `Events/InterestDropLog.cs` | Why a beat stayed B or missed A |
|
||||
| `Events/DiscreteEventEntry.cs` | Shared catalog row shape |
|
||||
|
||||
`InterestDirector` only ranks registered candidates for the camera - it does not author event inventory.
|
||||
|
||||
Rules:
|
||||
|
||||
- Sentence form with names: `{a} is fighting {b}`, `{a} is eating a beehive`, `{a} is seeking a lover`.
|
||||
- Sentence form with names: `{a} is fighting {b}`, `{a} is eating a beehive`, `{a} decides to change the kingdom's culture`.
|
||||
|
||||
Decision orange reasons use the `action` field (infinitive after "decides to").
|
||||
Other domains use `label` or happiness `variantsWithRelated` / `variantsWithoutRelated`.
|
||||
All of that - plus per-id `strength` - lives in [`event-catalog.json`](../IdleSpectator/event-catalog.json).
|
||||
`EventReason.Decision` only formats `{name} decides to {phrase}`.
|
||||
Species/character score overlays live in the same JSON; global formula stays in `scoring-model.json`.
|
||||
- Set `RelatedUnit` when the sentence names a second party.
|
||||
- Never invent stranger subjects for unit-led events.
|
||||
- Never use engine jargon that misstates the action (`Consumes a building` → eating/foraging via `BehConsumeTargetBuilding`).
|
||||
- Never use engine jargon that misstates the action.
|
||||
|
||||
## Removed crumbs
|
||||
|
||||
|
|
@ -44,19 +58,20 @@ Do not author these as orange reasons:
|
|||
- identity titles (`King of X`, `Leader of X`, `Lone beetle`)
|
||||
- `Consumes a building` / `Damages a building: {a}` crumb form
|
||||
|
||||
## Strength dial (no ownership enum)
|
||||
## Strength dial (Layer A only)
|
||||
|
||||
`InterestOwnership` / `OwnsCamera` were removed.
|
||||
Every registration is EventLed and may own the camera.
|
||||
`CreatesInterest` gates whether an id registers; `EventStrength` ranks.
|
||||
Instant cut-in when `next.TotalScore >= current + cutInMargin` (see `scoring-model.json`).
|
||||
`CreatesInterest` / Signal gates whether an id registers for the camera; `EventStrength` ranks among A.
|
||||
Instant cut-in when `next.TotalScore >= current + cutInMargin` (sticky scenes use `stickyCutInMargin`).
|
||||
See `scoring-model.json`.
|
||||
|
||||
## Dossier display
|
||||
|
||||
`UnitDossier.EventLabelBeat` keeps the authored sentence.
|
||||
It only rejects identity/weak crumbs and scrubs living stranger names (`ReasonNamesStranger` / `LeadsWithForeignName`).
|
||||
`UnitDossier.EventLabelBeat` keeps EventReason / `Name verb …` sentences.
|
||||
It rejects identity/weak crumbs and scrubs living stranger names (`ReasonNamesStranger` / `LeadsWithForeignName`).
|
||||
Active EventLed A scenes should not silently blank a valid Label.
|
||||
|
||||
## See also
|
||||
|
||||
- [`event-audit.md`](event-audit.md) - full source inventory, MaxWatch classes, drowning policy
|
||||
- [`event-audit.md`](event-audit.md) - sources, MaxWatch, A demotion set
|
||||
- [`event-prose-audit.md`](event-prose-audit.md) - prose vs game call sites
|
||||
- [`scoring-model.md`](scoring-model.md) - score weights
|
||||
|
|
|
|||
381
docs/event-trigger-audit.tsv
Normal file
381
docs/event-trigger-audit.tsv
Normal file
|
|
@ -0,0 +1,381 @@
|
|||
domain id strength camera game_call_site verdict fix_action done
|
||||
decisions find_lover 62 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
|
||||
decisions try_to_steal_money 68 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
|
||||
decisions banish_unruly_clan_members 76 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
|
||||
decisions kill_unruly_clan_members 82 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
|
||||
decisions try_new_plot 78 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
|
||||
decisions king_change_kingdom_culture 84 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
|
||||
decisions king_change_kingdom_language 82 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
|
||||
decisions king_change_kingdom_religion 84 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
|
||||
decisions leader_change_city_culture 74 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
|
||||
decisions leader_change_city_language 72 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
|
||||
decisions leader_change_city_religion 74 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
|
||||
decisions warrior_army_follow_leader 58 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
|
||||
decisions warrior_train_with_dummy 42 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
|
||||
decisions warrior_try_join_army_group 64 Actor.setDecisionCooldown → DeferredInterestFeed.EmitDecision accurate Camera via IsInterestingDecision predicate; action=infinitive done
|
||||
happiness death_family_member 78 Signal Actor accurate done
|
||||
happiness death_lover 90 Signal Actor accurate done
|
||||
happiness death_child 96 Signal Actor accurate done
|
||||
happiness death_best_friend 80 Signal Actor accurate done
|
||||
happiness got_robbed 56 Signal Actor bridge Context bag / bridge for related names done
|
||||
happiness got_poked 30 Ambient Actor accurate done
|
||||
happiness lost_fight 58 Signal Actor accurate done
|
||||
happiness got_caught 20 Ambient HappinessLibrary asset only - no live caller (Ambient stub) prose_fix Updated catalog prose to match call timing/action done
|
||||
happiness paid_tax 30 Ambient Actor accurate done
|
||||
happiness just_ate 30 Ambient Actor prose_fix Updated catalog prose to match call timing/action done
|
||||
happiness just_received_gift 52 Signal BehFinishTalk bridge Context bag / bridge for related names done
|
||||
happiness just_gave_gift 52 Signal BehFinishTalk bridge Context bag / bridge for related names done
|
||||
happiness just_pooped 30 Ambient Actor accurate done
|
||||
happiness just_slept 30 Signal StatusLibrary accurate done
|
||||
happiness had_bad_dream 40 Signal StatusLibrary accurate done
|
||||
happiness had_good_dream 38 Signal StatusLibrary accurate done
|
||||
happiness had_nightmare 44 Signal StatusLibrary accurate done
|
||||
happiness slept_outside 34 Signal BehTrySleep accurate done
|
||||
happiness just_kissed 62 Signal BehCheckForBabiesFromSexualReproduction bridge Context bag / bridge for related names done
|
||||
happiness just_killed 70 Signal Actor bridge Context bag / bridge for related names done
|
||||
happiness become_king 80 Signal Kingdom accurate done
|
||||
happiness become_leader 72 Signal City accurate done
|
||||
happiness just_won_war 52 Aggregate Actor accurate done
|
||||
happiness just_made_peace 50 Aggregate Actor accurate done
|
||||
happiness just_lost_war 55 Aggregate Actor accurate done
|
||||
happiness was_conquered 58 Aggregate City.joinAnotherKingdom → forceUnits changeHappiness accurate done
|
||||
happiness kingdom_fell_apart 60 Aggregate City.makeOwnKingdom(pFellApart) → forceUnits accurate done
|
||||
happiness just_started_war 78 Aggregate WarManager accurate done
|
||||
happiness just_rebelled 58 Aggregate City.makeOwnKingdom/joinAnotherKingdom → forceUnits accurate done
|
||||
happiness fallen_in_love 74 Signal StatusLibrary fell_in_love action_on_receive (+ becomeLoversWith bridge) bridge Status receive + no-emotions PublishApplied bridge done
|
||||
happiness just_had_child 76 Signal Actor accurate done
|
||||
happiness just_read_book 34 Signal BehFinishReading accurate done
|
||||
happiness just_played 32 Signal BehActorChangeHappiness(child play tasks) prose_fix Updated catalog prose to match call timing/action done
|
||||
happiness just_talked 34 Signal BehFinishTalk bridge Context bag / bridge for related names done
|
||||
happiness just_laughed 34 Signal BehAddHappiness(happy_laughing end) prose_fix Updated catalog prose to match call timing/action done
|
||||
happiness just_sang 34 Signal BehAddHappiness(singing end) accurate done
|
||||
happiness just_swore 32 Signal BehAddHappiness(swearing end) prose_fix Updated catalog prose to match call timing/action done
|
||||
happiness just_cried 42 Signal BehAddHappiness(crying end) accurate done
|
||||
happiness just_talked_gossip 32 Signal BehFinishTalk bridge Context bag / bridge for related names done
|
||||
happiness just_surprised 36 Signal StatusLibrary accurate done
|
||||
happiness just_born 68 Signal Actor accurate done
|
||||
happiness just_magnetised 48 Signal StatusLibrary accurate done
|
||||
happiness just_forced_power 58 Signal MapBox accurate done
|
||||
happiness just_possessed 62 Signal StatusLibrary accurate done
|
||||
happiness strange_urge 46 Signal StatusLibrary accurate done
|
||||
happiness just_had_tantrum 50 Signal StatusLibrary accurate done
|
||||
happiness just_felt_the_divine 56 Signal PowerLibrary prose_fix Updated catalog prose to match call timing/action done
|
||||
happiness just_enchanted 54 Signal StatusLibrary accurate done
|
||||
happiness just_inspired 52 Signal StatusLibrary accurate done
|
||||
happiness wrote_book 62 Signal BookManager accurate done
|
||||
happiness just_became_adult 70 Signal Actor accurate done
|
||||
happiness just_got_out_of_egg 72 Signal Actor accurate done
|
||||
happiness just_finished_plot 70 Signal Plot accurate done
|
||||
happiness just_found_house 58 Signal BehFindHouse accurate done
|
||||
happiness just_lost_house 68 Signal Actor accurate done
|
||||
happiness just_made_friend 58 Signal Actor accurate done
|
||||
happiness just_injured 60 Signal Actor accurate done
|
||||
happiness just_cursed 60 Signal StatusLibrary accurate done
|
||||
happiness starving 50 Signal StatusLibrary accurate done
|
||||
happiness conquered_city 70 Aggregate Kingdom accurate done
|
||||
happiness destroyed_city 62 Aggregate Kingdom accurate done
|
||||
happiness lost_crown 72 Signal Kingdom accurate done
|
||||
happiness razed_capital 68 Aggregate Kingdom accurate done
|
||||
happiness lost_capital 64 Aggregate Kingdom accurate done
|
||||
happiness razed_city 60 Aggregate Kingdom accurate done
|
||||
happiness lost_city 66 Aggregate Kingdom accurate done
|
||||
happiness become_alpha 74 Signal Family accurate done
|
||||
status afterglow 20 False BehCheckForBabiesFromSexualReproduction accurate done
|
||||
status cough 20 False StatusLibrary / addNewStatusEffect accurate done
|
||||
status dash 20 False CombatActionLibrary accurate done
|
||||
status dodge 20 False CombatActionLibrary accurate done
|
||||
status flicked 20 False God power / finger flick accurate done
|
||||
status just_ate 20 False Actor consume / StatusLibrary accurate done
|
||||
status on_guard 20 False StatusLibrary / addNewStatusEffect accurate done
|
||||
status recovery_combat_action 20 False CombatActionLibrary cooldown accurate done
|
||||
status recovery_plot 20 False Plot cooldown accurate done
|
||||
status recovery_social 20 False Social cooldown accurate done
|
||||
status recovery_spell 20 False Spell cooldown accurate done
|
||||
status shield 20 False Combat / spell shield accurate done
|
||||
status slowness 20 False StatusLibrary / addNewStatusEffect accurate done
|
||||
status spell_boost 20 False Spell cast accurate done
|
||||
status spell_silence 20 False Spell cast accurate done
|
||||
status burning 72 True Fire / tile / combat accurate done
|
||||
status possessed 74 True Possession convert / god power accurate done
|
||||
status possessed_follower 60 True ControllableUnit.checkTalk convert target prose_fix Target becomes follower; not under sway done
|
||||
status frozen 65 True Cold / freeze powers accurate done
|
||||
status poisoned 58 True Poison attacks / tiles accurate done
|
||||
status drowning 50 True Water drowning check accurate done
|
||||
status stunned 50 True Combat stun / makeStunned accurate done
|
||||
status rage 62 True Combat rage buff accurate done
|
||||
status angry 48 True Actor.addAggro prose_fix Aggro anger; do not conflate with rage done
|
||||
status cursed 60 True Curse powers / traits accurate done
|
||||
status soul_harvested 78 True Soul harvest action accurate done
|
||||
status voices_in_my_head 55 True StatusLibrary / addNewStatusEffect accurate done
|
||||
status tantrum 52 True StatusLibrary; finish→just_had_tantrum accurate done
|
||||
status magnetized 55 True Magnet powers accurate done
|
||||
status invincible 50 True Invincibility powers accurate done
|
||||
status powerup 54 True Powerup effects accurate done
|
||||
status enchanted 52 True Enchant powers accurate done
|
||||
status ash_fever 50 True Ash fever effect accurate done
|
||||
status starving 56 True Hunger / nutrition accurate done
|
||||
status surprised 42 True getSurprised / tryToGetSurprised accurate done
|
||||
status confused 40 True StatusLibrary / addNewStatusEffect accurate done
|
||||
status strange_urge 45 True StatusLibrary / addNewStatusEffect accurate done
|
||||
status inspired 42 True StatusLibrary; finish→just_inspired accurate done
|
||||
status motivated 40 True StatusLibrary / addNewStatusEffect accurate done
|
||||
status fell_in_love 74 True becomeLoversWith / StatusLibrary receive accurate done
|
||||
status pregnant 72 True Sexual reproduction; finish→makeBabyFromPregnancy prose_fix Loss = gives birth done
|
||||
status pregnant_parthenogenesis 72 True BehCheckParthenogenesisReproduction; finish→baby prose_fix Loss = gives birth alone done
|
||||
status crying 58 True Emotion tasks / StatusLibrary accurate done
|
||||
status festive_spirit 36 True Festival / StatusLibrary accurate done
|
||||
status laughing 34 True Emotion tasks / StatusLibrary accurate done
|
||||
status singing 34 True Emotion tasks / StatusLibrary accurate done
|
||||
status sleeping 32 True Sleep tasks; finish→dream pot accurate done
|
||||
status handsome_migrant 44 True Migration status accurate done
|
||||
status being_suspicious 40 True Steal / suspicious tasks accurate done
|
||||
status budding 56 True BehCheckBuddingReproduction; finish→makeBabyViaBudding prose_fix Loss = bud offspring done
|
||||
status caffeinated 36 True Consume caffeine accurate done
|
||||
status had_bad_dream 40 True Sleep action_finish dream pot accurate done
|
||||
status had_good_dream 38 True Sleep action_finish dream pot accurate done
|
||||
status had_nightmare 48 True Sleep action_finish dream pot accurate done
|
||||
status swearing 34 True Emotion tasks / possession swear accurate done
|
||||
status taking_roots 54 True BehCheckVegetativeReproduction; finish→vegetative baby prose_fix Loss = sprouts offspring, not pull free done
|
||||
status uprooting 54 True BabyMaker plant birth prose_fix Plant emerging/maturing, not generic uproot done
|
||||
status egg 40 False Egg form; loss→InterestFeeds.EmitHatch accurate done
|
||||
worldLog king_new 80 True WorldLogMessage.add; special1=kingdom special2=king accurate {b}=king {a}=kingdom matches kingReplacer done
|
||||
worldLog king_left 72 True WorldLogMessage.add; special1=kingdom special2=king accurate {b}=king {a}=kingdom matches kingReplacer done
|
||||
worldLog king_fled_capital 70 True WorldLogMessage.add; special1=kingdom special2=king accurate {b}=king {a}=kingdom matches kingReplacer done
|
||||
worldLog king_fled_city 68 True WorldLogMessage.add; special1=kingdom special2=king accurate {b}=king {a}=kingdom matches kingReplacer done
|
||||
worldLog king_dead 84 True WorldLogMessage.add; special1=kingdom special2=king accurate {b}=king {a}=kingdom matches kingReplacer done
|
||||
worldLog king_killed 88 True WorldLogMessage.add; special1=kingdom special2=king accurate {b}=king {a}=kingdom matches kingReplacer done
|
||||
worldLog favorite_dead 82 True WorldLogMessage.add accurate done
|
||||
worldLog favorite_killed 85 True WorldLogMessage.add accurate done
|
||||
worldLog city_new 72 True WorldLogMessage.add accurate done
|
||||
worldLog log_city_revolted 90 True WorldLogMessage.add accurate done
|
||||
worldLog city_destroyed 92 True WorldLogMessage.add accurate done
|
||||
worldLog diplomacy_war_ended 72 True WorldLogMessage.add accurate done
|
||||
worldLog diplomacy_war_started 100 True WorldLogMessage.add; special1/2=kingdoms accurate Also WarManager.newWar feed (dual path, distinct keys) done
|
||||
worldLog total_war_started 100 True WorldLogMessage.add accurate done
|
||||
worldLog alliance_new 70 True WorldLogMessage.add accurate done
|
||||
worldLog alliance_dissolved 68 True WorldLogMessage.add accurate done
|
||||
worldLog kingdom_new 75 True WorldLogMessage.add accurate done
|
||||
worldLog kingdom_destroyed 100 True WorldLogMessage.add accurate done
|
||||
worldLog kingdom_shattered 98 True WorldLogMessage.add accurate done
|
||||
worldLog kingdom_fractured 95 True WorldLogMessage.add accurate done
|
||||
worldLog kingdom_royal_clan_new 74 True WorldLogMessage.add accurate done
|
||||
worldLog kingdom_royal_clan_changed 72 True WorldLogMessage.add accurate done
|
||||
worldLog kingdom_royal_clan_dead 76 True WorldLogMessage.add accurate done
|
||||
worldLog disaster_tornado 95 True WorldLogMessage.add (disaster→world_log) accurate done
|
||||
worldLog disaster_meteorite 95 True WorldLogMessage.add (disaster→world_log) accurate done
|
||||
worldLog disaster_hellspawn 96 True WorldLogMessage.add (disaster→world_log) accurate done
|
||||
worldLog disaster_earthquake 95 True WorldLogMessage.add (disaster→world_log) accurate done
|
||||
worldLog disaster_greg_abominations 96 True WorldLogMessage.add (disaster→world_log) accurate done
|
||||
worldLog disaster_ice_ones 95 True WorldLogMessage.add (disaster→world_log) accurate done
|
||||
worldLog disaster_sudden_snowman 90 True WorldLogMessage.add (disaster→world_log) accurate done
|
||||
worldLog disaster_garden_surprise 90 True WorldLogMessage.add (disaster→world_log) accurate done
|
||||
worldLog disaster_dragon_from_farlands 98 True WorldLogMessage.add (disaster→world_log) accurate done
|
||||
worldLog disaster_bandits 92 True WorldLogMessage.add (disaster→world_log) accurate done
|
||||
worldLog disaster_alien_invasion 97 True WorldLogMessage.add (disaster→world_log) accurate done
|
||||
worldLog disaster_biomass 96 True WorldLogMessage.add (disaster→world_log) accurate done
|
||||
worldLog disaster_tumor 96 True WorldLogMessage.add (disaster→world_log) accurate done
|
||||
worldLog disaster_heatwave 88 True WorldLogMessage.add (disaster→world_log) accurate done
|
||||
worldLog disaster_evil_mage 94 True WorldLogMessage.add (disaster→world_log) accurate done
|
||||
worldLog disaster_underground_necromancer 95 True WorldLogMessage.add (disaster→world_log) accurate done
|
||||
worldLog disaster_mad_thoughts 90 True WorldLogMessage.add (disaster→world_log) accurate done
|
||||
worldLog auto_tester 10 False WorldLogMessage.add accurate CreatesInterest=false harness-only done
|
||||
plots rebellion 92 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
|
||||
plots new_war 94 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
|
||||
plots alliance_create 80 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
|
||||
plots alliance_join 68 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
|
||||
plots alliance_destroy 86 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
|
||||
plots attacker_stop_war 78 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
|
||||
plots new_book 52 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
|
||||
plots new_language 60 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
|
||||
plots new_religion 70 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
|
||||
plots new_culture 68 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
|
||||
plots clan_ascension 82 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
|
||||
plots culture_divide 75 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
|
||||
plots religion_schism 84 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
|
||||
plots language_divergence 58 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
|
||||
plots summon_meteor_rain 96 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
|
||||
plots summon_earthquake 95 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
|
||||
plots summon_thunderstorm 93 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
|
||||
plots summon_stormfront 93 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
|
||||
plots summon_hellstorm 97 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
|
||||
plots summon_demons 96 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
|
||||
plots summon_angles 96 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
|
||||
plots summon_skeletons 92 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
|
||||
plots summon_living_plants 90 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
|
||||
plots big_cast_coffee 48 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
|
||||
plots big_cast_bubble_shield 64 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
|
||||
plots big_cast_madness 80 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
|
||||
plots big_cast_slowness 70 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
|
||||
plots cause_rebellion 90 True PlotManager/Actor.setPlot/finishPlot → PlotInterestFeed prose_fix Phase-aware EventReason.Plot (new/complete/cancel/leave) done
|
||||
relationship set_lover 78 True Actor.setLover accurate done
|
||||
relationship clear_lover 60 True setLover(null)+MapBox dead-lover cleanup hook_retarget done
|
||||
relationship add_child 74 True Actor.setParent1/setParent2 hook_retarget done
|
||||
relationship new_family 72 True FamilyManager.newFamily accurate done
|
||||
relationship family_removed 58 True FamilyManager.removeObject accurate Confirmed patch + EventReason prose done
|
||||
relationship find_lover 52 True BehFindLover Continue && !hasLover gate done
|
||||
relationship become_alpha 74 True Family.setAlpha accurate done
|
||||
relationship family_group_new 58 True BehFamilyGroupNew; shares new_family key merge done
|
||||
relationship family_group_join 36 True BehFamilyGroupJoin accurate Confirmed patch + EventReason prose done
|
||||
relationship family_group_leave 36 True BehFamilyGroupLeave accurate Confirmed patch + EventReason prose done
|
||||
relationship baby_created 72 True ActorManager.createBabyActorFromData accurate done
|
||||
traits zombie 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits wise 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits whirlwind 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits weightless 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits weak 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits veteran 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits venomous 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits unlucky 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits ugly 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits tumor_infection 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits tough 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits titan_lungs 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits tiny 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits thorns 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits thief 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits super_health 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits sunblessed 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits stupid 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits strong_minded 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits strong 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits soft_skin 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits slow 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits skin_burns 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits short_sighted 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits shiny 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits scar_of_divinity 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits savage 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits regeneration 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits pyromaniac 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits psychopath 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits poisonous 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits poison_immune 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits plague 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits peaceful 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits paranoid 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits pacifist 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits nightchild 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits mute 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits mush_spores 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits moonchild 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits miracle_born 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits miracle_bearer 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits miner 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits metamorphed 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits mega_heartbeat 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits mageslayer 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits madness 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits lustful 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits lucky 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits long_liver 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits light_lamp 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits kingslayer 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits infertile 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits infected 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits immune 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits immortal 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits hotheaded 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits honest 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits heliophobia 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits heart_of_wizard 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits healing_aura 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits hard_skin 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits greedy 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits golden_tooth 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits gluttonous 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits giant 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits genius 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits freeze_proof 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits fragile_health 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits flower_prints 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits flesh_eater 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits fire_proof 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits fire_blood 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits fertile 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits fat 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits fast 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits eyepatch 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits evil 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits energized 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits eagle_eyed 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits dragonslayer 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits dodge 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits desire_harp 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits desire_golden_egg 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits desire_computer 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits desire_alien_mold 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits deflect_projectile 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits deceitful 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits death_nuke 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits death_mark 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits death_bomb 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits dash 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits crippled 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits content 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits contagious 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits cold_aura 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits clumsy 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits clone 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits chosen_one 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits burning_feet 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits bubble_defense 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits boosted_vitality 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits bomberman 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits boat 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits bloodlust 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits block 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits blessed 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits battle_reflexes 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits backstep 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits attractive 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits arcane_reflexes 58 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits ambitious 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits agile 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits acid_touch 72 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits acid_proof 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
traits acid_blood 38 True Actor.addTrait/removeTrait → EventReason.Trait prose_fix Catalog label aligned to gains/loses; spawn-window gate done
|
||||
books family_story 52 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
|
||||
books love_story 56 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
|
||||
books friendship_story 50 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
|
||||
books bad_story_about_king 64 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
|
||||
books fable 48 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
|
||||
books warfare_manual 58 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
|
||||
books economy_manual 48 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
|
||||
books stewardship_manual 48 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
|
||||
books diplomacy_manual 55 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
|
||||
books mathbook 45 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
|
||||
books biology_book 45 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
|
||||
books history_book 55 True BookManager.newBook/generateNewBook/burnBook accurate burn forces burns-a-book prose done
|
||||
eras age_hope 78 True WorldAgeManager.startNextAge → MetaInterestFeed.EmitEra accurate done
|
||||
eras age_sun 80 True WorldAgeManager.startNextAge → MetaInterestFeed.EmitEra accurate done
|
||||
eras age_dark 88 True WorldAgeManager.startNextAge → MetaInterestFeed.EmitEra accurate done
|
||||
eras age_tears 86 True WorldAgeManager.startNextAge → MetaInterestFeed.EmitEra accurate done
|
||||
eras age_moon 82 True WorldAgeManager.startNextAge → MetaInterestFeed.EmitEra accurate done
|
||||
eras age_chaos 92 True WorldAgeManager.startNextAge → MetaInterestFeed.EmitEra accurate done
|
||||
eras age_wonders 84 True WorldAgeManager.startNextAge → MetaInterestFeed.EmitEra accurate done
|
||||
eras age_ice 87 True WorldAgeManager.startNextAge → MetaInterestFeed.EmitEra accurate done
|
||||
eras age_ash 90 True WorldAgeManager.startNextAge → MetaInterestFeed.EmitEra accurate done
|
||||
eras age_despair 91 True WorldAgeManager.startNextAge → MetaInterestFeed.EmitEra accurate done
|
||||
eras age_unknown 70 True WorldAgeManager.startNextAge → MetaInterestFeed.EmitEra accurate done
|
||||
disasters tornado 95 True Policy→WorldLog disaster_tornado; no DisasterManager patch accurate Camera via paired worldLog row done
|
||||
disasters heatwave 88 True Policy→WorldLog disaster_heatwave; no DisasterManager patch accurate Camera via paired worldLog row done
|
||||
disasters small_meteorite 95 True Policy→WorldLog disaster_meteorite; no DisasterManager patch accurate Camera via paired worldLog row done
|
||||
disasters small_earthquake 95 True Policy→WorldLog disaster_earthquake; no DisasterManager patch accurate Camera via paired worldLog row done
|
||||
disasters hellspawn 96 True Policy→WorldLog disaster_hellspawn; no DisasterManager patch accurate Camera via paired worldLog row done
|
||||
disasters ice_ones_awoken 95 True Policy→WorldLog disaster_ice_ones; no DisasterManager patch accurate Camera via paired worldLog row done
|
||||
disasters sudden_snowman 90 True Policy→WorldLog disaster_sudden_snowman; no DisasterManager patch accurate Camera via paired worldLog row done
|
||||
disasters garden_surprise 90 True Policy→WorldLog disaster_garden_surprise; no DisasterManager patch accurate Camera via paired worldLog row done
|
||||
disasters dragon_from_farlands 98 True Policy→WorldLog disaster_dragon_from_farlands; no DisasterManager patch accurate Camera via paired worldLog row done
|
||||
disasters ash_bandits 92 True Policy→WorldLog disaster_bandits; no DisasterManager patch accurate Camera via paired worldLog row done
|
||||
disasters alien_invasion 97 True Policy→WorldLog disaster_alien_invasion; no DisasterManager patch accurate Camera via paired worldLog row done
|
||||
disasters biomass 96 True Policy→WorldLog disaster_biomass; no DisasterManager patch accurate Camera via paired worldLog row done
|
||||
disasters tumor 96 True Policy→WorldLog disaster_tumor; no DisasterManager patch accurate Camera via paired worldLog row done
|
||||
disasters wild_mage 94 True Policy→WorldLog disaster_evil_mage; no DisasterManager patch accurate Camera via paired worldLog row done
|
||||
disasters underground_necromancer 95 True Policy→WorldLog disaster_underground_necromancer; no DisasterManager patch accurate Camera via paired worldLog row done
|
||||
disasters mad_thoughts 90 True Policy→WorldLog disaster_mad_thoughts; no DisasterManager patch accurate Camera via paired worldLog row done
|
||||
disasters greg_abominations 96 True Policy→WorldLog disaster_greg_abominations; no DisasterManager patch accurate Camera via paired worldLog row done
|
||||
warTypes normal 90 True WarManager.newWar/endWar + WarInterestFeed.Tick accurate Live label often Attacker vs Defender done
|
||||
warTypes spite 88 True WarManager.newWar/endWar + WarInterestFeed.Tick accurate Live label often Attacker vs Defender done
|
||||
warTypes inspire 86 True WarManager.newWar/endWar + WarInterestFeed.Tick accurate Live label often Attacker vs Defender done
|
||||
warTypes rebellion 92 True WarManager.newWar/endWar + WarInterestFeed.Tick accurate Live label often Attacker vs Defender done
|
||||
warTypes whisper_of_war 84 True WarManager.newWar/endWar + WarInterestFeed.Tick accurate Live label often Attacker vs Defender done
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
# Scoring model
|
||||
|
||||
**Source of truth (game loads this):** [`IdleSpectator/scoring-model.json`](../IdleSpectator/scoring-model.json)
|
||||
**Global formula:** [`IdleSpectator/scoring-model.json`](../IdleSpectator/scoring-model.json)
|
||||
|
||||
**Per-event inventory (strength + prose):** [`IdleSpectator/event-catalog.json`](../IdleSpectator/event-catalog.json)
|
||||
|
||||
- `scoring-model.json` - margins, multipliers, rarity caps
|
||||
- `event-catalog.json` - every authored event id (`happiness`, `status`, `worldLog`, `plots`, `decisions`, …)
|
||||
- Decisions use `action` (clause after "decides to …")
|
||||
- Most other domains use `label`
|
||||
- Happiness uses `variantsWithRelated` / `variantsWithoutRelated`
|
||||
- Optional `species` / `character` bonuses
|
||||
|
||||
**Visual tracker (open beside chat):** [scoring-model canvas](/home/dazed/.cursor/projects/home-dazed-Projects-worldbox-mods-idle-mode/canvases/scoring-model.canvas.tsx)
|
||||
|
||||
Edit the `weights` block in the JSON to change live scoring.
|
||||
The canvas charts and calculator mirror those weights.
|
||||
After changing the JSON, refresh the canvas `W` snapshot so the visual stays accurate.
|
||||
Edit the `weights` block in scoring-model.json to change live scoring formula knobs.
|
||||
Edit event-catalog.json to change what each event is worth and what it says.
|
||||
|
|
|
|||
91
docs/world-event-inventory.md
Normal file
91
docs/world-event-inventory.md
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
# World event inventory (full surface)
|
||||
|
||||
Source of truth for what IdleSpectator covers across the **whole** game.
|
||||
Refresh via live libraries + `mutation_discovery` / domain audits under `.harness/`.
|
||||
|
||||
Audit date: 2026-07-16.
|
||||
|
||||
## Policy
|
||||
|
||||
**If it is interesting, it is an interest candidate.**
|
||||
`CreatesInterest` / Happiness `Signal` registers Layer A.
|
||||
`EventStrength` + scoring decide who gets the spotlight.
|
||||
Only true brief FX, cooldowns, civic Aggregates, and god-tool / biome spam stay B-only.
|
||||
|
||||
## How to read this
|
||||
|
||||
| Layer | Meaning |
|
||||
|-------|---------|
|
||||
| **B** | Activity / Life / chronicle prose may fire; no interest candidate |
|
||||
| **A** | Registers interest; director ranks by points |
|
||||
| **Gap** | Game fires a lifecycle beat; we have no reliable A path |
|
||||
|
||||
## Live libraries (enumerated)
|
||||
|
||||
| Library | ~Count | Owner |
|
||||
|---------|-------:|-------|
|
||||
| happiness_library | 67 | EventCatalog.Happiness |
|
||||
| status | 58 | EventCatalog.Status |
|
||||
| world_log_library | 41 | EventCatalog.WorldLog |
|
||||
| disasters | 17 | via WorldLog |
|
||||
| war_types_library | 5 | EventCatalog.WarType |
|
||||
| traits | 116 | EventCatalog.Trait (all CI; strength ranks) |
|
||||
| plots_library | 28 | EventCatalog.Plot |
|
||||
| era_library | 11 | EventCatalog.Era |
|
||||
| book_types | 12 | EventCatalog.Book (all A) |
|
||||
| decisions / powers / spells / items | 127 / 339 / 12 / 111 | EventCatalog.Libraries |
|
||||
| subspecies_traits / genes / phenotypes | 204 / 47 / 52 | Libraries |
|
||||
| buildings | 618 | patches (consume/destroy A; damage skipped) |
|
||||
| actor_library | 322 | SpeciesDiscovery / scoring |
|
||||
| culture/religion/clan/language traits | 78 / 40 / 29 / 26 | **no event catalog yet** |
|
||||
|
||||
## Layer A sizes (policy)
|
||||
|
||||
| Domain | Authored | Layer A |
|
||||
|--------|--------:|--------:|
|
||||
| Happiness | 67 | ~49 Signal (5 Ambient FX; 13 Aggregate civic) |
|
||||
| Status | 58 | ~42 CreatesInterest (FX/cooldowns + egg gain stay B) |
|
||||
| WorldLog | 41 | ~40 |
|
||||
| Relationship | 11 | all interesting discrete (seeker/pack at low strength) |
|
||||
| Plot / Era / War / Book | 28 / 11 / 5 / 12 | all A |
|
||||
| Trait | 116 | all CI (no noticeScoreMin hard drop) |
|
||||
| Spell / Item / Gene / Phenotype / WorldLaw / SubspeciesTrait | live | ambient A at low strength; Signal bumps spectacle |
|
||||
| Power / Decision / Biome | live | Signal predicate only (tools / idle AI / terrain) |
|
||||
| Building / Boat | none | destroy/consume/trade/unload; damage/load skipped |
|
||||
|
||||
## Wired hooks (major)
|
||||
|
||||
Happiness, status (+ egg-loss hatch), WorldLog, wars, plots (new/cancel/join/leave/finish),
|
||||
meta genesis (culture/religion/language/clan/army/alliance/city/kingdom),
|
||||
books, traits, deferred libraries, boats (trade/unload), buildings (consume/destroy),
|
||||
relationship (lover/child/family/baby/alpha), Family.setAlpha, SpeciesDiscovery.
|
||||
|
||||
## Known gaps / thin areas
|
||||
|
||||
| Gap | Why it matters | Status |
|
||||
|-----|----------------|--------|
|
||||
| Culture/religion/clan/language trait gain | Meta drama libraries exist | **Open** |
|
||||
| Building complete / wonder finish | Settlement spectacle | **Open** |
|
||||
| Alliance/kingdom destroy managers | End beats if WorldLog thin | **Open** |
|
||||
| Era/earthquake poll (ongoing) | Age change is wired; poll not | **Open** |
|
||||
|
||||
## Stay B (true noise)
|
||||
|
||||
- Happiness: `got_poked`, `got_caught`, `paid_tax`, `just_ate`, `just_pooped` + all Aggregates
|
||||
- Status: dash/dodge/shield/recovery_*/spell_*/cough/flicked/just_ate/on_guard/afterglow/slowness + egg gain
|
||||
- Libraries: Power ambient (god paint tools), Decision ambient (idle AI; interesting decisions are Signal), Biome ambient
|
||||
- Patches: `building_damage`, `boat_load`
|
||||
|
||||
## Scoring notes
|
||||
|
||||
`TotalScore = EventStrength + scale + CharacterSignificance * charWeight + visual + novelty`.
|
||||
|
||||
CharacterSignificance includes favorite/king/leader/renown/kills/level and species rarity.
|
||||
|
||||
## Refresh commands
|
||||
|
||||
```bash
|
||||
./scripts/harness-run.sh --no-launch mutation_discovery
|
||||
./scripts/harness-run.sh --no-launch domain_event_audit
|
||||
./scripts/harness-run.sh --no-launch activity_happiness_audit
|
||||
```
|
||||
843
scripts/export-event-catalog-from-cs.py
Executable file
843
scripts/export-event-catalog-from-cs.py
Executable file
|
|
@ -0,0 +1,843 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Export IdleSpectator/event-catalog.json from EventCatalog C# sources.
|
||||
|
||||
Parses authored catalog partials under IdleSpectator/Events/Catalogs/ and merges
|
||||
with existing decisions/species/character overlays from event-catalog.json
|
||||
(JSON decisions strengths/actions win).
|
||||
|
||||
Usage (from repo root):
|
||||
python3 scripts/export-event-catalog-from-cs.py
|
||||
python3 scripts/export-event-catalog-from-cs.py --out IdleSpectator/event-catalog.json
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
CATALOGS = REPO / "IdleSpectator" / "Events" / "Catalogs"
|
||||
DEFAULT_OUT = REPO / "IdleSpectator" / "event-catalog.json"
|
||||
DEFAULT_EXISTING = DEFAULT_OUT
|
||||
|
||||
MOD_VERSION = "0.25.0"
|
||||
UPDATED = "2026-07-16"
|
||||
NOTE = (
|
||||
"Authored IdleSpectator event catalog exported from EventCatalog C# sources "
|
||||
"(happiness, status, worldLog, plots, relationship, traits, books, eras, "
|
||||
"disasters, warTypes, libraries defaults). decisions/species/character merge "
|
||||
"from prior JSON (JSON wins). Loaded by EventCatalogConfig; scoring-model.json "
|
||||
"remains the global formula."
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# C# text helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def strip_csharp_comments(text: str) -> str:
|
||||
"""Remove // and /* */ comments outside of string literals."""
|
||||
out: list[str] = []
|
||||
i = 0
|
||||
n = len(text)
|
||||
while i < n:
|
||||
c = text[i]
|
||||
if c == '"':
|
||||
j = i + 1
|
||||
while j < n:
|
||||
if text[j] == "\\":
|
||||
j += 2
|
||||
continue
|
||||
if text[j] == '"':
|
||||
j += 1
|
||||
break
|
||||
j += 1
|
||||
out.append(text[i:j])
|
||||
i = j
|
||||
continue
|
||||
if c == "/" and i + 1 < n:
|
||||
nxt = text[i + 1]
|
||||
if nxt == "/":
|
||||
while i < n and text[i] not in "\r\n":
|
||||
i += 1
|
||||
continue
|
||||
if nxt == "*":
|
||||
i += 2
|
||||
while i + 1 < n and not (text[i] == "*" and text[i + 1] == "/"):
|
||||
i += 1
|
||||
i = min(n, i + 2)
|
||||
continue
|
||||
out.append(c)
|
||||
i += 1
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def enum_leaf(expr: str) -> str:
|
||||
"""HappinessPresentationTier.Signal -> Signal."""
|
||||
s = expr.strip().rstrip(",")
|
||||
if "." in s:
|
||||
return s.rsplit(".", 1)[-1]
|
||||
return s
|
||||
|
||||
|
||||
def parse_csharp_string(token: str) -> str:
|
||||
token = token.strip()
|
||||
if len(token) >= 2 and token[0] == '"' and token[-1] == '"':
|
||||
inner = token[1:-1]
|
||||
return (
|
||||
inner.replace(r"\\", "\0")
|
||||
.replace(r"\"", '"')
|
||||
.replace(r"\n", "\n")
|
||||
.replace(r"\t", "\t")
|
||||
.replace("\0", "\\")
|
||||
)
|
||||
return token
|
||||
|
||||
|
||||
def parse_number(token: str) -> float | int:
|
||||
t = token.strip().rstrip("fFdDmM")
|
||||
if re.fullmatch(r"-?\d+", t):
|
||||
return int(t)
|
||||
return float(t)
|
||||
|
||||
|
||||
def parse_bool(token: str) -> bool:
|
||||
t = token.strip().lower()
|
||||
if t == "true":
|
||||
return True
|
||||
if t == "false":
|
||||
return False
|
||||
raise ValueError(f"not a bool: {token!r}")
|
||||
|
||||
|
||||
def split_top_level_args(arg_text: str) -> list[str]:
|
||||
"""Split comma-separated C# call args, respecting strings/parens/braces."""
|
||||
args: list[str] = []
|
||||
buf: list[str] = []
|
||||
depth_paren = depth_brace = depth_bracket = 0
|
||||
in_string = False
|
||||
escape = False
|
||||
i = 0
|
||||
while i < len(arg_text):
|
||||
c = arg_text[i]
|
||||
if in_string:
|
||||
buf.append(c)
|
||||
if escape:
|
||||
escape = False
|
||||
elif c == "\\":
|
||||
escape = True
|
||||
elif c == '"':
|
||||
in_string = False
|
||||
i += 1
|
||||
continue
|
||||
if c == '"':
|
||||
in_string = True
|
||||
buf.append(c)
|
||||
i += 1
|
||||
continue
|
||||
if c == "(":
|
||||
depth_paren += 1
|
||||
buf.append(c)
|
||||
elif c == ")":
|
||||
depth_paren -= 1
|
||||
buf.append(c)
|
||||
elif c == "{":
|
||||
depth_brace += 1
|
||||
buf.append(c)
|
||||
elif c == "}":
|
||||
depth_brace -= 1
|
||||
buf.append(c)
|
||||
elif c == "[":
|
||||
depth_bracket += 1
|
||||
buf.append(c)
|
||||
elif c == "]":
|
||||
depth_bracket -= 1
|
||||
buf.append(c)
|
||||
elif (
|
||||
c == ","
|
||||
and depth_paren == 0
|
||||
and depth_brace == 0
|
||||
and depth_bracket == 0
|
||||
):
|
||||
args.append("".join(buf).strip())
|
||||
buf = []
|
||||
else:
|
||||
buf.append(c)
|
||||
i += 1
|
||||
tail = "".join(buf).strip()
|
||||
if tail:
|
||||
args.append(tail)
|
||||
return args
|
||||
|
||||
|
||||
def parse_call_args(arg_text: str) -> tuple[list[Any], dict[str, Any]]:
|
||||
"""Parse positional + named (name: value) C# call arguments."""
|
||||
positional: list[Any] = []
|
||||
named: dict[str, Any] = {}
|
||||
for raw in split_top_level_args(arg_text):
|
||||
if not raw:
|
||||
continue
|
||||
m = re.match(r"^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.*)$", raw, re.S)
|
||||
if m and not raw.lstrip().startswith('"'):
|
||||
name, val = m.group(1), m.group(2).strip()
|
||||
named[name] = coerce_literal(val)
|
||||
else:
|
||||
positional.append(coerce_literal(raw))
|
||||
return positional, named
|
||||
|
||||
|
||||
def is_seed_call(arg_text: str) -> bool:
|
||||
"""True when the first arg is a string literal (seed), not a method signature."""
|
||||
parts = split_top_level_args(arg_text)
|
||||
return bool(parts) and parts[0].lstrip().startswith('"')
|
||||
|
||||
|
||||
def coerce_literal(token: str) -> Any:
|
||||
t = token.strip()
|
||||
if not t:
|
||||
return t
|
||||
if t.startswith('"'):
|
||||
return parse_csharp_string(t)
|
||||
low = t.lower()
|
||||
if low in ("true", "false"):
|
||||
return low == "true"
|
||||
if re.fullmatch(r"-?\d+(\.\d+)?[fFdDmM]?", t):
|
||||
return parse_number(t)
|
||||
if t.startswith("new[]") or t.startswith("new string[]"):
|
||||
return parse_string_array(t)
|
||||
return t
|
||||
|
||||
|
||||
def parse_string_array(expr: str) -> list[str]:
|
||||
m = re.search(r"\{(.*)\}", expr, re.S)
|
||||
if not m:
|
||||
return []
|
||||
inner = m.group(1).strip()
|
||||
if not inner:
|
||||
return []
|
||||
return [parse_csharp_string(p) for p in split_top_level_args(inner) if p.strip()]
|
||||
|
||||
|
||||
def find_method_calls(text: str, method: str) -> list[tuple[int, str]]:
|
||||
"""Return (index, arg_text) for method(...); calls, balanced parens."""
|
||||
results: list[tuple[int, str]] = []
|
||||
pattern = re.compile(rf"\b{re.escape(method)}\s*\(")
|
||||
for m in pattern.finditer(text):
|
||||
start = m.end()
|
||||
depth = 1
|
||||
i = start
|
||||
in_string = False
|
||||
escape = False
|
||||
while i < len(text) and depth > 0:
|
||||
c = text[i]
|
||||
if in_string:
|
||||
if escape:
|
||||
escape = False
|
||||
elif c == "\\":
|
||||
escape = True
|
||||
elif c == '"':
|
||||
in_string = False
|
||||
else:
|
||||
if c == '"':
|
||||
in_string = True
|
||||
elif c == "(":
|
||||
depth += 1
|
||||
elif c == ")":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
results.append((m.start(), text[start:i]))
|
||||
break
|
||||
i += 1
|
||||
return results
|
||||
|
||||
|
||||
def read_catalog(name: str) -> str:
|
||||
path = CATALOGS / name
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError(path)
|
||||
return strip_csharp_comments(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Section parsers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_FAILURES: list[str] = []
|
||||
|
||||
|
||||
def fail(msg: str) -> None:
|
||||
_FAILURES.append(msg)
|
||||
print(f"PARSE FAIL: {msg}", file=sys.stderr)
|
||||
|
||||
|
||||
def parse_happiness(text: str) -> list[dict[str, Any]]:
|
||||
entries: list[dict[str, Any]] = []
|
||||
# Match ["id"] = new HappinessCatalogEntry { ... },
|
||||
pattern = re.compile(
|
||||
r'\["([^"]+)"\]\s*=\s*new\s+HappinessCatalogEntry\s*\{',
|
||||
re.M,
|
||||
)
|
||||
for m in pattern.finditer(text):
|
||||
entry_id = m.group(1)
|
||||
body_start = m.end()
|
||||
depth = 1
|
||||
i = body_start
|
||||
in_string = False
|
||||
escape = False
|
||||
while i < len(text) and depth > 0:
|
||||
c = text[i]
|
||||
if in_string:
|
||||
if escape:
|
||||
escape = False
|
||||
elif c == "\\":
|
||||
escape = True
|
||||
elif c == '"':
|
||||
in_string = False
|
||||
else:
|
||||
if c == '"':
|
||||
in_string = True
|
||||
elif c == "{":
|
||||
depth += 1
|
||||
elif c == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
break
|
||||
i += 1
|
||||
if depth != 0:
|
||||
fail(f"happiness: unclosed block for {entry_id}")
|
||||
continue
|
||||
body = text[body_start:i]
|
||||
try:
|
||||
entries.append(parse_happiness_body(entry_id, body))
|
||||
except Exception as ex: # noqa: BLE001
|
||||
fail(f"happiness[{entry_id}]: {ex}")
|
||||
return entries
|
||||
|
||||
|
||||
def parse_happiness_body(entry_id: str, body: str) -> dict[str, Any]:
|
||||
props: dict[str, str] = {}
|
||||
# Property = value; (value may be new[] { ... })
|
||||
prop_re = re.compile(
|
||||
r"(\w+)\s*=\s*"
|
||||
r"("
|
||||
r"new\s*(?:string\s*)?\[\s*\]\s*\{(?:[^{}]|\{[^{}]*\})*\}"
|
||||
r'|"[^"\\]*(?:\\.[^"\\]*)*"'
|
||||
r"|[^,\n]+"
|
||||
r")\s*,?",
|
||||
re.M,
|
||||
)
|
||||
for m in prop_re.finditer(body):
|
||||
props[m.group(1)] = m.group(2).strip().rstrip(",")
|
||||
|
||||
def req(name: str) -> str:
|
||||
if name not in props:
|
||||
raise ValueError(f"missing {name}")
|
||||
return props[name]
|
||||
|
||||
strength = parse_number(req("EventStrength"))
|
||||
row: dict[str, Any] = {
|
||||
"id": parse_csharp_string(props["Id"]) if "Id" in props else entry_id,
|
||||
"strength": strength,
|
||||
"presentation": enum_leaf(req("Presentation")),
|
||||
"context": enum_leaf(req("Context")),
|
||||
"life": enum_leaf(req("Life")),
|
||||
"overlap": enum_leaf(req("Overlap")),
|
||||
"relationRole": enum_leaf(req("RelationRole")),
|
||||
"category": parse_csharp_string(req("InterestCategory"))
|
||||
if req("InterestCategory").startswith('"')
|
||||
else req("InterestCategory").strip('"'),
|
||||
"variantsWithRelated": parse_string_array(req("VariantsWithRelated"))
|
||||
if "VariantsWithRelated" in props
|
||||
else [],
|
||||
"variantsWithoutRelated": parse_string_array(req("VariantsWithoutRelated"))
|
||||
if "VariantsWithoutRelated" in props
|
||||
else [],
|
||||
}
|
||||
if "CanonicalMilestoneKey" in props:
|
||||
key = props["CanonicalMilestoneKey"]
|
||||
val = parse_csharp_string(key) if key.startswith('"') else key
|
||||
if val:
|
||||
row["canonicalMilestoneKey"] = val
|
||||
if "StatusOverlapId" in props:
|
||||
key = props["StatusOverlapId"]
|
||||
val = parse_csharp_string(key) if key.startswith('"') else key
|
||||
if val:
|
||||
row["statusOverlapId"] = val
|
||||
return row
|
||||
|
||||
|
||||
def parse_status(text: str) -> list[dict[str, Any]]:
|
||||
entries: list[dict[str, Any]] = []
|
||||
for _, args in find_method_calls(text, "AddNoInterest"):
|
||||
if not is_seed_call(args):
|
||||
continue
|
||||
pos, _ = parse_call_args(args)
|
||||
if not pos:
|
||||
fail(f"status AddNoInterest: empty args ({args!r})")
|
||||
continue
|
||||
entries.append(
|
||||
{
|
||||
"id": pos[0],
|
||||
"strength": 20,
|
||||
"category": "StatusAmbient",
|
||||
"camera": False,
|
||||
"extendsGrief": False,
|
||||
}
|
||||
)
|
||||
for _, args in find_method_calls(text, "Add"):
|
||||
if not is_seed_call(args):
|
||||
continue
|
||||
pos, named = parse_call_args(args)
|
||||
if len(pos) < 4:
|
||||
continue
|
||||
try:
|
||||
entry = {
|
||||
"id": pos[0],
|
||||
"strength": _num(pos[1]),
|
||||
"category": pos[2],
|
||||
"camera": bool(pos[3]),
|
||||
"extendsGrief": bool(named.get("extendsGrief", False)),
|
||||
}
|
||||
entries.append(entry)
|
||||
except Exception as ex: # noqa: BLE001
|
||||
fail(f"status Add({args[:60]}…): {ex}")
|
||||
return entries
|
||||
|
||||
|
||||
def parse_world_log(text: str) -> list[dict[str, Any]]:
|
||||
entries: list[dict[str, Any]] = []
|
||||
for _, args in find_method_calls(text, "Add"):
|
||||
if not is_seed_call(args):
|
||||
continue
|
||||
pos, _ = parse_call_args(args)
|
||||
# Seed calls: id, strength, category, createsInterest, label, minWatch, maxWatch
|
||||
if len(pos) < 7:
|
||||
fail(f"worldLog Add: expected 7 args, got {len(pos)} ({args[:60]}…)")
|
||||
continue
|
||||
try:
|
||||
strength = float(pos[1])
|
||||
camera = bool(pos[3])
|
||||
chronicle = camera and strength >= 65.0
|
||||
row: dict[str, Any] = {
|
||||
"id": pos[0],
|
||||
"strength": _num(pos[1]),
|
||||
"category": pos[2],
|
||||
"camera": camera,
|
||||
"label": pos[4],
|
||||
"minWatch": _num(pos[5]),
|
||||
"maxWatch": _num(pos[6]),
|
||||
"chronicle": chronicle,
|
||||
}
|
||||
entries.append(row)
|
||||
except Exception as ex: # noqa: BLE001
|
||||
fail(f"worldLog Add({args[:60]}…): {ex}")
|
||||
return entries
|
||||
|
||||
|
||||
def parse_simple_add(
|
||||
text: str,
|
||||
*,
|
||||
default_category: str | None = None,
|
||||
default_label: str | None = None,
|
||||
default_camera: bool = True,
|
||||
min_pos: int = 2,
|
||||
style: str = "id_strength_category_label",
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Parse Add(...) seed calls for DiscreteEventEntry-style catalogs."""
|
||||
entries: list[dict[str, Any]] = []
|
||||
for _, args in find_method_calls(text, "Add"):
|
||||
if not is_seed_call(args):
|
||||
continue
|
||||
pos, named = parse_call_args(args)
|
||||
if len(pos) < min_pos:
|
||||
continue
|
||||
try:
|
||||
if style == "id_strength_only":
|
||||
# Trait: Add(id, strength) - apply defaults
|
||||
if len(pos) != 2 or not isinstance(pos[0], str):
|
||||
continue
|
||||
row = {
|
||||
"id": pos[0],
|
||||
"strength": _num(pos[1]),
|
||||
"category": default_category or "Trait",
|
||||
"camera": default_camera,
|
||||
"label": default_label or "Trait: {id} ({a})",
|
||||
}
|
||||
elif style == "id_strength_category_label":
|
||||
# Plot/Book/Era/Relationship: Add(id, strength, category, label [, createsInterest])
|
||||
if len(pos) < 4:
|
||||
continue
|
||||
camera = bool(named.get("createsInterest", pos[4] if len(pos) > 4 else default_camera))
|
||||
row = {
|
||||
"id": pos[0],
|
||||
"strength": _num(pos[1]),
|
||||
"category": pos[2],
|
||||
"camera": camera,
|
||||
"label": pos[3],
|
||||
}
|
||||
elif style == "disaster":
|
||||
# Add(id, strength, worldLogId)
|
||||
if len(pos) < 3:
|
||||
continue
|
||||
row = {
|
||||
"id": pos[0],
|
||||
"strength": _num(pos[1]),
|
||||
"category": "Disaster",
|
||||
"camera": True,
|
||||
"worldLogId": pos[2],
|
||||
}
|
||||
elif style == "war_type":
|
||||
# Add(id, strength, labelTemplate)
|
||||
if len(pos) < 3:
|
||||
continue
|
||||
row = {
|
||||
"id": pos[0],
|
||||
"strength": _num(pos[1]),
|
||||
"category": "Politics",
|
||||
"camera": True,
|
||||
"label": pos[2],
|
||||
}
|
||||
else:
|
||||
fail(f"unknown add style {style}")
|
||||
continue
|
||||
entries.append(row)
|
||||
except Exception as ex: # noqa: BLE001
|
||||
fail(f"{style} Add({args[:60]}…): {ex}")
|
||||
return entries
|
||||
|
||||
|
||||
def _num(v: Any) -> int | float:
|
||||
if isinstance(v, float) and v.is_integer():
|
||||
return int(v)
|
||||
return v
|
||||
|
||||
|
||||
def parse_libraries(text: str) -> dict[str, Any]:
|
||||
"""Emit defaults per library class; include signalIds only when HashSet listed."""
|
||||
# Class blocks: public static class Spell { ... }
|
||||
class_re = re.compile(
|
||||
r"public\s+static\s+class\s+(\w+)\s*\{",
|
||||
re.M,
|
||||
)
|
||||
classes = list(class_re.finditer(text))
|
||||
wanted_order = [
|
||||
("Spell", "spell"),
|
||||
("Power", "power"),
|
||||
("Item", "item"),
|
||||
("Gene", "gene"),
|
||||
("Phenotype", "phenotype"),
|
||||
("WorldLaw", "worldLaw"),
|
||||
("SubspeciesTrait", "subspeciesTrait"),
|
||||
("Biome", "biome"),
|
||||
]
|
||||
by_cs: dict[str, dict[str, Any]] = {}
|
||||
for idx, m in enumerate(classes):
|
||||
name = m.group(1)
|
||||
if name not in {cs for cs, _ in wanted_order}:
|
||||
continue
|
||||
start = m.end()
|
||||
end = classes[idx + 1].start() if idx + 1 < len(classes) else len(text)
|
||||
block = text[start:end]
|
||||
defaults = extract_ensure_seeded_defaults(name, block)
|
||||
if defaults is None:
|
||||
fail(f"libraries[{name}]: could not parse EnsureSeeded defaults")
|
||||
continue
|
||||
by_cs[name] = defaults
|
||||
libs: dict[str, Any] = {}
|
||||
for cs_name, json_key in wanted_order:
|
||||
if cs_name in by_cs:
|
||||
libs[json_key] = by_cs[cs_name]
|
||||
return libs
|
||||
|
||||
|
||||
def extract_ensure_seeded_defaults(name: str, block: str) -> dict[str, Any] | None:
|
||||
# Find EnsureSeeded( ... ) call args
|
||||
calls = find_method_calls(block, "EnsureSeeded")
|
||||
if not calls:
|
||||
# LiveLibraryInterest.EnsureSeeded via expression body
|
||||
calls = find_method_calls(block, "LiveLibraryInterest.EnsureSeeded")
|
||||
if not calls:
|
||||
return None
|
||||
_, arg_text = calls[0]
|
||||
pos, named = parse_call_args(arg_text)
|
||||
# Signature (positional):
|
||||
# Entries, enumerate, category, label, ambientStrength, signalIds, signalStrength,
|
||||
# [signalPredicate], ambientCreatesInterest
|
||||
# Named kwargs also used: ambientStrength, signalIds, signalStrength,
|
||||
# signalPredicate, ambientCreatesInterest
|
||||
category = None
|
||||
label = None
|
||||
ambient_strength = named.get("ambientStrength")
|
||||
signal_strength = named.get("signalStrength")
|
||||
ambient_creates = named.get("ambientCreatesInterest")
|
||||
signal_ids_expr = named.get("signalIds")
|
||||
|
||||
# Positional layout from LiveLibraryInterest.EnsureSeeded - read that file if needed.
|
||||
# From usage:
|
||||
# Entries, Enumerate..., "Category", "label", ambientStrength: N, SignalIds|null,
|
||||
# signalStrength: N, [signalPredicate: ...], ambientCreatesInterest: bool
|
||||
if len(pos) >= 4:
|
||||
category = pos[2] if isinstance(pos[2], str) else category
|
||||
label = pos[3] if isinstance(pos[3], str) else label
|
||||
if ambient_strength is None and len(pos) >= 5 and isinstance(pos[4], (int, float)):
|
||||
ambient_strength = pos[4]
|
||||
# signalIds may be positional after ambientStrength
|
||||
if signal_ids_expr is None and len(pos) >= 6:
|
||||
signal_ids_expr = pos[5]
|
||||
if signal_strength is None:
|
||||
# often named; sometimes after signalIds
|
||||
for p in pos[6:]:
|
||||
if isinstance(p, (int, float)):
|
||||
signal_strength = p
|
||||
break
|
||||
if ambient_creates is None:
|
||||
for p in reversed(pos):
|
||||
if isinstance(p, bool):
|
||||
ambient_creates = p
|
||||
break
|
||||
|
||||
# Also pull named ambientStrength etc. from the raw call via regex for robustness
|
||||
am = re.search(r"ambientStrength\s*:\s*([\d.]+)f?", block)
|
||||
if am and ambient_strength is None:
|
||||
ambient_strength = parse_number(am.group(1))
|
||||
sm = re.search(r"signalStrength\s*:\s*([\d.]+)f?", block)
|
||||
if sm and signal_strength is None:
|
||||
signal_strength = parse_number(sm.group(1))
|
||||
acm = re.search(r"ambientCreatesInterest\s*:\s*(true|false)", block)
|
||||
if acm and ambient_creates is None:
|
||||
ambient_creates = acm.group(1) == "true"
|
||||
|
||||
# Category / label from EnsureSeeded positional strings
|
||||
cat_m = re.search(
|
||||
r"EnsureSeeded\s*\(\s*Entries\s*,\s*[^,]+,\s*\"([^\"]+)\"\s*,\s*\"([^\"]*)\"",
|
||||
block,
|
||||
)
|
||||
if cat_m:
|
||||
category = cat_m.group(1)
|
||||
label = cat_m.group(2)
|
||||
|
||||
if category is None or label is None or ambient_strength is None or signal_strength is None:
|
||||
return None
|
||||
if ambient_creates is None:
|
||||
ambient_creates = True
|
||||
|
||||
out: dict[str, Any] = {
|
||||
"ambientStrength": _num(ambient_strength),
|
||||
"signalStrength": _num(signal_strength),
|
||||
"category": category,
|
||||
"label": label,
|
||||
"ambientCreatesInterest": bool(ambient_creates),
|
||||
}
|
||||
|
||||
# signalIds HashSet initializer if present
|
||||
hs = re.search(
|
||||
r"HashSet\s*<\s*string\s*>\s+\w+\s*=\s*new\s+HashSet\s*<\s*string\s*>\s*"
|
||||
r"(?:\([^)]*\))?\s*\{([^}]*)\}",
|
||||
block,
|
||||
re.S,
|
||||
)
|
||||
if hs:
|
||||
ids = []
|
||||
for sm in re.finditer(r'"([^"]+)"', hs.group(1)):
|
||||
ids.append(sm.group(1))
|
||||
if ids:
|
||||
out["signalIds"] = ids
|
||||
return out
|
||||
|
||||
|
||||
def load_existing_overlays(path: Path) -> dict[str, Any]:
|
||||
if not path.is_file():
|
||||
return {"decisions": [], "species": [], "character": []}
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
return {
|
||||
"decisions": data.get("decisions") or [],
|
||||
"species": data.get("species") or [],
|
||||
"character": data.get("character") or [],
|
||||
}
|
||||
|
||||
|
||||
def merge_decisions(
|
||||
existing: list[dict[str, Any]],
|
||||
decision_cs: str,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Keep JSON decisions (strength/action win). Fill gaps from BuiltinActionPhrases."""
|
||||
by_id: dict[str, dict[str, Any]] = {}
|
||||
# Builtin phrases from C#
|
||||
m = re.search(
|
||||
r"BuiltinActionPhrases\s*=\s*new\s+Dictionary[^\{]*\{(.*)\};\s*\n\s*///",
|
||||
decision_cs,
|
||||
re.S,
|
||||
)
|
||||
if not m:
|
||||
# fallback: grab until closing of static field
|
||||
m = re.search(
|
||||
r"BuiltinActionPhrases\s*=\s*new\s+Dictionary[^\{]*\{(.*?)^\s*\};",
|
||||
decision_cs,
|
||||
re.S | re.M,
|
||||
)
|
||||
builtins: dict[str, str] = {}
|
||||
if m:
|
||||
for km in re.finditer(r'\["([^"]+)"\]\s*=\s*"([^"]*)"', m.group(1)):
|
||||
builtins[km.group(1)] = km.group(2)
|
||||
else:
|
||||
fail("decisions: could not parse BuiltinActionPhrases")
|
||||
|
||||
for row in existing:
|
||||
rid = row.get("id")
|
||||
if not rid:
|
||||
continue
|
||||
by_id[rid] = {
|
||||
"id": rid,
|
||||
"action": row.get("action") or builtins.get(rid, ""),
|
||||
"strength": row.get("strength", 86),
|
||||
}
|
||||
|
||||
# Do not invent strengths for builtin-only ids not in JSON (user: JSON wins / keep from existing)
|
||||
# Optionally add builtin ids missing from JSON with no strength? User said keep from existing JSON.
|
||||
# So only existing decisions list.
|
||||
return list(by_id.values()) if by_id else [
|
||||
{"id": k, "action": v, "strength": 86} for k, v in builtins.items()
|
||||
]
|
||||
|
||||
|
||||
def build_document(existing_path: Path) -> dict[str, Any]:
|
||||
overlays = load_existing_overlays(existing_path)
|
||||
|
||||
happiness = parse_happiness(read_catalog("EventCatalog.Happiness.cs"))
|
||||
status = parse_status(read_catalog("EventCatalog.Status.cs"))
|
||||
world_log = parse_world_log(read_catalog("EventCatalog.WorldLog.cs"))
|
||||
plots = parse_simple_add(
|
||||
read_catalog("EventCatalog.Plot.cs"),
|
||||
style="id_strength_category_label",
|
||||
min_pos=4,
|
||||
)
|
||||
relationship = parse_simple_add(
|
||||
read_catalog("EventCatalog.Relationship.cs"),
|
||||
style="id_strength_category_label",
|
||||
min_pos=4,
|
||||
)
|
||||
traits = parse_simple_add(
|
||||
read_catalog("EventCatalog.Trait.cs"),
|
||||
style="id_strength_only",
|
||||
default_category="Trait",
|
||||
default_label="Trait: {id} ({a})",
|
||||
default_camera=True,
|
||||
min_pos=2,
|
||||
)
|
||||
books = parse_simple_add(
|
||||
read_catalog("EventCatalog.Book.cs"),
|
||||
style="id_strength_category_label",
|
||||
min_pos=4,
|
||||
)
|
||||
eras = parse_simple_add(
|
||||
read_catalog("EventCatalog.Era.cs"),
|
||||
style="id_strength_category_label",
|
||||
min_pos=4,
|
||||
)
|
||||
disasters = parse_simple_add(
|
||||
read_catalog("EventCatalog.Disaster.cs"),
|
||||
style="disaster",
|
||||
min_pos=3,
|
||||
)
|
||||
war_types = parse_simple_add(
|
||||
read_catalog("EventCatalog.WarType.cs"),
|
||||
style="war_type",
|
||||
min_pos=3,
|
||||
)
|
||||
libraries = parse_libraries(read_catalog("EventCatalog.Libraries.cs"))
|
||||
|
||||
decision_cs = read_catalog("EventCatalog.Decision.cs")
|
||||
decisions = merge_decisions(overlays["decisions"], decision_cs)
|
||||
|
||||
doc: dict[str, Any] = {
|
||||
"modVersion": MOD_VERSION,
|
||||
"title": "IdleSpectator event catalog",
|
||||
"updated": UPDATED,
|
||||
"note": NOTE,
|
||||
"decisions": decisions,
|
||||
"species": overlays["species"],
|
||||
"character": overlays["character"],
|
||||
"happiness": happiness,
|
||||
"status": status,
|
||||
"worldLog": world_log,
|
||||
"plots": plots,
|
||||
"relationship": relationship,
|
||||
"traits": traits,
|
||||
"books": books,
|
||||
"eras": eras,
|
||||
"disasters": disasters,
|
||||
"warTypes": war_types,
|
||||
"libraries": libraries,
|
||||
}
|
||||
return doc
|
||||
|
||||
|
||||
def print_counts(doc: dict[str, Any]) -> None:
|
||||
sections = [
|
||||
"decisions",
|
||||
"species",
|
||||
"character",
|
||||
"happiness",
|
||||
"status",
|
||||
"worldLog",
|
||||
"plots",
|
||||
"relationship",
|
||||
"traits",
|
||||
"books",
|
||||
"eras",
|
||||
"disasters",
|
||||
"warTypes",
|
||||
]
|
||||
print("Counts:")
|
||||
for key in sections:
|
||||
val = doc.get(key)
|
||||
n = len(val) if isinstance(val, list) else 0
|
||||
print(f" {key}: {n}")
|
||||
libs = doc.get("libraries") or {}
|
||||
print(f" libraries: {len(libs)} keys ({', '.join(libs.keys())})")
|
||||
for k, v in libs.items():
|
||||
sig = v.get("signalIds")
|
||||
extra = f", signalIds={len(sig)}" if isinstance(sig, list) else ""
|
||||
print(
|
||||
f" {k}: ambient={v.get('ambientStrength')} signal={v.get('signalStrength')} "
|
||||
f"category={v.get('category')}{extra}"
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument(
|
||||
"--out",
|
||||
type=Path,
|
||||
default=DEFAULT_OUT,
|
||||
help=f"Output JSON path (default: {DEFAULT_OUT})",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--existing",
|
||||
type=Path,
|
||||
default=DEFAULT_EXISTING,
|
||||
help="Existing event-catalog.json to merge decisions/species/character from",
|
||||
)
|
||||
args = ap.parse_args()
|
||||
|
||||
doc = build_document(args.existing)
|
||||
args.out.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.out.write_text(
|
||||
json.dumps(doc, indent=2, ensure_ascii=False) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(f"Wrote {args.out}")
|
||||
print_counts(doc)
|
||||
if _FAILURES:
|
||||
print(f"\nParse failures ({len(_FAILURES)}):", file=sys.stderr)
|
||||
for f in _FAILURES:
|
||||
print(f" - {f}", file=sys.stderr)
|
||||
return 1
|
||||
print("\nParse failures: none")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Reference in a new issue