4361 lines
177 KiB
C#
4361 lines
177 KiB
C#
using System.Globalization;
|
||
using System.Text;
|
||
using AcDream.Plugin.Abstractions;
|
||
using AcDream.Plugins.MossTank.Expressions;
|
||
|
||
namespace AcDream.Plugins.MossTank;
|
||
|
||
/// <summary>
|
||
/// The panel's binding object and the buff loop's state machine.
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// Everything here runs on the host's update thread — the button clicks and the
|
||
/// tick all arrive there — so no locking is used, deliberately: a lock would
|
||
/// imply a second thread that does not exist.
|
||
/// </remarks>
|
||
internal sealed partial class MossTankPanel
|
||
{
|
||
private enum TankTab
|
||
{
|
||
Options,
|
||
Profiles,
|
||
Vitals,
|
||
Monsters,
|
||
Items,
|
||
Consumables,
|
||
Buffs,
|
||
Route,
|
||
Meta,
|
||
}
|
||
|
||
/// <summary>
|
||
/// Give up on a pass that stops making progress. Generous, because the
|
||
/// pacing is now the server acknowledging each cast rather than a fixed
|
||
/// delay, and a slow server should not read as a stall.
|
||
/// </summary>
|
||
private const double StallTimeoutSeconds = 30.0;
|
||
|
||
/// <summary>
|
||
/// The retained markup host reads label bindings while drawing. Coverage
|
||
/// walks the complete known-buff table, so it belongs on the update side
|
||
/// and only needs to refresh at human-readable cadence.
|
||
/// </summary>
|
||
private const double CoverageRefreshIntervalSeconds = 1.0;
|
||
|
||
private readonly IPluginHost _host;
|
||
private readonly BuffSettings _buffSettings = new();
|
||
private readonly VitalSettings _vitalSettings = new();
|
||
private readonly CombatSettings _combatSettings = new();
|
||
private readonly InventorySettings _inventorySettings = new();
|
||
private readonly NavigationSettings _navigationSettings = new();
|
||
private readonly MossTankProfileStore _profiles;
|
||
private readonly MossTankLootProfileStore _lootProfiles;
|
||
private readonly MossTankRouteProfileStore _routeProfiles;
|
||
private readonly MossTankMetaProfileStore _metaProfiles;
|
||
private readonly MetaViewManager _metaViews;
|
||
private readonly CombatController _combat;
|
||
private readonly VitalRechargeController _vitalRecharge;
|
||
private readonly DispelController _dispel;
|
||
private readonly InventoryMaintenanceController _inventoryMaintenance;
|
||
private readonly CraftingController _crafting;
|
||
private readonly ItemManaRechargeController _itemManaRecharge;
|
||
private readonly LootController _loot;
|
||
private readonly ProfileGiveController _profileGive;
|
||
private readonly NavigationController _navigation;
|
||
private readonly FellowshipManager _fellowshipManager;
|
||
private readonly MossTankExpressionRuntime _expressions;
|
||
private MetaProfile _metaProfile;
|
||
private readonly MetaEngine _meta;
|
||
|
||
// A pass works through a queue captured at the start rather than a plan
|
||
// re-derived each tick. Force Buff deliberately ignores what is already in
|
||
// force, so a re-derived plan would never shrink and the pass could never
|
||
// reach "done" -- it would cast forever.
|
||
private List<PluginSpellInfo> _queue = new();
|
||
private int _queueIndex;
|
||
private bool _running;
|
||
private bool _forcePass;
|
||
private bool _announceBuffPass;
|
||
private double _automaticBuffScanRemaining;
|
||
private double _buffCastRecastRemaining;
|
||
private bool _fastCastMovementActive;
|
||
private long _fastCastStartCompletionRevision;
|
||
private double _fastCastMovementElapsed;
|
||
private double _randomHelperRemaining;
|
||
private int _randomHelperCursor;
|
||
private double _sinceProgress;
|
||
private int _castThisPass;
|
||
private string _status = "Idle.";
|
||
private string _vitals = string.Empty;
|
||
private string _coverage = string.Empty;
|
||
private bool _vitalsInitialized;
|
||
private uint _currentHealth;
|
||
private uint _maxHealth;
|
||
private uint _currentStamina;
|
||
private uint _maxStamina;
|
||
private uint _currentMana;
|
||
private uint _maxMana;
|
||
private IReadOnlyList<PluginSpellInfo>? _coverageSpellSnapshot;
|
||
private int _coverageBuffLineCount;
|
||
private double _coverageRefreshRemaining;
|
||
private TankTab _activeTab = TankTab.Options;
|
||
private readonly HashSet<string> _noBuffItemNames =
|
||
new(StringComparer.Ordinal);
|
||
private string _profileNotice =
|
||
"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 =
|
||
[
|
||
"Pierce", "Bludgeon", "Slash", "Acid", "Lightning", "Cold",
|
||
"Fire", "Harm", "Auto", "Void Basic", "Drain Auto", "Prismatic",
|
||
"Random", "Fists",
|
||
];
|
||
private static readonly string[] MonsterExtraVulnerabilityNames =
|
||
[
|
||
"Pierce", "Bludgeon", "Slash", "Acid", "Lightning", "Cold",
|
||
"Fire", "Auto", "None",
|
||
];
|
||
private static readonly string[] MonsterPetDamageNames =
|
||
[
|
||
"Pierce", "Bludgeon", "Slash", "Acid", "Lightning", "Cold",
|
||
"Fire", "PAuto", "Auto", "None",
|
||
];
|
||
private IReadOnlyList<string> _itemRows = Array.Empty<string>();
|
||
private IReadOnlyList<string> _consumableRows = Array.Empty<string>();
|
||
private int _selectedItemRow;
|
||
private int _selectedConsumableRow;
|
||
private bool _lootEditorVisible;
|
||
private bool _advancedOptionsVisible;
|
||
private int _selectedAdvancedOption;
|
||
private string _advancedOptionValueDraft = string.Empty;
|
||
private string _advancedOptionNotice =
|
||
"All VTank settings are available here.";
|
||
private IReadOnlyList<string> _lootRuleRows = Array.Empty<string>();
|
||
private int _selectedLootRule;
|
||
private string _lootExpressionDraft = "*";
|
||
private string _lootEditorNotice = "Add a rule or select one to edit.";
|
||
private string _lootProfileNameDraft = string.Empty;
|
||
private IReadOnlyList<string> _routeRows = Array.Empty<string>();
|
||
private int _selectedRouteWaypoint;
|
||
private string _routeProfileNameDraft = string.Empty;
|
||
private string _routeNotice = "Add the current position or a selected object.";
|
||
private string _routeChatDraft = "/ls";
|
||
private int _routePauseSeconds = 5;
|
||
private RouteRecallKind _routeRecallKind = RouteRecallKind.PrimaryPortal;
|
||
private bool _routeAddToEnd = true;
|
||
private IReadOnlyList<string> _metaRows = Array.Empty<string>();
|
||
private int _selectedMetaRule;
|
||
private string _metaProfileNameDraft = string.Empty;
|
||
private string _metaStateDraft = MetaEngine.DefaultState;
|
||
private string _metaConditionTextDraft = string.Empty;
|
||
private string _metaActionTextDraft = string.Empty;
|
||
private string _metaSecondaryTextDraft = string.Empty;
|
||
private MetaConditionKind _metaConditionKind = MetaConditionKind.Always;
|
||
private MetaActionKind _metaActionKind = MetaActionKind.None;
|
||
private int _metaNumber;
|
||
private int _metaSecondaryNumber;
|
||
private string _metaNotice = "Add a rule or select one to edit.";
|
||
private bool _applyingProfileOptions;
|
||
private bool _initialized;
|
||
private bool _firstRunGuidancePending;
|
||
private bool _automationWasAvailable;
|
||
|
||
/// <summary>
|
||
/// What the player had selected before the pass, so targeting yourself for
|
||
/// banes does not quietly steal the selection and leave it changed.
|
||
/// </summary>
|
||
private uint? _selectionBeforePass;
|
||
|
||
public MossTankPanel(IPluginHost host)
|
||
{
|
||
_host = host;
|
||
_firstRunGuidancePending = NeedsFirstRunGuidance(host);
|
||
_profiles = new MossTankProfileStore(host);
|
||
_profiles.BindCharacter(host.Automation.Character.Name);
|
||
_profiles.LoadCurrent(
|
||
_combatSettings,
|
||
_buffSettings,
|
||
_vitalSettings,
|
||
_inventorySettings,
|
||
_noBuffItemNames);
|
||
_lootProfiles = new MossTankLootProfileStore(host);
|
||
_lootProfiles.BindCharacter(host.Automation.Character.Name);
|
||
if (!_lootProfiles.LoadCurrent(
|
||
_inventorySettings.Loot.Rules,
|
||
_inventorySettings.Loot))
|
||
{
|
||
_lootProfiles.SaveCurrent(
|
||
_inventorySettings.Loot.Rules,
|
||
_inventorySettings.Loot);
|
||
}
|
||
_routeProfiles = new MossTankRouteProfileStore(host);
|
||
_routeProfiles.BindCharacter(host.Automation.Character.Name);
|
||
if (!_routeProfiles.LoadCurrent(_navigationSettings))
|
||
_routeProfiles.SaveCurrent(_navigationSettings);
|
||
_combat = new CombatController(host, _combatSettings, _vitalSettings);
|
||
_vitalRecharge = new VitalRechargeController(
|
||
host,
|
||
_vitalSettings,
|
||
_combatSettings);
|
||
_dispel = new DispelController(host, _vitalSettings);
|
||
_inventoryMaintenance = new InventoryMaintenanceController(
|
||
host,
|
||
_inventorySettings);
|
||
_crafting = new CraftingController(
|
||
host,
|
||
_inventorySettings,
|
||
_combatSettings);
|
||
_combat.BindAmmunitionCraftRequest(
|
||
_crafting.CanRequest,
|
||
_crafting.Request);
|
||
_itemManaRecharge = new ItemManaRechargeController(
|
||
host,
|
||
_inventorySettings,
|
||
_combatSettings);
|
||
_loot = new LootController(
|
||
host,
|
||
_inventorySettings.Loot);
|
||
_profileGive = new ProfileGiveController(host, _lootProfiles);
|
||
_navigation = new NavigationController(host, _navigationSettings);
|
||
_fellowshipManager = new FellowshipManager(host);
|
||
_metaProfiles = new MossTankMetaProfileStore(host);
|
||
_metaViews = new MetaViewManager(host);
|
||
_metaProfiles.BindCharacter(host.Automation.Character.Name);
|
||
_metaProfile = _metaProfiles.LoadCurrent();
|
||
_expressions = new MossTankExpressionRuntime(host);
|
||
_meta = new MetaEngine(
|
||
host,
|
||
_expressions,
|
||
_metaProfile,
|
||
new MetaServices
|
||
{
|
||
IsNavigationRouteEmpty = () => _navigationSettings.Waypoints.Count == 0,
|
||
NeedsBuff = () => BuildPlan(_host.Automation, force: false).Count != 0,
|
||
DistanceFromAnyRoutePoint = DistanceFromAnyRoutePoint,
|
||
CountMonstersByPriority = CountMonstersByPriority,
|
||
LoadEmbeddedNavigationRoute = LoadEmbeddedNavigationRoute,
|
||
GetOption = GetMetaOption,
|
||
SetOption = SetMetaOption,
|
||
CreateView = _metaViews.Create,
|
||
DestroyView = _metaViews.Destroy,
|
||
DestroyAllViews = _metaViews.DestroyAll,
|
||
});
|
||
RegisterVtankExpressionFunctions();
|
||
_initialized = true;
|
||
ApplyPersistedOptionOverrides();
|
||
RefreshMonsterEditor();
|
||
RefreshItemEditors();
|
||
RefreshLootEditor();
|
||
RefreshRouteEditor();
|
||
RefreshMetaEditor();
|
||
_automationWasAvailable = host.Automation.IsAvailable;
|
||
}
|
||
|
||
// ── main panel bindings ───────────────────────────────────────────────
|
||
public Action Buff => StartOrStop;
|
||
public Action ForceBuff => StartForceBuff;
|
||
public Action CancelForceBuff => CancelForceBuffCore;
|
||
public Action ToggleCombat => ToggleMacro;
|
||
public Action ToggleCombatEnabled => () =>
|
||
{
|
||
_combatSettings.Enabled = !_combatSettings.Enabled;
|
||
SaveProfile();
|
||
};
|
||
public Action ToggleAutoFellowManagement => () => SetMetaOption(
|
||
"AutoFellowManagement",
|
||
ExpressionValue.Boolean(!AutoFellowManagementEnabled));
|
||
public Action ShowOptions => () => SelectTab(TankTab.Options);
|
||
public Action ShowProfiles => () => SelectTab(TankTab.Profiles);
|
||
public Action ShowVitals => () => SelectTab(TankTab.Vitals);
|
||
public Action ShowMonsters => () => SelectTab(TankTab.Monsters);
|
||
public Action ShowItems => () => SelectTab(TankTab.Items);
|
||
public Action ShowConsumables => () => SelectTab(TankTab.Consumables);
|
||
public Action ShowBuffs => () => SelectTab(TankTab.Buffs);
|
||
public Action ShowRoute => () => SelectTab(TankTab.Route);
|
||
public Action ShowMeta => () => SelectTab(TankTab.Meta);
|
||
|
||
/// <summary>Keeps the gameplay window off login/character-select screens.</summary>
|
||
public bool WindowAvailable => _host.Automation.IsAvailable;
|
||
|
||
internal ExpressionValue EvaluateExpression(string source) =>
|
||
_expressions.Evaluate(source);
|
||
|
||
internal IReadOnlyCollection<string> ExpressionFunctionNames =>
|
||
_expressions.Functions.Select(static function => function.Name).ToArray();
|
||
|
||
public bool OptionsSelected => _activeTab == TankTab.Options;
|
||
public bool ProfilesSelected => _activeTab == TankTab.Profiles;
|
||
public bool VitalsSelected => _activeTab == TankTab.Vitals;
|
||
public bool MonstersSelected => _activeTab == TankTab.Monsters;
|
||
public bool ItemsSelected => _activeTab == TankTab.Items;
|
||
public bool ConsumablesSelected => _activeTab == TankTab.Consumables;
|
||
public bool BuffsSelected => _activeTab == TankTab.Buffs;
|
||
public bool RouteSelected => _activeTab == TankTab.Route;
|
||
public bool MetaSelected => _activeTab == TankTab.Meta;
|
||
|
||
public bool OptionsTabEnabled => true;
|
||
public bool ProfilesTabEnabled => true;
|
||
public bool VitalsTabEnabled => true;
|
||
public bool MonstersTabEnabled => true;
|
||
public bool ItemsTabEnabled => true;
|
||
public bool ConsumablesTabEnabled => true;
|
||
public bool BuffsTabEnabled => true;
|
||
public bool RouteTabEnabled => true;
|
||
public bool MetaTabEnabled => true;
|
||
|
||
public bool OptionsVisible => OptionsSelected
|
||
&& !_lootEditorVisible
|
||
&& !_advancedOptionsVisible;
|
||
public bool ProfilesVisible => ProfilesSelected && !_lootEditorVisible;
|
||
public bool VitalsVisible => VitalsSelected && !_lootEditorVisible;
|
||
public bool MonstersVisible => MonstersSelected && !_lootEditorVisible;
|
||
public bool ItemsVisible => ItemsSelected && !_lootEditorVisible;
|
||
public bool ConsumablesVisible => ConsumablesSelected && !_lootEditorVisible;
|
||
public bool BuffsVisible => BuffsSelected && !_lootEditorVisible;
|
||
public bool RouteVisible => RouteSelected && !_lootEditorVisible;
|
||
public bool MetaVisible => MetaSelected && !_lootEditorVisible;
|
||
public bool LootEditorVisible => _lootEditorVisible;
|
||
public bool AdvancedOptionsVisible => _advancedOptionsVisible;
|
||
public IReadOnlyList<string> AdvancedOptionNames => VtankOptionCatalog.Names;
|
||
public int SelectedAdvancedOptionIndex => _selectedAdvancedOption;
|
||
public string AdvancedOptionName => VtankOptionCatalog.Names[
|
||
Math.Clamp(
|
||
_selectedAdvancedOption,
|
||
0,
|
||
VtankOptionCatalog.Names.Length - 1)];
|
||
public string AdvancedOptionValueDraft => _advancedOptionValueDraft;
|
||
public string AdvancedOptionNotice => _advancedOptionNotice;
|
||
public Action ShowAdvancedOptions => () =>
|
||
{
|
||
_advancedOptionsVisible = true;
|
||
LoadAdvancedOptionDraft();
|
||
};
|
||
public Action HideAdvancedOptions => () => _advancedOptionsVisible = false;
|
||
public Action<int> SelectAdvancedOption => index =>
|
||
{
|
||
_selectedAdvancedOption = Math.Clamp(
|
||
index,
|
||
0,
|
||
VtankOptionCatalog.Names.Length - 1);
|
||
LoadAdvancedOptionDraft();
|
||
};
|
||
public Action<string> SetAdvancedOptionValueDraft => value =>
|
||
_advancedOptionValueDraft = value;
|
||
public Action<string> SubmitAdvancedOption => value =>
|
||
{
|
||
_advancedOptionValueDraft = value;
|
||
ApplyAdvancedOptionCore();
|
||
};
|
||
public Action ApplyAdvancedOption => ApplyAdvancedOptionCore;
|
||
|
||
/// <summary>The button is Force Buff; while a pass runs it cancels.</summary>
|
||
public string BuffButtonText => _running ? "Stop buffing" : "Force buff";
|
||
public string BuffStatus => _status;
|
||
public string CombatButtonText => _combat.ButtonText;
|
||
public string CombatStatus => _combat.Status;
|
||
public string CombatTarget => _combat.TargetText;
|
||
public string CombatMode => _combat.ModeText;
|
||
public string VitalStatus => _vitalRecharge.Status;
|
||
public string DispelStatus => _dispel.Status;
|
||
public string InventoryMaintenanceStatus => _inventoryMaintenance.Status;
|
||
public string CraftingStatus => _crafting.Status;
|
||
public string ItemManaRechargeStatus => _itemManaRecharge.Status;
|
||
public string LootStatus => _loot.Status;
|
||
public bool CombatEnabled => _combatSettings.Enabled;
|
||
public bool BuffingEnabled => _buffSettings.Enabled;
|
||
public bool IdlePeaceModeEnabled => _combatSettings.IdlePeaceMode;
|
||
public bool IdleBuffTopoffEnabled => _buffSettings.IdleBuffTopoff;
|
||
public bool ManaChargesWhenOffEnabled =>
|
||
_inventorySettings.ManaChargesWhenOff;
|
||
public bool AutoFellowManagementEnabled =>
|
||
_combatSettings.AutoFellowManagement;
|
||
public string FellowshipManagerStatus => _fellowshipManager.Status;
|
||
public bool AutoStackEnabled => _inventorySettings.AutoStack;
|
||
public bool AutoCramEnabled => _inventorySettings.AutoCram;
|
||
public bool AutoCraftItemsEnabled => _inventorySettings.AutoCraftItems;
|
||
public bool CastDispelSelfEnabled => _vitalSettings.CastDispelSelf;
|
||
public bool UseDispelItemsEnabled => _vitalSettings.UseDispelItems;
|
||
public bool RefillWornManaEnabled => _inventorySettings.RefillWornMana;
|
||
public float RefillWornManaValue => _inventorySettings.RefillWornManaPercent / 100f;
|
||
public string RefillWornManaText =>
|
||
$"Refill worn mana below {_inventorySettings.RefillWornManaPercent}%";
|
||
public string ItemProfileText => ProfileText(
|
||
"Weapons / Wands / Shields / Pets",
|
||
_combatSettings.CombatItemNames);
|
||
public string ConsumableProfileText => ProfileText(
|
||
"Gems / Food / Kits / Potions / Charges / Grenades / Lockpicks",
|
||
_combatSettings.ConsumableNames);
|
||
public string ProfileNotice => _profileNotice;
|
||
public Action AddSelectedItem => () => AddSelectedProfileItem(noBuffs: false);
|
||
public Action AddSelectedItemNoBuffs => () => AddSelectedProfileItem(noBuffs: true);
|
||
public Action AddSelectedConsumable => AddSelectedConsumableCore;
|
||
public Action AddAllPeas => AddAllPeasCore;
|
||
public IReadOnlyList<string> ItemRows => _itemRows;
|
||
public IReadOnlyList<string> ConsumableRows => _consumableRows;
|
||
public int SelectedItemRowIndex => _selectedItemRow;
|
||
public int SelectedConsumableRowIndex => _selectedConsumableRow;
|
||
public Action<int> SelectItemRow => index =>
|
||
_selectedItemRow = ClampRow(index, _itemRows.Count);
|
||
public Action<int> SelectConsumableRow => index =>
|
||
_selectedConsumableRow = ClampRow(index, _consumableRows.Count);
|
||
public Action RemoveSelectedItem => RemoveSelectedItemCore;
|
||
public Action RemoveSelectedConsumable => RemoveSelectedConsumableCore;
|
||
public Action ToggleAutoStack => () =>
|
||
{
|
||
_inventorySettings.AutoStack = !_inventorySettings.AutoStack;
|
||
_inventoryMaintenance.Reset();
|
||
SaveProfile();
|
||
};
|
||
public Action ToggleAutoCram => () =>
|
||
{
|
||
_inventorySettings.AutoCram = !_inventorySettings.AutoCram;
|
||
_inventoryMaintenance.Reset();
|
||
SaveProfile();
|
||
};
|
||
public Action ToggleAutoCraftItems => () =>
|
||
{
|
||
_inventorySettings.AutoCraftItems = !_inventorySettings.AutoCraftItems;
|
||
_crafting.Reset();
|
||
SaveProfile();
|
||
};
|
||
public Action ToggleCastDispelSelf => () => SetMetaOption(
|
||
"CastDispelSelf",
|
||
ExpressionValue.Boolean(!_vitalSettings.CastDispelSelf));
|
||
public Action ToggleUseDispelItems => () => SetMetaOption(
|
||
"UseDispelItems",
|
||
ExpressionValue.Boolean(!_vitalSettings.UseDispelItems));
|
||
public Action ToggleRefillWornMana => () =>
|
||
{
|
||
_inventorySettings.RefillWornMana = !_inventorySettings.RefillWornMana;
|
||
_itemManaRecharge.Reset();
|
||
SaveProfile();
|
||
};
|
||
public Action<float> SetRefillWornMana => value =>
|
||
{
|
||
_inventorySettings.RefillWornManaPercent = Math.Clamp(
|
||
(int)MathF.Round(value * 100f),
|
||
0,
|
||
99);
|
||
SaveProfile();
|
||
};
|
||
|
||
// ── Loot profile / editor ────────────────────────────────────────────
|
||
public bool LootEnabled => _inventorySettings.Loot.Enabled;
|
||
public bool LootPriorityBoostEnabled =>
|
||
_inventorySettings.Loot.PriorityBoost;
|
||
public bool LootAllCorpsesEnabled =>
|
||
_inventorySettings.Loot.LootAllCorpses;
|
||
public bool LootFellowCorpsesEnabled =>
|
||
_inventorySettings.Loot.LootFellowCorpses;
|
||
public bool LootOnlyRareCorpsesEnabled =>
|
||
_inventorySettings.Loot.LootOnlyRareCorpses;
|
||
public bool ReadUnknownScrollsEnabled =>
|
||
_inventorySettings.Loot.ReadUnknownScrolls;
|
||
public IReadOnlyList<string> LootProfileNames =>
|
||
_lootProfiles.AvailableNames;
|
||
public string LootProfileName => _lootProfiles.Selected;
|
||
public string LootProfileNameDraft => _lootProfileNameDraft;
|
||
public IReadOnlyList<string> LootClassifierNames
|
||
{
|
||
get
|
||
{
|
||
var names = new List<string> { "VTClassic" };
|
||
names.AddRange(_host.LootClassifiers.Available.Select(FormatClassifier));
|
||
string selected = SelectedLootClassifier;
|
||
if (!names.Contains(selected, StringComparer.Ordinal))
|
||
names.Add(selected);
|
||
return names;
|
||
}
|
||
}
|
||
public string SelectedLootClassifier
|
||
{
|
||
get
|
||
{
|
||
string id = _inventorySettings.Loot.ExternalClassifierId;
|
||
if (string.IsNullOrWhiteSpace(id))
|
||
return "VTClassic";
|
||
foreach (PluginLootClassifierInfo info in
|
||
_host.LootClassifiers.Available)
|
||
{
|
||
if (string.Equals(info.Id, id, StringComparison.OrdinalIgnoreCase))
|
||
return FormatClassifier(info);
|
||
}
|
||
return $"Unavailable [{id}]";
|
||
}
|
||
}
|
||
public string LootRangeText =>
|
||
$"Corpse range {_inventorySettings.Loot.CorpseApproachRange:0}m";
|
||
public IReadOnlyList<string> LootRuleRows => _lootRuleRows;
|
||
public int SelectedLootRuleIndex => _selectedLootRule;
|
||
public string LootExpressionDraft => _lootExpressionDraft;
|
||
public string LootEditorNotice => _lootEditorNotice;
|
||
public IReadOnlyList<string> LootActionNames => Enum.GetNames<LootAction>();
|
||
public string SelectedLootAction => SelectedLootRule?.Action.ToString()
|
||
?? LootAction.Keep.ToString();
|
||
public string LootPriorityText =>
|
||
$"Priority {SelectedLootRule?.Priority ?? 0}";
|
||
public string LootKeepCountText =>
|
||
$"Keep up to {SelectedLootRule?.KeepCount ?? 1}";
|
||
public Action ToggleLooting => () =>
|
||
{
|
||
_inventorySettings.Loot.Enabled = !_inventorySettings.Loot.Enabled;
|
||
_loot.Reset();
|
||
SaveProfile();
|
||
};
|
||
public Action ToggleLootPriorityBoost => () =>
|
||
{
|
||
_inventorySettings.Loot.PriorityBoost =
|
||
!_inventorySettings.Loot.PriorityBoost;
|
||
SaveProfile();
|
||
};
|
||
public Action ToggleLootAllCorpses => () =>
|
||
{
|
||
_inventorySettings.Loot.LootAllCorpses =
|
||
!_inventorySettings.Loot.LootAllCorpses;
|
||
SaveProfile();
|
||
};
|
||
public Action ToggleLootFellowCorpses => () =>
|
||
{
|
||
_inventorySettings.Loot.LootFellowCorpses =
|
||
!_inventorySettings.Loot.LootFellowCorpses;
|
||
SaveProfile();
|
||
};
|
||
public Action ToggleLootOnlyRareCorpses => () =>
|
||
{
|
||
_inventorySettings.Loot.LootOnlyRareCorpses =
|
||
!_inventorySettings.Loot.LootOnlyRareCorpses;
|
||
SaveProfile();
|
||
};
|
||
public Action ToggleReadUnknownScrolls => () =>
|
||
{
|
||
_inventorySettings.Loot.ReadUnknownScrolls =
|
||
!_inventorySettings.Loot.ReadUnknownScrolls;
|
||
SaveProfile();
|
||
};
|
||
public Action ShowLootEditor => () =>
|
||
{
|
||
_activeTab = TankTab.Profiles;
|
||
_lootEditorVisible = true;
|
||
RefreshLootEditor();
|
||
};
|
||
public Action<string> SelectLootProfile => SelectLootProfileCore;
|
||
public Action<string> SelectLootClassifier => value =>
|
||
{
|
||
string? id = ResolveClassifierId(value);
|
||
if (id is null)
|
||
{
|
||
_profileLifecycleNotice = $"Loot engine '{value}' is unavailable.";
|
||
return;
|
||
}
|
||
_inventorySettings.Loot.ExternalClassifierId = id;
|
||
_loot.Reset();
|
||
SaveProfile();
|
||
_profileLifecycleNotice = id.Length == 0
|
||
? "Loot engine set to VTClassic."
|
||
: $"Loot engine set to {SelectedLootClassifier}.";
|
||
};
|
||
public Action<string> SetLootProfileNameDraft => value =>
|
||
_lootProfileNameDraft = value;
|
||
public Action<string> CreateNamedLootProfile => value =>
|
||
{
|
||
_lootProfileNameDraft = value;
|
||
CreateLootProfileCore(copyCurrent: false);
|
||
};
|
||
public Action CreateLootProfile => () =>
|
||
CreateLootProfileCore(copyCurrent: false);
|
||
public Action CopyLootProfile => () =>
|
||
CreateLootProfileCore(copyCurrent: true);
|
||
public Action ClearLootProfile => ClearLootProfileCore;
|
||
public Action CloseLootEditor => () => _lootEditorVisible = false;
|
||
public Action<int> SelectLootRule => SelectLootRuleCore;
|
||
public Action<string> SetLootExpressionDraft => value =>
|
||
_lootExpressionDraft = value;
|
||
public Action<string> ApplyLootExpression => value =>
|
||
{
|
||
_lootExpressionDraft = value;
|
||
ApplyLootExpressionCore();
|
||
};
|
||
public Action ApplyLootRule => ApplyLootExpressionCore;
|
||
public Action AddLootRule => AddLootRuleCore;
|
||
public Action RemoveLootRule => RemoveLootRuleCore;
|
||
public Action MoveLootRuleUp => () => MoveLootRule(-1);
|
||
public Action MoveLootRuleDown => () => MoveLootRule(1);
|
||
public Action<string> SelectLootAction => SelectLootActionCore;
|
||
public Action LootPriorityDown => () => UpdateSelectedLootRule(rule =>
|
||
rule.Priority = Math.Max(-1000, rule.Priority - 1));
|
||
public Action LootPriorityUp => () => UpdateSelectedLootRule(rule =>
|
||
rule.Priority = Math.Min(1000, rule.Priority + 1));
|
||
public Action LootKeepCountDown => () => UpdateSelectedLootRule(rule =>
|
||
rule.KeepCount = Math.Max(0, rule.KeepCount - 1));
|
||
public Action LootKeepCountUp => () => UpdateSelectedLootRule(rule =>
|
||
rule.KeepCount = Math.Min(100000, rule.KeepCount + 1));
|
||
public Action LootRangeDown => () =>
|
||
{
|
||
_inventorySettings.Loot.CorpseApproachRange = Math.Max(
|
||
2f,
|
||
_inventorySettings.Loot.CorpseApproachRange - 2f);
|
||
SaveProfile();
|
||
};
|
||
public Action LootRangeUp => () =>
|
||
{
|
||
_inventorySettings.Loot.CorpseApproachRange = Math.Min(
|
||
100f,
|
||
_inventorySettings.Loot.CorpseApproachRange + 2f);
|
||
SaveProfile();
|
||
};
|
||
|
||
// ── Navigation / route profiles ──────────────────────────────────────
|
||
public bool NavigationEnabled => _navigationSettings.Enabled;
|
||
public bool NavigationPriorityEnabled => _navigationSettings.Priority;
|
||
public bool FollowAroundCornersEnabled =>
|
||
_navigationSettings.FollowAroundCorners;
|
||
public bool OpenDoorsEnabled => _navigationSettings.OpenDoors;
|
||
public string NavigationStatus => _navigation.Status;
|
||
public IReadOnlyList<string> RouteRows => _routeRows;
|
||
public int SelectedRouteWaypointIndex => _selectedRouteWaypoint;
|
||
public IReadOnlyList<string> RouteModeNames => Enum.GetNames<RouteMode>();
|
||
public string SelectedRouteMode => _navigationSettings.Mode.ToString();
|
||
public IReadOnlyList<string> RouteRecallNames =>
|
||
Enum.GetNames<RouteRecallKind>();
|
||
public string SelectedRouteRecall => _routeRecallKind.ToString();
|
||
public IReadOnlyList<string> RouteProfileNames =>
|
||
_routeProfiles.AvailableNames;
|
||
public string SelectedRouteProfile => _routeProfiles.Selected;
|
||
public string RouteProfileNameDraft => _routeProfileNameDraft;
|
||
public string RouteNotice => _routeNotice;
|
||
public string RouteChatDraft => _routeChatDraft;
|
||
public string RoutePauseText => $"{_routePauseSeconds} seconds";
|
||
public string RouteMinimumDistanceText => string.Create(
|
||
CultureInfo.InvariantCulture,
|
||
$"Follow/Nav Min Distance: {_navigationSettings.MinimumDistanceMeters:0.0}m");
|
||
public string RouteFollowTargetText =>
|
||
_navigationSettings.FollowTargetObjectId == 0u
|
||
? "Follow target: [None]"
|
||
: $"Follow target: {_navigationSettings.FollowTargetName}";
|
||
public string RouteAddPositionText => _routeAddToEnd
|
||
? "Add to End"
|
||
: "Insert After Selection";
|
||
|
||
public Action ToggleNavigation => () =>
|
||
{
|
||
_navigationSettings.Enabled = !_navigationSettings.Enabled;
|
||
_navigation.Reset();
|
||
SaveRouteProfile();
|
||
};
|
||
public Action ToggleNavigationPriority => () =>
|
||
{
|
||
_navigationSettings.Priority = !_navigationSettings.Priority;
|
||
SaveRouteProfile();
|
||
};
|
||
public Action ToggleFollowAroundCorners => () =>
|
||
{
|
||
_navigationSettings.FollowAroundCorners =
|
||
!_navigationSettings.FollowAroundCorners;
|
||
_navigation.Reset();
|
||
SaveRouteProfile();
|
||
};
|
||
public Action ToggleOpenDoors => () =>
|
||
{
|
||
_navigationSettings.OpenDoors = !_navigationSettings.OpenDoors;
|
||
_navigation.Reset();
|
||
SaveRouteProfile();
|
||
};
|
||
public Action<string> SelectRouteMode => value =>
|
||
{
|
||
if (!Enum.TryParse(value, ignoreCase: true, out RouteMode mode))
|
||
return;
|
||
_navigationSettings.Mode = mode;
|
||
if (mode == RouteMode.Target)
|
||
CaptureFollowTarget();
|
||
_navigation.Reset();
|
||
RefreshRouteEditor();
|
||
SaveRouteProfile();
|
||
};
|
||
public Action<string> SelectRouteRecall => value =>
|
||
{
|
||
if (Enum.TryParse(value, ignoreCase: true, out RouteRecallKind recall))
|
||
_routeRecallKind = recall;
|
||
};
|
||
public Action<int> SelectRouteWaypoint => index =>
|
||
_selectedRouteWaypoint = ClampRow(index, _navigationSettings.Waypoints.Count);
|
||
public Action AddRoutePoint => AddRoutePointCore;
|
||
public Action AddRouteUseSelected => () =>
|
||
AddSelectedObjectWaypoint(RouteWaypointType.UseNpc);
|
||
public Action AddRouteOpenVendor => () =>
|
||
AddSelectedObjectWaypoint(RouteWaypointType.OpenVendor);
|
||
public Action AddRoutePortal => () =>
|
||
AddSelectedObjectWaypoint(RouteWaypointType.PortalByName);
|
||
public Action AddRouteRecall => AddRouteRecallCore;
|
||
public Action AddRoutePause => AddRoutePauseCore;
|
||
public Action AddRouteChat => AddRouteChatCore;
|
||
public Action AddRouteCheckpoint => AddRouteCheckpointCore;
|
||
public Action AddRouteJump => AddRouteJumpCore;
|
||
public Action RemoveRouteWaypoint => RemoveRouteWaypointCore;
|
||
public Action MoveRouteWaypointUp => () => MoveRouteWaypoint(-1);
|
||
public Action MoveRouteWaypointDown => () => MoveRouteWaypoint(1);
|
||
public Action ToggleRouteAddPosition => () =>
|
||
_routeAddToEnd = !_routeAddToEnd;
|
||
public Action RoutePauseDown => () =>
|
||
_routePauseSeconds = Math.Max(0, _routePauseSeconds - 1);
|
||
public Action RoutePauseUp => () =>
|
||
_routePauseSeconds = Math.Min(3600, _routePauseSeconds + 1);
|
||
public Action RouteMinimumDistanceDown => () =>
|
||
{
|
||
_navigationSettings.MinimumDistanceMeters = Math.Max(
|
||
0.5d,
|
||
_navigationSettings.MinimumDistanceMeters - 0.5d);
|
||
SaveRouteProfile();
|
||
};
|
||
public Action RouteMinimumDistanceUp => () =>
|
||
{
|
||
_navigationSettings.MinimumDistanceMeters = Math.Min(
|
||
50d,
|
||
_navigationSettings.MinimumDistanceMeters + 0.5d);
|
||
SaveRouteProfile();
|
||
};
|
||
public Action<string> SetRouteChatDraft => value =>
|
||
_routeChatDraft = value;
|
||
public Action<string> SetRouteProfileNameDraft => value =>
|
||
_routeProfileNameDraft = value;
|
||
public Action<string> SelectRouteProfile => SelectRouteProfileCore;
|
||
public Action<string> CreateNamedRouteProfile => value =>
|
||
{
|
||
_routeProfileNameDraft = value;
|
||
CreateRouteProfileCore(copyCurrent: false);
|
||
};
|
||
public Action CreateRouteProfile => () =>
|
||
CreateRouteProfileCore(copyCurrent: false);
|
||
public Action CopyRouteProfile => () =>
|
||
CreateRouteProfileCore(copyCurrent: true);
|
||
public Action ClearRouteProfile => ClearRouteProfileCore;
|
||
public Action SetFollowTarget => CaptureFollowTarget;
|
||
|
||
// ── Meta profile / editor ────────────────────────────────────────────
|
||
public bool MetaEnabled => _meta.Enabled;
|
||
public string MetaState => _meta.CurrentState;
|
||
public string MetaStateText => $"State: {MetaState}";
|
||
public string MetaStatus => _meta.Status;
|
||
public IReadOnlyList<string> MetaRows => _metaRows;
|
||
public int SelectedMetaRuleIndex => _selectedMetaRule;
|
||
public IReadOnlyList<string> MetaConditionNames =>
|
||
Enum.GetNames<MetaConditionKind>();
|
||
public IReadOnlyList<string> MetaActionNames => Enum.GetNames<MetaActionKind>();
|
||
public string SelectedMetaCondition => _metaConditionKind.ToString();
|
||
public string SelectedMetaAction => _metaActionKind.ToString();
|
||
public string MetaStateDraft => _metaStateDraft;
|
||
public string MetaConditionTextDraft => _metaConditionTextDraft;
|
||
public string MetaActionTextDraft => _metaActionTextDraft;
|
||
public string MetaSecondaryTextDraft => _metaSecondaryTextDraft;
|
||
public string MetaNumberText => _metaNumber.ToString(CultureInfo.InvariantCulture);
|
||
public string MetaNumberLabel => $"N: {MetaNumberText}";
|
||
public string MetaSecondaryNumberText =>
|
||
_metaSecondaryNumber.ToString(CultureInfo.InvariantCulture);
|
||
public string MetaSecondaryNumberLabel => $"N2: {MetaSecondaryNumberText}";
|
||
public string MetaNotice => _metaNotice;
|
||
public IReadOnlyList<string> MetaProfileNames => _metaProfiles.AvailableNames;
|
||
public string SelectedMetaProfile => _metaProfiles.Selected;
|
||
public string MetaProfileNameDraft => _metaProfileNameDraft;
|
||
public Action ToggleMeta => () =>
|
||
{
|
||
_meta.SetEnabled(!_meta.Enabled);
|
||
_combatSettings.MetaState = _meta.CurrentState;
|
||
};
|
||
public Action<int> SelectMetaRule => SelectMetaRuleCore;
|
||
public Action<string> SelectMetaCondition => value =>
|
||
{
|
||
if (Enum.TryParse(value, ignoreCase: true, out MetaConditionKind parsed))
|
||
_metaConditionKind = parsed;
|
||
};
|
||
public Action<string> SelectMetaAction => value =>
|
||
{
|
||
if (Enum.TryParse(value, ignoreCase: true, out MetaActionKind parsed))
|
||
_metaActionKind = parsed;
|
||
};
|
||
public Action<string> SetMetaStateDraft => value => _metaStateDraft = value;
|
||
public Action<string> SetMetaConditionTextDraft => value =>
|
||
_metaConditionTextDraft = value;
|
||
public Action<string> SetMetaActionTextDraft => value =>
|
||
_metaActionTextDraft = value;
|
||
public Action<string> SetMetaSecondaryTextDraft => value =>
|
||
_metaSecondaryTextDraft = value;
|
||
public Action MetaNumberDown => () => _metaNumber--;
|
||
public Action MetaNumberUp => () => _metaNumber++;
|
||
public Action MetaSecondaryNumberDown => () => _metaSecondaryNumber--;
|
||
public Action MetaSecondaryNumberUp => () => _metaSecondaryNumber++;
|
||
public Action AddMetaRule => AddMetaRuleCore;
|
||
public Action ApplyMetaRule => ApplyMetaRuleCore;
|
||
public Action RemoveMetaRule => RemoveMetaRuleCore;
|
||
public Action MoveMetaRuleUp => () => MoveMetaRule(-1);
|
||
public Action MoveMetaRuleDown => () => MoveMetaRule(1);
|
||
public Action<string> SetMetaProfileNameDraft => value =>
|
||
_metaProfileNameDraft = value;
|
||
public Action<string> SelectMetaProfile => SelectMetaProfileCore;
|
||
public Action<string> CreateNamedMetaProfile => value =>
|
||
{
|
||
_metaProfileNameDraft = value;
|
||
CreateMetaProfileCore(copyCurrent: false);
|
||
};
|
||
public Action CreateMetaProfile => () => CreateMetaProfileCore(copyCurrent: false);
|
||
public Action CopyMetaProfile => () => CreateMetaProfileCore(copyCurrent: true);
|
||
public Action ClearMetaProfile => ClearMetaProfileCore;
|
||
|
||
// ── Profiles tab ─────────────────────────────────────────────────────
|
||
public IReadOnlyList<string> MacroProfileNames => _profiles.AvailableNames;
|
||
public string SelectedMacroProfile => _profiles.Selected;
|
||
public string ProfileNameDraft => _profileNameDraft;
|
||
public string ProfileLifecycleNotice => ProfileRecoveryNotice
|
||
?? _profileLifecycleNotice;
|
||
private string? ProfileRecoveryNotice =>
|
||
_profiles.RecoveryNotice
|
||
?? _lootProfiles.RecoveryNotice
|
||
?? _routeProfiles.RecoveryNotice
|
||
?? _metaProfiles.RecoveryNotice;
|
||
public bool MineOnlyEnabled => _profiles.MineOnly;
|
||
public Action<string> SetProfileNameDraft => value =>
|
||
_profileNameDraft = value;
|
||
public Action<string> SelectMacroProfile => SelectProfile;
|
||
public Action<string> CreateNamedProfile => value =>
|
||
{
|
||
_profileNameDraft = value;
|
||
CreateProfileCore(copyCurrent: false);
|
||
};
|
||
public Action CreateProfile => () => CreateProfileCore(copyCurrent: false);
|
||
public Action CopyProfile => () => CreateProfileCore(copyCurrent: true);
|
||
public Action ClearProfile => ClearProfileCore;
|
||
public Action ToggleMineOnly => () =>
|
||
{
|
||
string before = _profiles.Selected;
|
||
_profiles.SetMineOnly(!_profiles.MineOnly);
|
||
if (!string.Equals(before, _profiles.Selected, StringComparison.OrdinalIgnoreCase))
|
||
LoadSelectedProfile();
|
||
_profileLifecycleNotice = _profiles.MineOnly
|
||
? "Showing profiles owned by this character."
|
||
: "Showing profiles from all characters.";
|
||
};
|
||
|
||
// ── Monsters editor ──────────────────────────────────────────────────
|
||
public IReadOnlyList<string> MonsterRows => _monsterRows;
|
||
public int SelectedMonsterRuleIndex => _selectedMonsterRule;
|
||
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;
|
||
public Action<string> SetMonsterExpressionDraft => value =>
|
||
_monsterExpressionDraft = value;
|
||
public Action<string> ApplyMonsterExpression => value =>
|
||
{
|
||
_monsterExpressionDraft = value;
|
||
ApplyMonsterExpressionCore();
|
||
};
|
||
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);
|
||
|
||
/// <summary>Vitals line, using the same numbers the character panel shows.</summary>
|
||
public string Vitals => _vitals;
|
||
|
||
/// <summary>What a buff pass would cover, named from the retail tables.</summary>
|
||
public string Coverage => _coverage;
|
||
|
||
// ── settings bindings ─────────────────────────────────────────────────
|
||
// Adjuster buttons rather than typed entry: buttons are a proven primitive
|
||
// in plugin markup, whereas an editable field would need keyboard routing
|
||
// plumbed through to plugin panels first.
|
||
|
||
/// <summary>
|
||
/// VTank's <c>SpellDiffExcessThreshold-Buff</c>. Signed on purpose — the
|
||
/// wiki is explicit that "a positive number raises the skill necessary to
|
||
/// cast spells, a negative number lowers it", so a lower-level character
|
||
/// can reach for higher tiers by going negative.
|
||
/// </summary>
|
||
public string DifficultyText =>
|
||
$"Spell difficulty margin: {_buffSettings.SkillExcessOverDifficulty:+0;-0;0}";
|
||
|
||
public string RebuffText =>
|
||
$"Rebuff when under: {_buffSettings.RebuffWhenUnderSeconds / 60.0:0.#} min";
|
||
|
||
public string NormalHealthText => Percent(_vitalSettings.NormalHealth);
|
||
public string NormalStaminaText => Percent(_vitalSettings.NormalStamina);
|
||
public string NormalManaText => Percent(_vitalSettings.NormalMana);
|
||
public string NoTargetHealthText => Percent(_vitalSettings.NoTargetHealth);
|
||
public string NoTargetStaminaText => Percent(_vitalSettings.NoTargetStamina);
|
||
public string NoTargetManaText => Percent(_vitalSettings.NoTargetMana);
|
||
public string HelperHealthText => Percent(_vitalSettings.HelperHealth);
|
||
public string HelperStaminaText => Percent(_vitalSettings.HelperStamina);
|
||
public string HelperManaText => Percent(_vitalSettings.HelperMana);
|
||
public string VitalUpkeepText =>
|
||
$"Stamina to Mana / Revitalize: {OnOff(_vitalSettings.Enabled)}";
|
||
public string TrainedOnlyText =>
|
||
$"Trained skills only: {OnOff(_buffSettings.BuffTrainedSkillsOnly)}";
|
||
public string AttributesText =>
|
||
$"Buff attributes: {OnOff(_buffSettings.BuffAttributes)}";
|
||
public string ProtectionsText =>
|
||
$"Buff protections: {OnOff(_buffSettings.BuffProtections)}";
|
||
public string AurasText =>
|
||
$"Buff weapon auras: {OnOff(_buffSettings.BuffAuras)}";
|
||
public string BanesText =>
|
||
$"Buff banes (armor): {OnOff(_buffSettings.BuffBanes)}";
|
||
public string RegenerationText =>
|
||
$"Buff regen rates: {OnOff(_buffSettings.BuffRegeneration)}";
|
||
public string OtherText =>
|
||
$"Buff other self-spells: {OnOff(_buffSettings.BuffOther)}";
|
||
|
||
public bool VitalUpkeepEnabled => _vitalSettings.Enabled;
|
||
public bool HelpOthersEnabled => _vitalSettings.HelpOthers;
|
||
public bool TrainedOnlyEnabled => _buffSettings.BuffTrainedSkillsOnly;
|
||
public bool AttributesEnabled => _buffSettings.BuffAttributes;
|
||
public bool ProtectionsEnabled => _buffSettings.BuffProtections;
|
||
public bool AurasEnabled => _buffSettings.BuffAuras;
|
||
public bool BanesEnabled => _buffSettings.BuffBanes;
|
||
public bool RegenerationEnabled => _buffSettings.BuffRegeneration;
|
||
public bool OtherEnabled => _buffSettings.BuffOther;
|
||
|
||
public string TargetMethodText =>
|
||
$"Target selection: {_combatSettings.SelectionMethod}";
|
||
public string TargetLockText =>
|
||
$"Target lock: {OnOff(_combatSettings.TargetLock)}";
|
||
public string AttackRangeText =>
|
||
$"Maximum target range: {_combatSettings.MaximumRange:0}m";
|
||
public string MonsterRangeValueText =>
|
||
_combatSettings.MaximumRange.ToString("0.#", CultureInfo.InvariantCulture);
|
||
public string RingRangeValueText =>
|
||
_combatSettings.RingDistance.ToString("0.#", CultureInfo.InvariantCulture);
|
||
public string ApproachRangeValueText =>
|
||
_combatSettings.ApproachDistance.ToString("0.#", CultureInfo.InvariantCulture);
|
||
public string FollowNavMinimumValueText =>
|
||
_navigationSettings.MinimumDistanceMeters.ToString(
|
||
"0.#",
|
||
CultureInfo.InvariantCulture);
|
||
public string AngleRangeText =>
|
||
$"Angle-selection range: {_combatSettings.TargetSelectAngleRange:0}m";
|
||
public string AttackHeightText =>
|
||
$"Physical attack height: {_combatSettings.AttackHeight}";
|
||
public string AttackPowerText =>
|
||
$"Power / accuracy: {_combatSettings.AttackPower * 100f:0}%";
|
||
public bool TargetLockEnabled => _combatSettings.TargetLock;
|
||
public bool SummonPetsEnabled => _combatSettings.SummonPets;
|
||
public bool CustomPetRangeEnabled =>
|
||
_combatSettings.PetRangeMode == PetRangeMode.Custom;
|
||
public string PetRangeText => _combatSettings.PetRangeMode == PetRangeMode.Custom
|
||
? $"Pet range: {_combatSettings.PetCustomRange:0}m"
|
||
: $"Pet range: attack ({_combatSettings.MaximumRange:0}m)";
|
||
public string PetCustomRangeValueText =>
|
||
_combatSettings.PetCustomRange.ToString("0.#", CultureInfo.InvariantCulture);
|
||
public string PetDensityText =>
|
||
$"Pet min. monsters: {_combatSettings.PetMonsterDensity}";
|
||
public string PetDensityValueText =>
|
||
_combatSettings.PetMonsterDensity.ToString(CultureInfo.InvariantCulture);
|
||
public float NormalHealthValue => (float)_vitalSettings.NormalHealth;
|
||
public float NormalStaminaValue => (float)_vitalSettings.NormalStamina;
|
||
public float NormalManaValue => (float)_vitalSettings.NormalMana;
|
||
public float NoTargetHealthValue => (float)_vitalSettings.NoTargetHealth;
|
||
public float NoTargetStaminaValue => (float)_vitalSettings.NoTargetStamina;
|
||
public float NoTargetManaValue => (float)_vitalSettings.NoTargetMana;
|
||
public float HelperHealthValue => (float)_vitalSettings.HelperHealth;
|
||
public float HelperStaminaValue => (float)_vitalSettings.HelperStamina;
|
||
public float HelperManaValue => (float)_vitalSettings.HelperMana;
|
||
public float AttackPowerValue => _combatSettings.AttackPower;
|
||
|
||
public Action<float> SetNormalHealth => value =>
|
||
UpdateVital(() => _vitalSettings.NormalHealth = Clamp(value));
|
||
public Action<float> SetNormalStamina => value =>
|
||
UpdateVital(() => _vitalSettings.NormalStamina = Clamp(value));
|
||
public Action<float> SetNormalMana => value =>
|
||
UpdateVital(() => _vitalSettings.NormalMana = Clamp(value));
|
||
public Action<float> SetNoTargetHealth => value =>
|
||
UpdateVital(() => _vitalSettings.NoTargetHealth = Clamp(value));
|
||
public Action<float> SetNoTargetStamina => value =>
|
||
UpdateVital(() => _vitalSettings.NoTargetStamina = Clamp(value));
|
||
public Action<float> SetNoTargetMana => value =>
|
||
UpdateVital(() => _vitalSettings.NoTargetMana = Clamp(value));
|
||
public Action<float> SetHelperHealth => value =>
|
||
UpdateVital(() => _vitalSettings.HelperHealth = Clamp(value));
|
||
public Action<float> SetHelperStamina => value =>
|
||
UpdateVital(() => _vitalSettings.HelperStamina = Clamp(value));
|
||
public Action<float> SetHelperMana => value =>
|
||
UpdateVital(() => _vitalSettings.HelperMana = Clamp(value));
|
||
public Action<float> SetAttackPower => value => UpdateProfile(() =>
|
||
_combatSettings.AttackPower = Math.Clamp(value, 0f, 1f));
|
||
|
||
public Action DifficultyDown => () => UpdateProfile(() =>
|
||
_buffSettings.SkillExcessOverDifficulty =
|
||
Math.Max(-100, _buffSettings.SkillExcessOverDifficulty - 5));
|
||
public Action DifficultyUp => () => UpdateProfile(() =>
|
||
_buffSettings.SkillExcessOverDifficulty =
|
||
Math.Min(100, _buffSettings.SkillExcessOverDifficulty + 5));
|
||
|
||
public Action RebuffDown => () => UpdateProfile(() =>
|
||
_buffSettings.RebuffWhenUnderSeconds =
|
||
Math.Max(30, _buffSettings.RebuffWhenUnderSeconds - 30));
|
||
public Action RebuffUp => () => UpdateProfile(() =>
|
||
_buffSettings.RebuffWhenUnderSeconds =
|
||
Math.Min(1800, _buffSettings.RebuffWhenUnderSeconds + 30));
|
||
|
||
public Action ToggleVitalUpkeep => () =>
|
||
{
|
||
_vitalSettings.Enabled = !_vitalSettings.Enabled;
|
||
if (!_vitalSettings.Enabled)
|
||
_vitalRecharge.Reset();
|
||
SaveProfile();
|
||
};
|
||
public Action ToggleBuffing => () => SetMetaOption(
|
||
"EnableBuffing",
|
||
ExpressionValue.Boolean(!_buffSettings.Enabled));
|
||
public Action ToggleIdlePeaceMode => () => SetMetaOption(
|
||
"IdlePeaceMode",
|
||
ExpressionValue.Boolean(!_combatSettings.IdlePeaceMode));
|
||
public Action ToggleIdleBuffTopoff => () => SetMetaOption(
|
||
"IdleBuffTopoff",
|
||
ExpressionValue.Boolean(!_buffSettings.IdleBuffTopoff));
|
||
public Action ToggleManaChargesWhenOff => () => SetMetaOption(
|
||
"ManaChargesWhenOff",
|
||
ExpressionValue.Boolean(!_inventorySettings.ManaChargesWhenOff));
|
||
public Action ToggleHelpOthers => () => UpdateVital(() =>
|
||
_vitalSettings.HelpOthers = !_vitalSettings.HelpOthers);
|
||
public Action ToggleTrainedOnly => () => UpdateProfile(() =>
|
||
_buffSettings.BuffTrainedSkillsOnly = !_buffSettings.BuffTrainedSkillsOnly);
|
||
public Action ToggleAttributes => () => UpdateProfile(() =>
|
||
_buffSettings.BuffAttributes = !_buffSettings.BuffAttributes);
|
||
public Action ToggleProtections => () => UpdateProfile(() =>
|
||
_buffSettings.BuffProtections = !_buffSettings.BuffProtections);
|
||
public Action ToggleAuras => () => UpdateProfile(() =>
|
||
_buffSettings.BuffAuras = !_buffSettings.BuffAuras);
|
||
public Action ToggleBanes => () => UpdateProfile(() =>
|
||
_buffSettings.BuffBanes = !_buffSettings.BuffBanes);
|
||
public Action ToggleRegeneration => () => UpdateProfile(() =>
|
||
_buffSettings.BuffRegeneration = !_buffSettings.BuffRegeneration);
|
||
public Action ToggleOther => () => UpdateProfile(() =>
|
||
_buffSettings.BuffOther = !_buffSettings.BuffOther);
|
||
public Action CycleTargetMethod => () => UpdateProfile(() =>
|
||
_combatSettings.SelectionMethod = _combatSettings.SelectionMethod switch
|
||
{
|
||
TargetSelectionMethod.Range => TargetSelectionMethod.Angle,
|
||
TargetSelectionMethod.Angle => TargetSelectionMethod.Both,
|
||
_ => TargetSelectionMethod.Range,
|
||
});
|
||
public Action ToggleTargetLock => () => UpdateProfile(() =>
|
||
_combatSettings.TargetLock = !_combatSettings.TargetLock);
|
||
public Action ToggleSummonPets => () => UpdateProfile(() =>
|
||
_combatSettings.SummonPets = !_combatSettings.SummonPets);
|
||
public Action TogglePetRangeMode => () => UpdateProfile(() =>
|
||
_combatSettings.PetRangeMode = _combatSettings.PetRangeMode == PetRangeMode.Custom
|
||
? PetRangeMode.AttackDistance
|
||
: PetRangeMode.Custom);
|
||
public Action PetRangeDown => () => UpdateProfile(() =>
|
||
_combatSettings.PetCustomRange =
|
||
Math.Max(1f, _combatSettings.PetCustomRange - 1f));
|
||
public Action PetRangeUp => () => UpdateProfile(() =>
|
||
_combatSettings.PetCustomRange =
|
||
Math.Min(100f, _combatSettings.PetCustomRange + 1f));
|
||
public Action<string> SetPetCustomRangeText => value =>
|
||
SetDistanceText(value, 1f, 100f, distance =>
|
||
_combatSettings.PetCustomRange = distance);
|
||
public Action PetDensityDown => () => UpdateProfile(() =>
|
||
_combatSettings.PetMonsterDensity =
|
||
Math.Max(1, _combatSettings.PetMonsterDensity - 1));
|
||
public Action PetDensityUp => () => UpdateProfile(() =>
|
||
_combatSettings.PetMonsterDensity =
|
||
Math.Min(25, _combatSettings.PetMonsterDensity + 1));
|
||
public Action<string> SetPetDensityText => value =>
|
||
{
|
||
if (!int.TryParse(
|
||
value,
|
||
NumberStyles.Integer,
|
||
CultureInfo.InvariantCulture,
|
||
out int density))
|
||
{
|
||
return;
|
||
}
|
||
_combatSettings.PetMonsterDensity = Math.Clamp(density, 1, 25);
|
||
SaveProfile();
|
||
};
|
||
public Action AttackRangeDown => () => UpdateProfile(() =>
|
||
_combatSettings.MaximumRange =
|
||
Math.Max(2f, _combatSettings.MaximumRange - 2f));
|
||
public Action AttackRangeUp => () => UpdateProfile(() =>
|
||
_combatSettings.MaximumRange =
|
||
Math.Min(100f, _combatSettings.MaximumRange + 2f));
|
||
public Action<string> SetMonsterRangeText => value =>
|
||
SetDistanceText(value, 1f, 100f, distance =>
|
||
_combatSettings.MaximumRange = distance);
|
||
public Action<string> SetRingRangeText => value =>
|
||
SetDistanceText(value, 1f, 100f, distance =>
|
||
_combatSettings.RingDistance = distance);
|
||
public Action<string> SetApproachRangeText => value =>
|
||
SetDistanceText(value, 0f, 100f, distance =>
|
||
_combatSettings.ApproachDistance = distance);
|
||
public Action<string> SetFollowNavMinimumText => value =>
|
||
SetDistanceText(value, 0.5f, 50f, distance =>
|
||
{
|
||
_navigationSettings.MinimumDistanceMeters = distance;
|
||
SaveRouteProfile();
|
||
}, saveProfile: false);
|
||
public Action AngleRangeDown => () => UpdateProfile(() =>
|
||
_combatSettings.TargetSelectAngleRange =
|
||
Math.Max(2f, _combatSettings.TargetSelectAngleRange - 2f));
|
||
public Action AngleRangeUp => () => UpdateProfile(() =>
|
||
_combatSettings.TargetSelectAngleRange = Math.Min(
|
||
_combatSettings.MaximumRange,
|
||
_combatSettings.TargetSelectAngleRange + 2f));
|
||
public Action CycleAttackHeight => () => UpdateProfile(() =>
|
||
_combatSettings.AttackHeight = _combatSettings.AttackHeight switch
|
||
{
|
||
PluginAttackHeight.High => PluginAttackHeight.Medium,
|
||
PluginAttackHeight.Medium => PluginAttackHeight.Low,
|
||
_ => PluginAttackHeight.High,
|
||
});
|
||
public Action AttackPowerDown => () => UpdateProfile(() =>
|
||
_combatSettings.AttackPower =
|
||
Math.Max(0f, MathF.Round(_combatSettings.AttackPower - 0.1f, 2)));
|
||
public Action AttackPowerUp => () => UpdateProfile(() =>
|
||
_combatSettings.AttackPower =
|
||
Math.Min(1f, MathF.Round(_combatSettings.AttackPower + 0.1f, 2)));
|
||
|
||
private static double Clamp(float value) => Math.Clamp((double)value, 0d, 1d);
|
||
|
||
private void UpdateVital(Action update)
|
||
{
|
||
UpdateProfile(update);
|
||
}
|
||
|
||
private void UpdateProfile(Action update)
|
||
{
|
||
update();
|
||
SaveProfile();
|
||
}
|
||
|
||
private void SetDistanceText(
|
||
string text,
|
||
float minimum,
|
||
float maximum,
|
||
Action<float> apply,
|
||
bool saveProfile = true)
|
||
{
|
||
if (!float.TryParse(
|
||
text,
|
||
NumberStyles.Float,
|
||
CultureInfo.InvariantCulture,
|
||
out float distance)
|
||
&& !float.TryParse(
|
||
text,
|
||
NumberStyles.Float,
|
||
CultureInfo.CurrentCulture,
|
||
out distance))
|
||
{
|
||
return;
|
||
}
|
||
apply(Math.Clamp(distance, minimum, maximum));
|
||
if (saveProfile)
|
||
SaveProfile();
|
||
}
|
||
|
||
private static string Percent(double fraction) =>
|
||
(fraction * 100).ToString("0", CultureInfo.InvariantCulture) + "%";
|
||
|
||
private static string OnOff(bool value) => value ? "on" : "off";
|
||
|
||
private static string ProfileText(string heading, IEnumerable<string> names)
|
||
{
|
||
string[] entries = names
|
||
.OrderBy(static name => name, StringComparer.Ordinal)
|
||
.Take(8)
|
||
.ToArray();
|
||
return entries.Length == 0
|
||
? $"{heading}: [None]"
|
||
: $"{heading}: {string.Join(" | ", entries)}";
|
||
}
|
||
|
||
private void RefreshItemEditors()
|
||
{
|
||
RefreshConsumableCategories();
|
||
_itemRows = _combatSettings.CombatItemNames
|
||
.OrderBy(static name => name, StringComparer.Ordinal)
|
||
.Select(name => _noBuffItemNames.Contains(name)
|
||
? name + " [no buffs]"
|
||
: name)
|
||
.ToArray();
|
||
_consumableRows = _combatSettings.ConsumableNames
|
||
.OrderBy(static name => name, StringComparer.Ordinal)
|
||
.ToArray();
|
||
_selectedItemRow = ClampRow(_selectedItemRow, _itemRows.Count);
|
||
_selectedConsumableRow = ClampRow(
|
||
_selectedConsumableRow,
|
||
_consumableRows.Count);
|
||
}
|
||
|
||
private static int ClampRow(int index, int count) => count == 0
|
||
? 0
|
||
: Math.Clamp(index, 0, count - 1);
|
||
|
||
private void RemoveSelectedItemCore()
|
||
{
|
||
string[] names = _combatSettings.CombatItemNames
|
||
.OrderBy(static name => name, StringComparer.Ordinal)
|
||
.ToArray();
|
||
if (names.Length == 0)
|
||
{
|
||
_profileNotice = "The Items profile is empty.";
|
||
return;
|
||
}
|
||
string removed = names[ClampRow(_selectedItemRow, names.Length)];
|
||
_combatSettings.CombatItemNames.Remove(removed);
|
||
_noBuffItemNames.Remove(removed);
|
||
_profileNotice = $"Removed {removed}.";
|
||
RefreshItemEditors();
|
||
SaveProfile();
|
||
}
|
||
|
||
private void RemoveSelectedConsumableCore()
|
||
{
|
||
string[] names = _combatSettings.ConsumableNames
|
||
.OrderBy(static name => name, StringComparer.Ordinal)
|
||
.ToArray();
|
||
if (names.Length == 0)
|
||
{
|
||
_profileNotice = "The Consumables profile is empty.";
|
||
return;
|
||
}
|
||
string removed = names[ClampRow(_selectedConsumableRow, names.Length)];
|
||
_combatSettings.ConsumableNames.Remove(removed);
|
||
_combatSettings.ConsumableCategories.Remove(removed);
|
||
_profileNotice = $"Removed {removed}.";
|
||
RefreshItemEditors();
|
||
SaveProfile();
|
||
}
|
||
|
||
private LootRule? SelectedLootRule => _inventorySettings.Loot.Rules.Count == 0
|
||
? null
|
||
: _inventorySettings.Loot.Rules[Math.Clamp(
|
||
_selectedLootRule,
|
||
0,
|
||
_inventorySettings.Loot.Rules.Count - 1)];
|
||
|
||
private static string FormatClassifier(PluginLootClassifierInfo info) =>
|
||
$"{info.DisplayName} [{info.Id}]";
|
||
|
||
private string? ResolveClassifierId(string? value)
|
||
{
|
||
if (string.Equals(value?.Trim(), "VTClassic", StringComparison.OrdinalIgnoreCase))
|
||
return string.Empty;
|
||
foreach (PluginLootClassifierInfo info in _host.LootClassifiers.Available)
|
||
{
|
||
if (string.Equals(value?.Trim(), FormatClassifier(info),
|
||
StringComparison.Ordinal)
|
||
|| string.Equals(value?.Trim(), info.Id,
|
||
StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return info.Id;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private void RefreshLootEditor(bool retainDraft = false)
|
||
{
|
||
int count = _inventorySettings.Loot.Rules.Count;
|
||
_selectedLootRule = count == 0
|
||
? 0
|
||
: Math.Clamp(_selectedLootRule, 0, count - 1);
|
||
if (!retainDraft)
|
||
_lootExpressionDraft = SelectedLootRule?.Expression ?? "*";
|
||
_lootRuleRows = _inventorySettings.Loot.Rules
|
||
.Select((rule, index) =>
|
||
$"{index + 1}. {rule.Action} P{rule.Priority} "
|
||
+ (rule.VtankRequirements.Count == 0
|
||
? rule.Expression
|
||
: $"[VTClassic: {rule.VtankRequirements.Count} requirements]"))
|
||
.ToArray();
|
||
}
|
||
|
||
private void SelectLootRuleCore(int index)
|
||
{
|
||
if (index < 0 || index >= _inventorySettings.Loot.Rules.Count)
|
||
return;
|
||
_selectedLootRule = index;
|
||
_lootExpressionDraft =
|
||
_inventorySettings.Loot.Rules[index].Expression;
|
||
_lootEditorNotice = $"Editing rule {index + 1}.";
|
||
RefreshLootEditor(retainDraft: true);
|
||
}
|
||
|
||
private void SelectLootProfileCore(string name)
|
||
{
|
||
SaveProfile();
|
||
if (!_lootProfiles.Select(name))
|
||
{
|
||
_lootEditorNotice = $"Loot profile '{name}' is unavailable.";
|
||
return;
|
||
}
|
||
LoadLootProfile();
|
||
_lootEditorNotice = $"Loaded loot profile {_lootProfiles.Selected}.";
|
||
}
|
||
|
||
private void CreateLootProfileCore(bool copyCurrent)
|
||
{
|
||
if (!_lootProfiles.Create(
|
||
_lootProfileNameDraft,
|
||
copyCurrent,
|
||
_inventorySettings.Loot.Rules,
|
||
out string notice,
|
||
_inventorySettings.Loot))
|
||
{
|
||
_lootEditorNotice = notice;
|
||
return;
|
||
}
|
||
_lootProfileNameDraft = string.Empty;
|
||
LoadLootProfile();
|
||
_lootEditorNotice = notice;
|
||
}
|
||
|
||
private void ClearLootProfileCore()
|
||
{
|
||
_lootProfiles.ClearCurrent(
|
||
_inventorySettings.Loot.Rules,
|
||
_inventorySettings.Loot);
|
||
_loot.Reset();
|
||
RefreshLootEditor();
|
||
_lootEditorNotice = $"Cleared {_lootProfiles.Selected}.";
|
||
}
|
||
|
||
private void LoadLootProfile()
|
||
{
|
||
if (!_lootProfiles.LoadCurrent(
|
||
_inventorySettings.Loot.Rules,
|
||
_inventorySettings.Loot))
|
||
{
|
||
_lootProfiles.SaveCurrent(
|
||
_inventorySettings.Loot.Rules,
|
||
_inventorySettings.Loot);
|
||
}
|
||
_loot.Reset();
|
||
RefreshLootEditor();
|
||
}
|
||
|
||
private void ApplyLootExpressionCore()
|
||
{
|
||
if (SelectedLootRule is not { } rule)
|
||
{
|
||
_lootEditorNotice = "Add a loot rule first.";
|
||
return;
|
||
}
|
||
try
|
||
{
|
||
_ = LootRuleExpression.Compile(_lootExpressionDraft);
|
||
rule.Expression = _lootExpressionDraft;
|
||
rule.CustomExpression = string.Empty;
|
||
rule.VtankRequirements.Clear();
|
||
_lootEditorNotice = $"Updated {rule.Name}.";
|
||
_loot.Reset();
|
||
RefreshLootEditor();
|
||
SaveProfile();
|
||
}
|
||
catch (FormatException error)
|
||
{
|
||
_lootEditorNotice = error.Message;
|
||
}
|
||
}
|
||
|
||
private void AddLootRuleCore()
|
||
{
|
||
var rule = new LootRule
|
||
{
|
||
Name = $"Rule {_inventorySettings.Loot.Rules.Count + 1}",
|
||
Expression = "*",
|
||
Action = LootAction.Keep,
|
||
};
|
||
_inventorySettings.Loot.Rules.Add(rule);
|
||
_selectedLootRule = _inventorySettings.Loot.Rules.Count - 1;
|
||
_lootExpressionDraft = rule.Expression;
|
||
_lootEditorNotice = $"Added {rule.Name}.";
|
||
_loot.Reset();
|
||
RefreshLootEditor();
|
||
SaveProfile();
|
||
}
|
||
|
||
private void RemoveLootRuleCore()
|
||
{
|
||
if (SelectedLootRule is not { } rule)
|
||
{
|
||
_lootEditorNotice = "The loot profile is empty.";
|
||
return;
|
||
}
|
||
_inventorySettings.Loot.Rules.RemoveAt(_selectedLootRule);
|
||
_selectedLootRule = Math.Min(
|
||
_selectedLootRule,
|
||
Math.Max(0, _inventorySettings.Loot.Rules.Count - 1));
|
||
_lootEditorNotice = $"Removed {rule.Name}.";
|
||
_loot.Reset();
|
||
RefreshLootEditor();
|
||
SaveProfile();
|
||
}
|
||
|
||
private void MoveLootRule(int direction)
|
||
{
|
||
if (SelectedLootRule is not { } rule)
|
||
return;
|
||
int destination = _selectedLootRule + Math.Sign(direction);
|
||
if (destination < 0 || destination >= _inventorySettings.Loot.Rules.Count)
|
||
return;
|
||
_inventorySettings.Loot.Rules.RemoveAt(_selectedLootRule);
|
||
_inventorySettings.Loot.Rules.Insert(destination, rule);
|
||
_selectedLootRule = destination;
|
||
_lootEditorNotice = $"Moved {rule.Name}.";
|
||
_loot.Reset();
|
||
RefreshLootEditor();
|
||
SaveProfile();
|
||
}
|
||
|
||
private void SelectLootActionCore(string value)
|
||
{
|
||
if (SelectedLootRule is not { } rule
|
||
|| !Enum.TryParse(value, ignoreCase: true, out LootAction action))
|
||
{
|
||
return;
|
||
}
|
||
rule.Action = action;
|
||
_lootEditorNotice = $"{rule.Name}: {action}.";
|
||
_loot.Reset();
|
||
RefreshLootEditor();
|
||
SaveProfile();
|
||
}
|
||
|
||
private void UpdateSelectedLootRule(Action<LootRule> update)
|
||
{
|
||
if (SelectedLootRule is not { } rule)
|
||
return;
|
||
update(rule);
|
||
_lootEditorNotice = $"Updated {rule.Name}.";
|
||
_loot.Reset();
|
||
RefreshLootEditor();
|
||
SaveProfile();
|
||
}
|
||
|
||
private void RefreshRouteEditor()
|
||
{
|
||
_selectedRouteWaypoint = ClampRow(
|
||
_selectedRouteWaypoint,
|
||
_navigationSettings.Waypoints.Count);
|
||
int active = _navigation.CurrentWaypointIndex;
|
||
_routeRows = _navigationSettings.Waypoints
|
||
.Select((waypoint, index) =>
|
||
$"{(index == active && _navigationSettings.Enabled ? "<<" : " ")} "
|
||
+ waypoint.DisplayText)
|
||
.ToArray();
|
||
}
|
||
|
||
private void AddRoutePointCore()
|
||
{
|
||
PluginNavigationSnapshot snapshot =
|
||
_host.Automation.Navigation.Snapshot;
|
||
if (!snapshot.IsAvailable)
|
||
{
|
||
_routeNotice = "Current position is unavailable.";
|
||
return;
|
||
}
|
||
AddRouteWaypoint(new RouteWaypoint
|
||
{
|
||
Type = RouteWaypointType.Point,
|
||
Position = snapshot.Position,
|
||
});
|
||
_routeNotice = $"Added point {RouteWaypoint.FormatPosition(snapshot.Position)}.";
|
||
}
|
||
|
||
private void AddRouteCheckpointCore()
|
||
{
|
||
PluginNavigationSnapshot snapshot =
|
||
_host.Automation.Navigation.Snapshot;
|
||
if (!snapshot.IsAvailable)
|
||
{
|
||
_routeNotice = "Current position is unavailable.";
|
||
return;
|
||
}
|
||
AddRouteWaypoint(new RouteWaypoint
|
||
{
|
||
Type = RouteWaypointType.Checkpoint,
|
||
Position = snapshot.Position,
|
||
});
|
||
_routeNotice = "Added checkpoint.";
|
||
}
|
||
|
||
private void AddSelectedObjectWaypoint(RouteWaypointType type)
|
||
{
|
||
uint selected = _host.Selection.SelectedObjectId ?? 0u;
|
||
if (selected == 0u
|
||
|| !_host.Automation.Navigation.TryGetObject(
|
||
selected,
|
||
out PluginNavigationObject target))
|
||
{
|
||
_routeNotice = "Select a live portal, NPC, or vendor first.";
|
||
return;
|
||
}
|
||
AddRouteWaypoint(new RouteWaypoint
|
||
{
|
||
Type = type,
|
||
Position = target.Position,
|
||
ObjectId = target.ObjectId,
|
||
ObjectName = target.Name,
|
||
});
|
||
_routeNotice = $"Added {type}: {target.Name}.";
|
||
}
|
||
|
||
private void AddRouteRecallCore()
|
||
{
|
||
AddRouteWaypoint(new RouteWaypoint
|
||
{
|
||
Type = RouteWaypointType.Recall,
|
||
Recall = _routeRecallKind,
|
||
Position = _host.Automation.Navigation.Snapshot.Position,
|
||
});
|
||
_routeNotice = $"Added {RouteWaypoint.RecallDisplayName(_routeRecallKind)}.";
|
||
}
|
||
|
||
private void AddRoutePauseCore()
|
||
{
|
||
AddRouteWaypoint(new RouteWaypoint
|
||
{
|
||
Type = RouteWaypointType.Pause,
|
||
DurationMilliseconds = _routePauseSeconds * 1000,
|
||
Position = _host.Automation.Navigation.Snapshot.Position,
|
||
});
|
||
_routeNotice = $"Added {_routePauseSeconds}-second pause.";
|
||
}
|
||
|
||
private void AddRouteChatCore()
|
||
{
|
||
string text = _routeChatDraft.Trim();
|
||
if (text.Length == 0)
|
||
{
|
||
_routeNotice = "Enter a chat command first.";
|
||
return;
|
||
}
|
||
AddRouteWaypoint(new RouteWaypoint
|
||
{
|
||
Type = RouteWaypointType.ChatCommand,
|
||
Text = text,
|
||
Position = _host.Automation.Navigation.Snapshot.Position,
|
||
});
|
||
_routeNotice = $"Added chat command {text}.";
|
||
}
|
||
|
||
private void AddRouteJumpCore()
|
||
{
|
||
PluginNavigationSnapshot snapshot =
|
||
_host.Automation.Navigation.Snapshot;
|
||
if (!snapshot.IsAvailable)
|
||
{
|
||
_routeNotice = "Current heading is unavailable.";
|
||
return;
|
||
}
|
||
AddRouteWaypoint(new RouteWaypoint
|
||
{
|
||
Type = RouteWaypointType.Jump,
|
||
Position = snapshot.Position,
|
||
JumpHeadingDegrees = snapshot.Position.HeadingDegrees,
|
||
JumpRun = true,
|
||
JumpChargeMilliseconds = 1000,
|
||
JumpDirection = RouteJumpDirection.Forward,
|
||
});
|
||
_routeNotice = "Added forward jump.";
|
||
}
|
||
|
||
private void AddRouteWaypoint(RouteWaypoint waypoint)
|
||
{
|
||
int insertion = _routeAddToEnd
|
||
? _navigationSettings.Waypoints.Count
|
||
: Math.Min(
|
||
_navigationSettings.Waypoints.Count,
|
||
_selectedRouteWaypoint + 1);
|
||
_navigationSettings.Waypoints.Insert(insertion, waypoint);
|
||
_selectedRouteWaypoint = insertion;
|
||
_navigation.Reset();
|
||
RefreshRouteEditor();
|
||
SaveRouteProfile();
|
||
}
|
||
|
||
private void RemoveRouteWaypointCore()
|
||
{
|
||
if (_navigationSettings.Waypoints.Count == 0)
|
||
{
|
||
_routeNotice = "The route is empty.";
|
||
return;
|
||
}
|
||
int index = ClampRow(
|
||
_selectedRouteWaypoint,
|
||
_navigationSettings.Waypoints.Count);
|
||
string removed = _navigationSettings.Waypoints[index].DisplayText;
|
||
_navigationSettings.Waypoints.RemoveAt(index);
|
||
_selectedRouteWaypoint = ClampRow(
|
||
index,
|
||
_navigationSettings.Waypoints.Count);
|
||
_navigation.Reset();
|
||
RefreshRouteEditor();
|
||
SaveRouteProfile();
|
||
_routeNotice = $"Removed {removed}.";
|
||
}
|
||
|
||
private void MoveRouteWaypoint(int direction)
|
||
{
|
||
if (_navigationSettings.Waypoints.Count < 2)
|
||
return;
|
||
int source = ClampRow(
|
||
_selectedRouteWaypoint,
|
||
_navigationSettings.Waypoints.Count);
|
||
int destination = source + Math.Sign(direction);
|
||
if (destination < 0 || destination >= _navigationSettings.Waypoints.Count)
|
||
return;
|
||
RouteWaypoint waypoint = _navigationSettings.Waypoints[source];
|
||
_navigationSettings.Waypoints.RemoveAt(source);
|
||
_navigationSettings.Waypoints.Insert(destination, waypoint);
|
||
_selectedRouteWaypoint = destination;
|
||
_navigation.Reset();
|
||
RefreshRouteEditor();
|
||
SaveRouteProfile();
|
||
}
|
||
|
||
private void CaptureFollowTarget()
|
||
{
|
||
uint selected = _host.Selection.SelectedObjectId ?? 0u;
|
||
if (selected == 0u
|
||
|| !_host.Automation.Navigation.TryGetObject(
|
||
selected,
|
||
out PluginNavigationObject target))
|
||
{
|
||
_routeNotice = "Select a live object to follow first.";
|
||
return;
|
||
}
|
||
_navigationSettings.FollowTargetObjectId = target.ObjectId;
|
||
_navigationSettings.FollowTargetName = target.Name;
|
||
_navigationSettings.Mode = RouteMode.Target;
|
||
_navigation.Reset();
|
||
RefreshRouteEditor();
|
||
SaveRouteProfile();
|
||
_routeNotice = $"Following {target.Name}.";
|
||
}
|
||
|
||
private void SelectRouteProfileCore(string name)
|
||
{
|
||
SaveRouteProfile();
|
||
if (!_routeProfiles.Select(name))
|
||
{
|
||
_routeNotice = $"Route profile '{name}' is unavailable.";
|
||
return;
|
||
}
|
||
LoadRouteProfile();
|
||
_routeNotice = $"Loaded route {_routeProfiles.Selected}.";
|
||
}
|
||
|
||
private void CreateRouteProfileCore(bool copyCurrent)
|
||
{
|
||
if (!_routeProfiles.Create(
|
||
_routeProfileNameDraft,
|
||
copyCurrent,
|
||
_navigationSettings,
|
||
out string notice))
|
||
{
|
||
_routeNotice = notice;
|
||
return;
|
||
}
|
||
_routeProfileNameDraft = string.Empty;
|
||
LoadRouteProfile();
|
||
_routeNotice = notice;
|
||
}
|
||
|
||
private void ClearRouteProfileCore()
|
||
{
|
||
_routeProfiles.ClearCurrent(_navigationSettings);
|
||
_navigation.Reset();
|
||
RefreshRouteEditor();
|
||
_routeNotice = $"Cleared {_routeProfiles.Selected}.";
|
||
}
|
||
|
||
private void LoadRouteProfile()
|
||
{
|
||
if (!_routeProfiles.LoadCurrent(_navigationSettings))
|
||
_routeProfiles.SaveCurrent(_navigationSettings);
|
||
if (_initialized)
|
||
ApplyPersistedOptionOverrides();
|
||
_navigation.Reset();
|
||
RefreshRouteEditor();
|
||
}
|
||
|
||
private void SaveRouteProfile() =>
|
||
_routeProfiles.SaveCurrent(_navigationSettings);
|
||
|
||
private void SelectTab(TankTab tab)
|
||
{
|
||
_activeTab = tab;
|
||
_lootEditorVisible = false;
|
||
_advancedOptionsVisible = false;
|
||
}
|
||
|
||
private void LoadAdvancedOptionDraft()
|
||
{
|
||
_advancedOptionValueDraft = GetMetaOption(AdvancedOptionName)
|
||
.ToDisplayString();
|
||
_advancedOptionNotice = $"Editing {AdvancedOptionName}.";
|
||
}
|
||
|
||
private void ApplyAdvancedOptionCore()
|
||
{
|
||
if (!TryParseOptionValue(
|
||
_advancedOptionValueDraft.Trim(),
|
||
out ExpressionValue value))
|
||
{
|
||
_advancedOptionNotice = "Enter a value first.";
|
||
return;
|
||
}
|
||
try
|
||
{
|
||
if (!SetMetaOption(AdvancedOptionName, value))
|
||
{
|
||
_advancedOptionNotice = $"{AdvancedOptionName} is unavailable.";
|
||
return;
|
||
}
|
||
LoadAdvancedOptionDraft();
|
||
_advancedOptionNotice = $"Applied {AdvancedOptionName}.";
|
||
}
|
||
catch (Exception exception) when (exception is FormatException
|
||
or OverflowException)
|
||
{
|
||
_advancedOptionNotice = exception.Message;
|
||
}
|
||
}
|
||
|
||
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)
|
||
{
|
||
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 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 AddMonsterRuleCore(string expression)
|
||
{
|
||
try
|
||
{
|
||
var rule = new MonsterRule(expression, new MonsterRuleActions());
|
||
_combatSettings.Rules.Add(rule);
|
||
_selectedMonsterRule = _combatSettings.Rules.Count - 1;
|
||
_monsterEditorNotice = $"Added {expression}.";
|
||
RefreshMonsterEditor();
|
||
SaveProfile();
|
||
}
|
||
catch (FormatException error)
|
||
{
|
||
_monsterEditorNotice = error.Message;
|
||
}
|
||
}
|
||
|
||
private void AddSelectedMonsterCore()
|
||
{
|
||
uint selectedId = _host.Selection.SelectedObjectId ?? 0u;
|
||
PluginCombatTarget selected = default;
|
||
bool found = false;
|
||
foreach (PluginCombatTarget candidate in
|
||
_host.Automation.Combat.CaptureHostileTargets(float.MaxValue))
|
||
{
|
||
if (candidate.ObjectId != selectedId)
|
||
continue;
|
||
selected = candidate;
|
||
found = true;
|
||
break;
|
||
}
|
||
if (!found || string.IsNullOrWhiteSpace(selected.Name))
|
||
{
|
||
_monsterEditorNotice = "Select a monster in the world first.";
|
||
return;
|
||
}
|
||
AddMonsterRuleCore(EscapeMonsterLiteral(selected.Name));
|
||
}
|
||
|
||
private static string EscapeMonsterLiteral(string value)
|
||
{
|
||
const string operators = "%/*+-#><=&|()";
|
||
var result = new System.Text.StringBuilder(value.Length + 8);
|
||
foreach (char character in value)
|
||
{
|
||
if (char.IsDigit(character)
|
||
|| character == '\\'
|
||
|| operators.Contains(character))
|
||
{
|
||
result.Append('\\');
|
||
}
|
||
result.Append(character);
|
||
}
|
||
return result.ToString();
|
||
}
|
||
|
||
private void RemoveMonsterRuleCore()
|
||
{
|
||
if (SelectedMonsterRule.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();
|
||
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
|
||
{
|
||
Flags = actions.Flags ^ flag,
|
||
});
|
||
|
||
private void SetMonsterDamage(string value, bool extra)
|
||
{
|
||
if (!TryParseDamageType(value, out MonsterDamageType parsed))
|
||
return;
|
||
UpdateSelectedMonsterActions(actions => extra
|
||
? actions with { ExtraVulnerability = parsed }
|
||
: actions with { DamageType = parsed });
|
||
}
|
||
|
||
private static string DamageTypeDisplay(MonsterDamageType value) => value switch
|
||
{
|
||
MonsterDamageType.Electric => "Lightning",
|
||
MonsterDamageType.VoidBasic or MonsterDamageType.Nether => "Void Basic",
|
||
MonsterDamageType.DrainAuto => "Drain Auto",
|
||
MonsterDamageType.PlayerAuto => "PAuto",
|
||
_ => 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))
|
||
return durableName;
|
||
if (objectId == 0u)
|
||
return "<AUTO>";
|
||
foreach (PluginInventoryItem item in
|
||
_host.Automation.Items.CaptureOwnedItems())
|
||
{
|
||
if (item.ObjectId == objectId)
|
||
return item.Name;
|
||
}
|
||
return $"0x{objectId:X8}";
|
||
}
|
||
|
||
private void UpdateSelectedMonsterActions(
|
||
Func<MonsterRuleActions, MonsterRuleActions> update)
|
||
{
|
||
MonsterRule selected = SelectedMonsterRule;
|
||
_combatSettings.Rules[_selectedMonsterRule] = new MonsterRule(
|
||
selected.Expression,
|
||
update(selected.Actions));
|
||
_monsterEditorNotice = $"Updated {selected.Expression}.";
|
||
RefreshMonsterEditor();
|
||
SaveProfile();
|
||
}
|
||
|
||
private void AddSelectedProfileItem(bool noBuffs)
|
||
{
|
||
if (!TryGetSelectedInventoryItem(out PluginInventoryItem item))
|
||
{
|
||
_profileNotice = "Select an owned inventory item first.";
|
||
return;
|
||
}
|
||
_combatSettings.CombatItemObjectIds.Add(item.ObjectId);
|
||
_combatSettings.CombatItemNames.Add(item.Name);
|
||
if (noBuffs)
|
||
_noBuffItemNames.Add(item.Name);
|
||
else
|
||
_noBuffItemNames.Remove(item.Name);
|
||
_profileNotice = noBuffs
|
||
? $"Added {item.Name} (no buffs)."
|
||
: $"Added {item.Name}.";
|
||
RefreshItemEditors();
|
||
SaveProfile();
|
||
}
|
||
|
||
private void AddSelectedConsumableCore()
|
||
{
|
||
if (!TryGetSelectedInventoryItem(out PluginInventoryItem item))
|
||
{
|
||
_profileNotice = "Select an owned consumable first.";
|
||
return;
|
||
}
|
||
_combatSettings.ConsumableNames.Add(item.Name);
|
||
_combatSettings.ConsumableCategories[item.Name] =
|
||
ConsumableClassifier.Classify(item);
|
||
_profileNotice = $"Added {item.Name}.";
|
||
RefreshItemEditors();
|
||
SaveProfile();
|
||
}
|
||
|
||
private void AddAllPeasCore()
|
||
{
|
||
bool added = _combatSettings.ConsumableNames.Add(
|
||
CraftingPlanner.AllPeas);
|
||
_combatSettings.ConsumableCategories[CraftingPlanner.AllPeas] =
|
||
ConsumableCategory.AllPeas;
|
||
_profileNotice = added
|
||
? "Added [All Peas]."
|
||
: "[All Peas] is already in this profile.";
|
||
if (added)
|
||
{
|
||
RefreshItemEditors();
|
||
SaveProfile();
|
||
}
|
||
}
|
||
|
||
private void RefreshConsumableCategories()
|
||
{
|
||
foreach (string stale in _combatSettings.ConsumableCategories.Keys
|
||
.Where(name => !_combatSettings.ConsumableNames.Contains(name))
|
||
.ToArray())
|
||
{
|
||
_combatSettings.ConsumableCategories.Remove(stale);
|
||
}
|
||
foreach (string name in _combatSettings.ConsumableNames)
|
||
{
|
||
if (!_combatSettings.ConsumableCategories.ContainsKey(name))
|
||
{
|
||
_combatSettings.ConsumableCategories[name] =
|
||
ConsumableClassifier.ClassifyName(name);
|
||
}
|
||
}
|
||
foreach (PluginInventoryItem item in
|
||
_host.Automation.Items.CaptureOwnedItems())
|
||
{
|
||
if (_combatSettings.ConsumableNames.Contains(item.Name))
|
||
{
|
||
_combatSettings.ConsumableCategories[item.Name] =
|
||
ConsumableClassifier.Classify(item);
|
||
}
|
||
}
|
||
}
|
||
|
||
private bool TryGetSelectedInventoryItem(out PluginInventoryItem selected)
|
||
{
|
||
uint selectedId = _host.Selection.SelectedObjectId ?? 0u;
|
||
if (selectedId != 0u)
|
||
{
|
||
foreach (PluginInventoryItem item in
|
||
_host.Automation.Items.CaptureOwnedItems())
|
||
{
|
||
if (item.ObjectId == selectedId)
|
||
{
|
||
selected = item;
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
selected = default;
|
||
return false;
|
||
}
|
||
|
||
private MetaRule? SelectedMetaRule =>
|
||
(uint)_selectedMetaRule < (uint)_metaProfile.Rules.Count
|
||
? _metaProfile.Rules[_selectedMetaRule]
|
||
: null;
|
||
|
||
private void RefreshMetaEditor()
|
||
{
|
||
_metaRows = _metaProfile.Rules.Select(static rule =>
|
||
$"{rule.State,-16} {DescribeMetaCondition(rule.Condition),-34} "
|
||
+ DescribeMetaAction(rule.Action)).ToArray();
|
||
_selectedMetaRule = ClampRow(_selectedMetaRule, _metaProfile.Rules.Count);
|
||
if (SelectedMetaRule is not MetaRule selected)
|
||
return;
|
||
_metaStateDraft = selected.State;
|
||
_metaConditionKind = selected.Condition.Kind;
|
||
_metaConditionTextDraft = selected.Condition.Text;
|
||
_metaActionKind = selected.Action.Kind;
|
||
_metaActionTextDraft = selected.Action.Text;
|
||
_metaSecondaryTextDraft = selected.Action.SecondaryText;
|
||
_metaNumber = checked((int)Math.Clamp(
|
||
selected.Condition.Number,
|
||
int.MinValue,
|
||
int.MaxValue));
|
||
_metaSecondaryNumber = checked((int)Math.Clamp(
|
||
selected.Condition.SecondaryNumber,
|
||
int.MinValue,
|
||
int.MaxValue));
|
||
}
|
||
|
||
private void SelectMetaRuleCore(int index)
|
||
{
|
||
_selectedMetaRule = ClampRow(index, _metaProfile.Rules.Count);
|
||
RefreshMetaEditor();
|
||
_metaNotice = SelectedMetaRule is null
|
||
? "Add a rule or select one to edit."
|
||
: "Editing the selected ordered Meta rule.";
|
||
}
|
||
|
||
private void AddMetaRuleCore()
|
||
{
|
||
if (!TryBuildMetaRule(out MetaRule rule, out string error))
|
||
{
|
||
_metaNotice = error;
|
||
return;
|
||
}
|
||
_metaProfile.Rules.Add(rule);
|
||
_selectedMetaRule = _metaProfile.Rules.Count - 1;
|
||
SaveMetaProfile();
|
||
RefreshMetaEditor();
|
||
_metaNotice = $"Added rule in {rule.State}.";
|
||
}
|
||
|
||
private void ApplyMetaRuleCore()
|
||
{
|
||
if (SelectedMetaRule is not MetaRule current)
|
||
{
|
||
AddMetaRuleCore();
|
||
return;
|
||
}
|
||
if (!TryBuildMetaRule(out MetaRule replacement, out string error))
|
||
{
|
||
_metaNotice = error;
|
||
return;
|
||
}
|
||
replacement.Id = current.Id;
|
||
_metaProfile.Rules[_selectedMetaRule] = replacement;
|
||
SaveMetaProfile();
|
||
RefreshMetaEditor();
|
||
_metaNotice = $"Updated rule in {replacement.State}.";
|
||
}
|
||
|
||
private bool TryBuildMetaRule(out MetaRule rule, out string error)
|
||
{
|
||
string state = string.IsNullOrWhiteSpace(_metaStateDraft)
|
||
? MetaEngine.DefaultState
|
||
: _metaStateDraft.Trim();
|
||
try
|
||
{
|
||
if (_metaConditionKind == MetaConditionKind.Expression)
|
||
_ = ExpressionProgram.Compile(_metaConditionTextDraft);
|
||
if (_metaActionKind is MetaActionKind.ExpressionAction
|
||
or MetaActionKind.ChatExpression)
|
||
{
|
||
_ = ExpressionProgram.Compile(_metaActionTextDraft);
|
||
}
|
||
rule = new MetaRule
|
||
{
|
||
State = state,
|
||
Condition = new MetaCondition
|
||
{
|
||
Kind = _metaConditionKind,
|
||
Text = _metaConditionTextDraft,
|
||
Number = _metaNumber,
|
||
SecondaryNumber = _metaSecondaryNumber,
|
||
},
|
||
Action = new MetaAction
|
||
{
|
||
Kind = _metaActionKind,
|
||
Text = _metaActionTextDraft,
|
||
SecondaryText = _metaSecondaryTextDraft,
|
||
Number = _metaNumber,
|
||
SecondaryNumber = _metaSecondaryNumber,
|
||
},
|
||
};
|
||
error = string.Empty;
|
||
return true;
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
rule = new MetaRule();
|
||
error = exception.Message;
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private void RemoveMetaRuleCore()
|
||
{
|
||
if (SelectedMetaRule is not MetaRule selected)
|
||
return;
|
||
_metaProfile.Rules.RemoveAt(_selectedMetaRule);
|
||
_selectedMetaRule = Math.Min(
|
||
_selectedMetaRule,
|
||
Math.Max(0, _metaProfile.Rules.Count - 1));
|
||
SaveMetaProfile();
|
||
RefreshMetaEditor();
|
||
_metaNotice = $"Removed rule from {selected.State}.";
|
||
}
|
||
|
||
private void MoveMetaRule(int direction)
|
||
{
|
||
int destination = _selectedMetaRule + Math.Sign(direction);
|
||
if ((uint)_selectedMetaRule >= (uint)_metaProfile.Rules.Count
|
||
|| (uint)destination >= (uint)_metaProfile.Rules.Count)
|
||
{
|
||
return;
|
||
}
|
||
MetaRule rule = _metaProfile.Rules[_selectedMetaRule];
|
||
_metaProfile.Rules.RemoveAt(_selectedMetaRule);
|
||
_metaProfile.Rules.Insert(destination, rule);
|
||
_selectedMetaRule = destination;
|
||
SaveMetaProfile();
|
||
RefreshMetaEditor();
|
||
_metaNotice = $"Moved {rule.State} rule.";
|
||
}
|
||
|
||
private void SelectMetaProfileCore(string name)
|
||
{
|
||
SaveMetaProfile();
|
||
if (!_metaProfiles.Select(name))
|
||
{
|
||
_metaNotice = $"Meta profile '{name}' is unavailable.";
|
||
return;
|
||
}
|
||
LoadMetaProfile();
|
||
_metaNotice = $"Loaded Meta profile {_metaProfiles.Selected}.";
|
||
}
|
||
|
||
private void CreateMetaProfileCore(bool copyCurrent)
|
||
{
|
||
if (!_metaProfiles.Create(
|
||
_metaProfileNameDraft,
|
||
copyCurrent,
|
||
_metaProfile,
|
||
out string notice))
|
||
{
|
||
_metaNotice = notice;
|
||
return;
|
||
}
|
||
_metaProfileNameDraft = string.Empty;
|
||
LoadMetaProfile();
|
||
_metaNotice = notice;
|
||
}
|
||
|
||
private void ClearMetaProfileCore()
|
||
{
|
||
_metaProfile = _metaProfiles.ClearCurrent();
|
||
_meta.ReplaceProfile(_metaProfile);
|
||
_selectedMetaRule = 0;
|
||
RefreshMetaEditor();
|
||
_metaNotice = $"Cleared Meta profile {_metaProfiles.Selected}.";
|
||
}
|
||
|
||
private void LoadMetaProfile()
|
||
{
|
||
_metaProfile = _metaProfiles.LoadCurrent();
|
||
_meta.ReplaceProfile(_metaProfile);
|
||
if (_initialized)
|
||
ApplyPersistedOptionOverrides();
|
||
_selectedMetaRule = 0;
|
||
RefreshMetaEditor();
|
||
}
|
||
|
||
private void SaveMetaProfile() => _metaProfiles.SaveCurrent(_metaProfile);
|
||
|
||
private static string DescribeMetaCondition(MetaCondition condition) =>
|
||
condition.Kind switch
|
||
{
|
||
MetaConditionKind.Expression => $"Expression: {condition.Text}",
|
||
MetaConditionKind.ChatMessage or MetaConditionKind.ChatMessageCapture =>
|
||
$"{condition.Kind}: {condition.Text}",
|
||
MetaConditionKind.Always or MetaConditionKind.Never =>
|
||
condition.Kind.ToString(),
|
||
_ => $"{condition.Kind} {condition.Number:0.###}",
|
||
};
|
||
|
||
private static string DescribeMetaAction(MetaAction action) => action.Kind switch
|
||
{
|
||
MetaActionKind.SetMetaState or MetaActionKind.CallMetaState
|
||
or MetaActionKind.ChatCommand or MetaActionKind.ExpressionAction
|
||
or MetaActionKind.ChatExpression => $"{action.Kind}: {action.Text}",
|
||
_ => action.Kind.ToString(),
|
||
};
|
||
|
||
private double DistanceFromAnyRoutePoint()
|
||
{
|
||
PluginNavigationSnapshot player = _host.Automation.Navigation.Snapshot;
|
||
if (!player.IsAvailable)
|
||
return double.PositiveInfinity;
|
||
double nearest = double.PositiveInfinity;
|
||
foreach (RouteWaypoint waypoint in _navigationSettings.Waypoints)
|
||
{
|
||
if (waypoint.Position.CellId == 0u)
|
||
continue;
|
||
nearest = Math.Min(
|
||
nearest,
|
||
player.Position.HorizontalDistanceMeters(waypoint.Position));
|
||
}
|
||
return nearest;
|
||
}
|
||
|
||
private void LoadEmbeddedNavigationRoute(string source)
|
||
{
|
||
_navigation.Reset();
|
||
if (!VtankNavRouteSerializer.TryLoad(
|
||
source,
|
||
_navigationSettings,
|
||
_host.Automation.Spells,
|
||
out string error))
|
||
{
|
||
_routeNotice = $"Embedded route rejected: {error}";
|
||
_host.Log.Warn($"MossTank Meta embedded route rejected: {error}");
|
||
return;
|
||
}
|
||
_routeProfiles.SaveCurrent(_navigationSettings);
|
||
_selectedRouteWaypoint = 0;
|
||
RefreshRouteEditor();
|
||
_routeNotice = $"Loaded embedded route ({_navigationSettings.Waypoints.Count} points).";
|
||
}
|
||
|
||
private int CountMonstersByPriority(int priority, double distance)
|
||
{
|
||
int count = 0;
|
||
foreach (PluginCombatTarget target in _host.Automation.Combat
|
||
.CaptureHostileTargets(checked((float)distance)))
|
||
{
|
||
if (_combatSettings.ResolveRule(target).Priority == priority)
|
||
count++;
|
||
}
|
||
return count;
|
||
}
|
||
|
||
private ExpressionValue GetMetaOption(string name)
|
||
{
|
||
string key = name.Trim();
|
||
return key.ToLowerInvariant() switch
|
||
{
|
||
"enablebuffing" => ExpressionValue.Boolean(_buffSettings.Enabled),
|
||
"enablecombat" => ExpressionValue.Boolean(_combatSettings.Enabled),
|
||
"enablenav" or "enablenavigation" or "enableautonavigator" =>
|
||
ExpressionValue.Boolean(_navigationSettings.Enabled),
|
||
"enablelooting" => ExpressionValue.Boolean(_inventorySettings.Loot.Enabled),
|
||
"enablemeta" => ExpressionValue.Boolean(_meta.Enabled),
|
||
"spelldiffexcessthreshold-hunt" => ExpressionValue.Number(
|
||
_combatSettings.HuntSkillExcessOverDifficulty),
|
||
"spelldiffexcessthreshold-buff" => ExpressionValue.Number(
|
||
_buffSettings.SkillExcessOverDifficulty),
|
||
"arrowheadfletchdiffexcessthreshold" => ExpressionValue.Number(
|
||
_inventorySettings.ArrowheadFletchDifficultyExcess),
|
||
"dohelp" => ExpressionValue.Boolean(_vitalSettings.HelpOthers),
|
||
"monsterrange" => ExpressionValue.Number(_combatSettings.MaximumRange),
|
||
"attackdistance" => ExpressionValue.Number(
|
||
_combatSettings.MaximumRange / 240d),
|
||
"attackminimumdistance" => ExpressionValue.Number(
|
||
_combatSettings.MinimumRange / 240d),
|
||
"approachdistance" => ExpressionValue.Number(
|
||
_combatSettings.ApproachDistance / 240d),
|
||
"ringdistance" => ExpressionValue.Number(_combatSettings.RingDistance / 240d),
|
||
"arcrange" => ExpressionValue.Number(_combatSettings.ArcRange / 240d),
|
||
"targetselectanglerange" => ExpressionValue.Number(
|
||
_combatSettings.TargetSelectAngleRange / 240d),
|
||
"corpseapproachrange-max" => ExpressionValue.Number(
|
||
_inventorySettings.Loot.CorpseApproachRange / 240d),
|
||
"corpseapproachrange-min" => ExpressionValue.Number(
|
||
_inventorySettings.Loot.CorpseMinimumApproachRange / 240d),
|
||
"navclosestoprange" => ExpressionValue.Number(
|
||
_navigationSettings.MinimumDistanceMeters / 240d),
|
||
"navfarstoprange" => ExpressionValue.Number(
|
||
_navigationSettings.MaximumDistanceMeters / 240d),
|
||
"useportaldistance" => ExpressionValue.Number(
|
||
_navigationSettings.PortalUseDistanceMeters / 240d),
|
||
"helperdistancehitp" => ExpressionValue.Number(
|
||
_vitalSettings.HelperHealthDistance / 240d),
|
||
"helperdistancestam" => ExpressionValue.Number(
|
||
_vitalSettings.HelperStaminaDistance / 240d),
|
||
"helperdistancemana" => ExpressionValue.Number(
|
||
_vitalSettings.HelperManaDistance / 240d),
|
||
"minimumringtargets" => ExpressionValue.Number(
|
||
_combatSettings.MinimumRingTargets),
|
||
"defaultmeleeattackheight" => ExpressionValue.Number(
|
||
(int)_combatSettings.AttackHeight),
|
||
"defaultmeleeattackpower" or "attackpower" =>
|
||
ExpressionValue.Number(_combatSettings.AttackPower),
|
||
"targetlock" => ExpressionValue.Boolean(_combatSettings.TargetLock),
|
||
"idlepeacemode" => ExpressionValue.Boolean(
|
||
_combatSettings.IdlePeaceMode),
|
||
"stopmacroondeath" => ExpressionValue.Boolean(
|
||
_combatSettings.StopMacroOnDeath),
|
||
"jumpoutwandcasting" => ExpressionValue.Boolean(
|
||
_combatSettings.JumpOutWandCasting),
|
||
"dojiggle" => ExpressionValue.Boolean(_combatSettings.DoJiggle),
|
||
"randomhelperbuffs" => ExpressionValue.Boolean(
|
||
_buffSettings.RandomHelperBuffs),
|
||
"randomhelperintervalseconds" => ExpressionValue.Number(
|
||
_buffSettings.RandomHelperIntervalSeconds),
|
||
"idlebufftopoff" => ExpressionValue.Boolean(
|
||
_buffSettings.IdleBuffTopoff),
|
||
"idlebufftopofftimeseconds" => ExpressionValue.Number(
|
||
_buffSettings.IdleBuffTopoffSeconds),
|
||
"buffprofile-prots" => ExpressionValue.String(
|
||
_buffSettings.ProtectionElements),
|
||
"buffprofile-banes" => ExpressionValue.String(
|
||
_buffSettings.BaneElements),
|
||
"buffprofile_prots" => ExpressionValue.Number(
|
||
_buffSettings.ProtectionProfileMode),
|
||
"buffprofile_banes" => ExpressionValue.Number(
|
||
_buffSettings.BaneProfileMode),
|
||
"targetselectmethod" => ExpressionValue.Number(
|
||
(int)_combatSettings.SelectionMethod + 1),
|
||
"autoattackpower" => ExpressionValue.Boolean(
|
||
_combatSettings.AutoAttackPower),
|
||
"userecklessness" => ExpressionValue.Boolean(
|
||
_combatSettings.UseRecklessness),
|
||
"debuffeachfirst" => ExpressionValue.Number(
|
||
(int)_combatSettings.DebuffEachFirst),
|
||
"debuffselectionmethod" => ExpressionValue.Number(
|
||
(int)_combatSettings.DebuffSelectionMethod),
|
||
"debuffprecastseconds" => ExpressionValue.Number(
|
||
_combatSettings.DebuffPrecastSeconds),
|
||
"switchwandstodebuff" => ExpressionValue.Boolean(
|
||
_combatSettings.SwitchWandsToDebuff),
|
||
"usearcs" => ExpressionValue.Boolean(_combatSettings.UseArcs),
|
||
"deleteghostmonsters" => ExpressionValue.Boolean(
|
||
_combatSettings.DeleteGhostMonsters),
|
||
"ghostmonsterspellattemptcount" => ExpressionValue.Number(
|
||
_combatSettings.GhostMonsterSpellAttemptCount),
|
||
"blacklistmonsterattemptcount" => ExpressionValue.Number(
|
||
_combatSettings.BlacklistMonsterAttemptCount),
|
||
"blacklistmonstertimeoutseconds" => ExpressionValue.Number(
|
||
_combatSettings.BlacklistMonsterTimeoutSeconds),
|
||
"deleteghostmonstersbyhptracker" => ExpressionValue.Boolean(
|
||
_combatSettings.DeleteGhostMonstersByHealthTracker),
|
||
"ghostdeletehptrackerseconds" => ExpressionValue.Number(
|
||
_combatSettings.GhostDeleteHealthTrackerSeconds),
|
||
"summonpets" => ExpressionValue.Boolean(_combatSettings.SummonPets),
|
||
"petrangemode" => ExpressionValue.Number((int)_combatSettings.PetRangeMode),
|
||
"petcustomrange" => ExpressionValue.Number(
|
||
_combatSettings.PetCustomRange / 240d),
|
||
"petmonsterdensity" => ExpressionValue.Number(
|
||
_combatSettings.PetMonsterDensity),
|
||
"petrefillcount-idle" => ExpressionValue.Number(
|
||
_combatSettings.PetRefillCountIdle),
|
||
"petrefillcount-normal" => ExpressionValue.Number(
|
||
_combatSettings.PetRefillCountNormal),
|
||
"openapproachdoors" or "opendoors" =>
|
||
ExpressionValue.Boolean(_navigationSettings.OpenDoors),
|
||
"dooridrange" => ExpressionValue.Number(
|
||
_navigationSettings.DoorIdentifyRangeMeters / 240d),
|
||
"dooropenrange" => ExpressionValue.Number(
|
||
_navigationSettings.DoorOpenRangeMeters / 240d),
|
||
"doorlockpickdiffexcessthreshold" => ExpressionValue.Number(
|
||
_navigationSettings.DoorLockpickExcessThreshold),
|
||
"navpriorityboost" => ExpressionValue.Boolean(
|
||
_navigationSettings.Priority),
|
||
"followaroundcorners" => ExpressionValue.Boolean(
|
||
_navigationSettings.FollowAroundCorners),
|
||
"autofellowmanagement" => ExpressionValue.Boolean(
|
||
_combatSettings.AutoFellowManagement),
|
||
"enablestack" or "enableautostack" =>
|
||
ExpressionValue.Boolean(_inventorySettings.AutoStack),
|
||
"autostack" => ExpressionValue.Boolean(_inventorySettings.AutoStack),
|
||
"enablecram" or "enableautocram" =>
|
||
ExpressionValue.Boolean(_inventorySettings.AutoCram),
|
||
"autocram" => ExpressionValue.Boolean(_inventorySettings.AutoCram),
|
||
"autocraftitems" => ExpressionValue.Boolean(_inventorySettings.AutoCraftItems),
|
||
"splitpeas" => ExpressionValue.Boolean(_inventorySettings.SplitPeas),
|
||
"spellcompmin-critical" => ExpressionValue.Number(
|
||
_inventorySettings.CriticalComponentMinimum),
|
||
"spellcompmin-normal" => ExpressionValue.Number(
|
||
_inventorySettings.NormalComponentMinimum),
|
||
"spellcompmin-idle" => ExpressionValue.Number(
|
||
_inventorySettings.IdleComponentMinimum),
|
||
"idlecraftcount_healthkits" or "idlecraftcount-healthkits" =>
|
||
ExpressionValue.Number(
|
||
_inventorySettings.IdleHealthKitCount),
|
||
"idlecraftcount_stamkits" or "idlecraftcount-stamkits" =>
|
||
ExpressionValue.Number(
|
||
_inventorySettings.IdleStaminaKitCount),
|
||
"idlecraftcount_manakits" or "idlecraftcount-manakits" =>
|
||
ExpressionValue.Number(
|
||
_inventorySettings.IdleManaKitCount),
|
||
"idlecraftcount_healthfood" or "idlecraftcount-healthfood" =>
|
||
ExpressionValue.Number(
|
||
_inventorySettings.IdleHealthFoodCount),
|
||
"idlecraftcount_stamfood" or "idlecraftcount-stamfood" =>
|
||
ExpressionValue.Number(
|
||
_inventorySettings.IdleStaminaFoodCount),
|
||
"idlecraftcount_manafood" or "idlecraftcount-manafood" =>
|
||
ExpressionValue.Number(
|
||
_inventorySettings.IdleManaFoodCount),
|
||
"refillwornmana" => ExpressionValue.Boolean(
|
||
_inventorySettings.RefillWornMana),
|
||
"manachargeswhenoff" => ExpressionValue.Boolean(
|
||
_inventorySettings.ManaChargesWhenOff),
|
||
"refillwornmana-item-manapercent" => ExpressionValue.Number(
|
||
_inventorySettings.RefillWornManaPercent),
|
||
"readunknownscrolls" => ExpressionValue.Boolean(
|
||
_inventorySettings.Loot.ReadUnknownScrolls),
|
||
"lootallcorpses" => ExpressionValue.Boolean(
|
||
_inventorySettings.Loot.LootAllCorpses),
|
||
"lootfellowcorpses" => ExpressionValue.Boolean(
|
||
_inventorySettings.Loot.LootFellowCorpses),
|
||
"lootpriorityboost" => ExpressionValue.Boolean(
|
||
_inventorySettings.Loot.PriorityBoost),
|
||
"lootonlyrarecorpses" => ExpressionValue.Boolean(
|
||
_inventorySettings.Loot.LootOnlyRareCorpses),
|
||
"combinesalvage" => ExpressionValue.Boolean(
|
||
_inventorySettings.Loot.CombineSalvage),
|
||
"manastonelootcount" => ExpressionValue.Number(
|
||
_inventorySettings.Loot.ManaStoneLootCount),
|
||
"manatankminimummana" => ExpressionValue.Number(
|
||
_inventorySettings.Loot.ManaTankMinimumMana),
|
||
"corpsecachetimeoutminutes" => ExpressionValue.Number(
|
||
_inventorySettings.Loot.CorpseCacheTimeoutMinutes),
|
||
"corpseitemappearancetimeoutseconds" => ExpressionValue.Number(
|
||
_inventorySettings.Loot.CorpseItemAppearanceTimeoutSeconds),
|
||
"corpseitemidtimeoutseconds" => ExpressionValue.Number(
|
||
_inventorySettings.Loot.CorpseItemIdentifyTimeoutSeconds),
|
||
"corpseopentimeoutseconds" => ExpressionValue.Number(
|
||
_inventorySettings.Loot.CorpseOpenTimeoutSeconds),
|
||
"blacklistcorpseopenattemptcount" => ExpressionValue.Number(
|
||
_inventorySettings.Loot.BlacklistCorpseOpenAttemptCount),
|
||
"blacklistcorpseopentimeoutseconds" => ExpressionValue.Number(
|
||
_inventorySettings.Loot.BlacklistCorpseOpenTimeoutSeconds),
|
||
"corpselootitemmaxattempts" => ExpressionValue.Number(
|
||
_inventorySettings.Loot.CorpseLootItemMaxAttempts),
|
||
"minimumhealkitsuccesschance" => ExpressionValue.Number(
|
||
_vitalSettings.MinimumHealKitSuccessChance),
|
||
"usehealersheart" => ExpressionValue.Boolean(
|
||
_vitalSettings.UseHealersHeart),
|
||
"rechargeboosttimeseconds" => ExpressionValue.Number(
|
||
_vitalSettings.RechargeBoostTimeSeconds),
|
||
"rechargeboostamount" => ExpressionValue.Number(
|
||
_vitalSettings.RechargeBoostAmount),
|
||
"clearlevelboostflagoncast" => ExpressionValue.Boolean(
|
||
_vitalSettings.ClearLevelBoostFlagOnCast),
|
||
"whoyougonnacall" => ExpressionValue.Boolean(
|
||
_combatSettings.WhoYouGonnaCall),
|
||
"castdispelself" => ExpressionValue.Boolean(
|
||
_vitalSettings.CastDispelSelf),
|
||
"usedispelitems" => ExpressionValue.Boolean(
|
||
_vitalSettings.UseDispelItems),
|
||
"usedispeldrum" => ExpressionValue.Boolean(
|
||
_vitalSettings.UseDispelDrum),
|
||
"usekitsinmagicmode" => ExpressionValue.Boolean(
|
||
_vitalSettings.UseKitsInMagicMode),
|
||
"gotopeacemodetousekits" => ExpressionValue.Boolean(
|
||
_vitalSettings.GoToPeaceModeToUseKits),
|
||
"staminatohealthmultiplier" => ExpressionValue.Number(
|
||
_vitalSettings.StaminaToHealthMultiplier),
|
||
"manatohealthmultiplier" => ExpressionValue.Number(
|
||
_vitalSettings.ManaToHealthMultiplier),
|
||
"recharge-norm-hitp" => ExpressionValue.Number(
|
||
_vitalSettings.NormalHealth * 100d),
|
||
"recharge-norm-stam" => ExpressionValue.Number(
|
||
_vitalSettings.NormalStamina * 100d),
|
||
"recharge-norm-mana" => ExpressionValue.Number(
|
||
_vitalSettings.NormalMana * 100d),
|
||
"recharge-notarg-hitp" => ExpressionValue.Number(
|
||
_vitalSettings.NoTargetHealth * 100d),
|
||
"recharge-notarg-stam" => ExpressionValue.Number(
|
||
_vitalSettings.NoTargetStamina * 100d),
|
||
"recharge-notarg-mana" => ExpressionValue.Number(
|
||
_vitalSettings.NoTargetMana * 100d),
|
||
"recharge-helper-hitp" => ExpressionValue.Number(
|
||
_vitalSettings.HelperHealth * 100d),
|
||
"recharge-helper-stam" => ExpressionValue.Number(
|
||
_vitalSettings.HelperStamina * 100d),
|
||
"recharge-helper-mana" => ExpressionValue.Number(
|
||
_vitalSettings.HelperMana * 100d),
|
||
"rebufftimeremainingseconds" => ExpressionValue.Number(
|
||
_buffSettings.RebuffWhenUnderSeconds),
|
||
"buffcastrecast_seconds" => ExpressionValue.Number(
|
||
_buffSettings.BuffCastRecastSeconds),
|
||
"buffcastrecastreset_seconds" => ExpressionValue.Number(
|
||
_buffSettings.BuffCastRecastResetSeconds),
|
||
"blacklistedspellcomps" => ExpressionValue.String(
|
||
_buffSettings.BlacklistedSpellComponents),
|
||
"droptopeacemoderetrycount" => ExpressionValue.Number(
|
||
_vitalSettings.DropToPeaceModeRetryCount),
|
||
"fastcastbuffs" => ExpressionValue.Boolean(
|
||
_buffSettings.FastCastBuffs),
|
||
"usebreakableturnto" => ExpressionValue.Boolean(
|
||
_combatSettings.UseBreakableTurnTo),
|
||
"useprojectileawareness" => ExpressionValue.Boolean(
|
||
_combatSettings.UseProjectileAwareness),
|
||
"collisionprojectileradius" => ExpressionValue.Number(
|
||
_combatSettings.CollisionProjectileRadius),
|
||
"collisionstepdistance" => ExpressionValue.Number(
|
||
_combatSettings.CollisionStepDistance),
|
||
"showcollisiondebug" => ExpressionValue.Boolean(
|
||
_combatSettings.ShowCollisionDebug),
|
||
"maximumcollisioncheckspertick" => ExpressionValue.Number(
|
||
_combatSettings.MaximumCollisionChecksPerTick),
|
||
"usespecialammo" => ExpressionValue.Number(
|
||
_combatSettings.UseSpecialAmmo),
|
||
"spellrangefudge" => ExpressionValue.Number(
|
||
_combatSettings.SpellRangeFudge),
|
||
"buffwithuntrained-item" => ExpressionValue.Number(
|
||
_buffSettings.BuffWithUntrainedItemSkill),
|
||
"buffwithuntrained-creature" => ExpressionValue.Number(
|
||
_buffSettings.BuffWithUntrainedCreatureSkill),
|
||
"buffwithuntrained-life" => ExpressionValue.Number(
|
||
_buffSettings.BuffWithUntrainedLifeSkill),
|
||
"allowdebufffallback" => ExpressionValue.Boolean(
|
||
_combatSettings.AllowDebuffFallback),
|
||
"rechargehandlerset" => ExpressionValue.String(
|
||
_vitalSettings.RechargeHandlerSet),
|
||
_ => _combatSettings.DynamicSettings.TryGetValue(key, out MonsterValue value)
|
||
? ToExpressionValue(value)
|
||
: ToExpressionValue(VtankOptionCatalog.Default(key)),
|
||
};
|
||
}
|
||
|
||
private static ExpressionValue ToExpressionValue(MonsterValue value) =>
|
||
value.Kind switch
|
||
{
|
||
MonsterValueKind.Number => ExpressionValue.Number(value.Number),
|
||
MonsterValueKind.Boolean => ExpressionValue.Boolean(value.Boolean),
|
||
_ => ExpressionValue.String(value.Text),
|
||
};
|
||
|
||
private double GetDynamicNumber(string name, double fallback) =>
|
||
_combatSettings.DynamicSettings.TryGetValue(name, out MonsterValue value)
|
||
&& value.Kind == MonsterValueKind.Number
|
||
? value.Number
|
||
: fallback;
|
||
|
||
private void RegisterVtankExpressionFunctions()
|
||
{
|
||
ExpressionFunctionRegistry functions = _expressions.Registry;
|
||
functions.Register("vtsetmetastate", 1, 1, (_, args) =>
|
||
{
|
||
_meta.Transition(args[0].AsString("vtsetmetastate"));
|
||
_combatSettings.MetaState = _meta.CurrentState;
|
||
return ExpressionValue.One;
|
||
}, "vtsetmetastate[state]");
|
||
functions.Register("vtgetmetastate", 0, 0, (_, _) =>
|
||
ExpressionValue.String(_meta.CurrentState), "vtgetmetastate[]");
|
||
functions.Register("vtgetmeta", 0, 0, (_, _) =>
|
||
ExpressionValue.String(_metaProfiles.Selected), "vtgetmeta[]");
|
||
functions.Register("vtsetsetting", 2, 2, (_, args) =>
|
||
{
|
||
string name = args[0].AsString("vtsetsetting");
|
||
ExpressionValue value = args[1];
|
||
if (value.Kind == ExpressionValueKind.String
|
||
&& double.TryParse(
|
||
value.AsString(),
|
||
NumberStyles.Float,
|
||
CultureInfo.InvariantCulture,
|
||
out double number))
|
||
{
|
||
value = ExpressionValue.Number(number);
|
||
}
|
||
return ExpressionValue.Boolean(SetMetaOption(name, value));
|
||
}, "vtsetsetting[setting,value]");
|
||
functions.Register("vtgetsetting", 1, 1, (_, args) =>
|
||
ExpressionValue.String(GetMetaOption(
|
||
args[0].AsString("vtgetsetting")).ToDisplayString()),
|
||
"vtgetsetting[setting]");
|
||
functions.Register("uboptset", 2, 2, (_, args) =>
|
||
ExpressionValue.Boolean(SetMetaOption(
|
||
args[0].AsString("uboptset"),
|
||
args[1])),
|
||
"uboptset[setting,value]");
|
||
functions.Register("uboptget", 1, 1, (_, args) =>
|
||
GetMetaOption(args[0].AsString("uboptget")),
|
||
"uboptget[setting]");
|
||
functions.Register("actiontrygiveprofile", 2, 2, (_, args) =>
|
||
ExpressionValue.Boolean(_profileGive.TryStart(
|
||
args[0].AsString("actiontrygiveprofile"),
|
||
args[1].AsString("actiontrygiveprofile"))),
|
||
"actiontrygiveprofile[lootprofile,target]");
|
||
functions.Register("vtmacroenabled", 0, 0, (_, _) =>
|
||
ExpressionValue.Boolean(_combat.Enabled),
|
||
"vtmacroenabled[]");
|
||
}
|
||
|
||
private bool SetMetaOption(string name, ExpressionValue value)
|
||
{
|
||
string canonical = VtankOptionCatalog.IsKnown(name)
|
||
? VtankOptionCatalog.Canonical(name)
|
||
: name.Trim();
|
||
string key = canonical.ToLowerInvariant();
|
||
switch (key)
|
||
{
|
||
case "enablebuffing":
|
||
_buffSettings.Enabled = value.IsTruthy;
|
||
break;
|
||
case "enablecombat":
|
||
_combatSettings.Enabled = value.IsTruthy;
|
||
break;
|
||
case "enablenav":
|
||
case "enablenavigation":
|
||
case "enableautonavigator":
|
||
_navigationSettings.Enabled = value.IsTruthy;
|
||
break;
|
||
case "enablelooting":
|
||
_inventorySettings.Loot.Enabled = value.IsTruthy;
|
||
break;
|
||
case "enablemeta":
|
||
_meta.SetEnabled(value.IsTruthy);
|
||
break;
|
||
case "spelldiffexcessthreshold-hunt":
|
||
_combatSettings.HuntSkillExcessOverDifficulty = Math.Clamp(
|
||
value.AsInt32("SpellDiffExcessThreshold-Hunt"), -100, 500);
|
||
break;
|
||
case "arrowheadfletchdiffexcessthreshold":
|
||
_inventorySettings.ArrowheadFletchDifficultyExcess = Math.Clamp(
|
||
value.AsInt32("ArrowheadFletchDiffExcessThreshold"),
|
||
-100,
|
||
500);
|
||
break;
|
||
case "dohelp":
|
||
_vitalSettings.HelpOthers = value.IsTruthy;
|
||
break;
|
||
case "monsterrange":
|
||
_combatSettings.MaximumRange = Math.Clamp(
|
||
checked((float)value.AsNumber("MonsterRange")), 1f, 100f);
|
||
break;
|
||
case "attackdistance":
|
||
_combatSettings.MaximumRange = Math.Clamp(
|
||
checked((float)(value.AsNumber("AttackDistance") * 240d)),
|
||
1f,
|
||
100f);
|
||
break;
|
||
case "attackminimumdistance":
|
||
_combatSettings.MinimumRange = Math.Clamp(
|
||
checked((float)(value.AsNumber("AttackMinimumDistance") * 240d)),
|
||
0f,
|
||
100f);
|
||
break;
|
||
case "approachdistance":
|
||
_combatSettings.ApproachDistance = Math.Clamp(
|
||
checked((float)(value.AsNumber("ApproachDistance") * 240d)),
|
||
0f,
|
||
100f);
|
||
break;
|
||
case "ringdistance":
|
||
_combatSettings.RingDistance = Math.Clamp(
|
||
checked((float)(value.AsNumber("RingDistance") * 240d)),
|
||
1f,
|
||
100f);
|
||
break;
|
||
case "arcrange":
|
||
_combatSettings.ArcRange = Math.Clamp(
|
||
checked((float)(value.AsNumber("ArcRange") * 240d)),
|
||
1f,
|
||
100f);
|
||
break;
|
||
case "targetselectanglerange":
|
||
_combatSettings.TargetSelectAngleRange = Math.Clamp(
|
||
checked((float)(value.AsNumber("TargetSelectAngleRange") * 240d)),
|
||
1f,
|
||
100f);
|
||
break;
|
||
case "corpseapproachrange-max":
|
||
_inventorySettings.Loot.CorpseApproachRange = Math.Clamp(
|
||
checked((float)(value.AsNumber("CorpseApproachRange-Max") * 240d)),
|
||
1f,
|
||
100f);
|
||
break;
|
||
case "corpseapproachrange-min":
|
||
_inventorySettings.Loot.CorpseMinimumApproachRange = Math.Clamp(
|
||
checked((float)(value.AsNumber("CorpseApproachRange-Min") * 240d)),
|
||
0f,
|
||
100f);
|
||
break;
|
||
case "navclosestoprange":
|
||
_navigationSettings.MinimumDistanceMeters = Math.Clamp(
|
||
value.AsNumber("NavCloseStopRange") * 240d,
|
||
0.5d,
|
||
50d);
|
||
break;
|
||
case "navfarstoprange":
|
||
_navigationSettings.MaximumDistanceMeters = Math.Clamp(
|
||
value.AsNumber("NavFarStopRange") * 240d,
|
||
_navigationSettings.MinimumDistanceMeters,
|
||
240_000_000d);
|
||
break;
|
||
case "useportaldistance":
|
||
_navigationSettings.PortalUseDistanceMeters = Math.Clamp(
|
||
value.AsNumber("UsePortalDistance") * 240d,
|
||
0.5d,
|
||
50d);
|
||
break;
|
||
case "helperdistancehitp":
|
||
_vitalSettings.HelperHealthDistance = Math.Clamp(
|
||
checked((float)(value.AsNumber("HelperDistanceHitP") * 240d)),
|
||
1f,
|
||
100f);
|
||
break;
|
||
case "helperdistancestam":
|
||
_vitalSettings.HelperStaminaDistance = Math.Clamp(
|
||
checked((float)(value.AsNumber("HelperDistanceStam") * 240d)),
|
||
1f,
|
||
100f);
|
||
break;
|
||
case "helperdistancemana":
|
||
_vitalSettings.HelperManaDistance = Math.Clamp(
|
||
checked((float)(value.AsNumber("HelperDistanceMana") * 240d)),
|
||
1f,
|
||
100f);
|
||
break;
|
||
case "minimumringtargets":
|
||
_combatSettings.MinimumRingTargets = Math.Clamp(
|
||
value.AsInt32("MinimumRingTargets"), 1, 25);
|
||
break;
|
||
case "defaultmeleeattackheight":
|
||
_combatSettings.AttackHeight = (PluginAttackHeight)Math.Clamp(
|
||
value.AsInt32("DefaultMeleeAttackHeight"), 1, 3);
|
||
break;
|
||
case "defaultmeleeattackpower":
|
||
case "attackpower":
|
||
_combatSettings.AttackPower = Math.Clamp(
|
||
checked((float)value.AsNumber("AttackPower")), 0f, 1f);
|
||
break;
|
||
case "targetlock":
|
||
_combatSettings.TargetLock = value.IsTruthy;
|
||
break;
|
||
case "idlepeacemode":
|
||
_combatSettings.IdlePeaceMode = value.IsTruthy;
|
||
break;
|
||
case "stopmacroondeath":
|
||
_combatSettings.StopMacroOnDeath = value.IsTruthy;
|
||
break;
|
||
case "jumpoutwandcasting":
|
||
_combatSettings.JumpOutWandCasting = value.IsTruthy;
|
||
break;
|
||
case "dojiggle":
|
||
_combatSettings.DoJiggle = value.IsTruthy;
|
||
break;
|
||
case "randomhelperbuffs":
|
||
_buffSettings.RandomHelperBuffs = value.IsTruthy;
|
||
break;
|
||
case "randomhelperintervalseconds":
|
||
_buffSettings.RandomHelperIntervalSeconds = Math.Clamp(
|
||
value.AsNumber("RandomHelperIntervalSeconds"), 0.25d, 3600d);
|
||
break;
|
||
case "idlebufftopoff":
|
||
_buffSettings.IdleBuffTopoff = value.IsTruthy;
|
||
break;
|
||
case "idlebufftopofftimeseconds":
|
||
_buffSettings.IdleBuffTopoffSeconds = Math.Clamp(
|
||
value.AsNumber("IdleBuffTopoffTimeSeconds"),
|
||
30d,
|
||
7200d);
|
||
break;
|
||
case "buffprofile-prots":
|
||
_buffSettings.ProtectionElements = NormalizeElementProfile(
|
||
value.ToDisplayString());
|
||
break;
|
||
case "buffprofile-banes":
|
||
_buffSettings.BaneElements = NormalizeElementProfile(
|
||
value.ToDisplayString());
|
||
break;
|
||
case "buffprofile_prots":
|
||
_buffSettings.ProtectionProfileMode = Math.Clamp(
|
||
value.AsInt32("BuffProfile_Prots"), 1, 8);
|
||
break;
|
||
case "buffprofile_banes":
|
||
_buffSettings.BaneProfileMode = Math.Clamp(
|
||
value.AsInt32("BuffProfile_Banes"), 1, 8);
|
||
break;
|
||
case "targetselectmethod":
|
||
_combatSettings.SelectionMethod = (TargetSelectionMethod)Math.Clamp(
|
||
value.AsInt32("TargetSelectMethod") - 1, 0, 2);
|
||
break;
|
||
case "autoattackpower":
|
||
_combatSettings.AutoAttackPower = value.IsTruthy;
|
||
break;
|
||
case "userecklessness":
|
||
_combatSettings.UseRecklessness = value.IsTruthy;
|
||
break;
|
||
case "debuffeachfirst":
|
||
_combatSettings.DebuffEachFirst = (DebuffEachFirst)Math.Clamp(
|
||
value.AsInt32("DebuffEachFirst"), 1, 3);
|
||
break;
|
||
case "debuffselectionmethod":
|
||
_combatSettings.DebuffSelectionMethod =
|
||
(DebuffSelectionMethod)Math.Clamp(
|
||
value.AsInt32("DebuffSelectionMethod"), 1, 2);
|
||
break;
|
||
case "debuffprecastseconds":
|
||
_combatSettings.DebuffPrecastSeconds = Math.Clamp(
|
||
value.AsNumber("DebuffPrecastSeconds"), 0d, 60d);
|
||
break;
|
||
case "switchwandstodebuff":
|
||
_combatSettings.SwitchWandsToDebuff = value.IsTruthy;
|
||
break;
|
||
case "usearcs":
|
||
_combatSettings.UseArcs = value.IsTruthy;
|
||
break;
|
||
case "deleteghostmonsters":
|
||
_combatSettings.DeleteGhostMonsters = value.IsTruthy;
|
||
break;
|
||
case "ghostmonsterspellattemptcount":
|
||
_combatSettings.GhostMonsterSpellAttemptCount = Math.Clamp(
|
||
value.AsInt32("GhostMonsterSpellAttemptCount"), 1, 1000);
|
||
break;
|
||
case "blacklistmonsterattemptcount":
|
||
_combatSettings.BlacklistMonsterAttemptCount = Math.Clamp(
|
||
value.AsInt32("BlacklistMonsterAttemptCount"), 1, 20);
|
||
break;
|
||
case "blacklistmonstertimeoutseconds":
|
||
_combatSettings.BlacklistMonsterTimeoutSeconds = Math.Clamp(
|
||
value.AsNumber("BlacklistMonsterTimeoutSeconds"), 1d, 3600d);
|
||
break;
|
||
case "deleteghostmonstersbyhptracker":
|
||
_combatSettings.DeleteGhostMonstersByHealthTracker = value.IsTruthy;
|
||
break;
|
||
case "ghostdeletehptrackerseconds":
|
||
_combatSettings.GhostDeleteHealthTrackerSeconds = Math.Clamp(
|
||
value.AsNumber("GhostDeleteHPTrackerSeconds"), 1d, 300d);
|
||
break;
|
||
case "summonpets":
|
||
_combatSettings.SummonPets = value.IsTruthy;
|
||
break;
|
||
case "petrangemode":
|
||
_combatSettings.PetRangeMode = (PetRangeMode)Math.Clamp(
|
||
value.AsInt32("PetRangeMode"), 0, 1);
|
||
break;
|
||
case "petcustomrange":
|
||
_combatSettings.PetCustomRange = Math.Clamp(
|
||
checked((float)(value.AsNumber("PetCustomRange") * 240d)),
|
||
1f,
|
||
100f);
|
||
break;
|
||
case "petmonsterdensity":
|
||
_combatSettings.PetMonsterDensity = Math.Clamp(
|
||
value.AsInt32("PetMonsterDensity"), 1, 25);
|
||
break;
|
||
case "petrefillcount-idle":
|
||
_combatSettings.PetRefillCountIdle = Math.Clamp(
|
||
value.AsInt32("PetRefillCount-Idle"), 0, 3);
|
||
break;
|
||
case "petrefillcount-normal":
|
||
_combatSettings.PetRefillCountNormal = Math.Clamp(
|
||
value.AsInt32("PetRefillCount-Normal"), 0, 3);
|
||
break;
|
||
case "openapproachdoors":
|
||
case "opendoors":
|
||
_navigationSettings.OpenDoors = value.IsTruthy;
|
||
break;
|
||
case "dooridrange":
|
||
_navigationSettings.DoorIdentifyRangeMeters = Math.Clamp(
|
||
value.AsNumber("DoorIDRange") * 240d, 1d, 100d);
|
||
break;
|
||
case "dooropenrange":
|
||
_navigationSettings.DoorOpenRangeMeters = Math.Clamp(
|
||
value.AsNumber("DoorOpenRange") * 240d,
|
||
0.5d,
|
||
_navigationSettings.DoorIdentifyRangeMeters);
|
||
break;
|
||
case "doorlockpickdiffexcessthreshold":
|
||
_navigationSettings.DoorLockpickExcessThreshold = Math.Clamp(
|
||
value.AsInt32("DoorLockpickDiffExcessThreshold"), -500, 500);
|
||
break;
|
||
case "navpriorityboost":
|
||
_navigationSettings.Priority = value.IsTruthy;
|
||
break;
|
||
case "followaroundcorners":
|
||
_navigationSettings.FollowAroundCorners = value.IsTruthy;
|
||
break;
|
||
case "autofellowmanagement":
|
||
_combatSettings.AutoFellowManagement = value.IsTruthy;
|
||
break;
|
||
case "enablestack":
|
||
case "enableautostack":
|
||
case "autostack":
|
||
_inventorySettings.AutoStack = value.IsTruthy;
|
||
break;
|
||
case "enablecram":
|
||
case "enableautocram":
|
||
case "autocram":
|
||
_inventorySettings.AutoCram = value.IsTruthy;
|
||
break;
|
||
case "autocraftitems":
|
||
_inventorySettings.AutoCraftItems = value.IsTruthy;
|
||
break;
|
||
case "splitpeas":
|
||
_inventorySettings.SplitPeas = value.IsTruthy;
|
||
break;
|
||
case "spellcompmin-critical":
|
||
_inventorySettings.CriticalComponentMinimum = Math.Clamp(
|
||
value.AsInt32("SpellCompMin-Critical"), 0, 1000);
|
||
break;
|
||
case "spellcompmin-normal":
|
||
_inventorySettings.NormalComponentMinimum = Math.Clamp(
|
||
value.AsInt32("SpellCompMin-Normal"), 0, 1000);
|
||
break;
|
||
case "spellcompmin-idle":
|
||
_inventorySettings.IdleComponentMinimum = Math.Clamp(
|
||
value.AsInt32("SpellCompMin-Idle"), 0, 1000);
|
||
break;
|
||
case "idlecraftcount_healthkits":
|
||
case "idlecraftcount-healthkits":
|
||
_inventorySettings.IdleHealthKitCount = Math.Clamp(
|
||
value.AsInt32("IdleCraftCount_HealthKits"), 0, 1000);
|
||
break;
|
||
case "idlecraftcount_stamkits":
|
||
case "idlecraftcount-stamkits":
|
||
_inventorySettings.IdleStaminaKitCount = Math.Clamp(
|
||
value.AsInt32("IdleCraftCount_StamKits"), 0, 1000);
|
||
break;
|
||
case "idlecraftcount_manakits":
|
||
case "idlecraftcount-manakits":
|
||
_inventorySettings.IdleManaKitCount = Math.Clamp(
|
||
value.AsInt32("IdleCraftCount_ManaKits"), 0, 1000);
|
||
break;
|
||
case "idlecraftcount_healthfood":
|
||
case "idlecraftcount-healthfood":
|
||
_inventorySettings.IdleHealthFoodCount = Math.Clamp(
|
||
value.AsInt32("IdleCraftCount_HealthFood"), 0, 1000);
|
||
break;
|
||
case "idlecraftcount_stamfood":
|
||
case "idlecraftcount-stamfood":
|
||
_inventorySettings.IdleStaminaFoodCount = Math.Clamp(
|
||
value.AsInt32("IdleCraftCount_StamFood"), 0, 1000);
|
||
break;
|
||
case "idlecraftcount_manafood":
|
||
case "idlecraftcount-manafood":
|
||
_inventorySettings.IdleManaFoodCount = Math.Clamp(
|
||
value.AsInt32("IdleCraftCount_ManaFood"), 0, 1000);
|
||
break;
|
||
case "refillwornmana":
|
||
_inventorySettings.RefillWornMana = value.IsTruthy;
|
||
break;
|
||
case "manachargeswhenoff":
|
||
_inventorySettings.ManaChargesWhenOff = value.IsTruthy;
|
||
break;
|
||
case "refillwornmana-item-manapercent":
|
||
_inventorySettings.RefillWornManaPercent = Math.Clamp(
|
||
value.AsInt32("RefillWornMana-Item-ManaPercent"), 0, 100);
|
||
break;
|
||
case "readunknownscrolls":
|
||
_inventorySettings.Loot.ReadUnknownScrolls = value.IsTruthy;
|
||
break;
|
||
case "lootallcorpses":
|
||
_inventorySettings.Loot.LootAllCorpses = value.IsTruthy;
|
||
break;
|
||
case "lootfellowcorpses":
|
||
_inventorySettings.Loot.LootFellowCorpses = value.IsTruthy;
|
||
break;
|
||
case "lootpriorityboost":
|
||
_inventorySettings.Loot.PriorityBoost = value.IsTruthy;
|
||
break;
|
||
case "lootonlyrarecorpses":
|
||
_inventorySettings.Loot.LootOnlyRareCorpses = value.IsTruthy;
|
||
break;
|
||
case "combinesalvage":
|
||
_inventorySettings.Loot.CombineSalvage = value.IsTruthy;
|
||
break;
|
||
case "manastonelootcount":
|
||
_inventorySettings.Loot.ManaStoneLootCount = Math.Clamp(
|
||
value.AsInt32("ManaStoneLootCount"), 0, 1000);
|
||
break;
|
||
case "manatankminimummana":
|
||
_inventorySettings.Loot.ManaTankMinimumMana = Math.Clamp(
|
||
value.AsInt32("ManaTankMinimumMana"), 1, int.MaxValue);
|
||
break;
|
||
case "corpsecachetimeoutminutes":
|
||
_inventorySettings.Loot.CorpseCacheTimeoutMinutes = Math.Clamp(
|
||
value.AsNumber("CorpseCacheTimeoutMinutes"), 1d, 1440d);
|
||
break;
|
||
case "corpseitemappearancetimeoutseconds":
|
||
_inventorySettings.Loot.CorpseItemAppearanceTimeoutSeconds =
|
||
Math.Clamp(
|
||
value.AsNumber("CorpseItemAppearanceTimeoutSeconds"),
|
||
0d,
|
||
300d);
|
||
break;
|
||
case "corpseitemidtimeoutseconds":
|
||
_inventorySettings.Loot.CorpseItemIdentifyTimeoutSeconds =
|
||
Math.Clamp(
|
||
value.AsNumber("CorpseItemIDTimeoutSeconds"),
|
||
1d,
|
||
600d);
|
||
break;
|
||
case "corpseopentimeoutseconds":
|
||
_inventorySettings.Loot.CorpseOpenTimeoutSeconds = Math.Clamp(
|
||
value.AsNumber("CorpseOpenTimeoutSeconds"), 0.1d, 60d);
|
||
break;
|
||
case "blacklistcorpseopenattemptcount":
|
||
_inventorySettings.Loot.BlacklistCorpseOpenAttemptCount = Math.Clamp(
|
||
value.AsInt32("BlacklistCorpseOpenAttemptCount"), 1, 1000);
|
||
break;
|
||
case "blacklistcorpseopentimeoutseconds":
|
||
_inventorySettings.Loot.BlacklistCorpseOpenTimeoutSeconds = Math.Clamp(
|
||
value.AsNumber("BlacklistCorpseOpenTimeoutSeconds"), 1d, 3600d);
|
||
break;
|
||
case "corpselootitemmaxattempts":
|
||
_inventorySettings.Loot.CorpseLootItemMaxAttempts = Math.Clamp(
|
||
value.AsInt32("CorpseLootItemMaxAttempts"), 1, 1000);
|
||
break;
|
||
case "minimumhealkitsuccesschance":
|
||
_vitalSettings.MinimumHealKitSuccessChance = Math.Clamp(
|
||
value.AsInt32("MinimumHealKitSuccessChance"), 0, 100);
|
||
break;
|
||
case "usehealersheart":
|
||
_vitalSettings.UseHealersHeart = value.IsTruthy;
|
||
_vitalRecharge.Reset();
|
||
break;
|
||
case "rechargeboosttimeseconds":
|
||
_vitalSettings.RechargeBoostTimeSeconds = Math.Clamp(
|
||
value.AsNumber("RechargeBoostTimeSeconds"), 0d, 300d);
|
||
break;
|
||
case "rechargeboostamount":
|
||
_vitalSettings.RechargeBoostAmount = Math.Clamp(
|
||
value.AsInt32("RechargeBoostAmount"), 0, 1000);
|
||
break;
|
||
case "clearlevelboostflagoncast":
|
||
_vitalSettings.ClearLevelBoostFlagOnCast = value.IsTruthy;
|
||
break;
|
||
case "whoyougonnacall":
|
||
_combatSettings.WhoYouGonnaCall = value.IsTruthy;
|
||
break;
|
||
case "castdispelself":
|
||
_vitalSettings.CastDispelSelf = value.IsTruthy;
|
||
_dispel.Reset();
|
||
break;
|
||
case "usedispelitems":
|
||
_vitalSettings.UseDispelItems = value.IsTruthy;
|
||
_dispel.Reset();
|
||
break;
|
||
case "usedispeldrum":
|
||
_vitalSettings.UseDispelDrum = value.IsTruthy;
|
||
_dispel.Reset();
|
||
break;
|
||
case "usekitsinmagicmode":
|
||
_vitalSettings.UseKitsInMagicMode = value.IsTruthy;
|
||
break;
|
||
case "gotopeacemodetousekits":
|
||
_vitalSettings.GoToPeaceModeToUseKits = value.IsTruthy;
|
||
break;
|
||
case "staminatohealthmultiplier":
|
||
_vitalSettings.StaminaToHealthMultiplier = Math.Clamp(
|
||
value.AsNumber("StaminaToHealthMultiplier"), 0d, 10d);
|
||
break;
|
||
case "manatohealthmultiplier":
|
||
_vitalSettings.ManaToHealthMultiplier = Math.Clamp(
|
||
value.AsNumber("ManaToHealthMultiplier"), 0d, 10d);
|
||
break;
|
||
case "recharge-norm-hitp":
|
||
_vitalSettings.NormalHealth = Math.Clamp(
|
||
value.AsNumber("Recharge-Norm-HitP") / 100d, 0d, 1d);
|
||
break;
|
||
case "recharge-norm-stam":
|
||
_vitalSettings.NormalStamina = Math.Clamp(
|
||
value.AsNumber("Recharge-Norm-Stam") / 100d, 0d, 1d);
|
||
break;
|
||
case "recharge-norm-mana":
|
||
_vitalSettings.NormalMana = Math.Clamp(
|
||
value.AsNumber("Recharge-Norm-Mana") / 100d, 0d, 1d);
|
||
break;
|
||
case "recharge-notarg-hitp":
|
||
_vitalSettings.NoTargetHealth = Math.Clamp(
|
||
value.AsNumber("Recharge-NoTarg-HitP") / 100d, 0d, 1d);
|
||
break;
|
||
case "recharge-notarg-stam":
|
||
_vitalSettings.NoTargetStamina = Math.Clamp(
|
||
value.AsNumber("Recharge-NoTarg-Stam") / 100d, 0d, 1d);
|
||
break;
|
||
case "recharge-notarg-mana":
|
||
_vitalSettings.NoTargetMana = Math.Clamp(
|
||
value.AsNumber("Recharge-NoTarg-Mana") / 100d, 0d, 1d);
|
||
break;
|
||
case "recharge-helper-hitp":
|
||
_vitalSettings.HelperHealth = Math.Clamp(
|
||
value.AsNumber("Recharge-Helper-HitP") / 100d, 0d, 1d);
|
||
break;
|
||
case "recharge-helper-stam":
|
||
_vitalSettings.HelperStamina = Math.Clamp(
|
||
value.AsNumber("Recharge-Helper-Stam") / 100d, 0d, 1d);
|
||
break;
|
||
case "recharge-helper-mana":
|
||
_vitalSettings.HelperMana = Math.Clamp(
|
||
value.AsNumber("Recharge-Helper-Mana") / 100d, 0d, 1d);
|
||
break;
|
||
case "spelldiffexcessthreshold-buff":
|
||
_buffSettings.SkillExcessOverDifficulty = Math.Clamp(
|
||
value.AsInt32("SpellDiffExcessThreshold-Buff"), -100, 100);
|
||
break;
|
||
case "rebufftimeremainingseconds":
|
||
_buffSettings.RebuffWhenUnderSeconds = Math.Clamp(
|
||
value.AsNumber("RebuffTimeRemainingSeconds"), 0d, 3600d);
|
||
break;
|
||
case "buffcastrecast_seconds":
|
||
_buffSettings.BuffCastRecastSeconds = Math.Clamp(
|
||
value.AsNumber("BuffCastRecast_Seconds"), 0d, 3600d);
|
||
break;
|
||
case "buffcastrecastreset_seconds":
|
||
_buffSettings.BuffCastRecastResetSeconds = Math.Clamp(
|
||
value.AsNumber("BuffCastRecastReset_Seconds"), 0d, 3600d);
|
||
break;
|
||
case "blacklistedspellcomps":
|
||
_buffSettings.BlacklistedSpellComponents =
|
||
value.ToDisplayString();
|
||
_combatSettings.BlacklistedSpellComponents =
|
||
_buffSettings.BlacklistedSpellComponents;
|
||
break;
|
||
case "droptopeacemoderetrycount":
|
||
_vitalSettings.DropToPeaceModeRetryCount = Math.Clamp(
|
||
value.AsInt32("DropToPeaceModeRetryCount"), 1, 1000);
|
||
break;
|
||
case "fastcastbuffs":
|
||
_buffSettings.FastCastBuffs = value.IsTruthy;
|
||
break;
|
||
case "usebreakableturnto":
|
||
_combatSettings.UseBreakableTurnTo = value.IsTruthy;
|
||
break;
|
||
case "useprojectileawareness":
|
||
_combatSettings.UseProjectileAwareness = value.IsTruthy;
|
||
break;
|
||
case "collisionprojectileradius":
|
||
_combatSettings.CollisionProjectileRadius = Math.Clamp(
|
||
checked((float)value.AsNumber("CollisionProjectileRadius")),
|
||
0f,
|
||
10f);
|
||
break;
|
||
case "collisionstepdistance":
|
||
_combatSettings.CollisionStepDistance = Math.Clamp(
|
||
checked((float)value.AsNumber("CollisionStepDistance")),
|
||
0.01f,
|
||
10f);
|
||
break;
|
||
case "showcollisiondebug":
|
||
_combatSettings.ShowCollisionDebug = value.IsTruthy;
|
||
break;
|
||
case "maximumcollisioncheckspertick":
|
||
_combatSettings.MaximumCollisionChecksPerTick = Math.Clamp(
|
||
value.AsInt32("MaximumCollisionChecksPerTick"), 1, 100_000);
|
||
break;
|
||
case "usespecialammo":
|
||
_combatSettings.UseSpecialAmmo = Math.Clamp(
|
||
value.AsInt32("UseSpecialAmmo"), 0, 3);
|
||
break;
|
||
case "spellrangefudge":
|
||
_combatSettings.SpellRangeFudge = Math.Clamp(
|
||
checked((float)value.AsNumber("SpellRangeFudge")),
|
||
0f,
|
||
75f);
|
||
break;
|
||
case "buffwithuntrained-item":
|
||
_buffSettings.BuffWithUntrainedItemSkill = Math.Clamp(
|
||
value.AsInt32("BuffWithUntrained-Item"), 0, 275);
|
||
break;
|
||
case "buffwithuntrained-creature":
|
||
_buffSettings.BuffWithUntrainedCreatureSkill = Math.Clamp(
|
||
value.AsInt32("BuffWithUntrained-Creature"), 0, 275);
|
||
break;
|
||
case "buffwithuntrained-life":
|
||
_buffSettings.BuffWithUntrainedLifeSkill = Math.Clamp(
|
||
value.AsInt32("BuffWithUntrained-Life"), 0, 275);
|
||
break;
|
||
case "allowdebufffallback":
|
||
_combatSettings.AllowDebuffFallback = value.IsTruthy;
|
||
break;
|
||
case "rechargehandlerset":
|
||
_vitalSettings.RechargeHandlerSet = value.ToDisplayString();
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
_combatSettings.DynamicSettings[canonical] = ToMonsterValue(value);
|
||
if (!_applyingProfileOptions)
|
||
SaveProfile();
|
||
return true;
|
||
}
|
||
|
||
private static MonsterValue ToMonsterValue(ExpressionValue value) =>
|
||
value.Kind switch
|
||
{
|
||
ExpressionValueKind.Boolean => MonsterValue.FromBoolean(value.IsTruthy),
|
||
ExpressionValueKind.Number => MonsterValue.FromNumber(value.AsNumber()),
|
||
_ => MonsterValue.FromText(value.ToDisplayString()),
|
||
};
|
||
|
||
private static string NormalizeElementProfile(string value)
|
||
{
|
||
const string order = "ALFCBPS";
|
||
var result = new StringBuilder(order.Length);
|
||
foreach (char element in order)
|
||
{
|
||
if (value.IndexOf(element, StringComparison.OrdinalIgnoreCase) >= 0)
|
||
result.Append(element);
|
||
}
|
||
return result.ToString();
|
||
}
|
||
|
||
private void SelectProfile(string name)
|
||
{
|
||
SaveProfile();
|
||
if (!_profiles.Select(name))
|
||
{
|
||
_profileLifecycleNotice = $"Profile '{name}' is unavailable.";
|
||
return;
|
||
}
|
||
LoadSelectedProfile();
|
||
_profileLifecycleNotice = $"Loaded {_profiles.Selected}.";
|
||
}
|
||
|
||
private void CreateProfileCore(bool copyCurrent)
|
||
{
|
||
if (!_profiles.Create(
|
||
_profileNameDraft,
|
||
copyCurrent,
|
||
_combatSettings,
|
||
_buffSettings,
|
||
_vitalSettings,
|
||
_inventorySettings,
|
||
_noBuffItemNames,
|
||
out string notice))
|
||
{
|
||
_profileLifecycleNotice = notice;
|
||
return;
|
||
}
|
||
_profileNameDraft = string.Empty;
|
||
_profileLifecycleNotice = notice;
|
||
ResetProfileConsumers();
|
||
}
|
||
|
||
private void ClearProfileCore()
|
||
{
|
||
_profiles.ClearCurrent(
|
||
_combatSettings,
|
||
_buffSettings,
|
||
_vitalSettings,
|
||
_inventorySettings,
|
||
_noBuffItemNames);
|
||
_profileLifecycleNotice = $"Cleared {_profiles.Selected} to VTank defaults.";
|
||
ResetProfileConsumers();
|
||
}
|
||
|
||
private void LoadSelectedProfile()
|
||
{
|
||
_profiles.LoadCurrent(
|
||
_combatSettings,
|
||
_buffSettings,
|
||
_vitalSettings,
|
||
_inventorySettings,
|
||
_noBuffItemNames);
|
||
LoadLootProfile();
|
||
LoadRouteProfile();
|
||
ApplyPersistedOptionOverrides();
|
||
ResetProfileConsumers();
|
||
}
|
||
|
||
private void ResetProfileConsumers()
|
||
{
|
||
_vitalRecharge.Reset();
|
||
_dispel.Reset();
|
||
_inventoryMaintenance.Reset();
|
||
_crafting.Reset();
|
||
_itemManaRecharge.Reset();
|
||
_loot.Reset();
|
||
_navigation.Reset();
|
||
_coverageSpellSnapshot = null;
|
||
_coverageRefreshRemaining = 0d;
|
||
RefreshMonsterEditor();
|
||
RefreshItemEditors();
|
||
RefreshLootEditor();
|
||
RefreshRouteEditor();
|
||
}
|
||
|
||
private void ClearMossTankActionLocks()
|
||
{
|
||
ClearFastCastMovement();
|
||
_buffCastRecastRemaining = 0d;
|
||
_randomHelperRemaining = 0d;
|
||
_combat.ClearActionLocks();
|
||
_vitalRecharge.Reset();
|
||
_dispel.Reset();
|
||
_inventoryMaintenance.Reset();
|
||
_crafting.Reset();
|
||
_itemManaRecharge.Reset();
|
||
_loot.Reset();
|
||
_profileGive.Reset();
|
||
_navigation.ClearActionLocks();
|
||
}
|
||
|
||
private void FakeImperil()
|
||
{
|
||
uint target = _host.Selection.SelectedObjectId ?? 0u;
|
||
if (target == 0u
|
||
|| !_host.Automation.Objects.TryGet(target, out PluginWorldObject value)
|
||
|| value.ObjectClass != PluginObjectClass.Monster)
|
||
{
|
||
WriteVtank("Select a monster first.");
|
||
return;
|
||
}
|
||
_combat.RecordFakeImperil(target);
|
||
WriteVtank("Fake cast complete.");
|
||
}
|
||
|
||
private void EnsureCharacterProfile()
|
||
{
|
||
string characterName = _host.Automation.Character.Name;
|
||
bool macroChanged = _profiles.BindCharacter(characterName);
|
||
bool lootChanged = _lootProfiles.BindCharacter(characterName);
|
||
bool routeChanged = _routeProfiles.BindCharacter(characterName);
|
||
bool metaChanged = _metaProfiles.BindCharacter(characterName);
|
||
if (!macroChanged && !lootChanged && !routeChanged && !metaChanged)
|
||
return;
|
||
if (macroChanged)
|
||
LoadSelectedProfile();
|
||
else
|
||
{
|
||
if (lootChanged)
|
||
LoadLootProfile();
|
||
if (routeChanged)
|
||
LoadRouteProfile();
|
||
if (metaChanged)
|
||
LoadMetaProfile();
|
||
}
|
||
if (macroChanged && metaChanged)
|
||
LoadMetaProfile();
|
||
_profileLifecycleNotice = $"Loaded {_profiles.Selected} for "
|
||
+ (_host.Automation.Character.Name.Length == 0
|
||
? "this character."
|
||
: _host.Automation.Character.Name + ".");
|
||
}
|
||
|
||
private void SaveProfile()
|
||
{
|
||
_profiles.SaveCurrent(
|
||
_combatSettings,
|
||
_buffSettings,
|
||
_vitalSettings,
|
||
_inventorySettings,
|
||
_noBuffItemNames);
|
||
_lootProfiles.SaveCurrent(
|
||
_inventorySettings.Loot.Rules,
|
||
_inventorySettings.Loot);
|
||
SaveRouteProfile();
|
||
SaveMetaProfile();
|
||
}
|
||
|
||
private void ApplyPersistedOptionOverrides()
|
||
{
|
||
KeyValuePair<string, MonsterValue>[] overrides = _combatSettings
|
||
.DynamicSettings
|
||
.Where(pair => VtankOptionCatalog.IsKnown(pair.Key))
|
||
.ToArray();
|
||
if (overrides.Length == 0)
|
||
return;
|
||
_applyingProfileOptions = true;
|
||
try
|
||
{
|
||
foreach ((string name, MonsterValue value) in overrides)
|
||
SetMetaOption(name, ToExpressionValue(value));
|
||
}
|
||
finally
|
||
{
|
||
_applyingProfileOptions = false;
|
||
}
|
||
}
|
||
|
||
// ── the loop ──────────────────────────────────────────────────────────
|
||
private void Announce(string text) =>
|
||
_host.Automation.Chat.PostSystemMessage($"[MossTank] {text}");
|
||
|
||
private void StartOrStop()
|
||
{
|
||
_host.Log.Info(
|
||
$"MossTank: Buff clicked (running={_running}, inWorld={_host.Automation.IsAvailable})");
|
||
|
||
if (_running)
|
||
{
|
||
Stop("Stopped.");
|
||
Announce("Stopped.");
|
||
return;
|
||
}
|
||
|
||
StartForceBuff();
|
||
}
|
||
|
||
private void StartForceBuff()
|
||
{
|
||
if (_running)
|
||
return;
|
||
|
||
IAutomationSurface automation = _host.Automation;
|
||
if (!automation.IsAvailable)
|
||
{
|
||
_status = "Not in world.";
|
||
return;
|
||
}
|
||
|
||
StartBuffPass(
|
||
automation,
|
||
force: true,
|
||
rebuffWhenUnderSeconds: null,
|
||
announce: true);
|
||
}
|
||
|
||
private void CancelForceBuffCore()
|
||
{
|
||
if (!_running || !_forcePass)
|
||
return;
|
||
Stop("Stopped.");
|
||
Announce("Stopped.");
|
||
}
|
||
|
||
private void StartBuffPass(
|
||
IAutomationSurface automation,
|
||
bool force,
|
||
double? rebuffWhenUnderSeconds,
|
||
bool announce)
|
||
{
|
||
double? effectiveThreshold = rebuffWhenUnderSeconds;
|
||
if (!force && _buffCastRecastRemaining > 0d)
|
||
{
|
||
effectiveThreshold = (effectiveThreshold
|
||
?? _buffSettings.RebuffWhenUnderSeconds)
|
||
+ _buffSettings.BuffCastRecastSeconds;
|
||
}
|
||
_queue = BuildPlan(
|
||
automation,
|
||
force,
|
||
effectiveThreshold);
|
||
_queueIndex = 0;
|
||
_castThisPass = 0;
|
||
_sinceProgress = 0;
|
||
_forcePass = force;
|
||
_announceBuffPass = announce;
|
||
if (_queue.Count > 0)
|
||
{
|
||
_selectionBeforePass = _host.Selection.SelectedObjectId;
|
||
_buffCastRecastRemaining = Math.Max(
|
||
0d,
|
||
_buffSettings.BuffCastRecastResetSeconds);
|
||
}
|
||
_running = _queue.Count > 0;
|
||
if (_queue.Count == 0)
|
||
{
|
||
if (force)
|
||
_status = "Nothing to buff.";
|
||
if (announce)
|
||
{
|
||
Announce(
|
||
"Nothing to buff — no known self-buffs match your skills.");
|
||
}
|
||
return;
|
||
}
|
||
|
||
string kind = force ? "Force buffing" : "Buffing";
|
||
_status = $"{kind} 0/{_queue.Count}…";
|
||
_host.Log.Info(
|
||
$"MossTank: {(force ? "force" : "automatic")} pass started, "
|
||
+ $"{_queue.Count} buff(s) queued");
|
||
if (announce)
|
||
Announce($"{kind} — {_queue.Count} spell(s).");
|
||
}
|
||
|
||
private void Stop(string status)
|
||
{
|
||
ClearFastCastMovement();
|
||
_running = false;
|
||
_forcePass = false;
|
||
_announceBuffPass = false;
|
||
_queue = new List<PluginSpellInfo>();
|
||
_queueIndex = 0;
|
||
_status = status;
|
||
RestoreSelection();
|
||
}
|
||
|
||
private void RestoreSelection()
|
||
{
|
||
if (_selectionBeforePass is { } previous && previous != 0)
|
||
_host.Selection.Select(previous);
|
||
else
|
||
_host.Selection.Clear();
|
||
_selectionBeforePass = null;
|
||
}
|
||
|
||
private List<PluginSpellInfo> BuildPlan(
|
||
IAutomationSurface automation,
|
||
bool force,
|
||
double? rebuffWhenUnderSeconds = null)
|
||
{
|
||
List<PluginSpellInfo> plan = BuffPlan.Build(
|
||
BuffProfile.Build(automation.Spells.KnownSelfBuffs),
|
||
automation.Character.Skills,
|
||
automation.Character.Attributes,
|
||
automation.Character.ActiveEnchantments,
|
||
_buffSettings,
|
||
force,
|
||
rebuffWhenUnderSeconds,
|
||
automation.Character.Level);
|
||
plan.RemoveAll(spell => SpellComponentPolicy.UsesBlacklistedComponent(
|
||
automation.Spells,
|
||
spell,
|
||
_buffSettings.BlacklistedSpellComponents));
|
||
return plan;
|
||
}
|
||
|
||
private void ToggleMacro() => SetMacroRunning(!_combat.Enabled);
|
||
|
||
private void SetMacroRunning(bool running)
|
||
{
|
||
if (_combat.Enabled == running)
|
||
return;
|
||
_combat.Toggle();
|
||
if (running || _combat.Enabled)
|
||
return;
|
||
|
||
// A stopped VTank macro owns no movement or staged maintenance work.
|
||
// Worn-mana upkeep is intentionally not reset: VTank's
|
||
// ManaChargesWhenOff option permits that one controller to continue.
|
||
_vitalRecharge.Reset();
|
||
_dispel.Reset();
|
||
_inventoryMaintenance.Reset();
|
||
_crafting.Reset();
|
||
_loot.Reset();
|
||
_profileGive.Reset();
|
||
_navigation.Reset();
|
||
}
|
||
|
||
/// <summary>Driven by <see cref="IEvents.Tick"/> on the host update thread.</summary>
|
||
public void OnTick(double elapsedSeconds)
|
||
{
|
||
bool automationAvailable = _host.Automation.IsAvailable;
|
||
if (!automationAvailable)
|
||
{
|
||
if (_automationWasAvailable)
|
||
HandleSessionEnded();
|
||
_automationWasAvailable = false;
|
||
RefreshDisplayBindings(elapsedSeconds);
|
||
return;
|
||
}
|
||
if (!_automationWasAvailable)
|
||
{
|
||
_automationWasAvailable = true;
|
||
HandleSessionStarted();
|
||
}
|
||
|
||
ObserveFastCastMovement(elapsedSeconds);
|
||
_buffCastRecastRemaining = Math.Max(
|
||
0d,
|
||
_buffCastRecastRemaining - Math.Max(0d, elapsedSeconds));
|
||
EnsureCharacterProfile();
|
||
ShowFirstRunGuidance();
|
||
ObserveCommandPortalState();
|
||
bool macroRunning = _combat.Enabled;
|
||
if (macroRunning
|
||
&& _combatSettings.StopMacroOnDeath
|
||
&& _host.Automation.IsAvailable
|
||
&& _host.Automation.Character.MaxHealth > 0u
|
||
&& _host.Automation.Character.CurrentHealth == 0u)
|
||
{
|
||
SetMacroRunning(false);
|
||
macroRunning = false;
|
||
Announce("Macro stopped because the character died.");
|
||
}
|
||
_fellowshipManager.Tick(
|
||
elapsedSeconds,
|
||
macroRunning && AutoFellowManagementEnabled);
|
||
if (macroRunning)
|
||
_meta.OnTick(elapsedSeconds);
|
||
_combatSettings.MetaState = _meta.CurrentState;
|
||
RefreshDisplayBindings(elapsedSeconds);
|
||
|
||
bool commandJumpOwnsAction = TickCommandJump(elapsedSeconds);
|
||
bool giveOwnsAction = _profileGive.Tick(
|
||
elapsedSeconds,
|
||
canAct: !_running && !commandJumpOwnsAction);
|
||
bool criticalCraftOwnsAction = _crafting.TickCritical(
|
||
elapsedSeconds,
|
||
canAct: macroRunning
|
||
&& !_running
|
||
&& !commandJumpOwnsAction
|
||
&& !giveOwnsAction);
|
||
bool vitalOwnsAction = _vitalRecharge.Tick(
|
||
elapsedSeconds,
|
||
(macroRunning || _running)
|
||
&& !commandJumpOwnsAction
|
||
&& !giveOwnsAction
|
||
&& !criticalCraftOwnsAction,
|
||
noTarget: !_combat.HasTarget);
|
||
TickAutomaticBuffing(
|
||
elapsedSeconds,
|
||
macroRunning,
|
||
canAct: !commandJumpOwnsAction
|
||
&& !giveOwnsAction
|
||
&& !criticalCraftOwnsAction
|
||
&& !vitalOwnsAction);
|
||
bool dispelOwnsAction = _dispel.Tick(
|
||
elapsedSeconds,
|
||
canAct: macroRunning
|
||
&& !_running
|
||
&& !commandJumpOwnsAction
|
||
&& !giveOwnsAction
|
||
&& !criticalCraftOwnsAction
|
||
&& !vitalOwnsAction);
|
||
bool manaRechargeOwnsAction = _itemManaRecharge.Tick(
|
||
canAct: (macroRunning || _inventorySettings.ManaChargesWhenOff)
|
||
&& !_running
|
||
&& !commandJumpOwnsAction
|
||
&& !giveOwnsAction
|
||
&& !criticalCraftOwnsAction
|
||
&& !vitalOwnsAction
|
||
&& !dispelOwnsAction);
|
||
bool craftingOwnsAction = _crafting.Tick(
|
||
elapsedSeconds,
|
||
canAct: macroRunning
|
||
&& !_running
|
||
&& !commandJumpOwnsAction
|
||
&& !giveOwnsAction
|
||
&& !criticalCraftOwnsAction
|
||
&& !vitalOwnsAction
|
||
&& !dispelOwnsAction
|
||
&& !manaRechargeOwnsAction);
|
||
bool idleCraftingOwnsAction = _crafting.TickIdle(
|
||
elapsedSeconds,
|
||
canAct: macroRunning
|
||
&& !_running
|
||
&& !commandJumpOwnsAction
|
||
&& !giveOwnsAction
|
||
&& !criticalCraftOwnsAction
|
||
&& !vitalOwnsAction
|
||
&& !dispelOwnsAction
|
||
&& !manaRechargeOwnsAction
|
||
&& !craftingOwnsAction
|
||
&& !_combat.HasTarget);
|
||
bool lootOwnsAction = _loot.Tick(
|
||
elapsedSeconds,
|
||
canAct: macroRunning
|
||
&& !_running
|
||
&& !commandJumpOwnsAction
|
||
&& !giveOwnsAction
|
||
&& !criticalCraftOwnsAction
|
||
&& !vitalOwnsAction
|
||
&& !dispelOwnsAction
|
||
&& !manaRechargeOwnsAction
|
||
&& !craftingOwnsAction
|
||
&& !idleCraftingOwnsAction
|
||
&& (_inventorySettings.Loot.PriorityBoost
|
||
|| !_combat.HasTarget));
|
||
bool inventoryOwnsAction = _inventoryMaintenance.Tick(
|
||
elapsedSeconds,
|
||
canAct: macroRunning
|
||
&& !_running
|
||
&& !commandJumpOwnsAction
|
||
&& !giveOwnsAction
|
||
&& !criticalCraftOwnsAction
|
||
&& !vitalOwnsAction
|
||
&& !dispelOwnsAction
|
||
&& !manaRechargeOwnsAction
|
||
&& !lootOwnsAction
|
||
&& !craftingOwnsAction
|
||
&& !idleCraftingOwnsAction
|
||
&& !_combat.HasTarget);
|
||
bool navigationOwnsAction = _navigation.Tick(
|
||
elapsedSeconds,
|
||
canAct: macroRunning
|
||
&& !_running
|
||
&& !commandJumpOwnsAction
|
||
&& !giveOwnsAction
|
||
&& !criticalCraftOwnsAction
|
||
&& !vitalOwnsAction
|
||
&& !dispelOwnsAction
|
||
&& !manaRechargeOwnsAction
|
||
&& !lootOwnsAction
|
||
&& !craftingOwnsAction
|
||
&& !idleCraftingOwnsAction
|
||
&& !inventoryOwnsAction
|
||
&& (_navigationSettings.Priority || !_combat.HasTarget));
|
||
bool randomHelperOwnsAction = TickRandomHelper(
|
||
elapsedSeconds,
|
||
canAct: macroRunning
|
||
&& !_running
|
||
&& !commandJumpOwnsAction
|
||
&& !giveOwnsAction
|
||
&& !criticalCraftOwnsAction
|
||
&& !vitalOwnsAction
|
||
&& !dispelOwnsAction
|
||
&& !manaRechargeOwnsAction
|
||
&& !lootOwnsAction
|
||
&& !craftingOwnsAction
|
||
&& !idleCraftingOwnsAction
|
||
&& !inventoryOwnsAction
|
||
&& !navigationOwnsAction
|
||
&& !_combat.HasTarget);
|
||
_combat.SetPaused(
|
||
_running || commandJumpOwnsAction || giveOwnsAction
|
||
|| criticalCraftOwnsAction || vitalOwnsAction
|
||
|| dispelOwnsAction
|
||
|| manaRechargeOwnsAction
|
||
|| lootOwnsAction
|
||
|| craftingOwnsAction
|
||
|| idleCraftingOwnsAction
|
||
|| inventoryOwnsAction
|
||
|| randomHelperOwnsAction
|
||
|| (navigationOwnsAction && _navigationSettings.Priority));
|
||
_combat.OnTick(elapsedSeconds, _navigationSettings.Enabled);
|
||
if (_activeTab == TankTab.Route)
|
||
RefreshRouteEditor();
|
||
|
||
if (!_running)
|
||
return;
|
||
|
||
IAutomationSurface automation = _host.Automation;
|
||
if (!automation.IsAvailable)
|
||
{
|
||
Stop("Lost the session.");
|
||
return;
|
||
}
|
||
|
||
_sinceProgress += elapsedSeconds;
|
||
if (_sinceProgress > StallTimeoutSeconds)
|
||
{
|
||
Stop($"Stalled after {_castThisPass} cast(s).");
|
||
_host.Log.Warn("MossTank: pass stalled; stopping");
|
||
Announce($"Stopped — no progress after {_castThisPass} cast(s).");
|
||
return;
|
||
}
|
||
|
||
// The server is the only throttle: cast the moment the previous action
|
||
// is acknowledged. The fixed delay this replaces was standing in for a
|
||
// busy signal we now have, and it only made the pass slow.
|
||
if (automation.Magic.IsCasting)
|
||
return;
|
||
|
||
if (vitalOwnsAction)
|
||
return;
|
||
|
||
if (inventoryOwnsAction)
|
||
return;
|
||
|
||
if (craftingOwnsAction)
|
||
return;
|
||
|
||
if (idleCraftingOwnsAction)
|
||
return;
|
||
|
||
if (manaRechargeOwnsAction)
|
||
return;
|
||
|
||
if (lootOwnsAction)
|
||
return;
|
||
|
||
if (giveOwnsAction)
|
||
return;
|
||
|
||
if (_queueIndex >= _queue.Count)
|
||
{
|
||
bool announce = _announceBuffPass;
|
||
bool force = _forcePass;
|
||
Stop($"Done — {_castThisPass} cast(s).");
|
||
_host.Log.Info(
|
||
$"MossTank: {(force ? "force" : "automatic")} pass complete "
|
||
+ $"({_castThisPass} cast)");
|
||
if (announce)
|
||
Announce($"Finished — {_castThisPass} spell(s) cast.");
|
||
return;
|
||
}
|
||
|
||
PluginSpellInfo next = _queue[_queueIndex];
|
||
// Advance on both outcomes. A spell that will not go now (missing
|
||
// components, a gate that stays shut) must not block the rest of the
|
||
// queue behind it; the status line names why it was skipped.
|
||
TryCast(
|
||
automation,
|
||
next,
|
||
$"{(_forcePass ? "Force buffing" : "Buffing")} "
|
||
+ $"{_castThisPass + 1}/{_queue.Count}");
|
||
_queueIndex++;
|
||
}
|
||
|
||
private void TickAutomaticBuffing(
|
||
double elapsedSeconds,
|
||
bool macroRunning,
|
||
bool canAct)
|
||
{
|
||
_automaticBuffScanRemaining -= Math.Max(0d, elapsedSeconds);
|
||
if (!macroRunning || !_buffSettings.Enabled || _running || !canAct
|
||
|| _automaticBuffScanRemaining > 0d)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_automaticBuffScanRemaining = 1d;
|
||
double threshold = !_combat.HasTarget && _buffSettings.IdleBuffTopoff
|
||
? _buffSettings.IdleBuffTopoffSeconds
|
||
: _buffSettings.RebuffWhenUnderSeconds;
|
||
StartBuffPass(
|
||
_host.Automation,
|
||
force: false,
|
||
rebuffWhenUnderSeconds: threshold,
|
||
announce: false);
|
||
}
|
||
|
||
private bool TickRandomHelper(double elapsedSeconds, bool canAct)
|
||
{
|
||
_randomHelperRemaining = Math.Max(
|
||
0d,
|
||
_randomHelperRemaining - Math.Max(0d, elapsedSeconds));
|
||
if (!canAct
|
||
|| !_buffSettings.RandomHelperBuffs
|
||
|| _randomHelperRemaining > 0d
|
||
|| !_host.Automation.IsAvailable)
|
||
{
|
||
return false;
|
||
}
|
||
if (_host.Automation.Magic.IsCasting)
|
||
return true;
|
||
|
||
PluginNavigationSnapshot navigation =
|
||
_host.Automation.Navigation.Snapshot;
|
||
if (!navigation.IsAvailable)
|
||
return false;
|
||
PluginWorldObject[] players = _host.Automation.Objects.CaptureObjects()
|
||
.Where(value => value.ObjectClass == PluginObjectClass.Player
|
||
&& value.ObjectId != _host.Automation.Character.ObjectId
|
||
&& value.HasPosition
|
||
&& navigation.Position.HorizontalDistanceMeters(value.Position)
|
||
< 18d)
|
||
.OrderBy(static value => value.ObjectId)
|
||
.ToArray();
|
||
if (players.Length == 0)
|
||
return false;
|
||
|
||
string[] stems =
|
||
[
|
||
"Endurance Other", "Regeneration Other", "Rejuvenation Other",
|
||
"Armor Other", "Blade Protection Other",
|
||
"Bludgeoning Protection Other", "Cold Protection Other",
|
||
"Fire Protection Other", "Lightning Protection Other",
|
||
"Piercing Protection Other", "Acid Protection Other",
|
||
];
|
||
int attempts = players.Length * stems.Length;
|
||
for (int offset = 0; offset < attempts; offset++)
|
||
{
|
||
int slot = (_randomHelperCursor + offset) % attempts;
|
||
PluginWorldObject player = players[slot % players.Length];
|
||
string stem = stems[(slot / players.Length) % stems.Length];
|
||
PluginSpellInfo spell = _host.Automation.Spells.KnownSelfBuffs
|
||
.Where(value => value.Name.StartsWith(
|
||
stem,
|
||
StringComparison.OrdinalIgnoreCase))
|
||
.OrderByDescending(static value => value.Quality)
|
||
.ThenByDescending(static value => value.Tier)
|
||
.FirstOrDefault();
|
||
if (spell.SpellId == 0u
|
||
|| SpellComponentPolicy.UsesBlacklistedComponent(
|
||
_host.Automation.Spells,
|
||
spell,
|
||
_buffSettings.BlacklistedSpellComponents)
|
||
|| _host.Automation.Magic.EvaluateGate(
|
||
spell.SpellId,
|
||
player.ObjectId) != PluginCastGate.Ready
|
||
|| !_host.Automation.Magic.Cast(
|
||
spell.SpellId,
|
||
player.ObjectId))
|
||
{
|
||
continue;
|
||
}
|
||
_randomHelperCursor = (slot + 1) % attempts;
|
||
_randomHelperRemaining = Math.Max(
|
||
0.25d,
|
||
_buffSettings.RandomHelperIntervalSeconds);
|
||
_host.Log.Info(
|
||
$"MossTank: random helper {spell.Name} -> {player.Name}");
|
||
return true;
|
||
}
|
||
_randomHelperCursor = (_randomHelperCursor + 1) % attempts;
|
||
return false;
|
||
}
|
||
|
||
private void ShowFirstRunGuidance()
|
||
{
|
||
if (!_firstRunGuidancePending || !_host.Automation.IsAvailable)
|
||
return;
|
||
_firstRunGuidancePending = false;
|
||
const string guidance = "First run: choose profiles, configure the "
|
||
+ "Options tab, then press Run Macro. Minimize with –; MossTank "
|
||
+ "keeps running from the right-side plugin shelf. Put VTank "
|
||
+ ".nav/.utl/.met files in imports and use /vt nav, /vt loot, or "
|
||
+ "/vt meta import <name>.";
|
||
Announce(guidance);
|
||
try
|
||
{
|
||
_host.Storage.WriteText("onboarding/v1.txt", "shown");
|
||
}
|
||
catch (Exception error)
|
||
{
|
||
_host.Log.Warn(
|
||
"MossTank could not persist first-run guidance state: "
|
||
+ error.Message);
|
||
}
|
||
}
|
||
|
||
private static bool NeedsFirstRunGuidance(IPluginHost host)
|
||
{
|
||
if (!host.Storage.IsAvailable)
|
||
return false;
|
||
try
|
||
{
|
||
return string.IsNullOrWhiteSpace(
|
||
host.Storage.ReadText("onboarding/v1.txt"));
|
||
}
|
||
catch (Exception error)
|
||
{
|
||
host.Log.Warn(
|
||
"MossTank could not read first-run guidance state: "
|
||
+ error.Message);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
public void Disable()
|
||
{
|
||
if (_combat.Enabled)
|
||
SetMacroRunning(false);
|
||
if (_running)
|
||
Stop("Stopped.");
|
||
_vitalRecharge.Reset();
|
||
_inventoryMaintenance.Reset();
|
||
_crafting.Reset();
|
||
_itemManaRecharge.Reset();
|
||
_loot.Reset();
|
||
_profileGive.Reset();
|
||
_navigation.Reset();
|
||
_fellowshipManager.Reset();
|
||
_meta.SetEnabled(false);
|
||
_metaViews.DestroyAll();
|
||
_expressions.DestroyAuxiliaryViews();
|
||
_expressions.ClearSession();
|
||
}
|
||
|
||
private void HandleSessionEnded()
|
||
{
|
||
// Combat owns the authoritative physical abort and all of its receipt
|
||
// cursors. Drive its existing session-loss path before resetting the
|
||
// sibling schedulers.
|
||
if (_combat.Enabled)
|
||
_combat.OnTick(0d, navigationEnabled: false);
|
||
|
||
ClearFastCastMovement();
|
||
_running = false;
|
||
_forcePass = false;
|
||
_announceBuffPass = false;
|
||
_queue.Clear();
|
||
_queueIndex = 0;
|
||
_selectionBeforePass = null;
|
||
_status = "Lost the session.";
|
||
ResetSessionScopedControllers();
|
||
}
|
||
|
||
private void HandleSessionStarted()
|
||
{
|
||
// Re-baseline portal/death/chat state against the NEW session. This is
|
||
// also required for a same-character relog, where identity-based
|
||
// persistence cannot itself distinguish the old and new sessions.
|
||
_meta.ResetSession();
|
||
_expressions.ClearSession();
|
||
_expressions.DestroyAuxiliaryViews();
|
||
_metaViews.DestroyAll();
|
||
ResetCommandSession();
|
||
_automaticBuffScanRemaining = 0d;
|
||
_buffCastRecastRemaining = 0d;
|
||
_randomHelperRemaining = 0d;
|
||
_randomHelperCursor = 0;
|
||
_coverageSpellSnapshot = null;
|
||
_coverageRefreshRemaining = 0d;
|
||
_status = "Idle.";
|
||
}
|
||
|
||
private void ResetSessionScopedControllers()
|
||
{
|
||
_vitalRecharge.Reset();
|
||
_dispel.Reset();
|
||
_inventoryMaintenance.Reset();
|
||
_crafting.Reset();
|
||
_itemManaRecharge.Reset();
|
||
_loot.Reset();
|
||
_profileGive.Reset();
|
||
_navigation.Reset();
|
||
_fellowshipManager.Reset();
|
||
_meta.ResetSession();
|
||
_metaViews.DestroyAll();
|
||
_expressions.DestroyAuxiliaryViews();
|
||
_expressions.ClearSession();
|
||
ResetCommandSession();
|
||
_automaticBuffScanRemaining = 0d;
|
||
_buffCastRecastRemaining = 0d;
|
||
_randomHelperRemaining = 0d;
|
||
_randomHelperCursor = 0;
|
||
_coverageSpellSnapshot = null;
|
||
_coverageRefreshRemaining = 0d;
|
||
_combatSettings.MetaState = MetaEngine.DefaultState;
|
||
}
|
||
|
||
private void RefreshDisplayBindings(double elapsedSeconds)
|
||
{
|
||
IAutomationSurface automation = _host.Automation;
|
||
if (!automation.IsAvailable)
|
||
{
|
||
_vitals = string.Empty;
|
||
_coverage = string.Empty;
|
||
_vitalsInitialized = false;
|
||
_coverageSpellSnapshot = null;
|
||
_coverageBuffLineCount = 0;
|
||
_coverageRefreshRemaining = 0.0;
|
||
return;
|
||
}
|
||
|
||
ICharacterInfo character = automation.Character;
|
||
uint currentHealth = character.CurrentHealth;
|
||
uint maxHealth = character.MaxHealth;
|
||
uint currentStamina = character.CurrentStamina;
|
||
uint maxStamina = character.MaxStamina;
|
||
uint currentMana = character.CurrentMana;
|
||
uint maxMana = character.MaxMana;
|
||
if (!_vitalsInitialized
|
||
|| currentHealth != _currentHealth
|
||
|| maxHealth != _maxHealth
|
||
|| currentStamina != _currentStamina
|
||
|| maxStamina != _maxStamina
|
||
|| currentMana != _currentMana
|
||
|| maxMana != _maxMana)
|
||
{
|
||
_currentHealth = currentHealth;
|
||
_maxHealth = maxHealth;
|
||
_currentStamina = currentStamina;
|
||
_maxStamina = maxStamina;
|
||
_currentMana = currentMana;
|
||
_maxMana = maxMana;
|
||
_vitals = $"Health {currentHealth}/{maxHealth}"
|
||
+ $" Stam {currentStamina}/{maxStamina}"
|
||
+ $" Mana {currentMana}/{maxMana}";
|
||
_vitalsInitialized = true;
|
||
}
|
||
|
||
IReadOnlyList<PluginSpellInfo> spells = automation.Spells.KnownSelfBuffs;
|
||
bool spellbookChanged = !ReferenceEquals(spells, _coverageSpellSnapshot);
|
||
_coverageRefreshRemaining -= Math.Max(0.0, elapsedSeconds);
|
||
if (!spellbookChanged && _coverageRefreshRemaining > 0.0)
|
||
return;
|
||
|
||
if (spellbookChanged)
|
||
{
|
||
_coverageSpellSnapshot = spells;
|
||
_coverageBuffLineCount = BuffProfile.Build(spells).Count;
|
||
}
|
||
|
||
int trained = 0;
|
||
foreach (PluginSkillInfo skill in character.Skills)
|
||
{
|
||
if (skill.Training is PluginSkillTraining.Trained
|
||
or PluginSkillTraining.Specialized)
|
||
{
|
||
trained++;
|
||
}
|
||
}
|
||
_coverage = $"{character.Attributes.Count} attributes, "
|
||
+ $"{trained} trained skills, "
|
||
+ $"{_coverageBuffLineCount} buff lines";
|
||
_coverageRefreshRemaining = CoverageRefreshIntervalSeconds;
|
||
}
|
||
|
||
private bool TryCast(
|
||
IAutomationSurface automation, PluginSpellInfo spell, string label)
|
||
{
|
||
// A spell without the self-targeted flag still needs a target, and for
|
||
// a bane that target is the player: retail's text is "Target yourself
|
||
// to cast this spell on all of your equipped armor". Select first, or
|
||
// the gate refuses for want of a target.
|
||
if (!spell.IsSelfTargeted)
|
||
{
|
||
uint self = automation.Character.ObjectId;
|
||
if (self == 0)
|
||
{
|
||
_status = $"{spell.Name}: no self target";
|
||
return false;
|
||
}
|
||
if (_host.Selection.SelectedObjectId != self)
|
||
_host.Selection.Select(self);
|
||
}
|
||
|
||
PluginCastGate gate = automation.Magic.EvaluateGate(spell.SpellId);
|
||
if (gate != PluginCastGate.Ready)
|
||
{
|
||
_status = $"{spell.Name}: {gate}";
|
||
return false;
|
||
}
|
||
|
||
if (!automation.Magic.Cast(spell.SpellId))
|
||
{
|
||
_status = $"Refused {spell.Name}.";
|
||
return false;
|
||
}
|
||
|
||
BeginFastCastMovement(automation, spell);
|
||
_castThisPass++;
|
||
_sinceProgress = 0;
|
||
_status = $"{label}: {spell.Name}";
|
||
_host.Log.Info($"MossTank: casting {spell.Name} (0x{spell.SpellId:X4})");
|
||
return true;
|
||
}
|
||
|
||
private void BeginFastCastMovement(
|
||
IAutomationSurface automation,
|
||
in PluginSpellInfo spell)
|
||
{
|
||
if (!_buffSettings.FastCastBuffs || !IsVtankInstantCast(spell))
|
||
return;
|
||
// VTank excludes War (school 1) and Void (school 5). The plugin API
|
||
// projects schools as their retail skill ids: 34 and 43 respectively.
|
||
if (spell.School is 34u or 43u)
|
||
return;
|
||
|
||
PluginNavigationCommandStatus result = automation.Navigation
|
||
.SetMovementIntent(new PluginMovementIntent(Forward: true));
|
||
if (result != PluginNavigationCommandStatus.Accepted)
|
||
return;
|
||
_fastCastMovementActive = true;
|
||
_fastCastStartCompletionRevision = automation.Magic.LastCompletion.Revision;
|
||
_fastCastMovementElapsed = 0d;
|
||
}
|
||
|
||
private void ObserveFastCastMovement(double elapsedSeconds)
|
||
{
|
||
if (!_fastCastMovementActive)
|
||
return;
|
||
_fastCastMovementElapsed += Math.Max(0d, elapsedSeconds);
|
||
IMagicCommands magic = _host.Automation.Magic;
|
||
bool receiptArrived = magic.LastCompletion.Revision
|
||
!= _fastCastStartCompletionRevision;
|
||
bool castEnded = _fastCastMovementElapsed >= 0.2d && !magic.IsCasting;
|
||
if (receiptArrived || castEnded || _fastCastMovementElapsed >= 10d)
|
||
ClearFastCastMovement();
|
||
}
|
||
|
||
private void ClearFastCastMovement()
|
||
{
|
||
if (!_fastCastMovementActive)
|
||
return;
|
||
_host.Automation.Navigation.ClearMovementIntent();
|
||
_fastCastMovementActive = false;
|
||
_fastCastStartCompletionRevision = 0;
|
||
_fastCastMovementElapsed = 0d;
|
||
}
|
||
|
||
private static bool IsVtankInstantCast(in PluginSpellInfo spell)
|
||
{
|
||
if (spell.Difficulty < 50)
|
||
return true;
|
||
if (spell.IsUntargeted
|
||
&& !spell.IsFellowship
|
||
&& spell.DurationSeconds >= 60f
|
||
&& spell.School is 31u or 33u)
|
||
{
|
||
return true;
|
||
}
|
||
return spell.Family is >= 243u and <= 249u or 639u;
|
||
}
|
||
}
|