用JS完成植物大战僵尸(前端作业)

1. 先搭架子

整体效果:

点击开始后进入主场景

左侧是植物卡片

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

  1. <!DOCTYPEhtml>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>Document</title>
  7. <link rel="stylesheet" href="css/common.css">
  8. <link rel="stylesheet" href="css/style.css">
  9. </head>
  10. <body>
  11. <div id="js-startGame-btn" class="startGame-btn">点击开始游戏</div>
  12. <!--主场景-->
  13. <div class="content-box">
  14. <canvas id="canvas" width="1400" height="600"> </canvas>
  15. </div>
  16. <!--左侧植物-->
  17. <ul class="cards-list">
  18. <li class="cards-item" data-section="sunflower">
  19. <div class="card-intro">
  20. <span>向日葵</span>
  21. <span>冷却时间:5秒</span>
  22. </div>
  23. </li>
  24. <li class="cards-item" data-section="wallnut">
  25. <div class="card-intro">
  26. <span>坚果墙</span>
  27. <span>冷却时间:12秒</span>
  28. </div>
  29. </li>
  30. <li class="cards-item" data-section="peashooter">
  31. <div class="card-intro">
  32. <span>豌豆射手</span>
  33. <span>冷却时间:7秒</span>
  34. </div>
  35. </li>
  36. <li class="cards-item" data-section="repeater">
  37. <div class="card-intro">
  38. <span>双发豌豆射手</span>
  39. <span>冷却时间:10秒</span>
  40. </div>
  41. </li>
  42. <li class="cards-item" data-section="gatlingpea">
  43. <div class="card-intro">
  44. <span>加特林射手</span>
  45. <span>冷却时间:15秒</span>
  46. </div>
  47. </li>
  48. <li class="cards-item" data-section="chomper">
  49. <div class="card-intro">
  50. <span>食人花</span>
  51. <span>冷却时间:15秒</span>
  52. </div>
  53. </li>
  54. <li class="cards-item" data-section="cherrybomb">
  55. <div class="card-intro">
  56. <span>樱桃炸弹</span>
  57. <span>冷却时间:25秒</span>
  58. </div>
  59. </li>
  60. </ul>
  61. <!--Start and Pause-->
  62. <div class="menu-box">
  63. <div id="pauseGame" class="contro-btn">暂停</div>
  64. <div id="restartGame" class="contro-btn">开始游戏</div>
  65. </div>
  66. <!--自动生成阳光-->
  67. <!-- <img class="sum-img systemSun" src="images/sun.gif" alt=""> -->
  68. <script src="js/common.js"> </script>
  69. <script src="js/scene.js"> </script>
  70. <script src="js/game.js"> </script>
  71. <script src="js/main.js"> </script>
  72. </body>
  73. </html>

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

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

  1. constimageFromPath =function(src){
  2. let img = new Image()
  3. img.src = './images/' + src
  4. return img
  5. }
  6. // 原生动画参数
  7. // const keyframesOptions = {
  8. // iterations: 1,
  9. // iterationStart: 0,
  10. // delay: 0,
  11. // endDelay: 0,
  12. // direction: 'alternate',
  13. // duration: 3000,
  14. // fill: 'forwards',
  15. // easing: 'ease-out',
  16. // }
  17. // 图片素材路径
  18. constallImg = {
  19. startBg: 'coverBg.jpg', // 首屏背景图
  20. bg: 'background1.jpg', // 游戏背景
  21. bullet: 'bullet.png', // 子弹普通状态
  22. bulletHit: 'bullet_hit.png', // 子弹击中敌人状态
  23. sunback: 'sunback.png', // 阳光背景框
  24. zombieWon: 'zombieWon.png', // 僵尸胜利画面
  25. car: 'car.png', // 小汽车图片
  26. loading: { // loading 画面
  27. write: {
  28. path: 'loading/loading_*.png',
  29. len: 3,
  30. },
  31. },
  32. plantsCard: { // 植物卡片
  33. sunflower: { // 向日葵
  34. img: 'cards/plants/SunFlower.png',
  35. imgG: 'cards/plants/SunFlowerG.png',
  36. },
  37. peashooter: { // 豌豆射手
  38. img: 'cards/plants/Peashooter.png',
  39. imgG: 'cards/plants/PeashooterG.png',
  40. },
  41. repeater: { // 双发射手
  42. img: 'cards/plants/Repeater.png',
  43. imgG: 'cards/plants/RepeaterG.png',
  44. },
  45. gatlingpea: { // 加特林射手
  46. img: 'cards/plants/GatlingPea.png',
  47. imgG: 'cards/plants/GatlingPeaG.png',
  48. },
  49. cherrybomb: { // 樱桃炸弹
  50. img: 'cards/plants/CherryBomb.png',
  51. imgG: 'cards/plants/CherryBombG.png',
  52. },
  53. wallnut: { // 坚果墙
  54. img: 'cards/plants/WallNut.png',
  55. imgG: 'cards/plants/WallNutG.png',
  56. },
  57. chomper: { // 食人花
  58. img: 'cards/plants/Chomper.png',
  59. imgG: 'cards/plants/ChomperG.png',
  60. },
  61. },
  62. plants: { // 植物
  63. sunflower: { // 向日葵
  64. idle: {
  65. path: 'plants/sunflower/idle/idle_*.png',
  66. len: 18,
  67. },
  68. },
  69. peashooter: { // 豌豆射手
  70. idle: {
  71. path: 'plants/peashooter/idle/idle_*.png',
  72. len: 8,
  73. },
  74. attack: {
  75. path: 'plants/peashooter/attack/attack_*.png',
  76. len: 8,
  77. },
  78. },
  79. repeater: { // 双发射手
  80. idle: {
  81. path: 'plants/repeater/idle/idle_*.png',
  82. len: 15,
  83. },
  84. attack: {
  85. path: 'plants/repeater/attack/attack_*.png',
  86. len: 15,
  87. },
  88. },
  89. gatlingpea: { // 加特林射手
  90. idle: {
  91. path: 'plants/gatlingpea/idle/idle_*.png',
  92. len: 13,
  93. },
  94. attack: {
  95. path: 'plants/gatlingpea/attack/attack_*.png',
  96. len: 13,
  97. },
  98. },
  99. cherrybomb: { // 樱桃炸弹
  100. idle: {
  101. path: 'plants/cherrybomb/idle/idle_*.png',
  102. len: 7,
  103. },
  104. attack: {
  105. path: 'plants/cherrybomb/attack/attack_*.png',
  106. len: 5,
  107. },
  108. },
  109. wallnut: { // 坚果墙
  110. idleH: { // 血量高时动画
  111. path: 'plants/wallnut/idleH/idleH_*.png',
  112. len: 16,
  113. },
  114. idleM: { // 血量中等时动画
  115. path: 'plants/wallnut/idleM/idleM_*.png',
  116. len: 11,
  117. },
  118. idleL: { // 血量低时动画
  119. path: 'plants/wallnut/idleL/idleL_*.png',
  120. len: 15,
  121. },
  122. },
  123. chomper: { // 食人花
  124. idle: { // 站立动画
  125. path: 'plants/chomper/idle/idle_*.png',
  126. len: 13,
  127. },
  128. attack: { // 攻击动画
  129. path: 'plants/chomper/attack/attack_*.png',
  130. len: 8,
  131. },
  132. digest: { // 消化阶段动画
  133. path: 'plants/chomper/digest/digest_*.png',
  134. len: 6,
  135. }
  136. },
  137. },
  138. zombies: { // 僵尸
  139. idle: { // 站立动画
  140. path: 'zombies/idle/idle_*.png',
  141. len: 31,
  142. },
  143. run: { // 移动动画
  144. path: 'zombies/run/run_*.png',
  145. len: 31,
  146. },
  147. attack: { // 攻击动画
  148. path: 'zombies/attack/attack_*.png',
  149. len: 21,
  150. },
  151. dieboom: { // 被炸死亡动画
  152. path: 'zombies/dieboom/dieboom_*.png',
  153. len: 20,
  154. },
  155. dying: { // 濒死动画
  156. head: {
  157. path: 'zombies/dying/head/head_*.png',
  158. len: 12,
  159. },
  160. body: {
  161. path: 'zombies/dying/body/body_*.png',
  162. len: 18,
  163. },
  164. },
  165. die: { // 死亡动画
  166. head: {
  167. path: 'zombies/dying/head/head_*.png',
  168. len: 12,
  169. },
  170. body: {
  171. path: 'zombies/die/die_*.png',
  172. len: 10,
  173. },
  174. },
  175. }
  176. }

3. 场景的塑造

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

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

  1. <canvas id="canvas"width="500"height="500"></canvas>
  2. <script>
  3. letcanvas=document.getElementById("canvas")
  4. letcxt=canvas.getContext("2d") //画笔
  5. //绘制一个矩形
  6. ctx.rect(0, 0, 100, 200)
  7. //实心
  8. ctx.fill()
  9. //描边
  10. ctx.stroke()
  11. //为上下文填充颜色
  12. cxt.fillStyle="orange"
  13. //填充文本
  14. ctx.font="700 16px Arial"
  15. ctx.fillText("内容",x,y,[,maxWidth])
  16. //添加图片
  17. let img=new Image()
  18. img.src='myImage.png'
  19. cxt.drawImage(img,x,y,width,height)
  20. //预加载
  21. let img=new Image()
  22. img.onload=function(){
  23. ctx.drawImage(img,0, 0)
  24. }
  25. img.src='myImage.png'
  26. </script>

 

 

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

  1. class SunNum{
  2. constructor(){
  3. let s={
  4. img:null,
  5. sun_num:window._main.allSunVal, //阳光总数量
  6. x:105,
  7. y:0,
  8. }
  9. Object.assign(this,s)
  10. }
  11. static new(){
  12. let s=new this()
  13. s.img=imageFromPath(allImg.sunback)
  14. return s
  15. }
  16. draw(cxt){
  17. let self=this
  18. cxt.drawImage(self.img,self.x+120,self.y) //用于在Canvas上绘制图像
  19. cxt.fillStyle='black'
  20. cxt.font='24px Microsoft YaHei'
  21. cxt.fontWeight=700
  22. cxt.fillText(self.sun_num,self.x+175,self.y+27)
  23. }
  24. //修改阳光 !!!!!
  25. changeSunNum(num=25){
  26. let self=this
  27. window._main.allSunVal+=num
  28. self.sun_num+=num
  29. }
  30. }

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

  1. class Card{
  2. constructor(obj){
  3. let c={
  4. name:obj.name,
  5. canGrow:true,
  6. canClick:true,
  7. img:null,
  8. images:[],
  9. timer:null,
  10. timer_spacing:obj.timer_spacing,
  11. timer_num:1,
  12. sun_val:obj.sun_val,
  13. row:obj.row,
  14. x:0,
  15. y:obj.y,
  16. }
  17. Object.assign(this,c)
  18. }
  19. static new(obj){
  20. let b=new this(obj)
  21. b.images.push(imageFromPath(allImg.plantsCard[b.name].img))
  22. b.images.push(imageFromPath(allImg.plantsCard[b.name].imgG))
  23. if(b.canClick){
  24. b.img=b.images[0]
  25. }else{
  26. b.img=b.images[1]
  27. }
  28. b.timer_num = b.timer_spacing / 1000 //1000ms
  29. return b
  30. }
  31. draw(cxt) {
  32. letself =this, marginLeft =120
  33. if(self.sun_val > window._main.allSunVal){
  34. self.canGrow = false
  35. }else{
  36. self.canGrow = true
  37. }
  38. if(self.canGrow&& self.canClick){
  39. self.img= self.images[0]
  40. }else{
  41. self.img= self.images[1]
  42. }
  43. cxt.drawImage(self.img, self.x+ marginLeft, self.y)
  44. cxt.fillStyle = 'black'
  45. cxt.font = '16px Microsoft YaHei'
  46. cxt.fillText(self.sun_val, self.x+ marginLeft +60, self.y + 55)
  47. if(!self.canClick&& self.canGrow) {
  48. cxt.fillStyle = 'rgb(255, 255, 0)'
  49. cxt.font = '20px Microsoft YaHei'
  50. cxt.fillText(self.timer_num, self.x+ marginLeft +30, self.y + 35)
  51. }
  52. }
  53. drawCountDown(){
  54. let self=this
  55. self.timer=setInterval(()=>{ //定时器
  56. if(self.timer_num>0){
  57. self.timer_num--
  58. }else{
  59. clearInterval(self.timer)
  60. self.timer_num=self.timer_spacing/1000
  61. }
  62. },1000)
  63. }
  64. changeState(){
  65. let self=this
  66. if(!self.canClick){
  67. self.timer=setTimeout(()=> { //延时器
  68. self.canClick=true
  69. },self.timer_spacing)
  70. }
  71. }
  72. }

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

  1. class Car{
  2. constructor(obj){
  3. let c={
  4. img: imageFromPath(allImg.car),
  5. state:1,
  6. state_NORMALE:1,
  7. state_ATTACK:2,
  8. w:71,
  9. h:57,
  10. x:obj.x,
  11. y:obj.y,
  12. row:obj.row,
  13. }
  14. Object.assign(this,c)
  15. }
  16. static new(obj){
  17. let c=new this(obj)
  18. return c
  19. }
  20. draw(game,cxt){
  21. letself =this
  22. self.canMove()
  23. self.state=== self.state_ATTACK&& self.step(game)
  24. cxt.drawImage(self.img, self.x, self.y)
  25. }
  26. step(game) {
  27. game.state=== game.state_RUNNING ? this.x += 15 : this.x = this.x
  28. }
  29. 否移动小车 (zombie.x < 150时)就是// 判断
  30. canMove () {
  31. letself =this
  32. for (letzombieof window._main.zombies) {
  33. if(zombie.row=== self.row) {
  34. if(zombie.x < 150) {
  35. self.state= self.state_ATTACK
  36. }
  37. if(self.state=== self.state_ATTACK) {
  38. if(zombie.x- self.x< self.w&& zombie.x < 950) {
  39. zombie.life = 0
  40. zombie.changeAnimation('die')
  41. }
  42. }
  43. }
  44. }
  45. }
  46. }

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

  1. class Bullet{
  2. constructor(plant){
  3. let b={
  4. img: imageFromPath(allImg.bullet),
  5. w:56,
  6. h:34,
  7. x:0,
  8. y:0,
  9. }
  10. Object.assign(this,b)
  11. }
  12. static new(plant){
  13. let b=new this(plant)
  14. switch(plant.section) {
  15. case 'peashooter':
  16. b.x= plant.x + 30
  17. b.y= plant.y
  18. break
  19. case 'repeater':
  20. b.x= plant.x + 30
  21. b.y= plant.y
  22. break
  23. case 'gatlingpea':
  24. b.x= plant.x + 30
  25. b.y= plant.y + 10
  26. break
  27. }
  28. return b
  29. }
  30. draw(game,cxt){
  31. let self=this
  32. self.step(game)
  33. cxt.drawImage(self.img,self.x,self.y)
  34. }
  35. step(game){
  36. if(game.state=== game.state_RUNNING){
  37. this.x+=4
  38. }else{
  39. this.x=this.x
  40. }
  41. }
  42. }

 为角色设置动画

  1. class Animation{
  2. constructor(role, action, fps) {
  3. let a = {
  4. type: role.type, // 动画类型(植物、僵尸等等)
  5. section: role.section, // 植物或者僵尸类别(向日葵、豌豆射手)
  6. action: action,// 根据传入动作生成不同动画对象数组
  7. images: [], // 当前引入角色图片对象数组
  8. img: null, // 当前显示角色图片
  9. imgIdx: 0, // 当前角色图片序列号
  10. count: 0, // 计数器,控制动画运行
  11. imgHead: null, // 当前显示角色头部图片
  12. imgBody: null, // 当前展示角色身体图片
  13. imgIdxHead: 0, // 当前角色头部图片序列号
  14. imgIdxBody: 0, // 当前角色身体图片序列号
  15. countHead: 0, // 当前角色头部计数器,控制动画运行
  16. countBody: 0, // 当前角色身体计数器,控制动画运行
  17. fps: fps,// 角色动画运行速度系数,值越小,速度越快
  18. }
  19. Object.assign(this, a)
  20. }
  21. // 创建,并初始化当前对象
  22. static new(role, action, fps) {
  23. let a = new this(role, action, fps)
  24. // 濒死动画、死亡动画对象(僵尸)
  25. if(action ==='dying'|| action ==='die') {
  26. a.images = {
  27. head: [],
  28. body: [],
  29. }
  30. a.create()
  31. } else {
  32. a.create()
  33. a.images[0].onload = function () {
  34. role.w = this.width
  35. role.h = this.height
  36. }
  37. }
  38. return a
  39. }
  40. /**
  41. * 为角色不同动作创造动画序列
  42. */
  43. create () {
  44. letself =this,
  45. section = self.section // 植物种类
  46. switch(self.type) {
  47. case 'plant':
  48. for(let i = 0; i < allImg.plants[section][self.action].len; i++){
  49. letidx = i <10 ? '0'+ i : i,
  50. path = allImg.plants[section][self.action].path
  51. // 依次添加动画序列
  52. self.images.push(imageFromPath(path.replace(/\*/, idx)))
  53. }
  54. break
  55. case 'zombie':
  56. // 濒死动画、死亡动画对象,包含头部动画以及身体动画
  57. if(self.action === 'dying'|| self.action === 'die') {
  58. for(let i = 0; i < allImg.zombies[self.action].head.len; i++){
  59. letidx = i <10 ? '0'+ i : i,
  60. path = allImg.zombies[self.action].head.path
  61. // 依次添加动画序列
  62. self.images.head.push(imageFromPath(path.replace(/\*/, idx)))
  63. }
  64. for(let i = 0; i < allImg.zombies[self.action].body.len; i++){
  65. letidx = i <10 ? '0'+ i : i,
  66. path = allImg.zombies[self.action].body.path
  67. // 依次添加动画序列
  68. self.images.body.push(imageFromPath(path.replace(/\*/, idx)))
  69. }
  70. } else { // 普通动画对象
  71. for(let i = 0; i < allImg.zombies[self.action].len; i++){
  72. letidx = i <10 ? '0'+ i : i,
  73. path = allImg.zombies[self.action].path
  74. // 依次添加动画序列
  75. self.images.push(imageFromPath(path.replace(/\*/, idx)))
  76. }
  77. }
  78. break
  79. case 'loading': // loading动画
  80. for(let i = 0; i < allImg.loading[self.action].len; i++){
  81. letidx = i <10 ? '0'+ i : i,
  82. path = allImg.loading[self.action].path
  83. // 依次添加动画序列
  84. self.images.push(imageFromPath(path.replace(/\*/, idx)))
  85. }
  86. break
  87. }
  88. }
  89. }

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

  1. /**
  2. * 角色类
  3. * 植物、僵尸类继承的基础属性
  4. */
  5. class Role{
  6. constructor(obj) {
  7. let r = {
  8. id: Math.random().toFixed(6) * Math.pow(10, 6), // 随机生成 id 值,用于设置当前角色 ID
  9. type: obj.type, // 角色类型(植物或僵尸)
  10. section: obj.section, // 角色类别(豌豆射手、双发射手...)
  11. x: obj.x, // x轴坐标
  12. y: obj.y, // y轴坐标
  13. row: obj.row, // 角色初始化行坐标
  14. col: obj.col, // 角色初始化列坐标
  15. w: 0, // 角色图片宽度
  16. h: 0, // 角色图片高度
  17. isAnimeLenMax: false, // 是否处于动画最后一帧,用于判断动画是否执行完一轮
  18. isDel: false, 否死亡并移除当前角色就是// 判断
  19. isHurt: false, // 判断是否受伤
  20. }
  21. Object.assign(this, r)
  22. }
  23. }
  24. // 植物类
  25. class Plant extends Role{
  26. constructor(obj) {
  27. super(obj)
  28. // 植物类私有属性
  29. let p = {
  30. life: 3, // 角色血量
  31. idle: null, // 站立动画对象
  32. idleH: null, // 坚果高血量动画对象
  33. idleM: null, // 坚果中等血量动画对象
  34. idleL: null, // 坚果低血量动画对象
  35. attack: null, // 角色攻击动画对象
  36. digest: null, // 角色消化动画对象
  37. bullets: [], // 子弹数组对象
  38. state: obj.section === 'wallnut' ? 2 : 1, // 保存当前状态值
  39. state_IDLE: 1, // 站立不动状态
  40. state_IDLE_H: 2, // 站立不动高血量状态(坚果墙相关动画)
  41. state_IDLE_M: 3, // 站立不动中等血量状态(坚果墙相关动画)
  42. state_IDLE_L: 4, // 站立不动低血量状态(坚果墙相关动画)
  43. state_ATTACK: 5, // 攻击状态
  44. state_DIGEST: 6, // 待攻击状态(食人花消化僵尸状态)
  45. canShoot: false, // 植物是否具有发射子弹功能
  46. canSetTimer: obj.canSetTimer, // 能否设置生成阳光定时器
  47. sunTimer: null, // 生成阳光定时器
  48. sunTimer_spacing: 10, // 生成阳光时间间隔(秒)
  49. }
  50. Object.assign(this, p)
  51. }
  52. // 创建,并初始化当前对象
  53. static new(obj) {
  54. let p = new this(obj)
  55. p.init()
  56. return p
  57. }
  58. // 设置阳光生成定时器
  59. setSunTimer () {
  60. letself =this
  61. self.sunTimer = setInterval(function () {
  62. // 创建阳光元素
  63. let img = document.createElement('img'), // 创建元素
  64. container =document.getElementsByTagName('body')[0], // 父级元素容器
  65. id = self.id, // 当前角色 ID
  66. top = self.y + 30,
  67. left = self.x - 130,
  68. keyframes1 = [// 阳光移动动画 keyframes
  69. { transform: 'translate(0,0)', opacity: 0 },
  70. { offset: .3, transform: 'translate(0,0)', opacity: 1 },
  71. { offset: .5, transform: 'translate(0,0)', opacity: 1 },
  72. { offset: 1, transform: 'translate(-'+ (left -110) +'px,-'+ (top +50) +'px)', opacity: 0 }
  73. ]
  74. // 添加阳关元素
  75. img.src = 'images/sun.gif'
  76. img.className += 'sun-img plantSun' + id
  77. img.style.top= top +'px'
  78. img.style.left= left +'px'
  79. container.appendChild(img)
  80. // 添加阳光移动动画
  81. let sun = document.getElementsByClassName('plantSun'+ id)[0]
  82. sun.animate(keyframes1,keyframesOptions)
  83. // 动画完成,清除阳光元素
  84. setTimeout(()=> {
  85. sun.parentNode.removeChild(sun)
  86. // 增加阳光数量
  87. window._main.sunnum.changeSunNum()
  88. }, 2700)
  89. }, self.sunTimer_spacing * 1000)
  90. }
  91. // 清除阳光生成定时器
  92. clearSunTimer () {
  93. letself =this
  94. clearInterval(self.sunTimer)
  95. }
  96. // 初始化
  97. init () {
  98. letself =this,
  99. setPlantFn =null
  100. // 初始化植物动画对象方法集
  101. setPlantFn = {
  102. sunflower () {// 向日葵
  103. self.idle = Animation.new(self,'idle', 12)
  104. // 定时生成阳光
  105. self.canSetTimer&& self.setSunTimer()
  106. },
  107. peashooter () {// 豌豆射手
  108. self.canShoot = true
  109. self.idle = Animation.new(self,'idle', 12)
  110. self.attack = Animation.new(self,'attack', 12)
  111. },
  112. repeater () {// 双发射手
  113. self.canShoot = true
  114. self.idle = Animation.new(self,'idle', 12)
  115. self.attack = Animation.new(self,'attack', 8)
  116. },
  117. gatlingpea () {// 加特林射手
  118. // 改变加特林渲染 y 轴距离
  119. self.y -= 12
  120. self.canShoot = true
  121. self.idle = Animation.new(self,'idle', 8)
  122. self.attack = Animation.new(self,'attack', 4)
  123. },
  124. cherrybomb () {// 樱桃炸弹
  125. self.x -= 15
  126. self.idle = Animation.new(self,'idle', 15)
  127. self.attack = Animation.new(self,'attack', 15)
  128. setTimeout(()=> {
  129. self.state= self.state_ATTACK
  130. }, 2000)
  131. },
  132. wallnut () {// 坚果墙
  133. self.x += 15
  134. // 设置坚果血量
  135. self.life = 12
  136. // 创建坚果三种不同血量下的动画对象
  137. self.idleH = Animation.new(self,'idleH', 10)
  138. self.idleM = Animation.new(self,'idleM', 8)
  139. self.idleL = Animation.new(self,'idleL', 10)
  140. },
  141. chomper () {// 食人花
  142. self.life = 5
  143. self.y -= 45
  144. self.idle = Animation.new(self,'idle', 10)
  145. self.attack = Animation.new(self,'attack', 12)
  146. self.digest = Animation.new(self,'digest', 12)
  147. },
  148. }
  149. // 执行对应植物初始化技巧
  150. for (let key insetPlantFn) {
  151. if(self.section=== key) {
  152. setPlantFn[key]()
  153. }
  154. }
  155. }
  156. // 绘制途径
  157. draw (cxt) {
  158. letself =this,
  159. stateName = self.switchState()
  160. switch(self.isHurt) {
  161. case false:
  162. if(self.section === 'cherrybomb'&& self.state=== self.state_ATTACK) {
  163. // 正常状态,绘制樱桃炸弹爆炸图片
  164. cxt.drawImage(self[stateName].img, self.x - 60, self.y - 50)
  165. } else {
  166. // 正常状态,绘制普通植物图片
  167. cxt.drawImage(self[stateName].img, self.x, self.y)
  168. }
  169. break
  170. case true:
  171. // 受伤或移动植物时,绘制半透明图片
  172. cxt.globalAlpha = 0.5
  173. cxt.beginPath()
  174. cxt.drawImage(self[stateName].img, self.x, self.y)
  175. cxt.closePath()
  176. cxt.save()
  177. cxt.globalAlpha = 1
  178. break
  179. }
  180. }
  181. // 更新状态
  182. update (game) {
  183. letself =this,
  184. section = self.section,
  185. stateName = self.switchState()
  186. // 修改当前动画序列长度
  187. letanimateLen = allImg.plants[section][stateName].len
  188. // 累加动画计数器
  189. self[stateName].count += 1
  190. // 设置角色动画运行速度
  191. self[stateName].imgIdx = Math.floor(self[stateName].count/ self[stateName].fps)
  192. // 一整套动画完成后重置动画计数器
  193. self[stateName].imgIdx=== animateLen -1? self[stateName].count = 0: self[stateName].count= self[stateName].count
  194. // 绘制发射子弹动画
  195. if(game.state=== game.state_RUNNING) {
  196. // 设置当前帧动画对象
  197. self[stateName].img= self[stateName].images[self[stateName].imgIdx]
  198. if(self[stateName].imgIdx=== animateLen -1) {
  199. if(stateName ==='attack'&& !self.isDel) {
  200. // 未死亡,且为可发射子弹植物时
  201. if(self.canShoot) {
  202. // 发射子弹
  203. self.shoot()
  204. // 双发射手额外发射子弹
  205. self.section === 'repeater'&&setTimeout(()=>{self.shoot()}, 250)
  206. }
  207. // 当为樱桃炸弹时,执行完一轮动画,自动消失
  208. self.section === 'cherrybomb'? self.isDel = true: self.isDel = false
  209. // 当为食人花时,执行完攻击动画,切换为消化动画
  210. if(self.section === 'chomper') {
  211. // 立即切换动画会出现图片未加载完成报错
  212. setTimeout(()=> {
  213. self.changeAnimation('digest')
  214. }, 0)
  215. }
  216. } else if(self.section === 'chomper'&& stateName ==='digest') {
  217. // 消化动画完毕后,间隔一段时间切换为正常状态
  218. setTimeout(()=> {
  219. self.changeAnimation('idle')
  220. }, 30000)
  221. }
  222. self.isAnimeLenMax = true
  223. } else {
  224. self.isAnimeLenMax = false
  225. }
  226. }
  227. }
  228. // 检测植物是否可攻击僵尸方法
  229. canAttack () {
  230. letself =this
  231. // 植物类别为向日葵和坚果墙时,不需判定
  232. if(self.section === 'sunflower'|| self.section === 'wallnut') return false
  233. // 循环僵尸对象数组
  234. for (letzombieof window._main.zombies) {
  235. if(self.section === 'cherrybomb') { // 当为樱桃炸弹时
  236. // 僵尸在以樱桃炸弹为圆心的 9 个格子内时
  237. if (Math.abs(self.row- zombie.row) <=1&&Math.abs(self.col- zombie.col) <=1&& zombie.col < 10) {
  238. // 执行爆炸动画
  239. self.changeAnimation('attack')
  240. zombie.life = 0
  241. // 僵尸炸死动画
  242. zombie.changeAnimation('dieboom')
  243. }
  244. } else if(self.section === 'chomper'&& self.state=== self.state_IDLE) { // 当为食人花时
  245. // 僵尸在食人花正前方时
  246. if(self.row=== zombie.row&& (zombie.col- self.col) <=1&& zombie.col < 10) {
  247. self.changeAnimation('attack')
  248. setTimeout(()=> {
  249. zombie.isDel = true
  250. }, 1300)
  251. }
  252. } else if(self.canShoot&& self.row=== zombie.row) { // 当植物可发射子弹,且僵尸和植物处于同行时
  253. // 僵尸进入植物射程范围
  254. zombie.x < 940&& self.x< zombie.x + 10&& zombie.life > 0? self.changeAnimation('attack') : self.changeAnimation('idle')
  255. // 植物未被移除时,可发射子弹
  256. if(!self.isDel) {
  257. self.bullets.forEach(function (bullet, j) {
  258. // 当子弹打中僵尸,且僵尸未死亡时
  259. if (Math.abs(zombie.x+ bullet.w- bullet.x) <10&& zombie.life > 0) { // 子弹和僵尸距离小于 10 且僵尸未死亡
  260. // 移除子弹
  261. self.bullets.splice(j, 1)
  262. // 根据血量判断执行不同阶段动画
  263. if(zombie.life !== 0) {
  264. zombie.life--
  265. zombie.isHurt = true
  266. setTimeout(()=> {
  267. zombie.isHurt = false
  268. }, 200)
  269. }
  270. if(zombie.life === 2) {
  271. zombie.changeAnimation('dying')
  272. } else if(zombie.life === 0) {
  273. zombie.changeAnimation('die')
  274. }
  275. }
  276. })
  277. }
  278. }
  279. }
  280. }
  281. // 射击方法
  282. shoot () {
  283. letself =this
  284. self.bullets[self.bullets.length] = Bullet.new(self)
  285. }
  286. /**
  287. * 判断角色状态并返回对应动画对象名称方法
  288. */
  289. switchState () {
  290. letself =this,
  291. state = self.state,
  292. dictionary = {
  293. idle: self.state_IDLE,
  294. idleH: self.state_IDLE_H,
  295. idleM: self.state_IDLE_M,
  296. idleL: self.state_IDLE_L,
  297. attack: self.state_ATTACK,
  298. digest: self.state_DIGEST,
  299. }
  300. for (let key indictionary) {
  301. if(state === dictionary[key]) {
  302. return key
  303. }
  304. }
  305. }
  306. /**
  307. * 切换角色动画
  308. * game => 游戏引擎对象
  309. * action => 动作类型
  310. * -idle: 站立动画
  311. * -idleH: 角色高血量动画(坚果墙)
  312. * -idleM: 角色中等血量动画(坚果墙)
  313. * -idleL: 角色低血量动画(坚果墙)
  314. * -attack: 攻击动画
  315. * -digest: 消化动画(食人花)
  316. */
  317. changeAnimation (action) {
  318. letself =this,
  319. stateName = self.switchState(),
  320. dictionary = {
  321. idle: self.state_IDLE,
  322. idleH: self.state_IDLE_H,
  323. idleM: self.state_IDLE_M,
  324. idleL: self.state_IDLE_L,
  325. attack: self.state_ATTACK,
  326. digest: self.state_DIGEST,
  327. }
  328. if(action === stateName)return
  329. self.state= dictionary[action]
  330. }
  331. }
  332. // 僵尸类
  333. class Zombie extends Role{
  334. constructor(obj) {
  335. super(obj)
  336. // 僵尸类私有属性
  337. let z = {
  338. life: 10, // 角色血量
  339. canMove: true, // 判断当前角色是否可移动
  340. attackPlantID: 0, // 当前攻击植物对象 ID
  341. idle: null, // 站立动画对象
  342. run: null, // 奔跑动画对象
  343. attack: null, // 攻击动画对象
  344. dieboom: null, // 被炸死亡动画对象
  345. dying: null, // 濒临死亡动画对象
  346. die: null, // 死亡动画对象
  347. state: 1, // 保存当前状态值,默认为1
  348. state_IDLE: 1, // 站立不动状态
  349. state_RUN: 2, // 奔跑状态
  350. state_ATTACK: 3, // 攻击状态
  351. state_DIEBOOM: 4, // 死亡状态
  352. state_DYING: 5, // 濒临死亡状态
  353. state_DIE: 6, // 死亡状态
  354. state_DIGEST: 7, // 消化死亡状态
  355. speed: 3, // 移动速度
  356. head_x: 0, // 头部动画 x 轴坐标
  357. head_y: 0, // 头部动画 y 轴坐标
  358. }
  359. Object.assign(this, z)
  360. }
  361. // 创建,并初始化当前对象
  362. static new(obj) {
  363. let p = new this(obj)
  364. p.init()
  365. return p
  366. }
  367. // 初始化
  368. init () {
  369. letself =this
  370. // 站立
  371. self.idle = Animation.new(self,'idle', 12)
  372. // 移动
  373. self.run = Animation.new(self,'run', 12)
  374. // 攻击
  375. self.attack = Animation.new(self,'attack', 8)
  376. // 炸死
  377. self.dieboom = Animation.new(self,'dieboom', 8)
  378. // 濒死
  379. self.dying = Animation.new(self,'dying', 8)
  380. // 死亡
  381. self.die = Animation.new(self,'die', 12)
  382. }
  383. // 绘制方法
  384. draw (cxt) {
  385. letself =this,
  386. stateName = self.switchState()
  387. if(stateName !=='dying'&& stateName !=='die') { // 绘制普通动画
  388. if(!self.isHurt) { // 未受伤时,绘制正常动画
  389. cxt.drawImage(self[stateName].img, self.x, self.y)
  390. } else { // 受伤时,绘制带透明度动画
  391. // 绘制带透明度动画
  392. cxt.globalAlpha = 0.5
  393. cxt.beginPath()
  394. cxt.drawImage(self[stateName].img, self.x, self.y)
  395. cxt.closePath()
  396. cxt.save()
  397. cxt.globalAlpha = 1
  398. }
  399. } else { // 绘制濒死、死亡动画
  400. if(!self.isHurt) { // 未受伤时,绘制正常动画
  401. cxt.drawImage(self[stateName].imgHead, self.head_x + 70, self.head_y - 10)
  402. cxt.drawImage(self[stateName].imgBody, self.x, self.y)
  403. } else { // 受伤时,绘制带透明度动画
  404. // 绘制带透明度身体
  405. cxt.globalAlpha = 0.5
  406. cxt.beginPath()
  407. cxt.drawImage(self[stateName].imgBody, self.x, self.y)
  408. cxt.closePath()
  409. cxt.save()
  410. cxt.globalAlpha = 1
  411. // 头部不带透明度
  412. cxt.drawImage(self[stateName].imgHead, self.head_x + 70, self.head_y - 10)
  413. }
  414. }
  415. }
  416. // 更新状态
  417. update (game) {
  418. letself =this,
  419. stateName = self.switchState()
  420. // 更新能否移动状态值
  421. self.canMove? self.speed = 3: self.speed = 0
  422. // 更新僵尸列坐标值
  423. self.col = Math.floor((self.x - window._main.zombies_info.x) / 80 + 1)
  424. if(stateName !=='dying'&& stateName !=='die') { // 普通动画(站立,移动,攻击)
  425. // 修改当前动画序列长度
  426. letanimateLen = allImg.zombies[stateName].len
  427. // 累加动画计数器
  428. self[stateName].count += 1
  429. // 设置角色动画运行速度
  430. self[stateName].imgIdx = Math.floor(self[stateName].count/ self[stateName].fps)
  431. // 一整套动画完成后重置动画计数器
  432. if(self[stateName].imgIdx=== animateLen) {
  433. self[stateName].count = 0
  434. self[stateName].imgIdx = 0
  435. if(stateName ==='dieboom') { // 被炸死亡状态
  436. // 当死亡动画执行完一轮后,移除当前角色
  437. self.isDel = true
  438. }
  439. // 当前动画帧数达到最大值
  440. self.isAnimeLenMax = true
  441. } else {
  442. self.isAnimeLenMax = false
  443. }
  444. // 游戏运行状态
  445. if(game.state=== game.state_RUNNING) {
  446. // 设置当前帧动画对象
  447. self[stateName].img= self[stateName].images[self[stateName].imgIdx]
  448. if(stateName ==='run') { // 当僵尸移动时,控制移动速度
  449. self.x-= self.speed / 17
  450. }
  451. }
  452. } else if(stateName ==='dying') { // 濒死动画,包含两个动画对象
  453. // 获取当前动画序列长度
  454. letheadAnimateLen = allImg.zombies[stateName].head.len,
  455. bodyAnimateLen = allImg.zombies[stateName].body.len
  456. // 累加动画计数器
  457. if(self[stateName].imgIdxHead!== headAnimateLen -1) {
  458. self[stateName].countHead += 1
  459. }
  460. self[stateName].countBody += 1
  461. // 设置角色动画运行速度
  462. self[stateName].imgIdxHead = Math.floor(self[stateName].countHead/ self[stateName].fps)
  463. self[stateName].imgIdxBody = Math.floor(self[stateName].countBody/ self[stateName].fps)
  464. // 设置当前帧动画对象,头部动画
  465. if(self[stateName].imgIdxHead === 0) {
  466. self.head_x= self.x
  467. self.head_y= self.y
  468. self[stateName].imgHead= self[stateName].images.head[self[stateName].imgIdxHead]
  469. } else if(self[stateName].imgIdxHead=== headAnimateLen) {
  470. self[stateName].imgHead= self[stateName].images.head[headAnimateLen -1]
  471. } else {
  472. self[stateName].imgHead= self[stateName].images.head[self[stateName].imgIdxHead]
  473. }
  474. // 设置当前帧动画对象,身体动画
  475. if(self[stateName].imgIdxBody=== bodyAnimateLen) {
  476. self[stateName].countBody = 0
  477. self[stateName].imgIdxBody = 0
  478. // 当前动画帧数达到最大值
  479. self.isAnimeLenMax = true
  480. } else {
  481. self.isAnimeLenMax = false
  482. }
  483. // 游戏运行状态
  484. if(game.state=== game.state_RUNNING) {
  485. // 设置当前帧动画对象
  486. self[stateName].imgBody= self[stateName].images.body[self[stateName].imgIdxBody]
  487. if(stateName ==='dying') { // 濒死状态,可以移动
  488. self.x-= self.speed / 17
  489. }
  490. }
  491. } else if(stateName ==='die') { // 死亡动画,囊括两个动画对象
  492. // 获取当前动画序列长度
  493. letheadAnimateLen = allImg.zombies[stateName].head.len,
  494. bodyAnimateLen = allImg.zombies[stateName].body.len
  495. // 累加动画计数器
  496. if(self[stateName].imgIdxBody!== bodyAnimateLen -1) {
  497. self[stateName].countBody += 1
  498. }
  499. // 设置角色动画运行速度
  500. self[stateName].imgIdxBody = Math.floor(self[stateName].countBody/ self[stateName].fps)
  501. // 设置当前帧动画对象,死亡状态,定格头部动画
  502. if(self[stateName].imgIdxHead === 0) {
  503. if(self.head_x == 0&& self.head_y == 0) {
  504. self.head_x= self.x
  505. self.head_y= self.y
  506. }
  507. self[stateName].imgHead= self[stateName].images.head[headAnimateLen -1]
  508. }
  509. // 设置当前帧动画对象,身体动画
  510. if(self[stateName].imgIdxBody === 0) {
  511. self[stateName].imgBody= self[stateName].images.body[self[stateName].imgIdxBody]
  512. } else if(self[stateName].imgIdxBody=== bodyAnimateLen -1) {
  513. // 当死亡动画执行完一轮后,移除当前角色
  514. self.isDel = true
  515. self[stateName].imgBody= self[stateName].images.body[bodyAnimateLen -1]
  516. } else {
  517. self[stateName].imgBody= self[stateName].images.body[self[stateName].imgIdxBody]
  518. }
  519. }
  520. }
  521. // 检测僵尸是否可攻击植物
  522. canAttack () {
  523. letself =this
  524. // 循环植物对象数组
  525. for (let plant of window._main.plants) {
  526. if(plant.row=== self.row&& !plant.isDel) { // 当僵尸和植物处于同行时
  527. if(self.x- plant.x< -20&& self.x- plant.x> -60) {
  528. if(self.life > 2) {
  529. // 保存当前攻击植物 hash 值,在该植物被删除时,再控制当前僵尸移动
  530. self.attackPlantID!== plant.id? self.attackPlantID= plant.id: self.attackPlantID= self.attackPlantID
  531. self.changeAnimation('attack')
  532. } else {
  533. self.canMove = false
  534. }
  535. if(self.isAnimeLenMax&& self.life > 2) { // 僵尸动画每执行完一轮次
  536. // 扣除植物血量
  537. if(plant.life !== 0) {
  538. plant.life--
  539. plant.isHurt = true
  540. setTimeout(()=> {
  541. plant.isHurt = false
  542. // 坚果墙判断切换动画状态
  543. if(plant.life <= 8&& plant.section === 'wallnut') {
  544. plant.life <= 4? plant.changeAnimation('idleL') : plant.changeAnimation('idleM')
  545. }
  546. // 判断植物是否可移除
  547. if(plant.life <= 0) {
  548. // 设置植物死亡状态
  549. plant.isDel = true
  550. // 清除死亡向日葵的阳光生成定时器
  551. plant.section === 'sunflower'&& plant.clearSunTimer()
  552. }
  553. }, 200)
  554. }
  555. }
  556. }
  557. }
  558. }
  559. }
  560. /**
  561. * 判断角色状态并返回对应动画对象名称方法
  562. */
  563. switchState () {
  564. letself =this,
  565. state = self.state,
  566. dictionary = {
  567. idle: self.state_IDLE,
  568. run: self.state_RUN,
  569. attack: self.state_ATTACK,
  570. dieboom: self.state_DIEBOOM,
  571. dying: self.state_DYING,
  572. die: self.state_DIE,
  573. digest: self.state_DIGEST,
  574. }
  575. for (let key indictionary) {
  576. if(state === dictionary[key]) {
  577. return key
  578. }
  579. }
  580. }
  581. /**
  582. * 切换角色动画
  583. * game => 游戏引擎对象
  584. * action => 动作类型
  585. * -idle: 站立不动
  586. * -attack: 攻击
  587. * -die: 死亡
  588. * -dying: 濒死
  589. * -dieboom: 爆炸
  590. * -digest: 被消化
  591. */
  592. changeAnimation (action) {
  593. letself =this,
  594. stateName = self.switchState(),
  595. dictionary = {
  596. idle: self.state_IDLE,
  597. run: self.state_RUN,
  598. attack: self.state_ATTACK,
  599. dieboom: self.state_DIEBOOM,
  600. dying: self.state_DYING,
  601. die: self.state_DIE,
  602. digest: self.state_DIGEST,
  603. }
  604. if(action === stateName)return
  605. self.state= dictionary[action]
  606. }
  607. }

 游戏引擎

  1. class Game {
  2. constructor (){
  3. let g = {
  4. actions: {}, // 注册按键操作
  5. keydowns: {}, // 按键事件对象
  6. cardSunVal: null, // 当前选中植物卡片index以及需消耗阳光值
  7. cardSection: '', // 绘制随鼠标移动植物类别
  8. canDrawMousePlant: false, // 能否绘制随鼠标移动植物
  9. canLayUp: false, // 能否放置植物
  10. mousePlant: null, // 鼠标绘制植物对象
  11. mouseX: 0, // 鼠标 x 轴坐标
  12. mouseY: 0, // 鼠标 y 轴坐标
  13. mouseRow: 0, // 鼠标移动至可种植植物区域的行坐标
  14. mouseCol: 0, // 鼠标移动至可种植植物区域的列坐标
  15. state: 0, // 游戏状态值,初始默认为 0
  16. state_LOADING: 0, // 准备阶段
  17. state_START: 1, // 开始游戏
  18. state_RUNNING: 2, // 游戏开始运行
  19. state_STOP: 3, // 暂停游戏
  20. state_PLANTWON: 4, // 游戏结束,玩家胜利
  21. state_ZOMBIEWON: 5, // 游戏结束,僵尸胜利
  22. canvas: document.getElementById("canvas"), // canvas元素
  23. context: document.getElementById("canvas").getContext("2d"), // canvas画布
  24. timer: null, // 轮询定时器
  25. fps: window._main.fps, // 动画帧数
  26. }
  27. Object.assign(this,g)
  28. }
  29. static new(){
  30. let g=new this()
  31. g.init()
  32. return g
  33. }
  34. // clearGameTimer(){
  35. // let g=this
  36. // clearInterval(g.timer)
  37. // }
  38. drawBg(){
  39. let g=this,cxt=g.context,sunnum=window._main.sunnum,cards=window._main.cards,img=imageFromPath(allImg.bg)
  40. cxt.drawImage(img,0, 0)
  41. sunnum.draw(cxt)
  42. }
  43. drawCars(){
  44. let g=this,cxt=g.context,cars=window._main.cars
  45. cars.forEach((car,idx)=>{
  46. if(car.x>950){
  47. cars.splice(idx,1)
  48. }
  49. car.draw(g,cxt)
  50. })
  51. }
  52. drawCards(){
  53. let g=this,cxt=g.context,cards=window._main.cards
  54. for(let card ofcards){
  55. card.draw(cxt)
  56. }
  57. }
  58. drawPlantWon(){
  59. let g=this,cxt=g.context,text='恭喜玩家获得胜利!'
  60. cxt.fillStyle='red'
  61. cxt.font='48px Microsoft YaHei'
  62. cxt.fillText(text,354, 300)
  63. }
  64. drawZombieWon(){
  65. let g=this,cxt=g.context,img=imageFromPath(allImg.zombieWon)
  66. cxt.drawImage(img,293, 66)
  67. }
  68. drawLoading(){
  69. let g=this,cxt=g.context,img=imageFromPath(allImg.startBg)
  70. cxt.drawImage(img,119, 0)
  71. }
  72. drawStartAnime(){
  73. let g=this,stateName='write',loading=window._main.loading,cxt=g.context,canvas_w=g.canvas.width,canvas_h=g.canvas.height,
  74. animateLen=allImg.loading[stateName].len
  75. if(loading.imgIdx!=animateLen){
  76. loading.count+=1
  77. }
  78. loading.imgIdx=Math.floor(loading.count/loading.fps)
  79. if(loading.imgIdx==animateLen){
  80. loading.img=loading.images[loading.imgIdx-1]
  81. }else{
  82. loading.img=loading.images[loading.imgIdx]
  83. }
  84. cxt.drawImage(loading.img, 437, 246)
  85. }
  86. drawBullets(plants){
  87. let g=this,context = g.context, canvas_w = g.canvas.width - 440
  88. for(let item ofplants){
  89. item.bullets.forEach((bullet,idx,arr)=>{
  90. bullet.draw(g,context)
  91. if(bullet.x>=canvas_w){
  92. arr.splice(idx,1)
  93. }
  94. })
  95. }
  96. }
  97. drawBlood (role) {
  98. let g = this,cxt = g.context,x = role.x,y = role.y
  99. cxt.fillStyle = 'red'
  100. cxt.font = '18px Microsoft YaHei'
  101. if(role.type === 'plant'){
  102. cxt.fillText(role.life, x + 30, y - 10)
  103. }else if(role.type === 'zombie') {
  104. cxt.fillText(role.life, x + 85, y + 10)
  105. }
  106. }
  107. updateImage(plants,zombies){
  108. let g = this,cxt = g.context
  109. plants.forEach((plant, idx)=>{ plant.canAttack()
  110. plant.update(g)
  111. })
  112. zombies.forEach((zombie, idx)=>{
  113. if(zombie.x < 50){
  114. g.state = g.state_ZOMBIEWON
  115. }
  116. zombie.canAttack()
  117. zombie.update(g)
  118. })
  119. }
  120. drawImage (plants, zombies){
  121. let g = this,cxt = g.context, delPlantsArr = []
  122. plants.forEach((plant, idx, arr)=>{
  123. if(plant.isDel){
  124. delPlantsArr.push(plant)
  125. arr.splice(idx,1)
  126. }else{
  127. plant.draw(cxt)
  128. // g.drawBlood(plant)
  129. }
  130. })
  131. zombies.forEach(function (zombie, idx) {
  132. if(zombie.isDel){
  133. zombies.splice(idx, 1)
  134. if(zombies.length === 0) {
  135. g.state = g.state_PLANTWON
  136. }
  137. }else{
  138. zombie.draw(cxt)
  139. // g.drawBlood(zombie)
  140. }
  141. for(let plant ofdelPlantsArr) {
  142. if(zombie.attackPlantID=== plant.id) {
  143. zombie.canMove = true
  144. if(zombie.life > 2){
  145. zombie.changeAnimation('run')
  146. }
  147. }
  148. }
  149. })
  150. }
  151. getMousePos(){
  152. let g = this,_main=window._main,cxt=g.context,cards=_main.cards,x=g.mouseX,y=g.mouseY
  153. if(g.canDrawMousePlant){
  154. g.mousePlantCallback(x,y)
  155. }
  156. }
  157. drawMousePlant(plant_info){
  158. let g = this,cxt = g.context,plant =null
  159. letmousePlant_info={
  160. type:'plant',
  161. section:g.cardSection,
  162. x: g.mouseX + 82,
  163. y: g.mouseY - 40,
  164. row: g.mouseRow,
  165. col: g.mouseCol,
  166. }
  167. if(g.canLayUp){
  168. plant=Plant.new(plant_info)
  169. plant.isHurt=true
  170. plant.update(g)
  171. plant.draw(cxt)
  172. }
  173. g.mousePlant = Plant.new(mousePlant_info)
  174. g.mousePlant.update(g)
  175. g.mousePlant.draw(cxt)
  176. }
  177. mousePlantCallback(x,y){
  178. let g = this,_main =window._main,cxt = g.context, row =Math.floor((y - 75) / 100) + 1, col =Math.floor((x - 175) / 80) + 1
  179. letplant_info={
  180. type:'plant' ,
  181. section: g.cardSection,
  182. x: _main.plants_info.x + 80* (col -1),
  183. y: _main.plants_info.y + 100* (row -1),
  184. row: row,
  185. col: col,
  186. }
  187. g.mouseRow = row
  188. g.mouseCol = col
  189. if(row>=1&&row<=5&&col>=1&&col<=9){
  190. g.canLayUp=true
  191. for(let plant of_main.plants){
  192. if(row==plant.row&&col==plant.col){
  193. g.canLayUp=false
  194. }
  195. }
  196. }else{
  197. g.canLayUp=false
  198. }
  199. if(g.canDrawMousePlant){
  200. g.drawMousePlant(plant_info)
  201. }
  202. }
  203. registerAction (key, callback) {
  204. this.actions[key] = callback
  205. }
  206. setTimer(_main) {
  207. let g = this,plants = _main.plants,zombies = _main.zombies
  208. letactions =Object.keys(g.actions)
  209. for (let i = 0; i < actions.length; i++) {
  210. letkey = actions[i]
  211. if (g.keydowns[key]) {
  212. g.actions[key]()
  213. }
  214. }
  215. g.context.clearRect(0, 0, g.canvas.width, g.canvas.height)
  216. if (g.state=== g.state_LOADING) {
  217. g.drawLoading()
  218. } else if (g.state=== g.state_START) {
  219. g.drawBg()
  220. g.drawCars()
  221. g.drawCards()
  222. g.drawStartAnime()
  223. } else if (g.state=== g.state_RUNNING) {
  224. g.drawBg()
  225. g.updateImage(plants, zombies)
  226. g.drawImage(plants, zombies)
  227. g.drawCars()
  228. g.drawCards()
  229. g.drawBullets(plants)
  230. g.getMousePos()
  231. } else if (g.state=== g.state_STOP) {
  232. g.drawBg()
  233. g.updateImage(plants, zombies)
  234. g.drawImage(plants, zombies)
  235. g.drawCars()
  236. g.drawCards()
  237. g.drawBullets(plants)
  238. _main.clearTiemr()
  239. } else if (g.state=== g.state_PLANTWON) {
  240. g.drawBg()
  241. g.drawCars()
  242. g.drawCards()
  243. g.drawPlantWon()
  244. _main.clearTiemr()
  245. } else if (g.state=== g.state_ZOMBIEWON) {
  246. g.drawBg()
  247. g.drawCars()
  248. g.drawCards()
  249. g.drawZombieWon()
  250. _main.clearTiemr()
  251. }
  252. }
  253. //========================================================================
  254. init(){
  255. let g=this,_main=window._main
  256. // window.addEventListener('keydown', function (event) {
  257. // g.keydowns[event.keyCode] = 'down'
  258. // })
  259. // window.addEventListener('keyup', function (event) {
  260. // g.keydowns[event.keyCode] = 'up'
  261. // })
  262. g.registerAction = function (key, callback) {
  263. g.actions[key] = callback
  264. }
  265. g.timer = setInterval(function () {
  266. g.setTimer(_main)
  267. }, 1000/g.fps)
  268. document.getElementById('canvas').onmousemove = function (event) {
  269. lete = event ||window.event,
  270. scrollX =document.documentElement.scrollLeft || document.body.scrollLeft,
  271. scrollY =document.documentElement.scrollTop || document.body.scrollTop,
  272. x = e.pageX || e.clientX+ scrollX,
  273. y = e.pageY || e.clientY+ scrollY
  274. g.mouseX = x
  275. g.mouseY = y
  276. }
  277. document.getElementById('js-startGame-btn').onclick = function () {
  278. g.state = g.state_START
  279. setTimeout(function () {
  280. g.state = g.state_RUNNING
  281. document.getElementById('pauseGame').className += ' show'
  282. document.getElementById('restartGame').className += ' show'
  283. _main.clearTiemr()
  284. _main.setTimer()
  285. }, 2500)
  286. document.getElementsByClassName('cards-list')[0].className += ' show'
  287. document.getElementsByClassName('menu-box')[0].className += ' show'
  288. document.getElementById('js-startGame-btn').style.display = 'none'
  289. document.getElementById('js-intro-game').style.display = 'none'
  290. document.getElementById('js-log-btn').style.display = 'none'
  291. }
  292. document.querySelectorAll('.cards-item').forEach(function (card, idx) {
  293. card.onclick = function () {
  294. letplant =null,cards = _main.cards
  295. if(cards[idx].canClick) {
  296. g.cardSection = this.dataset.section
  297. g.canDrawMousePlant = true
  298. g.cardSunVal = {
  299. idx: idx,
  300. val: cards[idx].sun_val,
  301. }
  302. }
  303. }
  304. })
  305. document.getElementById('canvas').onclick = function (event) {
  306. letplant =null,cards = _main.cards,x = g.mouseX,y = g.mouseY,
  307. plant_info = {
  308. type: 'plant',
  309. section: g.cardSection,
  310. x: _main.plants_info.x + 80 * (g.mouseCol - 1),
  311. y: _main.plants_info.y + 100 * (g.mouseRow - 1),
  312. row: g.mouseRow,
  313. col: g.mouseCol,
  314. canSetTimer: g.cardSection === 'sunflower' ? true : false,
  315. }
  316. for (let item of_main.plants){
  317. if(g.mouseRow=== item.row&& g.mouseCol=== item.col) {
  318. g.canLayUp = false
  319. g.mousePlant = null
  320. }
  321. }
  322. if (g.canLayUp&& g.canDrawMousePlant) {
  323. letcardSunVal = g.cardSunVal
  324. if(cardSunVal.val<= _main.allSunVal) {
  325. cards[cardSunVal.idx].canClick = false
  326. cards[cardSunVal.idx].changeState()
  327. cards[cardSunVal.idx].drawCountDown()
  328. plant =Plant.new(plant_info)
  329. _main.plants.push(plant)
  330. _main.sunnum.changeSunNum(-cardSunVal.val)
  331. g.canDrawMousePlant = false
  332. } else {
  333. g.canDrawMousePlant = false
  334. g.mousePlant = null
  335. }
  336. } else {
  337. g.canDrawMousePlant = false
  338. g.mousePlant = null
  339. }
  340. }
  341. document.getElementById('pauseGame').onclick = function (event) {
  342. g.state = g.state_STOP
  343. }
  344. document.getElementById('restartGame').onclick = function (event) {
  345. if (g.state=== g.state_LOADING) {
  346. g.state = g.state_START
  347. }else{
  348. g.state = g.state_RUNNING
  349. for (let plant of_main.plants) {
  350. if(plant.section === 'sunflower') {
  351. plant.setSunTimer()
  352. }
  353. }
  354. }
  355. _main.setTimer()
  356. }
  357. }
  358. }

 主程序入口

  1. class Main{
  2. constructor(){
  3. let m={
  4. allSunVal:200, // 阳光总数量
  5. loading:null, // loading 动画对象
  6. sunnum:null, // 阳光实例对象
  7. cars:[], // 实例化除草车对象数组
  8. cars_info:{ // 初始化参数
  9. x:170, // x 轴坐标
  10. y:102, // y 轴坐标
  11. position:[
  12. {row:1},
  13. {row:2},
  14. {row:3},
  15. {row:4},
  16. {row:5},
  17. ],
  18. },
  19. cards:[],
  20. cards_info:{
  21. x:0,
  22. y:0,
  23. position:[
  24. {name: 'sunflower', row: 1, sun_val: 50, timer_spacing: 5 * 1000},
  25. {name: 'wallnut', row: 2, sun_val: 50, timer_spacing: 12 * 1000},
  26. {name: 'peashooter', row: 3, sun_val: 100, timer_spacing: 7 * 1000},
  27. {name: 'repeater', row: 4, sun_val: 150, timer_spacing: 10 * 1000},
  28. {name: 'gatlingpea', row: 5, sun_val: 200, timer_spacing: 15 * 1000},
  29. {name: 'chomper', row: 6, sun_val: 200, timer_spacing: 15 * 1000},
  30. {name: 'cherrybomb', row: 7, sun_val: 250, timer_spacing: 25 * 1000},
  31. ]
  32. },
  33. plants:[],
  34. zombies:[],
  35. plants_info:{
  36. type:'plant',
  37. x:250,
  38. y:92,
  39. position:[]
  40. },
  41. zombies_info:{
  42. type:'zombie',
  43. x:170,
  44. y:15,
  45. position:[]
  46. },
  47. zombies_idx: 0,
  48. zombies_row: 0,
  49. zombies_iMax: 50,
  50. sunTimer: null,
  51. sunTimer_difference: 20,
  52. zombieTimer: null,
  53. zombieTimer_difference: 12,
  54. game: null,
  55. fps: 60,
  56. }
  57. Object.assign(this,m)
  58. }
  59. setZombiesInfo () {
  60. letself =this,
  61. iMax = self.zombies_iMax
  62. for(let i = 0; i < iMax; i++) {
  63. let row = Math.ceil(Math.random() * 4 + 1)
  64. self.zombies_info.position.push({
  65. section: 'zombie',
  66. row: row,
  67. col: 11 + Number(Math.random().toFixed(1))
  68. })
  69. }
  70. }
  71. clearTiemr(){
  72. let self=this
  73. clearInterval(self.sunTimer)
  74. clearInterval(self.zombieTimer)
  75. for(let plant of self.plants){
  76. if(plant.section=='sunflower'){
  77. plant.clearSunTimer()
  78. }
  79. }
  80. }
  81. // 设置全局阳光、僵尸生成定时器
  82. setTimer(){
  83. let self=this,zombies=self.zombies
  84. self.sunTimer = setInterval(function () {
  85. letleft =parseInt(window.getComputedStyle(document.getElementsByClassName('systemSun')[0],null).left), // 获取当前元素left值
  86. top = '-100px',
  87. keyframes1 = [
  88. { transform: 'translate(0,0)', opacity: 0 },
  89. { offset: .5, transform: 'translate(0,300px)', opacity: 1 },
  90. { offset: .75, transform: 'translate(0,300px)', opacity: 1 },
  91. { offset: 1, transform: 'translate(-'+ (left -110) +'px,50px)', opacity: 0 }
  92. ]
  93. document.getElementsByClassName('systemSun')[0].animate(keyframes1,keyframesOptions)
  94. setTimeout(function () {
  95. self.sunnum.changeSunNum()
  96. document.getElementsByClassName('systemSun')[0].style.left = Math.floor(Math.random() * 200 + 300) + 'px'
  97. document.getElementsByClassName('systemSun')[0].style.top = '-100px'
  98. }, 2700)
  99. }, 1000* self.sunTimer_difference)
  100. self.zombieTimer = setInterval(function () {
  101. letidx = self.zombies_iMax- self.zombies_idx - 1
  102. if(self.zombies_idx=== self.zombies_iMax) { // 僵尸生成数量达到最大值,清除定时器
  103. return clearInterval(self.zombieTimer)
  104. }
  105. if(self.zombies[idx]) {
  106. self.zombies[idx].state= self.zombies[idx].state_RUN
  107. }
  108. self.zombies_idx++
  109. },1000* self.zombieTimer_difference)
  110. }
  111. setCars(cars_info){
  112. let self=this
  113. for(let car ofcars_info.position){
  114. letinfo={
  115. x: cars_info.x,
  116. y: cars_info.y + 100* (car.row - 1),
  117. row: car.row,
  118. }
  119. self.cars.push(Car.new(info))
  120. }
  121. }
  122. setCards(cards_info){
  123. let self=this
  124. for (let card ofcards_info.position) {
  125. letinfo={
  126. name:card.name,
  127. row:card.row,
  128. sun_val:card.sun_val,
  129. timer_spacing: card.timer_spacing,
  130. y: cards_info.y + 60* (card.row - 1),
  131. }
  132. self.cards.push(Card.new(info))
  133. }
  134. }
  135. //palnt or zombie
  136. setRoles(roles_info){
  137. let self=this,type = roles_info.type
  138. for (let role ofroles_info.position){
  139. letinfo = {
  140. type: roles_info.type,
  141. section: role.section,
  142. x: roles_info.x + 80* (role.col - 1),
  143. y: roles_info.y + 100* (role.row - 1),
  144. col: role.col,
  145. row: role.row,
  146. }
  147. if(type==='plant'){
  148. self.plants.push(Plant.new(info))
  149. }else if(type==='zombie'){
  150. self.zombies.push(Zombie.new(info))
  151. }
  152. }
  153. }
  154. //===========================================
  155. start(){
  156. let self=this
  157. self.loading = Animation.new({type: 'loading'}, 'write', 55)
  158. self.sunnum = SunNum.new()
  159. self.setZombiesInfo()
  160. self.setCars(self.cars_info)
  161. self.setCards(self.cards_info)
  162. self.setRoles(self.plants_info)
  163. self.setRoles(self.zombies_info)
  164. self.game = Game.new()
  165. }
  166. }
  167. window._main=new Main()
  168. window._main.start()

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

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

 

posted on 2025-06-03 08:04  ljbguanli  阅读(56)  评论(0)    收藏  举报