- 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
92 lines
3.7 KiB
C#
92 lines
3.7 KiB
C#
using System.Collections.Generic;
|
|
|
|
namespace IdleSpectator;
|
|
|
|
public sealed class EpisodeShotProposal
|
|
{
|
|
public InterestCandidate Candidate;
|
|
public NarrativeInterruptClass InterruptClass;
|
|
public string Reason = "";
|
|
public float EditorialScore;
|
|
}
|
|
|
|
/// <summary>Selects the best presentable shot that can explain its place in the active episode.</summary>
|
|
public static class EpisodeShotSelector
|
|
{
|
|
public static EpisodeShotProposal Pick(
|
|
IList<InterestCandidate> candidates,
|
|
EpisodePlan episode,
|
|
NarrativeThread thread,
|
|
bool requirePresentable = true)
|
|
{
|
|
if (candidates == null || candidates.Count == 0 || episode == null) return null;
|
|
EpisodeShotProposal bestRelated = null;
|
|
EpisodeShotProposal bestInterrupt = null;
|
|
for (int i = 0; i < candidates.Count; i++)
|
|
{
|
|
InterestCandidate c = candidates[i];
|
|
bool harness = c != null && string.Equals(c.Source, "harness", System.StringComparison.OrdinalIgnoreCase);
|
|
if (c == null || c.Selected
|
|
|| InterestVariety.IsEditorialReplayCooled(c)
|
|
|| !StoryScheduler.MayUseCombatShot(episode, c)
|
|
|| (requirePresentable && !harness && !EventPresentability.WouldShow(c))) continue;
|
|
NarrativeFunction function = NarrativeFunctionPolicy.Classify(c);
|
|
if (function == NarrativeFunction.Noise) continue;
|
|
NarrativeInterruptClass kind = InterruptPolicy.Classify(c, episode, thread);
|
|
float score = EditorialScore(c, episode, kind, function);
|
|
var proposal = new EpisodeShotProposal
|
|
{
|
|
Candidate = c,
|
|
InterruptClass = kind,
|
|
EditorialScore = score,
|
|
Reason = Reason(kind)
|
|
};
|
|
if (kind == NarrativeInterruptClass.RelatedEscalation)
|
|
{
|
|
if (bestRelated == null || score > bestRelated.EditorialScore) bestRelated = proposal;
|
|
}
|
|
else if (InterruptPolicy.MayInterrupt(kind)
|
|
&& (bestInterrupt == null || score > bestInterrupt.EditorialScore))
|
|
{
|
|
bestInterrupt = proposal;
|
|
}
|
|
}
|
|
|
|
// A productive related development owns the episode. Critical/payoff material interrupts
|
|
// only when no related shot is ready, or when its editorial advantage is decisive.
|
|
if (bestRelated != null
|
|
&& (bestInterrupt == null || bestInterrupt.EditorialScore < bestRelated.EditorialScore + 25f))
|
|
{
|
|
return bestRelated;
|
|
}
|
|
|
|
return bestInterrupt ?? bestRelated;
|
|
}
|
|
|
|
private static float EditorialScore(
|
|
InterestCandidate c,
|
|
EpisodePlan episode,
|
|
NarrativeInterruptClass kind,
|
|
NarrativeFunction function)
|
|
{
|
|
float relation = c.SubjectId == episode.ProtagonistId ? 40f
|
|
: c.RelatedId == episode.ProtagonistId ? 30f : 0f;
|
|
float interrupt = kind == NarrativeInterruptClass.WorldCritical ? 80f
|
|
: kind == NarrativeInterruptClass.StoryPayoff ? 35f
|
|
: kind == NarrativeInterruptClass.RelatedEscalation ? 25f : 0f;
|
|
return relation + interrupt + NarrativeFunctionPolicy.EditorialBonus(function)
|
|
+ c.EventStrength * 0.35f
|
|
+ c.VisualConfidence * 10f + c.Novelty * 3f;
|
|
}
|
|
|
|
private static string Reason(NarrativeInterruptClass kind)
|
|
{
|
|
switch (kind)
|
|
{
|
|
case NarrativeInterruptClass.RelatedEscalation: return "active_episode";
|
|
case NarrativeInterruptClass.WorldCritical: return "world_critical_interrupt";
|
|
case NarrativeInterruptClass.StoryPayoff: return "story_payoff_interrupt";
|
|
default: return "not_episode_worthy";
|
|
}
|
|
}
|
|
}
|