简单的网页游戏之植物大战僵尸

 一、HTML部分

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport" />
		<title>植物大战僵尸</title>
		<link rel="stylesheet" href="css/common.css">
		<link rel="stylesheet" href="css/style.css">
	</head>
	<body>
		<div id="js-startGame-btn" class="startGame-btn">点击开始游戏</div>
		<!-- 游戏主场景 -->
		<div class="content-box">
			<canvas id="canvas" width="1400" height="600"></canvas>
		</div>
		<!-- 卡片列表 -->
		<ul class="cards-list">
			<li class="cards-item" data-section="sunflower">
				<div class="card-intro">
					<span>向日葵</span>
					<span>冷却时间:5秒</span>
					<span>向日葵是你收集额外阳光必不可少的植物。为什么不多种一些呢?</span>
				</div>
			</li>
			<li class="cards-item" data-section="wallnut">
				<div class="card-intro">
					<span>坚果墙</span>
					<span>冷却时间:12秒</span>
					<span>墙果具备坚硬的外壳,你可以使用他们来保护其他植物</span>
				</div>
			</li>
			<li class="cards-item" data-section="peashooter">
				<div class="card-intro">
					<span>豌豆射手</span>
					<span>冷却时间:7秒</span>
					<span>向敌人射出豌豆</span>
				</div>
			</li>
			<li class="cards-item" data-section="repeater">
				<div class="card-intro">
					<span>双发豌豆射手</span>
					<span>冷却时间:10秒</span>
					<span>向敌人连续射出两发豌豆</span>
				</div>
			</li>
			<li class="cards-item" data-section="gatlingpea">
				<div class="card-intro">
					<span>加特林射手</span>
					<span>冷却时间:15秒</span>
					<span>向敌人快速射出豌豆</span>
				</div>
			</li>
			<li class="cards-item" data-section="chomper">
				<div class="card-intro">
					<span>食人花</span>
					<span>冷却时间:15秒</span>
					<span>食人花能吞下所有自己可以吞下的僵尸</span>
				</div>
			</li>
			<li class="cards-item" data-section="cherrybomb">
				<div class="card-intro">
					<span>樱桃炸弹</span>
					<span>冷却时间:25秒</span>
					<span>樱桃炸弹能够炸飞一片区域(以鼠标点击位置为中心9格内)的所有僵尸。</span>
				</div>
			</li>
		</ul>
		<!-- 游戏控制按键 -->
		<div class="menu-box">
			<div id="pauseGame" class="contro-btn">暂停</div>
			<div id="restartGame" class="contro-btn">开始游戏</div>
		</div>
		<!-- 系统自动生成阳光 -->
		<img class="sun-img systemSun" src="images/sun.gif" alt="">
		<script src="js/common.js"></script>
		<script src="js/scene.js"></script>
		<script src="js/game.js"></script>
		<script src="js/main.js"></script>
	</body>
</html>

二、css样式

1.style.css

body {
	background: #f5f5d8;
}

.content-box {
	display: block;
	width: 900px;
	height: 600px;
	margin: 0;
	overflow: hidden;
	border: 1px solid black;
	border-top: none;
}

#canvas {
	display: block;
	margin-left: -120px;
	border: 1px solid black;
	background: #000;
}

.intro-game {
	position: absolute;
	left: 460px;
	top: 10px;
	width: 430px;
	font-size: 18px;
	line-height: 25px;
	color: #F60;
}

.intro-game a {
	color: #ffbe93;
}

.startGame-btn {
	position: absolute;
	left: 240px;
	top: 520px;
	width: 321px;
	font-size: 18px;
	color: #FF0;
	height: 69px;
	line-height: 80px;
	text-align: center;
	cursor: pointer;
	background: url(../images/LogoLine.png) no-repeat center/contain;
	z-index: 9;
}

.cards-list {
	display: none;
	position: absolute;
	left: 0;
	top: 0;
	width: 100px;
	z-index: 99;
}

.cards-list .cards-item {
	position: relative;
	float: left;
	width: 100%;
	height: 60px;
	cursor: pointer;
}

.cards-list .cards-item:hover .card-intro {
	display: block;
}

.cards-list .cards-item .card-intro {
	display: none;
	position: absolute;
	left: 100px;
	top: 0;
	width: 150px;
	height: auto;
	padding-bottom: 5px;
	border: 1px solid #000;
	background: #FFFFDD;
	color: #000;
}

.cards-list .cards-item .card-intro span {
	display: inline-block;
	width: 100%;
	height: 20px;
	font-size: 12px;
	line-height: 20px;
	text-align: center;
}

.menu-box {
	display: none;
	position: absolute;
	left: 674px;
	top: 0;
	width: 226px;
	height: 41px;
	line-height: 41px;
}

.menu-box .contro-btn {
	display: none;
	float: left;
	width: 113px;
	height: 100%;
	line-height: inherit;
	color: #00CB08;
	text-align: center;
	font-size: 16px;
	font-weight: bold;
	font-family: '黑体';
	background: url(../images/button.png) center/cover;
	cursor: pointer;
}

.menu-box .contro-btn.show {
	display: block;
}

.sun-img {
	position: absolute;
	left: 300px;
	top: -100px;
	opacity: 1;
	cursor: pointer;
	z-index: 100;
}

.show {
	display: block;
}

.none {
	display: none;
}

.check-log-btn {
	position: absolute;
	left: 600px;
	top: 532px;
	font-size: 14px;
	color: #FF0;
	height: 50px;
	line-height: 50px;
	text-align: center;
	text-decoration: underline;
	cursor: pointer;
	z-index: 9;
}

.update-log {
	/*display: none;*/
	position: absolute;
	left: 130px;
	top: 50px;
	width: 640px;
	height: 500px;
	text-align: center;
	border: 1px solid #FFF;
	background-color: #000;
	color: #FFE;
	z-index: 250;
	padding: 10px;
}

.update-log .title {
	font-size: 16px;
	color: #FF0;
	margin: 10px 0;
}

.update-log dl {
	text-align: left;
	height: 400px;
	overflow: auto;
}

.update-log dl dd,
.update-log dl dt {
	font-size: 14px;
	line-height: 18px;
}

.update-log dl dd {
	padding-left: 30px;
}

.update-log .close-log-btn {
	position: absolute;
	bottom: 20px;
	left: 260px;
	cursor: pointer;
	width: 120px;
	height: 30px;
	line-height: 24px;
	border-left: 3px solid #85411C;
	border-right: 3px solid #4E250C;
	border-top: 3px solid #85411C;
	border-bottom: 3px solid #4E250C;
	background-color: #8F431B;
	color: #FC6;
	font-weight: bold;
	font-size: 14px;
}

2.common.css

@charset 'utf-8';
/*===========================  重置初始样式 start  ===========================*/
* {box-sizing: border-box;}
html,div,body,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td {margin:0;padding:0;border:0;outline:0;font-size:100%;line-height:100%;vertical-align:baseline;background:transparent;}
ol,ul,li,ul li{list-style:none;}
:focus {outline:0;}
ins {text-decoration:none;}
table {border-collapse:collapse;border-spacing:0;}
html,body{height:100%;}article,aside,dialog,details,footer,figure,header,hgroup,menu,media,nav,section{display:block;padding:0;margin:0;}
body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,iframe,fieldset,input,textarea,p,blockquote,th,td,button{padding:0;margin:0;}
fieldset,img,button{border:0;}
body,input,select,textarea{font-family:'微软雅黑 Regular','微软雅黑';color:#333;font-size:12px;}

body{background:#fff;}
table{border-collapse:collapse;border-spacing:0;}
a{text-decoration:none;}
a:hover{text-decoration:none;}
/*===========================  重置初始样式 end  ===========================*/


/*===========================  通用样式 start  ===========================*/
/*---------------------------  常用样式 start  ---------------------------*/
.wh_mar{width:1200px;margin:0 auto;}
.fl{float:left;}
.fr{float:right;}
.wh{width:100%;display:inline-block;}
.none{display:none;}
.ov{overflow:hidden;}
.re{position:relative;}
.just{text-align:justify;text-justify:inter-ideograph;}
.wrap{word-break:break-all;word-wrap:break-word;}
/*---------------------------  常用样式 end  ---------------------------*/

/*---------------------------  边距 start  ---------------------------*/
.mt5{margin-top:5px;}.mt10{margin-top:10px;}.mt15{margin-top:15px;}.mt20{margin-top:20px;}.mt25{margin-top:25px;}.mt30{margin-top:30px;}.mt35{margin-top:35px;}.mt40{margin-top:40px;}.mt45{margin-top:45px;}.mt50{margin-top:50px;}
.ml5{margin-left:5px;}.ml10{margin-left:10px;}.ml15{margin-left:15px;}.ml20{margin-left:20px;}.ml25{margin-left:25px;}.ml30{margin-left:30px;}
.mr5{margin-right:5px;}.mr10{margin-right:10px;}.mr15{margin-right:15px;}.mr20{margin-right:20px;}.mr25{margin-right:25px;}.mr30{margin-right:30px;}
.mb5{margin-bottom:5px;}.mb10{margin-bottom:10px;}.mb15{margin-bottom:15px;}.mb20{margin-bottom:20px;}.mb25{margin-bottom:25px;}.mb30{margin-bottom:30px;}
/*---------------------------  边距 end  ---------------------------*/

/*---------------------------  填充 start  ---------------------------*/
.pt5{padding-top:5px;}.pt10{padding-top:10px;}.pt15{padding-top:15px;}.pt20{padding-top:20px;}.pt25{padding-top:25px;}.pt30{padding-top:30px;}
.pl5{padding-left:5px;}.pl10{padding-left:10px;}.pl15{padding-left:15px;}.pl20{padding-left:20px;}.pl25{padding-left:25px;}.pl30{padding-left:30px;}
.pr5{padding-right:5px;}.pr10{padding-right:10px;}.pr15{padding-right:15px;}.pr20{padding-right:20px;}.pr25{padding-right:25px;}.pr30{padding-right:30px;}
.pb5{padding-bottom:5px;}.pb10{padding-bottom:10px;}.pb15{padding-bottom:15px;}.pb20{padding-bottom:20px;}.pb25{padding-bottom:25px;}.pb30{padding-bottom:30px;}
/*---------------------------  填充 end  ---------------------------*/

/*===========================  通用样式 end  ===========================*/


三、JavaScript部分

1.common.js

// 封装打印日志方法
const log = console.log.bind(console)
// 生成图片对象方法
const imageFromPath = function(src) {
	let img = new Image()
	img.src = './images/' + src
	return img
}
// 原生动画参数
const keyframesOptions = {
	iterations: 1,
	iterationStart: 0,
	delay: 0,
	endDelay: 0,
	direction: 'alternate',
	duration: 3000,
	fill: 'forwards',
	easing: 'ease-out',
}
// 图片素材路径
const allImg = {
	startBg: 'coverBg.jpg', // 首屏背景图
	bg: 'background1.jpg', // 游戏背景
	bullet: 'bullet.png', // 子弹普通状态
	bulletHit: 'bullet_hit.png', // 子弹击中敌人状态
	sunback: 'sunback.png', // 阳光背景框
	zombieWon: 'zombieWon.png', // 僵尸胜利画面
	car: 'car.png', // 小汽车图片
	loading: { // loading 画面
		write: {
			path: 'loading/loading_*.png',
			len: 3,
		},
	},
	plantsCard: { // 植物卡片
		sunflower: { // 向日葵
			img: 'cards/plants/SunFlower.png',
			imgG: 'cards/plants/SunFlowerG.png',
		},
		peashooter: { // 豌豆射手
			img: 'cards/plants/Peashooter.png',
			imgG: 'cards/plants/PeashooterG.png',
		},
		repeater: { // 双发射手
			img: 'cards/plants/Repeater.png',
			imgG: 'cards/plants/RepeaterG.png',
		},
		gatlingpea: { // 加特林射手
			img: 'cards/plants/GatlingPea.png',
			imgG: 'cards/plants/GatlingPeaG.png',
		},
		cherrybomb: { // 樱桃炸弹
			img: 'cards/plants/CherryBomb.png',
			imgG: 'cards/plants/CherryBombG.png',
		},
		wallnut: { // 坚果墙
			img: 'cards/plants/WallNut.png',
			imgG: 'cards/plants/WallNutG.png',
		},
		chomper: { // 食人花
			img: 'cards/plants/Chomper.png',
			imgG: 'cards/plants/ChomperG.png',
		},
	},
	plants: { // 植物 
		sunflower: { // 向日葵
			idle: {
				path: 'plants/sunflower/idle/idle_*.png',
				len: 18,
			},
		},
		peashooter: { // 豌豆射手
			idle: {
				path: 'plants/peashooter/idle/idle_*.png',
				len: 8,
			},
			attack: {
				path: 'plants/peashooter/attack/attack_*.png',
				len: 8,
			},
		},
		repeater: { // 双发射手
			idle: {
				path: 'plants/repeater/idle/idle_*.png',
				len: 15,
			},
			attack: {
				path: 'plants/repeater/attack/attack_*.png',
				len: 15,
			},
		},
		gatlingpea: { // 加特林射手
			idle: {
				path: 'plants/gatlingpea/idle/idle_*.png',
				len: 13,
			},
			attack: {
				path: 'plants/gatlingpea/attack/attack_*.png',
				len: 13,
			},
		},
		cherrybomb: { // 樱桃炸弹
			idle: {
				path: 'plants/cherrybomb/idle/idle_*.png',
				len: 7,
			},
			attack: {
				path: 'plants/cherrybomb/attack/attack_*.png',
				len: 5,
			},
		},
		wallnut: { // 坚果墙
			idleH: { // 血量高时动画
				path: 'plants/wallnut/idleH/idleH_*.png',
				len: 16,
			},
			idleM: { // 血量中等时动画
				path: 'plants/wallnut/idleM/idleM_*.png',
				len: 11,
			},
			idleL: { // 血量低时动画
				path: 'plants/wallnut/idleL/idleL_*.png',
				len: 15,
			},
		},
		chomper: { // 食人花
			idle: { // 站立动画
				path: 'plants/chomper/idle/idle_*.png',
				len: 13,
			},
			attack: { // 攻击动画
				path: 'plants/chomper/attack/attack_*.png',
				len: 8,
			},
			digest: { // 消化阶段动画
				path: 'plants/chomper/digest/digest_*.png',
				len: 6,
			}
		},
	},
	zombies: { // 僵尸
		idle: { // 站立动画
			path: 'zombies/idle/idle_*.png',
			len: 31,
		},
		run: { // 移动动画
			path: 'zombies/run/run_*.png',
			len: 31,
		},
		attack: { // 攻击动画
			path: 'zombies/attack/attack_*.png',
			len: 21,
		},
		dieboom: { // 被炸死亡动画
			path: 'zombies/dieboom/dieboom_*.png',
			len: 20,
		},
		dying: { // 濒死动画
			head: {
				path: 'zombies/dying/head/head_*.png',
				len: 12,
			},
			body: {
				path: 'zombies/dying/body/body_*.png',
				len: 18,
			},
		},
		die: { // 死亡动画
			head: {
				path: 'zombies/dying/head/head_*.png',
				len: 12,
			},
			body: {
				path: 'zombies/die/die_*.png',
				len: 10,
			},
		},
	}
}

2.scene.js

/**
 * 阳光类
 */
class SunNum {
	constructor() {
		let s = {
			img: null, // 当前显示阳光背景框
			sun_num: window._main.allSunVal, // 阳光总数量
			x: 105, // x 轴坐标
			y: 0, // y 轴坐标
		}
		Object.assign(this, s)
	}
	// 创建,并初始化当前对象
	static new() {
		let s = new this()
		s.img = imageFromPath(allImg.sunback)
		return s
	}
	// 绘制方法
	draw(cxt) {
		let self = this
		// 绘制阳光背景框
		cxt.drawImage(self.img, self.x + 120, self.y)
		// 绘制阳光数量
		cxt.fillStyle = 'black'
		cxt.font = '24px Microsoft YaHei'
		cxt.fontWeight = 700
		cxt.fillText(self.sun_num, self.x + 175, self.y + 27)
	}
	// 改变阳光数量值
	changeSunNum(num = 25) {
		let self = this
		window._main.allSunVal += num
		self.sun_num += num
	}
}
/**
 * 卡片类
 */
class Card {
	constructor(obj) {
		let c = {
			name: obj.name, // 当前卡片名称
			canGrow: true, // 阳光数量是否够种植植物
			canClick: true, // 是否可点击,并放置卡片对应植物
			img: null, // 当前显示卡片对象
			images: [], // 当前卡片图片序列
			timer: null, // 定时器,每隔一段时间切换可点击状态
			timer_spacing: obj.timer_spacing, // 定时器时间间隔,卡片冷却时间
			timer_num: 1, // 倒计时显示数字
			sun_val: obj.sun_val, // 每次种植消耗阳光数量
			row: obj.row, // 卡片行坐标
			x: 0, // 卡片 x 轴坐标
			y: obj.y, // 卡片 y 轴坐标,由行坐标计算得出
		}
		Object.assign(this, c)
	}
	// 创建,并初始化当前对象
	static new(obj) {
		let b = new this(obj)
		b.images.push(imageFromPath(allImg.plantsCard[b.name].img)) // 生成可点击卡片对象
		b.images.push(imageFromPath(allImg.plantsCard[b.name].imgG)) // 生成不可点击卡片对象
		b.canClick ? b.img = b.images[0] : b.img = b.images[1]
		b.timer_num = b.timer_spacing / 1000 // 设置卡片冷却时间  
		return b
	}
	// 绘制卡片方法
	draw(cxt) {
		let self = this,
			marginLeft = 120
		// 根据阳光总数量判断能否种植植物
		self.sun_val > window._main.allSunVal ? self.canGrow = false : self.canGrow = true
		// 根据当前状态渲染对应卡片
		self.canGrow && self.canClick ? self.img = self.images[0] : self.img = self.images[1]
		// 绘制卡片
		cxt.drawImage(self.img, self.x + marginLeft, self.y)
		// 绘制消耗阳光数量
		cxt.fillStyle = 'black'
		cxt.font = '16px Microsoft YaHei'
		cxt.fillText(self.sun_val, self.x + marginLeft + 60, self.y + 55)
		// 绘制倒计时
		if (!self.canClick && self.canGrow) {
			cxt.fillStyle = 'rgb(255, 255, 0)'
			cxt.font = '20px Microsoft YaHei'
			cxt.fillText(self.timer_num, self.x + marginLeft + 30, self.y + 35)
		}
	}
	// 计算倒计时时间
	drawCountDown() {
		let self = this
		self.timer = setInterval(function() {
			if (self.timer_num !== 0) {
				self.timer_num--
			} else {
				clearInterval(self.timer)
				self.timer_num = self.timer_spacing / 1000
			}
		}, 1000)
	}
	// 切换当前状态(可点击、不可点击状态)
	changeState() {
		let self = this
		if (!self.canClick) {
			// 设置定时器,恢复可点击状态
			self.timer = setTimeout(() => {
				self.canClick = true
			}, self.timer_spacing)
		}
	}
}
/**
 * 除草车类
 */
class Car {
	constructor(obj) {
		let c = {
			img: imageFromPath(allImg.car), // 生成子弹普通形态图片对象
			state: 1, // 当前状态值
			state_NORMALE: 1, // 正常状态
			state_ATTACK: 2, // 攻击状态
			w: 71, // 图片宽度
			h: 57, // 图片高度
			x: obj.x, // x轴坐标
			y: obj.y, // y轴坐标
			row: obj.row, // 角色初始化行坐标
		}
		Object.assign(this, c)
	}
	// 创建,并初始化当前对象
	static new(obj) {
		let c = new this(obj)
		return c
	}
	// 绘制方法
	draw(game, cxt) {
		let self = this
		self.canMove()
		// 移动除草车
		self.state === self.state_ATTACK && self.step(game)
		// 绘制除草车
		cxt.drawImage(self.img, self.x, self.y)
	}
	// 移动方法
	step(game) {
		// 只有在游戏运行状态除草车才能移动
		game.state === game.state_RUNNING ? this.x += 15 : this.x = this.x
	}
	// 判断是否移动小车 (zombie.x < 150时)
	canMove() {
		let self = this
		for (let zombie of window._main.zombies) {
			if (zombie.row === self.row) {
				if (zombie.x < 150) { // 当僵尸靠近房子时,启动除草车
					self.state = self.state_ATTACK
				}
				if (self.state === self.state_ATTACK) { // 当除草车启动时,清除整行僵尸
					if (zombie.x - self.x < self.w && zombie.x < 950) {
						zombie.life = 0
						zombie.changeAnimation('die')
					}
				}
			}
		}
	}
}
/**
 * 子弹类
 */
class Bullet {
	constructor(plant) {
		let b = {
			img: imageFromPath(allImg.bullet), // 生成子弹普通形态图片对象
			w: 56,
			h: 34,
			x: 0,
			y: 0,
		}
		Object.assign(this, b)
	}
	// 创建,并初始化当前对象
	static new(plant) {
		let b = new this(plant)
		// 定义子弹的坐标值
		switch (plant.section) {
			case 'peashooter':
				b.x = plant.x + 30
				b.y = plant.y
				break
			case 'repeater':
				b.x = plant.x + 30
				b.y = plant.y
				break
			case 'gatlingpea':
				b.x = plant.x + 30
				b.y = plant.y + 10
				break
		}
		return b
	}
	// 绘制方法
	draw(game, cxt) {
		let self = this
		// 移动子弹
		self.step(game)
		// 绘制子弹
		cxt.drawImage(self.img, self.x, self.y)
	}
	// 移动方法
	step(game) {
		// 只有在游戏运行状态子弹才能移动
		game.state === game.state_RUNNING ? this.x += 4 : this.x = this.x
	}
}
/**
 * 动画类
 */
class Animation {
	constructor(role, action, fps) {
		let a = {
			type: role.type, // 动画类型(植物、僵尸等等)
			section: role.section, // 植物或者僵尸类别(向日葵、豌豆射手)
			action: action, // 根据传入动作生成不同动画对象数组
			images: [], // 当前引入角色图片对象数组
			img: null, // 当前显示角色图片
			imgIdx: 0, // 当前角色图片序列号
			count: 0, // 计数器,控制动画运行
			imgHead: null, // 当前显示角色头部图片
			imgBody: null, // 当前显示角色身体图片
			imgIdxHead: 0, // 当前角色头部图片序列号
			imgIdxBody: 0, // 当前角色身体图片序列号
			countHead: 0, // 当前角色头部计数器,控制动画运行
			countBody: 0, // 当前角色身体计数器,控制动画运行
			fps: fps, // 角色动画运行速度系数,值越小,速度越快
		}
		Object.assign(this, a)
	}
	// 创建,并初始化当前对象
	static new(role, action, fps) {
		let a = new this(role, action, fps)
		// 濒死动画、死亡动画对象(僵尸)
		if (action === 'dying' || action === 'die') {
			a.images = {
				head: [],
				body: [],
			}
			a.create()
		} else {
			a.create()
			a.images[0].onload = function() {
				role.w = this.width
				role.h = this.height
			}
		}
		return a
	}
	/**
	 * 为角色不同动作创造动画序列
	 */
	create() {
		let self = this,
			section = self.section // 植物种类
		switch (self.type) {
			case 'plant':
				for (let i = 0; i < allImg.plants[section][self.action].len; i++) {
					let idx = i < 10 ? '0' + i : i,
						path = allImg.plants[section][self.action].path
					// 依次添加动画序列
					self.images.push(imageFromPath(path.replace(/\*/, idx)))
				}
				break
			case 'zombie':
				// 濒死动画、死亡动画对象,包含头部动画以及身体动画
				if (self.action === 'dying' || self.action === 'die') {
					for (let i = 0; i < allImg.zombies[self.action].head.len; i++) {
						let idx = i < 10 ? '0' + i : i,
							path = allImg.zombies[self.action].head.path
						// 依次添加动画序列
						self.images.head.push(imageFromPath(path.replace(/\*/, idx)))
					}
					for (let i = 0; i < allImg.zombies[self.action].body.len; i++) {
						let idx = i < 10 ? '0' + i : i,
							path = allImg.zombies[self.action].body.path
						// 依次添加动画序列
						self.images.body.push(imageFromPath(path.replace(/\*/, idx)))
					}
				} else { // 普通动画对象
					for (let i = 0; i < allImg.zombies[self.action].len; i++) {
						let idx = i < 10 ? '0' + i : i,
							path = allImg.zombies[self.action].path
						// 依次添加动画序列
						self.images.push(imageFromPath(path.replace(/\*/, idx)))
					}
				}
				break
			case 'loading': // loading动画
				for (let i = 0; i < allImg.loading[self.action].len; i++) {
					let idx = i < 10 ? '0' + i : i,
						path = allImg.loading[self.action].path
					// 依次添加动画序列
					self.images.push(imageFromPath(path.replace(/\*/, idx)))
				}
				break
		}
	}
}
/**
 * 角色类
 * 植物、僵尸类继承的基础属性
 */
class Role {
	constructor(obj) {
		let r = {
			id: Math.random().toFixed(6) * Math.pow(10, 6), // 随机生成 id 值,用于设置当前角色 ID
			type: obj.type, // 角色类型(植物或僵尸)
			section: obj.section, // 角色类别(豌豆射手、双发射手...)
			x: obj.x, // x轴坐标
			y: obj.y, // y轴坐标
			row: obj.row, // 角色初始化行坐标
			col: obj.col, // 角色初始化列坐标
			w: 0, // 角色图片宽度
			h: 0, // 角色图片高度
			isAnimeLenMax: false, // 是否处于动画最后一帧,用于判断动画是否执行完一轮
			isDel: false, // 判断是否死亡并移除当前角色
			isHurt: false, // 判断是否受伤
		}
		Object.assign(this, r)
	}
}
// 植物类
class Plant extends Role {
	constructor(obj) {
		super(obj)
		// 植物类私有属性
		let p = {
			life: 3, // 角色血量
			idle: null, // 站立动画对象
			idleH: null, // 坚果高血量动画对象
			idleM: null, // 坚果中等血量动画对象
			idleL: null, // 坚果低血量动画对象
			attack: null, // 角色攻击动画对象
			digest: null, // 角色消化动画对象
			bullets: [], // 子弹数组对象
			state: obj.section === 'wallnut' ? 2 : 1, // 保存当前状态值
			state_IDLE: 1, // 站立不动状态
			state_IDLE_H: 2, // 站立不动高血量状态(坚果墙相关动画)
			state_IDLE_M: 3, // 站立不动中等血量状态(坚果墙相关动画)
			state_IDLE_L: 4, // 站立不动低血量状态(坚果墙相关动画)
			state_ATTACK: 5, // 攻击状态
			state_DIGEST: 6, // 待攻击状态(食人花消化僵尸状态)
			canShoot: false, // 植物是否具有发射子弹功能
			canSetTimer: obj.canSetTimer, // 能否设置生成阳光定时器
			sunTimer: null, // 生成阳光定时器
			sunTimer_spacing: 10, // 生成阳光时间间隔(秒)
		}
		Object.assign(this, p)
	}
	// 创建,并初始化当前对象
	static new(obj) {
		let p = new this(obj)
		p.init()
		return p
	}
	// 设置阳光生成定时器
	setSunTimer() {
		let self = this
		self.sunTimer = setInterval(function() {
			// 创建阳光元素
			let img = document.createElement('img'), // 创建元素
				container = document.getElementsByTagName('body')[0], // 父级元素容器
				id = self.id, // 当前角色 ID
				top = self.y + 30,
				left = self.x - 130,
				keyframes1 = [ // 阳光移动动画 keyframes
					{
						transform: 'translate(0,0)',
						opacity: 0
					},
					{
						offset: .3,
						transform: 'translate(0,0)',
						opacity: 1
					},
					{
						offset: .5,
						transform: 'translate(0,0)',
						opacity: 1
					},
					{
						offset: 1,
						transform: 'translate(-' + (left - 110) + 'px,-' + (top + 50) + 'px)',
						opacity: 0
					}
				]
			// 添加阳关元素
			img.src = 'images/sun.gif'
			img.className += 'sun-img plantSun' + id
			img.style.top = top + 'px'
			img.style.left = left + 'px'
			container.appendChild(img)
			// 添加阳光移动动画
			let sun = document.getElementsByClassName('plantSun' + id)[0]
			sun.animate(keyframes1, keyframesOptions)
			// 动画完成,清除阳光元素
			setTimeout(() => {
				sun.parentNode.removeChild(sun)
				// 增加阳光数量
				window._main.sunnum.changeSunNum()
			}, 2700)
		}, self.sunTimer_spacing * 1000)
	}
	// 清除阳光生成定时器
	clearSunTimer() {
		let self = this
		clearInterval(self.sunTimer)
	}
	// 初始化
	init() {
		let self = this,
			setPlantFn = null
		// 初始化植物动画对象方法集
		setPlantFn = {
			sunflower() { // 向日葵
				self.idle = Animation.new(self, 'idle', 12)
				// 定时生成阳光
				self.canSetTimer && self.setSunTimer()
			},
			peashooter() { // 豌豆射手
				self.canShoot = true
				self.idle = Animation.new(self, 'idle', 12)
				self.attack = Animation.new(self, 'attack', 12)
			},
			repeater() { // 双发射手
				self.canShoot = true
				self.idle = Animation.new(self, 'idle', 12)
				self.attack = Animation.new(self, 'attack', 8)
			},
			gatlingpea() { // 加特林射手
				// 改变加特林渲染 y 轴距离
				self.y -= 12
				self.canShoot = true
				self.idle = Animation.new(self, 'idle', 8)
				self.attack = Animation.new(self, 'attack', 4)
			},
			cherrybomb() { // 樱桃炸弹
				self.x -= 15
				self.idle = Animation.new(self, 'idle', 15)
				self.attack = Animation.new(self, 'attack', 15)
				setTimeout(() => {
					self.state = self.state_ATTACK
				}, 2000)
			},
			wallnut() { // 坚果墙
				self.x += 15
				// 设置坚果血量
				self.life = 12
				// 创建坚果三种不同血量下的动画对象
				self.idleH = Animation.new(self, 'idleH', 10)
				self.idleM = Animation.new(self, 'idleM', 8)
				self.idleL = Animation.new(self, 'idleL', 10)
			},
			chomper() { // 食人花
				self.life = 5
				self.y -= 45
				self.idle = Animation.new(self, 'idle', 10)
				self.attack = Animation.new(self, 'attack', 12)
				self.digest = Animation.new(self, 'digest', 12)
			},
		}
		// 执行对应植物初始化方法
		for (let key in setPlantFn) {
			if (self.section === key) {
				setPlantFn[key]()
			}
		}
	}
	// 绘制方法
	draw(cxt) {
		let self = this,
			stateName = self.switchState()
		switch (self.isHurt) {
			case false:
				if (self.section === 'cherrybomb' && self.state === self.state_ATTACK) {
					// 正常状态,绘制樱桃炸弹爆炸图片
					cxt.drawImage(self[stateName].img, self.x - 60, self.y - 50)
				} else {
					// 正常状态,绘制普通植物图片
					cxt.drawImage(self[stateName].img, self.x, self.y)
				}
				break
			case true:
				// 受伤或移动植物时,绘制半透明图片
				cxt.globalAlpha = 0.5
				cxt.beginPath()
				cxt.drawImage(self[stateName].img, self.x, self.y)
				cxt.closePath()
				cxt.save()
				cxt.globalAlpha = 1
				break
		}
	}
	// 更新状态
	update(game) {
		let self = this,
			section = self.section,
			stateName = self.switchState()
		// 修改当前动画序列长度
		let animateLen = allImg.plants[section][stateName].len
		// 累加动画计数器
		self[stateName].count += 1
		// 设置角色动画运行速度
		self[stateName].imgIdx = Math.floor(self[stateName].count / self[stateName].fps)
		// 一整套动画完成后重置动画计数器
		self[stateName].imgIdx === animateLen - 1 ? self[stateName].count = 0 : self[stateName].count = self[stateName].count
		// 绘制发射子弹动画
		if (game.state === game.state_RUNNING) {
			// 设置当前帧动画对象
			self[stateName].img = self[stateName].images[self[stateName].imgIdx]
			if (self[stateName].imgIdx === animateLen - 1) {
				if (stateName === 'attack' && !self.isDel) {
					// 未死亡,且为可发射子弹植物时
					if (self.canShoot) {
						// 发射子弹
						self.shoot()
						// 双发射手额外发射子弹
						self.section === 'repeater' && setTimeout(() => {
							self.shoot()
						}, 250)
					}
					// 当为樱桃炸弹时,执行完一轮动画,自动消失
					self.section === 'cherrybomb' ? self.isDel = true : self.isDel = false
					// 当为食人花时,执行完攻击动画,切换为消化动画
					if (self.section === 'chomper') {
						// 立即切换动画会出现图片未加载完成报错
						setTimeout(() => {
							self.changeAnimation('digest')
						}, 0)
					}
				} else if (self.section === 'chomper' && stateName === 'digest') {
					// 消化动画完毕后,间隔一段时间切换为正常状态
					setTimeout(() => {
						self.changeAnimation('idle')
					}, 30000)
				}
				self.isAnimeLenMax = true
			} else {
				self.isAnimeLenMax = false
			}
		}
	}
	// 检测植物是否可攻击僵尸方法
	canAttack() {
		let self = this
		// 植物类别为向日葵和坚果墙时,不需判定
		if (self.section === 'sunflower' || self.section === 'wallnut') return false
		// 循环僵尸对象数组
		for (let zombie of window._main.zombies) {
			if (self.section === 'cherrybomb') { // 当为樱桃炸弹时
				// 僵尸在以樱桃炸弹为圆心的 9 个格子内时
				if (Math.abs(self.row - zombie.row) <= 1 && Math.abs(self.col - zombie.col) <= 1 && zombie.col < 10) {
					// 执行爆炸动画
					self.changeAnimation('attack')
					zombie.life = 0
					// 僵尸炸死动画
					zombie.changeAnimation('dieboom')
				}
			} else if (self.section === 'chomper' && self.state === self.state_IDLE) { // 当为食人花时
				// 僵尸在食人花正前方时
				if (self.row === zombie.row && (zombie.col - self.col) <= 1 && zombie.col < 10) {
					self.changeAnimation('attack')
					setTimeout(() => {
						zombie.isDel = true
					}, 1300)
				}
			} else if (self.canShoot && self.row === zombie.row) { // 当植物可发射子弹,且僵尸和植物处于同行时
				// 僵尸进入植物射程范围
				zombie.x < 940 && self.x < zombie.x + 10 && zombie.life > 0 ? self.changeAnimation('attack') : self.changeAnimation(
					'idle')
				// 植物未被移除时,可发射子弹
				if (!self.isDel) {
					self.bullets.forEach(function(bullet, j) {
						// 当子弹打中僵尸,且僵尸未死亡时
						if (Math.abs(zombie.x + bullet.w - bullet.x) < 10 && zombie.life > 0) { // 子弹和僵尸距离小于 10 且僵尸未死亡
							// 移除子弹
							self.bullets.splice(j, 1)
							// 根据血量判断执行不同阶段动画
							if (zombie.life !== 0) {
								zombie.life--
								zombie.isHurt = true
								setTimeout(() => {
									zombie.isHurt = false
								}, 200)
							}
							if (zombie.life === 2) {
								zombie.changeAnimation('dying')
							} else if (zombie.life === 0) {
								zombie.changeAnimation('die')
							}
						}
					})
				}
			}
		}
	}
	// 射击方法
	shoot() {
		let self = this
		self.bullets[self.bullets.length] = Bullet.new(self)
	}
	/**
	 * 判断角色状态并返回对应动画对象名称方法
	 */
	switchState() {
		let self = this,
			state = self.state,
			dictionary = {
				idle: self.state_IDLE,
				idleH: self.state_IDLE_H,
				idleM: self.state_IDLE_M,
				idleL: self.state_IDLE_L,
				attack: self.state_ATTACK,
				digest: self.state_DIGEST,
			}
		for (let key in dictionary) {
			if (state === dictionary[key]) {
				return key
			}
		}
	}
	/**
	 * 切换角色动画
	 * game => 游戏引擎对象
	 * action => 动作类型
	 *  -idle: 站立动画
	 *  -idleH: 角色高血量动画(坚果墙)
	 *  -idleM: 角色中等血量动画(坚果墙)
	 *  -idleL: 角色低血量动画(坚果墙)
	 *  -attack: 攻击动画
	 *  -digest: 消化动画(食人花)
	 */
	changeAnimation(action) {
		let self = this,
			stateName = self.switchState(),
			dictionary = {
				idle: self.state_IDLE,
				idleH: self.state_IDLE_H,
				idleM: self.state_IDLE_M,
				idleL: self.state_IDLE_L,
				attack: self.state_ATTACK,
				digest: self.state_DIGEST,
			}
		if (action === stateName) return
		self.state = dictionary[action]
	}
}
// 僵尸类
class Zombie extends Role {
	constructor(obj) {
		super(obj)
		// 僵尸类私有属性
		let z = {
			life: 10, // 角色血量
			canMove: true, // 判断当前角色是否可移动
			attackPlantID: 0, // 当前攻击植物对象 ID
			idle: null, // 站立动画对象
			run: null, // 奔跑动画对象
			attack: null, // 攻击动画对象
			dieboom: null, // 被炸死亡动画对象
			dying: null, // 濒临死亡动画对象
			die: null, // 死亡动画对象
			state: 1, // 保存当前状态值,默认为1
			state_IDLE: 1, // 站立不动状态
			state_RUN: 2, // 奔跑状态
			state_ATTACK: 3, // 攻击状态
			state_DIEBOOM: 4, // 死亡状态
			state_DYING: 5, // 濒临死亡状态
			state_DIE: 6, // 死亡状态
			state_DIGEST: 7, // 消化死亡状态
			speed: 3, // 移动速度
			head_x: 0, // 头部动画 x 轴坐标
			head_y: 0, // 头部动画 y 轴坐标
		}
		Object.assign(this, z)
	}
	// 创建,并初始化当前对象
	static new(obj) {
		let p = new this(obj)
		p.init()
		return p
	}
	// 初始化
	init() {
		let self = this
		// 站立
		self.idle = Animation.new(self, 'idle', 12)
		// 移动
		self.run = Animation.new(self, 'run', 12)
		// 攻击
		self.attack = Animation.new(self, 'attack', 8)
		// 炸死
		self.dieboom = Animation.new(self, 'dieboom', 8)
		// 濒死
		self.dying = Animation.new(self, 'dying', 8)
		// 死亡
		self.die = Animation.new(self, 'die', 12)
	}
	// 绘制方法
	draw(cxt) {
		let self = this,
			stateName = self.switchState()
		if (stateName !== 'dying' && stateName !== 'die') { // 绘制普通动画
			if (!self.isHurt) { // 未受伤时,绘制正常动画
				cxt.drawImage(self[stateName].img, self.x, self.y)
			} else { // 受伤时,绘制带透明度动画
				// 绘制带透明度动画
				cxt.globalAlpha = 0.5
				cxt.beginPath()
				cxt.drawImage(self[stateName].img, self.x, self.y)
				cxt.closePath()
				cxt.save()
				cxt.globalAlpha = 1
			}
		} else { // 绘制濒死、死亡动画
			if (!self.isHurt) { // 未受伤时,绘制正常动画
				cxt.drawImage(self[stateName].imgHead, self.head_x + 70, self.head_y - 10)
				cxt.drawImage(self[stateName].imgBody, self.x, self.y)
			} else { // 受伤时,绘制带透明度动画
				// 绘制带透明度身体
				cxt.globalAlpha = 0.5
				cxt.beginPath()
				cxt.drawImage(self[stateName].imgBody, self.x, self.y)
				cxt.closePath()
				cxt.save()
				cxt.globalAlpha = 1
				// 头部不带透明度
				cxt.drawImage(self[stateName].imgHead, self.head_x + 70, self.head_y - 10)
			}
		}
	}
	// 更新状态
	update(game) {
		let self = this,
			stateName = self.switchState()
		// 更新能否移动状态值
		self.canMove ? self.speed = 3 : self.speed = 0
		// 更新僵尸列坐标值
		self.col = Math.floor((self.x - window._main.zombies_info.x) / 80 + 1)
		if (stateName !== 'dying' && stateName !== 'die') { // 普通动画(站立,移动,攻击)
			// 修改当前动画序列长度
			let animateLen = allImg.zombies[stateName].len
			// 累加动画计数器
			self[stateName].count += 1
			// 设置角色动画运行速度
			self[stateName].imgIdx = Math.floor(self[stateName].count / self[stateName].fps)
			// 一整套动画完成后重置动画计数器
			if (self[stateName].imgIdx === animateLen) {
				self[stateName].count = 0
				self[stateName].imgIdx = 0
				if (stateName === 'dieboom') { // 被炸死亡状态
					// 当死亡动画执行完一轮后,移除当前角色
					self.isDel = true
				}
				// 当前动画帧数达到最大值
				self.isAnimeLenMax = true
			} else {
				self.isAnimeLenMax = false
			}
			// 游戏运行状态
			if (game.state === game.state_RUNNING) {
				// 设置当前帧动画对象
				self[stateName].img = self[stateName].images[self[stateName].imgIdx]
				if (stateName === 'run') { // 当僵尸移动时,控制移动速度
					self.x -= self.speed / 17
				}
			}
		} else if (stateName === 'dying') { // 濒死动画,包含两个动画对象
			// 获取当前动画序列长度
			let headAnimateLen = allImg.zombies[stateName].head.len,
				bodyAnimateLen = allImg.zombies[stateName].body.len
			// 累加动画计数器
			if (self[stateName].imgIdxHead !== headAnimateLen - 1) {
				self[stateName].countHead += 1
			}
			self[stateName].countBody += 1
			// 设置角色动画运行速度
			self[stateName].imgIdxHead = Math.floor(self[stateName].countHead / self[stateName].fps)
			self[stateName].imgIdxBody = Math.floor(self[stateName].countBody / self[stateName].fps)
			// 设置当前帧动画对象,头部动画
			if (self[stateName].imgIdxHead === 0) {
				self.head_x = self.x
				self.head_y = self.y
				self[stateName].imgHead = self[stateName].images.head[self[stateName].imgIdxHead]
			} else if (self[stateName].imgIdxHead === headAnimateLen) {
				self[stateName].imgHead = self[stateName].images.head[headAnimateLen - 1]
			} else {
				self[stateName].imgHead = self[stateName].images.head[self[stateName].imgIdxHead]
			}
			// 设置当前帧动画对象,身体动画
			if (self[stateName].imgIdxBody === bodyAnimateLen) {
				self[stateName].countBody = 0
				self[stateName].imgIdxBody = 0
				// 当前动画帧数达到最大值
				self.isAnimeLenMax = true
			} else {
				self.isAnimeLenMax = false
			}
			// 游戏运行状态
			if (game.state === game.state_RUNNING) {
				// 设置当前帧动画对象
				self[stateName].imgBody = self[stateName].images.body[self[stateName].imgIdxBody]
				if (stateName === 'dying') { // 濒死状态,可以移动
					self.x -= self.speed / 17
				}
			}
		} else if (stateName === 'die') { // 死亡动画,包含两个动画对象
			// 获取当前动画序列长度
			let headAnimateLen = allImg.zombies[stateName].head.len,
				bodyAnimateLen = allImg.zombies[stateName].body.len
			// 累加动画计数器
			if (self[stateName].imgIdxBody !== bodyAnimateLen - 1) {
				self[stateName].countBody += 1
			}
			// 设置角色动画运行速度
			self[stateName].imgIdxBody = Math.floor(self[stateName].countBody / self[stateName].fps)
			// 设置当前帧动画对象,死亡状态,定格头部动画
			if (self[stateName].imgIdxHead === 0) {
				if (self.head_x == 0 && self.head_y == 0) {
					self.head_x = self.x
					self.head_y = self.y
				}
				self[stateName].imgHead = self[stateName].images.head[headAnimateLen - 1]
			}
			// 设置当前帧动画对象,身体动画
			if (self[stateName].imgIdxBody === 0) {
				self[stateName].imgBody = self[stateName].images.body[self[stateName].imgIdxBody]
			} else if (self[stateName].imgIdxBody === bodyAnimateLen - 1) {
				// 当死亡动画执行完一轮后,移除当前角色
				self.isDel = true
				self[stateName].imgBody = self[stateName].images.body[bodyAnimateLen - 1]
			} else {
				self[stateName].imgBody = self[stateName].images.body[self[stateName].imgIdxBody]
			}
		}
	}
	// 检测僵尸是否可攻击植物
	canAttack() {
		let self = this
		// 循环植物对象数组
		for (let plant of window._main.plants) {
			if (plant.row === self.row && !plant.isDel) { // 当僵尸和植物处于同行时
				if (self.x - plant.x < -20 && self.x - plant.x > -60) {
					if (self.life > 2) {
						// 保存当前攻击植物 hash 值,在该植物被删除时,再控制当前僵尸移动
						self.attackPlantID !== plant.id ? self.attackPlantID = plant.id : self.attackPlantID = self.attackPlantID
						self.changeAnimation('attack')
					} else {
						self.canMove = false
					}
					if (self.isAnimeLenMax && self.life > 2) { // 僵尸动画每执行完一轮次
						// 扣除植物血量
						if (plant.life !== 0) {
							plant.life--
							plant.isHurt = true
							setTimeout(() => {
								plant.isHurt = false
								// 坚果墙判断切换动画状态
								if (plant.life <= 8 && plant.section === 'wallnut') {
									plant.life <= 4 ? plant.changeAnimation('idleL') : plant.changeAnimation('idleM')
								}
								// 判断植物是否可移除
								if (plant.life <= 0) {
									// 设置植物死亡状态
									plant.isDel = true
									// 清除死亡向日葵的阳光生成定时器
									plant.section === 'sunflower' && plant.clearSunTimer()
								}
							}, 200)
						}
					}
				}
			}
		}
	}
	/**
	 * 判断角色状态并返回对应动画对象名称方法
	 */
	switchState() {
		let self = this,
			state = self.state,
			dictionary = {
				idle: self.state_IDLE,
				run: self.state_RUN,
				attack: self.state_ATTACK,
				dieboom: self.state_DIEBOOM,
				dying: self.state_DYING,
				die: self.state_DIE,
				digest: self.state_DIGEST,
			}
		for (let key in dictionary) {
			if (state === dictionary[key]) {
				return key
			}
		}
	}
	/**
	 * 切换角色动画
	 * game => 游戏引擎对象
	 * action => 动作类型
	 *  -idle: 站立不动
	 *  -attack: 攻击
	 *  -die: 死亡
	 *  -dying: 濒死
	 *  -dieboom: 爆炸
	 *  -digest: 被消化
	 */
	changeAnimation(action) {
		let self = this,
			stateName = self.switchState(),
			dictionary = {
				idle: self.state_IDLE,
				run: self.state_RUN,
				attack: self.state_ATTACK,
				dieboom: self.state_DIEBOOM,
				dying: self.state_DYING,
				die: self.state_DIE,
				digest: self.state_DIGEST,
			}
		if (action === stateName) return
		self.state = dictionary[action]
	}
}

3.game.js

/**
 * 游戏引擎函数
 */
class Game {
	constructor() {
		let g = {
			actions: {}, // 注册按键操作
			keydowns: {}, // 按键事件对象
			cardSunVal: null, // 当前选中植物卡片index以及需消耗阳光值
			cardSection: '', // 绘制随鼠标移动植物类别
			canDrawMousePlant: false, // 能否绘制随鼠标移动植物
			canLayUp: false, // 能否放置植物
			mousePlant: null, // 鼠标绘制植物对象
			mouseX: 0, // 鼠标 x 轴坐标
			mouseY: 0, // 鼠标 y 轴坐标
			mouseRow: 0, // 鼠标移动至可种植植物区域的行坐标
			mouseCol: 0, // 鼠标移动至可种植植物区域的列坐标
			state: 0, // 游戏状态值,初始默认为 0
			state_LOADING: 0, // 准备阶段
			state_START: 1, // 开始游戏
			state_RUNNING: 2, // 游戏开始运行
			state_STOP: 3, // 暂停游戏
			state_PLANTWON: 4, // 游戏结束,玩家胜利
			state_ZOMBIEWON: 5, // 游戏结束,僵尸胜利
			canvas: document.getElementById("canvas"), // canvas元素
			context: document.getElementById("canvas").getContext("2d"), // canvas画布
			timer: null, // 轮询定时器
			fps: window._main.fps, // 动画帧数
		}
		Object.assign(this, g)
	}
	// 创建,并初始化当前对象
	static new() {
		let g = new this()
		g.init()
		return g
	}
	// 清除当前游戏定时器
	clearGameTimer() {
		let g = this
		clearInterval(g.timer)
	}
	// 绘制场景
	drawBg() {
		let g = this,
			cxt = g.context,
			sunnum = window._main.sunnum, // 阳光数量对象
			cards = window._main.cards, // 植物卡片对象
			img = imageFromPath(allImg.bg) // 背景图片对象
		// 绘制背景
		cxt.drawImage(img, 0, 0)
		// 绘制阳光数量框
		sunnum.draw(cxt)
	}
	// 绘制小汽车
	drawCars() {
		let g = this,
			cxt = g.context,
			cars = window._main.cars // 小汽车对象
		// 绘制植物卡片
		cars.forEach(function(car, idx) {
			if (car.x > 950) { // 移除使用过的小汽车
				cars.splice(idx, 1)
			}
			car.draw(g, cxt)
		})
	}
	// 绘制植物卡片
	drawCards() {
		let g = this,
			cxt = g.context,
			cards = window._main.cards // 植物卡片对象
		// 绘制植物卡片
		for (let card of cards) {
			card.draw(cxt)
		}
	}
	// 绘制玩家胜利动画
	drawPlantWon() {
		let g = this,
			cxt = g.context,
			text = '恭喜玩家获得胜利!' // 胜利文案
		// 绘制胜利动画
		cxt.fillStyle = 'red'
		cxt.font = '48px Microsoft YaHei'
		cxt.fillText(text, 354, 300)
	}
	// 绘制僵尸胜利动画
	drawZombieWon() {
		let g = this,
			cxt = g.context,
			img = imageFromPath(allImg.zombieWon) // 胜利图片对象
		// 绘制胜利动画
		cxt.drawImage(img, 293, 66)
	}
	// 绘制loading首屏画面
	drawLoading() {
		let g = this,
			cxt = g.context,
			img = imageFromPath(allImg.startBg)
		// 绘制loading图片
		cxt.drawImage(img, 119, 0)
	}
	// 绘制Start动画
	drawStartAnime() {
		let g = this,
			stateName = 'write',
			loading = window._main.loading,
			cxt = g.context,
			canvas_w = g.canvas.width,
			canvas_h = g.canvas.height,
			animateLen = allImg.loading[stateName].len // 修改当前动画序列长度
		// 累加动画计数器
		if (loading.imgIdx !== animateLen) {
			loading.count += 1
		}
		// 设置角色动画运行速度
		loading.imgIdx = Math.floor(loading.count / loading.fps)
		// 一整套动画完成后重置动画计数器,并设置当前帧动画对象
		if (loading.imgIdx === animateLen) {
			loading.img = loading.images[loading.imgIdx - 1]
		} else {
			loading.img = loading.images[loading.imgIdx]
		}
		// 绘制Start动画
		cxt.drawImage(loading.img, 437, 246)
	}
	// 绘制所有子弹的函数
	drawBullets(plants) {
		let g = this,
			context = g.context,
			canvas_w = g.canvas.width - 440
		for (let item of plants) {
			item.bullets.forEach(function(bullet, idx, arr) {
				// 绘制子弹
				bullet.draw(g, context)
				// 移除超出射程的子弹
				if (bullet.x >= canvas_w) {
					arr.splice(idx, 1)
				}
			})
		}
	}
	// 绘制角色血量
	drawBlood(role) {
		let g = this,
			cxt = g.context,
			x = role.x,
			y = role.y
		cxt.fillStyle = 'red'
		cxt.font = '18px Microsoft YaHei'
		if (role.type === 'plant') {
			cxt.fillText(role.life, x + 30, y - 10)
		} else if (role.type === 'zombie') {
			cxt.fillText(role.life, x + 85, y + 10)
		}
	}
	// 更新角色状态
	updateImage(plants, zombies) {
		let g = this,
			cxt = g.context
		plants.forEach(function(plant, idx) {
			// 判断是否进入攻击状态
			plant.canAttack()
			// 更新状态
			plant.update(g)
		})
		zombies.forEach(function(zombie, idx) {
			if (zombie.x < 50) { // 僵尸到达房屋,获得胜利
				g.state = g.state_ZOMBIEWON
			}
			// 判断是否进入攻击状态
			zombie.canAttack()
			// 更新状态
			zombie.update(g)
		})
	}
	// 绘制角色
	drawImage(plants, zombies) {
		let g = this,
			cxt = g.context,
			delPlantsArr = [] // 被删除植物元素集合
		plants.forEach(function(plant, idx, arr) {
			if (plant.isDel) { // 移除死亡对象
				delPlantsArr.push(plant)
				arr.splice(idx, 1)
			} else { // 绘制未死亡角色
				plant.draw(cxt)
				// g.drawBlood(plant)
			}
		})
		zombies.forEach(function(zombie, idx) {
			if (zombie.isDel) { // 移除死亡对象
				zombies.splice(idx, 1)
				// 当僵尸被消灭完,玩家获得胜利
				if (zombies.length === 0) {
					g.state = g.state_PLANTWON
				}
			} else { // 绘制未死亡角色
				zombie.draw(cxt)
				// g.drawBlood(zombie)
			}
			// 使僵尸在植物死亡后可正确移动
			for (let plant of delPlantsArr) {
				if (zombie.attackPlantID === plant.id) {
					zombie.canMove = true
					if (zombie.life > 2) {
						zombie.changeAnimation('run')
					}
				}
			}
		})
	}
	// 检测当前鼠标移动坐标,并处理相关事件
	getMousePos() {
		let g = this,
			_main = window._main,
			cxt = g.context,
			cards = _main.cards,
			x = g.mouseX,
			y = g.mouseY
		// 鼠标移动绘制植物
		if (g.canDrawMousePlant) {
			g.mousePlantCallback(x, y)
		}
	}
	// 鼠标移动绘制植物
	mousePlantCallback(x, y) {
		let g = this,
			_main = window._main,
			cxt = g.context,
			row = Math.floor((y - 75) / 100) + 1, // 定义行坐标
			col = Math.floor((x - 175) / 80) + 1 // 定义列坐标
		// 绘制植物信息
		let plant_info = {
			type: 'plant',
			section: g.cardSection,
			x: _main.plants_info.x + 80 * (col - 1),
			y: _main.plants_info.y + 100 * (row - 1),
			row: row,
			col: col,
		}
		g.mouseRow = row
		g.mouseCol = col
		// 判断是否在可种植区域
		if (row >= 1 && row <= 5 && col >= 1 && col <= 9) {
			g.canLayUp = true
			// 判断当前位置是否可放置植物
			for (let plant of _main.plants) {
				if (row === plant.row && col === plant.col) {
					g.canLayUp = false
				}
			}
		} else {
			g.canLayUp = false
		}
		// 绘制随鼠标移动植物函数
		if (g.canDrawMousePlant) {
			g.drawMousePlant(plant_info)
		}
	}
	// 绘制随鼠标移动植物
	drawMousePlant(plant_info) {
		let g = this,
			cxt = g.context,
			plant = null,
			mousePlant_info = { // 随鼠标移动植物信息
				type: 'plant',
				section: g.cardSection,
				x: g.mouseX + 82,
				y: g.mouseY - 40,
				row: g.mouseRow,
				col: g.mouseCol,
			}
		// 判断是否允许放置
		if (g.canLayUp) {
			// 绘制半透明植物
			plant = Plant.new(plant_info)
			plant.isHurt = true
			plant.update(g)
			plant.draw(cxt)
		}
		// 绘制随鼠标移动植物
		g.mousePlant = Plant.new(mousePlant_info)
		g.mousePlant.update(g)
		g.mousePlant.draw(cxt)
	}
	// 注册事件
	registerAction(key, callback) {
		this.actions[key] = callback
	}
	// 设置逐帧动画
	setTimer(_main) {
		let g = this,
			plants = _main.plants, // 植物对象数组
			zombies = _main.zombies // 僵尸对象数组
		// 事件集合
		let actions = Object.keys(g.actions)
		for (let i = 0; i < actions.length; i++) {
			let key = actions[i]
			if (g.keydowns[key]) {
				// 如果按键被按下,调用注册的action
				g.actions[key]()
			}
		}
		// 清除画布
		g.context.clearRect(0, 0, g.canvas.width, g.canvas.height)
		if (g.state === g.state_LOADING) {
			// 绘制场景
			g.drawLoading()
		} else if (g.state === g.state_START) {
			// 绘制场景
			g.drawBg()
			// 绘制小汽车
			g.drawCars()
			// 绘制植物卡片
			g.drawCards()
			// 绘制 Start 动画
			g.drawStartAnime()
		} else if (g.state === g.state_RUNNING) {
			// 绘制场景
			g.drawBg()
			// 更新所有植物,僵尸状态
			g.updateImage(plants, zombies)
			// 绘制所有植物,僵尸
			g.drawImage(plants, zombies)
			// 绘制小汽车
			g.drawCars()
			// 绘制植物卡片
			g.drawCards()
			// 绘制所有子弹
			g.drawBullets(plants)
			// 绘制随鼠标移动植物
			g.getMousePos()
		} else if (g.state === g.state_STOP) {
			// 绘制场景
			g.drawBg()
			// 更新所有植物,僵尸状态
			g.updateImage(plants, zombies)
			// 绘制所有植物,僵尸
			g.drawImage(plants, zombies)
			// 绘制小汽车
			g.drawCars()
			// 绘制植物卡片
			g.drawCards()
			// 绘制所有子弹
			g.drawBullets(plants)
			// 清除全局生成阳光定时器
			_main.clearTiemr()
		} else if (g.state === g.state_PLANTWON) { // 玩家胜利
			// 绘制场景
			g.drawBg()
			// 绘制小汽车
			g.drawCars()
			// 绘制植物卡片
			g.drawCards()
			// 绘制玩家胜利画面
			g.drawPlantWon()
			// 清除全局生成阳光定时器
			_main.clearTiemr()
		} else if (g.state === g.state_ZOMBIEWON) { // 僵尸胜利
			// 绘制场景
			g.drawBg()
			// 绘制小汽车
			g.drawCars()
			// 绘制植物卡片
			g.drawCards()
			// 绘制僵尸胜利画面
			g.drawZombieWon()
			// 清除全局生成阳光定时器
			_main.clearTiemr()
		}
	}
	/**
	 * 初始化函数
	 * _main: 游戏入口函数对象
	 */
	init() {
		let g = this,
			_main = window._main

		// 设置键盘按下及松开相关注册函数
		window.addEventListener('keydown', function(event) {
			g.keydowns[event.keyCode] = 'down'
		})
		window.addEventListener('keyup', function(event) {
			g.keydowns[event.keyCode] = 'up'
		})
		g.registerAction = function(key, callback) {
			g.actions[key] = callback
		}
		// 设置轮询定时器
		g.timer = setInterval(function() {
			g.setTimer(_main)
		}, 1000 / g.fps)
		// 注册鼠标移动事件
		document.getElementById('canvas').onmousemove = function(event) {
			let e = event || window.event,
				scrollX = document.documentElement.scrollLeft || document.body.scrollLeft,
				scrollY = document.documentElement.scrollTop || document.body.scrollTop,
				x = e.pageX || e.clientX + scrollX,
				y = e.pageY || e.clientY + scrollY
			// 设置当前鼠标坐标位置
			g.mouseX = x
			g.mouseY = y
		}
		// 查看更新日志按钮点击事件
		document.querySelectorAll('.change-log-btn').forEach(function(el, idx) {
			el.onclick = function() {
				let updateLog = document.getElementsByClassName('update-log')[0]
				updateLog.style.display === 'none' ? updateLog.style.display = 'block' : updateLog.style.display = 'none'
			}
		})
		// 开始游戏按钮点击事件
		document.getElementById('js-startGame-btn').onclick = function() {
			// 播放Start动画
			g.state = g.state_START
			// 设置定时器,切换至开始游戏状态
			setTimeout(function() {
				g.state = g.state_RUNNING
				// 显示控制按钮
				document.getElementById('pauseGame').className += ' show'
				document.getElementById('restartGame').className += ' show'
				// 设置全局生成阳光、僵尸定时器
				_main.clearTiemr()
				_main.setTimer()
			}, 2500)
			// 显示卡片列表信息
			document.getElementsByClassName('cards-list')[0].className += ' show'
			// 显示控制按钮菜单
			document.getElementsByClassName('menu-box')[0].className += ' show'
			// 隐藏开始游戏按钮,游戏介绍,查看更新日志按钮
			document.getElementById('js-startGame-btn').style.display = 'none'
			document.getElementById('js-intro-game').style.display = 'none'
			document.getElementById('js-log-btn').style.display = 'none'
		}
		// 植物卡片点击事件
		document.querySelectorAll('.cards-item').forEach(function(card, idx) {
			card.onclick = function() {
				let plant = null, // 鼠标放置植物对象
					cards = _main.cards
				// 当卡片可点击时
				if (cards[idx].canClick) {
					// 设置当前随鼠标移动植物类别
					g.cardSection = this.dataset.section
					// 可绘制随鼠标移动植物
					g.canDrawMousePlant = true
					// 设置当前选中植物卡片idx以及需消耗阳光数量
					g.cardSunVal = {
						idx: idx,
						val: cards[idx].sun_val,
					}
				}
			}
		})
		// 鼠标点击画布事件
		document.getElementById('canvas').onclick = function(event) {
			let plant = null, // 鼠标放置植物对象
				cards = _main.cards,
				x = g.mouseX,
				y = g.mouseY,
				plant_info = { // 鼠标放置植物对象初始化信息
					type: 'plant',
					section: g.cardSection,
					x: _main.plants_info.x + 80 * (g.mouseCol - 1),
					y: _main.plants_info.y + 100 * (g.mouseRow - 1),
					row: g.mouseRow,
					col: g.mouseCol,
					canSetTimer: g.cardSection === 'sunflower' ? true : false, // 能否设置阳光生成定时器
				}
			// 判断当前位置是否可放置植物
			for (let item of _main.plants) {
				if (g.mouseRow === item.row && g.mouseCol === item.col) {
					g.canLayUp = false
					g.mousePlant = null
				}
			}
			// 在可放置时,绘制植物
			if (g.canLayUp && g.canDrawMousePlant) {
				let cardSunVal = g.cardSunVal
				if (cardSunVal.val <= _main.allSunVal) { // 在阳光数量足够时绘制
					// 禁用当前卡片
					cards[cardSunVal.idx].canClick = false
					// 定时改变卡片可点击状态
					cards[cardSunVal.idx].changeState()
					// 绘制倒计时
					cards[cardSunVal.idx].drawCountDown()
					// 放置对应植物
					plant = Plant.new(plant_info)
					_main.plants.push(plant)
					// 改变阳光数量
					_main.sunnum.changeSunNum(-cardSunVal.val)
					// 禁止绘制随鼠标移动植物
					g.canDrawMousePlant = false
				} else { // 阳光数量不足
					// 禁止绘制随鼠标移动植物
					g.canDrawMousePlant = false
					// 清空随鼠标移动植物对象
					g.mousePlant = null
				}
			} else {
				// 禁止绘制随鼠标移动植物
				g.canDrawMousePlant = false
				// 清空随鼠标移动植物对象
				g.mousePlant = null
			}
		}
		// 暂停按钮事件
		document.getElementById('pauseGame').onclick = function(event) {
			g.state = g.state_STOP
		}
		// 重启游戏按钮事件
		document.getElementById('restartGame').onclick = function(event) {
			if (g.state === g.state_LOADING) { // 加载动画
				g.state = g.state_START
			} else {
				g.state = g.state_RUNNING
				// 开启向日葵的阳光生成定时器
				for (let plant of _main.plants) {
					if (plant.section === 'sunflower') {
						plant.setSunTimer()
					}
				}
			}
			// 设置全局生成阳光、僵尸定时器
			_main.setTimer()
		}
	}
}

4.main.js

/**
 * 游戏运行主函数
 */
class Main {
	constructor() {
		let m = {
			allSunVal: 200, // 阳光总数量
			loading: null, // loading 动画对象
			sunnum: null, // 阳光实例对象
			cars: [], // 实例化除草车对象数组
			cars_info: { // 初始化参数
				x: 170, // x 轴坐标
				y: 102, // y 轴坐标
				position: [{
						row: 1
					},
					{
						row: 2
					},
					{
						row: 3
					},
					{
						row: 4
					},
					{
						row: 5
					},
				],
			},
			cards: [], // 实例化植物卡片对象数组
			cards_info: { // 初始化参数
				x: 0, // x 轴坐标
				y: 0, // y 轴坐标
				position: [{
						name: 'sunflower',
						row: 1,
						sun_val: 50,
						timer_spacing: 5 * 1000
					},
					{
						name: 'wallnut',
						row: 2,
						sun_val: 50,
						timer_spacing: 12 * 1000
					},
					{
						name: 'peashooter',
						row: 3,
						sun_val: 100,
						timer_spacing: 7 * 1000
					},
					{
						name: 'repeater',
						row: 4,
						sun_val: 150,
						timer_spacing: 10 * 1000
					},
					{
						name: 'gatlingpea',
						row: 5,
						sun_val: 200,
						timer_spacing: 15 * 1000
					},
					{
						name: 'chomper',
						row: 6,
						sun_val: 200,
						timer_spacing: 15 * 1000
					},
					{
						name: 'cherrybomb',
						row: 7,
						sun_val: 250,
						timer_spacing: 25 * 1000
					},
				]
			},
			plants: [], // 实例化植物对象数组
			zombies: [], // 实例化僵尸对象数组
			plants_info: { // 初始化参数
				type: 'plant', // 角色类型
				x: 250, // 初始 x 轴坐标,递增量 80
				y: 92, // 初始 y 轴坐标,递增量 100
				position: [ // section:植物类别,row:横行坐标(最大值为 5),col:竖列坐标(最大值为 9)
					// 设置初始数据:{section: 'sunflower', row: 1, col: 1},
				]
			},
			zombies_info: { // 初始化参数
				type: 'zombie', // 角色类型
				x: 170, // x轴坐标
				y: 15, // y轴坐标
				position: [ // section:僵尸类别,row:横行坐标(最大值为 5),col:竖列坐标(最大值为 13)
					// 设置初始数据:{section: 'zombie', row: 1, col: 1},
				]
			},
			zombies_idx: 0, // 随机生成僵尸 idx
			zombies_row: 0, // 随机生成僵尸的行坐标
			zombies_iMax: 50, // 随机生成僵尸数量上限
			sunTimer: null, // 全局定时器,用于控制全局定时生成阳光
			sunTimer_difference: 20, // 定时生成阳光时间差值(单位:秒)
			zombieTimer: null, // 全局定时器,用于控制全局定时生成僵尸
			zombieTimer_difference: 12, // 定时生成僵尸时间差值(单位:秒)
			game: null, // 游戏引擎对象
			fps: 60,
		}
		Object.assign(this, m)
	}
	// 设置随机生成僵尸信息
	setZombiesInfo() {
		let self = this,
			iMax = self.zombies_iMax
		for (let i = 0; i < iMax; i++) {
			let row = Math.ceil(Math.random() * 4 + 1)
			self.zombies_info.position.push({
				section: 'zombie',
				row: row,
				col: 11 + Number(Math.random().toFixed(1))
			})
		}
	}
	// 清除全局定时器
	clearTiemr() {
		let self = this
		// 清除全局阳光生成定时器
		clearInterval(self.sunTimer)
		// 清除全局僵尸生成定时器
		clearInterval(self.zombieTimer)
		// 清除向日葵的阳光生成定时器
		for (let plant of self.plants) {
			if (plant.section === 'sunflower') {
				plant.clearSunTimer()
			}
		}
	}
	// 设置全局阳光、僵尸生成定时器
	setTimer() {
		let self = this,
			zombies = self.zombies
		// 设置全局阳光定时器
		self.sunTimer = setInterval(function() {
			// 生成全局阳光动画
			let left = parseInt(window.getComputedStyle(document.getElementsByClassName('systemSun')[0], null).left), // 获取当前元素left值
				top = '-100px',
				keyframes1 = [{
						transform: 'translate(0,0)',
						opacity: 0
					},
					{
						offset: .5,
						transform: 'translate(0,300px)',
						opacity: 1
					},
					{
						offset: .75,
						transform: 'translate(0,300px)',
						opacity: 1
					},
					{
						offset: 1,
						transform: 'translate(-' + (left - 110) + 'px,50px)',
						opacity: 0
					}
				]
			document.getElementsByClassName('systemSun')[0].animate(keyframes1, keyframesOptions)
			setTimeout(function() {
				// 增加阳光数量
				self.sunnum.changeSunNum()
				// 重新设置系统阳光定位
				document.getElementsByClassName('systemSun')[0].style.left = Math.floor(Math.random() * 200 + 300) + 'px'
				document.getElementsByClassName('systemSun')[0].style.top = '-100px'
			}, 2700)
		}, 1000 * self.sunTimer_difference)
		// 设置生成僵尸定时器
		self.zombieTimer = setInterval(function() {
			let idx = self.zombies_iMax - self.zombies_idx - 1
			if (self.zombies_idx === self.zombies_iMax) { // 僵尸生成数量达到最大值,清除定时器
				return clearInterval(self.zombieTimer)
			}
			// 僵尸开始移动
			if (self.zombies[idx]) {
				self.zombies[idx].state = self.zombies[idx].state_RUN
			}
			self.zombies_idx++
		}, 1000 * self.zombieTimer_difference)
	}
	// 创建除草车对象初始化数组
	setCars(cars_info) {
		let self = this
		for (let car of cars_info.position) {
			let info = {
				x: cars_info.x,
				y: cars_info.y + 100 * (car.row - 1),
				row: car.row,
			}
			self.cars.push(Car.new(info))
		}
	}
	// 创建卡片对象初始化数组
	setCards(cards_info) {
		let self = this
		for (let card of cards_info.position) {
			/**
			 * 卡片初始化信息
			 * name: 卡片名称
			 * row: 卡片行坐标
			 * sun_val: 阳光消耗数量
			 * timer_spacing: 卡片冷却时间
			 * y: y 轴坐标
			 */
			let info = {
				name: card.name,
				row: card.row,
				sun_val: card.sun_val,
				timer_spacing: card.timer_spacing,
				y: cards_info.y + 60 * (card.row - 1),
			}
			self.cards.push(Card.new(info))
		}
	}
	// 创建初始信息包含角色
	setRoles(roles_info) {
		let self = this,
			type = roles_info.type
		// 根据坐标创建对应角色坐标数组
		for (let role of roles_info.position) {
			/**
			 * 角色初始化信息
			 * type: 角色类型
			 * x: x 轴坐标
			 * y: y 轴坐标
			 * col: 列坐标
			 * row: 行坐标
			 */
			let info = {
				type: roles_info.type,
				section: role.section,
				x: roles_info.x + 80 * (role.col - 1),
				y: roles_info.y + 100 * (role.row - 1),
				col: role.col,
				row: role.row,
			}
			// 由角色坐标数组创建对应角色
			if (type === 'plant') {
				self.plants.push(Plant.new(info))
			} else if (type === 'zombie') {
				self.zombies.push(Zombie.new(info))
			}
		}

	}
	// 游戏启动程序
	start() {
		let self = this
		// 创建 loading 对象,绘制 loading 画面
		self.loading = Animation.new({
			type: 'loading'
		}, 'write', 55)
		// 创建 阳光 实例对象
		self.sunnum = SunNum.new()
		// 生成僵尸数组信息
		self.setZombiesInfo()
		// 创建 除草车 实例对象数组,可清除一整行僵尸
		self.setCars(self.cars_info)
		// 创建 卡片 实例对象数组,左上角可放置植物卡片
		self.setCards(self.cards_info)
		// 创建 植物 实例对象数组,绘制植物
		self.setRoles(self.plants_info)
		// 创建 僵尸 实例对象数组,绘制僵尸
		self.setRoles(self.zombies_info)
		// 创建游戏引擎类
		self.game = Game.new()
	}
}
window._main = new Main()
window._main.start()

四、项目截图

  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
提供国人写的强大的html5植物大战僵尸(源码) 写得很棒~占用资源少。 JSPVZ 程序制作进度(2011.1.5) 本程序提供源码由HTML5中文网整理打包下载,该下载包可以使用服务器环境运行,也可以在电脑上双击Index.htm直接运行 另外智能手机可以安装OperaMobile10.1及其以上版本,把代码整个拷贝到手机存储卡上,在浏览器中输入形如“E:/jspvz”形式的地址直接本地运行程序,无需联网 转载使用请勿修改LonelyStar署名,pvz.lonelystar.org网址和PopCap公司版权声明 保留对该JS版植物大战僵尸版权所有 2011.1.5 添加了第二大关的第三小关 修正一大波和最后一波字样无法消失的BUG 2010.12.31 添加了“解谜模式” 调整了程序中关卡对于胜利和失败的算法 几个植物和僵尸做了调整 修改了几个BUG 2010.12.27 对初始界面稍作修改 2010.12.9 添加了“靠天吃饭”小游戏 给领带僵尸添加两种形象 修正辣椒爆炸图片的问题 咖啡豆0耗的数据修正 2010.12.8 提高了一下僵尸行走的纵坐标 修正了土豆雷和樱桃炸弹爆炸图片在IE下的问题 调整了一大波僵尸和最后一波僵尸出现的图片效果 2010.12.7 添加了第二大关的两小关 添加了“乱葬岗”小游戏 修改了几个BUG 调整了进度显示 2010.12.1 添加了“贫瘠之地”小游戏 调整游戏初始化界面和选择模式界面 修正第十关IE下运行报错的BUG 修正IE下单机运行有部分图片看不到的问题 修正蘑菇植物无睡眠动画而实际却在睡眠的BUG 修正曾哥蘑菇种植在醒着的大喷菇上仍然睡着的BUG 修正僵尸出场界面显示僵尸种类的BUG 修正地刺和地刺王伤害过高的BUG 加大“僵尸快跑!”的难度
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小孙同学1024

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值