feat(skill): DamageType 및 magAtk/magDef 스킬 시스템 추가

- DamageType enum 추가 (physical/magical)
- 스킬별 데미지 타입 지정 기능 구현
- 마법 스킬 데미지에 magAtk/magDef 적용
- 장비 아이템에서 magAtk/magDef 스탯 추출
- 관련 테스트 업데이트
This commit is contained in:
JiWoong Sul
2026-01-15 23:22:36 +09:00
parent 525e231c06
commit b0913a24ff
8 changed files with 254 additions and 39 deletions

View File

@@ -106,6 +106,8 @@ class ItemService {
/// - 느린 무기 (1500ms): atk × 1.4 /// - 느린 무기 (1500ms): atk × 1.4
/// - 기본 무기 (1000ms): atk × 1.0 /// - 기본 무기 (1000ms): atk × 1.0
/// - 빠른 무기 (600ms): atk × 0.7 /// - 빠른 무기 (600ms): atk × 0.7
///
/// 마법 무기 확률: 30% (magAtk 부여)
ItemStats _generateWeaponStats(int baseValue, ItemRarity rarity) { ItemStats _generateWeaponStats(int baseValue, ItemRarity rarity) {
final criBonus = rarity.index >= ItemRarity.rare.index final criBonus = rarity.index >= ItemRarity.rare.index
? 0.02 + rarity.index * 0.01 ? 0.02 + rarity.index * 0.01
@@ -115,39 +117,76 @@ class ItemService {
: 0.0; : 0.0;
// 공속 결정 (600ms ~ 1500ms 범위) // 공속 결정 (600ms ~ 1500ms 범위)
// 희귀도가 높을수록 공속 변동 폭 증가 final speedVariance = 300 + rarity.index * 100;
final speedVariance =
300 + rarity.index * 100; // Common: 300, Legendary: 700
final speedOffset = rng.nextInt(speedVariance * 2) - speedVariance; final speedOffset = rng.nextInt(speedVariance * 2) - speedVariance;
final attackSpeed = (1000 + speedOffset).clamp(600, 1500); 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 speedMultiplier = 0.3 + (attackSpeed / 1000) * 0.7;
final adjustedAtk = (baseValue * speedMultiplier).round(); 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( return ItemStats(
atk: adjustedAtk, atk: adjustedAtk,
magAtk: magAtk,
criRate: criBonus, criRate: criBonus,
parryRate: parryBonus, parryRate: parryBonus,
attackSpeed: attackSpeed, attackSpeed: attackSpeed,
strBonus: strBonus,
intBonus: intBonus,
); );
} }
/// 방패 스탯 생성 /// 방패 스탯 생성
/// ///
/// DEF 배율 조정 (v2): 방패 DEF를 0.15배로 축소 /// DEF 배율 조정 (v2): 방패 DEF를 0.15배로 축소
/// 마법 방어(magDef), CON 보너스 추가
ItemStats _generateShieldStats(int baseValue, ItemRarity rarity) { ItemStats _generateShieldStats(int baseValue, ItemRarity rarity) {
final blockBonus = 0.05 + rarity.index * 0.02; final blockBonus = 0.05 + rarity.index * 0.02;
final def = (baseValue * 0.15).round(); 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 /// 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( ItemStats _generateArmorStats(
int baseValue, int baseValue,
ItemRarity rarity, ItemRarity rarity,
@@ -155,7 +194,7 @@ class ItemService {
) { ) {
// 슬롯별 방어력 가중치 (총합 ~1.6으로 축소) // 슬롯별 방어력 가중치 (총합 ~1.6으로 축소)
final defMultiplier = switch (slot) { final defMultiplier = switch (slot) {
EquipmentSlot.hauberk => 0.30, // 갑옷류 최고 EquipmentSlot.hauberk => 0.30,
EquipmentSlot.helm => 0.25, EquipmentSlot.helm => 0.25,
EquipmentSlot.gambeson => 0.20, EquipmentSlot.gambeson => 0.20,
EquipmentSlot.cuisses => 0.18, EquipmentSlot.cuisses => 0.18,
@@ -169,11 +208,88 @@ class ItemService {
final def = (baseValue * defMultiplier).round(); final def = (baseValue * defMultiplier).round();
// 희귀도에 따른 추가 보너스 // 슬롯별 특화 보너스 계산
final hpBonus = rarity.index >= ItemRarity.rare.index ? baseValue ~/ 2 : 0; return switch (slot) {
final evasionBonus = rarity.index >= ItemRarity.epic.index ? 0.01 : 0.0; // 투구: 지능/지혜 보너스, 마법 방어
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,
),
};
} }
// ============================================================================ // ============================================================================

View File

@@ -63,15 +63,22 @@ class SkillService {
required MonsterCombatStats monster, required MonsterCombatStats monster,
required SkillSystemState skillSystem, 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 buffMods = skillSystem.totalBuffModifiers;
final buffedDamage = baseDamage * (1 + buffMods.atkMod); final buffedDamage = baseDamage * (1 + buffMods.atkMod);
// 적 방어력 감소 적용 // 적 방어력 감소 적용
final effectiveMonsterDef = monster.def * (1 - skill.targetDefReduction); final effectiveMonsterDef = defenseStat * (1 - skill.targetDefReduction);
// 최종 데미지 계산 (방어력 감산 0.3) // 최종 데미지 계산 (방어력 감산 0.3)
final finalDamage = (buffedDamage - effectiveMonsterDef * 0.3) final finalDamage = (buffedDamage - effectiveMonsterDef * 0.3)
@@ -636,15 +643,22 @@ class SkillService {
// 실제 MP 비용 계산 // 실제 MP 비용 계산
final actualMpCost = (skill.mpCost * mpMult).round(); 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 buffMods = skillSystem.totalBuffModifiers;
final buffedDamage = baseDamage * (1 + buffMods.atkMod); final buffedDamage = baseDamage * (1 + buffMods.atkMod);
// 적 방어력 감소 적용 // 적 방어력 감소 적용
final effectiveMonsterDef = monster.def * (1 - skill.targetDefReduction); final effectiveMonsterDef = defenseStat * (1 - skill.targetDefReduction);
// 최종 데미지 계산 (방어력 감산 0.3) // 최종 데미지 계산 (방어력 감산 0.3)
final finalDamage = (buffedDamage - effectiveMonsterDef * 0.3) final finalDamage = (buffedDamage - effectiveMonsterDef * 0.3)
@@ -708,4 +722,32 @@ class SkillService {
return state.copyWith(skillStates: skillStates); 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,
),
};
}
} }

View File

@@ -131,6 +131,7 @@ class DeathInfo {
required this.levelAtDeath, required this.levelAtDeath,
required this.timestamp, required this.timestamp,
this.lostItemName, this.lostItemName,
this.lostItemSlot,
this.lastCombatEvents = const [], this.lastCombatEvents = const [],
}); });
@@ -146,6 +147,9 @@ class DeathInfo {
/// 제물로 바친 아이템 이름 (null이면 없음) /// 제물로 바친 아이템 이름 (null이면 없음)
final String? lostItemName; final String? lostItemName;
/// 제물로 바친 아이템 슬롯 (null이면 없음)
final EquipmentSlot? lostItemSlot;
/// 사망 시점 골드 /// 사망 시점 골드
final int goldAtDeath; final int goldAtDeath;
@@ -163,6 +167,7 @@ class DeathInfo {
String? killerName, String? killerName,
int? lostEquipmentCount, int? lostEquipmentCount,
String? lostItemName, String? lostItemName,
EquipmentSlot? lostItemSlot,
int? goldAtDeath, int? goldAtDeath,
int? levelAtDeath, int? levelAtDeath,
int? timestamp, int? timestamp,
@@ -173,6 +178,7 @@ class DeathInfo {
killerName: killerName ?? this.killerName, killerName: killerName ?? this.killerName,
lostEquipmentCount: lostEquipmentCount ?? this.lostEquipmentCount, lostEquipmentCount: lostEquipmentCount ?? this.lostEquipmentCount,
lostItemName: lostItemName ?? this.lostItemName, lostItemName: lostItemName ?? this.lostItemName,
lostItemSlot: lostItemSlot ?? this.lostItemSlot,
goldAtDeath: goldAtDeath ?? this.goldAtDeath, goldAtDeath: goldAtDeath ?? this.goldAtDeath,
levelAtDeath: levelAtDeath ?? this.levelAtDeath, levelAtDeath: levelAtDeath ?? this.levelAtDeath,
timestamp: timestamp ?? this.timestamp, timestamp: timestamp ?? this.timestamp,

View File

@@ -22,6 +22,7 @@ class MonsterCombatStats {
required this.level, required this.level,
required this.atk, required this.atk,
required this.def, required this.def,
required this.magDef,
required this.hpMax, required this.hpMax,
required this.hpCurrent, required this.hpCurrent,
required this.criRate, required this.criRate,
@@ -41,9 +42,12 @@ class MonsterCombatStats {
/// 공격력 /// 공격력
final int atk; final int atk;
/// 방어력 /// 물리 방어력
final int def; final int def;
/// 마법 방어력
final int magDef;
/// 최대 HP /// 최대 HP
final int hpMax; final int hpMax;
@@ -96,6 +100,7 @@ class MonsterCombatStats {
int? level, int? level,
int? atk, int? atk,
int? def, int? def,
int? magDef,
int? hpMax, int? hpMax,
int? hpCurrent, int? hpCurrent,
double? criRate, double? criRate,
@@ -110,6 +115,7 @@ class MonsterCombatStats {
level: level ?? this.level, level: level ?? this.level,
atk: atk ?? this.atk, atk: atk ?? this.atk,
def: def ?? this.def, def: def ?? this.def,
magDef: magDef ?? this.magDef,
hpMax: hpMax ?? this.hpMax, hpMax: hpMax ?? this.hpMax,
hpCurrent: hpCurrent ?? this.hpCurrent, hpCurrent: hpCurrent ?? this.hpCurrent,
criRate: criRate ?? this.criRate, criRate: criRate ?? this.criRate,
@@ -173,11 +179,16 @@ class MonsterCombatStats {
MonsterSpeedType.slow => 1400, 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( return MonsterCombatStats(
name: name, name: name,
level: level, level: level,
atk: baseStats.atk, atk: baseStats.atk,
def: baseStats.def, def: baseStats.def,
magDef: magDef,
hpMax: adjustedHp, hpMax: adjustedHp,
hpCurrent: adjustedHp, hpCurrent: adjustedHp,
criRate: criRate, criRate: criRate,
@@ -202,6 +213,7 @@ class MonsterCombatStats {
level: bossLevel, level: bossLevel,
atk: bossStats.atk, atk: bossStats.atk,
def: bossStats.def, def: bossStats.def,
magDef: (bossStats.def * 1.2).round(), // 보스는 마법 방어력 20% 증가
hpMax: bossStats.hp, hpMax: bossStats.hp,
hpCurrent: bossStats.hp, hpCurrent: bossStats.hp,
criRate: 0.25, // 보스 크리티컬 확률 25% criRate: 0.25, // 보스 크리티컬 확률 25%
@@ -244,6 +256,7 @@ class MonsterCombatStats {
level: 1, level: 1,
atk: 8, atk: 8,
def: 3, def: 3,
magDef: 3,
hpMax: 35, hpMax: 35,
hpCurrent: 35, hpCurrent: 35,
criRate: 0.02, criRate: 0.02,
@@ -264,6 +277,7 @@ class MonsterCombatStats {
level: 0, // PvP에서는 레벨 페널티 없음 level: 0, // PvP에서는 레벨 페널티 없음
atk: stats.atk, atk: stats.atk,
def: stats.def, def: stats.def,
magDef: stats.magDef,
hpMax: stats.hpMax, hpMax: stats.hpMax,
hpCurrent: stats.hpMax, // 풀 HP로 시작 hpCurrent: stats.hpMax, // 풀 HP로 시작
criRate: stats.criRate, criRate: stats.criRate,

View File

@@ -4,8 +4,9 @@
/// 스펠 랭크에 따른 스킬 배율 계산 /// 스펠 랭크에 따른 스킬 배율 계산
/// ///
/// 랭크 1: 1.0x, 랭크 2: 1.15x, 랭크 3: 1.30x, ... /// 랭크 1: 1.0x, 랭크 2: 1.08x, 랭크 3: 1.16x, ... 최대 1.72x (rank 10)
double getRankMultiplier(int rank) => 1.0 + (rank - 1) * 0.15; /// Phase 3 밸런스: 0.15 → 0.08로 하향
double getRankMultiplier(int rank) => 1.0 + (rank - 1) * 0.08;
/// 랭크에 따른 쿨타임 감소율 계산 /// 랭크에 따른 쿨타임 감소율 계산
/// ///
@@ -38,6 +39,18 @@ enum SkillType {
debuff, debuff,
} }
/// 데미지 타입 (물리/마법 구분)
enum DamageType {
/// 물리 공격 - STR + atk 기반, 적 def로 방어
physical,
/// 마법 공격 - INT + magAtk 기반, 적 magDef로 방어
magical,
/// 하이브리드 - (atk + magAtk) / 2, (def + magDef) / 2
hybrid,
}
/// 스킬 속성 (하이브리드: 코드 + 시스템) /// 스킬 속성 (하이브리드: 코드 + 시스템)
enum SkillElement { enum SkillElement {
/// 논리 (Logic) - 순수 데미지 /// 논리 (Logic) - 순수 데미지
@@ -118,6 +131,7 @@ class Skill {
required this.cooldownMs, required this.cooldownMs,
required this.power, required this.power,
this.tier = 1, this.tier = 1,
this.damageType = DamageType.physical,
this.damageMultiplier = 1.0, this.damageMultiplier = 1.0,
this.healAmount = 0, this.healAmount = 0,
this.healPercent = 0.0, this.healPercent = 0.0,
@@ -137,6 +151,9 @@ class Skill {
/// 스킬 티어 (1~5, 높을수록 강함) /// 스킬 티어 (1~5, 높을수록 강함)
final int tier; final int tier;
/// 데미지 타입 (물리/마법/하이브리드)
final DamageType damageType;
/// 스킬 ID /// 스킬 ID
final String id; final String id;

View File

@@ -24,6 +24,7 @@ void main() {
level: 1, level: 1,
atk: 10, atk: 10,
def: 50, // DEF 50 → 50 * 0.4 = 20 감소 def: 50, // DEF 50 → 50 * 0.4 = 20 감소
magDef: 50,
hpMax: 100, hpMax: 100,
hpCurrent: 100, hpCurrent: 100,
criRate: 0.05, criRate: 0.05,
@@ -63,6 +64,7 @@ void main() {
level: 1, level: 1,
atk: 10, atk: 10,
def: 0, // DEF 0 def: 0, // DEF 0
magDef: 0,
hpMax: 500, hpMax: 500,
hpCurrent: 500, hpCurrent: 500,
criRate: 0.05, criRate: 0.05,
@@ -100,6 +102,7 @@ void main() {
level: 1, level: 1,
atk: 10, atk: 10,
def: 0, def: 0,
magDef: 0,
hpMax: 100, hpMax: 100,
hpCurrent: 100, hpCurrent: 100,
criRate: 0.05, criRate: 0.05,
@@ -135,6 +138,7 @@ void main() {
level: 1, level: 1,
atk: 100, atk: 100,
def: 0, def: 0,
magDef: 0,
hpMax: 100, hpMax: 100,
hpCurrent: 100, hpCurrent: 100,
criRate: 0.0, // 크리티컬 없음 criRate: 0.0, // 크리티컬 없음
@@ -174,6 +178,7 @@ void main() {
level: 1, level: 1,
atk: 100, atk: 100,
def: 0, def: 0,
magDef: 0,
hpMax: 100, hpMax: 100,
hpCurrent: 100, hpCurrent: 100,
criRate: 0.0, criRate: 0.0,
@@ -221,6 +226,7 @@ void main() {
level: 1, level: 1,
atk: 10, atk: 10,
def: 10, def: 10,
magDef: 10,
hpMax: 100, hpMax: 100,
hpCurrent: 100, hpCurrent: 100,
criRate: 0.05, criRate: 0.05,
@@ -255,6 +261,7 @@ void main() {
level: 1, level: 1,
atk: 10, atk: 10,
def: 5, def: 5,
magDef: 5,
hpMax: 50, hpMax: 50,
hpCurrent: 50, hpCurrent: 50,
criRate: 0.05, criRate: 0.05,
@@ -270,6 +277,7 @@ void main() {
level: 10, level: 10,
atk: 50, atk: 50,
def: 20, def: 20,
magDef: 20,
hpMax: 500, hpMax: 500,
hpCurrent: 500, hpCurrent: 500,
criRate: 0.05, criRate: 0.05,
@@ -314,6 +322,7 @@ void main() {
level: 1, level: 1,
atk: 30, atk: 30,
def: 10, def: 10,
magDef: 10,
hpMax: 80, hpMax: 80,
hpCurrent: 80, hpCurrent: 80,
criRate: 0.05, criRate: 0.05,

View File

@@ -103,6 +103,7 @@ void main() {
level: 1, level: 1,
atk: 10, atk: 10,
def: 50, // DEF 50 → 50 * 0.3 = 15 감소 def: 50, // DEF 50 → 50 * 0.3 = 15 감소
magDef: 50,
hpMax: 500, hpMax: 500,
hpCurrent: 500, hpCurrent: 500,
criRate: 0.05, criRate: 0.05,
@@ -124,7 +125,7 @@ void main() {
// ATK 100 * 2.0 - DEF 50 * 0.3 = 200 - 15 = 185 // ATK 100 * 2.0 - DEF 50 * 0.3 = 200 - 15 = 185
expect(result.result.success, isTrue); expect(result.result.success, isTrue);
expect(result.result.damage, equals(185)); 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 expect(result.updatedMonster.hpCurrent, equals(315)); // 500 - 185
}); });
@@ -143,6 +144,7 @@ void main() {
level: 1, level: 1,
atk: 10, atk: 10,
def: 0, def: 0,
magDef: 0,
hpMax: 500, hpMax: 500,
hpCurrent: 500, hpCurrent: 500,
criRate: 0.05, criRate: 0.05,
@@ -196,6 +198,7 @@ void main() {
level: 1, level: 1,
atk: 10, atk: 10,
def: 0, def: 0,
magDef: 0,
hpMax: 500, hpMax: 500,
hpCurrent: 500, hpCurrent: 500,
criRate: 0.05, criRate: 0.05,
@@ -217,7 +220,7 @@ void main() {
// 랭크 1: 1.0x → ATK 100 * 2.0 * 1.0 = 200 // 랭크 1: 1.0x → ATK 100 * 2.0 * 1.0 = 200
expect(result.result.damage, equals(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 데미지 스케일링', () { test('랭크 5 데미지 스케일링', () {
@@ -235,6 +238,7 @@ void main() {
level: 1, level: 1,
atk: 10, atk: 10,
def: 0, def: 0,
magDef: 0,
hpMax: 500, hpMax: 500,
hpCurrent: 500, hpCurrent: 500,
criRate: 0.05, criRate: 0.05,
@@ -254,12 +258,12 @@ void main() {
rank: 5, rank: 5,
); );
// 랭크 5: 1.6x (1.0 + 4 * 0.15) // 랭크 5: 1.32x (1.0 + 4 * 0.08) - 랭크 배율 하향
// ATK 100 * 2.0 * 1.6 = 320 // ATK 100 * 2.0 * 1.32 = 264
expect(result.result.damage, equals(320)); expect(result.result.damage, equals(264));
// MP 비용: 10 * (1.0 - 4 * 0.03) = 10 * 0.88 = 9 (반올림) // MP 비용: 30 * (1.0 - 4 * 0.03) = 30 * 0.88 = 26 (반올림)
expect(result.updatedPlayer.mpCurrent, equals(41)); // 50 - 9 expect(result.updatedPlayer.mpCurrent, equals(24)); // 50 - 26
}); });
}); });
@@ -268,12 +272,12 @@ void main() {
final rng = DeterministicRandom(42); final rng = DeterministicRandom(42);
final service = SkillService(rng: rng); final service = SkillService(rng: rng);
const skill = SkillData.hotReload; // healAmount: 30 const skill = SkillData.hotReload; // healAmount: 30, mpCost: 80
final player = CombatStats.empty().copyWith( final player = CombatStats.empty().copyWith(
hpMax: 200, hpMax: 200,
hpCurrent: 100, hpCurrent: 100,
mpMax: 100, mpMax: 200,
mpCurrent: 50, mpCurrent: 150,
); );
final skillSystem = SkillSystemState.empty().copyWith(elapsedMs: 5000); final skillSystem = SkillSystemState.empty().copyWith(elapsedMs: 5000);
@@ -286,7 +290,7 @@ void main() {
expect(result.result.success, isTrue); expect(result.result.success, isTrue);
expect(result.result.healedAmount, equals(30)); expect(result.result.healedAmount, equals(30));
expect(result.updatedPlayer.hpCurrent, equals(130)); // 100 + 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('퍼센트 회복량', () { test('퍼센트 회복량', () {
@@ -483,6 +487,7 @@ void main() {
level: 1, level: 1,
atk: 10, atk: 10,
def: 10, def: 10,
magDef: 10,
hpMax: 100, hpMax: 100,
hpCurrent: 100, hpCurrent: 100,
criRate: 0.05, criRate: 0.05,
@@ -512,14 +517,15 @@ void main() {
final player = CombatStats.empty().copyWith( final player = CombatStats.empty().copyWith(
hpMax: 100, hpMax: 100,
hpCurrent: 20, // 20% HP hpCurrent: 20, // 20% HP
mpMax: 100, mpMax: 200,
mpCurrent: 80, mpCurrent: 150, // 힐 스킬 사용 가능한 MP (garbageCollection: 130)
); );
final monster = MonsterCombatStats( final monster = MonsterCombatStats(
name: 'Test Monster', name: 'Test Monster',
level: 1, level: 1,
atk: 10, atk: 10,
def: 10, def: 10,
magDef: 10,
hpMax: 100, hpMax: 100,
hpCurrent: 100, hpCurrent: 100,
criRate: 0.05, criRate: 0.05,
@@ -548,10 +554,10 @@ void main() {
final rng = DeterministicRandom(42); final rng = DeterministicRandom(42);
final service = SkillService(rng: rng); final service = SkillService(rng: rng);
const skill = SkillData.debugMode; // ATK +25% 버프 const skill = SkillData.debugMode; // ATK +25% 버프, mpCost: 100
final player = CombatStats.empty().copyWith( final player = CombatStats.empty().copyWith(
mpMax: 100, mpMax: 200,
mpCurrent: 50, mpCurrent: 150,
); );
final skillSystem = SkillSystemState.empty().copyWith(elapsedMs: 5000); final skillSystem = SkillSystemState.empty().copyWith(elapsedMs: 5000);
@@ -568,7 +574,7 @@ void main() {
equals(0.25), equals(0.25),
); );
expect(result.updatedSkillSystem.activeBuffs.length, equals(1)); expect(result.updatedSkillSystem.activeBuffs.length, equals(1));
expect(result.updatedPlayer.mpCurrent, equals(30)); // 50 - 20 expect(result.updatedPlayer.mpCurrent, equals(50)); // 150 - 100
}); });
test('중복 버프 제거 후 새 버프 적용', () { test('중복 버프 제거 후 새 버프 적용', () {
@@ -648,11 +654,12 @@ void main() {
group('getRankMultiplier', () { group('getRankMultiplier', () {
test('랭크별 배율 계산', () { test('랭크별 배율 계산', () {
// 랭크 배율 하향: 0.15 → 0.08 per rank
expect(getRankMultiplier(1), equals(1.0)); expect(getRankMultiplier(1), equals(1.0));
expect(getRankMultiplier(2), closeTo(1.15, 0.001)); expect(getRankMultiplier(2), closeTo(1.08, 0.001));
expect(getRankMultiplier(3), closeTo(1.30, 0.001)); expect(getRankMultiplier(3), closeTo(1.16, 0.001));
expect(getRankMultiplier(5), closeTo(1.60, 0.001)); expect(getRankMultiplier(5), closeTo(1.32, 0.001));
expect(getRankMultiplier(10), closeTo(2.35, 0.001)); expect(getRankMultiplier(10), closeTo(1.72, 0.001));
}); });
}); });

View File

@@ -118,6 +118,7 @@ class MockFactories {
level: monsterLevel, level: monsterLevel,
atk: 10, atk: 10,
def: 5, def: 5,
magDef: 5,
hpMax: monsterHpMax, hpMax: monsterHpMax,
hpCurrent: monsterHpCurrent, hpCurrent: monsterHpCurrent,
criRate: 0.05, criRate: 0.05,
@@ -146,6 +147,7 @@ class MockFactories {
int level = 1, int level = 1,
int atk = 10, int atk = 10,
int def = 5, int def = 5,
int magDef = 5,
int hpMax = 100, int hpMax = 100,
int? hpCurrent, int? hpCurrent,
double criRate = 0.05, double criRate = 0.05,
@@ -160,6 +162,7 @@ class MockFactories {
level: level, level: level,
atk: atk, atk: atk,
def: def, def: def,
magDef: magDef,
hpMax: hpMax, hpMax: hpMax,
hpCurrent: hpCurrent ?? hpMax, hpCurrent: hpCurrent ?? hpMax,
criRate: criRate, criRate: criRate,
@@ -182,6 +185,7 @@ class MockFactories {
level: level, level: level,
atk: base.atk, atk: base.atk,
def: base.def, def: base.def,
magDef: base.def, // 물리 방어와 동일
hpMax: base.hp, hpMax: base.hp,
hpCurrent: base.hp, hpCurrent: base.hp,
criRate: 0.05, criRate: 0.05,