feat(mosstank): cast back-to-back, and a settings view for the thresholds
Three things from the first working buff pass. **Pacing.** Casts were three seconds apart because of a fixed interval I added before there was a real busy signal. There is one now -- the shared busy count, incremented by the cast and decremented by the server's UseDone -- so the interval is gone and the server is the only throttle. MossTank casts the moment the previous action is acknowledged, which is the spam behaviour VTank has. The stall timeout moved 25s -> 30s so a slow server does not read as a stall. **Stamina to Mana was surprising.** VTank does convert vitals by default (Recharge-*-Mana), so the behaviour is right, but a buff pass quietly spending your stamina is not something to discover by watching. It is now a setting, with its own thresholds, and can be turned off outright. **Settings view.** Spell difficulty margin, rebuff time, the three vital thresholds, and three toggles. Two details worth recording: * The difficulty margin is SIGNED, per the VTank wiki: "a positive number raises the skill necessary to cast spells, a negative number lowers it. To attempt higher spells at a low level use a negative number." So the range spans -100..+100 rather than starting at zero. * It is a second registered panel with a complementary visible binding rather than a tab control. Two panels and an Action need no new markup vocabulary, and only one is ever on screen. Adjuster buttons rather than typed fields: buttons are proven in plugin markup, while an editable UiField would need keyboard routing plumbed through to plugin panels first. Worth doing, but not as a side quest inside this change. Also fixed: the App copy target names plugin files explicitly, so the new markup would have been left out of the plugin directory and the settings panel would have failed to load at runtime with the build perfectly green. Caught by listing the output rather than trusting the build. Solution builds clean; 14,438 tests pass on the standard hermetic lane filter, 0 failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
1a7b0ed3d5
commit
54005a864c
9 changed files with 179 additions and 69 deletions
|
|
@ -167,7 +167,7 @@
|
|||
</PropertyGroup>
|
||||
<MakeDir Directories="$(_MossTankDestDir)" />
|
||||
<Copy
|
||||
SourceFiles="$(_MossTankSourceDir)/AcDream.Plugins.MossTank.dll;$(_MossTankSourceDir)/mosstank.xml"
|
||||
SourceFiles="$(_MossTankSourceDir)/AcDream.Plugins.MossTank.dll;$(_MossTankSourceDir)/mosstank.xml;$(_MossTankSourceDir)/mosstank-settings.xml"
|
||||
DestinationFolder="$(_MossTankDestDir)"
|
||||
SkipUnchangedFiles="true" />
|
||||
<WriteLinesToFile
|
||||
|
|
@ -187,7 +187,7 @@
|
|||
</PropertyGroup>
|
||||
<MakeDir Directories="$(_MossTankPublishDestDir)" />
|
||||
<Copy
|
||||
SourceFiles="$(_MossTankPublishSourceDir)/AcDream.Plugins.MossTank.dll;$(_MossTankPublishSourceDir)/mosstank.xml"
|
||||
SourceFiles="$(_MossTankPublishSourceDir)/AcDream.Plugins.MossTank.dll;$(_MossTankPublishSourceDir)/mosstank.xml;$(_MossTankPublishSourceDir)/mosstank-settings.xml"
|
||||
DestinationFolder="$(_MossTankPublishDestDir)"
|
||||
SkipUnchangedFiles="true" />
|
||||
<WriteLinesToFile
|
||||
|
|
|
|||
|
|
@ -22,5 +22,8 @@
|
|||
<None Update="mosstank.xml">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="mosstank-settings.xml">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -3,29 +3,29 @@ using AcDream.Plugin.Abstractions;
|
|||
namespace AcDream.Plugins.MossTank;
|
||||
|
||||
/// <summary>Settings that shape a buff pass. Defaults follow Virindi Tank's.</summary>
|
||||
public sealed record BuffSettings
|
||||
public sealed class BuffSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// VTank recasts buffs once they drop below five minutes remaining
|
||||
/// ("all buff spells are recast when they go below 5 minutes").
|
||||
/// </summary>
|
||||
public double RebuffWhenUnderSeconds { get; init; } = 300.0;
|
||||
public double RebuffWhenUnderSeconds { get; set; } = 300.0;
|
||||
|
||||
/// <summary>
|
||||
/// How far the casting skill must exceed a spell's difficulty before the
|
||||
/// tier is considered reliable — VTank's
|
||||
/// <c>SpellDiffExcessThreshold-Buff</c>.
|
||||
/// </summary>
|
||||
public int SkillExcessOverDifficulty { get; init; } = 10;
|
||||
public int SkillExcessOverDifficulty { get; set; } = 10;
|
||||
|
||||
/// <summary>Buff every attribute (VTank's default).</summary>
|
||||
public bool BuffAttributes { get; init; } = true;
|
||||
public bool BuffAttributes { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Buff trained and specialised skills only — VTank's stated default:
|
||||
/// "automatically buffs every Attribute and Skill you have trained".
|
||||
/// </summary>
|
||||
public bool BuffTrainedSkillsOnly { get; init; } = true;
|
||||
public bool BuffTrainedSkillsOnly { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Globalization;
|
||||
using AcDream.Plugin.Abstractions;
|
||||
|
||||
namespace AcDream.Plugins.MossTank;
|
||||
|
|
@ -6,17 +7,18 @@ namespace AcDream.Plugins.MossTank;
|
|||
/// 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 click and the
|
||||
/// tick both arrive there — so no locking is used, deliberately: a lock would
|
||||
/// 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 class MossTankPanel
|
||||
{
|
||||
/// <summary>Roughly a retail cast plus windup, so casts do not stack up.</summary>
|
||||
private const double CastIntervalSeconds = 3.0;
|
||||
|
||||
/// <summary>Give up on a pass that stops making progress.</summary>
|
||||
private const double StallTimeoutSeconds = 25.0;
|
||||
/// <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;
|
||||
|
||||
private readonly IPluginHost _host;
|
||||
private readonly BuffSettings _buffSettings = new();
|
||||
|
|
@ -25,24 +27,24 @@ internal sealed class MossTankPanel
|
|||
private List<PluginSpellInfo> _plan = new();
|
||||
private int _planIndex;
|
||||
private bool _running;
|
||||
private double _sinceLastCast;
|
||||
private double _sinceProgress;
|
||||
private int _castThisPass;
|
||||
private string _status = "Idle.";
|
||||
|
||||
public MossTankPanel(IPluginHost host) => _host = host;
|
||||
|
||||
/// <summary>Bound to the panel's Buff button.</summary>
|
||||
// ── main panel bindings ───────────────────────────────────────────────
|
||||
public Action Buff => StartOrStop;
|
||||
public Action OpenSettings => () => SettingsOpen = true;
|
||||
public Action CloseSettings => () => SettingsOpen = false;
|
||||
|
||||
/// <summary>
|
||||
/// Bound to the panel's <c>visible</c>. Keeps the window off the character
|
||||
/// select and login screens, where there is no character to buff.
|
||||
/// </summary>
|
||||
public bool IsInWorld => _host.Automation.IsAvailable;
|
||||
public bool SettingsOpen { get; private set; }
|
||||
|
||||
/// <summary>Keeps the windows off the character-select and login screens.</summary>
|
||||
public bool MainVisible => _host.Automation.IsAvailable && !SettingsOpen;
|
||||
public bool SettingsVisible => _host.Automation.IsAvailable && SettingsOpen;
|
||||
|
||||
public string ButtonText => _running ? "Stop" : "Buff";
|
||||
|
||||
public string Status => _status;
|
||||
|
||||
/// <summary>Vitals line, using the same numbers the character panel shows.</summary>
|
||||
|
|
@ -77,17 +79,78 @@ internal sealed class MossTankPanel
|
|||
trained++;
|
||||
}
|
||||
}
|
||||
int attributes = automation.Character.Attributes.Count;
|
||||
int lines = BuffProfile.Build(automation.Spells.KnownSelfBuffs).Count;
|
||||
return $"{attributes} attributes, {trained} trained skills, {lines} buff lines known";
|
||||
return $"{automation.Character.Attributes.Count} attributes, "
|
||||
+ $"{trained} trained skills, "
|
||||
+ $"{BuffProfile.Build(automation.Spells.KnownSelfBuffs).Count} buff lines";
|
||||
}
|
||||
}
|
||||
|
||||
// ── 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 ManaFloorText => $"Convert below mana: {Percent(_vitalSettings.ManaFloor)}";
|
||||
public string ManaTargetText => $"Stop converting at: {Percent(_vitalSettings.ManaTarget)}";
|
||||
public string StaminaFloorText => $"Keep stamina above: {Percent(_vitalSettings.StaminaFloor)}";
|
||||
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 Action DifficultyDown => () => _buffSettings.SkillExcessOverDifficulty =
|
||||
Math.Max(-100, _buffSettings.SkillExcessOverDifficulty - 5);
|
||||
public Action DifficultyUp => () => _buffSettings.SkillExcessOverDifficulty =
|
||||
Math.Min(100, _buffSettings.SkillExcessOverDifficulty + 5);
|
||||
|
||||
public Action RebuffDown => () => _buffSettings.RebuffWhenUnderSeconds =
|
||||
Math.Max(30, _buffSettings.RebuffWhenUnderSeconds - 30);
|
||||
public Action RebuffUp => () => _buffSettings.RebuffWhenUnderSeconds =
|
||||
Math.Min(1800, _buffSettings.RebuffWhenUnderSeconds + 30);
|
||||
|
||||
public Action ManaFloorDown => () => _vitalSettings.ManaFloor = Step(_vitalSettings.ManaFloor, -1);
|
||||
public Action ManaFloorUp => () => _vitalSettings.ManaFloor = Step(_vitalSettings.ManaFloor, +1);
|
||||
|
||||
public Action ManaTargetDown => () => _vitalSettings.ManaTarget = Step(_vitalSettings.ManaTarget, -1);
|
||||
public Action ManaTargetUp => () => _vitalSettings.ManaTarget = Step(_vitalSettings.ManaTarget, +1);
|
||||
|
||||
public Action StaminaFloorDown => () => _vitalSettings.StaminaFloor = Step(_vitalSettings.StaminaFloor, -1);
|
||||
public Action StaminaFloorUp => () => _vitalSettings.StaminaFloor = Step(_vitalSettings.StaminaFloor, +1);
|
||||
|
||||
public Action ToggleVitalUpkeep => () => _vitalSettings.Enabled = !_vitalSettings.Enabled;
|
||||
public Action ToggleTrainedOnly => () =>
|
||||
_buffSettings.BuffTrainedSkillsOnly = !_buffSettings.BuffTrainedSkillsOnly;
|
||||
public Action ToggleAttributes => () =>
|
||||
_buffSettings.BuffAttributes = !_buffSettings.BuffAttributes;
|
||||
|
||||
private static double Step(double value, int direction) =>
|
||||
Math.Clamp(Math.Round(value + direction * 0.05, 2), 0.0, 1.0);
|
||||
|
||||
private static string Percent(double fraction) =>
|
||||
(fraction * 100).ToString("0", CultureInfo.InvariantCulture) + "%";
|
||||
|
||||
private static string OnOff(bool value) => value ? "on" : "off";
|
||||
|
||||
// ── the loop ──────────────────────────────────────────────────────────
|
||||
private void Announce(string text) =>
|
||||
_host.Automation.Chat.PostSystemMessage($"[MossTank] {text}");
|
||||
|
||||
private void StartOrStop()
|
||||
{
|
||||
// Logged unconditionally: "nothing happened when I clicked" is
|
||||
// ambiguous between the click never arriving and the click arriving and
|
||||
// declining to act. This line separates those two without guesswork.
|
||||
_host.Log.Info(
|
||||
$"MossTank: Buff clicked (running={_running}, inWorld={_host.Automation.IsAvailable})");
|
||||
|
||||
|
|
@ -108,27 +171,15 @@ internal sealed class MossTankPanel
|
|||
_plan = BuildPlan(automation);
|
||||
_planIndex = 0;
|
||||
_castThisPass = 0;
|
||||
_sinceLastCast = CastIntervalSeconds; // cast the first one immediately
|
||||
_sinceProgress = 0;
|
||||
_running = true;
|
||||
_status = _plan.Count == 0
|
||||
? "Checking…"
|
||||
: $"Buffing 0/{_plan.Count}…";
|
||||
_status = _plan.Count == 0 ? "Checking…" : $"Buffing 0/{_plan.Count}…";
|
||||
_host.Log.Info($"MossTank: pass started, {_plan.Count} buff(s) queued");
|
||||
Announce(_plan.Count == 0
|
||||
? "Buffing — checking what needs recasting."
|
||||
: $"Buffing — {_plan.Count} spell(s) to cast.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One chat line, tagged so it reads like a client notice rather than
|
||||
/// something the character said.
|
||||
/// </summary>
|
||||
private void Announce(string text)
|
||||
{
|
||||
_host.Automation.Chat.PostSystemMessage($"[MossTank] {text}");
|
||||
}
|
||||
|
||||
private void Stop(string status)
|
||||
{
|
||||
_running = false;
|
||||
|
|
@ -137,16 +188,13 @@ internal sealed class MossTankPanel
|
|||
_status = status;
|
||||
}
|
||||
|
||||
private List<PluginSpellInfo> BuildPlan(IAutomationSurface automation)
|
||||
{
|
||||
List<BuffLine> lines = BuffProfile.Build(automation.Spells.KnownSelfBuffs);
|
||||
return BuffPlan.Build(
|
||||
lines,
|
||||
private List<PluginSpellInfo> BuildPlan(IAutomationSurface automation) =>
|
||||
BuffPlan.Build(
|
||||
BuffProfile.Build(automation.Spells.KnownSelfBuffs),
|
||||
automation.Character.Skills,
|
||||
automation.Character.Attributes,
|
||||
automation.Character.ActiveEnchantments,
|
||||
_buffSettings);
|
||||
}
|
||||
|
||||
private Dictionary<uint, uint> SkillLevels(IAutomationSurface automation)
|
||||
{
|
||||
|
|
@ -169,9 +217,7 @@ internal sealed class MossTankPanel
|
|||
return;
|
||||
}
|
||||
|
||||
_sinceLastCast += elapsedSeconds;
|
||||
_sinceProgress += elapsedSeconds;
|
||||
|
||||
if (_sinceProgress > StallTimeoutSeconds)
|
||||
{
|
||||
Stop($"Stalled after {_castThisPass} cast(s).");
|
||||
|
|
@ -180,11 +226,12 @@ internal sealed class MossTankPanel
|
|||
return;
|
||||
}
|
||||
|
||||
if (_sinceLastCast < CastIntervalSeconds || automation.Magic.IsCasting)
|
||||
// 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;
|
||||
|
||||
// Vitals come first: a buff pass that runs itself out of mana and keeps
|
||||
// trying is worse than one that pauses to convert stamina.
|
||||
if (TryVitalUpkeep(automation))
|
||||
return;
|
||||
|
||||
|
|
@ -249,7 +296,6 @@ internal sealed class MossTankPanel
|
|||
}
|
||||
|
||||
_castThisPass++;
|
||||
_sinceLastCast = 0;
|
||||
_sinceProgress = 0;
|
||||
_planIndex = 0;
|
||||
_status = $"{label}: {spell.Name}";
|
||||
|
|
|
|||
|
|
@ -33,11 +33,13 @@ public sealed class MossTankPlugin : IAcDreamPlugin
|
|||
// Markup ships beside the plugin assembly, so it is found relative to
|
||||
// this DLL rather than the host's working directory -- plugins are
|
||||
// loaded from their own directory and the two are not the same.
|
||||
string markup = Path.Combine(
|
||||
Path.GetDirectoryName(typeof(MossTankPlugin).Assembly.Location) ?? ".",
|
||||
"mosstank.xml");
|
||||
string directory =
|
||||
Path.GetDirectoryName(typeof(MossTankPlugin).Assembly.Location) ?? ".";
|
||||
|
||||
_host.Ui.AddMarkupPanel(markup, _panel);
|
||||
// Two panels with complementary visible bindings stand in for a tab
|
||||
// control: only one is ever on screen, and switching is just an Action.
|
||||
_host.Ui.AddMarkupPanel(Path.Combine(directory, "mosstank.xml"), _panel);
|
||||
_host.Ui.AddMarkupPanel(Path.Combine(directory, "mosstank-settings.xml"), _panel);
|
||||
|
||||
_tick = _panel.OnTick;
|
||||
_host.Events.Tick += _tick;
|
||||
|
|
|
|||
|
|
@ -13,18 +13,25 @@ public enum VitalAction
|
|||
}
|
||||
|
||||
/// <summary>Thresholds for vital upkeep, following VTank's Recharge-* settings.</summary>
|
||||
public sealed record VitalSettings
|
||||
public sealed class VitalSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether to convert vitals at all. VTank does this by default through its
|
||||
/// Recharge-* thresholds, but it is surprising the first time a buff pass
|
||||
/// spends your stamina, so it is worth being able to turn off.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>Convert stamina to mana below this fraction of max mana.</summary>
|
||||
public double ManaFloor { get; init; } = 0.50;
|
||||
public double ManaFloor { get; set; } = 0.50;
|
||||
|
||||
/// <summary>Stop converting once mana is back above this fraction.</summary>
|
||||
public double ManaTarget { get; init; } = 0.85;
|
||||
public double ManaTarget { get; set; } = 0.85;
|
||||
|
||||
/// <summary>Refuse to drain stamina below this fraction — the conversion
|
||||
/// takes half your stamina, and stranding the character at zero is worse
|
||||
/// than being short of mana.</summary>
|
||||
public double StaminaFloor { get; init; } = 0.35;
|
||||
public double StaminaFloor { get; set; } = 0.35;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -53,6 +60,9 @@ public static class VitalPlan
|
|||
|
||||
public static VitalAction Decide(ICharacterInfo character, VitalSettings settings)
|
||||
{
|
||||
if (!settings.Enabled)
|
||||
return VitalAction.None;
|
||||
|
||||
double mana = Fraction(character.CurrentMana, character.MaxMana);
|
||||
double stamina = Fraction(character.CurrentStamina, character.MaxStamina);
|
||||
|
||||
|
|
|
|||
38
src/AcDream.Plugins.MossTank/mosstank-settings.xml
Normal file
38
src/AcDream.Plugins.MossTank/mosstank-settings.xml
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- MossTank's settings view. A second registered panel rather than a tab
|
||||
control: two panels with complementary visible bindings need no new markup
|
||||
vocabulary, and the toggle is just another Action.
|
||||
Adjuster buttons rather than typed fields, because editable text in a
|
||||
plugin panel needs keyboard routing plumbed through first. -->
|
||||
<panel x="40" y="120" w="360" h="286" title="MossTank — Settings" visible="{SettingsVisible}">
|
||||
<label x="12" y="30" text="{DifficultyText}" color="#FFE8E4C8" />
|
||||
<button x="292" y="26" w="26" h="22" text="-" onclick="{DifficultyDown}" />
|
||||
<button x="322" y="26" w="26" h="22" text="+" onclick="{DifficultyUp}" />
|
||||
|
||||
<label x="12" y="58" text="{RebuffText}" color="#FFE8E4C8" />
|
||||
<button x="292" y="54" w="26" h="22" text="-" onclick="{RebuffDown}" />
|
||||
<button x="322" y="54" w="26" h="22" text="+" onclick="{RebuffUp}" />
|
||||
|
||||
<label x="12" y="86" text="{ManaFloorText}" color="#FFB9C7A0" />
|
||||
<button x="292" y="82" w="26" h="22" text="-" onclick="{ManaFloorDown}" />
|
||||
<button x="322" y="82" w="26" h="22" text="+" onclick="{ManaFloorUp}" />
|
||||
|
||||
<label x="12" y="114" text="{ManaTargetText}" color="#FFB9C7A0" />
|
||||
<button x="292" y="110" w="26" h="22" text="-" onclick="{ManaTargetDown}" />
|
||||
<button x="322" y="110" w="26" h="22" text="+" onclick="{ManaTargetUp}" />
|
||||
|
||||
<label x="12" y="142" text="{StaminaFloorText}" color="#FFB9C7A0" />
|
||||
<button x="292" y="138" w="26" h="22" text="-" onclick="{StaminaFloorDown}" />
|
||||
<button x="322" y="138" w="26" h="22" text="+" onclick="{StaminaFloorUp}" />
|
||||
|
||||
<label x="12" y="172" text="{VitalUpkeepText}" color="#FF8F9C78" />
|
||||
<button x="292" y="168" w="56" h="22" text="toggle" onclick="{ToggleVitalUpkeep}" />
|
||||
|
||||
<label x="12" y="200" text="{TrainedOnlyText}" color="#FF8F9C78" />
|
||||
<button x="292" y="196" w="56" h="22" text="toggle" onclick="{ToggleTrainedOnly}" />
|
||||
|
||||
<label x="12" y="228" text="{AttributesText}" color="#FF8F9C78" />
|
||||
<button x="292" y="224" w="56" h="22" text="toggle" onclick="{ToggleAttributes}" />
|
||||
|
||||
<button x="12" y="252" w="108" h="26" text="Back" onclick="{CloseSettings}" />
|
||||
</panel>
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- MossTank's panel. Bindings resolve by property name against MossTankPanel:
|
||||
{Buff} is an Action bound to the button, {IsInWorld} hides the whole panel
|
||||
at character select, the rest are read every frame. -->
|
||||
<panel x="40" y="120" w="360" h="132" title="MossTank" visible="{IsInWorld}">
|
||||
<label x="12" y="30" text="{Vitals}" color="#FFB9C7A0" />
|
||||
<label x="12" y="50" text="{Coverage}" color="#FF8F9C78" />
|
||||
<label x="12" y="70" text="{Status}" color="#FFE8E4C8" />
|
||||
<button x="12" y="94" w="108" h="28" text="{ButtonText}" onclick="{Buff}" />
|
||||
<!-- MossTank's main panel. Bindings resolve by property name against
|
||||
MossTankPanel: {Buff}/{OpenSettings} are Actions, {MainVisible} hides the
|
||||
panel at character select and while the settings view is open. -->
|
||||
<panel x="40" y="120" w="360" h="132" title="MossTank" visible="{MainVisible}">
|
||||
<label x="12" y="30" text="{Vitals}" color="#FFB9C7A0" />
|
||||
<label x="12" y="50" text="{Coverage}" color="#FF8F9C78" />
|
||||
<label x="12" y="70" text="{Status}" color="#FFE8E4C8" />
|
||||
<button x="12" y="94" w="108" h="28" text="{ButtonText}" onclick="{Buff}" />
|
||||
<button x="128" y="94" w="108" h="28" text="Settings" onclick="{OpenSettings}" />
|
||||
</panel>
|
||||
|
|
|
|||
|
|
@ -102,6 +102,16 @@ public class VitalPlanTests
|
|||
Assert.Equal(1u, pick.SpellId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DoesNothingWhenVitalUpkeepIsTurnedOff()
|
||||
{
|
||||
// Converting stamina mid-buff surprises people, so it has to be a
|
||||
// setting rather than a behaviour.
|
||||
var character = new Character { CurrentMana = 1, CurrentStamina = 100 };
|
||||
var off = new VitalSettings { Enabled = false };
|
||||
Assert.Equal(VitalAction.None, VitalPlan.Decide(character, off));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReportsNoConversionWhenTheCharacterKnowsNone()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue