Files

383 lines
13 KiB
JavaScript
Raw Permalink 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.

eval(file.readTextSync("lib/ocr.js"));
const rewardIcon = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RewardIcon.png"), 1430, 760, 100, 70);
const tavernRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/TavernIcon.png"), 800, 450, 500, 330);
const adventurersRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/AdventurersGuild.png"), 800, 450, 500, 330);
const stateMachine = {
"任意界面": { "猫尾酒馆进门": teleportToTheCatsTail },
"猫尾酒馆进门": { "邀请界面": gotoInvitationPanel },
"邀请界面": { "牌组编辑器界面": gotoDeckDesignPanel, "对战": startChallenge },
"牌组编辑器界面": { "邀请界面": goBackInvitationPanel },
"对战": {}, // 对战结束后将状态维护至 猫尾酒馆进门
};
let currentState = "任意界面";
const outFile = "卡牌熟练度.json";
let allProficiencys = {};
try {
allProficiencys = JSON.parse(file.readTextSync(outFile));
} catch (error) {
log.debug("历史熟练度文件不存在");
}
(async function () {
setGameMetrics(1920, 1080, 1.25);
log.info("首次运行前请阅读脚本说明");
log.info("请确保已进入双人联机模式");
await sleep(1000);
let runMode = settings.runMode;
if (!runMode) {
runMode = "作为主账号:速刷熟练度";
log.info("未配置运行模式,默认以{0}模式运行", runMode);
}
if (runMode === "作为主账号:速刷熟练度") {
await runAsMain(settings.targetProficiency);
} else if (runMode === "作为辅助账号:快速投降") {
await runAsPartner();
} else if (runMode === "扫描当前账号卡牌熟练度") {
currentState = await getCurrentState();
await goToTargetState("牌组编辑器界面");
allProficiencys = await scanCardsProficiency(true);
file.writeTextSync(outFile, JSON.stringify(allProficiencys, null, 2));
log.info("卡牌熟练度数据已写入{0}", outFile);
} else {
log.error("不支持的运行模式: {0}", runMode);
}
})();
// 需要刷熟练度的角色的执行逻辑
async function runAsMain(targetProficiency) {
log.info("主账号开始工作如需停止请按下你设置的BetterGI停止按键");
if (!targetProficiency) {
targetProficiency = 30;
}
let loopCount = -1;
currentState = await getCurrentState();
log.info("当前界面: {0}", currentState);
await goToTargetState("牌组编辑器界面");
let teamProficiencys = await scanCardsProficiency(false);
loopCount = targetProficiency - Math.min(...Object.values(teamProficiencys));
if (loopCount > 0) {
log.info("需重复执行{0}次以达成{1}熟练度", loopCount, targetProficiency);
await goToTargetState("邀请界面");
}
while (loopCount > 0) {
await goToTargetState("对战");
try {
await sleep(10);
} catch (error) {
log.info("用户停止运行");
break;
}
loopCount--;
log.info(`对局结束,当前熟练度 ${targetProficiency - loopCount}/${targetProficiency}`);
currentState = "猫尾酒馆进门";
await waitTpFinish();
};
// 执行过对战,且对战结束
if (loopCount === 0 && currentState === "猫尾酒馆进门") {
await goToTargetState("牌组编辑器界面");
teamProficiencys = await scanCardsProficiency(false, true);
}
Object.assign(allProficiencys, teamProficiencys);
recommendNextTeam(targetProficiency);
file.writeTextSync(outFile, JSON.stringify(allProficiencys, null, 2));
}
// 小号的执行逻辑
async function runAsPartner() {
log.info("辅助账号开始工作如需停止请按下你设置的BetterGI停止按键");
while (true) {
log.info("等待对局邀请");
await waitForTextAppear("点击进行准备", [820, 70, 296, 31], 1800000);
keyPress("Y");
await recognizeTextAndClick("接受", [1177, 713, 82, 55]);
log.info("等待加载");
await waitForTextAppear("七圣召唤", [902, 870, 119, 46]);
await waitForTextAppear("请选择要替换的手牌", [847, 232, 229, 39], 30000);
log.info("选择初始手牌");
await recognizeTextAndClick("确定", [937, 922, 82, 54], 30000);
log.info("等待进入牌桌界面");
await waitForTextAppear("出战角色", [1766, 850, 118, 43]);
log.info("认输");
click(1864, 46);
await sleep(500);
await recognizeTextAndClick("放弃对局", [1543, 172, 112, 43]);
await waitForTextAppear("确定要放弃", [785, 497, 339, 39]);
await recognizeTextAndClick("确认", [1142, 732, 78, 51]);
await waitForTextAppear("退出挑战", [913, 893, 130, 43], 60000);
await sleep(300);
await recognizeTextAndClick("退出挑战", [913, 893, 130, 43]);
log.info("退出挑战");
try {
await sleep(3000);
} catch (error) {
log.info("用户停止运行");
break;
}
}
}
/** 从刚传送至猫尾酒馆的状态前往邀请界面 */
async function gotoInvitationPanel() {
keyDown("MBUTTON");
await sleep(500);
keyDown("A");
await sleep(1500);
keyUp("A");
await sleep(1000);
await keyMouseScript.runFile(`assets/ALT点击.json`);
await sleep(500);
await recognizeTextAndClick("联机对局", [1217, 411, 220, 280]);
await sleep(500);
await keyMouseScript.runFile(`assets/ALT释放.json`);
}
/** 从邀请界面前往牌组编辑器界面 */
async function gotoDeckDesignPanel() {
click(1306, 747);
log.info("等待进入牌组界面");
await recognizeTextAndClick("编辑牌组", [733, 997, 129, 48]);
await waitForTextAppear("更改牌组外观", [1592, 186, 139, 35]);
}
/** 从牌组编辑器界面返回邀请界面 */
async function goBackInvitationPanel() {
keyPress("VK_ESCAPE");
await waitForTextAppear("更改牌组外观", [1592, 186, 139, 35]);
keyPress("VK_ESCAPE");
await waitForTextAppear("编辑牌组", [733, 997, 129, 48]);
keyPress("VK_ESCAPE");
await waitForTextAppear("我方出战牌组", [1248, 688, 156, 36]);
}
/** 从邀请界面进入对战模式(并完成对战) */
async function startChallenge() {
log.info("邀请队友");
for (let i = 0; i < 60; i++) {
await recognizeTextAndClick("邀请队友", [1332, 886, 130, 49]);
let r = await waitForTextAppear("正在匹配对局", [870, 385, 184, 42], 500);
if (r.success) {
break;
}
await sleep(500);
}
log.info("等待对方同意");
await waitForTextAppear("正在匹配对局", [871, 386, 182, 39], 30000);
log.info("等待加载");
await waitForTextAppear("七圣召唤", [902, 870, 119, 46]);
await waitForTextAppear("请选择要替换的手牌", [847, 232, 229, 39], 30000);
log.info("选择初始手牌");
await recognizeTextAndClick("确定", [937, 922, 82, 54], 30000);
log.info("等待对方认输");
await waitForTextAppear("退出挑战", [913, 893, 130, 43], 60000);
await sleep(300);
await recognizeTextAndClick("退出挑战", [913, 893, 130, 43]);
currentState = "猫尾酒馆进门";
}
function recommendNextTeam(targetProficiency) {
log.info("当前牌组中卡牌已达成熟练度目标,请更换牌组内角色");
const nextTeam = Object.fromEntries(
Object.entries(allProficiencys)
.filter(([_, v]) => v < targetProficiency)
.slice(0, 3)
);
if (Object.keys(nextTeam).length > 0) {
let text = JSON.stringify(nextTeam).replace(/[{}"]/g, "");
log.info("熟练度未满的角色推荐: {0}", text);
}
}
/**
* 等待传送结束
* @param {Int} timeout 单位为ms
* @note 参考了七圣召唤七日历练脚本
*/
async function waitTpFinish(timeout = 30000) {
const region = RecognitionObject.ocr(1690, 230, 75, 350); // 队伍名称区域
const startTime = new Date();
await sleep(1000); //点击传送后等待一段时间避免误判
while (new Date() - startTime < timeout) {
let res = captureGameRegion().find(region);
if (!res.isEmpty()) {
await sleep(600); //传送结束后有僵直
return;
}
await sleep(100);
}
throw new Error("传送时间超时");
}
/** 传送到猫尾酒馆并等待传送后摇结束 */
async function teleportToTheCatsTail() {
await genshin.moveMapTo(-867, 2281, "蒙德");
await genshin.setBigMapZoomLevel(1.0);
let clickIcon = null;
for (let i = 0; i < 5; i++) {
const region = captureGameRegion();
const tarvern = region.find(tavernRo);
clickIcon = tarvern.isExist() ? tarvern : region.find(adventurersRo);
if (clickIcon.isExist()) {
clickIcon.click();
await sleep(500);
break;
}
await sleep(500);
}
if (!(clickIcon && clickIcon.isExist())) {
throw new Error("找不到猫尾酒馆如果2P标志遮挡了酒馆图标请将2P玩家传送至别处");
}
await recognizeTextAndClick("猫尾酒馆", [1320, 560, 300, 410]);
await recognizeTextAndClick("传送至", [1580, 980, 225, 59]);
await waitTpFinish();
}
async function getCurrentState() {
let window = captureGameRegion();
const invite = window.find(RecognitionObject.ocr(1248, 688, 156, 36));
if (invite.text && invite.text.includes("我方出战牌组")) {
return "邀请界面";
}
const deck = window.find(RecognitionObject.ocr(1592, 186, 139, 35));
if (deck.text && deck.text.includes("更改牌组外观")) {
return "牌组编辑器界面";
}
return "任意界面";
}
async function scanCardsProficiency(scanAll = false, getReward = false) {
let proficiencys = {};
let last_char = "";
let character = null;
let retry = 0;
for (let i = 0; i < 5; i++) {
if (scanAll) {
click(198, 717);
} else {
click(700, 276);
}
let r = await waitForTextAppear("卡牌", [867, 266, 68, 47], 500);
if (r.success) {
break;
}
}
while (true) {
let captureRegion = captureGameRegion();
if (getReward) {
const icon = captureRegion.find(rewardIcon);
if (icon.isExist()) {
icon.click();
await sleep(200);
icon.click();
await sleep(200);
}
}
let proficiency = null;
let proficiencyResult = captureRegion.find(RecognitionObject.ocr(855, 780, 210, 40));
if (proficiencyResult.text) {
const match = proficiencyResult.text.match(/(\d+)/);
proficiency = parseInt(match[0]);
}
character = null;
let ch_result = captureRegion.find(RecognitionObject.ocr(851, 326, 398, 65));
if (ch_result.text) {
character = ch_result.text.trim().replace(/t/g, "七");
} else {
retry++;
if (retry >= 3) {
const skill = captureRegion.find(RecognitionObject.ocr(897, 441, 300, 58));
if (skill.text) {
log.warn("OCR无法识别当前角色名称使用技能名作为替代");
character = `?${skill.text.trim()}`;
} else {
log.warn("OCR无法识别当前角色名称跳过");
click(1635, 538);
retry = 0;
continue;
}
}
}
if (last_char === character) {
log.info("已到达角色列表末尾");
keyPress("VK_ESCAPE");
await sleep(400);
break;
}
if (proficiency !== null && character !== null) {
proficiencys[character] = proficiency;
log.info("{ch} = {v}", character, proficiency);
click(1635, 538);
retry = 0;
last_char = character;
}
await sleep(400);
}
const sortedProficiencys = Object.entries(proficiencys).sort((a, b) => b[1] - a[1]);
proficiencys = Object.fromEntries(sortedProficiencys);
return proficiencys;
}
/**
* 使用BFS方法计算应当如何到达目标状态
*/
function searchPathWithBFS(start, target) {
const queue = [[start, []]];
const visited = new Set();
while (queue.length > 0) {
const [current, ops] = queue.shift();
if (current === target) return ops;
if (visited.has(current)) continue;
visited.add(current);
const transitions = stateMachine[current] || {};
for (const [nextState, op] of Object.entries(transitions)) {
queue.push([nextState, [...ops, op]]);
}
}
// 无法到达时走最保守的路径
if (start !== "任意界面") {
return searchPathWithBFS("任意界面", target);
}
return null;
}
async function goToTargetState(target) {
// log.info("切换状态: {0} -> {1}", currentState, target);
const pathList = searchPathWithBFS(currentState, target);
for (const fn of pathList) {
// log.info("执行函数: {0}", fn.name);
await fn();
}
currentState = target;
}