From 9b668d80a4ce8bfadc3f6609c3d94e013fa356dd Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Wed, 31 Dec 2025 18:14:31 +0900 Subject: [PATCH] =?UTF-8?q?feat(debug):=20=EB=AA=A8=EB=B0=94=EC=9D=BC=20?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=EC=97=90=20=EC=B9=98?= =?UTF-8?q?=ED=8A=B8=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MobileCarouselLayout에 치트 버튼 추가 - GameSessionController에 치트 활성화 상태 관리 추가 - ProgressLoop/ProgressService에 치트 메서드 추가 --- lib/src/core/engine/progress_loop.dart | 11 ++++ lib/src/core/engine/progress_service.dart | 39 ++++++++++-- lib/src/features/game/game_play_screen.dart | 5 ++ .../game/game_session_controller.dart | 32 +++++++++- .../game/layouts/mobile_carousel_layout.dart | 62 +++++++++++++++++++ 5 files changed, 144 insertions(+), 5 deletions(-) diff --git a/lib/src/core/engine/progress_loop.dart b/lib/src/core/engine/progress_loop.dart index 4d57b5d..017d0d7 100644 --- a/lib/src/core/engine/progress_loop.dart +++ b/lib/src/core/engine/progress_loop.dart @@ -40,6 +40,7 @@ class ProgressLoop { DateTime Function()? now, this.cheatsEnabled = false, this.onPlayerDied, + this.onGameComplete, List availableSpeeds = const [1, 5], }) : _state = initialState, _tickInterval = tickInterval, @@ -54,6 +55,9 @@ class ProgressLoop { /// 플레이어 사망 시 콜백 (Phase 4) final void Function()? onPlayerDied; + + /// 게임 클리어 시 콜백 (Act V 완료) + final void Function()? onGameComplete; final AutoSaveConfig _autoSaveConfig; final DateTime Function() _now; final StreamController _stateController; @@ -135,6 +139,13 @@ class ProgressLoop { onPlayerDied?.call(); } + // 게임 클리어 시 루프 정지 및 콜백 호출 (Act V 완료) + if (result.gameComplete) { + _timer?.cancel(); + _timer = null; + onGameComplete?.call(); + } + return _state; } diff --git a/lib/src/core/engine/progress_service.dart b/lib/src/core/engine/progress_service.dart index ef8a69f..786a004 100644 --- a/lib/src/core/engine/progress_service.dart +++ b/lib/src/core/engine/progress_service.dart @@ -26,6 +26,7 @@ class ProgressTickResult { this.completedQuest = false, this.completedAct = false, this.playerDied = false, + this.gameComplete = false, }); final GameState state; @@ -36,8 +37,11 @@ class ProgressTickResult { /// 플레이어 사망 여부 (Phase 4) final bool playerDied; + /// 게임 클리어 여부 (Act V 완료) + final bool gameComplete; + bool get shouldAutosave => - leveledUp || completedQuest || completedAct || playerDied; + leveledUp || completedQuest || completedAct || playerDied || gameComplete; } /// Drives quest/plot/task progression by applying queued actions and rewards. @@ -155,6 +159,7 @@ class ProgressService { var leveledUp = false; var questDone = false; var actDone = false; + var gameComplete = false; // 스킬 시스템 시간 업데이트 (Phase 3) final skillService = SkillService(rng: state.rng); @@ -400,8 +405,10 @@ class ProgressService { // plot 타입이 dequeue 되면 completeAct 실행 (원본 Main.pas 로직) if (dq.kind == QueueKind.plot) { nextState = nextState.copyWith(progress: progress, queue: queue); - nextState = completeAct(nextState); + final actResult = completeAct(nextState); + nextState = actResult.state; actDone = true; + gameComplete = actResult.gameComplete; progress = nextState.progress; queue = nextState.queue; } @@ -422,6 +429,7 @@ class ProgressService { leveledUp: leveledUp, completedQuest: questDone, completedAct: actDone, + gameComplete: gameComplete, ); } @@ -617,7 +625,30 @@ class ProgressService { } /// Advances plot to next act and applies any act-level rewards. - GameState completeAct(GameState state) { + /// Returns gameComplete=true if Act V was completed (game ends). + ({GameState state, bool gameComplete}) completeAct(GameState state) { + // Act V 완료 시 (plotStageCount == 5) 게임 클리어 + // plotStageCount: 0=Prologue, 1=Act I, 2=Act II, 3=Act III, 4=Act IV, 5=Act V + if (state.progress.plotStageCount >= 5) { + // Act V 완료 - 게임 클리어! + // 히스토리만 업데이트하고 새 Act는 생성하지 않음 + final updatedPlotHistory = [ + ...state.progress.plotHistory.map( + (e) => e.isComplete ? e : e.copyWith(isComplete: true), + ), + const HistoryEntry(caption: '*** THE END ***', isComplete: true), + ]; + + final updatedProgress = state.progress.copyWith( + plotHistory: updatedPlotHistory, + ); + + return ( + state: state.copyWith(progress: updatedProgress), + gameComplete: true, + ); + } + final actResult = pq_logic.completeAct(state.progress.plotStageCount); var nextState = state; for (final reward in actResult.rewards) { @@ -648,7 +679,7 @@ class ProgressService { nextState = _startFirstQuest(nextState); } - return _recalculateEncumbrance(nextState); + return (state: _recalculateEncumbrance(nextState), gameComplete: false); } /// 첫 퀘스트 시작 (Act I 시작 시) diff --git a/lib/src/features/game/game_play_screen.dart b/lib/src/features/game/game_play_screen.dart index b78a09f..7687b0d 100644 --- a/lib/src/features/game/game_play_screen.dart +++ b/lib/src/features/game/game_play_screen.dart @@ -844,6 +844,11 @@ class _GamePlayScreenState extends State // 통계 및 도움말 onShowStatistics: () => _showStatisticsDialog(context), onShowHelp: () => HelpDialog.show(context), + // 치트 (디버그 모드) + cheatsEnabled: widget.controller.cheatsEnabled, + onCheatTask: () => widget.controller.loop?.cheatCompleteTask(), + onCheatQuest: () => widget.controller.loop?.cheatCompleteQuest(), + onCheatPlot: () => widget.controller.loop?.cheatCompletePlot(), ), // 사망 오버레이 if (state.isDead && state.deathInfo != null) diff --git a/lib/src/features/game/game_session_controller.dart b/lib/src/features/game/game_session_controller.dart index b66e50a..d07e2bb 100644 --- a/lib/src/features/game/game_session_controller.dart +++ b/lib/src/features/game/game_session_controller.dart @@ -6,12 +6,13 @@ import 'package:asciineverdie/src/core/engine/resurrection_service.dart'; import 'package:asciineverdie/src/core/engine/shop_service.dart'; import 'package:asciineverdie/src/core/model/game_state.dart'; import 'package:asciineverdie/src/core/model/game_statistics.dart'; +import 'package:asciineverdie/src/core/model/hall_of_fame.dart'; import 'package:asciineverdie/src/core/storage/hall_of_fame_storage.dart'; import 'package:asciineverdie/src/core/storage/save_manager.dart'; import 'package:asciineverdie/src/core/storage/statistics_storage.dart'; import 'package:flutter/foundation.dart'; -enum GameSessionStatus { idle, loading, running, error, dead } +enum GameSessionStatus { idle, loading, running, error, dead, complete } /// Presentation-friendly wrapper that owns ProgressLoop and SaveManager. class GameSessionController extends ChangeNotifier { @@ -104,6 +105,7 @@ class GameSessionController extends ChangeNotifier { now: _now, cheatsEnabled: cheatsEnabled, onPlayerDied: _onPlayerDied, + onGameComplete: _onGameComplete, availableSpeeds: availableSpeeds, ); @@ -272,6 +274,31 @@ class GameSessionController extends ChangeNotifier { notifyListeners(); } + /// 게임 클리어 콜백 (ProgressLoop에서 호출, Act V 완료 시) + void _onGameComplete() { + _status = GameSessionStatus.complete; + notifyListeners(); + + // Hall of Fame 등록 (비동기) + unawaited(_registerToHallOfFame()); + } + + /// 명예의 전당 등록 + Future _registerToHallOfFame() async { + if (_state == null) return; + + final entry = HallOfFameEntry.fromGameState( + state: _state!, + totalDeaths: _sessionStats.deathCount, + monstersKilled: _state!.progress.monstersKilled, + ); + + await _hallOfFameStorage.addEntry(entry); + + // 통계 기록 + await _statisticsStorage.recordGameComplete(); + } + /// 플레이어 부활 처리 (상태만 업데이트, 게임 재개는 별도로) /// /// HP/MP 회복, 빈 슬롯에 장비 자동 구매 @@ -308,4 +335,7 @@ class GameSessionController extends ChangeNotifier { /// 사망 상태 여부 bool get isDead => _status == GameSessionStatus.dead || (_state?.isDead ?? false); + + /// 게임 클리어 여부 + bool get isComplete => _status == GameSessionStatus.complete; } diff --git a/lib/src/features/game/layouts/mobile_carousel_layout.dart b/lib/src/features/game/layouts/mobile_carousel_layout.dart index 0d360dd..284b330 100644 --- a/lib/src/features/game/layouts/mobile_carousel_layout.dart +++ b/lib/src/features/game/layouts/mobile_carousel_layout.dart @@ -46,6 +46,10 @@ class MobileCarouselLayout extends StatefulWidget { this.onSfxVolumeChange, this.onShowStatistics, this.onShowHelp, + this.cheatsEnabled = false, + this.onCheatTask, + this.onCheatQuest, + this.onCheatPlot, }); final GameState state; @@ -81,6 +85,18 @@ class MobileCarouselLayout extends StatefulWidget { /// 도움말 표시 콜백 final VoidCallback? onShowHelp; + /// 치트 모드 활성화 여부 + final bool cheatsEnabled; + + /// 치트: 태스크 완료 + final VoidCallback? onCheatTask; + + /// 치트: 퀘스트 완료 + final VoidCallback? onCheatQuest; + + /// 치트: 액트(플롯) 완료 + final VoidCallback? onCheatPlot; + @override State createState() => _MobileCarouselLayoutState(); } @@ -547,6 +563,52 @@ class _MobileCarouselLayoutState extends State { }, ), + // 치트 섹션 (디버그 모드에서만 표시) + if (widget.cheatsEnabled) ...[ + const Divider(), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: Text( + 'DEBUG CHEATS', + style: TextStyle( + fontFamily: 'PressStart2P', + fontSize: 8, + color: Colors.red.shade300, + ), + ), + ), + ListTile( + leading: const Icon(Icons.fast_forward, color: Colors.red), + title: const Text('Skip Task (L+1)'), + subtitle: const Text('태스크 즉시 완료'), + onTap: () { + Navigator.pop(context); + widget.onCheatTask?.call(); + }, + ), + ListTile( + leading: const Icon(Icons.skip_next, color: Colors.red), + title: const Text('Skip Quest (Q!)'), + subtitle: const Text('퀘스트 즉시 완료'), + onTap: () { + Navigator.pop(context); + widget.onCheatQuest?.call(); + }, + ), + ListTile( + leading: const Icon(Icons.double_arrow, color: Colors.red), + title: const Text('Skip Act (P!)'), + subtitle: const Text('액트 즉시 완료 (명예의 전당 테스트용)'), + onTap: () { + Navigator.pop(context); + widget.onCheatPlot?.call(); + }, + ), + ], + const SizedBox(height: 8), ], ),