Files
bettergi-scripts-list/repo/js/AAA-Auto-Story-Loader/main.js
LX666-666 d909d2450a 自动剧情加载器 (#1637)
从自动委托中分分离出来的
2025-08-18 22:29:00 +08:00

1858 lines
61 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.
(async function () {
// 版本和编译信息
const VERSION = "1.0";
const BUILD_TIME = "2025.08.18";
async function errorlog() {
// 输出版本和编译时间信息
log.info("=".repeat(20));
log.info("版本: {version}", VERSION);
log.info("编译时间: {buildTime}", BUILD_TIME);
log.info("=".repeat(20));
}
// 统一常量定义
const Datas = {
// 文件路径常量
SUPPORT_LIST_PATH: "name.json",
OUTPUT_DIR: "Data",
TALK_PROCESS_BASE_PATH: "assets/process"
};
// 获取设置
/*const getSetting = async () => {
try {
const skipRecognition = settings.skipRecognition || false;
const prepare = settings.prepare || false;
const team = settings.team || "";
const skipCommissions = "";
const result = {
skipRecognition,
prepare,
team,
skipCommissions,
};
log.debug("setting:{index}", result);
return result;
} catch {
log.error("getSetting函数出现错误,将使用默认配置");
return {
skipRecognition: false,
prepare: true,
team: "",
skipCommissions: "",
};
}
};
const { skipRecognition, prepare, team, skipCommissions } =
await getSetting();*/
const Utils = {
iframe: async ({ X, Y, WIDTH, HEIGHT }) => {
try {
log.info("i{index}", { X, Y, WIDTH, HEIGHT });
// 最简单的方式创建OCR识别对象
const ro = RecognitionObject.Ocr(X, Y, WIDTH, HEIGHT);
ro.Name = "debug";
ro.DrawOnWindow = true;
// 捕获并识别
const region = captureGameRegion();
region.Find(ro);
// 2000毫秒后移除绘制的边框
setTimeout(() => {
// 使用相同的名称移除边框
const drawContent = VisionContext.Instance().DrawContent;
drawContent.RemoveRect("debug");
// 或者也可以使用 drawContent.Clear() 清除所有绘制的内容
log.info("已移除边框");
}, 2000);
} catch (error) {
// 记录完整错误信息
log.error("详细错误: " + JSON.stringify(error));
}
},
easyOCR: async ({ X, Y, WIDTH, HEIGHT }) => {
try {
// log.info("进行文字识别")
// 创建OCR识别对象
const locationOcrRo = RecognitionObject.Ocr(X, Y, WIDTH, HEIGHT);
// 截图识别
let captureRegion = captureGameRegion();
let OCRresults = await captureRegion.findMulti(locationOcrRo);
return OCRresults;
} catch (error) {
log.error("easyOCR识别出错: {error}", error.message);
return { count: 0 };
}
},
easyOCROne: async (ocrdata) => {
results = await Utils.easyOCR(ocrdata);
if (results.count > 0) {
// 取第一个结果作为地点
return results[0].text.trim();
}
return "";
},
// 清理文本(去除标点符号等)
cleanText: (text) => {
if (!text) return "";
// 去除标点符号和特殊字符
return text.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, "").trim();
},
// 读取角色别名文件
readAliases: () => {
try {
const combatText = file.ReadTextSync("Data/avatar/combat_avatar.json");
const combatData = JSON.parse(combatText);
const aliases = {};
for (const character of combatData) {
if (character.alias && character.name) {
for (const alias of character.alias) {
aliases[alias] = character.name;
}
}
}
return aliases;
} catch (error) {
log.error("读取角色别名文件失败: {error}", error.message);
return {};
}
},
};
const UI = {
// 角色选择界面滚动页面函数
scrollPage: async (totalDistance, stepDistance = 10, delayMs = 5) => {
try {
moveMouseTo(400, 750);
await sleep(50);
leftButtonDown();
const steps = Math.ceil(totalDistance / stepDistance);
for (let j = 0; j < steps; j++) {
const remainingDistance = totalDistance - j * stepDistance;
const moveDistance =
remainingDistance < stepDistance ? remainingDistance : stepDistance;
moveMouseBy(0, -moveDistance);
await sleep(delayMs);
}
await sleep(700);
leftButtonUp();
await sleep(100);
return true;
} catch (error) {
log.error(`角色选择界面滚动操作时发生错误:${error.message}`);
return false;
}
},
};
// 步骤处理器类 - 处理不同类型的委托执行步骤
// TAG:添加脚本功能点1
const StepProcessor = {
// 处理地图追踪步骤
processMapTracking: async (step, commissionName, location) => {
const fullPath = `${
Datas.TALK_PROCESS_BASE_PATH
}/${commissionName}/${location}/${step.data || step}`;
log.info("执行地图追踪: {path}", fullPath);
try {
await pathingScript.runFile(fullPath);
log.info("地图追踪执行完成");
} catch (error) {
log.error("执行地图追踪时出错: {error}", error.message);
throw error;
}
},
// 处理追踪委托步骤
processCommissionTracking: async (step) => {
try {
// 获取目标NPC名称和图标类型
let targetNpc = "";
let iconType = "bigmap";
if (typeof step.data === "string") {
targetNpc = step.data;
} else if (typeof step.data === "object") {
if (step.data.npc) targetNpc = step.data.npc;
if (step.data.iconType) iconType = step.data.iconType;
}
log.info(
"执行追踪委托目标NPC: {target},图标类型: {type}",
targetNpc,
iconType
);
await Execute.autoNavigateToTalk(targetNpc, iconType);
log.info("追踪委托执行完成");
} catch (error) {
log.error("执行追踪委托时出错: {error}", error.message);
throw error;
}
},
// 处理键鼠脚本步骤
processKeyMouseScript: async (step, commissionName, location) => {
log.info("执行键鼠脚本: {path}", step.data);
try {
const fullPath = `${Datas.TALK_PROCESS_BASE_PATH}/${commissionName}/${location}/${step.data}`;
await keyMouseScript.runFile(fullPath);
log.info("键鼠脚本执行完成");
} catch (error) {
log.error("执行键鼠脚本时出错: {error}", error.message);
throw error;
}
},
// 处理按键步骤
processKeyPress: async (step) => {
if (typeof step.data === "string") {
log.info("执行按键: {key}", step.data);
keyPress(step.data);
} else if (typeof step.data === "object") {
if (step.data.action === "down") {
log.info("按下按键: {key}", step.data.key);
keyDown(step.data.key);
} else if (step.data.action === "up") {
log.info("释放按键: {key}", step.data.key);
keyUp(step.data.key);
} else if (step.data.action === "press") {
log.info("点击按键: {key}", step.data.key);
keyPress(step.data.key);
}
}
},
// 处理传送步骤
processTeleport: async (step) => {
if (Array.isArray(step.data) && step.data.length >= 2) {
log.info("执行传送: {x}, {y}", step.data[0], step.data[1]);
const force = step.data.length > 2 ? step.data[2] : false;
await genshin.tp(step.data[0], step.data[1], force);
log.info("传送完成");
} else {
log.error("传送参数格式错误");
throw new Error("传送参数格式错误");
}
},
// 处理等待主界面步骤
processWaitMainUI: async (isInMainUI) => {
for (let i = 0; i < 120; i++) {
if (isInMainUI()) {
log.info("检测到已返回主界面,结束等待");
break;
}
await sleep(1000);
}
if (!isInMainUI()) {
log.info("等待返回主界面超时,尝试继续执行后续步骤");
}
},
// 处理地址检测步骤
processLocationDetection: async (
step,
commissionName,
location,
processSteps,
currentIndex
) => {
if (Array.isArray(step.data) && step.data.length >= 2) {
log.info(
`地址检测: {${step.data[0]}},{${step.data[1]}},run:${step.run}`
);
try {
// 获取当前委托目标位置
let commissionTarget = await Execute.findCommissionTarget(
commissionName
);
if (commissionTarget) {
const distance2 = CommissionsFunc.calculateDistance(
commissionTarget,
{
x: step.data[0],
y: step.data[1],
}
);
log.info(
"地址检测 - 委托位置: ({x}, {y}), 目标位置: ({tx}, {ty}), 距离: {d}",
commissionTarget.x,
commissionTarget.y,
step.data[0],
step.data[1],
distance2
);
if (distance2 < 15) {
log.info("地址检测成功,执行后续步骤");
const nextSteps = await Execute.loadAndParseProcessFile(
commissionName,
location,
step.run
);
// 插入到processSteps的这一步后面
if (nextSteps && Array.isArray(nextSteps)) {
processSteps.splice(currentIndex + 1, 0, ...nextSteps);
log.info("已插入 {count} 个后续步骤", nextSteps.length);
}
} else {
log.info("地址检测失败,距离过远: {distance}", distance2);
}
} else {
log.warn("无法获取委托目标位置,跳过地址检测");
}
} catch (error) {
log.error("地址检测时出错: {error}", error.message);
throw error;
}
} else {
log.error("地址检测参数格式错误");
throw new Error("地址检测参数格式错误");
}
},
// 处理委托描述检测步骤
processCommissionDescriptionDetection: async (
step,
commissionName,
location,
processSteps,
currentIndex
) => {
// 按v键打开任务界面
keyPress("v");
await sleep(300);
if (step.data !== "") {
log.info(`委托描述检测: {${step.data}}`);
// 循环检测,直到稳定
for (let c = 0; c < 13; c++) {
try {
// 使用委托详情检测区域进行OCR
const taskRegion = { X: 75, Y: 240, WIDTH: 280, HEIGHT: 43 };
const ocrResult = await Utils.easyOCROne(taskRegion);
if (ocrResult === commissionName || ocrResult === "") {
await sleep(1000);
// 没有延时13s的错误提示继续检测
log.debug("检测到委托名称或空文本,继续等待...");
keyPress("v");
}
// 成功匹配开始插入step
else if (ocrResult === step.data) {
log.info("委托描述检测成功,执行后续步骤");
const nextSteps = await Execute.loadAndParseProcessFile(
commissionName,
location,
step.run
);
// 插入到这一步后面
if (nextSteps && Array.isArray(nextSteps)) {
processSteps.splice(currentIndex + 1, 0, ...nextSteps);
log.info("已插入 {count} 个后续步骤", nextSteps.length);
}
break;
} else {
log.warn(`委托描述不匹配,识别:${ocrResult},期望:${step.data}`);
break;
}
} catch (ocrError) {
log.error("委托描述OCR识别出错: {error}", ocrError);
break;
}
}
} else {
log.error("委托描述检测参数格式错误");
throw new Error("委托描述检测参数格式错误");
}
},
// 处理角色切换步骤
processSwitchRole: async (step) => {
try {
const { position, character } = step.data;
if (!position || !character) {
log.error("角色切换参数不完整,需要 position 和 character");
return false;
}
log.info(`开始切换角色:第${position}号位 -> ${character}`);
const positionCoordinates = [
[460, 538],
[792, 538],
[1130, 538],
[1462, 538],
];
// 读取别名
const aliases = Utils.readAliases();
const actualName = aliases[character] || character;
log.info(`设置对应号位为【${character}】,切换角色为【${actualName}`);
// 识别对象定义
const roTeamConfig = RecognitionObject.TemplateMatch(
file.ReadImageMatSync(`Data/RecognitionObject/队伍配置.png`),
0,
0,
1920,
1080
);
const roReplace = RecognitionObject.TemplateMatch(
file.ReadImageMatSync(`Data/RecognitionObject/更换.png`),
0,
0,
1920,
1080
);
const roJoin = RecognitionObject.TemplateMatch(
file.ReadImageMatSync(`Data/RecognitionObject/加入.png`),
0,
0,
1920,
1080
);
let openPairingTries = 0;
let totalOpenPairingTries = 0;
// 打开配对界面的内部函数
const openPairingInterface = async () => {
while (openPairingTries < 3) {
keyPress("l");
await sleep(3500);
const teamConfigResult = captureGameRegion().find(roTeamConfig);
if (teamConfigResult.isExist()) {
openPairingTries = 0;
return true;
}
openPairingTries++;
totalOpenPairingTries++;
}
if (totalOpenPairingTries < 6) {
await genshin.tp("2297.630859375", "-824.5517578125");
openPairingTries = 0;
return openPairingInterface();
} else {
log.error("无法打开配对界面,任务结束");
return false;
}
};
if (!(await openPairingInterface())) {
return false;
}
const rolenum = position;
const selectedCharacter = actualName;
const [x, y] = positionCoordinates[position - 1];
click(x, y);
log.info(`开始设置${rolenum}号位角色`);
await sleep(1000);
let characterFound = false;
let pageTries = 0;
// 最多尝试滚动页面20次
while (pageTries < 20) {
// 尝试识别所有可能的角色文件名
for (let num = 1; ; num++) {
const paddedNum = num.toString().padStart(2, "0");
const characterFileName = `${selectedCharacter}${paddedNum}`;
try {
const characterRo = RecognitionObject.TemplateMatch(
file.ReadImageMatSync(
`Data/characterimage/${characterFileName}.png`
),
0,
0,
1920,
1080
);
const characterResult = captureGameRegion().find(characterRo);
if (characterResult.isExist()) {
log.info(`已找到角色${selectedCharacter}`);
// 计算向右偏移35像素、向下偏移35像素的位置
const targetX = characterResult.x + 35;
const targetY = characterResult.y + 35;
// 边界检查,确保坐标在屏幕范围内
const safeX = Math.min(Math.max(targetX, 0), 1920);
const safeY = Math.min(Math.max(targetY, 0), 1080);
click(safeX, safeY);
await sleep(500); // 点击角色后等待0.5秒
characterFound = true;
break;
}
} catch (error) {
// 如果文件不存在,跳出循环
break;
}
}
if (characterFound) {
break;
}
// 如果不是最后一次尝试,尝试滚动页面
if (pageTries < 15) {
log.info("当前页面没有目标角色,滚动页面");
await UI.scrollPage(200); // 使用UI模块的scrollPage函数
}
pageTries++;
}
if (!characterFound) {
log.error(`未找到【${selectedCharacter}`);
return false;
}
// 识别"更换"或"加入"按钮
const replaceResult = captureGameRegion().find(roReplace);
const joinResult = captureGameRegion().find(roJoin);
if (replaceResult.isExist() || joinResult.isExist()) {
await sleep(300);
click(68, 1020);
keyPress("VK_LBUTTON");
await sleep(500);
log.info(`角色切换完成:${character} -> ${actualName}`);
return true;
} else {
log.error(`该角色已在队伍中,无需切换`);
await sleep(300);
keyPress("VK_ESCAPE");
await sleep(500);
return false;
}
} catch (error) {
log.error("角色切换过程中出错: {error}", error.message);
return false;
}
},
// 处理自动任务步骤
processAutoTask: async (step) => {
try {
const { action, taskType, config } = step.data;
if (!action) {
log.error("自动任务参数不完整,需要 action 参数");
return false;
}
log.info("执行自动任务操作: {action}", action);
switch (action) {
case "enable":
// 启用自动任务
if (!taskType) {
log.error("启用自动任务需要指定 taskType");
return false;
}
if (config && typeof config === "object") {
log.info("启用自动任务: {type},配置: {config}", taskType, JSON.stringify(config));
dispatcher.addTimer(new RealtimeTimer(taskType, config));
} else {
log.info("启用自动任务: {type}", taskType);
dispatcher.addTimer(new RealtimeTimer(taskType));
}
break;
case "disable":
// 取消所有自动任务
log.info("取消所有自动任务");
dispatcher.ClearAllTriggers();
break;
default:
log.error("未知的自动任务操作: {action}", action);
return false;
}
return true;
} catch (error) {
log.error("处理自动任务步骤时出错: {error}", error.message);
return false;
}
},
};
// 步骤处理器工厂 - 更好的扩展性设计
// TAG:添加脚本功能点2
const StepProcessorFactory = {
// 步骤处理器映射表
processors: {
地图追踪: async (step, context) => {
await StepProcessor.processMapTracking(
step,
context.commissionName,
context.location
);
},
等待: async (step, context) => {
const waitTime = step.data || 5000;
log.info("等待 {time} 毫秒", waitTime);
await sleep(waitTime);
},
追踪委托: async (step, context) => {
await StepProcessor.processCommissionTracking(step);
},
键鼠脚本: async (step, context) => {
await StepProcessor.processKeyMouseScript(
step,
context.commissionName,
context.location
);
},
对话: async (step, context) => {
await Execute.processDialogStep(
step,
context.priorityOptions,
context.npcWhiteList,
context.isInMainUI
);
},
按键: async (step, context) => {
await StepProcessor.processKeyPress(step);
},
tp: async (step, context) => {
await StepProcessor.processTeleport(step);
},
等待返回主界面: async (step, context) => {
await StepProcessor.processWaitMainUI(context.isInMainUI);
},
地址检测: async (step, context) => {
//await StepProcessor.processLocationDetection(step,context.commissionName,context.location,context.processSteps,context.currentIndex);
log.info("地址检测当前版本用不了,请联请联系作者获取最新版");
},
委托描述检测: async (step, context) => {
await StepProcessor.processCommissionDescriptionDetection(
step,
context.commissionName,
context.location,
context.processSteps,
context.currentIndex
);
},
切换角色: async (step, context) => {
await StepProcessor.processSwitchRole(step);
},
自动任务: async (step, context) => {
await StepProcessor.processAutoTask(step);
},
},
// 注册新的步骤处理器
register: (stepType, processor) => {
StepProcessorFactory.processors[stepType] = processor;
log.info("注册新的步骤处理器: {type}", stepType);
},
// 处理步骤
process: async (step, context) => {
const processor = StepProcessorFactory.processors[step.type];
if (processor) {
await processor(step, context);
} else {
log.warn("未知的流程类型: {type}", step.type);
}
},
};
// UI工具模块 - 处理UI检测和文本提取等工具函数
const UIUtils = {
// 创建主界面检测函数
createMainUIChecker: () => {
const paimonMenuRo = RecognitionObject.TemplateMatch(
file.ReadImageMatSync("Data/RecognitionObject/paimon_menu.png"),
0,
0,
genshin.width / 3.0,
genshin.width / 5.0
);
return () => {
let captureRegion = captureGameRegion();
let res = captureRegion.Find(paimonMenuRo);
return !res.isEmpty();
};
},
// 人名提取函数
extractName: (text) => {
const patterns = [
/与(.+?)对话/,
/与(.+?)一起/,
/同(.+?)交谈/,
/向(.+?)打听/,
/向(.+?)回报/,
/向(.+?)报告/,
/给(.+?)听/,
/陪同(.+?)\S+/,
/找到(.+?)\S+/,
/询问(.+?)\S+/,
/拜访(.+?)\S+/,
/寻找(.+?)\S+/,
/告诉(.+?)\S+/,
/带(.+?)去\S+/,
/跟随(.+?)\S+/,
/协助(.+?)\S+/,
/请教(.+?)\S+/,
/拜托(.+?)\S+/,
/委托(.+?)\S+/,
];
for (const pattern of patterns) {
const match = text.match(pattern);
if (match && match[1]) {
return match[1].trim();
}
}
return null;
},
};
// 对话处理模块 - 处理自动对话相关功能
const DialogProcessor = {
// 执行优化的自动对话
executeOptimizedAutoTalk: async (
extractedName = null,
skipCount = 5,
customPriorityOptions = null,
customNpcWhiteList = null,
isInMainUI
) => {
// 使用传入的参数,不再加载默认配置
const effectivePriorityOptions = customPriorityOptions || [];
const effectiveNpcWhiteList = customNpcWhiteList || [];
// 初始化
keyPress("V");
// 初始触发剧情 - 识别人名并点击
extractedName = [];
// 人名区域OCR识别
const nameRegion = { X: 75, Y: 240, WIDTH: 225, HEIGHT: 60 };
let nameResults = await Utils.easyOCR(nameRegion);
// 尝试提取任务人名
for (let i = 0; i < nameResults.count; i++) {
let text = nameResults[i].text;
log.info(`任务区域识别文本: ${text}`);
// 尝试提取任务人名
let name = UIUtils.extractName(text);
if (name) {
extractedName = name;
log.info(`提取到人名: ${extractedName}`);
break;
}
}
// 对话选项区域OCR识别
const dialogRegion = { X: 1150, Y: 300, WIDTH: 350, HEIGHT: 400 };
nameResults = await Utils.easyOCR(dialogRegion);
let clickedWhitelistNPC = false;
let clickedExtractedName = false;
// 处理人名区域的OCR结果
if (nameResults.count > 0) {
log.info(`人名区域识别到 ${nameResults.count} 个文本`);
// 首先尝试点击白名单中的NPC
for (let i = 0; i < nameResults.count; i++) {
let text = nameResults[i].text;
let res = nameResults[i];
log.info(
"人名区域识别到{text}:位置({x},{y},{h},{w})",
res.text,
res.x,
res.y,
res.width,
res.Height
);
// 检查是否包含白名单中的NPC名称
for (let j = 0; j < effectiveNpcWhiteList.length; j++) {
if (text.includes(effectiveNpcWhiteList[j])) {
log.info(`找到白名单NPC: ${effectiveNpcWhiteList[j]}点击该NPC`);
keyDown("VK_MENU");
await sleep(500);
click(res.x, res.y);
leftButtonClick();
keyUp("VK_MENU");
clickedWhitelistNPC = true;
break;
}
}
if (clickedWhitelistNPC) break;
}
// 如果没有点击白名单NPC尝试点击包含提取到的人名的选项
if (!clickedWhitelistNPC && extractedName) {
for (let i = 0; i < nameResults.count; i++) {
let text = nameResults[i].text;
let res = nameResults[i];
if (text.includes(extractedName)) {
log.info(`点击包含提取到任务人名的选项: ${text}`);
keyDown("VK_MENU");
await sleep(500);
click(res.x, res.y);
leftButtonClick();
keyUp("VK_MENU");
clickedExtractedName = true;
break;
}
}
}
}
// 如果没有找到NPC使用默认触发
if (!clickedWhitelistNPC && !clickedExtractedName) {
log.info("未找到匹配的NPC使用默认触发方式");
keyPress("F"); // 默认触发剧情
await sleep(500);
}
// 重复执行自动剧情,直到返回主界面
let maxAttempts = 100; // 设置最大尝试次数,防止无限循环
let attempts = 0;
let repetition = 0;
let oldcount = 1;
await sleep(1000);
log.info("开始执行自动剧情");
while (!isInMainUI() && attempts < maxAttempts) {
attempts++;
// 正常跳过对话
await genshin.chooseTalkOption("纳西妲美貌举世无双", skipCount, false);
if (isInMainUI()) {
log.info("检测到已返回主界面,结束循环");
break;
}
//keyPress("VK_ESCAPE");//关弹窗
// 每skipCount次跳过后进行OCR识别
if (true) {
// 检查是否有匹配的优先选项
let foundPriorityOption = false;
// 获取对话区域截图并进行OCR识别
const dialogOptionsRegion = {
X: 1250,
Y: 450,
WIDTH: 550,
HEIGHT: 400,
};
let ocrResults = await Utils.easyOCR(dialogOptionsRegion);
if (ocrResults.count > 0) {
log.info(`识别到 ${ocrResults.count} 个选项`);
if (ocrResults.count === oldcount) {
repetition++;
}
else {
repetition = 0;
}
oldcount = ocrResults.count;
if (repetition >= 5) {
log.info("连续5次选项数量一样执行F跳过");
keyPress("F");
keyPress("VK_ESCAPE");
repetition = 0;
}
for (let i = 0; i < ocrResults.count; i++) {
let ocrText = ocrResults[i].text;
// 检查是否在优先选项列表中
for (let j = 0; j < effectivePriorityOptions.length; j++) {
if (ocrText.includes(effectivePriorityOptions[j])) {
log.info(
`找到优先选项: ${effectivePriorityOptions[j]},点击该选项`
);
// 点击该选项
ocrResults[i].click();
await sleep(500);
foundPriorityOption = true;
break;
}
}
if (foundPriorityOption) break;
}
// 如果没有找到优先选项,则使用默认跳过
if (!foundPriorityOption) {
await genshin.chooseTalkOption("", 1, false);
}
}
}
// 检查是否已返回主界面
if (isInMainUI()) {
log.info("检测到已返回主界面,结束循环");
break;
}
}
if (isInMainUI()) {
log.info("已返回主界面,自动剧情执行完成");
keyPress("V");
} else {
log.warn(`已达到最大尝试次数 ${maxAttempts},但未检测到返回主界面`);
}
},
};
const Execute = {
// 寻找委托目的地址带追踪任务
findCommissionTarget: async (commissionName) => {
try {
log.info("开始寻找委托目标位置: {name}", commissionName);
// 确保回到主界面
await genshin.returnMainUi();
// 第一步检测这个任务是否在1-3之中
let index = 4;
try {
// 进入委托界面
const enterSuccess = await UI.enterCommissionScreen();
if (!enterSuccess) {
log.error("无法进入委托界面");
return null;
}
await sleep(1000);
// 识别前3个委托
log.debug("findCommissionTarget识别前3个委托");
// 先识别前3个Main_Dev区域索引0-2
for (let regionIndex = 0; regionIndex < 3; regionIndex++) {
const region = Datas.OCR_REGIONS.Main_Dev[regionIndex];
try {
let results = await Utils.easyOCR(region);
// 处理识别结果,取第一个有效结果
for (let i = 0; i < results.count; i++) {
let result = results[i];
let text = Utils.cleanText(result.text);
if (text && text.length >= 2) {
log.info(
'第{regionIndex}个委托: "{text}"',
regionIndex + 1,
text
);
if (text === commissionName) {
index = regionIndex + 1;
log.info(
"找到委托 {name} 在位置 {index}",
commissionName,
index
);
break;
}
}
}
// 如果找到了委托,跳出外层循环
if (index !== 4) {
break;
}
} catch (regionError) {
log.error(
"识别第{index}个委托区域时出错: {error}",
regionIndex + 1,
regionError
);
continue;
}
}
} catch (error) {
log.error("findCommissionTarget第一步失败: {error}", error.message);
}
// 如果前3个没找到检查第4个委托需要翻页
if (index === 4) {
try {
log.info("前3个委托中未找到检查第4个委托");
await UI.pageScroll(1);
const region = Datas.OCR_REGIONS.Main_Dev[3]; // 第4个区域
let results = await Utils.easyOCR(region);
for (let i = 0; i < results.count; i++) {
let result = results[i];
let text = Utils.cleanText(result.text);
if (text && text.length >= 2) {
log.info('第4个委托: "{text}"', text);
if (text === commissionName) {
index = 4;
log.info("找到委托 {name} 在第4个位置", commissionName);
break;
}
}
}
} catch (fourthError) {
log.error("识别第4个委托时出错: {error}", fourthError);
}
}
// 第二步:进入对应的大地图,获取位置
let currentCommissionPosition = null;
try {
// 点击详情按钮
if (index === 4) {
// 第4个委托已经翻页了使用索引3
index = 3;
}
const button = Datas.COMMISSION_DETAIL_BUTTONS[index - 1];
if (button) {
log.info("点击委托详情按钮: {id}", button.id);
click(button.x, button.y);
await sleep(2000);
// 检查是否有追踪按钮并点击
const trackingResult = await Utils.easyOCROne(
Datas.OCR_REGIONS.COMMISSION_TRACKING
);
if (trackingResult === "追踪") {
log.info("发现追踪按钮,点击追踪");
click(1693, 1000);
await sleep(1000);
}
// 退出详情页面
log.info("退出详情页面 - 按ESC");
keyDown("VK_ESCAPE");
await sleep(300);
keyUp("VK_ESCAPE");
await sleep(1200);
await genshin.setBigMapZoomLevel(2);
// 获取地图坐标并保存
const bigMapPosition = genshin.getPositionFromBigMap();
if (bigMapPosition) {
currentCommissionPosition = bigMapPosition;
log.info(
"当前委托位置: ({x}, {y})",
bigMapPosition.x,
bigMapPosition.y
);
}
await genshin.returnMainUi();
} else {
log.error("无效的委托按钮索引: {index}", index);
}
} catch (error) {
log.error("findCommissionTarget第2步失败: {error}", error.message);
}
return currentCommissionPosition;
} catch (error) {
log.error("寻找委托目标位置时出错: {error}", error.message);
return null;
}
},
// 读取并解析流程文件为步骤数组
loadAndParseProcessFile: async (
commissionName,
location,
locationprocessFilePath = "process.json"
) => {
const processFilePath = `${Datas.TALK_PROCESS_BASE_PATH}/${commissionName}/${location}/${locationprocessFilePath}`;
let processContent;
let processSteps;
try {
processContent = await file.readText(processFilePath);
log.info("找到对话委托流程文件: {path}", processFilePath);
} catch (error) {
log.warn(
"未找到对话委托 {name} 在 {location} 的流程文件: {path}",
commissionName,
location,
processFilePath
);
return false;
}
// 解析流程内容
try {
// 尝试解析为JSON格式
const jsonData = JSON.parse(processContent);
if (Array.isArray(jsonData)) {
processSteps = jsonData;
log.debug("JSON流程解析成功");
} else {
log.error("JSON流程格式错误应为数组");
return false;
}
} catch (jsonError) {
// 如果不是JSON格式按简单格式处理
const lines = processContent
.split("\n")
.map((line) => line.trim())
.filter((line) => line.length > 0);
processSteps = lines;
}
return processSteps;
},
// 执行对话委托流程(优化版)
executeTalkCommission: async (commissionName, location) => {
try {
const processSteps = await Execute.loadAndParseProcessFile(
commissionName,
location,
"process.json"
);
// 使用统一的处理器执行流程
return await Execute.executeUnifiedTalkProcess(
processSteps,
commissionName,
location
);
} catch (error) {
log.error("执行对话委托时出错: {error}", error.message);
return false;
}
},
// 自动导航到NPC对话位置从main_branch.js移植
autoNavigateToTalk: async (npcName = "", iconType = "") => {
try {
// 设置目标NPC名称
const textArray = npcName;
// 根据图标类型选择不同的识别对象
let boxIconRo;
if (iconType === "Bigmap") {
boxIconRo = RecognitionObject.TemplateMatch(
file.ReadImageMatSync(
"Data/RecognitionObject/IconBigmapCommission.jpg"
)
);
log.info("使用大地图图标");
}
else if (iconType === "Question") {
boxIconRo = RecognitionObject.TemplateMatch(
file.ReadImageMatSync(
"Data/RecognitionObject/IconQuestionCommission.png"
)
);
log.info("使用问号任务图标");
}
else {
// 默认使用任务图标
boxIconRo = RecognitionObject.TemplateMatch(
file.ReadImageMatSync(
"Data/RecognitionObject/IconTaskCommission.png"
)
);
log.info("使用任务图标");
}
let advanceNum = 0; //前进次数
middleButtonClick();
await sleep(800);
while (true) {
// 1. 优先检查是否已到达
await sleep(500);// 等待0.5秒
let captureRegion = captureGameRegion();
let rewardTextArea = captureRegion.DeriveCrop(1210, 515, 200, 50);
let rewardResult = rewardTextArea.find(RecognitionObject.ocrThis);
log.debug("检测到文字: " + rewardResult.text);
// 检测到特点文字则结束!!!
if (rewardResult.text == textArray) {
log.info("已到达指定位置,检测到文字: " + rewardResult.text);
return;
} else if (advanceNum > 80) {
throw new Error("前进时间超时");
}
// 2. 未到达领奖点,则调整视野
for (let i = 0; i < 100; i++) {
captureRegion = captureGameRegion();
let iconRes = captureRegion.Find(boxIconRo);
log.info("检测到委托图标位置 ({x}, {y})", iconRes.x, iconRes.y);
if (iconRes.x >= 920 && iconRes.x <= 980 && iconRes.y <= 540) {
advanceNum++;
log.info(`视野已调正,前进第${advanceNum}`);
break;
} else {
// 小幅度调整
if (iconRes.y >= 520) moveMouseBy(0, 920);
let adjustAmount = iconRes.x < 920 ? -20 : 20;
let distanceToCenter = Math.abs(iconRes.x - 920); // 计算与920的距离
let scaleFactor = Math.max(1, Math.floor(distanceToCenter / 50)); // 根据距离缩放最小为1
let adjustAmount2 = iconRes.y < 540 ? scaleFactor : 10;
moveMouseBy(adjustAmount * adjustAmount2, 0);
await sleep(100);
}
if (i > 50) throw new Error("视野调整超时");
}
// 3. 前进一小步
keyDown("w");
await sleep(200);
keyPress("VK_SPACE");
await sleep(200);
keyPress("VK_SPACE");
await sleep(200);
keyUp("w");
await sleep(200); // 等待角色移动稳定
}
} catch (error) {
log.error("自动导航到NPC对话位置时出错: {error}", error.message);
throw error;
}
},
// 统一的对话委托流程处理器(重构版 - 更简洁的主控制函数)
executeUnifiedTalkProcess: async (
processSteps,
commissionName,
location
) => {
try {
log.info("执行统一对话委托流程: {name}", commissionName);
if (!processSteps || processSteps.length === 0) {
log.warn("没有找到有效的流程步骤");
return false;
}
// 初始化UI检测器和配置
const isInMainUI = UIUtils.createMainUIChecker();
let priorityOptions = [];
let npcWhiteList = [];
// 刚开始就追踪委托目标
//await Execute.findCommissionTarget(commissionName);
// 执行处理步骤
for (let i = 0; i < processSteps.length; i++) {
const step = processSteps[i];
log.info("执行流程步骤 {step}: {type}", i + 1, step.type || step);
try {
// 重置为默认值并处理自定义配置
const stepConfig = Execute.processStepConfiguration(
step,
priorityOptions,
npcWhiteList
);
priorityOptions = stepConfig.priorityOptions;
npcWhiteList = stepConfig.npcWhiteList;
// TAG:添加脚本功能点3
const context = {
commissionName,
location,
processSteps,
currentIndex: i,
isInMainUI,
priorityOptions,
npcWhiteList,
};
// 处理步骤
await Execute.processStep(step, context);
} catch (stepError) {
log.error(
"执行步骤 {step} 时出错: {error}",
i + 1,
stepError.message
);
// 继续执行下一步,不中断整个流程
}
// 每个步骤之间等待一段时间
await sleep(2000);
}
log.info("统一对话委托流程执行完成: {name}", commissionName);
return true;
} catch (error) {
log.error("执行统一对话委托流程时出错: {error}", error.message);
return false;
}
},
// 处理步骤配置优先选项和NPC白名单
processStepConfiguration: (
step,
defaultPriorityOptions,
defaultNpcWhiteList
) => {
let priorityOptions = [...defaultPriorityOptions];
let npcWhiteList = [...defaultNpcWhiteList];
// 如果步骤中包含自定义的优先选项和NPC白名单则使用它们
if (step.data && typeof step.data === "object") {
if (Array.isArray(step.data.priorityOptions)) {
priorityOptions = step.data.priorityOptions;
log.info("使用自定义优先选项: {options}", priorityOptions.join(", "));
}
if (Array.isArray(step.data.npcWhiteList)) {
npcWhiteList = step.data.npcWhiteList;
log.info("使用自定义NPC白名单: {npcs}", npcWhiteList.join(", "));
}
}
return { priorityOptions, npcWhiteList };
},
// 处理单个步骤
processStep: async (step, context) => {
if (typeof step === "string") {
// 简单格式处理
await Execute.processStringStep(step, context);
} else if (typeof step === "object") {
// JSON格式处理
await Execute.processObjectStep(step, context);
}
},
// 处理字符串格式的步骤
processStringStep: async (step, context) => {
if (step.endsWith(".json")) {
// 地图追踪文件
await StepProcessor.processMapTracking(
step,
context.commissionName,
context.location
);
} else if (step === "F") {
// 按F键并执行优化的自动剧情
log.info("执行自动剧情");
await DialogProcessor.executeOptimizedAutoTalk(
null,
5,
context.priorityOptions,
context.npcWhiteList,
context.isInMainUI
);
}
},
// 处理对象格式的步骤
processObjectStep: async (step, context) => {
if (step.note) {
log.info("步骤说明: {note}", step.note);
}
// 使用步骤处理器工厂来处理步骤
await StepProcessorFactory.process(step, context);
},
// 示例:注册自定义步骤处理器
registerCustomStepProcessors: () => {
// 注册等待步骤处理器
StepProcessorFactory.register("等待", async (step, context) => {
const waitTime = step.data || 1000;
log.info("等待 {time} 毫秒", waitTime);
await sleep(waitTime);
});
// 注册截图步骤处理器
StepProcessorFactory.register("截图", async (step, context) => {
const filename = step.data || `screenshot_${Date.now()}.png`;
log.info("截图保存为: {filename}", filename);
// 这里可以添加实际的截图逻辑
});
// 注册条件检查步骤处理器
StepProcessorFactory.register("条件检查", async (step, context) => {
const condition = step.data.condition;
const trueSteps = step.data.trueSteps || [];
const falseSteps = step.data.falseSteps || [];
log.info("执行条件检查: {condition}", condition);
// 这里可以添加条件判断逻辑
const conditionResult = true; // 示例结果
const stepsToExecute = conditionResult ? trueSteps : falseSteps;
for (const subStep of stepsToExecute) {
await Execute.processStep(subStep, context);
}
});
log.info("自定义步骤处理器注册完成");
},
// 处理对话步骤
processDialogStep: async (
step,
priorityOptions,
npcWhiteList,
isInMainUI
) => {
log.info("执行对话");
let skipCount = 2; // 默认跳过2次
// 处理对话选项
if (typeof step.data === "number") {
// 兼容旧版本如果data是数字则视为skipCount
skipCount = step.data;
} else if (typeof step.data === "object" && step.data.skipCount) {
// 新版本data是对象包含skipCount
skipCount = step.data.skipCount;
}
// 执行对话使用当前步骤的优先选项和NPC白名单
await DialogProcessor.executeOptimizedAutoTalk(
null,
skipCount,
priorityOptions,
npcWhiteList,
isInMainUI
);
},
};
const CommissionsFunc = {
// 计算两点之间的距离
calculateDistance: (point1, point2) => {
if (
!point1 ||
!point2 ||
!point1.X ||
!point1.Y ||
!point2.x ||
!point2.y
) {
log.warn("无效的位置数据");
return Infinity;
}
return Math.sqrt(
Math.pow(point1.X - point2.x, 2) + Math.pow(point1.Y - point2.y, 2)
);
},
// 获取委托的目标坐标(从路径追踪文件中获取最后一个坐标)
getCommissionTargetPosition: async (scriptPath) => {
try {
const scriptContent = await file.readText(scriptPath);
const pathData = JSON.parse(scriptContent);
if (!pathData.positions || pathData.positions.length === 0) {
log.warn("路径追踪文件 {path} 中没有有效的坐标数据", scriptPath);
return null;
}
const lastPosition = pathData.positions[pathData.positions.length - 1];
if (!lastPosition.x || !lastPosition.y) {
log.warn(
"路径追踪文件 {path} 的最后一个路径点缺少坐标数据",
scriptPath
);
return null;
}
log.debug(
"从脚本路径 {path} 获取到目标坐标: ({x}, {y})",
scriptPath,
lastPosition.x,
lastPosition.y
);
return {
x: lastPosition.x,
y: lastPosition.y,
};
} catch (error) {
log.error("获取委托目标坐标时出错: {error}", error.message);
return null;
}
},
// 执行带分支的对话委托流程从main_branch.js移植
executeTalkCommissionWithBranches: async (processPath) => {
try {
log.info("开始执行对话委托流程: {path}", processPath);
// 读取流程文件
const processContent = await file.readText(processPath);
// 解析流程内容
const branches = CommissionsFunc.parseProcessBranches(processContent);
// 确定要执行的分支
const branchToExecute = await CommissionsFunc.determineBranch(branches);
if (branchToExecute) {
log.info("执行分支: {id}", branchToExecute.id);
await Execute.executeUnifiedTalkProcess(branchToExecute.steps);
} else {
log.warn("没有找到匹配的分支,执行默认流程");
// 尝试解析整个内容作为单一流程
const steps = JSON.parse(processContent);
await Execute.executeUnifiedTalkProcess(steps);
}
} catch (error) {
log.error("执行对话委托流程出错: {error}", error.message);
}
},
// 解析流程文件中的分支从main_branch.js移植
parseProcessBranches: (content) => {
const branches = [];
const branchRegex =
/分支:(\d+)[\s\S]*?判断方法"([^"]+)"[\s\S]*?data:"([^"]+)"([\s\S]*?)(?=分支:|$)/g;
let match;
while ((match = branchRegex.exec(content)) !== null) {
const branchId = parseInt(match[1]);
const judgmentMethod = match[2];
const judgmentData = match[3];
const stepsContent = match[4].trim();
// 解析步骤
let steps = [];
try {
// 尝试解析JSON数组
const jsonContent = `[${stepsContent}]`;
steps = JSON.parse(jsonContent);
} catch (error) {
log.warn("解析分支{id}的步骤出错: {error}", branchId, error);
continue;
}
branches.push({
id: branchId,
method: judgmentMethod,
data: judgmentData,
steps: steps,
});
}
return branches;
},
// 确定要执行的分支从main_branch.js移植
determineBranch: async (branches) => {
for (const branch of branches) {
switch (branch.method) {
case "坐标":
if (await CommissionsFunc.checkCoordinateMatch(branch.data)) {
return branch;
}
break;
case "任务追踪":
if (await CommissionsFunc.checkTaskMatch(branch.data)) {
return branch;
}
break;
default:
log.warn("未知的判断方法: {method}", branch.method);
}
}
return null;
},
// 检查当前坐标是否匹配从main_branch.js移植
checkCoordinateMatch: async (coordData) => {
try {
const [targetX, targetY] = coordData
.split(",")
.map((c) => parseFloat(c.trim()));
// 获取当前委托位置
const playerPos = await CommissionsFunc.getCurrentCommissionPosition();
if (!playerPos) return false;
// 计算距离
const distance = CommissionsFunc.calculateDistance(playerPos, {
x: targetX,
y: targetY,
});
log.info(
"当前位置: ({x}, {y}),目标位置: ({tx}, {ty}),距离: {d}",
playerPos.x,
playerPos.y,
targetX,
targetY,
distance
);
// 如果距离小于阈值,认为匹配
return distance < 100; // 可以调整阈值
} catch (error) {
log.error("检查坐标匹配出错: {error}", error.message);
return false;
}
},
// 检查当前任务是否匹配从main_branch.js移植
checkTaskMatch: async (taskName) => {
try {
// 识别左上角任务区域文本
const taskRegion = { X: 75, Y: 240, WIDTH: 225, HEIGHT: 60 };
const taskResults = await Utils.easyOCR(taskRegion);
// 检查是否包含目标任务名称
for (let i = 0; i < taskResults.count; i++) {
const text = taskResults[i].text;
log.info(`任务区域识别文本: ${text}`);
if (text.includes(taskName)) {
log.info(`找到匹配任务: ${taskName}`);
return true;
}
}
log.info(`未找到匹配任务: ${taskName}`);
return false;
} catch (error) {
log.error("检查任务匹配出错: {error}", error.message);
return false;
}
},
// 获取当前委托位置(辅助函数)
getCurrentCommissionPosition: async () => {
try {
// 这里可以通过多种方式获取当前位置
// 1. 从大地图获取
await genshin.setBigMapZoomLevel(2);
const bigMapPosition = genshin.getPositionFromBigMap();
if (bigMapPosition) {
return bigMapPosition;
}
// 2. 从当前委托位置变量获取(如果有的话)
if (currentCommissionPosition) {
return currentCommissionPosition;
}
log.warn("无法获取当前委托位置");
return null;
} catch (error) {
log.error("获取当前委托位置时出错: {error}", error.message);
return null;
}
},
};
const Test = async () => {
Utils.iframe(Datas.OCR_REGIONS.Main_Dev[0]);
// 角色切换步骤使用示例:
// 在process.json文件中添加如下步骤
// {
// "type": "切换角色",
// "data": {
// "position": 1,
// "character": "枫原万叶"
// },
// "note": "切换第1号位为枫原万叶"
// }
// // 切换角色示例(可选)
// // 如果需要切换角色,可以取消注释下面的代码
// const switchRoleStep = {
// type: "切换角色",
// data: { position: 1, character: "枫原万叶" },
// note: "切换第1号位为枫原万叶",
// };
// await StepProcessor.processSwitchRole(switchRoleStep);
};
//Main
const Main = async () => {
log.debug("版本: {version}", VERSION);
try {
// 读取设置
const team = settings.team || "";
const elementTeam = settings.elementTeam || "";
const selectedProcess = settings.process_selector || "刷新剧情列表";
if (selectedProcess === "刷新剧情列表") {
// 刷新操作扫描所有process.json并更新设置
await refreshProcessList();
log.info("委托列表已刷新,请重新选择并运行");
} else {
// 解析选中的委托路径
const pathParts = selectedProcess.split('-');
// 确保至少有两个文件夹层级
if (pathParts.length < 2) {
throw new Error("无效的委托路径格式");
}
// 提取最后两个文件夹名
const folder1 = pathParts[pathParts.length - 2];
const folder2 = pathParts[pathParts.length - 1];
// 设置动态基础路径(倒数第二个文件夹之前的所有部分)
Datas.TALK_PROCESS_BASE_PATH = "process/"+pathParts.slice(0, pathParts.length - 2).join('/');
log.info("执行委托: {path}", selectedProcess);
log.debug("基础路径: {basePath}", Datas.TALK_PROCESS_BASE_PATH);
log.debug("文件夹1: {folder1}, 文件夹2: {folder2}", folder1, folder2);
await Execute.executeTalkCommission(folder1, folder2);
dispatcher.ClearAllTriggers();
}
} catch (error) {
log.error("执行出错: {error}", error.message);
errorlog();
}
};
// 刷新委托列表(保留完整路径结构)
async function refreshProcessList() {
// 读取所有process.json文件
const allFiles = await readFolder("process/", true);
// 筛选并处理符合条件的process.json文件
const processEntries = allFiles
.filter(file => file.fileName === "process.json")
.map(file => {
const pathSegments = file.folderPathArray;
// 确保路径中有"process"部分
const processIndex = pathSegments.indexOf("process");
if (processIndex === -1 || processIndex >= pathSegments.length - 2) {
throw new Error(`无效的路径结构: ${file.fullPath}`);
}
// 提取"process"之后的所有部分
const relativePath = pathSegments.slice(processIndex + 1);
// 确保至少有两个文件夹层级
if (relativePath.length < 2) {
throw new Error(`路径层级不足: ${file.fullPath}`);
}
// 创建选项名称(用连字符连接所有文件夹名)
const optionName = relativePath.join('-');
return {
name: optionName,
path: file.fullPath
};
});
// 创建选项列表(以"刷新剧情列表"开头)
const options = ["刷新剧情列表", ...processEntries.map(entry => entry.name)];
// 更新settings.json
await updateSettingsFile(options);
log.info("已更新{count}个委托选项", processEntries.length);
}
// 更新settings.json文件
async function updateSettingsFile(options) {
const settingsPath = "./settings.json";
let settingsArray;
try {
// 读取现有设置
const content = file.readTextSync(settingsPath);
settingsArray = JSON.parse(content);
} catch (e) {
// 文件不存在或解析失败时创建默认设置
throw new Error("设置文件不存在");
}
// 更新process_selector选项
const selectorIndex = settingsArray.findIndex(item => item.name === "process_selector");
if (selectorIndex !== -1) {
settingsArray[selectorIndex].options = options;
settingsArray[selectorIndex].default = "刷新剧情列表";
} else {
// 如果不存在则添加
settingsArray.push({
"name": "process_selector",
"type": "select",
"label": "可执行剧情列表",
"options": options,
"default": "刷新剧情列表"
});
}
// 写入更新后的设置
const success = file.writeTextSync(settingsPath, JSON.stringify(settingsArray, null, 2));
if (!success) {
throw new Error("写入设置文件失败");
}
}
// 文件夹读取函数(优化版)
async function readFolder(folderPath, onlyJson) {
log.info(`开始读取文件夹: ${folderPath}`);
const folderStack = [folderPath];
const files = [];
while (folderStack.length > 0) {
const currentPath = folderStack.pop();
const items = file.ReadPathSync(currentPath);
const subFolders = [];
for (const itemPath of items) {
if (file.IsFolder(itemPath)) {
subFolders.push(itemPath);
} else if (!onlyJson || itemPath.toLowerCase().endsWith(".json")) {
const pathParts = itemPath.split(/[\\/]/).filter(Boolean);
const fileName = pathParts.pop();
files.push({
fullPath: itemPath,
fileName: fileName,
folderPathArray: pathParts
});
}
}
// 保持原始顺序添加子文件夹
folderStack.push(...subFolders.reverse());
}
return files;
}
await Main();
})();