Files
BardFrontline/Plugin.cs
T

221 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
}
}