오늘의 학습 키워드
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 해결
- 예전 메이플 스토리 주사위 굴리는 것처럼 스탯을 만들고 싶은데 어떻게 해야 그걸 구현 할 수 있지?
- ShuffleStats,RandomStats 사용
- 던전에 들어가면 몬스터가 랜덤적으로 여러 마리 나오게 하고 싶은데 어떻게 구현 할 수 있지?
- WeightedRandomSelection 매서드 사용해서 약한 몬스터 많이나오게하기
- 몬스터의 턴과 플레이어 턴을 어떻게 구분하지?
- string respond에서 BattleEnd사용해서 전투 끝날때마다 하는 행동에 대해 넣어야겠다.
- 캐릭터 스탯에 특정값만 출력되면 모든 클래스가 영향을 받게하고싶은데 어떻게하지?
- 상속 사용
ps.이러한 것들 위주로 구현을 하다보니 시간에 쫓겨서 TIL 작성을 미뤄뒀던게 기억난다...
그냥 앞으로는 30분정도 남기고 써야지...