From 7068ac44c583198fa13d04e4eac85946cf58ec9d Mon Sep 17 00:00:00 2001 From: DazedAnon Date: Wed, 15 Jul 2026 13:37:55 -0500 Subject: [PATCH] dont dedupe important verbs --- IdleSpectator/ActivityLog.cs | 2 +- IdleSpectator/ActivityVerbMap.cs | 33 +++++++++++ IdleSpectator/AgentHarness.cs | 41 ++++++++++++-- IdleSpectator/HarnessScenarios.cs | 1 + IdleSpectator/WatchCaption.cs | 91 +++++++++++++++++++++++-------- IdleSpectator/mod.json | 2 +- 6 files changed, 140 insertions(+), 30 deletions(-) diff --git a/IdleSpectator/ActivityLog.cs b/IdleSpectator/ActivityLog.cs index 9e92441..f617f9e 100644 --- a/IdleSpectator/ActivityLog.cs +++ b/IdleSpectator/ActivityLog.cs @@ -849,7 +849,7 @@ public static class ActivityLog private static bool ShouldSuppressSameVerb(long subjectId, string key) { string verb = ResolveVerbKey(key); - if (string.IsNullOrEmpty(verb)) + if (string.IsNullOrEmpty(verb) || ActivityVerbMap.IsSameVerbCooldownExempt(verb)) { return false; } diff --git a/IdleSpectator/ActivityVerbMap.cs b/IdleSpectator/ActivityVerbMap.cs index f7d2e10..f45d941 100644 --- a/IdleSpectator/ActivityVerbMap.cs +++ b/IdleSpectator/ActivityVerbMap.cs @@ -128,6 +128,39 @@ public static class ActivityVerbMap return !string.IsNullOrEmpty(Resolve(key)); } + /// + /// High-signal verbs skip the ActivityLog same-verb cooldown (combat, danger, romance, etc.). + /// Spammy verbs like play/move/work still dedupe. + /// + public static bool IsSameVerbCooldownExempt(string verb) + { + if (string.IsNullOrEmpty(verb)) + { + return false; + } + + switch (verb) + { + case "fight": + case "hunt": + case "flee": + case "lover": + case "steal": + case "loot": + case "heal": + case "ignite": + case "extinguish": + case "fire": + case "harvest_life": + case "possessed": + case "confused": + case "strange_urge": + return true; + default: + return false; + } + } + public static List EnumerateLiveTaskIds() { return ActivityAssetCatalog.EnumerateLiveTaskIds(); diff --git a/IdleSpectator/AgentHarness.cs b/IdleSpectator/AgentHarness.cs index a678391..b0617d9 100644 --- a/IdleSpectator/AgentHarness.cs +++ b/IdleSpectator/AgentHarness.cs @@ -2006,15 +2006,20 @@ public static class AgentHarness } case "dossier_task_refresh": { - // Stale focus snapshot must recover: blank chip, then sync from live AI. + // Stale focus snapshot must recover: blank dossier TaskText, then sync from live AI. + // Empty live AI is ok - chip may keep a sticky prior label (never cleared on gaps). Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; string live = UnitDossier.ReadLiveTask(focus); string after = WatchCaption.HarnessBlankAndRefreshTask(); string chip = WatchCaption.ShownTaskChipText; - bool chipMatches = string.IsNullOrEmpty(live) - ? string.IsNullOrEmpty(chip) - : !string.IsNullOrEmpty(chip); - pass = after == live && chipMatches; + if (string.IsNullOrEmpty(live)) + { + pass = string.IsNullOrEmpty(after); + detail = $"live_empty dossier='{after}' sticky_chip='{chip}'"; + break; + } + + pass = after == live && !string.IsNullOrEmpty(chip); detail = $"live='{live}' dossier='{after}' chip='{chip}'"; break; } @@ -2292,6 +2297,32 @@ public static class AgentHarness $"added={added} want_added=1 suppressed={suppressed} want_suppressed>={keys.Length - 1} keys={keys.Length}"; break; } + case "activity_verb_exempt": + { + // Combat / high-signal verbs skip same-verb cooldown. + Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; + if (focus == null || !focus.isAlive()) + { + pass = false; + detail = "no_live_focus"; + break; + } + + ActivityLog.ResetSampleCounters(); + string[] keys = + { + "task_attack", + "punch_a_building", + "tantrum", + "task_unit_attack" + }; + int added = ActivityLog.HarnessSpamSameVerb(focus, keys); + int suppressed = ActivityLog.VerbSuppressedSinceClear; + pass = added == keys.Length && suppressed == 0; + detail = + $"added={added} want_added={keys.Length} suppressed={suppressed} want_suppressed=0"; + break; + } case "activity_prose_sentence": { // Ensure mood/task labels become sentences, not "{name} laughing". diff --git a/IdleSpectator/HarnessScenarios.cs b/IdleSpectator/HarnessScenarios.cs index 1d0a726..6aef175 100644 --- a/IdleSpectator/HarnessScenarios.cs +++ b/IdleSpectator/HarnessScenarios.cs @@ -614,6 +614,7 @@ internal static class HarnessScenarios Step("act13f", "assert", expect: "activity_taxonomy_audit"), Step("act13g", "assert", expect: "activity_status_audit"), Step("act13h", "assert", expect: "activity_verb_dedupe"), + Step("act13h2", "assert", expect: "activity_verb_exempt"), Step("act13i", "assert", expect: "dossier_task_refresh"), Step("act14", "activity_force", asset: "laughing", label: "Laughing", count: 1, value: "angle"), Step("act14b", "assert", expect: "activity_log_contains", value: "celestial"), diff --git a/IdleSpectator/WatchCaption.cs b/IdleSpectator/WatchCaption.cs index 4bd7397..2785c6a 100644 --- a/IdleSpectator/WatchCaption.cs +++ b/IdleSpectator/WatchCaption.cs @@ -16,6 +16,8 @@ public static class WatchCaption private const float SexSize = 14f; private const float LiveMax = 44f; private const float ChipIcon = 12f; + /// Fixed nametag task label slot so text swaps do not autofit-tick the header. + private const float TaskLabelW = 72f; private const float TraitIcon = 12f; private const float HistoryIcon = 10f; private const float HistoryColMinW = 118f; @@ -623,6 +625,7 @@ public static class WatchCaption /// /// Keep the nametag task chip in sync with live AI (focus snapshot can miss brief no-task gaps). + /// Never clears the chip on empty AI gaps - only replaces text when a new task is present. /// private static void RefreshLiveTask() { @@ -638,19 +641,30 @@ public static class WatchCaption } string live = UnitDossier.ReadLiveTask(actor); + // Hold the last shown label through brief no-task gaps so autofit does not collapse. + if (string.IsNullOrEmpty(live)) + { + return; + } + string prev = _current.TaskText ?? ""; if (live == prev) { return; } + bool wasShowing = _taskText != null && _taskText.gameObject.activeSelf; _current.TaskText = live; - ApplyTaskChip(live); + ReplaceTaskChip(live); + + // First appearance needs layout; later swaps only change text inside a fixed-width slot. + if (!wasShowing) + { + bool hasBody = _current.UnitId != 0; + bool hasReason = _reasonText != null && _reasonText.gameObject.activeSelf; + Relayout(hasBody, CountActiveTraitSlots(), hasTask: true, hasReason, CountActiveHistorySlots()); + } - bool hasBody = _current.UnitId != 0; - bool hasTask = !string.IsNullOrEmpty(live); - bool hasReason = _reasonText != null && _reasonText.gameObject.activeSelf; - Relayout(hasBody, CountActiveTraitSlots(), hasTask, hasReason, CountActiveHistorySlots()); BringHeaderFront(); } @@ -678,29 +692,61 @@ public static class WatchCaption return actor; } - private static void ApplyTaskChip(string taskText) + private static string TruncateTaskLabel(string taskText) { - bool hasTask = !string.IsNullOrEmpty(taskText); + if (string.IsNullOrEmpty(taskText)) + { + return ""; + } + + return taskText.Length > 14 ? taskText.Substring(0, 13) + "..." : taskText; + } + + /// Show or replace the task chip without ever blanking mid-update. + private static void ReplaceTaskChip(string taskText) + { + if (string.IsNullOrEmpty(taskText)) + { + return; + } + if (_taskIcon != null) { - HudIcons.Apply(_taskIcon, hasTask ? HudIcons.Task() : null); - _taskIcon.gameObject.SetActive(hasTask); + HudIcons.Apply(_taskIcon, HudIcons.Task()); + _taskIcon.gameObject.SetActive(true); } if (_taskText != null) { - string task = hasTask ? taskText : ""; - if (task.Length > 14) - { - task = task.Substring(0, 13) + "..."; - } - - _taskText.text = task; - _taskText.gameObject.SetActive(hasTask); + _taskText.text = TruncateTaskLabel(taskText); + _taskText.gameObject.SetActive(true); } } - /// Harness: blank the nametag task, then sync from live AI. + private static void ApplyTaskChip(string taskText) + { + if (!string.IsNullOrEmpty(taskText)) + { + ReplaceTaskChip(taskText); + return; + } + + if (_taskIcon != null) + { + HudIcons.Apply(_taskIcon, null); + _taskIcon.gameObject.SetActive(false); + } + + if (_taskText != null) + { + _taskText.text = ""; + _taskText.gameObject.SetActive(false); + } + } + + /// + /// Harness: simulate a stale empty snapshot, then sync from live AI (replace, never stay blank if live has a task). + /// public static string HarnessBlankAndRefreshTask() { if (_current != null) @@ -708,7 +754,7 @@ public static class WatchCaption _current.TaskText = ""; } - ApplyTaskChip(""); + // Do not hide the chip here - RefreshLiveTask must replace in place when live task exists. RefreshLiveTask(); return _current != null ? (_current.TaskText ?? "") : ""; } @@ -1465,9 +1511,8 @@ public static class WatchCaption _taskText.gameObject.SetActive(true); PlaceLeftChip(_taskIcon.rectTransform, x, yFromTop + 3f, ChipIcon, ChipIcon); x += ChipIcon + 1f; - float taskW = Mathf.Clamp(MeasureTextWidth(_taskText, 36f), 20f, 72f); - PlaceLeftChip(_taskText.rectTransform, x, yFromTop, taskW, HeaderH); - x += taskW + 4f; + PlaceLeftChip(_taskText.rectTransform, x, yFromTop, TaskLabelW, HeaderH); + x += TaskLabelW + 4f; } else { @@ -1521,7 +1566,7 @@ public static class WatchCaption if (hasTask && _taskIcon != null && _taskText != null) { x += ChipIcon + 1f; - x += Mathf.Clamp(MeasureTextWidth(_taskText, 36f), 20f, 72f) + 4f; + x += TaskLabelW + 4f; } if (_sexIcon != null && _sexIcon.gameObject.activeSelf) diff --git a/IdleSpectator/mod.json b/IdleSpectator/mod.json index b274dd8..9ad5bc8 100644 --- a/IdleSpectator/mod.json +++ b/IdleSpectator/mod.json @@ -1,7 +1,7 @@ { "name": "IdleSpectator", "author": "dazed", - "version": "0.12.100", + "version": "0.12.103", "description": "AFK Idle Spectator (I) + Lore (L). Detailed living activity prose by default.", "GUID": "com.dazed.idlespectator" }