오늘의 삽질
RogueLike TextGame
애니메이션 효과 넣기
- 텍스트 게임으로써 한 번에 모든 문장이 나오는게 몰입감이 떨어지는 느낌이라 약간의 애니메이션 효과가 있으면 좋겠다 생각을 했다.
-
스켈레톤 코드를 뜯어보았을 때, logs[] 에 값을 저장하고 while을 돌려 console.log()를 쓰는 것을 발견헀다.
while(player.hp > 0) { console.clear(); displayStatus(stage, player, monster); logs.forEach((log) => console.log(log)); console.log( chalk.green( `\n1. 공격한다 2. 아무것도 하지않는다.`, ), ); const choice = readlineSync.question('당신의 선택은? '); // 플레이어의 선택에 따라 다음 행동 처리 logs.push(chalk.green(`${choice}를 선택하셨습니다.`)); } -
그렇기에 logs 내부를 전부 돌릴 때, timer를 중간에 넣어주는 형식이 좋겠다고 생각했다.
-
forEach의 콜백함수에 async를 붙이고 사용해도 비동기 작업을 기다리지 않는다..
=> 여기서 동기 관련 for 함수들을 찾아볼 때 for await of을 발견했다! -
timer 기능을 하여 한번만 실행해주는 setTimeout()함수를 이용했다.
for await (const log of logs) { console.log(log) // 타이머로 Promise 의 resolve를 반환하게 하여 동기적 표현! await new Promise(resolve => setTimeout(resolve, 300)); }
파일 분리 및 관리하기(+Github연결)
- 스켈레톤 코드에선 코드들이 game.js에 뭉쳐있어 코드 수정 중 스크롤을 너무 왔다갔다 하게 된다..
=> 파일들을 분리해서 각각의 코드들의 접근성을 높이자!
-
구현 예정 기능들을 생각해서 event 폴더와 class 폴더를 만들어 분리하기로 하였다!
-
event 폴더에는 battle(전투) / reward(보상) / gameclear,over(엔딩)을 넣어줄 예정이다.
-
class 폴더에는 player / monster / reward / weapon 등의 구조들을 생각했다!
-
-
저번에 첫 프로젝트 경험으로 파일들을 분리하는건 import와 export로 쉽게 구현 하였다.
전투 이벤트
- 플레이어의 공격 / 회복 및 몬스터의 공격 / 회복 등의 기능을 구현하기 위해 class 폴더에 파일들을 나눠주어 메서드를 만드는게 좋다고 생각했다!
=> 보통 객체의 행동이나 상태를 제어하는 건 그 객체의 메서드를 이용하는게 좋다고 본적이 있다! (수정용이)
-
player의 필요한 값들과 행동제어 메서드들을 구상하며 기본적인 틀을 짯다!
class Player { //기본 구조 constructor() { // 정신력 this.maxHp = 100; this.hp = this.maxHp; // 무기 this.weapon = Weapons[0]; // 몰입도 this.minDmg = 20 + this.weapon.damage; this.maxDmg = 30 + this.weapon.damage; // 이해력 this.lev = 1; // 수면의 질 this.minHeal = 100 + this.weapon.heal; this.maxHeal = 100 + this.weapon.heal; // 통계 값 this.kills = 0; this.totalDmg = 0; this.totalHeal = 0; this.maxLev = this.lev; } // 문제 풀기(공격) attack(monster, logs) { //랜덤 값 추출 let playerDmg = Math.floor(Math.random() * (this.maxDmg - this.minDmg)) + this.minDmg; monster.hp -= playerDmg; if (monster.hp > 0) { this.totalDmg += playerDmg; logs.push( chalk.green(`${playerDmg} Page 만큼의 문제를 풀었습니다!`), ); } else { this.totalDmg += playerDmg + monster.hp; logs.push( chalk.green( `${playerDmg + monster.hp} Page 만큼의 문제를 풀었습니다!`, ), ); } // 제출기한 업데이트 메서드 monster.Day(logs); } // 회복 sleep(monster, logs) { const playerHeal = Math.floor(Math.random() * (this.maxHeal - this.minHeal)) + this.minHeal; if (this.hp === this.maxHp) { logs.push(chalk.green(`정신이 온전합니다! 열심히 공부 하세요!!`)); } else if (this.hp + playerHeal >= this.maxHp) { logs.push(chalk.green(`정신이 매우 말끔해졌습니다!!`)); this.hp = this.maxHp; } else { this.hp += playerHeal; logs.push( chalk.green(`${playerHeal} 만큼의 정신력을 회복했습니다!!`), ); } // 제출기한 업데이트 monster.Day(logs); } } -
monster는 좀 더 다양성을 위해 랜덤으로 과목을 정하게 구상했다.
class Monster { constructor(stage) { const typeRand = Math.floor(Math.random() * 100); if (typeRand < 5) { this.type = '철학'; this.value = 10; } else if (typeRand < 20) { this.type = '수학'; this.value = 7; } else if (typeRand < 55) { this.type = '영어'; this.value = 5; } else if (typeRand < 80) { this.type = '국어'; this.value = 3; } else { this.type = '체육'; this.value = 1; } // 문제집 과목 설정 this.name = `${this.type} 문제집`; // 남은 제출 기한 this.maxDay = this.value * 5; this.day = this.maxDay; // 페이지 수 this.maxHp = (this.value + stage) * 30; this.hp = this.maxHp; // 학습 피로도 day가 지날수록 강해짐 + 과목에따라 달라짐 this.minDmg = Math.round(this.value + stage) + 5; this.maxDmg = Math.round(this.value + stage) + 10; // 난이도 this.lev = stage + Math.floor(this.value / 2); // 망각 Page 수 this.minHeal = 5; this.maxHeal = 20; } attack(player, logs) { //랜덤 값 추출 const monsterDmg = Math.floor(Math.random() * (this.maxDmg - this.minDmg)) + this.minDmg; player.hp -= monsterDmg; logs.push( chalk.redBright(`정신력이 ${monsterDmg}만큼 소모되었습니다! `), ); if (player.hp <= 0) { player.die(logs); } } } -
event 폴더에 있는 battle(전투) 파일의 선택지와 연결을 해준다.
//battle 함수에서 매개변수로 stage / player / monster를 받아온다! const battle = async (stage, player, monster) => { let logs = []; let run = false; while (player.hp > 0) { //콘솔 로그 정리 console.clear(); console.log( chalk.green( `\n1. 문제풀기(공격) 2. 수면(회복) 3. 포기(도망) 4. 복습(버프)`, ), ); // readlinSync는 추가 package로 가져온 함수로 입력을 동기적으로 받는다. const choice = readlineSync.question('Choice? '); // 플레이어의 선택에 따라 다음 행동 처리 logs.push(chalk.green(`${choice}번을 선택하셨습니다.`)); switch (choice) { case '1': logs.push(chalk.blue('문제를 풀기시작합니다!')); //받아온 매개변수 player의 메서드를 이용하여 공격을 실행! player.attack(monster, logs); //몬스터가 죽었는데 공격 당하는 일을 방지! if (monster.hp > 0) { monster.attack(player, logs); } else { logs.push( chalk.green('문제집을 전부 풀어 정신이 멀쩡합니다!'), ); } break; case '2': console.log(chalk.blue('구현 준비중입니다.. ')); handleUserInput(); break; case '3': console.log(chalk.blue('구현 준비중입니다.. ')); handleUserInput(); break; case '4': console.log(chalk.blue('구현 준비중입니다.. ')); handleUserInput(); break; default: console.log(chalk.red('올바르지 않은 접근입니다.')); handleUserInput(); // 유효하지 않은 입력일 경우 다시 입력 받음 } } }
보상 이벤트
- 스테이지를 클리어 한 이후 랜덤한 보상을 얻는 것보단, 선택지가 어느정도 정해져있었으면 해서 구현을 하게 되었다!
-
event 파일에 reward 파일을 만들고, 보상을 정해주기 위해 class에도 reward를 만들어준다!
-
reward(event)를 battle과 비슷하게 구성을 하고, 매개변수로 reward(class)를 받아 reward에 메서드를 구성해주어 불러오게 만든다. => event에 함수들을 전부 넣어 관리하기에는 조금 불편해 보여서 그래따..
-
전투 이벤트와 동일하게 reward(event)에 class를 이어주며, battle이 끝난뒤에 몬스터를 잡았을 경우 이동하게 구성을 해준다!
- reward 이벤트 구현
// reward(event).js import chalk from 'chalk'; import readlineSync from 'readline-sync'; import figlet from 'figlet'; import endgame from './gameover.js'; const rewardEvent = async (stage, player, reward) => { let logs = []; let exit = false; logs.push( chalk.magentaBright( `============================= 보상 정보 =============================`, ), ); logs.push( chalk.greenBright( `| 기본보상 | 회복 : ${reward.heal} | 이해력 증가 : ${reward.levUp} |`, ), ); 1; //player에 회복 및 레벨업 메서드를 이용해준다! (보상 이벤트를 구현하며 만든) player.heal(reward.heal, logs); player.levelSet(reward.levUp, logs); while (!exit) { console.clear(); //단순 큰 글자를 그려주는 함수 console.log( chalk.green( figlet.textSync('Reward Time', { font: 'Standard', horizontalLayout: 'default', verticalLayout: 'default', }), ), ); for await (const log of logs) { console.log(log); // 애니메이션 효과 딜레이 await new Promise((resolve) => setTimeout(resolve, 200)); } displayReward(stage, player); logs = []; console.log( chalk.green( `\n선택은 한 번만 가능하며, 변경사항을 확인하고 취소할 수 있습니다(되돌아오기)`, `\n1. 휴식 2. 필기구 강화 3. 뽑기 4. 포기`, ), ); const choice = readlineSync.question('Choice? '); // 플레이어의 선택에 따라 다음 행동 처리 logs.push(chalk.green(`${choice}를 선택하셨습니다.`)); switch (choice) { case '1': //reward의 메서드를 이용하여 진행 exit의 리턴값으로 while문 제어 exit = await reward.rest(player, stage, displayReward); break; case '2': exit = await reward.upgrade(player, stage); break; case '3': exit = await reward.gamble(); break; case '4': console.log(chalk.red('게임을 마무리 합니다.')); return await endgame(stage, player); default: console.log(chalk.red('올바르지 않은 접근입니다.')); handleUserInput(); // 유효하지 않은 입력일 경우 다시 입력 받음 } } }- reward 구조/메서드 구현
//reward(class).js class Rewards { constructor(player, monster, stage) { //회복량 this.heal = (Math.floor(Math.random() * (player.maxHeal - player.minHeal)) + player.minHeal) * 2; //이해력 레벌업 수치 this.levUp = Math.round(Math.random() * monster.value) + Math.round(monster.value / 2) + stage; //수면효과 증가 this.healUp = Math.round(Math.random() * (monster.value * 2)) + Math.round(monster.value) + stage; //최대 정신력 증가 this.hpUp = Math.round(Math.random() * (monster.value * 5)) + monster.value * 2 + stage; } // 휴식 기능 async rest(player, stage, displayReward) { let logs = []; let results = false; let exit = false; let choice; console.clear(); console.log( chalk.green( figlet.textSync('Rest', { font: 'Standard', horizontalLayout: 'default', verticalLayout: 'default', }), ), ); logs.push( chalk.magentaBright( `============================= 보상 정보 =============================`, ), ); logs.push( chalk.greenBright( `| 휴식보상 | 최대 정신력 : ${this.hpUp} | 최대 수면효과 증가 : ${this.healUp} |`, ), ); while (!exit) { for await (const log of logs) { console.log(log); // 애니메이션 효과 딜레이 await new Promise((resolve) => setTimeout(resolve, 200)); } if (choice) { await new Promise((resolve) => setTimeout(resolve, 1000)); return results; } displayReward(stage, player); logs = []; console.log(chalk.green(`\n1. 수락 2. 취소(뒤로가기)`)); choice = readlineSync.question('Choice? '); // 플레이어의 선택에 따라 다음 행동 처리 logs.push(chalk.green(`${choice}를 선택하셨습니다.`)); switch (choice) { case '1': player.maxHpSet(this.hpUp, logs); player.heal(this.hpUp, logs); results = true; break; case '2': results = false; logs.push( chalk.redBright( `선택을 취소했습니다! 선택지로 다시 이동합니다..`, ), ); break; default: console.log(chalk.red('올바르지 않은 접근입니다.')); handleUserInput(); // 유효하지 않은 입력일 경우 다시 입력 받음 } } } }- 주요 파일들과 연결해주기
// battle.js // while 문 내부 if (monster.hp <= 0) { // 몬스터를 처치 시 await new Promise((resolve) => setTimeout(resolve, 1000)); // 글자를 1초 동안 유지되도록 하구 player.kills += 1; // 통계값에 쓸 값 업데이트 return true; } // game.js // status 를 이용하여 리턴되는 값에 따라 다음 event로 이동을 하게 한다! let status; while (stage <= 10) { const monster = new Monster(stage); status = await battle(stage, player, monster); if (status === 'run') { // 도주 시 player.hp = player.maxHp; continue; } else if (status) { // 스테이지 클리어 시 const reward = new Rewards(player, monster, stage); await rewardEvent(stage, player, reward); stage++; } else { // 게임 오버! return await endgame(stage, player); } }
무기 시스템
-
스테이지를 통과할 때마다 선형적인 성장보다 로그라이크류 답게 여러 랜덤요소가 있었으면 해서 무기 시스템을 만들어두면 좋을 것 같다 생각했다!
-
추가로 강화 시스템도 보상 이벤트에 추가하면 좋을 것 같아 만들었다.
-
class에 weapon 파일을 추가하고 무기들을 저장할 수 있는 저장 파일’weapons’를 만들어준다
//weapon class class Weapon { constructor(name, damage, heal, plus, prob) { //강화된 횟수 this.plus = plus ? plus : 0; //강화 성공 확률 this.plusProb = prob ? prob : 75; // 이름 this.name = name; //기본 데미지 this.damage = damage; //회복량 this.heal = heal; } } export default Weapon; //weapons storage import Weapon from '../class/weapon.js'; //무기 저장 변수 const Weapons = []; //기본 (테스트용 무기) Weapons.push(new Weapon('평범한 노트', 1000, 0)); export default Weapons; -
무기를 player에 저장할 수 있는 변수를 만들어주고, 무기 교환 메서드도 만들어 준다!
class Player { constructor() { // 정신력 this.maxHp = 100; this.hp = this.maxHp; // 기본 무기로 설정해두기 this.weapon = Weapons[0]; // 몰입도 this.minDmg = 20 + this.weapon.damage; this.maxDmg = 30 + this.weapon.damage; // 이해력 this.lev = 1; // 수면의 질 this.minHeal = 100 + this.weapon.heal; this.maxHeal = 100 + this.weapon.heal; // 통계 값 this.kills = 0; this.totalDmg = 0; this.totalHeal = 0; this.maxLev = this.lev; } // 무기교체 메서드 changeUpdate(weapon) { // 현재 값과 받아온 값들을 저장 const preDamage = this.weapon.damage; const preHeal = this.weapon.heal; const aftDamage = weapon.damage; const aftHeal = weapon.heal; //데미지 변경 this.minDmg -= preDamage; this.maxDmg -= preDamage; this.minDmg += aftDamage; this.maxDmg += aftDamage; //회복력 변경 this.minHeal -= preHeal; this.maxHeal -= preHeal; this.minHeal += aftHeal; this.maxHeal += aftHeal; //weapon 업데이트 (교체) this.weapon = weapon; } } -
무기 강화를 위해 reward(event / class)에 메서드와 연결구조를 구성해준다!
(연결 구조는 맨위의 보상 이벤트란을 참고해주세요!)//무기 강화 메서드 async upgrade(player, stage) { let logs = []; let results = false; let exit = false; let choice; const plusWpn = player.weapon.plusWeapon(stage); console.clear(); console.log( chalk.green( figlet.textSync('Rest', { font: 'Standard', horizontalLayout: 'default', verticalLayout: 'default', }), ), ); logs.push( chalk.magentaBright( `============================= 강화 정보 =============================`, ), ); logs.push( chalk.cyanBright(`| 장착한 필기구 : ${player.weapon.name} |`), ); logs.push( chalk.yellowBright( `| 강화 이전 | 이름 : ${player.weapon.name} | 몰입도 상승 : ${player.weapon.damage} Page | 수면효과 상승 : ${player.weapon.heal} |`, ), ); logs.push( chalk.greenBright( `| 강화 이후 | 이름 : ${plusWpn.name} | 몰입도 상승 : ${plusWpn.damage} Page | 수면효과 상승 : ${plusWpn.heal} |`, ), ); logs.push( chalk.redBright( `| 강화 비용 | 이해력 : ${player.lev}/${player.weapon.plus * 2 + 1} | 확률 : ${player.weapon.plusProb}% | `, ), ); while (!exit) { for await (const log of logs) { console.log(log); // 애니메이션 효과 딜레이 await new Promise((resolve) => setTimeout(resolve, 200)); } logs = []; if (choice) { await new Promise((resolve) => setTimeout(resolve, 1000)); return results; } console.log(chalk.green(`\n1. 수락 2. 취소(뒤로가기)`)); choice = readlineSync.question('Choice? '); // 플레이어의 선택에 따라 다음 행동 처리 logs.push(chalk.green(`${choice}를 선택하셨습니다.`)); switch (choice) { case '1': if (Math.random() * 100 <= player.weapon.plusProb) { logs.push( chalk.greenBright(`무기 강화에 성공했습니다!!`), ); player.levelSet(-(player.weapon.plus * 2 + 1), logs); player.changeUpdate(plusWpn, logs); } else { logs.push( chalk.redBright(`무기 강화에 실패했습니다..`), ); player.levelSet(-(player.weapon.plus * 2 + 1), logs); } results = true; break; case '2': logs.push( chalk.redBright( `선택을 취소했습니다! 선택지로 다시 이동합니다..`, ), ); results = false; break; default: console.log(chalk.red('올바르지 않은 접근입니다.')); handleUserInput(); // 유효하지 않은 입력일 경우 다시 입력 받음 } } }
게임 종료 및 재시작 + 통계확인 시스템
- 게임이 승리했을 때나 패배했을 때를 가정한 이벤트를 구현할 필요성을 느껴 event폴더에 파일을 추가해주었다!
-
게임 종료 시, 여러 통계값들이 보이면 재밌을 꺼 같아 player에 통계전용 값들을 만들어 준다!
(전투 이벤트 참조) -
통계값들을 저장해주는 시스템을 메서드 곳곳에 넣어준다!
(전투 이벤트 참조) -
엔딩 이벤트들을 구성해주어 통계값들을 불러와준다
//gameover import chalk from 'chalk'; import readlineSync from 'readline-sync'; import { startGame } from '../game.js'; const endgame = async (stage, player) => { let logs = []; let exit = true; while (exit) { console.clear(); logs.push(chalk.magentaBright(`==== 결과 ====`)); logs.push(chalk.cyanBright(`| Stage: ${stage} |`)); logs.push(chalk.cyanBright(`| Ending : 0 | 나는 바보다 |`)); logs.push(chalk.cyanBright(`| 최대 이해력 : ${player.maxLev} |`)); logs.push(chalk.yellowBright(`| 푼 문제집 수 : ${player.kills} |`)); logs.push(chalk.redBright(`| 풀었던 Page들 : ${player.totalDmg} |`)); logs.push(chalk.greenBright(`| 회복한 정신력 : ${player.totalHeal} |`)); logs.push(chalk.magentaBright(`=============`)); for await (const log of logs) { console.log(log); // 애니메이션 효과 딜레이 await new Promise((resolve) => setTimeout(resolve, 300)); } console.log(chalk.green(`\n1. 새로운 인생 2. 현타와서 종료`)); const choice = readlineSync.question('Choice? '); switch (choice) { case '1': logs.push(chalk.green('게임을 다시 시작합니다!')); //함수를 빠져나간 뒤 실행! return startGame(); case '2': console.log(chalk.green('게임이 종료됩니다!')); process.exit(0); default: console.log(chalk.red('올바르지 않은 접근입니다.')); handleUserInput(); // 유효하지 않은 입력일 경우 다시 입력 받음 } } }; export default endgame; -
엔딩 이벤트를 기존에 있는 시스템과 연결해준다!
(보상 이벤트 란의 game.js 참고) -
게임이 끝나고, 재시작하기 위해 게임시작 기능인 game.js/startGame()을 재사용 하려는데.. 기존의 게임인 startGame() 함수 에서 빠져나오지 못해 console.log들이 겹쳐져 나오는 오류가 생겼다!
=> 그래서 함수 내부를 빠져나갈 수 있도록 return을 이용하여 endgame()를 나오고 startGame()을 실행할 수 있도록 해줬다!
=> 또한 endgame() 자체도 return으로 만들어 기존의 startGame()함수 내부에 있기에 나올 수 있도록 해줬다.
개선점 분석
- 여러 시스템들을 구상하는 과정에서 너무 여러 요소를 고려하려 해서 시작할 엄두가 안났던 시간이 있다..
=> 우선순위를 정하여 구현을 마치고, 이후 과정에서 리팩토링/확장 하는 형식으로 작업을 해야겠다!
지식창고
RogueLike 과제
for await of
-
Array의 요소들을 돌아가며 순차적으로 비동기 함수를 동기적으로 실행한다!
-
await은 Promise 함수의 resolve 값을 기다리기 때문에 타이머를 기다리게 된다!
const Array = [1,2,3,4];
for await (const value of Array) {
//콘솔에 값 로그 올리기
console.log(value);
// 비동기 함수인 setTimeout을 for await of과 Promise를 이용하여 타이머기능을 구현했다.
await new Promise(resolve => setTimeout(resolve, 1000));
}