Initial import of BardFrontline
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
namespace BardFrontline.Core;
|
||||
|
||||
public sealed class Rotation
|
||||
{
|
||||
private static readonly (Skill Skill, uint Status)[] TimedAttacks =
|
||||
[(Skill.PitchPerfect, Statuses.Repertoire), (Skill.BlastArrow, Statuses.BlastReady), (Skill.EncoreOfLight, Statuses.EncoreReady)];
|
||||
public static bool IsExpiring(Skill skill, Actor self) => TimedAttacks.Any(p => p.Skill == skill && self.Remaining(p.Status) <= 3);
|
||||
|
||||
public ActionDecision? Decide(CombatSnapshot s, Settings c, IActionAvailability availability, bool emergencyOnly = false)
|
||||
{
|
||||
if (s.ManualControl || !s.Player.Alive || s.Player.Has(Statuses.Guard) || s.GuardPending) return null;
|
||||
var attackSnapshot = s;
|
||||
ActionDecision? Use(Skill skill, Actor target, string why, bool emergency = false) =>
|
||||
Strategy.PolicyReason(skill, s, c) == null && availability.CanUse(skill, target) ? new(skill, target.Id, why, emergency) : null;
|
||||
ActionDecision? Attack(Skill skill) => !c.AutoAttack || Strategy.PolicyReason(skill, s, c) != null ? null
|
||||
: TargetSelector.Select(attackSnapshot, c, skill, availability) is { } t ? new(skill, t.Id, "RSR 攻击优先级") : null;
|
||||
var self = s.Player;
|
||||
ActionDecision? d;
|
||||
if (!s.CleansePending && Statuses.Controls.Any(self.Has))
|
||||
{
|
||||
if ((d = Use(Skill.WardensPaean, self, "自身解控", true)) != null) return d;
|
||||
if (Statuses.Controls.Any(id => self.Has(id) && !c.DisabledPurifyStatuses.Contains(id))
|
||||
&& (d = Use(Skill.Purify, self, "净化控制", true)) != null) return d;
|
||||
}
|
||||
if (s.AliveSeconds >= 5)
|
||||
{
|
||||
if (s.InCombat && Strategy.GuardNeeded(s, c)
|
||||
&& (d = Use(Skill.Guard, self, Strategy.FocusDanger(s, c) ? "集火 / 快速掉血防御" : "低血量防御", true)) != null) return d;
|
||||
if ((long)self.MaxHp - self.Hp >= Strategy.HealDeficit(s, c)
|
||||
&& (d = Use(Skill.Recuperate, self, "恢复生命", true)) != null) return d;
|
||||
}
|
||||
if (emergencyOnly) return null;
|
||||
if (c.HelpParty && !s.CleansePending)
|
||||
{
|
||||
foreach (var member in s.Party.Where(a => a.Id != self.Id && a.Alive && a.Targetable && Statuses.Controls.Any(a.Has))
|
||||
.OrderBy(a => a.HpRatio).ThenBy(a => a.Distance).ThenBy(a => a.Id))
|
||||
if ((d = Use(Skill.WardensPaean, member, "队友解控")) != null) return d;
|
||||
}
|
||||
if (s.AliveSeconds >= 5 && !s.Moving && !TargetSelector.HasNearbyThreat(s)
|
||||
&& (self.HpRatio * 100 <= c.ElixirPercent || s.MaxMp > 0 && (double)s.Mp / s.MaxMp * 100 <= c.ElixirPercent)
|
||||
&& (d = Use(Skill.Elixir, self, "安全补给")) != null) return d;
|
||||
|
||||
// Finishers are attack decisions: survival always comes first. Guard is bypassed by Eagle itself.
|
||||
if (TargetSelector.EagleFinisher(s, c, availability) is { } finish)
|
||||
return new(Skill.EagleEyeShot, finish.Id, $"优先收割:有效 HP {finish.EffectiveHp:N0}");
|
||||
|
||||
if (c.AutoAttack)
|
||||
{
|
||||
attackSnapshot = s with { Enemies = TargetSelector.CombatCandidates(s, c, availability) };
|
||||
foreach (var proc in TimedAttacks.Where(p => IsExpiring(p.Skill, self)).OrderBy(p => self.Remaining(p.Status)))
|
||||
if ((d = Attack(proc.Skill)) != null) return d with { Reason = "优先消耗即将过期的技能状态" };
|
||||
// Consume the LB follow-up at the next legal action opportunity. Leaving it behind
|
||||
// every ready weaponskill and proc generator can starve it throughout the LB buff.
|
||||
if (self.Has(Statuses.EncoreReady) && (d = Attack(Skill.EncoreOfLight)) != null)
|
||||
return d with { Reason = "优先释放极限技后续:英雄的安可" };
|
||||
}
|
||||
|
||||
// Build the weapon choice first. Abilities weave only when the execution adapter permits it.
|
||||
ActionDecision? weapon = null;
|
||||
if (c.AutoAttack)
|
||||
{
|
||||
if (!self.Has(Statuses.March) && !self.Has(Statuses.BlastReady)) weapon = Attack(Skill.ApexArrow);
|
||||
if (c.Burst == BurstStrategy.Pressure)
|
||||
{
|
||||
// Spend ready follow-ups before draining multiple Harmonic charges.
|
||||
if (self.Has(Statuses.Repertoire)) weapon ??= Attack(Skill.PitchPerfect);
|
||||
if (self.Has(Statuses.BlastReady)) weapon ??= Attack(Skill.BlastArrow);
|
||||
}
|
||||
weapon ??= Attack(Skill.HarmonicArrow);
|
||||
if (self.Has(Statuses.Repertoire)) weapon ??= Attack(Skill.PitchPerfect);
|
||||
if (self.Has(Statuses.BlastReady)) weapon ??= Attack(Skill.BlastArrow);
|
||||
if (!self.Has(Statuses.BlastReady)) weapon ??= Attack(Skill.ApexArrow);
|
||||
if (!self.Has(Statuses.Repertoire)) weapon ??= Attack(Skill.PowerfulShot);
|
||||
}
|
||||
// Target existence is independent of recast/resources; checked by the adapter via the probe interface.
|
||||
var hasEnemy = s.Enemies.Any(a => Actions.All.Any(skill => TargetSelector.Relevant(skill, s, c)
|
||||
&& TargetSelector.Allowed(a, c, skill)
|
||||
&& (availability is ITargetProbe probe ? probe.CanTarget(skill, a) : true)));
|
||||
if (c.AutoAttack && s.InCombat && hasEnemy)
|
||||
{
|
||||
if (self.Has(Statuses.RoleBravery) && !self.Has(Statuses.Bravery)
|
||||
&& (d = Use(Skill.Bravery, self, "职能增益")) != null) return d;
|
||||
if (self.Has(Statuses.RoleDervish) && !self.Has(Statuses.Dervish)
|
||||
&& (d = Use(Skill.Dervish, self, "职能增益")) != null) return d;
|
||||
if (s.LimitFull && !self.Has(Statuses.Fantasia) && !self.Has(Statuses.EncoreReady)
|
||||
&& (d = Use(Skill.FinalFantasia, self, "极限技已满")) != null) return d;
|
||||
}
|
||||
// Pressure spends offensive cooldowns before Harmonic/filler, but keeps ready arrow follow-ups.
|
||||
// Repelling remains in its original fallback position so this mode does not force extra retreats.
|
||||
if (c.AutoAttack && c.Burst == BurstStrategy.Pressure
|
||||
&& (weapon == null || weapon.Skill is Skill.HarmonicArrow or Skill.PowerfulShot))
|
||||
{
|
||||
if (c.EagleStrategy == EagleStrategy.FinisherFirst && (d = Attack(Skill.EagleEyeShot)) != null)
|
||||
return d with { Reason = "主动爆发:提前使用鹰眼" };
|
||||
if ((d = Attack(Skill.SilentNocturne)) != null)
|
||||
return d with { Reason = "主动爆发:沉默衔接完美音调" };
|
||||
}
|
||||
// Normal mode preserves ready weapons over offensive weaving to prevent GCD starvation.
|
||||
if (weapon != null) return weapon;
|
||||
if (!self.Has(Statuses.Repertoire))
|
||||
{
|
||||
if ((d = Attack(Skill.RepellingShot)) != null) return d;
|
||||
if ((d = Attack(Skill.SilentNocturne)) != null) return d;
|
||||
}
|
||||
if (c.EagleStrategy == EagleStrategy.FinisherFirst && self.Has(Statuses.RoleEagle)
|
||||
&& (d = Attack(Skill.EagleEyeShot)) != null) return d;
|
||||
if (s.Moving && !hasEnemy && !self.Has(Statuses.Sprint)) return Use(Skill.Sprint, self, "移动冲刺");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public interface ITargetProbe { bool CanTarget(Skill skill, Actor target); }
|
||||
Reference in New Issue
Block a user