[2024.08.23 TIL] 로그라이크 텍스트 게임

박지영·2024년 8월 23일
0

Today I Learned

목록 보기
30/67

추가 기능 구현

어제는 아이템을 단순히 확률로 드랍하고 장착하는 것 까지만 했지만

오늘은 아이템을 교체하고 능력치를 상태창에 표시하고 이름을 여러개로 만들도록

해볼 예정이다.

리팩토링

로거 파일 분리

  • 로그와 관련된 함수를 따로 파일로 분류해서 관리
    • loggers.js 생성
import chalk from 'chalk';

// 현재 상태 창
export function displayStatus(stage, player, monster) {
  console.log(chalk.magentaBright(`\n=== Current Status ===`));
  console.log(
    chalk.cyanBright(
      `| Stage: ${stage} | ${player.item ? player.item.name + ` =` : ''}`,
      `${player.item ? Object.keys(player.item.stat) + ` +` : ''}`,
      `${player.item ? Object.values(player.item.stat) : ''} \n`,
    ) +
      chalk.blueBright(
        `| player  체력 = ${player.hp} 공격력 = ${player.damage} ~ ${Math.round(player.damage * player.maxDamageMag)}`,
        ` 방어력 = ${player.defense} 치명타 확률 = ${player.criticalChance}`,
        ` 최대 공격력 배율 = ${player.maxDamageMag} |`,
      ) +
      chalk.redBright(
        `\n| monster 체력 = ${monster.hp} 공격력 = ${monster.damage} ~ ${Math.round(monster.damage * monster.maxDamageMag)}`,
        ` 방어력 = ${monster.defense} 치명타 확률 = ${monster.criticalChance}`,
        ` 최대 공격력 배율 = ${monster.maxDamageMag} |`,
      ),
  );
  console.log(chalk.magentaBright(`=====================\n`));
}


// 플레이어 로그
export function handlePlayerLog(turnCnt, result, logs) {
  logs.push(
    chalk.green(
      `[${turnCnt}] 몬스터에게 ${result[1] ? '치명타로' : ''} ${result[0]}의 피해를 입혔습니다!`,
    ),
  );
}

// 몬스터 로그
export function handleMonsterLog(turnCnt, result, logs) {
  logs.push(
    chalk.red(
      `[${turnCnt}] 플레이어가 ${result[1] ? '치명타로' : ''} ${result[0]}의 피해를 입었습니다!`,
    ),
  );
}

플레이어 클래스 보상 로직 간소화

  • 기존 로직
// 스테이지 클리어 보상
  /**
   * 스테이지 클리어 시 기본 능력치 상승 + 랜덤 능력치 (보상 테이블 중 한 개) 상승
   * @param {number} stage
   * @returns
   */
  reward(stage) {
    // 기본 보상
    this.damage += Math.round(stage / 2);
    this.defense += Math.floor(stage / 2);

    // 랜덤 추가 보상
    const rewardTable = this.rewardTable[Math.floor(Math.random() * this.rewardTable.length)];
    const amount = this.randomReward(rewardTable);
    this[rewardTable] += amount;
    switch (rewardTable) {
      // 체력
      case 'hp':
        // 20 ~ 50
        const hp = 20 + Math.round(Math.random() * 30);
        this.hp += hp;
        return { type: '체력', amount: hp };
      // 최소 공격력
      case 'damage':
        // 1 ~ 10
        const damage = 1 + Math.round(Math.random() * 10);
        this.damage += damage;
        return { type: '최소 공격력', amount: damage };
      // 최대 공격력 배율
      case 'maxDamageMag':
        // 0.1 ~ 1
        const maxDamageMag = Math.ceil(Math.random() * 100) / 100;
        this.maxDamageMag += maxDamageMag;
        return { type: '최대 공격력 배율', amount: maxDamageMag };
      // 방어 확률
      case 'counterChance':
        // 3 ~ 10
        const counterChance = 3 + Math.round(Math.random() * 7);
        this.counterChance += counterChance;
        return { type: '방어 확률', amount: counterChance };
      // 도망 확률
      case 'runChance':
        // 1 ~ 3
        const runChance = 1 + Math.round(Math.random() * 2);
        this.runChance += runChance;
        return { type: '도망 확률', amount: runChance };
      // 연속 공격 확률
      case 'doubleAttackChance':
        // 3 ~ 7
        const doubleAttackChance = 3 + Math.round(Math.random() * 4);
        this.doubleAttackChance += doubleAttackChance;
        return { type: '연속 공격 확률', amount: doubleAttackChance };
      // 방어 수치
      case 'defense':
        // 1 ~ 3
        const defense = 1 + Math.round(Math.random() * 2);
        this.defense += defense;
        return { type: '방어 수치', amount: defense };
      // 치명타 확률
      case 'criticalChance':
        // 3 ~ 7
        const criticalChance = 3 + Math.round(Math.random() * 4);
        this.criticalChance += criticalChance;
        return { type: '치명타 확률', amount: criticalChance };
    }
  }
  • 변경 로직

    • 랜덤 능력치 로직 메소드

      randomReward(table) {
       const table = {
         hp: [20, 50],
         damage: [1, 10],
         maxDamageMag: [0.1, 1],
         counterChance: [3, 10],
         runChance: [1, 3],
         doubleAttackChance: [3, 7],
         defense: [1, 3],
         criticalChance: [3, 7],
       };
      
       const [min, max] = table[type];
       return Math.round(min + Math.random() * (max - min));
      }
    • reward 메소드 간소화

       reward(stage) {
       // 기본 보상
       this.damage += Math.round(stage / 2);
       this.defense += Math.floor(stage / 2);
      
       // 랜덤 추가 보상
       const rewardTable = this.rewardTable[Math.floor(Math.random() * this.rewardTable.length)];
       const amount = this.randomReward(rewardTable);
       this[rewardTable] += amount;
       return { type: rewardTable, amount: amount };
      }

아이템 이름 생성 로직 간소화

  • 변경 전
static dropItemName(stat) {
    let name = [];
    const type = Math.floor(Math.random() * 3);

    if (stat.damage) {
      if (stat.damage < 3) name.push('녹슨');
      else if (stat.damage < 8) name.push('평범한');
      else name.push('전설적인');
    }

    if (stat.defense) {
      if (stat.defense < 3) name.push('녹슨');
      else if (stat.defense < 8) name.push('평범한');
      else name.push('전설적인');
    }
    if (stat.criticalChance) {
      if (stat.criticalChance < 15) name.push('녹슨');
      else if (stat.criticalChance < 30) name.push('평범한');
      else name.push('전설적인');
    }

    if (type === 0) name.push('도끼');
    if (type === 1) name.push('검');
    if (type === 2) name.push('창');
    return name;
  }
  • 변경 후
    • 중복된 if else 문을 함수로 변경
    • 기준 값을 배열로 넘겨 기준 값에 따라 분류
    • 무기 타입을 배열만들고 랜덤으로 하나 선정
static dropItemName(stat) {
    let name = [];
  
    function quality(amount, quality) {
      if (amount < quality[0]) return '녹슨';
      if (amount < quality[1]) return '평범한';
      return '전설적인';
    }

    if (stat.damage) name.push(quality(stat.damage, [3, 8]));
    if (stat.defense) name.push(quality(stat.damage, [3, 8]));
    if (stat.criticalChance) name.push(quality(stat.criticalChance, [15, 30]));

    const type = Math.floor(Math.random() * 3);
    const weapon = ['도끼', '검', '창'];
    name.push(weapon[type]);

    return name;
  }

아이템 기능

  • 아이템 이름 상태창에 표시
    • 이름 및 효과 표시 + 상승량
 chalk.cyanBright(
      `| Stage: ${stage} | ${player.item ? player.item.name + ` =` : ''}`,
      `${player.item ? Object.keys(player.item.stat) + ` +` : ''}`,
      `${player.item ? Object.values(player.item.stat) : ''} \n`,
    )

  • 아이템 해제 메소드
    • 플레이어의 아이템의 효과만큼 그대로 감소 + 아이템 속성 null
// 아이템 해제 메소드
  unEquipItem(player) {
    const item = player.item;
    if (item.stat.damage) player.damage -= player.item.stat.damage;

    if (item.stat.defense) player.defense -= player.item.stat.defense;

    if (item.stat.criticalChance) player.criticalChance -= player.item.stat.criticalChance;

    player.item = null;
  }
  • 아이템 드랍부터 아이템 장착 및 해제
if (Math.random() * 100 < 100) {
  const item = Item.dropItem();
  console.log(chalk.yellow(`몬스터가 ${item.name}을/를 드랍했습니다!`));
  console.log(
    chalk.yellow( `${item.name}의 효과 ${Object.keys(item.stat)} + ${Object.values(item.stat)}`,),
  );
  let choice;
  do {
    choice = readlineSync.question(
      ` ${player.item ? '1. 교체한다.' + '2. 교체하지 않는다.' : '1. 장착한다.' + '2. 장착하지 않는다.'} `,
    );

    switch (choice) {
      case '1':
        console.log(chalk.green(`${item.name}을/를 장착했습니다!`));
        if (player.item) item.unEquipItem(player);

        item.equipItem(player, item);
        break;
      case '2':
        console.log(chalk.red(`${item.name}을/를 포기했습니다.`));
        break;
      default:
        console.log(chalk.red('올바른 선택을 입력해주세요'));
        break;
    }
  } while (choice !== '1' && choice !== '2');
}



  • 아이템 이름 생성 메소드
    • 능력치 수치에 따라 녹슨, 평범한, 전설적인으로 나뉘고 무기 유형을 도끼, 검, 창으로 구분
static dropItemName(stat) {
    let name = [];
  
    function quality(amount, quality) {
      if (amount < quality[0]) return '녹슨';
      if (amount < quality[1]) return '평범한';
      return '전설적인';
    }

    if (stat.damage) name.push(quality(stat.damage, [3, 8]));
    if (stat.defense) name.push(quality(stat.damage, [3, 8]));
    if (stat.criticalChance) name.push(quality(stat.criticalChance, [15, 30]));

    const type = Math.floor(Math.random() * 3);
    const weapon = ['도끼', '검', '창'];
    name.push(weapon[type]);

    return name;
  }

트러블 슈팅

랜덤 능력치 로직 문제

  • 현상 - 최대 공격력 배율 속성이 0.1 ~ 1 사이의 값이라
    Math.round 함수에 의해 0 or 1만 나오는 현상

  • 접근

    • 최대 공격력 배율만 따로 계산한다.

      if (table == 'maxDamageMag') {
        return (min + Math.random() * (max - min)).toFixed(2);
      }
      return Math.round(min + Math.random() * (max - min));

      실수 연산을 최대한 피해왔는데 여기서 어떻게 해야할지 모르겠다.

      실수 끼리의 연산이 아니라 문제는 없겠지만 일정 소수점 까지만 사용하고 싶었다.

      구글링을 해보니 number.prototype에 toFixed 라는 함수가 있었다.

      toFixed 함수를 사용하여 소숫점 2번째 자리 까지만 사용한다.

  • toFixed()

    • 숫자를 고정 소수점 표기법으로 표시하는 함수.

    • 문자열로 반환해준다.

    • 길면 반올림하면 표현한다.

    • 짧으면 0으로 채운다.

  • 해결

    • toFixed() 가 문자열로 반환하기 때문에 혹시 모를 문제를 대비해 parseFloat로 예방한다.
    if (table == 'maxDamageMag') {
      return parseFloat((min + Math.random() * (max - min)).toFixed(2));
    }
    return Math.round(min + Math.random() * (max - min));
profile
신입 개발자

0개의 댓글