营养袋统计吃药 (#2414)

* 营养袋统计吃药

* Update main.js
This commit is contained in:
skyflag2022
2025-11-30 11:55:07 +08:00
committed by GitHub
parent 799228b270
commit 1ed7de2c58
4 changed files with 495 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
原理是将每天4点后第一次运行的数据记为初始值后续再次运行得出数量变化。
每天只自动记录初始化一次,如果需要手动初始化,可以在选项设置中勾选。
手动运行初始化一次后记得取消勾选,不然运行就一直初始化。
脚本运行前需要在自定义设置里填写准确的食物名称。
使用前请确认已装备便携营养袋并且已打开bgi的自动吃药功能。
脚本记录不了烹饪增加的数量,所以如果你打算做菜,建议第一次运行前或最后一次运行后做菜。
账户名只允许使用数字中英文同时长度在20个字符以内。

View File

@@ -0,0 +1,435 @@
let userName = settings.userName || "默认账户";
const recoveryFoodName = settings.recoveryFoodName || "回血药名字没填";
const resurrectionFoodName = settings.resurrectionFoodName || "复活药名字没填";
const ocrRegion = {
x: 150,
y: 250,
width: 220,
height: 270
};
(async function () {
// 检验账户名
async function getUserName() {
userName = userName.trim();
//数字中英文长度在20个字符以内
if (!userName || !/^[\u4e00-\u9fa5A-Za-z0-9]{1,20}$/.test(userName)) {
log.error(`账户名${userName}违规暂时使用默认账户名请查看readme后修改`)
userName = "默认账户";
}
return userName;
}
/**
* 文字OCR识别封装函数支持空文本匹配任意文字
* @param {string} text - 要识别的文字,默认为"空参数",空字符串会匹配任意文字
* @param {number} timeout - 超时时间单位为秒默认为10秒
* @param {number} afterBehavior - 点击模式0=不点击1=点击文字位置2=按F键默认为0
* @param {number} debugmodel - 调试模式0=无输出1=基础日志2=详细输出3=立即返回默认为0
* @param {number} x - OCR识别区域起始X坐标默认为0
* @param {number} y - OCR识别区域起始Y坐标默认为0
* @param {number} w - OCR识别区域宽度默认为1920
* @param {number} h - OCR识别区域高度默认为1080
* @param {number} matchMode - 匹配模式0=包含匹配1=精确匹配默认为0
* @returns {object} 包含识别结果的对象 {text, x, y, found}
*/
async function textOCREnhanced(
text = "空参数",
timeout = 10,
afterBehavior = 0,
debugmodel = 0,
x = 0,
y = 0,
w = 1920,
h = 1080,
matchMode = 0
) {
const startTime = Date.now();
const timeoutMs = timeout * 1000;
let lastResult = null;
let captureRegion = null; // 用于存储截图对象
// 只在调试模式1下输出基本信息
if (debugmodel === 1) {
if (text === "") {
log.info(`OCR: 空文本模式 - 匹配任意文字`);
} else if (text === "空参数") {
log.warn(`OCR: 使用默认参数"空参数"`);
}
}
while (Date.now() - startTime < timeoutMs) {
try {
// 获取截图并进行OCR识别
captureRegion = captureGameRegion();
const resList = captureRegion.findMulti(RecognitionObject.ocr(x, y, w, h));
// 遍历识别结果
for (let i = 0; i < resList.count; i++) {
const res = resList[i];
// 检查是否匹配
let isMatched = false;
if (text === "") {
// 空文本匹配任意文字
isMatched = true;
} else if (matchMode === 1) {
// 精确匹配
isMatched = res.text === text;
} else {
// 包含匹配(默认)
isMatched = res.text.includes(text);
}
if (isMatched) {
// 只在调试模式1下输出匹配成功信息
if (debugmodel === 1) {
log.info(`OCR成功: "${res.text}" 位置(${res.x},${res.y})`);
}
// 调试模式3: 立即返回
if (debugmodel === 3) {
// 释放内存
if (captureRegion) {
captureRegion.dispose();
}
return { text: res.text, x: res.x, y: res.y, found: true };
}
// 执行后续行为
switch (afterBehavior) {
case 1: // 点击文字位置
await sleep(1000);
click(res.x, res.y);
break;
case 2: // 按F键
await sleep(100);
keyPress("F");
break;
default:
// 不执行任何操作
break;
}
// 记录最后一个匹配结果但不立即返回
lastResult = { text: res.text, x: res.x, y: res.y, found: true };
}
}
// 释放截图对象内存
if (captureRegion) {
captureRegion.dispose();
}
// 如果找到匹配结果,根据调试模式决定是否立即返回
if (lastResult && debugmodel !== 2) {
return lastResult;
}
// 短暂延迟后继续下一轮识别
await sleep(100);
} catch (error) {
// 发生异常时释放内存
if (captureRegion) {
captureRegion.dispose();
}
log.error(`OCR异常: ${error.message}`);
await sleep(100);
}
}
if (debugmodel === 1) {
// 超时处理
if (text === "") {
log.info(`OCR超时: ${timeout}秒内未找到任何文字`);
} else {
log.info(`OCR超时: ${timeout}秒内未找到"${text}"`);
}
}
// 返回最后一个结果或未找到
return lastResult || { found: false };
}
/**
* 判断任务是否已刷新
* @param {string} filePath - 存储最后完成时间的文件路径
* @param {object} options - 配置选项
* @param {string} [options.refreshType] - 刷新类型: 'hourly'|'daily'|'weekly'|'monthly'|'custom'
* @param {number} [options.customHours] - 自定义小时数(用于'custom'类型)
* @param {number} [options.dailyHour=4] - 每日刷新的小时(0-23)
* @param {number} [options.weeklyDay=1] - 每周刷新的星期(0-6, 0是周日)
* @param {number} [options.weeklyHour=4] - 每周刷新的小时(0-23)
* @param {number} [options.monthlyDay=1] - 每月刷新的日期(1-31)
* @param {number} [options.monthlyHour=4] - 每月刷新的小时(0-23)
* @returns {Promise<boolean>} - 是否已刷新
*/
async function checkRefreshStatus(filePath, options = {}) {
const {
refreshType = 'daily', // 默认每小时刷新
customHours = 24, // 自定义刷新小时数默认24
dailyHour = 4, // 每日刷新默认凌晨4点
weeklyDay = 1, // 每周刷新默认周一(0是周日)
weeklyHour = 4, // 每周刷新默认凌晨4点
monthlyDay = 1, // 每月刷新默认第1天
monthlyHour = 4 // 每月刷新默认凌晨4点
} = options;
try {
// 读取文件内容
let content = await file.readText(filePath);
const parts = content.split("|");
if (parts.length < 3) {
return { refreshed: true, recovery: 0, resurrection: 0 };
}
const lastTime = new Date(parts[0]);
const savedRecovery = parseInt(parts[1]) || 0;
const savedResurrection = parseInt(parts[2]) || 0;
const nowTime = new Date();
let shouldRefresh = false;
switch (refreshType) {
case 'hourly': // 每小时刷新
shouldRefresh = (nowTime - lastTime) >= 3600 * 1000;
break;
case 'daily': // 每天固定时间刷新
// 检查是否已经过了当天的刷新时间
const todayRefresh = new Date(nowTime);
todayRefresh.setHours(dailyHour, 0, 0, 0);
// 如果当前时间已经过了今天的刷新时间,检查上次完成时间是否在今天刷新之前
if (nowTime >= todayRefresh) {
shouldRefresh = lastTime < todayRefresh;
} else {
// 否则检查上次完成时间是否在昨天刷新之前
const yesterdayRefresh = new Date(todayRefresh);
yesterdayRefresh.setDate(yesterdayRefresh.getDate() - 1);
shouldRefresh = lastTime < yesterdayRefresh;
}
break;
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;
} else {
// 否则检查上次完成时间是否在上周刷新之前
const lastWeekRefresh = new Date(thisWeekRefresh);
lastWeekRefresh.setDate(lastWeekRefresh.getDate() - 7);
shouldRefresh = lastTime < lastWeekRefresh;
}
break;
case 'monthly': // 每月固定时间刷新
// 获取本月的刷新时间
const thisMonthRefresh = new Date(nowTime);
// 设置为本月指定日期的凌晨
thisMonthRefresh.setDate(monthlyDay);
thisMonthRefresh.setHours(monthlyHour, 0, 0, 0);
// 如果当前时间已经过了本月的刷新时间
if (nowTime >= thisMonthRefresh) {
shouldRefresh = lastTime < thisMonthRefresh;
} else {
// 否则检查上次完成时间是否在上月刷新之前
const lastMonthRefresh = new Date(thisMonthRefresh);
lastMonthRefresh.setMonth(lastMonthRefresh.getMonth() - 1);
shouldRefresh = lastTime < lastMonthRefresh;
}
break;
case 'custom': // 自定义小时数刷新
shouldRefresh = (nowTime - lastTime) >= customHours * 3600 * 1000;
break;
default:
throw new Error(`未知的刷新类型: ${refreshType}`);
}
return {
refreshed: shouldRefresh,
recovery: savedRecovery,
resurrection: savedResurrection
};
} catch (error) {
// 文件不存在时视为需要刷新
return { refreshed: true, recovery: 0, resurrection: 0 };
}
}
// 背包过期物品识别需要在背包界面并且是1920x1080分辨率下使用
async function handleExpiredItems() {
const ifGuoqi = await textOCREnhanced("物品过期", 1.5, 0, 3, 870, 280, 170, 40);
if (ifGuoqi.found) {
log.info("检测到过期物品,正在处理...");
await sleep(500);
await click(980, 750); // 点击确认按钮,关闭提示
}
else { log.info("未检测到过期物品"); }
}
async function recognizeNumberByOCR(ocrRegion, pattern) {
let captureRegion = null;
try {
const ocrRo = RecognitionObject.ocr(ocrRegion.x, ocrRegion.y, ocrRegion.width, ocrRegion.height);
captureRegion = captureGameRegion();
const resList = captureRegion.findMulti(ocrRo);
if (!resList || resList.length === 0) {
log.warn("OCR未识别到任何文本");
return null;
}
for (const res of resList) {
if (!res || !res.text) {
continue;
}
const numberMatch = res.text.match(pattern);
if (numberMatch) {
const number = parseInt(numberMatch[1] || numberMatch[0]);
if (!isNaN(number)) {
return number;
}
}
}
}
catch (error) {
log.error(`OCR识别时发生异常: ${error.message}`);
}
finally {
if (captureRegion) {
captureRegion.dispose();
}
}
return null;
}
async function getFoodNum(){
keyPress("B");//打开背包
await handleExpiredItems(); //处理过期物品弹窗
await sleep(2000);
click(863, 51);//选择食物
await sleep(1000);
click(170, 1020);//筛选
await sleep(1000);
click(195, 1020);//重置
await sleep(1000);
click(110, 110);//输入名字
await sleep(1000);
inputText(recoveryFoodName);
await sleep(500);
click(490, 1020);//确认筛选
await sleep(1000);
var recoveryNumber=await recognizeNumberByOCR(ocrRegion,/\d+/) //识别回血药数量
// 处理回血药识别结果
if (recoveryNumber === null) {
recoveryNumber = 0;
notification.send(`未识别到回血药数量设置数量为0药品名${recoveryFoodName}`)
}
await sleep(1000);
click(170, 1020);//筛选
await sleep(1000);
click(195, 1020);//重置
await sleep(1000);
click(110, 110);//输入名字
await sleep(1000);
inputText(resurrectionFoodName);
await sleep(500);
click(490, 1020);//确认筛选
await sleep(1000);
var resurrectionNumber=await recognizeNumberByOCR(ocrRegion,/\d+/) //识别复活药数量
// 处理复活药识别结果
if (resurrectionNumber === null) {
resurrectionNumber = 0;
notification.send(`未识别到复活药数量设置数量为0药品名${resurrectionFoodName}`)
}
await sleep(1000);
click(170, 1020);//筛选
await sleep(1000);
click(195, 1020);//重置
await sleep(1000);
click(490, 1020);//确认筛选
await genshin.returnMainUi();
return { recoveryNumber, resurrectionNumber };
}
async function main() {
// 设置分辨率和缩放
setGameMetrics(1920, 1080, 1);
// 点击领月卡
await genshin.blessingOfTheWelkinMoon();
await sleep(1000);
await genshin.returnMainUi();
await sleep(1000);
// 获取食物数量
return await getFoodNum();
}
// 主执行流程
userName = await getUserName();
const recordPath = `assets/${userName}.txt`;
// 检查刷新状态
const refreshStatus = await checkRefreshStatus(recordPath, { dailyHour: 4 });
// 获取当前药物数量
const { recoveryNumber, resurrectionNumber } = await main();
// initSelect处理逻辑
if (settings.initSelect) {
// 获取当前时间戳
const currentTime = new Date().toISOString();
// 构建保存内容:时间戳|回血药数量|复活药数量
const saveContent = `${currentTime}|${recoveryNumber}|${resurrectionNumber}`;
try {
// 写入本地文件
await file.writeText(recordPath, saveContent);
notification.send(`${userName}: 强制初始化完成!${recoveryFoodName}${recoveryNumber}个, ${resurrectionFoodName}${resurrectionNumber}`);
} catch (error) {
notification.send(`${userName}: 药品保存失败!`);
}
return; // 终止后续流程
}
if (refreshStatus.refreshed) {
// 今日未初始化 - 写入初始数量
await file.writeText(recordPath, `${new Date().toISOString()}|${recoveryNumber}|${resurrectionNumber}`);
// 添加账户名称的通知
notification.send(`${userName}: 今日初始化完成!${recoveryFoodName}${recoveryNumber}个, ${resurrectionFoodName}${resurrectionNumber}`);
} else {
// 使用初始数量进行对比
const initialRecovery = refreshStatus.recovery;
const initialResurrection = refreshStatus.resurrection;
// 计算消耗/增加数量
const diffRecovery = initialRecovery - recoveryNumber;
const diffResurrection = initialResurrection - resurrectionNumber;
let logMsg = "";
if (diffRecovery > 0 || diffResurrection > 0) {
// 数量减少
logMsg = `${userName}: 今日消耗:${recoveryFoodName}${diffRecovery}个,${resurrectionFoodName}${diffResurrection}`;
} else if (diffRecovery < 0 || diffResurrection < 0) {
// 数量增加
const addRecovery = -diffRecovery;
const addResurrection = -diffResurrection;
logMsg = `${userName}: 今日新增:${recoveryFoodName}${addRecovery}个,${resurrectionFoodName}${addResurrection}`;
} else {
// 数量无变化
logMsg = `${userName}: 今日药物数量无变化`;
}
// 添加库存信息
logMsg += ` | 当前库存:${recoveryFoodName}${recoveryNumber}个, ${resurrectionFoodName}${resurrectionNumber}`;
// 发送通知
notification.send(logMsg);
}
})();

View File

@@ -0,0 +1,18 @@
{
"manifest_version": 1,
"name": "吃药统计",
"version": "1.0",
"bgi_version": "0.51",
"description": "用于统计指定两个食物的消耗,推荐锄地前后使用",
"authors": [
{
"name": "勺子",
"links": "https://github.com/skyflag2022"
}
],
"settings_ui": "settings.json",
"main": "main.js",
"saved_files": [
"assets/*.txt"
]
}

View File

@@ -0,0 +1,26 @@
[
{
"name": "userName",
"type": "input-text",
"label": "账户名称\n用于多账户运行时区分不同账户",
"default": "默认账户"
},
{
"name": "initSelect",
"type": "checkbox",
"label": "重新初始化药品数量",
"default": false
},
{
"name": "recoveryFoodName",
"type": "input-text",
"label": "回血药名称",
"default": ""
},
{
"name": "resurrectionFoodName",
"type": "input-text",
"label": "复活药名称",
"default": ""
}
]