현재 개인과제로 TextRPG를 만들고 있습니다.
만들던 도중에 알게된 사실들에 대해서 기술하겠습니다.
아직 개발 도중이지만 어느정도 틀을 잡았습니다.
1. 전체 코드
import chalk from 'chalk';
import readlineSync from 'readline-sync';
import { getRandomNum, playerRank, randomPlay } from './util.js';
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;
}
}
class Player extends Creature {
constructor() {
// hp, maxHp, minDamage, maxDamage, armor, level
super(100, 100000, 10, 15, 5, 1);
this.damageModify();
this.firstAttack = 0;
this.secondAttack = 0;
this.defenseProbability = 80;
this.doubleAttackProbability = 25;
this.runAwayProbability = 10;
this.exp = 0;
this.maxExp = 10;
this.state = 'idle';
}
getHeal() {
this.hp += 25 * this.level;
}
doubleAttack(target) {
this.firstAttack = Math.max(this.getDamage() - target.armor);
this.secondAttack = Math.max(this.getDamage() - target.armor);
}
damageModify() {
this.minDamage = this.originDamage;
this.maxDamage += getRandomNum(5, 10);
}
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;
}
}
}
}
}
class Monster extends Creature {
constructor() {
// hp, maxHp, minDamage, maxDamage, armor, level
super(10, 10, 10, 13, 1, 1);
this.xp = 6;
this.setMonsterInfo(1);
}
setMonsterInfo(stage) {
this.level = stage;
this.maxHp = 10 * this.level + (this.level > 1 ? getRandomNum(1, stage) * stage : 0);
this.hp = this.maxHp;
this.originDamage = 10 + (this.level > 1 ? getRandomNum(2, 4) * stage : 0);
this.minDamage = this.originDamage;
this.maxDamage = this.minDamage + (this.level > 1 ? getRandomNum(2, 4) * stage : 3);
this.armor = this.armor + (this.level > 1 ? getRandomNum(1, 3) * stage : 0);
}
}
function displayStatus(stage, player, monster) {
console.log(chalk.magentaBright(`\n${'='.repeat(30)} Current Status ${'='.repeat(30)}`));
console.log(
chalk.gray(`| 현재 플레이어 명성 : ${playerRank(player.level)} Level : ${player.level} |`),
);
console.log(
chalk.cyanBright(`| Stage: ${stage} `) +
chalk.blueBright(
`| 플레이어 정보 => 체력 : ${player.hp} 공격력 : ${player.minDamage}-${player.maxDamage} 방어도 : ${player.armor} |\n`,
) +
chalk.redBright(
`| 몬스터 정보 => 체력 : ${monster.hp} 공격력 : ${monster.minDamage}-${monster.maxDamage} 방어도 : ${monster.armor} |`,
),
);
console.log(chalk.magentaBright(`${'='.repeat(90)}\n`));
}
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;
}
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(`다시는 이 용사를 볼 수 없습니다!`));
}
}
}
};
export async function startGame() {
console.clear();
const player = new Player();
let stage = 1;
while (stage <= 10) {
const monster = new Monster(stage);
await battle(stage, player, monster);
// 스테이지 클리어 및 게임 종료 조건
stage++;
}
}
2. 내부에 있는 내용을 밖에다 선언해 코드 가독성 늘리기
function playerPlay(choice, stage, player, monster) {
const logs = [];
...
return logs
}
원래라면 밑의 코드 안에 playerPlay함수 내용이들어가야 하지만 함수로 빼내서 실행하니 코드를 관리하기 편해졌습니다.
const battle = async (stage, player, monster) => {
...
playerPlay(choice, stage, player, monster).forEach((log) => logs.push(log));
...
}
바로 이 코드가 실행이 되지 않았었습니다.
그 이유는 playerPlay의 return값을 어떤 것을 반환할지에 대해서 생각을 하게 되었고 그 과정에서 바로 함수로 코딩을 하는 것은 아직 실력부족이었습니다.
따라서, 일단 battle함수안에 코드를 다 작성한 후 switch ~ case 문 정도는 밖으로 빼내는게 맞다는 생각이 들어서 const battle의 화살표함수의 인자를 다 받고 거기다 필요한 인자를 매개변수로 받는 함수인 playerPlay를 만들고 일단 복사해서 다 넣었습니다.
그러고난 후 playerPlay는 말그대로 플레이어가 게임을 한다.
즉, player가 게임을 하는 동작을 수행하는 곳을 뜻하기 때문에 내가 숫자를 골랐을 때 그 동작을 실행하는 부분을 저기다 넣어주면 되었습니다.
3. Player class안에 getExp
getExp(xp) {
this.exp += xp;
while (this.exp >= this.maxExp) {
....
while (booleanValue) {
const choice = readlineSync.question('당신의 선택은? ');
switch (Number(choice)) {
...
}
}
}
}
여기서도 이 getExp를 함수로써 이 부분이실행되면 while문에 의해서 원래 계속 battle함수에서만 돌고 있었는데 다른 함수가 실행되지 않게 했습니다.
class안의 함수도 호출되었을 때 무한루프를 넣으면 다른 것이 실행되지 않게 막을 수 있다는 방법을 알게 되었고 코드를 더 유연하게 짤 수 있었습니다.
4. 현재 문제점
1) 내가 원하는 콘솔들이 아직 깔끔하게 다 찍히지 않는다.
즉, 찍히긴 하는데 바로 console.clear(); 부분들이 실행이 되서 찍어놨던 콘솔들이 사라져 눈으로 보지 못한다.
현재 생각나는 방법 : console.clear()가 찍히기 전에 await같은걸 걸어서 다른 것을 호출하는 동안 clear가 되지 않게 만들어야 할 것 같습니다.
2) 클리어 조건을 상정하지 못했다.
현재 생각나는 방법 : 클리어 조건을 stage뿐만 아니라 플레이어 랭크 등급 그리고 마지막 보스까지 만약에 구현을 한다면 되겠지만, 보스 구현은 넘어가고 클리어 조건에 대해서 좀더 고민 후 밑의 함수에다가 넣어 주면 조금 더 게임 같이 될 것 같다.
export async function startGame() {
console.clear();
const player = new Player();
let stage = 1;
while (stage <= 10) {
const monster = new Monster(stage);
await battle(stage, player, monster);
// 스테이지 클리어 및 게임 종료 조건
stage++;
}
}
'Node.js' 카테고리의 다른 글
데이터 링크 계층에 관하여... (1) | 2024.09.03 |
---|---|
물리 계층이란? (1) | 2024.09.03 |
Node.js 용어 정리 - 2주차 (0) | 2024.08.30 |
Node.js 용어 정리 - 1주차 (0) | 2024.08.29 |
2024-08-12 (0) | 2024.08.12 |