Compare commits

...

2 Commits

Author SHA1 Message Date
KnackAtNite ee7a2d71a0 Merge pull request 'fix: Duty List hide-unless-hovered + tooltip job gauge cost section formatting' (#3) from feature/improvement into main 2026-03-03 00:39:30 +00:00
Jorg 7a61e11e37 fix: Duty List hide-unless-hovered + tooltip job gauge cost section formatting
Duty List & Scenario Guide:
- Fix 'Hide unless hovered' visibility option not working. Override
  GetScreenBounds() in DutyListScenarioHud so the hover hit-test rect
  matches the drawn panel position exactly (same formula as draw code).

Tooltips (game-style formatting):
- Treat job gauge/resource costs as a new section with section label color
  (e.g. Soul Gauge Cost, Beast Gauge Cost, Lily Cost, Kenki, Ninki,
  Cartridge, Oath, Polyglot, Addersgall, Astral/Lunar Sign, Battery/Heat).
- In BuildFormattedActionTooltipBody: recognize gauge cost lines in the
  stats block and emit them as their own section (newline + green label).
- Fix tooltip break when stats and description share the first block:
  switch to description on first non-stats line instead of dropping it,
  so ability description text is never lost.

Made-with: Cursor
2026-03-02 13:33:30 -06:00
2 changed files with 43 additions and 11 deletions
+36 -11
View File
@@ -463,8 +463,9 @@ namespace HSUI.Helpers
if (string.IsNullOrEmpty(description))
return result;
// Pattern: section labels to color (Additional Effect, Duration, X Effect, Maximum Charges, Blood Gauge Cost, Combo, roles)
var sectionRegex = new Regex(@"\b(Additional Effect:|Duration:|Maximum Charges:|Blood Gauge Cost:|Combo Action:|Combo Potency:|Combo Bonus:|Tank:|Melee:|Ranged:|Caster:|Healer:|[A-Za-z][A-Za-z0-9\s]+ Effect:)", RegexOptions.None);
// 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);
@@ -579,25 +580,33 @@ namespace HSUI.Helpers
var secondaryColor = config.SecondaryLabelColor.Vector;
var blocks = body.Split(new[] { "\n\n" }, StringSplitOptions.None);
int descStart = 0;
bool hasStats = false;
// Stats block: may contain "Potency: X" and "Cast: ... | Recast: ..." on separate lines
// 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<string> descBlocks = new List<string>();
bool inDescription = false;
for (int i = 0; i < blocks.Length; i++)
{
string block = blocks[i].Trim();
var lines = block.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
bool blockIsStats = false;
List<string> descLinesInBlock = new List<string>();
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;
blockIsStats = true;
}
else if (part.Contains("Cast:") || part.Contains("Recast:"))
{
@@ -605,19 +614,35 @@ namespace HSUI.Helpers
var statsSegs = FormatActionStats(part, textColor, secondaryColor);
result.AddRange(statsSegs);
hasStats = true;
blockIsStats = 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 (!blockIsStats)
if (descLinesInBlock.Count > 0)
descBlocks.Add(string.Join("\n", descLinesInBlock));
if (inDescription)
{
descStart = i;
descBlocks.AddRange(blocks.Skip(i + 1).Select(b => b.Trim()).Where(b => b.Length > 0));
break;
}
descStart = i + 1;
}
// Description: exactly one newline after stats, then description with compact section spacing
string desc = string.Join("\n", blocks.Skip(descStart)).Trim();
string desc = string.Join("\n\n", descBlocks).Trim();
if (!string.IsNullOrEmpty(desc))
{
if (hasStats) result.Add(new TooltipSegment("\n", textColor));
@@ -21,6 +21,13 @@ namespace HSUI.Interface.GeneralElements
return (new List<Vector2> { Config.Position }, new List<Vector2> { Config.Size });
}
/// <summary>Screen-space bounds for visibility "hide unless hovered" must match the draw position exactly.</summary>
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)