From f5409530f299bf98382e73e29fad13b97083a35f Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 7 Sep 2026 10:26:13 +0200 Subject: [PATCH] =?UTF-8?q?feat(vtank):=20slice=207=20S7.4=20=E2=80=94=20I?= =?UTF-8?q?tems/Consumables/Buffs=20grids=20and=20picker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/AcDream.App/AcDream.App.csproj | 9 +- .../AcDream.Plugins.MossTank.csproj | 3 + src/AcDream.Plugins.MossTank/BuffPlan.cs | 24 ++ src/AcDream.Plugins.MossTank/MossTankPanel.cs | 233 ++++++++++++++++++ .../MossTankPlugin.cs | 12 + .../mosstank-buffpicker.xml | 40 +++ src/AcDream.Plugins.MossTank/mosstank.xml | 104 ++++++-- .../AcDream.Plugins.MossTank.Tests.csproj | 3 + .../MossTankMarkupContractTests.cs | 13 +- .../MossTankPanelTests.cs | 106 ++++++++ 10 files changed, 516 insertions(+), 31 deletions(-) create mode 100644 src/AcDream.Plugins.MossTank/mosstank-buffpicker.xml diff --git a/src/AcDream.App/AcDream.App.csproj b/src/AcDream.App/AcDream.App.csproj index 6a25e9205..529a86b74 100644 --- a/src/AcDream.App/AcDream.App.csproj +++ b/src/AcDream.App/AcDream.App.csproj @@ -103,8 +103,9 @@ @@ -127,7 +128,7 @@ PreserveNewest + + PreserveNewest + diff --git a/src/AcDream.Plugins.MossTank/BuffPlan.cs b/src/AcDream.Plugins.MossTank/BuffPlan.cs index eef0a884d..8e35fe7d4 100644 --- a/src/AcDream.Plugins.MossTank/BuffPlan.cs +++ b/src/AcDream.Plugins.MossTank/BuffPlan.cs @@ -84,6 +84,30 @@ public sealed class BuffSettings public int BuffWithUntrainedItemSkill { get; set; } = 80; public int BuffWithUntrainedCreatureSkill { get; set; } = 80; public int BuffWithUntrainedLifeSkill { get; set; } = 80; + + /// + /// VTank's ExtraBuffSpells table (single-field ExemplarId list, + /// defaultsettings.usd:21-25) — named spell exemplars to cast "best + /// similar" beyond the school-driven picks above. Campaign VT slice 7 + /// S7.4 adds the storage and the Buffs-tab UI (list + the + /// SelfBuffChoiceView-style picker) but does NOT wire this set into + /// 's cast selection — resolving an + /// exemplar name to a real castable line/tier is genuine casting- + /// algorithm behavior, out of this UI-parity slice's scope (tracked as + /// a real, accepted gap in the slice 7 plan ledger, owned by a future + /// Campaign VT behavior slice). + /// + public ISet ExtraBuffSpellNames { get; } = + new HashSet(StringComparer.Ordinal); + + /// + /// VTank's AntiExtraBuffSpells table (defaultsettings.usd:1-6) — named + /// buff families to never cast even if the school-driven rules above + /// would otherwise want them. Same storage-only scope note as + /// . + /// + public ISet BlacklistedBuffFamilyNames { get; } = + new HashSet(StringComparer.Ordinal); } /// diff --git a/src/AcDream.Plugins.MossTank/MossTankPanel.cs b/src/AcDream.Plugins.MossTank/MossTankPanel.cs index 185052e5d..61e489818 100644 --- a/src/AcDream.Plugins.MossTank/MossTankPanel.cs +++ b/src/AcDream.Plugins.MossTank/MossTankPanel.cs @@ -160,6 +160,31 @@ internal sealed partial class MossTankPanel private IReadOnlyList _consumableRows = Array.Empty(); private int _selectedItemRow; private int _selectedConsumableRow; + // Campaign VT S7.4: VTank's Items-tab Hands column + // (PluginCore.cs:8546-8560, case 1) cycles a real eItemUseSpecifier + // WEAP_* enum persisted per item. MossTank tracks no handedness data at + // all for a registered weapon/wand/shield/pet (CombatItemNames is a bare + // name set) and the plugin surface exposes no wieldable-handedness field + // to derive one from — so this is session-local UI state only (not + // persisted across profile save/load), a documented, accepted gap + // matching the S7.3 precedent (CycleMonsterWeaponAt's own "deliberate + // adaptation" note) rather than inventing new profile-wide behavior. + private readonly Dictionary _itemHandedness = + new(StringComparer.Ordinal); + private static readonly string[] HandednessCycle = + ["Auto", "1-Handed", "2-Handed"]; + private IReadOnlyList _excludedComponentRows = Array.Empty(); + private int _selectedExcludedComponentRow; + private bool _buffPickerVisible; + private bool _buffPickerForBlacklist; + private string _buffPickerSearchText = string.Empty; + private int _selectedBuffPickerRow; + // The Buffs-tab lists themselves have no meaningful "selected row" the + // way the picker popup does (VTank's own click-anywhere-deletes model + // never leaves a row highlighted, PluginCore.cs:7323-7355) — these two + // fields exist only because is a required binding. + private int _selectedExtraBuffRow; + private int _selectedBlacklistedBuffRow; private bool _lootEditorVisible; private bool _advancedOptionsVisible; private int _selectedAdvancedOption; @@ -487,6 +512,32 @@ internal sealed partial class MossTankPanel _selectedConsumableRow = ClampRow(index, _consumableRows.Count); public Action RemoveSelectedItem => RemoveSelectedItemCore; public Action RemoveSelectedConsumable => RemoveSelectedConsumableCore; + + // ── Items tab grid (Campaign VT S7.4: VTank's own 2-column + // clWeaponName/clHandedness list, docs/research/vtank-kb/08-ui-views.md + // §1 "Tab: Items"; PluginCore.cs:8529-8562 — col 0 click deletes, col 1 + // click cycles handedness) ────────────────────────────────────────── + public IReadOnlyList ItemNameColumn => _itemRows; + public IReadOnlyList ItemHandsColumn => _itemRows + .Select(row => HandednessCycle[HandednessIndex(BaseItemName(row))]) + .ToArray(); + public Action DeleteItemRowAt => DeleteItemRowAtCore; + public Action CycleItemHandsAt => CycleItemHandsAtCore; + + // ── Consumables tab "Excluded Scarab Types" grid (Campaign VT S7.4: + // VTank's own lstExcludedComponents, docs/research/vtank-kb/ + // 08-ui-views.md §1 "Tab: Consumables"; PluginCore.cs:7683-7776 — "Add + // Selected" requires a selected spell-component-class object, + // per-cell click deletes) ──────────────────────────────────────────── + public IReadOnlyList ExcludedComponentRows => _excludedComponentRows; + public IReadOnlyList ExcludedComponentIcons => _excludedComponentRows + .Select(ResolveComponentIcon) + .ToArray(); + public int SelectedExcludedComponentIndex => _selectedExcludedComponentRow; + public Action SelectExcludedComponentRow => index => + _selectedExcludedComponentRow = ClampRow(index, _excludedComponentRows.Count); + public Action DeleteExcludedComponentAt => DeleteExcludedComponentAtCore; + public Action AddSelectedComponent => AddSelectedComponentCore; public Action ToggleAutoStack => () => { _inventorySettings.AutoStack = !_inventorySettings.AutoStack; @@ -1156,6 +1207,49 @@ internal sealed partial class MossTankPanel 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/ + // 08-ui-views.md §1 "Tab: Buffs"; PluginCore.cs:7323-7355 — any cell + // click removes that row; "Add..." opens SelfBuffChoiceView, a + // search-filtered single-column exemplar picker, §1's secondary-view + // table). ExtraBuffSpellNames/BlacklistedBuffFamilyNames + // (BuffPlan.cs) are storage + UI only for this slice — see that + // property's own doc comment for why BuffPlan.Build does not yet + // consult them. ──────────────────────────────────────────────────── + public IReadOnlyList ExtraBuffRows => Sorted(_buffSettings.ExtraBuffSpellNames); + public IReadOnlyList BlacklistedBuffFamilyRows => + Sorted(_buffSettings.BlacklistedBuffFamilyNames); + public int SelectedExtraBuffIndex => _selectedExtraBuffRow; + public int SelectedBlacklistedBuffIndex => _selectedBlacklistedBuffRow; + public Action DeleteExtraBuffAt => row => + { + _selectedExtraBuffRow = row; + DeleteFromNamedSet(_buffSettings.ExtraBuffSpellNames, row); + }; + public Action DeleteBlacklistedBuffFamilyAt => row => + { + _selectedBlacklistedBuffRow = row; + DeleteFromNamedSet(_buffSettings.BlacklistedBuffFamilyNames, row); + }; + public Action ShowExtraBuffPicker => () => ShowBuffPickerCore(forBlacklist: false); + public Action ShowBlacklistedBuffPicker => () => ShowBuffPickerCore(forBlacklist: true); + public bool BuffPickerVisible => _buffPickerVisible; + public string BuffPickerSearchText => _buffPickerSearchText; + public Action SetBuffPickerSearchText => value => + _buffPickerSearchText = value; + public IReadOnlyList BuffPickerRows => _host.Automation.Spells.KnownSelfBuffs + .Select(static spell => spell.Name) + .Where(name => string.IsNullOrWhiteSpace(_buffPickerSearchText) + || name.Contains(_buffPickerSearchText, StringComparison.OrdinalIgnoreCase)) + .Distinct(StringComparer.Ordinal) + .OrderBy(static name => name, StringComparer.Ordinal) + .ToArray(); + public int SelectedBuffPickerIndex => _selectedBuffPickerRow; + public Action SelectBuffPickerRow => index => + _selectedBuffPickerRow = index; + public Action PickBuffAt => PickBuffAtCore; + public Action HideBuffPicker => () => _buffPickerVisible = false; + public string TargetMethodText => $"Target selection: {_combatSettings.SelectionMethod}"; public string TargetLockText => @@ -1455,6 +1549,145 @@ internal sealed partial class MossTankPanel _selectedConsumableRow = ClampRow( _selectedConsumableRow, _consumableRows.Count); + _excludedComponentRows = ParseExcludedComponents( + _buffSettings.BlacklistedSpellComponents); + _selectedExcludedComponentRow = ClampRow( + _selectedExcludedComponentRow, + _excludedComponentRows.Count); + } + + private static string BaseItemName(string displayRow) + { + const string suffix = " [no buffs]"; + return displayRow.EndsWith(suffix, StringComparison.Ordinal) + ? displayRow[..^suffix.Length] + : displayRow; + } + + private int HandednessIndex(string name) => + _itemHandedness.TryGetValue(name, out int value) ? value : 0; + + private void DeleteItemRowAtCore(int row) + { + string[] names = _combatSettings.CombatItemNames + .OrderBy(static name => name, StringComparer.Ordinal) + .ToArray(); + if ((uint)row >= (uint)names.Length) + return; + string removed = names[row]; + _combatSettings.CombatItemNames.Remove(removed); + _noBuffItemNames.Remove(removed); + _itemHandedness.Remove(removed); + _profileNotice = $"Removed {removed}."; + RefreshItemEditors(); + SaveProfile(); + } + + private void CycleItemHandsAtCore(int row) + { + string[] names = _combatSettings.CombatItemNames + .OrderBy(static name => name, StringComparer.Ordinal) + .ToArray(); + if ((uint)row >= (uint)names.Length) + return; + string name = names[row]; + _itemHandedness[name] = (HandednessIndex(name) + 1) % HandednessCycle.Length; + } + + private const string ExcludedComponentDelimiter = "; "; + + private static IReadOnlyList ParseExcludedComponents(string setting) => + string.IsNullOrWhiteSpace(setting) + ? Array.Empty() + : setting.Split( + ';', + StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + + private uint ResolveComponentIcon(string name) + { + foreach (PluginInventoryItem item in _host.Automation.Items.CaptureOwnedItems()) + { + if (string.Equals(item.Name, name, StringComparison.OrdinalIgnoreCase)) + return item.IconId; + } + return 0u; + } + + /// + /// PluginCore.cs:7714-7746 (VTank's "Add Selected"): requires the + /// current selection to resolve to an owned SpellComponent-class + /// object; MossTank's plugin surface has no object-class classifier, so + /// this accepts any selected owned inventory item (matching retail + /// component naming, where a reagent's inventory Name IS the + /// SpellComponentTable Name that SpellComponentPolicy already matches + /// against — see SpellComponentPolicy.cs). + /// + private void AddSelectedComponentCore() + { + if (!TryGetSelectedInventoryItem(out PluginInventoryItem item)) + { + _profileNotice = "Select an owned spell component first."; + return; + } + if (_excludedComponentRows.Contains(item.Name, StringComparer.OrdinalIgnoreCase)) + { + _profileNotice = "Blacklist entry already exists."; + return; + } + var updated = new List(_excludedComponentRows) { item.Name }; + string joined = string.Join(ExcludedComponentDelimiter, updated); + _buffSettings.BlacklistedSpellComponents = joined; + _combatSettings.BlacklistedSpellComponents = joined; + _profileNotice = $"Added {item.Name}."; + RefreshItemEditors(); + SaveProfile(); + } + + private void DeleteExcludedComponentAtCore(int row) + { + if ((uint)row >= (uint)_excludedComponentRows.Count) + return; + var updated = new List(_excludedComponentRows); + string removed = updated[row]; + updated.RemoveAt(row); + string joined = string.Join(ExcludedComponentDelimiter, updated); + _buffSettings.BlacklistedSpellComponents = joined; + _combatSettings.BlacklistedSpellComponents = joined; + _profileNotice = $"Removed {removed}."; + RefreshItemEditors(); + SaveProfile(); + } + + private static string[] Sorted(IEnumerable names) => + names.OrderBy(static name => name, StringComparer.Ordinal).ToArray(); + + private static void DeleteFromNamedSet(ISet set, int row) + { + string[] names = Sorted(set); + if ((uint)row >= (uint)names.Length) + return; + set.Remove(names[row]); + } + + private void ShowBuffPickerCore(bool forBlacklist) + { + _buffPickerForBlacklist = forBlacklist; + _buffPickerSearchText = string.Empty; + _selectedBuffPickerRow = 0; + _buffPickerVisible = true; + } + + private void PickBuffAtCore(int row) + { + string[] rows = BuffPickerRows as string[] ?? BuffPickerRows.ToArray(); + if ((uint)row >= (uint)rows.Length) + return; + string name = rows[row]; + if (_buffPickerForBlacklist) + _buffSettings.BlacklistedBuffFamilyNames.Add(name); + else + _buffSettings.ExtraBuffSpellNames.Add(name); + _buffPickerVisible = false; } private static int ClampRow(int index, int count) => count == 0 diff --git a/src/AcDream.Plugins.MossTank/MossTankPlugin.cs b/src/AcDream.Plugins.MossTank/MossTankPlugin.cs index fa6ba8312..51e3c064e 100644 --- a/src/AcDream.Plugins.MossTank/MossTankPlugin.cs +++ b/src/AcDream.Plugins.MossTank/MossTankPlugin.cs @@ -101,6 +101,18 @@ public sealed class MossTankPlugin : IAcDreamPlugin }, Path.Combine(directory, "mosstank-loot-editor.xml"), _panel); + // Campaign VT S7.4: the Buffs tab's "Add..." picker (VTank's own + // SelfBuffChoiceView), same StartVisible=true/ShowInSidePanel=false + // popup pattern as the two panels above — see their own comments + // for why StartVisible must be true. + _host.Ui.AddPanel( + new PluginPanelDescriptor("buff-picker", "MossTank Add Buff") + { + StartVisible = true, + ShowInSidePanel = false, + }, + Path.Combine(directory, "mosstank-buffpicker.xml"), + _panel); _commandRegistration = _host.Commands.Register( "vt", diff --git a/src/AcDream.Plugins.MossTank/mosstank-buffpicker.xml b/src/AcDream.Plugins.MossTank/mosstank-buffpicker.xml new file mode 100644 index 000000000..1de7fd585 --- /dev/null +++ b/src/AcDream.Plugins.MossTank/mosstank-buffpicker.xml @@ -0,0 +1,40 @@ + + + + +