825 lines
27 KiB
JavaScript
825 lines
27 KiB
JavaScript
/**
|
||
* 原神地脉花自动化脚本 (Genshin Impact Ley Line Outcrop Automation Script)
|
||
*
|
||
* 功能:自动寻找并完成地脉花挑战,领取奖励
|
||
*/
|
||
|
||
// 全局变量
|
||
let leyLineX = 0; // 地脉花X坐标
|
||
let leyLineY = 0; // 地脉花Y坐标
|
||
let currentFlower = null; // 当前花的引用
|
||
let strategyName = ""; // 任务策略名称
|
||
let retryCount = 0; // 重试次数
|
||
let marksStatus = true; // 自定义标记状态
|
||
let currentRunTimes = 0; // 当前运行次数
|
||
let isNotification = false; // 是否发送通知
|
||
let config = {}; // 全局配置对象
|
||
const ocrRegion1 = { x: 800, y: 200, width: 300, height: 100 }; // 中心区域
|
||
const ocrRegion2 = { x: 0, y: 200, width: 300, height: 300 }; // 追踪任务区域
|
||
const ocrRegion3 = { x: 1200, y: 520, width: 300, height: 300 }; // 拾取区域
|
||
|
||
// 预定义识别对象
|
||
const openRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/icon/open.png"));
|
||
const closeRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/icon/close.png"));
|
||
const paimonMenuRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/icon/paimon_menu.png"), 0, 0, genshin.width / 3.0, genshin.width / 5.0);
|
||
const boxIconRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/icon/box.png"));
|
||
const ocrRo1 = RecognitionObject.ocr(ocrRegion1.x, ocrRegion1.y, ocrRegion1.width, ocrRegion1.height);
|
||
const ocrRo2 = RecognitionObject.ocr(ocrRegion2.x, ocrRegion2.y, ocrRegion2.width, ocrRegion2.height);
|
||
const ocrRo3 = RecognitionObject.ocr(ocrRegion3.x, ocrRegion3.y, ocrRegion3.width, ocrRegion3.height);
|
||
const ocrRoThis = RecognitionObject.ocrThis;
|
||
/**
|
||
* 主函数 - 脚本入口点
|
||
*/
|
||
(async function () {
|
||
dispatcher.addTimer(new RealtimeTimer("AutoPick"));
|
||
try {
|
||
await runLeyLineOutcropScript();
|
||
} catch (error) {
|
||
log.error("出错了! {error}", error.message);
|
||
if (isNotification) {
|
||
notification.error("出错了! {error}", error.message);
|
||
}
|
||
if (!marksStatus) {
|
||
await openCustomMarks();
|
||
}
|
||
}
|
||
})();
|
||
|
||
/**
|
||
* 运行地脉花脚本的主要逻辑
|
||
* @returns {Promise<void>}
|
||
*/
|
||
async function runLeyLineOutcropScript() {
|
||
// 初始化加载配置和设置并校验
|
||
await initialize();
|
||
await loadConfig();
|
||
loadSettings();
|
||
retryCount = 0;
|
||
|
||
await prepareForLeyLineRun();
|
||
|
||
// 执行地脉花挑战
|
||
await runLeyLineChallenges();
|
||
|
||
// 完成后恢复自定义标记
|
||
if (!marksStatus) {
|
||
await openCustomMarks();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 初始化
|
||
* @returns {Promise<void>}
|
||
*/
|
||
async function initialize() {
|
||
await genshin.returnMainUi();
|
||
setGameMetrics(1920, 1080, 1);
|
||
try {
|
||
const utils = [
|
||
"attemptReward.js",
|
||
"breadthFirstPathSearch.js",
|
||
"executePathsUsingNodeData.js",
|
||
"findLeyLineOutcrop.js",
|
||
"loadSettings.js",
|
||
"locateLeyLineOutcrop.js",
|
||
"processLeyLineOutcrop.js",
|
||
"recognizeTextInRegion.js"
|
||
];
|
||
for (const fileName of utils) {
|
||
eval(file.readTextSync(`utils/${fileName}`));
|
||
log.debug(`utils/${fileName} 加载成功`);
|
||
}
|
||
} catch (error) {
|
||
throw new Error(`JS文件缺失,请重新安装脚本! ${error.message}`);
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* 执行地脉花挑战前的准备工作
|
||
* @returns {Promise<void>}
|
||
*/
|
||
async function prepareForLeyLineRun() {
|
||
// 开局传送到七天神像
|
||
await genshin.tpToStatueOfTheSeven();
|
||
|
||
// 切换战斗队伍
|
||
if (settings.team) {
|
||
log.info(`切换至队伍 ${settings.team}`);
|
||
await genshin.switchParty(settings.team);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 执行地脉花挑战的主要逻辑
|
||
* @returns {Promise<void>}
|
||
*/
|
||
async function runLeyLineChallenges() {
|
||
while (currentRunTimes < settings.timesValue) {
|
||
// 寻找地脉花位置
|
||
await findLeyLineOutcrop(settings.country, settings.leyLineOutcropType);
|
||
|
||
// 查找并执行对应的策略
|
||
const foundStrategy = await executeMatchingStrategy();
|
||
|
||
// 未找到策略的错误处理
|
||
if (!foundStrategy) {
|
||
handleNoStrategyFound();
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 切换指定的队伍
|
||
* @param {string} teamName - 队伍名称
|
||
* @returns {Promise<void>}
|
||
*/
|
||
async function switchTeam(teamName) {
|
||
try {
|
||
return await genshin.switchParty(teamName);
|
||
} catch (error) {
|
||
log.error(`切换队伍时出错: ${error.message}`);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 执行匹配的地脉花策略
|
||
* @returns {Promise<boolean>} 是否找到并执行了策略
|
||
*/
|
||
async function executeMatchingStrategy() {
|
||
let foundStrategy = false;
|
||
|
||
// 从配置中查找匹配的位置和策略
|
||
if (config.leyLinePositions[settings.country]) {
|
||
const positions = config.leyLinePositions[settings.country];
|
||
|
||
for (const position of positions) {
|
||
if (isNearPosition(leyLineX, leyLineY, position.x, position.y, config.errorThreshold)) {
|
||
foundStrategy = true;
|
||
strategyName = position.strategy;
|
||
order = position.order;
|
||
log.info(`找到匹配的地脉花策略:${strategyName},次序:${order}`);
|
||
|
||
// 使用 LeyLineOutcropData.json 数据处理路径
|
||
await executePathsUsingNodeData(position);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
return foundStrategy;
|
||
}
|
||
|
||
/**
|
||
* 加载节点数据
|
||
* @returns {Promise<Object>} 节点数据对象
|
||
*/
|
||
async function loadNodeData() {
|
||
try {
|
||
const nodeDataText = await file.readText("LeyLineOutcropData.json");
|
||
const rawData = JSON.parse(nodeDataText);
|
||
|
||
// 适配数据结构:将原始数据转换为代码期望的格式
|
||
return adaptNodeData(rawData);
|
||
} catch (error) {
|
||
log.error(`加载节点数据失败: ${error.message}`);
|
||
throw new Error("无法加载 LeyLineOutcropData.json 文件");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 适配数据结构:将原始数据转换为代码期望的格式
|
||
* @param {Object} rawData - 原始JSON数据
|
||
* @returns {Object} 适配后的节点数据
|
||
*/
|
||
function adaptNodeData(rawData) {
|
||
const adaptedData = {
|
||
node: [],
|
||
indexes: rawData.indexes
|
||
};
|
||
|
||
// 添加传送点,设置type为"teleport"
|
||
if (rawData.teleports) {
|
||
for (const teleport of rawData.teleports) {
|
||
adaptedData.node.push({
|
||
...teleport,
|
||
type: "teleport",
|
||
next: [],
|
||
prev: []
|
||
});
|
||
}
|
||
}
|
||
|
||
// 添加地脉花节点,设置type为"blossom"
|
||
if (rawData.blossoms) {
|
||
for (const blossom of rawData.blossoms) {
|
||
adaptedData.node.push({
|
||
...blossom,
|
||
type: "blossom",
|
||
next: [],
|
||
prev: []
|
||
});
|
||
}
|
||
}
|
||
|
||
// 根据edges构建next和prev关系
|
||
if (rawData.edges) {
|
||
for (const edge of rawData.edges) {
|
||
const sourceNode = adaptedData.node.find(node => node.id === edge.source);
|
||
const targetNode = adaptedData.node.find(node => node.id === edge.target);
|
||
|
||
if (sourceNode && targetNode) {
|
||
sourceNode.next.push({
|
||
target: edge.target,
|
||
route: edge.route
|
||
});
|
||
targetNode.prev.push(edge.source);
|
||
}
|
||
}
|
||
}
|
||
|
||
log.debug(`适配数据完成:传送点 ${rawData.teleports ? rawData.teleports.length : 0} 个,地脉花 ${rawData.blossoms ? rawData.blossoms.length : 0} 个,边缘 ${rawData.edges ? rawData.edges.length : 0} 个`);
|
||
|
||
return adaptedData;
|
||
}
|
||
|
||
/**
|
||
* 根据位置找到对应的目标节点
|
||
* @param {Object} nodeData - 节点数据
|
||
* @param {number} x - 目标X坐标
|
||
* @param {number} y - 目标Y坐标
|
||
* @returns {Object|null} 找到的节点或null
|
||
*/
|
||
function findTargetNodeByPosition(nodeData, x, y) {
|
||
const errorThreshold = 50; // 坐标匹配误差范围
|
||
|
||
for (const node of nodeData.node) {
|
||
if (node.type === "blossom" &&
|
||
Math.abs(node.position.x - x) <= errorThreshold &&
|
||
Math.abs(node.position.y - y) <= errorThreshold) {
|
||
return node;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 查找到达目标节点的所有可能路径
|
||
* @param {Object} nodeData - 节点数据
|
||
* @param {Object} targetNode - 目标节点
|
||
* @returns {Array} 可行路径数组
|
||
*/
|
||
function findPathsToTarget(nodeData, targetNode) {
|
||
// 构建节点映射表
|
||
const nodeMap = {};
|
||
nodeData.node.forEach(node => {
|
||
nodeMap[node.id] = node;
|
||
});
|
||
|
||
log.info(`目标节点ID: ${targetNode.id}, 类型: ${targetNode.type}, 区域: ${targetNode.region}`);
|
||
|
||
// 采用广度优先搜索查找所有可能路径
|
||
return breadthFirstPathSearch(nodeData, targetNode, nodeMap);
|
||
}
|
||
|
||
/**
|
||
* 如果需要,尝试查找反向路径(从目标节点的前置节点到传送点再到目标)
|
||
* @param {Object} nodeData - 节点数据
|
||
* @param {Object} targetNode - 目标节点
|
||
* @param {Object} nodeMap - 节点映射
|
||
* @param {Array} existingPaths - 已找到的路径
|
||
* @returns {Array} 找到的反向路径
|
||
*/
|
||
function findReversePathsIfNeeded(nodeData, targetNode, nodeMap, existingPaths) {
|
||
// 如果已经找到路径,或者目标节点没有前置节点,则不需要查找反向路径
|
||
if (existingPaths.length > 0 || !targetNode.prev || targetNode.prev.length === 0) {
|
||
return [];
|
||
}
|
||
|
||
const reversePaths = [];
|
||
|
||
// 检查每个前置节点
|
||
for (const prevNodeId of targetNode.prev) {
|
||
const prevNode = nodeMap[prevNodeId];
|
||
if (!prevNode) continue;
|
||
|
||
// 找到从前置节点到传送点的路径
|
||
const pathsToPrevNode = [];
|
||
|
||
// 获取所有能从这个前置节点到达的传送点
|
||
const teleportNodes = nodeData.node.filter(node =>
|
||
node.type === "teleport" && node.next.some(route => route.target === prevNode.id)
|
||
);
|
||
|
||
for (const teleportNode of teleportNodes) {
|
||
// 寻找传送点到前置节点的路径
|
||
const route = teleportNode.next.find(r => r.target === prevNode.id);
|
||
if (route) {
|
||
// 找到路径从前置节点到目标
|
||
const nextRoute = prevNode.next.find(r => r.target === targetNode.id);
|
||
if (nextRoute) {
|
||
reversePaths.push({
|
||
startNode: teleportNode,
|
||
targetNode: targetNode,
|
||
routes: [route.route, nextRoute.route]
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return reversePaths;
|
||
}
|
||
|
||
/**
|
||
* 从多个可行路径中选择最优的一条
|
||
* @param {Array} paths - 路径数组
|
||
* @returns {Object} 最优路径
|
||
*/
|
||
function selectOptimalPath(paths) {
|
||
if (!paths || paths.length === 0) {
|
||
throw new Error("没有可用路径");
|
||
}
|
||
|
||
// 按路径段数从少到多排序
|
||
paths.sort((a, b) => a.routes.length - b.routes.length);
|
||
|
||
// 记录路径选择日志
|
||
for (let i = 0; i < Math.min(paths.length, 3); i++) {
|
||
log.debug(`路径选项 ${i + 1}: 起点ID ${paths[i].startNode.id}, ${paths[i].routes.length} 段路径`);
|
||
for (let j = 0; j < paths[i].routes.length; j++) {
|
||
log.debug(` - 路径 ${j + 1}: ${paths[i].routes[j]}`);
|
||
}
|
||
}
|
||
|
||
return paths[0]; // 返回路径段最少的路径
|
||
}
|
||
|
||
/**
|
||
* 执行路径
|
||
* @param {Object} path - 路径对象
|
||
* @returns {Promise<void>}
|
||
*/
|
||
async function executePath(path) {
|
||
log.info(`开始执行路径,起始点ID: ${path.startNode.id}, 目标点ID: ${path.targetNode.id}`);
|
||
log.info(`路径包含 ${path.routes.length} 个路径段`);
|
||
|
||
// 依次执行每个路径段
|
||
for (let i = 0; i < path.routes.length; i++) {
|
||
const routePath = path.routes[i];
|
||
log.info(`执行路径 ${i + 1}/${path.routes.length}: ${routePath}`);
|
||
|
||
try {
|
||
// 运行路径文件
|
||
await pathingScript.runFile(routePath);
|
||
} catch (error) {
|
||
log.error(`执行路径 ${i + 1} 时出错: ${error.message}`);
|
||
throw error;
|
||
}
|
||
}
|
||
const routePath = path.routes[path.routes.length - 1];
|
||
const targetPath = routePath.replace('assets/pathing/', 'assets/pathing/target/').replace('-rerun', '');
|
||
await processLeyLineOutcrop(settings.timeout, targetPath);
|
||
await attemptReward();
|
||
}
|
||
|
||
/**
|
||
* 如果需要,切换到好感队
|
||
* @returns {Promise<void>}
|
||
*/
|
||
async function switchToFriendshipTeamIfNeeded() {
|
||
if (!settings.friendshipTeam) {
|
||
return;
|
||
}
|
||
|
||
log.info(`切换至队伍 ${settings.friendshipTeam}`);
|
||
|
||
try {
|
||
await genshin.switchParty(settings.friendshipTeam);
|
||
} catch (error) {
|
||
// 切换失败时的恢复策略
|
||
keyPress("ESCAPE");
|
||
await sleep(500);
|
||
keyPress("ESCAPE");
|
||
await sleep(500);
|
||
await genshin.returnMainUi();
|
||
log.info(`再次切换至队伍 ${settings.friendshipTeam}`);
|
||
try {
|
||
await genshin.switchParty(settings.friendshipTeam);
|
||
} catch (error) {
|
||
// 如果切换队伍失败,记录日志并继续执行
|
||
log.warn(`切换队伍失败: ${error.message}`);
|
||
log.warn("跳过切换队伍,直接领取奖励");
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理未找到策略的情况
|
||
*/
|
||
async function handleNoStrategyFound() {
|
||
log.error("未找到对应的地脉花策略,请再次运行脚本");
|
||
log.error("如果仍然不行,请截图{1}游戏界面,并反馈给作者!", "*完整的*");
|
||
log.error("完整的游戏界面!完整的游戏界面!完整的游戏界面!");
|
||
if (isNotification) {
|
||
notification.error("未找到对应的地脉花策略");
|
||
await genshin.returnMainUi();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 加载配置文件
|
||
* @returns {Promise<void>}
|
||
*/
|
||
async function loadConfig() {
|
||
try {
|
||
const configData = JSON.parse(await file.readText("config.json"));
|
||
config = configData; // 直接赋值给全局变量
|
||
} catch (error) {
|
||
log.error(`加载配置文件失败: ${error.message}`);
|
||
throw new Error("配置文件加载失败,请检查config.json文件是否存在");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 地脉花寻找和定位相关函数
|
||
*/
|
||
|
||
/**
|
||
* 判断坐标是否在指定位置附近(误差范围内)
|
||
* @param {number} x - 当前X坐标
|
||
* @param {number} y - 当前Y坐标
|
||
* @param {number} targetX - 目标X坐标
|
||
* @param {number} targetY - 目标Y坐标
|
||
* @param {number} threshold - 误差阈值
|
||
* @returns {boolean} 是否在指定范围内
|
||
*/
|
||
function isNearPosition(x, y, targetX, targetY, threshold) {
|
||
// 使用配置中的阈值或默认值50
|
||
const errorThreshold = threshold || 50;
|
||
return Math.abs(x - targetX) <= errorThreshold && Math.abs(y - targetY) <= errorThreshold;
|
||
}
|
||
|
||
/**
|
||
* 计算两点之间的二维欧几里得距离
|
||
* @param {number} x1 - 第一个点的X坐标
|
||
* @param {number} y1 - 第一个点的Y坐标
|
||
* @param {number} x2 - 第二个点的X坐标
|
||
* @param {number} y2 - 第二个点的Y坐标
|
||
* @returns {number} 两点之间的距离
|
||
*/
|
||
function calculate2DDistance(x1, y1, x2, y2) {
|
||
return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
|
||
}
|
||
|
||
/**
|
||
* 奖励和战斗相关函数
|
||
*/
|
||
|
||
/**
|
||
* 打开地脉花
|
||
* @param {string} targetPath - 目标路径
|
||
* @returns {Promise<boolean>} 区域是否出现地脉任务
|
||
*/
|
||
async function openOutcrop(targetPath) {
|
||
let startTime = Date.now();
|
||
let recognized = false;
|
||
|
||
keyPress("F");
|
||
|
||
while (Date.now() - startTime < 5000) {
|
||
captureRegion = captureGameRegion();
|
||
if (recognizeFightText(captureRegion)) {
|
||
recognized = true;
|
||
break;
|
||
}
|
||
keyPress("F");
|
||
await sleep(500);
|
||
}
|
||
|
||
// 返回识别结果
|
||
return recognized;
|
||
}
|
||
|
||
/**
|
||
* 识别地脉开启进入战斗文本
|
||
* @returns {Promise<boolean>} 区域是否出现战斗文本
|
||
*/
|
||
function recognizeFightText(captureRegion) {
|
||
try {
|
||
let result = captureRegion.find(ocrRo2);
|
||
let text = result.text;
|
||
keywords = ["打倒", "所有", "敌人"];
|
||
for (let keyword of keywords) {
|
||
if (text.includes(keyword)) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
} catch (error) {
|
||
log.error("OCR过程中出错: {0}", error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 自动异步战斗
|
||
* @param {number} timeout - 超时时间
|
||
* @returns {Promise<boolean>} 战斗是否成功
|
||
*/
|
||
async function autoFight(timeout) {
|
||
const cts = new CancellationTokenSource();
|
||
log.info("开始战斗");
|
||
dispatcher.RunTask(new SoloTask("AutoFight"), cts);
|
||
let fightResult = await recognizeTextInRegion(timeout);
|
||
logFightResult = fightResult ? "成功" : "失败";
|
||
log.info(`战斗结束,战斗结果:${logFightResult}`);
|
||
cts.cancel();
|
||
return fightResult;
|
||
}
|
||
|
||
// 地脉花奖励相关函数
|
||
/**
|
||
* 自动导航到地脉花奖励点
|
||
* @returns {Promise<void>}
|
||
*/
|
||
async function autoNavigateToReward() {
|
||
// 定义识别对象
|
||
const cts = new CancellationTokenSource();
|
||
const MAX_RETRY = 3; // 最大重试次数
|
||
let retryCount = 0;
|
||
|
||
try {
|
||
// 调整初始视角为俯视角
|
||
log.info("调整视角...");
|
||
middleButtonClick();
|
||
await sleep(300);
|
||
|
||
while (retryCount < MAX_RETRY) {
|
||
try {
|
||
log.info(`开始自动导航到地脉花...(尝试 ${retryCount + 1}/${MAX_RETRY})`);
|
||
let rewardDetectionPromise = startRewardTextDetection(cts);
|
||
|
||
// 启动导航任务,添加超时参数
|
||
await Promise.race([
|
||
navigateTowardReward(60000, cts.token), // 设置60秒超时
|
||
rewardDetectionPromise
|
||
]);
|
||
|
||
// 取消导航任务
|
||
cts.cancel();
|
||
keyUp("w"); // 确保停止前进
|
||
log.info("已到达领奖点");
|
||
return; // 成功完成
|
||
} catch (error) {
|
||
retryCount++;
|
||
cts.cancel(); // 确保取消旧的令牌
|
||
keyUp("w"); // 确保停止前进
|
||
|
||
if (error.message === '前进时间超时') {
|
||
log.warn(`导航超时,正在重试 (${retryCount}/${MAX_RETRY})`);
|
||
|
||
// 尝试进行恢复操作
|
||
keyPress("x"); // 尝试重置视角
|
||
await sleep(500);
|
||
keyDown("s");
|
||
await sleep(1000);
|
||
keyUp("s");
|
||
await sleep(500);
|
||
|
||
// 创建新的令牌
|
||
cts = new CancellationTokenSource();
|
||
} else {
|
||
// 对于其他错误,直接抛出
|
||
throw error;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果达到最大重试次数仍然失败
|
||
throw new Error(`导航到地脉花失败,已尝试 ${MAX_RETRY} 次`);
|
||
} catch (error) {
|
||
// 确保清理
|
||
cts.cancel();
|
||
keyUp("w");
|
||
log.error(`导航过程中出错: ${error}`);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 监测文字区域,检测到地脉之花文字时返回
|
||
* @param {CancellationTokenSource} cts - 取消令牌源
|
||
* @returns {Promise<boolean>} - 是否检测到文字
|
||
*/
|
||
async function startRewardTextDetection(cts) {
|
||
return new Promise((resolve, reject) => {
|
||
(async () => {
|
||
try {
|
||
while (!cts.token.isCancellationRequested) {
|
||
// 首先检查异常界面
|
||
let captureRegion = captureGameRegion();
|
||
|
||
// 检查是否误触发其他页面
|
||
if (captureRegion.Find(paimonMenuRo).IsEmpty()) {
|
||
log.debug("误触发其他页面,尝试关闭页面");
|
||
await genshin.returnMainUi();
|
||
await sleep(300);
|
||
continue;
|
||
}
|
||
|
||
// 检查是否已经到达领奖界面
|
||
let resList = captureRegion.findMulti(ocrRoThis); // 使用预定义的ocrRoThis对象
|
||
if (resList && resList.count > 0) {
|
||
for (let i = 0; i < resList.count; i++) {
|
||
if (resList[i].text.includes("原粹树脂")) {
|
||
log.debug("已到达领取页面,可以领奖");
|
||
resolve(true);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
let ocrResults = captureRegion.findMulti(ocrRo3);
|
||
|
||
if (ocrResults && ocrResults.count > 0) {
|
||
for (let i = 0; i < ocrResults.count; i++) {
|
||
if (ocrResults[i].text.includes("接触") ||
|
||
ocrResults[i].text.includes("地脉") ||
|
||
ocrResults[i].text.includes("之花")) {
|
||
log.debug("检测到文字: " + ocrResults[i].text);
|
||
resolve(true);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
await sleep(200);
|
||
}
|
||
} catch (error) {
|
||
reject(error);
|
||
}
|
||
})();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 导航向奖励点
|
||
* @param {number} timeout - 超时时间
|
||
* @param {CancellationToken} token - 取消令牌
|
||
* @returns {Promise<void>}
|
||
*/
|
||
async function navigateTowardReward(timeout, token) {
|
||
let navigateStartTime = Date.now();
|
||
try {
|
||
while (!token.isCancellationRequested) {
|
||
if (await adjustViewForReward(boxIconRo, token)) {
|
||
keyDown("w");
|
||
await sleep(300);
|
||
} else if (!token.isCancellationRequested) { // 如果没有取消,则继续尝试调整
|
||
keyPress("x");
|
||
keyUp("w");
|
||
keyDown("s");
|
||
await sleep(1000);
|
||
keyUp("s");
|
||
keyDown("w");
|
||
}
|
||
|
||
if (Date.now() - navigateStartTime > timeout) {
|
||
keyUp("w");
|
||
throw new Error('前进时间超时');
|
||
}
|
||
|
||
// 增加短暂延迟以避免过于频繁的检测
|
||
await sleep(100);
|
||
}
|
||
} catch (error) {
|
||
keyUp("w"); // 确保释放按键
|
||
throw error;
|
||
} finally {
|
||
keyUp("w"); // 确保释放按键
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 调整视野直到图标位于正前方
|
||
* @param {Object} boxIconRo - 宝箱图标识别对象
|
||
* @param {CancellationToken} token - 取消令牌
|
||
* @returns {Promise<boolean>}
|
||
*/
|
||
async function adjustViewForReward(boxIconRo, token) {
|
||
const screenCenterX = 960;
|
||
const screenCenterY = 540;
|
||
const maxAngle = 10; // 最大允许偏离角度(度)
|
||
for (let i = 0; i < 20; i++) {
|
||
// 检查是否取消操作
|
||
if (token && token.isCancellationRequested) {
|
||
log.info("视角调整已取消");
|
||
return false;
|
||
}
|
||
|
||
let captureRegion = captureGameRegion();
|
||
let iconRes = captureRegion.Find(boxIconRo);
|
||
|
||
if (!iconRes.isExist()) {
|
||
log.warn("未找到图标,等待一下");
|
||
await sleep(1000);
|
||
continue; // 没有找到图标等一秒再继续
|
||
// throw new Error('未找到图标,没有地脉花');
|
||
}
|
||
|
||
// 计算图标相对于屏幕中心的位置
|
||
const xOffset = iconRes.x - screenCenterX;
|
||
const yOffset = screenCenterY - iconRes.y; // 注意:y坐标向下增加,所以翻转差值
|
||
|
||
// 计算图标与中心垂直线的角度
|
||
const angleInRadians = Math.atan2(Math.abs(xOffset), yOffset);
|
||
const angleInDegrees = angleInRadians * (180 / Math.PI);
|
||
|
||
// 检查图标是否在中心上方,且角度在允许范围内
|
||
const isAboveCenter = iconRes.y < screenCenterY;
|
||
const isWithinAngle = angleInDegrees <= maxAngle;
|
||
|
||
log.debug(`图标位置: (${iconRes.x}, ${iconRes.y}), 角度: ${angleInDegrees.toFixed(2)}°`);
|
||
|
||
if (isAboveCenter && isWithinAngle) {
|
||
log.debug(`视野调整成功,图标角度: ${angleInDegrees.toFixed(2)}°,在${maxAngle}°范围内`);
|
||
return true;
|
||
} else {
|
||
keyUp("w"); // 确保停止前进
|
||
// 调整视野方向,根据图标位置调整鼠标移动
|
||
moveMouseBy(xOffset > 0 ? Math.min(xOffset, 300) : Math.max(xOffset, -300), 0);
|
||
await sleep(100);
|
||
|
||
if (!isAboveCenter) {
|
||
log.warn("图标不在屏幕中心上方");
|
||
// 尝试将视角向下调整
|
||
moveMouseBy(0, 500);
|
||
await sleep(100);
|
||
} else if (!isWithinAngle) {
|
||
log.warn(`图标角度${angleInDegrees.toFixed(2)}°不在范围内`);
|
||
}
|
||
}
|
||
}
|
||
|
||
log.warn("调整视野20次后仍未成功");
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 地图标记相关函数
|
||
*/
|
||
|
||
/**
|
||
* 关闭自定义标记
|
||
* @returns {Promise<void>}
|
||
*/
|
||
async function closeCustomMarks() {
|
||
await genshin.returnMainUi();
|
||
keyPress("M");
|
||
await sleep(1000);
|
||
click(60, 1020);
|
||
await sleep(600);
|
||
|
||
let button = captureGameRegion().find(openRo);
|
||
if (button.isExist()) {
|
||
marksStatus = false;
|
||
log.info("关闭自定义标记");
|
||
click(Math.round(button.x + button.width / 2), Math.round(button.y + button.height / 2));
|
||
await sleep(600);
|
||
keyPress("ESCAPE");
|
||
} else {
|
||
log.error("未找到开关按钮");
|
||
keyPress("ESCAPE");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 打开自定义标记
|
||
* @returns {Promise<void>}
|
||
*/
|
||
async function openCustomMarks() {
|
||
await genshin.returnMainUi();
|
||
keyPress("M");
|
||
await sleep(1000);
|
||
click(60, 1020);
|
||
await sleep(600);
|
||
|
||
let button = captureGameRegion().find(closeRo);
|
||
if (button.isExist()) {
|
||
for (let i = 0; i < button.count; i++) {
|
||
let b = button[i];
|
||
if (b.y > 280 && b.y < 350) {
|
||
log.info("打开自定义标记");
|
||
marksStatus = true;
|
||
click(Math.round(b.x + b.width / 2), Math.round(b.y + b.height / 2));
|
||
}
|
||
}
|
||
} else {
|
||
log.error("未找到开关按钮");
|
||
keyPress("ESCAPE");
|
||
}
|
||
}
|