the great migration
This commit is contained in:
parent
f4be23e23d
commit
cc0d44ae2e
26 changed files with 6781 additions and 2045 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -230,16 +230,58 @@ public static class DomainEventHarness
|
|||
|
||||
CatalogCoverageAudit.WriteReport(outputDirectory, "domain_event_audit.tsv", tsv.ToString());
|
||||
|
||||
// Camera decisions must have authored ActionPhrase + EventStrength (event-catalog.json).
|
||||
int decisionProseMissing = 0;
|
||||
int decisionStrengthMissing = 0;
|
||||
int decisionProseTotal = 0;
|
||||
List<string> liveDecisions = ActivityAssetCatalog.EnumerateLiveDecisionIds();
|
||||
for (int i = 0; i < liveDecisions.Count; i++)
|
||||
{
|
||||
string id = liveDecisions[i];
|
||||
if (!EventCatalog.Decision.IsCameraWorthy(id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
decisionProseTotal++;
|
||||
DiscreteEventEntry entry = EventCatalog.Decision.GetOrFallback(id);
|
||||
if (!EventCatalog.Decision.TryGetAuthoredActionPhrase(id, out _))
|
||||
{
|
||||
decisionProseMissing++;
|
||||
tsv.Append("decision_prose\t").Append(id).Append("\t0\t1\t\tmissing_action_phrase").AppendLine();
|
||||
}
|
||||
else if (entry.EventStrength <= 0f)
|
||||
{
|
||||
decisionStrengthMissing++;
|
||||
tsv.Append("decision_prose\t").Append(id).Append("\t0\t1\t")
|
||||
.Append(entry.EventStrength.ToString("0.#"))
|
||||
.Append("\tmissing_strength").AppendLine();
|
||||
}
|
||||
else
|
||||
{
|
||||
string phrase = EventCatalog.Decision.ActionPhraseFor(id);
|
||||
tsv.Append("decision_prose\t").Append(id).Append("\t1\t1\t")
|
||||
.Append(entry.EventStrength.ToString("0.#")).Append('\t')
|
||||
.Append(phrase).AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
CatalogCoverageAudit.WriteReport(outputDirectory, "domain_event_audit.tsv", tsv.ToString());
|
||||
|
||||
bool passed = plots.Passed && eras.Passed && books.Passed && traits.Passed
|
||||
&& spells.Passed && powers.Passed && decisions.Passed && items.Passed
|
||||
&& subspTraits.Passed && genes.Passed && phenotypes.Passed
|
||||
&& worldLaws.Passed && biomes.Passed
|
||||
&& relMissing == 0 && relTotal > 0;
|
||||
&& relMissing == 0 && relTotal > 0
|
||||
&& decisionProseMissing == 0 && decisionStrengthMissing == 0
|
||||
&& decisionProseTotal > 0;
|
||||
string detail =
|
||||
$"{plots.Detail}; {eras.Detail}; {books.Detail}; {traits.Detail}; "
|
||||
+ $"{spells.Detail}; {powers.Detail}; {decisions.Detail}; {items.Detail}; "
|
||||
+ $"{subspTraits.Detail}; {genes.Detail}; {phenotypes.Detail}; "
|
||||
+ $"{worldLaws.Detail}; {biomes.Detail}; relationship: authored={relTotal} missing={relMissing}";
|
||||
+ $"{worldLaws.Detail}; {biomes.Detail}; relationship: authored={relTotal} missing={relMissing}; "
|
||||
+ $"decision_prose: camera={decisionProseTotal} missing={decisionProseMissing} "
|
||||
+ $"strengthMissing={decisionStrengthMissing} catalog={EventCatalogConfig.LoadedFromFile}";
|
||||
return new DomainEventAuditResult { Passed = passed, Detail = detail };
|
||||
}
|
||||
|
||||
|
|
|
|||
726
IdleSpectator/EventCatalogConfig.cs
Normal file
726
IdleSpectator/EventCatalogConfig.cs
Normal file
|
|
@ -0,0 +1,726 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using NeoModLoader.services;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>One decision row from event-catalog.json (action phrase + EventStrength).</summary>
|
||||
public sealed class DecisionCatalogRow
|
||||
{
|
||||
public string Id = "";
|
||||
public string Action = "";
|
||||
public float Strength;
|
||||
}
|
||||
|
||||
/// <summary>Library dial defaults from event-catalog.json <c>libraries</c>.</summary>
|
||||
public sealed class LibraryCatalogDefaults
|
||||
{
|
||||
public float AmbientStrength = 40f;
|
||||
public float SignalStrength = 80f;
|
||||
public string Category = "Event";
|
||||
public string Label = "{a} · {id}";
|
||||
public bool AmbientCreatesInterest = true;
|
||||
public readonly HashSet<string> SignalIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads event-catalog.json — SoT for per-event strength/prose overlays.
|
||||
/// <c>action</c> is decision-only (clause after "decides to"); other domains use <c>label</c> / happiness variants.
|
||||
/// </summary>
|
||||
public static class EventCatalogConfig
|
||||
{
|
||||
public const string FileName = "event-catalog.json";
|
||||
public const float DefaultDecisionStrength = 86f;
|
||||
|
||||
private static readonly Dictionary<string, DecisionCatalogRow> Decisions =
|
||||
new Dictionary<string, DecisionCatalogRow>(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly Dictionary<string, float> SpeciesBonuses =
|
||||
new Dictionary<string, float>(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly Dictionary<string, float> CharacterBonuses =
|
||||
new Dictionary<string, float>(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly Dictionary<string, HappinessCatalogEntry> Happiness =
|
||||
new Dictionary<string, HappinessCatalogEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly Dictionary<string, StatusInterestEntry> Status =
|
||||
new Dictionary<string, StatusInterestEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly Dictionary<string, WorldLogEventEntry> WorldLog =
|
||||
new Dictionary<string, WorldLogEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Plots =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Relationship =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Traits =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Books =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Eras =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly Dictionary<string, DisasterInterestEntry> Disasters =
|
||||
new Dictionary<string, DisasterInterestEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly Dictionary<string, WarTypeInterestEntry> WarTypes =
|
||||
new Dictionary<string, WarTypeInterestEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly Dictionary<string, LibraryCatalogDefaults> Libraries =
|
||||
new Dictionary<string, LibraryCatalogDefaults>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static string _loadedPath = "";
|
||||
private static string _loadedVersion = "";
|
||||
private static bool _loadedFromFile;
|
||||
private static int _generation;
|
||||
|
||||
public static bool LoadedFromFile => _loadedFromFile;
|
||||
public static string LoadedPath => _loadedPath;
|
||||
public static string LoadedVersion => _loadedVersion;
|
||||
public static int Generation => _generation;
|
||||
|
||||
public static IEnumerable<string> DecisionIds => Decisions.Keys;
|
||||
public static int DecisionCount => Decisions.Count;
|
||||
public static int SpeciesCount => SpeciesBonuses.Count;
|
||||
public static int CharacterCount => CharacterBonuses.Count;
|
||||
public static int HappinessCount => Happiness.Count;
|
||||
public static int StatusCount => Status.Count;
|
||||
public static int WorldLogCount => WorldLog.Count;
|
||||
public static int PlotCount => Plots.Count;
|
||||
public static int RelationshipCount => Relationship.Count;
|
||||
public static int TraitCount => Traits.Count;
|
||||
public static int BookCount => Books.Count;
|
||||
public static int EraCount => Eras.Count;
|
||||
public static int DisasterCount => Disasters.Count;
|
||||
public static int WarTypeCount => WarTypes.Count;
|
||||
public static int LibraryCount => Libraries.Count;
|
||||
|
||||
public static void LoadFromModFolder(string modFolder)
|
||||
{
|
||||
ClearAll();
|
||||
_loadedFromFile = false;
|
||||
_loadedPath = "";
|
||||
_loadedVersion = "";
|
||||
|
||||
if (string.IsNullOrEmpty(modFolder))
|
||||
{
|
||||
LogService.LogInfo("[IdleSpectator] event-catalog.json: no mod folder, using C# fallbacks");
|
||||
BumpAndInvalidate();
|
||||
return;
|
||||
}
|
||||
|
||||
string path = Path.Combine(modFolder, FileName);
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
LogService.LogInfo($"[IdleSpectator] event-catalog.json missing at {path}, using C# fallbacks");
|
||||
BumpAndInvalidate();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string json = File.ReadAllText(path);
|
||||
if (!TryParseDocument(json, out string version))
|
||||
{
|
||||
LogService.LogInfo("[IdleSpectator] event-catalog.json parse failed, using C# fallbacks");
|
||||
ClearAll();
|
||||
BumpAndInvalidate();
|
||||
return;
|
||||
}
|
||||
|
||||
_loadedFromFile = Happiness.Count > 0 || Decisions.Count > 0 || Status.Count > 0;
|
||||
_loadedPath = path;
|
||||
_loadedVersion = version ?? "";
|
||||
BumpAndInvalidate();
|
||||
LogService.LogInfo(
|
||||
$"[IdleSpectator] event-catalog.json loaded v={_loadedVersion} "
|
||||
+ $"happiness={Happiness.Count} status={Status.Count} worldLog={WorldLog.Count} "
|
||||
+ $"plots={Plots.Count} relationship={Relationship.Count} traits={Traits.Count} "
|
||||
+ $"books={Books.Count} eras={Eras.Count} disasters={Disasters.Count} "
|
||||
+ $"warTypes={WarTypes.Count} decisions={Decisions.Count} libraries={Libraries.Count} "
|
||||
+ $"species={SpeciesBonuses.Count} character={CharacterBonuses.Count}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ClearAll();
|
||||
LogService.LogInfo($"[IdleSpectator] event-catalog.json failed ({ex.Message}), using C# fallbacks");
|
||||
BumpAndInvalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ClearAll()
|
||||
{
|
||||
Decisions.Clear();
|
||||
SpeciesBonuses.Clear();
|
||||
CharacterBonuses.Clear();
|
||||
Happiness.Clear();
|
||||
Status.Clear();
|
||||
WorldLog.Clear();
|
||||
Plots.Clear();
|
||||
Relationship.Clear();
|
||||
Traits.Clear();
|
||||
Books.Clear();
|
||||
Eras.Clear();
|
||||
Disasters.Clear();
|
||||
WarTypes.Clear();
|
||||
Libraries.Clear();
|
||||
}
|
||||
|
||||
private static void BumpAndInvalidate()
|
||||
{
|
||||
_generation++;
|
||||
EventCatalog.InvalidateAllOverlays();
|
||||
}
|
||||
|
||||
internal static bool TryParseDocument(string json, out string version)
|
||||
{
|
||||
version = "";
|
||||
if (string.IsNullOrEmpty(json))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
version = ExtractJsonString(json, "modVersion") ?? "";
|
||||
if (TryExtractArray(json, "decisions", out string a)) IngestDecisionObjects(a);
|
||||
if (TryExtractArray(json, "species", out a)) IngestBonusObjects(a, SpeciesBonuses);
|
||||
if (TryExtractArray(json, "character", out a)) IngestBonusObjects(a, CharacterBonuses);
|
||||
if (TryExtractArray(json, "happiness", out a)) IngestHappiness(a);
|
||||
if (TryExtractArray(json, "status", out a)) IngestStatus(a);
|
||||
if (TryExtractArray(json, "worldLog", out a)) IngestWorldLog(a);
|
||||
if (TryExtractArray(json, "plots", out a)) IngestDiscrete(a, Plots);
|
||||
if (TryExtractArray(json, "relationship", out a)) IngestDiscrete(a, Relationship);
|
||||
if (TryExtractArray(json, "traits", out a)) IngestDiscrete(a, Traits);
|
||||
if (TryExtractArray(json, "books", out a)) IngestDiscrete(a, Books);
|
||||
if (TryExtractArray(json, "eras", out a)) IngestDiscrete(a, Eras);
|
||||
if (TryExtractArray(json, "disasters", out a)) IngestDisasters(a);
|
||||
if (TryExtractArray(json, "warTypes", out a)) IngestWarTypes(a);
|
||||
IngestLibraries(json);
|
||||
|
||||
return Happiness.Count > 0 || Decisions.Count > 0 || Status.Count > 0 || Plots.Count > 0;
|
||||
}
|
||||
|
||||
public static int FillHappiness(Dictionary<string, HappinessCatalogEntry> into)
|
||||
{
|
||||
into.Clear();
|
||||
foreach (KeyValuePair<string, HappinessCatalogEntry> kv in Happiness)
|
||||
{
|
||||
into[kv.Key] = kv.Value;
|
||||
}
|
||||
return into.Count;
|
||||
}
|
||||
|
||||
public static int FillStatus(Dictionary<string, StatusInterestEntry> into)
|
||||
{
|
||||
into.Clear();
|
||||
foreach (KeyValuePair<string, StatusInterestEntry> kv in Status)
|
||||
{
|
||||
into[kv.Key] = kv.Value;
|
||||
}
|
||||
return into.Count;
|
||||
}
|
||||
|
||||
public static int FillWorldLog(Dictionary<string, WorldLogEventEntry> into)
|
||||
{
|
||||
into.Clear();
|
||||
foreach (KeyValuePair<string, WorldLogEventEntry> kv in WorldLog)
|
||||
{
|
||||
into[kv.Key] = kv.Value;
|
||||
}
|
||||
return into.Count;
|
||||
}
|
||||
|
||||
public static int FillPlots(Dictionary<string, DiscreteEventEntry> into) => FillDiscrete(Plots, into);
|
||||
public static int FillRelationship(Dictionary<string, DiscreteEventEntry> into) => FillDiscrete(Relationship, into);
|
||||
public static int FillTraits(Dictionary<string, DiscreteEventEntry> into) => FillDiscrete(Traits, into);
|
||||
public static int FillBooks(Dictionary<string, DiscreteEventEntry> into) => FillDiscrete(Books, into);
|
||||
public static int FillEras(Dictionary<string, DiscreteEventEntry> into) => FillDiscrete(Eras, into);
|
||||
|
||||
public static int FillDisasters(Dictionary<string, DisasterInterestEntry> into)
|
||||
{
|
||||
into.Clear();
|
||||
foreach (KeyValuePair<string, DisasterInterestEntry> kv in Disasters)
|
||||
{
|
||||
into[kv.Key] = kv.Value;
|
||||
}
|
||||
return into.Count;
|
||||
}
|
||||
|
||||
public static int FillWarTypes(Dictionary<string, WarTypeInterestEntry> into)
|
||||
{
|
||||
into.Clear();
|
||||
foreach (KeyValuePair<string, WarTypeInterestEntry> kv in WarTypes)
|
||||
{
|
||||
into[kv.Key] = kv.Value;
|
||||
}
|
||||
return into.Count;
|
||||
}
|
||||
|
||||
private static int FillDiscrete(
|
||||
Dictionary<string, DiscreteEventEntry> src,
|
||||
Dictionary<string, DiscreteEventEntry> into)
|
||||
{
|
||||
into.Clear();
|
||||
foreach (KeyValuePair<string, DiscreteEventEntry> kv in src)
|
||||
{
|
||||
into[kv.Key] = kv.Value;
|
||||
}
|
||||
return into.Count;
|
||||
}
|
||||
|
||||
public static bool TryGetLibraryDefaults(string key, out LibraryCatalogDefaults defaults)
|
||||
{
|
||||
defaults = null;
|
||||
if (string.IsNullOrEmpty(key))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return Libraries.TryGetValue(key.Trim(), out defaults) && defaults != null;
|
||||
}
|
||||
|
||||
private static void IngestHappiness(string arrayJson)
|
||||
{
|
||||
foreach (string obj in EnumerateObjects(arrayJson))
|
||||
{
|
||||
string id = ExtractJsonString(obj, "id");
|
||||
if (string.IsNullOrEmpty(id)) continue;
|
||||
var entry = new HappinessCatalogEntry
|
||||
{
|
||||
Id = id.Trim(),
|
||||
EventStrength = ExtractJsonFloat(obj, "strength", 40f),
|
||||
Presentation = ParseEnum(obj, "presentation", HappinessPresentationTier.Signal),
|
||||
Context = ParseEnum(obj, "context", HappinessContextRequirement.None),
|
||||
Life = ParseEnum(obj, "life", HappinessLifePolicy.ActivityOnly),
|
||||
Overlap = ParseEnum(obj, "overlap", HappinessOverlapPolicy.Independent),
|
||||
RelationRole = ParseEnum(obj, "relationRole", HappinessRelationRole.None),
|
||||
InterestCategory = ExtractJsonString(obj, "category") ?? "",
|
||||
CanonicalMilestoneKey = ExtractJsonString(obj, "canonicalMilestoneKey") ?? "",
|
||||
StatusOverlapId = ExtractJsonString(obj, "statusOverlapId") ?? "",
|
||||
VariantsWithRelated = ExtractStringArray(obj, "variantsWithRelated"),
|
||||
VariantsWithoutRelated = ExtractStringArray(obj, "variantsWithoutRelated")
|
||||
};
|
||||
Happiness[entry.Id] = entry;
|
||||
}
|
||||
}
|
||||
|
||||
private static void IngestStatus(string arrayJson)
|
||||
{
|
||||
foreach (string obj in EnumerateObjects(arrayJson))
|
||||
{
|
||||
string id = ExtractJsonString(obj, "id");
|
||||
if (string.IsNullOrEmpty(id)) continue;
|
||||
Status[id.Trim()] = new StatusInterestEntry
|
||||
{
|
||||
Id = id.Trim(),
|
||||
EventStrength = ExtractJsonFloat(obj, "strength", 40f),
|
||||
Category = ExtractJsonString(obj, "category") ?? "Status",
|
||||
CreatesInterest = ExtractJsonBool(obj, "camera", false),
|
||||
ExtendsGrief = ExtractJsonBool(obj, "extendsGrief", false)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static void IngestWorldLog(string arrayJson)
|
||||
{
|
||||
foreach (string obj in EnumerateObjects(arrayJson))
|
||||
{
|
||||
string id = ExtractJsonString(obj, "id");
|
||||
if (string.IsNullOrEmpty(id)) continue;
|
||||
bool camera = ExtractJsonBool(obj, "camera", true);
|
||||
float strength = ExtractJsonFloat(obj, "strength", 40f);
|
||||
bool chronicle = ExtractJsonBool(obj, "chronicle", camera && strength >= 65f);
|
||||
WorldLog[id.Trim()] = new WorldLogEventEntry
|
||||
{
|
||||
Id = id.Trim(),
|
||||
EventStrength = strength,
|
||||
Category = ExtractJsonString(obj, "category") ?? "WorldLog",
|
||||
CreatesInterest = camera,
|
||||
ChronicleEligible = chronicle,
|
||||
LabelTemplate = ExtractJsonString(obj, "label") ?? "{id}: {a}",
|
||||
MinWatch = ExtractJsonFloat(obj, "minWatch", 6f),
|
||||
MaxWatch = ExtractJsonFloat(obj, "maxWatch", 28f)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static void IngestDiscrete(string arrayJson, Dictionary<string, DiscreteEventEntry> into)
|
||||
{
|
||||
foreach (string obj in EnumerateObjects(arrayJson))
|
||||
{
|
||||
string id = ExtractJsonString(obj, "id");
|
||||
if (string.IsNullOrEmpty(id)) continue;
|
||||
into[id.Trim()] = new DiscreteEventEntry
|
||||
{
|
||||
Id = id.Trim(),
|
||||
EventStrength = ExtractJsonFloat(obj, "strength", 40f),
|
||||
Category = ExtractJsonString(obj, "category") ?? "Event",
|
||||
CreatesInterest = ExtractJsonBool(obj, "camera", true),
|
||||
LabelTemplate = ExtractJsonString(obj, "label") ?? "{a} · {id}",
|
||||
ActionPhrase = ExtractJsonString(obj, "action") ?? ""
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static void IngestDisasters(string arrayJson)
|
||||
{
|
||||
foreach (string obj in EnumerateObjects(arrayJson))
|
||||
{
|
||||
string id = ExtractJsonString(obj, "id");
|
||||
if (string.IsNullOrEmpty(id)) continue;
|
||||
Disasters[id.Trim()] = new DisasterInterestEntry
|
||||
{
|
||||
Id = id.Trim(),
|
||||
EventStrength = ExtractJsonFloat(obj, "strength", 90f),
|
||||
Category = ExtractJsonString(obj, "category") ?? "Disaster",
|
||||
CreatesInterest = ExtractJsonBool(obj, "camera", true),
|
||||
WorldLogId = ExtractJsonString(obj, "worldLogId") ?? ""
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static void IngestWarTypes(string arrayJson)
|
||||
{
|
||||
foreach (string obj in EnumerateObjects(arrayJson))
|
||||
{
|
||||
string id = ExtractJsonString(obj, "id");
|
||||
if (string.IsNullOrEmpty(id)) continue;
|
||||
WarTypes[id.Trim()] = new WarTypeInterestEntry
|
||||
{
|
||||
Id = id.Trim(),
|
||||
EventStrength = ExtractJsonFloat(obj, "strength", 85f),
|
||||
Category = ExtractJsonString(obj, "category") ?? "Politics",
|
||||
CreatesInterest = ExtractJsonBool(obj, "camera", true),
|
||||
LabelTemplate = ExtractJsonString(obj, "label") ?? "War ({id})"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static void IngestLibraries(string json)
|
||||
{
|
||||
if (!TryExtractObject(json, "libraries", out string libsJson))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string[] keys =
|
||||
{
|
||||
"spell", "power", "item", "gene", "phenotype", "worldLaw", "subspeciesTrait", "biome"
|
||||
};
|
||||
for (int i = 0; i < keys.Length; i++)
|
||||
{
|
||||
string key = keys[i];
|
||||
if (!TryExtractObject(libsJson, key, out string obj))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var d = new LibraryCatalogDefaults
|
||||
{
|
||||
AmbientStrength = ExtractJsonFloat(obj, "ambientStrength", 40f),
|
||||
SignalStrength = ExtractJsonFloat(obj, "signalStrength", 80f),
|
||||
Category = ExtractJsonString(obj, "category") ?? "Event",
|
||||
Label = ExtractJsonString(obj, "label") ?? "{a} · {id}",
|
||||
AmbientCreatesInterest = ExtractJsonBool(obj, "ambientCreatesInterest", true)
|
||||
};
|
||||
string[] ids = ExtractStringArray(obj, "signalIds");
|
||||
for (int j = 0; j < ids.Length; j++)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(ids[j]))
|
||||
{
|
||||
d.SignalIds.Add(ids[j]);
|
||||
}
|
||||
}
|
||||
Libraries[key] = d;
|
||||
}
|
||||
}
|
||||
|
||||
private static int IngestDecisionObjects(string arrayJson)
|
||||
{
|
||||
int n = 0;
|
||||
foreach (string obj in EnumerateObjects(arrayJson))
|
||||
{
|
||||
string id = ExtractJsonString(obj, "id");
|
||||
string action = ExtractJsonString(obj, "action");
|
||||
if (string.IsNullOrEmpty(id) || string.IsNullOrEmpty(action)) continue;
|
||||
float strength = ExtractJsonFloat(obj, "strength", DefaultDecisionStrength);
|
||||
Decisions[id.Trim()] = new DecisionCatalogRow
|
||||
{
|
||||
Id = id.Trim(),
|
||||
Action = action,
|
||||
Strength = strength > 0f ? strength : DefaultDecisionStrength
|
||||
};
|
||||
n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
private static int IngestBonusObjects(string arrayJson, Dictionary<string, float> into)
|
||||
{
|
||||
int n = 0;
|
||||
foreach (string obj in EnumerateObjects(arrayJson))
|
||||
{
|
||||
string id = ExtractJsonString(obj, "id");
|
||||
if (string.IsNullOrEmpty(id)) continue;
|
||||
into[id.Trim()] = ExtractJsonFloat(obj, "bonus", 0f);
|
||||
n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
private static TEnum ParseEnum<TEnum>(string json, string key, TEnum fallback) where TEnum : struct
|
||||
{
|
||||
string raw = ExtractJsonString(json, key);
|
||||
if (string.IsNullOrEmpty(raw)) return fallback;
|
||||
return Enum.TryParse(raw, true, out TEnum value) ? value : fallback;
|
||||
}
|
||||
|
||||
private static string[] ExtractStringArray(string json, string key)
|
||||
{
|
||||
if (!TryExtractArray(json, key, out string arr) || string.IsNullOrEmpty(arr))
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
var list = new List<string>();
|
||||
int i = 0;
|
||||
while (i < arr.Length)
|
||||
{
|
||||
int q = arr.IndexOf('"', i);
|
||||
if (q < 0) break;
|
||||
int end = q + 1;
|
||||
var sb = new StringBuilder();
|
||||
while (end < arr.Length)
|
||||
{
|
||||
char c = arr[end++];
|
||||
if (c == '\\' && end < arr.Length) { sb.Append(arr[end++]); continue; }
|
||||
if (c == '"') break;
|
||||
sb.Append(c);
|
||||
}
|
||||
list.Add(sb.ToString());
|
||||
i = end;
|
||||
}
|
||||
return list.ToArray();
|
||||
}
|
||||
|
||||
private static bool ExtractJsonBool(string json, string key, bool fallback)
|
||||
{
|
||||
string needle = "\"" + key + "\"";
|
||||
int keyAt = json.IndexOf(needle, StringComparison.Ordinal);
|
||||
if (keyAt < 0) return fallback;
|
||||
int colon = json.IndexOf(':', keyAt + needle.Length);
|
||||
if (colon < 0) return fallback;
|
||||
int i = colon + 1;
|
||||
while (i < json.Length && char.IsWhiteSpace(json[i])) i++;
|
||||
if (i + 4 <= json.Length && string.Compare(json, i, "true", 0, 4, StringComparison.OrdinalIgnoreCase) == 0)
|
||||
return true;
|
||||
if (i + 5 <= json.Length && string.Compare(json, i, "false", 0, 5, StringComparison.OrdinalIgnoreCase) == 0)
|
||||
return false;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateObjects(string arrayJson)
|
||||
{
|
||||
if (string.IsNullOrEmpty(arrayJson)) yield break;
|
||||
int i = 0;
|
||||
while (i < arrayJson.Length)
|
||||
{
|
||||
int start = arrayJson.IndexOf('{', i);
|
||||
if (start < 0) yield break;
|
||||
int depth = 0;
|
||||
bool inString = false;
|
||||
bool escape = false;
|
||||
for (int j = start; j < arrayJson.Length; j++)
|
||||
{
|
||||
char c = arrayJson[j];
|
||||
if (inString)
|
||||
{
|
||||
if (escape) { escape = false; continue; }
|
||||
if (c == '\\') { escape = true; continue; }
|
||||
if (c == '"') inString = false;
|
||||
continue;
|
||||
}
|
||||
if (c == '"') { inString = true; continue; }
|
||||
if (c == '{') depth++;
|
||||
else if (c == '}')
|
||||
{
|
||||
depth--;
|
||||
if (depth == 0)
|
||||
{
|
||||
yield return arrayJson.Substring(start, j - start + 1);
|
||||
i = j + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (depth != 0) yield break;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ExtractJsonString(string json, string key)
|
||||
{
|
||||
string needle = "\"" + key + "\"";
|
||||
int keyAt = json.IndexOf(needle, StringComparison.Ordinal);
|
||||
if (keyAt < 0) return null;
|
||||
int colon = json.IndexOf(':', keyAt + needle.Length);
|
||||
if (colon < 0) return null;
|
||||
int i = colon + 1;
|
||||
while (i < json.Length && char.IsWhiteSpace(json[i])) i++;
|
||||
if (i >= json.Length || json[i] != '"') return null;
|
||||
i++;
|
||||
var sb = new StringBuilder();
|
||||
while (i < json.Length)
|
||||
{
|
||||
char c = json[i++];
|
||||
if (c == '\\' && i < json.Length) { sb.Append(json[i++]); continue; }
|
||||
if (c == '"') break;
|
||||
sb.Append(c);
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static float ExtractJsonFloat(string json, string key, float fallback)
|
||||
{
|
||||
string needle = "\"" + key + "\"";
|
||||
int keyAt = json.IndexOf(needle, StringComparison.Ordinal);
|
||||
if (keyAt < 0) return fallback;
|
||||
int colon = json.IndexOf(':', keyAt + needle.Length);
|
||||
if (colon < 0) return fallback;
|
||||
int i = colon + 1;
|
||||
while (i < json.Length && char.IsWhiteSpace(json[i])) i++;
|
||||
int start = i;
|
||||
while (i < json.Length && (char.IsDigit(json[i]) || json[i] == '.' || json[i] == '-' || json[i] == '+'))
|
||||
i++;
|
||||
if (i <= start) return fallback;
|
||||
string token = json.Substring(start, i - start);
|
||||
return float.TryParse(token, NumberStyles.Float, CultureInfo.InvariantCulture, out float value)
|
||||
? value : fallback;
|
||||
}
|
||||
|
||||
private static bool TryExtractArray(string json, string key, out string arrayJson)
|
||||
{
|
||||
arrayJson = null;
|
||||
string keyColon = "\"" + key + "\"";
|
||||
int searchFrom = 0;
|
||||
int keyAt = -1;
|
||||
while (true)
|
||||
{
|
||||
int at = json.IndexOf(keyColon, searchFrom, StringComparison.Ordinal);
|
||||
if (at < 0) break;
|
||||
int after = at + keyColon.Length;
|
||||
while (after < json.Length && char.IsWhiteSpace(json[after])) after++;
|
||||
if (after < json.Length && json[after] == ':') { keyAt = at; break; }
|
||||
searchFrom = at + 1;
|
||||
}
|
||||
if (keyAt < 0) return false;
|
||||
int bracket = json.IndexOf('[', keyAt + keyColon.Length);
|
||||
if (bracket < 0) return false;
|
||||
int depth = 0;
|
||||
bool inString = false;
|
||||
bool escape = false;
|
||||
for (int i = bracket; i < json.Length; i++)
|
||||
{
|
||||
char c = json[i];
|
||||
if (inString)
|
||||
{
|
||||
if (escape) { escape = false; continue; }
|
||||
if (c == '\\') { escape = true; continue; }
|
||||
if (c == '"') inString = false;
|
||||
continue;
|
||||
}
|
||||
if (c == '"') { inString = true; continue; }
|
||||
if (c == '[') depth++;
|
||||
else if (c == ']')
|
||||
{
|
||||
depth--;
|
||||
if (depth == 0)
|
||||
{
|
||||
arrayJson = json.Substring(bracket, i - bracket + 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryExtractObject(string json, string key, out string objectJson)
|
||||
{
|
||||
objectJson = null;
|
||||
string keyColon = "\"" + key + "\"";
|
||||
int searchFrom = 0;
|
||||
int keyAt = -1;
|
||||
while (true)
|
||||
{
|
||||
int at = json.IndexOf(keyColon, searchFrom, StringComparison.Ordinal);
|
||||
if (at < 0) break;
|
||||
int after = at + keyColon.Length;
|
||||
while (after < json.Length && char.IsWhiteSpace(json[after])) after++;
|
||||
if (after < json.Length && json[after] == ':') { keyAt = at; break; }
|
||||
searchFrom = at + 1;
|
||||
}
|
||||
if (keyAt < 0) return false;
|
||||
int brace = json.IndexOf('{', keyAt + keyColon.Length);
|
||||
if (brace < 0) return false;
|
||||
int depth = 0;
|
||||
bool inString = false;
|
||||
bool escape = false;
|
||||
for (int i = brace; i < json.Length; i++)
|
||||
{
|
||||
char c = json[i];
|
||||
if (inString)
|
||||
{
|
||||
if (escape) { escape = false; continue; }
|
||||
if (c == '\\') { escape = true; continue; }
|
||||
if (c == '"') inString = false;
|
||||
continue;
|
||||
}
|
||||
if (c == '"') { inString = true; continue; }
|
||||
if (c == '{') depth++;
|
||||
else if (c == '}')
|
||||
{
|
||||
depth--;
|
||||
if (depth == 0)
|
||||
{
|
||||
objectJson = json.Substring(brace, i - brace + 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool TryGetDecision(string id, out DecisionCatalogRow row)
|
||||
{
|
||||
row = null;
|
||||
if (string.IsNullOrEmpty(id)) return false;
|
||||
return Decisions.TryGetValue(id.Trim(), out row) && row != null;
|
||||
}
|
||||
|
||||
public static bool TryGetDecisionAction(string id, out string action)
|
||||
{
|
||||
action = "";
|
||||
if (!TryGetDecision(id, out DecisionCatalogRow row)) return false;
|
||||
action = row.Action ?? "";
|
||||
return !string.IsNullOrEmpty(action);
|
||||
}
|
||||
|
||||
public static float DecisionStrengthOrDefault(string id, float fallback = DefaultDecisionStrength)
|
||||
{
|
||||
if (TryGetDecision(id, out DecisionCatalogRow row) && row.Strength > 0f) return row.Strength;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
public static float SpeciesBonus(string speciesId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(speciesId)) return 0f;
|
||||
return SpeciesBonuses.TryGetValue(speciesId.Trim(), out float bonus) ? bonus : 0f;
|
||||
}
|
||||
|
||||
public static float CharacterBonus(string roleId, float scoringModelFallback)
|
||||
{
|
||||
if (string.IsNullOrEmpty(roleId)) return scoringModelFallback;
|
||||
if (CharacterBonuses.TryGetValue(roleId.Trim(), out float bonus)) return bonus;
|
||||
return scoringModelFallback;
|
||||
}
|
||||
|
||||
public static bool Reload()
|
||||
{
|
||||
string folder = ModClass.ModFolder;
|
||||
if (string.IsNullOrEmpty(folder)) return false;
|
||||
LoadFromModFolder(folder);
|
||||
return _loadedFromFile;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ using System.Collections.Generic;
|
|||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>Authored interest overlay for every live book_types asset.</summary>
|
||||
/// <summary>Book dials from <c>event-catalog.json</c> <c>books</c>.</summary>
|
||||
public static partial class EventCatalog
|
||||
{
|
||||
public static class Book
|
||||
|
|
@ -11,46 +11,43 @@ public static partial class EventCatalog
|
|||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(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<string> AuthoredIds => Entries.Keys;
|
||||
public static IEnumerable<string> 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;
|
||||
|
|
|
|||
406
IdleSpectator/Events/Catalogs/EventCatalog.Decision.cs
Normal file
406
IdleSpectator/Events/Catalogs/EventCatalog.Decision.cs
Normal file
|
|
@ -0,0 +1,406 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>
|
||||
/// Decision interest dials + authored action phrases.
|
||||
/// <para>
|
||||
/// <b>Source of truth:</b> <c>event-catalog.json</c> (<c>decisions</c> rows: action + strength).
|
||||
/// <see cref="BuiltinActionPhrases"/> is the offline fallback when that file is missing.
|
||||
/// Live ids still seed via <see cref="LiveLibraryInterest"/>; camera Signal uses
|
||||
/// <see cref="IsCameraWorthy"/>; missing ActionPhrase falls back to a generic humanize.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static partial class EventCatalog
|
||||
{
|
||||
public static class Decision
|
||||
{
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static int _appliedOverlayGeneration = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Offline fallback infinitive clauses when event-catalog.json is missing a row.
|
||||
/// Prefer editing <c>event-catalog.json</c> instead.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<string, string> BuiltinActionPhrases =
|
||||
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
// Social / crime
|
||||
["find_lover"] = "find a lover",
|
||||
["try_to_steal_money"] = "try to steal money",
|
||||
["banish_unruly_clan_members"] = "banish unruly clan members",
|
||||
["kill_unruly_clan_members"] = "kill unruly clan members",
|
||||
|
||||
// Plots
|
||||
["try_new_plot"] = "try a new plot",
|
||||
|
||||
// King kingdom policy
|
||||
["king_change_kingdom_culture"] = "change the kingdom's culture",
|
||||
["king_change_kingdom_language"] = "change the kingdom's language",
|
||||
["king_change_kingdom_religion"] = "change the kingdom's religion",
|
||||
|
||||
// City leader policy
|
||||
["leader_change_city_culture"] = "change the city's culture",
|
||||
["leader_change_city_language"] = "change the city's language",
|
||||
["leader_change_city_religion"] = "change the city's religion",
|
||||
|
||||
// Army / warrior (keep train-with-dummy authored for if we ever Signal it)
|
||||
["warrior_army_follow_leader"] = "follow their leader",
|
||||
["warrior_train_with_dummy"] = "train with a dummy",
|
||||
["warrior_try_join_army_group"] = "try to join an army",
|
||||
};
|
||||
|
||||
/// <summary>Drop cached rows so the next Ensure re-applies JSON overlays.</summary>
|
||||
public static void InvalidateOverlays()
|
||||
{
|
||||
Entries.Clear();
|
||||
_appliedOverlayGeneration = -1;
|
||||
}
|
||||
|
||||
private static bool IsInterestingDecision(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string s = id.ToLowerInvariant();
|
||||
// Pure AI ticks / daily fluff - not story beats.
|
||||
if (s.Contains("idle")
|
||||
|| s.Contains("walking")
|
||||
|| s.Contains("check")
|
||||
|| s.Contains("wait")
|
||||
|| s.Contains("sleep")
|
||||
|| s.Contains("random")
|
||||
|| s.Contains("play")
|
||||
|| s.Contains("jump")
|
||||
|| s.Contains("flip")
|
||||
|| s.Contains("diet_")
|
||||
|| s.Contains("move")
|
||||
|| s.Contains("swim")
|
||||
|| s.Contains("follow_parent")
|
||||
|| s.Contains("follow_desire")
|
||||
|| s.Contains("reflection")
|
||||
|| s.Contains("poop"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return HasToken(s, "war")
|
||||
|| s.Contains("rebellion")
|
||||
|| s.Contains("revolt")
|
||||
|| s.Contains("alliance")
|
||||
|| s.Contains("coup")
|
||||
|| s.Contains("betray")
|
||||
|| s.Contains("declare")
|
||||
|| s.Contains("invade")
|
||||
|| s.Contains("siege")
|
||||
|| s.Contains("found")
|
||||
|| s.Contains("city_foundation")
|
||||
|| s.Contains("army")
|
||||
|| s.Contains("lover")
|
||||
|| s.Contains("plot")
|
||||
|| s.Contains("marry")
|
||||
|| s.Contains("banish")
|
||||
|| s.Contains("steal")
|
||||
|| s.Contains("kill_unruly")
|
||||
|| HasToken(s, "king")
|
||||
|| HasToken(s, "leader")
|
||||
|| s.Contains("clan")
|
||||
|| s.Contains("religion")
|
||||
|| s.Contains("culture")
|
||||
|| s.Contains("baby_make")
|
||||
|| s.Contains("have_child")
|
||||
|| s.Contains("birth");
|
||||
}
|
||||
|
||||
/// <summary>Underscore-token match so <c>war</c> does not hit <c>warrior</c>.</summary>
|
||||
private static bool HasToken(string haystack, string token)
|
||||
{
|
||||
if (string.IsNullOrEmpty(haystack) || string.IsNullOrEmpty(token))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (haystack == token)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return haystack.StartsWith(token + "_", StringComparison.Ordinal)
|
||||
|| haystack.EndsWith("_" + token, StringComparison.Ordinal)
|
||||
|| haystack.Contains("_" + token + "_");
|
||||
}
|
||||
|
||||
/// <summary>True when a decision id should be allowed to own the idle camera.</summary>
|
||||
public static bool IsCameraWorthy(string id) => IsInterestingDecision(id);
|
||||
|
||||
/// <summary>Authored action-phrase ids (JSON catalog ∪ builtin fallback).</summary>
|
||||
public static IEnumerable<string> AuthoredActionPhraseIds
|
||||
{
|
||||
get
|
||||
{
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (string id in EventCatalogConfig.DecisionIds)
|
||||
{
|
||||
if (seen.Add(id))
|
||||
{
|
||||
yield return id;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string id in BuiltinActionPhrases.Keys)
|
||||
{
|
||||
if (seen.Add(id))
|
||||
{
|
||||
yield return id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryGetAuthoredActionPhrase(string id, out string actionPhrase)
|
||||
{
|
||||
actionPhrase = "";
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string key = id.Trim();
|
||||
if (EventCatalogConfig.TryGetDecisionAction(key, out actionPhrase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return BuiltinActionPhrases.TryGetValue(key, out actionPhrase)
|
||||
&& !string.IsNullOrEmpty(actionPhrase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Infinitive clause for <c>decides to …</c>. Prefers event-catalog.json / builtin,
|
||||
/// else a generic humanize of the asset id.
|
||||
/// </summary>
|
||||
public static string ActionPhraseFor(string decisionId)
|
||||
{
|
||||
Ensure();
|
||||
if (string.IsNullOrEmpty(decisionId))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
string key = decisionId.Trim();
|
||||
if (Entries.TryGetValue(key, out DiscreteEventEntry entry)
|
||||
&& !string.IsNullOrEmpty(entry.ActionPhrase))
|
||||
{
|
||||
return entry.ActionPhrase;
|
||||
}
|
||||
|
||||
if (TryGetAuthoredActionPhrase(key, out string authored))
|
||||
{
|
||||
return authored;
|
||||
}
|
||||
|
||||
return FallbackHumanize(key);
|
||||
}
|
||||
|
||||
private static void Ensure()
|
||||
{
|
||||
if (_appliedOverlayGeneration != EventCatalogConfig.Generation)
|
||||
{
|
||||
Entries.Clear();
|
||||
_appliedOverlayGeneration = EventCatalogConfig.Generation;
|
||||
}
|
||||
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLiveDecisionIds,
|
||||
"Politics",
|
||||
"{a} decides to act",
|
||||
ambientStrength: 40f,
|
||||
signalIds: null,
|
||||
signalStrength: EventCatalogConfig.DefaultDecisionStrength,
|
||||
signalPredicate: IsInterestingDecision,
|
||||
ambientCreatesInterest: false);
|
||||
|
||||
// Apply authored prose + per-id strength (JSON wins over builtin).
|
||||
foreach (string id in AuthoredActionPhraseIds)
|
||||
{
|
||||
if (!TryGetAuthoredActionPhrase(id, out string action) || string.IsNullOrEmpty(action))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float strength = EventCatalogConfig.DecisionStrengthOrDefault(
|
||||
id,
|
||||
IsInterestingDecision(id) ? EventCatalogConfig.DefaultDecisionStrength : 40f);
|
||||
string label = "{a} decides to " + action;
|
||||
if (Entries.TryGetValue(id, out DiscreteEventEntry existing))
|
||||
{
|
||||
existing.ActionPhrase = action;
|
||||
existing.LabelTemplate = label;
|
||||
if (EventCatalogConfig.TryGetDecision(id, out _))
|
||||
{
|
||||
existing.EventStrength = strength;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
Entries[id] = new DiscreteEventEntry
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = strength,
|
||||
Category = "Politics",
|
||||
CreatesInterest = IsInterestingDecision(id),
|
||||
ActionPhrase = action,
|
||||
LabelTemplate = label
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
DiscreteEventEntry entry = LiveLibraryInterest.Lookup(
|
||||
Entries, id, "Politics", "{a} decides to act", 35f);
|
||||
if (!entry.IsFallback && string.IsNullOrEmpty(entry.ActionPhrase))
|
||||
{
|
||||
// Attach fallback humanize so callers can still read a clause.
|
||||
entry.ActionPhrase = FallbackHumanize(entry.Id);
|
||||
if (!string.IsNullOrEmpty(entry.ActionPhrase))
|
||||
{
|
||||
entry.LabelTemplate = "{a} decides to " + entry.ActionPhrase;
|
||||
}
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic id → infinitive clause when no authored row exists.
|
||||
/// Prefer adding a row to <c>event-catalog.json</c> instead of relying on this.
|
||||
/// </summary>
|
||||
public static string FallbackHumanize(string decisionId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(decisionId))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
string raw = decisionId.Trim().Replace('-', '_').ToLowerInvariant();
|
||||
if (raw.StartsWith("type_", StringComparison.Ordinal))
|
||||
{
|
||||
raw = raw.Substring(5);
|
||||
}
|
||||
|
||||
string[] parts = raw.Split(new[] { '_' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length == 0)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
int start = 0;
|
||||
while (start < parts.Length
|
||||
&& (parts[start] == "child"
|
||||
|| parts[start] == "baby"
|
||||
|| parts[start] == "adult"
|
||||
|| parts[start] == "warrior"
|
||||
|| parts[start] == "city"
|
||||
|| parts[start] == "actor"
|
||||
|| parts[start] == "animal"
|
||||
|| parts[start] == "herd"
|
||||
|| parts[start] == "civ"
|
||||
|| parts[start] == "mob"
|
||||
|| parts[start] == "king"
|
||||
|| parts[start] == "leader"))
|
||||
{
|
||||
start++;
|
||||
}
|
||||
|
||||
if (start >= parts.Length)
|
||||
{
|
||||
start = 0;
|
||||
}
|
||||
|
||||
var words = new List<string>(parts.Length - start);
|
||||
for (int i = start; i < parts.Length; i++)
|
||||
{
|
||||
words.Add(parts[i]);
|
||||
}
|
||||
|
||||
if (words.Count == 0)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
if (words.Count >= 3
|
||||
&& words[0] == "change"
|
||||
&& (words[1] == "kingdom" || words[1] == "city" || words[1] == "clan"))
|
||||
{
|
||||
return "change the " + words[1] + "'s " + string.Join(" ", words.GetRange(2, words.Count - 2));
|
||||
}
|
||||
|
||||
if (words[0] == "diet" && words.Count >= 2)
|
||||
{
|
||||
words[0] = "eat";
|
||||
}
|
||||
|
||||
if (words.Count >= 2 && words[0] == "try" && words[1] == "new")
|
||||
{
|
||||
words.Insert(1, "a");
|
||||
return string.Join(" ", words);
|
||||
}
|
||||
|
||||
if (words.Count == 2 && words[0] == "find")
|
||||
{
|
||||
return "find a " + words[1];
|
||||
}
|
||||
|
||||
if (words[0] == "join" && words.Count >= 2 && words[1] == "army")
|
||||
{
|
||||
return "join an army";
|
||||
}
|
||||
|
||||
if (words[0] == "try" && words.Count >= 2 && words[1] != "to")
|
||||
{
|
||||
words.Insert(1, "to");
|
||||
}
|
||||
|
||||
if (words.Count >= 4
|
||||
&& words[0] == "try"
|
||||
&& words[1] == "to"
|
||||
&& words[2] == "join"
|
||||
&& words[3] == "army")
|
||||
{
|
||||
return "try to join an army";
|
||||
}
|
||||
|
||||
if (words.Count >= 3 && words[0] == "train" && words[1] == "with")
|
||||
{
|
||||
return "train with a " + string.Join(" ", words.GetRange(2, words.Count - 2));
|
||||
}
|
||||
|
||||
if (words.Count >= 2
|
||||
&& words[words.Count - 1] == "leader"
|
||||
&& (words[0] == "follow" || (words[0] == "army" && words[1] == "follow")))
|
||||
{
|
||||
return "follow their leader";
|
||||
}
|
||||
|
||||
return string.Join(" ", words);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,82 +13,77 @@ public sealed class DisasterInterestEntry
|
|||
public bool IsFallback;
|
||||
}
|
||||
|
||||
/// <summary>Authored interest overlay for every live disasters library asset.</summary>
|
||||
/// <summary>Disaster dials from <c>event-catalog.json</c> <c>disasters</c>.</summary>
|
||||
public static partial class EventCatalog
|
||||
{
|
||||
public static class Disaster
|
||||
{
|
||||
private static readonly Dictionary<string, DisasterInterestEntry> Entries =
|
||||
new Dictionary<string, DisasterInterestEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
static Disaster()
|
||||
{
|
||||
Add("tornado", 95f, "disaster_tornado");
|
||||
Add("heatwave", 88f, "disaster_heatwave");
|
||||
Add("small_meteorite", 95f, "disaster_meteorite");
|
||||
Add("small_earthquake", 95f, "disaster_earthquake");
|
||||
Add("hellspawn", 96f, "disaster_hellspawn");
|
||||
Add("ice_ones_awoken", 95f, "disaster_ice_ones");
|
||||
Add("sudden_snowman", 90f, "disaster_sudden_snowman");
|
||||
Add("garden_surprise", 90f, "disaster_garden_surprise");
|
||||
Add("dragon_from_farlands", 98f, "disaster_dragon_from_farlands");
|
||||
Add("ash_bandits", 92f, "disaster_bandits");
|
||||
Add("alien_invasion", 97f, "disaster_alien_invasion");
|
||||
Add("biomass", 96f, "disaster_biomass");
|
||||
Add("tumor", 96f, "disaster_tumor");
|
||||
Add("wild_mage", 94f, "disaster_evil_mage");
|
||||
Add("underground_necromancer", 95f, "disaster_underground_necromancer");
|
||||
Add("mad_thoughts", 90f, "disaster_mad_thoughts");
|
||||
Add("greg_abominations", 96f, "disaster_greg_abominations");
|
||||
}
|
||||
private static readonly Dictionary<string, DisasterInterestEntry> Entries =
|
||||
new Dictionary<string, DisasterInterestEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static void Add(string id, float strength, string worldLogId)
|
||||
{
|
||||
Entries[id] = new DisasterInterestEntry
|
||||
private static int _appliedGeneration = -1;
|
||||
|
||||
public static void InvalidateOverlays()
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = strength,
|
||||
WorldLogId = worldLogId,
|
||||
CreatesInterest = true,
|
||||
Category = "Disaster"
|
||||
};
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds => Entries.Keys;
|
||||
|
||||
public static bool HasAuthored(string disasterId)
|
||||
{
|
||||
return !string.IsNullOrEmpty(disasterId) && Entries.ContainsKey(disasterId.Trim());
|
||||
}
|
||||
|
||||
public static bool TryGet(string disasterId, out DisasterInterestEntry entry)
|
||||
{
|
||||
entry = null;
|
||||
if (string.IsNullOrEmpty(disasterId))
|
||||
{
|
||||
return false;
|
||||
Entries.Clear();
|
||||
_appliedGeneration = -1;
|
||||
}
|
||||
|
||||
return Entries.TryGetValue(disasterId.Trim(), out entry);
|
||||
}
|
||||
|
||||
public static DisasterInterestEntry GetOrFallback(string disasterId)
|
||||
{
|
||||
if (TryGet(disasterId, out DisasterInterestEntry entry))
|
||||
private static void Ensure()
|
||||
{
|
||||
return entry;
|
||||
if (_appliedGeneration == EventCatalogConfig.Generation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_appliedGeneration = EventCatalogConfig.Generation;
|
||||
EventCatalogConfig.FillDisasters(Entries);
|
||||
}
|
||||
|
||||
string id = string.IsNullOrEmpty(disasterId) ? "unknown" : disasterId.Trim();
|
||||
return new DisasterInterestEntry
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = 90f,
|
||||
Category = "Disaster",
|
||||
WorldLogId = "",
|
||||
CreatesInterest = true,
|
||||
IsFallback = true
|
||||
};
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool HasAuthored(string disasterId)
|
||||
{
|
||||
Ensure();
|
||||
return !string.IsNullOrEmpty(disasterId) && Entries.ContainsKey(disasterId.Trim());
|
||||
}
|
||||
|
||||
public static bool TryGet(string disasterId, out DisasterInterestEntry entry)
|
||||
{
|
||||
Ensure();
|
||||
entry = null;
|
||||
if (string.IsNullOrEmpty(disasterId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Entries.TryGetValue(disasterId.Trim(), out entry);
|
||||
}
|
||||
|
||||
public static DisasterInterestEntry GetOrFallback(string disasterId)
|
||||
{
|
||||
if (TryGet(disasterId, out DisasterInterestEntry entry))
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
|
||||
string id = string.IsNullOrEmpty(disasterId) ? "unknown" : disasterId.Trim();
|
||||
return new DisasterInterestEntry
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = 90f,
|
||||
Category = "Disaster",
|
||||
WorldLogId = "",
|
||||
CreatesInterest = true,
|
||||
IsFallback = true
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,66 +3,66 @@ using System.Collections.Generic;
|
|||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>Authored interest overlay for every live era_library asset.</summary>
|
||||
/// <summary>Era dials from <c>event-catalog.json</c> <c>eras</c>.</summary>
|
||||
public static partial class EventCatalog
|
||||
{
|
||||
public static class Era
|
||||
{
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
static Era()
|
||||
{
|
||||
Add("age_hope", 78f, "World", "Age of Hope");
|
||||
Add("age_sun", 80f, "World", "Age of Sun");
|
||||
Add("age_dark", 88f, "World", "Age of Dark");
|
||||
Add("age_tears", 86f, "World", "Age of Tears");
|
||||
Add("age_moon", 82f, "World", "Age of Moon");
|
||||
Add("age_chaos", 92f, "World", "Age of Chaos");
|
||||
Add("age_wonders", 84f, "World", "Age of Wonders");
|
||||
Add("age_ice", 87f, "World", "Age of Ice");
|
||||
Add("age_ash", 90f, "World", "Age of Ash");
|
||||
Add("age_despair", 91f, "World", "Age of Despair");
|
||||
Add("age_unknown", 70f, "World", "Unknown age");
|
||||
}
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static void Add(
|
||||
string id,
|
||||
float strength,
|
||||
string category,
|
||||
string label)
|
||||
{
|
||||
Entries[id] = new DiscreteEventEntry
|
||||
private static int _appliedGeneration = -1;
|
||||
|
||||
public static void InvalidateOverlays()
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = strength,
|
||||
Category = category,
|
||||
CreatesInterest = true,
|
||||
LabelTemplate = label
|
||||
};
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds => Entries.Keys;
|
||||
|
||||
public static bool HasAuthored(string id) =>
|
||||
!string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim());
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry))
|
||||
{
|
||||
return entry;
|
||||
Entries.Clear();
|
||||
_appliedGeneration = -1;
|
||||
}
|
||||
|
||||
string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim();
|
||||
return new DiscreteEventEntry
|
||||
private static void Ensure()
|
||||
{
|
||||
Id = key,
|
||||
EventStrength = 70f,
|
||||
Category = "World",
|
||||
LabelTemplate = "Era ({id})",
|
||||
IsFallback = true
|
||||
};
|
||||
if (_appliedGeneration == EventCatalogConfig.Generation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_appliedGeneration = EventCatalogConfig.Generation;
|
||||
EventCatalogConfig.FillEras(Entries);
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool HasAuthored(string id)
|
||||
{
|
||||
Ensure();
|
||||
return !string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim());
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry))
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
|
||||
string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim();
|
||||
return new DiscreteEventEntry
|
||||
{
|
||||
Id = key,
|
||||
EventStrength = 70f,
|
||||
Category = "World",
|
||||
LabelTemplate = "Era ({id})",
|
||||
CreatesInterest = true,
|
||||
IsFallback = true
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -3,428 +3,519 @@ using System.Collections.Generic;
|
|||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>Live AssetManager library event dials (powers, genes, …).</summary>
|
||||
/// <summary>
|
||||
/// Live AssetManager library event dials (powers, genes, …).
|
||||
/// Strengths/labels from <c>event-catalog.json</c> <c>libraries</c>; signal predicates stay in C#.
|
||||
/// </summary>
|
||||
public static partial class EventCatalog
|
||||
{
|
||||
private static LibraryCatalogDefaults LibOr(
|
||||
string key,
|
||||
float ambient,
|
||||
float signal,
|
||||
string category,
|
||||
string label,
|
||||
bool ambientCreatesInterest)
|
||||
{
|
||||
if (EventCatalogConfig.TryGetLibraryDefaults(key, out LibraryCatalogDefaults d) && d != null)
|
||||
{
|
||||
return d;
|
||||
}
|
||||
|
||||
return new LibraryCatalogDefaults
|
||||
{
|
||||
AmbientStrength = ambient,
|
||||
SignalStrength = signal,
|
||||
Category = category,
|
||||
Label = label,
|
||||
AmbientCreatesInterest = ambientCreatesInterest
|
||||
};
|
||||
}
|
||||
|
||||
public static class Spell
|
||||
{
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static readonly HashSet<string> SignalIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"summon_lightning", "summon_tornado", "cast_curse", "cast_fire",
|
||||
"cast_blood_rain", "summon_meteor", "teleport"
|
||||
};
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static void Ensure() =>
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLiveSpellIds,
|
||||
"Spectacle",
|
||||
"{a} casts {id}",
|
||||
ambientStrength: 48f,
|
||||
SignalIds,
|
||||
signalStrength: 88f,
|
||||
ambientCreatesInterest: true);
|
||||
private static int _appliedGeneration = -1;
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
public static void InvalidateOverlays()
|
||||
{
|
||||
Entries.Clear();
|
||||
_appliedGeneration = -1;
|
||||
}
|
||||
|
||||
private static void Ensure()
|
||||
{
|
||||
if (_appliedGeneration == EventCatalogConfig.Generation && Entries.Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Entries.Clear();
|
||||
_appliedGeneration = EventCatalogConfig.Generation;
|
||||
LibraryCatalogDefaults d = LibOr("spell", 48f, 88f, "Spectacle", "{a} casts {id}", true);
|
||||
HashSet<string> signals = d.SignalIds.Count > 0
|
||||
? d.SignalIds
|
||||
: new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"summon_lightning", "summon_tornado", "cast_curse", "cast_fire",
|
||||
"cast_blood_rain", "summon_meteor", "teleport"
|
||||
};
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLiveSpellIds,
|
||||
d.Category,
|
||||
d.Label,
|
||||
d.AmbientStrength,
|
||||
signals,
|
||||
d.SignalStrength,
|
||||
ambientCreatesInterest: d.AmbientCreatesInterest);
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
LibraryCatalogDefaults d = LibOr("spell", 48f, 88f, "Spectacle", "{a} casts {id}", true);
|
||||
return LiveLibraryInterest.Lookup(Entries, id, d.Category, d.Label, 55f);
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
return LiveLibraryInterest.Lookup(Entries, id, "Spectacle", "{a} casts {id}", 55f);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>God powers library - mostly Ambient; Signal for disaster/spectacle ids.</summary>
|
||||
public static class Power
|
||||
{
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsSpectaclePower(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static int _appliedGeneration = -1;
|
||||
|
||||
public static void InvalidateOverlays()
|
||||
{
|
||||
return false;
|
||||
Entries.Clear();
|
||||
_appliedGeneration = -1;
|
||||
}
|
||||
|
||||
string s = id.ToLowerInvariant();
|
||||
return s.Contains("meteor")
|
||||
|| s.Contains("earthquake")
|
||||
|| s.Contains("tornado")
|
||||
|| s.Contains("volcano")
|
||||
|| s.Contains("nuke")
|
||||
|| s.Contains("bomb")
|
||||
|| s.Contains("rain_blood")
|
||||
|| s.Contains("hell")
|
||||
|| s.Contains("lightning")
|
||||
|| s.Contains("storm")
|
||||
|| s.Contains("acid")
|
||||
|| s.Contains("fire")
|
||||
|| s.Contains("disaster");
|
||||
}
|
||||
private static bool IsSpectaclePower(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void Ensure() =>
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLivePowerIds,
|
||||
"Spectacle",
|
||||
"God power: {id}",
|
||||
ambientStrength: 28f,
|
||||
signalIds: null,
|
||||
signalStrength: 92f,
|
||||
signalPredicate: IsSpectaclePower,
|
||||
ambientCreatesInterest: false);
|
||||
string s = id.ToLowerInvariant();
|
||||
return s.Contains("meteor")
|
||||
|| s.Contains("earthquake")
|
||||
|| s.Contains("tornado")
|
||||
|| s.Contains("volcano")
|
||||
|| s.Contains("nuke")
|
||||
|| s.Contains("bomb")
|
||||
|| s.Contains("rain_blood")
|
||||
|| s.Contains("hell")
|
||||
|| s.Contains("lightning")
|
||||
|| s.Contains("storm")
|
||||
|| s.Contains("acid")
|
||||
|| s.Contains("fire")
|
||||
|| s.Contains("disaster");
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
private static void Ensure()
|
||||
{
|
||||
if (_appliedGeneration == EventCatalogConfig.Generation && Entries.Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Entries.Clear();
|
||||
_appliedGeneration = EventCatalogConfig.Generation;
|
||||
LibraryCatalogDefaults d = LibOr("power", 28f, 92f, "Spectacle", "God power: {id}", false);
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLivePowerIds,
|
||||
d.Category,
|
||||
d.Label,
|
||||
d.AmbientStrength,
|
||||
signalIds: null,
|
||||
d.SignalStrength,
|
||||
signalPredicate: IsSpectaclePower,
|
||||
ambientCreatesInterest: d.AmbientCreatesInterest);
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
LibraryCatalogDefaults d = LibOr("power", 28f, 92f, "Spectacle", "God power: {id}", false);
|
||||
return LiveLibraryInterest.Lookup(Entries, id, d.Category, d.Label, 30f);
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
return LiveLibraryInterest.Lookup(Entries, id, "Spectacle", "God power: {id}", 30f);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>AI decisions - Signal for war/rebellion/kingdom-affecting; Ambient otherwise.</summary>
|
||||
public static class Decision
|
||||
{
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsKingdomDecision(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string s = id.ToLowerInvariant();
|
||||
// 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");
|
||||
}
|
||||
|
||||
/// <summary>True when a decision id should be allowed to own the idle camera.</summary>
|
||||
public static bool IsCameraWorthy(string id) => IsKingdomDecision(id);
|
||||
|
||||
private static void Ensure() =>
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLiveDecisionIds,
|
||||
"Politics",
|
||||
"{a} decides {id}",
|
||||
ambientStrength: 40f,
|
||||
signalIds: null,
|
||||
signalStrength: 86f,
|
||||
signalPredicate: IsKingdomDecision,
|
||||
ambientCreatesInterest: false);
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
return LiveLibraryInterest.Lookup(Entries, id, "Politics", "{a} decides {id}", 35f);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Items library - Signal for legendary/wonder subset.</summary>
|
||||
public static class Item
|
||||
{
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsLegendaryItem(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static int _appliedGeneration = -1;
|
||||
|
||||
public static void InvalidateOverlays()
|
||||
{
|
||||
return false;
|
||||
Entries.Clear();
|
||||
_appliedGeneration = -1;
|
||||
}
|
||||
|
||||
string s = id.ToLowerInvariant();
|
||||
return s.Contains("legendary")
|
||||
|| s.Contains("mythic")
|
||||
|| s.Contains("artifact")
|
||||
|| s.Contains("relic")
|
||||
|| s.Contains("divine")
|
||||
|| s.Contains("demon")
|
||||
|| s.Contains("dragon")
|
||||
|| s.Contains("nuke")
|
||||
|| s.Contains("excalibur")
|
||||
|| s.Contains("wonder");
|
||||
}
|
||||
private static bool IsLegendaryItem(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void Ensure() =>
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLiveItemIds,
|
||||
"Item",
|
||||
"{a} forges {id}",
|
||||
ambientStrength: 36f,
|
||||
signalIds: null,
|
||||
signalStrength: 80f,
|
||||
signalPredicate: IsLegendaryItem,
|
||||
ambientCreatesInterest: 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<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
private static void Ensure()
|
||||
{
|
||||
if (_appliedGeneration == EventCatalogConfig.Generation && Entries.Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Entries.Clear();
|
||||
_appliedGeneration = EventCatalogConfig.Generation;
|
||||
LibraryCatalogDefaults d = LibOr("item", 36f, 80f, "Item", "{a} forges {id}", true);
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLiveItemIds,
|
||||
d.Category,
|
||||
d.Label,
|
||||
d.AmbientStrength,
|
||||
signalIds: null,
|
||||
d.SignalStrength,
|
||||
signalPredicate: IsLegendaryItem,
|
||||
ambientCreatesInterest: d.AmbientCreatesInterest);
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
LibraryCatalogDefaults d = LibOr("item", 36f, 80f, "Item", "{a} forges {id}", true);
|
||||
return LiveLibraryInterest.Lookup(Entries, id, d.Category, d.Label, 40f);
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
return LiveLibraryInterest.Lookup(Entries, id, "Item", "{a} forges {id}", 40f);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Subspecies traits - Signal for dramatic; Ambient fill-capped.</summary>
|
||||
public static class SubspeciesTrait
|
||||
{
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsDramatic(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static int _appliedGeneration = -1;
|
||||
|
||||
public static void InvalidateOverlays()
|
||||
{
|
||||
return false;
|
||||
Entries.Clear();
|
||||
_appliedGeneration = -1;
|
||||
}
|
||||
|
||||
string s = id.ToLowerInvariant();
|
||||
return s.Contains("immortal")
|
||||
|| s.Contains("giant")
|
||||
|| s.Contains("tiny")
|
||||
|| s.Contains("fire")
|
||||
|| s.Contains("frost")
|
||||
|| s.Contains("poison")
|
||||
|| s.Contains("plague")
|
||||
|| s.Contains("magic")
|
||||
|| s.Contains("divine")
|
||||
|| s.Contains("demon")
|
||||
|| s.Contains("undead")
|
||||
|| s.Contains("dragon")
|
||||
|| s.Contains("evolve")
|
||||
|| s.Contains("mutate");
|
||||
}
|
||||
private static bool IsDramatic(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void Ensure() =>
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLiveSubspeciesTraitIds,
|
||||
"Evolution",
|
||||
"{a} evolves {id}",
|
||||
ambientStrength: 34f,
|
||||
signalIds: null,
|
||||
signalStrength: 78f,
|
||||
signalPredicate: IsDramatic,
|
||||
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<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
private static void Ensure()
|
||||
{
|
||||
if (_appliedGeneration == EventCatalogConfig.Generation && Entries.Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Entries.Clear();
|
||||
_appliedGeneration = EventCatalogConfig.Generation;
|
||||
LibraryCatalogDefaults d = LibOr("subspeciesTrait", 34f, 78f, "Evolution", "{a} evolves {id}", true);
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLiveSubspeciesTraitIds,
|
||||
d.Category,
|
||||
d.Label,
|
||||
d.AmbientStrength,
|
||||
signalIds: null,
|
||||
d.SignalStrength,
|
||||
signalPredicate: IsDramatic,
|
||||
ambientCreatesInterest: d.AmbientCreatesInterest);
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
LibraryCatalogDefaults d = LibOr("subspeciesTrait", 34f, 78f, "Evolution", "{a} evolves {id}", true);
|
||||
return LiveLibraryInterest.Lookup(Entries, id, d.Category, d.Label, 36f);
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
return LiveLibraryInterest.Lookup(Entries, id, "Evolution", "{a} evolves {id}", 36f);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Genes - Ambient default.</summary>
|
||||
public static class Gene
|
||||
{
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static void Ensure() =>
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLiveGeneIds,
|
||||
"Genetics",
|
||||
"{a} gains gene {id}",
|
||||
ambientStrength: 30f,
|
||||
signalIds: null,
|
||||
signalStrength: 70f,
|
||||
signalPredicate: id => id != null && (id.Contains("warfare") || id.Contains("magic") || id.Contains("mutation")),
|
||||
ambientCreatesInterest: true);
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static int _appliedGeneration = -1;
|
||||
|
||||
public static void InvalidateOverlays()
|
||||
{
|
||||
Entries.Clear();
|
||||
_appliedGeneration = -1;
|
||||
}
|
||||
|
||||
private static void Ensure()
|
||||
{
|
||||
if (_appliedGeneration == EventCatalogConfig.Generation && Entries.Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Entries.Clear();
|
||||
_appliedGeneration = EventCatalogConfig.Generation;
|
||||
LibraryCatalogDefaults d = LibOr("gene", 30f, 70f, "Genetics", "{a} gains gene {id}", true);
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLiveGeneIds,
|
||||
d.Category,
|
||||
d.Label,
|
||||
d.AmbientStrength,
|
||||
signalIds: null,
|
||||
d.SignalStrength,
|
||||
signalPredicate: id => id != null && (id.Contains("warfare") || id.Contains("magic") || id.Contains("mutation")),
|
||||
ambientCreatesInterest: d.AmbientCreatesInterest);
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
LibraryCatalogDefaults d = LibOr("gene", 30f, 70f, "Genetics", "{a} gains gene {id}", true);
|
||||
return LiveLibraryInterest.Lookup(Entries, id, d.Category, d.Label, 30f);
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
return LiveLibraryInterest.Lookup(Entries, id, "Genetics", "{a} gains gene {id}", 30f);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Phenotypes - Ambient default.</summary>
|
||||
public static class Phenotype
|
||||
{
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static void Ensure() =>
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLivePhenotypeIds,
|
||||
"Genetics",
|
||||
"{a} shows {id}",
|
||||
ambientStrength: 28f,
|
||||
signalIds: null,
|
||||
signalStrength: 60f,
|
||||
ambientCreatesInterest: true);
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static int _appliedGeneration = -1;
|
||||
|
||||
public static void InvalidateOverlays()
|
||||
{
|
||||
Entries.Clear();
|
||||
_appliedGeneration = -1;
|
||||
}
|
||||
|
||||
private static void Ensure()
|
||||
{
|
||||
if (_appliedGeneration == EventCatalogConfig.Generation && Entries.Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Entries.Clear();
|
||||
_appliedGeneration = EventCatalogConfig.Generation;
|
||||
LibraryCatalogDefaults d = LibOr("phenotype", 28f, 60f, "Genetics", "{a} shows {id}", true);
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLivePhenotypeIds,
|
||||
d.Category,
|
||||
d.Label,
|
||||
d.AmbientStrength,
|
||||
signalIds: null,
|
||||
d.SignalStrength,
|
||||
ambientCreatesInterest: d.AmbientCreatesInterest);
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
LibraryCatalogDefaults d = LibOr("phenotype", 28f, 60f, "Genetics", "{a} shows {id}", true);
|
||||
return LiveLibraryInterest.Lookup(Entries, id, d.Category, d.Label, 28f);
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
return LiveLibraryInterest.Lookup(Entries, id, "Genetics", "{a} shows {id}", 28f);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>World laws - Ambient / location-only style.</summary>
|
||||
public static class WorldLaw
|
||||
{
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static void Ensure() =>
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLiveWorldLawIds,
|
||||
"WorldLaw",
|
||||
"Law changed: {id}",
|
||||
ambientStrength: 40f,
|
||||
signalIds: null,
|
||||
signalStrength: 70f,
|
||||
signalPredicate: id => id != null && (id.Contains("war") || id.Contains("death") || id.Contains("plague")),
|
||||
ambientCreatesInterest: true);
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static int _appliedGeneration = -1;
|
||||
|
||||
public static void InvalidateOverlays()
|
||||
{
|
||||
Entries.Clear();
|
||||
_appliedGeneration = -1;
|
||||
}
|
||||
|
||||
private static void Ensure()
|
||||
{
|
||||
if (_appliedGeneration == EventCatalogConfig.Generation && Entries.Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Entries.Clear();
|
||||
_appliedGeneration = EventCatalogConfig.Generation;
|
||||
LibraryCatalogDefaults d = LibOr("worldLaw", 40f, 70f, "WorldLaw", "Law changed: {id}", true);
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLiveWorldLawIds,
|
||||
d.Category,
|
||||
d.Label,
|
||||
d.AmbientStrength,
|
||||
signalIds: null,
|
||||
d.SignalStrength,
|
||||
signalPredicate: id => id != null && (id.Contains("war") || id.Contains("death") || id.Contains("plague")),
|
||||
ambientCreatesInterest: d.AmbientCreatesInterest);
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
LibraryCatalogDefaults d = LibOr("worldLaw", 40f, 70f, "WorldLaw", "Law changed: {id}", true);
|
||||
return LiveLibraryInterest.Lookup(Entries, id, d.Category, d.Label, 40f);
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
return LiveLibraryInterest.Lookup(Entries, id, "WorldLaw", "Law changed: {id}", 40f);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Biomes - Ambient.</summary>
|
||||
public static class Biome
|
||||
{
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static void Ensure() =>
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLiveBiomeIds,
|
||||
"Biome",
|
||||
"Biome shifts: {id}",
|
||||
ambientStrength: 26f,
|
||||
signalIds: null,
|
||||
signalStrength: 50f,
|
||||
ambientCreatesInterest: false);
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static int _appliedGeneration = -1;
|
||||
|
||||
public static void InvalidateOverlays()
|
||||
{
|
||||
Entries.Clear();
|
||||
_appliedGeneration = -1;
|
||||
}
|
||||
|
||||
private static void Ensure()
|
||||
{
|
||||
if (_appliedGeneration == EventCatalogConfig.Generation && Entries.Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Entries.Clear();
|
||||
_appliedGeneration = EventCatalogConfig.Generation;
|
||||
LibraryCatalogDefaults d = LibOr("biome", 26f, 50f, "Biome", "Biome shifts: {id}", false);
|
||||
LiveLibraryInterest.EnsureSeeded(
|
||||
Entries,
|
||||
ActivityAssetCatalog.EnumerateLiveBiomeIds,
|
||||
d.Category,
|
||||
d.Label,
|
||||
d.AmbientStrength,
|
||||
signalIds: null,
|
||||
d.SignalStrength,
|
||||
ambientCreatesInterest: d.AmbientCreatesInterest);
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
LibraryCatalogDefaults d = LibOr("biome", 26f, 50f, "Biome", "Biome shifts: {id}", false);
|
||||
return LiveLibraryInterest.Lookup(Entries, id, d.Category, d.Label, 26f);
|
||||
}
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
return LiveLibraryInterest.Lookup(Entries, id, "Biome", "Biome shifts: {id}", 26f);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,83 +3,66 @@ using System.Collections.Generic;
|
|||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>Authored interest overlay for every live plots_library asset.</summary>
|
||||
/// <summary>Plot dials from <c>event-catalog.json</c> <c>plots</c>.</summary>
|
||||
public static partial class EventCatalog
|
||||
{
|
||||
public static class Plot
|
||||
{
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
static Plot()
|
||||
{
|
||||
Add("rebellion", 92f, "Politics", "{a} is plotting a rebellion");
|
||||
Add("new_war", 94f, "Politics", "{a} is plotting a war");
|
||||
Add("alliance_create", 80f, "Politics", "{a} is plotting an alliance");
|
||||
Add("alliance_join", 72f, "Politics", "{a} is joining an alliance");
|
||||
Add("alliance_destroy", 86f, "Politics", "{a} is breaking an alliance");
|
||||
Add("attacker_stop_war", 78f, "Politics", "{a} is ending a war");
|
||||
Add("new_book", 55f, "Culture", "{a} is writing a book");
|
||||
Add("new_language", 62f, "Culture", "{a} is forging a language");
|
||||
Add("new_religion", 70f, "Culture", "{a} is founding a religion");
|
||||
Add("new_culture", 68f, "Culture", "{a} is founding a culture");
|
||||
Add("clan_ascension", 82f, "Politics", "{a} is ascending a clan");
|
||||
Add("culture_divide", 75f, "Culture", "{a} splits a culture");
|
||||
Add("religion_schism", 84f, "Culture", "{a} causes a religion schism");
|
||||
Add("language_divergence", 60f, "Culture", "{a} splits a language");
|
||||
Add("summon_meteor_rain", 96f, "Spectacle", "{a} summons a meteor rain");
|
||||
Add("summon_earthquake", 95f, "Spectacle", "{a} summons an earthquake");
|
||||
Add("summon_thunderstorm", 93f, "Spectacle", "{a} summons a thunderstorm");
|
||||
Add("summon_stormfront", 93f, "Spectacle", "{a} summons a stormfront");
|
||||
Add("summon_hellstorm", 97f, "Spectacle", "{a} summons a hellstorm");
|
||||
Add("summon_demons", 96f, "Spectacle", "{a} summons demons");
|
||||
Add("summon_angles", 96f, "Spectacle", "{a} summons angels");
|
||||
Add("summon_skeletons", 92f, "Spectacle", "{a} summons skeletons");
|
||||
Add("summon_living_plants", 90f, "Spectacle", "{a} summons living plants");
|
||||
Add("big_cast_coffee", 58f, "Spectacle", "{a} performs a coffee ritual");
|
||||
Add("big_cast_bubble_shield", 64f, "Spectacle", "{a} casts a bubble shield");
|
||||
Add("big_cast_madness", 80f, "Spectacle", "{a} casts madness");
|
||||
Add("big_cast_slowness", 70f, "Spectacle", "{a} casts slowness");
|
||||
Add("cause_rebellion", 90f, "Politics", "{a} causes a rebellion");
|
||||
}
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static void Add(
|
||||
string id,
|
||||
float strength,
|
||||
string category,
|
||||
string label)
|
||||
{
|
||||
Entries[id] = new DiscreteEventEntry
|
||||
private static int _appliedGeneration = -1;
|
||||
|
||||
public static void InvalidateOverlays()
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = strength,
|
||||
Category = category,
|
||||
CreatesInterest = true,
|
||||
LabelTemplate = label
|
||||
};
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds => Entries.Keys;
|
||||
|
||||
public static bool HasAuthored(string id) =>
|
||||
!string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim());
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry))
|
||||
{
|
||||
return entry;
|
||||
Entries.Clear();
|
||||
_appliedGeneration = -1;
|
||||
}
|
||||
|
||||
string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim();
|
||||
return new DiscreteEventEntry
|
||||
private static void Ensure()
|
||||
{
|
||||
Id = key,
|
||||
EventStrength = 55f,
|
||||
Category = "Plot",
|
||||
LabelTemplate = "{a} · {id}",
|
||||
IsFallback = true
|
||||
};
|
||||
if (_appliedGeneration == EventCatalogConfig.Generation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_appliedGeneration = EventCatalogConfig.Generation;
|
||||
EventCatalogConfig.FillPlots(Entries);
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool HasAuthored(string id)
|
||||
{
|
||||
Ensure();
|
||||
return !string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim());
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry))
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
|
||||
string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim();
|
||||
return new DiscreteEventEntry
|
||||
{
|
||||
Id = key,
|
||||
EventStrength = 55f,
|
||||
Category = "Plot",
|
||||
LabelTemplate = "{a} · {id}",
|
||||
CreatesInterest = false,
|
||||
IsFallback = true
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,69 +3,66 @@ using System.Collections.Generic;
|
|||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>Authored discrete relationship lifecycle events (not a live asset library).</summary>
|
||||
/// <summary>Relationship dials from <c>event-catalog.json</c> <c>relationship</c>.</summary>
|
||||
public static partial class EventCatalog
|
||||
{
|
||||
public static class Relationship
|
||||
{
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
static Relationship()
|
||||
{
|
||||
Add("set_lover", 72f, "Relationship", "{a} became lovers with {b}");
|
||||
Add("clear_lover", 55f, "Relationship", "{a} parted from a lover");
|
||||
Add("add_child", 68f, "Relationship", "{a} gains a child, {b}");
|
||||
Add("new_family", 70f, "Relationship", "{a} starts a new family");
|
||||
Add("family_removed", 50f, "Relationship", "{a}'s family ends");
|
||||
// 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<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static void Add(
|
||||
string id,
|
||||
float strength,
|
||||
string category,
|
||||
string label,
|
||||
bool createsInterest = true)
|
||||
{
|
||||
Entries[id] = new DiscreteEventEntry
|
||||
private static int _appliedGeneration = -1;
|
||||
|
||||
public static void InvalidateOverlays()
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = strength,
|
||||
Category = category,
|
||||
CreatesInterest = createsInterest,
|
||||
LabelTemplate = label
|
||||
};
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds => Entries.Keys;
|
||||
|
||||
public static bool HasAuthored(string id) =>
|
||||
!string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim());
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry))
|
||||
{
|
||||
return entry;
|
||||
Entries.Clear();
|
||||
_appliedGeneration = -1;
|
||||
}
|
||||
|
||||
string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim();
|
||||
return new DiscreteEventEntry
|
||||
private static void Ensure()
|
||||
{
|
||||
Id = key,
|
||||
EventStrength = 40f,
|
||||
Category = "Relationship",
|
||||
LabelTemplate = "{a} · {id}",
|
||||
CreatesInterest = false,
|
||||
IsFallback = true
|
||||
};
|
||||
if (_appliedGeneration == EventCatalogConfig.Generation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_appliedGeneration = EventCatalogConfig.Generation;
|
||||
EventCatalogConfig.FillRelationship(Entries);
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool HasAuthored(string id)
|
||||
{
|
||||
Ensure();
|
||||
return !string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim());
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry))
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
|
||||
string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim();
|
||||
return new DiscreteEventEntry
|
||||
{
|
||||
Id = key,
|
||||
EventStrength = 40f,
|
||||
Category = "Relationship",
|
||||
LabelTemplate = "{a} · {id}",
|
||||
CreatesInterest = false,
|
||||
IsFallback = true
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,142 +13,76 @@ public sealed class StatusInterestEntry
|
|||
public bool IsFallback;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <summary>Status interest dials from <c>event-catalog.json</c> <c>status</c>.</summary>
|
||||
public static partial class EventCatalog
|
||||
{
|
||||
public static class Status
|
||||
{
|
||||
private static readonly Dictionary<string, StatusInterestEntry> Entries =
|
||||
new Dictionary<string, StatusInterestEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
static Status()
|
||||
{
|
||||
// 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<string, StatusInterestEntry> Entries =
|
||||
new Dictionary<string, StatusInterestEntry>(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<string> AuthoredIds => Entries.Keys;
|
||||
|
||||
public static bool HasAuthored(string statusId)
|
||||
{
|
||||
return !string.IsNullOrEmpty(statusId) && Entries.ContainsKey(statusId.Trim());
|
||||
}
|
||||
|
||||
public static bool TryGet(string statusId, out StatusInterestEntry entry)
|
||||
{
|
||||
entry = null;
|
||||
if (string.IsNullOrEmpty(statusId))
|
||||
{
|
||||
return false;
|
||||
Entries.Clear();
|
||||
_appliedGeneration = -1;
|
||||
}
|
||||
|
||||
return Entries.TryGetValue(statusId.Trim(), out entry);
|
||||
}
|
||||
|
||||
public static StatusInterestEntry GetOrFallback(string statusId)
|
||||
{
|
||||
if (TryGet(statusId, out StatusInterestEntry entry))
|
||||
private static void Ensure()
|
||||
{
|
||||
return entry;
|
||||
if (_appliedGeneration == EventCatalogConfig.Generation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_appliedGeneration = EventCatalogConfig.Generation;
|
||||
EventCatalogConfig.FillStatus(Entries);
|
||||
}
|
||||
|
||||
string id = string.IsNullOrEmpty(statusId) ? "unknown" : statusId.Trim();
|
||||
return new StatusInterestEntry
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = 40f,
|
||||
Category = "Status",
|
||||
CreatesInterest = false,
|
||||
IsFallback = true
|
||||
};
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool HasAuthored(string statusId)
|
||||
{
|
||||
Ensure();
|
||||
return !string.IsNullOrEmpty(statusId) && Entries.ContainsKey(statusId.Trim());
|
||||
}
|
||||
|
||||
public static bool TryGet(string statusId, out StatusInterestEntry entry)
|
||||
{
|
||||
Ensure();
|
||||
entry = null;
|
||||
if (string.IsNullOrEmpty(statusId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Entries.TryGetValue(statusId.Trim(), out entry);
|
||||
}
|
||||
|
||||
public static StatusInterestEntry GetOrFallback(string statusId)
|
||||
{
|
||||
if (TryGet(statusId, out StatusInterestEntry entry))
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
|
||||
string id = string.IsNullOrEmpty(statusId) ? "unknown" : statusId.Trim();
|
||||
return new StatusInterestEntry
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = 40f,
|
||||
Category = "Status",
|
||||
CreatesInterest = false,
|
||||
IsFallback = true
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,167 +3,66 @@ using System.Collections.Generic;
|
|||
|
||||
namespace IdleSpectator;
|
||||
|
||||
/// <summary>Authored interest overlay for every live traits library asset.</summary>
|
||||
/// <summary>Trait dials from <c>event-catalog.json</c> <c>traits</c>.</summary>
|
||||
public static partial class EventCatalog
|
||||
{
|
||||
public static class Trait
|
||||
{
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
static Trait()
|
||||
{
|
||||
Add("zombie", 82f);
|
||||
Add("wise", 42f);
|
||||
Add("whirlwind", 82f);
|
||||
Add("weightless", 42f);
|
||||
Add("weak", 42f);
|
||||
Add("veteran", 42f);
|
||||
Add("venomous", 82f);
|
||||
Add("unlucky", 42f);
|
||||
Add("ugly", 42f);
|
||||
Add("tumor_infection", 82f);
|
||||
Add("tough", 42f);
|
||||
Add("titan_lungs", 42f);
|
||||
Add("tiny", 42f);
|
||||
Add("thorns", 42f);
|
||||
Add("thief", 42f);
|
||||
Add("super_health", 42f);
|
||||
Add("sunblessed", 42f);
|
||||
Add("stupid", 42f);
|
||||
Add("strong_minded", 42f);
|
||||
Add("strong", 42f);
|
||||
Add("soft_skin", 42f);
|
||||
Add("slow", 42f);
|
||||
Add("skin_burns", 42f);
|
||||
Add("short_sighted", 42f);
|
||||
Add("shiny", 42f);
|
||||
Add("scar_of_divinity", 82f);
|
||||
Add("savage", 82f);
|
||||
Add("regeneration", 42f);
|
||||
Add("pyromaniac", 82f);
|
||||
Add("psychopath", 82f);
|
||||
Add("poisonous", 82f);
|
||||
Add("poison_immune", 42f);
|
||||
Add("plague", 82f);
|
||||
Add("peaceful", 42f);
|
||||
Add("paranoid", 42f);
|
||||
Add("pacifist", 42f);
|
||||
Add("nightchild", 42f);
|
||||
Add("mute", 42f);
|
||||
Add("mush_spores", 82f);
|
||||
Add("moonchild", 42f);
|
||||
Add("miracle_born", 82f);
|
||||
Add("miracle_bearer", 82f);
|
||||
Add("miner", 42f);
|
||||
Add("metamorphed", 42f);
|
||||
Add("mega_heartbeat", 42f);
|
||||
Add("mageslayer", 82f);
|
||||
Add("madness", 82f);
|
||||
Add("lustful", 42f);
|
||||
Add("lucky", 42f);
|
||||
Add("long_liver", 42f);
|
||||
Add("light_lamp", 42f);
|
||||
Add("kingslayer", 82f);
|
||||
Add("infertile", 42f);
|
||||
Add("infected", 82f);
|
||||
Add("immune", 42f);
|
||||
Add("immortal", 82f);
|
||||
Add("hotheaded", 42f);
|
||||
Add("honest", 42f);
|
||||
Add("heliophobia", 42f);
|
||||
Add("heart_of_wizard", 42f);
|
||||
Add("healing_aura", 42f);
|
||||
Add("hard_skin", 42f);
|
||||
Add("greedy", 42f);
|
||||
Add("golden_tooth", 42f);
|
||||
Add("gluttonous", 42f);
|
||||
Add("giant", 82f);
|
||||
Add("genius", 42f);
|
||||
Add("freeze_proof", 42f);
|
||||
Add("fragile_health", 42f);
|
||||
Add("flower_prints", 42f);
|
||||
Add("flesh_eater", 82f);
|
||||
Add("fire_proof", 42f);
|
||||
Add("fire_blood", 82f);
|
||||
Add("fertile", 42f);
|
||||
Add("fat", 42f);
|
||||
Add("fast", 42f);
|
||||
Add("eyepatch", 42f);
|
||||
Add("evil", 82f);
|
||||
Add("energized", 42f);
|
||||
Add("eagle_eyed", 42f);
|
||||
Add("dragonslayer", 82f);
|
||||
Add("dodge", 42f);
|
||||
Add("desire_harp", 42f);
|
||||
Add("desire_golden_egg", 42f);
|
||||
Add("desire_computer", 42f);
|
||||
Add("desire_alien_mold", 42f);
|
||||
Add("deflect_projectile", 42f);
|
||||
Add("deceitful", 42f);
|
||||
Add("death_nuke", 82f);
|
||||
Add("death_mark", 82f);
|
||||
Add("death_bomb", 82f);
|
||||
Add("dash", 42f);
|
||||
Add("crippled", 42f);
|
||||
Add("content", 42f);
|
||||
Add("contagious", 82f);
|
||||
Add("cold_aura", 82f);
|
||||
Add("clumsy", 42f);
|
||||
Add("clone", 42f);
|
||||
Add("chosen_one", 82f);
|
||||
Add("burning_feet", 82f);
|
||||
Add("bubble_defense", 42f);
|
||||
Add("boosted_vitality", 42f);
|
||||
Add("bomberman", 82f);
|
||||
Add("boat", 42f);
|
||||
Add("bloodlust", 82f);
|
||||
Add("block", 42f);
|
||||
Add("blessed", 42f);
|
||||
Add("battle_reflexes", 42f);
|
||||
Add("backstep", 42f);
|
||||
Add("attractive", 42f);
|
||||
Add("arcane_reflexes", 42f);
|
||||
Add("ambitious", 42f);
|
||||
Add("agile", 42f);
|
||||
Add("acid_touch", 82f);
|
||||
Add("acid_proof", 42f);
|
||||
Add("acid_blood", 42f);
|
||||
}
|
||||
private static readonly Dictionary<string, DiscreteEventEntry> Entries =
|
||||
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static void Add(string id, float strength)
|
||||
{
|
||||
Entries[id] = new DiscreteEventEntry
|
||||
private static int _appliedGeneration = -1;
|
||||
|
||||
public static void InvalidateOverlays()
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = strength,
|
||||
Category = "Trait",
|
||||
CreatesInterest = true,
|
||||
LabelTemplate = "Trait: {id} ({a})"
|
||||
};
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds => Entries.Keys;
|
||||
|
||||
public static bool HasAuthored(string id) =>
|
||||
!string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim());
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry))
|
||||
{
|
||||
return entry;
|
||||
Entries.Clear();
|
||||
_appliedGeneration = -1;
|
||||
}
|
||||
|
||||
string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim();
|
||||
return new DiscreteEventEntry
|
||||
private static void Ensure()
|
||||
{
|
||||
Id = key,
|
||||
EventStrength = 40f,
|
||||
Category = "Trait",
|
||||
LabelTemplate = "Trait: {id} ({a})",
|
||||
IsFallback = true
|
||||
};
|
||||
if (_appliedGeneration == EventCatalogConfig.Generation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_appliedGeneration = EventCatalogConfig.Generation;
|
||||
EventCatalogConfig.FillTraits(Entries);
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool HasAuthored(string id)
|
||||
{
|
||||
Ensure();
|
||||
return !string.IsNullOrEmpty(id) && Entries.ContainsKey(id.Trim());
|
||||
}
|
||||
|
||||
public static DiscreteEventEntry GetOrFallback(string id)
|
||||
{
|
||||
Ensure();
|
||||
if (!string.IsNullOrEmpty(id) && Entries.TryGetValue(id.Trim(), out DiscreteEventEntry entry))
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
|
||||
string key = string.IsNullOrEmpty(id) ? "unknown" : id.Trim();
|
||||
return new DiscreteEventEntry
|
||||
{
|
||||
Id = key,
|
||||
EventStrength = 40f,
|
||||
Category = "Trait",
|
||||
LabelTemplate = "Trait: {id} ({a})",
|
||||
CreatesInterest = true,
|
||||
IsFallback = true
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,83 +13,90 @@ public sealed class WarTypeInterestEntry
|
|||
public bool IsFallback;
|
||||
}
|
||||
|
||||
/// <summary>Authored interest overlay for every live war_types_library asset.</summary>
|
||||
/// <summary>War type dials from <c>event-catalog.json</c> <c>warTypes</c>.</summary>
|
||||
public static partial class EventCatalog
|
||||
{
|
||||
public static class WarType
|
||||
{
|
||||
private static readonly Dictionary<string, WarTypeInterestEntry> Entries =
|
||||
new Dictionary<string, WarTypeInterestEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
static WarType()
|
||||
{
|
||||
Add("normal", 90f, "War: {a}");
|
||||
Add("spite", 88f, "Spite war: {a}");
|
||||
Add("inspire", 86f, "Inspired war: {a}");
|
||||
Add("rebellion", 92f, "Rebellion: {a}");
|
||||
Add("whisper_of_war", 84f, "Whisper of war: {a}");
|
||||
}
|
||||
private static readonly Dictionary<string, WarTypeInterestEntry> Entries =
|
||||
new Dictionary<string, WarTypeInterestEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static void Add(string id, float strength, string labelTemplate)
|
||||
{
|
||||
Entries[id] = new WarTypeInterestEntry
|
||||
private static int _appliedGeneration = -1;
|
||||
|
||||
public static void InvalidateOverlays()
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = strength,
|
||||
Category = "Politics",
|
||||
LabelTemplate = labelTemplate,
|
||||
CreatesInterest = true
|
||||
};
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds => Entries.Keys;
|
||||
|
||||
public static bool HasAuthored(string warTypeId)
|
||||
{
|
||||
return !string.IsNullOrEmpty(warTypeId) && Entries.ContainsKey(warTypeId.Trim());
|
||||
}
|
||||
|
||||
public static bool TryGet(string warTypeId, out WarTypeInterestEntry entry)
|
||||
{
|
||||
entry = null;
|
||||
if (string.IsNullOrEmpty(warTypeId))
|
||||
{
|
||||
return false;
|
||||
Entries.Clear();
|
||||
_appliedGeneration = -1;
|
||||
}
|
||||
|
||||
return Entries.TryGetValue(warTypeId.Trim(), out entry);
|
||||
}
|
||||
|
||||
public static WarTypeInterestEntry GetOrFallback(string warTypeId)
|
||||
{
|
||||
if (TryGet(warTypeId, out WarTypeInterestEntry entry))
|
||||
private static void Ensure()
|
||||
{
|
||||
return entry;
|
||||
if (_appliedGeneration == EventCatalogConfig.Generation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_appliedGeneration = EventCatalogConfig.Generation;
|
||||
EventCatalogConfig.FillWarTypes(Entries);
|
||||
}
|
||||
|
||||
string id = string.IsNullOrEmpty(warTypeId) ? "unknown" : warTypeId.Trim();
|
||||
return new WarTypeInterestEntry
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = 85f,
|
||||
Category = "Politics",
|
||||
LabelTemplate = "War ({id}): {a}",
|
||||
CreatesInterest = true,
|
||||
IsFallback = true
|
||||
};
|
||||
}
|
||||
|
||||
public static string MakeLabel(WarTypeInterestEntry entry, string special1)
|
||||
{
|
||||
if (entry == null)
|
||||
{
|
||||
return "War";
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
string template = string.IsNullOrEmpty(entry.LabelTemplate) ? "War ({id})" : entry.LabelTemplate;
|
||||
return template
|
||||
.Replace("{id}", entry.Id ?? "")
|
||||
.Replace("{a}", special1 ?? "");
|
||||
public static bool HasAuthored(string warTypeId)
|
||||
{
|
||||
Ensure();
|
||||
return !string.IsNullOrEmpty(warTypeId) && Entries.ContainsKey(warTypeId.Trim());
|
||||
}
|
||||
|
||||
public static bool TryGet(string warTypeId, out WarTypeInterestEntry entry)
|
||||
{
|
||||
Ensure();
|
||||
entry = null;
|
||||
if (string.IsNullOrEmpty(warTypeId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Entries.TryGetValue(warTypeId.Trim(), out entry);
|
||||
}
|
||||
|
||||
public static WarTypeInterestEntry GetOrFallback(string warTypeId)
|
||||
{
|
||||
if (TryGet(warTypeId, out WarTypeInterestEntry entry))
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
|
||||
string id = string.IsNullOrEmpty(warTypeId) ? "unknown" : warTypeId.Trim();
|
||||
return new WarTypeInterestEntry
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = 85f,
|
||||
Category = "Politics",
|
||||
LabelTemplate = "War ({id}): {a}",
|
||||
CreatesInterest = true,
|
||||
IsFallback = true
|
||||
};
|
||||
}
|
||||
|
||||
public static string MakeLabel(WarTypeInterestEntry entry, string special1)
|
||||
{
|
||||
if (entry == null)
|
||||
{
|
||||
return "War";
|
||||
}
|
||||
|
||||
string template = string.IsNullOrEmpty(entry.LabelTemplate) ? "War ({id})" : entry.LabelTemplate;
|
||||
return template
|
||||
.Replace("{id}", entry.Id ?? "")
|
||||
.Replace("{a}", special1 ?? "");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,155 +39,105 @@ public sealed class WorldLogEventEntry
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Authored policy for every live WorldLog asset. Game library is inventory; this is presentation/scoring only.
|
||||
/// </summary>
|
||||
/// <summary>WorldLog dials from <c>event-catalog.json</c> <c>worldLog</c>.</summary>
|
||||
public static partial class EventCatalog
|
||||
{
|
||||
public static class WorldLog
|
||||
{
|
||||
private static readonly Dictionary<string, WorldLogEventEntry> Entries =
|
||||
new Dictionary<string, WorldLogEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
static WorldLog()
|
||||
{
|
||||
// Kings
|
||||
Add("king_new", 78f, "Politics", true, "New king: {b} ({a})", 6f, 28f);
|
||||
Add("king_left", 65f, "Politics", true, "King left: {b} ({a})", 6f, 28f);
|
||||
Add("king_fled_capital", 70f, "Politics", true, "King fled capital: {b}", 6f, 28f);
|
||||
Add("king_fled_city", 68f, "Politics", true, "King fled city: {b}", 6f, 28f);
|
||||
Add("king_dead", 80f, "Politics", true, "King died: {b} ({a})", 6f, 28f);
|
||||
Add("king_killed", 85f, "Politics", true, "King killed: {b} ({a})", 6f, 28f);
|
||||
private static readonly Dictionary<string, WorldLogEventEntry> Entries =
|
||||
new Dictionary<string, WorldLogEventEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Favorites
|
||||
Add("favorite_dead", 82f, "Politics", true, "Favorite fell: {a}", 6f, 28f);
|
||||
Add("favorite_killed", 85f, "Politics", true, "Favorite fell: {a}", 6f, 28f);
|
||||
private static int _appliedGeneration = -1;
|
||||
|
||||
// Cities / kingdoms / clans / wars / alliances
|
||||
Add("city_new", 72f, "Settlement", true, "New city: {a}", 6f, 28f);
|
||||
Add("log_city_revolted", 90f, "Politics", true, "Revolt: {a}", 8f, 35f);
|
||||
Add("city_destroyed", 92f, "Settlement", true, "City destroyed: {a}", 8f, 35f);
|
||||
Add("diplomacy_war_ended", 72f, "Politics", true, "War ended: {a}", 6f, 28f);
|
||||
Add("diplomacy_war_started", 100f, "Politics", true, "War: {a} vs {b}", 8f, 35f);
|
||||
Add("total_war_started", 100f, "Politics", true, "Total war: {a}", 8f, 35f);
|
||||
Add("alliance_new", 70f, "Politics", true, "Alliance: {a}", 6f, 28f);
|
||||
Add("alliance_dissolved", 68f, "Politics", true, "Alliance dissolved: {a}", 6f, 28f);
|
||||
Add("kingdom_new", 75f, "Settlement", true, "New kingdom: {a}", 6f, 28f);
|
||||
Add("kingdom_destroyed", 100f, "Politics", true, "Kingdom fell: {a}", 8f, 35f);
|
||||
Add("kingdom_shattered", 98f, "Politics", true, "Kingdom shattered: {a}", 8f, 35f);
|
||||
Add("kingdom_fractured", 95f, "Politics", true, "Kingdom fractured: {a}", 8f, 35f);
|
||||
Add("kingdom_royal_clan_new", 74f, "Politics", true, "Royal clan: {a}", 6f, 28f);
|
||||
Add("kingdom_royal_clan_changed", 72f, "Politics", true, "Royal clan: {a}", 6f, 28f);
|
||||
Add("kingdom_royal_clan_dead", 76f, "Politics", true, "Royal clan ended: {a}", 6f, 28f);
|
||||
|
||||
// Disasters (WorldLog ids; linked to disasters library by naming)
|
||||
Add("disaster_tornado", 95f, "Disaster", true, "Tornado: {a}", 8f, 35f);
|
||||
Add("disaster_meteorite", 95f, "Disaster", true, "Meteorite: {a}", 8f, 35f);
|
||||
Add("disaster_hellspawn", 96f, "Disaster", true, "Hellspawn: {a}", 8f, 35f);
|
||||
Add("disaster_earthquake", 95f, "Disaster", true, "Earthquake: {a}", 8f, 35f);
|
||||
Add("disaster_greg_abominations", 96f, "Disaster", true, "Greg abominations: {a}", 8f, 35f);
|
||||
Add("disaster_ice_ones", 95f, "Disaster", true, "Ice ones awaken: {a}", 8f, 35f);
|
||||
Add("disaster_sudden_snowman", 90f, "Disaster", true, "Sudden snowman: {a}", 8f, 35f);
|
||||
Add("disaster_garden_surprise", 90f, "Disaster", true, "Garden surprise: {a}", 8f, 35f);
|
||||
Add("disaster_dragon_from_farlands", 98f, "Disaster", true, "Dragon from the farlands: {a}", 8f, 35f);
|
||||
Add("disaster_bandits", 92f, "Disaster", true, "Bandit raid: {a}", 8f, 35f);
|
||||
Add("disaster_alien_invasion", 97f, "Disaster", true, "Alien invasion: {a}", 8f, 35f);
|
||||
Add("disaster_biomass", 96f, "Disaster", true, "Biomass outbreak: {a}", 8f, 35f);
|
||||
Add("disaster_tumor", 96f, "Disaster", true, "Tumor outbreak: {a}", 8f, 35f);
|
||||
Add("disaster_heatwave", 88f, "Disaster", true, "Heatwave: {a}", 8f, 35f);
|
||||
Add("disaster_evil_mage", 94f, "Disaster", true, "Evil mage: {a}", 8f, 35f);
|
||||
Add("disaster_underground_necromancer", 95f, "Disaster", true, "Underground necromancer: {a}", 8f, 35f);
|
||||
Add("disaster_mad_thoughts", 90f, "Disaster", true, "Mad thoughts: {a}", 8f, 35f);
|
||||
|
||||
// Harness / internal - authored but never owns camera interest
|
||||
Add("auto_tester", 10f, "Harness", false, "Auto tester: {a}", 2f, 6f);
|
||||
}
|
||||
|
||||
private static void Add(
|
||||
string id,
|
||||
float strength,
|
||||
string category,
|
||||
bool createsInterest,
|
||||
string labelTemplate,
|
||||
float minWatch,
|
||||
float maxWatch)
|
||||
{
|
||||
Entries[id] = new WorldLogEventEntry
|
||||
public static void InvalidateOverlays()
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = strength,
|
||||
Category = category,
|
||||
CreatesInterest = createsInterest,
|
||||
ChronicleEligible = createsInterest && strength >= 65f,
|
||||
LabelTemplate = labelTemplate,
|
||||
MinWatch = minWatch,
|
||||
MaxWatch = maxWatch
|
||||
};
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AuthoredIds => Entries.Keys;
|
||||
|
||||
public static bool HasAuthored(string assetId)
|
||||
{
|
||||
return !string.IsNullOrEmpty(assetId) && Entries.ContainsKey(assetId.Trim());
|
||||
}
|
||||
|
||||
public static bool TryGet(string assetId, out WorldLogEventEntry entry)
|
||||
{
|
||||
entry = null;
|
||||
if (string.IsNullOrEmpty(assetId))
|
||||
{
|
||||
return false;
|
||||
Entries.Clear();
|
||||
_appliedGeneration = -1;
|
||||
}
|
||||
|
||||
return Entries.TryGetValue(assetId.Trim(), out entry);
|
||||
}
|
||||
|
||||
public static WorldLogEventEntry GetOrFallback(string assetId)
|
||||
{
|
||||
if (TryGet(assetId, out WorldLogEventEntry entry))
|
||||
private static void Ensure()
|
||||
{
|
||||
return entry;
|
||||
if (_appliedGeneration == EventCatalogConfig.Generation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_appliedGeneration = EventCatalogConfig.Generation;
|
||||
EventCatalogConfig.FillWorldLog(Entries);
|
||||
}
|
||||
|
||||
string id = string.IsNullOrEmpty(assetId) ? "unknown" : assetId.Trim();
|
||||
float strength = 40f;
|
||||
string category = "WorldLog";
|
||||
if (id.IndexOf("disaster", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
public static IEnumerable<string> AuthoredIds
|
||||
{
|
||||
strength = 95f;
|
||||
category = "Disaster";
|
||||
get
|
||||
{
|
||||
Ensure();
|
||||
return Entries.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
return new WorldLogEventEntry
|
||||
public static bool HasAuthored(string assetId)
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = strength,
|
||||
Category = category,
|
||||
CreatesInterest = true,
|
||||
ChronicleEligible = strength >= 65f,
|
||||
LabelTemplate = "{id}: {a}",
|
||||
MinWatch = strength >= 90f ? 8f : 6f,
|
||||
MaxWatch = strength >= 90f ? 35f : 28f,
|
||||
IsFallback = true
|
||||
};
|
||||
}
|
||||
|
||||
public static bool TryGetEventStrength(string assetId, out float eventStrength)
|
||||
{
|
||||
WorldLogEventEntry entry = GetOrFallback(assetId);
|
||||
eventStrength = entry.EventStrength;
|
||||
return !string.IsNullOrEmpty(assetId);
|
||||
}
|
||||
|
||||
public static string MakeLabel(WorldLogMessage message)
|
||||
{
|
||||
if (message == null)
|
||||
{
|
||||
return "event";
|
||||
Ensure();
|
||||
return !string.IsNullOrEmpty(assetId) && Entries.ContainsKey(assetId.Trim());
|
||||
}
|
||||
|
||||
return GetOrFallback(message.asset_id).MakeLabel(message);
|
||||
public static bool TryGet(string assetId, out WorldLogEventEntry entry)
|
||||
{
|
||||
Ensure();
|
||||
entry = null;
|
||||
if (string.IsNullOrEmpty(assetId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Entries.TryGetValue(assetId.Trim(), out entry);
|
||||
}
|
||||
|
||||
public static WorldLogEventEntry GetOrFallback(string assetId)
|
||||
{
|
||||
if (TryGet(assetId, out WorldLogEventEntry entry))
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
|
||||
string id = string.IsNullOrEmpty(assetId) ? "unknown" : assetId.Trim();
|
||||
float strength = 40f;
|
||||
string category = "WorldLog";
|
||||
if (id.IndexOf("disaster", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
strength = 95f;
|
||||
category = "Disaster";
|
||||
}
|
||||
|
||||
return new WorldLogEventEntry
|
||||
{
|
||||
Id = id,
|
||||
EventStrength = strength,
|
||||
Category = category,
|
||||
CreatesInterest = true,
|
||||
ChronicleEligible = strength >= 65f,
|
||||
LabelTemplate = "{id}: {a}",
|
||||
MinWatch = strength >= 90f ? 8f : 6f,
|
||||
MaxWatch = strength >= 90f ? 35f : 28f,
|
||||
IsFallback = true
|
||||
};
|
||||
}
|
||||
|
||||
public static bool TryGetEventStrength(string assetId, out float eventStrength)
|
||||
{
|
||||
WorldLogEventEntry entry = GetOrFallback(assetId);
|
||||
eventStrength = entry.EventStrength;
|
||||
return !string.IsNullOrEmpty(assetId);
|
||||
}
|
||||
|
||||
public static string MakeLabel(WorldLogMessage message)
|
||||
{
|
||||
if (message == null)
|
||||
{
|
||||
return "event";
|
||||
}
|
||||
|
||||
return GetOrFallback(message.asset_id).MakeLabel(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,11 @@ public sealed class DiscreteEventEntry
|
|||
public string Category = "Event";
|
||||
public bool CreatesInterest = true;
|
||||
public string LabelTemplate = "{a}";
|
||||
/// <summary>
|
||||
/// Optional infinitive clause for decision-style reasons
|
||||
/// (e.g. <c>change the kingdom's culture</c> → <c>{a} decides to …</c>).
|
||||
/// </summary>
|
||||
public string ActionPhrase = "";
|
||||
public bool IsFallback;
|
||||
|
||||
public string MakeLabel(Actor a, Actor b = null)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,30 @@ namespace IdleSpectator;
|
|||
/// </summary>
|
||||
public static partial class EventCatalog
|
||||
{
|
||||
/// <summary>Drop cached catalog rows so the next Ensure reloads from event-catalog.json.</summary>
|
||||
public static void InvalidateAllOverlays()
|
||||
{
|
||||
Decision.InvalidateOverlays();
|
||||
Happiness.InvalidateOverlays();
|
||||
Status.InvalidateOverlays();
|
||||
WorldLog.InvalidateOverlays();
|
||||
Plot.InvalidateOverlays();
|
||||
Relationship.InvalidateOverlays();
|
||||
Trait.InvalidateOverlays();
|
||||
Book.InvalidateOverlays();
|
||||
Era.InvalidateOverlays();
|
||||
Disaster.InvalidateOverlays();
|
||||
WarType.InvalidateOverlays();
|
||||
Spell.InvalidateOverlays();
|
||||
Power.InvalidateOverlays();
|
||||
Item.InvalidateOverlays();
|
||||
Gene.InvalidateOverlays();
|
||||
Phenotype.InvalidateOverlays();
|
||||
WorldLaw.InvalidateOverlays();
|
||||
SubspeciesTrait.InvalidateOverlays();
|
||||
Biome.InvalidateOverlays();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Layer A gate for happiness: Signal may own the camera; Ambient/Aggregate are B-only.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply a sentence template with {a}/{b}/{id}. Prefer typed helpers for new copy.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
3652
IdleSpectator/event-catalog.json
Normal file
3652
IdleSpectator/event-catalog.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "IdleSpectator",
|
||||
"author": "dazed",
|
||||
"version": "0.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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
843
scripts/export-event-catalog-from-cs.py
Executable file
843
scripts/export-event-catalog-from-cs.py
Executable file
|
|
@ -0,0 +1,843 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Export IdleSpectator/event-catalog.json from EventCatalog C# sources.
|
||||
|
||||
Parses authored catalog partials under IdleSpectator/Events/Catalogs/ and merges
|
||||
with existing decisions/species/character overlays from event-catalog.json
|
||||
(JSON decisions strengths/actions win).
|
||||
|
||||
Usage (from repo root):
|
||||
python3 scripts/export-event-catalog-from-cs.py
|
||||
python3 scripts/export-event-catalog-from-cs.py --out IdleSpectator/event-catalog.json
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
CATALOGS = REPO / "IdleSpectator" / "Events" / "Catalogs"
|
||||
DEFAULT_OUT = REPO / "IdleSpectator" / "event-catalog.json"
|
||||
DEFAULT_EXISTING = DEFAULT_OUT
|
||||
|
||||
MOD_VERSION = "0.25.0"
|
||||
UPDATED = "2026-07-16"
|
||||
NOTE = (
|
||||
"Authored IdleSpectator event catalog exported from EventCatalog C# sources "
|
||||
"(happiness, status, worldLog, plots, relationship, traits, books, eras, "
|
||||
"disasters, warTypes, libraries defaults). decisions/species/character merge "
|
||||
"from prior JSON (JSON wins). Loaded by EventCatalogConfig; scoring-model.json "
|
||||
"remains the global formula."
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# C# text helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def strip_csharp_comments(text: str) -> str:
|
||||
"""Remove // and /* */ comments outside of string literals."""
|
||||
out: list[str] = []
|
||||
i = 0
|
||||
n = len(text)
|
||||
while i < n:
|
||||
c = text[i]
|
||||
if c == '"':
|
||||
j = i + 1
|
||||
while j < n:
|
||||
if text[j] == "\\":
|
||||
j += 2
|
||||
continue
|
||||
if text[j] == '"':
|
||||
j += 1
|
||||
break
|
||||
j += 1
|
||||
out.append(text[i:j])
|
||||
i = j
|
||||
continue
|
||||
if c == "/" and i + 1 < n:
|
||||
nxt = text[i + 1]
|
||||
if nxt == "/":
|
||||
while i < n and text[i] not in "\r\n":
|
||||
i += 1
|
||||
continue
|
||||
if nxt == "*":
|
||||
i += 2
|
||||
while i + 1 < n and not (text[i] == "*" and text[i + 1] == "/"):
|
||||
i += 1
|
||||
i = min(n, i + 2)
|
||||
continue
|
||||
out.append(c)
|
||||
i += 1
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def enum_leaf(expr: str) -> str:
|
||||
"""HappinessPresentationTier.Signal -> Signal."""
|
||||
s = expr.strip().rstrip(",")
|
||||
if "." in s:
|
||||
return s.rsplit(".", 1)[-1]
|
||||
return s
|
||||
|
||||
|
||||
def parse_csharp_string(token: str) -> str:
|
||||
token = token.strip()
|
||||
if len(token) >= 2 and token[0] == '"' and token[-1] == '"':
|
||||
inner = token[1:-1]
|
||||
return (
|
||||
inner.replace(r"\\", "\0")
|
||||
.replace(r"\"", '"')
|
||||
.replace(r"\n", "\n")
|
||||
.replace(r"\t", "\t")
|
||||
.replace("\0", "\\")
|
||||
)
|
||||
return token
|
||||
|
||||
|
||||
def parse_number(token: str) -> float | int:
|
||||
t = token.strip().rstrip("fFdDmM")
|
||||
if re.fullmatch(r"-?\d+", t):
|
||||
return int(t)
|
||||
return float(t)
|
||||
|
||||
|
||||
def parse_bool(token: str) -> bool:
|
||||
t = token.strip().lower()
|
||||
if t == "true":
|
||||
return True
|
||||
if t == "false":
|
||||
return False
|
||||
raise ValueError(f"not a bool: {token!r}")
|
||||
|
||||
|
||||
def split_top_level_args(arg_text: str) -> list[str]:
|
||||
"""Split comma-separated C# call args, respecting strings/parens/braces."""
|
||||
args: list[str] = []
|
||||
buf: list[str] = []
|
||||
depth_paren = depth_brace = depth_bracket = 0
|
||||
in_string = False
|
||||
escape = False
|
||||
i = 0
|
||||
while i < len(arg_text):
|
||||
c = arg_text[i]
|
||||
if in_string:
|
||||
buf.append(c)
|
||||
if escape:
|
||||
escape = False
|
||||
elif c == "\\":
|
||||
escape = True
|
||||
elif c == '"':
|
||||
in_string = False
|
||||
i += 1
|
||||
continue
|
||||
if c == '"':
|
||||
in_string = True
|
||||
buf.append(c)
|
||||
i += 1
|
||||
continue
|
||||
if c == "(":
|
||||
depth_paren += 1
|
||||
buf.append(c)
|
||||
elif c == ")":
|
||||
depth_paren -= 1
|
||||
buf.append(c)
|
||||
elif c == "{":
|
||||
depth_brace += 1
|
||||
buf.append(c)
|
||||
elif c == "}":
|
||||
depth_brace -= 1
|
||||
buf.append(c)
|
||||
elif c == "[":
|
||||
depth_bracket += 1
|
||||
buf.append(c)
|
||||
elif c == "]":
|
||||
depth_bracket -= 1
|
||||
buf.append(c)
|
||||
elif (
|
||||
c == ","
|
||||
and depth_paren == 0
|
||||
and depth_brace == 0
|
||||
and depth_bracket == 0
|
||||
):
|
||||
args.append("".join(buf).strip())
|
||||
buf = []
|
||||
else:
|
||||
buf.append(c)
|
||||
i += 1
|
||||
tail = "".join(buf).strip()
|
||||
if tail:
|
||||
args.append(tail)
|
||||
return args
|
||||
|
||||
|
||||
def parse_call_args(arg_text: str) -> tuple[list[Any], dict[str, Any]]:
|
||||
"""Parse positional + named (name: value) C# call arguments."""
|
||||
positional: list[Any] = []
|
||||
named: dict[str, Any] = {}
|
||||
for raw in split_top_level_args(arg_text):
|
||||
if not raw:
|
||||
continue
|
||||
m = re.match(r"^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.*)$", raw, re.S)
|
||||
if m and not raw.lstrip().startswith('"'):
|
||||
name, val = m.group(1), m.group(2).strip()
|
||||
named[name] = coerce_literal(val)
|
||||
else:
|
||||
positional.append(coerce_literal(raw))
|
||||
return positional, named
|
||||
|
||||
|
||||
def is_seed_call(arg_text: str) -> bool:
|
||||
"""True when the first arg is a string literal (seed), not a method signature."""
|
||||
parts = split_top_level_args(arg_text)
|
||||
return bool(parts) and parts[0].lstrip().startswith('"')
|
||||
|
||||
|
||||
def coerce_literal(token: str) -> Any:
|
||||
t = token.strip()
|
||||
if not t:
|
||||
return t
|
||||
if t.startswith('"'):
|
||||
return parse_csharp_string(t)
|
||||
low = t.lower()
|
||||
if low in ("true", "false"):
|
||||
return low == "true"
|
||||
if re.fullmatch(r"-?\d+(\.\d+)?[fFdDmM]?", t):
|
||||
return parse_number(t)
|
||||
if t.startswith("new[]") or t.startswith("new string[]"):
|
||||
return parse_string_array(t)
|
||||
return t
|
||||
|
||||
|
||||
def parse_string_array(expr: str) -> list[str]:
|
||||
m = re.search(r"\{(.*)\}", expr, re.S)
|
||||
if not m:
|
||||
return []
|
||||
inner = m.group(1).strip()
|
||||
if not inner:
|
||||
return []
|
||||
return [parse_csharp_string(p) for p in split_top_level_args(inner) if p.strip()]
|
||||
|
||||
|
||||
def find_method_calls(text: str, method: str) -> list[tuple[int, str]]:
|
||||
"""Return (index, arg_text) for method(...); calls, balanced parens."""
|
||||
results: list[tuple[int, str]] = []
|
||||
pattern = re.compile(rf"\b{re.escape(method)}\s*\(")
|
||||
for m in pattern.finditer(text):
|
||||
start = m.end()
|
||||
depth = 1
|
||||
i = start
|
||||
in_string = False
|
||||
escape = False
|
||||
while i < len(text) and depth > 0:
|
||||
c = text[i]
|
||||
if in_string:
|
||||
if escape:
|
||||
escape = False
|
||||
elif c == "\\":
|
||||
escape = True
|
||||
elif c == '"':
|
||||
in_string = False
|
||||
else:
|
||||
if c == '"':
|
||||
in_string = True
|
||||
elif c == "(":
|
||||
depth += 1
|
||||
elif c == ")":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
results.append((m.start(), text[start:i]))
|
||||
break
|
||||
i += 1
|
||||
return results
|
||||
|
||||
|
||||
def read_catalog(name: str) -> str:
|
||||
path = CATALOGS / name
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError(path)
|
||||
return strip_csharp_comments(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Section parsers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_FAILURES: list[str] = []
|
||||
|
||||
|
||||
def fail(msg: str) -> None:
|
||||
_FAILURES.append(msg)
|
||||
print(f"PARSE FAIL: {msg}", file=sys.stderr)
|
||||
|
||||
|
||||
def parse_happiness(text: str) -> list[dict[str, Any]]:
|
||||
entries: list[dict[str, Any]] = []
|
||||
# Match ["id"] = new HappinessCatalogEntry { ... },
|
||||
pattern = re.compile(
|
||||
r'\["([^"]+)"\]\s*=\s*new\s+HappinessCatalogEntry\s*\{',
|
||||
re.M,
|
||||
)
|
||||
for m in pattern.finditer(text):
|
||||
entry_id = m.group(1)
|
||||
body_start = m.end()
|
||||
depth = 1
|
||||
i = body_start
|
||||
in_string = False
|
||||
escape = False
|
||||
while i < len(text) and depth > 0:
|
||||
c = text[i]
|
||||
if in_string:
|
||||
if escape:
|
||||
escape = False
|
||||
elif c == "\\":
|
||||
escape = True
|
||||
elif c == '"':
|
||||
in_string = False
|
||||
else:
|
||||
if c == '"':
|
||||
in_string = True
|
||||
elif c == "{":
|
||||
depth += 1
|
||||
elif c == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
break
|
||||
i += 1
|
||||
if depth != 0:
|
||||
fail(f"happiness: unclosed block for {entry_id}")
|
||||
continue
|
||||
body = text[body_start:i]
|
||||
try:
|
||||
entries.append(parse_happiness_body(entry_id, body))
|
||||
except Exception as ex: # noqa: BLE001
|
||||
fail(f"happiness[{entry_id}]: {ex}")
|
||||
return entries
|
||||
|
||||
|
||||
def parse_happiness_body(entry_id: str, body: str) -> dict[str, Any]:
|
||||
props: dict[str, str] = {}
|
||||
# Property = value; (value may be new[] { ... })
|
||||
prop_re = re.compile(
|
||||
r"(\w+)\s*=\s*"
|
||||
r"("
|
||||
r"new\s*(?:string\s*)?\[\s*\]\s*\{(?:[^{}]|\{[^{}]*\})*\}"
|
||||
r'|"[^"\\]*(?:\\.[^"\\]*)*"'
|
||||
r"|[^,\n]+"
|
||||
r")\s*,?",
|
||||
re.M,
|
||||
)
|
||||
for m in prop_re.finditer(body):
|
||||
props[m.group(1)] = m.group(2).strip().rstrip(",")
|
||||
|
||||
def req(name: str) -> str:
|
||||
if name not in props:
|
||||
raise ValueError(f"missing {name}")
|
||||
return props[name]
|
||||
|
||||
strength = parse_number(req("EventStrength"))
|
||||
row: dict[str, Any] = {
|
||||
"id": parse_csharp_string(props["Id"]) if "Id" in props else entry_id,
|
||||
"strength": strength,
|
||||
"presentation": enum_leaf(req("Presentation")),
|
||||
"context": enum_leaf(req("Context")),
|
||||
"life": enum_leaf(req("Life")),
|
||||
"overlap": enum_leaf(req("Overlap")),
|
||||
"relationRole": enum_leaf(req("RelationRole")),
|
||||
"category": parse_csharp_string(req("InterestCategory"))
|
||||
if req("InterestCategory").startswith('"')
|
||||
else req("InterestCategory").strip('"'),
|
||||
"variantsWithRelated": parse_string_array(req("VariantsWithRelated"))
|
||||
if "VariantsWithRelated" in props
|
||||
else [],
|
||||
"variantsWithoutRelated": parse_string_array(req("VariantsWithoutRelated"))
|
||||
if "VariantsWithoutRelated" in props
|
||||
else [],
|
||||
}
|
||||
if "CanonicalMilestoneKey" in props:
|
||||
key = props["CanonicalMilestoneKey"]
|
||||
val = parse_csharp_string(key) if key.startswith('"') else key
|
||||
if val:
|
||||
row["canonicalMilestoneKey"] = val
|
||||
if "StatusOverlapId" in props:
|
||||
key = props["StatusOverlapId"]
|
||||
val = parse_csharp_string(key) if key.startswith('"') else key
|
||||
if val:
|
||||
row["statusOverlapId"] = val
|
||||
return row
|
||||
|
||||
|
||||
def parse_status(text: str) -> list[dict[str, Any]]:
|
||||
entries: list[dict[str, Any]] = []
|
||||
for _, args in find_method_calls(text, "AddNoInterest"):
|
||||
if not is_seed_call(args):
|
||||
continue
|
||||
pos, _ = parse_call_args(args)
|
||||
if not pos:
|
||||
fail(f"status AddNoInterest: empty args ({args!r})")
|
||||
continue
|
||||
entries.append(
|
||||
{
|
||||
"id": pos[0],
|
||||
"strength": 20,
|
||||
"category": "StatusAmbient",
|
||||
"camera": False,
|
||||
"extendsGrief": False,
|
||||
}
|
||||
)
|
||||
for _, args in find_method_calls(text, "Add"):
|
||||
if not is_seed_call(args):
|
||||
continue
|
||||
pos, named = parse_call_args(args)
|
||||
if len(pos) < 4:
|
||||
continue
|
||||
try:
|
||||
entry = {
|
||||
"id": pos[0],
|
||||
"strength": _num(pos[1]),
|
||||
"category": pos[2],
|
||||
"camera": bool(pos[3]),
|
||||
"extendsGrief": bool(named.get("extendsGrief", False)),
|
||||
}
|
||||
entries.append(entry)
|
||||
except Exception as ex: # noqa: BLE001
|
||||
fail(f"status Add({args[:60]}…): {ex}")
|
||||
return entries
|
||||
|
||||
|
||||
def parse_world_log(text: str) -> list[dict[str, Any]]:
|
||||
entries: list[dict[str, Any]] = []
|
||||
for _, args in find_method_calls(text, "Add"):
|
||||
if not is_seed_call(args):
|
||||
continue
|
||||
pos, _ = parse_call_args(args)
|
||||
# Seed calls: id, strength, category, createsInterest, label, minWatch, maxWatch
|
||||
if len(pos) < 7:
|
||||
fail(f"worldLog Add: expected 7 args, got {len(pos)} ({args[:60]}…)")
|
||||
continue
|
||||
try:
|
||||
strength = float(pos[1])
|
||||
camera = bool(pos[3])
|
||||
chronicle = camera and strength >= 65.0
|
||||
row: dict[str, Any] = {
|
||||
"id": pos[0],
|
||||
"strength": _num(pos[1]),
|
||||
"category": pos[2],
|
||||
"camera": camera,
|
||||
"label": pos[4],
|
||||
"minWatch": _num(pos[5]),
|
||||
"maxWatch": _num(pos[6]),
|
||||
"chronicle": chronicle,
|
||||
}
|
||||
entries.append(row)
|
||||
except Exception as ex: # noqa: BLE001
|
||||
fail(f"worldLog Add({args[:60]}…): {ex}")
|
||||
return entries
|
||||
|
||||
|
||||
def parse_simple_add(
|
||||
text: str,
|
||||
*,
|
||||
default_category: str | None = None,
|
||||
default_label: str | None = None,
|
||||
default_camera: bool = True,
|
||||
min_pos: int = 2,
|
||||
style: str = "id_strength_category_label",
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Parse Add(...) seed calls for DiscreteEventEntry-style catalogs."""
|
||||
entries: list[dict[str, Any]] = []
|
||||
for _, args in find_method_calls(text, "Add"):
|
||||
if not is_seed_call(args):
|
||||
continue
|
||||
pos, named = parse_call_args(args)
|
||||
if len(pos) < min_pos:
|
||||
continue
|
||||
try:
|
||||
if style == "id_strength_only":
|
||||
# Trait: Add(id, strength) - apply defaults
|
||||
if len(pos) != 2 or not isinstance(pos[0], str):
|
||||
continue
|
||||
row = {
|
||||
"id": pos[0],
|
||||
"strength": _num(pos[1]),
|
||||
"category": default_category or "Trait",
|
||||
"camera": default_camera,
|
||||
"label": default_label or "Trait: {id} ({a})",
|
||||
}
|
||||
elif style == "id_strength_category_label":
|
||||
# Plot/Book/Era/Relationship: Add(id, strength, category, label [, createsInterest])
|
||||
if len(pos) < 4:
|
||||
continue
|
||||
camera = bool(named.get("createsInterest", pos[4] if len(pos) > 4 else default_camera))
|
||||
row = {
|
||||
"id": pos[0],
|
||||
"strength": _num(pos[1]),
|
||||
"category": pos[2],
|
||||
"camera": camera,
|
||||
"label": pos[3],
|
||||
}
|
||||
elif style == "disaster":
|
||||
# Add(id, strength, worldLogId)
|
||||
if len(pos) < 3:
|
||||
continue
|
||||
row = {
|
||||
"id": pos[0],
|
||||
"strength": _num(pos[1]),
|
||||
"category": "Disaster",
|
||||
"camera": True,
|
||||
"worldLogId": pos[2],
|
||||
}
|
||||
elif style == "war_type":
|
||||
# Add(id, strength, labelTemplate)
|
||||
if len(pos) < 3:
|
||||
continue
|
||||
row = {
|
||||
"id": pos[0],
|
||||
"strength": _num(pos[1]),
|
||||
"category": "Politics",
|
||||
"camera": True,
|
||||
"label": pos[2],
|
||||
}
|
||||
else:
|
||||
fail(f"unknown add style {style}")
|
||||
continue
|
||||
entries.append(row)
|
||||
except Exception as ex: # noqa: BLE001
|
||||
fail(f"{style} Add({args[:60]}…): {ex}")
|
||||
return entries
|
||||
|
||||
|
||||
def _num(v: Any) -> int | float:
|
||||
if isinstance(v, float) and v.is_integer():
|
||||
return int(v)
|
||||
return v
|
||||
|
||||
|
||||
def parse_libraries(text: str) -> dict[str, Any]:
|
||||
"""Emit defaults per library class; include signalIds only when HashSet listed."""
|
||||
# Class blocks: public static class Spell { ... }
|
||||
class_re = re.compile(
|
||||
r"public\s+static\s+class\s+(\w+)\s*\{",
|
||||
re.M,
|
||||
)
|
||||
classes = list(class_re.finditer(text))
|
||||
wanted_order = [
|
||||
("Spell", "spell"),
|
||||
("Power", "power"),
|
||||
("Item", "item"),
|
||||
("Gene", "gene"),
|
||||
("Phenotype", "phenotype"),
|
||||
("WorldLaw", "worldLaw"),
|
||||
("SubspeciesTrait", "subspeciesTrait"),
|
||||
("Biome", "biome"),
|
||||
]
|
||||
by_cs: dict[str, dict[str, Any]] = {}
|
||||
for idx, m in enumerate(classes):
|
||||
name = m.group(1)
|
||||
if name not in {cs for cs, _ in wanted_order}:
|
||||
continue
|
||||
start = m.end()
|
||||
end = classes[idx + 1].start() if idx + 1 < len(classes) else len(text)
|
||||
block = text[start:end]
|
||||
defaults = extract_ensure_seeded_defaults(name, block)
|
||||
if defaults is None:
|
||||
fail(f"libraries[{name}]: could not parse EnsureSeeded defaults")
|
||||
continue
|
||||
by_cs[name] = defaults
|
||||
libs: dict[str, Any] = {}
|
||||
for cs_name, json_key in wanted_order:
|
||||
if cs_name in by_cs:
|
||||
libs[json_key] = by_cs[cs_name]
|
||||
return libs
|
||||
|
||||
|
||||
def extract_ensure_seeded_defaults(name: str, block: str) -> dict[str, Any] | None:
|
||||
# Find EnsureSeeded( ... ) call args
|
||||
calls = find_method_calls(block, "EnsureSeeded")
|
||||
if not calls:
|
||||
# LiveLibraryInterest.EnsureSeeded via expression body
|
||||
calls = find_method_calls(block, "LiveLibraryInterest.EnsureSeeded")
|
||||
if not calls:
|
||||
return None
|
||||
_, arg_text = calls[0]
|
||||
pos, named = parse_call_args(arg_text)
|
||||
# Signature (positional):
|
||||
# Entries, enumerate, category, label, ambientStrength, signalIds, signalStrength,
|
||||
# [signalPredicate], ambientCreatesInterest
|
||||
# Named kwargs also used: ambientStrength, signalIds, signalStrength,
|
||||
# signalPredicate, ambientCreatesInterest
|
||||
category = None
|
||||
label = None
|
||||
ambient_strength = named.get("ambientStrength")
|
||||
signal_strength = named.get("signalStrength")
|
||||
ambient_creates = named.get("ambientCreatesInterest")
|
||||
signal_ids_expr = named.get("signalIds")
|
||||
|
||||
# Positional layout from LiveLibraryInterest.EnsureSeeded - read that file if needed.
|
||||
# From usage:
|
||||
# Entries, Enumerate..., "Category", "label", ambientStrength: N, SignalIds|null,
|
||||
# signalStrength: N, [signalPredicate: ...], ambientCreatesInterest: bool
|
||||
if len(pos) >= 4:
|
||||
category = pos[2] if isinstance(pos[2], str) else category
|
||||
label = pos[3] if isinstance(pos[3], str) else label
|
||||
if ambient_strength is None and len(pos) >= 5 and isinstance(pos[4], (int, float)):
|
||||
ambient_strength = pos[4]
|
||||
# signalIds may be positional after ambientStrength
|
||||
if signal_ids_expr is None and len(pos) >= 6:
|
||||
signal_ids_expr = pos[5]
|
||||
if signal_strength is None:
|
||||
# often named; sometimes after signalIds
|
||||
for p in pos[6:]:
|
||||
if isinstance(p, (int, float)):
|
||||
signal_strength = p
|
||||
break
|
||||
if ambient_creates is None:
|
||||
for p in reversed(pos):
|
||||
if isinstance(p, bool):
|
||||
ambient_creates = p
|
||||
break
|
||||
|
||||
# Also pull named ambientStrength etc. from the raw call via regex for robustness
|
||||
am = re.search(r"ambientStrength\s*:\s*([\d.]+)f?", block)
|
||||
if am and ambient_strength is None:
|
||||
ambient_strength = parse_number(am.group(1))
|
||||
sm = re.search(r"signalStrength\s*:\s*([\d.]+)f?", block)
|
||||
if sm and signal_strength is None:
|
||||
signal_strength = parse_number(sm.group(1))
|
||||
acm = re.search(r"ambientCreatesInterest\s*:\s*(true|false)", block)
|
||||
if acm and ambient_creates is None:
|
||||
ambient_creates = acm.group(1) == "true"
|
||||
|
||||
# Category / label from EnsureSeeded positional strings
|
||||
cat_m = re.search(
|
||||
r"EnsureSeeded\s*\(\s*Entries\s*,\s*[^,]+,\s*\"([^\"]+)\"\s*,\s*\"([^\"]*)\"",
|
||||
block,
|
||||
)
|
||||
if cat_m:
|
||||
category = cat_m.group(1)
|
||||
label = cat_m.group(2)
|
||||
|
||||
if category is None or label is None or ambient_strength is None or signal_strength is None:
|
||||
return None
|
||||
if ambient_creates is None:
|
||||
ambient_creates = True
|
||||
|
||||
out: dict[str, Any] = {
|
||||
"ambientStrength": _num(ambient_strength),
|
||||
"signalStrength": _num(signal_strength),
|
||||
"category": category,
|
||||
"label": label,
|
||||
"ambientCreatesInterest": bool(ambient_creates),
|
||||
}
|
||||
|
||||
# signalIds HashSet initializer if present
|
||||
hs = re.search(
|
||||
r"HashSet\s*<\s*string\s*>\s+\w+\s*=\s*new\s+HashSet\s*<\s*string\s*>\s*"
|
||||
r"(?:\([^)]*\))?\s*\{([^}]*)\}",
|
||||
block,
|
||||
re.S,
|
||||
)
|
||||
if hs:
|
||||
ids = []
|
||||
for sm in re.finditer(r'"([^"]+)"', hs.group(1)):
|
||||
ids.append(sm.group(1))
|
||||
if ids:
|
||||
out["signalIds"] = ids
|
||||
return out
|
||||
|
||||
|
||||
def load_existing_overlays(path: Path) -> dict[str, Any]:
|
||||
if not path.is_file():
|
||||
return {"decisions": [], "species": [], "character": []}
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
return {
|
||||
"decisions": data.get("decisions") or [],
|
||||
"species": data.get("species") or [],
|
||||
"character": data.get("character") or [],
|
||||
}
|
||||
|
||||
|
||||
def merge_decisions(
|
||||
existing: list[dict[str, Any]],
|
||||
decision_cs: str,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Keep JSON decisions (strength/action win). Fill gaps from BuiltinActionPhrases."""
|
||||
by_id: dict[str, dict[str, Any]] = {}
|
||||
# Builtin phrases from C#
|
||||
m = re.search(
|
||||
r"BuiltinActionPhrases\s*=\s*new\s+Dictionary[^\{]*\{(.*)\};\s*\n\s*///",
|
||||
decision_cs,
|
||||
re.S,
|
||||
)
|
||||
if not m:
|
||||
# fallback: grab until closing of static field
|
||||
m = re.search(
|
||||
r"BuiltinActionPhrases\s*=\s*new\s+Dictionary[^\{]*\{(.*?)^\s*\};",
|
||||
decision_cs,
|
||||
re.S | re.M,
|
||||
)
|
||||
builtins: dict[str, str] = {}
|
||||
if m:
|
||||
for km in re.finditer(r'\["([^"]+)"\]\s*=\s*"([^"]*)"', m.group(1)):
|
||||
builtins[km.group(1)] = km.group(2)
|
||||
else:
|
||||
fail("decisions: could not parse BuiltinActionPhrases")
|
||||
|
||||
for row in existing:
|
||||
rid = row.get("id")
|
||||
if not rid:
|
||||
continue
|
||||
by_id[rid] = {
|
||||
"id": rid,
|
||||
"action": row.get("action") or builtins.get(rid, ""),
|
||||
"strength": row.get("strength", 86),
|
||||
}
|
||||
|
||||
# Do not invent strengths for builtin-only ids not in JSON (user: JSON wins / keep from existing)
|
||||
# Optionally add builtin ids missing from JSON with no strength? User said keep from existing JSON.
|
||||
# So only existing decisions list.
|
||||
return list(by_id.values()) if by_id else [
|
||||
{"id": k, "action": v, "strength": 86} for k, v in builtins.items()
|
||||
]
|
||||
|
||||
|
||||
def build_document(existing_path: Path) -> dict[str, Any]:
|
||||
overlays = load_existing_overlays(existing_path)
|
||||
|
||||
happiness = parse_happiness(read_catalog("EventCatalog.Happiness.cs"))
|
||||
status = parse_status(read_catalog("EventCatalog.Status.cs"))
|
||||
world_log = parse_world_log(read_catalog("EventCatalog.WorldLog.cs"))
|
||||
plots = parse_simple_add(
|
||||
read_catalog("EventCatalog.Plot.cs"),
|
||||
style="id_strength_category_label",
|
||||
min_pos=4,
|
||||
)
|
||||
relationship = parse_simple_add(
|
||||
read_catalog("EventCatalog.Relationship.cs"),
|
||||
style="id_strength_category_label",
|
||||
min_pos=4,
|
||||
)
|
||||
traits = parse_simple_add(
|
||||
read_catalog("EventCatalog.Trait.cs"),
|
||||
style="id_strength_only",
|
||||
default_category="Trait",
|
||||
default_label="Trait: {id} ({a})",
|
||||
default_camera=True,
|
||||
min_pos=2,
|
||||
)
|
||||
books = parse_simple_add(
|
||||
read_catalog("EventCatalog.Book.cs"),
|
||||
style="id_strength_category_label",
|
||||
min_pos=4,
|
||||
)
|
||||
eras = parse_simple_add(
|
||||
read_catalog("EventCatalog.Era.cs"),
|
||||
style="id_strength_category_label",
|
||||
min_pos=4,
|
||||
)
|
||||
disasters = parse_simple_add(
|
||||
read_catalog("EventCatalog.Disaster.cs"),
|
||||
style="disaster",
|
||||
min_pos=3,
|
||||
)
|
||||
war_types = parse_simple_add(
|
||||
read_catalog("EventCatalog.WarType.cs"),
|
||||
style="war_type",
|
||||
min_pos=3,
|
||||
)
|
||||
libraries = parse_libraries(read_catalog("EventCatalog.Libraries.cs"))
|
||||
|
||||
decision_cs = read_catalog("EventCatalog.Decision.cs")
|
||||
decisions = merge_decisions(overlays["decisions"], decision_cs)
|
||||
|
||||
doc: dict[str, Any] = {
|
||||
"modVersion": MOD_VERSION,
|
||||
"title": "IdleSpectator event catalog",
|
||||
"updated": UPDATED,
|
||||
"note": NOTE,
|
||||
"decisions": decisions,
|
||||
"species": overlays["species"],
|
||||
"character": overlays["character"],
|
||||
"happiness": happiness,
|
||||
"status": status,
|
||||
"worldLog": world_log,
|
||||
"plots": plots,
|
||||
"relationship": relationship,
|
||||
"traits": traits,
|
||||
"books": books,
|
||||
"eras": eras,
|
||||
"disasters": disasters,
|
||||
"warTypes": war_types,
|
||||
"libraries": libraries,
|
||||
}
|
||||
return doc
|
||||
|
||||
|
||||
def print_counts(doc: dict[str, Any]) -> None:
|
||||
sections = [
|
||||
"decisions",
|
||||
"species",
|
||||
"character",
|
||||
"happiness",
|
||||
"status",
|
||||
"worldLog",
|
||||
"plots",
|
||||
"relationship",
|
||||
"traits",
|
||||
"books",
|
||||
"eras",
|
||||
"disasters",
|
||||
"warTypes",
|
||||
]
|
||||
print("Counts:")
|
||||
for key in sections:
|
||||
val = doc.get(key)
|
||||
n = len(val) if isinstance(val, list) else 0
|
||||
print(f" {key}: {n}")
|
||||
libs = doc.get("libraries") or {}
|
||||
print(f" libraries: {len(libs)} keys ({', '.join(libs.keys())})")
|
||||
for k, v in libs.items():
|
||||
sig = v.get("signalIds")
|
||||
extra = f", signalIds={len(sig)}" if isinstance(sig, list) else ""
|
||||
print(
|
||||
f" {k}: ambient={v.get('ambientStrength')} signal={v.get('signalStrength')} "
|
||||
f"category={v.get('category')}{extra}"
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument(
|
||||
"--out",
|
||||
type=Path,
|
||||
default=DEFAULT_OUT,
|
||||
help=f"Output JSON path (default: {DEFAULT_OUT})",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--existing",
|
||||
type=Path,
|
||||
default=DEFAULT_EXISTING,
|
||||
help="Existing event-catalog.json to merge decisions/species/character from",
|
||||
)
|
||||
args = ap.parse_args()
|
||||
|
||||
doc = build_document(args.existing)
|
||||
args.out.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.out.write_text(
|
||||
json.dumps(doc, indent=2, ensure_ascii=False) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(f"Wrote {args.out}")
|
||||
print_counts(doc)
|
||||
if _FAILURES:
|
||||
print(f"\nParse failures ({len(_FAILURES)}):", file=sys.stderr)
|
||||
for f in _FAILURES:
|
||||
print(f" - {f}", file=sys.stderr)
|
||||
return 1
|
||||
print("\nParse failures: none")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Reference in a new issue