From 2efd50a09d7c3ea695b4dcec08bbec0167babf0f Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Tue, 6 Jan 2026 18:29:01 +0900 Subject: [PATCH] =?UTF-8?q?feat(arena):=20=EC=95=84=EB=A0=88=EB=82=98=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=B0=8F=20=EB=AA=A8=EB=8D=B8=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ArenaService 로직 확장 - ArenaMatch 모델 필드 추가 --- lib/src/core/engine/arena_service.dart | 335 ++++++++++++++++++++++--- lib/src/core/model/arena_match.dart | 32 +++ 2 files changed, 337 insertions(+), 30 deletions(-) diff --git a/lib/src/core/engine/arena_service.dart b/lib/src/core/engine/arena_service.dart index 43cd657..dd19a01 100644 --- a/lib/src/core/engine/arena_service.dart +++ b/lib/src/core/engine/arena_service.dart @@ -1,9 +1,14 @@ +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'; import 'package:asciineverdie/src/core/model/hall_of_fame.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'; /// 아레나 서비스 @@ -18,6 +23,90 @@ class ArenaService { final DeterministicRandom _rng; + late final SkillService _skillService = SkillService(rng: _rng); + + // ============================================================================ + // 스킬 시스템 헬퍼 + // ============================================================================ + + /// HallOfFameEntry의 finalSpells에서 Skill 목록 추출 + List _getSkillsFromEntry(HallOfFameEntry entry) { + final spells = entry.finalSpells; + if (spells == null || spells.isEmpty) return []; + + final skills = []; + for (final spell in spells) { + final spellName = spell['name']; + if (spellName != null) { + final skill = SkillData.getSkillBySpellName(spellName); + if (skill != null) { + skills.add(skill); + } + } + } + return skills; + } + + /// AI 스킬 선택 (우선순위: 회복 > 버프 > 공격) + Skill? _selectBestSkill({ + required List skills, + required CombatStats stats, + required SkillSystemState skillSystem, + required MonsterCombatStats? target, + }) { + if (skills.isEmpty) return null; + + final currentMp = stats.mpCurrent; + final hpRatio = stats.hpCurrent / stats.hpMax; + + // 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; + } + } + } + + // 버프가 없으면 버프 스킬 + 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; // 스킬 사용 안 함 (기본 공격) + } + // ============================================================================ // 상대 결정 // ============================================================================ @@ -148,11 +237,25 @@ class ArenaService { return; } + // 스킬 목록 로드 + final challengerSkills = _getSkillsFromEntry(match.challenger); + final opponentSkills = _getSkillsFromEntry(match.opponent); + + // 스킬 시스템 상태 초기화 + var challengerSkillSystem = SkillSystemState.empty(); + var opponentSkillSystem = SkillSystemState.empty(); + 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, match.opponent.characterName, @@ -160,20 +263,30 @@ class ArenaService { int playerAccum = 0; int opponentAccum = 0; + int elapsedMs = 0; const tickMs = 200; int turns = 0; // 초기 상태 전송 yield ArenaCombatTurn( challengerHp: playerCombatStats.hpCurrent, - opponentHp: opponentMonsterStats.hpCurrent, + opponentHp: opponentCombatStats.hpCurrent, challengerHpMax: playerCombatStats.hpMax, - opponentHpMax: opponentMonsterStats.hpMax, + opponentHpMax: opponentCombatStats.hpMax, + challengerMp: playerCombatStats.mpCurrent, + opponentMp: opponentCombatStats.mpCurrent, + challengerMpMax: playerCombatStats.mpMax, + opponentMpMax: opponentCombatStats.mpMax, ); - while (playerCombatStats.isAlive && opponentMonsterStats.isAlive) { + while (playerCombatStats.isAlive && opponentCombatStats.hpCurrent > 0) { playerAccum += tickMs; opponentAccum += tickMs; + elapsedMs += tickMs; + + // 스킬 시스템 시간 업데이트 + challengerSkillSystem = challengerSkillSystem.copyWith(elapsedMs: elapsedMs); + opponentSkillSystem = opponentSkillSystem.copyWith(elapsedMs: elapsedMs); int? challengerDamage; int? opponentDamage; @@ -183,59 +296,144 @@ class ArenaService { bool isOpponentEvaded = false; bool isChallengerBlocked = false; bool isOpponentBlocked = false; + String? challengerSkillUsed; + String? opponentSkillUsed; + int? challengerHealAmount; + int? opponentHealAmount; - // 플레이어 공격 + // 도전자 턴 if (playerAccum >= playerCombatStats.attackDelayMs) { - final result = calculator.playerAttackMonster( - attacker: playerCombatStats, - defender: opponentMonsterStats, - ); - opponentMonsterStats = result.updatedDefender; playerAccum = 0; - if (result.result.isHit) { - challengerDamage = result.result.damage; - isChallengerCritical = result.result.isCritical; + // 스킬 선택 + final skill = _selectBestSkill( + skills: challengerSkills, + stats: playerCombatStats, + skillSystem: challengerSkillSystem, + target: opponentMonsterStats, + ); + + if (skill != null) { + // 스킬 사용 + final skillResult = _useSkill( + skill: skill, + attacker: playerCombatStats, + defender: opponentCombatStats, + skillSystem: challengerSkillSystem, + ); + playerCombatStats = skillResult.updatedAttacker; + opponentCombatStats = skillResult.updatedDefender; + challengerSkillSystem = skillResult.updatedSkillSystem; + challengerSkillUsed = skill.name; + challengerDamage = skillResult.damage; + challengerHealAmount = skillResult.healAmount; + isChallengerCritical = skillResult.isCritical; } else { - isOpponentEvaded = true; + // 기본 공격 + // 상대 몬스터 스탯 동기화 + opponentMonsterStats = MonsterCombatStats.fromCombatStats( + opponentCombatStats, + match.opponent.characterName, + ); + final result = calculator.playerAttackMonster( + attacker: playerCombatStats, + defender: opponentMonsterStats, + ); + opponentMonsterStats = result.updatedDefender; + opponentCombatStats = opponentCombatStats.copyWith( + hpCurrent: opponentMonsterStats.hpCurrent, + ); + + if (result.result.isHit) { + challengerDamage = result.result.damage; + isChallengerCritical = result.result.isCritical; + } else { + isOpponentEvaded = true; + } } } - // 상대 공격 - if (opponentMonsterStats.isAlive && - opponentAccum >= opponentMonsterStats.attackDelayMs) { - final result = calculator.monsterAttackPlayer( - attacker: opponentMonsterStats, - defender: playerCombatStats, - ); - playerCombatStats = result.updatedDefender; + // 상대 턴 + if (opponentCombatStats.hpCurrent > 0 && + opponentAccum >= opponentCombatStats.attackDelayMs) { opponentAccum = 0; - if (result.result.isHit) { - opponentDamage = result.result.damage; - isOpponentCritical = result.result.isCritical; - isChallengerBlocked = result.result.isBlocked; + // 상대 스킬 선택 + final skill = _selectBestSkill( + skills: opponentSkills, + stats: opponentCombatStats, + skillSystem: opponentSkillSystem, + target: null, + ); + + if (skill != null) { + // 스킬 사용 + final skillResult = _useSkill( + skill: skill, + attacker: opponentCombatStats, + defender: playerCombatStats, + skillSystem: opponentSkillSystem, + ); + opponentCombatStats = skillResult.updatedAttacker; + playerCombatStats = skillResult.updatedDefender; + opponentSkillSystem = skillResult.updatedSkillSystem; + opponentSkillUsed = skill.name; + opponentDamage = skillResult.damage; + opponentHealAmount = skillResult.healAmount; + isOpponentCritical = skillResult.isCritical; } else { - isChallengerEvaded = true; + // 기본 공격 (몬스터 형태로) + opponentMonsterStats = MonsterCombatStats.fromCombatStats( + opponentCombatStats, + match.opponent.characterName, + ); + final result = calculator.monsterAttackPlayer( + attacker: opponentMonsterStats, + defender: playerCombatStats, + ); + playerCombatStats = result.updatedDefender; + + if (result.result.isHit) { + opponentDamage = result.result.damage; + isOpponentCritical = result.result.isCritical; + isChallengerBlocked = result.result.isBlocked; + } else { + isChallengerEvaded = true; + } } } - // 공격이 발생했을 때만 턴 전송 - if (challengerDamage != null || opponentDamage != null) { + // 액션이 발생했을 때만 턴 전송 + final hasAction = challengerDamage != null || + opponentDamage != null || + challengerHealAmount != null || + opponentHealAmount != null || + challengerSkillUsed != null || + opponentSkillUsed != null; + + if (hasAction) { turns++; yield ArenaCombatTurn( challengerDamage: challengerDamage, opponentDamage: opponentDamage, challengerHp: playerCombatStats.hpCurrent, - opponentHp: opponentMonsterStats.hpCurrent, + opponentHp: opponentCombatStats.hpCurrent, challengerHpMax: playerCombatStats.hpMax, - opponentHpMax: opponentMonsterStats.hpMax, + opponentHpMax: opponentCombatStats.hpMax, + challengerMp: playerCombatStats.mpCurrent, + opponentMp: opponentCombatStats.mpCurrent, + challengerMpMax: playerCombatStats.mpMax, + opponentMpMax: opponentCombatStats.mpMax, isChallengerCritical: isChallengerCritical, isOpponentCritical: isOpponentCritical, isChallengerEvaded: isChallengerEvaded, isOpponentEvaded: isOpponentEvaded, isChallengerBlocked: isChallengerBlocked, isOpponentBlocked: isOpponentBlocked, + challengerSkillUsed: challengerSkillUsed, + opponentSkillUsed: opponentSkillUsed, + challengerHealAmount: challengerHealAmount, + opponentHealAmount: opponentHealAmount, ); // 애니메이션을 위한 딜레이 @@ -247,6 +445,83 @@ class ArenaService { } } + /// 스킬 사용 (내부 헬퍼) + ({ + 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, // 스킬 크리티컬은 별도 처리 필요 + ); + } + // ============================================================================ // 장비 교환 // ============================================================================ diff --git a/lib/src/core/model/arena_match.dart b/lib/src/core/model/arena_match.dart index 98e401f..d8e5c0b 100644 --- a/lib/src/core/model/arena_match.dart +++ b/lib/src/core/model/arena_match.dart @@ -62,12 +62,20 @@ class ArenaCombatTurn { required this.opponentHp, required this.challengerHpMax, required this.opponentHpMax, + this.challengerMp, + this.opponentMp, + this.challengerMpMax, + this.opponentMpMax, this.isChallengerCritical = false, this.isOpponentCritical = false, this.isChallengerEvaded = false, this.isOpponentEvaded = false, this.isChallengerBlocked = false, this.isOpponentBlocked = false, + this.challengerSkillUsed, + this.opponentSkillUsed, + this.challengerHealAmount, + this.opponentHealAmount, }) : timestamp = DateTime.now().microsecondsSinceEpoch; /// 턴 식별용 타임스탬프 @@ -91,6 +99,18 @@ class ArenaCombatTurn { /// 상대 최대 HP final int opponentHpMax; + /// 도전자 현재 MP + final int? challengerMp; + + /// 상대 현재 MP + final int? opponentMp; + + /// 도전자 최대 MP + final int? challengerMpMax; + + /// 상대 최대 MP + final int? opponentMpMax; + /// 도전자 크리티컬 여부 final bool isChallengerCritical; @@ -108,4 +128,16 @@ class ArenaCombatTurn { /// 상대 블록 여부 final bool isOpponentBlocked; + + /// 도전자 사용 스킬명 (null이면 기본 공격) + final String? challengerSkillUsed; + + /// 상대 사용 스킬명 (null이면 기본 공격) + final String? opponentSkillUsed; + + /// 도전자 회복량 + final int? challengerHealAmount; + + /// 상대 회복량 + final int? opponentHealAmount; }