- Implement checks for civilized terminology in camera focus events - Update event handling to suppress inappropriate pack language for civilized characters - Enhance narrative presentation to differentiate between family and pack language - Introduce new scenarios to validate significance of beats and relationships - Improve documentation to clarify narrative structure and event significance
837 lines
26 KiB
C#
837 lines
26 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using UnityEngine;
|
|
|
|
namespace IdleSpectator;
|
|
|
|
/// <summary>
|
|
/// Adaptive saga presentation model. Consumes live actor state + LifeSagaMemory.
|
|
/// No task/status/trait/Chronicle dumps.
|
|
/// </summary>
|
|
public static class LifeSagaPresentation
|
|
{
|
|
private static bool _heirProbeDone;
|
|
private static bool _heirApiAvailable;
|
|
private static MethodInfo _heirMethod;
|
|
private static object _heirTargetSample;
|
|
|
|
public static LifeSagaViewModel Build(long unitId)
|
|
{
|
|
var model = new LifeSagaViewModel { UnitId = unitId };
|
|
if (unitId == 0)
|
|
{
|
|
return model;
|
|
}
|
|
|
|
LifeSagaSlot slot = LifeSagaRoster.Get(unitId);
|
|
Actor actor = EventFeedUtil.FindAliveById(unitId);
|
|
LifeSagaLifeMemory memory = LifeSagaMemory.Get(unitId);
|
|
|
|
string name = FirstNonEmpty(
|
|
slot?.DisplayName,
|
|
memory?.Identity.Name,
|
|
SafeName(actor),
|
|
"Someone");
|
|
model.Name = name;
|
|
if (slot != null && (slot.Prefer || slot.GameFavorite))
|
|
{
|
|
model.Name = "★ " + name;
|
|
}
|
|
|
|
model.Title = BuildTitle(slot, actor, memory);
|
|
model.SpeciesId = FirstNonEmpty(
|
|
slot?.SpeciesId,
|
|
memory?.Identity.SpeciesId,
|
|
actor != null && actor.asset != null ? actor.asset.id : "");
|
|
ResolveLenses(slot, actor, memory, out LifeSagaLens primary, out LifeSagaLens secondary);
|
|
model.PrimaryLens = primary;
|
|
model.SecondaryLens = secondary;
|
|
|
|
LifeSagaFact stake = StrongestDisplayStake(unitId, model.Title);
|
|
model.StakeLine = stake != null
|
|
? LifeSagaMemory.FormatFactLine(stake)
|
|
: (string.IsNullOrEmpty(model.Title) ? BuildFallbackStake(slot, actor, memory) : "");
|
|
model.StakeProvenance = stake != null ? "observed" : (string.IsNullOrEmpty(model.StakeLine) ? "" : "is");
|
|
|
|
BuildCast(model, slot, actor, memory, primary);
|
|
model.LensEyebrow = FormatLensEyebrow(primary, secondary);
|
|
BuildLegacy(model, unitId, stake);
|
|
return model;
|
|
}
|
|
|
|
public static bool HarnessProbeCivilizedTerminology(Actor actor, out string detail)
|
|
{
|
|
bool civilized = false;
|
|
try
|
|
{
|
|
civilized = actor != null && actor.asset != null && actor.asset.civ;
|
|
}
|
|
catch
|
|
{
|
|
civilized = false;
|
|
}
|
|
|
|
if (!civilized)
|
|
{
|
|
detail = "civilized=false";
|
|
return false;
|
|
}
|
|
|
|
LifeSagaViewModel model = Build(EventFeedUtil.SafeId(actor));
|
|
string panel = model?.ToPlainText() ?? "";
|
|
string relation = EventReason.FamilyPack(actor, null, "join");
|
|
bool packAlpha = LifeSagaRoster.IsPackAlpha(actor);
|
|
bool leaked = panel.IndexOf("Packmate", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| panel.IndexOf("Pack Alpha", StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| relation.IndexOf("pack", StringComparison.OrdinalIgnoreCase) >= 0;
|
|
bool pass = !packAlpha && !leaked;
|
|
detail = "alpha=" + packAlpha
|
|
+ " relation='" + relation + "'"
|
|
+ " panelPack=" + (panel.IndexOf("pack", StringComparison.OrdinalIgnoreCase) >= 0)
|
|
+ " pass=" + pass;
|
|
return pass;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pick the strongest unresolved pressure that adds information beyond Identity.
|
|
/// Current-role/founding facts belong in the title, not a duplicate tooltip stake.
|
|
/// </summary>
|
|
private static LifeSagaFact StrongestDisplayStake(long unitId, string title)
|
|
{
|
|
LifeSagaFact best = null;
|
|
var facts = LifeSagaMemory.FactsFor(unitId);
|
|
for (int i = 0; i < facts.Count; i++)
|
|
{
|
|
LifeSagaFact fact = facts[i];
|
|
if (fact == null || fact.Resolved || !LifeSagaMemory.IsUnresolvedStakeCandidate(fact))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
bool repeatsIdentity = SagaProse.RoleChangeEchoesTitle(fact, title)
|
|
|| (fact.Kind == LifeSagaFactKind.Founding
|
|
&& !string.IsNullOrEmpty(title)
|
|
&& title.IndexOf("founder", StringComparison.OrdinalIgnoreCase) >= 0);
|
|
if (repeatsIdentity)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (best == null
|
|
|| fact.Strength > best.Strength
|
|
|| (Mathf.Approximately(fact.Strength, best.Strength) && fact.At > best.At))
|
|
{
|
|
best = fact;
|
|
}
|
|
}
|
|
|
|
return best;
|
|
}
|
|
|
|
/// <summary>Compatibility plain text for harness diagnostics.</summary>
|
|
public static string BuildPlainText(long unitId)
|
|
{
|
|
return Build(unitId).ToPlainText();
|
|
}
|
|
|
|
public static string BuildPlainText(LifeSagaSlot slot)
|
|
{
|
|
return Build(slot?.UnitId ?? 0).ToPlainText();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Live probe: only report an heir when a game-authored succession API is discoverable.
|
|
/// </summary>
|
|
public static bool TryGetLikelySuccessor(Actor actor, out Actor heir, out string evidence)
|
|
{
|
|
heir = null;
|
|
evidence = "";
|
|
EnsureHeirProbe();
|
|
if (!_heirApiAvailable || actor == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
object clan = actor.GetType().GetProperty("clan")?.GetValue(actor, null)
|
|
?? actor.GetType().GetField("clan")?.GetValue(actor);
|
|
if (clan == null || _heirMethod == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
object result = null;
|
|
if (_heirMethod.GetParameters().Length == 0)
|
|
{
|
|
result = _heirMethod.Invoke(clan, null);
|
|
}
|
|
else if (_heirMethod.GetParameters().Length == 1)
|
|
{
|
|
result = _heirMethod.Invoke(clan, new object[] { actor });
|
|
}
|
|
|
|
heir = result as Actor;
|
|
if (heir != null && heir.isAlive() && heir != actor)
|
|
{
|
|
evidence = "clan_succession";
|
|
return true;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// omit rather than guess
|
|
}
|
|
|
|
heir = null;
|
|
return false;
|
|
}
|
|
|
|
public static bool HeirApiAvailable
|
|
{
|
|
get
|
|
{
|
|
EnsureHeirProbe();
|
|
return _heirApiAvailable;
|
|
}
|
|
}
|
|
|
|
private static void EnsureHeirProbe()
|
|
{
|
|
if (_heirProbeDone)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_heirProbeDone = true;
|
|
try
|
|
{
|
|
Type clanType = typeof(Actor).Assembly.GetType("Clan")
|
|
?? Type.GetType("Clan, Assembly-CSharp");
|
|
if (clanType == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
string[] names =
|
|
{
|
|
"findNextHeir", "getNextHeir", "getHeir", "findHeir", "calculateHeir", "getSuccessor"
|
|
};
|
|
for (int i = 0; i < names.Length; i++)
|
|
{
|
|
MethodInfo m = clanType.GetMethod(
|
|
names[i],
|
|
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
|
if (m != null && (typeof(Actor).IsAssignableFrom(m.ReturnType) || m.ReturnType == typeof(object)))
|
|
{
|
|
_heirMethod = m;
|
|
_heirApiAvailable = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Culture succession helpers.
|
|
Type cultureType = typeof(Actor).Assembly.GetType("Culture");
|
|
if (cultureType != null)
|
|
{
|
|
MethodInfo m = cultureType.GetMethod(
|
|
"findNextHeir",
|
|
BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
|
|
if (m != null)
|
|
{
|
|
_heirMethod = m;
|
|
_heirApiAvailable = true;
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
_heirApiAvailable = false;
|
|
}
|
|
|
|
_ = _heirTargetSample;
|
|
}
|
|
|
|
private static void ResolveLenses(
|
|
LifeSagaSlot slot,
|
|
Actor actor,
|
|
LifeSagaLifeMemory memory,
|
|
out LifeSagaLens primary,
|
|
out LifeSagaLens secondary)
|
|
{
|
|
var scores = new Dictionary<LifeSagaLens, float>();
|
|
void Add(LifeSagaLens lens, float amount)
|
|
{
|
|
if (lens == LifeSagaLens.None || amount <= 0f)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!scores.TryGetValue(lens, out float cur))
|
|
{
|
|
cur = 0f;
|
|
}
|
|
|
|
scores[lens] = cur + amount;
|
|
}
|
|
|
|
if (slot != null)
|
|
{
|
|
if (slot.IsKing || slot.IsClanChief || slot.IsLeader)
|
|
{
|
|
Add(LifeSagaLens.CrownClan, 40f);
|
|
}
|
|
|
|
if (slot.IsAlpha)
|
|
{
|
|
Add(LifeSagaLens.PackAlpha, 38f);
|
|
}
|
|
|
|
if (slot.IsPlotAuthor)
|
|
{
|
|
Add(LifeSagaLens.Plotter, 36f);
|
|
}
|
|
|
|
if (slot.IsFounder)
|
|
{
|
|
Add(LifeSagaLens.FounderWanderer, 30f);
|
|
}
|
|
|
|
if (slot.IsSingleton)
|
|
{
|
|
Add(LifeSagaLens.LastOfKind, 34f);
|
|
}
|
|
|
|
if (slot.Kills > 0 || slot.IsArmyCaptain)
|
|
{
|
|
Add(LifeSagaLens.WarriorKiller, Mathf.Min(35f, 12f + slot.Kills * 0.4f));
|
|
}
|
|
|
|
switch (slot.AdmissionReason)
|
|
{
|
|
case LifeSagaAdmissionReason.King:
|
|
case LifeSagaAdmissionReason.ClanChief:
|
|
case LifeSagaAdmissionReason.CityLeader:
|
|
Add(LifeSagaLens.CrownClan, 8f);
|
|
break;
|
|
case LifeSagaAdmissionReason.Alpha:
|
|
if (LifeSagaRoster.UsesPackLanguage(actor))
|
|
{
|
|
Add(LifeSagaLens.PackAlpha, 8f);
|
|
}
|
|
break;
|
|
case LifeSagaAdmissionReason.PlotAuthor:
|
|
Add(LifeSagaLens.Plotter, 8f);
|
|
break;
|
|
case LifeSagaAdmissionReason.Founder:
|
|
Add(LifeSagaLens.FounderWanderer, 8f);
|
|
break;
|
|
case LifeSagaAdmissionReason.Singleton:
|
|
Add(LifeSagaLens.LastOfKind, 8f);
|
|
break;
|
|
case LifeSagaAdmissionReason.NotableKills:
|
|
Add(LifeSagaLens.WarriorKiller, 8f);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (memory != null)
|
|
{
|
|
for (int i = 0; i < memory.Facts.Count; i++)
|
|
{
|
|
LifeSagaFact f = memory.Facts[i];
|
|
if (f == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
switch (f.Kind)
|
|
{
|
|
case LifeSagaFactKind.BondFormed:
|
|
case LifeSagaFactKind.BondBroken:
|
|
case LifeSagaFactKind.HardArcLove:
|
|
case LifeSagaFactKind.HardArcGrief:
|
|
Add(LifeSagaLens.LoveGrief, 12f);
|
|
break;
|
|
case LifeSagaFactKind.Kill:
|
|
case LifeSagaFactKind.RivalEarned:
|
|
case LifeSagaFactKind.HardArcCombat:
|
|
Add(LifeSagaLens.WarriorKiller, 14f);
|
|
break;
|
|
case LifeSagaFactKind.WarJoin:
|
|
case LifeSagaFactKind.HardArcWar:
|
|
Add(LifeSagaLens.CrownClan, 10f);
|
|
Add(LifeSagaLens.WarriorKiller, 8f);
|
|
break;
|
|
case LifeSagaFactKind.PlotJoin:
|
|
case LifeSagaFactKind.HardArcPlot:
|
|
Add(LifeSagaLens.Plotter, 16f);
|
|
break;
|
|
case LifeSagaFactKind.Founding:
|
|
case LifeSagaFactKind.HomeGained:
|
|
case LifeSagaFactKind.HomeLost:
|
|
Add(LifeSagaLens.FounderWanderer, 12f);
|
|
break;
|
|
case LifeSagaFactKind.RoleChange:
|
|
if ((f.Note ?? "").IndexOf("alpha", StringComparison.OrdinalIgnoreCase) >= 0)
|
|
{
|
|
if (LifeSagaRoster.UsesPackLanguage(actor))
|
|
{
|
|
Add(LifeSagaLens.PackAlpha, 14f);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Add(LifeSagaLens.CrownClan, 12f);
|
|
}
|
|
|
|
break;
|
|
case LifeSagaFactKind.ParentChild:
|
|
if (LifeSagaRoster.UsesPackLanguage(actor))
|
|
{
|
|
Add(LifeSagaLens.PackAlpha, 6f);
|
|
}
|
|
Add(LifeSagaLens.LoveGrief, 4f);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (actor != null && LifeSagaRoster.IsPackAlpha(actor))
|
|
{
|
|
Add(LifeSagaLens.PackAlpha, 10f);
|
|
}
|
|
|
|
primary = LifeSagaLens.None;
|
|
secondary = LifeSagaLens.None;
|
|
if (scores.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
float best = -1f;
|
|
float second = -1f;
|
|
foreach (KeyValuePair<LifeSagaLens, float> kv in scores)
|
|
{
|
|
if (kv.Value > best)
|
|
{
|
|
second = best;
|
|
secondary = primary;
|
|
best = kv.Value;
|
|
primary = kv.Key;
|
|
}
|
|
else if (kv.Value > second)
|
|
{
|
|
second = kv.Value;
|
|
secondary = kv.Key;
|
|
}
|
|
}
|
|
|
|
if (secondary == primary || second < best * 0.45f)
|
|
{
|
|
secondary = LifeSagaLens.None;
|
|
}
|
|
}
|
|
|
|
private static string BuildTitle(LifeSagaSlot slot, Actor actor, LifeSagaLifeMemory memory)
|
|
{
|
|
return SagaProse.Title(slot, actor, memory);
|
|
}
|
|
|
|
private static string BuildFallbackStake(LifeSagaSlot slot, Actor actor, LifeSagaLifeMemory memory)
|
|
{
|
|
if (LifeSagaMemory.TryGetEarnedRival(slot?.UnitId ?? 0, out LifeSagaFact rival))
|
|
{
|
|
return SagaProse.StakeLine(rival);
|
|
}
|
|
|
|
if (slot != null)
|
|
{
|
|
string roleStake = SagaProse.RoleFallbackStake(slot.AdmissionReason, slot, actor);
|
|
if (!string.IsNullOrEmpty(roleStake))
|
|
{
|
|
return roleStake;
|
|
}
|
|
}
|
|
|
|
if (actor != null && TryGetLikelySuccessor(actor, out Actor heir, out _))
|
|
{
|
|
return SagaProse.Sentence("Succession points toward " + SafeName(heir));
|
|
}
|
|
|
|
return memory != null && memory.Facts.Count > 0
|
|
? SagaProse.FormatFactLine(memory.Facts[0])
|
|
: "";
|
|
}
|
|
|
|
private static void BuildCast(
|
|
LifeSagaViewModel model,
|
|
LifeSagaSlot slot,
|
|
Actor actor,
|
|
LifeSagaLifeMemory memory,
|
|
LifeSagaLens primary)
|
|
{
|
|
var seen = new HashSet<long>();
|
|
long selfId = model.UnitId;
|
|
seen.Add(selfId);
|
|
|
|
void AddLive(Actor other, string label, string truth)
|
|
{
|
|
if (other == null || model.Cast.Count >= LifeSagaPanel.MaxCastCandidates)
|
|
{
|
|
return;
|
|
}
|
|
|
|
long id = EventFeedUtil.SafeId(other);
|
|
if (id == 0 || !seen.Add(id))
|
|
{
|
|
return;
|
|
}
|
|
|
|
model.Cast.Add(new LifeSagaCastMember
|
|
{
|
|
UnitId = id,
|
|
Name = SafeName(other),
|
|
SpeciesId = other.asset != null ? other.asset.id : "",
|
|
Relation = label,
|
|
Truth = truth,
|
|
Alive = true
|
|
});
|
|
}
|
|
|
|
void AddObserved(LifeSagaIdentity identity, string label, string truth)
|
|
{
|
|
if (identity.Id == 0 || model.Cast.Count >= LifeSagaPanel.MaxCastCandidates || !seen.Add(identity.Id))
|
|
{
|
|
return;
|
|
}
|
|
|
|
bool alive = EventFeedUtil.FindAliveById(identity.Id) != null;
|
|
model.Cast.Add(new LifeSagaCastMember
|
|
{
|
|
UnitId = identity.Id,
|
|
Name = FirstNonEmpty(identity.Name, "Someone"),
|
|
SpeciesId = identity.SpeciesId ?? "",
|
|
Relation = label,
|
|
Truth = truth,
|
|
Alive = alive
|
|
});
|
|
}
|
|
|
|
if (actor != null)
|
|
{
|
|
AddLive(ActorRelation.GetLover(actor), SagaProse.CastLabelLive("lover"), "is");
|
|
AddLive(ActorRelation.GetBestFriend(actor), SagaProse.CastLabelLive("best friend"), "is");
|
|
foreach (Actor parent in ActorRelation.EnumerateParents(actor, 2))
|
|
{
|
|
AddLive(parent, SagaProse.CastLabelLive("parent"), "is");
|
|
}
|
|
|
|
foreach (Actor child in ActorRelation.EnumerateChildren(actor, 2))
|
|
{
|
|
AddLive(child, SagaProse.CastLabelLive("child"), "is");
|
|
}
|
|
|
|
if (TryGetLikelySuccessor(actor, out Actor heir, out _))
|
|
{
|
|
AddLive(heir, SagaProse.CastLabelLive("heir"), "likely");
|
|
}
|
|
}
|
|
|
|
if (LifeSagaMemory.TryGetEarnedRival(model.UnitId, out LifeSagaFact rivalFact))
|
|
{
|
|
string foeLabel = SagaProse.CastLabel(rivalFact, EventFeedUtil.FindAliveById(rivalFact.OtherId));
|
|
if (string.IsNullOrEmpty(foeLabel))
|
|
{
|
|
foeLabel = SagaProse.TitleLabel("Old Foe");
|
|
}
|
|
|
|
Actor liveRival = EventFeedUtil.FindAliveById(rivalFact.OtherId);
|
|
if (liveRival != null)
|
|
{
|
|
AddLive(liveRival, foeLabel, "observed");
|
|
}
|
|
else
|
|
{
|
|
AddObserved(rivalFact.Other, foeLabel, "observed");
|
|
}
|
|
}
|
|
|
|
// Former bonds from memory when live cast still has room.
|
|
if (memory != null && model.Cast.Count < LifeSagaPanel.MaxCastCandidates)
|
|
{
|
|
for (int i = 0; i < memory.Facts.Count && model.Cast.Count < LifeSagaPanel.MaxCastCandidates; i++)
|
|
{
|
|
LifeSagaFact f = memory.Facts[i];
|
|
if (f == null || f.OtherId == 0 || seen.Contains(f.OtherId))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (f.Kind == LifeSagaFactKind.BondBroken)
|
|
{
|
|
AddObserved(f.Other, SagaProse.CastLabelLive("once loved"), "observed");
|
|
}
|
|
else if (f.Kind == LifeSagaFactKind.ParentChild && EventFeedUtil.FindAliveById(f.OtherId) == null)
|
|
{
|
|
AddObserved(f.Other, SagaProse.CastLabelLive("child"), "observed");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Living Others from unresolved war/plot joins.
|
|
if (memory != null && model.Cast.Count < LifeSagaPanel.MaxCastCandidates)
|
|
{
|
|
for (int i = 0; i < memory.Facts.Count && model.Cast.Count < LifeSagaPanel.MaxCastCandidates; i++)
|
|
{
|
|
LifeSagaFact f = memory.Facts[i];
|
|
if (f == null || f.Resolved || f.OtherId == 0 || seen.Contains(f.OtherId))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Actor liveOther = EventFeedUtil.FindAliveById(f.OtherId);
|
|
if (liveOther == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (f.Kind == LifeSagaFactKind.WarJoin)
|
|
{
|
|
AddLive(liveOther, SagaProse.TitleLabel("War Enemy"), "observed");
|
|
}
|
|
else if (f.Kind == LifeSagaFactKind.PlotJoin)
|
|
{
|
|
AddLive(liveOther, SagaProse.TitleLabel("Plot Partner"), "observed");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Family peers fill remaining slots for crown/combat/plot/pack/bond lenses.
|
|
if (actor != null
|
|
&& model.Cast.Count < LifeSagaPanel.MaxCastCandidates
|
|
&& (primary == LifeSagaLens.PackAlpha
|
|
|| primary == LifeSagaLens.LoveGrief
|
|
|| primary == LifeSagaLens.CrownClan
|
|
|| primary == LifeSagaLens.WarriorKiller
|
|
|| primary == LifeSagaLens.Plotter))
|
|
{
|
|
string peerLabel = primary == LifeSagaLens.PackAlpha
|
|
&& LifeSagaRoster.UsesPackLanguage(actor)
|
|
? SagaProse.CastLabelLive("packmate")
|
|
: SagaProse.CastLabelLive("kin");
|
|
foreach (Actor peer in ActorRelation.EnumerateFamilyMembers(actor, max: LifeSagaPanel.MaxCastCandidates + 2))
|
|
{
|
|
if (peer == null || peer == actor)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
AddLive(peer, peerLabel, "is");
|
|
if (model.Cast.Count >= LifeSagaPanel.MaxCastCandidates)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
_ = slot;
|
|
}
|
|
|
|
private static string FormatLensEyebrow(LifeSagaLens primary, LifeSagaLens secondary)
|
|
{
|
|
string a = LensLabel(primary);
|
|
string b = LensLabel(secondary);
|
|
if (string.IsNullOrEmpty(a))
|
|
{
|
|
return b;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(b) || string.Equals(a, b, StringComparison.Ordinal))
|
|
{
|
|
return a;
|
|
}
|
|
|
|
return a + " · " + b;
|
|
}
|
|
|
|
private static string LensLabel(LifeSagaLens lens)
|
|
{
|
|
switch (lens)
|
|
{
|
|
case LifeSagaLens.CrownClan: return "Crown";
|
|
case LifeSagaLens.PackAlpha: return "Pack";
|
|
case LifeSagaLens.WarriorKiller: return "Combat";
|
|
case LifeSagaLens.Plotter: return "Intrigue";
|
|
case LifeSagaLens.FounderWanderer: return "Journey";
|
|
case LifeSagaLens.LoveGrief: return "Bond";
|
|
case LifeSagaLens.LastOfKind: return "Survival";
|
|
default: return "";
|
|
}
|
|
}
|
|
|
|
private static void BuildLegacy(
|
|
LifeSagaViewModel model,
|
|
long unitId,
|
|
LifeSagaFact stake)
|
|
{
|
|
var castIds = new HashSet<long>();
|
|
for (int i = 0; i < model.Cast.Count; i++)
|
|
{
|
|
if (model.Cast[i] != null && model.Cast[i].UnitId != 0)
|
|
{
|
|
castIds.Add(model.Cast[i].UnitId);
|
|
}
|
|
}
|
|
|
|
model.Legacy.AddRange(
|
|
LegacyChapterBuilder.Build(unitId, model.Title, stake, castIds, displayLimit: 4));
|
|
}
|
|
|
|
private static string KingdomName(Actor actor)
|
|
{
|
|
try { return actor?.kingdom?.name ?? ""; }
|
|
catch { return ""; }
|
|
}
|
|
|
|
private static string CityName(Actor actor)
|
|
{
|
|
try { return actor?.city?.name ?? ""; }
|
|
catch { return ""; }
|
|
}
|
|
|
|
private static string SafeName(Actor actor)
|
|
{
|
|
try
|
|
{
|
|
string name = actor?.getName();
|
|
if (!string.IsNullOrEmpty(name))
|
|
{
|
|
return name;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
return actor?.asset?.id ?? "";
|
|
}
|
|
|
|
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 "";
|
|
}
|
|
}
|
|
|
|
public enum LifeSagaLens
|
|
{
|
|
None,
|
|
CrownClan,
|
|
PackAlpha,
|
|
WarriorKiller,
|
|
Plotter,
|
|
FounderWanderer,
|
|
LoveGrief,
|
|
LastOfKind
|
|
}
|
|
|
|
public sealed class LifeSagaCastMember
|
|
{
|
|
public long UnitId;
|
|
public string Name = "";
|
|
public string SpeciesId = "";
|
|
public string Relation = "";
|
|
public string Truth = "is";
|
|
public bool Alive;
|
|
}
|
|
|
|
public sealed class LifeSagaLegacyBeat
|
|
{
|
|
public string ChapterId = "";
|
|
public string SourceEventId = "";
|
|
public string SourceDevelopmentId = "";
|
|
public string SourceThreadId = "";
|
|
public LifeSagaFactKind Kind;
|
|
public string Line = "";
|
|
public string Truth = "observed";
|
|
public float At;
|
|
public float Importance;
|
|
public string DateLabel = "";
|
|
}
|
|
|
|
public sealed class LifeSagaViewModel
|
|
{
|
|
public long UnitId;
|
|
public string Name = "";
|
|
public string Title = "";
|
|
public string SpeciesId = "";
|
|
public string StakeLine = "";
|
|
public string StakeProvenance = "";
|
|
public LifeSagaLens PrimaryLens;
|
|
public LifeSagaLens SecondaryLens;
|
|
public string LensEyebrow = "";
|
|
public readonly List<LifeSagaCastMember> Cast = new List<LifeSagaCastMember>(4);
|
|
public readonly List<LifeSagaLegacyBeat> Legacy = new List<LifeSagaLegacyBeat>(5);
|
|
|
|
public string ToPlainText()
|
|
{
|
|
var sb = new StringBuilder(320);
|
|
if (!string.IsNullOrEmpty(Name))
|
|
{
|
|
sb.Append(Name);
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(LensEyebrow))
|
|
{
|
|
if (sb.Length > 0) sb.Append('\n');
|
|
sb.Append(LensEyebrow);
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(Title))
|
|
{
|
|
if (sb.Length > 0) sb.Append('\n');
|
|
sb.Append(Title);
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(StakeLine))
|
|
{
|
|
if (sb.Length > 0) sb.Append('\n');
|
|
sb.Append("At stake ").Append(StakeLine);
|
|
}
|
|
|
|
if (Cast.Count > 0)
|
|
{
|
|
if (sb.Length > 0) sb.Append('\n');
|
|
sb.Append("Cast ");
|
|
for (int i = 0; i < Cast.Count; i++)
|
|
{
|
|
if (i > 0) sb.Append(" · ");
|
|
sb.Append(Cast[i].Relation).Append(' ').Append(Cast[i].Name);
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < Legacy.Count; i++)
|
|
{
|
|
if (sb.Length > 0) sb.Append('\n');
|
|
sb.Append("Legacy ").Append(Legacy[i].Line);
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
}
|