From d652d576e358bfe0b603329a26ad7ebb3500202b Mon Sep 17 00:00:00 2001 From: 1165565692-lang <1165565692@qq.com> Date: Wed, 10 Jun 2026 00:31:53 +0800 Subject: [PATCH] Fix collision editing in map editor --- tools/map_editor/README.md | 10 ++++ tools/map_editor/app.js | 109 ++++++++++++++++++++++++++++++++---- tools/map_editor/index.html | 11 ++++ 3 files changed, 120 insertions(+), 10 deletions(-) diff --git a/tools/map_editor/README.md b/tools/map_editor/README.md index fa5b74d..c8a98b6 100644 --- a/tools/map_editor/README.md +++ b/tools/map_editor/README.md @@ -24,10 +24,20 @@ http://127.0.0.1:8787/tools/map_editor/ - 可视化世界范围、摄像机范围、玩家出生点、碰撞块、场景矩形、铺砖区、战斗区和敌人出生点。 - 从画布或对象列表选择对象。 - 在画布上拖动对象,支持网格吸附。 +- 通过“编辑目标”过滤画布选择对象,默认优先编辑碰撞块,避免点到重叠的铺砖区。 +- 选中矩形对象后,拖动右下角蓝色手柄可以调整宽高。 - 编辑选中对象的数值和文本属性。 - 新增/删除图块集、图层、碰撞块、铺砖区、战斗区和出生点。 - 下载兼容 `StageMapLoader` 的 `.map` 文本。 +## 调整碰撞块 + +1. 在左侧“编辑目标”里选择“碰撞块”。 +2. 在画布上点击红色碰撞框,或从对象列表选择对应碰撞块。 +3. 拖动碰撞块本体可以移动位置。 +4. 拖动右下角蓝色手柄可以调整宽高。 +5. 也可以在右侧属性面板直接修改 `X`、`Y`、`W`、`H`。 + ## 当前限制 - 浏览器安全限制不允许静态网页直接覆盖本地源文件。当前需要先下载 `.map`,再手动替换项目里的地图文件;部署到服务器后可以增加服务端保存接口。 diff --git a/tools/map_editor/app.js b/tools/map_editor/app.js index a326781..5c0bb18 100644 --- a/tools/map_editor/app.js +++ b/tools/map_editor/app.js @@ -5,6 +5,7 @@ const state = { selected: null, view: { x: -80, y: -40, scale: 0.35 }, gridSize: 10, + hitFilter: "collision", dragging: null, }; @@ -21,6 +22,7 @@ function bindElements() { for (const id of [ "mapCanvas", "fileInput", "loadSampleButton", "downloadButton", "mapIdInput", "worldWidthInput", "worldHeightInput", "gridSizeInput", + "hitFilterInput", "objectList", "propertyPanel", "selectionSummary", "deleteButton", "tilesetList", "outputText", "statusText", "viewText", "addTilesetButton", "addLayerButton", "addCollisionButton", @@ -53,6 +55,10 @@ function bindEvents() { state.gridSize = Math.max(1, numberValue(elements.gridSizeInput, state.gridSize)); refresh(); }); + elements.hitFilterInput.addEventListener("change", () => { + state.hitFilter = elements.hitFilterInput.value; + refresh(); + }); elements.addCollisionButton.addEventListener("click", addCollision); elements.addTilesetButton.addEventListener("click", addTileset); @@ -260,6 +266,7 @@ function refresh() { elements.worldWidthInput.value = state.map.world.width; elements.worldHeightInput.value = state.map.world.height; elements.gridSizeInput.value = state.gridSize; + elements.hitFilterInput.value = state.hitFilter; elements.outputText.value = serializeMap(state.map); renderTilesets(); renderObjectList(); @@ -430,7 +437,35 @@ function drawSelection(ctx) { const bounds = object.bounds && object.bounds(); ctx.strokeStyle = "#54a8ff"; ctx.lineWidth = 3 / state.view.scale; - if (bounds) ctx.strokeRect(bounds.x, bounds.y, bounds.width, bounds.height); + if (bounds) { + ctx.strokeRect(bounds.x, bounds.y, bounds.width, bounds.height); + if (object.resize) { + const handle = resizeHandleFor(bounds); + ctx.fillStyle = "#54a8ff"; + ctx.fillRect(handle.x, handle.y, handle.width, handle.height); + ctx.strokeStyle = "#0b0d10"; + ctx.lineWidth = 1 / state.view.scale; + ctx.strokeRect(handle.x, handle.y, handle.width, handle.height); + } + } +} + +function resizeHandleFor(bounds) { + const size = 12 / state.view.scale; + return { + x: bounds.x + bounds.width - size, + y: bounds.y + bounds.height - size, + width: size, + height: size, + }; +} + +function isInsideResizeHandle(object, x, y) { + const bounds = object.bounds && object.bounds(); + if (!bounds) return false; + const handle = resizeHandleFor(bounds); + return x >= handle.x && x <= handle.x + handle.width && + y >= handle.y && y <= handle.y + handle.height; } function drawFilledRect(ctx, rect, color, alpha, stroke) { @@ -473,15 +508,15 @@ function collectObjects() { objectForRect("camera", "摄像机范围", state.map.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))); + state.map.collisions.forEach((rect, index) => objects.push(objectForRect(`collision:${index}`, `碰撞块 ${index + 1}`, rect, true, null, "collision"))); state.map.layers.forEach((layer, layerIndex) => { objects.push(objectForLayer(`layer:${layerIndex}`, `图层 ${layer.id}`, layer, true)); - layer.rects.forEach((rect, rectIndex) => objects.push(objectForRect(`rect:${layerIndex}:${rectIndex}`, `${layer.id} 矩形 ${rectIndex + 1}`, rect.bounds, true))); + layer.rects.forEach((rect, rectIndex) => objects.push(objectForRect(`rect:${layerIndex}:${rectIndex}`, `${layer.id} 矩形 ${rectIndex + 1}`, rect.bounds, true, null, "rect"))); layer.tileRects.forEach((tileRect, tileIndex) => objects.push(objectForTileRect(`tile:${layerIndex}:${tileIndex}`, `${layer.id} 铺砖区 ${tileIndex + 1}`, tileRect, true))); }); state.map.battleZones.forEach((zone, zoneIndex) => { - objects.push(objectForRect(`zone:${zoneIndex}`, `战斗区 ${zoneIndex + 1}`, zone.trigger, true, zone.id)); - objects.push(objectForRect(`zone_camera:${zoneIndex}`, `战斗区镜头 ${zoneIndex + 1}`, zone.camera, false, zone.id)); + objects.push(objectForRect(`zone:${zoneIndex}`, `战斗区 ${zoneIndex + 1}`, zone.trigger, true, zone.id, "zone")); + objects.push(objectForRect(`zone_camera:${zoneIndex}`, `战斗区镜头 ${zoneIndex + 1}`, zone.camera, false, zone.id, "zone")); zone.spawns.forEach((spawn, spawnIndex) => objects.push(objectForPoint(`spawn:${zoneIndex}:${spawnIndex}`, `出生点 ${spawn.id}`, spawn, true))); }); return objects; @@ -517,14 +552,19 @@ function objectForLayer(id, title, layer, deletable) { }; } -function objectForRect(id, title, rect, deletable = false, meta = null) { +function objectForRect(id, title, rect, deletable = false, meta = null, type = "rect") { return { key: id, + type, title, meta: meta || `${fmt(rect.x)}, ${fmt(rect.y)}, ${fmt(rect.width)}x${fmt(rect.height)}`, deletable, bounds: () => rect, move(dx, dy) { rect.x += dx; rect.y += dy; }, + resize(dw, dh) { + rect.width = Math.max(1, rect.width + dw); + rect.height = Math.max(1, rect.height + dh); + }, fields: () => rectFields(rect), }; } @@ -532,6 +572,7 @@ function objectForRect(id, title, rect, deletable = false, meta = null) { function objectForPoint(id, title, point, deletable = false) { return { key: id, + type: "spawn", title, meta: `${fmt(point.x)}, ${fmt(point.y)}`, deletable, @@ -547,11 +588,18 @@ function objectForTileRect(id, title, tileRect, deletable) { const th = tileset ? tileset.tileHeight : 10; return { key: id, + type: "tile", title, meta: `${tileRect.tileset} ${tileRect.columns}x${tileRect.rows}`, deletable, bounds: () => ({ x: tileRect.x, y: tileRect.y, width: tileRect.columns * tw, height: tileRect.rows * th }), move(dx, dy) { tileRect.x += dx; tileRect.y += dy; }, + resize(dw, dh) { + const nextWidth = Math.max(tw, tileRect.columns * tw + dw); + const nextHeight = Math.max(th, tileRect.rows * th + dh); + tileRect.columns = Math.max(1, Math.round(nextWidth / tw)); + tileRect.rows = Math.max(1, Math.round(nextHeight / th)); + }, fields: () => [ textField("图块集", () => tileRect.tileset, (v) => { tileRect.tileset = v; }, state.map.tilesets.map((item) => item.id)), numberField("X", () => tileRect.x, (v) => { tileRect.x = v; }), @@ -571,18 +619,30 @@ function getSelectedObject() { function onCanvasMouseDown(event) { const world = screenToWorld(event.offsetX, event.offsetY); + const selected = getSelectedObject(); + if (selected && selected.resize && isInsideResizeHandle(selected, world.x, world.y)) { + state.dragging = { resize: true, last: snapPoint(world) }; + setCanvasCursor("nwse-resize"); + return; + } + const object = hitTest(world.x, world.y); if (object) { state.selected = object.key; state.dragging = { last: snapPoint(world) }; + setCanvasCursor("move"); refresh(); } else { state.dragging = { pan: true, x: event.clientX, y: event.clientY, viewX: state.view.x, viewY: state.view.y }; + setCanvasCursor("grabbing"); } } function onCanvasMouseMove(event) { - if (!state.dragging) return; + if (!state.dragging) { + updateCanvasCursor(event); + return; + } if (state.dragging.pan) { state.view.x = state.dragging.viewX - (event.clientX - state.dragging.x) / state.view.scale; state.view.y = state.dragging.viewY - (event.clientY - state.dragging.y) / state.view.scale; @@ -590,12 +650,16 @@ function onCanvasMouseMove(event) { return; } const object = getSelectedObject(); - if (!object || !object.move) return; + if (!object) return; const current = snapPoint(screenToWorld(event.offsetX, event.offsetY)); const dx = current.x - state.dragging.last.x; const dy = current.y - state.dragging.last.y; if (dx || dy) { - object.move(dx, dy); + if (state.dragging.resize && object.resize) { + object.resize(dx, dy); + } else if (object.move) { + object.move(dx, dy); + } state.dragging.last = current; refresh(); } @@ -603,6 +667,7 @@ function onCanvasMouseMove(event) { function onCanvasMouseUp() { state.dragging = null; + setCanvasCursor("crosshair"); } function onCanvasWheel(event) { @@ -617,7 +682,7 @@ function onCanvasWheel(event) { } function hitTest(x, y) { - const objects = collectObjects().reverse(); + const objects = collectObjects().filter(isHitFilterEnabled).reverse(); for (const object of objects) { const bounds = object.bounds && object.bounds(); if (!bounds) continue; @@ -626,6 +691,30 @@ function hitTest(x, y) { return null; } +function isHitFilterEnabled(object) { + if (state.hitFilter === "all") return true; + if (state.hitFilter === "zone") return object.type === "zone"; + return object.type === state.hitFilter; +} + +function updateCanvasCursor(event) { + const world = screenToWorld(event.offsetX, event.offsetY); + const selected = getSelectedObject(); + if (selected && selected.resize && isInsideResizeHandle(selected, world.x, world.y)) { + setCanvasCursor("nwse-resize"); + return; + } + if (hitTest(world.x, world.y)) { + setCanvasCursor("move"); + return; + } + setCanvasCursor("crosshair"); +} + +function setCanvasCursor(cursor) { + elements.mapCanvas.style.cursor = cursor; +} + function addCollision() { const point = snapPoint(viewCenter()); state.map.collisions.push({ x: point.x, y: point.y, width: 160, height: 40 }); diff --git a/tools/map_editor/index.html b/tools/map_editor/index.html index bdf53a9..325cb38 100644 --- a/tools/map_editor/index.html +++ b/tools/map_editor/index.html @@ -43,6 +43,17 @@ 网格 +