1
This commit is contained in:
205
frontend/map-designer/index.html
Normal file
205
frontend/map-designer/index.html
Normal 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>
|
||||
@@ -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
|
||||
|
||||
@@ -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 玩家颜色
|
||||
|
||||
@@ -8,6 +8,10 @@ export default defineConfig({
|
||||
'/ws': {
|
||||
target: 'ws://localhost:8080',
|
||||
ws: true
|
||||
},
|
||||
'/api': {
|
||||
target: 'http://localhost:8081',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user