From 3823e38b075fe3dc8e06fe10b80218e4683645e3 Mon Sep 17 00:00:00 2001 From: Kitasami_Nya Date: Sat, 24 Oct 2026 01:37:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8F=91=E5=B8=83=E5=92=B8=E9=B1=BC?= =?UTF-8?q?=E5=96=B5=E5=96=B5=E5=85=A8=E8=83=BD=E5=8A=A9=E6=89=8B=EF=BC=8C?= =?UTF-8?q?=E5=8A=A0=E5=85=A5=E5=85=A8=E6=94=B6=E9=9B=86=E5=9C=B0=E5=9B=BE?= =?UTF-8?q?=E4=B8=8E=E5=9B=BE=E6=96=87=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + CHANGELOG.md | 16 + README.md | 122 +++++-- docs/images/encyclopedia.png | Bin 0 -> 83431 bytes docs/images/inventory.png | Bin 0 -> 91898 bytes docs/images/world-map.png | Bin 0 -> 92613 bytes tool/README.md | 68 +++- tool/config.example.json | 6 + tool/launcher.js | 5 +- tool/lib/catalog-position.js | 108 ++++++ tool/lib/combat.js | 43 +++ tool/lib/data.js | 2 +- tool/lib/encyclopedia.js | 94 ++++++ tool/lib/item-icons.js | 31 ++ tool/lib/motion.js | 168 ++++++++++ tool/lib/ops.js | 151 +++++++-- tool/lib/safety.js | 9 + tool/lib/trainer.js | 136 ++++++-- tool/lib/world-map.js | 45 +++ tool/package-release.js | 41 +++ tool/server.js | 38 ++- tool/tests/catalog-position.test.js | 103 ++++++ tool/tests/combat.test.js | 22 ++ tool/tests/encyclopedia-ui.test.js | 49 +++ tool/tests/encyclopedia.test.js | 30 ++ tool/tests/item-icons.test.js | 15 + tool/tests/motion.test.js | 187 +++++++++++ tool/tests/regression.test.js | 498 ++++++++++++++++++++++++++++ tool/tests/release.test.js | 59 ++++ tool/tests/story-cache.test.js | 57 ++++ tool/tests/trainer.test.js | 186 +++++++++++ tool/tests/world-map-ui.test.js | 96 ++++++ tool/tests/world-map.test.js | 27 ++ tool/web/app.js | 393 ++++++++++++++++++++-- tool/web/index.html | 64 +++- tool/web/style.css | 70 ++++ tool/使用须知.md | 92 ++--- tool/发布清单.json | 20 +- tool/开发文档.md | 58 ++++ 39 files changed, 2918 insertions(+), 194 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 docs/images/encyclopedia.png create mode 100644 docs/images/inventory.png create mode 100644 docs/images/world-map.png create mode 100644 tool/config.example.json create mode 100644 tool/lib/catalog-position.js create mode 100644 tool/lib/combat.js create mode 100644 tool/lib/encyclopedia.js create mode 100644 tool/lib/item-icons.js create mode 100644 tool/lib/motion.js create mode 100644 tool/lib/world-map.js create mode 100644 tool/package-release.js create mode 100644 tool/tests/catalog-position.test.js create mode 100644 tool/tests/combat.test.js create mode 100644 tool/tests/encyclopedia-ui.test.js create mode 100644 tool/tests/encyclopedia.test.js create mode 100644 tool/tests/item-icons.test.js create mode 100644 tool/tests/motion.test.js create mode 100644 tool/tests/regression.test.js create mode 100644 tool/tests/release.test.js create mode 100644 tool/tests/story-cache.test.js create mode 100644 tool/tests/trainer.test.js create mode 100644 tool/tests/world-map-ui.test.js create mode 100644 tool/tests/world-map.test.js create mode 100644 tool/开发文档.md diff --git a/.gitignore b/.gitignore index c041bd4..6499bf6 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ Thumbs.db desktop.ini .DS_Store + +# 可重建的发布产物。 +/dist/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..bb4ce6c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +# 变更记录 + +## 2026-10-24 · 发布版功能同步 + +- 悬停 v5:支持空中连续点跳、按下下降、技能主动位移;保留地形碰撞和原版出口换图,支持 1/4/16 像素长按微调。 +- 背包:完整分类、筛选批量操作、装备/卸下、铃铛容量调整;图标在游戏运行时内存生成,批量失败回退并保留至少一件武器。 +- 战斗:玩家与莉莉娅的输出倍率、受击倍率和有效命中秒杀,保留原作免疫与治疗规则。 +- 百科:物品、来源、商店和收集状态分开显示;分块传送先检查角色身体与地形,目标受阻时寻找邻近分块/同图备用位置。 +- 地图:原版分块布局、区域/页选择、缩放拖动、状态筛选、三态图标、明确按钮传送;金币使用双圈猫爪矢量图标。 +- 按发布范围移除快照与 TAS 整页、专属接口、模块及打包依赖;保留顶部实时状态、战斗倍率及常规存档备份/还原。发布版不提供战斗中途记录与回放。 +- 保留发布版运行时全文台词 C 方案:内存缓存、限次重试、断线失效和重连读取。未引入本地剧本、原始地图、素材或私人配置。 +- 增加独立目录配置模板、自动测试、白名单打包脚本和干净发布目录启动测试。旧依赖原始游戏文件的测试改为公开结构索引与人工运行时夹具。 + +当前游戏版本的既有只读验证统计为:48 项背包物品,百科含金币共 49 项、426 条来源、24 处商店,覆盖 411 个官方收集点;地图包含 16 区域、12 页、996 分块。不同游戏版本或运行时尚未就绪时,以面板实际读取结果为准;这些数字不代表发布包附带游戏数据。 + +本轮完整自动测试 72 项通过,另有 JavaScript 语法检查和干净发布目录启动验证。独立面板连接测试游戏,只读确认运行时台词已加载、背包 48 项、地图 996 格及官方收集点 411/411;独立浏览器验证地图缩放/拖动、金币图标、菜单拦截和窄窗口显示。浏览器传送请求全部拦截,没有移动角色、读档或重放真实游戏。既有剧情摆拍并非全部地图均已验收;动态掉落和部分分支奖励不在确定收集索引内。 diff --git a/README.md b/README.md index c8213c2..5c52ad4 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,102 @@ -# 咸鱼喵喵截图&修改辅助工具:简单使用方法 +# 咸鱼喵喵全能助手 -一个在浏览器里操作的本地辅助工具,支持剧情全文搜索、传送、播放对白、角色摆拍,以及角色数值和背包调整,方便回看剧情和截图。 +**修改器 · 全收集地图 · 物品百科 · 剧情回看 · 截图摆拍** -需要 Windows、已安装的《咸鱼喵喵》、[Node.js 22 或更新版本](https://nodejs.org/),以及 [NW.js SDK 0.64.1(Windows x64)](https://dl.nwjs.io/v0.64.1/nwjs-sdk-v0.64.1-win-x64.zip)。发布包不含游戏本体、存档、完整剧本和上述运行环境;无需 npm install。 +想回头截一段剧情、找漏掉的收集物,或者试试不同配饰搭配?这个工具把常用操作放进了浏览器面板:游戏开在一边,查地图、调背包、找台词都在另一边完成。 -1. 将下载解压后、包含 `tool`、`out`、`unpacked` 的整个工具文件夹放进游戏安装目录,与游戏的 `package.json` 同级。外层文件夹名称不限,保留下载后的名字即可,无需另建 `aitemp` 文件夹。可从 Steam 的“管理 → 浏览本地文件”找到游戏目录。 -2. 解压 NW.js SDK,把 `nwjs-sdk-v0.64.1-win-x64` 文件夹放进上述工具文件夹。保留内部目录结构,别多套一层文件夹: +支持 Windows 版《咸鱼喵喵》(Nyaruru)。推荐先用面板创建一份独立游戏副本,在副本里截图和尝试各种玩法。 + +## 看地图,补收集 + +世界地图按原作的房间和通道排列,能看见完整区域布局,也能定位自己现在在哪。拖动平移、滚轮缩放,放大后直接看每个分块里的收集物。 + +- **找漏项**:未收集显示彩色图标,拿过的变灰并带 ✓,暂时无法判断的显示 ?;可以只看未收集,或按物品类型筛选。 +- **查位置**:点一个格子或图标,右侧显示该分块的物品、来源和获取条件。 +- **去附近**:选好分块,再点「传送到此分块」。工具会找附近能容纳角色的位置,目标被挡住时改用备用位置,并显示到达提示。 + +当前版本整理了 **16 个区域、12 个地图页、996 个分块**。地图是房间布局示意图,查看完整地图不会改变游戏自己的探索记录。 + +![世界地图:区域布局、收集图标与分块详情](docs/images/world-map.png) + +收集百科适合反过来查:先搜物品,再看用途、获取位置和收集进度。例如,想找某件配饰,就搜名字、展开来源,再前往对应分块附近。 + +百科整理了 **49 种物品、426 条地图来源和 24 处商店入口**,覆盖当前版本 **411 个地图收集点**。它会分别显示「背包现在有多少」与「地图上是否拿过」,吃掉的食物不会因此变回未收集。 + +![收集百科:物品用途、持有数量与获取地点](docs/images/encyclopedia.png) + +## 改背包,试搭配 + +「角色与背包」页列出 **48 项物品,包括 26 种配饰**,带游戏图标和效果说明。可以按名称搜索、按类别筛选,逐项添加、删除、装备或卸下,也可以批量获得或清除筛选结果。 + +想快速试一套配饰,就筛选「配饰」,找到想要的几件并装备;想补给,则筛选食物后批量获得。配饰容量跟随铃铛数量,面板会显示当前容量。 + +同一页还能调整: + +- **金币、生命(HP)、体力(SP)、能量(ZP)**:直接设值,或分别锁定。 +- **伤害与受击倍率**:1 倍为正常,可分别调整。 +- **一击秒杀**:有效攻击命中时生效,剧情里的无敌阶段仍按游戏规则处理。 + +顶部还有常驻的「无敌」快捷开关,切换页面时也能随手开启或关闭。 + +![角色与背包:物品图标、效果说明与装备操作](docs/images/inventory.png) + +## 回看剧情,慢慢取景 + +「对话剧情」支持按台词、说话人、地图或故事编号搜索。连接游戏后会自动读取全文台词,断开连接后清空,再次连接时重新读取。 + +找到想看的片段后,可以选择: + +| 操作 | 适合什么时候用 | +| --- | --- | +| 前往 | 去剧情发生的位置,自己走过去触发 | +| 只播台词 | 想快速重看某一段对白或截对话框 | +| 运行过场 | 想连同人物站位和动作一起回看;会从所在事件页开头播放 | +| 临时摆拍 | 场景里缺了角色时,让角色临时出现,拍完点「撤销场景还原」 | + +取景时不用一直和跳跃时机较劲: + +- **悬停**:停在空中,每次点跳都能继续上升;按住游戏「下」键下降,松开停住。保留技能位移、地形碰撞和正常出口换图。 +- **穿墙**:需要穿过障碍时单独开启;回到空处后关闭,再选择落地。 +- **像素微调**:按 1、4 或 16 像素调整位置,方向按钮支持长按。 +- **隐藏画面元素**:随时切换对话框、立绘、游戏界面和莉莉娅的显示,让构图更干净。 + +## 第一次使用 + +准备好已安装的游戏,再安装或下载这两个运行环境: + +- [Node.js 22 或更新版本](https://nodejs.org/):安装即可。 +- [NW.js SDK 0.64.1 · Windows x64](https://dl.nwjs.io/v0.64.1/nwjs-sdk-v0.64.1-win-x64.zip):完整解压,稍后放进工具文件夹。 + +工具无需运行 `npm install`。仓库和发布包不包含游戏本体、存档或完整剧本,地图、图标和全文台词会从你的游戏中读取。 + +1. **找到游戏目录。** 在 Steam 中右键游戏 → 管理 → 浏览本地文件。 +2. **放入工具和 SDK。** 将下载解压后包含 `tool`、`out`、`unpacked` 的整个文件夹放进游戏目录,外层名字随意;再把完整的 `nwjs-sdk-v0.64.1-win-x64` 文件夹放到工具文件夹里。目录像这样: ```text - Nyaruru(游戏目录) - ├─package.json - └─Nyaruru_modifier(名字不限) - ├─README.md - ├─.gitignore - ├─nwjs-sdk-v0.64.1-win-x64 ← 另行下载并解压 - │ └─nw.exe - ├─out - │ └─triggers.csv - ├─tool - │ ├─启动.bat - │ ├─停止面板.bat - │ ├─lib - │ └─web - └─unpacked - └─data - ├─System.json - └─MapInfos.json + 游戏目录/ + ├─ package.json + └─ Nyaruru_modifier/ ← 工具文件夹,名字不限 + ├─ README.md + ├─ nwjs-sdk-v0.64.1-win-x64/ + │ ├─ nw.exe + │ └─ …… ← SDK 的其他文件也要保留 + ├─ out/ + ├─ unpacked/ + └─ tool/ + ├─ 启动.bat + └─ 停止面板.bat ``` - (仅列出主要文件;NW.js SDK 文件夹需完整保留。) -3. 先在原游戏中正常保存一次并退出。双击 `tool/启动.bat`,首次选择 **1:只启动面板**,在“存档与沙盒”页创建游戏副本。副本需要额外磁盘空间,默认放在游戏所在盘的 `Nyaruru_shots` 文件夹。 -4. 副本创建完成后,再次双击 `启动.bat`,工具会备份存档并启动副本游戏。在游戏里正常读档进入地图,确认面板顶部连接的是沙盒。 -5. 在面板搜索台词或故事编号,按需要选择“前往”“运行过场”或“只播台词”;摆拍后可点“撤销场景还原”。全文台词在连接游戏后自动读取,断开连接后清空。 +3. **先创建游戏副本。** 在原游戏中正常保存并退出。双击 `tool/启动.bat`,首次选择 **1:只启动面板**,到「存档与沙盒」页创建副本。这里的「沙盒」就是一份单独运行的游戏副本,会额外占用磁盘空间。 +4. **启动副本并读档。** 创建完成后再次双击 `启动.bat`,工具会备份存档并启动副本游戏。在游戏里正常读档进入地图,查看面板顶部的「当前连接」,确认连的是沙盒副本。 +5. **打开想用的页面。** 查漏项用「地图」或「收集百科」,改物品用「角色与背包」,找台词用「对话剧情」。 -面板默认地址为 `http://127.0.0.1:8787`。用完双击 `tool/停止面板.bat`,游戏窗口另行关闭。升级前先退出旧面板和游戏。 +面板默认地址:**http://127.0.0.1:8787**。用完双击 `tool/停止面板.bat`,游戏窗口另外关闭。升级时先退出旧面板和游戏,再替换工具文件。 -建议始终在沙盒里使用:剧情操作可能改变进度,自动存档和手动保存照常生效。“全部还原”会解除临时效果和锁定,但不会回退已经改过的数值、背包或剧情进度;摆拍状态也不保证能通过读档完整恢复。详细说明见包内 `tool/使用须知.md`。 \ No newline at end of file +## 用之前记住这几件事 + +- **在副本里试更省心。** 改过的数值、物品和剧情进度可能随正常存档保留;「全部还原」会解除辅助和锁定,已经改过的内容要靠存档备份恢复。备份和还原都在「存档与沙盒」页。 +- **传送是帮你到附近。** 物品仍需自己取得,剧情条件和商店库存仍跟随游戏进度;地图传送前先退出游戏菜单,回到地图画面。 +- **过场可能推动剧情。** 只想看对白就用「只播台词」;临时摆拍结束后先撤销,再继续游玩或保存。 + +遇到具体操作问题,可以查看 [使用须知](tool/使用须知.md);想了解配置和源码,可看 [开发文档](tool/开发文档.md) 与 [更新记录](CHANGELOG.md)。上面的数量对应当前整理的游戏版本,其他版本以面板实际读取为准。 diff --git a/docs/images/encyclopedia.png b/docs/images/encyclopedia.png new file mode 100644 index 0000000000000000000000000000000000000000..4a92ca0b40623d95476f28ec2c62c189fcc436f9 GIT binary patch literal 83431 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

fGx? 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

^!=J!iL6&Vcy-%+M_g2DlVUyNk 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%0DMo-<2Lnc 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 literal 0 HcmV?d00001 diff --git a/docs/images/world-map.png b/docs/images/world-map.png new file mode 100644 index 0000000000000000000000000000000000000000..f88355f81a5c92950d48834d994acaf44517e047 GIT binary patch literal 92613 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¨+e=H2XXb8?t`+2Z2zqjcd$DB??qfc%pEY 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

h3Ny8&OmNFp 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
R1+6K|mq4_WSp|-tBS`T}Lj?I3N(3mxXDy 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 b7JXa new Promise(r => setTimeout(r, ms)); async function listening(port) { return new Promise(resolve => { @@ -26,7 +26,8 @@ async function panel(config, matchConfig = true) { } async function main() { if (Number(process.versions.node.split('.')[0]) < 22) throw new Error('需要 Node.js 22 或更新版本'); - const config = readConfig(path.join(TOOL, 'config.json'), { cdpPort: 9223, sandboxDir: path.join(path.parse(GAME).root, 'Nyaruru_shots'), webPort: 8787 }, GAME); + const config = readConfig(path.join(TOOL, 'config.json'), { cdpPort: 9223, sandboxDir: path.join(path.parse(LEGACY_GAME).root, 'Nyaruru_shots'), webPort: 8787 }, LEGACY_GAME); + const GAME = config.gameDir || LEGACY_GAME; if (process.argv.includes('--stop')) { const runtimeFile = path.join(TOOL, '.server-runtime.json'); const port = fs.existsSync(runtimeFile) ? JSON.parse(fs.readFileSync(runtimeFile, 'utf8')).port : config.webPort; diff --git a/tool/lib/catalog-position.js b/tool/lib/catalog-position.js new file mode 100644 index 0000000..f0b45a2 --- /dev/null +++ b/tool/lib/catalog-position.js @@ -0,0 +1,108 @@ +// Pure geometry helpers plus an adapter evaluated inside the game. +function catalogCellBounds(data, thumbnail, cell) { + if (!data || !thumbnail || !cell || !thumbnail.data?.some(p=>p.x===cell.x&&p.y===cell.y)) throw Error('这个地点缺少可核实的小地图分块信息,未执行传送'); + const columns=thumbnail.maxX-thumbnail.minX+1,rows=thumbnail.maxY-thumbnail.minY+1; + const ox=Number(data.meta?.cellOx)||0,oy=Number(data.meta?.cellOy)||0; + const width=data.width/columns,height=data.height/rows; + const left=Math.max(0,(cell.x-thumbnail.minX)*width-ox),top=Math.max(0,(cell.y-thumbnail.minY)*height-oy); + const right=Math.min(data.width,(cell.x-thumbnail.minX+1)*width-ox),bottom=Math.min(data.height,(cell.y-thumbnail.minY+1)*height-oy); + if(![left,top,right,bottom].every(Number.isFinite)||right<=left||bottom<=top)throw Error('小地图分块范围无效,未执行传送'); + return{left,top,right,bottom,cellX:cell.x,cellY:cell.y}; +} + +function findCatalogLanding({bounds,shape,tileWidth:tw,tileHeight:th,hitsAt,maxProbes=14000}) { + const minX=Math.ceil(bounds.left*tw+shape.left+8),maxX=Math.floor(bounds.right*tw-shape.right-8); + const minY=Math.ceil(bounds.top*th+shape.top+8),maxY=Math.floor(bounds.bottom*th-shape.bottom-8); + if(minX>maxX||minY>maxY)return null; + const axis=(min,max,step)=>{const out=[min];for(let v=min+step;va.edge-b.edge||b.py-a.py||a.px-b.px); + let probes=0,floating=null; + const budgetError=Error('分块落点检查达到上限'); + const hit=(px,py)=>{if(++probes>maxProbes)throw budgetError;return hitsAt(px,py)||[];}; + const hazard=o=>o.type===2||!!o.triggerDamage||Number(o.upElastic)>0; + try{ + for(const candidate of candidates){ + const{px,py}=candidate; + if(hit(px,py).length)continue; + let supported=null,danger=false; + for(let d=1;d<=Math.min(2*th,maxY-py);d+=Math.max(1,th/8)){ + const hits=hit(px,py+d); + if(!hits.length)continue; + danger=hits.some(hazard)||hits.some(o=>!o.terrain&&!o.fix); + if(!danger&&hits.some(o=>o.terrain||o.fix)){ + let low=0,high=d; + while(high-low>1){const mid=Math.floor((low+high)/2);if(hit(px,py+mid).length)high=mid;else low=mid;} + supported={px,py:py+low,grounded:true}; + } + break; + } + if(supported)return{...supported,probes}; + if(!danger&&!floating)floating={px,py,grounded:false}; + } + }catch(error){if(error!==budgetError||!floating)throw error;} + return floating?{...floating,probes}:null; +} + +function planCatalogLanding(mapId,target,live=false) { + const data=DataManager.getCachedData(mapId); + const thumbnail=DataManager.getThumbnailMapInfo(mapId); + let cell; + if(target && typeof target==='object') { + if(![target.cellX,target.cellY,target.page].every(Number.isInteger))throw Error('地图分块坐标无效'); + cell=Object.values(DataManager._thumbnailMapCell||{}).find(c=>c.mapId===mapId&&c.x===target.cellX&&c.y===target.cellY&&c.page===target.page); + if(!cell)throw Error('目标分块不属于指定地图或页面,未执行传送'); + catalogCellBounds(data,thumbnail,cell); + } else { + const event=data?.events?.[target]; + if(!event)throw Error('收集来源事件不存在,请刷新百科'); + cell=DataManager.getThumbnailCell(event.x,event.y,mapId); + } + let targetBounds=null; + if(cell?.mapId===mapId)try{targetBounds=catalogCellBounds(data,thumbnail,cell);}catch{} + const current=mapId===$gameMap.mapId(); + if(live&&!current)throw Error('目标地图尚未载入'); + let terrain; + if(current)terrain=$gameMap._terrain; + else{ + const preview=Object.create(Game_Map.prototype); + Object.assign(preview,{_mapId:mapId,_tmxExternalVertices:{},_terrain:new PhysicsGrid()}); + // No setup(), events, scene loading, or swaps of the real $gameMap/$dataMap. + preview.tilemapTerrain();preview._terrain.rebuild();terrain=preview._terrain; + } + if(!terrain)throw Error('目标地图碰撞资料尚未就绪,未执行传送'); + const tw=$gameMap.tileWidth(),th=$gameMap.tileHeight(),characters=[$gamePlayer]; + if(typeof $gameLily!=='undefined'&&$gameLily&&!$gameLily.isRemove())characters.push($gameLily); + const shape={left:Math.max(...characters.map(p=>p.hitOffsetL()))+4,right:Math.max(...characters.map(p=>p.hitOffsetR()))+4, + top:Math.max(th*0.8,...characters.map(p=>p.hitOffsetT()))+4,bottom:Math.max(...characters.map(p=>p.hitOffsetB()))+4}; + const probe=new Game_CharacterBase(); + Object.assign(probe,{_hitOffsetL:shape.left,_hitOffsetR:shape.right,_hitOffsetT:shape.top,_hitOffsetB:shape.bottom, + _direction:6,_invertMode:false,_physicsFlag:0,_isFastRect:true,vertices:null}); + probe.hitTest=function(other){return !characters.includes(other)&&this.baseHitTest(other);}; + const grids=[terrain]; + if(current)grids.push($gameMap._physicsFixGrid,$gameMap._physicsDynamicGrid); + const hitsAt=(px,py)=>{ + probe.px=probe.rx=px;probe.py=probe.ry=py; + const hits=[];for(const grid of grids)grid?.getObjGrid(probe,hits);return hits; + }; + const areas=targetBounds?[targetBounds]:[]; + const otherCells=[...(thumbnail?.data||[])].filter(p=>!cell||p.x!==cell.x||p.y!==cell.y) + .sort((a,b)=>Math.abs(a.x-(cell?.x||0))+Math.abs(a.y-(cell?.y||0))-Math.abs(b.x-(cell?.x||0))-Math.abs(b.y-(cell?.y||0))); + for(const p of otherCells)try{areas.push(catalogCellBounds(data,thumbnail,p));}catch{} + areas.push({left:0,top:0,right:data.width,bottom:data.height,cellX:null,cellY:null}); + for(const bounds of areas){ + let result; + try{result=findCatalogLanding({bounds,shape,tileWidth:tw,tileHeight:th,hitsAt});} + catch(error){if(!error.message.includes('检查达到上限'))throw error;} + if(!result)continue; + const alternate=bounds!==targetBounds; + return{...result,bounds,targetBounds,alternate,x:(result.px-$gamePlayer.tileXToCenterX(0))/tw,y:(result.py-$gamePlayer.tileYToCenterY(0))/th, + positionNote:alternate?'目标分块受阻,已改到同图其他可用位置':result.grounded?'目标分块边缘附近的可落脚空处':'目标分块内的空处,保持悬停'}; + } + // Only an entirely blocked/unavailable map fails; a blocked target cell alone + // always expands the search instead of cancelling the user's journey. + throw Error('已检查目标分块、邻近分块和整张地图,仍未找到能容纳角色的空处'); +} + +module.exports={catalogCellBounds,findCatalogLanding,planCatalogLanding}; diff --git a/tool/lib/combat.js b/tool/lib/combat.js new file mode 100644 index 0000000..7d30211 --- /dev/null +++ b/tool/lib/combat.js @@ -0,0 +1,43 @@ +function installCombat() { + if (window.__nyaruruCombat) return window.__nyaruruCombat; + let settings={outgoing:1,incoming:1,oneHit:false}, attacker=null, touched=Date.now(), actors=$gameActors; + const prototype=Game_CharacterBase.prototype; + const hit=prototype.onAttackHit, direct=prototype.requestDamage, hooked=new WeakMap(); + const playerSide=c=>c===$gamePlayer || (typeof $gameLily!=='undefined' && c===$gameLily); + function reset(){settings={outgoing:1,incoming:1,oneHit:false};} + function touch(){if(actors!==$gameActors){reset();actors=$gameActors;}touched=Date.now();} + function scale(character,value){ + if(!(value>0))return value; + if(playerSide(character))return value*settings.incoming; + const target=character.battleObject?.(), source=attacker?.battleObject?.(); + if(playerSide(attacker) && target && source && target.team!==source.team){ + const scaled=value*settings.outgoing; + return settings.oneHit && !target.unlimitHp ? Math.max(scaled,target.hp+1) : scaled; + } + return value; + } + prototype.onAttackHit=function(info,character,...args){ + const previous=attacker;attacker=info?.bulletOwner||this; + const already=character&&hooked.has(character); + if(character&&!already){ + const descriptor=Object.getOwnPropertyDescriptor(character,'overrideBeAttackDamage'),original=character.overrideBeAttackDamage; + hooked.set(character,{descriptor}); + Object.defineProperty(character,'overrideBeAttackDamage',{configurable:true,writable:true,enumerable:false,value(...values){return scale(this,original.apply(this,values));}}); + } + try{return hit.call(this,info,character,...args);}finally{ + attacker=previous; + if(character&&!already){const {descriptor}=hooked.get(character);if(descriptor)Object.defineProperty(character,'overrideBeAttackDamage',descriptor);else delete character.overrideBeAttackDamage;hooked.delete(character);} + } + }; + prototype.requestDamage=function(value,...args){return direct.call(this,playerSide(this)&&value>0?value*settings.incoming:value,...args);}; + const api={touch,clear:reset,read(){touch();return{ok:true,...settings};},change(patch){ + touch();const next={...settings}; + for(const key of ['outgoing','incoming'])if(patch[key]!==undefined){if(typeof patch[key]!=='number'||!Number.isFinite(patch[key])||patch[key]<0||patch[key]>1000)throw Error('倍率必须为 0~1000 的数字');next[key]=patch[key];} + if(patch.oneHit!==undefined){if(typeof patch.oneHit!=='boolean')throw Error('秒杀开关无效');next.oneHit=patch.oneHit;} + settings=next;return api.read(); + }}; + Object.defineProperty(window,'__nyaruruCombat',{value:api,configurable:true}); + setInterval(()=>{if(Date.now()-touched>120000||actors!==$gameActors){reset();actors=$gameActors;}},1000); + return api; +} +module.exports={installCombat}; diff --git a/tool/lib/data.js b/tool/lib/data.js index ad6e491..0c8a562 100644 --- a/tool/lib/data.js +++ b/tool/lib/data.js @@ -7,7 +7,7 @@ // unpacked/data/MapInfos.json → 393 张地图 // out/triggers.csv → 897 条剧情触发点(其中 731 条有坐标) // -// ⚠️ **台词不在这里。** 这是"C 方案"(见开发文档 12.5)的核心约定: +// ⚠️ **台词不在这里。** 这是"C 方案"(见 tool/开发文档.md 的运行时数据约定)的核心约定: // 随包分发的只有**结构索引**(哪里有个触发点), // 一个字创作内容都不带。台词在连上游戏后从 `$dataStory` 实时读, // 只存在于内存,**绝不落盘**(读取逻辑在 lib/ops.js 的 readStoryIndex)。 diff --git a/tool/lib/encyclopedia.js b/tool/lib/encyclopedia.js new file mode 100644 index 0000000..0643cbc --- /dev/null +++ b/tool/lib/encyclopedia.js @@ -0,0 +1,94 @@ +// Read-only inventory/location join, evaluated in the game. No progress is modified. +function readEncyclopedia(inventory) { + const maps = DataManager._cacheMapData; + if (!maps || !Object.keys(maps).length) throw new Error('地图资料尚未加载,请先正常读档'); + const items = inventory.items.map(item => ({ ...item, locations: [] })); + items.push({ type: 'coin', id: 0, name: '金币', desc: '可用于商店购买物品。', count: inventory.stats.coin.value, max: 99999, locations: [] }); + const byKey = new Map(items.map(item => [item.type + ':' + item.id, item])); + const shops = [], locations = [], seen = new Set(); + const collectedKeys = new Set(); + for (const region of Object.values(typeof $dataRegion === 'undefined' ? {} : $dataRegion)) { + for (const [mapId, eventId] of region?.exploreItem || []) collectedKeys.add(mapId + ':' + eventId); + } + const conditions = cond => { + const result = []; + for (const n of [1, 2]) if (cond?.['switch' + n + 'Valid']) { + const id = cond['switch' + n + 'Id']; + result.push({ label: '开关 ' + ($dataSystem.switches[id] || id), met: !!$gameSwitches.value(id) }); + } + if (cond?.variableValid) result.push({ label: '变量 ' + ($dataSystem.variables[cond.variableId] || cond.variableId) + ' ≥ ' + cond.variableValue, + met: $gameVariables.value(cond.variableId) >= cond.variableValue }); + if (cond?.actorValid) result.push({ label: '队伍角色 ' + cond.actorId, met: null }); + if (cond?.itemValid) result.push({ label: '持有道具 ' + cond.itemId, met: null }); + return result; + }; + for (const [mapKey, map] of Object.entries(maps)) { + const mapId = Number(mapKey); + if (!map || !Number.isInteger(mapId)) continue; + const regionId = DataManager.mapRegion?.(mapId) || 0; + const region = typeof $dataRegion !== 'undefined' && $dataRegion[regionId]; + const area = region?.name || '未分类区域'; + for (const event of map.events || []) { + if (!event) continue; + for (const [pageIndex, page] of (event.pages || []).entries()) { + const list = page.list || []; + for (const [commandIndex, command] of list.entries()) { + if (command.code !== 357) continue; + const [, name, , args = {}] = command.parameters || []; + const coinPile = name === 'ai' && Number(args.type) === 60; + if (!coinPile && !['addItem', 'add_coin', 'shop'].includes(name)) continue; + if (name === 'add_coin' && !(Number(args.num) > 0)) continue; + const base = { mapId, mapName: $dataMapInfos?.[mapId]?.name || '地图 ' + mapId, area, regionId, + eventId: event.id, eventName: event.name || '', x: event.x, y: event.y, pageIndex, + conditions: conditions(page.conditions), branch: Number(command.indent) > 0 }; + if (page.conditions?.selfSwitchValid) base.conditions.push({ label: '独立开关 ' + page.conditions.selfSwitchCh, + met: !!$gameSelfSwitches.value([mapId, event.id, page.conditions.selfSwitchCh]) }); + if (name === 'shop') { shops.push({ ...base, kind: 'shop' }); continue; } + const type = name === 'add_coin' || coinPile ? 'coin' : args.type; + const id = name === 'add_coin' || coinPile ? 0 : Number(args.idx); + const item = byKey.get(type + ':' + id); + if (!item) continue; + const key = `${type}:${id}:${mapId}:${event.id}:${pageIndex}:${commandIndex}`; + if (seen.has(key)) continue; + seen.add(key); + const official = collectedKeys.has(mapId + ':' + event.id); + // A later marker is reliable only for a straight-line pickup. Branch + // rewards may reuse A for another path, so those remain unknown. + const marker = list.slice(commandIndex + 1).find(c => c.code === 123 && Number(c.indent || 0) === Number(command.indent || 0) && c.parameters?.[1] === 0); + const nextPageHides = (event.pages || []).some(pg => pg.conditions?.selfSwitchValid && pg.conditions.selfSwitchCh === marker?.parameters?.[0]); + const reliable = official || (!base.branch && marker && nextPageHides); + const status = reliable ? !!$gameSelfSwitches.value([mapId, event.id, official ? 'A' : marker.parameters[0]]) : null; + const location = { ...base, key, type, id, kind: coinPile ? 'coinpile' : 'pickup', collected: status, official, + sourceNote: coinPile ? '猫币堆:击打 8 次耗尽,掉落数量受配饰影响' : '', + amount: coinPile ? null : name === 'add_coin' ? Number(args.num) : 1, antiCollection: args.antiCollection === 'true' }; + item.locations.push(location); locations.push(location); + } + } + } + } + const actor = $gamePlayer.actor(); + for (const item of items) { + let definition = null; + if (['consumables','weapons','stones','ornaments'].includes(item.type)) definition = actor['_' + item.type].data()[item.id]; + else if (item.type === 'bell') definition = actor._ornaments.bellData(); + else if (item.type === 'hpst') definition = actor.hpStrengthData(); + else if (item.type === 'skillst') definition = actor.skillStrengthData(); + else if (item.type === 'joycoin') definition = actor.joyCoinData(); + item.price = definition?.price ?? null; + item.shopCandidate = Number(definition?.price) > 0; + item.openTask = definition?.openTask ?? null; + } + const counted = new Map(); + for (const location of locations) if (location.official) counted.set(location.mapId + ':' + location.eventId, location); + const summaries = new Map(); + for (const location of counted.values()) { + if (!summaries.has(location.area)) summaries.set(location.area, { area: location.area, total: 0, collected: 0 }); + const s = summaries.get(location.area); s.total++; if (location.collected) s.collected++; + } + const regions = [...summaries.values()]; + return { ok: true, items, locations, shops, regions, subject: inventory.subject, + stats: { items: items.length, locations: locations.length, shops: shops.length, + total: counted.size, collected: [...counted.values()].filter(l => l.collected).length }, + coverage: '来源为当前游戏已加载的地图物品指令、猫币堆、收集点和商店入口。商店候选需结合任务进度在店内确认;脚本动态奖励及普通敌人掉落不在地图索引中。未知状态不会按未收集处理。' }; +} +module.exports = { readEncyclopedia }; diff --git a/tool/lib/item-icons.js b/tool/lib/item-icons.js new file mode 100644 index 0000000..222be34 --- /dev/null +++ b/tool/lib/item-icons.js @@ -0,0 +1,31 @@ +// Renders only inventory thumbnails through the game's own UI icon mapping. +// Data URLs stay in memory; no game textures are exported to the filesystem. +async function readItemIcons(items) { + if (typeof Sprite_ItemIcon === 'undefined') throw new Error('游戏图标组件尚未就绪'); + const bitmaps = new Map(); + for (const item of items) { + if (!bitmaps.has(item.type)) bitmaps.set(item.type,Sprite_ItemIcon.prototype.itemBitmap(item.type)); + } + await Promise.all([...new Set(bitmaps.values())].map(bitmap=>new Promise((resolve,reject)=>{ + if (bitmap.isReady()) return resolve(); + if (bitmap.isError?.()) return reject(new Error('物品图标加载失败')); + const timer=setTimeout(()=>reject(new Error('物品图标加载超时,请重试')),8000); + bitmap.addLoadListener(()=>{ clearTimeout(timer); resolve(); }); + }))); + const icons = {}; + for (const item of items) { + const bitmap = bitmaps.get(item.type); + let frame; + const proxy = { _type:item.type, bitmap, setFrame(x,y,width,height) { frame={x,y,width,height}; } }; + Sprite_ItemIcon.prototype.refreshIdx.call(proxy,item.id); + const {x,y,width,height} = frame; + if (![x,y,width,height].every(Number.isFinite) || width <= 0 || height <= 0 || width > 512 || height > 512 || + x < 0 || y < 0 || x+width > bitmap.width || y+height > bitmap.height) throw new Error('物品图标裁切范围无效'); + const canvas=document.createElement('canvas'); canvas.width=width; canvas.height=height; + const context=canvas.getContext('2d'); context.imageSmoothingEnabled=false; + context.drawImage(bitmap.canvas,x,y,width,height,0,0,width,height); + icons[item.type+':'+item.id]=canvas.toDataURL('image/png'); + } + return {ok:true,icons}; +} +module.exports = { readItemIcons }; diff --git a/tool/lib/motion.js b/tool/lib/motion.js new file mode 100644 index 0000000..e090a6d --- /dev/null +++ b/tool/lib/motion.js @@ -0,0 +1,168 @@ +// Runs inside the game. Hooks live outside saved actors and can be fully removed. +function installMotion() { + if (window.__nyaruruMotion?.version === 5) { window.__nyaruruMotion.touch(); return window.__nyaruruMotion; } + // Preserve an active hover when upgrading the in-game helper after a service restart. + const previous = window.__nyaruruMotion; + const resume = [$gamePlayer, typeof $gameLily !== 'undefined' ? $gameLily : null] + .filter(c => c && previous?.has(c)).map(c => ({ character:c, noclip:!!c._suspendPhysicsHitTest, vx:c._vx, vy:c._vy })); + if (previous?.dispose) previous.dispose(); else previous?.clear(); + const records = new Map(); + const actorSet = () => typeof $gameActors !== 'undefined' ? $gameActors : null; + let touched = Date.now(), player = $gamePlayer, data = $dataMap, map = $gameMap, actors = actorSet(); + const reset = c => { + c.vx = c.vy = c._vx = c._vy = c.dotX = c.dotY = 0; + c.clearImpact?.(); c._impact = null; c.clearGrounding?.(); + c._groundingX = c._terrainGroundingX = c.px; + c._groundingY = c._terrainGroundingY = c.py; + }; + function disable(c) { + const r = records.get(c); + if (!r) return; + if (r.gravity) Object.defineProperty(c, 'doGravity', r.gravity); else delete c.doGravity; + if (r.transfer) Object.defineProperty(c, 'updateMapBoundTransfer', r.transfer); else delete c.updateMapBoundTransfer; + if (r.cache) Object.defineProperty(c, 'cacheDeltaTimeSpeed', r.cache); else delete c.cacheDeltaTimeSpeed; + if (r.suspendCheck) Object.defineProperty(c, 'isSuspendGravity', r.suspendCheck); else delete c.isSuspendGravity; + for (const [name, descriptor] of Object.entries(r.controls)) { + if (descriptor) Object.defineProperty(c,name,descriptor); else delete c[name]; + } + for (const [key, descriptor] of Object.entries(r.flags)) { + if (descriptor) Object.defineProperty(c,key,{...descriptor,value:c[key]}); + else delete c[key]; + } + records.delete(c); reset(c); + } + function clear() { + for (const c of [...records.keys()]) { + disable(c); c.resumeGravity?.(); c.resumePhysicsHitTest?.(); + } + } + function sync() { + // Normal exits reuse these objects; loading a save replaces them. + if (player !== $gamePlayer || map !== $gameMap || actors !== actorSet()) { + clear(); player = $gamePlayer; map = $gameMap; actors = actorSet(); data = $dataMap; + } else if (data !== $dataMap && $dataMap && $gameMap.mapId() > 0 && + SceneManager._scene?.constructor.name === 'Scene_Map' && !SceneManager.isSceneChanging?.()) { + data = $dataMap; + for (const [c, r] of records) { + reset(c); c.suspendGravity(); + if (r.noclip) c.suspendPhysicsHitTest(); + } + } + } + function touch() { sync(); touched = Date.now(); } + function keepInMap(c) { + if (!c.tileXToCenterX || !c.tileYToCenterY || !c.unflush) return; + const tw=$gameMap.tileWidth(), th=$gameMap.tileHeight(); + const left=c.hitOffsetL?.() ?? c.tileXToCenterX(0), top=c.hitOffsetT?.() ?? c.tileYToCenterY(0); + const right=$gameMap.width()*tw-(c.hitOffsetR?.() ?? tw-c.tileXToCenterX(0)); + const bottom=$gameMap.height()*th-(c.hitOffsetB?.() ?? th-c.tileYToCenterY(0)); + const px=Math.max(left,Math.min(right,c.px)),py=Math.max(top,Math.min(bottom,c.py)); + if(px===c.px && py===c.py)return; + const blockedX=px!==c.px,blockedY=py!==c.py; + const x=(px-c.tileXToCenterX(0))/tw,y=(py-c.tileYToCenterY(0))/th; + const locate=typeof Game_CharacterBase!=='undefined' ? Game_CharacterBase.prototype.locate : c.locate; + locate.call(c,x,y);c.unflush(x,y); + if(blockedX){c.vx=0;c.dotX=0;}if(blockedY){c.vy=0;c.dotY=0;} + } + function enable(c, noclip = false) { + if (!c) return; + touch(); + if (!records.has(c)) { + const gravity = Object.getOwnPropertyDescriptor(c, 'doGravity'); + const transfer = Object.getOwnPropertyDescriptor(c, 'updateMapBoundTransfer'), originalTransfer = c.updateMapBoundTransfer; + const cache = Object.getOwnPropertyDescriptor(c, 'cacheDeltaTimeSpeed'), originalCache = c.cacheDeltaTimeSpeed; + const originalGravity = c.doGravity; + const suspendCheck = Object.getOwnPropertyDescriptor(c, 'isSuspendGravity'); + const flags = Object.fromEntries(['_suspendGravity','_suspendPhysicsHitTest'].map(key=>[key,Object.getOwnPropertyDescriptor(c,key)])); + const controls = Object.fromEntries(['controlJump','controlDoubleJump'].map(key=>[key,Object.getOwnPropertyDescriptor(c,key)])); + const record = { gravity, transfer, cache, suspendCheck, flags, controls, handledJump:false, noclip:!!(noclip || c._suspendPhysicsHitTest) }; + records.set(c, record); + // Native skill/stagger cleanup calls resumeGravity(). Keep the tool's + // hover independent, without blocking those native state transitions. + Object.defineProperty(c, 'isSuspendGravity', { configurable:true, writable:true, enumerable:false, value() { return true; } }); + const skillActive = () => !!c._bodyAttacking; + const descending = () => c === $gamePlayer && !c.isOutControl?.() && !!c.isPressDown?.(); + const extraJump = actor => { + if (c !== $gamePlayer || c.isFlexibleGrounding?.() || !c.isTriggeredJump?.() || c.isOutControl?.()) return false; + const sign = c._invertMode ? -1 : 1; + const native = c.jumpStrength?.(actor); + const strength = Number.isFinite(native) && native !== 0 ? Math.abs(native) : Math.abs(actor?.doubleJumpStrength?.() || 10); + c.vy = -sign * strength; + c.frameJump = true; c._nextFrameImmueElasticCollision = true; c._impact = null; + if (typeof AudioManager !== 'undefined') AudioManager.playNormalSe?.('mtv_jump2'); + return true; + }; + for (const name of ['controlJump','controlDoubleJump']) { + const original = c[name]; + if (typeof original !== 'function') continue; + Object.defineProperty(c,name,{configurable:true,writable:true,enumerable:false,value(...args) { + if (name === 'controlJump') record.handledJump = false; + else if (record.handledJump) return true; + if (extraJump(args[0])) { record.handledJump = true; return true; } + return original.apply(this,args); + }}); + } + Object.defineProperty(c, 'doGravity', { configurable: true, writable: true, enumerable: false, value() { + sync(); + if (!records.has(c)) return originalGravity.apply(this,arguments); + // Positive Y is down; reversed-gravity scenes use the other sign. + const sign = this._invertMode ? -1 : 1; + const vy = Number.isFinite(this._vy) ? this._vy : Number(this.vy) || 0; + const g = Math.max(0.1, Math.abs(Number($gameMap.g?.() || 0) + Number(this.g?.() || 0)) || 0.35); + if (!skillActive()) { + if (descending()) this.vy = sign * 4; + else this.vy = vy * sign < 0 ? sign * Math.min(0, vy * sign + g * Math.max(0.1, Math.min(2, this.deltaTime?.() || 1))) : 0; + } + this.setTempGravity?.(0); this.setTempMGravity?.(0); + } }); + // Apply intent once BEFORE time scaling and collision resolution. + // makeMomentum is also called AFTER collisions by endTestV; changing + // velocity there overwrites the resolved floor contact and tunnels down. + if (typeof originalCache === 'function') Object.defineProperty(c, 'cacheDeltaTimeSpeed', { + configurable:true, writable:true, enumerable:false, value(...args) { + sync(); + if (records.has(c) && !this._testV && !skillActive()) { + if (descending()) this.vy = (this._invertMode ? -1 : 1) * 4; + else if (this._vy * (this._invertMode ? -1 : 1) > 0) this.vy = 0; + } + return originalCache.apply(this,args); + } + }); + if(c === $gamePlayer && typeof originalTransfer === 'function') Object.defineProperty(c,'updateMapBoundTransfer',{ + configurable:true,writable:true,enumerable:false,value(...args){ + sync(); + const result=originalTransfer.apply(this,args); + if(!records.has(c) || this.isTransferring?.() || $gameMap.isEventRunning?.())return result; + // Side walls sit outside the map, and noclip has no impact. + // Resolve real exits before our tighter fallback boundary. + const left=this.px-(this.hitOffsetL?.()||0)<0; + const right=this.px+(this.hitOffsetR?.()||0)>$gameMap.width()*$gameMap.tileWidth(); + const down=this.py+(this.hitOffsetB?.()||0)>$gameMap.height()*$gameMap.tileHeight(); + if((left && this.autoTransfer?.(3)) || (right && this.autoTransfer?.(4)) || (down && this.autoTransfer?.(2)))return result; + if(!this.isTransferring?.())keepInMap(this); + return result; + } + }); + } + records.get(c).noclip=!!(noclip || c._suspendPhysicsHitTest); + reset(c); c.suspendGravity(); + if (noclip) c.suspendPhysicsHitTest(); + // These are temporary tool states. Omitting them from saves avoids loading + // suspended gravity without the per-session controller that stops a jump. + for (const key of ['_suspendGravity','_suspendPhysicsHitTest']) { + const descriptor=Object.getOwnPropertyDescriptor(c,key); + if (descriptor?.configurable && 'value' in descriptor) Object.defineProperty(c,key,{...descriptor,enumerable:false}); + } + } + let timer; + const api = { version:5, touch, enable, disable, clear, has: c => records.has(c), reset, + setNoclip(c,on) { if(on)enable(c,true);else {const r=records.get(c);if(r)r.noclip=false;c.resumePhysicsHitTest();} }, + dispose() { clear(); if (typeof clearInterval === 'function') clearInterval(timer); } }; + Object.defineProperty(window, '__nyaruruMotion', { value: api, configurable: true }); + if (typeof setInterval === 'function') timer = setInterval(() => { + if (Date.now() - touched > 120000) clear(); else sync(); + }, 1000); + for (const r of resume) { enable(r.character,r.noclip); r.character._vx=r.vx; r.character._vy=r.vy; } + return api; +} +module.exports = { installMotion }; diff --git a/tool/lib/ops.js b/tool/lib/ops.js index c0ff91a..8a8c07f 100644 --- a/tool/lib/ops.js +++ b/tool/lib/ops.js @@ -1,6 +1,13 @@ const { installPhotoTools } = require('./photo-state'); const { chooseStoryPosition } = require('./story-position'); const { installTrainer } = require('./trainer'); +const { installMotion } = require('./motion'); +const { readEncyclopedia } = require('./encyclopedia'); +const { readWorldMap } = require('./world-map'); +const { readItemIcons } = require('./item-icons'); +const { installCombat } = require('./combat'); +const { catalogCellBounds,findCatalogLanding,planCatalogLanding } = require('./catalog-position'); +const CATALOG = `const catalogCellBounds=(${catalogCellBounds.toString()}),findCatalogLanding=(${findCatalogLanding.toString()}),planCatalogLanding=(${planCatalogLanding.toString()});`; const PHOTO = `const photo = (${installPhotoTools.toString()})();`; // 游戏操作脚本库 // @@ -8,12 +15,12 @@ const PHOTO = `const photo = (${installPhotoTools.toString()})();`; // 之所以存成字符串而非直接执行,是因为这些代码必须在游戏进程里跑, // 才能访问 $gamePlayer / $gameMap 这些游戏全局对象。 // -// ⚠️ 关键设计约束(来自 2026-10-02 实机探查,详见 aitemp/探查结果.md): +// ⚠️ 关键设计约束(当前规则见 tool/开发文档.md): // 1. 传送必须用 $gamePlayer.locate(x, y),不能手写 _x/_y/_realX/_realY // —— 后者会漏掉物理引擎的 px/py,导致"画面过去了、碰撞体还在原地" // 2. 游戏有 4 套坐标系,locate() 内部会一次同步全部 // 3. 无敌用 lockInvincible()(常驻)而非 makeInvincible()(会自己失效) -// 4. 工具不负责读档 —— CDP 读档会崩(物理网格无法反序列化) +// 4. 读档由用户在游戏正常菜单完成,不能裸调 loadGame(还需重建物理网格) /** * 包一层 IIFE,统一处理:游戏未进地图的守卫、异常捕获、JSON 返回格式。 @@ -23,12 +30,14 @@ const PHOTO = `const photo = (${installPhotoTools.toString()})();`; * CDP 那边开了 `awaitPromise: true`,所以返回 Promise 会被自动等待。 * 同步的操作写成 async 也无妨,只是多包一层 Promise。 */ -function wrap(body, { needMap = true } = {}) { +function wrap(body, { needMap = true, allowMenu = false } = {}) { const guard = needMap ? ` if (typeof $gamePlayer === 'undefined' || !$gamePlayer || typeof $gameMap === 'undefined' || !$gameMap || $gameMap.mapId() === 0 || - (typeof SceneManager !== 'undefined' && SceneManager._scene?.constructor.name !== 'Scene_Map')) { - return JSON.stringify({ ok: false, error: '游戏还没进入地图。请先在游戏里读档或开始游戏。' }); + (typeof SceneManager !== 'undefined' && !${JSON.stringify(allowMenu ? ['Scene_Map', 'Scene_Menu', 'Scene_Shop'] : ['Scene_Map'])}.includes(SceneManager._scene?.constructor.name))) { + return JSON.stringify({ ok: false, error: typeof SceneManager !== 'undefined' && ['Scene_Menu','Scene_Shop'].includes(SceneManager._scene?.constructor.name) + ? '请先关闭游戏菜单,回到地图后再执行移动或剧情操作。' + : '游戏还没进入地图。请先在游戏里读档或开始游戏。' }); } ` : ''; return `(async function(){ try { ${guard} ${body} } catch (e) { return JSON.stringify({ ok:false, error: String(e && e.message || e) }); } })()`; @@ -46,6 +55,7 @@ const SLEEP = `const sleep = (ms) => new Promise(r => setTimeout(r, ms));`; * 机制见开发文档第 6 节。 */ const SAFE_CHAR = ` + const motion = (${installMotion.toString()})(); // 清残留碰撞 + 重置落地锚点,否则 isFallDown() 会一直为真 const safeChar = (c) => { if (!c) return; @@ -63,7 +73,7 @@ const SAFE_CHAR = ` const floatChar = (c) => { if (!c) return; safeChar(c); - try { c.suspendGravity(); c.suspendPhysicsHitTest(); } catch (e) {} + motion.enable(c, true); }; // 莉莉娅在不在(她被移除时不该动她) const lilyAny = () => typeof $gameLily !== 'undefined' ? $gameLily : null; @@ -110,13 +120,38 @@ const OPS = { `, { needMap: false }); }, + worldMapTravel(mapId,cellX,cellY,page){ + if(!Number.isInteger(mapId)||mapId<1||![cellX,cellY,page].every(Number.isInteger))throw Error('地图分块坐标无效'); + return this.teleport(0,0,{mapId,catalog:{cellX,cellY,page}}); + }, + worldMap(){ + return wrap(`const trainer=(${installTrainer.toString()})(); + const encyclopedia=(${readEncyclopedia.toString()})(trainer.read()); + return JSON.stringify((${readWorldMap.toString()})(encyclopedia));`,{allowMenu:true}); + }, + catalogTravel(mapId,eventId){ + if(!Number.isInteger(mapId)||mapId<1||!Number.isInteger(eventId)||eventId<1)throw Error('收集地点无效'); + return this.teleport(0,0,{mapId,catalog:{eventId}}); + }, + combat(body=null){return wrap(`const api=(${installCombat.toString()})();return JSON.stringify(${body?`api.change(${JSON.stringify(body)})`:'api.read()'});`,{allowMenu:true});}, + itemIcons() { + return wrap(`const trainer = (${installTrainer.toString()})(); + return JSON.stringify(await (${readItemIcons.toString()})(trainer.read().items));`, { allowMenu: true }); + }, + encyclopedia() { + return wrap(`const trainer = (${installTrainer.toString()})(); + return JSON.stringify((${readEncyclopedia.toString()})(trainer.read()));`, { allowMenu: true }); + }, trainer(body = null) { return wrap(`const trainer = (${installTrainer.toString()})(); - return JSON.stringify(${body ? `trainer.change(${JSON.stringify(body)})` : 'trainer.read()'});`); + return JSON.stringify(${body ? `trainer.change(${JSON.stringify(body)})` : 'trainer.read()'});`, { allowMenu: true }); }, /** 读取当前游戏状态(供控制面板顶栏显示) */ snapshot() { return wrap(` + if (window.__nyaruruMotion && window.__nyaruruMotion.version !== 5) (${installMotion.toString()})(); + window.__nyaruruMotion?.touch(); + window.__nyaruruCombat?.touch(); if (window.__nyaruruPhoto?.touch) window.__nyaruruPhoto.touch(); if (window.__nyaruruTrainer) window.__nyaruruTrainer.touch(); const p = $gamePlayer, m = $gameMap; @@ -129,12 +164,12 @@ const OPS = { mapId: m.mapId(), mapName: mi ? mi.name : ('地图 ' + m.mapId()), mapSize: [m.width(), m.height()], - x: p._x, - y: p._y, + x: p.tileXToCenterX ? Math.round((p.px - p.tileXToCenterX(0)) / m.tileWidth() * 10000) / 10000 : p._x, + y: p.tileYToCenterY ? Math.round((p.py - p.tileYToCenterY(0)) / m.tileHeight() * 10000) / 10000 : p._y, realX: p._realX, realY: p._realY, hp: bo ? bo.hp : null, - gravitySuspended: !!p.isSuspendGravity(), + gravitySuspended: !!window.__nyaruruMotion?.has(p), invincible: !!p.isInvincible(), noclip: !!p._suspendPhysicsHitTest, // 莉莉娅在不在画面里(通关后她会一直跟着,可以用开关藏起来) @@ -147,12 +182,14 @@ const OPS = { uiHidden: SceneManager._scene.__nyaruruUiHidden || {message:false, artwork:false, hud:false}, switchesOn: (function(){ let n=0; for (let i=1;i<$dataSystem.switches.length;i++) if ($gameSwitches.value(i)) n++; return n; })(), }); - `); + `, { allowMenu: true }); }, /** 实机 NW.js 用 chrome-extension URL;身份以 Node 入口和实际存档目录为准。 */ selfPath() { return wrap(` + window.__nyaruruMotion?.touch(); + window.__nyaruruCombat?.touch(); if (window.__nyaruruTrainer) window.__nyaruruTrainer.touch(); if (!window.__nyaruruIdentity) Object.defineProperty(window, '__nyaruruIdentity', { value: { maps: new WeakMap(), next: 0, epoch: Date.now().toString(36) + Math.random().toString(36).slice(2) } @@ -167,14 +204,15 @@ const OPS = { const saveDir = typeof StorageManager !== 'undefined' ? StorageManager.fileDirectoryPath() : ''; return JSON.stringify({ ok: true, gameDir, saveDir, href: String(location.href), pathname: String(location.pathname), mapToken: state.epoch + ':' + (player ? state.maps.get(player) : 0) + ':' + mapId + ':' + (mapData ? state.maps.get(mapData) : 0), - mapId, inMap: typeof SceneManager !== 'undefined' && SceneManager._scene?.constructor.name === 'Scene_Map' }); + mapId, inMap: typeof SceneManager !== 'undefined' && SceneManager._scene?.constructor.name === 'Scene_Map', + inMenu: mapId > 0 && ['Scene_Menu','Scene_Shop'].includes(SceneManager._scene?.constructor.name) }); `, { needMap: false }); }, /** * 传送到指定格。 * - * ⚠️ 这个函数处理了一个**实测踩出来的坑**,改动前务必先读 `探查结果.md` 与开发文档第 6 节。 + * ⚠️ 这个函数处理了游戏物理坐标同步问题,改动前先读 `tool/开发文档.md` 的「功能模块与边界」。 * * 游戏的"坠落复位"机制(nya_statem.js:5986): * isFallDown() 在玩家碰到 type===2 的地形(坑、虚空)时返回 true, @@ -194,19 +232,23 @@ const OPS = { * * 用户想真正站到地上走动时,调用 land()。 */ - teleport(x, y, { float = true, mapId = null, story = null } = {}) { + teleport(x, y, { float = true, mapId = null, story = null, catalog = null } = {}) { if (x === null || y === null || x === '' || y === '' || !Number.isFinite(Number(x)) || !Number.isFinite(Number(y))) throw new Error('坐标必须是有限数值'); if (mapId !== null && (!Number.isInteger(Number(mapId)) || Number(mapId) < 1)) throw new Error('地图编号无效'); return wrap(` ${SLEEP} + ${catalog ? CATALOG : ''} const p = $gamePlayer, m = $gameMap; const targetMap = ${mapId !== null && mapId !== undefined ? Number(mapId) : 'null'}; const sameMap = (targetMap === null || targetMap === m.mapId()); const fromMap = m.mapId(); const previous = { x: p._x, y: p._y }; + const priorPhysical={px:p.px,py:p.py}; + let catalogPlacement = ${catalog ? `planCatalogLanding(targetMap,${JSON.stringify(catalog.eventId ?? catalog)})` : 'null'}; + const destination=catalogPlacement||{x:${Number(x)},y:${Number(y)}}; - const tx = Math.max(0, Math.min(m.width() - 1, Math.round(${Number(x)}))); - const ty = Math.max(0, Math.min(m.height() - 1, Math.round(${Number(y)}))); + const tx = catalogPlacement?destination.x:Math.max(0, Math.min(m.width() - 1, Math.round(destination.x))); + const ty = catalogPlacement?destination.y:Math.max(0, Math.min(m.height() - 1, Math.round(destination.y))); if (!sameMap) { if (typeof $dataMapInfos === 'undefined' || !$dataMapInfos[targetMap]) return JSON.stringify({ok:false,error:'目标地图不存在'}); @@ -242,13 +284,34 @@ const OPS = { }); } // 换图后坐标可能被目标地图的出生点逻辑改过,再定一次位 - const nx = Math.max(0, Math.min(m.width() - 1, Math.round(${Number(x)}))); - const ny = Math.max(0, Math.min(m.height() - 1, Math.round(${Number(y)}))); - p.locate(nx, ny); + if(!catalogPlacement){ + const nx = Math.max(0, Math.min(m.width() - 1, Math.round(destination.x))); + const ny = Math.max(0, Math.min(m.height() - 1, Math.round(destination.y))); + p.locate(nx, ny); + } } else { - p.locate(tx, ty); + if(!catalogPlacement)p.locate(tx, ty); } + ${catalog ? ` + try{catalogPlacement=planCatalogLanding(targetMap,${JSON.stringify(catalog.eventId ?? catalog)},true);} + catch(error){ + // A live gate/actor can differ from the cached terrain preview. + // Return to the starting map/position instead of forcing a landing. + if(!sameMap){ + p.reserveTransfer(fromMap,previous.x,previous.y,2,0); + const rollbackStart=Date.now(); + while(Date.now()-rollbackStart<5000){await sleep(100);if($gameMap!==m||$gamePlayer!==p)break;if(m.mapId()===fromMap&&!p.isTransferring()&&SceneManager._scene?.constructor.name==='Scene_Map')break;} + if($gameMap!==m||$gamePlayer!==p||m.mapId()!==fromMap||p.isTransferring())throw Error('目标分块未找到落点,返回原地图超时或游戏状态已变化,请检查游戏状态'); + const bx=(priorPhysical.px-p.tileXToCenterX(0))/m.tileWidth(),by=(priorPhysical.py-p.tileYToCenterY(0))/m.tileHeight(); + Game_CharacterBase.prototype.locate.call(p,bx,by);p.unflush(bx,by); + } + throw Error('目标分块暂时被地形或角色占用,已保留或返回原位置:'+error.message); + } + Game_CharacterBase.prototype.locate.call(p,catalogPlacement.x,catalogPlacement.y); + p.unflush(catalogPlacement.x,catalogPlacement.y); + ` : ''} + let storyPlacement = null; const storyRequest = ${JSON.stringify(story ? { eventId: Number(story.eventId), pageIndex: story.pageIndex == null ? null : Number(story.pageIndex), storyId: Number(story.storyId) } : null)}; if (storyRequest) { @@ -279,10 +342,14 @@ const OPS = { const floatMode = ${float ? 'true' : 'false'}; if (floatMode) { ${SAFE_CHAR} - floatChar(p); + ${catalog ? 'motion.enable(p);motion.setNoclip(p,false);' : 'floatChar(p);'} const companion = lilyHere(); if (!sameMap && storyPlacement?.ok && storyPlacement.controller && companion?.locate) companion.locate(p._x, p._y); - floatChar(companion); + ${catalog ? `if(companion){ + const lx=(catalogPlacement.px-companion.tileXToCenterX(0))/m.tileWidth(),ly=(catalogPlacement.py-companion.tileYToCenterY(0))/m.tileHeight(); + Game_CharacterBase.prototype.locate.call(companion,lx,ly);companion.unflush(lx,ly); + motion.enable(companion);motion.setNoclip(companion,false); + }` : 'floatChar(companion);'} } // 检测是否正在播放剧情 / 有事件在跑。 @@ -299,7 +366,8 @@ const OPS = { return JSON.stringify({ ok: storyPlacement?.ok !== false, error: storyPlacement?.error, - positionNote: storyPlacement?.positionNote, + positionNote: catalogPlacement?.positionNote||storyPlacement?.positionNote, + catalogCell:catalogPlacement?.bounds, x: p._x, y: p._y, px: p.px, py: p.py, mapId: m.mapId(), changedMap: fromMap !== m.mapId(), @@ -325,6 +393,22 @@ const OPS = { * 插件指令参数格式实测为 [插件名, 命令名, 显示文本, 参数对象] —— 4 个元素, * 参数在**第 3 位**(rmmz_objects.js:11315 的 command357 取的就是 params[3])。 */ + nudge(dx, dy, pixels = 4) { + if (![dx, dy].every(Number.isInteger) || Math.abs(dx) + Math.abs(dy) !== 1 || !Number.isFinite(pixels) || pixels < 1 || pixels > 96) throw new Error('微调方向或像素步长无效'); + return wrap(` + ${SAFE_CHAR} + const p = $gamePlayer, m = $gameMap; + const x = Math.max(0, Math.min(m.width() - 1, (p.px - p.tileXToCenterX(0) + ${dx * pixels}) / m.tileWidth())); + const y = Math.max(0, Math.min(m.height() - 1, (p.py - p.tileYToCenterY(0) + ${dy * pixels}) / m.tileHeight())); + // Base locate synchronizes physical coordinates without player locate's + // automatic floor search or companion transfer, which defeats tiny steps. + if (typeof Game_CharacterBase !== 'undefined') Game_CharacterBase.prototype.locate.call(p, x, y); + else p.locate(x, y); + p.unflush(x, y); floatChar(p); + const lily = lilyHere(); if (lily) floatChar(lily); + return JSON.stringify({ok:true,x,y,px:p.px,py:p.py}); + `); + }, playStory(storyId) { return wrap(` ${SLEEP} @@ -1116,6 +1200,7 @@ const OPS = { } // 恢复碰撞;锚点设到落点,确保万一出岔子也落在附近而不是旧地图的位置 + window.__nyaruruMotion?.disable(p); p.resumePhysicsHitTest(); p.clearImpact && p.clearImpact(); p._impact = null; @@ -1132,7 +1217,7 @@ const OPS = { const hits = (lily.hitPreview(0, dy) || []).filter(o => o && o.terrain === true); if (hits.length) { safe = !hits.some(o => o.type === 2); break; } } - if (safe) { safeChar(lily); lily.resumeGravity(); lily.resumePhysicsHitTest(); lilyLanded = true; } + if (safe) { motion.disable(lily); safeChar(lily); lily.resumeGravity(); lily.resumePhysicsHitTest(); lilyLanded = true; } else { floatChar(lily); warning = '主角已落地;莉莉娅脚下不安全,仍保持悬浮。请移动到安全地面再点落地'; } } return JSON.stringify({ ok: true, dropDistance: found, lilyLanded, warning }); @@ -1147,7 +1232,7 @@ const OPS = { out[i] = $gameSwitches.value(i) ? 1 : 0; } return JSON.stringify({ ok: true, values: out }); - `); + `, { allowMenu: true }); }, /** 设置单个开关 */ @@ -1178,14 +1263,15 @@ const OPS = { * 并触发坠落复位循环(用户实测反馈过)。 */ setGravity(suspended) { + if (!suspended) return this.land(); const on = suspended ? 'true' : 'false'; return wrap(` ${SAFE_CHAR} const p = $gamePlayer; const lh = lilyAny(); if (${on}) { - floatChar(p); - if (lh) floatChar(lh); + safeChar(p); motion.enable(p); + if (lh) { safeChar(lh); motion.enable(lh); } } else { p.resumeGravity(); p.resumePhysicsHitTest(); @@ -1221,13 +1307,16 @@ const OPS = { return wrap(` ${SAFE_CHAR} const p = $gamePlayer; + if (!${on ? 'true' : 'false'} && (p.hitPreview(0, 0) || []).some(o => o?.terrain === true)) return JSON.stringify({ok:false,error:'当前与地形重叠,请先微调到空处再关闭穿墙'}); + let warning = ''; for (const c of [p, lilyAny()]) { if (!c) continue; safeChar(c); - if (${on ? 'true' : 'false'}) c.suspendPhysicsHitTest(); - else c.resumePhysicsHitTest(); + if (${on ? 'true' : 'false'}) motion.enable(c, true); + else if (c !== p && (c.hitPreview(0,0) || []).some(o => o?.terrain === true)) warning = '莉莉娅与地形重叠,仍保持穿墙悬停'; + else motion.setNoclip(c,false); } - return JSON.stringify({ok:true,noclip:!!p._suspendPhysicsHitTest}); + return JSON.stringify({ok:true,noclip:!!p._suspendPhysicsHitTest,warning}); `); }, @@ -1242,6 +1331,8 @@ const OPS = { const lh = lilyAny(); if (window.__nyaruruPhoto) window.__nyaruruPhoto.unpinAll(); if (window.__nyaruruTrainer) window.__nyaruruTrainer.clear(); + window.__nyaruruCombat?.clear(); + motion.clear(); // 玩家和莉莉娅都要清 —— 只清玩家的话,同伴会继续坠落复位循环 for (const c of [p, lh]) { if (!c) continue; diff --git a/tool/lib/safety.js b/tool/lib/safety.js index d13dfd4..3b4b8a1 100644 --- a/tool/lib/safety.js +++ b/tool/lib/safety.js @@ -20,6 +20,15 @@ function contains(a, b) { } function validateConfig(config, gameDir) { const next = { ...config }; + // 独立工作区可显式指定游戏;旧版放在游戏/aitemp 下的布局仍然可用。 + if (next.gameDir !== undefined) { + next.gameDir = canonical(String(next.gameDir).trim()); + for (const name of ['package.json', 'index.html']) { + const file = path.join(next.gameDir, name); + if (!fs.existsSync(file) || !fs.statSync(file).isFile()) throw new Error('游戏目录不完整,缺少:' + file); + } + gameDir = next.gameDir; + } for (const key of ['cdpPort', 'webPort']) { const n = Number(next[key]); if (!Number.isInteger(n) || n < 1024 || n > 65535) throw new Error(`${key} 必须是 1024~65535 的整数`); diff --git a/tool/lib/trainer.js b/tool/lib/trainer.js index 4ca1e62..29a3846 100644 --- a/tool/lib/trainer.js +++ b/tool/lib/trainer.js @@ -1,6 +1,8 @@ // Injected into the game. Uses NYA's actor inventory/battle values, not RPG Maker HP/gold. function installTrainer() { - if (window.__nyaruruTrainer) return window.__nyaruruTrainer; + if (window.__nyaruruTrainer?.version === 3) return window.__nyaruruTrainer; + if (window.__nyaruruTrainer?.dispose) window.__nyaruruTrainer.dispose(); + else window.__nyaruruTrainer?.clear(); const fields = { coin: '_coin', hp: '_hp', sp: 'sp', zp: 'zp' }; const locks = new Map(); const ids = new WeakMap(); @@ -54,7 +56,7 @@ function installTrainer() { } function touch() { sync(); touched = Date.now(); } // Only one timer per game page; neither hooks nor lock settings enter save data. - setInterval(() => { try { sync(); } catch { clear(); } }, 100); + const timer = setInterval(() => { try { sync(); } catch { clear(); } }, 100); const categories = ['consumables', 'weapons', 'stones', 'ornaments']; const specials = { @@ -87,7 +89,67 @@ function installTrainer() { const data = actor[dataMethod](); items.push({ type, id: 0, name: localized(data.name), desc: localized(data.desc), count: actor[countMethod](), max: 99999, equipped: false }); } - return { ok: true, subject, role: $gamePlayer.isLilyMode() ? '莉莉娅' : actor.name(), stats, items }; + if (actor._ornaments.bellData) { + const data = actor._ornaments.bellData(), fromMap = actor._ornaments.eventBellGainNum(); + items.push({ type: 'bell', id: 0, name: localized(data.name), desc: localized(data.desc) + ' 默认保留地图铃铛;允许重置收集点时可减少到 0。', + count: actor.bellNum(), min: fromMap, max: 25, equipped: false }); + } + const capacity = actor._ornaments.maxEquip ? { used: actor._ornaments.hasEquipNum(), max: actor._ornaments.maxEquip(), + mapBells: actor._ornaments.eventBellGainNum(), extraBells: actor._ornaments._hasBuyBellNum } : null; + return { ok: true, subject, role: $gamePlayer.isLilyMode() ? '莉莉娅' : actor.name(), stats, items, capacity }; + } + function validateItem(actor, body) { + const { type } = body, itemId = integer(body.id, 99999); + if (type === 'bell') { + if (itemId !== 0 || !actor._ornaments.bellData) throw new Error('铃铛数据不可用'); + const count = integer(body.count, 25), minimum = actor._ornaments.eventBellGainNum(); + if (count < minimum) { + if (body.adjustMapBells !== true) throw new Error(`已从地图取得 ${minimum} 个铃铛;勾选允许重置地图铃铛后可继续减少`); + const keys = typeof DataManager !== 'undefined' && DataManager._cacheBell; + if (!Array.isArray(keys) || keys.filter(key => $gameSelfSwitches.value(key)).length !== minimum) throw new Error('地图铃铛记录不完整,不能减少'); + } + return; + } + if (Object.prototype.hasOwnProperty.call(specials, type)) { + if (itemId !== 0) throw new Error('物品编号无效'); + integer(body.count, 99999); return; + } + if (!categories.includes(type)) throw new Error('物品分类无效'); + const container = actor['_' + type], data = container.data(); + if (!Object.prototype.hasOwnProperty.call(data, itemId) || !data[itemId]) throw new Error('物品不存在'); + integer(body.count, type === 'consumables' ? (body.overCap === true ? 99999 : container.maxNum(itemId)) : 1); + } + function applyItem(actor, body) { + const { type, id: itemId, count } = body; + if (type === 'bell') { + const o = actor._ornaments; + if (count < o.eventBellGainNum()) { + const acquired = DataManager._cacheBell.filter(key => $gameSelfSwitches.value(key)); + // Remove the highest map/event keys first for deterministic results. + const ordered = [...acquired].sort((a,b)=>a[0]-b[0] || a[1]-b[1]); + for (const key of ordered.slice(count)) $gameSelfSwitches.setValue(key, false); + } + o._hasBuyBellNum = count - o.eventBellGainNum(); + while (o.hasEquipNum() > o.maxEquip()) { + const removable = [...o._equip].reverse().find(id => id !== 20); + if (removable === undefined) break; + o.unequip(removable); + } + } else if (Object.prototype.hasOwnProperty.call(specials, type)) actor[specials[type][2]] = count; + else { + const container = actor['_' + type]; + if (type === 'consumables') container.add(itemId, count - container.itemNum(itemId), body.overCap === true); + else if (count) container.unlock(itemId); + else if (type === 'weapons') { + const remaining = container._unlock.filter(n => n !== itemId); + if (container.currentWeapon() === itemId) { actor.changeWeapon(remaining[0]); $gamePlayer.setWeaponChange(); } + container._unlock = remaining; + } else if (type === 'ornaments') container.remove(itemId); + else { + if (container.isEquip(itemId)) container.equip(itemId); + container._unlock = container._unlock.filter(n => n !== itemId); + } + } } function change(body) { touch(); @@ -114,35 +176,55 @@ function installTrainer() { locks.set(field, { field, target: value, object: null }); try { sync(); } catch (e) { clear(); throw e; } } - } else if (body.action === 'item') { - const { type } = body; - const itemId = integer(body.id, 99999); - if (Object.prototype.hasOwnProperty.call(specials, type)) { - if (itemId !== 0) throw new Error('物品编号无效'); - actor[specials[type][2]] = integer(body.count, 99999); - } else { - if (!categories.includes(type)) throw new Error('物品分类无效'); - const container = actor['_' + type], data = container.data(); - if (!Object.prototype.hasOwnProperty.call(data, itemId) || !data[itemId]) throw new Error('物品不存在'); - const max = type === 'consumables' ? (body.overCap === true ? 99999 : container.maxNum(itemId)) : 1; - const count = integer(body.count, max); - if (type === 'consumables') container.add(itemId, count - container.itemNum(itemId), body.overCap === true); - else if (count) container.unlock(itemId); - else if (type === 'weapons') { - const remaining = container._unlock.filter(n => n !== itemId); - if (!remaining.length) throw new Error('至少需要保留一件武器,避免游戏攻击时找不到武器'); - if (container.currentWeapon() === itemId) { actor.changeWeapon(remaining[0]); $gamePlayer.setWeaponChange(); } - container._unlock = remaining; - } else if (type === 'ornaments') container.remove(itemId); - else { - if (container.isEquip(itemId)) container.equip(itemId); - container._unlock = container._unlock.filter(n => n !== itemId); + } else if (body.action === 'equip') { + const { type, id: itemId } = body; + if (!['weapons','stones','ornaments'].includes(type) || typeof body.equipped !== 'boolean') throw new Error('装备参数无效'); + integer(itemId, 99999); + const container = actor['_' + type]; + if (!Object.hasOwn(container.data(), itemId) || !container.isUnlock(itemId)) throw new Error('需要先获得该物品'); + if (type === 'weapons') { if (!body.equipped) throw new Error('当前武器不能卸为空'); actor.changeWeapon(itemId); $gamePlayer.setWeaponChange(); } + else if (type === 'stones') { if (container.isEquip(itemId) !== body.equipped) container.equip(itemId); } + else if (!body.equipped) container.unequip(itemId); + else if (!container.isEquip(itemId) && !container.equip(itemId)) throw new Error('配饰栏位不足,请增加铃铛或先卸下其他配饰'); + } else if (body.action === 'batch' || body.action === 'item') { + const changes = body.action === 'item' ? [body] : body.changes; + if (!Array.isArray(changes) || !changes.length || changes.length > 100) throw new Error('批量物品列表无效'); + const seen = new Set(); + for (const item of changes) { + if (!item || typeof item !== 'object') throw new Error('物品参数无效'); + validateItem(actor, item); + const key = item.type + ':' + item.id; + if (seen.has(key)) throw new Error('批量列表含重复物品'); + seen.add(key); + } + const weapons = new Set(actor._weapons._unlock); + for (const item of changes) if (item.type === 'weapons') { if (item.count) weapons.add(item.id); else weapons.delete(item.id); } + if (!weapons.size) throw new Error('至少需要保留一件武器,避免游戏攻击时找不到武器'); + const objects = categories.map(type => actor['_' + type]); + const snapshots = objects.map(object => Object.fromEntries(Reflect.ownKeys(object).map(key => { + const d = Object.getOwnPropertyDescriptor(object, key); + if ('value' in d && d.value && typeof d.value === 'object') d.value = Array.isArray(d.value) ? [...d.value] : { ...d.value }; + return [key, d]; + }))); + const materialKeys = Object.values(specials).map(v => v[2]); + const materials = materialKeys.map(key => actor[key]); + const bellFlags = changes.some(i=>i.type==='bell' && i.adjustMapBells === true) ? DataManager._cacheBell.map(key=>[key,$gameSelfSwitches.value(key)]) : []; + try { + const ordered = [...changes].sort((a,b) => Number(b.type === 'weapons' && b.count === 1) - Number(a.type === 'weapons' && a.count === 1)); + for (const item of ordered) applyItem(actor, item); + } catch (e) { + for (let i = 0; i < objects.length; i++) { + for (const key of Reflect.ownKeys(objects[i])) if (!Object.hasOwn(snapshots[i], key)) delete objects[i][key]; + Object.defineProperties(objects[i], snapshots[i]); } + materialKeys.forEach((key, i) => { actor[key] = materials[i]; }); + for (const [key,value] of bellFlags) $gameSelfSwitches.setValue(key,value); + throw e; } } else throw new Error('未知修改操作'); return read(); } - return window.__nyaruruTrainer = { read, change, touch, clear }; + return window.__nyaruruTrainer = { version: 3, read, change, touch, clear, dispose() { clear(); if (typeof clearInterval === 'function') clearInterval(timer); } }; } module.exports = { installTrainer }; diff --git a/tool/lib/world-map.js b/tool/lib/world-map.js new file mode 100644 index 0000000..b943061 --- /dev/null +++ b/tool/lib/world-map.js @@ -0,0 +1,45 @@ +// Evaluated in the game: read original schematic cells, never activate exploration. +function readWorldMap(encyclopedia) { + const source = Object.values(DataManager._thumbnailMapCell || {}); + if (!source.length) throw Error('世界地图资料尚未加载,请先正常读档'); + const keyOf = c => `${c.page}:${c.x}:${c.y}`; + const cells = source.map(c => ({ key: keyOf(c), page: c.page, regionId: c.regionId, mapId: c.mapId, + x: c.x, y: c.y, hidden: !!c.hidden, + mapName: $dataMapInfos?.[c.mapId]?.name || '地图 ' + c.mapId, + explored: !!$gameSystem.isThumbnailActive(DataManager.makePosKey(c.x,c.y)), + walls: { left:!!c.l, top:!!c.t, right:!!c.r, bottom:!!c.b }, + exits: { left:!!c.pl, top:!!c.pt, right:!!c.pr, bottom:!!c.pb } })); + const byCell = new Map(cells.map(c => [c.key,c])); + const definitions = new Map(encyclopedia.items.map(i => [i.type + ':' + i.id,i])); + const markers = [], unmapped = []; + for (const location of encyclopedia.locations) { + const c = DataManager.getThumbnailCell(location.x,location.y,location.mapId); + const cell = c && byCell.get(keyOf(c)); + if (!cell || cell.mapId !== location.mapId) { unmapped.push(location.key); continue; } + const iconKey = location.type + ':' + location.id, item = definitions.get(iconKey); + markers.push({ ...location, cellKey:cell.key, page:cell.page, regionId:cell.regionId, + name:item?.name || iconKey, desc:item?.desc || '', iconKey }); + } + const official = new Set(); + for (const region of Object.values($dataRegion || {})) + for (const [mapId,eventId] of region?.exploreItem || []) official.add(mapId + ':' + eventId); + const mapped = new Set(markers.filter(m=>m.official).map(m=>m.mapId + ':' + m.eventId)); + const regions = [...new Set(cells.map(c=>c.regionId))].sort((a,b)=>a-b).map(id=>({id, + name:$dataRegion?.[id]?.name || '区域 ' + id, + color:'#' + (DataManager.regionColor?.(id) || 0).toString(16).padStart(6,'0')})); + const pages = [...new Set(cells.map(c=>c.page))].sort((a,b)=>a-b).map(id=>{ + const group=cells.filter(c=>c.page===id); + return {id,label:'页面 ' + id,regionIds:[...new Set(group.map(c=>c.regionId))], + minX:Math.min(...group.map(c=>c.x)),maxX:Math.max(...group.map(c=>c.x)), + minY:Math.min(...group.map(c=>c.y)),maxY:Math.max(...group.map(c=>c.y))}; + }); + const current=DataManager.getCurrentThumbnailCell(); + const playerCell=current && byCell.get(keyOf(current)); + const player=playerCell ? {cellKey:playerCell.key,page:playerCell.page,regionId:playerCell.regionId, + mapId:$gameMap.mapId(),x:$gamePlayer._x,y:$gamePlayer._y} : {cellKey:null,mapId:$gameMap.mapId()}; + const missing=[...official].filter(k=>!mapped.has(k)), mappedOfficial=official.size-missing.length; + return {ok:true,regions,pages,cells,markers,player,coverage:{cells:cells.length,markers:markers.length, + officialTotal:official.size,mappedOfficial,officialMapped:mappedOfficial,missing,missingOfficial:missing,unmapped, + message:`原版收集点已映射 ${mappedOfficial}/${official.size};地图 ${cells.length} 格`}}; +} +module.exports={readWorldMap}; diff --git a/tool/package-release.js b/tool/package-release.js new file mode 100644 index 0000000..c837983 --- /dev/null +++ b/tool/package-release.js @@ -0,0 +1,41 @@ +// Explicit allow-list packaging. Never copy the working tree recursively. +const fs = require('node:fs'); +const path = require('node:path'); +const crypto = require('node:crypto'); +const ROOT = path.resolve(__dirname, '..'); + +function manifestFiles(root = ROOT) { + const manifest = JSON.parse(fs.readFileSync(path.join(root, 'tool/发布清单.json'), 'utf8')); + const files = manifest.files; + if (!Array.isArray(files) || new Set(files).size !== files.length) throw Error('发布清单无效或包含重复文件'); + for (const file of files) { + if (typeof file !== 'string' || file.includes('\\') || path.posix.isAbsolute(file) || file.split('/').some(p => !p || p === '.' || p === '..')) throw Error('发布路径无效:' + file); + if (/^(?:out|unpacked)\//.test(file) && !['out/triggers.csv', 'unpacked/data/System.json', 'unpacked/data/MapInfos.json'].includes(file)) throw Error('原始游戏数据不能打包:' + file); + if (/(?:^|\/)(?:\.git|config\.json|\.server-runtime\.json|backups|save|probe|docs\/历史|nwjs-sdk[^/]*)(?:\/|$)|\.(?:rmmzsave|bin|log|zip)$/i.test(file)) throw Error('私人或运行时文件不能打包:' + file); + let current = root; + for (const part of file.split('/')) { current = path.join(current, part); if (fs.lstatSync(current).isSymbolicLink()) throw Error('不能打包链接:' + file); } + if (!fs.statSync(current).isFile()) throw Error('发布项不是文件:' + file); + } + return files; +} + +function buildRelease(destination = path.join(ROOT, 'dist', 'Nyaruru_modifier'), root = ROOT) { + destination = path.resolve(destination); + const files = manifestFiles(root); + if (fs.existsSync(destination)) throw Error('输出目录已存在,请使用新的空路径:' + destination); + fs.mkdirSync(destination, { recursive: true }); + const hashes = []; + for (const file of files) { + const target = path.join(destination, file), data = fs.readFileSync(path.join(root, file)); + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.writeFileSync(target, data, { flag: 'wx' }); + hashes.push({ file, bytes: data.length, sha256: crypto.createHash('sha256').update(data).digest('hex') }); + } + return { destination, files: hashes }; +} + +if (require.main === module) { + try { const result = buildRelease(process.argv[2]); console.log(JSON.stringify(result, null, 2)); } + catch (error) { console.error(error.message); process.exitCode = 1; } +} +module.exports = { buildRelease, manifestFiles }; diff --git a/tool/server.js b/tool/server.js index 2784cec..c96ccaf 100644 --- a/tool/server.js +++ b/tool/server.js @@ -25,7 +25,7 @@ const { assertGamesStopped } = require('./lib/processes'); // ---------- 路径与配置 ---------- const TOOL_DIR = __dirname; const AITEMP_DIR = path.resolve(TOOL_DIR, '..'); // aitemp/ -const GAME_DIR = path.resolve(AITEMP_DIR, '..'); // 游戏根目录 +const LEGACY_GAME_DIR = path.resolve(AITEMP_DIR, '..'); // 旧布局的游戏根目录 const WEB_DIR = path.join(TOOL_DIR, 'web'); const BACKUP_DIR = path.join(AITEMP_DIR, 'backups'); const CONFIG_FILE = path.join(TOOL_DIR, 'config.json'); @@ -33,7 +33,7 @@ const CONFIG_FILE = path.join(TOOL_DIR, 'config.json'); // 沙盒默认位置:和游戏同一个盘的根目录下。 // ⚠️ 不要写死具体盘符 —— 别人的游戏不一定装在 L 盘。 // 放在盘根是为了**避开 Steam 库**(放库里 Steam 会扫到,云同步也可能够得着)。 -const DEFAULT_SANDBOX = path.join(path.parse(GAME_DIR).root, 'Nyaruru_shots'); +const DEFAULT_SANDBOX = path.join(path.parse(LEGACY_GAME_DIR).root, 'Nyaruru_shots'); const DEFAULTS = { cdpPort: 9223, // 沙盒实例的调试端口 @@ -41,8 +41,9 @@ const DEFAULTS = { webPort: 8787, }; -function loadConfig() { return SAFETY.readConfig(CONFIG_FILE, DEFAULTS, GAME_DIR); } +function loadConfig() { return SAFETY.readConfig(CONFIG_FILE, DEFAULTS, LEGACY_GAME_DIR); } const CONFIG = loadConfig(); +const GAME_DIR = CONFIG.gameDir || LEGACY_GAME_DIR; function classifyGameDir(dir) { if (!dir) return 'unknown'; try { @@ -108,7 +109,7 @@ function targetDir(body) { } let operationTail = Promise.resolve(); const offlineActions = new Set(['/api/config', '/api/backup', '/api/restore-backup', '/api/sandbox', '/api/shutdown', '/api/story-text']); -const getRoutes = new Set(['/api/trainer', '/api/status', '/api/snapshot', '/api/maps', '/api/switches', '/api/config', '/api/ground', '/api/events/lost', '/api/events', '/api/triggers', '/api/backups', '/api/sandbox/status', '/api/health', '/api/story-text']); +const getRoutes = new Set(['/api/status', '/api/snapshot', '/api/trainer', '/api/item-icons', '/api/encyclopedia', '/api/world-map', '/api/combat', '/api/maps', '/api/switches', '/api/config', '/api/ground', '/api/events/lost', '/api/events', '/api/triggers', '/api/backups', '/api/sandbox/status', '/api/health', '/api/story-text']); async function identity() { const r = await cdp.evalJson(OPS.selfPath()); if (!r?.ok) throw new Error(r?.error || '无法识别游戏实例'); @@ -172,6 +173,12 @@ async function executeApi(req, res, urlPath, body) { return cdp.evalJson(current ? guarded(script, current.mapToken) : script); }; switch (urlPath) { + case '/api/encyclopedia/travel':return sendJson(res,await game(OPS.catalogTravel(body.mapId,body.eventId))); + case '/api/world-map/travel':return sendJson(res,await game(OPS.worldMapTravel(body.mapId,body.cellX,body.cellY,body.page))); + case '/api/combat': { + if(req.method==='GET')current=await identity(); + const r=await game(OPS.combat(req.method==='POST'?body:null));return sendJson(res,{...r,context:current.context}); + } case '/api/shutdown': { if (sandboxJob.running) throw new Error('沙盒仍在复制,请等待任务结束再停止面板'); sendJson(res, { ok: true }); @@ -191,7 +198,7 @@ async function executeApi(req, res, urlPath, body) { // 顺手问一句:游戏窗口自己是从哪个目录加载的? // 面板据此显示"当前连接:沙盒 / 原版",把"以为在沙盒、其实改了原版"这种 - // 误用摆在明面上(见 复查与隐患.md 坑 5)。 + // 将实际实例身份显示给用户;约定见 tool/开发文档.md。 let gameSelf = null; if (connected) { try { gameSelf = await identity(); } catch { connected = false; } } if (!connected) { @@ -215,6 +222,24 @@ async function executeApi(req, res, urlPath, body) { } // --- 游戏状态快照 --- + case '/api/world-map': { + if (req.method !== 'GET') throw new Error('世界地图为只读功能'); + const before = await identity(); + const r = await cdp.evalJson(guarded(OPS.worldMap(), before.mapToken)); + return sendJson(res, { ...r, context: before.context }); + } + case '/api/encyclopedia': { + if (req.method !== 'GET') throw new Error('百科为只读功能'); + const before = await identity(); + const r = await cdp.evalJson(guarded(OPS.encyclopedia(), before.mapToken)); + return sendJson(res, { ...r, context: before.context }); + } + case '/api/item-icons': { + if (req.method !== 'GET') throw new Error('图标为只读功能'); + const before = await identity(); + const r = await cdp.evalJson(guarded(OPS.itemIcons(), before.mapToken)); + return sendJson(res, { ...r, context:before.context }); + } case '/api/trainer': { if (req.method === 'GET') current = await identity(); const r = await game(OPS.trainer(req.method === 'POST' ? body : null)); @@ -264,6 +289,7 @@ async function executeApi(req, res, urlPath, body) { })); return sendJson(res, r); } + case '/api/nudge': return sendJson(res, await game(OPS.nudge(body.dx, body.dy, body.pixels))); // --- 直接播放一段剧情序列 --- // 绕开地图事件与开关条件,用于通关存档上复现"错过就没了"的对话 @@ -345,7 +371,7 @@ async function executeApi(req, res, urlPath, body) { case '/api/restore': return sendJson(res, await game(OPS.restoreAll())); // --- 场景还原(摆拍):扫描本图丢失的角色 → 强行切页让它们出现 --- - // 静态摆拍与撤销由 photo-state 管理;自动和手动存档照常,仅提示风险。 + // 静态摆拍与撤销由 photo-state 管理,自动和手动存档照常,仅提示风险。 case '/api/events/lost': { const before = await identity(); const r = await cdp.evalJson(guarded(OPS.scanLostEvents(), before.mapToken)); diff --git a/tool/tests/catalog-position.test.js b/tool/tests/catalog-position.test.js new file mode 100644 index 0000000..2fc6151 --- /dev/null +++ b/tool/tests/catalog-position.test.js @@ -0,0 +1,103 @@ +const test=require('node:test'); +const assert=require('node:assert/strict'); +const vm=require('node:vm'); +const {catalogCellBounds,findCatalogLanding}=require('../lib/catalog-position'); +const {OPS}=require('../lib/ops'); + +test('catalog cell bounds use the actual submap cell and coordinate offsets',()=>{ + const thumbnail={minX:35,maxX:35,minY:32,maxY:35,data:[{x:35,y:32},{x:35,y:33},{x:35,y:34},{x:35,y:35}]}; + assert.deepEqual(catalogCellBounds({width:20,height:64,meta:{}},thumbnail,{x:35,y:34}),{left:0,top:32,right:20,bottom:48,cellX:35,cellY:34}); + assert.deepEqual(catalogCellBounds({width:20,height:64,meta:{cellOx:2,cellOy:3}},thumbnail,{x:35,y:34}),{left:0,top:29,right:18,bottom:45,cellX:35,cellY:34}); + assert.throws(()=>catalogCellBounds({},thumbnail,{x:99,y:99}),/分块信息/); +}); + +test('catalog landing checks the whole body, avoids walls and refines standing height above thin ground',()=>{ + const bounds={left:0,top:12,right:18,bottom:24},shape={left:20,right:20,top:80,bottom:20}; + const floorY=23*96+7; + const hitsAt=(px,py)=>px-shape.left<96?[{terrain:true,type:0}]:py+shape.bottom>=floorY?[{terrain:true,type:0}]:[]; + const pick=findCatalogLanding({bounds,shape,tileWidth:96,tileHeight:96,hitsAt}); + assert.equal(pick.grounded,true);assert.equal(hitsAt(pick.px,pick.py).length,0); + assert.ok(pick.py+shape.bottom=96);assert.ok(pick.py-shape.top>=bounds.top*96); + assert.ok(pick.py+shape.bottom<=bounds.bottom*96); +}); + +test('catalog landing refuses blocked or undersized cells and bounds its search',()=>{ + const args={bounds:{left:0,top:0,right:4,bottom:4},shape:{left:20,right:20,top:80,bottom:20},tileWidth:96,tileHeight:96}; + assert.equal(findCatalogLanding({...args,hitsAt:()=>[{terrain:true,type:0}]}),null); + assert.equal(findCatalogLanding({...args,bounds:{left:0,top:0,right:1,bottom:0.5},hitsAt:()=>[]}),null); + const limited=findCatalogLanding({...args,maxProbes:2,hitsAt:()=>[]});assert.equal(limited.grounded,false);assert.ok(limited.probes<=3); + const floating=findCatalogLanding({...args,hitsAt:()=>[]});assert.equal(floating.grounded,false); + const hazardous=findCatalogLanding({...args,hitsAt:()=>[{terrain:true,type:2}]});assert.equal(hazardous,null); +}); + +function runtime({blocked=false,blockAfterTransfer=false,blockedTargetCell=false}={}){ + class Character{ + constructor(){Object.assign(this,{px:500,py:500,_vx:0,_vy:0,dotX:0,dotY:0,_direction:6});} + hitOffsetL(){return this._hitOffsetL??24;}hitOffsetR(){return this._hitOffsetR??24;} + hitOffsetT(){return this._hitOffsetT??72;}hitOffsetB(){return this._hitOffsetB??16;} + tileXToCenterX(x){return x*96+48;}tileYToCenterY(y){return y*96+80;} + locate(x,y){this._x=x;this._y=y;this.unflush(x,y);}unflush(x,y){this.px=this.tileXToCenterX(x);this.py=this.tileYToCenterY(y);} + suspendGravity(){this._suspendGravity=true;}resumeGravity(){this._suspendGravity=false;} + suspendPhysicsHitTest(){this._suspendPhysicsHitTest=true;}resumePhysicsHitTest(){this._suspendPhysicsHitTest=false;} + isSuspendGravity(){return !!this._suspendGravity;}clearImpact(){}clearGrounding(){} + doGravity(){}makeMomentum(){}cacheDeltaTimeSpeed(){}isRemove(){return false;} + } + class Grid{ + rebuild(){} + getObjGrid(p,hits){if(this.blocked||(this.blockedTargetCell&&p.px<10*96)||p.py+p.hitOffsetB()>=14*96)hits.push({terrain:true,type:0});} + } + class Map{ + mapId(){return this._mapId;}tileWidth(){return 96;}tileHeight(){return 96;}width(){return 20;}height(){return 16;} + tilemapTerrain(){}isAnyEventStarting(){return false;} + } + const p=new Character(),map=new Map(),terrain=new Grid();map._mapId=1;map._terrain=terrain;terrain.blocked=blocked;terrain.blockedTargetCell=blockedTargetCell; + const data={width:20,height:16,meta:{},events:[null,{id:1,x:10,y:10}]}; + const context=vm.createContext({window:{},Game_CharacterBase:Character,Game_Map:Map,PhysicsGrid:Grid, + $gamePlayer:p,$gameLily:null,$gameActors:{},$gameMap:map,$dataMap:data,$dataMapInfos:[null,{},{}], + SceneManager:{_scene:{constructor:{name:'Scene_Map'}}},setTimeout:fn=>fn(), + DataManager:{getCachedData:()=>data,getThumbnailCell:(x,y,id)=>({x:0,y:0,mapId:id}), + getThumbnailMapInfo:()=>({minX:0,maxX:blockedTargetCell?1:0,minY:0,maxY:0,data:blockedTargetCell?[{x:0,y:0},{x:1,y:0}]:[{x:0,y:0}]})}}); + const transfers=[]; + p.reserveTransfer=(id,x,y)=>{transfers.push(id);map._mapId=id;map._terrain=new Grid();map._terrain.blocked=id===2&&blockAfterTransfer;context.$dataMap=data;p._x=x;p._y=y;}; + p.isTransferring=()=>false; + p.locate=()=>{throw Error('Catalog must use base locate, without the native player grounding adjustment');}; + return{context,p,map,transfers}; +} + +test('catalog travel validates before moving and arrives with hover and collision',async()=>{ + const f=runtime();const r=JSON.parse(await vm.runInContext(OPS.catalogTravel(1,1),f.context)); + assert.equal(r.ok,true);assert.equal(r.gravitySuspended,true);assert.equal(r.noclip,false); + assert.equal(r.catalogCell.cellX,0);assert.ok(f.p.py+f.p.hitOffsetB()<14*96); + const bad=runtime({blocked:true}),before=[bad.p.px,bad.p.py]; + const refused=JSON.parse(await vm.runInContext(OPS.catalogTravel(1,1),bad.context)); + assert.equal(refused.ok,false);assert.deepEqual([bad.p.px,bad.p.py],before);assert.deepEqual(bad.transfers,[]); +}); + +test('a blocked target cell expands to another cell in the same map and still teleports',async()=>{ + const f=runtime({blockedTargetCell:true}); + const r=JSON.parse(await vm.runInContext(OPS.catalogTravel(1,1),f.context)); + assert.equal(r.ok,true);assert.equal(r.catalogCell.cellX,1);assert.ok(f.p.px>10*96); + assert.match(r.positionNote,/同图其他可用位置/);assert.equal(r.noclip,false); +}); + +test('catalog travel rechecks the loaded destination and returns on newly blocked geometry',async()=>{ + const f=runtime({blockAfterTransfer:true}),before=[f.p.px,f.p.py]; + const r=JSON.parse(await vm.runInContext(OPS.catalogTravel(2,1),f.context)); + assert.equal(r.ok,false);assert.match(r.error,/返回原位置/); + assert.deepEqual(f.transfers,[2,1]);assert.equal(f.map.mapId(),1);assert.deepEqual([f.p.px,f.p.py],before); +}); + +test('world cell travel validates map and page before moving, and supports empty cells with safe fallback',async()=>{ + assert.throws(()=>OPS.worldMapTravel(1,0,0,'1'),/坐标无效/); + const f=runtime({blockedTargetCell:true}); + f.context.DataManager._thumbnailMapCell={a:{mapId:1,x:0,y:0,page:3},b:{mapId:1,x:1,y:0,page:3}}; + const before=[f.p.px,f.p.py]; + for(const args of [[1,0,0,4],[2,0,0,3],[1,99,0,3]]){ + const r=JSON.parse(await vm.runInContext(OPS.worldMapTravel(...args),f.context)); + assert.equal(r.ok,false);assert.match(r.error,/不属于/);assert.deepEqual([f.p.px,f.p.py],before); + } + assert.deepEqual(f.transfers,[]); + const r=JSON.parse(await vm.runInContext(OPS.worldMapTravel(1,0,0,3),f.context)); + assert.equal(r.ok,true);assert.equal(r.catalogCell.cellX,1);assert.equal(r.noclip,false);assert.equal(r.gravitySuspended,true); +}); diff --git a/tool/tests/combat.test.js b/tool/tests/combat.test.js new file mode 100644 index 0000000..3c18b6b --- /dev/null +++ b/tool/tests/combat.test.js @@ -0,0 +1,22 @@ +const test=require('node:test'),assert=require('node:assert/strict'),vm=require('node:vm'); +const {installCombat}=require('../lib/combat'); +test('combat scales player attacks and incoming damage, supports bullets and respects healing and scripted immunity',()=>{ + function Character(team){this.battle={team,hp:100,unlimitHp:false};} + Character.prototype.battleObject=function(){return this.battle;}; + Character.prototype.overrideBeAttackDamage=function(d){return d;}; + Character.prototype.onAttackHit=function(info,target){const d=target.overrideBeAttackDamage(info.damage);if(!target.battle.unlimitHp)target.battle.hp-=d;return d;}; + Character.prototype.requestDamage=function(d){this.battle.hp-=d;}; + const p=new Character(0),lily=new Character(0),enemy=new Character(1),other=new Character(1),timers=[]; + const c=vm.createContext({window:{},Game_CharacterBase:Character,$gamePlayer:p,$gameLily:lily,$gameActors:{},setInterval:fn=>timers.push(fn)}); + const api=vm.runInContext(`(${installCombat.toString()})()`,c); + // The real Player and Event classes override the base method independently. + enemy.overrideBeAttackDamage=function(d){return d;};p.overrideBeAttackDamage=function(d){return d;}; + api.change({outgoing:3,incoming:0.5});assert.equal(p.onAttackHit({damage:10},enemy),30); + assert.equal(enemy.onAttackHit({damage:10},p),5);p.requestDamage(10);assert.equal(p.battle.hp,90); + assert.equal(other.onAttackHit({damage:10,bulletOwner:p},enemy),30); + assert.equal(enemy.onAttackHit({damage:-10},p),-10); + api.change({oneHit:true});p.onAttackHit({damage:1},enemy);assert.ok(enemy.battle.hp<=0); + enemy.battle.hp=100;enemy.battle.unlimitHp=true;p.onAttackHit({damage:1},enemy);assert.equal(enemy.battle.hp,100); + assert.throws(()=>api.change({outgoing:Infinity}));assert.throws(()=>api.change({incoming:-1})); + c.$gameActors={};api.read();assert.equal(api.read().outgoing,1);assert.equal(api.read().oneHit,false); +}); diff --git a/tool/tests/encyclopedia-ui.test.js b/tool/tests/encyclopedia-ui.test.js new file mode 100644 index 0000000..dc4307c --- /dev/null +++ b/tool/tests/encyclopedia-ui.test.js @@ -0,0 +1,49 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const vm = require('node:vm'); + +function fixture() { + const source=fs.readFileSync(path.join(__dirname,'../web/app.js'),'utf8'); + const start=source.indexOf('// ---------- 收集百科 ----------'),end=source.indexOf('// ---------- 标签页 ----------',start); + assert.ok(start>=0&&end>start); + const a={key:'ornaments:0:14:1:0:9',type:'ornaments',id:0,mapId:14,mapName:'MAP014',area:'森林', + eventId:1,pageIndex:0,x:9,y:22,collected:false,conditions:[]}; + const b={...a,key:'ornaments:0:88:2:0:9',mapId:88,mapName:'MAP088',area:'城堡',eventId:2,x:0,y:0}; + // HTTP JSON duplicates shared objects; item.locations and locations no longer + // contain identical object references even though their keys still match. + const encyclopedia=JSON.parse(JSON.stringify({context:'current',locations:[a,b],shops:[{...a,x:5,y:6}], + items:[{type:'ornaments',id:0,name:'挂件',desc:'效果',count:0,shopCandidate:true,price:100,locations:[a,b]}]})); + const elements=new Map(),calls=[],messages=[];let refreshed=0; + const $=key=>{if(!elements.has(key))elements.set(key,{value:'',innerHTML:'',replaceChildren(){}});return elements.get(key);}; + const context=vm.createContext({$,ENCYCLOPEDIA:encyclopedia,ENCYCLOPEDIA_BUSY:false,ENCYCLOPEDIA_REVISION:0, + CURRENT_CONTEXT:'current',CURRENT_GAME:{inMap:true},ITEM_TYPES:{ornaments:'配饰'},esc:v=>String(v??''), + api:async(route,body)=>{calls.push({route,body:JSON.parse(JSON.stringify(body))});return{ok:true,positionNote:'目标分块边缘附近的可落脚空处'};}, + toast:(message,kind)=>messages.push({message,kind}),refreshStatus:()=>refreshed++}); + vm.runInContext(source.slice(start,end),context); + const click=(attribute,value)=>$('#encyclopedia-items').onclick({target:{closest:()=>({ + hasAttribute:key=>key===attribute,dataset:{location:value,shop:value}})}}); + return{context,$,calls,messages,encyclopedia,click,refreshed:()=>refreshed}; +} + +test('encyclopedia buttons resolve JSON-loaded and filtered locations by stable key',async()=>{ + const f=fixture();assert.equal(f.encyclopedia.locations.indexOf(f.encyclopedia.items[0].locations[0]),-1); + f.$('#encyclopedia-area').value='城堡';vm.runInContext('renderEncyclopedia()',f.context); + const html=f.$('#encyclopedia-items').innerHTML; + assert.match(html,/data-location="ornaments:0:88:2:0:9"/);assert.doesNotMatch(html,/data-location="-1"|data-location="ornaments:0:14/); + await f.click('data-location','ornaments:0:88:2:0:9'); + assert.deepEqual(f.calls,[{route:'/encyclopedia/travel',body:{mapId:88,eventId:2}}]); + assert.match(f.messages[0].message,/MAP088.*分块边缘.*碰撞已恢复/);assert.equal(f.refreshed(),1); + await f.click('data-location','ornaments:0:14:1:0:9'); + assert.deepEqual(f.calls[1],{route:'/encyclopedia/travel',body:{mapId:14,eventId:1}}); +}); + +test('encyclopedia still rejects stale locations and menu movement; shop travel keeps working',async()=>{ + const f=fixture();f.context.CURRENT_CONTEXT='another-map'; + await f.click('data-location','ornaments:0:14:1:0:9');assert.equal(f.calls.length,0);assert.match(f.messages.at(-1).message,/刷新百科/); + f.context.CURRENT_CONTEXT='current';f.context.CURRENT_GAME.inMap=false; + await f.click('data-location','ornaments:0:14:1:0:9');assert.equal(f.calls.length,0);assert.match(f.messages.at(-1).message,/关闭游戏菜单/); + f.context.CURRENT_GAME.inMap=true;await f.click('data-location','missing');assert.equal(f.calls.length,0); + await f.click('data-shop','0');assert.deepEqual(f.calls,[{route:'/encyclopedia/travel',body:{mapId:14,eventId:1}}]); +}); diff --git a/tool/tests/encyclopedia.test.js b/tool/tests/encyclopedia.test.js new file mode 100644 index 0000000..957f1d8 --- /dev/null +++ b/tool/tests/encyclopedia.test.js @@ -0,0 +1,30 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const vm = require('node:vm'); +const { readEncyclopedia } = require('../lib/encyclopedia'); + +test('encyclopedia separates ownership from collected flags and never guesses branch rewards', () => { + const flags = {'14,1,A':true}; + const add=(type,idx,indent=0)=>({code:357,indent,parameters:['NYA.Utils','addItem','',{type,idx:String(idx),antiCollection:'false'}]}); + const event=(id,list,conditions={})=>({id,name:'pickup',x:id+1,y:4,pages:[{conditions,list}]}); + const map={events:[null,event(1,[add('hpst',0)]),event(2,[add('hpst',0)]),event(3,[add('ornaments',0,1),{code:123,indent:1,parameters:['A',0]}]), + event(4,[{code:357,parameters:['NYA.Utils','shop','',{}]}]),event(5,[{code:357,parameters:['NYA.Utils','add_coin','',{num:'-20'}]}]), + event(6,[{code:357,parameters:['NYA.EVENT.AI_Adapter','ai','',{type:'60',battleObject:'104'}]}]) ]}; + const inventory={subject:'actor',stats:{coin:{value:100}},items:[{type:'hpst',id:0,name:'逗猫棒',count:0},{type:'ornaments',id:0,name:'挂件',count:1}]}; + const actor={_ornaments:{data:()=>({0:{price:900,openTask:0}})},hpStrengthData:()=>({price:500})}; + const context=vm.createContext({inventory,$gamePlayer:{actor:()=>actor},DataManager:{_cacheMapData:{14:map},mapRegion:()=>2}, + $dataRegion:{2:{name:'幽暗之森',exploreItem:[[14,1],[14,2],[14,6]]}},$dataMapInfos:{14:{name:'MAP014'}},$dataSystem:{switches:[],variables:[]}, + $gameSwitches:{value:()=>false},$gameVariables:{value:()=>0},$gameSelfSwitches:{value:key=>!!flags[key.join(',')]}}); + const result=JSON.parse(vm.runInContext(`JSON.stringify((${readEncyclopedia.toString()})(inventory))`,context)); + assert.equal(result.items[0].count,0); assert.equal(result.items[0].locations[0].collected,true); assert.equal(result.items[0].locations[1].collected,false); + assert.equal(result.items[1].locations[0].collected,null); assert.equal(result.items[1].shopCandidate,true); + assert.equal(result.locations.length,4); assert.equal(result.shops.length,1); + assert.equal(result.items[2].locations[0].kind,'coinpile'); assert.equal(result.items[2].locations[0].collected,false); + assert.deepEqual(result.stats,{items:3,locations:4,shops:1,total:3,collected:1}); + assert.deepEqual(flags,{'14,1,A':true}); assert.equal(inventory.items[0].locations,undefined); +}); + +test('encyclopedia reports missing runtime map data instead of empty success', () => { + const context=vm.createContext({DataManager:{_cacheMapData:{}},inventory:{}}); + assert.throws(()=>vm.runInContext(`(${readEncyclopedia.toString()})(inventory)`,context),/尚未加载/); +}); diff --git a/tool/tests/item-icons.test.js b/tool/tests/item-icons.test.js new file mode 100644 index 0000000..5e4a978 --- /dev/null +++ b/tool/tests/item-icons.test.js @@ -0,0 +1,15 @@ +const test=require('node:test'); +const assert=require('node:assert/strict'); +const vm=require('node:vm'); +const {readItemIcons}=require('../lib/item-icons'); + +test('item thumbnails use the native icon frames without creating sprites or writing files',async()=>{ + const draws=[]; + const bitmap={width:384,height:192,isReady:()=>true,canvas:{source:true}}; + const c=vm.createContext({items:[{type:'consumables',id:0},{type:'ornaments',id:25}],setTimeout,clearTimeout, + Sprite_ItemIcon:{prototype:{itemBitmap:()=>bitmap,refreshIdx(id){this.setFrame(id===25?32:0,id===25?160:32,32,32);}}}, + document:{createElement:()=>({getContext:()=>({drawImage(...args){draws.push(args.slice(1));}}),toDataURL:()=> 'data:image/png;base64,dGVzdA=='})}}); + const result=JSON.parse(await vm.runInContext(`(${readItemIcons.toString()})(items).then(JSON.stringify)`,c)); + assert.equal(result.ok,true);assert.equal(Object.keys(result.icons).length,2); + assert.deepEqual(draws[0],[0,32,32,32,0,0,32,32]);assert.deepEqual(draws[1],[32,160,32,32,0,0,32,32]); +}); diff --git a/tool/tests/motion.test.js b/tool/tests/motion.test.js new file mode 100644 index 0000000..7186a1a --- /dev/null +++ b/tool/tests/motion.test.js @@ -0,0 +1,187 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const vm = require('node:vm'); +const { installMotion } = require('../lib/motion'); +const { OPS } = require('../lib/ops'); + +function fixture() { + let now = 0; + const timers = []; + const character = () => ({ _x: 3, _y: 4, px: 3 * 96 + 40, py: 4 * 96 + 80, _vx: 0, _vy: 12, + get vx() { return this._vx; }, set vx(v) { this._vx = v; }, + get vy() { return Math.round(this._vy); }, set vy(v) { this._vy = v; }, + g: () => 0, deltaTime: () => 1, doGravity() { this._vy += 0.5; }, updateMapBoundTransfer() { this.transfer = true; }, + makeMomentum() { this.rx = this.px + this._vx; this.ry = this.py + this._vy; }, + cacheDeltaTimeSpeed() { this._vx *= this.deltaTime(); this._vy *= this.deltaTime(); }, + startTestV() { if(!this._testV){this._testV=true;this._tvx=this._vx;this._tvy=this._vy;} }, + testV(v) {this.vx=v.x;this.vy=v.y;this.makeMomentum();}, + endTestV() {if(this._testV){this.vx=this._tvx;this.vy=this._tvy;this._testV=false;this.makeMomentum();}}, + tileXToCenterX: x => x * 96 + 40, tileYToCenterY: y => y * 96 + 80, + locate(x,y) { this._x = Math.round(x); this._y = Math.round(y); this.unflush(x,y); }, + unflush(x,y) { this.px = Math.round(x * 96 + 40); this.py = Math.round(y * 96 + 80); }, + clearImpact() {}, clearGrounding() {}, suspendGravity() { this._suspendGravity = true; }, resumeGravity() { this._suspendGravity = false; }, + suspendPhysicsHitTest() { this._suspendPhysicsHitTest = true; }, resumePhysicsHitTest() { this._suspendPhysicsHitTest = false; }, + isSuspendGravity() { return !!this._suspendGravity; }, isRemove() { return false; }, + hitPreview() { return []; }, + isFlexibleGrounding() { return !!this.grounded; }, isTriggeredJump() { return !!this.jumpPressed; }, + isPressDown() { return !!this.downPressed; }, isOutControl() { return !!this.blocked; }, jumpStrength() { return -8; }, + controlJump() { if(this.grounded && this.jumpPressed)this.vy=-8; }, + controlDoubleJump() { if(!this.grounded && !this.useDoubleJump && this.jumpPressed) { this.vy=-6; this.useDoubleJump=true; } }, + }); + const p = character(), lily = character(); + const context = vm.createContext({ window: {}, $gamePlayer:p, $gameLily:lily, $dataMap: {}, + $gameMap: { g: () => 0.5, width: () => 20, height: () => 12, tileWidth: () => 96, tileHeight: () => 96, mapId: () => 1 }, + SceneManager: { _scene: { constructor: { name:'Scene_Map' } } }, + Date: { now: () => now }, setInterval: fn => timers.push(fn), + }); + const api = vm.runInContext(`(${installMotion.toString()})()`, context); + return { context, api, p, lily, advance(ms) { now += ms; timers.forEach(fn=>fn()); } }; +} + +test('hover stops falling, slows upward jump to zero, supports inverted gravity and restores hooks', () => { + const f=fixture(), original=f.p.doGravity, transfer=f.p.updateMapBoundTransfer; + f.api.enable(f.p); assert.equal(f.p._vy,0); assert.equal(f.p._suspendGravity,true); + const saved=JSON.parse(JSON.stringify(f.p)); + assert.equal(Object.hasOwn(saved,'_suspendGravity'),false); + assert.equal(Object.hasOwn(saved,'_suspendPhysicsHitTest'),false); + f.p._vy=-8; const speeds=[]; + for(let i=0;i<40;i++) { f.p.doGravity(); speeds.push(f.p._vy); } + assert.equal(speeds[0],-7.5); assert.equal(speeds.at(-1),0); assert.ok(speeds.every(v=>v<=0)); + f.p._vy=20; f.p.doGravity(); assert.equal(f.p._vy,0); + f.p._vy=20; f.p.cacheDeltaTimeSpeed(); f.p.makeMomentum(); assert.equal(f.p.ry,f.p.py); + f.p._testV=true; f.p._vy=5; f.p.makeMomentum(); assert.equal(f.p.ry,f.p.py+5); f.p._testV=false; + f.p._invertMode=true; f.p._vy=8; + for(let i=0;i<40;i++) f.p.doGravity(); + assert.equal(f.p._vy,0); + f.p.updateMapBoundTransfer(); assert.equal(f.p.transfer,true); + assert.equal(Object.keys(f.p).includes('doGravity'),false); + f.api.clear(); assert.equal(f.p.doGravity,original); assert.equal(f.p.updateMapBoundTransfer,transfer); + assert.equal(f.p._suspendGravity,false); assert.equal(f.p._suspendPhysicsHitTest,false); +}); + +test('noclip enables hover for both characters and refuses to restore collision inside terrain', async () => { + const f=fixture(); + const run=async code=>JSON.parse(await vm.runInContext(code,f.context)); + assert.equal((await run(OPS.setNoclip(true))).ok,true); + for(const c of [f.p,f.lily]) { assert.equal(c._suspendGravity,true); assert.equal(c._suspendPhysicsHitTest,true); c._vy=99; c.doGravity(); assert.equal(c._vy,0); } + f.p.hitPreview=()=>[{terrain:true,type:0}]; + assert.equal((await run(OPS.setNoclip(false))).ok,false); assert.equal(f.p._suspendPhysicsHitTest,true); + f.p.hitPreview=()=>[]; + assert.equal((await run(OPS.setNoclip(false))).ok,true); assert.equal(f.p._suspendPhysicsHitTest,false); assert.equal(f.p._suspendGravity,true); +}); + +test('pixel nudge uses physical position, accumulates exact small steps and respects map bounds', async () => { + const f=fixture(), run=async code=>JSON.parse(await vm.runInContext(code,f.context)); + f.p.px=500; f.p.py=600; + const first=await run(OPS.nudge(1,0,4)); assert.equal(first.px,504); assert.equal(first.py,600); + const next=await run(OPS.nudge(1,0,4)); assert.equal(next.px,508); + f.p.px=40; assert.equal((await run(OPS.nudge(-1,0,16))).px,40); + f.p.py=80; assert.equal((await run(OPS.nudge(0,-1,1))).py,80); + f.p.px=99999; f.p.py=99999; f.p.updateMapBoundTransfer(); assert.equal(f.p.px,19*96+40); assert.equal(f.p.py,11*96+80); + assert.throws(()=>OPS.nudge(1,1,4)); assert.throws(()=>OPS.nudge(1,0,Infinity)); +}); + +test('hover survives normal map changes but releases on save load and heartbeat expiry', () => { + const f=fixture(); f.api.enable(f.p,true); f.advance(120001); + assert.equal(f.api.has(f.p),false); assert.equal(f.p._suspendGravity,false); + f.api.enable(f.p,true); f.context.$dataMap={}; f.p.resumeGravity();f.p.resumePhysicsHitTest();f.advance(1); + assert.equal(f.api.has(f.p),true);assert.equal(f.p.isSuspendGravity(),true);assert.equal(f.p._suspendPhysicsHitTest,true); + f.context.$gameMap={...f.context.$gameMap};f.advance(1); + assert.equal(f.api.has(f.p),false); +}); + +test('hover allows repeated airborne jumps, manual descent, and preserves downward skill movement', () => { + const f=fixture(), jump=f.p.controlJump, double=f.p.controlDoubleJump; + f.p.useDoubleJump=true; f.api.enable(f.p); f.p.jumpPressed=true; + for(let i=0;i<5;i++) { f.p._vy=0; f.p.controlJump({}); f.p.controlDoubleJump({}); assert.equal(f.p._vy,-8); } + assert.equal(f.p.useDoubleJump,true); + f.p.jumpPressed=false; f.p.downPressed=true; f.p.doGravity(); f.p.makeMomentum(); + assert.equal(f.p.ry-f.p.py,4); + f.p.downPressed=false; f.p.doGravity(); f.p.makeMomentum(); assert.equal(f.p.ry,f.p.py); + f.p._bodyAttacking='toru_down_dash_attack'; f.p._vy=22; f.p.doGravity(); f.p.makeMomentum(); assert.equal(f.p.ry-f.p.py,22); + f.p.downPressed=true; f.p._vy=25; f.p.doGravity(); f.p.makeMomentum(); assert.equal(f.p.ry-f.p.py,25); + f.p._bodyAttacking=''; f.p.downPressed=false; f.p.doGravity(); f.p.makeMomentum(); assert.equal(f.p.ry,f.p.py); + f.p.blocked=true; f.p.downPressed=true; f.p.doGravity(); assert.equal(f.p._vy,0); + f.api.clear(); assert.equal(f.p.controlJump,jump); assert.equal(f.p.controlDoubleJump,double); +}); + +test('native skill and stagger cleanup cannot cancel tool hover; disabling restores the native query', async () => { + const f=fixture(), originalQuery=f.p.isSuspendGravity; + f.api.enable(f.p); + // The original game does this when a skill or stagger ends. + f.p.resumeGravity(); + assert.equal(f.p._suspendGravity,false); + assert.equal(f.api.has(f.p),true); + assert.equal(f.p.isSuspendGravity(),true); + f.p._vy=20; f.p.doGravity(); f.p.makeMomentum(); + assert.equal(f.p.ry,f.p.py); + f.advance(15000); + f.p.isInvincible=()=>false; + Object.assign(f.context,{$dataMapInfos:[], $dataSystem:{switches:[]}, $gameSwitches:{value:()=>false}, Graphics:{}}); + const snap=JSON.parse(await vm.runInContext(OPS.snapshot(),f.context)); + assert.equal(snap.ok,true); + assert.equal(snap.gravitySuspended,true); + assert.equal(f.p.isSuspendGravity(),true); + assert.equal(Object.keys(f.p).includes('isSuspendGravity'),false); + f.api.disable(f.p); + assert.equal(f.p.isSuspendGravity,originalQuery); + assert.equal(f.p.isSuspendGravity(),false); + // Native skill suspension must not turn the panel's tool switch back on. + f.p.suspendGravity(); + assert.equal(JSON.parse(await vm.runInContext(OPS.snapshot(),f.context)).gravitySuspended,false); +}); + +test('hot upgrading an active v4 helper preserves hover, noclip and velocity', () => { + const f=fixture(), nativeQuery=f.p.isSuspendGravity; + f.api.enable(f.p,true); f.p._vx=3; f.p._vy=-7; + f.api.version=4; + const next=vm.runInContext(`(${installMotion.toString()})()`,f.context); + assert.equal(next.version,5); assert.equal(next.has(f.p),true); + assert.equal(f.p._suspendPhysicsHitTest,true); + assert.equal(f.p._vx,3); assert.equal(f.p._vy,-7); + f.p.resumeGravity(); assert.equal(f.p.isSuspendGravity(),true); + next.clear(); assert.equal(f.p.isSuspendGravity,nativeQuery); + assert.equal(f.p.isSuspendGravity(),false); +}); + +test('collision-resolved motion is preserved through grounding probes while holding down', () => { + const f=fixture(), momentum=f.p.makeMomentum,cache=f.p.cacheDeltaTimeSpeed; + f.api.enable(f.p); f.p.downPressed=true; + assert.equal(f.p.makeMomentum,momentum); + for(let i=0;i<30;i++){ + f.p.doGravity();f.p.cacheDeltaTimeSpeed();f.p.makeMomentum(); + assert.equal(f.p._vy,4); + // Native solid-floor collision resolves to no vertical displacement. + f.p._vy=0;f.p.makeMomentum(); + // generateObjectPhysicsInfo probes below, then restores resolved velocity. + f.p.startTestV();f.p.testV({x:0,y:1});f.p.endTestV(); + assert.equal(f.p._vy,0);assert.equal(f.p.ry,f.p.py); + } + f.p.deltaTime=()=>0.5;f.p.cacheDeltaTimeSpeed();assert.equal(f.p._vy,2); + f.api.clear();assert.equal(f.p.cacheDeltaTimeSpeed,cache); +}); + +test('native map exits run before the safety clamp; noclip side exits use native destinations', () => { + const f=fixture();let nativeCalls=0,exitType=null,transferring=false; + f.p.hitOffsetL=f.p.hitOffsetR=()=>12;f.p.hitOffsetT=()=>64;f.p.hitOffsetB=()=>16; + f.p.updateMapBoundTransfer=()=>{nativeCalls++;};f.p.isTransferring=()=>transferring; + f.p.autoTransfer=type=>{exitType=type;transferring=true;return true;}; + f.api.enable(f.p,true);f.p.px=-5;f.p.py=600;f.p.updateMapBoundTransfer(); + assert.equal(nativeCalls,1);assert.equal(exitType,3);assert.equal(f.p.px,-5); + transferring=false;f.p.autoTransfer=()=>false;f.p.updateMapBoundTransfer();assert.equal(f.p.px,12); + f.p.updateMapBoundTransfer();assert.equal(nativeCalls,3); + f.api.setNoclip(f.p,false);f.context.$dataMap={};f.api.touch();assert.equal(f.p._suspendPhysicsHitTest,false); + assert.equal(f.api.has(f.p),true); +}); + +test('normal map transitions retain helpers; transient loading and replaced actors do not revive old helpers', () => { + const f=fixture();f.context.$gameActors={};f.api.touch();f.api.enable(f.p,true); + f.context.$dataMap=null;f.context.SceneManager.isSceneChanging=()=>true; + f.api.touch();assert.equal(f.api.has(f.p),true); + f.context.$dataMap={};f.context.SceneManager.isSceneChanging=()=>false; + f.p.resumePhysicsHitTest();f.p._vy=40;f.api.touch(); + assert.equal(f.p._suspendPhysicsHitTest,true);assert.equal(f.p._vy,0); + f.context.$gameActors={};f.api.touch(); + assert.equal(f.api.has(f.p),false);assert.equal(f.p.isSuspendGravity(),false); + f.context.$dataMap={};f.api.touch();assert.equal(f.api.has(f.p),false); +}); diff --git a/tool/tests/regression.test.js b/tool/tests/regression.test.js new file mode 100644 index 0000000..e6222d7 --- /dev/null +++ b/tool/tests/regression.test.js @@ -0,0 +1,498 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const path = require('path'); +const os = require('os'); +const vm = require('vm'); +const { EventEmitter } = require('events'); +const SAFETY = require('../lib/safety'); +const FILES = require('../lib/files'); +const { OPS } = require('../lib/ops'); +const { loadAll } = require('../lib/data'); +const { storyPosition, chooseStoryPosition } = require('../lib/story-position'); + +function fixture(t) { + const root = fs.realpathSync.native(fs.mkdtempSync(path.join(os.tmpdir(), 'nyaruru-regression-'))); + t.after(() => { + const resolved = path.resolve(root); + assert.equal(path.dirname(resolved), fs.realpathSync.native(os.tmpdir())); + assert.match(path.basename(resolved), /^nyaruru-regression-/); + fs.rmSync(resolved, { recursive: true, force: true }); + }); + const game = path.join(root, 'game'), sandbox = path.join(root, 'sandbox'), backup = path.join(root, 'backups'); + fs.mkdirSync(path.join(game, 'save'), { recursive: true }); + fs.mkdirSync(path.join(game, 'aitemp', 'tool'), { recursive: true }); + fs.writeFileSync(path.join(game, 'save', 'file1.rmmzsave'), 'first'); + fs.writeFileSync(path.join(game, 'save', 'global.rmmzsave'), 'global'); + fs.writeFileSync(path.join(game, 'save', 'steam_autocloud.vdf'), 'cloud'); + fs.writeFileSync(path.join(game, 'package.json'), '{}'); + fs.writeFileSync(path.join(game, 'index.html'), 'game'); + return { root, game, sandbox, backup, tool: path.join(game, 'aitemp', 'tool') }; +} +test('reject overlapping paths, relative paths and identical ports at startup and save', t => { + const f = fixture(t), defaults = { cdpPort: 9223, webPort: 8787, sandboxDir: f.sandbox }; + assert.equal(SAFETY.validateConfig(defaults, f.game).sandboxDir, f.sandbox); + for (const patch of [{ cdpPort: 8787 }, { sandboxDir: f.game }, { sandboxDir: f.root }, { sandboxDir: path.join(f.game, 'nested') }, { sandboxDir: 'relative' }]) { + assert.throws(() => SAFETY.validateConfig({ ...defaults, ...patch }, f.game)); + } + const file = path.join(f.root, 'config.json'); + fs.writeFileSync(file, '{bad'); assert.throws(() => SAFETY.readConfig(file, defaults, f.game), /损坏/); + fs.writeFileSync(file, JSON.stringify({ webPort: 9223 })); assert.throws(() => SAFETY.readConfig(file, defaults, f.game), /不能相同/); +}); + +test('standalone layout uses configured game directory and rejects incomplete or overlapping targets', t => { + const f = fixture(t); + const defaults = { cdpPort: 9224, webPort: 8788, sandboxDir: f.sandbox, gameDir: f.game }; + const inferred = path.join(f.root, 'unrelated-workspace'); + assert.equal(SAFETY.validateConfig(defaults, inferred).gameDir, f.game); + assert.throws(() => SAFETY.validateConfig({ ...defaults, sandboxDir: path.join(f.game, 'copy') }, inferred), /完全分开/); + assert.throws(() => SAFETY.validateConfig({ ...defaults, gameDir: f.root }, inferred), /缺少/); + assert.throws(() => SAFETY.validateConfig({ ...defaults, gameDir: 'relative' }, inferred), /绝对路径/); + const configFile = path.join(f.tool, 'config.json'); + fs.writeFileSync(configFile, JSON.stringify(defaults)); + const config = SAFETY.readConfig(configFile, {}, inferred); + assert.equal(config.gameDir, f.game); + assert.equal(FILES.sandboxStatus(config.gameDir, config.sandboxDir, path.dirname(f.tool)).ready, false); +}); +test('backup is unique, verifies content, records source and rejects traversal/corruption', t => { + const f = fixture(t); + const a = FILES.backupSaves(f.backup, f.game, 'original'); + fs.writeFileSync(path.join(f.game, 'save', 'file1.rmmzsave'), 'second'); + const b = FILES.backupSaves(f.backup, f.game, 'original'); + assert.notEqual(a.name, b.name); + assert.equal(fs.readFileSync(path.join(a.dir, 'file1.rmmzsave'), 'utf8'), 'first'); + assert.equal(FILES.listBackups(f.backup)[0].sourceDir, f.game); + assert.throws(() => FILES.inspectBackup(f.backup, '../outside'), /非法/); + fs.writeFileSync(path.join(a.dir, 'file1.rmmzsave'), 'corrupt'); + assert.throws(() => FILES.inspectBackup(f.backup, a.name), /校验失败/); + assert.equal(FILES.listBackups(f.backup).length, 1); +}); +test('restore produces exact save set, preserves cloud settings, keeps previous directory', t => { + const f = fixture(t), b = FILES.backupSaves(f.backup, f.game, 'original'); + fs.writeFileSync(path.join(f.game, 'save', 'file1.rmmzsave'), 'changed'); + fs.writeFileSync(path.join(f.game, 'save', 'file9.rmmzsave'), 'extra'); + const r = FILES.restoreBackup(f.backup, b.name, f.game); + assert.equal(fs.readFileSync(path.join(f.game, 'save', 'file1.rmmzsave'), 'utf8'), 'first'); + assert.equal(fs.existsSync(path.join(f.game, 'save', 'file9.rmmzsave')), false); + assert.equal(fs.readFileSync(path.join(f.game, 'save', 'steam_autocloud.vdf'), 'utf8'), 'cloud'); + assert.equal(fs.readFileSync(path.join(r.previousDirectory, 'file9.rmmzsave'), 'utf8'), 'extra'); + assert.ok(r.safetyBackup); +}); +test('empty backup is rejected and legacy backups remain readable', t => { + const f = fixture(t); + const empty = path.join(f.root, 'empty'); fs.mkdirSync(path.join(empty, 'save'), { recursive: true }); + assert.throws(() => FILES.backupSaves(f.backup, empty), /没有找到/); + const legacy = path.join(f.backup, '20261002_123456'); fs.mkdirSync(legacy, { recursive: true }); + fs.writeFileSync(path.join(legacy, 'file1.rmmzsave'), 'old'); + assert.equal(FILES.listBackups(f.backup)[0].legacy, true); +}); +test('sandbox rejects partial copy; actual robocopy excludes both tool versions', { skip: process.platform !== 'win32' }, async t => { + const f = fixture(t); + fs.mkdirSync(f.sandbox); fs.writeFileSync(path.join(f.sandbox, 'package.json'), '{}'); + assert.equal(FILES.sandboxStatus(f.game, f.sandbox, path.dirname(f.tool)).ready, false); + fs.mkdirSync(path.join(f.game, 'aitemp发布版')); fs.writeFileSync(path.join(f.game, 'aitemp发布版', 'private.txt'), 'no copy'); + const target = path.join(f.root, 'complete'); + await FILES.copySandbox(f.game, target, path.dirname(f.tool)); + assert.equal(FILES.sandboxStatus(f.game, target, path.dirname(f.tool)).ready, true); + assert.equal(fs.existsSync(path.join(target, 'aitemp')), false); + assert.equal(fs.existsSync(path.join(target, 'aitemp发布版')), false); + assert.equal(fs.readFileSync(path.join(target, 'save', 'file1.rmmzsave'), 'utf8'), 'first'); + fs.writeFileSync(path.join(target, '.nyaruru-sandbox-incomplete'), 'interrupted'); + assert.equal(FILES.sandboxStatus(f.game, target, path.dirname(f.tool)).ready, false); +}); + +function character() { + return { _x: 1, _y: 1, px: 48, py: 48, _suspendPhysicsHitTest: true, gravity: true, + locate(x, y) { this._x = x; this._y = y; }, + clearImpact() {}, clearGrounding() {}, resumeFriction() {}, unlockInvincible() {}, + suspendGravity() { this.gravity = true; }, resumeGravity() { this.gravity = false; }, + suspendPhysicsHitTest() { this._suspendPhysicsHitTest = true; }, resumePhysicsHitTest() { this._suspendPhysicsHitTest = false; }, + isSuspendGravity() { return this.gravity; }, isInvincible() { return false; }, + isRemove() { return !!this.removed; }, hitPreview() { return [{ terrain: true, type: 0 }]; }, + }; +} +function gameContext() { + const c = { window: {}, $gamePlayer: character(), $gameLily: character(), $dataMapInfos: [null, {}, {}], $dataMap: {}, + SceneManager: { _scene: { constructor: { name: 'Scene_Map' } } }, + $gameMap: { mapId: () => 1, width: () => 20, height: () => 20, tileHeight: () => 48 }, + }; + vm.createContext(c); return c; +} +async function run(c, script) { return JSON.parse(await vm.runInContext(script, c)); } +test('land restores both companions, preserves unsafe lily and restore handles hidden lily', async () => { + const c = gameContext(); assert.equal((await run(c, OPS.land())).ok, true); + assert.equal(c.$gamePlayer.gravity, false); assert.equal(c.$gameLily.gravity, false); + assert.equal(c.$gameLily._suspendPhysicsHitTest, false); + c.$gameLily.hitPreview = () => [{ terrain: true, type: 2 }]; + const unsafe = await run(c, OPS.land()); assert.match(unsafe.warning, /不安全/); assert.equal(c.$gameLily.gravity, true); + c.$gameLily.removed = true; await run(c, OPS.restoreAll()); + assert.equal(c.$gameLily.gravity, false); assert.equal(c.$gameLily._suspendPhysicsHitTest, false); +}); +test('invalid coordinates cannot reach locate; unfinished transfer fails and cancels reservation', async () => { + assert.throws(() => OPS.teleport(NaN, 2)); assert.throws(() => OPS.teleport('', 2)); assert.throws(() => OPS.teleport(1, 2, { mapId: -1 })); + const c = gameContext(); let id = 1, now = 0, cancelled = false; + c.$gameMap.mapId = () => id; c.$gamePlayer.reserveTransfer = () => { id = 2; }; + c.$gamePlayer.isTransferring = () => true; c.$gamePlayer.clearTransferInfo = () => { cancelled = true; }; + c.Date = { now: () => now }; c.setTimeout = (fn, ms) => { now += ms; fn(); }; + const result = await run(c, OPS.teleport(3, 4, { mapId: 2 })); + assert.equal(result.ok, false); assert.equal(cancelled, true); assert.equal(now, 12000); +}); +test('identity is based on runtime storage path and changes for reload and revisiting a map', async () => { + const c = gameContext(); + c.require = require; c.process = { mainModule: { filename: path.join(os.tmpdir(), 'game', 'index.html') } }; + c.StorageManager = { fileDirectoryPath: () => path.join(os.tmpdir(), 'game', 'save') }; + c.location = { href: 'chrome-extension://id/index.html', pathname: '/index.html' }; + const a = await run(c, OPS.selfPath()); c.$dataMap = {}; + const b = await run(c, OPS.selfPath()); assert.notEqual(a.mapToken, b.mapToken); + c.$gamePlayer = character(); const d = await run(c, OPS.selfPath()); assert.notEqual(b.mapToken, d.mapToken); + assert.equal(a.gameDir, path.join(os.tmpdir(), 'game')); +}); +test('scene pin is inert and reversible; staging permits all game saves and panel saves', async () => { + const c = gameContext(); let started = 0, saved = 0; + let now = 0, watchdog; + c.Date = { now: () => now }; c.setInterval = fn => { watchdog = fn; }; + c.Game_Map = function() {}; c.Game_Map.prototype.setup = function() {}; + c.DataManager = { maxSavefiles: () => 23, extractSaveContents() {} }; + const saveMethods = ['saveGame', 'saveGameHidden', 'autoSaveGame', 'autoSaveGameInternal']; + const originals = {}; + for (const name of saveMethods) { + originals[name] = c.DataManager[name] = function(slot) { + assert.equal(this, c.DataManager); assert.equal(slot, 23); + saved++; return Promise.resolve(name); + }; + } + c.$gameSystem = { onBeforeSave() {} }; c.SceneManager.snapForBackground = () => {}; + c.StorageManager = { saveZip() {}, saveObject() {} }; + const ev = { _erased: true, _pageIndex: -1, physics: true, _statemMembers: { name: 'old' }, + eventId: () => 1, event: () => ({ name: 'event', pages: [{ image: { characterName: 'body' } }] }), + findProperPageIndex: () => 0, setupPageSettings() { this._trigger = 4; this._interpreter = {}; this._statemMembers.name = 'new'; }, + characterName: () => 'body', start() { started++; }, update() { this.start(); }, + }; + const originalUpdate = ev.update; c.$gameMap.events = () => [ev]; c.$gameMap.requestRefresh = () => {}; + assert.equal((await run(c, OPS.forcePage(1, 0))).ok, true); + ev.update(); ev.start(); assert.equal(started, 0); assert.equal(ev._interpreter, null); assert.equal(ev.physics, false); + for (const name of saveMethods) { + assert.equal(c.DataManager[name], originals[name]); + assert.equal(await c.DataManager[name](23), name); + } + assert.equal(saved, 4); + assert.equal((await run(c, OPS.savePhotoSlot(23))).ok, true); assert.equal(saved, 5); + assert.equal(c.window.__nyaruruPhoto.count(), 1); assert.equal(ev.physics, false); + await run(c, OPS.unpinPages()); assert.equal(ev._erased, true); assert.equal(ev._pageIndex, -1); + assert.equal(ev.update, originalUpdate); assert.equal(ev._statemMembers.name, 'old'); + await c.DataManager.saveGame(23); assert.equal(saved, 6); + await run(c, OPS.forcePage(1, 0)); now = 119000; watchdog(); assert.equal(c.window.__nyaruruPhoto.count(), 1); + c.window.__nyaruruPhoto.touch(); now += 119000; watchdog(); assert.equal(c.window.__nyaruruPhoto.count(), 1); + now += 2000; watchdog(); assert.equal(c.window.__nyaruruPhoto.count(), 0); assert.equal(ev._erased, true); +}); +test('staged actors settle on terrain, animate without AI and face the player; undo restores pose', async () => { + const c = gameContext(); let aiCalls = 0; + c.Game_Map = function() {}; c.Game_Map.prototype.setup = function() {}; + c.DataManager = { extractSaveContents() {} }; + c.$gameMap.tileWidth = c.$gameMap.tileHeight = () => 96; + c.$gameMap.requestRefresh = () => {}; + c.ImageFrame = { info: () => ({ width: 3, height: 1 }) }; + const ai = { smoothfloor: 'true', skipSmoothfloor: 'false', fix: 'false' }; + const ev = { _x: 16, _y: 12, _realX: 16, _realY: 12, px: 1584, py: 1248, rx: 1584, ry: 1248, + _direction: 6, _directionFix: true, _framePos: 0, physics: true, fix: false, + eventId: () => 1, event: () => ({ pages: [{ list: [{ code: 357, parameters: ['ai-plugin', 'ai', '', ai] }] }] }), + setupPageSettings() {}, characterName: () => 'actor', + tileXToCenterX: x => (x + 0.5) * 96, tileYToCenterY: y => (y + 1) * 96, + locate(x, y) { this._realX = x; this._realY = y; this._x = Math.round(x); this._y = Math.round(y); this.px = this.rx = (x + 0.5) * 96; this.py = this.ry = (y + 1) * 96; }, + setDirection(d) { if (!this._directionFix) this._direction = d; }, + update() { aiCalls++; }, start() { aiCalls++; }, + updateFrameAnimation(info) { this._framePos = (this._framePos + 1) % info.width; }, + hitPreview(dx, dy) { return [{ terrain: false, type: 0 }, ...(dy >= 47 ? [{ terrain: true, type: 0 }] : [])]; }, + }; + c.$gameMap.events = () => [ev]; c.$gamePlayer.px = 1488; + const originalUpdate = ev.update; + await run(c, OPS.forcePage(1, 0)); + assert.equal(ev.py, 1294); assert.equal(ev.ry, 1294); assert.equal(ev.physics, false); + assert.equal(ev._direction, 4); assert.equal(ev._directionFix, true); + ev.update(); assert.equal(ev._framePos, 1); + ev.update(); ev.update(); assert.equal(ev._framePos, 0); ev.start(); assert.equal(aiCalls, 0); + c.$gamePlayer.px = 1680; c.window.__nyaruruPhoto.facePlayer(ev); assert.equal(ev._direction, 6); + await run(c, OPS.unpinPages()); + assert.equal(ev.py, 1248); assert.equal(ev.ry, 1248); assert.equal(ev.physics, true); assert.equal(ev.update, originalUpdate); + const originalProbe = ev.hitPreview; + for (const mode of ['flying', 'hazard', 'no-ground']) { + ai.smoothfloor = mode === 'flying' ? 'false' : 'true'; + ev.hitPreview = mode === 'hazard' ? () => [{ terrain: true, type: 2 }] : mode === 'no-ground' ? () => [] : originalProbe; + await run(c, OPS.forcePage(1, 0)); assert.equal(ev.py, 1248); + await run(c, OPS.unpinPages()); + } +}); + +for (const delayed of [false, true]) test(`staging crops the sprite sheet on pin and undo (${delayed ? 'delayed' : 'cached'} bitmap)`, async () => { + const c = gameContext(); + c.Game_Map = function() {}; c.Game_Map.prototype.setup = function() {}; + c.DataManager = { extractSaveContents() {} }; + const ev = { + _characterName: 'original', _pageIndex: 0, _framePos: 0, + eventId: () => 1, event: () => ({ pages: [{}, {}] }), + characterName() { return this._characterName; }, + bitmapName() { return this._characterName + '_atlas'; }, + characterIndex: () => 0, tileId: () => 0, + setupPageSettings() { this._characterName = 'dracula'; }, + }; + c.$gameMap.events = () => [ev]; c.$gameMap.requestRefresh = () => {}; + const images = {}, sprite = { + _character: ev, _framePos: 0, children: [], _contentSprite: {}, updates: 0, + setCharacterBitmap() { + const name = this._characterName; + const bitmap = images[name] ||= { + width: name === 'dracula_atlas' ? 558 : 240, height: 73, + columns: name === 'dracula_atlas' ? 6 : 3, ready: !delayed, listeners: [], + addLoadListener(fn) { if (this.ready) fn(); else this.listeners.push(fn); }, + load() { this.ready = true; for (const fn of this.listeners.splice(0)) fn(); }, + }; + this._contentSprite.bitmap = bitmap; + bitmap.addLoadListener(() => { + if (this._contentSprite.bitmap === bitmap) this._contentSprite.frame = { width: bitmap.width, height: bitmap.height }; + }); + }, + updateFrame() { + this.updates++; + if (this._framePos === ev._framePos) return; + this._framePos = ev._framePos; + const bitmap = this._contentSprite.bitmap; + this._contentSprite.frame = { width: bitmap.width / bitmap.columns, height: bitmap.height }; + }, + }; + c.SceneManager._scene.children = [sprite]; + assert.equal((await run(c, OPS.forcePage(1, 1))).ok, true); + if (delayed) images.dracula_atlas.load(); + assert.equal(sprite._characterName, 'dracula_atlas'); + assert.deepEqual(sprite._contentSprite.frame, { width: 93, height: 73 }); + await run(c, OPS.unpinPages()); + if (delayed) images.original_atlas.load(); + assert.equal(ev.characterName(), 'original'); + assert.deepEqual(sprite._contentSprite.frame, { width: 80, height: 73 }); + if (delayed) { + images.dracula_atlas.ready = false; + await run(c, OPS.forcePage(1, 1)); + await run(c, OPS.unpinPages()); + const updates = sprite.updates; + images.dracula_atlas.load(); // 旧图迟到,不能改动已撤销的显示状态。 + assert.equal(sprite.updates, updates); + assert.deepEqual(sprite._contentSprite.frame, { width: 80, height: 73 }); + } +}); + +test('save awaits disk rejection instead of reporting success', async () => { + const c = gameContext(); c.setTimeout = setTimeout; + c.$gameSystem = { onBeforeSave() {} }; c.SceneManager.snapForBackground = () => {}; + const originalZip = () => new Promise((resolve, reject) => setTimeout(() => reject(new Error('disk full')), 5)); + c.StorageManager = { saveZip: originalZip, saveObject() { return Promise.resolve(); } }; + c.DataManager = { maxSavefiles: () => 23, saveGame() { c.StorageManager.saveZip(); return Promise.resolve(); } }; + const r = await run(c, OPS.savePhotoSlot(23)); assert.equal(r.ok, false); assert.match(r.error, /disk full/); + assert.equal(c.StorageManager.saveZip, originalZip); +}); + +test('CDP shares concurrent connect, rejects pending on disconnect, ignores old socket close', async () => { + const sockets = []; + class WS { + constructor() { this.readyState = 1; sockets.push(this); queueMicrotask(() => this.onopen()); } + send(raw) { + const p = JSON.parse(raw); + if (p.method === 'Runtime.evaluate') return; + const result = p.method === 'Target.getTargets' ? { targetInfos: [{ type: 'page', targetId: 'game', url: 'chrome-extension://id/index.html' }] } : { sessionId: 'session' }; + queueMicrotask(() => this.onmessage({ data: JSON.stringify({ id: p.id, result }) })); + } + close() {} + } + const c = { module: { exports: {} }, WebSocket: WS, fetch: async () => ({ ok: true, json: async () => ({ webSocketDebuggerUrl: 'ws://test' }) }), AbortSignal, setTimeout, clearTimeout }; + vm.runInNewContext(fs.readFileSync(path.join(__dirname, '../lib/cdp.js'), 'utf8'), c); + const client = new c.module.exports.CdpClient(); + await Promise.all([client.connect(), client.connect()]); assert.equal(sockets.length, 1); + const old = sockets[0]; client.close(); await client.connect(); old.onclose(); assert.equal(client.ws, sockets[1]); + const pending = client.eval('test'); await new Promise(r => setImmediate(r)); + const rejection = assert.rejects(pending, /断开/); sockets[1].onclose(); await rejection; + assert.equal(client._pending.size, 0); client.close(); +}); + +function serverHarness(t, ops = OPS) { + const f = fixture(t), calls = []; + const fake = { generation: 1, port: 9223, close() {}, async isAlive() { return false; }, + async evalJson(script) { calls.push(script); return { ok: true, gameDir: f.game, saveDir: path.join(f.game, 'save'), mapToken: 'mock:1', mapId: 1 }; } }; + const c = { __dirname: f.tool, module: { exports: {} }, process, console: { log() {}, error() {} }, URL, Buffer, setTimeout, + require(n) { + if (n === './lib/cdp.js') return { CdpClient: function() { return fake; } }; + if (n === './lib/ops.js') return { OPS: ops }; + if (n === './lib/story-position') return { storyPosition }; + if (n === './lib/data.js') return { loadAll: () => ({ stats: {}, triggers: [] }) }; + if (n === './lib/safety') return SAFETY; + if (n === './lib/files') return FILES; + if (n === './lib/processes') return { assertGamesStopped: async () => {} }; + if (n === './lib/story-cache') return require('../lib/story-cache'); + return require(n); + }, + }; + vm.runInNewContext(fs.readFileSync(path.join(__dirname, '../server.js'), 'utf8'), c); + c.module.exports.CONFIG.sandboxDir = f.sandbox; + return { ...f, api: c.module.exports, fake, calls }; +} +async function request(h, url, body, method = 'POST') { + const req = new EventEmitter(); req.method = method; req.headers = {}; + let result, code; + const res = { writeHead(c) { code = c; }, end(s) { result = JSON.parse(s); } }; + const task = h.api.handleApi(req, res, url); + if (method === 'POST') { req.emit('data', JSON.stringify(body)); req.emit('end'); } + await task; return { result, code }; +} +test('controller stories use the verified scene entry without changing other or explicit positions', () => { + const data = loadAll(path.resolve(__dirname, '../..')); + for (const [story, map, expected] of [[449, 9, [11, 9]], [449, 296, [28, 13]], [467, 254, [6, 5]]]) { + const trigger = data.triggers.find(t => t.storyId === story && t.mapId === map); + assert.ok(trigger); + const p = storyPosition(trigger); assert.deepEqual([p.x, p.y], expected); + } + // Published tests validate the structural trigger index; original map scripts are not distributed. + for (const storyId of [298, 299, 305]) { + const trigger = data.triggers.find(t => t.storyId === storyId && t.mapId === 182 && Number(t.eventId) === 3 && t.pageIndex === 0); + assert.ok(trigger); assert.equal(trigger.x, 0); assert.equal(trigger.y, 0); + const p = storyPosition(trigger); + assert.equal(p.x, 11); assert.equal(p.y, 11); assert.ok(p.positionNote); + for (const patch of [{ mapId: 183 }, { eventId: 4 }, { storyId: 370 }, { pageIndex: 1 }, { pageIndex: null }, { x: 7, y: 8 }, { x: null }]) { + const input = { ...trigger, ...patch }; + assert.deepEqual(storyPosition(input), { x: input.x, y: input.y }); + } + } +}); + +test('unknown controller scenes preserve safe positions or find floor without story-number rules', () => { + const actor = { _x: 6, _y: 7, eventId: () => 2, characterName: () => 'actor', event: () => ({ pages: [] }) }; + const page = { trigger: 3, image: { characterName: '', tileId: 0 }, list: [] }; + let probes = 0, hazard = false, noFloor = false; + const player = { _x: 0, _y: 0, hitPreview(dx, dy) { + probes++; if (noFloor || dy < 8 * 96) return []; + return [{ terrain: true, type: hazard && dx === 5 * 96 ? 2 : 0 }]; + } }; + const input = { map: { tileWidth: () => 96, tileHeight: () => 96, width: () => 20, height: () => 15, events: () => [actor] }, + player, data: { events: [null, { x: 0, y: 0, pages: [page] }] }, eventId: 1, pageIndex: 0, + storyId: 987654, sameMap: true, previous: { x: 10, y: 7 }, requested: { x: 0, y: 0 } }; + let r = chooseStoryPosition(input); assert.equal(r.x, 10); assert.equal(r.y, 7); + input.sameMap = false; hazard = true; + r = chooseStoryPosition(input); assert.equal(r.x, 7); assert.equal(r.y, 7); // 左侧陷阱被拒绝,选右侧。 + page.trigger = 4; r = chooseStoryPosition(input); assert.equal(r.controller, true); + noFloor = true; probes = 0; r = chooseStoryPosition(input); + assert.equal(r.ok, false); assert.match(r.error, /未找到/); assert.ok(probes <= 800); + page.trigger = 0; probes = 0; r = chooseStoryPosition(input); + assert.equal(r.controller, false); assert.equal(r.x, 0); assert.equal(probes, 0); + page.trigger = 3; page.image.characterName = 'visible'; + assert.equal(chooseStoryPosition(input).controller, false); // 有形事件不能当成控制事件。 +}); + +test('scene, dialogue and goto APIs correct stale controller coordinates before teleport', async t => { + const targets = [], order = []; + const h = serverHarness(t, { ...OPS, + teleport(x, y, options) { targets.push({ x, y, map: options.mapId }); return OPS.teleport(x, y, options); }, + runStoryScene(...args) { order.push('scene'); return OPS.runStoryScene(...args); }, + land() { order.push('land'); return OPS.land(); }, + }); + const body = { context: '1:mock:1', mapId: 182, eventId: 3, pageIndex: 0, storyId: 305, x: 0, y: 0 }; + await request(h, '/api/story/start', { ...body, mode: 'scene' }); + await request(h, '/api/story/start', { ...body, mode: 'play' }); + await request(h, '/api/story/goto', { ...body, pairs: [] }); + assert.deepEqual(order, ['scene', 'land']); // 只对运行过场自动落地,且在补角色之后。 + assert.deepEqual(targets, Array.from({ length: 3 }, () => ({ x: 11, y: 11, map: 182 }))); + await request(h, '/api/story/start', { ...body, mode: 'play', x: 7, y: 8 }); + assert.deepEqual(targets[3], { x: 7, y: 8, map: 182 }); +}); + +test('restore never defaults to original; GET cannot mutate', async t => { + const h = serverHarness(t); + await assert.rejects(request(h, '/api/restore-backup', { name: 'any', confirm: true }), /明确/); + assert.equal((await request(h, '/api/restore', null, 'GET')).code, 405); + assert.equal(h.calls.length, 0); +}); +test('photo save checks target and blocks both stale context and backup failure', async t => { + const h = serverHarness(t); + await assert.rejects(request(h, '/api/save-photo', { context: '1:mock:1', slot: 23, target: 'sandbox' }), /不一致/); + await assert.rejects(request(h, '/api/save-photo', { context: '1:old', slot: 23, target: 'original' }), /已变化/); + fs.unlinkSync(path.join(h.game, 'save', 'file1.rmmzsave')); fs.unlinkSync(path.join(h.game, 'save', 'global.rmmzsave')); + await assert.rejects(request(h, '/api/save-photo', { context: '1:mock:1', slot: 23, target: 'original' }), /没有找到/); + assert.equal(h.calls.some(s => s.includes('await DataManager.saveGame(s)')), false); +}); +test('mutations are serialized and stale map-local actions are rejected', async t => { + const h = serverHarness(t); let active = 0, max = 0; + h.fake.evalJson = async s => { + if (!s.includes('const i=JSON.parse')) return { ok: true, gameDir: h.game, saveDir: path.join(h.game, 'save'), mapToken: 'mock:1' }; + active++; max = Math.max(max, active); await new Promise(r => setTimeout(r, 10)); active--; return { ok: true }; + }; + await Promise.all([request(h, '/api/gravity', { context: '1:mock:1', on: true }), request(h, '/api/gravity', { context: '1:mock:1', on: false })]); + assert.equal(max, 1); + await assert.rejects(request(h, '/api/events/force-page', { context: '1:old-map', eventId: 1, page: 0 }), /已变化/); +}); +test('public structural index excludes text and preserves distinct event/page alternatives', () => { + const data = loadAll(path.resolve(__dirname, '../..')); + assert.ok(data.triggers.length > 0); + assert.ok(data.triggers.every(t => !('lines' in t) && !('line' in t) && !('searchText' in t))); + assert.equal(data.storyIndex, undefined); + const story11 = data.triggers.filter(t => t.storyId === 11); + assert.ok(new Set(story11.map(t => t.page)).size > 1); +}); + +function launcherHarness(t, { ready = false, choice = '3', occupied = false, identityError = false, backupError = false } = {}) { + const f = fixture(t), actions = []; + fs.writeFileSync(path.join(f.tool, 'config.json'), JSON.stringify({ sandboxDir: f.sandbox })); + const c = { __dirname: f.tool, module: { exports: {} }, console: { log() {} }, setTimeout, + process: { versions: { node: '24.0.0' }, argv: [], stdin: {}, stdout: {}, execPath: process.execPath }, + require(n) { + if (n === './lib/safety') return SAFETY; + if (n === './lib/files') return { sandboxStatus: () => ({ ready, reason: 'mock' }), backupSaves: () => { actions.push('backup'); if (backupError) throw Error('backup failed'); return { name: 'mock' }; } }; + if (n === './lib/cdp') return { CdpClient: function() { return { close() {}, async evalJson() { if (identityError) throw Error('unknown'); return { ok: true, gameDir: f.game, saveDir: path.join(f.game, 'save') }; } }; } }; + if (n === './lib/ops') return { OPS }; + if (n === 'readline/promises') return { createInterface: () => ({ question: async () => choice, close() {} }) }; + if (n === 'net') return { connect: ({ port }) => { const s = new EventEmitter(); s.destroy = () => {}; s.setTimeout = () => {}; queueMicrotask(() => s.emit(occupied && port === 9223 ? 'connect' : 'error')); return s; } }; + if (n === 'child_process') return { spawn: () => { actions.push('spawn'); throw Error('unexpected spawn'); } }; + return require(n); + }, + }; + vm.runInNewContext(fs.readFileSync(path.join(__dirname, '../launcher.js'), 'utf8'), c); + return { main: c.module.exports.main, actions }; +} +test('launcher exit stops completely, wrong and unknown instances stop before backup/panel', async t => { + const exit = launcherHarness(t); await exit.main(); assert.deepEqual(exit.actions, []); + for (const identityError of [false, true]) { + const h = launcherHarness(t, { ready: true, occupied: true, identityError }); + await assert.rejects(h.main(), /不能确认/); assert.deepEqual(h.actions, []); + } +}); +test('launcher backup failure prevents starting either game or panel', async t => { + const h = launcherHarness(t, { ready: true, backupError: true }); + await assert.rejects(h.main(), /backup failed/); assert.deepEqual(h.actions, ['backup']); +}); +test('panel save warning lets the user continue or cancel using the existing confirmation', async () => { + const source = fs.readFileSync(path.join(__dirname, '../web/app.js'), 'utf8'); + const start = source.indexOf("$('#photo-save').onclick"); + const end = source.indexOf('// ---------- 对话跳转点', start); + for (const proceed of [false, true]) { + const button = {}, calls = []; let confirmation; + vm.runInNewContext(source.slice(start, end), { + $: key => key === '#photo-save' ? button : { value: '23' }, + CURRENT_CONTEXT: 'current', CURRENT_GAME: { kind: 'sandbox', saveDir: 'sandbox/save' }, + confirm: text => { confirmation = text; return proceed; }, + api: async (route, body) => { calls.push({ route, body }); return { ok: true, slot: 23 }; }, + toast() {}, loadBackups() {}, + }); + await button.onclick(); + assert.match(confirmation, /不推荐.*保存/); assert.match(confirmation, /仍可确定继续保存/); + assert.equal(calls.length, proceed ? 1 : 0); + if (proceed) { assert.equal(calls[0].route, '/save-photo'); assert.equal(calls[0].body.slot, 23); assert.equal(calls[0].body.target, 'sandbox'); } + } +}); + +test('map event search matches later lines and displays the matched text', () => { + const source = fs.readFileSync(path.join(__dirname, '../web/app.js'), 'utf8'); + const start = source.indexOf('function renderEvents()'); + const end = source.indexOf("$('#ev-load').onclick", start); + const list = { innerHTML: '', querySelectorAll: () => [] }; + const elements = { '#ev-filter': { value: 'later sentence' }, '#ev-all': { checked: false }, '#ev-list': list }; + vm.runInNewContext(source.slice(start, end) + ';renderEvents();', { + $: key => elements[key], esc: value => String(value), spriteLabel: () => '', + EVENTS: [{ id: 1, hasStory: true, name: 'event', sprite: '', trigger: 'test', x: 0, y: 0, + lines: [{ first: 'first sentence', speaker: 'actor', searchText: 'first sentence\nlater sentence', lines: [{ text: 'first sentence', speaker: 'actor' }, { text: 'later sentence', speaker: 'actor' }] }] }], + }); + assert.match(list.innerHTML, /later sentence/); assert.doesNotMatch(list.innerHTML, /没有匹配|没有带剧情/); +}); diff --git a/tool/tests/release.test.js b/tool/tests/release.test.js new file mode 100644 index 0000000..1adf2f4 --- /dev/null +++ b/tool/tests/release.test.js @@ -0,0 +1,59 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const os = require('node:os'); +const { buildRelease, manifestFiles } = require('../package-release'); + +function tempRoot(t) { + const parent = fs.realpathSync.native(os.tmpdir()); + const root = fs.mkdtempSync(path.join(parent, 'nyaruru-public-release-')); + t.after(() => { + assert.equal(path.dirname(path.resolve(root)), parent); + assert.match(path.basename(root), /^nyaruru-public-release-/); + fs.rmSync(root, { recursive: true, force: true }); + }); + return root; +} +function walk(root, prefix = '') { + return fs.readdirSync(path.join(root, prefix), { withFileTypes: true }).flatMap(entry => { + const relative = prefix ? prefix + '/' + entry.name : entry.name; + return entry.isDirectory() ? walk(root, relative) : [relative]; + }); +} + +test('clean release contains only allow-listed files and serves its page without game access', async t => { + const root = tempRoot(t), destination = path.join(root, 'Nyaruru_modifier'); + const result = buildRelease(destination); + assert.deepEqual(walk(destination).sort(), manifestFiles().sort()); + assert.equal(result.files.length, manifestFiles().length); + assert.throws(() => buildRelease(destination), /已存在/); + const mod = require(path.join(destination, 'tool/server.js')); + let gameCalls = 0; + for (const method of ['connect', 'evalJson', 'isAlive']) mod.cdp[method] = async () => { gameCalls++; throw Error('No live game access in release tests'); }; + await new Promise(resolve => mod.server.listen(0, '127.0.0.1', resolve)); + t.after(() => new Promise(resolve => { mod.server.closeAllConnections(); mod.server.close(resolve); })); + const origin = 'http://127.0.0.1:' + mod.server.address().port; + const health = await fetch(origin + '/api/health').then(r => r.json()); + assert.equal(health.ok, true); assert.equal(health.config.webPort, 8787); + const html = await fetch(origin).then(r => r.text()); + for (const id of ['story-text-hint', 'story-load-text', 'panel-world-map', 'panel-encyclopedia']) assert.ok(html.includes('id="' + id + '"')); + assert.equal((await fetch(origin + '/app.js')).status, 200); + assert.equal(gameCalls, 0); + assert.doesNotMatch(html, /panel-workspace|data-panel="workspace"|快照与 TAS/); + for (const route of ['/api/checkpoints','/api/tas']) assert.equal((await fetch(origin + route)).status, 405); + const app = await fetch(origin + '/app.js').then(r => r.text()); + assert.doesNotMatch(app, /loadWorkspace|WORK_BUSY|panel-workspace|checkpoints|\/tas/); + const { OPS } = require(path.join(destination, 'tool/lib/ops.js')); + for (const name of ['tas','captureCheckpoint','loadCheckpoint','restoreCheckpointTools']) assert.equal(OPS[name], undefined); + assert.equal(typeof OPS.snapshot, 'function'); assert.equal(typeof OPS.combat, 'function'); + assert.deepEqual(walk(destination).sort(), manifestFiles().sort()); +}); + +test('release manifest rejects raw maps, runtime configuration and path traversal', t => { + const root = tempRoot(t); fs.mkdirSync(path.join(root, 'tool')); + for (const file of ['unpacked/data/Map181.json', 'out/story.json', 'tool/config.json', '../outside', 'backups/states.bin']) { + fs.writeFileSync(path.join(root, 'tool/发布清单.json'), JSON.stringify({ files: [file] })); + assert.throws(() => manifestFiles(root)); + } +}); diff --git a/tool/tests/story-cache.test.js b/tool/tests/story-cache.test.js new file mode 100644 index 0000000..12a0520 --- /dev/null +++ b/tool/tests/story-cache.test.js @@ -0,0 +1,57 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const vm = require('node:vm'); +const { StoryCache } = require('../lib/story-cache'); +const { OPS } = require('../lib/ops'); + +function fixture(options = {}) { + let calls = 0; + const cdp = { port: 9223, generation: 1, sessionId: 'session-1', ws: { readyState: 1 }, + async isAlive() { return true; }, async connect() { this.ws = { readyState: 1 }; }, + async evalJson() { calls++; return { ok: true, index: { 1: { first: 'fixture', lines: [{ text: 'fixture' }] } } }; } }; + const cache = new StoryCache(cdp, OPS, [{ storyId: 1 }, { storyId: 2 }], options); + return { cdp, cache, calls: () => calls }; +} + +test('runtime story index searches later synthetic lines and excludes commands and comments', async () => { + const context = vm.createContext({ $dataActors: { 2: { name: '模拟角色' } }, $dataStory: { + 1: [{ name: 'story', contents: '模拟首句', speaker: 2 }, { name: 'story', contents: '模拟后续检索句' }, + { name: 'story', contents: '#注释' }, { name: 'story', contents: '///备注' }, { name: 'other', contents: '不属于台词' }], + 2: [{ name: 'story', contents: ' ' }], invalid: [{ name: 'story', contents: '忽略' }], + } }); + const result = JSON.parse(await vm.runInContext(OPS.readStoryIndex(), context)); + assert.equal(result.ok, true); assert.equal(result.count, 1); + assert.equal(result.index[1].count, 2); assert.equal(result.index[1].speaker, '模拟角色'); + assert.match(result.index[1].searchText, /模拟后续检索句/); + assert.doesNotMatch(result.index[1].searchText, /注释|备注|不属于台词/); +}); + +test('story cache shares reads, clears disconnected text and reloads a new connection', async () => { + const f = fixture(); + await Promise.all([f.cache.ensure(), f.cache.ensure()]); + assert.equal(f.calls(), 1); assert.equal(f.cache.status().triggersWithLine, 1); + f.cdp.ws = null; assert.equal(f.cache.get(1), null); assert.equal(f.cache.status().state, 'disconnected'); + f.cdp.generation++; f.cdp.sessionId = 'session-2'; await f.cache.ensure(); + assert.equal(f.calls(), 2); assert.equal(f.cache.status().loaded, true); +}); + +test('story cache rate limits four retries and permits explicit retry after failure', async () => { + let now = 0; const f = fixture({ now: () => now }); + f.cdp.evalJson = async () => ({ ok: false, error: 'fixture not ready' }); + await f.cache.ensure(); await f.cache.ensure(); assert.equal(f.cache.status().attempts, 1); + for (let i = 0; i < 3; i++) { now += 3000; await f.cache.ensure(); } + now += 3000; await f.cache.ensure(); + assert.equal(f.cache.status().attempts, 4); assert.equal(f.cache.status().state, 'error'); + f.cdp.evalJson = async () => ({ ok: true, index: { 1: { first: 'ready' } } }); + await f.cache.ensure({ force: true }); assert.equal(f.cache.get(1).first, 'ready'); +}); + +test('late story response from an old connection cannot overwrite new text', async () => { + const f = fixture(); let resolve; + f.cdp.evalJson = () => new Promise(r => { resolve = r; }); + const old = f.cache.ensure(); + f.cdp.generation++; f.cdp.sessionId = 'session-2'; f.cache.status(); + f.cdp.evalJson = async () => ({ ok: true, index: { 1: { first: 'new connection' } } }); + await f.cache.ensure(); resolve({ ok: true, index: { 1: { first: 'stale connection' } } }); await old; + assert.equal(f.cache.get(1).first, 'new connection'); +}); diff --git a/tool/tests/trainer.test.js b/tool/tests/trainer.test.js new file mode 100644 index 0000000..014bb40 --- /dev/null +++ b/tool/tests/trainer.test.js @@ -0,0 +1,186 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const vm = require('node:vm'); +const { installTrainer } = require('../lib/trainer'); +const { OPS } = require('../lib/ops'); + +function fixture() { + let now = 0; + const timers = []; + function makeActor() { + const battle = { _hp: 80, mhp: 100, sp: 30, msp: 100, zp: 60, mzp: 200, + get hp() { return this._hp | 0; }, + set hp(value) { if (this._hp !== 0) this._hp = Math.max(0, value); }, + addHp(delta) { this.hp = Math.min(this.mhp, Math.max(0, this._hp + delta)); }, + addSp(delta) { this.sp = Math.min(this.msp, Math.max(0, this.sp + delta)); }, + addZp(delta) { this.zp = Math.min(this.mzp, Math.max(0, this.zp + delta)); }, + }; + const definitions = { 0: { name: 'item0', desc: 'desc0', maxNum: 6 }, 1: { name: 'item1', maxNum: 2 } }; + function equipment(type) { + return { _unlock: [0, 1], _equip: [0], _currentWeapon: 0, + data: () => definitions, isUnlock(id) { return this._unlock.includes(id); }, + isEquip(id) { return this._equip.includes(id); }, currentWeapon() { return this._currentWeapon; }, + unlock(id) { if (!this.isUnlock(id)) { this._unlock.push(id); if (type === 'stones') this._equip.push(id); } }, + equip(id) { if (type === 'weapons') this._currentWeapon = id; else this._equip = this.isEquip(id) ? this._equip.filter(n => n !== id) : [...this._equip, id]; }, + remove(id) { this._equip = this._equip.filter(n => n !== id); this._unlock = this._unlock.filter(n => n !== id); }, + }; + } + return { battle, hp: 1, _coin: 123, name: () => '喵露露', coin() { return this._coin; }, + addCoin(n) { this._coin = Math.min(99999, this._coin + n); }, + deductCoin(n) { if (this._coin < n) return false; this._coin -= n; return true; }, + _consumables: { _data: { 0: 3 }, data: () => definitions, itemNum(id) { return this._data[id] || 0; }, maxNum: id => definitions[id].maxNum, + add(id, n, over) { this._data[id] = Math.max(0, Math.min(over ? Infinity : definitions[id].maxNum, this.itemNum(id) + n)); } }, + _weapons: equipment('weapons'), _stones: equipment('stones'), _ornaments: equipment('ornaments'), + changeWeapon(id) { this._weapons.equip(id); }, + _joyCoin: 2, _hpStrengthItem: 1, _skillStrengthItem: 3, + joyCoinData: () => definitions[0], hpStrengthData: () => definitions[0], skillStrengthData: () => definitions[0], + joyCoinNum() { return this._joyCoin; }, hpStrengthNum() { return this._hpStrengthItem; }, skillStrengthNum() { return this._skillStrengthItem; }, + }; + } + const actor = makeActor(), companion = makeActor(); + const player = { current: actor, lily: false, actor() { return this.current; }, battleObject() { return this.current.battle; }, isLilyMode() { return this.lily; }, setWeaponChange() { this.weaponChanged = true; } }; + const context = vm.createContext({ window: {}, $gamePlayer: player, $gameLily: { actor: () => companion }, $gameParty: {}, $gameActors: {}, + TextManager: { item0: '卷心菜', item1: '物品一', desc0: '恢复生命' }, + Date: { now: () => now }, setInterval: fn => { timers.push(fn); }, + $gameMap: { mapId: () => 1 }, SceneManager: { _scene: { constructor: { name: 'Scene_Map' } } }, + }); + const api = vm.runInContext(`(${installTrainer.toString()})()`, context); + const change = body => api.change({ subject: api.read().subject, ...body }); + return { api, change, context, actor, companion, player, timers, makeActor, advance(ms) { now += ms; timers.forEach(fn => fn()); } }; +} + +test('trainer edits actual NYA values, supports Lily mode and rejects stale actor requests', () => { + const f = fixture(); + f.change({ action: 'stat', field: 'coin', value: 9000 }); + f.change({ action: 'stat', field: 'hp', value: 95 }); + f.change({ action: 'stat', field: 'sp', value: 40 }); + f.change({ action: 'stat', field: 'zp', value: 150 }); + assert.equal(f.actor.hp, 1); assert.equal(f.actor.battle.hp, 95); assert.equal(f.actor.coin(), 9000); + const old = f.api.read().subject; + f.player.lily = true; + assert.equal(f.api.read().role, '莉莉娅'); + assert.throws(() => f.api.change({ subject: old, action: 'stat', field: 'hp', value: 20 }), /角色已变化/); + f.change({ action: 'stat', field: 'hp', value: 50 }); + assert.equal(f.actor.battle.hp, 50); assert.equal(f.companion.battle.hp, 80); + for (const value of [null, '20', -1, 101, NaN, Infinity, 1.5]) { + assert.throws(() => f.change({ action: 'stat', field: 'hp', value }), /整数/); + } + assert.equal(f.actor.battle.hp, 50); + f.change({ action: 'stat', field: 'hp', value: 0 }); + assert.throws(() => f.change({ action: 'stat', field: 'hp', value: 50 }), /已倒下/); +}); + +test('locks stop lethal damage and spending immediately, survive form changes and restore descriptors', () => { + const f = fixture(), b = f.actor.battle; + for (const [field, value] of Object.entries({ hp: 70, sp: 45, zp: 90, coin: 1000 })) f.change({ action: 'stat', field, value, locked: true }); + b.hp = 0; b.sp -= 50; b.zp += 80; + assert.equal(f.actor.deductCoin(999), true); f.actor.addCoin(500); + assert.equal(b.hp, 70); assert.equal(b.sp, 45); assert.equal(b.zp, 90); assert.equal(f.actor.coin(), 1000); + f.player.lily = true; b.hp = 0; assert.equal(b.hp, 70); + b.mhp = 40; assert.equal(b.hp, 40); + f.change({ action: 'stat', field: 'coin', value: 2000 }); + assert.equal(f.api.read().stats.coin.locked, true); assert.equal(f.actor.coin(), 2000); + const saved = JSON.parse(JSON.stringify(f.actor)); + assert.equal(saved.battle._hp, 40); assert.equal(saved._coin, 2000); + assert.equal(Object.keys(saved).some(k => /lock|trainer/i.test(k)), false); + f.change({ action: 'stat', field: 'hp', locked: false }); + const desc = Object.getOwnPropertyDescriptor(b, '_hp'); + assert.equal(desc.value, 40); assert.equal(desc.writable, true); assert.equal(desc.enumerable, true); + b.hp = 0; assert.equal(b.hp, 0); + f.api.clear(); assert.equal(f.actor.deductCoin(500), true); assert.equal(f.actor.coin(), 1500); +}); + +test('locks follow current player objects but clear on load and heartbeat expiry; install is idempotent', () => { + const f = fixture(); + f.change({ action: 'stat', field: 'sp', value: 55, locked: true }); + const other = f.makeActor(); f.player.current = other; f.advance(100); + other.battle.sp = 0; assert.equal(other.battle.sp, 55); + f.actor.battle.sp = 10; assert.equal(f.actor.battle.sp, 10); + vm.runInContext(`(${installTrainer.toString()})()`, f.context); assert.equal(f.timers.length, 1); + f.context.$gameActors = {}; f.context.$gameParty = {}; f.player.current = f.makeActor(); f.advance(100); + assert.equal(f.api.read().stats.sp.locked, false); assert.equal(f.player.current.battle.sp, 30); + f.change({ action: 'stat', field: 'coin', value: 500, locked: true }); + f.advance(120001); assert.equal(f.api.read().stats.coin.locked, false); + f.player.current.addCoin(1); assert.equal(f.player.current.coin(), 501); +}); + +test('inventory accepts item zero, respects optional consumable cap and keeps equipment references valid', () => { + const f = fixture(); + assert.equal(f.api.read().items[0].name, '卷心菜'); + const item = (type, id, count, extra = {}) => f.change({ action: 'item', type, id, count, ...extra }); + item('consumables', 0, 6); assert.equal(f.actor._consumables.itemNum(0), 6); + assert.throws(() => item('consumables', 0, 7), /整数/); + item('consumables', 0, 99, { overCap: true }); assert.equal(f.actor._consumables.itemNum(0), 99); + item('consumables', 0, 0); assert.equal(f.actor._consumables.itemNum(0), 0); + for (const type of ['stones', 'ornaments']) { + item(type, 0, 0); assert.equal(f.actor['_' + type].isEquip(0), false); assert.equal(f.actor['_' + type].isUnlock(0), false); + item(type, 0, 1); item(type, 0, 1); assert.equal(f.actor['_' + type]._unlock.filter(n => n === 0).length, 1); + } + item('weapons', 0, 0); assert.equal(f.actor._weapons.currentWeapon(), 1); assert.equal(f.player.weaponChanged, true); + assert.throws(() => item('weapons', 1, 0), /保留一件/); assert.equal(f.actor._weapons.isUnlock(1), true); + item('joycoin', 0, 500); assert.equal(f.actor.joyCoinNum(), 500); + assert.throws(() => item('__proto__', 0, 1), /分类/); + assert.throws(() => item('consumables', 999, 1), /不存在/); + assert.throws(() => item('hpst', 1, 1), /编号/); +}); + +test('OPS wraps trainer scripts with map guard and returns structured validation failures', async () => { + const f = fixture(); + const result = JSON.parse(await vm.runInContext(OPS.trainer(), f.context)); + assert.equal(result.ok, true); assert.equal(result.stats.hp.value, 80); + const bad = JSON.parse(await vm.runInContext(OPS.trainer({ action: 'stat', field: 'hp', value: 999, subject: result.subject }), f.context)); + assert.equal(bad.ok, false); assert.match(bad.error, /整数/); + f.context.SceneManager._scene.constructor.name = 'Scene_Title'; + const menu = JSON.parse(await vm.runInContext(OPS.trainer(), f.context)); + assert.equal(menu.ok, false); assert.match(menu.error, /进入地图/); +}); + +test('batch changes validate every entry before modifying and roll back failed game methods', () => { + const f=fixture(); + assert.throws(()=>f.change({action:'batch',changes:[{type:'joycoin',id:0,count:9},{type:'ornaments',id:999,count:1}]}),/不存在/); + assert.equal(f.actor.joyCoinNum(),2); + assert.throws(()=>f.change({action:'batch',changes:[{type:'weapons',id:0,count:0},{type:'weapons',id:1,count:0}]}),/保留一件/); + f.change({action:'batch',changes:[{type:'consumables',id:0,count:6},{type:'ornaments',id:1,count:0},{type:'joycoin',id:0,count:9}]}); + assert.equal(f.actor._consumables.itemNum(0),6); assert.equal(f.actor._ornaments.isUnlock(1),false); assert.equal(f.actor.joyCoinNum(),9); + const original=f.actor._ornaments.unlock; + f.actor._ornaments.unlock=()=>{throw Error('simulated failure');}; + assert.throws(()=>f.change({action:'batch',changes:[{type:'joycoin',id:0,count:20},{type:'consumables',id:0,count:1},{type:'ornaments',id:1,count:1}]}),/simulated/); + assert.equal(f.actor.joyCoinNum(),9); assert.equal(f.actor._consumables.itemNum(0),6); assert.equal(f.actor._ornaments.isUnlock(1),false); + f.actor._ornaments.unlock=original; +}); + +test('accessories equip and unequip; bells control extra capacity without altering map collection', () => { + const f=fixture(), o=f.actor._ornaments; + o._equip=[]; o._hasBuyBellNum=1; + o.bellData=()=>({name:'bell',desc:'capacity'}); o.eventBellGainNum=()=>1; + o.hasEquipNum=()=>o._equip.filter(id=>id!==20).length; o.maxEquip=()=>1+o._hasBuyBellNum+o.eventBellGainNum(); + o.unequip=id=>{o._equip=o._equip.filter(n=>n!==id);}; + o.equip=id=>{if(o.hasEquipNum()>=o.maxEquip())return false;if(!o.isEquip(id))o._equip.push(id);return true;}; + f.actor.bellNum=()=>o._hasBuyBellNum+o.eventBellGainNum(); + f.change({action:'equip',type:'ornaments',id:0,equipped:true}); assert.equal(o.isEquip(0),true); + f.change({action:'equip',type:'ornaments',id:0,equipped:false}); assert.equal(o.isEquip(0),false); + assert.throws(()=>f.change({action:'item',type:'bell',id:0,count:0}),/地图取得/); + f.change({action:'item',type:'bell',id:0,count:8}); assert.equal(o.maxEquip(),9); + o._equip=[0,1,2,3]; f.change({action:'item',type:'bell',id:0,count:1}); + assert.equal(o.hasEquipNum(),2); assert.equal(o.eventBellGainNum(),1); assert.equal(o._hasBuyBellNum,0); + assert.equal(f.api.read().items.find(i=>i.type==='bell').min,1); + const flags={'174,229,A':true,'222,6,A':false}; + f.context.DataManager={_cacheBell:[[174,229,'A'],[222,6,'A']]}; + f.context.$gameSelfSwitches={value:key=>!!flags[key.join(',')],setValue:(key,value)=>{flags[key.join(',')]=value;}}; + o.eventBellGainNum=()=>Number(flags['174,229,A'])+Number(flags['222,6,A']); + f.change({action:'item',type:'bell',id:0,count:0,adjustMapBells:true}); + assert.equal(flags['174,229,A'],false); assert.equal(f.actor.bellNum(),0); assert.equal(o.maxEquip(),1); + flags['174,229,A']=true; + o.unlock=()=>{throw Error('bell rollback');}; + assert.throws(()=>f.change({action:'batch',changes:[{type:'bell',id:0,count:0,adjustMapBells:true},{type:'ornaments',id:0,count:1}]}),/bell rollback/); + assert.equal(flags['174,229,A'],true); assert.equal(o._hasBuyBellNum,0); +}); + +test('trainer is usable in backpack menu and rejects title screen', async () => { + const f=fixture(); f.context.SceneManager._scene.constructor.name='Scene_Menu'; + const read=JSON.parse(await vm.runInContext(OPS.trainer(),f.context)); assert.equal(read.ok,true); + const changed=JSON.parse(await vm.runInContext(OPS.trainer({action:'item',type:'ornaments',id:0,count:0,subject:read.subject}),f.context)); + assert.equal(changed.ok,true); assert.equal(f.actor._ornaments.isUnlock(0),false); + f.context.SceneManager._scene.constructor.name='Scene_Title'; + assert.equal(JSON.parse(await vm.runInContext(OPS.trainer(),f.context)).ok,false); +}); diff --git a/tool/tests/world-map-ui.test.js b/tool/tests/world-map-ui.test.js new file mode 100644 index 0000000..bb7f5f3 --- /dev/null +++ b/tool/tests/world-map-ui.test.js @@ -0,0 +1,96 @@ +const test=require('node:test'); +const assert=require('node:assert/strict'); +const fs=require('node:fs'); +const path=require('node:path'); +const vm=require('node:vm'); + +function fixture() { + const source=fs.readFileSync(path.join(__dirname,'../web/app.js'),'utf8'); + const start=source.indexOf('// ---------- 世界地图 ----------'),end=source.indexOf('// ---------- 收集百科 ----------'); + const elements=new Map(),calls=[],messages=[]; + const $=key=>{ + if(!elements.has(key))elements.set(key,{value:'',checked:false,innerHTML:'',clientWidth:800,clientHeight:560, + setAttribute(){},addEventListener(){},setPointerCapture(){}}); + return elements.get(key); + }; + const data=JSON.parse(JSON.stringify({context:'current',regions:[{id:2,name:'小镇'}],pages:[{id:1},{id:2}], + player:{cellKey:'1:3:4',page:1,regionId:2},coverage:{message:'映射 3/3'}, + cells:[{key:'1:3:4',page:1,mapId:14,regionId:2,x:3,y:4,explored:true,walls:{}}, + {key:'2:3:4',page:2,mapId:99,regionId:2,x:3,y:4,walls:{}}], + markers:[false,true,null].map((collected,i)=>({key:'marker-'+i,cellKey:'1:3:4',type:'bell',id:0,collected,name:'铃铛',desc:'增加容量',official:true}))})); + const context=vm.createContext({$,CURRENT_CONTEXT:'current',CURRENT_GAME:{inMap:true},ITEM_ICONS:{},ITEM_TYPES:{bell:'铃铛'}, + esc:v=>String(v??''),itemArt:()=>'',loadItemIcons(){},refreshStatus:async()=>{}, + api:async(route,body)=>{calls.push({route,body});return route==='/world-map'?data:{ok:true};},toast:message=>messages.push(message)}); + // Use the actual shared SVG definition, including its encoding, for coin markers. + vm.runInContext(source.slice(source.indexOf('const COIN_ICON ='),source.indexOf('function invalidateTrainer')),context); + vm.runInContext(source.slice(start,end),context); + const run=code=>vm.runInContext(code,context); + context.fixtureData=data;run('WORLD_MAP=fixtureData');$('#world-map-page').value='1'; + return {$,data,calls,messages,context,run}; +} + +test('map JSON marker keys associate correctly; overlapping page coordinates stay separated',()=>{ + const f=fixture();f.run('renderWorldMap()'); + assert.match(f.$('#world-map-drawing').innerHTML,/data-world-cell="1:3:4"/); + assert.doesNotMatch(f.$('#world-map-drawing').innerHTML,/data-world-cell="2:3:4"/); + assert.equal((f.$('#world-map-drawing').innerHTML.match(/data-world-marker=/g)||[]).length,3); + f.run("WORLD_MAP_SELECTED='1:3:4';renderWorldMapDetail()"); + assert.match(f.$('#world-map-detail').innerHTML,/未收集 1 · 已收集 1 · 未知 1/); +}); + +test('mixed collected state never marks a group fully collected; unknown is not uncollected',()=>{ + const f=fixture(),state=f.run('worldMapState(WORLD_MAP.markers)'); + assert.equal(state.allCollected,false);assert.equal(state.missing,1);assert.equal(state.unknown,1); + f.$('#world-map-missing').checked=true;assert.equal(f.run("worldMapMarkers('1:3:4').length"),1); + f.run('renderWorldMap()');assert.doesNotMatch(f.$('#world-map-drawing').innerHTML,/data-world-marker="marker-[12]"/); +}); + +test('coin map markers render the shared double-rim paw SVG at every collection state',()=>{ + const f=fixture(); + for(const marker of f.data.markers)Object.assign(marker,{type:'coin',name:'金币',iconKey:'coin:0'}); + f.run('renderWorldMap()'); + const html=f.$('#world-map-drawing').innerHTML; + assert.equal((html.match(/data:image\/svg\+xml/g)||[]).length,3); + const svg=decodeURIComponent(f.run('COIN_ICON').split(',')[1]); + assert.match(svg,/viewBox="0 0 32 32"/); + assert.ok((svg.match(/=2); +}); + +test('map details explain conditions and branch sources without exposing JSON',()=>{ + const f=fixture();Object.assign(f.data.markers[0],{conditions:[{label:'已打开大门',met:true},{label:'击败守卫',met:false},{label:'剧情条件',met:null}],branch:true,sourceNote:'击打 8 次耗尽'}); + f.run("WORLD_MAP_SELECTED='1:3:4';renderWorldMapDetail()"); + const html=f.$('#world-map-detail').innerHTML; + assert.match(html,/✓ 已打开大门;✕ 击败守卫;\? 剧情条件;位于条件分支,需满足剧情条件;击打 8 次耗尽/); + assert.doesNotMatch(html,/"label"|"met"/); +}); + +test('travel uses cell coordinates and captured context, not marker event coordinates',async()=>{ + const f=fixture();await f.run("travelWorldMap('1:3:4')"); + assert.deepEqual(JSON.parse(JSON.stringify(f.calls)),[{route:'/world-map/travel',body:{context:'current',mapId:14,cellX:3,cellY:4,page:1}}]); +}); + +test('stale contexts and game menus reject travel before any request',async()=>{ + const f=fixture();f.context.CURRENT_CONTEXT='changed';await f.run("travelWorldMap('1:3:4')"); + assert.equal(f.calls.length,0);assert.match(f.messages.at(-1),/刷新地图/); + f.context.CURRENT_CONTEXT='current';f.context.CURRENT_GAME.inMap=false;await f.run("travelWorldMap('1:3:4')"); + assert.equal(f.calls.length,0);assert.match(f.messages.at(-1),/关闭游戏菜单/); +}); + +test('drag does not select or travel, but a tap selects by stable cell key',()=>{ + const f=fixture(),viewport=f.$('#world-map-viewport'); + const target={closest:selector=>selector==='[data-world-cell]'?{dataset:{worldCell:'1:3:4'}}:null}; + const down={button:0,pointerId:1,clientX:10,clientY:20,target,currentTarget:viewport}; + viewport.onpointerdown(down);viewport.onpointermove({...down,clientX:40});viewport.onpointerup(down); + assert.equal(f.run('WORLD_MAP_SELECTED'),null);assert.equal(f.calls.length,0); + viewport.onpointerdown(down);viewport.onpointerup(down); + assert.equal(f.run('WORLD_MAP_SELECTED'),'1:3:4');assert.equal(f.calls.length,0); +}); + +test('poll refresh preserves selection and zoom; late response cannot restore invalidated data',async()=>{ + const f=fixture();f.run("WORLD_MAP_FITTED=true;WORLD_MAP_VIEW={x:123,y:234,scale:1.75};WORLD_MAP_SELECTED='1:3:4'"); + await f.run('loadWorldMap()');assert.equal(f.run('WORLD_MAP_VIEW.scale'),1.75);assert.equal(f.run('WORLD_MAP_VIEW.x'),123); + assert.equal(f.run('WORLD_MAP_SELECTED'),'1:3:4'); + let finish;f.context.api=()=>new Promise(resolve=>{finish=resolve;}); + const pending=f.run('loadWorldMap()');f.context.CURRENT_CONTEXT='new-save';f.run('invalidateWorldMap()');finish(f.data);await pending; + assert.equal(f.run('WORLD_MAP'),null);assert.equal(f.run('WORLD_MAP_SELECTED'),null); +}); diff --git a/tool/tests/world-map.test.js b/tool/tests/world-map.test.js new file mode 100644 index 0000000..13076d0 --- /dev/null +++ b/tool/tests/world-map.test.js @@ -0,0 +1,27 @@ +const test=require('node:test'); +const assert=require('node:assert/strict'); +const vm=require('node:vm'); +const {readWorldMap}=require('../lib/world-map'); + +test('world schematic preserves page boundaries, per-item collection and unexplored cells without writes',()=>{ + const cells=[{x:1,y:2,page:1,mapId:7,regionId:2,l:true,pt:true}, + {x:1,y:2,page:2,mapId:8,regionId:3,hidden:true}]; + const locations=[{key:'a',type:'hpst',id:0,mapId:7,eventId:1,x:4,y:4,collected:true,official:true}, + {key:'b',type:'hpst',id:0,mapId:7,eventId:2,x:4,y:4,collected:false,official:true}, + {key:'c',type:'hpst',id:0,mapId:8,eventId:3,x:4,y:4,collected:null,official:false}]; + const encyclopedia={items:[{type:'hpst',id:0,name:'强化',desc:'效果'}],locations}; + const before=JSON.stringify({cells,encyclopedia}); + const context=vm.createContext({encyclopedia,DataManager:{_thumbnailMapCell:{a:cells[0],b:cells[1]}, + makePosKey:(x,y)=>x+':'+y,getThumbnailCell:(x,y,id)=>cells.find(c=>c.mapId===id),getCurrentThumbnailCell:()=>cells[1]}, + $gameSystem:{isThumbnailActive:()=>false},$dataMapInfos:{},$dataRegion:{2:{name:'森林',exploreItem:[[7,1],[7,2],[7,9]]},3:{name:'里'}}, + $gameMap:{mapId:()=>8},$gamePlayer:{_x:5,_y:6}}); + const r=JSON.parse(vm.runInContext(`JSON.stringify((${readWorldMap.toString()})(encyclopedia))`,context)); + assert.equal(r.cells.length,2);assert.equal(r.pages.length,2); + assert.deepEqual(r.markers.map(m=>m.collected),[true,false,null]); + assert.deepEqual(r.markers.map(m=>m.cellKey),['1:1:2','1:1:2','2:1:2']); + assert.equal(r.cells[0].walls.left,true);assert.equal(r.cells[0].exits.top,true); + assert.equal(r.cells[1].explored,false);assert.equal(r.cells[1].hidden,true); + assert.equal(r.player.cellKey,'2:1:2');assert.equal(r.markers[0].name,'强化'); + assert.equal(r.coverage.mappedOfficial,2);assert.equal(r.coverage.officialTotal,3);assert.deepEqual(r.coverage.missing,['7:9']); + assert.equal(JSON.stringify({cells,encyclopedia}),before); +}); diff --git a/tool/web/app.js b/tool/web/app.js index 4583b09..1a25297 100644 --- a/tool/web/app.js +++ b/tool/web/app.js @@ -40,8 +40,10 @@ let EVENTS_CONTEXT = null; let LOST_CONTEXT = null; let STATUS_BUSY = false; let LAST_MAP = null; // 上一次看到的地图号,用来检测换图 - let TRAINER = null, TRAINER_READING = false, TRAINER_WRITING = false, TRAINER_REVISION = 0; +let ENCYCLOPEDIA = null, ENCYCLOPEDIA_BUSY = false, ENCYCLOPEDIA_REVISION = 0; +let ITEM_ICONS = {}, ICON_CONTEXT = null, ICON_REQUEST = null; +let COMBAT_CONTEXT=null; // ---------- 基础工具 ---------- async function api(path, body) { @@ -71,16 +73,36 @@ function esc(s) { // ---------- 角色与背包 ---------- const STAT_NAMES = { coin: '金币', hp: '生命 HP', sp: '体力 SP', zp: '能量 ZP' }; -const ITEM_TYPES = { consumables: '消耗品', weapons: '武器', stones: '宝石', ornaments: '饰品', joycoin: '特殊材料', hpst: '特殊材料', skillst: '特殊材料' }; +const ITEM_TYPES = { consumables: '食物', weapons: '咸鱼(武器)', stones: '鱼眼宝石', ornaments: '配饰', joycoin: '材料与铃铛', hpst: '材料与铃铛', skillst: '材料与铃铛', bell: '材料与铃铛', coin: '金币' }; +// Gold has no inventory thumbnail; use a vector coin that stays sharp at map zoom levels. +const COIN_ICON = 'data:image/svg+xml,' + encodeURIComponent(` + + + + + + + + + + + +`); function invalidateTrainer(message = '请先连接游戏并进入地图') { TRAINER = null; TRAINER_REVISION++; + ITEM_ICONS = {}; ICON_CONTEXT = null; $('#trainer-role').textContent = message; $('#trainer-stats').replaceChildren(); + $('#trainer-capacity').textContent = ''; + $('#trainer-item-summary').textContent = ''; $('#trainer-items').innerHTML = `

`; + trainerDisabled(); } function renderTrainer(resetInputs = false) { if (!TRAINER) return; $('#trainer-role').textContent = `当前操作角色:${TRAINER.role}`; + const capacity = TRAINER.capacity; + $('#trainer-capacity').textContent = capacity ? `配饰栏位:已用 ${capacity.used} / ${capacity.max};地图铃铛 ${capacity.mapBells},额外铃铛 ${capacity.extraBells}。调低容量会自动卸下超出的配饰。` : ''; for (const [field, label] of Object.entries(STAT_NAMES)) { const stat = TRAINER.stats[field]; let row = $('#trainer-stat-' + field); @@ -104,19 +126,45 @@ function renderTrainer(resetInputs = false) { } function numericInput(input) { return input.value.trim() === '' ? null : Number(input.value); } function trainerDisabled() { - $$('#trainer-stats button, #trainer-stats input, #trainer-items button, #trainer-items input').forEach(el => { el.disabled = TRAINER_WRITING || !TRAINER; }); + $$('#trainer-stats button, #trainer-stats input, #trainer-items button, #trainer-items input, #trainer-give-filtered, #trainer-clear-filtered').forEach(el => { el.disabled = TRAINER_WRITING || !TRAINER; }); $('#trainer-refresh').disabled = TRAINER_WRITING || TRAINER_READING; } +function filteredTrainerItems() { + if (!TRAINER) return []; + const query = $('#trainer-search').value.trim().toLowerCase(), category = $('#trainer-category').value; + return TRAINER.items.filter(item => + (!category || category === item.type || (category === 'special' && ITEM_TYPES[item.type] === '材料与铃铛')) && + (!$('#trainer-owned').checked || item.count > 0) && `${item.name} ${item.desc} ${item.id}`.toLowerCase().includes(query)); +} +function itemArt(item) { + const key=item.type+':'+item.id, src=item.type==='coin'?COIN_ICON:ITEM_ICONS[key]; + return `${src ? `` : ''}`; +} +async function loadItemIcons(force = false) { + const context = CURRENT_CONTEXT; + if (!context || (!force && ICON_CONTEXT === context) || ICON_REQUEST?.context === context) return; + const request={context}; ICON_REQUEST=request; + try { + const result=await api('/item-icons'); + if (CURRENT_CONTEXT !== context || result.context !== context || ICON_REQUEST !== request) return; + ITEM_ICONS=Object.fromEntries(Object.entries(result.icons).filter(([,src])=>typeof src==='string' && src.startsWith('data:image/png;base64,'))); + ICON_CONTEXT=context; + // Update thumbnails in place so a late image response cannot replace typed quantities. + $$('[data-item-icon]').forEach(el=>{const src=ITEM_ICONS[el.dataset.itemIcon]; if(src) el.innerHTML=``;}); + if (WORLD_MAP) renderWorldMap(); + } catch(e) { if (CURRENT_CONTEXT===context) toast('物品图标暂时未加载:'+e.message,'err'); } + finally { if (ICON_REQUEST===request) ICON_REQUEST=null; } +} function renderTrainerItems() { if (!TRAINER) return; - const query = $('#trainer-search').value.trim().toLowerCase(), category = $('#trainer-category').value; - const items = TRAINER.items.filter(item => - (!category || category === item.type || (category === 'special' && ITEM_TYPES[item.type] === '特殊材料')) && - (!$('#trainer-owned').checked || item.count > 0) && `${item.name} ${item.desc} ${item.id}`.toLowerCase().includes(query)); + const items = filteredTrainerItems(); + const adjustMapBells = $('#trainer-map-bells').checked; + $('#trainer-item-summary').textContent = `显示 ${items.length} / ${TRAINER.items.length} 项(含未拥有物品)`; $('#trainer-items').innerHTML = items.map(item => { - const counted = item.type === 'consumables' || ITEM_TYPES[item.type] === '特殊材料'; + const counted = item.type === 'consumables' || ITEM_TYPES[item.type] === '材料与铃铛'; const max = item.type === 'consumables' && $('#trainer-overcap').checked ? 99999 : item.max; - return `
${esc(item.name)} ${item.equipped ? '已装备' : ''}
${ITEM_TYPES[item.type]} #${item.id} · 已有 ${item.count}${counted ? ` · 上限 ${item.max}` : ''}
${esc(item.desc)}
${counted ? `` : ``}
`; + const equip = !counted && item.count && !(item.type === 'weapons' && item.equipped) ? `` : ''; + return `
${itemArt(item)}
${esc(item.name)} ${item.count ? '已拥有' : '未拥有'} ${item.equipped ? '已装备' : ''}
${ITEM_TYPES[item.type]} #${item.id} · 已有 ${item.count}${counted ? ` · 上限 ${item.max}` : ''}
${esc(item.desc)}
${counted ? `` : `${equip}`}
`; }).join('') || '
没有匹配物品
'; trainerDisabled(); } @@ -129,6 +177,7 @@ async function loadTrainer(resetInputs = false) { if (revision !== TRAINER_REVISION || context !== CURRENT_CONTEXT || result.context !== context) return; const changed = TRAINER?.subject !== result.subject; TRAINER = result; renderTrainer(resetInputs || changed); + loadItemIcons(); } catch (e) { if (revision === TRAINER_REVISION) invalidateTrainer('读取失败:' + e.message); } finally { TRAINER_READING = false; trainerDisabled(); } @@ -141,19 +190,283 @@ async function changeTrainer(body) { const result = await api('/trainer', { ...body, context, subject }); if (revision !== TRAINER_REVISION || CURRENT_CONTEXT !== context) return; TRAINER = result; renderTrainer(true); + invalidateEncyclopedia('背包已修改,请刷新百科以更新数量'); toast(body.locked === false ? '已解除锁定' : '已修改', 'ok'); } catch (e) { toast(e.message, 'err'); } finally { TRAINER_WRITING = false; trainerDisabled(); } } -$('#trainer-refresh').onclick = () => loadTrainer(true); +$('#trainer-refresh').onclick = () => { loadTrainer(true); loadItemIcons(true); }; $('#trainer-search').oninput = renderTrainerItems; -['trainer-category', 'trainer-owned', 'trainer-overcap'].forEach(id => { $('#' + id).onchange = renderTrainerItems; }); +['trainer-category', 'trainer-owned', 'trainer-overcap', 'trainer-map-bells'].forEach(id => { $('#' + id).onchange = renderTrainerItems; }); $('#trainer-items').onclick = ev => { const button = ev.target.closest('button'); if (!button) return; const row = button.closest('[data-type]'); + const item = TRAINER?.items.find(i => i.type === row.dataset.type && i.id === Number(row.dataset.id)); + if (!item) return; + if (button.hasAttribute('data-equip')) return changeTrainer({action:'equip',type:item.type,id:item.id,equipped:button.dataset.equip === '1'}); + const max = item.type === 'consumables' && $('#trainer-overcap').checked ? 99999 : item.max; + const adjustMapBells = $('#trainer-map-bells').checked; changeTrainer({ action: 'item', type: row.dataset.type, id: Number(row.dataset.id), - count: button.hasAttribute('data-count') ? numericInput(row.querySelector('input')) : Number(button.dataset.owned), overCap: $('#trainer-overcap').checked }); + count: button.hasAttribute('data-count') ? numericInput(row.querySelector('input')) : button.hasAttribute('data-add') ? Math.min(max, item.count + 1) : button.hasAttribute('data-remove') ? (item.type==='bell'&&adjustMapBells ? 0 : item.min || 0) : Number(button.dataset.owned), overCap: $('#trainer-overcap').checked, adjustMapBells }); +}; +async function batchTrainer(remove) { + if (!TRAINER || TRAINER_WRITING) return; + const adjustMapBells = $('#trainer-map-bells').checked; + const changes = filteredTrainerItems().filter(i => !(remove && i.type === 'weapons' && i.equipped)).map(i => ({ type:i.type, id:i.id, + count: remove ? (i.type==='bell'&&adjustMapBells ? 0 : i.min || 0) : i.type === 'consumables' ? Math.max(i.count, i.max) : ['weapons','stones','ornaments'].includes(i.type) ? 1 : Math.min(i.max, i.count + 1), overCap: i.count > i.max, adjustMapBells })); + if (!changes.length) return toast('没有可修改的筛选结果'); + if (!confirm(`${remove ? '清除' : '获得'}当前筛选的 ${changes.length} 项物品?${remove ? '保留当前武器,移除的装备会卸下。' + (adjustMapBells ? '已允许重置地图铃铛收集点。' : '保留地图铃铛记录。') : '食物补满,装备添加,各材料与铃铛加 1。'}游戏存档会保留修改结果。`)) return; + await changeTrainer({action:'batch',changes}); +} +$('#trainer-give-filtered').onclick = () => batchTrainer(false); +$('#trainer-clear-filtered').onclick = () => batchTrainer(true); + +// ---------- 战斗 ---------- +async function loadCombat(reset=false){ + if(!CURRENT_CONTEXT)return;const context=CURRENT_CONTEXT; + try{const r=await api('/combat');if(context!==CURRENT_CONTEXT||r.context!==context)return; + if(reset||COMBAT_CONTEXT!==context){$('#combat-outgoing').value=r.outgoing;$('#combat-incoming').value=r.incoming;COMBAT_CONTEXT=context;} + $('#combat-onehit').checked=r.oneHit;$('#combat-status').textContent=`当前:伤害 ${r.outgoing} 倍 · 受击 ${r.incoming} 倍 · ${r.oneHit?'一击秒杀开启':'秒杀关闭'}。作用于玩家与莉莉娅;不突破剧情无敌,受击 0 倍仍可能有击退。`; + }catch(e){$('#combat-status').textContent=e.message;} +} +async function changeCombat(body){try{await api('/combat',body);await loadCombat(true);toast('战斗设置已更新','ok');}catch(e){toast(e.message,'err');}} +$('#combat-apply').onclick=()=>changeCombat({outgoing:numericInput($('#combat-outgoing')),incoming:numericInput($('#combat-incoming'))}); +$('#combat-onehit').onchange=()=>changeCombat({oneHit:$('#combat-onehit').checked}); +$('#combat-reset').onclick=()=>changeCombat({outgoing:1,incoming:1,oneHit:false}); +// ---------- 世界地图 ---------- +let WORLD_MAP = null, WORLD_MAP_REQUEST = null, WORLD_MAP_REVISION = 0, WORLD_MAP_TRAVELLING = false; +let WORLD_MAP_SELECTED = null, WORLD_MAP_MARKER = null, WORLD_MAP_VIEW = {x:0,y:0,scale:1}, WORLD_MAP_FITTED = false; +const WORLD_MAP_SIZE = 52; +function worldMapState(markers) { + const missing=markers.filter(m=>m.collected===false).length, collected=markers.filter(m=>m.collected===true).length; + return {missing,collected,unknown:markers.length-missing-collected,allCollected:markers.length>0&&collected===markers.length}; +} +function worldMapStatus(marker) { return marker.collected===true?'已收集':marker.collected===false?'未收集':'状态未知(脚本条件)'; } +function worldMapConditions(marker) { + return (marker.conditions||[]).map(c=>typeof c==='string'?c:`${c.met===true?'✓':c.met===false?'✕':'?'} ${c.label||'未说明的条件'}`) + .concat(marker.branch?['位于条件分支,需满足剧情条件']:[]) + .concat(marker.sourceNote?[marker.sourceNote]:[]).join(';'); +} +function worldMapHTML(selector,html) { + const node=$(selector); if(node._worldHTML===html)return; + const scroll=node.scrollTop; node.innerHTML=html; node._worldHTML=html; node.scrollTop=scroll; +} +function invalidateWorldMap(message='地图或存档变化,正在更新地图…') { + WORLD_MAP=null; WORLD_MAP_REVISION++; WORLD_MAP_REQUEST=null; WORLD_MAP_SELECTED=null; WORLD_MAP_MARKER=null; + worldMapHTML('#world-map-drawing',''); $('#world-map-summary').textContent=message; + $('#world-map-empty').textContent=message; $('#world-map-empty').hidden=false; + worldMapHTML('#world-map-detail','

点击地图上的方格或收集图标查看详情。

'); +} +async function loadWorldMap() { + if (!CURRENT_CONTEXT || WORLD_MAP_REQUEST) return; + const request={context:CURRENT_CONTEXT,revision:WORLD_MAP_REVISION}; WORLD_MAP_REQUEST=request; + try { + const result=await api('/world-map'); + if(WORLD_MAP_REQUEST!==request || request.context!==CURRENT_CONTEXT || request.revision!==WORLD_MAP_REVISION || result.context!==CURRENT_CONTEXT)return; + WORLD_MAP=result; + const region=$('#world-map-region').value, type=$('#world-map-type').value; + $('#world-map-region').innerHTML=''+result.regions.map(r=>``).join(''); + $('#world-map-region').value=result.regions.some(r=>String(r.id)===region)?region:''; + const types=[...new Set(result.markers.map(m=>m.type))]; + $('#world-map-type').innerHTML=''+types.map(t=>``).join(''); + $('#world-map-type').value=types.includes(type)?type:''; + worldMapPages(); renderWorldMap(); if(!WORLD_MAP_FITTED)fitWorldMap(); loadItemIcons(); + } catch(e) { if(WORLD_MAP_REQUEST===request){$('#world-map-summary').textContent='地图读取失败:'+e.message;} } + finally { if(WORLD_MAP_REQUEST===request)WORLD_MAP_REQUEST=null; } +} +function worldMapPages() { + if(!WORLD_MAP)return; + const region=$('#world-map-region').value, old=$('#world-map-page').value; + const pages=WORLD_MAP.pages.filter(p=>WORLD_MAP.cells.some(c=>String(c.page)===String(p.id)&&(!region||String(c.regionId)===region))); + $('#world-map-page').innerHTML=pages.map(p=>``).join(''); + const player=String(WORLD_MAP.player?.page); + $('#world-map-page').value=pages.some(p=>String(p.id)===old)?old:pages.some(p=>String(p.id)===player)?player:String(pages[0]?.id??''); +} +function worldMapCells() { + if(!WORLD_MAP)return[]; + const page=$('#world-map-page').value, region=$('#world-map-region').value; + return WORLD_MAP.cells.filter(c=>String(c.page)===page&&(!region||String(c.regionId)===region)); +} +function worldMapMarkers(cellKey) { + const type=$('#world-map-type').value, missing=$('#world-map-missing').checked; + return (WORLD_MAP?.markers||[]).filter(m=>m.cellKey===cellKey&&(!type||m.type===type)&&(!missing||m.collected===false)); +} +function renderWorldMap() { + if(!WORLD_MAP)return; + const cells=worldMapCells(), size=WORLD_MAP_SIZE; + const byPosition=new Map(cells.map(c=>[`${c.x}:${c.y}`,c])); + let shown=0; + worldMapHTML('#world-map-drawing',cells.map(c=>{ + const markers=worldMapMarkers(c.key); shown+=markers.length; + const x=c.x*size,y=c.y*size; + const title=`${c.mapName||'房间 '+c.mapId} · 分块 (${c.x}, ${c.y}) · ${c.explored?'已探索':'未探索'}${c.hidden?' · 隐藏区域':''}`; + const edges=[['left',-1,0,0,0,0,size],['top',0,-1,0,0,size,0],['right',1,0,size,0,size,size],['bottom',0,1,0,size,size,size]]; + const lines=edges.map(([side,dx,dy,x1,y1,x2,y2])=>{ + const neighbor=byPosition.get(`${c.x+dx}:${c.y+dy}`); + if(neighbor&&neighbor.mapId===c.mapId)return ''; + const open=c.walls?.[side]===false||!!c.exits?.[side]; + // Door gaps preserve the original room outline without joining unrelated pages. + if(open) { + const ax=x1+(x2-x1)*.3,ay=y1+(y2-y1)*.3,bx=x1+(x2-x1)*.7,by=y1+(y2-y1)*.7; + return ``; + } + return ``; + }).join(''); + const art=markers.map((m,i)=>{ + const mx=markers.length===1?14:5+(i%2)*23,my=markers.length<=2?14:5+Math.floor(i/2)*23; + const src=m.type==='coin'?COIN_ICON:ITEM_ICONS[m.iconKey||m.type+':'+m.id]; + return `${esc(m.name)} · ${worldMapStatus(m)}\n${esc(m.desc)}${src?``:`${esc((ITEM_TYPES[m.type]||m.type||'物').slice(0,1))}`}${m.collected!==false?`${m.collected===true?'✓':'?'}`:''}`; + }).join(''); + return `${esc(title)}${lines}${art}${c.key===WORLD_MAP.player?.cellKey?'':''}`; + }).join('')); + $('#world-map-empty').hidden=!!cells.length; $('#world-map-empty').textContent='该区域在本地图页没有分块'; + $('#world-map-summary').textContent=`${cells.length} 个分块 · ${shown} 个收集点 · ${Math.round(WORLD_MAP_VIEW.scale*100)}%`; + const coverage=WORLD_MAP.coverage; + $('#world-map-coverage').textContent=typeof coverage==='string'?coverage:coverage?.message||'包含未探索分块;未知收集状态请查看详情条件。'; + applyWorldMapView(); renderWorldMapDetail(); +} +function applyWorldMapView() { + const v=WORLD_MAP_VIEW; + $('#world-map-drawing').setAttribute('transform',`translate(${v.x},${v.y}) scale(${v.scale})`); +} +function fitWorldMap() { + const cells=worldMapCells(); if(!cells.length)return; + const box=$('#world-map-viewport'), minX=Math.min(...cells.map(c=>c.x))*WORLD_MAP_SIZE,minY=Math.min(...cells.map(c=>c.y))*WORLD_MAP_SIZE; + const width=(Math.max(...cells.map(c=>c.x))*WORLD_MAP_SIZE-minX+WORLD_MAP_SIZE),height=(Math.max(...cells.map(c=>c.y))*WORLD_MAP_SIZE-minY+WORLD_MAP_SIZE); + const scale=Math.max(.12,Math.min(2,(box.clientWidth-36)/width,(box.clientHeight-36)/height)); + WORLD_MAP_VIEW={scale,x:(box.clientWidth-width*scale)/2-minX*scale,y:(box.clientHeight-height*scale)/2-minY*scale}; + WORLD_MAP_FITTED=true; renderWorldMap(); +} +function zoomWorldMap(factor,x,y) { + const box=$('#world-map-viewport'),v=WORLD_MAP_VIEW; + x??=box.clientWidth/2; y??=box.clientHeight/2; + const scale=Math.min(4,Math.max(.12,v.scale*factor)),ratio=scale/v.scale; + WORLD_MAP_VIEW={scale,x:x-(x-v.x)*ratio,y:y-(y-v.y)*ratio};renderWorldMap(); +} +function renderWorldMapDetail() { + const cell=WORLD_MAP?.cells.find(c=>c.key===WORLD_MAP_SELECTED); + if(!cell){worldMapHTML('#world-map-detail','

点击地图上的方格或收集图标查看详情。

');return;} + const markers=WORLD_MAP.markers.filter(m=>m.cellKey===cell.key), state=worldMapState(markers); + worldMapHTML('#world-map-detail',`

${esc(cell.mapName||'房间 '+cell.mapId)}

地图 #${cell.mapId} · 分块 (${cell.x}, ${cell.y})
${cell.explored?'已探索':'未探索'}${cell.hidden?' · 隐藏区域':''}

目标分块附近选择空处,抵达后悬停并开启碰撞。请先关闭游戏菜单回到地图。

未收集 ${state.missing} · 已收集 ${state.collected} · 未知 ${state.unknown}

${markers.map(m=>`
${itemArt(m)}${esc(m.name||ITEM_TYPES[m.type]||m.type)}
${worldMapStatus(m)}${!m.official?'额外脚本点':''}

${esc(m.desc||'暂无用途说明')}

${worldMapConditions(m)?`

条件与来源:${esc(worldMapConditions(m))}

`:''}
`).join('')||'

此分块没有已知收集点。

'}`); + $('#world-map-travel').onclick=()=>travelWorldMap(cell.key); +} +async function travelWorldMap(key) { + if(!WORLD_MAP||WORLD_MAP_TRAVELLING)return; + if(WORLD_MAP.context!==CURRENT_CONTEXT)return toast('地图或存档已变化,请刷新地图','err'); + if(!CURRENT_GAME?.inMap)return toast('请先关闭游戏菜单回到地图,再传送','err'); + const cell=WORLD_MAP.cells.find(c=>c.key===key); if(!cell)return; + const context=WORLD_MAP.context; WORLD_MAP_TRAVELLING=true; renderWorldMapDetail(); + try {const result=await api('/world-map/travel',{context,mapId:cell.mapId,cellX:cell.x,cellY:cell.y,page:cell.page});toast(result.positionNote||'已传送到目标分块附近,悬停中,碰撞已恢复','ok');await refreshStatus();} + catch(e){toast(e.message,'err');}finally{WORLD_MAP_TRAVELLING=false;renderWorldMapDetail();} +} +$('#world-map-refresh').onclick=()=>loadWorldMap(); +$('#world-map-region').onchange=()=>{WORLD_MAP_SELECTED=null;WORLD_MAP_MARKER=null;worldMapPages();fitWorldMap();}; +$('#world-map-page').onchange=()=>{WORLD_MAP_SELECTED=null;WORLD_MAP_MARKER=null;fitWorldMap();}; +$('#world-map-type').onchange=renderWorldMap; $('#world-map-missing').onchange=renderWorldMap; +$('#world-map-fit').onclick=fitWorldMap; +$('#world-map-plus').onclick=()=>zoomWorldMap(1.25); $('#world-map-minus').onclick=()=>zoomWorldMap(.8); +$('#world-map-locate').onclick=()=>{ + if(!WORLD_MAP?.player?.cellKey)return toast('当前房间不在原版地图布局中'); + const cell=WORLD_MAP.cells.find(c=>c.key===WORLD_MAP.player.cellKey); if(!cell)return; + $('#world-map-region').value=String(cell.regionId);worldMapPages();$('#world-map-page').value=String(cell.page); + WORLD_MAP_SELECTED=cell.key;WORLD_MAP_MARKER=null;fitWorldMap(); +}; +let WORLD_MAP_DRAG=null; +$('#world-map-viewport').onpointerdown=e=>{ + if(e.button!==0)return; + WORLD_MAP_DRAG={id:e.pointerId,x:e.clientX,y:e.clientY,vx:WORLD_MAP_VIEW.x,vy:WORLD_MAP_VIEW.y,moved:false,target:e.target}; + e.currentTarget.setPointerCapture(e.pointerId); +}; +$('#world-map-viewport').onpointermove=e=>{ + const drag=WORLD_MAP_DRAG;if(!drag||drag.id!==e.pointerId)return; + const dx=e.clientX-drag.x,dy=e.clientY-drag.y;if(Math.hypot(dx,dy)>5)drag.moved=true; + if(drag.moved){WORLD_MAP_VIEW.x=drag.vx+dx;WORLD_MAP_VIEW.y=drag.vy+dy;applyWorldMapView();} +}; +$('#world-map-viewport').onpointerup=e=>{ + const drag=WORLD_MAP_DRAG;WORLD_MAP_DRAG=null;if(!drag||drag.moved)return; + const cell=drag.target.closest('[data-world-cell]'); if(!cell)return; + WORLD_MAP_SELECTED=cell.dataset.worldCell;WORLD_MAP_MARKER=drag.target.closest('[data-world-marker]')?.dataset.worldMarker||null;renderWorldMap(); +}; +$('#world-map-viewport').onpointercancel=()=>{WORLD_MAP_DRAG=null;}; +$('#world-map-viewport').addEventListener('wheel',e=>{e.preventDefault();const r=e.currentTarget.getBoundingClientRect();zoomWorldMap(e.deltaY<0?1.15:1/1.15,e.clientX-r.left,e.clientY-r.top);},{passive:false}); +$('#world-map-viewport').onkeydown=e=>{ + if(e.key==='+'||e.key==='='){e.preventDefault();zoomWorldMap(1.25);}else if(e.key==='-'){e.preventDefault();zoomWorldMap(.8);} + else if(['ArrowLeft','ArrowRight','ArrowUp','ArrowDown'].includes(e.key)){e.preventDefault();WORLD_MAP_VIEW.x+=e.key==='ArrowLeft'?40:e.key==='ArrowRight'?-40:0;WORLD_MAP_VIEW.y+=e.key==='ArrowUp'?40:e.key==='ArrowDown'?-40:0;applyWorldMapView();} +}; + +// ---------- 收集百科 ---------- +function invalidateEncyclopedia(message = '请刷新百科') { + ENCYCLOPEDIA = null; ENCYCLOPEDIA_REVISION++; + $('#encyclopedia-summary').textContent = message; + $('#encyclopedia-regions').replaceChildren(); + $('#encyclopedia-items').innerHTML = `
${esc(message)}
`; + $('#encyclopedia-coverage').textContent = ''; +} +async function loadEncyclopedia() { + if (ENCYCLOPEDIA_BUSY || !CURRENT_CONTEXT) return; + const context = CURRENT_CONTEXT, revision = ENCYCLOPEDIA_REVISION; + ENCYCLOPEDIA_BUSY = true; $('#encyclopedia-refresh').disabled = true; + $('#encyclopedia-summary').textContent = '正在读取物品与地图收集记录…'; + try { + const result = await api('/encyclopedia'); + if (revision !== ENCYCLOPEDIA_REVISION || context !== CURRENT_CONTEXT || result.context !== context) return; + ENCYCLOPEDIA = result; + const area = $('#encyclopedia-area'), previous = area.value; + const areas = [...new Set(result.locations.map(l => l.area).concat(result.shops.map(l => l.area)))].sort((a,b) => a.localeCompare(b,'zh-CN')); + area.innerHTML = '' + areas.map(a => ``).join(''); + if (areas.includes(previous)) area.value = previous; + $('#encyclopedia-summary').textContent = `${result.stats.items} 种物品 · ${result.stats.locations} 条地图来源 · 地图收集 ${result.stats.collected} / ${result.stats.total} · ${result.stats.shops} 处商店入口`; + $('#encyclopedia-coverage').textContent = result.coverage; + $('#encyclopedia-regions').innerHTML = result.regions.map(r => ``).join(''); + renderEncyclopedia(); + } catch(e) { + if (revision === ENCYCLOPEDIA_REVISION) invalidateEncyclopedia('读取失败:' + e.message); + } finally { ENCYCLOPEDIA_BUSY = false; $('#encyclopedia-refresh').disabled = false; } +} +function renderEncyclopedia() { + if (!ENCYCLOPEDIA) return; + const query = $('#encyclopedia-search').value.trim().toLowerCase(), type = $('#encyclopedia-category').value; + const area = $('#encyclopedia-area').value, status = $('#encyclopedia-status').value; + const matchStatus = l => status === 'uncollected' ? l.collected === false : status === 'collected' ? l.collected === true : status === 'unknown' ? l.collected === null : true; + const rows = []; + for (const item of ENCYCLOPEDIA.items) { + if (type && type !== item.type && !(type === 'special' && ITEM_TYPES[item.type] === '材料与铃铛')) continue; + if ((status === 'owned' && item.count <= 0) || (status === 'missing' && item.count > 0)) continue; + const textMatch = `${item.name} ${item.desc} ${item.id}`.toLowerCase().includes(query); + const locations = item.locations.filter(l => (!area || l.area === area) && matchStatus(l) && (textMatch || `${l.area} ${l.mapName} ${l.mapId} ${l.eventName}`.toLowerCase().includes(query))); + if (['uncollected','collected','unknown'].includes(status) && !locations.length) continue; + const shopMatches = item.shopCandidate ? ENCYCLOPEDIA.shops.filter(l => (!area || l.area === area) && (textMatch || `${l.area} ${l.mapName} ${l.mapId}`.toLowerCase().includes(query))) : []; + if (area && !locations.length && !shopMatches.length) continue; + if (!textMatch && !locations.length && !shopMatches.length) continue; + const got = item.locations.filter(l => l.collected === true).length, missed = item.locations.filter(l => l.collected === false).length; + const locationHTML = locations.map(l => { + const label = l.collected === true ? '地图已收集' : l.collected === false ? '地图未收集' : '收集状态未知'; + const conditions = l.conditions.map(c => `${c.met === true ? '✓' : c.met === false ? '✕' : '?'} ${c.label}`).concat(l.branch ? ['位于条件分支,需满足剧情条件'] : []).concat(l.sourceNote ? [l.sourceNote] : []); + return `
${label} ${esc(l.area)} · ${esc(l.mapName)} (#${l.mapId})
事件 ${l.eventId} · 第 ${l.pageIndex+1} 页 · 坐标 (${l.x}, ${l.y})${l.amount > 1 ? ` · 数量 ${l.amount}` : ''}${l.official ? ' · 计入地图收集率' : ''}
${conditions.length ? `
${esc(conditions.join(';'))}
` : ''}
`; + }).join(''); + const shopHTML = item.shopCandidate && !['uncollected','collected','unknown'].includes(status) ? `
商店候选 · 基础价格 ${item.price} 金币 · ${shopMatches.length} 处入口

是否售卖受任务及购买进度影响,请到店确认。此处列的是商店入口,不代表每家店当前都有库存。

${shopMatches.map(l => `
${esc(l.area)} · ${esc(l.mapName)} (#${l.mapId}) · (${l.x}, ${l.y})
`).join('')}
` : ''; + rows.push(`
${esc(item.name)} ${ITEM_TYPES[item.type]}当前 ${item.count > 0 ? `拥有 ${item.count}` : '未拥有'} · 地图已收集 ${got} / 未收集 ${missed} · ${item.locations.length} 条来源

${esc(item.desc)}

${locationHTML || '

当前筛选下没有已定位的地图来源。

'}${shopHTML}
`); + } + $('#encyclopedia-items').innerHTML = rows.join('') || '
没有符合筛选条件的物品或地点。
'; +} +async function goCatalogLocation(location) { + if (!location || !ENCYCLOPEDIA || ENCYCLOPEDIA.context !== CURRENT_CONTEXT) return toast('请刷新百科后重试', 'err'); + if (!CURRENT_GAME?.inMap) return toast('请先关闭游戏菜单,回到地图后再前往', 'err'); + try { + const result = await api('/encyclopedia/travel', {mapId:location.mapId,eventId:location.eventId}); + toast(`已到 ${location.mapName} (#${location.mapId}) 的${result.positionNote||'目标分块空处'},保持悬停,碰撞已恢复;物品请自行前往获取。`, 'ok'); + refreshStatus(); + } catch(e) { toast(e.message, 'err'); } +} +$('#encyclopedia-refresh').onclick = loadEncyclopedia; +$('#encyclopedia-search').oninput = renderEncyclopedia; +['encyclopedia-category','encyclopedia-area','encyclopedia-status'].forEach(id => { $('#' + id).onchange = renderEncyclopedia; }); +$('#encyclopedia-regions').onclick = ev => { const button=ev.target.closest('[data-area]'); if (button) { $('#encyclopedia-area').value=button.dataset.area; $('#encyclopedia-status').value='uncollected'; renderEncyclopedia(); } }; +$('#encyclopedia-items').onclick = ev => { + const button=ev.target.closest('button'); if (!button || !ENCYCLOPEDIA) return; + if (button.hasAttribute('data-location')) return goCatalogLocation(ENCYCLOPEDIA.locations.find(l => l.key === button.dataset.location)); + else if (button.hasAttribute('data-shop')) return goCatalogLocation(ENCYCLOPEDIA.shops[Number(button.dataset.shop)]); }; // ---------- 标签页 ---------- @@ -162,6 +475,9 @@ $$('.tab').forEach(tab => { $$('.tab').forEach(t => t.classList.toggle('active', t === tab)); $$('.panel').forEach(p => p.classList.toggle('active', p.id === 'panel-' + tab.dataset.panel)); if (tab.dataset.panel === 'trainer') loadTrainer(); + if (tab.dataset.panel === 'trainer') loadCombat(); + if (tab.dataset.panel === 'encyclopedia' && !ENCYCLOPEDIA) loadEncyclopedia(); + if (tab.dataset.panel === 'world-map') loadWorldMap(); }; }); @@ -172,7 +488,7 @@ $$('.tab').forEach(tab => { * * 背景:沙盒和原版默认共用同一个调试端口,面板光看"已连接"分不出对面是谁。 * 用户以为在沙盒里操作、实际改的是原版存档,是这套工具最危险的误用方式 - * (见 复查与隐患.md 坑 5)。这一格是最后一道兜底。 + * (见 tool/开发文档.md 的运行结构与配置)。这一格用于核对实际实例。 * * 判断由服务端做(它有 gameDir 和 sandboxDir 两个路径可以比对), * 前端只负责显示,免得两边各写一套判断逻辑。 @@ -215,12 +531,15 @@ async function refreshStatus() { $('#connText').textContent = s.connected ? (s.gameSelf?.inMap ? `已连接游戏(端口 ${s.cdpPort})` : '已连接,请在游戏里读档进入地图') : (s.gameRunning ? '调试端口可达,但连接未建立' : '未检测到游戏 — 请先启动游戏'); renderGameSrc(s); applyStoryState(s.storyText || { state: 'disconnected', loaded: false, connection: null }); - const nextContext = s.connected && s.gameSelf?.inMap ? s.context : null; + const nextContext = s.connected && (s.gameSelf?.inMap || s.gameSelf?.inMenu) ? s.context : null; CURRENT_GAME = s.gameSelf; if (CURRENT_CONTEXT !== nextContext) { CURRENT_CONTEXT = nextContext; EVENTS_CONTEXT = LOST_CONTEXT = null; invalidateTrainer(nextContext ? '正在读取角色与背包…' : '请先连接游戏并进入地图'); + COMBAT_CONTEXT=null; + invalidateEncyclopedia(nextContext ? '地图或存档变化,请刷新百科' : '请先连接游戏并读档'); + invalidateWorldMap(nextContext ? '地图或存档变化,正在更新地图…' : '请先连接游戏并读档'); EVENTS = []; LOST = []; VISIBLE = []; renderEvents(); renderLost(); if (TRIGGERS.length) loadTriggers(); @@ -228,15 +547,19 @@ async function refreshStatus() { const where = $('#photo-target'); if (where) where.textContent = s.gameSelf?.saveDir || '未连接游戏,不能保存'; - if (s.connected && s.gameSelf?.inMap) { + if (s.connected && (s.gameSelf?.inMap || s.gameSelf?.inMenu)) { if ($('#panel-trainer').classList.contains('active')) loadTrainer(); + if ($('#panel-trainer').classList.contains('active')) loadCombat(); + if ($('#panel-encyclopedia').classList.contains('active') && !ENCYCLOPEDIA) loadEncyclopedia(); + if ($('#panel-world-map').classList.contains('active')) loadWorldMap(); + if (!s.gameSelf.inMap) $('#connText').textContent = '已连接(游戏菜单中,可管理背包及查看百科)'; try { const snap = await api('/snapshot'); $('#st-map').textContent = `${snap.mapId} ${snap.mapName}`.trim(); $('#st-pos').textContent = `(${snap.x}, ${snap.y})`; $('#st-hp').textContent = snap.hp ?? '—'; $('#st-sw').textContent = snap.switchesOn ?? '—'; - $('#st-grav').textContent = snap.gravitySuspended ? '已关' : '正常'; + $('#st-grav').textContent = snap.gravitySuspended ? '悬停' : '正常'; $('#st-inv').textContent = snap.invincible ? '已开' : '关'; // 换地图了 → 重新拉一次跳转点,好把新地图的"已抹除"状态标出来 @@ -247,7 +570,7 @@ async function refreshStatus() { } $('#photo-state').textContent = snap.stagedActors ? `摆拍角色 ${snap.stagedActors} 个:存档照常;不推荐此时保存,读档后摆拍状态可能无法完整恢复` : ''; // 顶部常驻开关:高亮 = 这个开关是"开着/显示着"的 - $('#q-gravity').classList.toggle('on', !snap.gravitySuspended); // 高亮=重力正常 + $('#q-gravity').classList.toggle('on', !!snap.gravitySuspended); $('#q-invincible').classList.toggle('on', !!snap.invincible); $('#q-noclip').classList.toggle('on', !!snap.noclip); if (snap.lilyVisible !== null && snap.lilyVisible !== undefined) { @@ -271,6 +594,8 @@ async function refreshStatus() { applyStoryState({ state: 'disconnected', loaded: false, connection: null }); CURRENT_CONTEXT = null; CURRENT_GAME = null; EVENTS_CONTEXT = LOST_CONTEXT = null; invalidateTrainer('连接已断开,请重连后刷新'); + invalidateEncyclopedia('连接已断开,请重连后刷新'); + invalidateWorldMap('连接已断开,请重连后刷新'); renderGameSrc({gameRunning:false}); $('#photo-target').textContent = '未连接游戏,不能保存'; $('#photo-state').textContent = ''; @@ -320,14 +645,28 @@ $('#tp-land').onclick = async () => { btn.disabled = false; }; -$$('[data-nudge]').forEach(b => { - b.onclick = () => { - const [dx, dy] = b.dataset.nudge.split(',').map(Number); - $('#tp-x').value = Number($('#tp-x').value || 0) + dx; - $('#tp-y').value = Number($('#tp-y').value || 0) + dy; - $('#tp-go').click(); - }; +let nudgeTimer = null, nudgeHeld = null, nudgeBusy = false; +function stopNudge() { nudgeHeld = null; clearTimeout(nudgeTimer); } +async function nudge(button) { + if (nudgeBusy) return; + nudgeBusy = true; + const context = CURRENT_CONTEXT; + try { + const [dx, dy] = button.dataset.nudge.split(',').map(Number); + const r = await api('/nudge', {dx,dy,pixels:Number($('#tp-step').value)}); + if (context !== CURRENT_CONTEXT) return stopNudge(); + $('#tp-x').value = Math.round(r.x * 10000) / 10000; $('#tp-y').value = Math.round(r.y * 10000) / 10000; + $('#tp-state').textContent = '悬停中'; + } catch(e) { stopNudge(); toast(e.message, 'err'); } + finally { nudgeBusy = false; if (nudgeHeld === button) nudgeTimer = setTimeout(() => nudge(button), 100); } +} +$$('[data-nudge]').forEach(button => { + button.onpointerdown = ev => { if (ev.button !== 0) return; ev.preventDefault(); stopNudge(); nudgeHeld = button; button.setPointerCapture?.(ev.pointerId); nudge(button); }; + button.onpointerup = button.onpointercancel = button.onlostpointercapture = stopNudge; + button.onclick = ev => { if (ev.detail === 0) nudge(button); }; }); +window.addEventListener('blur', stopNudge); +document.addEventListener('visibilitychange', () => { if (document.hidden) stopNudge(); }); // ---------- 本图事件 ---------- let EVENTS = []; @@ -847,11 +1186,11 @@ async function flipChip(id, fn) { el.disabled = false; } -// 重力:高亮 = 正常。点一下切换成"关掉重力"(角色悬空) -$('#q-gravity').onclick = () => flipChip('q-gravity', on => api('/gravity', { on: !on })); +// 悬停:高亮 = 保持高度。关闭时走安全落地检查。 +$('#q-gravity').onclick = () => flipChip('q-gravity', on => api('/gravity', { on })); // 无敌 / 穿墙:高亮 = 开启 $('#q-invincible').onclick = () => flipChip('q-invincible', on => api('/invincible', { on })); -$('#q-noclip').onclick = () => flipChip('q-noclip', on => api('/noclip', { on })); +$('#q-noclip').onclick = () => flipChip('q-noclip', async on => { const r = await api('/noclip', { on }); if (r.warning) toast(r.warning, 'err'); }); // 莉莉娅:高亮 = 显示中 // ⚠️ flipChip 传进来的是**按钮的新状态**(true = 希望莉莉娅显示),直接当 show 用。 // 之前写成 show: !on,等于每次都"再显示一遍",点了毫无反应(用户报过)。 diff --git a/tool/web/index.html b/tool/web/index.html index 581ce5c..b457bc8 100644 --- a/tool/web/index.html +++ b/tool/web/index.html @@ -35,7 +35,7 @@
状态 - + @@ -54,15 +54,43 @@
+
+
+

世界地图 原版房间布局

+
+ + + + +
+
连接游戏后可查看
+

拖动平移,滚轮缩放;点击方格或图标查看详情。彩色:未收集 · 灰色 ✓:已收集 · ?:状态未知 · 虚线:未探索 · 绿色框:当前位置。查看完整布局不会更改探索记录。

+
+
+ +
连接游戏后打开地图
+
+ +
+

+
+
+
+

伤害与受击倍率

+
+

1 倍为正常;0 倍不扣血。秒杀在玩家或莉莉娅的有效攻击命中敌人时生效,不突破剧情无敌。倍率不改变基础攻击力。

+

金币、生命与能量

请先连接游戏并进入地图
@@ -71,19 +99,22 @@

数值锁定只在本次游戏中有效;读档、新游戏或面板失联超过 2 分钟自动解除。「全部还原」也会解除锁定,已修改的数值与物品不回退。

-

背包物品

+

全部背包物品

+

直接修改当前背包,游戏正常存档时会保存。移除已装备的饰品/宝石会同时卸下;移除当前武器会换到另一件已有武器。新增宝石按游戏规则自动启用。

- + +
+
+

物品逐项添加/移除,配饰与宝石也可装备/卸下。批量获得会补满食物、添加装备及各 1 个材料/铃铛;批量清除保留当前武器。默认保留地图铃铛;勾选允许减少后,按地图编号从后往前重置对应铃铛收集点。其他物品的背包修改不影响地图收集记录。

连接后读取背包
-
@@ -98,11 +129,7 @@ 播掉落音效并把你弹回"上次站立的位置",如果这个记录还是上一张地图的,人就会直接消失。
想站到地上走动时,点下面的「落地」(会先探测脚下安不安全)。
-
- ⚠️ 悬浮时别按跳跃键 —— 跳跃给了向上的速度,而重力是关掉的, - 没有任何力把它拉回来,人会一直往上飘。
- 想恢复正常走动:点 「落地」,或顶部 「全部还原」。 -
+
悬停时每次点跳都能继续上升;按住游戏「下」键下降,松开停住,遇到实心地面会停下。下冲等技能保留原有位移。开「穿墙」后四向均可穿过障碍;已卡进地里时先开穿墙飞到空处再关闭。支持原版出口换图并保留设置,无出口的边界仍会阻挡。方向微调点按小步移动,按住连续移动。
@@ -113,6 +140,7 @@
+ @@ -162,6 +190,24 @@
+
+
+

收集百科

+

查看物品用途、获取位置和当前存档的收集状态。背包为空不代表没拿过;地图记录与当前持有数量独立显示。打开游戏背包时也能查百科。

+

「前往分块边缘」优先选择物品所属分块边缘附近、不碰墙且脚下无伤害地形的落点;这个分块被堵住时,会改找邻近分块或同图其他空处,继续传送。到达后保持悬停并恢复碰撞,不会自动穿墙。物品位置和获取条件照常,请自行走过去获取。

+
连接后读取游戏资料
+
+
+ + + + +
+

+
请先读档,再刷新百科。
+
+
+
diff --git a/tool/web/style.css b/tool/web/style.css index 1beaf09..1a8ce75 100644 --- a/tool/web/style.css +++ b/tool/web/style.css @@ -1,5 +1,41 @@ /* 咸鱼喵喵 · 截图辅助工具 —— 控制面板样式 */ +.world-map-layout { display:grid; grid-template-columns:minmax(0,1fr) 280px; gap:16px; margin-top:12px; } +#world-map-viewport { height:560px; min-width:0; position:relative; overflow:hidden; border:1px solid var(--border); border-radius:10px; background:#121019; touch-action:none; cursor:grab; } +#world-map-viewport:active { cursor:grabbing; } +#world-map-viewport:focus-visible { outline:2px solid var(--accent); } +#world-map-svg { width:100%; height:100%; display:block; user-select:none; } +#world-map-empty { position:absolute; inset:0; pointer-events:none; padding:70px 20px; } +#world-map-empty[hidden] { display:none; } +.world-floor { fill:#635375; stroke:#706480; stroke-width:.4; } +.world-cell.unexplored .world-floor { fill:#302b3d; } +.world-wall { fill:none; stroke:#d3c7e2; stroke-width:2; } +.world-cell.unexplored .world-wall { stroke:#8e829f; stroke-dasharray:4 3; } +.world-cell-outline { fill:none; stroke:transparent; stroke-width:2; pointer-events:none; } +.world-cell:hover .world-floor { fill:#847098; } +.world-cell.selected .world-cell-outline { stroke:#ff8ab8; stroke-width:3; } +.world-cell.current .world-cell-outline { stroke:#5fd39a; stroke-width:3; } +.world-player { fill:#5fd39a; stroke:#12271d; stroke-width:2; pointer-events:none; } +.world-marker { cursor:pointer; } +.world-marker rect { fill:#21152c; stroke:#bd92cb; stroke-width:.7; } +.world-marker text { fill:#ffd46b; font-size:13px; text-anchor:middle; font-weight:bold; } +.world-marker.collected { filter:grayscale(1); opacity:.65; } +.world-marker .world-marker-state { fill:white; stroke:#15101e; stroke-width:2; paint-order:stroke; font-size:11px; } +.world-map-detail { min-width:0; max-height:560px; overflow:auto; padding:14px; border:1px solid var(--border); border-radius:10px; overflow-wrap:anywhere; } +.world-map-detail h3 { margin-top:0; } +.world-detail-item { padding:12px 0; border-top:1px solid var(--border); } +.world-detail-item.focused { border-left:3px solid var(--accent); padding-left:9px; } +.world-detail-item p { margin:6px 0; } +.world-detail-item .item-art, .world-detail-item .item-art img { width:36px; height:36px; flex-basis:36px; } +.world-map-controls select { max-width:100%; } +#panel-world-map { min-width:0; } +@media(max-width:760px) { + .world-map-layout { grid-template-columns:minmax(0,1fr); } + #world-map-viewport { height:440px; } + .world-map-detail { max-height:360px; } + .world-map-controls label { max-width:100%; } +} + :root { --bg: #14121a; --bg-card: #1e1b26; @@ -306,3 +342,37 @@ input[type="checkbox"] { accent-color: var(--accent); width: 15px; height: 15px; .trainer-stat .row { margin-top: 12px; flex-wrap: wrap; } .trainer-stat input, #trainer-items input[type=number] { width: 95px; } #trainer-items .item { flex-wrap: wrap; } +/* Inventory and collection encyclopedia */ +select { background:var(--bg-input); color:var(--text); border:1px solid var(--border); border-radius:6px; padding:8px 10px; font:inherit; max-width:100%; } +#photo-state:empty { display:none; } +.inventory-row { align-items: flex-start; } +#combat-outgoing,#combat-incoming { width:100px; } +.inventory-main { display:flex; gap:16px; flex:1 1 430px; min-width:0; } +.item-art { display:flex; align-items:center; justify-content:center; flex:0 0 76px; width:76px; height:76px; border:1px solid var(--border); border-radius:12px; background:rgba(255,255,255,.035); } +.item-art img { display:block; width:64px; height:64px; object-fit:contain; image-rendering:pixelated; } +.icon-placeholder { color:var(--text-dim); font-size:28px; } +#trainer-items .inventory-row { padding:16px; gap:16px; } +#trainer-items .inventory-row .name { font-size:18px; line-height:1.5; margin-bottom:4px; } +#trainer-items .inventory-row .meta { font-size:13px; } +#trainer-items .inventory-row .item-description { font-size:16px; line-height:1.75; color:#d6d1df; margin-top:6px; } +.inventory-row.missing { border-left: 3px solid #b9bcc8; } +.inventory-row.owned { border-left: 3px solid #5ea988; } +.item-actions { display:flex; flex-wrap:wrap; gap:6px; align-items:center; justify-content:flex-end; } +.item-actions input { width:90px; } +.collection-regions { display:flex; flex-wrap:wrap; gap:10px; margin:16px 0; } +.collection-region { display:grid; gap:5px; text-align:left; min-width:155px; } +.collection-region span { font-size:12px; opacity:.8; } +.collection-region progress { width:100%; height:7px; accent-color:#6f8e72; } +.catalog-list { display:grid; gap:10px; margin-top:16px; } +.catalog-item { border:1px solid var(--border, #d9dce5); border-radius:10px; overflow:hidden; } +.catalog-item > summary { display:flex; flex-wrap:wrap; justify-content:space-between; align-items:center; gap:8px; padding:16px; cursor:pointer; } +.catalog-item > summary::before { content:'▸'; opacity:.6; } +.catalog-item[open] > summary::before { content:'▾'; } +.catalog-title { flex:1; font-weight:600; } +.catalog-content { padding:0 16px 16px; } +.catalog-location { display:flex; align-items:center; gap:12px; border-top:1px solid var(--border, #d9dce5); padding:12px 0; } +.shop-sources { margin-top:14px; } +.shop-sources summary { cursor:pointer; } +.tag.warn { background:#fff0d1; color:#865313; } +[data-nudge] { touch-action:none; user-select:none; min-width:44px; } +@media(max-width:700px) { .inventory-row { flex-wrap:wrap; } .item-actions { width:100%; justify-content:flex-start; } .catalog-item > summary { align-items:flex-start; } .catalog-location { flex-wrap:wrap; } } diff --git a/tool/使用须知.md b/tool/使用须知.md index 7fc4155..3ad3760 100644 --- a/tool/使用须知.md +++ b/tool/使用须知.md @@ -1,76 +1,52 @@ -# 使用须知(发布版,2026-10-03 修订) +# 使用须知(发布版) -## 确认当前操作哪份游戏 +更新:2026-10-24。完整功能说明见 [README](README.md)。 -看顶部“当前连接”和存档页“实际保存目录”。身份从游戏的 Node 入口及 StorageManager 存档路径读取,不依赖网页 URL。实机网页 URL 是 chrome-extension 地址。 +## 先确认实例 -调试端口可达不等于已连接,更不等于已进入地图。标题画面请正常读档;不要从外部直接调用 DataManager.loadGame 绕过游戏的 completeLoad 流程。 +看面板「当前连接」,建议确认为已创建的沙盒副本。端口可达不等于已读档;标题界面请从游戏正常菜单进入。移动/剧情需回地图,背包、百科和战斗支持菜单画面。 -## 常见提示 +不要自行调用裸 DataManager.loadGame:本游戏读档还要重建物理网格。请通过游戏菜单正常读档。 + +## 常见操作 | 提示或现象 | 处理 | |---|---| -| 调试端口和面板端口相同 | 设置两个不同的端口,再启动 | -| 端口上的游戏与目标不符/无法识别 | 关闭占用该端口的游戏,或为各实例配置不同端口;不会自动放行 | -| 旧服务或旧配置占用面板端口 | 停止旧面板后再启动;本次升级前的旧服务需关闭原来的命令行窗口 | -| 副本缺失/复制未完成 | 不启动残缺副本;选择新的空目录重建。不会覆盖已有目录 | -| 地图、存档或连接已变化 | 刷新状态并重新读取本图事件/扫描角色,旧列表不能用于新地图 | -| 莉莉娅脚下不安全 | 她保持悬浮;移动到安全地面再点“落地” | -| 跨地图超时 | 工具会取消尚未完成的传送请求;确认游戏是否被菜单或演出占用,再重试 | -| 备份失败 | 不会继续启动需要备份的游戏或覆盖槽位。检查目录、磁盘空间及是否仍在写档 | -| 当前有摆拍角色,提示不推荐保存 | 读档后摆拍状态可能无法完整恢复;可自行决定继续保存,不要求先撤销 | +| 端口上的游戏与目标不符 | 关闭占用实例或改端口,不会自动放行 | +| 地图、存档或连接已变化 | 刷新并重读旧列表 | +| 百科“前往分块边缘” | 优先目标分块边缘,堵住就改到邻近分块/同图其他空处,保持悬停并恢复碰撞,再自行前往获取 | +| 地图怎么传送 | 点击方格或图标选中,再点右侧「传送到此分块」;游戏菜单中可看图,需回地图后传送 | +| 地图太小或图标看不清 | 选择大区域后点「适应地图」,用滚轮/加号放大、拖动查看 | +| 地图灰色图标和问号 | 灰色 ✓ 为已收集,? 为条件无法判定;「只看未收集」不包括未知点 | +| 图标尚未加载 | 等待片刻,或点刷新数值与背包;不影响数量操作 | +| 关闭穿墙被拒绝 | 先微调到空处;开着悬停不代表已离开墙体 | +| 脚下没有安全地面 | 继续悬停,移动到安全处再落地 | +| 空中跳跃/下降 | 悬停中可连续点跳,按住游戏下键下降;技能移动保留 | +| 悬停与穿墙的区别 | 悬停仍受实心地形阻挡;开穿墙后四向均可穿过 | +| 已经卡进地里 | 先开穿墙,跳到空处再关穿墙;脚下安全后可落地 | +| 悬停换地图 | 通过原版出口换图,保留悬停和穿墙;无出口的边界仍会阻挡 | +| 配饰栏位不足 | 增加铃铛或卸下其他配饰 | +| 无法减少地图铃铛 | 勾选对应选项;此举会重置部分地图收集点 | +| 秒杀没有生效 | 需有效命中敌人;不会突破剧情无敌或无限生命对象 | -悬浮时按跳跃仍可能一直向上运动;关重力没有取消跳跃输入。落地会检查地形,“全部还原”会直接恢复正常物理状态,使用时应处于安全位置。 +## 保存与恢复的边界 -## 搜索和剧情 +普通手动和自动存档照常,数值和背包修改可能随游戏保存。全部还原只解除辅助,不回退物品、数值及剧情变化。 -发布版从游戏运行时读取全文台词,全部在内存中使用,不写剧本或缓存文件。先开面板再开游戏也会自动读取;故事表未就绪时最多尝试四次、间隔至少三秒,仍失败会显示原因,可手动重新读取。断线、换实例或重连都会使旧台词失效,重连成功后自动补齐。 +发布版不提供快照、逐帧或输入录制(TAS)。常规游戏存档备份与还原继续可用。 -搜索覆盖整段台词和说话人,并显示命中句;未连接时仍可搜索地图、坐标、事件和条件。 +## 数据来源与收集状态 -“运行过场”包含剧情指令,可能改变游戏开关和进度。分支剧情可能跳过先前的演出;前往只自动设置普通开关,不承诺还原完整历史进度。没有任何一页提供角色形象的事件,也不能凭空生成合适 NPC。 +百科里的「背包未拥有」和「地图未收集」不是同一回事。食物吃掉或配饰删除后,原来的拾取记录仍在。商店只列候选入口,实际库存由进度决定;未知奖励不会被判成未收集。 -## 场景还原与撤销 +面板「地图」显示原版分块示意布局,包含未探索和隐藏区域;查看不会修改游戏探索记录。它与百科共用来源和状态,当前覆盖全部 411 个官方收集点,但未索引的动态掉落也不会凭空出现在图上。分块传送复用百科备用落点规则,目标受阻时可能抵达同图的邻近分块或其他空处,请留意到达提示。 -恢复的角色是固定的摆拍对象,不再自动执行原事件、移动或参与物理碰撞。清除“已抹除”状态只是临时操作;撤销会恢复修改前的字段、原有抹除状态及方法。 +发布版从当前游戏运行时读取全文台词,仅保存在内存中。自动读取最多尝试四次、间隔至少三秒;失败后可手动重新读取。断线、换实例和重连会清除旧内容;未连接时仍可搜索地图、坐标、事件和条件。百科、世界地图与物品图标同样从运行时读取,无需提供完整提取资料。 -“被抹除”不证明这个人应该出现在当前剧情。选择哪些角色和站位仍需核对画面;不要把全部恢复后的画面当成原作的准确复现。 +## 剧情、摆拍与备份 -摆拍期间自动和手动存档照常,工具不拦截游戏自身的保存。顶部提示不推荐摆拍时保存;面板保存的原有确认框会说明风险,仍可确定继续。完成后可撤销;切图或正常读档自动撤销。面板关闭或失联超过两分钟也会撤销,恢复事件原有行为。 +运行过场会执行整个事件页,可能改变开关和剧情;只播台词不等于完整还原演出。#305 的整页过场可能先播放 #298/#299,直接回看请用只播台词。临时摆拍不保证跨读档恢复;切图、正常读档或面板失联两分钟会撤销,普通存档照常生效。升级前关闭旧面板和游戏,再启动新版本并刷新页面。 -这次没有实现“保存并读回完整摆拍布局”。临时函数、UI 显示状态、剧情解释器不承诺跨存档复现。 +还原备份前先关闭游戏。工具校验存档文件与来源,保留还原前的 save 目录和额外安全备份;目标自己的 Steam 云配置保留。备份不存在的槽位不会混入恢复结果。任何恢复操作都应明确选择原版或沙盒目标。 -## 备份与还原 - -- 新备份记录来源,使用唯一目录并校验每个 .rmmzsave 文件。未完成或损坏备份不列为可恢复项。 -- 旧时间戳备份仍可读取,但显示来源未知。还原时必须明确选目标,不能凭备份名称猜。 -- 还原前先退出游戏和 NW.js 游戏窗口,防止运行中的游戏把旧状态写回。 -- 还原得到所选备份中的完整存档集合;备份没有的槽位不会继续留在当前 save 目录。 -- 当前存档会先备份;原 save 目录保留为游戏目录下的 `.save-before-restore-…`,不会直接删除。 -- steam_autocloud.vdf 等非存档文件保留目标目录自己的版本,不从另一份游戏的备份覆盖。 - -## 已知画面限制 - -先前记录的“切换剧情分支时角色短暂抽动”,没有足够实机证据保证所有地图均已解决;本轮不把它标成已修。相同立绘的多个事件、剧情自身的移动与刷新仍可能影响演出效果。 - -并非全部剧情均已验收。遇到问题请记录故事编号、地图、事件页、存档进度和使用的按钮。 - - -## 本轮剧情显示与定位修复 - -#370 横排重复角色的原因是动画图整张显示,现已同步单帧裁切修复。升级后需重启面板和游戏,再刷新网页,才会加载新的游戏内 helper。 - -运行无形自动/并行控制事件时,会优先保留同图的安全站位,或在目标地图寻找可落地位置。找不到安全地面会停止启动剧情并提示手动定位。运行过场在准备完成后尝试落地;普通手动传送仍默认悬浮。自动选点不等于还原原作站位。 - -#305 的“运行过场”仍可能先播同页的 #298/#299;直接查看 #305 请用“只播台词”。 - - -## 角色与背包、锁定 - -“角色与背包”页可设置并分别锁定金币、HP、SP、ZP,以及搜索、添加、移除背包物品。作用于当前操作角色,包含切换后的莉莉娅形态,不修改独立跟随的莉莉娅。金币范围 0~99999,其余不能超过当前最大值;不会改变上限。已倒下时请先在游戏里复活或读档。 - -消耗品默认遵循携带上限,可勾选超上限(最多 99999)。移除装备会同步卸下,移除当前武器会换到另一件已有武器,至少保留一件。铃铛容量、任务及收集进度不在修改范围内。 - -锁定立即阻止该数值的消耗和增加。读档、新游戏、面板失联超过两分钟或点“全部还原”会解除锁定;已修改的数值和背包不会回退,正常保存会把当前数值和物品写进存档。 - -本轮同时更新了摆拍人物贴地、原图待机动画和面向主角。仅对原页要求贴地的人物小范围探地,飞行角色和固定摆件不强制落地;撤销恢复原始位置和朝向。首次升级这批摆拍修复,需要重启面板和游戏后刷新网页。 +详细安装与默认端口见 README;独立目录可使用 config.example.json,但必须先修改示例路径。 diff --git a/tool/发布清单.json b/tool/发布清单.json index aa26e26..2beb262 100644 --- a/tool/发布清单.json +++ b/tool/发布清单.json @@ -1,5 +1,5 @@ { - "version": 1, + "version": 2, "files": [ "tool/启动.bat", "tool/停止面板.bat", @@ -26,8 +26,22 @@ "unpacked/data/System.json", "unpacked/data/MapInfos.json", "README.md", - ".gitignore" + ".gitignore", + "tool/lib/catalog-position.js", + "tool/lib/combat.js", + "tool/lib/encyclopedia.js", + "tool/lib/item-icons.js", + "tool/lib/motion.js", + "tool/lib/world-map.js", + "tool/开发文档.md", + "CHANGELOG.md", + "tool/package-release.js", + "tool/config.example.json", + "docs/images/world-map.png", + "docs/images/inventory.png", + "docs/images/encyclopedia.png" ], "optionalRuntime": "nwjs-sdk-v0.64.1-win-x64/", - "note": "本包仅含工具及必要结构索引;台词从游戏运行时读取。Node.js 和 NW.js SDK 需另行安装。" + "note": "本包仅含工具及必要结构索引;台词从游戏运行时读取。Node.js 和 NW.js SDK 需另行安装。", + "release": "2026-10-24" } diff --git a/tool/开发文档.md b/tool/开发文档.md new file mode 100644 index 0000000..735bcd1 --- /dev/null +++ b/tool/开发文档.md @@ -0,0 +1,58 @@ +# 发布版开发文档 + +更新:2026-10-24。运行要求为 Windows、Node.js 22+ 和用户另行安装的 NW.js SDK 0.64.1,无 npm 依赖。 + +## 运行结构与配置 + +`launcher.js` 校验目录、端口和实例身份,备份存档,再按用户选择启动游戏和面板。`server.js` 仅监听本机回环地址;`lib/cdp.js` 通过 Chrome DevTools Protocol(浏览器调试协议)执行 `lib/ops.js` 生成的游戏脚本。静态网页位于 `web`。 + +默认将整个工具目录放在游戏目录内;原版目录为工具目录的上级,调试端口 9223、面板端口 8787,副本位于同盘根目录 `Nyaruru_shots`。配置缺省时无需创建 config.json。独立布局请复制 `config.example.json` 为同目录 `config.json`,修改示例中的 gameDir 和 sandboxDir 为实际绝对路径。gameDir 需含 package.json 和 index.html;原版和副本目录不能相同或互相包含。JSON 中 Windows 路径的反斜杠需写成双反斜杠,也可使用正斜杠。 + +`gameDir` 在服务启动时确定;修改它后重启面板。调试端口变更会关闭旧连接并清空地图/台词缓存。面板端口变更需重启。不要提交 config.json 或 `.server-runtime.json`。 + +## 运行时数据约定(C 方案) + +随包只含既有 `out/triggers.csv`、`unpacked/data/System.json` 和 `MapInfos.json` 结构索引。`lib/data.js` 不读取完整剧本,不把台词写入触发点。 + +`OPS.readStoryIndex()` 从当前游戏 `$dataStory` 和 `$dataActors` 生成首句、说话人、全文及检索文本,忽略指令和备注。`StoryCache` 仅持有内存对象,以端口、连接代次和会话标识区分实例;并发读取复用同一个请求。最多自动尝试四次,间隔至少三秒;手动重新读取可重置重试次数。断线、端口变化、换实例和重连使旧结果失效;旧请求迟到不能覆盖新连接结果。 + +服务端 `/api/status` 发起自动读取;`/api/story-text` 提供状态及主动重读;`/api/events` 和 `/api/triggers` 合并当前连接的内存台词。前端显示载入/错误提示,断线清空旧台词和事件列表,并用请求编号拒绝过期返回。不得改成读取本机 `story.json` 或把响应保存到仓库。 + +百科读取 `DataManager._cacheMapData`,地图读取 `_thumbnailMapCell`,图标调用原游戏图标组件并在内存 canvas 裁切为 Data URL。三个模块均从当前用户游戏读取,不要求随包放置原始地图或贴图。金币使用工具自身的 SVG(可缩放矢量图)图标。 + +## 功能模块与边界 + +| 模块 | 职责与关键约定 | +|---|---| +| motion | 悬停 v5、连续点跳、下降、技能位移、碰撞及原版出口;读档和失联清理,普通换图保留 | +| trainer / item-icons | 当前操作角色的数值、锁定、背包、装备、铃铛和缩略图;批量先校验,失败回退 | +| combat | 在有效攻击命中链路应用倍率/秒杀,保留免疫与治疗;不永久修改基础攻击力 | +| encyclopedia / world-map | 收集来源与世界分块,只读探索记录;拥有、已收集、未知分别处理 | +| catalog-position | 检查角色整个身体与危险地形,优先分块边缘,受阻时扩展同图搜索;载入后复查 | +| photo-state / story-position | 临时摆拍、单帧裁切、角色贴地/面向、剧情站位;撤销恢复原字段及方法 | +| files / safety / processes | 路径隔离、备份校验、原存档目录保留、实例/进程检查 | + +读写请求携带连接与地图上下文;服务端串行执行修改操作,在实际调用前重新核对身份。菜单中可管理背包、查看百科/地图和调整战斗倍率;移动和剧情操作必须回地图。 + +传送同步游戏逻辑和物理坐标,不直接乱改 `_x/_y`。百科/地图到达后悬停并恢复碰撞,不主动领取物品。跨图失败尝试返回原位置,但仍需核对提示和实际画面。 + +## 发布功能范围 + +发布版不提供快照与 TAS:不含对应页面、事件绑定、轮询、API、模块或打包依赖。常规存档备份/还原保留;`/api/snapshot` 只是顶部实时状态读取,与已移除的多槽记录功能无关。不要把它一并删除。 + +## 验证与发布 + +在仓库根目录使用 PowerShell 7: + +```powershell +node --test tool/tests/*.test.js +Get-ChildItem -LiteralPath tool -Recurse -Filter *.js | ForEach-Object { node --check $_.FullName; if ($LASTEXITCODE -ne 0) { throw "语法检查失败" } } +node tool/package-release.js +Compress-Archive -LiteralPath dist/Nyaruru_modifier -DestinationPath dist/Nyaruru_modifier-public-2026-10-24.zip +``` + +打包脚本只复制 `发布清单.json` 的显式文件,并返回每项大小和 SHA-256。默认输出目录为 `dist/Nyaruru_modifier`;存在时拒绝覆盖,可传入新输出路径。清单排除 config.json、存档、快照、日志、SDK、完整剧本和原始地图。不要以递归复制工作目录替代该脚本。 + +源码仓库包含测试;面向玩家的包仅含运行文件、说明与打包清单,省略测试。`release.test.js` 检查白名单、拒绝敏感条目,并在临时干净目录启动 HTTP 页面,不访问真实游戏。功能测试使用人工对象、公开结构索引及临时存档夹具;不依赖私人原始资料或预先安装 SDK。 + +测试通过不替代所有地图/剧情的实机验收。真实游戏核对需单独说明范围;用户正在游玩时不为测试移动角色、读档或回放。