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()) { 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 apply) { if (ImGui.Checkbox(label, ref value)) { apply(value); Save(); } } private void Float(string label, float value, float min, float max, Action 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 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(); } }