feat(item): Phase 2 아이템 시스템 구현
- ItemStats, ItemRarity 클래스 추가 (아이템 스탯/희귀도) - EquipmentItem 클래스 추가 (개별 장비 아이템) - ItemService 추가 (아이템 생성/관리/무게 시스템) - Equipment 클래스 확장 (EquipmentItem 기반, 기존 API 호환) - CombatStats에서 장비 스탯 반영 - 레거시 세이브 파일 호환성 유지
This commit is contained in:
240
lib/src/core/engine/item_service.dart
Normal file
240
lib/src/core/engine/item_service.dart
Normal file
@@ -0,0 +1,240 @@
|
||||
import 'package:askiineverdie/src/core/model/equipment_item.dart';
|
||||
import 'package:askiineverdie/src/core/model/equipment_slot.dart';
|
||||
import 'package:askiineverdie/src/core/model/item_stats.dart';
|
||||
import 'package:askiineverdie/src/core/util/deterministic_random.dart';
|
||||
|
||||
/// 아이템 관리 서비스
|
||||
///
|
||||
/// 아이템 생성, 스탯 계산, 무게 관리, 자동 장착 로직을 담당.
|
||||
/// SRP 준수: 아이템 관련 로직만 처리.
|
||||
class ItemService {
|
||||
const ItemService({required this.rng});
|
||||
|
||||
final DeterministicRandom rng;
|
||||
|
||||
// ============================================================================
|
||||
// 무게 시스템
|
||||
// ============================================================================
|
||||
|
||||
/// STR 기반 최대 휴대 가능 무게
|
||||
///
|
||||
/// 기본 100 + STR당 10
|
||||
static int calculateMaxWeight(int str) {
|
||||
return 100 + str * 10;
|
||||
}
|
||||
|
||||
/// 장비 목록의 총 무게 계산
|
||||
static int calculateTotalWeight(List<EquipmentItem> items) {
|
||||
return items.fold(0, (sum, item) => sum + item.weight);
|
||||
}
|
||||
|
||||
/// 새 아이템 장착 가능 여부 (무게 기준)
|
||||
///
|
||||
/// [newItem] 장착하려는 아이템
|
||||
/// [currentItems] 현재 장착 중인 아이템 목록
|
||||
/// [str] 플레이어 STR
|
||||
/// [replacingSlot] 교체할 슬롯 (해당 슬롯 아이템 무게 제외)
|
||||
static bool canEquip({
|
||||
required EquipmentItem newItem,
|
||||
required List<EquipmentItem> currentItems,
|
||||
required int str,
|
||||
EquipmentSlot? replacingSlot,
|
||||
}) {
|
||||
final maxWeight = calculateMaxWeight(str);
|
||||
var currentWeight = calculateTotalWeight(currentItems);
|
||||
|
||||
// 교체할 슬롯이 있으면 해당 아이템 무게 제외
|
||||
if (replacingSlot != null) {
|
||||
final existingItem = currentItems.where((i) => i.slot == replacingSlot).firstOrNull;
|
||||
if (existingItem != null) {
|
||||
currentWeight -= existingItem.weight;
|
||||
}
|
||||
}
|
||||
|
||||
return currentWeight + newItem.weight <= maxWeight;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 희귀도 결정
|
||||
// ============================================================================
|
||||
|
||||
/// 희귀도 결정 (레벨 기반 확률)
|
||||
///
|
||||
/// 레벨이 높을수록 희귀한 아이템 확률 증가
|
||||
ItemRarity determineRarity(int level) {
|
||||
final roll = rng.nextInt(100);
|
||||
final legendaryChance = (level * 0.5).clamp(0, 5).toInt(); // 최대 5%
|
||||
final epicChance = (level * 1.0).clamp(0, 10).toInt(); // 최대 10%
|
||||
final rareChance = (level * 2.0).clamp(0, 20).toInt(); // 최대 20%
|
||||
final uncommonChance = (level * 3.0).clamp(0, 30).toInt(); // 최대 30%
|
||||
|
||||
if (roll < legendaryChance) return ItemRarity.legendary;
|
||||
if (roll < legendaryChance + epicChance) return ItemRarity.epic;
|
||||
if (roll < legendaryChance + epicChance + rareChance) return ItemRarity.rare;
|
||||
if (roll < legendaryChance + epicChance + rareChance + uncommonChance) {
|
||||
return ItemRarity.uncommon;
|
||||
}
|
||||
return ItemRarity.common;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 아이템 스탯 생성
|
||||
// ============================================================================
|
||||
|
||||
/// 아이템 스탯 생성 (레벨/희귀도/슬롯 기반)
|
||||
ItemStats generateItemStats({
|
||||
required int level,
|
||||
required ItemRarity rarity,
|
||||
required EquipmentSlot slot,
|
||||
}) {
|
||||
final baseValue = (level * 2 * rarity.multiplier).round();
|
||||
|
||||
return switch (slot) {
|
||||
EquipmentSlot.weapon => _generateWeaponStats(baseValue, rarity),
|
||||
EquipmentSlot.shield => _generateShieldStats(baseValue, rarity),
|
||||
_ => _generateArmorStats(baseValue, rarity, slot),
|
||||
};
|
||||
}
|
||||
|
||||
/// 무기 스탯 생성
|
||||
ItemStats _generateWeaponStats(int baseValue, ItemRarity rarity) {
|
||||
final criBonus = rarity.index >= ItemRarity.rare.index ? 0.02 + rarity.index * 0.01 : 0.0;
|
||||
final parryBonus = rarity.index >= ItemRarity.uncommon.index ? 0.01 + rarity.index * 0.005 : 0.0;
|
||||
|
||||
return ItemStats(
|
||||
atk: baseValue,
|
||||
criRate: criBonus,
|
||||
parryRate: parryBonus,
|
||||
);
|
||||
}
|
||||
|
||||
/// 방패 스탯 생성
|
||||
ItemStats _generateShieldStats(int baseValue, ItemRarity rarity) {
|
||||
final blockBonus = 0.05 + rarity.index * 0.02;
|
||||
|
||||
return ItemStats(
|
||||
def: baseValue ~/ 2,
|
||||
blockRate: blockBonus,
|
||||
);
|
||||
}
|
||||
|
||||
/// 방어구 스탯 생성
|
||||
ItemStats _generateArmorStats(int baseValue, ItemRarity rarity, EquipmentSlot slot) {
|
||||
// 슬롯별 방어력 가중치
|
||||
final defMultiplier = switch (slot) {
|
||||
EquipmentSlot.hauberk => 1.5, // 갑옷류 최고
|
||||
EquipmentSlot.helm => 1.2,
|
||||
EquipmentSlot.gambeson => 1.0,
|
||||
EquipmentSlot.cuisses => 0.9,
|
||||
EquipmentSlot.greaves => 0.8,
|
||||
EquipmentSlot.brassairts => 0.7,
|
||||
EquipmentSlot.vambraces => 0.7,
|
||||
EquipmentSlot.gauntlets => 0.6,
|
||||
EquipmentSlot.sollerets => 0.6,
|
||||
_ => 0.5,
|
||||
};
|
||||
|
||||
final def = (baseValue * defMultiplier).round();
|
||||
|
||||
// 희귀도에 따른 추가 보너스
|
||||
final hpBonus = rarity.index >= ItemRarity.rare.index ? baseValue ~/ 2 : 0;
|
||||
final evasionBonus = rarity.index >= ItemRarity.epic.index ? 0.01 : 0.0;
|
||||
|
||||
return ItemStats(
|
||||
def: def,
|
||||
hpBonus: hpBonus,
|
||||
evasion: evasionBonus,
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 아이템 생성
|
||||
// ============================================================================
|
||||
|
||||
/// 무게 계산 (레벨/슬롯 기반)
|
||||
int calculateWeight({
|
||||
required int level,
|
||||
required EquipmentSlot slot,
|
||||
}) {
|
||||
// 슬롯별 기본 무게
|
||||
final baseWeight = switch (slot) {
|
||||
EquipmentSlot.weapon => 10,
|
||||
EquipmentSlot.shield => 15,
|
||||
EquipmentSlot.helm => 8,
|
||||
EquipmentSlot.hauberk => 25,
|
||||
EquipmentSlot.brassairts => 6,
|
||||
EquipmentSlot.vambraces => 5,
|
||||
EquipmentSlot.gauntlets => 4,
|
||||
EquipmentSlot.gambeson => 12,
|
||||
EquipmentSlot.cuisses => 10,
|
||||
EquipmentSlot.greaves => 8,
|
||||
EquipmentSlot.sollerets => 6,
|
||||
};
|
||||
|
||||
// 레벨에 따라 무게 증가 (고급 아이템일수록 무거움)
|
||||
return baseWeight + (level ~/ 5);
|
||||
}
|
||||
|
||||
/// 장비 아이템 생성
|
||||
///
|
||||
/// [name] 아이템 이름
|
||||
/// [slot] 장착 슬롯
|
||||
/// [level] 아이템 레벨
|
||||
/// [rarity] 희귀도 (null이면 자동 결정)
|
||||
EquipmentItem generateEquipment({
|
||||
required String name,
|
||||
required EquipmentSlot slot,
|
||||
required int level,
|
||||
ItemRarity? rarity,
|
||||
}) {
|
||||
final itemRarity = rarity ?? determineRarity(level);
|
||||
final stats = generateItemStats(level: level, rarity: itemRarity, slot: slot);
|
||||
final weight = calculateWeight(level: level, slot: slot);
|
||||
|
||||
return EquipmentItem(
|
||||
name: name,
|
||||
slot: slot,
|
||||
level: level,
|
||||
weight: weight,
|
||||
stats: stats,
|
||||
rarity: itemRarity,
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 자동 장착
|
||||
// ============================================================================
|
||||
|
||||
/// 새 아이템이 현재 장비보다 좋은지 비교
|
||||
///
|
||||
/// 가중치 기준으로 비교하며, 무게 제한도 고려
|
||||
bool shouldEquip({
|
||||
required EquipmentItem newItem,
|
||||
required EquipmentItem currentItem,
|
||||
required List<EquipmentItem> allEquipped,
|
||||
required int str,
|
||||
}) {
|
||||
// 빈 슬롯이면 무조건 장착
|
||||
if (currentItem.isEmpty) {
|
||||
return canEquip(
|
||||
newItem: newItem,
|
||||
currentItems: allEquipped,
|
||||
str: str,
|
||||
replacingSlot: newItem.slot,
|
||||
);
|
||||
}
|
||||
|
||||
// 새 아이템이 더 좋은지 확인
|
||||
if (newItem.itemWeight <= currentItem.itemWeight) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 무게 제한 확인
|
||||
return canEquip(
|
||||
newItem: newItem,
|
||||
currentItems: allEquipped,
|
||||
str: str,
|
||||
replacingSlot: newItem.slot,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user