diff --git a/lib/src/core/engine/item_service.dart b/lib/src/core/engine/item_service.dart new file mode 100644 index 0000000..e131348 --- /dev/null +++ b/lib/src/core/engine/item_service.dart @@ -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 items) { + return items.fold(0, (sum, item) => sum + item.weight); + } + + /// 새 아이템 장착 가능 여부 (무게 기준) + /// + /// [newItem] 장착하려는 아이템 + /// [currentItems] 현재 장착 중인 아이템 목록 + /// [str] 플레이어 STR + /// [replacingSlot] 교체할 슬롯 (해당 슬롯 아이템 무게 제외) + static bool canEquip({ + required EquipmentItem newItem, + required List 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 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, + ); + } +} diff --git a/lib/src/core/model/combat_stats.dart b/lib/src/core/model/combat_stats.dart index e813411..bf1dfab 100644 --- a/lib/src/core/model/combat_stats.dart +++ b/lib/src/core/model/combat_stats.dart @@ -198,55 +198,71 @@ class CombatStats { /// Stats와 Equipment에서 CombatStats 생성 /// /// [stats] 캐릭터 기본 스탯 - /// [equipment] 장착 장비 (향후 장비 스탯 적용 시 사용) + /// [equipment] 장착 장비 (장비 스탯 적용) /// [level] 캐릭터 레벨 (스케일링용) factory CombatStats.fromStats({ required Stats stats, required Equipment equipment, required int level, }) { - // 기본 공격력: STR 기반 + 레벨 보정 - final baseAtk = stats.str * 2 + level; + // 장비 총 스탯 가져오기 + final equipStats = equipment.totalStats; - // 기본 방어력: CON 기반 + 레벨 보정 - final baseDef = stats.con + (level ~/ 2); + // 장비 보너스가 적용된 기본 스탯 + final effectiveStr = stats.str + equipStats.strBonus; + final effectiveCon = stats.con + equipStats.conBonus; + final effectiveDex = stats.dex + equipStats.dexBonus; + final effectiveInt = stats.intelligence + equipStats.intBonus; + final effectiveWis = stats.wis + equipStats.wisBonus; - // 마법 공격력: INT 기반 - final baseMagAtk = stats.intelligence * 2 + level; + // 기본 공격력: STR 기반 + 레벨 보정 + 장비 ATK + final baseAtk = effectiveStr * 2 + level + equipStats.atk; - // 마법 방어력: WIS 기반 - final baseMagDef = stats.wis + (level ~/ 2); + // 기본 방어력: CON 기반 + 레벨 보정 + 장비 DEF + final baseDef = effectiveCon + (level ~/ 2) + equipStats.def; - // 크리티컬 확률: DEX 기반 (0.05 ~ 0.5) - final criRate = (0.05 + stats.dex * 0.005).clamp(0.05, 0.5); + // 마법 공격력: INT 기반 + 장비 MAG_ATK + final baseMagAtk = effectiveInt * 2 + level + equipStats.magAtk; + + // 마법 방어력: WIS 기반 + 장비 MAG_DEF + final baseMagDef = effectiveWis + (level ~/ 2) + equipStats.magDef; + + // 크리티컬 확률: DEX 기반 + 장비 보너스 (0.05 ~ 0.5) + final criRate = (0.05 + effectiveDex * 0.005 + equipStats.criRate).clamp(0.05, 0.5); // 크리티컬 데미지: 기본 1.5배, DEX에 따라 증가 (최대 3.0) - final criDamage = (1.5 + stats.dex * 0.01).clamp(1.5, 3.0); + final criDamage = (1.5 + effectiveDex * 0.01).clamp(1.5, 3.0); - // 회피율: DEX 기반 (0.0 ~ 0.5) - final evasion = (stats.dex * 0.005).clamp(0.0, 0.5); + // 회피율: DEX 기반 + 장비 보너스 (0.0 ~ 0.5) + final evasion = (effectiveDex * 0.005 + equipStats.evasion).clamp(0.0, 0.5); // 명중률: DEX 기반 (0.8 ~ 1.0) - final accuracy = (0.8 + stats.dex * 0.002).clamp(0.8, 1.0); + final accuracy = (0.8 + effectiveDex * 0.002).clamp(0.8, 1.0); - // 방패 방어율: 방패 장착 여부에 따라 (0.0 ~ 0.4) + // 방패 방어율: 방패 장착 시 기본 + CON 보정 + 장비 보너스 (0.0 ~ 0.5) final hasShield = equipment.shield.isNotEmpty; - final blockRate = hasShield ? (0.1 + stats.con * 0.003).clamp(0.1, 0.4) : 0.0; + final baseBlockRate = hasShield ? (0.1 + effectiveCon * 0.003) : 0.0; + final blockRate = (baseBlockRate + equipStats.blockRate).clamp(0.0, 0.5); - // 무기 쳐내기: DEX + STR 기반 (0.0 ~ 0.3) - final parryRate = ((stats.dex + stats.str) * 0.002).clamp(0.0, 0.3); + // 무기 쳐내기: DEX + STR 기반 + 장비 보너스 (0.0 ~ 0.4) + final baseParryRate = (effectiveDex + effectiveStr) * 0.002; + final parryRate = (baseParryRate + equipStats.parryRate).clamp(0.0, 0.4); // 공격 속도: DEX 기반 (기본 1000ms, 최소 357ms) - final speedModifier = 1.0 + (stats.dex - 10) * 0.02; + final speedModifier = 1.0 + (effectiveDex - 10) * 0.02; final attackDelayMs = (1000 / speedModifier).round().clamp(357, 1500); + // HP/MP: 기본 + 장비 보너스 + final totalHpMax = stats.hpMax + equipStats.hpBonus; + final totalMpMax = stats.mpMax + equipStats.mpBonus; + return CombatStats( - str: stats.str, - con: stats.con, - dex: stats.dex, - intelligence: stats.intelligence, - wis: stats.wis, - cha: stats.cha, + str: effectiveStr, + con: effectiveCon, + dex: effectiveDex, + intelligence: effectiveInt, + wis: effectiveWis, + cha: stats.cha + equipStats.chaBonus, atk: baseAtk, def: baseDef, magAtk: baseMagAtk, @@ -258,10 +274,10 @@ class CombatStats { blockRate: blockRate, parryRate: parryRate, attackDelayMs: attackDelayMs, - hpMax: stats.hpMax, - hpCurrent: stats.hp, - mpMax: stats.mpMax, - mpCurrent: stats.mp, + hpMax: totalHpMax, + hpCurrent: stats.hp.clamp(0, totalHpMax), + mpMax: totalMpMax, + mpCurrent: stats.mp.clamp(0, totalMpMax), ); } diff --git a/lib/src/core/model/equipment_item.dart b/lib/src/core/model/equipment_item.dart new file mode 100644 index 0000000..1649cc6 --- /dev/null +++ b/lib/src/core/model/equipment_item.dart @@ -0,0 +1,94 @@ +import 'package:askiineverdie/src/core/model/equipment_slot.dart'; +import 'package:askiineverdie/src/core/model/item_stats.dart'; + +/// 장비 아이템 +/// +/// 개별 장비의 이름, 슬롯, 레벨, 스탯 등을 포함하는 클래스. +/// 불변(immutable) 객체로 설계됨. +class EquipmentItem { + const EquipmentItem({ + required this.name, + required this.slot, + required this.level, + required this.weight, + required this.stats, + required this.rarity, + }); + + /// 아이템 이름 (예: "Flaming Sword of Doom") + final String name; + + /// 장착 슬롯 + final EquipmentSlot slot; + + /// 아이템 레벨 + final int level; + + /// 무게 (STR 기반 휴대 제한용) + final int weight; + + /// 아이템 스탯 보정치 + final ItemStats stats; + + /// 희귀도 + final ItemRarity rarity; + + /// 가중치 (자동 장착 비교용) + /// + /// 가중치 = 기본값 + (레벨 * 10) + (희귀도 보너스) + (스탯 합계) + int get itemWeight { + const baseValue = 10; + return baseValue + (level * 10) + rarity.weightBonus + stats.totalStatValue; + } + + /// 빈 아이템 여부 + bool get isEmpty => name.isEmpty; + + /// 유효한 아이템 여부 + bool get isNotEmpty => name.isNotEmpty; + + /// 빈 아이템 생성 (특정 슬롯) + factory EquipmentItem.empty(EquipmentSlot slot) { + return EquipmentItem( + name: '', + slot: slot, + level: 0, + weight: 0, + stats: ItemStats.empty, + rarity: ItemRarity.common, + ); + } + + /// 기본 무기 (Keyboard) + factory EquipmentItem.defaultWeapon() { + return const EquipmentItem( + name: 'Keyboard', + slot: EquipmentSlot.weapon, + level: 1, + weight: 5, + stats: ItemStats(atk: 1), + rarity: ItemRarity.common, + ); + } + + EquipmentItem copyWith({ + String? name, + EquipmentSlot? slot, + int? level, + int? weight, + ItemStats? stats, + ItemRarity? rarity, + }) { + return EquipmentItem( + name: name ?? this.name, + slot: slot ?? this.slot, + level: level ?? this.level, + weight: weight ?? this.weight, + stats: stats ?? this.stats, + rarity: rarity ?? this.rarity, + ); + } + + @override + String toString() => name.isEmpty ? '(empty)' : name; +} diff --git a/lib/src/core/model/game_state.dart b/lib/src/core/model/game_state.dart index 8e0fa3e..c568e92 100644 --- a/lib/src/core/model/game_state.dart +++ b/lib/src/core/model/game_state.dart @@ -1,6 +1,9 @@ import 'dart:collection'; import 'package:askiineverdie/src/core/model/combat_state.dart'; +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'; /// Minimal skeletal state to mirror Progress Quest structures. @@ -275,33 +278,17 @@ class Inventory { } /// 장비 (원본 Main.dfm Equips ListView, 11개 슬롯) +/// +/// Phase 2에서 EquipmentItem 기반으로 확장됨. +/// 기존 문자열 API(weapon, shield 등)는 호환성을 위해 유지. class Equipment { - const Equipment({ - required this.weapon, - required this.shield, - required this.helm, - required this.hauberk, - required this.brassairts, - required this.vambraces, - required this.gauntlets, - required this.gambeson, - required this.cuisses, - required this.greaves, - required this.sollerets, + Equipment({ + required this.items, required this.bestIndex, - }); + }) : assert(items.length == slotCount, 'Equipment must have $slotCount items'); - final String weapon; // 0: 무기 - final String shield; // 1: 방패 - final String helm; // 2: 투구 - final String hauberk; // 3: 사슬갑옷 - final String brassairts; // 4: 상완갑 - final String vambraces; // 5: 전완갑 - final String gauntlets; // 6: 건틀릿 - final String gambeson; // 7: 갬비슨 - final String cuisses; // 8: 허벅지갑 - final String greaves; // 9: 정강이갑 - final String sollerets; // 10: 철제신발 + /// 장비 아이템 목록 (11개 슬롯) + final List items; /// 최고 아이템 슬롯 인덱스 (원본 Equips.Tag, 0-10) final int bestIndex; @@ -309,83 +296,168 @@ class Equipment { /// 슬롯 개수 static const slotCount = 11; - factory Equipment.empty() => const Equipment( - weapon: 'Keyboard', - shield: '', - helm: '', - hauberk: '', - brassairts: '', - vambraces: '', - gauntlets: '', - gambeson: '', - cuisses: '', - greaves: '', - sollerets: '', - bestIndex: 0, - ); + // ============================================================================ + // 문자열 API (기존 코드 호환성) + // ============================================================================ - /// 인덱스로 슬롯 값 가져오기 - String getByIndex(int index) { - return switch (index) { - 0 => weapon, - 1 => shield, - 2 => helm, - 3 => hauberk, - 4 => brassairts, - 5 => vambraces, - 6 => gauntlets, - 7 => gambeson, - 8 => cuisses, - 9 => greaves, - 10 => sollerets, - _ => '', - }; + String get weapon => items[0].name; // 0: 무기 + String get shield => items[1].name; // 1: 방패 + String get helm => items[2].name; // 2: 투구 + String get hauberk => items[3].name; // 3: 사슬갑옷 + String get brassairts => items[4].name; // 4: 상완갑 + String get vambraces => items[5].name; // 5: 전완갑 + String get gauntlets => items[6].name; // 6: 건틀릿 + String get gambeson => items[7].name; // 7: 갬비슨 + String get cuisses => items[8].name; // 8: 허벅지갑 + String get greaves => items[9].name; // 9: 정강이갑 + String get sollerets => items[10].name; // 10: 철제신발 + + // ============================================================================ + // EquipmentItem API + // ============================================================================ + + EquipmentItem get weaponItem => items[0]; + EquipmentItem get shieldItem => items[1]; + EquipmentItem get helmItem => items[2]; + EquipmentItem get hauberkItem => items[3]; + EquipmentItem get brassairtsItem => items[4]; + EquipmentItem get vambracesItem => items[5]; + EquipmentItem get gauntletsItem => items[6]; + EquipmentItem get gambesonItem => items[7]; + EquipmentItem get cuissesItem => items[8]; + EquipmentItem get greavesItem => items[9]; + EquipmentItem get solleretsItem => items[10]; + + /// 모든 장비 스탯 합산 + ItemStats get totalStats { + return items.fold( + ItemStats.empty, + (sum, item) => sum + item.stats, + ); } - /// 인덱스로 슬롯 값 설정한 새 Equipment 반환 + /// 모든 장비 무게 합산 + int get totalWeight { + return items.fold(0, (sum, item) => sum + item.weight); + } + + /// 장착된 아이템 목록 (빈 슬롯 제외) + List get equippedItems { + return items.where((item) => item.isNotEmpty).toList(); + } + + // ============================================================================ + // 팩토리 메서드 + // ============================================================================ + + factory Equipment.empty() { + return Equipment( + items: [ + EquipmentItem.defaultWeapon(), // 0: 무기 (Keyboard) + EquipmentItem.empty(EquipmentSlot.shield), // 1: 방패 + EquipmentItem.empty(EquipmentSlot.helm), // 2: 투구 + EquipmentItem.empty(EquipmentSlot.hauberk), // 3: 사슬갑옷 + EquipmentItem.empty(EquipmentSlot.brassairts), // 4: 상완갑 + EquipmentItem.empty(EquipmentSlot.vambraces), // 5: 전완갑 + EquipmentItem.empty(EquipmentSlot.gauntlets), // 6: 건틀릿 + EquipmentItem.empty(EquipmentSlot.gambeson), // 7: 갬비슨 + EquipmentItem.empty(EquipmentSlot.cuisses), // 8: 허벅지갑 + EquipmentItem.empty(EquipmentSlot.greaves), // 9: 정강이갑 + EquipmentItem.empty(EquipmentSlot.sollerets), // 10: 철제신발 + ], + bestIndex: 0, + ); + } + + /// 레거시 문자열 기반 생성자 (세이브 파일 호환용) + factory Equipment.fromStrings({ + required String weapon, + required String shield, + required String helm, + required String hauberk, + required String brassairts, + required String vambraces, + required String gauntlets, + required String gambeson, + required String cuisses, + required String greaves, + required String sollerets, + required int bestIndex, + }) { + return Equipment( + items: [ + _itemFromString(weapon, EquipmentSlot.weapon), + _itemFromString(shield, EquipmentSlot.shield), + _itemFromString(helm, EquipmentSlot.helm), + _itemFromString(hauberk, EquipmentSlot.hauberk), + _itemFromString(brassairts, EquipmentSlot.brassairts), + _itemFromString(vambraces, EquipmentSlot.vambraces), + _itemFromString(gauntlets, EquipmentSlot.gauntlets), + _itemFromString(gambeson, EquipmentSlot.gambeson), + _itemFromString(cuisses, EquipmentSlot.cuisses), + _itemFromString(greaves, EquipmentSlot.greaves), + _itemFromString(sollerets, EquipmentSlot.sollerets), + ], + bestIndex: bestIndex, + ); + } + + /// 문자열에서 기본 EquipmentItem 생성 (레거시 호환) + static EquipmentItem _itemFromString(String name, EquipmentSlot slot) { + if (name.isEmpty) { + return EquipmentItem.empty(slot); + } + // 레거시 아이템: 레벨 1, Common, 기본 스탯 + return EquipmentItem( + name: name, + slot: slot, + level: 1, + weight: 5, + stats: ItemStats.empty, + rarity: ItemRarity.common, + ); + } + + // ============================================================================ + // 유틸리티 메서드 + // ============================================================================ + + /// 인덱스로 슬롯 이름 가져오기 (기존 API 호환) + String getByIndex(int index) { + if (index < 0 || index >= slotCount) return ''; + return items[index].name; + } + + /// 인덱스로 EquipmentItem 가져오기 + EquipmentItem getItemByIndex(int index) { + if (index < 0 || index >= slotCount) { + return EquipmentItem.empty(EquipmentSlot.weapon); + } + return items[index]; + } + + /// 인덱스로 슬롯 값 설정 (문자열, 기존 API 호환) Equipment setByIndex(int index, String value) { - return switch (index) { - 0 => copyWith(weapon: value), - 1 => copyWith(shield: value), - 2 => copyWith(helm: value), - 3 => copyWith(hauberk: value), - 4 => copyWith(brassairts: value), - 5 => copyWith(vambraces: value), - 6 => copyWith(gauntlets: value), - 7 => copyWith(gambeson: value), - 8 => copyWith(cuisses: value), - 9 => copyWith(greaves: value), - 10 => copyWith(sollerets: value), - _ => this, - }; + if (index < 0 || index >= slotCount) return this; + final slot = EquipmentSlot.values[index]; + final newItem = _itemFromString(value, slot); + return setItemByIndex(index, newItem); + } + + /// 인덱스로 EquipmentItem 설정 + Equipment setItemByIndex(int index, EquipmentItem item) { + if (index < 0 || index >= slotCount) return this; + final newItems = List.from(items); + newItems[index] = item; + return Equipment(items: newItems, bestIndex: bestIndex); } Equipment copyWith({ - String? weapon, - String? shield, - String? helm, - String? hauberk, - String? brassairts, - String? vambraces, - String? gauntlets, - String? gambeson, - String? cuisses, - String? greaves, - String? sollerets, + List? items, int? bestIndex, }) { return Equipment( - weapon: weapon ?? this.weapon, - shield: shield ?? this.shield, - helm: helm ?? this.helm, - hauberk: hauberk ?? this.hauberk, - brassairts: brassairts ?? this.brassairts, - vambraces: vambraces ?? this.vambraces, - gauntlets: gauntlets ?? this.gauntlets, - gambeson: gambeson ?? this.gambeson, - cuisses: cuisses ?? this.cuisses, - greaves: greaves ?? this.greaves, - sollerets: sollerets ?? this.sollerets, + items: items ?? List.from(this.items), bestIndex: bestIndex ?? this.bestIndex, ); } diff --git a/lib/src/core/model/item_stats.dart b/lib/src/core/model/item_stats.dart new file mode 100644 index 0000000..06b7ca4 --- /dev/null +++ b/lib/src/core/model/item_stats.dart @@ -0,0 +1,182 @@ +/// 아이템 희귀도 +enum ItemRarity { + common, + uncommon, + rare, + epic, + legendary; + + /// 희귀도 배율 (스탯 계산용) + double get multiplier => switch (this) { + common => 1.0, + uncommon => 1.3, + rare => 1.7, + epic => 2.2, + legendary => 3.0, + }; + + /// 가중치 보너스 + int get weightBonus => switch (this) { + common => 0, + uncommon => 50, + rare => 150, + epic => 400, + legendary => 1000, + }; +} + +/// 아이템 스탯 보정치 +/// +/// 장비 아이템이 제공하는 스탯 보너스. +/// 모든 값은 기본 0이며, 장착 시 플레이어 스탯에 가산됨. +class ItemStats { + const ItemStats({ + this.atk = 0, + this.def = 0, + this.magAtk = 0, + this.magDef = 0, + this.criRate = 0.0, + this.evasion = 0.0, + this.blockRate = 0.0, + this.parryRate = 0.0, + this.hpBonus = 0, + this.mpBonus = 0, + this.strBonus = 0, + this.conBonus = 0, + this.dexBonus = 0, + this.intBonus = 0, + this.wisBonus = 0, + this.chaBonus = 0, + }); + + /// 물리 공격력 보정 + final int atk; + + /// 물리 방어력 보정 + final int def; + + /// 마법 공격력 보정 + final int magAtk; + + /// 마법 방어력 보정 + final int magDef; + + /// 크리티컬 확률 보정 (0.0 ~ 1.0) + final double criRate; + + /// 회피율 보정 (0.0 ~ 1.0) + final double evasion; + + /// 방패 방어율 (방패 전용, 0.0 ~ 1.0) + final double blockRate; + + /// 무기 쳐내기 확률 (무기 전용, 0.0 ~ 1.0) + final double parryRate; + + /// HP 보너스 + final int hpBonus; + + /// MP 보너스 + final int mpBonus; + + /// STR 보너스 + final int strBonus; + + /// CON 보너스 + final int conBonus; + + /// DEX 보너스 + final int dexBonus; + + /// INT 보너스 + final int intBonus; + + /// WIS 보너스 + final int wisBonus; + + /// CHA 보너스 + final int chaBonus; + + /// 스탯 합계 (가중치 계산용) + int get totalStatValue { + return atk + + def + + magAtk + + magDef + + (criRate * 100).round() + + (evasion * 100).round() + + (blockRate * 100).round() + + (parryRate * 100).round() + + hpBonus + + mpBonus + + strBonus * 5 + + conBonus * 5 + + dexBonus * 5 + + intBonus * 5 + + wisBonus * 5 + + chaBonus * 5; + } + + /// 빈 스탯 (보너스 없음) + static const empty = ItemStats(); + + /// 두 스탯 합산 + ItemStats operator +(ItemStats other) { + return ItemStats( + atk: atk + other.atk, + def: def + other.def, + magAtk: magAtk + other.magAtk, + magDef: magDef + other.magDef, + criRate: criRate + other.criRate, + evasion: evasion + other.evasion, + blockRate: blockRate + other.blockRate, + parryRate: parryRate + other.parryRate, + hpBonus: hpBonus + other.hpBonus, + mpBonus: mpBonus + other.mpBonus, + strBonus: strBonus + other.strBonus, + conBonus: conBonus + other.conBonus, + dexBonus: dexBonus + other.dexBonus, + intBonus: intBonus + other.intBonus, + wisBonus: wisBonus + other.wisBonus, + chaBonus: chaBonus + other.chaBonus, + ); + } + + ItemStats copyWith({ + int? atk, + int? def, + int? magAtk, + int? magDef, + double? criRate, + double? evasion, + double? blockRate, + double? parryRate, + int? hpBonus, + int? mpBonus, + int? strBonus, + int? conBonus, + int? dexBonus, + int? intBonus, + int? wisBonus, + int? chaBonus, + }) { + return ItemStats( + atk: atk ?? this.atk, + def: def ?? this.def, + magAtk: magAtk ?? this.magAtk, + magDef: magDef ?? this.magDef, + criRate: criRate ?? this.criRate, + evasion: evasion ?? this.evasion, + blockRate: blockRate ?? this.blockRate, + parryRate: parryRate ?? this.parryRate, + hpBonus: hpBonus ?? this.hpBonus, + mpBonus: mpBonus ?? this.mpBonus, + strBonus: strBonus ?? this.strBonus, + conBonus: conBonus ?? this.conBonus, + dexBonus: dexBonus ?? this.dexBonus, + intBonus: intBonus ?? this.intBonus, + wisBonus: wisBonus ?? this.wisBonus, + chaBonus: chaBonus ?? this.chaBonus, + ); + } +} diff --git a/lib/src/core/model/save_data.dart b/lib/src/core/model/save_data.dart index 9983b4c..0ba1081 100644 --- a/lib/src/core/model/save_data.dart +++ b/lib/src/core/model/save_data.dart @@ -172,7 +172,7 @@ class GameSave { ) .toList(), ), - equipment: Equipment( + equipment: Equipment.fromStrings( weapon: equipmentJson['weapon'] as String? ?? 'Keyboard', shield: equipmentJson['shield'] as String? ?? '', helm: equipmentJson['helm'] as String? ?? '',