From b0913a24ff9c8868290d94cc7f0e8f6882ba2ef9 Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Thu, 15 Jan 2026 23:22:36 +0900 Subject: [PATCH] =?UTF-8?q?feat(skill):=20DamageType=20=EB=B0=8F=20magAtk/?= =?UTF-8?q?magDef=20=EC=8A=A4=ED=82=AC=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DamageType enum 추가 (physical/magical) - 스킬별 데미지 타입 지정 기능 구현 - 마법 스킬 데미지에 magAtk/magDef 적용 - 장비 아이템에서 magAtk/magDef 스탯 추출 - 관련 테스트 업데이트 --- lib/src/core/engine/item_service.dart | 138 +++++++++++++++++-- lib/src/core/engine/skill_service.dart | 50 ++++++- lib/src/core/model/game_state.dart | 6 + lib/src/core/model/monster_combat_stats.dart | 16 ++- lib/src/core/model/skill.dart | 21 ++- test/core/engine/combat_calculator_test.dart | 9 ++ test/core/engine/skill_service_test.dart | 49 ++++--- test/helpers/mock_factories.dart | 4 + 8 files changed, 254 insertions(+), 39 deletions(-) diff --git a/lib/src/core/engine/item_service.dart b/lib/src/core/engine/item_service.dart index 0756f4f..69da083 100644 --- a/lib/src/core/engine/item_service.dart +++ b/lib/src/core/engine/item_service.dart @@ -106,6 +106,8 @@ class ItemService { /// - 느린 무기 (1500ms): atk × 1.4 /// - 기본 무기 (1000ms): atk × 1.0 /// - 빠른 무기 (600ms): atk × 0.7 + /// + /// 마법 무기 확률: 30% (magAtk 부여) ItemStats _generateWeaponStats(int baseValue, ItemRarity rarity) { final criBonus = rarity.index >= ItemRarity.rare.index ? 0.02 + rarity.index * 0.01 @@ -115,39 +117,76 @@ class ItemService { : 0.0; // 공속 결정 (600ms ~ 1500ms 범위) - // 희귀도가 높을수록 공속 변동 폭 증가 - final speedVariance = - 300 + rarity.index * 100; // Common: 300, Legendary: 700 + final speedVariance = 300 + rarity.index * 100; final speedOffset = rng.nextInt(speedVariance * 2) - speedVariance; final attackSpeed = (1000 + speedOffset).clamp(600, 1500); // 공속-데미지 역비례 계산 - // 기준: 1000ms = 1.0x, 600ms = 0.7x, 1500ms = 1.4x final speedMultiplier = 0.3 + (attackSpeed / 1000) * 0.7; final adjustedAtk = (baseValue * speedMultiplier).round(); + // 마법 무기 여부 (30% 확률) + final isMagicWeapon = rng.nextInt(100) < 30; + final magAtk = isMagicWeapon ? (adjustedAtk * 0.8).round() : 0; + + // 능력치 보너스 (Rare 이상) + final strBonus = rarity.index >= ItemRarity.rare.index && !isMagicWeapon + ? rarity.index + : 0; + final intBonus = rarity.index >= ItemRarity.rare.index && isMagicWeapon + ? rarity.index + : 0; + return ItemStats( atk: adjustedAtk, + magAtk: magAtk, criRate: criBonus, parryRate: parryBonus, attackSpeed: attackSpeed, + strBonus: strBonus, + intBonus: intBonus, ); } /// 방패 스탯 생성 /// /// DEF 배율 조정 (v2): 방패 DEF를 0.15배로 축소 + /// 마법 방어(magDef), CON 보너스 추가 ItemStats _generateShieldStats(int baseValue, ItemRarity rarity) { final blockBonus = 0.05 + rarity.index * 0.02; final def = (baseValue * 0.15).round(); - return ItemStats(def: def, blockRate: blockBonus); + // 마법 방어 (50% 확률) + final hasMagDef = rng.nextInt(100) < 50; + final magDef = hasMagDef ? (def * 0.7).round() : 0; + + // HP 보너스 (Uncommon 이상) + final hpBonus = + rarity.index >= ItemRarity.uncommon.index ? baseValue ~/ 3 : 0; + + // CON 보너스 (Rare 이상) + final conBonus = rarity.index >= ItemRarity.rare.index ? rarity.index : 0; + + return ItemStats( + def: def, + magDef: magDef, + blockRate: blockBonus, + hpBonus: hpBonus, + conBonus: conBonus, + ); } /// 방어구 스탯 생성 /// /// DEF 배율 조정 (v2): 9개 방어구 합산 DEF ≈ 무기 ATK × 1.6 - /// 기존 배율(합계 8.0)에서 대폭 축소하여 일반 공격 데미지 정상화 + /// 슬롯별 특화 보너스 추가: + /// - 투구(helm): INT, WIS 보너스 + /// - 갑옷(hauberk): HP, CON 보너스 + /// - 상완갑/전완갑(brassairts, vambraces): STR, DEX 보너스 + /// - 건틀릿(gauntlets): STR, 크리티컬 보너스 + /// - 갬비슨(gambeson): HP, MP 보너스 + /// - 허벅지갑/정강이갑(cuisses, greaves): CON, 회피 보너스 + /// - 철제부츠(sollerets): DEX, 회피 보너스 ItemStats _generateArmorStats( int baseValue, ItemRarity rarity, @@ -155,7 +194,7 @@ class ItemService { ) { // 슬롯별 방어력 가중치 (총합 ~1.6으로 축소) final defMultiplier = switch (slot) { - EquipmentSlot.hauberk => 0.30, // 갑옷류 최고 + EquipmentSlot.hauberk => 0.30, EquipmentSlot.helm => 0.25, EquipmentSlot.gambeson => 0.20, EquipmentSlot.cuisses => 0.18, @@ -169,11 +208,88 @@ class ItemService { 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 switch (slot) { + // 투구: 지능/지혜 보너스, 마법 방어 + EquipmentSlot.helm => ItemStats( + def: def, + magDef: rarity.index >= ItemRarity.uncommon.index ? def ~/ 2 : 0, + hpBonus: rarity.index >= ItemRarity.rare.index ? baseValue ~/ 3 : 0, + intBonus: rarity.index >= ItemRarity.epic.index ? rarity.index : 0, + wisBonus: rarity.index >= ItemRarity.rare.index ? rarity.index - 1 : 0, + ), - return ItemStats(def: def, hpBonus: hpBonus, evasion: evasionBonus); + // 갑옷: HP, CON 보너스 (주력 방어구) + EquipmentSlot.hauberk => ItemStats( + def: def, + hpBonus: rarity.index >= ItemRarity.uncommon.index ? baseValue : 0, + conBonus: rarity.index >= ItemRarity.rare.index ? rarity.index : 0, + strBonus: rarity.index >= ItemRarity.epic.index ? rarity.index - 1 : 0, + ), + + // 상완갑: STR 보너스 + EquipmentSlot.brassairts => ItemStats( + def: def, + hpBonus: rarity.index >= ItemRarity.rare.index ? baseValue ~/ 4 : 0, + strBonus: rarity.index >= ItemRarity.uncommon.index ? rarity.index : 0, + ), + + // 전완갑: DEX 보너스 + EquipmentSlot.vambraces => ItemStats( + def: def, + hpBonus: rarity.index >= ItemRarity.rare.index ? baseValue ~/ 4 : 0, + dexBonus: rarity.index >= ItemRarity.uncommon.index ? rarity.index : 0, + ), + + // 건틀릿: STR, 크리티컬 보너스 + EquipmentSlot.gauntlets => ItemStats( + def: def, + criRate: rarity.index >= ItemRarity.rare.index + ? 0.01 + rarity.index * 0.005 + : 0.0, + strBonus: rarity.index >= ItemRarity.uncommon.index ? rarity.index : 0, + ), + + // 갬비슨: HP, MP 보너스 + EquipmentSlot.gambeson => ItemStats( + def: def, + hpBonus: rarity.index >= ItemRarity.uncommon.index ? baseValue ~/ 2 : 0, + mpBonus: rarity.index >= ItemRarity.uncommon.index ? baseValue ~/ 3 : 0, + wisBonus: rarity.index >= ItemRarity.epic.index ? rarity.index - 1 : 0, + ), + + // 허벅지갑: CON, 회피 보너스 + EquipmentSlot.cuisses => ItemStats( + def: def, + hpBonus: rarity.index >= ItemRarity.rare.index ? baseValue ~/ 3 : 0, + conBonus: rarity.index >= ItemRarity.uncommon.index ? rarity.index : 0, + evasion: rarity.index >= ItemRarity.epic.index ? 0.01 : 0.0, + ), + + // 정강이갑: CON, 회피 보너스 + EquipmentSlot.greaves => ItemStats( + def: def, + hpBonus: rarity.index >= ItemRarity.rare.index ? baseValue ~/ 3 : 0, + conBonus: + rarity.index >= ItemRarity.rare.index ? rarity.index - 1 : 0, + evasion: rarity.index >= ItemRarity.epic.index ? 0.01 : 0.0, + ), + + // 철제부츠: DEX, 회피 보너스 + EquipmentSlot.sollerets => ItemStats( + def: def, + dexBonus: rarity.index >= ItemRarity.uncommon.index ? rarity.index : 0, + evasion: rarity.index >= ItemRarity.rare.index + ? 0.01 + rarity.index * 0.005 + : 0.0, + ), + + // 기본 (무기, 방패는 여기 안옴) + _ => ItemStats( + def: def, + hpBonus: rarity.index >= ItemRarity.rare.index ? baseValue ~/ 2 : 0, + ), + }; } // ============================================================================ diff --git a/lib/src/core/engine/skill_service.dart b/lib/src/core/engine/skill_service.dart index 8837644..d12b876 100644 --- a/lib/src/core/engine/skill_service.dart +++ b/lib/src/core/engine/skill_service.dart @@ -63,15 +63,22 @@ class SkillService { required MonsterCombatStats monster, required SkillSystemState skillSystem, }) { + // 데미지 타입에 따른 공격력/방어력 선택 + final (attackStat, defenseStat) = _getStatsByDamageType( + skill.damageType, + player, + monster, + ); + // 기본 데미지 계산 - final baseDamage = player.atk * skill.damageMultiplier; + final baseDamage = attackStat * skill.damageMultiplier; // 버프 효과 적용 final buffMods = skillSystem.totalBuffModifiers; final buffedDamage = baseDamage * (1 + buffMods.atkMod); // 적 방어력 감소 적용 - final effectiveMonsterDef = monster.def * (1 - skill.targetDefReduction); + final effectiveMonsterDef = defenseStat * (1 - skill.targetDefReduction); // 최종 데미지 계산 (방어력 감산 0.3) final finalDamage = (buffedDamage - effectiveMonsterDef * 0.3) @@ -636,15 +643,22 @@ class SkillService { // 실제 MP 비용 계산 final actualMpCost = (skill.mpCost * mpMult).round(); + // 데미지 타입에 따른 공격력/방어력 선택 + final (attackStat, defenseStat) = _getStatsByDamageType( + skill.damageType, + player, + monster, + ); + // 기본 데미지 계산 (랭크 배율 적용) - final baseDamage = player.atk * skill.damageMultiplier * rankMult; + final baseDamage = attackStat * skill.damageMultiplier * rankMult; // 버프 효과 적용 final buffMods = skillSystem.totalBuffModifiers; final buffedDamage = baseDamage * (1 + buffMods.atkMod); // 적 방어력 감소 적용 - final effectiveMonsterDef = monster.def * (1 - skill.targetDefReduction); + final effectiveMonsterDef = defenseStat * (1 - skill.targetDefReduction); // 최종 데미지 계산 (방어력 감산 0.3) final finalDamage = (buffedDamage - effectiveMonsterDef * 0.3) @@ -708,4 +722,32 @@ class SkillService { return state.copyWith(skillStates: skillStates); } + + // ============================================================================ + // 데미지 타입 헬퍼 + // ============================================================================ + + /// 데미지 타입에 따른 공격력/방어력 스탯 반환 + /// + /// [damageType] 스킬의 데미지 타입 + /// [player] 플레이어 전투 스탯 + /// [monster] 몬스터 전투 스탯 + /// Returns: (공격력, 방어력) 튜플 + (double, double) _getStatsByDamageType( + DamageType damageType, + CombatStats player, + MonsterCombatStats monster, + ) { + return switch (damageType) { + DamageType.physical => (player.atk.toDouble(), monster.def.toDouble()), + DamageType.magical => ( + player.magAtk.toDouble(), + monster.magDef.toDouble(), + ), + DamageType.hybrid => ( + (player.atk + player.magAtk) / 2, + (monster.def + monster.magDef) / 2, + ), + }; + } } diff --git a/lib/src/core/model/game_state.dart b/lib/src/core/model/game_state.dart index 754c4b4..dedf65f 100644 --- a/lib/src/core/model/game_state.dart +++ b/lib/src/core/model/game_state.dart @@ -131,6 +131,7 @@ class DeathInfo { required this.levelAtDeath, required this.timestamp, this.lostItemName, + this.lostItemSlot, this.lastCombatEvents = const [], }); @@ -146,6 +147,9 @@ class DeathInfo { /// 제물로 바친 아이템 이름 (null이면 없음) final String? lostItemName; + /// 제물로 바친 아이템 슬롯 (null이면 없음) + final EquipmentSlot? lostItemSlot; + /// 사망 시점 골드 final int goldAtDeath; @@ -163,6 +167,7 @@ class DeathInfo { String? killerName, int? lostEquipmentCount, String? lostItemName, + EquipmentSlot? lostItemSlot, int? goldAtDeath, int? levelAtDeath, int? timestamp, @@ -173,6 +178,7 @@ class DeathInfo { killerName: killerName ?? this.killerName, lostEquipmentCount: lostEquipmentCount ?? this.lostEquipmentCount, lostItemName: lostItemName ?? this.lostItemName, + lostItemSlot: lostItemSlot ?? this.lostItemSlot, goldAtDeath: goldAtDeath ?? this.goldAtDeath, levelAtDeath: levelAtDeath ?? this.levelAtDeath, timestamp: timestamp ?? this.timestamp, diff --git a/lib/src/core/model/monster_combat_stats.dart b/lib/src/core/model/monster_combat_stats.dart index 0ac6e0a..f4e56ab 100644 --- a/lib/src/core/model/monster_combat_stats.dart +++ b/lib/src/core/model/monster_combat_stats.dart @@ -22,6 +22,7 @@ class MonsterCombatStats { required this.level, required this.atk, required this.def, + required this.magDef, required this.hpMax, required this.hpCurrent, required this.criRate, @@ -41,9 +42,12 @@ class MonsterCombatStats { /// 공격력 final int atk; - /// 방어력 + /// 물리 방어력 final int def; + /// 마법 방어력 + final int magDef; + /// 최대 HP final int hpMax; @@ -96,6 +100,7 @@ class MonsterCombatStats { int? level, int? atk, int? def, + int? magDef, int? hpMax, int? hpCurrent, double? criRate, @@ -110,6 +115,7 @@ class MonsterCombatStats { level: level ?? this.level, atk: atk ?? this.atk, def: def ?? this.def, + magDef: magDef ?? this.magDef, hpMax: hpMax ?? this.hpMax, hpCurrent: hpCurrent ?? this.hpCurrent, criRate: criRate ?? this.criRate, @@ -173,11 +179,16 @@ class MonsterCombatStats { MonsterSpeedType.slow => 1400, }; + // 마법 방어력: 물리 방어력의 70~130% (레벨에 따라 변동) + final magDefRatio = 0.7 + (level % 60) * 0.01; // 0.7 ~ 1.3 + final magDef = (baseStats.def * magDefRatio).round(); + return MonsterCombatStats( name: name, level: level, atk: baseStats.atk, def: baseStats.def, + magDef: magDef, hpMax: adjustedHp, hpCurrent: adjustedHp, criRate: criRate, @@ -202,6 +213,7 @@ class MonsterCombatStats { level: bossLevel, atk: bossStats.atk, def: bossStats.def, + magDef: (bossStats.def * 1.2).round(), // 보스는 마법 방어력 20% 증가 hpMax: bossStats.hp, hpCurrent: bossStats.hp, criRate: 0.25, // 보스 크리티컬 확률 25% @@ -244,6 +256,7 @@ class MonsterCombatStats { level: 1, atk: 8, def: 3, + magDef: 3, hpMax: 35, hpCurrent: 35, criRate: 0.02, @@ -264,6 +277,7 @@ class MonsterCombatStats { level: 0, // PvP에서는 레벨 페널티 없음 atk: stats.atk, def: stats.def, + magDef: stats.magDef, hpMax: stats.hpMax, hpCurrent: stats.hpMax, // 풀 HP로 시작 criRate: stats.criRate, diff --git a/lib/src/core/model/skill.dart b/lib/src/core/model/skill.dart index 454e8bb..ff0e34a 100644 --- a/lib/src/core/model/skill.dart +++ b/lib/src/core/model/skill.dart @@ -4,8 +4,9 @@ /// 스펠 랭크에 따른 스킬 배율 계산 /// -/// 랭크 1: 1.0x, 랭크 2: 1.15x, 랭크 3: 1.30x, ... -double getRankMultiplier(int rank) => 1.0 + (rank - 1) * 0.15; +/// 랭크 1: 1.0x, 랭크 2: 1.08x, 랭크 3: 1.16x, ... 최대 1.72x (rank 10) +/// Phase 3 밸런스: 0.15 → 0.08로 하향 +double getRankMultiplier(int rank) => 1.0 + (rank - 1) * 0.08; /// 랭크에 따른 쿨타임 감소율 계산 /// @@ -38,6 +39,18 @@ enum SkillType { debuff, } +/// 데미지 타입 (물리/마법 구분) +enum DamageType { + /// 물리 공격 - STR + atk 기반, 적 def로 방어 + physical, + + /// 마법 공격 - INT + magAtk 기반, 적 magDef로 방어 + magical, + + /// 하이브리드 - (atk + magAtk) / 2, (def + magDef) / 2 + hybrid, +} + /// 스킬 속성 (하이브리드: 코드 + 시스템) enum SkillElement { /// 논리 (Logic) - 순수 데미지 @@ -118,6 +131,7 @@ class Skill { required this.cooldownMs, required this.power, this.tier = 1, + this.damageType = DamageType.physical, this.damageMultiplier = 1.0, this.healAmount = 0, this.healPercent = 0.0, @@ -137,6 +151,9 @@ class Skill { /// 스킬 티어 (1~5, 높을수록 강함) final int tier; + /// 데미지 타입 (물리/마법/하이브리드) + final DamageType damageType; + /// 스킬 ID final String id; diff --git a/test/core/engine/combat_calculator_test.dart b/test/core/engine/combat_calculator_test.dart index 7609154..436e9f8 100644 --- a/test/core/engine/combat_calculator_test.dart +++ b/test/core/engine/combat_calculator_test.dart @@ -24,6 +24,7 @@ void main() { level: 1, atk: 10, def: 50, // DEF 50 → 50 * 0.4 = 20 감소 + magDef: 50, hpMax: 100, hpCurrent: 100, criRate: 0.05, @@ -63,6 +64,7 @@ void main() { level: 1, atk: 10, def: 0, // DEF 0 + magDef: 0, hpMax: 500, hpCurrent: 500, criRate: 0.05, @@ -100,6 +102,7 @@ void main() { level: 1, atk: 10, def: 0, + magDef: 0, hpMax: 100, hpCurrent: 100, criRate: 0.05, @@ -135,6 +138,7 @@ void main() { level: 1, atk: 100, def: 0, + magDef: 0, hpMax: 100, hpCurrent: 100, criRate: 0.0, // 크리티컬 없음 @@ -174,6 +178,7 @@ void main() { level: 1, atk: 100, def: 0, + magDef: 0, hpMax: 100, hpCurrent: 100, criRate: 0.0, @@ -221,6 +226,7 @@ void main() { level: 1, atk: 10, def: 10, + magDef: 10, hpMax: 100, hpCurrent: 100, criRate: 0.05, @@ -255,6 +261,7 @@ void main() { level: 1, atk: 10, def: 5, + magDef: 5, hpMax: 50, hpCurrent: 50, criRate: 0.05, @@ -270,6 +277,7 @@ void main() { level: 10, atk: 50, def: 20, + magDef: 20, hpMax: 500, hpCurrent: 500, criRate: 0.05, @@ -314,6 +322,7 @@ void main() { level: 1, atk: 30, def: 10, + magDef: 10, hpMax: 80, hpCurrent: 80, criRate: 0.05, diff --git a/test/core/engine/skill_service_test.dart b/test/core/engine/skill_service_test.dart index 4453302..18040dc 100644 --- a/test/core/engine/skill_service_test.dart +++ b/test/core/engine/skill_service_test.dart @@ -103,6 +103,7 @@ void main() { level: 1, atk: 10, def: 50, // DEF 50 → 50 * 0.3 = 15 감소 + magDef: 50, hpMax: 500, hpCurrent: 500, criRate: 0.05, @@ -124,7 +125,7 @@ void main() { // ATK 100 * 2.0 - DEF 50 * 0.3 = 200 - 15 = 185 expect(result.result.success, isTrue); expect(result.result.damage, equals(185)); - expect(result.updatedPlayer.mpCurrent, equals(40)); // 50 - 10 + expect(result.updatedPlayer.mpCurrent, equals(20)); // 50 - 30 (mpCost 30) expect(result.updatedMonster.hpCurrent, equals(315)); // 500 - 185 }); @@ -143,6 +144,7 @@ void main() { level: 1, atk: 10, def: 0, + magDef: 0, hpMax: 500, hpCurrent: 500, criRate: 0.05, @@ -196,6 +198,7 @@ void main() { level: 1, atk: 10, def: 0, + magDef: 0, hpMax: 500, hpCurrent: 500, criRate: 0.05, @@ -217,7 +220,7 @@ void main() { // 랭크 1: 1.0x → ATK 100 * 2.0 * 1.0 = 200 expect(result.result.damage, equals(200)); - expect(result.updatedPlayer.mpCurrent, equals(40)); // MP 10 소모 + expect(result.updatedPlayer.mpCurrent, equals(20)); // MP 30 소모 }); test('랭크 5 데미지 스케일링', () { @@ -235,6 +238,7 @@ void main() { level: 1, atk: 10, def: 0, + magDef: 0, hpMax: 500, hpCurrent: 500, criRate: 0.05, @@ -254,12 +258,12 @@ void main() { rank: 5, ); - // 랭크 5: 1.6x (1.0 + 4 * 0.15) - // ATK 100 * 2.0 * 1.6 = 320 - expect(result.result.damage, equals(320)); + // 랭크 5: 1.32x (1.0 + 4 * 0.08) - 랭크 배율 하향 + // ATK 100 * 2.0 * 1.32 = 264 + expect(result.result.damage, equals(264)); - // MP 비용: 10 * (1.0 - 4 * 0.03) = 10 * 0.88 = 9 (반올림) - expect(result.updatedPlayer.mpCurrent, equals(41)); // 50 - 9 + // MP 비용: 30 * (1.0 - 4 * 0.03) = 30 * 0.88 = 26 (반올림) + expect(result.updatedPlayer.mpCurrent, equals(24)); // 50 - 26 }); }); @@ -268,12 +272,12 @@ void main() { final rng = DeterministicRandom(42); final service = SkillService(rng: rng); - const skill = SkillData.hotReload; // healAmount: 30 + const skill = SkillData.hotReload; // healAmount: 30, mpCost: 80 final player = CombatStats.empty().copyWith( hpMax: 200, hpCurrent: 100, - mpMax: 100, - mpCurrent: 50, + mpMax: 200, + mpCurrent: 150, ); final skillSystem = SkillSystemState.empty().copyWith(elapsedMs: 5000); @@ -286,7 +290,7 @@ void main() { expect(result.result.success, isTrue); expect(result.result.healedAmount, equals(30)); expect(result.updatedPlayer.hpCurrent, equals(130)); // 100 + 30 - expect(result.updatedPlayer.mpCurrent, equals(35)); // 50 - 15 + expect(result.updatedPlayer.mpCurrent, equals(70)); // 150 - 80 }); test('퍼센트 회복량', () { @@ -483,6 +487,7 @@ void main() { level: 1, atk: 10, def: 10, + magDef: 10, hpMax: 100, hpCurrent: 100, criRate: 0.05, @@ -512,14 +517,15 @@ void main() { final player = CombatStats.empty().copyWith( hpMax: 100, hpCurrent: 20, // 20% HP - mpMax: 100, - mpCurrent: 80, + mpMax: 200, + mpCurrent: 150, // 힐 스킬 사용 가능한 MP (garbageCollection: 130) ); final monster = MonsterCombatStats( name: 'Test Monster', level: 1, atk: 10, def: 10, + magDef: 10, hpMax: 100, hpCurrent: 100, criRate: 0.05, @@ -548,10 +554,10 @@ void main() { final rng = DeterministicRandom(42); final service = SkillService(rng: rng); - const skill = SkillData.debugMode; // ATK +25% 버프 + const skill = SkillData.debugMode; // ATK +25% 버프, mpCost: 100 final player = CombatStats.empty().copyWith( - mpMax: 100, - mpCurrent: 50, + mpMax: 200, + mpCurrent: 150, ); final skillSystem = SkillSystemState.empty().copyWith(elapsedMs: 5000); @@ -568,7 +574,7 @@ void main() { equals(0.25), ); expect(result.updatedSkillSystem.activeBuffs.length, equals(1)); - expect(result.updatedPlayer.mpCurrent, equals(30)); // 50 - 20 + expect(result.updatedPlayer.mpCurrent, equals(50)); // 150 - 100 }); test('중복 버프 제거 후 새 버프 적용', () { @@ -648,11 +654,12 @@ void main() { group('getRankMultiplier', () { test('랭크별 배율 계산', () { + // 랭크 배율 하향: 0.15 → 0.08 per rank expect(getRankMultiplier(1), equals(1.0)); - expect(getRankMultiplier(2), closeTo(1.15, 0.001)); - expect(getRankMultiplier(3), closeTo(1.30, 0.001)); - expect(getRankMultiplier(5), closeTo(1.60, 0.001)); - expect(getRankMultiplier(10), closeTo(2.35, 0.001)); + expect(getRankMultiplier(2), closeTo(1.08, 0.001)); + expect(getRankMultiplier(3), closeTo(1.16, 0.001)); + expect(getRankMultiplier(5), closeTo(1.32, 0.001)); + expect(getRankMultiplier(10), closeTo(1.72, 0.001)); }); }); diff --git a/test/helpers/mock_factories.dart b/test/helpers/mock_factories.dart index ed747c3..f041573 100644 --- a/test/helpers/mock_factories.dart +++ b/test/helpers/mock_factories.dart @@ -118,6 +118,7 @@ class MockFactories { level: monsterLevel, atk: 10, def: 5, + magDef: 5, hpMax: monsterHpMax, hpCurrent: monsterHpCurrent, criRate: 0.05, @@ -146,6 +147,7 @@ class MockFactories { int level = 1, int atk = 10, int def = 5, + int magDef = 5, int hpMax = 100, int? hpCurrent, double criRate = 0.05, @@ -160,6 +162,7 @@ class MockFactories { level: level, atk: atk, def: def, + magDef: magDef, hpMax: hpMax, hpCurrent: hpCurrent ?? hpMax, criRate: criRate, @@ -182,6 +185,7 @@ class MockFactories { level: level, atk: base.atk, def: base.def, + magDef: base.def, // 물리 방어와 동일 hpMax: base.hp, hpCurrent: base.hp, criRate: 0.05,