Files
2026-10-22 23:16:27 +08:00

143 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/prev` action 模型。
- 新模块 `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` 实测暴露):
1. **空中也按地面摩擦 0.25 衰减**(`roadFric` 不区分空/地)→ 滑出冰块边缘掉下去时水平速度 7 帧内归零,根本飞不出去,这是玩家最直观感到「没惯性」的场景。
2. 松开键时 0.25 的强摩擦一下刹死(实测只滑 2.5px);而按住同向键反而每帧 +0.5 一路涨到 4.05 且永不回落,滑行比走路还快。
- 现在 `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`(兼容旧单 grid `doors` 格式自动包成 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-json` textarea);`.editor-main{overflow:auto}`。
- 新增 `tools/map-test.mjs`(4 项:跨面门目标挂载 / `onPortal` 触发 / `savePoint.face` 记录 / 旧格式兼容)。`/tmp/ed_harness.mjs` 8 断言(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` 九套全绿。
## 编辑器:保存=更新当前地图 / 打开=新图 / 新增「导入地图」按钮(用户要求)
- 需求:①点「保存」不再每次新建,而是保存当前地图;②每次打开编辑器都是新地图;③加一个「导入地图」按钮,从以前保存的地图里选。
- `js/save.js`:新增 `updateCustom(lv)`(按 id 就地替换,id 不存在则追加 + flush)。`addCustom` 保留未删以兼容旧调用。
- `js/editor.js`:
- `open()` 改为**每次重置**为空白新图(宽高沿用 `#ed-w/#ed-h` 输入值)并 `this.cur=null`;抽出 `show()` 只重绘不重置。
- `save()`:`makeCustomLevel` 后若 `this.cur` 存在则 `lv.id = this.cur`,统一走 `Save.updateCustom` → 首次新建、之后就地更新(id 不变、列表不涨)。
- 新增导入:`openImport()`(弹窗 `#ed-importbox` 列 `Save.customs()`,空则提示)、`closeImport()`、`pickImport(id)`(hydrate + 设 cur + 关弹窗)。
- `updateSaveState()`:`#ed-savestate` 显示「→ 更新「XX」」/「→ 另存为新图」,在 open/show/save/pickImport/删除/列表点击后刷新。
- 「我的地图」列表点击仍是导入(设 cur + hydrate + 更新提示);删除正在编辑的地图时 `cur` 复位为 null。
- `index.html`:工具栏加 `data-act="ed-import"` 按钮 + `#ed-savestate` 提示;`#scr-editor` 内加 `#ed-importbox` 弹窗(`.modal`)。
- `css/style.css`:`.modal/.modal-panel/.imp-row/.imp-empty/.modal-btns/.save-state` 样式。
- 关键坑:`toEditor()`(暂停菜单「返回编辑器」)不能调 `open()`——那会把正在试玩的地图清空。改为 `Editor.faces ? Editor.show() : Editor.open()`。
- 验证:Node 桩冒烟(`/tmp/ed_save_import.mjs`,22 断言:open 恒新图/cur=null、首存新建+再存就地更新 id 不变、新图再存新增、导入载入+保存更新原图、删除解除关联、show 保留、savestate 提示切换、空存档导入提示);暂停菜单测试 5 断言仍过;`death/map/check/solve/smoke/save/key/dom-smoke/ice` 九套全绿。
## 编辑器去掉「我的地图」+ 关卡选择自制页加地图管理(删除)(用户要求)
- 需求:①编辑器右侧去掉「我的地图」,只保留「导入地图」;②关卡选择的自制页要有地图管理选项,可删除某个地图。
- 编辑器瘦身:`index.html` 删掉 `<h3>我的地图</h3><div id="my-levels">`;`js/editor.js` 删除 `renderMyLevels()` 及其 4 处调用(init/show/save/pickImport)。导入地图弹窗 `#ed-importbox` 成为唯一入口,`updateSaveState()`(保存去向提示)保留。
- 自制页管理(`js/ui.js`):
- `UI.manage` 状态;`#manage-btn`(放在 `#scr-levels` 的 panel-head,`hidden` 类控制只在「自制」分类显示)。
- `setManage(on)` 切管理态(按钮文案 管理↔完成 + `.on` 高亮 + 重绘);`show('scr-levels')` 与切 tab 都会 `setManage(false)` 复位。
- `renderLevels()`:管理态且 `builtin===false` 时给卡片加 `.manage` 类 + 追加 `.lv-del`(✕)按钮,且**不给卡片绑 play**(避免误点开始);`delCustom(l)` = confirm → `Save.removeCustom(id)` → `renderLevels()`。
- `confirm` 用 `typeof confirm === 'function' &&` 守卫以便 Node 测试。
- `js/main.js`:`UI.init` 加 `manage: () => UI.setManage(!UI.manage)`。
- `css/style.css`:`.head-btns/.head-btns .btn.on`、`.lv-card.manage`(cursor:default、禁 hover 位移)、`.lv-del`(右上角红色 ✕)。
- 验证:Node 桩冒烟(`/tmp/ed_manage.mjs`,18 断言:管理按钮仅自制页可见、管理态渲染 ✕、删除指定图、confirm 取消不删、退出复位、切分类隐藏按钮并复位、管理态点卡不开始);编辑器 22 断言 + 暂停 5 断言仍过;`death/map/check/solve/smoke/save/key/dom-smoke/ice` 九套全绿;`grep renderMyLevels|my-levels` 无残留。
## 修复:管理按钮在其他分类仍显示 + 文案改「管理地图」(用户要求)
- 需求复述:①「管理地图」按钮**只在自制分类显示**,其他分类一律不显示;②按钮文字改成「管理地图」。
- **根因**:CSS 里只有 `.screen.hidden` / `#hud.hidden` / `.modal.hidden` / `#touch.hidden` 这类限定选择器,**没有通用 `.hidden{display:none}` 规则**。`js/ui.js` 给 `#manage-btn`(class=`btn tiny hidden`)加 `hidden` 类根本匹配不上任何规则 → 按钮在全部/跳刺/耐久/BOSS/坑向分类都照样显示。JS 逻辑(`classList.toggle('hidden', filter!=='custom')`)一直是对的,纯 CSS 漏规则。
- 修复:
- `css/style.css`:新增通用 `.hidden{display:none !important}`(放在 `.screen.hidden` 附近)。所有 `hidden` 类使用场景(screens/hud/boss-bar/modal/touch/manage-btn)语义一致,无副作用。
- `index.html`:按钮文案 `管理` → `管理地图`。
- `js/ui.js`:`renderLevels`/`setManage` 里重复的按钮刷新逻辑收敛为新方法 `updateManageBtn()`(hidden + 文案 + .on 一处维护,防漂移);`setManage(on)` 语义改为「非自制分类的开启请求按退出处理」(按钮不可见时必须自动复位管理态,不能 return 留下隐藏的激活态)。
- 教训:**JS 层 class 断言测试测不出 CSS 漏规则**(class 加上了但样式不存在)。已在 `/tmp/ed_manage.mjs` 增加 CSS 回归断言:正则检查 `css/style.css` 存在通用 `.hidden{...display:none...}` 规则。
- 验证:`/tmp/ed_manage.mjs` 24 断言全过(含 CSS 回归、setManage 非法请求复位、show 复位、文案切换);其余 3 个编辑器 harness(save_import 22 / pause_quit 5 / noclip_spike 13 / props 14)仍全过;`death/map/check/solve/smoke/save/key/dom-smoke/ice` 九套全绿;预览 8123 返回 200 且按钮文案/规则已生效。