worldbox-observer-mod/IdleSpectator/Narrative/NarrativeFunctionCatalog.cs
DazedAnon 739cc6492c feat(spectator): make narrative threads own camera direction
- Route confirmed life events into evidence-based threads, consequences, and Legacy chapters
- Schedule episode shots with critical interrupts, replay guards, and a combat budget
- Admit emerging main characters through story momentum instead of spectacle
- Remove causal heat, hard-arc memory writes, and synthetic epilogue continuity
- Expand narrative harness coverage, soak auditing, and product documentation
2026-07-23 12:17:18 -05:00

205 lines
9 KiB
C#

using System;
using System.Collections.Generic;
namespace IdleSpectator;
/// <summary>
/// Explicit catalog-to-editorial-role mapping at the single candidate intake boundary.
/// Structural fallbacks cover live theaters; string heuristics remain only a compatibility fallback.
/// </summary>
public static class NarrativeFunctionCatalog
{
private static readonly Dictionary<string, NarrativeFunction> Exact =
new Dictionary<string, NarrativeFunction>(StringComparer.OrdinalIgnoreCase);
private static readonly HashSet<string> ObservedKeys = new HashSet<string>(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<string>(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());
}
}