pvz网页版 V1.3
2026-09-06 17:37:39
发布于:上海
9阅读
0回复
0点赞
较1.2更新了更多的关卡地图和增加boss战
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>植物大战僵尸 H5 - 关卡与BOSS版</title>
<style>
:root {
--bg-color: #1a1a2e;
--ui-bg: #16213e;
--ui-border: #0f3460;
--text-color: #e94560;
--accent: #f1c40f;
--boss-hp-color: #d32f2f;
}
body {
margin: 0;
padding: 0;
background-color: var(--bg-color);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
overflow: hidden;
user-select: none;
color: white;
}
#game-container {
position: relative;
width: 900px;
height: 600px;
box-shadow: 0 0 30px rgba(0,0,0,0.8);
background: #2e7d32;
border: 4px solid var(--ui-border);
border-radius: 12px;
overflow: hidden;
}
canvas {
display: block;
cursor: crosshair;
}
/* UI 通用样式 */
.ui-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10;
}
/* 顶部栏 */
#top-bar {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 85px;
background: linear-gradient(to bottom, #5d4037, #3e2723);
border-bottom: 4px solid #281815;
display: flex;
align-items: center;
padding: 0 15px;
box-sizing: border-box;
pointer-events: auto;
z-index: 20;
box-shadow: 0 4px 10px rgba(0,0,0,0.3);
}
.sun-counter {
background: #fff8e1;
color: #333;
padding: 8px 15px;
border-radius: 25px;
font-weight: bold;
font-size: 22px;
border: 3px solid #fbc02d;
margin-right: 20px;
display: flex;
align-items: center;
gap: 10px;
box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
}
.sun-icon {
width: 28px;
height: 28px;
background: radial-gradient(circle, #ffeb3b 40%, #f57f17 100%);
border-radius: 50%;
box-shadow: 0 0 8px #ffeb3b;
animation: spin 10s linear infinite;
}
@keyframes spin { 100% { transform: rotate(360deg); } }
#plant-bar {
display: flex;
gap: 8px;
flex: 1;
overflow-x: auto;
padding-bottom: 5px;
}
.plant-card {
width: 65px;
height: 75px;
background: linear-gradient(135deg, #a1887f, #8d6e63);
border: 2px solid #5d4037;
border-radius: 6px;
cursor: pointer;
position: relative;
transition: all 0.2s;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: 0 4px 0 #3e2723;
}
.plant-card:active {
transform: translateY(4px);
box-shadow: 0 0 0 #3e2723;
}
.plant-card.selected {
border-color: #ffeb3b;
box-shadow: 0 0 15px #ffeb3b, 0 4px 0 #3e2723;
background: linear-gradient(135deg, #bcaaa4, #a1887f);
}
.plant-card.disabled {
opacity: 0.6;
filter: grayscale(0.8);
cursor: not-allowed;
transform: none;
box-shadow: 0 4px 0 #3e2723;
}
.card-cost {
position: absolute;
bottom: 2px;
right: 2px;
font-size: 12px;
color: #fff;
background: rgba(0,0,0,0.7);
padding: 2px 5px;
border-radius: 4px;
font-weight: bold;
}
.card-preview {
width: 35px;
height: 35px;
border-radius: 50%;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
}
#tools {
display: flex;
gap: 10px;
margin-left: 15px;
}
.tool-btn {
width: 55px;
height: 55px;
background: linear-gradient(to bottom, #795548, #5d4037);
border: 2px solid #3e2723;
border-radius: 50%;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
color: white;
font-size: 24px;
transition: all 0.2s;
box-shadow: 0 4px 0 #281815;
}
.tool-btn:active {
transform: translateY(4px);
box-shadow: 0 0 0 #281815;
}
.tool-btn.active {
background: linear-gradient(to bottom, #d32f2f, #b71c1c);
border-color: #7f0000;
}
/* 菜单覆盖层 */
#menu-overlay, #level-select-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.9);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 100;
pointer-events: auto;
backdrop-filter: blur(5px);
}
.menu-title {
font-size: 56px;
color: #8bc34a;
text-shadow: 3px 3px 0 #1b5e20, 0 0 20px rgba(139, 195, 74, 0.5);
margin-bottom: 40px;
font-weight: 900;
letter-spacing: 3px;
font-family: 'Courier New', Courier, monospace;
}
.mode-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
max-width: 600px;
}
.mode-btn {
padding: 18px 40px;
font-size: 20px;
background: linear-gradient(to bottom, #5d4037, #3e2723);
color: white;
border: none;
border-radius: 12px;
cursor: pointer;
transition: all 0.2s;
border: 2px solid #281815;
box-shadow: 0 6px 0 #1a100e;
font-weight: bold;
}
.mode-btn:hover {
transform: translateY(-2px);
background: linear-gradient(to bottom, #795548, #5d4037);
box-shadow: 0 8px 0 #1a100e;
}
.mode-btn:active {
transform: translateY(4px);
box-shadow: 0 2px 0 #1a100e;
}
/* 关卡选择网格 */
.level-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 15px;
margin-top: 20px;
}
.level-btn {
width: 60px;
height: 60px;
background: #4caf50;
border: 2px solid #2e7d32;
border-radius: 8px;
color: white;
font-weight: bold;
font-size: 18px;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 4px 0 #1b5e20;
}
.level-btn.locked {
background: #757575;
border-color: #424242;
color: #bdbdbd;
cursor: not-allowed;
box-shadow: 0 4px 0 #212121;
}
.level-btn.boss-level {
background: #d32f2f;
border-color: #b71c1c;
box-shadow: 0 4px 0 #7f0000;
}
/* BOSS 血条 */
#boss-hud {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
width: 60%;
height: 30px;
background: rgba(0,0,0,0.7);
border: 2px solid #fff;
border-radius: 15px;
display: none;
z-index: 15;
overflow: hidden;
}
#boss-hp-bar {
width: 100%;
height: 100%;
background: linear-gradient(to right, #d32f2f, #f44336);
transition: width 0.2s;
}
#boss-name {
position: absolute;
top: -25px;
left: 0;
color: #fff;
font-weight: bold;
text-shadow: 1px 1px 2px black;
}
/* 暂停/结束面板 */
.panel {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.95);
padding: 40px;
border-radius: 15px;
text-align: center;
border: 5px solid #5d4037;
z-index: 50;
display: none;
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
}
.panel h2 {
margin-top: 0;
color: #3e2723;
font-size: 32px;
}
.action-btn {
background: #4caf50;
color: white;
border: none;
padding: 12px 25px;
font-size: 18px;
border-radius: 8px;
cursor: pointer;
margin: 10px;
font-weight: bold;
box-shadow: 0 4px 0 #2e7d32;
}
.action-btn:active {
transform: translateY(4px);
box-shadow: none;
}
.action-btn.secondary {
background: #f44336;
box-shadow: 0 4px 0 #d32f2f;
}
/* 消息提示 */
#message-area {
position: absolute;
top: 120px;
left: 50%;
transform: translateX(-50%);
background: rgba(0,0,0,0.8);
color: #ffeb3b;
padding: 12px 25px;
border-radius: 25px;
font-size: 20px;
font-weight: bold;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
border: 2px solid #ffeb3b;
z-index: 30;
}
.hidden { display: none !important; }
</style>
</head>
<body>
<div id="game-container">
<canvas id="gameCanvas" width="900" height="600"></canvas>
<!-- 顶部UI -->
<div id="top-bar" class="hidden">
<div class="sun-counter">
<div class="sun-icon"></div>
<span id="sun-display">50</span>
</div>
<div id="plant-bar">
<!-- 动态生成植物卡片 -->
</div>
<div id="tools">
<div class="tool-btn" id="shovel-btn" title="铲子">🥄</div>
<div class="tool-btn" id="pause-btn" title="暂停">⏸️</div>
</div>
</div>
<!-- BOSS 血条 HUD -->
<div id="boss-hud">
<div id="boss-name">巨型僵尸 BOSS</div>
<div id="boss-hp-bar"></div>
</div>
<!-- 消息提示 -->
<div id="message-area">游戏开始!</div>
<!-- 暂停面板 -->
<div id="pause-panel" class="panel">
<h2>游戏暂停</h2>
<button class="action-btn" id="resume-btn">继续游戏</button>
<button class="action-btn secondary" id="quit-btn">退出关卡</button>
</div>
<!-- 游戏结束/胜利面板 -->
<div id="result-panel" class="panel">
<h2 id="result-title">胜利!</h2>
<p id="result-msg">你成功保卫了脑子。</p>
<button class="action-btn" id="next-level-btn">下一关</button>
<button class="action-btn secondary" id="menu-return-btn">返回主菜单</button>
</div>
<!-- 关卡选择 -->
<div id="level-select-overlay" class="hidden">
<div class="menu-title">选择关卡</div>
<div class="level-grid" id="level-grid">
<!-- 动态生成关卡按钮 -->
</div>
<button class="mode-btn" style="margin-top: 30px;" id="back-to-main">返回主菜单</button>
</div>
<!-- 主菜单 -->
<div id="menu-overlay">
<div class="menu-title">PVZ H5 进阶版</div>
<div class="mode-grid">
<button class="mode-btn" id="start-adventure">冒险模式</button>
<button class="mode-btn" id="start-endless">无尽模式</button>
<button class="mode-btn" id="start-minigame">迷你游戏</button>
<button class="mode-btn" id="start-survival">生存模式</button>
</div>
<p style="color: #aaa; margin-top: 30px; font-size: 14px;">点击模式开始游戏 | 鼠标左键种植/收集阳光</p>
</div>
</div>
<script>
/**
* 游戏核心逻辑 - 包含关卡系统与BOSS战
*/
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// 游戏配置
const CONFIG = {
GRID_ROWS: 5,
GRID_COLS: 9,
CELL_WIDTH: 80,
CELL_HEIGHT: 100,
OFFSET_X: 20,
OFFSET_Y: 100,
SUN_VALUE: 25,
INITIAL_SUN: 50,
FPS: 60
};
// 植物定义
const PLANTS = {
SUNFLOWER: { id: 1, name: '向日葵', cost: 50, hp: 300, color: '#f1c40f', type: 'producer', cooldown: 500 },
PEASHOOTER: { id: 2, name: '豌豆射手', cost: 100, hp: 300, color: '#4caf50', type: 'shooter', damage: 20, rate: 1500 },
WALLNUT: { id: 3, name: '坚果墙', cost: 50, hp: 2000, color: '#795548', type: 'defender' },
SNOWPEA: { id: 4, name: '寒冰射手', cost: 175, hp: 300, color: '#00bcd4', type: 'shooter', damage: 20, rate: 1500, effect: 'slow' },
CHERRYBOMB: { id: 5, name: '樱桃炸弹', cost: 150, hp: 100, color: '#d32f2f', type: 'instant', damage: 1000, range: 1.5 } // 范围伤害
};
// 僵尸定义
const ZOMBIES = {
NORMAL: { hp: 200, speed: 0.3, color: '#9e9e9e', damage: 10, type: 'normal' },
CONEHEAD: { hp: 560, speed: 0.3, color: '#ff9800', damage: 10, type: 'normal' },
BUCKETHEAD: { hp: 1300, speed: 0.25, color: '#607d8b', damage: 10, type: 'normal' },
BOSS: { hp: 5000, speed: 0.1, color: '#3e2723', damage: 50, type: 'boss', width: 120, height: 180 } // BOSS属性
};
// 关卡配置
const LEVELS = [
{ id: 1, waves: 3, types: ['NORMAL'], boss: false },
{ id: 2, waves: 5, types: ['NORMAL', 'CONEHEAD'], boss: false },
{ id: 3, waves: 8, types: ['NORMAL', 'CONEHEAD', 'BUCKETHEAD'], boss: false },
{ id: 4, waves: 10, types: ['CONEHEAD', 'BUCKETHEAD'], boss: true }, // BOSS关
{ id: 5, waves: 15, types: ['NORMAL', 'CONEHEAD', 'BUCKETHEAD'], boss: false },
{ id: 6, waves: 20, types: ['CONEHEAD', 'BUCKETHEAD'], boss: true } // BOSS关
];
// 游戏状态
let gameState = {
mode: null,
currentLevelId: 1,
unlockedLevels: 1,
sun: 50,
plants: [],
zombies: [],
bullets: [],
suns: [],
particles: [],
grid: [],
lastTime: 0,
isPaused: false,
isGameOver: false,
wave: 1,
zombieSpawnTimer: 0,
sunSpawnTimer: 0,
selectedPlant: null,
isShovelActive: false,
levelConfig: null,
bossActive: false,
bossEntity: null
};
// 初始化网格
function initGrid() {
gameState.grid = [];
for (let r = 0; r < CONFIG.GRID_ROWS; r++) {
let row = [];
for (let c = 0; c < CONFIG.GRID_COLS; c++) {
row.push(null);
}
gameState.grid.push(row);
}
}
// 类定义
class Plant {
constructor(r, c, typeKey) {
this.r = r;
this.c = c;
this.typeKey = typeKey;
this.config = PLANTS[typeKey];
this.hp = this.config.hp;
this.maxHp = this.config.hp;
this.x = CONFIG.OFFSET_X + c * CONFIG.CELL_WIDTH + CONFIG.CELL_WIDTH / 2;
this.y = CONFIG.OFFSET_Y + r * CONFIG.CELL_HEIGHT + CONFIG.CELL_HEIGHT / 2;
this.lastActionTime = 0;
this.animFrame = 0;
this.isDead = false;
}
update(dt, now) {
this.animFrame += dt;
if (this.config.type === 'producer') {
if (now - this.lastActionTime > 10000) {
createSun(this.x, this.y, false);
this.lastActionTime = now;
}
} else if (this.config.type === 'shooter') {
const hasZombieInRow = gameState.zombies.some(z => z.r === this.r && z.x > this.x);
if (hasZombieInRow && now - this.lastActionTime > this.config.rate) {
createBullet(this.x + 20, this.y, this.r, this.config.damage, this.config.effect);
this.lastActionTime = now;
}
} else if (this.config.type === 'instant') {
// 樱桃炸弹逻辑
if (now - this.lastActionTime > 1000) { // 1秒后爆炸
explodeCherryBomb(this);
this.isDead = true;
}
}
}
draw(ctx) {
if (this.isDead) return;
ctx.save();
ctx.translate(this.x, this.y);
const scale = 1 + Math.sin(this.animFrame / 200) * 0.05;
ctx.scale(scale, scale);
ctx.fillStyle = this.config.color;
if (this.typeKey === 'SUNFLOWER') {
for(let i=0; i<8; i++) {
ctx.beginPath();
ctx.rotate(Math.PI / 4);
ctx.ellipse(0, -15, 8, 15, 0, 0, Math.PI*2);
ctx.fill();
}
ctx.beginPath();
ctx.arc(0, 0, 12, 0, Math.PI*2);
ctx.fillStyle = '#5d4037';
ctx.fill();
} else if (this.typeKey === 'PEASHOOTER' || this.typeKey === 'SNOWPEA') {
ctx.beginPath();
ctx.arc(0, -5, 18, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = '#333';
ctx.beginPath();
ctx.arc(12, -5, 8, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = '#2e7d32';
ctx.beginPath();
ctx.ellipse(-5, 15, 10, 5, Math.PI/4, 0, Math.PI*2);
ctx.fill();
} else if (this.typeKey === 'WALLNUT') {
ctx.beginPath();
ctx.ellipse(0, 0, 20, 25, 0, 0, Math.PI*2);
ctx.fill();
if (this.hp < this.maxHp * 0.6) {
ctx.strokeStyle = '#3e2723';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(-5, -10);
ctx.lineTo(5, 5);
ctx.stroke();
}
} else if (this.typeKey === 'CHERRYBOMB') {
ctx.beginPath();
ctx.arc(-8, 0, 12, 0, Math.PI*2);
ctx.arc(8, 0, 12, 0, Math.PI*2);
ctx.fill();
// 引线
ctx.strokeStyle = '#3e2723';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, -10);
ctx.lineTo(0, -20);
ctx.stroke();
}
if (this.hp < this.maxHp && this.config.type !== 'instant') {
ctx.fillStyle = 'red';
ctx.fillRect(-15, -35, 30, 4);
ctx.fillStyle = '#0f0';
ctx.fillRect(-15, -35, 30 * (this.hp / this.maxHp), 4);
}
ctx.restore();
}
}
class Zombie {
constructor(r, typeKey, isBoss = false) {
this.r = r;
this.typeKey = typeKey;
this.isBoss = isBoss;
this.config = isBoss ? ZOMBIES.BOSS : ZOMBIES[typeKey];
this.hp = this.config.hp;
this.maxHp = this.config.hp;
this.speed = this.config.speed;
this.damage = this.config.damage;
// BOSS位置特殊处理
if (isBoss) {
this.x = canvas.width - 100;
this.y = CONFIG.OFFSET_Y + (CONFIG.GRID_ROWS * CONFIG.CELL_HEIGHT) / 2;
this.width = this.config.width;
this.height = this.config.height;
} else {
this.x = canvas.width + 50;
this.y = CONFIG.OFFSET_Y + r * CONFIG.CELL_HEIGHT + CONFIG.CELL_HEIGHT / 2;
}
this.isEating = false;
this.eatTarget = null;
this.animFrame = 0;
this.slowFactor = 1;
this.isDead = false;
}
update(dt) {
if (this.isDead) return;
this.animFrame += dt;
if (this.isBoss) {
// BOSS逻辑:缓慢向左移动,或者释放小僵尸
if (!this.isEating) {
this.x -= this.speed * this.slowFactor * (dt / 16);
if (this.x < canvas.width / 2) this.x = canvas.width / 2; // BOSS不会进屋
// BOSS特殊技能:每隔一段时间召唤普通僵尸
if (Math.random() < 0.005) {
const spawnR = Math.floor(Math.random() * CONFIG.GRID_ROWS);
gameState.zombies.push(new Zombie(spawnR, 'NORMAL'));
showMessage("BOSS召唤了小僵尸!");
}
}
} else {
// 普通僵尸逻辑
if (this.isEating && this.eatTarget) {
if (this.eatTarget.hp > 0) {
this.eatTarget.hp -= this.damage * (dt / 1000);
} else {
this.isEating = false;
this.eatTarget = null;
}
} else {
this.x -= this.speed * this.slowFactor * (dt / 16);
const col = Math.floor((this.x - CONFIG.OFFSET_X) / CONFIG.CELL_WIDTH);
if (col >= 0 && col < CONFIG.GRID_COLS) {
const plant = gameState.grid[this.r][col];
if (plant && Math.abs(plant.x - this.x) < 30) {
this.isEating = true;
this.eatTarget = plant;
}
}
if (this.slowFactor < 1) {
this.slowFactor += 0.001;
if (this.slowFactor > 1) this.slowFactor = 1;
}
}
if (this.x < CONFIG.OFFSET_X - 20) {
gameOver(false);
}
}
if (this.hp <= 0) {
this.isDead = true;
if (this.isBoss) {
gameOver(true); // BOSS死亡即胜利
}
}
}
draw(ctx) {
if (this.isDead) return;
ctx.save();
ctx.translate(this.x, this.y);
if (this.isBoss) {
// 绘制BOSS
ctx.fillStyle = this.config.color;
ctx.fillRect(-this.width/2, -this.height/2, this.width, this.height);
// BOSS眼睛
ctx.fillStyle = 'red';
ctx.beginPath();
ctx.arc(-20, -40, 10, 0, Math.PI*2);
ctx.arc(20, -40, 10, 0, Math.PI*2);
ctx.fill();
// BOSS牙齿
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.moveTo(-30, 20);
ctx.lineTo(-20, 40);
ctx.lineTo(-10, 20);
ctx.lineTo(0, 40);
ctx.lineTo(10, 20);
ctx.lineTo(20, 40);
ctx.lineTo(30, 20);
ctx.fill();
// 更新BOSS血条UI
const bossHud = document.getElementById('boss-hud');
const bossBar = document.getElementById('boss-hp-bar');
bossHud.style.display = 'block';
bossBar.style.width = `${(this.hp / this.maxHp) * 100}%`;
} else {
// 绘制普通僵尸
const wobble = Math.sin(this.animFrame / 100) * 2;
ctx.rotate(wobble * Math.PI / 180);
ctx.fillStyle = this.config.color;
ctx.fillRect(-15, -20, 30, 50);
ctx.fillStyle = '#9e9e9e';
ctx.beginPath();
ctx.arc(0, -25, 15, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(-5, -28, 4, 0, Math.PI*2);
ctx.arc(5, -28, 4, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = 'black';
ctx.beginPath();
ctx.arc(-5, -28, 1, 0, Math.PI*2);
ctx.arc(5, -28, 1, 0, Math.PI*2);
ctx.fill();
if (this.typeKey === 'CONEHEAD') {
ctx.fillStyle = '#ff9800';
ctx.beginPath();
ctx.moveTo(-12, -35);
ctx.lineTo(12, -35);
ctx.lineTo(0, -60);
ctx.fill();
} else if (this.typeKey === 'BUCKETHEAD') {
ctx.fillStyle = '#607d8b';
ctx.fillRect(-14, -45, 28, 20);
}
ctx.fillStyle = this.config.color;
if (this.isEating) {
ctx.fillRect(10, -10, 15, 8);
} else {
ctx.fillRect(10, -10 + wobble, 15, 8);
}
ctx.fillStyle = 'red';
ctx.fillRect(-15, -50, 30, 4);
ctx.fillStyle = '#0f0';
ctx.fillRect(-15, -50, 30 * (this.hp / this.maxHp), 4);
}
ctx.restore();
}
}
class Bullet {
constructor(x, y, r, damage, effect) {
this.x = x;
this.y = y;
this.r = r;
this.damage = damage;
this.effect = effect;
this.speed = 5;
this.active = true;
}
update() {
this.x += this.speed;
if (this.x > canvas.width) {
this.active = false;
}
}
draw(ctx) {
ctx.fillStyle = this.effect === 'slow' ? '#00bcd4' : '#4caf50';
ctx.beginPath();
ctx.arc(this.x, this.y, 6, 0, Math.PI*2);
ctx.fill();
}
}
class Sun {
constructor(x, y, targetY, isNatural) {
this.x = x;
this.y = y;
this.targetY = targetY || y;
this.isNatural = isNatural;
this.active = true;
this.life = 10000;
this.vy = isNatural ? 1 : -2;
}
update(dt) {
this.life -= dt;
if (this.life <= 0) this.active = false;
if (this.isNatural && this.y < this.targetY) {
this.y += this.vy;
} else if (!this.isNatural && this.y > this.targetY) {
this.y += this.vy;
}
}
draw(ctx) {
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(Date.now() / 500);
ctx.fillStyle = '#f1c40f';
for(let i=0; i<8; i++) {
ctx.beginPath();
ctx.rotate(Math.PI / 4);
ctx.rect(-2, -15, 4, 10);
ctx.fill();
}
ctx.beginPath();
ctx.arc(0, 0, 12, 0, Math.PI*2);
ctx.fillStyle = '#ffeb3b';
ctx.fill();
ctx.strokeStyle = '#f57f17';
ctx.lineWidth = 2;
ctx.stroke();
ctx.restore();
}
}
// 辅助函数
function createSun(x, y, isNatural) {
const targetY = isNatural ? CONFIG.OFFSET_Y + Math.random() * (CONFIG.GRID_ROWS * CONFIG.CELL_HEIGHT) : y - 50;
gameState.suns.push(new Sun(x, y, targetY, isNatural));
}
function createBullet(x, y, r, damage, effect) {
gameState.bullets.push(new Bullet(x, y, r, damage, effect));
}
function explodeCherryBomb(plant) {
// 范围伤害
const rangePx = plant.config.range * CONFIG.CELL_WIDTH;
gameState.zombies.forEach(z => {
const dx = z.x - plant.x;
const dy = z.y - plant.y;
const dist = Math.sqrt(dx*dx + dy*dy);
if (dist < rangePx) {
z.hp -= plant.config.damage;
}
});
// 特效
showMessage("轰隆!");
}
function showMessage(msg) {
const el = document.getElementById('message-area');
el.innerText = msg;
el.style.opacity = 1;
setTimeout(() => {
el.style.opacity = 0;
}, 2000);
}
// 游戏主循环
function gameLoop(timestamp) {
if (gameState.isPaused || gameState.isGameOver) return;
const dt = timestamp - gameState.lastTime;
gameState.lastTime = timestamp;
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBackground();
handleSpawning(dt, timestamp);
updateAndDrawEntities(dt);
checkCollisions();
updateUI();
requestAnimationFrame(gameLoop);
}
function drawBackground() {
ctx.fillStyle = '#4caf50';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = 'rgba(0,0,0,0.1)';
ctx.lineWidth = 1;
for (let r = 0; r <= CONFIG.GRID_ROWS; r++) {
ctx.beginPath();
ctx.moveTo(CONFIG.OFFSET_X, CONFIG.OFFSET_Y + r * CONFIG.CELL_HEIGHT);
ctx.lineTo(CONFIG.OFFSET_X + CONFIG.GRID_COLS * CONFIG.CELL_WIDTH, CONFIG.OFFSET_Y + r * CONFIG.CELL_HEIGHT);
ctx.stroke();
}
for (let c = 0; c <= CONFIG.GRID_COLS; c++) {
ctx.beginPath();
ctx.moveTo(CONFIG.OFFSET_X + c * CONFIG.CELL_WIDTH, CONFIG.OFFSET_Y);
ctx.lineTo(CONFIG.OFFSET_X + c * CONFIG.CELL_WIDTH, CONFIG.OFFSET_Y + CONFIG.GRID_ROWS * CONFIG.CELL_HEIGHT);
ctx.stroke();
}
ctx.fillStyle = '#8d6e63';
ctx.fillRect(0, CONFIG.OFFSET_Y, CONFIG.OFFSET_X, CONFIG.GRID_ROWS * CONFIG.CELL_HEIGHT);
}
function handleSpawning(dt, now) {
gameState.sunSpawnTimer += dt;
if (gameState.sunSpawnTimer > 10000) {
createSun(CONFIG.OFFSET_X + Math.random() * (CONFIG.GRID_COLS * CONFIG.CELL_WIDTH), -50, true);
gameState.sunSpawnTimer = 0;
}
if (gameState.mode === 'adventure' || gameState.mode === 'endless') {
// 检查是否所有波次已完成且无僵尸
if (gameState.wave > gameState.levelConfig.waves && gameState.zombies.length === 0 && !gameState.bossActive) {
gameOver(true);
return;
}
// BOSS关特殊逻辑
if (gameState.levelConfig.boss && gameState.wave >= gameState.levelConfig.waves && !gameState.bossActive) {
gameState.bossActive = true;
const boss = new Zombie(2, 'BOSS', true); // BOSS出现在中间行
gameState.zombies.push(boss);
gameState.bossEntity = boss;
showMessage("警告:巨型僵尸接近!");
return;
}
if (!gameState.bossActive) {
gameState.zombieSpawnTimer += dt;
let spawnRate = 5000;
if (gameState.wave > 5) spawnRate = 3000;
if (gameState.zombieSpawnTimer > spawnRate) {
const r = Math.floor(Math.random() * CONFIG.GRID_ROWS);
const types = gameState.levelConfig.types;
const type = types[Math.floor(Math.random() * types.length)];
gameState.zombies.push(new Zombie(r, type));
gameState.zombieSpawnTimer = 0;
if (gameState.zombies.length % 5 === 0 && !gameState.bossActive) gameState.wave++;
}
}
}
}
function updateAndDrawEntities(dt) {
gameState.plants.forEach(p => {
p.update(dt, Date.now());
p.draw(ctx);
});
gameState.zombies.forEach((z, index) => {
z.update(dt);
z.draw(ctx);
if (z.isDead) {
gameState.zombies.splice(index, 1);
if (z.isBoss) {
gameState.bossEntity = null;
document.getElementById('boss-hud').style.display = 'none';
}
}
});
gameState.bullets.forEach((b, index) => {
b.update();
b.draw(ctx);
if (!b.active) {
gameState.bullets.splice(index, 1);
}
});
gameState.suns.forEach((s, index) => {
s.update(dt);
s.draw(ctx);
if (!s.active) {
gameState.suns.splice(index, 1);
}
});
// 清理死亡植物
for (let r = 0; r < CONFIG.GRID_ROWS; r++) {
for (let c = 0; c < CONFIG.GRID_COLS; c++) {
const p = gameState.grid[r][c];
if (p && p.isDead) {
gameState.grid[r][c] = null;
gameState.plants = gameState.plants.filter(plant => plant !== p);
}
}
}
}
function checkCollisions() {
gameState.bullets.forEach(b => {
if (!b.active) return;
gameState.zombies.forEach(z => {
if (z.isDead) return;
// 简单的碰撞检测
let hit = false;
if (z.isBoss) {
// BOSS碰撞箱较大
if (b.r === 2 && Math.abs(z.x - b.x) < z.width/2 && Math.abs(z.y - b.y) < z.height/2) {
hit = true;
}
} else {
if (z.r === b.r && Math.abs(z.x - b.x) < 20 && Math.abs(z.y - b.y) < 30) {
hit = true;
}
}
if (hit) {
z.hp -= b.damage;
b.active = false;
if (b.effect === 'slow') {
z.slowFactor = 0.5;
}
}
});
});
}
function updateUI() {
document.getElementById('sun-display').innerText = Math.floor(gameState.sun);
const cards = document.querySelectorAll('.plant-card');
cards.forEach(card => {
const cost = parseInt(card.dataset.cost);
if (gameState.sun < cost) {
card.classList.add('disabled');
} else {
card.classList.remove('disabled');
}
});
}
// 交互处理
canvas.addEventListener('click', (e) => {
if (gameState.isPaused || gameState.isGameOver) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
let sunClicked = false;
gameState.suns.forEach((s, index) => {
if (Math.abs(x - s.x) < 30 && Math.abs(y - s.y) < 30) {
gameState.sun += CONFIG.SUN_VALUE;
s.active = false;
sunClicked = true;
}
});
if (sunClicked) return;
if (gameState.isShovelActive) {
const c = Math.floor((x - CONFIG.OFFSET_X) / CONFIG.CELL_WIDTH);
const r = Math.floor((y - CONFIG.OFFSET_Y) / CONFIG.CELL_HEIGHT);
if (r >= 0 && r < CONFIG.GRID_ROWS && c >= 0 && c < CONFIG.GRID_COLS) {
const plant = gameState.grid[r][c];
if (plant) {
gameState.grid[r][c] = null;
gameState.plants = gameState.plants.filter(p => p !== plant);
gameState.isShovelActive = false;
document.getElementById('shovel-btn').classList.remove('active');
}
}
return;
}
if (gameState.selectedPlant) {
const c = Math.floor((x - CONFIG.OFFSET_X) / CONFIG.CELL_WIDTH);
const r = Math.floor((y - CONFIG.OFFSET_Y) / CONFIG.CELL_HEIGHT);
if (r >= 0 && r < CONFIG.GRID_ROWS && c >= 0 && c < CONFIG.GRID_COLS) {
if (!gameState.grid[r][c]) {
const plantConfig = PLANTS[gameState.selectedPlant];
if (gameState.sun >= plantConfig.cost) {
gameState.sun -= plantConfig.cost;
const newPlant = new Plant(r, c, gameState.selectedPlant);
gameState.plants.push(newPlant);
gameState.grid[r][c] = newPlant;
gameState.selectedPlant = null;
document.querySelectorAll('.plant-card').forEach(c => c.classList.remove('selected'));
} else {
showMessage("阳光不足!");
}
} else {
showMessage("这里已经有植物了!");
}
}
}
});
// UI 事件绑定
function initUI() {
const plantBar = document.getElementById('plant-bar');
Object.keys(PLANTS).forEach(key => {
const p = PLANTS[key];
const div = document.createElement('div');
div.className = 'plant-card';
div.dataset.id = key;
div.dataset.cost = p.cost;
div.innerHTML = `
<div class="card-preview" style="background:${p.color}"></div>
<div class="card-cost">${p.cost}</div>
`;
div.onclick = () => {
if (gameState.sun >= p.cost) {
gameState.selectedPlant = key;
gameState.isShovelActive = false;
document.getElementById('shovel-btn').classList.remove('active');
document.querySelectorAll('.plant-card').forEach(c => c.classList.remove('selected'));
div.classList.add('selected');
}
};
plantBar.appendChild(div);
});
document.getElementById('shovel-btn').onclick = function() {
gameState.isShovelActive = !gameState.isShovelActive;
gameState.selectedPlant = null;
document.querySelectorAll('.plant-card').forEach(c => c.classList.remove('selected'));
this.classList.toggle('active');
};
const pauseBtn = document.getElementById('pause-btn');
const pausePanel = document.getElementById('pause-panel');
const resumeBtn = document.getElementById('resume-btn');
const quitBtn = document.getElementById('quit-btn');
pauseBtn.onclick = () => {
gameState.isPaused = true;
pausePanel.style.display = 'block';
};
resumeBtn.onclick = () => {
gameState.isPaused = false;
pausePanel.style.display = 'none';
gameState.lastTime = performance.now();
requestAnimationFrame(gameLoop);
};
quitBtn.onclick = () => {
location.reload();
};
// 结果面板
const resultPanel = document.getElementById('result-panel');
const nextLevelBtn = document.getElementById('next-level-btn');
const menuReturnBtn = document.getElementById('menu-return-btn');
nextLevelBtn.onclick = () => {
if (gameState.currentLevelId < LEVELS.length) {
startGame('adventure', gameState.currentLevelId + 1);
} else {
alert("恭喜通关所有关卡!");
location.reload();
}
};
menuReturnBtn.onclick = () => {
location.reload();
};
// 主菜单
document.getElementById('start-adventure').onclick = () => {
document.getElementById('menu-overlay').classList.add('hidden');
showLevelSelect();
};
document.getElementById('start-endless').onclick = () => startGame('endless', 1);
document.getElementById('start-minigame').onclick = () => startGame('minigame', 1);
document.getElementById('start-survival').onclick = () => startGame('survival', 1);
// 关卡选择
document.getElementById('back-to-main').onclick = () => {
document.getElementById('level-select-overlay').classList.add('hidden');
document.getElementById('menu-overlay').classList.remove('hidden');
};
}
function showLevelSelect() {
const overlay = document.getElementById('level-select-overlay');
const grid = document.getElementById('level-grid');
grid.innerHTML = '';
LEVELS.forEach(level => {
const btn = document.createElement('div');
btn.className = 'level-btn';
btn.innerText = level.id;
if (level.id > gameState.unlockedLevels) {
btn.classList.add('locked');
} else {
if (level.boss) btn.classList.add('boss-level');
btn.onclick = () => startGame('adventure', level.id);
}
grid.appendChild(btn);
});
overlay.classList.remove('hidden');
}
function startGame(mode, levelId = 1) {
gameState.mode = mode;
gameState.currentLevelId = levelId;
gameState.sun = CONFIG.INITIAL_SUN;
gameState.plants = [];
gameState.zombies = [];
gameState.bullets = [];
gameState.suns = [];
gameState.wave = 1;
gameState.isGameOver = false;
gameState.isPaused = false;
gameState.bossActive = false;
gameState.bossEntity = null;
// 加载关卡配置
if (mode === 'adventure') {
gameState.levelConfig = LEVELS.find(l => l.id === levelId) || LEVELS;
} else {
gameState.levelConfig = { waves: 100, types: ['NORMAL', 'CONEHEAD', 'BUCKETHEAD'], boss: false };
}
initGrid();
document.getElementById('menu-overlay').classList.add('hidden');
document.getElementById('level-select-overlay').classList.add('hidden');
document.getElementById('top-bar').classList.remove('hidden');
document.getElementById('boss-hud').style.display = 'none';
showMessage(`关卡 ${levelId} 开始!`);
gameState.lastTime = performance.now();
requestAnimationFrame(gameLoop);
}
function gameOver(victory) {
gameState.isGameOver = true;
const panel = document.getElementById('result-panel');
const title = document.getElementById('result-title');
const msg = document.getElementById('result-msg');
const nextBtn = document.getElementById('next-level-btn');
panel.style.display = 'block';
if (victory) {
title.innerText = "胜利!";
title.style.color = "#4caf50";
msg.innerText = `你成功通过了第 ${gameState.currentLevelId} 关。`;
// 解锁下一关
if (gameState.mode === 'adventure' && gameState.currentLevelId === gameState.unlockedLevels) {
gameState.unlockedLevels++;
}
if (gameState.mode === 'adventure' && gameState.currentLevelId < LEVELS.length) {
nextBtn.style.display = 'inline-block';
} else {
nextBtn.style.display = 'none';
}
} else {
title.innerText = "失败!";
title.style.color = "#f44336";
msg.innerText = "僵尸吃掉了你的脑子。";
nextBtn.style.display = 'none';
}
}
// 启动
initUI();
</script>
</body>
</html>
这里空空如也








有帮助,赞一个