Node.js

TextRPG 만들기 - 1탄

추운날_너를_기다리며 2024. 8. 22. 23:26

현재 개인과제로 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