fix(vtank): slice 7 round F item 6 — Buffs gap, Loot Editor Back, Route count column

Three small resemblance-re-check nits.

Buffs: the extras strip (toggles/Difficulty/Rebuff/Buff button, no VTank
counterpart) stayed at its pre-Round-D x=332 while Round D item 4's own
+128 right-anchor shift moved the Blacklisted Buff Families list from
x=524 to x=652 — an 8px left margin against a 138px right margin.
Discovered a hard geometric ceiling while fixing this: Extra Buff
Spells never moves (anchor="left top bottom", pinned x=4..324) but
Blacklisted Buff Families tracks the growing right edge, sitting at
x=524 at the panel's own tested FLOOR (856x236) and x=652 at the
984-wide enlarged default — a 128px swing. A single fixed-position
strip can only be centered against ONE of those widths; centering
against the 984 default (tried x=397 first) put the strip's right edge
55px inside where the list sits at the floor — a real overlap the
existing ResolvedMainPanelHasNoOverlapOrOutOfBoundsChildAtThisSize pin
(856x236 case) caught immediately. x=333 is the widest safe position
(9px margins against the floor's 324..524 gap) — 1px from where fix
round B item 2 already had it. The residual 984-default asymmetry
(~9/137, barely changed from ~8/138) is an accepted, unavoidable-
without-a-redesign limitation, the same "dead margin, polish later"
trade already accepted for six other tabs at this size. Coverage moved
from y=213 (anchored to the group's bottom edge, a real 55px empty
hole once the panel grew past its original 194-tall size) to sit
directly under the Buff button row (y=176, no anchor).

Loot Editor: dropped the leftover standalone "Back" button — the
window's own OS-level title bar already closes it, matching every
other plugin popup (Advanced Options included) since round D.
CloseLootEditor stays real, still called by ToggleLootEditorVisible.

Route: the waypoint grid's count column (declared width="30") was the
grid's LAST column, so docs/plugin-ui-markup.md's own "Width semantics"
rule made that declared width never validated/used — it silently
absorbed whatever remainder was left (33px normally, but only 17px once
the scrollbar's 16px reservation ate into the list's own 370px width,
since 337+30 already exceeded 370-16=354). A trailing filler column
alone wasn't sufficient — proved by a targeted mutation below — since
even non-last, a truly fixed 30px column still gets clamped by the
same 354px ceiling. The real text column narrows 337->324
(324+30=354, exactly matching the scrollbar-reduced width) so the
count column gets its full 30px whether or not the scrollbar shows.

Every new/changed pin shown to fail against a targeted mutation first:
LootEditorHasNoLeftoverBackButton failed with the button re-inserted,
then passed once removed. RouteShapedGrid_CountColumnStaysThirtyPxWhen
TheListScrolls (a synthetic list matching Route's real 370/324/30/*
shape, 9 rows forcing the scrollbar, reflecting into UiMarkupList's own
_cachedLayout) failed both against the original 2-column 337/30 shape
(17px, reproducing the reported squeeze) AND against filler-only-no-
narrowing (also 17px, proving the filler alone isn't the fix), then
passed at exactly 30px once both changes landed together.
BuffsExtrasStripIsCenteredAtThePanelsFloorAndCoverageSitsUnderTheButtonRow
and its App-side resolved-tree counterpart
BuffsExtrasStripStaysCenteredAndNonOverlappingAtTheFloor pin the
floor-centered position and the Coverage gap.

MossTank suite 725 -> 726 (one new pin); App markup/plugin filter
260 -> 262 (two new pins).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-07 19:28:30 +02:00
parent bae34aaa8b
commit 8376105936
6 changed files with 312 additions and 32 deletions

View file

@ -1040,6 +1040,18 @@ internal sealed partial class MossTankPanel
// PluginCore.cs:3576-3599 — any cell click deletes the waypoint).
public IReadOnlyList<string> RouteWaypointTextColumn => _routeRows;
public IReadOnlyList<string> RouteWaypointCountColumn => _routeWaypointCounts;
// Round F item 6 (resemblance re-check: "Route's count column
// squeezes to 17 when the list scrolls"): RouteWaypointCountColumn
// was the grid's LAST column, so docs/plugin-ui-markup.md's own
// "Width semantics" rule made its declared width="30" NEVER validated
// or used — it silently absorbed whatever remainder was left instead
// (33px normally, but only 17px once the scrollbar's own 16px
// reservation ate into that remainder). A trailing filler column
// (empty items) now sits after it so the count column is no longer
// last and keeps its own real, always-honored 30px width — the same
// fix as the Monsters grid's MonsterFillerColumn.
public IReadOnlyList<string> RouteWaypointFillerColumn => Array.Empty<string>();
public Action<int> RouteWaypointFillerClick => static _ => { };
public Action<int> DeleteRouteWaypointAt => DeleteRouteWaypointAtCore;
public Action SelectNearestRouteWaypoint => SelectNearestRouteWaypointCore;
// VTank's own cmbNavType captions read Circular/Linear/Follow/Once

View file

@ -13,11 +13,20 @@
panel, registered as a THIRD AddPanel call from MossTankPlugin.cs with
ShowInSidePanel=false (VTank's popup has no independent shelf entry). This
panel's own visible="{LootEditorVisible}" binding is the SAME flag the
checkbox flips and this popup's own "Back" button clears
checkbox flips and CloseLootEditor clears
(CloseLootEditor => _lootEditorVisible = false) — closing the popup
unchecks the box for free (see mosstank-advanced.xml's comment for why no
new host show/hide API was needed).
Round F item 6 (resemblance re-check: "Loot Editor: drop the leftover
'Back' button — the window closes from its title bar like the
others"): the standalone "Back" button bound directly to
CloseLootEditor is gone — the window's own OS-level title bar already
closes it, the same shape every other plugin popup (Advanced Options
included) has used with no Back button of its own since round D.
CloseLootEditor stays real: ToggleLootEditorVisible (the Profiles-tab
"Show Editor" checkbox's own close path) still calls it directly.
Fix round A also replaced the "↑"/"↓" text buttons (MoveLootRuleUp/Down)
with the same DAT move-up/move-down icon glyphs the Monsters grid already
uses (0x060028FC / 0x060028FD) — the retail default UI font (0x40000000)
@ -40,8 +49,6 @@
-->
<panel x="660" y="405" w="268" h="300" title="MossTank Loot Editor"
visible="{LootEditorVisible}" resize="none">
<button x="198" y="2" w="64" h="18" text="Back"
onclick="{CloseLootEditor}" />
<field x="4" y="22" w="260" h="18" text="{LootProfileNameDraft}"
onchange="{SetLootProfileNameDraft}"
onsubmit="{CreateNamedLootProfile}" maxlength="64"

View file

@ -582,13 +582,42 @@
catches. Both lists are restored to VTank's full 320x116 at the far
left (x=4) and far right (x=524, 848-4-320) with their own
120-wide "Add..." row below each list; the whole MossTank-extras
block moves into the EMPTY MIDDLE strip between the two lists
(x=332..516, the 184px VTank's own layout never uses), stacked
compactly: 7 toggles at a 16px pitch, then Difficulty +/-, then
Rebuff +/-, then the Buff button + status, then Coverage at y=178
(194-16, item 3's bottom-fit rule) — a proportion-preserving
compromise fitting a real 184x194 box, not a pixel-for-pixel VVS
port (owner's bar: "looks basically the same", never VVS pixels). -->
block moves into the EMPTY MIDDLE strip between the two lists,
stacked compactly: 7 toggles at a 16px pitch, then Difficulty +/-,
then Rebuff +/-, then the Buff button + status, then Coverage
right under it — a proportion-preserving compromise fitting a
real box, not a pixel-for-pixel VVS port (owner's bar: "looks
basically the same", never VVS pixels).
Round F item 6 (resemblance re-check: "Buffs' 136 px middle gap
and the orphaned Coverage label"). Coverage moved from y=213
(anchored to the group's bottom edge, a real 55px empty hole
above it once the panel grew past its original 194-tall size) to
sit directly under the Buff button row instead — no anchor, since
nothing below it needs the space that anchor used to protect.
The middle-gap half of this item turned out to have a hard
geometric ceiling: Extra Buff Spells never moves (anchor="left
top bottom", pinned at x=4..324) but Blacklisted Buff Families
tracks the growing right edge (anchor="right"), sitting at
x=524 at the panel's own tested floor (856x236, PanelIsResizable
FlooredAtThePreRoundDAuthoredSize) and x=652 at the 984-wide
enlarged default — a 128px swing. The extras strip (182px wide)
is a single FIXED position, so it can only be centered relative
to ONE of those two widths; centering it against the 984 default
(x=397) put its right edge at 579, 55px INSIDE where the same
list sits at the floor (524) — a real overlap
ResolvedMainPanelHasNoOverlapOrOutOfBoundsChildAtThisSize's own
856x236 case catches. x=333 is the strip's floor-centered
position (9px margin on both sides against the floor's 324..524
gap, the widest safe position a fixed strip can take) — the same
position fix round B item 2 effectively already used (x=332, 1px
off from exactly centered) before Round D's enlargement existed.
The residual asymmetric gap at the 984 default (previously ~8/138,
now ~9/137) is therefore an accepted, unavoidable-without-a-
redesign limitation — the same "dead margin, polish later" trade
the resemblance re-check already accepted for six other tabs at
this same enlarged size. -->
<group x="8" y="42" w="976" h="229" visible="{BuffsVisible}" anchor="left top right bottom">
<!-- Fix round B item 14 (owner honesty rule): BuffPlan.Build does not
read this set at all yet — a real, tracked wiring gap (see
@ -616,39 +645,39 @@
onchange="{DeleteBlacklistedBuffFamilyAt}" tooltip="Click a row to remove it." />
<button x="652" y="173" w="120" h="18" text="Add..." onclick="{ShowBlacklistedBuffPicker}" anchor="right bottom" />
<toggle x="332" y="0" w="180" h="16" text="Trained skills only"
<toggle x="333" y="0" w="180" h="16" text="Trained skills only"
checked="{TrainedOnlyEnabled}" onclick="{ToggleTrainedOnly}" />
<toggle x="332" y="16" w="180" h="16" text="Attributes"
<toggle x="333" y="16" w="180" h="16" text="Attributes"
checked="{AttributesEnabled}" onclick="{ToggleAttributes}" />
<toggle x="332" y="32" w="180" h="16" text="Protections"
<toggle x="333" y="32" w="180" h="16" text="Protections"
checked="{ProtectionsEnabled}" onclick="{ToggleProtections}" />
<toggle x="332" y="48" w="180" h="16" text="Weapon auras"
<toggle x="333" y="48" w="180" h="16" text="Weapon auras"
checked="{AurasEnabled}" onclick="{ToggleAuras}" />
<toggle x="332" y="64" w="180" h="16" text="Armor banes"
<toggle x="333" y="64" w="180" h="16" text="Armor banes"
checked="{BanesEnabled}" onclick="{ToggleBanes}" />
<toggle x="332" y="80" w="180" h="16" text="Regeneration"
<toggle x="333" y="80" w="180" h="16" text="Regeneration"
checked="{RegenerationEnabled}" onclick="{ToggleRegeneration}" />
<toggle x="332" y="96" w="180" h="16" text="Other self-spells"
<toggle x="333" y="96" w="180" h="16" text="Other self-spells"
checked="{OtherEnabled}" onclick="{ToggleOther}" />
<label x="332" y="116" w="140" text="{DifficultyText}" color="#FFE8DEC3" />
<button x="472" y="112" w="20" h="18" text="-" onclick="{DifficultyDown}"
<label x="333" y="116" w="140" text="{DifficultyText}" color="#FFE8DEC3" />
<button x="473" y="112" w="20" h="18" text="-" onclick="{DifficultyDown}"
tooltip="Decrease the skill-over-difficulty margin." />
<button x="494" y="112" w="20" h="18" text="+" onclick="{DifficultyUp}"
<button x="495" y="112" w="20" h="18" text="+" onclick="{DifficultyUp}"
tooltip="Increase the skill-over-difficulty margin." />
<label x="332" y="136" w="140" text="{RebuffText}" color="#FFE8DEC3" />
<button x="472" y="132" w="20" h="18" text="-" onclick="{RebuffDown}"
<label x="333" y="136" w="140" text="{RebuffText}" color="#FFE8DEC3" />
<button x="473" y="132" w="20" h="18" text="-" onclick="{RebuffDown}"
tooltip="Rebuff later." />
<button x="494" y="132" w="20" h="18" text="+" onclick="{RebuffUp}"
<button x="495" y="132" w="20" h="18" text="+" onclick="{RebuffUp}"
tooltip="Rebuff earlier." />
<button x="332" y="154" w="110" h="18" text="{BuffButtonText}" onclick="{Buff}" />
<label x="446" y="158" w="70" text="{BuffStatus}" color="#FFC7B98F" />
<!-- Round D item 4: the "bottom-fit rule" the original comment names
(194-16) now targets the enlarged group's own bottom edge
(229-16), anchor="bottom" so a further manual resize keeps it
pinned there. -->
<label x="332" y="213" w="184" h="16" text="{Coverage}" color="#FF9B9072" anchor="bottom" />
<button x="333" y="154" w="110" h="18" text="{BuffButtonText}" onclick="{Buff}" />
<label x="447" y="158" w="70" text="{BuffStatus}" color="#FFC7B98F" />
<!-- Round F item 6: sits right under the button row above (154+18=172,
+4px) instead of anchored to the group's bottom edge — the
anchor used to leave a growing empty hole (55px at the enlarged
984-wide default) between this label and the row it belongs to. -->
<label x="333" y="176" w="184" h="16" text="{Coverage}" color="#FF9B9072" />
</group>
<!-- Route: VTank's own 2-across button grid, row order, and bottom
@ -657,7 +686,13 @@
verbatim, +4px left margin for our own convention). The waypoint
list is VTank's own clWP/clWPc 2-column grid (Campaign VT S7.5;
PluginCore.cs:3576-3599 — any cell click deletes the waypoint).
PITCH: text 330+7=337, count (last column) auto.
PITCH: text was 330+7=337, count 23+7=30. Round F item 6 narrowed
text to 324 (337+30=367 already exceeded this list's own
w=370 minus the scrollbar's 16px reservation — 354 — so count
was clamped to 17px the moment the list actually needed to
scroll) and appended a third, trailing filler column so count is
no longer the grid's LAST column and keeps its declared 30px
honored instead of silently absorbed/starved.
MossTank-only extras that have no VTank Route-tab counterpart moved
to the Advanced Options popup's own "MossTank Extras" section:
@ -686,8 +721,19 @@
<list x="4" y="16" w="370" h="151" rowheight="17" anchor="left top bottom"
selected="{SelectedRouteWaypointIndex}" onchange="{SelectRouteWaypoint}"
tooltip="Click a waypoint to remove it.">
<column type="text" width="337" items="{RouteWaypointTextColumn}" onclick="{DeleteRouteWaypointAt}" />
<column type="text" width="324" items="{RouteWaypointTextColumn}" onclick="{DeleteRouteWaypointAt}" />
<column type="text" width="30" items="{RouteWaypointCountColumn}" onclick="{DeleteRouteWaypointAt}" />
<!-- Round F item 6: a trailing filler so the count column above is
no longer the grid's LAST column and keeps its own real 30px
width instead of silently absorbing whatever's left. The text
column is also narrowed 337->324: 324+30=354 exactly matches
this list's own w=370 minus the scrollbar's 16px reservation,
so the count column gets its full 30px whether or not the
scrollbar is showing — previously 337+30=367 already exceeded
354, so the count column was clamped to 17px the moment the
list actually needed to scroll, no matter which column was
"last". -->
<column type="text" width="*" items="{RouteWaypointFillerColumn}" onclick="{RouteWaypointFillerClick}" />
</list>
<menu x="4" y="171" w="80" h="16" items="{RouteModeNames}" anchor="bottom"

View file

@ -1460,6 +1460,58 @@ public sealed class MarkupListColumnsTests
Assert.Equal(new[] { 0 }, binding.ActionClicks);
}
private sealed class RouteShapedBinding
{
public IReadOnlyList<string> Names { get; } =
Enumerable.Range(0, 9).Select(static i => $"WP{i}").ToArray();
public IReadOnlyList<string> Counts { get; } =
Enumerable.Range(0, 9).Select(static i => i.ToString()).ToArray();
public IReadOnlyList<string> Filler { get; } = Array.Empty<string>();
public int Selected { get; set; } = -1;
public Action<int> Click => static _ => { };
}
/// <summary>
/// Round F item 6 (resemblance re-check: "Route's count column
/// squeezes to 17 when the list scrolls"). Mirrors the Route tab's
/// EXACT real shape (mosstank.xml: a 370px-wide list, columns
/// 324/30/*) with enough rows (9, past the 151/17≈8 visible-row
/// ceiling) to force the scrollbar — the count column's declared
/// 30px was previously only honored while the scrollbar was hidden;
/// with it showing, 337(the OLD text width)+30 already exceeded
/// 370-16=354, clamping the count column to 17px. Reflects into
/// UiMarkupList's own cached column layout (the same one
/// DrawColumns/OnEventColumns use) rather than measuring glyphs —
/// this is a pure geometry question, no font involved.
/// </summary>
[Fact]
public void RouteShapedGrid_CountColumnStaysThirtyPxWhenTheListScrolls()
{
var binding = new RouteShapedBinding();
const string xml =
"<panel x=\"0\" y=\"0\" w=\"400\" h=\"200\">" +
"<list x=\"0\" y=\"0\" w=\"370\" h=\"151\" rowheight=\"17\" " +
"selected=\"{Selected}\">" +
" <column type=\"text\" width=\"324\" items=\"{Names}\" onclick=\"{Click}\"/>" +
" <column type=\"text\" width=\"30\" items=\"{Counts}\" onclick=\"{Click}\"/>" +
" <column type=\"text\" width=\"*\" items=\"{Filler}\"/>" +
"</list></panel>";
var panel = MarkupDocument.Build(xml, binding, Sprite);
var list = Assert.IsType<UiMarkupList>(panel.Children[0]);
var (renderer, ctx) = MakeContext(800f, 400f);
list.DrawSelfAndChildren(ctx);
System.Reflection.FieldInfo layoutField = typeof(UiMarkupList).GetField(
"_cachedLayout",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!;
var layout = ((float x, float w)[])layoutField.GetValue(list)!;
Assert.Equal(3, layout.Length);
Assert.Equal(30f, layout[1].w, 3);
}
private sealed class MonsterShapedBinding
{
public IReadOnlyList<bool> Checks { get; } = new[] { true, false };

View file

@ -254,6 +254,73 @@ public sealed class MossTankMarkupBuildOverRealFilesTests
}
}
/// <summary>
/// Round F item 6 (resemblance re-check: "Buffs' 136 px middle gap").
/// <see cref="ResolvedMainPanelHasNoOverlapOrOutOfBoundsChildAtThisSize"/>
/// already proves the extras strip never overlaps the Blacklisted
/// Buff Families list at the floor or the enlarged size (that generic
/// sibling-overlap pass is exactly what caught the original x=397
/// attempt at true 984-default centering colliding with the list's
/// own floor position, x=524). This is the narrower, POSITIVE claim
/// that overlap pass alone doesn't make: the strip is actually
/// CENTERED at the panel's floor (856x236) — the one width at which
/// centering and non-overlap can both hold, since Extra Buff Spells
/// never moves but Blacklisted Buff Families tracks the growing
/// right edge.
/// </summary>
[Fact]
public void BuffsExtrasStripStaysCenteredAndNonOverlappingAtTheFloor()
{
string xml = File.ReadAllText(
Path.Combine(MossTankMarkupDirectory, "mosstank.xml"));
var panel = new MossTankPanel(new StubHost());
UiNineSlicePanel built = MarkupDocument.Build(xml, panel, static id => (id, 32, 32));
UiPanel[] tabGroups = built.Children
.Where(static child => child.GetType() == typeof(UiPanel))
.Cast<UiPanel>()
.ToArray();
Assert.Equal(9, tabGroups.Length);
foreach (UiPanel group in tabGroups)
group.Visible = false;
UiPanel buffsGroup = tabGroups[6];
buffsGroup.Visible = true;
var device = new RecordingGpuDevice();
var renderer = new TextRenderer(device, new NullGpuFrameSource(), "unused");
renderer.Begin(new Vector2(1400f, 900f));
var ctx = new UiRenderContext(renderer, new Vector2(1400f, 900f));
// First draw at the authored default captures anchor baselines;
// only then resize down to the panel's own tested floor.
built.DrawSelfAndChildren(ctx);
built.Width = 856f;
built.Height = 236f;
built.DrawSelfAndChildren(ctx);
UiMarkupList[] lists = buffsGroup.Children.OfType<UiMarkupList>().ToArray();
Assert.Equal(2, lists.Length);
UiMarkupList leftList = lists.Single(static l => l.Left < 300f);
UiMarkupList rightList = lists.Single(static l => l.Left >= 300f);
UiMarkupToggle trainedToggle = Assert.Single(
buffsGroup.Children.OfType<UiMarkupToggle>(), static t => t.Text == "Trained skills only");
UiSimpleButton rebuffUpButton = Assert.Single(
buffsGroup.Children.OfType<UiSimpleButton>(), static b => b.Text == "+" && b.Top > 120f);
float leftMargin = trainedToggle.Left - (leftList.Left + leftList.Width);
float rightMargin = rightList.Left - (rebuffUpButton.Left + rebuffUpButton.Width);
Assert.True(
rightMargin >= 0f,
$"Extras strip (right edge {rebuffUpButton.Left + rebuffUpButton.Width}) "
+ $"overlaps the right list (left edge {rightList.Left}) at the floor.");
Assert.True(
MathF.Abs(leftMargin - rightMargin) <= 4f,
$"Extras strip not centered at the floor: left margin "
+ $"{leftMargin}px, right margin {rightMargin}px.");
}
private static void AssertResolvedWithinParent(UiElement parent)
{
foreach (UiElement child in parent.Children)

View file

@ -301,6 +301,102 @@ public sealed class MossTankMarkupContractTests
Assert.DoesNotContain("{ProfileNotice}", labelBindings);
}
/// <summary>
/// Round F item 6 (resemblance re-check nits: "Buffs' 136 px middle
/// gap and the orphaned Coverage label"). Coverage sits right under
/// the Buff button row instead of anchored to the group's bottom
/// edge, which used to leave a 55px empty hole above it as the panel
/// grew.
///
/// The middle-gap half has a hard geometric ceiling documented at
/// mosstank.xml's own Buffs comment: Extra Buff Spells never moves
/// (x=4..324) but Blacklisted Buff Families tracks the growing right
/// edge (anchor="right"), sitting at x=524 at the panel's own tested
/// FLOOR (856x236) and x=652 (its authored declaration below) at the
/// 984-wide enlarged default — a 128px swing this campaign's own
/// Round D item 4 introduced. A single FIXED-position strip can only
/// be centered relative to ONE of those two widths; centering
/// against the 984 default would put the strip's right edge INSIDE
/// where the list sits at the floor — a real overlap
/// ResolvedMainPanelHasNoOverlapOrOutOfBoundsChildAtThisSize's own
/// 856x236 case would catch. This pins centering against the FLOOR
/// value instead (declared x minus the 128px Round D growth delta —
/// 984-856) — the widest safe position a fixed strip can take,
/// verified against the real-file resolved tree at both sizes by
/// <c>MossTankMarkupBuildOverRealFilesTests.BuffsExtrasStripStaysCenteredAndNonOverlappingAtTheFloor</c>
/// in the App test project.
/// </summary>
[Fact]
public void BuffsExtrasStripIsCenteredAtThePanelsFloorAndCoverageSitsUnderTheButtonRow()
{
XDocument document = XDocument.Load(
Path.Combine(AppContext.BaseDirectory, "mosstank.xml"));
XElement root = Assert.IsType<XElement>(document.Root);
XElement buffsGroup = root.Elements("group")
.Single(static g => (string?)g.Attribute("visible") == "{BuffsVisible}");
XElement[] lists = buffsGroup.Elements("list").ToArray();
Assert.Equal(2, lists.Length);
float leftListRightEdge = Number(lists[0], "x") + Number(lists[0], "w");
// Round D item 4's own +128 growth: the declared x below is the
// 984-wide authored default; subtracting it back out gives the
// resolved position at the panel's tested floor (856x236).
const float roundDGrowthDelta = 984f - 856f;
float rightListLeftEdgeAtFloor = Number(lists[1], "x") - roundDGrowthDelta;
XElement trainedToggle = buffsGroup.Elements("toggle")
.Single(static t => (string?)t.Attribute("onclick") == "{ToggleTrainedOnly}");
XElement rebuffUpButton = buffsGroup.Elements("button")
.Single(static b => (string?)b.Attribute("onclick") == "{RebuffUp}");
float extrasLeft = Number(trainedToggle, "x");
float extrasRight = Number(rebuffUpButton, "x") + Number(rebuffUpButton, "w");
float leftMargin = extrasLeft - leftListRightEdge;
float rightMarginAtFloor = rightListLeftEdgeAtFloor - extrasRight;
Assert.True(
rightMarginAtFloor >= 0f,
$"Extras strip (right edge {extrasRight}) overlaps the "
+ $"Blacklisted Buff Families list at the panel's floor "
+ $"(left edge {rightListLeftEdgeAtFloor}).");
Assert.True(
MathF.Abs(leftMargin - rightMarginAtFloor) <= 4f,
$"Extras strip not centered at the panel's floor: left margin "
+ $"{leftMargin}px, right margin {rightMarginAtFloor}px.");
XElement buffButton = buffsGroup.Elements("button")
.Single(static b => (string?)b.Attribute("onclick") == "{Buff}");
XElement coverage = buffsGroup.Elements("label")
.Single(static l => (string?)l.Attribute("text") == "{Coverage}");
float rowBottom = Number(buffButton, "y") + Number(buffButton, "h");
float coverageGap = Number(coverage, "y") - rowBottom;
Assert.True(
coverageGap is >= 0f and <= 12f,
$"Coverage sits {coverageGap}px below the Buff button row — "
+ "expected <=12px, not the old 55px hole from anchoring it to "
+ "the group's bottom edge instead.");
}
/// <summary>
/// Round F item 6 (resemblance re-check: "Loot Editor: drop the
/// leftover 'Back' button — the window closes from its title bar
/// like the others"). The standalone button is gone;
/// CloseLootEditor stays a real, bound property (still called by
/// ToggleLootEditorVisible, the Profiles-tab checkbox's own close
/// path).
/// </summary>
[Fact]
public void LootEditorHasNoLeftoverBackButton()
{
XDocument document = XDocument.Load(
Path.Combine(AppContext.BaseDirectory, "mosstank-loot-editor.xml"));
XElement root = Assert.IsType<XElement>(document.Root);
Assert.DoesNotContain(
root.Elements("button"),
static b => (string?)b.Attribute("text") == "Back");
Assert.NotNull(typeof(MossTankPanel).GetProperty("CloseLootEditor"));
}
[Fact]
public void AuthoredShellFitsTheMinimumCanvasAndEverySizedChildFitsItsParent()
{