카테고리 없음

2025.04.25 3주차

mochoa 2025. 4. 25. 21:02

오늘의 학습 키워드

1. Text Rpg 만든거 복습

2. Text Rpg 유지보수

 

드디어 Textrpg가 다 끝났다.

내가 구현한 부분 

1. 전투

2. 캐릭터 스텟 

3.포션

 

1. 전투는 Scene을 3개를 나눴다.

  • DungeonSelect (주요 포인트 : WeightedRandomSelection 매서드)  
  • private MonsterData WeightedRandomSelection(List<MonsterData> monsters) { if (monsters.Count == 0) return null; if (monsters.Count == 1) return monsters[0]; // 가중치 계산 (던전 안에 레벨이 낮을수록 높은 가중치) var weights = monsters.Select(m => 1f / (m.Level + 1)).ToList(); // 전체 가중치 합을 구함 float totalWeight = weights.Sum(); //그중에 랜덤으로 포인트 하나를 뽑음. float randomPoint = (float)rnd.NextDouble() * totalWeight; //randomPoint가 현재몬스터의 가중치보다 작으면 선택 // 아니라면 다음 몬스터로 넘기되 randompoint 에서 가중치 뺌. for (int i = 0; i < monsters.Count; i++) { if (randomPoint < weights[i]) { return monsters[i]; } randomPoint -= weights[i]; } return monsters.Last(); }
  • Battle (주요 포인트 : string respond매서드에서 공격 주고받는부분 )  
  • // 전투종료 조건 체크하는 함수 private string? CheckBattleEnd() { // 모든 몬스터가 죽은 경우 (승리) if (gameContext.currentBattleMonsters!.All(m => m.HP <= 0)) { // 보상 계산 (죽은 몬스터만) var deadMonsters = gameContext.currentBattleMonsters!.Where(m => m.HP <= 0).ToList(); //클리어 보상용으로 쓰러진 몬스터 정보 저장 gameContext.clearedMonsters = deadMonsters .Select(m => new MonsterData { Name = m.Name, ExpReward = m.ExpReward, GoldReward = m.GoldReward }) .ToList(); // 전투 몬스터 리스트 초기화 ((LogView)viewMap[ViewID.Log]).AddLog("모든 몬스터를 처치했습니다!"); ((LogView)viewMap[ViewID.Log]).ClearText(); return SceneID.DungeonClear; } // 플레이어 사망 경우 (패배) if (player.hp <= 0) { return SceneID.DungeonFail; } return null; // 전투 계속 } public override string respond(int input) { var battleResult = CheckBattleEnd(); if (battleResult != null) return battleResult; //전투종료 조건부터 체크 bool actionPerformed = true; switch (input) { case 1: actionPerformed = PerformPhysicalAttack(); break; case 2: actionPerformed = PerformMagicAttack(); break; case 3: return sceneNext.next![input]; case 4: if (TryEscape()) return SceneID.DungeonSelect; break; case 5: actionPerformed = UsePotion(); break; default: Console.WriteLine("잘못된 입력입니다. 다시 선택해주세요."); Thread.Sleep(1000); return SceneID.BattleScene; } if (!actionPerformed) { // 공격 대상 선택 취소 시 턴 유지 return SceneID.BattleScene; } battleResult = CheckBattleEnd(); if (battleResult != null) return battleResult; //행동 이후 다시 종료조건 확인 foreach (var monster in gameContext.currentBattleMonsters!.Where(m => m.HP > 0).ToList()) { //살아있는 몬스터가 플레이어 공격 MonsterAttack(monster); battleResult = CheckBattleEnd(); if (battleResult != null) return battleResult; } return SceneID.BattleScene; } //몬스터가 플레이어 공격하는 매서드 private void MonsterAttack(MonsterData monster) { if (monster.HP <= 0) return; int damage = (int)(monster.Power - player.getTotalGuard()); if (damage < 0) damage = 0; player.hp -= damage; if (player.hp < 0) player.hp = 0; ((LogView)viewMap[ViewID.Log]).AddLog($"{monster.Name}가 {player.name}에게 공격! {damage} 데미지!"); if (player.hp <= 0) { ((LogView)viewMap[ViewID.Log]).AddLog("플레이어가 쓰러졌습니다. 게임 오버!"); } } // 공격할 몬스터를 선택 private MonsterData? ChooseTarget() { var aliveMonsters = gameContext.currentBattleMonsters! .Where(m => m.HP > 0) .ToList(); //살아있는 몬스터만 출력 if (aliveMonsters.Count == 0) { ((LogView)viewMap[ViewID.Log]).AddLog("공격할 수 있는 몬스터가 없습니다."); return null; } ((LogView)viewMap[ViewID.Log]).AddLog("어떤 몬스터를 공격하시겠습니까?"); ((LogView)viewMap[ViewID.Log]).AddLog("0. 취소"); for (int i = 0; i < aliveMonsters.Count; i++) { ((LogView)viewMap[ViewID.Log]).AddLog($"{i + 1}. {aliveMonsters[i].Name} (HP: {aliveMonsters[i].HP}/{aliveMonsters[i].MaxHP})"); } ((LogView)viewMap[ViewID.Log]).Update(); ((LogView)viewMap[ViewID.Log]).Render(); int choice; while (true) { if (int.TryParse(Console.ReadLine(), out choice)) { // 0은 취소 if (choice == 0) { ((LogView)viewMap[ViewID.Log]).AddLog("대상 선택이 취소되었습니다."); return null; // 호출한 쪽에서 판단하게 } if (choice > 0 && choice <= aliveMonsters.Count) { Console.Clear(); return aliveMonsters[choice - 1]; } } ((InputView)viewMap[ViewID.Input]).SetCursor(); Console.WriteLine("잘못된 선택입니다. 다시 입력하세요."); Render(); } } }
  •  DungeonFairScene (생략가능. 별로 중요하지않음)
    • DungeonClearScene
    •  
 public override void DrawScene()
    {
        ClearScene();
        List<string> dynamicText = new();

        // 디버그 정보 출력
        Console.WriteLine($"clearedMonsters 개수: {gameContext.clearedMonsters?.Count}");

        if (gameContext.clearedMonsters != null && gameContext.clearedMonsters.Count > 0)
        {
            dynamicText.Add("축하합니다!!");
            dynamicText.Add($"{gameContext.enteredDungeon!.Name}을 클리어 하였습니다");
            dynamicText.Add("");

            int totalExp = gameContext.clearedMonsters.Sum(m => m.ExpReward);
            int totalGold = gameContext.clearedMonsters.Sum(m => m.GoldReward);

            // 실제 보상 적용
            gameContext.ch.Exp += totalExp;            // 총 경험치 누적 (기록용)
            gameContext.ch.CurrentExp += totalExp;     // 실제 레벨업 계산에 사용됨
            gameContext.ch.getLevel();                 // 레벨업 처리 (CurrentExp, MaxExp 반영)
            gameContext.ch.gold += totalGold;

            gameContext.curGold = gameContext.ch.gold;
            gameContext.curHp = gameContext.ch.hp;
            gameContext.curMp = gameContext.ch.Mp;

            dynamicText.Add($"획득 경험치: {totalExp} EXP");
            dynamicText.Add($"획득 골드: {totalGold} G");
            dynamicText.Add("");
            dynamicText.Add("[탐험 결과]");
            dynamicText.Add($"체력 {gameContext.prevHp} -> {gameContext.curHp}");
            dynamicText.Add($"체력 {gameContext.prevMp} -> {gameContext.curMp}");
            dynamicText.Add($"골드 {gameContext.prevGold}G -> {gameContext.curGold}G");

            gameContext.ch.clearCount++;
        }

        // 뷰 업데이트
        ((DynamicView)viewMap[ViewID.Dynamic]).SetText(dynamicText.ToArray());
        Render();
    }


    public override string respond(int i)
    {
        convertSceneAnimationPlay(sceneNext.next![i]);
        return sceneNext.next![i];
    }
}
  • DungeonFairScene}
public override void DrawScene()
    {
        ClearScene();

        List<string> dynamicText = new();
            dynamicText.Add($"{gameContext.enteredDungeon!.Name} 클리어를 실패했습니다.");
            dynamicText.Add("\\n");
            dynamicText.Add("[탐험 결과]");
            gameContext.ch.hp = 0;
        dynamicText.Add($"체력 {gameContext.prevHp} -> {gameContext.ch.hp}");
        dynamicText.Add($"체력 {gameContext.prevMp} -> {gameContext.ch.Mp}");

        ((DynamicView)viewMap[ViewID.Dynamic]).SetText(dynamicText.ToArray());
        //((SpriteView)viewMap[ViewID.Sprite]).SetText(sceneText.spriteText!);

        Render();
    }

    public override string respond(int i)
    {
        convertSceneAnimationPlay(sceneNext.next![i]);
        return sceneNext.next![i];
    }
}

 

 

 

2.Character State 생성,관리

  • CharacterBase 생성해서 다른 스탯이 생기더라도 참조 쉽도록 생성(나머지에 다 상속시킴)}
// 스탯 정보
    public Dictionary<string, int> Stats { get; protected set; }

    // 전투 관련 속성
    public float attack { get; set; }
    public float guard { get; set; }
    public int hp { get; set; }
    public int Str { get; set; }
    public int Int { get; set; }
    public int Dex { get; set; }
    public int Luk { get; set; }
    public int MaxHp { get; set; }
    public int Mp { get; set; }
    public int MaxMp { get; set; }
    public int Exp { get; set; }
    public int CurrentExp { get; set; }
    public int MaxExp => (int)(100 * Math.Pow(1.2, Level - 1));
    public int Point { get; set; }
    public float critical { get; set; }


    // 기본 공격력/방어력
    public float defaultAttack { get; set; }
    public float defaultGuard { get; set; }

 

  • Character Character (주요 포인트 : getLevel)
public int getLevel()
    {
    //현재 경험치가 레벨업 조건( 최대경험치 이상인 경우에만 조건문 반복 )
        while (CurrentExp >= MaxExp)
        {
		        // 레벨업에 소모함ㄴ 경험치만큼 CurrentExp 줄임.
            CurrentExp -= MaxExp;
            //레벨업, 포인트 상승
            Level++;
            Point += 3;

            Console.WriteLine($"레벨업! 현재 레벨: {Level}, 포인트: {Point}");
            Console.WriteLine($"현재 EXP: {CurrentExp} / {MaxExp}");
        }
        return Level;
    }
  •  FirstStatsCreater(주요 포인트 : shuffleStats, RandomStats)
public void ShuffleStats()
    {
		    //배열에 담은후스탯 4개의 값은 유지하면서 위치만 랜덤으로 섞음.
        int[] stats = { Str, Int, Dex, Luk };
        //배열의 길이만큼 반복
        for (int i = 0; i < stats.Length; i++)
        {
		        // i부터 배열 끝까지 무작위 j 선택
		        //(Fisher-Yates 셔플 알고리즘 기반)
            int j = rnd.Next(i, stats.Length);
            int temp = stats[i];
            stats[i] = stats[j];
            //stats i값과 j 값을 교환
            stats[j] = temp;
            // 이 과정을 통해 전체 배열이 무작위로 섞이게됨.
        }
				//섞인 배열들을 원래 변수들에 저장
        Str = stats[0];
        Int = stats[1];
        Dex = stats[2];
        Luk = stats[3];
    }

    private void RandomStats()
    {
    
		    //statLimit(최대 스텟) 을 reamining에 저장
        int remaining = statLimit;
        //0부터 remaining+1 까지 무작위 값 Str에 넣기
        Str = rnd.Next(0, remaining + 1);
        // reamining 에서 Str
        //remaining -= Str 값 빼기
        Int = rnd.Next(0, remaining + 1);
        //0부터 remaining+1 까지 무작위 값 Str에 넣기
        remaining -= Int;
        Dex = rnd.Next(0, remaining + 1);
        remaining -= Dex;
        Luk = rnd.Next(0, remaining + 1);
        remaining -= Luk;
				//마지막까지 분배된값들을 무작위로 재배치까지
        ShuffleStats();
    }

 

3. 포션

    private bool UsePotion()
    {
        var potions = gameContext.shop.items
    .Where(i => i.isPotion && i.quantity > 0)
    .ToList();

        if (potions.Count == 0)
        {
            ((LogView)viewMap[ViewID.Log]).AddLog("사용할 수 있는 포션이 없습니다.");
            return false;
        }

        List<string> potionMenu = new List<string> { "사용할 포션을 선택하세요:" };
        for (int i = 0; i < potions.Count; i++)
        {
            potionMenu.Add($"{i + 1}. {potions[i].name} (남은 수량: {potions[i].quantity})");
        }
        potionMenu.Add("0. 취소");

        ((DynamicView)viewMap[ViewID.Dynamic]).SetText(potionMenu.ToArray());
        Render();

        int choice;
        while (true)
        {
            Console.Write("선택: ");
            if (int.TryParse(Console.ReadLine(), out choice))
            {
                if (choice == 0) return false;

                if (choice >= 1 && choice <= potions.Count)
                {
                    var selectedPotion = potions[choice - 1];
                    int healAmount = 0;
                    int manaAmount = 0;

                    if (selectedPotion.key == "HealPotion")
                    {
                        healAmount = 100;
                    }
                    else if (selectedPotion.key == "ManaPotion")
                    {
                        manaAmount = 100;
                    }

                    int beforeHp = player.hp;
                    int beforeMp = player.Mp;
                    player.hp = Math.Min(player.MaxHp, player.hp + healAmount);
                    player.Mp = Math.Min(player.MaxMp, player.Mp + manaAmount);

                    selectedPotion.quantity--;
                    ((LogView)viewMap[ViewID.Log]).AddLog($"{selectedPotion.name} 사용! {player.hp - beforeHp} 회복!");

                    if (selectedPotion.quantity <= 0)
                    {
                        gameContext.shop.items.Remove(selectedPotion);
                    }

                    return true;
                }
            }

((LogView)viewMap[ViewID.Log]).AddLog("잘못된 입력입니다.");
        }

    }

 

 

고민 했던 부분 and 해결

  1. 예전 메이플 스토리 주사위 굴리는 것처럼 스탯을 만들고 싶은데 어떻게 해야 그걸 구현 할 수 있지?
    • ShuffleStats,RandomStats 사용
  2. 던전에 들어가면 몬스터가 랜덤적으로 여러 마리 나오게 하고 싶은데 어떻게 구현 할 수 있지?
    • WeightedRandomSelection 매서드 사용해서 약한 몬스터 많이나오게하기
  3. 몬스터의 턴과 플레이어 턴을 어떻게 구분하지?
    • string respond에서 BattleEnd사용해서 전투 끝날때마다 하는 행동에 대해 넣어야겠다.
  4. 캐릭터 스탯에 특정값만 출력되면 모든 클래스가 영향을 받게하고싶은데 어떻게하지?
    • 상속 사용

 

ps.이러한 것들 위주로 구현을 하다보니 시간에 쫓겨서 TIL 작성을 미뤄뒀던게 기억난다...

그냥 앞으로는 30분정도 남기고 써야지...