using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Interface; using Dalamud.Interface.Utility; using HSUI.Config; using HSUI.Config.Attributes; using HSUI.Interface.GeneralElements; using Dalamud.Bindings.ImGui; using FFXIVClientStructs.FFXIV.Client.Game.Object; using System; using System.Collections.Generic; using System.Linq; using System.Numerics; using System.Text; using System.Text.RegularExpressions; namespace HSUI.Helpers { /// Colored segment for game-style tooltip formatting. public readonly struct TooltipSegment { public readonly string Text; public readonly Vector4 Color; public TooltipSegment(string text, Vector4 color) { Text = text ?? ""; Color = color; } } /// When showing an ID in the tooltip title, use Action for action IDs or Status for status effect IDs. public enum TooltipIdKind { None = 0, Action = 1, Status = 2, } public class TooltipsHelper : IDisposable { #region Singleton private TooltipsHelper() { } public static void Initialize() { Instance = new TooltipsHelper(); } public static TooltipsHelper Instance { get; private set; } = null!; ~TooltipsHelper() { Dispose(false); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected void Dispose(bool disposing) { if (!disposing) { return; } Instance = null!; } #endregion private float MaxWidth => Math.Max(160, _config.TooltipMaxWidth * ImGuiHelpers.GlobalScale * _config.TooltipScale); private float Margin => Math.Max(4, 5 * ImGuiHelpers.GlobalScale * _config.TooltipScale); private TooltipsConfig _config => ConfigurationManager.Instance.GetConfigObject(); private string? _currentTooltipText = null; private Vector2 _textSize; private string? _currentTooltipTitle = null; private Vector2 _titleSize; private string? _previousRawText = null; private uint? _currentIconId = null; private List? _formattedSegments = null; private Vector2 _position; private Vector2 _size; private bool _dataIsValid = false; private const float IconSize = 24f; public void ShowTooltipOnCursor(string text, string? title = null, uint id = 0, string name = "", uint? iconId = null, TooltipIdKind idKind = TooltipIdKind.None, List? formattedText = null) { ShowTooltip(text, ImGui.GetMousePos(), title, id, name, iconId, idKind, formattedText); } public void ShowTooltip(string text, Vector2 position, string? title = null, uint id = 0, string name = "", uint? iconId = null, TooltipIdKind idKind = TooltipIdKind.None, List? formattedText = null) { if (text == null) { if (_config.DebugTooltips) Plugin.Logger.Information("[HSUI Tooltip DBG] ShowTooltip skipped: text is null"); return; } // remove styling tags from text if (_previousRawText != text) { _currentTooltipText = text; _previousRawText = text; } _currentIconId = iconId; _formattedSegments = formattedText; // calcualte title size _titleSize = Vector2.Zero; if (title != null) { _currentTooltipTitle = title; if (_config.ShowSourceName && name.Length > 0) { _currentTooltipTitle += $" ({name})"; } bool showId = id != 0 && ( (idKind == TooltipIdKind.Action && _config.ShowActionIDs) || (idKind == TooltipIdKind.Status && _config.ShowStatusIDs) || (idKind == TooltipIdKind.None && _config.ShowStatusIDs)); if (showId) { _currentTooltipTitle += " (ID: " + id + ")"; } string fontId = _config.FontID ?? FontsConfig.DefaultSmallFontKey; using (FontsManager.Instance.PushFont(fontId)) { _titleSize = ImGui.CalcTextSize(_currentTooltipTitle, false, MaxWidth); _titleSize.Y += Margin; } } // calculate text size (build combined text from segments for sizing) string fontIdForText = _config.FontID ?? FontsConfig.DefaultSmallFontKey; using (FontsManager.Instance.PushFont(fontIdForText)) { float lineHeight = ImGui.GetTextLineHeight(); if (_formattedSegments != null && _formattedSegments.Count > 0) { var sb = new StringBuilder(); int newlineCount = 0; foreach (var seg in _formattedSegments) { sb.Append(seg.Text); if (seg.Text == "\n") newlineCount++; } _textSize = ImGui.CalcTextSize(sb.ToString(), false, MaxWidth); // Account for Dummy spacing between sections (each \n adds compactSpacing) float compactSpacing = Math.Max(2, lineHeight * _config.TooltipLineSpacing); _textSize.Y += newlineCount * compactSpacing; } else { _textSize = ImGui.CalcTextSize(_currentTooltipText, false, MaxWidth); } } float fontScale = Math.Max(0.7f, _config.TooltipFontScale); float contentWidth = Math.Max(_titleSize.X, _textSize.X) * fontScale; float contentHeight = (_titleSize.Y + _textSize.Y) * fontScale; float iconSizeScaled = IconSize * ImGuiHelpers.GlobalScale * _config.TooltipScale * _config.TooltipIconScale; float iconGap = _currentIconId.HasValue && _currentIconId.Value > 0 ? iconSizeScaled + Margin : 0; // Add buffer to prevent clipping (font/scale changes, rounding, padding) float heightBuffer = _config.TooltipHeightBuffer; float minContentHeight = contentHeight + heightBuffer; _size = new Vector2( contentWidth + iconGap + Margin * 2, Math.Max(minContentHeight, _currentIconId.HasValue && _currentIconId.Value > 0 ? iconSizeScaled + Margin * 2 : 0) + Margin * 2); // position tooltip using the given coordinates as bottom center position.X = position.X - _size.X / 2f; position.Y = position.Y - _size.Y; // correct tooltips off screen _position = ConstrainPosition(position, _size); _dataIsValid = true; if (_config.DebugTooltips) Plugin.Logger.Information($"[HSUI Tooltip DBG] ShowTooltip: title='{title}' textLen={text?.Length ?? 0} textPreview='{(text != null && text.Length > 80 ? text[..80] + "..." : text ?? "")}' pos=({_position.X:F0},{_position.Y:F0})"); } private WorldObjectTooltipConfig? _worldTooltipConfig; /// /// Shows a tooltip for the game object the mouse is hovering over in the 3D world. /// Call this each frame before Draw(). /// public void ShowWorldObjectTooltip() { try { _worldTooltipConfig ??= ConfigurationManager.Instance.GetConfigObject(); if (_worldTooltipConfig == null || !_worldTooltipConfig.Enabled) return; } catch (Exception ex) { // Config not ready yet or not found - silently skip if (_config.DebugTooltips) Plugin.Logger.Warning($"[HSUI Tooltip] WorldObjectTooltipConfig not available: {ex.Message}"); return; } IGameObject? mouseOverTarget = Plugin.TargetManager.MouseOverTarget; if (mouseOverTarget == null) return; // Don't show tooltip for ourselves if (mouseOverTarget.GameObjectId == Plugin.ObjectTable.LocalPlayer?.GameObjectId) return; string name = mouseOverTarget.Name.ToString(); if (string.IsNullOrEmpty(name)) name = "Unknown"; var sb = new StringBuilder(); // Free Company tag for players if (_worldTooltipConfig.ShowTitle && mouseOverTarget is IPlayerCharacter player) { string fcTag = player.CompanyTag.ToString(); if (!string.IsNullOrEmpty(fcTag)) sb.AppendLine($"«{fcTag}»"); } // Level if (_worldTooltipConfig.ShowLevel && mouseOverTarget is ICharacter charLevel) { byte level = charLevel.Level; if (level > 0) sb.AppendLine($"Level {level}"); } // Job (for players) if (_worldTooltipConfig.ShowJob && mouseOverTarget is IPlayerCharacter playerJob) { uint jobId = playerJob.ClassJob.RowId; if (jobId > 0 && JobsHelper.JobNames.TryGetValue(jobId, out string? jobName)) sb.AppendLine($"Job: {jobName}"); } // HP if (_worldTooltipConfig.ShowHP && mouseOverTarget is ICharacter charHp) { uint currentHp = charHp.CurrentHp; uint maxHp = charHp.MaxHp; if (maxHp > 0) { float pct = (float)currentHp / maxHp * 100f; sb.AppendLine($"HP: {currentHp:N0} / {maxHp:N0} ({pct:F0}%)"); } } // Distance if (_worldTooltipConfig.ShowDistance) { var localPlayer = Plugin.ObjectTable.LocalPlayer; if (localPlayer != null) { float dist = Vector3.Distance(localPlayer.Position, mouseOverTarget.Position); sb.AppendLine($"Distance: {dist:F1}y"); } } // Object ID (debug) if (_worldTooltipConfig.ShowObjectId) { sb.AppendLine($"ID: {mouseOverTarget.GameObjectId}"); } string body = sb.ToString().TrimEnd('\r', '\n'); if (string.IsNullOrEmpty(body)) body = "(No info)"; uint? iconId = _worldTooltipConfig.ShowIcon ? GetWorldObjectIconId(mouseOverTarget) : null; if (_worldTooltipConfig.DetachFromCursor) { ShowTooltip(body, _worldTooltipConfig.Position, name, 0, "", iconId); } else { ShowTooltipOnCursor(body, name, 0, "", iconId); } } private static unsafe uint? GetWorldObjectIconId(IGameObject gameObject) { if (gameObject == null || gameObject.Address == IntPtr.Zero) return null; var obj = (GameObject*)gameObject.Address; if (obj == null) return null; uint iconId = obj->NamePlateIconId; return iconId > 0 ? iconId : null; } public void RemoveTooltip() { _dataIsValid = false; _formattedSegments = null; } private void DrawTooltipBody() { if (_formattedSegments != null && _formattedSegments.Count > 0) { bool afterNewline = true; float lineHeight = ImGui.GetTextLineHeight(); float compactSpacing = Math.Max(2, lineHeight * _config.TooltipLineSpacing); foreach (var seg in _formattedSegments) { if (seg.Text == "\n") { ImGui.Dummy(new Vector2(0, compactSpacing)); afterNewline = true; } else if (!string.IsNullOrEmpty(seg.Text)) { if (!afterNewline) ImGui.SameLine(0, 0); string text = afterNewline ? seg.Text.TrimStart() : seg.Text; if (!string.IsNullOrEmpty(text)) { ImGui.TextColored(seg.Color, text); afterNewline = false; } } } } else { ImGui.TextColored(_config.TextColor.Vector, _currentTooltipText); } } public void Draw() { if (!_dataIsValid) return; if (ConfigurationManager.Instance.ShowingModalWindow) { if (_config.DebugTooltips) Plugin.Logger.Information("[HSUI Tooltip DBG] Draw SKIPPED: ShowingModalWindow=true"); return; } // bg ImGuiWindowFlags windowFlags = ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoFocusOnAppearing; // imgui clips the left and right borders inside windows for some reason // we make the window bigger so the actual drawable size is the expected one var windowMargin = new Vector2(4, 0); var windowPos = _position - windowMargin; ImGui.SetNextWindowPos(windowPos, ImGuiCond.Always); ImGui.SetNextWindowSize(_size + windowMargin * 2); ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0); ImGui.Begin("DelvUI_tooltip", windowFlags); ImGui.SetWindowFontScale(_config.TooltipFontScale); var drawList = ImGui.GetWindowDrawList(); drawList.AddRectFilled(_position, _position + _size, _config.BackgroundColor.Base); if (_config.BorderConfig.Enabled) { drawList.AddRect(_position, _position + _size, _config.BorderConfig.Color.Base, 0, ImDrawFlags.None, _config.BorderConfig.Thickness); } // no idea why i have to do this float globalScaleCorrection = -15 + 15 * ImGuiHelpers.GlobalScale; string fontId = _config.FontID ?? FontsConfig.DefaultSmallFontKey; float iconSizeScaled = IconSize * ImGuiHelpers.GlobalScale * _config.TooltipScale * _config.TooltipIconScale; float iconGap = (_currentIconId.HasValue && _currentIconId.Value > 0) ? iconSizeScaled + Margin : 0; float wrapWidth = (_currentIconId.HasValue && _currentIconId.Value > 0) ? _size.X - iconGap - Margin * 2 : Math.Max(_titleSize.X, _textSize.X) + Margin; float textBlockX = windowMargin.X + Margin + iconGap; if (_currentIconId.HasValue && _currentIconId.Value > 0) { var iconPos = _position + new Vector2(Margin, Margin); DrawHelper.DrawIcon(_currentIconId.Value, iconPos, new Vector2(iconSizeScaled, iconSizeScaled), false, drawList); } if (_currentTooltipTitle != null) { // title Vector2 cursorPos; using (FontsManager.Instance.PushFont(fontId)) { cursorPos = new Vector2(textBlockX, Margin); ImGui.SetCursorPos(cursorPos); ImGui.PushTextWrapPos(cursorPos.X + wrapWidth + globalScaleCorrection); ImGui.TextColored(_config.TitleColor.Vector, _currentTooltipTitle); ImGui.PopTextWrapPos(); } // text (formatted or plain) using (FontsManager.Instance.PushFont(fontId)) { cursorPos = new Vector2(textBlockX, Margin + _titleSize.Y); ImGui.SetCursorPos(cursorPos); ImGui.PushTextWrapPos(cursorPos.X + wrapWidth + globalScaleCorrection); DrawTooltipBody(); ImGui.PopTextWrapPos(); } } else { // text (formatted or plain) using (FontsManager.Instance.PushFont(fontId)) { var cursorPos = new Vector2(textBlockX, Margin); var textWidth = _size.X - iconGap - Margin * 2; ImGui.SetCursorPos(cursorPos); ImGui.PushTextWrapPos(cursorPos.X + textWidth + globalScaleCorrection); DrawTooltipBody(); ImGui.PopTextWrapPos(); } } ImGui.SetWindowFontScale(1f); ImGui.End(); ImGui.PopStyleVar(); if (_config.DebugTooltips) Plugin.Logger.Information($"[HSUI Tooltip DBG] Draw rendered tooltip at ({_position.X:F0},{_position.Y:F0})"); RemoveTooltip(); } /// /// Parses action/ability description text into colored segments for game-style tooltips. /// Adds newlines before section headers and colors: section labels (Additional Effect, Duration) green, /// status names (e.g. Blood of the Dragon) yellow, secondary labels grey. /// public static List FormatActionTooltip(string description, Vector4 textColor, Vector4 sectionLabelColor, Vector4 statusNameColor) { var result = new List(); if (string.IsNullOrEmpty(description)) return result; // Pattern: section labels to color (Additional Effect, Duration, X Effect, Maximum Charges, job gauge costs, Combo, roles) // Job gauge/resource costs: Soul (RPR), Blood/Beast (DRK/WAR), Kenki/Ninki (SAM/NIN), Cartridge (GNB), Oath (PLD), Lily (WHM), etc. var sectionRegex = new Regex(@"\b(Additional Effect:|Duration:|Maximum Charges:|Blood Gauge Cost:|Soul Gauge Cost:|Beast Gauge Cost:|Kenki Gauge Cost:|Kenki Cost:|Ninki Gauge Cost:|Ninki Cost:|Cartridge Cost:|Oath Gauge Cost:|Lily Cost:|Polyglot Cost:|Addersgall Cost:|Addersting Cost:|Astral Sign Cost:|Lunar Sign Cost:|Battery Gauge Cost:|Heat Gauge Cost:|[A-Za-z][A-Za-z0-9\s]*\s+Cost:|Combo Action:|Combo Potency:|Combo Bonus:|Tank:|Melee:|Ranged:|Caster:|Healer:|[A-Za-z][A-Za-z0-9\s]+ Effect:)", RegexOptions.None); // Pattern: "Grants X X Effect:" - captures status name X var grantsRegex = new Regex(@"Grants\s+(.+?)\s+\1\s+Effect:", RegexOptions.Singleline); int lastEnd = 0; foreach (Match m in sectionRegex.Matches(description)) { string chunk = description.Substring(lastEnd, m.Index - lastEnd).TrimEnd(); if (!string.IsNullOrEmpty(chunk)) { // Check for "Grants StatusName" in this chunk to color status name yellow var grantsMatch = grantsRegex.Match(chunk); if (grantsMatch.Success) { string statusName = grantsMatch.Groups[1].Value.Trim(); string grantsPrefix = "Grants " + statusName; int idx = chunk.IndexOf(grantsPrefix, StringComparison.Ordinal); if (idx >= 0) { if (idx > 0) result.Add(new TooltipSegment(chunk.Substring(0, idx), textColor)); result.Add(new TooltipSegment("Grants ", textColor)); result.Add(new TooltipSegment(statusName + " ", statusNameColor)); int after = idx + grantsPrefix.Length; if (after < chunk.Length) result.Add(new TooltipSegment(chunk.Substring(after), textColor)); } else result.Add(new TooltipSegment(chunk, textColor)); } else result.Add(new TooltipSegment(chunk, textColor)); } result.Add(new TooltipSegment("\n", textColor)); result.Add(new TooltipSegment(m.Value + " ", sectionLabelColor)); // Add newline so description wraps below label like stock game (not floating beside it) if (m.Value.IndexOf("Effect:", StringComparison.OrdinalIgnoreCase) >= 0) result.Add(new TooltipSegment("\n", textColor)); lastEnd = m.Index + m.Length; } if (lastEnd < description.Length) { string tail = description.Substring(lastEnd).TrimStart(); if (!string.IsNullOrEmpty(tail)) { string valuePart = ""; string notePart = ""; bool hasValue = false; int newlineIdx = tail.IndexOf('\n'); if (newlineIdx >= 0) { // "2\nCannot be executed..." - value on first line, note on next valuePart = tail.Substring(0, newlineIdx).TrimEnd(); notePart = tail.Substring(newlineIdx + 1).TrimStart(); hasValue = Regex.IsMatch(valuePart, @"^\d+[ms]?$"); } else { // "2 Cannot be executed while bound." - value + space + note (no newline) var valueNoteMatch = Regex.Match(tail, @"^(\d+[ms]?)\s+([A-Z].+)$", RegexOptions.Singleline); if (valueNoteMatch.Success) { valuePart = valueNoteMatch.Groups[1].Value; notePart = valueNoteMatch.Groups[2].Value; hasValue = true; } else if (Regex.IsMatch(tail, @"^\d+[ms]?$")) { // Plain value only: "50" (Blood Gauge Cost), "2" (charges) - stay on same line as label valuePart = tail; hasValue = true; } } // Add valuePart when present - section content (e.g. "Restore party HP..." for Healer, "2" for charges) if (!string.IsNullOrEmpty(valuePart)) { result.Add(new TooltipSegment(valuePart, textColor)); } if (!string.IsNullOrEmpty(notePart)) { result.Add(new TooltipSegment("\n", textColor)); result.Add(new TooltipSegment(notePart, textColor)); } else if (!hasValue) { // Standalone note: "(Duration can be extended...)", "Cannot be executed...", footnotes result.Add(new TooltipSegment("\n", textColor)); result.Add(new TooltipSegment(tail, textColor)); } } } return result; } /// /// Builds formatted segments from a full action tooltip body (stats + description). /// Used when UseGameStyleFormatting is enabled. /// Stats are placed first, then exactly one newline, then description below. /// public static List? BuildFormattedActionTooltipBody(string body, TooltipsConfig config) { if (string.IsNullOrEmpty(body) || !config.UseGameStyleFormatting) return null; var result = new List(); var textColor = config.TextColor.Vector; var sectionColor = config.SectionLabelColor.Vector; var statusColor = config.StatusNameColor.Vector; var secondaryColor = config.SecondaryLabelColor.Vector; var blocks = body.Split(new[] { "\n\n" }, StringSplitOptions.None); bool hasStats = false; // Job gauge/resource cost line pattern (e.g. "Soul Gauge Cost: 50", "Lily Cost: 1") - treat as own section with section color var gaugeCostLineRegex = new Regex(@"^(.+?\s+Cost:)\s*(.*)$", RegexOptions.None); // Stats block: may contain "Potency: X", "Cast: ... | Recast: ...", and job gauge cost lines. // Once we hit a line that doesn't match any of these, treat it and the rest as description (don't drop it). List descBlocks = new List(); bool inDescription = false; for (int i = 0; i < blocks.Length; i++) { string block = blocks[i].Trim(); var lines = block.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); List descLinesInBlock = new List(); foreach (string line in lines) { string part = line.Trim(); if (inDescription) { descLinesInBlock.Add(part); continue; } if (part.StartsWith("Potency:", StringComparison.OrdinalIgnoreCase)) { if (hasStats) result.Add(new TooltipSegment("\n", textColor)); result.Add(new TooltipSegment("Potency:", secondaryColor)); result.Add(new TooltipSegment(" " + part.Substring(8).TrimStart(), textColor)); hasStats = true; } else if (part.Contains("Cast:") || part.Contains("Recast:")) { if (hasStats) result.Add(new TooltipSegment("\n", textColor)); var statsSegs = FormatActionStats(part, textColor, secondaryColor); result.AddRange(statsSegs); hasStats = true; } else if (gaugeCostLineRegex.IsMatch(part)) { var match = gaugeCostLineRegex.Match(part); string label = match.Groups[1].Value.TrimEnd(); string value = match.Groups[2].Value.Trim(); if (hasStats) result.Add(new TooltipSegment("\n", textColor)); result.Add(new TooltipSegment(label + (string.IsNullOrEmpty(value) ? "" : " "), sectionColor)); if (!string.IsNullOrEmpty(value)) result.Add(new TooltipSegment(value, textColor)); hasStats = true; } else { inDescription = true; descLinesInBlock.Add(part); } } if (descLinesInBlock.Count > 0) descBlocks.Add(string.Join("\n", descLinesInBlock)); if (inDescription) { descBlocks.AddRange(blocks.Skip(i + 1).Select(b => b.Trim()).Where(b => b.Length > 0)); break; } } // Description: exactly one newline after stats, then description with compact section spacing string desc = string.Join("\n\n", descBlocks).Trim(); if (!string.IsNullOrEmpty(desc)) { if (hasStats) result.Add(new TooltipSegment("\n", textColor)); var descSegs = FormatActionTooltip(desc, textColor, sectionColor, statusColor); result.AddRange(descSegs); } return result.Count > 0 ? result : null; } /// /// Formats action stats (Cast, Recast, Range, Radius) as colored segments. /// public static List FormatActionStats(string statsLine, Vector4 textColor, Vector4 secondaryLabelColor) { var result = new List(); if (string.IsNullOrEmpty(statsLine)) return result; // statsLine format: "Cast: Instant | Recast: 60.0s | Range: 15y | Radius: 15y" var parts = statsLine.Split(new[] { " | " }, StringSplitOptions.None); for (int i = 0; i < parts.Length; i++) { if (i > 0) result.Add(new TooltipSegment(" | ", textColor)); int colonIdx = parts[i].IndexOf(':'); if (colonIdx >= 0) { result.Add(new TooltipSegment(parts[i].Substring(0, colonIdx + 1), secondaryLabelColor)); result.Add(new TooltipSegment(parts[i].Substring(colonIdx + 1), textColor)); } else { result.Add(new TooltipSegment(parts[i], textColor)); } } return result; } private Vector2 ConstrainPosition(Vector2 position, Vector2 size) { var screenSize = ImGui.GetWindowViewport().Size; if (position.X < 0) { position.X = Margin; } else if (position.X + size.X > screenSize.X) { position.X = screenSize.X - size.X - Margin; } if (position.Y < 0) { position.Y = Margin; } return position; } } [Section("Misc")] [SubSection("Tooltips", 0)] public class TooltipsConfig : PluginConfigObject { public new static TooltipsConfig DefaultConfig() { return new TooltipsConfig(); } [Checkbox("Debug Tooltips")] [Order(3)] public bool DebugTooltips = false; [Font] [Order(4)] public string? FontID = null; [DragFloat("Tooltip Scale", min = 0.5f, max = 2f, help = "Scale the tooltip window size. Useful for large resolutions.")] [Order(5)] public float TooltipScale = 1f; [DragFloat("Tooltip Max Width", min = 160f, max = 600f, help = "Maximum width of the tooltip box in pixels.")] [Order(6)] public float TooltipMaxWidth = 340f; [DragFloat("Line Spacing", min = 0.2f, max = 1.5f, help = "Spacing between lines and sections (1 = normal, lower = compact).")] [Order(7)] public float TooltipLineSpacing = 0.6f; [DragFloat("Font Scale", min = 0.7f, max = 1.5f, help = "Scale factor for tooltip font size.")] [Order(8)] public float TooltipFontScale = 1f; [DragFloat("Icon Scale", min = 0.5f, max = 2f, help = "Scale factor for the ability/status icon in tooltips.")] [Order(9)] public float TooltipIconScale = 1f; [DragFloat("Height Buffer", min = 0f, max = 80f, help = "Extra space at bottom of tooltip to prevent clipping. Adjust if text is cut off.")] [Order(10)] public float TooltipHeightBuffer = 28f; [Checkbox("Show Status Effects IDs")] [Order(11)] public bool ShowStatusIDs = false; [Checkbox("Show Action ID", help = "Show action ID in hotbar and action tooltips.")] [Order(12)] public bool ShowActionIDs = false; [Checkbox("Show Source Name")] [Order(13)] public bool ShowSourceName = false; [Checkbox("Game-style formatting", help = "Color section labels (Additional Effect, Duration) green, status names yellow, and add new lines between sections like the base game tooltips.")] [Order(14)] public bool UseGameStyleFormatting = true; [ColorEdit4("Section Label Color", help = "Color for labels like 'Additional Effect:', 'Duration:'.")] [Order(15)] public PluginConfigColor SectionLabelColor = new PluginConfigColor(new(0f, 255f / 255f, 0f, 1f)); [ColorEdit4("Status Name Color", help = "Color for buff/status effect names like 'Blood of the Dragon'.")] [Order(16)] public PluginConfigColor StatusNameColor = new PluginConfigColor(new(255f / 255f, 255f / 255f, 0f, 1f)); [ColorEdit4("Secondary Label Color", help = "Color for Cast, Recast, Range, Radius labels.")] [Order(17)] public PluginConfigColor SecondaryLabelColor = new PluginConfigColor(new(204f / 255f, 204f / 255f, 204f / 255f, 1f)); [ColorEdit4("Background Color")] [Order(15)] public PluginConfigColor BackgroundColor = new PluginConfigColor(new(19f / 255f, 19f / 255f, 19f / 255f, 190f / 250f)); [ColorEdit4("Title Color")] [Order(20)] public PluginConfigColor TitleColor = new PluginConfigColor(new(255f / 255f, 210f / 255f, 31f / 255f, 100f / 100f)); [ColorEdit4("Text Color")] [Order(35)] public PluginConfigColor TextColor = new PluginConfigColor(new(255f / 255f, 255f / 255f, 255f / 255f, 100f / 100f)); [NestedConfig("Border", 40, separator = false, spacing = true, collapsingHeader = false)] public TooltipBorderConfig BorderConfig = new(); } [Exportable(false)] public class TooltipBorderConfig : PluginConfigObject { [ColorEdit4("Color")] [Order(5)] public PluginConfigColor Color = new(new Vector4(10f / 255f, 10f / 255f, 10f / 255f, 160f / 255f)); [DragInt("Thickness", min = 1, max = 100)] [Order(10)] public int Thickness = 4; public TooltipBorderConfig() { } public TooltipBorderConfig(PluginConfigColor color, int thickness) { Color = color; Thickness = thickness; } } [Section("Misc")] [SubSection("World Tooltip", 0)] public class WorldObjectTooltipConfig : PluginConfigObject { public new static WorldObjectTooltipConfig DefaultConfig() { return new WorldObjectTooltipConfig(); } [Checkbox("Detach from Cursor", spacing = true)] [Order(1)] public bool DetachFromCursor = false; [DragFloat2("Position (when detached)", min = -4000f, max = 4000f)] [Order(2, collapseWith = nameof(DetachFromCursor))] public Vector2 Position = new Vector2(100, 100); [Checkbox("Show Icon (quest/NPC icons)", spacing = true)] [Order(3)] public bool ShowIcon = true; [Checkbox("Shift+Click action in Actions & Traits → insert \"You should check out X\" in chat", help = "When chat is focused, inserts directly. Otherwise copies to clipboard.")] [Order(4)] public bool ActionChatLinkEnabled = true; [Checkbox("Show Level", spacing = true)] [Order(6)] public bool ShowLevel = true; [Checkbox("Show HP")] [Order(10)] public bool ShowHP = true; [Checkbox("Show Job (Players)")] [Order(15)] public bool ShowJob = true; [Checkbox("Show FC Tag (Players)")] [Order(20)] public bool ShowTitle = false; [Checkbox("Show Distance")] [Order(25)] public bool ShowDistance = true; [Checkbox("Show Object ID")] [Order(30)] public bool ShowObjectId = false; } }