fix(mosstank): Buffs tab is VTank's own six controls, nothing more

Owner's third live look (2026-09-07): "Screenshot shows options under
the buffing tab circled in red. Those are made up, remove them from
here." VTank's real Buffs tab (docs/research/vtank-kb/08-ui-views.md
§1 "Tab: Buffs") is exactly two labels + two 320x116 lists + two
"Add..." buttons — nothing in the middle strip. The MossTank-only
extras block that used to live there (7 category toggles, the
difficulty-margin/rebuff-under spinners, a second Force/Stop buffing
button + status, and the "N attributes, N trained skills, N buff
lines" coverage counts) is removed outright from mosstank.xml.

The underlying settings are not lost: BuffSettings' seven category
bools, SkillExcessOverDifficulty, and RebuffWhenUnderSeconds stay in
the model and its JSON side-car, and the latter two are real,
already-editable VtankOptionCatalog entries
(SpellDiffExcessThreshold-Buff / RebuffTimeRemainingSeconds) reachable
from Advanced Options. Force Buff / Cancel Force Buff stay on the
Options tab, exactly where VTank has them.

Orphaned XML-only bindings deleted from MossTankPanel.cs: the seven
*Enabled/Toggle* pairs (TrainedOnlyEnabled/ToggleTrainedOnly,
AttributesEnabled/ToggleAttributes, ProtectionsEnabled/
ToggleProtections, AurasEnabled/ToggleAuras, BanesEnabled/ToggleBanes,
RegenerationEnabled/ToggleRegeneration, OtherEnabled/ToggleOther),
DifficultyText/DifficultyUp/DifficultyDown, RebuffText/RebuffUp/
RebuffDown, BuffButtonText, and the Buff action property (StartOrStop
itself stays — still reachable from the `/mt buff` slash command,
MossTankCommands.cs). BuffStatus and Coverage stay: both are still
exercised directly by MossTankPanelTests independent of any control
reading them, and neither is listed among the settings the owner's
report says must stay in the model, since neither backs a real
BuffSettings field — they're the "force-buff status" and "coverage"
strings the report never asked to keep alive as UI, only as state.

Test changes: MossTankMarkupContractTests' old
BuffsExtrasStripIsCenteredAtThePanelsFloorAndCoverageSitsUnderTheButtonRow
(which asserted the removed strip's geometry) is replaced with
BuffsTabIsVtankOnlySixControls, asserting the Buffs group is exactly
two lists + two "Add..." buttons + two labels, with no toggle, no
slider, and none of the deleted bindings anywhere in it. Shown to fail
first against a targeted mutation (a stray toggle re-added to the
group) before the fix was restored. MossTankMarkupBuildOverRealFilesTests'
BuffsExtrasStripStaysCenteredAndNonOverlappingAtTheFloor (which proved
the same now-deleted strip stayed centered at the panel's floor) is
deleted outright; the two remaining lists' own non-overlap/
within-bounds behavior stays covered generically by
ResolvedMainPanelHasNoOverlapOrOutOfBoundsChildAtThisSize.

MossTank suite: 726 -> 726 (one Fact replaced, same count). App
markup/plugin filter: 262 -> 261 (one Fact removed, none added here).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-07 21:11:42 +02:00
parent a2df6efaeb
commit 6be555ad47
5 changed files with 94 additions and 264 deletions

View file

@ -28,7 +28,11 @@ public sealed class BuffSettings
/// <summary>
/// How far the casting skill must exceed a spell's difficulty before the
/// tier is considered reliable — VTank's
/// <c>SpellDiffExcessThreshold-Buff</c>.
/// <c>SpellDiffExcessThreshold-Buff</c>, editable through Advanced
/// Options (VtankOptionCatalog). Signed on purpose — the wiki is
/// explicit that "a positive number raises the skill necessary to cast
/// spells, a negative number lowers it", so a lower-level character can
/// reach for higher tiers by going negative.
/// </summary>
public int SkillExcessOverDifficulty { get; set; } = 5;

View file

@ -426,7 +426,11 @@ internal sealed partial class MossTankPanel
}
// ── main panel bindings ───────────────────────────────────────────────
public Action Buff => StartOrStop;
// Campaign VT slice 7 — owner's third live look (2026-09-07): the
// Buffs tab's own "{Buff}"/"{BuffButtonText}" button (a redundant
// second Force/Stop control with no VTank counterpart) was deleted
// from mosstank.xml; StartOrStop itself stays, still reachable from
// the `/mt buff` slash command (MossTankCommands.cs).
public Action ForceBuff => StartForceBuff;
public Action CancelForceBuff => CancelForceBuffCore;
public Action ToggleCombat => ToggleMacro;
@ -695,8 +699,13 @@ internal sealed partial class MossTankPanel
ApplyAdvancedOptionCore();
};
/// <summary>The button is Force Buff; while a pass runs it cancels.</summary>
public string BuffButtonText => _running ? "Stop buffing" : "Force buff";
/// <summary>
/// The force-buff status. No longer read by any Buffs-tab control
/// (owner's third live look, 2026-09-07 — the Buffs tab's own
/// "{BuffStatus}" label had no VTank counterpart and is gone), but the
/// underlying <c>_status</c> state stays real and is exercised directly
/// by <c>MossTankPanelTests</c>.
/// </summary>
public string BuffStatus => _status;
public string CombatButtonText => _combat.ButtonText;
/// <summary>
@ -1578,7 +1587,13 @@ internal sealed partial class MossTankPanel
/// <summary>Vitals line, using the same numbers the character panel shows.</summary>
public string Vitals => _vitals;
/// <summary>What a buff pass would cover, named from the retail tables.</summary>
/// <summary>
/// What a buff pass would cover, named from the retail tables. No
/// longer read by any Buffs-tab control (owner's third live look,
/// 2026-09-07 — the count had no VTank counterpart and is gone), but
/// still exercised directly by <c>MossTankPanelTests</c>' allocation
/// and throttle pins.
/// </summary>
public string Coverage => _coverage;
// ── settings bindings ─────────────────────────────────────────────────
@ -1586,18 +1601,6 @@ internal sealed partial class MossTankPanel
// in plugin markup, whereas an editable field would need keyboard routing
// plumbed through to plugin panels first.
/// <summary>
/// VTank's <c>SpellDiffExcessThreshold-Buff</c>. Signed on purpose — the
/// wiki is explicit that "a positive number raises the skill necessary to
/// cast spells, a negative number lowers it", so a lower-level character
/// can reach for higher tiers by going negative.
/// </summary>
public string DifficultyText =>
$"Spell difficulty margin: {_buffSettings.SkillExcessOverDifficulty:+0;-0;0}";
public string RebuffText =>
$"Rebuff when under: {_buffSettings.RebuffWhenUnderSeconds / 60.0:0.#} min";
public string NormalHealthText => Percent(_vitalSettings.NormalHealth);
public string NormalStaminaText => Percent(_vitalSettings.NormalStamina);
public string NormalManaText => Percent(_vitalSettings.NormalMana);
@ -1626,13 +1629,6 @@ internal sealed partial class MossTankPanel
public bool VitalUpkeepEnabled => _vitalSettings.Enabled;
public bool HelpOthersEnabled => _vitalSettings.HelpOthers;
public bool TrainedOnlyEnabled => _buffSettings.BuffTrainedSkillsOnly;
public bool AttributesEnabled => _buffSettings.BuffAttributes;
public bool ProtectionsEnabled => _buffSettings.BuffProtections;
public bool AurasEnabled => _buffSettings.BuffAuras;
public bool BanesEnabled => _buffSettings.BuffBanes;
public bool RegenerationEnabled => _buffSettings.BuffRegeneration;
public bool OtherEnabled => _buffSettings.BuffOther;
// ── Buffs tab Extra/Blacklisted lists + picker (Campaign VT S7.4:
// VTank's own lstBuffSpells/lstAntiBuffSpells, docs/research/vtank-kb/
@ -1774,20 +1770,6 @@ internal sealed partial class MossTankPanel
public Action<float> SetHelperStaminaPercent => value => SetHelperStamina(value / 100f);
public Action<float> SetHelperManaPercent => value => SetHelperMana(value / 100f);
public Action DifficultyDown => () => UpdateProfile(() =>
_buffSettings.SkillExcessOverDifficulty =
Math.Max(-100, _buffSettings.SkillExcessOverDifficulty - 5));
public Action DifficultyUp => () => UpdateProfile(() =>
_buffSettings.SkillExcessOverDifficulty =
Math.Min(100, _buffSettings.SkillExcessOverDifficulty + 5));
public Action RebuffDown => () => UpdateProfile(() =>
_buffSettings.RebuffWhenUnderSeconds =
Math.Max(30, _buffSettings.RebuffWhenUnderSeconds - 30));
public Action RebuffUp => () => UpdateProfile(() =>
_buffSettings.RebuffWhenUnderSeconds =
Math.Min(1800, _buffSettings.RebuffWhenUnderSeconds + 30));
public Action ToggleVitalUpkeep => () =>
{
_vitalSettings.Enabled = !_vitalSettings.Enabled;
@ -1809,20 +1791,6 @@ internal sealed partial class MossTankPanel
ExpressionValue.Boolean(!_inventorySettings.ManaChargesWhenOff));
public Action ToggleHelpOthers => () => UpdateVital(() =>
_vitalSettings.HelpOthers = !_vitalSettings.HelpOthers);
public Action ToggleTrainedOnly => () => UpdateProfile(() =>
_buffSettings.BuffTrainedSkillsOnly = !_buffSettings.BuffTrainedSkillsOnly);
public Action ToggleAttributes => () => UpdateProfile(() =>
_buffSettings.BuffAttributes = !_buffSettings.BuffAttributes);
public Action ToggleProtections => () => UpdateProfile(() =>
_buffSettings.BuffProtections = !_buffSettings.BuffProtections);
public Action ToggleAuras => () => UpdateProfile(() =>
_buffSettings.BuffAuras = !_buffSettings.BuffAuras);
public Action ToggleBanes => () => UpdateProfile(() =>
_buffSettings.BuffBanes = !_buffSettings.BuffBanes);
public Action ToggleRegeneration => () => UpdateProfile(() =>
_buffSettings.BuffRegeneration = !_buffSettings.BuffRegeneration);
public Action ToggleOther => () => UpdateProfile(() =>
_buffSettings.BuffOther = !_buffSettings.BuffOther);
public Action CycleTargetMethod => () => UpdateProfile(() =>
_combatSettings.SelectionMethod = _combatSettings.SelectionMethod switch
{

View file

@ -566,58 +566,37 @@
<!-- Buffs: VTank's REAL Buffs tab (docs/research/vtank-kb/08-ui-views.md
§1 "Tab: Buffs") is ONLY two lists + two "Add..." buttons — Extra Buff
Spells and Blacklisted Buff Families, each VTank's own 320x116, at the
far left and far right of the 848-wide canvas (PluginCore.cs:
Spells and Blacklisted Buff Families, each VTank's own 320x116 (grown
+35 tall per Round D item 4's own font-driven scaling, matching every
other tab), at the far left and far right of the panel (PluginCore.cs:
7323-7355 — any cell click removes that row; "Add..." opens the
shared SelfBuffChoiceView-style picker, mosstank-buffpicker.xml).
Fix round B item 2 (owner's silhouette rule: VTank's own controls are
never shrunk/moved/reordered to make room for MossTank extras): the
category toggles / difficulty / rebuff / Buff-cast-now block below
(MossTank's own pre-existing extension, no VTank Buffs-tab
counterpart) used to live in the same two columns as the narrowed
lists, and its Difficulty "+" button (338,108) overlapped the Extra
Buff Spells "Add..." button (336,102) — a real sibling-overlap bug
the new AuthoredControlsInTheSameGroupNeverOverlap pin below now
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,
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. -->
Owner's third live look (2026-09-07): "Screenshot shows options under
the buffing tab circled in red. Those are made up, remove them from
here." The MossTank-only extras block that used to sit in the empty
middle strip between the two lists (7 category toggles, the
difficulty-margin and rebuff-under spinners, the Force/Stop buffing
button + status, and the "N attributes, N trained skills, N buff
lines" coverage counts — added by fix round B item 2, then repolished
by round F item 6) is gone outright: VTank's real Buffs tab has no
such block, and this campaign's own silhouette rule (fix round B item
2's own reasoning) cuts both ways — a VTank-authentic tab gets no
MossTank-only middle strip either. The underlying settings are not
lost: BuffSettings' seven category bools, SkillExcessOverDifficulty
and RebuffWhenUnderSeconds all stay in the model and its JSON
side-car, and the same two numbers are real, already-editable
VtankOptionCatalog entries (SpellDiffExcessThreshold-Buff,
RebuffTimeRemainingSeconds) reachable from Advanced Options. Force
Buff / Cancel Force Buff stay right where VTank has them, on the
Options tab (ForceBuff/CancelForceBuff, unchanged by this edit). The
XML-only bindings that fed the removed controls (the seven
*Enabled/Toggle* pairs, DifficultyText/Up/Down, RebuffText/Up/Down,
BuffButtonText, and the Buff action — StartOrStop itself stays, still
reachable from the `/mt buff` slash command) are deleted from
MossTankPanel.cs as orphans; BuffStatus and Coverage stay (both are
still exercised directly by MossTankPanelTests, independent of any
control reading them). -->
<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
@ -644,40 +623,6 @@
items="{BlacklistedBuffFamilyRows}" selected="{SelectedBlacklistedBuffIndex}"
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="333" y="0" w="180" h="16" text="Trained skills only"
checked="{TrainedOnlyEnabled}" onclick="{ToggleTrainedOnly}" />
<toggle x="333" y="16" w="180" h="16" text="Attributes"
checked="{AttributesEnabled}" onclick="{ToggleAttributes}" />
<toggle x="333" y="32" w="180" h="16" text="Protections"
checked="{ProtectionsEnabled}" onclick="{ToggleProtections}" />
<toggle x="333" y="48" w="180" h="16" text="Weapon auras"
checked="{AurasEnabled}" onclick="{ToggleAuras}" />
<toggle x="333" y="64" w="180" h="16" text="Armor banes"
checked="{BanesEnabled}" onclick="{ToggleBanes}" />
<toggle x="333" y="80" w="180" h="16" text="Regeneration"
checked="{RegenerationEnabled}" onclick="{ToggleRegeneration}" />
<toggle x="333" y="96" w="180" h="16" text="Other self-spells"
checked="{OtherEnabled}" onclick="{ToggleOther}" />
<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="495" y="112" w="20" h="18" text="+" onclick="{DifficultyUp}"
tooltip="Increase the skill-over-difficulty margin." />
<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="495" y="132" w="20" h="18" text="+" onclick="{RebuffUp}"
tooltip="Rebuff earlier." />
<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

View file

@ -254,73 +254,17 @@ 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.");
}
// Owner's third live look (2026-09-07) deleted the MossTank-only extras
// strip that used to sit between the Buffs tab's two lists (7 category
// toggles, the difficulty/rebuff spinners, the Force/Stop buffing
// button + status, the coverage counts) outright — VTank's real Buffs
// tab has no such block (see MossTankMarkupContractTests'
// BuffsTabIsVtankOnlySixControls for the authored-tree pin). The
// former BuffsExtrasStripStaysCenteredAndNonOverlappingAtTheFloor test
// that proved the (now-deleted) strip stayed centered at the panel's
// floor is gone with it; the two remaining lists' own non-overlap/
// within-bounds behavior at both the floor and the enlarged size stays
// covered generically by ResolvedMainPanelHasNoOverlapOrOutOfBoundsChildAtThisSize.
private static void AssertResolvedWithinParent(UiElement parent)
{
foreach (UiElement child in parent.Children)

View file

@ -302,32 +302,21 @@ public sealed class MossTankMarkupContractTests
}
/// <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.
/// Owner's third live look (2026-09-07): "Screenshot shows options
/// under the buffing tab circled in red. Those are made up, remove
/// them from here." VTank's real Buffs tab
/// (docs/research/vtank-kb/08-ui-views.md §1 "Tab: Buffs") is exactly
/// two labels + two lists + two "Add..." buttons, nothing in the empty
/// middle strip between the lists. The MossTank-only extras block that
/// used to live there (7 category toggles, the difficulty/rebuff
/// spinners, the Force/Stop buffing button + status, the coverage
/// counts — added by fix round B item 2, repolished by round F item 6)
/// is gone outright, matching this same file's
/// <see cref="ItemsTabIsVtankOnlyPlusTheAcceptedSliceOneRemoveButton"/>
/// precedent for the Items tab's own round-F removal.
/// </summary>
[Fact]
public void BuffsExtrasStripIsCenteredAtThePanelsFloorAndCoverageSitsUnderTheButtonRow()
public void BuffsTabIsVtankOnlySixControls()
{
XDocument document = XDocument.Load(
Path.Combine(AppContext.BaseDirectory, "mosstank.xml"));
@ -335,45 +324,25 @@ public sealed class MossTankMarkupContractTests
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;
Assert.Equal(2, buffsGroup.Elements("list").Count());
string[] buttonTexts = buffsGroup.Elements("button")
.Select(static b => (string?)b.Attribute("text") ?? string.Empty)
.ToArray();
Assert.Equal(["Add...", "Add..."], buttonTexts);
string[] labelTexts = buffsGroup.Elements("label")
.Select(static l => (string?)l.Attribute("text") ?? string.Empty)
.ToArray();
Assert.Equal(["Extra Buff Spells", "Blacklisted Buff Families"], labelTexts);
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.");
// Nothing else — no toggle, no slider, no MossTank-only status/
// difficulty/rebuff/coverage label ever made it back in.
Assert.Empty(buffsGroup.Elements("toggle"));
Assert.Empty(buffsGroup.Elements("slider"));
Assert.DoesNotContain("{BuffButtonText}", labelTexts);
Assert.DoesNotContain("{BuffStatus}", labelTexts);
Assert.DoesNotContain("{DifficultyText}", labelTexts);
Assert.DoesNotContain("{RebuffText}", labelTexts);
Assert.DoesNotContain("{Coverage}", labelTexts);
}
/// <summary>