113 lines
7.0 KiB
C#
113 lines
7.0 KiB
C#
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); }
|