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); }