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