13 KiB
13 KiB
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 画)。用户明确要求跳跃/落地都不要身体晃动。
死亡与重生流程(2026-10-24 新语义,推翻早期"立即复活"需求)
- 死亡后不自动重生:
kill()后玩家定格在死亡点,世界继续运转(敌人/子弹/粒子/相机照走)但玩家不响应输入(player.jsupdate 开头if (this.dead) { this.deadT++; return; })。等玩家手动按重生键。 - 重生键 = R(复用 IWBT 标准自杀键):活着按 R = 自杀回最近存档点;死亡中按 R = 手动重生(
main.js的suicide()按G.player.dead分流)。触屏重生键走同一入口(touch.js→api.suicide)。Backspace 仍是整关重来。 - 死亡动画("小"三件套,红叉之外附加):①
kill()时 16 个红白粒子爆发(L.particles.spawn,life 34);②L.shake=6屏幕轻震(camera 原生支持,0.86/帧衰减);③ 死亡点扩散冲击环(player.draw里dead分支,deadT<=14帧内半径 4→29px 淡出,kid 本体不画)。 - 死亡坐标 clamp:
deathX/deathY记录时 clamp 到关卡内(掉坑死亡时玩家已在屏幕外),红叉/粒子/圆环都落在可见区域;红叉(deathMarks)仍常驻保留、多次死亡只留最后一次、reset(true)清空。 - render 死亡提示:
main.jsrender 的 playing 分支画居中"按 R 重生"(呼吸闪烁,Math.sin(t/12))+ 副行"Backspace 整关重来";触屏文案"按右下角重生键"。 - 历史:早期用户曾要求"死亡立即复活、无任何动画"(旧
death-test.mjs首行注释为证),2026-10-24 明确推翻。回归:tools/death-test.mjs(23 断言:定格/输入无效/manual respawn/连续死亡/红叉/clamp/渲染冒烟);key-test(R 两段式语义)、save-test、smoke的死亡段已同步改。
冰面滑行惯性(易反复踩坑)
- 冰面物理只在
js/player.js的水平输入段,sliding是唯一惯性开关:冰上|vx|>SLIDE_MIN置位,离冰后仍按sliding走惯性分支,撞墙/归零/交还控制时才清位。 - 空中与地面必须分开摩擦:
SLIDE_AIR 0.02(几乎无损,能滑出冰块边缘飞出去)vsSLIDE_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 连续三段因此误判不可达)。
- 关键修正:跳跃水平方向被墙挡住时不能把 vx 永久清零(真实
- 其余:
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%b3(可达性求解跳过)、坑向 r1~r4。11.6%)、耐久 e1e4、boss b1
移动端虚拟手柄(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映射。