- 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
205 lines
9 KiB
C#
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());
|
|
}
|
|
}
|