From 5223a95ab26d475a79c25caeafff69fa1119c7e2 Mon Sep 17 00:00:00 2001 From: JJMdzh Date: Thu, 9 Oct 2025 00:42:19 +0800 Subject: [PATCH] =?UTF-8?q?=E8=83=8C=E5=8C=85=E7=BB=9F=E8=AE=A1JS=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E7=8B=AC=E7=AB=8B=E5=90=8D=E5=8D=95=E6=8B=BE=E5=8F=96?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=8A=A0=E5=BC=B9=E7=AA=97=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=8A=A0=E5=AF=B9=E6=80=AA=E7=89=A9=E5=90=8D?= =?UTF-8?q?=E7=9A=84=E6=94=AF=E6=8C=81=20(#2101)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 背包js增加独立名单拾取,增加弹窗模块,增加对怪物名的支持 v2.5 增加独立名单拾取,增加弹窗模块,这个2个是背包材料统计JS运行时实时运行。增加对怪物名的支持 * Delete repo/js/背包材料统计/materialsCD directory * Add files via upload * Add files via upload 更新说明 * Update manifest.json --- repo/js/背包材料统计/README.md | 82 +- repo/js/背包材料统计/assets/E_Dialogue.png | Bin 0 -> 547 bytes repo/js/背包材料统计/assets/F_Dialogue.png | Bin 0 -> 515 bytes .../背包材料统计/assets/Monster-Materials.txt | 53 + repo/js/背包材料统计/assets/XP.png | Bin 0 -> 2863 bytes .../信件/Picture/snipaste_20250630_191227.png | Bin 0 -> 17524 bytes .../assets/imageClick/信件/icon/Back4.png | Bin 0 -> 2933 bytes .../assets/imageClick/调查/Picture/image6.png | Bin 0 -> 14139 bytes .../assets/imageClick/调查/icon/123.png | Bin 0 -> 698 bytes .../assets/imageClick/过期物品/Picture/image1.png | Bin 0 -> 14969 bytes .../assets/imageClick/过期物品/icon/Confirm.png | Bin 0 -> 3851 bytes repo/js/背包材料统计/assets/滚轮下翻.json | 3 + repo/js/背包材料统计/lib/autoPick.js | 234 ++ repo/js/背包材料统计/lib/backStats.js | 733 ++++ repo/js/背包材料统计/lib/displacement.js | 62 + repo/js/背包材料统计/lib/exp.js | 159 + repo/js/背包材料统计/lib/file.js | 153 + repo/js/背包材料统计/lib/imageClick.js | 195 ++ repo/js/背包材料统计/lib/region.js | 151 + repo/js/背包材料统计/main.js | 3062 ++++++++--------- repo/js/背包材料统计/manifest.json | 12 +- repo/js/背包材料统计/materialsCD/怪物.txt | 2 + .../materialsCD/{掉落CD.txt => 掉落.txt} | 0 repo/js/背包材料统计/materialsCD/狗粮.txt | 3 + .../materialsCD/{资源CD.txt => 采集.txt} | 14 +- repo/js/背包材料统计/settings.json | 29 +- repo/js/背包材料统计/targetText/交互.txt | 3 + repo/js/背包材料统计/targetText/宝箱.txt | 4 + repo/js/背包材料统计/targetText/掉落.txt | 2 + repo/js/背包材料统计/targetText/采集.txt | 10 + 30 files changed, 3240 insertions(+), 1726 deletions(-) create mode 100644 repo/js/背包材料统计/assets/E_Dialogue.png create mode 100644 repo/js/背包材料统计/assets/F_Dialogue.png create mode 100644 repo/js/背包材料统计/assets/Monster-Materials.txt create mode 100644 repo/js/背包材料统计/assets/XP.png create mode 100644 repo/js/背包材料统计/assets/imageClick/信件/Picture/snipaste_20250630_191227.png create mode 100644 repo/js/背包材料统计/assets/imageClick/信件/icon/Back4.png create mode 100644 repo/js/背包材料统计/assets/imageClick/调查/Picture/image6.png create mode 100644 repo/js/背包材料统计/assets/imageClick/调查/icon/123.png create mode 100644 repo/js/背包材料统计/assets/imageClick/过期物品/Picture/image1.png create mode 100644 repo/js/背包材料统计/assets/imageClick/过期物品/icon/Confirm.png create mode 100644 repo/js/背包材料统计/assets/滚轮下翻.json create mode 100644 repo/js/背包材料统计/lib/autoPick.js create mode 100644 repo/js/背包材料统计/lib/backStats.js create mode 100644 repo/js/背包材料统计/lib/displacement.js create mode 100644 repo/js/背包材料统计/lib/exp.js create mode 100644 repo/js/背包材料统计/lib/file.js create mode 100644 repo/js/背包材料统计/lib/imageClick.js create mode 100644 repo/js/背包材料统计/lib/region.js create mode 100644 repo/js/背包材料统计/materialsCD/怪物.txt rename repo/js/背包材料统计/materialsCD/{掉落CD.txt => 掉落.txt} (100%) create mode 100644 repo/js/背包材料统计/materialsCD/狗粮.txt rename repo/js/背包材料统计/materialsCD/{资源CD.txt => 采集.txt} (68%) create mode 100644 repo/js/背包材料统计/targetText/交互.txt create mode 100644 repo/js/背包材料统计/targetText/宝箱.txt create mode 100644 repo/js/背包材料统计/targetText/掉落.txt create mode 100644 repo/js/背包材料统计/targetText/采集.txt diff --git a/repo/js/背包材料统计/README.md b/repo/js/背包材料统计/README.md index 0febace29..7b240d935 100644 --- a/repo/js/背包材料统计/README.md +++ b/repo/js/背包材料统计/README.md @@ -1,58 +1,37 @@ -// ==UserScript== -// @name 背包统计采集系统 -// @version 2.42 -// @description 识别路径文件,根据材料数量,自动执行路线,或者主动选择材料类别,统计材料数量 -// @author 吉吉喵 -// @match 原神版本:5.6;BGI 版本:0.44.8 -// ==/UserScript== -/** - * === 重要免责声明 === - * 1. 使用风险 - * - 本脚本为开源学习项目,禁止用于商业用途或违反游戏条款的行为。 - * - 滥用可能导致游戏账号封禁,开发者不承担任何直接或间接责任。 - * - * 2. 责任限制 - * - 本脚本按“现状”提供,不承诺兼容性、安全性或功能完整性。 - * - 因使用本脚本导致的账号、数据、设备损失,开发者概不负责。 - * - * 3. 禁止条款 - * - 严禁逆向工程、恶意篡改或用于外挂等非法用途。 - * - 如游戏运营商提出要求,开发者保留随时停止维护的权利。 - * - * 使用即表示您已阅读并同意上述条款。 - * - * Last Updated: 2025-09-23 - */ -# 背包材料统计 +# 背包材料统计 v2.5 ## 简介: -背包材料统计,可统计背包养成道具、部分食物、素材的数量,并保存本地;还可根据设定数量,自动根据CD执行采集路径。 +可统计背包养成道具、部分食物、素材的数量;根据设定数量、根据材料刷新CD执行挖矿、采集、刷怪等的路径。优势: ++ 1.自动判断材料CD,不需要管材料CD有没有好; ++ 2.可以随意添加路径,能自动排除低效、无效路径; ++ 3.有独立名单识别,不会交互路边的npc或是神像; ++ 4.有实时的弹窗模块,提供了常见的几种; -## 文件结构 - -./📁BetterGI/📁User/📁JsScript/ -📁背包材料统计/ - 📁pathing/ - 📁 薄荷/ - 📄 薄荷1.json - 📁 薄荷效率/ - 📄 薄荷-吉吉喵.json - 📁 苹果/ - 📄 旅行者的果园.json - 📁 名刀镡/ - 📄 旅行者的名刀镡.json - 📁history_record/ 背包统计 自动生成,每类的历史记录(每类中旧纪录上限为365个) - 📁overwrite_record/ 背包统计 自动生成,每类的最新一次记录 - 📁pathing_record/ 自动生成,路径运行时间记录 - 📁materialsCD/ 路径CD管理 自带的CD文件 - 📄 latest_record.txt 背包统计 自动生成,最新一次统计 - 📁assets 图包 +## 用前须知:要有一点的动手能力!该JS不提供路径文件,需要文件夹操作 ## 使用方法 -1. 将脚本添加至调度器。 -2. 右键点击脚本以修改JS自定义配置:刷怪、采集推荐:2.仅📁pathing材料;纯材料数量识别选3.仅【材料分类】勾选。 -3. 执行路径功能前,需要📁pathing有【材料正式名】的文件夹,具体可参考BetterGI\Repos\bettergi-scripts-list-git\repo\pathing目录,选取【材料正式名】的文件夹,内有子文件夹或者路径。复制到BetterGI\User\JsScript\背包材料统计\pathing目录,具体参考## 文件结构 + +## 基础教程(会订阅路径、添加JS的可跳过) ++ 1.仓库 订阅 所需路径文件; ++ 2.打开路径所在文件夹BetterGI\Repos\bettergi-scripts-list-git\repo\pathing; + 把地方特产、敌人与魔物、矿物、食材与炼金四个文件夹复制到背包材料统计JS目录BetterGI\User\JsScript\背包材料统计\pathing中;最好手动去除重复多余路径; ++ 3.推荐根据队伍来把路径分组放置。比如 + 背包统计刷怪组,适合挂机输出的队:火神 奶奶 钟离 万叶,能够胜任需要刷怪的路径; + 背包统计附魔材料组,附魔队:钟离 芭芭拉 久岐忍 砂糖或班尼特,适合附魔采集; + 背包统计采集组,生存队:迪希雅 芭芭拉 瑶瑶 草神。适合一般情况下的采集; ++ 4.找到背包材料统计js,右键 背包材料统计,选择JS修改脚本自定义设置; + +## JS的自定义设置说明 ++ 1.目标数量,是扫描背包中材料数量,只有低于目标的材料,其路径才会被纳入执行序列; ++ 2.优先级材料,是最高优先级,直接无视上述目标数量,纳入执行序列最顶层; ++ 3.时间成本,在一个路径存在3到5次记录时,会计算时间成本,单个材料获取时间超过默认30秒的,则跳过; ++ 4.发送通知,每类材料跑完会通知一次,全部材料跑完会汇总通知一次。需在BGI通知里开启,接收端企业微信的使用自行寻找; ++ 5.取消扫描数量,是取消每个路径执行完的扫描;当大部分需要的路径都有足够多记录,就可以不需要新增运行记录,可以不扫描了,以节约时间;但全部材料执行始末会各扫一次,以汇总材料信息。 ++ 6.仅pathing材料,是平衡背包材料统计和路径CD管理的选项;选择仅pathing材料,则直接无视下方勾选的材料分类,只扫描pathing文件夹已有的路径材料,没有的就不扫,以缩短扫描时间; ++ 7.弹窗名,不填默认assets\imageClick文件夹下所有弹窗循环执行;弹窗模块会在背包统计运行时全程保护路径,防止弹窗影响路径执行; ++ 8.采用的CD分类,不在materialsCD文件夹的不执行,这个文件夹可以按格式新增材料CD分类txt,只要背包材料统计的图库里有,路径所在的文件夹名正确,就能按CD执行; ++ 9.targetText文件夹里的是独立名单识别的名单,可以按随意新增txt,内容按格式,英文冒号前的名字也随便写;冒号后的文字会被当成目标来识别,不在targetText文件夹下不识别。 ## 注意 因食物部分图片未补足,为适配快速滑页,苹果、日落果、星蕈、活化的星蕈、枯焦的星蕈、泡泡桔、烛伞蘑菇、美味的宝石闪闪,这八个食物必须有,且在第一行。不然这几个食物会无法识别。 @@ -81,7 +60,8 @@ + v2.27 修复计算材料数错误、目标数量临界值、"3"识别成"三"等bug + v2.28 材料更变时初始数量更新;正常记录排除0位移和0数量的路径记录(可能是卡路径,需手动根据0记录去甄别),新增材料名0后缀本地记录;新增背包弹窗识别 + v2.29 新增排除提示;调整平均时间成本计算;过滤掉差异较大的记录; -+ v2.30 更改路径专注模式默认值,加log提示;去除注释掉的调试log;背包材料统计更改名为背包统计采集系统 ++ v2.30 更改路径专注模式默认值,加log提示;去除注释掉的调试log; + v2.40 优化背包识别时占用的内存;增加通知; + v2.41 修复勾选分类的本地记录bug,新增仅背包统计选项;如果本地记录已经遭到破坏。比如每条路径都产生大量材料名-0.txt,就只能手动清理或者删除本地记录pathing_record,重新跑; -+ v2.42 增加无路径间的扫描、无数量记录的noRecord模式,适合路径记录已经炼成的玩家。新增怪物材料CD文件,以支持轻度刷怪需求。 \ No newline at end of file ++ v2.42 增加无路径间的扫描、无数量记录的noRecord模式,适合路径记录已经炼成的玩家。新增怪物材料CD文件,以支持轻度刷怪需求。 ++ v2.5 增加独立名单拾取,增加弹窗模块,这个2个是背包材料统计JS运行时实时运行。增加对怪物名的支持。 \ No newline at end of file diff --git a/repo/js/背包材料统计/assets/E_Dialogue.png b/repo/js/背包材料统计/assets/E_Dialogue.png new file mode 100644 index 0000000000000000000000000000000000000000..9114ff5bf4a3e232f971a679f78dd28e564ee5c1 GIT binary patch literal 547 zcmV+;0^I$HP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0k}y-K~z{r?Up;L z!ax*;|M-5OC&Ksto$^%|SaMkpH5XcWOe z8lhUP;&?oAok%25uh$U@g}8peUbkAUFc=KbXfzOy$Hg6O$Oa~p2`ZHew%aY&*=!b_ zP6v@lgzH!K`#ro~FPu)Nh-mY*EC>VwhLbbwI$-(?#9}d&%VoaQugJ2DVzJ0G#R$^| zK2azX&}y|vg~;?4csw2?lS!o0>9>(erQmkE#gm`;S{hQZv*7<4xLhu{-|s@{W7@#= zdgV=Lxm-St#bSZ!bjs6^eKvxe1;gPG&1Ms)(}`{-41JvVm`5 l*8$src)= literal 0 HcmV?d00001 diff --git a/repo/js/背包材料统计/assets/F_Dialogue.png b/repo/js/背包材料统计/assets/F_Dialogue.png new file mode 100644 index 0000000000000000000000000000000000000000..314a85f134cbc0dd620dea029b010905c6415165 GIT binary patch literal 515 zcmV+e0{s1nP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0hmcdK~z{r?UucY z!ax*8_nG_y!6GkUsR)W-uOLPcyMQlXXKN>^f>w5dT8JnqM6eOGuuw!yAt-M0$%KpS zZXh$p6<5{+A0gb_$sxHU^MNN~TAz5T0OKw2_XSMTgkcz1E|*v=7TmDww%aXr^Wh2{ z4hM8P9Tba2WV2ara=9F;)hafd4Xxk~SKxFyVKSMZR4T#e^9d!HOhVUnTEQKm88{w~ zXti4K`~CI}*fAcDLsivpX*3$B)oQ3zDrmRc)a{P&5O%v=1cSk=Kt7+hpU&4X(cNx` z*=&aSd`_M22n+OjJ%mD`t3aVppo$aAvh3j%;UNOI@VWx2RLUN-pXvAeSg+UA#mAEX zOC%COEEaQw4d-Ampe{b11tdvAI-Pc;TrOiY8c`P?p$zVJlJ2IV4vET2hi_hy0 z;r|v01Okv{nJV|;Ngx~!Ls1l}+=nNDXfz5<)2MPE!UF6I$38xZL<04CU3kO6Ls($7 zTA|r&VmKV$wB_OxB$fb_r!DV!r%Y^002ovPDHLk FV1jS-*o^=H literal 0 HcmV?d00001 diff --git a/repo/js/背包材料统计/assets/Monster-Materials.txt b/repo/js/背包材料统计/assets/Monster-Materials.txt new file mode 100644 index 000000000..7c8f0a10f --- /dev/null +++ b/repo/js/背包材料统计/assets/Monster-Materials.txt @@ -0,0 +1,53 @@ +丘丘人:破损的面具,污秽的面具,不祥的面具 +丘丘萨满:导能绘卷,封魔绘卷,禁咒绘卷 +丘丘人射手:锐利的箭簇,牢固的箭簇,历战的箭簇 +丘丘暴徒/丘丘王:沉重号角,黑铜号角,黑晶号角 +丘丘游侠:来自何处的待放之花,何人所珍藏之花,漫游者的盛放之花 + +愚人众先遣队:新兵的徽记,士官的徽记,尉官的徽记 +萤术士:雾虚灯芯,雾虚草囊,雾虚花粉 +债务处理人:特工祭刀,猎兵祭刀,督察长祭刀 +冬国仕女:黯淡棱晶,水晶棱晶,偏光棱晶 +愚人众风役人:老旧的役人怀表,役人的制式怀表,役人的时时刻刻 +愚人众特辖队:磨损的执凭,精致的执凭,霜镌的执凭 + +盗宝团:寻宝鸦印,藏银鸦印,攫金鸦印 +野伏众/海乱鬼:破旧的刀镡,影打刀镡,名刀镡 +镀金旅团:褪色红绸,镶边红绸,织金红绸 +黑蛇众/黯色空壳:晦暗刻像,夤夜刻像,幽邃刻像 +部族龙形武士:卫从的木哨,战士的铁哨,龙冠武士的金哨 + +遗迹机械:混沌装置,混沌回路,混沌炉心 +元能构装体:混浊棱晶,破缺棱晶,辉光棱晶 +遗迹机兵:混沌机关,混沌枢纽,混沌真眼 +遗迹龙兽:混沌锚栓,混沌模块,混沌容器 +发条机关:啮合齿轮,机关正齿轮,奇械机芯齿轮 +秘源机兵:秘源轴,秘源机鞘,秘源真芯 +巡陆艇:毁损机轴,精制机轴,加固机轴 + +史莱姆:史莱姆凝液,史莱姆原浆,史莱姆清 +骗骗花:骗骗花蜜,微光花蜜,原素花蜜 +飘浮灵:浮游干核,浮游幽核,浮游晶化核 +蕈兽:蕈兽孢子,荧光孢粉,孢囊晶尘 +蕈兽:失活菌核,休眠菌核,茁壮菌核 +浊水幻灵:浊水的一滴,浊水的一掬,初生的浊水幻灵 +原海异种:异海凝珠,异海之块,异色结晶石 +隙境原体:隙间之核,外世突触,异界生命核 +魔像禁卫:残毁的剑柄,裂断的剑柄,未熄的剑柄 +大灵显化身:意志破碎的残片,意志明晰的寄偶,意志巡游的符像 +熔岩游像:聚燃的石块,聚燃的命种,聚燃的游像眼 + +龙蜥:脆弱的骨片,结实的骨片,石化的骨片 +圣骸兽:残毁的横脊,密固的横脊,锲纹的横脊 +玄文兽:羽状鳍翅,月色鳍翅,渊光鳍翅 +纳塔龙众:稚嫩的尖齿,老练的坚齿,横行霸者的利齿 +炉壳山鼬:冷裂壳块,蕴热的背壳,明燃的棱状壳 +蕴光异兽:失光块骨,稀光遗骼,繁光躯外骸 + +深渊法师/深渊使徒:地脉的旧枝,地脉的枯叶,地脉的新芽 +兽境群狼:隐兽利爪,隐兽指爪,隐兽鬼爪 +深邃拟覆叶:折光的胚芽,迷光的蜷叶之心,惑光的阔叶 +荒野狂猎:幽雾片甲,幽雾兜盔,幽雾化形 +霜夜灵嗣:霜夜的煌荣,霜夜的柔辉,霜夜的残照 + +地脉花:流浪者的经验,冒险家的经验,大英雄的经验 \ No newline at end of file diff --git a/repo/js/背包材料统计/assets/XP.png b/repo/js/背包材料统计/assets/XP.png new file mode 100644 index 0000000000000000000000000000000000000000..c5fca1cd9ff271d8ebc180b5e20a83227f1d0975 GIT binary patch literal 2863 zcmV+~3()k5P))NYQL2TJ2l`%rK~?^>dy)+%OaI_>1xZdXRw#y{vKLevt-jLl+pkQ@#4-P zHUcDtq3rog+bvABwdH@ zt~L&h^l|*)0I5_Q+p>X|wq73)K(SQicNea4ar`b%<`&UPlS##C&1K0oW$=T5ZyztP zT=lRmi8cldbZ=^*sVPI;wkedW%*-y5FO~^IO)8mSWU!aFPakA^Z#%YSVT^ehkTwwI zXpmBnUs~p${{0iKe)RyQ6+#GFnlfzb=^&koQz(^*$6T`66c@&B;QKzIf@22@gu99m^v$-S3_>D<|Fu-Vo zW!YrXF5O-2xQ@-_)Dsq#N@!zfZ_V-UnM0fz9l_{`e_|~lA;9wk{^lQl!}yI!T+708 zZMJl_(9)b??rDK1b4%2GA6r_4TH)9>dxy62!^6Y0=Cb_tKl~?!LK$I2^w7o-hMH_T zN&k*b*wW_W7k992Fj|nyHSnuHKg+<5%}8k>gh57Z7x-bw2Oo`b>+Upe%%Y|OS{l=2 zG6|l{EHN{;L=c7%i6z*U#1BGl-J53bz!tV|?O?fFLu-W)vR-k4V@t}O$K=#3feIKs zI*ir=%YylZ0v~>Sh0<~rgjwm5zz=-By!ntnT)B2vU2CdY0BozXTp;)T$?c+s2Fx20T=ZCnCHw$kg7|C;>N8h?oB?ytNF-^SL5#FH1{4p!IA={4QBkChLFOh=7%rIh?IgLG(620DHJPg?r0%U zig?W7@m!wkw~sMF=pOn&&g0e?~(#h0&4Bgki|!)GTvP3&_QMk*Vo< z3}&?oPRwSx>JtX)CE*G~g_IUMw|8;ow(gKX3Qt{AQBkboZ6NDiK!Hz8*j2_uddvg}0 z4IoyN$)pqf_}CtX2e#nE9Q@#UP)0`lQ^v5ex@Ad9Wsk{kX0RVj%~7sYu`F570(@U# zTM}CcjEro-_X84%IDNgHoIJ9dL;L#B>zGauD*Co<;>?K=l-5k#d4yNpH91MHve$DHzz<&3@NqcxMm2J#C~DaSQ^jHHh!} zzs^XrrVR?$b@|cJVU8adh>rKIhdiEH#27<1ouXLhuMh%(!FKAs)LP?*nwDGxEzMbe zcz8EkyW1jOD1}n$729tdQMJHvY<6tvpnqEzi%SL0U%Y`3@X6Q&F&$Zq8*>n&Z~ui+ zN+E@$Z(9f3dNy(A{xow7MK*WlIC*%OL;Lz@ZE3`qNLkmL^wtLxsdUV-DU`}wx&Af3 zzjTv34`y&3$&vm2bhbA$cJ(%oXYQ4TuSn;sOb9v5>-QW+O-=SRgFgo%%Q?FiEM>B){Jy=3gESC{~ z_xJy7&Rx8Y3POyCj20u1Qq)@+{T#=pF`MT2p+Vj`eUN0*eaW*{B2r()xzDb1er$r7 z`8<9QVhMwkYertJ5k?SCx>!Oow5ta@n@tdgnsU{{u`Ps%#*39KEh`!olv2z;ea3Ik zkF$_3@n`QGqb1j{u8{_Q$iIE?DSsTlN4Zh~z_H}&Sh5mUUE{oRjhYJ4N+Y{FTF5pe zR{OmXKP*e)IFfRu!o@G{@E;$I;rroprYnTN_XGa*gHO3|d4gxf5|$;gZR;g-UQIv< zv@j&&E?u2DWOGxRuJ-2Vgzi2=}894gf+pM8FtnYksT6bJ;q?{o3<+bmbA ztIGenFvcK-U~5l1ovlsCRMKUzzlYxLHoRK!+A2Zhca~*QDOdUU_g`XL7Pez?>FQk; zmWuTW;QMKVAB1GHX%6h`Bb!YkrI55XXL)mPAL(SAAP6z*yuiH}wk4_hKA&EiKx2>s ze)Gu}RLare_&%Yv#DBwUUk7~sH2KN$qDwW{u=ssp<^E`MogH62SPdua{ znZOnhd=D_j7=lpa#$z0PbC5lQTUfEMRR&uE`nPrQ&QA`}oXcW_L~FAlr%dz#fW~Z^ z{8EvJ-_DY4NFnNix>0DYQ7UTHTcdCAr)LkZf`K)YgQ?#-*qK6^YZ_FGsn;W*7%(RL$WcmVVl;*{{tRUNWLhBpTz(G N002ovPDHLkV1hdIgLMD^ literal 0 HcmV?d00001 diff --git a/repo/js/背包材料统计/assets/imageClick/信件/Picture/snipaste_20250630_191227.png b/repo/js/背包材料统计/assets/imageClick/信件/Picture/snipaste_20250630_191227.png new file mode 100644 index 0000000000000000000000000000000000000000..36f887a666327d72f8b6dac5f9acc98bffbd8b5a GIT binary patch literal 17524 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 literal 0 HcmV?d00001 diff --git a/repo/js/背包材料统计/assets/imageClick/信件/icon/Back4.png b/repo/js/背包材料统计/assets/imageClick/信件/icon/Back4.png new file mode 100644 index 0000000000000000000000000000000000000000..c48b2c1711caff9715edfba5a993cbb39231b4e6 GIT binary patch literal 2933 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

-(vY5xE_sA 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 Vw%q)#0{d&JuY3J+zP4S&{{TO;yF35@ literal 0 HcmV?d00001 diff --git a/repo/js/背包材料统计/assets/imageClick/调查/icon/123.png b/repo/js/背包材料统计/assets/imageClick/调查/icon/123.png new file mode 100644 index 0000000000000000000000000000000000000000..f4534713312101c5aa0fc3d48ca6931e60ad63db GIT binary patch literal 698 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 line.trim()); + const categories = {}; + + lines.forEach(line => { + if (!line.includes(':')) return; + + const [category, items] = line.split(':'); + if (!category || !items) return; + + categories[category.trim()] = items.split(',').map(item => item.trim()).filter(item => item !== ''); + }); + + return categories; +} +// 从 targetText 文件夹中读取分类信息 +function readtargetTextCategories(targetTextDir) { + // log.info(`开始读取材料分类信息:${targetTextDir}`); + const targetTextFilePaths = readAllFilePaths(targetTextDir, 0, 1); + const materialCategories = {}; + + for (const filePath of targetTextFilePaths) { + if (state.cancelRequested) break; + const content = file.readTextSync(filePath); + if (!content) { + log.error(`加载文件失败:${filePath}`); + continue; // 跳过当前文件 + } + + const sourceCategory = basename(filePath).replace('.txt', ''); // 去掉文件扩展名 + materialCategories[sourceCategory] = parseCategoryContent(content); + } + // log.info(`完成材料分类信息读取,分类信息:${JSON.stringify(materialCategories, null, 2)}`); + return materialCategories; +} +// 定义替换映射表 +const replacementMap = { + "监": "盐", + "炽": "烬", + "盞": "盏", + "攜": "携", + "於": "于", + "卵": "卯" +}; + +/** + * 执行OCR识别并匹配目标文本 + * @param {string[]} targetTexts - 待匹配的目标文本列表 + * @param {Object} xRange - X轴范围 { min: number, max: number } + * @param {Object} yRange - Y轴范围 { min: number, max: number } + * @param {number} timeout - 超时时间(毫秒),默认200ms + * @param {Object} ra - 图像捕获对象(外部传入,需确保已初始化) + * @returns {Promise} 识别结果数组,包含匹配目标的文本及坐标信息 + */ +async function performOcr(targetTexts, xRange, yRange, timeout = 10, ra = null) { + // 正则特殊字符转义工具函数(避免替换时的正则语法错误) + const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + + const startTime = Date.now(); // 记录开始时间,用于超时判断 + + // 2. 计算识别区域宽高(xRange.max - xRange.min 为宽度,y同理) + const regionWidth = xRange.max - xRange.min; + const regionHeight = yRange.max - yRange.min; + if (regionWidth <= 0 || regionHeight <= 0) { + throw new Error(`无效的识别区域:宽=${regionWidth}, 高=${regionHeight}`); + } + + // 在超时时间内循环重试识别(处理临时识别失败) + while (Date.now() - startTime < timeout) { + try { + // 1. 检查图像捕获对象是否有效 + if (!ra) { + throw new Error("图像捕获对象(ra)未初始化"); + } + + // 3. 执行OCR识别(在指定区域内查找多结果) + const resList = ra.findMulti( + RecognitionObject.ocr(xRange.min, yRange.min, regionWidth, regionHeight) + ); + + // 4. 处理识别结果(文本修正 + 目标匹配) + const results = []; + for (let i = 0; i < resList.count; i++) { + const res = resList[i]; + let correctedText = res.text; // 原始识别文本 + log.info(`原始识别文本: ${res.text}`); + + // 4.1 修正识别错误(替换错误字符) + for (const [wrongChar, correctChar] of Object.entries(replacementMap)) { + const escapedWrong = escapeRegExp(wrongChar); // 转义特殊字符 + correctedText = correctedText.replace(new RegExp(escapedWrong, 'g'), correctChar); + } + + // 4.2 检查是否包含任意目标文本(避免重复添加同个结果) + const isTargetMatched = targetTexts.some(target => correctedText.includes(target)); + if (isTargetMatched) { + results.push({ + text: correctedText, + x: res.x, + y: res.y, + width: res.width, + height: res.height + }); + } + } + + // 5. 识别成功,返回结果(无论是否匹配到目标,均返回当前识别到的有效结果) + return results; + + } catch (error) { + // 识别异常时记录日志,继续重试(直到超时) + log.error(`OCR识别异常(将重试): ${error.message}`); + // 短暂等待后重试,避免高频失败占用资源 + await sleep(1); + } + await sleep(5); // 每次间隔 5 毫秒 + } + + // 超时未完成识别,返回空数组 + log.warn(`OCR识别超时(超过${timeout}ms)`); + return []; +} + +// const OCRdelay = Math.min(100, Math.max(0, Math.floor(Number(settings.OcrDelay) || 2))); // F识别基准时长 + +// 尝试找到 F 图标并返回其坐标 +async function findFIcon(recognitionObject, timeout = 10, ra = null) { + let startTime = Date.now(); + while (Date.now() - startTime < timeout && !state.cancelRequested) { + try { + let result = ra.find(recognitionObject); + if (result.isExist() && result.x !== 0 && result.y !== 0) { + return { success: true, x: result.x, y: result.y, width: result.width, height: result.height }; + } + } catch (error) { + log.error(`识别图像时发生异常: ${error.message}`); + if (state.cancelRequested) { + break; // 如果请求了取消,则退出循环 + } + return null; + } + await sleep(5); // 每次检测间隔 5 毫秒 + } + if (state.cancelRequested) { + log.info("图像识别任务已取消"); + } + return null; +} + +// 对齐并交互目标 +async function alignAndInteractTarget(targetTexts, fDialogueRo, textxRange, texttolerance, cachedFrame=null) { + let lastLogTime = Date.now(); + // 记录每个材料的识别次数(文本+坐标 → 计数) + const recognitionCount = new Map(); + + while (!state.completed && !state.cancelRequested) { + const currentTime = Date.now(); + if (currentTime - lastLogTime >= 10000) { // 每5秒记录一次日志 + log.info("检测中..."); + lastLogTime = currentTime; + } + await sleep(50); // 关键50时可避免F多目标滚动中拾取错,背包js这边有弹窗模块,就没必要增加延迟降低效率了 + cachedFrame?.dispose(); + cachedFrame = captureGameRegion(); + + // 尝试找到 F 图标 + let fRes = await findFIcon(fDialogueRo, 10, cachedFrame); + if (!fRes) { + continue; + } + + // 获取 F 图标的中心点 Y 坐标 + let centerYF = fRes.y + fRes.height / 2; + + // 在当前屏幕范围内进行 OCR 识别 + let ocrResults = await performOcr(targetTexts, textxRange, { min: fRes.y - 3, max: fRes.y + 37 }, 10, cachedFrame); + + // 检查所有目标文本是否在当前页面中 + let foundTarget = false; + for (let targetText of targetTexts) { + let targetResult = ocrResults.find(res => res.text.includes(targetText)); + if (targetResult) { + log.info(`找到目标文本: ${targetText}`); + + // 生成唯一标识并更新识别计数(文本+Y坐标) + const materialId = `${targetText}-${targetResult.y}`; + recognitionCount.set(materialId, (recognitionCount.get(materialId) || 0) + 1); + + let centerYTargetText = targetResult.y + targetResult.height / 2; + if (Math.abs(centerYTargetText - centerYF) <= texttolerance) { + // log.info(`目标文本 '${targetText}' 和 F 图标水平对齐`); + if (recognitionCount.get(materialId) >= 1) { + keyPress("F"); // 执行交互操作 + // log.info(`F键执行成功,识别计数: ${recognitionCount.get(materialId)}`); + + // F键后清除计数,确保单次交互 + recognitionCount.delete(materialId); + } + + foundTarget = true; + break; // 成功交互后退出当前循环,但继续检测 + } + } + } + + // 如果在当前页面中没有找到任何目标文本,则滚动到下一页 + if (!foundTarget) { + await keyMouseScript.runFile(`assets/滚轮下翻.json`); + // verticalScroll(-20); + } + if (state.cancelRequested) { + break; + } + cachedFrame?.dispose(); + } + + if (state.cancelRequested) { + log.info("检测任务已取消"); + } else if (!state.completed) { + log.error("未能找到正确的目标文本或未成功交互,跳过该目标文本"); + } +} diff --git a/repo/js/背包材料统计/lib/backStats.js b/repo/js/背包材料统计/lib/backStats.js new file mode 100644 index 000000000..ed28cd14b --- /dev/null +++ b/repo/js/背包材料统计/lib/backStats.js @@ -0,0 +1,733 @@ +eval(file.readTextSync("lib/region.js")); + +const holdX = Math.min(1920, Math.max(0, Math.floor(Number(settings.HoldX) || 1050))); +const holdY = Math.min(1080, Math.max(0, Math.floor(Number(settings.HoldY) || 750))); +const totalPageDistance = Math.max(10, Math.floor(Number(settings.PageScrollDistance) || 711)); +const targetCount = Math.min(9999, Math.max(0, Math.floor(Number(settings.TargetCount) || 5000))); // 设定的目标数量 +const imageDelay = Math.min(1000, Math.max(0, Math.floor(Number(settings.ImageDelay) || 0))); // 识图基准时长 await sleep(imageDelay); + +// 配置参数 +const pageScrollCount = 22; // 最多滑页次数 + +// 材料分类映射表 +const materialTypeMap = { + "锻造素材": "5", + "怪物掉落素材": "3", + "一般素材": "5", + "周本素材": "3", + "烹饪食材": "5", + "角色突破素材": "3", + "木材": "5", + "宝石": "3", + "鱼饵鱼类": "5", + "角色天赋素材": "3", + "武器突破素材": "3", + "采集食物": "4", + "料理": "4", +}; + +// 材料前位定义 +const materialPriority = { + "养成道具": 1, + "祝圣精华": 2, + "锻造素材": 1, + "怪物掉落素材": 1, + "采集食物": 1, + "一般素材": 2, + "周本素材": 2, + "料理": 2, + "烹饪食材": 3, + "角色突破素材": 3, + "木材": 4, + "宝石": 4, + "鱼饵鱼类": 5, + "角色天赋素材": 5, + "武器突破素材": 6, +}; + + + +// 提前计算所有动态坐标 +// 物品区左顶处物品左上角坐标(117,121) +// 物品图片大小(123,152) +// 物品间隔(24,24) +// 第一点击区位置:123/2+117=178.5; 152/2+121=197 +// const menuClickX = Math.round(575 + (Number(menuOffset) - 1) * 96.25); // 背包菜单的 X 坐标 + +// log.info(`材料分类: ${materialsCategory}, 菜单偏移值: ${menuOffset}, 计算出的点击 X 坐标: ${menuClickX}`); + +// OCR识别文本 +async function recognizeText(ocrRegion, timeout = 1000, retryInterval = 20, maxAttempts = 10, maxFailures = 3, ra = null) { + let startTime = Date.now(); + let retryCount = 0; + let failureCount = 0; // 用于记录连续失败的次数 + // const results = []; + const frequencyMap = {}; // 用于记录每个结果的出现次数 + + const numberReplaceMap = { + "O": "0", "o": "0", "Q": "0", "0": "0", + "I": "1", "l": "1", "i": "1", "1": "1", "一": "1", + "Z": "2", "z": "2", "2": "2", "二": "2", + "E": "3", "e": "3", "3": "3", "三": "3", + "A": "4", "a": "4", "4": "4", + "S": "5", "s": "5", "5": "5", + "G": "6", "b": "6", "6": "6", + "T": "7", "t": "7", "7": "7", + "B": "8", "θ": "8", "8": "8", + "g": "9", "q": "9", "9": "9", + }; + + while (Date.now() - startTime < timeout && retryCount < maxAttempts) { + let ocrObject = RecognitionObject.Ocr(ocrRegion.x, ocrRegion.y, ocrRegion.width, ocrRegion.height); + ocrObject.threshold = 0.85; // 适当降低阈值以提高速度 + let resList = ra.findMulti(ocrObject); + + if (resList.count === 0) { + failureCount++; + if (failureCount >= maxFailures) { + ocrRegion.x += 3; // 每次缩小6像素 + ocrRegion.width -= 6; // 每次缩小6像素 + retryInterval += 10; + + if (ocrRegion.width <= 12) { + return false; + } + } + retryCount++; + await sleep(retryInterval); + continue; + } + + for (let res of resList) { + let text = res.text; + text = text.split('').map(char => numberReplaceMap[char] || char).join(''); + // results.push(text); + + if (!frequencyMap[text]) { + frequencyMap[text] = 0; + } + frequencyMap[text]++; + + if (frequencyMap[text] >= 2) { + return text; + } + } + + await sleep(retryInterval); + } + + const sortedResults = Object.keys(frequencyMap).sort((a, b) => frequencyMap[b] - frequencyMap[a]); + return sortedResults.length > 0 ? sortedResults[0] : false; +} + +// 优化后的滑动页面函数(基于通用函数) +async function scrollPage(totalDistance, stepDistance = 10, delayMs = 5) { + await mouseDrag({ + holdMouseX: holdX, // 固定起点X + holdMouseY: holdY, // 固定起点Y + totalDistance: totalDistance, // 原逻辑中是向上滑动(原代码中是-moveDistance) + stepDistance, + stepInterval: delayMs, + waitBefore: 50, + waitAfter: 700, // 原逻辑中松开后等待700ms + repeat: 1 + }); +} + +// 通用鼠标拖动函数(提取重复逻辑) +/** + * 通用鼠标拖动工具函数 + * @param {Object} options 拖动参数 + * @param {number} [options.holdX] 拖动起点X坐标(默认当前鼠标位置) + * @param {number} [options.holdY] 拖动起点Y坐标(默认当前鼠标位置) + * @param {number} options.totalDistance 总拖动距离(垂直方向,正数向下,负数向上) + * @param {number} [options.stepDistance=10] 每步拖动距离 + * @param {number} [options.stepInterval=20] 步间隔时间(ms) + * @param {number} [options.waitBefore=50] 按下左键前的等待时间(ms) + * @param {number} [options.waitAfter=100] 拖动结束后的等待时间(ms) + * @param {number} [options.repeat=1] 重复拖动次数 + * @param {Object} [options.state] 状态对象(用于外部控制中断) + */ +async function mouseDrag({ + holdMouseX, + holdMouseY, + totalDistance, + stepDistance = 10, + stepInterval = 20, + waitBefore = 50, + waitAfter = 100, + repeat = 1, + state = { isRunning: true } +}) { + try { + // 移动到起点(如果指定了起点) + if (holdMouseX !== undefined && holdMouseY !== undefined) { + moveMouseTo(holdMouseX, holdMouseY); + await sleep(waitBefore); + } + + leftButtonDown(); + await sleep(waitBefore); // 按下后短暂等待 + + for (let r = 0; r < repeat; r++) { + if (!state.isRunning) break; + + const steps = Math.ceil(Math.abs(totalDistance) / stepDistance); + const direction = totalDistance > 0 ? 1 : -1; // 方向:正向下,负向上 + + for (let s = 0; s < steps; s++) { + if (!state.isRunning) break; + // 计算当前步实际移动距离(最后一步可能不足stepDistance) + const remaining = Math.abs(totalDistance) - s * stepDistance; + const move = Math.min(stepDistance, remaining) * direction; + moveMouseBy(0, move); // 垂直拖动 + await sleep(stepInterval); + } + + // 更新进度(如果有状态对象) + if (state) { + state.progress = Math.min(100, Math.round((r + 1) / repeat * 100)); + } + await sleep(waitAfter); + } + + leftButtonUp(); + await sleep(waitBefore); // 松开后等待稳定 + } catch (e) { + log.error(`拖动出错: ${e.message}`); + leftButtonUp(); // 确保鼠标抬起,避免卡死 + } +} + +function filterMaterialsByPriority(materialsCategory) { + // 获取当前材料分类的优先级 + const currentPriority = materialPriority[materialsCategory]; + if (currentPriority === undefined) { + throw new Error(`Invalid materialsCategory: ${materialsCategory}`); + } + + // 获取当前材料分类的 materialTypeMap 对应值 + const currentType = materialTypeMap[materialsCategory]; + if (currentType === undefined) { + throw new Error(`Invalid materialTypeMap for: ${materialsCategory}`); + } + + // 获取所有优先级更高的材料分类(前位材料) + const frontPriorityMaterials = Object.keys(materialPriority) + .filter(mat => materialPriority[mat] < currentPriority && materialTypeMap[mat] === currentType); + + // 获取所有优先级更低的材料分类(后位材料) + const backPriorityMaterials = Object.keys(materialPriority) + .filter(mat => materialPriority[mat] > currentPriority && materialTypeMap[mat] === currentType); + // 合并当前和后位材料分类 + const finalFilteredMaterials = [...backPriorityMaterials,materialsCategory ];// 当前材料 + return finalFilteredMaterials +} + + // 扫描材料 +async function scanMaterials(materialsCategory, materialCategoryMap) { + // 获取当前+后位材料名单 + const priorityMaterialNames = []; + const finalFilteredMaterials = await filterMaterialsByPriority(materialsCategory); + for (const category of finalFilteredMaterials) { + const materialIconDir = `assets/images/${category}`; + const materialIconFilePaths = file.ReadPathSync(materialIconDir); + for (const filePath of materialIconFilePaths) { + const name = basename(filePath).replace(".png", ""); // 去掉文件扩展名 + priorityMaterialNames.push({ category, name }); + } + } + + // 根据材料分类获取对应的材料图片文件夹路径 + const materialIconDir = `assets/images/${materialsCategory}`; + + // 使用 ReadPathSync 读取所有材料图片路径 + const materialIconFilePaths = file.ReadPathSync(materialIconDir); + + // 创建材料种类集合 + const materialCategories = []; + const allMaterials = new Set(); // 用于记录所有需要扫描的材料名称 + const materialImages = {}; // 用于缓存加载的图片 + + // 检查 materialCategoryMap 中当前分类的数组是否为空 + const categoryMaterials = materialCategoryMap[materialsCategory] || []; + const shouldScanAllMaterials = categoryMaterials.length === 0; // 如果为空,则扫描所有材料 + + for (const filePath of materialIconFilePaths) { + const name = basename(filePath).replace(".png", ""); // 去掉文件扩展名 + + // 如果 materialCategoryMap 中当前分类的数组不为空 + // 且当前材料名称不在指定的材料列表中,则跳过加载 + if (pathingMode.onlyPathing && !shouldScanAllMaterials && !categoryMaterials.includes(name)) { + continue; + } + + const mat = file.readImageMatSync(filePath); + if (mat.empty()) { + log.error(`加载图标失败:${filePath}`); + continue; // 跳过当前文件 + } + + materialCategories.push({ name, filePath }); + allMaterials.add(name); // 将材料名称添加到集合中 + materialImages[name] = mat; // 缓存图片 + } + + // 已识别的材料集合,避免重复扫描 + const recognizedMaterials = new Set(); + const unmatchedMaterialNames = new Set(); // 未匹配的材料名称 + const materialInfo = []; // 存储材料名称和数量 + + // 扫描参数 + const tolerance = 1; + const startX = 117; + const startY = 121; + const OffsetWidth = 146.428; // 146.428 + const columnWidth = 123; + const columnHeight = 680; + const maxColumns = 8; + // 跟踪已截图的区域(避免重复) + const capturedRegions = new Set(); + + // 扫描状态 + let hasFoundFirstMaterial = false; + let lastFoundTime = null; + let shouldEndScan = false; + let foundPriorityMaterial = false; + + // 俏皮话逻辑 + const scanPhrases = [ + "扫描中... 太好啦,有这么多素材!", + "扫描中... 不错的珍宝!", + "扫描中... 侦查骑士,发现目标!", + "扫描中... 嗯哼,意外之喜!", + "扫描中... 嗯?", + "扫描中... 很好,没有放过任何角落!", + "扫描中... 会有烟花材料嘛?", + "扫描中... 嗯,这是什么?", + "扫描中... 这些宝藏积灰了,先清洗一下", + "扫描中... 哇!都是好东西!", + "扫描中... 不虚此行!", + "扫描中... 瑰丽的珍宝,令人欣喜。", + "扫描中... 是对长高有帮助的东西吗?", + "扫描中... 嗯!品相卓越!", + "扫描中... 虽无法比拟黄金,但终有价值。", + "扫描中... 收获不少,可以拿去换几瓶好酒啦。", + "扫描中... 房租和伙食费,都有着落啦!", + "扫描中... 还不赖。", + "扫描中... 荒芜的世界,竟藏有这等瑰宝。", + "扫描中... 运气还不错。", + ]; + + let tempPhrases = [...scanPhrases]; + tempPhrases.sort(() => Math.random() - 0.5); // 打乱数组顺序,确保随机性 + let phrasesStartTime = Date.now(); + // 扫描背包中的材料 + for (let scroll = 0; scroll <= pageScrollCount; scroll++) { + + const ra = captureGameRegion(); + if (!foundPriorityMaterial) { + for (const { category, name } of priorityMaterialNames) { + if (recognizedMaterials.has(name)) { + continue; // 如果已经识别过,跳过 + } + + const filePath = `assets/images/${category}/${name}.png`; + const mat = file.readImageMatSync(filePath); + if (mat.empty()) { + log.error(`加载材料图库失败:${filePath}`); + continue; // 跳过当前文件 + } + + const recognitionObject = RecognitionObject.TemplateMatch(mat, 1142, startY, columnWidth, columnHeight); + recognitionObject.threshold = 0.8; // 设置识别阈值 + recognitionObject.Use3Channels = true; + + const result = ra.find(recognitionObject); + if (result.isExist() && result.x !== 0 && result.y !== 0) { + foundPriorityMaterial = true; // 标记找到前位材料 + // drawAndClearRedBox(result, ra, 100);// 调用异步函数绘制红框并延时清除 + log.info(`发现当前或后位材料: ${name},开始全列扫描`); + break; // 发现前位材料后,退出当前循环 + } + } + } + + if (foundPriorityMaterial) { + for (let column = 0; column < maxColumns; column++) { + const scanX0 = startX + column * OffsetWidth; + const scanX = Math.round(scanX0); + + + for (let i = 0; i < materialCategories.length; i++) { + const { name } = materialCategories[i]; + if (recognizedMaterials.has(name)) { + continue; // 如果已经识别过,跳过 + } + + const mat = materialImages[name]; + const recognitionObject = RecognitionObject.TemplateMatch(mat, scanX, startY, columnWidth, columnHeight); + recognitionObject.threshold = 0.85; + recognitionObject.Use3Channels = true; + + const result = ra.find(recognitionObject); + + if (result.isExist() && result.x !== 0 && result.y !== 0) { + recognizedMaterials.add(name); + moveMouseTo(result.x, result.y); + + // drawAndClearRedBox(result, ra, 100);// 调用异步函数绘制红框并延时清除 + const ocrRegion = { + x: result.x - tolerance, + y: result.y + 97 - tolerance, + width: 66 + 2 * tolerance, + height: 22 + 2 * tolerance + }; + const ocrResult = await recognizeText(ocrRegion, 1000, 10, 10, 3, ra); + materialInfo.push({ name, count: ocrResult || "?" }); + + if (!hasFoundFirstMaterial) { + hasFoundFirstMaterial = true; + lastFoundTime = Date.now(); + } else { + lastFoundTime = Date.now(); + } + } + await sleep(imageDelay); + } + } + } + + // 每5秒输出一句俏皮话 + const phrasesTime = Date.now(); + if (phrasesTime - phrasesStartTime >= 5000) { + const selectedPhrase = tempPhrases.shift(); + log.info(selectedPhrase); + if (tempPhrases.length === 0) { + tempPhrases = [...scanPhrases]; + tempPhrases.sort(() => Math.random() - 0.5); + } + phrasesStartTime = phrasesTime; + } + + // 检查是否结束扫描 + if (recognizedMaterials.size === allMaterials.size) { + log.info("所有材料均已识别!"); + shouldEndScan = true; + break; + } + + if (hasFoundFirstMaterial && Date.now() - lastFoundTime > 5000) { + log.info("未发现新的材料,结束扫描"); + shouldEndScan = true; + break; + } + + // 检查是否到达最后一页 + const sliderBottomRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/SliderBottom.png"), 1284, 916, 9, 26); + sliderBottomRo.threshold = 0.8; + const sliderBottomResult = ra.find(sliderBottomRo); + if (sliderBottomResult.isExist()) { + log.info("已到达最后一页!"); + shouldEndScan = true; + break; + } + + // 滑动到下一页 + if (scroll < pageScrollCount) { + await scrollPage(-totalPageDistance, 10, 5); + await sleep(100); + } + } + + // 处理未匹配的材料 + for (const name of allMaterials) { + if (!recognizedMaterials.has(name)) { + unmatchedMaterialNames.add(name); + } + } + + // 日志记录 + const now = new Date(); + const formattedTime = now.toLocaleString(); + const scanMode = shouldScanAllMaterials ? "全材料扫描" : "指定材料扫描"; + const logContent = ` +${formattedTime} +${scanMode} - ${materialsCategory} 种类: ${recognizedMaterials.size} 数量: +${materialInfo.map(item => `${item.name}: ${item.count}`).join(",")} +未匹配的材料 种类: ${unmatchedMaterialNames.size} 数量: +${Array.from(unmatchedMaterialNames).join(",")} +`; + + const categoryFilePath = `history_record/${materialsCategory}.txt`; // 勾选【材料分类】的历史记录 + const overwriteFilePath = `overwrite_record/${materialsCategory}.txt`; // 所有的历史记录分类储存 + const latestFilePath = "latest_record.txt"; // 所有的历史记录集集合 + if (pathingMode.onlyCategory) { + writeLog(categoryFilePath, logContent); + } + writeLog(overwriteFilePath, logContent); + writeLog(latestFilePath, logContent); // 覆盖模式? + + // 返回结果 + return materialInfo; +} + +function writeLog(filePath, logContent) { + try { + // 1. 读取现有内容(原样读取,不做任何分割处理) + let existingContent = ""; + try { + existingContent = file.readTextSync(filePath); + } catch (e) { + // 文件不存在则保持空 + } + + // 2. 拼接新记录(新记录加在最前面,用两个换行分隔,保留原始格式) + const finalContent = logContent + "\n\n" + existingContent; + + // 3. 按行分割,保留最近365条完整记录(按原始换行分割,不过滤) + const lines = finalContent.split("\n"); + const keepLines = lines.length > 365 * 5 ? lines.slice(0, 365 * 5) : lines; // 假设每条记录最多5行 + const result = file.writeTextSync(filePath, keepLines.join("\n"), false); + + if (result) { + log.info(`写入成功: ${filePath}`); + } else { + log.error(`写入失败: ${filePath}`); + } + } catch (error) { + // 只在文件完全不存在时创建,避免覆盖 + file.writeTextSync(filePath, logContent, false); + log.info(`创建新文件: ${filePath}`); + } +} + +// 定义所有图标的图像识别对象,每个图片都有自己的识别区域 +const BagpackRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Bagpack.png"), 58, 31, 38, 38); +const MaterialsRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Materials.png"), 941, 29, 38, 38); +const CultivationItemsRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/CultivationItems.png"), 749, 30, 38, 38); +const FoodRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Food.png"), 845, 31, 38, 38); + +const specialMaterials = [ + "水晶块", "魔晶块", "星银矿石", "紫晶块", "萃凝晶", "铁块", "白铁块", + "精锻用魔矿", "精锻用良矿", "精锻用杂矿" +]; +function filterLowCountMaterials(pathingMaterialCounts, materialCategoryMap) { + // 将 materialCategoryMap 中的所有材料名提取出来 + const allMaterials = Object.values(materialCategoryMap).flat(); + + // 筛选 pathingMaterialCounts 中的材料,只保留 materialCategoryMap 中定义的材料,并且数量低于 targetCount 或 count 为 "?" 或 name 在 specialMaterials 中 + return pathingMaterialCounts + .filter(item => + allMaterials.includes(item.name) && + (item.count < targetCount || item.count === "?") + ) + .map(item => { + // 如果 name 在 specialMaterials 数组中 + if (specialMaterials.includes(item.name)) { + // 如果 count 是 "?",直接保留 + if (item.count === "?") { + return item; + } + // 否则,将 count 除以 10 并向下取整 + item.count = Math.floor(item.count / 10); + } + return item; + }); +} + +function dynamicMaterialGrouping(materialCategoryMap) { + // 初始化动态分组对象 + const dynamicMaterialGroups = {}; + + // 遍历 materialCategoryMap 的 entries + for (const category in materialCategoryMap) { + const type = materialTypeMap[category]; // 获取材料分类对应的组编号(3、4、5) + if (!dynamicMaterialGroups[type]) { + dynamicMaterialGroups[type] = []; // 初始化组 + } + dynamicMaterialGroups[type].push(category); // 将分类加入对应组 + } + + // 对每组内的材料分类按照 materialPriority 排序 + for (const type in dynamicMaterialGroups) { + dynamicMaterialGroups[type].sort((a, b) => materialPriority[a] - materialPriority[b]); + } + + // 将分组结果转换为数组并按类型排序(3, 4, 5) + const sortedGroups = Object.entries(dynamicMaterialGroups) + .map(([type, categories]) => ({ type: parseInt(type), categories })) + .sort((a, b) => a.type - b.type); + + // 返回分组结果 + return sortedGroups; +} + +// 主逻辑函数 +async function MaterialPath(materialCategoryMap, cachedFrame = null) { + + // 1. 先记录原始名称与别名的映射关系(用于最后反向转换) + const nameMap = new Map(); + Object.values(materialCategoryMap).flat().forEach(originalName => { + const aliasName = MATERIAL_ALIAS[originalName] || originalName; + nameMap.set(aliasName, originalName); // 存储:别名→原始名 + }); + + // 2. 转换materialCategoryMap为别名(用于内部处理) + const processedMap = {}; + Object.entries(materialCategoryMap).forEach(([category, names]) => { + processedMap[category] = names.map(name => MATERIAL_ALIAS[name] || name); + }); + materialCategoryMap = processedMap; + + const maxStage = 4; // 最大阶段数 + let stage = 0; // 当前阶段 + let currentGroupIndex = 0; // 当前处理的分组索引 + let currentCategoryIndex = 0; // 当前处理的分类索引 + let materialsCategory = ""; // 当前处理的材料分类名称 + const allLowCountMaterials = []; // 用于存储所有识别到的低数量材料信息 + + const sortedGroups = dynamicMaterialGrouping(materialCategoryMap); +// log.info("材料 动态[分组]结果:"); + sortedGroups.forEach(group => { + log.info(`类型 ${group.type} | 包含分类: ${group.categories.join(', ')}`); +}); + + while (stage <= maxStage) { + switch (stage) { + case 0: // 返回主界面 + log.info("返回主界面"); + await genshin.returnMainUi(); + await sleep(500); + stage = 1; // 进入下一阶段 + break; + + case 1: // 打开背包界面 + // log.info("打开背包界面"); + keyPress("B"); // 打开背包界面 + await sleep(1000); + + cachedFrame?.dispose(); + cachedFrame = captureGameRegion(); + + const backpackResult = await recognizeImage(BagpackRo, cachedFrame, 2000); + if (backpackResult.isDetected) { + // log.info("成功识别背包图标"); + stage = 2; // 进入下一阶段 + } else { + log.warn("未识别到背包图标,重新尝试"); + stage = 0; // 回退 + } + break; + + case 2: // 按分组处理材料分类 + if (currentGroupIndex < sortedGroups.length) { + const group = sortedGroups[currentGroupIndex]; + + if (currentCategoryIndex < group.categories.length) { + materialsCategory = group.categories[currentCategoryIndex]; + const offset = materialTypeMap[materialsCategory]; + const menuClickX = Math.round(575 + (offset - 1) * 96.25); + // log.info(`点击坐标 (${menuClickX},75)`); + click(menuClickX, 75); + + await sleep(500); + + cachedFrame?.dispose(); + cachedFrame = captureGameRegion(); + + stage = 3; // 进入下一阶段 + } else { + currentGroupIndex++; + currentCategoryIndex = 0; // 重置分类索引 + stage = 2; // 继续处理下一组 + } + } else { + stage = 5; // 跳出循环 + } + break; + + case 3: // 识别材料分类 + let CategoryObject; + switch (materialsCategory) { + case "锻造素材": + case "一般素材": + case "烹饪食材": + case "木材": + case "鱼饵鱼类": + CategoryObject = MaterialsRo; + break; + case "采集食物": + case "料理": + CategoryObject = FoodRo; + break; + case "怪物掉落素材": + case "周本素材": + case "角色突破素材": + case "宝石": + case "角色天赋素材": + case "武器突破素材": + CategoryObject = CultivationItemsRo; + break; + default: + log.error("未知的材料分类"); + stage = 0; // 回退到阶段0 + return; + } + + const CategoryResult = await recognizeImage(CategoryObject, cachedFrame); + if (CategoryResult.isDetected) { + log.info(`识别到${materialsCategory} 所在分类。`); + stage = 4; // 进入下一阶段 + } else { + log.warn("未识别到材料分类图标,重新尝试"); + log.warn(`识别结果:${JSON.stringify(CategoryResult)}`); + stage = 2; // 回退到阶段2 + } + break; + + case 4: // 扫描材料 + log.info("芭芭拉,冲鸭!"); + await moveMouseTo(1288, 124); // 移动鼠标至滑条顶端 + await sleep(200); + leftButtonDown(); // 长按左键重置材料滑条 + await sleep(300); + leftButtonUp(); + await sleep(200); + + // 扫描材料并获取低于目标数量的材料 + const lowCountMaterials = await scanMaterials(materialsCategory, materialCategoryMap); + allLowCountMaterials.push(lowCountMaterials); + + currentCategoryIndex++; + stage = 2; // 返回阶段2处理下一个分类 + break; + + case 5: // 所有分组处理完毕 + log.info("所有分组处理完毕,返回主界面"); + await genshin.returnMainUi(); + stage = maxStage + 1; // 确保退出循环 + break; + } + } + + await genshin.returnMainUi(); // 返回主界面 + log.info("扫描流程结束"); + + + // 3. 处理完成后,将输出结果转换回原始名称 + const finalResult = allLowCountMaterials.map(categoryMaterials => { + return categoryMaterials.map(material => { + // 假设material包含name属性,将别名转回原始名 + return { + ...material, + name: nameMap.get(material.name) || material.name // 反向映射 + }; + }); + }); + + cachedFrame?.dispose(); + return finalResult; // 返回转换后的结果(如"晶蝶") +} + + diff --git a/repo/js/背包材料统计/lib/displacement.js b/repo/js/背包材料统计/lib/displacement.js new file mode 100644 index 000000000..44bb3682c --- /dev/null +++ b/repo/js/背包材料统计/lib/displacement.js @@ -0,0 +1,62 @@ + +/* + +// 位移计算逻辑 + +*/ + +// 辅助函数:计算两点之间的距离 +function calculateDistance(initialPosition, finalPosition) { + const deltaX = finalPosition.X - initialPosition.X; + const deltaY = finalPosition.Y - initialPosition.Y; + return Math.sqrt(deltaX * deltaX + deltaY * deltaY); +} +/* +// 位移监测函数 +async function monitorDisplacement(monitoring, resolve) { + // 获取对象的实际初始位置 + let lastPosition = genshin.getPositionFromMap(); + let cumulativeDistance = 0; // 初始化累计位移量 + let lastUpdateTime = Date.now(); // 记录上一次位置更新的时间 + + while (monitoring) { + const currentPosition = genshin.getPositionFromMap(); // 获取当前位置 + const currentTime = Date.now(); // 获取当前时间 + + // 计算位移量 + const deltaX = currentPosition.X - lastPosition.X; + const deltaY = currentPosition.Y - lastPosition.Y; + let distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); + + // 如果位移量小于0.5,则视为0 + if (distance < 0.5) { + distance = 0; + } + + // 如果有位移,更新累计位移量和最后更新时间 + if (distance > 0) { + cumulativeDistance += distance; // 累计位移量 + lastUpdateTime = currentTime; // 更新最后更新时间 + } + + // 检测是否超过5秒没有位移 + if (currentTime - lastUpdateTime >= 5000) { + // 触发跳跃 + keyPress(VK_SPACE); + lastUpdateTime = currentTime; // 重置最后更新时间 + } + + // 输出位移信息和累计位移量 + log.info(`时间:${(currentTime - lastUpdateTime) / 1000}秒,位移信息: X=${currentPosition.X}, Y=${currentPosition.Y}, 当前位移量=${distance.toFixed(2)}, 累计位移量=${cumulativeDistance.toFixed(2)}`); + + // 更新最后位置 + lastPosition = currentPosition; + + // 等待1秒再次检查 + await sleep(1000); + } + + // 当监测结束时,返回累计位移量 + resolve(cumulativeDistance); +} +*/ diff --git a/repo/js/背包材料统计/lib/exp.js b/repo/js/背包材料统计/lib/exp.js new file mode 100644 index 000000000..255cbca5c --- /dev/null +++ b/repo/js/背包材料统计/lib/exp.js @@ -0,0 +1,159 @@ + +// ===================== 狗粮模式专属函数 ===================== +// 1. 狗粮分解配置与OCR区域 +const AUTO_SALVAGE_CONFIG = { + autoSalvage3: settings.autoSalvage3 || "是", + autoSalvage4: settings.autoSalvage4 || "是" +}; +const OCR_REGIONS = { + expStorage: { x: 1472, y: 883, width: 170, height: 34 }, + expCount: { x: 1472, y: 895, width: 170, height: 34 } +}; + +// 2. 数字替换映射表(处理OCR识别误差) +const numberReplaceMap = { + "O": "0", "o": "0", "Q": "0", "0": "0", + "I": "1", "l": "1", "i": "1", "1": "1", "一": "1", + "Z": "2", "z": "2", "2": "2", "二": "2", + "E": "3", "e": "3", "3": "3", "三": "3", + "A": "4", "a": "4", "4": "4", + "S": "5", "s": "5", "5": "5", + "G": "6", "b": "6", "6": "6", + "T": "7", "t": "7", "7": "7", + "B": "8", "θ": "8", "8": "8", + "g": "9", "q": "9", "9": "9", +}; + +// 3. OCR文本处理 +function processExpText(text) { + let correctedText = text || ""; + let removedSymbols = []; + + // 替换错误字符 + for (const [wrong, correct] of Object.entries(numberReplaceMap)) { + correctedText = correctedText.replace(new RegExp(wrong, 'g'), correct); + } + + // 提取纯数字 + let finalText = ''; + for (const char of correctedText) { + if (/[0-9]/.test(char)) { + finalText += char; + } else if (char.trim() !== '') { + removedSymbols.push(char); + } + } + + return { + processedText: finalText, + removedSymbols: [...new Set(removedSymbols)] + }; +} + +// 4. OCR识别EXP +async function recognizeExpRegion(regionName, ra = null, timeout = 2000) { + const ocrRegion = OCR_REGIONS[regionName]; + if (!ocrRegion) { + log.error(`[狗粮OCR] 无效区域:${regionName}`); + return { success: false, expCount: 0 }; + } + + log.info(`[狗粮OCR] 识别${regionName}(x=${ocrRegion.x}, y=${ocrRegion.y})`); + const startTime = Date.now(); + let retryCount = 0; + + while (Date.now() - startTime < timeout) { + try { + const ocrResult = ra.find(RecognitionObject.ocr( + ocrRegion.x, + ocrRegion.y, + ocrRegion.width, + ocrRegion.height + )); + log.info(`[狗粮OCR] 原始文本:${ocrResult.text}`); + + if (ocrResult?.text) { + const { processedText, removedSymbols } = processExpText(ocrResult.text); + if (removedSymbols.length > 0) { + log.info(`[狗粮OCR] 去除无效字符:${removedSymbols.join(', ')}`); + } + const expCount = processedText ? parseInt(processedText, 10) : 0; + log.info(`[狗粮OCR] ${regionName}结果:${expCount}`); + return { success: true, expCount }; + } + } catch (error) { + retryCount++; + log.warn(`[狗粮OCR] ${regionName}第${retryCount}次识别失败:${error.message}`); + } + await sleep(500); + } + + log.error(`[狗粮OCR] ${regionName}超时未识别,默认0`); + return { success: false, expCount: 0 }; +} + +// 5. 狗粮分解流程 +async function executeSalvageWithOCR() { + log.info("[狗粮分解] 开始执行分解流程"); + let storageExp = 0; + let countExp = 0; + + let cachedFrame = null; + + try { + keyPress("B"); await sleep(1000); + const coords = [ + [670, 40], // 打开背包 + [660, 1010], // 打开分解 + [300, 1020], // 打开分解选项页面 + // [200, 150, 500], // 勾选1星狗粮 + // [200, 220, 500], // 勾选2星狗粮 + [200, 300, 500, AUTO_SALVAGE_CONFIG.autoSalvage3 !== '否'], // 3星(按配置) + [200, 380, 500, AUTO_SALVAGE_CONFIG.autoSalvage4 !== '否'], // 4星(按配置) + [340, 1000], // 确认选择 + [1720, 1015], // 点击是否分解 + [1320, 756], // 确认分解 + [1840, 45, 1500], // 退出 + [1840, 45], // 退出 + [1840, 45], // 退出 + ]; + + for (const coord of coords) { + const [x, y, delay = 1000, condition = true] = coord; + if (condition) { + click(x, y); + await sleep(delay); + log.debug(`[狗粮分解] 点击(${x},${y}),延迟${delay}ms`); + + // 分解前识别储存EXP + if (x === 660 && y === 1010) { + cachedFrame?.dispose(); + cachedFrame = captureGameRegion(); + const { expCount } = await recognizeExpRegion("expStorage", cachedFrame, 1000); + storageExp = expCount; + } + + // 分解后识别新增EXP + if (x === 340 && y === 1000) { + cachedFrame?.dispose(); + cachedFrame = captureGameRegion(); + const { expCount } = await recognizeExpRegion("expCount", cachedFrame, 1000); + countExp = expCount; + } + } + } + + const totalExp = countExp - storageExp; // 分解新增EXP = 分解后 - 分解前 + log.info(`[狗粮分解] 完成,新增EXP:${totalExp}(分解前:${storageExp},分解后:${countExp})`); + return { success: true, totalExp: Math.max(totalExp, 0) }; // 避免负数 + } catch (error) { + log.error(`[狗粮分解] 失败:${error.message}`); + return { success: false, totalExp: 0 }; + } +} + +// 6. 判断是否为狗粮资源(关键词匹配) +function isFoodResource(resourceName) { + const foodKeywords = ["12h狗粮", "24h狗粮"]; + return resourceName && foodKeywords.some(keyword => resourceName.includes(keyword)); +} diff --git a/repo/js/背包材料统计/lib/file.js b/repo/js/背包材料统计/lib/file.js new file mode 100644 index 000000000..a7d83cca6 --- /dev/null +++ b/repo/js/背包材料统计/lib/file.js @@ -0,0 +1,153 @@ + + +// ============================================== +// 5. 角色识别与策略执行相关函数(保留原始功能) +// ============================================== +// 工具函数 +function basename(filePath) { + if (!filePath || typeof filePath !== 'string') return ''; + const normalizedPath = filePath.replace(/\\/g, '/'); + const lastSlashIndex = normalizedPath.lastIndexOf('/'); + return lastSlashIndex !== -1 ? normalizedPath.substring(lastSlashIndex + 1) : normalizedPath; +} + +/* +// 如果路径存在且返回的是数组,则认为是目录Folder +function pathExists(path) { + try { return file.readPathSync(path)?.length >= 0; } + catch { return false; } +} +*/ + +function pathExists(path) { + try { + return file.isFolder(path); + } catch { + return false; + } +} + +// 判断文件是否存在(非目录且能读取) +function fileExists(filePath) { + try { + // 先排除目录(是目录则不是文件) + if (file.isFolder(filePath)) { + return false; + } + // 尝试读取文件(能读则存在) + file.readTextSync(filePath); + return true; + } catch { + return false; + } +} +// 带深度限制的非递归文件夹读取 +function readAllFilePaths(dir, depth = 0, maxDepth = 3, includeExtensions = ['.png', '.json', '.txt'], includeDirs = false) { + if (!pathExists(dir)) { + log.error(`目录 ${dir} 不存在`); + return []; + } + + try { + const filePaths = []; + const stack = [[dir, depth]]; // 存储(路径, 当前深度)的栈 + + while (stack.length > 0) { + const [currentDir, currentDepth] = stack.pop(); + const entries = file.readPathSync(currentDir); + + for (const entry of entries) { + const isDirectory = pathExists(entry); + if (isDirectory) { + if (includeDirs) filePaths.push(entry); + if (currentDepth < maxDepth) stack.push([entry, currentDepth + 1]); + } else { + const ext = entry.substring(entry.lastIndexOf('.')).toLowerCase(); + if (includeExtensions.includes(ext)) filePaths.push(entry); + } + } + } + // log.info(`完成目录 ${dir} 的递归读取,共找到 ${filePaths.length} 个文件`); + return filePaths; + } catch (error) { + log.error(`读取目录 ${dir} 错误: ${error}`); + return []; + } +} +/* +// 新记录在最下面 +async function writeFile(filePath, content, isAppend = false, maxRecords = 365) { + try { + if (isAppend) { + // 读取现有内容(如果文件不存在则为空) + const existingContent = file.readTextSync(filePath) || ""; + // 分割成记录数组(过滤空字符串) + const records = existingContent.split("\n\n").filter(Boolean); + + // 关键修复:将新内容添加到末尾,然后只保留最后maxRecords条 + const allRecords = [...records, content]; // 新内容放在最后 + const latestRecords = allRecords.slice(-maxRecords); // 整体截取最新的maxRecords条 + + // 拼接成最终内容 + const finalContent = latestRecords.join("\n\n"); + const result = file.WriteTextSync(filePath, finalContent, false); + + // 日志输出(可根据需要启用) + // log.info(result ? `[追加] 成功写入: ${filePath}` : `[追加] 写入失败: ${filePath}`); + return result; + } else { + // 非追加模式:直接覆盖写入 + const result = file.WriteTextSync(filePath, content, false); + // log.info(result ? `[覆盖] 成功写入: ${filePath}` : `[覆盖] 写入失败: ${filePath}`); + return result; + } + } catch (error) { + // 发生错误时尝试创建文件并写入 + const result = file.WriteTextSync(filePath, content, false); + log.info(result ? `[新建] 成功创建: ${filePath}` : `[新建] 创建失败: ${filePath}`); + return result; + } +} +*/ +// 新记录在最上面20250531 +async function writeFile(filePath, content, isAppend = false, maxRecords = 365) { + try { + if (isAppend) { + // 读取现有内容,处理文件不存在的情况 + let existingContent = ""; + try { + existingContent = file.readTextSync(filePath); + } catch (err) { + // 文件不存在时视为空内容 + existingContent = ""; + } + + // 分割现有记录并过滤空记录 + const records = existingContent.split("\n\n").filter(Boolean); + + // 新内容放在最前面,形成完整记录列表 + const allRecords = [content, ...records]; + + // 只保留最新的maxRecords条(超过则删除最老的) + const keptRecords = allRecords.slice(0, maxRecords); + + // 拼接记录并写入文件 + const finalContent = keptRecords.join("\n\n"); + const result = file.WriteTextSync(filePath, finalContent, false); + + // log.info(result ? `[追加] 成功写入: ${filePath}` : `[追加] 写入失败: ${filePath}`); + return result; + } else { + // 覆盖模式直接写入 + const result = file.WriteTextSync(filePath, content, false); + // log.info(result ? `[覆盖] 成功写入: ${filePath}` : `[覆盖] 写入失败: ${filePath}`); + return result; + } + } catch (error) { + // 出错时尝试创建/写入文件 + const result = file.WriteTextSync(filePath, content, false); + log.info(result ? `[新建/恢复] 成功处理: ${filePath}` : `[新建/恢复] 处理失败: ${filePath}`); + return result; + } +} + diff --git a/repo/js/背包材料统计/lib/imageClick.js b/repo/js/背包材料统计/lib/imageClick.js new file mode 100644 index 000000000..b82271560 --- /dev/null +++ b/repo/js/背包材料统计/lib/imageClick.js @@ -0,0 +1,195 @@ +// 新增:独立的预加载函数,负责所有资源预处理 +async function preloadImageResources(specificNames) { + log.info("开始预加载所有图片资源"); + + // 统一参数格式(与原逻辑一致) + let preSpecificNames = specificNames; + if (typeof specificNames === 'string') { + preSpecificNames = [specificNames]; + } + const isAll = !preSpecificNames || preSpecificNames.length === 0; + if (isAll) { + log.info("未指定具体弹窗名称,将执行所有弹窗目录处理"); + } else { + log.info(`指定处理弹窗名称:${preSpecificNames.join(', ')}`); + } + + // 定义根目录(与原代码一致) + const rootDir = "assets/imageClick"; + + // 获取所有子目录(与原代码一致) + const subDirs = readAllFilePaths(rootDir, 0, 0, [], true); + + // 筛选目标目录(与原代码一致) + const targetDirs = isAll + ? subDirs + : subDirs.filter(subDir => { + const dirName = basename(subDir); + return preSpecificNames.includes(dirName); + }); + + if (targetDirs.length === 0) { + log.info(`未找到与指定名称匹配的目录,名称列表:${preSpecificNames?.join(', ') || '所有'}`); + return []; + } + + // 预加载所有目录的资源(原imageClick内的资源加载逻辑) + const preloadedResources = []; + for (const subDir of targetDirs) { + const dirName = basename(subDir); + // log.info(`开始预处理弹窗类型:${dirName}`); + + // 查找icon和Picture文件夹(与原代码一致) + const entries = readAllFilePaths(subDir, 0, 1, [], true); + const iconDir = entries.find(entry => entry.endsWith('\icon')); + const pictureDir = entries.find(entry => entry.endsWith('\Picture')); + + if (!iconDir) { + log.warn(`未找到 icon 文件夹,跳过分类文件夹:${subDir}`); + continue; + } + if (!pictureDir) { + log.warn(`未找到 Picture 文件夹,跳过分类文件夹:${subDir}`); + continue; + } + + // 读取图片文件(与原代码一致) + const iconFilePaths = readAllFilePaths(iconDir, 0, 0, ['.png', '.jpg', '.jpeg']); + const pictureFilePaths = readAllFilePaths(pictureDir, 0, 0, ['.png', '.jpg', '.jpeg']); + + // 预创建icon识别对象(与原代码一致) + const iconRecognitionObjects = []; + for (const filePath of iconFilePaths) { + const mat = file.readImageMatSync(filePath); + if (mat.empty()) { + log.error(`加载图标失败:${filePath}`); + continue; + } + const recognitionObject = RecognitionObject.TemplateMatch(mat, 0, 0, 1920, 1080); + iconRecognitionObjects.push({ name: basename(filePath), ro: recognitionObject, iconDir }); + } + + // 预创建图库区域(与原代码一致) + const pictureRegions = []; + for (const filePath of pictureFilePaths) { + const mat = file.readImageMatSync(filePath); + if (mat.empty()) { + log.error(`加载图库图片失败:${filePath}`); + continue; + } + pictureRegions.push({ name: basename(filePath), region: new ImageRegion(mat, 0, 0) }); + } + + // 预计算匹配区域(与原代码一致) + const foundRegions = []; + for (const picture of pictureRegions) { + for (const icon of iconRecognitionObjects) { + const foundRegion = picture.region.find(icon.ro); + if (foundRegion.isExist()) { + foundRegions.push({ + pictureName: picture.name, + iconName: icon.name, + region: foundRegion, + iconDir: icon.iconDir + }); + } + } + } + + // 保存预处理结果 + preloadedResources.push({ + dirName, + foundRegions + }); + } + + log.info(`预加载完成,共处理 ${preloadedResources.length} 个目录`); + return preloadedResources; +} + +// 新增:imageClick后台任务函数 +async function imageClickBackgroundTask() { + log.info("imageClick后台任务已启动"); + const imageClickDelay = Math.min(99, Math.max(1, Math.floor(Number(settings.PopupClickDelay) || 5)))*1000; + // 可以根据需要配置要处理的弹窗名称 + const specificNamesStr = settings.PopupNames || ""; + const specificNames = specificNamesStr + .split(/[,,、 \s]+/) + .map(name => name.trim()) + .filter(name => name !== ""); + + // 调用独立预加载函数(循环前仅执行一次) + const preloadedResources = await preloadImageResources(specificNames); + if (preloadedResources.length === 0) { + log.info("无可用预加载资源,任务结束"); + return { success: false }; + } + + // 循环执行,仅使用预加载资源 + while (!state.completed && !state.cancelRequested) { + try { + // 调用imageClick时传入预加载资源 + await imageClick(preloadedResources, null, specificNames, true); + } catch (error) { + log.info(`弹窗识别失败(继续重试):${error.message}`); + } + // 短暂等待后再次执行 + await sleep(imageClickDelay); + } + + log.info("imageClick后台任务已结束"); + return { success: true }; +} + +// 优化:使用预加载资源,保留所有原执行逻辑 +async function imageClick(preloadedResources, ra = null, specificNames = null, useNewScreenshot = false) { + // 保留原参数格式处理(兼容历史调用) + if (typeof specificNames === 'string') { + specificNames = [specificNames]; + } + const isAll = !specificNames || specificNames.length === 0; + + // 遍历预处理好的资源(原targetDirs循环逻辑) + for (const resource of preloadedResources) { + const { dirName, foundRegions } = resource; + for (const foundRegion of foundRegions) { + // 保留原识别对象创建逻辑(使用预处理的路径) + const tolerance = 1; + const iconMat = file.readImageMatSync(`${foundRegion.iconDir}/${foundRegion.iconName}`); + const recognitionObject = RecognitionObject.TemplateMatch( + iconMat, + foundRegion.region.x - tolerance, + foundRegion.region.y - tolerance, + foundRegion.region.width + 2 * tolerance, + foundRegion.region.height + 2 * tolerance + ); + recognitionObject.threshold = 0.85; + + // 保留原识别逻辑 + const result = await recognizeImage( + recognitionObject, + ra, + 1000, // timeout + 500, // interval + useNewScreenshot, + dirName // iconType + ); + + // 保留原点击逻辑 + if (result.isDetected && result.x !== 0 && result.y !== 0) { + const x = Math.round(result.x + result.width / 2); + const y = Math.round(result.y + result.height / 2); + log.info(`即将点击【${dirName}】类型下的图标:${foundRegion.iconName},位置: (${x}, ${y})`); + await click(x, y); + log.info(`点击【${dirName}】类型下的${foundRegion.iconName}成功`); + await sleep(10); + return { success: true }; + } else { + // log.info(`未发现弹窗【${dirName}】的图标:${foundRegion.iconName}`); + } + } + } + + // 所有目标处理完毕仍未成功(保留原返回逻辑) + return { success: false }; +} diff --git a/repo/js/背包材料统计/lib/region.js b/repo/js/背包材料统计/lib/region.js new file mode 100644 index 000000000..9d4a6d4f1 --- /dev/null +++ b/repo/js/背包材料统计/lib/region.js @@ -0,0 +1,151 @@ + +// ########################################################################### +// 【核心工具函数】 +// ########################################################################### + +var globalLatestRa = null; +async function recognizeImage( + recognitionObject, + ra, + timeout = 1000, + interval = 500, + useNewScreenshot = false, + iconType = null +) { + let startTime = Date.now(); + globalLatestRa = ra; + const originalRa = ra; + let tempRa = null; // 用于管理临时创建的资源 + + try { + while (Date.now() - startTime < timeout) { + let currentRa; + if (useNewScreenshot) { + // 释放之前的临时资源 + if (tempRa) { + tempRa.dispose(); + } + tempRa = captureGameRegion(); + currentRa = tempRa; + globalLatestRa = currentRa; + } else { + // 不使用新截图时直接使用原始ra,不重复释放 + currentRa = originalRa; + } + + if (currentRa) { + try { + const result = currentRa.find(recognitionObject); + if (result.isExist() && result.x !== 0 && result.y !== 0) { + return { + isDetected: true, + iconType: iconType, + x: result.x, + y: result.y, + width: result.width, + height: result.height, + ra: globalLatestRa, + usedNewScreenshot: useNewScreenshot + }; + } + } catch (error) { + log.error(`【${iconType || '未知'}识别异常】: ${error.message}`); + } + } + + await sleep(interval); + } + } finally { + // 释放临时资源但保留全局引用的资源 + if (tempRa && tempRa !== globalLatestRa) { + tempRa.dispose(); + } + } + + return { + isDetected: false, + iconType: iconType, + x: null, + y: null, + width: null, + height: null, + ra: globalLatestRa, + usedNewScreenshot: useNewScreenshot + }; +} + +// 定义一个异步函数来绘制红框并延时清除 +async function drawAndClearRedBox(searchRegion, ra, delay = 500) { + let drawRegion = null; + try { + // 创建绘制区域 + drawRegion = ra.DeriveCrop( + searchRegion.x, searchRegion.y, + searchRegion.width, searchRegion.height + ); + drawRegion.DrawSelf("icon"); // 绘制红框 + + // 等待指定时间 + await sleep(delay); + + // 清除红框 - 使用更可靠的方式 + if (drawRegion && typeof drawRegion.DrawSelf === 'function') { + // 可能需要使用透明绘制来清除,或者绘制一个0大小的区域 + ra.DeriveCrop(0, 0, 0, 0).DrawSelf("icon"); + } + } catch (e) { + log.error("红框绘制异常:" + e.message); + } finally { + // 正确释放资源,如果dispose方法存在的话 + if (drawRegion && typeof drawRegion.dispose === 'function') { + drawRegion.dispose(); + } + } +} + +// 截图保存函数 +function imageSaver(mat, saveFile) { + // 获取当前时间并格式化为 "YYYY-MM-DD_HH-MM-SS" + const now = new Date(); + const timestamp = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}_${String(now.getHours()).padStart(2, '0')}-${String(now.getMinutes()).padStart(2, '0')}-${String(now.getSeconds()).padStart(2, '0')}`; + + // 获取当前脚本所在的目录 + const scriptDir = getScriptDirPath(); + if (!scriptDir) { + log.error("无法获取脚本目录"); + return; + } + + // 构建完整的目标目录路径和文件名 + const savePath = `${scriptDir}/${saveFile}/screenshot_${timestamp}.png`; + const tempFilePath = `${scriptDir}/${saveFile}`; + + // 检查临时文件是否存在,如果不存在则创建目录 + try { + // 尝试读取临时文件 + file.readPathSync(tempFilePath); + log.info("目录存在,继续执行保存图像操作"); + } catch (error) { + log.error(`确保目录存在时出错: ${error}`); + return; + } + + // 保存图像 + try { + mat.saveImage(savePath); + // log.info(`图像已成功保存到: ${savePath}`); + } catch (error) { + log.error(`保存图像失败: ${error}`); + } +} + +// 获取脚本目录 +function getScriptDirPath() { + try { + file.readTextSync(`temp-${Math.random()}.txt`); + } catch (e) { + const match = e.toString().match(/'([^']+)'/); + return match ? match[1].replace(/\\[^\\]+$/, "") : null; + } + return null; +} \ No newline at end of file diff --git a/repo/js/背包材料统计/main.js b/repo/js/背包材料统计/main.js index c4503a85c..0e82ec419 100644 --- a/repo/js/背包材料统计/main.js +++ b/repo/js/背包材料统计/main.js @@ -1,1716 +1,1474 @@ +// ============================================== +// 常量与配置(集中管理硬编码值) +// ============================================== +const CONSTANTS = { + // 路径与目录配置 + MATERIAL_CD_DIR: "materialsCD", + TARGET_TEXT_DIR: "targetText", + PATHING_DIR: "pathing", + RECORD_DIR: "pathing_record", + NO_RECORD_DIR: "pathing_record/noRecord", + IMAGES_DIR: "assets/images", + MONSTER_MATERIALS_PATH: "assets/Monster-Materials.txt", + + // 解析与处理配置 + MAX_PATH_DEPTH: 3, // 路径解析最大深度 + NOTIFICATION_CHUNK_SIZE: 500, // 通知拆分长度 + FOOD_EXP_RECORD_SUFFIX: "_狗粮.txt", + SUMMARY_FILE_NAME: "材料收集汇总.txt", + ZERO_COUNT_SUFFIX: "-0.txt", + + // 日志模块标识 + LOG_MODULES: { + INIT: "[初始化]", + PATH: "[路径处理]", + MATERIAL: "[材料管理]", + MONSTER: "[怪物映射]", + CD: "[CD控制]", + RECORD: "[记录管理]", + MAIN: "[主流程]" + } +}; -const targetCount = Math.min(9999, Math.max(0, Math.floor(Number(settings.TargetCount) || 5000))); // 设定的目标数量 -const OCRdelay = Math.min(50, Math.max(0, Math.floor(Number(settings.OcrDelay) || 10))); // OCR基准时长 -const imageDelay = Math.min(1000, Math.max(0, Math.floor(Number(settings.ImageDelay) || 0))); // 识图基准时长 -const timeCost = Math.min(300, Math.max(0, Math.floor(Number(settings.TimeCost) || 30))); // 耗时和材料数量的比值,即一个材料多少秒 +// ============================================== +// 引入外部脚本(源码不变) +// ============================================== +eval(file.readTextSync("lib/file.js")); +eval(file.readTextSync("lib/autoPick.js")); +eval(file.readTextSync("lib/exp.js")); +eval(file.readTextSync("lib/backStats.js")); +eval(file.readTextSync("lib/imageClick.js")); +eval(file.readTextSync("lib/displacement.js")); + +// ============================================== +// 全局状态(保持不变) +// ============================================== +var state = { completed: false, cancelRequested: false }; + +// ============================================== +// 初始化配置参数 +// ============================================== +const timeCost = Math.min(300, Math.max(0, Math.floor(Number(settings.TimeCost) || 30))); const notify = settings.notify || false; -const noRecord = settings.noRecord || false; // 全局控制参数(无需传递) -// 定义映射表"unselected": "反选材料分类", -const material_mapping = { - "General": "一般素材", - "Drops": "怪物掉落素材", - "CookingIngs": "烹饪食材", - "ForagedFood": "采集食物", - "Weekly": "周本素材", - "Wood": "木材", - "CharAscension": "角色突破素材", - "Fishing": "鱼饵鱼类", - "Smithing": "锻造素材", - "Gems": "宝石", - "Talent": "角色天赋素材", - "WeaponAscension": "武器突破素材" -} -// 安全获取 Pathing 的前缀数字(处理 undefined 或非字符串的情况) -const pathingValue = settings.Pathing || ''; // 若未定义,用空字符串兜底 -const pathingPrefix = String(pathingValue).split('.')[0]; // 确保转为字符串后再分割 +const noRecord = settings.noRecord || false; + +// 解析需要处理的CD分类 +const allowedCDCategories = (settings.CDCategories || "") + .split(/[,,、 \s]+/) + .map(cat => cat.trim()) + .filter(cat => cat !== ""); + +if (allowedCDCategories.length > 0) { + log.info(`${CONSTANTS.LOG_MODULES.INIT}已配置只处理以下CD分类:${allowedCDCategories.join('、')}`); +} else { + log.info(`${CONSTANTS.LOG_MODULES.INIT}未配置CD分类过滤,将处理所有分类`); +} + +// ============================================== +// 材料与怪物映射管理"XP": "祝圣精华" +// ============================================== +// 材料分类映射 +const material_mapping = { + "General": "一般素材", + "Drops": "怪物掉落素材", + "CookingIngs": "烹饪食材", + "ForagedFood": "采集食物", + "Weekly": "周本素材", + "Wood": "木材", + "CharAscension": "角色突破素材", + "Fishing": "鱼饵鱼类", + "Smithing": "锻造素材", + "Gems": "宝石", + "Talent": "角色天赋素材", + "WeaponAscension": "武器突破素材", +}; + +// 怪物-材料映射(双向,优化为Set提高查找效率) +let monsterToMaterials = {}; // 怪物名 -> [掉落材料列表] +let materialToMonsters = {}; // 材料名 -> Set(关联怪物列表) + +/** + * 解析怪物-材料映射文件,初始化双向映射 + * 优化点:使用Set存储材料对应的怪物,提高存在性判断效率 + */ +function parseMonsterMaterials() { + try { + const content = file.readTextSync(CONSTANTS.MONSTER_MATERIALS_PATH); + const lines = content.split('\n').map(line => line.trim()).filter(line => line); + + lines.forEach(line => { + if (!line.includes(':')) return; + const [monsterName, materialsStr] = line.split(':'); + const materials = materialsStr.split(/[,,、 \s]+/) + .map(mat => mat.trim()) + .filter(mat => mat); + + if (monsterName && materials.length > 0) { + monsterToMaterials[monsterName] = materials; + materials.forEach(mat => { + if (!materialToMonsters[mat]) { + materialToMonsters[mat] = new Set(); // 用Set替代Array + } + materialToMonsters[mat].add(monsterName); + }); + log.debug(`${CONSTANTS.LOG_MODULES.MONSTER}解析怪物材料:${monsterName} -> [${materials.join(', ')}]`); + } + }); + } catch (error) { + log.error(`${CONSTANTS.LOG_MODULES.MONSTER}解析怪物材料文件失败:${error.message}`); + } +} +parseMonsterMaterials(); // 初始化怪物材料映射 + +// ============================================== +// 路径模式配置 +// ============================================== +const pathingValue = settings.Pathing || ''; +const pathingPrefix = String(pathingValue).split('.')[0]; -// 根据三个选项值设置不同的逻辑标识 const pathingMode = { - // 二者兼并:📁pathing材料覆盖【材料分类】 includeBoth: pathingPrefix === "1", - // 无视【材料分类】勾选:只扫描pathing下的材料,不考虑【材料分类】勾选 onlyPathing: pathingPrefix === "2", - // 无视pathing材料:不扫描pathing下的材料,只考虑【材料分类】勾选 onlyCategory: pathingPrefix === "3" }; -// 增加默认模式兜底(当 prefix 不是 1/2/3 时) const isInvalidMode = !pathingMode.includeBoth && !pathingMode.onlyPathing && !pathingMode.onlyCategory; if (isInvalidMode) { - log.warn(`检测到无效的 Pathing 设置(${pathingValue}),自动切换为默认模式`); - pathingMode.includeBoth = true; // 强制启用默认模式 + log.warn(`${CONSTANTS.LOG_MODULES.PATH}检测到无效的Pathing设置(${pathingValue}),自动切换为【路径材料】专注模式`); + pathingMode.onlyPathing = true; } -// 输出当前模式日志 -if (pathingMode.includeBoth) { - log.warn("默认模式,📁pathing材料 将覆盖 勾选的分类"); -} -if (pathingMode.onlyCategory) { - log.warn("已开启【背包统计】专注模式,将忽略📁pathing材料"); -} -if (pathingMode.onlyPathing) { - log.warn("已开启【路径材料】专注模式,将忽略勾选的分类"); -} -// 初始化 settings,将 material_mapping 中的所有键设置为 false -const initialSettings = Object.keys(material_mapping).reduce((acc, key) => { +if (pathingMode.includeBoth) log.warn(`${CONSTANTS.LOG_MODULES.PATH}默认模式,📁pathing材料 将覆盖 勾选的分类`); +if (pathingMode.onlyCategory) log.warn(`${CONSTANTS.LOG_MODULES.PATH}已开启【背包统计】专注模式,将忽略📁pathing材料`); +if (pathingMode.onlyPathing) log.warn(`${CONSTANTS.LOG_MODULES.PATH}已开启【路径材料】专注模式,将忽略勾选的分类`); + +// ============================================== +// 材料分类处理 +// ============================================== +/** + * 初始化并筛选选中的材料分类 + * @returns {string[]} 选中的材料分类列表 + */ +function getSelectedMaterialCategories() { + const initialSettings = Object.keys(material_mapping).reduce((acc, key) => { acc[key] = false; return acc; -}, {}); + }, {}); -// 合并初始设置和实际的 settings,实际的 settings 会覆盖初始设置 -const finalSettings = { ...initialSettings, ...settings }; + const finalSettings = { ...initialSettings, ...settings }; -// 检查是否启用反选功能 -const isUnselected = finalSettings.unselected === true; - -// 根据反选功能生成选中的材料分类数组 -const selected_materials_array = Object.keys(finalSettings) - .filter(key => key !== "unselected") // 排除 "unselected" 键 + return Object.keys(finalSettings) + .filter(key => key !== "unselected") .filter(key => { - // 确保 finalSettings[key] 是布尔值 - if (typeof finalSettings[key] !== 'boolean') { - console.warn(`非布尔值的键: ${key}, 值: ${finalSettings[key]}`); - return false; - } - return isUnselected ? !finalSettings[key] : finalSettings[key]; + if (typeof finalSettings[key] !== 'boolean') { + log.warn(`${CONSTANTS.LOG_MODULES.MATERIAL}非布尔值的键: ${key}, 值: ${finalSettings[key]}`); + return false; + } + return finalSettings[key]; }) .map(name => { - // 确保 material_mapping 中存在对应的键 - if (!material_mapping[name]) { - console.warn(`material_mapping 中缺失的键: ${name}`); - return null; - } - return material_mapping[name]; + if (!material_mapping[name]) { + log.warn(`${CONSTANTS.LOG_MODULES.MATERIAL}material_mapping中缺失的键: ${name}`); + return null; + } + return material_mapping[name]; }) - .filter(name => name !== null); // 过滤掉 null 值 - - // 初始化游戏窗口大小和返回主界面 - setGameMetrics(1920, 1080, 1); - - // 配置参数 - const pageScrollCount = 22; // 最多滑页次数 - - // 材料分类映射表 - const materialTypeMap = { - "锻造素材": "5", - "怪物掉落素材": "3", - "一般素材": "5", - "周本素材": "3", - "烹饪食材": "5", - "角色突破素材": "3", - "木材": "5", - "宝石": "3", - "鱼饵鱼类": "5", - "角色天赋素材": "3", - "武器突破素材": "3", - "采集食物": "4", - "料理": "4", - }; - - // 材料前位定义 - const materialPriority = { - "锻造素材": 1, - "怪物掉落素材": 1, - "采集食物": 1, - "一般素材": 2, - "周本素材": 2, - "料理": 2, - "烹饪食材": 3, - "角色突破素材": 3, - "木材": 4, - "宝石": 4, - "鱼饵鱼类": 5, - "角色天赋素材": 5, - "武器突破素材": 6, - }; - - // OCR识别文本 - async function recognizeText(ocrRegion, timeout = 10000, retryInterval = 20, maxAttempts = 10, maxFailures = 3, cachedFrame = null) { - let startTime = Date.now(); - let retryCount = 0; - let failureCount = 0; // 用于记录连续失败的次数 - // const results = []; - const frequencyMap = {}; // 用于记录每个结果的出现次数 - - const numberReplaceMap = { - "O": "0", "o": "0", "Q": "0", "0": "0", - "I": "1", "l": "1", "i": "1", "1": "1", "一": "1", - "Z": "2", "z": "2", "2": "2", "二": "2", - "E": "3", "e": "3", "3": "3", "三": "3", - "A": "4", "a": "4", "4": "4", - "S": "5", "s": "5", "5": "5", - "G": "6", "b": "6", "6": "6", - "T": "7", "t": "7", "7": "7", - "B": "8", "θ": "8", "8": "8", - "g": "9", "q": "9", "9": "9", - }; - - const ra = cachedFrame || captureGameRegion(); - while (Date.now() - startTime < timeout && retryCount < maxAttempts) { - let ocrObject = RecognitionObject.Ocr(ocrRegion.x, ocrRegion.y, ocrRegion.width, ocrRegion.height); - ocrObject.threshold = 0.85; // 适当降低阈值以提高速度 - let resList = ra.findMulti(ocrObject); - - if (resList.count === 0) { - failureCount++; - if (failureCount >= maxFailures) { - ocrRegion.x += 3; // 每次缩小6像素 - ocrRegion.width -= 6; // 每次缩小6像素 - retryInterval += 10; - - if (ocrRegion.width <= 12) { - return { success: false }; - } - } - retryCount++; - await sleep(retryInterval); - continue; - } - - for (let res of resList) { - let text = res.text; - text = text.split('').map(char => numberReplaceMap[char] || char).join(''); - // results.push(text); - - if (!frequencyMap[text]) { - frequencyMap[text] = 0; - } - frequencyMap[text]++; - - if (frequencyMap[text] >= 2) { - return { success: true, text: text }; - } - } - - await sleep(retryInterval); - } - - const sortedResults = Object.keys(frequencyMap).sort((a, b) => frequencyMap[b] - frequencyMap[a]); - return sortedResults.length > 0 ? { success: true, text: sortedResults[0] } : { success: false }; - } - - // 滚动页面 - async function scrollPage(totalDistance, stepDistance = 10, delayMs = 5) { - moveMouseTo(999, 750); - await sleep(50); - leftButtonDown(); - const steps = Math.ceil(totalDistance / stepDistance); - for (let j = 0; j < steps; j++) { - const remainingDistance = totalDistance - j * stepDistance; - const moveDistance = remainingDistance < stepDistance ? remainingDistance : stepDistance; - moveMouseBy(0, -moveDistance); - await sleep(delayMs); - } - await sleep(700); - leftButtonUp(); - await sleep(100); - } - -function filterMaterialsByPriority(materialsCategory) { - // 获取当前材料分类的优先级 - const currentPriority = materialPriority[materialsCategory]; - if (currentPriority === undefined) { - throw new Error(`Invalid materialsCategory: ${materialsCategory}`); - } - - // 获取当前材料分类的 materialTypeMap 对应值 - const currentType = materialTypeMap[materialsCategory]; - if (currentType === undefined) { - throw new Error(`Invalid materialTypeMap for: ${materialsCategory}`); - } - - // 获取所有优先级更高的材料分类(前位材料) - const frontPriorityMaterials = Object.keys(materialPriority) - .filter(mat => materialPriority[mat] < currentPriority && materialTypeMap[mat] === currentType); - - // 获取所有优先级更低的材料分类(后位材料) - const backPriorityMaterials = Object.keys(materialPriority) - .filter(mat => materialPriority[mat] > currentPriority && materialTypeMap[mat] === currentType); - // 合并当前和后位材料分类 - const finalFilteredMaterials = [...backPriorityMaterials,materialsCategory ];// 当前材料 - return finalFilteredMaterials + .filter(name => name !== null); } - // 扫描材料 -async function scanMaterials(materialsCategory, materialCategoryMap) { - // 获取当前+后位材料名单 - const priorityMaterialNames = []; - const finalFilteredMaterials = await filterMaterialsByPriority(materialsCategory); - for (const category of finalFilteredMaterials) { - const materialIconDir = `assets/images/${category}`; - const materialIconFilePaths = file.ReadPathSync(materialIconDir); - for (const filePath of materialIconFilePaths) { - const name = basename(filePath).replace(".png", ""); // 去掉文件扩展名 - priorityMaterialNames.push({ category, name }); - } - } - - // 根据材料分类获取对应的材料图片文件夹路径 - const materialIconDir = `assets/images/${materialsCategory}`; - - // 使用 ReadPathSync 读取所有材料图片路径 - const materialIconFilePaths = file.ReadPathSync(materialIconDir); - - // 创建材料种类集合 - const materialCategories = []; - const allMaterials = new Set(); // 用于记录所有需要扫描的材料名称 - const materialImages = {}; // 用于缓存加载的图片 - - // 检查 materialCategoryMap 中当前分类的数组是否为空 - const categoryMaterials = materialCategoryMap[materialsCategory] || []; - const shouldScanAllMaterials = categoryMaterials.length === 0; // 如果为空,则扫描所有材料 - - for (const filePath of materialIconFilePaths) { - const name = basename(filePath).replace(".png", ""); // 去掉文件扩展名 - - // 如果 materialCategoryMap 中当前分类的数组不为空 - // 且当前材料名称不在指定的材料列表中,则跳过加载 - if (pathingMode.onlyPathing && !shouldScanAllMaterials && !categoryMaterials.includes(name)) { - continue; - } - - const mat = file.readImageMatSync(filePath); - if (mat.empty()) { - log.error(`加载图标失败:${filePath}`); - continue; // 跳过当前文件 - } - - materialCategories.push({ name, filePath }); - allMaterials.add(name); // 将材料名称添加到集合中 - materialImages[name] = mat; // 缓存图片 - } - - // 已识别的材料集合,避免重复扫描 - const recognizedMaterials = new Set(); - const unmatchedMaterialNames = new Set(); // 未匹配的材料名称 - const materialInfo = []; // 存储材料名称和数量 - - // 扫描参数 - const tolerance = 1; - const startX = 117; - const startY = 121; - const OffsetWidth = 147; - const columnWidth = 123; - const columnHeight = 750; - const maxColumns = 8; - - // 扫描状态 - let hasFoundFirstMaterial = false; - let lastFoundTime = null; - let shouldEndScan = false; - let foundPriorityMaterial = false; - - // 俏皮话逻辑 - const scanPhrases = [ - "扫描中... 太好啦,有这么多素材!", - "扫描中... 不错的珍宝!", - "扫描中... 侦查骑士,发现目标!", - "扫描中... 嗯哼,意外之喜!", - "扫描中... 嗯?", - "扫描中... 很好,没有放过任何角落!", - "扫描中... 会有烟花材料嘛?", - "扫描中... 嗯,这是什么?", - "扫描中... 这些宝藏积灰了,先清洗一下", - "扫描中... 哇!都是好东西!", - "扫描中... 不虚此行!", - "扫描中... 瑰丽的珍宝,令人欣喜。", - "扫描中... 是对长高有帮助的东西吗?", - "扫描中... 嗯!品相卓越!", - "扫描中... 虽无法比拟黄金,但终有价值。", - "扫描中... 收获不少,可以拿去换几瓶好酒啦。", - "扫描中... 房租和伙食费,都有着落啦!", - "扫描中... 还不赖。", - "扫描中... 荒芜的世界,竟藏有这等瑰宝。", - "扫描中... 运气还不错。", - ]; - - let tempPhrases = [...scanPhrases]; - tempPhrases.sort(() => Math.random() - 0.5); // 打乱数组顺序,确保随机性 - let phrasesStartTime = Date.now(); - - // 扫描背包中的材料 - for (let scroll = 0; scroll <= pageScrollCount; scroll++) { - const ra = captureGameRegion(); - if (!foundPriorityMaterial) { - for (const { category, name } of priorityMaterialNames) { - if (recognizedMaterials.has(name)) { - continue; // 如果已经识别过,跳过 - } - - const filePath = `assets/images/${category}/${name}.png`; - const mat = file.readImageMatSync(filePath); - if (mat.empty()) { - log.error(`加载材料图库失败:${filePath}`); - continue; // 跳过当前文件 - } - - const recognitionObject = RecognitionObject.TemplateMatch(mat, 1146, startY, columnWidth, columnHeight); - recognitionObject.threshold = 0.8; // 设置识别阈值 - - const result = ra.find(recognitionObject); - if (result.isExist() && result.x !== 0 && result.y !== 0) { - foundPriorityMaterial = true; // 标记找到前位材料 - log.info(`发现当前或后位材料: ${name},开始全列扫描`); - break; // 发现前位材料后,退出当前循环 - } - } - } - - if (foundPriorityMaterial) { - for (let column = 0; column < maxColumns; column++) { - const scanX = startX + column * OffsetWidth; - for (let i = 0; i < materialCategories.length; i++) { - const { name } = materialCategories[i]; - if (recognizedMaterials.has(name)) { - continue; // 如果已经识别过,跳过 - } - - const mat = materialImages[name]; - const recognitionObject = RecognitionObject.TemplateMatch(mat, scanX, startY, columnWidth, columnHeight); - recognitionObject.threshold = 0.85; - - const result = ra.find(recognitionObject); - await sleep(imageDelay); - - if (result.isExist() && result.x !== 0 && result.y !== 0) { - recognizedMaterials.add(name); - await moveMouseTo(result.x, result.y); - - const ocrRegion = { - x: result.x - tolerance, - y: result.y + 97 - tolerance, - width: 66 + 2 * tolerance, - height: 22 + 2 * tolerance - }; - const ocrResult = await recognizeText(ocrRegion, 1000, 10, 10, 3); - materialInfo.push({ name, count: ocrResult.success ? ocrResult.text : "?" }); - - if (!hasFoundFirstMaterial) { - hasFoundFirstMaterial = true; - lastFoundTime = Date.now(); - } else { - lastFoundTime = Date.now(); - } - } - } - } - } - - // 每2秒输出一句俏皮话 - const phrasesTime = Date.now(); - if (phrasesTime - phrasesStartTime >= 5000) { - const selectedPhrase = tempPhrases.shift(); - log.info(selectedPhrase); - if (tempPhrases.length === 0) { - tempPhrases = [...scanPhrases]; - tempPhrases.sort(() => Math.random() - 0.5); - } - phrasesStartTime = phrasesTime; - } - - // 检查是否结束扫描 - if (recognizedMaterials.size === allMaterials.size) { - log.info("所有材料均已识别!"); - shouldEndScan = true; - break; - } - - if (hasFoundFirstMaterial && Date.now() - lastFoundTime > 5000) { - log.info("未发现新的材料,结束扫描"); - shouldEndScan = true; - break; - } - - // 检查是否到达最后一页 - const sliderBottomRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/SliderBottom.png"), 1284, 916, 9, 26); - sliderBottomRo.threshold = 0.8; - const sliderBottomResult = ra.find(sliderBottomRo); - if (sliderBottomResult.isExist()) { - log.info("已到达最后一页!"); - shouldEndScan = true; - break; - } - - // 滑动到下一页 - if (scroll < pageScrollCount) { - await scrollPage(680, 10, 5); - await sleep(10); - } - } - - // 处理未匹配的材料 - for (const name of allMaterials) { - if (!recognizedMaterials.has(name)) { - unmatchedMaterialNames.add(name); - } - } - - // 日志记录 - const now = new Date(); - const formattedTime = now.toLocaleString(); - const scanMode = shouldScanAllMaterials ? "全材料扫描" : "指定材料扫描"; - const logContent = ` -${formattedTime} -${scanMode} - ${materialsCategory} 种类: ${recognizedMaterials.size} 数量: -${materialInfo.map(item => `${item.name}: ${item.count}`).join(",")} -未匹配的材料 种类: ${unmatchedMaterialNames.size} 数量: -${Array.from(unmatchedMaterialNames).join(",")} -`; - - const categoryFilePath = `history_record/${materialsCategory}.txt`; // 勾选【材料分类】的历史记录 - const overwriteFilePath = `overwrite_record/${materialsCategory}.txt`; // 所有的历史记录分类储存 - const latestFilePath = "latest_record.txt"; // 所有的历史记录集集合 - if (pathingMode.onlyCategory) { - writeLog(categoryFilePath, logContent); - } - writeLog(overwriteFilePath, logContent); - writeLog(latestFilePath, logContent); // 覆盖模式? - - // 返回结果 - return materialInfo; -} - -function writeLog(filePath, logContent) { - try { - // 1. 读取现有内容(原样读取,不做任何分割处理) - let existingContent = ""; - try { - existingContent = file.readTextSync(filePath); - } catch (e) { - // 文件不存在则保持空 - } - - // 2. 拼接新记录(新记录加在最前面,用两个换行分隔,保留原始格式) - const finalContent = logContent + "\n\n" + existingContent; - - // 3. 按行分割,保留最近365条完整记录(按原始换行分割,不过滤) - const lines = finalContent.split("\n"); - const keepLines = lines.length > 365 * 5 ? lines.slice(0, 365 * 5) : lines; // 假设每条记录最多5行 - const result = file.writeTextSync(filePath, keepLines.join("\n"), false); - - if (result) { - log.info(`写入成功: ${filePath}`); - } else { - log.error(`写入失败: ${filePath}`); - } - } catch (error) { - // 只在文件完全不存在时创建,避免覆盖 - file.writeTextSync(filePath, logContent, false); - log.info(`创建新文件: ${filePath}`); - } -} - -// 定义所有图标的图像识别对象,每个图片都有自己的识别区域 -const BagpackRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Bagpack.png"), 58, 31, 38, 38); -const MaterialsRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Materials.png"), 941, 29, 38, 38); -const CultivationItemsRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/CultivationItems.png"), 749, 30, 38, 38); -const FoodRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Food.png"), 845, 31, 38, 38); - -// 定义一个函数用于识别图像 -async function recognizeImage(recognitionObject, timeout = 5000, cachedFrame=null) { - let startTime = Date.now(); - const ra = cachedFrame || captureGameRegion(); - while (Date.now() - startTime < timeout) { - try { - // 尝试识别图像 - const imageResult = ra.find(recognitionObject); - if (imageResult.isExist() && imageResult.x !== 0 && imageResult.y !== 0) { - return { success: true, x: imageResult.x, y: imageResult.y }; - } - } catch (error) { - log.error(`识别图像时发生异常: ${error.message}`); - } - await sleep(500); // 短暂延迟,避免过快循环 - } - log.warn(`经过多次尝试,仍然无法识别图像`); - return { success: false }; -} -const specialMaterials = [ - "水晶块", "魔晶块", "星银矿石", "紫晶块", "萃凝晶", "铁块", "白铁块", - "精锻用魔矿", "精锻用良矿", "精锻用杂矿" -]; -function filterLowCountMaterials(pathingMaterialCounts, materialCategoryMap) { - // 将 materialCategoryMap 中的所有材料名提取出来 - const allMaterials = Object.values(materialCategoryMap).flat(); - - // 筛选 pathingMaterialCounts 中的材料,只保留 materialCategoryMap 中定义的材料,并且数量低于 targetCount 或 count 为 "?" 或 name 在 specialMaterials 中 - return pathingMaterialCounts - .filter(item => - allMaterials.includes(item.name) && - (item.count < targetCount || item.count === "?") - ) - .map(item => { - // 如果 name 在 specialMaterials 数组中 - if (specialMaterials.includes(item.name)) { - // 如果 count 是 "?",直接保留 - if (item.count === "?") { - return item; - } - // 否则,将 count 除以 10 并向下取整 - item.count = Math.floor(item.count / 10); - } - return item; - }); -} - -function dynamicMaterialGrouping(materialCategoryMap) { - // 初始化动态分组对象 - const dynamicMaterialGroups = {}; - - // 遍历 materialCategoryMap 的 entries - for (const category in materialCategoryMap) { - const type = materialTypeMap[category]; // 获取材料分类对应的组编号(3、4、5) - if (!dynamicMaterialGroups[type]) { - dynamicMaterialGroups[type] = []; // 初始化组 - } - dynamicMaterialGroups[type].push(category); // 将分类加入对应组 - } - - // 对每组内的材料分类按照 materialPriority 排序 - for (const type in dynamicMaterialGroups) { - dynamicMaterialGroups[type].sort((a, b) => materialPriority[a] - materialPriority[b]); - } - - // 将分组结果转换为数组并按类型排序(3, 4, 5) - const sortedGroups = Object.entries(dynamicMaterialGroups) - .map(([type, categories]) => ({ type: parseInt(type), categories })) - .sort((a, b) => a.type - b.type); - - // 返回分组结果 - return sortedGroups; -} - -// 主逻辑函数 -async function MaterialPath(materialCategoryMap) { - // 1. 先记录原始名称与别名的映射关系(用于最后反向转换) - const nameMap = new Map(); - Object.values(materialCategoryMap).flat().forEach(originalName => { - const aliasName = MATERIAL_ALIAS[originalName] || originalName; - nameMap.set(aliasName, originalName); // 存储:别名→原始名 - }); - - // 2. 转换materialCategoryMap为别名(用于内部处理) - const processedMap = {}; - Object.entries(materialCategoryMap).forEach(([category, names]) => { - processedMap[category] = names.map(name => MATERIAL_ALIAS[name] || name); - }); - materialCategoryMap = processedMap; - - const maxStage = 4; // 最大阶段数 - let stage = 0; // 当前阶段 - let currentGroupIndex = 0; // 当前处理的分组索引 - let currentCategoryIndex = 0; // 当前处理的分类索引 - let materialsCategory = ""; // 当前处理的材料分类名称 - const allLowCountMaterials = []; // 用于存储所有识别到的低数量材料信息 - - const sortedGroups = dynamicMaterialGrouping(materialCategoryMap); - sortedGroups.forEach(group => { - log.info(`类型 ${group.type} | 包含分类: ${group.categories.join(', ')}`); -}); - - while (stage <= maxStage) { - switch (stage) { - case 0: // 返回主界面 - log.info("返回主界面"); - await genshin.returnMainUi(); - await sleep(500); - stage = 1; // 进入下一阶段 - break; - - case 1: // 打开背包界面 - keyPress("B"); // 打开背包界面 - await sleep(1000); - await imageClick() - - let backpackResult = await recognizeImage(BagpackRo, 2000); - if (backpackResult.success) { - stage = 2; // 进入下一阶段 - } else { - log.warn("未识别到背包图标,重新尝试"); - stage = 0; // 回退 - } - break; - - case 2: // 按分组处理材料分类 - if (currentGroupIndex < sortedGroups.length) { - const group = sortedGroups[currentGroupIndex]; - - if (currentCategoryIndex < group.categories.length) { - materialsCategory = group.categories[currentCategoryIndex]; - const offset = materialTypeMap[materialsCategory]; - const menuClickX = Math.round(575 + (offset - 1) * 96.25); - click(menuClickX, 75); - - await sleep(500); - stage = 3; // 进入下一阶段 - } else { - currentGroupIndex++; - currentCategoryIndex = 0; // 重置分类索引 - stage = 2; // 继续处理下一组 - } - } else { - stage = 5; // 跳出循环 - } - break; - - case 3: // 识别材料分类 - let CategoryObject; - switch (materialsCategory) { - case "锻造素材": - case "一般素材": - case "烹饪食材": - case "木材": - case "鱼饵鱼类": - CategoryObject = MaterialsRo; - break; - case "采集食物": - case "料理": - CategoryObject = FoodRo; - break; - case "怪物掉落素材": - case "周本素材": - case "角色突破素材": - case "宝石": - case "角色天赋素材": - case "武器突破素材": - CategoryObject = CultivationItemsRo; - break; - default: - log.error("未知的材料分类"); - stage = 0; // 回退到阶段0 - return; - } - - let CategoryResult = await recognizeImage(CategoryObject, 2000); - if (CategoryResult.success) { - log.info(`识别到${materialsCategory} 所在分类。`); - stage = 4; // 进入下一阶段 - } else { - log.warn("未识别到材料分类图标,重新尝试"); - log.warn(`识别结果:${JSON.stringify(CategoryResult)}`); - stage = 2; // 回退到阶段2 - } - break; - - case 4: // 扫描材料 - log.info("芭芭拉,冲鸭!"); - await moveMouseTo(1288, 124); // 移动鼠标至滑条顶端 - await sleep(200); - leftButtonDown(); // 长按左键重置材料滑条 - await sleep(300); - leftButtonUp(); - await sleep(200); - - // 扫描材料并获取低于目标数量的材料 - const lowCountMaterials = await scanMaterials(materialsCategory, materialCategoryMap); - allLowCountMaterials.push(lowCountMaterials); - - currentCategoryIndex++; - stage = 2; // 返回阶段2处理下一个分类 - break; - - case 5: // 所有分组处理完毕 - log.info("所有分组处理完毕,返回主界面"); - await genshin.returnMainUi(); - stage = maxStage + 1; // 确保退出循环 - break; - } - } - - await genshin.returnMainUi(); // 返回主界面 - log.info("扫描流程结束"); - - // 3. 处理完成后,将输出结果转换回原始名称 - const finalResult = allLowCountMaterials.map(categoryMaterials => { - return categoryMaterials.map(material => { - // 假设material包含name属性,将别名转回原始名 - return { - ...material, - name: nameMap.get(material.name) || material.name // 反向映射 - }; - }); - }); - - return finalResult; // 返回转换后的结果(如"晶蝶") - -} - -// 自定义 basename 函数 -function basename(filePath) { - if (typeof filePath !== 'string') throw new Error('Invalid file path'); - const lastSlash = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\')); - return filePath.substring(lastSlash + 1); -} -// 检查路径是否存在 -function pathExists(path) { - try { - const entries = file.readPathSync(path); - return entries !== undefined && entries.length >= 0; - } catch (error) { - return false; - } -} -// 递归读取目录下的所有文件路径,并排除特定后缀的文件 -function readAllFilePaths(dirPath, currentDepth = 0, maxDepth = 3, includeExtensions = ['.png', '.json', '.txt'], includeDirs = false) { - if (!pathExists(dirPath)) { - log.error(`目录 ${dirPath} 不存在`); - return []; - } - - try { - const entries = file.readPathSync(dirPath); // 读取目录内容,返回的是完整路径 - - const filePaths = []; - for (const entry of entries) { - const isDirectory = pathExists(entry); // 如果路径存在且返回的是数组,则认为是目录 - - if (isDirectory) { - if (includeDirs) { - filePaths.push(entry); // 添加目录路径 - } - if (currentDepth < maxDepth) { - filePaths.push(...readAllFilePaths(entry, currentDepth + 1, maxDepth, includeExtensions, includeDirs)); // 递归读取子目录 - } - } else { - const fileExtension = entry.substring(entry.lastIndexOf('.')); - if (includeExtensions.includes(fileExtension.toLowerCase())) { - filePaths.push(entry); // 添加文件路径 - } - } - } - - return filePaths; - } catch (error) { - log.error(`读取目录 ${dirPath} 时发生错误: ${error}`); - return []; - } -} - - -// 解析文件内容,提取材料信息 +const selected_materials_array = getSelectedMaterialCategories(); + +// ============================================== +// CD内容解析 +// ============================================== +/** + * 解析材料CD文件内容,转换为刷新时间与材料的映射 + * @param {string} content - CD文件内容 + * @returns {Object} 刷新时间(JSON字符串)到材料列表的映射 + */ function parseMaterialContent(content) { + if (!content) { + log.warn(`${CONSTANTS.LOG_MODULES.CD}文件内容为空`); + return {}; + } + + const lines = content.split('\n').map(line => line.trim()); + const materialCDInfo = {}; + + lines.forEach(line => { + if (!line.includes(':')) return; + + const [refreshCD, materials] = line.split(':'); + if (!refreshCD || !materials) return; + + let refreshCDInHours; + if (refreshCD.includes('次0点')) { + const times = parseInt(refreshCD.split('次')[0], 10); + if (isNaN(times)) { + log.error(`${CONSTANTS.LOG_MODULES.CD}无效的刷新时间格式:${refreshCD}`); + return; + } + refreshCDInHours = { type: 'midnight', times: times }; + } else if (refreshCD.includes('点')) { + const hours = parseFloat(refreshCD.replace('点', '')); + if (isNaN(hours)) { + log.error(`${CONSTANTS.LOG_MODULES.CD}无效的刷新时间格式:${refreshCD}`); + return; + } + refreshCDInHours = { type: 'specific', hour: hours }; + } else if (refreshCD.includes('小时')) { + const hours = parseFloat(refreshCD.replace('小时', '')); + if (isNaN(hours)) { + log.error(`${CONSTANTS.LOG_MODULES.CD}无效的刷新时间格式:${refreshCD}`); + return; + } + refreshCDInHours = hours; + } else if (refreshCD === '即时刷新') { + refreshCDInHours = { type: 'instant' }; + } else { + log.error(`${CONSTANTS.LOG_MODULES.CD}未知的刷新时间格式:${refreshCD}`); + return; + } + materialCDInfo[JSON.stringify(refreshCDInHours)] = materials + .split(/[,,]\s*/) + .map(material => material.trim()) + .filter(material => material !== ''); + }); + + return materialCDInfo; +} + +// ============================================== +// 路径资源提取(复用并优化) +// ============================================== +/** + * 从路径中提取材料名和怪物名(基于目录结构) + * @param {string} filePath - 路径文件路径 + * @param {Set} cdMaterialNames - CD中存在的材料名集合 + * @returns {Object} { materialName, monsterName } + */ +function extractResourceNameFromPath(filePath, cdMaterialNames) { + const pathParts = filePath.split(/[\\/]/); // 分割路径 + const validMaterials = []; // 材料名匹配结果 + const validMonsters = []; // 怪物名匹配结果 + + // 检查前MAX_PATH_DEPTH层目录 + for (let i = 1; i <= CONSTANTS.MAX_PATH_DEPTH && i < pathParts.length; i++) { + const folderName = pathParts[i].trim(); + // 匹配CD中的材料名 + if (folderName && cdMaterialNames.has(folderName)) { + validMaterials.push({ name: folderName, depth: i }); + } + // 匹配怪物映射中的怪物名 + if (folderName && monsterToMaterials[folderName]) { + validMonsters.push({ name: folderName, depth: i }); + } + } + + // 确定材料名(取最深层匹配) + let materialName = null; + if (validMaterials.length > 0) { + validMaterials.sort((a, b) => a.depth - b.depth); + materialName = validMaterials[0].name; + } + + // 确定怪物名(取最深层匹配) + let monsterName = null; + if (validMonsters.length > 0) { + validMonsters.sort((a, b) => a.depth - b.depth); + monsterName = validMonsters[0].name; + } + + return { materialName, monsterName }; +} + +// ============================================== +// CD分类加载 +// ============================================== +/** + * 读取并解析所有材料CD分类文件 + * @returns {Object} 分类名到CD信息的映射 + */ +function readMaterialCD() { + const materialFilePaths = readAllFilePaths(CONSTANTS.MATERIAL_CD_DIR, 0, 1, ['.txt']); + const materialCDCategories = {}; + + for (const filePath of materialFilePaths) { + const content = file.readTextSync(filePath); if (!content) { - log.warn(`文件内容为空`); - return {}; // 如果内容为空,直接返回空对象 + log.error(`${CONSTANTS.LOG_MODULES.CD}加载文件失败:${filePath}`); + continue; } - const lines = content.split('\n').map(line => line.trim()); - const materialCDInfo = {}; + const sourceCategory = basename(filePath).replace('.txt', ''); + if (allowedCDCategories.length > 0 && !allowedCDCategories.includes(sourceCategory)) { + log.debug(`${CONSTANTS.LOG_MODULES.CD}跳过未选中的CD分类文件:${filePath}`); + continue; + } + materialCDCategories[sourceCategory] = parseMaterialContent(content); + } + return materialCDCategories; +} + +// ============================================== +// 时间工具 +// ============================================== +/** + * 获取当前时间(小时,含小数) + * @returns {number} 当前时间(小时) + */ +function getCurrentTimeInHours() { + const now = new Date(); + return now.getHours() + now.getMinutes() / 60 + now.getSeconds() / 3600; +} + +// ============================================== +// 记录管理 +// ============================================== +/** + * 写入内容到文件(追加模式) + * @param {string} filePath - 目标文件路径 + * @param {string} content - 要写入的内容 + */ +function writeContentToFile(filePath, content) { + try { + let existingContent = ''; + try { + existingContent = file.readTextSync(filePath); + } catch (readError) { + log.debug(`${CONSTANTS.LOG_MODULES.RECORD}文件不存在或读取失败: ${filePath}`); + } + + const updatedContent = content + existingContent; + const result = file.writeTextSync(filePath, updatedContent, false); + if (result) { + log.info(`${CONSTANTS.LOG_MODULES.RECORD}记录成功: ${filePath}`); + } else { + log.error(`${CONSTANTS.LOG_MODULES.RECORD}记录失败: ${filePath}`); + } + } catch (error) { + log.error(`${CONSTANTS.LOG_MODULES.RECORD}记录失败: ${error}`); + } +} + +/** + * 检查路径名出现频率(避免重复无效路径) + * @param {string} resourceName - 资源名 + * @param {string} pathName - 路径名 + * @param {string} recordDir - 记录目录 + * @returns {boolean} 是否允许运行(true=允许) + */ +function checkPathNameFrequency(resourceName, pathName, recordDir) { + const recordPath = `${recordDir}/${resourceName}${CONSTANTS.ZERO_COUNT_SUFFIX}`; + let totalCount = 0; + + try { + const content = file.readTextSync(recordPath); + const lines = content.split('\n'); lines.forEach(line => { - if (!line.includes(':')) { - return; + if (line.startsWith('路径名: ') && line.split('路径名: ')[1] === pathName) { + totalCount++; + } + }); + } catch (error) { + log.debug(`${CONSTANTS.LOG_MODULES.RECORD}目录${recordDir}中无${resourceName}记录,跳过检查`); + } + + if (totalCount >= 3) { + log.info(`${CONSTANTS.LOG_MODULES.RECORD}路径文件: ${pathName},普通模式累计0采集${totalCount}次,请清理记录后再执行`); + return false; + } + return true; +} + +/** + * 记录路径运行时间与材料变化 + * @param {string} resourceName - 资源名 + * @param {string} pathName - 路径名 + * @param {string} startTime - 开始时间 + * @param {string} endTime - 结束时间 + * @param {number} runTime - 运行时间(秒) + * @param {string} recordDir - 记录目录 + * @param {Object} materialCountDifferences - 材料数量变化 + * @param {number} finalCumulativeDistance - 累计移动距离 + */ +function recordRunTime(resourceName, pathName, startTime, endTime, runTime, recordDir, materialCountDifferences = {}, finalCumulativeDistance) { + const recordPath = `${recordDir}/${resourceName}.txt`; + const normalContent = `路径名: ${pathName}\n开始时间: ${startTime}\n结束时间: ${endTime}\n运行时间: ${runTime}秒\n数量变化: ${JSON.stringify(materialCountDifferences)}\n\n`; + + try { + if (runTime >= 3) { + // 处理0数量记录 + for (const [material, count] of Object.entries(materialCountDifferences)) { + if (material === resourceName && count === 0) { + const zeroMaterialPath = `${recordDir}/${material}${CONSTANTS.ZERO_COUNT_SUFFIX}`; + const zeroMaterialContent = `路径名: ${pathName}\n开始时间: ${startTime}\n结束时间: ${endTime}\n运行时间: ${runTime}秒\n数量变化: ${JSON.stringify(materialCountDifferences)}\n\n`; + writeContentToFile(zeroMaterialPath, zeroMaterialContent); + log.warn(`${CONSTANTS.LOG_MODULES.RECORD}材料数目为0,已写入单独文件: ${zeroMaterialPath}`); } + } - const [refreshCD, materials] = line.split(':'); - if (!refreshCD || !materials) { - return; + const hasZeroMaterial = Object.values(materialCountDifferences).includes(0); + const isFinalCumulativeDistanceZero = finalCumulativeDistance === 0; + + if (!(hasZeroMaterial && isFinalCumulativeDistanceZero)) { + writeContentToFile(recordPath, normalContent); + log.info(`${CONSTANTS.LOG_MODULES.RECORD}正常记录已写入: ${recordPath}`); + } else { + if (hasZeroMaterial) log.warn(`${CONSTANTS.LOG_MODULES.RECORD}存在材料数目为0的情况: ${JSON.stringify(materialCountDifferences)}`); + if (isFinalCumulativeDistanceZero) log.warn(`${CONSTANTS.LOG_MODULES.RECORD}累计距离为0: finalCumulativeDistance=${finalCumulativeDistance}`); + log.warn(`${CONSTANTS.LOG_MODULES.RECORD}未写入正常记录: ${recordPath}`); + } + } else { + log.warn(`${CONSTANTS.LOG_MODULES.RECORD}运行时间小于3秒,未满足记录条件: ${recordPath}`); + } + } catch (error) { + log.error(`${CONSTANTS.LOG_MODULES.RECORD}记录运行时间失败: ${error}`); + } +} + +/** + * 获取上次运行结束时间 + * @param {string} resourceName - 资源名 + * @param {string} pathName - 路径名 + * @param {string} recordDir - 记录目录 + * @param {string} noRecordDir - 无记录目录 + * @returns {string|null} 上次结束时间字符串(null=无记录) + */ +function getLastRunEndTime(resourceName, pathName, recordDir, noRecordDir) { + const checkDirs = [recordDir, noRecordDir]; + let latestEndTime = null; + + checkDirs.forEach(dir => { + const recordPath = `${dir}/${resourceName}.txt`; + try { + const content = file.readTextSync(recordPath); + const lines = content.split('\n'); + + for (let i = 0; i < lines.length; i++) { + if (lines[i].startsWith('路径名: ') && lines[i].split('路径名: ')[1] === pathName) { + const endTimeLine = lines[i + 2]; + if (endTimeLine?.startsWith('结束时间: ')) { + const endTimeStr = endTimeLine.split('结束时间: ')[1]; + const endTime = new Date(endTimeStr); + + if (!latestEndTime || endTime > new Date(latestEndTime)) { + latestEndTime = endTimeStr; + } + } } + } + } catch (error) { + log.debug(`${CONSTANTS.LOG_MODULES.RECORD}目录${dir}中无${resourceName}记录,跳过检查`); + } + }); - // 处理特殊规则,如“N次0点”和“即时刷新” - let refreshCDInHours; - if (refreshCD.includes('次0点')) { - const times = parseInt(refreshCD.split('次')[0], 10); - if (isNaN(times)) { - log.error(`无效的刷新时间格式:${refreshCD}`); - return; - } - refreshCDInHours = { type: 'midnight', times: times }; - } else if (refreshCD.includes('点')) { - const hours = parseFloat(refreshCD.replace('点', '')); - if (isNaN(hours)) { - log.error(`无效的刷新时间格式:${refreshCD}`); - return; - } - refreshCDInHours = { type: 'specific', hour: hours }; - } else if (refreshCD.includes('小时')) { - const hours = parseFloat(refreshCD.replace('小时', '')); - if (isNaN(hours)) { - log.error(`无效的刷新时间格式:${refreshCD}`); - return; - } - refreshCDInHours = hours; - } else if (refreshCD === '即时刷新') { - refreshCDInHours = { type: 'instant' }; - } else { - log.error(`未知的刷新时间格式:${refreshCD}`); - return; + return latestEndTime; +} + +/** + * 计算单次时间成本(平均耗时/材料数量) + * @param {string} resourceName - 资源名 + * @param {string} pathName - 路径名 + * @param {string} recordDir - 记录目录 + * @returns {number|null} 时间成本(秒/个),null=无法计算 + */ +function calculatePerTime(resourceName, pathName, recordDir) { + const recordPath = `${recordDir}/${resourceName}.txt`; + try { + const content = file.readTextSync(recordPath); + const lines = content.split('\n'); + const completeRecords = []; + + for (let i = 0; i < lines.length; i++) { + if (lines[i].startsWith('路径名: ') && lines[i].split('路径名: ')[1] === pathName) { + const runTimeLine = lines[i + 3]; + const quantityChangeLine = lines[i + 4]; + + if (runTimeLine?.startsWith('运行时间: ') && quantityChangeLine?.startsWith('数量变化: ')) { + const runTime = parseInt(runTimeLine.split('运行时间: ')[1].split('秒')[0], 10); + const quantityChange = JSON.parse(quantityChangeLine.split('数量变化: ')[1]); + + if (quantityChange[resourceName] !== undefined && quantityChange[resourceName] !== 0) { + completeRecords.push(parseFloat((runTime / quantityChange[resourceName]).toFixed(2))); + } } + } + } - materialCDInfo[JSON.stringify(refreshCDInHours)] = materials.split(',').map(material => material.trim()).filter(material => material !== ''); + if (completeRecords.length < 3) { + log.warn(`${CONSTANTS.LOG_MODULES.RECORD}路径${pathName}有效记录不足3条,无法计算时间成本`); + return null; + } + const recentRecords = completeRecords.slice(-5).filter(record => !isNaN(record) && record !== Infinity); + log.debug(`${CONSTANTS.LOG_MODULES.RECORD}路径${pathName}最近记录: ${JSON.stringify(recentRecords)}`); + + const mean = recentRecords.reduce((acc, val) => acc + val, 0) / recentRecords.length; + const stdDev = Math.sqrt(recentRecords.reduce((acc, val) => acc + Math.pow(val - mean, 2), 0) / recentRecords.length); + const filteredRecords = recentRecords.filter(record => Math.abs(record - mean) <= 1 * stdDev); + + if (filteredRecords.length === 0) { + log.warn(`${CONSTANTS.LOG_MODULES.RECORD}路径${pathName}记录数据差异过大,无法计算有效时间成本`); + return null; + } + + return parseFloat((filteredRecords.reduce((acc, val) => acc + val, 0) / filteredRecords.length).toFixed(2)); + } catch (error) { + log.warn(`${CONSTANTS.LOG_MODULES.RECORD}路径${pathName}无有效记录,无法计算时间成本`); + } + return null; +} + +// ============================================== +// 路径运行控制 +// ============================================== +/** + * 检查路径是否可运行(基于CD和上次运行时间) + * @param {number} currentTime - 当前时间(小时) + * @param {string|null} lastEndTime - 上次结束时间 + * @param {Object|number} refreshCD - 刷新CD配置 + * @param {string} pathName - 路径名 + * @returns {boolean} 是否可运行 + */ +function canRunPathingFile(currentTime, lastEndTime, refreshCD, pathName) { + if (!lastEndTime) return true; + + const lastEndTimeDate = new Date(lastEndTime); + const currentDate = new Date(); + + if (typeof refreshCD === 'object') { + if (refreshCD.type === 'midnight') { + const times = refreshCD.times; + const nextRunTime = new Date(lastEndTimeDate); + nextRunTime.setDate(lastEndTimeDate.getDate() + times); + nextRunTime.setHours(0, 0, 0, 0); + const canRun = currentDate >= nextRunTime; + log.info(`${CONSTANTS.LOG_MODULES.CD}路径${pathName}上次运行:${lastEndTimeDate.toLocaleString()},下次运行:${nextRunTime.toLocaleString()}`); + return canRun; + } else if (refreshCD.type === 'specific') { + const specificHour = refreshCD.hour; + const currentDate = new Date(); + const lastDate = new Date(lastEndTimeDate); + const todayRefresh = new Date(currentDate); + todayRefresh.setHours(specificHour, 0, 0, 0); + if (currentDate > todayRefresh && currentDate.getDate() !== lastDate.getDate()) { + return true; + } + const nextRefreshTime = new Date(todayRefresh); + if (currentDate >= todayRefresh) nextRefreshTime.setDate(nextRefreshTime.getDate() + 1); + log.info(`${CONSTANTS.LOG_MODULES.CD}路径${pathName}上次运行:${lastEndTimeDate.toLocaleString()},下次运行:${nextRefreshTime.toLocaleString()}`); + return false; + } else if (refreshCD.type === 'instant') { + return true; + } + } else { + const nextRefreshTime = new Date(lastEndTimeDate.getTime() + refreshCD * 3600 * 1000); + log.info(`${CONSTANTS.LOG_MODULES.CD}路径${pathName}上次运行:${lastEndTimeDate.toLocaleString()},下次运行:${nextRefreshTime.toLocaleString()}`); + return currentDate >= nextRefreshTime; + } + + return false; +} + +// ============================================== +// 图像匹配与分类 +// ============================================== +// 材料别名映射 +const MATERIAL_ALIAS = { + '晶蝶': '晶核', + '白铁矿': '白铁块', + '铁矿': '铁块', +}; +const imageMapCache = new Map(); // 保持固定,不动态刷新 + +/** + * 创建图像分类映射(目录到分类的映射) + * @param {string} imagesDir - 图像目录 + * @returns {Object} 图像名到分类的映射 + */ +const createImageCategoryMap = (imagesDir) => { + const map = {}; + const imageFiles = readAllFilePaths(imagesDir, 0, 1, ['.png']); + + for (const imagePath of imageFiles) { + const pathParts = imagePath.split(/[\\/]/); + if (pathParts.length < 3) continue; + + const imageName = pathParts.pop() + .replace(/\.png$/i, '') + .trim() + .toLowerCase(); + + if (!(imageName in map)) { + map[imageName] = pathParts[2]; + } + } + + return map; +}; + +const loggedResources = new Set(); + +/** + * 匹配图像并获取材料分类 + * @param {string} resourceName - 资源名 + * @param {string} imagesDir - 图像目录 + * @returns {string|null} 材料分类(null=未匹配) + */ +function matchImageAndGetCategory(resourceName, imagesDir) { + const processedName = (MATERIAL_ALIAS[resourceName] || resourceName).toLowerCase(); + + if (!imageMapCache.has(imagesDir)) { + log.debug(`${CONSTANTS.LOG_MODULES.MATERIAL}初始化图像分类缓存:${imagesDir}`); + imageMapCache.set(imagesDir, createImageCategoryMap(imagesDir)); + } + + const result = imageMapCache.get(imagesDir)[processedName] ?? null; + if (result) { + log.debug(`${CONSTANTS.LOG_MODULES.MATERIAL}资源${resourceName}匹配分类:${result}`); + } else { + log.debug(`${CONSTANTS.LOG_MODULES.MATERIAL}资源${resourceName}未匹配到分类`); + } + + if (!loggedResources.has(processedName)) { + loggedResources.add(processedName); + } + + return result; +} + +// ============================================== +// 路径处理(拆分巨型函数) +// ============================================== +/** + * 处理狗粮路径条目 + * @param {Object} entry - 路径条目 { path, resourceName } + * @param {Object} accumulators - 累加器 { foodExpAccumulator, currentMaterialName } + * @param {string} recordDir - 记录目录 + * @param {string} noRecordDir - 无记录目录 + * @returns {Object} 更新后的累加器 + */ +async function processFoodPathEntry(entry, accumulators, recordDir, noRecordDir) { + const { path: pathingFilePath, resourceName } = entry; + const pathName = basename(pathingFilePath); + const { foodExpAccumulator, currentMaterialName: prevMaterialName } = accumulators; + + // 切换目标材料 + let currentMaterialName = prevMaterialName; + if (currentMaterialName !== resourceName) { + if (prevMaterialName && foodExpAccumulator[prevMaterialName]) { + const prevMsg = `材料[${prevMaterialName}]收集完成,累计EXP:${foodExpAccumulator[prevMaterialName]}`; + sendNotificationInChunks(prevMsg, notification.Send); + } + currentMaterialName = resourceName; + foodExpAccumulator[resourceName] = 0; + log.info(`${CONSTANTS.LOG_MODULES.PATH}切换至狗粮材料【${resourceName}】`); + } + + // 执行路径 + const startTime = new Date().toLocaleString(); + const initialPosition = genshin.getPositionFromMap(); + await pathingScript.runFile(pathingFilePath); + const finalPosition = genshin.getPositionFromMap(); + const finalCumulativeDistance = calculateDistance(initialPosition, finalPosition); + const endTime = new Date().toLocaleString(); + const runTime = (new Date(endTime) - new Date(startTime)) / 1000; + + // 处理分解与记录 + const { success, totalExp } = await executeSalvageWithOCR(); + foodExpAccumulator[resourceName] += totalExp; + + const recordDirFinal = noRecord ? noRecordDir : recordDir; + const foodRecordContent = `路径名: ${pathName}\n开始时间: ${startTime}\n结束时间: ${endTime}\n运行时间: ${runTime}秒\n移动距离: ${finalCumulativeDistance.toFixed(2)}\n分解状态: ${success ? "成功" : "失败"}\n本次EXP获取: ${totalExp}\n累计EXP获取: ${foodExpAccumulator[resourceName]}\n\n`; + writeContentToFile(`${recordDirFinal}/${resourceName}${CONSTANTS.FOOD_EXP_RECORD_SUFFIX}`, foodRecordContent); + + const foodMsg = `狗粮路径【${pathName}】执行完成\n耗时:${runTime.toFixed(1)}秒\n本次EXP:${totalExp}\n累计EXP:${foodExpAccumulator[resourceName]}`; + sendNotificationInChunks(foodMsg, notification.Send); + + await sleep(1); // 保留sleep(1) + return { ...accumulators, foodExpAccumulator, currentMaterialName }; +} + +/** + * 处理怪物路径条目 + * @param {Object} entry - 路径条目 { path, monsterName, resourceName } + * @param {Object} context - 上下文 { CDCategories, timeCost, recordDir, noRecordDir, imagesDir, ... } + * @returns {Object} 更新后的上下文 + */ +async function processMonsterPathEntry(entry, context) { + const { path: pathingFilePath, monsterName } = entry; + const pathName = basename(pathingFilePath); + const { + CDCategories, timeCost, recordDir, noRecordDir, imagesDir, + materialCategoryMap, flattenedLowCountMaterials, + currentMaterialName: prevMaterialName, + materialAccumulatedDifferences, globalAccumulatedDifferences + } = context; + + // 用怪物名查CD + let refreshCD = null; + for (const [categoryName, cdInfo] of Object.entries(CDCategories)) { + if (allowedCDCategories.length > 0 && !allowedCDCategories.includes(categoryName)) continue; + for (const [cdKey, cdItems] of Object.entries(cdInfo)) { + if (cdItems.includes(monsterName)) { + refreshCD = JSON.parse(cdKey); + break; + } + } + if (refreshCD) break; + } + + if (!refreshCD) { + log.debug(`${CONSTANTS.LOG_MODULES.MONSTER}怪物【${monsterName}】未找到CD配置,跳过路径:${pathName}`); + await sleep(1); + return context; + } + + // 检查是否可运行 + const currentTime = getCurrentTimeInHours(); + const lastEndTime = getLastRunEndTime(monsterName, pathName, recordDir, noRecordDir); + const isPathValid = checkPathNameFrequency(monsterName, pathName, recordDir); + const perTime = noRecord ? null : calculatePerTime(monsterName, pathName, recordDir); + + log.info(`${CONSTANTS.LOG_MODULES.PATH}怪物路径${pathName} 单个材料耗时:${perTime ?? '忽略'}`); + + if (!(canRunPathingFile(currentTime, lastEndTime, refreshCD, pathName) && isPathValid && (noRecord || perTime === null || perTime <= timeCost))) { + log.info(`${CONSTANTS.LOG_MODULES.PATH}怪物路径${pathName} 不符合运行条件`); + await sleep(1); + return context; + } + + // 构建怪物掉落材料的分类映射(用于扫描) + const resourceCategoryMap = {}; + const materials = monsterToMaterials[monsterName] || []; + materials.forEach(mat => { + const category = matchImageAndGetCategory(mat, imagesDir); + if (category) { + if (!resourceCategoryMap[category]) resourceCategoryMap[category] = []; + if (!resourceCategoryMap[category].includes(mat)) { + resourceCategoryMap[category].push(mat); + } + } + }); + log.debug(`${CONSTANTS.LOG_MODULES.MONSTER}怪物${monsterName}的扫描分类:${JSON.stringify(resourceCategoryMap)}`); + + // 处理运行逻辑 + let currentMaterialName = prevMaterialName; + let updatedFlattened = flattenedLowCountMaterials; + + if (noRecord) { + // noRecord模式 + if (currentMaterialName !== monsterName) { + currentMaterialName = monsterName; + materialAccumulatedDifferences[monsterName] = {}; + log.info(`${CONSTANTS.LOG_MODULES.PATH}noRecord模式:切换目标至怪物【${monsterName}】`); + } + + const startTime = new Date().toLocaleString(); + const initialPosition = genshin.getPositionFromMap(); + await pathingScript.runFile(pathingFilePath); + const finalPosition = genshin.getPositionFromMap(); + const finalCumulativeDistance = calculateDistance(initialPosition, finalPosition); + const endTime = new Date().toLocaleString(); + const runTime = (new Date(endTime) - new Date(startTime)) / 1000; + + const noRecordContent = `路径名: ${pathName}\n开始时间: ${startTime}\n结束时间: ${endTime}\n运行时间: ${runTime}秒\n数量变化: noRecord模式忽略\n\n`; + writeContentToFile(`${noRecordDir}/${monsterName}.txt`, noRecordContent); + } else { + // 普通记录模式 + if (currentMaterialName !== monsterName) { + if (prevMaterialName && materialAccumulatedDifferences[prevMaterialName]) { + const prevMsg = `目标[${prevMaterialName}]收集完成,累计获取:${JSON.stringify(materialAccumulatedDifferences[prevMaterialName])}`; + sendNotificationInChunks(prevMsg, notification.Send); + } + currentMaterialName = monsterName; + const updatedLowCountMaterials = await MaterialPath(resourceCategoryMap); + updatedFlattened = updatedLowCountMaterials + .flat() + .sort((a, b) => parseInt(a.count, 10) - parseInt(b.count, 10)); + materialAccumulatedDifferences[monsterName] = {}; + } + + const startTime = new Date().toLocaleString(); + const initialPosition = genshin.getPositionFromMap(); + await pathingScript.runFile(pathingFilePath); + const finalPosition = genshin.getPositionFromMap(); + const finalCumulativeDistance = calculateDistance(initialPosition, finalPosition); + const endTime = new Date().toLocaleString(); + const runTime = (new Date(endTime) - new Date(startTime)) / 1000; + + // 计算材料变化 + const updatedLowCountMaterials = await MaterialPath(resourceCategoryMap); + const flattenedUpdated = updatedLowCountMaterials.flat().sort((a, b) => a.count - b.count); + + const materialCountDifferences = {}; + flattenedUpdated.forEach(updated => { + const original = updatedFlattened.find(m => m.name === updated.name); + if (original) { + const diff = parseInt(updated.count) - parseInt(original.count); + if (diff !== 0 || updated.name === updated.name) { + materialCountDifferences[updated.name] = diff; + globalAccumulatedDifferences[updated.name] = (globalAccumulatedDifferences[updated.name] || 0) + diff; + materialAccumulatedDifferences[monsterName][updated.name] = (materialAccumulatedDifferences[monsterName][updated.name] || 0) + diff; + } + } }); - return materialCDInfo; + // 更新材料计数缓存 + updatedFlattened = updatedFlattened.map(m => { + const updated = flattenedUpdated.find(u => u.name === m.name); + return updated ? { ...m, count: updated.count } : m; + }); + + log.info(`${CONSTANTS.LOG_MODULES.MATERIAL}怪物路径${pathName}数量变化: ${JSON.stringify(materialCountDifferences)}`); + recordRunTime(monsterName, pathName, startTime, endTime, runTime, recordDir, materialCountDifferences, finalCumulativeDistance); + } + + await sleep(1); // 保留sleep(1) + return { + ...context, + currentMaterialName, + flattenedLowCountMaterials: updatedFlattened, + materialAccumulatedDifferences, + globalAccumulatedDifferences + }; } -// 从路径中提取材料名 -function extractResourceNameFromPath(filePath) { - const pathParts = filePath.split('\\'); // 或者使用 '/',取决于你的路径分隔符 - if (pathParts.length < 3) { - log.warn(`路径格式不正确,无法提取材料名:${filePath}`); - return null; // 返回 null 表示无法提取材料名 - } - // 第一层文件夹名即为材料名 - return pathParts[1]; -} -// 从 materials 文件夹中读取分类信息 -function readMaterialCategories(materialDir) { - const materialFilePaths = readAllFilePaths(materialDir, 0, 1, ['.txt']); - const materialCategories = {}; - - for (const filePath of materialFilePaths) { - const content = file.readTextSync(filePath); // 同步读取文本文件内容 - if (!content) { - log.error(`加载文件失败:${filePath}`); - continue; // 跳过当前文件 - } - - const sourceCategory = basename(filePath).replace('.txt', ''); // 去掉文件扩展名 - materialCategories[sourceCategory] = parseMaterialContent(content); - } - return materialCategories; -} - -// 获取当前时间(以小时为单位) -function getCurrentTimeInHours() { - const now = new Date(); - return now.getHours() + now.getMinutes() / 60 + now.getSeconds() / 3600; -} - -// 辅助函数:写入内容到文件 -function writeContentToFile(filePath, content) { - try { - // 读取文件现有内容 - let existingContent = ''; - try { - existingContent = file.readTextSync(filePath); // 读取文件内容 - } catch (readError) { - // 如果文件不存在或读取失败,existingContent 保持为空字符串 - log.warn(`文件读取失败或文件不存在: ${filePath}`); - } - - // 将新的记录内容插入到最前面 - const updatedContent = content + existingContent; - - // 将更新后的内容写回文件 - const result = file.writeTextSync(filePath, updatedContent, false); // 覆盖写入 - if (result) { - log.info(`记录成功: ${filePath}`); - } else { - log.error(`记录失败: ${filePath}`); - } - } catch (error) { - log.error(`记录失败: ${error}`); - } -} - -function checkPathNameFrequency(recordDir, resourceName, pathName) { - const recordPath = `${recordDir}/${resourceName}-0.txt`; // 记录文件路径,以 resourceName-0.txt 命名 - try { - const content = file.readTextSync(recordPath); // 同步读取记录文件 - const lines = content.split('\n'); - - let totalCount = 0; // 用于记录路径名出现的总次数 - - // 从文件内容的开头开始查找 - for (let i = 0; i < lines.length; i++) { - if (lines[i].startsWith('路径名: ')) { - const currentPathName = lines[i].split('路径名: ')[1]; - if (currentPathName === pathName) { - totalCount++; // 如果当前路径名匹配,计数加1 - } - } - } - - // 如果路径名出现次数超过3次,返回 false - if (totalCount >= 3) { - log.info(`路径文件: ${pathName}, 多次0采集,请检查后,删除记录再执行`); - return false; - } - - // 如果路径名出现次数不超过3次,返回 true - return true; - } catch (error) { - log.warn(`读取文件时发生错误: ${recordPath}`, error); - return true; // 如果文件不存在或读取失败,认为路径名出现次数不超过3次 - } -} - -function recordRunTime(resourceName, pathName, startTime, endTime, runTime, recordDir, materialCountDifferences = {}, finalCumulativeDistance) { - const recordPath = `${recordDir}/${resourceName}.txt`; // 正常记录文件路径 - const normalContent = `路径名: ${pathName}\n开始时间: ${startTime}\n结束时间: ${endTime}\n运行时间: ${runTime}秒\n数量变化: ${JSON.stringify(materialCountDifferences)}\n\n`; - - try { - // 只有当运行时间大于或等于3秒时,才记录运行时间 - if (runTime >= 3) { - // 检查 materialCountDifferences 中是否存在材料数目为 0 的情况 - for (const [material, count] of Object.entries(materialCountDifferences)) { - if (material === resourceName && count === 0) { - // 如果材料数目为 0,记录到单独的文件 - const zeroMaterialPath = `${recordDir}/${material}-0.txt`; // 材料数目为0的记录文件路径 - const zeroMaterialContent = `路径名: ${pathName}\n开始时间: ${startTime}\n结束时间: ${endTime}\n运行时间: ${runTime}秒\n数量变化: ${JSON.stringify(materialCountDifferences)}\n\n`; - writeContentToFile(zeroMaterialPath, zeroMaterialContent); // 写入材料数目为0的记录 - log.warn(`材料数目为0,已写入单独文件: ${zeroMaterialPath}`); - } - } - - // 检查是否需要记录正常内容 - const hasZeroMaterial = Object.values(materialCountDifferences).includes(0); - const isFinalCumulativeDistanceZero = finalCumulativeDistance === 0; - - if (!(hasZeroMaterial && isFinalCumulativeDistanceZero)) { - // 写入正常记录的内容 - writeContentToFile(recordPath, normalContent); - log.info(`正常记录已写入: ${recordPath}`); - } else { - if (hasZeroMaterial) { - log.warn(`存在材料数目为0的情况: ${JSON.stringify(materialCountDifferences)}`); - } - if (isFinalCumulativeDistanceZero) { - log.warn(`累计距离为0: finalCumulativeDistance=${finalCumulativeDistance}`); - } - log.warn(`未写入正常记录: ${recordPath}`); - } - } else { - log.warn(`运行时间小于3秒,未满足记录条件: ${recordPath}`); - } - } catch (error) { - log.error(`记录运行时间失败: ${error}`); - } -} - -// -------------------------- 新增:通知分段发送工具函数 -------------------------- /** - * 通知分段发送(超过500字符自动拆分) - * @param {string} msg - 原始通知消息 - * @param {Function} sendFn - 原始通知发送函数(如notification.Send) - * @param {number} chunkSize - 每段最大字符数(默认500) + * 处理普通材料路径条目 + * @param {Object} entry - 路径条目 { path, resourceName } + * @param {Object} context - 上下文(同怪物路径) + * @returns {Object} 更新后的上下文 */ -function sendNotificationInChunks(msg, sendFn, chunkSize = 500) { - if (!notify) return; // 未开启通知,直接返回 - if (typeof msg !== 'string' || msg.length === 0) return; +async function processNormalPathEntry(entry, context) { + const { path: pathingFilePath, resourceName } = entry; + const pathName = basename(pathingFilePath); + const { + CDCategories, timeCost, recordDir, noRecordDir, + materialCategoryMap, flattenedLowCountMaterials, + currentMaterialName: prevMaterialName, + materialAccumulatedDifferences, globalAccumulatedDifferences + } = context; - // 短消息直接发送 - if (msg.length <= chunkSize) { - sendFn(msg); - return; + // 用材料名查CD + let refreshCD = null; + for (const [categoryName, cdInfo] of Object.entries(CDCategories)) { + if (allowedCDCategories.length > 0 && !allowedCDCategories.includes(categoryName)) continue; + for (const [cdKey, cdItems] of Object.entries(cdInfo)) { + if (cdItems.includes(resourceName)) { + refreshCD = JSON.parse(cdKey); + break; + } + } + if (refreshCD) break; + } + + if (!refreshCD) { + log.debug(`${CONSTANTS.LOG_MODULES.MATERIAL}材料【${resourceName}】未找到CD配置,跳过路径:${pathName}`); + await sleep(1); + return context; + } + + // 检查是否可运行 + const currentTime = getCurrentTimeInHours(); + const lastEndTime = getLastRunEndTime(resourceName, pathName, recordDir, noRecordDir); + const isPathValid = checkPathNameFrequency(resourceName, pathName, recordDir); + const perTime = noRecord ? null : calculatePerTime(resourceName, pathName, recordDir); + + log.info(`${CONSTANTS.LOG_MODULES.PATH}材料路径${pathName} 单个材料耗时:${perTime ?? '忽略'}`); + + if (!(canRunPathingFile(currentTime, lastEndTime, refreshCD, pathName) && isPathValid && (noRecord || perTime === null || perTime <= timeCost))) { + log.info(`${CONSTANTS.LOG_MODULES.PATH}材料路径${pathName} 不符合运行条件`); + await sleep(1); + return context; + } + + // 构建材料分类映射(用于扫描) + const resourceCategoryMap = {}; + for (const [cat, list] of Object.entries(materialCategoryMap)) { + if (list.includes(resourceName)) { + resourceCategoryMap[cat] = [resourceName]; + break; + } + } + + // 处理运行逻辑(同怪物路径,区别在于用resourceName作为记录键) + let currentMaterialName = prevMaterialName; + let updatedFlattened = flattenedLowCountMaterials; + + if (noRecord) { + if (currentMaterialName !== resourceName) { + currentMaterialName = resourceName; + materialAccumulatedDifferences[resourceName] = {}; + log.info(`${CONSTANTS.LOG_MODULES.PATH}noRecord模式:切换目标至材料【${resourceName}】`); } - // 长消息拆分发送 - const totalChunks = Math.ceil(msg.length / chunkSize); - log.info(`通知消息过长(${msg.length}字符),将拆分为${totalChunks}段发送`); - - let start = 0; - for (let i = 0; i < totalChunks; i++) { - // 截取当前段(最后一段取剩余所有字符) - const end = Math.min(start + chunkSize, msg.length); - const chunkMsg = `【通知${i+1}/${totalChunks}】\n${msg.substring(start, end)}`; - - // 发送当前段 - sendFn(chunkMsg); - log.info(`已发送第${i+1}段通知(${chunkMsg.length}字符)`); - - // 更新起始位置 - start = end; + const startTime = new Date().toLocaleString(); + const initialPosition = genshin.getPositionFromMap(); + await pathingScript.runFile(pathingFilePath); + const finalPosition = genshin.getPositionFromMap(); + const finalCumulativeDistance = calculateDistance(initialPosition, finalPosition); + const endTime = new Date().toLocaleString(); + const runTime = (new Date(endTime) - new Date(startTime)) / 1000; + + const noRecordContent = `路径名: ${pathName}\n开始时间: ${startTime}\n结束时间: ${endTime}\n运行时间: ${runTime}秒\n数量变化: noRecord模式忽略\n\n`; + writeContentToFile(`${noRecordDir}/${resourceName}.txt`, noRecordContent); + } else { + if (currentMaterialName !== resourceName) { + if (prevMaterialName && materialAccumulatedDifferences[prevMaterialName]) { + const prevMsg = `目标[${prevMaterialName}]收集完成,累计获取:${JSON.stringify(materialAccumulatedDifferences[prevMaterialName])}`; + sendNotificationInChunks(prevMsg, notification.Send); + } + currentMaterialName = resourceName; + const updatedLowCountMaterials = await MaterialPath(resourceCategoryMap); + updatedFlattened = updatedLowCountMaterials + .flat() + .sort((a, b) => parseInt(a.count, 10) - parseInt(b.count, 10)); + materialAccumulatedDifferences[resourceName] = {}; } + + const startTime = new Date().toLocaleString(); + const initialPosition = genshin.getPositionFromMap(); + await pathingScript.runFile(pathingFilePath); + const finalPosition = genshin.getPositionFromMap(); + const finalCumulativeDistance = calculateDistance(initialPosition, finalPosition); + const endTime = new Date().toLocaleString(); + const runTime = (new Date(endTime) - new Date(startTime)) / 1000; + + // 计算材料变化 + const updatedLowCountMaterials = await MaterialPath(resourceCategoryMap); + const flattenedUpdated = updatedLowCountMaterials.flat().sort((a, b) => a.count - b.count); + + const materialCountDifferences = {}; + flattenedUpdated.forEach(updated => { + const original = updatedFlattened.find(m => m.name === updated.name); + if (original) { + const diff = parseInt(updated.count) - parseInt(original.count); + if (diff !== 0 || updated.name === resourceName) { + materialCountDifferences[updated.name] = diff; + globalAccumulatedDifferences[updated.name] = (globalAccumulatedDifferences[updated.name] || 0) + diff; + materialAccumulatedDifferences[resourceName][updated.name] = (materialAccumulatedDifferences[resourceName][updated.name] || 0) + diff; + } + } + }); + + // 更新材料计数缓存 + updatedFlattened = updatedFlattened.map(m => { + const updated = flattenedUpdated.find(u => u.name === m.name); + return updated ? { ...m, count: updated.count } : m; + }); + + log.info(`${CONSTANTS.LOG_MODULES.MATERIAL}材料路径${pathName}数量变化: ${JSON.stringify(materialCountDifferences)}`); + recordRunTime(resourceName, pathName, startTime, endTime, runTime, recordDir, materialCountDifferences, finalCumulativeDistance); + } + + await sleep(1); // 保留sleep(1) + return { + ...context, + currentMaterialName, + flattenedLowCountMaterials: updatedFlattened, + materialAccumulatedDifferences, + globalAccumulatedDifferences + }; } -// noRecord=true时的最终汇总记录函数 +/** + * 批量处理所有路径 + * @param {Object[]} allPaths - 所有路径条目 + * @param {Object} CDCategories - CD分类配置 + * @param {Object} materialCategoryMap - 材料分类映射 + * @param {number} timeCost - 时间成本阈值 + * @param {Object[]} flattenedLowCountMaterials - 低数量材料列表 + * @param {string|null} currentMaterialName - 当前处理的材料名 + * @param {string} recordDir - 记录目录 + * @param {string} noRecordDir - 无记录目录 + * @param {string} imagesDir - 图像目录 + * @returns {Object} 处理结果 + */ +async function processAllPaths(allPaths, CDCategories, materialCategoryMap, timeCost, flattenedLowCountMaterials, currentMaterialName, recordDir, noRecordDir, imagesDir) { + try { + // 初始化累加器 + const foodExpAccumulator = {}; + const globalAccumulatedDifferences = {}; + const materialAccumulatedDifferences = {}; + let context = { + CDCategories, timeCost, recordDir, noRecordDir, imagesDir, + materialCategoryMap, flattenedLowCountMaterials, + currentMaterialName, materialAccumulatedDifferences, + globalAccumulatedDifferences + }; + + for (const entry of allPaths) { + if (state.cancelRequested) break; + + try { + const { path: pathingFilePath, resourceName, monsterName } = entry; + log.info(`${CONSTANTS.LOG_MODULES.PATH}开始处理路径:${basename(pathingFilePath)}`); + + // 区分路径类型并处理 + if (resourceName && isFoodResource(resourceName)) { + // 狗粮路径 + const result = await processFoodPathEntry(entry, { + foodExpAccumulator, + currentMaterialName: context.currentMaterialName + }, recordDir, noRecordDir); + foodExpAccumulator = result.foodExpAccumulator; + context.currentMaterialName = result.currentMaterialName; + } else if (monsterName) { + // 怪物路径 + context = await processMonsterPathEntry(entry, context); + } else if (resourceName) { + // 普通材料路径 + context = await processNormalPathEntry(entry, context); + } else { + log.warn(`${CONSTANTS.LOG_MODULES.PATH}跳过无效路径条目:${JSON.stringify(entry)}`); + } + } catch (singleError) { + log.error(`${CONSTANTS.LOG_MODULES.PATH}处理路径出错,已跳过:${singleError.message}`); + await sleep(1); + } + } + + // 最后一个目标收尾 + if (context.currentMaterialName) { + if (isFoodResource(context.currentMaterialName) && foodExpAccumulator[context.currentMaterialName]) { + const finalMsg = `狗粮材料[${context.currentMaterialName}]收集完成,累计EXP:${foodExpAccumulator[context.currentMaterialName]}`; + sendNotificationInChunks(finalMsg, notification.Send); + } else if (materialAccumulatedDifferences[context.currentMaterialName]) { + const finalMsg = `目标[${context.currentMaterialName}]收集完成,累计获取:${JSON.stringify(materialAccumulatedDifferences[context.currentMaterialName])}`; + sendNotificationInChunks(finalMsg, notification.Send); + } + } + + return { + currentMaterialName: context.currentMaterialName, + flattenedLowCountMaterials: context.flattenedLowCountMaterials, + globalAccumulatedDifferences: context.globalAccumulatedDifferences, + foodExpAccumulator + }; + + } catch (error) { + log.error(`${CONSTANTS.LOG_MODULES.PATH}路径处理整体错误:${error.message}`); + throw error; + } finally { + log.info(`${CONSTANTS.LOG_MODULES.PATH}路径组处理结束`); + state.completed = true; + } +} + +// ============================================== +// 路径分类与优先级(简化逻辑) +// ============================================== +/** + * 分类普通材料路径(按目标和低数量排序) + * @param {string} pathingDir - 路径目录 + * @param {string[]} targetResourceNames - 目标资源名列表 + * @param {string[]} lowCountMaterialNames - 低数量材料名列表 + * @param {Set} cdMaterialNames - CD中存在的材料名集合 + * @returns {Object[]} 分类后的路径条目 + */ +function classifyNormalPathFiles(pathingDir, targetResourceNames, lowCountMaterialNames, cdMaterialNames) { + const pathingFilePaths = readAllFilePaths(pathingDir, 0, 3, ['.json']); + const pathEntries = pathingFilePaths.map(path => { + const { materialName } = extractResourceNameFromPath(path, cdMaterialNames); + return { path, resourceName: materialName }; + }).filter(entry => entry.resourceName); + + if (pathEntries.length > 0) { + log.info(`${CONSTANTS.LOG_MODULES.PATH}\n===== 匹配到的材料路径列表 =====`); + pathEntries.forEach((entry, index) => { + log.info(`${index + 1}. 材料:${entry.resourceName},路径:${entry.path}`); + }); + log.info(`=================================\n`); + } else { + log.info(`${CONSTANTS.LOG_MODULES.PATH}未匹配到任何有效的材料路径`); + } + + // 按优先级分类 + const prioritizedPaths = []; + const normalPaths = []; + for (const entry of pathEntries) { + if (targetResourceNames.includes(entry.resourceName)) { + prioritizedPaths.push(entry); + } else if (lowCountMaterialNames.includes(entry.resourceName)) { + normalPaths.push(entry); + } + } + // 按低数量排序 + normalPaths.sort((a, b) => { + const indexA = lowCountMaterialNames.indexOf(a.resourceName); + const indexB = lowCountMaterialNames.indexOf(b.resourceName); + return indexA - indexB; + }); + return prioritizedPaths.concat(normalPaths); +} + +/** + * 生成最终路径数组(按优先级排序,简化逻辑) + * @param {string} pathingDir - 路径目录 + * @param {string[]} targetResourceNames - 目标资源名列表 + * @param {Set} cdMaterialNames - CD中存在的材料名集合 + * @param {Object} materialCategoryMap - 材料分类映射 + * @param {Object} pathingMode - 路径模式配置 + * @param {string} imagesDir - 图像目录 + * @returns {Object} { allPaths, pathingMaterialCounts } + */ +async function generateAllPaths(pathingDir, targetResourceNames, cdMaterialNames, materialCategoryMap, pathingMode, imagesDir) { + // 缓存路径文件列表(减少IO) + const pathingFilePaths = readAllFilePaths(pathingDir, 0, 3, ['.json']); + const pathEntries = pathingFilePaths.map(path => { + const { materialName, monsterName } = extractResourceNameFromPath(path, cdMaterialNames); + return { path, resourceName: materialName, monsterName }; + }).filter(entry => (entry.resourceName || entry.monsterName) && entry.path.trim() !== ""); + + log.info(`${CONSTANTS.LOG_MODULES.PATH}[路径初始化] 共读取有效路径 ${pathEntries.length} 条`); + + // 分类路径(狗粮 > 怪物 > 普通材料) + const foodPaths = pathEntries.filter(entry => entry.resourceName && isFoodResource(entry.resourceName)); + const monsterPaths = pathEntries.filter(entry => entry.monsterName && !isFoodResource(entry.resourceName)); + const normalPaths = pathEntries.filter(entry => entry.resourceName && !isFoodResource(entry.resourceName) && !entry.monsterName); + + log.info(`${CONSTANTS.LOG_MODULES.PATH}[路径分类] 狗粮:${foodPaths.length} 怪物:${monsterPaths.length} 普通:${normalPaths.length}`); + + // 怪物路径关联材料到分类(扫描用) + log.info(`${CONSTANTS.LOG_MODULES.MONSTER}开始处理${monsterPaths.length}条怪物路径的材料分类关联...`); + monsterPaths.forEach((entry, index) => { + const materials = monsterToMaterials[entry.monsterName] || []; + if (materials.length === 0) { + log.warn(`${CONSTANTS.LOG_MODULES.MONSTER}[怪物路径${index+1}] 怪物【${entry.monsterName}】无对应材料映射`); + return; + } + materials.forEach(mat => { + const category = matchImageAndGetCategory(mat, imagesDir); + if (!category) return; + if (!materialCategoryMap[category]) materialCategoryMap[category] = []; + if (!materialCategoryMap[category].includes(mat)) { + materialCategoryMap[category].push(mat); + log.debug(`${CONSTANTS.LOG_MODULES.MONSTER}怪物【${entry.monsterName}】的材料【${mat}】加入分类【${category}】`); + } + }); + }); + + // 处理普通材料路径 + let processedFoodPaths = foodPaths; + let processedNormalPaths = []; + let processedMonsterPaths = monsterPaths; + let pathingMaterialCounts = []; + + if (normalPaths.length > 0) { + log.info(`${CONSTANTS.LOG_MODULES.PATH}[普通材料] 执行背包扫描→低数量筛选`); + pathingMaterialCounts = await MaterialPath(materialCategoryMap); + if (pathingMode.onlyCategory) { + state.cancelRequested = true; + return { allPaths: [], pathingMaterialCounts }; + } + const lowCountMaterialsFiltered = filterLowCountMaterials(pathingMaterialCounts.flat(), materialCategoryMap); + const flattenedLowCountMaterials = lowCountMaterialsFiltered.flat().sort((a, b) => a.count - b.count); + const lowCountMaterialNames = flattenedLowCountMaterials.map(material => material.name); + + processedNormalPaths = classifyNormalPathFiles(pathingDir, targetResourceNames, lowCountMaterialNames, cdMaterialNames) + .filter(entry => normalPaths.some(n => n.path.replace(/\\/g, '/') === entry.path.replace(/\\/g, '/'))); + log.info(`${CONSTANTS.LOG_MODULES.PATH}[普通材料] 筛选后保留路径 ${processedNormalPaths.length} 条`); + } + + // 简化路径优先级逻辑:用规则数组定义优先级 + const PATH_PRIORITIES = [ + // 1. 目标狗粮 + { + source: processedFoodPaths, + filter: e => targetResourceNames.includes(e.resourceName) + }, + // 2. 目标怪物(掉落材料含目标) + { + source: processedMonsterPaths, + filter: e => { + const materials = monsterToMaterials[e.monsterName] || []; + return materials.some(mat => targetResourceNames.includes(mat)); + } + }, + // 3. 目标普通材料 + { + source: processedNormalPaths, + filter: e => targetResourceNames.includes(e.resourceName) + }, + // 4. 剩余狗粮 + { + source: processedFoodPaths, + filter: e => !targetResourceNames.includes(e.resourceName) + }, + // 5. 剩余怪物 + { + source: processedMonsterPaths, + filter: e => { + const materials = monsterToMaterials[e.monsterName] || []; + return !materials.some(mat => targetResourceNames.includes(mat)); + } + }, + // 6. 剩余普通材料 + { + source: processedNormalPaths, + filter: e => !targetResourceNames.includes(e.resourceName) + } + ]; + + // 按优先级合并路径 + const allPaths = []; + PATH_PRIORITIES.forEach(({ source, filter }, index) => { + const filtered = source.filter(filter); + allPaths.push(...filtered); + log.info(`${CONSTANTS.LOG_MODULES.PATH}[优先级${index+1}] 路径 ${filtered.length} 条`); + }); + + log.info(`${CONSTANTS.LOG_MODULES.PATH}[最终路径] 共${allPaths.length}条:${allPaths.map(p => basename(p.path))}`); + return { allPaths, pathingMaterialCounts }; +} + +// ============================================== +// 通知工具 +// ============================================== +/** + * 分块发送通知(保持原有逻辑) + * @param {string} msg - 通知消息 + * @param {Function} sendFn - 发送函数 + */ +function sendNotificationInChunks(msg, sendFn) { + if (!notify) return; + if (typeof msg !== 'string' || msg.length === 0) return; + + const chunkSize = CONSTANTS.NOTIFICATION_CHUNK_SIZE; + if (msg.length <= chunkSize) { + sendFn(msg); + return; + } + + const totalChunks = Math.ceil(msg.length / chunkSize); + log.info(`${CONSTANTS.LOG_MODULES.MAIN}通知消息过长(${msg.length}字符),拆分为${totalChunks}段发送`); + + let start = 0; + for (let i = 0; i < totalChunks; i++) { + const end = Math.min(start + chunkSize, msg.length); + const chunkMsg = `【通知${i+1}/${totalChunks}】\n${msg.substring(start, end)}`; + sendFn(chunkMsg); + log.info(`${CONSTANTS.LOG_MODULES.MAIN}已发送第${i+1}段通知(${chunkMsg.length}字符)`); + start = end; + } +} + +// ============================================== +// 结果汇总 +// ============================================== +/** + * 记录最终汇总结果 + * @param {string} recordDir - 记录目录 + * @param {string} firstScanTime - 首次扫描时间 + * @param {string} endTime - 结束时间 + * @param {number} totalRunTime - 总耗时(秒) + * @param {Object} totalDifferences - 总材料变化 + */ function recordFinalSummary(recordDir, firstScanTime, endTime, totalRunTime, totalDifferences) { - const summaryPath = `${recordDir}/final_summary_noRecord.txt`; - const content = `===== 材料收集最终汇总(noRecord模式)===== + const summaryPath = `${recordDir}/${CONSTANTS.SUMMARY_FILE_NAME}`; + const content = `===== 材料收集汇总 ===== 首次扫描时间:${firstScanTime} 末次扫描时间:${endTime} 总耗时:${totalRunTime.toFixed(1)}秒 累计获取材料: ${Object.entries(totalDifferences).map(([name, diff]) => ` ${name}: +${diff}个`).join('\n')} =========================================\n\n`; - writeContentToFile(summaryPath, content); - log.info(`noRecord模式:最终汇总已记录至 ${summaryPath}`); + writeContentToFile(summaryPath, content); + log.info(`${CONSTANTS.LOG_MODULES.RECORD}最终汇总已记录至 ${summaryPath}`); } -// 读取材料对应的文件,获取上次运行的结束时间 -function getLastRunEndTime(resourceName, pathName, recordDir, noRecordDir) { // 接收recordDir和noRecordDir参数 - const checkDirs = [recordDir, noRecordDir]; - let latestEndTime = null; +// ============================================== +// 主执行函数 +// ============================================== +(async function () { + setGameMetrics(1920, 1080, 1); + await genshin.returnMainUi(); - checkDirs.forEach(dir => { - const recordPath = `${dir}/${resourceName}.txt`; - try { - const content = file.readTextSync(recordPath); - const lines = content.split('\n'); + // 初始化固定资源 + const fDialogueRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/F_Dialogue.png"), 1102, 335, 34, 400); + const textxRange = { min: 1210, max: 1412 }; + const texttolerance = 2; - for (let i = 0; i < lines.length; i++) { - if (lines[i].startsWith('路径名: ') && lines[i].split('路径名: ')[1] === pathName) { - const endTimeLine = lines[i + 2]; - if (endTimeLine?.startsWith('结束时间: ')) { - const endTimeStr = endTimeLine.split('结束时间: ')[1]; - const endTime = new Date(endTimeStr); + // 目标资源处理 + const targetResourceNamesStr = settings.TargetresourceName || ""; + const targetResourceNames = targetResourceNamesStr + .split(/[,,、 \s]+/) + .map(name => name.trim()) + .filter(name => name !== ""); - if (!latestEndTime || endTime > new Date(latestEndTime)) { - latestEndTime = endTimeStr; - } - } - } - } - } catch (error) { - log.debug(`目录${dir}中无${resourceName}.txt记录,跳过检查`); + const targetTextCategories = readtargetTextCategories(CONSTANTS.TARGET_TEXT_DIR); + + // 并行任务:OCR交互 + const ocrTask = (async () => { + let allTargetTexts = []; + for (const categoryName in targetTextCategories) { + const targetTexts = targetTextCategories[categoryName]; + allTargetTexts = allTargetTexts.concat(Object.values(targetTexts).flat()); + } + await alignAndInteractTarget(allTargetTexts, fDialogueRo, textxRange, texttolerance); + })(); + + // 并行任务:路径处理 + const pathTask = (async () => { + log.info(`${CONSTANTS.LOG_MODULES.MAIN}开始路径处理流程`); + + // 加载CD分类 + const CDCategories = readMaterialCD(); + const cdMaterialNames = new Set(); + for (const [categoryName, cdInfo] of Object.entries(CDCategories)) { + if (allowedCDCategories.length > 0 && !allowedCDCategories.includes(categoryName)) continue; + for (const [_, materialList] of Object.entries(cdInfo)) { + materialList.forEach(name => cdMaterialNames.add(name)); + } + } + log.info(`${CONSTANTS.LOG_MODULES.CD}CD文件中材料名(已过滤):${Array.from(cdMaterialNames).join(', ')}`); + + // 生成材料分类映射(含怪物掉落) + let materialCategoryMap = {}; + if (!pathingMode.onlyCategory) { + const pathingFilePaths = readAllFilePaths(CONSTANTS.PATHING_DIR, 0, 3, ['.json']); + const pathEntries = pathingFilePaths.map(path => { + const { materialName, monsterName } = extractResourceNameFromPath(path, cdMaterialNames); + return { materialName, monsterName }; + }); + + // 收集所有材料(含怪物掉落) + const allMaterials = new Set(); + pathEntries.forEach(({ materialName, monsterName }) => { + if (materialName) allMaterials.add(materialName); + if (monsterName) { + (monsterToMaterials[monsterName] || []).forEach(mat => allMaterials.add(mat)); } + }); + + // 构建分类映射 + materialCategoryMap = Array.from(allMaterials).reduce((acc, resourceName) => { + const category = matchImageAndGetCategory(resourceName, CONSTANTS.IMAGES_DIR); + if (category) { + if (!acc[category]) acc[category] = []; + if (!acc[category].includes(resourceName)) { + acc[category].push(resourceName); + } + } + return acc; + }, {}); + } + + // 处理选中的材料分类 + if (selected_materials_array.length > 0) { + selected_materials_array.forEach(selectedCategory => { + if (!materialCategoryMap[selectedCategory]) { + materialCategoryMap[selectedCategory] = []; + } + }); + } else { + log.warn(`${CONSTANTS.LOG_MODULES.MATERIAL}未选择【材料分类】,采用【路径材料】专注模式`); + } + + if (pathingMode.onlyPathing) { + Object.keys(materialCategoryMap).forEach(category => { + if (materialCategoryMap[category].length === 0) { + delete materialCategoryMap[category]; + } + }); + } + + // 生成路径数组 + const { allPaths, pathingMaterialCounts } = await generateAllPaths( + CONSTANTS.PATHING_DIR, + targetResourceNames, + cdMaterialNames, + materialCategoryMap, + pathingMode, + CONSTANTS.IMAGES_DIR + ); + + // 处理所有路径 + let currentMaterialName = null; + let flattenedLowCountMaterials = []; + + const firstScanTime = new Date().toLocaleString(); + const initialMaterialCounts = pathingMaterialCounts.flat().reduce((acc, mat) => { + acc[mat.name] = parseInt(mat.count, 10) || 0; + return acc; + }, {}); + + const processResult = await processAllPaths( + allPaths, + CDCategories, + materialCategoryMap, + timeCost, + flattenedLowCountMaterials, + currentMaterialName, + CONSTANTS.RECORD_DIR, + CONSTANTS.NO_RECORD_DIR, + CONSTANTS.IMAGES_DIR + ); + + // 汇总结果 + const globalAccumulatedDifferences = processResult.globalAccumulatedDifferences; + const foodExpAccumulator = processResult.foodExpAccumulator || {}; + + // 末次扫描 + log.info(`${CONSTANTS.LOG_MODULES.MAIN}开始末次背包扫描,计算总差值`); + const finalMaterialCounts = await MaterialPath(materialCategoryMap); + const flattenedFinal = finalMaterialCounts.flat(); + const finalCounts = flattenedFinal.reduce((acc, mat) => { + acc[mat.name] = parseInt(mat.count, 10) || 0; + return acc; + }, {}); + + // 计算总差异 + const totalDifferences = {}; + // 普通材料差异 + Object.keys(initialMaterialCounts).forEach(name => { + const diff = finalCounts[name] - initialMaterialCounts[name]; + if (diff > 0) { + totalDifferences[name] = diff; + } + }); + // 狗粮EXP差异 + Object.keys(foodExpAccumulator).forEach(name => { + totalDifferences[`${name}_EXP`] = foodExpAccumulator[name]; }); - return latestEndTime; -} - -// 计算时间成本 -function calculatePerTime(resourceName, pathName, recordDir) { - const recordPath = `${recordDir}/${resourceName}.txt`; // 记录文件路径,以材料名命名 - try { - const content = file.readTextSync(recordPath); // 同步读取记录文件 - const lines = content.split('\n'); - - const completeRecords = []; // 用于存储完整的记录 - - // 从文件内容的开头开始查找 - for (let i = 0; i < lines.length; i++) { - if (lines[i].startsWith('路径名: ')) { - const currentPathName = lines[i].split('路径名: ')[1]; - if (currentPathName === pathName) { - const runTimeLine = lines[i + 3]; // 假设运行时间在路径名后的第四行 - const quantityChangeLine = lines[i + 4]; // 假设数量变化在路径名后的第五行 - - if (runTimeLine.startsWith('运行时间: ') && quantityChangeLine.startsWith('数量变化: ')) { - const runTime = parseInt(runTimeLine.split('运行时间: ')[1].split('秒')[0], 10); - const quantityChange = JSON.parse(quantityChangeLine.split('数量变化: ')[1]); - - // 检查数量变化是否有效 - if (quantityChange[resourceName] !== undefined) { - let perTime; - if (quantityChange[resourceName] !== 0) { - // 保留两位小数 - perTime = parseFloat((runTime / quantityChange[resourceName]).toFixed(2)); - } else { - perTime = Infinity; // 数量变化为 0 时,设置为 Infinity - } - completeRecords.push(perTime); - } - } - } - } - } - - // 如果完整记录少于3条,返回 null - if (completeRecords.length < 3) { - log.warn(` ${pathName}有效记录不足3条,无法计算平均时间成本: ${recordPath}`); - return null; - } - - // 只考虑最近的5条记录, 过滤掉 Infinity 和 NaN 值 - const recentRecords = completeRecords.slice(-5).filter(record => !isNaN(record) && record !== Infinity); - - // 打印最近的记录 - log.info(` ${pathName}最近的记录: ${JSON.stringify(recentRecords)}`); - - // 计算平均值和标准差 - const mean = recentRecords.reduce((acc, val) => acc + val, 0) / recentRecords.length; - const stdDev = Math.sqrt(recentRecords.reduce((acc, val) => acc + Math.pow(val - mean, 2), 0) / recentRecords.length); - - // 排除差异过大的数据 - const filteredRecords = recentRecords.filter(record => Math.abs(record - mean) <= 1 * stdDev);// 使用1倍标准差作为过滤条件 - - // 如果过滤后没有剩余数据,返回 null - if (filteredRecords.length === 0) { - log.warn(` ${pathName}记录数据差异过大,无法计算有效的时间成本: ${recordPath}`); - return null; - } - - // 计算平均时间成本 - const averagePerTime = parseFloat((filteredRecords.reduce((acc, val) => acc + val, 0) / filteredRecords.length).toFixed(2)); - return averagePerTime; - } catch (error) { - log.warn(`缺失耗时或者数量变化,无法计算 ${pathName}时间成本: ${recordPath}`); - } - return null; // 如果未找到记录文件或效率数据,返回 null -} - -// 判断是否可以运行脚本 -function canRunPathingFile(currentTime, lastEndTime, refreshCD, pathName) { - if (!lastEndTime) { - return true; // 如果没有上次运行记录,直接可以运行 - } - - const lastEndTimeDate = new Date(lastEndTime); - const currentDate = new Date(); - - if (typeof refreshCD === 'object') { - if (refreshCD.type === 'midnight') { - // 处理“N次0点”这样的特殊规则 - const times = refreshCD.times; - - // 计算下一个刷新时间 - const nextRunTime = new Date(lastEndTimeDate); - nextRunTime.setDate(lastEndTimeDate.getDate() + times); // 在上次运行时间的基础上加上N天 - nextRunTime.setHours(0, 0, 0, 0); // 将时间设置为午夜0点 - - // 判断是否可以运行 - const canRun = currentDate >= nextRunTime; - - log.info(`路径文件${pathName}上次运行时间:${lastEndTimeDate.toLocaleString()},下次运行时间:${nextRunTime.toLocaleString()}`); - return canRun; - } else if (refreshCD.type === 'specific') { - const specificHour = refreshCD.hour; - const currentDate = new Date(); - const lastDate = new Date(lastEndTimeDate); - // 当天固定刷新点(如今天4:00) - const todayRefresh = new Date(currentDate); - todayRefresh.setHours(specificHour, 0, 0, 0); - // 条件:过了今天刷新点 + 跨日期 - if (currentDate > todayRefresh && currentDate.getDate() !== lastDate.getDate()) { - return true; - } - // 计算下次时间(今天没到就今天,过了就明天) - const nextRefreshTime = new Date(todayRefresh); - if (currentDate >= todayRefresh) nextRefreshTime.setDate(nextRefreshTime.getDate() + 1); - log.info(`路径文件${pathName}上次运行时间:${lastEndTimeDate.toLocaleString()},下次运行时间:${nextRefreshTime.toLocaleString()}`); - return false; - } else if (refreshCD.type === 'instant') { - // 处理“即时刷新”这样的特殊规则 - return true; - } - } else { - // 处理普通刷新时间 - const nextRefreshTime = new Date(lastEndTimeDate.getTime() + refreshCD * 3600 * 1000); - log.info(`路径文件${pathName}上次运行时间:${lastEndTimeDate.toLocaleString()},下次运行时间:${nextRefreshTime.toLocaleString()}`); - return currentDate >= nextRefreshTime; - } - - return false; -} - -const MATERIAL_ALIAS = { - '晶蝶': '晶核', - '白铁矿': '白铁块', - '铁矿': '铁块', - // 添加更多别名映射... -}; -const imageMapCache = new Map(); - -const createImageCategoryMap = (imagesDir) => { - const map = {}; - const imageFiles = readAllFilePaths(imagesDir, 0, 1, ['.png']); - - for (const imagePath of imageFiles) { - const pathParts = imagePath.split(/[\\/]/); - if (pathParts.length < 3) continue; - - // 统一小写存储(新增逻辑) - const imageName = pathParts.pop() - .replace(/\.png$/i, '') - .trim() - .toLowerCase(); // 新增 - - if (!(imageName in map)) { - map[imageName] = pathParts[2]; - } - } - return map; -}; -// 模块级去重集合(新增) -const loggedResources = new Set(); - -function matchImageAndGetCategory(resourceName, imagesDir) { - const processedName = (MATERIAL_ALIAS[resourceName] || resourceName) - .toLowerCase(); - - if (!imageMapCache.has(imagesDir)) { - imageMapCache.set(imagesDir, createImageCategoryMap(imagesDir)); - } - - const result = imageMapCache.get(imagesDir)[processedName] ?? null; - - // Set 去重逻辑 - if (!loggedResources.has(processedName)) { - loggedResources.add(processedName); - } - - return result; -} - -(async function () { - // 定义文件夹路径 - const materialDir = "materialsCD"; // 存储材料信息的文件夹 - const pathingDir = "pathing"; // 存储路径信息的文件夹 - const recordDir = "pathing_record"; // 存储运行记录的文件夹 - const noRecordDir = `${recordDir}/noRecord`; // 基于recordDir定义 - - const imagesDir = "assets\\images"; // 存储图片的文件夹 - - // 从设置中获取目标材料名称 - const targetResourceNamesStr = settings.TargetresourceName || ""; - - // 使用正则表达式分割字符串,支持多种分隔符(如逗号、分号、空格等) - const targetResourceNames = targetResourceNamesStr - .split(/[,,、 \s]+/) // 使用正则表达式分割字符串 - .map(name => name.trim()) // 去除每个元素的多余空格 - .filter(name => name !== ""); // 过滤掉空字符串 - - // 打印目标材料名称数组 - log.info(`优先材料名称数组: ${JSON.stringify(targetResourceNames)}`); - - try { - // 读取材料分类信息 - const materialCategories = readMaterialCategories(materialDir); - - // 递归读取路径信息文件夹 - const pathingFilePaths = readAllFilePaths(pathingDir, 0, 3, ['.json']); - - // 将路径和资源名绑定,避免重复提取 - const pathEntries = pathingFilePaths.map(path => ({ - path, - resourceName: extractResourceNameFromPath(path) - })); - - // 从路径文件中提取材料名 - const resourceNames = pathEntries - ?.map(entry => entry.resourceName) - .filter(name => name) || []; // 确保 resourceNames 是一个数组 - - // 生成材料与分类的映射对象 - let materialCategoryMap = {}; - // 选项2: +选项1: 二者兼并 - 把路径材料名resourceNames纳入materialCategoryMap - if (!pathingMode.onlyCategory) { - materialCategoryMap = resourceNames.reduce((acc, resourceName) => { - const category = matchImageAndGetCategory(resourceName, imagesDir); // 获取材料的分类 - if (category) { - // 初始化分类键(如果不存在) - if (!acc[category]) acc[category] = []; - // 将材料名加入对应分类数组(避免重复) - if (!acc[category].includes(resourceName)) { - acc[category].push(resourceName); - } - } - return acc; - }, {}); - } - - // 确保 selected_materials_array 中的分类被初始化为空数组 - if (Object.keys(selected_materials_array).length === 0) { - log.warn("==================\n 未选择【材料分类】!将采用【兼容模式】\n =================="); - } else { - selected_materials_array.forEach(selectedCategory => { - if (!materialCategoryMap[selectedCategory]) { - materialCategoryMap[selectedCategory] = []; - } - }); - } - - // 选项2: 仅路径材料 - 移除空数组 - if (pathingMode.onlyPathing) { - Object.keys(materialCategoryMap).forEach(category => { - if (materialCategoryMap[category].length === 0) { - delete materialCategoryMap[category]; - } - }); - } - - // 调用背包材料统计 - const pathingMaterialCounts = await MaterialPath(materialCategoryMap); - // log.info(`materialCategoryMap文本:${JSON.stringify(materialCategoryMap)}`); - // log.info(`目标文本:${JSON.stringify(pathingMaterialCounts)}`); - if (pathingMode.onlyCategory) { - return; - } - // 调用 filterLowCountMaterials 过滤材料信息,先将嵌套数组展平,然后再进行筛选 - const lowCountMaterialsFiltered = filterLowCountMaterials(pathingMaterialCounts.flat(), materialCategoryMap); - - // 展平数组并按数量从小到大排序 - let flattenedLowCountMaterials = lowCountMaterialsFiltered - .flat() - .sort((a, b) => parseInt(a.count, 10) - parseInt(b.count, 10)); - - // 提取低数量材料的名称 - const lowCountMaterialNames = flattenedLowCountMaterials.map(material => material.name); - - // 当低数量材料名称数组为空时,输出所有路径材料都高于目标数量的日志 - if (lowCountMaterialNames.length === 0) { - log.info(`所有路径材料的数量均高于目标数量${targetCount}`); - } - // log.info(`目标文本:${JSON.stringify(lowCountMaterialNames)}`); - // 将路径文件按是否为目标材料分类 - const prioritizedPaths = []; - const normalPaths = []; - - for (const { path, resourceName } of pathEntries) { - if (!resourceName) { - log.warn(`无法提取材料名:${path}`); - continue; - } - - // 检查当前 resourceName 是否在 targetResourceNames 中 - if (targetResourceNames.includes(resourceName)) { - prioritizedPaths.push({ path, resourceName }); - } else if (lowCountMaterialNames.includes(resourceName)) { - // 只有当 resourceName 不在 targetResourceNames 中时,才将其加入到 normalPaths - normalPaths.push({ path, resourceName }); - } - } - - // 按照 flattenedLowCountMaterials 的顺序对 normalPaths 进行排序 - normalPaths.sort((a, b) => { - const indexA = lowCountMaterialNames.indexOf(a.resourceName); - const indexB = lowCountMaterialNames.indexOf(b.resourceName); - return indexA - indexB; - }); - // 合并优先路径和普通路径 - const allPaths = prioritizedPaths.concat(normalPaths); - - dispatcher.addTimer(new RealtimeTimer("AutoPick", { "forceInteraction": false })); - - // 假设 flattenedLowCountMaterials 是一个全局变量或在外部定义的变量 - let currentMaterialName = null; // 用于记录当前材料名 - - // 记录首次扫描时间和初始数量(noRecord=true时用) - const firstScanTime = new Date().toLocaleString(); - const initialMaterialCounts = flattenedLowCountMaterials.reduce((acc, mat) => { - acc[mat.name] = parseInt(mat.count, 10) || 0; - return acc; - }, {}); - // 全局累积差值统计(记录所有材料的总变化量) - const globalAccumulatedDifferences = {}; - // 按材料分类的累积差值统计(记录每种材料的累计变化) - const materialAccumulatedDifferences = {}; - - // 遍历所有路径文件 - for (const { path: pathingFilePath, resourceName } of allPaths) { - const pathName = basename(pathingFilePath); // 假设路径文件名即为材料路径 - - // 查找材料对应的CD分类 - let categoryFound = false; - for (const [category, materials] of Object.entries(materialCategories)) { - for (const [refreshCDKey, materialList] of Object.entries(materials)) { - const refreshCD = JSON.parse(refreshCDKey); - if (materialList.includes(resourceName)) { - const currentTime = getCurrentTimeInHours(); - // 调用时传递recordDir和noRecordDir - const lastEndTime = getLastRunEndTime(resourceName, pathName, recordDir, noRecordDir); - // 调用时传递recordDir - const isPathValid = checkPathNameFrequency(resourceName, pathName, recordDir); - // 调用时传递recordDir - const perTime = noRecord ? null : calculatePerTime(resourceName, pathName, recordDir); - - log.info(`路径文件:${pathName} 单个材料耗时:${perTime || 'noRecord模式忽略'}`); - - if ( - canRunPathingFile(currentTime, lastEndTime, refreshCD, pathName) && - isPathValid && - (noRecord || perTime === null || perTime <= timeCost) - ) { - log.info(`可调用路径文件:${pathName}`); - - const resourceCategoryMap = {}; - for (const [materialCategory, materialList] of Object.entries(materialCategoryMap)) { - if (materialList.includes(resourceName)) { - resourceCategoryMap[materialCategory] = [resourceName]; - break; - } - } - log.info(`resourceCategoryMap: ${JSON.stringify(resourceCategoryMap, null, 2)}`); - - if (noRecord) { - if (currentMaterialName !== resourceName) { - currentMaterialName = resourceName; - materialAccumulatedDifferences[resourceName] = {}; - log.info(`noRecord模式:材料名变更为【${resourceName}】`); - } - - const startTime = new Date().toLocaleString(); - const initialPosition = genshin.getPositionFromMap(); - await pathingScript.runFile(pathingFilePath); - const finalPosition = genshin.getPositionFromMap(); - const finalCumulativeDistance = calculateDistance(initialPosition, finalPosition); - const endTime = new Date().toLocaleString(); - const runTime = (new Date(endTime) - new Date(startTime)) / 1000; - - const noRecordContent = `路径名: ${pathName}\n开始时间: ${startTime}\n结束时间: ${endTime}\n运行时间: ${runTime}秒\n数量变化: noRecord模式忽略\n\n`; - writeContentToFile(`${noRecordDir}/${resourceName}.txt`, noRecordContent); - log.info(`noRecord模式:已记录至 ${noRecordDir}/${resourceName}.txt`); - } else { - if (currentMaterialName !== resourceName) { - if (currentMaterialName && materialAccumulatedDifferences[currentMaterialName]) { - const prevDiffs = materialAccumulatedDifferences[currentMaterialName]; - log.info(`材料[${currentMaterialName}]收集完成,累积差值:${JSON.stringify(prevDiffs, null, 2)}`); - const prevMsg = `材料[${currentMaterialName}]收集完成,累计获取:${JSON.stringify(prevDiffs, null, 2)}`; - sendNotificationInChunks(prevMsg, notification.Send); - } - currentMaterialName = resourceName; - const updatedLowCountMaterials = await MaterialPath(resourceCategoryMap); - flattenedLowCountMaterials = updatedLowCountMaterials - .flat() - .sort((a, b) => parseInt(a.count, 10) - parseInt(b.count, 10)); - materialAccumulatedDifferences[resourceName] = {}; - } - - const startTime = new Date().toLocaleString(); - const initialPosition = genshin.getPositionFromMap(); - await pathingScript.runFile(pathingFilePath); - const finalPosition = genshin.getPositionFromMap(); - const finalCumulativeDistance = calculateDistance(initialPosition, finalPosition); - const endTime = new Date().toLocaleString(); - const runTime = (new Date(endTime) - new Date(startTime)) / 1000; - - const updatedLowCountMaterials = await MaterialPath(resourceCategoryMap); - const flattenedUpdatedMaterialCounts = updatedLowCountMaterials - .flat() - .sort((a, b) => parseInt(a.count, 10) - parseInt(b.count, 10)); - - const materialCountDifferences = {}; - flattenedUpdatedMaterialCounts.forEach(updatedMaterial => { - const originalMaterial = flattenedLowCountMaterials.find(material => material.name === updatedMaterial.name); - if (originalMaterial) { - const originalCount = parseInt(originalMaterial.count, 10); - const updatedCount = parseInt(updatedMaterial.count, 10); - const difference = updatedCount - originalCount; - if (difference !== 0 || updatedMaterial.name === resourceName) { - materialCountDifferences[updatedMaterial.name] = difference; - if (globalAccumulatedDifferences[updatedMaterial.name]) { - globalAccumulatedDifferences[updatedMaterial.name] += difference; - } else { - globalAccumulatedDifferences[updatedMaterial.name] = difference; - } - if (materialAccumulatedDifferences[resourceName][updatedMaterial.name]) { - materialAccumulatedDifferences[resourceName][updatedMaterial.name] += difference; - } else { - materialAccumulatedDifferences[resourceName][updatedMaterial.name] = difference; - } - } - } - }); - - flattenedLowCountMaterials = flattenedLowCountMaterials.map(material => { - const updatedMaterial = flattenedUpdatedMaterialCounts.find(updated => updated.name === material.name); - if (updatedMaterial) { - return { ...material, count: updatedMaterial.count }; - } - return material; - }); - - log.info(`数量变化: ${JSON.stringify(materialCountDifferences, null, 2)}`); - // 调用时传递recordDir - recordRunTime(resourceName, pathName, startTime, endTime, runTime, recordDir, materialCountDifferences, finalCumulativeDistance); - } - - categoryFound = true; - break; - } else { - log.info(`路径文件 ${pathName} 未能执行`); - } - } - } - if (categoryFound) break; - } - await sleep(1); // 夹断 - } - - // noRecord=false时保留原有全局通知(true时跳过,末次统一通知) - if (!noRecord && Object.keys(globalAccumulatedDifferences).length > 0) { - log.info(`所有材料收集完成,全局累积差值:${JSON.stringify(globalAccumulatedDifferences, null, 2)}`); - // -------------------------- 替换2:所有材料完成通知(分段发送)-------------------------- - let allDoneMsg = "所有材料收集完成,累计获取:\n"; - for (const [name, diff] of Object.entries(globalAccumulatedDifferences)) { - allDoneMsg += ` ${name}: ${diff}个\n`; - } - sendNotificationInChunks(allDoneMsg, notification.Send); - } - - // noRecord=true时执行末次扫描和总汇总 - if (noRecord) { - log.info(`\nnoRecord模式:开始末次背包扫描,计算总差值`); - // 末次扫描获取最终数量 - const finalMaterialCounts = await MaterialPath(materialCategoryMap); - const flattenedFinal = finalMaterialCounts.flat(); - const finalCounts = flattenedFinal.reduce((acc, mat) => { - acc[mat.name] = parseInt(mat.count, 10) || 0; - return acc; - }, {}); - - // 计算总差值(仅保留增加的材料) - const totalDifferences = {}; - Object.keys(initialMaterialCounts).forEach(name => { - const diff = finalCounts[name] - initialMaterialCounts[name]; - if (diff > 0) { - totalDifferences[name] = diff; - } - }); - - // 输出总结果 - const endTime = new Date().toLocaleString(); - const totalRunTime = (new Date(endTime) - new Date(firstScanTime)) / 1000; - log.info(`\nnoRecord模式:所有材料收集完成`); - log.info(`首次扫描时间:${firstScanTime}`); - log.info(`末次扫描时间:${endTime}`); - log.info(`总耗时:${totalRunTime.toFixed(1)}秒`); - log.info(`总累积获取:${JSON.stringify(totalDifferences, null, 2)}`); - - // 记录最终汇总 - recordFinalSummary(recordDir, firstScanTime, endTime, totalRunTime, totalDifferences); - - // 发送最终通知(分段发送) - // -------------------------- 替换3:noRecord模式最终通知(分段发送)-------------------------- - let finalMsg = `材料收集完成(noRecord模式)\n总耗时:${totalRunTime.toFixed(1)}秒\n累计获取:\n`; - for (const [n, d] of Object.entries(totalDifferences)) { - finalMsg += ` ${n}: ${d}个\n`; - } - sendNotificationInChunks(finalMsg, notification.Send); - } - await sleep(1); // 夹断 - } catch (error) { - log.error(`操作失败: ${error}`); - } + // 输出汇总信息 + const endTime = new Date().toLocaleString(); + const totalRunTime = (new Date(endTime) - new Date(firstScanTime)) / 1000; + log.info(`${CONSTANTS.LOG_MODULES.MAIN}\n所有目标收集完成`); + log.info(`${CONSTANTS.LOG_MODULES.MAIN}首次扫描时间:${firstScanTime}`); + log.info(`${CONSTANTS.LOG_MODULES.MAIN}末次扫描时间:${endTime}`); + log.info(`${CONSTANTS.LOG_MODULES.MAIN}总耗时:${totalRunTime.toFixed(1)}秒`); + log.info(`${CONSTANTS.LOG_MODULES.MAIN}总累积获取:${JSON.stringify(totalDifferences, null, 2)}`); + + recordFinalSummary(CONSTANTS.RECORD_DIR, firstScanTime, endTime, totalRunTime, totalDifferences); + + let finalMsg = `收集完成\n总耗时:${totalRunTime.toFixed(1)}秒\n累计获取:\n`; + Object.entries(totalDifferences).forEach(([n, d]) => { + if (n.includes('_EXP')) { + finalMsg += ` ${n.replace('_EXP', '')}(EXP): ${d}\n`; + } else { + finalMsg += ` ${n}: ${d}个\n`; + } + }); + sendNotificationInChunks(finalMsg, notification.Send); + + })(); + + // 并行任务:图像点击 + const imageTask = imageClickBackgroundTask(); + + // 执行所有任务 + try { + await Promise.allSettled([ocrTask, pathTask, imageTask]); + } catch (error) { + log.error(`${CONSTANTS.LOG_MODULES.MAIN}执行任务时发生错误:${error.message}`); + state.cancelRequested = true; + } finally { + log.info(`${CONSTANTS.LOG_MODULES.MAIN}执行结束或取消后的清理操作...`); + state.cancelRequested = true; + } })(); - -// 辅助函数:计算两点之间的距离 -function calculateDistance(initialPosition, finalPosition) { - const deltaX = finalPosition.X - initialPosition.X; - const deltaY = finalPosition.Y - initialPosition.Y; - return Math.sqrt(deltaX * deltaX + deltaY * deltaY); -} -// 修改后的位移监测函数 -async function monitorDisplacement(monitoring, resolve) { - // 获取对象的实际初始位置 - let lastPosition = genshin.getPositionFromMap(); - let cumulativeDistance = 0; // 初始化累计位移量 - let lastUpdateTime = Date.now(); // 记录上一次位置更新的时间 - - while (monitoring) { - const currentPosition = genshin.getPositionFromMap(); // 获取当前位置 - const currentTime = Date.now(); // 获取当前时间 - - // 计算位移量 - const deltaX = currentPosition.X - lastPosition.X; - const deltaY = currentPosition.Y - lastPosition.Y; - let distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); - - // 如果位移量小于0.5,则视为0 - if (distance < 0.5) { - distance = 0; - } - - // 如果有位移,更新累计位移量和最后更新时间 - if (distance > 0) { - cumulativeDistance += distance; // 累计位移量 - lastUpdateTime = currentTime; // 更新最后更新时间 - } - - // 检测是否超过5秒没有位移 - if (currentTime - lastUpdateTime >= 5000) { - // 触发跳跃 - keyPress(VK_SPACE); - lastUpdateTime = currentTime; // 重置最后更新时间 - } - - // 输出位移信息和累计位移量 - log.info(`时间:${(currentTime - lastUpdateTime) / 1000}秒,位移信息: X=${currentPosition.X}, Y=${currentPosition.Y}, 当前位移量=${distance.toFixed(2)}, 累计位移量=${cumulativeDistance.toFixed(2)}`); - - // 更新最后位置 - lastPosition = currentPosition; - - // 等待1秒再次检查 - await sleep(1000); - } - - // 当监测结束时,返回累计位移量 - resolve(cumulativeDistance); -} - -// 识图点击主逻辑 - -async function imageClick(cachedFrame = null) { - - // 定义包含多个文件夹的根目录 - const rootDir = "assets/imageClick"; - - // 获取根目录下的所有子目录路径,深度为 1 - const subDirs = readAllFilePaths(rootDir, 0, 0, [], true); - - // 遍历子目录 - for (const subDir of subDirs) { - - // 从 subDir 中找到 icon 和 Picture 文件夹 - const entries = readAllFilePaths(subDir, 0, 1, [], true); // 获取当前子目录下的所有条目 - - // 筛选出 icon 和 Picture 文件夹 - const iconDir = entries.find(entry => entry.endsWith('\icon')); - const pictureDir = entries.find(entry => entry.endsWith('\Picture')); - - if (!iconDir) { - continue; - } - - if (!pictureDir) { - continue; - } - - // 读取 icon 文件夹下的所有文件路径 - const iconFilePaths = readAllFilePaths(iconDir, 0, 0, ['.png', '.jpg', '.jpeg']); - // 读取 Picture 文件夹下的所有文件路径 - const pictureFilePaths = readAllFilePaths(pictureDir, 0, 0, ['.png', '.jpg', '.jpeg']); - - // 创建图标的 RecognitionObject - const iconRecognitionObjects = []; - for (const filePath of iconFilePaths) { - const mat = file.readImageMatSync(filePath); - if (mat.empty()) { - log.error(`加载图标失败:${filePath}`); - continue; // 跳过当前文件 - } - const recognitionObject = RecognitionObject.TemplateMatch(mat, 0, 0, 1920, 1080); - iconRecognitionObjects.push({ name: basename(filePath), ro: recognitionObject }); - } - - // 创建图库的 ImageRegion,以获取图标的X,Y,W,H - const pictureRegions = []; - for (const filePath of pictureFilePaths) { - const mat = file.readImageMatSync(filePath); - if (mat.empty()) { - log.error(`加载图库失败:${filePath}`); - continue; // 跳过当前文件 - } - pictureRegions.push({ name: basename(filePath), region: new ImageRegion(mat, 0, 0) }); - } - - // 在每张图片中查找图标的位置信息 - const foundRegions = []; - for (const picture of pictureRegions) { - for (const icon of iconRecognitionObjects) { - const foundRegion = picture.region.find(icon.ro); - if (foundRegion.isExist()) { - foundRegions.push({ - pictureName: picture.name, - iconName: icon.name, - region: foundRegion - }); - } - } - } - - // 在屏幕上查找并点击图标 - const ra = cachedFrame || captureGameRegion(); - for (const foundRegion of foundRegions) { - const tolerance = 1; // 容错区间 - const iconMat = file.readImageMatSync(`${iconDir}/${foundRegion.iconName}`); - const recognitionObject = RecognitionObject.TemplateMatch(iconMat, foundRegion.region.x - tolerance, foundRegion.region.y - tolerance, foundRegion.region.width + 2 * tolerance, foundRegion.region.height + 2 * tolerance); - recognitionObject.threshold = 0.9; // 设置识别阈值为 0.9 - const result = ra.find(recognitionObject); - if (result.isExist()) { - const x = Math.round(foundRegion.region.x + foundRegion.region.width / 2); - const y = Math.round(foundRegion.region.y + foundRegion.region.height / 2); - log.info(`即将点击图标:${foundRegion.iconName},位置: (${x}, ${y})`); - await click(x, y); // 假设 click 是一个可用的点击函数 - log.info(`点击 ${foundRegion.iconName}成功,位置: (${x}, ${y})`); - await sleep(500); // 等待一段时间 - } else { - // log.info(`无过期材料弹窗:${foundRegion.iconName},正常跳过`); - } - } - } -} diff --git a/repo/js/背包材料统计/manifest.json b/repo/js/背包材料统计/manifest.json index aeac173ba..b88f83ec0 100644 --- a/repo/js/背包材料统计/manifest.json +++ b/repo/js/背包材料统计/manifest.json @@ -1,15 +1,15 @@ { "manifest_version": 1, - "name": "背包统计采集系统", - "version": "2.42", + "name": "背包统计采集", + "version": "2.5", "bgi_version": "0.44.8", "description": "模板匹配材料,OCR识别数量;\n支持背包材料的数量统计+路径CD管理,自动优选路径;\n具体支持看材料CD文件,可自行增减材料CD。", "saved_files": [ - "history_record/*.txt", - "overwrite_record/*.txt", + "pathing/", + "history_record/", + "overwrite_record/", "latest_record.txt", - "pathing_record/*.txt", - "pathing_record/noRecord/*.txt" + "pathing_record/" ], "authors": [ { diff --git a/repo/js/背包材料统计/materialsCD/怪物.txt b/repo/js/背包材料统计/materialsCD/怪物.txt new file mode 100644 index 000000000..a25c9562d --- /dev/null +++ b/repo/js/背包材料统计/materialsCD/怪物.txt @@ -0,0 +1,2 @@ + +4点:丘丘人,丘丘萨满,丘丘人射手,丘丘暴徒,丘丘王,丘丘游侠,愚人众先遣队,萤术士,债务处理人,冬国仕女,愚人众风役人,愚人众特辖队,盗宝团,野伏众,海乱鬼,镀金旅团,黑蛇众,黯色空壳,部族龙形武士,遗迹机械,元能构装体,遗迹机兵,遗迹龙兽,发条机关,秘源机兵,巡陆艇,史莱姆,骗骗花,飘浮灵,蕈兽,浊水幻灵,原海异种,隙境原体,魔像禁卫,大灵显化身,熔岩游像,龙蜥,圣骸兽,玄文兽,纳塔龙众,炉壳山鼬,蕴光异兽,深渊法师,深渊使徒,兽境群狼,深邃拟覆叶,荒野狂猎,霜夜灵嗣,地脉花, diff --git a/repo/js/背包材料统计/materialsCD/掉落CD.txt b/repo/js/背包材料统计/materialsCD/掉落.txt similarity index 100% rename from repo/js/背包材料统计/materialsCD/掉落CD.txt rename to repo/js/背包材料统计/materialsCD/掉落.txt diff --git a/repo/js/背包材料统计/materialsCD/狗粮.txt b/repo/js/背包材料统计/materialsCD/狗粮.txt new file mode 100644 index 000000000..d020921b9 --- /dev/null +++ b/repo/js/背包材料统计/materialsCD/狗粮.txt @@ -0,0 +1,3 @@ +12小时:12h狗粮, + +24小时:24h狗粮, diff --git a/repo/js/背包材料统计/materialsCD/资源CD.txt b/repo/js/背包材料统计/materialsCD/采集.txt similarity index 68% rename from repo/js/背包材料统计/materialsCD/资源CD.txt rename to repo/js/背包材料统计/materialsCD/采集.txt index e9e292064..64d592032 100644 --- a/repo/js/背包材料统计/materialsCD/资源CD.txt +++ b/repo/js/背包材料统计/materialsCD/采集.txt @@ -1,20 +1,20 @@ -12小时:晶蝶,野猪,松鼠,狐狸,鼬,鸟,鱼,鸭子,路边闪光点,兽肉,禽肉,神秘的肉,鱼肉,鳗肉,螃蟹,青蛙,发光髓,蜥蜴尾巴,晶核,鳅鳅宝玉,吉光虫,燃素蜜虫,固晶甲虫,月萤虫, +12小时:12h狗粮,晶蝶,野猪,松鼠,狐狸,鼬,鸟,鱼,鸭子,路边闪光点,兽肉,禽肉,神秘的肉,鱼肉,鳗肉,螃蟹,青蛙,发光髓,蜥蜴尾巴,晶核,鳅鳅宝玉,吉光虫,燃素蜜虫,固晶甲虫,月萤虫, -24小时:沉玉仙茗,狗粮, +24小时:24h狗粮,沉玉仙茗, -46小时:小灯草,嘟嘟莲,落落莓,塞西莉亚花,慕风蘑菇,蒲公英籽,钩钩果,风车菊,霓裳花,清心,琉璃袋,琉璃百合,夜泊石,绝云椒椒,星螺,石珀,清水玉,海灵芝,鬼兜虫,绯樱绣球,鸣草,珊瑚真珠,晶化骨髓,血斛,天云草实,幽灯蕈,沙脂蛹,月莲,帕蒂沙兰,树王圣体菇,圣金虫,万相石,悼灵花,劫波莲,赤念果,苍晶螺,海露花,柔灯铃,子探测单元,湖光铃兰,幽光星星,虹彩蔷薇,初露之源,浪沫羽鳃,灼灼彩菊,肉龙掌,青蜜莓,枯叶紫英,微光角菌,云岩裂叶,琉鳞石,奇异的「牙齿」,冰雾花花朵,烈焰花花蕊,便携轴承,霜盏花,月落银, +46小时:小灯草,嘟嘟莲,落落莓,塞西莉亚花,慕风蘑菇,蒲公英籽,钩钩果,风车菊,霓裳花,清心,琉璃袋,琉璃百合,夜泊石,绝云椒椒,星螺,石珀,清水玉,海灵芝,鬼兜虫,绯樱绣球,鸣草,珊瑚真珠,晶化骨髓,血斛,天云草实,幽灯蕈,沙脂蛹,月莲,帕蒂沙兰,树王圣体菇,圣金虫,万相石,悼灵花,劫波莲,赤念果,苍晶螺,海露花,柔灯铃,子探测单元,湖光铃兰,幽光星星,虹彩蔷薇,初露之源,浪沫羽鳃,灼灼彩菊,肉龙掌,青蜜莓,枯叶紫英,微光角菌,云岩裂叶,琉鳞石,奇异的「牙齿」,冰雾花花朵,烈焰花花蕊,便携轴承,霜盏花,月落银 72小时: -1次0点:铁块,甜甜花,胡萝卜,蘑菇,松茸,松果,金鱼草,莲蓬,薄荷,鸟蛋,马尾,树莓,白萝卜,苹果,日落果,竹笋,海草,堇瓜,星蕈,墩墩桃,须弥蔷薇,香辛果,枣椰,泡泡桔,汐藻,茉洁草,久雨莲,颗粒果,烛伞蘑菇,澄晶实,红果果菇,苦种,烬芯花,夏槲果,白灵果,寒涌石,宿影花, +1次0点:铁块,甜甜花,胡萝卜,蘑菇,松茸,松果,金鱼草,莲蓬,薄荷,鸟蛋,树莓,白萝卜,苹果,马尾,日落果,竹笋,海草,堇瓜,星蕈,墩墩桃,须弥蔷薇,香辛果,枣椰,泡泡桔,汐藻,茉洁草,久雨莲,颗粒果,烛伞蘑菇,澄晶实,红果果菇,苦种,烬芯花,夏槲果,白灵果,寒涌石,宿影花, -2次0点:白铁块,星银矿石, +2次0点:白铁块,星银矿石,电气水晶, 3次0点:水晶块,紫晶块,萃凝晶,魔晶块,钓鱼点,虹滴晶, -4点:盐,胡椒,洋葱,牛奶,番茄,卷心菜,土豆,小麦,稻米,虾仁,豆腐,杏仁,发酵果实汁,咖啡豆,秃秃豆,面粉,奶油,熏禽肉,黄油,火腿,糖,香辛料,蟹黄,果酱,奶酪,培根,香肠,「冷鲜肉」,黑麦,黑麦粉,酸奶油, +4点:盐,胡椒,洋葱,牛奶,番茄,卷心菜,土豆,小麦,稻米,虾仁,豆腐,杏仁,发酵果实汁,咖啡豆,秃秃豆,面粉,奶油,熏禽肉,黄油,火腿,糖,香辛料,蟹黄,果酱,奶酪,培根,香肠,「冷鲜肉」,黑麦,黑麦粉,酸奶油,冷鲜肉, 6点: -即时刷新:蝴蝶,萤火虫,蝴蝶的翅膀,发光髓 +即时刷新:蝴蝶,萤火虫,蝴蝶的翅膀, diff --git a/repo/js/背包材料统计/settings.json b/repo/js/背包材料统计/settings.json index abfa56b39..157b9faa5 100644 --- a/repo/js/背包材料统计/settings.json +++ b/repo/js/背包材料统计/settings.json @@ -2,7 +2,7 @@ { "name": "TargetCount", "type": "input-text", - "label": "js目录下默认扫描的文件结构:\n./📁BetterGI/📁User/📁JsScript/\n📁背包材料统计/\n 📁pathing/\n 📁 薄荷/\n 📄 薄荷1.json\n 📁 薄荷效率/\n 📄 薄荷-吉吉喵.json\n 📁 名刀镡/\n 📄 旅行者的名刀镡.json\n----------------------------------\n目标数量,默认5000\n给📁pathing下材料设定的目标数" + "label": "js目录下默认扫描的文件结构:\n./📁BetterGI/📁User/📁JsScript/\n📁背包材料统计/\n 📁pathing/\n 📁 薄荷/\n 📄 薄荷1.json\n 📁 薄荷效率/\n 📄 薄荷-吉吉喵.json\n 📁 苹果/\n 📄 旅行者的果园.json\n----------------------------------\n目标数量,默认5000\n给📁pathing下材料设定的目标数" }, { "name": "TargetresourceName", @@ -22,12 +22,12 @@ { "name": "noRecord", "type": "checkbox", - "label": "----------------------------------\n取消扫描数量。默认:否\n勾选将不进行单路径的扫描,但保留时间记录\n(推荐路径记录炼成后启用)" + "label": "----------------------------------\n取消扫描数量。默认:否\n勾选将不进行单路径的扫描,但保留时间记录\n(推荐路径记录炼成后启用)" }, { "name": "Pathing", "type": "select", - "label": "====================\n扫描📁pathing下的\n或勾选【材料分类】的材料。默认:兼并", + "label": "====================\n扫描📁pathing下的\n或勾选【材料分类】的材料。默认:仅📁pathing材料", "options": [ "1.兼并:📁pathing材料+【材料分类】", "2.仅📁pathing材料", @@ -37,7 +37,7 @@ { "name": "Smithing", "type": "checkbox", - "label": "刷怪、采集推荐:2.仅📁pathing材料\n----------------------------------\n【锻造素材】" + "label": "\n----------------------------------\n【锻造素材】" }, { "name": "Drops", @@ -72,7 +72,7 @@ { "name": "CharAscension", "type": "checkbox", - "label": "----------------------------------\n\n【角色突破素材】" + "label": "----------------------------------\n\n【角色培养素材】世界BOSS树脂材料" }, { "name": "Fishing", @@ -87,7 +87,7 @@ { "name": "Talent", "type": "checkbox", - "label": "----------------------------------\n\n【角色天赋素材】" + "label": "----------------------------------\n\n【角色天赋素材】天赋书" }, { "name": "WeaponAscension", @@ -95,9 +95,18 @@ "label": "----------------------------------\n\n【武器突破素材】" }, { - "name": "ImageDelay", + "name": "PopupNames", "type": "input-text", - "label": "数字太小可能无法识别,用?代替\n====================\n识图延迟时间(默认:10 毫秒)" + "label": "数字太小可能无法识别,用?代替\n====================\n弹窗名(默认:全部)" + }, + { + "name": "PopupClickDelay", + "type": "input-text", + "label": "如 过期物品,信件,自定义文件夹名。注意分隔符和文件夹格式\n----------------------------------\n弹窗循环间隔(默认:5 秒)" + }, + { + "name": "CDCategories", + "type": "input-text", + "label": "----------------------------------\n\n采用的CD分类(默认:全部)" } -] - +] \ No newline at end of file diff --git a/repo/js/背包材料统计/targetText/交互.txt b/repo/js/背包材料统计/targetText/交互.txt new file mode 100644 index 000000000..23bf1f28b --- /dev/null +++ b/repo/js/背包材料统计/targetText/交互.txt @@ -0,0 +1,3 @@ +狗粮:冒险家头带,冒险家金杯,冒险家怀表,冒险家尾羽,冒险家之花,幸运儿银冠,幸运儿之杯,幸运儿沙漏,幸运儿鹰羽,幸运儿绿花,游医的方巾,游医的药壶,游医的怀钟,游医的枭羽,游医的银莲,感别之冠,异国之盏,逐光之石,归乡之羽,故人之心,奇迹耳坠,奇迹之杯,奇迹之沙,奇迹之羽,奇迹之花,战狂的鬼面,战狂的骨杯,战狂的时计,战狂的翎羽,战狂的蔷薇,教官的帽子,教官的茶杯,教官的怀表,教官的羽饰,教官的胸花,流放者头冠,流放者之杯,流放者怀表,流放者之羽,流放者之花,守护束带,守护之皿,守护座钟,守护徽印,守护之花,勇士的勋章,勇士的期许,勇士的坚毅,勇士的壮行,勇士的冠冕,武人的红花,武人的羽饰,武人的水漏,武人的酒杯,武人的头巾,赌徒的胸花,赌徒的羽饰,赌徒的怀表,赌徒的骰蛊,赌徒的耳环,学士的书签,学士的羽笔,学士的时钟,学士的墨杯,学士的镜片,祭雷礼冠,祭火礼冠,祭水礼冠,祭冰礼冠, + +交互:触摸,调查,拾取, diff --git a/repo/js/背包材料统计/targetText/宝箱.txt b/repo/js/背包材料统计/targetText/宝箱.txt new file mode 100644 index 000000000..4b7c31a58 --- /dev/null +++ b/repo/js/背包材料统计/targetText/宝箱.txt @@ -0,0 +1,4 @@ +武器:无锋剑,银剑,旅行剑,吃虎鱼刀,训练大剑,佣兵重剑,飞天大御剑,白铁大剑,新手长枪,铁尖枪,钺矛,白缨枪,猎弓,历练的猎弓,信使,反曲弓,学徒笔记,口袋魔导书,甲级宝珏,异世界行记, + +养成:精锻用,的教导,的经验 + diff --git a/repo/js/背包材料统计/targetText/掉落.txt b/repo/js/背包材料统计/targetText/掉落.txt new file mode 100644 index 000000000..21cde2b08 --- /dev/null +++ b/repo/js/背包材料统计/targetText/掉落.txt @@ -0,0 +1,2 @@ +掉落:偏光棱镜,地脉的旧枝,大英雄的经验,特工祭刀,冒险家的经验,水晶棱镜,混沌炉心,猎兵祭刀,流浪者的经验,混沌回路,石化的骨片,黯淡棱镜,混沌装置,结实的骨片,隐兽鬼爪,黑晶号角,脆弱的骨片,隐兽利爪,雾虚灯芯,黑铜号角,沉重号角,混沌真眼,隐兽指爪,雾虚草囊,地脉的新芽,幽邃刻像,混沌枢纽,雾虚花粉,地脉的枯叶,夤夜刻像,混沌机关,督察长祭刀,初生的浊水幻灵,晦暗刻像,混浊棱晶,老旧的役人怀表,浊水的一掬,渊光鳍翅,破缺棱晶,茁壮菌核,休眠菌核,月色鳍翅,浊水的一滴,锲纹的横脊,失活菌核,密固的横脊,异界生命核,羽状鳍翅,外世突触,未熄的剑柄,残毁的横脊,混沌锚栓,混沌模块,漫游者的盛放之花,裂断的剑柄,隙间之核,何人所珍藏之花,役人的时时刻刻,残毁的剑柄,混沌容器,役人的制式怀表,意志巡游的符像,来自何处的待放之花,辉光棱晶,史莱姆凝液,意志明晰的寄偶,繁光躯外骸,迷光的蜷叶之心,不祥的面具,惑光的阔叶,意志破碎的残片,稀光遗骼,失光块骨,折光的胚芽,污秽的面具,聚燃的游像眼,幽雾兜盔,破损的面具,聚燃的命种,明燃的棱状壳,蕴热的背壳,冷裂壳块,幽雾片甲,禁咒绘卷,聚燃的石块,封魔绘卷,幽雾化形,秘源真芯,霜夜的煌荣,史莱姆原浆,导能绘卷,秘源机鞘,霜夜的柔辉,历战的箭簇,史莱姆清,秘源轴,霜夜的残照,原素花蜜,异海之块,浮游干核,锐利的箭簇,孢囊晶尘,异海凝珠,微光花蜜,牢固的箭簇,奇械机芯齿轮,尉官的徽记,荧光孢粉,骗骗花蜜,名刀镡,士官的徽记,机关正齿轮,蕈兽孢子,啮合齿轮,影打刀镡,新兵的徽记,织金红绸,攫金鸦印,横行霸者的利齿,破旧的刀镡,镶边红绸,浮游晶化核,老练的坚齿,藏银鸦印,褪色红绸,寻宝鸦印,异色结晶石,浮游幽核,稚嫩的尖齿,磨损的执凭,龙冠武士的金哨,战士的铁哨,卫从的木哨,精制机轴,加固机轴,毁损机轴,霜镌的执凭,精致的执凭 + diff --git a/repo/js/背包材料统计/targetText/采集.txt b/repo/js/背包材料统计/targetText/采集.txt new file mode 100644 index 000000000..a831233eb --- /dev/null +++ b/repo/js/背包材料统计/targetText/采集.txt @@ -0,0 +1,10 @@ +小动物:蜜虫,甲虫,陆鳗,海鳗,沙鳗,青蛙,泥蛙,蓝蛙,树蛙,金虫,角蜥,髓蜥,尾蜥,蝴蝶,鳅鳅,晶蝶,金蟹,太阳蟹,海蓝蟹,将军蟹,薄红蟹,光虫,子探测,黑背鲈,蓝鳍鲈,黄金鲈,火虫,月萤虫, + +素材:塞西莉亚花,霓裳花,夜泊石,落落莓,绝云椒椒,钩钩果,蒲公英籽,嘟嘟莲,小灯草,慕风蘑菇,风车菊,初露之源,树王圣体菇,绯樱绣球,万相石,幽灯蕈,鬼兜虫,天云草实,悼灵花,清水玉,湖光铃兰,幽光星星,沙脂蛹,珊瑚真珠,石珀,海灵芝,琉璃袋,虹彩蔷薇,赤念果,劫波莲,星螺,柔灯铃,鸣草,月莲,海露花,清心,血斛,帕蒂沙兰,晶化骨髓,琉璃百合,苍晶螺,琉鳞石,蜥蜴尾巴,云岩裂叶,枯叶紫英,肉龙掌,烈焰花,冰雾花,青蜜莓,灼灼彩菊,浪沫羽鳃,奇异的「牙齿」,便携轴承,霜盏花,月落银,芯花,微光角菌,苦种,久雨莲,毗波耶,马尾,寒涌石,宿影花,电气水晶 + +食材:苹果,日落果,星蕈,泡泡桔,烛伞蘑菇,胡萝卜,白萝卜,金鱼草,薄荷,小麦,卷心菜,松果,蘑菇,甜甜花,莲蓬,树莓,海草,稻米,兽肉,堇瓜,冷鲜肉,番茄,香辛果,鸟蛋,土豆,墩墩桃,松茸,禽肉,须弥蔷薇,鱼肉,枣椰,神秘的肉,茉洁草,汐藻,洋葱,竹笋,沉玉仙茗,颗粒果,澄晶实,红果,黑麦,黑麦粉,酸奶油,夏槲果,白灵果, + +矿石:萃凝晶,紫晶块,星银矿石,魔晶块,水晶块,白铁块,铁块,虹滴晶, + +狗粮:冒险家头带,冒险家金杯,冒险家怀表,冒险家尾羽,冒险家之花,幸运儿银冠,幸运儿之杯,幸运儿沙漏,幸运儿鹰羽,幸运儿绿花,游医的方巾,游医的药壶,游医的怀钟,游医的枭羽,游医的银莲,感别之冠,异国之盏,逐光之石,归乡之羽,故人之心,奇迹耳坠,奇迹之杯,奇迹之沙,奇迹之羽,奇迹之花,战狂的鬼面,战狂的骨杯,战狂的时计,战狂的翎羽,战狂的蔷薇,教官的帽子,教官的茶杯,教官的怀表,教官的羽饰,教官的胸花,流放者头冠,流放者之杯,流放者怀表,流放者之羽,流放者之花,守护束带,守护之皿,守护座钟,守护徽印,守护之花,勇士的勋章,勇士的期许,勇士的坚毅,勇士的壮行,勇士的冠冕,武人的红花,武人的羽饰,武人的水漏,武人的酒杯,武人的头巾,赌徒的胸花,赌徒的羽饰,赌徒的怀表,赌徒的骰蛊,赌徒的耳环,学士的书签,学士的羽笔,学士的时钟,学士的墨杯,学士的镜片,祭雷礼冠,祭火礼冠,祭水礼冠,祭冰礼冠, +