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:
Erik 2026-08-20 19:17:32 +02:00
parent 54005a864c
commit 5ca4a63272
3 changed files with 77 additions and 33 deletions

View file

@ -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()
{