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>

View File

@@ -524,6 +524,11 @@ export class GameEngine {
}
}
// 更新坚果墙体
if (state.nutWalls !== undefined) {
this.scene.updateNutWalls(state.nutWalls)
}
// 更新游戏状态
if (state.gameTime !== undefined) this.gameTime = state.gameTime
if (state.waveNumber !== undefined) this.waveNumber = state.waveNumber

View File

@@ -33,6 +33,7 @@ export class GameScene {
this.loots = new Map() // Map<lootId, {mesh, type}>
this.effects = [] // 特效数组
this.wallMeshes = [] // 墙壁网格
this.nutWalls = new Map() // Map<key, {mesh, healthBar}>
// 摄像机辅助
this.gridHelper = null
@@ -228,6 +229,10 @@ export class GameScene {
const zombieSpawnGeo = new THREE.BoxGeometry(1, 0.15, 1)
const zombieSpawnMat = new THREE.MeshLambertMaterial({ color: 0xff4400, transparent: true, opacity: 0.7 })
// 坚果墙体
const nutWallGeo = new THREE.BoxGeometry(1, 1.2, 1)
const nutWallMat = new THREE.MeshLambertMaterial({ color: 0x8B4513 })
// 遍历地图数据
for (let y = 0; y < GRID_SIZE; y++) {
for (let x = 0; x < GRID_SIZE; x++) {
@@ -256,6 +261,82 @@ export class GameScene {
}
}
/**
* 更新坚果墙体状态
* @param {Array} nutWalls 坚果墙体状态数组 [{x, y, health, maxHealth}]
*/
updateNutWalls(nutWalls) {
const currentKeys = new Set()
for (const wall of nutWalls) {
const key = `${wall.x},${wall.y}`
currentKeys.add(key)
if (this.nutWalls.has(key)) {
// 更新已有墙体的血量表现
const data = this.nutWalls.get(key)
const healthPercent = wall.health / wall.maxHealth
// 根据血量改变颜色:满血棕色 -> 低血红棕色
const r = 0.545 + (1 - healthPercent) * 0.4
const g = 0.271 - (1 - healthPercent) * 0.1
const b = 0.075 - (1 - healthPercent) * 0.05
data.mesh.material.color.setRGB(r, g, b)
// 更新血条
if (data.healthBar) {
data.healthBar.scale.x = healthPercent
data.healthBar.material.color.setHex(healthPercent > 0.5 ? 0x00ff00 : healthPercent > 0.25 ? 0xffff00 : 0xff0000)
}
} else {
// 创建新的坚果墙体
const nutWallGeo = new THREE.BoxGeometry(1, 1.2, 1)
const nutWallMat = new THREE.MeshLambertMaterial({ color: 0x8B4513 })
const mesh = new THREE.Mesh(nutWallGeo, nutWallMat)
mesh.position.set(wall.x + 0.5, 0.6, wall.y + 0.5)
mesh.castShadow = true
mesh.receiveShadow = true
this.scene.add(mesh)
// 创建血条背景
const barBgGeo = new THREE.PlaneGeometry(0.8, 0.1)
const barBgMat = new THREE.MeshBasicMaterial({ color: 0x333333 })
const barBg = new THREE.Mesh(barBgGeo, barBgMat)
barBg.position.set(wall.x + 0.5, 1.3, wall.y + 0.5)
barBg.rotation.x = -Math.PI / 2
this.scene.add(barBg)
// 创建血条前景
const barFgGeo = new THREE.PlaneGeometry(0.8, 0.1)
const barFgMat = new THREE.MeshBasicMaterial({ color: 0x00ff00 })
const barFg = new THREE.Mesh(barFgGeo, barFgMat)
barFg.position.set(wall.x + 0.5, 1.31, wall.y + 0.5)
barFg.rotation.x = -Math.PI / 2
this.scene.add(barFg)
this.nutWalls.set(key, { mesh, healthBar: barFg, barBg })
}
}
// 移除已经不存在的墙体
for (const [key, data] of this.nutWalls) {
if (!currentKeys.has(key)) {
this.scene.remove(data.mesh)
data.mesh.geometry.dispose()
data.mesh.material.dispose()
if (data.healthBar) {
this.scene.remove(data.healthBar)
data.healthBar.geometry.dispose()
data.healthBar.material.dispose()
}
if (data.barBg) {
this.scene.remove(data.barBg)
data.barBg.geometry.dispose()
data.barBg.material.dispose()
}
this.nutWalls.delete(key)
}
}
}
/**
* 创建玩家3D模型
* @param {number} color 玩家颜色

View File

@@ -8,6 +8,10 @@ export default defineConfig({
'/ws': {
target: 'ws://localhost:8080',
ws: true
},
'/api': {
target: 'http://localhost:8081',
changeOrigin: true
}
}
}