# 2026-10-24 ## HSPD 2.7→2.6 走路再慢一点点(用户:「kid走路速度再稍微慢一点点」) - `js/player.js` PHY + `tools/lib-reach.mjs` 同步:HSPD 2.7→2.6。扫参:2.5 断 j7+j10 两关,2.6 只断 j10 一个存档,选 2.6。 - **j10 修图**:L3 钢丝区 x55 加过渡柱 `m.vline(55, 5, 7, '#')`。根因(逐帧 trace + 组合探针实证):x58→x52 是 6 格=192px,2.7 下唯一生路是"晚二段 dj=30"(先走完单跳全程再二段,滞空 83 帧水平 224px)落 tile 52.94;2.6 下同一组合水平少 8px,轨迹到达柱区时已掉过柱顶 8.7px,盒左沿蹭柱右壁后摔进 x53 刺毯。dj=12 的轨迹在 3 格处 feet 恰回 160、4 格处 feet 恰回 192,故 x55 拆成 3+3 格两跳,两跳都精确接住。save/终点/steps 数组全未动。 - 验证:solve 23 关终点+存档+苹果全可达(j10 11/11,状态 3796→4003);16 项回归全绿。 - 教训(已入 MEMORY.md):GRAV_FALL 0.55 下"高等抛物线接远处柱顶"类路线全部失效(落地高度时水平只飞 113~130px,够不到 160px+ 柱距);HSPD 每降 0.1 让晚二段落点整体平移 8px 级,钢丝柱/跳台类窄窗关卡首当其冲——修法优先"拆间距加过渡柱"而非加宽(轨迹低于柱顶时加宽接不住)。 ## GRAV_FALL 0.55→0.40 下落变飘(用户:「下落速度还是不够飘」) - 注意这是用户对下落的**反向修正**:早些时候要求「下落重力加速度再明显点」(当时 0.33→分出 0.55),现在要飘,落 0.40。跳跃高度/JUMP/GRAV 均不动。 - `js/player.js` PHY + `tools/lib-reach.mjs` 同步。滞空变久:二段跳射程 132.6~240px(原 120~231px)。 - 破坏 j6(缺 5 存档+终点)、j9(缺 1 存档 1 苹果),断点都是**从右往左过 hgap 对刺门**(j6 @x95、j9 @x68):GRAV_FALL 0.40 让满跳轨迹在门区下降变慢,正好嵌进通道列里留的 '^'(carpet 缺口间距 3 对齐不到门列)。trace 实证 t=43 盒底 416.1 已擦 x69 '>' 刺左缘、t=45 嵌入 x68 '^' 顶点区。 - 修图(两处 gaps,纯补通道列净空,不动对刺):j6 L1 `carpet(m,9,15,112,[86])`;j9 L1 `carpet(m,9,13,108,[27,59])`。修后 23 关全可达(j6 状态 852→1875、j9 1149→1460)。 - 教训(已入 MEMORY.md):**GRAV_FALL 变小会让"回跳过对刺门"的轨迹在门区滞留更久**,门芯 '^'(hgap 通道列未净空)从"能勉强擦过"变成"必嵌进去"——改下落重力后重点复查所有从右往左的 hgap 门。 - 16 项回归全绿。 ## GRAV/GRAV_FALL 再各降一档 + 工作流变更(用户:「跳跃和下落再稍微飘一点点,你以后不用测试自带地图是否能通关,直接改就行」) - `js/player.js` PHY + `tools/lib-reach.mjs` 同步:GRAV 0.29→0.27、GRAV_FALL 0.40→0.37。 - **用户免除地图通关验证流程**:今后改物理直接落地,不跑 solve、不为可达性修图(已入 MEMORY.md)。本次仅跑 smoke/dom-smoke 确认代码无坏。 - 预计影响:单跳 apex 91→94px、二段滞空更久;按用户偏好未验证/未修图。 ## GRAV/GRAV_FALL 再降一档(用户:「再飘一点」) - `js/player.js` PHY + `tools/lib-reach.mjs` 同步:GRAV 0.27→0.25、GRAV_FALL 0.37→0.35。 - 按用户豁免的工作流直接落地,未跑 solve/未修图;smoke/dom-smoke 通过。 ## JUMP 7.5→6.6 满跳高度定为 2.8 格(用户:「一个满力跳的高度应该大约是2.8个方块」) - `js/player.js` PHY + `tools/lib-reach.mjs` 同步:JUMP 7.5→6.6(GRAV 0.25 不变)。实测满跳顶点 90.4px = 2.83 格(目标 2.80 格=89.6px,偏差 0.8px;JUMP 6.5 会偏浅到 2.74 格)。二段跳顶点 209.8px = 6.56 格(未按用户要求调整)。 - 用户豁免地图通关验证,仅跑 smoke/dom-smoke/jumpdist(jumpdist 是纯物理测量,保留)。 ## DJUMP 7.6→5.0 + 二段跳惯性叠加(用户:「满力二段跳的最高高度应该约为4.5个砖块,且如果在下落时二段,需要考虑下落的惯性的速度叠加跳跃的速度」) - `js/player.js` PHY + `tools/lib-reach.mjs` 同步:DJUMP 7.6→5.0。二段实现从直接覆盖 `vy = -DJUMP` 改为叠减 `vy = Math.max(vy, 0) - DJUMP`(上升中按等价覆盖,与原版语义一致;下落中按下落惯性抵消冲量)。 - 实测(探针复刻积分):顶点接二段(满力最高)142.9px = 4.47 格 ≈ 目标 4.5 格;起跳瞬间接二段 52.5px = 1.64 格(比单跳低,与原版一致);下落 vy=+2 时二段回升 19.5px、vy≥+5 时完全失效(vy_new≥0 升不起来)——惯性叠加的直接后果,生效窗口为顶点后约 14 帧(GRAV_FALL 0.35)。 - 用户豁免地图通关验证;smoke/dom-smoke/jumpdist/ice/death 5 项通过。 ## 二段跳改同款飘:GRAV_DJ 0.15 + DJUMP 4.0(用户:「二段跳的高度和一段差的也太明显了,二段也要和一段一样飘,但是高度在4.5格」) - 问题:DJUMP 5.0 + 通用 GRAV 0.25 时二段上升只有 20 帧/52.5px,与一段的 27 帧/90.4px 观感差距大("短冲"vs"慢飘")。 - 方案:新增 `PHY.GRAV_DJ: 0.15`(二段上升段专属轻重力)+ `DJUMP 5.0→4.0`。二段初速小、重力更轻 → 上升帧数与一段持平。 - `js/player.js`:二段时置 `this.djUp = this.vy < 0`;积分行改 `vy + (vy<0 ? (djUp ? GRAV_DJ : GRAV) : GRAV_FALL)`,到顶(vy>=0)交回普通重力;`djUp` 在构造/reset/水中/落地四处清零。惯性叠保留:`vy = Math.max(vy,0) - DJUMP`。 - `tools/lib-reach.mjs` 同步 GRAV_DJ 常量 + jump() 内 djUp 标志同款逻辑。 - 实测:一段 27 帧/90.4px=2.83 格;顶点接二段 27 帧慢飘、总高 145.7px=4.55 格≈4.5 ✓;下落惯性叠加保留(vy=+1 回升 0.98 格、+2→0.45、+3→0.12,均为慢飘)。 - smoke/dom-smoke/jumpdist/ice/death 5 项通过;按用户豁免未跑 solve。 ## 二段跳去悬停:DJUMP 4.0→6.0 / GRAV_DJ 0.15→0.35(用户:「现在二段就像在空中停了一段时间,请你参考视频里的完美复刻」) - 用户发了参考视频(微信录屏,某 IWanna 系手游)。cv2 抽帧分析:该游戏 TILE≈44.75px、kid 高 35px、一段约 30 逻辑帧(60fps)升 2.9 格(平均 3.3px/帧)、上升干脆顶点不拖沓;kid 沿砖柱贴墙反复跳。 - 病根:上轮 GRAV_DJ 0.15 + DJUMP 4.0 使二段平均上升速度仅 2px/帧、耗时 27 帧——又慢又长=悬停感。 - 修正:DJUMP 6.0 / GRAV_DJ 0.35。二段 18 帧、平均 3.02px/帧(一段 3.35,速度感一致)、总高 144.8px=4.52 格 ✓。下落惯性叠加保留(vy=max(vy,0)-DJUMP),窗口加宽:vy=+1/+2/+3/+5 接二段分别回升 38.3/24.9/14.4/1.9px。 - 5 项冒烟通过;按用户豁免未跑 solve。 - 方法论备忘:无 ffmpeg 时用 `pip install opencv-python-headless`(装进 /tmp venv,全局 pip 会被 SIGTERM)抽帧;定位游戏内物体先量参照物(砖柱网格)反推 TILE 像素,再用特征色(kid 蓝衣/棕发)追踪。 ## 二段跳「非常短」bug 修复(用户报告) - 病根:`player.js` 二段公式 `vy = max(vy,0) - DJUMP` 把当前下落速度**全额**抵消冲量。满力一段到顶点后,人类反应(松手再按)天然晚 10+ 帧,vy 已 +3.5~7,二段实际提升仅 5~15px 甚至归零。 - 修复:`PHY.DJ_INERTIA = 0.5`,公式改 `vy = max(vy,0) * DJ_INERTIA - DJUMP`;`tools/lib-reach.mjs` 同步同值同式。 - 实测(`tools/dj-test.mjs`,真实引擎,二段自身提升高度):顶点 46→47px、+3帧 30.6→39px、+7帧 14.9→29.3px、+11帧 4.8→20.9px、+15帧 0.3→13.9px、+19帧 -1.1→8.4px。顶点附近保持满力,摔得快仍按惯性变弱(满足用户此前「下落惯性叠加」的要求)。 - 回归:dj-test / smoke / ice-test / spike-test 全过;solve 对照实验(DJ_INERTIA 临时置 1.0)证实 **j8 不可达是更早物理轮次的既有问题**(状态数 158 两次一致),非本次引入,按用户豁免未修图。 - 教训:抄 jumpdist 的 harness 时 `addL` 漏了 `handlers[t] =` 赋值,监听全丢、测试全 0——先跑最小实验(dbg3:裸 addEventListener 替换生效)再怀疑业务代码。 ## 二段"好沉+高度短"二连修(用户报告)+ 工作流禁令收紧 - 用户新规矩(原话):「以后没有明确说明,你不要给我自动测试地图的可达性」——即使用户要求改物理也不主动跑 solve.mjs,已写入 MEMORY.md 工作流偏好节。 - 病根①「沉」:GRAV_DJ 0.35 比 GRAV 0.25 重 40%,二段上升减速快、发闷。修复:GRAV_DJ 0.35→0.25(与一段完全一致,"二段和一段一样飘")。 - 病根②「短」:DJ_INERTIA 0.5 对下落接二段的冲量抵消过多。修复:0.5→0.35。 - 配套定标:DJUMP 6.7→5.4 时顶点总高仅 4.32 格(引擎积分是**先加重力再移动**,此前预估公式高估 ~6px);微调 5.65 后顶点二段提升 59.9px、总高 4.49 格 ✓。 - 最终曲线(dj-test 实测,二段自身提升):顶点 59.9px / +3帧 52.7 / +7帧 44.0 / +11帧 36.0 / +15帧 28.8 / +19帧 22.4。 - `tools/lib-reach.mjs` 常量同步:GRAV_DJ 0.25 / DJUMP 5.65 / DJ_INERTIA 0.35。 - 回归:dj-test / smoke / ice / spike / save / key 全过(未跑 solve,遵守用户禁令)。 - 踩坑记录:Edit 工具对 player.js 含中文注释的多行块连续匹配失败(原因不明),单行 Edit 正常;php/python 改文件可行。以后改 player.js 优先单行 Edit。 ## 二段"好沉+短"根治(用户二次反馈"还是和上一个问题一样") - 诊断翻案:前两轮误把"沉"归因于 GRAV_DJ 0.35,实际根源是**起跳初速被下落速度吃掉**——一段起跳初速 -6.6,而下落接二段时 `vy = max(vy,0)*k - DJUMP` 使初速只剩 -3.3~-4.4(k=0.5/0.35),起跳那一下就没劲;初速小→上升自然短。调重力治标不治本。 - 修复:DJ_INERTIA 0.35→**0**(覆盖式二段,同 IWBT 原版,任何时机接二段初速恒 -DJUMP);GRAV_DJ 回 0.35(平均上升 3.18px/帧 与一段 3.23 一致;0.25 时只有 2.72 偏"肉");DJUMP 回 6.7。 - 实测(dj-test):任意时机(顶点~顶点后19帧)二段自身提升全部 60.8px,曲线完全持平;顶点总高 4.52 格 ✓。 - 用户最初提的"下落惯性叠加"经三次实践证明给不出好手感,放弃;参数保留 DJ_INERTIA(现值 0)供回退。 - 回归:dj-test/smoke/ice/spike/save/key 全过;未跑 solve(用户禁令)。 - dj-test 断言已随 INERTIA=0 语义重写(各时机 ≥55px 且与顶点差 ≤3px)。 ## 死亡流程改造:手动重生 + 小死亡动画(用户需求) - 需求原话:「当角色死亡后不要立马重生,需要用户手动按重生键重生,并且死亡时除了原先的红叉,还要附加一个小的死亡动画」。推翻 2026-10-23 的"立即复活、无动画"旧需求。 - player.js:kill() 重写(定格 + 粒子爆发 16 个 + shake=6 + deathX/deathY clamp 到关卡内,红叉位置改用 clamp 后坐标);update() 开头 `if (this.dead) { this.deadT++; return; }` 早退;移除 4 处"kill 后当帧 respawn"(掉坑/刺/update 末尾兜底/假存档);draw() dead 分支画扩散冲击环(deadT<=14,半径 4→29px 淡出,kid 不画);reset() 初始化 deathX/deathY。 - main.js:suicide() 分流(alive→kill;dead→respawn),R 键两段式语义;render 的 playing 分支加死亡提示("按 R 重生"呼吸闪烁 + "Backspace 整关重来",触屏文案"按右下角重生键")。 - 触屏重生键(touch.js→api.suicide)自动获得死亡中重生能力,无需改。 - 测试:death-test.mjs 全面重写为 23 断言(定格/输入无效/手动 respawn 回存档点/连续 5 次死亡/红叉常驻+去重+reset 清空/掉坑 clamp/渲染冒烟)全过;key-test(R 自杀→定格、死亡中 R→重生)、save-test(kill 后手动 respawn)、smoke(死亡段改新语义)同步更新;dj/ice/spike/dom-smoke/editor-pick 全过;未跑 solve(禁令)。 - 踩坑:测试 grid 里 'S' 是出生点、存档点字符是 'G'(level.js:24),写测试地图别搞错。 ## 重生键语义简化:任何状态按 R 都直接重生(用户反馈) - 用户原话:「如果没死,由玩家手动按了重生键就直接重生」。此前 R 是两段式(活着=kill 定格→再按一次才回存档点),用户嫌按一次没反应。 - main.js suicide() 改为无条件 `G.player.respawn()`:活着按 = 放弃当前进度秒回存档点(不死亡、不计死亡数——主动重生不是失误);死亡中按 = 复活。触屏重生键走同一入口自动生效。 - key-test R 段重写:先站到出生点→按 R→验证回到存档点且 alive/dead/死亡数不变;再 kill→死亡中按 R→验证复活。全过。 - 回归:key/death/save/smoke/dj/ice/spike/dom-smoke/editor-pick 全过;未跑 solve(禁令)。 ## 重生键最终形态:按一次 = 死亡动画 + 自动重生(用户二次反馈) - 用户先要求「没死按重生键直接重生」,AI 做成无动画秒回;用户再反馈「不是,直接按重生键有死亡动画的」——要的是**一次按键=播死亡动画→演完自动重生**。 - 最终语义(main.js suicide):死亡中按 R = 立即复活;活着按 R = kill({mark:false, count:false, autoRespawn:30})——粒子+轻震+冲击环照播,不留红叉、不计死亡,30 帧(0.5s)后自动回存档点。 - player.js:kill() 参数化 opts{mark,count,autoRespawn};update 死亡早退里 autoRespawn 到期自动 respawn;reset/respawn 清理该字段。软重置(softReset 清粒子+shake)会收走动画残余粒子,属预期(爆发与冲击环已看完)。 - 测试:death-test 新增场景 9(主动重生键 7 断言)全过;key-test 活着按 R 段重写(演出中 dead/零计数/零红叉→33 帧后回存档点);save/smoke/dj/ice/spike/editor-pick/dom-smoke 全过。 - 补充(用户三次反馈):主动重生键演出期间**不显示**「按 R 重生」提示(main.js render 条件改为 `P.dead && !P.autoRespawn`)——演完自动重生,提示多余;真死亡时提示保留。 - 补充(用户四次反馈):主动重生键原定 autoRespawn=30 帧,冲击环 14 帧就播完,中间 16 帧只剩稀疏粒子=「空档」。改为 **autoRespawn=15**(冲击环 deadT<=14 播完的下一帧重生,零空档零腰斩)+ kill() 新增 opts.life 覆盖粒子寿命(主动模式 life=15 与冲击环同步收尾,避免被 softReset 腰斩满 alpha 粒子)。shake=6 每帧 ×0.86 衰减,15 帧时≈0.56px 无感,不会被重生清场掐断。 ## 主动重生键终版:当帧重生 + 特效留在死亡位置播完(用户五次反馈) - 用户原话:「不用等死亡动画结束再重生,直接重生就行了,动画在死亡位置放完」。此前 autoRespawn=15 帧的"演完再重生"方案废弃,autoRespawn 机制整个删除。 - 架构调整:冲击环从 player.draw 的 dead 分支**搬到 Level 层**(`deathFX` 数组 + addDeathFX + drawDeathRing + update 推进 14 帧移除)——否则玩家一重生环就消失,无法"在死亡位置放完"。 - player.js:新增 `deathBurst()`(纯演出:粒子+shake+addDeathFX,不进死亡状态);kill() 改为调 deathBurst + 红叉/计数(opts 仅剩 mark/count);respawn(opts) 透传 `softReset(opts.keepFX)`;draw() dead 分支简化为不画 kid。 - level.js:`softReset(keepFX=false)`——keepFX=true 时不清粒子/震屏(主动重生专用,让特效自然播完);reset(true) 清 deathFX。 - main.js suicide:活着按 = `deathBurst() + respawn({keepFX:true})` 当帧完成,零等待;死亡中按 = respawn()。 - 真死亡流程不变(定格+手动+红叉+计数);粒子 life 34 / 冲击环 14 帧 / shake ×0.86 全部自然播完(重生不清场)。 - 测试:death-test 场景 8/9 重写(当帧重生、纯演出不进死亡态、粒子/环/震屏全程自然播完断言),key-test 活着按 R 段重写;全套回归过。 - 补充(用户六次反馈):「kid 被动死亡的,死亡动画也应该放完,尽管玩家很快按了重生键」。根因:`softReset()` 无脑清 particles+shake——重生即清场,特效被腰斩。修复:softReset 永久不清粒子/震屏("演出层"语义:重生只重置关卡逻辑状态,不掐断演出;仅 reset(true) 整关重开才清空)。keepFX 参数随之删除,respawn() 恢复无参。踩坑:python 替换时把 `softReset(keepFX=false){` 函数头和 vanish/bullets 清逻辑一起吃进 old 串,文件语法损坏——已修复(补回函数头+4 行清逻辑,node import 验证)。教训:rep() 的 old 串跨函数签名行时,new 必须原样带回签名与后续语句。 - 测试:death-test 新增场景 10(真死亡 3 帧秒按 R:粒子/环/震屏全程保留并自然播完,7 断言);key-test 死亡中按 R 段加"残余粒子"断言;全套回归过。