worldbox-observer-mod/IdleSpectator/Narrative/NarrativeThreadRules.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

265 lines
12 KiB
C#

using System;
namespace IdleSpectator;
/// <summary>Maps semantic developments onto durable character-centered threads.</summary>
public static class NarrativeThreadRules
{
public static void Apply(NarrativeDevelopment d)
{
if (d == null || d.SubjectId == 0) return;
switch (d.Kind)
{
case NarrativeDevelopmentKind.BondFormed:
ApplyBondFormed(d);
break;
case NarrativeDevelopmentKind.BondBroken:
ApplyBondBroken(d);
break;
case NarrativeDevelopmentKind.ChildBorn:
ApplyChildBorn(d);
break;
case NarrativeDevelopmentKind.FriendFormed:
ApplyFriend(d);
break;
case NarrativeDevelopmentKind.Death:
ApplyDeath(d);
break;
case NarrativeDevelopmentKind.Kill:
case NarrativeDevelopmentKind.RivalEncounter:
ApplyConflict(d);
break;
case NarrativeDevelopmentKind.RoleGained:
case NarrativeDevelopmentKind.RoleLost:
ApplyPower(d);
break;
case NarrativeDevelopmentKind.HomeFounded:
case NarrativeDevelopmentKind.HomeGained:
case NarrativeDevelopmentKind.HomeLost:
ApplyHome(d);
break;
case NarrativeDevelopmentKind.WarJoined:
case NarrativeDevelopmentKind.WarEnded:
ApplyWar(d);
break;
case NarrativeDevelopmentKind.PlotJoined:
case NarrativeDevelopmentKind.PlotEnded:
ApplyPlot(d);
break;
case NarrativeDevelopmentKind.Transformation:
case NarrativeDevelopmentKind.Recovery:
ApplySurvival(d);
break;
}
}
private static void ApplyBondFormed(NarrativeDevelopment d)
{
string anchor = PairAnchor("bond", d.SubjectId, d.OtherId);
string id = "thread:partnership:" + anchor + ":" + d.SubjectId;
NarrativeThread t = NarrativeStoryStore.OpenOrUpdateThread(
id, NarrativeThreadKind.Partnership, d.SubjectId, anchor, d,
"How will this partnership shape their lives?", NarrativePhase.Outcome,
NarrativeThreadStatus.Active);
t?.AddCast(d.OtherId, "partner");
AddConsequence(d, t, d.SubjectId, CharacterConsequenceKind.FoundLove, "partner", d.OtherId, 72f);
if (d.OtherId != 0)
{
string mirrorId = "thread:partnership:" + anchor + ":" + d.OtherId;
NarrativeThread mirror = NarrativeStoryStore.OpenOrUpdateThread(
mirrorId, NarrativeThreadKind.Partnership, d.OtherId, anchor, d,
"How will this partnership shape their lives?", NarrativePhase.Outcome,
NarrativeThreadStatus.Active);
mirror?.AddCast(d.SubjectId, "partner");
AddConsequence(d, mirror, d.OtherId, CharacterConsequenceKind.FoundLove, "partner", d.SubjectId, 72f);
}
}
private static void ApplyBondBroken(NarrativeDevelopment d)
{
string anchor = PairAnchor("bond", d.SubjectId, d.OtherId);
string id = "thread:partnership:" + anchor + ":" + d.SubjectId;
NarrativeThread t = NarrativeStoryStore.OpenOrUpdateThread(
id, NarrativeThreadKind.SeparationGrief, d.SubjectId, anchor, d,
"What follows the loss of this partnership?", NarrativePhase.Consequence,
NarrativeThreadStatus.Resolved);
if (t != null) t.Resolution = "partnership ended";
AddConsequence(d, t, d.SubjectId, CharacterConsequenceKind.LostPartner, "survivor", d.OtherId, 88f);
}
private static void ApplyChildBorn(NarrativeDevelopment d)
{
string anchor = !string.IsNullOrEmpty(d.FamilyKey) ? d.FamilyKey : d.SubjectId.ToString();
string id = "thread:lineage:" + anchor + ":" + d.SubjectId;
NarrativeThread t = NarrativeStoryStore.OpenOrUpdateThread(
id, NarrativeThreadKind.Lineage, d.SubjectId, anchor, d,
"How will this growing lineage endure?", NarrativePhase.Outcome,
NarrativeThreadStatus.Active);
t?.AddCast(d.OtherId, "child");
AddConsequence(d, t, d.SubjectId, CharacterConsequenceKind.BecameParent, "parent", d.OtherId, 76f);
if (d.OtherId != 0)
{
string childId = "thread:lineage:" + anchor + ":" + d.OtherId;
NarrativeThread childThread = NarrativeStoryStore.OpenOrUpdateThread(
childId, NarrativeThreadKind.Lineage, d.OtherId, anchor, d,
"What life will begin from this lineage?", NarrativePhase.Setup,
NarrativeThreadStatus.Emerging);
childThread?.AddCast(d.SubjectId, "parent");
AddConsequence(d, childThread, d.OtherId, CharacterConsequenceKind.WasBorn, "child", d.SubjectId, 65f);
}
}
private static void ApplyFriend(NarrativeDevelopment d)
{
string anchor = PairAnchor("friend", d.SubjectId, d.OtherId);
NarrativeStoryStore.OpenOrUpdateThread(
"thread:partnership:" + anchor + ":" + d.SubjectId,
NarrativeThreadKind.Partnership, d.SubjectId, anchor, d,
"Where will this friendship lead?", NarrativePhase.Setup, NarrativeThreadStatus.Emerging);
}
private static void ApplyDeath(NarrativeDevelopment d)
{
string id = "thread:life-end:" + d.SubjectId;
NarrativeThread t = NarrativeStoryStore.OpenOrUpdateThread(
id, NarrativeThreadKind.SeparationGrief, d.SubjectId, id, d,
"Their life has ended", NarrativePhase.Outcome, NarrativeThreadStatus.Resolved);
if (t != null) t.Resolution = "died";
AddConsequence(d, t, d.SubjectId, CharacterConsequenceKind.LostLife, "deceased", d.OtherId, 100f);
}
private static void ApplyConflict(NarrativeDevelopment d)
{
string anchor = PairAnchor("conflict", d.SubjectId, d.OtherId);
NarrativeThreadKind kind = d.Kind == NarrativeDevelopmentKind.RivalEncounter
? NarrativeThreadKind.Rivalry : NarrativeThreadKind.PersonalCombat;
NarrativeThread t = NarrativeStoryStore.OpenOrUpdateThread(
"thread:conflict:" + anchor + ":" + d.SubjectId, kind, d.SubjectId, anchor, d,
"Will this conflict end their repeated struggle?", NarrativePhase.TurningPoint,
NarrativeThreadStatus.Active);
if (d.Kind == NarrativeDevelopmentKind.Kill)
{
AddConsequence(d, t, d.SubjectId, CharacterConsequenceKind.TookLife, "killer", d.OtherId, 82f);
}
}
private static void ApplyPower(NarrativeDevelopment d)
{
string anchor = "role:" + d.SubjectId + ":" + d.NewState;
NarrativeThread t = NarrativeStoryStore.OpenOrUpdateThread(
"thread:power:" + anchor, NarrativeThreadKind.RiseToRule, d.SubjectId, anchor, d,
"What will they do with their new standing?", NarrativePhase.Outcome,
d.Kind == NarrativeDevelopmentKind.RoleLost ? NarrativeThreadStatus.Resolved : NarrativeThreadStatus.Active);
AddConsequence(d, t, d.SubjectId,
d.Kind == NarrativeDevelopmentKind.RoleLost ? CharacterConsequenceKind.LostRole : CharacterConsequenceKind.RoseToRole,
"office holder", 0, 78f);
}
private static void ApplyHome(NarrativeDevelopment d)
{
string anchor = FirstNonEmpty(d.CityKey, d.KingdomKey, d.CorrelationKey, d.SubjectId.ToString());
bool lost = d.Kind == NarrativeDevelopmentKind.HomeLost;
NarrativeThread t = NarrativeStoryStore.OpenOrUpdateThread(
"thread:home:" + anchor + ":" + d.SubjectId,
lost ? NarrativeThreadKind.HomeLoss : NarrativeThreadKind.Founding,
d.SubjectId, anchor, d,
lost ? "Where will they go after losing their home?" : "Will what they founded endure?",
lost ? NarrativePhase.TurningPoint : NarrativePhase.Outcome,
NarrativeThreadStatus.Active);
AddConsequence(d, t, d.SubjectId,
lost ? CharacterConsequenceKind.LostHome : CharacterConsequenceKind.FoundedHome,
lost ? "displaced" : "founder", 0, lost ? 90f : 82f);
}
private static void ApplyWar(NarrativeDevelopment d)
{
string anchor = FirstNonEmpty(d.WarKey, d.CorrelationKey, "unknown");
bool ended = d.Kind == NarrativeDevelopmentKind.WarEnded;
NarrativeThread t = NarrativeStoryStore.OpenOrUpdateThread(
"thread:war:" + anchor + ":" + d.SubjectId, NarrativeThreadKind.WarInvolvement,
d.SubjectId, anchor, d,
ended ? "What did the war change for them?" : "Will they survive this war?",
ended ? NarrativePhase.Outcome : NarrativePhase.Pressure,
ended ? NarrativeThreadStatus.Resolved : NarrativeThreadStatus.Active);
AddConsequence(d, t, d.SubjectId,
ended ? CharacterConsequenceKind.SurvivedWar : CharacterConsequenceKind.EnteredWar,
"participant", d.OtherId, ended ? 86f : 62f);
}
private static void ApplyPlot(NarrativeDevelopment d)
{
string anchor = FirstNonEmpty(d.PlotKey, d.CorrelationKey, "unknown");
bool ended = d.Kind == NarrativeDevelopmentKind.PlotEnded;
NarrativeThread t = NarrativeStoryStore.OpenOrUpdateThread(
"thread:plot:" + anchor + ":" + d.SubjectId, NarrativeThreadKind.PlotBetrayal,
d.SubjectId, anchor, d,
ended ? "What did the plot change?" : "Will their plot succeed?",
ended ? NarrativePhase.Outcome : NarrativePhase.Escalation,
ended ? NarrativeThreadStatus.Resolved : NarrativeThreadStatus.Active);
AddConsequence(d, t, d.SubjectId,
ended ? CharacterConsequenceKind.PlotResolved : CharacterConsequenceKind.JoinedPlot,
"plotter", d.OtherId, ended ? 78f : 58f);
}
private static void ApplySurvival(NarrativeDevelopment d)
{
bool recovered = d.Kind == NarrativeDevelopmentKind.Recovery;
string anchor = FirstNonEmpty(d.CorrelationKey, "change:" + d.SubjectId + ":" + d.NewState);
NarrativeThread t = NarrativeStoryStore.OpenOrUpdateThread(
"thread:survival:" + anchor, recovered ? NarrativeThreadKind.Recovery : NarrativeThreadKind.Transformation,
d.SubjectId, anchor, d,
recovered ? "How will recovery change their path?" : "Will they endure this transformation?",
recovered ? NarrativePhase.Outcome : NarrativePhase.TurningPoint,
recovered ? NarrativeThreadStatus.Resolved : NarrativeThreadStatus.Active);
AddConsequence(d, t, d.SubjectId,
recovered ? CharacterConsequenceKind.Recovered : CharacterConsequenceKind.Transformed,
"subject", d.OtherId, 72f);
}
private static void AddConsequence(
NarrativeDevelopment d,
NarrativeThread thread,
long characterId,
CharacterConsequenceKind kind,
string role,
long otherId,
float importance)
{
LifeSagaIdentity other = otherId == d.OtherId ? d.Other : LifeSagaMemory.SnapshotId(otherId);
NarrativeStoryStore.AddConsequence(new CharacterConsequence
{
Id = "consequence:" + d.Id + ":" + characterId + ":" + kind,
CharacterId = characterId,
ThreadId = thread?.Id ?? "",
DevelopmentId = d.Id,
Kind = kind,
Role = role ?? "",
OtherId = otherId,
Other = other,
Context = FirstNonEmpty(d.WarKey, d.PlotKey, d.CityKey, d.KingdomKey),
Outcome = d.Outcome ?? "",
DateLabel = d.DateLabel ?? "",
OccurredAt = d.OccurredAt,
Importance = importance,
Confidence = d.Confidence
});
}
private static string PairAnchor(string prefix, long a, long b)
{
if (b == 0) return prefix + ":" + a;
return a <= b ? prefix + ":" + a + ":" + b : prefix + ":" + b + ":" + a;
}
private static string FirstNonEmpty(params string[] values)
{
if (values == null) return "";
for (int i = 0; i < values.Length; i++)
{
if (!string.IsNullOrEmpty(values[i])) return values[i];
}
return "";
}
}