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 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 availableSkillIds, List activeDoTs = const [], List 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() .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 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 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 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 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 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 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.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 getAvailableSkillsFromSkillBook(SkillBook skillBook) { return skillBook.skills .map((entry) => SkillData.getSkillBySpellName(entry.name)) .whereType() .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 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.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, ), }; } }