whatisthis?

javaScript. 턴제 게임 - 텍스트 기반 RPG (中) 본문

PRACTICE/SELF

javaScript. 턴제 게임 - 텍스트 기반 RPG (中)

thisisyjin 2022. 1. 31. 12:25

턴제 게임 - 텍스트 기반 RGP

- 지난번 진행사항

 

javaScript. 턴제 게임 - 텍스트 기반 RPG (上)

턴제 게임 - 텍스트 기반 RGP skill html / css javascript 텍스트 RPG style.css * { margin: 0; box-sizing : border-box; } html { font-family:Impact, Haettenschweiler, 'Arial Narrow Bold', sans-ser..

mywebproject.tistory.com

index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>턴제 게임</title>
  <link rel="styleshee" href="./style.css">
</head>
<body>
<form id="start-screen">
  <input id="name-input" placeholder="영웅 이름을 입력하세요!" />
  <button id="start">시작</button>
</form>
<div id="screen">
  <div id="hero-stat">
    <span id="hero-name"></span>
    <span id="hero-level"></span>
    <span id="hero-hp"></span>
    <span id="hero-xp"></span>
    <span id="hero-att"></span>
  </div>
  <form id="game-menu" style="display: none;">
    <div id="menu-1">1.모험</div>
    <div id="menu-2">2.휴식</div>
    <div id="menu-3">3.종료</div>
    <input id="menu-input" />
    <button id="menu-button">입력</button>
  </form>
  <form id="battle-menu" style="display: none;">
    <div id="battle-1">1.공격</div>
    <div id="battle-2">2.회복</div>
    <div id="battle-3">3.도망</div>
    <input id="battle-input" />
    <button id="battle-button">입력</button>
  </form>
  <div id="message"></div>
  <div id="monster-stat">
    <span id="monster-name"></span>
    <span id="monster-hp"></span>
    <span id="monster-att"></span>  
  </div>
</div>
<script src="./turn.js"></script>
</body>
</html>

turn.js

let TurnGame = (function() {
  let instance;
  let initiate = function(heroName) {   // initiate 변수에 익명함수 저장
    let hero = {
      name: heroName,
      lev: 1,
      maxHp: 100,
      hp: 100,
      xp: 0,
      att: 10
    };
    return {    // showLevel, showXp, showHp, toogleMenu, setMessage 메서드를 가진 객체 반환
      showLevel: function() {
        document.getElementById('hero-level').innerHTML = hero.lev + 'lev';
        return this;
      },
      showXp: function() {
        let self = this;      // setTimeout에선 this가 소멸되니까 미리 저장해둠
        if (hero.xp > 15 * hero.lev) {
          hero.xp -= 15 * hero.lev;
          hero.maxHp += 10;
          hero.hp = hero.maxHp;
          hero.att += hero.lev;
          hero.lev++;
          window.setTimeout(function() {     // setTimeout에선 저장해둔 this(self변수) 사용
            self.setMessage('레벨업!');
          }, 1000);                          // 1000ms 후에 실행
        }
        document.getElementById('hero-xp').innerHTML = 'XP: ' + hero.xp + '/' + 15 * hero.lev;
        document.getElementById('hero-att').innerHTML = 'ATT: ' + hero.att;
        return this.showLevel().showHp();     // 💡 메소드 체이닝
      },
      showHp: function() {
        if (hero.hp < 0) {
          return this.gameOver();
        }
        document.getElementById('hero-hp').innerHTML = 'HP: ' + hero.hp + '/' + hero.maxHp;
        return this;         
      },                                ////// 여기서부턴 toggleMenu , setMessage메서드
      toggleMenu: function () {           // toggleMenu 메서드
        document.getElementById('hero-name').innerHTML = hero.name;
        document.getElementById('start-screen').style.display = 'none';   // 시작화면 안보이게
        
        if (document.getElementById('game-menu').style.display === 'block') {  // 1️⃣ 게임메뉴 보이면 
          document.getElementById('game-menu').style.display = 'none';      // 게임메뉴 가리고
          document.getElementById('battle-menu').style.display = 'block';    // 배틀메뉴 보이게
          document.getElementById('battle-input').focus();       //  배틀 input태그에 포커스되게
          
        } else {                       						// 2️⃣ 게임메뉴 안보이면
          document.getElementById('game-menu').style.display = 'block';   // 게임메뉴 보이게
          document.getElementById('battle-menu').style.display = 'none';   // 배틀메뉴 가리고
          document.getElementById('menu-input').focus();         // 메뉴 input태그에 포커스되게
        }
        return this;
      },
      setMessage: function(msg) {
        document.getElementById('message').innerHTML = msg;
        return this;
      },
    };
  };                ////// 여기까지 객체 return
  return {
    getInstance: function(name) {       // instance가 비어있으면(즉, 맨처음 호출시)에만  
      if (!instance) {                    
        instance = initiate(name);      // 맨처음 호출시에만 ㅡinitiate를 거침 ㅡ instance에 저장
      }
      return instance;            // instance 반환 
    }
  };
})();

document.getElementById('start-screen').onsubmit = function(e) {   // 🎲 시작화면 폼 제출시 ㅡ 이름 
  var name = document.getElementById('name-input').value;   // input의 value
  e.preventDefault();           // submit 기본이벤트 막음
  if (name && name.trim() && confirm(name + '으로 하시겠습니까?')) {   // 확인, 취소중 선택
    TurnGame.getInstance(name).showXp().toggleMenu();    // 💡 메소드 체이닝 ㅡ getInstance함수 호출
                            // showXp엔 return this.showLevel().showHp()가 되어있음
                            // toggleMenu엔 if-else문이 있음 (메뉴 없앴다가 생겼다가)
  } else {
    alert('이름을 입력해주세요');             // 취소 선택시
  }
};

document.getElementById('game-menu').onsubmit = function(e) {     // 🎲 게임메뉴 폼 제출시
  var input = document.getElementById('menu-input');     
  var option = input.value;      // option변수에 input태그 값 저장
  e.preventDefault();        // submit 기본이벤트 막음 (순서 주의❗)
  input.value = '';       // input값 초기화 (빈칸)
};

document.getElementById('battle-menu').onsubmit = function(e) {   // 🎲 배틀메뉴 폼 제출시
  var input = document.getElementById('battle-input');    
  var option = input.value;     // 상동
  e.preventDefault();
  input.value = '';
};

 

<주요 개념>

 

1.  Turngame 싱글턴 객체

2.  이벤트리스너 (HTMLElements.onsubmit)

 

계속해서 같은 객체(Turngame)를 리턴하기 때문에

그 객체의 메소드를 연속으로 체이닝 하듯이 사용할 수 있음.

>> 이런 패턴을 메소드 체이닝(Method Chaining) 이라고 한다.

 

 


 

- 메뉴 선택

더보기
더보기

<form id="game-menu" style="display: none;">
    <div id="menu-1">1.모험</div>
    <div id="menu-2">2.휴식</div>
    <div id="menu-3">3.종료</div>
    <input id="menu-input" />
    <button id="menu-button">입력</button>
  </form>

위 부분에서 메뉴 입력 버튼에 이벤트 리스너를 달아야 한다. (#menu-button)

 

document.getElementById('menu-button').onsubmit = function(e) {
    let input = document.getElementById('menu-input');
    let option = input.value;
    e.preventDefault();
    input.value = '';
    TurnGame.getInstance().menuInput(option);      // 새로 추가
}

document.getElementById('battle-button').onsubmit = function(e) {
    let input = document.getElementById('battle-input');
    let option = input.value;
    e.preventDefault();
    input.value = '';
    TurnGame.getInstance().menuInput(option);      // 새로 추가
}

menuInput 메서드와 battleInput 메서드를 instance에 추가한다.

- 아래에 메서드 추가 예정임.

 

 

 

- 몬스터 객체 리스트

var TurnGame = (function() {
  var instance;
  var initiate = function(heroName) {
    var hero = {
      name: heroName,
      lev: 1,
      maxHp: 100,
      hp: 100,
      xp: 0,
      att: 10
    };
    var monsters = [{
      name: '슬라임',
      hp: 25 + hero.lev * 3,
      att: 10 + hero.lev,
      xp: 10 + hero.lev,
    }, {
      name: '스켈레톤',
      hp: 50 + hero.lev * 5,
      att: 15 + hero.lev * 2,
      xp: 20 + hero.lev * 2,
    }, {
      name: '찬호[보스]',
      hp: 100 + hero.lev * 10,
      att: 25 + hero.lev * 5,
      xp: 50 + hero.lev * 5,
    }];
    var monster = null;
    var turn = true;
    return {
      ... // 이전과 동일 다음 메소드 추가
      generateMonster: function() {
        monster = JSON.parse(JSON.stringify(monsters[Math.floor(Math.random() * monsters.length)]));
        document.getElementById('monster-name').innerHTML = monster.name;
        document.getElementById('monster-hp').innerHTML = 'HP: ' + monster.hp;
        document.getElementById('monster-att').innerHTML = 'ATT: ' + monster.att;
        this.setMessage(monster.name + '이(가) 공격해옵니다');
        return this.toggleMenu();
      },
      menuInput: function(input) {
        if (input === '1') {
          return this.generateMonster();
        } else if (input === '2') {
          hp = maxHp;
          return this.updateStat().setMessage('체력을 회복했습니다');
        } else if (input === '3') {
          return this.exit();
        } else {
          alert('잘못된 입력');
        }
      },
      battleInput: function(input) {}, // 구현필요
      attackMonster: function() {}, // 구현필요
      attackHero: function() {}, // 구현필요
      nextTurn: function() {}, // 구현필요
      win: function() {}, // 구현필요
      clearMonster: function() {}, // 구현필요
      gameOver: function() {}, // 구현필요
      exit: function(input) {
        document.getElementById('screen').innerHTML = '이용해주셔서 감사합니다.새로 시작하려면 새로고침하세요';
      }
    };
  };
  return {
    getInstance: function(name) {
      if (!instance) {
        instance = initiate(name);
      }
      return instance;
    }
  };
})();

 

 

지금까지 완성본 🔻

#start-screen이 onsubmit일때 

name-input의 value가 name 변수에 저장되고,

confirm창에서 확인을 클릭하면 ㅡ 메소드체이닝) TurnGame.getInstance(name).showXp().toggleMenu() 실행.

 

>> 여기서 showXp메서드는 내부적으로 showLevel과 showHp를 실행함.

return this.showLevel().showHp();

 

 


- 전투메뉴 (battleInput, attackMonster, attackHero, nextTurn, win, clearMonster, gameOver)

battleInput: function (input) {
  if (input === '1') {
    return this.attackMonster();
  } else if (input === '2') {
    if (hero.hp + hero.lev * 20 < hero.maxHp) {
      hero.hp += hero.lev * 20;
    } else {
      hero.hp = hero.maxHp;
    }
    return this.showHp().setMessage('체력을 회복했습니다').nextTurn();
  } else if (input === '3') {
    return this.clearMonster().setMessage('도망쳤습니다');
  } else {
    alert('잘못된 입력');
  }
},
attackMonster: function () {
  monster.hp -= hero.att;
  document.getElementById('monster-hp').innerHTML = 'HP: ' + monster.hp;
  if (monster.hp > 0) {
    return this.setMessage(hero.att + '의 데미지를 입혔습니다.').nextTurn();
  }
  return this.win();
},
attackHero: function () {
  hero.hp -= monster.att;
  return this.showHp();
},
nextTurn: function () {
  var self = this;
  turn = !turn;
  document.getElementById('battle-button').disabled = true;
  if (!turn) {
    window.setTimeout(function () {
      self.setMessage(monster.name + '의 턴입니다');
      window.setTimeout(function () {
        document.getElementById('battle-button').disabled = false;
        if (self.attackHero()) {
          self.setMessage(monster.att + '의 데미지를 입었습니다');
          window.setTimeout(function () {
            self.setMessage(hero.name + '의 턴입니다');
          }, 1000);
        }
      }, 1000);
    }, 1000);
    return this.nextTurn();
  }
  return this;
},
win: function () {
  this.setMessage(monster.name + ' 사냥에 성공해 경험치 ' + monster.xp + '을 얻었습니다');
  hero.xp += monster.xp;
  return this.clearMonster().showXp();
},
clearMonster: function () {
  monster = null;
  document.getElementById('monster-name').innerHTML = '';
  document.getElementById('monster-hp').innerHTML = '';
  document.getElementById('monster-att').innerHTML = '';
  return this.toggleMenu();
},
gameOver: function () {
  document.getElementById('screen').innerHTML = hero.name + '은 레벨' + hero.lev + '에서 죽었습니다. 새로 시작하려면 새로고침하세요';
  return false;
},

 

setTimeout부분 이해 🔻

더보기
더보기

nextTurn 메소드

nextTurn: function () {
  var self = this;
  turn = !turn;
  document.getElementById('battle-button').disabled = true;       // 
  if (!turn) {
    window.setTimeout(function () {
      self.setMessage(monster.name + '의 턴입니다');
      window.setTimeout(function () {
        document.getElementById('battle-button').disabled = false;
        if (self.attackHero()) {
          self.setMessage(monster.att + '의 데미지를 입었습니다');
          window.setTimeout(function () {
            self.setMessage(hero.name + '의 턴입니다');
          }, 1000);
        }
      }, 1000);
    }, 1000);11
    return this.nextTurn();      // 지금 !turn이므로 다시 nextTurn()으로 가면 ㅡ turn이 된다
  }
  return this;       // 지금이 turn이면
}

우선, setTimeout에 들어가면 콜백에 의해 this가 소멸되므로, self라는 변수에 this값을 미리 저장해둔다.

그리고, battle-button을 disabled하게 한다. ( html 요소를 안보이게 처리하는 것 )

 

즉, turn이 아닐때는 다음과 같은 루프가 돈다.

 

몬스터의 턴일때 (즉, !turn일때)

setMessage 메서드 (몬스터의 턴입니다) >> 버튼 disabled + attackHero 메서드 >> setMessage (00데미지 입음)

>> setMessage (본인 턴) 

 

 

 

 


style.css

* {
    font-family: 'SUIT-Medium';
    margin: 0;
    box-sizing : border-box;
}

@font-face {
    font-family: 'SUIT-Medium';
    src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_suit@1.0/SUIT-Medium.woff2') format('woff2');
    font-weight: normal;
    font-style: normal;
}

html {
    font-size: 16px;
    line-height: 2;
    color: #1f2d3d;
}


body {
    /* display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center; */
    width: 100%;
    height: 100vh;
    background-color: #f2f4ff;
}


.container {
    display: flex;
    justify-content: center;
    align-items: center;
    background-color: #c1c1c1;
    margin: 50px;
    border-radius: 20px;
}


#hero-stat {
    background-color: rgb(241, 241, 241);
}

#hero-name {
    font-size: 20px;
    font-weight: bold;
}

#hero-level {
    background-color: #ae1e1e;
    color: rgb(255, 253, 243);
    font-weight: bold;
    border-radius: 50%;
}

.menu-container {
    text-align: center;
}

- js위주 실습이다보니 대충 짬.

- 추후에 수정예정임.

 

 

 


 

 

실행 결과

초기 화면

 

메뉴 화면

 

배틀 메뉴

 

 

 

게임오버시

 

 

 


 

 

Test 화면 (gif 파일) 🔻

 

 

 

다음 포스팅에서는 js 코드 리뷰 + 이해를 위한 부가설명 진행.

 

 


 

 

REFERENCE

ZeroCho Blog

 

ZeroCho Blog

ZeroCho의 Javascript와 Node.js 그리고 Web 이야기

www.zerocho.com