v1.0.8.21: Duty Information in duty, objective progress as X out of Y steps + checkmark, average wait from queue

Made-with: Cursor
This commit is contained in:
2026-03-02 04:06:13 -05:00
parent 2822f730f5
commit 27e743c80f
11 changed files with 1502 additions and 6 deletions
@@ -0,0 +1,72 @@
using Dalamud.Bindings.ImGui;
using HSUI.Config;
using HSUI.Config.Attributes;
using HSUI.Enums;
using System.Numerics;
namespace HSUI.Interface.GeneralElements
{
[Section("Other Elements")]
[SubSection("Duty List & Scenario Guide", 0)]
public class DutyListScenarioConfig : AnchorablePluginConfigObject
{
[Checkbox("Show Quest Icons")]
[Order(20)]
public bool ShowQuestIcons = true;
[DragInt("Icon Size", min = 12, max = 48)]
[Order(21, collapseWith = nameof(ShowQuestIcons))]
public int IconSize = 24;
[DragFloat("Font Scale", min = 0.5f, max = 2f)]
[Order(23)]
public float FontScale = 1f;
[Font("Font")]
[Order(24)]
public string FontID = "Default";
[ColorEdit4("Text Color")]
[Order(30)]
public PluginConfigColor TextColor = new(new Vector4(1f, 1f, 1f, 1f));
[ColorEdit4("Background Color")]
[Order(31)]
public PluginConfigColor BackgroundColor = new(new Vector4(0f, 0f, 0f, 0.5f));
[ColorEdit4("Divider Color")]
[Order(32)]
public PluginConfigColor DividerColor = new(new Vector4(1f, 1f, 1f, 0.6f));
[Checkbox("Show Duty Finder section when in queue")]
[Order(35)]
public bool ShowDutyFinderSection = true;
[ColorEdit4("Duty Finder title color")]
[Order(36, collapseWith = nameof(ShowDutyFinderSection))]
public PluginConfigColor DutyFinderTitleColor = new(new Vector4(1f, 0.85f, 0.2f, 1f));
[ColorEdit4("Duty Finder detail color")]
[Order(37, collapseWith = nameof(ShowDutyFinderSection))]
public PluginConfigColor DutyFinderDetailColor = new(new Vector4(0.4f, 0.75f, 1f, 1f));
[NestedConfig("Visibility", 70)]
public VisibilityConfig VisibilityConfig = new();
public DutyListScenarioConfig()
{
Size = new Vector2(320, 200);
}
public new static DutyListScenarioConfig DefaultConfig()
{
var config = new DutyListScenarioConfig
{
Position = new Vector2(ImGui.GetMainViewport().Size.X * 0.38f, -ImGui.GetMainViewport().Size.Y * 0.35f),
Size = new Vector2(320, 200),
Anchor = DrawAnchor.TopRight
};
return config;
}
}
}
@@ -0,0 +1,272 @@
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<Vector2>, List<Vector2>) ChildrenPositionsAndSizes()
{
return (new List<Vector2> { Config.Position }, new List<Vector2> { 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) =>
{
drawList.AddRectFilled(basePos, basePos + Config.Size, Config.BackgroundColor.Base, 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<DutyListScenarioHelper.DutyListEntry> dutyListEntries,
List<DutyListScenarioHelper.ScenarioGuideEntry> 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;
}
}
}
}
}
}