merge(vt): slice 7 round G — Buffs tab is VTank's six controls; no tooltips in Advanced Options (owner's third look)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

# Conflicts:
#	docs/plans/2026-09-07-campaign-vt-slice7-tabs.md
This commit is contained in:
Erik 2026-09-07 21:13:47 +02:00
commit 43582efc4e
7 changed files with 144 additions and 271 deletions

View file

@ -154,3 +154,4 @@ re-review, merge to the campaign branch, then the owner's visual gate.
## Closeout
- 2026-09-07 21:10 final combined re-check (E+F, both lenses): all twelve items CLOSED with failable tests, no per-frame allocation regression, the three recall tables agree pairwise, plugin references BCL + Abstractions only — MERGE-READY. **Merged into the campaign branch at `4a825c23c`.** Owner's second-look items all closed (Advanced Options bottom block gone; string settings hidden; Items tab VTank-only). Accepted, documented deviations: the Buffs extras strip centres at the 856 floor only (anchors have no "centre"); the recall combo lists "Marketplace" in place of VTank's duplicated "Fac. Hub"; dead margins on six tabs at the 984 default. Carried polish (owner: "we do that later"): strip the retired `scroll="true"` from five markup sites and make its test failable or document that unknown attributes are ignored; delete the ~20 bindings round F orphaned; cache the two Buffs row sources like every other column; two comment corrections; Meta's delete cell as an icon. Open issues from this slice: #490 part 1 (host: `StartVisible=false` + `ShowInSidePanel=false` unshowable), #491 (buff lists not consumed by `BuffPlan.Build`), #492 (Vulkan device-loss crash + shutdown re-throw), #493 (in-game meta/loot rule editors, deferred). Owner's visual gate: the merged build launched from the gate worktree for the final look.
- 2026-09-07 21:30 OWNER THIRD LIVE LOOK at the merged build (`4a825c23c`): (1) the Buffs tab's middle block (seven category toggles, difficulty/rebuff spinners, Stop/Force buffing + status, the counts line) is "made up — remove them from here": VTank's Buffs tab is exactly two lists + two Add… buttons; the real settings behind two of them (`SpellDiffExcessThreshold-Buff`, `RebuffTimeRemainingSeconds`) stay editable in Advanced Options, Force Buff stays on Options; the MossTank-only category bools stay in the model only. (2) Advanced Options still shows tooltips — remove them (VTank's popup has none; the description box is the info channel). Round G dispatched on a fresh worktree off `a2df6efae`; merge + relaunch follow.
- 2026-09-07 owner's third live look, post-closeout: (1) "Screenshot shows options under the buffing tab circled in red. Those are made up, remove them from here" — the whole MossTank-extras middle strip fix round B item 2 added between the Buffs tab's two lists (7 category toggles, difficulty/rebuff spinners, a second Force/Stop buffing button + status, the coverage counts) is gone outright; the Buffs tab is now VTank's own exact six controls (2 labels, 2 lists, 2 "Add..." buttons), matching the Items-tab precedent from round F item 3. The underlying BuffSettings fields (seven category bools, SkillExcessOverDifficulty, RebuffWhenUnderSeconds) stay in the model/side-car and remain editable through Advanced Options (SpellDiffExcessThreshold-Buff / RebuffTimeRemainingSeconds); Force Buff / Cancel Force Buff stay on the Options tab. Twenty-two now-orphaned XML-only bindings deleted from MossTankPanel.cs (the seven Enabled/Toggle pairs, DifficultyText/Up/Down, RebuffText/Up/Down, BuffButtonText, the Buff action) — this is the "delete the ~20 bindings round F orphaned" carried-polish item from the closeout, done as part of this fix rather than separately; BuffStatus/Coverage stay since MossTankPanelTests still exercises both directly. Commit `6be555ad4`. (2) "Also still have tooltips in the advanced options, remove please" — the three tooltip= attributes mosstank-advanced.xml had grown (option list, category checklist, value field) are removed; VTank's real AdvancedOptionsView has no tooltip mechanism, only its own description readout. New whole-tree test AdvancedOptionsPopupHasNoElementWithATooltip guards against any future re-addition. Commit `617a9c221`. Both fixes' new/changed tests shown to fail first against targeted mutations (a stray toggle re-added to the Buffs group; a tooltip= reintroduced on the option list), then reverted and restored green. MossTank suite 726 -> 726 (one Fact replaced); App markup/plugin filter 262 -> 261 (item 1, one Fact removed) -> 262 (item 2, one Fact added). Full solution Release build green throughout.

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

@ -167,12 +167,19 @@
AddRouteCheckpoint/AddRouteJump themselves) — nothing was deleted from
the plugin, only the second, redundant UI surface for it. Panel height
shrinks 450->300 to match VTank's real 392x300 AdvancedOptionsView
footprint (minh follows); minw stays 392. -->
footprint (minh follows); minw stays 392.
Owner's third live look (2026-09-07): "Also still have tooltips in the
advanced options, remove please." VTank's own AdvancedOptionsView has no
tooltip mechanism at all — its own description readout
(AdvancedOptionDescription, the label at the bottom) is the retail
in-popup info channel, not a hover tooltip. The three tooltip= attributes
this popup had grown (the option list, the category checklist, the value
field) are removed; nothing else in this file ever set one. -->
<panel x="253" y="405" w="392" h="300" title="MossTank Advanced Options"
visible="{AdvancedOptionsVisible}" resizable="true" minw="392" minh="300">
<list x="4" y="4" w="256" h="160" rowheight="17" anchor="left top right"
selected="{SelectedAdvancedOptionIndex}" onchange="{SelectAdvancedOption}"
tooltip="Select one of Virindi Tank's 137 advanced options.">
selected="{SelectedAdvancedOptionIndex}" onchange="{SelectAdvancedOption}">
<column type="text" width="187" items="{AdvancedOptionNames}" onclick="{SelectAdvancedOption}" />
<column type="text" width="69" items="{AdvancedOptionValueColumn}" onclick="{ClickAdvancedOptionValue}" />
</list>
@ -181,15 +188,13 @@
pure checklist with no real selection concept — see
SelectedAdvancedOptionCategoryIndex's own doc comment. -->
<list x="268" y="4" w="120" h="180" rowheight="18" anchor="right"
selected="{SelectedAdvancedOptionCategoryIndex}"
tooltip="Uncheck a category to hide its settings from the list on the left.">
selected="{SelectedAdvancedOptionCategoryIndex}">
<column type="check" width="20" values="{AdvancedOptionCategoryEnabled}" onchange="{ToggleAdvancedOptionCategoryAt}" />
<column type="text" width="*" items="{AdvancedOptionCategoryNames}" onclick="{ToggleAdvancedOptionCategoryAt}" />
</list>
<field x="4" y="168" w="260" h="16" text="{AdvancedOptionValueDraft}"
onchange="{SetAdvancedOptionValueDraft}" onsubmit="{SubmitAdvancedOption}"
maxlength="160" clearonsubmit="false" background="#E6000000"
tooltip="Edit the selected advanced-option value and press Enter to apply." />
maxlength="160" clearonsubmit="false" background="#E6000000" />
<label x="4" y="188" w="384" h="80" text="{AdvancedOptionDescription}" color="#FFE8DEC3"
anchor="left top right" />
</panel>

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

@ -184,6 +184,43 @@ public sealed class MossTankMarkupBuildOverRealFilesTests
+ $"the repositioned category list (left edge {categoryList.Left}).");
}
/// <summary>
/// Owner's third live look (2026-09-07): "Also still have tooltips in
/// the advanced options, remove please." VTank's own AdvancedOptionsView
/// has no tooltip mechanism at all — its description readout is the
/// retail in-popup info channel, not a hover tooltip. Builds the real
/// mosstank-advanced.xml against a real panel (the same shape every
/// other test in this file uses) and walks the WHOLE resolved tree —
/// not just the three elements that used to carry tooltip= — so a
/// tooltip added anywhere else in this popup in the future fails this
/// pin too. <see cref="MarkupDocument.Build"/> only ever sets
/// <see cref="UiElement.RuntimeTooltipTextSource"/> when the source
/// element declared a real tooltip= attribute (MarkupDocument.cs's own
/// ApplyCommonAttributes), so a null source function here means no
/// tooltip was ever bound for that element.
/// </summary>
[Fact]
public void AdvancedOptionsPopupHasNoElementWithATooltip()
{
string xml = File.ReadAllText(
Path.Combine(MossTankMarkupDirectory, "mosstank-advanced.xml"));
var panel = new MossTankPanel(new StubHost());
UiNineSlicePanel built = MarkupDocument.Build(xml, panel, static id => (id, 32, 32));
AssertNoElementHasATooltip(built);
}
private static void AssertNoElementHasATooltip(UiElement element)
{
Assert.True(
element.RuntimeTooltipTextSource is null,
$"{element.GetType().Name} carries a tooltip — the Advanced "
+ "Options popup must have none (owner's third live look).");
foreach (UiElement child in element.Children)
AssertNoElementHasATooltip(child);
}
/// <summary>
/// D-6 (round E architecture re-check): every real-file re-layout pin
/// so far only inspects ONE named element after a resize (the
@ -254,73 +291,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>