Files
bettergi-scripts-list/repo/js/AutoLeyLineOutcrop/main.js
2025-05-05 09:44:57 +08:00

1274 lines
44 KiB
JavaScript
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.

/**
* 原神地脉花自动化脚本 (Genshin Impact Ley Line Outcrop Automation Script)
*
* 功能:自动寻找并完成地脉花挑战,领取奖励
*
* 术语对照表:
* 中文 - 英文:
* 地脉之花 - Ley Line Outcrop
* 地脉 - Ley Line
* 启示之花 - Blossom of Revelation (蓝花,产出经验书)
* 藏金之花 - Blossom of Wealth (黄花,产出摩拉)
*/
// 全局变量
let leyLineX = 0; // 地脉花X坐标
let leyLineY = 0; // 地脉花Y坐标
let currentFlower = null; // 当前花的引用
let strategyName = ""; // 任务策略名称
let retryCount = 0; // 重试次数
let marksStatus = true; // 自定义标记状态
let currentRunTimes = 0; // 当前运行次数
/**
* 主函数 - 脚本入口点
*/
(async function () {
dispatcher.addTimer(new RealtimeTimer("AutoPick"));
try {
await runLeyLineOutcropScript();
} catch (error) {
log.error("出错了! {error}", error.message);
if (!marksStatus) {
await openCustomMarks();
}
}
})();
/**
* 运行地脉花脚本的主要逻辑
* @returns {Promise<void>}
*/
async function runLeyLineOutcropScript() {
// 初始化
await initializeGame();
// 加载配置和设置并校验
const config = await loadConfig();
const settings = loadSettings();
retryCount = 0;
// 显示设置信息
logSettings(settings);
// 开局准备
await prepareForLeyLineRun(settings);
// 执行地脉花挑战
await runLeyLineChallenges(config, settings);
// 完成后恢复自定义标记
if (!marksStatus) {
await openCustomMarks();
}
}
/**
* 初始化游戏状态
* @returns {Promise<void>}
*/
async function initializeGame() {
await genshin.returnMainUi();
setGameMetrics(1920, 1080, 1);
}
/**
* 记录设置信息到日志
* @param {Object} settings - 用户设置对象
*/
function logSettings(settings) {
log.info(`地脉花类型:${settings.leyLineOutcropType}`);
log.info(`国家:${settings.country}`);
if (settings.friendshipTeam) {
log.info(`好感队:${settings.friendshipTeam}`);
}
log.info(`刷取次数:${settings.timesValue}`);
if (settings.reRun) {
log.info("已开启可重跑模式,将选择可重跑路线");
}
}
/**
* 执行地脉花挑战前的准备工作
* @param {Object} settings - 用户设置对象
* @returns {Promise<void>}
*/
async function prepareForLeyLineRun(settings) {
// 开局传送到七天神像
await genshin.tpToStatueOfTheSeven();
// 切换战斗队伍
if (settings.team) {
log.info(`切换至队伍 ${settings.team}`);
await genshin.switchParty(settings.team);
}
}
/**
* 执行地脉花挑战的主要逻辑
* @param {Object} config - 配置对象
* @param {Object} settings - 用户设置对象
* @returns {Promise<void>}
*/
async function runLeyLineChallenges(config, settings) {
while (currentRunTimes < settings.timesValue) {
// 寻找地脉花位置
await findLeyLineOutcrop(settings.country, settings.leyLineOutcropType);
// 查找并执行对应的策略
const foundStrategy = await executeMatchingStrategy(config, settings);
// 未找到策略的错误处理
if (!foundStrategy) {
handleNoStrategyFound();
return;
}
}
}
/**
* 执行匹配的地脉花策略
* @param {Object} config - 配置对象
* @param {Object} settings - 用户设置对象
* @returns {Promise<boolean>} 是否找到并执行了策略
*/
async function executeMatchingStrategy(config, settings) {
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, settings);
break;
}
}
}
return foundStrategy;
}
/**
* 使用节点数据执行路径
* @param {Object} position - 位置对象
* @param {Object} settings - 用户设置对象
* @returns {Promise<void>}
*/
async function executePathsUsingNodeData(position, settings) {
try {
const nodeData = await loadNodeData();
let currentNodePosition = position;
const targetNode = findTargetNodeByPosition(nodeData, currentNodePosition.x, currentNodePosition.y);
if (!targetNode) {
log.error(`未找到与坐标(${currentNodePosition.x}, ${currentNodePosition.y})匹配的目标节点`);
return;
}
log.info(`找到目标节点: ID ${targetNode.id}, 位置(${targetNode.position.x}, ${targetNode.position.y})`);
const paths = findPathsToTarget(nodeData, targetNode);
if (paths.length === 0) {
log.error(`未找到通向目标节点(ID: ${targetNode.id})的路径`);
return;
}
// 选择最短的路径执行
const optimalPath = selectOptimalPath(paths);
log.info(`选择了含有 ${optimalPath.routes.length} 个路径点的最优路径`);
// 执行路径
await executePath(optimalPath, settings);
currentRunTimes++;
// 如果达到刷取次数上限,退出循环
if (currentRunTimes >= settings.timesValue) {
return;
}
// 循环检查并执行当前节点的单一next路径直到遇到没有next或有多个next的情况
let currentNode = targetNode;
while (currentNode.next && currentRunTimes < settings.timesValue) {
if(currentNode.next.length === 1){
// 获取下一个节点的ID 和 路径,并在节点数据中找到下一个节点
const nextNodeId = currentNode.next[0].target;
const nextRoute = currentNode.next[0].route;
const nextNode = nodeData.node.find(node => node.id === nextNodeId);
if (!nextNode) {
return;
}
// 创建一个适合executePath函数的路径对象
const pathObject = {
startNode: currentNode,
targetNode: nextNode,
routes: [nextRoute]
};
log.info(`直接执行下一个节点路径: ${nextRoute}`);
await executePath(pathObject, settings);
currentRunTimes++;
log.info(`完成节点 ID ${nextNodeId}, 已执行 ${currentRunTimes}/${settings.timesValue}`);
// 更新当前节点为下一个节点,继续检查
currentNode = nextNode;
currentNodePosition = { x: nextNode.position.x, y: nextNode.position.y };
}
else if(currentNode.next.length > 1){
// 如果存在分支路线,先打开大地图判断下一个地脉花的位置,根据下一个地脉花的位置选择路线
log.info("检测到多个分支路线,开始查找下一个地脉花位置");
// 备份当前地脉花坐标
const currentLeyLineX = leyLineX;
const currentLeyLineY = leyLineY;
// 打开大地图
await genshin.returnMainUi();
keyPress("M");
await sleep(1000);
// 查找下一个地脉花
const found = await locateLeyLineOutcrop(settings.leyLineOutcropType);
await genshin.returnMainUi();
if (!found) {
log.warn("无法在分支点找到下一个地脉花,退出本次循环");
return;
}
log.info(`找到下一个地脉花,位置: (${leyLineX}, ${leyLineY})`);
// 计算每个分支节点到地脉花的距离,选择最近的路径
let closestRoute = null;
let closestDistance = Infinity;
let closestNodeId = null;
for (const nextRoute of currentNode.next) {
const nextNodeId = nextRoute.target;
const nextNode = nodeData.node.find(node => node.id === nextNodeId);
if (!nextNode) continue;
const distance = calculate2DDistance(
leyLineX, leyLineY,
nextNode.position.x, nextNode.position.y
);
log.info(`路线到地脉花距离: ID ${nextNodeId}, 距离: ${distance.toFixed(2)}`);
if (distance < closestDistance) {
closestDistance = distance;
closestRoute = nextRoute.route;
closestNodeId = nextNodeId;
}
}
if (!closestRoute) {
log.error("无法找到合适的路线,终止执行");
// 恢复原始坐标
leyLineX = currentLeyLineX;
leyLineY = currentLeyLineY;
return;
}
const nextNode = nodeData.node.find(node => node.id === closestNodeId);
log.info(`选择最近的路线: ${closestRoute}, 目标节点ID: ${closestNodeId}`);
// 创建路径对象并执行
const pathObject = {
startNode: currentNode,
targetNode: nextNode,
routes: [closestRoute]
};
await executePath(pathObject, settings);
currentRunTimes++;
// 更新当前节点为下一个节点,继续检查
currentNode = nextNode;
currentNodePosition = { x: nextNode.position.x, y: nextNode.position.y };
}
else{
log.info("当前路线完成,退出循环");
break;
}
}
} catch (error) {
log.error(`执行路径时出错: ${error.message}`);
throw error;
}
}
/**
* 加载节点数据
* @returns {Promise<Object>} 节点数据对象
*/
async function loadNodeData() {
try {
const nodeDataText = await file.readText("LeyLineOutcropData.json");
return JSON.parse(nodeDataText);
} catch (error) {
log.error(`加载节点数据失败: ${error.message}`);
throw new Error("无法加载 LeyLineOutcropData.json 文件");
}
}
/**
* 根据位置找到对应的目标节点
* @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 - 节点映射
* @returns {Array} 找到的所有可行路径
*/
function breadthFirstPathSearch(nodeData, targetNode, nodeMap) {
// 存储找到的所有有效路径
const validPaths = [];
// 获取所有传送点作为起点
const teleportNodes = nodeData.node.filter(node => node.type === "teleport");
log.info(`找到 ${teleportNodes.length} 个传送点作为可能的起点`);
// 对每个传送点,尝试查找到目标的路径
for (const startNode of teleportNodes) {
// 初始化队列,每个元素包含 [当前节点, 路径信息]
const queue = [[startNode, {
startNode: startNode,
routes: [],
visitedNodes: new Set([startNode.id])
}]];
// 广度优先搜索
while (queue.length > 0) {
const [currentNode, pathInfo] = queue.shift();
// 如果已经到达目标节点
if (currentNode.id === targetNode.id) {
validPaths.push({
startNode: pathInfo.startNode,
targetNode: targetNode,
routes: [...pathInfo.routes]
});
continue; // 找到一条路径,继续搜索其他可能路径
}
// 检查当前节点的下一个连接
if (currentNode.next && currentNode.next.length > 0) {
for (const nextRoute of currentNode.next) {
const nextNodeId = nextRoute.target;
// 避免循环
if (pathInfo.visitedNodes.has(nextNodeId)) {
continue;
}
const nextNode = nodeMap[nextNodeId];
if (!nextNode) {
continue;
}
// 创建新的路径信息
const newPathInfo = {
startNode: pathInfo.startNode,
routes: [...pathInfo.routes, nextRoute.route],
visitedNodes: new Set([...pathInfo.visitedNodes, nextNodeId])
};
// 加入队列
queue.push([nextNode, newPathInfo]);
}
}
}
}
// 检查是否存在反向路径
const reversePaths = findReversePathsIfNeeded(nodeData, targetNode, nodeMap, validPaths);
validPaths.push(...reversePaths);
log.info(`共找到 ${validPaths.length} 条有效路径`);
return validPaths;
}
/**
* 如果需要,尝试查找反向路径(从目标节点的前置节点到传送点再到目标)
* @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.info(`路径选项 ${i+1}: 起点ID ${paths[i].startNode.id}, ${paths[i].routes.length} 段路径`);
for (let j = 0; j < paths[i].routes.length; j++) {
log.info(` - 路径 ${j+1}: ${paths[i].routes[j]}`);
}
}
return paths[0]; // 返回路径段最少的路径
}
/**
* 执行路径
* @param {Object} path - 路径对象
* @param {Object} settings - 用户设置对象
* @returns {Promise<void>}
*/
async function executePath(path, settings) {
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', '');
// 处理地脉花
log.info(`处理地脉花: ${targetPath}`);
await processLeyLineOutcrop(settings.timeout, targetPath);
await switchToFriendshipTeamIfNeeded(settings);
await attemptReward(settings);
}
/**
* 如果需要,切换到好感队
* @param {Object} settings - 用户设置对象
* @returns {Promise<void>}
*/
async function switchToFriendshipTeamIfNeeded(settings) {
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}`);
await genshin.switchParty(settings.friendshipTeam);
}
}
/**
* 处理未找到策略的情况
*/
function handleNoStrategyFound() {
log.error("未找到对应的地脉花策略,请再次运行脚本");
log.error("如果仍然不行,请截图{1}游戏界面,并反馈给作者!", "*完整的*");
log.error("完整的游戏界面!完整的游戏界面!完整的游戏界面!");
}
/**
* 配置相关函数
*/
/**
* 加载配置文件
* @returns {Object} 配置对象
*/
async function loadConfig() {
try {
const configData = JSON.parse(await file.readText("config.json"));
return configData;
} catch (error) {
log.error(`加载配置文件失败: ${error.message}`);
throw new Error("配置文件加载失败请检查config.json文件是否存在");
}
}
/**
* 加载并验证用户设置
* @returns {Object} 处理过的设置对象
*/
function loadSettings() {
try {
// 从全局settings加载用户设置
const settingsData = {
start: settings.start,
leyLineOutcropType: settings.leyLineOutcropType,
country: settings.country,
team: settings.team,
reRun: settings.reRun,
friendshipTeam: settings.friendshipTeam,
timeout: settings.timeout * 1000 ? settings.timeout * 1000 : 120000,
count: settings.count ? settings.count : "6"
};
// 验证必要的设置
if (!settingsData.start) {
throw new Error("请仔细阅读脚本介绍,并在调度器内进行配置,如果你是直接运行的脚本,请将脚本加入调度器内运行!");
}
if (!settingsData.leyLineOutcropType) {
throw new Error("请在游戏中确认地脉花的类型然后在js设置中选择地脉花的类型。");
}
if (!settingsData.country) {
throw new Error("请在游戏中确认地脉花的第一个点的位置然后在js设置中选择地脉花所在的国家。");
}
if (settingsData.friendshipTeam && !settingsData.team) {
throw new Error("未配置战斗队伍!当配置了好感队时必须配置战斗队伍!");
}
// 处理刷取次数
if (!/^-?\d+\.?\d*$/.test(settingsData.count)) {
log.warn(`刷取次数 ${settingsData.count} 不是数字使用默认次数6次`);
settingsData.timesValue = 6;
} else {
// 转换为数字
const num = parseFloat(settingsData.count);
// 范围检查
if (num < 1) {
settingsData.timesValue = 1;
log.info(`⚠️ 次数 ${num} 小于1已调整为1`);
} else {
// 处理小数
if (!Number.isInteger(num)) {
settingsData.timesValue = Math.floor(num);
log.info(`⚠️ 次数 ${num} 不是整数,已向下取整为 ${settingsData.timesValue}`);
} else {
settingsData.timesValue = num;
}
}
}
return settingsData;
} catch (error) {
log.error(`加载设置失败: ${error.message}`);
throw error;
}
}
/**
* 地脉花寻找和定位相关函数
*/
/**
* 查找地脉花位置
* @param {string} country - 国家名称
* @param {string} type - 地脉花类型
* @returns {Promise<void>}
*/
async function findLeyLineOutcrop(country, type) {
const config = await loadConfig();
const maxRetries = 6;
log.info("开始寻找地脉花");
if (!config.mapPositions[country] || config.mapPositions[country].length === 0) {
throw new Error(`未找到国家 ${country} 的位置信息`);
}
const defaultPos = config.mapPositions[country][0];
await genshin.moveMapTo(defaultPos.x, defaultPos.y, country);
await sleep(1000); // 移动完等地图稳定
currentFlower = null;
for (let retryCount = 0; retryCount < maxRetries; retryCount++) {
log.info(`${retryCount + 1} 次尝试定位地脉花`);
const found = await locateLeyLineOutcrop(type);
if (found) {
// log.info("成功找到地脉花!");
return; // 找到就直接结束
}
// 第一次失败,执行特殊操作
if (retryCount === 0) {
log.warn("未找到地脉花,关闭自定义标记并继续尝试");
await closeCustomMarks();
}
// 如果 shouldMoveMap 建议移动地图,就移动一下
if (shouldMoveMap(country, retryCount)) {
const position = await getMapPosition(country, retryCount, config);
log.info(`移动到特定位置:(${position.x},${position.y}), ${position.name}`);
await genshin.moveMapTo(position.x, position.y);
await sleep(1000); // 移动后也等一下
}
await sleep(1000); // 每次循环结束也等一下,防止太快
}
// 如果到这里还没找到
throw new Error("寻找地脉花失败,已达最大重试次数");
}
/**
* 在地图上定位地脉花
* @param {string} type - 地脉花类型
* @returns {Promise<boolean>} 是否找到地脉花
*/
async function locateLeyLineOutcrop(type) {
await sleep(500); // 确保画面稳定
await genshin.setBigMapZoomLevel(3.0);
const iconPath = type === "蓝花(经验书)"
? "assets/icon/Blossom_of_Revelation.png"
: "assets/icon/Blossom_of_Wealth.png";
const flowerList = captureGameRegion().findMulti(RecognitionObject.TemplateMatch(file.ReadImageMatSync(iconPath)));
if (flowerList && flowerList.count > 0) {
currentFlower = flowerList[0];
const flowerType = type === "蓝花(经验书)" ? "经验" : "摩拉";
log.info(`找到${flowerType}地脉花,位置:(${currentFlower.x},${currentFlower.y})`);
const center = genshin.getPositionFromBigMap();
const mapZoomLevel = genshin.getBigMapZoomLevel();
const mapScaleFactor = 2.361;
leyLineX = (960 - currentFlower.x - 25) * mapZoomLevel / mapScaleFactor + center.x;
leyLineY = (540 - currentFlower.y - 25) * mapZoomLevel / mapScaleFactor + center.y;
log.info(`地脉花的实际坐标:(${leyLineX},${leyLineY})`);
return true;
} else {
log.warn("未找到地脉花");
return false;
}
}
/**
* 判断是否需要移动地图
* @param {string} country - 国家名称
* @param {number} retryCount - 重试次数
* @returns {boolean} 是否需要移动地图
*/
function shouldMoveMap(country, retryCount) {
if (retryCount == 0) return false;
// 不同国家的重试策略
const countryRetryMap = {
"蒙德": [0, 1, 2],
"璃月": [0, 1, 2, 3],
"稻妻": [0, 1, 2, 3, 4],
"枫丹": [0, 1, 2],
"纳塔": [0, 1, 2, 3, 4]
};
return countryRetryMap[country] && countryRetryMap[country].includes(retryCount);
}
/**
* 获取地图移动位置
* @param {string} country - 国家名称
* @param {number} retryCount - 重试次数
* @param {Object} config - 配置对象
* @returns {Object} 包含x,y坐标的位置对象
*/
async function getMapPosition(country, retryCount, config) {
// 从配置文件获取位置
if (config.mapPositions[country]) {
const positions = config.mapPositions[country];
// 确保retryCount不超过位置数量
let index = Math.min(retryCount, positions.length - 1);
log.warn(`retryCount${retryCount}`);
log.warn(`countryIndex${index}`);
return positions[index];
}
// 默认返回
log.warn(`未找到国家 ${country} 的位置信息`);
return { x: 0, y: 0, name: "默认位置" };
}
/**
* 判断坐标是否在指定位置附近(误差范围内)
* @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 {number} timeout - 超时时间
* @param {string} targetPath - 目标路径
* @param {number} [retries=0] - 当前函数内重试次数
* @returns {Promise<void>}
*/
async function processLeyLineOutcrop(timeout, targetPath, retries = 0) {
// 设置最大重试次数,防止死循环
const MAX_RETRIES = 3;
// 如果超过最大重试次数,记录错误并返回,避免死循环
if (retries >= MAX_RETRIES) {
log.error(`打开地脉花失败,已重试${MAX_RETRIES}次,终止处理`);
log.error("我辣么大一个地脉花哪去了?");
throw new Error("打开地脉花失败");
}
let ocr = captureGameRegion().find(RecognitionObject.ocrThis);
if (ocr && ocr.text.includes("地脉溢口")) {
log.info("识别到地脉溢口");
// await openOutcrop(targetPath);
keyPress("F");
await autoFight(timeout);
await autoNavigateToReward();
} else if (ocr && ocr.text.includes("打倒所有敌人")) {
log.info("地脉花已经打开,直接战斗");
await autoFight(timeout);
await autoNavigateToReward();
} else if (ocr && ocr.text.includes("地脉之花")) {
log.info("识别到地脉之花");
} else {
log.warn(`未识别到地脉花文本,当前重试次数: ${retries + 1}/${MAX_RETRIES}`);
try {
await pathingScript.runFile(targetPath);
await processLeyLineOutcrop(timeout, targetPath, retries + 1);
} catch (error) {
throw new Error(`打开地脉花失败: ${error.message}`);
}
}
}
/**
* 尝试领取地脉花奖励
* @param {Object} settings - 设置对象包含friendshipTeam
* @returns {Promise<void>}
*/
async function attemptReward(settings) {
const MAX_RETRY = 5;
// 超时处理
if (retryCount >= MAX_RETRY) {
retryCount = 0;
throw new Error("超过最大重试次数,领取奖励失败");
}
log.info("领取奖励,优先使用浓缩树脂");
keyPress("F");
await sleep(500);
// 识别是否为地脉之花界面
let resList = captureGameRegion().findMulti(RecognitionObject.ocrThis);
let isValid = false;
let condensedResin = null;
let originalResin = null;
let isResinEmpty = false;
let dobuleReward = false;
if (resList && resList.count > 0) {
// 分析识别到的文本
for (let i = 0; i < resList.count; i++) {
let res = resList[i];
if (res.text.includes("使用浓缩树脂")) {
isValid = true;
condensedResin = res;
} else if (res.text.includes("使用原粹树脂")) {
isValid = true;
originalResin = res;
} else if (res.text.includes("补充原粹树脂")) {
isValid = true;
isResinEmpty = true;
} else if (res.text.includes("产出")) {
isValid = true;
dobuleReward = true;
}
}
// 处理不同的树脂情况
if (originalResin && dobuleReward == true) {
log.info("选择使用原粹树脂,获得双倍产出");
click(Math.round(originalResin.x + originalResin.width / 2), Math.round(originalResin.y + originalResin.height / 2));
if (settings.friendshipTeam) {
log.info("切换回战斗队伍");
await genshin.switchParty(settings.team);
}
return;
} else if (condensedResin) {
log.info("选择使用浓缩树脂");
click(Math.round(condensedResin.x + condensedResin.width / 2), Math.round(condensedResin.y + condensedResin.height / 2));
if (settings.friendshipTeam) {
log.info("切换回战斗队伍");
await genshin.switchParty(settings.team);
}
return;
} else if (originalResin) {
log.info("选择使用原粹树脂");
click(Math.round(originalResin.x + originalResin.width / 2), Math.round(originalResin.y + originalResin.height / 2));
if (settings.friendshipTeam) {
log.info("切换回战斗队伍");
await genshin.switchParty(settings.team);
}
return;
} else if (isResinEmpty) {
log.error("识别到补充原粹树脂,看来树脂用完了呢");
await keyPress("VK_ESCAPE");
throw new Error("树脂已用完");
}
}
// 界面不正确,尝试重试
if (!isValid) {
log.info("当前界面不是地脉之花界面,重试");
await genshin.returnMainUi();
await sleep(1000);
retryCount++;
await autoNavigateToReward();
await attemptReward(settings);
}
}
/**
* 开启地脉花
* @param {string} targetPath - 目标路径
* @returns {Promise<boolean>} 区域是否出现地脉任务
*/
async function openOutcrop(targetPath) {
let ocrRegion1 = { x: 800, y: 200, width: 300, height: 100 }; // 中心区域
let ocrRegion2 = { x: 0, y: 200, width: 300, height: 300 }; // 追踪任务区域
let startTime = Date.now();
let recognized = false;
keyPress("F");
// 前5秒识别中心区域弹出的横幅任务提示
while (Date.now() - startTime < 5000) {
if (recognizeFightText(ocrRegion1)) {
recognized = true;
break;
}
keyPress("F");
await sleep(500);
}
// 如果5秒内没有识别成功再追加识别追踪任务区域及尝试重新开启地脉花
if (!recognized) {
let secondStartTime = Date.now();
while (Date.now() - secondStartTime < 60000) {
if (recognizeFightText(ocrRegion1) || recognizeFightText(ocrRegion2)) {
recognized = true;
break;
}
await pathingScript.runFile(targetPath);
keyPress("F");
await sleep(500);
}
}
}
/**
* 识别地脉开启进入战斗文本
* @param {Object} ocrRegion - OCR识别区域
* @returns {Promise<boolean>} 区域是否出现战斗文本
*/
function recognizeFightText(ocrRegion) {
try {
let result = captureGameRegion().find(RecognitionObject.ocr(ocrRegion.x, ocrRegion.y, ocrRegion.width, ocrRegion.height));
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<void>}
*/
async function autoFight(timeout) {
const cts = new CancellationTokenSource();
try {
const ocrRegionX = 850;
const ocrRegionY = 230;
const ocrRegionWidth = 1040 - ocrRegionX;
const ocrRegionHeight = 300 - ocrRegionY;
let ocrRegion = { x: ocrRegionX, y: ocrRegionY, width: ocrRegionWidth, height: ocrRegionHeight };
log.info("开始战斗");
dispatcher.RunTask(new SoloTask("AutoFight"), cts);
let fightResult = await recognizeTextInRegion(ocrRegion, timeout) ? "成功" : "失败";
log.info(`战斗结束,战斗结果:${fightResult}`);
cts.cancel();
} catch (error) {
log.error(`执行过程中出错: ${error}`);
}
}
/**
* 识别战斗结果
* @param {Object} ocrRegion - OCR识别区域
* @param {number} timeout - 超时时间
* @returns {Promise<boolean>} 战斗是否成功
*/
async function recognizeTextInRegion(ocrRegion, timeout) {
let startTime = Date.now();
const successKeywords = ["挑战达成", "战斗胜利", "挑战成功"];
const failureKeywords = ["挑战失败"];
// 循环检测直到超时
while (Date.now() - startTime < timeout) {
try {
let result = captureGameRegion().find(RecognitionObject.ocr(ocrRegion.x, ocrRegion.y, ocrRegion.width, ocrRegion.height));
let text = result.text;
// 检查成功关键词
for (let keyword of successKeywords) {
if (text.includes(keyword)) {
log.info("检测到战斗成功关键词: {0}", keyword);
return true;
}
}
// 检查失败关键词
for (let keyword of failureKeywords) {
if (text.includes(keyword)) {
log.warn("检测到战斗失败关键词: {0}", keyword);
return false;
}
}
}
catch (error) {
log.error("OCR过程中出错: {0}", error);
}
await sleep(1000); // 检查间隔
}
log.warn("在超时时间内未检测到战斗结果");
return false;
}
/**
* 地图标记相关函数
*/
/**
* 关闭自定义标记,使用前确保在地图界面
* @returns {Promise<void>}
*/
async function closeCustomMarks() {
// await genshin.returnMainUi();
// keyPress("M");
await sleep(600);
click(60, 1020);
await sleep(600);
let button = captureGameRegion().find(RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/icon/open.png"),));
if (button) {
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("未找到开关按钮");
}
}
/**
* 打开自定义标记,使用前确保在地图界面
* @returns {Promise<void>}
*/
async function openCustomMarks() {
await sleep(600);
click(60, 1020);
await sleep(600);
let button = captureGameRegion().findMulti(RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/icon/close.png"),));
if (button) {
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("未找到开关按钮");
}
}
/**
* 自动导航到地脉花奖励点
* @returns {Promise<void>}
*/
async function autoNavigateToReward() {
// 定义识别对象
const boxIconRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/icon/box.png"));
const rewardTextRo = RecognitionObject.Ocr(1210, 515, 200, 50); // 领奖区域检测
let advanceNum = 0; // 前进次数
// 调整初始视角为俯视角
log.info("调整为俯视视角...");
middleButtonClick();
await sleep(1000);
moveMouseBy(0, 1030);
await sleep(500);
moveMouseBy(0, 920);
await sleep(500);
moveMouseBy(0, 710);
await sleep(500);
log.info("开始自动导航到地脉花...");
while (true) {
// 1. 优先检查是否已到达领奖点
let captureRegion = captureGameRegion();
let rewardTextArea = captureRegion.DeriveCrop(1210, 515, 200, 50);
let ocrResults = rewardTextArea.findMulti(RecognitionObject.ocrThis);
if (advanceNum % 15 == 0 && advanceNum >= 10) {
log.warn("前进又超时15次啦先往旁边挪挪再继续试试")
keyDown("s");
await sleep(500);
keyUp("s");
middleButtonClick();
await sleep(1000);
keyDown("w");
await sleep(1000);
keyUp("w");
}
else if (advanceNum > 45) {
throw new Error('前进时间超时');
}
// 检测到地脉之花文字则结束
else if (ocrResults.count > 0 && ocrResults[0].text.trim().length > 0) {
for (let i = 0; i < ocrResults.count; i++) {
if (ocrResults[i].text.includes("地脉之花")) {
log.info("已到达领奖点,检测到文字: " + ocrResults[i].text);
return;
}
}
}
// 2. 未到达领奖点,则调整视野
await adjustViewForReward(boxIconRo, advanceNum);
// 3. 前进一小步
keyDown("w");
await sleep(900);
keyUp("w");
await sleep(100); // 等待角色移动稳定
advanceNum++;
}
}
/**
* 调整视野直到图标位于正前方
* @param {Object} boxIconRo - 宝箱图标识别对象
* @param {number} advanceNum - 当前前进次数
* @returns {Promise<void>}
*/
async function adjustViewForReward(boxIconRo, advanceNum) {
for (let i = 0; i < 100; i++) {
// 每10次执行一轮异常页面检查
if (i % 10 == 0) {
// 识别误触发领取导致超时的情况
let resList = captureGameRegion().findMulti(RecognitionObject.ocrThis);
if (resList && resList.count > 0) {
for (let i = 0; i < resList.count; i++) {
let res = resList[i];
if (res.text.includes("原粹树脂")) {
log.info("误触发领取页面,尝试关闭页面")
keyPress("ESCAPE");
await sleep(500);
keyPress("ESCAPE");
await sleep(500);
await genshin.returnMainUi();
}
}
}
// 识别误触发其他页面导致超时的情况
const paimonMenuRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/icon/paimon_menu.png"), 0, 0, genshin.width / 3.0, genshin.width / 5.0);
let res = captureGameRegion().Find(paimonMenuRo);
if (res.isEmpty()) {
log.info("误触发其他页面,尝试关闭页面")
click(960, 800);
keyPress("ESCAPE");
await sleep(500);
keyPress("ESCAPE");
await sleep(500);
await genshin.returnMainUi();
}
}
let captureRegion = captureGameRegion();
let iconRes = captureRegion.Find(boxIconRo);
if (!iconRes) {
// 未找到图标,小幅度转动视角
moveMouseBy(20, 0);
await sleep(100);
continue;
}
if (iconRes.x >= 920 && iconRes.x <= 980 && iconRes.y <= 540) {
log.info(`视野已调正,前进第 ${advanceNum}`);
return;
} else {
// 小幅度调整
let adjustAmount = iconRes.x < 920 ? -20 : 20;
let adjustAmount2 = iconRes.y < 540 ? 1 : 10;
moveMouseBy(adjustAmount * adjustAmount2, 0);
await sleep(100);
}
}
// throw new Error('视野调整超时');
}