240 lines
5.3 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|