// 编辑器交互回归:调色板无「查看属性」工具;无论手上拿什么方块(橡皮除外), // 点击画布上任意方块都选中该格并在属性面板显示其属性(不覆盖已有方块)。 const noop = () => { }; let fail = 0; const ok = (c, m) => { console.log((c ? ' ✔ ' : ' ✗ ') + m); if (!c) fail++; }; // ---------- 最小假 DOM(addEventListener 收句柄,children 真实入列)---------- const els = new Map(); function mkEl(id = '') { const e = { id, tagName: 'DIV', value: '', _html: '', textContent: '', style: {}, dataset: {}, children: [], width: 800, height: 608, classList: { _s: new Set(), add(...c) { c.forEach(x => this._s.add(x)); }, remove(...c) { c.forEach(x => this._s.delete(x)); }, toggle(c, on) { on ? this._s.add(c) : this._s.delete(c); }, contains(c) { return this._s.has(c); }, }, _h: {}, addEventListener(t, fn) { (this._h[t] = this._h[t] || []).push(fn); }, removeEventListener: noop, appendChild(c) { this.children.push(c); return c; }, querySelector: () => mkEl(), querySelectorAll: () => [], getBoundingClientRect: () => ({ left: 0, top: 0, width: 800, height: 608 }), getContext: () => ctx2d, focus: noop, click: noop, preventDefault: noop, }; Object.defineProperty(e, 'innerHTML', { get() { return this._html; }, set(v) { this._html = String(v); this.children = []; }, }); return e; } const ctx2d = new Proxy({ canvas: { width: 800, height: 608 }, measureText: () => ({ width: 12 }), createLinearGradient: () => ({ addColorStop: noop }), createRadialGradient: () => ({ addColorStop: noop }), getImageData: () => ({ data: [] }), }, { get(t, k) { return k in t ? t[k] : noop; }, set(t, k, v) { t[k] = v; return true; }, }); const getEl = s => { if (!els.has(s)) els.set(s, mkEl(s)); return els.get(s); }; globalThis.location = { search: '', href: 'http://localhost/', pathname: '/', reload: noop }; Object.defineProperty(globalThis, 'navigator', { value: { maxTouchPoints: 0, userAgent: 'node' }, configurable: true }); globalThis.matchMedia = q => ({ matches: false, media: q, addEventListener: noop, removeEventListener: noop }); globalThis.window = { addEventListener: noop, removeEventListener: noop, AudioContext: null, webkitAudioContext: null, innerWidth: 1280, innerHeight: 800, location: globalThis.location, navigator: globalThis.navigator, matchMedia: globalThis.matchMedia, }; globalThis.addEventListener = noop; globalThis.removeEventListener = noop; globalThis.confirm = () => true; globalThis.alert = noop; globalThis.localStorage = { _d: {}, getItem(k) { return this._d[k] ?? null; }, setItem(k, v) { this._d[k] = String(v); }, removeItem(k) { delete this._d[k]; } }; globalThis.document = { getElementById: getEl, querySelector: getEl, querySelectorAll: () => [], createElement(t) { const e = mkEl(); e.tagName = t.toUpperCase(); return e; }, addEventListener: noop, body: mkEl('body'), }; const { Editor, PALETTE } = await import('../js/editor.js'); // 画布坐标 → 格子:pos() 用 clientX/Y(scroll=0,缩放 1:1),格心 (tx*32+16, ty*32+16) const ev = (tx, ty, button = 0) => ({ clientX: tx * 32 + 16, clientY: ty * 32 + 16, button, preventDefault: noop }); const clickTile = (tx, ty, button = 0) => { for (const fn of getEl('#ed-canvas')._h.mousedown) fn(ev(tx, ty, button)); }; // renderProps 先写面板 innerHTML(标题),再把每个属性字段 appendChild 成子行 const propsHtml = () => { const box = getEl('#ed-props'); return box.innerHTML + box.children.map(c => c.innerHTML || '').join(''); }; console.log('=== 调色板不再有「查看属性」工具 ==='); ok(!PALETTE.some(p => p[0] === '?'), "PALETTE 中已无 '?'(查看属性)"); ok(!PALETTE.some(p => p[1] === '查看属性'), 'PALETTE 中已无「查看属性」条目'); Editor.init({ startLevel: noop }); const palLabels = getEl('#palette').children.map(c => c.textContent || ''); ok(palLabels.length === PALETTE.length, `调色板渲染 ${PALETTE.length} 项(无工具项),实际 ${palLabels.length}`); ok(!palLabels.some(t => t.includes('查看属性')), '调色板 UI 无「查看属性」按钮'); console.log('=== 打开地图,构造一块带属性的摄像头 C ==='); Editor.open(); const f = Editor.curFace(); f.grid[5][7] = 'C'; // 摄像头:SCHEMA 里有 4 个可编辑属性 f.props[5][7] = { w: 4, h: 4, mode: 'zoom', zoom: 1.5 }; console.log('=== 手上拿刺(^),点击已有摄像头 → 只看属性,不覆盖 ==='); Editor.sel = '^'; clickTile(7, 5); ok(f.grid[5][7] === 'C', '格子内容未被覆盖(仍是 C)'); ok(Editor.selTile && Editor.selTile.tx === 7 && Editor.selTile.ty === 5, 'selTile 指向被点方块 (7,5)'); const html1 = propsHtml(); ok(html1.includes('区域宽') && html1.includes('镜头模式'), '属性面板显示该方块的属性(区域宽/镜头模式)'); console.log('=== 手上拿方块(#),点击同一格 → 同样只查看,不覆盖 ==='); Editor.sel = '#'; clickTile(7, 5); ok(f.grid[5][7] === 'C', '拿 # 点击也不覆盖已有方块'); ok(propsHtml().includes('区域宽'), '属性面板仍显示 C 的属性'); console.log('=== 手上拿炮台(O),点击空格子 → 放置并显示新方块属性 ==='); Editor.sel = 'O'; clickTile(3, 9); ok(f.grid[9][3] === 'O', '空格子正常放置 O'); ok(propsHtml().includes('发射间隔'), '放置后属性面板显示 O 的属性'); console.log('=== 提示牌 T 的文本属性可随点击查看 ==='); f.grid[9][4] = 'T'; f.props[9][4] = { msg: '小心刺' }; Editor.sel = '%'; // 拿着冰面点提示牌 clickTile(4, 9); ok(f.grid[9][4] === 'T' && propsHtml().includes('提示文字'), '非相关工具点击 T 也能查看文字属性'); console.log('=== 橡皮擦例外:选中橡皮点击 → 擦除 ==='); Editor.sel = '.'; clickTile(4, 9); ok(f.grid[9][4] === '.', '橡皮擦点击后 T 被擦除'); Editor.sel = '^'; // 恢复非橡皮工具 clickTile(7, 5); // 顺手确认 C 还在 console.log('=== 右键擦除与工具无关 ==='); Editor.sel = '#'; clickTile(3, 9, 2); // button=2 右键 ok(f.grid[9][3] === '.', '右键擦除不受手上工具影响'); console.log('=== 属性面板空态提示仍存在 ==='); Editor.show(); // 重置 selTile ok(propsHtml().includes('点击画布上的方块'), '未选中时显示引导文案'); console.log(fail === 0 ? '\n✅ 编辑器点击查看属性 全部断言通过' : `\n❌ 失败 ${fail} 项`); process.exit(fail === 0 ? 0 : 1);