using UnityEngine;
namespace IdleSpectator;
/// How interesting a unit's current AI task/job is for idle scoring.
public enum ActivityBand
{
Cold = 0,
Warm = 1,
Hot = 2
}
///
/// 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.
///
public static class ActivityInterestTable
{
public const float WarmScoreFloor = 18f;
public const float HotScoreFloor = 40f;
/// Activity score 0–100 used by idle scoring.
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;
}
}