feat(debug): 모바일 레이아웃에 치트 기능 추가
- MobileCarouselLayout에 치트 버튼 추가 - GameSessionController에 치트 활성화 상태 관리 추가 - ProgressLoop/ProgressService에 치트 메서드 추가
This commit is contained in:
@@ -40,6 +40,7 @@ class ProgressLoop {
|
|||||||
DateTime Function()? now,
|
DateTime Function()? now,
|
||||||
this.cheatsEnabled = false,
|
this.cheatsEnabled = false,
|
||||||
this.onPlayerDied,
|
this.onPlayerDied,
|
||||||
|
this.onGameComplete,
|
||||||
List<int> availableSpeeds = const [1, 5],
|
List<int> availableSpeeds = const [1, 5],
|
||||||
}) : _state = initialState,
|
}) : _state = initialState,
|
||||||
_tickInterval = tickInterval,
|
_tickInterval = tickInterval,
|
||||||
@@ -54,6 +55,9 @@ class ProgressLoop {
|
|||||||
|
|
||||||
/// 플레이어 사망 시 콜백 (Phase 4)
|
/// 플레이어 사망 시 콜백 (Phase 4)
|
||||||
final void Function()? onPlayerDied;
|
final void Function()? onPlayerDied;
|
||||||
|
|
||||||
|
/// 게임 클리어 시 콜백 (Act V 완료)
|
||||||
|
final void Function()? onGameComplete;
|
||||||
final AutoSaveConfig _autoSaveConfig;
|
final AutoSaveConfig _autoSaveConfig;
|
||||||
final DateTime Function() _now;
|
final DateTime Function() _now;
|
||||||
final StreamController<GameState> _stateController;
|
final StreamController<GameState> _stateController;
|
||||||
@@ -135,6 +139,13 @@ class ProgressLoop {
|
|||||||
onPlayerDied?.call();
|
onPlayerDied?.call();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 게임 클리어 시 루프 정지 및 콜백 호출 (Act V 완료)
|
||||||
|
if (result.gameComplete) {
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = null;
|
||||||
|
onGameComplete?.call();
|
||||||
|
}
|
||||||
|
|
||||||
return _state;
|
return _state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class ProgressTickResult {
|
|||||||
this.completedQuest = false,
|
this.completedQuest = false,
|
||||||
this.completedAct = false,
|
this.completedAct = false,
|
||||||
this.playerDied = false,
|
this.playerDied = false,
|
||||||
|
this.gameComplete = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
final GameState state;
|
final GameState state;
|
||||||
@@ -36,8 +37,11 @@ class ProgressTickResult {
|
|||||||
/// 플레이어 사망 여부 (Phase 4)
|
/// 플레이어 사망 여부 (Phase 4)
|
||||||
final bool playerDied;
|
final bool playerDied;
|
||||||
|
|
||||||
|
/// 게임 클리어 여부 (Act V 완료)
|
||||||
|
final bool gameComplete;
|
||||||
|
|
||||||
bool get shouldAutosave =>
|
bool get shouldAutosave =>
|
||||||
leveledUp || completedQuest || completedAct || playerDied;
|
leveledUp || completedQuest || completedAct || playerDied || gameComplete;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drives quest/plot/task progression by applying queued actions and rewards.
|
/// Drives quest/plot/task progression by applying queued actions and rewards.
|
||||||
@@ -155,6 +159,7 @@ class ProgressService {
|
|||||||
var leveledUp = false;
|
var leveledUp = false;
|
||||||
var questDone = false;
|
var questDone = false;
|
||||||
var actDone = false;
|
var actDone = false;
|
||||||
|
var gameComplete = false;
|
||||||
|
|
||||||
// 스킬 시스템 시간 업데이트 (Phase 3)
|
// 스킬 시스템 시간 업데이트 (Phase 3)
|
||||||
final skillService = SkillService(rng: state.rng);
|
final skillService = SkillService(rng: state.rng);
|
||||||
@@ -400,8 +405,10 @@ class ProgressService {
|
|||||||
// plot 타입이 dequeue 되면 completeAct 실행 (원본 Main.pas 로직)
|
// plot 타입이 dequeue 되면 completeAct 실행 (원본 Main.pas 로직)
|
||||||
if (dq.kind == QueueKind.plot) {
|
if (dq.kind == QueueKind.plot) {
|
||||||
nextState = nextState.copyWith(progress: progress, queue: queue);
|
nextState = nextState.copyWith(progress: progress, queue: queue);
|
||||||
nextState = completeAct(nextState);
|
final actResult = completeAct(nextState);
|
||||||
|
nextState = actResult.state;
|
||||||
actDone = true;
|
actDone = true;
|
||||||
|
gameComplete = actResult.gameComplete;
|
||||||
progress = nextState.progress;
|
progress = nextState.progress;
|
||||||
queue = nextState.queue;
|
queue = nextState.queue;
|
||||||
}
|
}
|
||||||
@@ -422,6 +429,7 @@ class ProgressService {
|
|||||||
leveledUp: leveledUp,
|
leveledUp: leveledUp,
|
||||||
completedQuest: questDone,
|
completedQuest: questDone,
|
||||||
completedAct: actDone,
|
completedAct: actDone,
|
||||||
|
gameComplete: gameComplete,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -617,7 +625,30 @@ class ProgressService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Advances plot to next act and applies any act-level rewards.
|
/// 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);
|
final actResult = pq_logic.completeAct(state.progress.plotStageCount);
|
||||||
var nextState = state;
|
var nextState = state;
|
||||||
for (final reward in actResult.rewards) {
|
for (final reward in actResult.rewards) {
|
||||||
@@ -648,7 +679,7 @@ class ProgressService {
|
|||||||
nextState = _startFirstQuest(nextState);
|
nextState = _startFirstQuest(nextState);
|
||||||
}
|
}
|
||||||
|
|
||||||
return _recalculateEncumbrance(nextState);
|
return (state: _recalculateEncumbrance(nextState), gameComplete: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 첫 퀘스트 시작 (Act I 시작 시)
|
/// 첫 퀘스트 시작 (Act I 시작 시)
|
||||||
|
|||||||
@@ -844,6 +844,11 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
// 통계 및 도움말
|
// 통계 및 도움말
|
||||||
onShowStatistics: () => _showStatisticsDialog(context),
|
onShowStatistics: () => _showStatisticsDialog(context),
|
||||||
onShowHelp: () => HelpDialog.show(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)
|
if (state.isDead && state.deathInfo != null)
|
||||||
|
|||||||
@@ -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/engine/shop_service.dart';
|
||||||
import 'package:asciineverdie/src/core/model/game_state.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/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/hall_of_fame_storage.dart';
|
||||||
import 'package:asciineverdie/src/core/storage/save_manager.dart';
|
import 'package:asciineverdie/src/core/storage/save_manager.dart';
|
||||||
import 'package:asciineverdie/src/core/storage/statistics_storage.dart';
|
import 'package:asciineverdie/src/core/storage/statistics_storage.dart';
|
||||||
import 'package:flutter/foundation.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.
|
/// Presentation-friendly wrapper that owns ProgressLoop and SaveManager.
|
||||||
class GameSessionController extends ChangeNotifier {
|
class GameSessionController extends ChangeNotifier {
|
||||||
@@ -104,6 +105,7 @@ class GameSessionController extends ChangeNotifier {
|
|||||||
now: _now,
|
now: _now,
|
||||||
cheatsEnabled: cheatsEnabled,
|
cheatsEnabled: cheatsEnabled,
|
||||||
onPlayerDied: _onPlayerDied,
|
onPlayerDied: _onPlayerDied,
|
||||||
|
onGameComplete: _onGameComplete,
|
||||||
availableSpeeds: availableSpeeds,
|
availableSpeeds: availableSpeeds,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -272,6 +274,31 @@ class GameSessionController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 게임 클리어 콜백 (ProgressLoop에서 호출, Act V 완료 시)
|
||||||
|
void _onGameComplete() {
|
||||||
|
_status = GameSessionStatus.complete;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
// Hall of Fame 등록 (비동기)
|
||||||
|
unawaited(_registerToHallOfFame());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 명예의 전당 등록
|
||||||
|
Future<void> _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 회복, 빈 슬롯에 장비 자동 구매
|
/// HP/MP 회복, 빈 슬롯에 장비 자동 구매
|
||||||
@@ -308,4 +335,7 @@ class GameSessionController extends ChangeNotifier {
|
|||||||
/// 사망 상태 여부
|
/// 사망 상태 여부
|
||||||
bool get isDead =>
|
bool get isDead =>
|
||||||
_status == GameSessionStatus.dead || (_state?.isDead ?? false);
|
_status == GameSessionStatus.dead || (_state?.isDead ?? false);
|
||||||
|
|
||||||
|
/// 게임 클리어 여부
|
||||||
|
bool get isComplete => _status == GameSessionStatus.complete;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ class MobileCarouselLayout extends StatefulWidget {
|
|||||||
this.onSfxVolumeChange,
|
this.onSfxVolumeChange,
|
||||||
this.onShowStatistics,
|
this.onShowStatistics,
|
||||||
this.onShowHelp,
|
this.onShowHelp,
|
||||||
|
this.cheatsEnabled = false,
|
||||||
|
this.onCheatTask,
|
||||||
|
this.onCheatQuest,
|
||||||
|
this.onCheatPlot,
|
||||||
});
|
});
|
||||||
|
|
||||||
final GameState state;
|
final GameState state;
|
||||||
@@ -81,6 +85,18 @@ class MobileCarouselLayout extends StatefulWidget {
|
|||||||
/// 도움말 표시 콜백
|
/// 도움말 표시 콜백
|
||||||
final VoidCallback? onShowHelp;
|
final VoidCallback? onShowHelp;
|
||||||
|
|
||||||
|
/// 치트 모드 활성화 여부
|
||||||
|
final bool cheatsEnabled;
|
||||||
|
|
||||||
|
/// 치트: 태스크 완료
|
||||||
|
final VoidCallback? onCheatTask;
|
||||||
|
|
||||||
|
/// 치트: 퀘스트 완료
|
||||||
|
final VoidCallback? onCheatQuest;
|
||||||
|
|
||||||
|
/// 치트: 액트(플롯) 완료
|
||||||
|
final VoidCallback? onCheatPlot;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<MobileCarouselLayout> createState() => _MobileCarouselLayoutState();
|
State<MobileCarouselLayout> createState() => _MobileCarouselLayoutState();
|
||||||
}
|
}
|
||||||
@@ -547,6 +563,52 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// 치트 섹션 (디버그 모드에서만 표시)
|
||||||
|
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),
|
const SizedBox(height: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user