feat(vtank): slice 7 S7.3 — Monsters tab as VTank's own 23-column grid
Campaign VT slice 7 (docs/plans/2026-09-07-campaign-vt-slice7-tabs.md), S7.3 (Monsters). Owner's bar: looks basically the same as VTank and behaves the same from the player's seat. The pre-S7.3 Monsters tab was a select-then-edit adaptation (one padded-text list + a separate expression/priority/damage-menu/equip editor row + 14 standalone toggles) forced by acdream's earlier single-column-only <list>. Campaign VT slice 1 Part B already closed that gap (the <list><column> markup extension, docs/plugin-ui-markup.md "Columns"), so this slice replaces the whole tab body with VTank's own one 23-column HudList grid (docs/research/vtank-kb/08-ui-views.md §1 "Tab: Monsters"): 14 check columns (F B G I Y V A R S WC FC Cp DC Cs, each 20px = VVS's forced 13px check width + WPadding per the markup doc's PITCH convention), 7 text columns (Name 127/P 27/Dmg type 63/ Ex. Vuln 63/Weapon 87/Offhand 87/PetDmg 63, each pitch = fixedwidth+7), 2 icon columns (move up/down, DAT ids 0x060028FC/0x060028FD), 15 header <label>s carrying VTank's own decompiled tooltip text (PluginCore.cs:1693-1707 — corrects "Broadside of a Tumerok" to the decompile's actual "Broadside of a Barn"), and the txtNewMonster field + Add + Add Sel row at VTank's exact geometry (8,136,388,16 / 404,136,50,16 / 462,136,50,16). Selection is unused, matching VTank — every real cell is its own click target. Per-cell behavior ported from PluginCore.cs:7885-8105's `b(object,int, int)` handler: Name click deletes the row (guarded on the rule's own IsDefault rather than literal row 0, so the invariant holds even if a caller reorders DEFAULT); P cycles -1->0->1->2->3->4->-1; Dmg type cycles every eDamageElement 0..13; Ex. Vuln and PetDmg reproduce the decompile's own non-contiguous jumps (6->8->98 and 6->101->8->98 respectively — eDamageElement.cs); move up/down reorder but never displace DEFAULT (a deliberate symmetric guard — VTank's own case 21 keeps MoveUp off row 0 but case 22 has no matching MoveDown guard, which reads as an authoring oversight, not intended asymmetric behavior, so a literal port was not preserved here). Case 6's "A" column write is inverted in VTank's own obfuscated field, but that inversion is an artifact of VTank's internal storage, not the checkbox the player sees — our Attack flag is already declared un-inverted, so no inversion is needed for parity. Deviation: cases 18/19's Weapon/Offhand cycle VTank's own runtime list of owned weapon-TYPE ids via an opaque item-kind classifier the decompile alone doesn't resolve. MossTank has no "weapon type" abstraction — Weapon/Offhand are always concrete owned items — so these columns instead cycle the same registered weapon roster the Items tab already maintains (ordinal-sorted for a deterministic order), preserving the click-to-cycle-through-<AUTO> contract without inventing a type abstraction outside this slice's scope. MossTankPanel: the whole select-then-edit surface (SelectedMonsterRule selection, RefreshMonsterEditor's cached row formatting, Apply/Remove/ priority-stepper/damage-menu/equip-from-selection actions, HasMonsterFlag/ToggleMonsterFlag) is retired in favor of per-row column-array properties (14 flag columns, 7 text columns, 2 icon columns) and per-row mutation actions, all still writing through the unchanged CombatSettings.Rules/MonsterRule/MonsterRuleActions model. EnsureDefaultMonsterRule replaces RefreshMonsterEditor's DEFAULT invariant at the two remaining call sites (ctor init, profile reload, and the /vt refresh command). MonsterEquipmentText is kept as a read-only DEFAULT-rule readout for the (out-of-scope) Items tab label that already depended on it. Tests (every pin below shown to fail against a targeted mutation, reverted after confirming the failure — mutations: wrong priority-wrap boundary, dropped DEFAULT-delete guard, dropped DEFAULT-displacement guard on move, spliced Harm into the Ex.Vuln cycle, flag toggle targeting row 0 unconditionally, a wrong header tooltip string; the xml column-count contract's 205->202 pin was independently shown to fail against the pre-edit test file and the new markup): MossTankMarkupContractTests gained MonstersGridHasVtanksTwentyThreeColumnsInOrderWithRetailHeaderTooltips (column type/order + the 15 header tooltips) and updated the 205->202 interactive-control count plus the tooltip-check's <column> skip (columns have no text/tooltip grammar). MossTankPanelTests replaced the three old select-then-edit tests with MonstersGridMutationsPersistAcrossSessions, ToggleMonsterFlagAtWritesOnlyTheTargetedRow, CycleMonsterPriorityAtWrapsExactlyNegativeOneThroughFour, MonsterDamageColumnsCycleInTheExactRetailOrder (all three cycles), DeleteMonsterRuleAtRemovesNonDefaultRowsButNeverDefault, MoveMonsterRuleAtReordersButNeverDisplacesDefault, AddMonsterRuleUsesTheDraftTextAndAddSelectedMonsterUsesTheWorldTarget, and MonsterWeaponColumnCyclesTheRegisteredRosterAndPersistsByName AcrossSessions, and updated MacroWieldsCasterEntersMagicBuffsThen WieldsWeaponFightsThenIdlePeace for the new weapon-cycle API. Full suite green: 651/651 MossTank tests (645 + 6 net), 109/109 App markup-filtered tests. Deviation not requiring a divergence-register row: this campaign ports VTank plugin UI behavior, not retail AC client behavior, so docs/architecture/retail-divergence-register.md's scope (acdream vs. retail) does not apply here. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
89a49836ca
commit
57ced0aff1
5 changed files with 593 additions and 368 deletions
|
|
@ -172,7 +172,7 @@ internal sealed partial class MossTankPanel
|
|||
WriteVtank($"Portal space toggle count: {_commandPortalCount}");
|
||||
return;
|
||||
case "refresh":
|
||||
RefreshMonsterEditor();
|
||||
EnsureDefaultMonsterRule();
|
||||
RefreshItemEditors();
|
||||
RefreshLootEditor();
|
||||
RefreshRouteEditor();
|
||||
|
|
|
|||
|
|
@ -115,30 +115,46 @@ internal sealed partial class MossTankPanel
|
|||
"Select an inventory item, then add it to this profile.";
|
||||
private string _profileNameDraft = string.Empty;
|
||||
private string _profileLifecycleNotice = "Macro settings are stored by character.";
|
||||
private IReadOnlyList<string> _monsterRows = Array.Empty<string>();
|
||||
private int _selectedMonsterRule;
|
||||
private string _monsterExpressionDraft = "DEFAULT";
|
||||
private string _monsterEditorNotice = "Select a rule to edit.";
|
||||
// PluginCore's three columns deliberately expose different cycles. The
|
||||
// damage column is eDamageElement 0..13; Ex. Vuln omits Harm/Void/etc.;
|
||||
// PetDmg adds VTank's PAuto sentinel. A shared Enum.GetNames list made
|
||||
// several choices visible in columns where retail could never select
|
||||
// them, and also exposed our internal "Electric"/"PlayerAuto" names.
|
||||
private static readonly string[] MonsterDamageNames =
|
||||
private string _monsterExpressionDraft = string.Empty;
|
||||
private string _monsterEditorNotice = "Add a monster name or expression, or select one in the world.";
|
||||
// VVS's own HudList always has a "selected" backing int even though
|
||||
// VTank's Monsters grid never reads it for anything (every real cell in
|
||||
// that row is an action target, not a select-then-edit target — see
|
||||
// docs/research/vtank-kb/08-ui-views.md §2's HudList row). acdream's
|
||||
// <list> markup requires the same "selected" binding regardless; this
|
||||
// field exists purely to satisfy that grammar and give a click a visible
|
||||
// highlight, never read by any monster-editing behavior.
|
||||
private int _monsterListSelectedRow;
|
||||
// Campaign VT S7.3: VTank's own Monsters-tab per-cell cycles
|
||||
// (refs/vtank/decompiled/uTank2/PluginCore.cs:7948-8089, cases 16/17/20 —
|
||||
// eDamageElement wraparound, refs/vtank/decompiled/uTank2/eDamageElement.cs).
|
||||
// The three columns deliberately expose DIFFERENT cycles: Dmg type walks
|
||||
// every LISTEDTYPES_END value 0..13; Ex. Vuln's non-contiguous case 17
|
||||
// stops at Fire then jumps 6->8->98 (skipping Harm=7, landing on
|
||||
// Auto=8 then None=98); PetDmg's case 20 jumps 6->101->8->98 (adding
|
||||
// VTank's PAuto=101 sentinel ahead of Auto). Declared as MonsterDamageType
|
||||
// sequences (not bare strings) so DamageTypeDisplay is the single source
|
||||
// of truth for both the cycle's on-screen text and its wrap order.
|
||||
private static readonly MonsterDamageType[] MonsterDamageCycle =
|
||||
[
|
||||
"Pierce", "Bludgeon", "Slash", "Acid", "Lightning", "Cold",
|
||||
"Fire", "Harm", "Auto", "Void Basic", "Drain Auto", "Prismatic",
|
||||
"Random", "Fists",
|
||||
MonsterDamageType.Pierce, MonsterDamageType.Bludgeon, MonsterDamageType.Slash,
|
||||
MonsterDamageType.Acid, MonsterDamageType.Electric, MonsterDamageType.Cold,
|
||||
MonsterDamageType.Fire, MonsterDamageType.Harm, MonsterDamageType.Auto,
|
||||
MonsterDamageType.VoidBasic, MonsterDamageType.DrainAuto, MonsterDamageType.Prismatic,
|
||||
MonsterDamageType.Random, MonsterDamageType.Fists,
|
||||
];
|
||||
private static readonly string[] MonsterExtraVulnerabilityNames =
|
||||
private static readonly MonsterDamageType[] MonsterExtraVulnerabilityCycle =
|
||||
[
|
||||
"Pierce", "Bludgeon", "Slash", "Acid", "Lightning", "Cold",
|
||||
"Fire", "Auto", "None",
|
||||
MonsterDamageType.Pierce, MonsterDamageType.Bludgeon, MonsterDamageType.Slash,
|
||||
MonsterDamageType.Acid, MonsterDamageType.Electric, MonsterDamageType.Cold,
|
||||
MonsterDamageType.Fire, MonsterDamageType.Auto, MonsterDamageType.None,
|
||||
];
|
||||
private static readonly string[] MonsterPetDamageNames =
|
||||
private static readonly MonsterDamageType[] MonsterPetDamageCycle =
|
||||
[
|
||||
"Pierce", "Bludgeon", "Slash", "Acid", "Lightning", "Cold",
|
||||
"Fire", "PAuto", "Auto", "None",
|
||||
MonsterDamageType.Pierce, MonsterDamageType.Bludgeon, MonsterDamageType.Slash,
|
||||
MonsterDamageType.Acid, MonsterDamageType.Electric, MonsterDamageType.Cold,
|
||||
MonsterDamageType.Fire, MonsterDamageType.PlayerAuto, MonsterDamageType.Auto,
|
||||
MonsterDamageType.None,
|
||||
];
|
||||
private IReadOnlyList<string> _itemRows = Array.Empty<string>();
|
||||
private IReadOnlyList<string> _consumableRows = Array.Empty<string>();
|
||||
|
|
@ -271,7 +287,7 @@ internal sealed partial class MossTankPanel
|
|||
RegisterVtankExpressionFunctions();
|
||||
_initialized = true;
|
||||
ApplyPersistedOptionOverrides();
|
||||
RefreshMonsterEditor();
|
||||
EnsureDefaultMonsterRule();
|
||||
RefreshItemEditors();
|
||||
RefreshLootEditor();
|
||||
RefreshRouteEditor();
|
||||
|
|
@ -923,102 +939,157 @@ internal sealed partial class MossTankPanel
|
|||
: "Showing profiles from all characters.";
|
||||
};
|
||||
|
||||
// ── Monsters editor ──────────────────────────────────────────────────
|
||||
public IReadOnlyList<string> MonsterRows => _monsterRows;
|
||||
public int SelectedMonsterRuleIndex => _selectedMonsterRule;
|
||||
// ── Monsters grid (Campaign VT S7.3) ────────────────────────────────────
|
||||
// VTank's own Monsters tab (docs/research/vtank-kb/08-ui-views.md §1) is
|
||||
// one 23-column HudList — 14 check flags, 7 text cells (each a per-cell
|
||||
// click target, not a select-then-edit surface), 2 icon reorder cells —
|
||||
// plus txtNewMonster/Add/AddSel below. There is no separate expression
|
||||
// editor, priority stepper, damage menu, or equip-from-selection row in
|
||||
// VTank at all; every one of those was this plugin's own pre-S7.3
|
||||
// adaptation to acdream's earlier single-column <list>. Replacing the
|
||||
// whole surface with per-row per-column bindings (below) is what actually
|
||||
// restores VTank's at-a-glance grid — see docs/research/vtank-kb/
|
||||
// 08-ui-views.md §5's "Monsters tab — the real gap".
|
||||
public string MonsterExpressionDraft => _monsterExpressionDraft;
|
||||
public string MonsterEditorNotice => _monsterEditorNotice;
|
||||
public IReadOnlyList<string> DamageTypeNames => MonsterDamageNames;
|
||||
public IReadOnlyList<string> ExtraVulnerabilityNames =>
|
||||
MonsterExtraVulnerabilityNames;
|
||||
public IReadOnlyList<string> PetDamageTypeNames => MonsterPetDamageNames;
|
||||
public string SelectedDamageType => DamageTypeDisplay(
|
||||
SelectedMonsterActions.DamageType);
|
||||
public string SelectedExtraVulnerability =>
|
||||
DamageTypeDisplay(SelectedMonsterActions.ExtraVulnerability);
|
||||
public string SelectedPetDamage => DamageTypeDisplay(
|
||||
SelectedMonsterActions.PetDamageType);
|
||||
public string MonsterPriorityText =>
|
||||
$"Priority {SelectedMonsterActions.BoundedPriority}";
|
||||
public string MonsterEquipmentText =>
|
||||
$"Weapon {ItemDisplayName(
|
||||
SelectedMonsterActions.WeaponObjectId,
|
||||
SelectedMonsterActions.WeaponName)} "
|
||||
+ $"Offhand {ItemDisplayName(
|
||||
SelectedMonsterActions.OffhandObjectId,
|
||||
SelectedMonsterActions.OffhandName)}";
|
||||
|
||||
public Action<int> SelectMonsterRule => SelectMonsterRuleCore;
|
||||
/// <summary>
|
||||
/// Read-only convenience readout consumed by the ITEMS tab (mosstank.xml,
|
||||
/// unaffected by this slice's scope), showing DEFAULT's configured
|
||||
/// weapon/offhand now that the Monsters tab itself has no "selected
|
||||
/// rule" concept left to read from.
|
||||
/// </summary>
|
||||
public string MonsterEquipmentText
|
||||
{
|
||||
get
|
||||
{
|
||||
MonsterRuleActions actions = _combatSettings.Rules
|
||||
.FirstOrDefault(static r => r.IsDefault)
|
||||
?.Actions ?? new MonsterRuleActions();
|
||||
return $"Weapon {ItemDisplayName(actions.WeaponObjectId, actions.WeaponName)} "
|
||||
+ $"Offhand {ItemDisplayName(actions.OffhandObjectId, actions.OffhandName)}";
|
||||
}
|
||||
}
|
||||
public int SelectedMonsterListRow => _monsterListSelectedRow;
|
||||
public Action<int> SelectMonsterListRow => row => _monsterListSelectedRow = row;
|
||||
public Action<string> SetMonsterExpressionDraft => value =>
|
||||
_monsterExpressionDraft = value;
|
||||
public Action<string> ApplyMonsterExpression => value =>
|
||||
public Action AddMonsterRule => () =>
|
||||
{
|
||||
_monsterExpressionDraft = value;
|
||||
ApplyMonsterExpressionCore();
|
||||
string expression = string.IsNullOrWhiteSpace(_monsterExpressionDraft)
|
||||
? "New monster"
|
||||
: _monsterExpressionDraft;
|
||||
AddMonsterRuleCore(expression);
|
||||
_monsterExpressionDraft = string.Empty;
|
||||
};
|
||||
public Action ApplyMonsterRule => ApplyMonsterExpressionCore;
|
||||
public Action AddMonsterRule => AddMonsterRuleCore;
|
||||
public Action AddSelectedMonster => AddSelectedMonsterCore;
|
||||
public Action RemoveMonsterRule => RemoveMonsterRuleCore;
|
||||
public Action MoveMonsterRuleUp => () => MoveMonsterRule(-1);
|
||||
public Action MoveMonsterRuleDown => () => MoveMonsterRule(1);
|
||||
public Action MonsterPriorityDown => () => UpdateSelectedMonsterActions(
|
||||
actions => actions with { Priority = Math.Max(-1, actions.Priority - 1) });
|
||||
public Action MonsterPriorityUp => () => UpdateSelectedMonsterActions(
|
||||
actions => actions with { Priority = Math.Min(4, actions.Priority + 1) });
|
||||
public Action<string> SelectMonsterDamage => value =>
|
||||
SetMonsterDamage(value, extra: false);
|
||||
public Action<string> SelectMonsterExtraVulnerability => value =>
|
||||
SetMonsterDamage(value, extra: true);
|
||||
public Action<string> SelectMonsterPetDamage => value =>
|
||||
{
|
||||
if (TryParseDamageType(value, out MonsterDamageType parsed))
|
||||
{
|
||||
UpdateSelectedMonsterActions(actions => actions with
|
||||
{
|
||||
PetDamageType = parsed,
|
||||
});
|
||||
}
|
||||
};
|
||||
public Action SetMonsterWeapon => () => SetSelectedMonsterEquipment(offhand: false);
|
||||
public Action SetMonsterOffhand => () => SetSelectedMonsterEquipment(offhand: true);
|
||||
public Action ClearMonsterEquipment => () => UpdateSelectedMonsterActions(
|
||||
actions => actions with
|
||||
{
|
||||
WeaponObjectId = 0u,
|
||||
OffhandObjectId = 0u,
|
||||
WeaponName = string.Empty,
|
||||
OffhandName = string.Empty,
|
||||
});
|
||||
|
||||
public bool MonsterFester => HasMonsterFlag(MonsterActionFlags.Fester);
|
||||
public bool MonsterBroadside => HasMonsterFlag(MonsterActionFlags.Broadside);
|
||||
public bool MonsterGravityWell => HasMonsterFlag(MonsterActionFlags.GravityWell);
|
||||
public bool MonsterImperil => HasMonsterFlag(MonsterActionFlags.Imperil);
|
||||
public bool MonsterYield => HasMonsterFlag(MonsterActionFlags.Yield);
|
||||
public bool MonsterVulnerability => HasMonsterFlag(MonsterActionFlags.Vulnerability);
|
||||
public bool MonsterAttack => HasMonsterFlag(MonsterActionFlags.Attack);
|
||||
public bool MonsterRing => HasMonsterFlag(MonsterActionFlags.Ring);
|
||||
public bool MonsterStreak => HasMonsterFlag(MonsterActionFlags.Streak);
|
||||
public bool MonsterWeakening => HasMonsterFlag(MonsterActionFlags.WeakeningCurse);
|
||||
public bool MonsterFestering => HasMonsterFlag(MonsterActionFlags.FesteringCurse);
|
||||
public bool MonsterCorruption => HasMonsterFlag(MonsterActionFlags.Corruption);
|
||||
public bool MonsterDestructive => HasMonsterFlag(MonsterActionFlags.DestructiveCurse);
|
||||
public bool MonsterCorrosion => HasMonsterFlag(MonsterActionFlags.Corrosion);
|
||||
public Action ToggleMonsterFester => () => ToggleMonsterFlag(MonsterActionFlags.Fester);
|
||||
public Action ToggleMonsterBroadside => () => ToggleMonsterFlag(MonsterActionFlags.Broadside);
|
||||
public Action ToggleMonsterGravityWell => () => ToggleMonsterFlag(MonsterActionFlags.GravityWell);
|
||||
public Action ToggleMonsterImperil => () => ToggleMonsterFlag(MonsterActionFlags.Imperil);
|
||||
public Action ToggleMonsterYield => () => ToggleMonsterFlag(MonsterActionFlags.Yield);
|
||||
public Action ToggleMonsterVulnerability => () => ToggleMonsterFlag(MonsterActionFlags.Vulnerability);
|
||||
public Action ToggleMonsterAttack => () => ToggleMonsterFlag(MonsterActionFlags.Attack);
|
||||
public Action ToggleMonsterRing => () => ToggleMonsterFlag(MonsterActionFlags.Ring);
|
||||
public Action ToggleMonsterStreak => () => ToggleMonsterFlag(MonsterActionFlags.Streak);
|
||||
public Action ToggleMonsterWeakening => () => ToggleMonsterFlag(MonsterActionFlags.WeakeningCurse);
|
||||
public Action ToggleMonsterFestering => () => ToggleMonsterFlag(MonsterActionFlags.FesteringCurse);
|
||||
public Action ToggleMonsterCorruption => () => ToggleMonsterFlag(MonsterActionFlags.Corruption);
|
||||
public Action ToggleMonsterDestructive => () => ToggleMonsterFlag(MonsterActionFlags.DestructiveCurse);
|
||||
public Action ToggleMonsterCorrosion => () => ToggleMonsterFlag(MonsterActionFlags.Corrosion);
|
||||
// ── the 14 check columns (decompile cases 0-13; order F B G I Y V A R S
|
||||
// WC FC Cp DC Cs matches MonsterActionFlags's own declared bit order) ──
|
||||
public IReadOnlyList<bool> MonsterFesterColumn => MonsterFlagColumn(MonsterActionFlags.Fester);
|
||||
public IReadOnlyList<bool> MonsterBroadsideColumn => MonsterFlagColumn(MonsterActionFlags.Broadside);
|
||||
public IReadOnlyList<bool> MonsterGravityWellColumn => MonsterFlagColumn(MonsterActionFlags.GravityWell);
|
||||
public IReadOnlyList<bool> MonsterImperilColumn => MonsterFlagColumn(MonsterActionFlags.Imperil);
|
||||
public IReadOnlyList<bool> MonsterYieldColumn => MonsterFlagColumn(MonsterActionFlags.Yield);
|
||||
public IReadOnlyList<bool> MonsterVulnerabilityColumn => MonsterFlagColumn(MonsterActionFlags.Vulnerability);
|
||||
// Case 6 ("A") assigns inverted in VTank's own obfuscated field
|
||||
// (a10.t = !checkbox), but that inversion is purely an artifact of
|
||||
// VTank's internal storage — the CHECKBOX ITSELF still shows "checked
|
||||
// means will-attack" on screen (VTank repopulates the cell by inverting
|
||||
// the field back). Our MonsterActionFlags.Attack is already declared in
|
||||
// the un-inverted, on-screen sense, so no inversion is needed here: this
|
||||
// column reads/writes Attack directly and shows exactly what VTank shows.
|
||||
public IReadOnlyList<bool> MonsterAttackColumn => MonsterFlagColumn(MonsterActionFlags.Attack);
|
||||
public IReadOnlyList<bool> MonsterRingColumn => MonsterFlagColumn(MonsterActionFlags.Ring);
|
||||
public IReadOnlyList<bool> MonsterStreakColumn => MonsterFlagColumn(MonsterActionFlags.Streak);
|
||||
public IReadOnlyList<bool> MonsterWeakeningColumn => MonsterFlagColumn(MonsterActionFlags.WeakeningCurse);
|
||||
public IReadOnlyList<bool> MonsterFesteringColumn => MonsterFlagColumn(MonsterActionFlags.FesteringCurse);
|
||||
public IReadOnlyList<bool> MonsterCorruptionColumn => MonsterFlagColumn(MonsterActionFlags.Corruption);
|
||||
public IReadOnlyList<bool> MonsterDestructiveColumn => MonsterFlagColumn(MonsterActionFlags.DestructiveCurse);
|
||||
public IReadOnlyList<bool> MonsterCorrosionColumn => MonsterFlagColumn(MonsterActionFlags.Corrosion);
|
||||
|
||||
public Action<int> ToggleMonsterFesterAt => row => ToggleMonsterFlagAt(row, MonsterActionFlags.Fester);
|
||||
public Action<int> ToggleMonsterBroadsideAt => row => ToggleMonsterFlagAt(row, MonsterActionFlags.Broadside);
|
||||
public Action<int> ToggleMonsterGravityWellAt => row => ToggleMonsterFlagAt(row, MonsterActionFlags.GravityWell);
|
||||
public Action<int> ToggleMonsterImperilAt => row => ToggleMonsterFlagAt(row, MonsterActionFlags.Imperil);
|
||||
public Action<int> ToggleMonsterYieldAt => row => ToggleMonsterFlagAt(row, MonsterActionFlags.Yield);
|
||||
public Action<int> ToggleMonsterVulnerabilityAt => row => ToggleMonsterFlagAt(row, MonsterActionFlags.Vulnerability);
|
||||
public Action<int> ToggleMonsterAttackAt => row => ToggleMonsterFlagAt(row, MonsterActionFlags.Attack);
|
||||
public Action<int> ToggleMonsterRingAt => row => ToggleMonsterFlagAt(row, MonsterActionFlags.Ring);
|
||||
public Action<int> ToggleMonsterStreakAt => row => ToggleMonsterFlagAt(row, MonsterActionFlags.Streak);
|
||||
public Action<int> ToggleMonsterWeakeningAt => row => ToggleMonsterFlagAt(row, MonsterActionFlags.WeakeningCurse);
|
||||
public Action<int> ToggleMonsterFesteringAt => row => ToggleMonsterFlagAt(row, MonsterActionFlags.FesteringCurse);
|
||||
public Action<int> ToggleMonsterCorruptionAt => row => ToggleMonsterFlagAt(row, MonsterActionFlags.Corruption);
|
||||
public Action<int> ToggleMonsterDestructiveAt => row => ToggleMonsterFlagAt(row, MonsterActionFlags.DestructiveCurse);
|
||||
public Action<int> ToggleMonsterCorrosionAt => row => ToggleMonsterFlagAt(row, MonsterActionFlags.Corrosion);
|
||||
|
||||
// ── the 7 text columns (decompile cases 14-20) ──────────────────────────
|
||||
public IReadOnlyList<string> MonsterNameColumn =>
|
||||
_combatSettings.Rules.Select(static r => r.Expression).ToArray();
|
||||
public IReadOnlyList<string> MonsterPriorityColumn =>
|
||||
_combatSettings.Rules.Select(
|
||||
static r => r.Actions.BoundedPriority.ToString(CultureInfo.InvariantCulture))
|
||||
.ToArray();
|
||||
public IReadOnlyList<string> MonsterDamageColumn =>
|
||||
_combatSettings.Rules.Select(static r => DamageTypeDisplay(r.Actions.DamageType)).ToArray();
|
||||
public IReadOnlyList<string> MonsterExtraVulnColumn =>
|
||||
_combatSettings.Rules.Select(static r => DamageTypeDisplay(r.Actions.ExtraVulnerability)).ToArray();
|
||||
public IReadOnlyList<string> MonsterWeaponColumn =>
|
||||
_combatSettings.Rules.Select(
|
||||
r => ItemDisplayName(r.Actions.WeaponObjectId, r.Actions.WeaponName))
|
||||
.ToArray();
|
||||
public IReadOnlyList<string> MonsterOffhandColumn =>
|
||||
_combatSettings.Rules.Select(
|
||||
r => ItemDisplayName(r.Actions.OffhandObjectId, r.Actions.OffhandName))
|
||||
.ToArray();
|
||||
public IReadOnlyList<string> MonsterPetDamageColumn =>
|
||||
_combatSettings.Rules.Select(static r => DamageTypeDisplay(r.Actions.PetDamageType)).ToArray();
|
||||
|
||||
// Case 14: clicking the Name cell of any non-DEFAULT row deletes it
|
||||
// (PluginCore.cs:7948-7955's `if (A_1 != 0) { ...; b(A_1); }`, where
|
||||
// `b(int)` is the row-delete helper at PluginCore.cs:8168-8179). VTank
|
||||
// guards on row index 0; we guard on the rule's own IsDefault so the
|
||||
// invariant holds even if a caller reorders DEFAULT away from row 0.
|
||||
public Action<int> DeleteMonsterRuleAt => DeleteMonsterRuleAtCore;
|
||||
// Case 15: priority cycles -1 -> 0 -> 1 -> 2 -> 3 -> 4 -> -1
|
||||
// (PluginCore.cs:7956-7967).
|
||||
public Action<int> CycleMonsterPriorityAt => CycleMonsterPriorityAtCore;
|
||||
// Case 16: Dmg type cycles every eDamageElement 0..13
|
||||
// (PluginCore.cs:7968-7979).
|
||||
public Action<int> CycleMonsterDamageAt => row => UpdateMonsterActionsAt(
|
||||
row, actions => actions with { DamageType = CycleDamage(actions.DamageType, MonsterDamageCycle) });
|
||||
// Case 17: Ex. Vuln's non-contiguous cycle (PluginCore.cs:7980-7993).
|
||||
public Action<int> CycleMonsterExtraVulnerabilityAt => row => UpdateMonsterActionsAt(
|
||||
row, actions => actions with
|
||||
{
|
||||
ExtraVulnerability = CycleDamage(actions.ExtraVulnerability, MonsterExtraVulnerabilityCycle),
|
||||
});
|
||||
// Cases 18/19: VTank cycles a runtime list of OWNED weapon-TYPE ids
|
||||
// (dz.k.j(), PluginCore.cs:7994-8074) filtered by an opaque item-kind
|
||||
// classifier we cannot resolve from the decompile alone. MossTank has no
|
||||
// "weapon type" concept at all — Weapon/Offhand are always concrete owned
|
||||
// ITEMS (WeaponObjectId/WeaponName), matching the pre-existing "Weapon /
|
||||
// Offhand <- selected" design this slice retires. Deliberate adaptation:
|
||||
// cycle the same REGISTERED weapon roster the Items tab already
|
||||
// maintains (_combatSettings.CombatItemNames, ordinal-sorted for a
|
||||
// deterministic cycle order) instead of VTank's type-id list, preserving
|
||||
// the player-facing contract ("click cycles the equip choice, wraps
|
||||
// through <AUTO>") without inventing a type abstraction this slice's
|
||||
// scope doesn't call for.
|
||||
public Action<int> CycleMonsterWeaponAt => row => CycleMonsterEquipmentAt(row, offhand: false);
|
||||
public Action<int> CycleMonsterOffhandAt => row => CycleMonsterEquipmentAt(row, offhand: true);
|
||||
// Case 20: PetDmg's non-contiguous cycle (PluginCore.cs:8075-8089).
|
||||
public Action<int> CycleMonsterPetDamageAt => row => UpdateMonsterActionsAt(
|
||||
row, actions => actions with { PetDamageType = CycleDamage(actions.PetDamageType, MonsterPetDamageCycle) });
|
||||
|
||||
// ── the 2 icon columns (decompile cases 21/22 — move up/down; DAT ids
|
||||
// 0x060028FC/0x060028FD, docs/plans/2026-09-07-campaign-vt-slice7-tabs.md
|
||||
// S7.3) ──────────────────────────────────────────────────────────────
|
||||
public IReadOnlyList<uint> MonsterMoveUpIcons =>
|
||||
Enumerable.Repeat(0x060028FCu, _combatSettings.Rules.Count).ToArray();
|
||||
public IReadOnlyList<uint> MonsterMoveDownIcons =>
|
||||
Enumerable.Repeat(0x060028FDu, _combatSettings.Rules.Count).ToArray();
|
||||
public Action<int> MoveMonsterRuleUpAt => row => MoveMonsterRuleAtCore(row, -1);
|
||||
public Action<int> MoveMonsterRuleDownAt => row => MoveMonsterRuleAtCore(row, 1);
|
||||
|
||||
/// <summary>Vitals line, using the same numbers the character panel shows.</summary>
|
||||
public string Vitals => _vitals;
|
||||
|
|
@ -1948,85 +2019,18 @@ internal sealed partial class MossTankPanel
|
|||
}
|
||||
}
|
||||
|
||||
private MonsterRule SelectedMonsterRule => _combatSettings.Rules.Count == 0
|
||||
? new MonsterRule("DEFAULT", 0)
|
||||
: _combatSettings.Rules[Math.Clamp(
|
||||
_selectedMonsterRule,
|
||||
0,
|
||||
_combatSettings.Rules.Count - 1)];
|
||||
private MonsterRuleActions SelectedMonsterActions => SelectedMonsterRule.Actions;
|
||||
|
||||
private void RefreshMonsterEditor(bool retainDraft = false)
|
||||
/// <summary>VTank: row 0 is always DEFAULT and can never be empty.</summary>
|
||||
private void EnsureDefaultMonsterRule()
|
||||
{
|
||||
if (_combatSettings.Rules.Count == 0)
|
||||
_combatSettings.Rules.Add(new MonsterRule("DEFAULT", 0));
|
||||
_selectedMonsterRule = Math.Clamp(
|
||||
_selectedMonsterRule,
|
||||
0,
|
||||
_combatSettings.Rules.Count - 1);
|
||||
if (!retainDraft)
|
||||
_monsterExpressionDraft = SelectedMonsterRule.Expression;
|
||||
_monsterRows = _combatSettings.Rules.Select(FormatMonsterRow).ToArray();
|
||||
}
|
||||
|
||||
private static string FormatMonsterRow(MonsterRule rule)
|
||||
{
|
||||
MonsterRuleActions actions = rule.Actions;
|
||||
static char Mark(MonsterActionFlags flags, MonsterActionFlags flag) =>
|
||||
(flags & flag) != 0 ? '●' : '○';
|
||||
return string.Concat(
|
||||
Mark(actions.Flags, MonsterActionFlags.Fester), " ",
|
||||
Mark(actions.Flags, MonsterActionFlags.Broadside), " ",
|
||||
Mark(actions.Flags, MonsterActionFlags.GravityWell), " ",
|
||||
Mark(actions.Flags, MonsterActionFlags.Imperil), " ",
|
||||
Mark(actions.Flags, MonsterActionFlags.Yield), " ",
|
||||
Mark(actions.Flags, MonsterActionFlags.Vulnerability), " ",
|
||||
Mark(actions.Flags, MonsterActionFlags.Attack), " ",
|
||||
Mark(actions.Flags, MonsterActionFlags.Ring), " ",
|
||||
Mark(actions.Flags, MonsterActionFlags.Streak), " ",
|
||||
Mark(actions.Flags, MonsterActionFlags.WeakeningCurse), " ",
|
||||
Mark(actions.Flags, MonsterActionFlags.FesteringCurse), " ",
|
||||
Mark(actions.Flags, MonsterActionFlags.Corruption), " ",
|
||||
Mark(actions.Flags, MonsterActionFlags.DestructiveCurse), " ",
|
||||
Mark(actions.Flags, MonsterActionFlags.Corrosion), " ",
|
||||
rule.Expression, " P", actions.BoundedPriority.ToString(
|
||||
CultureInfo.InvariantCulture), " ", actions.DamageType.ToString());
|
||||
}
|
||||
private IReadOnlyList<bool> MonsterFlagColumn(MonsterActionFlags flag) =>
|
||||
_combatSettings.Rules.Select(r => (r.Actions.Flags & flag) != 0).ToArray();
|
||||
|
||||
private void SelectMonsterRuleCore(int index)
|
||||
{
|
||||
if (index < 0 || index >= _combatSettings.Rules.Count)
|
||||
return;
|
||||
_selectedMonsterRule = index;
|
||||
_monsterExpressionDraft = SelectedMonsterRule.Expression;
|
||||
_monsterEditorNotice = $"Editing row {index + 1}.";
|
||||
RefreshMonsterEditor();
|
||||
}
|
||||
|
||||
private void ApplyMonsterExpressionCore()
|
||||
{
|
||||
string expression = _monsterExpressionDraft.Trim();
|
||||
if (expression.Length == 0)
|
||||
{
|
||||
_monsterEditorNotice = "Monster expression cannot be empty.";
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
_combatSettings.Rules[_selectedMonsterRule] = new MonsterRule(
|
||||
expression,
|
||||
SelectedMonsterActions);
|
||||
_monsterEditorNotice = $"Updated {expression}.";
|
||||
RefreshMonsterEditor();
|
||||
SaveProfile();
|
||||
}
|
||||
catch (FormatException error)
|
||||
{
|
||||
_monsterEditorNotice = error.Message;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddMonsterRuleCore() => AddMonsterRuleCore("New monster");
|
||||
private void ToggleMonsterFlagAt(int row, MonsterActionFlags flag) =>
|
||||
UpdateMonsterActionsAt(row, actions => actions with { Flags = actions.Flags ^ flag });
|
||||
|
||||
private void AddMonsterRuleCore(string expression)
|
||||
{
|
||||
|
|
@ -2034,9 +2038,7 @@ internal sealed partial class MossTankPanel
|
|||
{
|
||||
var rule = new MonsterRule(expression, new MonsterRuleActions());
|
||||
_combatSettings.Rules.Add(rule);
|
||||
_selectedMonsterRule = _combatSettings.Rules.Count - 1;
|
||||
_monsterEditorNotice = $"Added {expression}.";
|
||||
RefreshMonsterEditor();
|
||||
SaveProfile();
|
||||
}
|
||||
catch (FormatException error)
|
||||
|
|
@ -2084,53 +2086,82 @@ internal sealed partial class MossTankPanel
|
|||
return result.ToString();
|
||||
}
|
||||
|
||||
private void RemoveMonsterRuleCore()
|
||||
private void DeleteMonsterRuleAtCore(int row)
|
||||
{
|
||||
if (SelectedMonsterRule.IsDefault)
|
||||
if (row < 0 || row >= _combatSettings.Rules.Count)
|
||||
return;
|
||||
MonsterRule rule = _combatSettings.Rules[row];
|
||||
if (rule.IsDefault)
|
||||
{
|
||||
_monsterEditorNotice = "DEFAULT cannot be removed.";
|
||||
return;
|
||||
}
|
||||
string removed = SelectedMonsterRule.Expression;
|
||||
_combatSettings.Rules.RemoveAt(_selectedMonsterRule);
|
||||
_selectedMonsterRule = Math.Min(
|
||||
_selectedMonsterRule,
|
||||
_combatSettings.Rules.Count - 1);
|
||||
_monsterEditorNotice = $"Removed {removed}.";
|
||||
RefreshMonsterEditor();
|
||||
_combatSettings.Rules.RemoveAt(row);
|
||||
_monsterEditorNotice = $"Removed {rule.Expression}.";
|
||||
EnsureDefaultMonsterRule();
|
||||
SaveProfile();
|
||||
}
|
||||
|
||||
private void MoveMonsterRule(int direction)
|
||||
{
|
||||
int destination = _selectedMonsterRule + Math.Sign(direction);
|
||||
if (destination < 0 || destination >= _combatSettings.Rules.Count)
|
||||
return;
|
||||
MonsterRule current = _combatSettings.Rules[_selectedMonsterRule];
|
||||
_combatSettings.Rules.RemoveAt(_selectedMonsterRule);
|
||||
_combatSettings.Rules.Insert(destination, current);
|
||||
_selectedMonsterRule = destination;
|
||||
_monsterEditorNotice = $"Moved {current.Expression}.";
|
||||
RefreshMonsterEditor();
|
||||
SaveProfile();
|
||||
}
|
||||
|
||||
private bool HasMonsterFlag(MonsterActionFlags flag) =>
|
||||
(SelectedMonsterActions.Flags & flag) != 0;
|
||||
|
||||
private void ToggleMonsterFlag(MonsterActionFlags flag) =>
|
||||
UpdateSelectedMonsterActions(actions => actions with
|
||||
private void CycleMonsterPriorityAtCore(int row) =>
|
||||
UpdateMonsterActionsAt(row, actions =>
|
||||
{
|
||||
Flags = actions.Flags ^ flag,
|
||||
int next = actions.BoundedPriority + 1;
|
||||
if (next == 5)
|
||||
next = -1;
|
||||
return actions with { Priority = next };
|
||||
});
|
||||
|
||||
private void SetMonsterDamage(string value, bool extra)
|
||||
private static MonsterDamageType CycleDamage(
|
||||
MonsterDamageType current, MonsterDamageType[] cycle)
|
||||
{
|
||||
if (!TryParseDamageType(value, out MonsterDamageType parsed))
|
||||
return;
|
||||
UpdateSelectedMonsterActions(actions => extra
|
||||
? actions with { ExtraVulnerability = parsed }
|
||||
: actions with { DamageType = parsed });
|
||||
int index = Array.IndexOf(cycle, current);
|
||||
int next = index < 0 ? 0 : (index + 1) % cycle.Length;
|
||||
return cycle[next];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cases 18/19's adaptation (see the public CycleMonsterWeaponAt/
|
||||
/// CycleMonsterOffhandAt doc above): AUTO (no name, id 0) -> the
|
||||
/// ordinal-sorted registered weapon roster -> AUTO, wrapping.
|
||||
/// </summary>
|
||||
private void CycleMonsterEquipmentAt(int row, bool offhand)
|
||||
{
|
||||
string[] names = _combatSettings.CombatItemNames
|
||||
.OrderBy(static n => n, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
UpdateMonsterActionsAt(row, actions =>
|
||||
{
|
||||
string currentName = offhand ? actions.OffhandName : actions.WeaponName;
|
||||
int currentIndex = string.IsNullOrEmpty(currentName)
|
||||
? -1
|
||||
: Array.IndexOf(names, currentName);
|
||||
if (names.Length == 0)
|
||||
return SetWeaponSlot(actions, offhand, null);
|
||||
int nextIndex = currentIndex + 1;
|
||||
return nextIndex >= names.Length
|
||||
? SetWeaponSlot(actions, offhand, null)
|
||||
: SetWeaponSlot(actions, offhand, names[nextIndex]);
|
||||
});
|
||||
}
|
||||
|
||||
private MonsterRuleActions SetWeaponSlot(
|
||||
MonsterRuleActions actions, bool offhand, string? name)
|
||||
{
|
||||
uint objectId = 0u;
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
foreach (PluginInventoryItem item in _host.Automation.Items.CaptureOwnedItems())
|
||||
{
|
||||
if (string.Equals(item.Name, name, StringComparison.Ordinal))
|
||||
{
|
||||
objectId = item.ObjectId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return offhand
|
||||
? actions with { OffhandObjectId = objectId, OffhandName = name ?? string.Empty }
|
||||
: actions with { WeaponObjectId = objectId, WeaponName = name ?? string.Empty };
|
||||
}
|
||||
|
||||
private static string DamageTypeDisplay(MonsterDamageType value) => value switch
|
||||
|
|
@ -2142,49 +2173,6 @@ internal sealed partial class MossTankPanel
|
|||
_ => value.ToString(),
|
||||
};
|
||||
|
||||
private static bool TryParseDamageType(
|
||||
string value,
|
||||
out MonsterDamageType parsed)
|
||||
{
|
||||
parsed = value.Trim() switch
|
||||
{
|
||||
string name when name.Equals("Lightning", StringComparison.OrdinalIgnoreCase) =>
|
||||
MonsterDamageType.Electric,
|
||||
string name when name.Equals("Void Basic", StringComparison.OrdinalIgnoreCase) =>
|
||||
MonsterDamageType.VoidBasic,
|
||||
string name when name.Equals("Drain Auto", StringComparison.OrdinalIgnoreCase) =>
|
||||
MonsterDamageType.DrainAuto,
|
||||
string name when name.Equals("PAuto", StringComparison.OrdinalIgnoreCase) =>
|
||||
MonsterDamageType.PlayerAuto,
|
||||
_ => (MonsterDamageType)(-1),
|
||||
};
|
||||
return (int)parsed >= 0
|
||||
|| Enum.TryParse(value, ignoreCase: true, out parsed);
|
||||
}
|
||||
|
||||
private void SetSelectedMonsterEquipment(bool offhand)
|
||||
{
|
||||
if (!TryGetSelectedInventoryItem(out PluginInventoryItem item))
|
||||
{
|
||||
_monsterEditorNotice = "Select an owned weapon or offhand item first.";
|
||||
return;
|
||||
}
|
||||
_combatSettings.CombatItemNames.Add(item.Name);
|
||||
_combatSettings.CombatItemObjectIds.Add(item.ObjectId);
|
||||
RefreshItemEditors();
|
||||
UpdateSelectedMonsterActions(actions => offhand
|
||||
? actions with
|
||||
{
|
||||
OffhandObjectId = item.ObjectId,
|
||||
OffhandName = item.Name,
|
||||
}
|
||||
: actions with
|
||||
{
|
||||
WeaponObjectId = item.ObjectId,
|
||||
WeaponName = item.Name,
|
||||
});
|
||||
}
|
||||
|
||||
private string ItemDisplayName(uint objectId, string durableName)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(durableName))
|
||||
|
|
@ -2200,15 +2188,42 @@ internal sealed partial class MossTankPanel
|
|||
return $"0x{objectId:X8}";
|
||||
}
|
||||
|
||||
private void UpdateSelectedMonsterActions(
|
||||
Func<MonsterRuleActions, MonsterRuleActions> update)
|
||||
/// <summary>
|
||||
/// Cases 21/22: move up/down, guarded so DEFAULT never moves and never
|
||||
/// gets displaced from its slot (VTank's own guard is asymmetric — case
|
||||
/// 21's `A_1 >= 2` keeps MoveUp off row 0, but case 22's MoveDown has no
|
||||
/// matching guard, PluginCore.cs:8090-8105 — a literal port would let
|
||||
/// MoveDown push DEFAULT out of place. Pinning DEFAULT symmetrically is a
|
||||
/// deliberate, safer choice: it preserves the same OBSERVABLE outcome
|
||||
/// case 21 already enforces (DEFAULT never moves) without reproducing
|
||||
/// what reads as an asymmetric VTank oversight rather than intended
|
||||
/// behavior.
|
||||
/// </summary>
|
||||
private void MoveMonsterRuleAtCore(int row, int direction)
|
||||
{
|
||||
MonsterRule selected = SelectedMonsterRule;
|
||||
_combatSettings.Rules[_selectedMonsterRule] = new MonsterRule(
|
||||
selected.Expression,
|
||||
update(selected.Actions));
|
||||
_monsterEditorNotice = $"Updated {selected.Expression}.";
|
||||
RefreshMonsterEditor();
|
||||
if (row < 0 || row >= _combatSettings.Rules.Count)
|
||||
return;
|
||||
MonsterRule current = _combatSettings.Rules[row];
|
||||
if (current.IsDefault)
|
||||
return;
|
||||
int destination = row + direction;
|
||||
if (destination < 0 || destination >= _combatSettings.Rules.Count)
|
||||
return;
|
||||
if (_combatSettings.Rules[destination].IsDefault)
|
||||
return;
|
||||
_combatSettings.Rules.RemoveAt(row);
|
||||
_combatSettings.Rules.Insert(destination, current);
|
||||
_monsterEditorNotice = $"Moved {current.Expression}.";
|
||||
SaveProfile();
|
||||
}
|
||||
|
||||
private void UpdateMonsterActionsAt(
|
||||
int row, Func<MonsterRuleActions, MonsterRuleActions> update)
|
||||
{
|
||||
if (row < 0 || row >= _combatSettings.Rules.Count)
|
||||
return;
|
||||
MonsterRule current = _combatSettings.Rules[row];
|
||||
_combatSettings.Rules[row] = new MonsterRule(current.Expression, update(current.Actions));
|
||||
SaveProfile();
|
||||
}
|
||||
|
||||
|
|
@ -3619,7 +3634,7 @@ internal sealed partial class MossTankPanel
|
|||
_navigation.Reset();
|
||||
_coverageSpellSnapshot = null;
|
||||
_coverageRefreshRemaining = 0d;
|
||||
RefreshMonsterEditor();
|
||||
EnsureDefaultMonsterRule();
|
||||
RefreshItemEditors();
|
||||
RefreshLootEditor();
|
||||
RefreshRouteEditor();
|
||||
|
|
|
|||
|
|
@ -308,66 +308,77 @@
|
|||
<label x="360" y="88" w="290" h="16" text="{Vitals}" color="#FFE8DEC3" />
|
||||
</group>
|
||||
|
||||
<!-- Monsters: VTank's ordered expression/action table. -->
|
||||
<!-- Monsters: VTank's own 23-column HudList grid, transcribed control-for-
|
||||
control (docs/research/vtank-kb/08-ui-views.md §1 "Tab: Monsters";
|
||||
Campaign VT slice 7 S7.3). Every column pitch below is
|
||||
fixedwidth+7 (VVS's own WPadding baked into the cell, per
|
||||
docs/plugin-ui-markup.md's "PITCH convention"), and every check
|
||||
column uses VVS's forced 13px check width (13+7=20) regardless of
|
||||
the KB table's own declared 16. No selected-row editor: every real
|
||||
cell is its own click target, exactly like VTank's own row (VVS's
|
||||
HudList has no header row either — the 15 <label>s below are
|
||||
ordinary StaticText placed above the list, same as mainView.xml). -->
|
||||
<group x="8" y="42" w="848" h="194" visible="{MonstersVisible}">
|
||||
<label x="4" y="0" text="F B G I Y V A R S W FC Cp DC Cs Name / expression P Dmg type"
|
||||
color="#FFE8DEC3" />
|
||||
<list x="4" y="16" w="776" h="62" rowheight="15"
|
||||
items="{MonsterRows}" selected="{SelectedMonsterRuleIndex}"
|
||||
onchange="{SelectMonsterRule}" tooltip="Select an ordered monster rule to edit." />
|
||||
<label x="0" y="0" w="20" text="F" color="#FFE8DEC3" tooltip="Fester" />
|
||||
<label x="20" y="0" w="20" text="B" color="#FFE8DEC3" tooltip="Broadside of a Barn" />
|
||||
<label x="40" y="0" w="20" text="G" color="#FFE8DEC3" tooltip="Gravity Well" />
|
||||
<label x="60" y="0" w="20" text="I" color="#FFE8DEC3" tooltip="Imperil" />
|
||||
<label x="80" y="0" w="20" text="Y" color="#FFE8DEC3" tooltip="Yield" />
|
||||
<label x="100" y="0" w="20" text="V" color="#FFE8DEC3" tooltip="Vuln (Element)" />
|
||||
<label x="120" y="0" w="20" text="A" color="#FFE8DEC3" tooltip="Attack" />
|
||||
<label x="140" y="0" w="20" text="R" color="#FFE8DEC3" tooltip="Ring Spell" />
|
||||
<label x="160" y="0" w="20" text="S" color="#FFE8DEC3" tooltip="Streak" />
|
||||
<label x="180" y="0" w="20" text="WC" color="#FFE8DEC3" tooltip="Weakening Curse" />
|
||||
<label x="200" y="0" w="20" text="FC" color="#FFE8DEC3" tooltip="Festering Curse" />
|
||||
<label x="220" y="0" w="20" text="Cp" color="#FFE8DEC3" tooltip="Corruption" />
|
||||
<label x="240" y="0" w="20" text="DC" color="#FFE8DEC3" tooltip="Destructive Curse" />
|
||||
<label x="260" y="0" w="20" text="Cs" color="#FFE8DEC3" tooltip="Corrosion" />
|
||||
<label x="280" y="0" w="127" text="Name" color="#FFE8DEC3" />
|
||||
<label x="407" y="0" w="27" text="P" color="#FFE8DEC3" tooltip="Priority" />
|
||||
<label x="434" y="0" w="63" text="Dmg type" color="#FFE8DEC3" />
|
||||
<label x="497" y="0" w="63" text="Ex. Vuln" color="#FFE8DEC3" />
|
||||
<label x="560" y="0" w="87" text="Weapon" color="#FFE8DEC3" />
|
||||
<label x="647" y="0" w="87" text="Offhand" color="#FFE8DEC3" />
|
||||
<label x="734" y="0" w="63" text="PetDmg" color="#FFE8DEC3" />
|
||||
<!-- clMoveUp/clMoveDown (#22/#23) carry no header label in VTank's own
|
||||
mainView.xml either (KB table: "-" for both) — skipped to match. -->
|
||||
|
||||
<field x="4" y="82" w="210" h="21" text="{MonsterExpressionDraft}"
|
||||
onchange="{SetMonsterExpressionDraft}" onsubmit="{ApplyMonsterExpression}"
|
||||
maxlength="256" background="#E6000000" color="#FFE8DEC3"
|
||||
tooltip="Monster name or UtilityBelt-compatible match expression." />
|
||||
<button x="220" y="82" w="52" h="21" text="Apply" onclick="{ApplyMonsterRule}" />
|
||||
<button x="278" y="82" w="46" h="21" text="Add" onclick="{AddMonsterRule}" />
|
||||
<button x="330" y="82" w="62" h="21" text="Add Sel" onclick="{AddSelectedMonster}" />
|
||||
<button x="398" y="82" w="62" h="21" text="Remove" onclick="{RemoveMonsterRule}" />
|
||||
<button x="466" y="82" w="28" h="21" text="↑" onclick="{MoveMonsterRuleUp}"
|
||||
tooltip="Move the selected monster rule up." />
|
||||
<button x="500" y="82" w="28" h="21" text="↓" onclick="{MoveMonsterRuleDown}"
|
||||
tooltip="Move the selected monster rule down." />
|
||||
<label x="538" y="86" text="{MonsterPriorityText}" color="#FFC7B98F" />
|
||||
<button x="632" y="82" w="28" h="21" text="-" onclick="{MonsterPriorityDown}"
|
||||
tooltip="Decrease the selected monster rule priority." />
|
||||
<button x="666" y="82" w="28" h="21" text="+" onclick="{MonsterPriorityUp}"
|
||||
tooltip="Increase the selected monster rule priority." />
|
||||
<list x="0" y="16" w="848" h="116"
|
||||
selected="{SelectedMonsterListRow}" onchange="{SelectMonsterListRow}"
|
||||
tooltip="Click a check to toggle it, a name to delete the rule, or another cell to cycle its value.">
|
||||
<column type="check" width="20" values="{MonsterFesterColumn}" onchange="{ToggleMonsterFesterAt}" />
|
||||
<column type="check" width="20" values="{MonsterBroadsideColumn}" onchange="{ToggleMonsterBroadsideAt}" />
|
||||
<column type="check" width="20" values="{MonsterGravityWellColumn}" onchange="{ToggleMonsterGravityWellAt}" />
|
||||
<column type="check" width="20" values="{MonsterImperilColumn}" onchange="{ToggleMonsterImperilAt}" />
|
||||
<column type="check" width="20" values="{MonsterYieldColumn}" onchange="{ToggleMonsterYieldAt}" />
|
||||
<column type="check" width="20" values="{MonsterVulnerabilityColumn}" onchange="{ToggleMonsterVulnerabilityAt}" />
|
||||
<column type="check" width="20" values="{MonsterAttackColumn}" onchange="{ToggleMonsterAttackAt}" />
|
||||
<column type="check" width="20" values="{MonsterRingColumn}" onchange="{ToggleMonsterRingAt}" />
|
||||
<column type="check" width="20" values="{MonsterStreakColumn}" onchange="{ToggleMonsterStreakAt}" />
|
||||
<column type="check" width="20" values="{MonsterWeakeningColumn}" onchange="{ToggleMonsterWeakeningAt}" />
|
||||
<column type="check" width="20" values="{MonsterFesteringColumn}" onchange="{ToggleMonsterFesteringAt}" />
|
||||
<column type="check" width="20" values="{MonsterCorruptionColumn}" onchange="{ToggleMonsterCorruptionAt}" />
|
||||
<column type="check" width="20" values="{MonsterDestructiveColumn}" onchange="{ToggleMonsterDestructiveAt}" />
|
||||
<column type="check" width="20" values="{MonsterCorrosionColumn}" onchange="{ToggleMonsterCorrosionAt}" />
|
||||
<column type="text" width="127" items="{MonsterNameColumn}" onclick="{DeleteMonsterRuleAt}" />
|
||||
<column type="text" width="27" items="{MonsterPriorityColumn}" onclick="{CycleMonsterPriorityAt}" />
|
||||
<column type="text" width="63" items="{MonsterDamageColumn}" onclick="{CycleMonsterDamageAt}" />
|
||||
<column type="text" width="63" items="{MonsterExtraVulnColumn}" onclick="{CycleMonsterExtraVulnerabilityAt}" />
|
||||
<column type="text" width="87" items="{MonsterWeaponColumn}" onclick="{CycleMonsterWeaponAt}" />
|
||||
<column type="text" width="87" items="{MonsterOffhandColumn}" onclick="{CycleMonsterOffhandAt}" />
|
||||
<column type="text" width="63" items="{MonsterPetDamageColumn}" onclick="{CycleMonsterPetDamageAt}" />
|
||||
<column type="icon" width="23" iconkind="did" values="{MonsterMoveUpIcons}" onclick="{MoveMonsterRuleUpAt}" />
|
||||
<column type="icon" width="23" iconkind="did" values="{MonsterMoveDownIcons}" onclick="{MoveMonsterRuleDownAt}" />
|
||||
</list>
|
||||
|
||||
<label x="4" y="110" text="Dmg" color="#FFE8DEC3" />
|
||||
<menu x="38" y="106" w="92" h="20" items="{DamageTypeNames}"
|
||||
selected="{SelectedDamageType}" onchange="{SelectMonsterDamage}"
|
||||
rows="8" openupward="true" tooltip="Choose the attack damage type for this rule." />
|
||||
<label x="140" y="110" text="Ex Vuln" color="#FFE8DEC3" />
|
||||
<menu x="194" y="106" w="92" h="20" items="{ExtraVulnerabilityNames}"
|
||||
selected="{SelectedExtraVulnerability}"
|
||||
onchange="{SelectMonsterExtraVulnerability}" rows="8" openupward="true"
|
||||
tooltip="Choose an extra vulnerability element for this rule." />
|
||||
<label x="296" y="110" text="Pet" color="#FFE8DEC3" />
|
||||
<menu x="324" y="106" w="92" h="20" items="{PetDamageTypeNames}"
|
||||
selected="{SelectedPetDamage}" onchange="{SelectMonsterPetDamage}"
|
||||
rows="8" openupward="true" tooltip="Choose the summoned-pet damage type for this rule." />
|
||||
<label x="432" y="110" text="{MonsterEditorNotice}" color="#FF9B9072" />
|
||||
|
||||
<toggle x="4" y="132" w="48" h="18" text="F" checked="{MonsterFester}" onclick="{ToggleMonsterFester}" tooltip="Fester Other" />
|
||||
<toggle x="56" y="132" w="48" h="18" text="B" checked="{MonsterBroadside}" onclick="{ToggleMonsterBroadside}" tooltip="Broadside of a Tumerok" />
|
||||
<toggle x="108" y="132" w="48" h="18" text="G" checked="{MonsterGravityWell}" onclick="{ToggleMonsterGravityWell}" tooltip="Gravity Well" />
|
||||
<toggle x="160" y="132" w="48" h="18" text="I" checked="{MonsterImperil}" onclick="{ToggleMonsterImperil}" tooltip="Imperil Other" />
|
||||
<toggle x="212" y="132" w="48" h="18" text="Y" checked="{MonsterYield}" onclick="{ToggleMonsterYield}" tooltip="Yield Other" />
|
||||
<toggle x="264" y="132" w="48" h="18" text="V" checked="{MonsterVulnerability}" onclick="{ToggleMonsterVulnerability}" tooltip="Elemental Vulnerability Other" />
|
||||
<toggle x="316" y="132" w="48" h="18" text="A" checked="{MonsterAttack}" onclick="{ToggleMonsterAttack}" tooltip="Attack this monster" />
|
||||
<toggle x="368" y="132" w="48" h="18" text="R" checked="{MonsterRing}" onclick="{ToggleMonsterRing}" tooltip="Allow ring spells" />
|
||||
<toggle x="420" y="132" w="48" h="18" text="S" checked="{MonsterStreak}" onclick="{ToggleMonsterStreak}" tooltip="Allow streak spells" />
|
||||
<toggle x="472" y="132" w="48" h="18" text="W" checked="{MonsterWeakening}" onclick="{ToggleMonsterWeakening}" tooltip="Weakening Curse" />
|
||||
<toggle x="524" y="132" w="48" h="18" text="FC" checked="{MonsterFestering}" onclick="{ToggleMonsterFestering}" tooltip="Festering Curse" />
|
||||
<toggle x="576" y="132" w="48" h="18" text="Cp" checked="{MonsterCorruption}" onclick="{ToggleMonsterCorruption}" tooltip="Corruption" />
|
||||
<toggle x="628" y="132" w="48" h="18" text="DC" checked="{MonsterDestructive}" onclick="{ToggleMonsterDestructive}" tooltip="Destructive Curse" />
|
||||
<toggle x="680" y="132" w="48" h="18" text="Cs" checked="{MonsterCorrosion}" onclick="{ToggleMonsterCorrosion}" tooltip="Corrosion" />
|
||||
|
||||
<label x="4" y="160" text="{MonsterEquipmentText}" color="#FFC7B98F" />
|
||||
<button x="390" y="155" w="116" h="22" text="Weapon ← selected" onclick="{SetMonsterWeapon}" />
|
||||
<button x="512" y="155" w="116" h="22" text="Offhand ← selected" onclick="{SetMonsterOffhand}" />
|
||||
<button x="634" y="155" w="94" h="22" text="Clear equip" onclick="{ClearMonsterEquipment}" />
|
||||
<field x="8" y="136" w="388" h="16" text="{MonsterExpressionDraft}"
|
||||
onchange="{SetMonsterExpressionDraft}" maxlength="256"
|
||||
background="#E6000000" color="#FFE8DEC3"
|
||||
tooltip="Monster name or expression to match." />
|
||||
<button x="404" y="136" w="50" h="16" text="Add" onclick="{AddMonsterRule}"
|
||||
tooltip="Add a new monster rule using the name/expression above." />
|
||||
<button x="462" y="136" w="50" h="16" text="Add Sel" onclick="{AddSelectedMonster}"
|
||||
tooltip="Add the monster currently selected in the world." />
|
||||
</group>
|
||||
|
||||
<!-- Items: exact-name profile membership drives weapons, wands and pets. -->
|
||||
|
|
|
|||
|
|
@ -181,7 +181,13 @@ public sealed class MossTankMarkupContractTests
|
|||
// Editor popup lost LootEditorNotice (a status label, not an
|
||||
// action — dropped to fit VTank's 268x300 footprint without
|
||||
// clipping, see the group's own comment).
|
||||
Assert.Equal(205, controls.Length);
|
||||
// S7.3 replaced the whole Monsters tab body with VTank's own
|
||||
// 23-column grid (docs/research/vtank-kb/08-ui-views.md §1 "Tab:
|
||||
// Monsters"): the select-then-edit surface (1 list + 1 field + 8
|
||||
// buttons + 3 menus + 14 toggles + 3 equip buttons = 30 controls)
|
||||
// is gone, replaced by 1 list + 23 columns + 1 field + 2 buttons
|
||||
// (27) — net 205 -> 202.
|
||||
Assert.Equal(202, controls.Length);
|
||||
|
||||
foreach (XElement control in controls)
|
||||
{
|
||||
|
|
@ -209,6 +215,56 @@ public sealed class MossTankMarkupContractTests
|
|||
Assert.True(panel.MetaTabEnabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MonstersGridHasVtanksTwentyThreeColumnsInOrderWithRetailHeaderTooltips()
|
||||
{
|
||||
// Campaign VT S7.3: pins the transcription against
|
||||
// docs/research/vtank-kb/08-ui-views.md §1 "Tab: Monsters" (column
|
||||
// type/order) and the decompile's own tooltip strings
|
||||
// (refs/vtank/decompiled/uTank2/PluginCore.cs:1693-1707).
|
||||
XDocument document = XDocument.Load(
|
||||
Path.Combine(AppContext.BaseDirectory, "mosstank.xml"));
|
||||
XElement root = Assert.IsType<XElement>(document.Root);
|
||||
XElement monstersGroup = root.Elements("group")
|
||||
.Single(static g => (string?)g.Attribute("visible") == "{MonstersVisible}");
|
||||
|
||||
XElement list = Assert.Single(monstersGroup.Elements("list"));
|
||||
XElement[] columns = list.Elements("column").ToArray();
|
||||
Assert.Equal(23, columns.Length);
|
||||
|
||||
string[] expectedTypes =
|
||||
[
|
||||
"check", "check", "check", "check", "check", "check", "check",
|
||||
"check", "check", "check", "check", "check", "check", "check",
|
||||
"text", "text", "text", "text", "text", "text", "text",
|
||||
"icon", "icon",
|
||||
];
|
||||
Assert.Equal(expectedTypes, columns.Select(c => (string?)c.Attribute("type")));
|
||||
|
||||
// The 14 check columns' header letters carry VTank's own decompiled
|
||||
// tooltip text on the <label> placed above them (columns themselves
|
||||
// have no tooltip grammar — docs/plugin-ui-markup.md's "Columns").
|
||||
Dictionary<string, string?> tooltipsByHeaderText = monstersGroup.Elements("label")
|
||||
.ToDictionary(
|
||||
static l => (string?)l.Attribute("text") ?? string.Empty,
|
||||
static l => (string?)l.Attribute("tooltip"));
|
||||
Assert.Equal("Fester", tooltipsByHeaderText["F"]);
|
||||
Assert.Equal("Broadside of a Barn", tooltipsByHeaderText["B"]);
|
||||
Assert.Equal("Gravity Well", tooltipsByHeaderText["G"]);
|
||||
Assert.Equal("Imperil", tooltipsByHeaderText["I"]);
|
||||
Assert.Equal("Yield", tooltipsByHeaderText["Y"]);
|
||||
Assert.Equal("Vuln (Element)", tooltipsByHeaderText["V"]);
|
||||
Assert.Equal("Attack", tooltipsByHeaderText["A"]);
|
||||
Assert.Equal("Ring Spell", tooltipsByHeaderText["R"]);
|
||||
Assert.Equal("Streak", tooltipsByHeaderText["S"]);
|
||||
Assert.Equal("Weakening Curse", tooltipsByHeaderText["WC"]);
|
||||
Assert.Equal("Festering Curse", tooltipsByHeaderText["FC"]);
|
||||
Assert.Equal("Corruption", tooltipsByHeaderText["Cp"]);
|
||||
Assert.Equal("Destructive Curse", tooltipsByHeaderText["DC"]);
|
||||
Assert.Equal("Corrosion", tooltipsByHeaderText["Cs"]);
|
||||
Assert.Equal("Priority", tooltipsByHeaderText["P"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AuthoredShellFitsTheMinimumCanvasAndEverySizedChildFitsItsParent()
|
||||
{
|
||||
|
|
@ -245,6 +301,16 @@ public sealed class MossTankMarkupContractTests
|
|||
element.Name.LocalName,
|
||||
StringComparer.Ordinal)))
|
||||
{
|
||||
// Campaign VT S7.3: <column> has no text/tooltip grammar at all
|
||||
// (docs/plugin-ui-markup.md's "Columns" section — a column's
|
||||
// attribute set is items/values/onclick/onchange/iconkind, never
|
||||
// text or tooltip). Every column's "caption" is the header
|
||||
// <label> placed above the <list> instead, which — being a
|
||||
// plain label, not one of InteractiveElementNames — this loop
|
||||
// never visits; the Monsters grid's own header labels carry
|
||||
// real tooltip text for the abbreviated ones (see mosstank.xml).
|
||||
if (element.Name.LocalName == "column")
|
||||
continue;
|
||||
string? text = (string?)element.Attribute("text");
|
||||
if (!string.IsNullOrWhiteSpace(text) && !terse.Contains(text))
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -639,7 +639,10 @@ public sealed class MossTankPanelTests
|
|||
host.Selection.Select(10);
|
||||
panel.AddSelectedItem(); // Items profile: the wand
|
||||
host.Selection.Select(20);
|
||||
panel.SetMonsterWeapon(); // DEFAULT rule's weapon: the axe
|
||||
panel.AddSelectedItem(); // Items profile: the axe
|
||||
// Ordinal-sorted roster is ["Battle Axe", "War Wand"], so one cycle
|
||||
// from AUTO lands on the axe — DEFAULT rule's weapon: the axe.
|
||||
panel.CycleMonsterWeaponAt(0);
|
||||
// (DEFAULT's Attack flag is already on by construction — VTank's own
|
||||
// MonsterRuleActions default — so it is never toggled here.)
|
||||
panel.ToggleIdlePeaceMode();
|
||||
|
|
@ -1669,71 +1672,193 @@ public sealed class MossTankPanelTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void MonstersEditorMutatesAndPersistsExecutableOrderedRules()
|
||||
public void MonstersGridMutationsPersistAcrossSessions()
|
||||
{
|
||||
// Campaign VT S7.3: the select-then-edit surface (SelectMonsterRule,
|
||||
// ApplyMonsterRule, MonsterPriorityUp, SelectMonsterDamage, ...) is
|
||||
// retired in favor of VTank's own per-cell grid (docs/research/
|
||||
// vtank-kb/08-ui-views.md §1 "Tab: Monsters"). This test proves the
|
||||
// replacement round-trips through the same CombatSettings.Rules
|
||||
// persistence the old editor used.
|
||||
var storage = new MemoryStorage();
|
||||
var first = new MossTankPanel(new FakeHost(
|
||||
new FakeAutomation { Name = "Rule Maker" },
|
||||
storage));
|
||||
|
||||
first.AddMonsterRule();
|
||||
first.SetMonsterExpressionDraft("species==drudge");
|
||||
first.ApplyMonsterRule();
|
||||
first.ToggleMonsterImperil();
|
||||
first.SelectMonsterDamage(nameof(MonsterDamageType.Fire));
|
||||
first.MonsterPriorityUp();
|
||||
first.AddMonsterRule(); // row 1: "species==drudge"
|
||||
first.ToggleMonsterImperilAt(1);
|
||||
first.CycleMonsterDamageAt(1); // Auto -> Void Basic (MonsterDamageCycle order)
|
||||
first.CycleMonsterPriorityAt(1); // 0 -> 1
|
||||
|
||||
Assert.Equal(1, first.SelectedMonsterRuleIndex);
|
||||
Assert.Equal(nameof(MonsterDamageType.Fire), first.SelectedDamageType);
|
||||
Assert.True(first.MonsterImperil);
|
||||
Assert.Contains("species==drudge", first.MonsterRows[1], StringComparison.Ordinal);
|
||||
Assert.Equal(["DEFAULT", "species==drudge"], first.MonsterNameColumn);
|
||||
Assert.True(first.MonsterImperilColumn[1]);
|
||||
Assert.Equal("Void Basic", first.MonsterDamageColumn[1]);
|
||||
Assert.Equal("1", first.MonsterPriorityColumn[1]);
|
||||
// Add clears the draft so a stray re-click doesn't add it twice.
|
||||
Assert.Equal(string.Empty, first.MonsterExpressionDraft);
|
||||
|
||||
var second = new MossTankPanel(new FakeHost(
|
||||
new FakeAutomation { Name = "Rule Maker" },
|
||||
storage));
|
||||
second.SelectMonsterRule(1);
|
||||
|
||||
Assert.Equal(nameof(MonsterDamageType.Fire), second.SelectedDamageType);
|
||||
Assert.True(second.MonsterImperil);
|
||||
Assert.Equal("Priority 1", second.MonsterPriorityText);
|
||||
Assert.Equal(["DEFAULT", "species==drudge"], second.MonsterNameColumn);
|
||||
Assert.True(second.MonsterImperilColumn[1]);
|
||||
Assert.Equal("Void Basic", second.MonsterDamageColumn[1]);
|
||||
Assert.Equal("1", second.MonsterPriorityColumn[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MonsterColumnsExposeRetailSpecificDamageCyclesAndLabels()
|
||||
public void ToggleMonsterFlagAtWritesOnlyTheTargetedRow()
|
||||
{
|
||||
var panel = new MossTankPanel(new FakeHost(new FakeAutomation()));
|
||||
panel.AddMonsterRule(); // row 1: "New monster" (empty draft default)
|
||||
|
||||
Assert.Equal(
|
||||
Assert.False(panel.MonsterImperilColumn[0]);
|
||||
Assert.False(panel.MonsterImperilColumn[1]);
|
||||
|
||||
panel.ToggleMonsterImperilAt(1);
|
||||
|
||||
Assert.False(panel.MonsterImperilColumn[0]);
|
||||
Assert.True(panel.MonsterImperilColumn[1]);
|
||||
|
||||
panel.ToggleMonsterImperilAt(1);
|
||||
Assert.False(panel.MonsterImperilColumn[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CycleMonsterPriorityAtWrapsExactlyNegativeOneThroughFour()
|
||||
{
|
||||
// PluginCore.cs:7956-7967 (case 15): -1 -> 0 -> 1 -> 2 -> 3 -> 4 -> -1.
|
||||
var panel = new MossTankPanel(new FakeHost(new FakeAutomation()));
|
||||
Assert.Equal("0", panel.MonsterPriorityColumn[0]); // DEFAULT starts at 0
|
||||
|
||||
foreach (string next in new[] { "1", "2", "3", "4", "-1", "0" })
|
||||
{
|
||||
panel.CycleMonsterPriorityAt(0);
|
||||
Assert.Equal(next, panel.MonsterPriorityColumn[0]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MonsterDamageColumnsCycleInTheExactRetailOrder()
|
||||
{
|
||||
// PluginCore.cs:7968-8089 (cases 16/17/20) via eDamageElement's own
|
||||
// wraparound (refs/vtank/decompiled/uTank2/eDamageElement.cs) — three
|
||||
// DIFFERENT sequences, each asserted in full before it wraps.
|
||||
var panel = new MossTankPanel(new FakeHost(new FakeAutomation()));
|
||||
|
||||
AssertCyclesExactly(
|
||||
() => panel.MonsterDamageColumn[0],
|
||||
() => panel.CycleMonsterDamageAt(0),
|
||||
[
|
||||
"Pierce", "Bludgeon", "Slash", "Acid", "Lightning", "Cold",
|
||||
"Fire", "Harm", "Auto", "Void Basic", "Drain Auto",
|
||||
"Prismatic", "Random", "Fists",
|
||||
],
|
||||
panel.DamageTypeNames);
|
||||
Assert.Equal(
|
||||
[
|
||||
"Pierce", "Bludgeon", "Slash", "Acid", "Lightning", "Cold",
|
||||
"Fire", "Auto", "None",
|
||||
],
|
||||
panel.ExtraVulnerabilityNames);
|
||||
Assert.Equal(
|
||||
]);
|
||||
AssertCyclesExactly(
|
||||
() => panel.MonsterExtraVulnColumn[0],
|
||||
() => panel.CycleMonsterExtraVulnerabilityAt(0),
|
||||
["Pierce", "Bludgeon", "Slash", "Acid", "Lightning", "Cold", "Fire", "Auto", "None"]);
|
||||
AssertCyclesExactly(
|
||||
() => panel.MonsterPetDamageColumn[0],
|
||||
() => panel.CycleMonsterPetDamageAt(0),
|
||||
[
|
||||
"Pierce", "Bludgeon", "Slash", "Acid", "Lightning", "Cold",
|
||||
"Fire", "PAuto", "Auto", "None",
|
||||
],
|
||||
panel.PetDamageTypeNames);
|
||||
]);
|
||||
|
||||
panel.SelectMonsterDamage("Void Basic");
|
||||
panel.SelectMonsterExtraVulnerability("Lightning");
|
||||
panel.SelectMonsterPetDamage("PAuto");
|
||||
|
||||
Assert.Equal("Void Basic", panel.SelectedDamageType);
|
||||
Assert.Equal("Lightning", panel.SelectedExtraVulnerability);
|
||||
Assert.Equal("PAuto", panel.SelectedPetDamage);
|
||||
static void AssertCyclesExactly(
|
||||
Func<string> readCurrent, Action cycle, string[] expected)
|
||||
{
|
||||
// Cycle until row 0 reads the array's own first entry, so every
|
||||
// subsequent step lines up with `expected` regardless of the
|
||||
// rule's starting value (DamageType/PetDamageType default to
|
||||
// different sentinels — Auto vs PAuto — that are not index 0).
|
||||
// `readCurrent` re-reads the live column on every call — the
|
||||
// column property itself returns a fresh snapshot array on each
|
||||
// get, so capturing one snapshot up front would freeze `[0]`
|
||||
// forever and spin this loop.
|
||||
int guard = 0;
|
||||
while (readCurrent() != expected[0])
|
||||
{
|
||||
cycle();
|
||||
Assert.True(++guard <= expected.Length, "cycle never reached the array's first entry");
|
||||
}
|
||||
foreach (string next in expected.Skip(1).Append(expected[0]))
|
||||
{
|
||||
cycle();
|
||||
Assert.Equal(next, readCurrent());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MonsterEquipmentPersistsByNameAcrossSessionObjectIds()
|
||||
public void DeleteMonsterRuleAtRemovesNonDefaultRowsButNeverDefault()
|
||||
{
|
||||
// PluginCore.cs:7948-7955 (case 14) + the row-delete helper at
|
||||
// PluginCore.cs:8168-8179.
|
||||
var panel = new MossTankPanel(new FakeHost(new FakeAutomation()));
|
||||
panel.SetMonsterExpressionDraft("drudge");
|
||||
panel.AddMonsterRule();
|
||||
panel.SetMonsterExpressionDraft("mosswart");
|
||||
panel.AddMonsterRule();
|
||||
Assert.Equal(["DEFAULT", "drudge", "mosswart"], panel.MonsterNameColumn);
|
||||
|
||||
panel.DeleteMonsterRuleAt(0); // DEFAULT: refused
|
||||
Assert.Equal(["DEFAULT", "drudge", "mosswart"], panel.MonsterNameColumn);
|
||||
|
||||
panel.DeleteMonsterRuleAt(1); // drudge
|
||||
Assert.Equal(["DEFAULT", "mosswart"], panel.MonsterNameColumn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MoveMonsterRuleAtReordersButNeverDisplacesDefault()
|
||||
{
|
||||
// PluginCore.cs:8090-8105 (cases 21/22).
|
||||
var panel = new MossTankPanel(new FakeHost(new FakeAutomation()));
|
||||
panel.SetMonsterExpressionDraft("drudge");
|
||||
panel.AddMonsterRule();
|
||||
panel.SetMonsterExpressionDraft("mosswart");
|
||||
panel.AddMonsterRule();
|
||||
Assert.Equal(["DEFAULT", "drudge", "mosswart"], panel.MonsterNameColumn);
|
||||
|
||||
panel.MoveMonsterRuleDownAt(1); // drudge <-> mosswart
|
||||
Assert.Equal(["DEFAULT", "mosswart", "drudge"], panel.MonsterNameColumn);
|
||||
|
||||
panel.MoveMonsterRuleUpAt(2); // back (row 2 "drudge" up to row 1)
|
||||
Assert.Equal(["DEFAULT", "drudge", "mosswart"], panel.MonsterNameColumn);
|
||||
|
||||
panel.MoveMonsterRuleUpAt(1); // would displace DEFAULT from row 0: refused
|
||||
Assert.Equal(["DEFAULT", "drudge", "mosswart"], panel.MonsterNameColumn);
|
||||
|
||||
Assert.Equal(3, panel.MonsterMoveUpIcons.Count);
|
||||
Assert.All(panel.MonsterMoveUpIcons, id => Assert.Equal(0x060028FCu, id));
|
||||
Assert.All(panel.MonsterMoveDownIcons, id => Assert.Equal(0x060028FDu, id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddMonsterRuleUsesTheDraftTextAndAddSelectedMonsterUsesTheWorldTarget()
|
||||
{
|
||||
var automation = new CombatCapableFakeAutomation
|
||||
{
|
||||
Targets = [new PluginCombatTarget(30, "Drudge", 700, 2f, 0f, true, 1f)],
|
||||
};
|
||||
var host = new FakeHost(automation);
|
||||
var panel = new MossTankPanel(host);
|
||||
|
||||
panel.SetMonsterExpressionDraft("species==drudge");
|
||||
panel.AddMonsterRule();
|
||||
Assert.Equal(["DEFAULT", "species==drudge"], panel.MonsterNameColumn);
|
||||
Assert.Equal(string.Empty, panel.MonsterExpressionDraft);
|
||||
|
||||
host.Selection.Select(30);
|
||||
panel.AddSelectedMonster();
|
||||
Assert.Equal(["DEFAULT", "species==drudge", "Drudge"], panel.MonsterNameColumn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MonsterWeaponColumnCyclesTheRegisteredRosterAndPersistsByNameAcrossSessions()
|
||||
{
|
||||
var storage = new MemoryStorage();
|
||||
var firstAutomation = new FakeAutomation
|
||||
|
|
@ -1744,7 +1869,15 @@ public sealed class MossTankPanelTests
|
|||
var firstHost = new FakeHost(firstAutomation, storage);
|
||||
var first = new MossTankPanel(firstHost);
|
||||
firstHost.Selection.Select(10);
|
||||
first.SetMonsterWeapon();
|
||||
first.AddSelectedItem(); // registers "Fire Sword" into the weapon roster
|
||||
|
||||
Assert.Equal("<AUTO>", first.MonsterWeaponColumn[0]);
|
||||
first.CycleMonsterWeaponAt(0); // AUTO -> Fire Sword (only registered item)
|
||||
Assert.Equal("Fire Sword", first.MonsterWeaponColumn[0]);
|
||||
first.CycleMonsterWeaponAt(0); // Fire Sword -> AUTO (wraps)
|
||||
Assert.Equal("<AUTO>", first.MonsterWeaponColumn[0]);
|
||||
first.CycleMonsterWeaponAt(0); // back to Fire Sword for the persistence check
|
||||
Assert.Equal("Fire Sword", first.MonsterWeaponColumn[0]);
|
||||
|
||||
var second = new MossTankPanel(new FakeHost(
|
||||
new FakeAutomation
|
||||
|
|
@ -1754,7 +1887,7 @@ public sealed class MossTankPanelTests
|
|||
},
|
||||
storage));
|
||||
|
||||
Assert.Contains("Fire Sword", second.MonsterEquipmentText);
|
||||
Assert.Equal("Fire Sword", second.MonsterWeaponColumn[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -2654,7 +2787,7 @@ public sealed class MossTankPanelTests
|
|||
public void PostSystemMessage(string text) => Messages.Add(text);
|
||||
|
||||
// ── inventory (used only to build the Items profile through the
|
||||
// panel's real AddSelectedItem/SetMonsterWeapon UI flow) ────────
|
||||
// panel's real AddSelectedItem/CycleMonsterWeaponAt UI flow) ────
|
||||
bool IItemAutomation.IsAvailable => true;
|
||||
bool IItemAutomation.IsBusy => false;
|
||||
public IReadOnlyList<PluginInventoryItem> ItemEntries { get; set; } = [];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue