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
+220
View File
@@ -0,0 +1,220 @@
using System.Numerics;
using BardFrontline.Core;
using BardFrontline.Runtime;
using Dalamud.Bindings.ImGui;
using Dalamud.Game.Command;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
namespace BardFrontline;
public sealed class Plugin : IDalamudPlugin
{
private readonly IDalamudPluginInterface pi;
private readonly ICommandManager commands;
private readonly IFramework framework;
private readonly Configuration configuration;
private readonly CombatRuntime runtime;
private bool settingsOpen;
private string? frozenDiagnostic;
public Plugin(IDalamudPluginInterface pluginInterface, ICommandManager commandManager,
IFramework framework, IObjectTable objects, IClientState client, ICondition condition,
IDutyState duty, IDataManager data, ITargetManager targets, IPluginLog log)
{
pi = pluginInterface; commands = commandManager; this.framework = framework;
configuration = pi.GetPluginConfig() as Configuration ?? new();
configuration.Settings ??= new(); configuration.Settings.Normalize();
runtime = new(objects, client, condition, duty, data, targets, log, configuration.Settings);
commands.AddHandler("/bardpvp", new CommandInfo(OnCommand)
{ HelpMessage = "设置;on/off/toggle 开关;manual/auto 手动接管/恢复;burst 开启10秒爆发,burst off 结束;backstep 切换后跳手动/自动。" });
pi.UiBuilder.Draw += Draw;
pi.UiBuilder.OpenConfigUi += OpenSettings;
pi.UiBuilder.OpenMainUi += OpenSettings;
framework.Update += runtime.Update;
}
private void OpenSettings() => settingsOpen = true;
private void OnCommand(string command, string args)
{
lock (runtime.SyncRoot) HandleCommand(args);
}
private void HandleCommand(string args)
{
switch (args.Trim().ToLowerInvariant())
{
case "on": runtime.Enabled = true; break;
case "off": runtime.Enabled = false; break;
case "toggle": runtime.Enabled = !runtime.Enabled; break;
case "manual": runtime.ManualPaused = true; break;
case "auto": runtime.ManualPaused = false; break;
case "burst": runtime.StartBurst(); break;
case "burst off": runtime.StopBurst(); break;
case "backstep": configuration.Settings.ManualRepelling = !configuration.Settings.ManualRepelling; Save(); break;
case "backstep manual": configuration.Settings.ManualRepelling = true; Save(); break;
case "backstep auto": configuration.Settings.ManualRepelling = false; Save(); break;
default: settingsOpen = true; break;
}
}
private void Save() { configuration.Settings.Normalize(); pi.SavePluginConfig(configuration); }
private void RunToggle()
{
var running = runtime.Enabled;
if (ImGui.Checkbox("自动运行##master", ref running)) runtime.Enabled = running;
ImGui.TextWrapped(runtime.State);
ImGui.TextWrapped($"模式:{runtime.ModeText}");
ImGui.TextWrapped(runtime.ControlText);
if (ImGui.Button(runtime.ManualPaused ? "恢复自动控制" : "手动接管")) runtime.ManualPaused = !runtime.ManualPaused;
ImGui.SameLine(); if (ImGui.Button("爆发 10 秒")) runtime.StartBurst();
}
private void Draw()
{
lock (runtime.SyncRoot) DrawLocked();
}
private void DrawLocked()
{
var c = configuration.Settings;
if (c.ShowOverlay)
{
ImGui.SetNextWindowSize(new Vector2(330, 0), ImGuiCond.FirstUseEver);
if (ImGui.Begin("诗人前线##BardFrontlineHUD", ImGuiWindowFlags.AlwaysAutoResize))
{
RunToggle();
ImGui.TextWrapped($"目标:{runtime.TargetText}");
ImGui.TextWrapped($"下一技能:{runtime.NextText}");
if (ImGui.Button("设置")) settingsOpen = true;
}
ImGui.End();
}
if (!settingsOpen) return;
ImGui.SetNextWindowSize(new Vector2(570, 680), ImGuiCond.FirstUseEver);
if (ImGui.Begin("Bard Frontline 设置", ref settingsOpen))
{
RunToggle();
ImGui.TextWrapped("支持纷争前线和烈羽争锋;驾驶机甲时暂停诗人循环。每次加载总开关默认关闭。使用 /bardpvp off 可随时停止。后跳会改变角色位置。关闭后不再提交新动作,游戏已经接受的动作仍由游戏完成。");
ImGui.Separator();
Check("自动攻击循环与进攻增益", c.AutoAttack, v => c.AutoAttack = v);
Check("无有效玩家时攻击战场目标", c.AttackObjectives, v => c.AttackObjectives = v);
ImGui.TextWrapped("普通玩家优先;无可用玩家攻击时,攻击冰块、敌方兵线、建筑和机甲等合法目标。鹰眼仅对未驾驶机甲的玩家使用。");
Check("无视敌方 Guard", c.IgnoreEnemyGuard, v => c.IgnoreEnemyGuard = v);
ImGui.TextWrapped("允许向 Guard 目标释放攻击;不改变实际伤害减免。自身 Guard 期间始终暂停自动技能。");
Check("礼赞帮助小队成员解除控制", c.HelpParty, v => c.HelpParty = v);
Check("显示状态浮窗", c.ShowOverlay, v => c.ShowOverlay = v);
var order = (int)c.TargetOrder;
if (ImGui.Combo("选敌排序", ref order, "当前 HP 最低\0HP 比例最低\0距离最近\0攻击标记 1–8 优先\0"))
{ c.TargetOrder = (TargetOrder)order; Save(); }
var guard = c.GuardHpPercent;
if (ImGui.SliderFloat("防御 HP %", ref guard, 1, 100, "%.0f%%")) { c.GuardHpPercent = guard; Save(); }
var heal = c.RecuperateMissingHp;
if (ImGui.InputInt("自疗所需缺失 HP", ref heal, 1000, 5000)) { c.RecuperateMissingHp = heal; Save(); }
var elixir = c.ElixirPercent;
if (ImGui.SliderFloat("补给 HP / MP %", ref elixir, 1, 100, "%.1f%%")) { c.ElixirPercent = elixir; Save(); }
ImGui.TextWrapped("补给还要求静止、30 米内无敌方玩家;烈羽争锋的敌方机甲、兵线和建筑也计入威胁。碎冰战冰块不计入。防御、自疗、补给在复活 5 秒后启用。");
if (ImGui.CollapsingHeader("Eagle Eye Shot 收割", ImGuiTreeNodeFlags.DefaultOpen))
{
var strategy = (int)c.EagleStrategy;
if (ImGui.Combo("鹰眼使用策略", ref strategy, "优先收割,否则按原循环\0严格保留,只用于收割\0"))
{ c.EagleStrategy = (EagleStrategy)strategy; Save(); }
var hp = c.EagleFinisherHp;
if (ImGui.InputInt("收割有效 HP 上限", ref hp, 500, 2000)) { c.EagleFinisherHp = hp; Save(); }
ImGui.TextWrapped("有效 HP = 当前 HP + 吸收护盾。范围内满足阈值的敌人按有效 HP 最低优先,鹰眼抢在普通输出与极限技前释放;生存技能仍优先。阈值是筛选条件,不是精确伤害预测;可根据实战伤害调整。需装备鹰眼职能技能。");
}
if (ImGui.CollapsingHeader("技能独立开关", ImGuiTreeNodeFlags.DefaultOpen))
foreach (var skill in Actions.All)
{
var active = c.Enabled(skill);
if (ImGui.Checkbox($"{Actions.Name(skill)}##skill{(uint)skill}", ref active))
{ if (active) c.DisabledSkills.Remove(skill); else c.DisabledSkills.Add(skill); Save(); }
}
if (ImGui.CollapsingHeader("选敌稳定与手动接管"))
{
Check("启用选敌稳定", c.StableTargets, v => c.StableTargets = v);
Float("目标保持秒数", c.TargetHoldSeconds, 0, 3, v => c.TargetHoldSeconds = v);
Integer("切换 HP 优势", c.TargetHpMargin, 0, 20000, v => c.TargetHpMargin = v);
ImGui.TextWrapped("默认仍即时选择最低当前 HP。开启后短时间保持有效目标;明显更优的目标、目标失效或鹰眼收割可立即切换。");
Check("检测手动切目标 / 其他来源动作后暂时接管", c.DetectManualControl, v => c.DetectManualControl = v);
Float("自动接管持续秒数", c.ManualControlSeconds, .5f, 15, v => c.ManualControlSeconds = v);
ImGui.TextWrapped("接管期间暂停所有自动动作和选敌,包括生存技能。按钮或 /bardpvp manual 持续接管,auto 恢复。自动检测无法区分手动动作与其他插件动作。");
}
if (ImGui.CollapsingHeader("穿插、爆发与范围攻击"))
{
Check("自适应穿插预算", c.AdaptiveWeaving, v => c.AdaptiveWeaving = v);
ImGui.TextWrapped(runtime.TimingText);
ImGui.TextWrapped("根据本插件瞬发动作的确认及动画锁样本估算;不足 3 个样本时使用原来的 0.65 秒。不会绕过游戏锁。");
var burst = (int)c.Burst;
if (ImGui.Combo("爆发策略", ref burst, "原循环,不保留\0和弦箭等待增益 / 充能\0手动爆发窗口\0主动爆发\0"))
{ c.Burst = (BurstStrategy)burst; Save(); }
if (c.Burst == BurstStrategy.Pressure)
ImGui.TextWrapped("优先消耗已就绪的完美音调 / 爆破箭,提前使用鹰眼和沉默。瞬发输出及进攻增益不额外等待穿插空间,可能短暂推迟普攻;仍遵守鹰眼保留策略、游戏动作锁和技能条件。生存及后跳设置不变。");
Integer("和弦目标充能", c.BurstCharges, 1, 4, v => c.BurstCharges = v);
Float("等待增益最多秒数", c.BurstMaxHoldSeconds, .5f, 10, v => c.BurstMaxHoldSeconds = v);
ImGui.TextWrapped("手动窗口仅保留和弦箭、职能增益及极限技;安可、鹰眼收割和生存不等待。按钮或 /bardpvp burst 开启 10 秒窗口。");
if (ImGui.Button("结束爆发窗口")) runtime.StopBurst();
Check("范围技能优先更多有效目标", c.PreferAoe, v => c.PreferAoe = v);
Integer("范围选敌最少命中数", c.AoeMinTargets, 2, 10, v => c.AoeMinTargets = v);
ImGui.TextWrapped("仍优先玩家。按客户端范围和目标位置估算圆形 / 直线覆盖;未达到数量时回到原排序。命中数是估算,不保证实际命中。");
}
if (ImGui.CollapsingHeader("战场目标分类"))
{
foreach (var kind in Enum.GetValues<ObjectiveKind>())
{
var attack = !c.DisabledObjectives.Contains(kind);
if (ImGui.Checkbox($"攻击{Battlefield.KindName(kind)}", ref attack))
{ if (attack) c.DisabledObjectives.Remove(kind); else c.DisabledObjectives.Add(kind); Save(); }
}
ImGui.TextWrapped("分类开关受战场目标总开关控制;全开保持原有偏好。未知类型归入其他目标。关闭攻击某类不会忽略它的补给威胁。");
}
if (ImGui.CollapsingHeader("集火生存与后跳"))
{
Check("启用集火 / 快速掉血防御", c.FocusDefense, v => c.FocusDefense = v);
Integer("集火指向人数", c.FocusAttackers, 2, 10, v => c.FocusAttackers = v);
Float("集火时防御 HP %", c.FocusGuardPercent, 1, 80, v => c.FocusGuardPercent = v);
ImGui.TextWrapped("附近敌人目标或读条指向自己达到人数,或 2 秒累计掉血达到最大 HP 的 25% 时,按集火阈值防御,并将自疗缺失阈值降低到最多 6000。目标指向只是风险信号。");
Check("后跳改为手动释放", c.ManualRepelling, v => c.ManualRepelling = v);
ImGui.TextWrapped("默认保持自动后跳。可用 /bardpvp backstep 随时切换;手动模式由你按游戏技能,不影响其他循环。");
}
if (ImGui.CollapsingHeader("净化触发控制"))
{
string[] labels = ["眩晕", "加重", "止步", "沉默", "深度冻结", "自然的奇迹"];
for (var i = 0; i < Statuses.Controls.Length; i++)
{
var id = Statuses.Controls[i]; var active = !c.DisabledPurifyStatuses.Contains(id);
if (ImGui.Checkbox(labels[i], ref active))
{ if (active) c.DisabledPurifyStatuses.Remove(id); else c.DisabledPurifyStatuses.Add(id); Save(); }
}
}
if (ImGui.CollapsingHeader("运行诊断"))
{
if (ImGui.Button("复制诊断")) ImGui.SetClipboardText(frozenDiagnostic ?? runtime.DiagnosticText());
ImGui.SameLine();
if (ImGui.Button(frozenDiagnostic == null ? "冻结快照" : "恢复实时"))
frozenDiagnostic = frozenDiagnostic == null ? runtime.DiagnosticText() : null;
ImGui.TextWrapped($"最近请求:{runtime.LastAction}");
if (frozenDiagnostic != null) ImGui.TextWrapped(frozenDiagnostic);
else
{
ImGui.TextWrapped(runtime.AuditSummary);
ImGui.TextWrapped("每 0.25 秒重建完整快照。就绪检查与执行锁分开;复制内容含资源、状态、候选目标和近期事件。");
foreach (var (skill, reason) in runtime.Diagnostics) ImGui.TextWrapped($"{Actions.Name(skill)}:{reason}");
}
}
}
ImGui.End();
}
private void Check(string label, bool value, Action<bool> apply)
{ if (ImGui.Checkbox(label, ref value)) { apply(value); Save(); } }
private void Float(string label, float value, float min, float max, Action<float> apply)
{ if (ImGui.SliderFloat(label, ref value, min, max, "%.2f")) { apply(value); Save(); } }
private void Integer(string label, int value, int min, int max, Action<int> apply)
{ if (ImGui.SliderInt(label, ref value, min, max)) { apply(value); Save(); } }
public void Dispose()
{
framework.Update -= runtime.Update;
commands.RemoveHandler("/bardpvp");
pi.UiBuilder.Draw -= Draw;
pi.UiBuilder.OpenConfigUi -= OpenSettings;
pi.UiBuilder.OpenMainUi -= OpenSettings;
runtime.Dispose();
}
}