<!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);
}
.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.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;
}
}
/* ========== 小屏幕竖屏优化 ========== */
@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;
}
}
</style>
</head>
<body>
<div class="game-container" id="gameContainer">
<!-- 全屏按钮 -->
<button class="fullscreen-btn" id="fullscreenBtn" 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" 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 gameContainer = document.getElementById('gameContainer');
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 gameOver = false;
let waveActive = false;
let selectedTowerType = 'arrow';
let showRange = true;
let isFullscreen = false;
let enemies = [];
let towers = [];
let bullets = [];
let particles = [];
let pathCells = [];
let spawnPoint = {col: 0, row: 3};
let endPoint = {col: 9, row: 4};
let gridMap = [];
const towerDefs = {
arrow: {
name: '箭塔', cost: 80, range: 2.5, damage: 20, cooldown: 18,
color: '#2196F3', bulletColor: '#64B5F6', bulletSpeed: 8,
upgradeCost: 60, maxLevel: 5, projectile: 'arrow', shape: 'arrow'
},
cannon: {
name: '炮塔', cost: 150, range: 2.0, damage: 55, cooldown: 40,
color: '#FF5722', bulletColor: '#FF8A65', bulletSpeed: 6,
upgradeCost: 100, maxLevel: 4, splash: 1.2, projectile: 'cannonball', shape: 'cannon'
},
ice: {
name: '冰塔', cost: 120, range: 2.3, damage: 12, cooldown: 22,
color: '#00BCD4', bulletColor: '#80DEEA', bulletSpeed: 7,
upgradeCost: 80, maxLevel: 5, slow: 0.5, projectile: 'ice', shape: 'ice'
},
lightning: {
name: '电塔', cost: 200, range: 2.8, damage: 35, cooldown: 25,
color: '#FFC107', bulletColor: '#FFE082', bulletSpeed: 15,
upgradeCost: 130, maxLevel: 4, chain: 2, projectile: 'lightning', shape: 'lightning'
}
};
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() {
if (!isFullscreen) {
if (gameContainer.requestFullscreen) {
gameContainer.requestFullscreen();
} 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() {
isFullscreen = !!(
document.fullscreenElement ||
document.webkitFullscreenElement ||
document.msFullscreenElement
);
fullscreenBtn.textContent = isFullscreen ? '✕' : '⛶';
fullscreenBtn.title = isFullscreen ? '退出全屏' : '全屏模式';
}
fullscreenBtn.addEventListener('click', toggleFullscreen);
document.addEventListener('fullscreenchange', handleFullscreenChange);
document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
document.addEventListener('msfullscreenchange', handleFullscreenChange);
// 范围显示切换
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 extendedPath = [...path];
for (let i = 1; i < path.length - 1; i++) {
if (Math.random() < 0.15) {
let current = path[i];
let next = path[i + 1];
let midCol = current.col;
let midRow = next.row;
let key = `${midCol},${midRow}`;
if (midCol >= 0 && midCol < COLS && midRow >= 0 && midRow < ROWS) {
let alreadyInPath = path.some(p => p.col === midCol && p.row === midRow);
if (!alreadyInPath) {
extendedPath.splice(i + 1, 0, {col: midCol, row: midRow});
}
}
}
}
let pathSet = new Set();
let finalPath = [];
for (let p of extendedPath) {
let key = `${p.col},${p.row}`;
if (!pathSet.has(key)) {
pathSet.add(key);
finalPath.push(p);
}
}
pathCells = finalPath;
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
gridMap[r][c] = pathSet.has(`${c},${r}`) ? 'path' : 'empty';
}
}
}
function spawnWave(waveNum) {
let enemyList = [];
let count = 5 + waveNum * 3;
for (let i = 0; i < count; i++) {
let type = 'normal';
let r = Math.random();
if (waveNum >= 8 && i % 4 === 0) type = 'boss';
else if (waveNum >= 5 && r < 0.2) type = 'tank';
else if (waveNum >= 3 && r < 0.45) type = 'fast';
let stats = enemyTypes[type];
let hpMult = 1 + (waveNum - 1) * 0.3;
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: stats.reward,
pathIndex: 0,
slowTimer: 0,
originalSpeed: stats.speed * 0.8
});
}
return enemyList.sort(() => Math.random() - 0.5);
}
function moveEnemy(enemy) {
if (enemy.pathIndex >= pathCells.length) {
lives--;
spawnParticles(enemy.x, enemy.y, '#ff3333', 20, 4);
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', 20, 4);
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 = 10, speed = 3) {
for (let i = 0; i < count; i++) {
let angle = Math.random() * Math.PI * 2;
let sp = Math.random() * speed + 1;
particles.push({
x, y,
vx: Math.cos(angle) * sp,
vy: Math.sin(angle) * sp,
life: 0.6 + Math.random() * 0.6,
maxLife: 0.6 + Math.random() * 0.6,
color: color,
size: 2 + Math.random() * 3
});
}
}
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) {
let def = towerDefs[tower.type];
bullets.push({
x: tower.col * GRID_SIZE + GRID_SIZE / 2,
y: tower.row * GRID_SIZE + GRID_SIZE / 2,
target: target,
damage: tower.damage,
speed: def.bulletSpeed,
color: def.bulletColor,
type: tower.type,
splash: tower.splash || 0,
slow: tower.slow || 0,
chain: tower.chain || 0,
tower: tower
});
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, 8, 2.5);
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);
spawnParticles(e.x, e.y, '#ff8844', 5, 2);
}
}
}
if (bullet.chain > 0) {
let lastTarget = enemy;
for (let c = 0; c < bullet.chain; c++) {
let nearest = null, minDist = GRID_SIZE * 2;
for (let e of enemies) {
if (e === lastTarget || 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);
spawnParticles(nearest.x, nearest.y, '#ffff00', 6, 2);
lastTarget = nearest;
} else break;
}
}
if (enemy.hp <= 0) {
money += enemy.reward;
spawnParticles(enemy.x, enemy.y, '#ffffff', 15, 4);
}
}
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', 20, 4);
return true;
}
function update() {
if (gameOver) return;
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) {
waveActive = false;
money += 100 + wave * 20;
wave++;
if (wave > 10) { alert('🎉 恭喜!你击败了所有波次!你是塔防大师!'); gameOver = true; }
document.getElementById('startWaveBtn').disabled = false;
}
if (lives <= 0) { gameOver = true; lives = 0; alert('💔 游戏结束!敌人突破了防线...'); }
updateUI();
}
function updateUI() {
livesDisplay.textContent = lives;
moneyDisplay.textContent = money;
waveDisplay.textContent = wave + '/10';
}
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, level) {
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 = 12;
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, level) {
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 = 10;
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;
let rx = cx + Math.cos(angle) * size * 0.6;
let ry = cy + Math.sin(angle) * size * 0.6;
ctx.fillStyle = '#FFD54F';
ctx.beginPath();
ctx.arc(rx, ry, 3, 0, Math.PI * 2);
ctx.fill();
}
ctx.shadowBlur = 0;
}
function drawIceTower(cx, cy, size, color, level) {
ctx.fillStyle = '#B3E5FC';
ctx.beginPath();
for (let i = 0; i < 6; i++) {
let angle = (i / 6) * Math.PI * 2 - Math.PI / 2;
let px = cx + Math.cos(angle) * size * 0.8;
let py = cy + Math.sin(angle) * size * 0.8;
if (i === 0) ctx.moveTo(px, py);
else ctx.lineTo(px, py);
}
ctx.closePath();
ctx.fill();
ctx.fillStyle = color;
ctx.shadowColor = color;
ctx.shadowBlur = 15;
ctx.beginPath();
for (let i = 0; i < 6; i++) {
let angle = (i / 6) * Math.PI * 2 - Math.PI / 2;
let px = cx + Math.cos(angle) * size * 0.7;
let py = cy + Math.sin(angle) * size * 0.7;
if (i === 0) ctx.moveTo(px, py);
else ctx.lineTo(px, py);
let midAngle = angle + Math.PI / 6;
let mx = cx + Math.cos(midAngle) * size * 0.35;
let my = cy + Math.sin(midAngle) * size * 0.35;
ctx.lineTo(mx, my);
}
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, level) {
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 = 20;
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 + size * 0.0, cy - size * 0.05);
ctx.lineTo(cx + size * 0.25, cy - size * 0.8);
ctx.closePath();
ctx.fill();
ctx.fillStyle = '#FFFFFF';
for (let i = 0; i < 3; i++) {
let angle = Math.random() * Math.PI * 2;
let dist = size * (0.5 + Math.random() * 0.4);
ctx.beginPath();
ctx.arc(cx + Math.cos(angle) * dist, cy + Math.sin(angle) * dist, 2, 0, Math.PI * 2);
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, tower.level);
break;
case 'cannon':
drawCannonTower(cx, cy, size, def.color, tower.level);
break;
case 'ice':
drawIceTower(cx, cy, size, def.color, tower.level);
break;
case 'lightning':
drawLightningTower(cx, cy, size, def.color, tower.level);
break;
default:
ctx.fillStyle = def.color;
ctx.beginPath();
ctx.arc(cx, cy, size, 0, Math.PI * 2);
ctx.fill();
}
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 drawTowers() {
for (let t of towers) {
drawTowerShape(t);
}
}
function drawEnemies() {
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();
}
}
function drawBullets() {
for (let b of bullets) {
ctx.shadowColor = b.color;
ctx.shadowBlur = 10;
ctx.fillStyle = b.color;
ctx.beginPath();
ctx.arc(b.x, b.y, 5, 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;
}
function drawParticles() {
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;
}
function drawAll() {
ctx.clearRect(0, 0, MAP_W, MAP_H);
drawGrid();
drawTowers();
drawEnemies();
drawBullets();
drawParticles();
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) => {
if (gameOver) return;
const rect = canvas.getBoundingClientRect();
const scaleX = MAP_W / rect.width;
const scaleY = MAP_H / rect.height;
const mx = (e.clientX - rect.left) * scaleX;
const my = (e.clientY - rect.top) * scaleY;
const col = Math.floor(mx / GRID_SIZE);
const row = Math.floor(my / 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 cost = Math.floor(def.upgradeCost * clickedTower.level);
if (clickedTower.level >= def.maxLevel) { alert('已达到最高等级!'); return; }
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', 12, 3);
});
canvas.addEventListener('contextmenu', (e) => {
e.preventDefault();
selectedTowerType = null;
document.querySelectorAll('.tower-btn').forEach(b => b.classList.remove('active'));
});
document.querySelectorAll('.tower-btn').forEach(btn => {
btn.addEventListener('click', function() {
document.querySelectorAll('.tower-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
selectedTowerType = this.dataset.type;
});
});
document.getElementById('startWaveBtn').addEventListener('click', () => {
if (gameOver || waveActive || wave > 10) return;
waveActive = true;
enemies.push(...spawnWave(wave));
document.getElementById('startWaveBtn').disabled = true;
});
document.getElementById('resetBtn').addEventListener('click', resetGame);
function resetGame() {
lives = 20; money = 400; wave = 1;
gameOver = false; waveActive = false;
enemies = []; towers = []; bullets = []; particles = [];
selectedTowerType = 'arrow';
showRange = true;
rangeToggleBtn.classList.add('active-range');
rangeToggleBtn.textContent = '🎯 范围';
document.querySelectorAll('.tower-btn').forEach(b => b.classList.remove('active'));
document.querySelector('.tower-btn[data-type="arrow"]').classList.add('active');
document.getElementById('startWaveBtn').disabled = false;
initGridMap();
generateRandomPath();
updateUI();
}
function gameLoop() {
update();
drawAll();
requestAnimationFrame(gameLoop);
}
initGridMap();
generateRandomPath();
updateUI();
gameLoop();
})();
</script>
</body>
</html>