Initial import of BardFrontline

This commit is contained in:
2026-10-13 22:55:05 +08:00
commit f3ab7c58c3
24 changed files with 2917 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<Compile Include="../Core/*.cs" Link="Core/%(Filename)%(Extension)" />
</ItemGroup>
</Project>
+235
View File
@@ -0,0 +1,235 @@
using BardFrontline.Core;
using System.Numerics;
using System.Text.Json;
static class OptimizationTests
{
static Actor Self(params uint[] states) => new(1, "self", 60000, 60000, 0, false, true, false, 0, [.. states]);
static Actor Enemy(ulong id, uint hp = 30000) => new(id, $"enemy{id}", hp, 60000, 20, true, true, false, 0, []);
static CombatSnapshot Snap(params Actor[] enemies) => new(Self(), 10000, 10000, true, false, 10, false, enemies, [], 0);
static ActionDecision? Decide(CombatSnapshot s, Settings c, params Skill[] ready) => new Rotation().Decide(s, c, new Fake(ready));
static void SkillIs(Skill skill, ActionDecision? d) => Suite.Equal(skill, d!.Skill);
public static void Run(Suite suite)
{
suite.Run("Optimization defaults preserve automatic HP player-first finisher and backstep preferences", () =>
{
var c = JsonSerializer.Deserialize<Settings>("{\"EagleFinisherHp\":9000,\"HelpParty\":true}")!; c.Normalize();
Suite.True(c.AutoAttack && c.AttackObjectives && !c.IgnoreEnemyGuard && c.HelpParty);
Suite.True(!c.StableTargets && !c.DetectManualControl && !c.ManualRepelling && !c.FocusDefense && !c.PreferAoe);
Suite.Equal(BurstStrategy.Normal, c.Burst); Suite.Equal(EagleStrategy.FinisherFirst, c.EagleStrategy);
Suite.Equal(TargetOrder.LowestHp, c.TargetOrder); Suite.Equal(9000, c.EagleFinisherHp);
Suite.Equal(0, c.DisabledObjectives.Count);
});
suite.Run("New strategy settings serialize and malformed values normalize", () =>
{
var c = new Settings { StableTargets = true, DetectManualControl = true, ManualRepelling = true,
FocusDefense = true, PreferAoe = true, Burst = BurstStrategy.ManualWindow, DisabledObjectives = [ObjectiveKind.Core] };
var roundTrip = JsonSerializer.Deserialize<Settings>(JsonSerializer.Serialize(c))!;
Suite.True(roundTrip.ManualRepelling && roundTrip.DisabledObjectives.Contains(ObjectiveKind.Core));
c.TargetHoldSeconds = float.NaN; c.ManualControlSeconds = float.PositiveInfinity;
c.FocusAttackers = -1; c.BurstCharges = 50; c.AoeMinTargets = 0; c.Burst = (BurstStrategy)99; c.Normalize();
Suite.Equal(.6f, c.TargetHoldSeconds); Suite.Equal(3f, c.ManualControlSeconds);
Suite.Equal(2, c.FocusAttackers); Suite.Equal(4, c.BurstCharges); Suite.Equal(2, c.AoeMinTargets);
Suite.Equal(BurstStrategy.Normal, c.Burst);
});
suite.Run("Stable targeting absorbs small HP jitter but default remains instant", () =>
{
var s = Snap(Enemy(10, 21000), Enemy(11, 20000)) with { CurrentTarget = 10, TargetAge = .2 };
Suite.Equal(11ul, Decide(s, new(), Skill.PowerfulShot)!.TargetId);
Suite.Equal(10ul, Decide(s, new() { StableTargets = true }, Skill.PowerfulShot)!.TargetId);
Suite.Equal(11ul, Decide(s with { TargetAge = .7 }, new() { StableTargets = true }, Skill.PowerfulShot)!.TargetId);
});
suite.Run("Stable targeting yields immediately to major HP advantage or invalid targets", () =>
{
var c = new Settings { StableTargets = true };
var s = Snap(Enemy(10, 30000), Enemy(11, 20000)) with { CurrentTarget = 10, TargetAge = .1 };
Suite.Equal(11ul, Decide(s, c, Skill.PowerfulShot)!.TargetId);
foreach (var old in new[] { Enemy(10, 19000) with { Dead = true }, Enemy(10, 19000) with { Status = [Statuses.Guard] },
Enemy(10, 19000) with { Targetable = false } })
Suite.Equal(11ul, Decide(s with { Enemies = [old, Enemy(11, 20000)] }, c, Skill.PowerfulShot)!.TargetId);
});
suite.Run("Stability cannot retain an objective over a player or delay an Eagle finisher", () =>
{
var c = new Settings { StableTargets = true };
var s = Snap(Enemy(10, 100) with { IsObjective = true }, Enemy(11, 9000)) with { CurrentTarget = 10, TargetAge = .1 };
Suite.Equal(11ul, Decide(s, c, Skill.PowerfulShot)!.TargetId);
SkillIs(Skill.EagleEyeShot, Decide(s with { Player = Self(Statuses.RoleEagle) }, c, Skill.EagleEyeShot, Skill.PowerfulShot));
});
suite.Run("Planned and dispatched stable targets agree", () =>
{
var s = Snap(Enemy(10, 21000), Enemy(11, 20000)) with { CurrentTarget = 10, TargetAge = .1 };
var c = new Settings { StableTargets = true }; var f = new Fake(Skill.PowerfulShot);
Suite.Equal(10ul, TargetSelector.PlanTarget(s, c, f, f)!.Id);
Suite.Equal(10ul, new Rotation().Decide(s, c, f)!.TargetId);
});
suite.Run("Plugin target writes and native requests never trigger automatic takeover", () =>
{
var control = new ManualControl(); var c = new Settings { DetectManualControl = true };
control.Observe(0, 10, 1, false, c); control.OwnTarget(11, .1); control.OwnAction(2);
control.Observe(.2, 11, 2, false, c); Suite.True(!control.Active(.2));
control.Observe(.3, 11, 3, true, c); control.Observe(.4, 11, 3, false, c); Suite.True(!control.Active(.4));
});
suite.Run("Manual target and foreign action takeover has bounded expiry", () =>
{
var control = new ManualControl(); var c = new Settings { DetectManualControl = true, ManualControlSeconds = 3 };
control.Observe(0, 10, 65535, false, c); control.Observe(1, 11, 65535, false, c);
Suite.True(control.Active(3.9)); Suite.True(!control.Active(4));
control.Observe(5, 11, 0, false, c); Suite.True(control.Active(5));
control.Resume(); Suite.True(!control.Active(5));
});
suite.Run("Forced takeover persists across context resets but explicit resume clears it", () =>
{
var control = new ManualControl { Forced = true }; control.ResetObservations();
Suite.True(control.Active(100)); control.Resume(); Suite.True(!control.Active(100));
control.Observe(0, 10, 1, false, new()); control.Observe(1, 11, 2, false, new()); Suite.True(!control.Active(1));
});
suite.Run("Manual control pauses offense survival and target planning", () =>
{
var s = Snap(Enemy(10, 100)) with { ManualControl = true, Player = Self(Statuses.RoleEagle) with { Hp = 1000 } };
var f = new Fake(Actions.All);
Suite.True(new Rotation().Decide(s, new(), f) == null);
Suite.True(TargetSelector.PlanTarget(s, new(), f, f) == null);
});
suite.Run("Adaptive weave starts at legacy budget and learns observed confirmation latency", () =>
{
var w = new WeaveTiming(); Suite.Equal(.65f, w.Budget);
for (var i = 0; i < 3; i++) { w.Submitted(i * 2, false); w.Observe(i * 2 + .1, false, .6f, false, false); w.Observe(i * 2 + .8, true, 0, false, false); }
Suite.Equal(3, w.SampleCount); Suite.True(w.Budget > .8f && w.Budget < .9f);
Suite.True(!w.Allows(.75f, Skill.SilentNocturne, true)); Suite.True(w.Allows(.75f, Skill.SilentNocturne, false));
Suite.True(w.Allows(.1f, Skill.EncoreOfLight, true)); Suite.True(w.Allows(0, Skill.HarmonicArrow, true));
});
suite.Run("Weave learning ignores casts timeouts and pending queues", () =>
{
var w = new WeaveTiming(); w.Submitted(0, true); w.Observe(1, true, 0, false, false); Suite.Equal(0, w.SampleCount);
w.Submitted(2, false); w.Observe(5, false, 0, false, false); Suite.Equal(0, w.SampleCount);
w.Submitted(6, false); w.Observe(6.6, true, 0, true, false); Suite.Equal(0, w.SampleCount);
w.Observe(6.8, true, 0, false, false); Suite.Equal(1, w.SampleCount); w.Reset(); Suite.Equal(.65f, w.Budget);
});
suite.Run("Adaptive sample window is bounded and budget responds to recent samples", () =>
{
var w = new WeaveTiming();
for (var i = 0; i < 30; i++) { w.Submitted(i * 2, false); w.Observe(i * 2 + (i < 5 ? 1.4 : .55), true, 0, false, false); }
Suite.Equal(20, w.SampleCount); Suite.True(w.Budget < .65f && w.Budget >= .5f);
});
suite.Run("Rival Wings names classify all objective families without localized strings", () =>
{
foreach (var (id, kind) in new[] { (6857u, ObjectiveKind.Core), (6860u, ObjectiveKind.Tower), (6864u, ObjectiveKind.Mech),
(6871u, ObjectiveKind.Infantry), (7891u, ObjectiveKind.Mercenary), (7906u, ObjectiveKind.Mercenary), (99999u, ObjectiveKind.Other) })
Suite.Equal(kind, Battlefield.Classify(BattlefieldMode.RivalWings, true, false, id));
Suite.Equal(ObjectiveKind.Mech, Battlefield.Classify(BattlefieldMode.RivalWings, false, true, 0));
Suite.Equal(ObjectiveKind.Other, Battlefield.Classify(BattlefieldMode.Frontline, true, false, 6857));
Suite.Equal(ObjectiveKind.Ice, Battlefield.Classify(BattlefieldMode.Frontline, true, false, 4822));
});
suite.Run("Objective family switches affect attacks but never hide threats", () =>
{
foreach (var kind in Enum.GetValues<ObjectiveKind>())
{
var s = Snap(Enemy(10) with { IsObjective = true, ThreateningObjective = true, Kind = kind });
var c = new Settings { DisabledObjectives = [kind] };
Suite.True(Decide(s, c, Skill.PowerfulShot) == null); Suite.True(TargetSelector.HasNearbyThreat(s));
SkillIs(Skill.PowerfulShot, Decide(s, new(), Skill.PowerfulShot));
}
});
suite.Run("Original Harmonic behavior remains immediate and aligned mode has bounded hold", () =>
{
var s = Snap(Enemy(10)) with { HarmonicCharges = 1 };
SkillIs(Skill.HarmonicArrow, Decide(s, new(), Skill.HarmonicArrow, Skill.PowerfulShot));
var c = new Settings { Burst = BurstStrategy.AlignBuffs };
SkillIs(Skill.PowerfulShot, Decide(s, c, Skill.HarmonicArrow, Skill.PowerfulShot));
SkillIs(Skill.HarmonicArrow, Decide(s with { HarmonicReadySeconds = 4 }, c, Skill.HarmonicArrow));
SkillIs(Skill.HarmonicArrow, Decide(s with { HarmonicCharges = 3 }, c, Skill.HarmonicArrow));
SkillIs(Skill.HarmonicArrow, Decide(s with { Player = Self(Statuses.Bravery) }, c, Skill.HarmonicArrow));
});
suite.Run("Manual burst holds only declared burst skills and releases in window", () =>
{
var s = Snap(Enemy(10)) with { Player = Self(Statuses.RoleBravery), LimitFull = true };
var c = new Settings { Burst = BurstStrategy.ManualWindow };
SkillIs(Skill.PowerfulShot, Decide(s, c, Skill.HarmonicArrow, Skill.Bravery, Skill.FinalFantasia, Skill.PowerfulShot));
SkillIs(Skill.Bravery, Decide(s with { BurstWindow = true }, c, Skill.Bravery, Skill.PowerfulShot));
SkillIs(Skill.EncoreOfLight, Decide(s with { Player = Self(Statuses.EncoreReady) }, c, Skill.EncoreOfLight, Skill.PowerfulShot));
SkillIs(Skill.EagleEyeShot, Decide(s with { Player = Self(Statuses.RoleEagle), Enemies = [Enemy(10, 9000)] }, c, Skill.EagleEyeShot));
});
suite.Run("Aoe circle strategy chooses dense players only when enabled", () =>
{
var s = Snap(Enemy(10, 1000) with { Position = new(0, 0, 20) }, Enemy(11, 40000) with { Position = new(10, 0, 10) },
Enemy(12, 50000) with { Position = new(11, 0, 10) }) with
{ Player = Self(Statuses.EncoreReady), Shapes = new Dictionary<Skill, AoeShape> { [Skill.EncoreOfLight] = new(false, 5) } };
Suite.Equal(10ul, Decide(s, new(), Skill.EncoreOfLight)!.TargetId);
Suite.Equal(11ul, Decide(s, new() { PreferAoe = true }, Skill.EncoreOfLight)!.TargetId);
Suite.Equal(10ul, Decide(s, new() { PreferAoe = true, AoeMinTargets = 4 }, Skill.EncoreOfLight)!.TargetId);
});
suite.Run("Aoe line counts width range direction elevation and immunity", () =>
{
var aim = Enemy(10) with { Position = new(0, 0, 10) };
var s = Snap(aim, Enemy(11) with { Position = new(1, 0, 20) }, Enemy(12) with { Position = new(0, 0, -5) },
Enemy(13) with { Position = new(4, 0, 10) }, Enemy(14) with { Position = new(0, 8, 10) },
Enemy(15) with { Position = new(0, 0, 30) }, Enemy(16) with { Position = new(0, 0, 15), Invulnerable = true });
Suite.Equal(2, Strategy.AoeCount(s, new(), Skill.ApexArrow, aim, new(true, 25, 2)));
});
suite.Run("Aoe strategy never diverts from players to a denser objective pack", () =>
{
var s = Snap(Enemy(10) with { Position = new(0, 0, 20) }, Enemy(11) with { IsObjective = true }, Enemy(12) with { IsObjective = true })
with { Player = Self(Statuses.EncoreReady), Shapes = new Dictionary<Skill, AoeShape> { [Skill.EncoreOfLight] = new(false, 5) } };
Suite.Equal(10ul, Decide(s, new() { PreferAoe = true }, Skill.EncoreOfLight)!.TargetId);
});
suite.Run("Focus defense uses enemy target and cast signals without double-counting actors", () =>
{
var s = Snap(Enemy(10) with { TargetId = 1, CastTargetId = 1 }, Enemy(11) with { CastTargetId = 1 }, Enemy(12) with { TargetId = 1 },
Enemy(13) with { TargetId = 1, Enemy = false }, Enemy(14) with { TargetId = 1, Dead = true }, Enemy(15) with { TargetId = 1, Distance = 60 })
with { Player = Self() with { Hp = 23000 } };
Suite.Equal(3, Strategy.FocusCount(s));
SkillIs(Skill.Recuperate, Decide(s, new(), Skill.Guard, Skill.Recuperate));
SkillIs(Skill.Guard, Decide(s, new() { FocusDefense = true }, Skill.Guard, Skill.Recuperate));
Suite.True(Decide(s with { AliveSeconds = 4 }, new() { FocusDefense = true }, Skill.Guard, Skill.Recuperate) == null);
});
suite.Run("Focus pressure lowers healing threshold but never raises it or removes Guard protection", () =>
{
var s = Snap() with { Player = Self() with { Hp = 53000 }, RecentHpLossRatio = .3 };
Suite.True(Decide(s, new(), Skill.Recuperate) == null);
SkillIs(Skill.Recuperate, Decide(s, new() { FocusDefense = true }, Skill.Recuperate));
Suite.Equal(3000, Strategy.HealDeficit(s, new() { FocusDefense = true, RecuperateMissingHp = 3000 }));
Suite.True(Decide(s with { Player = Self(Statuses.Guard) with { Hp = 53000 } }, new() { FocusDefense = true }, Skill.Recuperate) == null);
});
suite.Run("HP pressure expires after two seconds and ignores healing increments", () =>
{
var p = new HealthPressure(); Suite.Equal(0d, p.Observe(0, 60000, 60000));
Suite.Equal(.25, p.Observe(.5, 45000, 60000)); Suite.Equal(.25, p.Observe(1, 55000, 60000));
Suite.Equal(0d, p.Observe(2.6, 55000, 60000)); p.Reset(); Suite.Equal(0d, p.Observe(3, 1000, 60000));
});
suite.Run("Manual backstep switch leaves the rest of the rotation and skill toggle intact", () =>
{
var s = Snap(Enemy(10) with { Distance = 8 });
SkillIs(Skill.RepellingShot, Decide(s, new(), Skill.RepellingShot, Skill.SilentNocturne));
SkillIs(Skill.SilentNocturne, Decide(s, new() { ManualRepelling = true }, Skill.RepellingShot, Skill.SilentNocturne));
Suite.True(Decide(s, new() { DisabledSkills = [Skill.RepellingShot] }, Skill.RepellingShot) == null);
});
suite.Run("Decision audit contains every skill and does not carry stale results to another frame", () =>
{
var s = Snap(Enemy(10)); var c = new Settings();
var selected = Decide(s, c, Skill.PowerfulShot);
var rows = DecisionAudit.Evaluate(s, c, selected, "运行", (_, _) => "冷却");
Suite.Equal(Actions.All.Length, rows.Count); Suite.True(rows.Single(r => r.Skill == Skill.PowerfulShot).Result.Contains("本轮选中"));
Suite.True(rows.Single(r => r.Skill == Skill.EncoreOfLight).Result.Contains("4312"));
var paused = DecisionAudit.Evaluate(null, c, null, "非支持区域", (_, _) => throw new Exception("must not probe"));
Suite.True(paused.All(r => r.TargetId == 0 && r.Result == "非支持区域"));
});
suite.Run("Decision audit distinguishes toggle policy native failure and higher priority selection", () =>
{
var s = Snap(Enemy(10) with { Distance = 8 }) with { Player = Self(Statuses.EncoreReady) };
var c = new Settings { ManualRepelling = true, DisabledSkills = [Skill.HarmonicArrow] };
var selected = Decide(s, c, Skill.EncoreOfLight);
var rows = DecisionAudit.Evaluate(s, c, selected, "运行", (skill, _) => skill == Skill.SilentNocturne ? "原生状态 582" : null);
Suite.True(rows.Single(r => r.Skill == Skill.HarmonicArrow).Result.Contains("关闭"));
Suite.True(rows.Single(r => r.Skill == Skill.RepellingShot).Result.Contains("手动"));
Suite.True(rows.Single(r => r.Skill == Skill.SilentNocturne).Result.Contains("582"));
Suite.True(rows.Single(r => r.Skill == Skill.PowerfulShot).Result.Contains("优先选择"));
});
suite.Run("Audit never claims Sprint ready with a valid attack target", () =>
{
var s = Snap(Enemy(10)) with { Moving = true };
var rows = DecisionAudit.Evaluate(s, new(), null, "等待", (_, _) => null, (_, _) => true);
Suite.True(rows.Single(r => r.Skill == Skill.Sprint).Result.Contains("不自动冲刺"));
});
}
}
+126
View File
@@ -0,0 +1,126 @@
using BardFrontline.Core;
using System.Text.Json;
static class PressureTests
{
static Actor Self(params uint[] states) => new(1, "self", 60000, 60000, 0, false, true, false, 0, [.. states]);
static Actor Enemy(ulong id = 10, uint hp = 30000) => new(id, "enemy", hp, 60000, 18, true, true, false, 0, []);
static CombatSnapshot Snap(params uint[] states) => new(Self(states), 10000, 10000, true, false, 10, false, [Enemy()], [], 0);
static Settings Config() => new() { Burst = BurstStrategy.Pressure };
static ActionDecision? Decide(CombatSnapshot s, Settings c, params Skill[] ready) => new Rotation().Decide(s, c, new Fake(ready));
static void Is(Skill skill, ActionDecision? d) => Suite.Equal(skill, d?.Skill);
public static void Run(Suite suite)
{
suite.Run("Pressure opt-in preserves serialized modes and original default", () =>
{
Suite.Equal(BurstStrategy.Normal, new Settings().Burst);
foreach (var mode in Enum.GetValues<BurstStrategy>())
{
var c = JsonSerializer.Deserialize<Settings>(JsonSerializer.Serialize(new Settings { Burst = mode }))!;
c.Normalize(); Suite.Equal(mode, c.Burst);
}
Suite.Equal(0, (int)BurstStrategy.Normal); Suite.Equal(1, (int)BurstStrategy.AlignBuffs);
Suite.Equal(2, (int)BurstStrategy.ManualWindow);
});
suite.Run("Pressure releases regular Eagle before Harmonic and filler without changing default", () =>
{
var s = Snap(Statuses.March, Statuses.RoleEagle);
Skill[] ready = [Skill.HarmonicArrow, Skill.PowerfulShot, Skill.EagleEyeShot];
Is(Skill.HarmonicArrow, Decide(s, new(), ready));
Is(Skill.EagleEyeShot, Decide(s, Config(), ready));
});
suite.Run("Pressure uses silence before filler and still avoids overwriting Repertoire", () =>
{
Is(Skill.PowerfulShot, Decide(Snap(), new(), Skill.PowerfulShot, Skill.SilentNocturne));
Is(Skill.SilentNocturne, Decide(Snap(), Config(), Skill.PowerfulShot, Skill.SilentNocturne));
Is(Skill.HarmonicArrow, Decide(Snap(Statuses.Repertoire), Config(), Skill.SilentNocturne, Skill.HarmonicArrow));
});
suite.Run("Pressure consumes ready procs before multiple Harmonic charges and normal Eagle", () =>
{
foreach (var (status, skill) in new[] { (Statuses.Repertoire, Skill.PitchPerfect), (Statuses.BlastReady, Skill.BlastArrow) })
{
var s = Snap(Statuses.March, status, Statuses.RoleEagle) with { HarmonicCharges = 4 };
Is(skill, Decide(s, Config(), skill, Skill.HarmonicArrow, Skill.EagleEyeShot));
Is(Skill.HarmonicArrow, Decide(s, new(), skill, Skill.HarmonicArrow, Skill.EagleEyeShot));
}
});
suite.Run("Pressure preserves opener buffs Encore and Eagle finisher precedence", () =>
{
Is(Skill.ApexArrow, Decide(Snap(Statuses.RoleEagle), Config(), Skill.ApexArrow, Skill.EagleEyeShot, Skill.HarmonicArrow));
Is(Skill.Bravery, Decide(Snap(Statuses.RoleBravery, Statuses.RoleEagle), Config(), Skill.Bravery, Skill.EagleEyeShot));
Is(Skill.FinalFantasia, Decide(Snap(Statuses.RoleEagle) with { LimitFull = true }, Config(), Skill.FinalFantasia, Skill.EagleEyeShot));
Is(Skill.EncoreOfLight, Decide(Snap(Statuses.EncoreReady, Statuses.RoleEagle), Config(), Skill.EncoreOfLight, Skill.EagleEyeShot));
Is(Skill.EagleEyeShot, Decide(Snap(Statuses.EncoreReady, Statuses.RoleEagle) with { Enemies = [Enemy(hp: 9000)] },
Config(), Skill.EncoreOfLight, Skill.EagleEyeShot));
});
suite.Run("Pressure respects strict Eagle reservation and unavailable ability fallback", () =>
{
var s = Snap(Statuses.RoleEagle);
var c = Config(); c.EagleStrategy = EagleStrategy.FinisherOnly;
Is(Skill.PowerfulShot, Decide(s, c, Skill.PowerfulShot, Skill.EagleEyeShot));
c = Config(); c.DisabledSkills = [Skill.EagleEyeShot, Skill.SilentNocturne];
Is(Skill.PowerfulShot, Decide(s, c, Skill.PowerfulShot, Skill.EagleEyeShot, Skill.SilentNocturne));
var f = new Fake(Skill.EagleEyeShot, Skill.PowerfulShot); f.Charges[Skill.EagleEyeShot] = 0;
Is(Skill.PowerfulShot, new Rotation().Decide(s, Config(), f));
});
suite.Run("Pressure never holds charges or buffs for alignment", () =>
{
var s = Snap() with { HarmonicCharges = 1, HarmonicReadySeconds = 0 };
foreach (var skill in new[] { Skill.HarmonicArrow, Skill.Bravery, Skill.Dervish, Skill.FinalFantasia })
Suite.True(Strategy.HoldReason(skill, s, Config()) == null);
Is(Skill.HarmonicArrow, Decide(s, Config(), Skill.HarmonicArrow, Skill.PowerfulShot));
});
suite.Run("Pressure waives extra instant damage weave margin but excludes casts and backstep", () =>
{
var w = new WeaveTiming();
for (var i = 0; i < 3; i++) { w.Submitted(i * 2, false); w.Observe(i * 2 + 1.2, true, 0, false, false); }
foreach (var adaptive in new[] { true, false })
foreach (var skill in new[] { Skill.HarmonicArrow, Skill.PitchPerfect, Skill.BlastArrow, Skill.ApexArrow,
Skill.EagleEyeShot, Skill.SilentNocturne, Skill.Bravery, Skill.Dervish, Skill.FinalFantasia })
{
Suite.True(!w.Allows(.1f, skill, adaptive));
Suite.True(w.Allows(.1f, skill, adaptive, BurstStrategy.Pressure));
Suite.True(!w.Allows(.1f, skill, adaptive, BurstStrategy.Pressure, casting: true));
}
Suite.True(!w.Allows(.1f, Skill.RepellingShot, true, BurstStrategy.Pressure));
Suite.True(!w.Allows(.1f, Skill.PowerfulShot, true, BurstStrategy.Pressure));
});
suite.Run("Pressure retains survival Guard manual pause and attack switches", () =>
{
var s = Snap(Statuses.RoleEagle);
Is(Skill.Guard, Decide(s with { Player = Self(Statuses.RoleEagle) with { Hp = 8000 } },
Config(), Skill.Guard, Skill.EagleEyeShot));
Is(Skill.Recuperate, Decide(s with { Player = Self(Statuses.RoleEagle) with { Hp = 40000 } },
Config(), Skill.Recuperate, Skill.EagleEyeShot));
Suite.True(Decide(s with { ManualControl = true }, Config(), Skill.EagleEyeShot) == null);
Suite.True(Decide(s with { Player = Self(Statuses.RoleEagle, Statuses.Guard) }, Config(), Skill.EagleEyeShot) == null);
var c = Config(); c.AutoAttack = false;
Suite.True(Decide(s, c, Skill.EagleEyeShot, Skill.SilentNocturne) == null);
});
suite.Run("Pressure keeps player-first targeting and Eagle off objectives", () =>
{
var objective = Enemy(11, 1000) with { IsObjective = true };
var s = Snap(Statuses.RoleEagle) with { Enemies = [objective, Enemy()] };
Suite.Equal(10ul, Decide(s, Config(), Skill.EagleEyeShot, Skill.HarmonicArrow)!.TargetId);
Is(Skill.HarmonicArrow, Decide(s with { Enemies = [objective] }, Config(), Skill.EagleEyeShot, Skill.HarmonicArrow));
});
suite.Run("Pressure does not move backstep ahead of filler or override manual backstep", () =>
{
var s = Snap() with { Enemies = [Enemy() with { Distance = 8 }] };
Is(Skill.PowerfulShot, Decide(s, Config(), Skill.RepellingShot, Skill.PowerfulShot));
var c = Config(); c.ManualRepelling = true;
Suite.True(Decide(s, c, Skill.RepellingShot) == null);
});
suite.Run("Pressure burst falls back to filler after spending cooldowns and consuming proc", () =>
{
var s = Snap(Statuses.March, Statuses.RoleEagle);
Is(Skill.EagleEyeShot, Decide(s, Config(), Skill.EagleEyeShot, Skill.SilentNocturne, Skill.HarmonicArrow, Skill.PowerfulShot));
Is(Skill.SilentNocturne, Decide(s, Config(), Skill.SilentNocturne, Skill.HarmonicArrow, Skill.PowerfulShot));
Is(Skill.PitchPerfect, Decide(s with { Player = Self(Statuses.March, Statuses.RoleEagle, Statuses.Repertoire) },
Config(), Skill.PitchPerfect, Skill.HarmonicArrow));
Is(Skill.HarmonicArrow, Decide(s, Config(), Skill.HarmonicArrow, Skill.PowerfulShot));
Is(Skill.PowerfulShot, Decide(s, Config(), Skill.PowerfulShot));
});
}
}
+781
View File
@@ -0,0 +1,781 @@
using BardFrontline.Core;
var suite = new Suite();
var engine = new Rotation();
Actor Self(uint hp = 60000, params uint[] states) => new(1, "Self", hp, 60000, 0, false, true, false, 0, [.. states]);
Actor Enemy(ulong id = 10, uint hp = 30000, float distance = 20, params uint[] states) =>
new(id, "Enemy", hp, 60000, distance, true, true, false, 0, [.. states]);
CombatSnapshot Snap(Actor? player = null, params Actor[] enemies) =>
new(player ?? Self(), 10000, 10000, true, false, 10, false, enemies, [], 0);
ActionDecision? Decide(CombatSnapshot s, Fake f, Settings? c = null, bool emergency = false) => engine.Decide(s, c ?? new(), f, emergency);
void IsSkill(ActionDecision? d, Skill skill) => Suite.Equal(skill, d?.Skill);
void None(ActionDecision? d) => Suite.True(d == null, $"Expected no action, got {d}");
suite.Run("Default configuration and invalid persisted values", () =>
{
var c = new Settings();
Suite.True(c.AutoAttack && !c.IgnoreEnemyGuard && !c.HelpParty);
Suite.True(Actions.All.All(c.Enabled));
c.GuardHpPercent = float.NaN; c.ElixirPercent = float.PositiveInfinity;
c.RecuperateMissingHp = -10; c.TargetOrder = (TargetOrder)99; c.Normalize();
Suite.Equal(15f, c.GuardHpPercent); Suite.Equal(1, c.RecuperateMissingHp);
Suite.Equal(TargetOrder.LowestHp, c.TargetOrder);
});
suite.Run("Missing March prioritizes Apex over charged Harmonic", () =>
IsSkill(Decide(Snap(null, Enemy()), new(Skill.ApexArrow, Skill.HarmonicArrow, Skill.PowerfulShot)), Skill.ApexArrow));
suite.Run("March present prioritizes Harmonic", () =>
IsSkill(Decide(Snap(Self(60000, Statuses.March), Enemy()), new(Skill.ApexArrow, Skill.HarmonicArrow)), Skill.HarmonicArrow));
suite.Run("Harmonic is usable at each charge count, zero falls through", () =>
{
foreach (var count in new[] { 1, 2, 3, 4 })
{
var fake = new Fake(Skill.HarmonicArrow, Skill.PowerfulShot);
fake.Charges[Skill.HarmonicArrow] = count;
IsSkill(Decide(Snap(null, Enemy()), fake), Skill.HarmonicArrow);
}
var empty = new Fake(Skill.HarmonicArrow, Skill.PowerfulShot);
empty.Charges[Skill.HarmonicArrow] = 0;
IsSkill(Decide(Snap(null, Enemy()), empty), Skill.PowerfulShot);
});
suite.Run("Repertoire uses Pitch and prevents wasteful generators", () =>
{
IsSkill(Decide(Snap(Self(60000, Statuses.Repertoire), Enemy()), new(Skill.PitchPerfect, Skill.PowerfulShot)), Skill.PitchPerfect);
None(Decide(Snap(Self(60000, Statuses.Repertoire), Enemy()), new(Skill.RepellingShot, Skill.SilentNocturne, Skill.PowerfulShot)));
});
suite.Run("No Repertoire does not attempt Pitch", () =>
IsSkill(Decide(Snap(null, Enemy()), new(Skill.PitchPerfect, Skill.PowerfulShot)), Skill.PowerfulShot));
suite.Run("Blast ready consumes Blast and blocks Apex", () =>
IsSkill(Decide(Snap(Self(60000, Statuses.BlastReady), Enemy()), new(Skill.ApexArrow, Skill.BlastArrow)), Skill.BlastArrow));
suite.Run("No Blast ready falls through to Apex", () =>
IsSkill(Decide(Snap(Self(60000, Statuses.March), Enemy()), new(Skill.BlastArrow, Skill.ApexArrow)), Skill.ApexArrow));
suite.Run("Offensive abilities preserve RSR order", () =>
{
var s = Snap(Self(60000, Statuses.RoleEagle), Enemy(distance: 8));
IsSkill(Decide(s, new(Skill.RepellingShot, Skill.SilentNocturne, Skill.EagleEyeShot, Skill.EncoreOfLight)), Skill.RepellingShot);
IsSkill(Decide(s, new(Skill.SilentNocturne, Skill.EagleEyeShot, Skill.EncoreOfLight)), Skill.SilentNocturne);
IsSkill(Decide(s, new(Skill.EagleEyeShot, Skill.EncoreOfLight)), Skill.EagleEyeShot);
IsSkill(Decide(s with { Player = Self(60000, Statuses.EncoreReady) }, new(Skill.EncoreOfLight)), Skill.EncoreOfLight);
});
suite.Run("Repelling out of range falls through to Nocturne", () =>
IsSkill(Decide(Snap(null, Enemy(distance: 18)), new(Skill.RepellingShot, Skill.SilentNocturne)), Skill.SilentNocturne));
suite.Run("Ready weapons prevent offensive ability starvation of GCD", () =>
IsSkill(Decide(Snap(null, Enemy(distance: 8)), new(Skill.PowerfulShot, Skill.RepellingShot)), Skill.PowerfulShot));
suite.Run("Unavailable high priority skills fall through", () =>
IsSkill(Decide(Snap(null, Enemy()), new(Skill.PowerfulShot)), Skill.PowerfulShot));
suite.Run("Disabled offensive skills are never selected", () =>
{
var c = new Settings(); c.DisabledSkills.Add(Skill.HarmonicArrow);
IsSkill(Decide(Snap(null, Enemy()), new(Skill.HarmonicArrow, Skill.PowerfulShot), c), Skill.PowerfulShot);
});
suite.Run("Limit Break requires full gauge, combat and target", () =>
{
var f = new Fake(Skill.FinalFantasia);
var s = Snap(null, Enemy()) with { LimitFull = true };
IsSkill(Decide(s, f), Skill.FinalFantasia);
None(Decide(s with { LimitFull = false }, f));
None(Decide(s with { InCombat = false }, f));
None(Decide(s with { Enemies = [] }, f));
None(Decide(s with { Player = Self(60000, Statuses.Fantasia) }, f));
None(Decide(s with { Player = Self(60000, Statuses.EncoreReady) }, f));
});
suite.Run("Encore requires its status and uses hostile target", () =>
{
var f = new Fake(Skill.EncoreOfLight);
None(Decide(Snap(null, Enemy()), f));
var d = Decide(Snap(Self(60000, Statuses.EncoreReady), Enemy(33)), f);
IsSkill(d, Skill.EncoreOfLight); Suite.Equal(33ul, d!.TargetId);
});
suite.Run("Only equipped role actions are attempted", () =>
{
var f = new Fake(Skill.Bravery, Skill.Dervish, Skill.EagleEyeShot);
None(Decide(Snap(null, Enemy()), f));
var bravery = Decide(Snap(Self(60000, Statuses.RoleBravery), Enemy()), f);
IsSkill(bravery, Skill.Bravery); Suite.Equal(1ul, bravery!.TargetId);
IsSkill(Decide(Snap(Self(60000, Statuses.RoleDervish), Enemy()), f), Skill.Dervish);
IsSkill(Decide(Snap(Self(60000, Statuses.RoleEagle), Enemy()), f), Skill.EagleEyeShot);
None(Decide(Snap(Self(60000, Statuses.RoleBravery, Statuses.Bravery), Enemy()), f));
None(Decide(Snap(Self(60000, Statuses.RoleDervish, Statuses.Dervish), Enemy()), f));
});
suite.Run("Own Guard stops all actions including healing and purify", () =>
None(Decide(Snap(Self(1000, Statuses.Guard, 1343), Enemy()), new(Actions.All))));
suite.Run("Pending Guard stops actions before buff arrives", () =>
None(Decide(Snap(Self(1000, 1343), Enemy()) with { GuardPending = true }, new(Actions.All))));
suite.Run("Self cleanse priority is Paean then Purify then Guard", () =>
{
var s = Snap(Self(1000, 1343), Enemy());
IsSkill(Decide(s, new(Skill.WardensPaean, Skill.Purify, Skill.Guard, Skill.Recuperate)), Skill.WardensPaean);
IsSkill(Decide(s, new(Skill.Purify, Skill.Guard, Skill.Recuperate)), Skill.Purify);
IsSkill(Decide(s, new(Skill.Guard, Skill.Recuperate)), Skill.Guard);
});
suite.Run("All six cleanse statuses work, disabled Purify trigger is respected", () =>
{
foreach (var id in Statuses.Controls)
{
var s = Snap(Self(60000, id));
IsSkill(Decide(s, new(Skill.Purify)), Skill.Purify);
var c = new Settings(); c.DisabledPurifyStatuses.Add(id);
None(Decide(s, new(Skill.Purify), c));
IsSkill(Decide(s, new(Skill.WardensPaean), c), Skill.WardensPaean);
}
});
suite.Run("Paean acknowledgement delay does not immediately spend Purify", () =>
None(Decide(Snap(Self(60000, 1343)) with { CleansePending = true }, new(Skill.Purify))));
suite.Run("Successful cleanse stops retries after debuff disappears", () =>
None(Decide(Snap(), new(Skill.WardensPaean, Skill.Purify))));
suite.Run("Guard threshold and fallback when unavailable", () =>
{
IsSkill(Decide(Snap(Self(9000)), new(Skill.Guard, Skill.Recuperate)), Skill.Guard);
IsSkill(Decide(Snap(Self(9001)), new(Skill.Guard, Skill.Recuperate)), Skill.Recuperate);
IsSkill(Decide(Snap(Self(1000)), new(Skill.Recuperate)), Skill.Recuperate);
None(Decide(Snap(Self(1000)) with { InCombat = false }, new(Skill.Guard)));
});
suite.Run("Recuperate works without an enemy, respects deficit and available MP", () =>
{
IsSkill(Decide(Snap(Self(45000)), new(Skill.Recuperate)), Skill.Recuperate);
None(Decide(Snap(Self(45001)), new(Skill.Recuperate)));
var f = new Fake(Skill.Recuperate) { Mp = 1999 };
None(Decide(Snap(Self(30000)) with { Mp = 1999 }, f));
});
suite.Run("Five second revive protection includes boundary", () =>
{
var s = Snap(Self(1000)) with { AliveSeconds = 4.999 };
None(Decide(s, new(Skill.Guard, Skill.Recuperate, Skill.Elixir)));
IsSkill(Decide(s with { AliveSeconds = 5 }, new(Skill.Guard)), Skill.Guard);
IsSkill(Decide(s with { Player = Self(1000, 1343) }, new(Skill.Purify)), Skill.Purify);
});
suite.Run("Party Paean is opt-in and lowest HP ratio first", () =>
{
var ally = Enemy(20, 10000, 10, 1343) with { Enemy = false };
var other = Enemy(21, 20000, 5, 1345) with { Enemy = false };
var s = Snap() with { Party = [other, ally] };
None(Decide(s, new(Skill.WardensPaean)));
var d = Decide(s, new(Skill.WardensPaean), new() { HelpParty = true });
Suite.Equal(20ul, d!.TargetId);
IsSkill(Decide(s with { Player = Self(10000) }, new(Skill.WardensPaean, Skill.Recuperate), new() { HelpParty = true }), Skill.Recuperate);
});
suite.Run("Elixir stationary resource thresholds and no enemy requirement", () =>
{
var f = new Fake(Skill.Elixir);
IsSkill(Decide(Snap(Self(10000)), f), Skill.Elixir);
IsSkill(Decide(Snap() with { Mp = 1000 }, f), Skill.Elixir);
None(Decide(Snap(), f));
None(Decide(Snap(Self(10000)) with { Moving = true }, f));
});
suite.Run("Guard, invincible, untargetable and occluded enemies remain elixir threats", () =>
{
foreach (var enemy in new[] { Enemy(distance: 30), Enemy(distance: 30, states: [Statuses.Guard]),
Enemy(distance: 30, states: [1302]), Enemy(distance: 30) with { Targetable = false } })
None(Decide(Snap(Self(10000), enemy), new(Skill.Elixir) { Occluded = [10] }));
IsSkill(Decide(Snap(Self(10000), Enemy(distance: 31)), new(Skill.Elixir)), Skill.Elixir);
IsSkill(Decide(Snap(Self(10000), Enemy() with { Dead = true }), new(Skill.Elixir)), Skill.Elixir);
});
suite.Run("Sprint requires movement, no attack target, and no existing buff", () =>
{
var s = Snap() with { Moving = true };
IsSkill(Decide(s, new(Skill.Sprint)), Skill.Sprint);
None(Decide(s with { Moving = false }, new(Skill.Sprint)));
None(Decide(s with { Enemies = [Enemy()] }, new(Skill.Sprint)));
None(Decide(s with { Player = Self(60000, Statuses.Sprint) }, new(Skill.Sprint)));
});
suite.Run("Automatic attack off preserves common survival skills", () =>
{
var c = new Settings { AutoAttack = false };
None(Decide(Snap(null, Enemy()), new(Skill.PowerfulShot), c));
IsSkill(Decide(Snap(Self(30000), Enemy()), new(Skill.PowerfulShot, Skill.Recuperate), c), Skill.Recuperate);
});
suite.Run("Emergency-only cast probe never suggests offense or routine elixir", () =>
{
None(Decide(Snap(null, Enemy()), new(Actions.All), emergency: true));
None(Decide(Snap() with { Mp = 0 }, new(Skill.Elixir), emergency: true));
Suite.True(Decide(Snap(Self(30000)), new(Skill.Recuperate), emergency: true)!.Emergency);
});
suite.Run("Lowest HP across both hostile groups, not HP ratio or shield", () =>
{
var a = Enemy(11, 10000) with { MaxHp = 20000 };
var b = Enemy(12, 20000) with { MaxHp = 100000 };
var s = Snap(null, b, a);
Suite.Equal(11ul, Decide(s, new(Skill.PowerfulShot))!.TargetId);
Suite.Equal(12ul, Decide(s, new(Skill.PowerfulShot), new() { TargetOrder = TargetOrder.LowestHpRatio })!.TargetId);
});
suite.Run("HP ties use distance then current target then stable ID", () =>
{
var s = Snap(null, Enemy(12, distance: 10), Enemy(11, distance: 10), Enemy(10, distance: 15));
Suite.Equal(11ul, Decide(s, new(Skill.PowerfulShot))!.TargetId);
Suite.Equal(12ul, Decide(s with { CurrentTarget = 12 }, new(Skill.PowerfulShot))!.TargetId);
});
suite.Run("Nearest and mark ordering are independently selectable", () =>
{
var s = Snap(null, Enemy(11, 100, 20), Enemy(12, 50000, 10) with { AttackMarker = 2 },
Enemy(13, 60000, 15) with { AttackMarker = 1 });
Suite.Equal(12ul, Decide(s, new(Skill.PowerfulShot), new() { TargetOrder = TargetOrder.Nearest })!.TargetId);
Suite.Equal(13ul, Decide(s, new(Skill.PowerfulShot), new() { TargetOrder = TargetOrder.AttackMarker })!.TargetId);
});
suite.Run("Invalid candidates do not block valid next candidate", () =>
{
var good = Enemy(99, 40000);
var s = Snap(null, Enemy(10, 1) with { Dead = true }, Enemy(11, 2) with { Targetable = false },
Enemy(12, 3) with { Enemy = false }, Enemy(13, 4, 80), Enemy(14, 5), good);
Suite.Equal(99ul, Decide(s, new(Skill.PowerfulShot) { Occluded = [14] })!.TargetId);
});
suite.Run("Guard off excludes normal attacks, on allows them", () =>
{
var s = Snap(null, Enemy(states: [Statuses.Guard]));
None(Decide(s, new(Skill.PowerfulShot)));
IsSkill(Decide(s, new(Skill.PowerfulShot), new() { IgnoreEnemyGuard = true }), Skill.PowerfulShot);
});
suite.Run("Eagle Eye bypasses Guard but not invincibility", () =>
{
var s = Snap(Self(60000, Statuses.RoleEagle), Enemy(states: [Statuses.Guard]));
IsSkill(Decide(s, new(Skill.EagleEyeShot)), Skill.EagleEyeShot);
None(Decide(s with { Enemies = [Enemy(states: [1302, Statuses.Guard])] }, new(Skill.EagleEyeShot), new() { IgnoreEnemyGuard = true }));
});
suite.Run("All independent skill switches prevent corresponding request", () =>
{
var c = new Settings { DisabledSkills = Actions.All.ToHashSet(), HelpParty = true };
None(Decide(Snap(Self(1000, 1343, Statuses.RoleEagle, Statuses.EncoreReady), Enemy()), new(Actions.All), c));
});
suite.Run("Dead player cannot receive any decision", () => None(Decide(Snap(Self(0), Enemy()), new(Actions.All))));
suite.Run("Gate waits for new sequence acknowledgement, not stale ack", () =>
{
var g = new RequestGate(); g.Submitted(new(Skill.PowerfulShot, 10, "test"), 0, 10, 11);
Suite.True(g.Update(.5, 11, 10, false, false));
Suite.True(!g.Update(.6, 11, 11, false, false));
});
suite.Run("Gate handles delayed sequence increment", () =>
{
var g = new RequestGate(); g.Submitted(new(Skill.PowerfulShot, 10, "test"), 0, 10, 10);
Suite.True(g.Update(.5, 10, 10, false, false));
Suite.True(!g.Update(.6, 11, 11, false, false));
});
suite.Run("Gate sequence rollover", () =>
{
var g = new RequestGate(); g.Submitted(new(Skill.PowerfulShot, 10, "test"), 0, 65535, 0);
Suite.True(g.Update(.2, 0, 65535, false, false));
Suite.True(!g.Update(.3, 0, 0, false, false));
});
suite.Run("Guard and cleanse guard against delayed status replication", () =>
{
foreach (var skill in new[] { Skill.Guard, Skill.Purify, Skill.WardensPaean })
{
var g = new RequestGate(); g.Submitted(new(skill, 1, "test"), 0, 1, 2);
Suite.True(g.Update(.1, 2, 2, false, false));
Suite.True(!g.Update(.5, 2, 2, false, false));
}
});
suite.Run("Actual Guard or changed debuff confirms pending status action", () =>
{
var g = new RequestGate(); g.Submitted(new(Skill.Guard, 1, "test"), 0, 1, 2);
Suite.True(!g.Update(.1, 2, 1, true, false));
g.Submitted(new(Skill.WardensPaean, 1, "test"), 0, 1, 2);
Suite.True(!g.Update(.1, 2, 1, false, true));
});
suite.Run("Timeout causes bounded per-skill per-target backoff", () =>
{
var g = new RequestGate(); g.Submitted(new(Skill.PowerfulShot, 10, "test"), 0, 1, 2);
Suite.True(!g.Update(2, 2, 1, false, false));
Suite.True(g.IsBlocked(Skill.PowerfulShot, 10, 2.1));
Suite.True(!g.IsBlocked(Skill.PowerfulShot, 11, 2.1));
Suite.True(!g.IsBlocked(Skill.HarmonicArrow, 10, 2.1));
Suite.True(!g.IsBlocked(Skill.PowerfulShot, 10, 2.5));
});
suite.Run("Reset removes pending actions and stale backoff", () =>
{
var g = new RequestGate(); var d = new ActionDecision(Skill.Guard, 1, "test");
g.Fail(d, 1, "failure"); g.Submitted(d, 1, 1, 2); g.Reset();
Suite.True(g.Pending == null && !g.IsBlocked(Skill.Guard, 1, 1.1));
Suite.True(!g.Update(1.1, 2, 1, false, false));
});
suite.Run("Finisher Eagle preempts normal GCD, buffs and LB", () =>
{
var s = Snap(Self(60000, Statuses.RoleEagle), Enemy(hp: 9000)) with { LimitFull = true };
var d = Decide(s, new(Skill.EagleEyeShot, Skill.ApexArrow, Skill.FinalFantasia));
IsSkill(d, Skill.EagleEyeShot); Suite.True(d!.Reason.Contains("收割"));
});
suite.Run("Finisher changes target to lowest effective HP in Eagle range", () =>
{
var shielded = Enemy(10, 1000, 10) with { ShieldHp = 20000 };
var distant = Enemy(11, 8000, 38, Statuses.Guard);
var near = Enemy(12, 9000, 10);
var s = Snap(Self(60000, Statuses.RoleEagle), shielded, near, distant) with { CurrentTarget = 10 };
Suite.Equal(11ul, Decide(s, new(Skill.EagleEyeShot, Skill.PowerfulShot))!.TargetId);
});
suite.Run("Finisher eligibility includes shield, exact threshold, and invulnerability", () =>
{
var s = Snap(Self(60000, Statuses.RoleEagle), Enemy(hp: 8000) with { ShieldHp = 2000 });
var c = new Settings { EagleStrategy = EagleStrategy.FinisherOnly };
IsSkill(Decide(s, new(Skill.EagleEyeShot), c), Skill.EagleEyeShot);
None(Decide(s with { Enemies = [Enemy(hp: 8000) with { ShieldHp = 2001 }] }, new(Skill.EagleEyeShot), c));
None(Decide(s with { Enemies = [Enemy(hp: 8000) with { Invulnerable = true }] }, new(Skill.EagleEyeShot), c));
});
suite.Run("First strategy uses Eagle normally without finisher; reserve strategy holds it", () =>
{
var s = Snap(Self(60000, Statuses.RoleEagle), Enemy());
IsSkill(Decide(s, new(Skill.EagleEyeShot)), Skill.EagleEyeShot);
None(Decide(s, new(Skill.EagleEyeShot), new() { EagleStrategy = EagleStrategy.FinisherOnly }));
IsSkill(Decide(s, new(Skill.EagleEyeShot, Skill.PowerfulShot)), Skill.PowerfulShot);
});
suite.Run("Survival and shared native availability still take precedence over finishing", () =>
{
var s = Snap(Self(1000, Statuses.RoleEagle, 1343), Enemy(hp: 1000));
IsSkill(Decide(s, new(Skill.EagleEyeShot, Skill.Purify)), Skill.Purify);
IsSkill(Decide(s, new(Skill.EagleEyeShot, Skill.Guard)), Skill.Guard);
IsSkill(Decide(s, new(Skill.EagleEyeShot, Skill.Recuperate)), Skill.Recuperate);
IsSkill(Decide(s with { Player = Self(60000, Statuses.RoleEagle) }, new(Skill.PowerfulShot)), Skill.PowerfulShot);
None(Decide(s, new(Skill.EagleEyeShot), emergency: true));
});
suite.Run("Finisher setting is adjustable and does not change ordinary HP sorting", () =>
{
var s = Snap(Self(60000, Statuses.RoleEagle), Enemy(10, 11000), Enemy(11, 12000));
var c = new Settings { EagleFinisherHp = 11000, EagleStrategy = EagleStrategy.FinisherOnly };
IsSkill(Decide(s, new(Skill.EagleEyeShot, Skill.PowerfulShot), c), Skill.EagleEyeShot);
var shielded = Enemy(10, 5000) with { ShieldHp = 50000 };
Suite.Equal(10ul, Decide(s with { Enemies = [shielded, Enemy(11, 10000)] }, new(Skill.PowerfulShot))!.TargetId);
});
suite.Run("Eagle range counts as an attack target even while Eagle is on cooldown", () =>
{
var s = Snap(Self(60000, Statuses.RoleEagle), Enemy(distance: 38)) with { Moving = true };
None(Decide(s, new(Skill.Sprint)));
IsSkill(Decide(s with { Player = Self() }, new(Skill.Sprint)), Skill.Sprint);
});
suite.Run("Enabled only in a started Frontline match as living unmounted Bard", () =>
{
var active = new RuntimeConditions(true, true, false, BattlefieldMode.Frontline, true, false, true, true, false, false, true);
Suite.True(ExecutionPolicy.PauseReason(active) == null);
RuntimeConditions[] inactive = [active with { Enabled = false }, active with { LoggedIn = false },
active with { ChangingArea = true }, active with { Mode = BattlefieldMode.None }, active with { DutyStarted = false },
active with { DutyCompleted = true }, active with { Bard = false }, active with { Alive = false },
active with { Mounted = true }, active with { Occupied = true }, active with { CoreReady = false }];
foreach (var s in inactive) Suite.True(ExecutionPolicy.PauseReason(s) != null);
});
suite.Run("Dispatcher never overrides a queued action, animation lock, active cast, pending request or Guard", () =>
{
Suite.True(ExecutionPolicy.CanDispatch(false, 0, false, false, false));
Suite.True(!ExecutionPolicy.CanDispatch(true, 0, false, false, false));
Suite.True(!ExecutionPolicy.CanDispatch(false, .01f, false, false, false));
Suite.True(!ExecutionPolicy.CanDispatch(false, 0, true, false, false));
Suite.True(!ExecutionPolicy.CanDispatch(false, 0, false, true, false));
Suite.True(!ExecutionPolicy.CanDispatch(false, 0, false, false, true));
});
suite.Run("Weaving reserves shared GCD boundary without blocking off-GCD windows", () =>
{
Suite.True(ExecutionPolicy.CanWeave(0)); Suite.True(ExecutionPolicy.CanWeave(.65f));
Suite.True(ExecutionPolicy.CanWeave(2.4f)); Suite.True(!ExecutionPolicy.CanWeave(.64f));
});
suite.Run("Only matching action, target and native sequence establishes cast ownership", () =>
{
Suite.True(ExecutionPolicy.OwnsCast(29391, 10, 5, 29391, 10, 5));
Suite.True(!ExecutionPolicy.OwnsCast(29391, 10, 5, 29391, 10, 6));
Suite.True(!ExecutionPolicy.OwnsCast(29391, 10, 5, 29391, 11, 5));
Suite.True(!ExecutionPolicy.OwnsCast(29391, 10, 5, 29055, 10, 5));
});
suite.Run("Enemy Guard cancellation follows the same ignore switch as target selection", () =>
{
Suite.True(ExecutionPolicy.ShouldCancelCast(Skill.PowerfulShot, true, false, true, false, null, true, false));
Suite.True(!ExecutionPolicy.ShouldCancelCast(Skill.PowerfulShot, true, false, true, true, null, true, false));
Suite.True(ExecutionPolicy.ShouldCancelCast(Skill.PowerfulShot, true, true, false, true, null, true, false));
});
suite.Run("Manual casts are untouched; plugin offensive casts yield to survival", () =>
{
var heal = new ActionDecision(Skill.Recuperate, 1, "heal", true);
Suite.True(!ExecutionPolicy.ShouldCancelCast(Skill.PowerfulShot, false, true, true, false, heal, true, true));
Suite.True(ExecutionPolicy.ShouldCancelCast(Skill.PowerfulShot, true, false, false, false, heal, true, false));
});
suite.Run("Elixir completes safely when a routine heal comes off cooldown", () =>
{
var heal = new ActionDecision(Skill.Recuperate, 1, "heal", true);
Suite.True(!ExecutionPolicy.ShouldCancelCast(Skill.Elixir, true, false, false, false, heal, false, false));
Suite.True(ExecutionPolicy.ShouldCancelCast(Skill.Elixir, true, false, false, false, heal, true, false));
var purify = new ActionDecision(Skill.Purify, 1, "cleanse", true);
Suite.True(ExecutionPolicy.ShouldCancelCast(Skill.Elixir, true, false, false, false, purify, false, true));
});
suite.Run("Long elixir cast is not classified as request timeout", () =>
{
var g = new RequestGate(); g.Submitted(new(Skill.Elixir, 1, "drink"), 0, 1, 2);
Suite.True(g.Update(3, 2, 1, false, false, ownedCastInProgress: true));
Suite.True(!g.Update(4.5, 2, 2, false, false));
Suite.True(!g.IsBlocked(Skill.Elixir, 1, 4.5));
});
suite.Run("Guard is never classified as generic invulnerability", () =>
{
Suite.True(!Statuses.IsInvulnerability(Statuses.Guard, 15024));
Suite.True(Statuses.IsInvulnerability(1302, 0));
Suite.True(Statuses.IsInvulnerability(99999, 15024));
Suite.True(!Statuses.IsInvulnerability(Statuses.March, 0));
});
suite.Run("Target planning updates during acknowledgement wait without replacing accepted target", () =>
{
var gate = new RequestGate();
gate.Submitted(new(Skill.PowerfulShot, 10, "already sent"), 0, 1, 2);
var s = Snap(null, Enemy(10, 40000), Enemy(11, 5000)) with { CurrentTarget = 10 };
var f = new Fake(Skill.PowerfulShot);
Suite.True(gate.Update(.05, 2, 1, false, false));
Suite.Equal(11ul, TargetSelector.PlanTarget(s, new(), f, f)!.Id);
Suite.Equal(10ul, gate.Pending!.TargetId);
Suite.True(!ExecutionPolicy.CanDispatch(false, 0, false, gate.Pending != null, false));
});
suite.Run("Target planning follows current HP changes without waiting for target death", () =>
{
var f = new Fake(Skill.PowerfulShot);
var s = Snap(null, Enemy(10, 5000), Enemy(11, 10000));
Suite.Equal(10ul, TargetSelector.PlanTarget(s, new(), f, f)!.Id);
s = s with { Enemies = [Enemy(10, 20000), Enemy(11, 10000)], CurrentTarget = 10 };
Suite.Equal(11ul, TargetSelector.PlanTarget(s, new(), f, f)!.Id);
});
suite.Run("Guard-only target remains available to Eagle outside normal job range", () =>
{
var s = Snap(Self(60000, Statuses.RoleEagle), Enemy(22, 30000, 38, Statuses.Guard));
var f = new Fake(Skill.EagleEyeShot);
Suite.Equal(22ul, TargetSelector.PlanTarget(s, new(), f, f)!.Id);
IsSkill(Decide(s, f), Skill.EagleEyeShot);
});
suite.Run("Guard finisher planning preempts unguarded ordinary target", () =>
{
var s = Snap(Self(60000, Statuses.RoleEagle), Enemy(10, 7000) with { ShieldHp = 10000 }, Enemy(11, 8000, 38, Statuses.Guard));
var f = new Fake(Skill.EagleEyeShot, Skill.PowerfulShot);
Suite.Equal(11ul, TargetSelector.PlanTarget(s with { CurrentTarget = 10 }, new(), f, f)!.Id);
});
suite.Run("Eagle on cooldown does not lure ordinary attacks onto Guard", () =>
{
var s = Snap(Self(60000, Statuses.RoleEagle), Enemy(10, 5000, 20, Statuses.Guard), Enemy(11, 40000));
var f = new Fake(Skill.PowerfulShot);
Suite.Equal(11ul, TargetSelector.PlanTarget(s, new(), f, f)!.Id);
Suite.True(TargetSelector.PlanTarget(s with { Enemies = [s.Enemies[0]] }, new(), f, f) == null);
});
suite.Run("Target planning respects own Guard, auto-attack switch and manual Eagle disable", () =>
{
var s = Snap(Self(60000, Statuses.RoleEagle), Enemy(states: [Statuses.Guard]));
var f = new Fake(Skill.EagleEyeShot);
Suite.True(TargetSelector.PlanTarget(s, new() { AutoAttack = false }, f, f) == null);
Suite.True(TargetSelector.PlanTarget(s with { GuardPending = true }, new(), f, f) == null);
Suite.True(TargetSelector.PlanTarget(s with { Player = Self(60000, Statuses.Guard, Statuses.RoleEagle) }, new(), f, f) == null);
Suite.True(TargetSelector.PlanTarget(s, new() { DisabledSkills = [Skill.EagleEyeShot] }, f, f) == null);
});
Actor Ice(ulong id = 50, uint hp = 1000) => Enemy(id, hp, 10) with { IsObjective = true, Name = "Ice", MaxHp = 1000000 };
Actor TimedSelf(params (uint Status, float Seconds)[] procs) => Self(60000, procs.Select(p => p.Status).ToArray())
with { StatusRemaining = procs.ToDictionary(p => p.Status, p => p.Seconds) };
suite.Run("Objectives enabled by default and ordinary rotation attacks ice", () =>
{
Suite.True(new Settings().AttackObjectives);
var d = Decide(Snap(null, Ice()), new(Skill.PowerfulShot));
IsSkill(d, Skill.PowerfulShot); Suite.Equal(50ul, d!.TargetId);
IsSkill(Decide(Snap(Self(60000, Statuses.BlastReady), Ice()), new(Skill.BlastArrow)), Skill.BlastArrow);
});
suite.Run("Players outrank lower HP or marked ice under every sorting mode", () =>
{
foreach (var order in Enum.GetValues<TargetOrder>())
{
var s = Snap(null, Ice() with { AttackMarker = 1 }, Enemy(10, 50000, 20));
Suite.Equal(10ul, Decide(s, new(Skill.PowerfulShot), new() { TargetOrder = order })!.TargetId);
var f = new Fake(Skill.PowerfulShot);
Suite.Equal(10ul, TargetSelector.PlanTarget(s, new() { TargetOrder = order }, f, f)!.Id);
}
});
suite.Run("Global player priority prevents a higher priority ice skill preempting a player skill", () =>
{
var f = new Fake(Skill.ApexArrow, Skill.SilentNocturne) { Blocked = [(Skill.ApexArrow, 10)] };
var d = Decide(Snap(null, Ice(), Enemy()), f);
IsSkill(d, Skill.SilentNocturne); Suite.Equal(10ul, d!.TargetId);
});
suite.Run("Invalid players permit objective fallback", () =>
{
foreach (var player in new[] { Enemy() with { Dead = true }, Enemy() with { Targetable = false },
Enemy(distance: 80), Enemy(states: [Statuses.Guard]), Enemy() with { Invulnerable = true }, Enemy() })
{
var f = new Fake(Skill.PowerfulShot) { Occluded = player.Status.Count == 0 && player.Alive && player.Targetable && !player.Invulnerable && player.Distance == 20 ? [10] : [] };
Suite.Equal(50ul, Decide(Snap(null, player, Ice()), f)!.TargetId);
}
});
suite.Run("Eagle player priority follows readiness and equipped role", () =>
{
var s = Snap(Self(60000, Statuses.RoleEagle), Ice(), Enemy(10, 30000, 38, Statuses.Guard));
var d = Decide(s, new(Skill.EagleEyeShot, Skill.PowerfulShot));
IsSkill(d, Skill.EagleEyeShot); Suite.Equal(10ul, d!.TargetId);
Suite.Equal(50ul, Decide(s, new(Skill.PowerfulShot))!.TargetId);
Suite.Equal(50ul, Decide(s with { Player = Self() }, new(Skill.EagleEyeShot, Skill.PowerfulShot))!.TargetId);
Suite.Equal(50ul, Decide(s, new(Skill.EagleEyeShot, Skill.PowerfulShot), new() { EagleStrategy = EagleStrategy.FinisherOnly })!.TargetId);
});
suite.Run("Eagle never spends on objectives even in finisher range", () =>
{
foreach (var strategy in Enum.GetValues<EagleStrategy>())
{
var s = Snap(Self(60000, Statuses.RoleEagle), Ice());
var f = new Fake(Skill.EagleEyeShot);
var c = new Settings { EagleStrategy = strategy, DisabledSkills = Actions.All.Where(a => a != Skill.EagleEyeShot).ToHashSet() };
None(Decide(s, f, c));
Suite.True(TargetSelector.EagleFinisher(s, c, f) == null);
Suite.True(TargetSelector.PlanTarget(s, c, f, f) == null);
}
});
suite.Run("Objective switch excludes ice from offense and display planning", () =>
{
var s = Snap(null, Ice()); var c = new Settings { AttackObjectives = false }; var f = new Fake(Skill.PowerfulShot);
None(Decide(s, f, c)); Suite.True(TargetSelector.PlanTarget(s, c, f, f) == null);
Suite.Equal(10ul, Decide(s with { Enemies = [Ice(), Enemy()] }, f, c)!.TargetId);
IsSkill(Decide(s with { Player = Self(30000) }, new(Skill.Recuperate), c), Skill.Recuperate);
});
suite.Run("Dead inaccessible or immune objectives are skipped", () =>
{
foreach (var ice in new[] { Ice() with { Dead = true }, Ice() with { Targetable = false },
Ice() with { Invulnerable = true }, Ice() with { Hp = 0 }, Ice() with { Distance = 80 } })
None(Decide(Snap(null, ice), new(Skill.PowerfulShot)));
None(Decide(Snap(null, Ice()), new(Skill.PowerfulShot) { Occluded = [50] }));
});
suite.Run("Ice does not prevent safe elixir but protected players still do", () =>
{
var s = Snap(Self(10000), Ice());
IsSkill(Decide(s, new(Skill.Elixir, Skill.PowerfulShot)), Skill.Elixir);
Suite.True(!TargetSelector.HasNearbyThreat(s));
Suite.True(TargetSelector.HasNearbyThreat(s with { Enemies = [Ice(), Enemy(states: [Statuses.Guard])] }));
None(Decide(s with { Enemies = [Ice(), Enemy(states: [Statuses.Guard])] }, new(Skill.Elixir)));
});
suite.Run("Objective target planning returns to players as they enter range", () =>
{
var f = new Fake(Skill.PowerfulShot); var s = Snap(null, Ice(), Enemy(distance: 80));
Suite.Equal(50ul, TargetSelector.PlanTarget(s, new(), f, f)!.Id);
Suite.Equal(10ul, TargetSelector.PlanTarget(s with { Enemies = [Ice(), Enemy()], CurrentTarget = 50 }, new(), f, f)!.Id);
});
suite.Run("Display follows objective fallback when all player attacks are unavailable", () =>
{
var f = new Fake(Skill.PowerfulShot) { Blocked = [(Skill.PowerfulShot, 10)] };
var s = Snap(null, Ice(), Enemy());
Suite.Equal(50ul, Decide(s, f)!.TargetId);
Suite.Equal(50ul, TargetSelector.PlanTarget(s, new(), f, f)!.Id);
});
suite.Run("Expiring Pitch Blast and Encore preempt ordinary damage and LB", () =>
{
foreach (var (skill, status) in new[] { (Skill.PitchPerfect, Statuses.Repertoire),
(Skill.BlastArrow, Statuses.BlastReady), (Skill.EncoreOfLight, Statuses.EncoreReady) })
{
var s = Snap(TimedSelf((status, 3)), Enemy()) with { LimitFull = true };
IsSkill(Decide(s, new(skill, Skill.HarmonicArrow, Skill.FinalFantasia)), skill);
}
});
suite.Run("Nonexpiring or unknown proc duration preserves normal RSR ordering", () =>
{
foreach (var seconds in new[] { 3.01f, 10f, 0f, -1f, float.NaN })
IsSkill(Decide(Snap(TimedSelf((Statuses.Repertoire, seconds)), Enemy()), new(Skill.PitchPerfect, Skill.HarmonicArrow)), Skill.HarmonicArrow);
IsSkill(Decide(Snap(Self(60000, Statuses.Repertoire), Enemy()), new(Skill.PitchPerfect, Skill.HarmonicArrow)), Skill.HarmonicArrow);
});
suite.Run("Earliest expiration wins and unusable expiring skills fall through", () =>
{
var s = Snap(TimedSelf((Statuses.Repertoire, 2), (Statuses.BlastReady, 1), (Statuses.EncoreReady, .5f)), Enemy());
IsSkill(Decide(s, new(Skill.PitchPerfect, Skill.BlastArrow, Skill.EncoreOfLight)), Skill.EncoreOfLight);
IsSkill(Decide(s, new(Skill.PitchPerfect, Skill.BlastArrow)), Skill.BlastArrow);
IsSkill(Decide(s, new(Skill.HarmonicArrow, Skill.PitchPerfect), new() { DisabledSkills = [Skill.PitchPerfect] }), Skill.HarmonicArrow);
});
suite.Run("Survival and player Eagle finishers outrank expiration rescue", () =>
{
var s = Snap(TimedSelf((Statuses.RoleEagle, 100), (Statuses.Repertoire, 1)), Enemy(hp: 1000));
IsSkill(Decide(s, new(Skill.PitchPerfect, Skill.EagleEyeShot)), Skill.EagleEyeShot);
IsSkill(Decide(s with { Player = s.Player with { Hp = 30000 } }, new(Skill.PitchPerfect, Skill.Recuperate)), Skill.Recuperate);
None(Decide(s, new(Skill.PitchPerfect), new() { AutoAttack = false }));
});
suite.Run("Expiration rescue also damages objectives without defeating player priority", () =>
{
var s = Snap(TimedSelf((Statuses.BlastReady, 1)), Ice());
Suite.Equal(50ul, Decide(s, new(Skill.BlastArrow))!.TargetId);
Suite.Equal(10ul, Decide(s with { Enemies = [Ice(), Enemy()] }, new(Skill.BlastArrow))!.TargetId);
});
suite.Run("Shared recast identification includes additional groups", () =>
{
Suite.True(ExecutionPolicy.SharesGcd(57, -1, 57));
Suite.True(ExecutionPolicy.SharesGcd(10, 57, 57));
Suite.True(!ExecutionPolicy.SharesGcd(10, -1, 57));
Suite.True(!ExecutionPolicy.SharesGcd(-1, -1, -1));
});
suite.Run("Inactive or completed recasts cannot create a phantom weave lock", () =>
{
Suite.Equal(0f, ExecutionPolicy.RecastRemaining(false, 2.4f, 2));
Suite.True(ExecutionPolicy.CanWeave(ExecutionPolicy.RecastRemaining(false, 2.4f, 2)));
Suite.True(!ExecutionPolicy.CanWeave(ExecutionPolicy.RecastRemaining(true, 2.4f, 2)));
Suite.Equal(0f, ExecutionPolicy.RecastRemaining(true, 2.4f, 3));
});
suite.Run("Encore fires before ready filler and proc generators without waiting for expiry", () =>
{
var s = Snap(TimedSelf((Statuses.EncoreReady, 29)), Enemy(distance: 8));
var d = Decide(s, new(Skill.PowerfulShot, Skill.ApexArrow, Skill.HarmonicArrow, Skill.RepellingShot, Skill.SilentNocturne, Skill.EncoreOfLight));
IsSkill(d, Skill.EncoreOfLight);
Suite.True(d!.Reason.Contains("极限技后续"));
});
suite.Run("Encore cannot be starved by short weave windows under stacked haste", () =>
{
foreach (var remaining in new[] { .01f, .3f, .64f })
{
var f = new Fake(Skill.PowerfulShot, Skill.EncoreOfLight) { SharedGcdRemaining = remaining };
IsSkill(Decide(Snap(TimedSelf((Statuses.EncoreReady, 25)), Enemy()), f), Skill.EncoreOfLight);
Suite.True(!ExecutionPolicy.CanWeave(remaining, Skill.SilentNocturne));
}
});
suite.Run("Encore still yields to survival and Eagle finishers but outranks ordinary Eagle", () =>
{
var s = Snap(Self(60000, Statuses.EncoreReady, Statuses.RoleEagle), Enemy());
IsSkill(Decide(s, new(Skill.EncoreOfLight, Skill.EagleEyeShot)), Skill.EncoreOfLight);
IsSkill(Decide(s with { Enemies = [Enemy(hp: 9000)] }, new(Skill.EncoreOfLight, Skill.EagleEyeShot)), Skill.EagleEyeShot);
IsSkill(Decide(s with { Player = s.Player with { Hp = 30000 } }, new(Skill.EncoreOfLight, Skill.Recuperate)), Skill.Recuperate);
});
suite.Run("Encore cannot bypass its status toggle target or native availability", () =>
{
var s = Snap(Self(60000, Statuses.EncoreReady), Enemy());
IsSkill(Decide(s with { Player = Self() }, new(Skill.EncoreOfLight, Skill.PowerfulShot)), Skill.PowerfulShot);
IsSkill(Decide(s, new(Skill.EncoreOfLight, Skill.PowerfulShot), new() { DisabledSkills = [Skill.EncoreOfLight] }), Skill.PowerfulShot);
IsSkill(Decide(s, new(Skill.PowerfulShot)), Skill.PowerfulShot);
None(Decide(s with { Enemies = [Enemy(states: [Statuses.Guard])] }, new(Skill.EncoreOfLight)));
None(Decide(s with { Enemies = [Enemy(distance: 30)] }, new(Skill.EncoreOfLight)));
None(Decide(s, new(Skill.EncoreOfLight) { Occluded = [10] }));
None(Decide(s with { GuardPending = true }, new(Skill.EncoreOfLight)));
});
suite.Run("Encore uses players before ice and does not divert available player attacks", () =>
{
var s = Snap(Self(60000, Statuses.EncoreReady), Ice(), Enemy());
Suite.Equal(10ul, Decide(s, new(Skill.EncoreOfLight))!.TargetId);
var d = Decide(s, new(Skill.EncoreOfLight, Skill.PowerfulShot) { Blocked = [(Skill.EncoreOfLight, 10)] });
IsSkill(d, Skill.PowerfulShot); Suite.Equal(10ul, d!.TargetId);
Suite.Equal(50ul, Decide(s with { Enemies = [Ice()] }, new(Skill.EncoreOfLight))!.TargetId);
});
suite.Run("Encore remains usable at zero player MP and requires the transformed LB ID", () =>
{
var s = Snap(Self(60000, Statuses.EncoreReady), Enemy()) with { Mp = 0 };
IsSkill(Decide(s, new(Skill.EncoreOfLight) { Mp = 0 }), Skill.EncoreOfLight);
None(Decide(s, new(Skill.EncoreOfLight) { AdjustedIds = { [Skill.EncoreOfLight] = (uint)Skill.FinalFantasia } }));
});
suite.Run("All known Harmonic variants survive native ID validation and reach rotation", () =>
{
foreach (var id in new uint[] { 41464, 41465, 41466, 41964 })
{
var f = new Fake(Skill.HarmonicArrow, Skill.PowerfulShot) { AdjustedIds = { [Skill.HarmonicArrow] = id } };
IsSkill(Decide(Snap(null, Enemy()), f), Skill.HarmonicArrow);
f.Charges[Skill.HarmonicArrow] = 0;
IsSkill(Decide(Snap(null, Enemy()), f), Skill.PowerfulShot);
}
});
suite.Run("Harmonic variant acceptance does not permit unknown or unrelated transformations", () =>
{
foreach (var id in new uint[] { 0, 41965, (uint)Skill.EncoreOfLight })
IsSkill(Decide(Snap(null, Enemy()), new(Skill.HarmonicArrow, Skill.PowerfulShot)
{ AdjustedIds = { [Skill.HarmonicArrow] = id } }), Skill.PowerfulShot);
Suite.True(!Actions.Matches(Skill.PowerfulShot, (uint)Skill.PitchPerfect));
Suite.True(!Actions.Matches(Skill.ApexArrow, (uint)Skill.BlastArrow));
Suite.True(!Actions.Matches(Skill.EncoreOfLight, 41964));
});
suite.Run("Battlefield mode identification supports Frontline and Rival Wings only", () =>
{
Suite.Equal(BattlefieldMode.Frontline, Battlefield.Identify(18, true));
Suite.Equal(BattlefieldMode.RivalWings, Battlefield.Identify(39, true));
foreach (var use in new uint[] { 0, 3, 12, 28, 37, 99 })
Suite.Equal(BattlefieldMode.None, Battlefield.Identify(use, true));
Suite.Equal(BattlefieldMode.None, Battlefield.Identify(39, false));
Suite.Equal(BattlefieldMode.None, Battlefield.Identify(18, false));
});
suite.Run("Rival Wings runs on foot and retains all ordinary pause gates", () =>
{
var active = new RuntimeConditions(true, true, false, BattlefieldMode.RivalWings, true, false, true, true, false, false, true);
Suite.True(ExecutionPolicy.PauseReason(active) == null);
foreach (var stopped in new[] { active with { Enabled = false }, active with { LoggedIn = false },
active with { ChangingArea = true }, active with { DutyStarted = false }, active with { DutyCompleted = true },
active with { Bard = false }, active with { Alive = false }, active with { Mounted = true },
active with { Occupied = true }, active with { CoreReady = false }, active with { Piloting = true } })
Suite.True(ExecutionPolicy.PauseReason(stopped) != null);
Suite.True(ExecutionPolicy.PauseReason(active with { Piloting = true })!.Contains("机甲"));
});
suite.Run("Mech detection covers condition status and content armor with mode isolation", () =>
{
Suite.True(Battlefield.IsPiloting(BattlefieldMode.RivalWings, true, false, 0));
Suite.True(Battlefield.IsPiloting(BattlefieldMode.RivalWings, false, true, 0));
Suite.True(Battlefield.IsPiloting(BattlefieldMode.RivalWings, false, false, 1));
Suite.True(!Battlefield.IsPiloting(BattlefieldMode.RivalWings, false, false, 0));
Suite.True(!Battlefield.IsPiloting(BattlefieldMode.Frontline, true, true, 1));
Suite.True(!Battlefield.IsPiloting(BattlefieldMode.None, true, true, 1));
});
suite.Run("Battle NPC candidates exclude pets companions owned summons and noncombat kinds", () =>
{
foreach (byte kind in new byte[] { 0, 1, 5 })
{
Suite.True(Battlefield.IsBattleNpcCandidate(kind, 0));
Suite.True(Battlefield.IsBattleNpcCandidate(kind, 0xE0000000));
Suite.True(!Battlefield.IsBattleNpcCandidate(kind, 12345));
}
foreach (byte kind in new byte[] { 2, 3, 4, 6, 7, 9 })
Suite.True(!Battlefield.IsBattleNpcCandidate(kind, 0));
});
suite.Run("Rival Wings hostile NPCs block elixir even behind walls or while untargetable", () =>
{
foreach (var npc in new[] { Ice() with { ThreateningObjective = true },
Ice() with { ThreateningObjective = true, Targetable = false },
Ice() with { ThreateningObjective = true, Invulnerable = true } })
{
var s = Snap(Self(10000), npc);
Suite.True(TargetSelector.HasNearbyThreat(s));
None(Decide(s, new(Skill.Elixir) { Occluded = [npc.Id] }, new() { AttackObjectives = false }));
}
});
suite.Run("Distant dead and friendly NPCs do not block elixir and ice behavior is unchanged", () =>
{
foreach (var npc in new[] { Ice(), Ice() with { ThreateningObjective = true, Distance = 31 },
Ice() with { ThreateningObjective = true, Dead = true }, Ice() with { ThreateningObjective = true, Enemy = false } })
IsSkill(Decide(Snap(Self(10000), npc), new(Skill.Elixir)), Skill.Elixir);
});
suite.Run("Players outrank enemy mech and NPC targets while Eagle avoids mechs", () =>
{
var mech = Enemy(50, 1000, 10, Statuses.Warmachina) with { IsObjective = true, ThreateningObjective = true };
var s = Snap(Self(60000, Statuses.RoleEagle), mech, Enemy(10, 50000));
Suite.Equal(10ul, Decide(s, new(Skill.PowerfulShot, Skill.EagleEyeShot))!.TargetId);
var onlyMech = s with { Enemies = [mech] };
IsSkill(Decide(onlyMech, new(Skill.PowerfulShot, Skill.EagleEyeShot)), Skill.PowerfulShot);
None(Decide(onlyMech, new(Skill.EagleEyeShot)));
None(Decide(onlyMech, new(Skill.PowerfulShot), new() { AttackObjectives = false }));
});
suite.Run("Unattackable structures and friendly battle NPCs cannot receive attacks", () =>
{
foreach (var npc in new[] { Ice() with { Enemy = false }, Ice() with { Invulnerable = true },
Ice() with { Targetable = false } })
None(Decide(Snap(null, npc with { ThreateningObjective = true }), new(Skill.PowerfulShot, Skill.HarmonicArrow)));
});
suite.Run("Rival Wings NPC threats let emergency healing interrupt unsafe elixir", () =>
{
var heal = new ActionDecision(Skill.Recuperate, 1, "heal", true);
var s = Snap(Self(10000), Ice() with { ThreateningObjective = true });
Suite.True(ExecutionPolicy.ShouldCancelCast(Skill.Elixir, true, false, false, false,
heal, TargetSelector.HasNearbyThreat(s), false));
Suite.True(!ExecutionPolicy.ShouldCancelCast(Skill.Elixir, true, false, false, false,
heal, TargetSelector.HasNearbyThreat(s with { Enemies = [Ice()] }), false));
});
OptimizationTests.Run(suite);
PressureTests.Run(suite);
suite.Finish();
sealed class Fake(params Skill[] ready) : IActionAvailability, ITargetProbe
{
public Dictionary<Skill, int> Charges { get; } = [];
public Dictionary<Skill, uint> AdjustedIds { get; } = [];
public float? SharedGcdRemaining { get; init; }
public HashSet<ulong> Occluded { get; init; } = [];
public HashSet<(Skill, ulong)> Blocked { get; init; } = [];
public int Mp { get; init; } = 10000;
public bool CanTarget(Skill skill, Actor a) => a.Alive && a.Targetable && !Occluded.Contains(a.Id)
&& (!Actions.IsHostile(skill) || a.Distance <= (skill == Skill.RepellingShot ? 10 : skill == Skill.SilentNocturne ? 20 : skill == Skill.EagleEyeShot ? 40 : 25));
public bool CanUse(Skill skill, Actor a) => ready.Contains(skill) && !Blocked.Contains((skill, a.Id)) && CanTarget(skill, a)
&& Actions.Matches(skill, AdjustedIds.GetValueOrDefault(skill, (uint)skill))
&& (SharedGcdRemaining is not { } remain || skill is not (Skill.EncoreOfLight or Skill.SilentNocturne or Skill.RepellingShot)
|| ExecutionPolicy.CanWeave(remain, skill))
&& (!Charges.TryGetValue(skill, out var count) || count > 0)
&& (skill is not (Skill.Recuperate or Skill.Purify) || Mp >= 2000);
}
sealed class Suite
{
private int passed, failed;
public void Run(string name, Action test)
{
try { test(); passed++; Console.WriteLine($"PASS {name}"); }
catch (Exception e) { failed++; Console.WriteLine($"FAIL {name}: {e.Message}"); }
}
public static void True(bool b, string message = "Assertion failed") { if (!b) throw new Exception(message); }
public static void Equal<T>(T expected, T actual) { if (!EqualityComparer<T>.Default.Equals(expected, actual)) throw new Exception($"Expected {expected}, got {actual}"); }
public void Finish() { Console.WriteLine($"{passed} passed, {failed} failed"); Environment.ExitCode = failed == 0 ? 0 : 1; }
}