worldbox-observer-mod/IdleSpectator/Story/CharacterLedger.cs
2026-07-21 13:35:07 -05:00

240 lines
5.3 KiB
C#

using System.Collections.Generic;
using UnityEngine;
namespace IdleSpectator;
/// <summary>
/// Small cast of watched lives. Heat from favorites, focus, CausalHeat, and Chronicle density.
/// </summary>
public static class CharacterLedger
{
private struct LedgerEntry
{
public long UnitId;
public float Heat;
public float TouchedAt;
}
private static readonly List<LedgerEntry> Entries = new List<LedgerEntry>(20);
private static readonly List<int> ScratchIdx = new List<int>(8);
public static void Clear()
{
Entries.Clear();
}
public static int Count => Entries.Count;
public static bool IsOnLedger(long unitId)
{
if (unitId == 0)
{
return false;
}
for (int i = 0; i < Entries.Count; i++)
{
if (Entries[i].UnitId == unitId)
{
return true;
}
}
return false;
}
public static float Heat(long unitId)
{
if (unitId == 0)
{
return 0f;
}
for (int i = 0; i < Entries.Count; i++)
{
if (Entries[i].UnitId == unitId)
{
return Entries[i].Heat;
}
}
return 0f;
}
public static float ScoreBonus(InterestCandidate c)
{
if (c == null)
{
return 0f;
}
StoryWeights s = InterestScoringConfig.Story;
float h = Heat(c.SubjectId);
if (h <= 0f && c.RelatedId != 0)
{
h = Heat(c.RelatedId) * 0.5f;
}
return h * s.ledgerWeight;
}
public static void Touch(long unitId, float heatAdd = 1f)
{
if (unitId == 0)
{
return;
}
float now = Time.unscaledTime;
for (int i = 0; i < Entries.Count; i++)
{
if (Entries[i].UnitId != unitId)
{
continue;
}
LedgerEntry e = Entries[i];
e.Heat = Mathf.Min(8f, e.Heat + heatAdd);
e.TouchedAt = now;
Entries[i] = e;
return;
}
EnsureCapacityForNew(unitId);
Entries.Add(new LedgerEntry
{
UnitId = unitId,
Heat = Mathf.Clamp(heatAdd, 0.5f, 8f),
TouchedAt = now
});
}
public static void NoteFeatured(InterestCandidate scene)
{
if (scene == null)
{
return;
}
float add = 1f;
if (scene.FollowUnit != null && Chronicle.IsFavoriteSubject(EventFeedUtil.SafeId(scene.FollowUnit)))
{
add += 1.5f;
}
if (InterestScoring.IsNotable(scene.FollowUnit))
{
add += 0.75f;
}
if (scene.SubjectId != 0)
{
Touch(scene.SubjectId, add);
float density = Chronicle.RecentHistoryCount(
scene.SubjectId,
InterestScoringConfig.Story.historyDensityWindowSeconds);
if (density > 0)
{
Touch(scene.SubjectId, Mathf.Min(2f, density * 0.15f));
}
}
if (scene.RelatedId != 0)
{
Touch(scene.RelatedId, add * 0.6f);
}
float causal = CausalHeat.Get(scene.SubjectId);
if (causal > 0.5f)
{
Touch(scene.SubjectId, Mathf.Min(1.5f, causal));
}
}
public static void Enumerate(List<long> into)
{
if (into == null)
{
return;
}
into.Clear();
for (int i = 0; i < Entries.Count; i++)
{
into.Add(Entries[i].UnitId);
}
}
public static Actor PreferLedgerUnit(IEnumerable<Actor> candidates)
{
if (candidates == null)
{
return null;
}
Actor best = null;
float bestHeat = -1f;
foreach (Actor a in candidates)
{
if (a == null || !a.isAlive())
{
continue;
}
long id = EventFeedUtil.SafeId(a);
float h = Heat(id);
if (h > bestHeat)
{
bestHeat = h;
best = a;
}
}
return bestHeat > 0f ? best : null;
}
private static void EnsureCapacityForNew(long incomingId)
{
StoryWeights s = InterestScoringConfig.Story;
int cap = s.ledgerCap > 0 ? s.ledgerCap : 16;
if (Entries.Count < cap)
{
return;
}
// Never evict current arc cast.
ScratchIdx.Clear();
int worst = -1;
float worstHeat = float.MaxValue;
for (int i = 0; i < Entries.Count; i++)
{
long id = Entries[i].UnitId;
if (StoryPlanner.Active != null && StoryPlanner.Active.ContainsCast(id))
{
continue;
}
if (id == incomingId)
{
continue;
}
float h = Entries[i].Heat;
if (h < worstHeat)
{
worstHeat = h;
worst = i;
}
}
if (worst >= 0)
{
Entries.RemoveAt(worst);
}
else if (Entries.Count > 0)
{
// All protected - drop oldest non-incoming.
Entries.RemoveAt(0);
}
}
}