worldbox-observer-mod/IdleSpectator/Narrative/NarrativeThreadRules.cs
DazedAnon c9000531fc feat(happiness): enhance happiness event handling and telemetry tracking
- Introduce bounded happiness probe to improve event diagnostics
- Update happiness drain logic to limit processed occurrences per tick
- Enhance telemetry for happiness processing times and maximum items
- Refactor narrative development to include new home-related events
- Improve narrative thread handling for home establishment and recovery
2026-07-23 22:45:18 -05:00

278 lines
13 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);
NarrativeStoryStore.ResolveCombatThreadsForDeath(d.SubjectId, d);
}
private static void ApplyConflict(NarrativeDevelopment d)
{
string anchor = PairAnchor("conflict", d.SubjectId, d.OtherId);
bool terminal = d.Kind == NarrativeDevelopmentKind.Kill;
NarrativeThreadKind kind = terminal
? NarrativeThreadKind.PersonalCombat
: NarrativeThreadKind.Rivalry;
NarrativeThread t = NarrativeStoryStore.OpenOrUpdateThread(
"thread:conflict:" + anchor + ":" + d.SubjectId, kind, d.SubjectId, anchor, d,
terminal ? "The fight has reached its outcome"
: "Will this conflict become a repeated struggle?",
terminal ? NarrativePhase.Outcome : NarrativePhase.Pressure,
terminal ? NarrativeThreadStatus.Resolved : NarrativeThreadStatus.Active);
if (terminal)
{
if (t != null) t.Resolution = "killed opponent";
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;
bool founded = d.Kind == NarrativeDevelopmentKind.HomeFounded;
NarrativeThread t = NarrativeStoryStore.OpenOrUpdateThread(
"thread:home:" + anchor + ":" + d.SubjectId,
lost ? NarrativeThreadKind.HomeLoss
: founded ? NarrativeThreadKind.Founding : NarrativeThreadKind.Recovery,
d.SubjectId, anchor, d,
lost ? "Where will they go after losing their home?"
: founded ? "They established a home" : "They found a place to belong",
lost ? NarrativePhase.TurningPoint : NarrativePhase.Outcome,
lost ? NarrativeThreadStatus.Active : NarrativeThreadStatus.Resolved);
if (!lost && t != null) t.Resolution = founded ? "home established" : "found a home";
AddConsequence(d, t, d.SubjectId,
lost ? CharacterConsequenceKind.LostHome
: founded ? CharacterConsequenceKind.FoundedHome : CharacterConsequenceKind.FoundHome,
lost ? "displaced" : founded ? "founder" : "resident",
0,
lost ? 90f : founded ? 82f : 64f);
}
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 "";
}
}