feat(mosstank): the button is Force Buff — always recast everything
Virindi Tank's own term: Force Buff recasts the lot rather than only what has lapsed. The button now does that, and says so. BuffPlan gained a force flag that skips the already-in-force check. Forcing means "ignore what is already up", NOT "ignore the settings" -- the trained- skill filter, the attribute toggle and the difficulty margin all still apply, and there is a test pinning that. The loop had to change shape for this. It used to re-derive the plan every tick and treat "plan is empty" as done, which works only because the ordinary plan shrinks as buffs land. A forced plan never shrinks -- that is the point -- so the same loop would have cast forever. A pass now captures a queue at the start and works through it by index, which is also cheaper: no rebuilding 80-odd buff lines every frame. A spell that will not go now advances the queue rather than blocking it. One missing component used to mean everything behind it waited for the stall timeout; now the status line names the refusal and the pass carries on. Solution builds clean; 14,440 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
54005a864c
commit
5ca4a63272
3 changed files with 77 additions and 33 deletions
|
|
@ -38,12 +38,18 @@ public sealed class BuffSettings
|
|||
/// </remarks>
|
||||
public static class BuffPlan
|
||||
{
|
||||
/// <param name="force">
|
||||
/// Virindi Tank's Force Buff: queue every wanted line at its best castable
|
||||
/// tier, ignoring what is already in force and how long it has left. The
|
||||
/// ordinary path skips buffs that are already covered; forcing does not.
|
||||
/// </param>
|
||||
public static List<PluginSpellInfo> Build(
|
||||
IReadOnlyList<BuffLine> lines,
|
||||
IReadOnlyList<PluginSkillInfo> skills,
|
||||
IReadOnlyList<PluginAttributeInfo> attributes,
|
||||
IReadOnlyList<PluginActiveEnchantment> active,
|
||||
BuffSettings settings)
|
||||
BuffSettings settings,
|
||||
bool force = false)
|
||||
{
|
||||
var trainedSkills = new Dictionary<string, PluginSkillInfo>(
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
|
@ -96,7 +102,8 @@ public static class BuffPlan
|
|||
if (!TryPickTier(line, skillLevels, settings, out PluginSpellInfo pick))
|
||||
continue;
|
||||
|
||||
if (inForce.TryGetValue(line.Family, out var held)
|
||||
if (!force
|
||||
&& inForce.TryGetValue(line.Family, out var held)
|
||||
&& held.Tier >= pick.Tier
|
||||
&& held.Seconds >= settings.RebuffWhenUnderSeconds)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -24,8 +24,12 @@ internal sealed class MossTankPanel
|
|||
private readonly BuffSettings _buffSettings = new();
|
||||
private readonly VitalSettings _vitalSettings = new();
|
||||
|
||||
private List<PluginSpellInfo> _plan = new();
|
||||
private int _planIndex;
|
||||
// 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 double _sinceProgress;
|
||||
private int _castThisPass;
|
||||
|
|
@ -44,7 +48,8 @@ internal sealed class MossTankPanel
|
|||
public bool MainVisible => _host.Automation.IsAvailable && !SettingsOpen;
|
||||
public bool SettingsVisible => _host.Automation.IsAvailable && SettingsOpen;
|
||||
|
||||
public string ButtonText => _running ? "Stop" : "Buff";
|
||||
/// <summary>The button is Force Buff; while a pass runs it cancels.</summary>
|
||||
public string ButtonText => _running ? "Stop" : "Force Buff";
|
||||
public string Status => _status;
|
||||
|
||||
/// <summary>Vitals line, using the same numbers the character panel shows.</summary>
|
||||
|
|
@ -168,33 +173,38 @@ internal sealed class MossTankPanel
|
|||
return;
|
||||
}
|
||||
|
||||
_plan = BuildPlan(automation);
|
||||
_planIndex = 0;
|
||||
// Always a force pass: the button is Virindi Tank's Force Buff, which
|
||||
// recasts everything rather than only what has lapsed.
|
||||
_queue = BuildPlan(automation, force: true);
|
||||
_queueIndex = 0;
|
||||
_castThisPass = 0;
|
||||
_sinceProgress = 0;
|
||||
_running = true;
|
||||
_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.");
|
||||
_running = _queue.Count > 0;
|
||||
_status = _queue.Count == 0
|
||||
? "Nothing to buff."
|
||||
: $"Force buffing 0/{_queue.Count}…";
|
||||
_host.Log.Info($"MossTank: force pass started, {_queue.Count} buff(s) queued");
|
||||
Announce(_queue.Count == 0
|
||||
? "Nothing to buff — no known self-buffs match your skills."
|
||||
: $"Force buffing — {_queue.Count} spell(s).");
|
||||
}
|
||||
|
||||
private void Stop(string status)
|
||||
{
|
||||
_running = false;
|
||||
_plan = new List<PluginSpellInfo>();
|
||||
_planIndex = 0;
|
||||
_queue = new List<PluginSpellInfo>();
|
||||
_queueIndex = 0;
|
||||
_status = status;
|
||||
}
|
||||
|
||||
private List<PluginSpellInfo> BuildPlan(IAutomationSurface automation) =>
|
||||
private List<PluginSpellInfo> BuildPlan(IAutomationSurface automation, bool force) =>
|
||||
BuffPlan.Build(
|
||||
BuffProfile.Build(automation.Spells.KnownSelfBuffs),
|
||||
automation.Character.Skills,
|
||||
automation.Character.Attributes,
|
||||
automation.Character.ActiveEnchantments,
|
||||
_buffSettings);
|
||||
_buffSettings,
|
||||
force);
|
||||
|
||||
private Dictionary<uint, uint> SkillLevels(IAutomationSurface automation)
|
||||
{
|
||||
|
|
@ -235,26 +245,20 @@ internal sealed class MossTankPanel
|
|||
if (TryVitalUpkeep(automation))
|
||||
return;
|
||||
|
||||
// Re-derive against current enchantments so anything that landed since
|
||||
// the pass began drops out rather than being cast twice.
|
||||
_plan = BuildPlan(automation);
|
||||
|
||||
if (_plan.Count == 0)
|
||||
if (_queueIndex >= _queue.Count)
|
||||
{
|
||||
Stop($"Done — {_castThisPass} cast(s).");
|
||||
_host.Log.Info($"MossTank: pass complete ({_castThisPass} cast)");
|
||||
Announce(_castThisPass == 0
|
||||
? "Already fully buffed."
|
||||
: $"Finished — {_castThisPass} spell(s) cast.");
|
||||
_host.Log.Info($"MossTank: force pass complete ({_castThisPass} cast)");
|
||||
Announce($"Finished — {_castThisPass} spell(s) cast.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_planIndex >= _plan.Count)
|
||||
_planIndex = 0;
|
||||
|
||||
PluginSpellInfo next = _plan[_planIndex];
|
||||
if (!TryCast(automation, next, $"Buffing {_castThisPass + 1}/{_plan.Count}"))
|
||||
_planIndex++;
|
||||
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, $"Force buffing {_castThisPass + 1}/{_queue.Count}");
|
||||
_queueIndex++;
|
||||
}
|
||||
|
||||
private bool TryVitalUpkeep(IAutomationSurface automation)
|
||||
|
|
@ -297,7 +301,6 @@ internal sealed class MossTankPanel
|
|||
|
||||
_castThisPass++;
|
||||
_sinceProgress = 0;
|
||||
_planIndex = 0;
|
||||
_status = $"{label}: {spell.Name}";
|
||||
_host.Log.Info($"MossTank: casting {spell.Name} (0x{spell.SpellId:X4})");
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -202,6 +202,40 @@ public class BuffPlanTests
|
|||
Assert.Equal(new uint[] { 2, 3, 1 }, plan.Select(s => s.SpellId).ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForceQueuesBuffsThatAreAlreadyInForce()
|
||||
{
|
||||
// Virindi Tank's Force Buff recasts everything rather than only what
|
||||
// has lapsed, which is what the Buff button does.
|
||||
var lines = Lines(
|
||||
Spell(1, 47, 4, "Increases the caster's Life Magic skill by 10 points.",
|
||||
difficulty: 100, school: LifeMagicSkill));
|
||||
var skills = new[] { Skill(LifeMagicSkill, "Life Magic", PluginSkillTraining.Trained) };
|
||||
var active = new[] { new PluginActiveEnchantment(1, 47, 4, 1800) };
|
||||
|
||||
Assert.Empty(BuffPlan.Build(lines, skills, Array.Empty<PluginAttributeInfo>(),
|
||||
active, Default));
|
||||
Assert.Single(BuffPlan.Build(lines, skills, Array.Empty<PluginAttributeInfo>(),
|
||||
active, Default, force: true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForceStillRespectsSkillAndTrainingFilters()
|
||||
{
|
||||
// Forcing means "ignore what is already up", not "ignore the settings".
|
||||
var lines = Lines(
|
||||
Spell(1, 71, 1, "Increases the caster's Leadership skill by 10 points."));
|
||||
|
||||
var plan = BuffPlan.Build(
|
||||
lines,
|
||||
new[] { Skill(35, "Leadership", PluginSkillTraining.Untrained) },
|
||||
Array.Empty<PluginAttributeInfo>(),
|
||||
Array.Empty<PluginActiveEnchantment>(),
|
||||
Default, force: true);
|
||||
|
||||
Assert.Empty(plan);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptySpellbookProducesNoPlan()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue