diff --git a/lib/data/skill_data.dart b/lib/data/skill_data.dart new file mode 100644 index 0000000..cc384ab --- /dev/null +++ b/lib/data/skill_data.dart @@ -0,0 +1,233 @@ +import 'package:askiineverdie/src/core/model/skill.dart'; + +/// 게임 내 스킬 정의 +/// +/// 프로그래밍 테마에 맞춘 스킬 목록 +class SkillData { + SkillData._(); + + // ============================================================================ + // 공격 스킬 + // ============================================================================ + + /// Debug Strike - 기본 공격 스킬 + static const debugStrike = Skill( + id: 'debug_strike', + name: 'Debug Strike', + type: SkillType.attack, + mpCost: 10, + cooldownMs: 3000, // 3초 + power: 15, + damageMultiplier: 1.5, + ); + + /// Memory Leak - 방어력 감소 효과 + static const memoryLeak = Skill( + id: 'memory_leak', + name: 'Memory Leak', + type: SkillType.attack, + mpCost: 25, + cooldownMs: 8000, // 8초 + power: 25, + damageMultiplier: 2.5, + targetDefReduction: 0.2, // 적 방어력 -20% + ); + + /// Core Dump - 강력한 공격 + static const coreDump = Skill( + id: 'core_dump', + name: 'Core Dump', + type: SkillType.attack, + mpCost: 50, + cooldownMs: 20000, // 20초 + power: 40, + damageMultiplier: 4.0, + ); + + /// Kernel Panic - 최강 공격 (자해 데미지) + static const kernelPanic = Skill( + id: 'kernel_panic', + name: 'Kernel Panic', + type: SkillType.attack, + mpCost: 100, + cooldownMs: 60000, // 60초 + power: 80, + damageMultiplier: 8.0, + selfDamagePercent: 0.1, // 자신 HP -10% + ); + + /// Stack Overflow - 중급 공격 + static const stackOverflow = Skill( + id: 'stack_overflow', + name: 'Stack Overflow', + type: SkillType.attack, + mpCost: 35, + cooldownMs: 12000, // 12초 + power: 30, + damageMultiplier: 3.0, + ); + + /// Null Pointer - 빠른 공격 + static const nullPointer = Skill( + id: 'null_pointer', + name: 'Null Pointer', + type: SkillType.attack, + mpCost: 15, + cooldownMs: 4000, // 4초 + power: 18, + damageMultiplier: 1.8, + ); + + // ============================================================================ + // 회복 스킬 + // ============================================================================ + + /// Hot Reload - HP 회복 + static const hotReload = Skill( + id: 'hot_reload', + name: 'Hot Reload', + type: SkillType.heal, + mpCost: 20, + cooldownMs: 10000, // 10초 + power: 0, + healPercent: 0.3, // HP 30% 회복 + ); + + /// Garbage Collection - 대량 회복 + static const garbageCollection = Skill( + id: 'garbage_collection', + name: 'Garbage Collection', + type: SkillType.heal, + mpCost: 45, + cooldownMs: 25000, // 25초 + power: 0, + healPercent: 0.5, // HP 50% 회복 + ); + + /// Quick Fix - 빠른 소량 회복 + static const quickFix = Skill( + id: 'quick_fix', + name: 'Quick Fix', + type: SkillType.heal, + mpCost: 10, + cooldownMs: 5000, // 5초 + power: 0, + healAmount: 20, // 고정 20 회복 + ); + + // ============================================================================ + // 버프 스킬 + // ============================================================================ + + /// Safe Mode - 방어 버프 + static const safeMode = Skill( + id: 'safe_mode', + name: 'Safe Mode', + type: SkillType.buff, + mpCost: 30, + cooldownMs: 30000, // 30초 + power: 0, + buff: BuffEffect( + id: 'safe_mode_buff', + name: 'Safe Mode', + durationMs: 10000, // 10초 지속 + defModifier: 0.5, // 방어력 +50% + ), + ); + + /// Overclock - 공격 버프 + static const overclock = Skill( + id: 'overclock', + name: 'Overclock', + type: SkillType.buff, + mpCost: 25, + cooldownMs: 25000, // 25초 + power: 0, + buff: BuffEffect( + id: 'overclock_buff', + name: 'Overclock', + durationMs: 8000, // 8초 지속 + atkModifier: 0.4, // 공격력 +40% + criRateModifier: 0.1, // 크리티컬 +10% + ), + ); + + /// Firewall - 회피 버프 + static const firewall = Skill( + id: 'firewall', + name: 'Firewall', + type: SkillType.buff, + mpCost: 20, + cooldownMs: 20000, // 20초 + power: 0, + buff: BuffEffect( + id: 'firewall_buff', + name: 'Firewall', + durationMs: 12000, // 12초 지속 + evasionModifier: 0.15, // 회피율 +15% + defModifier: 0.2, // 방어력 +20% + ), + ); + + // ============================================================================ + // 스킬 목록 + // ============================================================================ + + /// 모든 스킬 목록 + static const List allSkills = [ + // 공격 스킬 + debugStrike, + nullPointer, + memoryLeak, + stackOverflow, + coreDump, + kernelPanic, + // 회복 스킬 + quickFix, + hotReload, + garbageCollection, + // 버프 스킬 + overclock, + safeMode, + firewall, + ]; + + /// ID로 스킬 찾기 + static Skill? getSkillById(String id) { + for (final skill in allSkills) { + if (skill.id == id) return skill; + } + return null; + } + + /// 타입별 스킬 목록 + static List getSkillsByType(SkillType type) { + return allSkills.where((s) => s.type == type).toList(); + } + + /// 공격 스킬 목록 (MP 효율순 정렬) + static List get attackSkillsByEfficiency { + final attacks = getSkillsByType(SkillType.attack); + attacks.sort((a, b) => b.mpEfficiency.compareTo(a.mpEfficiency)); + return attacks; + } + + /// 회복 스킬 목록 + static List get healSkills => getSkillsByType(SkillType.heal); + + /// 버프 스킬 목록 + static List get buffSkills => getSkillsByType(SkillType.buff); + + /// 기본 스킬 세트 (새 캐릭터용) + static List get defaultSkillIds => [ + debugStrike.id, + quickFix.id, + ]; + + /// MP 비용 이하의 사용 가능한 공격 스킬 + static List getAffordableAttackSkills(int currentMp) { + return getSkillsByType(SkillType.attack) + .where((s) => s.mpCost <= currentMp) + .toList(); + } +} diff --git a/lib/src/core/engine/progress_service.dart b/lib/src/core/engine/progress_service.dart index 50dca80..3624387 100644 --- a/lib/src/core/engine/progress_service.dart +++ b/lib/src/core/engine/progress_service.dart @@ -1,9 +1,11 @@ import 'dart:math' as math; import 'package:askiineverdie/data/game_text_l10n.dart' as l10n; +import 'package:askiineverdie/data/skill_data.dart'; import 'package:askiineverdie/src/core/engine/combat_calculator.dart'; import 'package:askiineverdie/src/core/engine/game_mutations.dart'; import 'package:askiineverdie/src/core/engine/reward_service.dart'; +import 'package:askiineverdie/src/core/engine/skill_service.dart'; import 'package:askiineverdie/src/core/model/combat_state.dart'; import 'package:askiineverdie/src/core/model/combat_stats.dart'; import 'package:askiineverdie/src/core/model/game_state.dart'; @@ -141,6 +143,34 @@ class ProgressService { var questDone = false; var actDone = false; + // 스킬 시스템 시간 업데이트 (Phase 3) + final skillService = SkillService(rng: state.rng); + var skillSystem = skillService.updateElapsedTime(state.skillSystem, clamped); + + // 만료된 버프 정리 + skillSystem = skillService.cleanupExpiredBuffs(skillSystem); + + // 비전투 시 MP 회복 + final isInCombat = progress.currentTask.type == TaskType.kill && + progress.currentCombat != null && + progress.currentCombat!.isActive; + + if (!isInCombat && nextState.stats.mp < nextState.stats.mpMax) { + final mpRegen = skillService.calculateMpRegen( + elapsedMs: clamped, + isInCombat: false, + wis: nextState.stats.wis, + ); + if (mpRegen > 0) { + final newMp = (nextState.stats.mp + mpRegen).clamp(0, nextState.stats.mpMax); + nextState = nextState.copyWith( + stats: nextState.stats.copyWith(mpCurrent: newMp), + ); + } + } + + nextState = nextState.copyWith(skillSystem: skillSystem); + // Advance task bar if still running. if (progress.task.position < progress.task.max) { final uncapped = progress.task.position + clamped; @@ -148,10 +178,18 @@ class ProgressService { ? progress.task.max : uncapped; - // 킬 태스크 중 전투 진행 + // 킬 태스크 중 전투 진행 (스킬 자동 사용 포함) var updatedCombat = progress.currentCombat; + var updatedSkillSystem = nextState.skillSystem; if (progress.currentTask.type == TaskType.kill && updatedCombat != null && updatedCombat.isActive) { - updatedCombat = _processCombatTick(nextState, updatedCombat, clamped); + final combatResult = _processCombatTickWithSkills( + nextState, + updatedCombat, + updatedSkillSystem, + clamped, + ); + updatedCombat = combatResult.combat; + updatedSkillSystem = combatResult.skillSystem; } progress = progress.copyWith( @@ -159,7 +197,7 @@ class ProgressService { currentCombat: updatedCombat, ); nextState = _recalculateEncumbrance( - nextState.copyWith(progress: progress), + nextState.copyWith(progress: progress, skillSystem: updatedSkillSystem), ); return ProgressTickResult(state: nextState); } @@ -791,22 +829,25 @@ class ProgressService { ); } - /// 전투 틱 처리 + /// 전투 틱 처리 (스킬 자동 사용 포함) /// /// [state] 현재 게임 상태 /// [combat] 현재 전투 상태 + /// [skillSystem] 스킬 시스템 상태 /// [elapsedMs] 경과 시간 (밀리초) - /// Returns: 업데이트된 전투 상태 - CombatState _processCombatTick( + /// Returns: 업데이트된 전투 상태 및 스킬 시스템 상태 + ({CombatState combat, SkillSystemState skillSystem}) _processCombatTickWithSkills( GameState state, CombatState combat, + SkillSystemState skillSystem, int elapsedMs, ) { if (!combat.isActive || combat.isCombatOver) { - return combat; + return (combat: combat, skillSystem: skillSystem); } final calculator = CombatCalculator(rng: state.rng); + final skillService = SkillService(rng: state.rng); var playerStats = combat.playerStats; var monsterStats = combat.monsterStats; var playerAccumulator = combat.playerAttackAccumulatorMs + elapsedMs; @@ -814,15 +855,66 @@ class ProgressService { var totalDamageDealt = combat.totalDamageDealt; var totalDamageTaken = combat.totalDamageTaken; var turnsElapsed = combat.turnsElapsed; + var updatedSkillSystem = skillSystem; // 플레이어 공격 체크 if (playerAccumulator >= playerStats.attackDelayMs) { - final attackResult = calculator.playerAttackMonster( - attacker: playerStats, - defender: monsterStats, + // 스킬 자동 선택 + final availableSkillIds = updatedSkillSystem.skillStates + .map((s) => s.skillId) + .toList(); + // 기본 스킬이 없으면 기본 스킬 추가 + if (availableSkillIds.isEmpty) { + availableSkillIds.addAll(SkillData.defaultSkillIds); + } + + final selectedSkill = skillService.selectAutoSkill( + player: playerStats, + monster: monsterStats, + skillSystem: updatedSkillSystem, + availableSkillIds: availableSkillIds, ); - monsterStats = attackResult.updatedDefender; - totalDamageDealt += attackResult.result.damage; + + if (selectedSkill != null && selectedSkill.isAttack) { + // 공격 스킬 사용 + final skillResult = skillService.useAttackSkill( + skill: selectedSkill, + player: playerStats, + monster: monsterStats, + skillSystem: updatedSkillSystem, + ); + playerStats = skillResult.updatedPlayer; + monsterStats = skillResult.updatedMonster; + totalDamageDealt += skillResult.result.damage; + updatedSkillSystem = skillResult.updatedSkillSystem; + } else if (selectedSkill != null && selectedSkill.isHeal) { + // 회복 스킬 사용 + final skillResult = skillService.useHealSkill( + skill: selectedSkill, + player: playerStats, + skillSystem: updatedSkillSystem, + ); + playerStats = skillResult.updatedPlayer; + updatedSkillSystem = skillResult.updatedSkillSystem; + } else if (selectedSkill != null && selectedSkill.isBuff) { + // 버프 스킬 사용 + final skillResult = skillService.useBuffSkill( + skill: selectedSkill, + player: playerStats, + skillSystem: updatedSkillSystem, + ); + playerStats = skillResult.updatedPlayer; + updatedSkillSystem = skillResult.updatedSkillSystem; + } else { + // 일반 공격 + final attackResult = calculator.playerAttackMonster( + attacker: playerStats, + defender: monsterStats, + ); + monsterStats = attackResult.updatedDefender; + totalDamageDealt += attackResult.result.damage; + } + playerAccumulator -= playerStats.attackDelayMs; turnsElapsed++; } @@ -841,15 +933,18 @@ class ProgressService { // 전투 종료 체크 final isActive = playerStats.isAlive && monsterStats.isAlive; - return combat.copyWith( - playerStats: playerStats, - monsterStats: monsterStats, - playerAttackAccumulatorMs: playerAccumulator, - monsterAttackAccumulatorMs: monsterAccumulator, - totalDamageDealt: totalDamageDealt, - totalDamageTaken: totalDamageTaken, - turnsElapsed: turnsElapsed, - isActive: isActive, + return ( + combat: combat.copyWith( + playerStats: playerStats, + monsterStats: monsterStats, + playerAttackAccumulatorMs: playerAccumulator, + monsterAttackAccumulatorMs: monsterAccumulator, + totalDamageDealt: totalDamageDealt, + totalDamageTaken: totalDamageTaken, + turnsElapsed: turnsElapsed, + isActive: isActive, + ), + skillSystem: updatedSkillSystem, ); } } diff --git a/lib/src/core/engine/skill_service.dart b/lib/src/core/engine/skill_service.dart new file mode 100644 index 0000000..90478da --- /dev/null +++ b/lib/src/core/engine/skill_service.dart @@ -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 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() + .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 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; + } + + // ============================================================================ + // 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); + } +} diff --git a/lib/src/core/model/game_state.dart b/lib/src/core/model/game_state.dart index c568e92..b250119 100644 --- a/lib/src/core/model/game_state.dart +++ b/lib/src/core/model/game_state.dart @@ -4,6 +4,7 @@ import 'package:askiineverdie/src/core/model/combat_state.dart'; import 'package:askiineverdie/src/core/model/equipment_item.dart'; import 'package:askiineverdie/src/core/model/equipment_slot.dart'; import 'package:askiineverdie/src/core/model/item_stats.dart'; +import 'package:askiineverdie/src/core/model/skill.dart'; import 'package:askiineverdie/src/core/util/deterministic_random.dart'; /// Minimal skeletal state to mirror Progress Quest structures. @@ -20,6 +21,7 @@ class GameState { SpellBook? spellBook, ProgressState? progress, QueueState? queue, + SkillSystemState? skillSystem, }) : rng = DeterministicRandom.clone(rng), traits = traits ?? Traits.empty(), stats = stats ?? Stats.empty(), @@ -27,7 +29,8 @@ class GameState { equipment = equipment ?? Equipment.empty(), spellBook = spellBook ?? SpellBook.empty(), progress = progress ?? ProgressState.empty(), - queue = queue ?? QueueState.empty(); + queue = queue ?? QueueState.empty(), + skillSystem = skillSystem ?? SkillSystemState.empty(); factory GameState.withSeed({ required int seed, @@ -38,6 +41,7 @@ class GameState { SpellBook? spellBook, ProgressState? progress, QueueState? queue, + SkillSystemState? skillSystem, }) { return GameState( rng: DeterministicRandom(seed), @@ -48,6 +52,7 @@ class GameState { spellBook: spellBook, progress: progress, queue: queue, + skillSystem: skillSystem, ); } @@ -60,6 +65,9 @@ class GameState { final ProgressState progress; final QueueState queue; + /// 스킬 시스템 상태 (Phase 3) + final SkillSystemState skillSystem; + GameState copyWith({ DeterministicRandom? rng, Traits? traits, @@ -69,6 +77,7 @@ class GameState { SpellBook? spellBook, ProgressState? progress, QueueState? queue, + SkillSystemState? skillSystem, }) { return GameState( rng: rng ?? DeterministicRandom.clone(this.rng), @@ -79,6 +88,76 @@ class GameState { spellBook: spellBook ?? this.spellBook, progress: progress ?? this.progress, queue: queue ?? this.queue, + skillSystem: skillSystem ?? this.skillSystem, + ); + } +} + +/// 스킬 시스템 상태 (Phase 3) +/// +/// 스킬 쿨타임, 활성 버프, 게임 경과 시간 등을 관리 +class SkillSystemState { + const SkillSystemState({ + required this.skillStates, + required this.activeBuffs, + required this.elapsedMs, + }); + + /// 스킬별 쿨타임 상태 + final List skillStates; + + /// 현재 활성화된 버프 목록 + final List activeBuffs; + + /// 게임 진행 경과 시간 (밀리초, 스킬 쿨타임 계산용) + final int elapsedMs; + + factory SkillSystemState.empty() => const SkillSystemState( + skillStates: [], + activeBuffs: [], + elapsedMs: 0, + ); + + /// 특정 스킬 상태 가져오기 + SkillState? getSkillState(String skillId) { + for (final state in skillStates) { + if (state.skillId == skillId) return state; + } + return null; + } + + /// 버프 효과 합산 (동일 버프는 중복 적용 안 됨) + ({double atkMod, double defMod, double criMod, double evasionMod}) get totalBuffModifiers { + double atkMod = 0; + double defMod = 0; + double criMod = 0; + double evasionMod = 0; + + final seenBuffIds = {}; + for (final buff in activeBuffs) { + if (seenBuffIds.contains(buff.effect.id)) continue; + seenBuffIds.add(buff.effect.id); + + if (!buff.isExpired(elapsedMs)) { + atkMod += buff.effect.atkModifier; + defMod += buff.effect.defModifier; + criMod += buff.effect.criRateModifier; + evasionMod += buff.effect.evasionModifier; + } + } + + return (atkMod: atkMod, defMod: defMod, criMod: criMod, evasionMod: evasionMod); + } + + SkillSystemState copyWith({ + List? skillStates, + List? activeBuffs, + int? elapsedMs, + }) { + return SkillSystemState( + skillStates: skillStates ?? this.skillStates, + activeBuffs: activeBuffs ?? this.activeBuffs, + elapsedMs: elapsedMs ?? this.elapsedMs, ); } } diff --git a/lib/src/core/model/skill.dart b/lib/src/core/model/skill.dart new file mode 100644 index 0000000..ae2d182 --- /dev/null +++ b/lib/src/core/model/skill.dart @@ -0,0 +1,267 @@ +/// 스킬 타입 +enum SkillType { + /// 공격 스킬 + attack, + + /// 회복 스킬 + heal, + + /// 버프 스킬 + buff, + + /// 디버프 스킬 + debuff, +} + +/// 버프 효과 +class BuffEffect { + const BuffEffect({ + required this.id, + required this.name, + required this.durationMs, + this.atkModifier = 0.0, + this.defModifier = 0.0, + this.criRateModifier = 0.0, + this.evasionModifier = 0.0, + }); + + /// 버프 ID + final String id; + + /// 버프 이름 + final String name; + + /// 지속 시간 (밀리초) + final int durationMs; + + /// 공격력 배율 보정 (0.0 = 변화 없음, 0.5 = +50%) + final double atkModifier; + + /// 방어력 배율 보정 + final double defModifier; + + /// 크리티컬 확률 보정 + final double criRateModifier; + + /// 회피율 보정 + final double evasionModifier; +} + +/// 스킬 정의 +class Skill { + const Skill({ + required this.id, + required this.name, + required this.type, + required this.mpCost, + required this.cooldownMs, + required this.power, + this.damageMultiplier = 1.0, + this.healAmount = 0, + this.healPercent = 0.0, + this.buff, + this.selfDamagePercent = 0.0, + this.targetDefReduction = 0.0, + }); + + /// 스킬 ID + final String id; + + /// 스킬 이름 + final String name; + + /// 스킬 타입 + final SkillType type; + + /// MP 소모량 + final int mpCost; + + /// 쿨타임 (밀리초) + final int cooldownMs; + + /// 스킬 위력 (기본 값) + final int power; + + /// 데미지 배율 (공격 스킬용) + final double damageMultiplier; + + /// 고정 회복량 (회복 스킬용) + final int healAmount; + + /// HP% 회복 (회복 스킬용, 0.0 ~ 1.0) + final double healPercent; + + /// 버프 효과 (버프/디버프 스킬용) + final BuffEffect? buff; + + /// 자해 데미지 % (일부 강력한 스킬) + final double selfDamagePercent; + + /// 적 방어력 감소 % (일부 공격 스킬) + final double targetDefReduction; + + /// 공격 스킬 여부 + bool get isAttack => type == SkillType.attack; + + /// 회복 스킬 여부 + bool get isHeal => type == SkillType.heal; + + /// 버프 스킬 여부 + bool get isBuff => type == SkillType.buff; + + /// 디버프 스킬 여부 + bool get isDebuff => type == SkillType.debuff; + + /// MP 효율 (데미지 당 MP 비용) + double get mpEfficiency { + if (type != SkillType.attack || damageMultiplier <= 0) return 0; + return damageMultiplier / mpCost; + } +} + +/// 스킬 사용 상태 (쿨타임 추적) +class SkillState { + const SkillState({ + required this.skillId, + required this.lastUsedMs, + required this.rank, + }); + + /// 스킬 ID + final String skillId; + + /// 마지막 사용 시간 (게임 내 경과 시간, 밀리초) + final int lastUsedMs; + + /// 스킬 랭크 (레벨) + final int rank; + + /// 쿨타임 완료 여부 + bool isReady(int currentMs, int cooldownMs) { + return currentMs - lastUsedMs >= cooldownMs; + } + + /// 남은 쿨타임 (밀리초) + int remainingCooldown(int currentMs, int cooldownMs) { + final elapsed = currentMs - lastUsedMs; + if (elapsed >= cooldownMs) return 0; + return cooldownMs - elapsed; + } + + SkillState copyWith({ + String? skillId, + int? lastUsedMs, + int? rank, + }) { + return SkillState( + skillId: skillId ?? this.skillId, + lastUsedMs: lastUsedMs ?? this.lastUsedMs, + rank: rank ?? this.rank, + ); + } + + /// 새 스킬 상태 생성 (쿨타임 0) + factory SkillState.fresh(String skillId, {int rank = 1}) { + return SkillState( + skillId: skillId, + lastUsedMs: -999999, // 즉시 사용 가능하도록 먼 과거 + rank: rank, + ); + } +} + +/// 활성 버프 상태 +class ActiveBuff { + const ActiveBuff({ + required this.effect, + required this.startedMs, + required this.sourceSkillId, + }); + + /// 버프 효과 + final BuffEffect effect; + + /// 버프 시작 시간 (게임 내 경과 시간) + final int startedMs; + + /// 버프를 발동한 스킬 ID + final String sourceSkillId; + + /// 버프 만료 여부 + bool isExpired(int currentMs) { + return currentMs - startedMs >= effect.durationMs; + } + + /// 남은 지속 시간 (밀리초) + int remainingDuration(int currentMs) { + final elapsed = currentMs - startedMs; + if (elapsed >= effect.durationMs) return 0; + return effect.durationMs - elapsed; + } + + ActiveBuff copyWith({ + BuffEffect? effect, + int? startedMs, + String? sourceSkillId, + }) { + return ActiveBuff( + effect: effect ?? this.effect, + startedMs: startedMs ?? this.startedMs, + sourceSkillId: sourceSkillId ?? this.sourceSkillId, + ); + } +} + +/// 스킬 사용 결과 +class SkillUseResult { + const SkillUseResult({ + required this.skill, + required this.success, + this.damage = 0, + this.healedAmount = 0, + this.appliedBuff, + this.failReason, + }); + + /// 사용한 스킬 + final Skill skill; + + /// 성공 여부 + final bool success; + + /// 데미지 (공격 스킬) + final int damage; + + /// 회복량 (회복 스킬) + final int healedAmount; + + /// 적용된 버프 (버프 스킬) + final ActiveBuff? appliedBuff; + + /// 실패 사유 + final SkillFailReason? failReason; + + /// 실패 결과 생성 + factory SkillUseResult.failed(Skill skill, SkillFailReason reason) { + return SkillUseResult( + skill: skill, + success: false, + failReason: reason, + ); + } +} + +/// 스킬 실패 사유 +enum SkillFailReason { + /// MP 부족 + notEnoughMp, + + /// 쿨타임 중 + onCooldown, + + /// 스킬 없음 + skillNotFound, + + /// 사용 불가 상태 + invalidState, +}