Fix map editor tile selection
This commit is contained in:
@@ -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。
|
- Canvas 可视化 world、camera、player spawn、collision、rect、tile_rect、battle zone、spawn。
|
||||||
- 对象列表选择和画布点击选择。
|
- 对象列表选择和画布点击选择。
|
||||||
- 网格吸附拖动对象。
|
- 网格吸附拖动对象。
|
||||||
- 通过“编辑目标”过滤画布命中目标,默认优先编辑 `collision`。
|
- 通过“编辑目标”过滤画布命中目标,默认“智能选择”优先选中 `tile_rect` 和图形,避免被重叠 `collision` 抢选。
|
||||||
- 矩形对象支持右下角手柄缩放。
|
- 矩形对象支持右下角手柄缩放。
|
||||||
- 属性面板编辑选中对象的数值或文本。
|
- 属性面板编辑选中对象的数值或文本。
|
||||||
- 新增/删除 tileset、layer、collision、tile_rect、battle zone、spawn。
|
- 新增/删除 tileset、layer、collision、tile_rect、battle zone、spawn。
|
||||||
@@ -133,12 +133,13 @@ assets/stage/stage_01/props/<图片文件名>
|
|||||||
2. 已完成:能看到地面、平台、battle zone、spawn point。
|
2. 已完成:能看到地面、平台、battle zone、spawn point。
|
||||||
3. 已完成:能新增/移动/删除一个 `collision`。
|
3. 已完成:能新增/移动/删除一个 `collision`。
|
||||||
4. 已完成:能新增/移动/删除一个 `tile_rect`。
|
4. 已完成:能新增/移动/删除一个 `tile_rect`。
|
||||||
5. 已完成:能导入图形,生成 `rect_sprite` 和同尺寸 `collision`。
|
5. 已完成:`tile_rect` 与 `collision` 重叠时,默认智能选择可直接选中并移动铺砖区。
|
||||||
6. 已完成:导入图形支持移动、缩放和同步调整绑定碰撞块。
|
6. 已完成:能导入图形,生成 `rect_sprite` 和同尺寸 `collision`。
|
||||||
7. 已完成:能导出 `.map`。
|
7. 已完成:导入图形支持移动、缩放和同步调整绑定碰撞块。
|
||||||
8. 已完成:编辑器 parser/serializer 烟测通过,保存格式保持 `StageMapLoader` 兼容。
|
8. 已完成:能导出 `.map`。
|
||||||
9. 待后续地图修改后复测:Windows 构建通过,短启动通过。
|
9. 已完成:编辑器 parser/serializer 烟测通过,保存格式保持 `StageMapLoader` 兼容。
|
||||||
10. 待后续地图修改后复测:Switch 构建能把保存后的 `.map` 打进 RomFS。
|
10. 待后续地图修改后复测:Windows 构建通过,短启动通过。
|
||||||
|
11. 待后续地图修改后复测:Switch 构建能把保存后的 `.map` 打进 RomFS。
|
||||||
|
|
||||||
## 后续增强
|
## 后续增强
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ http://127.0.0.1:8787/tools/map_editor/
|
|||||||
- 可视化世界范围、摄像机范围、玩家出生点、碰撞块、场景矩形、铺砖区、战斗区和敌人出生点。
|
- 可视化世界范围、摄像机范围、玩家出生点、碰撞块、场景矩形、铺砖区、战斗区和敌人出生点。
|
||||||
- 从画布或对象列表选择对象。
|
- 从画布或对象列表选择对象。
|
||||||
- 在画布上拖动对象,支持网格吸附。
|
- 在画布上拖动对象,支持网格吸附。
|
||||||
- 通过“编辑目标”过滤画布选择对象,默认优先编辑碰撞块,避免点到重叠的铺砖区。
|
- 通过“编辑目标”过滤画布选择对象,默认“智能选择”会优先选中铺砖区和图形,避免被重叠碰撞块挡住。
|
||||||
- 选中矩形对象后,拖动右下角蓝色手柄可以调整宽高。
|
- 选中矩形对象后,拖动右下角蓝色手柄可以调整宽高。
|
||||||
- 通过“导入图形”选择图片,自动生成一个 `rect_sprite` 图形对象和一个同尺寸 `collision`。
|
- 通过“导入图形”选择图片,自动生成一个 `rect_sprite` 图形对象和一个同尺寸 `collision`。
|
||||||
- 导入的图形会在浏览器内临时预览,拖动或缩放图形时,同步移动或缩放绑定的碰撞块。
|
- 导入的图形会在浏览器内临时预览,拖动或缩放图形时,同步移动或缩放绑定的碰撞块。
|
||||||
@@ -40,6 +40,14 @@ http://127.0.0.1:8787/tools/map_editor/
|
|||||||
4. 拖动右下角蓝色手柄可以调整宽高。
|
4. 拖动右下角蓝色手柄可以调整宽高。
|
||||||
5. 也可以在右侧属性面板直接修改 `X`、`Y`、`W`、`H`。
|
5. 也可以在右侧属性面板直接修改 `X`、`Y`、`W`、`H`。
|
||||||
|
|
||||||
|
## 调整铺砖区
|
||||||
|
|
||||||
|
1. 在左侧“编辑目标”里选择“智能选择”或“铺砖区”。
|
||||||
|
2. 在画布上点击铺砖区,或从对象列表选择对应铺砖区。
|
||||||
|
3. 拖动铺砖区本体可以移动位置。
|
||||||
|
4. 拖动右下角蓝色手柄可以调整列数和行数。
|
||||||
|
5. 如果要编辑同位置碰撞体,再把“编辑目标”切到“碰撞块”。
|
||||||
|
|
||||||
## 导入和调整图形
|
## 导入和调整图形
|
||||||
|
|
||||||
1. 点击左侧“导入图形”,选择一个图片素材。
|
1. 点击左侧“导入图形”,选择一个图片素材。
|
||||||
|
|||||||
+33
-10
@@ -5,7 +5,7 @@ const state = {
|
|||||||
selected: null,
|
selected: null,
|
||||||
view: { x: -80, y: -40, scale: 0.35 },
|
view: { x: -80, y: -40, scale: 0.35 },
|
||||||
gridSize: 10,
|
gridSize: 10,
|
||||||
hitFilter: "collision",
|
hitFilter: "all",
|
||||||
imagePreviews: new Map(),
|
imagePreviews: new Map(),
|
||||||
dragging: null,
|
dragging: null,
|
||||||
};
|
};
|
||||||
@@ -563,8 +563,8 @@ function line(ctx, x1, y1, x2, y2) {
|
|||||||
|
|
||||||
function collectObjects() {
|
function collectObjects() {
|
||||||
const objects = [
|
const objects = [
|
||||||
objectForPoint("player", "玩家出生点", state.map.playerSpawn),
|
objectForPoint("player", "玩家出生点", state.map.playerSpawn, false, "player"),
|
||||||
objectForRect("camera", "摄像机范围", state.map.camera),
|
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.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")));
|
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 {
|
return {
|
||||||
key: id,
|
key: id,
|
||||||
type: "spawn",
|
type,
|
||||||
title,
|
title,
|
||||||
meta: `${fmt(point.x)}, ${fmt(point.y)}`,
|
meta: `${fmt(point.x)}, ${fmt(point.y)}`,
|
||||||
deletable,
|
deletable,
|
||||||
@@ -701,18 +701,18 @@ function getSelectedObject() {
|
|||||||
function onCanvasMouseDown(event) {
|
function onCanvasMouseDown(event) {
|
||||||
const world = screenToWorld(event.offsetX, event.offsetY);
|
const world = screenToWorld(event.offsetX, event.offsetY);
|
||||||
const selected = getSelectedObject();
|
const selected = getSelectedObject();
|
||||||
|
const object = hitTest(world.x, world.y);
|
||||||
if (selected && selected.resize && isInsideResizeHandle(selected, world.x, world.y)) {
|
if (selected && selected.resize && isInsideResizeHandle(selected, world.x, world.y)) {
|
||||||
state.dragging = { resize: true, last: snapPoint(world) };
|
state.dragging = { resize: true, last: snapPoint(world) };
|
||||||
setCanvasCursor("nwse-resize");
|
setCanvasCursor("nwse-resize");
|
||||||
return;
|
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) };
|
state.dragging = { last: snapPoint(world) };
|
||||||
setCanvasCursor("move");
|
setCanvasCursor("move");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const object = hitTest(world.x, world.y);
|
|
||||||
if (object) {
|
if (object) {
|
||||||
state.selected = object.key;
|
state.selected = object.key;
|
||||||
state.dragging = { last: snapPoint(world) };
|
state.dragging = { last: snapPoint(world) };
|
||||||
@@ -768,7 +768,7 @@ function onCanvasWheel(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function hitTest(x, y) {
|
function hitTest(x, y) {
|
||||||
const objects = collectObjects().filter(isHitFilterEnabled).reverse();
|
const objects = hitCandidates();
|
||||||
for (const object of objects) {
|
for (const object of objects) {
|
||||||
const bounds = object.bounds && object.bounds();
|
const bounds = object.bounds && object.bounds();
|
||||||
if (!bounds) continue;
|
if (!bounds) continue;
|
||||||
@@ -777,12 +777,32 @@ function hitTest(x, y) {
|
|||||||
return null;
|
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) {
|
function isInsideBounds(object, x, y) {
|
||||||
const bounds = object.bounds && object.bounds();
|
const bounds = object.bounds && object.bounds();
|
||||||
if (!bounds) return false;
|
if (!bounds) return false;
|
||||||
return x >= bounds.x && x <= bounds.x + bounds.width && y >= bounds.y && y <= bounds.y + bounds.height;
|
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) {
|
function isHitFilterEnabled(object) {
|
||||||
if (state.hitFilter === "all") return true;
|
if (state.hitFilter === "all") return true;
|
||||||
if (state.hitFilter === "zone") return object.type === "zone";
|
if (state.hitFilter === "zone") return object.type === "zone";
|
||||||
@@ -792,15 +812,16 @@ function isHitFilterEnabled(object) {
|
|||||||
function updateCanvasCursor(event) {
|
function updateCanvasCursor(event) {
|
||||||
const world = screenToWorld(event.offsetX, event.offsetY);
|
const world = screenToWorld(event.offsetX, event.offsetY);
|
||||||
const selected = getSelectedObject();
|
const selected = getSelectedObject();
|
||||||
|
const object = hitTest(world.x, world.y);
|
||||||
if (selected && selected.resize && isInsideResizeHandle(selected, world.x, world.y)) {
|
if (selected && selected.resize && isInsideResizeHandle(selected, world.x, world.y)) {
|
||||||
setCanvasCursor("nwse-resize");
|
setCanvasCursor("nwse-resize");
|
||||||
return;
|
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");
|
setCanvasCursor("move");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (hitTest(world.x, world.y)) {
|
if (object) {
|
||||||
setCanvasCursor("move");
|
setCanvasCursor("move");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -815,6 +836,7 @@ function addCollision() {
|
|||||||
const point = snapPoint(viewCenter());
|
const point = snapPoint(viewCenter());
|
||||||
state.map.collisions.push({ x: point.x, y: point.y, width: 160, height: 40 });
|
state.map.collisions.push({ x: point.x, y: point.y, width: 160, height: 40 });
|
||||||
state.selected = `collision:${state.map.collisions.length - 1}`;
|
state.selected = `collision:${state.map.collisions.length - 1}`;
|
||||||
|
state.hitFilter = "collision";
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -878,6 +900,7 @@ function addTileRect() {
|
|||||||
const point = snapPoint(viewCenter());
|
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 } });
|
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.selected = `tile:${layerIndex}:${layer.tileRects.length - 1}`;
|
||||||
|
state.hitFilter = "tile";
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,8 +46,8 @@
|
|||||||
<label>
|
<label>
|
||||||
编辑目标
|
编辑目标
|
||||||
<select id="hitFilterInput">
|
<select id="hitFilterInput">
|
||||||
|
<option value="all">智能选择</option>
|
||||||
<option value="collision">碰撞块</option>
|
<option value="collision">碰撞块</option>
|
||||||
<option value="all">全部对象</option>
|
|
||||||
<option value="tile">铺砖区</option>
|
<option value="tile">铺砖区</option>
|
||||||
<option value="zone">战斗区</option>
|
<option value="zone">战斗区</option>
|
||||||
<option value="spawn">出生点</option>
|
<option value="spawn">出生点</option>
|
||||||
|
|||||||
Reference in New Issue
Block a user