276 lines
6.8 KiB
C#
276 lines
6.8 KiB
C#
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)
|
||
{
|
||
if (actor == null)
|
||
{
|
||
return "";
|
||
}
|
||
|
||
try
|
||
{
|
||
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();
|
||
return string.IsNullOrEmpty(n) ? "foe" : n;
|
||
}
|
||
}
|
||
|
||
return "target";
|
||
}
|
||
|
||
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))
|
||
{
|
||
return type.Replace("type_", "");
|
||
}
|
||
|
||
return string.IsNullOrEmpty(id) ? "building" : id;
|
||
}
|
||
|
||
if (actor.beh_tile_target != null)
|
||
{
|
||
return "tile";
|
||
}
|
||
}
|
||
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;
|
||
}
|
||
}
|