16 KiB
16 KiB
2026-10-22
工作区被清空后从对话记录重建 + 移动端虚拟手柄
- 本会话开始时
/Users/cancerai/tmp/iwanna被清空(workspace_recovery 重建为空目录)。游戏全部源码并未丢失:完整对话记录在/Users/cancerai/.workbuddy/projects/Users-cancerai-tmp-iwanna/dd6693af-142a-4164-8c3a-fb33ab9626d8.jsonl(约 7.9MB)。 - 重建方法:按 JSONL 行序重放所有
Write/Edit调用;另有 7 处通过 Bash 的 sed/perl 修改项目文件(diamond→vgap、R-3→R-2、corner→block、j2 台阶加宽、3 个测试脚本mv进tools/并改 import)必须在对应行号处同步插值,否则jump.js大量 Edit 会 miss。重建后check.mjs与solve.mjs全部通过(23 关可达)。 - 新增移动端虚拟手柄:
js/input.js增加touch状态层 +setTouch(action,bool)/clearTouch(),与键盘共用now/prevaction 模型。- 新模块
js/touch.js:左下◀▶移动、右下大「跳」键 + 🔫射击(按住连发)+ ▼下蹲 + 暂停/重生小键;pointer 事件 +setPointerCapture支持多指;show/hide/setGun。 index.html加#touch覆盖层 + viewport 锁缩放;css/style.css加 IWBT 风格半透明控件(含env(safe-area-inset-bottom)安全区)。js/main.js:触屏自动检测(?touch=1强制开 /?touch=0关);startLevel/resume显示、pause/finish/菜单隐藏;HUD 同步hasGun控制射击键置灰;触屏射击走shoot()回调(受hasGun与冷却限制)。
- 验证:
node --check三个 JS 均通过;mock-DOM 运行测试确认按下/松开正确置位、pause/shoot 回调触发、hide 清理卡键;check/solve仍全绿。
桌面端不显示触屏按钮(用户明确要求)
- 原来只靠 JS 切换
#touch.hidden类来显隐,若某处误调Touch.show()桌面端会冒出按钮。 - 改为「CSS 硬兜底 +
<html>类门控」:仅当检测到移动端/触屏(ontouchstart/maxTouchPoints>0/(pointer:coarse))或?touch=1时,给documentElement加.touch类;css/style.css新增html:not(.touch) #touch{display:none !important}。这样桌面端无论 JS 状态如何都绝对不显示,触屏设备才显示。?touch=0仍可强制关。 - 验证:node 模拟 5 种环境(纯桌面/触屏笔电/手机/
?touch=1/?touch=0),输出与预期一致;服务端css/style.css已含该规则。
修复:滑出冰块后惯性丢失
- 根因:
js/player.js水平移动里onIce每帧按当前位置重算;一旦脚下滑出冰面,onIce=false走else分支this.vx = target,把冰面攒出的速度瞬间归零。 - 修复:新增
this.sliding状态。在冰上攒出|vx|>0.6即标记sliding=true;离开冰后只要sliding且|vx|>0.05,就用比冰面稍大的地面摩擦(0.25)缓慢收速,保留惯性;顺向按键保持/恢复满速、反向按键较快抵消;|vx|<0.1才清零并退出滑行,恢复 IWBT 标准「松手即停」。reset/respawn/kill三处都重置sliding=false。 - 验证:node 模拟「冰上按右 20 帧→vx=3.60,滑出冰面松手后 12 帧 vx 由 3.35 缓降到 0.60 仍滑行」;
solve.mjs仍全绿(可达性未退化)。
二次修复:滑出冰块后仍「感觉不到惯性」(真正根因)
- 上次只解决了「离冰瞬间归零」,但残留两个致命点(用
tools/ice-test.mjs实测暴露):- 空中也按地面摩擦 0.25 衰减(
roadFric不区分空/地)→ 滑出冰块边缘掉下去时水平速度 7 帧内归零,根本飞不出去,这是玩家最直观感到「没惯性」的场景。 - 松开键时 0.25 的强摩擦一下刹死(实测只滑 2.5px);而按住同向键反而每帧 +0.5 一路涨到 4.05 且永不回落,滑行比走路还快。
- 空中也按地面摩擦 0.25 衰减(
- 现在
PHY新增滑行常量:SLIDE_FRIC 0.12(地面收住)/SLIDE_AIR 0.02(空中几乎无损)/SLIDE_EASE 0.14(超速时收敛回走速)/SLIDE_BRAKE 0.45(反向刹车)/SLIDE_MIN 0.35/SLIDE_STOP 0.05;ICE_MAX = HSPD*1.35。 - 行为约定:空中保留惯性;地面缓慢收住(4.05→0 约 34 帧 ≈ 70px);按住同向键只让超速收敛回 3(不再加速),低于走速即交还玩家控制;反向键刹车后立刻反向;撞墙/归零即退出滑行。
- 另加可视化反馈:超速滑行且着地时每 5 帧在脚下溅 2 粒冰屑(
js/player.js),让惯性看得见。 - 新增回归测试
tools/ice-test.mjs(4 个场景 + 断言,退出码 0/1),已并入全套:check / solve / smoke / save-test / key-test / dom-smoke全绿。 - 顺手补了
tools/key-test.mjs与tools/dom-smoke.mjs缺失的浏览器桩:location、以及移动端手柄引入后需要的navigator(用Object.defineProperty覆盖,Node 22 只读) 与matchMedia——这两个脚本此前在 Node 22 下直接崩,现在可跑。
死亡即复活(无死亡动画)
- 用户要求:kid 死亡后立刻复活,不要死亡动画。
js/player.js:kill()去掉粒子爆炸、震屏、死亡张嘴,只留sfx.death()与onDeath计数;update()顶部if (this.dead) this.respawn();让 R 键自杀/假存档触发的死亡下帧即复;边界坠落与deadlyRect两处致死点改为kill()后同帧respawn()并return(不再等 34 帧的死亡动画);respawn()末尾置justRespawned=true。js/main.js:复活帧镜头snapTo瞬移(wasDead && !P.dead或justRespawned);删除渲染里的死亡血色闪烁rgba(200,0,40,.18)。js/sprites.js:删除p.dead时的张嘴绘制。- 新增回归测试
tools/death-test.mjs(10 项断言:同帧复活/无粒子/无震屏/justRespawned/连续 5 次/R 键自杀后立即可操作/30 帧无特效残留)。全套 7 个测试仍全绿。
死亡地点加 IWBTG 风格血迹效果(用户追加要求)
- 需求:即时复活仍保留,但在死亡地点留下模仿 IWBT(I Wanna Be The Guy)的 kid 死亡效果(血腥飞溅)。
- 实现:在
js/level.js新增常驻deathMarks列表(仅reset(true)整关重开时清空,softReset()不碰 → 复活后血迹仍在)。addDeathMark(x,y):在死亡坐标落一处血洼 + 10 个卫星血点(基于 seed 的稳定伪随机,形状逐帧一致),外加 18 个红色方块飞溅碎块(自带 parts 列表,受重力、~32 帧后消失);最多保留 60 处。drawDeathMark(ctx,dm):世界坐标下绘制常驻血迹 + 动画碎块;放在draw()的实体/子弹之后、全局粒子之前。update()末尾演化各 mark 的碎块(重力、衰减、移除死亡碎块),血点常驻。
js/player.js的kill():sfx.death()后调用this.level.addDeathMark(this.cx, this.cy)(死亡坐标)。仍不触发震屏/屏幕级粒子(保持「无死亡动画」)。tools/death-test.mjs扩充到 14 项:新增「死亡地点留下 IWBT 血迹」「30 帧后血迹常驻」「多次死亡血迹累积」「整关重开清空」。全套仍全绿。
死亡血迹改为「只显示上一次死亡」(用户再次调整)
- 用户要求:多次死亡时只显示上一次死亡的血迹,不要累积成尸体堆。
js/level.js的addDeathMark改为新增前先this.deathMarks.length = 0(清掉旧血迹),所以任意时刻场上仅保留最近一次死亡的血迹;整关重开reset(true)仍清空。tools/death-test.mjs地图 row2 加第二枚刺(col10),测试 5 改为「4 次死亡(前后两处)后deathMarks.length === 1且位于最后一次 col10」,验证只保留上一次。全套仍全绿。
死亡标记从血迹改为「红叉(X)」(用户再次调整)
- 用户要求:把死亡效果从血洼/血点/飞溅碎块改成简单的红叉即可。
js/level.js:addDeathMark只记{x,y}(不再生成 parts 碎块);drawDeathMark改为画一个带深色描边的红色 X(半边长 9,lineWidth 5 黑描边 + 3 红主体);update()去掉碎块演化循环(红叉为常驻静态标记)。仍只在死亡地点显示、只保留上一次、整关重开清空。tools/death-test.mjs措辞「血迹」→「红叉」。全套仍全绿。
自制关卡:可变宽高 + 传送门 + 多面地图 + 极简导入导出(用户新需求)
- 需求:编辑器里可自定义地图宽高;引入「传送门(W)」,一个地图可含多面(子关卡);导入导出对非专业者友好。
- 经
AskUserQuestion确认三处设计:- 传送门配对 = 显式指定目标(每个门直接选「去哪个面 + 哪个门」,不自动两两配对);
- 面尺寸 = 每个面独立宽高(clamp 12–160 × 8–80);
- 导入导出 = 一键文件 + 分享码(下载 .json / 复制分享码;从文件导入 / 粘贴导入)。
js/level.js:reset()里从this.data.doors读取门目标to:{face,door}写到 warp 实体的e.to;e.doorId取门 id、e.pair兜底取相邻序号、e.cool/hidden支持。js/player.js:warp 分支识别「跨面目标对象(to.face为 number 且无 x/y)」→ 调this.onPortal(e)交主程序处理,否则走原spawn/坐标/pair逻辑;save 分支把face写进savePoint。js/main.js:G.map[]+G.face;startLevel在data.faces存在时建多面;doPortal(e)→switchFace换激活 Level、dest.cool=40防回弹;doRespawnFace()回到savePoint.face;HUD 多面时显示面N/M。js/editor.js重写:状态faces[](每面{name,w,h,seq,grid,doors});resize(w,h)可变高、裁剪越界门、addFace/copyFace/delFace;门放W自动注册 id,renderDoors()每门两个下拉(目标面+目标门,禁自连);buildData()输出{name,type:'custom',startFace:0,faces:[...]};validate()校验每面有 1 个 S 且 ≥1 个 X、门不自连;导入导出download/copyCode/importFile/importMap(兼容旧单 griddoors格式自动包成 1 面);3 处confirm()加typeof confirm==='function'守卫以兼容 Node。index.html+css/style.css:编辑器区改为ed-name/ed-w/ed-h/ed-resize+ 面面板(ed-addface/copyface/delface+#ed-faces) + 门面板(#ed-doors) + 一键导入导出(ed-download/ed-copy/ed-file/ed-import/ed-export+#ed-jsontextarea);.editor-main{overflow:auto}。- 新增
tools/map-test.mjs(4 项:跨面门目标挂载 /onPortal触发 /savePoint.face记录 / 旧格式兼容)。/tmp/ed_harness.mjs8 断言(buildData 双面、门目标读写、validate、importMap 保留、增删面计数、resize、旧单 grid 导入、缺 S 拦截)全过。 - 验证:
death/map/check/solve/smoke/save/key/dom-smoke/ice九套全绿;23 关可达性未退化;预览服务8123返回 200。
编辑器:去掉导入导出,传送门改为「属性面板」方式(用户新需求)
- 用户要求:去掉导入/导出,只用「保存」;把多关传送门设置改成「修改属性」方式——在自制关卡右侧显示一列「属性列表」,展示当前选中方块的可设属性(含跳转到下一关),其他有属性的方块也放进去。
js/editor.js改动:- 删除
download/copyCode/importFile/importMap/importPaste/exportToTextarea/mapString/renderDoors全部导入导出与独立传送门面板逻辑;保留save()与「我的地图」「面」面板。 - 新增 每格属性 模型
f.props[ty][tx](null 或对象),与grid同形;blank/propsBlank同步、resize/copyFace/hydrate都维护 props。 - 新增
SCHEMA:按方块类型声明可编辑字段(portal/text/num/select)。W→跳转到下一关(两下拉写doors[].to);T→提示文字;O→朝向/间隔/速度/延迟;C→区域宽高/镜头模式/缩放;B→类型/血量。 - 选中格
this.selTile:画布点击即选中并高亮(黄框);调色板首项⌖ 查看属性为只查看不修改的画笔。右侧#ed-props由renderProps()按SCHEMA渲染。 buildData()按扫描顺序把props导出为messages/turrets/camzones/bosses数组(引擎按扫描下标读取,顺序一致);doors原样导出。hydrate(o)反向:按扫描顺序把这些数组回填到f.props[ty][tx]。
- 删除
index.html:删除「导入/导出」区块(按钮 +#ed-json)与「传送门(显式指定目标)」面板;新增#ed-props属性列表面板;editor-tip文案更新。css/style.css:新增#ed-props/.prop-head/.prop-empty/.prop-row/.prop-lab/.prop-in/.prop-ctl样式;删除无用的#ed-json/.io-btns/.door-row样式。- 验证:Node 桩冒烟(
/tmp/ed_props_harness2.mjs,14 断言全过:各类型 renderProps 不抛异常、buildData 各数组导出正确、hydrate 往返恢复、validate 门自指/缺 S 拦截);death/map/check/solve/smoke/save/key/dom-smoke/ice九套仍全绿。
编辑器:放置不覆盖 + 刺合并为单类型(方向走属性面板)(用户再次要求)
- 需求:①放置方块时若当前格已有方块则不覆盖;②把「上/下/左/右」四个刺合并成一个刺,上下左右方向改到右侧属性面板调。
- 引擎侧不动:
js/level.js的CHAR2TILE仍是^=SPIKE_U / v=SPIKE_D / <=SPIKE_L / >=SPIKE_R,spikeBox()按 T.SPIKE_* 收紧判定。所以只在编辑器层做「单 char + props.dir ↔ 四字符」双向翻译,solve/check等可达性完全不受影响。 js/editor.js:PALETTE删掉v < >,只留['^','刺',...];新增SPIKE_CHAR{up:^,down:v,left:<,right:>}/SPIKE_DIR(反向) /SPIKE_ARROW{up:↑,...}。SCHEMA增加'^': [{type:'select',key:'dir',def:'up',opts:[上/下/左/右]}];renderProps的 select 分支改为cur ?? fd.def兜底默认值。act()放置段新增:if(!erase && ch!=='.' && prev!=='.') { this.renderProps(); return; }—— 放实方块遇已有方块直接跳过(仍选中该格以便看属性);擦除/橡皮ch==='.'与?查看工具不受此限。draw()刺格不再写^,改写SPIKE_ARROW[dir](↑↓←→)让方向在画布上可见。buildData()导出 grid 时把每格^按props.dir翻译成^ v < >;hydrate()反向把^ v < >统一还原成^并写props.dir。往返一致。
index.html:editor-tip改为说明「不会覆盖已有方块」「刺只有一个、方向在右侧属性面板调」。- 验证:Node 桩冒烟(
/tmp/ed_noclip_spike.mjs,13 断言全过:不覆盖/擦除/橡皮、buildData 四向翻译、renderProps('^') 不抛、hydrate 往返一致);death/map/check/solve/smoke/save/key/dom-smoke/ice九套全绿。
暂停菜单末项:试玩时改为「返回编辑器」
- 需求:试玩界面暂停菜单的最后一项从「返回关卡选择」改成「返回编辑器」,跳转功能同步改。
- 只在「从编辑器试玩」时生效,普通游玩(含从关卡选择启动自制关)仍是「返回关卡选择」,不影响原流程。
js/main.js:G新增fromEditor标志;Editor.init的startLevel回调置true,UI.init的play回调置false(restart/retry不重置,保持来源)。- 新增
toEditor()(=editor动作原逻辑:state=editor + show scr-editor +Editor.open(),因 faces 已存在只是重渲染,地图不丢)。 pause()里按G.fromEditor改#scr-pause [data-act="quit"]的 textContent;UI.init的quit改为() => (G.fromEditor ? toEditor() : toLevels())。
- 结算屏(scr-result)的 quit 未动,仍为「关卡选择」——用户只要求暂停界面。
- 验证:Node 桩冒烟(
/tmp/ed_pause_quit.mjs,5 断言:两种来源标签切换、quit 两种跳转、从关卡选择启动自制关复位 fromEditor);death/map/check/solve/smoke/save/key/dom-smoke/ice九套全绿。