import 'package:asciineverdie/data/potion_data.dart'; import 'package:asciineverdie/src/core/engine/potion_service.dart'; import 'package:asciineverdie/src/core/model/monster_grade.dart'; import 'package:asciineverdie/src/core/model/potion.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { const service = PotionService(); // ============================================================================ // 물약 사용 가능 여부 (canUsePotion) // ============================================================================ group('canUsePotion', () { test('보유 물약이 있으면 사용 가능', () { final inventory = const PotionInventory().addPotion( PotionData.minorHealthPatch.id, 3, ); final (canUse, reason) = service.canUsePotion( PotionData.minorHealthPatch.id, inventory, ); expect(canUse, isTrue); expect(reason, isNull); }); test('존재하지 않는 물약 ID는 potionNotFound', () { final (canUse, reason) = service.canUsePotion( 'nonexistent_potion', PotionInventory.empty, ); expect(canUse, isFalse); expect(reason, PotionUseFailReason.potionNotFound); }); test('보유 수량이 0이면 outOfStock', () { final (canUse, reason) = service.canUsePotion( PotionData.minorHealthPatch.id, PotionInventory.empty, ); expect(canUse, isFalse); expect(reason, PotionUseFailReason.outOfStock); }); }); // ============================================================================ // 물약 사용 (usePotion) // ============================================================================ group('usePotion', () { test('HP 물약 사용 시 HP가 회복된다', () { final potion = PotionData.minorHealthPatch; // 고정 30, 비율 0% final inventory = PotionInventory.empty.addPotion(potion.id); final result = service.usePotion( potionId: potion.id, inventory: inventory, currentHp: 50, maxHp: 100, currentMp: 30, maxMp: 50, ); expect(result.success, isTrue); expect(result.newHp, 80); // 50 + 30 = 80 expect(result.healedAmount, 30); // MP는 변하지 않음 expect(result.newMp, 30); // 인벤토리에서 수량 감소 expect(result.newInventory!.hasPotion(potion.id), isFalse); }); test('HP 물약은 maxHp를 초과하지 않는다', () { final potion = PotionData.minorHealthPatch; final inventory = PotionInventory.empty.addPotion(potion.id); final result = service.usePotion( potionId: potion.id, inventory: inventory, currentHp: 90, maxHp: 100, currentMp: 50, maxMp: 50, ); expect(result.success, isTrue); expect(result.newHp, 100); // 90 + 30 = 120 → clamp → 100 expect(result.healedAmount, 10); // 실제 회복량 = 100 - 90 }); test('MP 물약 사용 시 MP가 회복된다', () { final potion = PotionData.minorManaCache; // 고정 20, 비율 0% final inventory = PotionInventory.empty.addPotion(potion.id); final result = service.usePotion( potionId: potion.id, inventory: inventory, currentHp: 100, maxHp: 100, currentMp: 10, maxMp: 50, ); expect(result.success, isTrue); expect(result.newMp, 30); // 10 + 20 = 30 expect(result.healedAmount, 20); // HP는 변하지 않음 expect(result.newHp, 100); }); test('비율 회복(percent heal) 물약은 최대치 비례로 회복', () { final potion = PotionData.healthPatch; // 고정 50, 비율 10% final inventory = PotionInventory.empty.addPotion(potion.id); final result = service.usePotion( potionId: potion.id, inventory: inventory, currentHp: 100, maxHp: 1000, currentMp: 50, maxMp: 50, ); // 고정 50 + (1000 * 0.10) = 50 + 100 = 150 expect(result.success, isTrue); expect(result.newHp, 250); // 100 + 150 expect(result.healedAmount, 150); }); test('healingMultiplier 적용 시 회복량 증가', () { final potion = PotionData.minorHealthPatch; // 고정 30 final inventory = PotionInventory.empty.addPotion(potion.id); final result = service.usePotion( potionId: potion.id, inventory: inventory, currentHp: 50, maxHp: 200, currentMp: 50, maxMp: 50, healingMultiplier: 1.5, // +50% 회복력 보너스(bonus) ); // 30 * 1.5 = 45 expect(result.success, isTrue); expect(result.newHp, 95); // 50 + 45 expect(result.healedAmount, 45); }); test('보유 물약이 없으면 실패', () { final result = service.usePotion( potionId: PotionData.minorHealthPatch.id, inventory: PotionInventory.empty, currentHp: 50, maxHp: 100, currentMp: 30, maxMp: 50, ); expect(result.success, isFalse); expect(result.failReason, PotionUseFailReason.outOfStock); }); }); // ============================================================================ // 물약 구매 (purchasePotion) // ============================================================================ group('purchasePotion', () { test('골드가 충분하면 구매 성공', () { final potion = PotionData.minorHealthPatch; // 가격 25 final result = service.purchasePotion( potionId: potion.id, inventory: PotionInventory.empty, gold: 100, ); expect(result.success, isTrue); expect(result.totalCost, 25); expect(result.newGold, 75); expect(result.newInventory!.getQuantity(potion.id), 1); }); test('복수 구매 시 총 비용 정확 계산', () { final potion = PotionData.minorHealthPatch; // 가격 25 final result = service.purchasePotion( potionId: potion.id, inventory: PotionInventory.empty, gold: 1000, count: 5, ); expect(result.success, isTrue); expect(result.totalCost, 125); // 25 * 5 expect(result.newGold, 875); expect(result.newInventory!.getQuantity(potion.id), 5); }); test('골드 부족 시 구매 실패', () { final potion = PotionData.ultraHealthPatch; // 가격 1200 final result = service.purchasePotion( potionId: potion.id, inventory: PotionInventory.empty, gold: 100, ); expect(result.success, isFalse); expect(result.failReason, PotionPurchaseFailReason.insufficientGold); }); test('존재하지 않는 물약 ID는 구매 실패', () { final result = service.purchasePotion( potionId: 'invalid_potion', inventory: PotionInventory.empty, gold: 99999, ); expect(result.success, isFalse); expect(result.failReason, PotionPurchaseFailReason.potionNotFound); }); }); // ============================================================================ // canPurchasePotion - 구매 가능 여부 체크 // ============================================================================ group('canPurchasePotion', () { test('골드 충분 시 구매 가능', () { final (canBuy, reason) = service.canPurchasePotion( PotionData.minorHealthPatch.id, 100, ); expect(canBuy, isTrue); expect(reason, isNull); }); test('골드 부족 시 insufficientGold', () { final (canBuy, reason) = service.canPurchasePotion( PotionData.ultraHealthPatch.id, 10, ); expect(canBuy, isFalse); expect(reason, PotionPurchaseFailReason.insufficientGold); }); }); // ============================================================================ // 자동 구매 (autoPurchasePotions) // ============================================================================ group('autoPurchasePotions', () { test('레벨에 맞는 티어(tier) 물약을 자동 구매', () { final result = service.autoPurchasePotions( playerLevel: 5, // 티어 1 inventory: PotionInventory.empty, gold: 10000, ); expect(result.success, isTrue); expect(result.quantity, greaterThan(0)); expect(result.totalCost, greaterThan(0)); // 사용 골드 비율은 20% 이내 expect(result.totalCost, lessThanOrEqualTo(2000)); }); test('골드가 0이면 구매 실패', () { final result = service.autoPurchasePotions( playerLevel: 5, inventory: PotionInventory.empty, gold: 0, ); expect(result.success, isFalse); }); }); // ============================================================================ // 물약 드랍 (potion drop) // ============================================================================ group('tryPotionDrop', () { test('roll이 드랍 확률(drop chance) 미만이면 물약 드랍', () { final (inventory, potion) = service.tryPotionDrop( playerLevel: 10, monsterLevel: 10, monsterGrade: MonsterGrade.normal, inventory: PotionInventory.empty, roll: 0, // 항상 드랍 성공 typeRoll: 30, // HP 물약 (< 60) ); expect(potion, isNotNull); expect(potion!.isHpPotion, isTrue); expect(inventory.hasPotion(potion.id), isTrue); }); test('roll이 드랍 확률 이상이면 드랍 실패', () { final (inventory, potion) = service.tryPotionDrop( playerLevel: 1, monsterLevel: 1, monsterGrade: MonsterGrade.normal, inventory: PotionInventory.empty, roll: 99, // 거의 무조건 실패 typeRoll: 30, ); expect(potion, isNull); // 인벤토리 변화 없음 expect(inventory.potions, isEmpty); }); test('typeRoll >= 60이면 MP 물약 드랍', () { final (_, potion) = service.tryPotionDrop( playerLevel: 10, monsterLevel: 10, monsterGrade: MonsterGrade.normal, inventory: PotionInventory.empty, roll: 0, typeRoll: 70, // MP 물약 (>= 60) ); expect(potion, isNotNull); expect(potion!.isMpPotion, isTrue); }); test('보스(boss) 몬스터는 드랍 확률 +15%', () { // 일반 몬스터 Lv1 기본 드랍 확률 = 15% // 보스 보너스 +15% → 총 30% // roll=25: 일반은 실패(25 >= 15), 보스는 성공(25 < 30) final (_, normalPotion) = service.tryPotionDrop( playerLevel: 1, monsterLevel: 1, monsterGrade: MonsterGrade.normal, inventory: PotionInventory.empty, roll: 25, typeRoll: 30, ); final (_, bossPotion) = service.tryPotionDrop( playerLevel: 1, monsterLevel: 1, monsterGrade: MonsterGrade.boss, inventory: PotionInventory.empty, roll: 25, typeRoll: 30, ); expect(normalPotion, isNull); expect(bossPotion, isNotNull); }); test('몬스터 레벨 > 플레이어 레벨이면 추가 확률 부여', () { // 기본 15% + 레벨 차이 10 * 1% = 25% // roll=20: 레벨 차이 없으면 실패(20 >= 15), 있으면 성공(20 < 25) final (_, withoutDiffPotion) = service.tryPotionDrop( playerLevel: 10, monsterLevel: 10, monsterGrade: MonsterGrade.normal, inventory: PotionInventory.empty, roll: 20, typeRoll: 30, ); final (_, withDiffPotion) = service.tryPotionDrop( playerLevel: 10, monsterLevel: 20, monsterGrade: MonsterGrade.normal, inventory: PotionInventory.empty, roll: 20, typeRoll: 30, ); // Lv10 기본 확률 = 15 + 10*0.5 = 20% → roll 20 >= 20 → 실패 expect(withoutDiffPotion, isNull); // 레벨 차이 10 → 추가 10% → 30% → roll 20 < 30 → 성공 expect(withDiffPotion, isNotNull); }); test('기존 인벤토리에 물약 추가', () { final existingInventory = PotionInventory.empty.addPotion( PotionData.minorHealthPatch.id, 5, ); final (inventory, potion) = service.tryPotionDrop( playerLevel: 1, monsterLevel: 1, monsterGrade: MonsterGrade.normal, inventory: existingInventory, roll: 0, // 드랍 성공 typeRoll: 30, // HP 물약 ); expect(potion, isNotNull); // 기존 5개 + 새로 1개 final droppedId = potion!.id; if (droppedId == PotionData.minorHealthPatch.id) { expect(inventory.getQuantity(droppedId), 6); } else { // 다른 티어(tier) 물약이 드랍될 수 있음 expect(inventory.getQuantity(droppedId), 1); expect(inventory.getQuantity(PotionData.minorHealthPatch.id), 5); } }); }); // ============================================================================ // 인벤토리 관리 (inventory management) // ============================================================================ group('addPotionDrop', () { test('물약 드랍 추가 시 수량 증가', () { final result = service.addPotionDrop( PotionInventory.empty, PotionData.minorHealthPatch.id, 3, ); expect(result.getQuantity(PotionData.minorHealthPatch.id), 3); }); }); // ============================================================================ // 긴급 물약 선택 (emergency potion selection) // ============================================================================ group('selectEmergencyHpPotion', () { test('HP 손실이 회복량 이상이면 물약 선택', () { final inventory = PotionInventory.empty.addPotion( PotionData.minorHealthPatch.id, 5, ); // maxHp=100, currentHp=50 → 손실 50 >= 회복량 30 final potion = service.selectEmergencyHpPotion( currentHp: 50, maxHp: 100, inventory: inventory, playerLevel: 5, ); expect(potion, isNotNull); expect(potion!.id, PotionData.minorHealthPatch.id); }); test('HP가 만충(full)이면 null', () { final inventory = PotionInventory.empty.addPotion( PotionData.minorHealthPatch.id, 5, ); final potion = service.selectEmergencyHpPotion( currentHp: 100, maxHp: 100, inventory: inventory, playerLevel: 5, ); expect(potion, isNull); }); test('보유 물약이 없으면 null', () { final potion = service.selectEmergencyHpPotion( currentHp: 10, maxHp: 100, inventory: PotionInventory.empty, playerLevel: 5, ); expect(potion, isNull); }); }); group('selectEmergencyMpPotion', () { test('MP 손실이 회복량 이상이면 물약 선택', () { final inventory = PotionInventory.empty.addPotion( PotionData.minorManaCache.id, 5, ); // maxMp=100, currentMp=50 → 손실 50 >= 회복량 20 final potion = service.selectEmergencyMpPotion( currentMp: 50, maxMp: 100, inventory: inventory, playerLevel: 5, ); expect(potion, isNotNull); expect(potion!.id, PotionData.minorManaCache.id); }); test('MP가 만충(full)이면 null', () { final inventory = PotionInventory.empty.addPotion( PotionData.minorManaCache.id, 5, ); final potion = service.selectEmergencyMpPotion( currentMp: 50, maxMp: 50, inventory: inventory, playerLevel: 5, ); expect(potion, isNull); }); }); }