worldbox-observer-mod/IdleSpectator/Story/LifeSagaOverview.cs
2026-07-21 23:46:20 -05:00

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 "";
}
}