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

 一、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
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小孙同学1024

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

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

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

打赏作者

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

抵扣说明:

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

余额充值