worldbox-observer-mod/IdleSpectator/ActivityInterestTable.cs
2026-07-14 23:11:51 -05:00

276 lines
6.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 0100 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;
}
}