- 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
144 lines
5.9 KiB
C#
144 lines
5.9 KiB
C#
using System;
|
|
|
|
namespace IdleSpectator;
|
|
|
|
/// <summary>Evidence-backed renderer for structured narrative beats and consequences.</summary>
|
|
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;
|
|
}
|
|
|
|
}
|