worldbox-observer-mod/IdleSpectator/UnitDossier.cs
2026-07-14 15:55:22 -05:00

243 lines
6.8 KiB
C#

using System.Collections.Generic;
using System.Text;
using UnityEngine;
namespace IdleSpectator;
/// <summary>
/// Snapshot of why a living unit is interesting (caption + chronicle source).
/// </summary>
public sealed class UnitDossier
{
public long UnitId;
public string Name = "";
public string SpeciesId = "";
public string Headline = "";
public string DetailLine = "";
public string CaptionText = "";
public float Score;
public int Kills;
public int Age;
public int Renown;
public bool IsFavorite;
public bool IsKing;
public bool IsCityLeader;
public bool IsFighting;
public bool IsSpectacle;
public bool IsLoneSpecies;
public string KingdomName = "";
public string CityName = "";
public readonly List<string> ScoreReasons = new List<string>();
public static UnitDossier FromActor(Actor actor, InterestTier? watchTier = null, string watchLabel = null)
{
UnitDossier d = new UnitDossier();
if (actor == null || !actor.isAlive())
{
d.Headline = "Nobody";
d.DetailLine = "no living unit";
d.CaptionText = d.Headline;
return d;
}
Dictionary<string, int> speciesCounts = WorldActivityScanner.CountSpeciesPopulations();
d.UnitId = actor.getID();
d.Name = SafeName(actor);
d.SpeciesId = actor.asset != null ? actor.asset.id : "creature";
d.Kills = actor.data != null ? actor.data.kills : 0;
d.Age = SafeAge(actor);
d.Renown = actor.renown;
d.IsFavorite = actor.isFavorite();
d.IsKing = actor.isKing();
d.IsCityLeader = actor.isCityLeader();
d.IsFighting = actor.has_attack_target;
d.IsSpectacle = WorldActivityScanner.IsSpectaclePublic(actor);
d.IsLoneSpecies = WorldActivityScanner.IsSingletonSpeciesPublic(actor, speciesCounts);
d.KingdomName = actor.kingdom != null ? actor.kingdom.name : "";
d.CityName = actor.city != null ? actor.city.name : "";
d.Score = WorldActivityScanner.ScoreActorPublic(actor, speciesCounts);
CollectReasons(d, actor, speciesCounts);
string tierPrefix = watchTier.HasValue ? $"Watching [{watchTier.Value}]: " : "";
d.Headline = tierPrefix + (string.IsNullOrEmpty(watchLabel)
? WorldActivityScanner.FormatUnitLabelPublic(actor, speciesCounts)
: watchLabel);
d.DetailLine = BuildDetailLine(d);
d.CaptionText = string.IsNullOrEmpty(d.DetailLine)
? d.Headline
: d.Headline + "\n" + d.DetailLine;
return d;
}
public bool ContainsIgnoreCase(string needle)
{
if (string.IsNullOrEmpty(needle))
{
return false;
}
return (CaptionText ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|| (Headline ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|| (DetailLine ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|| (Name ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0
|| (SpeciesId ?? "").IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0;
}
private static void CollectReasons(UnitDossier d, Actor actor, Dictionary<string, int> speciesCounts)
{
if (d.IsFavorite)
{
d.ScoreReasons.Add("favorite");
}
if (d.IsKing)
{
d.ScoreReasons.Add("king");
}
else if (d.IsCityLeader)
{
d.ScoreReasons.Add("leader");
}
else if (actor.is_profession_warrior)
{
d.ScoreReasons.Add("warrior");
}
if (d.IsFighting)
{
d.ScoreReasons.Add("fighting");
}
if (d.Kills >= 100)
{
d.ScoreReasons.Add("100+ kills");
}
else if (d.Kills >= 50)
{
d.ScoreReasons.Add("50+ kills");
}
else if (d.Kills >= 10)
{
d.ScoreReasons.Add("10+ kills");
}
else if (d.Kills > 0)
{
d.ScoreReasons.Add($"{d.Kills} kills");
}
if (d.IsLoneSpecies)
{
d.ScoreReasons.Add("lone of kind");
}
if (d.IsSpectacle)
{
d.ScoreReasons.Add("spectacle");
}
if (d.Renown >= 100)
{
d.ScoreReasons.Add($"renown {d.Renown}");
}
// A few trait ids for flavor (keep short).
try
{
if (actor.hasTraits())
{
int n = 0;
foreach (ActorTrait trait in actor.getTraits())
{
if (trait == null || string.IsNullOrEmpty(trait.id))
{
continue;
}
d.ScoreReasons.Add(trait.id);
n++;
if (n >= 2)
{
break;
}
}
}
}
catch
{
// ignore trait access failures on exotic units
}
}
private static string BuildDetailLine(UnitDossier d)
{
StringBuilder sb = new StringBuilder();
sb.Append(d.SpeciesId);
if (d.Age > 0)
{
sb.Append(" · age ").Append(d.Age);
}
if (d.Kills > 0)
{
sb.Append(" · ").Append(d.Kills).Append(" kills");
}
if (!string.IsNullOrEmpty(d.KingdomName))
{
sb.Append(" · ").Append(d.KingdomName);
}
else if (!string.IsNullOrEmpty(d.CityName))
{
sb.Append(" · ").Append(d.CityName);
}
for (int i = 0; i < d.ScoreReasons.Count && i < 4; i++)
{
string reason = d.ScoreReasons[i];
// Skip reasons already implied by headline pieces.
if (reason == "fighting" || reason == "king" || reason == "favorite")
{
continue;
}
if (sb.ToString().IndexOf(reason, System.StringComparison.OrdinalIgnoreCase) >= 0)
{
continue;
}
sb.Append(" · ").Append(reason);
}
return sb.ToString();
}
private static string SafeName(Actor actor)
{
try
{
string name = actor.getName();
if (!string.IsNullOrEmpty(name))
{
return name;
}
}
catch
{
// fall through
}
return actor.asset != null ? actor.asset.id : "unit";
}
private static int SafeAge(Actor actor)
{
try
{
return actor.getAge();
}
catch
{
return 0;
}
}
}