<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1.0, minimum-scale=1.0, shrink-to-fit=yes">
<title>🏰 阳光防线 · 无尽塔防</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: linear-gradient(135deg, #87CEEB 0%, #98FB98 30%, #90EE90 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Segoe UI', 'Microsoft YaHei', 'PingFang SC', sans-serif;
padding: 10px;
margin: 0;
overflow-x: hidden;
}
.game-container {
background: rgba(255, 255, 255, 0.25);
border-radius: 24px;
padding: 16px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2), 0 0 0 3px rgba(255, 255, 255, 0.5);
backdrop-filter: blur(12px);
width: 100%;
max-width: 1200px;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
transition: all 0.3s ease;
}
.canvas-wrapper {
width: 100%;
position: relative;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}
canvas {
display: block;
width: 100%;
height: auto;
cursor: crosshair;
background: #7ec850;
}
.fullscreen-btn {
position: absolute;
top: 24px;
right: 24px;
z-index: 10;
width: 44px;
height: 44px;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.9);
background: rgba(255, 255, 255, 0.8);
cursor: pointer;
font-size: 22px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(5px);
color: #2d5016;
line-height: 1;
}
.fullscreen-btn:hover {
background: #ffffff;
transform: scale(1.1);
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.3);
}
.sound-btn {
position: absolute;
top: 24px;
right: 78px;
z-index: 10;
width: 44px;
height: 44px;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.9);
background: rgba(255, 255, 255, 0.8);
cursor: pointer;
font-size: 20px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(5px);
color: #2d5016;
line-height: 1;
}
.sound-btn:hover {
background: #ffffff;
transform: scale(1.1);
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.3);
}
.sound-btn.muted {
background: #ffcccc;
color: #cc0000;
}
.panel {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 14px;
flex-wrap: wrap;
gap: 10px;
width: 100%;
}
.stats {
display: flex;
gap: 12px;
font-size: clamp(14px, 2.5vw, 18px);
font-weight: bold;
color: #2d5016;
flex-wrap: wrap;
}
.stat-item {
background: rgba(255, 255, 255, 0.7);
padding: 8px 14px;
border-radius: 25px;
border: 2px solid rgba(100, 180, 50, 0.6);
box-shadow: 0 3px 8px rgba(0,0,0,0.1);
display: flex;
align-items: center;
gap: 4px;
white-space: nowrap;
}
.tower-buttons {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.tower-btn {
padding: 9px 13px;
border-radius: 25px;
border: 2px solid rgba(255, 255, 255, 0.8);
background: rgba(255, 255, 255, 0.7);
color: #2d5016;
font-weight: bold;
cursor: pointer;
transition: all 0.3s;
font-size: clamp(11px, 1.8vw, 14px);
white-space: nowrap;
box-shadow: 0 3px 8px rgba(0,0,0,0.1);
}
.tower-btn:hover {
background: #c8e6a0;
transform: translateY(-2px);
box-shadow: 0 6px 18px rgba(0,0,0,0.2);
}
.tower-btn.active {
background: #4CAF50;
border-color: #ffffff;
color: white;
box-shadow: 0 0 20px rgba(76, 175, 80, 0.6);
transform: scale(1.05);
}
.action-btns {
display: flex;
gap: 8px;
flex-wrap: wrap;
align-items: center;
}
.btn {
padding: 9px 18px;
border-radius: 25px;
border: 2px solid rgba(255, 255, 255, 0.8);
background: rgba(255, 255, 255, 0.8);
color: #2d5016;
font-weight: bold;
cursor: pointer;
transition: all 0.3s;
font-size: clamp(12px, 2vw, 15px);
box-shadow: 0 3px 8px rgba(0,0,0,0.1);
white-space: nowrap;
}
.btn:hover {
background: #d4edda;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.btn.start {
background: #FF9800;
border-color: #ffffff;
color: white;
font-weight: bold;
}
.btn.start:hover {
background: #F57C00;
box-shadow: 0 5px 20px rgba(255, 152, 0, 0.5);
}
.btn.continue {
background: #4CAF50;
border-color: #ffffff;
color: white;
font-weight: bold;
display: none;
}
.btn.continue:hover {
background: #388E3C;
box-shadow: 0 5px 20px rgba(76, 175, 80, 0.5);
}
.btn.range-toggle {
background: #9C27B0;
border-color: #ffffff;
color: white;
font-weight: bold;
}
.btn.range-toggle:hover {
background: #7B1FA2;
box-shadow: 0 5px 20px rgba(156, 39, 176, 0.5);
}
.btn.range-toggle.active-range {
background: #E040FB;
box-shadow: 0 0 20px rgba(224, 64, 251, 0.7);
}
.info-text {
color: #3e6b27;
font-size: clamp(10px, 1.6vw, 13px);
margin-top: 8px;
text-align: center;
background: rgba(255,255,255,0.5);
padding: 6px 16px;
border-radius: 15px;
}
@media (orientation: landscape) and (min-width: 800px) {
.game-container {
flex-direction: row;
align-items: stretch;
gap: 16px;
padding: 16px;
}
.canvas-wrapper {
flex: 1;
min-width: 0;
}
.game-sidebar {
display: flex;
flex-direction: column;
justify-content: center;
gap: 12px;
width: 260px;
min-width: 240px;
}
.panel {
flex-direction: column;
align-items: stretch;
margin-top: 0;
gap: 10px;
}
.stats {
flex-direction: column;
gap: 8px;
}
.stat-item {
justify-content: center;
font-size: 16px;
}
.tower-buttons {
flex-direction: column;
gap: 6px;
}
.tower-btn {
text-align: center;
font-size: 14px;
padding: 11px 16px;
}
.action-btns {
flex-direction: column;
gap: 6px;
}
.btn {
text-align: center;
font-size: 14px;
padding: 11px 18px;
}
.info-text {
margin-top: 0;
}
.fullscreen-btn {
top: 24px;
right: 284px;
}
.sound-btn {
top: 24px;
right: 338px;
}
}
@media (max-width: 600px) {
.game-container {
padding: 10px;
border-radius: 16px;
}
.panel {
gap: 6px;
}
.tower-btn {
padding: 7px 10px;
}
.fullscreen-btn {
top: 16px;
right: 16px;
width: 36px;
height: 36px;
font-size: 18px;
}
.sound-btn {
top: 16px;
right: 60px;
width: 36px;
height: 36px;
font-size: 16px;
}
}
</style>
</head>
<body>
<div class="game-container" id="gameContainer">
<button class="fullscreen-btn" id="fullscreenBtn" title="全屏模式">⛶</button>
<button class="sound-btn" id="soundBtn" title="音效开关">🔊</button>
<div class="canvas-wrapper">
<canvas id="gameCanvas"></canvas>
</div>
<div class="game-sidebar" id="gameSidebar">
<div class="panel">
<div class="stats">
<div class="stat-item">❤️ <span id="livesDisplay">20</span></div>
<div class="stat-item">💰 <span id="moneyDisplay">400</span></div>
<div class="stat-item">🌊 <span id="waveDisplay">1</span>/10</div>
</div>
<div class="tower-buttons">
<button class="tower-btn active" data-type="arrow">🏹 箭塔 (80)</button>
<button class="tower-btn" data-type="cannon">💣 炮塔 (150)</button>
<button class="tower-btn" data-type="ice">❄️ 冰塔 (120)</button>
<button class="tower-btn" data-type="lightning">⚡ 电塔 (200)</button>
</div>
<div class="action-btns">
<button class="btn range-toggle active-range" id="rangeToggleBtn" title="显示/隐藏射击范围">🎯 范围</button>
<button class="btn start" id="startWaveBtn">▶ 开始波次</button>
<button class="btn continue" id="continueBtn">🔄 继续10关</button>
<button class="btn" id="resetBtn">🔄 重新开始</button>
</div>
</div>
<div class="info-text">
💡 左键建造/升级 | 右键拆除炮塔 | 🎯切换范围 | 🔊音效
</div>
</div>
</div>
<script>
(function() {
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const livesDisplay = document.getElementById('livesDisplay');
const moneyDisplay = document.getElementById('moneyDisplay');
const waveDisplay = document.getElementById('waveDisplay');
const rangeToggleBtn = document.getElementById('rangeToggleBtn');
const fullscreenBtn = document.getElementById('fullscreenBtn');
const soundBtn = document.getElementById('soundBtn');
const gameContainer = document.getElementById('gameContainer');
const startWaveBtn = document.getElementById('startWaveBtn');
const continueBtn = document.getElementById('continueBtn');
const GRID_SIZE = 64;
const COLS = 10;
const ROWS = 8;
const MAP_W = COLS * GRID_SIZE;
const MAP_H = ROWS * GRID_SIZE;
canvas.width = MAP_W;
canvas.height = MAP_H;
let lives = 20;
let money = 400;
let wave = 1;
let totalWavesCompleted = 0;
let gameOver = false;
let waveActive = false;
let selectedTowerType = 'arrow';
let showRange = true;
let isFullscreen = false;
let soundEnabled = true;
let enemies = [];
let towers = [];
let bullets = [];
let particles = [];
let pathCells = [];
let spawnPoint = {col: 0, row: 3};
let endPoint = {col: 9, row: 4};
let gridMap = [];
// 敌人生成队列
let spawnQueue = [];
let spawnTimer = 0;
let spawnInterval = 30; // 生成间隔(帧数)
let audioCtx = null;
function initAudio() {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
}
function playSound(frequency, type, duration, volume = 0.12, decay = true) {
if (!soundEnabled || !audioCtx) return;
try {
const oscillator = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
oscillator.type = type;
oscillator.frequency.setValueAtTime(frequency, audioCtx.currentTime);
gainNode.gain.setValueAtTime(volume, audioCtx.currentTime);
if (decay) {
gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + duration);
}
oscillator.connect(gainNode);
gainNode.connect(audioCtx.destination);
oscillator.start(audioCtx.currentTime);
oscillator.stop(audioCtx.currentTime + duration);
} catch(e) {}
}
function sfxArrowHit() { playSound(800, 'square', 0.06, 0.06); }
function sfxCannonHit() { playSound(80, 'sawtooth', 0.2, 0.15); }
function sfxIceHit() { playSound(2000, 'sine', 0.08, 0.05); }
function sfxLightningHit() { playSound(300, 'sawtooth', 0.1, 0.08); }
function sfxBuild() { playSound(500, 'sine', 0.08, 0.08); }
function sfxUpgrade() { playSound(400, 'sine', 0.06, 0.06); setTimeout(() => playSound(800, 'sine', 0.08, 0.08), 80); }
function sfxRemove() { playSound(300, 'triangle', 0.12, 0.08); }
function sfxEnemyDie() { playSound(200, 'square', 0.08, 0.06); }
function sfxEnemyReachEnd() { playSound(150, 'sawtooth', 0.15, 0.12); }
function sfxWaveStart() { playSound(300, 'sine', 0.1, 0.12); }
function sfxWaveComplete() { playSound(500, 'sine', 0.08, 0.15); }
soundBtn.addEventListener('click', (e) => {
e.stopPropagation();
soundEnabled = !soundEnabled;
if (soundEnabled) {
soundBtn.classList.remove('muted');
soundBtn.textContent = '🔊';
initAudio();
} else {
soundBtn.classList.add('muted');
soundBtn.textContent = '🔇';
}
});
const towerDefs = {
arrow: {
name: '箭塔', cost: 80, range: 2.5, damage: 20, cooldown: 18,
color: '#2196F3', bulletColor: '#64B5F6', bulletSpeed: 8,
upgradeCost: 60, maxLevel: 5, shape: 'arrow', hitSound: sfxArrowHit
},
cannon: {
name: '炮塔', cost: 150, range: 2.0, damage: 55, cooldown: 40,
color: '#FF5722', bulletColor: '#FF8A65', bulletSpeed: 6,
upgradeCost: 100, maxLevel: 4, splash: 1.2, shape: 'cannon', hitSound: sfxCannonHit
},
ice: {
name: '冰塔', cost: 120, range: 2.3, damage: 12, cooldown: 22,
color: '#00BCD4', bulletColor: '#80DEEA', bulletSpeed: 7,
upgradeCost: 80, maxLevel: 5, slow: 0.5, shape: 'ice', hitSound: sfxIceHit
},
lightning: {
name: '电塔', cost: 200, range: 2.8, damage: 35, cooldown: 25,
color: '#FFC107', bulletColor: '#FFE082', bulletSpeed: 15,
upgradeCost: 130, maxLevel: 4, chain: 2, shape: 'lightning', hitSound: sfxLightningHit
}
};
const enemyTypes = {
normal: {color: '#E53935', speed: 1.8, hp: 50, reward: 30, size: 14},
fast: {color: '#FB8C00', speed: 3.2, hp: 30, reward: 25, size: 11},
tank: {color: '#8E24AA', speed: 1.2, hp: 130, reward: 55, size: 18},
boss: {color: '#D32F2F', speed: 1.0, hp: 400, reward: 150, size: 22}
};
function toggleFullscreen() {
const fe = document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement;
if (!fe) {
if (gameContainer.requestFullscreen) gameContainer.requestFullscreen().catch(err => {});
else if (gameContainer.webkitRequestFullscreen) gameContainer.webkitRequestFullscreen();
else if (gameContainer.msRequestFullscreen) gameContainer.msRequestFullscreen();
} else {
if (document.exitFullscreen) document.exitFullscreen();
else if (document.webkitExitFullscreen) document.webkitExitFullscreen();
else if (document.msExitFullscreen) document.msExitFullscreen();
}
}
function handleFullscreenChange() {
const fe = document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement;
isFullscreen = !!fe;
fullscreenBtn.textContent = isFullscreen ? '✕' : '⛶';
}
fullscreenBtn.addEventListener('click', (e) => { e.stopPropagation(); initAudio(); toggleFullscreen(); });
document.addEventListener('fullscreenchange', handleFullscreenChange);
document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
document.addEventListener('msfullscreenchange', handleFullscreenChange);
document.addEventListener('keydown', (e) => { if (e.key === 'F11') setTimeout(handleFullscreenChange, 300); });
rangeToggleBtn.addEventListener('click', () => {
showRange = !showRange;
if (showRange) { rangeToggleBtn.classList.add('active-range'); rangeToggleBtn.textContent = '🎯 范围'; }
else { rangeToggleBtn.classList.remove('active-range'); rangeToggleBtn.textContent = '🎯 隐藏'; }
});
function initGridMap() {
gridMap = Array(ROWS).fill().map(() => Array(COLS).fill('empty'));
}
function generateRandomPath() {
const startCol = 0;
const startRow = Math.floor(Math.random() * (ROWS - 2)) + 1;
const endCol = COLS - 1;
const endRow = Math.floor(Math.random() * (ROWS - 2)) + 1;
spawnPoint = {col: startCol, row: startRow};
endPoint = {col: endCol, row: endRow};
let visited = new Set();
let parent = new Map();
let queue = [{col: startCol, row: startRow}];
visited.add(`${startCol},${startRow}`);
let found = false;
while (queue.length > 0 && !found) {
let current = queue.shift();
let directions = [{dc: 1, dr: 0}, {dc: 0, dr: 1}, {dc: 0, dr: -1}, {dc: -1, dr: 0}];
for (let i = directions.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[directions[i], directions[j]] = [directions[j], directions[i]];
}
for (let dir of directions) {
let nc = current.col + dir.dc;
let nr = current.row + dir.dr;
let key = `${nc},${nr}`;
if (nc >= 0 && nc < COLS && nr >= 0 && nr < ROWS && !visited.has(key)) {
visited.add(key);
parent.set(key, `${current.col},${current.row}`);
queue.push({col: nc, row: nr});
if (nc === endCol && nr === endRow) { found = true; break; }
}
}
}
let path = [];
let currentKey = `${endCol},${endRow}`;
while (currentKey !== `${startCol},${startRow}`) {
let [c, r] = currentKey.split(',').map(Number);
path.push({col: c, row: r});
currentKey = parent.get(currentKey);
if (!currentKey) break;
}
path.push({col: startCol, row: startRow});
path.reverse();
let pathSet = new Set();
pathCells = [];
for (let p of path) {
let key = `${p.col},${p.row}`;
if (!pathSet.has(key)) {
pathSet.add(key);
pathCells.push(p);
}
}
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
gridMap[r][c] = pathSet.has(`${c},${r}`) ? 'path' : 'empty';
}
}
towers = towers.filter(t => {
if (pathSet.has(`${t.col},${t.row}`)) {
let def = towerDefs[t.type];
let refund = Math.floor(def.cost * 0.5);
for (let lv = 2; lv <= t.level; lv++) refund += Math.floor(def.upgradeCost * (lv - 1) * 0.5);
money += refund;
return false;
}
return true;
});
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (gridMap[r][c] !== 'path') gridMap[r][c] = 'empty';
}
}
for (let t of towers) gridMap[t.row][t.col] = 'tower';
}
// 🔧 修复:正确的敌人生成数量和频率
function spawnWave(waveNum, loopCount = 0) {
let enemyList = [];
let difficultyMult = 1 + loopCount * 0.5;
// 修正敌人数量计算,使其更合理
let baseCount = Math.max(3, Math.floor(3 + (waveNum - 1) * 1.2)); // 第1波3个,每波递增
let count = Math.floor(baseCount * difficultyMult);
console.log(`🌊 第${waveNum}波,生成${count}个敌人 (难度倍率: ${difficultyMult})`);
for (let i = 0; i < count; i++) {
let type = 'normal';
let r = Math.random();
if (waveNum >= 9 && i % 3 === 0) type = 'boss';
else if (waveNum >= 6 && r < 0.15) type = 'tank';
else if (waveNum >= 3 && r < 0.3) type = 'fast';
let stats = enemyTypes[type];
let hpMult = (1 + (waveNum - 1) * 0.25) * difficultyMult;
enemyList.push({
x: spawnPoint.col * GRID_SIZE + GRID_SIZE / 2,
y: spawnPoint.row * GRID_SIZE + GRID_SIZE / 2,
type: type,
hp: Math.floor(stats.hp * hpMult),
maxHp: Math.floor(stats.hp * hpMult),
speed: stats.speed * 0.8,
color: stats.color,
size: stats.size,
reward: Math.floor(stats.reward * difficultyMult),
pathIndex: 0,
slowTimer: 0,
originalSpeed: stats.speed * 0.8
});
}
return enemyList;
}
function spawnEnemyFromQueue() {
if (spawnQueue.length === 0) return;
let enemy = spawnQueue.shift();
enemies.push(enemy);
spawnTimer = spawnInterval; // 重置生成计时器
}
function moveEnemy(enemy) {
if (enemy.pathIndex >= pathCells.length) {
lives--;
spawnParticles(enemy.x, enemy.y, '#ff3333', 8, 2);
sfxEnemyReachEnd();
return true;
}
let target = pathCells[enemy.pathIndex];
let tx = target.col * GRID_SIZE + GRID_SIZE / 2;
let ty = target.row * GRID_SIZE + GRID_SIZE / 2;
let dx = tx - enemy.x;
let dy = ty - enemy.y;
let dist = Math.hypot(dx, dy);
if (dist < 3) {
enemy.pathIndex++;
if (enemy.pathIndex >= pathCells.length) {
lives--;
spawnParticles(enemy.x, enemy.y, '#ff3333', 8, 2);
sfxEnemyReachEnd();
return true;
}
target = pathCells[enemy.pathIndex];
tx = target.col * GRID_SIZE + GRID_SIZE / 2;
ty = target.row * GRID_SIZE + GRID_SIZE / 2;
dx = tx - enemy.x;
dy = ty - enemy.y;
dist = Math.hypot(dx, dy);
}
if (dist > 0) {
enemy.x += (dx / dist) * enemy.speed;
enemy.y += (dy / dist) * enemy.speed;
}
return false;
}
function spawnParticles(x, y, color, count = 6, speed = 2) {
for (let i = 0; i < count; i++) {
let angle = Math.random() * Math.PI * 2;
let sp = Math.random() * speed + 0.5;
particles.push({
x, y, vx: Math.cos(angle) * sp, vy: Math.sin(angle) * sp,
life: 0.4 + Math.random() * 0.4, maxLife: 0.4 + Math.random() * 0.4,
color: color, size: 1.5 + Math.random() * 2
});
}
}
function updateTowers() {
for (let tower of towers) {
tower.cooldownCounter = (tower.cooldownCounter || 0) - 1;
if (tower.cooldownCounter > 0) continue;
let target = null;
let bestProgress = -1;
for (let enemy of enemies) {
let dx = enemy.x - (tower.col * GRID_SIZE + GRID_SIZE / 2);
let dy = enemy.y - (tower.row * GRID_SIZE + GRID_SIZE / 2);
let dist = Math.hypot(dx, dy) / GRID_SIZE;
if (dist <= tower.range && enemy.hp > 0 && enemy.pathIndex > bestProgress) {
bestProgress = enemy.pathIndex;
target = enemy;
}
}
if (target) {
bullets.push({
x: tower.col * GRID_SIZE + GRID_SIZE / 2,
y: tower.row * GRID_SIZE + GRID_SIZE / 2,
target: target,
damage: tower.damage,
speed: towerDefs[tower.type].bulletSpeed,
color: towerDefs[tower.type].bulletColor,
type: tower.type,
splash: tower.splash || 0,
slow: tower.slow || 0,
chain: tower.chain || 0
});
tower.cooldownCounter = tower.cooldown;
}
}
}
function updateBullets() {
for (let i = bullets.length - 1; i >= 0; i--) {
let b = bullets[i];
if (!b.target || b.target.hp <= 0) { bullets.splice(i, 1); continue; }
let dx = b.target.x - b.x;
let dy = b.target.y - b.y;
let dist = Math.hypot(dx, dy);
if (dist < b.speed + 4) {
applyDamage(b, b.target);
bullets.splice(i, 1);
} else {
b.x += (dx / dist) * b.speed;
b.y += (dy / dist) * b.speed;
}
}
}
function applyDamage(bullet, enemy) {
enemy.hp -= bullet.damage;
spawnParticles(enemy.x, enemy.y, bullet.color, 3, 1.5);
let def = towerDefs[bullet.type];
if (def && def.hitSound) def.hitSound();
if (bullet.slow > 0 && enemy.slowTimer <= 0) {
enemy.slowTimer = 35;
enemy.speed = enemy.originalSpeed * (1 - bullet.slow);
}
if (bullet.splash > 0) {
for (let e of enemies) {
if (e === enemy || e.hp <= 0) continue;
if (Math.hypot(e.x - enemy.x, e.y - enemy.y) / GRID_SIZE <= bullet.splash) {
e.hp -= Math.floor(bullet.damage * 0.5);
}
}
}
if (bullet.chain > 0) {
let lastTarget = enemy;
let chained = new Set([enemy]);
for (let c = 0; c < bullet.chain; c++) {
let nearest = null, minDist = GRID_SIZE * 2;
for (let e of enemies) {
if (chained.has(e) || e.hp <= 0) continue;
let d = Math.hypot(e.x - lastTarget.x, e.y - lastTarget.y);
if (d < minDist) { minDist = d; nearest = e; }
}
if (nearest) {
nearest.hp -= Math.floor(bullet.damage * 0.4);
chained.add(nearest);
lastTarget = nearest;
} else break;
}
}
if (enemy.hp <= 0) {
money += enemy.reward;
spawnParticles(enemy.x, enemy.y, '#ffffff', 6, 2);
sfxEnemyDie();
}
}
function upgradeTower(tower) {
let def = towerDefs[tower.type];
if (tower.level >= def.maxLevel) return false;
let cost = Math.floor(def.upgradeCost * tower.level);
if (money < cost) return false;
money -= cost;
tower.level++;
tower.damage = Math.floor(def.damage * Math.pow(1.5, tower.level - 1));
tower.range += 0.3;
tower.cooldown = Math.max(10, def.cooldown - tower.level * 2);
if (tower.splash) tower.splash += 0.2;
if (tower.slow) tower.slow = Math.min(0.75, tower.slow + 0.08);
if (tower.chain) tower.chain += (tower.level % 2 === 0 ? 1 : 0);
spawnParticles(tower.col * GRID_SIZE + GRID_SIZE/2, tower.row * GRID_SIZE + GRID_SIZE/2, '#FFD700', 10, 2.5);
sfxUpgrade();
return true;
}
function removeTower(tower) {
let def = towerDefs[tower.type];
let refund = Math.floor(def.cost * 0.5);
for (let lv = 2; lv <= tower.level; lv++) refund += Math.floor(def.upgradeCost * (lv - 1) * 0.5);
money += refund;
gridMap[tower.row][tower.col] = 'empty';
towers = towers.filter(t => t !== tower);
spawnParticles(tower.col * GRID_SIZE + GRID_SIZE/2, tower.row * GRID_SIZE + GRID_SIZE/2, '#ff6666', 8, 2);
sfxRemove();
}
function startNextLoop() {
wave = 1;
waveActive = false;
gameOver = false;
enemies = [];
bullets = [];
spawnQueue = [];
startWaveBtn.disabled = false;
continueBtn.style.display = 'none';
generateRandomPath();
sfxWaveStart();
updateUI();
}
function update() {
if (gameOver) return;
// 逐个生成队列中的敌人,确保有适当的间隔
if (spawnQueue.length > 0) {
spawnTimer--;
if (spawnTimer <= 0) {
spawnEnemyFromQueue();
}
}
updateTowers();
updateBullets();
for (let i = enemies.length - 1; i >= 0; i--) {
let enemy = enemies[i];
if (enemy.slowTimer > 0) {
enemy.slowTimer--;
if (enemy.slowTimer <= 0) enemy.speed = enemy.originalSpeed;
}
if (moveEnemy(enemy) || enemy.hp <= 0) enemies.splice(i, 1);
}
for (let i = particles.length - 1; i >= 0; i--) {
let p = particles[i];
p.life -= 0.02;
p.x += p.vx; p.y += p.vy;
p.vx *= 0.96; p.vy *= 0.96;
if (p.life <= 0) particles.splice(i, 1);
}
if (waveActive && enemies.length === 0 && spawnQueue.length === 0) {
waveActive = false;
money += 100 + wave * 20;
wave++;
totalWavesCompleted++;
sfxWaveComplete();
console.log(`✅ 第${wave-1}波完成!剩余敌人: ${enemies.length}`);
if (wave > 10) {
continueBtn.style.display = 'inline-block';
startWaveBtn.style.display = 'none';
waveDisplay.textContent = '完成!';
} else {
startWaveBtn.disabled = false;
}
}
if (lives <= 0) { gameOver = true; lives = 0; alert('💔 游戏结束!敌人突破了防线...'); }
updateUI();
}
function updateUI() {
livesDisplay.textContent = lives;
moneyDisplay.textContent = money;
if (wave <= 10) waveDisplay.textContent = wave + '/10';
else waveDisplay.textContent = '完成!';
}
function drawGrid() {
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
let x = c * GRID_SIZE, y = r * GRID_SIZE;
if (gridMap[r][c] === 'path') {
let gradient = ctx.createLinearGradient(x, y, x + GRID_SIZE, y + GRID_SIZE);
gradient.addColorStop(0, '#D4A854'); gradient.addColorStop(0.5, '#C49A3C'); gradient.addColorStop(1, '#B8892E');
ctx.fillStyle = gradient; ctx.fillRect(x, y, GRID_SIZE, GRID_SIZE);
ctx.fillStyle = 'rgba(139, 101, 38, 0.3)'; ctx.fillRect(x + 4, y + 4, GRID_SIZE - 8, GRID_SIZE - 8);
ctx.strokeStyle = '#A07828'; ctx.lineWidth = 3; ctx.strokeRect(x + 1, y + 1, GRID_SIZE - 2, GRID_SIZE - 2);
} else {
let gradient = ctx.createLinearGradient(x, y, x + GRID_SIZE, y + GRID_SIZE);
gradient.addColorStop(0, '#66BB6A'); gradient.addColorStop(0.5, '#4CAF50'); gradient.addColorStop(1, '#43A047');
ctx.fillStyle = gradient; ctx.fillRect(x, y, GRID_SIZE, GRID_SIZE);
ctx.fillStyle = 'rgba(129, 199, 132, 0.4)'; ctx.fillRect(x + 3, y + 3, GRID_SIZE - 6, GRID_SIZE - 6);
if ((r + c) % 3 === 0) {
ctx.fillStyle = '#FFEB3B'; ctx.beginPath();
ctx.arc(x + GRID_SIZE * 0.7, y + GRID_SIZE * 0.25, 3, 0, Math.PI * 2); ctx.fill();
}
}
ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)'; ctx.lineWidth = 1; ctx.strokeRect(x, y, GRID_SIZE, GRID_SIZE);
}
}
let sx = spawnPoint.col * GRID_SIZE + GRID_SIZE/2;
let sy = spawnPoint.row * GRID_SIZE + GRID_SIZE/2;
ctx.fillStyle = 'rgba(0, 200, 83, 0.4)'; ctx.beginPath(); ctx.arc(sx, sy, 24, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#00C853'; ctx.font = 'bold 24px Arial'; ctx.textAlign = 'center'; ctx.fillText('🚪', sx, sy + 8);
let ex = endPoint.col * GRID_SIZE + GRID_SIZE/2;
let ey = endPoint.row * GRID_SIZE + GRID_SIZE/2;
ctx.fillStyle = 'rgba(255, 82, 82, 0.4)'; ctx.beginPath(); ctx.arc(ex, ey, 24, 0, Math.PI * 2); ctx.fill();
ctx.fillText('🏠', ex, ey + 8);
}
function drawArrowTower(cx, cy, size, color) {
ctx.fillStyle = '#5D4037'; ctx.beginPath(); ctx.arc(cx, cy, size * 0.7, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = color; ctx.shadowColor = color; ctx.shadowBlur = 10;
ctx.beginPath(); ctx.moveTo(cx, cy - size * 1.2); ctx.lineTo(cx - size * 0.7, cy + size * 0.8); ctx.lineTo(cx + size * 0.7, cy + size * 0.8); ctx.closePath(); ctx.fill();
ctx.strokeStyle = '#FFFFFF'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(cx, cy, size * 0.6, -0.8, 0.8); ctx.stroke();
ctx.fillStyle = '#FFFFFF'; ctx.beginPath(); ctx.arc(cx, cy, size * 0.2, 0, Math.PI * 2); ctx.fill();
ctx.shadowBlur = 0;
}
function drawCannonTower(cx, cy, size, color) {
ctx.fillStyle = '#37474F'; ctx.beginPath(); ctx.arc(cx, cy, size * 0.8, 0, Math.PI * 2); ctx.fill();
ctx.save(); ctx.translate(cx, cy); ctx.fillStyle = color; ctx.shadowColor = color; ctx.shadowBlur = 8;
ctx.fillRect(-size * 0.3, -size * 1.1, size * 0.6, size * 1.3);
ctx.fillStyle = '#212121'; ctx.fillRect(-size * 0.25, -size * 1.15, size * 0.5, size * 0.3); ctx.restore();
for (let i = 0; i < 4; i++) {
let angle = (i / 4) * Math.PI * 2; ctx.fillStyle = '#FFD54F';
ctx.beginPath(); ctx.arc(cx + Math.cos(angle) * size * 0.6, cy + Math.sin(angle) * size * 0.6, 3, 0, Math.PI * 2); ctx.fill();
}
ctx.shadowBlur = 0;
}
function drawIceTower(cx, cy, size, color) {
ctx.fillStyle = '#B3E5FC'; ctx.beginPath();
for (let i = 0; i < 6; i++) {
let angle = (i / 6) * Math.PI * 2 - Math.PI / 2;
if (i === 0) ctx.moveTo(cx + Math.cos(angle) * size * 0.8, cy + Math.sin(angle) * size * 0.8);
else ctx.lineTo(cx + Math.cos(angle) * size * 0.8, cy + Math.sin(angle) * size * 0.8);
}
ctx.closePath(); ctx.fill();
ctx.fillStyle = color; ctx.shadowColor = color; ctx.shadowBlur = 12; ctx.beginPath();
for (let i = 0; i < 6; i++) {
let angle = (i / 6) * Math.PI * 2 - Math.PI / 2;
if (i === 0) ctx.moveTo(cx + Math.cos(angle) * size * 0.7, cy + Math.sin(angle) * size * 0.7);
else ctx.lineTo(cx + Math.cos(angle) * size * 0.7, cy + Math.sin(angle) * size * 0.7);
let mid = angle + Math.PI / 6;
ctx.lineTo(cx + Math.cos(mid) * size * 0.35, cy + Math.sin(mid) * size * 0.35);
}
ctx.closePath(); ctx.fill();
ctx.fillStyle = '#FFFFFF'; ctx.beginPath(); ctx.arc(cx, cy, size * 0.2, 0, Math.PI * 2); ctx.fill();
ctx.shadowBlur = 0;
}
function drawLightningTower(cx, cy, size, color) {
ctx.fillStyle = '#FFF9C4'; ctx.beginPath();
ctx.moveTo(cx, cy - size * 0.9); ctx.lineTo(cx + size * 0.7, cy);
ctx.lineTo(cx, cy + size * 0.9); ctx.lineTo(cx - size * 0.7, cy); ctx.closePath(); ctx.fill();
ctx.fillStyle = color; ctx.shadowColor = color; ctx.shadowBlur = 18; ctx.beginPath();
ctx.moveTo(cx + size * 0.15, cy - size * 0.8); ctx.lineTo(cx - size * 0.4, cy + size * 0.1);
ctx.lineTo(cx - size * 0.05, cy + size * 0.05); ctx.lineTo(cx - size * 0.25, cy + size * 0.8);
ctx.lineTo(cx + size * 0.35, cy - size * 0.15); ctx.lineTo(cx, cy - size * 0.05);
ctx.lineTo(cx + size * 0.25, cy - size * 0.8); ctx.closePath(); ctx.fill();
ctx.shadowBlur = 0;
}
function drawTowerShape(tower) {
let cx = tower.col * GRID_SIZE + GRID_SIZE/2;
let cy = tower.row * GRID_SIZE + GRID_SIZE/2;
let def = towerDefs[tower.type];
let size = 16 + tower.level * 2;
if (showRange) {
ctx.beginPath(); ctx.arc(cx, cy, tower.range * GRID_SIZE, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255, 255, 255, 0.12)'; ctx.fill();
ctx.strokeStyle = def.color; ctx.lineWidth = 2; ctx.setLineDash([5, 5]); ctx.stroke(); ctx.setLineDash([]);
}
switch(def.shape) {
case 'arrow': drawArrowTower(cx, cy, size, def.color); break;
case 'cannon': drawCannonTower(cx, cy, size, def.color); break;
case 'ice': drawIceTower(cx, cy, size, def.color); break;
case 'lightning': drawLightningTower(cx, cy, size, def.color); break;
}
ctx.fillStyle = '#FFFFFF'; ctx.font = 'bold 13px Arial'; ctx.textAlign = 'center';
ctx.strokeStyle = 'rgba(0,0,0,0.6)'; ctx.lineWidth = 2;
ctx.strokeText('Lv' + tower.level, cx, cy - size - 8);
ctx.fillText('Lv' + tower.level, cx, cy - size - 8);
}
function drawAll() {
ctx.clearRect(0, 0, MAP_W, MAP_H);
drawGrid();
for (let t of towers) drawTowerShape(t);
for (let e of enemies) {
ctx.save(); ctx.shadowColor = 'rgba(0,0,0,0.4)'; ctx.shadowBlur = 6;
ctx.fillStyle = e.color; ctx.beginPath(); ctx.arc(e.x, e.y, e.size, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.3)'; ctx.arc(e.x - 2, e.y - 2, e.size * 0.5, 0, Math.PI * 2); ctx.fill();
let hpPercent = e.hp / e.maxHp; ctx.shadowBlur = 0;
ctx.fillStyle = '#333'; ctx.fillRect(e.x - 14, e.y - e.size - 10, 28, 6);
ctx.fillStyle = hpPercent > 0.5 ? '#4CAF50' : hpPercent > 0.25 ? '#FF9800' : '#f44336';
ctx.fillRect(e.x - 14, e.y - e.size - 10, 28 * hpPercent, 6);
ctx.restore();
}
for (let b of bullets) {
ctx.shadowColor = b.color; ctx.shadowBlur = 8; ctx.fillStyle = b.color;
ctx.beginPath(); ctx.arc(b.x, b.y, 4, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#fff'; ctx.arc(b.x - 1, b.y - 1, 2, 0, Math.PI * 2); ctx.fill();
}
ctx.shadowBlur = 0;
for (let p of particles) {
ctx.globalAlpha = p.life * 0.9; ctx.fillStyle = p.color;
ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fill();
}
ctx.globalAlpha = 1;
if (gameOver) {
ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(0, 0, MAP_W, MAP_H);
ctx.fillStyle = '#ffffff'; ctx.font = 'bold 42px Arial'; ctx.textAlign = 'center';
ctx.fillText('游戏结束', MAP_W/2, MAP_H/2);
}
}
canvas.addEventListener('click', (e) => {
initAudio();
if (gameOver) return;
const rect = canvas.getBoundingClientRect();
const scaleX = MAP_W / rect.width, scaleY = MAP_H / rect.height;
const col = Math.floor((e.clientX - rect.left) * scaleX / GRID_SIZE);
const row = Math.floor((e.clientY - rect.top) * scaleY / GRID_SIZE);
if (col < 0 || col >= COLS || row < 0 || row >= ROWS) return;
let clickedTower = towers.find(t => t.col === col && t.row === row);
if (clickedTower) {
let def = towerDefs[clickedTower.type];
if (clickedTower.level >= def.maxLevel) { alert('已达到最高等级!右键拆除返还金币。'); return; }
let cost = Math.floor(def.upgradeCost * clickedTower.level);
if (confirm(`升级 ${def.name} 到 Lv${clickedTower.level + 1}?\n费用:${cost} 金币`)) {
if (!upgradeTower(clickedTower)) alert('金币不足!');
}
return;
}
if (gridMap[row][col] !== 'empty') { alert('不能在这里建造!'); return; }
let def = towerDefs[selectedTowerType];
if (money < def.cost) { alert('金币不足!'); return; }
money -= def.cost;
towers.push({ col, row, type: selectedTowerType, level: 1, damage: def.damage, range: def.range, cooldown: def.cooldown, cooldownCounter: 0, splash: def.splash || 0, slow: def.slow || 0, chain: def.chain || 0 });
gridMap[row][col] = 'tower';
spawnParticles(col * GRID_SIZE + GRID_SIZE/2, row * GRID_SIZE + GRID_SIZE/2, '#aaddff', 6, 2);
sfxBuild();
});
canvas.addEventListener('contextmenu', (e) => {
e.preventDefault(); initAudio();
if (gameOver) return;
const rect = canvas.getBoundingClientRect();
const scaleX = MAP_W / rect.width, scaleY = MAP_H / rect.height;
const col = Math.floor((e.clientX - rect.left) * scaleX / GRID_SIZE);
const row = Math.floor((e.clientY - rect.top) * scaleY / GRID_SIZE);
if (col < 0 || col >= COLS || row < 0 || row >= ROWS) return;
let clickedTower = towers.find(t => t.col === col && t.row === row);
if (clickedTower) {
let def = towerDefs[clickedTower.type];
let refund = Math.floor(def.cost * 0.5);
for (let lv = 2; lv <= clickedTower.level; lv++) refund += Math.floor(def.upgradeCost * (lv - 1) * 0.5);
if (confirm(`拆除 ${def.name} Lv${clickedTower.level}?\n返还:${refund} 金币`)) removeTower(clickedTower);
}
});
document.querySelectorAll('.tower-btn').forEach(btn => {
btn.addEventListener('click', function() {
initAudio();
document.querySelectorAll('.tower-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
selectedTowerType = this.dataset.type;
});
});
startWaveBtn.addEventListener('click', () => {
initAudio();
if (gameOver || waveActive || wave > 10) return;
waveActive = true;
let newEnemies = spawnWave(wave, totalWavesCompleted);
spawnQueue = newEnemies;
spawnTimer = spawnInterval; // 初始化生成计时器
startWaveBtn.disabled = true;
sfxWaveStart();
console.log(`🚀 开始第${wave}波!队列中有${spawnQueue.length}个敌人等待生成`);
});
continueBtn.addEventListener('click', () => {
initAudio();
totalWavesCompleted++;
startNextLoop();
continueBtn.style.display = 'none';
startWaveBtn.style.display = 'inline-block';
startWaveBtn.disabled = false;
alert(`进入第 ${totalWavesCompleted + 1} 轮!敌人更强了!`);
});
document.getElementById('resetBtn').addEventListener('click', () => {
initAudio();
if (confirm('确定要重新开始吗?')) resetGame();
});
function resetGame() {
lives = 20; money = 400; wave = 1; totalWavesCompleted = 0;
gameOver = false; waveActive = false;
enemies = []; towers = []; bullets = []; particles = []; spawnQueue = [];
selectedTowerType = 'arrow'; showRange = true;
rangeToggleBtn.classList.add('active-range'); rangeToggleBtn.textContent = '🎯 范围';
continueBtn.style.display = 'none'; startWaveBtn.style.display = 'inline-block'; startWaveBtn.disabled = false;
document.querySelectorAll('.tower-btn').forEach(b => b.classList.remove('active'));
document.querySelector('.tower-btn[data-type="arrow"]').classList.add('active');
initGridMap(); generateRandomPath(); updateUI();
}
function gameLoop() {
update();
drawAll();
requestAnimationFrame(gameLoop);
}
initGridMap();
generateRandomPath();
updateUI();
gameLoop();
})();
</script>
</body>
</html>