Improve map editor graphic imports
This commit is contained in:
@@ -94,8 +94,13 @@ http://127.0.0.1:8787/tools/map_editor/
|
||||
- Canvas 可视化 world、camera、player spawn、collision、rect、tile_rect、battle zone、spawn。
|
||||
- 对象列表选择和画布点击选择。
|
||||
- 网格吸附拖动对象。
|
||||
- 通过“编辑目标”过滤画布命中目标,默认优先编辑 `collision`。
|
||||
- 矩形对象支持右下角手柄缩放。
|
||||
- 属性面板编辑选中对象的数值或文本。
|
||||
- 新增/删除 tileset、layer、collision、tile_rect、battle zone、spawn。
|
||||
- 通过“导入图形”创建 `rect_sprite`,并自动追加一个同坐标、同尺寸的 `collision`。
|
||||
- 导入图形在当前浏览器会话内支持图片预览;图形移动和缩放时,同步更新绑定碰撞块。
|
||||
- 重新打开 `.map` 时,如果 `rect_sprite` 与某个 `collision` 坐标尺寸一致,编辑器会自动恢复二者的编辑绑定。
|
||||
- 导出兼容 `StageMapLoader` 的 `.map` 文本。
|
||||
|
||||
浏览器安全限制下,纯静态网页不能直接覆盖本地源文件;当前通过 Download 导出 `.map`。后续部署到服务器时,可以增加一个保存接口,把编辑器导出的文本写回服务端文件或数据库。
|
||||
@@ -112,6 +117,14 @@ http://127.0.0.1:8787/tools/map_editor/
|
||||
|
||||
素材路径仍由 `.map` 和 `game/assets/stage/stage_01/ASSETS.md` 记录。
|
||||
|
||||
导入图形时,静态网页不能直接复制图片文件到仓库。编辑器默认把贴图路径写为:
|
||||
|
||||
```text
|
||||
assets/stage/stage_01/props/<图片文件名>
|
||||
```
|
||||
|
||||
实际运行游戏前,需要把对应图片素材放到该目录。编辑器内的图形-碰撞绑定是工具层能力,导出的 `.map` 仍保持运行时兼容格式,不额外写入私有字段。
|
||||
|
||||
## 首版验收
|
||||
|
||||
完成时应达到:
|
||||
@@ -120,15 +133,18 @@ http://127.0.0.1:8787/tools/map_editor/
|
||||
2. 已完成:能看到地面、平台、battle zone、spawn point。
|
||||
3. 已完成:能新增/移动/删除一个 `collision`。
|
||||
4. 已完成:能新增/移动/删除一个 `tile_rect`。
|
||||
5. 已完成:能导出 `.map`。
|
||||
6. 已完成:编辑器 parser/serializer 烟测通过,保存格式保持 `StageMapLoader` 兼容。
|
||||
7. 待后续地图修改后复测:Windows 构建通过,短启动通过。
|
||||
8. 待后续地图修改后复测:Switch 构建能把保存后的 `.map` 打进 RomFS。
|
||||
5. 已完成:能导入图形,生成 `rect_sprite` 和同尺寸 `collision`。
|
||||
6. 已完成:导入图形支持移动、缩放和同步调整绑定碰撞块。
|
||||
7. 已完成:能导出 `.map`。
|
||||
8. 已完成:编辑器 parser/serializer 烟测通过,保存格式保持 `StageMapLoader` 兼容。
|
||||
9. 待后续地图修改后复测:Windows 构建通过,短启动通过。
|
||||
10. 待后续地图修改后复测:Switch 构建能把保存后的 `.map` 打进 RomFS。
|
||||
|
||||
## 后续增强
|
||||
|
||||
- tile palette。
|
||||
- tileset 图片预览。
|
||||
- 保存接口:部署到服务器后,把 `.map` 文本写回服务端文件或数据库。
|
||||
- battle zone 清场出口配置。
|
||||
- props 图层编辑。
|
||||
- 自动检查视觉 tile 与碰撞误差。
|
||||
|
||||
@@ -26,6 +26,8 @@ http://127.0.0.1:8787/tools/map_editor/
|
||||
- 在画布上拖动对象,支持网格吸附。
|
||||
- 通过“编辑目标”过滤画布选择对象,默认优先编辑碰撞块,避免点到重叠的铺砖区。
|
||||
- 选中矩形对象后,拖动右下角蓝色手柄可以调整宽高。
|
||||
- 通过“导入图形”选择图片,自动生成一个 `rect_sprite` 图形对象和一个同尺寸 `collision`。
|
||||
- 导入的图形会在浏览器内临时预览,拖动或缩放图形时,同步移动或缩放绑定的碰撞块。
|
||||
- 编辑选中对象的数值和文本属性。
|
||||
- 新增/删除图块集、图层、碰撞块、铺砖区、战斗区和出生点。
|
||||
- 下载兼容 `StageMapLoader` 的 `.map` 文本。
|
||||
@@ -38,8 +40,26 @@ http://127.0.0.1:8787/tools/map_editor/
|
||||
4. 拖动右下角蓝色手柄可以调整宽高。
|
||||
5. 也可以在右侧属性面板直接修改 `X`、`Y`、`W`、`H`。
|
||||
|
||||
## 导入和调整图形
|
||||
|
||||
1. 点击左侧“导入图形”,选择一个图片素材。
|
||||
2. 编辑器会把它添加到 `PropsBack` 图层,并在同一位置创建同尺寸碰撞块。
|
||||
3. 新图形会自动选中,左侧“编辑目标”会切到“场景矩形”。
|
||||
4. 拖动图形本体可以移动图形和绑定碰撞块。
|
||||
5. 拖动右下角蓝色手柄可以调整图形和绑定碰撞块尺寸。
|
||||
6. 也可以在右侧属性面板修改 `X`、`Y`、`W`、`H`、颜色和贴图路径。
|
||||
|
||||
静态网页不能自动把图片复制进项目目录。导入后 `.map` 默认写入:
|
||||
|
||||
```text
|
||||
assets/stage/stage_01/props/<图片文件名>
|
||||
```
|
||||
|
||||
实际运行游戏前,需要把图片素材放到这个路径下。
|
||||
|
||||
## 当前限制
|
||||
|
||||
- 浏览器安全限制不允许静态网页直接覆盖本地源文件。当前需要先下载 `.map`,再手动替换项目里的地图文件;部署到服务器后可以增加服务端保存接口。
|
||||
- 已导入图形的预览只在当前浏览器会话内有效;重新打开 `.map` 后,如果浏览器没有重新选择图片,图形会用颜色块显示。
|
||||
- 暂未实现图块集图片预览,铺砖区当前使用 fallback 颜色显示。
|
||||
- 暂未实现撤销/重做和多选。
|
||||
|
||||
+253
-5
@@ -6,6 +6,7 @@ const state = {
|
||||
view: { x: -80, y: -40, scale: 0.35 },
|
||||
gridSize: 10,
|
||||
hitFilter: "collision",
|
||||
imagePreviews: new Map(),
|
||||
dragging: null,
|
||||
};
|
||||
|
||||
@@ -21,6 +22,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
function bindElements() {
|
||||
for (const id of [
|
||||
"mapCanvas", "fileInput", "loadSampleButton", "downloadButton",
|
||||
"graphicFileInput",
|
||||
"mapIdInput", "worldWidthInput", "worldHeightInput", "gridSizeInput",
|
||||
"hitFilterInput",
|
||||
"objectList", "propertyPanel", "selectionSummary", "deleteButton",
|
||||
@@ -36,6 +38,7 @@ function bindEvents() {
|
||||
window.addEventListener("resize", resizeCanvas);
|
||||
elements.loadSampleButton.addEventListener("click", loadSampleMap);
|
||||
elements.fileInput.addEventListener("change", openFile);
|
||||
elements.graphicFileInput.addEventListener("change", importGraphicAsset);
|
||||
elements.downloadButton.addEventListener("click", downloadMap);
|
||||
elements.deleteButton.addEventListener("click", deleteSelected);
|
||||
|
||||
@@ -96,6 +99,32 @@ function openFile(event) {
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
async function importGraphicAsset(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
const previewUrl = URL.createObjectURL(file);
|
||||
const image = await loadImage(previewUrl);
|
||||
const texturePath = defaultGraphicTexturePath(file.name);
|
||||
state.imagePreviews.set(texturePath, image);
|
||||
addGraphicSprite(texturePath, image.naturalWidth || image.width || 64, image.naturalHeight || image.height || 64);
|
||||
setStatus(`已导入图形 ${file.name},并自动创建同尺寸碰撞块。请把素材放到 ${texturePath}`);
|
||||
} catch (error) {
|
||||
setStatus(`导入图形失败:${error.message}`);
|
||||
} finally {
|
||||
event.target.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
function loadImage(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = new Image();
|
||||
image.onload = () => resolve(image);
|
||||
image.onerror = () => reject(new Error("图片读取失败"));
|
||||
image.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
function loadMapText(text, status) {
|
||||
try {
|
||||
state.map = parseMap(text);
|
||||
@@ -214,6 +243,7 @@ function parseMap(text) {
|
||||
}
|
||||
}
|
||||
|
||||
rebuildGraphicCollisionLinks(map);
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -398,11 +428,40 @@ function drawGrid(ctx) {
|
||||
|
||||
function drawLayers(ctx) {
|
||||
for (const layer of sortLayers(state.map.layers)) {
|
||||
for (const rect of layer.rects) drawFilledRect(ctx, rect.bounds, rect.color, 0.55, "#7d8794");
|
||||
for (const rect of layer.rects) drawLayerRect(ctx, rect);
|
||||
for (const tileRect of layer.tileRects) drawTileRect(ctx, tileRect);
|
||||
}
|
||||
}
|
||||
|
||||
function drawLayerRect(ctx, rect) {
|
||||
const image = rect.texture ? state.imagePreviews.get(rect.texture) : null;
|
||||
if (image) {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = rect.color.a === undefined ? 1 : rect.color.a;
|
||||
if (rect.source) {
|
||||
ctx.drawImage(
|
||||
image,
|
||||
rect.source.x,
|
||||
rect.source.y,
|
||||
rect.source.width,
|
||||
rect.source.height,
|
||||
rect.bounds.x,
|
||||
rect.bounds.y,
|
||||
rect.bounds.width,
|
||||
rect.bounds.height
|
||||
);
|
||||
} else {
|
||||
ctx.drawImage(image, rect.bounds.x, rect.bounds.y, rect.bounds.width, rect.bounds.height);
|
||||
}
|
||||
ctx.restore();
|
||||
ctx.strokeStyle = "#7d8794";
|
||||
ctx.lineWidth = 1 / state.view.scale;
|
||||
ctx.strokeRect(rect.bounds.x, rect.bounds.y, rect.bounds.width, rect.bounds.height);
|
||||
return;
|
||||
}
|
||||
drawFilledRect(ctx, rect.bounds, rect.color, 0.55, "#7d8794");
|
||||
}
|
||||
|
||||
function drawTileRect(ctx, tileRect) {
|
||||
const tileset = state.map.tilesets.find((item) => item.id === tileRect.tileset);
|
||||
const tw = tileset ? tileset.tileWidth : 10;
|
||||
@@ -511,7 +570,7 @@ function collectObjects() {
|
||||
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, null, "rect")));
|
||||
layer.rects.forEach((rect, rectIndex) => objects.push(objectForLayerRect(`rect:${layerIndex}:${rectIndex}`, `${layer.id} 图形 ${rectIndex + 1}`, rect, true)));
|
||||
layer.tileRects.forEach((tileRect, tileIndex) => objects.push(objectForTileRect(`tile:${layerIndex}:${tileIndex}`, `${layer.id} 铺砖区 ${tileIndex + 1}`, tileRect, true)));
|
||||
});
|
||||
state.map.battleZones.forEach((zone, zoneIndex) => {
|
||||
@@ -569,6 +628,28 @@ function objectForRect(id, title, rect, deletable = false, meta = null, type = "
|
||||
};
|
||||
}
|
||||
|
||||
function objectForLayerRect(id, title, item, deletable) {
|
||||
return {
|
||||
key: id,
|
||||
type: "rect",
|
||||
title,
|
||||
meta: item.texture ? item.texture : `${fmt(item.bounds.x)}, ${fmt(item.bounds.y)}, ${fmt(item.bounds.width)}x${fmt(item.bounds.height)}`,
|
||||
deletable,
|
||||
bounds: () => item.bounds,
|
||||
move(dx, dy) {
|
||||
item.bounds.x += dx;
|
||||
item.bounds.y += dy;
|
||||
moveLinkedCollision(item, dx, dy);
|
||||
},
|
||||
resize(dw, dh) {
|
||||
item.bounds.width = Math.max(1, item.bounds.width + dw);
|
||||
item.bounds.height = Math.max(1, item.bounds.height + dh);
|
||||
resizeLinkedCollision(item, item.bounds.width, item.bounds.height);
|
||||
},
|
||||
fields: () => layerRectFields(item),
|
||||
};
|
||||
}
|
||||
|
||||
function objectForPoint(id, title, point, deletable = false) {
|
||||
return {
|
||||
key: id,
|
||||
@@ -625,6 +706,11 @@ function onCanvasMouseDown(event) {
|
||||
setCanvasCursor("nwse-resize");
|
||||
return;
|
||||
}
|
||||
if (selected && selected.move && isInsideBounds(selected, world.x, world.y)) {
|
||||
state.dragging = { last: snapPoint(world) };
|
||||
setCanvasCursor("move");
|
||||
return;
|
||||
}
|
||||
|
||||
const object = hitTest(world.x, world.y);
|
||||
if (object) {
|
||||
@@ -691,6 +777,12 @@ function hitTest(x, y) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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 isHitFilterEnabled(object) {
|
||||
if (state.hitFilter === "all") return true;
|
||||
if (state.hitFilter === "zone") return object.type === "zone";
|
||||
@@ -704,6 +796,10 @@ function updateCanvasCursor(event) {
|
||||
setCanvasCursor("nwse-resize");
|
||||
return;
|
||||
}
|
||||
if (selected && selected.move && isInsideBounds(selected, world.x, world.y)) {
|
||||
setCanvasCursor("move");
|
||||
return;
|
||||
}
|
||||
if (hitTest(world.x, world.y)) {
|
||||
setCanvasCursor("move");
|
||||
return;
|
||||
@@ -722,6 +818,30 @@ function addCollision() {
|
||||
refresh();
|
||||
}
|
||||
|
||||
function addGraphicSprite(texturePath, width, height) {
|
||||
const layerIndex = ensurePropsBackLayer();
|
||||
const layer = state.map.layers[layerIndex];
|
||||
const point = snapPoint(viewCenter());
|
||||
const bounds = {
|
||||
x: point.x,
|
||||
y: point.y,
|
||||
width: Math.max(1, Math.round(width)),
|
||||
height: Math.max(1, Math.round(height)),
|
||||
};
|
||||
const collision = { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height };
|
||||
state.map.collisions.push(collision);
|
||||
layer.rects.push({
|
||||
bounds,
|
||||
color: { r: 1, g: 1, b: 1, a: 1 },
|
||||
texture: texturePath,
|
||||
source: null,
|
||||
linkedCollisionIndex: state.map.collisions.length - 1,
|
||||
});
|
||||
state.selected = `rect:${layerIndex}:${layer.rects.length - 1}`;
|
||||
state.hitFilter = "rect";
|
||||
refresh();
|
||||
}
|
||||
|
||||
function addTileset() {
|
||||
const index = state.map.tilesets.length + 1;
|
||||
state.map.tilesets.push({
|
||||
@@ -786,7 +906,7 @@ function addSpawn() {
|
||||
function deleteSelected() {
|
||||
if (!state.selected) return;
|
||||
const parts = state.selected.split(":");
|
||||
if (parts[0] === "collision") state.map.collisions.splice(Number(parts[1]), 1);
|
||||
if (parts[0] === "collision") deleteCollision(Number(parts[1]));
|
||||
if (parts[0] === "tileset") {
|
||||
const tileset = state.map.tilesets[Number(parts[1])];
|
||||
if (tileset && isTilesetUsed(tileset.id)) {
|
||||
@@ -795,8 +915,8 @@ function deleteSelected() {
|
||||
}
|
||||
state.map.tilesets.splice(Number(parts[1]), 1);
|
||||
}
|
||||
if (parts[0] === "layer") state.map.layers.splice(Number(parts[1]), 1);
|
||||
if (parts[0] === "rect") state.map.layers[Number(parts[1])].rects.splice(Number(parts[2]), 1);
|
||||
if (parts[0] === "layer") deleteLayer(Number(parts[1]));
|
||||
if (parts[0] === "rect") deleteLayerRect(Number(parts[1]), Number(parts[2]));
|
||||
if (parts[0] === "tile") state.map.layers[Number(parts[1])].tileRects.splice(Number(parts[2]), 1);
|
||||
if (parts[0] === "zone") state.map.battleZones.splice(Number(parts[1]), 1);
|
||||
if (parts[0] === "spawn") state.map.battleZones[Number(parts[1])].spawns.splice(Number(parts[2]), 1);
|
||||
@@ -844,6 +964,35 @@ function rectFields(rect) {
|
||||
];
|
||||
}
|
||||
|
||||
function layerRectFields(item) {
|
||||
return [
|
||||
numberField("X", () => item.bounds.x, (v) => {
|
||||
const next = Number.isFinite(v) ? v : item.bounds.x;
|
||||
moveLinkedCollision(item, next - item.bounds.x, 0);
|
||||
item.bounds.x = next;
|
||||
}),
|
||||
numberField("Y", () => item.bounds.y, (v) => {
|
||||
const next = Number.isFinite(v) ? v : item.bounds.y;
|
||||
moveLinkedCollision(item, 0, next - item.bounds.y);
|
||||
item.bounds.y = next;
|
||||
}),
|
||||
numberField("W", () => item.bounds.width, (v) => {
|
||||
item.bounds.width = Math.max(1, v);
|
||||
resizeLinkedCollision(item, item.bounds.width, item.bounds.height);
|
||||
}),
|
||||
numberField("H", () => item.bounds.height, (v) => {
|
||||
item.bounds.height = Math.max(1, v);
|
||||
resizeLinkedCollision(item, item.bounds.width, item.bounds.height);
|
||||
}),
|
||||
textField("贴图路径", () => item.texture || "", (v) => {
|
||||
const next = String(v || "").trim();
|
||||
if (next) item.texture = next;
|
||||
else delete item.texture;
|
||||
}),
|
||||
...colorFields(item.color, true),
|
||||
];
|
||||
}
|
||||
|
||||
function pointFields(point) {
|
||||
return [
|
||||
numberField("X", () => point.x, (v) => { point.x = v; }),
|
||||
@@ -875,6 +1024,43 @@ function ensureLevelVisualLayer() {
|
||||
}
|
||||
}
|
||||
|
||||
function ensurePropsBackLayer() {
|
||||
let index = state.map.layers.findIndex((layer) => layer.role === "PropsBack");
|
||||
if (index >= 0) return index;
|
||||
state.map.layers.push({ id: "props_back", role: "PropsBack", parallax: 1, rects: [], tileRects: [] });
|
||||
index = state.map.layers.length - 1;
|
||||
return index;
|
||||
}
|
||||
|
||||
function defaultGraphicTexturePath(fileName) {
|
||||
return `assets/stage/stage_01/props/${safeAssetFileName(fileName)}`;
|
||||
}
|
||||
|
||||
function safeAssetFileName(fileName) {
|
||||
const clean = String(fileName || "graphic.png").replace(/\\/g, "/").split("/").pop();
|
||||
return clean.replace(/[^A-Za-z0-9._-]/g, "_") || "graphic.png";
|
||||
}
|
||||
|
||||
function linkedCollision(item) {
|
||||
const index = Number(item.linkedCollisionIndex);
|
||||
if (!Number.isInteger(index)) return null;
|
||||
return state.map.collisions[index] || null;
|
||||
}
|
||||
|
||||
function moveLinkedCollision(item, dx, dy) {
|
||||
const collision = linkedCollision(item);
|
||||
if (!collision) return;
|
||||
collision.x += dx;
|
||||
collision.y += dy;
|
||||
}
|
||||
|
||||
function resizeLinkedCollision(item, width, height) {
|
||||
const collision = linkedCollision(item);
|
||||
if (!collision) return;
|
||||
collision.width = Math.max(1, width);
|
||||
collision.height = Math.max(1, height);
|
||||
}
|
||||
|
||||
function sortLayers(layers) {
|
||||
return [...layers].sort((a, b) => ROLE_ORDER.indexOf(a.role) - ROLE_ORDER.indexOf(b.role));
|
||||
}
|
||||
@@ -987,6 +1173,68 @@ function isTilesetUsed(id) {
|
||||
return state.map.layers.some((layer) => layer.tileRects.some((tileRect) => tileRect.tileset === id));
|
||||
}
|
||||
|
||||
function deleteLayer(layerIndex) {
|
||||
const layer = state.map.layers[layerIndex];
|
||||
if (!layer) return;
|
||||
const linkedIndexes = layer.rects
|
||||
.map((item) => Number(item.linkedCollisionIndex))
|
||||
.filter((index) => Number.isInteger(index))
|
||||
.sort((a, b) => b - a);
|
||||
state.map.layers.splice(layerIndex, 1);
|
||||
for (const index of linkedIndexes) deleteCollision(index);
|
||||
}
|
||||
|
||||
function deleteLayerRect(layerIndex, rectIndex) {
|
||||
const layer = state.map.layers[layerIndex];
|
||||
if (!layer) return;
|
||||
const item = layer.rects[rectIndex];
|
||||
if (!item) return;
|
||||
const linkedIndex = Number(item.linkedCollisionIndex);
|
||||
layer.rects.splice(rectIndex, 1);
|
||||
if (Number.isInteger(linkedIndex)) deleteCollision(linkedIndex);
|
||||
}
|
||||
|
||||
function deleteCollision(index) {
|
||||
if (index < 0 || index >= state.map.collisions.length) return;
|
||||
state.map.collisions.splice(index, 1);
|
||||
for (const layer of state.map.layers) {
|
||||
for (const item of layer.rects) {
|
||||
const linkedIndex = Number(item.linkedCollisionIndex);
|
||||
if (!Number.isInteger(linkedIndex)) continue;
|
||||
if (linkedIndex === index) delete item.linkedCollisionIndex;
|
||||
else if (linkedIndex > index) item.linkedCollisionIndex = linkedIndex - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function rebuildGraphicCollisionLinks(map) {
|
||||
const used = new Set();
|
||||
for (const layer of map.layers) {
|
||||
for (const item of layer.rects) {
|
||||
delete item.linkedCollisionIndex;
|
||||
if (!item.texture) continue;
|
||||
const index = map.collisions.findIndex((collision, collisionIndex) => {
|
||||
return !used.has(collisionIndex) && sameRect(collision, item.bounds);
|
||||
});
|
||||
if (index >= 0) {
|
||||
item.linkedCollisionIndex = index;
|
||||
used.add(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sameRect(a, b) {
|
||||
return nearlyEqual(a.x, b.x) &&
|
||||
nearlyEqual(a.y, b.y) &&
|
||||
nearlyEqual(a.width, b.width) &&
|
||||
nearlyEqual(a.height, b.height);
|
||||
}
|
||||
|
||||
function nearlyEqual(a, b) {
|
||||
return Math.abs(Number(a) - Number(b)) < 0.001;
|
||||
}
|
||||
|
||||
function defaultMapText() {
|
||||
return `map stage\nworld 1280 720\ncamera 0 0 1280 720\nplayer_spawn 120 520\n\ncollision 0 600 1280 80\n\ntileset graybox assets/stage/stage_01/tiles/graybox.png 10 10 0.62 0.68 0.73 1\n\nlayer level_visual LevelVisual 1\ntile_rect graybox 0 600 128 8 0 0.62 0.68 0.73\nendlayer\n`;
|
||||
}
|
||||
|
||||
@@ -64,6 +64,10 @@
|
||||
<button id="addLayerButton" type="button">图层</button>
|
||||
<button id="addCollisionButton" type="button">碰撞块</button>
|
||||
<button id="addTileRectButton" type="button">铺砖区</button>
|
||||
<label class="file-button">
|
||||
导入图形
|
||||
<input id="graphicFileInput" type="file" accept="image/*">
|
||||
</label>
|
||||
<button id="addBattleZoneButton" type="button">战斗区</button>
|
||||
<button id="addSpawnButton" type="button">出生点</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user