worldbox-observer-mod/IdleSpectator/WorldActivityScanner.cs

1034 lines
31 KiB
C#

using System.Collections.Generic;
using NeoModLoader.services;
using UnityEngine;
namespace IdleSpectator;
/// <summary>
/// Scores live world activity across civs and wild mobs (wiki-aligned).
/// Throttled to ~2 Hz while Spectator is active.
/// </summary>
public static class WorldActivityScanner
{
private static readonly HashSet<string> SpectacleAssets = new HashSet<string>
{
"dragon",
"greg",
"demon",
"evil_mage",
"necromancer",
"ufo",
"cold_one",
"walker",
"snowman",
"bandit",
"tumor_monster",
"bioblob",
"assimilator",
"lil_pumpkin",
"fire_elemental",
"skeleton",
"zombie",
"ghost",
"mush"
};
private static readonly System.Random Rng = new System.Random();
private static float _lastScanAt = -999f;
private static InterestEvent _cachedBest;
private static InterestEvent _cachedBattle;
private const float ScanInterval = 0.5f;
private static float _lastEmptyLogAt = -999f;
public static InterestEvent FindBestLiveTarget()
{
EnsureScanned();
if (_cachedBest == null && Time.unscaledTime - _lastEmptyLogAt > 15f)
{
_lastEmptyLogAt = Time.unscaledTime;
LogService.LogInfo("[IdleSpectator] No live units to follow yet");
}
return Clone(_cachedBest);
}
public static InterestEvent FindHottestBattle()
{
EnsureScanned();
return Clone(_cachedBattle);
}
/// <summary>Register up to <paramref name="max"/> character-led vignette seeds into the registry.</summary>
public static void RegisterTopCharacterCandidates(int max = 4)
{
EnsureScanned();
Dictionary<string, int> speciesCounts = CountSpeciesPopulations();
List<Actor> alive = new List<Actor>(128);
foreach (Actor actor in EnumerateAliveUnits())
{
if (actor?.asset != null && actor.asset.is_boat)
{
continue;
}
if (actor != null && actor.isAlive())
{
alive.Add(actor);
}
}
if (alive.Count == 0)
{
return;
}
alive.Sort((a, b) => ScoreActor(b, speciesCounts).CompareTo(ScoreActor(a, speciesCounts)));
int n = Mathf.Min(max, alive.Count);
for (int i = 0; i < n; i++)
{
Actor actor = alive[i];
ActivityBand band = ActivityInterestTable.Classify(actor);
InterestLeadKind lead = InterestLeadKind.CharacterLed;
// Actions outrank characters: combat/hot work are events; kings walking stay vignettes.
if (band == ActivityBand.Hot || actor.has_attack_target)
{
lead = InterestLeadKind.EventLed;
}
else if (band >= ActivityBand.Warm)
{
lead = InterestLeadKind.EventLed;
}
else if (IsSpectacle(actor) || IsSingletonSpecies(actor, speciesCounts))
{
lead = InterestLeadKind.CharacterLed;
}
SplitActorScore(actor, speciesCounts, out float actionScore, out float charScore);
long id = 0;
try
{
id = actor.getID();
}
catch
{
continue;
}
string key = lead == InterestLeadKind.CharacterLed
? "vignette:" + id + ":" + (actor.asset != null ? actor.asset.id : "unit")
: "scan:evt:" + id;
string label = FormatUnitLabel(actor, speciesCounts);
if (actor.has_attack_target)
{
label = "Fighting: " + SafeName(actor);
}
else if (band == ActivityBand.Hot)
{
label = "In action";
}
bool combat = actor.has_attack_target || band == ActivityBand.Hot;
int fighters = 1;
int notables = InterestScoring.IsNotable(actor) ? 1 : 0;
if (combat)
{
CountFightCluster(actor.current_position, 10f, out fighters, out notables, out _);
fighters = Mathf.Max(1, fighters);
}
var candidate = new InterestCandidate
{
Key = key,
LeadKind = lead,
Category = combat
? "Combat"
: (lead == InterestLeadKind.CharacterLed ? "CharacterVignette" : "Work"),
Source = "scanner",
EventStrength = lead == InterestLeadKind.EventLed
? actionScore
: actionScore * InterestScoringConfig.W.characterLedEventMul,
CharacterSignificance = charScore,
VisualConfidence = band >= ActivityBand.Warm ? 0.7f : 0.4f,
Position = actor.current_position,
FollowUnit = actor,
SubjectId = id,
Label = label,
Verb = actor.has_attack_target ? "fighting" : (band == ActivityBand.Hot ? "busy" : ""),
AssetId = actor.asset != null ? actor.asset.id : "scored_unit",
SpeciesId = actor.asset != null ? actor.asset.id : "",
CityKey = actor.city != null ? (actor.city.name ?? "") : "",
KingdomKey = actor.kingdom != null ? (actor.kingdom.name ?? "") : "",
ParticipantCount = combat ? fighters : 0,
NotableParticipantCount = combat ? notables : 0,
CreatedAt = Time.unscaledTime,
LastSeenAt = Time.unscaledTime,
ExpiresAt = Time.unscaledTime + 18f,
MinWatch = 2f,
MaxWatch = 22f,
Completion = combat
? InterestCompletionKind.CombatActive
: (lead == InterestLeadKind.EventLed
? InterestCompletionKind.ActivityActive
: InterestCompletionKind.CharacterVignette)
};
InterestScoring.ScoreCheap(candidate);
InterestRegistry.Upsert(candidate);
}
}
private static void EnsureScanned()
{
if (Time.unscaledTime - _lastScanAt < ScanInterval)
{
return;
}
_lastScanAt = Time.unscaledTime;
ScanNow();
}
private static void ScanNow()
{
_cachedBattle = BuildHottestBattle();
InterestEvent bestUnit = BuildBestScoredUnit();
_cachedBest = PreferRicherAction(_cachedBattle, bestUnit);
}
/// <summary>
/// Pick battle vs unit dynamically: mass fights win unless a notable 1v1 outranks an anonymous melee.
/// </summary>
private static InterestEvent PreferRicherAction(InterestEvent battle, InterestEvent unit)
{
if (battle == null)
{
return unit;
}
if (unit == null)
{
return battle;
}
bool unitDuel = unit.FollowUnit != null
&& unit.FollowUnit.has_attack_target
&& unit.ParticipantCount > 0
&& unit.ParticipantCount <= InterestScoringConfig.W.duelMaxFighters
&& unit.NotableParticipantCount >= 2;
bool battleAnonymousMelee = battle.ParticipantCount >= InterestScoringConfig.W.massFighterThreshold
&& battle.NotableParticipantCount == 0;
if (unitDuel && battleAnonymousMelee
&& unit.Score + InterestScoringConfig.W.preferNotableDuelMargin >= battle.Score)
{
return unit;
}
return battle.Score >= unit.Score ? battle : unit;
}
private static InterestEvent BuildHottestBattle()
{
HashSet<BattleContainer> battles = BattleKeeperManager.get();
if (battles == null || battles.Count == 0)
{
// Fall through to live fight clusters with no BattleKeeper entry yet.
return BuildHottestFightCluster();
}
InterestEvent best = null;
float bestScore = float.MinValue;
foreach (BattleContainer battle in battles)
{
if (battle?.tile == null)
{
continue;
}
int deaths = battle.getDeathsTotal();
Vector3 pos = battle.tile.posV3;
CountFightCluster(pos, 14f, out int fighters, out int notables, out Actor follow);
if (deaths < 1 && fighters < 2)
{
continue;
}
fighters = Mathf.Max(fighters, deaths > 0 ? 2 : 0);
float score = ScoreFightCluster(deaths, fighters, notables);
if (score > bestScore)
{
bestScore = score;
string label = fighters >= 5 || deaths >= 6
? $"Battle ({fighters} fighting, {deaths} fallen)"
: fighters <= 2
? $"Duel ({deaths} fallen)"
: $"Skirmish ({fighters} fighting)";
best = new InterestEvent
{
Score = score,
Position = pos,
FollowUnit = follow ?? FindNearestAliveUnit(pos, 14f),
Label = label,
CreatedAt = Time.unscaledTime,
AssetId = "live_battle",
ParticipantCount = fighters,
NotableParticipantCount = notables,
CharacterSignificance = notables * InterestScoringConfig.W.battleCharPerNotable
};
}
}
InterestEvent live = BuildHottestFightCluster();
return PreferRicherAction(best, live);
}
/// <summary>Cluster of units currently in combat (even before BattleKeeper records deaths).</summary>
private static InterestEvent BuildHottestFightCluster()
{
InterestEvent best = null;
float bestScore = float.MinValue;
var seen = new HashSet<long>();
foreach (Actor actor in EnumerateAliveUnits())
{
if (actor == null || !actor.isAlive() || !actor.has_attack_target)
{
continue;
}
long id = 0;
try
{
id = actor.getID();
}
catch
{
continue;
}
if (seen.Contains(id))
{
continue;
}
CountFightCluster(actor.current_position, 10f, out int fighters, out int notables, out Actor follow);
if (fighters < 1)
{
continue;
}
// Mark cluster members so we don't emit N duplicates.
MarkClusterSeen(actor.current_position, 10f, seen);
float score = ScoreFightCluster(deaths: 0, fighters, notables);
if (score > bestScore)
{
bestScore = score;
best = new InterestEvent
{
Score = score,
Position = actor.current_position,
FollowUnit = follow ?? actor,
Label = fighters <= 2 ? "Duel" : $"Fight ({fighters})",
CreatedAt = Time.unscaledTime,
AssetId = "live_battle",
ParticipantCount = fighters,
NotableParticipantCount = notables,
CharacterSignificance = notables * InterestScoringConfig.W.battleCharPerNotable
};
}
}
return best;
}
private static float ScoreFightCluster(int deaths, int fighters, int notables)
{
ScoringWeights w = InterestScoringConfig.W;
float score = deaths * w.clusterDeathWeight
+ Mathf.Max(0, fighters) * w.clusterFighterWeight
+ notables * w.clusterNotableWeight;
// Multi-person melees get a clear bump over thin 1v1s.
if (fighters >= w.massFighterThreshold)
{
score += w.clusterMassBonus;
}
else if (fighters <= w.duelMaxFighters)
{
score += w.clusterDuelPenalty;
// Notable duels recover - king vs king / favorites.
if (notables >= 2)
{
score += w.clusterNotableDuelBonus;
}
else if (notables == 1)
{
score += w.clusterSingleNotableBonus;
}
}
return score;
}
private static void CountFightCluster(
Vector2 pos,
float radius,
out int fighters,
out int notables,
out Actor bestFollow)
{
fighters = 0;
notables = 0;
bestFollow = null;
float bestChar = -1f;
float r2 = radius * radius;
foreach (Actor actor in EnumerateAliveUnits())
{
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;
}
bool fighting = false;
try
{
fighting = actor.has_attack_target
|| (actor.hasTask() && actor.ai?.task != null && actor.ai.task.in_combat);
}
catch
{
fighting = false;
}
if (!fighting)
{
continue;
}
fighters++;
bool notable = InterestScoring.IsNotable(actor);
bool extreme = InterestScoring.IsExtremelyNotable(actor);
if (notable)
{
notables++;
}
SplitActorScore(actor, null, out _, out float charScore);
float weight = charScore + (extreme ? 30f : 0f);
if (weight > bestChar)
{
bestChar = weight;
bestFollow = actor;
}
}
}
private static void MarkClusterSeen(Vector2 pos, float radius, HashSet<long> seen)
{
float r2 = radius * radius;
foreach (Actor actor in EnumerateAliveUnits())
{
if (actor == null || !actor.isAlive() || !actor.has_attack_target)
{
continue;
}
float dx = actor.current_position.x - pos.x;
float dy = actor.current_position.y - pos.y;
if (dx * dx + dy * dy > r2)
{
continue;
}
try
{
seen.Add(actor.getID());
}
catch
{
// ignore
}
}
}
private static InterestEvent BuildBestScoredUnit()
{
Dictionary<string, int> speciesCounts = new Dictionary<string, int>();
List<Actor> alive = new List<Actor>(128);
foreach (Actor actor in EnumerateAliveUnits())
{
if (actor.asset != null && actor.asset.is_boat)
{
continue;
}
alive.Add(actor);
string speciesId = actor.asset != null ? actor.asset.id : "unknown";
speciesCounts.TryGetValue(speciesId, out int count);
speciesCounts[speciesId] = count + 1;
}
if (alive.Count == 0)
{
return null;
}
Actor best = null;
float bestScore = float.MinValue;
List<Actor> nearTies = new List<Actor>();
for (int i = 0; i < alive.Count; i++)
{
Actor actor = alive[i];
float unitScore = ScoreActor(actor, speciesCounts);
if (unitScore > bestScore + 0.01f)
{
bestScore = unitScore;
best = actor;
nearTies.Clear();
nearTies.Add(actor);
}
else if (Mathf.Abs(unitScore - bestScore) <= 2f)
{
nearTies.Add(actor);
}
}
if (nearTies.Count > 1)
{
best = nearTies[Rng.Next(nearTies.Count)];
bestScore = ScoreActor(best, speciesCounts);
}
if (best == null)
{
return null;
}
ActivityBand band = ActivityInterestTable.Classify(best);
SplitActorScore(best, speciesCounts, out float actionScore, out float charScore);
int fighters = 0;
int notables = 0;
if (best.has_attack_target || band == ActivityBand.Hot)
{
CountFightCluster(best.current_position, 10f, out fighters, out notables, out _);
fighters = Mathf.Max(1, fighters);
}
float finalScore = ScoreFightCluster(0, fighters, notables);
float rankChar = InterestScoringConfig.W.scannerRankCharWeight;
if (fighters <= 0)
{
finalScore = actionScore + charScore * rankChar;
}
else
{
finalScore = Mathf.Max(finalScore, actionScore + charScore * rankChar);
}
return new InterestEvent
{
Score = finalScore,
Position = best.current_position,
FollowUnit = best,
Label = FormatUnitLabel(best, speciesCounts),
CreatedAt = Time.unscaledTime,
AssetId = best.asset != null ? best.asset.id : "scored_unit",
ParticipantCount = fighters,
NotableParticipantCount = notables,
CharacterSignificance = charScore
};
}
/// <summary>Public wrappers for dossier / harness (same logic as private scorers).</summary>
public static Dictionary<string, int> CountSpeciesPopulations()
{
Dictionary<string, int> speciesCounts = new Dictionary<string, int>();
foreach (Actor actor in EnumerateAliveUnits())
{
if (actor?.asset == null)
{
continue;
}
string id = actor.asset.id;
if (!speciesCounts.ContainsKey(id))
{
speciesCounts[id] = 0;
}
speciesCounts[id]++;
}
return speciesCounts;
}
public static bool HarnessActivityFirstScoringOk(out string detail)
{
// Mirror ScoreActor importance gating with synthetic bands.
float Cold(float importance) =>
ComposeScore(4f, ActivityBand.Cold, importance);
float Warm(float importance) =>
ComposeScore(28f, ActivityBand.Warm, importance);
float Hot(float importance) =>
ComposeScore(55f, ActivityBand.Hot, importance);
float coldKingFav = Cold(40f); // favorite + king
float hotPeasant = Hot(0f);
float warmFav = Warm(22f);
float coldPeasant = Cold(0f);
bool ok = hotPeasant > coldKingFav
&& warmFav > coldKingFav
&& hotPeasant > warmFav
&& coldKingFav > coldPeasant;
detail =
$"coldKingFav={coldKingFav:0.0} hotPeasant={hotPeasant:0.0} warmFav={warmFav:0.0} coldPeasant={coldPeasant:0.0}";
return ok;
}
private static float ComposeScore(float activity, ActivityBand band, float importance)
{
if (band >= ActivityBand.Warm || activity >= ActivityInterestTable.WarmScoreFloor)
{
return activity + importance * 0.85f;
}
return activity + importance * 0.08f;
}
public static float ScoreActorPublic(Actor actor, Dictionary<string, int> speciesCounts)
{
return ScoreActor(actor, speciesCounts);
}
public static bool IsSpectaclePublic(Actor actor)
{
return IsSpectacle(actor);
}
public static bool IsSingletonSpeciesPublic(Actor actor, Dictionary<string, int> speciesCounts)
{
return IsSingletonSpecies(actor, speciesCounts);
}
public static string FormatUnitLabelPublic(Actor actor, Dictionary<string, int> speciesCounts)
{
return FormatUnitLabel(actor, speciesCounts);
}
private static float ScoreActor(Actor actor, Dictionary<string, int> speciesCounts)
{
SplitActorScore(actor, speciesCounts, out float action, out float character);
// Action-primary total used for ranking; character is a light tie-break.
return action + character * InterestScoringConfig.W.scannerRankCharWeight;
}
/// <summary>
/// Split live action intensity from who the unit is.
/// Same action → higher character wins; character alone never dominates a real fight.
/// </summary>
private static void SplitActorScore(
Actor actor,
Dictionary<string, int> speciesCounts,
out float actionScore,
out float characterScore)
{
actionScore = 0f;
characterScore = 0f;
if (actor == null)
{
return;
}
ScoringWeights w = InterestScoringConfig.W;
float activity = ActivityInterestTable.ScoreActivity(actor);
ActivityBand band = ActivityInterestTable.Classify(actor);
actionScore = activity;
if (actor.has_attack_target)
{
actionScore += w.scannerAttackBonus;
}
if (band == ActivityBand.Hot)
{
actionScore += w.scannerHotBonus;
}
else if (band == ActivityBand.Warm)
{
actionScore += w.scannerWarmBonus;
}
if (IsSpectacle(actor))
{
actionScore += w.scannerSpectacleBonus;
}
int kills = actor.data != null ? actor.data.kills : 0;
if (band >= ActivityBand.Warm || actor.has_attack_target)
{
actionScore += Mathf.Min(w.scannerActionKillsCap, kills * w.scannerActionKillsFactor);
}
// Character significance - tie-break / amplifier only.
if (actor.isFavorite())
{
characterScore += w.metaFavorite;
}
if (actor.isKing())
{
characterScore += w.metaKing;
}
else if (actor.isCityLeader())
{
characterScore += w.metaLeader;
}
else if (actor.is_profession_warrior)
{
characterScore += w.scannerWarriorBonus;
}
string speciesId = actor.asset != null ? actor.asset.id : "unknown";
if (speciesCounts != null
&& speciesCounts.TryGetValue(speciesId, out int pop)
&& pop == 1)
{
characterScore += w.scannerSingletonBonus;
}
float renownDiv = w.scannerCharRenownDivisor > 0f ? w.scannerCharRenownDivisor : 120f;
characterScore += Mathf.Min(w.scannerCharRenownCap, actor.renown / renownDiv);
characterScore += Mathf.Min(w.scannerCharKillsCap, kills * w.scannerCharKillsFactor);
// Cold kings barely score - do not let identity monopolize the camera.
if (band == ActivityBand.Cold && !actor.has_attack_target && !IsSpectacle(actor))
{
actionScore *= w.scannerColdActionMul;
characterScore *= w.scannerColdCharMul;
}
actionScore += (actor.getID() % 7) * 0.01f;
}
/// <summary>True when the focused unit's current activity is Cold (idle scoring).</summary>
public static bool IsFocusActivityCold(Actor actor)
{
return ActivityInterestTable.Classify(actor) == ActivityBand.Cold
&& (actor == null || !actor.has_attack_target);
}
private static int CountSubspeciesMembers(Subspecies subspecies)
{
int count = 0;
// Prefer actor.asset.units filtered by subspecies when possible.
if (subspecies == null)
{
return 0;
}
foreach (Actor unit in EnumerateAliveUnits())
{
if (unit.subspecies == subspecies)
{
count++;
if (count > 8)
{
return count;
}
}
}
return count;
}
private static bool IsSpectacle(Actor actor)
{
if (actor?.asset == null)
{
return false;
}
string id = actor.asset.id;
if (SpectacleAssets.Contains(id))
{
return true;
}
// Loose match for variant ids.
foreach (string key in SpectacleAssets)
{
if (id.Contains(key))
{
return true;
}
}
return false;
}
private static bool IsSingletonSpecies(Actor actor, Dictionary<string, int> speciesCounts)
{
if (actor?.asset == null)
{
return false;
}
return speciesCounts.TryGetValue(actor.asset.id, out int pop) && pop == 1;
}
private static string FormatUnitLabel(Actor actor, Dictionary<string, int> speciesCounts)
{
string name = SafeName(actor);
string species = actor.asset != null ? actor.asset.id : "creature";
// Event / activity first - titles alone are not a watch reason (nametag already has the name).
if (actor.has_attack_target)
{
return $"Fighting: {name}";
}
if (IsSpectacle(actor))
{
return $"Spectacle: {name} ({species})";
}
if (actor.isFavorite())
{
return $"Favorite: {name}";
}
if (actor.isKing())
{
string kingdom = actor.kingdom != null ? actor.kingdom.name : "realm";
return $"King of {kingdom}";
}
if (actor.isCityLeader())
{
string city = actor.city != null ? actor.city.name : "town";
return $"Leader of {city}";
}
if (IsSingletonSpecies(actor, speciesCounts))
{
return $"Lone {species}: {name}";
}
int kills = actor.data != null ? actor.data.kills : 0;
if (kills > 0)
{
return $"{name} ({species}, {kills} kills)";
}
return $"{name} ({species})";
}
public static InterestEvent FromNewSpecies(Subspecies subspecies, ActorAsset asset, WorldTile tile)
{
if (asset == null)
{
return null;
}
Vector3 pos = tile != null ? tile.posV3 : Vector3.zero;
Actor follow = FindMatchingSpeciesUnit(asset, subspecies, pos, 30f);
if (follow != null)
{
pos = follow.current_position;
}
// No matching unit yet = ghost tip (wrong creature gets focused). Skip until one exists.
if (follow == null)
{
return null;
}
return new InterestEvent
{
Score = 5f,
Position = pos,
FollowUnit = follow,
Label = $"New species: {asset.id}",
CreatedAt = Time.unscaledTime,
AssetId = asset.id
};
}
/// <summary>
/// Nearest living unit, optionally restricted to a specific actor asset id.
/// </summary>
public static Actor FindNearestAliveUnit(Vector3 pos, float maxDist, string requireAssetId = null)
{
// Freshly spawned units are often on asset.units before units_only_alive updates.
if (!string.IsNullOrEmpty(requireAssetId))
{
Actor fromAsset = FindAliveOnAssetList(requireAssetId, pos, maxDist);
if (fromAsset != null)
{
return fromAsset;
}
}
Actor best = null;
float bestDist = maxDist * maxDist;
foreach (Actor actor in EnumerateAliveUnits())
{
if (requireAssetId != null
&& (actor.asset == null || actor.asset.id != requireAssetId))
{
continue;
}
float dx = actor.current_position.x - pos.x;
float dy = actor.current_position.y - pos.y;
float d2 = dx * dx + dy * dy;
if (d2 < bestDist)
{
bestDist = d2;
best = actor;
}
}
return best;
}
/// <summary>
/// Any living unit of this asset id (asset.units first, then world scan).
/// </summary>
public static Actor FindAliveByAssetId(string assetId, Vector3 nearPos, float maxDist = 5000f)
{
if (string.IsNullOrEmpty(assetId))
{
return null;
}
return FindNearestAliveUnit(nearPos, maxDist, assetId);
}
private static Actor FindAliveOnAssetList(string assetId, Vector3 nearPos, float maxDist)
{
ActorAsset asset = AssetManager.actor_library?.get(assetId);
if (asset?.units == null)
{
return null;
}
Actor best = null;
float bestDist = maxDist * maxDist;
foreach (Actor unit in asset.units)
{
if (unit == null || !unit.isAlive())
{
continue;
}
float dx = unit.current_position.x - nearPos.x;
float dy = unit.current_position.y - nearPos.y;
float d2 = dx * dx + dy * dy;
if (d2 < bestDist)
{
bestDist = d2;
best = unit;
}
}
return best;
}
private static Actor FindMatchingSpeciesUnit(ActorAsset asset, Subspecies subspecies, Vector3 nearPos, float maxDist)
{
if (asset?.units != null)
{
foreach (Actor unit in asset.units)
{
if (unit != null && unit.isAlive() && (subspecies == null || unit.subspecies == subspecies))
{
return unit;
}
}
}
return FindNearestAliveUnit(nearPos, maxDist, asset != null ? asset.id : null);
}
private static IEnumerable<Actor> EnumerateAliveUnits()
{
return EnumerateAliveUnitsPublic();
}
public static IEnumerable<Actor> EnumerateAliveUnitsPublic()
{
List<Actor> prepared = World.world?.units?.units_only_alive;
if (prepared != null && prepared.Count > 0)
{
for (int i = 0; i < prepared.Count; i++)
{
Actor actor = prepared[i];
if (actor != null && actor.isAlive())
{
yield return actor;
}
}
yield break;
}
if (World.world?.units == null)
{
yield break;
}
foreach (Actor actor in World.world.units)
{
if (actor != null && actor.isAlive())
{
yield return actor;
}
}
}
private static string SafeName(Actor actor)
{
try
{
string name = actor.getName();
if (!string.IsNullOrEmpty(name))
{
return name;
}
}
catch
{
// ignore
}
return actor.asset != null ? actor.asset.id : "creature";
}
private static InterestEvent Clone(InterestEvent source)
{
if (source == null)
{
return null;
}
return new InterestEvent
{
Score = source.Score,
Position = source.Position,
FollowUnit = source.FollowUnit,
Label = source.Label,
CreatedAt = Time.unscaledTime,
AssetId = source.AssetId,
ParticipantCount = source.ParticipantCount,
NotableParticipantCount = source.NotableParticipantCount,
CharacterSignificance = source.CharacterSignificance
};
}
}