diff --git a/BetterGenshinImpact/BetterGenshinImpact.csproj b/BetterGenshinImpact/BetterGenshinImpact.csproj
index 618fc02f..2647a869 100644
--- a/BetterGenshinImpact/BetterGenshinImpact.csproj
+++ b/BetterGenshinImpact/BetterGenshinImpact.csproj
@@ -71,6 +71,108 @@
Always
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
Always
diff --git a/BetterGenshinImpact/Core/Recognition/OpenCv/CommonExtension.cs b/BetterGenshinImpact/Core/Recognition/OpenCv/CommonExtension.cs
index 5645efc7..abc5e1f3 100644
--- a/BetterGenshinImpact/Core/Recognition/OpenCv/CommonExtension.cs
+++ b/BetterGenshinImpact/Core/Recognition/OpenCv/CommonExtension.cs
@@ -4,6 +4,9 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Drawing;
+using OpenCvSharp;
+using Vanara.PInvoke;
+using Point = System.Drawing.Point;
namespace BetterGenshinImpact.Core.Recognition.OpenCv
{
@@ -55,6 +58,27 @@ namespace BetterGenshinImpact.Core.Recognition.OpenCv
}
+ public static OpenCvSharp.Point GetCenterPoint(this RECT rectangle)
+ {
+ if (rectangle.IsEmpty)
+ {
+ throw new ArgumentException("rectangle is empty");
+ }
+
+ return new OpenCvSharp.Point(rectangle.X + rectangle.Width / 2, rectangle.Y + rectangle.Height / 2);
+ }
+
+ public static OpenCvSharp.Point GetCenterPoint(this Rect rectangle)
+ {
+ if (rectangle == Rect.Empty)
+ {
+ throw new ArgumentException("rectangle is empty");
+ }
+
+ return new OpenCvSharp.Point(rectangle.X + rectangle.Width / 2, rectangle.Y + rectangle.Height / 2);
+ }
+
+
public static System.Windows.Media.Color ToWindowsColor(this Color color)
{
return System.Windows.Media.Color.FromArgb(color.A, color.R, color.G, color.B);
diff --git a/BetterGenshinImpact/Core/Recognition/OpenCv/MatchTemplateHelper.cs b/BetterGenshinImpact/Core/Recognition/OpenCv/MatchTemplateHelper.cs
index ce221497..8dfe81ff 100644
--- a/BetterGenshinImpact/Core/Recognition/OpenCv/MatchTemplateHelper.cs
+++ b/BetterGenshinImpact/Core/Recognition/OpenCv/MatchTemplateHelper.cs
@@ -1,13 +1,8 @@
-using OpenCvSharp;
+using Microsoft.Extensions.Logging;
+using OpenCvSharp;
using System;
using System.Collections.Generic;
-using System.Drawing;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using Microsoft.Extensions.Logging;
using Point = OpenCvSharp.Point;
-using BetterGenshinImpact.GameTask.AutoSkip;
namespace BetterGenshinImpact.Core.Recognition.OpenCv
{
@@ -17,12 +12,13 @@ namespace BetterGenshinImpact.Core.Recognition.OpenCv
///
/// 模板匹配
+ /// TODO 算法不一样的的时候找点的方法也不一样
///
- ///
- ///
- ///
- ///
- ///
+ /// 原图像
+ /// 模板
+ /// 匹配方式
+ /// 遮罩
+ /// 阈值
/// 左上角的标点
public static Point MatchTemplate(Mat srcMat, Mat dstMat, TemplateMatchModes matchMode, Mat? maskMat = null, double threshold = 0.8)
{
@@ -53,5 +49,60 @@ namespace BetterGenshinImpact.Core.Recognition.OpenCv
return new Point();
}
}
+
+ ///
+ /// 模板匹配多个结果
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static List MatchTemplateMulti(Mat srcMat, Mat dstMat, TemplateMatchModes matchMode = TemplateMatchModes.CCoeffNormed, Mat? maskMat = null, double threshold = 0.8)
+ {
+ var points = new List();
+ try
+ {
+ using var result = new Mat();
+ if (maskMat == null)
+ {
+ Cv2.MatchTemplate(srcMat, dstMat, result, matchMode);
+ }
+ else
+ {
+ Cv2.MatchTemplate(srcMat, dstMat, result, matchMode, maskMat);
+ }
+
+ while (true)
+ {
+ Cv2.MinMaxLoc(result, out _, out var maxValue, out _, out var maxLoc);
+
+ if (maxValue >= threshold)
+ {
+ points.Add(new Point(maxLoc.X, maxLoc.Y));
+
+ //Fill in the res Mat so you don't find the same area again in the MinMaxLoc
+ Cv2.FloodFill(result, maxLoc, new Scalar(0), out _, new Scalar(0.1), new Scalar(1.0));
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ return points;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError("{Ex}", ex);
+ return points;
+ }
+ }
+
+ public static List MatchTemplateMulti(Mat srcMat, Mat dstMat, double threshold)
+ {
+ return MatchTemplateMulti(srcMat, dstMat, TemplateMatchModes.CCoeffNormed, null, threshold);
+ }
}
}
\ No newline at end of file
diff --git a/BetterGenshinImpact/Core/Recognition/OpenCv/OldMatchTemplateHelper.cs b/BetterGenshinImpact/Core/Recognition/OpenCv/OldMatchTemplateHelper.cs
index a608410b..02b4ec5f 100644
--- a/BetterGenshinImpact/Core/Recognition/OpenCv/OldMatchTemplateHelper.cs
+++ b/BetterGenshinImpact/Core/Recognition/OpenCv/OldMatchTemplateHelper.cs
@@ -6,177 +6,177 @@ using System.Collections.Generic;
using System.Drawing;
using Point = OpenCvSharp.Point;
-namespace BetterGenshinImpact.Core.Recognition.OpenCv
+namespace BetterGenshinImpact.Core.Recognition.OpenCv;
+
+[Obsolete]
+public class OldMatchTemplateHelper
{
- public class OldMatchTemplateHelper
- {
- private static readonly ILogger _logger = App.GetLogger();
+ private static readonly ILogger _logger = App.GetLogger();
- public static double WidthScale = 1;
- public static double HeightScale = 1;
+ public static double WidthScale = 1;
+ public static double HeightScale = 1;
- public static Point FindSingleTarget(Bitmap imgSrc, Bitmap imgSub, double threshold = 0.8)
- {
- Mat? srcMat = null;
- Mat? dstMat = null;
- try
- {
- srcMat = imgSrc.ToMat();
- dstMat = imgSub.ToMat();
- return FindSingleTarget(srcMat, dstMat, threshold);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex.ToString());
- return new Point();
- }
- finally
- {
- srcMat?.Dispose();
- dstMat?.Dispose();
- }
- }
+ //public static Point FindSingleTarget(Bitmap imgSrc, Bitmap imgSub, double threshold = 0.8)
+ //{
+ // Mat? srcMat = null;
+ // Mat? dstMat = null;
+ // try
+ // {
+ // srcMat = imgSrc.ToMat();
+ // dstMat = imgSub.ToMat();
+ // return FindSingleTarget(srcMat, dstMat, threshold);
+ // }
+ // catch (Exception ex)
+ // {
+ // _logger.LogError(ex.ToString());
+ // return new Point();
+ // }
+ // finally
+ // {
+ // srcMat?.Dispose();
+ // dstMat?.Dispose();
+ // }
+ //}
- public static Point FindSingleTarget(Mat srcMat, Mat dstMat, double threshold = 0.8)
- {
- Point p = new Point();
+ //public static Point FindSingleTarget(Mat srcMat, Mat dstMat, double threshold = 0.8)
+ //{
+ // Point p = new Point();
- OutputArray? outArray = null;
- try
- {
- dstMat = ResizeHelper.Resize(dstMat, WidthScale);
+ // OutputArray? outArray = null;
+ // try
+ // {
+ // dstMat = ResizeHelper.Resize(dstMat, WidthScale);
- outArray = OutputArray.Create(srcMat);
- Cv2.MatchTemplate(srcMat, dstMat, outArray, TemplateMatchModes.CCoeffNormed);
- double minValue, maxValue;
- Point location, point;
- Cv2.MinMaxLoc(InputArray.Create(outArray.GetMat()), out minValue, out maxValue,
- out location, out point);
+ // outArray = OutputArray.Create(srcMat);
+ // Cv2.MatchTemplate(srcMat, dstMat, outArray, TemplateMatchModes.CCoeffNormed);
+ // double minValue, maxValue;
+ // Point location, point;
+ // Cv2.MinMaxLoc(InputArray.Create(outArray.GetMat()), out minValue, out maxValue,
+ // out location, out point);
- if (maxValue >= threshold)
- {
- p = new Point(point.X + dstMat.Width / 2, point.Y + dstMat.Height / 2);
- //if (VisionContext.Instance().Drawable)
- //{
- //VisionContext.Instance().DrawContent.PutRect("", new System.Windows.Rect(point.X, point.Y, dstMat.Width, dstMat.Height));
- //VisionContext.Instance().DrawContent.TextList
- // .Add(new Tuple(new System.Windows.Point(point.X, point.Y - 10), maxValue.ToString("0.00")));
- //}
- }
+ // if (maxValue >= threshold)
+ // {
+ // p = new Point(point.X + dstMat.Width / 2, point.Y + dstMat.Height / 2);
+ // if (VisionContext.Instance().Drawable)
+ // {
+ // VisionContext.Instance().DrawContent.PutRect("", new System.Windows.Rect(point.X, point.Y, dstMat.Width, dstMat.Height));
+ // VisionContext.Instance().DrawContent.TextList
+ // .Add(new Tuple(new System.Windows.Point(point.X, point.Y - 10), maxValue.ToString("0.00")));
+ // }
+ // }
- return p;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex.ToString());
- return p;
- }
- finally
- {
- outArray?.Dispose();
- }
- }
+ // return p;
+ // }
+ // catch (Exception ex)
+ // {
+ // _logger.LogError(ex.ToString());
+ // return p;
+ // }
+ // finally
+ // {
+ // outArray?.Dispose();
+ // }
+ //}
- public static List FindMultiTarget(Mat srcMat, Mat dstMat, string title, out Mat resMat,
- double threshold = 0.8, int findTargetCount = 8)
- {
- List pointList = new List();
- resMat = srcMat.Clone();
- try
- {
- dstMat = ResizeHelper.Resize(dstMat, WidthScale);
+ //public static List FindMultiTarget(Mat srcMat, Mat dstMat, string title, out Mat resMat,
+ // double threshold = 0.8, int findTargetCount = 8)
+ //{
+ // List pointList = new List();
+ // resMat = srcMat.Clone();
+ // try
+ // {
+ // dstMat = ResizeHelper.Resize(dstMat, WidthScale);
- Mat matchResult = new Mat();
- Cv2.MatchTemplate(srcMat, dstMat, matchResult, TemplateMatchModes.CCoeffNormed);
+ // Mat matchResult = new Mat();
+ // Cv2.MatchTemplate(srcMat, dstMat, matchResult, TemplateMatchModes.CCoeffNormed);
- double minValue = 0;
- double maxValue = 0;
- Point minLoc = new();
+ // double minValue = 0;
+ // double maxValue = 0;
+ // Point minLoc = new();
- //寻找最几个最值的位置
- Mat mask = new Mat(matchResult.Height, matchResult.Width, MatType.CV_8UC1, Scalar.White);
- Mat maskSub = new Mat(matchResult.Height, matchResult.Width, MatType.CV_8UC1, Scalar.Black);
- var point = new OpenCvSharp.Point(0, 0);
- for (int i = 0; i < findTargetCount; i++)
- {
- Cv2.MinMaxLoc(matchResult, out minValue, out maxValue, out minLoc, out point, mask);
- Rect maskRect = new Rect(point.X - dstMat.Width / 2, point.Y - dstMat.Height / 2, dstMat.Width,
- dstMat.Height);
- maskSub.Rectangle(maskRect, Scalar.White, -1);
- mask -= maskSub;
- if (maxValue >= threshold)
- {
- pointList.Add(new Point(point.X + dstMat.Width / 2, point.Y + dstMat.Height / 2));
+ // //寻找最几个最值的位置
+ // Mat mask = new Mat(matchResult.Height, matchResult.Width, MatType.CV_8UC1, Scalar.White);
+ // Mat maskSub = new Mat(matchResult.Height, matchResult.Width, MatType.CV_8UC1, Scalar.Black);
+ // var point = new OpenCvSharp.Point(0, 0);
+ // for (int i = 0; i < findTargetCount; i++)
+ // {
+ // Cv2.MinMaxLoc(matchResult, out minValue, out maxValue, out minLoc, out point, mask);
+ // Rect maskRect = new Rect(point.X - dstMat.Width / 2, point.Y - dstMat.Height / 2, dstMat.Width,
+ // dstMat.Height);
+ // maskSub.Rectangle(maskRect, Scalar.White, -1);
+ // mask -= maskSub;
+ // if (maxValue >= threshold)
+ // {
+ // pointList.Add(new Point(point.X + dstMat.Width / 2, point.Y + dstMat.Height / 2));
- //if (VisionContext.Instance().Drawable)
- //{
- // VisionContext.Instance().DrawContent.RectList
- // .Add(new System.Windows.Rect(point.X, point.Y, dstMat.Width, dstMat.Height));
- // VisionContext.Instance().DrawContent.TextList
- // .Add(new Tuple(new System.Windows.Point(point.X, point.Y - 10), maxValue.ToString("0.00")));
- //}
- //if (IsDebug)
- //{
- // VisionContext.Instance().Log
- // ?.LogInformation(title + " " + maxValue.ToString("0.000") + " " + point);
- // Cv2.Rectangle(resMat, point,
- // new OpenCvSharp.Point(point.X + dstMat.Width, point.Y + dstMat.Height),
- // Scalar.Red, 2);
- // Cv2.PutText(resMat, title + " " + maxValue.ToString("0.00"),
- // new OpenCvSharp.Point(point.X, point.Y - 10),
- // HersheyFonts.HersheySimplex, 0.5, Scalar.Red);
- //}
- }
- else
- {
- break;
- }
- }
+ // //if (VisionContext.Instance().Drawable)
+ // //{
+ // // VisionContext.Instance().DrawContent.RectList
+ // // .Add(new System.Windows.Rect(point.X, point.Y, dstMat.Width, dstMat.Height));
+ // // VisionContext.Instance().DrawContent.TextList
+ // // .Add(new Tuple(new System.Windows.Point(point.X, point.Y - 10), maxValue.ToString("0.00")));
+ // //}
+ // //if (IsDebug)
+ // //{
+ // // VisionContext.Instance().Log
+ // // ?.LogInformation(title + " " + maxValue.ToString("0.000") + " " + point);
+ // // Cv2.Rectangle(resMat, point,
+ // // new OpenCvSharp.Point(point.X + dstMat.Width, point.Y + dstMat.Height),
+ // // Scalar.Red, 2);
+ // // Cv2.PutText(resMat, title + " " + maxValue.ToString("0.00"),
+ // // new OpenCvSharp.Point(point.X, point.Y - 10),
+ // // HersheyFonts.HersheySimplex, 0.5, Scalar.Red);
+ // //}
+ // }
+ // else
+ // {
+ // break;
+ // }
+ // }
- return pointList;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex.ToString());
- return pointList;
- }
- finally
- {
- srcMat?.Dispose();
- dstMat?.Dispose();
- }
- }
+ // return pointList;
+ // }
+ // catch (Exception ex)
+ // {
+ // _logger.LogError(ex.ToString());
+ // return pointList;
+ // }
+ // finally
+ // {
+ // srcMat?.Dispose();
+ // dstMat?.Dispose();
+ // }
+ //}
- public static Dictionary> FindMultiPicFromOneImage(Bitmap imgSrc,
- Dictionary imgSubDictionary, double threshold = 0.8)
- {
- Dictionary> dictionary = new Dictionary>();
- Mat srcMat = imgSrc.ToMat();
- Mat resMat;
+ //public static Dictionary> FindMultiPicFromOneImage(Bitmap imgSrc,
+ // Dictionary imgSubDictionary, double threshold = 0.8)
+ //{
+ // Dictionary> dictionary = new Dictionary>();
+ // Mat srcMat = imgSrc.ToMat();
+ // Mat resMat;
- foreach (KeyValuePair kvp in imgSubDictionary)
- {
- dictionary.Add(kvp.Key, FindMultiTarget(srcMat, kvp.Value.ToMat(), kvp.Key, out resMat, threshold));
- srcMat = resMat.Clone();
- }
+ // foreach (KeyValuePair kvp in imgSubDictionary)
+ // {
+ // dictionary.Add(kvp.Key, FindMultiTarget(srcMat, kvp.Value.ToMat(), kvp.Key, out resMat, threshold));
+ // srcMat = resMat.Clone();
+ // }
- return dictionary;
- }
+ // return dictionary;
+ //}
- public static Dictionary> FindMultiPicFromOneImage(Mat srcMat,
- Dictionary imgSubDictionary, double threshold = 0.8)
- {
- Dictionary> dictionary = new Dictionary>();
- Mat resMat;
- foreach (KeyValuePair kvp in imgSubDictionary)
- {
- dictionary.Add(kvp.Key, FindMultiTarget(srcMat, kvp.Value.ToMat(), kvp.Key, out resMat, threshold));
- srcMat = resMat.Clone();
- }
+ //public static Dictionary> FindMultiPicFromOneImage(Mat srcMat,
+ // Dictionary imgSubDictionary, double threshold = 0.8)
+ //{
+ // Dictionary> dictionary = new Dictionary>();
+ // Mat resMat;
+ // foreach (KeyValuePair kvp in imgSubDictionary)
+ // {
+ // dictionary.Add(kvp.Key, FindMultiTarget(srcMat, kvp.Value.ToMat(), kvp.Key, out resMat, threshold));
+ // srcMat = resMat.Clone();
+ // }
- return dictionary;
- }
- }
+ // return dictionary;
+ //}
}
\ No newline at end of file
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_anemo.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_anemo.png
new file mode 100644
index 00000000..b0e95226
Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_anemo.png differ
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_cryo.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_cryo.png
new file mode 100644
index 00000000..0373df92
Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_cryo.png differ
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_dendro.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_dendro.png
new file mode 100644
index 00000000..ffe18b2d
Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_dendro.png differ
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_electro.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_electro.png
new file mode 100644
index 00000000..a5fcb6d9
Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_electro.png differ
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_geo.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_geo.png
new file mode 100644
index 00000000..996c6c01
Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_geo.png differ
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_hydro.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_hydro.png
new file mode 100644
index 00000000..b44962df
Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_hydro.png differ
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_omni.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_omni.png
new file mode 100644
index 00000000..e735dabb
Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_omni.png differ
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_pyro.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_pyro.png
new file mode 100644
index 00000000..e558810a
Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_pyro.png differ
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_anemo.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_anemo.png
new file mode 100644
index 00000000..b7325d0f
Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_anemo.png differ
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_cryo.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_cryo.png
new file mode 100644
index 00000000..92ef3ec9
Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_cryo.png differ
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_dendro.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_dendro.png
new file mode 100644
index 00000000..3a1a82b5
Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_dendro.png differ
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_electro.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_electro.png
new file mode 100644
index 00000000..c6ca2d57
Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_electro.png differ
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_geo.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_geo.png
new file mode 100644
index 00000000..52de0a99
Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_geo.png differ
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_hydro.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_hydro.png
new file mode 100644
index 00000000..6c5d0641
Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_hydro.png differ
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_omni.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_omni.png
new file mode 100644
index 00000000..9c17eac0
Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_omni.png differ
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_pyro.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_pyro.png
new file mode 100644
index 00000000..3c3a563f
Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_pyro.png differ
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/元素调和.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/元素调和.png
new file mode 100644
index 00000000..d94d2869
Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/元素调和.png differ
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/元素骰子不足.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/元素骰子不足.png
new file mode 100644
index 00000000..763160b8
Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/元素骰子不足.png differ
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/冻结.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/冻结.png
new file mode 100644
index 00000000..00e0e67e
Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/冻结.png differ
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/出战角色.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/出战角色.png
new file mode 100644
index 00000000..fc49ffe0
Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/出战角色.png differ
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/回合结束.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/回合结束.png
new file mode 100644
index 00000000..1017cbd9
Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/回合结束.png differ
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/回合结算阶段.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/回合结算阶段.png
new file mode 100644
index 00000000..707c8201
Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/回合结算阶段.png differ
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/对方行动中.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/对方行动中.png
new file mode 100644
index 00000000..256b1b4f
Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/对方行动中.png differ
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/满能量.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/满能量.png
new file mode 100644
index 00000000..b438849f
Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/满能量.png differ
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/确定.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/确定.png
new file mode 100644
index 00000000..37c12d1d
Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/确定.png differ
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/确定_1600x900.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/确定_1600x900.png
new file mode 100644
index 00000000..cfa6128d
Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/确定_1600x900.png differ
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/空能量.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/空能量.png
new file mode 100644
index 00000000..1657ba7c
Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/空能量.png differ
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色死亡.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色死亡.png
new file mode 100644
index 00000000..7ddb50fa
Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色死亡.png differ
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色状态_冻结.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色状态_冻结.png
new file mode 100644
index 00000000..aa5172dd
Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色状态_冻结.png differ
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色状态_冻结2.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色状态_冻结2.png
new file mode 100644
index 00000000..00e0e67e
Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色状态_冻结2.png differ
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色状态_水泡.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色状态_水泡.png
new file mode 100644
index 00000000..bfaed8ac
Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色状态_水泡.png differ
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色血量上方.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色血量上方.png
new file mode 100644
index 00000000..92873cfa
Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色血量上方.png differ
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色被打败.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色被打败.png
new file mode 100644
index 00000000..a967378c
Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色被打败.png differ
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/退出挑战.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/退出挑战.png
new file mode 100644
index 00000000..b049f962
Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/退出挑战.png differ
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/AutoGeniusInvokationAssets.cs b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/AutoGeniusInvokationAssets.cs
new file mode 100644
index 00000000..30676c5b
--- /dev/null
+++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/AutoGeniusInvokationAssets.cs
@@ -0,0 +1,154 @@
+using BetterGenshinImpact.Core.Config;
+using OpenCvSharp;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using BetterGenshinImpact.Core.Recognition;
+
+namespace BetterGenshinImpact.GameTask.AutoGeniusInvokation.Assets
+{
+ public class AutoGeniusInvokationAssets
+ {
+ public RecognitionObject ConfirmButtonRo;
+ public RecognitionObject RoundEndButtonRo;
+ public RecognitionObject ElementalTuningConfirmButtonRo;
+ public RecognitionObject ExitDuelButtonRo;
+
+ public RecognitionObject InOpponentActionRo;
+ public RecognitionObject EndPhaseRo;
+ public RecognitionObject ElementalDiceLackWarningRo;
+ public RecognitionObject CharacterTakenOutRo;
+ public Mat CharacterDefeatedMat;
+ public RecognitionObject InCharacterPickRo;
+
+ // 角色区域
+ public RecognitionObject CharacterHpUpperRo;
+ public Mat CharacterStatusFreezeMat;
+ public Mat CharacterStatusDizzinessMat;
+ public Mat CharacterEnergyOnMat;
+
+ public Dictionary RollPhaseDiceMats;
+ public Dictionary ActionPhaseDiceMats;
+
+ public AutoGeniusInvokationAssets()
+ {
+ var info = TaskContext.Instance().SystemInfo;
+ ConfirmButtonRo = new RecognitionObject
+ {
+ Name = "ConfirmButton",
+ RecognitionType = RecognitionTypes.TemplateMatch,
+ TemplateImageMat = GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"other\确定.png"),
+ DrawOnWindow = true
+ }.InitTemplate();
+ RoundEndButtonRo = new RecognitionObject
+ {
+ Name = "RoundEndButton",
+ RecognitionType = RecognitionTypes.TemplateMatch,
+ TemplateImageMat = GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"other\回合结束.png"),
+ RegionOfInterest = new Rect(0, 0, info.CaptureAreaRect.Width / 5, info.CaptureAreaRect.Height),
+ DrawOnWindow = true
+ }.InitTemplate();
+ ElementalTuningConfirmButtonRo = new RecognitionObject
+ {
+ Name = "ElementalTuningConfirmButton",
+ RecognitionType = RecognitionTypes.TemplateMatch,
+ TemplateImageMat = GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"other\元素调和.png"),
+ DrawOnWindow = true
+ }.InitTemplate();
+ ExitDuelButtonRo = new RecognitionObject
+ {
+ Name = "ExitDuelButton",
+ RecognitionType = RecognitionTypes.TemplateMatch,
+ TemplateImageMat = GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"other\退出挑战.png"),
+ RegionOfInterest = new Rect(0, info.CaptureAreaRect.Height / 2, info.CaptureAreaRect.Width / 2, info.CaptureAreaRect.Height - info.CaptureAreaRect.Height / 2),
+ DrawOnWindow = true
+ }.InitTemplate();
+ InOpponentActionRo = new RecognitionObject
+ {
+ Name = "InOpponentAction",
+ RecognitionType = RecognitionTypes.TemplateMatch,
+ TemplateImageMat = GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"other\对方行动中.png"),
+ RegionOfInterest = new Rect(0, 0, info.CaptureAreaRect.Width / 5, info.CaptureAreaRect.Height),
+ DrawOnWindow = true
+ }.InitTemplate();
+ EndPhaseRo = new RecognitionObject
+ {
+ Name = "EndPhase",
+ RecognitionType = RecognitionTypes.TemplateMatch,
+ TemplateImageMat = GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"other\回合结算阶段.png"),
+ RegionOfInterest = new Rect(0, 0, info.CaptureAreaRect.Width / 5, info.CaptureAreaRect.Height),
+ DrawOnWindow = true
+ }.InitTemplate();
+ ElementalDiceLackWarningRo = new RecognitionObject
+ {
+ Name = "ElementalDiceLackWarning",
+ RecognitionType = RecognitionTypes.TemplateMatch,
+ TemplateImageMat = GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"other\元素骰子不足.png"),
+ RegionOfInterest = new Rect(info.CaptureAreaRect.Width - info.CaptureAreaRect.Width / 2, 0,
+ info.CaptureAreaRect.Width / 2, info.CaptureAreaRect.Height),
+ DrawOnWindow = true
+ }.InitTemplate();
+ CharacterTakenOutRo = new RecognitionObject
+ {
+ Name = "CharacterTakenOut",
+ RecognitionType = RecognitionTypes.TemplateMatch,
+ TemplateImageMat = GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"other\角色死亡.png"),
+ DrawOnWindow = true
+ }.InitTemplate();
+
+
+ CharacterDefeatedMat = GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"other\角色被打败.png");
+
+ InCharacterPickRo = new RecognitionObject
+ {
+ Name = "InCharacterPick",
+ RecognitionType = RecognitionTypes.TemplateMatch,
+ TemplateImageMat = GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"other\出战角色.png"),
+ RegionOfInterest = new Rect(info.CaptureAreaRect.Width / 2, info.CaptureAreaRect.Height / 2,
+ info.CaptureAreaRect.Width - info.CaptureAreaRect.Width / 2,
+ info.CaptureAreaRect.Height - info.CaptureAreaRect.Height / 2),
+ DrawOnWindow = true
+ }.InitTemplate();
+ CharacterHpUpperRo = new RecognitionObject
+ {
+ Name = "CharacterHpUpper",
+ RecognitionType = RecognitionTypes.TemplateMatch,
+ TemplateImageMat = GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"other\角色血量上方.png"),
+ DrawOnWindow = true
+ }.InitTemplate();
+
+
+ CharacterStatusFreezeMat = GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"other\角色状态_冻结.png");
+ CharacterStatusDizzinessMat = GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"other\角色状态_水泡.png");
+ CharacterEnergyOnMat = GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"other\满能量.png");
+
+ // 投掷期间的骰子
+ RollPhaseDiceMats = new Dictionary()
+ {
+ { "anemo", GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"dice\roll_anemo.png") },
+ { "geo", GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"dice\roll_geo.png.png") },
+ { "electro", GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"dice\roll_electro.png.png") },
+ { "dendro", GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"dice\roll_dendro.png.png") },
+ { "hydro", GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"dice\roll_hydro.png.png") },
+ { "pyro", GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"dice\roll_pyro.png.png") },
+ { "cryo", GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"dice\roll_cryo.png.png") },
+ { "omni", GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"dice\roll_omni.png.png") },
+ };
+
+ // 主界面骰子
+ ActionPhaseDiceMats = new Dictionary()
+ {
+ { "anemo", GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"dice\action_anemo.png.png") },
+ { "geo", GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"dice\action_geo.png.png") },
+ { "electro", GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"dice\action_electro.png.png") },
+ { "dendro", GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"dice\action_dendro.png.png") },
+ { "hydro", GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"dice\action_hydro.png.png") },
+ { "pyro", GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"dice\action_pyro.png.png") },
+ { "cryo", GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"dice\action_cryo.png.png") },
+ { "omni", GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"dice\action_omni.png.png") },
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Exception/DuelEndException.cs b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Exception/DuelEndException.cs
new file mode 100644
index 00000000..67357681
--- /dev/null
+++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Exception/DuelEndException.cs
@@ -0,0 +1,8 @@
+namespace BetterGenshinImpact.GameTask.AutoGeniusInvokation.Exception;
+
+public class DuelEndException: System.Exception
+{
+ public DuelEndException(string message) : base(message)
+ {
+ }
+}
\ No newline at end of file
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Exception/RetryException.cs b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Exception/RetryException.cs
new file mode 100644
index 00000000..79435647
--- /dev/null
+++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Exception/RetryException.cs
@@ -0,0 +1,12 @@
+namespace BetterGenshinImpact.GameTask.AutoGeniusInvokation.Exception;
+
+public class RetryException : System.Exception
+{
+ public RetryException() : base()
+ {
+ }
+
+ public RetryException(string message) : base(message)
+ {
+ }
+}
\ No newline at end of file
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/GeniusInvokationControl.cs b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/GeniusInvokationControl.cs
new file mode 100644
index 00000000..5b9a5296
--- /dev/null
+++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/GeniusInvokationControl.cs
@@ -0,0 +1,1271 @@
+using BetterGenshinImpact.Core.Recognition.OpenCv;
+using BetterGenshinImpact.GameTask.AutoGeniusInvokation.Assets;
+using BetterGenshinImpact.GameTask.AutoGeniusInvokation.Exception;
+using BetterGenshinImpact.GameTask.AutoGeniusInvokation.Model;
+using BetterGenshinImpact.GameTask.Model;
+using BetterGenshinImpact.Helpers.Extensions;
+using Fischless.GameCapture;
+using GeniusInvokationAutoToy.Utils;
+using Microsoft.Extensions.Logging;
+using OpenCvSharp;
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Point = OpenCvSharp.Point;
+
+namespace BetterGenshinImpact.GameTask.AutoGeniusInvokation;
+
+///
+/// 用于操控游戏
+///
+public class GeniusInvokationControl
+{
+ private readonly ILogger _logger = App.GetLogger();
+
+ // 定义一个静态变量来保存类的实例
+ private static GeniusInvokationControl? _uniqueInstance;
+
+ // 定义一个标识确保线程同步
+ private static readonly object _locker = new object();
+
+ // 定义私有构造函数,使外界不能创建该类实例
+ private GeniusInvokationControl()
+ {
+ }
+
+ ///
+ /// 定义公有方法提供一个全局访问点,同时你也可以定义公有属性来提供全局访问点
+ ///
+ ///
+ public static GeniusInvokationControl GetInstance()
+ {
+ if (_uniqueInstance == null)
+ {
+ lock (_locker)
+ {
+ _uniqueInstance ??= new GeniusInvokationControl();
+ }
+ }
+
+ return _uniqueInstance;
+ }
+
+ public static bool OutputImageWhenError = true;
+
+
+ public CancellationTokenSource? cts;
+
+ //public ImageCapture capture = new ImageCapture();
+ //public YuanShenWindow window = new YuanShenWindow();
+ //public Rectangle windowRect;
+
+ private AutoGeniusInvokationAssets _assets = new();
+
+ private IGameCapture? _capture;
+
+ public void Init(CancellationTokenSource cts1)
+ {
+ cts = cts1;
+ //if (!window.FindYSHandle())
+ //{
+ // throw new Exception("未找到原神进程,请先启动原神!");
+ //}
+ }
+
+ //public void FocusGameWindow()
+ //{
+ // //window.Focus();
+ //}
+
+ //public string GetActiveProcessName()
+ //{
+ // try
+ // {
+ // IntPtr hwnd = Native.GetForegroundWindow();
+ // uint pid;
+ // GetWindowThreadProcessId(hwnd, out pid);
+ // Process p = Process.GetProcessById((int)pid);
+ // return p.ProcessName;
+ // }
+ // catch
+ // {
+ // return null;
+ // }
+ //}
+
+ //public void IsGameFocus()
+ //{
+ // windowRect = GetWindowRealRect(window);
+ // capture.Start(windowRect.X, windowRect.Y, windowRect.Width, windowRect.Height);
+ // if (windowRect.Width < 800)
+ // {
+ // throw new RetryException("原神窗口宽度小于800,请确认游戏已经获取到焦点!");
+ // }
+ //}
+
+
+ // public Rectangle GetWindowRealRect(YuanShenWindow yuanShenWindow)
+ // {
+ // Rectangle rc = yuanShenWindow.GetSize();
+ // int x = (int)Math.Ceiling(rc.Location.X * PrimaryScreen.ScaleX);
+ // int y = (int)Math.Ceiling(rc.Location.Y * PrimaryScreen.ScaleY);
+ // int w = (int)Math.Ceiling(rc.Width * PrimaryScreen.ScaleX);
+ // int h = (int)Math.Ceiling(rc.Height * PrimaryScreen.ScaleY);
+ // MyLogger.Debug($"原神窗口大小:{rc.Width} x {rc.Height}");
+ // MyLogger.Debug($"原神窗口大小(计算DPI缩放后):{rc.Width * PrimaryScreen.ScaleX} x {rc.Height * PrimaryScreen.ScaleY}");
+ //
+ // System.Drawing.Size size = new System.Drawing.Size(1920, 1080);
+ // if (w >= 1920 && w < 2000 && h >= 1080 && h < 1150)
+ // {
+ // size = new System.Drawing.Size(1920, 1080);
+ // ImageRecognition.WidthScale = 1;
+ // //ImageRecognition.HeightScale = 1;
+ // }
+ // else if (w >= 1600 && w < 2000 && h >= 900 && h < 950)
+ // {
+ // size = new System.Drawing.Size(1600, 900);
+ // ImageRecognition.WidthScale = 1600 * 1.0 / 1920;
+ // //ImageRecognition.HeightScale = 900 * 1.0 / 1080;
+ // }
+ // else
+ // {
+ // ImageRecognition.WidthScale = w * 1.0 / 1929; // 1929是计算了边缘
+ // //ImageRecognition.HeightScale = h * 1.0 / 1109; // 1109是计算了窗口标题栏
+ // }
+ //
+ // MyLogger.Debug($"匹配图片缩放比率:{ImageRecognition.WidthScale}");
+ // return new Rectangle(x, y, w, h);
+ // }
+
+ public void Sleep(int millisecondsTimeout)
+ {
+ CheckTask();
+ Thread.Sleep(millisecondsTimeout);
+ }
+
+ public Mat Capture()
+ {
+ CheckTask();
+ //return capture.Capture();
+ return null;
+ }
+
+ public RectArea CaptureRectArea()
+ {
+ CheckTask();
+ var bitmap = _capture.Capture();
+ if (bitmap == null)
+ {
+ _logger.LogWarning("截图失败!");
+ throw new RetryException("截图失败");
+ }
+
+ var systemInfo = TaskContext.Instance().SystemInfo;
+ return new RectArea(bitmap, systemInfo.CaptureAreaRect.X, systemInfo.CaptureAreaRect.Y, systemInfo.DesktopRectArea);
+ ;
+ }
+
+ public void CheckTask()
+ {
+ Retry.Do(() =>
+ {
+ if (cts != null && cts.IsCancellationRequested)
+ {
+ return;
+ }
+
+ string name = "GetActiveProcessName()";
+ if (!string.IsNullOrEmpty(name) && name != "YuanShen" && name != "GenshinImpact")
+ {
+ _logger.LogWarning("当前获取焦点的窗口进程名:{},不是原神窗口,暂停", name);
+ throw new RetryException("当前获取焦点的窗口不是原神窗口");
+ }
+ }, TimeSpan.FromSeconds(1), 100);
+
+
+ if (cts != null && cts.IsCancellationRequested)
+ {
+ throw new TaskCanceledException("任务取消");
+ }
+ }
+
+ public void CommonDuelPrepare()
+ {
+ // 1. 选择初始手牌
+ Sleep(1000);
+ _logger.LogInformation("开始选择初始手牌");
+ while (!ClickConfirm())
+ {
+ // 循环等待选择卡牌画面
+ Sleep(1000);
+ }
+
+ _logger.LogInformation("点击确认");
+
+ // 2. 选择出战角色
+ // 此处选择第2个角色 雷神
+ _logger.LogInformation("等待3s对局准备...");
+ Sleep(3000);
+
+ // 是否是再角色出战选择界面
+ Retry.Do(IsInCharacterPickRetryThrowable, TimeSpan.FromSeconds(0.8), 20);
+ _logger.LogInformation("识别到已经在角色出战界面,等待1.5s");
+ Sleep(1500);
+ }
+
+ ///
+ /// 获取我方三个角色卡牌区域
+ ///
+ ///
+ public List GetCharacterRects()
+ {
+ Mat srcMat = Capture();
+ int halfHeight = srcMat.Height / 2;
+ Mat bottomMat = new Mat(srcMat, new Rect(0, halfHeight, srcMat.Width, srcMat.Height - halfHeight));
+
+ var lowPurple = new Scalar(235, 245, 198);
+ var highPurple = new Scalar(255, 255, 236);
+ Mat gray = OpenCvCommonHelper.Threshold(bottomMat, lowPurple, highPurple);
+
+ // 水平投影到y轴 正常只有一个连续区域
+ int[] h = ArithmeticHelper.HorizontalProjection(gray);
+
+ // y轴 从上到下确认连续区域
+ int y1 = 0, y2 = 0;
+ int start = 0;
+ bool inLine = false;
+ for (int i = 0; i < h.Length; i++)
+ {
+ if (h[i] > h.Average() * 10)
+ {
+ // 直方图
+ Cv2.Line(bottomMat, 0, i, h[i], i, Scalar.Yellow);
+
+ if (!inLine)
+ {
+ //由空白进入字符区域了,记录标记
+ inLine = true;
+ start = i;
+ }
+ }
+ else if (inLine)
+ {
+ //由连续区域进入空白区域了
+ inLine = false;
+
+
+ if (y1 == 0)
+ {
+ y1 = start;
+ if (OutputImageWhenError)
+ {
+ Cv2.Line(bottomMat, 0, y1, bottomMat.Width, y1, Scalar.Red);
+ }
+ }
+ else if (y2 == 0 && i - y1 > 20)
+ {
+ y2 = i;
+ if (OutputImageWhenError)
+ {
+ Cv2.Line(bottomMat, 0, y2, bottomMat.Width, y2, Scalar.Red);
+ }
+
+ break;
+ }
+ }
+ }
+
+ if (y1 == 0 || y2 == 0)
+ {
+ _logger.LogWarning("未识别到角色卡牌区域(Y轴)");
+ if (OutputImageWhenError)
+ {
+ Cv2.ImWrite("logs\\character_card_error.jpg", bottomMat);
+ }
+
+ throw new RetryException("未获取到角色区域");
+ }
+
+ //if (y1 < windowRect.Height / 2 || y2 < windowRect.Height / 2)
+ //{
+ // MyLogger.Warn("识别的角色卡牌区域(Y轴)错误:y1:{} y2:{}", y1, y2);
+ // if (OutputImageWhenError)
+ // {
+ // Cv2.ImWrite("logs\\character_card_error.jpg", bottomMat);
+ // }
+
+ // throw new RetryException("未获取到角色区域");
+ //}
+
+
+ // 垂直投影
+ int[] v = ArithmeticHelper.VerticalProjection(gray);
+
+
+ inLine = false;
+ start = 0;
+ List colLines = new List();
+ //开始根据投影值识别分割点
+ for (int i = 0; i < v.Length; ++i)
+ {
+ if (v[i] > h.Average() * 10)
+ {
+ if (OutputImageWhenError)
+ {
+ Cv2.Line(bottomMat, i, 0, i, v[i], Scalar.Yellow);
+ }
+
+ if (!inLine)
+ {
+ //由空白进入字符区域了,记录标记
+ inLine = true;
+ start = i;
+ }
+ }
+ else if (i - start > 30 && inLine)
+ {
+ //由连续区域进入空白区域了
+ inLine = false;
+ if (OutputImageWhenError)
+ {
+ Cv2.Line(bottomMat, start, 0, start, bottomMat.Height, Scalar.Red);
+ }
+
+ colLines.Add(start);
+ }
+ }
+
+ if (colLines.Count != 6)
+ {
+ _logger.LogWarning("未识别到角色卡牌区域(X轴存在{Count}个识别点)", colLines.Count);
+ if (OutputImageWhenError)
+ {
+ Cv2.ImWrite("logs\\character_card_error.jpg", bottomMat);
+ }
+
+ throw new RetryException("未获取到角色区域");
+ }
+
+ var rects = new List();
+ for (int i = 0; i < colLines.Count - 1; i++)
+ {
+ if (i % 2 == 0)
+ {
+ var r = new Rect(colLines[i], halfHeight + y1, colLines[i + 1] - colLines[i],
+ y2 - y1);
+ rects.Add(r);
+ }
+ }
+
+ if (rects == null || rects.Count != 3)
+ {
+ throw new RetryException("未获取到角色区域");
+ }
+
+ //Cv2.ImWrite("logs\\character_card_success.jpg", bottomMat);
+ return rects;
+ }
+
+ /////
+ ///// 选择角色牌(首次)
+ /////
+ ///// 从左到右第几个角色,从1开始计数
+ /////
+ //public void ChooseCharacterFirst(int characterIndex)
+ //{
+ // // 首次执行获取角色区域
+ // if (MyCharacterRects == null || MyCharacterRects.Count == 0)
+ // {
+ // MyCharacterRects = GetCharacterRects();
+ // if (MyCharacterRects == null || MyCharacterRects.Count != 3)
+ // {
+ // throw new RetryException("未获取到角色区域");
+ // }
+ // }
+
+
+ // // 双击选择角色出战
+ // DoubleClick(MakeOffset(MyCharacterRects[characterIndex - 1].GetCenterPoint()));
+ //}
+
+
+ /////
+ ///// 切换角色牌(历次)
+ /////
+ ///// 从左到右第几个角色,从1开始计数
+ /////
+ //public bool SwitchCharacterLater(int characterIndex)
+ //{
+ // if (MyCharacterRects == null || MyCharacterRects.Count != 3)
+ // {
+ // return false;
+ // }
+
+ // Point p = MakeOffset(MyCharacterRects[characterIndex - 1].GetCenterPoint());
+ // // 选择角色
+ // Click(p);
+ // // // 双击切人
+ // // Sleep(1500);
+ // // Click(p);
+
+ // // 点击切人按钮
+ // ActionPhasePressSwitchButton();
+ // return true;
+ //}
+
+ /////
+ ///// 角色死亡的时候双击角色牌重新出战
+ /////
+ ///// 从左到右第几个角色,从1开始计数
+ /////
+ //public bool SwitchCharacterWhenTakenOut(int characterIndex)
+ //{
+ // if (MyCharacterRects == null || MyCharacterRects.Count != 3)
+ // {
+ // return false;
+ // }
+
+ // Point p = MakeOffset(MyCharacterRects[characterIndex - 1].GetCenterPoint());
+ // // 选择角色
+ // Click(p);
+ // // 双击切人
+ // Sleep(500);
+ // Click(p);
+ // Sleep(300);
+ // return true;
+ //}
+
+
+ public bool Click(Point p)
+ {
+ return false;
+ }
+
+ public bool Click(int x, int y)
+ {
+ return false;
+ }
+
+ ///
+ /// 点击捕获区域的相对位置
+ ///
+ ///
+ ///
+ ///
+ public void ClickCaptureArea(int x, int y)
+ {
+ var rect = TaskContext.Instance().SystemInfo.CaptureAreaRect;
+ ClickExtension.Click(rect.X + x, rect.Y + y);
+ }
+
+ ///
+ /// 点击游戏屏幕中心点
+ ///
+ public void ClickGameWindowCenter()
+ {
+ var p = TaskContext.Instance().SystemInfo.CaptureAreaRect.GetCenterPoint();
+ p.Click();
+ }
+
+ public static Dictionary> FindMultiPicFromOneImage(Mat srcMat,
+ Dictionary imgSubDictionary, double threshold = 0.8)
+ {
+ var dictionary = new Dictionary>();
+
+
+ foreach (var kvp in imgSubDictionary)
+ {
+ dictionary.Add(kvp.Key, MatchTemplateHelper.MatchTemplateMulti(srcMat, kvp.Value, threshold));
+ // 最好把结果给遮掩掉,避免识别率不高的时候重复识别
+ }
+
+ return dictionary;
+ }
+
+ ///
+ /// 重投骰子
+ ///
+ /// 保留的元素类型
+ public bool RollPhaseReRoll(params ElementalType[] holdElementalTypes)
+ {
+ var gameSnapshot = Capture();
+ var dictionary = FindMultiPicFromOneImage(gameSnapshot, _assets.RollPhaseDiceMats);
+
+ var count = dictionary.Sum(kvp => kvp.Value.Count);
+
+
+ if (count != 8)
+ {
+ _logger.LogDebug("投骰子界面识别到了{Count}个骰子,等待重试", count);
+ return false;
+ }
+ else
+ {
+ _logger.LogInformation("投骰子界面识别到了{Count}个骰子", count);
+ }
+
+ foreach (var kvp in dictionary)
+ {
+ // 跳过保留的元素类型
+ if (holdElementalTypes.Contains(kvp.Key.ToElementalType()))
+ {
+ continue;
+ }
+
+ // 选中重投
+ foreach (var point in kvp.Value)
+ {
+ ClickCaptureArea(point.X + _assets.RollPhaseDiceMats[kvp.Key].Width / 2, point.Y + _assets.RollPhaseDiceMats[kvp.Key].Height / 2);
+ Sleep(100);
+ }
+ }
+
+ return true;
+ }
+
+ ///
+ /// 选择手牌/重投骰子 确认
+ ///
+ public bool ClickConfirm()
+ {
+ var foundRectArea = CaptureRectArea().Find(_assets.ConfirmButtonRo);
+ if (!foundRectArea.IsEmpty())
+ {
+ foundRectArea.ClickCenter();
+ return true;
+ }
+
+ return false;
+ }
+
+ public void ReRollDice(params ElementalType[] holdElementalTypes)
+ {
+ // 3.重投骰子
+ _logger.LogInformation("等待5s投骰动画...");
+
+ var msg = holdElementalTypes.Aggregate(" ", (current, elementalType) => current + (elementalType.ToChinese() + " "));
+
+ _logger.LogInformation("保留{Msg}骰子", msg);
+ Sleep(5000);
+ var retryCount = 0;
+ // 保留 x、万能 骰子
+ while (!RollPhaseReRoll(holdElementalTypes))
+ {
+ retryCount++;
+
+ if (IsDuelEnd())
+ {
+ throw new DuelEndException("对战已结束,停止自动打牌!");
+ }
+
+ //MyLogger.Debug("识别骰子数量不正确,第{}次重试中...", retryCount);
+ Sleep(500);
+ if (retryCount > 35)
+ {
+ throw new System.Exception("识别骰子数量不正确,重试超时,停止自动打牌!");
+ }
+ }
+
+ ClickConfirm();
+ _logger.LogInformation("选择需要重投的骰子后点击确认完毕");
+
+ Sleep(1000);
+ // 鼠标移动到中心
+ ClickGameWindowCenter();
+
+ _logger.LogInformation("等待10s对方重投");
+ Sleep(10000);
+ }
+
+ public Point MakeOffset(Point p)
+ {
+ var rect = TaskContext.Instance().SystemInfo.CaptureAreaRect;
+ return new Point(rect.X + p.X, rect.Y + p.Y);
+ }
+
+ ///
+ /// 计算当前有那些骰子
+ ///
+ ///
+ public Dictionary ActionPhaseDice()
+ {
+ var srcMat = Capture();
+ // 切割图片后再识别 加快速度 位置没啥用,所以切割后比较方便
+ var dictionary = FindMultiPicFromOneImage(CutRight(srcMat, srcMat.Width / 5), _assets.ActionPhaseDiceMats);
+
+ var msg = "";
+ var result = new Dictionary();
+ foreach (var kvp in dictionary)
+ {
+ result.Add(kvp.Key, kvp.Value.Count);
+ msg += $"{kvp.Key.ToElementalType().ToChinese()} {kvp.Value.Count}| ";
+ }
+
+ _logger.LogInformation("当前骰子状态:{Res}", result);
+ return result;
+ }
+
+
+ ///
+ /// 烧牌
+ ///
+ public void ActionPhaseElementalTuning()
+ {
+ var rect = TaskContext.Instance().SystemInfo.CaptureAreaRect;
+ var m = ClickExtension.Move(rect.X + rect.Width / 2d, rect.Y + rect.Height - 50).LeftButtonClick();
+ Sleep(1500);
+ m.LeftButtonDown();
+ Sleep(100);
+ m = ClickExtension.Move(rect.X + rect.Width - 50, rect.Y + rect.Height / 2d);
+ Sleep(100);
+ m.LeftButtonUp();
+ }
+
+ ///
+ /// 烧牌确认(元素调和按钮)
+ ///
+ public bool ActionPhaseElementalTuningConfirm()
+ {
+ var foundRectArea = CaptureRectArea().Find(_assets.ElementalTuningConfirmButtonRo);
+ if (!foundRectArea.IsEmpty())
+ {
+ foundRectArea.ClickCenter();
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// 点击切人按钮
+ ///
+ ///
+ public void ActionPhasePressSwitchButton()
+ {
+ var info = TaskContext.Instance().SystemInfo;
+ var x = info.CaptureAreaRect.X + info.CaptureAreaRect.Width - 100 * info.AssetScale;
+ var y = info.CaptureAreaRect.Y + info.CaptureAreaRect.Height - 120 * info.AssetScale;
+
+ ClickExtension.Move(x, y).LeftButtonClick();
+ Sleep(800); // 等待动画彻底弹出
+
+ ClickExtension.Move(x, y).LeftButtonClick();
+ }
+
+
+ ///
+ /// 使用技能
+ ///
+ /// 技能编号,从右往左数,从1开始
+ /// 元素骰子是否充足
+ public bool ActionPhaseUseSkill(int skillIndex)
+ {
+ // 技能坐标写死 (w - 100 * n, h - 120)
+ var info = TaskContext.Instance().SystemInfo;
+ var x = info.CaptureAreaRect.X + info.CaptureAreaRect.Width - 100 * info.AssetScale * skillIndex;
+ var y = info.CaptureAreaRect.Y + info.CaptureAreaRect.Height - 120 * info.AssetScale;
+ ClickExtension.Move(x, y).LeftButtonClick();
+ Sleep(1000); // 等待动画彻底弹出
+
+ CaptureRectArea().Find(_assets.ElementalDiceLackWarningRo, foundRectArea =>
+ {
+ // 多点几次保证点击到
+ _logger.LogInformation("使用技能{SkillIndex}", skillIndex);
+ foundRectArea.ClickCenter();
+ Sleep(200);
+ foundRectArea.ClickCenter();
+ });
+
+ return true;
+ }
+
+ ///
+ /// 使用技能(元素骰子不够的情况下,自动烧牌)
+ ///
+ /// 技能编号,从右往左数,从1开始
+ /// 技能消耗骰子数
+ /// 消耗骰子元素类型
+ /// 对局对象
+ /// 手牌或者元素骰子是否充足
+ public bool ActionPhaseAutoUseSkill(int skillIndex, int diceCost, ElementalType elementalType, Duel duel)
+ {
+ var dice9RetryCount = 0;
+ var retryCount = 0;
+ var diceStatus = ActionPhaseDice();
+ while (true)
+ {
+ int dCount = diceStatus.Sum(x => x.Value);
+ if (dCount != duel.CurrentDiceCount)
+ {
+ if (retryCount > 20)
+ {
+ throw new System.Exception("骰子数量与预期不符,重试次数过多,可能出现了未知错误!");
+ }
+
+ if (dCount == 9 && duel.CurrentDiceCount == 8 && diceStatus[ElementalType.Omni.ToLowerString()] > 0)
+ {
+ dice9RetryCount++;
+ if (dice9RetryCount > 5)
+ {
+ // 支援区存在 鲸井小弟 情况下骰子数量增加导致识别出错的问题 #1
+ // 5次重试后仍然是9个骰子并且至少有一个万能骰子,出现多识别的情况是很稀少的,此时可以基本认为 支援区存在 鲸井小弟
+ // TODO : 但是这个方法并不是100%准确,后续需要添加支援区判断
+ _logger.LogInformation("期望的骰子数量8,应为开局期望,重试多次后累计实际识别9个骰子的情况为5次");
+ duel.CurrentDiceCount = 9; // 修正当前骰子数量
+ break;
+ }
+ }
+
+
+ _logger.LogInformation("当前骰子数量{Count}与期望的骰子数量{Expect}不相等,重试", dCount, duel.CurrentDiceCount);
+ diceStatus = ActionPhaseDice();
+ retryCount++;
+ Sleep(1000);
+ }
+ else
+ {
+ break;
+ }
+ }
+
+
+ int needSpecifyElementDiceCount = diceCost - diceStatus[ElementalType.Omni.ToLowerString()] - diceStatus[elementalType.ToLowerString()];
+ if (needSpecifyElementDiceCount > 0)
+ {
+ if (duel.CurrentCardCount < needSpecifyElementDiceCount)
+ {
+ _logger.LogInformation("当前手牌数{Current}小于需要烧牌数量{Expect},无法释放技能", duel.CurrentCardCount, needSpecifyElementDiceCount);
+ return false;
+ }
+
+ _logger.LogInformation("当前需要的元素骰子数量不足{Cost}个,还缺{Lack}个,当前手牌数{Current},烧牌", diceCost, needSpecifyElementDiceCount, duel.CurrentCardCount);
+
+ for (var i = 0; i < needSpecifyElementDiceCount; i++)
+ {
+ duel.CurrentCardCount--;
+ _logger.LogInformation("- {Count} 烧牌", i + 1);
+ ActionPhaseElementalTuning();
+ Sleep(100);
+ ActionPhaseElementalTuningConfirm();
+ Sleep(1000); // 烧牌动画
+ ClickGameWindowCenter(); // 复位
+ Sleep(500);
+ // 最后一张牌的回正速度较慢,多等一会
+ if (duel.CurrentCardCount <= 1)
+ {
+ ClickGameWindowCenter(); // 复位
+ Sleep(1000);
+ }
+ }
+ }
+
+ return ActionPhaseUseSkill(skillIndex);
+ }
+
+
+ ///
+ /// 回合结束
+ ///
+ public void RoundEnd()
+ {
+ CaptureRectArea().Find(_assets.RoundEndButtonRo, foundRectArea =>
+ {
+ foundRectArea.ClickCenter();
+ Sleep(1000); // 有弹出动画
+ foundRectArea.ClickCenter();
+ Sleep(300);
+ });
+
+ ClickGameWindowCenter(); // 复位
+ }
+
+ ///
+ /// 是否是再角色出战选择界面
+ /// 可重试方法
+ ///
+ public void IsInCharacterPickRetryThrowable()
+ {
+ if (!IsInCharacterPick())
+ {
+ throw new RetryException("当前不在角色出战选择界面");
+ }
+ }
+
+ ///
+ /// 是否是再角色出战选择界面
+ ///
+ ///
+ public bool IsInCharacterPick()
+ {
+ return !CaptureRectArea().Find(_assets.InCharacterPickRo).IsEmpty();
+ }
+
+ ///
+ /// 是否是我的回合
+ ///
+ ///
+ public bool IsInMyAction()
+ {
+ return !CaptureRectArea().Find(_assets.RoundEndButtonRo).IsEmpty();
+ }
+
+ ///
+ /// 是否是对方的回合
+ ///
+ ///
+ public bool IsInOpponentAction()
+ {
+ return !CaptureRectArea().Find(_assets.InOpponentActionRo).IsEmpty();
+ }
+
+ ///
+ /// 是否是回合结算阶段
+ ///
+ ///
+ public bool IsEndPhase()
+ {
+ return !CaptureRectArea().Find(_assets.EndPhaseRo).IsEmpty();
+ }
+
+
+ ///
+ /// 出战角色是否被打倒
+ ///
+ ///
+ public bool IsActiveCharacterTakenOut()
+ {
+ return !CaptureRectArea().Find(_assets.CharacterTakenOutRo).IsEmpty();
+ }
+
+ ///
+ /// 哪些出战角色被打倒了
+ ///
+ /// true 是已经被打倒
+ public bool[] WhatCharacterDefeated(List rects)
+ {
+ if (rects == null || rects.Count != 3)
+ {
+ throw new System.Exception("未能获取到我方角色卡位置");
+ }
+
+ var pList = MatchTemplateHelper.MatchTemplateMulti(Capture(), _assets.CharacterDefeatedMat, 0.8);
+
+ var res = new bool[3];
+ foreach (var p in pList)
+ {
+ for (var i = 0; i < rects.Count; i++)
+ {
+ if (IsOverlap(rects[i], new Rect(p.X, p.Y, _assets.CharacterDefeatedMat.Width, _assets.CharacterDefeatedMat.Height)))
+ {
+ res[i] = true;
+ }
+ }
+ }
+
+
+ return res;
+ }
+
+ ///
+ /// 判断矩形是否重叠
+ ///
+ ///
+ ///
+ ///
+ public bool IsOverlap(Rect rc1, Rect rc2)
+ {
+ if (rc1.X + rc1.Width > rc2.X &&
+ rc2.X + rc2.Width > rc1.X &&
+ rc1.Y + rc1.Height > rc2.Y &&
+ rc2.Y + rc2.Height > rc1.Y
+ )
+ {
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// 是否对局完全结束
+ ///
+ ///
+ public bool IsDuelEnd()
+ {
+ return !CaptureRectArea().Find(_assets.ExitDuelButtonRo).IsEmpty();
+ }
+
+
+ public Mat CutRight(Mat srcMat, int saveRightWidth)
+ {
+ srcMat = new Mat(srcMat, new Rect(srcMat.Width - saveRightWidth, 0, saveRightWidth, srcMat.Height));
+ return srcMat;
+ }
+
+ ///
+ /// 等待我的回合
+ /// 我方角色可能在此期间阵亡
+ ///
+ public void WaitForMyTurn(Duel duel, int waitTime = 0)
+ {
+ if (waitTime > 0)
+ {
+ _logger.LogInformation("等待对方行动{Time}s", waitTime / 1000);
+ Sleep(waitTime);
+ }
+
+ // 判断对方行动是否已经结束
+ var retryCount = 0;
+ var inMyActionCount = 0;
+ while (true)
+ {
+ if (IsInMyAction())
+ {
+ if (IsActiveCharacterTakenOut())
+ {
+ DoWhenCharacterDefeated(duel);
+ }
+ else
+ {
+ // 多延迟2s // 保证被击败提示已经完成显示
+ inMyActionCount++;
+ if (inMyActionCount == 3)
+ {
+ break;
+ }
+ }
+ }
+ else if (IsDuelEnd())
+ {
+ throw new DuelEndException("对战已结束,停止自动打牌!");
+ }
+
+ retryCount++;
+ if (retryCount >= 60)
+ {
+ throw new System.Exception("等待对方行动超时,停止自动打牌!");
+ }
+
+ _logger.LogInformation("对方仍在行动中,继续等待(次数{})...", retryCount);
+ Sleep(1000);
+ }
+ }
+
+ ///
+ /// 等待对方回合 和 回合结束阶段
+ /// 我方角色可能在此期间阵亡
+ ///
+ public void WaitOpponentAction(Duel duel)
+ {
+ var rd = new Random();
+ Sleep(3000 + rd.Next(1, 1000));
+ // 判断对方行动是否已经结束
+ var retryCount = 0;
+ while (true)
+ {
+ if (IsInOpponentAction())
+ {
+ _logger.LogInformation("对方仍在行动中,继续等待(次数{})...", retryCount);
+ }
+ else if (IsEndPhase())
+ {
+ _logger.LogInformation("正在回合结束阶段,继续等待(次数{})...", retryCount);
+ }
+ else if (IsInMyAction())
+ {
+ if (IsActiveCharacterTakenOut())
+ {
+ DoWhenCharacterDefeated(duel);
+ }
+ }
+ else if (IsDuelEnd())
+ {
+ throw new DuelEndException("对战已结束,停止自动打牌!");
+ }
+ else
+ {
+ // 至少走三次判断才能确定对方行动结束
+ if (retryCount > 2)
+ {
+ break;
+ }
+ else
+ {
+ _logger.LogError("等待对方回合 和 回合结束阶段 时程序未识别到有效内容(次数{})...", retryCount);
+ }
+ }
+
+ retryCount++;
+ if (retryCount >= 30)
+ {
+ throw new System.Exception("等待对方行动超时,停止自动打牌!");
+ }
+
+
+ Sleep(1000 + rd.Next(1, 500));
+ }
+ }
+
+ ///
+ /// 角色被打败后要切换角色
+ ///
+ ///
+ ///
+ public void DoWhenCharacterDefeated(Duel duel)
+ {
+ _logger.LogInformation("当前出战角色被打败,需要选择新的出战角色");
+ var defeatedArray = WhatCharacterDefeated(duel.CharacterCardRects);
+
+ for (var i = defeatedArray.Length - 1; i >= 0; i--)
+ {
+ duel.Characters[i + 1].IsDefeated = defeatedArray[i];
+ }
+
+ var orderList = duel.GetCharacterSwitchOrder();
+ if (orderList.Count == 0)
+ {
+ throw new DuelEndException("后续行动策略中,已经没有可切换且存活的角色了,结束自动打牌(建议添加更多行动)");
+ }
+
+ foreach (var j in orderList)
+ {
+ if (!duel.Characters[j].IsDefeated)
+ {
+ duel.Characters[j].SwitchWhenTakenOut();
+ break;
+ }
+ }
+
+ ClickGameWindowCenter();
+ Sleep(2000); // 切人动画
+ }
+
+
+ /////
+ ///// 哪个角色处于出战状态
+ /////
+ /////
+ //public Character WhichCharacterActive(Duel duel)
+ //{
+ // if (duel.CharacterCardRects == null || duel.CharacterCardRects.Count != 3)
+ // {
+ // throw new System.Exception("未能获取到我方角色卡位置");
+ // }
+
+ // Mat srcMat = Capture();
+
+ // // 切割下半部分
+ // int halfHeight = srcMat.Height / 2;
+ // Mat bottomMat = new Mat(srcMat, new Rect(0, halfHeight, srcMat.Width, srcMat.Height - halfHeight));
+ // Mat resMat;
+ // List pList = ImageRecognition.FindMultiTarget(bottomMat,
+ // ImageResCollections.CharacterHpUpperBitmap.ToMat(), "HpUpper", out resMat, 0.7);
+
+ // if (pList.Count != duel.GetCharacterAliveNum())
+ // {
+ // if (OutputImageWhenError)
+ // {
+ // var outMat = srcMat.Clone();
+ // foreach (var point in pList)
+ // {
+ // Cv2.Rectangle(outMat,
+ // new Rect(point.X, point.Y + halfHeight, ImageResCollections.CharacterHpUpperBitmap.Width,
+ // ImageResCollections.CharacterHpUpperBitmap.Height), Scalar.Red, 2);
+ // }
+
+ // Cv2.ImWrite("logs\\active_character_error.jpg", outMat);
+ // }
+
+ // throw new RetryException($"角色Hp区块识别有误,识别到区块数量{pList.Count} != 当前存活角色数{duel.GetCharacterAliveNum()}");
+ // }
+
+ // int cnt = 0;
+ // for (var i = 1; i < duel.Characters.Length; i++)
+ // {
+ // var cardRect = duel.Characters[i].Area;
+ // // 2倍高度 保证能够矩形相交
+ // var rect1 = new Rectangle(cardRect.X, cardRect.Y - cardRect.Height, cardRect.Width,
+ // cardRect.Height + cardRect.Height);
+
+ // foreach (var point in pList)
+ // {
+ // var rect2 = new Rectangle(point.X, halfHeight + point.Y,
+ // ImageResCollections.CharacterHpUpperBitmap.Width,
+ // ImageResCollections.CharacterHpUpperBitmap.Height);
+ // if (IsOverlap(rect1, rect2))
+ // {
+ // duel.Characters[i].HpUpperArea = rect2;
+ // // 出战角色判断
+ // if (halfHeight + point.Y < cardRect.Y)
+ // {
+ // cnt++;
+ // duel.CurrentCharacter = duel.Characters[i];
+ // }
+
+ // break;
+ // }
+ // }
+ // }
+
+ // if (cnt != 1)
+ // {
+ // if (OutputImageWhenError)
+ // {
+ // var outMat = srcMat.Clone();
+ // foreach (var point in pList)
+ // {
+ // Cv2.Rectangle(outMat,
+ // new Rect(point.X, point.Y + halfHeight, ImageResCollections.CharacterHpUpperBitmap.Width,
+ // ImageResCollections.CharacterHpUpperBitmap.Height), Scalar.Red, 2);
+ // }
+
+ // foreach (var rc in duel.CharacterCardRects)
+ // {
+ // Cv2.Rectangle(outMat,
+ // rc.ToCvRect(), Scalar.Green, 2);
+ // }
+
+ // Cv2.ImWrite("logs\\active_character_error.jpg", outMat);
+ // }
+
+ // throw new RetryException($"识别到{cnt}个出战角色");
+ // }
+
+ // AppendCharacterStatus(duel.CurrentCharacter, srcMat);
+ // return duel.CurrentCharacter;
+ //}
+
+ public void AppendCharacterStatus(Character character, Mat srcMat)
+ {
+ // 截取出战角色区域扩展
+ var characterMat = new Mat(srcMat, new Rect(character.Area.X,
+ character.Area.Y,
+ character.Area.Width + 40,
+ character.Area.Height + 10));
+ // 识别角色异常状态
+ var pCharacterStatusFreeze = MatchTemplateHelper.MatchTemplate(characterMat, _assets.CharacterStatusFreezeMat, TemplateMatchModes.CCoeffNormed);
+ if (pCharacterStatusFreeze != new Point())
+ {
+ character.StatusList.Add(CharacterStatusEnum.Frozen);
+ }
+ var pCharacterStatusDizziness = MatchTemplateHelper.MatchTemplate(characterMat, _assets.CharacterStatusDizzinessMat, TemplateMatchModes.CCoeffNormed);
+ if (pCharacterStatusDizziness != new Point())
+ {
+ character.StatusList.Add(CharacterStatusEnum.Frozen);
+ }
+
+ // 识别角色能量
+ var energyPointList = MatchTemplateHelper.MatchTemplateMulti(characterMat.Clone(),_assets.CharacterEnergyOnMat, 0.8);
+ character.EnergyByRecognition = energyPointList.Count;
+
+ _logger.LogInformation("当前出战{Character}", character);
+ }
+
+ public Character WhichCharacterActiveWithRetry(Duel duel)
+ {
+ // 检查角色是否被击败 // 这里又检查一次是因为最后一个角色存活的情况下,会自动出战
+ var defeatedArray = WhatCharacterDefeated(duel.CharacterCardRects);
+ for (var i = defeatedArray.Length - 1; i >= 0; i--)
+ {
+ duel.Characters[i + 1].IsDefeated = defeatedArray[i];
+ }
+
+ return Retry.Do(() => WhichCharacterActiveByHpWord(duel), TimeSpan.FromSeconds(0.3), 2);
+ }
+
+ public Character WhichCharacterActiveByHpWord(Duel duel)
+ {
+ if (duel.CharacterCardRects == null || duel.CharacterCardRects.Count != 3)
+ {
+ throw new System.Exception("未能获取到我方角色卡位置");
+ }
+
+ var srcMat = Capture();
+
+ int halfHeight = srcMat.Height / 2;
+ Mat bottomMat = new Mat(srcMat, new Rect(0, halfHeight, srcMat.Width, srcMat.Height - halfHeight));
+
+ var lowPurple = new Scalar(239, 239, 239);
+ var highPurple = new Scalar(242, 242, 250);
+ Mat gray = OpenCvCommonHelper.Threshold(bottomMat, lowPurple, highPurple);
+
+ var kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new OpenCvSharp.Size(15, 10), new OpenCvSharp.Point(-1, -1));
+ Cv2.Dilate(gray, gray, kernel); //膨胀
+
+
+ OpenCvSharp.Point[][] contours;
+ HierarchyIndex[] hierarchy;
+ Cv2.FindContours(gray, out contours, out hierarchy, RetrievalModes.External,
+ ContourApproximationModes.ApproxSimple, null);
+
+ if (contours.Length > 0)
+ {
+ // .Where(w => w.Width > 1 && w.Height >= 5)
+ var rects = contours.Select(Cv2.BoundingRect).ToList();
+
+
+ // 按照Y轴高度排序
+ rects = rects.OrderBy(r => r.Y).ToList();
+
+ // 第一个和角色卡重叠的矩形
+ foreach (var rect in rects)
+ {
+ for (var i = 0; i < duel.CharacterCardRects.Count; i++)
+ {
+ // 延长高度,确保能够相交
+ var rect1 = new Rect(rect.X, halfHeight + rect.Y, rect.Width + 20,
+ rect.Height + 20);
+ if (IsOverlap(rect1, duel.CharacterCardRects[i]) &&
+ halfHeight + rect.Y < duel.CharacterCardRects[i].Y)
+ {
+ // 首个相交矩形就是出战角色
+ duel.CurrentCharacter = duel.Characters[i + 1];
+ AppendCharacterStatus(duel.CurrentCharacter, srcMat);
+
+ Cv2.Rectangle(srcMat, rect1, Scalar.Yellow);
+ Cv2.Rectangle(srcMat, duel.CharacterCardRects[i], Scalar.Blue, 2);
+ OutputImage(duel, rects, bottomMat, halfHeight, "logs\\active_character2_success.jpg");
+ return duel.CurrentCharacter;
+ }
+ }
+ }
+
+ OutputImage(duel, rects, bottomMat, halfHeight, "logs\\active_character2_no_overlap_error.jpg");
+ }
+ else
+ {
+ if (OutputImageWhenError)
+ {
+ Cv2.ImWrite("logs\\active_character2_no_rects_error.jpg", gray);
+ }
+ }
+
+ throw new RetryException($"未识别到个出战角色");
+ }
+
+ private static void OutputImage(Duel duel, List rects, Mat bottomMat, int halfHeight, string fileName)
+ {
+ if (OutputImageWhenError)
+ {
+ foreach (var rect2 in rects)
+ {
+ Cv2.Rectangle(bottomMat, new OpenCvSharp.Point(rect2.X, rect2.Y),
+ new OpenCvSharp.Point(rect2.X + rect2.Width, rect2.Y + rect2.Height), Scalar.Red, 1);
+ }
+
+ foreach (var rc in duel.CharacterCardRects)
+ {
+ Cv2.Rectangle(bottomMat,
+ new Rect(rc.X, rc.Y - halfHeight, rc.Width, rc.Height), Scalar.Green, 1);
+ }
+
+
+ Cv2.ImWrite(fileName, bottomMat);
+ }
+ }
+}
\ No newline at end of file
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/ActionCommand.cs b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/ActionCommand.cs
new file mode 100644
index 00000000..b5b59016
--- /dev/null
+++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/ActionCommand.cs
@@ -0,0 +1,82 @@
+using System;
+
+namespace BetterGenshinImpact.GameTask.AutoGeniusInvokation.Model
+{
+ public class ActionCommand
+ {
+ ///
+ /// 角色
+ ///
+ public Character Character { get; set; }
+
+ public ActionEnum Action { get; set; }
+
+ ///
+ /// 目标编号(技能编号,从右往左)
+ ///
+ public int TargetIndex { get; set; }
+
+ public override string ToString()
+ {
+ if (Action == ActionEnum.UseSkill)
+ {
+ return $"【{Character.Name}】使用【技能{TargetIndex}】";
+ }
+ else if (Action == ActionEnum.SwitchLater)
+ {
+ return $"【{Character.Name}】切换至【角色{TargetIndex}】";
+ }
+ else
+ {
+ return base.ToString();
+ }
+ }
+
+
+ public int GetSpecificElementDiceUseCount()
+ {
+ if (Action == ActionEnum.UseSkill)
+ {
+ return Character.Skills[TargetIndex].SpecificElementCost;
+ }
+ else
+ {
+ throw new ArgumentException("未知行动");
+ }
+ }
+
+ public int GetAllDiceUseCount()
+ {
+ if (Action == ActionEnum.UseSkill)
+ {
+ return Character.Skills[TargetIndex].AllCost;
+ }
+ else
+ {
+ throw new ArgumentException("未知行动");
+ }
+ }
+
+ public ElementalType GetDiceUseElementType()
+ {
+ if (Action == ActionEnum.UseSkill)
+ {
+ return Character.Element;
+ }
+ else
+ {
+ throw new ArgumentException("未知行动");
+ }
+ }
+
+ public bool SwitchLater()
+ {
+ return Character.SwitchLater();
+ }
+
+ public bool UseSkill(Duel duel)
+ {
+ return Character.UseSkill(TargetIndex, duel);
+ }
+ }
+}
\ No newline at end of file
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/ActionEnum.cs b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/ActionEnum.cs
new file mode 100644
index 00000000..565e8a8c
--- /dev/null
+++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/ActionEnum.cs
@@ -0,0 +1,45 @@
+using System;
+
+namespace BetterGenshinImpact.GameTask.AutoGeniusInvokation.Model
+{
+ public enum ActionEnum
+ {
+ ChooseFirst, SwitchLater, UseSkill
+ }
+
+ public static class ActionEnumExtension
+ {
+ public static ActionEnum ChineseToActionEnum(this string type)
+ {
+ type = type.ToLower();
+ switch (type)
+ {
+ case "出战":
+ //return ActionEnum.ChooseFirst;
+ throw new ArgumentOutOfRangeException(nameof(type), type, null);
+ case "切换":
+ //return ActionEnum.SwitchLater;
+ throw new ArgumentOutOfRangeException(nameof(type), type, null);
+ case "使用":
+ return ActionEnum.UseSkill;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(type), type, null);
+ }
+ }
+
+ public static string ToChinese(this ActionEnum type)
+ {
+ switch (type)
+ {
+ case ActionEnum.ChooseFirst:
+ return "出战";
+ case ActionEnum.SwitchLater:
+ return "切换";
+ case ActionEnum.UseSkill:
+ return "使用";
+ default:
+ throw new ArgumentOutOfRangeException(nameof(type), type, null);
+ }
+ }
+ }
+}
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/Character.cs b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/Character.cs
new file mode 100644
index 00000000..6e990099
--- /dev/null
+++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/Character.cs
@@ -0,0 +1,115 @@
+using BetterGenshinImpact.Core.Recognition.OpenCv;
+using BetterGenshinImpact.Helpers.Extensions;
+using OpenCvSharp;
+using System.Collections.Generic;
+using System.Text;
+using Microsoft.Extensions.Logging;
+
+namespace BetterGenshinImpact.GameTask.AutoGeniusInvokation.Model
+{
+ public class Character
+ {
+ private readonly ILogger _logger = App.GetLogger();
+
+ ///
+ /// 1-3 所在数组下标一致
+ ///
+ public int Index { get; set; }
+
+ public string Name { get; set; }
+ public ElementalType Element { get; set; }
+ public Skill[] Skills { get; set; }
+
+
+ ///
+ /// 是否被打败
+ ///
+ public bool IsDefeated { get; set; }
+
+ ///
+ /// 充能点
+ ///
+ public int Energy { get; set; }
+
+
+ ///
+ /// 充能点来自于图像识别
+ ///
+ public int EnergyByRecognition { get; set; }
+
+ ///
+ /// 角色身上的负面状态
+ ///
+ public List StatusList { get; set; } = new List();
+
+ ///
+ /// 角色区域
+ ///
+ public Rect Area { get; set; }
+
+ ///
+ /// 血量上方区域,用于判断是否出战
+ ///
+ public Rect HpUpperArea { get; set; }
+
+ public override string ToString()
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.Append($"角色{Index},");
+ sb.Append($"充能={EnergyByRecognition},");
+ if (StatusList?.Count > 0)
+ {
+ sb.Append($"状态:{string.Join(",", StatusList)}");
+ }
+
+ return sb.ToString();
+ }
+
+ public void ChooseFirst()
+ {
+ ClickExtension.Move(GeniusInvokationControl.GetInstance().MakeOffset(Area.GetCenterPoint())).LeftButtonDoubleClick();
+ }
+
+ public bool SwitchLater()
+ {
+ var p = GeniusInvokationControl.GetInstance().MakeOffset(Area.GetCenterPoint());
+ // 选择角色
+ ClickExtension.Move(p).LeftButtonClick();
+
+ // 点击切人按钮
+ GeniusInvokationControl.GetInstance().ActionPhasePressSwitchButton();
+ return true;
+ }
+
+ ///
+ /// 角色被打败的时候双击角色牌重新出战
+ ///
+ ///
+ public void SwitchWhenTakenOut()
+ {
+ _logger.LogInformation("有角色被打败,当前选择{Name}出战", Name);
+ var p = GeniusInvokationControl.GetInstance().MakeOffset(Area.GetCenterPoint());
+ // 选择角色
+ ClickExtension.Move(p).LeftButtonClick();
+ // 双击切人
+ GeniusInvokationControl.GetInstance().Sleep(500);
+ ClickExtension.Move(p).LeftButtonClick();
+ GeniusInvokationControl.GetInstance().Sleep(300);
+ }
+
+ public bool UseSkill(int skillIndex, Duel duel)
+ {
+ bool res = GeniusInvokationControl.GetInstance()
+ .ActionPhaseAutoUseSkill(skillIndex, Skills[skillIndex].SpecificElementCost, Skills[skillIndex].Type, duel);
+ if (res)
+ {
+ return true;
+ }
+ else
+ {
+ _logger.LogWarning("没有足够的手牌或元素骰子释放技能");
+ return false;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/CharacterStatusEnum.cs b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/CharacterStatusEnum.cs
new file mode 100644
index 00000000..f193d3f4
--- /dev/null
+++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/CharacterStatusEnum.cs
@@ -0,0 +1,8 @@
+namespace BetterGenshinImpact.GameTask.AutoGeniusInvokation.Model
+{
+ public enum CharacterStatusEnum
+ {
+ Frozen,
+ Dizziness
+ }
+}
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/Duel.cs b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/Duel.cs
new file mode 100644
index 00000000..0edbd106
--- /dev/null
+++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/Duel.cs
@@ -0,0 +1,382 @@
+using BetterGenshinImpact.GameTask.AutoGeniusInvokation.Exception;
+using GeniusInvokationAutoToy.Utils;
+using Microsoft.Extensions.Logging;
+using OpenCvSharp;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace BetterGenshinImpact.GameTask.AutoGeniusInvokation.Model;
+
+public class Duel
+{
+ private readonly ILogger _logger = App.GetLogger();
+
+ public Character CurrentCharacter { get; set; }
+ public Character[] Characters { get; set; } = new Character[4];
+
+ ///
+ /// 行动指令队列
+ ///
+ public List ActionCommandQueue { get; set; } = new List();
+
+ ///
+ /// 当前回合数
+ ///
+ public int RoundNum { get; set; } = 1;
+
+ ///
+ /// 角色牌位置
+ ///
+ public List CharacterCardRects { get; set; }
+
+ ///
+ /// 手牌数量
+ ///
+ public int CurrentCardCount { get; set; } = 0;
+
+ ///
+ /// 骰子数量
+ ///
+ public int CurrentDiceCount { get; set; } = 0;
+
+
+ public CancellationTokenSource Cts { get; set; }
+
+
+ public async Task CustomStrategyRunAsync(CancellationTokenSource cts1)
+ {
+ await Task.Run(() => { CustomStrategyRun(cts1); });
+ }
+
+ public void CustomStrategyRun(CancellationTokenSource cts1)
+ {
+ Cts = cts1;
+ try
+ {
+ _logger.LogInformation("========================================");
+ _logger.LogInformation("→ {Text}", "全自动七圣召唤,启动!");
+
+ GeniusInvokationControl.GetInstance().Init(Cts);
+ SystemControl.ActivateWindow();
+
+ // 对局准备 选择初始手牌
+ GeniusInvokationControl.GetInstance().CommonDuelPrepare();
+
+
+ // 获取角色区域
+ CharacterCardRects = Retry.Do(() => GeniusInvokationControl.GetInstance().GetCharacterRects(),
+ TimeSpan.FromSeconds(1.5), 20);
+ if (CharacterCardRects == null || CharacterCardRects.Count != 3)
+ {
+ throw new DuelEndException("未成功获取到角色区域");
+ }
+
+ for (var i = 1; i < 4; i++)
+ {
+ Characters[i].Area = CharacterCardRects[i - 1];
+ }
+
+ // 出战角色
+ CurrentCharacter = ActionCommandQueue[0].Character;
+ CurrentCharacter.ChooseFirst();
+
+ // 开始执行回合
+ while (true)
+ {
+ _logger.LogInformation("--------------第{RoundNum}回合--------------", RoundNum);
+ ClearCharacterStatus(); // 清空单回合的异常状态
+ if (RoundNum == 1)
+ {
+ CurrentCardCount = 5;
+ }
+ else
+ {
+ CurrentCardCount += 2;
+ }
+
+ CurrentDiceCount = 8;
+
+ // 预计算本回合内的所有可能的元素
+ var elementSet = PredictionDiceType();
+
+ // 0 投骰子
+ GeniusInvokationControl.GetInstance().ReRollDice(elementSet.ToArray());
+
+ // 等待到我的回合 // 投骰子动画时间是不确定的 // 可能是对方先手
+ GeniusInvokationControl.GetInstance().WaitForMyTurn(this, 1000);
+
+ // 开始执行行动
+ while (true)
+ {
+ // 没骰子了就结束行动
+ _logger.LogInformation("行动开始,当前骰子数[{CurrentDiceCount}],当前手牌数[{CurrentCardCount}]", CurrentDiceCount, CurrentCardCount);
+ if (CurrentDiceCount <= 0)
+ {
+ _logger.LogInformation("骰子已经用完");
+ GeniusInvokationControl.GetInstance().Sleep(2000);
+ break;
+ }
+
+ // 每次行动前都要检查当前角色
+ CurrentCharacter = GeniusInvokationControl.GetInstance().WhichCharacterActiveWithRetry(this);
+
+ var alreadyExecutedActionIndex = new List();
+ var alreadyExecutedActionCommand = new List();
+ var i = 0;
+ for (i = 0; i < ActionCommandQueue.Count; i++)
+ {
+ var actionCommand = ActionCommandQueue[i];
+ // 指令中的角色未被打败、角色有异常状态 跳过指令
+ if (actionCommand.Character.IsDefeated || actionCommand.Character.StatusList?.Count > 0)
+ {
+ continue;
+ }
+
+ // 当前出战角色身上存在异常状态的情况下不执行本角色的指令
+ if (CurrentCharacter.StatusList?.Count > 0 &&
+ actionCommand.Character.Index == CurrentCharacter.Index)
+ {
+ continue;
+ }
+
+
+ // 1. 判断切人
+ if (CurrentCharacter.Index != actionCommand.Character.Index)
+ {
+ if (CurrentDiceCount >= 1)
+ {
+ actionCommand.SwitchLater();
+ CurrentDiceCount--;
+ alreadyExecutedActionIndex.Add(-actionCommand.Character.Index); // 标记为已执行
+ var switchAction = new ActionCommand
+ {
+ Character = CurrentCharacter,
+ Action = ActionEnum.SwitchLater,
+ TargetIndex = actionCommand.Character.Index
+ };
+ alreadyExecutedActionCommand.Add(switchAction);
+ _logger.LogInformation("→指令执行完成:{Action}", switchAction);
+ break;
+ }
+ else
+ {
+ _logger.LogInformation("骰子不足以进行下一步:切换角色 {CharacterIndex}", actionCommand.Character.Index);
+ break;
+ }
+ }
+
+ // 2. 判断使用技能
+ if (actionCommand.GetAllDiceUseCount() > CurrentDiceCount)
+ {
+ _logger.LogInformation("骰子不足以进行下一步:{Action}", actionCommand);
+ break;
+ }
+ else
+ {
+ bool useSkillRes = actionCommand.UseSkill(this);
+ if (useSkillRes)
+ {
+ CurrentDiceCount -= actionCommand.GetAllDiceUseCount();
+ alreadyExecutedActionIndex.Add(i);
+ alreadyExecutedActionCommand.Add(actionCommand);
+ _logger.LogInformation("→指令执行完成:{Action}", actionCommand);
+ }
+ else
+ {
+ _logger.LogWarning("→指令执行失败(可能是手牌不够):{Action}", actionCommand);
+ GeniusInvokationControl.GetInstance().Sleep(1000);
+ GeniusInvokationControl.GetInstance().ClickGameWindowCenter();
+ }
+
+ break;
+ }
+ }
+
+
+
+ if (alreadyExecutedActionIndex.Count != 0)
+ {
+ foreach (var index in alreadyExecutedActionIndex)
+ {
+ if (index >= 0)
+ {
+ ActionCommandQueue.RemoveAt(index);
+ }
+ }
+
+ alreadyExecutedActionIndex.Clear();
+ // 等待对方行动完成 (开大的时候等待时间久一点)
+ var sleepTime = ComputeWaitForMyTurnTime(alreadyExecutedActionCommand);
+ GeniusInvokationControl.GetInstance().WaitForMyTurn(this, sleepTime);
+ alreadyExecutedActionCommand.Clear();
+ }
+ else
+ {
+ // 如果没有任何指令可以执行 则跳出循环
+ // TODO 也有可能是角色死亡/所有角色被冻结导致没有指令可以执行
+ //if (i >= ActionCommandQueue.Count)
+ //{
+ // throw new DuelEndException("策略中所有指令已经执行完毕,结束自动打牌");
+ //}
+ GeniusInvokationControl.GetInstance().Sleep(2000);
+ break;
+ }
+
+ if (ActionCommandQueue.Count == 0)
+ {
+ throw new DuelEndException("策略中所有指令已经执行完毕,结束自动打牌");
+ }
+ }
+
+ // 回合结束
+ GeniusInvokationControl.GetInstance().Sleep(1000);
+ _logger.LogInformation("我方点击回合结束");
+ GeniusInvokationControl.GetInstance().RoundEnd();
+
+ // 等待对方行动+回合结算
+ GeniusInvokationControl.GetInstance().WaitOpponentAction(this);
+ RoundNum++;
+ }
+ }
+ catch (TaskCanceledException ex)
+ {
+ _logger.LogInformation(ex.Message);
+ }
+ catch (DuelEndException ex)
+ {
+ _logger.LogInformation(ex.Message);
+ _logger.LogInformation("← {Text}", "退出全自动七圣召唤");
+ }
+ catch (System.Exception ex)
+ {
+ _logger.LogError(ex.ToString());
+ }
+ finally
+ {
+ _logger.LogInformation("========================================");
+ }
+ }
+
+ private HashSet PredictionDiceType()
+ {
+ var actionUseDiceSum = 0;
+ var elementSet = new HashSet
+ {
+ ElementalType.Omni
+ };
+ for (var i = 0; i < ActionCommandQueue.Count; i++)
+ {
+ var actionCommand = ActionCommandQueue[i];
+
+ // 角色未被打败的情况下才能执行
+ if (actionCommand.Character.IsDefeated)
+ {
+ continue;
+ }
+
+ // 通过骰子数量判断是否可以执行
+
+ // 1. 判断切人
+ if (i > 0 && actionCommand.Character.Index != ActionCommandQueue[i - 1].Character.Index)
+ {
+ actionUseDiceSum++;
+ if (actionUseDiceSum > CurrentDiceCount)
+ {
+ break;
+ }
+ else
+ {
+ elementSet.Add(actionCommand.GetDiceUseElementType());
+ //executeActionIndex.Add(-actionCommand.Character.Index);
+ }
+ }
+
+ // 2. 判断使用技能
+ actionUseDiceSum += actionCommand.GetAllDiceUseCount();
+ if (actionUseDiceSum > CurrentDiceCount)
+ {
+ break;
+ }
+ else
+ {
+ elementSet.Add(actionCommand.GetDiceUseElementType());
+ //executeActionIndex.Add(i);
+ }
+ }
+
+ return elementSet;
+ }
+
+ public void ClearCharacterStatus()
+ {
+ foreach (var character in Characters)
+ {
+ character?.StatusList?.Clear();
+ }
+ }
+
+ ///
+ /// 根据前面执行的命令计算等待时间
+ /// 大招等待15秒
+ /// 快速切换等待3秒
+ ///
+ ///
+ ///
+ private int ComputeWaitForMyTurnTime(List alreadyExecutedActionCommand)
+ {
+ foreach (var command in alreadyExecutedActionCommand)
+ {
+ if (command.Action == ActionEnum.UseSkill && command.TargetIndex == 1)
+ {
+ return 15000;
+ }
+
+ // 莫娜切换等待3秒
+ if (command.Character.Name == "莫娜" && command.Action == ActionEnum.SwitchLater)
+ {
+ return 3000;
+ }
+ }
+
+ return 10000;
+ }
+
+ ///
+ /// 获取角色切换顺序
+ ///
+ ///
+ public List GetCharacterSwitchOrder()
+ {
+ List orderList = new List();
+ for (var i = 0; i < ActionCommandQueue.Count; i++)
+ {
+ if (!orderList.Contains(ActionCommandQueue[i].Character.Index))
+ {
+ orderList.Add(ActionCommandQueue[i].Character.Index);
+ }
+ }
+
+ return orderList;
+ }
+
+ ///
+ /// 获取角色存活数量
+ ///
+ ///
+ public int GetCharacterAliveNum()
+ {
+ int num = 0;
+ foreach (var character in Characters)
+ {
+ if (character != null && !character.IsDefeated)
+ {
+ num++;
+ }
+ }
+
+ return num;
+ }
+}
\ No newline at end of file
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/ElementalType.cs b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/ElementalType.cs
new file mode 100644
index 00000000..1e72d0ee
--- /dev/null
+++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/ElementalType.cs
@@ -0,0 +1,101 @@
+using System;
+
+namespace BetterGenshinImpact.GameTask.AutoGeniusInvokation.Model
+{
+ public enum ElementalType
+ {
+ Omni,
+ Cryo,
+ Hydro,
+ Pyro,
+ Electro,
+ Dendro,
+ Anemo,
+ Geo
+ }
+
+ public static class ElementalTypeExtension
+ {
+ public static ElementalType ToElementalType(this string type)
+ {
+ type = type.ToLower();
+ switch (type)
+ {
+ case "omni":
+ return ElementalType.Omni;
+ case "cryo":
+ return ElementalType.Cryo;
+ case "hydro":
+ return ElementalType.Hydro;
+ case "pyro":
+ return ElementalType.Pyro;
+ case "electro":
+ return ElementalType.Electro;
+ case "dendro":
+ return ElementalType.Dendro;
+ case "anemo":
+ return ElementalType.Anemo;
+ case "geo":
+ return ElementalType.Geo;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(type), type, null);
+ }
+ }
+
+ public static ElementalType ChineseToElementalType(this string type)
+ {
+ type = type.ToLower();
+ switch (type)
+ {
+ case "全":
+ return ElementalType.Omni;
+ case "冰":
+ return ElementalType.Cryo;
+ case "水":
+ return ElementalType.Hydro;
+ case "火":
+ return ElementalType.Pyro;
+ case "雷":
+ return ElementalType.Electro;
+ case "草":
+ return ElementalType.Dendro;
+ case "风":
+ return ElementalType.Anemo;
+ case "岩":
+ return ElementalType.Geo;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(type), type, null);
+ }
+ }
+
+ public static string ToChinese(this ElementalType type)
+ {
+ switch (type)
+ {
+ case ElementalType.Omni:
+ return "全";
+ case ElementalType.Cryo:
+ return "冰";
+ case ElementalType.Hydro:
+ return "水";
+ case ElementalType.Pyro:
+ return "火";
+ case ElementalType.Electro:
+ return "雷";
+ case ElementalType.Dendro:
+ return "草";
+ case ElementalType.Anemo:
+ return "风";
+ case ElementalType.Geo:
+ return "岩";
+ default:
+ throw new ArgumentOutOfRangeException(nameof(type), type, null);
+ }
+ }
+
+ public static string ToLowerString(this ElementalType type)
+ {
+ return type.ToString().ToLower();
+ }
+ }
+}
\ No newline at end of file
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/RollPhaseDice.cs b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/RollPhaseDice.cs
new file mode 100644
index 00000000..984f825d
--- /dev/null
+++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/RollPhaseDice.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Drawing;
+
+namespace BetterGenshinImpact.GameTask.AutoGeniusInvokation.Model
+{
+ ///
+ /// 投掷期间骰子
+ ///
+ [Obsolete]
+ public class RollPhaseDice
+ {
+ ///
+ /// 元素类型
+ ///
+ public ElementalType Type { get; set; }
+
+ ///
+ /// 中心点位置
+ ///
+ public Point CenterPosition { get; set; }
+
+ public RollPhaseDice(ElementalType type, Point centerPosition)
+ {
+ Type = type;
+ CenterPosition = centerPosition;
+ }
+
+ public RollPhaseDice()
+ {
+ }
+
+ public override string ToString()
+ {
+ return $"Type:{Type},CenterPosition:{CenterPosition}";
+ }
+
+ public void Click()
+ {
+ //MouseUtils.Click(CenterPosition.X, CenterPosition.Y);
+ }
+ }
+}
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/RoundStrategy.cs b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/RoundStrategy.cs
new file mode 100644
index 00000000..24c4ed44
--- /dev/null
+++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/RoundStrategy.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+
+namespace BetterGenshinImpact.GameTask.AutoGeniusInvokation.Model;
+
+[Obsolete]
+public class RoundStrategy
+{
+ public List RawCommandList { get; set; } = new List();
+
+ public List ActionCommands { get; set; } = new List();
+
+ public List MaybeNeedElement(Duel duel)
+ {
+ List result = new List();
+
+ for (int i = 0; i < ActionCommands.Count; i++)
+ {
+ if (ActionCommands[i].Action == ActionEnum.SwitchLater
+ && i != ActionCommands.Count-1
+ && ActionCommands[i+1].Action == ActionEnum.UseSkill)
+ {
+ result.Add(duel.Characters[ActionCommands[i].TargetIndex].Element);
+ }
+ }
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/Skill.cs b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/Skill.cs
new file mode 100644
index 00000000..811c83da
--- /dev/null
+++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/Skill.cs
@@ -0,0 +1,23 @@
+namespace BetterGenshinImpact.GameTask.AutoGeniusInvokation.Model;
+
+public class Skill
+{
+ ///
+ /// 1-4 和数组下标一致,游戏中是从右往左开始数的!
+ ///
+ public short Index { get; set; }
+ public ElementalType Type { get; set; }
+ ///
+ /// 消耗指定元素骰子数量
+ ///
+ public int SpecificElementCost { get; set; }
+
+ ///
+ /// 消耗杂色骰子数量
+ ///
+ public int AnyElementCost { get; set; } = 0;
+ ///
+ /// 消耗指定元素骰子数量 + 消耗杂色骰子数量 = 消耗总骰子数量
+ ///
+ public int AllCost { get; set; }
+}
\ No newline at end of file
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/ScriptParser.cs b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/ScriptParser.cs
new file mode 100644
index 00000000..93414741
--- /dev/null
+++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/ScriptParser.cs
@@ -0,0 +1,171 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Windows.Forms;
+using BetterGenshinImpact.GameTask.AutoGeniusInvokation.Model;
+using Microsoft.Extensions.Logging;
+
+namespace BetterGenshinImpact.GameTask.AutoGeniusInvokation;
+
+public class ScriptParser
+{
+ private static readonly ILogger MyLogger = App.GetLogger();
+ public static Duel Parse(string script)
+ {
+ var lines = script.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
+ var result = new List();
+ foreach (var line in lines)
+ {
+ string l = line.Trim();
+ result.Add(l);
+ }
+
+ return Parse(result);
+ }
+
+ public static Duel Parse(List lines)
+ {
+ Duel duel = new Duel();
+ string stage = "";
+
+ int i = 0;
+ try
+ {
+ for (i = 0; i < lines.Count; i++)
+ {
+ var line = lines[i];
+ if (line.Contains(":"))
+ {
+ stage = line;
+ continue;
+ }
+
+ if (line == "---" || line.StartsWith("//") || string.IsNullOrEmpty(line))
+ {
+ continue;
+ }
+
+ if (stage == "角色定义:")
+ {
+ var character = ParseCharacter(line);
+ duel.Characters[character.Index] = character;
+ }
+ else if (stage == "策略定义:")
+ {
+ MyAssert(duel.Characters[3] != null, "角色未定义");
+
+ string[] actionParts = line.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries);
+ MyAssert(actionParts.Length == 3, "策略中的行动命令解析错误");
+ MyAssert(actionParts[1] == "使用", "策略中的行动命令解析错误");
+
+ var actionCommand = new ActionCommand();
+ var action = actionParts[1].ChineseToActionEnum();
+ actionCommand.Action = action;
+
+ int j = 1;
+ for (j = 1; j <= 3; j++)
+ {
+ var character = duel.Characters[j];
+ if (character != null && character.Name == actionParts[0])
+ {
+ actionCommand.Character = character;
+ break;
+ }
+ }
+
+ MyAssert(j <= 3, "策略中的行动命令解析错误:角色名称无法从角色定义中匹配到");
+
+ int skillNum = int.Parse(Regex.Replace(actionParts[2], @"[^0-9]+", ""));
+ MyAssert(skillNum < 5, "策略中的行动命令解析错误:技能编号错误");
+ actionCommand.TargetIndex = skillNum;
+ duel.ActionCommandQueue.Add(actionCommand);
+ }
+ else
+ {
+ throw new System.Exception($"未知的定义字段:{stage}");
+ }
+ }
+
+ MyAssert(duel.Characters[3] != null, "角色未定义,请确认策略文本格式是否为UTF-8");
+ }
+ catch (System.Exception ex)
+ {
+ MyLogger.LogError($"解析脚本错误,行号:{i + 1},错误信息:{ex}");
+ MessageBox.Show($"解析脚本错误,行号:{i + 1},错误信息:{ex}", "策略解析失败", MessageBoxButtons.OK, MessageBoxIcon.Error);
+ return null;
+ }
+
+ return duel;
+ }
+
+ ///
+ /// 解析示例
+ /// 角色1=刻晴|雷{技能3消耗=1雷骰子+2任意,技能2消耗=3雷骰子,技能1消耗=4雷骰子}
+ /// 角色2=雷神|雷{技能3消耗=1雷骰子+2任意,技能2消耗=3雷骰子,技能1消耗=4雷骰子}
+ /// 角色3=甘雨|冰{技能4消耗=1冰骰子+2任意,技能3消耗=1冰骰子,技能2消耗=5冰骰子,技能1消耗=3冰骰子}
+ ///
+ ///
+ ///
+ public static Character ParseCharacter(string line)
+ {
+ var character = new Character();
+
+ var characterAndSkill = line.Split('{');
+
+ var parts = characterAndSkill[0].Split('=');
+ character.Index = int.Parse(Regex.Replace(parts[0], @"[^0-9]+", ""));
+ MyAssert(character.Index >= 1 && character.Index <= 3, "角色序号必须在1-3之间");
+ var nameAndElement = parts[1].Split('|');
+ character.Name = nameAndElement[0];
+ character.Element = nameAndElement[1].Substring(0, 1).ChineseToElementalType();
+
+ // 技能
+ string skillStr = characterAndSkill[1].Replace("}", "");
+ var skillParts = skillStr.Split(',');
+ var skills = new Skill[skillParts.Length + 1];
+ for (int i = 0; i < skillParts.Length; i++)
+ {
+ var skill = ParseSkill(skillParts[i]);
+ skills[skill.Index] = skill;
+ }
+
+ character.Skills = skills.ToArray();
+ return character;
+ }
+
+ ///
+ /// 技能3消耗=1雷骰子+2任意
+ /// 技能2消耗=3雷骰子
+ /// 技能1消耗=4雷骰子
+ ///
+ ///
+ ///
+ public static Skill ParseSkill(string oneSkillStr)
+ {
+ var skill = new Skill();
+ var parts = oneSkillStr.Split('=');
+ skill.Index = short.Parse(Regex.Replace(parts[0], @"[^0-9]+", ""));
+ MyAssert(skill.Index >= 1 && skill.Index <= 5, "技能序号必须在1-5之间");
+ var costStr = parts[1];
+ var costParts = costStr.Split('+');
+ skill.SpecificElementCost = int.Parse(costParts[0].Substring(0, 1));
+ skill.Type = costParts[0].Substring(1, 1).ChineseToElementalType();
+ // 杂色骰子在+号右边
+ if (costParts.Length > 1)
+ {
+ skill.AnyElementCost = int.Parse(costParts[1].Substring(0, 1));
+ }
+
+ skill.AllCost = skill.SpecificElementCost + skill.AnyElementCost;
+ return skill;
+ }
+
+ private static void MyAssert(bool b, string msg)
+ {
+ if (!b)
+ {
+ throw new System.Exception(msg);
+ }
+ }
+}
\ No newline at end of file
diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Utils/Retry.cs b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Utils/Retry.cs
new file mode 100644
index 00000000..e29084af
--- /dev/null
+++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Utils/Retry.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace GeniusInvokationAutoToy.Utils
+{
+ ///
+ /// https://stackoverflow.com/questions/1563191/cleanest-way-to-write-retry-logic
+ ///
+ public static class Retry
+ {
+ public static void Do(
+ Action action,
+ TimeSpan retryInterval,
+ int maxAttemptCount = 3)
+ {
+ Do