63 lines
13 KiB
Markdown
63 lines
13 KiB
Markdown
# 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.js` update 开头 `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.js` render 的 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`(几乎无损,能滑出冰块边缘飞出去)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 行 = 单跳安全/二段必死;放太近(3~4 行)会让单跳也撞死。冰面上**不要**加压顶垂刺(滑行无法控制跳跃高度,实测 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 关)
|
||
跳刺 j1~j10(68×52 ~ 150×44,刺密度 3.8%~11.6%)、耐久 e1~e4、boss b1~b3(可达性求解跳过)、坑向 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` 映射。
|
||
|