diff --git a/src/AcDream.Plugins.MossTank/MossTankCommands.cs b/src/AcDream.Plugins.MossTank/MossTankCommands.cs index 178a2797..9aa8d344 100644 --- a/src/AcDream.Plugins.MossTank/MossTankCommands.cs +++ b/src/AcDream.Plugins.MossTank/MossTankCommands.cs @@ -172,7 +172,7 @@ internal sealed partial class MossTankPanel WriteVtank($"Portal space toggle count: {_commandPortalCount}"); return; case "refresh": - RefreshMonsterEditor(); + EnsureDefaultMonsterRule(); RefreshItemEditors(); RefreshLootEditor(); RefreshRouteEditor(); diff --git a/src/AcDream.Plugins.MossTank/MossTankPanel.cs b/src/AcDream.Plugins.MossTank/MossTankPanel.cs index aa68162d..3e58f0c3 100644 --- a/src/AcDream.Plugins.MossTank/MossTankPanel.cs +++ b/src/AcDream.Plugins.MossTank/MossTankPanel.cs @@ -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 _monsterRows = Array.Empty(); - 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 + // 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 _itemRows = Array.Empty(); private IReadOnlyList _consumableRows = Array.Empty(); @@ -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 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 . 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 DamageTypeNames => MonsterDamageNames; - public IReadOnlyList ExtraVulnerabilityNames => - MonsterExtraVulnerabilityNames; - public IReadOnlyList 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 SelectMonsterRule => SelectMonsterRuleCore; + /// + /// 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. + /// + 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 SelectMonsterListRow => row => _monsterListSelectedRow = row; public Action SetMonsterExpressionDraft => value => _monsterExpressionDraft = value; - public Action 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 SelectMonsterDamage => value => - SetMonsterDamage(value, extra: false); - public Action SelectMonsterExtraVulnerability => value => - SetMonsterDamage(value, extra: true); - public Action 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 MonsterFesterColumn => MonsterFlagColumn(MonsterActionFlags.Fester); + public IReadOnlyList MonsterBroadsideColumn => MonsterFlagColumn(MonsterActionFlags.Broadside); + public IReadOnlyList MonsterGravityWellColumn => MonsterFlagColumn(MonsterActionFlags.GravityWell); + public IReadOnlyList MonsterImperilColumn => MonsterFlagColumn(MonsterActionFlags.Imperil); + public IReadOnlyList MonsterYieldColumn => MonsterFlagColumn(MonsterActionFlags.Yield); + public IReadOnlyList 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 MonsterAttackColumn => MonsterFlagColumn(MonsterActionFlags.Attack); + public IReadOnlyList MonsterRingColumn => MonsterFlagColumn(MonsterActionFlags.Ring); + public IReadOnlyList MonsterStreakColumn => MonsterFlagColumn(MonsterActionFlags.Streak); + public IReadOnlyList MonsterWeakeningColumn => MonsterFlagColumn(MonsterActionFlags.WeakeningCurse); + public IReadOnlyList MonsterFesteringColumn => MonsterFlagColumn(MonsterActionFlags.FesteringCurse); + public IReadOnlyList MonsterCorruptionColumn => MonsterFlagColumn(MonsterActionFlags.Corruption); + public IReadOnlyList MonsterDestructiveColumn => MonsterFlagColumn(MonsterActionFlags.DestructiveCurse); + public IReadOnlyList MonsterCorrosionColumn => MonsterFlagColumn(MonsterActionFlags.Corrosion); + + public Action ToggleMonsterFesterAt => row => ToggleMonsterFlagAt(row, MonsterActionFlags.Fester); + public Action ToggleMonsterBroadsideAt => row => ToggleMonsterFlagAt(row, MonsterActionFlags.Broadside); + public Action ToggleMonsterGravityWellAt => row => ToggleMonsterFlagAt(row, MonsterActionFlags.GravityWell); + public Action ToggleMonsterImperilAt => row => ToggleMonsterFlagAt(row, MonsterActionFlags.Imperil); + public Action ToggleMonsterYieldAt => row => ToggleMonsterFlagAt(row, MonsterActionFlags.Yield); + public Action ToggleMonsterVulnerabilityAt => row => ToggleMonsterFlagAt(row, MonsterActionFlags.Vulnerability); + public Action ToggleMonsterAttackAt => row => ToggleMonsterFlagAt(row, MonsterActionFlags.Attack); + public Action ToggleMonsterRingAt => row => ToggleMonsterFlagAt(row, MonsterActionFlags.Ring); + public Action ToggleMonsterStreakAt => row => ToggleMonsterFlagAt(row, MonsterActionFlags.Streak); + public Action ToggleMonsterWeakeningAt => row => ToggleMonsterFlagAt(row, MonsterActionFlags.WeakeningCurse); + public Action ToggleMonsterFesteringAt => row => ToggleMonsterFlagAt(row, MonsterActionFlags.FesteringCurse); + public Action ToggleMonsterCorruptionAt => row => ToggleMonsterFlagAt(row, MonsterActionFlags.Corruption); + public Action ToggleMonsterDestructiveAt => row => ToggleMonsterFlagAt(row, MonsterActionFlags.DestructiveCurse); + public Action ToggleMonsterCorrosionAt => row => ToggleMonsterFlagAt(row, MonsterActionFlags.Corrosion); + + // ── the 7 text columns (decompile cases 14-20) ────────────────────────── + public IReadOnlyList MonsterNameColumn => + _combatSettings.Rules.Select(static r => r.Expression).ToArray(); + public IReadOnlyList MonsterPriorityColumn => + _combatSettings.Rules.Select( + static r => r.Actions.BoundedPriority.ToString(CultureInfo.InvariantCulture)) + .ToArray(); + public IReadOnlyList MonsterDamageColumn => + _combatSettings.Rules.Select(static r => DamageTypeDisplay(r.Actions.DamageType)).ToArray(); + public IReadOnlyList MonsterExtraVulnColumn => + _combatSettings.Rules.Select(static r => DamageTypeDisplay(r.Actions.ExtraVulnerability)).ToArray(); + public IReadOnlyList MonsterWeaponColumn => + _combatSettings.Rules.Select( + r => ItemDisplayName(r.Actions.WeaponObjectId, r.Actions.WeaponName)) + .ToArray(); + public IReadOnlyList MonsterOffhandColumn => + _combatSettings.Rules.Select( + r => ItemDisplayName(r.Actions.OffhandObjectId, r.Actions.OffhandName)) + .ToArray(); + public IReadOnlyList 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 DeleteMonsterRuleAt => DeleteMonsterRuleAtCore; + // Case 15: priority cycles -1 -> 0 -> 1 -> 2 -> 3 -> 4 -> -1 + // (PluginCore.cs:7956-7967). + public Action CycleMonsterPriorityAt => CycleMonsterPriorityAtCore; + // Case 16: Dmg type cycles every eDamageElement 0..13 + // (PluginCore.cs:7968-7979). + public Action 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 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 ") without inventing a type abstraction this slice's + // scope doesn't call for. + public Action CycleMonsterWeaponAt => row => CycleMonsterEquipmentAt(row, offhand: false); + public Action CycleMonsterOffhandAt => row => CycleMonsterEquipmentAt(row, offhand: true); + // Case 20: PetDmg's non-contiguous cycle (PluginCore.cs:8075-8089). + public Action 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 MonsterMoveUpIcons => + Enumerable.Repeat(0x060028FCu, _combatSettings.Rules.Count).ToArray(); + public IReadOnlyList MonsterMoveDownIcons => + Enumerable.Repeat(0x060028FDu, _combatSettings.Rules.Count).ToArray(); + public Action MoveMonsterRuleUpAt => row => MoveMonsterRuleAtCore(row, -1); + public Action MoveMonsterRuleDownAt => row => MoveMonsterRuleAtCore(row, 1); /// Vitals line, using the same numbers the character panel shows. 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) + /// VTank: row 0 is always DEFAULT and can never be empty. + 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 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]; + } + + /// + /// 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. + /// + 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 update) + /// + /// 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. + /// + 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 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(); diff --git a/src/AcDream.Plugins.MossTank/mosstank.xml b/src/AcDream.Plugins.MossTank/mosstank.xml index bf43c153..8eb01d34 100644 --- a/src/AcDream.Plugins.MossTank/mosstank.xml +++ b/src/AcDream.Plugins.MossTank/mosstank.xml @@ -308,66 +308,77 @@