JavaScript

TextRPG 만들기 - 마무리

추운날_너를_기다리며 2024. 8. 23. 17:40

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. 게임 플레이 정리

  1. 스테이지 10단계를 클리어하면 게임은 종료 됩니다.
  2. 플레이어가 죽게 되면 게임은 종료 됩니다.
  3. 플레이어는 자신의 선택에 따라서 몬스터를 잡게 되면 경험치를 얻고 레벨업을 통해서 자신의 능력치를 강화합니다.
  4. 몬스터를 잡게 되면 3가지 선택중에 하나를 고르면 됩니다.

 

 

 

 

코드들이 잘 동작하는 것을 볼 수 있었습니다.

 

6. 깃허브

https://github.com/YongHyeon1231/RogueLike-TextRPG

 

7. 느낀점

이번 과제를 진행하면서 아직 과제를 받고 바로 코드를 작성할 수 있는 실력이 되지 않는 다는걸 다시 한번 뼈저리게 느꼈습니다.

코드를 처음 작성할 때 비동기를 작성해야하나...?

했지만 비동기 작성으로 할 필요없이 배운것으로만으로도 충분히 할 수 있었습니다.

또한, 과제를 쉽게 해결하기 위해서 코드를 주어줬을 때 그 코드 안에 많은 힌트들이 있었습니다.

가장 막혔던 부분은 내가 작성한 console.log가 console.clear() 때문에 더이상 보이지 않고 넘어갔는데 그부분에 선택지를 주어서 코드가 나타나게 한 다음 여러 선택지를 주어 코드가 뜨는 방식으로 해결할 수 있었습니다.

또한, 각 Player와 Monster class를 만들면서 어떤 스탯을 어떻게 사용해야할지 그리고 어떻게 연동하면 좋을지에 대해서 공부 할 수있는 좋은 경험이 되었습니다.