Files
kidgo/.workbuddy/memory/MEMORY.md
T

11 KiB
Raw Blame History

iwanna(IWBT C 同人网页游戏)项目长期笔记

物理与判定(像素级跳刺的核心)

  • 用户工作流偏好(2026-10-24 起,二次收紧):用户明确说「以后没有明确说明,你不要给我自动测试地图的可达性」——改物理/手感参数时直接落地 player.js + 同步 lib-reach.mjs,不主动跑 solve.mjs、不为重调地图修图(用户另行要求才跑/才修)。求解器常量仍要保持同步(供用户日后自行诊断用)。其他非通关类回归(smoke/dj-test/ice-test 等)可轻量跑。
  • 物理常量写在 js/player.js 的 PHY(2026-10-24 四调定格):GRAV 0.25(上升)/ GRAV_FALL 0.32(下落,分手重力;0.35→0.32 用户反馈"下落有点沉")/ GRAV_DJ 0.35(二段上升专属;试过 0.25,平均上升速度掉到 2.72px/帧 反而"肉")/ MAXFALL 9 / JUMP 6.6(满力一段 ≈84px=2.6 格,起跳初速 -6.6)/ DJUMP 6.7(起跳初速 -6.7 与一段几乎一致)/ HSPD 2.6 / DJ_INERTIA 0。手感调整链见日报;tools/lib-reach.mjs 有同名常量必须同步,且 jump/fall 的积分行是 vy + (vy<0 ? (djUp?GRAV_DJ:GRAV) : GRAV_FALL)。松手即停、无惯性、可变跳跃高度不变。
  • 满力二段总高 = 4.5 格定标(144px):一段 83.9px + 二段 60.8px。帧量化使 DJUMP 取值有跳变,且引擎积分顺序是先加重力再移动(预估必须按此算,否则高估 ~6px):GRAV_DJ 0.35 下 DJUMP 6.0→47px(总 4.09 格)、6.5→57px(4.40)、6.7→60.8px(4.52 ✓)。定标流程:改参数→跑 tools/dj-test.mjs 看"二段顶点离平台"列→微调 DJUMP 到 4.5 格。
  • 二段跳惯性规则(2026-10-24 三轮反馈后定型:INERTIA=0,覆盖式二段):vy = max(vy,0) * DJ_INERTIA - DJUMP,当前 DJ_INERTIA=0 即任何时机接二段初速恒为 -DJUMP(同 IWBT 原版)。"沉"的根源是起跳初速被下落速度吃掉,不是上升重力:用户三次反馈链路——全额抵消(顶点后 11 帧二段仅 4.8px,"非常短")→ 0.5(仍"短")→ 0.35 且误把 GRAV_DJ 降到 0.25(平均上升速度 3.18→2.72px/帧,更"肉",用户报"还是好沉+短")→ 0(初速恒 6.7 与一段 6.6 一致,起跳"嘭"感同款)。教训:用户说"沉"先查起跳初速(vy 被 max(vy,0)*k 吃掉),别急着调重力。现行曲线(dj-test):任意时机二段自身提升均 60.8px、顶点总高 4.52 格。回归:tools/dj-test.mjs。
  • 玩家判定盒 11×21(Player 构造函数里 this.w = 11; this.h = 21)。改这里会同时影响 level.js 的 spawn 偏移和全部可达性工具。
  • 刺碰撞是三角×AABB(SAT):js/level.js 的 spikeTri(t,x,y) 顶点与 drawSpike 一致(上刺尖在格顶 y、左右刺尖在格缘中点),triRectOverlap 做精确判定;deadlyRect 网格刺+掉落刺都走它。旧的 spikeBox 矩形盒已删除。T.KILLER 必须有兜底 return。
  • kid 绘制零形变:起跳/落地 squash 已全部删除(player.js 无 squash 字段,sprites.js 的 drawKid 直接按原始 11×21 画)。用户明确要求跳跃/落地都不要身体晃动。

冰面滑行惯性(易反复踩坑)

  • 冰面物理只在 js/player.js 的水平输入段,sliding 是唯一惯性开关:冰上 |vx|>SLIDE_MIN 置位,离冰后仍按 sliding 走惯性分支,撞墙/归零/交还控制时才清位。
  • 空中与地面必须分开摩擦:SLIDE_AIR 0.02(几乎无损,能滑出冰块边缘飞出去)vs SLIDE_FRIC 0.12(地面约 34 帧/70px 收住)。曾经两者都用 0.25,导致「滑出冰块掉下去 7 帧就没速度」——玩家最直观的「没惯性」就来自这里。
  • 按住同向键只能让超速收敛回 HSPD(SLIDE_EASE),不能继续加速;低于走速立刻 vx=target 交还控制。反向键 SLIDE_BRAKE 刹车后马上反向。
  • 回归测试:tools/ice-test.mjs(冰→普通地面/悬空/起跳 4 场景 + 断言),改冰面物理必跑。

关卡设计硬规则(都踩过坑)

  • tools/solve.mjs 是唯一权威:改完任何地图必须跑到「✅ 所有关卡终点与存档点均可达」。
  • 之字竖井用 needle.js 的 well(xL, xR, topRow, botRow, floorSide):每级升 2 格、台阶宽 5 且两级重叠 2 格、顶台与上层地板同高相接、靠地板侧井壁从 topRow+1 到 botRow-4(底部留 3 格入口)。
    • 台阶不能铺满井宽(会变成 1 格高封闭走廊,跳不起来)。
    • 顶台不能压在地板洞口正上方(会把井封死)——曾因此 j2 整关不可达。
  • 完整四向钻石(上下左右刺齐全)在矩形判定盒下横向无解,只能当装饰;要「跳穿门」必须用 vgap(上下夹缝)或 hgap(左右夹缝)。
  • 垂刺(压顶)与落脚点的关系:单跳最高 ~85px、二段跳 172px。压顶刺放在落脚点上方 5 行 = 单跳安全/二段必死;放太近(34 行)会让单跳也撞死。冰面上不要加压顶垂刺(滑行无法控制跳跃高度,实测 j8 直接不可达)。
  • 刺毯缺口间距统一 3 格(跳 2 连刺 = 64px):JUMP 7.5 + GRAV_FALL 0.55 下落地前一帧竖直速度 8.9px/帧,3 连刺(96px)起拍窗只有 0~1 帧(实测 j1/j2 第一跑窗口=单点或负值,不可玩);2 连刺留 55px 射程余量。2026-10-23 从 4/3 交替 → 统一 4 → 统一 3,配合 carpet(m, x0, y, n, gaps) 的显式 gaps 拆断特殊位置的 3 连刺。
  • 井口/左端出口的 3 连刺必须显式拆断(walk 限制使起拍位比所需位窄 1~2px,右行必卡):j2 L0 x25-27、L2/L4 井口 x11-13、L3 左端 x10-12(左行跳落点差 0.8px 蹭刺左坡)都是这么拆的。判据:任何"出口紧接着 3 连刺"的段,不管求解器当时过不过,加 gap 拆成 2+1。
  • hgap 夹缝的两段式穿越:通道列 x 必须为空(carpet gap 对齐),且左右接近列 x±2 也要是缺口/已清——否则从左侧跳过来落在 '>' 刺上,变成 4 格跳(128px > 单跳 118.8px)必死。j2 L2 的 hgap(22)/(42)、j2 L4 的 hgap(52)、j7 顶层的 vgap(52) 都靠补通道列缺口修复。
  • 天花板(实心顶/垂刺)会挡跳的横向漂移:高跳(apex 97px)撞到洞顶后横向停滞,下坠时砸在原点刺上——j9 矮洞入口 x57→x61 四格跳就是这么死的,拆成两个 2 格跳(x59 加缺口)即解。
  • apex 与刺顶点的 1px 竞争是隐形杀手:spikeTri 的 '^' 顶点就在格顶 y(无收紧)。apex 盒底与航线上下方 '^' 顶点只差 1px 时,顶点会在起跳后约 4 帧整段落在玩家盒内(SAT 铁死)——JUMP 8.5 apex 盒底 466 vs 顶点 480 从容擦过,8.0 时 479 就死。修法:去掉该格地刺只留压顶刺(j8 x86),或给落点台加宽(治标不治本,上升段照样死)。
  • hgap(x,y) 的通道列 x 必须本身是空的(carpet 缺口或显式 clear):hgap 只清 x±2、在 x±1 放对刺。若 carpet 缺口没对齐 x(如 j9 L1 的 x36、j1 回廊的 x92),通道里会留一根 '^',整门变成 3 连刺——往回跳必被 '<' 刺别住。修法:给 carpet 加对齐的 gap。
  • 对刺门的侧刺('<'/'>')会把起跳窗切成 2px 级窄窗:从门前起跳时,侧刺尖在起跳后第 3~5 帧别住盒子(上升不够),必须从门前偏右 ≥半个身位起跳。求解器 reach() 的 6px 状态桶对齐与否能直接翻转整关可解性——改物理后若某跳「全灭」,先逐帧 trace 看是不是侧刺别住起拍,而不是先怀疑跳距。j6 hgap(43) 曾只靠 fx≥1448 的 2px 窗可解,GRAV 0.34→0.33 时 hop 落点左移 2px 即全灭;修法=清掉一侧侧刺留双刺门(j6 x44)。
  • 调试利器:逐帧 trace。复刻 lib-reach.mjs 里 sim.jump 的主循环(vy 积分→x 移动→y 分段→每帧打印 x/y/feet/vy + L.deadlyRect + 涉及格子),比手推几何可靠得多;手算 SAT 三角形截面极易错(连错三次的教训:盒右沿压刺左坡、盒顶压垂刺尖,都要用精确坐标复算)。
  • kid 绘制零形变(见物理节):起跳/落地 squash 已删,别再加回"落地挤压"——用户明确说过落地拉伸也不要。

工具链

  • tools/lib-reach.mjs 是 solve / reach-diag 的共享 BFS 核心,改建模只改这一处。
    • 关键修正:跳跃水平方向被墙挡住时不能把 vx 永久清零(真实 player.js 里按住方向键下一帧会恢复)。清零会丢失「贴墙上升→越过台面→横移」这条经典路线,曾导致整个 j2 竖井判为不可达。
    • reach() 默认 DJSET/RELSET 必须与 solve.mjs 的 DJ/REL 环境变量一致,否则诊断和求解结果对不上。现值:DJ=0,6,12,18,24,30 / REL=0,4,5,6,8,14。
    • RELSET 必须有 5 和 6:vgap 夹缝(缝高 32px、盒高 21px)要求短跳 apex 落在 32~43px 且上升早期躲开地刺左坡——rel=4(apex 35.7)t=7 蹭坡 2.6px,rel=6(apex 44.7)t=11 擦垂刺尖 0.6px,只有 rel=5(apex 38.9)同时满足(j2 x16 夹缝);j7 顶层 x52 夹缝则要 rel=6。松键短跳是玩家的连续技巧,采样必须覆盖这两帧。
    • fy 规范化(canon):jump/fall 返回的落点常比地面高零点几 px(如 1535.5),与精确落点 1536 落进同一个 3px 桶——先占桶者赢,走链被错位几 px 的跳链顶掉,芝麻窗口翻转(加 rel=6 后 j2 状态数反而从 1572 暴跌到 80 的根因)。push() 里按 canStand 门控把 fy 吸附到 32 倍数(真实游戏重力下一帧即按到底)。
    • 状态桶 3px:6px 桶会把"贴右腰 1~3px"的起拍/落点窗量化吞掉(j2 连续三段因此误判不可达)。
  • 其余:check.mjs 格式校验、save-test.mjs 存档点、key-test.mjs 按键语义(R=回存档点 / Backspace=整关重来)、smoke.mjs、dom-smoke.mjs。
  • 本地预览:python3 -m http.server 8123(ES module 必须走 HTTP,file:// 打不开)。

关卡现状(23 关)

跳刺 j1j10(68×52 ~ 150×44,刺密度 3.8%11.6%)、耐久 e1e4、boss b1b3(可达性求解跳过)、坑向 r1~r4。

移动端虚拟手柄(2026-10-22 新增)

  • 触屏自动检测:matchMedia('(pointer: coarse)') / ontouchstart / maxTouchPoints>0;?touch=1 强制开、?touch=0 关(桌面预览用)。
  • js/input.js 的 touch 状态层 + setTouch/clearTouch 与键盘共用 action 模型(now/prev),不破坏可达性工具。
  • js/touch.js:左下 ◀▶ 移动、右下大「跳」+ 🔫射击(按住连发,受 hasGun/冷却限制)+ ▼下蹲 + 暂停/重生小键;pointer 事件支持多指,hide() 会 clearTouch() 防卡键。
  • 仅在 playing 态显示(startLevel/resume 显示,菜单/暂停/结算隐藏);HUD 每帧 Touch.setGun(P.hasGun) 控制射击键置灰。
  • 若改键位含义,记得同步 js/input.js 的 BIND 与 touch.js 的 HELD/api 映射。