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>
This commit is contained in:
parent
66b070def4
commit
f5409530f2
10 changed files with 516 additions and 31 deletions
|
|
@ -103,8 +103,9 @@
|
|||
<!-- MossTank ships as a copy-only plugin, plus its panel markup:
|
||||
MossTankPlugin resolves mosstank.xml (and, since fix round A moved
|
||||
the Advanced Options / Loot Editor popups to their own panels,
|
||||
mosstank-advanced.xml / mosstank-loot-editor.xml too) relative to its
|
||||
own assembly, so all four files must land in the same plugin
|
||||
mosstank-advanced.xml / mosstank-loot-editor.xml too, plus Campaign
|
||||
VT S7.4's mosstank-buffpicker.xml popup) relative to its own
|
||||
assembly, so all five files must land in the same plugin
|
||||
directory. -->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AcDream.Plugins.MossTank\AcDream.Plugins.MossTank.csproj">
|
||||
|
|
@ -127,7 +128,7 @@
|
|||
</MSBuild>
|
||||
<MakeDir Directories="$(_MossTankDestDir)" />
|
||||
<Copy
|
||||
SourceFiles="@(_MossTankPluginBuildTarget);$(MSBuildProjectDirectory)/../AcDream.Plugins.MossTank/mosstank.xml;$(MSBuildProjectDirectory)/../AcDream.Plugins.MossTank/mosstank-advanced.xml;$(MSBuildProjectDirectory)/../AcDream.Plugins.MossTank/mosstank-loot-editor.xml"
|
||||
SourceFiles="@(_MossTankPluginBuildTarget);$(MSBuildProjectDirectory)/../AcDream.Plugins.MossTank/mosstank.xml;$(MSBuildProjectDirectory)/../AcDream.Plugins.MossTank/mosstank-advanced.xml;$(MSBuildProjectDirectory)/../AcDream.Plugins.MossTank/mosstank-loot-editor.xml;$(MSBuildProjectDirectory)/../AcDream.Plugins.MossTank/mosstank-buffpicker.xml"
|
||||
DestinationFolder="$(_MossTankDestDir)"
|
||||
SkipUnchangedFiles="true" />
|
||||
<WriteLinesToFile
|
||||
|
|
@ -151,7 +152,7 @@
|
|||
</MSBuild>
|
||||
<MakeDir Directories="$(_MossTankPublishDestDir)" />
|
||||
<Copy
|
||||
SourceFiles="@(_MossTankPluginPublishTarget);$(MSBuildProjectDirectory)/../AcDream.Plugins.MossTank/mosstank.xml;$(MSBuildProjectDirectory)/../AcDream.Plugins.MossTank/mosstank-advanced.xml;$(MSBuildProjectDirectory)/../AcDream.Plugins.MossTank/mosstank-loot-editor.xml"
|
||||
SourceFiles="@(_MossTankPluginPublishTarget);$(MSBuildProjectDirectory)/../AcDream.Plugins.MossTank/mosstank.xml;$(MSBuildProjectDirectory)/../AcDream.Plugins.MossTank/mosstank-advanced.xml;$(MSBuildProjectDirectory)/../AcDream.Plugins.MossTank/mosstank-loot-editor.xml;$(MSBuildProjectDirectory)/../AcDream.Plugins.MossTank/mosstank-buffpicker.xml"
|
||||
DestinationFolder="$(_MossTankPublishDestDir)"
|
||||
SkipUnchangedFiles="true" />
|
||||
<WriteLinesToFile
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@
|
|||
<None Update="mosstank-loot-editor.xml">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="mosstank-buffpicker.xml">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<EmbeddedResource Include="VtankCraftRecipes.tsv" />
|
||||
<EmbeddedResource Include="VtankAmmunitionOptions.tsv" />
|
||||
<EmbeddedResource Include="VtankDefaultSettings.usd" />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <see cref="BuffPlan.Build"/>'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).
|
||||
/// </summary>
|
||||
public ISet<string> ExtraBuffSpellNames { get; } =
|
||||
new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <see cref="ExtraBuffSpellNames"/>.
|
||||
/// </summary>
|
||||
public ISet<string> BlacklistedBuffFamilyNames { get; } =
|
||||
new HashSet<string>(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -160,6 +160,31 @@ internal sealed partial class MossTankPanel
|
|||
private IReadOnlyList<string> _consumableRows = Array.Empty<string>();
|
||||
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<string, int> _itemHandedness =
|
||||
new(StringComparer.Ordinal);
|
||||
private static readonly string[] HandednessCycle =
|
||||
["Auto", "1-Handed", "2-Handed"];
|
||||
private IReadOnlyList<string> _excludedComponentRows = Array.Empty<string>();
|
||||
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 <list selected> 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<string> ItemNameColumn => _itemRows;
|
||||
public IReadOnlyList<string> ItemHandsColumn => _itemRows
|
||||
.Select(row => HandednessCycle[HandednessIndex(BaseItemName(row))])
|
||||
.ToArray();
|
||||
public Action<int> DeleteItemRowAt => DeleteItemRowAtCore;
|
||||
public Action<int> 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<string> ExcludedComponentRows => _excludedComponentRows;
|
||||
public IReadOnlyList<uint> ExcludedComponentIcons => _excludedComponentRows
|
||||
.Select(ResolveComponentIcon)
|
||||
.ToArray();
|
||||
public int SelectedExcludedComponentIndex => _selectedExcludedComponentRow;
|
||||
public Action<int> SelectExcludedComponentRow => index =>
|
||||
_selectedExcludedComponentRow = ClampRow(index, _excludedComponentRows.Count);
|
||||
public Action<int> 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<string> ExtraBuffRows => Sorted(_buffSettings.ExtraBuffSpellNames);
|
||||
public IReadOnlyList<string> BlacklistedBuffFamilyRows =>
|
||||
Sorted(_buffSettings.BlacklistedBuffFamilyNames);
|
||||
public int SelectedExtraBuffIndex => _selectedExtraBuffRow;
|
||||
public int SelectedBlacklistedBuffIndex => _selectedBlacklistedBuffRow;
|
||||
public Action<int> DeleteExtraBuffAt => row =>
|
||||
{
|
||||
_selectedExtraBuffRow = row;
|
||||
DeleteFromNamedSet(_buffSettings.ExtraBuffSpellNames, row);
|
||||
};
|
||||
public Action<int> 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<string> SetBuffPickerSearchText => value =>
|
||||
_buffPickerSearchText = value;
|
||||
public IReadOnlyList<string> 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<int> SelectBuffPickerRow => index =>
|
||||
_selectedBuffPickerRow = index;
|
||||
public Action<int> 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<string> ParseExcludedComponents(string setting) =>
|
||||
string.IsNullOrWhiteSpace(setting)
|
||||
? Array.Empty<string>()
|
||||
: 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
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<string>(_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<string>(_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<string> names) =>
|
||||
names.OrderBy(static name => name, StringComparer.Ordinal).ToArray();
|
||||
|
||||
private static void DeleteFromNamedSet(ISet<string> 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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
40
src/AcDream.Plugins.MossTank/mosstank-buffpicker.xml
Normal file
40
src/AcDream.Plugins.MossTank/mosstank-buffpicker.xml
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Buff picker popup — VTank's own SelfBuffChoiceView (268x236, icon 8241,
|
||||
docs/research/vtank-kb/08-ui-views.md §1's secondary-view table): a
|
||||
search-filtered single-column exemplar list, opened from the Buffs tab's
|
||||
two "Add..." buttons (cmdNewBuffSpell/cmdNewAntiBuffSpell — one picker
|
||||
type, two destination lists, disambiguated at open time by which button
|
||||
called ShowExtraBuffPicker/ShowBlacklistedBuffPicker). Same top-level
|
||||
plugin-panel pattern fix round A established for mosstank-advanced.xml/
|
||||
mosstank-loot-editor.xml: StartVisible=true, ShowInSidePanel=false, this
|
||||
panel's own visible="{BuffPickerVisible}" is the SAME flag both "Add..."
|
||||
buttons set and this popup's own "Cancel" button clears.
|
||||
|
||||
The KB's transcribed lblSearch/txtSearch geometry (4,168,384,16 /
|
||||
4,188,260,16) carries VTank's own field width past its 268px window —
|
||||
a source-XML artifact, not a real VVS layout (VVS itself would clip it
|
||||
the same way our own contract test would reject it) — so both are
|
||||
narrowed to fit the popup's own 268px canvas here, keeping VTank's
|
||||
vertical rhythm (list, then "Search:", then the field) exactly.
|
||||
|
||||
KB's own lExemplarList has no explicit onchange row-click behavior
|
||||
documented (only that txtSearch live-filters it) — picking a row here
|
||||
commits it directly to whichever list is being edited and closes the
|
||||
popup (PickBuffAt), rather than requiring a separate "OK" button VTank's
|
||||
own view doesn't have either.
|
||||
-->
|
||||
<panel x="253" y="405" w="268" h="236" title="MossTank Add Buff"
|
||||
visible="{BuffPickerVisible}" resize="none">
|
||||
<list x="4" y="22" w="260" h="160" rowheight="17"
|
||||
items="{BuffPickerRows}" selected="{SelectedBuffPickerIndex}"
|
||||
onchange="{PickBuffAt}"
|
||||
tooltip="Select a spell to add and close this picker." />
|
||||
<label x="4" y="188" w="60" h="16" text="Search:" color="#FFE8DEC3" />
|
||||
<field x="64" y="186" w="200" h="18" text="{BuffPickerSearchText}"
|
||||
onchange="{SetBuffPickerSearchText}" maxlength="64"
|
||||
clearonsubmit="false" background="#E6000000" color="#FFE8DEC3"
|
||||
tooltip="Live-filters the list above by name." />
|
||||
<button x="4" y="210" w="260" h="20" text="Cancel"
|
||||
onclick="{HideBuffPicker}" />
|
||||
</panel>
|
||||
|
|
@ -382,13 +382,24 @@
|
|||
tooltip="Add the monster currently selected in the world." />
|
||||
</group>
|
||||
|
||||
<!-- Items: exact-name profile membership drives weapons, wands and pets. -->
|
||||
<!-- Items: VTank's own 2-column clWeaponName/clHandedness grid
|
||||
(Campaign VT S7.4, docs/research/vtank-kb/08-ui-views.md §1 "Tab:
|
||||
Items"; PluginCore.cs:8529-8562). PITCH per docs/plugin-ui-markup.md:
|
||||
name 320+7=327, hands (last column) 80+7=87 (auto per the
|
||||
last-column-always-auto rule, +16 scrollbar reserve folded into the
|
||||
declared list width below rather than the column itself). Name-cell
|
||||
click deletes the row (case 0); hands-cell click cycles handedness
|
||||
(case 1) — session-local only, see DeleteItemRowAt/CycleItemHandsAt's
|
||||
own doc comments for why handedness isn't persisted. -->
|
||||
<group x="8" y="42" w="848" h="194" visible="{ItemsVisible}">
|
||||
<label x="4" y="8" text="Weapons / Wands / Shields / Pets" color="#FFE8DEC3" />
|
||||
<label x="438" y="8" text="Hands" color="#FFE8DEC3" />
|
||||
<list x="4" y="28" w="414" h="112" rowheight="18"
|
||||
items="{ItemRows}" selected="{SelectedItemRowIndex}"
|
||||
onchange="{SelectItemRow}" tooltip="Select a profiled weapon, wand, shield, or pet device." />
|
||||
<label x="4" y="8" w="327" text="Weapons / Wands / Shields / Pets" color="#FFE8DEC3" />
|
||||
<label x="331" y="8" w="103" text="Hands" color="#FFE8DEC3" />
|
||||
<list x="4" y="28" w="430" h="112" rowheight="18"
|
||||
selected="{SelectedItemRowIndex}" onchange="{SelectItemRow}"
|
||||
tooltip="Click a name to remove it, or Hands to cycle 1-Handed/2-Handed.">
|
||||
<column type="text" width="327" items="{ItemNameColumn}" onclick="{DeleteItemRowAt}" />
|
||||
<column type="text" width="103" items="{ItemHandsColumn}" onclick="{CycleItemHandsAt}" />
|
||||
</list>
|
||||
<button x="4" y="154" w="126" h="25" text="Add" onclick="{AddSelectedItem}" />
|
||||
<button x="138" y="154" w="138" h="25" text="Add (no buffs)"
|
||||
onclick="{AddSelectedItemNoBuffs}" />
|
||||
|
|
@ -404,7 +415,13 @@
|
|||
<label x="438" y="160" text="{ProfileNotice}" color="#FF9B9072" />
|
||||
</group>
|
||||
|
||||
<!-- Consumables: the same exact-name profile VTank consults for phials. -->
|
||||
<!-- Consumables: the same exact-name profile VTank consults for phials,
|
||||
plus VTank's own right-hand "Excluded Scarab Types:" grid (Campaign
|
||||
VT S7.4, docs/research/vtank-kb/08-ui-views.md §1 "Tab:
|
||||
Consumables"; PluginCore.cs:7683-7776 — any cell click removes that
|
||||
row; "Add Selected" requires a selected spell-component-class
|
||||
object, see AddSelectedComponent's own doc comment). PITCH: icon
|
||||
16+7=23, text column auto ("*", last column). -->
|
||||
<group x="8" y="42" w="848" h="194" visible="{ConsumablesVisible}">
|
||||
<label x="4" y="8"
|
||||
text="Gems / Food / Kits / Potions / Charges / Grenades / Lockpicks"
|
||||
|
|
@ -413,47 +430,84 @@
|
|||
<list x="4" y="28" w="414" h="112" rowheight="18"
|
||||
items="{ConsumableRows}" selected="{SelectedConsumableRowIndex}"
|
||||
onchange="{SelectConsumableRow}" tooltip="Select a profiled consumable to remove." />
|
||||
<list x="438" y="28" w="406" h="112" rowheight="18"
|
||||
selected="{SelectedExcludedComponentIndex}" onchange="{SelectExcludedComponentRow}"
|
||||
tooltip="Click a row to remove it from the blacklist.">
|
||||
<column type="icon" width="23" iconkind="did" values="{ExcludedComponentIcons}"
|
||||
onclick="{DeleteExcludedComponentAt}" />
|
||||
<column type="text" width="*" items="{ExcludedComponentRows}"
|
||||
onclick="{DeleteExcludedComponentAt}" />
|
||||
</list>
|
||||
<button x="4" y="154" w="176" h="25" text="Add"
|
||||
onclick="{AddSelectedConsumable}" />
|
||||
<button x="188" y="154" w="110" h="25" text="Add All Peas"
|
||||
onclick="{AddAllPeas}" />
|
||||
<button x="306" y="154" w="104" h="25" text="Remove"
|
||||
onclick="{RemoveSelectedConsumable}" />
|
||||
<label x="438" y="160" text="{ProfileNotice}" color="#FF9B9072" />
|
||||
<button x="438" y="154" w="176" h="25" text="Add Selected"
|
||||
onclick="{AddSelectedComponent}" />
|
||||
<label x="438" y="184" text="{ProfileNotice}" color="#FF9B9072" />
|
||||
</group>
|
||||
|
||||
<!-- Buffs: every visible switch maps to the live buff plan. -->
|
||||
<!-- Buffs: every visible switch maps to the live buff plan, plus VTank's
|
||||
own Extra Buff Spells / Blacklisted Buff Families lists (Campaign VT
|
||||
S7.4, docs/research/vtank-kb/08-ui-views.md §1 "Tab: Buffs" — VTank's
|
||||
REAL Buffs tab is only these two lists + two "Add..." buttons; the
|
||||
category toggles/difficulty/rebuff/Buff-button below are MossTank's
|
||||
own pre-existing extension, kept as a superset and compacted into
|
||||
two columns to make room rather than removed). Any cell click on
|
||||
either list removes that row (PluginCore.cs:7323-7355); "Add..."
|
||||
opens the shared SelfBuffChoiceView-style picker
|
||||
(mosstank-buffpicker.xml). List heights (84, not VTank's 116) and
|
||||
widths (246/256, not VTank's 320) are narrowed to coexist with the
|
||||
retained toggle columns in this tab's fixed 848x194 canvas — a
|
||||
proportion-preserving compromise, not a pixel-for-pixel port
|
||||
(owner's bar: "looks basically the same", never VVS pixels). -->
|
||||
<group x="8" y="42" w="848" h="194" visible="{BuffsVisible}">
|
||||
<toggle x="4" y="4" w="220" h="20" text="Trained skills only"
|
||||
<toggle x="4" y="0" w="160" h="18" text="Trained skills only"
|
||||
checked="{TrainedOnlyEnabled}" onclick="{ToggleTrainedOnly}" />
|
||||
<toggle x="4" y="28" w="220" h="20" text="Attributes"
|
||||
<toggle x="4" y="20" w="160" h="18" text="Attributes"
|
||||
checked="{AttributesEnabled}" onclick="{ToggleAttributes}" />
|
||||
<toggle x="4" y="52" w="220" h="20" text="Protections"
|
||||
<toggle x="4" y="40" w="160" h="18" text="Protections"
|
||||
checked="{ProtectionsEnabled}" onclick="{ToggleProtections}" />
|
||||
<toggle x="4" y="76" w="220" h="20" text="Weapon auras"
|
||||
<toggle x="4" y="60" w="160" h="18" text="Weapon auras"
|
||||
checked="{AurasEnabled}" onclick="{ToggleAuras}" />
|
||||
|
||||
<toggle x="244" y="4" w="220" h="20" text="Armor banes"
|
||||
<toggle x="168" y="0" w="160" h="18" text="Armor banes"
|
||||
checked="{BanesEnabled}" onclick="{ToggleBanes}" />
|
||||
<toggle x="244" y="28" w="220" h="20" text="Regeneration"
|
||||
<toggle x="168" y="20" w="160" h="18" text="Regeneration"
|
||||
checked="{RegenerationEnabled}" onclick="{ToggleRegeneration}" />
|
||||
<toggle x="244" y="52" w="220" h="20" text="Other self-spells"
|
||||
<toggle x="168" y="40" w="160" h="18" text="Other self-spells"
|
||||
checked="{OtherEnabled}" onclick="{ToggleOther}" />
|
||||
|
||||
<label x="486" y="4" text="{DifficultyText}" color="#FFE8DEC3" />
|
||||
<button x="670" y="0" w="30" h="23" text="-" onclick="{DifficultyDown}"
|
||||
<label x="336" y="0" w="246" text="Extra Buff Spells" color="#FFE8DEC3"
|
||||
tooltip="Named spell exemplars added beyond the school-driven picks (best similar will be used)." />
|
||||
<list x="336" y="16" w="246" h="84" rowheight="17"
|
||||
items="{ExtraBuffRows}" selected="{SelectedExtraBuffIndex}"
|
||||
onchange="{DeleteExtraBuffAt}" tooltip="Click a row to remove it." />
|
||||
<button x="336" y="102" w="120" h="16" text="Add..." onclick="{ShowExtraBuffPicker}" />
|
||||
|
||||
<label x="590" y="0" w="256" text="Blacklisted Buff Families" color="#FFE8DEC3"
|
||||
tooltip="Named buff families never cast, even if otherwise wanted." />
|
||||
<list x="590" y="16" w="256" h="84" rowheight="17"
|
||||
items="{BlacklistedBuffFamilyRows}" selected="{SelectedBlacklistedBuffIndex}"
|
||||
onchange="{DeleteBlacklistedBuffFamilyAt}" tooltip="Click a row to remove it." />
|
||||
<button x="590" y="102" w="120" h="16" text="Add..." onclick="{ShowBlacklistedBuffPicker}" />
|
||||
|
||||
<label x="4" y="112" w="300" text="{DifficultyText}" color="#FFE8DEC3" />
|
||||
<button x="308" y="108" w="26" h="20" text="-" onclick="{DifficultyDown}"
|
||||
tooltip="Decrease the skill-over-difficulty margin." />
|
||||
<button x="706" y="0" w="30" h="23" text="+" onclick="{DifficultyUp}"
|
||||
<button x="338" y="108" w="26" h="20" text="+" onclick="{DifficultyUp}"
|
||||
tooltip="Increase the skill-over-difficulty margin." />
|
||||
<label x="486" y="34" text="{RebuffText}" color="#FFE8DEC3" />
|
||||
<button x="670" y="28" w="30" h="23" text="-" onclick="{RebuffDown}"
|
||||
<label x="4" y="134" w="300" text="{RebuffText}" color="#FFE8DEC3" />
|
||||
<button x="308" y="130" w="26" h="20" text="-" onclick="{RebuffDown}"
|
||||
tooltip="Rebuff later." />
|
||||
<button x="706" y="28" w="30" h="23" text="+" onclick="{RebuffUp}"
|
||||
<button x="338" y="130" w="26" h="20" text="+" onclick="{RebuffUp}"
|
||||
tooltip="Rebuff earlier." />
|
||||
|
||||
<button x="4" y="124" w="140" h="26" text="{BuffButtonText}" onclick="{Buff}" />
|
||||
<label x="164" y="130" text="{BuffStatus}" color="#FFC7B98F" />
|
||||
<label x="4" y="160" text="{Coverage}" color="#FF9B9072" />
|
||||
<button x="4" y="156" w="140" h="26" text="{BuffButtonText}" onclick="{Buff}" />
|
||||
<label x="164" y="162" text="{BuffStatus}" color="#FFC7B98F" />
|
||||
<label x="4" y="186" text="{Coverage}" color="#FF9B9072" />
|
||||
</group>
|
||||
|
||||
<!-- Route: VTank's navigation-waypoint editor and execution surface. -->
|
||||
|
|
|
|||
|
|
@ -29,6 +29,9 @@
|
|||
<None Include="..\..\src\AcDream.Plugins.MossTank\mosstank-loot-editor.xml"
|
||||
Link="mosstank-loot-editor.xml"
|
||||
CopyToOutputDirectory="PreserveNewest" />
|
||||
<None Include="..\..\src\AcDream.Plugins.MossTank\mosstank-buffpicker.xml"
|
||||
Link="mosstank-buffpicker.xml"
|
||||
CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="Fixtures\vtank\**\*.*" CopyToOutputDirectory="PreserveNewest" />
|
||||
|
|
|
|||
|
|
@ -194,7 +194,15 @@ public sealed class MossTankMarkupContractTests
|
|||
// three stacked name-draft field+"New"-button rows and the
|
||||
// acdream-only "Loot engine" classifier menu (owner: legacy
|
||||
// leftovers) — net 202 -> 167.
|
||||
Assert.Equal(167, controls.Length);
|
||||
// S7.4 replaced the Items tab's single-column list with VTank's own
|
||||
// clWeaponName/clHandedness 2-column grid (+2: the list itself plus
|
||||
// 2 <column> elements, vs. 1 for the old single-column list), added
|
||||
// the Consumables tab's "Excluded Scarab Types" 2-column grid plus
|
||||
// its "Add Selected" button (+4: 1 list + 2 columns + 1 button),
|
||||
// and added the Buffs tab's Extra Buff Spells / Blacklisted Buff
|
||||
// Families lists plus their two "Add..." buttons (+4: 2 lists + 2
|
||||
// buttons) — net 167 -> 177.
|
||||
Assert.Equal(177, controls.Length);
|
||||
|
||||
foreach (XElement control in controls)
|
||||
{
|
||||
|
|
@ -308,6 +316,7 @@ public sealed class MossTankMarkupContractTests
|
|||
[Theory]
|
||||
[InlineData("mosstank-advanced.xml", 392f, 300f)]
|
||||
[InlineData("mosstank-loot-editor.xml", 268f, 300f)]
|
||||
[InlineData("mosstank-buffpicker.xml", 268f, 236f)]
|
||||
public void SecondaryPopupPanelsFitTheirOwnBoundsAndEveryBindingResolves(
|
||||
string fileName, float expectedWidth, float expectedHeight)
|
||||
{
|
||||
|
|
@ -353,7 +362,7 @@ public sealed class MossTankMarkupContractTests
|
|||
foreach (string fileName in new[]
|
||||
{
|
||||
"mosstank.xml", "mosstank-advanced.xml",
|
||||
"mosstank-loot-editor.xml",
|
||||
"mosstank-loot-editor.xml", "mosstank-buffpicker.xml",
|
||||
})
|
||||
{
|
||||
XDocument document = XDocument.Load(
|
||||
|
|
|
|||
|
|
@ -906,6 +906,112 @@ public sealed class MossTankPanelTests
|
|||
Assert.Single(panel.ConsumableRows);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItemsGridNameClickDeletesAndHandsClickCyclesHandedness()
|
||||
{
|
||||
// Campaign VT S7.4: VTank's own 2-column Items grid
|
||||
// (PluginCore.cs:8529-8562) — col 0 (name) click deletes the row,
|
||||
// col 1 (hands) click cycles handedness.
|
||||
var automation = new FakeAutomation
|
||||
{
|
||||
ItemEntries =
|
||||
[
|
||||
Item(10, "Fire Sword", 1),
|
||||
Item(11, "Ice Wand", 1),
|
||||
],
|
||||
};
|
||||
var host = new FakeHost(automation);
|
||||
var panel = new MossTankPanel(host);
|
||||
host.Selection.Select(10);
|
||||
panel.AddSelectedItem();
|
||||
host.Selection.Select(11);
|
||||
panel.AddSelectedItem();
|
||||
Assert.Equal(["Fire Sword", "Ice Wand"], panel.ItemNameColumn);
|
||||
|
||||
Assert.Equal("Auto", panel.ItemHandsColumn[0]);
|
||||
panel.CycleItemHandsAt(0);
|
||||
Assert.Equal("1-Handed", panel.ItemHandsColumn[0]);
|
||||
panel.CycleItemHandsAt(0);
|
||||
Assert.Equal("2-Handed", panel.ItemHandsColumn[0]);
|
||||
panel.CycleItemHandsAt(0);
|
||||
Assert.Equal("Auto", panel.ItemHandsColumn[0]);
|
||||
// Cycling row 0 never touches row 1's own state.
|
||||
Assert.Equal("Auto", panel.ItemHandsColumn[1]);
|
||||
|
||||
panel.DeleteItemRowAt(0); // "Fire Sword"
|
||||
Assert.Equal(["Ice Wand"], panel.ItemNameColumn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExcludedComponentsGridAddsBySelectionAndDeletesByAnyCellClick()
|
||||
{
|
||||
// Campaign VT S7.4: VTank's own Consumables-tab right list
|
||||
// (PluginCore.cs:7683-7776) — "Add Selected" requires a selected
|
||||
// owned item, any cell click on a row deletes it.
|
||||
var automation = new FakeAutomation
|
||||
{
|
||||
ItemEntries =
|
||||
[
|
||||
Item(20, "Charged Yellow Scarab", 0x20)
|
||||
with { IconId = 0x06001234u },
|
||||
],
|
||||
};
|
||||
var host = new FakeHost(automation);
|
||||
var panel = new MossTankPanel(host);
|
||||
|
||||
Assert.Empty(panel.ExcludedComponentRows);
|
||||
host.Selection.Select(20);
|
||||
panel.AddSelectedComponent();
|
||||
Assert.Equal(["Charged Yellow Scarab"], panel.ExcludedComponentRows);
|
||||
Assert.Equal(0x06001234u, panel.ExcludedComponentIcons[0]);
|
||||
|
||||
// Adding the same component again is refused, not duplicated.
|
||||
panel.AddSelectedComponent();
|
||||
Assert.Equal(["Charged Yellow Scarab"], panel.ExcludedComponentRows);
|
||||
|
||||
panel.DeleteExcludedComponentAt(0);
|
||||
Assert.Empty(panel.ExcludedComponentRows);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuffPickerAddsToTheRequestedListAndAnyCellClickDeletes()
|
||||
{
|
||||
// Campaign VT S7.4: the shared SelfBuffChoiceView-style picker adds
|
||||
// to whichever list opened it; each list's own click-anywhere-
|
||||
// deletes matches PluginCore.cs:7323-7355.
|
||||
var automation = new FakeAutomation
|
||||
{
|
||||
KnownSelfBuffs =
|
||||
[
|
||||
Spell(1, 10, "Increases the caster's Strength by 10 points."),
|
||||
Spell(2, 20, "Increases the caster's Focus by 10 points."),
|
||||
],
|
||||
};
|
||||
var panel = new MossTankPanel(new FakeHost(automation));
|
||||
|
||||
Assert.False(panel.BuffPickerVisible);
|
||||
panel.ShowExtraBuffPicker();
|
||||
Assert.True(panel.BuffPickerVisible);
|
||||
Assert.Equal(2, panel.BuffPickerRows.Count);
|
||||
|
||||
panel.SetBuffPickerSearchText("Spell 1");
|
||||
Assert.Equal(["Spell 1"], panel.BuffPickerRows);
|
||||
panel.PickBuffAt(0);
|
||||
Assert.False(panel.BuffPickerVisible);
|
||||
Assert.Equal(["Spell 1"], panel.ExtraBuffRows);
|
||||
Assert.Empty(panel.BlacklistedBuffFamilyRows);
|
||||
|
||||
panel.ShowBlacklistedBuffPicker();
|
||||
panel.SetBuffPickerSearchText(string.Empty);
|
||||
panel.PickBuffAt(1); // "Spell 2" (sorted after "Spell 1")
|
||||
Assert.Equal(["Spell 2"], panel.BlacklistedBuffFamilyRows);
|
||||
|
||||
panel.DeleteExtraBuffAt(0);
|
||||
Assert.Empty(panel.ExtraBuffRows);
|
||||
panel.DeleteBlacklistedBuffFamilyAt(0);
|
||||
Assert.Empty(panel.BlacklistedBuffFamilyRows);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItemProfilesPersistThroughHostScopedStorage()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue