worldbox-observer-mod/IdleSpectator/Events/StickyScoreboard.cs
DazedAnon c23b2b9c1e feat(spectator): add crisis chapters and harden combat theater truth
- Open war/disaster/outbreak crisis overlays with epilogue_crisis closers
- Keep Duel pairs hot only on owned cast; park sleep/freeze; drop ambient scrap pins
- Require a fallen theater partner for survivor aftermath; let king_killed cut mid-fight
- Gate crisis, combat cold, king cut-in, and living-partner aftermath in harness
2026-07-21 22:05:38 -05:00

945 lines
31 KiB
C#

using System;
using UnityEngine;
namespace IdleSpectator;
/// <summary>
/// Kind-agnostic sticky scoreboard: lock sides, enroll members, refresh alive counts,
/// hold elevated scale, promote follow on subject death.
/// Combat, WarFront, PlotCell, FamilyPack, and StatusOutbreak are consumers.
/// </summary>
public static class StickyScoreboard
{
public const float ScaleHoldSeconds = 2.5f;
/// <summary>Thin camp must reach this alive count to leave mop-up pack framing.</summary>
public const int WipeRecoverMin = 2;
public const float WarTheaterRadius = 48f;
public const float PlotTheaterRadius = 36f;
public const float FamilyTheaterRadius = 28f;
public const float StatusOutbreakRadius = 32f;
public static bool HasOpposingSides(InterestCandidate scene) =>
scene?.Sticky != null && scene.Sticky.HasOpposingSides;
public static bool IsOnRoster(InterestCandidate scene, Actor actor)
{
if (scene?.Sticky == null || actor == null || !actor.isAlive())
{
return false;
}
long id = EventFeedUtil.SafeId(actor);
if (id == 0)
{
return false;
}
LiveSceneStickyState sticky = scene.Sticky;
for (int i = 0; i < sticky.SideAIds.Count; i++)
{
if (sticky.SideAIds[i] == id)
{
return true;
}
}
for (int i = 0; i < sticky.SideBIds.Count; i++)
{
if (sticky.SideBIds[i] == id)
{
return true;
}
}
return false;
}
/// <summary>
/// Promote another living roster member onto the candidate when follow is lost.
/// </summary>
public static bool TryPromoteFollow(InterestCandidate scene)
{
if (scene?.Sticky == null || !scene.Sticky.HasOpposingSides)
{
return false;
}
LiveSceneStickyState sticky = scene.Sticky;
// Combat + war fronts: prefer units still in the scrap over chore bystanders.
bool preferCombat = sticky.Kind == EnsembleKind.Combat
|| sticky.Kind == EnsembleKind.WarFront;
bool plotLike = sticky.Kind == EnsembleKind.PlotCell;
bool familyLike = sticky.Kind == EnsembleKind.FamilyPack;
bool outbreakLike = sticky.Kind == EnsembleKind.StatusOutbreak;
bool singleSide = familyLike || outbreakLike;
Actor pickA = LiveEnsemble.FindBestAliveTracked(sticky.SideAIds, preferCombat: preferCombat);
Actor pickB = singleSide
? null
: LiveEnsemble.FindBestAliveTracked(sticky.SideBIds, preferCombat: preferCombat);
if (pickA == null
&& sticky.Frame == EnsembleFrame.KingdomVsKingdom
&& !LiveEnsemble.IsPlotterSideKey(sticky.SideAKey)
&& !LiveEnsemble.IsFamilyPackSideKey(sticky.SideAKey)
&& !LiveEnsemble.IsStatusOutbreakSideKey(sticky.SideAKey))
{
pickA = LiveEnsemble.PreferKingdomLeader(LiveEnsemble.FindKingdomByKey(sticky.SideAKey));
}
if (pickB == null
&& !singleSide
&& sticky.Frame == EnsembleFrame.KingdomVsKingdom)
{
pickB = LiveEnsemble.PreferKingdomLeader(LiveEnsemble.FindKingdomByKey(sticky.SideBKey));
}
// Family / plot / outbreak prefer Side A.
Actor pick = (plotLike || singleSide) ? (pickA ?? pickB) : (pickA ?? pickB);
if (pick == null || !pick.isAlive())
{
return false;
}
if (!plotLike && !singleSide && pickA != null && pickB != null)
{
if (sticky.SideBCount > sticky.SideACount)
{
pick = pickB;
}
else if (sticky.SideACount > sticky.SideBCount)
{
pick = pickA;
}
}
Actor other = singleSide
? null
: (pick == pickA ? pickB : pickA);
if (singleSide)
{
for (int i = 0; i < sticky.SideAIds.Count; i++)
{
Actor peer = LiveEnsemble.FindTrackedActor(sticky.SideAIds[i]);
if (peer != null && peer.isAlive() && peer != pick)
{
other = peer;
break;
}
}
}
scene.FollowUnit = pick;
scene.SubjectId = EventFeedUtil.SafeId(pick);
scene.RelatedUnit = other;
scene.RelatedId = other != null ? EventFeedUtil.SafeId(other) : 0;
try
{
scene.Position = pick.current_position;
}
catch
{
// keep
}
return true;
}
/// <summary>
/// Hold presented tip tier across brief Skirmish↔Battle↔Mass flaps (up and down).
/// Peak still tracks max fighters for headcount / Mass band recovery.
/// </summary>
public static void StabilizeScale(LiveSceneStickyState sticky, LiveEnsemble ensemble, float now)
{
if (sticky == null || ensemble == null)
{
return;
}
int n = Mathf.Max(1, ensemble.ParticipantCount);
if (n > sticky.PeakParticipants)
{
sticky.PeakParticipants = n;
}
UpdateMopUp(sticky, now);
EnsembleScale live = LiveEnsemble.ScaleForCount(n);
// Opposing camp wiped / mop-up: drop Peak Mass hold so tip tier matches living pack.
bool wipedOpposing = sticky.HasOpposingSides
&& sticky.TotalCount > 0
&& (sticky.MopUpActive
|| sticky.SideACount <= 0
|| sticky.SideBCount <= 0);
if (wipedOpposing)
{
sticky.HasPresentedScale = true;
sticky.PresentedScale = live;
sticky.ScaleChangeSince = -999f;
sticky.ScaleDropSince = -999f;
ensemble.Scale = live;
if (ensemble.Frame == EnsembleFrame.NamedPair && live > EnsembleScale.Pair)
{
ensemble.Frame = EnsembleFrame.CountOnly;
}
return;
}
if (!sticky.HasPresentedScale)
{
sticky.HasPresentedScale = true;
sticky.PresentedScale = live;
sticky.ScaleChangeSince = -999f;
sticky.ScaleDropSince = -999f;
ensemble.Scale = live;
if (ensemble.Frame == EnsembleFrame.NamedPair && live > EnsembleScale.Pair)
{
ensemble.Frame = EnsembleFrame.CountOnly;
}
return;
}
if (live == sticky.PresentedScale)
{
sticky.ScaleChangeSince = -999f;
sticky.ScaleDropSince = -999f;
ensemble.Scale = sticky.PresentedScale;
return;
}
if (sticky.ScaleChangeSince < 0f)
{
sticky.ScaleChangeSince = now;
}
if (now - sticky.ScaleChangeSince < ScaleHoldSeconds)
{
ensemble.Scale = sticky.PresentedScale;
if (ensemble.Frame == EnsembleFrame.NamedPair && sticky.PresentedScale > EnsembleScale.Pair)
{
ensemble.Frame = EnsembleFrame.CountOnly;
}
return;
}
sticky.PresentedScale = live;
sticky.ScaleChangeSince = -999f;
sticky.ScaleDropSince = -999f;
ensemble.Scale = live;
if (ensemble.Frame == EnsembleFrame.NamedPair && live > EnsembleScale.Pair)
{
ensemble.Frame = EnsembleFrame.CountOnly;
}
}
/// <summary>
/// Lock opposing camps once (when multi), enroll members, rewrite ensemble sides/counts.
/// </summary>
public static void Refresh(
InterestCandidate scene,
LiveEnsemble ensemble,
Vector3 pos,
float radius)
{
if (scene?.Sticky == null || ensemble == null)
{
return;
}
LiveSceneStickyState sticky = scene.Sticky;
TryLockFromEnsemble(sticky, ensemble);
if (!sticky.HasOpposingSides)
{
return;
}
SeedFromEnsemble(sticky, ensemble);
bool warLike = sticky.Kind == EnsembleKind.WarFront || ensemble.Kind == EnsembleKind.WarFront;
bool plotLike = sticky.Kind == EnsembleKind.PlotCell || ensemble.Kind == EnsembleKind.PlotCell;
bool familyLike = sticky.Kind == EnsembleKind.FamilyPack || ensemble.Kind == EnsembleKind.FamilyPack;
bool outbreakLike = sticky.Kind == EnsembleKind.StatusOutbreak
|| ensemble.Kind == EnsembleKind.StatusOutbreak;
Actor bestA = null;
Actor bestB = null;
if (outbreakLike)
{
SeedOutbreakRoster(sticky, ensemble);
string statusId = scene.StatusId ?? "";
if (string.IsNullOrEmpty(statusId) && LiveEnsemble.IsStatusOutbreakSideKey(sticky.SideAKey))
{
statusId = sticky.SideAKey.Substring("status:".Length);
}
// Re-scan nearby carriers into the roster, then recount.
Vector2 scanPos = new Vector2(pos.x, pos.y);
var carriers = new System.Collections.Generic.List<Actor>(8);
LiveEnsemble.CollectStatusCarriers(scanPos, Math.Max(radius, StatusOutbreakRadius), statusId, carriers);
for (int i = 0; i < carriers.Count; i++)
{
long id = EventFeedUtil.SafeId(carriers[i]);
if (id != 0)
{
AddUnique(sticky.SideAIds, id);
}
}
LiveEnsemble.TryApplyStatusOutbreakStickyCounts(sticky, statusId);
bestA = LiveEnsemble.FindBestAliveTracked(sticky.SideAIds, preferCombat: false)
?? ensemble.Focus;
bestB = null;
for (int i = 0; i < sticky.SideAIds.Count; i++)
{
Actor peer = LiveEnsemble.FindTrackedActor(sticky.SideAIds[i]);
if (peer != null && peer.isAlive() && peer != bestA)
{
bestB = peer;
break;
}
}
}
else if (familyLike)
{
SeedFamilyRoster(sticky, ensemble);
LiveEnsemble.TryApplyFamilyStickyCounts(sticky);
bestA = LiveEnsemble.FindBestAliveTracked(sticky.SideAIds, preferCombat: false)
?? ensemble.Focus;
bestB = null;
for (int i = 0; i < sticky.SideAIds.Count; i++)
{
Actor peer = LiveEnsemble.FindTrackedActor(sticky.SideAIds[i]);
if (peer != null && peer.isAlive() && peer != bestA)
{
bestB = peer;
break;
}
}
}
else if (plotLike)
{
SeedPlotRoster(sticky, ensemble);
LiveEnsemble.TryApplyPlotStickyCounts(sticky);
bestA = LiveEnsemble.FindBestAliveTracked(sticky.SideAIds, preferCombat: false)
?? ensemble.Focus;
bestB = LiveEnsemble.FindBestAliveTracked(sticky.SideBIds, preferCombat: false)
?? LiveEnsemble.PreferKingdomLeader(LiveEnsemble.FindKingdomByKey(sticky.SideBKey))
?? ensemble.Related;
}
else
{
float useRadius = warLike ? Math.Max(radius, WarTheaterRadius) : radius;
LiveEnsemble.RefreshStickyMemberRoster(
pos,
useRadius,
sticky.Frame,
sticky.SideAKey,
sticky.SideBKey,
sticky.SideAIds,
sticky.SideBIds,
out int countA,
out int countB,
out bestA,
out bestB,
requireCombat: !warLike);
sticky.SideACount = Math.Max(0, countA);
sticky.SideBCount = Math.Max(0, countB);
if (warLike)
{
// Counts stay kingdom-wide; focus picks prefer frontline fighters.
bestA = LiveEnsemble.FindBestAliveTracked(sticky.SideAIds, preferCombat: true) ?? bestA;
bestB = LiveEnsemble.FindBestAliveTracked(sticky.SideBIds, preferCombat: true) ?? bestB;
LiveEnsemble.TryApplyKingdomPopulationCounts(sticky);
}
}
int live = sticky.TotalCount;
scene.ParticipantCount = live;
if (live > sticky.PeakParticipants)
{
sticky.PeakParticipants = live;
}
ApplyToEnsemble(sticky, ensemble, bestA, bestB);
if (ensemble.Scale == EnsembleScale.Pair && sticky.Kind == EnsembleKind.Combat)
{
ensemble.Scale = LiveEnsemble.ScaleForCount(Math.Max(3, sticky.PeakParticipants));
}
if (warLike)
{
ensemble.Kind = EnsembleKind.WarFront;
ensemble.Scale = LiveEnsemble.ScaleForCount(Math.Max(3, live));
}
if (plotLike)
{
ensemble.Kind = EnsembleKind.PlotCell;
ensemble.Scale = LiveEnsemble.ScaleForCount(Math.Max(3, live));
}
if (familyLike)
{
ensemble.Kind = EnsembleKind.FamilyPack;
ensemble.Scale = LiveEnsemble.ScaleForCount(Math.Max(2, sticky.SideACount));
ensemble.SideB = null;
}
if (outbreakLike)
{
ensemble.Kind = EnsembleKind.StatusOutbreak;
ensemble.Scale = LiveEnsemble.ScaleForCount(Math.Max(2, sticky.SideACount));
ensemble.SideB = null;
}
if (bestA != null
&& (ensemble.Focus == null
|| ((sticky.Kind == EnsembleKind.Combat || sticky.Kind == EnsembleKind.WarFront)
&& !LiveEnsemble.IsCombatParticipant(ensemble.Focus))))
{
ensemble.Focus = bestA;
}
if (bestB != null)
{
ensemble.Related = bestB;
}
// War / combat: if Focus is still a chore bystander, swap to a live fighter on either side.
if ((sticky.Kind == EnsembleKind.Combat || sticky.Kind == EnsembleKind.WarFront)
&& ensemble.Focus != null
&& !LiveEnsemble.IsCombatParticipant(ensemble.Focus))
{
Actor fighterA = LiveEnsemble.IsCombatParticipant(bestA) ? bestA : null;
Actor fighterB = LiveEnsemble.IsCombatParticipant(bestB) ? bestB : null;
Actor fighter = fighterA ?? fighterB;
if (fighter != null)
{
ensemble.Related = fighter == bestA ? bestB : bestA;
ensemble.Focus = fighter;
}
}
}
/// <summary>Lock sticky keys from a live opposing collective ensemble (once).</summary>
public static bool TryLockFromEnsemble(LiveSceneStickyState sticky, LiveEnsemble ensemble)
{
if (sticky == null || ensemble == null || sticky.HasOpposingSides)
{
return false;
}
if (!LiveEnsemble.HasOpposingCollectiveSides(ensemble))
{
return false;
}
int liveN = Math.Max(
ensemble.ParticipantCount,
(ensemble.SideA?.Count ?? 0) + (ensemble.SideB?.Count ?? 0));
// War / plot / family / outbreak lock even at low local fighter counts.
if (ensemble.Kind != EnsembleKind.WarFront
&& ensemble.Kind != EnsembleKind.PlotCell
&& ensemble.Kind != EnsembleKind.FamilyPack
&& ensemble.Kind != EnsembleKind.StatusOutbreak
&& liveN <= 2
&& ensemble.Scale <= EnsembleScale.Pair)
{
return false;
}
sticky.Kind = ensemble.Kind;
sticky.Frame = ensemble.Frame;
sticky.SideAKey = ensemble.SideA.Key ?? "";
sticky.SideADisplay = ensemble.SideA.Display ?? "";
sticky.SideAKingdom = ensemble.SideA.KingdomDisplay ?? "";
sticky.SideACount = Math.Max(0, ensemble.SideA.Count);
if (ensemble.Kind == EnsembleKind.FamilyPack
|| ensemble.Kind == EnsembleKind.StatusOutbreak
|| ensemble.SideB == null)
{
sticky.SideBKey = "";
sticky.SideBDisplay = "";
sticky.SideBKingdom = "";
sticky.SideBCount = 0;
}
else
{
sticky.SideBKey = ensemble.SideB.Key ?? "";
sticky.SideBDisplay = ensemble.SideB.Display ?? "";
sticky.SideBKingdom = ensemble.SideB.KingdomDisplay ?? "";
sticky.SideBCount = Math.Max(0, ensemble.SideB.Count);
}
// Lock-time scale is a peak floor - live refresh may dip without erasing the theater.
sticky.PeakParticipants = Math.Max(sticky.PeakParticipants, liveN);
return true;
}
/// <summary>Build orange reason from sticky state; never thin Fight prose.</summary>
public static string FormatReason(InterestCandidate scene, Actor focus, Actor related)
{
if (scene?.Sticky == null || !scene.Sticky.HasOpposingSides)
{
return "";
}
LiveSceneStickyState sticky = scene.Sticky;
UpdateMopUp(sticky, Time.unscaledTime);
GetPresentationCounts(sticky, out int countA, out int countB);
bool wipedOpposing = sticky.HasOpposingSides
&& sticky.TotalCount > 0
&& (countA <= 0 || countB <= 0);
int liveN = Math.Max(0, countA) + Math.Max(0, countB);
int scaleN = wipedOpposing
? Math.Max(1, liveN)
: Math.Max(3, sticky.PeakParticipants);
var ensemble = new LiveEnsemble
{
Kind = sticky.Kind,
Scale = LiveEnsemble.ScaleForCount(scaleN),
Focus = focus,
Related = related,
Frame = sticky.Frame,
SideA = new EnsembleSide
{
Key = sticky.SideAKey,
Display = sticky.SideADisplay,
KingdomDisplay = sticky.SideAKingdom,
Count = countA,
Best = focus
},
SideB = new EnsembleSide
{
Key = sticky.SideBKey,
Display = sticky.SideBDisplay,
KingdomDisplay = sticky.SideBKingdom,
Count = countB,
Best = related
},
ParticipantCount = liveN
};
return EventReason.Ensemble(ensemble);
}
/// <summary>
/// Presentation counts for combat tips: mop-up forces a thin recovering camp to 0
/// until it reaches <see cref="WipeRecoverMin"/>.
/// </summary>
public static void GetPresentationCounts(LiveSceneStickyState sticky, out int countA, out int countB)
{
countA = 0;
countB = 0;
if (sticky == null)
{
return;
}
countA = Math.Max(0, sticky.SideACount);
countB = Math.Max(0, sticky.SideBCount);
if (!sticky.MopUpActive)
{
return;
}
if (countA > 0 && countA < WipeRecoverMin)
{
countA = 0;
}
if (countB > 0 && countB < WipeRecoverMin)
{
countB = 0;
}
}
private static void UpdateMopUp(LiveSceneStickyState sticky, float now)
{
if (sticky == null || !sticky.HasOpposingSides)
{
return;
}
int a = Math.Max(0, sticky.SideACount);
int b = Math.Max(0, sticky.SideBCount);
if (a <= 0 || b <= 0)
{
if (!sticky.MopUpActive)
{
sticky.MopUpActive = true;
sticky.MopUpSince = now;
}
return;
}
if (!sticky.MopUpActive)
{
return;
}
// Leave mop-up only when both camps are meaningfully back.
if (a >= WipeRecoverMin && b >= WipeRecoverMin)
{
sticky.MopUpActive = false;
sticky.MopUpSince = -999f;
}
}
/// <summary>
/// True when two sticky scenes share the same unordered kingdom theater
/// (WarFront ↔ local Mass on Igguorn vs The Godo).
/// Matches asset ids, display names, and KingdomDisplay aliases.
/// </summary>
public static bool SharesKingdomTheater(InterestCandidate a, InterestCandidate b)
{
if (a?.Sticky == null || b?.Sticky == null)
{
return false;
}
if (!TryKingdomAliasPair(a.Sticky, out string[] a1, out string[] a2)
|| !TryKingdomAliasPair(b.Sticky, out string[] b1, out string[] b2))
{
return false;
}
return (AliasesOverlap(a1, b1) && AliasesOverlap(a2, b2))
|| (AliasesOverlap(a1, b2) && AliasesOverlap(a2, b1));
}
/// <summary>True when sticky kingdom pair matches a stored crisis theater.</summary>
public static bool SharesKingdomTheater(InterestCandidate c, string theaterA, string theaterB)
{
if (c?.Sticky == null
|| string.IsNullOrEmpty(theaterA)
|| string.IsNullOrEmpty(theaterB)
|| !TryKingdomAliasPair(c.Sticky, out string[] a1, out string[] a2))
{
return false;
}
string[] tA = KingdomAliases(theaterA);
string[] tB = KingdomAliases(theaterB);
return (AliasesOverlap(a1, tA) && AliasesOverlap(a2, tB))
|| (AliasesOverlap(a1, tB) && AliasesOverlap(a2, tA));
}
public static bool TryKingdomPair(LiveSceneStickyState sticky, out string a, out string b)
{
a = "";
b = "";
if (!TryKingdomAliasPair(sticky, out string[] aAliases, out string[] bAliases))
{
return false;
}
a = aAliases.Length > 0 ? aAliases[0] : "";
b = bAliases.Length > 0 ? bAliases[0] : "";
return !string.IsNullOrEmpty(a) && !string.IsNullOrEmpty(b) && !KingdomEq(a, b);
}
private static bool TryKingdomAliasPair(
LiveSceneStickyState sticky,
out string[] sideA,
out string[] sideB)
{
sideA = null;
sideB = null;
if (sticky == null)
{
return false;
}
if (sticky.Frame == EnsembleFrame.KingdomVsKingdom)
{
sideA = KingdomAliases(sticky.SideAKey, sticky.SideAKingdom, sticky.SideADisplay);
sideB = KingdomAliases(sticky.SideBKey, sticky.SideBKingdom, sticky.SideBDisplay);
}
else
{
sideA = KingdomAliases(sticky.SideAKingdom, sticky.SideAKey, sticky.SideADisplay);
sideB = KingdomAliases(sticky.SideBKingdom, sticky.SideBKey, sticky.SideBDisplay);
}
return sideA.Length > 0 && sideB.Length > 0 && !AliasesOverlap(sideA, sideB);
}
private static string[] KingdomAliases(params string[] raw)
{
var list = new System.Collections.Generic.List<string>(6);
for (int i = 0; i < raw.Length; i++)
{
AddKingdomAlias(list, raw[i]);
if (!string.IsNullOrEmpty(raw[i]))
{
AddKingdomAlias(list, LiveEnsemble.KingdomDisplay(raw[i]));
}
}
return list.ToArray();
}
private static void AddKingdomAlias(System.Collections.Generic.List<string> list, string key)
{
string n = NormKingdom(key);
if (string.IsNullOrEmpty(n))
{
return;
}
for (int i = 0; i < list.Count; i++)
{
if (string.Equals(list[i], n, StringComparison.Ordinal))
{
return;
}
}
list.Add(n);
}
private static bool AliasesOverlap(string[] a, string[] b)
{
if (a == null || b == null || a.Length == 0 || b.Length == 0)
{
return false;
}
for (int i = 0; i < a.Length; i++)
{
for (int j = 0; j < b.Length; j++)
{
if (KingdomEq(a[i], b[j]))
{
return true;
}
}
}
return false;
}
private static string NormKingdom(string key)
{
return string.IsNullOrEmpty(key) ? "" : key.Trim().ToLowerInvariant();
}
private static bool KingdomEq(string a, string b)
{
return !string.IsNullOrEmpty(a)
&& !string.IsNullOrEmpty(b)
&& string.Equals(a, b, StringComparison.OrdinalIgnoreCase);
}
private static void ApplyToEnsemble(
LiveSceneStickyState sticky,
LiveEnsemble ensemble,
Actor bestA,
Actor bestB)
{
ensemble.Frame = sticky.Frame;
ensemble.SideA = new EnsembleSide
{
Key = sticky.SideAKey,
Display = string.IsNullOrEmpty(sticky.SideADisplay) ? sticky.SideAKey : sticky.SideADisplay,
KingdomDisplay = sticky.SideAKingdom,
Count = sticky.SideACount,
Best = bestA ?? ensemble.Focus
};
ensemble.SideB = new EnsembleSide
{
Key = sticky.SideBKey,
Display = string.IsNullOrEmpty(sticky.SideBDisplay) ? sticky.SideBKey : sticky.SideBDisplay,
KingdomDisplay = sticky.SideBKingdom,
Count = sticky.SideBCount,
Best = bestB ?? ensemble.Related
};
ensemble.ParticipantCount = sticky.TotalCount;
}
private static void SeedFromEnsemble(LiveSceneStickyState sticky, LiveEnsemble ensemble)
{
if (sticky == null || ensemble == null || !sticky.HasOpposingSides)
{
return;
}
bool plotLike = sticky.Kind == EnsembleKind.PlotCell || ensemble.Kind == EnsembleKind.PlotCell;
bool familyLike = sticky.Kind == EnsembleKind.FamilyPack || ensemble.Kind == EnsembleKind.FamilyPack;
bool outbreakLike = sticky.Kind == EnsembleKind.StatusOutbreak
|| ensemble.Kind == EnsembleKind.StatusOutbreak;
void Enroll(Actor actor, bool forcePlotter = false)
{
if (actor == null || !actor.isAlive())
{
return;
}
long id = EventFeedUtil.SafeId(actor);
if (id == 0)
{
return;
}
if (familyLike || outbreakLike)
{
AddUnique(sticky.SideAIds, id);
return;
}
if (plotLike || forcePlotter)
{
string kingdom = LiveEnsemble.KingdomKeyOf(actor);
if (!string.IsNullOrEmpty(kingdom)
&& kingdom.Equals(sticky.SideBKey, StringComparison.OrdinalIgnoreCase)
&& !forcePlotter)
{
AddUnique(sticky.SideBIds, id);
return;
}
// Default: living plot participants enroll on Side A.
AddUnique(sticky.SideAIds, id);
return;
}
string key = sticky.Frame == EnsembleFrame.KingdomVsKingdom
? LiveEnsemble.KingdomKeyOf(actor)
: LiveEnsemble.SpeciesKeyOf(actor);
if (string.IsNullOrEmpty(key))
{
return;
}
if (key.Equals(sticky.SideAKey, StringComparison.OrdinalIgnoreCase))
{
AddUnique(sticky.SideAIds, id);
}
else if (key.Equals(sticky.SideBKey, StringComparison.OrdinalIgnoreCase))
{
AddUnique(sticky.SideBIds, id);
}
}
Enroll(ensemble.Focus, forcePlotter: plotLike || familyLike || outbreakLike);
Enroll(ensemble.Related);
Enroll(ensemble.SideA?.Best, forcePlotter: plotLike || familyLike || outbreakLike);
Enroll(ensemble.SideB?.Best);
if (ensemble.ParticipantIds == null)
{
return;
}
for (int i = 0; i < ensemble.ParticipantIds.Count; i++)
{
long id = ensemble.ParticipantIds[i];
if (id == 0)
{
continue;
}
Actor actor = LiveEnsemble.FindTrackedActor(id);
if (actor != null)
{
// Participant ids from TryBuildPlot are plotters first; related may also appear.
bool forceA = plotLike
&& (actor == ensemble.Focus
|| actor == ensemble.SideA?.Best
|| (ensemble.Related == null || actor != ensemble.Related));
Enroll(actor, forcePlotter: forceA && actor != ensemble.Related);
}
}
}
private static void SeedPlotRoster(LiveSceneStickyState sticky, LiveEnsemble ensemble)
{
if (sticky == null || ensemble == null)
{
return;
}
// Ensure Side A display stays collective.
if (string.IsNullOrEmpty(sticky.SideADisplay)
|| LiveEnsemble.IsPlotterSideKey(sticky.SideADisplay))
{
sticky.SideADisplay = "Plotters";
}
if (ensemble.SideA != null && ensemble.SideA.Count > sticky.SideACount)
{
sticky.SideACount = ensemble.SideA.Count;
}
if (ensemble.SideB != null && ensemble.SideB.Count > 0)
{
sticky.SideBCount = ensemble.SideB.Count;
}
}
private static void SeedFamilyRoster(LiveSceneStickyState sticky, LiveEnsemble ensemble)
{
if (sticky == null || ensemble == null)
{
return;
}
if (ensemble.SideA != null)
{
if (!string.IsNullOrEmpty(ensemble.SideA.Display)
&& !LiveEnsemble.IsFamilyPackSideKey(ensemble.SideA.Display))
{
sticky.SideADisplay = ensemble.SideA.Display;
}
if (ensemble.SideA.Count > sticky.SideACount)
{
sticky.SideACount = ensemble.SideA.Count;
}
}
sticky.SideBKey = "";
sticky.SideBDisplay = "";
sticky.SideBCount = 0;
}
private static void SeedOutbreakRoster(LiveSceneStickyState sticky, LiveEnsemble ensemble)
{
if (sticky == null || ensemble == null)
{
return;
}
if (ensemble.SideA != null)
{
if (!string.IsNullOrEmpty(ensemble.SideA.Display)
&& !LiveEnsemble.IsStatusOutbreakSideKey(ensemble.SideA.Display))
{
sticky.SideADisplay = ensemble.SideA.Display;
}
if (ensemble.SideA.Count > sticky.SideACount)
{
sticky.SideACount = ensemble.SideA.Count;
}
}
sticky.SideBKey = "";
sticky.SideBDisplay = "";
sticky.SideBCount = 0;
}
private static void AddUnique(System.Collections.Generic.List<long> ids, long id)
{
for (int i = 0; i < ids.Count; i++)
{
if (ids[i] == id)
{
return;
}
}
ids.Add(id);
}
}