From cc0d44ae2e506bbbd311e64ca80939ea6f486ee1 Mon Sep 17 00:00:00 2001 From: DazedAnon Date: Thu, 16 Jul 2026 14:21:48 -0500 Subject: [PATCH] the great migration --- IdleSpectator/AgentHarness.cs | 25 +- IdleSpectator/CatalogCoverageAudit.cs | 46 +- IdleSpectator/EventCatalogConfig.cs | 726 ++++ .../Events/Catalogs/EventCatalog.Book.cs | 57 +- .../Events/Catalogs/EventCatalog.Decision.cs | 406 ++ .../Events/Catalogs/EventCatalog.Disaster.cs | 125 +- .../Events/Catalogs/EventCatalog.Era.cs | 104 +- .../Events/Catalogs/EventCatalog.Happiness.cs | 998 +---- .../Events/Catalogs/EventCatalog.Libraries.cs | 791 ++-- .../Events/Catalogs/EventCatalog.Plot.cs | 121 +- .../Catalogs/EventCatalog.Relationship.cs | 107 +- .../Events/Catalogs/EventCatalog.Status.cs | 182 +- .../Events/Catalogs/EventCatalog.Trait.cs | 205 +- .../Events/Catalogs/EventCatalog.WarType.cs | 135 +- .../Events/Catalogs/EventCatalog.WorldLog.cs | 214 +- IdleSpectator/Events/DiscreteEventEntry.cs | 5 + IdleSpectator/Events/EventCatalog.cs | 24 + IdleSpectator/Events/EventReason.cs | 14 +- IdleSpectator/HarnessScenarios.cs | 10 + IdleSpectator/InterestScoring.cs | 7 +- IdleSpectator/Main.cs | 1 + IdleSpectator/event-catalog.json | 3652 +++++++++++++++++ IdleSpectator/mod.json | 4 +- docs/event-reason.md | 8 +- docs/scoring-model.md | 16 +- scripts/export-event-catalog-from-cs.py | 843 ++++ 26 files changed, 6781 insertions(+), 2045 deletions(-) create mode 100644 IdleSpectator/EventCatalogConfig.cs create mode 100644 IdleSpectator/Events/Catalogs/EventCatalog.Decision.cs create mode 100644 IdleSpectator/event-catalog.json create mode 100755 scripts/export-event-catalog-from-cs.py diff --git a/IdleSpectator/AgentHarness.cs b/IdleSpectator/AgentHarness.cs index 2a36b95..7a813a0 100644 --- a/IdleSpectator/AgentHarness.cs +++ b/IdleSpectator/AgentHarness.cs @@ -1855,6 +1855,7 @@ public static class AgentHarness case "scoring_reload": { bool ok = InterestScoringConfig.Reload(); + bool catalogOk = EventCatalogConfig.Reload(); if (ok) { _cmdOk++; @@ -1868,11 +1869,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" diff --git a/IdleSpectator/CatalogCoverageAudit.cs b/IdleSpectator/CatalogCoverageAudit.cs index 829b119..131da3a 100644 --- a/IdleSpectator/CatalogCoverageAudit.cs +++ b/IdleSpectator/CatalogCoverageAudit.cs @@ -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 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 }; } diff --git a/IdleSpectator/EventCatalogConfig.cs b/IdleSpectator/EventCatalogConfig.cs new file mode 100644 index 0000000..ceceb23 --- /dev/null +++ b/IdleSpectator/EventCatalogConfig.cs @@ -0,0 +1,726 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text; +using NeoModLoader.services; + +namespace IdleSpectator; + +/// One decision row from event-catalog.json (action phrase + EventStrength). +public sealed class DecisionCatalogRow +{ + public string Id = ""; + public string Action = ""; + public float Strength; +} + +/// Library dial defaults from event-catalog.json libraries. +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 SignalIds = new HashSet(StringComparer.OrdinalIgnoreCase); +} + +/// +/// Loads event-catalog.json — SoT for per-event strength/prose overlays. +/// action is decision-only (clause after "decides to"); other domains use label / happiness variants. +/// +public static class EventCatalogConfig +{ + public const string FileName = "event-catalog.json"; + public const float DefaultDecisionStrength = 86f; + + private static readonly Dictionary Decisions = + new Dictionary(StringComparer.OrdinalIgnoreCase); + private static readonly Dictionary SpeciesBonuses = + new Dictionary(StringComparer.OrdinalIgnoreCase); + private static readonly Dictionary CharacterBonuses = + new Dictionary(StringComparer.OrdinalIgnoreCase); + private static readonly Dictionary Happiness = + new Dictionary(StringComparer.OrdinalIgnoreCase); + private static readonly Dictionary Status = + new Dictionary(StringComparer.OrdinalIgnoreCase); + private static readonly Dictionary WorldLog = + new Dictionary(StringComparer.OrdinalIgnoreCase); + private static readonly Dictionary Plots = + new Dictionary(StringComparer.OrdinalIgnoreCase); + private static readonly Dictionary Relationship = + new Dictionary(StringComparer.OrdinalIgnoreCase); + private static readonly Dictionary Traits = + new Dictionary(StringComparer.OrdinalIgnoreCase); + private static readonly Dictionary Books = + new Dictionary(StringComparer.OrdinalIgnoreCase); + private static readonly Dictionary Eras = + new Dictionary(StringComparer.OrdinalIgnoreCase); + private static readonly Dictionary Disasters = + new Dictionary(StringComparer.OrdinalIgnoreCase); + private static readonly Dictionary WarTypes = + new Dictionary(StringComparer.OrdinalIgnoreCase); + private static readonly Dictionary Libraries = + new Dictionary(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 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 into) + { + into.Clear(); + foreach (KeyValuePair kv in Happiness) + { + into[kv.Key] = kv.Value; + } + return into.Count; + } + + public static int FillStatus(Dictionary into) + { + into.Clear(); + foreach (KeyValuePair kv in Status) + { + into[kv.Key] = kv.Value; + } + return into.Count; + } + + public static int FillWorldLog(Dictionary into) + { + into.Clear(); + foreach (KeyValuePair kv in WorldLog) + { + into[kv.Key] = kv.Value; + } + return into.Count; + } + + public static int FillPlots(Dictionary into) => FillDiscrete(Plots, into); + public static int FillRelationship(Dictionary into) => FillDiscrete(Relationship, into); + public static int FillTraits(Dictionary into) => FillDiscrete(Traits, into); + public static int FillBooks(Dictionary into) => FillDiscrete(Books, into); + public static int FillEras(Dictionary into) => FillDiscrete(Eras, into); + + public static int FillDisasters(Dictionary into) + { + into.Clear(); + foreach (KeyValuePair kv in Disasters) + { + into[kv.Key] = kv.Value; + } + return into.Count; + } + + public static int FillWarTypes(Dictionary into) + { + into.Clear(); + foreach (KeyValuePair kv in WarTypes) + { + into[kv.Key] = kv.Value; + } + return into.Count; + } + + private static int FillDiscrete( + Dictionary src, + Dictionary into) + { + into.Clear(); + foreach (KeyValuePair 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 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 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(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(); + } + + var list = new List(); + 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 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; + } +} diff --git a/IdleSpectator/Events/Catalogs/EventCatalog.Book.cs b/IdleSpectator/Events/Catalogs/EventCatalog.Book.cs index 1b15e43..f6e52a9 100644 --- a/IdleSpectator/Events/Catalogs/EventCatalog.Book.cs +++ b/IdleSpectator/Events/Catalogs/EventCatalog.Book.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; namespace IdleSpectator; -/// Authored interest overlay for every live book_types asset. +/// Book dials from event-catalog.json books. public static partial class EventCatalog { public static class Book @@ -11,46 +11,43 @@ public static partial class EventCatalog private static readonly Dictionary Entries = new Dictionary(StringComparer.OrdinalIgnoreCase); - static Book() + private static int _appliedGeneration = -1; + + public static void InvalidateOverlays() { - // All authored books are Layer A; EventStrength ranks them. - 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"); + Entries.Clear(); + _appliedGeneration = -1; } - private static void Add( - string id, - float strength, - string category, - string label) + private static void Ensure() { - Entries[id] = new DiscreteEventEntry + if (_appliedGeneration == EventCatalogConfig.Generation) { - Id = id, - EventStrength = strength, - Category = category, - CreatesInterest = true, - LabelTemplate = label - }; + return; + } + + _appliedGeneration = EventCatalogConfig.Generation; + EventCatalogConfig.FillBooks(Entries); } - public static IEnumerable AuthoredIds => Entries.Keys; + public static IEnumerable AuthoredIds + { + get + { + Ensure(); + return Entries.Keys; + } + } - public static bool HasAuthored(string id) => - !string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim()); + 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; diff --git a/IdleSpectator/Events/Catalogs/EventCatalog.Decision.cs b/IdleSpectator/Events/Catalogs/EventCatalog.Decision.cs new file mode 100644 index 0000000..ff712d6 --- /dev/null +++ b/IdleSpectator/Events/Catalogs/EventCatalog.Decision.cs @@ -0,0 +1,406 @@ +using System; +using System.Collections.Generic; + +namespace IdleSpectator; + +/// +/// Decision interest dials + authored action phrases. +/// +/// Source of truth: event-catalog.json (decisions rows: action + strength). +/// is the offline fallback when that file is missing. +/// Live ids still seed via ; camera Signal uses +/// ; missing ActionPhrase falls back to a generic humanize. +/// +/// +public static partial class EventCatalog +{ + public static class Decision + { + private static readonly Dictionary Entries = + new Dictionary(StringComparer.OrdinalIgnoreCase); + + private static int _appliedOverlayGeneration = -1; + + /// + /// Offline fallback infinitive clauses when event-catalog.json is missing a row. + /// Prefer editing event-catalog.json instead. + /// + private static readonly Dictionary BuiltinActionPhrases = + new Dictionary(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", + }; + + /// Drop cached rows so the next Ensure re-applies JSON overlays. + 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"); + } + + /// Underscore-token match so war does not hit warrior. + 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 + "_"); + } + + /// True when a decision id should be allowed to own the idle camera. + public static bool IsCameraWorthy(string id) => IsInterestingDecision(id); + + /// Authored action-phrase ids (JSON catalog ∪ builtin fallback). + public static IEnumerable AuthoredActionPhraseIds + { + get + { + var seen = new HashSet(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); + } + + /// + /// Infinitive clause for decides to …. Prefers event-catalog.json / builtin, + /// else a generic humanize of the asset id. + /// + 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 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; + } + + /// + /// Generic id → infinitive clause when no authored row exists. + /// Prefer adding a row to event-catalog.json instead of relying on this. + /// + 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(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); + } + } +} diff --git a/IdleSpectator/Events/Catalogs/EventCatalog.Disaster.cs b/IdleSpectator/Events/Catalogs/EventCatalog.Disaster.cs index 6e91170..6c6fc48 100644 --- a/IdleSpectator/Events/Catalogs/EventCatalog.Disaster.cs +++ b/IdleSpectator/Events/Catalogs/EventCatalog.Disaster.cs @@ -13,82 +13,77 @@ public sealed class DisasterInterestEntry public bool IsFallback; } -/// Authored interest overlay for every live disasters library asset. +/// Disaster dials from event-catalog.json disasters. public static partial class EventCatalog { public static class Disaster -{ - private static readonly Dictionary Entries = - new Dictionary(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 Entries = + new Dictionary(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 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 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 + }; + } } } -} diff --git a/IdleSpectator/Events/Catalogs/EventCatalog.Era.cs b/IdleSpectator/Events/Catalogs/EventCatalog.Era.cs index e42c71c..3817e0e 100644 --- a/IdleSpectator/Events/Catalogs/EventCatalog.Era.cs +++ b/IdleSpectator/Events/Catalogs/EventCatalog.Era.cs @@ -3,66 +3,66 @@ using System.Collections.Generic; namespace IdleSpectator; -/// Authored interest overlay for every live era_library asset. +/// Era dials from event-catalog.json eras. public static partial class EventCatalog { public static class Era -{ - private static readonly Dictionary Entries = - new Dictionary(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 Entries = + new Dictionary(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 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 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 + }; + } } } -} diff --git a/IdleSpectator/Events/Catalogs/EventCatalog.Happiness.cs b/IdleSpectator/Events/Catalogs/EventCatalog.Happiness.cs index c926608..49b40af 100644 --- a/IdleSpectator/Events/Catalogs/EventCatalog.Happiness.cs +++ b/IdleSpectator/Events/Catalogs/EventCatalog.Happiness.cs @@ -3,954 +3,78 @@ using System.Collections.Generic; namespace IdleSpectator; -/// Authored policy + prose for every live happiness effect. +/// +/// Happiness policy + prose. Source of truth: event-catalog.json happiness section. +/// public static partial class EventCatalog { public static class Happiness -{ - private static readonly Dictionary Entries = - new Dictionary(StringComparer.OrdinalIgnoreCase) - { - ["death_family_member"] = new HappinessCatalogEntry - { - Id = "death_family_member", - EventStrength = 76f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.RequiresRelatedActor, - Life = HappinessLifePolicy.ProjectToLife, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.DeceasedFamily, - InterestCategory = "Grief", - VariantsWithRelated = new[] { "mourns the death of family member {related}", "grieves after family member {related} dies" }, - VariantsWithoutRelated = new[] { "mourns a family member's death", "grieves a family loss" }, - }, - ["death_lover"] = new HappinessCatalogEntry - { - Id = "death_lover", - EventStrength = 88f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.RequiresRelatedActor, - Life = HappinessLifePolicy.ProjectToLife, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.DeceasedLover, - InterestCategory = "Grief", - VariantsWithRelated = new[] { "falls into despair after their lover {related}'s death", "mourns their lover {related}" }, - VariantsWithoutRelated = new[] { "mourns a lost lover", "grieves a lover's death" }, - }, - ["death_child"] = new HappinessCatalogEntry - { - Id = "death_child", - EventStrength = 95f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.RequiresRelatedActor, - Life = HappinessLifePolicy.ProjectToLife, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.DeceasedChild, - InterestCategory = "Grief", - VariantsWithRelated = new[] { "falls into despair after their child {related}'s death", "mourns their child {related}" }, - VariantsWithoutRelated = new[] { "mourns a child's death", "grieves the loss of a child" }, - }, - ["death_best_friend"] = new HappinessCatalogEntry - { - Id = "death_best_friend", - EventStrength = 78f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.RequiresRelatedActor, - Life = HappinessLifePolicy.ProjectToLife, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.DeceasedBestFriend, - InterestCategory = "Grief", - VariantsWithRelated = new[] { "mourns their best friend {related}", "grieves after best friend {related} dies" }, - VariantsWithoutRelated = new[] { "mourns a best friend's death", "grieves a lost best friend" }, - }, - ["got_robbed"] = new HappinessCatalogEntry - { - Id = "got_robbed", - EventStrength = 50f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.RelatedOptional, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.Robber, - InterestCategory = "Social", - VariantsWithRelated = new[] { "is robbed by {related}", "loses belongings to {related}" }, - VariantsWithoutRelated = new[] { "is robbed", "gets robbed" }, - }, - ["got_poked"] = new HappinessCatalogEntry - { - Id = "got_poked", - EventStrength = 30f, - Presentation = HappinessPresentationTier.Ambient, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Emotion", - VariantsWithRelated = new[] { "gets poked by the gods", "is poked" }, - VariantsWithoutRelated = new[] { "gets poked by the gods", "is poked" }, - }, - ["lost_fight"] = new HappinessCatalogEntry - { - Id = "lost_fight", - EventStrength = 55f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Emotion", - VariantsWithRelated = new[] { "is forced to yield in a fight", "gives up mid-fight" }, - VariantsWithoutRelated = new[] { "is forced to yield in a fight", "gives up mid-fight" }, - }, - ["got_caught"] = new HappinessCatalogEntry - { - Id = "got_caught", - // Library-only id today - no changeHappiness("got_caught") call site in the game. - EventStrength = 20f, - Presentation = HappinessPresentationTier.Ambient, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Emotion", - VariantsWithRelated = new[] { "gets caught", "is caught" }, - VariantsWithoutRelated = new[] { "gets caught", "is caught" }, - }, - ["paid_tax"] = new HappinessCatalogEntry - { - Id = "paid_tax", - EventStrength = 30f, - Presentation = HappinessPresentationTier.Ambient, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Daily", - VariantsWithRelated = new[] { "pays taxes", "grumbles while paying taxes" }, - VariantsWithoutRelated = new[] { "pays taxes", "grumbles while paying taxes" }, - }, - ["just_ate"] = new HappinessCatalogEntry - { - Id = "just_ate", - EventStrength = 30f, - Presentation = HappinessPresentationTier.Ambient, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Continuation, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Daily", - StatusOverlapId = "just_ate", - VariantsWithRelated = new[] { "finishes a meal", "eats and feels better" }, - VariantsWithoutRelated = new[] { "finishes a meal", "eats and feels better" }, - }, - ["just_received_gift"] = new HappinessCatalogEntry - { - Id = "just_received_gift", - EventStrength = 50f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.RelatedOptional, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.GiftPartner, - InterestCategory = "Social", - VariantsWithRelated = new[] { "receives a gift from {related}", "is given a gift by {related}" }, - VariantsWithoutRelated = new[] { "receives a gift", "is given a gift" }, - }, - ["just_gave_gift"] = new HappinessCatalogEntry - { - Id = "just_gave_gift", - EventStrength = 50f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.RelatedOptional, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.GiftPartner, - InterestCategory = "Social", - VariantsWithRelated = new[] { "gives {related} a gift", "offers a gift to {related}" }, - VariantsWithoutRelated = new[] { "gives a gift", "offers a gift" }, - }, - ["just_pooped"] = new HappinessCatalogEntry - { - Id = "just_pooped", - EventStrength = 30f, - Presentation = HappinessPresentationTier.Ambient, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Daily", - VariantsWithRelated = new[] { "finds relief", "finishes relieving themselves" }, - VariantsWithoutRelated = new[] { "finds relief", "finishes relieving themselves" }, - }, - ["just_slept"] = new HappinessCatalogEntry - { - Id = "just_slept", - EventStrength = 30f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Daily", - VariantsWithRelated = new[] { "finishes sleeping", "ends a rest" }, - VariantsWithoutRelated = new[] { "finishes sleeping", "ends a rest" }, - }, - ["had_bad_dream"] = new HappinessCatalogEntry - { - Id = "had_bad_dream", - EventStrength = 30f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Continuation, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Daily", - StatusOverlapId = "had_bad_dream", - VariantsWithRelated = new[] { "has a bad dream", "is troubled by a bad dream" }, - VariantsWithoutRelated = new[] { "has a bad dream", "is troubled by a bad dream" }, - }, - ["had_good_dream"] = new HappinessCatalogEntry - { - Id = "had_good_dream", - EventStrength = 30f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Continuation, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Daily", - StatusOverlapId = "had_good_dream", - VariantsWithRelated = new[] { "has a good dream", "dreams pleasantly" }, - VariantsWithoutRelated = new[] { "has a good dream", "dreams pleasantly" }, - }, - ["had_nightmare"] = new HappinessCatalogEntry - { - Id = "had_nightmare", - EventStrength = 30f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Continuation, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Daily", - StatusOverlapId = "had_nightmare", - VariantsWithRelated = new[] { "has a nightmare", "is haunted by a nightmare" }, - VariantsWithoutRelated = new[] { "has a nightmare", "is haunted by a nightmare" }, - }, - ["slept_outside"] = new HappinessCatalogEntry - { - Id = "slept_outside", - EventStrength = 30f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Daily", - VariantsWithRelated = new[] { "has to sleep without a house", "settles down outdoors, unhappy" }, - VariantsWithoutRelated = new[] { "has to sleep without a house", "settles down outdoors, unhappy" }, - }, - ["just_kissed"] = new HappinessCatalogEntry - { - Id = "just_kissed", - // Game id is mating afterglow (BehCheckForBabiesFromSexualReproduction), not a kiss. - EventStrength = 50f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.RelatedOptional, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.Lover, - InterestCategory = "Social", - VariantsWithRelated = new[] { "mates with {related}", "shares intimacy with {related}" }, - VariantsWithoutRelated = new[] { "mates with their lover", "shares intimacy with their lover" }, - }, - ["just_killed"] = new HappinessCatalogEntry - { - Id = "just_killed", - // Only applied in newKillAction when the killer has trait bloodlust. - EventStrength = 45f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.CanonicalMilestone, - RelationRole = HappinessRelationRole.Victim, - InterestCategory = "Emotion", - CanonicalMilestoneKey = "milestone_kill", - VariantsWithRelated = new[] { "kills {related} and feels bloodlust's rush", "bloodlust flares after killing {related}" }, - VariantsWithoutRelated = new[] { "bloodlust flares after a kill", "feels bloodlust's rush after a kill" }, - }, - ["become_king"] = new HappinessCatalogEntry - { - Id = "become_king", - EventStrength = 72f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ProjectToLife, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "LifeChapter", - VariantsWithRelated = new[] { "takes the crown", "becomes ruler" }, - VariantsWithoutRelated = new[] { "takes the crown", "becomes ruler" }, - }, - ["become_leader"] = new HappinessCatalogEntry - { - Id = "become_leader", - EventStrength = 65f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ProjectToLife, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "LifeChapter", - VariantsWithRelated = new[] { "becomes a leader", "rises to leadership" }, - VariantsWithoutRelated = new[] { "becomes a leader", "rises to leadership" }, - }, - ["just_won_war"] = new HappinessCatalogEntry - { - Id = "just_won_war", - EventStrength = 35f, - Presentation = HappinessPresentationTier.Aggregate, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Civic", - VariantsWithRelated = new[] { "rejoices at a won war", "celebrates victory in war" }, - VariantsWithoutRelated = new[] { "rejoices at a won war", "celebrates victory in war" }, - }, - ["just_made_peace"] = new HappinessCatalogEntry - { - Id = "just_made_peace", - EventStrength = 35f, - Presentation = HappinessPresentationTier.Aggregate, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Civic", - VariantsWithRelated = new[] { "feels relief as peace is made", "welcomes peace" }, - VariantsWithoutRelated = new[] { "feels relief as peace is made", "welcomes peace" }, - }, - ["just_lost_war"] = new HappinessCatalogEntry - { - Id = "just_lost_war", - EventStrength = 35f, - Presentation = HappinessPresentationTier.Aggregate, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Civic", - VariantsWithRelated = new[] { "despairs after a lost war", "mourns a lost war" }, - VariantsWithoutRelated = new[] { "despairs after a lost war", "mourns a lost war" }, - }, - ["was_conquered"] = new HappinessCatalogEntry - { - Id = "was_conquered", - EventStrength = 35f, - Presentation = HappinessPresentationTier.Aggregate, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Civic", - VariantsWithRelated = new[] { "suffers under conquest", "is crushed by conquest" }, - VariantsWithoutRelated = new[] { "suffers under conquest", "is crushed by conquest" }, - }, - ["kingdom_fell_apart"] = new HappinessCatalogEntry - { - Id = "kingdom_fell_apart", - EventStrength = 35f, - Presentation = HappinessPresentationTier.Aggregate, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Civic", - VariantsWithRelated = new[] { "watches the kingdom fall apart", "despairs as the kingdom fractures" }, - VariantsWithoutRelated = new[] { "watches the kingdom fall apart", "despairs as the kingdom fractures" }, - }, - ["just_started_war"] = new HappinessCatalogEntry - { - Id = "just_started_war", - EventStrength = 80f, - Presentation = HappinessPresentationTier.Aggregate, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Civic", - VariantsWithRelated = new[] { "is glad war has started", "cheers a war their culture loves" }, - VariantsWithoutRelated = new[] { "is glad war has started", "cheers a war their culture loves" }, - }, - ["just_rebelled"] = new HappinessCatalogEntry - { - Id = "just_rebelled", - EventStrength = 35f, - Presentation = HappinessPresentationTier.Aggregate, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Civic", - VariantsWithRelated = new[] { "is swept up as the city rebels", "lives through a rebellion" }, - VariantsWithoutRelated = new[] { "is swept up as the city rebels", "lives through a rebellion" }, - }, - ["fallen_in_love"] = new HappinessCatalogEntry - { - Id = "fallen_in_love", - EventStrength = 50f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.RelatedOptional, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.CanonicalMilestone, - RelationRole = HappinessRelationRole.Lover, - InterestCategory = "Social", - CanonicalMilestoneKey = "milestone_lover", - StatusOverlapId = "fell_in_love", - VariantsWithRelated = new[] { "falls in love with {related}", "is smitten with {related}" }, - VariantsWithoutRelated = new[] { "falls in love", "is smitten" }, - }, - ["just_had_child"] = new HappinessCatalogEntry - { - Id = "just_had_child", - // birthEvent is on the parent; related newborn is not wired today. - EventStrength = 70f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ProjectToLife, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "LifeChapter", - VariantsWithRelated = new[] { "has a child", "feels the joy of parenthood" }, - VariantsWithoutRelated = new[] { "has a child", "feels the joy of parenthood" }, - }, - ["just_read_book"] = new HappinessCatalogEntry - { - Id = "just_read_book", - EventStrength = 30f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Daily", - VariantsWithRelated = new[] { "finishes reading a book", "puts down a book" }, - VariantsWithoutRelated = new[] { "finishes reading a book", "puts down a book" }, - }, - ["just_played"] = new HappinessCatalogEntry - { - Id = "just_played", - EventStrength = 30f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Daily", - VariantsWithRelated = new[] { "finishes playing", "has fun playing" }, - VariantsWithoutRelated = new[] { "finishes playing", "has fun playing" }, - }, - ["just_talked"] = new HappinessCatalogEntry - { - Id = "just_talked", - EventStrength = 30f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.RelatedOptional, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.ConversationPartner, - InterestCategory = "Social", - VariantsWithRelated = new[] { "finishes talking with {related}", "ends a conversation with {related}" }, - VariantsWithoutRelated = new[] { "finishes talking", "ends a conversation" }, - }, - ["just_laughed"] = new HappinessCatalogEntry - { - Id = "just_laughed", - EventStrength = 30f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Continuation, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Daily", - StatusOverlapId = "laughing", - VariantsWithRelated = new[] { "bursts out laughing", "shares a laugh" }, - VariantsWithoutRelated = new[] { "bursts out laughing", "shares a laugh" }, - }, - ["just_sang"] = new HappinessCatalogEntry - { - Id = "just_sang", - EventStrength = 30f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Continuation, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Daily", - StatusOverlapId = "singing", - VariantsWithRelated = new[] { "finishes singing", "sings with feeling" }, - VariantsWithoutRelated = new[] { "finishes singing", "sings with feeling" }, - }, - ["just_swore"] = new HappinessCatalogEntry - { - Id = "just_swore", - EventStrength = 30f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Continuation, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Daily", - StatusOverlapId = "swearing", - VariantsWithRelated = new[] { "lets out a curse", "swears loudly" }, - VariantsWithoutRelated = new[] { "lets out a curse", "swears loudly" }, - }, - ["just_cried"] = new HappinessCatalogEntry - { - Id = "just_cried", - EventStrength = 30f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Continuation, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Daily", - StatusOverlapId = "crying", - VariantsWithRelated = new[] { "finishes crying", "cries it out" }, - VariantsWithoutRelated = new[] { "finishes crying", "cries it out" }, - }, - ["just_talked_gossip"] = new HappinessCatalogEntry - { - Id = "just_talked_gossip", - EventStrength = 30f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.RelatedOptional, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.ConversationPartner, - InterestCategory = "Social", - VariantsWithRelated = new[] { "gossips with {related}", "shares lover-gossip with {related}" }, - VariantsWithoutRelated = new[] { "gossips about lovers", "shares lover-gossip" }, - }, - ["just_surprised"] = new HappinessCatalogEntry - { - Id = "just_surprised", - EventStrength = 30f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Continuation, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Emotion", - StatusOverlapId = "surprised", - VariantsWithRelated = new[] { "is startled", "jumps in surprise" }, - VariantsWithoutRelated = new[] { "is startled", "jumps in surprise" }, - }, - ["just_born"] = new HappinessCatalogEntry - { - // Actor.newCreature spawn bookkeeping - not live birth; may precede egg form. - Id = "just_born", - EventStrength = 48f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ProjectToLife, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "LifeChapter", - VariantsWithRelated = new[] { "appears in the world", "is brought into existence" }, - VariantsWithoutRelated = new[] { "appears in the world", "is brought into existence" }, - }, - ["just_magnetised"] = new HappinessCatalogEntry - { - // Game id uses British spelling; status asset is magnetized. - Id = "just_magnetised", - EventStrength = 45f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Continuation, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Emotion", - StatusOverlapId = "magnetized", - VariantsWithRelated = new[] { "is magnetized", "feels a magnetic pull" }, - VariantsWithoutRelated = new[] { "is magnetized", "feels a magnetic pull" }, - }, - ["just_forced_power"] = new HappinessCatalogEntry - { - Id = "just_forced_power", - EventStrength = 45f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Emotion", - VariantsWithRelated = new[] { "is flung by a god power", "is blasted aside by divine force" }, - VariantsWithoutRelated = new[] { "is flung by a god power", "is blasted aside by divine force" }, - }, - ["just_possessed"] = new HappinessCatalogEntry - { - Id = "just_possessed", - EventStrength = 45f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Continuation, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Emotion", - StatusOverlapId = "possessed", - VariantsWithRelated = new[] { "is possessed", "falls under possession" }, - VariantsWithoutRelated = new[] { "is possessed", "falls under possession" }, - }, - ["strange_urge"] = new HappinessCatalogEntry - { - Id = "strange_urge", - EventStrength = 45f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Continuation, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Emotion", - StatusOverlapId = "strange_urge", - VariantsWithRelated = new[] { "feels a strange urge", "is gripped by a strange urge" }, - VariantsWithoutRelated = new[] { "feels a strange urge", "is gripped by a strange urge" }, - }, - ["just_had_tantrum"] = new HappinessCatalogEntry - { - Id = "just_had_tantrum", - EventStrength = 45f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Continuation, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Emotion", - StatusOverlapId = "tantrum", - VariantsWithRelated = new[] { "finishes a tantrum", "calms after a tantrum" }, - VariantsWithoutRelated = new[] { "finishes a tantrum", "calms after a tantrum" }, - }, - ["just_felt_the_divine"] = new HappinessCatalogEntry - { - Id = "just_felt_the_divine", - EventStrength = 45f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Emotion", - VariantsWithRelated = new[] { "feels the divine touch", "is touched by the divine" }, - VariantsWithoutRelated = new[] { "feels the divine touch", "is touched by the divine" }, - }, - ["just_enchanted"] = new HappinessCatalogEntry - { - Id = "just_enchanted", - EventStrength = 45f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Continuation, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Emotion", - StatusOverlapId = "enchanted", - VariantsWithRelated = new[] { "is enchanted", "feels an enchantment take hold" }, - VariantsWithoutRelated = new[] { "is enchanted", "feels an enchantment take hold" }, - }, - ["just_inspired"] = new HappinessCatalogEntry - { - Id = "just_inspired", - EventStrength = 45f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Continuation, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Emotion", - StatusOverlapId = "inspired", - // Happiness fires on inspired status end (action_finish), not onset. - VariantsWithRelated = new[] { "comes down from inspiration", "lets inspiration settle into contentment" }, - VariantsWithoutRelated = new[] { "comes down from inspiration", "lets inspiration settle into contentment" }, - }, - ["wrote_book"] = new HappinessCatalogEntry - { - Id = "wrote_book", - EventStrength = 65f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ProjectToLife, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "LifeChapter", - VariantsWithRelated = new[] { "writes a book", "finishes writing a book" }, - VariantsWithoutRelated = new[] { "writes a book", "finishes writing a book" }, - }, - ["just_became_adult"] = new HappinessCatalogEntry - { - Id = "just_became_adult", - EventStrength = 65f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ProjectToLife, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "LifeChapter", - VariantsWithRelated = new[] { "comes of age", "becomes an adult" }, - VariantsWithoutRelated = new[] { "comes of age", "becomes an adult" }, - }, - ["just_got_out_of_egg"] = new HappinessCatalogEntry - { - Id = "just_got_out_of_egg", - EventStrength = 65f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ProjectToLife, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "LifeChapter", - VariantsWithRelated = new[] { "hatches from an egg", "breaks free of an egg" }, - VariantsWithoutRelated = new[] { "hatches from an egg", "breaks free of an egg" }, - }, - ["just_finished_plot"] = new HappinessCatalogEntry - { - Id = "just_finished_plot", - EventStrength = 65f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ProjectToLife, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "LifeChapter", - VariantsWithRelated = new[] { "completes a plot", "finishes a long plot" }, - VariantsWithoutRelated = new[] { "completes a plot", "finishes a long plot" }, - }, - ["just_found_house"] = new HappinessCatalogEntry - { - Id = "just_found_house", - EventStrength = 55f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ProjectToLife, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "LifeChapter", - VariantsWithRelated = new[] { "finds a home", "settles into a house" }, - VariantsWithoutRelated = new[] { "finds a home", "settles into a house" }, - }, - ["just_lost_house"] = new HappinessCatalogEntry - { - Id = "just_lost_house", - EventStrength = 65f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ProjectToLife, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "LifeChapter", - VariantsWithRelated = new[] { "loses a home", "is left homeless" }, - VariantsWithoutRelated = new[] { "loses a home", "is left homeless" }, - }, - ["just_made_friend"] = new HappinessCatalogEntry - { - Id = "just_made_friend", - EventStrength = 50f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.RelatedOptional, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.CanonicalMilestone, - RelationRole = HappinessRelationRole.BestFriend, - InterestCategory = "Social", - CanonicalMilestoneKey = "milestone_friend", - VariantsWithRelated = new[] { "befriends {related}", "makes a best friend of {related}" }, - VariantsWithoutRelated = new[] { "makes a best friend", "finds a best friend" }, - }, - ["just_injured"] = new HappinessCatalogEntry - { - Id = "just_injured", - EventStrength = 65f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ProjectToLife, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "LifeChapter", - VariantsWithRelated = new[] { "suffers a lasting injury", "is crippled by injury" }, - VariantsWithoutRelated = new[] { "suffers a lasting injury", "is crippled by injury" }, - }, - ["just_cursed"] = new HappinessCatalogEntry - { - Id = "just_cursed", - EventStrength = 45f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Continuation, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Emotion", - StatusOverlapId = "cursed", - VariantsWithRelated = new[] { "is cursed", "falls under a curse" }, - VariantsWithoutRelated = new[] { "is cursed", "falls under a curse" }, - }, - ["starving"] = new HappinessCatalogEntry - { - Id = "starving", - EventStrength = 30f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Continuation, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Daily", - StatusOverlapId = "starving", - VariantsWithRelated = new[] { "is starving", "goes hungry" }, - VariantsWithoutRelated = new[] { "is starving", "goes hungry" }, - }, - ["conquered_city"] = new HappinessCatalogEntry - { - Id = "conquered_city", - EventStrength = 75f, - Presentation = HappinessPresentationTier.Aggregate, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Civic", - VariantsWithRelated = new[] { "celebrates a conquered city", "rejoices at a city conquered" }, - VariantsWithoutRelated = new[] { "celebrates a conquered city", "rejoices at a city conquered" }, - }, - ["destroyed_city"] = new HappinessCatalogEntry - { - Id = "destroyed_city", - EventStrength = 35f, - Presentation = HappinessPresentationTier.Aggregate, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Civic", - VariantsWithRelated = new[] { "celebrates a destroyed city", "cheers a city's destruction" }, - VariantsWithoutRelated = new[] { "celebrates a destroyed city", "cheers a city's destruction" }, - }, - ["lost_crown"] = new HappinessCatalogEntry - { - Id = "lost_crown", - EventStrength = 65f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ProjectToLife, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "LifeChapter", - VariantsWithRelated = new[] { "loses the crown", "is stripped of the crown" }, - VariantsWithoutRelated = new[] { "loses the crown", "is stripped of the crown" }, - }, - ["razed_capital"] = new HappinessCatalogEntry - { - Id = "razed_capital", - EventStrength = 35f, - Presentation = HappinessPresentationTier.Aggregate, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Civic", - VariantsWithRelated = new[] { "despairs as the capital is razed", "mourns a razed capital" }, - VariantsWithoutRelated = new[] { "despairs as the capital is razed", "mourns a razed capital" }, - }, - ["lost_capital"] = new HappinessCatalogEntry - { - Id = "lost_capital", - EventStrength = 35f, - Presentation = HappinessPresentationTier.Aggregate, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Civic", - VariantsWithRelated = new[] { "despairs at the lost capital", "mourns the lost capital" }, - VariantsWithoutRelated = new[] { "despairs at the lost capital", "mourns the lost capital" }, - }, - ["razed_city"] = new HappinessCatalogEntry - { - Id = "razed_city", - EventStrength = 35f, - Presentation = HappinessPresentationTier.Aggregate, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Civic", - VariantsWithRelated = new[] { "despairs as a city is razed", "mourns a razed city" }, - VariantsWithoutRelated = new[] { "despairs as a city is razed", "mourns a razed city" }, - }, - ["lost_city"] = new HappinessCatalogEntry - { - Id = "lost_city", - EventStrength = 72f, - Presentation = HappinessPresentationTier.Aggregate, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "Civic", - VariantsWithRelated = new[] { "despairs at a lost city", "mourns a lost city" }, - VariantsWithoutRelated = new[] { "despairs at a lost city", "mourns a lost city" }, - }, - ["become_alpha"] = new HappinessCatalogEntry - { - Id = "become_alpha", - EventStrength = 65f, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ProjectToLife, - Overlap = HappinessOverlapPolicy.Independent, - RelationRole = HappinessRelationRole.None, - InterestCategory = "LifeChapter", - VariantsWithRelated = new[] { "becomes the family alpha", "is named family alpha" }, - VariantsWithoutRelated = new[] { "becomes the family alpha", "is named family alpha" }, - }, - }; - - public static IEnumerable AuthoredIds => Entries.Keys; - - public static bool HasAuthored(string effectId) { - return !string.IsNullOrEmpty(effectId) && Entries.ContainsKey(effectId.Trim()); - } + private static readonly Dictionary Entries = + new Dictionary(StringComparer.OrdinalIgnoreCase); - public static bool TryGet(string effectId, out HappinessCatalogEntry entry) - { - entry = null; - if (string.IsNullOrEmpty(effectId)) + private static int _appliedGeneration = -1; + + public static void InvalidateOverlays() { - return false; + Entries.Clear(); + _appliedGeneration = -1; } - return Entries.TryGetValue(effectId.Trim(), out entry); - } - - public static HappinessCatalogEntry GetOrFallback(string effectId) - { - if (TryGet(effectId, out HappinessCatalogEntry entry)) + private static void Ensure() { - return entry; + if (_appliedGeneration == EventCatalogConfig.Generation) + { + return; + } + + _appliedGeneration = EventCatalogConfig.Generation; + EventCatalogConfig.FillHappiness(Entries); } - string id = string.IsNullOrEmpty(effectId) ? "unknown" : effectId.Trim(); - return new HappinessCatalogEntry + public static IEnumerable AuthoredIds { - Id = id, - Presentation = HappinessPresentationTier.Signal, - Context = HappinessContextRequirement.None, - Life = HappinessLifePolicy.ActivityOnly, - Overlap = HappinessOverlapPolicy.Independent, - InterestCategory = "Unknown", - VariantsWithRelated = new[] { "feels a change in happiness" }, - VariantsWithoutRelated = new[] { "feels a change in happiness" }, - EventStrength = 40f, - IsFallback = true - }; + get + { + Ensure(); + return Entries.Keys; + } + } + + public static bool HasAuthored(string effectId) + { + Ensure(); + return !string.IsNullOrEmpty(effectId) && Entries.ContainsKey(effectId.Trim()); + } + + public static bool TryGet(string effectId, out HappinessCatalogEntry entry) + { + Ensure(); + entry = null; + if (string.IsNullOrEmpty(effectId)) + { + return false; + } + + return Entries.TryGetValue(effectId.Trim(), out entry); + } + + public static HappinessCatalogEntry GetOrFallback(string effectId) + { + if (TryGet(effectId, out HappinessCatalogEntry entry)) + { + return entry; + } + + string id = string.IsNullOrEmpty(effectId) ? "unknown" : effectId.Trim(); + return new HappinessCatalogEntry + { + Id = id, + EventStrength = 40f, + Presentation = HappinessPresentationTier.Ambient, + IsFallback = true, + VariantsWithoutRelated = new[] { "feels something (" + id + ")" } + }; + } } } -} diff --git a/IdleSpectator/Events/Catalogs/EventCatalog.Libraries.cs b/IdleSpectator/Events/Catalogs/EventCatalog.Libraries.cs index 8242671..c5782e1 100644 --- a/IdleSpectator/Events/Catalogs/EventCatalog.Libraries.cs +++ b/IdleSpectator/Events/Catalogs/EventCatalog.Libraries.cs @@ -3,428 +3,519 @@ using System.Collections.Generic; namespace IdleSpectator; -/// Live AssetManager library event dials (powers, genes, …). +/// +/// Live AssetManager library event dials (powers, genes, …). +/// Strengths/labels from event-catalog.json libraries; signal predicates stay in C#. +/// 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 Entries = - new Dictionary(StringComparer.OrdinalIgnoreCase); - - private static readonly HashSet SignalIds = new HashSet(StringComparer.OrdinalIgnoreCase) { - "summon_lightning", "summon_tornado", "cast_curse", "cast_fire", - "cast_blood_rain", "summon_meteor", "teleport" - }; + private static readonly Dictionary Entries = + new Dictionary(StringComparer.OrdinalIgnoreCase); - private static void Ensure() => - LiveLibraryInterest.EnsureSeeded( - Entries, - ActivityAssetCatalog.EnumerateLiveSpellIds, - "Spectacle", - "{a} casts {id}", - ambientStrength: 48f, - SignalIds, - signalStrength: 88f, - ambientCreatesInterest: true); + private static int _appliedGeneration = -1; - public static IEnumerable 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 signals = d.SignalIds.Count > 0 + ? d.SignalIds + : new HashSet(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 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); - } -} - -/// God powers library - mostly Ambient; Signal for disaster/spectacle ids. public static class Power -{ - private static readonly Dictionary Entries = - new Dictionary(StringComparer.OrdinalIgnoreCase); - - private static bool IsSpectaclePower(string id) { - if (string.IsNullOrEmpty(id)) + private static readonly Dictionary Entries = + new Dictionary(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 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 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); - } -} - -/// AI decisions - Signal for war/rebellion/kingdom-affecting; Ambient otherwise. - public static class Decision -{ - private static readonly Dictionary Entries = - new Dictionary(StringComparer.OrdinalIgnoreCase); - - private static bool IsKingdomDecision(string id) - { - if (string.IsNullOrEmpty(id)) - { - return false; - } - - string s = id.ToLowerInvariant(); - // Pure AI ticks - not story beats. - if (s.Contains("idle") - || s.Contains("walking") - || s.Contains("check") - || s.Contains("wait") - || s.Contains("sleep") - || s.Contains("random")) - { - 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("army") - || s.Contains("lover") - || s.Contains("plot") - || s.Contains("marry") - || s.Contains("baby") - || s.Contains("child") - || s.Contains("king") - || s.Contains("leader") - || s.Contains("clan") - || s.Contains("religion") - || s.Contains("culture"); - } - - /// True when a decision id should be allowed to own the idle camera. - public static bool IsCameraWorthy(string id) => IsKingdomDecision(id); - - private static void Ensure() => - LiveLibraryInterest.EnsureSeeded( - Entries, - ActivityAssetCatalog.EnumerateLiveDecisionIds, - "Politics", - "{a} decides {id}", - ambientStrength: 40f, - signalIds: null, - signalStrength: 86f, - signalPredicate: IsKingdomDecision, - ambientCreatesInterest: false); - - public static IEnumerable AuthoredIds - { - get - { - Ensure(); - return Entries.Keys; - } - } - - public static DiscreteEventEntry GetOrFallback(string id) - { - Ensure(); - return LiveLibraryInterest.Lookup(Entries, id, "Politics", "{a} decides {id}", 35f); - } -} - -/// Items library - Signal for legendary/wonder subset. public static class Item -{ - private static readonly Dictionary Entries = - new Dictionary(StringComparer.OrdinalIgnoreCase); - - private static bool IsLegendaryItem(string id) { - if (string.IsNullOrEmpty(id)) + private static readonly Dictionary Entries = + new Dictionary(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: true); + 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 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 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); - } -} - -/// Subspecies traits - Signal for dramatic; Ambient fill-capped. public static class SubspeciesTrait -{ - private static readonly Dictionary Entries = - new Dictionary(StringComparer.OrdinalIgnoreCase); - - private static bool IsDramatic(string id) { - if (string.IsNullOrEmpty(id)) + private static readonly Dictionary Entries = + new Dictionary(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, - ambientCreatesInterest: true); + 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 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 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); - } -} - -/// Genes - Ambient default. public static class Gene -{ - private static readonly Dictionary Entries = - new Dictionary(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: true); - - public static IEnumerable AuthoredIds { - get + private static readonly Dictionary Entries = + new Dictionary(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 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); - } -} - -/// Phenotypes - Ambient default. public static class Phenotype -{ - private static readonly Dictionary Entries = - new Dictionary(StringComparer.OrdinalIgnoreCase); - - private static void Ensure() => - LiveLibraryInterest.EnsureSeeded( - Entries, - ActivityAssetCatalog.EnumerateLivePhenotypeIds, - "Genetics", - "{a} shows {id}", - ambientStrength: 28f, - signalIds: null, - signalStrength: 60f, - ambientCreatesInterest: true); - - public static IEnumerable AuthoredIds { - get + private static readonly Dictionary Entries = + new Dictionary(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 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); - } -} - -/// World laws - Ambient / location-only style. public static class WorldLaw -{ - private static readonly Dictionary Entries = - new Dictionary(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: true); - - public static IEnumerable AuthoredIds { - get + private static readonly Dictionary Entries = + new Dictionary(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 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); - } -} - -/// Biomes - Ambient. public static class Biome -{ - private static readonly Dictionary Entries = - new Dictionary(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 AuthoredIds { - get + private static readonly Dictionary Entries = + new Dictionary(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 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); - } -} - } diff --git a/IdleSpectator/Events/Catalogs/EventCatalog.Plot.cs b/IdleSpectator/Events/Catalogs/EventCatalog.Plot.cs index ee50323..fdc24f4 100644 --- a/IdleSpectator/Events/Catalogs/EventCatalog.Plot.cs +++ b/IdleSpectator/Events/Catalogs/EventCatalog.Plot.cs @@ -3,83 +3,66 @@ using System.Collections.Generic; namespace IdleSpectator; -/// Authored interest overlay for every live plots_library asset. +/// Plot dials from event-catalog.json plots. public static partial class EventCatalog { public static class Plot -{ - private static readonly Dictionary Entries = - new Dictionary(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 Entries = + new Dictionary(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 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 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 + }; + } } } -} diff --git a/IdleSpectator/Events/Catalogs/EventCatalog.Relationship.cs b/IdleSpectator/Events/Catalogs/EventCatalog.Relationship.cs index 012fac1..58aaded 100644 --- a/IdleSpectator/Events/Catalogs/EventCatalog.Relationship.cs +++ b/IdleSpectator/Events/Catalogs/EventCatalog.Relationship.cs @@ -3,69 +3,66 @@ using System.Collections.Generic; namespace IdleSpectator; -/// Authored discrete relationship lifecycle events (not a live asset library). +/// Relationship dials from event-catalog.json relationship. public static partial class EventCatalog { public static class Relationship -{ - private static readonly Dictionary Entries = - new Dictionary(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"); - // Seeking / pack churn are still candidates; low strength lets the director bury them. - Add("find_lover", 45f, "Relationship", "{a} is seeking a lover"); - Add("become_alpha", 72f, "LifeChapter", "{a} becomes the family alpha"); - Add("family_group_new", 60f, "Relationship", "{a} forms a family pack"); - Add("family_group_join", 36f, "Relationship", "{a} joins a family pack"); - Add("family_group_leave", 36f, "Relationship", "{a} leaves a family pack"); - Add("baby_created", 70f, "Relationship", "{a} is created as a baby"); - } + private static readonly Dictionary Entries = + new Dictionary(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 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 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 + }; + } } } -} diff --git a/IdleSpectator/Events/Catalogs/EventCatalog.Status.cs b/IdleSpectator/Events/Catalogs/EventCatalog.Status.cs index 2ce2f86..2e1d9ac 100644 --- a/IdleSpectator/Events/Catalogs/EventCatalog.Status.cs +++ b/IdleSpectator/Events/Catalogs/EventCatalog.Status.cs @@ -13,142 +13,76 @@ public sealed class StatusInterestEntry public bool IsFallback; } -/// -/// Authored interest policy for every live status asset. Prose stays in ActivityStatusProse. -/// Default: story-readable statuses are Layer A. Only brief FX / cooldowns stay B-only. -/// +/// Status interest dials from event-catalog.json status. public static partial class EventCatalog { public static class Status -{ - private static readonly Dictionary Entries = - new Dictionary(StringComparer.OrdinalIgnoreCase); - - static Status() { - // Spectacles / crisis - 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, "StatusAmbient", true); - Add("cursed", 60f, "StatusTransformation", true); - Add("soul_harvested", 75f, "StatusTransformation", true); - Add("voices_in_my_head", 55f, "StatusAmbient", true); - Add("tantrum", 52f, "StatusTransformation", true); - Add("magnetized", 55f, "StatusAmbient", true); - Add("invincible", 50f, "StatusTransformation", true); - Add("powerup", 54f, "StatusAmbient", true); - Add("enchanted", 52f, "StatusTransformation", true); - Add("ash_fever", 50f, "StatusAmbient", true); - Add("starving", 52f, "StatusAmbient", true); - Add("surprised", 42f, "StatusAmbient", true); - Add("confused", 40f, "StatusAmbient", true); - Add("strange_urge", 45f, "StatusAmbient", true); - Add("inspired", 42f, "Emotion", true); - Add("motivated", 40f, "Emotion", true); - Add("fell_in_love", 58f, "Relationship", true); - Add("pregnant", 58f, "LifeChapter", true); - Add("pregnant_parthenogenesis", 58f, "LifeChapter", true); - Add("crying", 50f, "Grief", true, extendsGrief: true); + private static readonly Dictionary Entries = + new Dictionary(StringComparer.OrdinalIgnoreCase); - // Life / social readable beats (lower strength; still camera-eligible) - Add("festive_spirit", 36f, "StatusAmbient", true); - Add("laughing", 34f, "StatusAmbient", true); - Add("singing", 34f, "StatusAmbient", true); - Add("sleeping", 32f, "StatusAmbient", true); - Add("handsome_migrant", 44f, "StatusAmbient", true); - Add("being_suspicious", 40f, "StatusAmbient", true); - Add("budding", 48f, "LifeChapter", true); - Add("caffeinated", 36f, "StatusAmbient", true); - Add("had_bad_dream", 40f, "Emotion", true); - Add("had_good_dream", 38f, "Emotion", true); - Add("had_nightmare", 44f, "Emotion", true); - Add("swearing", 34f, "StatusAmbient", true); - Add("taking_roots", 46f, "LifeChapter", true); - Add("uprooting", 46f, "LifeChapter", true); + private static int _appliedGeneration = -1; - // Egg gain is becoming an egg (not a camera beat). Loss → EmitHatch. - Add("egg", 40f, "StatusTransformation", false); - - // Brief FX / combat cooldowns - ticker only (Layer B). - AddNoInterest("afterglow"); - AddNoInterest("cough"); - AddNoInterest("dash"); - AddNoInterest("dodge"); - AddNoInterest("flicked"); - 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"); - } - - 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 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 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 + }; + } } } -} diff --git a/IdleSpectator/Events/Catalogs/EventCatalog.Trait.cs b/IdleSpectator/Events/Catalogs/EventCatalog.Trait.cs index 9671393..58c4c3c 100644 --- a/IdleSpectator/Events/Catalogs/EventCatalog.Trait.cs +++ b/IdleSpectator/Events/Catalogs/EventCatalog.Trait.cs @@ -3,167 +3,66 @@ using System.Collections.Generic; namespace IdleSpectator; -/// Authored interest overlay for every live traits library asset. +/// Trait dials from event-catalog.json traits. public static partial class EventCatalog { public static class Trait -{ - private static readonly Dictionary Entries = - new Dictionary(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 Entries = + new Dictionary(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 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 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 + }; + } } } -} diff --git a/IdleSpectator/Events/Catalogs/EventCatalog.WarType.cs b/IdleSpectator/Events/Catalogs/EventCatalog.WarType.cs index 82995c8..15cc997 100644 --- a/IdleSpectator/Events/Catalogs/EventCatalog.WarType.cs +++ b/IdleSpectator/Events/Catalogs/EventCatalog.WarType.cs @@ -13,83 +13,90 @@ public sealed class WarTypeInterestEntry public bool IsFallback; } -/// Authored interest overlay for every live war_types_library asset. +/// War type dials from event-catalog.json warTypes. public static partial class EventCatalog { public static class WarType -{ - private static readonly Dictionary Entries = - new Dictionary(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 Entries = + new Dictionary(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 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 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 ?? ""); + } } } -} diff --git a/IdleSpectator/Events/Catalogs/EventCatalog.WorldLog.cs b/IdleSpectator/Events/Catalogs/EventCatalog.WorldLog.cs index b93c1af..49a9a01 100644 --- a/IdleSpectator/Events/Catalogs/EventCatalog.WorldLog.cs +++ b/IdleSpectator/Events/Catalogs/EventCatalog.WorldLog.cs @@ -39,155 +39,105 @@ public sealed class WorldLogEventEntry } } -/// -/// Authored policy for every live WorldLog asset. Game library is inventory; this is presentation/scoring only. -/// +/// WorldLog dials from event-catalog.json worldLog. public static partial class EventCatalog { public static class WorldLog -{ - private static readonly Dictionary Entries = - new Dictionary(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 Entries = + new Dictionary(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 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 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); + } } } -} diff --git a/IdleSpectator/Events/DiscreteEventEntry.cs b/IdleSpectator/Events/DiscreteEventEntry.cs index 2241056..486dc94 100644 --- a/IdleSpectator/Events/DiscreteEventEntry.cs +++ b/IdleSpectator/Events/DiscreteEventEntry.cs @@ -11,6 +11,11 @@ public sealed class DiscreteEventEntry public string Category = "Event"; public bool CreatesInterest = true; public string LabelTemplate = "{a}"; + /// + /// Optional infinitive clause for decision-style reasons + /// (e.g. change the kingdom's culture{a} decides to …). + /// + public string ActionPhrase = ""; public bool IsFallback; public string MakeLabel(Actor a, Actor b = null) diff --git a/IdleSpectator/Events/EventCatalog.cs b/IdleSpectator/Events/EventCatalog.cs index 438d142..c58f279 100644 --- a/IdleSpectator/Events/EventCatalog.cs +++ b/IdleSpectator/Events/EventCatalog.cs @@ -25,6 +25,30 @@ namespace IdleSpectator; /// public static partial class EventCatalog { + /// Drop cached catalog rows so the next Ensure reloads from event-catalog.json. + 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(); + } + /// /// Layer A gate for happiness: Signal may own the camera; Ambient/Aggregate are B-only. /// diff --git a/IdleSpectator/Events/EventReason.cs b/IdleSpectator/Events/EventReason.cs index 2857de3..f6f7442 100644 --- a/IdleSpectator/Events/EventReason.cs +++ b/IdleSpectator/Events/EventReason.cs @@ -263,7 +263,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": @@ -277,6 +277,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; + } + /// /// Apply a sentence template with {a}/{b}/{id}. Prefer typed helpers for new copy. /// diff --git a/IdleSpectator/HarnessScenarios.cs b/IdleSpectator/HarnessScenarios.cs index b845966..ce04112 100644 --- a/IdleSpectator/HarnessScenarios.cs +++ b/IdleSpectator/HarnessScenarios.cs @@ -1225,6 +1225,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"), diff --git a/IdleSpectator/InterestScoring.cs b/IdleSpectator/InterestScoring.cs index aaa4737..e496dc5 100644 --- a/IdleSpectator/InterestScoring.cs +++ b/IdleSpectator/InterestScoring.cs @@ -183,16 +183,16 @@ 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; @@ -200,6 +200,7 @@ public static class InterestScoring 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; diff --git a/IdleSpectator/Main.cs b/IdleSpectator/Main.cs index 25cf7de..31259b9 100644 --- a/IdleSpectator/Main.cs +++ b/IdleSpectator/Main.cs @@ -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); diff --git a/IdleSpectator/event-catalog.json b/IdleSpectator/event-catalog.json new file mode 100644 index 0000000..be6fc41 --- /dev/null +++ b/IdleSpectator/event-catalog.json @@ -0,0 +1,3652 @@ +{ + "modVersion": "0.25.0", + "title": "IdleSpectator event catalog", + "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.", + "decisions": [ + { + "id": "find_lover", + "action": "find a lover", + "strength": 90 + }, + { + "id": "try_to_steal_money", + "action": "try to steal money", + "strength": 78 + }, + { + "id": "banish_unruly_clan_members", + "action": "banish unruly clan members", + "strength": 82 + }, + { + "id": "kill_unruly_clan_members", + "action": "kill unruly clan members", + "strength": 88 + }, + { + "id": "try_new_plot", + "action": "try a new plot", + "strength": 86 + }, + { + "id": "king_change_kingdom_culture", + "action": "change the kingdom's culture", + "strength": 86 + }, + { + "id": "king_change_kingdom_language", + "action": "change the kingdom's language", + "strength": 86 + }, + { + "id": "king_change_kingdom_religion", + "action": "change the kingdom's religion", + "strength": 86 + }, + { + "id": "leader_change_city_culture", + "action": "change the city's culture", + "strength": 80 + }, + { + "id": "leader_change_city_language", + "action": "change the city's language", + "strength": 80 + }, + { + "id": "leader_change_city_religion", + "action": "change the city's religion", + "strength": 80 + }, + { + "id": "warrior_army_follow_leader", + "action": "follow their leader", + "strength": 70 + }, + { + "id": "warrior_train_with_dummy", + "action": "train with a dummy", + "strength": 55 + }, + { + "id": "warrior_try_join_army_group", + "action": "try to join an army", + "strength": 72 + } + ], + "species": [ + { + "id": "dragon", + "bonus": 16 + }, + { + "id": "demon", + "bonus": 12 + }, + { + "id": "angel", + "bonus": 12 + }, + { + "id": "cold_one", + "bonus": 10 + }, + { + "id": "alien", + "bonus": 10 + }, + { + "id": "ufo", + "bonus": 8 + }, + { + "id": "god_finger", + "bonus": 14 + } + ], + "character": [ + { + "id": "favorite", + "bonus": 22 + }, + { + "id": "king", + "bonus": 18 + }, + { + "id": "leader", + "bonus": 14 + } + ], + "happiness": [ + { + "id": "death_family_member", + "strength": 76, + "presentation": "Signal", + "context": "RequiresRelatedActor", + "life": "ProjectToLife", + "overlap": "Independent", + "relationRole": "DeceasedFamily", + "category": "Grief", + "variantsWithRelated": [ + "mourns the death of family member {related}", + "grieves after family member {related} dies" + ], + "variantsWithoutRelated": [ + "mourns a family member's death", + "grieves a family loss" + ] + }, + { + "id": "death_lover", + "strength": 88, + "presentation": "Signal", + "context": "RequiresRelatedActor", + "life": "ProjectToLife", + "overlap": "Independent", + "relationRole": "DeceasedLover", + "category": "Grief", + "variantsWithRelated": [ + "falls into despair after their lover {related}'s death", + "mourns their lover {related}" + ], + "variantsWithoutRelated": [ + "mourns a lost lover", + "grieves a lover's death" + ] + }, + { + "id": "death_child", + "strength": 95, + "presentation": "Signal", + "context": "RequiresRelatedActor", + "life": "ProjectToLife", + "overlap": "Independent", + "relationRole": "DeceasedChild", + "category": "Grief", + "variantsWithRelated": [ + "falls into despair after their child {related}'s death", + "mourns their child {related}" + ], + "variantsWithoutRelated": [ + "mourns a child's death", + "grieves the loss of a child" + ] + }, + { + "id": "death_best_friend", + "strength": 78, + "presentation": "Signal", + "context": "RequiresRelatedActor", + "life": "ProjectToLife", + "overlap": "Independent", + "relationRole": "DeceasedBestFriend", + "category": "Grief", + "variantsWithRelated": [ + "mourns their best friend {related}", + "grieves after best friend {related} dies" + ], + "variantsWithoutRelated": [ + "mourns a best friend's death", + "grieves a lost best friend" + ] + }, + { + "id": "got_robbed", + "strength": 50, + "presentation": "Signal", + "context": "RelatedOptional", + "life": "ActivityOnly", + "overlap": "Independent", + "relationRole": "Robber", + "category": "Social", + "variantsWithRelated": [ + "is robbed by {related}", + "loses belongings to {related}" + ], + "variantsWithoutRelated": [ + "is robbed", + "gets robbed" + ] + }, + { + "id": "got_poked", + "strength": 30, + "presentation": "Ambient", + "context": "None", + "life": "ActivityOnly", + "overlap": "Independent", + "relationRole": "None", + "category": "Emotion", + "variantsWithRelated": [ + "gets poked by the gods", + "is poked" + ], + "variantsWithoutRelated": [ + "gets poked by the gods", + "is poked" + ] + }, + { + "id": "lost_fight", + "strength": 55, + "presentation": "Signal", + "context": "None", + "life": "ActivityOnly", + "overlap": "Independent", + "relationRole": "None", + "category": "Emotion", + "variantsWithRelated": [ + "is forced to yield in a fight", + "gives up mid-fight" + ], + "variantsWithoutRelated": [ + "is forced to yield in a fight", + "gives up mid-fight" + ] + }, + { + "id": "got_caught", + "strength": 20, + "presentation": "Ambient", + "context": "None", + "life": "ActivityOnly", + "overlap": "Independent", + "relationRole": "None", + "category": "Emotion", + "variantsWithRelated": [ + "gets caught", + "is caught" + ], + "variantsWithoutRelated": [ + "gets caught", + "is caught" + ] + }, + { + "id": "paid_tax", + "strength": 30, + "presentation": "Ambient", + "context": "None", + "life": "ActivityOnly", + "overlap": "Independent", + "relationRole": "None", + "category": "Daily", + "variantsWithRelated": [ + "pays taxes", + "grumbles while paying taxes" + ], + "variantsWithoutRelated": [ + "pays taxes", + "grumbles while paying taxes" + ] + }, + { + "id": "just_ate", + "strength": 30, + "presentation": "Ambient", + "context": "None", + "life": "ActivityOnly", + "overlap": "Continuation", + "relationRole": "None", + "category": "Daily", + "variantsWithRelated": [ + "finishes a meal", + "eats and feels better" + ], + "variantsWithoutRelated": [ + "finishes a meal", + "eats and feels better" + ], + "statusOverlapId": "just_ate" + }, + { + "id": "just_received_gift", + "strength": 50, + "presentation": "Signal", + "context": "RelatedOptional", + "life": "ActivityOnly", + "overlap": "Independent", + "relationRole": "GiftPartner", + "category": "Social", + "variantsWithRelated": [ + "receives a gift from {related}", + "is given a gift by {related}" + ], + "variantsWithoutRelated": [ + "receives a gift", + "is given a gift" + ] + }, + { + "id": "just_gave_gift", + "strength": 50, + "presentation": "Signal", + "context": "RelatedOptional", + "life": "ActivityOnly", + "overlap": "Independent", + "relationRole": "GiftPartner", + "category": "Social", + "variantsWithRelated": [ + "gives {related} a gift", + "offers a gift to {related}" + ], + "variantsWithoutRelated": [ + "gives a gift", + "offers a gift" + ] + }, + { + "id": "just_pooped", + "strength": 30, + "presentation": "Ambient", + "context": "None", + "life": "ActivityOnly", + "overlap": "Independent", + "relationRole": "None", + "category": "Daily", + "variantsWithRelated": [ + "finds relief", + "finishes relieving themselves" + ], + "variantsWithoutRelated": [ + "finds relief", + "finishes relieving themselves" + ] + }, + { + "id": "just_slept", + "strength": 30, + "presentation": "Signal", + "context": "None", + "life": "ActivityOnly", + "overlap": "Independent", + "relationRole": "None", + "category": "Daily", + "variantsWithRelated": [ + "finishes sleeping", + "ends a rest" + ], + "variantsWithoutRelated": [ + "finishes sleeping", + "ends a rest" + ] + }, + { + "id": "had_bad_dream", + "strength": 30, + "presentation": "Signal", + "context": "None", + "life": "ActivityOnly", + "overlap": "Continuation", + "relationRole": "None", + "category": "Daily", + "variantsWithRelated": [ + "has a bad dream", + "is troubled by a bad dream" + ], + "variantsWithoutRelated": [ + "has a bad dream", + "is troubled by a bad dream" + ], + "statusOverlapId": "had_bad_dream" + }, + { + "id": "had_good_dream", + "strength": 30, + "presentation": "Signal", + "context": "None", + "life": "ActivityOnly", + "overlap": "Continuation", + "relationRole": "None", + "category": "Daily", + "variantsWithRelated": [ + "has a good dream", + "dreams pleasantly" + ], + "variantsWithoutRelated": [ + "has a good dream", + "dreams pleasantly" + ], + "statusOverlapId": "had_good_dream" + }, + { + "id": "had_nightmare", + "strength": 30, + "presentation": "Signal", + "context": "None", + "life": "ActivityOnly", + "overlap": "Continuation", + "relationRole": "None", + "category": "Daily", + "variantsWithRelated": [ + "has a nightmare", + "is haunted by a nightmare" + ], + "variantsWithoutRelated": [ + "has a nightmare", + "is haunted by a nightmare" + ], + "statusOverlapId": "had_nightmare" + }, + { + "id": "slept_outside", + "strength": 30, + "presentation": "Signal", + "context": "None", + "life": "ActivityOnly", + "overlap": "Independent", + "relationRole": "None", + "category": "Daily", + "variantsWithRelated": [ + "has to sleep without a house", + "settles down outdoors, unhappy" + ], + "variantsWithoutRelated": [ + "has to sleep without a house", + "settles down outdoors, unhappy" + ] + }, + { + "id": "just_kissed", + "strength": 50, + "presentation": "Signal", + "context": "RelatedOptional", + "life": "ActivityOnly", + "overlap": "Independent", + "relationRole": "Lover", + "category": "Social", + "variantsWithRelated": [ + "mates with {related}", + "shares intimacy with {related}" + ], + "variantsWithoutRelated": [ + "mates with their lover", + "shares intimacy with their lover" + ] + }, + { + "id": "just_killed", + "strength": 45, + "presentation": "Signal", + "context": "None", + "life": "ActivityOnly", + "overlap": "CanonicalMilestone", + "relationRole": "Victim", + "category": "Emotion", + "variantsWithRelated": [ + "kills {related} and feels bloodlust's rush", + "bloodlust flares after killing {related}" + ], + "variantsWithoutRelated": [ + "bloodlust flares after a kill", + "feels bloodlust's rush after a kill" + ], + "canonicalMilestoneKey": "milestone_kill" + }, + { + "id": "become_king", + "strength": 72, + "presentation": "Signal", + "context": "None", + "life": "ProjectToLife", + "overlap": "Independent", + "relationRole": "None", + "category": "LifeChapter", + "variantsWithRelated": [ + "takes the crown", + "becomes ruler" + ], + "variantsWithoutRelated": [ + "takes the crown", + "becomes ruler" + ] + }, + { + "id": "become_leader", + "strength": 65, + "presentation": "Signal", + "context": "None", + "life": "ProjectToLife", + "overlap": "Independent", + "relationRole": "None", + "category": "LifeChapter", + "variantsWithRelated": [ + "becomes a leader", + "rises to leadership" + ], + "variantsWithoutRelated": [ + "becomes a leader", + "rises to leadership" + ] + }, + { + "id": "just_won_war", + "strength": 35, + "presentation": "Aggregate", + "context": "None", + "life": "ActivityOnly", + "overlap": "Independent", + "relationRole": "None", + "category": "Civic", + "variantsWithRelated": [ + "rejoices at a won war", + "celebrates victory in war" + ], + "variantsWithoutRelated": [ + "rejoices at a won war", + "celebrates victory in war" + ] + }, + { + "id": "just_made_peace", + "strength": 35, + "presentation": "Aggregate", + "context": "None", + "life": "ActivityOnly", + "overlap": "Independent", + "relationRole": "None", + "category": "Civic", + "variantsWithRelated": [ + "feels relief as peace is made", + "welcomes peace" + ], + "variantsWithoutRelated": [ + "feels relief as peace is made", + "welcomes peace" + ] + }, + { + "id": "just_lost_war", + "strength": 35, + "presentation": "Aggregate", + "context": "None", + "life": "ActivityOnly", + "overlap": "Independent", + "relationRole": "None", + "category": "Civic", + "variantsWithRelated": [ + "despairs after a lost war", + "mourns a lost war" + ], + "variantsWithoutRelated": [ + "despairs after a lost war", + "mourns a lost war" + ] + }, + { + "id": "was_conquered", + "strength": 35, + "presentation": "Aggregate", + "context": "None", + "life": "ActivityOnly", + "overlap": "Independent", + "relationRole": "None", + "category": "Civic", + "variantsWithRelated": [ + "suffers under conquest", + "is crushed by conquest" + ], + "variantsWithoutRelated": [ + "suffers under conquest", + "is crushed by conquest" + ] + }, + { + "id": "kingdom_fell_apart", + "strength": 35, + "presentation": "Aggregate", + "context": "None", + "life": "ActivityOnly", + "overlap": "Independent", + "relationRole": "None", + "category": "Civic", + "variantsWithRelated": [ + "watches the kingdom fall apart", + "despairs as the kingdom fractures" + ], + "variantsWithoutRelated": [ + "watches the kingdom fall apart", + "despairs as the kingdom fractures" + ] + }, + { + "id": "just_started_war", + "strength": 80, + "presentation": "Aggregate", + "context": "None", + "life": "ActivityOnly", + "overlap": "Independent", + "relationRole": "None", + "category": "Civic", + "variantsWithRelated": [ + "is glad war has started", + "cheers a war their culture loves" + ], + "variantsWithoutRelated": [ + "is glad war has started", + "cheers a war their culture loves" + ] + }, + { + "id": "just_rebelled", + "strength": 35, + "presentation": "Aggregate", + "context": "None", + "life": "ActivityOnly", + "overlap": "Independent", + "relationRole": "None", + "category": "Civic", + "variantsWithRelated": [ + "is swept up as the city rebels", + "lives through a rebellion" + ], + "variantsWithoutRelated": [ + "is swept up as the city rebels", + "lives through a rebellion" + ] + }, + { + "id": "fallen_in_love", + "strength": 50, + "presentation": "Signal", + "context": "RelatedOptional", + "life": "ActivityOnly", + "overlap": "CanonicalMilestone", + "relationRole": "Lover", + "category": "Social", + "variantsWithRelated": [ + "falls in love with {related}", + "is smitten with {related}" + ], + "variantsWithoutRelated": [ + "falls in love", + "is smitten" + ], + "canonicalMilestoneKey": "milestone_lover", + "statusOverlapId": "fell_in_love" + }, + { + "id": "just_had_child", + "strength": 70, + "presentation": "Signal", + "context": "None", + "life": "ProjectToLife", + "overlap": "Independent", + "relationRole": "None", + "category": "LifeChapter", + "variantsWithRelated": [ + "has a child", + "feels the joy of parenthood" + ], + "variantsWithoutRelated": [ + "has a child", + "feels the joy of parenthood" + ] + }, + { + "id": "just_read_book", + "strength": 30, + "presentation": "Signal", + "context": "None", + "life": "ActivityOnly", + "overlap": "Independent", + "relationRole": "None", + "category": "Daily", + "variantsWithRelated": [ + "finishes reading a book", + "puts down a book" + ], + "variantsWithoutRelated": [ + "finishes reading a book", + "puts down a book" + ] + }, + { + "id": "just_played", + "strength": 30, + "presentation": "Signal", + "context": "None", + "life": "ActivityOnly", + "overlap": "Independent", + "relationRole": "None", + "category": "Daily", + "variantsWithRelated": [ + "finishes playing", + "has fun playing" + ], + "variantsWithoutRelated": [ + "finishes playing", + "has fun playing" + ] + }, + { + "id": "just_talked", + "strength": 30, + "presentation": "Signal", + "context": "RelatedOptional", + "life": "ActivityOnly", + "overlap": "Independent", + "relationRole": "ConversationPartner", + "category": "Social", + "variantsWithRelated": [ + "finishes talking with {related}", + "ends a conversation with {related}" + ], + "variantsWithoutRelated": [ + "finishes talking", + "ends a conversation" + ] + }, + { + "id": "just_laughed", + "strength": 30, + "presentation": "Signal", + "context": "None", + "life": "ActivityOnly", + "overlap": "Continuation", + "relationRole": "None", + "category": "Daily", + "variantsWithRelated": [ + "bursts out laughing", + "shares a laugh" + ], + "variantsWithoutRelated": [ + "bursts out laughing", + "shares a laugh" + ], + "statusOverlapId": "laughing" + }, + { + "id": "just_sang", + "strength": 30, + "presentation": "Signal", + "context": "None", + "life": "ActivityOnly", + "overlap": "Continuation", + "relationRole": "None", + "category": "Daily", + "variantsWithRelated": [ + "finishes singing", + "sings with feeling" + ], + "variantsWithoutRelated": [ + "finishes singing", + "sings with feeling" + ], + "statusOverlapId": "singing" + }, + { + "id": "just_swore", + "strength": 30, + "presentation": "Signal", + "context": "None", + "life": "ActivityOnly", + "overlap": "Continuation", + "relationRole": "None", + "category": "Daily", + "variantsWithRelated": [ + "lets out a curse", + "swears loudly" + ], + "variantsWithoutRelated": [ + "lets out a curse", + "swears loudly" + ], + "statusOverlapId": "swearing" + }, + { + "id": "just_cried", + "strength": 30, + "presentation": "Signal", + "context": "None", + "life": "ActivityOnly", + "overlap": "Continuation", + "relationRole": "None", + "category": "Daily", + "variantsWithRelated": [ + "finishes crying", + "cries it out" + ], + "variantsWithoutRelated": [ + "finishes crying", + "cries it out" + ], + "statusOverlapId": "crying" + }, + { + "id": "just_talked_gossip", + "strength": 30, + "presentation": "Signal", + "context": "RelatedOptional", + "life": "ActivityOnly", + "overlap": "Independent", + "relationRole": "ConversationPartner", + "category": "Social", + "variantsWithRelated": [ + "gossips with {related}", + "shares lover-gossip with {related}" + ], + "variantsWithoutRelated": [ + "gossips about lovers", + "shares lover-gossip" + ] + }, + { + "id": "just_surprised", + "strength": 30, + "presentation": "Signal", + "context": "None", + "life": "ActivityOnly", + "overlap": "Continuation", + "relationRole": "None", + "category": "Emotion", + "variantsWithRelated": [ + "is startled", + "jumps in surprise" + ], + "variantsWithoutRelated": [ + "is startled", + "jumps in surprise" + ], + "statusOverlapId": "surprised" + }, + { + "id": "just_born", + "strength": 48, + "presentation": "Signal", + "context": "None", + "life": "ProjectToLife", + "overlap": "Independent", + "relationRole": "None", + "category": "LifeChapter", + "variantsWithRelated": [ + "appears in the world", + "is brought into existence" + ], + "variantsWithoutRelated": [ + "appears in the world", + "is brought into existence" + ] + }, + { + "id": "just_magnetised", + "strength": 45, + "presentation": "Signal", + "context": "None", + "life": "ActivityOnly", + "overlap": "Continuation", + "relationRole": "None", + "category": "Emotion", + "variantsWithRelated": [ + "is magnetized", + "feels a magnetic pull" + ], + "variantsWithoutRelated": [ + "is magnetized", + "feels a magnetic pull" + ], + "statusOverlapId": "magnetized" + }, + { + "id": "just_forced_power", + "strength": 45, + "presentation": "Signal", + "context": "None", + "life": "ActivityOnly", + "overlap": "Independent", + "relationRole": "None", + "category": "Emotion", + "variantsWithRelated": [ + "is flung by a god power", + "is blasted aside by divine force" + ], + "variantsWithoutRelated": [ + "is flung by a god power", + "is blasted aside by divine force" + ] + }, + { + "id": "just_possessed", + "strength": 45, + "presentation": "Signal", + "context": "None", + "life": "ActivityOnly", + "overlap": "Continuation", + "relationRole": "None", + "category": "Emotion", + "variantsWithRelated": [ + "is possessed", + "falls under possession" + ], + "variantsWithoutRelated": [ + "is possessed", + "falls under possession" + ], + "statusOverlapId": "possessed" + }, + { + "id": "strange_urge", + "strength": 45, + "presentation": "Signal", + "context": "None", + "life": "ActivityOnly", + "overlap": "Continuation", + "relationRole": "None", + "category": "Emotion", + "variantsWithRelated": [ + "feels a strange urge", + "is gripped by a strange urge" + ], + "variantsWithoutRelated": [ + "feels a strange urge", + "is gripped by a strange urge" + ], + "statusOverlapId": "strange_urge" + }, + { + "id": "just_had_tantrum", + "strength": 45, + "presentation": "Signal", + "context": "None", + "life": "ActivityOnly", + "overlap": "Continuation", + "relationRole": "None", + "category": "Emotion", + "variantsWithRelated": [ + "finishes a tantrum", + "calms after a tantrum" + ], + "variantsWithoutRelated": [ + "finishes a tantrum", + "calms after a tantrum" + ], + "statusOverlapId": "tantrum" + }, + { + "id": "just_felt_the_divine", + "strength": 45, + "presentation": "Signal", + "context": "None", + "life": "ActivityOnly", + "overlap": "Independent", + "relationRole": "None", + "category": "Emotion", + "variantsWithRelated": [ + "feels the divine touch", + "is touched by the divine" + ], + "variantsWithoutRelated": [ + "feels the divine touch", + "is touched by the divine" + ] + }, + { + "id": "just_enchanted", + "strength": 45, + "presentation": "Signal", + "context": "None", + "life": "ActivityOnly", + "overlap": "Continuation", + "relationRole": "None", + "category": "Emotion", + "variantsWithRelated": [ + "is enchanted", + "feels an enchantment take hold" + ], + "variantsWithoutRelated": [ + "is enchanted", + "feels an enchantment take hold" + ], + "statusOverlapId": "enchanted" + }, + { + "id": "just_inspired", + "strength": 45, + "presentation": "Signal", + "context": "None", + "life": "ActivityOnly", + "overlap": "Continuation", + "relationRole": "None", + "category": "Emotion", + "variantsWithRelated": [ + "comes down from inspiration", + "lets inspiration settle into contentment" + ], + "variantsWithoutRelated": [ + "comes down from inspiration", + "lets inspiration settle into contentment" + ], + "statusOverlapId": "inspired" + }, + { + "id": "wrote_book", + "strength": 65, + "presentation": "Signal", + "context": "None", + "life": "ProjectToLife", + "overlap": "Independent", + "relationRole": "None", + "category": "LifeChapter", + "variantsWithRelated": [ + "writes a book", + "finishes writing a book" + ], + "variantsWithoutRelated": [ + "writes a book", + "finishes writing a book" + ] + }, + { + "id": "just_became_adult", + "strength": 65, + "presentation": "Signal", + "context": "None", + "life": "ProjectToLife", + "overlap": "Independent", + "relationRole": "None", + "category": "LifeChapter", + "variantsWithRelated": [ + "comes of age", + "becomes an adult" + ], + "variantsWithoutRelated": [ + "comes of age", + "becomes an adult" + ] + }, + { + "id": "just_got_out_of_egg", + "strength": 65, + "presentation": "Signal", + "context": "None", + "life": "ProjectToLife", + "overlap": "Independent", + "relationRole": "None", + "category": "LifeChapter", + "variantsWithRelated": [ + "hatches from an egg", + "breaks free of an egg" + ], + "variantsWithoutRelated": [ + "hatches from an egg", + "breaks free of an egg" + ] + }, + { + "id": "just_finished_plot", + "strength": 65, + "presentation": "Signal", + "context": "None", + "life": "ProjectToLife", + "overlap": "Independent", + "relationRole": "None", + "category": "LifeChapter", + "variantsWithRelated": [ + "completes a plot", + "finishes a long plot" + ], + "variantsWithoutRelated": [ + "completes a plot", + "finishes a long plot" + ] + }, + { + "id": "just_found_house", + "strength": 55, + "presentation": "Signal", + "context": "None", + "life": "ProjectToLife", + "overlap": "Independent", + "relationRole": "None", + "category": "LifeChapter", + "variantsWithRelated": [ + "finds a home", + "settles into a house" + ], + "variantsWithoutRelated": [ + "finds a home", + "settles into a house" + ] + }, + { + "id": "just_lost_house", + "strength": 65, + "presentation": "Signal", + "context": "None", + "life": "ProjectToLife", + "overlap": "Independent", + "relationRole": "None", + "category": "LifeChapter", + "variantsWithRelated": [ + "loses a home", + "is left homeless" + ], + "variantsWithoutRelated": [ + "loses a home", + "is left homeless" + ] + }, + { + "id": "just_made_friend", + "strength": 50, + "presentation": "Signal", + "context": "RelatedOptional", + "life": "ActivityOnly", + "overlap": "CanonicalMilestone", + "relationRole": "BestFriend", + "category": "Social", + "variantsWithRelated": [ + "befriends {related}", + "makes a best friend of {related}" + ], + "variantsWithoutRelated": [ + "makes a best friend", + "finds a best friend" + ], + "canonicalMilestoneKey": "milestone_friend" + }, + { + "id": "just_injured", + "strength": 65, + "presentation": "Signal", + "context": "None", + "life": "ProjectToLife", + "overlap": "Independent", + "relationRole": "None", + "category": "LifeChapter", + "variantsWithRelated": [ + "suffers a lasting injury", + "is crippled by injury" + ], + "variantsWithoutRelated": [ + "suffers a lasting injury", + "is crippled by injury" + ] + }, + { + "id": "just_cursed", + "strength": 45, + "presentation": "Signal", + "context": "None", + "life": "ActivityOnly", + "overlap": "Continuation", + "relationRole": "None", + "category": "Emotion", + "variantsWithRelated": [ + "is cursed", + "falls under a curse" + ], + "variantsWithoutRelated": [ + "is cursed", + "falls under a curse" + ], + "statusOverlapId": "cursed" + }, + { + "id": "starving", + "strength": 30, + "presentation": "Signal", + "context": "None", + "life": "ActivityOnly", + "overlap": "Continuation", + "relationRole": "None", + "category": "Daily", + "variantsWithRelated": [ + "is starving", + "goes hungry" + ], + "variantsWithoutRelated": [ + "is starving", + "goes hungry" + ], + "statusOverlapId": "starving" + }, + { + "id": "conquered_city", + "strength": 75, + "presentation": "Aggregate", + "context": "None", + "life": "ActivityOnly", + "overlap": "Independent", + "relationRole": "None", + "category": "Civic", + "variantsWithRelated": [ + "celebrates a conquered city", + "rejoices at a city conquered" + ], + "variantsWithoutRelated": [ + "celebrates a conquered city", + "rejoices at a city conquered" + ] + }, + { + "id": "destroyed_city", + "strength": 35, + "presentation": "Aggregate", + "context": "None", + "life": "ActivityOnly", + "overlap": "Independent", + "relationRole": "None", + "category": "Civic", + "variantsWithRelated": [ + "celebrates a destroyed city", + "cheers a city's destruction" + ], + "variantsWithoutRelated": [ + "celebrates a destroyed city", + "cheers a city's destruction" + ] + }, + { + "id": "lost_crown", + "strength": 65, + "presentation": "Signal", + "context": "None", + "life": "ProjectToLife", + "overlap": "Independent", + "relationRole": "None", + "category": "LifeChapter", + "variantsWithRelated": [ + "loses the crown", + "is stripped of the crown" + ], + "variantsWithoutRelated": [ + "loses the crown", + "is stripped of the crown" + ] + }, + { + "id": "razed_capital", + "strength": 35, + "presentation": "Aggregate", + "context": "None", + "life": "ActivityOnly", + "overlap": "Independent", + "relationRole": "None", + "category": "Civic", + "variantsWithRelated": [ + "despairs as the capital is razed", + "mourns a razed capital" + ], + "variantsWithoutRelated": [ + "despairs as the capital is razed", + "mourns a razed capital" + ] + }, + { + "id": "lost_capital", + "strength": 35, + "presentation": "Aggregate", + "context": "None", + "life": "ActivityOnly", + "overlap": "Independent", + "relationRole": "None", + "category": "Civic", + "variantsWithRelated": [ + "despairs at the lost capital", + "mourns the lost capital" + ], + "variantsWithoutRelated": [ + "despairs at the lost capital", + "mourns the lost capital" + ] + }, + { + "id": "razed_city", + "strength": 35, + "presentation": "Aggregate", + "context": "None", + "life": "ActivityOnly", + "overlap": "Independent", + "relationRole": "None", + "category": "Civic", + "variantsWithRelated": [ + "despairs as a city is razed", + "mourns a razed city" + ], + "variantsWithoutRelated": [ + "despairs as a city is razed", + "mourns a razed city" + ] + }, + { + "id": "lost_city", + "strength": 72, + "presentation": "Aggregate", + "context": "None", + "life": "ActivityOnly", + "overlap": "Independent", + "relationRole": "None", + "category": "Civic", + "variantsWithRelated": [ + "despairs at a lost city", + "mourns a lost city" + ], + "variantsWithoutRelated": [ + "despairs at a lost city", + "mourns a lost city" + ] + }, + { + "id": "become_alpha", + "strength": 65, + "presentation": "Signal", + "context": "None", + "life": "ProjectToLife", + "overlap": "Independent", + "relationRole": "None", + "category": "LifeChapter", + "variantsWithRelated": [ + "becomes the family alpha", + "is named family alpha" + ], + "variantsWithoutRelated": [ + "becomes the family alpha", + "is named family alpha" + ] + } + ], + "status": [ + { + "id": "afterglow", + "strength": 20, + "category": "StatusAmbient", + "camera": false, + "extendsGrief": false + }, + { + "id": "cough", + "strength": 20, + "category": "StatusAmbient", + "camera": false, + "extendsGrief": false + }, + { + "id": "dash", + "strength": 20, + "category": "StatusAmbient", + "camera": false, + "extendsGrief": false + }, + { + "id": "dodge", + "strength": 20, + "category": "StatusAmbient", + "camera": false, + "extendsGrief": false + }, + { + "id": "flicked", + "strength": 20, + "category": "StatusAmbient", + "camera": false, + "extendsGrief": false + }, + { + "id": "just_ate", + "strength": 20, + "category": "StatusAmbient", + "camera": false, + "extendsGrief": false + }, + { + "id": "on_guard", + "strength": 20, + "category": "StatusAmbient", + "camera": false, + "extendsGrief": false + }, + { + "id": "recovery_combat_action", + "strength": 20, + "category": "StatusAmbient", + "camera": false, + "extendsGrief": false + }, + { + "id": "recovery_plot", + "strength": 20, + "category": "StatusAmbient", + "camera": false, + "extendsGrief": false + }, + { + "id": "recovery_social", + "strength": 20, + "category": "StatusAmbient", + "camera": false, + "extendsGrief": false + }, + { + "id": "recovery_spell", + "strength": 20, + "category": "StatusAmbient", + "camera": false, + "extendsGrief": false + }, + { + "id": "shield", + "strength": 20, + "category": "StatusAmbient", + "camera": false, + "extendsGrief": false + }, + { + "id": "slowness", + "strength": 20, + "category": "StatusAmbient", + "camera": false, + "extendsGrief": false + }, + { + "id": "spell_boost", + "strength": 20, + "category": "StatusAmbient", + "camera": false, + "extendsGrief": false + }, + { + "id": "spell_silence", + "strength": 20, + "category": "StatusAmbient", + "camera": false, + "extendsGrief": false + }, + { + "id": "burning", + "strength": 70, + "category": "StatusTransformation", + "camera": true, + "extendsGrief": false + }, + { + "id": "possessed", + "strength": 72, + "category": "StatusTransformation", + "camera": true, + "extendsGrief": false + }, + { + "id": "possessed_follower", + "strength": 60, + "category": "StatusTransformation", + "camera": true, + "extendsGrief": false + }, + { + "id": "frozen", + "strength": 65, + "category": "StatusTransformation", + "camera": true, + "extendsGrief": false + }, + { + "id": "poisoned", + "strength": 58, + "category": "StatusTransformation", + "camera": true, + "extendsGrief": false + }, + { + "id": "drowning", + "strength": 50, + "category": "StatusTransformation", + "camera": true, + "extendsGrief": false + }, + { + "id": "stunned", + "strength": 50, + "category": "StatusTransformation", + "camera": true, + "extendsGrief": false + }, + { + "id": "rage", + "strength": 62, + "category": "StatusTransformation", + "camera": true, + "extendsGrief": false + }, + { + "id": "angry", + "strength": 48, + "category": "StatusAmbient", + "camera": true, + "extendsGrief": false + }, + { + "id": "cursed", + "strength": 60, + "category": "StatusTransformation", + "camera": true, + "extendsGrief": false + }, + { + "id": "soul_harvested", + "strength": 75, + "category": "StatusTransformation", + "camera": true, + "extendsGrief": false + }, + { + "id": "voices_in_my_head", + "strength": 55, + "category": "StatusAmbient", + "camera": true, + "extendsGrief": false + }, + { + "id": "tantrum", + "strength": 52, + "category": "StatusTransformation", + "camera": true, + "extendsGrief": false + }, + { + "id": "magnetized", + "strength": 55, + "category": "StatusAmbient", + "camera": true, + "extendsGrief": false + }, + { + "id": "invincible", + "strength": 50, + "category": "StatusTransformation", + "camera": true, + "extendsGrief": false + }, + { + "id": "powerup", + "strength": 54, + "category": "StatusAmbient", + "camera": true, + "extendsGrief": false + }, + { + "id": "enchanted", + "strength": 52, + "category": "StatusTransformation", + "camera": true, + "extendsGrief": false + }, + { + "id": "ash_fever", + "strength": 50, + "category": "StatusAmbient", + "camera": true, + "extendsGrief": false + }, + { + "id": "starving", + "strength": 52, + "category": "StatusAmbient", + "camera": true, + "extendsGrief": false + }, + { + "id": "surprised", + "strength": 42, + "category": "StatusAmbient", + "camera": true, + "extendsGrief": false + }, + { + "id": "confused", + "strength": 40, + "category": "StatusAmbient", + "camera": true, + "extendsGrief": false + }, + { + "id": "strange_urge", + "strength": 45, + "category": "StatusAmbient", + "camera": true, + "extendsGrief": false + }, + { + "id": "inspired", + "strength": 42, + "category": "Emotion", + "camera": true, + "extendsGrief": false + }, + { + "id": "motivated", + "strength": 40, + "category": "Emotion", + "camera": true, + "extendsGrief": false + }, + { + "id": "fell_in_love", + "strength": 58, + "category": "Relationship", + "camera": true, + "extendsGrief": false + }, + { + "id": "pregnant", + "strength": 58, + "category": "LifeChapter", + "camera": true, + "extendsGrief": false + }, + { + "id": "pregnant_parthenogenesis", + "strength": 58, + "category": "LifeChapter", + "camera": true, + "extendsGrief": false + }, + { + "id": "crying", + "strength": 50, + "category": "Grief", + "camera": true, + "extendsGrief": true + }, + { + "id": "festive_spirit", + "strength": 36, + "category": "StatusAmbient", + "camera": true, + "extendsGrief": false + }, + { + "id": "laughing", + "strength": 34, + "category": "StatusAmbient", + "camera": true, + "extendsGrief": false + }, + { + "id": "singing", + "strength": 34, + "category": "StatusAmbient", + "camera": true, + "extendsGrief": false + }, + { + "id": "sleeping", + "strength": 32, + "category": "StatusAmbient", + "camera": true, + "extendsGrief": false + }, + { + "id": "handsome_migrant", + "strength": 44, + "category": "StatusAmbient", + "camera": true, + "extendsGrief": false + }, + { + "id": "being_suspicious", + "strength": 40, + "category": "StatusAmbient", + "camera": true, + "extendsGrief": false + }, + { + "id": "budding", + "strength": 48, + "category": "LifeChapter", + "camera": true, + "extendsGrief": false + }, + { + "id": "caffeinated", + "strength": 36, + "category": "StatusAmbient", + "camera": true, + "extendsGrief": false + }, + { + "id": "had_bad_dream", + "strength": 40, + "category": "Emotion", + "camera": true, + "extendsGrief": false + }, + { + "id": "had_good_dream", + "strength": 38, + "category": "Emotion", + "camera": true, + "extendsGrief": false + }, + { + "id": "had_nightmare", + "strength": 44, + "category": "Emotion", + "camera": true, + "extendsGrief": false + }, + { + "id": "swearing", + "strength": 34, + "category": "StatusAmbient", + "camera": true, + "extendsGrief": false + }, + { + "id": "taking_roots", + "strength": 46, + "category": "LifeChapter", + "camera": true, + "extendsGrief": false + }, + { + "id": "uprooting", + "strength": 46, + "category": "LifeChapter", + "camera": true, + "extendsGrief": false + }, + { + "id": "egg", + "strength": 40, + "category": "StatusTransformation", + "camera": false, + "extendsGrief": false + } + ], + "worldLog": [ + { + "id": "king_new", + "strength": 78, + "category": "Politics", + "camera": true, + "label": "New king: {b} ({a})", + "minWatch": 6, + "maxWatch": 28, + "chronicle": true + }, + { + "id": "king_left", + "strength": 65, + "category": "Politics", + "camera": true, + "label": "King left: {b} ({a})", + "minWatch": 6, + "maxWatch": 28, + "chronicle": true + }, + { + "id": "king_fled_capital", + "strength": 70, + "category": "Politics", + "camera": true, + "label": "King fled capital: {b}", + "minWatch": 6, + "maxWatch": 28, + "chronicle": true + }, + { + "id": "king_fled_city", + "strength": 68, + "category": "Politics", + "camera": true, + "label": "King fled city: {b}", + "minWatch": 6, + "maxWatch": 28, + "chronicle": true + }, + { + "id": "king_dead", + "strength": 80, + "category": "Politics", + "camera": true, + "label": "King died: {b} ({a})", + "minWatch": 6, + "maxWatch": 28, + "chronicle": true + }, + { + "id": "king_killed", + "strength": 85, + "category": "Politics", + "camera": true, + "label": "King killed: {b} ({a})", + "minWatch": 6, + "maxWatch": 28, + "chronicle": true + }, + { + "id": "favorite_dead", + "strength": 82, + "category": "Politics", + "camera": true, + "label": "Favorite fell: {a}", + "minWatch": 6, + "maxWatch": 28, + "chronicle": true + }, + { + "id": "favorite_killed", + "strength": 85, + "category": "Politics", + "camera": true, + "label": "Favorite fell: {a}", + "minWatch": 6, + "maxWatch": 28, + "chronicle": true + }, + { + "id": "city_new", + "strength": 72, + "category": "Settlement", + "camera": true, + "label": "New city: {a}", + "minWatch": 6, + "maxWatch": 28, + "chronicle": true + }, + { + "id": "log_city_revolted", + "strength": 90, + "category": "Politics", + "camera": true, + "label": "Revolt: {a}", + "minWatch": 8, + "maxWatch": 35, + "chronicle": true + }, + { + "id": "city_destroyed", + "strength": 92, + "category": "Settlement", + "camera": true, + "label": "City destroyed: {a}", + "minWatch": 8, + "maxWatch": 35, + "chronicle": true + }, + { + "id": "diplomacy_war_ended", + "strength": 72, + "category": "Politics", + "camera": true, + "label": "War ended: {a}", + "minWatch": 6, + "maxWatch": 28, + "chronicle": true + }, + { + "id": "diplomacy_war_started", + "strength": 100, + "category": "Politics", + "camera": true, + "label": "War: {a} vs {b}", + "minWatch": 8, + "maxWatch": 35, + "chronicle": true + }, + { + "id": "total_war_started", + "strength": 100, + "category": "Politics", + "camera": true, + "label": "Total war: {a}", + "minWatch": 8, + "maxWatch": 35, + "chronicle": true + }, + { + "id": "alliance_new", + "strength": 70, + "category": "Politics", + "camera": true, + "label": "Alliance: {a}", + "minWatch": 6, + "maxWatch": 28, + "chronicle": true + }, + { + "id": "alliance_dissolved", + "strength": 68, + "category": "Politics", + "camera": true, + "label": "Alliance dissolved: {a}", + "minWatch": 6, + "maxWatch": 28, + "chronicle": true + }, + { + "id": "kingdom_new", + "strength": 75, + "category": "Settlement", + "camera": true, + "label": "New kingdom: {a}", + "minWatch": 6, + "maxWatch": 28, + "chronicle": true + }, + { + "id": "kingdom_destroyed", + "strength": 100, + "category": "Politics", + "camera": true, + "label": "Kingdom fell: {a}", + "minWatch": 8, + "maxWatch": 35, + "chronicle": true + }, + { + "id": "kingdom_shattered", + "strength": 98, + "category": "Politics", + "camera": true, + "label": "Kingdom shattered: {a}", + "minWatch": 8, + "maxWatch": 35, + "chronicle": true + }, + { + "id": "kingdom_fractured", + "strength": 95, + "category": "Politics", + "camera": true, + "label": "Kingdom fractured: {a}", + "minWatch": 8, + "maxWatch": 35, + "chronicle": true + }, + { + "id": "kingdom_royal_clan_new", + "strength": 74, + "category": "Politics", + "camera": true, + "label": "Royal clan: {a}", + "minWatch": 6, + "maxWatch": 28, + "chronicle": true + }, + { + "id": "kingdom_royal_clan_changed", + "strength": 72, + "category": "Politics", + "camera": true, + "label": "Royal clan: {a}", + "minWatch": 6, + "maxWatch": 28, + "chronicle": true + }, + { + "id": "kingdom_royal_clan_dead", + "strength": 76, + "category": "Politics", + "camera": true, + "label": "Royal clan ended: {a}", + "minWatch": 6, + "maxWatch": 28, + "chronicle": true + }, + { + "id": "disaster_tornado", + "strength": 95, + "category": "Disaster", + "camera": true, + "label": "Tornado: {a}", + "minWatch": 8, + "maxWatch": 35, + "chronicle": true + }, + { + "id": "disaster_meteorite", + "strength": 95, + "category": "Disaster", + "camera": true, + "label": "Meteorite: {a}", + "minWatch": 8, + "maxWatch": 35, + "chronicle": true + }, + { + "id": "disaster_hellspawn", + "strength": 96, + "category": "Disaster", + "camera": true, + "label": "Hellspawn: {a}", + "minWatch": 8, + "maxWatch": 35, + "chronicle": true + }, + { + "id": "disaster_earthquake", + "strength": 95, + "category": "Disaster", + "camera": true, + "label": "Earthquake: {a}", + "minWatch": 8, + "maxWatch": 35, + "chronicle": true + }, + { + "id": "disaster_greg_abominations", + "strength": 96, + "category": "Disaster", + "camera": true, + "label": "Greg abominations: {a}", + "minWatch": 8, + "maxWatch": 35, + "chronicle": true + }, + { + "id": "disaster_ice_ones", + "strength": 95, + "category": "Disaster", + "camera": true, + "label": "Ice ones awaken: {a}", + "minWatch": 8, + "maxWatch": 35, + "chronicle": true + }, + { + "id": "disaster_sudden_snowman", + "strength": 90, + "category": "Disaster", + "camera": true, + "label": "Sudden snowman: {a}", + "minWatch": 8, + "maxWatch": 35, + "chronicle": true + }, + { + "id": "disaster_garden_surprise", + "strength": 90, + "category": "Disaster", + "camera": true, + "label": "Garden surprise: {a}", + "minWatch": 8, + "maxWatch": 35, + "chronicle": true + }, + { + "id": "disaster_dragon_from_farlands", + "strength": 98, + "category": "Disaster", + "camera": true, + "label": "Dragon from the farlands: {a}", + "minWatch": 8, + "maxWatch": 35, + "chronicle": true + }, + { + "id": "disaster_bandits", + "strength": 92, + "category": "Disaster", + "camera": true, + "label": "Bandit raid: {a}", + "minWatch": 8, + "maxWatch": 35, + "chronicle": true + }, + { + "id": "disaster_alien_invasion", + "strength": 97, + "category": "Disaster", + "camera": true, + "label": "Alien invasion: {a}", + "minWatch": 8, + "maxWatch": 35, + "chronicle": true + }, + { + "id": "disaster_biomass", + "strength": 96, + "category": "Disaster", + "camera": true, + "label": "Biomass outbreak: {a}", + "minWatch": 8, + "maxWatch": 35, + "chronicle": true + }, + { + "id": "disaster_tumor", + "strength": 96, + "category": "Disaster", + "camera": true, + "label": "Tumor outbreak: {a}", + "minWatch": 8, + "maxWatch": 35, + "chronicle": true + }, + { + "id": "disaster_heatwave", + "strength": 88, + "category": "Disaster", + "camera": true, + "label": "Heatwave: {a}", + "minWatch": 8, + "maxWatch": 35, + "chronicle": true + }, + { + "id": "disaster_evil_mage", + "strength": 94, + "category": "Disaster", + "camera": true, + "label": "Evil mage: {a}", + "minWatch": 8, + "maxWatch": 35, + "chronicle": true + }, + { + "id": "disaster_underground_necromancer", + "strength": 95, + "category": "Disaster", + "camera": true, + "label": "Underground necromancer: {a}", + "minWatch": 8, + "maxWatch": 35, + "chronicle": true + }, + { + "id": "disaster_mad_thoughts", + "strength": 90, + "category": "Disaster", + "camera": true, + "label": "Mad thoughts: {a}", + "minWatch": 8, + "maxWatch": 35, + "chronicle": true + }, + { + "id": "auto_tester", + "strength": 10, + "category": "Harness", + "camera": false, + "label": "Auto tester: {a}", + "minWatch": 2, + "maxWatch": 6, + "chronicle": false + } + ], + "plots": [ + { + "id": "rebellion", + "strength": 92, + "category": "Politics", + "camera": true, + "label": "{a} is plotting a rebellion" + }, + { + "id": "new_war", + "strength": 94, + "category": "Politics", + "camera": true, + "label": "{a} is plotting a war" + }, + { + "id": "alliance_create", + "strength": 80, + "category": "Politics", + "camera": true, + "label": "{a} is plotting an alliance" + }, + { + "id": "alliance_join", + "strength": 72, + "category": "Politics", + "camera": true, + "label": "{a} is joining an alliance" + }, + { + "id": "alliance_destroy", + "strength": 86, + "category": "Politics", + "camera": true, + "label": "{a} is breaking an alliance" + }, + { + "id": "attacker_stop_war", + "strength": 78, + "category": "Politics", + "camera": true, + "label": "{a} is ending a war" + }, + { + "id": "new_book", + "strength": 55, + "category": "Culture", + "camera": true, + "label": "{a} is writing a book" + }, + { + "id": "new_language", + "strength": 62, + "category": "Culture", + "camera": true, + "label": "{a} is forging a language" + }, + { + "id": "new_religion", + "strength": 70, + "category": "Culture", + "camera": true, + "label": "{a} is founding a religion" + }, + { + "id": "new_culture", + "strength": 68, + "category": "Culture", + "camera": true, + "label": "{a} is founding a culture" + }, + { + "id": "clan_ascension", + "strength": 82, + "category": "Politics", + "camera": true, + "label": "{a} is ascending a clan" + }, + { + "id": "culture_divide", + "strength": 75, + "category": "Culture", + "camera": true, + "label": "{a} splits a culture" + }, + { + "id": "religion_schism", + "strength": 84, + "category": "Culture", + "camera": true, + "label": "{a} causes a religion schism" + }, + { + "id": "language_divergence", + "strength": 60, + "category": "Culture", + "camera": true, + "label": "{a} splits a language" + }, + { + "id": "summon_meteor_rain", + "strength": 96, + "category": "Spectacle", + "camera": true, + "label": "{a} summons a meteor rain" + }, + { + "id": "summon_earthquake", + "strength": 95, + "category": "Spectacle", + "camera": true, + "label": "{a} summons an earthquake" + }, + { + "id": "summon_thunderstorm", + "strength": 93, + "category": "Spectacle", + "camera": true, + "label": "{a} summons a thunderstorm" + }, + { + "id": "summon_stormfront", + "strength": 93, + "category": "Spectacle", + "camera": true, + "label": "{a} summons a stormfront" + }, + { + "id": "summon_hellstorm", + "strength": 97, + "category": "Spectacle", + "camera": true, + "label": "{a} summons a hellstorm" + }, + { + "id": "summon_demons", + "strength": 96, + "category": "Spectacle", + "camera": true, + "label": "{a} summons demons" + }, + { + "id": "summon_angles", + "strength": 96, + "category": "Spectacle", + "camera": true, + "label": "{a} summons angels" + }, + { + "id": "summon_skeletons", + "strength": 92, + "category": "Spectacle", + "camera": true, + "label": "{a} summons skeletons" + }, + { + "id": "summon_living_plants", + "strength": 90, + "category": "Spectacle", + "camera": true, + "label": "{a} summons living plants" + }, + { + "id": "big_cast_coffee", + "strength": 58, + "category": "Spectacle", + "camera": true, + "label": "{a} performs a coffee ritual" + }, + { + "id": "big_cast_bubble_shield", + "strength": 64, + "category": "Spectacle", + "camera": true, + "label": "{a} casts a bubble shield" + }, + { + "id": "big_cast_madness", + "strength": 80, + "category": "Spectacle", + "camera": true, + "label": "{a} casts madness" + }, + { + "id": "big_cast_slowness", + "strength": 70, + "category": "Spectacle", + "camera": true, + "label": "{a} casts slowness" + }, + { + "id": "cause_rebellion", + "strength": 90, + "category": "Politics", + "camera": true, + "label": "{a} causes a rebellion" + } + ], + "relationship": [ + { + "id": "set_lover", + "strength": 72, + "category": "Relationship", + "camera": true, + "label": "{a} became lovers with {b}" + }, + { + "id": "clear_lover", + "strength": 55, + "category": "Relationship", + "camera": true, + "label": "{a} parted from a lover" + }, + { + "id": "add_child", + "strength": 68, + "category": "Relationship", + "camera": true, + "label": "{a} gains a child, {b}" + }, + { + "id": "new_family", + "strength": 70, + "category": "Relationship", + "camera": true, + "label": "{a} starts a new family" + }, + { + "id": "family_removed", + "strength": 50, + "category": "Relationship", + "camera": true, + "label": "{a}'s family ends" + }, + { + "id": "find_lover", + "strength": 45, + "category": "Relationship", + "camera": true, + "label": "{a} is seeking a lover" + }, + { + "id": "become_alpha", + "strength": 72, + "category": "LifeChapter", + "camera": true, + "label": "{a} becomes the family alpha" + }, + { + "id": "family_group_new", + "strength": 60, + "category": "Relationship", + "camera": true, + "label": "{a} forms a family pack" + }, + { + "id": "family_group_join", + "strength": 36, + "category": "Relationship", + "camera": true, + "label": "{a} joins a family pack" + }, + { + "id": "family_group_leave", + "strength": 36, + "category": "Relationship", + "camera": true, + "label": "{a} leaves a family pack" + }, + { + "id": "baby_created", + "strength": 70, + "category": "Relationship", + "camera": true, + "label": "{a} is created as a baby" + } + ], + "traits": [ + { + "id": "zombie", + "strength": 82, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "wise", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "whirlwind", + "strength": 82, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "weightless", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "weak", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "veteran", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "venomous", + "strength": 82, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "unlucky", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "ugly", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "tumor_infection", + "strength": 82, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "tough", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "titan_lungs", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "tiny", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "thorns", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "thief", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "super_health", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "sunblessed", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "stupid", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "strong_minded", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "strong", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "soft_skin", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "slow", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "skin_burns", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "short_sighted", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "shiny", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "scar_of_divinity", + "strength": 82, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "savage", + "strength": 82, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "regeneration", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "pyromaniac", + "strength": 82, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "psychopath", + "strength": 82, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "poisonous", + "strength": 82, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "poison_immune", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "plague", + "strength": 82, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "peaceful", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "paranoid", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "pacifist", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "nightchild", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "mute", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "mush_spores", + "strength": 82, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "moonchild", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "miracle_born", + "strength": 82, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "miracle_bearer", + "strength": 82, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "miner", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "metamorphed", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "mega_heartbeat", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "mageslayer", + "strength": 82, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "madness", + "strength": 82, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "lustful", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "lucky", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "long_liver", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "light_lamp", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "kingslayer", + "strength": 82, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "infertile", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "infected", + "strength": 82, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "immune", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "immortal", + "strength": 82, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "hotheaded", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "honest", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "heliophobia", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "heart_of_wizard", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "healing_aura", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "hard_skin", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "greedy", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "golden_tooth", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "gluttonous", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "giant", + "strength": 82, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "genius", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "freeze_proof", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "fragile_health", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "flower_prints", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "flesh_eater", + "strength": 82, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "fire_proof", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "fire_blood", + "strength": 82, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "fertile", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "fat", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "fast", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "eyepatch", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "evil", + "strength": 82, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "energized", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "eagle_eyed", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "dragonslayer", + "strength": 82, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "dodge", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "desire_harp", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "desire_golden_egg", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "desire_computer", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "desire_alien_mold", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "deflect_projectile", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "deceitful", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "death_nuke", + "strength": 82, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "death_mark", + "strength": 82, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "death_bomb", + "strength": 82, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "dash", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "crippled", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "content", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "contagious", + "strength": 82, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "cold_aura", + "strength": 82, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "clumsy", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "clone", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "chosen_one", + "strength": 82, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "burning_feet", + "strength": 82, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "bubble_defense", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "boosted_vitality", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "bomberman", + "strength": 82, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "boat", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "bloodlust", + "strength": 82, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "block", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "blessed", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "battle_reflexes", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "backstep", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "attractive", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "arcane_reflexes", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "ambitious", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "agile", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "acid_touch", + "strength": 82, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "acid_proof", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + }, + { + "id": "acid_blood", + "strength": 42, + "category": "Trait", + "camera": true, + "label": "Trait: {id} ({a})" + } + ], + "books": [ + { + "id": "family_story", + "strength": 52, + "category": "Culture", + "camera": true, + "label": "{a} writes a family story" + }, + { + "id": "love_story", + "strength": 58, + "category": "Culture", + "camera": true, + "label": "{a} writes a love story" + }, + { + "id": "friendship_story", + "strength": 50, + "category": "Culture", + "camera": true, + "label": "{a} writes a friendship story" + }, + { + "id": "bad_story_about_king", + "strength": 62, + "category": "Culture", + "camera": true, + "label": "{a} writes a scandalous book" + }, + { + "id": "fable", + "strength": 48, + "category": "Culture", + "camera": true, + "label": "{a} writes a fable" + }, + { + "id": "warfare_manual", + "strength": 60, + "category": "Culture", + "camera": true, + "label": "{a} writes a warfare manual" + }, + { + "id": "economy_manual", + "strength": 48, + "category": "Culture", + "camera": true, + "label": "{a} writes an economy manual" + }, + { + "id": "stewardship_manual", + "strength": 48, + "category": "Culture", + "camera": true, + "label": "{a} writes a stewardship manual" + }, + { + "id": "diplomacy_manual", + "strength": 55, + "category": "Culture", + "camera": true, + "label": "{a} writes a diplomacy manual" + }, + { + "id": "mathbook", + "strength": 45, + "category": "Culture", + "camera": true, + "label": "{a} writes a math book" + }, + { + "id": "biology_book", + "strength": 45, + "category": "Culture", + "camera": true, + "label": "{a} writes a biology book" + }, + { + "id": "history_book", + "strength": 55, + "category": "Culture", + "camera": true, + "label": "{a} writes a history book" + } + ], + "eras": [ + { + "id": "age_hope", + "strength": 78, + "category": "World", + "camera": true, + "label": "Age of Hope" + }, + { + "id": "age_sun", + "strength": 80, + "category": "World", + "camera": true, + "label": "Age of Sun" + }, + { + "id": "age_dark", + "strength": 88, + "category": "World", + "camera": true, + "label": "Age of Dark" + }, + { + "id": "age_tears", + "strength": 86, + "category": "World", + "camera": true, + "label": "Age of Tears" + }, + { + "id": "age_moon", + "strength": 82, + "category": "World", + "camera": true, + "label": "Age of Moon" + }, + { + "id": "age_chaos", + "strength": 92, + "category": "World", + "camera": true, + "label": "Age of Chaos" + }, + { + "id": "age_wonders", + "strength": 84, + "category": "World", + "camera": true, + "label": "Age of Wonders" + }, + { + "id": "age_ice", + "strength": 87, + "category": "World", + "camera": true, + "label": "Age of Ice" + }, + { + "id": "age_ash", + "strength": 90, + "category": "World", + "camera": true, + "label": "Age of Ash" + }, + { + "id": "age_despair", + "strength": 91, + "category": "World", + "camera": true, + "label": "Age of Despair" + }, + { + "id": "age_unknown", + "strength": 70, + "category": "World", + "camera": true, + "label": "Unknown age" + } + ], + "disasters": [ + { + "id": "tornado", + "strength": 95, + "category": "Disaster", + "camera": true, + "worldLogId": "disaster_tornado" + }, + { + "id": "heatwave", + "strength": 88, + "category": "Disaster", + "camera": true, + "worldLogId": "disaster_heatwave" + }, + { + "id": "small_meteorite", + "strength": 95, + "category": "Disaster", + "camera": true, + "worldLogId": "disaster_meteorite" + }, + { + "id": "small_earthquake", + "strength": 95, + "category": "Disaster", + "camera": true, + "worldLogId": "disaster_earthquake" + }, + { + "id": "hellspawn", + "strength": 96, + "category": "Disaster", + "camera": true, + "worldLogId": "disaster_hellspawn" + }, + { + "id": "ice_ones_awoken", + "strength": 95, + "category": "Disaster", + "camera": true, + "worldLogId": "disaster_ice_ones" + }, + { + "id": "sudden_snowman", + "strength": 90, + "category": "Disaster", + "camera": true, + "worldLogId": "disaster_sudden_snowman" + }, + { + "id": "garden_surprise", + "strength": 90, + "category": "Disaster", + "camera": true, + "worldLogId": "disaster_garden_surprise" + }, + { + "id": "dragon_from_farlands", + "strength": 98, + "category": "Disaster", + "camera": true, + "worldLogId": "disaster_dragon_from_farlands" + }, + { + "id": "ash_bandits", + "strength": 92, + "category": "Disaster", + "camera": true, + "worldLogId": "disaster_bandits" + }, + { + "id": "alien_invasion", + "strength": 97, + "category": "Disaster", + "camera": true, + "worldLogId": "disaster_alien_invasion" + }, + { + "id": "biomass", + "strength": 96, + "category": "Disaster", + "camera": true, + "worldLogId": "disaster_biomass" + }, + { + "id": "tumor", + "strength": 96, + "category": "Disaster", + "camera": true, + "worldLogId": "disaster_tumor" + }, + { + "id": "wild_mage", + "strength": 94, + "category": "Disaster", + "camera": true, + "worldLogId": "disaster_evil_mage" + }, + { + "id": "underground_necromancer", + "strength": 95, + "category": "Disaster", + "camera": true, + "worldLogId": "disaster_underground_necromancer" + }, + { + "id": "mad_thoughts", + "strength": 90, + "category": "Disaster", + "camera": true, + "worldLogId": "disaster_mad_thoughts" + }, + { + "id": "greg_abominations", + "strength": 96, + "category": "Disaster", + "camera": true, + "worldLogId": "disaster_greg_abominations" + } + ], + "warTypes": [ + { + "id": "normal", + "strength": 90, + "category": "Politics", + "camera": true, + "label": "War: {a}" + }, + { + "id": "spite", + "strength": 88, + "category": "Politics", + "camera": true, + "label": "Spite war: {a}" + }, + { + "id": "inspire", + "strength": 86, + "category": "Politics", + "camera": true, + "label": "Inspired war: {a}" + }, + { + "id": "rebellion", + "strength": 92, + "category": "Politics", + "camera": true, + "label": "Rebellion: {a}" + }, + { + "id": "whisper_of_war", + "strength": 84, + "category": "Politics", + "camera": true, + "label": "Whisper of war: {a}" + } + ], + "libraries": { + "spell": { + "ambientStrength": 48, + "signalStrength": 88, + "category": "Spectacle", + "label": "{a} casts {id}", + "ambientCreatesInterest": true, + "signalIds": [ + "summon_lightning", + "summon_tornado", + "cast_curse", + "cast_fire", + "cast_blood_rain", + "summon_meteor", + "teleport" + ] + }, + "power": { + "ambientStrength": 28, + "signalStrength": 92, + "category": "Spectacle", + "label": "God power: {id}", + "ambientCreatesInterest": false + }, + "item": { + "ambientStrength": 36, + "signalStrength": 80, + "category": "Item", + "label": "{a} forges {id}", + "ambientCreatesInterest": true + }, + "gene": { + "ambientStrength": 30, + "signalStrength": 70, + "category": "Genetics", + "label": "{a} gains gene {id}", + "ambientCreatesInterest": true + }, + "phenotype": { + "ambientStrength": 28, + "signalStrength": 60, + "category": "Genetics", + "label": "{a} shows {id}", + "ambientCreatesInterest": true + }, + "worldLaw": { + "ambientStrength": 40, + "signalStrength": 70, + "category": "WorldLaw", + "label": "Law changed: {id}", + "ambientCreatesInterest": true + }, + "subspeciesTrait": { + "ambientStrength": 34, + "signalStrength": 78, + "category": "Evolution", + "label": "{a} evolves {id}", + "ambientCreatesInterest": true + }, + "biome": { + "ambientStrength": 26, + "signalStrength": 50, + "category": "Biome", + "label": "Biome shifts: {id}", + "ambientCreatesInterest": false + } + } +} diff --git a/IdleSpectator/mod.json b/IdleSpectator/mod.json index 0d2d362..41b1e6f 100644 --- a/IdleSpectator/mod.json +++ b/IdleSpectator/mod.json @@ -1,7 +1,7 @@ { "name": "IdleSpectator", "author": "dazed", - "version": "0.24.0", - "description": "AFK Idle Spectator (I) + Lore (L). Interesting beats are candidates; director ranks.", + "version": "0.25.0", + "description": "AFK Idle Spectator (I) + Lore (L). Event strengths/prose in event-catalog.json.", "GUID": "com.dazed.idlespectator" } diff --git a/docs/event-reason.md b/docs/event-reason.md index f40c5f8..3c18688 100644 --- a/docs/event-reason.md +++ b/docs/event-reason.md @@ -38,7 +38,13 @@ Feeds call typed helpers (`Fight`, `BuildingEat`, `SeekingLover`, …) or `Apply 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. diff --git a/docs/scoring-model.md b/docs/scoring-model.md index fedbcc5..2b2375e 100644 --- a/docs/scoring-model.md +++ b/docs/scoring-model.md @@ -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. diff --git a/scripts/export-event-catalog-from-cs.py b/scripts/export-event-catalog-from-cs.py new file mode 100755 index 0000000..0c0d6c7 --- /dev/null +++ b/scripts/export-event-catalog-from-cs.py @@ -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())