From 6156eef90d637de06233469ae81a7a8a63b690d0 Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Mon, 30 Mar 2026 20:45:36 +0900 Subject: [PATCH] =?UTF-8?q?refactor(engine):=20progress=5Fservice=20832?= =?UTF-8?q?=EC=A4=84=20=E2=86=92=20543=EC=A4=84=20=EB=B6=84=ED=95=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - kill_task_handler.dart (133줄): 킬 태스크 완료, 전리품, 보스 - quest_completion_handler.dart (177줄): 퀘스트/플롯 진행 - exp_handler.dart (92줄): 경험치 획득, 레벨업 - 콜백 기반 의존성 주입으로 순환 참조 방지 --- lib/src/core/engine/exp_handler.dart | 92 +++ lib/src/core/engine/kill_task_handler.dart | 129 +++++ lib/src/core/engine/progress_service.dart | 531 ++++-------------- .../core/engine/quest_completion_handler.dart | 177 ++++++ 4 files changed, 519 insertions(+), 410 deletions(-) create mode 100644 lib/src/core/engine/exp_handler.dart create mode 100644 lib/src/core/engine/kill_task_handler.dart create mode 100644 lib/src/core/engine/quest_completion_handler.dart diff --git a/lib/src/core/engine/exp_handler.dart b/lib/src/core/engine/exp_handler.dart new file mode 100644 index 0000000..c57a70a --- /dev/null +++ b/lib/src/core/engine/exp_handler.dart @@ -0,0 +1,92 @@ +import 'package:asciineverdie/data/race_data.dart'; +import 'package:asciineverdie/src/core/engine/game_mutations.dart'; +import 'package:asciineverdie/src/core/engine/reward_service.dart'; +import 'package:asciineverdie/src/core/model/game_state.dart'; +import 'package:asciineverdie/src/core/util/balance_constants.dart'; + +/// 경험치(EXP) 획득 및 레벨업(level-up) 처리를 담당하는 핸들러. +class ExpHandler { + const ExpHandler({ + required this.mutations, + required this.rewards, + required this.recalculateEncumbrance, + }); + + final GameMutations mutations; + final RewardService rewards; + + /// 무게(encumbrance) 재계산 콜백 + final GameState Function(GameState) recalculateEncumbrance; + + /// 경험치 획득 및 레벨업 처리 + ({GameState state, ProgressState progress, bool leveledUp}) handleExpGain( + GameState state, + ProgressState progress, + int monsterExpReward, + ) { + var nextState = state; + var leveledUp = false; + + 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: overflowExp), + ); + } + } else { + progress = progress.copyWith( + exp: progress.exp.copyWith(position: newExpPos), + ); + } + + return (state: nextState, progress: progress, leveledUp: leveledUp); + } + + /// 레벨업 처리 (스탯 증가, 스킬/주문 획득) + GameState levelUp(GameState state) { + // 최대 레벨(100) 안전장치: 이미 100레벨이면 레벨업하지 않음 + if (state.traits.level >= 100) { + return state; + } + + final nextLevel = state.traits.level + 1; + final rng = state.rng; + + // HP/MP 증가량 (PlayerScaling 기반 + 랜덤 변동) + // 기존: CON/3 + 1 + random(0-3) → ~6-9 HP/레벨 (너무 낮음) + // 신규: 18 + CON/5 + random(0-4) → ~20-25 HP/레벨 (생존율 개선) + final hpGain = 18 + state.stats.con ~/ 5 + rng.nextInt(5); + final mpGain = 6 + state.stats.intelligence ~/ 5 + rng.nextInt(3); + + var nextState = state.copyWith( + traits: state.traits.copyWith(level: nextLevel), + stats: state.stats.copyWith( + hpMax: state.stats.hpMax + hpGain, + mpMax: state.stats.mpMax + mpGain, + ), + ); + + // 스탯 2회, 주문(spell) 1회 획득 (원본 레벨업 규칙) + nextState = mutations.winStat(nextState); + nextState = mutations.winStat(nextState); + nextState = mutations.winSpell(nextState, nextState.stats.wis, nextLevel); + + final expBar = ProgressBarState( + position: 0, + max: ExpConstants.requiredExp(nextLevel), + ); + final progress = nextState.progress.copyWith(exp: expBar); + nextState = nextState.copyWith(progress: progress); + return recalculateEncumbrance(nextState); + } +} diff --git a/lib/src/core/engine/kill_task_handler.dart b/lib/src/core/engine/kill_task_handler.dart new file mode 100644 index 0000000..d03e252 --- /dev/null +++ b/lib/src/core/engine/kill_task_handler.dart @@ -0,0 +1,129 @@ +import 'package:asciineverdie/data/class_data.dart'; +import 'package:asciineverdie/src/core/engine/loot_handler.dart'; +import 'package:asciineverdie/src/core/engine/progress_service.dart'; +import 'package:asciineverdie/src/core/model/class_traits.dart'; +import 'package:asciineverdie/src/core/model/combat_event.dart'; +import 'package:asciineverdie/src/core/model/game_state.dart'; +import 'package:asciineverdie/src/core/model/pq_config.dart'; +import 'package:asciineverdie/src/core/util/pq_logic.dart' as pq_logic; + +/// 킬 태스크(kill task) 완료 시 처리를 담당하는 핸들러. +/// +/// HP 회복, 전리품(loot) 획득, 보스 처리 등을 수행한다. +class KillTaskHandler { + const KillTaskHandler({ + required this.config, + required this.lootHandler, + required this.completeActFn, + }); + + final PqConfig config; + final LootHandler lootHandler; + + /// Act 완료 처리 콜백 (ProgressService.completeAct 위임) + final ({GameState state, bool gameComplete}) Function(GameState) + completeActFn; + + /// 킬 태스크 완료 처리 (HP 회복, 전리품, 보스 처리) + ({ + GameState state, + ProgressState progress, + QueueState queue, + ProgressTickResult? earlyReturn, + }) + handle(GameState state, ProgressState progress, QueueState queue) { + var nextState = state; + + // 전투 후 HP 회복(heal) + 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), + ); + } + + // 전리품(loot) 획득 + final lootResult = lootHandler.winLoot(nextState); + nextState = lootResult.state; + + // 물약(potion) 드랍 로그 추가 + 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) { + 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); + + // 최종 보스(final boss) 처치 체크 + if (progress.finalBossState == FinalBossState.fighting) { + progress = progress.copyWith(finalBossState: FinalBossState.defeated); + nextState = nextState.copyWith(progress: progress); + final actResult = completeActFn(nextState); + return ( + state: actResult.state, + progress: actResult.state.progress, + queue: actResult.state.queue, + earlyReturn: ProgressTickResult( + state: actResult.state, + completedAct: true, + gameComplete: true, + ), + ); + } + + return ( + state: nextState, + progress: progress, + queue: queue, + earlyReturn: null, + ); + } +} diff --git a/lib/src/core/engine/progress_service.dart b/lib/src/core/engine/progress_service.dart index 0129fcc..ab82ace 100644 --- a/lib/src/core/engine/progress_service.dart +++ b/lib/src/core/engine/progress_service.dart @@ -1,17 +1,15 @@ -import 'package:asciineverdie/data/class_data.dart'; import 'package:asciineverdie/data/game_text_l10n.dart' as l10n; -import 'package:asciineverdie/data/race_data.dart'; -import 'package:asciineverdie/src/core/model/class_traits.dart'; -import 'package:asciineverdie/src/core/engine/act_progression_service.dart'; import 'package:asciineverdie/src/core/engine/combat_tick_service.dart'; import 'package:asciineverdie/src/core/engine/death_handler.dart'; +import 'package:asciineverdie/src/core/engine/exp_handler.dart'; import 'package:asciineverdie/src/core/engine/game_mutations.dart'; +import 'package:asciineverdie/src/core/engine/kill_task_handler.dart'; import 'package:asciineverdie/src/core/engine/loot_handler.dart'; import 'package:asciineverdie/src/core/engine/market_service.dart'; +import 'package:asciineverdie/src/core/engine/quest_completion_handler.dart'; import 'package:asciineverdie/src/core/engine/reward_service.dart'; import 'package:asciineverdie/src/core/engine/skill_service.dart'; import 'package:asciineverdie/src/core/engine/task_generator.dart'; -import 'package:asciineverdie/src/core/model/combat_event.dart'; import 'package:asciineverdie/src/core/model/game_state.dart'; import 'package:asciineverdie/src/core/model/pq_config.dart'; import 'package:asciineverdie/src/core/util/balance_constants.dart'; @@ -42,27 +40,68 @@ class ProgressTickResult { leveledUp || completedQuest || completedAct || playerDied || gameComplete; } -/// Drives quest/plot/task progression by applying queued actions and rewards. +/// 게임 진행(tick) 루프를 구동하는 메인 서비스. +/// +/// 각 세부 처리는 핸들러에 위임한다: +/// - [KillTaskHandler]: 킬 태스크 완료 (HP 회복, 전리품, 보스) +/// - [QuestCompletionHandler]: 퀘스트/플롯 진행 및 Act 완료 +/// - [ExpHandler]: 경험치 획득 및 레벨업 class ProgressService { ProgressService({ required this.config, required this.mutations, required this.rewards, }) : _taskGenerator = TaskGenerator(config: config), - _lootHandler = LootHandler(mutations: mutations); + _killTaskHandler = KillTaskHandler( + config: config, + lootHandler: LootHandler(mutations: mutations), + completeActFn: _placeholder, + ), + _questHandler = QuestCompletionHandler( + config: config, + rewards: rewards, + recalculateEncumbrance: _placeholderState, + ), + _expHandler = ExpHandler( + mutations: mutations, + rewards: rewards, + recalculateEncumbrance: _placeholderState, + ) { + // 초기화 후 콜백(callback) 바인딩 + _killTaskHandler = KillTaskHandler( + config: config, + lootHandler: LootHandler(mutations: mutations), + completeActFn: completeAct, + ); + _questHandler = QuestCompletionHandler( + config: config, + rewards: rewards, + recalculateEncumbrance: _recalculateEncumbrance, + ); + _expHandler = ExpHandler( + mutations: mutations, + rewards: rewards, + recalculateEncumbrance: _recalculateEncumbrance, + ); + } + + // 초기화 시점 placeholder (바로 덮어쓰여짐) + static ({GameState state, bool gameComplete}) _placeholder(GameState s) => + (state: s, gameComplete: false); + static GameState _placeholderState(GameState s) => s; final PqConfig config; final GameMutations mutations; final RewardService rewards; final TaskGenerator _taskGenerator; - final LootHandler _lootHandler; + late KillTaskHandler _killTaskHandler; + late QuestCompletionHandler _questHandler; + late ExpHandler _expHandler; static const _deathHandler = DeathHandler(); /// 새 게임 초기화 (원본 GoButtonClick, Main.pas:741-767) - /// Prologue 태스크들을 큐에 추가하고 첫 태스크 시작 GameState initializeNewGame(GameState state) { - // 초기 큐 설정 - ASCII NEVER DIE 세계관 프롤로그 (l10n 지원) final prologueTexts = l10n.prologueTexts; final initialQueue = [ QueueEntry( @@ -97,20 +136,16 @@ class ProgressService { ), ]; - // 첫 번째 태스크 시작 (원본 752줄) final taskResult = pq_logic.startTask( state.progress, l10n.taskCompiling, 2 * 1000, ); - // ExpBar 초기화 (원본 743-746줄) final expBar = ProgressBarState( position: 0, max: ExpConstants.requiredExp(1), ); - - // PlotBar 초기화 - Prologue 5분 (300초) final plotBar = const ProgressBarState(position: 0, max: 300); final progress = taskResult.progress.copyWith( @@ -120,7 +155,7 @@ class ProgressService { caption: '${l10n.taskCompiling}...', type: TaskType.load, ), - plotStageCount: 1, // Prologue + plotStageCount: 1, questCount: 0, plotHistory: [ HistoryEntry(caption: l10n.taskPrologue, isComplete: false), @@ -136,7 +171,7 @@ class ProgressService { ); } - /// Starts a task and tags its type (kill, plot, load, neutral). + /// 태스크 시작 (타입 태깅 포함) GameState startTask( GameState state, { required String caption, @@ -154,11 +189,11 @@ class ProgressService { return state.copyWith(progress: progress); } - /// Tick the timer loop (equivalent to Timer1Timer in the original code). + /// 메인 틱(tick) 처리 (원본 Timer1Timer 대응) ProgressTickResult tick(GameState state, int elapsedMillis) { final int clamped = elapsedMillis.clamp(0, 10000).toInt(); - // 1. 스킬 시스템 업데이트 (시간, 버프, MP 회복) + // 1. 스킬 시스템 업데이트 var nextState = _updateSkillSystem(state, clamped); var progress = nextState.progress; var queue = nextState.queue; @@ -180,7 +215,7 @@ class ProgressService { // 4. 킬 태스크 완료 처리 if (gain) { - final killResult = _handleKillTaskCompletion(nextState, progress, queue); + final killResult = _killTaskHandler.handle(nextState, progress, queue); if (killResult.earlyReturn != null) return killResult.earlyReturn!; nextState = killResult.state; progress = killResult.progress; @@ -200,14 +235,18 @@ class ProgressService { // 6. 경험치/레벨업 처리 if (gain && nextState.traits.level < 100 && monsterExpReward > 0) { - final expResult = _handleExpGain(nextState, progress, monsterExpReward); + final expResult = _expHandler.handleExpGain( + nextState, + progress, + monsterExpReward, + ); nextState = expResult.state; progress = expResult.progress; leveledUp = expResult.leveledUp; } // 7. 퀘스트 진행 처리 - final questResult = _handleQuestProgress( + final questResult = _questHandler.handleQuestProgress( nextState, progress, queue, @@ -220,7 +259,12 @@ class ProgressService { questDone = questResult.completed; // 8. 플롯 진행 및 Act Boss 소환 처리 - progress = _handlePlotProgress(nextState, progress, gain, incrementSeconds); + progress = _questHandler.handlePlotProgress( + nextState, + progress, + gain, + incrementSeconds, + ); // 9. 다음 태스크 디큐/생성 final dequeueResult = _handleTaskDequeue(nextState, progress, queue); @@ -243,6 +287,60 @@ class ProgressService { ); } + /// 퀘스트 완료 처리 (public API 유지) + GameState completeQuest(GameState state) => + _questHandler.completeQuest(state); + + /// Act 완료 처리 (public API 유지) + ({GameState state, bool gameComplete}) completeAct(GameState state) => + _questHandler.completeAct(state); + + /// 개발자 치트(cheat): 태스크 바 즉시 완료 + GameState forceTaskComplete(GameState state) { + final progress = state.progress.copyWith( + task: state.progress.task.copyWith(position: state.progress.task.max), + ); + return state.copyWith(progress: progress); + } + + /// 개발자 치트: 퀘스트 바 즉시 완료 + GameState forceQuestComplete(GameState state) { + final progress = state.progress.copyWith( + task: state.progress.task.copyWith(position: state.progress.task.max), + quest: state.progress.quest.copyWith(position: state.progress.quest.max), + ); + return state.copyWith(progress: progress); + } + + /// 개발자 치트: 플롯 바 즉시 완료 + GameState forcePlotComplete(GameState state) { + final nextPlotStage = state.progress.plotStageCount + 1; + final targetLevel = ActMonsterLevel.forPlotStage(nextPlotStage); + var nextState = state; + + while (nextState.traits.level < targetLevel && + nextState.traits.level < 100) { + nextState = _expHandler.levelUp(nextState); + } + + final equipLevel = nextState.traits.level; + for (var slotIndex = 0; slotIndex < Equipment.slotCount; slotIndex++) { + nextState = mutations.winEquipByIndex(nextState, equipLevel, slotIndex); + } + + var progress = nextState.progress.copyWith( + task: nextState.progress.task.copyWith( + position: nextState.progress.task.max, + ), + ); + nextState = nextState.copyWith(progress: progress); + + final actResult = completeAct(nextState); + return actResult.state; + } + + // ── 내부 헬퍼 메서드 ────────────────────────────────── + /// 스킬 시스템 업데이트 (시간, 버프 정리, MP 회복) GameState _updateSkillSystem(GameState state, int elapsedMs) { final skillService = SkillService(rng: state.rng); @@ -254,7 +352,6 @@ class ProgressService { var nextState = state.copyWith(skillSystem: skillSystem); - // 비전투 시 MP 회복 final isInCombat = state.progress.currentTask.type == TaskType.kill && state.progress.currentCombat != null && @@ -293,7 +390,6 @@ class ProgressService { var updatedPotionInventory = state.potionInventory; var nextState = state; - // 킬 태스크 중 전투 진행 if (progress.currentTask.type == TaskType.kill && updatedCombat != null && updatedCombat.isActive) { @@ -310,7 +406,6 @@ class ProgressService { updatedPotionInventory = combatResult.potionInventory!; } - // 플레이어 사망 체크 if (!updatedCombat.playerStats.isAlive) { final monsterName = updatedCombat.monsterStats.name; nextState = _deathHandler.processPlayerDeath( @@ -336,113 +431,6 @@ class ProgressService { 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), - ); - } - - // 전리품 획득 - final lootResult = _lootHandler.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) { - 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, - ), - ); - } - - return ( - state: nextState, - progress: progress, - queue: queue, - earlyReturn: null, - ); - } - /// 시장/판매/구매 태스크 완료 처리 ({ GameState state, @@ -489,113 +477,6 @@ class ProgressService { ); } - /// 경험치 획득 및 레벨업 처리 - ({GameState state, ProgressState progress, bool leveledUp}) _handleExpGain( - GameState state, - ProgressState progress, - int monsterExpReward, - ) { - var nextState = state; - var leveledUp = false; - - 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: overflowExp), - ); - } - } else { - progress = progress.copyWith( - exp: progress.exp.copyWith(position: newExpPos), - ); - } - - 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); - questDone = true; - progress = nextState.progress; - queue = nextState.queue; - } else { - progress = progress.copyWith( - quest: progress.quest.copyWith( - position: progress.quest.position + incrementSeconds, - ), - ); - } - } - - 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) { - final actProgressionService = ActProgressionService(config: config); - final actBoss = actProgressionService.createActBoss(state); - return progress.copyWith( - plot: progress.plot.copyWith(position: 0), - currentCombat: actBoss, - pendingActCompletion: true, - ); - } else if (progress.currentTask.type != TaskType.load && - progress.plot.max > 0 && - !progress.pendingActCompletion) { - final uncappedPlot = progress.plot.position + incrementSeconds; - final int newPlotPos = uncappedPlot > progress.plot.max - ? progress.plot.max - : uncappedPlot; - return progress.copyWith( - plot: progress.plot.copyWith(position: newPlotPos), - ); - } - return progress; - } - /// 태스크 디큐 및 생성 처리 ({ GameState state, @@ -645,178 +526,8 @@ class ProgressService { ); } - /// Advances quest completion, applies reward, and enqueues next quest task. - GameState completeQuest(GameState state) { - final result = pq_logic.completeQuest( - config, - state.rng, - state.traits.level, - ); - - var nextState = _applyReward(state, result.reward); - final questCount = nextState.progress.questCount + 1; - - // 퀘스트 히스토리 업데이트: 이전 퀘스트 완료 표시, 새 퀘스트 추가 - final updatedQuestHistory = [ - ...nextState.progress.questHistory.map( - (e) => e.isComplete ? e : e.copyWith(isComplete: true), - ), - HistoryEntry(caption: result.caption, isComplete: false), - ]; - - // 퀘스트 몬스터 정보 저장 (Exterminate 타입용) - // 원본 fQuest.Caption = monsterData, fQuest.Tag = monsterIndex - final questMonster = result.monsterIndex != null - ? QuestMonsterInfo( - monsterData: result.monsterName!, - monsterIndex: result.monsterIndex!, - ) - : null; - - // Append quest entry to queue (task kind). - final updatedQueue = QueueState( - entries: [ - ...nextState.queue.entries, - QueueEntry( - kind: QueueKind.task, - durationMillis: 50 + nextState.rng.nextInt(100), - caption: result.caption, - taskType: TaskType.neutral, - ), - ], - ); - - // Update quest progress bar with reset position. - final progress = nextState.progress.copyWith( - quest: ProgressBarState( - position: 0, - max: 50 + nextState.rng.nextInt(100), - ), - questCount: questCount, - questHistory: updatedQuestHistory, - currentQuestMonster: questMonster, - ); - - return _recalculateEncumbrance( - nextState.copyWith(progress: progress, queue: updatedQueue), - ); - } - - /// Advances plot to next act and applies any act-level rewards. - /// Returns gameComplete=true if Final Boss was defeated (game ends). - ({GameState state, bool gameComplete}) completeAct(GameState state) { - final actProgressionService = ActProgressionService(config: config); - - // Act 보상 먼저 적용 - final actRewards = actProgressionService.getActRewards( - state.progress.plotStageCount, - ); - var nextState = state; - for (final reward in actRewards) { - nextState = _applyReward(nextState, reward); - } - - // Act 완료 처리 (ActProgressionService 위임) - final result = actProgressionService.completeAct(nextState); - - return ( - state: _recalculateEncumbrance(result.state), - gameComplete: result.gameComplete, - ); - } - - /// Developer-only cheat hooks for quickly finishing bars. - GameState forceTaskComplete(GameState state) { - final progress = state.progress.copyWith( - task: state.progress.task.copyWith(position: state.progress.task.max), - ); - return state.copyWith(progress: progress); - } - - GameState forceQuestComplete(GameState state) { - final progress = state.progress.copyWith( - task: state.progress.task.copyWith(position: state.progress.task.max), - quest: state.progress.quest.copyWith(position: state.progress.quest.max), - ); - return state.copyWith(progress: progress); - } - - GameState forcePlotComplete(GameState state) { - // 다음 Act의 최소 몬스터 레벨까지 레벨업 - final nextPlotStage = state.progress.plotStageCount + 1; - final targetLevel = ActMonsterLevel.forPlotStage(nextPlotStage); - var nextState = state; - - // 현재 레벨이 목표 레벨보다 낮으면 레벨업 (최대 100레벨) - while (nextState.traits.level < targetLevel && - nextState.traits.level < 100) { - nextState = _levelUp(nextState); - } - - // 모든 장비 슬롯을 목표 레벨에 맞는 장비로 교체 (전투 보상 드랍 공식 사용) - final equipLevel = nextState.traits.level; - for (var slotIndex = 0; slotIndex < Equipment.slotCount; slotIndex++) { - nextState = mutations.winEquipByIndex(nextState, equipLevel, slotIndex); - } - - // 태스크 바 완료 처리 - var progress = nextState.progress.copyWith( - task: nextState.progress.task.copyWith( - position: nextState.progress.task.max, - ), - ); - nextState = nextState.copyWith(progress: progress); - - // 디버그 모드에서는 completeAct 직접 호출하여 plotStageCount 즉시 업데이트 - // 시네마틱은 생략하고 바로 다음 Act로 진입 - final actResult = completeAct(nextState); - return actResult.state; - } - - GameState _applyReward(GameState state, pq_logic.RewardKind reward) { - final updated = rewards.applyReward(state, reward); - return _recalculateEncumbrance(updated); - } - - GameState _levelUp(GameState state) { - // 최대 레벨(100) 안전장치: 이미 100레벨이면 레벨업하지 않음 - if (state.traits.level >= 100) { - return state; - } - - final nextLevel = state.traits.level + 1; - final rng = state.rng; - - // HP/MP 증가량 (PlayerScaling 기반 + 랜덤 변동) - // 기존: CON/3 + 1 + random(0-3) → ~6-9 HP/레벨 (너무 낮음) - // 신규: 18 + CON/5 + random(0-4) → ~20-25 HP/레벨 (생존율 개선) - final hpGain = 18 + state.stats.con ~/ 5 + rng.nextInt(5); - final mpGain = 6 + state.stats.intelligence ~/ 5 + rng.nextInt(3); - - var nextState = state.copyWith( - traits: state.traits.copyWith(level: nextLevel), - stats: state.stats.copyWith( - hpMax: state.stats.hpMax + hpGain, - mpMax: state.stats.mpMax + mpGain, - ), - ); - - // Win two stats and a spell, matching the original leveling rules. - nextState = mutations.winStat(nextState); - nextState = mutations.winStat(nextState); - nextState = mutations.winSpell(nextState, nextState.stats.wis, nextLevel); - - final expBar = ProgressBarState( - position: 0, - max: ExpConstants.requiredExp(nextLevel), - ); - final progress = nextState.progress.copyWith(exp: expBar); - nextState = nextState.copyWith(progress: progress); - return _recalculateEncumbrance(nextState); - } - + /// 무게(encumbrance) 재계산 GameState _recalculateEncumbrance(GameState state) { - // items에는 Gold가 포함되지 않음 (inventory.gold 필드로 관리) final encumValue = state.inventory.items.fold( 0, (sum, item) => sum + item.count, diff --git a/lib/src/core/engine/quest_completion_handler.dart b/lib/src/core/engine/quest_completion_handler.dart new file mode 100644 index 0000000..3201085 --- /dev/null +++ b/lib/src/core/engine/quest_completion_handler.dart @@ -0,0 +1,177 @@ +import 'package:asciineverdie/src/core/engine/act_progression_service.dart'; +import 'package:asciineverdie/src/core/engine/reward_service.dart'; +import 'package:asciineverdie/src/core/model/game_state.dart'; +import 'package:asciineverdie/src/core/model/pq_config.dart'; +import 'package:asciineverdie/src/core/util/pq_logic.dart' as pq_logic; + +/// 퀘스트(quest) 완료 및 플롯(plot) 진행을 담당하는 핸들러. +class QuestCompletionHandler { + const QuestCompletionHandler({ + required this.config, + required this.rewards, + required this.recalculateEncumbrance, + }); + + final PqConfig config; + final RewardService rewards; + + /// 무게(encumbrance) 재계산 콜백 + final GameState Function(GameState) recalculateEncumbrance; + + /// 퀘스트 진행 처리 + ({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); + questDone = true; + progress = nextState.progress; + queue = nextState.queue; + } else { + progress = progress.copyWith( + quest: progress.quest.copyWith( + position: progress.quest.position + incrementSeconds, + ), + ); + } + } + + return ( + state: nextState, + progress: progress, + queue: queue, + completed: questDone, + ); + } + + /// 퀘스트 완료 처리 (보상 적용, 다음 퀘스트 생성) + GameState completeQuest(GameState state) { + final result = pq_logic.completeQuest( + config, + state.rng, + state.traits.level, + ); + + var nextState = _applyReward(state, result.reward); + final questCount = nextState.progress.questCount + 1; + + // 퀘스트 히스토리(history) 업데이트: 이전 퀘스트 완료 표시, 새 퀘스트 추가 + final updatedQuestHistory = [ + ...nextState.progress.questHistory.map( + (e) => e.isComplete ? e : e.copyWith(isComplete: true), + ), + HistoryEntry(caption: result.caption, isComplete: false), + ]; + + // 퀘스트 몬스터 정보 저장 (Exterminate 타입용) + final questMonster = result.monsterIndex != null + ? QuestMonsterInfo( + monsterData: result.monsterName!, + monsterIndex: result.monsterIndex!, + ) + : null; + + // 큐에 퀘스트 태스크 추가 + final updatedQueue = QueueState( + entries: [ + ...nextState.queue.entries, + QueueEntry( + kind: QueueKind.task, + durationMillis: 50 + nextState.rng.nextInt(100), + caption: result.caption, + taskType: TaskType.neutral, + ), + ], + ); + + // 퀘스트 진행 바(bar) 리셋 + final progress = nextState.progress.copyWith( + quest: ProgressBarState( + position: 0, + max: 50 + nextState.rng.nextInt(100), + ), + questCount: questCount, + questHistory: updatedQuestHistory, + currentQuestMonster: questMonster, + ); + + return recalculateEncumbrance( + nextState.copyWith(progress: progress, queue: updatedQueue), + ); + } + + /// 플롯 진행 및 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) { + final actProgressionService = ActProgressionService(config: config); + final actBoss = actProgressionService.createActBoss(state); + return progress.copyWith( + plot: progress.plot.copyWith(position: 0), + currentCombat: actBoss, + pendingActCompletion: true, + ); + } else if (progress.currentTask.type != TaskType.load && + progress.plot.max > 0 && + !progress.pendingActCompletion) { + final uncappedPlot = progress.plot.position + incrementSeconds; + final int newPlotPos = uncappedPlot > progress.plot.max + ? progress.plot.max + : uncappedPlot; + return progress.copyWith( + plot: progress.plot.copyWith(position: newPlotPos), + ); + } + return progress; + } + + /// Act 완료 처리 (보상 적용 후 다음 Act로 진행) + /// gameComplete=true이면 최종 보스 격파로 게임 종료. + ({GameState state, bool gameComplete}) completeAct(GameState state) { + final actProgressionService = ActProgressionService(config: config); + + // Act 보상 먼저 적용 + final actRewards = actProgressionService.getActRewards( + state.progress.plotStageCount, + ); + var nextState = state; + for (final reward in actRewards) { + nextState = _applyReward(nextState, reward); + } + + // Act 완료 처리 (ActProgressionService 위임) + final result = actProgressionService.completeAct(nextState); + + return ( + state: recalculateEncumbrance(result.state), + gameComplete: result.gameComplete, + ); + } + + GameState _applyReward(GameState state, pq_logic.RewardKind reward) { + final updated = rewards.applyReward(state, reward); + return recalculateEncumbrance(updated); + } +}