This commit is contained in:
wfz
2026-04-26 11:00:59 +08:00
parent 7bffe41d41
commit f1a6f0fd75
19 changed files with 1026 additions and 142 deletions

View File

@@ -0,0 +1,205 @@
<!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>