- Introduce new narrative probes for episode grammar, ensemble balance, and prose redundancy - Implement narrative presentation coverage tracking for better event visibility - Update harness scenarios to include new narrative ingestion and persistence tests - Refactor event emission to include narrative development IDs and presentations - Expand InterestCandidate to support narrative presentation models and roles
130 lines
4.1 KiB
C#
130 lines
4.1 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace IdleSpectator;
|
|
|
|
/// <summary>
|
|
/// Bounded authored-cut ledger. Camera hold seconds do not count; only meaningful selections do.
|
|
/// </summary>
|
|
public static class NarrativeExposureLedger
|
|
{
|
|
private sealed class Exposure
|
|
{
|
|
public int Cuts;
|
|
public float LastCutAt = -999f;
|
|
public string LastSemanticFamily = "";
|
|
public int SameFamilyRun;
|
|
}
|
|
|
|
private static readonly Dictionary<long, Exposure> Characters =
|
|
new Dictionary<long, Exposure>();
|
|
private static readonly Dictionary<string, Exposure> Threads =
|
|
new Dictionary<string, Exposure>(StringComparer.Ordinal);
|
|
private static int _maxCharacterCuts;
|
|
private static int _maxThreadCuts;
|
|
|
|
public static void Clear()
|
|
{
|
|
Characters.Clear();
|
|
Threads.Clear();
|
|
_maxCharacterCuts = 0;
|
|
_maxThreadCuts = 0;
|
|
}
|
|
|
|
public static void NoteCut(
|
|
long characterId,
|
|
string threadId,
|
|
string semanticFamily,
|
|
float now)
|
|
{
|
|
if (characterId != 0)
|
|
{
|
|
Exposure exposure = Get(Characters, characterId);
|
|
Update(exposure, semanticFamily, now);
|
|
_maxCharacterCuts = Math.Max(_maxCharacterCuts, exposure.Cuts);
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(threadId))
|
|
{
|
|
Exposure exposure = Get(Threads, threadId);
|
|
Update(exposure, semanticFamily, now);
|
|
_maxThreadCuts = Math.Max(_maxThreadCuts, exposure.Cuts);
|
|
}
|
|
}
|
|
|
|
public static float CharacterDebt(long characterId)
|
|
{
|
|
if (characterId == 0) return 0f;
|
|
int cuts = Characters.TryGetValue(characterId, out Exposure exposure)
|
|
? exposure.Cuts : 0;
|
|
return Mathf.Min(12f, Mathf.Max(0, _maxCharacterCuts - cuts) * 1.5f);
|
|
}
|
|
|
|
public static float ThreadDebt(string threadId)
|
|
{
|
|
if (string.IsNullOrEmpty(threadId)) return 0f;
|
|
int cuts = Threads.TryGetValue(threadId, out Exposure exposure)
|
|
? exposure.Cuts : 0;
|
|
return Mathf.Min(10f, Mathf.Max(0, _maxThreadCuts - cuts) * 1.25f);
|
|
}
|
|
|
|
public static float RepetitionPenalty(long characterId, string semanticFamily)
|
|
{
|
|
if (characterId == 0 || string.IsNullOrEmpty(semanticFamily)
|
|
|| !Characters.TryGetValue(characterId, out Exposure exposure)
|
|
|| !string.Equals(
|
|
exposure.LastSemanticFamily, semanticFamily, StringComparison.Ordinal))
|
|
{
|
|
return 0f;
|
|
}
|
|
|
|
return Mathf.Min(24f, exposure.SameFamilyRun * 6f);
|
|
}
|
|
|
|
public static bool HarnessProbeBalance(out string detail)
|
|
{
|
|
Clear();
|
|
NoteCut(1, "thread:a", "combat", 1f);
|
|
NoteCut(1, "thread:a", "combat", 2f);
|
|
NoteCut(1, "thread:a", "combat", 3f);
|
|
NoteCut(2, "thread:b", "lineage", 4f);
|
|
float underCovered = CharacterDebt(2);
|
|
float prolific = CharacterDebt(1);
|
|
float repetition = RepetitionPenalty(1, "combat");
|
|
float diverse = RepetitionPenalty(1, "lineage");
|
|
bool ok = underCovered > prolific && repetition > 0f && diverse == 0f;
|
|
detail = "debt=" + underCovered.ToString("0.0") + "/" + prolific.ToString("0.0")
|
|
+ " repetition=" + repetition.ToString("0.0") + "/" + diverse.ToString("0.0")
|
|
+ " pass=" + ok;
|
|
Clear();
|
|
return ok;
|
|
}
|
|
|
|
private static Exposure Get<TKey>(Dictionary<TKey, Exposure> map, TKey key)
|
|
{
|
|
if (!map.TryGetValue(key, out Exposure exposure))
|
|
{
|
|
exposure = new Exposure();
|
|
map[key] = exposure;
|
|
}
|
|
|
|
return exposure;
|
|
}
|
|
|
|
private static void Update(Exposure exposure, string family, float now)
|
|
{
|
|
exposure.Cuts++;
|
|
exposure.LastCutAt = now;
|
|
if (!string.IsNullOrEmpty(family)
|
|
&& string.Equals(exposure.LastSemanticFamily, family, StringComparison.Ordinal))
|
|
{
|
|
exposure.SameFamilyRun++;
|
|
}
|
|
else
|
|
{
|
|
exposure.LastSemanticFamily = family ?? "";
|
|
exposure.SameFamilyRun = 1;
|
|
}
|
|
}
|
|
}
|