Commit graph

4831 commits

Author SHA1 Message Date
Erik
5318adbb30 fix(vtank): slice 7 round D item 3 — VTank's full 26-recall table
Owner live report 2026-09-07: "Under Route, Add recall is missing a
lot of recalls that VTank has and they do not work in routes yet."
RouteRecallKind's old 4-entry model (Lifestone/Marketplace/
PrimaryPortal/SecondaryPortal) is replaced with VTank's real
cmbRecallType table: 26 castable recall spells from metaf's own
NRecall table (metaf_monolithic.py:10981-11008) plus Marketplace
Recall, VTank's one entry with no spell (issued as /marketplace).

Deviation (no refs/vtank/ checkout in this worktree to confirm VTank's
real combo layout directly): the old Lifestone member, which issued
"/lifestone" as a slash command, is DROPPED rather than kept alongside
the new spell-based LifestoneRecall (1635) — VTank's real combo has
ONE "Lifestone Recall" entry and it is a real castable spell, so
keeping both would show two menu rows reading "Lifestone Recall" with
different behavior. Marketplace is the one member kept as a slash
command per the explicit "keep ours" instruction. PrimaryPortal/
SecondaryPortal are renamed to PrimaryPortalRecall/SecondaryPortalRecall
and now use their real spell ids (48, 2647) instead of the old runtime
KnownSelfBuffs name lookup. This reshuffles the enum's underlying
ordinals; the one place that mattered (MossTankRouteProfileStore's
legacy pre-cutover JSON migration DTO, which stores Recall as a raw
int) already guards with Enum.IsDefined and is documented as a
migration-only path for not-yet-migrated files — its fallback default
moved from the deleted Lifestone to PrimaryPortalRecall.

Changes:
- RouteRecallKind: 26 named members in VTank's own combo order (values
  are plain sequential indices, not spell ids, so Enum.GetNames/
  GetValues — which sort by underlying VALUE — reproduce that order)
  plus Marketplace appended last.
- RouteWaypoint.RecallDisplayName: VTank's exact label text per kind.
- RouteWaypoint.SpellIdForRecall (new): the real spell id per kind
  (Marketplace = 0, the existing "no spell" sentinel).
- NavigationController.SubmitRecall: unchanged fast path (RecallSpellId
  != 0 -> cast) now covers every recall added through the UI; the
  fallback for a waypoint with no recorded id resolves through
  SpellIdForRecall instead of the old runtime spell-name lookup, and
  Marketplace still falls through to "/marketplace".
- MossTankPanel.AddRouteRecallCore: populates RecallSpellId AND
  RecallSpellName on the new waypoint (matching what a metaf import or
  the binary .nav loader already produces), so a waypoint added from
  the Route tab's own combo executes identically to one round-tripped
  through a real route file.
- mosstank.xml: the recall <menu> comment updated; rows 4->7 now that
  scrolling through 27 entries is real, not grammar-only.

Tests added (NavigationTests.cs, MetafSerializerTests.cs):
- RouteRecallKindListsVTanksTwentySixRecallsInOrderPlusMarketplaceLast
  (the 26-entry order via Enum.GetNames).
- RecallNameAndSpellIdTablesAgree (27-case Theory: name<->id both ways;
  RouteRecallKind is internal so the Theory parameter is the public int
  ordinal, cast back inside the method — a public method cannot expose
  an internal-typed parameter, CS0051).
- RecallWaypointWithNonZeroSpellIdCastsThatSpell /
  RecallWaypointForMarketplaceSubmitsTheSlashCommandNotACast (execution,
  via a new FakeMagic tracking fake — FakeAutomation.Magic is now
  settable instead of always NoOpAutomationSurface).
- RecallNodeRoundTripsByNameAndResolvesTheRealSpellIdFromTheCatalog
  (three recalls through SaveNav/TryLoadNav with a new FakeSpellCatalog
  that actually knows the spells, proving both the name AND the
  resolved id survive the .af round trip).

Mutations shown to fail, then reverted: swapping the first two enum
members failed the order test; reducing SubmitRecall to `return false`
failed both execution tests (no cast recorded, "/marketplace" not
submitted).

Verified: dotnet build AcDream.slnx -c Release green; MossTank suite
713/713 (682 -> 713, 31 new tests); App markup/plugin filter 242/242
(unchanged).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 16:54:37 +02:00
Erik
435ced86f7 fix(vtank): slice 7 round D item 2 — VTank's Advanced Options click model
Owner live report 2026-09-07, screenshots 2/3 (relayed VTank click
model — this worktree has no refs/vtank/ checkout to read
db.cs:118-165 directly): clicking a VALUE cell used to always just
select the row, no matter the setting's type. VTank's own model
dispatches by declared type: a tBool value flips in place, a tEnum
value cycles to the next label, and only int/double/single/string
select the row and load the current value into the edit field below
for typing. Apply/Back buttons are gone entirely (retail's own
AdvancedOptionsView has neither; Enter on the field already applies
via its existing onsubmit) — the popup closes from its own title bar
or the Options tab's toggle, both already independent of this content.

Changes:
- VtankDefaultSettingsDatabase.SettingEnumValues parses VTank's own
  SettingsEnumInfo table (3 columns, 33 rows: Setting/Value/EnumValue)
  from the embedded .usd — the real per-setting enum code->label table
  (UseArcs: 1=No, 2=At Range, 3=Yes), not a hand-typed guess.
- MossTankPanel.DisplayAdvancedOptionValue shows the enum LABEL for
  Enum-typed settings in the value column instead of the raw stored
  integer.
- MossTankPanel.ClickAdvancedOptionValue dispatches to
  FlipAdvancedOptionBool / CycleAdvancedOptionEnum (wraps past the
  last entry) / SelectAdvancedOption by VtankOptionCatalog.DeclaredType.
- mosstank-advanced.xml: the value column's onclick is now
  {ClickAdvancedOptionValue}; Apply/Back buttons removed; the notice
  label and the whole "MossTank Extras" section below it moved up 26px
  to reclaim the space; panel height 476->450.
- Removed the now-unreferenced ApplyAdvancedOption wrapper property
  (ApplyAdvancedOptionCore is still used by the field's own onsubmit).

Mutations shown to fail: (1) reducing ClickAdvancedOptionValue to a
bare SelectAdvancedOption(index) call failed both the bool-flip test
(expected False, got True — the click never flipped it) and the
enum-cycle test (expected "At Range", got "No" — the click never
advanced); the numeric-select test correctly stayed green since
select-only is still its own expected behavior. Restored and
confirmed green.

Verified: dotnet build AcDream.slnx -c Release green; MossTank suite
682/682 (679 -> 682, three new interaction tests); App markup/plugin
filter 242/242 (unchanged, mosstank-advanced.xml's new footprint
392x450 updated in ExpectedPopupBounds).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 16:30:10 +02:00
Erik
ebe670adf5 fix(vtank): slice 7 round D item 1 — Advanced Options category names
Owner live report 2026-09-07: "Some Hex numbers with green button to
the right. What is that?" — the Advanced Options popup's category
filter checklist showed raw bitmask hex ("0x1", "0x2", ...) instead of
VTank's real category names (Misc, Recharge, MeleeCombat, SpellCombat,
Ranges, Navigation, Buffing, Crafting, Looting).

This worktree has no refs/vtank/ checkout, so VTank's own name/enum
string literal for each bit isn't directly readable. Derived the
mapping instead from real data already in the embedded .usd: for each
category name, docs/research/vtank-kb/01-settings-and-profiles.md §2
was searched for a setting whose Category column names EXACTLY that
one category (no `|` combination) — e.g. row 45 RandomHelperBuffs is
pure "Misc", row 18 AttackDistance is pure "Ranges" — then that
setting's own recorded bitmask was read back from
VtankDefaultSettingsDatabase.SettingCategoryBitmasks (never a typed-in
hex literal), so the mapping tracks the shipped database instead of
silently drifting from it. All 9 of VtankOptionCatalog.CategoryBits
resolve to a real name this way (VtankOptionCatalog.
CategoryNamesByBit), in VTank's own ascending-bit order.

Mutation shown to fail: reverting AdvancedOptionCategoryNames to the
old `$"0x{bit:X}"` projection failed the new
AdvancedOptionCategoryNamesShowRealNamesNotHexBitmasks test with
["0x1", "0x2", ...] instead of ["Misc", "Recharge", ...]; restored and
confirmed green.

Verified: dotnet build AcDream.slnx -c Release green; MossTank suite
679/679 (678 -> 679); App markup/plugin filter 242/242 (unchanged).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 16:21:57 +02:00
Erik
4ba0a557f6 merge(vt): bring in retail scrollbar chrome + resizable/anchor markup
Merges claude/latest-main-sync-497549 into the slice-7 panel worktree
so round D can build on both: b71a8ea37 (retail scrollbar chrome on
plain <menu> popups and overflowing <list>s — chat/inventory sprite
ids, always-scrollable single-column menu) and 2e63391cc (<panel
resizable minw minh> + anchor="left top right bottom" markup).

Conflicts resolved keeping both intents:
- Ledger (docs/plans/2026-09-07-campaign-vt-slice7-tabs.md): unioned
  both branches' entries into one chronological timeline instead of
  picking a side.
- docs/plugin-ui-markup.md: kept both attribute additions per element
  (slider min/max/style, menu scroll/style) AND anchor on every row.
- src/AcDream.App/UI/UiMarkupList.cs: kept this branch's fix round B
  item 10 (VVS HudList grids have no row-selection highlight) over the
  sync branch's older SelectedColor band draw in the <column> grid
  path — the legacy single-column list path is unaffected either way.
- src/AcDream.App/UI/MarkupDocument.cs: the auto-merge left two
  `Scrollable =` initializers on the same <menu> object (CS1912).
  Kept the sync branch's `Scrollable = true` (VTank's HudCombo is
  always a single scrolling column, never a wrapping grid) and
  dropped this branch's `Scrollable = B(el, "scroll", false)` opt-in,
  since the owner-driven always-scrollable design supersedes the
  S7.2 opt-in one. Updated MarkupDocumentTests.cs to match: removed
  Build_MenuWithNoScrollAttribute_KeepsScrollableFalse (asserted the
  now-false opt-in default) and
  Menu_Scroll_DrawsAPlainFlatThumbFillWhenTheMarkupItemCountOverflowsTheVisibleRows
  (asserted a flat DrawFill thumb; the scrollbar is sprite-chrome for
  every menu style now) — both fully superseded by
  Menu_Markup_IsAlwaysScrollable_WithRetailScrollbarChromeWired and
  UiMenuPlainStyleTests.Plain_OpenPopup_ScrollableOverflow_
  DrawsRetailScrollbarChrome_RowsStayPlain.

Verified: dotnet build AcDream.slnx -c Release green; MossTank suite
678/678; App markup/plugin filter 242/242 (241 before this commit's
test-file trim, +1 net from the merge's own new tests, 0 red).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 16:01:51 +02:00
Erik
9eeabb481d docs(vt): slice 7 ledger — fix round C landed; round D dispatched
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 15:49:48 +02:00
Erik
6b42fd68ed test(vtank): slice 7 fix round C item F13 — drop the control-count change detector
EveryInteractiveControlDeclaresARealHandlerBinding asserted an exact
Assert.Equal(156, controls.Length), backed by ~60 lines of running
commentary recording every markup edit that ever bumped the number. That
count was a pure change detector: it carried no signal the test's own
per-control handler/enabled loop couldn't already catch on its own, and
it forced an edit to THIS test file every time an unrelated tab gained
or lost a single control.

Replaced the exact count with Assert.NotEmpty(controls) — it still
guards the selector itself (a broken interactive-element-name filter
that matched nothing would otherwise pass the loop vacuously) — and kept
the real assertion (the per-control handler/enabled loop) unchanged.
Moved the removed count's full history into this same commit's ledger
entry in docs/plans/2026-09-07-campaign-vt-slice7-tabs.md, per item
F13's own instruction.

Mutation named: temporarily broke the controls selector (appended
`&& false` to the interactive-element filter) and confirmed
Assert.NotEmpty fails ("Collection was empty") before restoring it.

MossTank suite holds at 678 (no test added or removed, one assertion
replaced). Full solution build green; App markup/plugin filter 203/203.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 15:46:33 +02:00
Erik
e2cca20e23 docs(vt): slice 7 ledger — scrollbar chrome and resizable/anchor markup merged; round D queued
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 15:45:34 +02:00
Erik
2e63391cc4 merge(vt): resizable plugin panels (resizable/minw/minh) and anchor markup (owner: larger default, resizable)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 15:45:06 +02:00
Erik
b71a8ea377 merge(vt): retail scrollbar chrome on plain <menu> popups and overflowing <list>s (owner: same assets as chat/inventory)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 15:44:26 +02:00
Erik
c7fbbc5608 docs(ui): document resizable panels and anchor grammar
Adds the "Resizable panels and anchors" section to plugin-ui-markup.md:
the resizable/minw/minh attribute table for <panel>, the anchor grammar
for every element that now supports it, the AnchorEdges semantics
(left top default, left right / top bottom stretch, right/bottom pin-
and-move), group-relative child anchoring, and one worked example
(a resizable panel with a group and list that both stretch on drag).
Also updates the Elements table, the "every registered window" intro
paragraph, the ApplyCommon common-attributes paragraph, and the Testing
conventions section to reference the new grammar and its test coverage.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 15:43:28 +02:00
Erik
e9108277c4 feat: retail scrollbar chrome for markup <menu> overflow and <list> overflow
Owner live-client report 2026-09-07: "For scrollable dropdown or the meta
window we use the same assets as we do in for example chat or inventory
window."

<menu> markup wiring (MarkupDocument.cs): a plugin <menu> is now always
Scrollable (single-column, VTank HudCombo shape) instead of wrapping
overflow into more grid columns, with PopupScrollbarHideWhenDisabled=true
so the bar is entirely absent while the item count fits the "rows"
window. RetailScrollbarChrome.ApplyToMenuPopup wires the same chrome ids
the previous commit taught DrawScrollablePopupPlain to draw, for both
style="plain" and style="retail" markup menus.

<list> markup (UiMarkupList.cs / MarkupDocument.cs): a plugin <list>
(single-column or <column> multi-column) that overflows its own row
viewport now draws the retail scrollbar chrome at its right edge (VVS's
own placement, 16px wide) instead of being wheel-scroll-only with no
visible bar. The reserved 16px column only exists while rows actually
overflow, in both column-layout modes (ComputeColumnLayout receives the
already-shrunk width so the last/auto column absorbs the remainder
correctly); the bar is fully interactive (up/down arrows, track paging,
thumb drag) via a small UiScrollable projection kept in sync with the
list's own _topRow, which stays the single source of truth. Wheel
scrolling and a no-resolver hand-built list (draws nothing, no crash) are
unchanged.

Mutation shown to fail first: new
UiMarkupListScrollbarTests/MarkupDocumentTests cases were written against
pre-change UiMarkupList/MarkupDocument and failed (no scrollbar sprites
ever emitted since UiMarkupList had no SpriteResolve property at all, and
<menu> markup never set Scrollable) before the implementation landed;
after: SingleColumn_Overflowing_DrawsRetailScrollbarChromeAtRightEdge and
Columns_Overflowing_ReservesSixteenPixels_LastColumnShrinksAccordingly
pin sprite ids + exact reserved-width geometry,
*_ContentFits_DrawsNo(Scrollbar|ReservationLastColumnKeepsFullRemainder)
pin the no-overflow/no-bar case, *_UpArrowClick_ScrollsUpByOneRow and
ThumbDrag_MovesTopRowAndIsReadableByASubsequentClick pin interactivity via
a following row click resolving to the moved position (mirroring
MarkupListColumnsTests' own wheel-scroll pin), and the four new
MarkupDocumentTests menu cases pin Scrollable/PopupScrollbarHideWhenDisabled/
the six chrome-id properties plus an end-to-end open-popup draw for both
the overflowing (draws chrome) and non-overflowing (draws none) cases.
Every pre-existing MarkupListColumnsTests/MarkupDocumentTests case stays
green unchanged (none of their fixtures overflow their own viewport).

docs/plugin-ui-markup.md updated: the <menu> style paragraph and a new
<list> "Scrollbar" section describe the new chrome + auto-reservation, and
the PITCH-transcription guidance is corrected to say the 16px scrollbar
column is now automatic (no more manual fold-in/double-reservation advice).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 15:43:00 +02:00
Erik
67aba8c386 fix: plain <menu> popup scrollbar draws retail chrome, not a flat bar
Owner live-client report 2026-09-07: "For scrollable dropdown or the meta
window we use the same assets as we do in for example chat or inventory
window." The plain-style <menu> popup's scrollable-overflow scrollbar
(DrawScrollablePopupPlain / DrawPopupScrollbarPlain in UiMenu.cs) drew a
home-made flat 1px track + flat thumb instead of the gold track + up/down
arrow buttons + thumb the chat SpewBox and inventory UiItemList already
use through RetailScrollbarChrome. The owner only ever objected to the
retail ROW art (checkmark glyph, gradient panel) — the bar itself was
never in scope for the plain-row fix, so this change touches only the
scrollbar draw call and leaves the plain row rendering untouched.

DrawScrollablePopupPlain now calls the existing DrawPopupScrollbar helper
(the same procedural sprite-chrome draw VendorUiController/
ConfigOptionsPageController already use) whenever a SpriteResolve is
wired, falling back to the old flat DrawPopupScrollbarPlain only for a
hand-built UiMenu with no resolver at all. New
RetailScrollbarChrome.ApplyToMenuPopup(UiMenu) wires the same vertical
skin ids (Track/Up/Down/ThumbTop/Mid/Bot Normal) the chat/inventory
scrollbar uses onto a menu's own ScrollTrackSprite/etc properties.

Mutation shown to fail first: UiMenuPlainStyleTests's
Plain_OpenPopup_ScrollableOverflow_DrawsPlainTrackAndFlatThumb_NoDatArt
and Plain_ScrollablePopup_ContentFits_DrawsTrackWithNoThumb asserted
resolveCalls==0 and an all-fill scrollbar — both failed (6 resolve calls,
6 sprite quads instead of 0) against the new DrawPopupScrollbar call
before being rewritten to
Plain_OpenPopup_ScrollableOverflow_DrawsRetailScrollbarChrome_RowsStayPlain
and Plain_ScrollablePopup_ContentFits_DrawsNoScrollbarAtAll, which pin the
new sprite-chrome behavior (6 resolved ids on overflow: track, up, down,
thumb top/mid/bottom; 3 on content-fits: track+up+down, no thumb; 0 on a
menu built with no resolver) while re-asserting the rows are still plain
fills with zero retail row-sprite quads. Retail's own
RetailButtonArt=true popup path (DrawGridPopup/DrawScrollablePopup) is
untouched — its regression golden
(Retail_OpenPopup_DrawIsByteForByteUnchanged_RegressionGolden) still
passes byte-for-byte.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 15:42:25 +02:00
Erik
d20ad47c51 docs(vtank): slice 7 fix round C item F4 — cite issue #491 on the buff-wiring gap
BuffSettings.ExtraBuffSpellNames/BlacklistedBuffFamilyNames and their two
Buffs-tab tooltips already documented that BuffPlan.Build does not
consume these sets yet, but didn't name the tracked issue. Both field
doc comments and both tooltip strings now cite #491 (filed on the
campaign branch: "shown and persisted, not consumed by BuffPlan.Build
until slice 4"), so anyone hitting the gap has a concrete issue to read
instead of just a prose warning.

Text/comment-only change; no test behavior to pin. Full solution build
green, MossTank suite 678/678, App markup/plugin filter 203/203.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 15:42:09 +02:00
Erik
cd06f89e59 test(ui): cover resizable plugin windows through RetailWindowManager + persistence
Extends the resizable="true"/minw/minh markup grammar's coverage past
MarkupDocument's own parse tests to the two host seams a resizable
plugin panel actually flows through, proving no host-side wiring beyond
what MarkupDocument.Build already sets on the panel was needed:

- RetailWindowManagerTests: a resizable="true" markup panel accepts
  RetailWindowManager.ResizeTo within its minw/minh floor (and clamps
  to it below the floor); a plain (non-resizable) markup panel refuses
  — Width/Height unchanged and no Resized event, exactly today's
  fixed-size behavior.
- RetailWindowLayoutPersistenceTests: a resizable panel's dragged size
  round-trips through save/restore into a fresh session, and a saved
  size below the panel's CURRENT minw/minh floor (a legacy save, or a
  plugin update that raised its floor) clamps UP to the floor on
  restore rather than restoring the too-small legacy value.

Mutation proof: reverted MarkupDocument.cs to its pre-feature state and
reran the new tests — the two RetailWindowManagerTests cases failed
(the old UiNineSlicePanel ctor default of Resizable=true/MinWidth=40
let the "fixed" window resize and let the "resizable" window shrink
below the new floor), and the persistence floor-clamp case failed
(80x60 came back instead of clamping to 200x150). The plain
save/restore round-trip case passed either way — the old ctor default
was already resizable, so it exercises a real but coincidentally
already-covered path; kept for its own documentation value. Restoring
the implementation returns 256/257 (1 pre-existing unrelated skip) on
the full Markup/PluginSidePanel/RetailWindow/Anchor-filtered App suite
and 9/9 on the MossTank markup-filtered suite, both green.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 15:40:31 +02:00
Erik
0724761adb fix(vtank): slice 7 fix round C item F3 — discover secondary popups instead of hardcoding
SecondaryPopupPanelsFitTheirOwnBoundsAndEveryBindingResolves was a
[Theory] over a hand-maintained [InlineData] file list — every popup
added or renamed since (buffpicker, metaeditor) already needed a manual
row here, and a forgotten one would leave a new popup completely
uncovered with no test failure to flag it.

Converted to a single [Fact] that discovers popup files with the exact
same Directory.GetFiles(AppContext.BaseDirectory, "mosstank*.xml") glob
AuthoredControlsInTheSameContainerNeverOverlapASibling and
NoButtonAnywhereUsesTheUnrenderableArrowGlyphs already use (excluding
mosstank.xml itself, the main panel covered by its own tests). Expected
width/height per file now lives in an ExpectedPopupBounds dictionary; a
discovered file with no entry fails loudly, naming the file, instead of
silently going unchecked.

Mutation named: temporarily commented out the mosstank-buffpicker.xml
entry and confirmed the test now fails with the new "has no expected
width/height entry" message (rather than the old behavior, where an
unlisted file was simply never discovered) before restoring the entry.

MossTank suite 681 -> 678 (the four [InlineData] cases collapse into one
[Fact] with the same coverage; net -3 test count, not a coverage loss).
Full solution build green; App markup/plugin filter 203/203.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 15:40:26 +02:00
Erik
37055bddce fix(vtank): slice 7 fix round C items F7+F12 — materialize ItemHandsColumn once
ItemHandsColumn ran a fresh .Select(...).ToArray() over _itemRows, PLUS a
per-row BaseItemName suffix-strip to recover the undecorated name, on
every single retained-UI draw. Folded per item F12: RefreshItemEditors
now captures the raw SortedCombatItemNames() result once as
_itemBaseNames — the SAME array used both to build the decorated
_itemRows (append "   [no buffs]" where it applies) and to compute
_itemHandsColumn directly, so the "   [no buffs]" suffix has exactly one
definition (added going forward) instead of two (added in RefreshItemEditors,
parsed back off in the old ItemHandsColumn getter).

CycleItemHandsAtCore (the grid's own "click cycles handedness" cell
action) mutates _itemHandedness but did not call RefreshItemEditors —
unlike the Monsters grid's mutators, which all already refresh after
mutating. Added the call so the cached column stays correct; also
switched it to read the cached _itemBaseNames instead of a second
SortedCombatItemNames() call.

Mutation named: temporarily restored the old live-recomputing
ItemHandsColumn getter (with a local copy of the suffix-stripping helper)
and confirmed the new
ItemHandsColumnDoesNotReallocateOnEveryReadAndNoBuffSuffixNeverLeaks pin
fails (Assert.Same throws — different array instances per read) before
restoring the fix. The same test also proves the no-buffs case still
resolves handedness correctly through the folded base-name array.

MossTank suite 680 -> 681 (one new pin). Full solution build green; App
markup/plugin filter 203/203.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 15:37:26 +02:00
Erik
3a3b40fab5 feat(ui): add resizable/minw/minh + anchor grammar to plugin markup
Owner direction (2026-09-07): plugin panels need a bigger default size
and real resizability. Plugin markup panels were fixed-size with no way
to opt in to drag-resize, and only <meter> exposed an anchor attribute
(comma-separated, silently dropping unknown tokens) — no other element
could stretch or reposition when its window resized.

<panel resizable="true" minw= minh=> is now the explicit opt-in (default
false — a panel with none of these attributes gets Resizable=false,
ResizeX=false, ResizeY=false, matching every plugin panel shipped today,
e.g. mosstank.xml's resize="none"). resizable="true" arms both axes and
defaults the min size to the authored w/h so a resizable panel never
shrinks below the layout its author tested; the pre-existing resize=
attribute still narrows to one axis on top of that.

anchor="left top right bottom" (space-separated, case-insensitive) now
applies uniformly via ApplyCommon to every element (<group>, <list>,
<menu>, <field>, <label>, <button>, <icon>, plus <meter>/<tab>/<toggle>/
<slider> for free) instead of just <meter>'s own comma-separated,
non-throwing parse. An unknown token now throws FormatException naming
the element, matching this file's "malformed markup throws at Build"
convention everywhere else. No new plumbing is needed for live re-layout
or group-relative child anchoring — UiElement.ApplyAnchor/AnchorEdges
already measure a child's margins against its own direct Parent's
Width/Height every draw, and RetailWindowManager.ResizeTo/UiRoot's
existing edge-drag resize already respect Resizable/ResizeX/ResizeY/
MinWidth/MinHeight generically for any registered window.

Mutation proof: reverted MarkupDocument.cs to its pre-change state and
reran the 25 new MarkupResizableAnchorTests — 17 failed (the anchor
grammar, resizable/minw/minh parsing, live re-layout, and golden-draw
tests), 8 passed trivially (cases asserting the unchanged no-attribute
default). Restoring the implementation turned all 25 green with no
regression in the existing 227 Markup/PluginSidePanel/RetailWindow/
Anchor-filtered tests (252/253, 1 pre-existing unrelated skip).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 15:35:14 +02:00
Erik
3b8d021946 fix(vtank): slice 7 fix round C item D3 — bottom-band pin counts implicit widget height
AssertWithinParent only flagged a child crossing a group's bottom edge
when that child declared its own h attribute — Number(child, "h") reads
0 for an absent attribute, so an unsized <label>/<field>/<toggle>/
<button> positioned right against a group's bottom edge silently passed
even though the retained-UI runtime still gives it a real default row
height at draw time. Added EffectiveHeight: falls back to each widget
kind's own implicit default when h is absent (label/field 16, toggle 20,
button 16 as a defensive floor since real buttons in this markup range
16-25px and always declare h explicitly); <list>/<menu> keep the old
"0 when absent" behavior since they have no implicit default at all.

Mutation named: temporarily reverted the check back to Number(child, "h")
and confirmed the new AssertWithinParent_CatchesAnUnsizedLabelNearTheBottomEdge
pin fails (no exception thrown for a synthetic <label y="190"> with no h
inside a 194-tall group, which should clip 12px past the bottom once the
16px default is counted) before restoring the fix. Audited every real
mosstank*.xml file for label/toggle/field/button elements missing h
(grep for each without ` h="`) — none exist, so this stricter check
introduces no new failures against the shipped markup.

MossTank suite 679 -> 680 (one new pin). Full solution build green; App
markup/plugin filter 203/203.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 15:30:43 +02:00
Erik
f15667db5f fix(vtank): slice 7 fix round C item D2 — /mt refresh now re-syncs the Monsters grid
EnsureDefaultMonsterRule() is a _combatSettings.Rules mutator (it appends
a DEFAULT row when the list is empty), but the /mt refresh command
handler never called RefreshMonsterEditor() afterward, unlike every other
Rules mutator in the plugin (AddMonsterRuleCore, DeleteMonsterRuleAtCore,
MoveMonsterRuleAtCore, UpdateMonsterActionsAt, the constructor, and
ResetProfileConsumers's own EnsureDefaultMonsterRule call).

Audited every _combatSettings.Rules mutation site in the plugin (grep for
"_combatSettings.Rules" across all of src/AcDream.Plugins.MossTank):
AddMonsterRuleCore, DeleteMonsterRuleAtCore, MoveMonsterRuleAtCore, and
UpdateMonsterActionsAt already call RefreshMonsterEditor right after
mutating; the constructor and ResetProfileConsumers already pair their
own EnsureDefaultMonsterRule call with one. The /mt refresh handler was
the only gap.

Mutation named: since Rules can never actually be observed empty through
the panel's own public surface (delete refuses removing DEFAULT, and
every profile-load path already re-adds it via ResetProfileConsumers
before this handler could see it), the new pin reaches the private
CombatSettings instance via reflection to clear Rules directly, and also
pokes the cached _monsterNameColumn field to a "STALE" sentinel first —
otherwise EnsureDefaultMonsterRule() re-adding "DEFAULT" would coincide
with the grid's already-cached construction-time value and the test
would pass even with the fix missing. Confirmed it fails (shows "STALE"
instead of "DEFAULT") with the RefreshMonsterEditor() call removed,
before restoring the fix.

MossTank suite 678 -> 679 (one new pin). Full solution build green; App
markup/plugin filter 203/203.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 15:27:33 +02:00
Erik
23d4376cc3 fix(vtank): slice 7 fix round C item D4 — stop leaking the category-filter array
AdvancedOptionCategoryEnabled returned _advancedOptionCategoryEnabled (the
panel's own mutable bool[]) directly. Any caller holding the returned
IReadOnlyList<bool> could cast it back to bool[] and flip category flags
without going through ToggleAdvancedOptionCategoryAt — bypassing the
clamp/cache-refresh that action performs. The property now returns a
ReadOnlyCollection<bool> view built once over the same backing array
(built once since the array's own reference never changes, only its
elements, so no re-allocation is needed on every read): a cast back to
bool[] now throws InvalidCastException instead of handing out a mutable
reference.

Mutation named: reverted the property to return the raw array and
confirmed the new
AdvancedOptionCategoryEnabledIsNotTheMutableBackingArray pin fails (no
exception thrown) before restoring the fix.

MossTank suite 677 -> 678 (one new pin). Full solution build green; App
markup/plugin filter 203/203.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 15:17:49 +02:00
Erik
bc273adcbd docs(vt): slice 7 ledger — owner's live look; App-side scrollbar/resizable work dispatched; panel round D queued
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 15:17:12 +02:00
Erik
466fac426a fix(vtank): slice 7 fix round C item D1 — materialize Advanced Options bindings once
AdvancedOptionNames, AdvancedOptionValueColumn, AdvancedOptionName, and
AdvancedOptionDescription each re-ran FilteredAdvancedOptionNames() (a
Where over the full 163-entry VtankOptionCatalog) on EVERY retained-UI
draw of the popup, and the value column additionally ran
GetMetaOption(name).ToDisplayString() per row (Trim()/ToLowerInvariant()
allocations) every frame. All four are now materialized once into fields
by a new RefreshAdvancedOptions(), called from every real mutation point:
category toggle, edit/apply, selection change, and profile
load/create/clear/delete (via ResetProfileConsumers), plus construction.
Same pattern fix round B item 12 used for the Monsters/Meta/Route grids.

AdvancedOptionValueColumnMirrorsTheLiveSettingValue exercised a settings
change made OUTSIDE the popup's own mutators (ToggleCombatEnabled, a
main Options-tab checkbox) and expected the value column to reflect it
on the very next read — a real behavior this caching model intentionally
narrows (mirrored settings now catch up at the next real popup mutator,
not on every read). Updated the test to re-select the row afterward,
exercising the "selection change" refresh point, and documented why.

Mutation named: reverted the two property getters to their old live-
recomputing form and confirmed the new
AdvancedOptionsPopupBindingsDoNotReallocateOnEveryRead pin fails
(Assert.Same throws — different array instances per read) before
restoring the fix.

MossTank suite 676 -> 677 (one new pin). Full solution build green.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 15:11:26 +02:00
Erik
b36b036475 docs(vt): slice 7 ledger — round B closed at 16/18, arch re-check verdict, round C dispatched, owner testing live
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 14:48:37 +02:00
Erik
8d3c6ad7c3 fix(vtank): slice 7 fix round B item 16 — small ones (dedupe, tab-switch cleanup, slider validation)
Three small, unrelated fixes bundled per the fix round's own item list
(the fourth, cropping the three popup screenshots to the panel, belongs to
the live-capture pass, item 17):

- SortedCombatItemNames(): one shared helper for the ordinal-sorted
  registered weapon roster, replacing five separate identical
  `_combatSettings.CombatItemNames.OrderBy(...).ToArray()` call sites
  (RefreshItemEditors, DeleteItemRowAtCore, CycleItemHandsAtCore,
  RemoveSelectedItemCore, CycleMonsterEquipmentAt).
- SelectTab already cleared LootEditorVisible/AdvancedOptionsVisible when
  the user switched tabs (fix round A). The buff picker (S7.4) and the
  Meta rule editor (item 5) are the same "own popup, own window" shape and
  were missing from that guard — switching away from Buffs or Meta while
  either popup was open left it orphaned, open over whatever tab the user
  switched to. Both flags now clear in SelectTab too.
- <slider min max>: max<=min used to silently fall back to a range of 1
  (the old `range == 0f` check, covering only max==min) or produce a
  slider whose drag direction is inverted from its declared range
  (max<min, never caught at all). Both are now a build-time
  FormatException, the same rule every other <slider>/<menu>
  attribute-format check in this file already follows.

New tests: SwitchingTabsClosesTheBuffPickerAndMetaEditorPopups,
Build_SliderWithMaxLessThanOrEqualToMin_Throws (both max<min and
max==min cases). Mutation checks: removing the two SelectTab clears
turned the first red ("Expected: False, Actual: True"); reverting the
slider validation to the old range==0f fallback turned both Theory cases
of the second red ("No exception was thrown"). Both restored to green.

tests/AcDream.Plugins.MossTank.Tests: 676/676 (was 675/675, +1).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 285/3 skipped/288 (was 283/3/286, +2: the new Theory's 2 cases).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 14:09:58 +02:00
Erik
7ca9785665 test(app): slice 7 fix round B item 15 — build every mosstank*.xml against a real MossTankPanel
Every existing MossTank markup pin (MossTankMarkupContractTests) validates
mosstank*.xml against MossTankPanel through reflection alone — "does a
public property with this name and this CLR type exist" — never through
MarkupDocument.Build itself, the code that actually mounts a plugin panel
at runtime. Build has its own validation a reflection-only check can't
see (attribute-format exceptions, delegate-shape mismatches, numeric-
attribute parsing, column-type dispatch). Before this test, a markup bug
of that shape would throw inside RetailUiRuntime.MountPlugins's own
try/catch and the panel would simply not appear — no test failure, no
visible error short of a live client screenshot.

New MossTankMarkupBuildOverRealFilesTests (AcDream.App.Tests) builds every
real mosstank*.xml file against a REAL MossTankPanel with a stub
IPluginHost (same shape as MossTankMarkupContractTests.StubHost) and a
fake sprite resolver. Required: a real compile-time ProjectReference from
AcDream.App.Tests to AcDream.Plugins.MossTank (test-only exception to the
plugin/host boundary — AcDream.App itself never links this assembly, see
its own ReferenceOutputAssembly=false copy target) plus
InternalsVisibleTo("AcDream.App.Tests") on the plugin project (MossTankPanel
is internal). The test project also globs mosstank*.xml from the plugin's
source directory into its own output, same pattern item 13 established.

This test caught a REAL bug on its first run: mosstank-advanced.xml's new
lFilterList (item 9's category checklist) had no selected="..." attribute.
MarkupDocument.BindRequiredIntReader throws "list selected must be an int
binding" when a <list>'s selected attribute is missing at all — a pure
checklist with no real "selected row" concept still needs one to satisfy
the markup grammar. Fixed by adding SelectedAdvancedOptionCategoryIndex
(always -1; item 10 already removed row-selection-band rendering for
every column grid, so this drives no visible highlight) and wiring
selected="{SelectedAdvancedOptionCategoryIndex}" onto that list.

tests/AcDream.Plugins.MossTank.Tests: 675/675 (unchanged — this item's
scope is entirely App-side plus one real bug fix invisible to MossTank's
own reflection-only pins).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 283/3 skipped/286 (was 278/3/281, +5: the new Theory's 5 file cases).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 13:58:18 +02:00
Erik
b0fee80e6f fix(vtank): slice 7 fix round B item 14 — persist Extra Buff Spells / Blacklisted Buff Families, reword tooltips
ExtraBuffSpellNames/BlacklistedBuffFamilyNames (Buffs tab, Campaign VT
S7.4) were never captured by SideCarDocument (the active JSON persistence
alongside the real VTank .usd) — a restart or profile switch silently
dropped them. BuffPlan.Build still does not READ either set when choosing
what to cast — that remains a real, separately tracked wiring gap (see
BuffSettings' own doc comments); this only fixes the storage/display
honesty the owner asked for.

- SideCarDocument gained BuffExtraSpellNames/BuffBlacklistedFamilyNames
  (string[]), captured via the same Sorted() helper CombatItemNames/
  ConsumableNames use, and applied via the same Replace() clear-then-
  repopulate helper — a profile switch can't carry a stale entry over from
  whichever profile was loaded before.
- PickBuffAtCore and DeleteExtraBuffAt/DeleteBlacklistedBuffFamilyAt now
  call SaveProfile(), matching every other Add/Delete mutator in this file
  — without this the newly-wired capture/apply would only fire
  opportunistically on some unrelated save.
- Reworded both tab tooltips ("Named spell exemplars added beyond the
  school-driven picks" / "Named buff families never cast, even if
  otherwise wanted") to state plainly that the lists are stored and shown
  but not yet used for casting, instead of implying they already affect
  cast selection.
- (False start, reverted: an earlier pass edited LegacyBuffProfileDocument,
  which the file's own comment marks migration-only dead code that nothing
  else writes any more — the real fix belongs in SideCarDocument, the
  active format SaveCurrent/LoadCurrent actually round-trip.)

New test: ExtraBuffAndBlacklistedFamilyNamesPersistAcrossSessions (add via
the picker in one panel, confirm both survive in a second panel sharing
the same storage). Mutation check: commenting out the two Replace() calls
in SideCarDocument.Apply turned it red ("Expected: [Spell 1], Actual: []");
restoring them turns it green.

tests/AcDream.Plugins.MossTank.Tests: 675/675 (was 674/674, +1).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 278/3 skipped/281 (unchanged).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 13:46:22 +02:00
Erik
169a6c6e32 perf(vtank): slice 7 fix round B item 12 — materialize grid columns once per mutation, not once per frame
Every retained-UI draw re-evaluates every markup-bound column property.
Several of these did real per-row work on EVERY frame instead of returning
a cached field:

- ExcludedComponentIcons (Consumables) called ResolveComponentIcon per row,
  which itself calls IAutomation.Items.CaptureOwnedItems() — a live
  inventory snapshot — per row per frame.
- All 23 Monsters-grid columns (14 flag columns via MonsterFlagColumn, 7
  text columns, 2 move-icon columns) re-ran a LINQ .Select(...).ToArray()
  over _combatSettings.Rules every frame; Weapon/Offhand additionally
  called ItemDisplayName (another CaptureOwnedItems() scan) per row.
- Meta's MetaStateColumn/MetaConditionColumn/MetaActionColumn/
  MetaDeleteColumn/MetaMoveUpIcons/MetaMoveDownIcons and Route's
  RouteWaypointCountColumn allocated a fresh array with LINQ every frame.

Fixed by materializing each into a field, computed once at the point of
actual mutation:

- ExcludedComponentIcons: computed in RefreshItemEditors (already the sole
  owner of _excludedComponentRows).
- RouteWaypointCountColumn: computed in RefreshRouteEditor (already called
  at 13 real mutation points, including profile load).
- Meta's 6 columns: computed in RefreshMetaEditor (already called at every
  real mutation point, including profile load).
- Monsters' 23 columns: new RefreshMonsterEditor, wired into the 4 actual
  Rules-mutating methods (AddMonsterRuleCore, DeleteMonsterRuleAtCore,
  MoveMonsterRuleAtCore, UpdateMonsterActionsAt — the last already covers
  every flag toggle and value-cycle action) plus panel construction and
  ResetProfileConsumers (profile load/switch). The now-unused
  MonsterFlagColumn helper is removed.

New tests (FakeAutomation gained a CaptureOwnedItemsCallCount counter):
ExcludedComponentIconsDoesNotScanLiveInventoryOnEveryRead,
MonsterGridColumnsDoNotScanLiveInventoryOrAllocateOnEveryRead (also asserts
Assert.Same across reads), MetaAndRouteGridColumnsDoNotReallocateOnEveryRead.
All three mutation-checked by temporarily reverting to the old per-read
computation: each turned red (call-count mismatch or Assert.Same failure)
against the reverted code; restoring the cached-field getters turns each
green.

tests/AcDream.Plugins.MossTank.Tests: 674/674 (was 671/671, +3 new tests).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 278/3 skipped/281 (unchanged — App-layer surface untouched by this item).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 13:32:40 +02:00
Erik
da42c1fce6 feat(app): slice 7 fix round B item 11 — plain <slider> style, mirroring <menu style>
Plugin markup's <slider> always drew RetailScrollbarChrome's sprite
track/thumb, the same "big gold DAT art next to a plain plugin panel"
mismatch <menu style> already fixed for dropdowns. <slider style="..."> now
uses the identical plain/retail grammar (ValidateArtStyle, renamed and
generalized from the menu-only ValidateMenuStyle): plain (the default)
draws a flat dark track, 1px border, and a small flat nub via new
UiScrollbar.RetailArt=false + DrawPlainScalar — no SpriteResolve dependency
at all; style="retail" keeps RetailScrollbarChrome.ApplyHorizontal exactly
as before. RetailArt defaults to true on UiScrollbar itself, so every
non-plugin caller of this widget (retail LayoutDesc import, chat opacity
sliders, etc.) is byte-for-byte unaffected — only <slider>'s own
MarkupDocument case sets it false by default.

Every existing MossTank <slider> (Vitals' nine sliders, Buffs, Items'
Refill Worn Mana) has no style attribute, so they all switch to the plain
look automatically — consistent with the whole campaign's "no gold art"
direction, no XML changes needed.

Fixed a red pin this change created: Slider_MinMax_DrawsTheThumbAtTheRescaledNormalizedPosition
asserted the retail sprite thumb on a slider with no style attribute, which
now builds plain by default — opted it into style="retail" (same fix
shape as item 1's menu-scroll pin) and added a plain sibling,
Slider_NoStyleAttribute_DrawsAPlainFlatNubAtTheRescaledNormalizedPosition.
Mutation check: hardcoding DrawPlainScalar's horizontal nub x to 0 turned
the new plain test red ("expected a plain flat nub offset right of the
origin at 25%"); restoring the real ScalarPosition-driven x turns it green.

Documented <slider style> in docs/plugin-ui-markup.md, mirroring the
existing <menu style> paragraph.

tests/AcDream.Plugins.MossTank.Tests: 671/671 (unchanged — pure App-layer
rendering change, MossTank markup only sets no/default style).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 278/3 skipped/281 (was 277/3/280, +1 new test).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 13:09:33 +02:00
Erik
ed5378d9b0 fix(app): slice 7 fix round B item 10 — no row-selection band on column grids
VVS's own HudList grids (Monsters/Meta/Route/Items and every other
<list><column> grid this codebase has, present and future) have no
row-selection highlight at all — only the per-cell click callbacks are a
real VTank concept. UiMarkupList.DrawColumns painted a SelectedColor band
under the selected row anyway (a holdover from the legacy single-column
list path, which keeps its own band unchanged — that's a plain list, not a
VVS grid). Removed the band draw from DrawColumns only; SelectedIndexSource
still drives scroll-into-view, and every onclick/onchange callback is
untouched.

New test: ColumnGrids_NeverDrawARowSelectionBand (a column list with a
real in-range selected index must never paint SelectedColor). Mutation
check: before the fix this test was RED against the real code
("expected no SelectedColor fill in a column-based grid"); after removing
the band draw it's green.

tests/AcDream.Plugins.MossTank.Tests: 671/671 (unchanged — this is an
App-layer fix, MossTank markup only consumes the existing <column>
grammar).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 277/3 skipped/280 (was 276/3/279, +1 new test).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 12:59:43 +02:00
Erik
328faea995 feat(vtank): slice 7 fix round B item 9 — Advanced Options gets clVal, lFilterList, and a real description readout
VTank's own AdvancedOptionsView (docs/research/vtank-kb/08-ui-views.md §1's
secondary-view table): lOptionList is clOpt(180)+clVal(62) two columns, not
a name-only list; lFilterList (268,4,120,180) is a category checklist
(check+text columns) filtering it; txtInfo (4,188,384,80) is a description
readout.

- lOptionList is now a real 2-column <list><column> grid: name (clOpt,
  PITCH 187) + live value (clVal, PITCH 69, AdvancedOptionValueColumn via
  the existing GetMetaOption/.ToDisplayString() pipeline).
- lFilterList genuinely filters lOptionList: VtankDefaultSettingsDatabase
  now parses VTank's own SettingsCategories table (136 rows straight from
  the embedded .usd, 2 columns: Setting name + Categories bitmask) into
  VtankOptionCatalog.CategoryBits (9 distinct bits, 0x01-0x100). A setting
  with no recorded bitmask always shows regardless of filter state. This
  worktree has no refs/vtank/ checkout, so VTank's own category NAME
  strings for those 9 bits aren't available anywhere in this repo — the
  checklist honestly labels each by its raw bit value ("0x04" etc.)
  instead of a guessed name.
- AdvancedOptionDescription surfaces VTank's own real Settings.Description
  column (93 of 137 rows non-empty, genuine retail help text — e.g.
  DoHelp's own wording about fellowship healing) prefixed with the option
  name, filling the exact 384x80 readout and replacing the old bare
  name-only label.
- SelectAdvancedOption/AdvancedOptionName now resolve through the FILTERED
  list (not the raw 137-row catalog) so selection stays correct as the
  filter narrows/widens it.
- Apply/Back move beside each other below the two lists to make room;
  popup height grows 392 -> 476 for the taller lists + description block,
  pushing item 8's "MossTank Extras" section further down (unchanged
  internally).

New tests: AdvancedOptionCategoryFilterHidesNonMatchingSettings (unchecking
every category but EnableLooting's own 0x100 hides EnableNav but keeps
EnableLooting; re-checking restores the full list),
AdvancedOptionDescriptionSurfacesRealRetailHelpText,
AdvancedOptionValueColumnMirrorsTheLiveSettingValue. All three mutation-
checked: disabling FilteredAdvancedOptionNames's filter predicate, the
description's name-prefix, and the value column's GetMetaOption call each
turned their test red; restoring each turns it green.

tests/AcDream.Plugins.MossTank.Tests: 671/671 (was 668/668, +3).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 276/3 skipped/279 (unchanged).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 12:54:49 +02:00
Erik
95c93858a1 feat(vtank): slice 7 fix round B item 8 — Route tab collapses to VTank's 2-across grid
Transcribed VTank's own Route tab almost verbatim (docs/research/
vtank-kb/08-ui-views.md §1 "Tab: Route", 20 controls, +4px left margin for
our own convention): the waypoint list, a bottom row (nav-type menu +
insert-mode menu + three nav image buttons), and a right-hand 2-across
button grid in VTank's own row order (Add/Open Vendor; Add Portal-NPC/Add
NPC Talk; Add Recall+its recall-type menu; Add Pause+field+"seconds"
label; Add Chat+field).

- cmbNavInsertMode is now a real 3-option <menu> (RouteInsertMode.AddToEnd/
  InsertAbove/InsertBelow, new enum in Navigation.cs) instead of the old
  2-state ToggleRouteAddPosition button — InsertAbove/InsertBelow are now
  genuinely distinct (before vs. after the selected waypoint), not both
  collapsed into "not append".
- The three nav image buttons (advance/regress/nearest-point, DAT ids
  0x060028FD/0x060028FC/0x060011F7) move from the waypoint-actions row to
  the bottom row beside the nav-type menu, matching VTank's own layout.
- "Use NPC" is recaptioned "Add NPC Talk" (VTank's own cmdNavUseNPC text),
  same AddRouteUseSelected binding.
- The pause duration is now a real editable field
  (RoutePauseSecondsFieldText, default "5", parsed/clamped 0-3600 by
  SetRoutePauseSecondsText) plus a "seconds" label, matching VTank's own
  txtPauseWaypointTime/Label52 — replaces the RoutePauseDown/RoutePauseUp
  stepper (removed).
- MossTank-only controls with no VTank Route-tab counterpart — Checkpoint,
  Jump, Remove, Set Follow Target + its status label, Follow Corners, Open
  Doors, Nav Priority (a real second copy of Options' own "Boost Nav.
  Priority"), and the Follow/Nav Min Distance +/- stepper (a real
  duplicate of Options' own editable field) — move to
  mosstank-advanced.xml's new "MossTank Extras" section (popup height
  300 -> 392 to fit them without touching the retail editor above).

New tests: RouteInsertModeControlsWhereANewWaypointLands (AddToEnd/
InsertAbove/InsertBelow each produce the correct insertion index),
RoutePauseSecondsFieldParsesAndClampsInput. Both mutation-checked:
hardcoding AddRouteWaypoint's insertion to always-append turned the first
red ("Point: (0N, 0E)" instead of containing "99"); removing the Math.Clamp
in SetRoutePauseSecondsText turned the second red ("99999" instead of the
clamped "3600"). Both restored to green.

EveryInteractiveControlDeclaresARealHandlerBinding: 166 -> 156 (mosstank.xml
Route tab 28 -> 18 controls; the 10 relocated/removed controls are either
gone or moved into mosstank-advanced.xml, a separate file this scan
doesn't cover).
SecondaryPopupPanelsFitTheirOwnBoundsAndEveryBindingResolves: mosstank-
advanced.xml's expected height 300 -> 392.

tests/AcDream.Plugins.MossTank.Tests: 668/668 (was 666/666, +2 new tests).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 276/3 skipped/279 (unchanged).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 12:31:01 +02:00
Erik
e7ed603e6e fix(vtank): slice 7 fix round B item 7 — Consumables left-list click removes, not selects
Retail ground truth (PluginCore.cs:7683-7700): a click on the Consumables
tab's left list removes that row directly — there is no select-then-press-
Remove step for that list, unlike our previous SelectConsumableRow which
only updated the selection index. SelectConsumableRow now sets the index
AND immediately calls RemoveSelectedConsumableCore, matching the "click
removes" convention the right-hand Excluded Scarab Types list already
uses; the "Remove" button (RemoveSelectedConsumable) stays as a second
path, same as Items/Buffs/Route. Retitled the list's tooltip to match
("Click a row to remove it.").

New test: ConsumablesLeftListRowClickRemovesTheRowDirectly. Mutation
check: commenting out the RemoveSelectedConsumableCore() call turned it
red ("The collection contained 2 items" instead of Assert.Single); restoring
it turns it green.

tests/AcDream.Plugins.MossTank.Tests: 666/666 (was 665/665, +1).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 276/3 skipped/279 (unchanged).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 12:11:56 +02:00
Erik
ce5652f524 fix(vtank): slice 7 fix round B item 6 — separate and centre the Monsters two-letter headers
Owner: "WC FC Cp DC Cs" run together. Measured each against the real
installed DAT font 0x40000000 (AcDream.Cli's temporary measure-text-temp
verb, same pen-advance formula as items 4/6): WC=19px, FC=14px, Cp=14px,
DC=17px, Cs=13px against a 20px column pitch. UiLabel draws raw/unclipped/
left-aligned text with no built-in centering, so all five previously drew
flush against their column's left edge with no visual separation from the
DAT text metrics.

Fix: each header gets its own x nudge (columnStart + round((20-width)/2))
and its declared w shrunk to its real measured width, centering it over
its own check column. The shrunk w also keeps AssertWithinParent/the new
sibling-overlap pin honest — a couple of these (nudged DC/Cs) would
otherwise have crossed into the Name column's x=280 start at the old w=20.

tests/AcDream.Plugins.MossTank.Tests: 665/665 (unchanged — pure geometry,
no behavior change; MonstersGridHasVtanksTwentyThreeColumnsInOrderWithRetailHeaderTooltips
keys off text content, not position, so it's unaffected).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 276/3 skipped/279 (unchanged).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 12:07:52 +02:00
Erik
06c9b95222 feat(vtank): slice 7 fix round B item 5 — Meta tab down to VTank's real 5 controls, rule editor as a popup
VTank's own Meta tab (docs/research/vtank-kb/08-ui-views.md §1 "Tab: Meta")
is only 5 controls: the lstMetaRules grid, cmdMetaCreate ("Create"), a bold
"Current State:" caption, and a SETTABLE cmbMetaCurrentState choice — no
profile toolbar, no "Enable Meta" checkbox, no inline editor at all.

- Removed from the Meta tab entirely: the profile menu/name-draft field/
  New/CopyTo/Clear/Delete row (Profiles already carries the Meta combo/
  CopyTo/Delete) and the "Enable Meta" toggle (already on Options) — same
  accepted-regression shape as fix round A's Macro/Nav CopyTo-name-field
  loss (no Profiles-tab equivalent exists for these, and VTank's own tab
  has none either).
- Moved the inline State/Condition/Action editor (fields, condition/action
  menus, numeric steppers, Apply/Remove/MoveUp/MoveDown) into a new popup,
  mosstank-metaeditor.xml, registered the same StartVisible=true/
  ShowInSidePanel=false way as the buff picker. Opened by a grid text-cell
  click (SelectMetaRule, still populating the draft via SelectMetaRuleCore)
  or the tab's own "Create" button (CreateMetaRule — new, distinct from the
  still-existing AddMetaRule the tests call directly); Apply and Cancel
  (HideMetaEditor) both close it. DeleteMetaRuleAt/MoveMetaRuleUpAt/
  MoveMetaRuleDownAt call SelectMetaRuleCore directly and do NOT open the
  popup.
- "Add" -> "Create": CreateMetaRule adds a default rule (delegating to the
  existing AddMetaRuleCore) and opens the editor so it isn't left silently
  default-valued.
- Added the settable current-state menu: MetaCurrentStateNames/
  SelectedMetaCurrentState/SetMetaCurrentState expose MetaEngine's own
  already-public States/Transition(string) — choosing a state here forces
  the live engine into it, matching VTank's own manual override.
- The grid returns to VTank's full-width 856x116 proportion (848x116 here)
  now nothing else shares the tab.
- Bold text isn't representable in the plain retail UI font (0x40000000 has
  no bold face); "Current State:" uses the same bright caption color other
  tab headers use instead — documented in mosstank.xml's own comment, not
  silently dropped.

New tests: MetaEditorPopupOpensOnCellClickOrCreateAndClosesOnApplyOrCancel,
MetaCurrentStateMenuForcesTheLiveEngineIntoTheChosenState. Both mutation-
checked: commenting out SelectMetaRule's `_metaEditorVisible = true` turned
the first red ("Expected: True, Actual: False"); commenting out
SetMetaCurrentState's `_meta.Transition(value)` turned the second red
("Expected: Hunt, Actual: Default"). Restoring both turns them green.

EveryInteractiveControlDeclaresARealHandlerBinding's pinned control count:
186 -> 166 (-7 controls removed for good, -14 moved into the new popup file
this scan doesn't cover, +1 the new current-state menu).
SecondaryPopupPanelsFitTheirOwnBoundsAndEveryBindingResolves gained
mosstank-metaeditor.xml (630x236... — 630x160, corrected below).

tests/AcDream.Plugins.MossTank.Tests: 665/665 (was 663/663, +2 new tests).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 276/3 skipped/279 (unchanged).

Screenshot with at least two rules (item 17) is owed with the round's other
live captures.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 12:05:27 +02:00
Erik
897e262eda refactor(vtank): slice 7 fix round B item 13 — glob mosstank*.xml everywhere it's named
Reordered ahead of items 5-12 on purpose: item 5 adds a fourth popup
(mosstank-metaeditor.xml) and would otherwise need the same three-file
manual edit this item retires. Every place a mosstank-*.xml file used to be
named one at a time now globs mosstank*.xml instead, so adding a new popup
panel needs exactly one new file, not four matching edits:

- AcDream.Plugins.MossTank.csproj: <None Update="mosstank*.xml"> replaces
  four separate <None Update> entries.
- AcDream.Plugins.MossTank.Tests.csproj: one globbed <None Include> with
  Link="%(Filename)%(Extension)" replaces four separate Include/Link pairs.
- AcDream.App.csproj: a new _MossTankPluginMarkup item
  (Include="…/mosstank*.xml") replaces the literal four-file semicolon list
  in both CopyMossTankPluginToBuildOutput and …ToPublishOutput's Copy
  SourceFiles.
- MossTankMarkupContractTests: AuthoredControlsInTheSameContainerNeverOverlapASibling
  (item 2's new pin) and NoButtonAnywhereUsesTheUnrenderableArrowGlyphs now
  iterate Directory.GetFiles(AppContext.BaseDirectory, "mosstank*.xml")
  instead of a hardcoded 4-file array/Theory. Left
  SecondaryPopupPanelsFitTheirOwnBoundsAndEveryBindingResolves alone — it
  pairs each file with its OWN expected w/h, which a glob can't supply.
- LinuxPlatformBoundaryTests.ShippedPluginCopiesUseResolvedTargetPathsForBuildAndPublish
  now asserts on the glob pattern instead of the literal mosstank.xml
  substring the old Copy SourceFiles list contained.

A missed file in any of these four places used to silently drop a panel at
mount (fix round A's own #missing-popup-files near-miss) instead of failing
the build; the glob makes that failure mode structurally impossible.

tests/AcDream.Plugins.MossTank.Tests: 662/662 (665 -> 662: the 4-case
Theory collapsed into 1 Fact with an internal loop — same coverage, 3 fewer
reported xunit tests).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 276/3 skipped/279 (unchanged).
Verified the glob actually copies all four files: `ls
src/AcDream.App/bin/Release/net10.0/plugins/AcDream.Plugins.MossTank/*.xml`
lists all four post-build.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 11:53:01 +02:00
Erik
e6e47dd98a docs(vt): post-campaign idea — readable export/diff for settings and loot, not a second storage format
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 11:51:14 +02:00
Erik
7f95c771db fix(vtank): slice 7 fix round B item 4 — widen Profiles' CopyTo column, align the four Delete buttons
Measured "CopyTo" against the real installed DAT font 0x40000000 (a
temporary AcDream.Cli measure-text-temp verb using the same pen-advance
formula UiDatFont.GlyphAdvance uses, HorizontalOffsetBefore+Width+
HorizontalOffsetAfter, then removed): 42px wide against a declared 40px
button. UiSimpleButton centers its caption without clipping, so the extra
2px split evenly past both edges — a real, if small, overflow on three
rows (Macro/Nav/Meta all use "CopyTo"; Loot uses "New" instead).

Fix: widen the CopyTo/New column from 40 to 50px (296->306 x, +10 gap
preserved) and column-shift every following control on all four rows by
the same +10 to preserve every original gap. While shifting, also aligned
the four "Delete" buttons (DeleteProfile/DeleteRouteProfile/
DeleteLootProfile/DeleteMetaProfile) into one shared column at x=566 —
they previously sat at four different x's (546/456/346/346) because each
row has a different control count in front of it; the shorter rows now
have a wider gap before Delete instead of four misaligned columns.

tests/AcDream.Plugins.MossTank.Tests: 665/665 (unchanged — no new
behavior, pure geometry; the existing fit/overlap pins from items 2/3
still cover this file and stay green).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 276/3 skipped/279 (unchanged).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 11:44:18 +02:00
Erik
687f420d02 fix(vtank): slice 7 fix round B item 3 — bottom-band clipping on notice labels
AssertWithinParent (MossTankMarkupContractTests) already checks a child's
y+h against its parent's declared height, but only when h>0 — most notice/
status labels have no explicit h attribute (Height defaults to 0 in
MarkupDocument.F), so the check silently never fired for them even though
they visually clip the tab's 194px content band.

Fix: give the two labels that actually overflow an explicit h="16" and
raise them so 194-16=178 is their max y (item 3's "a full 16px line fits"
rule) — Consumables' ProfileNotice (184 -> 178; also trimmed the "Add
Selected" button above it from h=25 to h=24 so the two touch at y=178
without overlapping) and Meta's MetaNotice (already at the correct y=178,
just needed the explicit w/h so the existing pin actually applies to it).
Buffs' Coverage label already landed at y=178/h=16 in item 2's Buffs
redesign. The Loot Editor popup's own bottom row (LootRangeText/-/+) was
already explicit and already fit (282+16=298<=300) — no change needed.

Mutation check: reverting ProfileNotice to y=184 turned
AuthoredShellFitsTheMinimumCanvasAndEverySizedChildFitsItsParent red
("<label> crosses the bottom edge of <group>"); restoring y=178 turns it
green again.

tests/AcDream.Plugins.MossTank.Tests: 665/665 (unchanged count — no new
pins added, the existing one now actually fires).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 276/3 skipped/279 (unchanged).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 11:36:27 +02:00
Erik
0c2c3b572d fix(vtank): slice 7 fix round B item 2 — restore VTank's Buffs silhouette, add sibling-overlap pin
Owner's silhouette rule: VTank's own controls are never shrunk/moved/
reordered to make room for MossTank extras. The Buffs tab's Difficulty "+"
button (338,108,26,20) overlapped the Extra Buff Spells "Add..." button
(336,102,120,16) because both VTank lists had been narrowed to 246/256x84
to coexist with MossTank's toggle/difficulty/rebuff/Buff-cast-now block in
the same two columns.

Fix: restore both lists to VTank's real 320x116 at the far left (x=4) and
far right (x=524) with their own 120-wide "Add..." row below each list.
Move the MossTank-only extras block (7 toggles, Difficulty +/-, Rebuff +/-,
the Buff button+status, Coverage) into the empty 184px middle strip between
the two lists that VTank's own layout never uses. Coverage moves to y=178
(194-16) per the upcoming bottom-fit rule.

Also fixed: a second real overlap the new pin found on the Options tab —
"Pet Min. Monsters:" (614,48,125,16) overlapped its own field
(734,48,40,16) by 5px; narrowed the label to the same 120px column width
every other label/field pair in that block uses.

Added AuthoredControlsInTheSameContainerNeverOverlapASibling (4 file cases)
plus a direct predicate pin (AssertNoSiblingOverlap_CatchesARealOverlap...)
proving the helper catches a real overlap, ignores touching edges, and
ignores two <group> tab-pages sharing one rectangle (mutually exclusive via
their own visible="{...}" binding) — the existing AssertWithinParent only
ever checked a child against its OWN parent's bounds, never against a
sibling.

Mutation check: setting DifficultyUp's button to (472,112) — the same rect
as DifficultyDown — turned AuthoredControlsInTheSameContainerNeverOverlapASibling
red against the real mosstank.xml ("<button> text='-' ... overlaps sibling
<button> text='+' ..."); reverting turns it green.

tests/AcDream.Plugins.MossTank.Tests: 665/665 (was 660/660, +5: the new
theory's 4 file cases + 1 predicate fact).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 276/3 skipped/279 (unchanged).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 11:33:08 +02:00
Erik
040d5f3d81 test(vtank): slice 7 fix round B item 1 — retire the red plain-menu-scroll pin
Menu_Scroll_DrawsAScrollbarWhenTheMarkupItemCountOverflowsTheVisibleRows was
RED at HEAD: <menu> now defaults to the plain style (RetailButtonArt=false,
the "BIG gold/yellow buttons" owner fix), so a markup menu with no style
attribute draws DrawScrollablePopupPlain's flat DrawFill thumb, never the
retail sprite path (DrawPopupScrollbar) the test asserted on.

Fix: give the existing test style="retail" so it keeps proving the retail
sprite path draws ScrollThumbSprite. Add a plain sibling
(Menu_Scroll_DrawsAPlainFlatThumbFillWhenTheMarkupItemCountOverflowsTheVisibleRows)
that proves the DEFAULT (no style attribute) path draws the flat thumb fill:
an untextured (texture=0) quad sized ScrollbarWidth-2 wide, tinted
PlainBorderColor. Untextured DrawFill calls all batch into one texture=0
render segment, so the assertion scans per-quad (6 verts x 8 floats) inside
each segment rather than treating a whole segment as one quad.

Mutation check: commenting out DrawPopupScrollbarPlain's thumb DrawFill call
turned the new test red ("expected a plain flat thumb fill... among the
drawn segments"); restoring the call turns it green again.

tests/AcDream.Plugins.MossTank.Tests: 660/660 (unchanged).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 276 passed / 3
skipped / 279 total (was 274 passed / 1 failed / 3 skipped / 278 total).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 11:24:12 +02:00
Erik
c62dc495a5 docs(vt): slice 7 ledger — both review verdicts, lead decisions, fix round B dispatched
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 11:13:00 +02:00
Erik
805d10e2b8 docs: file #490 (plugin-panel host StartVisible/ShowInSidePanel + layout revision) and #491 (buff lists not consumed by BuffPlan.Build)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 11:12:40 +02:00
Erik
327e0e93df docs(vt): slice 7 ledger — all nine tabs landed; lead's read of Route/Meta for fix round B; reviews dispatched
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 10:55:58 +02:00
Erik
dbdde0783d merge(vt): plain <menu> popup from the campaign branch into the slice-7 panel work (ledger union)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 10:54:25 +02:00
Erik
41fc1d88d1 docs(vt): slice 7 ledger — S7.4-S7.6 landed, fresh live screenshots
Recaptured Items/Consumables/Buffs/Route/Meta screenshots (stale since
before this sub-slice) plus the new buff picker popup against a live
local ACE, isolated ACDREAM_CONFIG_DIR/ACDREAM_DATA_DIR (fresh authored
window positions, no stale-persisted-layout override), and an
ACDREAM_UI_PROBE_SCRIPT route through the five changed tabs. All six
confirm plain controls, no overlapping captions, correct grid rendering,
and real DAT move-icon art. Records the three S7.4-S7.6 commit SHAs and
their deviations in the ledger.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 10:51:55 +02:00
Erik
cc323f6a59 feat(vtank): slice 7 S7.6 — Meta tab's real 6-column rules grid
VTank's own lstMetaRules grid (delete/move-up/move-down icon-ish cells +
State/Condition/Action text cells, docs/research/vtank-kb/08-ui-views.md
§1 "Tab: Meta"; PluginCore.cs:2218-2247) replaces the padded
single-column rules list: any State/Condition/Action cell click opens
the rule editor below (reusing the existing SelectMetaRule), the
up/down cells reorder using the same 0x060028FC/FD DAT icons the
Monsters and Route grids already established, and the delete cell is a
plain text "X" — no retail DAT delete-glyph id is confirmed anywhere in
this codebase (unlike the already-established move icons), and "X" is
ASCII the default retail font renders without the unrenderable-glyph
risk NoButtonAnywhereUsesTheUnrenderableArrowGlyphs guards against.

The new pin (contract control count 180->186) and the new panel test
were shown to fail against a targeted mutation before being confirmed
green. MossTank suite 659 -> 660; App markup/plugin filter holds 192/192.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 10:37:52 +02:00
Erik
6118062a7a feat(vtank): slice 7 S7.5 — Route tab's real waypoint grid
VTank's own clWP/clWPc 2-column waypoint grid (text + a 1-based position
counter standing in for VTank's own count column) replaces the padded
single-column list, with any-cell-click delete matching
PluginCore.cs:3576-3599. Also: the recall menu gets scroll="true" for
grammar parity (still 4 real recall kinds — see the deviation note
below); the nav-type menu now shows VTank's own "Follow" caption for the
Target mode via a pure display remap (SelectedRouteMode/SelectRouteMode
translate the string, RouteMode.Target itself is unchanged, zero
behavior change); and a third nav image button (icon 0x060011F7, "Select
Nearest Point") is added.

Deviations, documented at their own binding site:
- VTank's cmbRecallType lists 27 named retail recalls; MossTank's
  RouteRecallKind models 4 (Lifestone/Marketplace/Primary/Secondary
  Portal). Expanding to 27 needs real per-recall spell-id data, which is
  casting-algorithm behavior out of this UI-parity slice's scope.
- VTank's btnNavResetPoint reassigns the LIVE navigation cursor mid-route
  (a mutable index MossTank's navigation controller never exposes to a
  plugin); SelectNearestRouteWaypoint instead moves the tab's own EDIT
  selection to the closest waypoint by horizontal distance — real,
  testable UI-only behavior that stays out of live-navigation territory.

The new pin (contract control count 177->180) and the new panel test
were shown to fail against a targeted mutation before being confirmed
green. MossTank suite 658 -> 659; App markup/plugin filter holds 192/192.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 10:33:49 +02:00
Erik
f5409530f2 feat(vtank): slice 7 S7.4 — Items/Consumables/Buffs grids and picker
VTank's real Items (2-col name/hands), Consumables (adds the right-hand
"Excluded Scarab Types" icon+text grid), and Buffs (Extra Buff Spells /
Blacklisted Buff Families lists + a shared SelfBuffChoiceView-style
picker popup) tabs are transcribed from docs/research/vtank-kb/
08-ui-views.md §1, replacing the old single-column-list adaptations with
real per-cell grids and VTank's own click-to-delete/cycle semantics
(PluginCore.cs:8529-8562 Items, :7683-7776 Consumables, :7323-7355 Buffs).

Deviations, documented at their own binding site:
- Items' Hands column has no backing wieldable-handedness data anywhere
  in the plugin surface, so handedness is session-local UI state only
  (not persisted across profile save/load) — same "deliberate adaptation"
  shape as S7.3's weapon-roster substitution for Monsters' Weapon/Offhand.
- Consumables' "Add Selected" accepts any selected owned item rather than
  requiring VTank's own SpellComponent object-class check (no classifier
  surface exists for plugins) — the added token is still the item's real
  Name, which SpellComponentPolicy already matches against.
- Buffs' ExtraBuffSpellNames/BlacklistedBuffFamilyNames (BuffPlan.cs) add
  storage + UI only; wiring them into BuffPlan.Build's cast selection is
  real casting-algorithm behavior, out of this UI-parity slice's scope
  (tracked in the slice 7 plan ledger as a real, accepted gap for a
  future Campaign VT behavior slice).

Every new/changed pin (contract control count 167->177, the new
mosstank-buffpicker.xml popup pin, the three new MossTankPanelTests
interaction tests) was shown to fail against a targeted mutation before
being confirmed green. MossTank suite 654 -> 658; App markup/plugin
filter holds 192/192.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 10:26:13 +02:00
Erik
22e6ae88a1 merge(vt): plain <menu> popup — dark rows, selected fill, plain scrollbar, no checkmark (owner: the open dropdown was still retail art)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 10:21:11 +02:00
Erik
fd5dfa49e0 docs(vt): document that plain <menu> style covers the open popup too
Follow-up to the UiMenu popup fix: the existing "menu style" paragraph in
plugin-ui-markup.md only described the closed-state button face swap from
the earlier S7 fix. Extend it to say the plain style now covers the whole
menu (closed AND open) — flat popup chrome matching <list>, a lighter hover
fill, no checkmark, and a plain scrollbar past the row cap — so a plugin
author reading the doc doesn't assume style="plain" only affects the
closed face.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 10:19:54 +02:00