diff --git a/lib/src/core/engine/progress_service.dart b/lib/src/core/engine/progress_service.dart index fc43010..e8809a2 100644 --- a/lib/src/core/engine/progress_service.dart +++ b/lib/src/core/engine/progress_service.dart @@ -161,36 +161,107 @@ class ProgressService { /// Tick the timer loop (equivalent to Timer1Timer in the original code). ProgressTickResult tick(GameState state, int elapsedMillis) { - // 10000ms 제한: 100x 배속 (50ms * 100 = 5000ms) + 여유 공간 - // 디버그 터보 모드(100x) 지원을 위해 확장 final int clamped = elapsedMillis.clamp(0, 10000).toInt(); - var progress = state.progress; - var queue = state.queue; - var nextState = state; + + // 1. 스킬 시스템 업데이트 (시간, 버프, MP 회복) + var nextState = _updateSkillSystem(state, clamped); + var progress = nextState.progress; + var queue = nextState.queue; + + // 2. 태스크 바 진행 중이면 전투 틱 처리 + if (progress.task.position < progress.task.max) { + return _processTaskInProgress(nextState, clamped); + } + + // 3. 태스크 완료 처리 + final gain = progress.currentTask.type == TaskType.kill; + final incrementSeconds = progress.task.max ~/ 1000; + final int monsterExpReward = + progress.currentCombat?.monsterStats.expReward ?? 0; var leveledUp = false; var questDone = false; var actDone = false; var gameComplete = false; - // 스킬 시스템 시간 업데이트 (Phase 3) + // 4. 킬 태스크 완료 처리 + if (gain) { + final killResult = _handleKillTaskCompletion(nextState, progress, queue); + if (killResult.earlyReturn != null) return killResult.earlyReturn!; + nextState = killResult.state; + progress = killResult.progress; + queue = killResult.queue; + } + + // 5. 시장/판매/구매 태스크 완료 처리 + final marketResult = _handleMarketTaskCompletion(nextState, progress, queue); + if (marketResult.earlyReturn != null) return marketResult.earlyReturn!; + nextState = marketResult.state; + progress = marketResult.progress; + queue = marketResult.queue; + + // 6. 경험치/레벨업 처리 + if (gain && nextState.traits.level < 100 && monsterExpReward > 0) { + final expResult = _handleExpGain(nextState, progress, monsterExpReward); + nextState = expResult.state; + progress = expResult.progress; + leveledUp = expResult.leveledUp; + } + + // 7. 퀘스트 진행 처리 + final questResult = _handleQuestProgress( + nextState, progress, queue, gain, incrementSeconds, + ); + nextState = questResult.state; + progress = questResult.progress; + queue = questResult.queue; + questDone = questResult.completed; + + // 8. 플롯 진행 및 Act Boss 소환 처리 + progress = _handlePlotProgress( + nextState, progress, gain, incrementSeconds, + ); + + // 9. 다음 태스크 디큐/생성 + final dequeueResult = _handleTaskDequeue(nextState, progress, queue); + nextState = dequeueResult.state; + progress = dequeueResult.progress; + queue = dequeueResult.queue; + actDone = dequeueResult.actDone; + gameComplete = dequeueResult.gameComplete; + + nextState = _recalculateEncumbrance( + nextState.copyWith(progress: progress, queue: queue), + ); + + return ProgressTickResult( + state: nextState, + leveledUp: leveledUp, + completedQuest: questDone, + completedAct: actDone, + gameComplete: gameComplete, + ); + } + + /// 스킬 시스템 업데이트 (시간, 버프 정리, MP 회복) + GameState _updateSkillSystem(GameState state, int elapsedMs) { final skillService = SkillService(rng: state.rng); var skillSystem = skillService.updateElapsedTime( state.skillSystem, - clamped, + elapsedMs, ); - - // 만료된 버프 정리 skillSystem = skillService.cleanupExpiredBuffs(skillSystem); + var nextState = state.copyWith(skillSystem: skillSystem); + // 비전투 시 MP 회복 final isInCombat = - progress.currentTask.type == TaskType.kill && - progress.currentCombat != null && - progress.currentCombat!.isActive; + state.progress.currentTask.type == TaskType.kill && + state.progress.currentCombat != null && + state.progress.currentCombat!.isActive; if (!isInCombat && nextState.stats.mp < nextState.stats.mpMax) { final mpRegen = skillService.calculateMpRegen( - elapsedMs: clamped, + elapsedMs: elapsedMs, isInCombat: false, wis: nextState.stats.wis, ); @@ -205,232 +276,271 @@ class ProgressService { } } - nextState = nextState.copyWith(skillSystem: skillSystem); + return nextState; + } - // Advance task bar if still running. - if (progress.task.position < progress.task.max) { - final uncapped = progress.task.position + clamped; - final int newTaskPos = uncapped > progress.task.max - ? progress.task.max - : uncapped; + /// 태스크 진행 중 처리 (전투 틱 포함) + ProgressTickResult _processTaskInProgress(GameState state, int elapsedMs) { + var progress = state.progress; + final uncapped = progress.task.position + elapsedMs; + final int newTaskPos = uncapped > progress.task.max + ? progress.task.max + : uncapped; - // 킬 태스크 중 전투 진행 (CombatTickService 사용) - var updatedCombat = progress.currentCombat; - var updatedSkillSystem = nextState.skillSystem; - var updatedPotionInventory = nextState.potionInventory; - if (progress.currentTask.type == TaskType.kill && - updatedCombat != null && - updatedCombat.isActive) { - final combatTickService = CombatTickService(rng: nextState.rng); - final combatResult = combatTickService.processTick( - state: nextState, - combat: updatedCombat, - skillSystem: updatedSkillSystem, - elapsedMs: clamped, - ); - updatedCombat = combatResult.combat; - updatedSkillSystem = combatResult.skillSystem; - if (combatResult.potionInventory != null) { - updatedPotionInventory = combatResult.potionInventory!; - } + var updatedCombat = progress.currentCombat; + var updatedSkillSystem = state.skillSystem; + var updatedPotionInventory = state.potionInventory; + var nextState = state; - // Phase 4: 플레이어 사망 체크 - if (!updatedCombat.playerStats.isAlive) { - final monsterName = updatedCombat.monsterStats.name; - nextState = _processPlayerDeath( - nextState, - killerName: monsterName, - cause: DeathCause.monster, - ); - return ProgressTickResult(state: nextState, playerDied: true); - } + // 킬 태스크 중 전투 진행 + if (progress.currentTask.type == TaskType.kill && + updatedCombat != null && + updatedCombat.isActive) { + final combatTickService = CombatTickService(rng: state.rng); + final combatResult = combatTickService.processTick( + state: state, + combat: updatedCombat, + skillSystem: updatedSkillSystem, + elapsedMs: elapsedMs, + ); + updatedCombat = combatResult.combat; + updatedSkillSystem = combatResult.skillSystem; + if (combatResult.potionInventory != null) { + updatedPotionInventory = combatResult.potionInventory!; } - progress = progress.copyWith( - task: progress.task.copyWith(position: newTaskPos), - currentCombat: updatedCombat, - ); - nextState = _recalculateEncumbrance( - nextState.copyWith( - progress: progress, - skillSystem: updatedSkillSystem, - potionInventory: updatedPotionInventory, - ), - ); - return ProgressTickResult(state: nextState); + // 플레이어 사망 체크 + if (!updatedCombat.playerStats.isAlive) { + final monsterName = updatedCombat.monsterStats.name; + nextState = _processPlayerDeath( + state, + killerName: monsterName, + cause: DeathCause.monster, + ); + return ProgressTickResult(state: nextState, playerDied: true); + } } - final gain = progress.currentTask.type == TaskType.kill; - final incrementSeconds = progress.task.max ~/ 1000; - - // 몬스터 경험치 미리 저장 (currentCombat이 null되기 전) - final int monsterExpReward = - progress.currentCombat?.monsterStats.expReward ?? 0; - - // 킬 태스크 완료 시 전투 결과 반영 및 전리품 획득 - if (gain) { - // 전투 결과에 따라 플레이어 HP 업데이트 + 전투 후 회복 - final combat = progress.currentCombat; - if (combat != null && combat.isActive) { - // 전투 중 남은 HP - final remainingHp = combat.playerStats.hpCurrent; - final maxHp = combat.playerStats.hpMax; - - // 전투 승리 시 HP 회복 (50% + CON/2 + 클래스 패시브) - // 아이들 게임 특성상 전투 사이 HP가 회복되어야 지속 플레이 가능 - final conBonus = nextState.stats.con ~/ 2; - var healAmount = (maxHp * 0.5).round() + conBonus; - - // 클래스 패시브: 전투 후 HP 회복 (예: Garbage Collector +5%) - final klass = ClassData.findById(nextState.traits.classId); - if (klass != null) { - final postCombatHealRate = - klass.getPassiveValue(ClassPassiveType.postCombatHeal); - if (postCombatHealRate > 0) { - healAmount += (maxHp * postCombatHealRate).round(); - } - } - - final newHp = (remainingHp + healAmount).clamp(0, maxHp); - - nextState = nextState.copyWith( - stats: nextState.stats.copyWith(hpCurrent: newHp), - ); - } - - // 전리품 획득 (원본 Main.pas:625-630) - final lootResult = _winLoot(nextState); - nextState = lootResult.state; - - // 물약 드랍 시 전투 로그에 이벤트 추가 - var combatForReset = progress.currentCombat; - if (lootResult.droppedPotion != null && combatForReset != null) { - final potionDropEvent = CombatEvent.potionDrop( - timestamp: nextState.skillSystem.elapsedMs, - potionName: lootResult.droppedPotion!.name, - isHp: lootResult.droppedPotion!.isHpPotion, - ); - final updatedEvents = [...combatForReset.recentEvents, potionDropEvent]; - combatForReset = combatForReset.copyWith( - recentEvents: updatedEvents.length > 10 - ? updatedEvents.sublist(updatedEvents.length - 10) - : updatedEvents, - ); - progress = progress.copyWith(currentCombat: combatForReset); - } - - // 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, - ); - } - - nextState = nextState.copyWith( + progress = progress.copyWith( + task: progress.task.copyWith(position: newTaskPos), + currentCombat: updatedCombat, + ); + nextState = _recalculateEncumbrance( + state.copyWith( progress: progress, - queue: queue, + skillSystem: updatedSkillSystem, + potionInventory: updatedPotionInventory, + ), + ); + return ProgressTickResult(state: nextState); + } + + /// 킬 태스크 완료 처리 (HP 회복, 전리품, 보스 처리) + ({ + GameState state, + ProgressState progress, + QueueState queue, + ProgressTickResult? earlyReturn, + }) _handleKillTaskCompletion( + GameState state, + ProgressState progress, + QueueState queue, + ) { + var nextState = state; + + // 전투 후 HP 회복 + final combat = progress.currentCombat; + if (combat != null && combat.isActive) { + final remainingHp = combat.playerStats.hpCurrent; + final maxHp = combat.playerStats.hpMax; + final conBonus = nextState.stats.con ~/ 2; + var healAmount = (maxHp * 0.5).round() + conBonus; + + final klass = ClassData.findById(nextState.traits.classId); + if (klass != null) { + final postCombatHealRate = + klass.getPassiveValue(ClassPassiveType.postCombatHeal); + if (postCombatHealRate > 0) { + healAmount += (maxHp * postCombatHealRate).round(); + } + } + + final newHp = (remainingHp + healAmount).clamp(0, maxHp); + nextState = nextState.copyWith( + stats: nextState.stats.copyWith(hpCurrent: newHp), ); + } - // 최종 보스 처치 체크 - if (progress.finalBossState == FinalBossState.fighting) { - // 글리치 갓 처치 완료 - 게임 클리어 - progress = progress.copyWith(finalBossState: FinalBossState.defeated); - nextState = nextState.copyWith(progress: progress); + // 전리품 획득 + final lootResult = _winLoot(nextState); + nextState = lootResult.state; - // completeAct를 호출하여 게임 완료 처리 - final actResult = completeAct(nextState); - nextState = actResult.state; + // 물약 드랍 로그 추가 + var combatForReset = progress.currentCombat; + if (lootResult.droppedPotion != null && combatForReset != null) { + final potionDropEvent = CombatEvent.potionDrop( + timestamp: nextState.skillSystem.elapsedMs, + potionName: lootResult.droppedPotion!.name, + isHp: lootResult.droppedPotion!.isHpPotion, + ); + final updatedEvents = [...combatForReset.recentEvents, potionDropEvent]; + combatForReset = combatForReset.copyWith( + recentEvents: updatedEvents.length > 10 + ? updatedEvents.sublist(updatedEvents.length - 10) + : updatedEvents, + ); + progress = progress.copyWith(currentCombat: combatForReset); + } - return ProgressTickResult( - state: nextState, - leveledUp: false, - completedQuest: false, + // Boss 승리 처리 + if (progress.pendingActCompletion) { + 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, + ); + } else { + progress = progress.copyWith( + currentCombat: null, + monstersKilled: progress.monstersKilled + 1, + ); + } + + nextState = nextState.copyWith(progress: progress, queue: queue); + + // 최종 보스 처치 체크 + if (progress.finalBossState == FinalBossState.fighting) { + progress = progress.copyWith(finalBossState: FinalBossState.defeated); + nextState = nextState.copyWith(progress: progress); + final actResult = completeAct(nextState); + return ( + state: actResult.state, + progress: actResult.state.progress, + queue: actResult.state.queue, + earlyReturn: ProgressTickResult( + state: actResult.state, completedAct: true, gameComplete: true, - ); - } + ), + ); } - // 시장/판매/구매 태스크 완료 시 처리 (MarketService 사용) - final marketService = MarketService(rng: nextState.rng); + return ( + state: nextState, + progress: progress, + queue: queue, + earlyReturn: null, + ); + } + + /// 시장/판매/구매 태스크 완료 처리 + ({ + GameState state, + ProgressState progress, + QueueState queue, + ProgressTickResult? earlyReturn, + }) _handleMarketTaskCompletion( + GameState state, + ProgressState progress, + QueueState queue, + ) { + var nextState = state; + final marketService = MarketService(rng: state.rng); final taskType = progress.currentTask.type; + if (taskType == TaskType.buying) { - // 장비 구매 완료 (원본 631-634) nextState = marketService.completeBuying(nextState); progress = nextState.progress; } else if (taskType == TaskType.market || taskType == TaskType.sell) { - // 시장 도착 또는 판매 완료 (원본 635-649) final sellResult = marketService.processSell(nextState); nextState = sellResult.state; progress = nextState.progress; queue = nextState.queue; - // 판매 중이면 다른 로직 건너뛰기 if (sellResult.continuesSelling) { nextState = _recalculateEncumbrance( nextState.copyWith(progress: progress, queue: queue), ); - return ProgressTickResult( + return ( state: nextState, - leveledUp: false, - completedQuest: false, - completedAct: false, + progress: progress, + queue: queue, + earlyReturn: ProgressTickResult(state: nextState), ); } } - // Gain XP / level up (몬스터 경험치 기반) - // 최대 레벨(100) 제한: 100레벨에서는 더 이상 레벨업하지 않음 - if (gain && nextState.traits.level < 100 && monsterExpReward > 0) { - // 종족 경험치 배율 적용 (예: Byte Human +5%, Callback Seraph +3%) - final race = RaceData.findById(nextState.traits.raceId); - final expMultiplier = race?.expMultiplier ?? 1.0; - final adjustedExp = (monsterExpReward * expMultiplier).round(); - final newExpPos = progress.exp.position + adjustedExp; + return ( + state: nextState, + progress: progress, + queue: queue, + earlyReturn: null, + ); + } - // 레벨업 체크 (경험치가 필요량 이상일 때) - if (newExpPos >= progress.exp.max) { - // 초과 경험치 계산 - final overflowExp = newExpPos - progress.exp.max; - nextState = _levelUp(nextState); - leveledUp = true; - progress = nextState.progress; + /// 경험치 획득 및 레벨업 처리 + ({GameState state, ProgressState progress, bool leveledUp}) _handleExpGain( + GameState state, + ProgressState progress, + int monsterExpReward, + ) { + var nextState = state; + var leveledUp = false; - // 초과 경험치를 다음 레벨에 적용 - if (overflowExp > 0 && nextState.traits.level < 100) { - progress = progress.copyWith( - exp: progress.exp.copyWith(position: overflowExp), - ); - } - } else { + final race = RaceData.findById(nextState.traits.raceId); + final expMultiplier = race?.expMultiplier ?? 1.0; + final adjustedExp = (monsterExpReward * expMultiplier).round(); + final newExpPos = progress.exp.position + adjustedExp; + + if (newExpPos >= progress.exp.max) { + final overflowExp = newExpPos - progress.exp.max; + nextState = _levelUp(nextState); + leveledUp = true; + progress = nextState.progress; + + if (overflowExp > 0 && nextState.traits.level < 100) { progress = progress.copyWith( - exp: progress.exp.copyWith(position: newExpPos), + exp: progress.exp.copyWith(position: overflowExp), ); } + } else { + progress = progress.copyWith( + exp: progress.exp.copyWith(position: newExpPos), + ); } - // Advance quest bar after Act I. + return (state: nextState, progress: progress, leveledUp: leveledUp); + } + + /// 퀘스트 진행 처리 + ({ + GameState state, + ProgressState progress, + QueueState queue, + bool completed, + }) _handleQuestProgress( + GameState state, + ProgressState progress, + QueueState queue, + bool gain, + int incrementSeconds, + ) { + var nextState = state; + var questDone = false; + final canQuestProgress = gain && progress.plotStageCount > 1 && progress.questCount > 0 && progress.quest.max > 0; + if (canQuestProgress) { if (progress.quest.position + incrementSeconds >= progress.quest.max) { nextState = completeQuest(nextState); @@ -446,19 +556,31 @@ class ProgressService { } } - // 플롯(plot) 바가 완료되면 Act Boss 소환 - // (개선: Boss 처치 → 시네마틱 → Act 전환 순서) + return ( + state: nextState, + progress: progress, + queue: queue, + completed: questDone, + ); + } + + /// 플롯 진행 및 Act Boss 소환 처리 + ProgressState _handlePlotProgress( + GameState state, + ProgressState progress, + bool gain, + int incrementSeconds, + ) { if (gain && progress.plot.max > 0 && progress.plot.position >= progress.plot.max && !progress.pendingActCompletion) { - // Act Boss 소환 및 플래그 설정 final actProgressionService = ActProgressionService(config: config); - final actBoss = actProgressionService.createActBoss(nextState); - progress = progress.copyWith( - plot: progress.plot.copyWith(position: 0), // Plot bar 리셋 + final actBoss = actProgressionService.createActBoss(state); + return progress.copyWith( + plot: progress.plot.copyWith(position: 0), currentCombat: actBoss, - pendingActCompletion: true, // Boss 처치 대기 플래그 + pendingActCompletion: true, ); } else if (progress.currentTask.type != TaskType.load && progress.plot.max > 0 && @@ -467,12 +589,29 @@ class ProgressService { final int newPlotPos = uncappedPlot > progress.plot.max ? progress.plot.max : uncappedPlot; - progress = progress.copyWith( + return progress.copyWith( plot: progress.plot.copyWith(position: newPlotPos), ); } + return progress; + } + + /// 태스크 디큐 및 생성 처리 + ({ + GameState state, + ProgressState progress, + QueueState queue, + bool actDone, + bool gameComplete, + }) _handleTaskDequeue( + GameState state, + ProgressState progress, + QueueState queue, + ) { + var nextState = state; + var actDone = false; + var gameComplete = false; - // Dequeue next scripted task if available. final dq = pq_logic.dequeue(progress, queue); if (dq != null) { progress = dq.progress.copyWith( @@ -480,7 +619,6 @@ class ProgressService { ); queue = dq.queue; - // plot 타입이 dequeue 되면 completeAct 실행 (원본 Main.pas 로직) if (dq.kind == QueueKind.plot) { nextState = nextState.copyWith(progress: progress, queue: queue); final actResult = completeAct(nextState); @@ -491,22 +629,17 @@ class ProgressService { queue = nextState.queue; } } else { - // 큐가 비어있으면 새 태스크 생성 (원본 Dequeue 667-684줄) nextState = nextState.copyWith(progress: progress, queue: queue); final newTaskResult = _generateNextTask(nextState); progress = newTaskResult.progress; queue = newTaskResult.queue; } - nextState = _recalculateEncumbrance( - nextState.copyWith(progress: progress, queue: queue), - ); - - return ProgressTickResult( + return ( state: nextState, - leveledUp: leveledUp, - completedQuest: questDone, - completedAct: actDone, + progress: progress, + queue: queue, + actDone: actDone, gameComplete: gameComplete, ); } @@ -519,120 +652,155 @@ class ProgressService { final queue = state.queue; final oldTaskType = progress.currentTask.type; - // 1. Encumbrance가 가득 찼으면 시장으로 이동 (원본 667-669줄) - if (progress.encumbrance.position >= progress.encumbrance.max && - progress.encumbrance.max > 0) { - final taskResult = pq_logic.startTask( - progress, - l10n.taskHeadingToMarket(), - 4 * 1000, - ); - progress = taskResult.progress.copyWith( - currentTask: TaskInfo( - caption: taskResult.caption, - type: TaskType.market, - ), - currentCombat: null, // 비전투 태스크이므로 전투 상태 초기화 - ); - return (progress: progress, queue: queue); + // 1. Encumbrance 초과 시 시장 이동 + if (_shouldGoToMarket(progress)) { + return _createMarketTask(progress, queue); } - // 2. kill/heading/buying 태스크가 아니었으면 heading 또는 buying 태스크 실행 - // (원본 670-677줄) - buying 완료 후 무한 루프 방지 - if (oldTaskType != TaskType.kill && - oldTaskType != TaskType.neutral && - oldTaskType != TaskType.buying) { - // Gold가 충분하면 장비 구매 (Common 장비 가격 기준) - // 실제 구매 가격과 동일한 공식 사용: level * 50 - final gold = state.inventory.gold; - final equipPrice = state.traits.level * 50; // Common 장비 1개 가격 - if (gold > equipPrice) { - final taskResult = pq_logic.startTask( - progress, - l10n.taskUpgradingHardware(), - 5 * 1000, - ); - progress = taskResult.progress.copyWith( - currentTask: TaskInfo( - caption: taskResult.caption, - type: TaskType.buying, - ), - currentCombat: null, // 비전투 태스크이므로 전투 상태 초기화 - ); - return (progress: progress, queue: queue); - } - - // Gold가 부족하면 전장으로 이동 (원본 674-676줄) - final taskResult = pq_logic.startTask( - progress, - l10n.taskEnteringDebugZone(), - 4 * 1000, - ); - progress = taskResult.progress.copyWith( - currentTask: TaskInfo( - caption: taskResult.caption, - type: TaskType.neutral, - ), - currentCombat: null, // 비전투 태스크이므로 전투 상태 초기화 - ); - return (progress: progress, queue: queue); + // 2. 전환 태스크 (buying/heading) + if (_needsTransitionTask(oldTaskType)) { + return _createTransitionTask(state, progress, queue); } - // 3. Act Boss 리트라이 체크 - // pendingActCompletion이 true면 Act Boss 재소환 + // 3. Act Boss 리트라이 if (state.progress.pendingActCompletion) { - final actProgressionService = ActProgressionService(config: config); - final actBoss = actProgressionService.createActBoss(state); - final combatCalculator = CombatCalculator(rng: state.rng); - final durationMillis = combatCalculator.estimateCombatDurationMs( - player: actBoss.playerStats, - monster: actBoss.monsterStats, - ); + return _createActBossRetryTask(state, progress, queue); + } + // 4. 최종 보스 전투 + if (state.progress.finalBossState == FinalBossState.fighting && + !state.progress.isInBossLevelingMode) { + if (state.progress.bossLevelingEndTime != null) { + progress = progress.copyWith(clearBossLevelingEndTime: true); + } + final actProgressionService = ActProgressionService(config: config); + return actProgressionService.startFinalBossFight(state, progress, queue); + } + + // 5. 일반 몬스터 전투 + return _createMonsterTask(state, progress, queue); + } + + /// 시장 이동 조건 확인 + bool _shouldGoToMarket(ProgressState progress) { + return progress.encumbrance.position >= progress.encumbrance.max && + progress.encumbrance.max > 0; + } + + /// 전환 태스크 필요 여부 확인 + bool _needsTransitionTask(TaskType oldTaskType) { + return oldTaskType != TaskType.kill && + oldTaskType != TaskType.neutral && + oldTaskType != TaskType.buying; + } + + /// 시장 이동 태스크 생성 + ({ProgressState progress, QueueState queue}) _createMarketTask( + ProgressState progress, + QueueState queue, + ) { + final taskResult = pq_logic.startTask( + progress, + l10n.taskHeadingToMarket(), + 4 * 1000, + ); + final updatedProgress = taskResult.progress.copyWith( + currentTask: TaskInfo( + caption: taskResult.caption, + type: TaskType.market, + ), + currentCombat: null, + ); + return (progress: updatedProgress, queue: queue); + } + + /// 전환 태스크 생성 (buying 또는 heading) + ({ProgressState progress, QueueState queue}) _createTransitionTask( + GameState state, + ProgressState progress, + QueueState queue, + ) { + final gold = state.inventory.gold; + final equipPrice = state.traits.level * 50; + + // Gold 충분 시 장비 구매 + if (gold > equipPrice) { final taskResult = pq_logic.startTask( progress, - l10n.taskDebugging(actBoss.monsterStats.name), - durationMillis, + l10n.taskUpgradingHardware(), + 5 * 1000, ); - - progress = taskResult.progress.copyWith( + final updatedProgress = 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, - monsterSize: getBossSizeForAct(state.progress.plotStageCount), + type: TaskType.buying, ), - currentCombat: actBoss, + currentCombat: null, ); - - return (progress: progress, queue: queue); + return (progress: updatedProgress, queue: queue); } - // 4. 최종 보스 전투 체크 - // finalBossState == fighting이면 Glitch God 스폰 - // 단, 레벨링 모드 중이면 일반 몬스터로 레벨업 후 재도전 - if (state.progress.finalBossState == FinalBossState.fighting) { - if (state.progress.isInBossLevelingMode) { - // 레벨링 모드: 일반 몬스터 전투로 대체 (아래 MonsterTask로 진행) - } else { - // 레벨링 모드 종료 또는 첫 도전: 보스전 시작 - // 레벨링 모드가 끝났으면 타이머 초기화 - if (state.progress.bossLevelingEndTime != null) { - progress = progress.copyWith(clearBossLevelingEndTime: true); - } - final actProgressionService = ActProgressionService(config: config); - return actProgressionService.startFinalBossFight(state, progress, queue); - } - } + // Gold 부족 시 전장 이동 + final taskResult = pq_logic.startTask( + progress, + l10n.taskEnteringDebugZone(), + 4 * 1000, + ); + final updatedProgress = taskResult.progress.copyWith( + currentTask: TaskInfo( + caption: taskResult.caption, + type: TaskType.neutral, + ), + currentCombat: null, + ); + return (progress: updatedProgress, queue: queue); + } - // 5. MonsterTask 실행 (원본 678-684줄) + /// Act Boss 재도전 태스크 생성 + ({ProgressState progress, QueueState queue}) _createActBossRetryTask( + GameState state, + ProgressState progress, + QueueState queue, + ) { + final actProgressionService = ActProgressionService(config: config); + final actBoss = actProgressionService.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, + ); + + final updatedProgress = taskResult.progress.copyWith( + currentTask: TaskInfo( + caption: taskResult.caption, + type: TaskType.kill, + monsterBaseName: actBoss.monsterStats.name, + monsterPart: '*', + monsterLevel: actBoss.monsterStats.level, + monsterGrade: MonsterGrade.boss, + monsterSize: getBossSizeForAct(state.progress.plotStageCount), + ), + currentCombat: actBoss, + ); + + return (progress: updatedProgress, queue: queue); + } + + /// 일반 몬스터 전투 태스크 생성 + ({ProgressState progress, QueueState queue}) _createMonsterTask( + GameState state, + ProgressState progress, + QueueState queue, + ) { final level = state.traits.level; - // 원본 Main.pas:548-551: 25% 확률로 Quest Monster 사용 - // fQuest.Caption이 비어있지 않으면 해당 몬스터 데이터 전달 + // 퀘스트 몬스터 데이터 확인 final questMonster = state.progress.currentQuestMonster; final questMonsterData = questMonster?.monsterData; final questLevel = questMonsterData != null @@ -640,6 +808,7 @@ class ProgressService { 0 : null; + // 몬스터 생성 final monsterResult = pq_logic.monsterTask( config, state.rng, @@ -648,8 +817,7 @@ class ProgressService { questLevel, ); - // 전투용 몬스터 레벨 조정 (밸런스) - // Act별 최소 레벨과 플레이어 레벨 중 큰 값을 기준으로 ±3 범위 제한 + // 몬스터 레벨 조정 (밸런스) final actMinLevel = ActMonsterLevel.forPlotStage( state.progress.plotStageCount, ); @@ -658,7 +826,7 @@ class ProgressService { .clamp(math.max(1, baseLevel - 3), baseLevel + 3) .toInt(); - // 전투 스탯 생성 (Phase 12: 몬스터 레벨 기반 페널티 적용) + // 전투 스탯 생성 final playerCombatStats = CombatStats.fromStats( stats: state.stats, equipment: state.equipment, @@ -673,13 +841,12 @@ class ProgressService { plotStageCount: state.progress.plotStageCount, ); - // 전투 상태 초기화 + // 전투 상태 및 지속시간 final combatState = CombatState.start( playerStats: playerCombatStats, monsterStats: monsterCombatStats, ); - // 태스크 지속시간 계산 (CombatCalculator 기반) final combatCalculator = CombatCalculator(rng: state.rng); final durationMillis = combatCalculator.estimateCombatDurationMs( player: playerCombatStats, @@ -692,14 +859,14 @@ class ProgressService { durationMillis, ); - // 몬스터 사이즈 결정 (Act 기반, Phase 13) + // 몬스터 사이즈 결정 final monsterSize = getMonsterSizeForAct( plotStageCount: state.progress.plotStageCount, grade: monsterResult.grade, rng: state.rng, ); - progress = taskResult.progress.copyWith( + final updatedProgress = taskResult.progress.copyWith( currentTask: TaskInfo( caption: taskResult.caption, type: TaskType.kill, @@ -712,7 +879,7 @@ class ProgressService { currentCombat: combatState, ); - return (progress: progress, queue: queue); + return (progress: updatedProgress, queue: queue); } /// Advances quest completion, applies reward, and enqueues next quest task.