diff --git a/.workbuddy/memory/2026-10-22.md b/.workbuddy/memory/2026-10-22.md index 34247fa..2f4244c 100644 --- a/.workbuddy/memory/2026-10-22.md +++ b/.workbuddy/memory/2026-10-22.md @@ -104,3 +104,39 @@ - `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` 删掉 `

我的地图

`;`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 且按钮文案/规则已生效。 diff --git a/css/style.css b/css/style.css index b46b02c..04dc4c1 100644 --- a/css/style.css +++ b/css/style.css @@ -59,6 +59,7 @@ canvas#game{ } .screen.hidden{display:none} #hud.hidden,#boss-bar.hidden{display:none} +.hidden{display:none !important} /* 通用:任何元素加 hidden 类即隐藏(如自制页「管理地图」按钮) */ .title-wrap{text-align:center;margin-bottom:1vh} .title-sub{color:var(--dim);letter-spacing:6px;font-size:.75em} @@ -92,6 +93,8 @@ canvas#game{ } .panel.narrow{width:min(400px,92%)} .panel-head{display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line);padding-bottom:6px} +.head-btns{display:flex;gap:6px;align-items:center} +.head-btns .btn.on{background:var(--red);border-color:var(--red2);color:#fff} .panel h2{font-size:1.3em;letter-spacing:3px} .panel h3{font-size:1em;letter-spacing:2px;color:var(--gold);margin-bottom:5px} @@ -117,6 +120,15 @@ canvas#game{ .lv-card .st{margin-top:5px;font-size:.78em;color:var(--dim)} .lv-card.cleared{border-color:#2f6b45} .lv-card .chk{position:absolute;right:7px;top:6px;color:var(--green)} +/* 自制页管理态:删除按钮 */ +.lv-card.manage{cursor:default;border-color:var(--red2)} +.lv-card.manage:hover{transform:none;border-color:var(--red);background:#241a22} +.lv-del{ + position:absolute;right:6px;top:6px;width:22px;height:22px;padding:0; + background:#3a1420;border:1px solid var(--red2);color:var(--red); + font-family:inherit;font-size:.8em;line-height:1;cursor:pointer;border-radius:2px; +} +.lv-del:hover{background:var(--red);color:#fff;border-color:var(--red)} .tag{padding:1px 6px;border:1px solid;font-size:.85em} .t-jump{color:#ff8a5c;border-color:#ff8a5c} .t-endur{color:#4ad0ff;border-color:#4ad0ff} @@ -187,6 +199,25 @@ canvas#ed-canvas{ .prop-in{background:#111119;border:1px solid var(--line);color:var(--fg);font-family:inherit;font-size:1em;padding:3px 4px;width:100%} .prop-ctl .prop-in{width:auto;flex:1} +/* 「保存」去向提示 */ +.save-state{color:var(--dim);font-size:.78em;white-space:nowrap;align-self:center} +.save-state.on{color:var(--gold)} + +/* 导入地图弹窗 */ +.modal{position:fixed;inset:0;z-index:60;display:flex;align-items:center;justify-content:center;background:rgba(4,4,10,.72);padding:4vh 4vw} +.modal.hidden{display:none} +.modal-panel{background:var(--bg2);border:2px solid var(--line);width:min(380px,94%);max-height:86vh;display:flex;flex-direction:column;gap:8px;padding:14px} +.modal-panel h3{font-size:1.05em;letter-spacing:2px;color:var(--gold)} +.modal-tip{color:var(--dim);font-size:.78em;line-height:1.5} +#ed-import-list{display:flex;flex-direction:column;gap:4px;overflow-y:auto} +.imp-row{background:#1b1b26;border:1px solid var(--line);padding:6px 8px;font-size:.84em;cursor:pointer;display:flex;justify-content:space-between;align-items:center;gap:6px} +.imp-row:hover{border-color:var(--gold)} +.imp-row.on{border-color:var(--gold);background:#2b2b3c} +.imp-nm{overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.imp-meta{color:var(--dim);font-size:.9em;white-space:nowrap} +.imp-empty{color:var(--dim);font-size:.8em;padding:10px 2px} +.modal-btns{display:flex;justify-content:flex-end;gap:6px} + ::-webkit-scrollbar{width:8px;height:8px} ::-webkit-scrollbar-track{background:#111} ::-webkit-scrollbar-thumb{background:#333;} diff --git a/index.html b/index.html index e5edf61..1abca4e 100644 --- a/index.html +++ b/index.html @@ -48,7 +48,10 @@

关卡选择

- +
+ + +
@@ -148,8 +151,10 @@ + + → 新地图
左键放置 / 右键擦除,滚轮水平平移。不会覆盖已有方块——放在空格才生效,要换掉先擦除。选「⌖ 查看属性」再点方块可只查看不修改。刺只有一个,放好后在右侧「属性」面板调方向(上/下/左/右);传送门 W、提示牌 T、炮台 O、摄像头 C、BOSS B 的属性也都在这里设置。每面至少 1 个 S(出生点) 与 1 个 X(终点)。
@@ -157,8 +162,6 @@

属性(选中方块)

点击画布上的方块,查看并设置它的属性
-

我的地图

-

面(多面地图)

@@ -168,6 +171,17 @@
+ +
diff --git a/js/editor.js b/js/editor.js index d1cf91d..8057572 100644 --- a/js/editor.js +++ b/js/editor.js @@ -65,7 +65,6 @@ export const Editor = { this.bind(); $('#ed-w').addEventListener('change', () => this.resize(+$('#ed-w').value, +$('#ed-h').value)); $('#ed-h').addEventListener('change', () => this.resize(+$('#ed-w').value, +$('#ed-h').value)); - this.renderMyLevels(); }, buildPalette() { @@ -133,6 +132,8 @@ export const Editor = { on('ed-clear', () => { if (typeof confirm === 'function' && !confirm('清空当前面?')) return; const f = this.curFace(); f.grid = blank(f.w, f.h); f.doors = []; f.props = propsBlank(f.w, f.h); this.selTile = null; this.draw(); this.renderProps(); }); on('ed-test', () => this.test()); on('ed-save', () => this.save()); + on('ed-import', () => this.openImport()); + on('ed-import-cancel', () => this.closeImport()); on('ed-addface', () => this.addFace()); on('ed-copyface', () => this.copyFace()); on('ed-delface', () => this.delFace()); @@ -142,18 +143,33 @@ export const Editor = { curFace() { return this.faces[this.face]; }, open() { - if (!this.faces) { - const w = +$('#ed-w').value || 25, h = +$('#ed-h').value || 19; - this.faces = [{ name: '面1', w, h, seq: 0, grid: blank(w, h), doors: [], props: propsBlank(w, h) }]; - this.face = 0; - } + // 从菜单进入编辑器:始终是新地图(宽高沿用输入框里的值) + const w = +$('#ed-w').value || 25, h = +$('#ed-h').value || 19; + this.faces = [{ name: '面1', w, h, seq: 0, grid: blank(w, h), doors: [], props: propsBlank(w, h) }]; + this.face = 0; + this.cur = null; // 未关联任何已存地图 → 首次保存为新建 + this.show(); + }, + + /** 仅重绘当前状态(不从零重置):用于试玩后「返回编辑器」等场景,保留正在编辑的地图 */ + show() { + if (!this.faces) { this.open(); return; } this.selTile = null; $('#ed-w').value = this.curFace().w; $('#ed-h').value = this.curFace().h; this.draw(); this.renderFaces(); this.renderProps(); - this.renderMyLevels(); + this.updateSaveState(); + }, + + /** 「保存」按钮旁的提示:会更新哪张图,还是另存为新图 */ + updateSaveState() { + const el = $('#ed-savestate'); + if (!el) return; + const l = this.cur ? Save.customs().find(o => o.id === this.cur) : null; + el.textContent = l ? '→ 更新「' + (l.name || 'MY MAP') + '」' : '→ 另存为新图'; + el.classList.toggle('on', !!l); }, resize(w, h) { @@ -408,41 +424,45 @@ export const Editor = { save() { const err = this.validate(); if (err) { alert(err); return; } - const lv = makeCustomLevel(this.buildData()); - Save.addCustom(lv); + let lv = makeCustomLevel(this.buildData()); + if (this.cur) lv.id = this.cur; // 保存当前地图:沿用已有 id,就地更新 + Save.updateCustom(lv); this.cur = lv.id; - this.renderMyLevels(); + this.updateSaveState(); this.api.refresh?.(); }, - // ---------------- 我的关卡 ---------------- - renderMyLevels() { - const box = $('#my-levels'); + // ---------------- 导入地图(从已保存的地图里选) ---------------- + openImport() { + const box = $('#ed-importbox'); if (!box) return; - box.innerHTML = ''; - const list = Save.customs(); - if (!list.length) box.innerHTML = '
还没有保存的关卡
'; - list.forEach((l, i) => { - const d = document.createElement('div'); - const fn = (l.faces || []).length; - d.className = 'my-lv' + (l.id === this.cur ? ' on' : ''); - d.innerHTML = `${i + 1}. ${escapeHtml(l.name)}${fn ? ` [${fn}面]` : ''}✕`; - d.querySelector('span').onclick = () => { - this.cur = l.id; - this.hydrate(l); - $('#ed-name').value = l.name || ''; - this.afterFaceSwitch(); - this.renderMyLevels(); - }; - d.querySelector('.x').onclick = (e) => { - e.stopPropagation(); - if (typeof confirm === 'function' && !confirm('删除 ' + l.name + '?')) return; - Save.removeCustom(l.id); - this.renderMyLevels(); - this.api.refresh?.(); - }; - box.appendChild(d); - }); + const list = $('#ed-import-list'); + const customs = Save.customs(); + list.innerHTML = ''; + if (!customs.length) { + list.innerHTML = '
还没有保存过的地图,先「保存」一个吧
'; + } else { + customs.forEach(l => { + const fn = (l.faces || []).length; + const d = document.createElement('div'); + d.className = 'imp-row' + (l.id === this.cur ? ' on' : ''); + d.innerHTML = `${escapeHtml(l.name || 'MY MAP')}${fn ? fn + ' 面' : '单关'}`; + d.onclick = () => this.pickImport(l.id); + list.appendChild(d); + }); + } + box.classList.remove('hidden'); + }, + closeImport() { $('#ed-importbox')?.classList.add('hidden'); }, + pickImport(id) { + const l = Save.customs().find(o => o.id === id); + if (!l) return; + this.cur = l.id; + this.hydrate(l); + $('#ed-name').value = l.name || ''; + this.closeImport(); + this.afterFaceSwitch(); + this.updateSaveState(); }, // 从存档/数据对象恢复多面 + 每格属性(按扫描顺序回填 messages/turrets/camzones/bosses) diff --git a/js/main.js b/js/main.js index 1944641..9ef68ac 100644 --- a/js/main.js +++ b/js/main.js @@ -169,7 +169,13 @@ function nextLevel() { } function toLevels() { G.state = 'menu'; UI.show('scr-levels'); Touch.hide(); } function toTitle() { G.state = 'menu'; UI.show('scr-title'); Touch.hide(); } -function toEditor() { G.state = 'editor'; UI.show('scr-editor'); Touch.hide(); Editor.open(); } +function toEditor() { + G.state = 'editor'; + UI.show('scr-editor'); + Touch.hide(); + // 试玩后返回:保留正在编辑的地图;从未进过编辑器时才新建 + if (Editor.faces) Editor.show(); else Editor.open(); +} function pause() { if (G.state !== 'playing') return; G.state = 'paused'; @@ -289,6 +295,7 @@ Audio2.enabled = Save.data.audio !== false; UI.init({ play: (lv) => { G.fromEditor = false; startLevel(lv, lv.builtin === false ? -1 : (lv.index ?? -1)); }, levels: toLevels, + manage: () => UI.setManage(!UI.manage), help: () => UI.show('scr-help'), editor: toEditor, back: toTitle, diff --git a/js/save.js b/js/save.js index 2a90df7..0150383 100644 --- a/js/save.js +++ b/js/save.js @@ -42,6 +42,15 @@ export const Save = { this.flush(); return lv; }, + /** 更新已保存的自制关卡(按 id 就地替换,不新增);id 不存在则追加 */ + updateCustom(lv) { + this.data.customs = this.data.customs || []; + const i = this.data.customs.findIndex(l => l.id === lv.id); + if (i >= 0) this.data.customs[i] = lv; + else this.data.customs.push(lv); + this.flush(); + return lv; + }, removeCustom(id) { this.data.customs = (this.data.customs || []).filter(l => l.id !== id); this.flush(); diff --git a/js/ui.js b/js/ui.js index d50eca7..813fd07 100644 --- a/js/ui.js +++ b/js/ui.js @@ -14,6 +14,7 @@ const TABS = [ export const UI = { H: {}, // handlers filter: 'all', + manage: false, // 自制页「管理」态:开启后每张自制地图显示删除按钮 cur: 'scr-title', toastTimer: 0, @@ -37,6 +38,7 @@ export const UI = { b.textContent = label; b.onclick = () => { this.filter = k; + this.setManage(false); // 切换分类时退出管理模式 $$('#level-tabs .tab').forEach(t => t.classList.remove('on')); b.classList.add('on'); this.renderLevels(); @@ -49,7 +51,7 @@ export const UI = { SCREENS.forEach(s => $('#' + s).classList.toggle('hidden', s !== name)); this.cur = name; $('#hud').classList.toggle('hidden', name !== null && name !== ''); - if (name === 'scr-levels') this.renderLevels(); + if (name === 'scr-levels') { this.setManage(false); this.renderLevels(); } }, showGame() { SCREENS.forEach(s => $('#' + s).classList.add('hidden')); @@ -64,6 +66,9 @@ export const UI = { const all = [...LEVELS, ...customs]; const list = all.filter(l => this.filter === 'all' ? true : l.type === this.filter); + // 「管理地图」按钮只在自制分类下显示 + this.updateManageBtn(); + if (!list.length) { grid.innerHTML = '
这个分类下还没有关卡。去「自制关卡」造一个吧。
'; return; @@ -71,7 +76,7 @@ export const UI = { list.forEach(l => { const rec = Save.get(l.id); const card = document.createElement('div'); - card.className = 'lv-card' + (rec ? ' cleared' : ''); + card.className = 'lv-card' + (rec ? ' cleared' : '') + (this.manage && l.builtin === false ? ' manage' : ''); const stars = '★'.repeat(l.diff || 1) + '☆'.repeat(Math.max(0, 5 - (l.diff || 1))); card.innerHTML = `
${l.builtin === false ? 'U' + (l.no || '') : String(l.index + 1).padStart(2, '0')}
@@ -83,11 +88,46 @@ export const UI = {
${rec ? `✔ 死亡 ${rec.deaths} · ${fmtTime(rec.time)}` : '未通关'}
${rec ? '
✔
' : ''} `; - card.onclick = () => this.H.play(l); + // 管理态下自制卡不再可进入,只保留删除按钮,避免误点开始 + if (!(this.manage && l.builtin === false)) card.onclick = () => this.H.play(l); + // 管理态:自制地图显示删除按钮 + if (l.builtin === false && this.manage) { + const del = document.createElement('button'); + del.className = 'lv-del'; + del.textContent = '✕'; + del.title = '删除这张地图'; + del.onclick = (e) => { e.stopPropagation(); this.delCustom(l); }; + card.appendChild(del); + } grid.appendChild(card); }); }, + /** 刷新「管理地图」按钮:仅在自制分类可见;管理态文案变「完成」并高亮 */ + updateManageBtn() { + const mb = $('#manage-btn'); + if (!mb) return; + mb.classList.toggle('hidden', this.filter !== 'custom'); + mb.textContent = this.manage ? '完成' : '管理地图'; + mb.classList.toggle('on', this.manage); + }, + + /** 切换自制页的管理态(显示/隐藏删除按钮) */ + setManage(on) { + // 仅自制分类可进入管理态;非自制分类的开启请求按退出处理(按钮不可见,须自动复位) + if (on && this.filter !== 'custom') on = false; + this.manage = !!on; + this.updateManageBtn(); + this.renderLevels(); + }, + + /** 删除一张自制地图(从关卡选择界面管理) */ + delCustom(l) { + if (typeof confirm === 'function' && !confirm('删除地图「' + (l.name || 'MY MAP') + '」?此操作不可恢复')) return; + Save.removeCustom(l.id); + this.renderLevels(); + }, + hud({ name, deaths, ms, apples, totalApples, hasGun, save, totalSaves }) { $('#hud-level').textContent = name || ''; $('#hud-deaths').textContent = 'DEATH ' + deaths;