js:锄地一条龙更新,新增js贵重物品识别 (#2755)

* js:锄地一条龙

从了汐姐姐了

* js:摩拉&原石识别

* Update main.js
This commit is contained in:
mno
2026-01-21 20:52:01 +08:00
committed by GitHub
parent b53d35b2e7
commit 9b3e8f7988
37 changed files with 399 additions and 42 deletions

View File

@@ -37,7 +37,7 @@
- - 选项 **输出地图追踪文件** 会将选择的路线读取并分组输出到js文件夹下pathingOut文件夹
- - 选项 **强制刷新所有运行记录** 用于清除js记录的运行历史
- **选择执行第几个路径组:** 本js支持分组运行地图追踪分组方式详见后续选项需要分组运行时请确保精英目标数量小怪目标数量各个路径组的标签等信息【完全相同】复制配置组时未知原因无法正确复制配置请不要使用
- 如果你需要分组执行请先建立和组数对应的配置组分别添加本js路径组一要【排除】的标签填写需要完全禁用的标签如蕈兽路径组二要【选择】的标签填写需要分配到路径组二的路线的标签如小怪不同配置组的js中选择对应的配队和路径组编号其他配置保持默认的情况下即可实现精英和小怪分队伍和配置组锄地更多路径组数量以此类推
- 如果你需要分组执行请先建立和组数对应的配置组分别添加本js路径组一要【排除】的标签填写需要完全禁用的标签如蕈兽路径组二要【选择】的标签填写需要分配到路径组二的路线的标签如小怪不同配置组的js中选择对应的配队和路径组编号其他配置保持默认的情况下即可实现精英和小怪分队伍和配置组锄地更多路径组数量以此类推需要分组运行的可以参考b站官号视频https://www.bilibili.com/video/BV1JYGVzHEmD/?spm_id_from=333.1387.collection.video_card.click
- **本路径组使用配队名称:** 填写该路径组使用的配队名称js会自动切换
- **拾取模式:** 需要注意,沙暴路线只在模板匹配模式下可用
- - 模板匹配拾取:推荐使用,速度最快,性能消耗最低

View File

@@ -1,4 +1,4 @@
//当前js版本1.20.0
//当前js版本1.21.0
let timeMoveUp;
let timeMoveDown;
@@ -78,7 +78,7 @@ let lastEatBuff = 0;
disableSelfOptimization: settings.disableSelfOptimization ?? false,
eEfficiencyIndex: settings.eEfficiencyIndex ?? 2.5,
mEfficiencyIndex: settings.mEfficiencyIndex ?? 0.5,
splitFactor: settings.splitFactor ?? 0,
ignoreFactor: settings.ignoreFactor ?? 0,
targetEliteNum: settings.targetEliteNum ?? 400,
targetMonsterNum: settings.targetMonsterNum ?? 2000,
priorityTags: settings.priorityTags ?? "",
@@ -117,7 +117,7 @@ let lastEatBuff = 0;
settings.disableSelfOptimization = cfg.disableSelfOptimization ?? false;
settings.eEfficiencyIndex = cfg.eEfficiencyIndex ?? 2.5;
settings.mEfficiencyIndex = cfg.mEfficiencyIndex ?? 0.5;
settings.splitFactor = cfg.splitFactor ?? 0;
settings.ignoreFactor = cfg.ignoreFactor ?? 0;
settings.targetEliteNum = cfg.targetEliteNum ?? 400;
settings.targetMonsterNum = cfg.targetMonsterNum ?? 2000;
settings.priorityTags = cfg.priorityTags ?? "";
@@ -294,9 +294,11 @@ async function processPathings(groupTags) {
const monsterInfoObject = JSON.parse(monsterInfoContent);
// 读取路径文件夹中的所有文件
log.info("开始读取路径文件");
let pathings = await readFolder("pathing", true);
//加载路线cd信息
log.info("路径文件读取完成开始加载cd信息");
await initializeCdTime(pathings, accountName);
// 定义解析 description 的函数
@@ -326,7 +328,7 @@ async function processPathings(groupTags) {
return routeInfo;
}
let index = 0
log.info("cd信息加载完成开始处理路线详细信息");
// 遍历每个路径文件并处理
for (const pathing of pathings) {
index++;
@@ -376,6 +378,22 @@ async function processPathings(groupTags) {
}
}
// ===== 根据 settings.ignoreFactor 过滤 =====
const ignoreFactor = Number(settings.ignoreFactor);
if (Number.isInteger(ignoreFactor) && ignoreFactor > 0) {
// 新增保护标签
const protectTags = ['精英高收益', '高危', '传奇'];
const hasProtectTag = protectTags.some(tag => pathing.tags.includes(tag));
if (!hasProtectTag && // 不含保护标签
pathing.e <= ignoreFactor && // 精英数达标
pathing.m >= 5 * pathing.e) { // 普通数足够
// 清零
pathing.e = 0;
pathing.mora_e = 0;
}
}
const allTags = groupTags[0]; // 已经是 [...new Set(...)] 的结果
// 2. 待匹配文本:路径名 + 描述
const textToMatch = (pathing.fullPath + " " + (description || ""));
@@ -421,6 +439,7 @@ async function processPathings(groupTags) {
pathing.t = avg;
}
}
log.info("预处理阶段完成");
return pathings; // 返回处理后的 pathings 数组
}
@@ -457,6 +476,7 @@ async function markPathings(pathings, groupTags, priorityTags, excludeTags) {
}
async function findBestRouteGroups(pathings, k1, k2, targetEliteNum, targetMonsterNum) {
log.info("开始根据配置寻找路线组合");
/* ========== 0. 原初始化不动 ========== */
let nextTargetEliteNum = targetEliteNum;
let iterationCount = 0;
@@ -476,25 +496,14 @@ async function findBestRouteGroups(pathings, k1, k2, targetEliteNum, targetMonst
const G1 = p.mora_e + p.mora_m, G2 = p.mora_m;
p.G1 = G1; p.G2 = G2;
/* 分离系数 0-10 无惩罚1 最大惩罚 95 % */
const splitFactor = +(settings.splitFactor ?? 0);
/* 混合度:纯血 λ=0最混合 λ=1 */
const λ = (p.e === 0 || p.m === 0) ? 0
: 1 - Math.min(p.e, p.m) / Math.max(p.e, p.m);
/* 仅 E2 惩罚,上限 95 %,线性 */
const penalty = 1 - 0.95 * splitFactor * λ;
/* 收益 */
const eliteGain = p.e === 0 ? 200 : (G1 - G2) / p.e;
const normalGain = p.m === 0 ? 40.5 : G2 / p.m;
/* 打分E1 不惩罚E2 带惩罚 */
p.E1 = (eliteGain ** k1) * (G1 / p.t);
if (p.e === 0) p.E1 = 0;
p.E2 = (normalGain ** k2) * (G2 / p.t) * penalty;
p.E2 = (normalGain ** k2) * (G2 / p.t);
maxE1 = Math.max(maxE1, p.E1);
maxE2 = Math.max(maxE2, p.E2);
@@ -539,18 +548,22 @@ async function findBestRouteGroups(pathings, k1, k2, targetEliteNum, targetMonst
}
/* ========== 2. 迭代:直到“双达标”才停 ========== */
while (iterationCount < 100) {
selectRoutesByEliteTarget(nextTargetEliteNum);
selectRoutesByMonsterTarget(targetMonsterNum);
// 新收敛条件:必须同时大于等于双目标
// 新条件:总量必须落在区间里
if (totalSelectedElites >= targetEliteNum &&
totalSelectedMonsters >= targetMonsterNum) {
totalSelectedElites <= iterationCount / 20 &&
totalSelectedMonsters >= targetMonsterNum &&
totalSelectedMonsters <= iterationCount / 4) {
break;
}
// 只要没达标,就加压:把精英目标向上推
const eliteShort = targetEliteNum - totalSelectedElites;
nextTargetEliteNum += Math.max(1, Math.round(0.1 * eliteShort));
// 只调精英目标:若当前选多了就降门槛,选少了就抬门槛
const eliteGap = targetEliteNum - totalSelectedElites;
nextTargetEliteNum += Math.round(0.7 * eliteGap); // 可正可负,自动收敛
iterationCount++;
}
@@ -577,6 +590,7 @@ async function findBestRouteGroups(pathings, k1, k2, targetEliteNum, targetMonst
const newE = totalSelectedElites - p.e;
const newM = totalSelectedMonsters - p.m;
if (newE >= targetEliteNum && newM >= targetMonsterNum) {
//log.info("调试-删掉了一条路线")
p.selected = false;
totalSelectedElites = newE;
totalSelectedMonsters = newM;
@@ -616,7 +630,7 @@ async function findBestRouteGroups(pathings, k1, k2, targetEliteNum, targetMonst
log.info("使用原文件顺序运行");
pathings.sort((a, b) => a.index - b.index);
}
log.info("路线组合结果如下:");
log.info(`总精英怪数量: ${totalSelectedElites.toFixed(0)}`);
log.info(`总普通怪数量: ${totalSelectedMonsters.toFixed(0)}`);
log.info(`总收益: ${totalGainCombined.toFixed(0)} 摩拉`);
@@ -725,9 +739,20 @@ async function runPath(fullPath, map_name, pm, pe) {
/* ---------- 主任务 ---------- */
const pathingTask = (async () => {
let doLogMonsterCount = true;
log.info(`开始执行路线: ${fullPath}`);
await fakeLog(`${fullPath}`, false, true, 0);
if (settings.logMonsterCount) {
try {
await pathingScript.runFile(fullPath);
} catch (error) {
log.error(`执行地图追踪出现错误${error.message}`);
}
try {
await sleep(1);
} catch (e) {
doLogMonsterCount = false;
}
if (settings.logMonsterCount && doLogMonsterCount) {
const m = Math.floor(pm);
const e = Math.floor(pe);
const lines = [];
@@ -737,12 +762,6 @@ async function runPath(fullPath, map_name, pm, pe) {
if (lines.length) log.debug(lines.join('\n'));
}
try {
await pathingScript.runFile(fullPath);
} catch (error) {
log.error(`执行地图追踪出现错误${error.message}`);
}
await fakeLog(`${fullPath}`, false, false, 0);
state.running = false;
})();

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 1,
"name": "锄地一条龙",
"version": "1.20.10",
"version": "1.21.0",
"description": "一站式解决自动化锄地支持只拾取狗粮请仔细阅读README.md后使用",
"authors": [
{

View File

@@ -31,7 +31,7 @@
{
"name": "partyName",
"type": "input-text",
"label": "本路径组使用配队名称"
"label": "本路径组使用配队名称【注意】请只在这里填写要使用的配队,配置组中配队项留空"
},
{
"name": "sortMode",
@@ -47,7 +47,7 @@
{
"name": "pickup_Mode",
"type": "select",
"label": "拾取模式",
"label": "拾取模式【注意】bgi原版拾取性能开销大准确低尽量不要使用",
"options": [
"模板匹配拾取,拾取狗粮和怪物材料",
"模板匹配拾取,只拾取狗粮",
@@ -59,7 +59,7 @@
{
"name": "activeDumperMode",
"type": "input-text",
"label": "泥头车模式,将在接近战斗点前提前释放部分角色e技能\n需要启用时填写这些角色在队伍中的编号\n有多个角色需要释放时用【中文逗号】分隔"
"label": "泥头车模式,将在接近战斗点前提前释放部分角色e技能\n需要启用时填写这些角色在队伍中的编号\n有多个角色需要释放时用【中文逗号】分隔\n【注意】精英路线启用泥头车将有可能导致狗粮损失\n【注意】盾位角色启用泥头车将可能导致第一轮战斗后半段无护盾覆盖"
},
{
"name": "eatBuff",
@@ -74,25 +74,25 @@
{
"name": "findFInterval",
"type": "input-text",
"label": "识别间隔(毫秒)\n两次检测f图标之间等待时间",
"label": "识别间隔(毫秒)\n两次检测f图标之间等待时间\n建议区间10-200",
"default": "100"
},
{
"name": "pickupDelay",
"type": "input-text",
"label": "拾取后延时(毫秒)\n观察到日志显示连续拾取相同物品时建议调大",
"label": "拾取后延时(毫秒)\n观察到日志显示连续拾取相同物品时建议调大\n建议区间32-200",
"default": "50"
},
{
"name": "rollingDelay",
"type": "input-text",
"label": "滚动后延时(毫秒)\n观察到拾取错误时建议调大",
"label": "滚动后延时(毫秒)\n观察到拾取错误时建议调大\n建议区间16-100",
"default": "32"
},
{
"name": "timeMove",
"type": "input-text",
"label": "单次滚动周期(毫秒)\n观察到上下滚动不全时建议调大",
"label": "单次滚动周期(毫秒)\n观察到上下滚动不全时建议调大\n建议区间800-2000",
"default": "1000"
},
{
@@ -135,7 +135,7 @@
{
"name": "disableSelfOptimization",
"type": "checkbox",
"label": "勾选后禁用根据运行记录优化路线选择的功能\n完全使用路线原有信息"
"label": "勾选后禁用根据运行记录优化路线选择的功能\n完全使用路线原有信息\n【注意】启用该选项将导致无法根据个人运行清空自动优化路线选择"
},
{
"name": "eEfficiencyIndex",
@@ -152,13 +152,13 @@
{
"name": "curiosityFactor",
"type": "input-text",
"label": "好奇系数缺少记录的路线预期用时将被削减对应比例以更多尝试未知路线填0-1之间的数,会导致显示的预计时出现偏差",
"label": "好奇系数缺少记录的路线预期用时将被削减对应比例以更多尝试未知路线填0-1之间的数\n【注意】启用该选项将导致显示的预计时出现偏差",
"default": "0"
},
{
"name": "splitFactor",
"name": "ignoreFactor",
"type": "input-text",
"label": "精英小怪分离系数填0-1数字\n越大越倾向于分离出更多的纯小怪路线\n总耗时大幅增加但是有利于小怪路线蹭更多经验\n建议保持默认即可",
"label": "精英数量小于等于该值且有5倍及以上精英数量的小怪的路线将被视为纯小怪路线用于将含有极少量精英和大量小怪路线当做小怪路线打\n【注意】启用该选项将导致总用时大幅增加",
"default": "0"
},
{

View File

@@ -0,0 +1,2 @@
建议设置分辨率1080p无滤镜等会影响画面的因素其他情况下识别错误属于正常现象
背包中图纸过多可能导致粉球蓝球被挤到下一页,无法识别

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 534 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 441 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 B

View File

@@ -0,0 +1,303 @@
(async function () {
setGameMetrics(1920, 1080, 1);
await canCanNeed();
})();
async function canCanNeed() {
let tryTimes = 0;
let moraRes = -1;
let primogemRes = -1;
let pinkRes = -1;
let blueRes = -1;
while ((tryTimes < 2) && ((moraRes < 0) || (primogemRes < 0) || (pinkRes <= 0 && settings.pink) || (blueRes <= 0 && settings.blue))) {
await genshin.returnMainUi();
await sleep(100);
keyPress("B");
await sleep(1000);
//切换到贵重物品
const gzwpRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/贵重物品.png"));
const gzwpRo2 = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/贵重物品2.png"));
let trys = 0;
while (trys < 10) {
trys++
let res1 = await findAndClick(gzwpRo, 1);
let res2 = await findAndClick(gzwpRo2, 2);
if (res1 || res2) {
break;
}
}
await sleep(1000);
if (moraRes < 0) {
const moraRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/mora.png"), 0, 970, 600, 1080 - 970);
const gameRegion = captureGameRegion();
let moraX = 336;
let moraY = 1004;
try {
const result = gameRegion.find(moraRo);
if (result.isExist()) {
moraX = result.x;
moraY = result.y;
}
} catch (err) {
} finally {
gameRegion.dispose();
}
let attempts = 0;
while (moraRes < 0 && attempts < 5) {
attempts++;
moraRes = await numberTemplateMatch("assets/背包摩拉数字", moraX, moraY, 300, 40, 0.95, 0.8, 5);
}
if (moraRes >= 0) {
log.info(`成功识别到摩拉数值: ${moraRes}`);
} else {
log.warn("未能识别到摩拉数值。");
}
}
if (primogemRes < 0) {
const primogemRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/原石.png"), 0, 970, 600, 1080 - 970);
const plusRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/加号.png"), 0, 970, 600, 1080 - 970);
const gameRegion = captureGameRegion();
let primogemX = 152;
let primogemY = 1007;
let plusX = 262;
let plusY = 1007;
try {
const result = gameRegion.find(primogemRo);
if (result.isExist()) {
primogemX = result.x;
primogemY = result.y;
}
} catch (err) { }
try {
const result = gameRegion.find(plusRo);
if (result.isExist()) {
plusX = result.x;
plusY = result.y;
}
} catch (err) {
} finally {
gameRegion.dispose();
}
let attempts = 0;
while (primogemRes < 0 && attempts < 5) {
attempts++;
primogemRes = await numberTemplateMatch("assets/背包摩拉数字", primogemX + 28, primogemY, plusX - primogemX, 40, 0.95, 0.8, 5);
}
if (primogemRes >= 0) {
log.info(`成功识别到原石数值: ${primogemRes}`);
} else {
log.warn("未能识别到原石数值。");
}
}
if (pinkRes <= 0 && settings.pink) {
const pinkRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/纠缠之缘.png"));
pinkRo.Use3Channels = true;
pinkRo.Threshold = 0.85;
pinkRo.InitTemplate();
const gameRegion = captureGameRegion();
let pinkX = 0;
let pinkY = 0;
try {
const result = gameRegion.find(pinkRo);
if (result.isExist()) {
pinkX = result.x;
pinkY = result.y;
log.info(`${pinkX},${pinkY}找到了纠缠之缘`);
let attempts = 0;
while (pinkRes < 0 && attempts < 3) {
attempts++;
pinkRes = await numberTemplateMatch("assets/背包物品数字", pinkX, pinkY + 97, 124, 26, 0.95, 0.8, 5);
}
if (pinkRes >= 0) {
log.info(`成功识别到纠缠之缘数量: ${pinkRes}`);
} else {
log.warn("未能识别到纠缠之缘数量。");
}
} else {
pinkRes = 0;
log.info("未找到纠缠之缘");
}
} catch (err) {
} finally {
gameRegion.dispose();
}
}
if (blueRes <= 0 && settings.blue) {
const blueRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/相遇之缘.png"));
blueRo.Use3Channels = true;
blueRo.Threshold = 0.85;
blueRo.InitTemplate();
const gameRegion = captureGameRegion();
let blueX = 0;
let blueY = 0;
try {
const result = gameRegion.find(blueRo);
if (result.isExist()) {
blueX = result.x;
blueY = result.y;
log.info(`${blueX},${blueY}找到了相遇之缘`);
let attempts = 0;
while (blueRes < 0 && attempts < 5) {
attempts++;
blueRes = await numberTemplateMatch("assets/背包物品数字", blueX, blueY + 97, 124, 26, 0.95, 0.8, 5);
}
if (blueRes >= 0) {
log.info(`成功识别到相遇之缘数量: ${blueRes}`);
} else {
log.warn("未能识别到相遇之缘数量。");
}
} else {
blueRes = 0;
log.info("未找到相遇之缘");
}
} catch (err) {
} finally {
gameRegion.dispose();
}
}
await sleep(500);
tryTimes++;
}
let logInfo = `当前贵重物品如图识别结果为:\n摩拉:${moraRes}\n原石:${primogemRes}`;
if (settings.pink) {
logInfo += `\n纠缠之缘:${pinkRes}`;
}
if (settings.blue) {
logInfo += `\n相遇之缘:${blueRes}`;
}
if (settings.accountName) {
logInfo = `当前账户:${settings.accountName}\n` + logInfo;
}
log.info(logInfo)
notification.Send(logInfo);
return;
}
/**
* 在指定区域内,用 0-9 的 PNG 模板做「多阈值 + 非极大抑制」数字识别,
* 最终把检测到的数字按左右顺序拼成一个整数返回。
*
* @param {string} numberPngFilePath - 存放 0.png ~ 9.png 的文件夹路径(不含文件名)
* @param {number} x - 待识别区域的左上角 x 坐标,默认 0
* @param {number} y - 待识别区域的左上角 y 坐标,默认 0
* @param {number} w - 待识别区域的宽度,默认 1920
* @param {number} h - 待识别区域的高度,默认 1080
* @param {number} maxThreshold - 模板匹配起始阈值,默认 0.95(最高可信度)
* @param {number} minThreshold - 模板匹配最低阈值,默认 0.8(最低可信度)
* @param {number} splitCount - 在 maxThreshold 与 minThreshold 之间做几次等间隔阈值递减,默认 3
* @param {number} maxOverlap - 非极大抑制时允许的最大重叠像素,默认 2只要 x 或 y 方向重叠大于该值即视为重复框
*
* @returns {number} 识别出的整数;若没有任何有效数字框则返回 -1
*
* @example
* const mora = await numberTemplateMatch('摩拉数字', 860, 70, 200, 40);
* if (mora >= 0) console.log(`当前摩拉:${mora}`);
*/
async function numberTemplateMatch(
numberPngFilePath,
x = 0, y = 0, w = 1920, h = 1080,
maxThreshold = 0.95,
minThreshold = 0.8,
splitCount = 3,
maxOverlap = 2
) {
let ros = [];
for (let i = 0; i <= 9; i++) {
ros[i] = RecognitionObject.TemplateMatch(
file.ReadImageMatSync(`${numberPngFilePath}/${i}.png`), x, y, w, h);
}
function setThreshold(roArr, newThreshold) {
for (let i = 0; i < roArr.length; i++) {
roArr[i].Threshold = newThreshold;
roArr[i].InitTemplate();
}
}
const gameRegion = captureGameRegion();
const allCandidates = [];
/* 1. splitCount 次等间隔阈值递减 */
for (let k = 0; k < splitCount; k++) {
const curThr = maxThreshold - (maxThreshold - minThreshold) * k / Math.max(splitCount - 1, 1);
setThreshold(ros, curThr);
/* 2. 0-9 每个模板跑一遍,所有框都收 */
for (let digit = 0; digit <= 9; digit++) {
const res = gameRegion.findMulti(ros[digit]);
if (res.count === 0) continue;
for (let i = 0; i < res.count; i++) {
const box = res[i];
allCandidates.push({
digit: digit,
x: box.x,
y: box.y,
w: box.width,
h: box.height,
thr: curThr
});
}
}
}
gameRegion.dispose();
/* 3. 无结果提前返回 -1 */
if (allCandidates.length === 0) {
return -1;
}
/* 4. 非极大抑制(必须 x、y 两个方向重叠都 > maxOverlap 才视为重复) */
const adopted = [];
for (const c of allCandidates) {
let overlap = false;
for (const a of adopted) {
const xOverlap = Math.max(0, Math.min(c.x + c.w, a.x + a.w) - Math.max(c.x, a.x));
const yOverlap = Math.max(0, Math.min(c.y + c.h, a.y + a.h) - Math.max(c.y, a.y));
if (xOverlap > maxOverlap && yOverlap > maxOverlap) {
overlap = true;
break;
}
}
if (!overlap) {
adopted.push(c);
//log.info(`在 [${c.x},${c.y},${c.w},${c.h}] 找到数字 ${c.digit},匹配阈值=${c.thr}`);
}
}
/* 5. 按 x 排序,拼整数;仍无有效框时返回 -1 */
if (adopted.length === 0) return -1;
adopted.sort((a, b) => a.x - b.x);
return adopted.reduce((num, item) => num * 10 + item.digit, 0);
}
async function findAndClick(target, maxAttempts = 20) {
for (let attempts = 0; attempts < maxAttempts; attempts++) {
const gameRegion = captureGameRegion();
try {
const result = gameRegion.find(target);
if (result.isExist) {
await sleep(50);
result.click();
await sleep(50);
return true; // 成功立刻返回
}
log.warn(`识别失败,第 ${attempts + 1} 次重试`);
} catch (err) {
} finally {
gameRegion.dispose();
}
if (attempts < maxAttempts - 1) { // 最后一次不再 sleep
await sleep(250);
}
}
return false;
}

View File

@@ -0,0 +1,16 @@
{
"manifest_version": 1,
"name": "摩拉&原石识别",
"version": "1.1",
"tags": [],
"description": "更准确、更低性能开销的摩拉和原石等贵重物品识别",
"saved_files": [],
"authors": [
{
"name": "mno",
"links": "https://github.com/Bedrockx"
}
],
"settings_ui": "settings.json",
"main": "main.js"
}

View File

@@ -0,0 +1,17 @@
[
{
"name": "accountName",
"type": "input-text",
"label": "账户名,用于输出识别结果时区分不同账号信息,非必须"
},
{
"name": "pink",
"type": "checkbox",
"label": "识别纠缠之缘/粉球"
},
{
"name": "blue",
"type": "checkbox",
"label": "识别相遇之缘/蓝球"
}
]