feat(skill): Phase 3 MP 기반 스킬 시스템 구현
- Skill, SkillType, BuffEffect, SkillState, SkillUseResult 클래스 정의 (skill.dart) - SkillSystemState를 GameState에 통합 (activeBuffs, skillStates, elapsedMs) - 프로그래밍 테마 스킬 데이터 정의 (skill_data.dart) - 공격: Debug Strike, Memory Leak, Core Dump, Kernel Panic 등 - 회복: Hot Reload, Garbage Collection, Quick Fix - 버프: Safe Mode, Overclock, Firewall - SkillService 구현 (skill_service.dart) - 스킬 사용 가능 여부 확인 (MP, 쿨타임) - 공격/회복/버프 스킬 사용 로직 - 자동 스킬 선택 (HP < 30% → 회복, 보스전 → 강력한 공격, 일반 → MP 효율) - MP 자연 회복 (비전투: 50ms당 1, 전투: WIS 기반) - progress_service.dart에 스킬 시스템 통합 - tick()에서 스킬 시간 업데이트 및 버프 만료 처리 - _processCombatTickWithSkills()로 전투 중 자동 스킬 사용
This commit is contained in:
345
lib/src/core/engine/skill_service.dart
Normal file
345
lib/src/core/engine/skill_service.dart
Normal file
@@ -0,0 +1,345 @@
|
||||
import 'package:askiineverdie/data/skill_data.dart';
|
||||
import 'package:askiineverdie/src/core/model/combat_stats.dart';
|
||||
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||
import 'package:askiineverdie/src/core/model/monster_combat_stats.dart';
|
||||
import 'package:askiineverdie/src/core/model/skill.dart';
|
||||
import 'package:askiineverdie/src/core/util/deterministic_random.dart';
|
||||
|
||||
/// 스킬 시스템 서비스
|
||||
///
|
||||
/// 스킬 사용, 쿨타임 관리, MP 관리, 자동 스킬 선택 등을 담당
|
||||
class SkillService {
|
||||
const SkillService({required this.rng});
|
||||
|
||||
final DeterministicRandom rng;
|
||||
|
||||
// ============================================================================
|
||||
// 스킬 사용 가능 여부 확인
|
||||
// ============================================================================
|
||||
|
||||
/// 스킬 사용 가능 여부 확인
|
||||
SkillFailReason? canUseSkill({
|
||||
required Skill skill,
|
||||
required int currentMp,
|
||||
required SkillSystemState skillSystem,
|
||||
}) {
|
||||
// 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 baseDamage = player.atk * skill.damageMultiplier;
|
||||
|
||||
// 버프 효과 적용
|
||||
final buffMods = skillSystem.totalBuffModifiers;
|
||||
final buffedDamage = baseDamage * (1 + buffMods.atkMod);
|
||||
|
||||
// 적 방어력 감소 적용
|
||||
final effectiveMonsterDef = monster.def * (1 - skill.targetDefReduction);
|
||||
|
||||
// 최종 데미지 계산 (방어력 감산)
|
||||
final finalDamage = (buffedDamage - effectiveMonsterDef * 0.5).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,
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 자동 스킬 선택
|
||||
// ============================================================================
|
||||
|
||||
/// 전투 중 자동 스킬 선택
|
||||
///
|
||||
/// 우선순위:
|
||||
/// 1. HP < 30% → 회복 스킬
|
||||
/// 2. 보스전 (레벨 차이 10 이상) → 가장 강력한 공격 스킬
|
||||
/// 3. 일반 전투 → MP 효율이 좋은 스킬
|
||||
/// 4. MP < 20% → null (일반 공격)
|
||||
Skill? selectAutoSkill({
|
||||
required CombatStats player,
|
||||
required MonsterCombatStats monster,
|
||||
required SkillSystemState skillSystem,
|
||||
required List<String> availableSkillIds,
|
||||
}) {
|
||||
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;
|
||||
}
|
||||
|
||||
// 보스전 판단 (몬스터 레벨이 높음)
|
||||
final isBossFight = monster.level >= 10 && monster.hpRatio > 0.5;
|
||||
|
||||
if (isBossFight) {
|
||||
// 가장 강력한 공격 스킬
|
||||
return _findStrongestAttackSkill(availableSkills);
|
||||
}
|
||||
|
||||
// 일반 전투 → MP 효율 좋은 스킬
|
||||
return _findEfficientAttackSkill(availableSkills);
|
||||
}
|
||||
|
||||
/// 가장 좋은 회복 스킬 찾기
|
||||
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;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user