
ゲーム内部の処理が大体出来上がってきたので、今回はステージクリアの処理を追加したいと思います。
今回の目的
- ステージクリア(エサを全部食べた時)に次のステージに進む
- クリア時に、敵キャラの動きを止めて非表示にする
- クリア時に、自キャラの動きを止める
- ステージ切り替えに必要な各種データのリセット処理
対象ファイル一覧
  - index.html
- assets/ghost.json
- css/frame.css
- css/ghost.css
- css/pacman.css
- js/control.js
- js/feed.js
- js/frame.js
- js/ghost.js
- js/main.js
- js/pacman.js
軽い気持ちで作業をしてみたところ、結構大本の箇所の修正なども行う必要があったので、ほとんどのソース修正が必要になりました。
ソースコード
[全部更新] index.html
自キャラのエレメントを、動的に作成するようにしました。
<!DOCTYPE html>
<html lang='ja'>
<head>
  <link rel='stylesheet' href='css/style.css' />
  <script type='module' src='js/main.js'></script>
  <style>body{background-color:black}</style>
</head>
<body>
  <div class='frame-area' data-power="0"></div>
</body>
</html>
[新規追加] assets/ghost.json
 敵キャラの設定データをjsonファイル化しました。
[
  {
    "id"        : 1,
    "direction" : null,
    "coodinate" : { "x" : 12, "y" : 11 }
  },
  {
    "id"        : 2,
    "direction" : null,
    "coodinate" : { "x" : 15, "y" : 11 }
  },
  {
    "id"        : 3,
    "direction" : null,
    "coodinate" : { "x" : 12, "y" : 14 }
  },
  {
    "id"        : 4,
    "direction" : null,
    "coodinate" : { "x" : 15, "y" : 14 }
  }
]
[最後に追加] css/frame.css
ステージクリアした時の、ステージ枠の色アニメーションをcssで追加しました。
.frame-area[data-status='clear']{
  animation-name : stage-clear;
  animation-duration:2s;
  animation-timing-function: linear;
  animation-iteration-count: 1;
}
@keyframes stage-clear{
  0%{
    --color:blue;
  }
  10%{
    --color:white;
  }
  20%{
    --color:blue;
  }
  30%{
    --color:white;
  }
  40%{
    --color:blue;
  }
  50%{
    --color:white;
  }
  60%{
    --color:blue;
  }
  70%{
    --color:white;
  }
  80%{
    --color:blue;
  }
  90%{
    --color:white;
  }
  100%{
    --color:blue;
  }
}
[一行削除] css/ghost.css
不要な処理を1行だけ削除しました。
赤色の行を消してください。
.ghost{
  position:absolute;
  z-index:9;
  display:inline-block;
  --size-width : calc(var(--size-chara) * 1.0);
  --size-eye   : calc(var(--size-width) * 0.3);
  --size-under : calc(var(--size-width) * 0.2);
  --color-body : red;
  width:var(--size-chara);
  height:var(--size-chara);
  transform:translate(calc(var(--block) / -2) , calc(var(--block) / -2));
}
.ghost[data-color='1']{--color-body : red;}
.ghost[data-color='2']{--color-body : orange;}
.ghost[data-color='3']{--color-body : lightblue;}
.ghost[data-color='4']{--color-body : pink;}
.ghost[data-status='weak']{--color-body : blue;}
/* .frame-area .ghost[data-status='1']{--color-body : blue;} */
.ghost > *{
  margin : 0 auto;
  width  : var(--size-width);
}
.ghost > .head{
  height : calc(var(--size-width) / 2);
  background-color : var(--color-body);
  border-radius    : var(--size-chara) var(--size-chara) 0 0;
  position:relative;
}
.ghost > .head .eyes{
  position:absolute;
  width:100%;
  bottom:0;
  z-index:10;
}
.ghost > .head .eyes *{
  position:absolute;
  bottom:0%;
  width  : var(--size-eye);
  height : var(--size-eye);
  background-color:white;
  border-radius:50%;
}
.ghost > .head .eyes .eye-left{
  left:13%;
}
.ghost > .head .eyes .eye-right{
  right:13%;
}
.ghost > .head .eyes *::before{
  content:'';
  display:block;
  background-color:black;
  width  : 50%;
  height : 50%;
  margin : 25%;
  border-radius:50%;
  transition-property : margin;
  transition-duration : 0.3s;
}
.ghost > .head[data-direction='right'] .eyes{
  left:12%;
}
.ghost > .head[data-direction='left'] .eyes{
  left:-12%;
}
.ghost > .head[data-direction='up'] .eyes{
  bottom:20%;
}
.ghost > .head[data-direction='down'] .eyes{
  bottom:-20%;
}
.ghost[data-status='weak'] > .head .eyes{
  left:0;
  bottom:20%;
}
.ghost > .head[data-direction='right'] .eyes > *::before{
  margin-left  : 50%;
  margin-right : 0;
}
.ghost > .head[data-direction='left'] .eyes > *::before{
  margin-left  : 0;
  margin-right : 50%;
}
.ghost > .head[data-direction='up'] .eyes > *::before{
  margin-top    : 0;
  margin-bottom : 50%;
}
.ghost > .head[data-direction='down'] .eyes > *::before{
  margin-top    : 50%;
  margin-bottom : 0;
}
.ghost[data-status='weak'] > .head .eyes *::before{
  display:none;
}
.ghost[data-status='weak'] > .head .eyes .eye-left,
.ghost[data-status='weak'] > .head .eyes .eye-right{
  width  : calc(var(--size-eye) / 2);
  height : calc(var(--size-eye) / 2);
  background-color:white;
  border-radius:50%;
}
.ghost[data-status='weak'] > .head .eyes .eye-left{
  left:25%;
}
.ghost[data-status='weak'] > .head .eyes .eye-right{
  right:25%;
}
.ghost > .mouse{
  height : 30%;
  background-color : var(--color-body);
  position:relative;
}
.frame-area .ghost > .mouse > svg{
  display:none;
}
.ghost[data-status='weak'] > .mouse > svg{
  position:absolute;
  top:-60%;
  display:block;
  margin-top:10px;
}
.ghost > .under{
  height : var(--size-under);
}
.ghost > .under path{
  fill:var(--color-body);
}
.ghost[data-status='dead']{
  --color-body : blue;
}
.ghost[data-status='dead']{
  --color-body : transparent;
}
/* power-limit-soon */
[data-power='2'] .ghost[data-status='weak']{
  animation-name : power_soon_body;
  animation-duration:0.4s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
[data-power='2'] .ghost[data-status='weak'] > .head .eyes .eye-left,
[data-power='2'] .ghost[data-status='weak'] > .head .eyes .eye-right{
  animation-name : power_soon_eyes;
  animation-duration:0.4s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
[data-power='2'] .ghost[data-status='weak'] > .mouse svg path{
  animation-name : power_soon_mouse;
  animation-duration:0.4s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
@keyframes power_soon_body{
  0%{
    --color-body:blue;
  }
  50%{
    --color-body:white;
  }
  100%{
    --color-body:blue;
  }
}
@keyframes power_soon_eyes{
  0%{
    background-color:white;
  }
  50%{
    background-color:red;
  }
  100%{
    background-color:white;
  }
}
@keyframes power_soon_mouse{
  0%{
    stroke:white;
  }
  50%{
    stroke:red;
  }
  100%{
    stroke:white;
  }
}
[一部追加] css/pacman.css
クリアした時のcss処理を追加しました。
赤字の箇所のみ更新してもらうだけで大丈夫です。
.pacman{
  position:absolute;
  z-index:10;
  display:inline-block;
  width:var(--size-chara);
  height:var(--size-chara);
}
.frame-area[data-status='clear'] .pacman::before,
.frame-area[data-status='clear'] .pacman::after,
.pacman[data-status='crashed']::before,
.pacman[data-status='crashed']::after{
  animation-play-state: paused;
}
.pacman::before,
.pacman::after{
  content:'';
  display:block;
  width:100%;
  height:50%;
  background-color:yellow;
  animation-duration:var(--anim-speed);
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.pacman::before{
  transform-origin:center bottom;
  border-radius:var(--size-chara) var(--size-chara) 0 0;
  transform:rotate(45deg);
}
.pacman::after{
  transform-origin:center top;
  border-radius:0 0 var(--size-chara) var(--size-chara);
  transform:rotate(-45deg);
}
.pacman[data-anim="true"]::before{
  animation-name : pacman_before;
}
.pacman[data-anim="true"]::after{
  animation-name : pacman_after;
}
@keyframes pacman_before{
  0%{
    transform:rotate(45deg);
  }
  50%{
    transform:rotate(0deg);
  }
  100%{
    transform:rotate(45deg);
  }
}
@keyframes pacman_after{
  0%{
    transform:rotate(-45deg);
  }
  50%{
    transform:rotate(0deg);
  }
  100%{
    transform:rotate(-45deg);
  }
}
.pacman,
.pacman[data-direction="left"]{
  transform:translate(calc(var(--block) / -2) , calc(var(--block) / -2));
}
.pacman[data-direction="right"]{
  transform:translate(calc(var(--block) / -2) , calc(var(--block) / -2)) scaleX(-1);
}
.pacman[data-direction="up"]{
  transform:translate(calc(var(--block) / -2) , calc(var(--block) / -2)) rotate(90deg);
}
.pacman[data-direction="down"]{
  transform:translate(calc(var(--block) / -2) , calc(var(--block) / -2)) rotate(-90deg);
}
.pacman[data-status="dead"]::before,
.pacman[data-status="dead"]::after{
  transform:rotate(0deg);
  animation-delay : 0.0s;
  animation-direction : normal;
  animation-duration : 2.0s;
  animation-fill-mode: forwards;
  animation-iteration-count: 1;
  animation-timing-function: linear;
}
.pacman[data-status="dead"]::before{
  animation-name : pacman-dead-before;
}
.pacman[data-status="dead"]::after{
  animation-name : pacman-dead-after;
}
@keyframes pacman-dead-before{
  0%{
    clip-path: polygon(
       50% 100%, 
        0% 100%, 
        0% 0%,
      100% 0%,
      100% 100%
    );
  }
  25%{
    clip-path: polygon(
      50% 100%, 
      0% 0%, 
      0% 0%, 
      100% 0%,
      100% 100%
    );
  }
  75%{
    clip-path: polygon(
       50% 100%, 
      100% 0%, 
      100% 0%,
      100% 0%,
      100% 100%
    );
  }
  
  100%{
    clip-path: polygon(
      50% 100%, 
      100% 100%, 
      100% 100%, 
      100% 100%, 
      100% 100% 
    );
  }
}
@keyframes pacman-dead-after{
  0%{
    clip-path: polygon(
       50% 0%, 
        0% 0%, 
        0% 100%,
      100% 100%,
      100% 0%
    );
  }
  25%{
    clip-path: polygon(
      50% 0%, 
      0% 100%, 
      0% 100%, 
      100% 100%,
      100% 0%
    );
  }
  75%{
    clip-path: polygon(
       50% 0%, 
      100% 100%, 
      100% 100%,
      100% 100%,
      100% 0%
    );
  }
  
  100%{
    clip-path: polygon(
      50% 0%, 
      100% 0%, 
      100% 0%, 
      100% 0%, 
      100% 0% 
    );
  }
}
[全部更新] js/control.js
全体的に、private処理をstatic処理に切り替えました。
import { Pacman } from './pacman.js'
import { Frame }  from './frame.js'
export class Control{
  constructor(){
    window.addEventListener('keydown' , Control.keydown.bind(this))
    window.addEventListener('keyup'   , Control.keyup.bind(this))
  }
  static key2name(key){
    switch(key){
      case 37 : return 'left'
      case 38 : return 'up'
      case 39 : return 'right'
      case 40 : return 'down'
    }
  }
  static keydown(e){
    if(e.repeat === true){return}
    if(Frame.is_clear){return}
    const key  = e.keyCode
    
    const name = Control.key2name(key)
    if(!name){return}
    Control.direction = name
    Pacman.move(Control.direction)
  }
  static keyup(e){
    if(!Control.direction){return}
    const name = Control.key2name(e.keyCode)
    Control.clear()
  }
  static clear(){
    if(Control.direction !== name){return}
    delete Control.direction
  }
}
  
[全部更新] js/feed.js
エサの残り数を計算する処理を全体的に追加しています。
ファイル内を全て入れ替えてください。
import { Frame }     from './frame.js'
import { Pacman }    from './pacman.js'
import { Ghost }     from './ghost.js'
export class Feed{
  constructor(){
    // Feed.number_of_bites = this.elm_number_of_bites
    Feed.reset_data()
  }
  static get elm_number_of_bites(){
    const elms = document.querySelectorAll(`.frame-area .P1,.frame-area .P2`)
    return elms.length
  }
  static reset_data(){
    Feed.number_of_bites = Feed.elm_number_of_bites
  }
  
  static move_map(){
    const num  = Frame.get_pos2num(Pacman.coodinates)
    const item = Frame.frame_datas[num]
    switch(item){
      case 'P1':
        this.eat_normal_dot(num)
        Feed.number_of_bites--
        break
      case 'P2':
        Feed.power_on()
        Feed.flg_soon = setTimeout(Feed.power_soon , 7000)
        Feed.flg_off  = setTimeout(Feed.power_off  , 10000)
        this.eat_big_dot(num)
        Feed.number_of_bites--
        break
    }
    if(Feed.number_of_bites <= 0){
      Frame.stage_clear()
    }
  }
  
  static eat_normal_dot(num){
    Frame.frame_datas[num] = 'S5'
    const elm = Frame.get_elm(num)
    if(!elm){return}
    
    elm.setAttribute('class','S5')
  }
  static eat_big_dot(num){
    Frame.frame_datas[num] = 'S5'
    const elm = Frame.get_elm(num)
    if(!elm){return}
    
    elm.setAttribute('class','S5')
  }
  static power_on(){
    Frame.root.setAttribute('data-power' , '1')
    Ghost.power_on()
    if(Feed.flg_soon){
      clearTimeout(Feed.flg_soon)
    }
    if(Feed.flg_off){
      clearTimeout(Feed.flg_off)
    }
  }
  static power_soon(){
    Frame.root.setAttribute('data-power' , '2')
  }
  static power_off(){
    Frame.root.setAttribute('data-power' , '0')
    Ghost.power_off()
    delete Feed.flg_soon
    delete Feed.flg_off
  }
}
  
[全部更新] js/frame.js
ステージ情報のモジュールなので、大幅に変更をしました。
import { Main }    from './main.js'
import { Pacman }  from './pacman.js'
import { Ghost }   from './ghost.js'
import { Feed }    from './feed.js'
import { Control } from './control.js'
export class Frame{
  constructor(){
    return new Promise(resolve => {
      this.resolve = resolve
      Frame.stage_datas = this.stage_datas = []
      this.load_asset()
    })
  }
  static get root(){
    return document.querySelector(`.frame-area`)
  }
  get block_size(){
    return Frame.block_size
  }
  static get block_size(){
    const s5 = document.querySelector('.S5')
    return s5.offsetWidth
  }
  static get cols_count(){
    return ~~(Frame.root.offsetWidth / Frame.block_size)
  }
  static get_elm(num){
    return Frame.root.querySelector(`[data-num='${num}']`)
  }
  static get is_weak(){
    const power = Frame.root.getAttribute('data-power')
    switch(power){
      case '1':
      case '2':
        return true
      default:
        return false
    }
  }
  static get is_clear(){
    return Frame.root.getAttribute('data-status') === 'clear' ? true : false
  }
  load_asset(){
    const xhr = new XMLHttpRequest()
    xhr.open('get' , `assets/frame.json` , true)
    xhr.setRequestHeader('Content-Type', 'text/html');
    xhr.onreadystatechange = ((e) => {
      if(xhr.readyState !== XMLHttpRequest.DONE){return}
      if(xhr.status === 404){return}
      if (xhr.status === 200) {
        // Frame.frame_datas = this.frame_datas = JSON.parse(e.target.response)
        // this.view()
        // this.set_collision()
        // this.finish()
        // this.set_ghost_start_area()
        Frame.asset_json  = e.target.response
        Frame.start()
        this.finish()
      }
    }).bind(this)
    xhr.send()
  }
  static start(){
    Frame.frame_datas = JSON.parse(Frame.asset_json)
    Frame.view()
    Frame.set_collision()
    Frame.set_ghost_start_area()
  }
  static crear(){
    Frame.root.innerHTML = ''
  }
  static view(){
    for(let i=0; i<Frame.frame_datas.length; i++){
        const p = document.createElement('p')
        p.className = Frame.frame_datas[i]
        Frame.root.appendChild(p)
        p.setAttribute('data-num' , i)
    }
  }
  finish(){
    if(this.resolve){
      this.resolve(this)
    }
  }
  static put(elm , coodinates){
    if(!elm){return}
    const pos = this.calc_coodinates2position(coodinates)
    this.pos(elm , pos)
    elm.setAttribute('data-x' , coodinates.x)
    elm.setAttribute('data-y' , coodinates.y)
  }
  static calc_coodinates2position(coodinates){
    const size = Frame.block_size
    return {
      x : (coodinates.x) * size,
      y : (coodinates.y) * size,
    }
  }
  static pos(elm , pos){
    elm.style.setProperty('left' , `${pos.x}px` , '')
    elm.style.setProperty('top'  , `${pos.y}px` , '')
  }
  // 壁座標に1を設置
  static set_collision(type){
    const cols_count = Frame.cols_count
    const maps = []
    let row_count = 0
    for(const frame_data of Frame.frame_datas){
      maps[row_count] = maps[row_count] || []
      // 移動できる
      if(frame_data.match(/^P/i)
      || frame_data.toUpperCase() === 'S5'
      || frame_data.match(/^W/i)
      || frame_data.match(/^T/i)){
        maps[row_count].push(0)
      }
      // 壁
      else{
        maps[row_count].push(1)
      }
      if(maps[row_count].length === cols_count){
        row_count++
      }
    }
    Frame.map = this.map = maps
  }
  static is_collision(map){
    if(!map || !Frame.map || !Frame.map[map.y]){return}
    return Frame.map[map.y][map.x]
  }
  // type @ [pacman , ghost]
  static is_through(map , direction, status){
    const through_item = Frame.frame_datas[Frame.get_pos2num(map)]
    if(status === 'dead'){
      // return true
      if(through_item === 'TU' && direction === 'up'
      || through_item === 'TD' && direction === 'down'
      || through_item === 'TL' && direction === 'left'
      || through_item === 'TR' && direction === 'right'){
        return false
      }
      else{
        return true
      }
    }
    else{
      if(through_item === 'TU' && direction !== 'up'
      || through_item === 'TD' && direction !== 'down'
      || through_item === 'TL' && direction !== 'left'
      || through_item === 'TR' && direction !== 'right'){
        return false
      }
      else{
        return true
      }
    }
  }
  static get_pos2num(pos){
    return pos.y * Frame.map[0].length + pos.x
  }
  static get_num2pos(num){
    return {
      x : num % Frame.map[0].length,
      y : ~~(num / Frame.map[0].length),
    }
  }
  static is_warp(map){
    const num = Frame.get_pos2num(map)
    return Frame.frame_datas[num] === 'W1' ? true : false
  }
  static get_another_warp_pos(map){
    const warp_index_arr = Frame.filterIndex(Frame.frame_datas , 'W1')
    const current_index = Frame.get_pos2num(map)
    const another_num = warp_index_arr.find(e => e !== current_index)
    return Frame.get_num2pos(another_num)
  }
  static filterIndex(datas,target){
    const res_arr = []
    for(let i=0; i<datas.length; i++){
      if(datas[i] === target){
        res_arr.push(i)
      }
    }
    return res_arr
  }
  static next_pos(direction , pos){
    const temp_pos = {
      x : pos.x,
      y : pos.y,
    }
    switch(direction){
      case 'left':
        temp_pos.x -= 1
        break
      case 'right':
        temp_pos.x += 1
        break
      case 'up':
        temp_pos.y -= 1
        break
      case 'down':
        temp_pos.y += 1
        break
      default: return
    }
    return temp_pos
  }
  static set_ghost_start_area(){
    const ghost_start_area = []
    for(let i=0; i<Frame.frame_datas.length; i++){
      if(Frame.frame_datas[i] !== 'TU'
      && Frame.frame_datas[i] !== 'TD'
      && Frame.frame_datas[i] !== 'TL'
      && Frame.frame_datas[i] !== 'TR'){continue}
      const pos = Frame.get_num2pos(i)
      ghost_start_area.push({
        num : i,
        pos : pos,
      })
    }
    const pos = {
      x : ghost_start_area.map(e => e.pos.x).reduce((sum , e)=>{ return sum + e}) / ghost_start_area.length,
      y : ghost_start_area.map(e => e.pos.y).reduce((sum , e)=>{ return sum + e}) / ghost_start_area.length,
    }
    Frame.ghost_start_area = pos
  }
  static stage_clear(){
    // console.log('stage cleared !!')
    Main.is_clear = true
    // 画面の動きを止める
    Control.clear()
    Pacman.move_stop()
    Pacman.close_mouse()
    Ghost.move_stops()
    Ghost.hidden_all()
    setTimeout((()=>{
      Frame.root.setAttribute('data-status' , 'clear')
    }),500)
    setTimeout((()=>{
      Main.is_clear = false
      Frame.crear()
      Frame.root.setAttribute('data-status' , '')
      Frame.start()
      Feed.reset_data()
      Ghost.reset_data()
      Pacman.reset_data()
    }),4000)
  }
}  
[全部更新] js/ghost.js
モジュール内に書いていた、ghostのデータをjsonデータとして、別ファイルに移動して、ajax読み込みするようにしました。
import { Main }   from './main.js'
import { Frame }  from './frame.js'
import { Pacman } from './pacman.js'
export class Ghost{
  constructor(){
    this.load_data()
  }
  static reset_data(){
    Ghost.datas = JSON.parse(Ghost.data_json)
    Ghost.put_element()
  }
  static get elm_ghosts(){
    return document.querySelectorAll('.ghost')
  }
  static get_data(elm){
    const color_num = elm.getAttribute('data-color')
    return Ghost.datas.find(e => e.id === Number(color_num))
  }
  static get_id(elm){
    const data = Ghost.get_data(elm)
    return data.id
  }
  static get_coodinate(elm){
    const data = Ghost.get_data(elm)
    return data.coodinate
  }
  load_data(){
    const xhr = new XMLHttpRequest()
    xhr.open('get' , `assets/ghost.json` , true)
    xhr.setRequestHeader('Content-Type', 'text/json');
    xhr.onreadystatechange = ((e) => {
      if(xhr.readyState !== XMLHttpRequest.DONE){return}
      if(xhr.status === 404){return}
      if (xhr.status === 200) {
        Ghost.data_json = e.target.response
        Ghost.datas = JSON.parse(Ghost.data_json)
        Ghost.put_element()
      }
    })
    xhr.send()
  }
  static put_element(){
    for(const data of Ghost.datas){
      const elm = document.createElement('div')
      elm.className = 'ghost'
      elm.setAttribute('data-color' , data.id)
      Frame.root.appendChild(elm)
    }
    Ghost.load_asset()
  }
  static load_asset(){
    const xhr = new XMLHttpRequest()
    xhr.open('get' , `assets/ghost.html` , true)
    xhr.setRequestHeader('Content-Type', 'text/html');
    xhr.onreadystatechange = ((e) => {
      if(xhr.readyState !== XMLHttpRequest.DONE){return}
      if(xhr.status === 404){return}
      if (xhr.status === 200) {
        Ghost.asset = e.target.response
        Ghost.set_ghost()
        Ghost.set_move()
        // setTimeout(this.set_move.bind(this) , 300)
      }
    })
    xhr.send()
  }
  static set_ghost(){
    const elm_ghosts = Ghost.elm_ghosts
    for(const elm_ghost of elm_ghosts){
      const coodinate = Ghost.get_coodinate(elm_ghost)
      Frame.put(elm_ghost, coodinate)
      elm_ghost.innerHTML = Ghost.asset
    }
  }
  static set_move(){
    const elm_ghosts = Ghost.elm_ghosts
    for(const elm_ghost of elm_ghosts){
      Ghost.move(elm_ghost)
    }
  }
  static move(elm_ghost){
    if(!elm_ghost){return}
    const data       = Ghost.get_data(elm_ghost)
    const coodinate  = Ghost.get_coodinate(elm_ghost)
    const status     = Ghost.get_status(elm_ghost)
    const directions = Ghost.get_enable_directions(coodinate , data.direction , status) || Ghost.get_enable_directions(coodinate)
    const direction  = Ghost.get_direction(elm_ghost, directions)
    const next_pos   = Frame.next_pos(direction , coodinate)
    Ghost.set_direction(elm_ghost , direction)
    Ghost.moving(elm_ghost , next_pos)
  }
  static moving(elm_ghost , next_pos){
    if(!elm_ghost || !next_pos){return}
    const data = Ghost.get_data(elm_ghost)
    if(!data){return}
    if(Main.is_dead){
      return
    }
    //warp
    if(Frame.is_warp(next_pos)){
      data.coodinate = Frame.get_another_warp_pos(next_pos)
      next_pos = Frame.next_pos(data.direction , data.coodinate)
    }
    if(!next_pos){return}
    const before_pos = {
      x : data.coodinate.x * Frame.block_size,
      y : data.coodinate.y * Frame.block_size,
    }
    const after_pos = {
      x : next_pos.x * Frame.block_size,
      y : next_pos.y * Frame.block_size,
    }
    if(Pacman.is_collision(next_pos)){
      switch(elm_ghost.getAttribute('data-status')){
        case 'weak':
          Ghost.dead(elm_ghost)
          break
        case 'dead':
          break
        default:
          Ghost.crashed(elm_ghost)
          Pacman.crashed(elm_ghost)
          return
      }
    }
    data.next_pos = next_pos
    const id = 'ghost_anim'
    elm_ghost.animate(
      [
        {
          left : `${before_pos.x}px`,
          top  : `${before_pos.y}px`,
        },
        {
          left : `${after_pos.x}px`,
          top  : `${after_pos.y}px`,
        }
      ],
      {
        id : id,
        duration: Ghost.get_speed(elm_ghost)
      }
    )
    Promise.all([elm_ghost.getAnimations().find(e => e.id === id).finished])
    .then(Ghost.moved.bind(this , elm_ghost))
  }
  static moved(elm_ghost , e){
    if(!elm_ghost){return}
    const data = Ghost.get_data(elm_ghost)
    
    elm_ghost.style.setProperty('left' , `${data.next_pos.x * Frame.block_size}px` , '')
    elm_ghost.style.setProperty('top'  , `${data.next_pos.y * Frame.block_size}px` , '')
    
    data.coodinate = data.next_pos
    
    if(Main.is_dead){return}
    if(Pacman.is_collision(data.coodinate)){
      switch(elm_ghost.getAttribute('data-status')){
        case 'weak':
          Ghost.dead(elm_ghost)
          break
        case 'dead':
          break
        default:
          Ghost.crashed(elm_ghost)
          Pacman.crashed(elm_ghost)
          return
      }
    }
    // dead -> alive
    if(Ghost.get_status(elm_ghost) === 'dead'){
      const current_stage_item = Frame.frame_datas[Frame.get_pos2num(data.coodinate)]
      if(current_stage_item.match(/^T/i)){
        Ghost.alive(elm_ghost)
      }
    }
    if(elm_ghost.hasAttribute('data-reverse')){
      elm_ghost.removeAttribute('data-reverse')
      Ghost.reverse_move(elm_ghost , data)
    }
    else{
      Ghost.next_move(elm_ghost , data)
    }
  }
  static reverse_move(elm_ghost , data){
    const direction = Ghost.reverse_direction(data.direction)
    Ghost.set_direction(elm_ghost , direction)
    data.direction = direction
    const next_pos = Frame.next_pos(data.direction , data.coodinate)
    Ghost.moving(elm_ghost , next_pos)
  }
  static next_move(elm_ghost , data){
    const directions = Ghost.get_enable_directions(data.coodinate , data.direction , Ghost.get_status(elm_ghost))
    const direction  = Ghost.get_direction(elm_ghost, directions)
    Ghost.set_direction(elm_ghost , direction)
    const next_pos = Frame.next_pos(data.direction , data.coodinate)
    if(Frame.is_collision(next_pos)){
      Ghost.move(elm_ghost)
    }
    else{
      Ghost.moving(elm_ghost , next_pos)
    }
  }
  static get_direction(elm_ghost, directions){
    if(!directions || !directions.length){return null}
    switch(Ghost.get_status(elm_ghost)){
      // dead : go to the start-area
      case 'dead':
        if(directions.length === 1){
          return directions[0]
        }
        
        const ghost_data  = Ghost.get_data(elm_ghost)
        const start_datas = Frame.ghost_start_area
        if(directions.indexOf('right') !== -1
        && ghost_data.coodinate.x > start_datas.x){
          const index = directions.findIndex(e => e === 'right')
          directions.splice(index,1)
        }
        if(directions.indexOf('left') !== -1
        && ghost_data.coodinate.x < start_datas.x){
          const index = directions.findIndex(e => e === 'left')
          directions.splice(index,1)
        }
        if(directions.indexOf('bottom') !== -1
        && ghost_data.coodinate.y > start_datas.y){
          const index = directions.findIndex(e => e === 'bottom')
          directions.splice(index,1)
        }
        if(directions.indexOf('top') !== -1
        && ghost_data.coodinate.y < start_datas.y){
          const index = directions.findIndex(e => e === 'top')
          directions.splice(index,1)
        }
        const num = Math.floor(Math.random() * directions.length)
        return directions[num] || null
      // normal
      default:
        const direction_num = Math.floor(Math.random() * directions.length)
        return directions[direction_num] || null
    }
    
  }
  // 移動可能な方向の一覧を取得する
  static get_enable_directions(pos , direction , status){
    const directions = []
    // Through(通り抜け)
    const frame_data = Frame.frame_datas[Frame.get_pos2num(pos)]
    if(frame_data.match(/^T/i)){
      return [direction]
    }
    // 右 : right
    if(pos.x + 1 < Frame.map[pos.y].length
    && !Frame.is_collision({x: pos.x + 1, y: pos.y})
    && Frame.is_through({x: pos.x + 1, y: pos.y} , 'right' , status)
    && direction !== 'left'){
      directions.push('right')
    }
    // 左 : left
    if(pos.x - 1 >= 0
    && !Frame.is_collision({x: pos.x - 1, y: pos.y})
    && Frame.is_through({x: pos.x - 1, y: pos.y} , 'left' , status)
    && direction !== 'right'){
      directions.push('left')
    }
    // 上 : up 
    if(pos.y - 1 >= 0
    && !Frame.is_collision({x: pos.x, y: pos.y - 1})
    && Frame.is_through({x: pos.x, y: pos.y - 1} , 'up' , status)
    && direction !== 'down' ){
      directions.push('up')
    }
    // 下 : down
    if(pos.y + 1 < Frame.map.length
    && !Frame.is_collision({x: pos.x, y: pos.y + 1})
    && Frame.is_through({x: pos.x, y: pos.y + 1} , 'down' , status)
    && direction !== 'up'){
      directions.push('down')
    }
    if(directions.length){
      return directions
    }
    else{
      return [Ghost.reverse_direction(direction)]
    }
  }
  static reverse_direction(direction){
    switch(direction){
      case 'right' : return 'left'
      case 'left'  : return 'right'
      case 'up'    : return 'down'
      case 'down'  : return 'up'
    }
  }
  static set_direction(elm_ghost , direction){
    const data     = Ghost.get_data(elm_ghost)
    data.direction = direction
    const head     = elm_ghost.querySelector('.head')
    if(!head){return}
    head.setAttribute('data-direction' , direction)
  }
  static power_on(){
    for(const elm of Ghost.elm_ghosts){
      if(Ghost.get_status(elm) === 'dead'){continue}
      elm.setAttribute('data-reverse' , '1')
      elm.setAttribute('data-status' , 'weak')
    }
  }
  static power_off(){
    for(const elm of Ghost.elm_ghosts){
      if(elm.getAttribute('data-status') === 'weak'){
        elm.setAttribute('data-status' , '')
      }
    }
  }
  
  static crashed(elm_ghost){
    Main.is_crash = true
    Main.is_dead = true
    Ghost.move_stop(elm_ghost)
  }
  static move_stops(){
    for(const elm_ghost of Ghost.elm_ghosts){
      Ghost.move_stop(elm_ghost)
    }
  }
  static move_stop(elm_ghost){
    const svg = elm_ghost.querySelector('.under svg')
    svg.pauseAnimations()
    const anim = elm_ghost.getAnimations()
    if(anim && anim.length){
      anim[0].pause()
    }
  }
  static hidden_all(){
    for(const elm of Ghost.elm_ghosts){
      elm.style.setProperty('display' , 'none' , '');
    }
  }
  static dead(elm_ghost){
    elm_ghost.setAttribute('data-status' , 'dead')
  }
  static alive(elm_ghost){
    elm_ghost.setAttribute('data-status' , '')
  }
  static get_status(elm_ghost){
    return elm_ghost.getAttribute('data-status')
  }
  static get_speed(elm_ghost){
    switch(Ghost.get_status(elm_ghost)){
      case 'weak':
        return Main.ghost_weak_speed
      case 'dead':
        return Main.ghost_dead_speed
      default:
        return Main.ghost_normal_speed
    }
  }
}
  
[全部更新] js/main.js
クリア処理に必要なフラグや、初期処理などを追加してます。
import { Ghost }   from './ghost.js'
import { Frame }   from './frame.js'
import { Control } from './control.js'
import { Pacman }  from './pacman.js'
import { Feed }    from './feed.js'
export const Main = {
  anim_speed         : 200,
  ghost_normal_speed : 200,
  ghost_weak_speed   : 400,
  ghost_dead_speed   : 50,
  is_crash           : false,
  is_dead            : false,
  is_clear           : false,
}
function init(){
  new Frame().then(()=>{
    new Ghost()
    new Pacman()
    new Control()
    new Feed()
  })
}
switch(document.readyState){
  case 'complete':
  case 'interactive':
    init()
    break
  default:
    window.addEventListener('DOMContentLoaded' , (()=>init()))
}
  
[全部更新] js/pacman.js
思っていたよりも変更箇所が多かったので、全部ファイルごと入れ替えちゃってください。
import { Main }     from './main.js'
import { Frame }    from './frame.js'
import { Control }  from './control.js'
import { Feed }     from './feed.js'
import { Ghost }    from './ghost.js'
export class Pacman{
  // 初期表示座標処理
  constructor(){
    Pacman.start()
  }
  static start(){
    Pacman.create()
    Pacman.coodinates = Pacman.start_coodinates
    Frame.put(Pacman.elm, Pacman.coodinates)
    Pacman.elm.style.setProperty('--anim-speed' , `${Main.anim_speed}ms` , '')
  }
  static reset_data(){
    Pacman.direction = null
    Pacman.start()
  }
  static create(){
    if(Pacman.elm){return}
    const div = document.createElement('div')
    div.className = 'pacman'
    Frame.root.appendChild(div)
  }
  static get start_coodinates(){
    return {
      x : 14,
      y : 23,
    }
  }
  static get elm(){
    return document.querySelector('.pacman')
  }
  static move(direction){
    if(Pacman.direction){
      return
    }
    Pacman.direction = direction
    
    Pacman.elm.setAttribute('data-anim' , "true")
    this.moving()
  }
  static moving(){
    if(Main.is_dead || Main.is_clear){return}
    Pacman.next_pos = Frame.next_pos(Pacman.direction , Pacman.coodinates)
    //warp
    if(Frame.is_warp(Pacman.next_pos)){
      Pacman.coodinates = Frame.get_another_warp_pos(Pacman.next_pos)
      Pacman.next_pos   = Frame.next_pos(Pacman.direction , Pacman.coodinates)
    }
    if(Frame.is_collision(Pacman.next_pos)
    && !Pacman.is_wall(Pacman.next_pos)){
      Pacman.elm.setAttribute('data-anim' , "")
      delete Pacman.direction
      return
    }
    Pacman.elm.setAttribute('data-direction' , Pacman.direction)
    Pacman.elm.animate(
      [
        {
          left : `${Pacman.coodinates.x * Frame.block_size}px`,
          top  : `${Pacman.coodinates.y * Frame.block_size}px`,
        },
        {
          left : `${Pacman.next_pos.x * Frame.block_size}px`,
          top  : `${Pacman.next_pos.y * Frame.block_size}px`,
        }
      ],
      {
        duration: Main.anim_speed
      }
    )
    Promise.all(Pacman.elm.getAnimations().map(e => e.finished)).then(()=>{
      Pacman.moved()
    })
  }
  static moved(){
    Pacman.coodinates = Pacman.next_pos
    Frame.put(Pacman.elm, Pacman.coodinates)
    Feed.move_map()
    
    if(Control.direction && Control.direction !== Pacman.direction){
      const temp_pos = Frame.next_pos(Control.direction , Pacman.coodinates)
      if(!Frame.is_collision(temp_pos)
      && !Pacman.is_wall(temp_pos)){
        Pacman.direction = Control.direction
      }
    }
    Pacman.moving()
  }
  static is_wall(map){
    const through_item = Frame.frame_datas[Frame.get_pos2num(map)]
    if(through_item === 'TU'
    || through_item === 'TD'
    || through_item === 'TL'
    || through_item === 'TR'){
      return true
    }
    else{
      false
    }
  }
  static is_collision(pos){
    if(!pos){return}
    if(Pacman.coodinates && pos.x === Pacman.coodinates.x && pos.y === Pacman.coodinates.y){
      return true
    }
    else if(Pacman.next_pos && pos.x === Pacman.next_pos.x && pos.y === Pacman.next_pos.y){
      return true
    }
    else{
      return false
    }
  }
  static crashed(elm_ghost){
    setTimeout(Pacman.dead , 1000)
    Pacman.move_stop()
  }
  static dead(){
    Ghost.hidden_all()
    Pacman.elm.setAttribute('data-direction' , 'up')
    Pacman.elm.setAttribute('data-status' , 'dead')
  }
  static move_stop(){
    const anim = Pacman.elm.getAnimations()
    if(anim && anim.length){
      anim[0].pause()
    }
  }
  static hidden(){
    Pacman.elm.style.setProperty('display','none','')
  }
  static close_mouse(){
    // Pacman.elm.setAttribute('data-status' , 'mouse-close')
  }
}
解説とポイント
初回の設定の甘さがここに来て出てしまいましたwww。
エサの残り数をFeedモジュールで処理させていますが、それをMainモジュールでセットしていなかったのと、敵キャラ、自キャラなどがステージクリアした時に、新しいステージでちゃんと最初の設定にもどるように、データリセットする事が考慮されていなかったので、全体の処理を見直しました。
なので、ステージは、assetデータを読み込んでいて、メモリに残した状態で、きれいに作り直すようにしました。
その時に、自キャラが消えてしまっていたのは、HTMLで直にタグを書いていたことが原因だったので、これを動的に処理するようにしました。
あと、ステージクリアした時に、画面が点滅する処理は、cssに全てやらせているので、さほど難しくなかったですね。
画面キャプチャ
 
  
あとがき
デバッグする時に、毎回1ステージをクリアする必要があったので、めちゃくちゃクリアしまくりました。
でも、全体処理として、足りていなかった箇所などがよく理解できて良かったです。
ただ、まだ自機の残り数などの設定自体が組み込まれていないので、一回でも死んでしまうと、ゲームがフリーズしてしまいます。
次回はその辺を進めましょうね。
知財
パックマンは、バンダイナムコ社の登録商標です。
PAC-MAN™ & ©1980 BANDAI NAMCO Entertainment Inc.
 
0 件のコメント:
コメントを投稿