worldbox-observer-mod/IdleSpectator/Narrative/NarrativeTelemetry.cs
DazedAnon 739cc6492c feat(spectator): make narrative threads own camera direction
- Route confirmed life events into evidence-based threads, consequences, and Legacy chapters
- Schedule episode shots with critical interrupts, replay guards, and a combat budget
- Admit emerging main characters through story momentum instead of spectacle
- Remove causal heat, hard-arc memory writes, and synthetic epilogue continuity
- Expand narrative harness coverage, soak auditing, and product documentation
2026-07-23 12:17:18 -05:00

127 lines
5 KiB
C#

using System.Collections.Generic;
namespace IdleSpectator;
public sealed class NarrativeTelemetryEntry
{
public float At;
public string ThreadId = "";
public long ProtagonistId;
public string ProposedKey = "";
public string ActualKey = "";
public string Reason = "";
public NarrativeInterruptClass InterruptClass;
public bool ActualRelated;
}
/// <summary>Bounded in-memory trace for shadow-vs-actual story scheduling.</summary>
public static class NarrativeTelemetry
{
private const int Cap = 256;
private static readonly List<NarrativeTelemetryEntry> Entries = new List<NarrativeTelemetryEntry>(Cap);
public static int Count => Entries.Count;
public static int RelatedActualCount { get; private set; }
public static int ComparedCount { get; private set; }
public static int RelatedProposalCount { get; private set; }
public static int CriticalProposalCount { get; private set; }
public static int AlignedSampleCount { get; private set; }
public static int PermittedInterruptSampleCount { get; private set; }
public static int PermittedContinuitySampleCount { get; private set; }
public static int UnalignedSampleCount { get; private set; }
public static int UnrelatedActualCount => ComparedCount - RelatedActualCount;
public static void Clear()
{
Entries.Clear();
RelatedActualCount = 0;
ComparedCount = 0;
RelatedProposalCount = 0;
CriticalProposalCount = 0;
AlignedSampleCount = 0;
PermittedInterruptSampleCount = 0;
PermittedContinuitySampleCount = 0;
UnalignedSampleCount = 0;
}
public static void NoteProposal(EpisodePlan episode, EpisodeShotProposal proposal, float now)
{
if (episode == null || proposal?.Candidate == null) return;
if (proposal.InterruptClass == NarrativeInterruptClass.RelatedEscalation) RelatedProposalCount++;
if (proposal.InterruptClass == NarrativeInterruptClass.WorldCritical) CriticalProposalCount++;
Entries.Add(new NarrativeTelemetryEntry
{
At = now,
ThreadId = episode.ThreadId,
ProtagonistId = episode.ProtagonistId,
ProposedKey = proposal.Candidate.Key ?? "",
Reason = proposal.Reason ?? "",
InterruptClass = proposal.InterruptClass
});
Trim();
}
public static void NoteActual(EpisodePlan episode, NarrativeThread thread, InterestCandidate actual)
{
if (episode == null || actual == null) return;
bool related = InterruptPolicy.IsRelated(actual, episode, thread);
ComparedCount++;
if (related) RelatedActualCount++;
NarrativeTelemetryEntry last = Entries.Count > 0 ? Entries[Entries.Count - 1] : null;
if (last != null && last.ThreadId == episode.ThreadId && string.IsNullOrEmpty(last.ActualKey))
{
last.ActualKey = actual.Key ?? "";
last.ActualRelated = related;
}
}
public static void NoteCurrentAlignment(
EpisodePlan episode,
NarrativeThread thread,
InterestCandidate current)
{
if (episode == null || current == null) return;
if (InterruptPolicy.IsRelated(current, episode, thread))
{
AlignedSampleCount++;
return;
}
NarrativeInterruptClass kind = InterruptPolicy.Classify(current, episode, thread);
if (InterruptPolicy.MayInterrupt(kind))
{
AlignedSampleCount++;
PermittedInterruptSampleCount++;
return;
}
// Live sticky scenes are deliberately allowed to finish their immediate outcome
// before an episode reclaims the camera; report them separately from true drift.
if (current.Completion == InterestCompletionKind.CombatActive
|| current.Completion == InterestCompletionKind.WarFront
|| current.Completion == InterestCompletionKind.PlotActive
|| current.Completion == InterestCompletionKind.StatusOutbreak)
{
AlignedSampleCount++;
PermittedContinuitySampleCount++;
return;
}
UnalignedSampleCount++;
}
public static string Summary => "compared=" + ComparedCount
+ " actualRelated=" + RelatedActualCount
+ " actualUnrelated=" + UnrelatedActualCount
+ " proposals=" + Count
+ " relatedProposals=" + RelatedProposalCount
+ " criticalProposals=" + CriticalProposalCount
+ " alignedSamples=" + AlignedSampleCount
+ " permittedInterruptSamples=" + PermittedInterruptSampleCount
+ " permittedContinuitySamples=" + PermittedContinuitySampleCount
+ " unalignedSamples=" + UnalignedSampleCount;
private static void Trim()
{
while (Entries.Count > Cap) Entries.RemoveAt(0);
}
}