Rogue-Like Text Game

Bzarre·2024년 9월 25일

TIL

목록 보기
6/8

전투 기획

  1. 플레이어에게는 여러 가지 선택지가 주어진다.
    (공격, 연속공격, 방어, 도망 등…)
    1. 공격 - 100% 확률로 공격 성공, 몬스터에게 피격
    2. 연속공격 - 일정 확률로 최대 10번까지의 공격 성공, 몬스터에게 피격
    3. 흡혈 - 100% 확률로 공격 성공, 몬스터에게 피격, 데미지의 60% 회복
    4. 반격 - 일정 확률로 반격, 몬스터에게 2배의 공격력으로 반격
    5. 도망 - 일정 확률로 해당 스테이지 클리어

  2. 플레이어의 공격력은 최소공격력과 최대공격력이 존재하며, 최소공격력은 정수이고 최대공격력은 플레이어가 가지고 있는 최대 공격력 배율에 따라 달라진다.

  3. 스테이지 클리어 시, 아래의 능력치 중 하나가 정해진 수치내에서 랜덤으로 증가한다. 증가하는 능력치는 아래와 같다.

    • 체력
    • 공격력
    • 돌파 확률
    • 연속 공격 확률
    • 반격 확률
  4. 스테이지 클리어 시, 체력이 일정수치 회복된다.

  5. 기본 전투형태는 턴제 형식으로 진행된다.

  6. 스테이지가 진행될 수록 몬스터의 체력과 공격력도 강해진다.


진행

타이틀 페이지

// 유저 입력을 받아 처리하는 함수
function handleUserInput() {
    const choice = readlineSync.question('입력: ');

    switch (choice) {
        case '1':
            console.log(chalk.green('게임을 시작합니다.'));
            // 여기에서 새로운 게임 시작 로직을 구현
            startGame();
            break;
        case '2':
            console.log(chalk.yellow('구현 준비중입니다.. 게임을 시작하세요'));
            // 업적 확인하기 로직을 구현
            handleUserInput();
            break;
        case '3':
            console.log(chalk.blue('구현 준비중입니다.. 게임을 시작하세요'));
            // 옵션 메뉴 로직을 구현
            handleUserInput();
            break;
        case '4':
            console.log(chalk.red('게임을 종료합니다.'));
            // 게임 종료 로직을 구현
            process.exit(0); // 게임 종료
            break;
        default:
            console.log(chalk.red('올바른 선택을 하세요.'));
            handleUserInput(); // 유효하지 않은 입력일 경우 다시 입력 받음
    }
}

// 게임 시작 함수
function start() {
    displayLobby();
    handleUserInput();
}

// 게임 실행
start();

플레이어 캐릭터

능력치 설정

class Player {
  constructor(hp, dmg, rech, actch, ptch) {
    this.hp = hp; 	//체력
    this.dmg = dmg; //공격력
    this.rech = 33;	//연속공격(REpeat CHance)
    this.actch = 67;//반격(reACT CHance)
    this.ptch = 50;	//돌파(PassThrough CHance)
  }

최대 대미지는 공격력의 1.7배로 설정하여 가하는 대미지는 공격력 ~ 공격력의 1.7배 사이의 자연수 값을 난수로 생성하였다

스테이지 클리어 보상

  reward() {
    let random = Math.round(Math.random() * 100);

    if (random >= 75) {
      console.log(chalk.cyanBright('능력치 상승!'))
      this.hp += random;
      this.dmg += Math.round(Math.random() * this.dmg);
      this.rech += Math.round(Math.random() * 3);
      this.actch += Math.round(Math.random() * 6);
      this.ptch += Math.round(Math.random() * 5);
    }
  }

25%의 확률로 모든 능력치가 상승하도록 하였는데 직관성을 위해 >= 75 대신 <= 25가 나았을 것이라는 생각이 든다

캐릭터 선택지

  • 통상공격
  attack(target) {
    this.deal = Math.round((Math.random() * (Math.round((this.dmg * 1.7)) - this.dmg) + this.dmg));
    target.hp -= this.deal;
  }
  • 연속공격
  chain(target) {
    this.deal = Math.round((Math.random() * (Math.round((this.dmg * 1.7)) - this.dmg) + this.dmg));
    while (this.count <= 10) {
      if (this.rech >= Math.random() * 100) {
        this.count++;
      } else
        break;
    }

    this.temp = this.count * this.deal;
    target.hp -= this.temp;
  }

추후 횟수를 로그에 남기기 위해 this.count를 사용하였지만 클래스에 중요하지 않은 요소를 추가하는 것보다는 count를 내부 변수로 설정하고 return count로 반환하는 것이 나은 방안이었다

  • 흡혈 공격
  drain(target) {
    this.deal = Math.round((Math.random() * (Math.round((this.dmg * 1.7)) - this.dmg) + this.dmg));
    this.heal = Math.round(this.deal * 0.6);

    target.hp -= this.deal;
    this.hp += this.heal;
  }
  • 반격
  react(target) {
    this.deal = Math.round((Math.random() * (Math.round((this.dmg * 1.7)) - this.dmg) + this.dmg));
    this.temp = 0;
    if (this.actch >= Math.random() * 100) {
      this.temp = this.deal * 2;
      target.hp -= this.temp;
    }
  }
  • 돌파
  passthrough(target) {
    if (this.ptch >= Math.random() * 100) {
      target.hp = 0;
    }
  }

피해량을 구해야 할 때 매번
'this.deal = Math.round((Math.random() (Math.round((this.dmg 1.7)) - this.dmg) + this.dmg));' 을 사용하여 보기에 좋지 않은듯하여 함수화하는 것이 좋을 듯하다

몬스터

class Monster {
  constructor(hp, dmg) {
    this.hp = hp;
    this.dmg = dmg;
    this.deal = Math.round((Math.random() * (Math.round(this.dmg * 1.3) - this.dmg) + this.dmg));
  }

  attack(target) {    // 몬스터의 공격
    target.hp -= this.deal;
  }
}

일반 공격만 존재하게 생성하였다

스테이터스 창

function displayStatus(stage, player, monster) {
  console.log(chalk.magentaBright(`\n=== Current Status ===`));
  console.log(
    chalk.cyanBright(`| Stage: ${stage} `) +
    chalk.blueBright(
      `| Player HP: ${player.hp}, DMG: ${player.dmg} ~ ${Math.round(player.dmg * 1.7)} `,
    ) +
    chalk.redBright(
      `| Monster HP: ${monster.hp}, DMG: ${monster.dmg} ~ ${Math.round(monster.dmg * 1.3)} |`,
    ),
  );
  console.log(chalk.magentaBright(`=====================\n`));
}

현재 스테이지, 캐릭터 HP와 데미지 범위, 몬스터 HP와 데미지 범위를 출력한다

전투

const battle = async (stage, player, monster) => {
  let logs = [];

  while (player.hp > 0 && monster.hp > 0) {
    console.clear();
    displayStatus(stage, player, monster);

    console.log(chalk.gray(`${stage}층 몬스터를 마주했습니다.`))

    logs.forEach((log) => console.log(log));

    console.log(
      chalk.green(
        `\n1.공격 2.연속공격(${player.rech}%) 3.흡혈공격(DMG의 60%) 4.반격(${player.actch}%) 5.돌파(${player.ptch}%)`
      )
    );
    const choice = readlineSync.question('선택: ');

    // 플레이어의 선택에 따라 다음 행동 처리
    logs.push(chalk.green(`${choice}를 선택하셨습니다.`));

    switch (choice) {
      case '1':
        player.attack(monster);
        logs.push(chalk.blue(`플레이어의 공격! ${player.deal}의 데미지!`));
        monster.attack(player);
        logs.push(chalk.red(`몬스터의 공격! ${monster.deal}의 데미지!`));
        break;
      case '2':
        player.temp = 0;
        player.count = 0;
        player.chain(monster);
        logs.push(chalk.blue(`플레이어의 ${player.count}회 연속공격! 총 ${player.temp}의 데미지!`));
        monster.attack(player);
        logs.push(chalk.red(`몬스터의 공격! ${monster.deal}의 데미지!`));
        break;
      case '3':
        player.drain(monster);
        logs.push(chalk.blue(`플레이어의 흡혈 공격! ${player.deal}의 데미지! ${player.heal} 회복`));
        monster.attack(player);
        logs.push(chalk.red(`몬스터의 공격! ${monster.deal}의 데미지!`));
        break;
      case '4':
        player.react(monster);
        if (player.temp === 0) {
          monster.attack(player);
          logs.push(chalk.red(`몬스터의 공격! ${monster.deal}의 데미지!`));
        } else
          logs.push(chalk.blue(`플레이어의 반격! ${player.temp}의 데미지!`));
        break;
      case '5':
        monster.attack(player);
        logs.push(chalk.red(`몬스터의 공격! ${monster.deal}의 데미지!`));
        if (player.hp > 0)
          player.passthrough(monster);
        break;
    }
  }
};

선택지 외의 값을 입력하여도 문제는 발생하지 않지만 편의를 위해 예외 처리를 하여 오류를 출력하는 것이 친화적이였다
또한 반격을 성공하였을 때를 제외하고는 몬스터는 항상 공격하기 때문에 반격시에만 예외처리를 하고 항상 공격하도록 하면 몬스터 코드가 분리되어 분별하기 쉬워진다

개시 및 종료

export async function startGame() {
  console.clear();
  const player = new Player(100, 7);
  let stage = 1;

  while (stage <= 10) {
    const monster = new Monster(stage * 100 + Math.round(Math.random() * stage) * 25, stage * 5 + Math.round(Math.random() * stage));
    await battle(stage, player, monster);

    // 스테이지 클리어 및 게임 종료 조건
    if (monster.hp <= 0) {
      console.log(chalk.cyan('전투가 종료되었습니다'))
      player.reward();
      player.hp += 50;
      readlineSync.question('입력하여 계속...')
      stage++;
    } else if (player.hp <= 0) {
      console.log(chalk.red('죽었습니다'))
      console.log(chalk.redBright('GAME OVER'));
      readlineSync.question(chalk.yellow('끝내기'))
      break;
    }
  }
  if (stage > 10)
    console.log(chalk.yellowBright('승리!'))
}

몬스터의 스테이터스 설정은 현재 스테이지의 100배를 기본 체력으로 설정하고 스테이지마다 추가 체력의 상한이 최대 25씩 증가하며 추가 체력은 랜덤성을 부여하였고 공격력 역시 스테이지 5배를 기본으로 설정하고 스테이지만큼의 추가 공격력을 획득 할 수 있도록 설정하였다
추가로 스테이지를 돌파하면 체력을 50 회복하도록 설정하였다

힘들었던 부분

전투에서 데미지 로그를 전투 함수에서 console.log를 추가하지 않고 선택지 내에서 체력변동 후 바로 로그를 출력하도록 설정하여 불러오기만 하면 출력되도록해 전투 함수의 길이를 줄이고 싶었으나 실패하여 함수부분에 하게 되었다.
Project GitHub Link

0개의 댓글