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

  1. 先搭架子

整体效果:

点击开始后进入主场景

左侧是植物卡片

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

html 复制代码
<!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>
  1. 导入植物/僵尸/阳光...的图片

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

javascript 复制代码
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,
        },
        },
    }
}
  1. 场景的塑造

例如:左上角的阳光显示板, 右侧的植物卡片, 小汽车和子弹等等...
先来了解一下Canvas这个标签, 你可以把它想像成一个画布,我们可以通过获取上下文来绘制在画布上进行绘画(坐标系如下)

javascript 复制代码
    <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. 字体大小和颜色

javascript 复制代码
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
    }
}

左侧卡片:当我们使用了一个植物后,它的状态就会改变, 类似于进入到冷却时间

javascript 复制代码
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(在一定范围内)的时候, 就会清除整行僵尸

javascript 复制代码
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状态下, 才会进行触发

javascript 复制代码
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
        }
    }
}

为角色设置动画

javascript 复制代码
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
        }
    }
}

为植物和僵尸设置不同状态下的动画效果

javascript 复制代码
/**
 * 角色类
 * 植物、僵尸类继承的基础属性
 */
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]
  }
}

游戏引擎

javascript 复制代码
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()
        }
    }


}

主程序入口

javascript 复制代码
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代码和相关图片资源也会上传

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

相关推荐
熊猫_豆豆2 小时前
一个模拟四轴飞行器在随机气流扰动下悬停飞行的交互式3D仿真网页,包含飞行器建模与PID控制算法
javascript·3d·html·四轴无人机模拟飞行
小贺儿开发2 小时前
一句话生成网页 + 自动化办公(OpenCode + DeepSeek-V4)
css·自动化·html·工具·代码·网页·deepseek
来恩10033 小时前
jQuery选择器
前端·javascript·jquery
前端繁华如梦3 小时前
树上挂苹果还是挂玻璃球?Three.js 程序化果实的完整实现指南
前端·javascript
CDwenhuohuo4 小时前
优惠券组件直接用 uview plus
前端·javascript·vue.js
川冰ICE4 小时前
TypeScript装饰器与元编程实战
前端·javascript·typescript
AI砖家5 小时前
Vue3组件传参大全,各种传参方式的对比
前端·javascript·vue.js
希望永不加班5 小时前
var局部变量类型推断的利弊
java·服务器·前端·javascript·html
97zz5 小时前
Claude+deepseek-v4pro+cc switch+VSCode AI编程配置教程(Java开发专属)
java·vscode·ai编程
threelab5 小时前
Three.js 3D 地图可视化 | 三维可视化 / AI 提示词
前端·javascript·人工智能·3d·着色器