206 lines
9.1 KiB
HTML
206 lines
9.1 KiB
HTML
<!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>
|