diff --git a/lib/src/core/engine/arena_service.dart b/lib/src/core/engine/arena_service.dart index 5991f02..ee99e22 100644 --- a/lib/src/core/engine/arena_service.dart +++ b/lib/src/core/engine/arena_service.dart @@ -2,7 +2,6 @@ import 'package:asciineverdie/data/skill_data.dart'; import 'package:asciineverdie/src/core/engine/combat_calculator.dart'; import 'package:asciineverdie/src/core/engine/skill_service.dart'; import 'package:asciineverdie/src/core/model/arena_match.dart'; -import 'package:asciineverdie/src/core/model/combat_stats.dart'; import 'package:asciineverdie/src/core/model/equipment_item.dart'; import 'package:asciineverdie/src/core/model/equipment_slot.dart'; import 'package:asciineverdie/src/core/model/game_state.dart'; @@ -47,64 +46,38 @@ class ArenaService { return skills; } - /// AI 스킬 선택 (우선순위: 회복 > 버프 > 공격) - Skill? _selectBestSkill({ - required List skills, - required CombatStats stats, - required SkillSystemState skillSystem, - required MonsterCombatStats? target, - }) { - if (skills.isEmpty) return null; + /// 스킬 ID 목록 추출 (HallOfFameEntry에서) + List _getSkillIdsFromEntry(HallOfFameEntry entry) { + return _getSkillsFromEntry(entry).map((s) => s.id).toList(); + } - final currentMp = stats.mpCurrent; - final hpRatio = stats.hpCurrent / stats.hpMax; + /// 스킬 랭크 조회 (HallOfFameEntry의 finalSkills에서) + int _getSkillRankFromEntry(HallOfFameEntry entry, String skillId) { + final skill = SkillData.getSkillById(skillId); + if (skill == null) return 1; - // HP가 낮으면 회복 스킬 우선 - if (hpRatio < 0.4) { - for (final skill in skills) { - if (skill.type == SkillType.heal && - _skillService.canUseSkill( - skill: skill, - currentMp: currentMp, - skillSystem: skillSystem, - ) == - null) { - return skill; - } + final skillData = entry.finalSkills; + if (skillData == null || skillData.isEmpty) return 1; + + for (final data in skillData) { + if (data['name'] == skill.name) { + final rankStr = data['rank'] ?? 'I'; + return _romanToInt(rankStr); } } + return 1; + } - // 버프가 없으면 버프 스킬 - if (skillSystem.activeBuffs.isEmpty) { - for (final skill in skills) { - if (skill.type == SkillType.buff && - _skillService.canUseSkill( - skill: skill, - currentMp: currentMp, - skillSystem: skillSystem, - ) == - null) { - return skill; - } - } - } - - // MP가 충분하면 공격 스킬 - if (currentMp >= 30) { - for (final skill in skills) { - if (skill.type == SkillType.attack && - _skillService.canUseSkill( - skill: skill, - currentMp: currentMp, - skillSystem: skillSystem, - ) == - null) { - return skill; - } - } - } - - return null; // 스킬 사용 안 함 (기본 공격) + /// 로마 숫자 → 정수 변환 + int _romanToInt(String roman) { + return switch (roman) { + 'I' => 1, + 'II' => 2, + 'III' => 3, + 'IV' => 4, + 'V' => 5, + _ => 1, + }; } // ============================================================================ @@ -223,8 +196,40 @@ class ArenaService { ); } + /// 시뮬레이션 결과를 기반으로 전투 결과 생성 + /// + /// [match] 대전 정보 + /// [challengerHp] 도전자 최종 HP + /// [opponentHp] 상대 최종 HP + /// [turns] 총 턴 수 + /// Returns: 대전 결과 (승패, 장비 교환 후 캐릭터) + ArenaMatchResult createResultFromSimulation({ + required ArenaMatch match, + required int challengerHp, + required int opponentHp, + required int turns, + }) { + // 도전자 HP가 0보다 크면 승리 + final isVictory = challengerHp > 0 && opponentHp <= 0; + + // 장비 교환 + final (updatedChallenger, updatedOpponent) = _exchangeEquipment( + match: match, + isVictory: isVictory, + ); + + return ArenaMatchResult( + match: match, + isVictory: isVictory, + turns: turns, + updatedChallenger: updatedChallenger, + updatedOpponent: updatedOpponent, + ); + } + /// 전투 시뮬레이션 (애니메이션용 스트림) /// + /// progress_service._processCombatTickWithSkills()와 동일한 로직 사용 /// [match] 대전 정보 /// Returns: 턴별 전투 상황 스트림 Stream simulateCombat(ArenaMatch match) async* { @@ -237,30 +242,48 @@ class ArenaService { return; } - // 스킬 목록 로드 - final challengerSkills = _getSkillsFromEntry(match.challenger); - final opponentSkills = _getSkillsFromEntry(match.opponent); + // 스킬 ID 목록 로드 (SkillBook과 동일한 방식) + var challengerSkillIds = _getSkillIdsFromEntry(match.challenger); + var opponentSkillIds = _getSkillIdsFromEntry(match.opponent); + + // 스킬이 없으면 기본 스킬 사용 + if (challengerSkillIds.isEmpty) { + challengerSkillIds = SkillData.defaultSkillIds; + } + if (opponentSkillIds.isEmpty) { + opponentSkillIds = SkillData.defaultSkillIds; + } // 스킬 시스템 상태 초기화 var challengerSkillSystem = SkillSystemState.empty(); var opponentSkillSystem = SkillSystemState.empty(); + // DOT 및 디버프 추적 (일반 전투와 동일) + var challengerDoTs = []; + var opponentDoTs = []; + var challengerDebuffs = []; + var opponentDebuffs = []; + var playerCombatStats = challengerStats.copyWith( hpCurrent: challengerStats.hpMax, mpCurrent: challengerStats.mpMax, ); - // 상대도 CombatStats로 관리 (스킬 사용 위해) var opponentCombatStats = opponentStats.copyWith( hpCurrent: opponentStats.hpMax, mpCurrent: opponentStats.mpMax, ); var opponentMonsterStats = MonsterCombatStats.fromCombatStats( - opponentStats, + opponentCombatStats, match.opponent.characterName, ); + var challengerMonsterStats = MonsterCombatStats.fromCombatStats( + playerCombatStats, + match.challenger.characterName, + ); + int playerAccum = 0; int opponentAccum = 0; int elapsedMs = 0; @@ -301,47 +324,164 @@ class ArenaService { int? challengerHealAmount; int? opponentHealAmount; - // 도전자 턴 + // ========================================================================= + // DOT 틱 처리 (도전자 → 상대에게 적용된 DOT) + // ========================================================================= + var dotDamageToOpponent = 0; + final updatedChallengerDoTs = []; + for (final dot in challengerDoTs) { + final (updatedDot, ticksTriggered) = dot.tick(tickMs); + if (ticksTriggered > 0) { + dotDamageToOpponent += dot.damagePerTick * ticksTriggered; + } + if (updatedDot.isActive) { + updatedChallengerDoTs.add(updatedDot); + } + } + challengerDoTs = updatedChallengerDoTs; + + if (dotDamageToOpponent > 0 && opponentCombatStats.hpCurrent > 0) { + opponentCombatStats = opponentCombatStats.copyWith( + hpCurrent: (opponentCombatStats.hpCurrent - dotDamageToOpponent) + .clamp(0, opponentCombatStats.hpMax), + ); + } + + // DOT 틱 처리 (상대 → 도전자에게 적용된 DOT) + var dotDamageToChallenger = 0; + final updatedOpponentDoTs = []; + for (final dot in opponentDoTs) { + final (updatedDot, ticksTriggered) = dot.tick(tickMs); + if (ticksTriggered > 0) { + dotDamageToChallenger += dot.damagePerTick * ticksTriggered; + } + if (updatedDot.isActive) { + updatedOpponentDoTs.add(updatedDot); + } + } + opponentDoTs = updatedOpponentDoTs; + + if (dotDamageToChallenger > 0 && playerCombatStats.isAlive) { + playerCombatStats = playerCombatStats.copyWith( + hpCurrent: (playerCombatStats.hpCurrent - dotDamageToChallenger) + .clamp(0, playerCombatStats.hpMax), + ); + } + + // ========================================================================= + // 만료된 디버프 정리 + // ========================================================================= + challengerDebuffs = challengerDebuffs + .where((ActiveBuff d) => !d.isExpired(elapsedMs)) + .toList(); + opponentDebuffs = opponentDebuffs + .where((ActiveBuff d) => !d.isExpired(elapsedMs)) + .toList(); + + // ========================================================================= + // 도전자 턴 (selectAutoSkill 사용 - 일반 전투와 동일) + // ========================================================================= if (playerAccum >= playerCombatStats.attackDelayMs) { playerAccum = 0; - // 스킬 선택 - final skill = _selectBestSkill( - skills: challengerSkills, - stats: playerCombatStats, - skillSystem: challengerSkillSystem, - target: opponentMonsterStats, + // 상대 몬스터 스탯 동기화 + opponentMonsterStats = MonsterCombatStats.fromCombatStats( + opponentCombatStats, + match.opponent.characterName, ); - if (skill != null) { - // 스킬 사용 - final skillResult = _useSkill( - skill: skill, - attacker: playerCombatStats, - defender: opponentCombatStats, + // 스킬 자동 선택 (progress_service와 동일한 로직) + final selectedSkill = _skillService.selectAutoSkill( + player: playerCombatStats, + monster: opponentMonsterStats, + skillSystem: challengerSkillSystem, + availableSkillIds: challengerSkillIds, + activeDoTs: challengerDoTs, + activeDebuffs: opponentDebuffs, + ); + + if (selectedSkill != null && selectedSkill.isAttack) { + // 스킬 랭크 조회 및 적용 + final skillRank = _getSkillRankFromEntry( + match.challenger, + selectedSkill.id, + ); + final skillResult = _skillService.useAttackSkillWithRank( + skill: selectedSkill, + player: playerCombatStats, + monster: opponentMonsterStats, + skillSystem: challengerSkillSystem, + rank: skillRank, + ); + playerCombatStats = skillResult.updatedPlayer; + opponentCombatStats = opponentCombatStats.copyWith( + hpCurrent: skillResult.updatedMonster.hpCurrent, + ); + challengerSkillSystem = skillResult.updatedSkillSystem; + challengerSkillUsed = selectedSkill.name; + challengerDamage = skillResult.result.damage; + } else if (selectedSkill != null && selectedSkill.isDot) { + // DOT 스킬 사용 + final skillResult = _skillService.useDotSkill( + skill: selectedSkill, + player: playerCombatStats, + skillSystem: challengerSkillSystem, + playerInt: playerCombatStats.atk ~/ 10, + playerWis: playerCombatStats.def ~/ 10, + ); + playerCombatStats = skillResult.updatedPlayer; + challengerSkillSystem = skillResult.updatedSkillSystem; + if (skillResult.dotEffect != null) { + challengerDoTs.add(skillResult.dotEffect!); + } + challengerSkillUsed = selectedSkill.name; + } else if (selectedSkill != null && selectedSkill.isHeal) { + // 회복 스킬 사용 + final skillResult = _skillService.useHealSkill( + skill: selectedSkill, + player: playerCombatStats, skillSystem: challengerSkillSystem, ); - playerCombatStats = skillResult.updatedAttacker; - opponentCombatStats = skillResult.updatedDefender; + playerCombatStats = skillResult.updatedPlayer; challengerSkillSystem = skillResult.updatedSkillSystem; - challengerSkillUsed = skill.name; - challengerDamage = skillResult.damage; - challengerHealAmount = skillResult.healAmount; - isChallengerCritical = skillResult.isCritical; - } else { - // 기본 공격 - // 상대 몬스터 스탯 동기화 - opponentMonsterStats = MonsterCombatStats.fromCombatStats( - opponentCombatStats, - match.opponent.characterName, + challengerSkillUsed = selectedSkill.name; + challengerHealAmount = skillResult.result.healedAmount; + } else if (selectedSkill != null && selectedSkill.isBuff) { + // 버프 스킬 사용 + final skillResult = _skillService.useBuffSkill( + skill: selectedSkill, + player: playerCombatStats, + skillSystem: challengerSkillSystem, ); + playerCombatStats = skillResult.updatedPlayer; + challengerSkillSystem = skillResult.updatedSkillSystem; + challengerSkillUsed = selectedSkill.name; + } else if (selectedSkill != null && selectedSkill.isDebuff) { + // 디버프 스킬 사용 + final skillResult = _skillService.useDebuffSkill( + skill: selectedSkill, + player: playerCombatStats, + skillSystem: challengerSkillSystem, + currentDebuffs: opponentDebuffs, + ); + playerCombatStats = skillResult.updatedPlayer; + challengerSkillSystem = skillResult.updatedSkillSystem; + final debuffEffect = skillResult.debuffEffect; + if (debuffEffect != null) { + opponentDebuffs = opponentDebuffs + .where((ActiveBuff d) => d.effect.id != debuffEffect.effect.id) + .toList() + ..add(debuffEffect); + } + challengerSkillUsed = selectedSkill.name; + } else { + // 일반 공격 final result = calculator.playerAttackMonster( attacker: playerCombatStats, defender: opponentMonsterStats, ); - opponentMonsterStats = result.updatedDefender; opponentCombatStats = opponentCombatStats.copyWith( - hpCurrent: opponentMonsterStats.hpCurrent, + hpCurrent: result.updatedDefender.hpCurrent, ); if (result.result.isHit) { @@ -353,38 +493,121 @@ class ArenaService { } } - // 상대 턴 + // ========================================================================= + // 상대 턴 (selectAutoSkill 사용 - 일반 전투와 동일) + // ========================================================================= if (opponentCombatStats.hpCurrent > 0 && opponentAccum >= opponentCombatStats.attackDelayMs) { opponentAccum = 0; - // 상대 스킬 선택 - final skill = _selectBestSkill( - skills: opponentSkills, - stats: opponentCombatStats, - skillSystem: opponentSkillSystem, - target: null, + // 도전자 몬스터 스탯 동기화 + challengerMonsterStats = MonsterCombatStats.fromCombatStats( + playerCombatStats, + match.challenger.characterName, ); - if (skill != null) { - // 스킬 사용 - final skillResult = _useSkill( - skill: skill, - attacker: opponentCombatStats, - defender: playerCombatStats, + // 스킬 자동 선택 (progress_service와 동일한 로직) + final selectedSkill = _skillService.selectAutoSkill( + player: opponentCombatStats, + monster: challengerMonsterStats, + skillSystem: opponentSkillSystem, + availableSkillIds: opponentSkillIds, + activeDoTs: opponentDoTs, + activeDebuffs: challengerDebuffs, + ); + + if (selectedSkill != null && selectedSkill.isAttack) { + // 스킬 랭크 조회 및 적용 + final skillRank = _getSkillRankFromEntry( + match.opponent, + selectedSkill.id, + ); + final skillResult = _skillService.useAttackSkillWithRank( + skill: selectedSkill, + player: opponentCombatStats, + monster: challengerMonsterStats, + skillSystem: opponentSkillSystem, + rank: skillRank, + ); + opponentCombatStats = skillResult.updatedPlayer; + playerCombatStats = playerCombatStats.copyWith( + hpCurrent: skillResult.updatedMonster.hpCurrent, + ); + opponentSkillSystem = skillResult.updatedSkillSystem; + opponentSkillUsed = selectedSkill.name; + opponentDamage = skillResult.result.damage; + } else if (selectedSkill != null && selectedSkill.isDot) { + // DOT 스킬 사용 + final skillResult = _skillService.useDotSkill( + skill: selectedSkill, + player: opponentCombatStats, + skillSystem: opponentSkillSystem, + playerInt: opponentCombatStats.atk ~/ 10, + playerWis: opponentCombatStats.def ~/ 10, + ); + opponentCombatStats = skillResult.updatedPlayer; + opponentSkillSystem = skillResult.updatedSkillSystem; + if (skillResult.dotEffect != null) { + opponentDoTs.add(skillResult.dotEffect!); + } + opponentSkillUsed = selectedSkill.name; + } else if (selectedSkill != null && selectedSkill.isHeal) { + // 회복 스킬 사용 + final skillResult = _skillService.useHealSkill( + skill: selectedSkill, + player: opponentCombatStats, skillSystem: opponentSkillSystem, ); - opponentCombatStats = skillResult.updatedAttacker; - playerCombatStats = skillResult.updatedDefender; + opponentCombatStats = skillResult.updatedPlayer; opponentSkillSystem = skillResult.updatedSkillSystem; - opponentSkillUsed = skill.name; - opponentDamage = skillResult.damage; - opponentHealAmount = skillResult.healAmount; - isOpponentCritical = skillResult.isCritical; + opponentSkillUsed = selectedSkill.name; + opponentHealAmount = skillResult.result.healedAmount; + } else if (selectedSkill != null && selectedSkill.isBuff) { + // 버프 스킬 사용 + final skillResult = _skillService.useBuffSkill( + skill: selectedSkill, + player: opponentCombatStats, + skillSystem: opponentSkillSystem, + ); + opponentCombatStats = skillResult.updatedPlayer; + opponentSkillSystem = skillResult.updatedSkillSystem; + opponentSkillUsed = selectedSkill.name; + } else if (selectedSkill != null && selectedSkill.isDebuff) { + // 디버프 스킬 사용 + final skillResult = _skillService.useDebuffSkill( + skill: selectedSkill, + player: opponentCombatStats, + skillSystem: opponentSkillSystem, + currentDebuffs: challengerDebuffs, + ); + opponentCombatStats = skillResult.updatedPlayer; + opponentSkillSystem = skillResult.updatedSkillSystem; + final debuffEffect = skillResult.debuffEffect; + if (debuffEffect != null) { + challengerDebuffs = challengerDebuffs + .where((ActiveBuff d) => d.effect.id != debuffEffect.effect.id) + .toList() + ..add(debuffEffect); + } + opponentSkillUsed = selectedSkill.name; } else { - // 기본 공격 (몬스터 형태로) + // 일반 공격 (디버프 효과 적용) + var debuffedOpponent = opponentCombatStats; + if (challengerDebuffs.isNotEmpty) { + double atkMod = 0; + for (final debuff in challengerDebuffs) { + if (!debuff.isExpired(elapsedMs)) { + atkMod += debuff.effect.atkModifier; + } + } + final newAtk = (opponentCombatStats.atk * (1 + atkMod)) + .round() + .clamp(opponentCombatStats.atk ~/ 10, opponentCombatStats.atk); + debuffedOpponent = opponentCombatStats.copyWith(atk: newAtk); + } + opponentMonsterStats = MonsterCombatStats.fromCombatStats( - opponentCombatStats, + debuffedOpponent, match.opponent.characterName, ); final result = calculator.monsterAttackPlayer( @@ -444,84 +667,6 @@ class ArenaService { if (turns > 1000) break; } } - - /// 스킬 사용 (내부 헬퍼) - ({ - CombatStats updatedAttacker, - CombatStats updatedDefender, - SkillSystemState updatedSkillSystem, - int? damage, - int? healAmount, - bool isCritical, - }) _useSkill({ - required Skill skill, - required CombatStats attacker, - required CombatStats defender, - required SkillSystemState skillSystem, - }) { - int? damage; - int? healAmount; - var updatedAttacker = attacker; - var updatedDefender = defender; - var updatedSkillSystem = skillSystem; - - switch (skill.type) { - case SkillType.attack: - final monsterStats = MonsterCombatStats.fromCombatStats(defender, ''); - final result = _skillService.useAttackSkill( - skill: skill, - player: attacker, - monster: monsterStats, - skillSystem: skillSystem, - ); - updatedAttacker = result.updatedPlayer; - updatedDefender = defender.copyWith( - hpCurrent: result.updatedMonster.hpCurrent, - ); - updatedSkillSystem = result.updatedSkillSystem; - damage = result.result.damage; - - case SkillType.heal: - final result = _skillService.useHealSkill( - skill: skill, - player: attacker, - skillSystem: skillSystem, - ); - updatedAttacker = result.updatedPlayer; - updatedSkillSystem = result.updatedSkillSystem; - healAmount = result.result.healedAmount; - - case SkillType.buff: - final result = _skillService.useBuffSkill( - skill: skill, - player: attacker, - skillSystem: skillSystem, - ); - updatedAttacker = result.updatedPlayer; - updatedSkillSystem = result.updatedSkillSystem; - - case SkillType.debuff: - // 디버프 스킬 사용 - final debuffResult = _skillService.useDebuffSkill( - skill: skill, - player: attacker, - skillSystem: skillSystem, - currentDebuffs: [], - ); - updatedAttacker = debuffResult.updatedPlayer; - updatedSkillSystem = debuffResult.updatedSkillSystem; - } - - return ( - updatedAttacker: updatedAttacker, - updatedDefender: updatedDefender, - updatedSkillSystem: updatedSkillSystem, - damage: damage, - healAmount: healAmount, - isCritical: false, // 스킬 크리티컬은 별도 처리 필요 - ); - } - // ============================================================================ // 장비 교환 // ============================================================================