- DamageType enum 추가 (physical/magical) - 스킬별 데미지 타입 지정 기능 구현 - 마법 스킬 데미지에 magAtk/magDef 적용 - 장비 아이템에서 magAtk/magDef 스탯 추출 - 관련 테스트 업데이트
754 lines
23 KiB
Dart
754 lines
23 KiB
Dart
import 'package:asciineverdie/data/skill_data.dart';
|
||
import 'package:asciineverdie/src/core/model/combat_stats.dart';
|
||
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||
import 'package:asciineverdie/src/core/model/monster_combat_stats.dart';
|
||
import 'package:asciineverdie/src/core/model/skill.dart';
|
||
import 'package:asciineverdie/src/core/util/deterministic_random.dart';
|
||
import 'package:asciineverdie/src/core/util/roman.dart';
|
||
|
||
/// 스킬 시스템 서비스
|
||
///
|
||
/// 스킬 사용, 쿨타임 관리, MP 관리, 자동 스킬 선택 등을 담당
|
||
class SkillService {
|
||
const SkillService({required this.rng});
|
||
|
||
final DeterministicRandom rng;
|
||
|
||
// ============================================================================
|
||
// 스킬 사용 가능 여부 확인
|
||
// ============================================================================
|
||
|
||
/// 스킬 사용 가능 여부 확인
|
||
SkillFailReason? canUseSkill({
|
||
required Skill skill,
|
||
required int currentMp,
|
||
required SkillSystemState skillSystem,
|
||
}) {
|
||
// GCD 체크 (글로벌 쿨타임 1500ms)
|
||
if (skillSystem.isGlobalCooldownActive) {
|
||
return SkillFailReason.onGlobalCooldown;
|
||
}
|
||
|
||
// MP 체크
|
||
if (currentMp < skill.mpCost) {
|
||
return SkillFailReason.notEnoughMp;
|
||
}
|
||
|
||
// 쿨타임 체크
|
||
final skillState = skillSystem.getSkillState(skill.id);
|
||
if (skillState != null &&
|
||
!skillState.isReady(skillSystem.elapsedMs, skill.cooldownMs)) {
|
||
return SkillFailReason.onCooldown;
|
||
}
|
||
|
||
return null; // 사용 가능
|
||
}
|
||
|
||
// ============================================================================
|
||
// 스킬 사용
|
||
// ============================================================================
|
||
|
||
/// 공격 스킬 사용
|
||
///
|
||
/// Returns: (결과, 업데이트된 플레이어 스탯, 업데이트된 몬스터 스탯)
|
||
({
|
||
SkillUseResult result,
|
||
CombatStats updatedPlayer,
|
||
MonsterCombatStats updatedMonster,
|
||
SkillSystemState updatedSkillSystem,
|
||
})
|
||
useAttackSkill({
|
||
required Skill skill,
|
||
required CombatStats player,
|
||
required MonsterCombatStats monster,
|
||
required SkillSystemState skillSystem,
|
||
}) {
|
||
// 데미지 타입에 따른 공격력/방어력 선택
|
||
final (attackStat, defenseStat) = _getStatsByDamageType(
|
||
skill.damageType,
|
||
player,
|
||
monster,
|
||
);
|
||
|
||
// 기본 데미지 계산
|
||
final baseDamage = attackStat * skill.damageMultiplier;
|
||
|
||
// 버프 효과 적용
|
||
final buffMods = skillSystem.totalBuffModifiers;
|
||
final buffedDamage = baseDamage * (1 + buffMods.atkMod);
|
||
|
||
// 적 방어력 감소 적용
|
||
final effectiveMonsterDef = defenseStat * (1 - skill.targetDefReduction);
|
||
|
||
// 최종 데미지 계산 (방어력 감산 0.3)
|
||
final finalDamage = (buffedDamage - effectiveMonsterDef * 0.3)
|
||
.round()
|
||
.clamp(1, 9999);
|
||
|
||
// 몬스터에 데미지 적용
|
||
var updatedMonster = monster.applyDamage(finalDamage);
|
||
|
||
// 자해 데미지 적용
|
||
var updatedPlayer = player;
|
||
if (skill.selfDamagePercent > 0) {
|
||
final selfDamage = (player.hpMax * skill.selfDamagePercent).round();
|
||
updatedPlayer = player.applyDamage(selfDamage);
|
||
}
|
||
|
||
// MP 소모
|
||
updatedPlayer = updatedPlayer.withMp(
|
||
updatedPlayer.mpCurrent - skill.mpCost,
|
||
);
|
||
|
||
// 스킬 상태 업데이트 (쿨타임 시작)
|
||
final updatedSkillSystem = _updateSkillCooldown(skillSystem, skill.id);
|
||
|
||
return (
|
||
result: SkillUseResult(skill: skill, success: true, damage: finalDamage),
|
||
updatedPlayer: updatedPlayer,
|
||
updatedMonster: updatedMonster,
|
||
updatedSkillSystem: updatedSkillSystem,
|
||
);
|
||
}
|
||
|
||
/// 회복 스킬 사용
|
||
({
|
||
SkillUseResult result,
|
||
CombatStats updatedPlayer,
|
||
SkillSystemState updatedSkillSystem,
|
||
})
|
||
useHealSkill({
|
||
required Skill skill,
|
||
required CombatStats player,
|
||
required SkillSystemState skillSystem,
|
||
}) {
|
||
// 회복량 계산
|
||
int healAmount = skill.healAmount;
|
||
if (skill.healPercent > 0) {
|
||
healAmount += (player.hpMax * skill.healPercent).round();
|
||
}
|
||
|
||
// HP 회복
|
||
var updatedPlayer = player.applyHeal(healAmount);
|
||
|
||
// MP 소모
|
||
updatedPlayer = updatedPlayer.withMp(
|
||
updatedPlayer.mpCurrent - skill.mpCost,
|
||
);
|
||
|
||
// 스킬 상태 업데이트
|
||
final updatedSkillSystem = _updateSkillCooldown(skillSystem, skill.id);
|
||
|
||
return (
|
||
result: SkillUseResult(
|
||
skill: skill,
|
||
success: true,
|
||
healedAmount: healAmount,
|
||
),
|
||
updatedPlayer: updatedPlayer,
|
||
updatedSkillSystem: updatedSkillSystem,
|
||
);
|
||
}
|
||
|
||
/// 버프 스킬 사용
|
||
({
|
||
SkillUseResult result,
|
||
CombatStats updatedPlayer,
|
||
SkillSystemState updatedSkillSystem,
|
||
})
|
||
useBuffSkill({
|
||
required Skill skill,
|
||
required CombatStats player,
|
||
required SkillSystemState skillSystem,
|
||
}) {
|
||
if (skill.buff == null) {
|
||
return (
|
||
result: SkillUseResult.failed(skill, SkillFailReason.invalidState),
|
||
updatedPlayer: player,
|
||
updatedSkillSystem: skillSystem,
|
||
);
|
||
}
|
||
|
||
// 버프 적용
|
||
final newBuff = ActiveBuff(
|
||
effect: skill.buff!,
|
||
startedMs: skillSystem.elapsedMs,
|
||
sourceSkillId: skill.id,
|
||
);
|
||
|
||
// 기존 같은 버프 제거 후 새 버프 추가
|
||
final updatedBuffs =
|
||
skillSystem.activeBuffs
|
||
.where((b) => b.effect.id != skill.buff!.id)
|
||
.toList()
|
||
..add(newBuff);
|
||
|
||
// MP 소모
|
||
var updatedPlayer = player.withMp(player.mpCurrent - skill.mpCost);
|
||
|
||
// 스킬 상태 업데이트
|
||
var updatedSkillSystem = _updateSkillCooldown(skillSystem, skill.id);
|
||
updatedSkillSystem = updatedSkillSystem.copyWith(activeBuffs: updatedBuffs);
|
||
|
||
return (
|
||
result: SkillUseResult(skill: skill, success: true, appliedBuff: newBuff),
|
||
updatedPlayer: updatedPlayer,
|
||
updatedSkillSystem: updatedSkillSystem,
|
||
);
|
||
}
|
||
|
||
/// 디버프 스킬 사용
|
||
///
|
||
/// 디버프 효과를 생성하여 반환. 호출자가 CombatState.activeDebuffs에 추가해야 함.
|
||
/// 디버프는 몬스터의 ATK/DEF를 감소시킴.
|
||
({
|
||
SkillUseResult result,
|
||
CombatStats updatedPlayer,
|
||
SkillSystemState updatedSkillSystem,
|
||
ActiveBuff? debuffEffect,
|
||
})
|
||
useDebuffSkill({
|
||
required Skill skill,
|
||
required CombatStats player,
|
||
required SkillSystemState skillSystem,
|
||
required List<ActiveBuff> currentDebuffs,
|
||
}) {
|
||
if (skill.buff == null) {
|
||
return (
|
||
result: SkillUseResult.failed(skill, SkillFailReason.invalidState),
|
||
updatedPlayer: player,
|
||
updatedSkillSystem: skillSystem,
|
||
debuffEffect: null,
|
||
);
|
||
}
|
||
|
||
// 디버프 효과 생성
|
||
final newDebuff = ActiveBuff(
|
||
effect: skill.buff!,
|
||
startedMs: skillSystem.elapsedMs,
|
||
sourceSkillId: skill.id,
|
||
);
|
||
|
||
// MP 소모
|
||
var updatedPlayer = player.withMp(player.mpCurrent - skill.mpCost);
|
||
|
||
// 스킬 상태 업데이트 (쿨타임 시작)
|
||
final updatedSkillSystem = _updateSkillCooldown(skillSystem, skill.id);
|
||
|
||
return (
|
||
result: SkillUseResult(
|
||
skill: skill,
|
||
success: true,
|
||
appliedBuff: newDebuff,
|
||
),
|
||
updatedPlayer: updatedPlayer,
|
||
updatedSkillSystem: updatedSkillSystem,
|
||
debuffEffect: newDebuff,
|
||
);
|
||
}
|
||
|
||
/// DOT 스킬 사용
|
||
///
|
||
/// DOT 효과를 생성하여 반환. 호출자가 전투 상태의 activeDoTs에 추가해야 함.
|
||
/// INT → 틱당 데미지 보정, WIS → 틱 간격 보정
|
||
({
|
||
SkillUseResult result,
|
||
CombatStats updatedPlayer,
|
||
SkillSystemState updatedSkillSystem,
|
||
DotEffect? dotEffect,
|
||
})
|
||
useDotSkill({
|
||
required Skill skill,
|
||
required CombatStats player,
|
||
required SkillSystemState skillSystem,
|
||
required int playerInt,
|
||
required int playerWis,
|
||
}) {
|
||
if (!skill.isDot) {
|
||
return (
|
||
result: SkillUseResult.failed(skill, SkillFailReason.invalidState),
|
||
updatedPlayer: player,
|
||
updatedSkillSystem: skillSystem,
|
||
dotEffect: null,
|
||
);
|
||
}
|
||
|
||
// DOT 효과 생성 (INT/WIS 보정 적용)
|
||
final dotEffect = DotEffect.fromSkill(
|
||
skill,
|
||
playerInt: playerInt,
|
||
playerWis: playerWis,
|
||
);
|
||
|
||
// MP 소모
|
||
var updatedPlayer = player.withMp(player.mpCurrent - skill.mpCost);
|
||
|
||
// 스킬 상태 업데이트 (쿨타임 시작)
|
||
final updatedSkillSystem = _updateSkillCooldown(skillSystem, skill.id);
|
||
|
||
// 예상 총 데미지 계산 (틱 수 × 틱당 데미지)
|
||
final expectedTicks = dotEffect.totalDurationMs ~/ dotEffect.tickIntervalMs;
|
||
final expectedDamage = expectedTicks * dotEffect.damagePerTick;
|
||
|
||
return (
|
||
result: SkillUseResult(
|
||
skill: skill,
|
||
success: true,
|
||
damage: expectedDamage,
|
||
),
|
||
updatedPlayer: updatedPlayer,
|
||
updatedSkillSystem: updatedSkillSystem,
|
||
dotEffect: dotEffect,
|
||
);
|
||
}
|
||
|
||
// ============================================================================
|
||
// 자동 스킬 선택
|
||
// ============================================================================
|
||
|
||
/// 전투 중 자동 스킬 선택
|
||
///
|
||
/// 우선순위:
|
||
/// 1. HP < 30% → 회복 스킬 (최우선)
|
||
/// 2. 70% 확률로 일반 공격 (MP 절약, 기본 전투)
|
||
/// 3. 30% 확률로 스킬 사용:
|
||
/// - 버프: HP > 80% & MP > 60% & 활성 버프 없음
|
||
/// - 디버프: 몬스터 HP > 80% & 활성 디버프 없음
|
||
/// - DOT: 몬스터 HP > 60% & 활성 DOT 없음
|
||
/// - 공격: 보스전이면 강력한 스킬, 일반전이면 효율적 스킬
|
||
/// 4. MP < 20% → 일반 공격
|
||
Skill? selectAutoSkill({
|
||
required CombatStats player,
|
||
required MonsterCombatStats monster,
|
||
required SkillSystemState skillSystem,
|
||
required List<String> availableSkillIds,
|
||
List<DotEffect> activeDoTs = const [],
|
||
List<ActiveBuff> activeDebuffs = const [],
|
||
}) {
|
||
final currentMp = player.mpCurrent;
|
||
final mpRatio = player.mpRatio;
|
||
final hpRatio = player.hpRatio;
|
||
|
||
// MP 20% 미만이면 일반 공격
|
||
if (mpRatio < 0.2) return null;
|
||
|
||
// 사용 가능한 스킬 필터링
|
||
final availableSkills = availableSkillIds
|
||
.map((id) => SkillData.getSkillById(id))
|
||
.whereType<Skill>()
|
||
.where(
|
||
(skill) =>
|
||
canUseSkill(
|
||
skill: skill,
|
||
currentMp: currentMp,
|
||
skillSystem: skillSystem,
|
||
) ==
|
||
null,
|
||
)
|
||
.toList();
|
||
|
||
if (availableSkills.isEmpty) return null;
|
||
|
||
// HP < 30% → 회복 스킬 최우선 (생존)
|
||
if (hpRatio < 0.3) {
|
||
final healSkill = _findBestHealSkill(availableSkills, currentMp);
|
||
if (healSkill != null) return healSkill;
|
||
}
|
||
|
||
// 70% 확률로 일반 공격 (스킬은 특별한 상황에서만)
|
||
final useNormalAttack = rng.nextInt(100) < 70;
|
||
if (useNormalAttack) return null;
|
||
|
||
// === 아래부터 30% 확률로 스킬 사용 ===
|
||
|
||
// 버프: HP > 80% & MP > 60% (매우 안전할 때만)
|
||
// 활성 버프가 있으면 건너뜀 (중복 방지)
|
||
if (hpRatio > 0.8 && mpRatio > 0.6) {
|
||
final hasActiveBuff = skillSystem.activeBuffs.isNotEmpty;
|
||
if (!hasActiveBuff) {
|
||
final buffSkill = _findBestBuffSkill(availableSkills, currentMp);
|
||
if (buffSkill != null) return buffSkill;
|
||
}
|
||
}
|
||
|
||
// 디버프: 몬스터 HP > 80% & 활성 디버프 없음 (전투 초반)
|
||
if (monster.hpRatio > 0.8 && activeDebuffs.isEmpty) {
|
||
final debuffSkill = _findBestDebuffSkill(availableSkills, currentMp);
|
||
if (debuffSkill != null) return debuffSkill;
|
||
}
|
||
|
||
// DOT: 몬스터 HP > 60% & 활성 DOT 없음 (장기전 유리)
|
||
if (monster.hpRatio > 0.6 && activeDoTs.isEmpty) {
|
||
final dotSkill = _findBestDotSkill(availableSkills, currentMp);
|
||
if (dotSkill != null) return dotSkill;
|
||
}
|
||
|
||
// 보스전 판단 (몬스터 레벨 20 이상 & HP 50% 이상)
|
||
final isBossFight = monster.level >= 20 && monster.hpRatio > 0.5;
|
||
|
||
if (isBossFight) {
|
||
// 가장 강력한 공격 스킬
|
||
return _findStrongestAttackSkill(availableSkills);
|
||
}
|
||
|
||
// 일반 전투 → MP 효율 좋은 공격 스킬
|
||
return _findEfficientAttackSkill(availableSkills);
|
||
}
|
||
|
||
/// 가장 좋은 DOT 스킬 찾기
|
||
///
|
||
/// 예상 총 데미지 (틱 × 데미지) 기준으로 선택
|
||
Skill? _findBestDotSkill(List<Skill> skills, int currentMp) {
|
||
final dotSkills = skills
|
||
.where((s) => s.isDot && s.mpCost <= currentMp)
|
||
.toList();
|
||
|
||
if (dotSkills.isEmpty) return null;
|
||
|
||
// 예상 총 데미지 기준 정렬
|
||
dotSkills.sort((a, b) {
|
||
final aTotal =
|
||
(a.baseDotDamage ?? 0) *
|
||
((a.baseDotDurationMs ?? 0) ~/ (a.baseDotTickMs ?? 1000));
|
||
final bTotal =
|
||
(b.baseDotDamage ?? 0) *
|
||
((b.baseDotDurationMs ?? 0) ~/ (b.baseDotTickMs ?? 1000));
|
||
return bTotal.compareTo(aTotal);
|
||
});
|
||
|
||
return dotSkills.first;
|
||
}
|
||
|
||
/// 가장 좋은 회복 스킬 찾기
|
||
Skill? _findBestHealSkill(List<Skill> skills, int currentMp) {
|
||
final healSkills = skills
|
||
.where((s) => s.isHeal && s.mpCost <= currentMp)
|
||
.toList();
|
||
|
||
if (healSkills.isEmpty) return null;
|
||
|
||
// 회복량 기준 정렬 (% 회복 > 고정 회복)
|
||
healSkills.sort((a, b) {
|
||
final aValue = a.healPercent * 100 + a.healAmount;
|
||
final bValue = b.healPercent * 100 + b.healAmount;
|
||
return bValue.compareTo(aValue);
|
||
});
|
||
|
||
return healSkills.first;
|
||
}
|
||
|
||
/// 가장 강력한 공격 스킬 찾기
|
||
Skill? _findStrongestAttackSkill(List<Skill> skills) {
|
||
final attackSkills = skills.where((s) => s.isAttack).toList();
|
||
if (attackSkills.isEmpty) return null;
|
||
|
||
attackSkills.sort(
|
||
(a, b) => b.damageMultiplier.compareTo(a.damageMultiplier),
|
||
);
|
||
return attackSkills.first;
|
||
}
|
||
|
||
/// MP 효율 좋은 공격 스킬 찾기
|
||
Skill? _findEfficientAttackSkill(List<Skill> skills) {
|
||
final attackSkills = skills.where((s) => s.isAttack).toList();
|
||
if (attackSkills.isEmpty) return null;
|
||
|
||
attackSkills.sort((a, b) => b.mpEfficiency.compareTo(a.mpEfficiency));
|
||
return attackSkills.first;
|
||
}
|
||
|
||
/// 가장 좋은 버프 스킬 찾기
|
||
///
|
||
/// ATK 증가 버프 우선, 그 다음 복합 버프
|
||
Skill? _findBestBuffSkill(List<Skill> skills, int currentMp) {
|
||
final buffSkills = skills
|
||
.where((s) => s.isBuff && s.mpCost <= currentMp && s.buff != null)
|
||
.toList();
|
||
|
||
if (buffSkills.isEmpty) return null;
|
||
|
||
// ATK 증가량 기준 정렬
|
||
buffSkills.sort((a, b) {
|
||
final aValue =
|
||
(a.buff?.atkModifier ?? 0) +
|
||
(a.buff?.defModifier ?? 0) * 0.5 +
|
||
(a.buff?.criRateModifier ?? 0) * 0.3;
|
||
final bValue =
|
||
(b.buff?.atkModifier ?? 0) +
|
||
(b.buff?.defModifier ?? 0) * 0.5 +
|
||
(b.buff?.criRateModifier ?? 0) * 0.3;
|
||
return bValue.compareTo(aValue);
|
||
});
|
||
|
||
return buffSkills.first;
|
||
}
|
||
|
||
/// 가장 좋은 디버프 스킬 찾기
|
||
///
|
||
/// 적 ATK 감소 디버프 우선
|
||
Skill? _findBestDebuffSkill(List<Skill> skills, int currentMp) {
|
||
final debuffSkills = skills
|
||
.where((s) => s.isDebuff && s.mpCost <= currentMp && s.buff != null)
|
||
.toList();
|
||
|
||
if (debuffSkills.isEmpty) return null;
|
||
|
||
// 디버프 효과 크기 기준 정렬 (음수 값이므로 절대값으로 비교)
|
||
debuffSkills.sort((a, b) {
|
||
final aValue =
|
||
(a.buff?.atkModifier ?? 0).abs() +
|
||
(a.buff?.defModifier ?? 0).abs() * 0.5;
|
||
final bValue =
|
||
(b.buff?.atkModifier ?? 0).abs() +
|
||
(b.buff?.defModifier ?? 0).abs() * 0.5;
|
||
return bValue.compareTo(aValue);
|
||
});
|
||
|
||
return debuffSkills.first;
|
||
}
|
||
|
||
// ============================================================================
|
||
// MP 회복
|
||
// ============================================================================
|
||
|
||
/// MP 자연 회복
|
||
///
|
||
/// [elapsedMs] 경과 시간 (밀리초)
|
||
/// [isInCombat] 전투 중 여부
|
||
/// [wis] 지혜 스탯 (회복 속도 보정)
|
||
int calculateMpRegen({
|
||
required int elapsedMs,
|
||
required bool isInCombat,
|
||
required int wis,
|
||
}) {
|
||
if (isInCombat) {
|
||
// 전투 중: WIS에 비례한 느린 회복 (500ms당 1 + WIS/20)
|
||
final regenPerTick = 1 + wis ~/ 20;
|
||
return (elapsedMs ~/ 500) * regenPerTick;
|
||
} else {
|
||
// 비전투: 50ms당 1 회복
|
||
return elapsedMs ~/ 50;
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// 버프 관리
|
||
// ============================================================================
|
||
|
||
/// 만료된 버프 제거
|
||
SkillSystemState cleanupExpiredBuffs(SkillSystemState state) {
|
||
final activeBuffs = state.activeBuffs
|
||
.where((b) => !b.isExpired(state.elapsedMs))
|
||
.toList();
|
||
|
||
return state.copyWith(activeBuffs: activeBuffs);
|
||
}
|
||
|
||
// ============================================================================
|
||
// 유틸리티
|
||
// ============================================================================
|
||
|
||
/// 스킬 쿨타임 업데이트
|
||
SkillSystemState _updateSkillCooldown(
|
||
SkillSystemState state,
|
||
String skillId,
|
||
) {
|
||
final skillStates = List<SkillState>.from(state.skillStates);
|
||
|
||
// 기존 상태 찾기
|
||
final existingIndex = skillStates.indexWhere((s) => s.skillId == skillId);
|
||
|
||
if (existingIndex >= 0) {
|
||
// 기존 상태 업데이트
|
||
skillStates[existingIndex] = skillStates[existingIndex].copyWith(
|
||
lastUsedMs: state.elapsedMs,
|
||
);
|
||
} else {
|
||
// 새 상태 추가
|
||
skillStates.add(
|
||
SkillState(skillId: skillId, lastUsedMs: state.elapsedMs, rank: 1),
|
||
);
|
||
}
|
||
|
||
return state.copyWith(skillStates: skillStates);
|
||
}
|
||
|
||
/// 스킬 시스템 시간 업데이트
|
||
SkillSystemState updateElapsedTime(SkillSystemState state, int deltaMs) {
|
||
return state.copyWith(elapsedMs: state.elapsedMs + deltaMs);
|
||
}
|
||
|
||
// ============================================================================
|
||
// SkillBook 연동
|
||
// ============================================================================
|
||
|
||
/// SkillBook에서 사용 가능한 스킬 목록 조회
|
||
///
|
||
/// SkillEntry 이름을 Skill로 매핑하여 반환
|
||
List<Skill> getAvailableSkillsFromSkillBook(SkillBook skillBook) {
|
||
return skillBook.skills
|
||
.map((entry) => SkillData.getSkillBySpellName(entry.name))
|
||
.whereType<Skill>()
|
||
.toList();
|
||
}
|
||
|
||
/// SkillBook에서 스킬의 랭크(레벨) 조회
|
||
///
|
||
/// 로마숫자 랭크(I, II, III)를 정수로 변환하여 반환
|
||
/// 스킬이 없으면 1 반환
|
||
int getSkillRankFromSkillBook(SkillBook skillBook, String skillId) {
|
||
// skillId로 스킬 찾기
|
||
final skill = SkillData.getSkillById(skillId);
|
||
if (skill == null) return 1;
|
||
|
||
// 스킬 이름으로 SkillEntry 찾기
|
||
for (final entry in skillBook.skills) {
|
||
if (entry.name == skill.name) {
|
||
return romanToInt(entry.rank);
|
||
}
|
||
}
|
||
|
||
return 1; // 기본 랭크
|
||
}
|
||
|
||
/// SkillBook에서 스킬 ID 목록 조회
|
||
///
|
||
/// 전투 시스템에서 사용 가능한 스킬 ID 목록 반환
|
||
List<String> getAvailableSkillIdsFromSkillBook(SkillBook skillBook) {
|
||
return getAvailableSkillsFromSkillBook(
|
||
skillBook,
|
||
).map((skill) => skill.id).toList();
|
||
}
|
||
|
||
/// 랭크 스케일링이 적용된 공격 스킬 사용
|
||
///
|
||
/// [rank] 스펠 랭크 (SkillBook에서 조회)
|
||
({
|
||
SkillUseResult result,
|
||
CombatStats updatedPlayer,
|
||
MonsterCombatStats updatedMonster,
|
||
SkillSystemState updatedSkillSystem,
|
||
})
|
||
useAttackSkillWithRank({
|
||
required Skill skill,
|
||
required CombatStats player,
|
||
required MonsterCombatStats monster,
|
||
required SkillSystemState skillSystem,
|
||
required int rank,
|
||
}) {
|
||
// 랭크 스케일링 적용
|
||
final rankMult = getRankMultiplier(rank);
|
||
final mpMult = getRankMpMultiplier(rank);
|
||
|
||
// 실제 MP 비용 계산
|
||
final actualMpCost = (skill.mpCost * mpMult).round();
|
||
|
||
// 데미지 타입에 따른 공격력/방어력 선택
|
||
final (attackStat, defenseStat) = _getStatsByDamageType(
|
||
skill.damageType,
|
||
player,
|
||
monster,
|
||
);
|
||
|
||
// 기본 데미지 계산 (랭크 배율 적용)
|
||
final baseDamage = attackStat * skill.damageMultiplier * rankMult;
|
||
|
||
// 버프 효과 적용
|
||
final buffMods = skillSystem.totalBuffModifiers;
|
||
final buffedDamage = baseDamage * (1 + buffMods.atkMod);
|
||
|
||
// 적 방어력 감소 적용
|
||
final effectiveMonsterDef = defenseStat * (1 - skill.targetDefReduction);
|
||
|
||
// 최종 데미지 계산 (방어력 감산 0.3)
|
||
final finalDamage = (buffedDamage - effectiveMonsterDef * 0.3)
|
||
.round()
|
||
.clamp(1, 9999);
|
||
|
||
// 몬스터에 데미지 적용
|
||
var updatedMonster = monster.applyDamage(finalDamage);
|
||
|
||
// 자해 데미지 적용
|
||
var updatedPlayer = player;
|
||
if (skill.selfDamagePercent > 0) {
|
||
final selfDamage = (player.hpMax * skill.selfDamagePercent).round();
|
||
updatedPlayer = player.applyDamage(selfDamage);
|
||
}
|
||
|
||
// MP 소모 (랭크 스케일링 적용)
|
||
updatedPlayer = updatedPlayer.withMp(
|
||
updatedPlayer.mpCurrent - actualMpCost,
|
||
);
|
||
|
||
// 스킬 상태 업데이트 (쿨타임 시작, 랭크 저장)
|
||
// 쿨타임 스케일링은 isReady 체크 시 적용됨
|
||
final updatedSkillSystem = _updateSkillCooldownWithRank(
|
||
skillSystem,
|
||
skill.id,
|
||
rank,
|
||
);
|
||
|
||
return (
|
||
result: SkillUseResult(skill: skill, success: true, damage: finalDamage),
|
||
updatedPlayer: updatedPlayer,
|
||
updatedMonster: updatedMonster,
|
||
updatedSkillSystem: updatedSkillSystem,
|
||
);
|
||
}
|
||
|
||
/// 랭크 정보를 포함한 스킬 쿨타임 업데이트
|
||
SkillSystemState _updateSkillCooldownWithRank(
|
||
SkillSystemState state,
|
||
String skillId,
|
||
int rank,
|
||
) {
|
||
final skillStates = List<SkillState>.from(state.skillStates);
|
||
|
||
// 기존 상태 찾기
|
||
final existingIndex = skillStates.indexWhere((s) => s.skillId == skillId);
|
||
|
||
if (existingIndex >= 0) {
|
||
// 기존 상태 업데이트
|
||
skillStates[existingIndex] = skillStates[existingIndex].copyWith(
|
||
lastUsedMs: state.elapsedMs,
|
||
rank: rank,
|
||
);
|
||
} else {
|
||
// 새 상태 추가
|
||
skillStates.add(
|
||
SkillState(skillId: skillId, lastUsedMs: state.elapsedMs, rank: rank),
|
||
);
|
||
}
|
||
|
||
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,
|
||
),
|
||
};
|
||
}
|
||
}
|