From 7b4ff19b01a035174955a3e5a64f59af08bee283 Mon Sep 17 00:00:00 2001 From: DazedAnon Date: Tue, 14 Jul 2026 19:18:57 -0500 Subject: [PATCH] Tooltip adjustments --- IdleSpectator/DossierAvatar.cs | 11 ++++ IdleSpectator/FocusTooltipPatches.cs | 77 ++++++++++++++++++++++++++++ IdleSpectator/WatchCaption.cs | 41 +++++++++++++-- IdleSpectator/mod.json | 4 +- 4 files changed, 127 insertions(+), 6 deletions(-) create mode 100644 IdleSpectator/FocusTooltipPatches.cs diff --git a/IdleSpectator/DossierAvatar.cs b/IdleSpectator/DossierAvatar.cs index 9a732a5..6877136 100644 --- a/IdleSpectator/DossierAvatar.cs +++ b/IdleSpectator/DossierAvatar.cs @@ -21,6 +21,17 @@ public static class DossierAvatar public static bool UsingVanilla => _usingVanilla; + /// True if is inside the live portrait rect. + public static bool ContainsScreenPoint(Vector2 screenPoint) + { + if (_host == null || !_host.gameObject.activeInHierarchy) + { + return false; + } + + return RectTransformUtility.RectangleContainsScreenPoint(_host, screenPoint, null); + } + public static void ResetHost() { _template = null; diff --git a/IdleSpectator/FocusTooltipPatches.cs b/IdleSpectator/FocusTooltipPatches.cs new file mode 100644 index 0000000..2e23000 --- /dev/null +++ b/IdleSpectator/FocusTooltipPatches.cs @@ -0,0 +1,77 @@ +using HarmonyLib; + +namespace IdleSpectator; + +/// +/// Idle pins so relationship arrows draw. +/// That same pin would always show the focus unit tip via +/// . +/// Allow tips for a real cursor unit or the dossier live sprite; suppress the pin tip on empty world / dossier chrome. +/// +[HarmonyPatch(typeof(CursorTooltipHelper), nameof(CursorTooltipHelper.updateGameplayTooltip))] +internal static class FocusTooltipPatches +{ + [HarmonyPrefix] + private static bool Prefix(ref bool __result, ref Actor __state) + { + __state = null; + if (!SpectatorMode.Active) + { + return true; + } + + // Live sprite: tip for the focused unit (last_actor is already pinned). + if (WatchCaption.IsPointerOverAvatar()) + { + return true; + } + + // Dossier chrome (not the sprite): no tip. + if (WatchCaption.IsPointerOverPanel()) + { + Tooltip.hideTooltip(); + __result = false; + return false; + } + + Actor near = GetActorNearCursorSafe(); + if (near != null && near.isAlive()) + { + // Real world hover: briefly use that unit for the tip, then restore the pin. + __state = UnitSelectionEffect.last_actor; + UnitSelectionEffect.setLastActor(near); + return true; + } + + // Empty world (grass, water, etc.): do not leak the idle focus tip. + Tooltip.hideTooltip(); + __result = false; + return false; + } + + [HarmonyPostfix] + private static void Postfix(Actor __state) + { + if (__state != null) + { + UnitSelectionEffect.setLastActor(__state); + } + } + + private static Actor GetActorNearCursorSafe() + { + try + { + if (World.world == null || ControllableUnit.isControllingUnit()) + { + return null; + } + + return World.world.getActorNearCursor(); + } + catch + { + return null; + } + } +} diff --git a/IdleSpectator/WatchCaption.cs b/IdleSpectator/WatchCaption.cs index 8a98ec6..c7abde5 100644 --- a/IdleSpectator/WatchCaption.cs +++ b/IdleSpectator/WatchCaption.cs @@ -19,6 +19,8 @@ public static class WatchCaption private const float HistoryIcon = 10f; private const float HistoryColMinW = 118f; private const float PadX = 4f; + /// Inset from the canvas top-right corner (grows left via pivot). + private const float ScreenInset = 12f; private const float PadTop = 4f; private const float HeaderH = 18f; private const float BodyH = 44f; @@ -133,6 +135,23 @@ public static class WatchCaption return pixelRect.width > 8f && pixelRect.height > 8f; } + /// True if the mouse is over the dossier panel (including the live sprite). + public static bool IsPointerOverPanel() + { + if (_rootRt == null || _root == null || !_root.activeInHierarchy) + { + return false; + } + + return RectTransformUtility.RectangleContainsScreenPoint(_rootRt, Input.mousePosition, null); + } + + /// True if the mouse is over the dossier live sprite only. + public static bool IsPointerOverAvatar() + { + return DossierAvatar.ContainsScreenPoint(Input.mousePosition); + } + public static void Clear() { _current = null; @@ -770,6 +789,7 @@ public static class WatchCaption float height = Mathf.Max(HeaderH + PadTop * 2f, y + PadTop - Gap); if (_rootRt != null) { + ApplyRootPlacement(); _rootRt.sizeDelta = new Vector2(_panelWidth, height); LastPanelSize = _rootRt.sizeDelta; } @@ -1159,11 +1179,8 @@ public static class WatchCaption _root.transform.SetParent(canvas.transform, false); _rootRt = _root.GetComponent(); - _rootRt.anchorMin = new Vector2(0f, 1f); - _rootRt.anchorMax = new Vector2(0f, 1f); - _rootRt.pivot = new Vector2(0f, 1f); + ApplyRootPlacement(); _rootRt.sizeDelta = new Vector2(LiveMax + PadX * 2f, HeaderH + PadTop * 2f); - _rootRt.anchoredPosition = new Vector2(12f, -12f); Image bg = _root.GetComponent(); bg.raycastTarget = false; @@ -1250,6 +1267,22 @@ public static class WatchCaption LogService.LogInfo("[IdleSpectator] Dossier HUD ready (nametag chips + mini history)"); } + /// + /// Pin the card to the canvas top-right. Pivot (1,1) so width growth expands left and stays on-screen. + /// + private static void ApplyRootPlacement() + { + if (_rootRt == null) + { + return; + } + + _rootRt.anchorMin = new Vector2(1f, 1f); + _rootRt.anchorMax = new Vector2(1f, 1f); + _rootRt.pivot = new Vector2(1f, 1f); + _rootRt.anchoredPosition = new Vector2(-ScreenInset, -ScreenInset); + } + private static void PlaceLine(RectTransform lineRt, float yFromTop, float height, float left) { if (lineRt == null) diff --git a/IdleSpectator/mod.json b/IdleSpectator/mod.json index 648131e..652042e 100644 --- a/IdleSpectator/mod.json +++ b/IdleSpectator/mod.json @@ -1,7 +1,7 @@ { "name": "IdleSpectator", "author": "dazed", - "version": "0.9.28", - "description": "AFK Idle Spectator (I) + Chronicle (F9). Dossier grows wide before history wraps.", + "version": "0.9.31", + "description": "AFK Idle Spectator (I) + Chronicle (F9). Dossier top-right; tip on world hover or live sprite.", "GUID": "com.dazed.idlespectator" }