using BetterGenshinImpact.Core.Recognition.OCR; using BetterGenshinImpact.Core.Recognition.OpenCv; using BetterGenshinImpact.Core.Simulator; using BetterGenshinImpact.GameTask.AutoGeniusInvokation.Assets; using BetterGenshinImpact.GameTask.AutoGeniusInvokation.Exception; using BetterGenshinImpact.GameTask.AutoGeniusInvokation.Model; using BetterGenshinImpact.GameTask.Common; using BetterGenshinImpact.GameTask.Model.Area; using BetterGenshinImpact.Helpers; using BetterGenshinImpact.Helpers.Extensions; using Microsoft.Extensions.Logging; using OpenCvSharp; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using static BetterGenshinImpact.GameTask.Common.TaskControl; 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(); private AutoGeniusInvokationConfig _config; // 定义私有构造函数,使外界不能创建该类实例 private GeniusInvokationControl() { _config = TaskContext.Instance().Config.AutoGeniusInvokationConfig; } /// /// 定义公有方法提供一个全局访问点,同时你也可以定义公有属性来提供全局访问点 /// /// public static GeniusInvokationControl GetInstance() { if (_uniqueInstance == null) { lock (_locker) { _uniqueInstance ??= new GeniusInvokationControl(); } } return _uniqueInstance; } public static bool OutputImageWhenError = true; private CancellationToken _ct; private readonly AutoGeniusInvokationAssets _assets = AutoGeniusInvokationAssets.Instance; // private IGameCapture? _gameCapture; public void Init(CancellationToken ct) { _ct = ct; // _gameCapture = taskParam.Dispatcher.GameCapture; } public void Sleep(int millisecondsTimeout) { CheckTask(); Thread.Sleep(millisecondsTimeout); var sleepDelay = TaskContext.Instance().Config.AutoGeniusInvokationConfig.SleepDelay; if (sleepDelay > 0) { Thread.Sleep(sleepDelay); } } public Mat CaptureGameMat() { return CaptureToRectArea().SrcMat; } public Mat CaptureGameGreyMat() { return CaptureToRectArea().SrcGreyMat; } public ImageRegion CaptureGameRectArea() { return CaptureToRectArea(); } public void CheckTask() { NewRetry.Do(() => { if (_ct is { IsCancellationRequested: true }) { return; } TaskControl.TrySuspend(); if (!SystemControl.IsGenshinImpactActiveByProcess()) { _logger.LogWarning("当前获取焦点的窗口不是原神,暂停"); throw new RetryException("当前获取焦点的窗口不是原神"); } }, TimeSpan.FromSeconds(1), 100); if (_ct is { IsCancellationRequested: true }) { throw new TaskCanceledException("任务取消"); } } public void CommonDuelPrepare() { // 1. 选择初始手牌 Sleep(1000); _logger.LogInformation("开始选择初始手牌"); while (!ClickConfirm()) { // 循环等待选择卡牌画面 Sleep(1000); } _logger.LogInformation("点击确认"); // 2. 选择出战角色 // 此处选择第2个角色 雷神 _logger.LogInformation("等待3s对局准备..."); Sleep(3000); // 是否是再角色出战选择界面 NewRetry.Do(IsInCharacterPickRetryThrowable, TimeSpan.FromSeconds(0.8), 20); _logger.LogInformation("识别到已经在角色出战界面,等待1.5s"); Sleep(1500); } public void SortActionPhaseDiceMats(HashSet elementSet) { _assets.ActionPhaseDiceMats = _assets.ActionPhaseDiceMats.OrderByDescending(kvp => { for (var i = 0; i < elementSet.Count; i++) { if (kvp.Key == elementSet.ElementAt(i).ToLowerString()) { return i; } } return -1; }) .ToDictionary(x => x.Key, x => x.Value); // 打印排序后的顺序 var msg = _assets.ActionPhaseDiceMats.Aggregate("", (current, kvp) => current + $"{kvp.Key.ToElementalType().ToChinese()}| "); _logger.LogDebug("当前骰子排序:{Msg}", msg); } /// /// 获取我方三个角色卡牌区域 /// /// public List GetCharacterRects() { var srcMat = CaptureGameMat(); var halfHeight = srcMat.Height / 2; var 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); var gray = OpenCvCommonHelper.Threshold(bottomMat, lowPurple, highPurple); // 水平投影到y轴 正常只有一个连续区域 var h = ArithmeticHelper.HorizontalProjection(gray); // y轴 从上到下确认连续区域 int y1 = 0, y2 = 0; int start = 0; var inLine = false; for (int i = 0; i < h.Length; i++) { // 直方图 if (OutputImageWhenError) { Cv2.Line(bottomMat, 0, i, h[i], i, Scalar.Yellow); } if (h[i] > h.Average() * 10) { 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("log\\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("log\\character_card_error.jpg", bottomMat); // } // throw new RetryException("未获取到角色区域"); //} // 垂直投影 var v = ArithmeticHelper.VerticalProjection(gray); inLine = false; start = 0; var colLines = new List(); //开始根据投影值识别分割点 for (int i = 0; i < v.Length; ++i) { if (OutputImageWhenError) { Cv2.Line(bottomMat, i, 0, i, v[i], Scalar.Yellow); } if (v[i] > h.Average() * 5) { 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("log\\character_card_error.jpg", bottomMat); } throw new RetryException("未获取到角色区域"); } var rects = new List(); for (var 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("未获取到角色区域"); } //_logger.LogInformation("识别到角色卡牌区域:{Rects}", rects); //Cv2.ImWrite("log\\character_card_success.jpg", bottomMat); return rects; } /// /// 点击捕获区域的相对位置 /// /// /// /// 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) { var list = MatchTemplateHelper.MatchTemplateMulti(srcMat, kvp.Value, threshold); dictionary.Add(kvp.Key, list); // 把结果给遮掩掉,避免重复识别 foreach (var point in list) { Cv2.Rectangle(srcMat, point, new Point(point.X + kvp.Value.Width, point.Y + kvp.Value.Height), Scalar.Black, -1); } } return dictionary; }*/ public static Dictionary> FindMultiPicFromOneImage2OneByOne(Mat srcMat, Dictionary imgSubDictionary, double threshold = 0.8) { var dictionary = new Dictionary>(); foreach (var kvp in imgSubDictionary) { var list = new List(); while (true) { var point = MatchTemplateHelper.MatchTemplate(srcMat, kvp.Value, TemplateMatchModes.CCoeffNormed, null, threshold); if (point != new Point()) { // 把结果给遮掩掉,避免重复识别 Cv2.Rectangle(srcMat, point, new Point(point.X + kvp.Value.Width, point.Y + kvp.Value.Height), Scalar.Black, -1); list.Add(point); } else { break; } } dictionary.Add(kvp.Key, list); } return dictionary; } /// /// 重投骰子 /// /// 保留的元素类型 public bool RollPhaseReRoll(params ElementalType[] holdElementalTypes) { var gameSnapshot = CaptureGameMat(); Cv2.CvtColor(gameSnapshot, gameSnapshot, ColorConversionCodes.BGRA2BGR); var dictionary = FindMultiPicFromOneImage2OneByOne(gameSnapshot, _assets.RollPhaseDiceMats, 0.73); var count = dictionary.Sum(kvp => kvp.Value.Count); if (count != 8) { _logger.LogDebug("投骰子界面识别到了{Count}个骰子,等待重试", count); return false; } else { _logger.LogInformation("投骰子界面识别到了{Count}个骰子", count); } int upper = 0, lower = 0; foreach (var kvp in dictionary) { foreach (var point in kvp.Value) { if (point.Y < gameSnapshot.Height / 2) { upper++; } else { lower++; } } } if (upper != 4 || lower != 4) { _logger.LogInformation("骰子识别位置错误,重试"); return false; } 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 = CaptureGameRectArea().Find(_assets.ConfirmButtonRo); if (!foundRectArea.IsEmpty()) { foundRectArea.Click(); foundRectArea.Dispose(); 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 NormalEndException("对战已结束,停止自动打牌!"); } //MyLogger.Debug("识别骰子数量不正确,第{}次重试中...", retryCount); Sleep(500); if (retryCount > 35) { throw new System.Exception("识别骰子数量不正确,重试超时,停止自动打牌!"); } } ClickConfirm(); _logger.LogInformation("选择需要重投的骰子后点击确认完毕"); Sleep(1000); // 鼠标移动到中心 ClickGameWindowCenter(); _logger.LogInformation("等待5s对方重投"); Sleep(5000); } 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 = CaptureGameMat(); Cv2.CvtColor(srcMat, srcMat, ColorConversionCodes.BGRA2BGR); // 切割图片后再识别 加快速度 位置没啥用,所以切割后比较方便 var dictionary = FindMultiPicFromOneImage2OneByOne(CutRight(srcMat, srcMat.Width / 5), _assets.ActionPhaseDiceMats, 0.7); 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}", msg); return result; } /// /// 烧牌 /// public void ActionPhaseElementalTuning(int currentCardCount) { var rect = TaskContext.Instance().SystemInfo.CaptureAreaRect; var m = Simulation.SendInput.Mouse; ClickExtension.Click(rect.X + rect.Width / 2d, rect.Y + rect.Height - 50); Sleep(1500); if (currentCardCount == 1) { // 最后一张牌在右侧,而不是中间 ClickExtension.Move(rect.X + rect.Width / 2d + 120, rect.Y + rect.Height - 50); } 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 ra = CaptureGameRectArea(); // Cv2.ImWrite("log\\" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:ffff") + ".png", ra.SrcMat); var foundRectArea = ra.Find(_assets.ElementalTuningConfirmButtonRo); if (!foundRectArea.IsEmpty()) { foundRectArea.Click(); 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) { ClickGameWindowCenter(); // 复位 Sleep(500); // 技能坐标写死 (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.Click(x, y); Sleep(1200); // 等待动画彻底弹出 var foundRectArea = CaptureGameRectArea().Find(_assets.ElementalDiceLackWarningRo); if (foundRectArea.IsEmpty()) { // 多点几次保证点击到 _logger.LogInformation("使用技能{SkillIndex}", skillIndex); ClickExtension.Click(x, y); Sleep(500); ClickGameWindowCenter(); // 复位 return true; } return false; } /// /// 使用技能(元素骰子不够的情况下,自动烧牌) /// /// 技能编号,从右往左数,从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++) { _logger.LogInformation("- 烧第{Count}张牌", i + 1); ActionPhaseElementalTuning(duel.CurrentCardCount); Sleep(1200); var res = ActionPhaseElementalTuningConfirm(); if (res == false) { _logger.LogWarning("烧牌失败,重试"); i--; ClickGameWindowCenter(); // 复位 Sleep(1000); continue; } Sleep(1000); // 烧牌动画 ClickGameWindowCenter(); // 复位 Sleep(500); duel.CurrentCardCount--; // 最后一张牌的回正速度较慢,多等一会 if (duel.CurrentCardCount <= 1) { ClickGameWindowCenter(); // 复位 Sleep(500); } } // 存在吞星之鲸的情况下,烧牌后等待加血动画 if (duel.Characters.Any(c => c is { Name: "吞星之鲸" })) { Debug.WriteLine("存在吞星之鲸的情况下,烧牌后等待动画"); Sleep(5000); } } return ActionPhaseUseSkill(skillIndex); } /// /// 回合结束 /// public void RoundEnd() { CaptureGameRectArea().Find(_assets.RoundEndButtonRo, foundRectArea => { foundRectArea.Click(); Sleep(1000); // 有弹出动画 foundRectArea.Click(); Sleep(300); }); ClickGameWindowCenter(); // 复位 } /// /// 是否是再角色出战选择界面 /// 可重试方法 /// public void IsInCharacterPickRetryThrowable() { if (!IsInCharacterPick()) { throw new RetryException("当前不在角色出战选择界面"); } } /// /// 是否是再角色出战选择界面 /// /// public bool IsInCharacterPick() { return !CaptureGameRectArea().Find(_assets.InCharacterPickRo).IsEmpty(); } /// /// 是否是我的回合 /// /// public bool IsInMyAction() { return !CaptureGameRectArea().Find(_assets.RoundEndButtonRo).IsEmpty(); } /// /// 是否是对方的回合 /// /// public bool IsInOpponentAction() { return !CaptureGameRectArea().Find(_assets.InOpponentActionRo).IsEmpty(); } /// /// 是否是回合结算阶段 /// /// public bool IsEndPhase() { return !CaptureGameRectArea().Find(_assets.EndPhaseRo).IsEmpty(); } /// /// 出战角色是否被打倒 /// /// public bool IsActiveCharacterTakenOut() { return !CaptureGameRectArea().Find(_assets.CharacterTakenOutRo).IsEmpty(); } /// /// 哪些出战角色被打倒了 /// /// true 是已经被打倒 public bool[] WhatCharacterDefeated(List rects) { if (rects == null || rects.Count != 3) { throw new System.Exception("未能获取到我方角色卡位置"); } var pList = MatchTemplateHelper.MatchTemplateMulti(CaptureGameGreyMat(), _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 !CaptureGameRectArea().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 NormalEndException("对战已结束,停止自动打牌!"); } retryCount++; if (retryCount >= 60) { throw new System.Exception("等待对方行动超时,停止自动打牌!"); } _logger.LogInformation("对方仍在行动中,继续等待(次数{Count})...", 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("对方仍在行动中,继续等待(次数{Count})...", retryCount); } else if (IsEndPhase()) { _logger.LogInformation("正在回合结束阶段,继续等待(次数{Count})...", retryCount); } else if (IsInMyAction()) { if (IsActiveCharacterTakenOut()) { DoWhenCharacterDefeated(duel); } } else if (IsDuelEnd()) { throw new NormalEndException("对战已结束,停止自动打牌!"); } else { // 至少走三次判断才能确定对方行动结束 if (retryCount > 2) { break; } else { _logger.LogError("等待对方回合 和 回合结束阶段 时程序未识别到有效内容(次数{Count})...", retryCount); } } retryCount++; if (retryCount >= 60) { 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 NormalEndException("后续行动策略中,已经没有可切换且存活的角色了,结束自动打牌(建议添加更多行动)"); } foreach (var j in orderList) { if (!duel.Characters[j].IsDefeated) { duel.Characters[j].SwitchWhenTakenOut(); break; } } ClickGameWindowCenter(); Sleep(2000); // 切人动画 } public void AppendCharacterStatus(Character character, Mat greyMat, int hp = -2) { // 截取出战角色区域扩展 using var characterMat = new Mat(greyMat, 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; character.Hp = hp; _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 WhichCharacterActiveByHpOcr(duel); } public Character WhichCharacterActiveByHpWord(Duel duel) { if (duel.CharacterCardRects == null || duel.CharacterCardRects.Count != 3) { throw new System.Exception("未能获取到我方角色卡位置"); } var srcMat = CaptureGameMat(); int halfHeight = srcMat.Height / 2; Mat bottomMat = new(srcMat, new Rect(0, halfHeight, srcMat.Width, srcMat.Height - halfHeight)); var lowPurple = new Scalar(239, 239, 239); var highPurple = new Scalar(255, 255, 255); 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); //膨胀 Cv2.FindContours(gray, out var contours, out _, RetrievalModes.External, ContourApproximationModes.ApproxSimple, null); if (contours.Length > 0) { // .Where(w => w.Width > 1 && w.Height >= 5) var rects = contours .Select(Cv2.BoundingRect) // 按照Y轴高度排序 .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]; var grayMat = new Mat(); Cv2.CvtColor(srcMat, grayMat, ColorConversionCodes.BGR2GRAY); AppendCharacterStatus(duel.CurrentCharacter, grayMat); Cv2.Rectangle(srcMat, rect1, Scalar.Yellow); Cv2.Rectangle(srcMat, duel.CharacterCardRects[i], Scalar.Blue, 2); OutputImage(duel, rects, bottomMat, halfHeight, "log\\active_character2_success.jpg"); return duel.CurrentCharacter; } } } OutputImage(duel, rects, bottomMat, halfHeight, "log\\active_character2_no_overlap_error.jpg"); } else { if (OutputImageWhenError) { Cv2.ImWrite("log\\active_character2_no_rects_error.jpg", gray); } } throw new RetryException("未识别到个出战角色"); } public Character WhichCharacterActiveByHpOcr(Duel duel) { if (duel.CharacterCardRects == null || duel.CharacterCardRects.Count != 3) { throw new System.Exception("未能获取到我方角色卡位置"); } var srcMat = CaptureGameGreyMat(); var hpArray = new int[3]; // 1 代表未出战 2 代表出战 for (var i = 0; i < duel.CharacterCardRects.Count; i++) { if (duel.Characters[i + 1].IsDefeated) { // 已经被击败的角色肯定未出战 hpArray[i] = 1; continue; } var cardRect = duel.CharacterCardRects[i]; // 未出战角色的hp区域 var hpMat = new Mat(srcMat, new Rect(cardRect.X + _config.CharacterCardExtendHpRect.X, cardRect.Y + _config.CharacterCardExtendHpRect.Y, _config.CharacterCardExtendHpRect.Width, _config.CharacterCardExtendHpRect.Height)); var text = OcrFactory.Paddle.Ocr(hpMat); //Cv2.ImWrite($"log\\hp_n_{i}.jpg", hpMat); Debug.WriteLine($"角色{i}未出战HP位置识别结果{text}"); if (!string.IsNullOrWhiteSpace(text)) { // 说明这个角色未出战 hpArray[i] = 1; } else { hpMat = new Mat(srcMat, new Rect(cardRect.X + _config.CharacterCardExtendHpRect.X, cardRect.Y + _config.CharacterCardExtendHpRect.Y - _config.ActiveCharacterCardSpace, _config.CharacterCardExtendHpRect.Width, _config.CharacterCardExtendHpRect.Height)); text = OcrFactory.Paddle.Ocr(hpMat); //Cv2.ImWrite($"log\\hp_active_{i}.jpg", hpMat); Debug.WriteLine($"角色{i}出战HP位置识别结果{text}"); if (!string.IsNullOrWhiteSpace(text)) { var hp = -2; if (RegexHelper.FullNumberRegex().IsMatch(text)) { hp = int.Parse(text); } hpArray[i] = 2; duel.CurrentCharacter = duel.Characters[i + 1]; AppendCharacterStatus(duel.CurrentCharacter, srcMat, hp); return duel.CurrentCharacter; } } } if (hpArray.Count(x => x == 1) == 2) { // 找到并不等1的 var index = hpArray.ToList().FindIndex(x => x != 1); Debug.WriteLine($"通过OCR HP的方式没有识别到出战角色,但是通过排除法确认角色{index + 1}处于出战状态!"); duel.CurrentCharacter = duel.Characters[index + 1]; AppendCharacterStatus(duel.CurrentCharacter, srcMat); return duel.CurrentCharacter; } // 上面判断失效 _logger.LogWarning("通过OCR HP的方式未识别到出战角色 {Array}", hpArray); return NewRetry.Do(() => WhichCharacterActiveByHpWord(duel), TimeSpan.FromSeconds(0.3), 2); } 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); } } /// /// 通过OCR识别当前骰子数量 /// /// public int GetDiceCountByOcr() { var srcMat = CaptureGameGreyMat(); var diceCountMap = new Mat(srcMat, _config.MyDiceCountRect); var text = OcrFactory.Paddle.OcrWithoutDetector(diceCountMap); text = text.Replace(" ", "") .Replace("①", "1") .Replace("②", "2") .Replace("③", "3") .Replace("④", "4") .Replace("⑤", "5") .Replace("⑥", "6") .Replace("⑦", "7") .Replace("⑧", "8") .Replace("⑨", "9") .Replace("⑩", "10") .Replace("⑪", "11") .Replace("⑫", "12") .Replace("⑬", "13") .Replace("⑭", "14") .Replace("⑮", "15"); if (string.IsNullOrWhiteSpace(text)) { _logger.LogWarning("通过OCR识别当前骰子数量结果为空,无影响"); #if DEBUG Cv2.ImWrite($"log\\dice_count_empty{DateTime.Now:yyyy-MM-dd HH:mm:ss:ffff}.jpg", diceCountMap); #endif return -10; } else if (RegexHelper.FullNumberRegex().IsMatch(text)) { _logger.LogInformation("通过OCR识别当前骰子数量: {Text}", text); return int.Parse(text); } else { _logger.LogWarning("通过OCR识别当前骰子结果: {Text}", text); #if DEBUG Cv2.ImWrite($"log\\dice_count_error_{DateTime.Now:yyyy-MM-dd HH:mm:ss:ffff}.jpg", diceCountMap); #endif return -10; } } }