MapleCalc 是一个用 React + Vite 做的冒险岛计算工具集。出发点并不宏大:游戏里有些事情要查表,有些事情要肉眼比较图片,还有些事情要盯着视频帧里的文字发呆。我想把这些“每次都要重新做一遍”的小麻烦装进浏览器,于是 MapleCalc 慢慢长成了一个个人工具实验台。
它里面有完美核心查找、找茬、起源之塔和卖轮等页面。几个工具用到的算法完全不同,却共用同一套前端外壳;这种“一个入口,几种奇怪问题”的组合,反而比单独做一个页面更能体现浏览器端应用的边界。(:з」∠)
为什么坚持做成纯前端
MapleCalc 的输入大多来自用户自己的技能选择、图片或屏幕画面,计算也不需要多人协作。所以它没有后端和数据库,图片、视频帧与 OCR 结果都留在浏览器内存里。部署到静态站点很轻,用户也不用上传自己的截图。
项目用 React、TypeScript、Vite 和 Ant Design,路由是 HashRouter。Hash 路由有点朴素,但特别适合 GitHub Pages:访问 #/home、#/find-diff、#/oz 或 #/fz 时,静态服务器不需要理解前端路由,也不用配回退规则。
左侧菜单和页面路由用同一份配置,打开首页、找茬、爬塔或卖轮工具时不会出现菜单高亮和实际地址对不上的情况。这里没什么惊天动地的架构,但个人工具最重要的就是“打开就能用”——少一点部署魔法,就少一点半夜排查 404 的痛苦。
完美核心:这其实是一个搜索问题
完美核心查找器的输入是角色技能和目标组合,输出是满足条件的核心方案。最开始很容易把它理解成“把所有排列组合生成出来再筛选一下”,但技能一多,浏览器会先陷入沉思,用户再跟着一起沉思。
实现上,技能组合被整理成可搜索节点。递归从候选技能开始构造路径,维护当前已经选过的技能集合;发现技能重复、目标已经不可能满足,或者当前路径走不下去,就立刻回退。找到满足条件的组合后,再通过 setPerfectNodes 把搜索结果反向整理成界面能读懂的方案。
1 | 候选技能节点 |
这里真正值钱的是剪枝,不是递归本身。每一层都知道还缺哪些目标技能,就不必把注定失败的分支走到底。页面也会显示无效技能和“没有找到完美核心”的反馈,让结果不是一张神秘的列表,而是一个有理由的计算结论。
找茬:最朴素的像素也很有用
找茬页面用 Jimp 的浏览器实现读取两张图片,遍历同一坐标上的像素。每个像素拆成 RGBA 通道比较,发现颜色差异后,在结果图上绘制标记区域。
这种方法看起来没有计算机视觉论文里的复杂名词,但对尺寸一致、差异区域明确的截图特别好使。因为计算发生在浏览器里,原图不需要传到服务器;图片越大,内存和遍历时间越明显,所以页面要在输入尺寸、比较精度和结果标记之间做取舍。
像素比较也有它的诚实之处:抗压缩、缩放、亮度变化的能力并不神奇。两张图尺寸不一样,或者截图被重新编码过,结果就可能是一大片差异。工具能把差异圈出来,但它不会假装自己理解了“这是不是有意义的变化”。小工具就该给用户这样的预期。
起源之塔:从屏幕流到答案匹配
oz 页面是一条更完整的浏览器视觉链路。用户通过 navigator.mediaDevices.getDisplayMedia 选择屏幕或视频流,程序把当前帧画到 Canvas 上,从固定的识别区域提取楼层号、地图或题目文字,最后交给 Tesseract.js OCR。
不同楼层的画面结构不一样,所以代码为 22、23、24、36、39、48 等楼层准备了独立的 useFxx 处理器。它们各自知道自己的文字区域、地图模式和答案规则,而不是强迫一个巨大的通用函数处理所有楼层。文件是多了一些,但每个问题都能在对应楼层的模块里找到,调试时反而轻松。
OCR 结果不能直接当答案用。截图里的字体、缩放、压缩和背景都会造成空格、字符或标点误识别,所以程序会把文本和预置题库做 Levenshtein 距离匹配,在有限误差内找最接近的题目或选项。地图识别则把视频帧转成 Jimp 图像,交给楼层模块匹配路线和答案。
识别区域、视频缩放和刷新间隔保存在 localStorage 的 oz_rect_settings 里。用户可以拖动矩形、调整缩放、降低识别频率,下次打开接着用自己的校准结果。不同录制分辨率下固定坐标几乎必挂;把校准交给用户,是一个很实际的“不要和屏幕分辨率硬碰硬”的决定。
OCR 的代价:识别频率不是越高越好
Tesseract.js 用 Worker 执行 OCR,初始化时加载 eng 语言,还可以针对不同楼层设置白名单字符或题目区域。它把计算从主 UI 线程挪开,页面至少不会因为一帧识别而完全卡死,但 OCR 本身仍然比普通文本匹配贵得多。
所以工具允许调整刷新间隔:画面变化很慢时没必要每帧都识别;识别太频繁会吃满 CPU,太慢又可能错过变化。这个参数不只是“性能开关”,而是准确率、响应速度和设备负载之间的选择。做前端工具时,给出这个选择通常比假装存在一个适合所有机器的默认值靠谱。
新增一个楼层处理器,大概会做什么
楼层模块通常要先定义自己的识别矩形,再决定哪些区域交给 OCR、哪些交给图片匹配;之后把 OCR 文本清洗、题库匹配和最终答案整理成统一返回值。这样 useF22、useF23 这些处理器虽然各管各的楼层细节,页面仍然可以用同一套加载和结果展示流程。
矩形设置放 localStorage,解决的是“用户的屏幕比例和我的开发环境不一样”。但本地校准也意味着页面必须考虑权限被拒、用户停止共享、视频轨道结束和 OCR Worker 初始化失败这些情况。浏览器 API 很方便,却不会替你处理生命周期;一个真正能用的工具,应该让失败状态也能被看懂。
完美核心搜索里,剪枝具体剪掉了什么
搜索的状态不只是“当前选了哪些技能”,还包括当前路径、已经满足的目标和仍然可用的候选节点。每次递归进入下一层前都可以问几个问题:当前组合有没有重复技能?剩余节点数量够不够填缺口?某个目标技能是不是已经被放在不可能继续扩展的位置?答案说明这条路注定失败的话,就应该在生成更多子节点之前返回。
这类剪枝对浏览器尤其重要——用户要的是很快拿到一组可读方案,而不是看 JavaScript 在后台慢慢堆完所有排列组合。搜索结果整理成界面数据时,还要把技能顺序、组合关系和失败原因表达出来;只有这样,“没有找到完美核心”才不是一个冷冰冰的空数组。
以后如果要加缓存,可以把已经搜过的“剩余目标 + 候选集合”作为状态键,复用相同子问题的结果。但缓存必须小心技能顺序和组合约束不能被错误合并。现在的实现保持递归逻辑直观,牺牲了一些高级优化,换来更容易验证的状态变化——对个人工具来说,这笔账划算。
找茬为什么不做成“万能图像识别”
Jimp 像素比较的前提是两张图能对齐。程序要先处理尺寸、坐标和 RGBA 通道,再决定颜色差到什么程度才算变化;要是把抗压缩、旋转、缩放和光照变化都加进来,问题就从“比较两张截图”膨胀成一个完整的图像配准系统了。
所以 MapleCalc 选择把边界说清楚:相同场景、相近尺寸、差异区域明确时,逐像素比较简单可靠;输入条件不满足时,结果需要用户自己复核,而不是让工具用一堆隐藏阈值假装理解图片。一个小工具知道自己不会什么,有时比功能列表更重要。
前端结构:算法不同,外壳共用
App.tsx 把 /home、/find-diff、/oz 和 /fz 映射到独立页面,Ant Design 菜单根据同一份路由配置生成。完美核心页面用递归搜索,找茬页面用像素比较,起源之塔用 Canvas、屏幕捕获和 OCR,卖轮页面处理另一类重复计算;它们不需要共享算法,但要共享加载状态、错误提示、导航和参数保存习惯。
这种结构很适合个人项目:新增一个工具时,先专注问题本身,再接入已有的布局和菜单。页面之间的差异不会被一个“万能组件”抹平,公共部分也不会因为某个页面的特殊需求变得难懂。
一点关于“辅助工具”的边界
MapleCalc 主要是把重复计算、图像比较和本地识别做成可解释的小工具。它没有后端,也不需要把用户画面上传到第三方服务;算法和输入都在页面里,用户能看到自己调整了什么、结果为什么变化。
我喜欢这种规模:问题足够具体,结果足够直观,技术又刚好覆盖搜索、图像处理、浏览器权限、OCR 和静态部署。它不替玩家做所有决定,只把规则和计算过程摊开来,让“我觉得应该这样”多一个可以核对的依据。能把小麻烦变小一点,已经很不错了 (`・ω・´)
把隐私和部署成本一起算进去
所有图片、屏幕流和 OCR 处理都在浏览器端完成,意味着用户不必把自己的素材传到服务器,也意味着低性能设备要多承担一些本地计算。这个取舍是透明的:部署简单、没有后端存储风险,但页面要给权限提示、加载状态和合理的刷新频率。
我更愿意让 MapleCalc 保持这种“小而明确”的形状。它不是一个无所不能的自动化平台,而是把几个高频、可解释的问题分别解决好;算法不确定时把结果交还给用户,而不是用更复杂的黑盒把不确定性藏起来。
项目代码见 github.com/Acceyuriko/MapleCalc,完美核心查找器可以直接访问 MapleCalc 在线工具。