用JS实现植物大战僵尸(前端作业)

发布于:2025-06-02 ⋅ 阅读:(26) ⋅ 点赞:(0)

1. 先搭架子

整体效果:

点击开始后进入主场景

左侧是植物卡片

右上角是游戏的开始和暂停键

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</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>
            </div>
        </li>
    <li class="cards-item" data-section="wallnut">
        <div class="card-intro">
        <span>坚果墙</span>
        <span>冷却时间:12秒</span>
        </div>
    </li>
    <li class="cards-item" data-section="peashooter">
        <div class="card-intro">
        <span>豌豆射手</span>
        <span>冷却时间:7秒</span>
        </div>
    </li>
    <li class="cards-item" data-section="repeater">
        <div class="card-intro">
        <span>双发豌豆射手</span>
        <span>冷却时间:10秒</span>
        </div>
    </li>
    <li class="cards-item" data-section="gatlingpea">
        <div class="card-intro">
        <span>加特林射手</span>
        <span>冷却时间:15秒</span>
        </div>
    </li>
    <li class="cards-item" data-section="chomper">
        <div class="card-intro">
        <span>食人花</span>
        <span>冷却时间:15秒</span>
        </div>
    </li>
    <li class="cards-item" data-section="cherrybomb">
        <div class="card-intro">
        <span>樱桃炸弹</span>
        <span>冷却时间:25秒</span>
        </div>
    </li>
    </ul>
    <!--Start and Pause-->
    <div class="menu-box">
        <div id="pauseGame" class="contro-btn">暂停</div>
        <div id="restartGame" class="contro-btn">开始游戏</div>
    </div>
    <!--自动生成阳光-->
    <!-- <img class="sum-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>

2. 导入植物/僵尸/阳光...的图片 

图片包含:植物cd好的状态和冷却期的状态,植物空闲状态/攻击状态,僵尸包含移动状态/攻击状态/樱桃炸弹炸的效果, 同时我们提供对外的imageFromPath函数, 用来生成图片路径

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,
        },
        },
    }
}

3. 场景的塑造

例如:左上角的阳光显示板, 右侧的植物卡片, 小汽车和子弹等等...

先来了解一下Canvas这个标签, 你可以把它想像成一个画布,我们可以通过获取上下文来绘制在画布上进行绘画(坐标系如下) 

    <canvas id="canvas" width="500" height="500"></canvas>
    <script>
        let canvas=document.getElementById("canvas")
        let cxt=canvas.getContext("2d")     //画笔
        //绘制一个矩形
        ctx.rect(0,0,100,200)
        //实心
        ctx.fill()    
        //描边
        ctx.stroke()

        //为上下文填充颜色
        cxt.fillStyle="orange"
        
        //填充文本
        ctx.font="700 16px Arial"
        ctx.fillText("内容",x,y,[,maxWidth])

        //添加图片
        let img=new Image()
        img.src='myImage.png'
        cxt.drawImage(img,x,y,width,height)

        //预加载
        let img=new Image()
        img.onload=function(){
            ctx.drawImage(img,0,0)
        }
        img.src='myImage.png'

    </script>

 

 

阳光显示板:1. 背景img  2. 所显示的阳光总数量 3. 字体大小和颜色

class SunNum{
    constructor(){
        let s={
            img:null,
            sun_num:window._main.allSunVal,  //阳光总数量
            x:105,
            y:0,
        }
        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)  //用于在Canvas上绘制图像
        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,
            y:obj.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)) 
        if(b.canClick){
            b.img=b.images[0]
        }else{
            b.img=b.images[1]
        }
        b.timer_num = b.timer_spacing / 1000  //1000ms                           
        return b
    }
    draw(cxt) {
        let self = this, marginLeft = 120
        if(self.sun_val > window._main.allSunVal){
            self.canGrow = false
        }else{
            self.canGrow = true
        }
        if(self.canGrow && self.canClick){
            self.img = self.images[0]
        }else{
            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(()=>{        //定时器
            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)
        }
    }
}

 除草车:当僵尸靠近坐标x(在一定范围内)的时候,  就会清除整行僵尸

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,
            y:obj.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')
                }
                }
            }
        }
    }
}

 子弹:例如像豌豆射手就会发射子弹,但是只有在state_RUNNING状态下, 才会进行触发

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){
        if(game.state === game.state_RUNNING){
            this.x+=4
        }else{
            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]
  }
}

 游戏引擎

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((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)
    }
    drawLoading(){
        let g=this,cxt=g.context,img=imageFromPath(allImg.startBg)
        cxt.drawImage(img,119,0)
    }
    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]
        }
        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((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((plant, idx)=>{ plant.canAttack() 
            plant.update(g)
        })
        zombies.forEach((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((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)
        }
    }
    drawMousePlant(plant_info){
        let g = this,cxt = g.context,plant = null
        let 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)
    }
    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)
        }
    }
    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]) {
                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()
            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()
        }
    }


    //========================================================================


    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.getElementById('js-startGame-btn').onclick = function () {
        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
                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()
        }
    }


}

 主程序入口

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,
                y:0,
                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,
                y:92,
                position:[]
            },
            zombies_info:{
                type:'zombie',
                x:170,
                y:15,
                position:[]
            },
            zombies_idx: 0,                           
            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) {
            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))
        }
    }


    //palnt or zombie
    setRoles(roles_info){
        let self=this,type = roles_info.type
        for (let role of roles_info.position){
            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
        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()

只对JS中常见的DOM/BOM和基础语法进行巩固,后续的CSS代码和相关图片资源也会上传

感谢大家的点赞和关注,你们的支持是我创作的动力!