Files
bettergi-scripts-list/repo/js/AutoLeyLineOutcrop/main.js

825 lines
27 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)
*
* 功能:自动寻找并完成地脉花挑战,领取奖励
*/
// 全局变量
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");
}
}