842 lines
22 KiB
C#
842 lines
22 KiB
C#
using System;
|
||
using UnityEngine;
|
||
|
||
namespace IdleSpectator;
|
||
|
||
/// <summary>How interesting a unit's current AI task/job is for idle scoring.</summary>
|
||
public enum ActivityBand
|
||
{
|
||
Cold = 0,
|
||
Warm = 1,
|
||
Hot = 2
|
||
}
|
||
|
||
/// <summary>
|
||
/// Maps WorldBox task / citizen-job ids (and combat flags) to Hot/Warm/Cold interest.
|
||
/// Unknown ids default to Warm if they look like work, else Cold.
|
||
/// </summary>
|
||
public static class ActivityInterestTable
|
||
{
|
||
public const float WarmScoreFloor = 18f;
|
||
public const float HotScoreFloor = 40f;
|
||
|
||
/// <summary>Activity score 0–100 used by idle scoring.</summary>
|
||
public static float ScoreActivity(Actor actor)
|
||
{
|
||
if (actor == null || !actor.isAlive())
|
||
{
|
||
return 0f;
|
||
}
|
||
|
||
ActivityBand band = Classify(actor);
|
||
float score = band switch
|
||
{
|
||
ActivityBand.Hot => 55f,
|
||
ActivityBand.Warm => 28f,
|
||
_ => 4f
|
||
};
|
||
|
||
if (actor.has_attack_target)
|
||
{
|
||
score = Mathf.Max(score, 70f);
|
||
}
|
||
|
||
try
|
||
{
|
||
if (actor.hasTask() && actor.ai?.task != null && actor.ai.task.in_combat)
|
||
{
|
||
score = Mathf.Max(score, 72f);
|
||
}
|
||
|
||
if (actor.hasTask() && actor.ai?.task != null && actor.ai.task.is_fireman)
|
||
{
|
||
score = Mathf.Max(score, 60f);
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// ignore
|
||
}
|
||
|
||
return Mathf.Clamp(score, 0f, 100f);
|
||
}
|
||
|
||
public static ActivityBand Classify(Actor actor)
|
||
{
|
||
if (actor == null || !actor.isAlive())
|
||
{
|
||
return ActivityBand.Cold;
|
||
}
|
||
|
||
if (actor.has_attack_target)
|
||
{
|
||
return ActivityBand.Hot;
|
||
}
|
||
|
||
string taskId = SafeTaskId(actor);
|
||
string jobId = SafeJobId(actor);
|
||
|
||
ActivityBand fromTask = ClassifyId(taskId);
|
||
ActivityBand fromJob = ClassifyId(jobId);
|
||
ActivityBand best = fromTask > fromJob ? fromTask : fromJob;
|
||
|
||
try
|
||
{
|
||
if (actor.hasTask() && actor.ai?.task != null)
|
||
{
|
||
if (actor.ai.task.in_combat || actor.ai.task.is_fireman)
|
||
{
|
||
return ActivityBand.Hot;
|
||
}
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// ignore
|
||
}
|
||
|
||
return best;
|
||
}
|
||
|
||
public static ActivityBand ClassifyId(string id)
|
||
{
|
||
if (string.IsNullOrEmpty(id))
|
||
{
|
||
return ActivityBand.Cold;
|
||
}
|
||
|
||
string s = id.ToLowerInvariant();
|
||
|
||
if (ContainsAny(s,
|
||
"fight", "attack", "hunt", "fireman", "fire", "war", "kill", "danger",
|
||
"unload", "throw_resource", "capture", "siege"))
|
||
{
|
||
return ActivityBand.Hot;
|
||
}
|
||
|
||
if (ContainsAny(s,
|
||
"farm", "harvest", "plant", "fertiliz", "mine", "gather", "woodcut", "chop",
|
||
"build", "construct", "road", "smith", "blacksmith", "pollinat", "bee",
|
||
"honey", "herb", "bush", "social", "lover", "breed", "fish", "trade",
|
||
"clean", "fertiliz", "bucket", "hoe"))
|
||
{
|
||
return ActivityBand.Warm;
|
||
}
|
||
|
||
if (ContainsAny(s,
|
||
"eat", "sleep", "wait", "idle", "random_move", "random_swim", "make_decision",
|
||
"run_away", "swim_to", "print_", "godfinger", "end_job"))
|
||
{
|
||
return ActivityBand.Cold;
|
||
}
|
||
|
||
// Unknown active task: mild warm so we do not ignore novel work.
|
||
if (s.Length > 0)
|
||
{
|
||
return ActivityBand.Warm;
|
||
}
|
||
|
||
return ActivityBand.Cold;
|
||
}
|
||
|
||
public static string SafeTaskId(Actor actor)
|
||
{
|
||
try
|
||
{
|
||
if (actor != null && actor.hasTask() && actor.ai?.task != null)
|
||
{
|
||
return actor.ai.task.id ?? "";
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// ignore
|
||
}
|
||
|
||
return "";
|
||
}
|
||
|
||
public static string SafeJobId(Actor actor)
|
||
{
|
||
try
|
||
{
|
||
if (actor?.citizen_job != null)
|
||
{
|
||
return actor.citizen_job.id ?? "";
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// ignore
|
||
}
|
||
|
||
return "";
|
||
}
|
||
|
||
public static string DumpFocusProbe(Actor actor)
|
||
{
|
||
if (actor == null)
|
||
{
|
||
return "actor=null";
|
||
}
|
||
|
||
string name = "?";
|
||
try
|
||
{
|
||
name = actor.getName() ?? "?";
|
||
}
|
||
catch
|
||
{
|
||
// ignore
|
||
}
|
||
|
||
string taskId = SafeTaskId(actor);
|
||
string jobId = SafeJobId(actor);
|
||
string taskLoc = "";
|
||
try
|
||
{
|
||
if (actor.hasTask() && actor.ai?.task != null)
|
||
{
|
||
taskLoc = actor.ai.task.getLocalizedText() ?? "";
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// ignore
|
||
}
|
||
|
||
string actorTarget = TargetLabel(actor);
|
||
ActivityBand band = Classify(actor);
|
||
return
|
||
$"name={name} task='{taskId}' job='{jobId}' loc='{taskLoc}' target='{actorTarget}' "
|
||
+ $"band={band} activity={ScoreActivity(actor):0.0} attack={actor.has_attack_target}";
|
||
}
|
||
|
||
public static string TargetLabel(Actor actor)
|
||
{
|
||
ResolveTarget(actor, out string name, out _, out string place);
|
||
return !string.IsNullOrEmpty(name) ? name : place;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Prefer live actor targets (by name). Places/buildings stay as type labels.
|
||
/// </summary>
|
||
public static void ResolveTarget(
|
||
Actor actor,
|
||
out string targetName,
|
||
out bool targetIsActor,
|
||
out string placeLabel)
|
||
{
|
||
targetName = "";
|
||
targetIsActor = false;
|
||
placeLabel = "";
|
||
if (actor == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
if (actor.has_attack_target && actor.attack_target != null && actor.attack_target.isAlive())
|
||
{
|
||
if (actor.attack_target.isActor())
|
||
{
|
||
Actor foe = actor.attack_target.a;
|
||
if (foe != null)
|
||
{
|
||
string n = foe.getName();
|
||
if (!string.IsNullOrEmpty(n))
|
||
{
|
||
targetName = n;
|
||
targetIsActor = true;
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (actor.beh_actor_target != null && actor.beh_actor_target.isAlive())
|
||
{
|
||
if (actor.beh_actor_target.isActor())
|
||
{
|
||
Actor other = actor.beh_actor_target.a;
|
||
if (other != null)
|
||
{
|
||
string n = other.getName();
|
||
if (!string.IsNullOrEmpty(n))
|
||
{
|
||
targetName = n;
|
||
targetIsActor = true;
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
placeLabel = "their mark";
|
||
return;
|
||
}
|
||
|
||
if (actor.beh_building_target != null && actor.beh_building_target.isAlive())
|
||
{
|
||
Building b = actor.beh_building_target;
|
||
string type = b.asset != null ? b.asset.type : "";
|
||
string id = b.asset != null ? b.asset.id : "";
|
||
if (!string.IsNullOrEmpty(type))
|
||
{
|
||
placeLabel = type.Replace("type_", "");
|
||
return;
|
||
}
|
||
|
||
placeLabel = string.IsNullOrEmpty(id) ? "a building" : id;
|
||
return;
|
||
}
|
||
|
||
if (actor.beh_tile_target != null)
|
||
{
|
||
placeLabel = "the wilds";
|
||
}
|
||
|
||
// Prefer settlement names when no building/tile label won.
|
||
if (string.IsNullOrEmpty(placeLabel) && string.IsNullOrEmpty(targetName))
|
||
{
|
||
try
|
||
{
|
||
if (actor.city != null && !string.IsNullOrEmpty(actor.city.name))
|
||
{
|
||
placeLabel = actor.city.name;
|
||
}
|
||
else if (actor.kingdom != null && !string.IsNullOrEmpty(actor.kingdom.name))
|
||
{
|
||
placeLabel = actor.kingdom.name;
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// ignore
|
||
}
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// ignore
|
||
}
|
||
}
|
||
|
||
public static string ActorName(Actor actor)
|
||
{
|
||
if (actor == null)
|
||
{
|
||
return "";
|
||
}
|
||
|
||
try
|
||
{
|
||
string n = actor.getName();
|
||
return string.IsNullOrEmpty(n) ? "" : n;
|
||
}
|
||
catch
|
||
{
|
||
return "";
|
||
}
|
||
}
|
||
|
||
/// <summary>Build a prose context snapshot from a live actor.</summary>
|
||
public static ActivityContext BuildContext(Actor actor, string taskKeyHint = null)
|
||
{
|
||
ActivityContext ctx = new ActivityContext();
|
||
if (actor == null || !actor.isAlive())
|
||
{
|
||
return ctx;
|
||
}
|
||
|
||
ctx.ActorName = ActorName(actor);
|
||
try
|
||
{
|
||
if (actor.asset != null)
|
||
{
|
||
ActorAsset asset = actor.asset;
|
||
ActivityVoiceResolver.ApplyToContext(ctx, asset);
|
||
ctx.SpeciesLabel = ActivityAssetCatalog.SpeciesDisplayLabel(asset);
|
||
}
|
||
else
|
||
{
|
||
ctx.SpeciesId = "";
|
||
ctx.SpeciesLabel = "";
|
||
ctx.Family = ActivityVoiceFamilies.Default;
|
||
ctx.Voice = ProseVoice.Unknown();
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
ctx.SpeciesId = "";
|
||
ctx.SpeciesLabel = "";
|
||
ctx.Family = ActivityVoiceFamilies.Default;
|
||
ctx.Voice = ProseVoice.Unknown();
|
||
}
|
||
|
||
if (ctx.Voice == null)
|
||
{
|
||
ctx.Voice = ActivityVoiceResolver.Resolve(ctx.SpeciesId);
|
||
ctx.Family = ctx.Voice.BaseFamily;
|
||
}
|
||
|
||
ctx.TaskKey = taskKeyHint ?? SafeTaskId(actor);
|
||
ctx.JobId = SafeJobId(actor);
|
||
|
||
try
|
||
{
|
||
ctx.Age = actor.getAge();
|
||
}
|
||
catch
|
||
{
|
||
ctx.Age = 0;
|
||
}
|
||
|
||
ctx.IsChild = false;
|
||
if (!string.IsNullOrEmpty(ctx.TaskKey)
|
||
&& ctx.TaskKey.StartsWith("child_", System.StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
ctx.IsChild = true;
|
||
}
|
||
else if (ctx.Age > 0 && ctx.Age < 12
|
||
&& (ctx.IsHumanoid || ctx.Family == ActivityVoiceFamilies.Humanoid))
|
||
{
|
||
// Age bands are reliable for civ humanoids; animals often stay "young" by year count.
|
||
ctx.IsChild = true;
|
||
}
|
||
|
||
try
|
||
{
|
||
ctx.CityName = actor.city != null ? (actor.city.name ?? "") : "";
|
||
}
|
||
catch
|
||
{
|
||
ctx.CityName = "";
|
||
}
|
||
|
||
try
|
||
{
|
||
ctx.KingdomName = actor.kingdom != null ? (actor.kingdom.name ?? "") : "";
|
||
}
|
||
catch
|
||
{
|
||
ctx.KingdomName = "";
|
||
}
|
||
|
||
try
|
||
{
|
||
ctx.IsKing = actor.isKing();
|
||
ctx.IsLeader = actor.isCityLeader();
|
||
ctx.IsWarrior = actor.is_profession_warrior;
|
||
ctx.HasAttackTarget = actor.has_attack_target;
|
||
}
|
||
catch
|
||
{
|
||
// ignore
|
||
}
|
||
|
||
try
|
||
{
|
||
if (actor.hasTask() && actor.ai?.task != null)
|
||
{
|
||
ctx.InCombat = actor.ai.task.in_combat || actor.has_attack_target;
|
||
}
|
||
else
|
||
{
|
||
ctx.InCombat = actor.has_attack_target;
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
ctx.InCombat = ctx.HasAttackTarget;
|
||
}
|
||
|
||
ResolveTarget(actor, out string targetName, out bool targetIsActor, out string place);
|
||
ctx.TargetName = targetName;
|
||
ctx.TargetIsActor = targetIsActor;
|
||
ctx.PlaceLabel = PickPlaceLabel(place, ctx.CityName, ctx.KingdomName, ctx.SpeciesId);
|
||
|
||
ctx.CarryingLabel = TryGetCarryingLabel(actor);
|
||
ctx.TopTraitId = TryTopInterestingTraitId(actor);
|
||
ctx.TopTraitLabel = TraitLabelFromId(ctx.TopTraitId);
|
||
ctx.RoleLabel = DeriveRoleLabel(ctx.IsKing, ctx.IsLeader, ctx.IsWarrior);
|
||
return ctx;
|
||
}
|
||
|
||
public static string DeriveRoleLabel(bool isKing, bool isLeader, bool isWarrior)
|
||
{
|
||
if (isKing)
|
||
{
|
||
return "king";
|
||
}
|
||
|
||
if (isLeader)
|
||
{
|
||
return "leader";
|
||
}
|
||
|
||
if (isWarrior)
|
||
{
|
||
return "warrior";
|
||
}
|
||
|
||
return "";
|
||
}
|
||
|
||
/// <summary>Humanize a live trait id the same way the dossier does - manner only.</summary>
|
||
public static string TraitLabelFromId(string traitId)
|
||
{
|
||
if (string.IsNullOrEmpty(traitId))
|
||
{
|
||
return "";
|
||
}
|
||
|
||
try
|
||
{
|
||
ActorTrait trait = AssetManager.traits?.get(traitId.Trim());
|
||
if (trait != null)
|
||
{
|
||
string locale = trait.getLocaleID();
|
||
if (!string.IsNullOrEmpty(locale))
|
||
{
|
||
string localized = LocalizedTextManager.getText(locale);
|
||
if (!string.IsNullOrEmpty(localized) && localized != locale)
|
||
{
|
||
return localized.Trim().ToLowerInvariant();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// fall through to id humanize
|
||
}
|
||
|
||
string s = traitId.Replace('_', ' ').Trim().ToLowerInvariant();
|
||
if (s.StartsWith("trait ", StringComparison.Ordinal))
|
||
{
|
||
s = s.Substring(6).Trim();
|
||
}
|
||
|
||
return s;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Real places only. Reject species ids, family/taxonomy buckets, and soft markers.
|
||
/// Wild kingdoms are often named after a species or type (buffalo, insect) - those are not places.
|
||
/// </summary>
|
||
public static string PickPlaceLabel(
|
||
string resolvedPlace,
|
||
string cityName,
|
||
string kingdomName,
|
||
string speciesId)
|
||
{
|
||
if (IsUsablePlace(cityName, speciesId))
|
||
{
|
||
return cityName.Trim();
|
||
}
|
||
|
||
if (IsConcreteResolvedPlace(resolvedPlace, speciesId))
|
||
{
|
||
return resolvedPlace.Trim();
|
||
}
|
||
|
||
// Kingdom only when it reads like a settlement/realm name, not a species bucket.
|
||
if (IsUsablePlace(kingdomName, speciesId) && LooksLikeRealmName(kingdomName))
|
||
{
|
||
return kingdomName.Trim();
|
||
}
|
||
|
||
if (!string.IsNullOrEmpty(resolvedPlace)
|
||
&& resolvedPlace.Equals("the wilds", System.StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return "the wilds";
|
||
}
|
||
|
||
return "";
|
||
}
|
||
|
||
private static bool IsUsablePlace(string name, string speciesId)
|
||
{
|
||
if (string.IsNullOrEmpty(name))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
return !LooksLikeSpeciesLabel(name, speciesId);
|
||
}
|
||
|
||
private static bool IsConcreteResolvedPlace(string place, string speciesId)
|
||
{
|
||
if (string.IsNullOrEmpty(place))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (place.Equals("their mark", System.StringComparison.OrdinalIgnoreCase)
|
||
|| place.Equals("the wilds", System.StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
return !LooksLikeSpeciesLabel(place, speciesId);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Civ kingdom/city names tend to be multi-word or proper-cased inventions.
|
||
/// Single taxonomic tokens like "insect" fail this check.
|
||
/// </summary>
|
||
private static bool LooksLikeRealmName(string name)
|
||
{
|
||
if (string.IsNullOrEmpty(name))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
string n = name.Trim();
|
||
if (n.IndexOf(' ') >= 0 || n.IndexOf('-') >= 0 || n.IndexOf('\'') >= 0)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
// Single token: allow only if it does not look like a plain species/type word.
|
||
// Realms like "Rome" pass; "insect" / "buffalo" fail via LooksLikeSpeciesLabel already,
|
||
// but also reject short all-lowercase taxonomy leftovers here.
|
||
if (n.Length <= 3)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
bool hasUpper = false;
|
||
for (int i = 0; i < n.Length; i++)
|
||
{
|
||
if (char.IsUpper(n[i]))
|
||
{
|
||
hasUpper = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
return hasUpper;
|
||
}
|
||
|
||
public static bool LooksLikeSpeciesPublic(string label, string speciesId)
|
||
{
|
||
return LooksLikeSpeciesLabel(label, speciesId);
|
||
}
|
||
|
||
private static readonly string[] TaxonomyBuckets =
|
||
{
|
||
"insect", "insects", "animal", "animals", "beast", "beasts", "creature", "creatures",
|
||
"monster", "monsters", "undead", "nature", "wildlife", "civilian", "civilians",
|
||
"unit", "units", "boat", "boats", "humanoid", "livestock", "bird", "birds",
|
||
"canine", "feline", "aquatic", "default", "wild"
|
||
};
|
||
|
||
private static bool LooksLikeSpeciesLabel(string label, string speciesId)
|
||
{
|
||
if (string.IsNullOrEmpty(label))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
string a = label.Trim();
|
||
string aLower = a.ToLowerInvariant();
|
||
|
||
for (int i = 0; i < TaxonomyBuckets.Length; i++)
|
||
{
|
||
if (aLower.Equals(TaxonomyBuckets[i], System.StringComparison.Ordinal))
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
|
||
if (!string.IsNullOrEmpty(speciesId))
|
||
{
|
||
string b = speciesId.Trim();
|
||
if (a.Equals(b, System.StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return true;
|
||
}
|
||
|
||
string bHuman = b.Replace('_', ' ');
|
||
if (a.Equals(bHuman, System.StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return true;
|
||
}
|
||
|
||
string family = ActivityVoiceResolver.Resolve(speciesId).BaseFamily;
|
||
if (!string.IsNullOrEmpty(family)
|
||
&& a.Equals(family, System.StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// Live actor library: any asset id / wild kingdom / locale name is not a place.
|
||
if (ActivityAssetCatalog.IsLiveSpeciesPlaceToken(a)
|
||
|| ActivityAssetCatalog.IsLiveActorAssetId(aLower))
|
||
{
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
public static string TryGetCarryingLabel(Actor actor)
|
||
{
|
||
if (actor == null)
|
||
{
|
||
return "";
|
||
}
|
||
|
||
try
|
||
{
|
||
if (!actor.isCarryingResources())
|
||
{
|
||
return "";
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
return "";
|
||
}
|
||
|
||
try
|
||
{
|
||
ActorBag inv = actor.inventory;
|
||
string id = "";
|
||
try
|
||
{
|
||
id = inv.getItemIDToRender() ?? "";
|
||
}
|
||
catch
|
||
{
|
||
id = "";
|
||
}
|
||
|
||
if (string.IsNullOrEmpty(id))
|
||
{
|
||
try
|
||
{
|
||
id = inv.getRandomResourceID() ?? "";
|
||
}
|
||
catch
|
||
{
|
||
id = "";
|
||
}
|
||
}
|
||
|
||
if (!string.IsNullOrEmpty(id))
|
||
{
|
||
return SanitizeCarrying(HumanizeResourceId(id));
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// ignore
|
||
}
|
||
|
||
return "what they carry";
|
||
}
|
||
|
||
private static string HumanizeResourceId(string id)
|
||
{
|
||
if (string.IsNullOrEmpty(id))
|
||
{
|
||
return "";
|
||
}
|
||
|
||
return id.Replace('_', ' ').Trim();
|
||
}
|
||
|
||
private static string SanitizeCarrying(string tip)
|
||
{
|
||
if (string.IsNullOrEmpty(tip))
|
||
{
|
||
return "";
|
||
}
|
||
|
||
string s = tip.Replace("\n", " ").Replace("\r", " ").Trim();
|
||
// Strip simple rich-text tags if present.
|
||
while (true)
|
||
{
|
||
int open = s.IndexOf('<');
|
||
if (open < 0)
|
||
{
|
||
break;
|
||
}
|
||
|
||
int close = s.IndexOf('>', open);
|
||
if (close < 0)
|
||
{
|
||
break;
|
||
}
|
||
|
||
s = s.Remove(open, close - open + 1);
|
||
}
|
||
|
||
s = s.Trim();
|
||
if (s.Length > 48)
|
||
{
|
||
s = s.Substring(0, 45).Trim() + "...";
|
||
}
|
||
|
||
return s;
|
||
}
|
||
|
||
private static string TryTopInterestingTraitId(Actor actor)
|
||
{
|
||
try
|
||
{
|
||
if (actor == null || !actor.hasTraits())
|
||
{
|
||
return "";
|
||
}
|
||
|
||
foreach (ActorTrait trait in actor.getTraits())
|
||
{
|
||
if (trait == null || string.IsNullOrEmpty(trait.id))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// Skip default species traits when detectable.
|
||
try
|
||
{
|
||
if (actor.asset != null
|
||
&& trait.default_for_actor_assets != null
|
||
&& trait.default_for_actor_assets.Contains(actor.asset))
|
||
{
|
||
continue;
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// ignore
|
||
}
|
||
|
||
return trait.id;
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// ignore
|
||
}
|
||
|
||
return "";
|
||
}
|
||
|
||
private static bool ContainsAny(string hay, params string[] needles)
|
||
{
|
||
for (int i = 0; i < needles.Length; i++)
|
||
{
|
||
if (hay.IndexOf(needles[i], System.StringComparison.Ordinal) >= 0)
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
}
|