feat(engine): 엔진 서비스 개선 및 테스트 캐릭터 서비스 추가

- ProgressService 로직 개선
- RewardService 확장
- CombatCalculator, ItemService 정리
- TestCharacterService 추가
This commit is contained in:
JiWoong Sul
2026-01-12 20:02:45 +09:00
parent d23dcd1e6f
commit a1d22369cb
5 changed files with 434 additions and 70 deletions

View File

@@ -6,6 +6,7 @@ import 'package:asciineverdie/src/core/engine/combat_calculator.dart';
import 'package:asciineverdie/src/core/engine/game_mutations.dart';
import 'package:asciineverdie/src/core/engine/potion_service.dart';
import 'package:asciineverdie/src/core/engine/reward_service.dart';
import 'package:asciineverdie/src/core/engine/shop_service.dart';
import 'package:asciineverdie/src/core/engine/skill_service.dart';
import 'package:asciineverdie/src/core/model/combat_event.dart';
import 'package:asciineverdie/src/core/model/combat_state.dart';
@@ -13,6 +14,7 @@ import 'package:asciineverdie/src/core/model/combat_stats.dart';
import 'package:asciineverdie/src/core/model/equipment_item.dart';
import 'package:asciineverdie/src/core/model/equipment_slot.dart';
import 'package:asciineverdie/src/core/model/game_state.dart';
import 'package:asciineverdie/src/core/model/item_stats.dart';
import 'package:asciineverdie/src/core/model/monster_combat_stats.dart';
import 'package:asciineverdie/src/core/model/monster_grade.dart';
import 'package:asciineverdie/src/core/model/potion.dart';
@@ -514,12 +516,15 @@ class ProgressService {
return (progress: progress, queue: queue);
}
// 2. kill 태스크가 아니었고 heading도 아니면 heading 또는 buying 태스크 실행
// (원본 670-677줄)
if (oldTaskType != TaskType.kill && oldTaskType != TaskType.neutral) {
// Gold가 충분하면 장비 구매 (원본 671-673줄)
// 2. kill/heading/buying 태스크가 아니었으면 heading 또는 buying 태스크 실행
// (원본 670-677줄) - buying 완료 후 무한 루프 방지
if (oldTaskType != TaskType.kill &&
oldTaskType != TaskType.neutral &&
oldTaskType != TaskType.buying) {
// Gold가 충분하면 장비 구매 (Common 장비 가격 기준)
// 실제 구매 가격과 동일한 공식 사용: level * 50
final gold = _getGold(state);
final equipPrice = _equipPrice(state.traits.level);
final equipPrice = state.traits.level * 50; // Common 장비 1개 가격
if (gold > equipPrice) {
final taskResult = pq_logic.startTask(
progress,
@@ -966,8 +971,12 @@ class ProgressService {
final nextLevel = state.traits.level + 1;
final rng = state.rng;
final hpGain = state.stats.con ~/ 3 + 1 + rng.nextInt(4);
final mpGain = state.stats.intelligence ~/ 3 + 1 + rng.nextInt(4);
// HP/MP 증가량 (PlayerScaling 기반 + 랜덤 변동)
// 기존: CON/3 + 1 + random(0-3) → ~6-9 HP/레벨 (너무 낮음)
// 신규: 18 + CON/5 + random(0-4) → ~20-25 HP/레벨 (생존율 개선)
final hpGain = 18 + state.stats.con ~/ 5 + rng.nextInt(5);
final mpGain = 6 + state.stats.intelligence ~/ 5 + rng.nextInt(3);
var nextState = state.copyWith(
traits: state.traits.copyWith(level: nextLevel),
@@ -1078,29 +1087,48 @@ class ProgressService {
return state.inventory.gold;
}
/// 장비 가격 계산 (원본 Main.pas:612-616)
/// Result := 5 * Level^2 + 10 * Level + 20
int _equipPrice(int level) {
return 5 * level * level + 10 * level + 20;
}
/// 장비 구매 완료 처리 (원본 Main.pas:631-634)
/// 장비 구매 완료 처리 (개선된 로직)
///
/// 1순위: 빈 슬롯에 Common 장비 최대한 채우기
/// 2순위: 골드 남으면 물약 구매
GameState _completeBuying(GameState state) {
var nextState = state;
final level = state.traits.level;
final price = _equipPrice(level);
final shopService = ShopService(rng: nextState.rng);
// Gold 차감 (inventory.gold 필드 사용)
final newGold = math.max(0, state.inventory.gold - price);
var nextState = state.copyWith(
inventory: state.inventory.copyWith(gold: newGold),
);
// 1. 빈 슬롯 목록 수집
final emptySlots = <int>[];
for (var i = 0; i < Equipment.slotCount; i++) {
if (nextState.equipment.getItemByIndex(i).isEmpty) {
emptySlots.add(i);
}
}
// 장비 획득 (WinEquip)
// 원본 Main.pas:797 - posn := Random(Equips.Items.Count); (11개 슬롯)
final slotIndex = nextState.rng.nextInt(Equipment.slotCount);
nextState = mutations.winEquipByIndex(nextState, level, slotIndex);
// 2. 골드가 허용하는 한 빈 슬롯에 Common 장비 구매
for (final slotIndex in emptySlots) {
final slot = EquipmentSlot.values[slotIndex];
final item = shopService.generateShopItem(
playerLevel: level,
slot: slot,
targetRarity: ItemRarity.common,
);
final price = shopService.calculateBuyPrice(item);
// 물약 자동 구매 (남은 골드의 20% 사용)
if (nextState.inventory.gold >= price) {
nextState = nextState.copyWith(
inventory: nextState.inventory.copyWith(
gold: nextState.inventory.gold - price,
),
equipment: nextState.equipment
.setItemByIndex(slotIndex, item)
.copyWith(bestIndex: slotIndex),
);
} else {
break; // 골드 부족 시 중단
}
}
// 3. 물약 자동 구매 (남은 골드의 20% 사용)
final potionService = const PotionService();
final purchaseResult = potionService.autoPurchasePotions(
playerLevel: level,
@@ -1644,30 +1672,35 @@ class ProgressService {
required String killerName,
required DeathCause cause,
}) {
// 상실할 장비 개수 계산
final lostCount = state.equipment.equippedItems.length;
// 사망 직전 전투 이벤트 저장 (최대 10개)
final lastCombatEvents =
state.progress.currentCombat?.recentEvents ?? const [];
// 빈 장비 생성 (기본 무기만 유지)
final emptyEquipment = Equipment(
items: [
EquipmentItem.defaultWeapon(),
EquipmentItem.empty(EquipmentSlot.shield),
EquipmentItem.empty(EquipmentSlot.helm),
EquipmentItem.empty(EquipmentSlot.hauberk),
EquipmentItem.empty(EquipmentSlot.brassairts),
EquipmentItem.empty(EquipmentSlot.vambraces),
EquipmentItem.empty(EquipmentSlot.gauntlets),
EquipmentItem.empty(EquipmentSlot.gambeson),
EquipmentItem.empty(EquipmentSlot.cuisses),
EquipmentItem.empty(EquipmentSlot.greaves),
EquipmentItem.empty(EquipmentSlot.sollerets),
],
bestIndex: 0,
);
// 무기(슬롯 0)를 제외한 장착된 장비 중 1개를 제물로 삭제
// 장착된 비무기 슬롯 인덱스 수집 (슬롯 1~10 중 장비가 있는 것)
final equippedNonWeaponSlots = <int>[];
for (var i = 1; i < Equipment.slotCount; i++) {
if (state.equipment.getItemByIndex(i).isNotEmpty) {
equippedNonWeaponSlots.add(i);
}
}
// 제물로 바칠 장비 선택 및 삭제
var newEquipment = state.equipment;
final lostCount = equippedNonWeaponSlots.isNotEmpty ? 1 : 0;
if (equippedNonWeaponSlots.isNotEmpty) {
// 랜덤하게 1개 슬롯 선택
final sacrificeIndex =
equippedNonWeaponSlots[state.rng.nextInt(equippedNonWeaponSlots.length)];
final slot = EquipmentSlot.values[sacrificeIndex];
// 해당 슬롯을 빈 장비로 교체
newEquipment = newEquipment.setItemByIndex(
sacrificeIndex,
EquipmentItem.empty(slot),
);
}
// 사망 정보 생성 (전투 로그 포함)
final deathInfo = DeathInfo(
@@ -1689,7 +1722,7 @@ class ProgressService {
);
return state.copyWith(
equipment: emptyEquipment,
equipment: newEquipment,
progress: progress,
deathInfo: deathInfo,
);