whatisthis?
javaScript. 턴제 게임 - 텍스트 기반 RPG (下) 본문
턴제 게임 - 텍스트 기반 RGP
- 진행사항
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="stylesheet" href="./style.css">
</head>
<body>
<div class="container">
<form id="start-screen">
<input id="name-input" required placeholder="영웅 이름을 입력하세요!" />
<button id="start">시작</button>
</form>
</div>
<div class="container">
<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>
<div class="menu-container">
<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" required placeholder="숫자를 입력하세요" maxlength="1" />
<button id="menu-button">입력</button>
</form>
</div>
<div class="menu-container">
<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" required placeholder="숫자를 입력하세요" maxlength="1" />
<button id="battle-button">입력</button>
</form>
</div>
<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>
</div>
<script src="./turn.js"></script>
</body>
</html>
- 스타일 적용 (flex)을 위한 container div를 추가했다.
- input 태그에 required속성 / placeholder / maxlength를 추가하였다.
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;
}
- 한글 웹폰트를 적용하였다.
url : https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_suit@1.0/SUIT-Medium.woff2
- * 선택자에 font-family를 적용하였다.
>> placeholder나 button에도 폰트가 적용할 수 있도록.
(물론, ::placeholder 같이 선택자로 적용해도 된다.)
turn.js
let TurnGame = (function () {
let instance;
let initiate = function (heroName) {
let hero = {
name: heroName,
lev: 1,
maxHp: 100,
hp: 100,
xp: 0,
att: 10
};
let 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,
}];
let monster = null;
let turn = true;
return {
showLevel: function () {
document.getElementById('hero-level').innerHTML = hero.lev + ' Lev';
return this;
},
showXp: function () {
let self = 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() {
self.setMessage('레벨업!');
}, 1000);
}
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: function () {
document.getElementById('hero-name').innerHTML = hero.name;
document.getElementById('start-screen').style.display = 'none';
if (document.getElementById('game-menu').style.display === 'block') {
document.getElementById('game-menu').style.display = 'none';
document.getElementById('battle-menu').style.display = 'block';
document.getElementById('battle-input').focus();
} else {
document.getElementById('game-menu').style.display = 'block';
document.getElementById('battle-menu').style.display = 'none';
document.getElementById('menu-input').focus();
}
return this;
},
setMessage: function (msg) {
document.getElementById('message').innerHTML = msg;
return this;
},
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') {
hero.hp = hero.maxHp;
return this.showHp().setMessage('체력을 회복했습니다');
} else if (input === '3') {
return this.exit();
} else {
alert('잘못된 입력');
}
},
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 () {
let 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 + '에서 죽었습니다. ㅡ 다시 시작은 F5';
return false;
},
exit: function (input) {
document.getElementById('screen').innerHTML = '이용해주셔서 감사합니다. ㅡ 다시 시작은 F5';
}
};
};
return {
getInstance: function (name) {
if (!instance) {
instance = initiate(name);
}
return instance;
}
};
})();
document.getElementById('start-screen').onsubmit = function (e) {
let name = document.getElementById('name-input').value;
e.preventDefault();
if (name && name.trim() && confirm(name + '으로 하시겠습니까?')) {
TurnGame.getInstance(name).showXp().toggleMenu();
} else {
alert('이름을 입력해주세요');
}
};
document.getElementById('game-menu').onsubmit = function (e) {
let input = document.getElementById('menu-input');
let option = input.value;
e.preventDefault();
input.value = '';
TurnGame.getInstance().menuInput(option);
};
document.getElementById('battle-menu').onsubmit = function (e) {
let input = document.getElementById('battle-input');
let option = input.value;
e.preventDefault();
input.value = '';
TurnGame.getInstance().battleInput(option);
};
설명이 매우 길 예정이니
🔻🔻🔻🔻🔻
1 / 싱글턴 객체
- 게임을 시작할 때 한번만 시작하기 위해서
객체를 '하나'만 만드는 싱글턴 객체 이용.
IIFE(모듈 패턴)으로 (function() {게임 전체})() 로, 선언하자마자 실행함.
- TurnGame 자체는 IIFE문으로 된 함수인데,
return하는 것이 객체 {} 이다.
- 즉, Turngame()을 하면 ㅡ 객체가 나오고
그 객체로 메소드들을 사용할 수 있는 것.
- 따라서, TurnGame이 반환하는 객체 안에있는 메서드들을 쓰려면
매번 TurnGame을 호출하는 것 보다는
한번만 호출하고, 메소드 체이닝을 사용하면 된다.
2 / 메소드 체이닝
메소드에서 return this를 하는 이유
- 다른 함수를 계속해서 호출하기 위해서 = 메소드 체이닝을 위해서.
return
명령문은 함수 실행을 종료하고, 주어진 값을 함수 호출 지점으로 반환함.
return this.showLevel().showHp();
위 코드는 showXp의 return 값을 나타낸다.
this.showLevel()을 먼저 호출했는데
showLevel을 실행하면
마지막줄에 return this;가 된다.
이 this가 showLevel의 호출부분에 반환되고,
결국엔 this는 메서드들이 다 들어있는 객체 { } 를 의미하므로
그 객체.showHp()에 의해 다음으로 showHp 함수가 호출됨.
showHp도 마찬가지로 return this임 ㅡ 다시한번 그 객체를 반환하고
showXp의 리턴값은? >>> 우선 showLevel을 실행하고 ㅡ showHp를 실행한 후 ㅡ 그 객체 {} 를 리턴함.
이 showXp를 호출한 부분으로 가보자.
TurnGame.getInstance(name).showXp().toggleMenu();
이 코드에서는
Turngame을 하면 getInstance가 있는 객체 반환 ㅡ if(!instance)이면 initiate 함수 실행 ㅡ return 객체 {}를 반환 ㅡ 호출부로 리턴되어서
그객체.showXp().toggleMenu를 실행
>> 아까 과정을 거쳐서 다시 그객체 반환
>>>> 그객체.toggleMenu() 실행.
따라서 위 코드는
1. 초기설정 ㅡ 받은 name값으로 initiate 함수 실행
2. showXp ㅡ 캐릭터 스탯 innerHTML로 표시
3. toggleMenu ㅡ 스타트메뉴 안보이게 ㅡ> 게임메뉴 보이게 / 배틀메뉴 안보이게
>> 맨처음 디폴트값은 둘다 안보이므로 (display:none)if (document.getElementById('game-menu').style.display === 'block') { document.getElementById('game-menu').style.display = 'none'; document.getElementById('battle-menu').style.display = 'block'; document.getElementById('battle-input').focus(); } else { document.getElementById('game-menu').style.display = 'block'; document.getElementById('battle-menu').style.display = 'none'; document.getElementById('menu-input').focus(); }
else 문 부분이 먼저 실행된다. (none 이므로)
>> 게임메뉴 보이게 / 배틀메뉴 안보이게.
다음에 이 if-else를 통과할 때에는 ㅡ submit 이벤트 후에. (1,2,3중에 골라서 제출했을 때)
ㅡ 이때는 game-menu 가 display:block 상태이므로 if문 부분이 실행되어
>> 게임메뉴 안보이게 / 배틀메뉴 보이게
전체 로직
TurnGame | f |
instance | undefined (default) | => getInstance | |||
f initiate |
hero 객체, monsters [] |
hero.name hero.lev hero.maxHp hero.hp hero.xp hero.att |
=> showLevel showXp showHp |
||||
return | showLevel showXp showHp |
innerHTML | showXp - setTimeout() 레벨업 - return this.showLevel().showHP(); |
그외 return this |
|||
toogleMenu setMessage |
display:none | game-menu battle-menu |
|||||
generateMonster | monster변수 : monsters 객체 복사(JSON) |
math.random() 인덱스 뽑기 |
toggleMenu | ||||
menuInput | 1 - generateMonster 3 - exit |
||||||
battleInput | 1 - attackMonster 3 - clearMonster |
||||||
attackMonster attackHero |
=> hp -= att | monster.hp >0 : __데미지 else : win() |
|||||
nextTurn | setTimeout() | button.disabled | |||||
win | xp += mon.xp clearMonster showXp (갱신) |
||||||
clearMonster | monster = null innerHTML = '' |
toggleMenu (배틀 -> 게임) |
|||||
gameOver | #screen. innerHTML |
return false | ** showHP에서 hp <0 |
||||
exit | #screen. innerHTML |
||||||
return | getInstance | initiate(name) | |||||
return instance |
이벤트리스너
start-screen 폼이 제출시 (onsubmit) |
name = #name-input의 value name입력하고, name.trim하고 (공백제거) confirm창 확인 누르면 🔻 TurnGame.getInstance(name).showXp().toggleMenu(); |
game-menu 폼이 제출시 (onsubmit) |
1,2,3 중 입력받아서 option 변수에 저장 후 🔻 TurnGame.getInstance().menuInput(option); - initial은 안거치고 바로 instance 리턴 (이름 입력해둔 initiate의 리턴결과가 instance임) - menuInput 메서드의 input 파라미터에 option이 들어감 |
battle-menu 폼이 제출시 (onsubmit) |
상동. - battleInput 메서드로 실행. - input 파라미터에 option이 들어감 |
+)
JSON 활용
JSON.stringify와 JSON.parse 를 동시에 사용하면 >>> 객체를 복사할 수 있다.
❕ 참조 (reference)가 아닌 복사 (copy)이다!
- 원래 객체(함수 / 일반객체 / 배열)은 대입연산자로는 참조가 되어 원형이 바뀜
사담 👻
역시 어려운 게임만들기.. 우선 메서드가 너무 많은 것도 있지만, 구현이 너무 어렵다.
개인적으로 캐릭터 밸런스가 좀 안맞는 것 같아서
레벨별로 출현하는 몬스터 확률을 패치해보고싶다.
예를들면 레벨이 낮으면 슬라임이 60%, 스켈레톤 30%, 보스가 10% 확률로 등장한다던지
>> 이걸 어떻게 구현할지?
monsters 배열에 index가 있으니까
[1,1,1,1,1,2,2,2,3] 배열에서 인덱스를 뽑아서 그 숫자에 해당하는 인덱스를 가진 몬스터가 나오게?
추가로, 너무 규모가 커지긴 하겠지만 ..
공격 / 방어 / 회복 정도로 나누어도 재밌을듯.
방어 >> 공격 무효화.
이건 몬스터도 쓸 수 있도록. 몬스터가 회복까지 하면 그건 좀 화나니까(?)
몬스터도 방어 쓸 수 있게.
대신, 방어를 연속으로 쓰진 못하게. >> 이 로직은 어떻게 짤지.
- 방어를 쓰면 depend = !depend가 되게 해서 다음번에
if(!depend) { setMessage('방어를 사용할 수 없습니다'); } 이런식으로 하거나?
REFERENCE
'PRACTICE > SELF' 카테고리의 다른 글
javaScript. Painting App 구현 - (2) Canvas Event (0) | 2022.02.02 |
---|---|
javaScript. Painting App 구현 - (1) HTML/CSS (0) | 2022.02.02 |
javaScript. 턴제 게임 - 텍스트 기반 RPG (中) (0) | 2022.01.31 |
javaScript. 턴제 게임 - 텍스트 기반 RPG (上) (0) | 2022.01.30 |
project. (js) 자동 텍스트 RPG 게임 - (수정) (0) | 2022.01.29 |