Files
better-genshin-impact/BetterGenshinImpact/GameTask/AutoStygianOnslaught/AutoStygianOnslaughtTask.cs

1154 lines
43 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using BetterGenshinImpact.Core.BgiVision;
using BetterGenshinImpact.Core.Recognition;
using BetterGenshinImpact.Core.Recognition.OCR;
using BetterGenshinImpact.Core.Simulator;
using BetterGenshinImpact.Core.Simulator.Extensions;
using BetterGenshinImpact.GameTask.AutoArtifactSalvage;
using BetterGenshinImpact.GameTask.AutoDomain;
using BetterGenshinImpact.GameTask.AutoDomain.Model;
using BetterGenshinImpact.GameTask.AutoFight;
using BetterGenshinImpact.GameTask.AutoFight.Assets;
using BetterGenshinImpact.GameTask.AutoFight.Model;
using BetterGenshinImpact.GameTask.AutoFight.Script;
using BetterGenshinImpact.GameTask.AutoGeniusInvokation.Exception;
using BetterGenshinImpact.GameTask.AutoPick.Assets;
using BetterGenshinImpact.GameTask.AutoTrackPath;
using BetterGenshinImpact.GameTask.Common;
using BetterGenshinImpact.GameTask.Common.BgiVision;
using BetterGenshinImpact.GameTask.Common.Element.Assets;
using BetterGenshinImpact.GameTask.Common.Job;
using BetterGenshinImpact.GameTask.Common.StateMachine;
using BetterGenshinImpact.GameTask.Model.Area;
using BetterGenshinImpact.GameTask.QuickTeleport.Assets;
using BetterGenshinImpact.Helpers.Extensions;
using BetterGenshinImpact.Service.Notification;
using BetterGenshinImpact.Service.Notification.Model.Enum;
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;
namespace BetterGenshinImpact.GameTask.AutoStygianOnslaught;
/// <summary>
/// 状态机状态定义
/// 每个状态代表一个明确的 UI 场景
/// </summary>
public enum StygianState
{
Unknown, // 未知状态
MainWorld, // 主世界(有派蒙图标)
EventMenu, // 活动菜单(活动一览)
StygianOnslaughtPage, // 幽境危战页面(前往挑战)
TeleportMap, // 传送地图(传送按钮)
DomainEntrance, // 秘境入口(交互提示)
DifficultySelect, // 难度选择(单人挑战、困难、至危挑战)
DomainLoading, // 秘境加载中
DomainLobby, // 秘境门厅(地脉异常图标 + 有背包图标,可走到钥匙)
BossSelect, // Boss选择界面开始挑战、角色预览
BattleArena, // 战斗场地(地脉异常图标 + 无背包图标,准备战斗)
BattleLoading, // 战斗加载
InBattle, // 战斗中无明显UI
BattleResultWin, // 战斗结果-胜利(有返回按钮)
BattleResultLose, // 战斗结果-失败
LeylineFlowerPrompt, // 地脉花领取界面
ResinSelect, // 树脂选择
ContinueOrExit, // 继续或退出选择
Exiting, // 退出中
}
/// <summary>
/// 自动幽境危战任务 - 使用状态机模式(注册式状态处理器)
///
/// 设计模式:
/// 1. 继承 StateMachineBase 获得状态机基础设施
/// 2. 子类只需实现 DetectCurrentState() 和注册各状态处理器
/// 3. 运行时调用 RunStateMachineUntil() 自动驱动状态机
/// </summary>
public class AutoStygianOnslaughtTask : StateMachineBase<StygianState, BvPage>, ISoloTask
{
public string Name => "自动幽境危战";
/// <summary>
/// 实现基类 Logger 抽象属性 - 复用 TaskControl.Logger
/// </summary>
protected override ILogger Logger => TaskControl.Logger;
private readonly AutoStygianOnslaughtParam _taskParam;
private readonly CombatScriptBag _combatScriptBag;
private List<ResinUseRecord> _resinPriorityListWhenSpecifyUse;
private LowerHeadThenWalkToTask? _lowerHeadThenWalkToTask;
public AutoStygianOnslaughtTask(AutoStygianOnslaughtParam taskParam)
{
AutoFightAssets.DestroyInstance();
_taskParam = taskParam;
_combatScriptBag = CombatScriptParser.ReadAndParse(taskParam.CombatScriptBagPath);
_resinPriorityListWhenSpecifyUse = ResinUseRecord.BuildFromDomainParam(taskParam);
// 注册所有状态处理器
RegisterAllStateHandlers();
}
public AutoStygianOnslaughtTask(AutoStygianOnslaughtParam taskParam, string path)
{
AutoFightAssets.DestroyInstance();
_taskParam = taskParam;
_combatScriptBag = CombatScriptParser.ReadAndParse(path);
_resinPriorityListWhenSpecifyUse = ResinUseRecord.BuildFromDomainParam(taskParam);
// 注册所有状态处理器
RegisterAllStateHandlers();
}
/// <summary>
/// 注册所有状态处理器和检测器 - 子类核心职责
/// 每个状态对应一个处理方法,消除 switch-case
/// </summary>
private void RegisterAllStateHandlers()
{
// ========== 注册状态检测器 ==========
// 注意:检测顺序会影响性能,先放快速的模板匹配检测
RegisterStateDetectors(
// 第一优先级:快速模板匹配(不需要 OCR
(StygianState.ContinueOrExit, DetectContinueOrExit),
(StygianState.TeleportMap, DetectTeleportMap),
(StygianState.DomainLobby, DetectDomainLobby),
(StygianState.BattleArena, DetectBattleArena),
(StygianState.MainWorld, DetectMainWorld),
// 第二优先级:模板匹配 + 局部 OCR
(StygianState.BattleResultWin, DetectBattleResultWin),
(StygianState.BattleResultLose, DetectBattleResultLose),
// 第三优先级OCR 检测
(StygianState.ResinSelect, DetectResinSelect),
(StygianState.LeylineFlowerPrompt, DetectLeylineFlowerPrompt),
(StygianState.BossSelect, DetectBossSelect),
(StygianState.DifficultySelect, DetectDifficultySelect),
(StygianState.DomainEntrance, DetectDomainEntrance),
(StygianState.EventMenu, DetectEventMenu),
(StygianState.StygianOnslaughtPage, DetectStygianOnslaughtPage)
);
// ========== 注册状态处理器 ==========
// 导航阶段处理器
RegisterStateHandlers(
(StygianState.MainWorld, HandleMainWorldState),
(StygianState.EventMenu, HandleEventMenuState),
(StygianState.StygianOnslaughtPage, HandleStygianOnslaughtPageState),
(StygianState.TeleportMap, HandleTeleportMapState),
(StygianState.DomainEntrance, HandleDomainEntranceState),
(StygianState.DifficultySelect, HandleDifficultySelectState),
(StygianState.DomainLobby, HandleDomainLobbyState),
// 战斗阶段处理器
(StygianState.BossSelect, HandleBossSelectState),
(StygianState.BattleArena, HandleBattleArenaState),
(StygianState.BattleResultWin, HandleBattleResultWinState),
(StygianState.BattleResultLose, HandleBattleResultLoseState),
(StygianState.LeylineFlowerPrompt, HandleLeylineFlowerState),
(StygianState.ResinSelect, HandleResinSelectState),
(StygianState.ContinueOrExit, HandleContinueOrExitState)
);
// 注册状态转换关系 - 有限状态机的核心
// 每个状态只能转换到有限的下一个状态,用于优化检测
// 注意:候选状态的顺序影响检测优先级,更具体的状态应该放前面
RegisterStateTransitions(
// 导航阶段
(StygianState.MainWorld, [StygianState.EventMenu, StygianState.StygianOnslaughtPage]),
(StygianState.EventMenu, [StygianState.StygianOnslaughtPage]),
(StygianState.StygianOnslaughtPage, [StygianState.TeleportMap, StygianState.DomainEntrance]),
(StygianState.TeleportMap, [StygianState.DomainEntrance]),
(StygianState.DomainEntrance, [StygianState.DomainEntrance, StygianState.DifficultySelect]),
(StygianState.DifficultySelect, [StygianState.DomainLobby]),
(StygianState.DomainLobby, [StygianState.BossSelect, StygianState.LeylineFlowerPrompt]),
// 战斗阶段
(StygianState.BossSelect, [StygianState.BattleArena]),
(StygianState.BattleArena, [StygianState.BattleResultWin, StygianState.BattleResultLose]),
(StygianState.BattleResultWin, [StygianState.DomainLobby]),
(StygianState.BattleResultLose, [StygianState.BossSelect]),
(StygianState.LeylineFlowerPrompt, [StygianState.ResinSelect]),
(StygianState.ResinSelect, [StygianState.ContinueOrExit, StygianState.DomainLobby]),
(StygianState.ContinueOrExit, [StygianState.BattleArena, StygianState.MainWorld])
);
// 未知状态处理器
RegisterUnknownStateHandler(HandleUnknownState);
}
public async Task Start(CancellationToken ct)
{
_lowerHeadThenWalkToTask = new LowerHeadThenWalkToTask("chest_tip.png", 20000);
Initialize(ct, StygianState.Unknown);
Init();
Notify.Event(NotificationEvent.DomainStart).Success($"{Name}启动");
try
{
await DoDomain();
}
catch (TaskCanceledException)
{
// do nothing
}
catch (Exception e)
{
Logger.LogInformation(e.Message);
}
await Delay(3000, ct);
await ArtifactSalvage();
Notify.Event(NotificationEvent.DomainEnd).Success($"{Name}结束");
}
private async Task DoDomain()
{
var page = new BvPage(_ct);
// 阶段1导航到秘境 - 状态机自动驱动
// MainWorld → EventMenu → StygianOnslaughtPage → TeleportMap → DomainEntrance → DifficultySelect → DomainLobby → BossSelect → BattleArena
await new ReturnMainUiTask().Start(_ct);
await RunStateMachineUntil(page, StygianState.BattleArena);
// 阶段2战斗循环
await BattleLoopStateMachine(page);
await ExitDomain(page);
}
#region - 使 ImageRegion
// ========== 第一优先级:快速模板匹配 ==========
private bool DetectContinueOrExit(ImageRegion ra)
{
return ra.Find(AutoFightAssets.Instance.ConfirmRa).IsExist() &&
ra.Find(AutoFightAssets.Instance.ExitRa).IsExist();
}
private bool DetectTeleportMap(ImageRegion ra)
{
return ra.Find(QuickTeleportAssets.Instance.TeleportButtonRo).IsExist();
}
private bool DetectDomainLobby(ImageRegion ra)
{
return ra.Find(ElementAssets.Instance.LeylineDisorderIconRo).IsExist() &&
ra.Find(ElementAssets.Instance.InventoryRo).IsExist();
}
private bool DetectBattleArena(ImageRegion ra)
{
return ra.Find(ElementAssets.Instance.LeylineDisorderIconRo).IsExist() &&
!ra.Find(ElementAssets.Instance.InventoryRo).IsExist();
}
private bool DetectMainWorld(ImageRegion ra)
{
return ra.Find(ElementAssets.Instance.PaimonMenuRo).IsExist();
}
// ========== 第二优先级:模板匹配 + 局部 OCR ==========
private bool DetectBattleResultWin(ImageRegion ra)
{
return ra.Find(ElementAssets.Instance.BtnWhiteCancel).IsExist() &&
ra.FindMulti(RecognitionObject.Ocr(ra.Width * 0.35, ra.Height * 0.7, ra.Width * 0.3, ra.Height * 0.2))
.Any(o => o.Text.Contains("返回"));
}
private bool DetectBattleResultLose(ImageRegion ra)
{
return ra.Find(ElementAssets.Instance.BtnWhiteConfirm).IsExist() &&
ra.FindMulti(RecognitionObject.Ocr(ra.Width * 0.2, ra.Height * 0.3, ra.Width * 0.6, ra.Height * 0.3))
.Any(o => o.Text.Contains("挑战失败") || o.Text.Contains("重新挑战"));
}
// ========== 第三优先级OCR 检测 ==========
private bool DetectResinSelect(ImageRegion ra)
{
var ocrResult = ra.FindMulti(RecognitionObject.Ocr(ra.Width * 0.2, ra.Height * 0.2, ra.Width * 0.6, ra.Height * 0.6));
return ocrResult.Any(t => t.Text.Contains("地脉之花")) &&
ocrResult.Any(t => t.Text.Contains("浓缩树脂") || t.Text.Contains("原粹树脂"));
}
private bool DetectLeylineFlowerPrompt(ImageRegion ra)
{
var ocrResult = ra.FindMulti(RecognitionObject.Ocr(ra.Width * 0.2, ra.Height * 0.2, ra.Width * 0.6, ra.Height * 0.6));
var found = ocrResult.Any(t => t.Text.Contains("地脉之花"));
return found;
}
private bool DetectBossSelect(ImageRegion ra)
{
// "角色预览" 在右上角,"开始挑战" 在右下角
// 检测右侧整个区域
var ocrResult = ra.FindMulti(RecognitionObject.Ocr(ra.Width * 0.5, 0, ra.Width * 0.5, ra.Height));
var hasPreview = ocrResult.Any(o => o.Text.Contains("角色预览"));
var hasStart = ocrResult.Any(o => o.Text.Contains("开始挑战"));
var found = hasPreview && hasStart;
return found;
}
private bool DetectDifficultySelect(ImageRegion ra)
{
// "单人挑战" 在右下角
return ra.FindMulti(RecognitionObject.Ocr(ra.Width * 0.5, ra.Height * 0.7, ra.Width * 0.5, ra.Height * 0.3))
.Any(o => o.Text.Contains("单人挑战"));
}
private bool DetectDomainEntrance(ImageRegion ra)
{
// 秘境入口特征:屏幕右侧有"幽境危战"四个字
// 坐标:左上角(1223, 510), 右下角(1376, 566)
// 宽度=153, 高度=56
return ra.FindMulti(RecognitionObject.Ocr(1223, 510, 153, 56))
.Any(o => o.Text.Contains("幽境危战"));
}
private bool DetectEventMenu(ImageRegion ra)
{
// 活动一览位置:左上角(125, 142), 右下角(238, 170)
// OCR 参数:(x, y, width, height)
return ra.FindMulti(RecognitionObject.Ocr(125, 142, 238 - 125, 170 - 142))
.Any(o => o.Text.Contains("活动一览"));
}
private bool DetectStygianOnslaughtPage(ImageRegion ra)
{
return ra.FindMulti(RecognitionObject.Ocr(ra.Width * 0.55, ra.Height * 0.3, ra.Width * 0.4, ra.Height * 0.6))
.Any(o => o.Text.Contains("前往挑战"));
}
#endregion
#region - case
private async Task<StateHandlerResult> HandleMainWorldState(BvPage page)
{
Logger.LogInformation($"{Name}:打开活动菜单");
Simulation.SendInput.SimulateAction(GIActions.OpenTheEventsMenu);
await Delay(500, _ct);
return StateHandlerResult.Success; // 等待转换到 EventMenu 或 StygianOnslaughtPage
}
private async Task<StateHandlerResult> HandleEventMenuState(BvPage page)
{
Logger.LogInformation($"{Name}:在活动菜单中查找幽境危战");
// 列表区域:左上角(195, 201), 右下角(491, 855),基于 1080P
var listCenterX = (195 + 491) / 2; // 343
var listCenterY = (201 + 855) / 2; // 528
var listRegion = new Rect(195, 201, 491 - 195, 855 - 201);
// 最多尝试两次(先往下滑动搜索,如果没找到再往上滑动搜索)
for (int attempt = 0; attempt < 2; attempt++)
{
// 1. 拖动滑动列表
page.Click(listCenterX, listCenterY - 200);
await Delay(100, _ct);
Simulation.SendInput.Mouse.LeftButtonDown();
await Delay(100, _ct);
// 从上往下拖动(内容往上滚动)
for (int y = listCenterY - 200; y < listCenterY + 200; y += 50)
{
GameCaptureRegion.GameRegion1080PPosMove(listCenterX, y);
await Delay(30, _ct);
}
Simulation.SendInput.Mouse.LeftButtonUp();
await Delay(500, _ct);
// 2. 在列表区域内查找"幽境危战"并点击
var target = page.GetByText("幽境危战").WithRoi(listRegion).FindAll().FirstOrDefault();
if (target != null)
{
target.Click();
await Delay(300, _ct);
return StateHandlerResult.Success; // 等待转换到 StygianOnslaughtPage
}
Logger.LogInformation($"{Name}:第 {attempt + 1} 次未找到幽境危战,尝试反向滑动");
}
// 如果两次都没找到,可能"幽境危战"已经被选中,直接尝试检测下一状态
Logger.LogWarning($"{Name}:未找到幽境危战,可能已被选中,尝试检测 StygianOnslaughtPage");
return StateHandlerResult.Success; // 让状态机继续等待邻接状态出现
}
private async Task<StateHandlerResult> HandleStygianOnslaughtPageState(BvPage page)
{
Logger.LogInformation($"{Name}:点击前往挑战");
var challengeButton = page.GetByText("前往挑战").WithRoi(r => r.CutRight(0.5)).FindAll().FirstOrDefault();
if (challengeButton == null)
{
Logger.LogWarning($"{Name}:未找到前往挑战按钮");
return StateHandlerResult.Retry;
}
challengeButton.Click();
await Delay(300, _ct);
return StateHandlerResult.Success; // 等待转换到 TeleportMap 或 DomainEntrance
}
private async Task<StateHandlerResult> HandleTeleportMapState(BvPage page)
{
Logger.LogInformation($"{Name}:点击传送");
var teleportButton = page.Locator(QuickTeleportAssets.Instance.TeleportButtonRo).FindAll().FirstOrDefault();
if (teleportButton == null)
{
Logger.LogWarning($"{Name}:未找到传送按钮");
return StateHandlerResult.Retry;
}
teleportButton.Click();
await Delay(300, _ct);
return StateHandlerResult.Success; // 等待转换到 DomainEntrance
}
private async Task<StateHandlerResult> HandleDomainEntranceState(BvPage page)
{
Logger.LogInformation($"{Name}:交互秘境入口");
Simulation.SendInput.SimulateAction(GIActions.PickUpOrInteract);
await Delay(500, _ct);
return StateHandlerResult.Success; // 等待转换到 DifficultySelect
}
private async Task<StateHandlerResult> HandleDifficultySelectState(BvPage page)
{
Logger.LogInformation($"{Name}:选择困难难度并进入");
// 切换到困难模式
await SwitchToHardModeLoop(page);
// 点击确认进入
using var ra = CaptureToRectArea();
var btn = ra.Find(ElementAssets.Instance.BtnWhiteConfirm);
if (btn.IsEmpty())
{
Logger.LogWarning($"{Name}:未找到进入确认按钮");
return StateHandlerResult.Retry;
}
btn.Click();
await Delay(300, _ct);
return StateHandlerResult.Success; // 等待转换到 DomainLobby
}
private async Task<StateHandlerResult> HandleDomainLobbyState(BvPage page)
{
Logger.LogInformation($"{Name}:步行前往钥匙");
await new WalkToFTask().Start(_ct);
return StateHandlerResult.Success; // 等待转换到 BossSelect 或 LeylineFlowerPrompt
}
private async Task<StateHandlerResult> HandleBossSelectState(BvPage page)
{
Logger.LogInformation($"{Name}选择Boss并开始挑战");
// 选择Boss
SelectBoss(page);
// 切换队伍
await SwitchTeam(page);
// 点击开始挑战
using var ra = CaptureToRectArea();
Bv.ClickWhiteConfirmButton(ra);
await Delay(300, _ct);
return StateHandlerResult.Success; // 等待转换到 BattleArena
}
private Task<StateHandlerResult> HandleBattleArenaState(BvPage page)
{
// 战斗场地已准备就绪,无需额外操作
// 状态机会检测到 BattleArena 是目标状态并退出
Logger.LogInformation($"{Name}:战斗场地已准备就绪");
return Task.FromResult(StateHandlerResult.Wait); // 状态机会检测到目标状态并退出
}
private async Task<StateHandlerResult> HandleBattleResultWinState(BvPage page)
{
Logger.LogInformation($"{Name}:挑战成功,等待返回大厅");
using var ra = CaptureToRectArea();
Bv.ClickWhiteCancelButton(ra);
await Delay(300, _ct);
return StateHandlerResult.Success; // 等待转换到 DomainLobby
}
private async Task<StateHandlerResult> HandleBattleResultLoseState(BvPage page)
{
Logger.LogWarning($"{Name}挑战失败等待返回Boss选择");
using var ra = CaptureToRectArea();
Bv.ClickWhiteConfirmButton(ra);
await Delay(300, _ct);
return StateHandlerResult.Success; // 等待转换到 BossSelect
}
private async Task<StateHandlerResult> HandleLeylineFlowerState(BvPage page)
{
Logger.LogInformation($"{Name}:交互地脉花");
Simulation.SendInput.SimulateAction(GIActions.PickUpOrInteract);
await Delay(300, _ct);
return StateHandlerResult.Success; // 等待转换到 ResinSelect
}
private async Task<StateHandlerResult> HandleResinSelectState(BvPage page)
{
Logger.LogInformation($"{Name}:选择树脂");
using var ra = CaptureToRectArea();
await UseResinAndCheckLast(ra);
return StateHandlerResult.Success; // 等待转换到 ContinueOrExit 或 DomainLobby
}
private async Task<StateHandlerResult> HandleContinueOrExitState(BvPage page)
{
Logger.LogInformation($"{Name}:处理继续/退出选择");
using var ra = CaptureToRectArea();
// 检查是否还有树脂
var isLastTurn = _resinPriorityListWhenSpecifyUse.Sum(o => o.RemainCount) <= 0;
if (isLastTurn)
{
var exitBtn = ra.Find(AutoFightAssets.Instance.ExitRa);
if (!exitBtn.IsEmpty())
{
exitBtn.Click();
}
}
else
{
var confirmBtn = ra.Find(AutoFightAssets.Instance.ConfirmRa);
if (!confirmBtn.IsEmpty())
{
confirmBtn.Click();
await Delay(60, _ct);
confirmBtn.Click();
}
}
await Delay(300, _ct);
return StateHandlerResult.Success; // 等待转换到 BattleArena 或 MainWorld
}
private async Task<StateHandlerResult> HandleUnknownState(BvPage page)
{
Logger.LogWarning("未知状态,尝试返回主界面");
await new ReturnMainUiTask().Start(_ct);
return StateHandlerResult.Wait; // 返回主界面后,状态机会重新检测状态
}
#endregion
#region
/// <summary>
/// 战斗循环状态机
/// </summary>
private async Task BattleLoopStateMachine(BvPage page)
{
Logger.LogInformation("========== 开始战斗循环 ==========");
for (var round = 0; round < 9999; round++)
{
_ct.ThrowIfCancellationRequested();
Logger.LogInformation(">>> 第 {Round} 轮战斗 <<<", round + 1);
// 执行战斗
await ExecuteBattleRound(page);
// 处理战斗结果(战斗结束后,等待 BattleArena 的邻接状态)
CurrentState = StygianState.BattleArena;
var resultState = await EnsureNextStateTransition(60000);
if (resultState == StygianState.BattleResultLose)
{
// 内部使用闭环检测确保返回Boss选择
await HandleBattleResultLoseState(page);
continue;
}
// 胜利后处理(内部使用闭环检测确保返回大厅)
await HandleBattleResultWinState(page);
// 防止在地脉花上
Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyDown);
await Delay(200, _ct);
Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyUp);
await Delay(2000, _ct);
// 寻找地脉花
Logger.LogInformation($"{Name}:寻找地脉花");
await FindAndInteractLeylineFlowerLoop();
// 处理奖励
var shouldContinue = await HandleRewardStateMachine();
if (!shouldContinue)
{
Logger.LogInformation($"{Name}:体力耗尽或轮次达标,结束战斗");
break;
}
Notify.Event(NotificationEvent.DomainReward).Success($"{Name}奖励领取");
// 点击继续后会直接进入战斗场地(等待 ContinueOrExit 的邻接状态)
CurrentState = StygianState.ContinueOrExit;
await EnsureNextStateTransition(60000);
}
Logger.LogInformation("========== 战斗循环结束 ==========");
}
private async Task ExecuteBattleRound(BvPage page)
{
// 等待进入战斗场地(从 BossSelect 转换到 BattleArena
CurrentState = StygianState.BossSelect;
await EnsureNextStateTransition(60000);
// 初始化战斗
var combatScenes = await InitializeCombatScenesLoop();
var combatCommands = await PrepareForBattleLoop(combatScenes);
Logger.LogInformation($"{Name}:执行战斗策略");
await StartFight(combatScenes, combatCommands);
}
/// <summary>
/// 处理奖励状态机
/// </summary>
private async Task<bool> HandleRewardStateMachine()
{
await Delay(300, _ct);
// 等待奖励界面(从 LeylineFlowerPrompt 转换到 ResinSelect
CurrentState = StygianState.LeylineFlowerPrompt;
var resinState = await EnsureNextStateTransition(10000);
if (resinState == StygianState.Unknown)
{
Logger.LogWarning("未检测到奖励界面");
return true;
}
using var ra = CaptureToRectArea();
var textList = ra.FindMulti(RecognitionObject.Ocr(ra.Width * 0.25, ra.Height * 0.2, ra.Width * 0.5, ra.Height * 0.6));
// 检查是否无树脂
if (textList.Any(t => t.Text.Contains("数量不足") || t.Text.Contains("补充原粹树脂")))
{
Logger.LogInformation("原粹树脂已用尽");
return false;
}
// 使用树脂
var isLastTurn = await UseResinAndCheckLast(ra);
await Delay(1000, _ct);
// 等待继续/退出界面(从 ResinSelect 转换到 ContinueOrExit 或 DomainLobby
CurrentState = StygianState.ResinSelect;
var continueState = await EnsureNextStateTransition(10000);
if (continueState == StygianState.ContinueOrExit)
{
if (isLastTurn)
{
using var ra2 = CaptureToRectArea();
var exitBtn = ra2.Find(AutoFightAssets.Instance.ExitRa);
if (!exitBtn.IsEmpty())
{
exitBtn.Click();
return false;
}
}
else
{
using var ra2 = CaptureToRectArea();
var confirmBtn = ra2.Find(AutoFightAssets.Instance.ConfirmRa);
if (!confirmBtn.IsEmpty())
{
confirmBtn.Click();
await Delay(60, _ct);
confirmBtn.Click();
}
}
}
return !isLastTurn;
}
private async Task<bool> UseResinAndCheckLast(ImageRegion ra)
{
bool isLastTurn = false;
if (!_taskParam.SpecifyResinUse)
{
// 自动刷干树脂
// 识别树脂状况
var resinStatus = ResinStatus.RecogniseFromRegion(ra, TaskContext.Instance().SystemInfo, OcrFactory.Paddle);
resinStatus.Print(Logger);
if (resinStatus is { CondensedResinCount: <= 0, OriginalResinCount: < 20 })
{
Logger.LogWarning("树脂不足");
return true;
}
if (resinStatus.CondensedResinCount > 0)
{
AutoDomainTask.PressUseResin(ra, "浓缩树脂", Name);
resinStatus.CondensedResinCount -= 1;
}
else if (resinStatus.OriginalResinCount >= 20)
{
var (_, num) = AutoDomainTask.PressUseResin(ra, "原粹树脂", Name);
resinStatus.OriginalResinCount -= num;
}
isLastTurn = resinStatus is { CondensedResinCount: <= 0, OriginalResinCount: < 20 };
}
else
{
var textList = ra.FindMulti(RecognitionObject.Ocr(ra.Width * 0.25, ra.Height * 0.2, ra.Width * 0.5, ra.Height * 0.6));
int successCount = 0;
foreach (var record in _resinPriorityListWhenSpecifyUse)
{
if (record.RemainCount > 0)
{
var (success, _) = AutoDomainTask.PressUseResin(textList, record.Name, Name);
if (success)
{
record.RemainCount -= 1;
Logger.LogInformation("自动秘境:{Name} 刷取 {Re}/{Max}",
record.Name, record.MaxCount - record.RemainCount, record.MaxCount);
successCount++;
break;
}
}
}
isLastTurn = _resinPriorityListWhenSpecifyUse.Sum(o => o.RemainCount) <= 0;
if (successCount == 0)
{
Logger.LogWarning("指定树脂领取次数时,当前可用树脂选项无法满足配置");
return true;
}
}
return isLastTurn;
}
#endregion
#region
private void Init()
{
LogScreenResolution();
if (_taskParam.SpecifyResinUse)
{
Logger.LogInformation("→ {Text} 指定使用树脂", $"{Name}");
}
else
{
Logger.LogInformation("→ {Text} 用尽所有浓缩树脂和原粹树脂后结束", $"{Name}");
}
}
private void LogScreenResolution()
{
var gameScreenSize = SystemControl.GetGameScreenRect(TaskContext.Instance().GameHandle);
if (gameScreenSize.Width * 9 != gameScreenSize.Height * 16)
{
Logger.LogError("游戏窗口分辨率不是 16:9 !当前分辨率为 {Width}x{Height}",
gameScreenSize.Width, gameScreenSize.Height);
throw new Exception("游戏窗口分辨率不是 16:9");
}
if (gameScreenSize.Width < 1920 || gameScreenSize.Height < 1080)
{
Logger.LogWarning("游戏窗口分辨率小于 1920x1080 !当前分辨率为 {Width}x{Height}",
gameScreenSize.Width, gameScreenSize.Height);
}
}
private async Task<CombatScenes> InitializeCombatScenesLoop()
{
CombatScenes? result = null;
var found = await NewRetry.WaitForAction(() =>
{
result = new CombatScenes().InitializeTeam(CaptureToRectArea());
return result.CheckTeamInitialized();
}, _ct, 10, 500);
if (!found || result == null)
{
throw new Exception("识别队伍角色失败!");
}
Logger.LogInformation($"{Name}:队伍初始化成功");
return result;
}
private async Task<List<CombatCommand>> PrepareForBattleLoop(CombatScenes combatScenes)
{
var combatCommands = FindCombatScriptAndSwitchAvatar(combatScenes);
await Delay(1500, _ct);
Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyDown);
await Delay(1200, _ct);
Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyUp);
return combatCommands;
}
private List<CombatCommand> FindCombatScriptAndSwitchAvatar(CombatScenes combatScenes)
{
var combatCommands = _combatScriptBag.FindCombatScript(combatScenes.GetAvatars());
var avatar = combatScenes.SelectAvatar(combatCommands[0].Name);
avatar?.SwitchWithoutCts();
Sleep(200, _ct);
return combatCommands;
}
private async Task FindAndInteractLeylineFlowerLoop()
{
// 先看看当前身边是否有F有的话直接F
using var ra1 = CaptureToRectArea();
var text = Bv.FindFKeyText(ra1);
if (string.IsNullOrEmpty(text) || !text.Contains("激活"))
{
await _lowerHeadThenWalkToTask!.Start(_ct);
}
await NewRetry.WaitForAction(() =>
{
Simulation.SendInput.SimulateAction(GIActions.PickUpOrInteract);
Sleep(300, _ct);
using var ra = CaptureToRectArea();
var ocrList = ra.FindMulti(RecognitionObject.Ocr(ra.Width * 0.25, ra.Height * 0.2, ra.Width * 0.5, ra.Height * 0.6));
if (ocrList.Any(t => t.Text.Contains("地脉之花")))
{
Logger.LogInformation($"{Name}:成功交互地脉花");
return true;
}
return false;
}, _ct, 10, 300);
}
private async Task SwitchToHardModeLoop(BvPage page)
{
var found = await NewRetry.WaitForAction(() =>
{
// 如果已经在困难模式,直接返回
if (page.GetByText("困难").WithRoi(r => r.CutRightTop(0.5, 0.2)).IsExist())
{
return true;
}
// 检测是否在至危挑战模式,点击切换到常规挑战
var ultimateChallenge = page.GetByText("至危挑战").WithRoi(r => r.CutLeftTop(0.5, 0.2)).FindAll().FirstOrDefault();
if (ultimateChallenge != null)
{
Logger.LogInformation($"{Name}:检测到至危挑战,点击切换到常规挑战");
ultimateChallenge.Click();
Sleep(500, _ct);
return false;
}
// 检测常规挑战模式,点击右侧打开难度选择菜单
var normalChallenge = page.GetByText("常规挑战").WithRoi(r => r.CutLeftTop(0.5, 0.2)).FindAll().FirstOrDefault();
if (normalChallenge != null)
{
Logger.LogInformation($"{Name}:检测到常规挑战,点击打开难度菜单");
// 点击常规挑战右侧 400 像素处打开难度菜单
page.Click(normalChallenge.X + normalChallenge.Width + 400, normalChallenge.Y + normalChallenge.Height / 2);
Sleep(500, _ct);
// 在难度菜单中查找并点击"困难"
var hardMode = page.GetByText("困难").FindAll().FirstOrDefault();
if (hardMode != null)
{
Logger.LogInformation($"{Name}:点击困难模式");
hardMode.Click();
Sleep(300, _ct);
}
return false;
}
Sleep(300, _ct);
return false;
}, _ct, 10, 500);
if (found)
{
Logger.LogInformation($"{Name}:确认困难模式");
}
else
{
Logger.LogWarning("切换困难模式失败,继续执行");
}
}
private void SelectBoss(BvPage page)
{
Logger.LogInformation($"{Name}选择BOSS编号{{Text}}", _taskParam.BossNum);
var bossPositions = new Dictionary<int, (int x, int y)>
{
{ 1, (196, 346) },
{ 2, (237, 541) },
{ 3, (203, 728) }
};
if (!bossPositions.TryGetValue(_taskParam.BossNum, out var pos))
{
pos = bossPositions[1];
}
// 直接点击选择BOSS无需重试或等待
page.Click(pos.x, pos.y);
}
private Task StartFight(CombatScenes combatScenes, List<CombatCommand> combatCommands)
{
CancellationTokenSource cts = new();
_ct.Register(cts.Cancel);
combatScenes.BeforeTask(cts.Token);
var combatTask = new Task(() =>
{
try
{
AutoFightTask.FightStatusFlag = true;
while (!cts.Token.IsCancellationRequested)
{
for (var i = 0; i < combatCommands.Count; i++)
{
var command = combatCommands[i];
var lastCommand = i == 0 ? command : combatCommands[i - 1];
command.Execute(combatScenes, lastCommand);
}
}
}
catch (NormalEndException e)
{
Logger.LogInformation("战斗操作中断:{Msg}", e.Message);
}
catch (Exception e)
{
Logger.LogWarning(e.Message);
throw;
}
finally
{
Logger.LogInformation("自动战斗线程结束");
Simulation.ReleaseAllKey();
Simulation.SendInput.Mouse.LeftButtonUp();
AutoFightTask.FightStatusFlag = false;
}
}, cts.Token);
var domainEndTask = DomainEndDetectionTask(cts);
combatTask.Start();
domainEndTask.Start();
return Task.WhenAll(combatTask, domainEndTask);
}
private Task DomainEndDetectionTask(CancellationTokenSource cts)
{
return new Task(async void () =>
{
try
{
var captureRect = TaskContext.Instance().SystemInfo.ScaleMax1080PCaptureRect;
var assetScale = TaskContext.Instance().SystemInfo.AssetScale;
RecognitionObject whiteCancelRo = new RecognitionObject
{
Name = "BtnWhiteCancel",
RecognitionType = RecognitionTypes.TemplateMatch,
TemplateImageMat = ElementAssets.Instance.BtnWhiteCancel.TemplateImageMat,
RegionOfInterest = new Rect(captureRect.Width / 3, captureRect.Height - (int)(captureRect.Height * 0.22), captureRect.Width / 3, (int)(captureRect.Height * 0.22)),
Use3Channels = true
}.InitTemplate();
await NewRetry.WaitForAction(() =>
{
using var ra = CaptureToRectArea();
using var ret = ra.Find(whiteCancelRo);
if (ret.IsExist())
{
var list = ra.FindMulti(RecognitionObject.Ocr(ret.X + 40 * assetScale, ret.Y - 20 * assetScale, 270 * assetScale, ret.Height * 2));
if (list.Any(o => o.Text.Contains("返回")))
{
return true;
}
}
return false;
}, cts.Token, 300, 1000);
Logger.LogInformation("检测到战斗结束,结束战斗操作线程");
await cts.CancelAsync();
}
catch (Exception e)
{
Logger.LogInformation("对局结束检测线程异常结束:{Msg}", e.Message);
Logger.LogDebug(e, "对局结束检测线程异常结束");
}
}, cts.Token);
}
private async Task SwitchTeam(BvPage page)
{
var fightTeamName = _taskParam.FightTeamName;
if (string.IsNullOrEmpty(fightTeamName))
{
Logger.LogInformation($"{Name}:不更换战斗队伍");
return;
}
Logger.LogInformation($"{Name}:配置战斗队伍为:{fightTeamName}");
await OpenTeamPanelLoop(page);
await FindAndSelectTeamLoop(page, fightTeamName);
}
private async Task OpenTeamPanelLoop(BvPage page)
{
var found = await NewRetry.WaitForAction(() =>
{
if (page.GetByText("预设队伍").WithRoi(r => r.CutLeftTop(0.15, 0.075)).IsExist())
{
return true;
}
var teamButton = page.GetByText("预设队伍").WithRoi(r => r.CutRightBottom(0.3, 0.1)).FindAll().FirstOrDefault();
teamButton?.Click();
Sleep(300, _ct);
return false;
}, _ct);
if (found)
{
Logger.LogInformation($"{Name}:预设队伍面板已打开");
}
else
{
Logger.LogWarning("未找到预设队伍按钮,不执行切换操作");
}
}
private async Task FindAndSelectTeamLoop(BvPage page, string fightTeamName)
{
page.Click(936, 150);
await Delay(100, _ct);
Simulation.SendInput.Mouse.LeftButtonDown();
await Delay(100, _ct);
GameCaptureRegion.GameRegion1080PPosMove(936, 140);
await Delay(100, _ct);
int yOffset = 0;
const int maxRetries = 30;
const int scrollStep = 100;
try
{
for (int retries = 0; retries < maxRetries; retries++)
{
var teamRegionList = page.GetByText(fightTeamName).WithRoi(r => r.CutLeft(0.18)).FindAll();
var foundTeam = teamRegionList.FirstOrDefault();
if (foundTeam != null)
{
Simulation.SendInput.Mouse.LeftButtonUp();
await Delay(200, _ct);
for (int j = 0; j < 5; j++)
{
foundTeam.Click();
await Delay(200, _ct);
}
Logger.LogInformation($"{Name}:已选择队伍 {fightTeamName}");
return;
}
yOffset += scrollStep;
if (130 + yOffset > 1080)
{
Logger.LogWarning("未找到预设战斗队伍名称:{TeamName},保持原有队伍", fightTeamName);
break;
}
GameCaptureRegion.GameRegion1080PPosMove(936, 130 + yOffset);
await Delay(200, _ct);
}
}
finally
{
Simulation.SendInput.Mouse.LeftButtonUp();
await Delay(100, _ct);
}
Simulation.SendInput.SimulateAction(GIActions.OpenPaimonMenu);
await Delay(300, _ct);
}
private async Task ExitDomain(BvPage page)
{
await OpenExitMenuAndClickLoop(page);
await WaitExitCompleteLoop(page);
}
private async Task OpenExitMenuAndClickLoop(BvPage page)
{
var found = await NewRetry.WaitForElementAppear(
ElementAssets.Instance.BtnExitDoor.Value,
() => Simulation.SendInput.SimulateAction(GIActions.OpenPaimonMenu),
_ct);
if (found)
{
await page.Locator(ElementAssets.Instance.BtnExitDoor.Value).Click();
Logger.LogInformation($"{Name}:点击退出秘境");
}
else
{
Logger.LogWarning("未能找到退出秘境按钮,可能已经退出秘境");
}
}
private async Task WaitExitCompleteLoop(BvPage page)
{
var found = await Bv.WaitUntilFound(ElementAssets.Instance.PaimonMenuRo, _ct, 200, 300);
if (found)
{
Logger.LogInformation($"{Name}:退出秘境完成");
await Delay(1000, _ct);
}
}
private async Task ArtifactSalvage()
{
if (!_taskParam.AutoArtifactSalvage)
{
return;
}
if (!int.TryParse(TaskContext.Instance().Config.AutoArtifactSalvageConfig.MaxArtifactStar, out var star))
{
star = 4;
}
await new AutoArtifactSalvageTask(new AutoArtifactSalvageTaskParam(star, javaScript: null, artifactSetFilter: null, maxNumToCheck: null, recognitionFailurePolicy: null)).Start(_ct);
}
#endregion
}