pvz网页版 V1.1
2026-09-06 17:39:14
发布于:上海
4阅读
0回复
0点赞
没啥东西,就只有一个普通关卡和无尽
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>植物大战僵尸 - Web复刻版</title>
<style>
:root {
--bg-color: #2c3e50;
--panel-bg: #8d6e63;
--grass-light: #7cb342;
--grass-dark: #689f38;
--ui-shadow: 0 4px 6px rgba(0,0,0,0.3);
}
body {
margin: 0;
padding: 0;
background-color: var(--bg-color);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
color: white;
user-select: none;
}
#game-container {
position: relative;
box-shadow: 0 10px 20px rgba(0,0,0,0.5);
border-radius: 8px;
overflow: hidden;
background: #333;
}
canvas {
display: block;
background-color: var(--grass-light);
cursor: crosshair;
}
/* UI Overlay */
#ui-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.top-bar {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 80px;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
pointer-events: auto;
box-sizing: border-box;
}
.sun-counter {
display: flex;
align-items: center;
font-size: 24px;
font-weight: bold;
color: #ffeb3b;
text-shadow: 2px 2px 0 #000;
}
.sun-icon {
width: 30px;
height: 30px;
background: radial-gradient(circle, #fff176 40%, #fbc02d 100%);
border-radius: 50%;
margin-right: 10px;
border: 2px solid #f57f17;
}
.card-slot {
width: 60px;
height: 70px;
background: #5d4037;
border: 2px solid #3e2723;
border-radius: 4px;
margin: 0 5px;
cursor: pointer;
position: relative;
transition: transform 0.1s;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.card-slot:hover {
transform: scale(1.05);
border-color: #fff;
}
.card-slot.selected {
border-color: #ffeb3b;
box-shadow: 0 0 10px #ffeb3b;
}
.card-slot.disabled {
opacity: 0.5;
cursor: not-allowed;
filter: grayscale(100%);
}
.card-cost {
position: absolute;
bottom: 2px;
right: 2px;
font-size: 12px;
color: white;
font-weight: bold;
}
.plant-preview {
width: 40px;
height: 40px;
border-radius: 50%;
}
.controls {
display: flex;
gap: 10px;
}
.btn {
padding: 8px 16px;
background: #1976d2;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.btn:hover {
background: #1565c0;
}
.btn-shovel {
background: #d84315;
}
.btn-shovel.active {
background: #ff5722;
box-shadow: 0 0 8px #ff5722;
}
/* Modal */
#modal {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.85);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 100;
pointer-events: auto;
}
.modal-content {
background: #fff;
color: #333;
padding: 30px;
border-radius: 10px;
text-align: center;
max-width: 400px;
box-shadow: 0 10px 25px rgba(0,0,0,0.5);
}
h1 { margin-top: 0; color: #2e7d32; }
.mode-select {
display: flex;
gap: 20px;
margin-top: 20px;
}
.hidden { display: none !important; }
/* Pause Overlay */
#pause-overlay {
position: absolute;
top: 80px;
left: 0;
width: 100%;
height: calc(100% - 80px);
background: rgba(0,0,0,0.4);
display: flex;
align-items: center;
justify-content: center;
font-size: 40px;
font-weight: bold;
color: white;
text-shadow: 2px 2px 4px black;
pointer-events: none;
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="gameCanvas" width="900" height="600"></canvas>
<div id="ui-layer">
<div class="top-bar">
<div class="sun-counter">
<div class="sun-icon"></div>
<span id="sun-display">50</span>
</div>
<div style="display:flex;">
<!-- Plant Cards -->
<div class="card-slot" onclick="selectPlant(0)" id="card-0">
<div class="plant-preview" style="background: #4caf50;"></div>
<span class="card-cost">100</span>
</div>
<div class="card-slot" onclick="selectPlant(1)" id="card-1">
<div class="plant-preview" style="background: #ffeb3b;"></div>
<span class="card-cost">50</span>
</div>
<div class="card-slot" onclick="selectPlant(2)" id="card-2">
<div class="plant-preview" style="background: #8d6e63;"></div>
<span class="card-cost">50</span>
</div>
</div>
<div class="controls">
<button class="btn btn-shovel" id="shovel-btn" onclick="toggleShovel()">铲子</button>
<button class="btn" onclick="togglePause()">暂停</button>
</div>
</div>
<div id="pause-overlay" class="hidden">已暂停</div>
</div>
<div id="modal">
<div class="modal-content">
<h1>植物大战僵尸</h1>
<p>保卫你的房子,阻止僵尸入侵!</p>
<div class="mode-select">
<button class="btn" onclick="startGame('adventure')">闯关模式</button>
<button class="btn" onclick="startGame('endless')">无尽模式</button>
</div>
</div>
</div>
</div>
<script>
/**
* 游戏核心逻辑
*/
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// 游戏常量
const CELL_SIZE = 100;
const GRID_ROWS = 5;
const GRID_COLS = 9;
const TOP_OFFSET = 80; // UI高度
// 游戏状态
let gameState = {
sun: 150,
mode: 'adventure', // 'adventure' or 'endless'
level: 1,
isPaused: false,
isGameOver: false,
selectedPlant: null, // 0: Peashooter, 1: Sunflower, 2: Wallnut
isShovelActive: false,
lastTime: 0,
spawnTimer: 0,
sunDropTimer: 0,
zombiesKilled: 0,
wave: 1
};
// 实体数组
let plants = [];
let zombies = [];
let bullets = [];
let suns = [];
let particles = [];
// 植物定义
const PLANT_TYPES = [
{ name: 'Peashooter', cost: 100, hp: 100, color: '#4caf50', cooldown: 500 },
{ name: 'Sunflower', cost: 50, hp: 50, color: '#ffeb3b', cooldown: 500 },
{ name: 'Wallnut', cost: 50, hp: 400, color: '#8d6e63', cooldown: 1000 }
];
// 僵尸定义
const ZOMBIE_TYPES = [
{ name: 'Normal', hp: 100, speed: 0.3, color: '#555', damage: 10 },
{ name: 'Conehead', hp: 200, speed: 0.3, color: '#ff9800', damage: 10 },
{ name: 'Buckethead', hp: 400, speed: 0.25, color: '#9e9e9e', damage: 15 }
];
class Entity {
constructor(x, y, width, height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.markedForDeletion = false;
}
draw() {
// Base draw method
}
update(deltaTime) {
// Base update method
}
}
class Plant extends Entity {
constructor(col, row, typeIndex) {
super(col * CELL_SIZE + 10, row * CELL_SIZE + TOP_OFFSET + 10, 80, 80);
this.col = col;
this.row = row;
this.typeIndex = typeIndex;
this.type = PLANT_TYPES[typeIndex];
this.hp = this.type.hp;
this.timer = 0;
this.shootTimer = 0;
}
update(deltaTime) {
this.timer += deltaTime;
// Sunflower logic
if (this.typeIndex === 1) {
if (this.timer > 10000) { // 10 seconds
createSun(this.x + 40, this.y + 40, 25);
this.timer = 0;
}
}
// Peashooter logic
if (this.typeIndex === 0) {
this.shootTimer += deltaTime;
// Check if zombie in row
let zombieInRow = zombies.some(z => z.row === this.row && z.x > this.x);
if (zombieInRow && this.shootTimer > 1500) {
bullets.push(new Bullet(this.x + 60, this.y + 40, this.row));
this.shootTimer = 0;
}
}
}
draw() {
ctx.fillStyle = this.type.color;
ctx.beginPath();
if (this.typeIndex === 0) { // Peashooter
ctx.arc(this.x + 40, this.y + 40, 30, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#2e7d32'; // Mouth
ctx.fillRect(this.x + 60, this.y + 30, 20, 20);
} else if (this.typeIndex === 1) { // Sunflower
ctx.arc(this.x + 40, this.y + 40, 35, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#f57f17'; // Face
ctx.beginPath();
ctx.arc(this.x + 40, this.y + 40, 20, 0, Math.PI * 2);
ctx.fill();
} else { // Wallnut
ctx.ellipse(this.x + 40, this.y + 40, 30, 40, 0, 0, Math.PI * 2);
ctx.fill();
}
// HP Bar
if (this.hp < this.type.hp) {
ctx.fillStyle = 'red';
ctx.fillRect(this.x + 10, this.y + 70, 60 * (this.hp / this.type.hp), 5);
}
}
}
class Zombie extends Entity {
constructor(row, typeIndex) {
super(canvas.width, row * CELL_SIZE + TOP_OFFSET + 10, 80, 80);
this.row = row;
this.typeIndex = typeIndex;
this.type = ZOMBIE_TYPES[typeIndex];
this.hp = this.type.hp;
this.maxHp = this.type.hp;
this.speed = this.type.speed;
this.eating = false;
this.eatTimer = 0;
}
update(deltaTime) {
if (this.eating) {
this.eatTimer += deltaTime;
if (this.eatTimer > 1000) {
// Damage plant
let plant = plants.find(p => p.row === this.row &&
this.x < p.x + p.width && this.x + this.width > p.x);
if (plant) {
plant.hp -= this.type.damage;
if (plant.hp <= 0) {
plant.markedForDeletion = true;
this.eating = false;
}
} else {
this.eating = false;
}
this.eatTimer = 0;
}
} else {
this.x -= this.speed * (deltaTime / 16); // Normalize speed
}
// Check collision with plants
let collidingPlant = plants.find(p => p.row === this.row &&
this.x < p.x + p.width - 10 &&
this.x + this.width > p.x + 10);
if (collidingPlant) {
this.eating = true;
}
if (this.x < -50) {
gameOver();
}
}
draw() {
ctx.fillStyle = this.type.color;
ctx.fillRect(this.x, this.y, 60, 80);
// Eyes
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(this.x + 15, this.y + 20, 8, 0, Math.PI * 2);
ctx.arc(this.x + 35, this.y + 20, 8, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = 'black';
ctx.beginPath();
ctx.arc(this.x + 15, this.y + 20, 3, 0, Math.PI * 2);
ctx.arc(this.x + 35, this.y + 20, 3, 0, Math.PI * 2);
ctx.fill();
// HP Bar
ctx.fillStyle = 'red';
ctx.fillRect(this.x, this.y - 10, 60 * (this.hp / this.maxHp), 5);
}
}
class Bullet extends Entity {
constructor(x, y, row) {
super(x, y, 20, 20);
this.row = row;
this.speed = 5;
this.damage = 20;
}
update(deltaTime) {
this.x += this.speed * (deltaTime / 16);
if (this.x > canvas.width) {
this.markedForDeletion = true;
}
// Collision with zombies
for (let zombie of zombies) {
if (zombie.row === this.row &&
this.x < zombie.x + zombie.width &&
this.x + this.width > zombie.x) {
zombie.hp -= this.damage;
this.markedForDeletion = true;
createParticles(zombie.x + 30, zombie.y + 40, '#555');
if (zombie.hp <= 0) {
zombie.markedForDeletion = true;
gameState.zombiesKilled++;
checkWaveProgress();
}
break;
}
}
}
draw() {
ctx.fillStyle = '#76ff03';
ctx.beginPath();
ctx.arc(this.x + 10, this.y + 10, 10, 0, Math.PI * 2);
ctx.fill();
}
}
class Sun extends Entity {
constructor(x, y, value, isFalling = true) {
super(x, y, 40, 40);
this.value = value;
this.targetY = isFalling ? y + 100 + Math.random() * 100 : y;
this.isFalling = isFalling;
this.lifeTime = 8000; // 8 seconds to collect
this.age = 0;
}
update(deltaTime) {
this.age += deltaTime;
if (this.age > this.lifeTime) {
this.markedForDeletion = true;
}
if (this.isFalling && this.y < this.targetY) {
this.y += 1 * (deltaTime / 16);
}
}
draw() {
ctx.fillStyle = '#ffeb3b';
ctx.beginPath();
ctx.arc(this.x + 20, this.y + 20, 20, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#f57f17';
ctx.lineWidth = 2;
ctx.stroke();
// Shine effect
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
ctx.beginPath();
ctx.arc(this.x + 15, this.y + 15, 5, 0, Math.PI * 2);
ctx.fill();
}
}
class Particle {
constructor(x, y, color) {
this.x = x;
this.y = y;
this.color = color;
this.size = Math.random() * 5 + 2;
this.speedX = Math.random() * 2 - 1;
this.speedY = Math.random() * 2 - 1;
this.life = 1.0;
}
update() {
this.x += this.speedX;
this.y += this.speedY;
this.life -= 0.05;
}
draw() {
ctx.globalAlpha = this.life;
ctx.fillStyle = this.color;
ctx.fillRect(this.x, this.y, this.size, this.size);
ctx.globalAlpha = 1.0;
}
}
function createParticles(x, y, color) {
for(let i=0; i<5; i++) {
particles.push(new Particle(x, y, color));
}
}
function createSun(x, y, value, isFalling = false) {
suns.push(new Sun(x, y, value, isFalling));
}
// 游戏控制函数
function selectPlant(index) {
if (gameState.isPaused || gameState.isGameOver) return;
// Deselect others
document.querySelectorAll('.card-slot').forEach(el => el.classList.remove('selected'));
if (gameState.selectedPlant === index) {
gameState.selectedPlant = null; // Toggle off
} else {
gameState.selectedPlant = index;
document.getElementById(`card-${index}`).classList.add('selected');
}
gameState.isShovelActive = false;
document.getElementById('shovel-btn').classList.remove('active');
}
function toggleShovel() {
if (gameState.isPaused || gameState.isGameOver) return;
gameState.isShovelActive = !gameState.isShovelActive;
gameState.selectedPlant = null;
document.querySelectorAll('.card-slot').forEach(el => el.classList.remove('selected'));
const btn = document.getElementById('shovel-btn');
if (gameState.isShovelActive) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
}
function togglePause() {
if (gameState.isGameOver) return;
gameState.isPaused = !gameState.isPaused;
const overlay = document.getElementById('pause-overlay');
if (gameState.isPaused) {
overlay.classList.remove('hidden');
} else {
overlay.classList.add('hidden');
gameState.lastTime = performance.now();
requestAnimationFrame(gameLoop);
}
}
function startGame(mode) {
gameState.mode = mode;
gameState.level = 1;
gameState.wave = 1;
gameState.sun = 150;
gameState.isGameOver = false;
gameState.isPaused = false;
gameState.zombiesKilled = 0;
plants = [];
zombies = [];
bullets = [];
suns = [];
particles = [];
document.getElementById('modal').classList.add('hidden');
updateUI();
gameState.lastTime = performance.now();
requestAnimationFrame(gameLoop);
}
function gameOver() {
gameState.isGameOver = true;
const modal = document.getElementById('modal');
modal.classList.remove('hidden');
modal.querySelector('h1').innerText = "僵尸吃掉了你的脑子!";
modal.querySelector('p').innerText = `你坚持了 ${gameState.wave} 波`;
modal.querySelector('.mode-select').innerHTML = '<button class="btn" onclick="location.reload()">重新开始</button>';
}
function checkWaveProgress() {
let zombiesToKill = gameState.wave * 5 + 5;
if (gameState.zombiesKilled >= zombiesToKill) {
gameState.wave++;
// Bonus sun for clearing wave
createSun(400, 300, 25, false);
if (gameState.mode === 'adventure' && gameState.wave > 5) {
// Win adventure
gameState.isGameOver = true;
const modal = document.getElementById('modal');
modal.classList.remove('hidden');
modal.querySelector('h1').innerText = "胜利!";
modal.querySelector('p').innerText = "你成功保卫了房子!";
modal.querySelector('.mode-select').innerHTML = '<button class="btn" onclick="location.reload()">再玩一次</button>';
}
}
}
function updateUI() {
document.getElementById('sun-display').innerText = Math.floor(gameState.sun);
// Update card availability
PLANT_TYPES.forEach((type, index) => {
const card = document.getElementById(`card-${index}`);
if (gameState.sun < type.cost) {
card.classList.add('disabled');
} else {
card.classList.remove('disabled');
}
});
}
// 输入处理
canvas.addEventListener('mousedown', (e) => {
if (gameState.isPaused || gameState.isGameOver) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// Check Sun Clicks first
let sunClicked = false;
for (let i = suns.length - 1; i >= 0; i--) {
let s = suns[i];
let dist = Math.sqrt((x - (s.x + 20))**2 + (y - (s.y + 20))**2);
if (dist < 30) {
gameState.sun += s.value;
s.markedForDeletion = true;
sunClicked = true;
updateUI();
break;
}
}
if (sunClicked) return;
// Grid Logic
if (y < TOP_OFFSET) return; // Clicked on UI area
const col = Math.floor(x / CELL_SIZE);
const row = Math.floor((y - TOP_OFFSET) / CELL_SIZE);
if (col >= 0 && col < GRID_COLS && row >= 0 && row < GRID_ROWS) {
if (gameState.isShovelActive) {
// Remove plant
const plantIndex = plants.findIndex(p => p.col === col && p.row === row);
if (plantIndex !== -1) {
plants.splice(plantIndex, 1);
gameState.isShovelActive = false;
document.getElementById('shovel-btn').classList.remove('active');
}
} else if (gameState.selectedPlant !== null) {
// Plant something
// Check if occupied
const occupied = plants.some(p => p.col === col && p.row === row);
if (!occupied) {
const type = PLANT_TYPES[gameState.selectedPlant];
if (gameState.sun >= type.cost) {
gameState.sun -= type.cost;
plants.push(new Plant(col, row, gameState.selectedPlant));
updateUI();
// Optional: Deselect after planting? No, keep selected for multiple plants
}
}
}
}
});
// 主循环
function gameLoop(timestamp) {
if (gameState.isPaused || gameState.isGameOver) return;
const deltaTime = timestamp - gameState.lastTime;
gameState.lastTime = timestamp;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw Background (Lawn)
for (let r = 0; r < GRID_ROWS; r++) {
for (let c = 0; c < GRID_COLS; c++) {
ctx.fillStyle = (r + c) % 2 === 0 ? '#7cb342' : '#689f38';
ctx.fillRect(c * CELL_SIZE, r * CELL_SIZE + TOP_OFFSET, CELL_SIZE, CELL_SIZE);
}
}
// Spawn Zombies
gameState.spawnTimer += deltaTime;
let spawnInterval = Math.max(2000, 10000 - (gameState.wave * 1000)); // Gets faster
if (gameState.mode === 'endless') spawnInterval = Math.max(1000, 5000 - (gameState.wave * 200));
if (gameState.spawnTimer > spawnInterval) {
let row = Math.floor(Math.random() * GRID_ROWS);
let typeIdx = 0;
if (gameState.wave > 2 && Math.random() > 0.7) typeIdx = 1;
if (gameState.wave > 4 && Math.random() > 0.8) typeIdx = 2;
zombies.push(new Zombie(row, typeIdx));
gameState.spawnTimer = 0;
}
// Natural Sun Drop
gameState.sunDropTimer += deltaTime;
if (gameState.sunDropTimer > 10000) {
let x = Math.random() * (canvas.width - 100) + 50;
createSun(x, -50, 25, true);
gameState.sunDropTimer = 0;
}
// Update & Draw Entities
[...plants, ...zombies, ...bullets, ...suns].forEach(entity => {
entity.update(deltaTime);
entity.draw();
});
// Particles
for (let i = particles.length - 1; i >= 0; i--) {
particles[i].update();
particles[i].draw();
if (particles[i].life <= 0) particles.splice(i, 1);
}
// Cleanup
plants = plants.filter(p => !p.markedForDeletion);
zombies = zombies.filter(z => !z.markedForDeletion);
bullets = bullets.filter(b => !b.markedForDeletion);
suns = suns.filter(s => !s.markedForDeletion);
requestAnimationFrame(gameLoop);
}
// Initial Render
ctx.fillStyle = '#333';
ctx.fillRect(0, 0, canvas.width, canvas.height);
</script>
</body>
</html>
这里空空如也








有帮助,赞一个