From 95528786eb4b5739261813bf7893bb0c78d61bdf Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Mon, 12 Jan 2026 16:17:00 +0900 Subject: [PATCH] =?UTF-8?q?refactor(engine):=20=EC=84=9C=EB=B9=84=EC=8A=A4?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ArenaService, PotionService, ProgressService 개선 - ResurrectionService, SkillService 정리 --- lib/src/core/engine/arena_service.dart | 45 +++-- lib/src/core/engine/potion_service.dart | 6 +- lib/src/core/engine/progress_service.dart | 163 ++++++++++++++---- lib/src/core/engine/resurrection_service.dart | 5 +- lib/src/core/engine/skill_service.dart | 12 +- 5 files changed, 176 insertions(+), 55 deletions(-) diff --git a/lib/src/core/engine/arena_service.dart b/lib/src/core/engine/arena_service.dart index edb5515..aff18cc 100644 --- a/lib/src/core/engine/arena_service.dart +++ b/lib/src/core/engine/arena_service.dart @@ -19,7 +19,7 @@ import 'package:asciineverdie/src/core/util/deterministic_random.dart'; /// - 장비 교환 class ArenaService { ArenaService({DeterministicRandom? rng}) - : _rng = rng ?? DeterministicRandom(DateTime.now().millisecondsSinceEpoch); + : _rng = rng ?? DeterministicRandom(DateTime.now().millisecondsSinceEpoch); final DeterministicRandom _rng; @@ -309,7 +309,9 @@ class ArenaService { elapsedMs += tickMs; // 스킬 시스템 시간 업데이트 - challengerSkillSystem = challengerSkillSystem.copyWith(elapsedMs: elapsedMs); + challengerSkillSystem = challengerSkillSystem.copyWith( + elapsedMs: elapsedMs, + ); opponentSkillSystem = opponentSkillSystem.copyWith(elapsedMs: elapsedMs); int? challengerDamage; @@ -469,10 +471,13 @@ class ArenaService { challengerSkillSystem = skillResult.updatedSkillSystem; final debuffEffect = skillResult.debuffEffect; if (debuffEffect != null) { - opponentDebuffs = opponentDebuffs - .where((ActiveBuff d) => d.effect.id != debuffEffect.effect.id) - .toList() - ..add(debuffEffect); + opponentDebuffs = + opponentDebuffs + .where( + (ActiveBuff d) => d.effect.id != debuffEffect.effect.id, + ) + .toList() + ..add(debuffEffect); } challengerSkillUsed = selectedSkill.name; } else { @@ -585,10 +590,13 @@ class ArenaService { opponentSkillSystem = skillResult.updatedSkillSystem; final debuffEffect = skillResult.debuffEffect; if (debuffEffect != null) { - challengerDebuffs = challengerDebuffs - .where((ActiveBuff d) => d.effect.id != debuffEffect.effect.id) - .toList() - ..add(debuffEffect); + challengerDebuffs = + challengerDebuffs + .where( + (ActiveBuff d) => d.effect.id != debuffEffect.effect.id, + ) + .toList() + ..add(debuffEffect); } opponentSkillUsed = selectedSkill.name; } else { @@ -628,7 +636,8 @@ class ArenaService { } // 액션이 발생했을 때만 턴 전송 - final hasAction = challengerDamage != null || + final hasAction = + challengerDamage != null || opponentDamage != null || challengerHealAmount != null || opponentHealAmount != null || @@ -722,12 +731,14 @@ class ArenaService { required bool isVictory, }) { // 도전자 장비 목록 복사 - final challengerEquipment = - List.from(match.challenger.finalEquipment ?? []); + final challengerEquipment = List.from( + match.challenger.finalEquipment ?? [], + ); // 상대 장비 목록 복사 - final opponentEquipment = - List.from(match.opponent.finalEquipment ?? []); + final opponentEquipment = List.from( + match.opponent.finalEquipment ?? [], + ); if (isVictory) { // 도전자 승리: 도전자가 선택한 슬롯의 상대 장비 획득 @@ -766,7 +777,9 @@ class ArenaService { /// 슬롯으로 장비 찾기 EquipmentItem _findItemBySlot( - List equipment, EquipmentSlot slot) { + List equipment, + EquipmentSlot slot, + ) { for (final item in equipment) { if (item.slot == slot) return item; } diff --git a/lib/src/core/engine/potion_service.dart b/lib/src/core/engine/potion_service.dart index 8669d7b..895c6ee 100644 --- a/lib/src/core/engine/potion_service.dart +++ b/lib/src/core/engine/potion_service.dart @@ -375,8 +375,10 @@ class PotionService { required int typeRoll, }) { // 기본 드랍 확률 계산 - var dropChance = (baseDropChance + playerLevel * dropChancePerLevel) - .clamp(baseDropChance, maxDropChance); + var dropChance = (baseDropChance + playerLevel * dropChancePerLevel).clamp( + baseDropChance, + maxDropChance, + ); // 몬스터 등급 보너스 (Elite +5%, Boss +15%) dropChance += monsterGrade.potionDropBonus; diff --git a/lib/src/core/engine/progress_service.dart b/lib/src/core/engine/progress_service.dart index d8aa34d..8d123c4 100644 --- a/lib/src/core/engine/progress_service.dart +++ b/lib/src/core/engine/progress_service.dart @@ -104,8 +104,10 @@ class ProgressService { ); // ExpBar 초기화 (원본 743-746줄) - final expBar = - ProgressBarState(position: 0, max: ExpConstants.requiredExp(1)); + final expBar = ProgressBarState( + position: 0, + max: ExpConstants.requiredExp(1), + ); // PlotBar 초기화 - Prologue 5분 (300초) final plotBar = const ProgressBarState(position: 0, max: 300); @@ -299,23 +301,40 @@ class ProgressService { progress = progress.copyWith(currentCombat: combatForReset); } - // 전투 상태 초기화, 몬스터 처치 수 증가 및 물약 사용 기록 초기화 - progress = progress.copyWith( - currentCombat: null, - monstersKilled: progress.monstersKilled + 1, - ); + // Boss 승리 처리: 시네마틱 트리거 + if (progress.pendingActCompletion) { + // Act Boss를 처치했으므로 시네마틱 재생 + final cinematicEntries = pq_logic.interplotCinematic( + config, + nextState.rng, + nextState.traits.level, + progress.plotStageCount, + ); + queue = QueueState(entries: [...queue.entries, ...cinematicEntries]); + progress = progress.copyWith( + currentCombat: null, + monstersKilled: progress.monstersKilled + 1, + pendingActCompletion: false, // Boss 처치 완료 + ); + } else { + // 일반 전투 종료 + progress = progress.copyWith( + currentCombat: null, + monstersKilled: progress.monstersKilled + 1, + ); + } + final resetPotionInventory = nextState.potionInventory.resetBattleUsage(); nextState = nextState.copyWith( progress: progress, + queue: queue, potionInventory: resetPotionInventory, ); // 최종 보스 처치 체크 if (progress.finalBossState == FinalBossState.fighting) { // 글리치 갓 처치 완료 - 게임 클리어 - progress = progress.copyWith( - finalBossState: FinalBossState.defeated, - ); + progress = progress.copyWith(finalBossState: FinalBossState.defeated); nextState = nextState.copyWith(progress: progress); // completeAct를 호출하여 게임 완료 처리 @@ -406,22 +425,22 @@ class ProgressService { } } - // 플롯(plot) 바가 완료되면 InterplotCinematic 트리거 - // (원본 Main.pas:1301-1304) + // 플롯(plot) 바가 완료되면 Act Boss 소환 + // (개선: Boss 처치 → 시네마틱 → Act 전환 순서) if (gain && progress.plot.max > 0 && - progress.plot.position >= progress.plot.max) { - // InterplotCinematic을 호출하여 시네마틱 이벤트 큐에 추가 - final cinematicEntries = pq_logic.interplotCinematic( - config, - nextState.rng, - nextState.traits.level, - nextState.progress.plotStageCount, + progress.plot.position >= progress.plot.max && + !progress.pendingActCompletion) { + // Act Boss 소환 및 플래그 설정 + final actBoss = _createActBoss(nextState); + progress = progress.copyWith( + plot: progress.plot.copyWith(position: 0), // Plot bar 리셋 + currentCombat: actBoss, + pendingActCompletion: true, // Boss 처치 대기 플래그 ); - queue = QueueState(entries: [...queue.entries, ...cinematicEntries]); - // 플롯 바를 0으로 리셋하지 않음 - completeAct에서 처리됨 } else if (progress.currentTask.type != TaskType.load && - progress.plot.max > 0) { + progress.plot.max > 0 && + !progress.pendingActCompletion) { final uncappedPlot = progress.plot.position + incrementSeconds; final int newPlotPos = uncappedPlot > progress.plot.max ? progress.plot.max @@ -531,13 +550,44 @@ class ProgressService { return (progress: progress, queue: queue); } - // 3. 최종 보스 전투 체크 + // 3. Act Boss 리트라이 체크 + // pendingActCompletion이 true면 Act Boss 재소환 + if (state.progress.pendingActCompletion) { + final actBoss = _createActBoss(state); + final combatCalculator = CombatCalculator(rng: state.rng); + final durationMillis = combatCalculator.estimateCombatDurationMs( + player: actBoss.playerStats, + monster: actBoss.monsterStats, + ); + + final taskResult = pq_logic.startTask( + progress, + l10n.taskDebugging(actBoss.monsterStats.name), + durationMillis, + ); + + progress = taskResult.progress.copyWith( + currentTask: TaskInfo( + caption: taskResult.caption, + type: TaskType.kill, + monsterBaseName: actBoss.monsterStats.name, + monsterPart: '*', // Boss는 WinItem 드랍 + monsterLevel: actBoss.monsterStats.level, + monsterGrade: MonsterGrade.boss, + ), + currentCombat: actBoss, + ); + + return (progress: progress, queue: queue); + } + + // 4. 최종 보스 전투 체크 // finalBossState == fighting이면 Glitch God 스폰 if (state.progress.finalBossState == FinalBossState.fighting) { return _startFinalBossFight(state, progress, queue); } - // 4. MonsterTask 실행 (원본 678-684줄) + // 5. MonsterTask 실행 (원본 678-684줄) final level = state.traits.level; // 원본 Main.pas:548-551: 25% 확률로 Quest Monster 사용 @@ -878,7 +928,8 @@ class ProgressService { var nextState = state; // 현재 레벨이 목표 레벨보다 낮으면 레벨업 (최대 100레벨) - while (nextState.traits.level < targetLevel && nextState.traits.level < 100) { + while (nextState.traits.level < targetLevel && + nextState.traits.level < 100) { nextState = _levelUp(nextState); } @@ -1390,10 +1441,13 @@ class ProgressService { // 디버프 효과 추가 (기존 같은 디버프 제거 후) if (skillResult.debuffEffect != null) { - activeDebuffs = activeDebuffs - .where((d) => d.effect.id != skillResult.debuffEffect!.effect.id) - .toList() - ..add(skillResult.debuffEffect!); + activeDebuffs = + activeDebuffs + .where( + (d) => d.effect.id != skillResult.debuffEffect!.effect.id, + ) + .toList() + ..add(skillResult.debuffEffect!); } // 디버프 이벤트 생성 @@ -1533,6 +1587,55 @@ class ProgressService { ); } + /// Act Boss 생성 (Act 완료 시) + /// + /// 보스 레벨 = min(플레이어 레벨, Act 최소 레벨)로 설정하여 + /// 플레이어가 이길 수 있는 수준 보장 + CombatState _createActBoss(GameState state) { + final plotStage = state.progress.plotStageCount; + final actNumber = plotStage + 1; + + // 보스 레벨 = min(플레이어 레벨, Act 최소 레벨) + // → 플레이어가 현재 레벨보다 높은 보스를 만나지 않도록 보장 + final actMinLevel = ActMonsterLevel.forPlotStage(actNumber); + final bossLevel = math.min(state.traits.level, actMinLevel); + + // Named monster 생성 (pq_logic.namedMonster 활용) + final bossName = pq_logic.namedMonster(config, state.rng, bossLevel); + + final bossStats = MonsterBaseStats.forLevel(bossLevel); + + // 플레이어 전투 스탯 생성 + final playerCombatStats = CombatStats.fromStats( + stats: state.stats, + equipment: state.equipment, + level: state.traits.level, + monsterLevel: bossLevel, + ); + + // Boss 몬스터 스탯 생성 (일반 몬스터 대비 강화) + final monsterCombatStats = MonsterCombatStats( + name: bossName, + level: bossLevel, + atk: (bossStats.atk * 1.5).round(), // Boss 보정 (1.5배) + def: (bossStats.def * 1.5).round(), + hpMax: (bossStats.hp * 2.0).round(), // HP는 2.0배 (보스다운 전투 시간) + hpCurrent: (bossStats.hp * 2.0).round(), + criRate: 0.05, + criDamage: 1.5, + evasion: 0.0, + accuracy: 0.8, + attackDelayMs: 1000, + expReward: (bossStats.exp * 2.5).round(), // 경험치 보상 증가 + ); + + // 전투 상태 초기화 + return CombatState.start( + playerStats: playerCombatStats, + monsterStats: monsterCombatStats, + ); + } + /// 플레이어 사망 처리 (Phase 4) /// /// 모든 장비 상실 및 사망 정보 기록 @@ -1578,9 +1681,11 @@ class ProgressService { ); // 전투 상태 초기화 및 사망 횟수 증가 + // pendingActCompletion 플래그는 유지 (Boss 리트라이를 위해) final progress = state.progress.copyWith( currentCombat: null, deathCount: state.progress.deathCount + 1, + // pendingActCompletion은 copyWith에서 명시하지 않으면 기존 값 유지 ); return state.copyWith( diff --git a/lib/src/core/engine/resurrection_service.dart b/lib/src/core/engine/resurrection_service.dart index 371f7ce..38ce195 100644 --- a/lib/src/core/engine/resurrection_service.dart +++ b/lib/src/core/engine/resurrection_service.dart @@ -167,10 +167,7 @@ class ResurrectionService { caption: firstTask.caption, type: firstTask.taskType, ), - task: ProgressBarState( - position: 0, - max: firstTask.durationMillis, - ), + task: ProgressBarState(position: 0, max: firstTask.durationMillis), currentCombat: null, // 전투 상태 명시적 초기화 ), ); diff --git a/lib/src/core/engine/skill_service.dart b/lib/src/core/engine/skill_service.dart index 2aab1c5..f2c00da 100644 --- a/lib/src/core/engine/skill_service.dart +++ b/lib/src/core/engine/skill_service.dart @@ -446,10 +446,12 @@ class SkillService { // ATK 증가량 기준 정렬 buffSkills.sort((a, b) { - final aValue = (a.buff?.atkModifier ?? 0) + + final aValue = + (a.buff?.atkModifier ?? 0) + (a.buff?.defModifier ?? 0) * 0.5 + (a.buff?.criRateModifier ?? 0) * 0.3; - final bValue = (b.buff?.atkModifier ?? 0) + + final bValue = + (b.buff?.atkModifier ?? 0) + (b.buff?.defModifier ?? 0) * 0.5 + (b.buff?.criRateModifier ?? 0) * 0.3; return bValue.compareTo(aValue); @@ -470,9 +472,11 @@ class SkillService { // 디버프 효과 크기 기준 정렬 (음수 값이므로 절대값으로 비교) debuffSkills.sort((a, b) { - final aValue = (a.buff?.atkModifier ?? 0).abs() + + final aValue = + (a.buff?.atkModifier ?? 0).abs() + (a.buff?.defModifier ?? 0).abs() * 0.5; - final bValue = (b.buff?.atkModifier ?? 0).abs() + + final bValue = + (b.buff?.atkModifier ?? 0).abs() + (b.buff?.defModifier ?? 0).abs() * 0.5; return bValue.compareTo(aValue); });