using System; namespace IdleSpectator; /// Maps semantic developments onto durable character-centered threads. 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 ""; } }