1. 개발 목표
- 이미 주어진 코드에서 TextRPG 구현하기
- 단순 행동 패턴
- 공격한다
- 연속 공격한다
- 방어한다
- 도망간다
- 클래스 문법 활용, 플레이어, 몬스터 스탯 관리
- 플레이어 경험치가 가득차면 업그레이드 시킬 능력치 고를 수 있게 하기
- 단일 책임의 원칙 최대한 지키기
- 확률 로직 적용
- 공격력 증가량
- 몬스터 스탯
2. 게임 화면
3. 개발 환경 세팅
- 프로젝트 시작
- npm init -y를 입력하면 프로젝트 폴더 안에는 package.json이 생성됩니다.
- -y 옵션의 사용으로 폴더명이 프로젝트의 이름이 됩니다.
- 글 깨짐
- chcp 65001 입력하면 임시방편으로 글깨짐이 해결된다.
- 글 깨짐은 VSCODE의 UTF-8로 모든설정을 바꿔준다.
- 라이브러리 다운로드
- npm install chalk figlet readline-sync
- npm install -D prettier // 개발상황에서만 쓰이는 라이브러리 (-D 옵션)
- 확장으로 Prettier -Code formatter 다운로드
- .prettierrc 파일을 최상위 폴더에 생성한 후
- .prettierrc 파일안에 인터넷 또는 튜터님이 주신 코드를 넣어서작성한다.
4. 개발 중 필수 코드
- Player또는 Monster의 관리를 Class로 해야하는데 이 관리하는 폴더를 따로 만들어서 빼내주는 역할을 만들었습니다.
- creature.js라는 모든 생명체의 base가 되는 class를 만들어 공용으로 필요한 스탯 또는 데미지를 주는 것들에 대해서 넣어줬습니다.
- 여기서 this.originDamage와 this.damage그리고 this.realDamage가 있는데 각자의 쓰임이 다릅니다.
- this.originDamage는 그 유닛이 레벨업을 해서 공격력이 강해지지 않는한 이 originDamage를 기준으로 공격을 가했을 때 랜덤 데미지가 계산이 됩니다.
- this.damage는 유닛이 타겟에게 공격을 가할 때 실제로 들어가는 데미지가 아닌 유닛이 공격을 가할 때 최소, 최대 사이의 값의 데미지로 랜덤하게 데미지가 들어가는데 그 값을 저장해두기 위해서입니다.
- this.realDamage는 타겟의 방어도까지 계산해 실제들어가는 데미지를 적어두기 위해서 만들었습니다.
import chalk from 'chalk';
import readlineSync from 'readline-sync';
import { getRandomNum, playerRank, randomPlay } from '../util.js';
export class Creature {
constructor(hp, maxHp, minDamage, maxDamage, armor, level) {
this.hp = hp;
this.maxHp = maxHp;
this.originDamage = minDamage;
this.damage = minDamage;
this.realDamage = minDamage;
this.minDamage = minDamage;
this.maxDamage = maxDamage;
this.armor = armor;
this.level = level;
}
getDamage() {
this.damage = getRandomNum(this.minDamage, this.maxDamage);
return this.damage;
}
// 타겟의 방어도까지 계산해서 데미지 계산
attack(target) {
this.realDamage = Math.max(0, Number(this.getDamage() - target.armor));
target.hp = Math.max(0, Number(target.hp - this.realDamage));
}
// 특정 데미지를 그냥 줄때
attackDamage(target, damage) {
target.hp -= damage;
}
}
- Player가 몬스터를 죽여서 경험치를 얻게 되었을 때의 코드를 보겠습니다.
- 최대 경험치보다 현재 얻은 경험치가 많다면 while문에 들어가게 되어 다른 곳에서 실행될 수 있는 함수들의 실행을 막아줍니다.
- 초과한 경험치는 레벨업 하기 전의 레벨의 최대경험치량을 빼준 다음 능력치를 고를 수 있게 해줍니다.
- 여기서 능력치중에 ~가 있는 것들은 운에 따라서 높은 능력치를 얻을 수 도 있고 적게 얻을 수도 있습니다.
getExp(xp) {
this.exp += xp;
while (this.exp >= this.maxExp) {
let booleanValue = true;
let tempLevel = this.level;
this.level += 1;
this.exp -= this.maxExp;
this.maxExp = Math.floor(this.maxExp * 1.5);
console.log(
chalk.green(`| 플레이어의 레벨이 ${tempLevel} => ${this.level} 이 되었습니다 !!! |`),
);
console.log(chalk.green(`${'='.repeat(30)} 증가할 능력치를 선택해주세요. ${'='.repeat(30)}`));
console.log(chalk.magenta('='.repeat(90)));
console.log(
chalk.white(
`| 1. 공격력 +5, 최대공격력 +5~10 2. 체력 +25*level 3. 연속 공격 확률 +5~10% 4. 방어도 +5~10 5. 방어확률 +10% 6. 도망 가기 확률 +5~10% |`,
),
);
console.log(chalk.magenta('='.repeat(90)));
while (booleanValue) {
const choice = readlineSync.question('당신의 선택은? ');
switch (Number(choice)) {
case 1:
let tempDamage = this.maxDamage;
this.originDamage += 5;
this.damageModify();
console.log(chalk.white(`플레이어의 공격력이 ${this.minDamage}로 증가하였습니다.`));
console.log(
chalk.white(
`플레이어의 최대 공격력이 ${tempDamage} => ${this.maxDamage}로 증가하였습니다.`,
),
);
booleanValue = false;
break;
case 2:
this.hp += 25 * this.level;
console.log(chalk.white(`플레이어의 체력이 ${this.hp}가 되었습니다.`));
booleanValue = false;
break;
case 3:
this.doubleAttackProbability += getRandomNum(5, 10);
console.log(
chalk.white(
`플레이어의 연속 공격 확률이 ${this.doubleAttackProbability}%가 되었습니다.`,
),
);
booleanValue = false;
break;
case 4:
this.armor += getRandomNum(5, 10);
console.log(chalk.white(`플레이어의 방어도가 ${this.armor}가 되었습니다.`));
booleanValue = false;
break;
case 5:
if (this.defenseProbability >= 100) {
console.log(
chalk.white(`이미 플레이어의 방어확률이 ${this.defenseProbability}%가 되었습니다.`),
);
} else {
this.defenseProbability += 10;
console.log(
chalk.white(`플레이어의 방어확률이 ${this.defenseProbability}%가 되었습니다.`),
);
booleanValue = false;
}
break;
case 6:
this.runAwayProbability += getRandomNum(5, 10);
console.log(
chalk.white(`플레이어의 도망칠 확률이 ${this.runAwayProbability}%가 되었습니다.`),
);
booleanValue = false;
break;
default:
console.log(chalk.red('올바른 숫자를 눌러주세요! '));
break;
}
}
}
}
- 플레이어의 선택에 따라서 공격 성공, 실패 또는 도망가기 성공 실패 와 같은 것에 따라서 플레이어의 상태가 바뀌어 그거에 따라서 다음에 실행되는 코드들이 바뀌게 해줍니다.
- 위의 말을 쉽게하면 FSM을 적용했습니다.
- 처음 Player를 생성하면 player.state 는 'idle' 상태입니다.
- 1.번을 눌러 공격을 하게 되면 플레이어는 'fight'상태가 됩니다.
- 4. 번을 눌러 도망가기에 성공하면 player.state는 'runAway'상태가 됩니다.
function playerPlay(choice, stage, player, monster) {
const logs = [];
switch (Number(choice)) {
case 1:
player.attack(monster);
logs.push(
chalk.green(
`[${choice}] 플레이어가 몬스터에게 ${player.realDamage} 만큼 데미지를 줬습니다.`,
),
);
player.state = 'fight';
break;
case 2:
if (randomPlay(player.doubleAttackProbability)) {
logs.push(chalk.green(`[${choice}] 플레이어가 연속 공격에 성공했습니다.`));
player.doubleAttack(monster);
player.attackDamage(monster, player.firstAttack);
logs.push(
chalk.green(
`[${choice}] 플레이어가 몬스터에게 ${player.firstAttack} 만큼 데미지를 줬습니다.`,
),
);
player.attackDamage(monster, player.secondAttack);
logs.push(
chalk.green(
`[${choice}] 플레이어가 몬스터에게 ${player.secondAttack} 만큼 데미지를 줬습니다.`,
),
);
} else {
logs.push(chalk.red(`[${choice}] 플레이어가 연속 공격에 실패했습니다.`));
}
player.state = 'fight';
break;
case 3:
if (randomPlay(player.defenseProbability)) {
logs.push(chalk.red(`[${choice}] 플레이어가 방어에 성공했습니다.`));
player.state = 'defense';
} else {
logs.push(chalk.red(`[${choice}] 플레이어가 방어에 실패했습니다.`));
player.state = 'fight';
}
break;
case 4:
if (randomPlay(player.runAwayProbability)) {
logs.push(chalk.red(`[${choice}] 플레이어가 도망치기에 성공했습니다.`));
player.state = 'runAway';
} else {
logs.push(chalk.red(`[${choice}] 플레이어가 도망치기에 실패했습니다.`));
player.state = 'idle';
}
break;
default:
logs.push(chalk.green(`[${choice}] 음? 키를 잘못 눌러 몬스터에게 공격받았습니다.`));
player.state = 'fight';
break;
}
// 방어상태 또는 도망간 상태가 아니라면 몬스터에게 맞아야 한다.
if (player.state != 'defense' && player.state != 'runAway') {
monster.attack(player);
logs.push(
chalk.green(
`[${choice}] 몬스터가 플레이어에게 ${monster.realDamage} 만큼 데미지를 줬습니다.`,
),
);
}
return logs;
}
- Player의 state에 따라서 다음 스테이지로 넘어갈지 아닐지 하는 선택지들이 나오게 한다.
- player.state가 'runAway'상태가 되면 break; 문에 의해서 while문이 바로 종료가 된다.
const battle = async (stage, player, monster) => {
const logs = [];
monster.setMonsterInfo(stage);
while (player.hp > 0 && monster.hp > 0) {
console.clear();
displayStatus(stage, player, monster);
logs.forEach((log) => console.log(log));
console.log(
chalk.green(
`\n1. 공격한다 2. 연속 공격(${player.doubleAttackProbability}%) 3. 방어한다(${player.defenseProbability}%) 4. 도망간다(${player.runAwayProbability}%)`,
),
);
const choice = readlineSync.question('당신의 선택은? ');
// 플레이어의 선택에 따라 다음 행동 처리
logs.push(chalk.green(`${choice}를 선택하셨습니다.`));
player.state = 'idle';
playerPlay(choice, stage, player, monster).forEach((log) => logs.push(log));
if (player.state != 'runAway') {
console.clear();
displayStatus(stage, player, monster);
logs.forEach((log) => console.log(log));
if (monster.hp <= 0) {
console.log(chalk.white(`Level${monster.level} 몬스터 토벌에 성공했습니다!`));
console.log(chalk.yellow(`플레이어가 경험치 ${monster.xp}만큼 획득 했습니다!`));
player.getExp(monster.xp);
} else if (player.hp <= 0) {
console.log(chalk.red(`공략에 실패했습니다!`));
console.log(chalk.red(`다시는 이 용사를 볼 수 없습니다!`));
process.exit(0);
}
} else {
break;
}
}
};
- Player의 state에 따라서 만약에 runAway를 해서 while문을 빠져나왔다면 밑의 콘솔이 찍히게 됩니다.
- 여기서 다음 스테이지 이동을 하면 몬스터는 급격히 강해집니다.
- 같은 스테이지 다른 몬스터 잡기를 하면 방금 싸웠던 몬스터와 큰 차이 없는 몬스터를 잡습니다.
- 이전 스테이지 이동은 stage 2이상일 때 만 실행되게 만들었습니다.
- 마지막으로 스테이지 클리어 및 게임 종료의 조건은 stage를 10단계를 다 클리어할 때 입니다.
export async function startGame() {
console.clear();
const player = createPlayer();
let stage = 1;
while (stage <= 10) {
const monster = createMonster(stage);
await battle(stage, player, monster);
// 스테이지 클리어 및 게임 종료 조건
if (player.state === 'runAway') {
console.log(chalk.green(`도망 성공!!`));
}
console.log(chalk.yellow('='.repeat(30)));
console.log(chalk.green(` 1. 다음 스테이지 이동 \n`));
console.log(chalk.green(` 2. 같은 스테이지 다른 몬스터 잡기 \n`));
console.log(chalk.green(` 3. 이전 스테이지 이동 \n`));
console.log(chalk.yellow('='.repeat(30)));
const choice = readlineSync.question('당신의 선택은? ');
switch (Number(choice)) {
case 1:
stage++;
break;
case 2:
break;
case 3:
if (stage >= 2){
stage--;
}
break;
default:
break;
}
}
5. 게임 플레이 정리
- 스테이지 10단계를 클리어하면 게임은 종료 됩니다.
- 플레이어가 죽게 되면 게임은 종료 됩니다.
- 플레이어는 자신의 선택에 따라서 몬스터를 잡게 되면 경험치를 얻고 레벨업을 통해서 자신의 능력치를 강화합니다.
- 몬스터를 잡게 되면 3가지 선택중에 하나를 고르면 됩니다.
코드들이 잘 동작하는 것을 볼 수 있었습니다.
6. 깃허브
https://github.com/YongHyeon1231/RogueLike-TextRPG
7. 느낀점
이번 과제를 진행하면서 아직 과제를 받고 바로 코드를 작성할 수 있는 실력이 되지 않는 다는걸 다시 한번 뼈저리게 느꼈습니다.
코드를 처음 작성할 때 비동기를 작성해야하나...?
했지만 비동기 작성으로 할 필요없이 배운것으로만으로도 충분히 할 수 있었습니다.
또한, 과제를 쉽게 해결하기 위해서 코드를 주어줬을 때 그 코드 안에 많은 힌트들이 있었습니다.
가장 막혔던 부분은 내가 작성한 console.log가 console.clear() 때문에 더이상 보이지 않고 넘어갔는데 그부분에 선택지를 주어서 코드가 나타나게 한 다음 여러 선택지를 주어 코드가 뜨는 방식으로 해결할 수 있었습니다.
또한, 각 Player와 Monster class를 만들면서 어떤 스탯을 어떻게 사용해야할지 그리고 어떻게 연동하면 좋을지에 대해서 공부 할 수있는 좋은 경험이 되었습니다.
'JavaScript' 카테고리의 다른 글
개인 과제를 하던 도중 비동기를 구현 할 줄 몰라서 비동기에 대해 다시 공부했다... (0) | 2024.08.21 |
---|---|
가장 많이 받은 선물 -JS (0) | 2024.08.20 |
Math.min(Array)를 넣었더니 NaN이 나왔다 (0) | 2024.08.19 |
클로저 (Closure) (0) | 2024.08.16 |
(CallBack)콜백 함수의 콜백 지옥과 비동기 제어 (2) | 2024.08.14 |