diff --git a/IdleSpectator/AgentHarness.cs b/IdleSpectator/AgentHarness.cs
index 9a611d9..78d3373 100644
--- a/IdleSpectator/AgentHarness.cs
+++ b/IdleSpectator/AgentHarness.cs
@@ -1996,6 +1996,23 @@ public static class AgentHarness
DoInterestForceSession(cmd);
break;
+ case "interest_combat_session":
+ DoInterestCombatSession(cmd);
+ break;
+
+ case "combat_maintain_focus":
+ {
+ InterestDirector.HarnessMaintainCombatFocus();
+ string tip = CameraDirector.LastWatchLabel ?? "";
+ string focus = FocusLabel();
+ _cmdOk++;
+ Emit(
+ cmd,
+ ok: true,
+ detail: $"key={InterestDirector.CurrentKey} tip='{tip}' focus={focus} completion={InterestDirector.CurrentCandidate?.Completion}");
+ break;
+ }
+
case "interest_variety_clear":
InterestVariety.Clear();
_cmdOk++;
@@ -3475,6 +3492,100 @@ public static class AgentHarness
Emit(cmd, ok, detail: $"key={InterestDirector.CurrentKey} score={InterestDirector.CurrentScoreLabel} active={InterestDirector.CurrentIsActive}");
}
+ ///
+ /// Force a CombatActive session with follow + related so handoff/tip rewrite can be asserted.
+ /// asset = follow, value = related asset (optional), label overrides Fight prose.
+ ///
+ private static void DoInterestCombatSession(HarnessCommand cmd)
+ {
+ if (!WorldReady())
+ {
+ _cmdFail++;
+ Emit(cmd, ok: false, detail: "world_not_ready");
+ return;
+ }
+
+ Actor follow = ResolveUnit(string.IsNullOrEmpty(cmd.asset) || cmd.asset == "auto" ? null : cmd.asset);
+ if (follow == null)
+ {
+ follow = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
+ }
+
+ if (follow == null)
+ {
+ follow = WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f);
+ }
+
+ Actor related = null;
+ if (!string.IsNullOrEmpty(cmd.value) && cmd.value != "auto")
+ {
+ related = ResolveUnit(cmd.value);
+ }
+
+ if (related == null || related == follow)
+ {
+ foreach (Actor actor in WorldActivityScanner.EnumerateAliveUnitsPublic())
+ {
+ if (actor != null && actor != follow && actor.isAlive())
+ {
+ related = actor;
+ break;
+ }
+ }
+ }
+
+ if (follow == null || !follow.isAlive())
+ {
+ _cmdFail++;
+ Emit(cmd, ok: false, detail: "no follow unit for interest_combat_session");
+ return;
+ }
+
+ string label = string.IsNullOrEmpty(cmd.label)
+ ? EventReason.Fight(follow, related)
+ : cmd.label.Replace("{a}", SafeName(follow)).Replace("{b}", SafeName(related));
+ string keySuffix = string.IsNullOrEmpty(cmd.expect) ? "combat_focus" : cmd.expect;
+ InterestCandidate c = InterestFeeds.RegisterHarness(
+ follow,
+ label,
+ keySuffix,
+ lead: InterestLeadKind.EventLed,
+ completion: InterestCompletionKind.CombatActive,
+ forceActive: true,
+ eventStrength: 120f,
+ characterSignificance: 20f,
+ maxWatch: 60f,
+ ttlSeconds: 60f,
+ related: related,
+ resumable: true);
+ if (c != null)
+ {
+ c.Category = "Combat";
+ c.Verb = "fighting";
+ if (related != null)
+ {
+ c.ParticipantCount = 2;
+ }
+
+ InterestScoring.ScoreCheap(c);
+ }
+
+ bool ok = InterestDirector.HarnessForceSession(c);
+ if (ok)
+ {
+ _cmdOk++;
+ }
+ else
+ {
+ _cmdFail++;
+ }
+
+ Emit(
+ cmd,
+ ok,
+ detail: $"key={InterestDirector.CurrentKey} follow={SafeName(follow)} related={SafeName(related)} tip='{CameraDirector.LastWatchLabel}'");
+ }
+
private static void DoSetSetting(HarnessCommand cmd)
{
string key = cmd.expect;
@@ -3581,6 +3692,41 @@ public static class AgentHarness
$"tipAsset={tipAsset} unitAsset={unitAsset} tip={tip}";
break;
}
+ case "tip_matches_focus":
+ {
+ Actor unit = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
+ string tip = CameraDirector.LastWatchLabel ?? "";
+ string name = SafeName(unit);
+ if (string.IsNullOrEmpty(tip))
+ {
+ // Cleared fight tip during combat cold is OK.
+ pass = true;
+ detail = "tip_empty focus=" + name;
+ }
+ else if (string.IsNullOrEmpty(name))
+ {
+ pass = false;
+ detail = "no_focus tip='" + tip + "'";
+ }
+ else
+ {
+ pass = tip.StartsWith(name, StringComparison.OrdinalIgnoreCase)
+ || tip.IndexOf(name, StringComparison.OrdinalIgnoreCase) == 0;
+ // Also accept Battle-style where name leads.
+ detail = $"tip='{tip}' focus='{name}' pass={pass}";
+ }
+
+ break;
+ }
+ case "tip_not_contains":
+ {
+ string needle = string.IsNullOrEmpty(cmd.value) ? cmd.label : cmd.value;
+ string tip = CameraDirector.LastWatchLabel ?? "";
+ pass = string.IsNullOrEmpty(needle)
+ || tip.IndexOf(needle, StringComparison.OrdinalIgnoreCase) < 0;
+ detail = $"tip='{tip}' not_contains='{needle}'";
+ break;
+ }
case "unit_asset":
{
string wantAsset = string.IsNullOrEmpty(cmd.asset) ? cmd.value : cmd.asset;
diff --git a/IdleSpectator/Events/Feeds/InterestFeeds.cs b/IdleSpectator/Events/Feeds/InterestFeeds.cs
index 2c703dd..d2b41e6 100644
--- a/IdleSpectator/Events/Feeds/InterestFeeds.cs
+++ b/IdleSpectator/Events/Feeds/InterestFeeds.cs
@@ -338,7 +338,6 @@ public static class InterestFeeds
return;
}
- long id = SafeId(actor);
Actor foe = null;
try
{
@@ -353,7 +352,9 @@ public static class InterestFeeds
}
string verb = taskOrBeat ?? "fighting";
- string key = EventCatalog.Combat.Key(id);
+ WorldActivityScanner.PreferCombatFollow(actor, foe, out Actor follow, out Actor related);
+ long followId = SafeId(follow ?? actor);
+ string key = EventCatalog.Combat.Key(followId);
var candidate = new InterestCandidate
{
Key = key,
@@ -362,17 +363,17 @@ public static class InterestFeeds
Source = "activity",
EventStrength = EventCatalog.Combat.ActivityStrength,
VisualConfidence = 0.8f,
- Position = actor.current_position,
- FollowUnit = actor,
- RelatedUnit = foe,
- SubjectId = id,
- RelatedId = foe != null ? SafeId(foe) : 0,
- Label = EventReason.Fight(actor, foe),
- AssetId = actor.asset != null ? actor.asset.id : "",
- SpeciesId = actor.asset != null ? actor.asset.id : "",
+ Position = follow != null ? follow.current_position : actor.current_position,
+ FollowUnit = follow ?? actor,
+ RelatedUnit = related,
+ SubjectId = followId,
+ RelatedId = related != null ? SafeId(related) : 0,
+ Label = EventReason.Fight(follow ?? actor, related),
+ AssetId = (follow ?? actor).asset != null ? (follow ?? actor).asset.id : "",
+ SpeciesId = (follow ?? actor).asset != null ? (follow ?? actor).asset.id : "",
Verb = verb,
- CityKey = actor.city != null ? (actor.city.name ?? "") : "",
- KingdomKey = actor.kingdom != null ? (actor.kingdom.name ?? "") : "",
+ CityKey = (follow ?? actor).city != null ? ((follow ?? actor).city.name ?? "") : "",
+ KingdomKey = (follow ?? actor).kingdom != null ? ((follow ?? actor).kingdom.name ?? "") : "",
CreatedAt = Time.unscaledTime,
LastSeenAt = Time.unscaledTime,
ExpiresAt = Time.unscaledTime + EventCatalog.Combat.ActivityTtl,
@@ -380,7 +381,15 @@ public static class InterestFeeds
MaxWatch = EventCatalog.Combat.MaxWatch,
Completion = InterestCompletionKind.CombatActive
};
- candidate.ParticipantIds.Add(id);
+ candidate.ParticipantIds.Add(followId);
+ if (related != null)
+ {
+ long rid = SafeId(related);
+ if (rid != 0)
+ {
+ candidate.ParticipantIds.Add(rid);
+ }
+ }
InterestScoring.ScoreCheap(candidate);
EventFeedUtil.RegisterCandidate(candidate);
}
diff --git a/IdleSpectator/HarnessScenarios.cs b/IdleSpectator/HarnessScenarios.cs
index 66bf0f6..25c64ad 100644
--- a/IdleSpectator/HarnessScenarios.cs
+++ b/IdleSpectator/HarnessScenarios.cs
@@ -50,6 +50,9 @@ internal static class HarnessScenarios
case "status_overlap_camera":
case "status_overlap":
return StatusOverlapCamera();
+ case "combat_focus":
+ case "combat_tip_align":
+ return CombatFocusAlign();
case "director_action_priority":
case "action_priority":
return DirectorActionPriority();
@@ -1029,6 +1032,48 @@ internal static class HarnessScenarios
Step("cp90", "fast_timing", value: "false"),
Step("cp99", "snapshot"),
Nested("cp_status_overlap", "status_overlap_camera"),
+ Nested("cp_combat_focus", "combat_focus"),
+ };
+ }
+
+ ///
+ /// Combat tip subject must match focus; death handoff rewrites tip; cold combat clears fight tip.
+ ///
+ private static List CombatFocusAlign()
+ {
+ return new List
+ {
+ Step("cf0", "dismiss_windows"),
+ Step("cf1", "wait_world"),
+ Step("cf2", "set_setting", expect: "enabled", value: "true"),
+ Step("cf3", "fast_timing", value: "true"),
+ Step("cf4", "spawn", asset: "human", count: 1),
+ Step("cf5", "spawn", asset: "wolf", count: 1),
+ Step("cf6", "spectator", value: "off"),
+ Step("cf7", "spectator", value: "on"),
+ Step("cf8", "pick_unit", asset: "human"),
+ Step("cf9", "focus", asset: "human"),
+ Step("cf10", "interest_end_session"),
+
+ // Combat scene: human fighting wolf. Clear follow → related (wolf) must own tip+focus.
+ Step("cf20", "interest_combat_session", asset: "human", value: "wolf", expect: "cf_fight"),
+ Step("cf21", "assert", expect: "tip_contains", value: "fighting"),
+ Step("cf22", "interest_clear_follow", asset: "wolf"),
+ Step("cf23", "combat_maintain_focus"),
+ Step("cf24", "assert", expect: "has_focus", value: "true"),
+ Step("cf25", "assert", expect: "tip_matches_focus"),
+ Step("cf26", "assert", expect: "unit_asset", asset: "wolf"),
+
+ // Cold combat must not keep "is fighting".
+ Step("cf30", "interest_combat_session", asset: "human", value: "wolf", expect: "cf_cold"),
+ Step("cf31", "assert", expect: "tip_contains", value: "fighting"),
+ Step("cf32", "interest_release_force"),
+ Step("cf33", "interest_mark_inactive", value: "1"),
+ Step("cf34", "combat_maintain_focus"),
+ Step("cf35", "assert", expect: "tip_not_contains", value: "fighting"),
+
+ Step("cf90", "fast_timing", value: "false"),
+ Step("cf99", "snapshot"),
};
}
diff --git a/IdleSpectator/InterestDirector.cs b/IdleSpectator/InterestDirector.cs
index fcbe9cb..29610db 100644
--- a/IdleSpectator/InterestDirector.cs
+++ b/IdleSpectator/InterestDirector.cs
@@ -44,6 +44,8 @@ public static class InterestDirector
private static float _lastFeedsAt = -999f;
private static float _enabledAt = -999f;
private static float _inactiveSince = -999f;
+ private static float _lastCombatFocusAt = -999f;
+ private const float CombatFocusThrottleSeconds = 0.4f;
private static readonly List PendingScratch = new List(96);
public static string CurrentTierName => CurrentScoreLabel;
@@ -357,6 +359,7 @@ public static class InterestDirector
float sinceSwitch = now - _lastSwitchAt;
UpdateSessionLiveness(now, onCurrent);
+ MaintainCombatFocus(now, force: false);
InterestCandidate next = SelectNext(now);
if (next != null && CanSwitchTo(next, onCurrent, sinceSwitch))
@@ -466,6 +469,18 @@ public static class InterestDirector
return;
}
+ if (_current.Completion == InterestCompletionKind.CombatActive)
+ {
+ MaintainCombatFocus(Time.unscaledTime, force: true);
+ if (!_current.HasFollowUnit
+ && (_current.RelatedUnit == null || !_current.RelatedUnit.isAlive()))
+ {
+ EndCurrent(Time.unscaledTime, reason: "follow_lost");
+ }
+
+ return;
+ }
+
if (_current.HasFollowUnit)
{
// Living subject still owned - reattach if vanilla cleared focus mid-scene.
@@ -490,6 +505,92 @@ public static class InterestDirector
EndCurrent(Time.unscaledTime, reason: "follow_lost");
}
+ ///
+ /// Combat scenes: keep camera on the highest-scored living participant and rewrite the tip
+ /// so the subject always matches focus. Clears fight Labels once combat goes cold.
+ ///
+ private static void MaintainCombatFocus(float now, bool force)
+ {
+ if (_current == null || _current.Completion != InterestCompletionKind.CombatActive)
+ {
+ return;
+ }
+
+ bool combatHot = InterestCompletion.IsActive(_current, _currentStartedAt, now);
+ if (!combatHot)
+ {
+ ClearStaleCombatLabel(now);
+ return;
+ }
+
+ if (!force && now - _lastCombatFocusAt < CombatFocusThrottleSeconds)
+ {
+ return;
+ }
+
+ _lastCombatFocusAt = now;
+ if (!WorldActivityScanner.TryPickBestCombatFocus(
+ _current, out Actor best, out Actor foe, out int fighters))
+ {
+ return;
+ }
+
+ bool mass = string.Equals(_current.AssetId, "live_battle", StringComparison.OrdinalIgnoreCase)
+ || fighters > 2
+ || _current.ParticipantCount > 2;
+ string label = mass
+ ? EventReason.Battle(best, fighters)
+ : EventReason.Fight(best, foe);
+
+ bool followChanged = _current.FollowUnit != best;
+ bool labelChanged = !string.Equals(_current.Label ?? "", label ?? "", StringComparison.Ordinal);
+ _current.FollowUnit = best;
+ _current.SubjectId = EventFeedUtil.SafeId(best);
+ _current.RelatedUnit = foe;
+ _current.RelatedId = foe != null ? EventFeedUtil.SafeId(foe) : 0;
+ _current.Label = label;
+ try
+ {
+ _current.Position = best.current_position;
+ }
+ catch
+ {
+ // keep prior position
+ }
+
+ if (followChanged
+ || labelChanged
+ || !HasLivingCameraFocus()
+ || MoveCamera._focus_unit != best)
+ {
+ CameraDirector.Watch(_current.ToInterestEvent());
+ }
+ }
+
+ private static void ClearStaleCombatLabel(float now)
+ {
+ if (_current == null || string.IsNullOrEmpty(_current.Label))
+ {
+ return;
+ }
+
+ if (_current.Label.IndexOf("fighting", StringComparison.OrdinalIgnoreCase) < 0
+ && _current.Label.IndexOf("fight of", StringComparison.OrdinalIgnoreCase) < 0)
+ {
+ return;
+ }
+
+ _current.Label = "";
+ _lastCombatFocusAt = now;
+ CameraDirector.Watch(_current.ToInterestEvent());
+ }
+
+ /// Harness: force one combat focus maintenance pass.
+ public static void HarnessMaintainCombatFocus()
+ {
+ MaintainCombatFocus(Time.unscaledTime, force: true);
+ }
+
///
/// Keep a living focus unit whenever Idle Spectator is on.
/// Covers death mid-combat and quiet_grace windows where vanilla cleared follow.
@@ -508,6 +609,15 @@ public static class InterestDirector
if (_current != null)
{
+ if (_current.Completion == InterestCompletionKind.CombatActive)
+ {
+ MaintainCombatFocus(now, force: true);
+ if (HasLivingCameraFocus())
+ {
+ return;
+ }
+ }
+
if (_current.HasFollowUnit)
{
CameraDirector.Watch(_current.ToInterestEvent());
@@ -519,12 +629,16 @@ public static class InterestDirector
if (_current.RelatedUnit != null && _current.RelatedUnit.isAlive())
{
- _current.FollowUnit = _current.RelatedUnit;
- _current.SubjectId = EventFeedUtil.SafeId(_current.RelatedUnit);
- CameraDirector.Watch(_current.ToInterestEvent());
- if (HasLivingCameraFocus())
+ // Non-combat ownership handoff; combat already tried picker above.
+ if (_current.Completion != InterestCompletionKind.CombatActive)
{
- return;
+ _current.FollowUnit = _current.RelatedUnit;
+ _current.SubjectId = EventFeedUtil.SafeId(_current.RelatedUnit);
+ CameraDirector.Watch(_current.ToInterestEvent());
+ if (HasLivingCameraFocus())
+ {
+ return;
+ }
}
}
diff --git a/IdleSpectator/WorldActivityScanner.cs b/IdleSpectator/WorldActivityScanner.cs
index 9a37321..fc765eb 100644
--- a/IdleSpectator/WorldActivityScanner.cs
+++ b/IdleSpectator/WorldActivityScanner.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using NeoModLoader.services;
using UnityEngine;
@@ -123,7 +124,8 @@ public static class WorldActivityScanner
CountFightCluster(actor.current_position, 10f, out int fighters, out int notables, out _);
fighters = Mathf.Max(1, fighters);
- string label = EventReason.Fight(actor, foe);
+ PreferCombatFollow(actor, foe, out Actor follow, out Actor related);
+ string label = EventReason.Fight(follow, related);
var candidate = new InterestCandidate
{
Key = EventCatalog.Combat.Key(id),
@@ -133,17 +135,17 @@ public static class WorldActivityScanner
EventStrength = Mathf.Max(actionScore, EventCatalog.Combat.ScannerStrengthFloor),
CharacterSignificance = charScore,
VisualConfidence = 0.7f,
- Position = actor.current_position,
- FollowUnit = actor,
- RelatedUnit = foe,
- SubjectId = id,
- RelatedId = foe != null ? EventFeedUtil.SafeId(foe) : 0,
+ Position = follow != null ? follow.current_position : actor.current_position,
+ FollowUnit = follow,
+ RelatedUnit = related,
+ SubjectId = follow != null ? EventFeedUtil.SafeId(follow) : id,
+ RelatedId = related != null ? EventFeedUtil.SafeId(related) : 0,
Label = label,
Verb = "fighting",
- AssetId = actor.asset != null ? actor.asset.id : "scored_unit",
- SpeciesId = actor.asset != null ? actor.asset.id : "",
- CityKey = actor.city != null ? (actor.city.name ?? "") : "",
- KingdomKey = actor.kingdom != null ? (actor.kingdom.name ?? "") : "",
+ AssetId = follow?.asset != null ? follow.asset.id : (actor.asset != null ? actor.asset.id : "scored_unit"),
+ SpeciesId = follow?.asset != null ? follow.asset.id : (actor.asset != null ? actor.asset.id : ""),
+ CityKey = follow?.city != null ? (follow.city.name ?? "") : (actor.city != null ? (actor.city.name ?? "") : ""),
+ KingdomKey = follow?.kingdom != null ? (follow.kingdom.name ?? "") : (actor.kingdom != null ? (actor.kingdom.name ?? "") : ""),
ParticipantCount = fighters,
NotableParticipantCount = notables,
CreatedAt = Time.unscaledTime,
@@ -318,6 +320,160 @@ public static class WorldActivityScanner
return best;
}
+ ///
+ /// Character weight for picking who to watch in a fight (notable / renown / role).
+ /// Matches bestFollow ranking.
+ ///
+ public static float CombatFocusWeight(Actor actor)
+ {
+ if (actor == null || !actor.isAlive())
+ {
+ return -1f;
+ }
+
+ SplitActorScore(actor, null, out _, out float charScore);
+ float weight = charScore;
+ if (InterestScoring.IsExtremelyNotable(actor))
+ {
+ weight += 30f;
+ }
+
+ return weight;
+ }
+
+ ///
+ /// Prefer the higher-scored of two combatants as follow; the other becomes related.
+ ///
+ public static void PreferCombatFollow(Actor a, Actor b, out Actor follow, out Actor related)
+ {
+ follow = a;
+ related = b;
+ if (a == null || !a.isAlive())
+ {
+ follow = b != null && b.isAlive() ? b : null;
+ related = null;
+ return;
+ }
+
+ if (b == null || !b.isAlive())
+ {
+ related = null;
+ return;
+ }
+
+ if (CombatFocusWeight(b) > CombatFocusWeight(a))
+ {
+ follow = b;
+ related = a;
+ }
+ }
+
+ ///
+ /// Highest-scored living participant for an active combat scene.
+ /// Considers Follow, Related, and (for mass fights) the nearby fight cluster.
+ ///
+ public static bool TryPickBestCombatFocus(
+ InterestCandidate scene,
+ out Actor best,
+ out Actor foe,
+ out int fighters)
+ {
+ best = null;
+ foe = null;
+ fighters = 1;
+ if (scene == null)
+ {
+ return false;
+ }
+
+ fighters = Mathf.Max(1, scene.ParticipantCount);
+ float bestWeight = -1f;
+ Actor bestLocal = null;
+
+ void Consider(Actor unit)
+ {
+ if (unit == null || !unit.isAlive())
+ {
+ return;
+ }
+
+ float w = CombatFocusWeight(unit);
+ if (w > bestWeight)
+ {
+ bestWeight = w;
+ bestLocal = unit;
+ }
+ }
+
+ Consider(scene.FollowUnit);
+ Consider(scene.RelatedUnit);
+
+ bool mass = string.Equals(scene.AssetId, "live_battle", StringComparison.OrdinalIgnoreCase)
+ || scene.ParticipantCount > 2;
+ Vector3 pos = scene.Position;
+ if (bestLocal != null)
+ {
+ pos = bestLocal.current_position;
+ }
+ else if (scene.FollowUnit != null)
+ {
+ pos = scene.FollowUnit.current_position;
+ }
+ else if (scene.RelatedUnit != null)
+ {
+ pos = scene.RelatedUnit.current_position;
+ }
+
+ if (mass || bestLocal == null)
+ {
+ float radius = mass ? 14f : 10f;
+ CountFightCluster(pos, radius, out int clusterFighters, out _, out Actor clusterBest);
+ fighters = Mathf.Max(fighters, clusterFighters);
+ Consider(clusterBest);
+ }
+
+ best = bestLocal;
+ if (best == null)
+ {
+ return false;
+ }
+
+ try
+ {
+ if (best.has_attack_target
+ && best.attack_target != null
+ && best.attack_target.isAlive()
+ && best.attack_target.isActor())
+ {
+ foe = best.attack_target.a;
+ if (foe != null && !foe.isAlive())
+ {
+ foe = null;
+ }
+ }
+ }
+ catch
+ {
+ foe = null;
+ }
+
+ if (foe == null)
+ {
+ Actor related = scene.RelatedUnit;
+ Actor follow = scene.FollowUnit;
+ if (related != null && related.isAlive() && related != best)
+ {
+ foe = related;
+ }
+ else if (follow != null && follow.isAlive() && follow != best)
+ {
+ foe = follow;
+ }
+ }
+
+ return true;
+ }
+
private static float ScoreFightCluster(int deaths, int fighters, int notables)
{
ScoringWeights w = InterestScoringConfig.W;
diff --git a/IdleSpectator/mod.json b/IdleSpectator/mod.json
index 5b5519d..a5a6f27 100644
--- a/IdleSpectator/mod.json
+++ b/IdleSpectator/mod.json
@@ -1,7 +1,7 @@
{
"name": "IdleSpectator",
"author": "dazed",
- "version": "0.25.50",
- "description": "AFK Idle Spectator (I) + Lore (L). Status-overlap camera hold gate.",
+ "version": "0.25.51",
+ "description": "AFK Idle Spectator (I) + Lore (L). Combat focus + tip alignment.",
"GUID": "com.dazed.idlespectator"
}
diff --git a/docs/event-audit.md b/docs/event-audit.md
index 0c5b282..5e8ca69 100644
--- a/docs/event-audit.md
+++ b/docs/event-audit.md
@@ -76,7 +76,11 @@ See [world-event-inventory.md](world-event-inventory.md) for live library counts
`mutation_discovery` refreshes `.harness/mutation_gaps.tsv`.
Open rows are unexplained Wire candidates only.
-## Idle quality (next)
+## Idle quality
-Fire accuracy is covered by [event-live-verification-plan.md](event-live-verification-plan.md).
-Idle watch quality (presentability gate, ambient fall demotion, focus hold, tip placeholder) is tracked in [camera-presentability-plan.md](camera-presentability-plan.md).
+Presentability gate, ambient fall demotion, focus hold, and tip placeholder bans are done
+(see [camera-presentability-plan.md](camera-presentability-plan.md)).
+
+**Polish next:** combat tip/focus alignment - camera follows the highest-scored living
+participant and rewrites the Watching tip so the subject matches focus; clears "is fighting"
+once combat goes cold.