diff --git a/lib/src/core/engine/market_service.dart b/lib/src/core/engine/market_service.dart new file mode 100644 index 0000000..092537c --- /dev/null +++ b/lib/src/core/engine/market_service.dart @@ -0,0 +1,166 @@ +import 'package:asciineverdie/data/game_text_l10n.dart' as l10n; +import 'package:asciineverdie/src/core/engine/potion_service.dart'; +import 'package:asciineverdie/src/core/engine/shop_service.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/util/deterministic_random.dart'; +import 'package:asciineverdie/src/core/util/pq_logic.dart' as pq_logic; + +/// 판매 처리 결과 +class SellResult { + const SellResult({ + required this.state, + required this.continuesSelling, + }); + + final GameState state; + final bool continuesSelling; +} + +/// 시장/판매/구매 서비스 +/// +/// ProgressService에서 분리된 시장 로직 담당: +/// - 장비 구매 +/// - 아이템 판매 +/// - 골드 관리 +class MarketService { + const MarketService({required this.rng}); + + final DeterministicRandom rng; + + /// 인벤토리에서 Gold 수량 반환 + int getGold(GameState state) { + return state.inventory.gold; + } + + /// 장비 구매 완료 처리 (개선된 로직) + /// + /// 1순위: 빈 슬롯에 Common 장비 최대한 채우기 + /// 2순위: 골드 남으면 물약 구매 + GameState completeBuying(GameState state) { + var nextState = state; + final level = state.traits.level; + final shopService = ShopService(rng: rng); + + // 1. 빈 슬롯 목록 수집 + final emptySlots = []; + for (var i = 0; i < Equipment.slotCount; i++) { + if (nextState.equipment.getItemByIndex(i).isEmpty) { + emptySlots.add(i); + } + } + + // 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); + + 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, + inventory: nextState.potionInventory, + gold: nextState.inventory.gold, + spendRatio: 0.20, + ); + + if (purchaseResult.success && purchaseResult.newInventory != null) { + nextState = nextState.copyWith( + inventory: nextState.inventory.copyWith(gold: purchaseResult.newGold), + potionInventory: purchaseResult.newInventory, + ); + } + + return nextState; + } + + /// 판매 처리 + /// + /// [state] 현재 게임 상태 + /// Returns: 업데이트된 상태와 판매 계속 여부 + SellResult processSell(GameState state) { + final taskType = state.progress.currentTask.type; + var items = [...state.inventory.items]; + var goldAmount = state.inventory.gold; + + // sell 태스크 완료 시 아이템 판매 (원본 Main.pas:636-643) + if (taskType == TaskType.sell) { + // 첫 번째 아이템 찾기 (items에는 Gold가 없음) + if (items.isNotEmpty) { + final item = items.first; + final level = state.traits.level; + + // 가격 계산: 수량 * 레벨 + var price = item.count * level; + + // " of " 포함 시 보너스 (원본 639-640) + if (item.name.contains(' of ')) { + price = + price * + (1 + pq_logic.randomLow(rng, 10)) * + (1 + pq_logic.randomLow(rng, level)); + } + + // 아이템 삭제 + items.removeAt(0); + + // Gold 추가 (inventory.gold 필드 사용) + goldAmount += price; + } + } + + // 판매할 아이템이 남아있는지 확인 + final hasItemsToSell = items.isNotEmpty; + + if (hasItemsToSell) { + // 다음 아이템 판매 태스크 시작 + final nextItem = items.first; + final translatedName = l10n.translateItemNameL10n(nextItem.name); + final itemDesc = l10n.indefiniteL10n(translatedName, nextItem.count); + final taskResult = pq_logic.startTask( + state.progress, + l10n.taskSelling(itemDesc), + 1 * 1000, + ); + final progress = taskResult.progress.copyWith( + currentTask: TaskInfo(caption: taskResult.caption, type: TaskType.sell), + currentCombat: null, // 비전투 태스크이므로 전투 상태 초기화 + ); + return SellResult( + state: state.copyWith( + inventory: state.inventory.copyWith(gold: goldAmount, items: items), + progress: progress, + ), + continuesSelling: true, + ); + } + + // 판매 완료 - 인벤토리 업데이트만 하고 다음 태스크로 + return SellResult( + state: state.copyWith( + inventory: state.inventory.copyWith(gold: goldAmount, items: items), + ), + continuesSelling: false, + ); + } +}