using System; using System.Collections.Generic; namespace IdleSpectator; /// /// Explicit catalog-to-editorial-role mapping at the single candidate intake boundary. /// Structural fallbacks cover live theaters; string heuristics remain only a compatibility fallback. /// public static class NarrativeFunctionCatalog { private static readonly Dictionary Exact = new Dictionary(StringComparer.OrdinalIgnoreCase); private static readonly HashSet ObservedKeys = new HashSet(StringComparer.Ordinal); private static readonly int[] FunctionCounts = new int[9]; public static int ObservedCount { get; private set; } public static int ExplicitCount { get; private set; } public static int StructuralCount { get; private set; } public static int FallbackCount { get; private set; } static NarrativeFunctionCatalog() { Add(NarrativeFunction.ThreadOpener, "set_lover", "fallen_in_love", "fell_in_love", "new_family", "family_group_new", "just_made_friend"); Add(NarrativeFunction.Development, "live_family", "family_group_join", "just_kissed", "alliance_join", "alliance_create", "alliance_new", "just_gave_gift", "just_received_gift", "just_talked_gossip"); Add(NarrativeFunction.Escalation, "live_combat", "live_battle", "live_war", "live_plot", "live_outbreak", "new_war", "diplomacy_war_started", "just_started_war", "total_war_started", "whisper_of_war", "rebellion", "cause_rebellion", "just_rebelled", "just_injured", "just_cursed", "just_possessed", "status_soul_harvested", "attacker_stop_war"); Add(NarrativeFunction.TurningPoint, "clear_lover", "just_killed", "milestone_kill", "become_alpha", "become_king", "become_leader", "king_new", "kingdom_new", "city_new", "just_found_house", "conquered_city", "was_conquered", "clan_ascension", "kingdom_royal_clan_changed", "kingdom_royal_clan_new", "culture_divide", "religion_schism", "language_divergence"); Add(NarrativeFunction.Resolution, "diplomacy_war_ended", "just_made_peace", "just_finished_plot", "just_won_war", "aftermath_war_linger", "aftermath_plot_fallout", "recovery_outbreak"); Add(NarrativeFunction.Consequence, "add_child", "just_had_child", "baby_created", "just_born", "just_got_out_of_egg", "just_became_adult", "death_best_friend", "death_child", "death_family_member", "death_lover", "king_dead", "king_killed", "favorite_dead", "favorite_killed", "city_destroyed", "destroyed_city", "kingdom_destroyed", "kingdom_fell_apart", "kingdom_fractured", "kingdom_shattered", "just_lost_house", "just_lost_war", "lost_capital", "lost_city", "lost_crown", "razed_capital", "razed_city", "family_removed", "alliance_destroy", "alliance_dissolved", "aftermath_love_linger", "aftermath_mourner", "aftermath_survivor", "epilogue_crisis", "epilogue_related", "wrote_book", "new_book"); Add(NarrativeFunction.WorldContext, "earthquake", "small_earthquake", "small_meteorite", "alien_invasion", "ash_bandits", "biomass", "dragon_from_farlands", "garden_surprise", "greg_abominations", "heatwave", "hellspawn", "ice_ones_awoken", "mad_thoughts", "sudden_snowman", "tornado", "tumor", "underground_necromancer", "new_culture", "new_language", "new_religion"); Add(NarrativeFunction.CharacterTexture, "find_lover", "sexual_reproduction_try", "family_group_follow", "family_group_leave", "just_ate", "just_cried", "just_laughed", "just_played", "just_pooped", "just_sang", "just_slept", "slept_outside", "just_talked", "paid_tax", "had_bad_dream", "had_good_dream", "had_nightmare", "just_surprised", "just_swore", "just_read_book"); } public static void ClearCoverage() { ObservedKeys.Clear(); Array.Clear(FunctionCounts, 0, FunctionCounts.Length); ObservedCount = 0; ExplicitCount = 0; StructuralCount = 0; FallbackCount = 0; } public static void Stamp(InterestCandidate candidate) { if (candidate == null) return; if (!candidate.HasNarrativeFunction) { if (TryExact(candidate.HappinessEffectId, out NarrativeFunction function) || TryExact(candidate.AssetId, out function) || TryExact(candidate.StatusId, out function) || TryExact(candidate.Verb, out function)) { Set(candidate, function, "catalog"); } else if (TryDomain(candidate, out function)) { Set(candidate, function, "structural"); } else { // Compatibility fallback is stamped once, so downstream policy is deterministic. Set(candidate, NarrativeFunctionPolicy.Classify(candidate), "fallback"); } } NoteCoverage(candidate); } public static string CoverageSummary => "observed=" + ObservedCount + " explicit=" + ExplicitCount + " structural=" + StructuralCount + " fallback=" + FallbackCount + " functions=" + FormatCounts(); private static bool TryDomain(InterestCandidate c, out NarrativeFunction function) { string source = c.Source ?? ""; string category = c.Category ?? ""; if (c.Completion == InterestCompletionKind.CombatActive || c.Completion == InterestCompletionKind.WarFront || c.Completion == InterestCompletionKind.PlotActive || c.Completion == InterestCompletionKind.StatusOutbreak) { function = NarrativeFunction.Escalation; return true; } if (c.Completion == InterestCompletionKind.HappinessGrief) { function = NarrativeFunction.TurningPoint; return true; } if (c.Completion == InterestCompletionKind.EarthquakeActive || Contains(source, "disaster") || Contains(category, "disaster") || Contains(source, "era")) { function = NarrativeFunction.WorldContext; return true; } if (Contains(source, "plot") || Contains(category, "plot")) { function = NarrativeFunction.Development; return true; } if (Contains(source, "book") || Contains(source, "trait") || c.Completion == InterestCompletionKind.CharacterVignette || c.Completion == InterestCompletionKind.StatusPhase) { function = NarrativeFunction.CharacterTexture; return true; } function = NarrativeFunction.Noise; return false; } private static void Set(InterestCandidate c, NarrativeFunction function, string source) { c.NarrativeFunction = function; c.HasNarrativeFunction = true; c.NarrativeFunctionSource = source; } private static void NoteCoverage(InterestCandidate c) { string key = c.Key ?? ""; if (!ObservedKeys.Add(key)) return; ObservedCount++; int index = (int)c.NarrativeFunction; if (index >= 0 && index < FunctionCounts.Length) FunctionCounts[index]++; if (c.NarrativeFunctionSource == "catalog") ExplicitCount++; else if (c.NarrativeFunctionSource == "structural") StructuralCount++; else FallbackCount++; } private static bool TryExact(string id, out NarrativeFunction function) { function = NarrativeFunction.Noise; if (string.IsNullOrEmpty(id)) return false; string key = id.Trim(); if (Exact.TryGetValue(key, out function)) return true; if (key.StartsWith("disaster_", StringComparison.OrdinalIgnoreCase)) { function = NarrativeFunction.WorldContext; return true; } if (key.StartsWith("summon_", StringComparison.OrdinalIgnoreCase)) { function = NarrativeFunction.Escalation; return true; } if (key.StartsWith("asexual_reproduction_", StringComparison.OrdinalIgnoreCase)) { function = NarrativeFunction.CharacterTexture; return true; } return false; } private static void Add(NarrativeFunction function, params string[] ids) { for (int i = 0; i < ids.Length; i++) Exact[ids[i]] = function; } private static bool Contains(string text, string needle) => text.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0; private static string FormatCounts() { var parts = new List(FunctionCounts.Length); for (int i = 0; i < FunctionCounts.Length; i++) { if (FunctionCounts[i] > 0) parts.Add(((NarrativeFunction)i) + ":" + FunctionCounts[i]); } return string.Join(",", parts.ToArray()); } }