mirror of
https://github.com/babalae/better-genshin-impact.git
synced 2026-04-01 10:39:50 +08:00
1032 lines
32 KiB
C#
1032 lines
32 KiB
C#
using BetterGenshinImpact.Core.Recognition.OCR;
|
||
using BetterGenshinImpact.Core.Recognition.OpenCv;
|
||
using BetterGenshinImpact.Core.Script.Dependence;
|
||
using BetterGenshinImpact.Core.Simulator;
|
||
using BetterGenshinImpact.Core.Simulator.Extensions;
|
||
using BetterGenshinImpact.GameTask.AutoFight.Config;
|
||
using BetterGenshinImpact.GameTask.Model.Area;
|
||
using BetterGenshinImpact.Helpers;
|
||
using Microsoft.Extensions.Logging;
|
||
using OpenCvSharp;
|
||
using System;
|
||
using System.Linq;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
using BetterGenshinImpact.GameTask.AutoGeniusInvokation.Exception;
|
||
using BetterGenshinImpact.GameTask.AutoTrackPath;
|
||
using BetterGenshinImpact.GameTask.Common.BgiVision;
|
||
using Vanara.PInvoke;
|
||
using static BetterGenshinImpact.GameTask.Common.TaskControl;
|
||
using BetterGenshinImpact.Core.Config;
|
||
using BetterGenshinImpact.GameTask.AutoFight.Assets;
|
||
using BetterGenshinImpact.ViewModel.Pages;
|
||
using BetterGenshinImpact.GameTask.AutoGeniusInvokation.Model;
|
||
using BetterGenshinImpact.GameTask.AutoPathing;
|
||
using BetterGenshinImpact.GameTask.AutoPathing.Model;
|
||
using BetterGenshinImpact.GameTask.AutoPathing.Model.Enum;
|
||
|
||
namespace BetterGenshinImpact.GameTask.AutoFight.Model;
|
||
|
||
/// <summary>
|
||
/// 队伍内的角色
|
||
/// </summary>
|
||
public class Avatar
|
||
{
|
||
/// <summary>
|
||
/// 配置文件中的角色信息
|
||
/// </summary>
|
||
public readonly CombatAvatar CombatAvatar;
|
||
|
||
/// <summary>
|
||
/// 角色名称 中文
|
||
/// </summary>
|
||
public string Name { get; set; }
|
||
|
||
/// <summary>
|
||
/// 队伍内序号
|
||
/// </summary>
|
||
public int Index { get; set; }
|
||
|
||
/// <summary>
|
||
/// 最近一次OCR识别出的CD到期时间
|
||
/// </summary>
|
||
private DateTime OcrSkillCd { get; set; }
|
||
|
||
/// <summary>
|
||
/// 手动配置的技能CD,有它就不使用OCR,小于0为自动
|
||
/// </summary>
|
||
public double ManualSkillCd { get; set; }
|
||
|
||
/// <summary>
|
||
/// 最近一次使用元素战技的时间
|
||
/// </summary>
|
||
public DateTime LastSkillTime { get; set; }
|
||
|
||
/// <summary>
|
||
/// 元素爆发是否就绪
|
||
/// </summary>
|
||
public bool IsBurstReady { get; set; }
|
||
|
||
/// <summary>
|
||
/// 名字所在矩形位置
|
||
/// </summary>
|
||
public Rect NameRect { get; set; }
|
||
|
||
/// <summary>
|
||
/// 名字右边的编号位置
|
||
/// </summary>
|
||
public Rect IndexRect { get; set; }
|
||
|
||
/// <summary>
|
||
/// 任务取消令牌
|
||
/// </summary>
|
||
public CancellationToken Ct { get; set; }
|
||
|
||
/// <summary>
|
||
/// 战斗场景
|
||
/// </summary>
|
||
public CombatScenes CombatScenes { get; set; }
|
||
|
||
|
||
public Avatar(CombatScenes combatScenes, string name, int index, Rect nameRect, double manualSkillCd = -1)
|
||
{
|
||
CombatScenes = combatScenes;
|
||
Name = name;
|
||
Index = index;
|
||
NameRect = nameRect;
|
||
CombatAvatar = DefaultAutoFightConfig.CombatAvatarMap[name];
|
||
ManualSkillCd = manualSkillCd;
|
||
AutoFightTask.FightStatusFlag = false;
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 是否存在角色被击败
|
||
/// 通过判断确认按钮
|
||
/// </summary>
|
||
/// <param name="region"></param>
|
||
/// <param name="ct"></param>
|
||
/// <returns></returns>
|
||
public static void ThrowWhenDefeated(ImageRegion region, CancellationToken ct)
|
||
{
|
||
if (Bv.IsInRevivePrompt(region))
|
||
{
|
||
Logger.LogWarning("检测到复苏界面,存在角色被击败,前往七天神像复活");
|
||
// 先打开地图
|
||
Simulation.SendInput.Keyboard.KeyPress(User32.VK.VK_ESCAPE); // NOTE: 此处按下Esc是为了关闭复苏界面,无需改键
|
||
Sleep(600, ct);
|
||
TpForRecover(ct, new RetryException("检测到复苏界面,存在角色被击败,前往七天神像复活"));
|
||
}
|
||
else if(AutoFightParam.SwimmingEnabled && AutoFightTask.FightStatusFlag && SwimmingConfirm(region))
|
||
{
|
||
if (AutoFightTask.FightWaypoint is not null)
|
||
{
|
||
using var ra = CaptureToRectArea();
|
||
if (!SwimmingConfirm(ra)) //二次确认
|
||
{
|
||
return;
|
||
}
|
||
|
||
Logger.LogInformation("游泳检测:尝试回到战斗地点");
|
||
var cts = new CancellationTokenSource();
|
||
var pathExecutor = new PathExecutor(cts.Token);
|
||
|
||
try
|
||
{
|
||
pathExecutor.FaceTo(AutoFightTask.FightWaypoint).Wait(2000,cts.Token);
|
||
AutoFightTask.FightWaypoint.MoveMode = MoveModeEnum.Fly.Code; // 改为跳飞
|
||
Simulation.SendInput.Mouse.RightButtonDown();
|
||
pathExecutor.MoveTo(AutoFightTask.FightWaypoint).Wait(15000,cts.Token);
|
||
cts.Cancel();
|
||
AutoFightTask.FightWaypoint = null;
|
||
Simulation.SendInput.Mouse.RightButtonUp();
|
||
}
|
||
catch (TaskCanceledException)
|
||
{
|
||
Logger.LogError("游泳检测:回到战斗地点任务被取消");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Logger.LogError(ex, "游泳检测:回到战斗地点异常");
|
||
}
|
||
finally
|
||
{
|
||
Simulation.ReleaseAllKey();
|
||
}
|
||
|
||
using var bitmap2 = CaptureToRectArea();
|
||
if (!SwimmingConfirm(bitmap2))
|
||
{
|
||
Logger.LogInformation("游泳检测:游泳脱困成功");
|
||
return;
|
||
}
|
||
|
||
Logger.LogWarning("游泳检测:回到战斗地点失败");
|
||
}
|
||
|
||
Logger.LogWarning("战斗过程检测到游泳,前往七天神像重试");
|
||
TpForRecover(ct, new RetryException("战斗过程检测到游泳,前往七天神像重试"));
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 游泳检测(色块连通性检测)
|
||
/// 游泳时右下角会出现鼠标图标,带有黄色色块,不受改按键影响
|
||
/// </summary>
|
||
private static bool SwimmingConfirm(Region region)
|
||
{
|
||
using var mask = OpenCvCommonHelper.Threshold(region.ToImageRegion().DeriveCrop(1819, 1025, 9, 11).SrcMat,
|
||
new Scalar(242, 223, 39),new Scalar(255, 233, 44));
|
||
using var labels = new Mat();
|
||
using var stats = new Mat();
|
||
using var centroids = new Mat();
|
||
|
||
var numLabels = Cv2.ConnectedComponentsWithStats(mask, labels, stats, centroids,
|
||
connectivity: PixelConnectivity.Connectivity4, ltype: MatType.CV_32S);
|
||
|
||
return numLabels > 1;
|
||
}
|
||
|
||
/// <summary>
|
||
/// tp 到七天神像恢复
|
||
/// </summary>
|
||
/// <param name="ct"></param>
|
||
/// <param name="ex"></param>
|
||
/// <exception cref="RetryException"></exception>
|
||
public static void TpForRecover(CancellationToken ct, Exception ex)
|
||
{
|
||
// tp 到七天神像复活
|
||
var tpTask = new TpTask(ct);
|
||
tpTask.TpToStatueOfTheSeven().Wait(ct);
|
||
Logger.LogInformation("血量恢复完成。【设置】-【七天神像设置】可以修改回血相关配置。");
|
||
throw ex;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 切换到本角色
|
||
/// 切换cd是1秒,如果切换失败,会尝试再次切换,最多尝试5次
|
||
/// </summary>
|
||
public void Switch()
|
||
{
|
||
var context = new AvatarActiveCheckContext();
|
||
for (var i = 0; i < 30; i++)
|
||
{
|
||
if (Ct is { IsCancellationRequested: true })
|
||
{
|
||
return;
|
||
}
|
||
|
||
using var region = CaptureToRectArea();
|
||
ThrowWhenDefeated(region, Ct);
|
||
|
||
// 切换成功
|
||
if (CombatScenes.GetActiveAvatarIndex(region, context) == Index)
|
||
{
|
||
return;
|
||
}
|
||
|
||
SimulateSwitchAction(Index);
|
||
// Debug.WriteLine($"切换到{Index}号位");
|
||
// Cv2.ImWrite($"log/切换.png", region.SrcMat);
|
||
Sleep(250, Ct);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 尝试切换到本角色
|
||
/// </summary>
|
||
/// <param name="tryTimes"></param>
|
||
/// <param name="needLog"></param>
|
||
/// <returns></returns>
|
||
public bool TrySwitch(int tryTimes = 4, bool needLog = true)
|
||
{
|
||
var context = new AvatarActiveCheckContext();
|
||
for (var i = 0; i < tryTimes; i++)
|
||
{
|
||
if (Ct is { IsCancellationRequested: true })
|
||
{
|
||
return false;
|
||
}
|
||
|
||
using var region = CaptureToRectArea();
|
||
ThrowWhenDefeated(region, Ct);
|
||
|
||
// 切换成功
|
||
if (CombatScenes.GetActiveAvatarIndex(region, context) == Index)
|
||
{
|
||
if (needLog && i > 0)
|
||
{
|
||
Logger.LogInformation("成功切换角色:{Name}", Name);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
|
||
SimulateSwitchAction(Index);
|
||
|
||
Sleep(250, Ct);
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private void SimulateSwitchAction(int index)
|
||
{
|
||
Simulation.SendInput.SimulateAction(GIActions.Drop); //反正会重试就不等落地了
|
||
switch (index)
|
||
{
|
||
case 1:
|
||
Simulation.SendInput.SimulateAction(GIActions.SwitchMember1);
|
||
break;
|
||
case 2:
|
||
Simulation.SendInput.SimulateAction(GIActions.SwitchMember2);
|
||
break;
|
||
case 3:
|
||
Simulation.SendInput.SimulateAction(GIActions.SwitchMember3);
|
||
break;
|
||
case 4:
|
||
Simulation.SendInput.SimulateAction(GIActions.SwitchMember4);
|
||
break;
|
||
case 5:
|
||
Simulation.SendInput.SimulateAction(GIActions.SwitchMember5);
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 切换到本角色
|
||
/// 切换cd是1秒,如果切换失败,会尝试再次切换,最多尝试5次
|
||
/// </summary>
|
||
public void SwitchWithoutCts()
|
||
{
|
||
var context = new AvatarActiveCheckContext();
|
||
for (var i = 0; i < 10; i++)
|
||
{
|
||
using var region = CaptureToRectArea();
|
||
ThrowWhenDefeated(region, Ct);
|
||
|
||
if (CombatScenes.GetActiveAvatarIndex(region, context) == Index)
|
||
{
|
||
return;
|
||
}
|
||
|
||
SimulateSwitchAction(Index);
|
||
|
||
Sleep(250);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 是否出战状态
|
||
/// </summary>
|
||
/// <returns></returns>
|
||
public bool IsActive(ImageRegion region)
|
||
{
|
||
if (IndexRect == default)
|
||
{
|
||
throw new Exception("IndexRect为空");
|
||
}
|
||
else
|
||
{
|
||
var white = IsIndexRectWhite(region, IndexRect);
|
||
return !white;
|
||
}
|
||
}
|
||
|
||
private bool IsIndexRectWhite(ImageRegion region, Rect rect)
|
||
{
|
||
// 剪裁出IndexRect区域
|
||
using var indexRa = region.DeriveCrop(rect);
|
||
using var mat = indexRa.CacheGreyMat;
|
||
var count = OpenCvCommonHelper.CountGrayMatColor(mat, 251, 255);
|
||
if (count * 1.0 / (mat.Width * mat.Height) > 0.5)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 是否出战状态
|
||
/// </summary>
|
||
/// <returns></returns>
|
||
[Obsolete]
|
||
public bool IsActiveNoIndexRect(ImageRegion region)
|
||
{
|
||
// 通过寻找右侧人物编号来判断是否出战
|
||
if (IndexRect == default)
|
||
{
|
||
var assetScale = TaskContext.Instance().SystemInfo.AssetScale;
|
||
// 剪裁出队伍区域
|
||
var teamRa = region.DeriveCrop(AutoFightAssets.Instance.TeamRect);
|
||
var blockX = NameRect.X + NameRect.Width * 2 - 10;
|
||
var block = teamRa.DeriveCrop(new Rect(blockX, NameRect.Y, teamRa.Width - blockX, NameRect.Height * 2));
|
||
// Cv2.ImWrite($"block_{Name}.png", block.SrcMat);
|
||
// 取白色区域
|
||
var bMat = OpenCvCommonHelper.Threshold(block.SrcMat, new Scalar(255, 255, 255), new Scalar(255, 255, 255));
|
||
// Cv2.ImWrite($"block_b_{Name}.png", bMat);
|
||
// 矩形识别
|
||
Cv2.FindContours(bMat, out var contours, out _, RetrievalModes.External,
|
||
ContourApproximationModes.ApproxSimple);
|
||
if (contours.Length > 0)
|
||
{
|
||
var boxes = contours.Select(Cv2.BoundingRect)
|
||
.Where(w => w.Width >= 20 * assetScale && w.Height >= 18 * assetScale)
|
||
.OrderByDescending(w => w.Width).ToList();
|
||
if (boxes.Count is not 0)
|
||
{
|
||
IndexRect = boxes.First();
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 剪裁出IndexRect区域
|
||
var teamRa = region.DeriveCrop(AutoFightAssets.Instance.TeamRect);
|
||
var blockX = NameRect.X + NameRect.Width * 2 - 10;
|
||
var indexBlock = teamRa.DeriveCrop(new Rect(blockX + IndexRect.X, NameRect.Y + IndexRect.Y, IndexRect.Width,
|
||
IndexRect.Height));
|
||
// Cv2.ImWrite($"indexBlock_{Name}.png", indexBlock.SrcMat);
|
||
var count = OpenCvCommonHelper.CountGrayMatColor(indexBlock.CacheGreyMat, 255);
|
||
if (count * 1.0 / (IndexRect.Width * IndexRect.Height) > 0.5)
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
Logger.LogInformation("{Name} 当前出战", Name);
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 普通攻击
|
||
/// </summary>
|
||
/// <param name="ms">攻击时长,建议是200的倍数</param>
|
||
public void Attack(int ms = 0)
|
||
{
|
||
while (ms >= 0)
|
||
{
|
||
if (Ct is { IsCancellationRequested: true })
|
||
{
|
||
return;
|
||
}
|
||
|
||
Simulation.SendInput.SimulateAction(GIActions.NormalAttack);
|
||
ms -= 200;
|
||
Sleep(200, Ct);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 使用元素战技 E
|
||
/// </summary>
|
||
public void UseSkill(bool hold = false)
|
||
{
|
||
for (var i = 0; i < 1; i++)
|
||
{
|
||
if (Ct is { IsCancellationRequested: true })
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (hold)
|
||
{
|
||
if (Name == "纳西妲")
|
||
{
|
||
Simulation.SendInput.SimulateAction(GIActions.ElementalSkill, KeyType.KeyDown);
|
||
Sleep(300, Ct);
|
||
for (int j = 0; j < 10; j++)
|
||
{
|
||
Simulation.SendInput.Mouse.MoveMouseBy(1000, 0);
|
||
Sleep(50); // 持续操作不应该被cts取消
|
||
}
|
||
|
||
Sleep(300); // 持续操作不应该被cts取消
|
||
Simulation.SendInput.SimulateAction(GIActions.ElementalSkill, KeyType.KeyUp);
|
||
}
|
||
else if (Name == "坎蒂丝")
|
||
{
|
||
Simulation.SendInput.SimulateAction(GIActions.ElementalSkill, KeyType.KeyDown);
|
||
Thread.Sleep(3000);
|
||
Simulation.SendInput.SimulateAction(GIActions.ElementalSkill, KeyType.KeyUp);
|
||
}
|
||
else
|
||
{
|
||
Simulation.SendInput.SimulateAction(GIActions.ElementalSkill, KeyType.Hold);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
Simulation.SendInput.SimulateAction(GIActions.ElementalSkill);
|
||
}
|
||
|
||
Sleep(200, Ct);
|
||
|
||
using var region = CaptureToRectArea();
|
||
ThrowWhenDefeated(region, Ct); // 检测是不是要跑神像
|
||
var cd = AfterUseSkill(region);
|
||
if (cd > 0)
|
||
{
|
||
Logger.LogInformation(hold ? "{Name} 长按元素战技,cd:{Cd} 秒" : "{Name} 点按元素战技,cd:{Cd} 秒", Name,
|
||
Math.Round(cd, 2));
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 使用完元素战技的回调,注意,不会在这里检测是不是需要跑七天神像 <br/>
|
||
/// UseSkill 方法内会调用,如果没有使用UseSkill但是释放了技能之后记得调用一下这个方法
|
||
/// </summary>
|
||
/// <returns>当前技能CD</returns>
|
||
public double AfterUseSkill(ImageRegion? givenRegion = null)
|
||
{
|
||
LastSkillTime = DateTime.UtcNow;
|
||
if (ManualSkillCd > 0)
|
||
{
|
||
return GetSkillCdSeconds();
|
||
}
|
||
|
||
using var region = givenRegion ?? CaptureToRectArea();
|
||
return GetSkillCurrentCd(region);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 元素战技是否正在CD中
|
||
/// 右下 267x132
|
||
/// 77x77
|
||
/// </summary>
|
||
private double GetSkillCurrentCd(ImageRegion imageRegion)
|
||
{
|
||
using var eRa = imageRegion.DeriveCrop(AutoFightAssets.Instance.ECooldownRect);
|
||
using var eRaWhite = OpenCvCommonHelper.InRangeHsv(eRa.SrcMat, new Scalar(0, 0, 235), new Scalar(0, 25, 255));
|
||
var text = OcrFactory.Paddle.OcrWithoutDetector(eRaWhite);
|
||
var cd = StringUtils.TryParseDouble(text);
|
||
if (cd > 0 && cd <= CombatAvatar.SkillCd)
|
||
{
|
||
OcrSkillCd = DateTime.UtcNow.AddSeconds(cd);
|
||
}
|
||
|
||
return cd;
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 使用元素爆发 Q
|
||
/// Q释放等待 2s 超时认为没有Q技能
|
||
/// </summary>
|
||
public void UseBurst()
|
||
{
|
||
for (var i = 0; i < 10; i++)
|
||
{
|
||
if (Ct is { IsCancellationRequested: true })
|
||
{
|
||
return;
|
||
}
|
||
|
||
Simulation.SendInput.SimulateAction(GIActions.ElementalBurst);
|
||
Sleep(200, Ct);
|
||
|
||
using var region = CaptureToRectArea();
|
||
ThrowWhenDefeated(region, Ct);
|
||
|
||
if (!PartyAvatarSideIndexHelper.HasAnyIndexRect(region))
|
||
{
|
||
// 找不到角色编号块意味者技能释放成功
|
||
Sleep(1500, Ct);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
// /// <summary>
|
||
// /// 元素爆发是否正在CD中
|
||
// /// 右下 157x165
|
||
// /// 110x110
|
||
// /// </summary>
|
||
// public double GetBurstCurrentCd(CaptureContent content)
|
||
// {
|
||
// var qRa = content.CaptureRectArea.Crop(AutoFightAssets.Instance.QRect);
|
||
// var text = OcrFactory.Paddle.Ocr(qRa.SrcGreyMat);
|
||
// return StringUtils.TryParseDouble(text);
|
||
// }
|
||
|
||
/// <summary>
|
||
/// 冲刺
|
||
/// </summary>
|
||
public void Dash(int ms = 0)
|
||
{
|
||
if (Ct is { IsCancellationRequested: true })
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (ms == 0)
|
||
{
|
||
ms = 200;
|
||
}
|
||
|
||
Simulation.SendInput.SimulateAction(GIActions.SprintMouse, KeyType.KeyDown);
|
||
Sleep(ms); // 冲刺不能被cts取消
|
||
Simulation.SendInput.SimulateAction(GIActions.SprintMouse, KeyType.KeyUp);
|
||
}
|
||
|
||
public void Walk(string key, int ms)
|
||
{
|
||
if (Ct is { IsCancellationRequested: true })
|
||
{
|
||
return;
|
||
}
|
||
|
||
User32.VK vk = User32.VK.VK_NONAME;
|
||
if (key == "w")
|
||
{
|
||
vk = GIActions.MoveForward.ToActionKey().ToVK();
|
||
}
|
||
else if (key == "s")
|
||
{
|
||
vk = GIActions.MoveBackward.ToActionKey().ToVK();
|
||
}
|
||
else if (key == "a")
|
||
{
|
||
vk = GIActions.MoveLeft.ToActionKey().ToVK();
|
||
}
|
||
else if (key == "d")
|
||
{
|
||
vk = GIActions.MoveRight.ToActionKey().ToVK();
|
||
}
|
||
|
||
if (vk == User32.VK.VK_NONAME)
|
||
{
|
||
return;
|
||
}
|
||
|
||
Simulation.SendInput.Keyboard.KeyDown(vk);
|
||
Sleep(ms); // 行走不能被cts取消
|
||
Simulation.SendInput.Keyboard.KeyUp(vk);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 移动摄像机
|
||
/// </summary>
|
||
/// <param name="pixelDeltaX">负数是左移,正数是右移</param>
|
||
/// <param name="pixelDeltaY"></param>
|
||
public void MoveCamera(int pixelDeltaX, int pixelDeltaY)
|
||
{
|
||
Simulation.SendInput.Mouse.MoveMouseBy(pixelDeltaX, pixelDeltaY);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 等待
|
||
/// </summary>
|
||
/// <param name="ms"></param>
|
||
public void Wait(int ms)
|
||
{
|
||
Sleep(ms); // 由于存在宏操作,等待不应被cts取消
|
||
}
|
||
|
||
/// <summary>
|
||
/// 等待完成
|
||
/// </summary>
|
||
public void Ready()
|
||
{
|
||
Sleep(200, Ct);
|
||
|
||
for (int i = 0; i < 20; i++)
|
||
{
|
||
if (Ct is { IsCancellationRequested: true })
|
||
{
|
||
return;
|
||
}
|
||
|
||
using var region = CaptureToRectArea();
|
||
// 等待角色编号块出现
|
||
if (PartyAvatarSideIndexHelper.HasAnyIndexRect(region))
|
||
{
|
||
region.Dispose();
|
||
return;
|
||
}
|
||
|
||
Sleep(150, Ct);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
///
|
||
/// 根据cd推算E技能是否好了
|
||
/// </summary>
|
||
/// <param name="skillCd">强制指定技能CD</param>
|
||
/// <param name="printLog">log是否输出</param>
|
||
/// <returns>是否好了</returns>
|
||
public bool IsSkillReady(bool printLog = false)
|
||
{
|
||
var cd = GetSkillCdSeconds();
|
||
if (cd > 0)
|
||
{
|
||
if (printLog)
|
||
{
|
||
Logger.LogInformation("{Name}的E技能未准备好,CD还有{Seconds}秒", Name, Math.Round(cd, 2));
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算上一次使用技能到现在还剩下多长时间的cd
|
||
/// </summary>
|
||
/// <returns></returns>
|
||
public double GetSkillCdSeconds()
|
||
{
|
||
switch (ManualSkillCd)
|
||
{
|
||
case < 0:
|
||
{
|
||
var now = DateTime.UtcNow;
|
||
// 若未经过OCR的技能释放,上次时间加上最长的技能时间
|
||
var maxCd = Math.Max(CombatAvatar.SkillHoldCd, CombatAvatar.SkillCd);
|
||
var target =
|
||
LastSkillTime >= OcrSkillCd
|
||
? LastSkillTime.AddSeconds(Math.Max(CombatAvatar.SkillHoldCd, CombatAvatar.SkillCd))
|
||
: OcrSkillCd;
|
||
var result = now > target ? 0d : (target - now).TotalSeconds;
|
||
if (!(result > maxCd)) return result;
|
||
Logger.LogWarning("{Name}的当前技能CD大于其最大技能CD{MaxCd}。如果你没有调整系统时间的话,这是一个bug。", Name, maxCd);
|
||
return maxCd;
|
||
}
|
||
case > 0:
|
||
{
|
||
// 用户设置,所以直接通过上次释放技能的时间计算
|
||
var dif = DateTime.UtcNow - LastSkillTime;
|
||
if (ManualSkillCd > dif.TotalSeconds)
|
||
{
|
||
return ManualSkillCd - dif.TotalSeconds;
|
||
}
|
||
|
||
break;
|
||
}
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 等待技能CD
|
||
/// </summary>
|
||
/// <param name="ct">CancellationToken</param>
|
||
public async Task WaitSkillCd(CancellationToken ct = default)
|
||
{
|
||
// 获取CD时间
|
||
if (IsSkillReady())
|
||
{
|
||
return;
|
||
}
|
||
|
||
var s = GetSkillCdSeconds() + 0.2;
|
||
Logger.LogInformation("{Name}的E技能CD未结束,等待{Seconds}秒", Name, Math.Round(s, 2));
|
||
await Delay((int)Math.Ceiling(s * 1000), ct);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 跳跃
|
||
/// </summary>
|
||
public void Jump()
|
||
{
|
||
Simulation.SendInput.SimulateAction(GIActions.Jump);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 重击
|
||
/// </summary>
|
||
public void Charge(int ms = 0)
|
||
{
|
||
if (ms == 0)
|
||
{
|
||
ms = 1000;
|
||
}
|
||
|
||
if (Name == "那维莱特")
|
||
{
|
||
var dpi = TaskContext.Instance().DpiScale;
|
||
Simulation.SendInput.SimulateAction(GIActions.NormalAttack, KeyType.KeyDown);
|
||
while (ms >= 0)
|
||
{
|
||
if (Ct is { IsCancellationRequested: true })
|
||
{
|
||
return;
|
||
}
|
||
|
||
Simulation.SendInput.Mouse.MoveMouseBy((int)(1000 * dpi), 0);
|
||
ms -= 50;
|
||
Sleep(50); // 持续操作不应该被cts取消
|
||
}
|
||
|
||
Simulation.SendInput.SimulateAction(GIActions.NormalAttack, KeyType.KeyUp);
|
||
}
|
||
else if (Name == "恰斯卡")
|
||
{
|
||
var dpi = TaskContext.Instance().DpiScale;
|
||
Simulation.SendInput.SimulateAction(GIActions.NormalAttack, KeyType.KeyDown);
|
||
int tick = -4; // 起飞那一刻需要多一点点时间用来矫正视角高度
|
||
while (ms >= 0)
|
||
{
|
||
if (Ct is { IsCancellationRequested: true })
|
||
{
|
||
return;
|
||
}
|
||
|
||
// 恰在蓄力时转得越快越容易把视角趋向于水平
|
||
// 基于上面这个特性,如果我们用同一个鼠标方向向量,大致能在所有设备上控制视角高低(只要帧率不太低)
|
||
|
||
// 恰的子弹上膛机制:怪物要在HUD准星框内超过一定时长(体感0.2-0.3秒)才能让子弹上膛。所以搜索敌人要低速。不然敌人体型小或者远就很容易锁不上。
|
||
const double lowspeed = 0.7, highspeed = 50;
|
||
double rateX, rateY;
|
||
if (tick < 3)
|
||
{
|
||
rateX = highspeed;
|
||
rateY = highspeed * 0.23;
|
||
}
|
||
else if (tick < 40)
|
||
{
|
||
rateX = lowspeed * 0.7;
|
||
rateY = 0;
|
||
}
|
||
else if (tick < 43)
|
||
{
|
||
rateX = highspeed;
|
||
rateY = highspeed * 0.4;
|
||
}
|
||
else if (tick < 70)
|
||
{
|
||
rateX = lowspeed * 0.9;
|
||
rateY = 0;
|
||
}
|
||
else if (tick < 73)
|
||
{
|
||
rateX = highspeed;
|
||
rateY = highspeed;
|
||
}
|
||
else
|
||
{
|
||
rateX = lowspeed;
|
||
rateY = 0;
|
||
}
|
||
|
||
Simulation.SendInput.Mouse.MoveMouseBy((int)(rateX * 50 * dpi), (int)(rateY * 50 * dpi));
|
||
|
||
tick = (tick + 1) % 100;
|
||
Sleep(25);
|
||
ms -= 25;
|
||
}
|
||
|
||
Simulation.SendInput.SimulateAction(GIActions.NormalAttack, KeyType.KeyUp);
|
||
}
|
||
else
|
||
{
|
||
Simulation.SendInput.SimulateAction(GIActions.NormalAttack, KeyType.KeyDown);
|
||
Sleep(ms); // 持续操作不应该被cts取消
|
||
Simulation.SendInput.SimulateAction(GIActions.NormalAttack, KeyType.KeyUp);
|
||
}
|
||
}
|
||
|
||
public void MouseDown(string key = "left")
|
||
{
|
||
key = key.ToLower();
|
||
if (key == "left")
|
||
{
|
||
Simulation.SendInput.Mouse.LeftButtonDown();
|
||
}
|
||
else if (key == "right")
|
||
{
|
||
Simulation.SendInput.Mouse.RightButtonDown();
|
||
}
|
||
else if (key == "middle")
|
||
{
|
||
Simulation.SendInput.Mouse.MiddleButtonDown();
|
||
}
|
||
}
|
||
|
||
public void MouseUp(string key = "left")
|
||
{
|
||
key = key.ToLower();
|
||
if (key == "left")
|
||
{
|
||
Simulation.SendInput.Mouse.LeftButtonUp();
|
||
}
|
||
else if (key == "right")
|
||
{
|
||
Simulation.SendInput.Mouse.RightButtonUp();
|
||
}
|
||
else if (key == "middle")
|
||
{
|
||
Simulation.SendInput.Mouse.MiddleButtonUp();
|
||
}
|
||
}
|
||
|
||
public void Click(string key = "left")
|
||
{
|
||
key = key.ToLower();
|
||
if (key == "left")
|
||
{
|
||
Simulation.SendInput.Mouse.LeftButtonClick();
|
||
}
|
||
else if (key == "right")
|
||
{
|
||
Simulation.SendInput.Mouse.RightButtonClick();
|
||
}
|
||
else if (key == "middle")
|
||
{
|
||
Simulation.SendInput.Mouse.MiddleButtonClick();
|
||
}
|
||
}
|
||
|
||
public void MoveBy(int x, int y)
|
||
{
|
||
GlobalMethod.MoveMouseBy(x, y);
|
||
}
|
||
|
||
public void Scroll(int scrollAmountInClicks)
|
||
{
|
||
Simulation.SendInput.Mouse.VerticalScroll(scrollAmountInClicks);
|
||
}
|
||
|
||
public void KeyDown(string key)
|
||
{
|
||
var vk = KeyBindingsSettingsPageViewModel.MappingKey(User32Helper.ToVk(key));
|
||
switch (key)
|
||
{
|
||
case "VK_LBUTTON":
|
||
Simulation.SendInput.Mouse.LeftButtonDown();
|
||
break;
|
||
case "VK_RBUTTON":
|
||
Simulation.SendInput.Mouse.RightButtonDown();
|
||
break;
|
||
case "VK_MBUTTON":
|
||
Simulation.SendInput.Mouse.MiddleButtonDown();
|
||
break;
|
||
case "VK_XBUTTON1":
|
||
Simulation.SendInput.Mouse.XButtonDown(0x0001);
|
||
break;
|
||
case "VK_XBUTTON2":
|
||
Simulation.SendInput.Mouse.XButtonDown(0x0001);
|
||
break;
|
||
default:
|
||
Simulation.SendInput.Keyboard.KeyDown(vk);
|
||
break;
|
||
}
|
||
}
|
||
|
||
public void KeyUp(string key)
|
||
{
|
||
var vk = KeyBindingsSettingsPageViewModel.MappingKey(User32Helper.ToVk(key));
|
||
switch (key)
|
||
{
|
||
case "VK_LBUTTON":
|
||
Simulation.SendInput.Mouse.LeftButtonUp();
|
||
break;
|
||
case "VK_RBUTTON":
|
||
Simulation.SendInput.Mouse.RightButtonUp();
|
||
break;
|
||
case "VK_MBUTTON":
|
||
Simulation.SendInput.Mouse.MiddleButtonUp();
|
||
break;
|
||
case "VK_XBUTTON1":
|
||
Simulation.SendInput.Mouse.XButtonUp(0x0001);
|
||
break;
|
||
case "VK_XBUTTON2":
|
||
Simulation.SendInput.Mouse.XButtonUp(0x0001);
|
||
break;
|
||
default:
|
||
Simulation.SendInput.Keyboard.KeyUp(vk);
|
||
break;
|
||
}
|
||
}
|
||
|
||
public void KeyPress(string key)
|
||
{
|
||
var vk = KeyBindingsSettingsPageViewModel.MappingKey(User32Helper.ToVk(key));
|
||
switch (key)
|
||
{
|
||
case "VK_LBUTTON":
|
||
Simulation.SendInput.Mouse.LeftButtonClick();
|
||
break;
|
||
case "VK_RBUTTON":
|
||
Simulation.SendInput.Mouse.RightButtonClick();
|
||
break;
|
||
case "VK_MBUTTON":
|
||
Simulation.SendInput.Mouse.MiddleButtonClick();
|
||
break;
|
||
case "VK_XBUTTON1":
|
||
Simulation.SendInput.Mouse.XButtonClick(0x0001);
|
||
break;
|
||
case "VK_XBUTTON2":
|
||
Simulation.SendInput.Mouse.XButtonClick(0x0001);
|
||
break;
|
||
default:
|
||
Simulation.SendInput.Keyboard.KeyPress(vk);
|
||
break;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从配置字符串中查找角色cd
|
||
/// 仅有角色名时返回 -1 ,没找到角色返回null
|
||
/// </summary>
|
||
/// <param name="avatarName">角色名</param>
|
||
/// <param name="input">序列</param>
|
||
/// <returns></returns>
|
||
public static double? ParseActionSchedulerByCd(string avatarName, string input)
|
||
{
|
||
if (string.IsNullOrEmpty(input) || string.IsNullOrEmpty(avatarName))
|
||
return null;
|
||
|
||
var searchIndex = input.Length - 1;
|
||
|
||
while (true)
|
||
{
|
||
// 逆向查找角色名最后一次出现的位置
|
||
var foundIndex = input.LastIndexOf(avatarName, searchIndex, StringComparison.Ordinal);
|
||
if (foundIndex == -1) return null;
|
||
|
||
// 验证前向边界(分号或字符串起点)
|
||
var startValid = foundIndex == 0 ||
|
||
input[foundIndex - 1] == ';';
|
||
|
||
// 验证后向边界(逗号或分号/字符串终点)
|
||
var endValid = foundIndex + avatarName.Length == input.Length ||
|
||
input[foundIndex + avatarName.Length] == ',' ||
|
||
input[foundIndex + avatarName.Length] == ';';
|
||
|
||
if (startValid && endValid)
|
||
{
|
||
var valueStart = foundIndex + avatarName.Length;
|
||
// 处理逗号后的数值部分
|
||
if (valueStart >= input.Length || input[valueStart] != ',') return -1;
|
||
var valueEnd = input.IndexOf(';', valueStart);
|
||
if (valueEnd == -1) valueEnd = input.Length;
|
||
|
||
if (double.TryParse(input.AsSpan(valueStart + 1, valueEnd - valueStart - 1),
|
||
out var result))
|
||
{
|
||
return result;
|
||
}
|
||
|
||
// 存在角色名但没有数值的情况
|
||
return -1;
|
||
}
|
||
|
||
// 更新搜索范围继续查找
|
||
searchIndex = foundIndex - 1;
|
||
if (searchIndex < 0) break;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
} |