Fix collision editing in map editor

This commit is contained in:
2026-06-10 00:31:53 +08:00
parent d7bc9e6a24
commit d652d576e3
3 changed files with 120 additions and 10 deletions
+99 -10
View File
@@ -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 });