js: 七圣召唤七日历练全自动: 支持为特定的对手使用专门的打牌策略 (#1844)

* js: 七圣召唤七日历练全自动: 代码自动格式化

* js: 七圣召唤七日历练全自动: 支持为特定的对手使用专门的打牌策略
This commit is contained in:
Patrick-Ze
2025-09-10 08:19:46 +08:00
committed by GitHub
parent 98a7902274
commit 91e3b572d8
10 changed files with 305 additions and 170 deletions

View File

@@ -1 +0,0 @@
// 请复制对应的卡牌策略,否则执行默认配置

View File

@@ -1 +0,0 @@
// 请复制对应的卡牌策略,否则执行默认配置

View File

@@ -1 +0,0 @@
// 请复制对应的卡牌策略,否则执行默认配置

View File

@@ -1,8 +1,29 @@
脚本兼容绝大部分的行走角色,一般不需要切换专门的队伍,可以自行测试。
## 工作过程
2.0版本之后,支持挑战失败更换卡牌重新挑战。把卡牌策略文本复制到脚本目录的文件中即可。
如果某个打牌对手有专用策略时,脚本会使用对应分享码自动导入牌组,并使用对应策略进行打牌;
举一个例子,我最常用的卡牌策略是雷柯刻,这个卡组在我游戏中的名字是"优先卡组",那么我会在 js 自定义设置中输入这个名字之后我会打开脚本目录将这个卡牌策略复制到脚本的1号卡牌策略.txt文件中之后的以此类推
否则,将使用通用性最高的'雷神柯莱刻晴'进行打牌
密码 : BugGI
## 脚本配置
| 配置项 | 说明 |
| ---- | ---- |
| 默认牌组 | 填写你游戏中'雷神柯莱刻晴'队所在的牌组名 |
| 临时牌组 | 填写用来导入分享码的牌组名 |
| 密码 | 随便填 |
**注意:**
1. 两个牌组名不能留空,也不能是一样的名字
2. 这两个牌组都必须在你牌组列表页面的第一页
## 如何添加某个角色的专用打牌策略
通常只有默认的"雷神柯莱刻晴"打不过的情况才有必要创建专用的策略。
打牌策略编写很容易,只要你能在不使用牌的技能(可以烧牌换万能元素)的情况下赢得对局,就能简单编写对应的策略文件。
具体请参考脚本的`牌组策略`文件夹下的`_策略模板.txt`文件。
如果你发现默认策略打不赢,欢迎提交你编写的策略。

View File

@@ -1,12 +1,14 @@
// 存储挑战玩家信息
let textArray = [];
let skipNum = 0;
// 切换到指定的队伍
async function switchCardTeam(Name) {
async function switchCardTeam(Name, shareCode) {
let captureRegion = captureGameRegion();
let teamName = captureRegion.find(RecognitionObject.ocr(1305, 793, 206, 46));
log.info("当前队伍名称: {text}", teamName.text);
if (teamName.text != Name) {
click(1312, 812); //点击队伍名称的糟糕UI
await sleep(1000);
async function selectTargetTeam(targetTeam) {
moveMouseTo(100, 200);
leftButtonDown();
// 不能一次移动太多,否则会丢拖动
@@ -22,56 +24,75 @@ async function switchCardTeam(Name) {
for (let i = 0; i < 4; i++) {
let x = 135 + 463 * i;
let res = captureRegion.find(RecognitionObject.ocr(x, 762, 230, 46));
if (res.text == Name) {
if (res.text == targetTeam) {
log.info("切换至队伍: {text}", res.text);
res.click();
await sleep(500);
click(1164, 1016); // 选择
await sleep(4000); // 等待"出战牌组"的强制延时框消失
break;
}
}
}
}
async function runCardStrategyFromFolder(folderName) {
try {
// 构建策略文件路径
const strategyFilePath = `${folderName}.txt`;
// 读取策略文件内容
const strategyContent = await file.readText(strategyFilePath);
// 检查策略内容是否为空
if (!strategyContent || strategyContent.trim() === '') {
log.error('策略文件内容为空');
return false;
}
log.info(`开始执行 ${folderName} `);
// 执行策略
await dispatcher.runTask(new SoloTask("AutoGeniusInvokation", {
strategy: strategyContent
}));
if (teamName.text != Name || settings.overwritePartyName == Name) {
click(1312, 812); //点击队伍名称的糟糕UI
await sleep(1000);
await selectTargetTeam(Name);
} else {
return true;
} catch (error) {
log.error(`执行策略失败: ${error},默认使用独立任务中的设置`);
return false;
}
async function stopNow() {
await sleep(250);
click(1795, 465); // 点空白处以便立即终止延时对话框
await sleep(250);
}
let userDefault = false;
if (shareCode) {
captureRegion = captureGameRegion();
let res = captureRegion.find(RecognitionObject.ocr(1140, 732, 83, 55));
if (res.text === "确认") {
res.click();
} else {
click(731, 998); // 编辑牌组
}
await sleep(800);
click(1756, 48); // ... 按钮
await sleep(200);
click(1546, 178); // 使用分享码
await sleep(500);
click(960, 520); // 输入区域
await sleep(500);
log.info("输入分享码 {0}", shareCode);
await inputText(shareCode);
await sleep(500);
click(1166, 750); // 导入
await stopNow();
click(1720, 1020); // 保存
await stopNow();
captureRegion = captureGameRegion();
res = captureRegion.find(RecognitionObject.ocr(770, 516, 381, 43));
if (res.text.includes("无法出战")) {
log.error(res.text);
userDefault = true;
await sleep(500);
click(1162, 760); // 保存修改
await stopNow();
}
click(1843, 46); // 关闭
await sleep(1000);
}
if (userDefault) {
log.info("分享码导入的牌组无法出战,切换到默认牌组: {0}", settings.defaultPartyName);
await selectTargetTeam(settings.defaultPartyName);
}
click(1164, 1016); // 选择
await sleep(4000); // 等待"出战牌组"的强制延时框消失
return !userDefault;
}
(async function () {
// 存储挑战玩家信息
let textArray = [];
let skipNum = 0;
let teamName = settings.partyName1;
let folderName = "1号卡牌策略";
/**
* 判断任务是否已刷新
* @param {string} filePath - 存储最后完成时间的文件路径
@@ -87,13 +108,13 @@ async function runCardStrategyFromFolder(folderName) {
*/
async function isTaskRefreshed(filePath, options = {}) {
const {
refreshType = 'hourly', // 默认每小时刷新
customHours = 24, // 自定义刷新小时数默认24
dailyHour = 4, // 每日刷新默认凌晨4点
weeklyDay = 1, // 每周刷新默认周一(0是周日)
weeklyHour = 4, // 每周刷新默认凌晨4点
monthlyDay = 1, // 每月刷新默认第1天
monthlyHour = 4 // 每月刷新默认凌晨4点
refreshType = "hourly", // 默认每小时刷新
customHours = 24, // 自定义刷新小时数默认24
dailyHour = 4, // 每日刷新默认凌晨4点
weeklyDay = 1, // 每周刷新默认周一(0是周日)
weeklyHour = 4, // 每周刷新默认凌晨4点
monthlyDay = 1, // 每月刷新默认第1天
monthlyHour = 4, // 每月刷新默认凌晨4点
} = options;
try {
@@ -101,21 +122,19 @@ async function isTaskRefreshed(filePath, options = {}) {
let content = await file.readText(filePath);
const lastTime = new Date(content);
const nowTime = new Date();
let shouldRefresh = false;
switch (refreshType) {
case 'hourly': // 每小时刷新
shouldRefresh = (nowTime - lastTime) >= 3600 * 1000;
case "hourly": // 每小时刷新
shouldRefresh = nowTime - lastTime >= 3600 * 1000;
break;
case 'daily': // 每天固定时间刷新
case "daily": // 每天固定时间刷新
// 检查是否已经过了当天的刷新时间
const todayRefresh = new Date(nowTime);
todayRefresh.setHours(dailyHour, 0, 0, 0);
// 如果当前时间已经过了今天的刷新时间,检查上次完成时间是否在今天刷新之前
if (nowTime >= todayRefresh) {
shouldRefresh = lastTime < todayRefresh;
@@ -126,15 +145,15 @@ async function isTaskRefreshed(filePath, options = {}) {
shouldRefresh = lastTime < yesterdayRefresh;
}
break;
case 'weekly': // 每周固定时间刷新
case "weekly": // 每周固定时间刷新
// 获取本周的刷新时间
const thisWeekRefresh = new Date(nowTime);
// 计算与本周指定星期几的差值
const dayDiff = (thisWeekRefresh.getDay() - weeklyDay + 7) % 7;
thisWeekRefresh.setDate(thisWeekRefresh.getDate() - dayDiff);
thisWeekRefresh.setHours(weeklyHour, 0, 0, 0);
// 如果当前时间已经过了本周的刷新时间
if (nowTime >= thisWeekRefresh) {
shouldRefresh = lastTime < thisWeekRefresh;
@@ -145,14 +164,14 @@ async function isTaskRefreshed(filePath, options = {}) {
shouldRefresh = lastTime < lastWeekRefresh;
}
break;
case 'monthly': // 每月固定时间刷新
case "monthly": // 每月固定时间刷新
// 获取本月的刷新时间
const thisMonthRefresh = new Date(nowTime);
// 设置为本月指定日期的凌晨
thisMonthRefresh.setDate(monthlyDay);
thisMonthRefresh.setHours(monthlyHour, 0, 0, 0);
// 如果当前时间已经过了本月的刷新时间
if (nowTime >= thisMonthRefresh) {
shouldRefresh = lastTime < thisMonthRefresh;
@@ -164,43 +183,38 @@ async function isTaskRefreshed(filePath, options = {}) {
}
break;
case 'custom': // 自定义小时数刷新
shouldRefresh = (nowTime - lastTime) >= customHours * 3600 * 1000;
case "custom": // 自定义小时数刷新
shouldRefresh = nowTime - lastTime >= customHours * 3600 * 1000;
break;
default:
throw new Error(`未知的刷新类型: ${refreshType}`);
}
// 如果文件内容无效或不存在,视为需要刷新
if (!content || isNaN(lastTime.getTime())) {
await file.writeText(filePath, '');
await file.writeText(filePath, "");
shouldRefresh = true;
}
if (shouldRefresh) {
notification.send(`七圣召唤七日历练周期已经刷新,执行脚本`);
return true;
} else {
log.info(`七圣召唤七日历练未刷新`);
return false;
}
} catch (error) {
// 如果文件不存在创建新文件并返回true(视为需要刷新)
const createResult = await file.writeText(filePath, '');
const createResult = await file.writeText(filePath, "");
if (createResult) {
log.info("创建新时间记录文件成功,执行脚本");
return true;
}
else throw new Error(`创建新文件失败`);
} else throw new Error(`创建新文件失败`);
}
}
//检查挑战结果 await checkChallengeResults();
async function checkChallengeResults() {
const region1 = RecognitionObject.ocr(785, 200, 380, 270); // 结果区域
@@ -216,8 +230,7 @@ async function checkChallengeResults() {
await autoConversation();
await sleep(1000);
return;
}
else if (res1.text.includes("对局胜利")) {
} else if (res1.text.includes("对局胜利")) {
log.info("对局胜利");
await sleep(1000);
click(754, 915); //退出挑战
@@ -225,7 +238,6 @@ async function checkChallengeResults() {
await autoConversation();
await sleep(1000);
return;
} else {
log.info("挑战异常中断,对局失败");
await sleep(1000);
@@ -233,13 +245,13 @@ async function checkChallengeResults() {
await sleep(500);
click(960, 540);
await sleep(500);
click(1860,50); //点击齿轮图标
click(1860, 50); //点击齿轮图标
await sleep(1000);
let res2 = captureGameRegion().find(region2);
if (res2.text.includes('设置')) click(1600, 260);//点击退出-选项4
else click(1600, 200);//点击退出-选项3
if (res2.text.includes("设置")) click(1600, 260); //点击退出-选项4
else click(1600, 200); //点击退出-选项3
await sleep(1000);
click(1180, 756);//点击确认
click(1180, 756); //点击确认
await sleep(6000);
click(754, 915); //退出挑战
await sleep(4000);
@@ -258,22 +270,21 @@ async function autoConversation() {
log.info("准备开始对话");
//最多10次对话
while (talkTime < 30) {
let talk = captureGameRegion().find(talkRo);
if (talk.isExist()) {
let talk = captureGameRegion().find(talkRo);
if (talk.isExist()) {
await sleep(300);
keyPress("VK_SPACE");
await sleep(300);
keyPress("F");
talkTimes++;
await sleep(1500);
await sleep(1500);
} else if (talkTimes) {
log.info("对话结束");
return;
}
talkTime++;
await sleep(1200);
}
else if(talkTimes){
log.info("对话结束");
return ;
}
talkTime++;
await sleep(1200);
}
throw new Error("对话时间超时");
}
@@ -308,13 +319,13 @@ const detectCardPlayer = async () => {
{ x: 1680, y: 780, action: async () => await gotoTable2() }, // 2号桌
{ x: 1645, y: 575, action: async () => await gotoTable3() }, // 3号桌
{ x: 1460, y: 360, action: async () => await gotoTable4() }, // 4号桌
{ x: 1550, y: 0 , action: async () => await gotoTable5() }, // 包间1
{ x: 1550, y: 0, action: async () => await gotoTable5() }, // 包间1
{ x: 1130, y: 520, action: async () => await gotoTable6() }, // 包间2
];
keyPress("M");
await sleep(1200);
await genshin.setBigMapZoomLevel(1.0); //放大地图
await genshin.setBigMapZoomLevel(1.0); //放大地图
await sleep(300);
//地图拖动到指定位置
@@ -386,7 +397,7 @@ async function captureAndStoreTexts() {
for (const pos of positions) {
// 创建OCR识别区域
const ocrRo = RecognitionObject.ocr(pos.x, pos.y, width, height); //挑战者名字区域
const ocrRo2 = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/completed.png"),pos.x, pos.y + 60, width, height+80);
const ocrRo2 = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/completed.png"), pos.x, pos.y + 60, width, height + 80);
// 在指定区域进行OCR识别
const result = captureRegion.find(ocrRo);
let res2 = captureRegion.find(ocrRo2);
@@ -414,10 +425,10 @@ async function captureAndStoreTexts() {
function isTextMatch(target, source) {
// 如果完全匹配直接返回true
if (target === source) return true;
// 如果长度不同,直接不匹配
if (target.length !== source.length) return false;
let diffCount = 0;
for (let i = 0; i < target.length; i++) {
if (target[i] !== source[i]) {
@@ -430,6 +441,48 @@ function isTextMatch(target, source) {
return true;
}
// 获取某个角色的专用牌组的分享码。如无专用策略,则返回 null
function getShareCodeOfCharStrategy(charName) {
const allFilesRaw = file.ReadPathSync("牌组策略");
let strategyMap = {};
for (const filePath of allFilesRaw) {
if (filePath.endsWith(".txt")) {
const parts = filePath.split("\\");
const fileName = parts.slice(-1)[0];
const baseName = fileName.split(".").slice(0, -1).join(".");
strategyMap[baseName] = filePath;
}
}
const content = file.readTextSync("牌组策略/雷神柯莱刻晴.txt");
let result = { team: settings.defaultPartyName, shareCode: null, defaultContent: content, content: content };
const matchFile = strategyMap[charName];
if (matchFile) {
const content = file.readTextSync(matchFile);
let shareCode = null;
for (const line of content.split("\n")) {
const trimmedLine = line.trim();
if (trimmedLine.startsWith("//") && trimmedLine.includes("shareCode=")) {
const parts = trimmedLine.split("=");
if (parts[1]) {
shareCode = parts.slice(1, parts.length).join("=");
break;
}
}
}
if (shareCode) {
result.team = settings.overwritePartyName;
result.shareCode = shareCode;
result.content = content;
log.debug("charName={0}, shareCode={1}", charName, shareCode);
} else {
log.error("策略文件中未找到有效的shareCode: {0}", matchFile);
}
}
log.info("使用牌组{0}与{1}对战", result.team, charName);
return result;
}
//检查是否有对应的挑战对手
async function searchAndClickTexts() {
middleButtonClick();
@@ -469,7 +522,8 @@ async function searchAndClickTexts() {
res.click();
await sleep(500);
await keyMouseScript.runFile(`assets/ALT释放.json`);
await Playcards();
const strategy = getShareCodeOfCharStrategy(textArray[index].text);
await Playcards(strategy);
// 从数组中移除已处理的文本
textArray.splice(index, 1);
@@ -534,17 +588,18 @@ async function waitOrCheckMaxCoin(wait_time_ms) {
}
//函数:对话和打牌
async function Playcards() {
async function Playcards(strategy) {
await sleep(800); //略微俯视,避免名字出现在选项框附近,导致错误点击
moveMouseBy(0, 1030);
await sleep(1000);
await autoConversation();
log.info("对话完成");
await sleep(1500);
await switchCardTeam(teamName);
const success = await switchCardTeam(strategy.team, strategy.shareCode);
const content = success ? strategy.content : strategy.defaultContent;
click(1610, 900); //点击挑战
await waitOrCheckMaxCoin(8000);
await runCardStrategyFromFolder(folderName);
await dispatcher.runTask(new SoloTask("AutoGeniusInvokation", { strategy: content }));
await sleep(3000);
await checkChallengeResults();
await sleep(1000);
@@ -667,56 +722,42 @@ async function gotoTable6() {
}
async function main() {
//主流程
const nowTime = new Date();
log.info(`前往猫尾酒馆`);
await gotoTavern();
await captureAndStoreTexts();
if (textArray.length != 0) {
await detectCardPlayer();
await searchAndClickTexts();
}
for (let i = 0; i < 20; i++) {
//循环兜底,避免角色未到达指定位置
if (textArray.length === 0) break;
//主流程
const nowTime = new Date();
log.info(`前往猫尾酒馆`);
await gotoTavern();
await detectCardPlayer();
await searchAndClickTexts();
}
await genshin.returnMainUi();
await captureAndStoreTexts();
notification.send(`打牌结束、剩余挑战人数:${textArray.length}`);
// 更新最后完成时间
if(textArray.length === 0) await file.writeText("assets/weekly.txt", nowTime.toISOString());
await captureAndStoreTexts();
if (textArray.length != 0) {
await detectCardPlayer();
await searchAndClickTexts();
}
for (let i = 0; i < 20; i++) {
//循环兜底,避免角色未到达指定位置
if (textArray.length === 0) break;
await gotoTavern();
await detectCardPlayer();
await searchAndClickTexts();
}
await genshin.returnMainUi();
await captureAndStoreTexts();
notification.send(`打牌结束、剩余挑战人数:${textArray.length}`);
// 更新最后完成时间
if (textArray.length === 0) {
await file.writeText("assets/weekly.txt", nowTime.toISOString());
}
}
if( await isTaskRefreshed("assets/weekly.txt", {
refreshType: 'weekly',
weeklyDay: 1, // 周一
weeklyHour: 4 // 凌晨4点
})){
if(settings.partyName1) await main();
teamName = settings.partyName2;
folderName = "2号卡牌策略";
if(textArray.length != 0 && settings.partyName2){
log.info(`尝试2号卡牌策略`);
await main();
}
teamName = settings.partyName3;
folderName = "3号卡牌策略";
if(textArray.length != 0 && settings.partyName3){
log.info(`尝试3号卡牌策略`);
await main();
}
}
(async function () {
const refresh = {
refreshType: "weekly",
weeklyDay: 1, // 周一
weeklyHour: 4, // 凌晨4点
};
if (!settings.defaultPartyName || !settings.overwritePartyName || settings.defaultPartyName == settings.overwritePartyName) {
log.error("需要在JS脚本配置中设置两个牌组且名称不能相同");
return;
}
if ((await isTaskRefreshed("assets/weekly.txt"), refresh)) {
await main();
}
})();

View File

@@ -1,8 +1,8 @@
{
"manifest_version": 1,
"name": "打牌一条龙",
"version": "2.2",
"description": "详见README.md",
"version": "2.3",
"description": "完成每周的七圣召唤七日历练(来客挑战)。详见README.md",
"tags": [
"七圣召唤"
],
@@ -10,8 +10,12 @@
{
"name": "柒叶子",
"links": "https://github.com/5117600049"
},
{
"name": "Ayaka-Main",
"link": "https://github.com/Patrick-Ze"
}
],
"settings_ui": "settings.json",
"main": "main.js"
}
}

View File

@@ -1,22 +1,17 @@
[
{
"name": "partyName1",
"name": "defaultPartyName",
"type": "input-text",
"label": "请输入游戏中的卡组名称(1号卡牌策略)"
"label": "默认使用的游戏牌组名称('雷神柯莱刻晴'队所在的牌组)"
},
{
"name": "partyName2",
"name": "overwritePartyName",
"type": "input-text",
"label": "请输入游戏中的卡组名称(2号卡牌策略)"
},
{
"name": "partyName3",
"type": "input-text",
"label": "请输入游戏中的卡组名称(3号卡牌策略)"
"label": "临时使用的游戏牌组名称(此牌组会被打牌策略的分享码覆盖)"
},
{
"name": "passWord",
"type": "input-text",
"label": "启动密码在README.md"
"label": "请阅读脚本说明或README.md文件获取启动密码后填写到此处"
}
]

View File

@@ -0,0 +1,22 @@
// shareCode=AXGxuSUMA4FR83oQCCGR9HsQCDGh9bQQDEEx9rUQDFFB97YQDGFR+LcQDHFh+bgQDIEB
// 作者:
// shareCode为该策略使用的牌组的分享码脚本在运行时将先导入牌组再执行打牌策略。
// 如果你要创建你自己的牌组请基于此模板中的shareCode的牌组来创建该牌组选取事件牌、支援牌、装备牌中商店最上面一行的卡牌构成最大可能确保尽可能多的用户已经拥有这些牌组
// 通常只有"雷神柯莱刻晴"打不过的情况才有必要创建专用的策略。
// 以下是打牌策略示例,角色从左往右编号,技能从右往左编号。详情见 https://bettergi.com/feats/task/tcg.html
角色定义:
角色1=雷神
角色2=柯莱
角色3=刻晴
---
策略定义:
雷神 使用 技能3
雷神 使用 技能2
雷神 使用 技能1
柯莱 使用 技能1
刻晴 使用 技能2
刻晴 使用 技能1

View File

@@ -0,0 +1,23 @@
// shareCode=AXFRuSUMA4FR83oQCCGR9HsQCDGh9bQQDEEx9rUQDFFB97YQDGFR+LcQDHFh+bgQDIEB
// 作者: Ayaka-Main
角色定义:
角色1=神里绫华
角色2=砂糖
角色3=琴
---
策略定义:
砂糖 使用 技能2
砂糖 使用 技能2
砂糖 使用 技能1
砂糖 使用 技能3
砂糖 使用 技能2
// 正常来说砂糖就已经能够解决战斗
琴 使用 技能2
琴 使用 技能2
琴 使用 技能2
琴 使用 技能1
琴 使用 技能2
琴 使用 技能2
琴 使用 技能2

View File

@@ -0,0 +1,32 @@
// shareCode=A3EBuTAMAoHB83oQCCGR9HsQCDGh9bQQDEEx9rUQDFFB97YQDGFR+LcQDHFh+bgQDIEB
// 作者:我欲乘风
角色定义:
角色1=雷神
角色2=柯莱
角色3=刻晴
---
策略定义:
雷神 使用 技能3
雷神 使用 技能2
雷神 使用 技能1
柯莱 使用 技能1
刻晴 使用 技能2
刻晴 使用 技能1
//
刻晴 使用 技能2
刻晴 使用 技能2
刻晴 使用 技能2
刻晴 使用 技能1
柯莱 使用 技能2
柯莱 使用 技能2
柯莱 使用 技能1
雷神 使用 技能3
雷神 使用 技能3
雷神 使用 技能1