diff --git a/repo/js/营养袋吃药统计/README.md b/repo/js/营养袋吃药统计/README.md index 71f716120..97387388d 100644 --- a/repo/js/营养袋吃药统计/README.md +++ b/repo/js/营养袋吃药统计/README.md @@ -1,12 +1,12 @@ -### 药品消耗统计脚本使用指南(更新版) +### 药品消耗统计脚本使用说明 ## 📋 一、脚本概述 -本脚本是专为《原神》设计的自动化药品消耗统计工具,核心能力升级如下: -- **多模式识别**:支持「营养袋模式」「筛选模式」双模式适配不同场景的药品识别 -- **精准OCR读取**:自动识别背包中指定回血/复活药品数量,适配不同界面布局 +- **多模式识别**:支持「营养袋模式」「筛选模式」「综合模式」三模式适配不同场景的药品识别 +- **精准OCR读取**:自动识别背包中指定回血/复活/攻击/防御/其他药品数量,适配不同界面布局 - **灵活时间配置**:支持自定义每日统计刷新时间(默认4:00,可设0-24小时及一位小数) - **智能数据管理**:自动保留30天历史记录,按自定义时间周期统计药品消耗/新增 - **多账户隔离**:不同账户数据独立存储,支持合规账户名校验(1-20位中英文/数字) - **异常友好处理**:识别失败自动兜底、参数错误智能修正,拒绝进入世界的申请 +- **模块化设计**:代码结构优化,新增攻击药、防御药、其他药支持 ## 🛠️ 二、环境与工具要求 ### 必备工具:茶包版BGI @@ -25,10 +25,13 @@ ## ⚙️ 四、全参数配置说明(新增/细化) | 参数项 | 类型 | 填写要求 | 默认值 | 核心说明 | |--------|------|----------|--------|----------| -| **runMode** | 下拉选择 | 二选一:「营养袋模式」/「筛选模式」 | 营养袋模式 | 营养袋模式:读取便携营养袋内药品;筛选模式:通过背包筛选搜索药品 | +| **runMode** | 下拉选择 | 三选一:「营养袋模式」/「筛选模式」/「综合模式」 | 营养袋模式 | 营养袋模式:读取便携营养袋内药品;筛选模式:通过背包筛选搜索药品;综合模式:营养袋模式+筛选模式结合 | | **initSelect** | 复选框 | 仅初始化/重置时临时勾选 | false | 勾选后运行脚本会删除当日同名记录,重新初始化数据(使用后务必取消) | | **recoveryFoodName** | 文本输入 | 与游戏内回血药名称完全一致 | 空 | 例:「美味的甜甜花酿鸡」,名称错误会导致识别失败 | | **resurrectionFoodName** | 文本输入 | 与游戏内复活药名称完全一致 | 空 | 例:「美味的提瓦特煎蛋」,支持全量游戏内可食用复活类物品 | +| **attackFoodName** | 文本输入 | 与游戏内攻击药名称完全一致 | 空 | 例:「美味的堆高高」,支持全量游戏内攻击增益类物品 | +| **defenseFoodName** | 文本输入 | 与游戏内防御药名称完全一致 | 空 | 例:「美味的贝壳彩糖」,支持全量游戏内防御增益类物品 | +| **otherFoodName** | 文本输入 | 与游戏内其他药名称完全一致 | 空 | 例:「美味的风神杂烩菜」,支持全量游戏内其他增益类物品 | | **userName** | 文本输入 | 1-20字符,仅支持中英文、数字 | 默认账户 | 多账户区分核心,违规名称自动替换为默认账户 | | **refreshTime** | 文本输入 | 0-24之间,支持一位小数 | 4.0 | 自定义每日统计周期分界点(如4.5=4:30),错误值自动修正为4.0 | | **loadDelay** | 文本输入 | 非负整数(单位:毫秒) | 800 | 界面打开/切换的等待延迟,低配置设备可适当增大 | @@ -54,11 +57,18 @@ #### 筛选模式 - 脚本自动打开背包→进入食物分类→筛选搜索指定药品→读取数量 - 无需装备营养袋,适配未配置营养袋的场景,识别流程稍长 +- 只处理回血药和复活药 + +#### 综合模式 +- 脚本自动执行:营养袋模式识别回血/复活药 + 筛选模式识别攻击/防御/其他药 +- 结合两种模式优势,支持全类型药品统计 +- 回血药和复活药通过营养袋模式获取,攻击药、防御药、其他药通过筛选模式获取 #### 通用规则 - 每日首次运行(自定义refreshTime后):记录为当日初始值 - 后续运行:对比初始值计算消耗/新增(正数=消耗,负数=新增) - 每次运行自动保存记录,仅保留30天内数据,过期自动清理 +- 只记录数量大于0的药品数据 ### 场景三:更换药品/重置统计数据 1. 在设置中勾选`initSelect`选项 @@ -77,8 +87,16 @@ ``` 时间:202X/XX/XX XX:XX:XX-【药品名称】-【数量】 时间:202X/XX/XX XX:XX:XX-【药品名称】-【数量】 +... ``` -每条记录包含时间戳+药品名+数量,双药品记录成对生成,保证数据完整性。 +每条记录包含时间戳+药品名+数量,支持5种药品类型: +- 回血药 +- 复活药 +- 攻击药 +- 防御药 +- 其他药 + +**注意**:只生成数量大于0的药品记录,保证数据有效性。 ### 3. OCR识别异常处理 | 异常场景 | 处理逻辑 | 通知/日志 | @@ -87,6 +105,7 @@ | 药品名称为空 | 识别结果无效 | log.warn「XX药名字没填」 | | refreshTime非法 | 自动修正为4.0 | log.warn「刷新时间设置错误,使用默认值4.0」 | | 账户名违规 | 替换为默认账户 | log.error「账户名XX违规,使用默认账户」 | +| 数量为0 | 不记录数据 | 只记录数量大于0的药品数据,通知中只显示有效数据 | ## ⚠️ 七、重要注意事项(强化) ### 1. 运行时机建议 @@ -94,13 +113,20 @@ - 避免在两次脚本运行之间大批量制作/使用药品,防止统计偏差 - 低配置设备:适当增大loadDelay/stepDelay,提升识别成功率 -### 2. 双模式适配要点 +### 2. 三模式适配要点 | 模式 | 优势 | 注意事项 | |------|------|----------| | 营养袋模式 | 识别快、步骤少 | 必须装备便携营养袋,仅识别营养袋内药品 | -| 筛选模式 | 无需装备营养袋 | 药品名称必须精准,依赖背包筛选功能正常 | +| 筛选模式 | 无需装备营养袋 | 药品名称必须精准,依赖背包筛选功能正常,只处理回血药和复活药 | +| 综合模式 | 支持全类型药品统计 | 结合营养袋模式和筛选模式,需要装备便携营养袋 | -### 3. 常见误区修正 +### 3. 建议搭配JS通知使用 +- **推荐使用JS通知**:本脚本会发送各类通知,包括初始化完成、使用情况、异常提醒等 +- **通知好处**:及时获取统计结果,不错过重要信息,方便日常使用 +- **确保通知功能正常**:请确保BGI的JS通知功能已开启,以便接收脚本发送的各类通知 +- **通知内容**:包含账户信息、药品使用情况、库存信息等,格式清晰易读 + +### 4. 常见误区修正 ❌ 错误:`initSelect`长期勾选 → 每次运行都重置数据,无法统计消耗 ✅ 正确:仅重置/换药品时勾选,运行后立即取消 @@ -113,8 +139,9 @@ ## 🔧 八、故障排查指南(新增模式/参数排查) | 问题现象 | 优先检查项 | 解决方案 | |----------|------------|----------| -| 药品数量识别为0 | 1. 运行模式是否匹配场景
2. 药品名称是否完全一致
| 1. 营养袋模式需装备营养袋;筛选模式检查背包食物分类
2. 复制游戏内药品全名(含「美味的/冷的」等前缀)
| +| 药品数量识别为0 | 1. 运行模式是否匹配场景
2. 药品名称是否完全一致
3. 综合模式是否装备了营养袋 | 1. 营养袋模式需装备营养袋;筛选模式检查背包食物分类
2. 复制游戏内药品全名(含「美味的/冷的」等前缀)
3. 综合模式需要装备便携营养袋 | | 统计周期错误 | 1. refreshTime是否合法
2. 系统时间是否准确 | 1. 确认值在0-24之间(如4.0/12.5),错误值会自动修正
2. 同步系统时间到网络标准时间 | -| 脚本运行卡顿/超时 | 1. loadDelay/stepDelay是否过小
2. 设备性能是否不足 | 1. 逐步增大延迟值(如loadDelay改为1000)
2. 关闭后台无关程序,保证游戏前台运行 | +| 脚本运行卡顿/超时 | 1. loadDelay/stepDelay是否过小
2. 设备性能是否不足
3. 综合模式下是否需要更长延迟 | 1. 逐步增大延迟值(如loadDelay改为1000)
2. 关闭后台无关程序,保证游戏前台运行
3. 综合模式包含更多步骤,可适当增大延迟 | +| 部分药品未记录 | 1. 药品名称是否填写
2. 药品数量是否大于0
3. 对应模式是否支持该类型药品 | 1. 检查对应药品名称是否正确填写
2. 只记录数量大于0的药品数据
3. 筛选模式只处理回血药和复活药,攻击/防御/其他药需使用综合模式 | | 多账户数据混淆 | 1. userName是否唯一
2. 记录文件是否存在重名 | 1. 为每个账户设置唯一名称(如「旅行者001/旅行者002」)
2. 检查assets目录,删除重名的错误记录文件 | -| 模式切换后识别失败 | 1. 对应模式的模板图片是否存在
2. 点击坐标是否匹配分辨率 | 1. 确认assets目录有「营养袋.png」「筛选1.png」「筛选2.png」等
2. 重新确认游戏分辨率为1920×1080 | +| 模式切换后识别失败 | 1. 对应模式的模板图片是否存在
2. 点击坐标是否匹配分辨率
3. 综合模式是否包含所有必要的模板图片 | 1. 确认assets目录有「营养袋.png」「筛选1.png」「筛选2.png」等
2. 重新确认游戏分辨率为1920×1080
3. 综合模式需要营养袋和筛选相关的所有模板图片 | diff --git a/repo/js/营养袋吃药统计/main.js b/repo/js/营养袋吃药统计/main.js index c2fd6f4c7..e480fceb5 100644 --- a/repo/js/营养袋吃药统计/main.js +++ b/repo/js/营养袋吃药统计/main.js @@ -1,7 +1,10 @@ let userName = settings.userName || "默认账户"; const mode = settings.runMode || "营养袋模式" -let recoveryFoodName = settings.recoveryFoodName || "回血药名字没填"; -let resurrectionFoodName = settings.resurrectionFoodName || "复活药名字没填"; +let recoveryFoodName = settings.recoveryFoodName || ""; +let resurrectionFoodName = settings.resurrectionFoodName || ""; +let attackFoodName = settings.attackFoodName || ""; +let defenseFoodName = settings.defenseFoodName || ""; +let otherFoodName = settings.otherFoodName || ""; const ocrRegion = { x: 1422, y: 586, @@ -31,11 +34,16 @@ if (isNaN(refreshTime) || refreshTime < 0 || refreshTime >= 24) { const refreshHour = Math.floor(refreshTime); const refreshMinute = Math.floor((refreshTime - refreshHour) * 60); log.info(`刷新时间为: ${refreshHour}:${String(refreshMinute).padStart(2, '0')}`); + +// 正则特殊字符转义函数 +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} (async function () { // 检验账户名 async function getUserName() { userName = userName.trim(); - // 数字,中英文,长度在20个字符以内 + // 账户名规则:数字、中英文,长度1-20字符 if (!userName || !/^[\u4e00-\u9fa5A-Za-z0-9]{1,20}$/.test(userName)) { log.error(`账户名${userName}违规,暂时使用默认账户名,请查看readme后修改`) userName = "默认账户"; @@ -81,16 +89,22 @@ log.info(`刷新时间为: ${refreshHour}:${String(refreshMinute).padStart(2, '0 * 获取本地记录中当天4点至次日4点间的最早记录 * @param {string} filePath - 记录文件路径 * @returns {Promise} 包含药品数据的对象 - * 格式: { recovery: { count }, resurrection: { count }, initialized: { recovery, resurrection } } + * 格式: { recovery: { count }, resurrection: { count }, attack: { count }, defense: { count }, other: { count }, initialized: { recovery, resurrection, attack, defense, other } } */ async function getLocalData(filePath) { // 初始化返回结果 const result = { recovery: null, resurrection: null, + attack: null, + defense: null, + other: null, initialized: { recovery: false, - resurrection: false + resurrection: false, + attack: false, + defense: false, + other: false } }; @@ -124,8 +138,11 @@ log.info(`刷新时间为: ${refreshHour}:${String(refreshMinute).padStart(2, '0 // 时间格式正则:匹配 "时间:YYYY/MM/DD HH:mm:ss" const timeRegex = /时间:(\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2})/; // 药品匹配正则 - const recoveryRegex = new RegExp(`${recoveryFoodName}-(\\d+)`); - const resurrectionRegex = new RegExp(`${resurrectionFoodName}-(\\d+)`); + const recoveryRegex = new RegExp(`${escapeRegExp(recoveryFoodName)}-(\\d+)`); + const resurrectionRegex = new RegExp(`${escapeRegExp(resurrectionFoodName)}-(\\d+)`); + const attackRegex = new RegExp(`${escapeRegExp(attackFoodName)}-(\\d+)`); + const defenseRegex = new RegExp(`${escapeRegExp(defenseFoodName)}-(\\d+)`); + const otherRegex = new RegExp(`${escapeRegExp(otherFoodName)}-(\\d+)`); // 正向遍历:找到第一个小于startTime的行索引(边界) let firstOutOfRangeIndex = -1; // 初始化为-1(表示所有行都在时间范围内) @@ -155,6 +172,9 @@ log.info(`刷新时间为: ${refreshHour}:${String(refreshMinute).padStart(2, '0 // 反向遍历的终止索引:0(顶部) const reverseEndIndex = 0; + // 根据当前模式确定需要处理的药品类型 + const needAttackDefenseOther = mode === "综合模式"; + // 反向遍历:找时间范围内最早的药品记录 // 遍历范围:[reverseStartIndex, reverseEndIndex](从时间范围的最旧→最新) for (let i = reverseStartIndex; i >= reverseEndIndex; i--) { @@ -189,8 +209,39 @@ log.info(`刷新时间为: ${refreshHour}:${String(refreshMinute).padStart(2, '0 } } - // 两个药品都找到,提前终止遍历(已拿到最早记录) - if (result.initialized.recovery && result.initialized.resurrection) { + // 匹配攻击药:未初始化时才赋值,仅在综合模式下处理 + if (needAttackDefenseOther && !result.initialized.attack) { + const attackMatch = line.match(attackRegex); + if (attackMatch) { + result.attack = { count: parseInt(attackMatch[1]) }; + result.initialized.attack = true; + } + } + + // 匹配防御药:未初始化时才赋值,仅在综合模式下处理 + if (needAttackDefenseOther && !result.initialized.defense) { + const defenseMatch = line.match(defenseRegex); + if (defenseMatch) { + result.defense = { count: parseInt(defenseMatch[1]) }; + result.initialized.defense = true; + } + } + + // 匹配其他药:未初始化时才赋值,仅在综合模式下处理 + if (needAttackDefenseOther && !result.initialized.other) { + const otherMatch = line.match(otherRegex); + if (otherMatch) { + result.other = { count: parseInt(otherMatch[1]) }; + result.initialized.other = true; + } + } + + // 所有需要的药品都找到,提前终止遍历(已拿到最早记录) + let allFound = result.initialized.recovery && result.initialized.resurrection; + if (needAttackDefenseOther) { + allFound = allFound && result.initialized.attack && result.initialized.defense && result.initialized.other; + } + if (allFound) { break; } } @@ -201,7 +252,7 @@ log.info(`刷新时间为: ${refreshHour}:${String(refreshMinute).padStart(2, '0 } } - async function updateRecord(filePath, currentRecovery, currentResurrection, deleteSameDayRecords = false) { + async function updateRecord(filePath, currentRecovery, currentResurrection, currentAttack, currentDefense, currentOther, deleteSameDayRecords = false) { // 生成当前时间字符串 const now = new Date(); const timeStr = `${now.getFullYear()}/${ @@ -216,9 +267,37 @@ log.info(`刷新时间为: ${refreshHour}:${String(refreshMinute).padStart(2, '0 String(now.getSeconds()).padStart(2, '0') }`; - // 生成两条新记录 - const recoveryLine = `时间:${timeStr}-${recoveryFoodName}-${currentRecovery}`; - const resurrectionLine = `时间:${timeStr}-${resurrectionFoodName}-${currentResurrection}`; + // 根据当前模式确定需要处理的药品类型 + const needAttackDefenseOther = mode === "综合模式"; + + // 基础药品:回血药和复活药 + const baseDrugs = [ + { name: recoveryFoodName, count: currentRecovery }, + { name: resurrectionFoodName, count: currentResurrection } + ]; + + // 根据模式确定要处理的药品列表 + let drugs = [...baseDrugs]; + + // 只在综合模式下添加攻击药、防御药和其他药 + if (needAttackDefenseOther) { + drugs = drugs.concat([ + { name: attackFoodName, count: currentAttack }, + { name: defenseFoodName, count: currentDefense }, + { name: otherFoodName, count: currentOther } + ]); + } + + // 生成记录,只包含name不为空且数量>0的数据 + const records = drugs + .filter(drug => drug.name.trim() && drug.count > 0) + .map(drug => `时间:${timeStr}-${drug.name}-${drug.count}`); + + // 如果没有需要记录的数据,直接返回 + if (records.length === 0) { + log.info("没有需要记录的数据"); + return true; + } try { let content = await file.readText(filePath); @@ -226,7 +305,7 @@ log.info(`刷新时间为: ${refreshHour}:${String(refreshMinute).padStart(2, '0 if (lines.length === 0) { // 文件为空,直接写入新记录 - await file.writeText(filePath, `${recoveryLine}\n${resurrectionLine}`); + await file.writeText(filePath, records.join('\n')); return true; } @@ -251,9 +330,24 @@ log.info(`刷新时间为: ${refreshHour}:${String(refreshMinute).padStart(2, '0 endTime.setDate(endTime.getDate() + 1); } - // 创建药品匹配正则 - const recoveryRegex = new RegExp(`${recoveryFoodName}-\\d+$`); - const resurrectionRegex = new RegExp(`${resurrectionFoodName}-\\d+$`); + // 根据当前模式确定需要处理的药品类型 + const needAttackDefenseOther = mode === "综合模式"; + + // 基础药品:回血药和复活药 + const baseDrugs = [recoveryFoodName, resurrectionFoodName]; + + // 根据模式确定要处理的药品列表 + let drugs = [...baseDrugs]; + + // 只在综合模式下添加攻击药、防御药和其他药 + if (needAttackDefenseOther) { + drugs = drugs.concat([attackFoodName, defenseFoodName, otherFoodName]); + } + + // 创建药品匹配正则,只处理需要记录的药品 + const regexList = drugs + .filter(name => name.trim()) + .map(name => new RegExp(`${escapeRegExp(name)}-\\d+$`)); // 过滤掉当天时间范围内的同名记录 lines = lines.filter(line => { @@ -263,9 +357,11 @@ log.info(`刷新时间为: ${refreshHour}:${String(refreshMinute).padStart(2, '0 const recordTime = new Date(timeMatch[1]); // 检查是否在当天时间范围内 if (recordTime >= startTime && recordTime < endTime) { - // 检查是否为回血药或复活药记录 - if (recoveryRegex.test(line) || resurrectionRegex.test(line)) { - return false; // 删除该记录 + // 检查是否为需要记录的药品记录 + for (const regex of regexList) { + if (regex.test(line)) { + return false; // 删除该记录 + } } } return true; // 保留该记录 @@ -273,8 +369,7 @@ log.info(`刷新时间为: ${refreshHour}:${String(refreshMinute).padStart(2, '0 } // 添加新记录到最前面 - lines.unshift(resurrectionLine); - lines.unshift(recoveryLine); + lines.unshift(...records); // 只保留30天内的记录 const thirtyDaysAgo = new Date(); @@ -292,7 +387,7 @@ log.info(`刷新时间为: ${refreshHour}:${String(refreshMinute).padStart(2, '0 return true; } catch (error) { // 文件不存在时创建新文件 - await file.writeText(filePath, `${recoveryLine}\n${resurrectionLine}`); + await file.writeText(filePath, records.join('\n')); return true; } } @@ -380,7 +475,7 @@ log.info(`刷新时间为: ${refreshHour}:${String(refreshMinute).padStart(2, '0 } } return { name: null, count: null }; -} + } async function findAndClick(target, doClick = true, maxAttempts = 60) { for (let i = 0; i < maxAttempts; i++) { @@ -402,9 +497,119 @@ log.info(`刷新时间为: ${refreshHour}:${String(refreshMinute).padStart(2, '0 return await findAndClick(pngRo, doClick, maxAttempts); } + // 生成药品描述的函数 + async function generateDrugDescription(drugName, diffValue,changes) { + if (!drugName.trim()) return; + + let desc = ""; + if (diffValue > 0) { + desc = `- ${drugName}:消耗 ${diffValue} 个`; + } else if (diffValue < 0) { + desc = `- ${drugName}:新增 ${-diffValue} 个`; + } else { + desc = `- ${drugName}:无变化`; + } + changes.push(desc); + } + async function main() { let recoveryNumber = 0; let resurrectionNumber = 0; + let attackNumber = 0; + let defenseNumber = 0; + let otherNumber = 0; + + // 进入界面的通用函数 + async function enterInterface(interfaceType, maxRetries = 5) { + let retryCount = 0; + let successClick = false; + + while (retryCount < maxRetries && !successClick) { + retryCount++; + await close_join_world_popup_window(); + + if (interfaceType === 'nutrition_bag') { + // 营养袋模式:进入小道具界面 + click(1051, 51); // 选择小道具 + await sleep(loadDelay); + + if (await clickPNG('拒绝', 3)) { + log.info("检测到进入世界申请,已拒绝,重新尝试点击分类标签"); + await sleep(stepDelay); + continue; + } + + if (await clickPNG('营养袋', 1, false)) { + successClick = true; + log.info("成功进入小道具界面"); + break; + } + } else if (interfaceType === 'filter') { + // 筛选模式:进入食物界面 + click(863, 51); // 选择食物 + await sleep(loadDelay); + + if (await clickPNG('拒绝', 3)) { + log.info("检测到进入世界申请,已拒绝,重新尝试点击分类标签"); + await sleep(stepDelay); + continue; + } + + if (await clickPNG('筛选1', 1, false) || await clickPNG('筛选2', 1, false)) { + successClick = true; + log.info("成功进入食物界面"); + break; + } + } + + log.warn(`尝试点击分类标签失败,第${retryCount}次重试`); + await sleep(stepDelay); + } + + return successClick; + } + + // 搜索和识别药品的通用函数 + async function searchAndRecognizeDrug(drugName, drugType) { + if (!drugName.trim()) return 0; + + await clickPNG('筛选1', 1); + await clickPNG('筛选2', 1); + await clickPNG('重置'); + await sleep(stepDelay); + await clickPNG('搜索'); + await sleep(loadDelay); + log.info(`搜索${drugName}`); + inputText(drugName); + await clickPNG('确认筛选'); + await sleep(loadDelay); + const count = await recognizeNumberByOCR(ocrRegion2, /\d+/) || 0; + + if (count === 0) { + notification.send(`【营养袋吃药统计】\n未识别到${drugType}数量\n药品名:${drugName}\n设置数量为:0`); + } + + return count; + } + + // 营养袋药品识别的通用函数 + async function recognizeNutritionBagDrug(ocrRegionId, pattern, drugType) { + let result = await recognizeFoodItemByOCR(ocrRegionId, pattern); + if (result.name && result.count !== null) { + log.info(`识别到: ${result.name}, 份数: ${result.count}`); + } else { + log.warn(`未识别到有效的${drugType}信息`); + } + const count = result.count || 0; + const name = result.name || `未识别到${drugType}名称`; + + if (count === 0) { + notification.send(`【营养袋吃药统计】\n未识别到${drugType}数量\n药品名:${name}\n设置数量为:0`); + } + + return { count, name }; + } + // 设置分辨率和缩放 setGameMetrics(1920, 1080, 1); await genshin.returnMainUi(); @@ -414,221 +619,325 @@ log.info(`刷新时间为: ${refreshHour}:${String(refreshMinute).padStart(2, '0 await close_expired_stuff_popup_window(); await close_join_world_popup_window(); await sleep(loadDelay); - // 打开界面 - let maxRetries = 5; // 最大重试次数 - let retryCount = 0; - let successClick = false; - // 根据模式选择点击的位置 - let clickX, clickY; - if (mode === "营养袋模式") { - clickX = 1051; // 选择小道具 - clickY = 51; + + if (mode === "综合模式") { + // 综合模式:回血药和复活药通过营养袋模式获取,攻击药和防御药通过筛选模式获取 + + // 1. 先处理营养袋模式(识别回血药和复活药) + const successClick = await enterInterface('nutrition_bag'); + if (successClick) { + await clickPNG('营养袋', 1); + await sleep(loadDelay); + const pattern = /(.+?)\s*[(\(](\d+)[份\s]*[)\)]/; + + // 使用模块化函数识别各种药品 + const recoveryResult = await recognizeNutritionBagDrug(ocrRegion, pattern, '回血药'); + recoveryNumber = recoveryResult.count; + recoveryFoodName = recoveryResult.name; + + const resurrectionResult = await recognizeNutritionBagDrug(ocrRegion1, pattern, '复活药'); + resurrectionNumber = resurrectionResult.count; + resurrectionFoodName = resurrectionResult.name; + } + // 2. 然后处理筛选模式(识别攻击药和防御药,只有填了名字才筛选) + // 检查是否需要进行筛选(攻击药、防御药或其他药名字已填) + const needFilter = !!attackFoodName.trim() || !!defenseFoodName.trim() || !!otherFoodName.trim(); + + if (needFilter) { + const successClick = await enterInterface('filter'); + if (successClick) { + // 使用模块化函数识别各种药品 + attackNumber = await searchAndRecognizeDrug(attackFoodName, '攻击药'); + defenseNumber = await searchAndRecognizeDrug(defenseFoodName, '防御药'); + otherNumber = await searchAndRecognizeDrug(otherFoodName, '其他药'); + + // 重置筛选 + await clickPNG('筛选1', 1); + await clickPNG('筛选2', 1); + await clickPNG('重置'); + await sleep(stepDelay); + await clickPNG('确认筛选'); + } + } + } else if (mode === "营养袋模式") { + // 使用通用进入界面函数 + const successClick = await enterInterface('nutrition_bag'); + + if (successClick) { + await clickPNG('营养袋', 1); + await sleep(loadDelay); + const pattern = /(.+?)\s*[(\(](\d+)[份\s]*[)\)]/; + + // 使用模块化函数识别各种药品 + const recoveryResult = await recognizeNutritionBagDrug(ocrRegion, pattern, '回血药'); + recoveryNumber = recoveryResult.count; + recoveryFoodName = recoveryResult.name; + + const resurrectionResult = await recognizeNutritionBagDrug(ocrRegion1, pattern, '复活药'); + resurrectionNumber = resurrectionResult.count; + resurrectionFoodName = resurrectionResult.name; + } } else if (mode === "筛选模式") { - clickX = 863; // 选择食物 - clickY = 51; - } - while (retryCount < maxRetries && !successClick) { - retryCount++; - await close_join_world_popup_window(); - click(clickX, clickY); - await sleep(loadDelay); - // 检查是否进入了申请界面(通过查找"拒绝"按钮) - if (await clickPNG('拒绝', 3)) { // 找到拒绝按钮,说明在申请界面 - log.info("检测到进入世界申请,已拒绝,重新尝试点击分类标签"); + // 筛选模式:只处理回血药和复活药 + // 使用通用进入界面函数 + const successClick = await enterInterface('filter'); + + if (successClick) { + // 使用模块化函数识别各种药品 + recoveryNumber = await searchAndRecognizeDrug(recoveryFoodName, '回血药'); + resurrectionNumber = await searchAndRecognizeDrug(resurrectionFoodName, '复活药'); + + // 重置筛选 + await clickPNG('筛选1', 1); + await clickPNG('筛选2', 1); + await clickPNG('重置'); await sleep(stepDelay); - continue; // 继续下一次循环 + await clickPNG('确认筛选'); } - if (mode === "营养袋模式") { - if (await clickPNG('营养袋', 1, false)) { // 只检查不点击 - successClick = true; - log.info("成功进入小道具界面"); - break; - } - } else if (mode === "筛选模式") { - if (await clickPNG('筛选1', 1, false)||await clickPNG('筛选2', 1, false)) { // 只检查不点击 - successClick = true; - log.info("成功进入食物界面"); - break; - } - } - log.warn(`尝试点击分类标签失败,第${retryCount}次重试`); - await sleep(stepDelay); - } - if (!successClick) { - log.error("多次尝试点击分类标签失败,脚本终止"); - return { recoveryNumber, resurrectionNumber}; - } - if (mode === "营养袋模式") { - // 营养袋模式 - await clickPNG('营养袋', 1); - await sleep(loadDelay); - const pattern = /(.+?)\s*[(\(](\d+)[份\s]*[)\)]/; - // 识别回血药 - let result = await recognizeFoodItemByOCR(ocrRegion, pattern); - if (result.name && result.count !== null) { - log.info(`识别到: ${result.name}, 份数: ${result.count}`); - } else { - log.warn("未识别到有效的回血药信息"); - } - recoveryNumber = result.count; // 识别回血药数量 - recoveryFoodName = result.name || '未识别到回血药名称'; // 如果识别失败,使用settings中的名字 - // 处理回血药识别结果 - if (recoveryNumber === null) { - recoveryNumber = 0; - notification.send(`未识别到回血药数量,设置数量为0,药品名:${recoveryFoodName}`); - } - // 识别复活药 - result = await recognizeFoodItemByOCR(ocrRegion1, pattern); - if (result.name && result.count !== null) { - log.info(`识别到: ${result.name}, 份数: ${result.count}`); - } else { - log.warn("未识别到有效的复活药信息"); - } - resurrectionNumber = result.count; // 识别复活药数量 - resurrectionFoodName = result.name || '未识别到复活药名称'; // 如果识别失败,使用settings中的名字 - // 处理复活药识别结果 - if (resurrectionNumber === null) { - resurrectionNumber = 0; - notification.send(`未识别到复活药数量,设置数量为0,药品名:${resurrectionFoodName}`); - } - } else if (mode === "筛选模式") { - // 食物筛选模式 - // 先识别回血药 - await clickPNG('筛选1', 1); - await clickPNG('筛选2', 1); - await clickPNG('重置'); - await sleep(stepDelay); - await clickPNG('搜索'); - await sleep(loadDelay); - log.info(`搜索${recoveryFoodName}`); - inputText(recoveryFoodName); - await clickPNG('确认筛选'); - await sleep(loadDelay); - recoveryNumber = await recognizeNumberByOCR(ocrRegion2, /\d+/); // 识别回血药数量 - // 处理回血药识别结果 - if (recoveryNumber === null) { - recoveryNumber = 0; - notification.send(`未识别到回血药数量,设置数量为0,药品名:${recoveryFoodName}`); - await sleep(5000); - click(863, 51); // 选择食物 - await sleep(1000); - } - // 重置筛选,识别复活药 - await clickPNG('筛选1', 1); - await clickPNG('筛选2', 1); - await clickPNG('重置'); - await sleep(stepDelay); - await clickPNG('搜索'); - await sleep(loadDelay); - log.info(`搜索${resurrectionFoodName}`); - inputText(resurrectionFoodName); - await clickPNG('确认筛选'); - await sleep(loadDelay); - resurrectionNumber = await recognizeNumberByOCR(ocrRegion2, /\d+/); // 识别复活药数量 - // 处理复活药识别结果 - if (resurrectionNumber === null) { - resurrectionNumber = 0; - notification.send(`未识别到复活药数量,设置数量为0,药品名:${resurrectionFoodName}`); - await sleep(5000); - click(863, 51); // 选择食物 - await sleep(1000); - } - // 重置筛选 - await clickPNG('筛选1', 1); - await clickPNG('筛选2', 1); - await clickPNG('重置'); - await sleep(stepDelay); - await clickPNG('确认筛选'); } + await genshin.returnMainUi(); - return { recoveryNumber, resurrectionNumber }; + return { recoveryNumber, resurrectionNumber, attackNumber, defenseNumber, otherNumber }; } // 主执行流程 userName = await getUserName(); const recordPath = `assets/${userName}.txt`; // 获取当前药物数量 - const { recoveryNumber, resurrectionNumber } = await main(); + const { recoveryNumber, resurrectionNumber, attackNumber, defenseNumber, otherNumber } = await main(); // 获取本地保存的数据 const localData = await getLocalData(recordPath); // 确定初始化数据 - let initRecovery, initResurrection; + let initRecovery, initResurrection, initAttack, initDefense, initOther; let useLocalDataAsInit = false; - if (localData.initialized.recovery && localData.initialized.resurrection) { - // 情况1:两者都有 - initRecovery = localData.recovery.count; - initResurrection = localData.resurrection.count; + + // 检查本地数据初始化情况,只处理name不为空的数据 + const hasLocalRecovery = recoveryFoodName.trim() && localData.initialized.recovery; + const hasLocalResurrection = resurrectionFoodName.trim() && localData.initialized.resurrection; + const hasLocalAttack = attackFoodName.trim() && localData.initialized.attack; + const hasLocalDefense = defenseFoodName.trim() && localData.initialized.defense; + const hasLocalOther = otherFoodName.trim() && localData.initialized.other; + + // 根据当前模式确定需要处理的药品类型 + const needAttackDefenseOther = mode === "综合模式"; + + // 计算有效药品数量(name不为空的药品),只考虑当前模式下需要处理的药品 + const baseFoods = [recoveryFoodName, resurrectionFoodName]; + const allFoods = needAttackDefenseOther + ? [...baseFoods, attackFoodName, defenseFoodName, otherFoodName] + : baseFoods; + const validFoodCount = allFoods.filter(name => name.trim()).length; + + // 计算已读取到本地数据的有效药品数量,只考虑当前模式下需要处理的药品 + const baseLoaded = [hasLocalRecovery, hasLocalResurrection]; + const allLoaded = needAttackDefenseOther + ? [...baseLoaded, hasLocalAttack, hasLocalDefense, hasLocalOther] + : baseLoaded; + const loadedFoodCount = allLoaded.filter(Boolean).length; + + if (validFoodCount > 0 && validFoodCount === loadedFoodCount) { + // 情况1:所有有效药品(name不为空)都有本地数据 + initRecovery = hasLocalRecovery ? localData.recovery.count : recoveryNumber; + initResurrection = hasLocalResurrection ? localData.resurrection.count : resurrectionNumber; + initAttack = hasLocalAttack ? localData.attack.count : attackNumber; + initDefense = hasLocalDefense ? localData.defense.count : defenseNumber; + initOther = hasLocalOther ? localData.other.count : otherNumber; useLocalDataAsInit = true; log.info(`已读取到本地数据`) - } else if (localData.initialized.recovery || localData.initialized.resurrection) { - // 情况2:一有一无,用有的那个,缺的用当前数据 - initRecovery = localData.initialized.recovery ? localData.recovery.count : recoveryNumber; - initResurrection = localData.initialized.resurrection ? localData.resurrection.count : resurrectionNumber; - log.info(`未读取到全部的本地数据,缺失部分使用当前数据作为初始数据`) } else { - // 情况3:两者都无,使用当前数据 - initRecovery = recoveryNumber; - initResurrection = resurrectionNumber; - log.info(`未读取到本地数据,使用当前数据作为初始数据`) + // 情况2:部分有,部分无,用有的那个,缺的用当前数据 + // 情况3:全部本地数据都没有,所有药品都使用当前数据作为初始数据 + initRecovery = hasLocalRecovery ? localData.recovery.count : recoveryNumber; + initResurrection = hasLocalResurrection ? localData.resurrection.count : resurrectionNumber; + initAttack = hasLocalAttack ? localData.attack.count : attackNumber; + initDefense = hasLocalDefense ? localData.defense.count : defenseNumber; + initOther = hasLocalOther ? localData.other.count : otherNumber; + if (loadedFoodCount === 0) { + log.info(`未读取到本地数据,所有药品使用当前数据作为初始数据`) + } else { + log.info(`未读取到全部的本地数据,缺失部分使用当前数据作为初始数据`) + } } - // 判断是否需要写入(两个数据都不为0时才写入) - const shouldWriteRecord = recoveryNumber > 0 && resurrectionNumber > 0; + + // 判断是否需要写入(只写入填了名字的药品) + const shouldWriteRecovery = recoveryFoodName.trim() && recoveryNumber > 0; + const shouldWriteResurrection = resurrectionFoodName.trim() && resurrectionNumber > 0; + const shouldWriteAttack = attackFoodName.trim() && attackNumber > 0; + const shouldWriteDefense = defenseFoodName.trim() && defenseNumber > 0; + const shouldWriteOther = otherFoodName.trim() && otherNumber > 0; + const shouldWriteRecord = shouldWriteRecovery || shouldWriteResurrection || shouldWriteAttack || shouldWriteDefense || shouldWriteOther; + // initSelect处理逻辑 if (settings.initSelect && shouldWriteRecord) { // 强制初始化:初始化数量和最后一次运行数量都设为当前值 - await updateRecord(recordPath, recoveryNumber, resurrectionNumber,deleteSameDayRecords=true); - notification.send(`${userName}: 强制初始化完成!${recoveryFoodName}${recoveryNumber}个, ${resurrectionFoodName}${resurrectionNumber}个`); + await updateRecord(recordPath, recoveryNumber, resurrectionNumber, attackNumber, defenseNumber, otherNumber, deleteSameDayRecords=true); + + // 构建通知消息 + let initMsg = `【营养袋吃药统计】\n\n`; + initMsg += `📋 强制初始化完成!\n`; + initMsg += `👤 账户:${userName}\n\n`; + initMsg += `📊 初始药品数据:\n`; + + let items = []; + if (shouldWriteRecovery) items.push(`- ${recoveryFoodName}:${recoveryNumber}个`); + if (shouldWriteResurrection) items.push(`- ${resurrectionFoodName}:${resurrectionNumber}个`); + if (shouldWriteAttack) items.push(`- ${attackFoodName}:${attackNumber}个`); + if (shouldWriteDefense) items.push(`- ${defenseFoodName}:${defenseNumber}个`); + if (shouldWriteOther) items.push(`- ${otherFoodName}:${otherNumber}个`); + + initMsg += items.join('\n'); + + notification.send(initMsg); + + // 添加简单格式的日志记录 + let initItemsSummary = items.map(item => item.replace(/- /g, "")).join(", "); + log.info(`${userName}:强制初始化完成|当前库存:${initItemsSummary}`); return } + if (shouldWriteRecord) { // 使用当前的数据更新记录 - await updateRecord(recordPath, recoveryNumber, resurrectionNumber); + await updateRecord(recordPath, recoveryNumber, resurrectionNumber, attackNumber, defenseNumber, otherNumber); + // 本地有初始记录 if(useLocalDataAsInit){ - // 计算消耗/增加数量 const diffRecovery = initRecovery - recoveryNumber; const diffResurrection = initResurrection - resurrectionNumber; + + // 根据当前模式确定需要处理的药品类型 + const needAttackDefenseOther = mode === "综合模式"; + const diffAttack = needAttackDefenseOther ? initAttack - attackNumber : 0; + const diffDefense = needAttackDefenseOther ? initDefense - defenseNumber : 0; + const diffOther = needAttackDefenseOther ? initOther - otherNumber : 0; - let logMsg = ""; - - // 处理回血药描述 - let descRecovery = ""; - if (diffRecovery > 0) { - descRecovery = `消耗${recoveryFoodName}${diffRecovery}个`; - } else if (diffRecovery < 0) { - descRecovery = `新增${recoveryFoodName}${-diffRecovery}个`; - } else { - descRecovery = `${recoveryFoodName}无变化`; + let changes = []; + + await generateDrugDescription(recoveryFoodName, diffRecovery,changes); + await generateDrugDescription(resurrectionFoodName, diffResurrection,changes); + + // 只在综合模式下处理攻击药、防御药和其他药 + if (needAttackDefenseOther) { + await generateDrugDescription(attackFoodName, diffAttack,changes); + await generateDrugDescription(defenseFoodName, diffDefense,changes); + await generateDrugDescription(otherFoodName, diffOther,changes); } - // 处理复活药描述 - let descResurrection = ""; - if (diffResurrection > 0) { - descResurrection = `消耗${resurrectionFoodName}${diffResurrection}个`; - } else if (diffResurrection < 0) { - descResurrection = `新增${resurrectionFoodName}${-diffResurrection}个`; + // 构建通知消息 + let logMsg = `【营养袋吃药统计】\n\n`; + logMsg += `📊 今日药品使用情况\n`; + logMsg += `👤 账户:${userName}\n\n`; + + if (changes.every(change => change.includes("无变化"))) { + logMsg += `✅ 今日药物数量无变化\n\n`; } else { - descResurrection = `${resurrectionFoodName}无变化`; - } - - // 根据变化组合日志消息 - if (diffRecovery === 0 && diffResurrection === 0) { - // 两个值都等于0,输出无变化 - logMsg = `${userName}: 今日药物数量无变化`; - } else { - // 其他情况 - logMsg = `${userName}: 今日${descRecovery},${descResurrection}`; + logMsg += `📝 使用记录:\n`; + logMsg += changes.join('\n'); + logMsg += `\n\n`; } // 添加库存信息 - logMsg += ` | 当前库存:${recoveryFoodName}${recoveryNumber}个, ${resurrectionFoodName}${resurrectionNumber}个`; - // 发送通知 + const baseDrugs = [ + { name: recoveryFoodName, count: recoveryNumber }, + { name: resurrectionFoodName, count: resurrectionNumber } + ]; + + let inventoryDrugs = [...baseDrugs]; + + // 只在综合模式下添加攻击药、防御药和其他药的库存信息 + if (needAttackDefenseOther) { + inventoryDrugs = inventoryDrugs.concat([ + { name: attackFoodName, count: attackNumber }, + { name: defenseFoodName, count: defenseNumber }, + { name: otherFoodName, count: otherNumber } + ]); + } + + let inventory = inventoryDrugs + .filter(drug => drug.name.trim() && drug.count > 0) + .map(drug => `- ${drug.name}:${drug.count}个`); + + if (inventory.length > 0) { + logMsg += `📦 当前库存:\n`; + logMsg += inventory.join('\n'); + } + notification.send(logMsg); - }else{ - // 添加账户名称的通知 - notification.send(`${userName}: 今日初始化完成!${recoveryFoodName}${initRecovery}个, ${resurrectionFoodName}${initResurrection}个`); + + // 添加简单格式的日志记录 + let usageSummary = changes.every(change => change.includes("无变化")) ? "药物数量无变化" : changes.map(change => change.replace(/- /g, "")).join(", "); + let inventorySummary = inventory.length > 0 ? inventory.map(item => item.replace(/- /g, "")).join(", ") : "无"; + log.info(`${userName}:今日使用情况|${usageSummary}|当前库存:${inventorySummary}`); + } else { + // 构建通知消息 + let initMsg = `【营养袋吃药统计】\n\n`; + initMsg += `✅ 今日初始化完成!\n`; + initMsg += `👤 账户:${userName}\n\n`; + + // 根据当前模式确定需要显示的药品类型 + const needAttackDefenseOther = mode === "综合模式"; + + const baseDrugs = [ + { name: recoveryFoodName, count: initRecovery }, + { name: resurrectionFoodName, count: initResurrection } + ]; + + let drugs = [...baseDrugs]; + + // 只在综合模式下添加攻击药、防御药和其他药 + if (needAttackDefenseOther) { + drugs = drugs.concat([ + { name: attackFoodName, count: initAttack }, + { name: defenseFoodName, count: initDefense }, + { name: otherFoodName, count: initOther } + ]); + } + + let items = drugs + .filter(drug => drug.name.trim() && drug.count > 0) + .map(drug => `- ${drug.name}:${drug.count}个`); + + if (items.length > 0) { + initMsg += `📊 初始药品数据:\n`; + initMsg += items.join('\n'); + notification.send(initMsg); + + // 添加简单格式的日志记录 + let initItemsSummary = items.map(item => item.replace(/- /g, "")).join(", "); + log.info(`${userName}:初始化完成|当前库存:${initItemsSummary}`); + } else { + initMsg += `⚠️ 未识别到有效药品数据\n`; + notification.send(initMsg); + + // 添加简单格式的日志记录 + log.info(`${userName}:初始化完成|当前库存:无`); + } } } else { - // 当前数据有任意一个为0,不写入记录,只发送通知 - notification.send(`${userName}: 当前药品数量识别不全(${recoveryFoodName}${recoveryNumber}个, ${resurrectionFoodName}${resurrectionNumber}个),不更新记录`); + // 构建通知消息 + let msg = `【营养袋吃药统计】\n\n`; + msg += `⚠️ 识别异常提醒\n`; + msg += `👤 账户:${userName}\n\n`; + msg += `📋 当前药品数量识别不全\n\n`; + + const drugs = [ + { name: recoveryFoodName, count: recoveryNumber }, + { name: resurrectionFoodName, count: resurrectionNumber }, + { name: attackFoodName, count: attackNumber }, + { name: defenseFoodName, count: defenseNumber }, + { name: otherFoodName, count: otherNumber } + ]; + + let items = drugs + .filter(drug => drug.name.trim()) + .map(drug => `- ${drug.name}:${drug.count}个`); + + msg += `🔍 识别结果:\n`; + msg += items.join('\n'); + msg += `\n\n`; + msg += `❌ 不更新记录\n`; + + notification.send(msg); + + // 添加简单格式的日志记录 + log.info(`${userName}:识别异常,未更新记录|当前库存:无`); } })(); \ No newline at end of file diff --git a/repo/js/营养袋吃药统计/manifest.json b/repo/js/营养袋吃药统计/manifest.json index 8c9a69c45..2a727e275 100644 --- a/repo/js/营养袋吃药统计/manifest.json +++ b/repo/js/营养袋吃药统计/manifest.json @@ -1,9 +1,9 @@ { "manifest_version": 1, "name": "吃药统计", - "version": "1.7", + "version": "1.8", "bgi_version": "0.51", - "description": "用于统计指定两个食物的消耗,推荐锄地前后使用", + "description": "用于查询指定多个食物的消耗,推荐锄地前后使用", "authors": [ { "name": "勺子", diff --git a/repo/js/营养袋吃药统计/settings.json b/repo/js/营养袋吃药统计/settings.json index 7be6e7a4a..be36edcf6 100644 --- a/repo/js/营养袋吃药统计/settings.json +++ b/repo/js/营养袋吃药统计/settings.json @@ -5,7 +5,8 @@ "label": "运行模式", "options": [ "营养袋模式", - "筛选模式" + "筛选模式", + "综合模式" ], "default": "营养袋模式" }, @@ -27,6 +28,24 @@ "label": "复活药名称", "default": "" }, + { + "name": "attackFoodName", + "type": "input-text", + "label": "攻击药名称", + "default": "" + }, + { + "name": "defenseFoodName", + "type": "input-text", + "label": "防御药名称", + "default": "" + }, + { + "name": "otherFoodName", + "type": "input-text", + "label": "其他药名称", + "default": "" + }, { "name": "userName", "type": "input-text",