using System; using UnityEngine; namespace IdleSpectator; /// /// Canonical confirmed-observation writer. It owns raw Saga facts and semantic development /// projection so callers cannot independently emit the same change twice. /// public static class NarrativeEventRecorder { public static NarrativeEventReceipt BondFormed( Actor subject, Actor partner, string source = "set_lover") { string developmentId = NarrativeDevelopmentRouter.BondFormedId(subject, partner); return Record( NarrativeEventKind.BondFormed, developmentId, subject, partner, source, () => LifeSagaMemory.RecordBondFormedFact(subject, partner, source), () => NarrativeDevelopmentRouter.BondFormedExact( subject, partner, source, developmentId), "set_lover"); } public static NarrativeEventReceipt BondBroken( Actor subject, Actor former, string source = "clear_lover") { string developmentId = NarrativeDevelopmentRouter.BondBrokenId(subject, former); return Record( NarrativeEventKind.BondBroken, developmentId, subject, former, source, () => LifeSagaMemory.RecordBondBrokenFact(subject, former, source), () => NarrativeDevelopmentRouter.BondBrokenExact( subject, former, source, developmentId), "clear_lover"); } public static NarrativeEventReceipt ChildBorn( Actor parent, Actor child, string source = "add_child") { string developmentId = NarrativeDevelopmentRouter.ChildBornId(parent, child); return Record( NarrativeEventKind.ChildBorn, developmentId, parent, child, source, () => LifeSagaMemory.RecordParentChildFact(parent, child, source), () => NarrativeDevelopmentRouter.ChildBornExact( parent, child, source, developmentId), "add_child"); } public static NarrativeEventReceipt FriendFormed( Actor subject, Actor friend, string source = "set_best_friend") { string developmentId = NarrativeDevelopmentRouter.FriendFormedId(subject, friend); return Record( NarrativeEventKind.FriendFormed, developmentId, subject, friend, source, () => LifeSagaMemory.RecordFriendFormedFact(subject, friend, source), () => NarrativeDevelopmentRouter.FriendFormedExact( subject, friend, source, developmentId), eventId: ""); } public static NarrativeEventReceipt Kill( Actor killer, Actor victim, string source = "newKillAction") { string developmentId = NarrativeDevelopmentRouter.KillId(killer, victim); return Record( NarrativeEventKind.Kill, developmentId, killer, victim, source, () => LifeSagaMemory.RecordKillFact(killer, victim, source), () => NarrativeDevelopmentRouter.KillExact( killer, victim, source, developmentId), eventId: "", outcome: "victim died", magnitude: 82f); } public static NarrativeEventReceipt Death( Actor victim, Actor killer, string attackType, string source = "die") { string developmentId = NarrativeDevelopmentRouter.DeathId(victim); return Record( NarrativeEventKind.Death, developmentId, victim, killer, source, () => LifeSagaMemory.RecordDeathFact(victim, killer, attackType, source), () => NarrativeDevelopmentRouter.DeathExact( victim, killer, attackType, source, developmentId), eventId: "", newState: "dead", outcome: attackType ?? "", resolved: true, magnitude: 100f); } public static NarrativeEventReceipt Role( Actor subject, string role, bool gained, string source = "role") { string developmentId = NarrativeDevelopmentRouter.RoleId(subject, role, gained); return Record( NarrativeEventKind.RoleChanged, developmentId, subject, null, source, () => LifeSagaMemory.RecordRoleFact(subject, role, gained, source), () => NarrativeDevelopmentRouter.RoleExact( subject, role, gained, source, developmentId), eventId: "", newState: gained ? role ?? "" : "", previousState: gained ? "" : role ?? "", resolved: !gained, magnitude: 78f); } public static NarrativeEventReceipt Founding( Actor subject, string kind, string key, string source) { string developmentId = NarrativeDevelopmentRouter.FoundingId(subject, kind, key); return Record( NarrativeEventKind.Founding, developmentId, subject, null, source, () => LifeSagaMemory.RecordFoundingFact(subject, kind, key, source), () => NarrativeDevelopmentRouter.FoundingExact( subject, kind, key, source, developmentId), eventId: "", familyKey: kind == "family" ? key : "", cityKey: kind == "city" ? key : "", kingdomKey: kind == "kingdom" ? key : "", newState: kind ?? "", magnitude: 82f); } public static NarrativeEventReceipt Home( Actor subject, bool gained, string source = "home") { string developmentId = NarrativeDevelopmentRouter.HomeId(subject, gained); return Record( NarrativeEventKind.HomeChanged, developmentId, subject, null, source, () => LifeSagaMemory.RecordHomeFact(subject, gained, source), () => NarrativeDevelopmentRouter.HomeExact( subject, gained, source, developmentId), eventId: "", previousState: gained ? "" : "home", newState: gained ? "home" : "", outcome: gained ? "" : "home lost", resolved: !gained, magnitude: gained ? 64f : 72f); } public static NarrativeEventReceipt War( Actor subject, Actor other, string warKey, string kingdomKey, bool ended, string source = "war", string note = "") { string developmentId = NarrativeDevelopmentRouter.WarId(subject, warKey, ended); return Record( NarrativeEventKind.WarChanged, developmentId, subject, other, source, () => LifeSagaMemory.RecordWarFact( subject, warKey, kingdomKey, ended, source, other, note), () => NarrativeDevelopmentRouter.WarExact( subject, other, warKey, ended, source, developmentId), eventId: "", kingdomKey: kingdomKey, warKey: warKey, outcome: ended ? "war ended" : "", note: note, resolved: ended, magnitude: ended ? 86f : 68f); } public static NarrativeEventReceipt Plot( Actor subject, Actor other, string plotKey, string plotAsset, bool ended, string source = "plot") { string developmentId = NarrativeDevelopmentRouter.PlotId(subject, plotKey, ended); return Record( NarrativeEventKind.PlotChanged, developmentId, subject, other, source, () => LifeSagaMemory.RecordPlotFact( subject, plotKey, plotAsset, ended, source, other), () => NarrativeDevelopmentRouter.PlotExact( subject, other, plotKey, ended, source, developmentId), eventId: "", plotKey: plotKey, outcome: ended ? "plot ended" : "", note: LifeSagaMemory.PlotDisplay(plotAsset), resolved: ended, magnitude: ended ? 78f : 64f); } private static NarrativeEventReceipt Record( NarrativeEventKind kind, string developmentId, Actor subject, Actor other, string source, Func recordFact, Func recordDevelopment, string eventId, string familyKey = "", string cityKey = "", string kingdomKey = "", string warKey = "", string plotKey = "", string previousState = "", string newState = "", string outcome = "", string note = "", bool resolved = false, float magnitude = 50f) { long subjectId = EventFeedUtil.SafeId(subject); if (subjectId == 0 || string.IsNullOrEmpty(developmentId)) { return new NarrativeEventReceipt { RejectionReason = "missing_identity" }; } string id = "event:" + developmentId; NarrativeEventRecord existing = NarrativeEventStore.Get(id); if (existing != null) { // The immutable event store and compact Saga memory have separate // lifetimes. Re-observing a confirmed event should repair a missing // presentation fact instead of leaving the relationship generic. recordFact?.Invoke(); NarrativePresentationModel duplicatePresentation = string.IsNullOrEmpty(eventId) ? null : NarrativePresentationFactory.Relationship(eventId, subject, other); StampPresentation(duplicatePresentation, existing.Id, existing.DevelopmentId, existing.Other); return new NarrativeEventReceipt { IsNew = false, EventId = existing.Id, DevelopmentId = existing.DevelopmentId, Presentation = duplicatePresentation }; } LifeSagaFact fact = recordFact != null ? recordFact() : null; if (fact == null) { return new NarrativeEventReceipt { RejectionReason = "fact_rejected" }; } bool newDevelopment = recordDevelopment != null && recordDevelopment(); NarrativeDevelopment development = NarrativeStoryStore.Development(developmentId); LifeSagaIdentity otherSnapshot = fact.Other.Id != 0 ? fact.Other : LifeSagaMemory.Snapshot(other); var record = new NarrativeEventRecord { Id = id, DedupeKey = developmentId, Kind = kind, SubjectId = subjectId, Subject = fact.Subject.Id != 0 ? fact.Subject : LifeSagaMemory.Snapshot(subject), OtherId = otherSnapshot.Id, Other = otherSnapshot, DevelopmentId = developmentId, CorrelationKey = fact.Key ?? developmentId, FamilyKey = familyKey ?? "", CityKey = cityKey ?? "", KingdomKey = kingdomKey ?? "", WarKey = warKey ?? "", PlotKey = plotKey ?? "", PreviousState = previousState ?? "", NewState = newState ?? "", Outcome = outcome ?? "", Note = note ?? "", Resolved = resolved, EvidenceSource = source ?? "", DateLabel = fact.DateLabel ?? "", WorldTime = fact.WorldTime, ObservedAt = Time.unscaledTime, Magnitude = magnitude, Confidence = 1f }; bool newEvent = NarrativeEventStore.Add(record); NarrativePresentationModel presentation = string.IsNullOrEmpty(eventId) ? null : NarrativePresentationFactory.Relationship(eventId, subject, other); StampPresentation(presentation, record.Id, developmentId, otherSnapshot); return new NarrativeEventReceipt { IsNew = newEvent || newDevelopment, EventId = record.Id, DevelopmentId = development?.Id ?? developmentId, Presentation = presentation }; } private static void StampPresentation( NarrativePresentationModel presentation, string eventId, string developmentId, LifeSagaIdentity other) { if (presentation == null) { return; } presentation.EventId = eventId ?? ""; presentation.DevelopmentId = developmentId ?? ""; if (presentation.OtherId == 0 && other.Id != 0) { presentation.OtherId = other.Id; presentation.Other = other; } } }