worldbox-observer-mod/IdleSpectator/Events/LiveEnsemble.cs
2026-07-16 23:10:54 -05:00

1068 lines
29 KiB
C#

using System;
using System.Collections.Generic;
using UnityEngine;
namespace IdleSpectator;
/// <summary>Families that can escalate into multi-actor live scenes.</summary>
public enum EnsembleKind
{
Combat = 0,
StatusOutbreak = 1,
FamilyPack = 2,
WarFront = 3,
PlotCell = 4,
DiscoveryCluster = 5
}
/// <summary>Size band for reason + scoring alignment.</summary>
public enum EnsembleScale
{
Pair = 0,
Skirmish = 1,
Battle = 2,
Mass = 3
}
/// <summary>How opposing camps are named in the orange reason.</summary>
public enum EnsembleFrame
{
NamedPair = 0,
KingdomVsKingdom = 1,
SpeciesVsSpecies = 2,
NamedLeaders = 3,
CountOnly = 4
}
/// <summary>One camp inside a <see cref="LiveEnsemble"/>.</summary>
public sealed class EnsembleSide
{
public string Key = "";
public string Display = "";
/// <summary>Optional kingdom pretty-name when the side is species-framed.</summary>
public string KingdomDisplay = "";
public int Count;
public int NotableCount;
public Actor Best;
}
/// <summary>
/// Snapshot of a live multi-actor scene. Combat is the first consumer;
/// wars / status / family / plots can reuse the same shape later.
/// </summary>
public sealed class LiveEnsemble
{
public EnsembleKind Kind = EnsembleKind.Combat;
public EnsembleScale Scale = EnsembleScale.Pair;
public EnsembleFrame Frame = EnsembleFrame.CountOnly;
public int ParticipantCount;
public int NotableParticipantCount;
public Actor Focus;
public Actor Related;
public EnsembleSide SideA;
public EnsembleSide SideB;
public readonly List<long> ParticipantIds = new List<long>(8);
public Vector3 Anchor;
public bool HasFocus => Focus != null && Focus.isAlive();
public static EnsembleScale ScaleForCount(int count)
{
ScoringWeights w = InterestScoringConfig.W;
int n = Math.Max(0, count);
if (n <= Math.Max(1, w.duelMaxFighters))
{
return EnsembleScale.Pair;
}
int crowd = Math.Max(w.massFighterThreshold + 1, w.massCrowdThreshold);
if (n >= crowd)
{
return EnsembleScale.Mass;
}
if (n >= Math.Max(3, w.massFighterThreshold))
{
return EnsembleScale.Battle;
}
return EnsembleScale.Skirmish;
}
/// <summary>
/// Collect living fighters near <paramref name="pos"/> and partition into sides.
/// </summary>
public static bool TryBuildCombat(
Vector2 pos,
float radius,
out LiveEnsemble ensemble,
ICollection<long> priorParticipantIds = null,
float newcomerBonus = 8f)
{
ensemble = null;
var fighters = new List<Actor>(16);
CollectCombatFighters(pos, radius, fighters);
if (fighters.Count == 0)
{
return false;
}
ensemble = FromCombatFighters(fighters, pos, priorParticipantIds, newcomerBonus);
return ensemble != null && ensemble.HasFocus;
}
public static LiveEnsemble FromCombatFighters(
List<Actor> fighters,
Vector2 anchor,
ICollection<long> priorParticipantIds = null,
float newcomerBonus = 8f)
{
if (fighters == null || fighters.Count == 0)
{
return null;
}
var ensemble = new LiveEnsemble
{
Kind = EnsembleKind.Combat,
Anchor = new Vector3(anchor.x, anchor.y, 0f)
};
int notables = 0;
float bestWeight = -1f;
Actor best = null;
for (int i = 0; i < fighters.Count; i++)
{
Actor actor = fighters[i];
if (actor == null || !actor.isAlive())
{
continue;
}
long id = EventFeedUtil.SafeId(actor);
if (id != 0)
{
ensemble.ParticipantIds.Add(id);
}
if (InterestScoring.IsNotable(actor))
{
notables++;
}
float weight = FocusWeight(actor, priorParticipantIds, newcomerBonus);
if (weight > bestWeight)
{
bestWeight = weight;
best = actor;
}
}
ensemble.ParticipantCount = ensemble.ParticipantIds.Count > 0
? ensemble.ParticipantIds.Count
: fighters.Count;
ensemble.NotableParticipantCount = notables;
ensemble.Focus = best;
ensemble.Scale = ScaleForCount(ensemble.ParticipantCount);
if (ensemble.Focus == null)
{
return null;
}
try
{
ensemble.Anchor = ensemble.Focus.current_position;
}
catch
{
// keep anchor
}
if (ensemble.ParticipantCount <= 2)
{
ApplyNamedPair(ensemble, fighters);
return ensemble;
}
if (TryPartitionByKingdom(fighters, ensemble, priorParticipantIds, newcomerBonus))
{
return ensemble;
}
if (TryPartitionBySpecies(fighters, ensemble, priorParticipantIds, newcomerBonus))
{
return ensemble;
}
if (TryPartitionByAttackCamps(fighters, ensemble, priorParticipantIds, newcomerBonus))
{
return ensemble;
}
ApplyCountOnly(ensemble, fighters, priorParticipantIds, newcomerBonus);
return ensemble;
}
public static float FocusWeight(
Actor actor,
ICollection<long> priorParticipantIds,
float newcomerBonus)
{
float weight = WorldActivityScanner.CombatFocusWeight(actor);
if (weight < 0f)
{
return weight;
}
if (priorParticipantIds != null
&& priorParticipantIds.Count > 0
&& newcomerBonus > 0f)
{
long id = EventFeedUtil.SafeId(actor);
if (id != 0 && !priorParticipantIds.Contains(id))
{
weight += newcomerBonus;
}
}
return weight;
}
public static string KingdomKeyOf(Actor actor)
{
if (actor?.kingdom == null)
{
return "";
}
string name = actor.kingdom.name ?? "";
return string.IsNullOrEmpty(name) ? "" : name.Trim();
}
/// <summary>Human-readable kingdom / civ label (never raw snake_case ids).</summary>
public static string KingdomDisplay(string kingdomKey)
{
if (string.IsNullOrEmpty(kingdomKey))
{
return "";
}
string key = kingdomKey.Trim();
// Some worlds use a bare species asset id as the kingdom name.
if (key.IndexOf(' ') < 0
&& key.IndexOf('_') < 0
&& ActivityAssetCatalog.IsLiveActorAssetId(key))
{
return SpeciesDisplay(key);
}
return HumanizeWorldLabel(key);
}
public static string SpeciesKeyOf(Actor actor)
{
return actor?.asset != null ? (actor.asset.id ?? "") : "";
}
public static string SpeciesDisplay(string speciesId)
{
if (string.IsNullOrEmpty(speciesId))
{
return "creatures";
}
string fromAsset = ActivityAssetCatalog.SpeciesDisplayLabel(speciesId);
string s = !string.IsNullOrEmpty(fromAsset) ? fromAsset : speciesId.Replace('_', ' ').Trim();
if (string.IsNullOrEmpty(s))
{
return "creatures";
}
string lower = s.ToLowerInvariant();
if (lower.StartsWith("nomads ", StringComparison.Ordinal))
{
s = s.Substring(7).Trim();
lower = s.ToLowerInvariant();
}
switch (lower)
{
case "wolf":
return "wolves";
case "human":
case "humans":
return "humans";
case "sheep":
return "sheep";
case "fish":
return "fish";
case "goose":
return "geese";
case "mouse":
return "mice";
case "ox":
return "oxen";
case "dwarf":
return "dwarves";
case "elf":
return "elves";
}
if (lower.EndsWith("s", StringComparison.Ordinal)
|| lower.EndsWith("x", StringComparison.Ordinal)
|| lower.EndsWith("ch", StringComparison.Ordinal)
|| lower.EndsWith("sh", StringComparison.Ordinal))
{
return lower;
}
if (lower.EndsWith("y", StringComparison.Ordinal) && lower.Length > 1)
{
char prev = lower[lower.Length - 2];
if (prev != 'a' && prev != 'e' && prev != 'i' && prev != 'o' && prev != 'u')
{
return lower.Substring(0, lower.Length - 1) + "ies";
}
}
return lower + "s";
}
/// <summary>Title-case / underscore cleanup for kingdom and civ-like ids.</summary>
public static string HumanizeWorldLabel(string raw)
{
if (string.IsNullOrEmpty(raw))
{
return "";
}
string s = raw.Trim().Replace('_', ' ');
while (s.IndexOf(" ", StringComparison.Ordinal) >= 0)
{
s = s.Replace(" ", " ");
}
if (s.Length == 0)
{
return "";
}
// Strip civ prefixes that leak into labels ("nomads human" → species when match).
string lower = s.ToLowerInvariant();
if (lower.StartsWith("nomads ", StringComparison.Ordinal))
{
string rest = s.Substring(7).Trim();
string restId = rest.Replace(' ', '_');
if (ActivityAssetCatalog.IsLiveActorAssetId(restId)
|| ActivityAssetCatalog.IsLiveActorAssetId(rest))
{
return SpeciesDisplay(
ActivityAssetCatalog.IsLiveActorAssetId(restId) ? restId : rest);
}
}
var sb = new System.Text.StringBuilder(s.Length);
bool cap = true;
for (int i = 0; i < s.Length; i++)
{
char c = s[i];
if (char.IsWhiteSpace(c))
{
sb.Append(' ');
cap = true;
continue;
}
if (cap)
{
sb.Append(char.ToUpperInvariant(c));
cap = false;
}
else
{
sb.Append(char.ToLowerInvariant(c));
}
}
return sb.ToString();
}
private static void CollectCombatFighters(Vector2 pos, float radius, List<Actor> into)
{
float r2 = radius * radius;
foreach (Actor actor in WorldActivityScanner.EnumerateAliveUnitsPublic())
{
if (actor == null || !actor.isAlive())
{
continue;
}
float dx = actor.current_position.x - pos.x;
float dy = actor.current_position.y - pos.y;
if (dx * dx + dy * dy > r2)
{
continue;
}
if (!IsCombatParticipant(actor))
{
continue;
}
into.Add(actor);
}
}
public static bool IsCombatParticipant(Actor actor)
{
if (actor == null || !actor.isAlive())
{
return false;
}
try
{
if (actor.has_attack_target)
{
return true;
}
return actor.hasTask()
&& actor.ai?.task != null
&& actor.ai.task.in_combat;
}
catch
{
return false;
}
}
/// <summary>
/// Sticky scoreboard roster: add units currently fighting for each camp, then count every
/// tracked member that is still alive (combat or not) until the combat scene grace ends.
/// Dead ids are pruned. New combatants matching a sticky key are enrolled.
/// </summary>
public static void RefreshStickyMemberRoster(
Vector2 pos,
float radius,
EnsembleFrame frame,
string keyA,
string keyB,
List<long> sideAIds,
List<long> sideBIds,
out int countA,
out int countB,
out Actor bestA,
out Actor bestB)
{
countA = 0;
countB = 0;
bestA = null;
bestB = null;
if (sideAIds == null || sideBIds == null
|| string.IsNullOrEmpty(keyA) || string.IsNullOrEmpty(keyB))
{
return;
}
float r2 = radius * radius;
// Enroll anyone currently fighting for a sticky camp.
foreach (Actor actor in WorldActivityScanner.EnumerateAliveUnitsPublic())
{
if (actor == null || !actor.isAlive() || !IsCombatParticipant(actor))
{
continue;
}
float dx = actor.current_position.x - pos.x;
float dy = actor.current_position.y - pos.y;
if (dx * dx + dy * dy > r2)
{
continue;
}
string key = frame == EnsembleFrame.KingdomVsKingdom
? KingdomKeyOf(actor)
: SpeciesKeyOf(actor);
if (string.IsNullOrEmpty(key))
{
continue;
}
long id = EventFeedUtil.SafeId(actor);
if (id == 0)
{
continue;
}
if (key.Equals(keyA, StringComparison.OrdinalIgnoreCase))
{
AddUniqueId(sideAIds, id);
}
else if (key.Equals(keyB, StringComparison.OrdinalIgnoreCase))
{
AddUniqueId(sideBIds, id);
}
}
countA = CountAliveTracked(sideAIds, out bestA, preferCombat: true);
countB = CountAliveTracked(sideBIds, out bestB, preferCombat: true);
}
public static Actor FindBestAliveTracked(List<long> ids, bool preferCombat = true)
{
CountAliveTracked(ids, out Actor best, preferCombat);
return best;
}
private static void AddUniqueId(List<long> ids, long id)
{
if (id == 0 || ids == null)
{
return;
}
for (int i = 0; i < ids.Count; i++)
{
if (ids[i] == id)
{
return;
}
}
ids.Add(id);
}
private static int CountAliveTracked(List<long> ids, out Actor best, bool preferCombat)
{
best = null;
if (ids == null || ids.Count == 0)
{
return 0;
}
float bestW = -1f;
int alive = 0;
for (int i = ids.Count - 1; i >= 0; i--)
{
long id = ids[i];
Actor actor = FindAliveById(id);
if (actor == null)
{
ids.RemoveAt(i);
continue;
}
alive++;
float w = FocusWeight(actor, priorParticipantIds: null, newcomerBonus: 0f);
if (preferCombat && IsCombatParticipant(actor))
{
w += 40f;
}
if (w > bestW)
{
bestW = w;
best = actor;
}
}
return alive;
}
private static Actor FindAliveById(long id)
{
if (id == 0)
{
return null;
}
foreach (Actor actor in WorldActivityScanner.EnumerateAliveUnitsPublic())
{
if (actor == null || !actor.isAlive())
{
continue;
}
if (EventFeedUtil.SafeId(actor) == id)
{
return actor;
}
}
return null;
}
public static bool HasOpposingCollectiveSides(LiveEnsemble ensemble)
{
if (ensemble?.SideA == null || ensemble.SideB == null)
{
return false;
}
if (ensemble.Frame != EnsembleFrame.SpeciesVsSpecies
&& ensemble.Frame != EnsembleFrame.KingdomVsKingdom)
{
return false;
}
if (string.IsNullOrEmpty(ensemble.SideA.Key) || string.IsNullOrEmpty(ensemble.SideB.Key))
{
return false;
}
return !ensemble.SideA.Key.Equals(ensemble.SideB.Key, StringComparison.OrdinalIgnoreCase);
}
private static void ApplyNamedPair(LiveEnsemble ensemble, List<Actor> fighters)
{
ensemble.Frame = EnsembleFrame.NamedPair;
Actor other = null;
for (int i = 0; i < fighters.Count; i++)
{
Actor a = fighters[i];
if (a != null && a.isAlive() && a != ensemble.Focus)
{
other = a;
break;
}
}
if (other == null)
{
other = AttackTargetOf(ensemble.Focus);
}
ensemble.Related = other;
ensemble.SideA = SideFromActor(ensemble.Focus, "focus");
if (other != null)
{
ensemble.SideB = SideFromActor(other, "foe");
}
}
private static void ApplyCountOnly(
LiveEnsemble ensemble,
List<Actor> fighters,
ICollection<long> prior,
float newcomerBonus)
{
ensemble.Frame = EnsembleFrame.CountOnly;
ensemble.SideA = SideFromActor(ensemble.Focus, "focus");
ensemble.SideA.Count = ensemble.ParticipantCount;
ensemble.SideA.NotableCount = ensemble.NotableParticipantCount;
Actor foe = AttackTargetOf(ensemble.Focus);
if (foe == null || !foe.isAlive())
{
// Second-best by weight as a soft related.
float bestW = -1f;
for (int i = 0; i < fighters.Count; i++)
{
Actor a = fighters[i];
if (a == null || a == ensemble.Focus || !a.isAlive())
{
continue;
}
float w = FocusWeight(a, prior, newcomerBonus);
if (w > bestW)
{
bestW = w;
foe = a;
}
}
}
ensemble.Related = foe;
if (foe != null)
{
ensemble.SideB = SideFromActor(foe, "foe");
// Multi-fighter: never NamedLeaders (unit-name sides). Species promotion happens in EventReason.
}
}
private static bool TryPartitionByKingdom(
List<Actor> fighters,
LiveEnsemble ensemble,
ICollection<long> prior,
float newcomerBonus)
{
var groups = new Dictionary<string, List<Actor>>(StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < fighters.Count; i++)
{
Actor a = fighters[i];
string key = KingdomKeyOf(a);
if (string.IsNullOrEmpty(key))
{
continue;
}
if (!groups.TryGetValue(key, out List<Actor> list))
{
list = new List<Actor>(4);
groups[key] = list;
}
list.Add(a);
}
if (groups.Count < 2)
{
return false;
}
return FinishTwoSides(
ensemble,
groups,
EnsembleFrame.KingdomVsKingdom,
prior,
newcomerBonus,
displayFromKey: KingdomDisplay);
}
private static bool TryPartitionBySpecies(
List<Actor> fighters,
LiveEnsemble ensemble,
ICollection<long> prior,
float newcomerBonus)
{
var groups = new Dictionary<string, List<Actor>>(StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < fighters.Count; i++)
{
Actor a = fighters[i];
string key = SpeciesKeyOf(a);
if (string.IsNullOrEmpty(key))
{
continue;
}
if (!groups.TryGetValue(key, out List<Actor> list))
{
list = new List<Actor>(4);
groups[key] = list;
}
list.Add(a);
}
if (groups.Count < 2)
{
return false;
}
return FinishTwoSides(
ensemble,
groups,
EnsembleFrame.SpeciesVsSpecies,
prior,
newcomerBonus,
displayFromKey: SpeciesDisplay);
}
private static bool TryPartitionByAttackCamps(
List<Actor> fighters,
LiveEnsemble ensemble,
ICollection<long> prior,
float newcomerBonus)
{
Actor focus = ensemble.Focus;
Actor foe = AttackTargetOf(focus);
if (foe == null || !foe.isAlive())
{
return false;
}
var sideA = new List<Actor> { focus };
var sideB = new List<Actor> { foe };
for (int i = 0; i < fighters.Count; i++)
{
Actor a = fighters[i];
if (a == null || a == focus || a == foe || !a.isAlive())
{
continue;
}
Actor target = AttackTargetOf(a);
if (target == foe || target == null && SameCampHint(a, focus))
{
sideA.Add(a);
}
else if (target == focus || SameCampHint(a, foe))
{
sideB.Add(a);
}
else if (AttackTargetOf(foe) == a)
{
sideB.Add(a);
}
else
{
// Default: share focus's kingdom/species if any, else side A.
if (!string.IsNullOrEmpty(KingdomKeyOf(focus))
&& string.Equals(KingdomKeyOf(a), KingdomKeyOf(focus), StringComparison.OrdinalIgnoreCase))
{
sideA.Add(a);
}
else if (!string.IsNullOrEmpty(SpeciesKeyOf(focus))
&& string.Equals(SpeciesKeyOf(a), SpeciesKeyOf(focus), StringComparison.OrdinalIgnoreCase))
{
sideA.Add(a);
}
else
{
sideB.Add(a);
}
}
}
if (sideA.Count == 0 || sideB.Count == 0)
{
return false;
}
// Prefer species framing over unit-named camps whenever species differ.
string spA = SpeciesKeyOf(focus);
string spB = SpeciesKeyOf(foe);
if (!string.IsNullOrEmpty(spA)
&& !string.IsNullOrEmpty(spB)
&& !spA.Equals(spB, StringComparison.OrdinalIgnoreCase))
{
ensemble.Frame = EnsembleFrame.SpeciesVsSpecies;
ensemble.SideA = BuildSide(spA, SpeciesDisplay(spA), sideA, prior, newcomerBonus, EnsembleFrame.SpeciesVsSpecies);
ensemble.SideB = BuildSide(spB, SpeciesDisplay(spB), sideB, prior, newcomerBonus, EnsembleFrame.SpeciesVsSpecies);
}
else
{
// Same species camps → count-only melee (EventReason prints "melee (n)").
ensemble.Frame = EnsembleFrame.CountOnly;
ensemble.SideA = null;
ensemble.SideB = null;
PreferFocusSide(ensemble);
ensemble.Related = foe;
return true;
}
PreferFocusSide(ensemble);
ensemble.Related = ensemble.SideB?.Best ?? foe;
return true;
}
private static bool SameCampHint(Actor a, Actor camp)
{
if (a == null || camp == null)
{
return false;
}
string ka = KingdomKeyOf(a);
string kc = KingdomKeyOf(camp);
if (!string.IsNullOrEmpty(ka) && string.Equals(ka, kc, StringComparison.OrdinalIgnoreCase))
{
return true;
}
string sa = SpeciesKeyOf(a);
string sc = SpeciesKeyOf(camp);
return !string.IsNullOrEmpty(sa) && string.Equals(sa, sc, StringComparison.OrdinalIgnoreCase);
}
private static bool FinishTwoSides(
LiveEnsemble ensemble,
Dictionary<string, List<Actor>> groups,
EnsembleFrame frame,
ICollection<long> prior,
float newcomerBonus,
Func<string, string> displayFromKey)
{
string keyA = null;
string keyB = null;
int countA = -1;
int countB = -1;
foreach (KeyValuePair<string, List<Actor>> kv in groups)
{
int n = kv.Value?.Count ?? 0;
if (n > countA)
{
keyB = keyA;
countB = countA;
keyA = kv.Key;
countA = n;
}
else if (n > countB)
{
keyB = kv.Key;
countB = n;
}
}
if (string.IsNullOrEmpty(keyA) || string.IsNullOrEmpty(keyB))
{
return false;
}
ensemble.Frame = frame;
ensemble.SideA = BuildSide(keyA, displayFromKey(keyA), groups[keyA], prior, newcomerBonus, frame);
ensemble.SideB = BuildSide(keyB, displayFromKey(keyB), groups[keyB], prior, newcomerBonus, frame);
PreferFocusSide(ensemble);
ensemble.Related = ensemble.SideB?.Best;
if (ensemble.Related == null)
{
ensemble.Related = AttackTargetOf(ensemble.Focus);
}
return ensemble.SideA != null && ensemble.SideB != null;
}
private static void PreferFocusSide(LiveEnsemble ensemble)
{
if (ensemble?.Focus == null || ensemble.SideA == null || ensemble.SideB == null)
{
return;
}
bool onA = ActorMatchesSide(ensemble.Focus, ensemble.SideA);
bool onB = ActorMatchesSide(ensemble.Focus, ensemble.SideB);
if (onB && !onA)
{
EnsembleSide tmp = ensemble.SideA;
ensemble.SideA = ensemble.SideB;
ensemble.SideB = tmp;
}
}
private static bool ActorMatchesSide(Actor actor, EnsembleSide side)
{
if (actor == null || side == null || string.IsNullOrEmpty(side.Key))
{
return false;
}
if (side.Key.StartsWith("camp:", StringComparison.OrdinalIgnoreCase))
{
return side.Best == actor;
}
string kingdom = KingdomKeyOf(actor);
if (!string.IsNullOrEmpty(kingdom)
&& kingdom.Equals(side.Key, StringComparison.OrdinalIgnoreCase))
{
return true;
}
string species = SpeciesKeyOf(actor);
return !string.IsNullOrEmpty(species)
&& species.Equals(side.Key, StringComparison.OrdinalIgnoreCase);
}
private static EnsembleSide BuildSide(
string key,
string display,
List<Actor> members,
ICollection<long> prior,
float newcomerBonus,
EnsembleFrame frame)
{
string pretty = string.IsNullOrEmpty(display) ? (key ?? "") : display;
if (frame == EnsembleFrame.KingdomVsKingdom)
{
pretty = KingdomDisplay(pretty);
}
else if (frame == EnsembleFrame.SpeciesVsSpecies)
{
pretty = SpeciesDisplay(key);
}
var side = new EnsembleSide
{
Key = key ?? "",
Display = pretty,
Count = members?.Count ?? 0
};
if (members == null)
{
return side;
}
float bestW = -1f;
for (int i = 0; i < members.Count; i++)
{
Actor a = members[i];
if (a == null || !a.isAlive())
{
continue;
}
if (InterestScoring.IsNotable(a))
{
side.NotableCount++;
}
float w = FocusWeight(a, prior, newcomerBonus);
if (w > bestW)
{
bestW = w;
side.Best = a;
}
}
if (frame == EnsembleFrame.SpeciesVsSpecies && side.Best != null)
{
string kingdom = KingdomKeyOf(side.Best);
if (!string.IsNullOrEmpty(kingdom))
{
side.KingdomDisplay = KingdomDisplay(kingdom);
}
}
return side;
}
private static EnsembleSide SideFromActor(Actor actor, string fallbackKey)
{
if (actor == null)
{
return null;
}
string kingdom = KingdomKeyOf(actor);
string species = SpeciesKeyOf(actor);
return new EnsembleSide
{
Key = !string.IsNullOrEmpty(kingdom) ? kingdom
: (!string.IsNullOrEmpty(species) ? species : fallbackKey),
Display = !string.IsNullOrEmpty(kingdom)
? KingdomDisplay(kingdom)
: (!string.IsNullOrEmpty(species) ? SpeciesDisplay(species) : EventFeedUtil.SafeName(actor)),
KingdomDisplay = !string.IsNullOrEmpty(kingdom) ? KingdomDisplay(kingdom) : "",
Count = 1,
NotableCount = InterestScoring.IsNotable(actor) ? 1 : 0,
Best = actor
};
}
private static Actor AttackTargetOf(Actor actor)
{
if (actor == null)
{
return null;
}
try
{
if (actor.has_attack_target
&& actor.attack_target != null
&& actor.attack_target.isAlive()
&& actor.attack_target.isActor())
{
Actor foe = actor.attack_target.a;
return foe != null && foe.isAlive() ? foe : null;
}
}
catch
{
// ignore
}
return null;
}
}