import 'package:flutter/foundation.dart'; import 'package:asciineverdie/data/potion_data.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/item_stats.dart'; import 'package:asciineverdie/src/core/model/treasure_chest.dart'; import 'package:asciineverdie/src/core/util/deterministic_random.dart'; /// 보물 상자 서비스 /// /// 상자 내용물 생성 및 오픈 로직 담당 class ChestService { ChestService({DeterministicRandom? rng}) : _rng = rng ?? DeterministicRandom(DateTime.now().millisecondsSinceEpoch); final DeterministicRandom _rng; // ========================================================================== // 상수 // ========================================================================== /// 보상 타입별 확률 (%) static const int _equipmentChance = 40; // 장비 40% static const int _potionChance = 30; // 포션 30% static const int _goldChance = 20; // 골드 20% // 경험치 10% (나머지) /// 골드 보상 범위 (레벨 * 배율) static const int _goldPerLevel = 50; static const int _goldVariance = 20; /// 경험치 보상 범위 (레벨 * 배율) static const int _expPerLevel = 100; static const int _expVariance = 30; /// 포션 수량 범위 static const int _minPotionCount = 1; static const int _maxPotionCount = 3; // ========================================================================== // 상자 오픈 // ========================================================================== /// 상자 오픈하여 보상 생성 /// /// [playerLevel] 플레이어 레벨 (보상 스케일링용) ChestReward openChest(int playerLevel) { final roll = _rng.nextInt(100); if (roll < _equipmentChance) { // 40%: 장비 return _generateEquipmentReward(playerLevel); } else if (roll < _equipmentChance + _potionChance) { // 30%: 포션 return _generatePotionReward(playerLevel); } else if (roll < _equipmentChance + _potionChance + _goldChance) { // 20%: 골드 return _generateGoldReward(playerLevel); } else { // 10%: 경험치 return _generateExperienceReward(playerLevel); } } /// 여러 상자 오픈 List openMultipleChests(int count, int playerLevel) { final rewards = []; for (var i = 0; i < count; i++) { rewards.add(openChest(playerLevel)); } return rewards; } // ========================================================================== // 보상 생성 // ========================================================================== /// 장비 보상 생성 ChestReward _generateEquipmentReward(int playerLevel) { // 랜덤 슬롯 선택 final slotIndex = _rng.nextInt(EquipmentSlot.values.length); final slot = EquipmentSlot.values[slotIndex]; // 희귀도 결정 (상자는 좀 더 좋은 확률) final rarity = _rollChestRarity(); // 아이템 레벨: 플레이어 레벨 ±2 final minLevel = (playerLevel - 2).clamp(1, 999); final maxLevel = playerLevel + 2; final itemLevel = minLevel + _rng.nextInt(maxLevel - minLevel + 1); // 아이템 생성 final item = EquipmentItem( name: _generateItemName(slot, rarity, itemLevel), slot: slot, level: itemLevel, weight: _calculateWeight(slot, itemLevel), stats: _generateItemStats(slot, itemLevel, rarity), rarity: rarity, ); debugPrint( '[ChestService] Equipment reward: ${item.name} (${rarity.name})', ); return ChestReward.equipment(item); } /// 포션 보상 생성 ChestReward _generatePotionReward(int playerLevel) { // 레벨에 맞는 티어 선택 final tier = PotionData.tierForLevel(playerLevel); // HP/MP 랜덤 선택 final isHp = _rng.nextInt(2) == 0; final potion = isHp ? PotionData.getHpPotionByTier(tier) : PotionData.getMpPotionByTier(tier); if (potion == null) { // 폴백: 기본 포션 return ChestReward.potion('minor_health_patch', 1); } // 수량 결정 final count = _minPotionCount + _rng.nextInt(_maxPotionCount - _minPotionCount + 1); debugPrint('[ChestService] Potion reward: ${potion.name} x$count'); return ChestReward.potion(potion.id, count); } /// 골드 보상 생성 ChestReward _generateGoldReward(int playerLevel) { final baseGold = playerLevel * _goldPerLevel; final variance = _rng.nextInt(_goldVariance * 2 + 1) - _goldVariance; final gold = (baseGold + (baseGold * variance / 100)).round().clamp( 10, 99999, ); debugPrint('[ChestService] Gold reward: $gold'); return ChestReward.gold(gold); } /// 경험치 보상 생성 ChestReward _generateExperienceReward(int playerLevel) { final baseExp = playerLevel * _expPerLevel; final variance = _rng.nextInt(_expVariance * 2 + 1) - _expVariance; final exp = (baseExp + (baseExp * variance / 100)).round().clamp( 10, 999999, ); debugPrint('[ChestService] Experience reward: $exp'); return ChestReward.experience(exp); } // ========================================================================== // 헬퍼 메서드 // ========================================================================== /// 상자 희귀도 롤 (일반 샵보다 좋은 확률) /// Common 50%, Uncommon 30%, Rare 15%, Epic 4%, Legendary 1% ItemRarity _rollChestRarity() { final roll = _rng.nextInt(100); if (roll < 50) return ItemRarity.common; if (roll < 80) return ItemRarity.uncommon; if (roll < 95) return ItemRarity.rare; if (roll < 99) return ItemRarity.epic; return ItemRarity.legendary; } /// 아이템 이름 생성 String _generateItemName(EquipmentSlot slot, ItemRarity rarity, int level) { final prefix = _getRarityPrefix(rarity); final baseName = _getSlotBaseName(slot); final suffix = level > 10 ? ' +${level ~/ 10}' : ''; return '$prefix$baseName$suffix'.trim(); } String _getRarityPrefix(ItemRarity rarity) { return switch (rarity) { ItemRarity.common => '', ItemRarity.uncommon => 'Fine ', ItemRarity.rare => 'Superior ', ItemRarity.epic => 'Epic ', ItemRarity.legendary => 'Legendary ', }; } String _getSlotBaseName(EquipmentSlot slot) { return switch (slot) { EquipmentSlot.weapon => 'Keyboard', EquipmentSlot.shield => 'Firewall Shield', EquipmentSlot.helm => 'Neural Headset', EquipmentSlot.hauberk => 'Server Rack Armor', EquipmentSlot.brassairts => 'Cable Brassairts', EquipmentSlot.vambraces => 'USB Vambraces', EquipmentSlot.gauntlets => 'Typing Gauntlets', EquipmentSlot.gambeson => 'Padded Gambeson', EquipmentSlot.cuisses => 'Circuit Cuisses', EquipmentSlot.greaves => 'Copper Greaves', EquipmentSlot.sollerets => 'Static Boots', }; } /// 스탯 생성 ItemStats _generateItemStats( EquipmentSlot slot, int level, ItemRarity rarity, ) { final multiplier = rarity.multiplier; final baseValue = (level * multiplier).round(); return switch (slot) { EquipmentSlot.weapon => ItemStats( atk: baseValue * 2, criRate: 0.01 * (level ~/ 5), parryRate: 0.005 * level, ), EquipmentSlot.shield => ItemStats( def: baseValue, blockRate: 0.02 * (level ~/ 3).clamp(1, 10), ), EquipmentSlot.helm => ItemStats( def: baseValue ~/ 2, magDef: baseValue ~/ 2, intBonus: level ~/ 10, ), EquipmentSlot.hauberk => ItemStats(def: baseValue, hpBonus: level * 2), EquipmentSlot.brassairts => ItemStats( def: baseValue ~/ 2, strBonus: level ~/ 15, ), EquipmentSlot.vambraces => ItemStats( def: baseValue ~/ 2, dexBonus: level ~/ 15, ), EquipmentSlot.gauntlets => ItemStats( atk: baseValue ~/ 2, def: baseValue ~/ 4, ), EquipmentSlot.gambeson => ItemStats( def: baseValue ~/ 2, conBonus: level ~/ 15, ), EquipmentSlot.cuisses => ItemStats( def: baseValue ~/ 2, evasion: 0.005 * level, ), EquipmentSlot.greaves => ItemStats( def: baseValue ~/ 2, evasion: 0.003 * level, ), EquipmentSlot.sollerets => ItemStats( def: baseValue ~/ 3, evasion: 0.002 * level, dexBonus: level ~/ 20, ), }; } /// 무게 계산 int _calculateWeight(EquipmentSlot slot, int level) { final baseWeight = switch (slot) { EquipmentSlot.weapon => 5, EquipmentSlot.shield => 8, EquipmentSlot.helm => 4, EquipmentSlot.hauberk => 15, EquipmentSlot.brassairts => 3, EquipmentSlot.vambraces => 3, EquipmentSlot.gauntlets => 2, EquipmentSlot.gambeson => 6, EquipmentSlot.cuisses => 5, EquipmentSlot.greaves => 4, EquipmentSlot.sollerets => 3, }; return baseWeight + (level ~/ 10); } }