using Dalamud.Bindings.ImGui; using Dalamud.Interface; using HSUI.Config; using HSUI.Helpers; using HSUI.Interface; using System.Collections.Generic; using System.Numerics; namespace HSUI.Interface.GeneralElements { public class DutyListScenarioHud : DraggableHudElement, IHudElementWithVisibilityConfig { private DutyListScenarioConfig Config => (DutyListScenarioConfig)_config; public VisibilityConfig VisibilityConfig => Config.VisibilityConfig; public DutyListScenarioHud(DutyListScenarioConfig config, string displayName) : base(config, displayName) { } protected override (List, List) ChildrenPositionsAndSizes() { return (new List { Config.Position }, new List { Config.Size }); } /// Screen-space bounds for visibility "hide unless hovered" must match the draw position exactly. public override (Vector2 min, Vector2 max) GetScreenBounds(Vector2 origin) { Vector2 topLeft = Utils.GetAnchoredPosition(origin + ParentPos() + Config.Position, Config.Size, Config.Anchor); return (topLeft, topLeft + Config.Size); } public override void DrawChildren(Vector2 origin) { if (!Config.Enabled) return; var dutyListEntries = DutyListScenarioHelper.GetDutyListEntries(); var scenarioEntries = DutyListScenarioHelper.GetScenarioGuideEntries(); var dutyFinderInfo = DutyListScenarioHelper.GetDutyFinderQueueInfo(); var dutyInfo = DutyListScenarioHelper.GetDutyInfo(); bool hasDutyList = dutyListEntries.Count > 0; bool hasScenario = scenarioEntries.Count > 0; // Show queue info when in queue; show duty info when in duty (not in queue). Same section, different content. bool hasDutyFinder = Config.ShowDutyFinderSection && dutyFinderInfo != null; bool hasDutyInfo = Config.ShowDutyFinderSection && dutyInfo != null; if (!hasDutyList && !hasScenario && !hasDutyFinder && !hasDutyInfo) return; Vector2 basePos = Utils.GetAnchoredPosition(origin + Config.Position, Config.Size, Config.Anchor); Vector2 iconSize = new Vector2(Config.IconSize, Config.IconSize); float lineHeight = Config.IconSize + 4; float padding = 6; float contentWidth = Config.Size.X - padding * 2; AddDrawAction(Config.StrataLevel, () => { DrawHelper.DrawInWindow(ID, basePos, Config.Size, true, (drawList) => { uint bgColor = Config.BackgroundColor.Base; if ((bgColor & 0xFF000000) != 0) drawList.AddRectFilled(basePos, basePos + Config.Size, bgColor, 4); drawList.PushClipRect(basePos, basePos + Config.Size, true); float y = basePos.Y + padding; uint textColor = Config.TextColor.Base; if (Config.FontID != "Default" && FontsManager.Instance != null) { using (FontsManager.Instance.PushFont(Config.FontID)) { ImGui.SetWindowFontScale(Config.FontScale); DrawContent(drawList, ref y, basePos.X + padding, contentWidth, lineHeight, iconSize, textColor, hasDutyList, hasScenario, hasDutyFinder, hasDutyInfo, dutyListEntries, scenarioEntries, dutyFinderInfo, dutyInfo); ImGui.SetWindowFontScale(1); } } else { ImGui.PushFont(UiBuilder.DefaultFont); ImGui.SetWindowFontScale(Config.FontScale); DrawContent(drawList, ref y, basePos.X + padding, contentWidth, lineHeight, iconSize, textColor, hasDutyList, hasScenario, hasDutyFinder, hasDutyInfo, dutyListEntries, scenarioEntries, dutyFinderInfo, dutyInfo); ImGui.SetWindowFontScale(1); ImGui.PopFont(); } drawList.PopClipRect(); }); }); } private static string TruncateText(string text, float maxWidth) { if (ImGui.CalcTextSize(text).X <= maxWidth) return text; string suffix = "..."; while (text.Length > 1 && ImGui.CalcTextSize(text + suffix).X > maxWidth) text = text[..^1]; return text + suffix; } private void DrawContent( ImDrawListPtr drawList, ref float y, float x, float contentWidth, float lineHeight, Vector2 iconSize, uint textColor, bool hasDutyList, bool hasScenario, bool hasDutyFinder, bool hasDutyInfo, List dutyListEntries, List scenarioEntries, DutyListScenarioHelper.DutyFinderQueueInfo? dutyFinderInfo, DutyListScenarioHelper.DutyInfo? dutyInfo) { const uint TankIconId = 62581; const uint HealerIconId = 62582; const uint DPSIconId = 62583; if (hasDutyInfo && dutyInfo != null) { uint titleColor = Config.DutyFinderTitleColor.Base; uint detailColor = Config.DutyFinderDetailColor.Base; drawList.AddText(new Vector2(x, y), titleColor, "Duty Information"); y += lineHeight; string dutyText = TruncateText(dutyInfo.DutyName, contentWidth); drawList.AddText(new Vector2(x, y), detailColor, dutyText); y += lineHeight; if (!string.IsNullOrEmpty(dutyInfo.TimerText)) { drawList.AddText(new Vector2(x, y), detailColor, TruncateText(dutyInfo.TimerText, contentWidth)); y += lineHeight; } foreach (var obj in dutyInfo.Objectives) { string line = string.IsNullOrWhiteSpace(obj.Text) ? "???" : obj.Text; if (!string.IsNullOrWhiteSpace(obj.Progress)) line += ": " + obj.Progress; drawList.AddText(new Vector2(x, y), detailColor, TruncateText(line, contentWidth)); y += lineHeight; } y += 4; if (hasScenario || hasDutyList) { float divY = y + 2; drawList.AddRectFilled(new Vector2(x, divY), new Vector2(x + contentWidth, divY + 2f), Config.DividerColor.Base); y += 8; } } else if (hasDutyFinder && dutyFinderInfo != null) { uint titleColor = Config.DutyFinderTitleColor.Base; uint detailColor = Config.DutyFinderDetailColor.Base; drawList.AddText(new Vector2(x, y), titleColor, "Duty Finder"); y += lineHeight; string dutyText = TruncateText(dutyFinderInfo.DutyName, contentWidth); drawList.AddText(new Vector2(x, y), detailColor, dutyText); y += lineHeight; string statusText = TruncateText(dutyFinderInfo.StatusText, contentWidth); drawList.AddText(new Vector2(x, y), detailColor, statusText); y += lineHeight; float roleX = x; roleX += 4; DrawHelper.DrawIcon(TankIconId, new Vector2(roleX, y), new Vector2(iconSize.X * 0.8f, iconSize.Y * 0.8f), false, detailColor, drawList); roleX += iconSize.X * 0.8f + 2; drawList.AddText(new Vector2(roleX, y), detailColor, $":{dutyFinderInfo.TanksFound}/{dutyFinderInfo.TanksNeeded}"); roleX += ImGui.CalcTextSize($":{dutyFinderInfo.TanksFound}/{dutyFinderInfo.TanksNeeded}").X + 6; DrawHelper.DrawIcon(HealerIconId, new Vector2(roleX, y), new Vector2(iconSize.X * 0.8f, iconSize.Y * 0.8f), false, detailColor, drawList); roleX += iconSize.X * 0.8f + 2; drawList.AddText(new Vector2(roleX, y), detailColor, $":{dutyFinderInfo.HealersFound}/{dutyFinderInfo.HealersNeeded}"); roleX += ImGui.CalcTextSize($":{dutyFinderInfo.HealersFound}/{dutyFinderInfo.HealersNeeded}").X + 6; DrawHelper.DrawIcon(DPSIconId, new Vector2(roleX, y), new Vector2(iconSize.X * 0.8f, iconSize.Y * 0.8f), false, detailColor, drawList); roleX += iconSize.X * 0.8f + 2; drawList.AddText(new Vector2(roleX, y), detailColor, $":{dutyFinderInfo.DPSFound}/{dutyFinderInfo.DPSNeeded}"); y += lineHeight; string elapsedStr = $"{(int)dutyFinderInfo.TimeElapsed.TotalMinutes}:{dutyFinderInfo.TimeElapsed.Seconds:D2}"; string timeLine = "Time Elapsed: " + elapsedStr; if (!string.IsNullOrEmpty(dutyFinderInfo.AverageWaitText)) timeLine += " / Average Wait Time: " + dutyFinderInfo.AverageWaitText; timeLine = TruncateText(timeLine, contentWidth); drawList.AddText(new Vector2(x, y), detailColor, timeLine); y += lineHeight + 4; if (hasScenario || hasDutyList) { float divY = y + 2; drawList.AddRectFilled(new Vector2(x, divY), new Vector2(x + contentWidth, divY + 2f), Config.DividerColor.Base); y += 8; } } if (hasScenario) { foreach (var entry in scenarioEntries) { float lineX = x; if (Config.ShowQuestIcons && entry.IconId != 0) { DrawHelper.DrawIcon(entry.IconId, new Vector2(lineX, y), iconSize, false, textColor, drawList); lineX += iconSize.X + 4; } string text = TruncateText($"{entry.Label}: Lv. {entry.Level} {entry.QuestName}", contentWidth - (lineX - x)); var textSize = ImGui.CalcTextSize(text); drawList.AddText(new Vector2(lineX, y), textColor, text); // Use full line height for hit area so the second line (Job) doesn't get covered by the first (MSQ) if (ImGui.IsMouseHoveringRect(new Vector2(lineX, y), new Vector2(x + contentWidth, y + lineHeight)) && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) { DutyListScenarioHelper.OpenMapForQuestIssuer(entry.QuestRowId); } y += lineHeight; } if (hasDutyList) { float divY = y + 2; drawList.AddRectFilled(new Vector2(x, divY), new Vector2(x + contentWidth, divY + 2f), Config.DividerColor.Base); y += 8; } } if (hasDutyList) { uint objectiveColor = ImGui.ColorConvertFloat4ToU32(new Vector4(0.78f, 0.78f, 0.78f, 1f)); foreach (var entry in dutyListEntries) { float lineX = x; if (Config.ShowQuestIcons && entry.IconId != 0) { DrawHelper.DrawIcon(entry.IconId, new Vector2(lineX, y), iconSize, false, textColor, drawList); lineX += iconSize.X + 4; } string nameText = TruncateText(entry.QuestName, contentWidth - (lineX - x)); var nameSize = ImGui.CalcTextSize(nameText); drawList.AddText(new Vector2(lineX, y), textColor, nameText); if (entry.QuestId != 0 && ImGui.IsMouseHoveringRect(new Vector2(lineX, y), new Vector2(lineX + nameSize.X, y + nameSize.Y)) && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) { DutyListScenarioHelper.OpenJournalForQuest(entry.QuestId); } y += lineHeight; if (!string.IsNullOrWhiteSpace(entry.ObjectiveText)) { float objX = lineX + 8; string objText = TruncateText("• " + entry.ObjectiveText, contentWidth - (objX - x)); var objSize = ImGui.CalcTextSize(objText); drawList.AddText(new Vector2(objX, y), objectiveColor, objText); if (entry.QuestId != 0 && ImGui.IsMouseHoveringRect(new Vector2(objX, y), new Vector2(objX + objSize.X, y + objSize.Y)) && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) { DutyListScenarioHelper.OpenMapForQuestObjective(entry.QuestId, entry.Sequence); } y += lineHeight; } } } } } }