Files
zp1/frontend/map-designer/index.html
2026-04-26 11:00:59 +08:00

206 lines
9.1 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>地图设计器</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: Arial, sans-serif; background: #1a1a2e; color: #fff; height: 100vh; display: flex; flex-direction: column; }
.header { background: #16213e; padding: 12px 20px; display: flex; align-items: center; gap: 16px; border-bottom: 2px solid #0f3460; }
.header h1 { font-size: 18px; color: #e94560; }
.toolbar { display: flex; gap: 8px; }
.size-set { display: flex; gap: 8px; align-items: center; margin-left: 20px; }
.size-set label { font-size: 14px; }
.size-set input { width: 60px; padding: 4px 8px; background: #0f3460; border: none; color: #fff; border-radius: 4px; }
.size-set button { padding: 6px 12px; background: #e94560; border: none; color: #fff; border-radius: 4px; cursor: pointer; }
.main { flex: 1; display: flex; overflow: hidden; }
.canvas-wrap { flex: 1; display: flex; align-items: center; justify-content: center; background: #0f0f1a; overflow: auto; padding: 20px; }
#canvas { background: #2d2d44; cursor: crosshair; }
.panel { width: 200px; background: #16213e; padding: 16px; border-left: 1px solid #0f3460; display: flex; flex-direction: column; gap: 16px; }
.panel h3 { font-size: 14px; color: #e94560; margin-bottom: 8px; }
.stat { display: flex; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid #0f3460; }
.stat-label { color: #888; }
.actions { display: flex; flex-direction: column; gap: 8px; }
.actions button { padding: 10px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; }
.btn-save { background: #e94560; color: #fff; }
.btn-clear { background: #16213e; color: #fff; border: 1px solid #0f3460; }
.msg { font-size: 12px; padding: 8px; border-radius: 4px; background: #0f0f1a; }
.msg.error { color: #ff6b6b; }
.msg.ok { color: #4ade80; }
.legend { display: flex; flex-direction: column; gap: 6px; }
.legend-item { display: flex; align-items: center; gap: 8px; font-size: 12px; cursor: pointer; padding: 4px 6px; border-radius: 4px; }
.legend-item:hover { background: #0f3460; }
.legend-item.active { background: #e94560; }
.legend-swatch { width: 16px; height: 16px; border-radius: 3px; }
</style>
</head>
<body>
<div class="header">
<h1>地图设计器</h1>
<div class="size-set">
<label>宽:<input type="number" id="w" value="32" min="8" max="128"></label>
<label>高:<input type="number" id="h" value="32" min="8" max="128"></label>
<button id="apply">应用</button>
</div>
</div>
<div class="main">
<div class="canvas-wrap">
<canvas id="canvas"></canvas>
</div>
<div class="panel">
<div>
<h3>统计</h3>
<div class="stat"><span class="stat-label">静态墙</span><span id="s0">0</span></div>
<div class="stat"><span class="stat-label">坚果墙</span><span id="s1">0</span></div>
<div class="stat"><span class="stat-label">玩家点</span><span id="s2">0/4</span></div>
<div class="stat"><span class="stat-label">出怪点</span><span id="s3">0/7</span></div>
</div>
<div>
<h3>放置</h3>
<div class="legend"><div class="legend-item" data-tool="0"><div class="legend-swatch" style="background:#ffffff;border:1px solid #666"></div>空地</div>
<div class="legend-item active" data-tool="1"><div class="legend-swatch" style="background:#4a4a6a"></div>静态墙</div>
<div class="legend-item" data-tool="2"><div class="legend-swatch" style="background:#8B4513"></div>坚果墙</div>
<div class="legend-item" data-tool="3"><div class="legend-swatch" style="background:#e94560"></div>玩家点</div>
<div class="legend-item" data-tool="4"><div class="legend-swatch" style="background:#4ade80"></div>出怪点</div></div>
</div>
<div class="actions">
<button class="btn-clear" id="clear">清空</button>
<button class="btn-save" id="save">保存JSON</button>
</div>
<div class="msg" id="msg">放置至少1个出怪点和1个玩家点</div>
</div>
</div>
<script>
const COLORS = ['#ffffff','#4a4a6a','#8B4513','#e94560','#4ade80'];
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let w = 32, h = 32, cs = 16, tool = 1;
let cells = [];
function init() {
cells = [];
for (let y = 0; y < h; y++) {
cells[y] = new Array(w).fill(0);
}
canvas.width = w * cs;
canvas.height = h * cs;
draw();
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
ctx.fillStyle = COLORS[cells[y][x]];
ctx.fillRect(x * cs, y * cs, cs, cs);
ctx.strokeStyle = '#222';
ctx.strokeRect(x * cs, y * cs, cs, cs);
}
}
updateStats();
}
function updateStats() {
let c = [0,0,0,0,0];
for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) c[cells[y][x]]++;
document.getElementById('s0').textContent = c[1];
document.getElementById('s1').textContent = c[2];
document.getElementById('s2').textContent = c[3] + '/4';
document.getElementById('s3').textContent = c[4] + '/7';
const msg = document.getElementById('msg');
if (c[3] < 1 || c[4] < 1) {
msg.className = 'msg error';
msg.textContent = '需要至少1个出怪点和1个玩家点';
document.getElementById('save').disabled = true;
} else if (c[3] > 4 || c[4] > 7) {
msg.className = 'msg error';
msg.textContent = c[3] > 4 ? '玩家点最多4个' : '出怪点最多7个';
document.getElementById('save').disabled = true;
} else {
msg.className = 'msg ok';
msg.textContent = '可以保存';
document.getElementById('save').disabled = false;
}
}
function getCell(e) {
const r = canvas.getBoundingClientRect();
const x = Math.floor((e.clientX - r.left) / cs);
const y = Math.floor((e.clientY - r.top) / cs);
if (x >= 0 && x < w && y >= 0 && y < h) return { x, y };
return null;
}
canvas.addEventListener('mousedown', e => {
const c = getCell(e);
if (c) { cells[c.y][c.x] = tool; draw(); }
});
canvas.addEventListener('mousemove', e => {
if (e.buttons) {
const c = getCell(e);
if (c) { cells[c.y][c.x] = tool; draw(); }
}
});
canvas.addEventListener('contextmenu', e => {
e.preventDefault();
const c = getCell(e);
if (c) { cells[c.y][c.x] = 0; draw(); }
});
document.querySelectorAll('.legend-item').forEach(b => {
b.addEventListener('click', () => {
document.querySelectorAll('.legend-item').forEach(x => x.classList.remove('active'));
b.classList.add('active');
tool = parseInt(b.dataset.tool);
});
});
document.getElementById('apply').addEventListener('click', () => {
const nw = parseInt(document.getElementById('w').value) || 32;
const nh = parseInt(document.getElementById('h').value) || 32;
w = Math.max(8, Math.min(128, nw));
h = Math.max(8, Math.min(128, nh));
document.getElementById('w').value = w;
document.getElementById('h').value = h;
cs = Math.max(4, Math.min(20, Math.floor(600 / Math.max(w, h))));
init();
});
document.getElementById('clear').addEventListener('click', () => {
if (confirm('清空?')) init();
});
document.getElementById('save').addEventListener('click', async () => {
const name = prompt('地图名称:', 'my-map') || 'my-map';
const walls = [], ps = [], zs = [];
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
if (cells[y][x] === 1) walls.push({x, y, type: 'static'});
if (cells[y][x] === 2) walls.push({x, y, type: 'nut'});
if (cells[y][x] === 3) ps.push({x, y});
if (cells[y][x] === 4) zs.push({x, y});
}
}
const data = { name, width: w, height: h, walls, playerSpawns: ps, zombieSpawns: zs };
try {
const resp = await fetch('/api/maps', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
if (!resp.ok) throw new Error('保存失败');
const result = await resp.json();
alert('保存成功ID: ' + result.id);
} catch (e) {
alert('保存失败: ' + e.message);
}
});
init();
</script>
</body>
</html>