worldbox-observer-mod/IdleSpectator/Narrative/InterruptPolicy.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

94 lines
3.5 KiB
C#

using System;
namespace IdleSpectator;
public enum NarrativeInterruptClass
{
None,
RelatedEscalation,
StoryPayoff,
WorldCritical,
OrdinaryInteresting,
TextureNoise
}
/// <summary>Explicit editorial interrupt classification for episode scheduling.</summary>
public static class InterruptPolicy
{
public static NarrativeInterruptClass Classify(
InterestCandidate candidate,
EpisodePlan episode,
NarrativeThread thread)
{
if (candidate == null) return NarrativeInterruptClass.None;
NarrativeFunction function = NarrativeFunctionPolicy.Classify(candidate);
if (function == NarrativeFunction.Noise || function == NarrativeFunction.CharacterTexture)
{
return NarrativeInterruptClass.TextureNoise;
}
if (IsRelated(candidate, episode, thread)
&& NarrativeFunctionPolicy.ChangesStoryState(function))
{
return NarrativeInterruptClass.RelatedEscalation;
}
if (IsWorldCritical(candidate)) return NarrativeInterruptClass.WorldCritical;
if (IsStoryPayoff(candidate)) return NarrativeInterruptClass.StoryPayoff;
return NarrativeInterruptClass.OrdinaryInteresting;
}
public static bool MayInterrupt(NarrativeInterruptClass kind)
{
return kind == NarrativeInterruptClass.WorldCritical
|| kind == NarrativeInterruptClass.StoryPayoff
|| kind == NarrativeInterruptClass.RelatedEscalation;
}
public static bool IsRelated(InterestCandidate candidate, EpisodePlan episode, NarrativeThread thread)
{
if (candidate == null || episode == null) return false;
if (Touches(candidate, episode.ProtagonistId)) return true;
for (int i = 0; i < episode.EligibleCastIds.Count; i++)
{
if (Touches(candidate, episode.EligibleCastIds[i])) return true;
}
if (thread == null || string.IsNullOrEmpty(thread.AnchorKey)) return false;
return EqualsKey(candidate.CorrelationKey, thread.AnchorKey)
|| EqualsKey(candidate.CityKey, thread.AnchorKey)
|| EqualsKey(candidate.KingdomKey, thread.AnchorKey)
|| (candidate.Key ?? "").IndexOf(thread.AnchorKey, StringComparison.OrdinalIgnoreCase) >= 0;
}
private static bool Touches(InterestCandidate c, long id)
{
if (id == 0) return false;
if (c.SubjectId == id || c.RelatedId == id || c.PairOwnerId == id
|| c.PairPartnerId == id || c.TheaterLeadId == id
|| EventFeedUtil.SafeId(c.FollowUnit) == id) return true;
for (int i = 0; i < c.ParticipantIds.Count; i++)
{
if (c.ParticipantIds[i] == id) return true;
}
return false;
}
private static bool IsWorldCritical(InterestCandidate c)
{
if (c.EventStrength >= 95f) return true;
return c.Completion == InterestCompletionKind.EarthquakeActive
|| c.Completion == InterestCompletionKind.StatusOutbreak
|| (c.Category ?? "").IndexOf("Disaster", StringComparison.OrdinalIgnoreCase) >= 0;
}
private static bool IsStoryPayoff(InterestCandidate c)
{
return StoryReason.IsStoryAsset(c.AssetId)
|| (c.Source ?? "").IndexOf("story", StringComparison.OrdinalIgnoreCase) >= 0;
}
private static bool EqualsKey(string a, string b) =>
!string.IsNullOrEmpty(a) && !string.IsNullOrEmpty(b)
&& string.Equals(a, b, StringComparison.OrdinalIgnoreCase);
}