import 'dart:math' as math; import 'package:asciineverdie/data/potion_data.dart'; import 'package:asciineverdie/src/core/model/monster_grade.dart'; import 'package:asciineverdie/src/core/model/potion.dart'; /// 물약 서비스 /// /// 물약 사용, 자동 사용 알고리즘, 인벤토리 관리 class PotionService { const PotionService(); /// 글로벌 물약 쿨타임 (1배속 기준 3초) static const int globalPotionCooldownMs = 3000; // ============================================================================ // 물약 사용 가능 여부 // ============================================================================ /// 물약 사용 가능 여부 체크 /// /// [potionId] 물약 ID /// [inventory] 물약 인벤토리 /// Returns: (사용 가능 여부, 실패 사유) (bool, PotionUseFailReason?) canUsePotion( String potionId, PotionInventory inventory, ) { // 물약 데이터 존재 체크 final potion = PotionData.getById(potionId); if (potion == null) { return (false, PotionUseFailReason.potionNotFound); } // 보유 수량 체크 if (!inventory.hasPotion(potionId)) { return (false, PotionUseFailReason.outOfStock); } return (true, null); } // ============================================================================ // 물약 사용 // ============================================================================ /// 물약 사용 /// /// [potionId] 물약 ID /// [inventory] 물약 인벤토리 /// [currentHp] 현재 HP /// [maxHp] 최대 HP /// [currentMp] 현재 MP /// [maxMp] 최대 MP PotionUseResult usePotion({ required String potionId, required PotionInventory inventory, required int currentHp, required int maxHp, required int currentMp, required int maxMp, }) { final (canUse, failReason) = canUsePotion(potionId, inventory); if (!canUse) { return PotionUseResult.failed(failReason!); } final potion = PotionData.getById(potionId)!; int healedAmount = 0; int newHp = currentHp; int newMp = currentMp; if (potion.isHpPotion) { healedAmount = potion.calculateHeal(maxHp); newHp = (currentHp + healedAmount).clamp(0, maxHp); healedAmount = newHp - currentHp; // 실제 회복량 } else if (potion.isMpPotion) { healedAmount = potion.calculateHeal(maxMp); newMp = (currentMp + healedAmount).clamp(0, maxMp); healedAmount = newMp - currentMp; // 실제 회복량 } final newInventory = inventory.usePotion(potionId); return PotionUseResult( success: true, potion: potion, healedAmount: healedAmount, newHp: newHp, newMp: newMp, newInventory: newInventory, ); } // ============================================================================ // 긴급 물약 자동 사용 // ============================================================================ /// 긴급 HP 물약 선택 /// /// 소모된 HP >= 물약 회복량이면 사용할 최적의 물약 선택 /// [currentHp] 현재 HP /// [maxHp] 최대 HP /// [inventory] 물약 인벤토리 /// [playerLevel] 플레이어 레벨 (적정 티어 판단용) Potion? selectEmergencyHpPotion({ required int currentHp, required int maxHp, required PotionInventory inventory, required int playerLevel, }) { final hpLost = maxHp - currentHp; if (hpLost <= 0) return null; // 적정 티어 계산 final targetTier = PotionData.tierForLevel(playerLevel); // 적정 티어부터 낮은 티어 순으로 검색 for (var tier = targetTier; tier >= 1; tier--) { final potion = PotionData.getHpPotionByTier(tier); if (potion != null && inventory.hasPotion(potion.id)) { final healAmount = potion.calculateHeal(maxHp); if (hpLost >= healAmount) { return potion; } } } // 적정 티어 이상도 검색 for (var tier = targetTier + 1; tier <= 5; tier++) { final potion = PotionData.getHpPotionByTier(tier); if (potion != null && inventory.hasPotion(potion.id)) { final healAmount = potion.calculateHeal(maxHp); if (hpLost >= healAmount) { return potion; } } } return null; } /// 긴급 MP 물약 선택 /// /// 소모된 MP >= 물약 회복량이면 사용할 최적의 물약 선택 Potion? selectEmergencyMpPotion({ required int currentMp, required int maxMp, required PotionInventory inventory, required int playerLevel, }) { final mpLost = maxMp - currentMp; if (mpLost <= 0) return null; // 적정 티어 계산 final targetTier = PotionData.tierForLevel(playerLevel); // 적정 티어부터 낮은 티어 순으로 검색 for (var tier = targetTier; tier >= 1; tier--) { final potion = PotionData.getMpPotionByTier(tier); if (potion != null && inventory.hasPotion(potion.id)) { final healAmount = potion.calculateHeal(maxMp); if (mpLost >= healAmount) { return potion; } } } // 적정 티어 이상도 검색 for (var tier = targetTier + 1; tier <= 5; tier++) { final potion = PotionData.getMpPotionByTier(tier); if (potion != null && inventory.hasPotion(potion.id)) { final healAmount = potion.calculateHeal(maxMp); if (mpLost >= healAmount) { return potion; } } } return null; } // ============================================================================ // 인벤토리 관리 // ============================================================================ /// 물약 드랍 추가 PotionInventory addPotionDrop( PotionInventory inventory, String potionId, [ int count = 1, ]) { return inventory.addPotion(potionId, count); } // ============================================================================ // 물약 구매 시스템 // ============================================================================ /// 물약 구매 가능 여부 체크 /// /// [potionId] 물약 ID /// [gold] 보유 골드 /// Returns: (구매 가능 여부, 실패 사유) (bool, PotionPurchaseFailReason?) canPurchasePotion( String potionId, int gold, ) { final potion = PotionData.getById(potionId); if (potion == null) { return (false, PotionPurchaseFailReason.potionNotFound); } if (gold < potion.price) { return (false, PotionPurchaseFailReason.insufficientGold); } return (true, null); } /// 물약 구매 /// /// [potionId] 물약 ID /// [inventory] 현재 물약 인벤토리 /// [gold] 보유 골드 /// [count] 구매 수량 (기본 1) PotionPurchaseResult purchasePotion({ required String potionId, required PotionInventory inventory, required int gold, int count = 1, }) { final potion = PotionData.getById(potionId); if (potion == null) { return PotionPurchaseResult.failed( PotionPurchaseFailReason.potionNotFound, ); } final totalCost = potion.price * count; if (gold < totalCost) { return PotionPurchaseResult.failed( PotionPurchaseFailReason.insufficientGold, ); } final newInventory = inventory.addPotion(potionId, count); final newGold = gold - totalCost; return PotionPurchaseResult( success: true, potion: potion, quantity: count, totalCost: totalCost, newGold: newGold, newInventory: newInventory, ); } /// 레벨에 맞는 물약 자동 구매 /// /// 골드의 일정 비율을 물약 구매에 사용 /// [playerLevel] 플레이어 레벨 /// [inventory] 현재 물약 인벤토리 /// [gold] 보유 골드 /// [spendRatio] 골드 사용 비율 (기본 20%) PotionPurchaseResult autoPurchasePotions({ required int playerLevel, required PotionInventory inventory, required int gold, double spendRatio = 0.20, }) { final tier = PotionData.tierForLevel(playerLevel); final hpPotion = PotionData.getHpPotionByTier(tier); final mpPotion = PotionData.getMpPotionByTier(tier); if (hpPotion == null && mpPotion == null) { return PotionPurchaseResult.failed( PotionPurchaseFailReason.potionNotFound, ); } // 사용 가능 골드 final spendableGold = (gold * spendRatio).floor(); if (spendableGold <= 0) { return PotionPurchaseResult.failed( PotionPurchaseFailReason.insufficientGold, ); } var currentInventory = inventory; var currentGold = gold; var totalSpent = 0; var hpPurchased = 0; var mpPurchased = 0; // HP 물약 우선 구매 (60%), MP 물약 (40%) final hpBudget = (spendableGold * 0.6).floor(); final mpBudget = spendableGold - hpBudget; // HP 물약 구매 if (hpPotion != null && hpBudget >= hpPotion.price) { final count = hpBudget ~/ hpPotion.price; final cost = count * hpPotion.price; currentInventory = currentInventory.addPotion(hpPotion.id, count); currentGold -= cost; totalSpent += cost; hpPurchased = count; } // MP 물약 구매 if (mpPotion != null && mpBudget >= mpPotion.price) { final count = mpBudget ~/ mpPotion.price; final cost = count * mpPotion.price; currentInventory = currentInventory.addPotion(mpPotion.id, count); currentGold -= cost; totalSpent += cost; mpPurchased = count; } if (totalSpent == 0) { return PotionPurchaseResult.failed( PotionPurchaseFailReason.insufficientGold, ); } return PotionPurchaseResult( success: true, potion: hpPotion ?? mpPotion, quantity: hpPurchased + mpPurchased, totalCost: totalSpent, newGold: currentGold, newInventory: currentInventory, ); } // ============================================================================ // 물약 드랍 시스템 // ============================================================================ /// 기본 물약 드랍 확률 (15%) static const double baseDropChance = 0.15; /// 레벨당 드랍 확률 증가 (0.5%씩) static const double dropChancePerLevel = 0.005; /// 최대 드랍 확률 (35%) static const double maxDropChance = 0.35; /// 물약 드랍 시도 /// /// 전투 승리 시 물약 드랍 여부 결정 및 물약 획득 /// [playerLevel] 플레이어 레벨 (드랍 확률 및 티어 결정) /// [monsterLevel] 몬스터 레벨 (티어 결정에 영향) /// [monsterGrade] 몬스터 등급 (드랍 확률 보너스) /// [inventory] 현재 물약 인벤토리 /// [roll] 0~99 범위의 난수 (드랍 확률 판정) /// [typeRoll] 0~99 범위의 난수 (HP/MP 결정) /// Returns: (업데이트된 인벤토리, 드랍된 물약 또는 null) (PotionInventory, Potion?) tryPotionDrop({ required int playerLevel, required int monsterLevel, required MonsterGrade monsterGrade, required PotionInventory inventory, required int roll, required int typeRoll, }) { // 기본 드랍 확률 계산 var dropChance = (baseDropChance + playerLevel * dropChancePerLevel).clamp( baseDropChance, maxDropChance, ); // 몬스터 등급 보너스 (Elite +5%, Boss +15%) dropChance += monsterGrade.potionDropBonus; // 몬스터 레벨 > 플레이어 레벨이면 추가 확률 (+1%/레벨 차이) if (monsterLevel > playerLevel) { dropChance += (monsterLevel - playerLevel) * 0.01; } // 최대 확률 제한 (50%) dropChance = dropChance.clamp(0.0, 0.5); final dropThreshold = (dropChance * 100).round(); // 드랍 실패 if (roll >= dropThreshold) { return (inventory, null); } // 물약 타입 결정 (60% HP, 40% MP) final isHpPotion = typeRoll < 60; // 티어 결정: max(플레이어 레벨, 몬스터 레벨) 기반 final effectiveLevel = math.max(playerLevel, monsterLevel); final tier = PotionData.tierForLevel(effectiveLevel); // 물약 선택 final Potion? potion; if (isHpPotion) { potion = PotionData.getHpPotionByTier(tier); } else { potion = PotionData.getMpPotionByTier(tier); } if (potion == null) { return (inventory, null); } // 인벤토리에 추가 final updatedInventory = inventory.addPotion(potion.id); return (updatedInventory, potion); } } /// 물약 사용 결과 class PotionUseResult { const PotionUseResult({ required this.success, this.potion, this.healedAmount = 0, this.newHp = 0, this.newMp = 0, this.newInventory, this.failReason, }); /// 성공 여부 final bool success; /// 사용한 물약 final Potion? potion; /// 실제 회복량 final int healedAmount; /// 사용 후 HP final int newHp; /// 사용 후 MP final int newMp; /// 업데이트된 인벤토리 final PotionInventory? newInventory; /// 실패 사유 final PotionUseFailReason? failReason; /// 실패 결과 생성 factory PotionUseResult.failed(PotionUseFailReason reason) { return PotionUseResult(success: false, failReason: reason); } } /// 물약 사용 실패 사유 enum PotionUseFailReason { /// 물약 없음 (데이터 없음) potionNotFound, /// 보유 물약 없음 (재고 부족) outOfStock, /// 글로벌 쿨타임 중 onCooldown, } /// 물약 구매 결과 class PotionPurchaseResult { const PotionPurchaseResult({ required this.success, this.potion, this.quantity = 0, this.totalCost = 0, this.newGold = 0, this.newInventory, this.failReason, }); /// 성공 여부 final bool success; /// 구매한 물약 final Potion? potion; /// 구매 수량 final int quantity; /// 총 비용 final int totalCost; /// 구매 후 골드 final int newGold; /// 업데이트된 인벤토리 final PotionInventory? newInventory; /// 실패 사유 final PotionPurchaseFailReason? failReason; /// 실패 결과 생성 factory PotionPurchaseResult.failed(PotionPurchaseFailReason reason) { return PotionPurchaseResult(success: false, failReason: reason); } } /// 물약 구매 실패 사유 enum PotionPurchaseFailReason { /// 물약 없음 (데이터 없음) potionNotFound, /// 골드 부족 insufficientGold, }