diff --git a/tools/map_editor/README.md b/tools/map_editor/README.md index 1aa6f7a..fa5b74d 100644 --- a/tools/map_editor/README.md +++ b/tools/map_editor/README.md @@ -1,35 +1,35 @@ -# Map Editor +# 地图编辑器 -Static browser-based editor for the game's `.map` files. +这是一个静态网页地图编辑器,用来编辑游戏使用的 `.map` 文本文件。 -## Run Locally +## 本地运行 -From the repository root: +在仓库根目录执行: ```powershell python -m http.server 8787 ``` -Open: +然后打开: ```text http://127.0.0.1:8787/tools/map_editor/ ``` -The editor can fetch `game/assets/map/stage_01.map` when served from the repo root. It can also open any `.map` file through the file picker. +从仓库根目录启动静态服务时,编辑器可以自动读取 `game/assets/map/stage_01.map`。也可以通过“打开 .map”选择任意 `.map` 文件。 -## Current Scope +## 当前能力 -- Open `.map` text files. -- Visualize world, camera, player spawn, collision, stage rects, tile rects, battle zones, and spawn points. -- Select objects from the canvas or object list. -- Drag objects on the canvas with grid snapping. -- Edit selected object numeric properties. -- Add/delete collision rectangles, tile rectangles, battle zones, and spawn points. -- Download a serialized `.map` file compatible with `StageMapLoader`. +- 打开 `.map` 文本文件。 +- 可视化世界范围、摄像机范围、玩家出生点、碰撞块、场景矩形、铺砖区、战斗区和敌人出生点。 +- 从画布或对象列表选择对象。 +- 在画布上拖动对象,支持网格吸附。 +- 编辑选中对象的数值和文本属性。 +- 新增/删除图块集、图层、碰撞块、铺砖区、战斗区和出生点。 +- 下载兼容 `StageMapLoader` 的 `.map` 文本。 -## Limitations +## 当前限制 -- Browser security prevents direct overwrite of the original local file. Use Download and replace the project `.map` manually, or deploy with a server-side save endpoint later. -- Tileset image preview is not implemented yet; tile rects use fallback colors. -- Undo/redo and multi-select are not implemented yet. +- 浏览器安全限制不允许静态网页直接覆盖本地源文件。当前需要先下载 `.map`,再手动替换项目里的地图文件;部署到服务器后可以增加服务端保存接口。 +- 暂未实现图块集图片预览,铺砖区当前使用 fallback 颜色显示。 +- 暂未实现撤销/重做和多选。 diff --git a/tools/map_editor/app.js b/tools/map_editor/app.js index 009ad3e..a326781 100644 --- a/tools/map_editor/app.js +++ b/tools/map_editor/app.js @@ -75,10 +75,10 @@ async function loadSampleMap() { if (!response.ok) { throw new Error(`HTTP ${response.status}`); } - loadMapText(await response.text(), "Loaded stage_01.map"); + loadMapText(await response.text(), "已加载 stage_01.map"); } catch (error) { - setStatus(`Could not fetch stage_01.map. Use Open .map. ${error.message}`); - loadMapText(defaultMapText(), "Loaded built-in starter map"); + setStatus(`无法读取 stage_01.map,请使用“打开 .map”。${error.message}`); + loadMapText(defaultMapText(), "已加载内置起始地图"); } } @@ -86,7 +86,7 @@ function openFile(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); - reader.onload = () => loadMapText(String(reader.result || ""), `Opened ${file.name}`); + reader.onload = () => loadMapText(String(reader.result || ""), `已打开 ${file.name}`); reader.readAsText(file); } @@ -98,7 +98,7 @@ function loadMapText(text, status) { setStatus(status); refresh(); } catch (error) { - setStatus(`Parse failed: ${error.message}`); + setStatus(`解析失败:${error.message}`); } } @@ -195,7 +195,7 @@ function parseMap(text) { }; map.battleZones.push(currentBattleZone); } else if (command === "spawn") { - if (!currentBattleZone) throw new Error(`Line ${lineNumber}: spawn outside battle_zone`); + if (!currentBattleZone) throw new Error(`第 ${lineNumber} 行:spawn 必须写在 battle_zone 内`); currentBattleZone.spawns.push({ id: requireToken(tokens, 1, lineNumber), x: num(tokens, 2, lineNumber), @@ -204,7 +204,7 @@ function parseMap(text) { } else if (command === "endbattle_zone") { currentBattleZone = null; } else { - throw new Error(`Line ${lineNumber}: unknown command ${command}`); + throw new Error(`第 ${lineNumber} 行:未知命令 ${command}`); } } @@ -303,7 +303,7 @@ function renderPropertyPanel() { const object = getSelectedObject(); elements.propertyPanel.innerHTML = ""; if (!object) { - elements.selectionSummary.textContent = "Nothing selected."; + elements.selectionSummary.textContent = "未选择对象。"; elements.deleteButton.disabled = true; return; } @@ -355,7 +355,7 @@ function drawCanvas() { drawSelection(ctx); ctx.restore(); - elements.viewText.textContent = `zoom ${state.view.scale.toFixed(2)} | grid ${state.gridSize}`; + elements.viewText.textContent = `缩放 ${state.view.scale.toFixed(2)} | 网格 ${state.gridSize}`; } function drawWorld(ctx) { @@ -469,20 +469,20 @@ function line(ctx, x1, y1, x2, y2) { function collectObjects() { const objects = [ - objectForPoint("player", "Player Spawn", state.map.playerSpawn), - objectForRect("camera", "Camera", state.map.camera), + objectForPoint("player", "玩家出生点", state.map.playerSpawn), + objectForRect("camera", "摄像机范围", state.map.camera), ]; - state.map.tilesets.forEach((tileset, index) => objects.push(objectForTileset(`tileset:${index}`, `Tileset ${tileset.id}`, tileset, true))); - state.map.collisions.forEach((rect, index) => objects.push(objectForRect(`collision:${index}`, `Collision ${index + 1}`, rect, 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))); state.map.layers.forEach((layer, layerIndex) => { - objects.push(objectForLayer(`layer:${layerIndex}`, `Layer ${layer.id}`, layer, true)); - layer.rects.forEach((rect, rectIndex) => objects.push(objectForRect(`rect:${layerIndex}:${rectIndex}`, `${layer.id} Rect ${rectIndex + 1}`, rect.bounds, true))); - layer.tileRects.forEach((tileRect, tileIndex) => objects.push(objectForTileRect(`tile:${layerIndex}:${tileIndex}`, `${layer.id} TileRect ${tileIndex + 1}`, tileRect, true))); + 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.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}`, `Battle Zone ${zoneIndex + 1}`, zone.trigger, true, zone.id)); - objects.push(objectForRect(`zone_camera:${zoneIndex}`, `Zone Camera ${zoneIndex + 1}`, zone.camera, false, zone.id)); - zone.spawns.forEach((spawn, spawnIndex) => objects.push(objectForPoint(`spawn:${zoneIndex}:${spawnIndex}`, `Spawn ${spawn.id}`, spawn, true))); + 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)); + zone.spawns.forEach((spawn, spawnIndex) => objects.push(objectForPoint(`spawn:${zoneIndex}:${spawnIndex}`, `出生点 ${spawn.id}`, spawn, true))); }); return objects; } @@ -495,9 +495,9 @@ function objectForTileset(id, title, tileset, deletable) { deletable, fields: () => [ textField("ID", () => tileset.id, (v) => { renameTileset(tileset.id, v || tileset.id); }), - textField("Texture", () => tileset.texture, (v) => { tileset.texture = v || tileset.texture; }), - numberField("Tile W", () => tileset.tileWidth, (v) => { tileset.tileWidth = Math.max(1, v); }), - numberField("Tile H", () => tileset.tileHeight, (v) => { tileset.tileHeight = Math.max(1, v); }), + textField("贴图路径", () => tileset.texture, (v) => { tileset.texture = v || tileset.texture; }), + numberField("图块宽", () => tileset.tileWidth, (v) => { tileset.tileWidth = Math.max(1, v); }), + numberField("图块高", () => tileset.tileHeight, (v) => { tileset.tileHeight = Math.max(1, v); }), ...colorFields(tileset.color, true), ], }; @@ -511,8 +511,8 @@ function objectForLayer(id, title, layer, deletable) { deletable, fields: () => [ textField("ID", () => layer.id, (v) => { layer.id = v || layer.id; }), - textField("Role", () => layer.role, (v) => { layer.role = v; }, ROLE_ORDER), - numberField("Parallax", () => layer.parallax, (v) => { layer.parallax = v; }, "0.05"), + textField("类型", () => layer.role, (v) => { layer.role = v; }, ROLE_ORDER), + numberField("视差", () => layer.parallax, (v) => { layer.parallax = v; }, "0.05"), ], }; } @@ -553,12 +553,12 @@ function objectForTileRect(id, title, tileRect, 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; }, fields: () => [ - textField("Tileset", () => tileRect.tileset, (v) => { tileRect.tileset = v; }, state.map.tilesets.map((item) => item.id)), + textField("图块集", () => tileRect.tileset, (v) => { tileRect.tileset = v; }, state.map.tilesets.map((item) => item.id)), numberField("X", () => tileRect.x, (v) => { tileRect.x = v; }), numberField("Y", () => tileRect.y, (v) => { tileRect.y = v; }), - numberField("Columns", () => tileRect.columns, (v) => { tileRect.columns = Math.max(1, Math.round(v)); }), - numberField("Rows", () => tileRect.rows, (v) => { tileRect.rows = Math.max(1, Math.round(v)); }), - numberField("Tile Index", () => tileRect.tileIndex, (v) => { tileRect.tileIndex = Math.max(0, Math.round(v)); }), + numberField("列数", () => tileRect.columns, (v) => { tileRect.columns = Math.max(1, Math.round(v)); }), + numberField("行数", () => tileRect.rows, (v) => { tileRect.rows = Math.max(1, Math.round(v)); }), + numberField("图块索引", () => tileRect.tileIndex, (v) => { tileRect.tileIndex = Math.max(0, Math.round(v)); }), ...colorFields(tileRect.color, false), ], }; @@ -701,7 +701,7 @@ function deleteSelected() { if (parts[0] === "tileset") { const tileset = state.map.tilesets[Number(parts[1])]; if (tileset && isTilesetUsed(tileset.id)) { - setStatus(`Cannot delete tileset ${tileset.id}: it is used by tile_rect.`); + setStatus(`不能删除图块集 ${tileset.id}:它仍被 tile_rect 使用。`); return; } state.map.tilesets.splice(Number(parts[1]), 1); @@ -800,17 +800,17 @@ function tokenize(line) { } function requireLayer(layer, lineNumber) { - if (!layer) throw new Error(`Line ${lineNumber}: command must be inside layer`); + if (!layer) throw new Error(`第 ${lineNumber} 行:该命令必须写在 layer 内`); } function requireToken(tokens, index, lineNumber) { - if (tokens.length <= index) throw new Error(`Line ${lineNumber}: missing token ${index}`); + if (tokens.length <= index) throw new Error(`第 ${lineNumber} 行:缺少第 ${index} 个参数`); return tokens[index]; } function num(tokens, index, lineNumber) { const value = Number(requireToken(tokens, index, lineNumber)); - if (!Number.isFinite(value)) throw new Error(`Line ${lineNumber}: invalid number ${tokens[index]}`); + if (!Number.isFinite(value)) throw new Error(`第 ${lineNumber} 行:无效数字 ${tokens[index]}`); return value; } @@ -881,7 +881,7 @@ function renameTileset(oldId, newId) { const cleanId = String(newId).trim(); if (!cleanId) return; if (cleanId !== oldId && state.map.tilesets.some((item) => item.id === cleanId)) { - setStatus(`Cannot rename tileset: ${cleanId} already exists.`); + setStatus(`不能重命名图块集:${cleanId} 已存在。`); return; } const tileset = state.map.tilesets.find((item) => item.id === oldId); diff --git a/tools/map_editor/index.html b/tools/map_editor/index.html index 7779a74..bdf53a9 100644 --- a/tools/map_editor/index.html +++ b/tools/map_editor/index.html @@ -1,72 +1,72 @@ - + - NS Unknown Game Map Editor + NS Unknown Game 地图编辑器
-

Map Editor

-

Edits Frostbite2D .map text files used by the game runtime.

+

地图编辑器

+

编辑游戏运行时使用的 Frostbite2D .map 文本地图。

- + - +
- Ready + 就绪
@@ -74,19 +74,19 @@