feat(game): 포션 시스템 및 UI 패널 추가
- 포션 시스템 구현 (PotionService, Potion 모델) - 포션 인벤토리 패널 위젯 - 활성 버프 패널 위젯 - 장비 스탯 패널 위젯 - 스킬 시스템 확장 - 일본어 번역 추가 - 전투 이벤트/상태 모델 개선
This commit is contained in:
497
lib/src/core/engine/potion_service.dart
Normal file
497
lib/src/core/engine/potion_service.dart
Normal file
@@ -0,0 +1,497 @@
|
||||
import 'package:askiineverdie/data/potion_data.dart';
|
||||
import 'package:askiineverdie/src/core/model/potion.dart';
|
||||
|
||||
/// 물약 서비스
|
||||
///
|
||||
/// 물약 사용, 자동 사용 알고리즘, 인벤토리 관리
|
||||
class PotionService {
|
||||
const PotionService();
|
||||
|
||||
/// 긴급 물약 사용 HP 임계치 (30%)
|
||||
static const double emergencyHpThreshold = 0.30;
|
||||
|
||||
/// 긴급 물약 사용 MP 임계치 (20%)
|
||||
static const double emergencyMpThreshold = 0.20;
|
||||
|
||||
// ============================================================================
|
||||
// 물약 사용 가능 여부
|
||||
// ============================================================================
|
||||
|
||||
/// 물약 사용 가능 여부 체크
|
||||
///
|
||||
/// [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);
|
||||
}
|
||||
|
||||
// 전투당 종류별 1회 제한 체크
|
||||
if (!inventory.canUseType(potion.type)) {
|
||||
return (false, PotionUseFailReason.alreadyUsedThisBattle);
|
||||
}
|
||||
|
||||
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, potion.type);
|
||||
|
||||
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 hpRatio = currentHp / maxHp;
|
||||
if (hpRatio > emergencyHpThreshold) return null;
|
||||
|
||||
// 전투 중 이미 HP 물약 사용했으면 불가
|
||||
if (!inventory.canUseType(PotionType.hp)) 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)) {
|
||||
return potion;
|
||||
}
|
||||
}
|
||||
|
||||
// 적정 티어 이상도 검색
|
||||
for (var tier = targetTier + 1; tier <= 5; tier++) {
|
||||
final potion = PotionData.getHpPotionByTier(tier);
|
||||
if (potion != null && inventory.hasPotion(potion.id)) {
|
||||
return potion;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 긴급 MP 물약 선택
|
||||
///
|
||||
/// MP가 임계치 이하일 때 사용할 최적의 물약 선택
|
||||
Potion? selectEmergencyMpPotion({
|
||||
required int currentMp,
|
||||
required int maxMp,
|
||||
required PotionInventory inventory,
|
||||
required int playerLevel,
|
||||
}) {
|
||||
// 임계치 체크
|
||||
final mpRatio = currentMp / maxMp;
|
||||
if (mpRatio > emergencyMpThreshold) return null;
|
||||
|
||||
// 전투 중 이미 MP 물약 사용했으면 불가
|
||||
if (!inventory.canUseType(PotionType.mp)) 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)) {
|
||||
return potion;
|
||||
}
|
||||
}
|
||||
|
||||
// 적정 티어 이상도 검색
|
||||
for (var tier = targetTier + 1; tier <= 5; tier++) {
|
||||
final potion = PotionData.getMpPotionByTier(tier);
|
||||
if (potion != null && inventory.hasPotion(potion.id)) {
|
||||
return potion;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 인벤토리 관리
|
||||
// ============================================================================
|
||||
|
||||
/// 전투 종료 시 사용 기록 초기화
|
||||
PotionInventory resetBattleUsage(PotionInventory inventory) {
|
||||
return inventory.resetBattleUsage();
|
||||
}
|
||||
|
||||
/// 물약 드랍 추가
|
||||
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] 플레이어 레벨 (드랍 확률 및 티어 결정)
|
||||
/// [inventory] 현재 물약 인벤토리
|
||||
/// [roll] 0~99 범위의 난수 (드랍 확률 판정)
|
||||
/// [typeRoll] 0~99 범위의 난수 (HP/MP 결정)
|
||||
/// Returns: (업데이트된 인벤토리, 드랍된 물약 또는 null)
|
||||
(PotionInventory, Potion?) tryPotionDrop({
|
||||
required int playerLevel,
|
||||
required PotionInventory inventory,
|
||||
required int roll,
|
||||
required int typeRoll,
|
||||
}) {
|
||||
// 드랍 확률 계산
|
||||
final dropChance = (baseDropChance + playerLevel * dropChancePerLevel)
|
||||
.clamp(baseDropChance, maxDropChance);
|
||||
final dropThreshold = (dropChance * 100).round();
|
||||
|
||||
// 드랍 실패
|
||||
if (roll >= dropThreshold) {
|
||||
return (inventory, null);
|
||||
}
|
||||
|
||||
// 물약 타입 결정 (60% HP, 40% MP)
|
||||
final isHpPotion = typeRoll < 60;
|
||||
|
||||
// 레벨 기반 티어 결정
|
||||
final tier = PotionData.tierForLevel(playerLevel);
|
||||
|
||||
// 물약 선택
|
||||
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,
|
||||
|
||||
/// 이번 전투에서 이미 해당 종류 물약 사용
|
||||
alreadyUsedThisBattle,
|
||||
}
|
||||
|
||||
/// 물약 구매 결과
|
||||
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,
|
||||
}
|
||||
Reference in New Issue
Block a user