90 lines
6.5 KiB
C#
90 lines
6.5 KiB
C#
using System.Numerics;
|
||
namespace BardFrontline.Core;
|
||
|
||
public sealed record AoeShape(bool Line, float Radius, float HalfWidth = 2);
|
||
|
||
public static class Strategy
|
||
{
|
||
public static int FocusCount(CombatSnapshot s) => s.Enemies.Count(a => a.Enemy && a.Alive && a.Distance <= 30
|
||
&& (!a.IsObjective || a.ThreateningObjective) && (a.TargetId == s.Player.Id || a.CastTargetId == s.Player.Id));
|
||
public static bool FocusDanger(CombatSnapshot s, Settings c) => c.FocusDefense && s.InCombat
|
||
&& (FocusCount(s) >= c.FocusAttackers || s.RecentHpLossRatio >= .25);
|
||
public static bool GuardNeeded(CombatSnapshot s, Settings c) => s.Player.HpRatio * 100 <= c.GuardHpPercent
|
||
|| FocusDanger(s, c) && s.Player.HpRatio * 100 <= c.FocusGuardPercent;
|
||
public static int HealDeficit(CombatSnapshot s, Settings c) => FocusDanger(s, c)
|
||
? Math.Min(c.RecuperateMissingHp, 6000) : c.RecuperateMissingHp;
|
||
|
||
public static string? HoldReason(Skill skill, CombatSnapshot s, Settings c)
|
||
{
|
||
if (skill == Skill.RepellingShot && c.ManualRepelling) return "后跳由玩家手动释放";
|
||
var burstSkill = skill is Skill.HarmonicArrow or Skill.Bravery or Skill.Dervish or Skill.FinalFantasia;
|
||
if (!burstSkill || c.Burst is BurstStrategy.Normal or BurstStrategy.Pressure || s.BurstWindow) return null;
|
||
if (c.Burst == BurstStrategy.ManualWindow) return "保留爆发:等待手动开启爆发窗口";
|
||
if (skill != Skill.HarmonicArrow || s.HarmonicCharges >= c.BurstCharges || s.HarmonicReadySeconds >= c.BurstMaxHoldSeconds
|
||
|| s.Player.Has(Statuses.Bravery) || s.Player.Has(Statuses.Fantasia)
|
||
|| s.Enemies.Any(a => !a.IsObjective && TargetSelector.Allowed(a, c, skill) && a.Distance <= 25 && a.EffectiveHp <= 12000)) return null;
|
||
return $"等待爆发增益 / {c.BurstCharges} 充能(最多 {c.BurstMaxHoldSeconds:F1}s)";
|
||
}
|
||
|
||
public static int AoeCount(CombatSnapshot s, Settings c, Skill skill, Actor center, AoeShape shape)
|
||
{
|
||
var origin = new Vector2(s.Player.Position.X, s.Player.Position.Z);
|
||
var aim = new Vector2(center.Position.X, center.Position.Z);
|
||
var delta = aim - origin;
|
||
var direction = delta.LengthSquared() > .001f ? Vector2.Normalize(delta) : Vector2.Zero;
|
||
return s.Enemies.Count(a =>
|
||
{
|
||
if (!TargetSelector.Allowed(a, c, skill) || Math.Abs(a.Position.Y - center.Position.Y) > 5) return false;
|
||
var point = new Vector2(a.Position.X, a.Position.Z);
|
||
if (!shape.Line) return Vector2.Distance(point, aim) <= shape.Radius + a.HitboxRadius;
|
||
if (direction == Vector2.Zero) return a.Id == center.Id;
|
||
var along = Vector2.Dot(point - origin, direction);
|
||
return along >= 0 && along <= shape.Radius + a.HitboxRadius
|
||
&& Math.Abs(direction.X * (point.Y - origin.Y) - direction.Y * (point.X - origin.X)) <= shape.HalfWidth + a.HitboxRadius;
|
||
});
|
||
}
|
||
|
||
// Shared by execution and diagnostics: a skill not visited due to an earlier return still gets a current policy reason.
|
||
public static string? PolicyReason(Skill skill, CombatSnapshot s, Settings c)
|
||
{
|
||
if (!c.Enabled(skill)) return "技能已关闭";
|
||
if (s.ManualControl) return "手动接管:暂停自动动作";
|
||
if (!s.Player.Alive || s.GuardPending || s.Player.Has(Statuses.Guard)) return "死亡 / 自身防御,暂停自动动作";
|
||
if (!c.AutoAttack && (Actions.IsHostile(skill) || skill is Skill.FinalFantasia or Skill.Bravery or Skill.Dervish)) return "自动攻击已关闭";
|
||
var self = s.Player;
|
||
var status = skill switch { Skill.PitchPerfect => Statuses.Repertoire, Skill.BlastArrow => Statuses.BlastReady,
|
||
Skill.EncoreOfLight => Statuses.EncoreReady, Skill.EagleEyeShot => Statuses.RoleEagle,
|
||
Skill.Bravery => Statuses.RoleBravery, Skill.Dervish => Statuses.RoleDervish, _ => 0u };
|
||
if (status != 0 && !self.Has(status)) return $"缺少前置状态 / 职能装备 ({status})";
|
||
if (skill == Skill.PowerfulShot && self.Has(Statuses.Repertoire) || skill == Skill.ApexArrow && self.Has(Statuses.BlastReady)) return "技能已变换为后续技能";
|
||
if (skill is Skill.RepellingShot or Skill.SilentNocturne && self.Has(Statuses.Repertoire)) return "已有完美音调,避免覆盖";
|
||
if (skill is Skill.Guard or Skill.Recuperate or Skill.Elixir && s.AliveSeconds < 5) return "复活后 5 秒保护";
|
||
if (skill == Skill.Guard && (!s.InCombat || !GuardNeeded(s, c))) return "未达到防御阈值 / 集火条件";
|
||
if (skill == Skill.Recuperate && (long)self.MaxHp - self.Hp < HealDeficit(s, c)) return "缺失 HP 未达到自疗阈值";
|
||
if (skill == Skill.Purify && (s.CleansePending || !Statuses.Controls.Any(id => self.Has(id) && !c.DisabledPurifyStatuses.Contains(id)))) return "无启用的净化控制 / 正在等待解控";
|
||
if (skill == Skill.WardensPaean && (s.CleansePending || !Statuses.Controls.Any(self.Has)
|
||
&& (!c.HelpParty || !s.Party.Any(a => a.Alive && a.Targetable && Statuses.Controls.Any(a.Has))))) return "无礼赞解控目标 / 正在等待解控";
|
||
if (skill == Skill.Elixir && (s.Moving || TargetSelector.HasNearbyThreat(s)
|
||
|| self.HpRatio * 100 > c.ElixirPercent && (s.MaxMp == 0 || (double)s.Mp / s.MaxMp * 100 > c.ElixirPercent))) return "补给条件不满足:资源 / 移动 / 周围威胁";
|
||
if (skill is Skill.Bravery or Skill.Dervish or Skill.FinalFantasia && (!s.InCombat || !s.Enemies.Any(a => a.Enemy && a.Alive))) return "进攻增益需要战斗和敌人";
|
||
if (skill == Skill.Bravery && self.Has(Statuses.Bravery) || skill == Skill.Dervish && self.Has(Statuses.Dervish)) return "增益已存在";
|
||
if (skill == Skill.FinalFantasia && (!s.LimitFull || self.Has(Statuses.Fantasia) || self.Has(Statuses.EncoreReady))) return "极限量谱未满 / 增益或安可尚未消耗";
|
||
if (skill == Skill.Sprint && (!s.Moving || self.Has(Statuses.Sprint))) return "未移动 / 冲刺已存在";
|
||
return HoldReason(skill, s, c);
|
||
}
|
||
}
|
||
|
||
public sealed class HealthPressure
|
||
{
|
||
private readonly Queue<(double Time, uint Loss)> losses = new();
|
||
private uint? lastHp;
|
||
public double Observe(double now, uint hp, uint maxHp)
|
||
{
|
||
if (lastHp is { } prior && prior > hp) losses.Enqueue((now, prior - hp));
|
||
lastHp = hp;
|
||
while (losses.TryPeek(out var item) && now - item.Time > 2) losses.Dequeue();
|
||
return maxHp == 0 ? 0 : losses.Sum(x => (double)x.Loss) / maxHp;
|
||
}
|
||
public void Reset() { losses.Clear(); lastHp = null; }
|
||
}
|