using System; namespace IdleSpectator; /// Evidence-backed renderer for structured narrative beats and consequences. public static class NarrativeProse { public static bool TryBuildBeat( Actor focus, InterestCandidate tip, out NarrativeBeatModel model, out string beat, out string context) { model = null; beat = ""; context = ""; long focusId = EventFeedUtil.SafeId(focus); if (focusId == 0 || tip == null) return false; NarrativePresentationModel presentation = tip.NarrativePresentation; if (presentation != null && presentation.PerspectiveId == focusId) { NarrativeDevelopment exact = !string.IsNullOrEmpty(presentation.DevelopmentId) ? NarrativeStoryStore.Development(presentation.DevelopmentId) : null; model = new NarrativeBeatModel { PerspectiveCharacterId = focusId, DevelopmentId = presentation.DevelopmentId ?? "", ActionKind = presentation.DevelopmentKind, OtherId = presentation.OtherId, Other = presentation.Other, Change = presentation.Change ?? "", Outcome = presentation.Outcome ?? "", Confidence = presentation.Confidence }; NarrativeThread exactThread = LatestThreadFor(focusId, presentation.DevelopmentId); if (exactThread != null) { model.ThreadId = exactThread.Id; model.ThreadPhase = exactThread.Phase; model.ContextEvidence = exactThread.PressureEvidence; } beat = NarrativeRenderer.Beat(presentation); context = ContextLine(model, exactThread); if (!string.IsNullOrEmpty(beat)) { NarrativePresentationCoverage.Note( presentation.EvidenceSource, beat, presentation, exact != null); return true; } } NarrativePresentationCoverage.Note("missing-presentation", tip.Label, null, false); return false; } public static string ContextLine(NarrativeBeatModel model, NarrativeThread thread) { if (model == null || thread == null) return ""; // Only show concrete pressure. A generic central question is useful scheduler state, // but is not evidence suitable for player-facing narration. if (!string.IsNullOrEmpty(thread.PressureEvidence)) return SagaProse.Sentence(thread.PressureEvidence); return ""; } public static string ConsequenceLine(CharacterConsequence consequence) { if (consequence == null || consequence.Confidence < 0.75f) return ""; string other = consequence.Other.Name ?? ""; switch (consequence.Kind) { case CharacterConsequenceKind.FoundLove: return string.IsNullOrEmpty(other) ? "Found love" : "Found love with " + other; case CharacterConsequenceKind.LostPartner: return string.IsNullOrEmpty(other) ? "Lost a partner" : "Lost " + other; case CharacterConsequenceKind.BecameParent: return string.IsNullOrEmpty(other) ? "Had a child" : "Welcomed child " + other; case CharacterConsequenceKind.WasBorn: return string.IsNullOrEmpty(other) ? "Was born" : "Was born to " + other; case CharacterConsequenceKind.LostLife: return string.IsNullOrEmpty(other) ? "Died" : "Was slain by " + other; case CharacterConsequenceKind.TookLife: return string.IsNullOrEmpty(other) ? "Took a life" : "Slew " + other; case CharacterConsequenceKind.RoseToRole: return "Rose in standing"; case CharacterConsequenceKind.LostRole: return "Lost their former standing"; case CharacterConsequenceKind.FoundedHome: return "Founded a new home"; case CharacterConsequenceKind.LostHome: return "Lost their home"; case CharacterConsequenceKind.FoundHome: return "Found a home"; case CharacterConsequenceKind.EnteredWar: return "Entered a war"; case CharacterConsequenceKind.SurvivedWar: return "Survived the war"; case CharacterConsequenceKind.JoinedPlot: return "Joined a plot"; case CharacterConsequenceKind.PlotResolved: return "Saw a plot resolved"; case CharacterConsequenceKind.Transformed: return "Was transformed"; case CharacterConsequenceKind.Recovered: return "Recovered"; default: return ""; } } public static string TryMergeChapter(string current, CharacterConsequence next) { if (string.IsNullOrEmpty(current) || next == null) return ""; // Partnership chapters keep formation and loss separate because both are identity-changing. // Lineage repeats collapse naturally in the presentation layer's family summary. return ""; } private static NarrativeThread LatestThreadFor(long characterId, string developmentId) { CharacterStoryState state = NarrativeStoryStore.Character(characterId); if (state == null) return null; for (int i = 0; i < state.OpenThreadIds.Count; i++) { NarrativeThread t = NarrativeStoryStore.Thread(state.OpenThreadIds[i]); if (t != null && t.DevelopmentIds.Contains(developmentId)) return t; } for (int i = 0; i < state.ResolvedThreadIds.Count; i++) { NarrativeThread t = NarrativeStoryStore.Thread(state.ResolvedThreadIds[i]); if (t != null && t.DevelopmentIds.Contains(developmentId)) return t; } return null; } }