- 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
278 lines
13 KiB
C#
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 "";
|
|
}
|
|
}
|