440 lines
12 KiB
C#
440 lines
12 KiB
C#
using System.Collections.Generic;
|
|
using System.Text;
|
|
|
|
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 ReasonLine = "";
|
|
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.ReasonLine = "";
|
|
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);
|
|
|
|
d.Headline = $"{d.Name} ({d.SpeciesId})";
|
|
d.ReasonLine = BuildReasonLine(d, watchTier, watchLabel, actor, speciesCounts);
|
|
d.DetailLine = BuildDetailLine(d);
|
|
d.CaptionText = JoinCaption(d.Headline, d.ReasonLine, 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
|
|
|| (ReasonLine ?? "").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 string JoinCaption(string headline, string reason, string detail)
|
|
{
|
|
StringBuilder sb = new StringBuilder();
|
|
if (!string.IsNullOrEmpty(headline))
|
|
{
|
|
sb.Append(headline);
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(reason))
|
|
{
|
|
if (sb.Length > 0)
|
|
{
|
|
sb.Append('\n');
|
|
}
|
|
|
|
sb.Append(reason);
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(detail))
|
|
{
|
|
if (sb.Length > 0)
|
|
{
|
|
sb.Append('\n');
|
|
}
|
|
|
|
sb.Append(detail);
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
private static string BuildReasonLine(UnitDossier d, InterestTier? watchTier, string watchLabel, Actor actor,
|
|
Dictionary<string, int> speciesCounts)
|
|
{
|
|
string tierPart = watchTier.HasValue ? watchTier.Value.ToString() : "";
|
|
string why = "";
|
|
if (!string.IsNullOrEmpty(watchLabel))
|
|
{
|
|
why = ShortenWatchLabel(watchLabel, d);
|
|
}
|
|
else
|
|
{
|
|
why = ShortenWatchLabel(
|
|
WorldActivityScanner.FormatUnitLabelPublic(actor, speciesCounts), d);
|
|
// Ambient focus: FormatUnitLabel often equals the headline ("Name (species)").
|
|
if (IsRedundantWithHeadline(why, d))
|
|
{
|
|
why = FirstFlavorTag(d);
|
|
}
|
|
}
|
|
|
|
if (IsRedundantWithHeadline(why, d))
|
|
{
|
|
why = "";
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(tierPart))
|
|
{
|
|
return why ?? "";
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(why) || why.IndexOf(tierPart, System.StringComparison.OrdinalIgnoreCase) >= 0)
|
|
{
|
|
return tierPart;
|
|
}
|
|
|
|
return tierPart + " · " + why;
|
|
}
|
|
|
|
private static string FirstFlavorTag(UnitDossier d)
|
|
{
|
|
for (int i = 0; i < d.ScoreReasons.Count; i++)
|
|
{
|
|
string reason = Humanize(d.ScoreReasons[i]);
|
|
if (string.IsNullOrEmpty(reason) || IsRedundantWithHeadline(reason, d))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (reason.IndexOf("kills", System.StringComparison.OrdinalIgnoreCase) >= 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
return reason;
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
private static bool IsRedundantWithHeadline(string text, UnitDossier d)
|
|
{
|
|
if (string.IsNullOrEmpty(text) || d == null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (string.Equals(text, d.Headline, System.StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (string.Equals(text, d.Name, System.StringComparison.OrdinalIgnoreCase)
|
|
|| string.Equals(text, d.SpeciesId, System.StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// "Name (species)" variants already covered by headline.
|
|
if (!string.IsNullOrEmpty(d.Name) && !string.IsNullOrEmpty(d.SpeciesId)
|
|
&& text.IndexOf(d.Name, System.StringComparison.OrdinalIgnoreCase) >= 0
|
|
&& text.IndexOf(d.SpeciesId, System.StringComparison.OrdinalIgnoreCase) >= 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static string ShortenWatchLabel(string label, UnitDossier d)
|
|
{
|
|
if (string.IsNullOrEmpty(label))
|
|
{
|
|
return "";
|
|
}
|
|
|
|
string s = label.Trim();
|
|
if (s.StartsWith("New species:", System.StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return "New species";
|
|
}
|
|
|
|
if (s.StartsWith("Watching [", System.StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
int close = s.IndexOf(']');
|
|
if (close > 0 && close + 1 < s.Length)
|
|
{
|
|
s = s.Substring(close + 1).TrimStart(':', ' ');
|
|
}
|
|
}
|
|
|
|
// Drop trailing "Name" duplicates like "Lone wolf: Waf" -> keep short form.
|
|
int colon = s.IndexOf(':');
|
|
if (colon > 0 && colon < 28)
|
|
{
|
|
s = s.Substring(0, colon).Trim();
|
|
}
|
|
|
|
if (d != null && IsRedundantWithHeadline(s, d))
|
|
{
|
|
return "";
|
|
}
|
|
|
|
if (s.Length > 36)
|
|
{
|
|
s = s.Substring(0, 33) + "...";
|
|
}
|
|
|
|
return s;
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
try
|
|
{
|
|
if (actor.hasTraits())
|
|
{
|
|
int n = 0;
|
|
foreach (ActorTrait trait in actor.getTraits())
|
|
{
|
|
if (trait == null || string.IsNullOrEmpty(trait.id))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
d.ScoreReasons.Add(Humanize(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();
|
|
if (d.Age > 0)
|
|
{
|
|
sb.Append("age ").Append(d.Age);
|
|
}
|
|
|
|
if (d.Kills > 0)
|
|
{
|
|
if (sb.Length > 0)
|
|
{
|
|
sb.Append(" · ");
|
|
}
|
|
|
|
sb.Append(d.Kills).Append(" kills");
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(d.KingdomName)
|
|
&& !string.Equals(d.KingdomName, d.SpeciesId, System.StringComparison.OrdinalIgnoreCase)
|
|
&& !string.Equals(d.KingdomName, d.Name, System.StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
if (sb.Length > 0)
|
|
{
|
|
sb.Append(" · ");
|
|
}
|
|
|
|
sb.Append(d.KingdomName);
|
|
}
|
|
else if (!string.IsNullOrEmpty(d.CityName)
|
|
&& !string.Equals(d.CityName, d.SpeciesId, System.StringComparison.OrdinalIgnoreCase)
|
|
&& !string.Equals(d.CityName, d.Name, System.StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
if (sb.Length > 0)
|
|
{
|
|
sb.Append(" · ");
|
|
}
|
|
|
|
sb.Append(d.CityName);
|
|
}
|
|
|
|
int tags = 0;
|
|
for (int i = 0; i < d.ScoreReasons.Count && tags < 2; i++)
|
|
{
|
|
string reason = d.ScoreReasons[i];
|
|
if (string.IsNullOrEmpty(reason))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (reason.IndexOf("kills", System.StringComparison.OrdinalIgnoreCase) >= 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
string shown = Humanize(reason);
|
|
if (IsRedundantWithHeadline(shown, d)
|
|
|| sb.ToString().IndexOf(shown, System.StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| (!string.IsNullOrEmpty(d.ReasonLine)
|
|
&& d.ReasonLine.IndexOf(shown, System.StringComparison.OrdinalIgnoreCase) >= 0))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (sb.Length > 0)
|
|
{
|
|
sb.Append(" · ");
|
|
}
|
|
|
|
sb.Append(shown);
|
|
tags++;
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
private static string Humanize(string raw)
|
|
{
|
|
if (string.IsNullOrEmpty(raw))
|
|
{
|
|
return "";
|
|
}
|
|
|
|
return raw.Replace('_', ' ');
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|