feat(skill): DamageType 및 magAtk/magDef 스킬 시스템 추가
- DamageType enum 추가 (physical/magical) - 스킬별 데미지 타입 지정 기능 구현 - 마법 스킬 데미지에 magAtk/magDef 적용 - 장비 아이템에서 magAtk/magDef 스탯 추출 - 관련 테스트 업데이트
This commit is contained in:
@@ -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,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user