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
/// - 기본 무기 (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,
),
};
}
// ============================================================================

View File

@@ -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,
),
};
}
}