From 078e13138127bfc94bc607aba9d79db28861b38d Mon Sep 17 00:00:00 2001 From: 1165565692-lang <1165565692@qq.com> Date: Wed, 10 Jun 2026 01:02:03 +0800 Subject: [PATCH] Fix map editor tile selection --- game/docs/modules/map_editor.md | 15 ++++++------ tools/map_editor/README.md | 10 +++++++- tools/map_editor/app.js | 43 +++++++++++++++++++++++++-------- tools/map_editor/index.html | 2 +- 4 files changed, 51 insertions(+), 19 deletions(-) diff --git a/game/docs/modules/map_editor.md b/game/docs/modules/map_editor.md index 0cadea7..493a6d4 100644 --- a/game/docs/modules/map_editor.md +++ b/game/docs/modules/map_editor.md @@ -94,7 +94,7 @@ http://127.0.0.1:8787/tools/map_editor/ - Canvas 可视化 world、camera、player spawn、collision、rect、tile_rect、battle zone、spawn。 - 对象列表选择和画布点击选择。 - 网格吸附拖动对象。 -- 通过“编辑目标”过滤画布命中目标,默认优先编辑 `collision`。 +- 通过“编辑目标”过滤画布命中目标,默认“智能选择”优先选中 `tile_rect` 和图形,避免被重叠 `collision` 抢选。 - 矩形对象支持右下角手柄缩放。 - 属性面板编辑选中对象的数值或文本。 - 新增/删除 tileset、layer、collision、tile_rect、battle zone、spawn。 @@ -133,12 +133,13 @@ assets/stage/stage_01/props/<图片文件名> 2. 已完成:能看到地面、平台、battle zone、spawn point。 3. 已完成:能新增/移动/删除一个 `collision`。 4. 已完成:能新增/移动/删除一个 `tile_rect`。 -5. 已完成:能导入图形,生成 `rect_sprite` 和同尺寸 `collision`。 -6. 已完成:导入图形支持移动、缩放和同步调整绑定碰撞块。 -7. 已完成:能导出 `.map`。 -8. 已完成:编辑器 parser/serializer 烟测通过,保存格式保持 `StageMapLoader` 兼容。 -9. 待后续地图修改后复测:Windows 构建通过,短启动通过。 -10. 待后续地图修改后复测:Switch 构建能把保存后的 `.map` 打进 RomFS。 +5. 已完成:`tile_rect` 与 `collision` 重叠时,默认智能选择可直接选中并移动铺砖区。 +6. 已完成:能导入图形,生成 `rect_sprite` 和同尺寸 `collision`。 +7. 已完成:导入图形支持移动、缩放和同步调整绑定碰撞块。 +8. 已完成:能导出 `.map`。 +9. 已完成:编辑器 parser/serializer 烟测通过,保存格式保持 `StageMapLoader` 兼容。 +10. 待后续地图修改后复测:Windows 构建通过,短启动通过。 +11. 待后续地图修改后复测:Switch 构建能把保存后的 `.map` 打进 RomFS。 ## 后续增强 diff --git a/tools/map_editor/README.md b/tools/map_editor/README.md index dd27f55..b8aa386 100644 --- a/tools/map_editor/README.md +++ b/tools/map_editor/README.md @@ -24,7 +24,7 @@ http://127.0.0.1:8787/tools/map_editor/ - 可视化世界范围、摄像机范围、玩家出生点、碰撞块、场景矩形、铺砖区、战斗区和敌人出生点。 - 从画布或对象列表选择对象。 - 在画布上拖动对象,支持网格吸附。 -- 通过“编辑目标”过滤画布选择对象,默认优先编辑碰撞块,避免点到重叠的铺砖区。 +- 通过“编辑目标”过滤画布选择对象,默认“智能选择”会优先选中铺砖区和图形,避免被重叠碰撞块挡住。 - 选中矩形对象后,拖动右下角蓝色手柄可以调整宽高。 - 通过“导入图形”选择图片,自动生成一个 `rect_sprite` 图形对象和一个同尺寸 `collision`。 - 导入的图形会在浏览器内临时预览,拖动或缩放图形时,同步移动或缩放绑定的碰撞块。 @@ -40,6 +40,14 @@ http://127.0.0.1:8787/tools/map_editor/ 4. 拖动右下角蓝色手柄可以调整宽高。 5. 也可以在右侧属性面板直接修改 `X`、`Y`、`W`、`H`。 +## 调整铺砖区 + +1. 在左侧“编辑目标”里选择“智能选择”或“铺砖区”。 +2. 在画布上点击铺砖区,或从对象列表选择对应铺砖区。 +3. 拖动铺砖区本体可以移动位置。 +4. 拖动右下角蓝色手柄可以调整列数和行数。 +5. 如果要编辑同位置碰撞体,再把“编辑目标”切到“碰撞块”。 + ## 导入和调整图形 1. 点击左侧“导入图形”,选择一个图片素材。 diff --git a/tools/map_editor/app.js b/tools/map_editor/app.js index 47dd543..6db29d5 100644 --- a/tools/map_editor/app.js +++ b/tools/map_editor/app.js @@ -5,7 +5,7 @@ const state = { selected: null, view: { x: -80, y: -40, scale: 0.35 }, gridSize: 10, - hitFilter: "collision", + hitFilter: "all", imagePreviews: new Map(), dragging: null, }; @@ -563,8 +563,8 @@ function line(ctx, x1, y1, x2, y2) { function collectObjects() { const objects = [ - objectForPoint("player", "玩家出生点", state.map.playerSpawn), - objectForRect("camera", "摄像机范围", state.map.camera), + objectForPoint("player", "玩家出生点", state.map.playerSpawn, false, "player"), + objectForRect("camera", "摄像机范围", state.map.camera, false, null, "camera"), ]; state.map.tilesets.forEach((tileset, index) => objects.push(objectForTileset(`tileset:${index}`, `图块集 ${tileset.id}`, tileset, true))); state.map.collisions.forEach((rect, index) => objects.push(objectForRect(`collision:${index}`, `碰撞块 ${index + 1}`, rect, true, null, "collision"))); @@ -650,10 +650,10 @@ function objectForLayerRect(id, title, item, deletable) { }; } -function objectForPoint(id, title, point, deletable = false) { +function objectForPoint(id, title, point, deletable = false, type = "spawn") { return { key: id, - type: "spawn", + type, title, meta: `${fmt(point.x)}, ${fmt(point.y)}`, deletable, @@ -701,18 +701,18 @@ function getSelectedObject() { function onCanvasMouseDown(event) { const world = screenToWorld(event.offsetX, event.offsetY); const selected = getSelectedObject(); + const object = hitTest(world.x, world.y); if (selected && selected.resize && isInsideResizeHandle(selected, world.x, world.y)) { state.dragging = { resize: true, last: snapPoint(world) }; setCanvasCursor("nwse-resize"); return; } - if (selected && selected.move && isInsideBounds(selected, world.x, world.y)) { + if (selected && selected.move && isInsideBounds(selected, world.x, world.y) && canDragSelectedInPlace(selected, object)) { state.dragging = { last: snapPoint(world) }; setCanvasCursor("move"); return; } - const object = hitTest(world.x, world.y); if (object) { state.selected = object.key; state.dragging = { last: snapPoint(world) }; @@ -768,7 +768,7 @@ function onCanvasWheel(event) { } function hitTest(x, y) { - const objects = collectObjects().filter(isHitFilterEnabled).reverse(); + const objects = hitCandidates(); for (const object of objects) { const bounds = object.bounds && object.bounds(); if (!bounds) continue; @@ -777,12 +777,32 @@ function hitTest(x, y) { return null; } +function hitCandidates() { + const objects = collectObjects().filter(isHitFilterEnabled); + if (state.hitFilter !== "all") return objects.reverse(); + const priority = { tile: 0, rect: 1, spawn: 2, collision: 3, zone: 4, player: 5, camera: 6 }; + return objects + .map((object, index) => ({ object, index })) + .sort((a, b) => { + const pa = priority[a.object.type] ?? 10; + const pb = priority[b.object.type] ?? 10; + if (pa !== pb) return pa - pb; + return b.index - a.index; + }) + .map((entry) => entry.object); +} + function isInsideBounds(object, x, y) { const bounds = object.bounds && object.bounds(); if (!bounds) return false; return x >= bounds.x && x <= bounds.x + bounds.width && y >= bounds.y && y <= bounds.y + bounds.height; } +function canDragSelectedInPlace(selected, topHit) { + if (state.hitFilter !== "all") return true; + return topHit && sameSelection(selected.key, topHit.key); +} + function isHitFilterEnabled(object) { if (state.hitFilter === "all") return true; if (state.hitFilter === "zone") return object.type === "zone"; @@ -792,15 +812,16 @@ function isHitFilterEnabled(object) { function updateCanvasCursor(event) { const world = screenToWorld(event.offsetX, event.offsetY); const selected = getSelectedObject(); + const object = hitTest(world.x, world.y); if (selected && selected.resize && isInsideResizeHandle(selected, world.x, world.y)) { setCanvasCursor("nwse-resize"); return; } - if (selected && selected.move && isInsideBounds(selected, world.x, world.y)) { + if (selected && selected.move && isInsideBounds(selected, world.x, world.y) && canDragSelectedInPlace(selected, object)) { setCanvasCursor("move"); return; } - if (hitTest(world.x, world.y)) { + if (object) { setCanvasCursor("move"); return; } @@ -815,6 +836,7 @@ function addCollision() { const point = snapPoint(viewCenter()); state.map.collisions.push({ x: point.x, y: point.y, width: 160, height: 40 }); state.selected = `collision:${state.map.collisions.length - 1}`; + state.hitFilter = "collision"; refresh(); } @@ -878,6 +900,7 @@ function addTileRect() { const point = snapPoint(viewCenter()); layer.tileRects.push({ tileset: state.map.tilesets[0].id, x: point.x, y: point.y, columns: 8, rows: 4, tileIndex: 0, color: { r: 0.62, g: 0.68, b: 0.73 } }); state.selected = `tile:${layerIndex}:${layer.tileRects.length - 1}`; + state.hitFilter = "tile"; refresh(); } diff --git a/tools/map_editor/index.html b/tools/map_editor/index.html index 7edf416..c6c8b4b 100644 --- a/tools/map_editor/index.html +++ b/tools/map_editor/index.html @@ -46,8 +46,8 @@