'use strict';

const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const grid = require('../grid-math');
const tools = require('../editor-tools');
const tileMeta = require('../tile-meta');
const spawnTools = require('../spawn-tools');
const spawnDefaults = require('../entity-spawn-defaults');
const entityTexPresets = require('../entity-texture-presets');
const levelIdUtil = require('../level-id');
const prefabSync = require('../prefab-sync');
const bakeIgnore = require('../bake-ignore');
let themeDb;
try {
    themeDb = require('../../theme-controller/dist/theme-db');
} catch (e) {
    themeDb = null;
}
const DB_REL = 'assets/level-data/levels-database.json';
const PALETTES_INDEX_REL = 'assets/resources/map-tiles/palettes/_index.json';

const THEME_LABELS = {
    silu: '丝路 silu',
    sanxing: '三星堆 sanxing',
    snow: '雪地 snow',
    chinese: '中国风 chinese',
    numMan: '数字人 numMan',
    redarmy: '红军 redarmy',
    default: '默认 default',
};

exports.style = `
  .lme-root { display:flex; flex-direction:column; height:100%; font-size:12px; }
  .lme-toolbar { display:flex; gap:6px; align-items:center; padding:8px; border-bottom:1px solid var(--color-normal-border); flex-wrap:wrap; }
  .lme-body { display:flex; flex:1; min-height:0; }
  .lme-center { flex:1; display:flex; flex-direction:column; min-width:0; border-right:1px solid var(--color-normal-border); }
  .lme-canvas-wrap { flex:1; overflow:hidden; background:#3a3a3a; padding:0; min-height:0; }
  #lme-canvas { display:block; cursor:crosshair; width:100%; height:100%; }
  .lme-palette { width:220px; padding:8px; overflow-y:auto; background:var(--color-normal-fill-emphasis); }
  .lme-palette h4 { margin:0 0 6px; font-size:12px; }
  .lme-palette-grid { display:grid; grid-template-columns:1fr 1fr; gap:6px; }
  .lme-tile { display:flex; flex-direction:column; align-items:center; padding:6px; border:2px solid transparent; border-radius:4px; cursor:pointer; background:var(--color-normal-fill); }
  .lme-tile.active { border-color:#4a9eff; }
  .lme-tile img { width:64px; height:64px; object-fit:contain; background:#2a2a2a; border-radius:2px; }
  .lme-tile span { margin-top:4px; font-size:10px; text-align:center; line-height:1.2; }
  .lme-status { padding:6px 8px; color:var(--color-normal-contrast-weaken); border-top:1px solid var(--color-normal-border); font-size:11px; }
  .lme-layer-btn.active { font-weight:bold; }
  #layer-ground.active { background:rgba(76,175,80,0.35); color:#e8f5e9; }
  #layer-border.active { background:rgba(255,152,0,0.35); color:#fff3e0; }
  .lme-mode-btn.active { font-weight:bold; background:rgba(156,39,176,0.35); }
  #spawn-player.active { background:rgba(33,150,243,0.35); }
  #spawn-vehicle.active { background:rgba(76,175,80,0.35); }
  #spawn-prop.active { background:rgba(255,193,7,0.35); }
  #spawn-prop-ground.active { background:rgba(255,152,0,0.35); }
  #spawn-erase.active { background:rgba(244,67,54,0.3); }
  .lme-spawn-tools { display:flex; gap:6px; align-items:center; flex-wrap:wrap; }
  .lme-spawn-dir { display:flex; gap:4px; align-items:center; }
  .lme-tools ui-button.active { font-weight:bold; background:var(--color-primary-fill); }
  .lme-tool-sep { width:1px; height:20px; background:var(--color-normal-border); margin:0 4px; }
`;

exports.template = `
<div class="lme-root">
  <div class="lme-toolbar">
    <ui-label>关卡 ID</ui-label>
    <ui-num-input id="level-id" step="1" min="91601" value="91601"></ui-num-input>
    <ui-button id="btn-prev">上一关</ui-button>
    <ui-button id="btn-next">下一关</ui-button>
    <ui-button id="btn-load">加载</ui-button>
    <ui-button id="btn-new">新建</ui-button>
    <ui-button id="btn-save" type="primary">保存地图</ui-button>
    <ui-button id="btn-import-prefab">从预制体同步</ui-button>
    <ui-button id="btn-open-prefab">打开 Level 预制体</ui-button>
    <ui-button id="btn-scene-snap">场景选中吸附</ui-button>
    <ui-button id="btn-attach-helper">启用场景吸附助手</ui-button>
    <span style="width:4px"></span>
    <ui-button id="btn-theme-ctrl">主题控制器</ui-button>
    <span style="width:4px"></span>
    <ui-label>地图主题</ui-label>
    <ui-select id="theme-select" style="min-width:140px"></ui-select>
    <span style="width:4px"></span>
    <ui-label>编辑</ui-label>
    <ui-button class="lme-mode-btn active" id="mode-map">地图</ui-button>
    <ui-button class="lme-mode-btn" id="mode-spawns">实体配置</ui-button>
    <span class="lme-spawn-tools" id="spawn-tools-wrap" style="display:none">
      <ui-button id="spawn-player" class="active">玩家×1</ui-button>
      <ui-button id="spawn-vehicle">载具 0/1</ui-button>
      <ui-button id="spawn-prop">可拾取物(砖)</ui-button>
      <ui-button id="spawn-prop-ground">可拾取物(空地)</ui-button>
      <ui-button id="spawn-erase">删除</ui-button>
      <ui-button id="spawn-clear-vehicle">清除载具</ui-button>
      <span class="lme-spawn-dir" id="spawn-dir-wrap">
        <ui-label>朝向</ui-label>
        <ui-select id="spawn-direction" style="min-width:96px"></ui-select>
      </span>
      <span class="lme-spawn-scale" id="spawn-scale-wrap">
        <ui-label>放置缩放</ui-label>
        <ui-num-input id="spawn-placement-scale" step="0.1" min="0.1" max="4" value="1" style="width:64px"></ui-num-input>
      </span>
    </span>
    <span style="width:4px"></span>
    <ui-label>绘制层</ui-label>
    <ui-button class="lme-layer-btn active" id="layer-ground">Ground</ui-button>
    <ui-button class="lme-layer-btn" id="layer-border">Border</ui-button>
    <span class="lme-tools">
      <ui-button id="tool-paint" class="active" title="画笔">画笔</ui-button>
      <ui-button id="tool-box" title="框选填充">框选</ui-button>
      <ui-button id="tool-picker" title="吸管">吸管</ui-button>
      <ui-button id="tool-eraser" title="橡皮擦">橡皮擦</ui-button>
      <ui-button id="tool-fill" title="油漆桶">填充</ui-button>
    </span>
  </div>
  <div class="lme-body">
    <div class="lme-center">
      <div class="lme-canvas-wrap">
        <canvas id="lme-canvas" width="720" height="520"></canvas>
      </div>
      <div class="lme-status" id="status">地图/出生点编辑 · 绘制后自动暂存 · 点「保存地图」写入 JSON 并烘焙预制体 · 滚轮缩放 · 长按右键拖动</div>
    </div>
    <div class="lme-palette">
      <h4 id="palette-title">平铺调色板</h4>
      <div class="lme-palette-grid" id="palette-list"></div>
    </div>
  </div>
</div>
`;

exports.$ = {
    levelId: '#level-id',
    btnPrev: '#btn-prev',
    btnNext: '#btn-next',
    btnLoad: '#btn-load',
    btnNew: '#btn-new',
    btnSave: '#btn-save',
    btnImportPrefab: '#btn-import-prefab',
    btnOpenPrefab: '#btn-open-prefab',
    btnSceneSnap: '#btn-scene-snap',
    btnAttachHelper: '#btn-attach-helper',
    btnThemeCtrl: '#btn-theme-ctrl',
    themeSelect: '#theme-select',
    modeMap: '#mode-map',
    modeSpawns: '#mode-spawns',
    spawnToolsWrap: '#spawn-tools-wrap',
    spawnPlayer: '#spawn-player',
    spawnVehicle: '#spawn-vehicle',
    spawnProp: '#spawn-prop',
    spawnPropGround: '#spawn-prop-ground',
    spawnErase: '#spawn-erase',
    spawnClearVehicle: '#spawn-clear-vehicle',
    spawnDirection: '#spawn-direction',
    spawnDirWrap: '#spawn-dir-wrap',
    spawnPlacementScale: '#spawn-placement-scale',
    spawnScaleWrap: '#spawn-scale-wrap',
    layerGround: '#layer-ground',
    layerBorder: '#layer-border',
    toolPaint: '#tool-paint',
    toolBox: '#tool-box',
    toolPicker: '#tool-picker',
    toolEraser: '#tool-eraser',
    toolFill: '#tool-fill',
    canvas: '#lme-canvas',
    paletteList: '#palette-list',
    paletteTitle: '#palette-title',
    status: '#status',
};

function projectPath() {
    return Editor.Project.path;
}

function dbPath() {
    return path.join(projectPath(), DB_REL);
}

function palettesIndexPath() {
    return path.join(projectPath(), PALETTES_INDEX_REL);
}

function textureFsPath(texRel) {
    const base = texRel.startsWith('textures/') ? texRel : `textures/${texRel}`;
    return path.join(projectPath(), 'assets/resources', `${base}.png`);
}

function bindOnceOpts(el, event, handler, opts) {
    if (!el || el._lmeBoundOpts === handler) return;
    el._lmeBoundOpts = handler;
    el.addEventListener(event, handler, opts);
}

function bindOnce(el, event, handler) {
    bindOnceOpts(el, event, handler, undefined);
}

function normalizeBorder(border) {
    const out = {};
    for (const [k, v] of Object.entries(border || {})) {
        if (v === true || v === 1) {
            out[k] = 'WallBlock';
            continue;
        }
        if (typeof v === 'string') {
            const s = v.trim();
            // 地砖名不得进入 Border
            out[k] = (!s || s === 'Baseblock' || s === 'JumpBlock') ? 'WallBlock' : s;
            continue;
        }
        out[k] = 'WallBlock';
    }
    return out;
}

function normalizeGround(ground) {
    const out = {};
    for (const [k, v] of Object.entries(ground || {})) {
        out[k] = v === 'JumpBlock' ? 'JumpBlock' : 'Baseblock';
    }
    return out;
}

function emptyLevelConfig(levelId, theme) {
    return levelIdUtil.syncLevelEntry({
        boundary: { x: 10, y: 10 },
        spawns: [
            {
                x: 0,
                y: 0,
                kind: 'player',
                playerDirection: 'Direction.South',
            },
            {
                x: 1,
                y: 0,
                kind: 'prop',
            },
        ],
        ground: {},
        border: {},
        theme: theme || 'sanxing',
    }, levelId);
}

function readDatabase() {
    const raw = fs.readFileSync(dbPath(), 'utf8');
    return levelIdUtil.normalizeDb(JSON.parse(raw));
}

function writeDatabase(db) {
    levelIdUtil.normalizeDb(db);
    levelIdUtil.updateDbStats(db);
    db.generatedAt = new Date().toISOString();
    fs.writeFileSync(dbPath(), JSON.stringify(db, null, 2), 'utf8');
}

function parseLevelIdInput(raw, db) {
    const n = parseInt(String(raw || ''), 10);
    if (!Number.isNaN(n) && n >= levelIdUtil.minLevelId()) return n;
    return levelIdUtil.nextAvailableLevelId(db);
}

function tileBrushId(tile) {
    return tile.tileKey || tile.name || tile.display;
}

function loadImageCache(state, texRel) {
    const fsPath = textureFsPath(texRel);
    if (state.imageCache[fsPath]) return state.imageCache[fsPath];
    const img = new Image();
    img.src = `file://${fsPath}`;
    state.imageCache[fsPath] = img;
    if (fs.existsSync(fsPath)) {
        img.onload = () => {
            if (state._drawPending) return;
            state._drawPending = true;
            requestAnimationFrame(() => {
                state._drawPending = false;
                if (state._lastDraw) state._lastDraw();
            });
        };
    }
    return img;
}

exports.ready = function () {
    console.log('[level-map-editor] panel ready');
    if (this._lmeReady) return;
    this._lmeReady = true;

    const state = {
        levelId: levelIdUtil.minLevelId(),
        config: null,
        db: null,
        theme: 'sanxing',
        palettes: {},
        tiles: [],
        editLayer: 'ground',
        editMode: 'map',
        spawnTool: 'player',
        spawnDirection: 'Direction.South',
        placementScale: { ...spawnDefaults.DEFAULT_SPAWN_SCALE },
        selectedSpawn: null,
        tool: 'paint',
        brush: null,
        painting: false,
        hoverCell: null,
        boxStart: null,
        boxEnd: null,
        lastPaintKey: null,
        imageCache: {},
        offsetX: 0,
        offsetY: 0,
        zoom: 1,
        panX: 0,
        panY: 0,
        panActive: false,
        rmbDown: false,
        rmbTimer: null,
        panLast: null,
        _drawPending: false,
        _lastDraw: null,
        _autoBakeTimer: null,
    };

    const setStatus = (msg) => {
        if (this.$.status) this.$.status.textContent = msg;
    };

    const applyThemeTiles = () => {
        const entry = state.palettes[state.theme];
        if (!entry || !entry.tiles) {
            state.tiles = [];
            return;
        }
        state.tiles = entry.tiles.map((t) => ({
            ...t,
            name: t.tileKey,
            display: t.display || t.tileKey,
        }));
        if (this.$.paletteTitle) {
            this.$.paletteTitle.textContent = `平铺调色板 — ${THEME_LABELS[state.theme] || state.theme}`;
        }
        renderPalette.call(this, state);
    };

    const loadPalettesIndex = () => {
        try {
            let palettes = {};
            let labels = { ...THEME_LABELS };
            if (themeDb) {
                const themesData = themeDb.readThemesDb(projectPath());
                if (themesData.themes && Object.keys(themesData.themes).length) {
                    palettes = themeDb.themesToPalettesMap(themesData);
                    labels = { ...labels, ...themeDb.buildThemeLabels(themesData) };
                }
            }
            if (!Object.keys(palettes).length) {
                const data = JSON.parse(fs.readFileSync(palettesIndexPath(), 'utf8'));
                palettes = data.themes || {};
            }
            state.palettes = palettes;
            state.themeLabels = labels;
            const sel = this.$.themeSelect;
            if (sel) {
                sel.innerHTML = '';
                const order = ['sanxing', 'silu', 'snow', 'chinese', 'numMan', 'redarmy', 'default'];
                const keys = [...new Set([...order, ...Object.keys(state.palettes)])];
                for (const key of keys) {
                    if (!state.palettes[key]) continue;
                    const opt = document.createElement('option');
                    opt.value = key;
                    const entry = state.palettes[key];
                    opt.textContent = labels[key] || entry.displayName || key;
                    if (key === state.theme) opt.selected = true;
                    sel.appendChild(opt);
                }
            }
            applyThemeTiles();
        } catch (e) {
            setStatus(`主题/调色板加载失败: ${e.message}。请打开「主题控制器」或运行 build_theme_palettes.py`);
        }
    };

    const loadLevel = () => {
        try {
            state.db = readDatabase();
            const id = parseLevelIdInput(this.$.levelId.value, state.db);
            this.$.levelId.value = String(id);
            if (Number.isNaN(id) || id < levelIdUtil.minLevelId()) {
                setStatus(`请输入有效的关卡 ID（≥${levelIdUtil.minLevelId()}）`);
                return;
            }
            const cfg = state.db.levels[String(id)];
            if (!cfg) {
                setStatus(`关卡 ${id} 不存在，可点「新建」创建空关卡`);
                return;
            }
            applyLevelConfig.call(this, id, cfg);
            setStatus(
                `关卡 ${id}：Ground ${Object.keys(state.config.ground).length} 格，Border ${Object.keys(state.config.border).length} 格，地图主题 ${state.config.theme || state.theme}`,
            );
        } catch (e) {
            setStatus(`加载失败: ${e.message}`);
        }
    };

    const navigateLevel = (direction) => {
        try {
            state.db = readDatabase();
            const ids = levelIdUtil.sortedLevelIds(state.db);
            if (!ids.length) {
                setStatus('关卡库为空，请先新建关卡');
                return;
            }
            const cur = state.levelId || parseLevelIdInput(this.$.levelId.value, state.db);
            const nextId = direction === 'prev'
                ? levelIdUtil.prevLevelIdInDb(state.db, cur)
                : levelIdUtil.nextLevelIdInDb(state.db, cur);
            if (nextId === cur && ids.length === 1) {
                setStatus(`仅有关卡 ${cur}，无法切换`);
                return;
            }
            this.$.levelId.value = String(nextId);
            loadLevel.call(this);
        } catch (e) {
            setStatus(`切换关卡失败: ${e.message}`);
        }
    };

    const reloadFromDisk = (levelId, hint) => {
        try {
            state.db = readDatabase();
            const cfg = state.db.levels[String(levelId)];
            if (!cfg) return false;
            if (state.levelId !== levelId) return false;
            applyLevelConfig.call(this, levelId, cfg);
            setStatus(
                hint ||
                    `关卡 ${levelId} 已从预制体同步 · Ground ${Object.keys(state.config.ground).length} · Border ${Object.keys(state.config.border).length}`,
            );
            return true;
        } catch (e) {
            console.warn('[level-map-editor] reloadFromDisk failed', e);
            return false;
        }
    };

    const importFromPrefab = async () => {
        const id = state.levelId;
        if (!id) return;
        try {
            const result = prefabSync.importLevelFromPrefab(id);
            if (!result || !result.ok) {
                setStatus(`从预制体同步失败: ${(result && result.reason) || '未知错误'}`);
                return;
            }
            await prefabSync.refreshDatabaseAsset();
            reloadFromDisk(id, `关卡 ${id} 已从 Level${id}.prefab 回写 JSON 并刷新`);
        } catch (e) {
            setStatus(`从预制体同步失败: ${e.message}`);
        }
    };

    const refreshSavedAssets = async () => {
        const assetUrl = `db://${DB_REL}`;
        try {
            await Editor.Message.request('asset-db', 'refresh-asset', assetUrl);
            const uuid = await Editor.Message.request('asset-db', 'query-uuid', assetUrl);
            if (uuid) await Editor.Message.request('asset-db', 'refresh-asset', uuid);
        } catch (e) {
            console.warn('[level-map-editor] asset refresh failed', e);
        }
    };

    const persistMapChanges = (options = {}) => {
        if (!state.config || !state.db) return false;
        try {
            state.config.theme = state.theme;
            state.config = levelIdUtil.syncLevelEntry(state.config, state.levelId);
            state.db.levels[String(state.levelId)] = state.config;
            writeDatabase(state.db);
        } catch (e) {
            console.error('[level-map-editor] persist failed', e);
            setStatus(`保存失败: ${e.message}`);
            return false;
        }
        void refreshSavedAssets();
        if (!options.quiet) {
            const hint = spawnTools.validationHint(state);
            setStatus(
                hint
                    ? `关卡 ${state.levelId} 已保存 · ${hint}`
                    : `关卡 ${state.levelId} 已自动保存（主题 ${state.theme}）`,
            );
        }
        console.log(`[level-map-editor] saved level ${state.levelId} → ${dbPath()}`);
        if (!options.skipBake) scheduleAutoBake();
        return true;
    };

    const AUTO_BAKE_DELAY_MS = 1200;

    const scheduleAutoBake = () => {
        if (state._autoBakeTimer) clearTimeout(state._autoBakeTimer);
        state._autoBakeTimer = setTimeout(() => {
            state._autoBakeTimer = null;
            runBake.call(this, { auto: true });
        }, AUTO_BAKE_DELAY_MS);
    };

    const cancelAutoBake = () => {
        if (state._autoBakeTimer) {
            clearTimeout(state._autoBakeTimer);
            state._autoBakeTimer = null;
        }
    };

    const applyLevelConfig = (id, cfg) => {
        state.levelId = id;
        if (this.$.levelId) this.$.levelId.value = String(id);
        state.config = levelIdUtil.syncLevelEntry(JSON.parse(JSON.stringify(cfg)), id);
        state.selectedSpawn = null;
        state.config.ground = normalizeGround(state.config.ground || {});
        state.config.border = normalizeBorder(state.config.border);
        state.config.spawns = Array.isArray(cfg.spawns) ? JSON.parse(JSON.stringify(cfg.spawns)) : [];
        spawnTools.ensureSpawns(state);
        if (cfg.theme && state.palettes[cfg.theme]) {
            state.theme = cfg.theme;
            state.config.theme = cfg.theme;
            if (this.$.themeSelect) this.$.themeSelect.value = state.theme;
            applyThemeTiles();
        } else if (!state.config.theme) {
            state.config.theme = state.theme;
        } else {
            state.theme = state.config.theme;
            if (this.$.themeSelect) this.$.themeSelect.value = state.theme;
            applyThemeTiles();
        }
        cancelAutoBake();
        computeBaseOffset(state, this.$.canvas?.width || 720, this.$.canvas?.height || 520);
        fitViewToGrid(state, this.$.canvas);
        drawGrid.call(this, state);
        syncSpawnDirUi();
        renderPalette.call(this, state);
    };

    const createNewLevel = () => {
        try {
            state.db = readDatabase();
            let id = parseLevelIdInput(this.$.levelId.value, state.db);
            if (state.db.levels[String(id)]) {
                id = levelIdUtil.nextAvailableLevelId(state.db);
            }
            this.$.levelId.value = String(id);
            const theme = this.$.themeSelect?.value || state.theme || 'sanxing';
            state.theme = theme;
            const cfg = emptyLevelConfig(id, theme);
            state.db.levels[String(id)] = cfg;
            writeDatabase(state.db);
            applyLevelConfig.call(this, id, cfg);
            void refreshSavedAssets();
            runBake.call(this, { auto: true });
            setStatus(
                `已新建关卡 ${id} → JSON / Level${id}.prefab / index 已同步（主题 ${theme}）`,
            );
            console.log(`[level-map-editor] created level ${id}`);
        } catch (e) {
            setStatus(`新建失败: ${e.message}`);
            console.error('[level-map-editor] create level failed', e);
        }
    };

    const runBake = (options = {}) => {
        if (!state.config) return;
        persistMapChanges({ quiet: true, skipBake: true });
        try {
            bakeIgnore.markBakePending(state.levelId);
            const outDir = prefabSync.resolvePrefabDirRel(projectPath());
            execSync(
                `python3 tools/bake_cocos_level_prefabs.py --db ${DB_REL} --out-dir ${outDir} --level-id ${state.levelId} --theme ${state.theme}`,
                { cwd: projectPath(), stdio: 'pipe' },
            );
            const prefabUrl = prefabSync.prefabDbUrl(state.levelId, projectPath());
            try {
                Editor.Message.request('asset-db', 'refresh-asset', prefabUrl);
            } catch (e) {
                console.warn('[level-map-editor] prefab refresh failed', e);
            }
            if (options.auto) {
                setStatus(`关卡 ${state.levelId} 已自动保存并烘焙 → Level${state.levelId}.prefab`);
            } else {
                const outDir = prefabSync.resolvePrefabDirRel(projectPath());
                setStatus(`关卡 ${state.levelId} 已保存地图 → JSON + ${outDir}/Level${state.levelId}.prefab`);
            }
            console.log(`[level-map-editor] baked Level${state.levelId}.prefab`);
        } catch (e) {
            const err = e.stderr ? e.stderr.toString() : e.message;
            if (options.auto) {
                setStatus(`关卡 ${state.levelId} 已保存 JSON，自动烘焙失败: ${err}`);
            } else {
                setStatus(`烘焙失败: ${err}`);
            }
        }
    };

    /** 保存 JSON + 烘焙预制体（运行时分包 bundle-level-prefabs） */
    const saveMapLevel = () => {
        cancelAutoBake();
        if (!state.config || !state.db) {
            setStatus('请先加载或新建关卡');
            return;
        }
        if (!persistMapChanges({ quiet: true, skipBake: true })) return;
        runBake.call(this, { auto: false });
    };

    /** 绘制时暂存 JSON，延迟自动烘焙 */
    const scheduleMapPersist = () => {
        persistMapChanges();
    };

    const openLevelPrefab = async () => {
        const id = state.levelId;
        const uuid = await Editor.Message.request('asset-db', 'query-uuid', prefabSync.prefabDbUrl(id, projectPath()));
        if (uuid) {
            await Editor.Message.request('asset-db', 'open-asset', uuid);
            setStatus(`已打开 Level${id}.prefab（Ground / Border 子节点，贴图主题 ${state.theme}）`);
        } else {
            setStatus(`未找到 Level${id}.prefab，请先烘焙`);
        }
    };

    const toolButtons = {
        paint: this.$.toolPaint,
        box: this.$.toolBox,
        picker: this.$.toolPicker,
        eraser: this.$.toolEraser,
        fill: this.$.toolFill,
    };

    const spawnToolButtons = {
        player: this.$.spawnPlayer,
        vehicle: this.$.spawnVehicle,
        prop: this.$.spawnProp,
        prop_ground: this.$.spawnPropGround,
        erase: this.$.spawnErase,
    };

    const syncSpawnDirUi = () => {
        const needDir = state.spawnTool === 'player' || state.spawnTool === 'vehicle';
        if (this.$.spawnDirWrap) {
            this.$.spawnDirWrap.style.display = needDir ? 'flex' : 'none';
        }
        if (this.$.spawnScaleWrap) {
            this.$.spawnScaleWrap.style.display = state.spawnTool === 'erase' ? 'none' : 'flex';
        }
        const toolScale = state.placementScale[state.spawnTool] ?? 1;
        if (this.$.spawnPlacementScale && document.activeElement !== this.$.spawnPlacementScale) {
            this.$.spawnPlacementScale.value = String(toolScale);
        }
        const vehicle = spawnTools.getVehicleSpawn(state);
        if (this.$.spawnVehicle) {
            this.$.spawnVehicle.textContent = vehicle ? '载具 1/1' : '载具 0/1';
        }
    };

    const syncEditModeUi = () => {
        const mapMode = state.editMode === 'map';
        if (this.$.modeMap) this.$.modeMap.classList.toggle('active', mapMode);
        if (this.$.modeSpawns) this.$.modeSpawns.classList.toggle('active', !mapMode);
        if (this.$.spawnToolsWrap) this.$.spawnToolsWrap.style.display = mapMode ? 'none' : 'flex';
        const mapOnly = this.$.layerGround?.parentElement;
        if (this.$.layerGround) this.$.layerGround.style.display = mapMode ? '' : 'none';
        if (this.$.layerBorder) this.$.layerBorder.style.display = mapMode ? '' : 'none';
        const mapTools = this.$.toolPaint?.parentElement;
        if (mapTools) mapTools.style.display = mapMode ? '' : 'none';
        renderPalette.call(this, state);
        syncSpawnDirUi();
        if (this.$.canvas) {
            this.$.canvas.style.cursor = mapMode
                ? (tools.TOOL_CURSORS[state.tool] || 'crosshair')
                : 'pointer';
        }
    };

    const setEditMode = (mode) => {
        state.editMode = mode;
        state.painting = false;
        state.boxStart = null;
        state.boxEnd = null;
        syncEditModeUi();
        if (state.config) {
            drawGrid.call(this, state);
        }
        if (mode === 'spawns') {
            const hint = spawnTools.validationHint(state);
            setStatus(
                `实体配置 · ${spawnTools.spawnSummary(state)}${hint ? ` · ${hint}` : ''}`,
            );
        } else {
            setStatus(tools.TOOL_LABELS[state.tool] || state.tool);
        }
    };

    const setSpawnTool = (toolId) => {
        state.spawnTool = toolId;
        for (const [id, btn] of Object.entries(spawnToolButtons)) {
            if (btn) btn.classList.toggle('active', id === toolId);
        }
        syncSpawnDirUi();
        const labels = {
            player: '放置玩家（每关仅 1 个，含 x/y/朝向）',
            vehicle: '放置载具（0 或 1 个；再次点击原格可清除）',
            prop: '放置可拾取物·砖块上（Unity Prop）',
            prop_ground: '放置可拾取物·空地（Unity nProp，偏低）',
            erase: '删除该格上所有实体',
        };
        setStatus(`实体 · ${labels[toolId] || toolId} · ${spawnTools.spawnSummary(state)}`);
    };

    const readPlacementScale = (st) => {
        const raw = this.$.spawnPlacementScale?.value;
        const n = spawnDefaults.clampScale(raw !== undefined && raw !== '' ? raw : st.placementScale[st.spawnTool]);
        st.placementScale[st.spawnTool] = n;
        return n;
    };

    const applyEntityTextures = () => {
        if (!state.config) return false;
        entityTexPresets.ensureEntityTextures(state);
        const read = entityTexPresets.readEntityTexturesFromPanel(this.$.paletteList);
        state.config.entityTextures = read;
        entityTexPresets.pruneEmptyEntityTextures(state);
        scheduleMapPersist();
        renderPalette.call(this, state);
        setStatus('关卡实体贴图已保存（entityTextures）');
        return true;
    };

    const applySpawnInspector = () => {
        if (!state.config || !state.selectedSpawn || !spawnTools.spawnStillExists(state, state.selectedSpawn)) {
            state.selectedSpawn = null;
            renderPalette.call(this, state);
            return false;
        }
        const sel = state.selectedSpawn;
        const xEl = this.$.paletteList?.querySelector('#spawn-inspect-x');
        const yEl = this.$.paletteList?.querySelector('#spawn-inspect-y');
        const dirEl = this.$.paletteList?.querySelector('#spawn-inspect-dir');
        const scaleEl = this.$.paletteList?.querySelector('#spawn-inspect-scale');
        const texEl = this.$.paletteList?.querySelector('#spawn-inspect-texture');
        const patch = {
            x: xEl ? parseInt(xEl.value, 10) : sel.x,
            y: yEl ? parseInt(yEl.value, 10) : sel.y,
            scale: scaleEl ? spawnDefaults.clampScale(scaleEl.value) : spawnTools.formatSpawnScale(sel),
            texture: texEl ? texEl.value : sel.texture,
        };
        if (sel.kind === 'player' && dirEl) patch.playerDirection = dirEl.value;
        if (sel.kind === 'vehicle' && dirEl) patch.vehicleDirection = dirEl.value;
        if (texEl) {
            const t = entityTexPresets.normalizeTexturePath(texEl.value);
            patch.texture = t || '';
        }
        if (Number.isNaN(patch.x) || Number.isNaN(patch.y)) {
            setStatus('坐标必须为整数');
            return false;
        }
        spawnTools.updateSpawnEntry(state, sel, patch);
        scheduleMapPersist();
        drawGrid.call(this, state);
        renderPalette.call(this, state);
        setStatus(`已更新 ${spawnTools.SPAWN_KIND_LABELS[sel.kind] || sel.kind} · ${spawnTools.spawnSummary(state)}`);
        return true;
    };

    const selectSpawnAtCell = (cell, options = {}) => {
        if (!cell || !state.config) return false;
        const prefer = options.preferKind || state.spawnTool;
        const hit = spawnTools.pickSpawnAtCell(state, cell.x, cell.y, prefer === 'erase' ? null : prefer);
        if (!hit) return false;
        state.selectedSpawn = hit;
        if (hit.kind === 'player' || hit.kind === 'vehicle') {
            const d = hit.playerDirection || hit.vehicleDirection;
            if (d) {
                state.spawnDirection = d;
                if (this.$.spawnDirection) this.$.spawnDirection.value = d;
            }
        }
        const sc = spawnTools.formatSpawnScale(hit);
        state.placementScale[hit.kind] = sc;
        if (this.$.spawnPlacementScale) this.$.spawnPlacementScale.value = String(sc);
        renderPalette.call(this, state);
        drawGrid.call(this, state);
        return true;
    };

    const applySpawnAt = (cell) => {
        if (!state.config || !cell || !isInGrid(cell)) return false;
        const dir = this.$.spawnDirection?.value || state.spawnDirection || 'Direction.South';
        state.spawnDirection = dir;
        const scale = readPlacementScale(state);
        let changed = false;
        let affectedSpawn = null;
        if (state.spawnTool === 'player') {
            affectedSpawn = spawnTools.setPlayerSpawn(state, cell.x, cell.y, dir, scale);
            changed = true;
        } else if (state.spawnTool === 'vehicle') {
            const r = spawnTools.toggleVehicleSpawnDetailed(state, cell.x, cell.y, dir, scale);
            changed = r.changed;
            affectedSpawn = r.spawn;
            if (r.removed) state.selectedSpawn = null;
        } else if (state.spawnTool === 'prop') {
            const r = spawnTools.togglePropSpawn(state, cell.x, cell.y, scale, 'block');
            changed = r.changed;
            affectedSpawn = r.spawn;
            if (r.removed && state.selectedSpawn?.x === cell.x && state.selectedSpawn?.y === cell.y) {
                state.selectedSpawn = null;
            }
        } else if (state.spawnTool === 'prop_ground') {
            const r = spawnTools.togglePropSpawn(state, cell.x, cell.y, scale, 'ground');
            changed = r.changed;
            affectedSpawn = r.spawn;
            if (r.removed && state.selectedSpawn?.x === cell.x && state.selectedSpawn?.y === cell.y) {
                state.selectedSpawn = null;
            }
        } else if (state.spawnTool === 'erase') {
            changed = spawnTools.removeSpawnAt(state, cell.x, cell.y);
            if (changed) state.selectedSpawn = null;
        }
        if (affectedSpawn) state.selectedSpawn = affectedSpawn;
        if (changed) {
            scheduleMapPersist();
            syncSpawnDirUi();
            renderPalette.call(this, state);
            const hint = spawnTools.validationHint(state);
            setStatus(`实体已更新：${spawnTools.spawnSummary(state)}${hint ? ` · ${hint}` : ''}`);
        }
        return changed;
    };

    const setActiveTool = (toolId) => {
        if (state.editMode !== 'map') return;
        state.tool = toolId;
        state.boxStart = null;
        state.boxEnd = null;
        state.lastPaintKey = null;
        for (const [id, btn] of Object.entries(toolButtons)) {
            if (btn) btn.classList.toggle('active', id === toolId);
        }
        if (this.$.canvas) {
            this.$.canvas.style.cursor = tools.TOOL_CURSORS[toolId] || 'default';
        }
        renderPalette.call(this, state);
        setStatus(tools.TOOL_LABELS[toolId] || toolId);
    };

    const setEditLayer = (layer) => {
        state.editLayer = layer;
        this.$.layerGround.classList.toggle('active', layer === 'ground');
        this.$.layerBorder.classList.toggle('active', layer === 'border');
        drawGrid.call(this, state);
        renderPalette.call(this, state);
        const count = Object.keys(tools.layerMap(state)).length;
        setStatus(
            `绘制层：${layer === 'ground' ? 'Ground' : 'Border'}（${count} 格已高亮）— ${tools.TOOL_LABELS[state.tool]}`,
        );
    };

    const selectBrush = (tile) => {
        state.brush = tile;
        if (state.tool === 'eraser') setActiveTool('paint');
        else renderPalette.call(this, state);
        state.editLayer = tile.layer;
        this.$.layerGround.classList.toggle('active', tile.layer === 'ground');
        this.$.layerBorder.classList.toggle('active', tile.layer === 'border');
        drawGrid.call(this, state);
    };

    const applyToolAt = (cell, options = {}) => {
        if (!state.config || !cell || !isInGrid(cell)) return false;
        const { x, y, key } = cell;
        const force = options.force === true;

        switch (state.tool) {
            case 'paint': {
                if (!tools.canPaintBrush(state)) {
                    if (!force) setStatus('请先在右侧调色板选择瓦片，或按吸管吸取');
                    return false;
                }
                if (!force && state.lastPaintKey === key) return false;
                tools.setCell(state, key, state.brush.tileKey);
                state.lastPaintKey = key;
                scheduleMapPersist();
                return true;
            }
            case 'eraser': {
                if (!force && state.lastPaintKey === key) return false;
                if (tools.hasCell(state, key)) {
                    tools.removeCell(state, key);
                    state.lastPaintKey = key;
                    scheduleMapPersist();
                    return true;
                }
                return false;
            }
            case 'picker': {
                let tileKey = tools.getCell(state, key);
                if (tileKey === undefined) {
                    setStatus(`(${x},${y}) 当前层为空，无法吸取`);
                    return false;
                }
                if (tileKey === true) tileKey = 'WallBlock';
                const tile = tools.findPaletteTile(state, tileKey, state.editLayer);
                if (!tile) {
                    setStatus(`(${x},${y}) 瓦片 ${tileKey} 不在当前主题调色板中`);
                    return false;
                }
                selectBrush(tile);
                setActiveTool('paint');
                setStatus(`已吸取 ${tile.display}，切换为画笔`);
                return true;
            }
            case 'fill': {
                if (!tools.canPaintBrush(state)) {
                    setStatus('填充前请先在右侧选择瓦片');
                    return false;
                }
                const n = tools.floodFill(state, key, state.brush.tileKey);
                setStatus(`油漆桶：已填充 ${n} 格`);
                if (n > 0) scheduleMapPersist();
                return n > 0;
            }
            default:
                return false;
        }
    };

    const finishBoxSelect = () => {
        if (!state.boxStart || !state.boxEnd) return;
        const x0 = Math.max(GRID_MIN, Math.min(state.boxStart.x, state.boxEnd.x));
        const y0 = Math.max(GRID_MIN, Math.min(state.boxStart.y, state.boxEnd.y));
        const x1 = Math.min(GRID_MAX, Math.max(state.boxStart.x, state.boxEnd.x));
        const y1 = Math.min(GRID_MAX, Math.max(state.boxStart.y, state.boxEnd.y));
        let n = 0;
        if (state.tool === 'box') {
            if (!tools.canPaintBrush(state)) {
                setStatus('框选填充前请先在右侧选择瓦片');
            } else {
                n = tools.fillRectangle(state, x0, y0, x1, y1, state.brush.tileKey);
                setStatus(`框选：已填充 ${n} 格（${state.brush.display}）`);
                if (n > 0) scheduleMapPersist();
            }
        }
        state.boxStart = null;
        state.boxEnd = null;
        state.lastPaintKey = null;
        drawGrid.call(this, state);
    };

    bindOnce(this.$.btnLoad, 'click', () => loadLevel.call(this));
    bindOnce(this.$.btnPrev, 'click', () => navigateLevel.call(this, 'prev'));
    bindOnce(this.$.btnNext, 'click', () => navigateLevel.call(this, 'next'));
    bindOnce(this.$.btnThemeCtrl, 'click', () => {
        try {
            Editor.Panel.open('theme-controller');
        } catch (e) {
            setStatus('请先在扩展管理器中启用 theme-controller 扩展');
        }
    });
    bindOnce(this.$.btnNew, 'click', () => createNewLevel.call(this));
    bindOnce(this.$.btnSave, 'click', () => saveMapLevel.call(this));
    bindOnce(this.$.btnImportPrefab, 'click', () => importFromPrefab.call(this));
    bindOnce(this.$.btnOpenPrefab, 'click', () => openLevelPrefab.call(this));

    const snapSceneSelection = async () => {
        let uuids = [];
        try {
            uuids = Editor.Selection.getSelected('node') || [];
        } catch (e) {
            setStatus('无法读取场景选中节点');
            return;
        }
        if (!uuids.length) {
            setStatus('请先在场景编辑器选中 Ground/Border 下的瓦片节点');
            return;
        }
        let ok = 0;
        for (const uuid of uuids) {
            try {
                const res = await Editor.Message.request('scene', 'execute-scene-script', {
                    name: 'level-map-editor',
                    method: 'snapNodeByUuid',
                    args: [uuid],
                });
                if (res && res.ok) ok++;
            } catch (e) {
                console.warn('[level-map-editor] snap failed', uuid, e);
            }
        }
        setStatus(`场景吸附完成：${ok}/${uuids.length} 个节点已对齐格子`);
    };

    const attachGridSnapHelper = async () => {
        let uuids = Editor.Selection.getSelected('node') || [];
        if (!uuids.length) {
            const prefabUuid = await Editor.Message.request(
                'asset-db',
                'query-uuid',
                prefabSync.prefabDbUrl(state.levelId, projectPath()),
            );
            if (prefabUuid) uuids = [prefabUuid];
        }
        if (!uuids.length) {
            setStatus('请选中 Level 预制体根节点，或先打开 Level 预制体');
            return;
        }
        try {
            await Editor.Message.request('scene', 'execute-scene-script', {
                name: 'level-map-editor',
                method: 'attachHelperOnSelection',
                args: [uuids[0]],
            });
            setStatus('已在 Level 根节点添加 GridSnapHelper（拖动瓦片将自动吸附）');
        } catch (e) {
            setStatus(`添加吸附助手失败: ${e.message}。也可手动添加组件 GridSnapHelper 到 Level 根节点`);
        }
    };

    bindOnce(this.$.btnSceneSnap, 'click', () => snapSceneSelection.call(this));
    bindOnce(this.$.btnAttachHelper, 'click', () => attachGridSnapHelper.call(this));

    bindOnce(this.$.themeSelect, 'change', () => {
        state.theme = this.$.themeSelect.value || 'silu';
        if (state.config) state.config.theme = state.theme;
        applyThemeTiles();
        drawGrid.call(this, state);
        if (state.config) scheduleMapPersist();
        setStatus(`关卡地图主题：${THEME_LABELS[state.theme] || state.theme}（修改后自动保存）`);
    });

    bindOnce(this.$.layerGround, 'click', () => setEditLayer.call(this, 'ground'));
    bindOnce(this.$.layerBorder, 'click', () => setEditLayer.call(this, 'border'));
    bindOnce(this.$.modeMap, 'click', () => setEditMode.call(this, 'map'));
    bindOnce(this.$.modeSpawns, 'click', () => setEditMode.call(this, 'spawns'));
    bindOnce(this.$.spawnPlayer, 'click', () => setSpawnTool.call(this, 'player'));
    bindOnce(this.$.spawnVehicle, 'click', () => setSpawnTool.call(this, 'vehicle'));
    bindOnce(this.$.spawnProp, 'click', () => setSpawnTool.call(this, 'prop'));
    bindOnce(this.$.spawnPropGround, 'click', () => setSpawnTool.call(this, 'prop_ground'));
    bindOnce(this.$.spawnErase, 'click', () => setSpawnTool.call(this, 'erase'));
    bindOnce(this.$.spawnClearVehicle, 'click', () => {
        if (spawnTools.clearVehicleSpawn(state)) {
            scheduleMapPersist();
            syncSpawnDirUi();
            drawGrid.call(this, state);
            setStatus(`已清除载具 · ${spawnTools.spawnSummary(state)}`);
        } else {
            setStatus('当前关卡无载具');
        }
    });
    bindOnce(this.$.spawnDirection, 'change', () => {
        state.spawnDirection = this.$.spawnDirection.value || 'Direction.South';
    });
    bindOnce(this.$.spawnPlacementScale, 'change', () => {
        const n = spawnDefaults.clampScale(this.$.spawnPlacementScale?.value);
        state.placementScale[state.spawnTool] = n;
        if (this.$.spawnPlacementScale) this.$.spawnPlacementScale.value = String(n);
    });
    bindOnce(this.$.toolPaint, 'click', () => setActiveTool('paint'));
    bindOnce(this.$.toolBox, 'click', () => setActiveTool('box'));
    bindOnce(this.$.toolPicker, 'click', () => setActiveTool('picker'));
    bindOnce(this.$.toolEraser, 'click', () => setActiveTool('eraser'));
    bindOnce(this.$.toolFill, 'click', () => setActiveTool('fill'));

    bindOnce(this.$.canvas, 'mousedown', (ev) => {
        if (!state.config) return;
        if (ev.button === 2) {
            ev.preventDefault();
            state.rmbDown = true;
            state.panActive = false;
            state.panLast = { x: ev.clientX, y: ev.clientY };
            if (state.rmbTimer) clearTimeout(state.rmbTimer);
            state.rmbTimer = setTimeout(() => {
                if (state.rmbDown) {
                    state.panActive = true;
                    if (this.$.canvas) this.$.canvas.style.cursor = 'grabbing';
                }
            }, RMB_HOLD_MS);
            return;
        }
        if (ev.button !== 0) return;
        state.painting = true;
        state.lastPaintKey = null;
        const cell = cellFromEvent.call(this, ev, state);
        if (!cell || !isInGrid(cell)) return;
        if (state.editMode === 'spawns') {
            if (ev.shiftKey && selectSpawnAtCell.call(this, cell)) {
                state.painting = false;
                return;
            }
            if (applySpawnAt.call(this, cell)) drawGrid.call(this, state);
            state.painting = false;
            return;
        }
        if (state.tool === 'box') {
            state.boxStart = cell;
            state.boxEnd = cell;
            drawGrid.call(this, state);
            return;
        }
        if (applyToolAt(cell, { force: true })) drawGrid.call(this, state);
    });
    bindOnce(this.$.canvas, 'mousemove', (ev) => {
        if (state.rmbDown && state.panActive && state.panLast) {
            const rect = this.$.canvas.getBoundingClientRect();
            const scaleX = this.$.canvas.width / rect.width;
            const scaleY = this.$.canvas.height / rect.height;
            state.panX += (ev.clientX - state.panLast.x) * scaleX;
            state.panY += (ev.clientY - state.panLast.y) * scaleY;
            state.panLast = { x: ev.clientX, y: ev.clientY };
            drawGrid.call(this, state);
            return;
        }
        if (!state.config) return;
        const c = cellFromEvent.call(this, ev, state);
        const changed = !state.hoverCell || !c || state.hoverCell.key !== c.key;
        state.hoverCell = c;
        if (state.painting && state.tool === 'box' && state.boxStart && c && isInGrid(c)) {
            state.boxEnd = c;
            drawGrid.call(this, state);
            return;
        }
        if (changed) drawGrid.call(this, state);
        if (!state.painting && this.$.status && c) {
            const modeTag = state.editMode === 'spawns'
                ? '实体'
                : (state.editLayer === 'ground' ? 'Ground' : 'Border');
            const bounds = isInGrid(c) ? '' : ' [超出 60×60]';
            const contents = isInGrid(c)
                ? tools.describeCellContents(
                    state,
                    c.x,
                    c.y,
                    spawnTools.spawnsAt(state, c.x, c.y),
                    spawnTools.SPAWN_KIND_LABELS,
                )
                : '空';
            const hint = state.editMode === 'spawns'
                ? spawnTools.validationHint(state)
                : tools.TOOL_LABELS[state.tool];
            const summary = state.editMode === 'spawns' ? ` · ${spawnTools.spawnSummary(state)}` : '';
            this.$.status.textContent =
                `[${modeTag}] 格子 (${c.x},${c.y}) ${contents}${bounds}`
                + ` · 缩放 ${Math.round(state.zoom * 100)}%`
                + `${summary}${hint ? ` — ${hint}` : ''}`;
        }
        if (state.painting && state.editMode === 'map' && (state.tool === 'paint' || state.tool === 'eraser') && c && isInGrid(c)) {
            if (applyToolAt(c)) drawGrid.call(this, state);
        }
    });
    const endPan = () => {
        if (state.rmbTimer) {
            clearTimeout(state.rmbTimer);
            state.rmbTimer = null;
        }
        state.rmbDown = false;
        state.panActive = false;
        state.panLast = null;
        if (this.$.canvas) {
            this.$.canvas.style.cursor = tools.TOOL_CURSORS[state.tool] || 'crosshair';
        }
    };
    bindOnce(this.$.canvas, 'mouseup', (ev) => {
        if (ev.button === 2) {
            endPan.call(this);
            return;
        }
        if (state.painting && state.tool === 'box') finishBoxSelect.call(this);
        state.painting = false;
        state.lastPaintKey = null;
    });
    bindOnce(this.$.canvas, 'mouseleave', () => {
        if (state.painting && state.tool === 'box') finishBoxSelect.call(this);
        state.painting = false;
        state.lastPaintKey = null;
        endPan.call(this);
    });
    bindOnce(this.$.canvas, 'contextmenu', (ev) => ev.preventDefault());
    const onWindowMouseUp = (ev) => {
        if (ev.button === 2 && state.rmbDown) endPan.call(this);
    };
    window.addEventListener('mouseup', onWindowMouseUp);
    this._lmeWindowMouseUp = onWindowMouseUp;
    bindOnceOpts(this.$.canvas, 'wheel', (ev) => {
        ev.preventDefault();
        if (!state.config || !this.$.canvas) return;
        const canvas = this.$.canvas;
        const rect = canvas.getBoundingClientRect();
        const scaleX = canvas.width / rect.width;
        const scaleY = canvas.height / rect.height;
        const mx = (ev.clientX - rect.left) * scaleX;
        const my = (ev.clientY - rect.top) * scaleY;
        const logical = screenToLogical(mx, my, state, canvas);
        const factor = ev.deltaY > 0 ? 0.96 : 1.04;
        const oldZoom = state.zoom;
        const newZoom = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, oldZoom * factor));
        if (newZoom === oldZoom) return;
        const cx = canvas.width / 2;
        const cy = canvas.height / 2;
        state.zoom = newZoom;
        state.panX = mx - cx - (logical.x - cx) * newZoom;
        state.panY = my - cy - (logical.y - cy) * newZoom;
        drawGrid.call(this, state);
    }, { passive: false });

    setActiveTool('paint');
    syncEditModeUi();
    setSpawnTool('player');

    if (this.$.spawnDirection) {
        this.$.spawnDirection.innerHTML = '';
        const dirLabels = {
            'Direction.North': '北 North',
            'Direction.East': '东 East',
            'Direction.South': '南 South',
            'Direction.West': '西 West',
        };
        for (const d of spawnTools.DIRECTIONS) {
            const opt = document.createElement('option');
            opt.value = d;
            opt.textContent = dirLabels[d] || d;
            if (d === state.spawnDirection) opt.selected = true;
            this.$.spawnDirection.appendChild(opt);
        }
    }

    const onKeyDown = (ev) => {
        if (ev.repeat) return;
        const tag = (ev.target && ev.target.tagName) || '';
        if (tag === 'INPUT' || tag === 'TEXTAREA') return;
        const shortcuts = {
            b: 'paint',
            e: 'eraser',
            i: 'picker',
            f: 'fill',
            r: 'box',
        };
        const t = shortcuts[ev.key && ev.key.toLowerCase()];
        if (t) {
            ev.preventDefault();
            setActiveTool(t);
        }
    };
    window.addEventListener('keydown', onKeyDown);
    this._lmeKeyHandler = onKeyDown;

    state._lastDraw = () => drawGrid.call(this, state);

    const canvasWrap = this.$.canvas?.parentElement;
    const resizeAndDraw = () => {
        if (resizeCanvas(this.$.canvas, canvasWrap)) {
            computeBaseOffset(state, this.$.canvas.width, this.$.canvas.height);
            if (!state._viewFitted) fitViewToGrid(state, this.$.canvas);
        }
        drawGrid.call(this, state);
    };
    if (canvasWrap && typeof ResizeObserver !== 'undefined') {
        const ro = new ResizeObserver(() => resizeAndDraw());
        ro.observe(canvasWrap);
        this._lmeResizeObserver = ro;
    }
    resizeAndDraw();

    loadPalettesIndex.call(this);
    try {
        state.db = readDatabase();
        const ids = Object.keys(state.db.levels || {})
            .map((k) => parseInt(k, 10))
            .filter((n) => !Number.isNaN(n))
            .sort((a, b) => a - b);
        const openId = ids.length ? ids[ids.length - 1] : levelIdUtil.minLevelId();
        if (this.$.levelId) this.$.levelId.value = String(openId);
    } catch (e) {
        console.warn('[level-map-editor] init db', e);
    }
    loadLevel.call(this);
    state._viewFitted = true;
    this._lmeState = state;
    this._lmeApplySpawnInspector = applySpawnInspector;
    this._lmeApplyEntityTextures = applyEntityTextures;
    this._lmePersistMap = () => scheduleMapPersist();
    this._lmeReloadFromDisk = reloadFromDisk;

    const onPrefabSynced = (payload) => {
        const levelId = payload && typeof payload === 'object' ? payload.levelId : payload;
        if (!levelId) return;
        reloadFromDisk(Number(levelId));
    };
    const addListener = Editor.Message.__protected__?.addBroadcastListener || Editor.Message.addBroadcastListener;
    if (typeof addListener === 'function') {
        addListener.call(Editor.Message, 'level-map-editor:prefab-synced', onPrefabSynced);
        this._lmePrefabSyncListener = onPrefabSynced;
    }
};

const MAP_GRID_SIZE = 60;
const GRID_MIN = -Math.floor(MAP_GRID_SIZE / 2);
const GRID_MAX = GRID_MIN + MAP_GRID_SIZE - 1;
const RMB_HOLD_MS = 200;
const ZOOM_MIN = 0.06;
const ZOOM_MAX = 4;
const INITIAL_VIEW_ZOOM = 0.5;

function isInGrid(cell) {
    if (!cell) return false;
    return cell.x >= GRID_MIN && cell.x <= GRID_MAX && cell.y >= GRID_MIN && cell.y <= GRID_MAX;
}

function resizeCanvas(canvas, wrap) {
    if (!canvas || !wrap) return false;
    const w = Math.max(320, wrap.clientWidth || 720);
    const h = Math.max(240, wrap.clientHeight || 520);
    if (canvas.width === w && canvas.height === h) return false;
    canvas.width = w;
    canvas.height = h;
    return true;
}

function computeBaseOffset(state, canvasW, canvasH) {
    const halfH = grid.PANEL_HALF_H;
    state.baseOffsetX = canvasW / 2;
    state.baseOffsetY = canvasH / 2 + halfH;
    state.offsetX = state.baseOffsetX;
    state.offsetY = state.baseOffsetY;
}

function fitViewToGrid(state, canvas) {
    if (!canvas || !canvas.width || !canvas.height) return;
    state.zoom = INITIAL_VIEW_ZOOM;
    state.panX = 0;
    state.panY = 0;
    computeBaseOffset(state, canvas.width, canvas.height);
}

function screenToLogical(mx, my, state, canvas) {
    const cx = canvas.width / 2;
    const cy = canvas.height / 2;
    return {
        x: (mx - cx - state.panX) / state.zoom + cx,
        y: (my - cy - state.panY) / state.zoom + cy,
    };
}

function visibleGridRange(state, canvas) {
    const pts = [
        screenToLogical(0, 0, state, canvas),
        screenToLogical(canvas.width, 0, state, canvas),
        screenToLogical(0, canvas.height, state, canvas),
        screenToLogical(canvas.width, canvas.height, state, canvas),
    ];
    let minX = GRID_MAX;
    let maxX = GRID_MIN;
    let minY = GRID_MAX;
    let maxY = GRID_MIN;
    const pad = 3;
    for (const p of pts) {
        const c = grid.cellFromCanvas(p.x, p.y, state.offsetX, state.offsetY);
        minX = Math.min(minX, c.x);
        maxX = Math.max(maxX, c.x);
        minY = Math.min(minY, c.y);
        maxY = Math.max(maxY, c.y);
    }
    return {
        minX: Math.max(GRID_MIN, minX - pad),
        maxX: Math.min(GRID_MAX, maxX + pad),
        minY: Math.max(GRID_MIN, minY - pad),
        maxY: Math.min(GRID_MAX, maxY + pad),
    };
}

/** 等距菱形：cx,cy 为格子中心（与贴图 pivot / Tilemap tileAnchor 一致） */
function traceIsoDiamondCenter(ctx, cx, cy, halfW, halfH) {
    ctx.beginPath();
    ctx.moveTo(cx, cy - halfH);
    ctx.lineTo(cx + halfW, cy);
    ctx.lineTo(cx, cy + halfH);
    ctx.lineTo(cx - halfW, cy);
    ctx.closePath();
}

function drawIsoDiamondCenter(ctx, cx, cy, halfW, halfH, strokeStyle, fillStyle, lineWidth) {
    traceIsoDiamondCenter(ctx, cx, cy, halfW, halfH);
    if (fillStyle) {
        ctx.fillStyle = fillStyle;
        ctx.fill();
    }
    if (strokeStyle) {
        ctx.strokeStyle = strokeStyle;
        ctx.lineWidth = lineWidth || 1;
        ctx.stroke();
    }
}

function drawIsoGrid(ctx, state, canvas) {
    const halfW = grid.PANEL_HALF_W;
    const halfH = grid.PANEL_HALF_H;
    const range = canvas ? visibleGridRange(state, canvas) : {
        minX: GRID_MIN, maxX: GRID_MAX, minY: GRID_MIN, maxY: GRID_MAX,
    };
    for (let x = range.minX; x <= range.maxX; x++) {
        for (let y = range.minY; y <= range.maxY; y++) {
            const { x: cx, y: cy } = cellCenterCanvas(x, y, state.offsetX, state.offsetY);
            const isHover = state.hoverCell && state.hoverCell.x === x && state.hoverCell.y === y;
            const onEdge = x === GRID_MIN || x === GRID_MAX || y === GRID_MIN || y === GRID_MAX;
            drawIsoDiamondCenter(
                ctx, cx, cy, halfW, halfH,
                isHover ? '#4a9eff' : (onEdge ? '#777' : '#555'),
                isHover ? 'rgba(74,158,255,0.2)' : 'rgba(60,60,60,0.25)',
                isHover ? 2 : 1,
            );
            if (isHover) {
                ctx.fillStyle = '#fff';
                ctx.font = '10px sans-serif';
                ctx.textAlign = 'center';
                ctx.fillText(`${x},${y}`, cx, cy + 4);
            }
        }
    }
}

function textureForTile(state, tileKey, layer) {
    const t = state.tiles.find((tile) => tile.tileKey === tileKey);
    if (t) return t.texture;
    const theme = state.theme || 'silu';
    if (layer === 'ground') {
        return `textures/${theme}/${tileKey === 'JumpBlock' ? 'JumpBlock' : 'Baseblock'}`;
    }
    return `textures/${theme}/${tileKey || 'WallBlock'}`;
}

/** 格子中心（Unity pivot 落点） */
function cellCenterCanvas(cx, cy, offsetX, offsetY) {
    return grid.cellCenterCanvas(cx, cy, offsetX, offsetY);
}

function getTileDrawRect(state, cell) {
    const { x: cx, y: cy } = cellCenterCanvas(cell.x, cell.y, state.offsetX, state.offsetY);
    const meta = tileMeta.getTileMeta(cell.tileName, state.config?.theme);
    const texRel = textureForTile(state, cell.tileName, cell.layer || 'ground');
    const img = loadImageCache(state, texRel);
    const w = (img.complete && img.naturalWidth > 0) ? img.naturalWidth : meta.width;
    const h = (img.complete && img.naturalHeight > 0) ? img.naturalHeight : meta.height;
    return tileMeta.spriteDrawRect(cx, cy, w, h, meta, grid.PANEL_HALF_W);
}

function cellHitTestTiles(mx, my, state) {
    if (!state.config) return null;
    const cells = allSortedCells(state);
    for (let i = cells.length - 1; i >= 0; i--) {
        const cell = cells[i];
        const r = getTileDrawRect(state, cell);
        if (mx >= r.x && mx <= r.x + r.w && my >= r.y && my <= r.y + r.h) {
            return { x: cell.x, y: cell.y, key: cell.key };
        }
    }
    return null;
}

function drawIsoCell(ctx, state, x, y, texRel, label) {
    const { x: cx, y: cy } = cellCenterCanvas(x, y, state.offsetX, state.offsetY);
    const meta = tileMeta.getTileMeta(label, state.config?.theme);
    const img = loadImageCache(state, texRel);
    if (img.complete && img.naturalWidth > 0) {
        const rect = tileMeta.spriteDrawRect(
            cx, cy, img.naturalWidth, img.naturalHeight, meta, grid.PANEL_HALF_W,
        );
        ctx.drawImage(img, rect.x, rect.y, rect.w, rect.h);
    } else {
        const halfW = grid.PANEL_HALF_W;
        const halfH = grid.PANEL_HALF_H;
        const { x: cx, y: cy } = cellCenterCanvas(x, y, state.offsetX, state.offsetY);
        drawIsoDiamondCenter(ctx, cx, cy, halfW, halfH, '#888', '#4a4a5a', 1);
        ctx.fillStyle = '#ccc';
        ctx.font = '9px sans-serif';
        ctx.textAlign = 'center';
        ctx.fillText((label || '').slice(0, 6), cx, cy + 3);
    }
}

function isoDrawOrder(a, b) {
    const ka = a.x + a.y;
    const kb = b.x + b.y;
    if (ka !== kb) return kb - ka;
    return b.x - a.x;
}

function sortedCells(mapObj) {
    return Object.entries(mapObj || {})
        .map(([key, tileName]) => {
            const [xs, ys] = key.split(',');
            const cx = parseInt(xs, 10);
            const cy = parseInt(ys, 10);
            return { key, x: cx, y: cy, tileName, sortKey: cx + cy };
        })
        .sort(isoDrawOrder);
}

function allSortedCells(state) {
    const cells = [];
    for (const cell of sortedCells(state.config.ground)) {
        cells.push({ ...cell, layer: 'ground' });
    }
    for (const cell of sortedCells(state.config.border)) {
        const name = typeof cell.tileName === 'string' ? cell.tileName : 'WallBlock';
        cells.push({ ...cell, tileName: name, layer: 'border' });
    }
    cells.sort(isoDrawOrder);
    return cells;
}

const LAYER_HIGHLIGHT = {
    ground: { stroke: '#66bb6a', fill: 'rgba(102,187,106,0.28)', dim: 0.32 },
    border: { stroke: '#ffb74d', fill: 'rgba(255,183,77,0.28)', dim: 0.32 },
};

function cellsForLayer(state, layer) {
    return allSortedCells(state).filter((c) => c.layer === layer);
}

function drawActiveLayerHighlight(ctx, state) {
    const layer = state.editLayer || 'ground';
    const style = LAYER_HIGHLIGHT[layer];
    const halfW = grid.PANEL_HALF_W;
    const halfH = grid.PANEL_HALF_H;
    const map = layer === 'ground' ? state.config.ground : state.config.border;
    ctx.save();
    for (const cell of sortedCells(map)) {
        const { x: cx, y: cy } = cellCenterCanvas(cell.x, cell.y, state.offsetX, state.offsetY);
        drawIsoDiamondCenter(ctx, cx, cy, halfW, halfH, style.stroke, style.fill, 2);
    }
    ctx.restore();
}

function drawGrid(state) {
    const canvas = this.$.canvas;
    const wrap = canvas?.parentElement;
    if (!canvas || !state.config) return;
    resizeCanvas(canvas, wrap);
    computeBaseOffset(state, canvas.width, canvas.height);
    const ctx = canvas.getContext('2d');
    ctx.setTransform(1, 0, 0, 1, 0, 0);
    ctx.fillStyle = '#3a3a3a';
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    const cx = canvas.width / 2;
    const cy = canvas.height / 2;
    ctx.save();
    ctx.translate(cx + state.panX, cy + state.panY);
    ctx.scale(state.zoom, state.zoom);
    ctx.translate(-cx, -cy);

    drawIsoGrid(ctx, state, canvas);

    const active = state.editLayer || 'ground';
    const inactive = active === 'ground' ? 'border' : 'ground';
    const dim = LAYER_HIGHLIGHT[active].dim;

    ctx.save();
    ctx.globalAlpha = dim;
    for (const cell of cellsForLayer(state, inactive)) {
        drawIsoCell(
            ctx, state, cell.x, cell.y,
            textureForTile(state, cell.tileName, cell.layer),
            cell.tileName,
        );
    }
    ctx.restore();

    for (const cell of cellsForLayer(state, active)) {
        drawIsoCell(
            ctx, state, cell.x, cell.y,
            textureForTile(state, cell.tileName, cell.layer),
            cell.tileName,
        );
    }

    drawActiveLayerHighlight(ctx, state);
    drawBoxPreview(ctx, state);
    drawSpawns(ctx, state);
    ctx.restore();
}

function directionAngle(dir) {
    switch (dir) {
        case 'Direction.North': return -Math.PI / 2;
        case 'Direction.East': return 0;
        case 'Direction.South': return Math.PI / 2;
        case 'Direction.West': return Math.PI;
        default: return Math.PI / 2;
    }
}

function drawSpawns(ctx, state) {
    const spawns = state.config?.spawns || [];
    if (!spawns.length) return;
    const grouped = new Map();
    for (const s of spawns) {
        const k = `${s.x},${s.y}`;
        if (!grouped.has(k)) grouped.set(k, []);
        grouped.get(k).push(s);
    }
    ctx.save();
    for (const [, list] of grouped) {
        const { x, y } = list[0];
        const { x: cx, y: cy } = cellCenterCanvas(x, y, state.offsetX, state.offsetY);
        const n = list.length;
        list.forEach((s, idx) => {
            const ox = (idx - (n - 1) / 2) * 18;
            const px = cx + ox;
            const py = cy - 6;
            const isPlayer = s.kind === 'player';
            const isVehicle = s.kind === 'vehicle';
            const scaleMul = spawnTools.formatSpawnScale(s);
            const baseR = isPlayer ? 15 : isVehicle ? 13 : 10;
            const r = baseR * scaleMul;
            const selected = state.selectedSpawn === s;
            const fill = isPlayer
                ? 'rgba(33,150,243,0.92)'
                : isVehicle
                    ? 'rgba(76,175,80,0.92)'
                    : 'rgba(255,193,7,0.95)';
            ctx.beginPath();
            ctx.arc(px, py, r, 0, Math.PI * 2);
            ctx.fillStyle = fill;
            ctx.fill();
            ctx.strokeStyle = selected ? '#ff5722' : '#fff';
            ctx.lineWidth = selected ? 3 : 2;
            ctx.stroke();
            if (selected) {
                ctx.beginPath();
                ctx.arc(px, py, r + 4, 0, Math.PI * 2);
                ctx.strokeStyle = '#ff5722';
                ctx.lineWidth = 2;
                ctx.stroke();
            }
            ctx.fillStyle = '#fff';
            ctx.font = 'bold 10px sans-serif';
            ctx.textAlign = 'center';
            ctx.textBaseline = 'middle';
            ctx.fillText(isPlayer ? 'P' : isVehicle ? 'V' : '●', px, py);
            const dirStr = isPlayer ? s.playerDirection : isVehicle ? s.vehicleDirection : null;
            if (dirStr) {
                const ang = directionAngle(dirStr);
                const len = 12;
                ctx.beginPath();
                ctx.moveTo(px, py);
                ctx.lineTo(px + Math.cos(ang) * len, py + Math.sin(ang) * len);
                ctx.strokeStyle = '#fff';
                ctx.lineWidth = 2;
                ctx.stroke();
            }
        });
        ctx.fillStyle = 'rgba(255,255,255,0.85)';
        ctx.font = '9px sans-serif';
        ctx.textAlign = 'center';
        ctx.fillText(`${x},${y}`, cx, cy + 16);
    }
    ctx.restore();
}

function drawBoxPreview(ctx, state) {
    if (state.tool !== 'box' || !state.boxStart || !state.boxEnd) return;
    const halfW = grid.PANEL_HALF_W;
    const halfH = grid.PANEL_HALF_H;
    const x0 = state.boxStart.x;
    const y0 = state.boxStart.y;
    const x1 = state.boxEnd.x;
    const y1 = state.boxEnd.y;
    const minX = Math.min(x0, x1);
    const maxX = Math.max(x0, x1);
    const minY = Math.min(y0, y1);
    const maxY = Math.max(y0, y1);
    ctx.save();
    ctx.fillStyle = 'rgba(74, 158, 255, 0.2)';
    ctx.strokeStyle = '#4a9eff';
    ctx.lineWidth = 2;
    for (let x = minX; x <= maxX; x++) {
        for (let y = minY; y <= maxY; y++) {
            const { x: cx, y: cy } = cellCenterCanvas(x, y, state.offsetX, state.offsetY);
            drawIsoDiamondCenter(ctx, cx, cy, halfW, halfH, '#4a9eff', 'rgba(74, 158, 255, 0.2)', 2);
        }
    }
    ctx.restore();
}

function cellFromEvent(ev, state) {
    const canvas = this.$.canvas;
    const rect = canvas.getBoundingClientRect();
    const scaleX = canvas.width / rect.width;
    const scaleY = canvas.height / rect.height;
    const mx = (ev.clientX - rect.left) * scaleX;
    const my = (ev.clientY - rect.top) * scaleY;
    const { x: lx, y: ly } = screenToLogical(mx, my, state, canvas);
    if (state.tool === 'picker') {
        const hit = cellHitTestTiles(lx, ly, state);
        if (hit) return hit;
    }
    return grid.cellFromCanvas(lx, ly, state.offsetX, state.offsetY);
}

function renderPalette(state) {
    const list = this.$.paletteList;
    const title = this.$.paletteTitle;
    if (!list) return;
    list.innerHTML = '';
    if (state.editMode === 'spawns') {
        if (title) title.textContent = `实体配置（主题 ${state.theme || state.config?.theme || 'sanxing'}）`;
        if (state.selectedSpawn && !spawnTools.spawnStillExists(state, state.selectedSpawn)) {
            state.selectedSpawn = null;
        }
        const hint = document.createElement('div');
        hint.style.cssText = 'grid-column:1/-1;font-size:11px;line-height:1.55;padding:4px;color:var(--color-normal-contrast-weaken);';
        hint.innerHTML = [
            '关卡贴图：<code>entityTextures</code>（整关默认）· 单个可拾取物可用 <code>spawns[].texture</code>',
            '<br>路径相对 <code>assets/resources</code>，不含 <code>.png</code>；留空则按地图主题推断',
            '<br>Shift+点击格子选中实体，可改坐标/朝向/缩放/贴图',
            `<br>${spawnTools.spawnSummary(state)}`,
            spawnTools.validationHint(state) ? `<br><span style="color:#ffb74d">${spawnTools.validationHint(state)}</span>` : '',
        ].join('');
        list.appendChild(hint);

        const texBox = document.createElement('div');
        texBox.style.cssText = 'grid-column:1/-1;display:flex;flex-direction:column;gap:6px;margin-top:6px;padding:8px;border:1px solid var(--color-normal-fill-emphasis);border-radius:4px;';
        const texTitle = document.createElement('div');
        texTitle.style.fontWeight = 'bold';
        texTitle.style.fontSize = '11px';
        texTitle.textContent = '关卡实体贴图（entityTextures）';
        texBox.appendChild(texTitle);

        const texRowStyle = 'display:flex;align-items:center;gap:6px;flex-wrap:wrap;';
        for (const f of entityTexPresets.ENTITY_TEXTURE_FIELDS) {
            const row = document.createElement('div');
            row.style.cssText = texRowStyle;
            const lab = document.createElement('ui-label');
            lab.textContent = f.label;
            lab.style.minWidth = '72px';
            lab.style.fontSize = '11px';
            const inp = document.createElement('ui-input');
            inp.id = `entity-tex-${f.key}`;
            inp.placeholder = 'textures/主题/...';
            inp.style.flex = '1';
            inp.style.minWidth = '120px';
            row.appendChild(lab);
            row.appendChild(inp);
            texBox.appendChild(row);
        }
        const texBtnRow = document.createElement('div');
        texBtnRow.style.cssText = 'display:flex;gap:6px;flex-wrap:wrap;';
        const fillBtn = document.createElement('ui-button');
        fillBtn.textContent = '从当前主题填充';
        fillBtn.addEventListener('click', () => {
            const theme = state.theme || state.config?.theme || 'silu';
            if (themeDb) {
                const themesData = themeDb.readThemesDb(projectPath());
                const t = themesData.themes?.[theme];
                if (t?.entities) {
                    entityTexPresets.ensureEntityTextures(state);
                    Object.assign(state.config.entityTextures, { ...t.entities });
                    entityTexPresets.pruneEmptyEntityTextures(state);
                    if (this._lmePersistMap) this._lmePersistMap();
                } else {
                    entityTexPresets.applyThemePreset(state, theme);
                }
            } else {
                entityTexPresets.applyThemePreset(state, theme);
            }
            renderPalette.call(this, state);
        });
        const saveTexBtn = document.createElement('ui-button');
        saveTexBtn.textContent = '应用贴图配置';
        saveTexBtn.addEventListener('click', () => {
            if (this._lmeApplyEntityTextures) this._lmeApplyEntityTextures();
        });
        const clearTexBtn = document.createElement('ui-button');
        clearTexBtn.textContent = '清空';
        clearTexBtn.addEventListener('click', () => {
            if (state.config) delete state.config.entityTextures;
            if (this._lmePersistMap) this._lmePersistMap();
            renderPalette.call(this, state);
            if (this.$.status) this.$.status.textContent = '已清空 entityTextures';
        });
        texBtnRow.appendChild(fillBtn);
        texBtnRow.appendChild(saveTexBtn);
        texBtnRow.appendChild(clearTexBtn);
        texBox.appendChild(texBtnRow);
        list.appendChild(texBox);
        entityTexPresets.writeEntityTexturesToPanel(list, state.config?.entityTextures);

        const spawns = state.config?.spawns || [];
        if (spawns.length) {
            const listTitle = document.createElement('div');
            listTitle.style.cssText = 'grid-column:1/-1;font-size:11px;font-weight:bold;margin-top:6px;';
            listTitle.textContent = '实体列表（点击选中）';
            list.appendChild(listTitle);
            for (const s of spawns) {
                const btn = document.createElement('ui-button');
                const label = spawnTools.SPAWN_KIND_LABELS[s.kind] || s.kind;
                const sc = spawnTools.formatSpawnScale(s);
                const dir = s.playerDirection || s.vehicleDirection || '';
                btn.textContent = `${label} (${s.x},${s.y})${dir ? ` ${dir.replace('Direction.', '')}` : ''}${sc !== 1 ? ` ×${sc}` : ''}${s.texture ? ' 🖼' : ''}`;
                if (state.selectedSpawn === s) btn.classList.add('active');
                btn.style.cssText = 'grid-column:1/-1;justify-content:flex-start;text-align:left;';
                btn.addEventListener('click', () => {
                    state.selectedSpawn = s;
                    const d = s.playerDirection || s.vehicleDirection;
                    if (d && this.$.spawnDirection) this.$.spawnDirection.value = d;
                    state.placementScale[s.kind] = sc;
                    if (this.$.spawnPlacementScale) this.$.spawnPlacementScale.value = String(sc);
                    renderPalette.call(this, state);
                    drawGrid.call(this, state);
                });
                list.appendChild(btn);
            }
        }

        const sel = state.selectedSpawn;
        const panel = document.createElement('div');
        panel.style.cssText = 'grid-column:1/-1;display:flex;flex-direction:column;gap:6px;margin-top:8px;padding:8px;border:1px solid var(--color-normal-fill-emphasis);border-radius:4px;';
        const panelTitle = document.createElement('div');
        panelTitle.style.fontWeight = 'bold';
        panelTitle.style.fontSize = '11px';
        panelTitle.textContent = sel
            ? `编辑：${spawnTools.SPAWN_KIND_LABELS[sel.kind] || sel.kind}`
            : '选中实体后可编辑属性';
        panel.appendChild(panelTitle);

        const rowStyle = 'display:flex;align-items:center;gap:6px;flex-wrap:wrap;';
        const mkRow = (labelText, inputEl) => {
            const row = document.createElement('div');
            row.style.cssText = rowStyle;
            const lab = document.createElement('ui-label');
            lab.textContent = labelText;
            lab.style.minWidth = '36px';
            row.appendChild(lab);
            row.appendChild(inputEl);
            return row;
        };

        const xIn = document.createElement('ui-num-input');
        xIn.id = 'spawn-inspect-x';
        xIn.step = '1';
        xIn.style.width = '72px';
        xIn.disabled = !sel;
        if (sel) xIn.value = String(sel.x);

        const yIn = document.createElement('ui-num-input');
        yIn.id = 'spawn-inspect-y';
        yIn.step = '1';
        yIn.style.width = '72px';
        yIn.disabled = !sel;
        if (sel) yIn.value = String(sel.y);

        panel.appendChild(mkRow('X', xIn));
        panel.appendChild(mkRow('Y', yIn));

        const needDir = sel && (sel.kind === 'player' || sel.kind === 'vehicle');
        if (needDir) {
            const dirSel = document.createElement('ui-select');
            dirSel.id = 'spawn-inspect-dir';
            dirSel.style.minWidth = '96px';
            for (const d of spawnTools.DIRECTIONS) {
                const opt = document.createElement('option');
                opt.value = d;
                opt.textContent = d.replace('Direction.', '');
                dirSel.appendChild(opt);
            }
            const curDir = sel.playerDirection || sel.vehicleDirection || 'Direction.South';
            dirSel.value = curDir;
            panel.appendChild(mkRow('朝向', dirSel));
        }

        const scaleIn = document.createElement('ui-num-input');
        scaleIn.id = 'spawn-inspect-scale';
        scaleIn.step = '0.1';
        scaleIn.min = '0.1';
        scaleIn.max = '4';
        scaleIn.style.width = '72px';
        scaleIn.disabled = !sel;
        scaleIn.value = sel ? String(spawnTools.formatSpawnScale(sel)) : '1';
        panel.appendChild(mkRow('缩放', scaleIn));

        const texIn = document.createElement('ui-input');
        texIn.id = 'spawn-inspect-texture';
        texIn.placeholder = '留空=用关卡默认';
        texIn.style.flex = '1';
        texIn.disabled = !sel;
        texIn.value = sel?.texture || '';
        panel.appendChild(mkRow('贴图', texIn));

        const applyBtn = document.createElement('ui-button');
        applyBtn.textContent = '应用修改';
        applyBtn.disabled = !sel;
        applyBtn.addEventListener('click', () => {
            if (this._lmeApplySpawnInspector) this._lmeApplySpawnInspector();
        });
        panel.appendChild(applyBtn);

        list.appendChild(panel);
        return;
    }
    if (title) title.textContent = `平铺调色板 — ${THEME_LABELS[state.theme] || state.theme}`;
    const needBrush = state.tool === 'paint' || state.tool === 'box' || state.tool === 'fill';
    const editLayer = state.editLayer || 'ground';
    for (const tile of state.tiles) {
        if (tile.layer !== editLayer) continue;
        const div = document.createElement('div');
        const active = needBrush && state.brush && tileBrushId(state.brush) === tileBrushId(tile);
        const disabled = state.tool === 'eraser' || state.tool === 'picker';
        div.className = 'lme-tile' + (active ? ' active' : '') + (disabled ? ' lme-tile-dim' : '');
        if (disabled) div.style.opacity = '0.55';
        const img = document.createElement('img');
        const texPath = textureFsPath(tile.texture);
        if (fs.existsSync(texPath)) {
            img.src = `file://${texPath}`;
        }
        const span = document.createElement('span');
        span.textContent = tile.display || tile.tileKey;
        div.appendChild(img);
        div.appendChild(span);
        div.addEventListener('click', () => {
            selectBrushFromPalette.call(this, state, tile);
        });
        list.appendChild(div);
    }
}

function selectBrushFromPalette(state, tile) {
    state.brush = tile;
    state.editLayer = tile.layer;
    if (this.$.layerGround && this.$.layerBorder) {
        this.$.layerGround.classList.toggle('active', tile.layer === 'ground');
        this.$.layerBorder.classList.toggle('active', tile.layer === 'border');
    }
    if (state.tool === 'eraser' || state.tool === 'picker') {
        for (const [id, btn] of Object.entries({
            paint: this.$.toolPaint,
            box: this.$.toolBox,
            picker: this.$.toolPicker,
            eraser: this.$.toolEraser,
            fill: this.$.toolFill,
        })) {
            if (btn) btn.classList.toggle('active', id === 'paint');
        }
        state.tool = 'paint';
        if (this.$.canvas) this.$.canvas.style.cursor = tools.TOOL_CURSORS.paint;
    }
    renderPalette.call(this, state);
    if (this.$.status) {
        this.$.status.textContent = `已选 ${tile.display}（${tile.layer}）— 可用画笔/框选/填充绘制`;
    }
    drawGrid.call(this, state);
}

exports.close = function () {
    if (this._lmeState) {
        const state = this._lmeState;
        if (state._autoBakeTimer) {
            clearTimeout(state._autoBakeTimer);
            state._autoBakeTimer = null;
        }
    }
    if (this._lmeResizeObserver) {
        this._lmeResizeObserver.disconnect();
        this._lmeResizeObserver = null;
    }
    if (this._lmeWindowMouseUp) {
        window.removeEventListener('mouseup', this._lmeWindowMouseUp);
        this._lmeWindowMouseUp = null;
    }
    if (this._lmeKeyHandler) {
        window.removeEventListener('keydown', this._lmeKeyHandler);
        this._lmeKeyHandler = null;
    }
    if (this._lmePrefabSyncListener) {
        const rm = Editor.Message.__protected__?.removeBroadcastListener || Editor.Message.removeBroadcastListener;
        if (typeof rm === 'function') {
            rm.call(Editor.Message, 'level-map-editor:prefab-synced', this._lmePrefabSyncListener);
        }
        this._lmePrefabSyncListener = null;
    }
    this._lmeReady = false;
};
