504 lines
12 KiB
C#
504 lines
12 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Text;
|
|
using UnityEngine;
|
|
|
|
namespace IdleSpectator;
|
|
|
|
/// <summary>
|
|
/// Multi-line saga hover card: Title / Why / Circle / Chapters.
|
|
/// No trait descs, no Chronicle dump, no planner jargon.
|
|
/// </summary>
|
|
public static class LifeSagaOverview
|
|
{
|
|
private const int EpithetMinScore = 80;
|
|
|
|
public static string Build(LifeSagaSlot slot)
|
|
{
|
|
if (slot == null || slot.UnitId == 0)
|
|
{
|
|
return "";
|
|
}
|
|
|
|
Actor actor = slot.Resolve();
|
|
var sb = new StringBuilder(256);
|
|
string title = BuildTitle(slot, actor);
|
|
if (!string.IsNullOrEmpty(title))
|
|
{
|
|
sb.Append(title);
|
|
}
|
|
|
|
string why = BuildWhy(slot, actor);
|
|
AppendLine(sb, why);
|
|
|
|
string circle = BuildCircle(actor);
|
|
AppendLine(sb, circle);
|
|
|
|
string chapters = BuildChapters(slot);
|
|
AppendLine(sb, chapters);
|
|
|
|
return sb.ToString().TrimEnd();
|
|
}
|
|
|
|
private static void AppendLine(StringBuilder sb, string line)
|
|
{
|
|
if (string.IsNullOrEmpty(line))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (sb.Length > 0)
|
|
{
|
|
sb.Append('\n');
|
|
}
|
|
|
|
sb.Append(line);
|
|
}
|
|
|
|
private static string BuildTitle(LifeSagaSlot slot, Actor actor)
|
|
{
|
|
string name = !string.IsNullOrEmpty(slot.DisplayName)
|
|
? slot.DisplayName
|
|
: "Someone";
|
|
string epithet = TryEpithetName(actor);
|
|
string role = BuildRole(slot, actor);
|
|
bool starred = slot.Prefer || slot.GameFavorite;
|
|
|
|
var sb = new StringBuilder(64);
|
|
if (starred)
|
|
{
|
|
sb.Append("★ ");
|
|
}
|
|
|
|
sb.Append(name);
|
|
if (!string.IsNullOrEmpty(epithet))
|
|
{
|
|
sb.Append(" the ").Append(epithet);
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(role))
|
|
{
|
|
sb.Append(" · ").Append(role);
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
private static string BuildRole(LifeSagaSlot slot, Actor actor)
|
|
{
|
|
if (slot.IsKing)
|
|
{
|
|
string kingdom = FirstNonEmpty(slot.KingdomKey, KingdomName(actor));
|
|
return string.IsNullOrEmpty(kingdom) ? "King" : "King of " + kingdom;
|
|
}
|
|
|
|
if (slot.IsAlpha)
|
|
{
|
|
return "Alpha of the pack";
|
|
}
|
|
|
|
if (slot.IsLeader)
|
|
{
|
|
string city = FirstNonEmpty(slot.CityKey, CityName(actor));
|
|
return string.IsNullOrEmpty(city) ? "City leader" : "Leader of " + city;
|
|
}
|
|
|
|
string species = SpeciesLabel(slot, actor);
|
|
string kingdomFallback = FirstNonEmpty(slot.KingdomKey, KingdomName(actor));
|
|
if (!string.IsNullOrEmpty(species) && !string.IsNullOrEmpty(kingdomFallback))
|
|
{
|
|
return species + " of " + kingdomFallback;
|
|
}
|
|
|
|
return FirstNonEmpty(species, kingdomFallback);
|
|
}
|
|
|
|
private static string BuildWhy(LifeSagaSlot slot, Actor actor)
|
|
{
|
|
// Title already carries role / favorite star - Why adds standing/event only.
|
|
var parts = new List<string>(3);
|
|
if (slot.Kills >= 10)
|
|
{
|
|
parts.Add(slot.Kills + " kills");
|
|
}
|
|
|
|
if (slot.Renown >= 80f)
|
|
{
|
|
parts.Add("renown " + Mathf.RoundToInt(slot.Renown));
|
|
}
|
|
|
|
if (IsAtWar(actor))
|
|
{
|
|
string side = FirstNonEmpty(slot.KingdomKey, KingdomName(actor), SpeciesLabel(slot, actor));
|
|
if (!string.IsNullOrEmpty(side))
|
|
{
|
|
parts.Add("leading " + side + " in war");
|
|
}
|
|
else
|
|
{
|
|
parts.Add("at war");
|
|
}
|
|
}
|
|
else if (slot.IsKing && parts.Count == 0)
|
|
{
|
|
// Newly crowned with no kill/war signal - omit rather than reprint King.
|
|
}
|
|
|
|
if (parts.Count == 0)
|
|
{
|
|
return "";
|
|
}
|
|
|
|
return string.Join(" · ", parts);
|
|
}
|
|
|
|
private static string BuildCircle(Actor actor)
|
|
{
|
|
if (actor == null)
|
|
{
|
|
return "";
|
|
}
|
|
|
|
var parts = new List<string>(3);
|
|
Actor lover = ActorRelation.GetLover(actor);
|
|
if (lover != null)
|
|
{
|
|
parts.Add("Lover " + SafeName(lover));
|
|
}
|
|
|
|
Actor friend = ActorRelation.GetBestFriend(actor);
|
|
if (friend != null && friend != lover)
|
|
{
|
|
parts.Add("Friend " + SafeName(friend));
|
|
}
|
|
|
|
foreach (Actor child in ActorRelation.EnumerateChildren(actor, max: 1))
|
|
{
|
|
if (child != null && child != lover && child != friend)
|
|
{
|
|
parts.Add("Child " + SafeName(child));
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (parts.Count == 0)
|
|
{
|
|
return "";
|
|
}
|
|
|
|
return string.Join(" · ", parts);
|
|
}
|
|
|
|
private static string BuildChapters(LifeSagaSlot slot)
|
|
{
|
|
if (slot.Chapters == null || slot.Chapters.Count == 0)
|
|
{
|
|
return "";
|
|
}
|
|
|
|
var sb = new StringBuilder(128);
|
|
int n = Mathf.Min(LifeSagaRoster.MaxChapters, slot.Chapters.Count);
|
|
for (int i = 0; i < n; i++)
|
|
{
|
|
string line = slot.Chapters[i];
|
|
if (string.IsNullOrEmpty(line))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (sb.Length > 0)
|
|
{
|
|
sb.Append('\n');
|
|
}
|
|
|
|
sb.Append("· ").Append(line);
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
public static float StandoutTraitScore(Actor actor)
|
|
{
|
|
ActorTrait top = PickTopTrait(actor, out int score);
|
|
if (top == null)
|
|
{
|
|
return 0f;
|
|
}
|
|
|
|
return Mathf.Max(0, score);
|
|
}
|
|
|
|
private static string TryEpithetName(Actor actor)
|
|
{
|
|
ActorTrait top = PickTopTrait(actor, out int score);
|
|
if (top == null || score < EpithetMinScore)
|
|
{
|
|
return "";
|
|
}
|
|
|
|
return TraitDisplayName(top);
|
|
}
|
|
|
|
private static ActorTrait PickTopTrait(Actor actor, out int bestScore)
|
|
{
|
|
bestScore = int.MinValue;
|
|
if (actor == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
try
|
|
{
|
|
if (!actor.hasTraits())
|
|
{
|
|
return null;
|
|
}
|
|
|
|
ActorTrait best = null;
|
|
foreach (ActorTrait trait in actor.getTraits())
|
|
{
|
|
if (trait == null || string.IsNullOrEmpty(trait.id))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (IsDefaultTraitForActor(trait, actor))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
int score = TraitInterestScore(trait, actor);
|
|
if (score > bestScore)
|
|
{
|
|
bestScore = score;
|
|
best = trait;
|
|
}
|
|
}
|
|
|
|
return best;
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static int TraitInterestScore(ActorTrait trait, Actor actor)
|
|
{
|
|
if (trait == null)
|
|
{
|
|
return int.MinValue;
|
|
}
|
|
|
|
int score = 0;
|
|
try
|
|
{
|
|
score += (int)trait.rarity * 100;
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
try
|
|
{
|
|
if (trait.type == TraitType.Negative)
|
|
{
|
|
score += 25;
|
|
}
|
|
else if (trait.type == TraitType.Positive)
|
|
{
|
|
score += 15;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
if (!IsDefaultTraitForActor(trait, actor))
|
|
{
|
|
score += 50;
|
|
}
|
|
else
|
|
{
|
|
score -= 40;
|
|
}
|
|
|
|
try
|
|
{
|
|
if (trait.rate_birth > 0 && trait.rate_birth <= 5)
|
|
{
|
|
score += 20;
|
|
}
|
|
else if (trait.rate_birth >= 40)
|
|
{
|
|
score -= 15;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
string id = trait.id ?? "";
|
|
if (id.IndexOf("miracle", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| id.IndexOf("immortal", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| id.IndexOf("zombie", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| id.IndexOf("plague", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| id.IndexOf("blessed", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| id.IndexOf("cursed", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| id.IndexOf("king", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| id.IndexOf("evil", StringComparison.OrdinalIgnoreCase) >= 0)
|
|
{
|
|
score += 35;
|
|
}
|
|
|
|
return score;
|
|
}
|
|
|
|
private static bool IsDefaultTraitForActor(ActorTrait trait, Actor actor)
|
|
{
|
|
try
|
|
{
|
|
if (trait == null || actor == null || actor.asset == null
|
|
|| trait.default_for_actor_assets == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return trait.default_for_actor_assets.Contains(actor.asset);
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static string TraitDisplayName(ActorTrait trait)
|
|
{
|
|
try
|
|
{
|
|
string locale = trait.getLocaleID();
|
|
if (!string.IsNullOrEmpty(locale))
|
|
{
|
|
string localized = LocalizedTextManager.getText(locale);
|
|
if (!string.IsNullOrEmpty(localized) && localized != locale)
|
|
{
|
|
return localized;
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// fall through
|
|
}
|
|
|
|
string id = trait.id ?? "";
|
|
if (id.Length == 0)
|
|
{
|
|
return "";
|
|
}
|
|
|
|
return char.ToUpperInvariant(id[0]) + id.Substring(1).Replace('_', ' ');
|
|
}
|
|
|
|
private static bool IsAtWar(Actor actor)
|
|
{
|
|
if (actor == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
Kingdom k = actor.kingdom;
|
|
if (k == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return k.hasEnemies();
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static string SpeciesLabel(LifeSagaSlot slot, Actor actor)
|
|
{
|
|
string id = FirstNonEmpty(slot?.SpeciesId, actor?.asset != null ? actor.asset.id : "");
|
|
if (string.IsNullOrEmpty(id))
|
|
{
|
|
return "";
|
|
}
|
|
|
|
if (id.Length == 1)
|
|
{
|
|
return id.ToUpperInvariant();
|
|
}
|
|
|
|
return char.ToUpperInvariant(id[0]) + id.Substring(1);
|
|
}
|
|
|
|
private static string KingdomName(Actor actor)
|
|
{
|
|
try
|
|
{
|
|
return actor?.kingdom != null ? (actor.kingdom.name ?? "") : "";
|
|
}
|
|
catch
|
|
{
|
|
return "";
|
|
}
|
|
}
|
|
|
|
private static string CityName(Actor actor)
|
|
{
|
|
try
|
|
{
|
|
return actor?.city != null ? (actor.city.name ?? "") : "";
|
|
}
|
|
catch
|
|
{
|
|
return "";
|
|
}
|
|
}
|
|
|
|
private static string SafeName(Actor actor)
|
|
{
|
|
try
|
|
{
|
|
string n = actor.getName();
|
|
if (!string.IsNullOrEmpty(n))
|
|
{
|
|
return n;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
return actor?.asset != null ? actor.asset.id : "Someone";
|
|
}
|
|
|
|
private static string FirstNonEmpty(params string[] values)
|
|
{
|
|
if (values == null)
|
|
{
|
|
return "";
|
|
}
|
|
|
|
for (int i = 0; i < values.Length; i++)
|
|
{
|
|
if (!string.IsNullOrEmpty(values[i]))
|
|
{
|
|
return values[i];
|
|
}
|
|
}
|
|
|
|
return "";
|
|
}
|
|
}
|