Files
asciinevrdie/lib/src/features/game/game_session_controller.dart
JiWoong Sul c577f9deed refactor(controller): GameSessionController 분할 (920→526 LOC)
- GameStatisticsManager: 세션/누적 통계 추적
- SpeedBoostManager: 광고 배속 부스트 기능
- ReturnRewardsManager: 복귀 보상 기능
- ResurrectionManager: 사망/부활 처리
- HallOfFameManager: 명예의 전당 관리
2026-01-21 17:33:37 +09:00

527 lines
16 KiB
Dart

import 'dart:async';
import 'package:asciineverdie/src/core/engine/progress_loop.dart';
import 'package:asciineverdie/src/core/engine/progress_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/monetization_state.dart';
import 'package:asciineverdie/src/core/model/treasure_chest.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:asciineverdie/src/features/game/managers/game_statistics_manager.dart';
import 'package:asciineverdie/src/features/game/managers/hall_of_fame_manager.dart';
import 'package:asciineverdie/src/features/game/managers/resurrection_manager.dart';
import 'package:asciineverdie/src/features/game/managers/return_rewards_manager.dart';
import 'package:asciineverdie/src/features/game/managers/speed_boost_manager.dart';
import 'package:flutter/foundation.dart';
enum GameSessionStatus { idle, loading, running, error, dead, complete }
/// Presentation-friendly wrapper that owns ProgressLoop and SaveManager.
///
/// 게임 루프 관리를 담당하며, 대부분의 기능은 매니저에 위임합니다.
/// - 통계: GameStatisticsManager
/// - 속도 부스트: SpeedBoostManager
/// - 복귀 보상: ReturnRewardsManager
/// - 부활: ResurrectionManager
/// - 명예의 전당: HallOfFameManager
class GameSessionController extends ChangeNotifier {
GameSessionController({
required this.progressService,
required this.saveManager,
this.autoSaveConfig = const AutoSaveConfig(),
Duration tickInterval = const Duration(milliseconds: 50),
DateTime Function()? now,
StatisticsStorage? statisticsStorage,
HallOfFameStorage? hallOfFameStorage,
}) : _tickInterval = tickInterval,
_now = now ?? DateTime.now {
// 매니저 초기화
_statisticsManager = GameStatisticsManager(
statisticsStorage: statisticsStorage,
);
_hallOfFameManager = HallOfFameManager(
hallOfFameStorage: hallOfFameStorage,
);
_speedBoostManager = SpeedBoostManager(
cheatsEnabledGetter: () => _cheatsEnabled,
getAvailableSpeeds: _hallOfFameManager.getAvailableSpeeds,
);
_returnRewardsManager = ReturnRewardsManager();
_resurrectionManager = ResurrectionManager();
// 매니저 콜백 설정
_speedBoostManager.onStateChanged = notifyListeners;
_returnRewardsManager.onReturnRewardAvailable = (reward) {
onReturnRewardAvailable?.call(reward);
};
}
final ProgressService progressService;
final SaveManager saveManager;
final AutoSaveConfig autoSaveConfig;
final Duration _tickInterval;
final DateTime Function() _now;
// 매니저들
late final GameStatisticsManager _statisticsManager;
late final HallOfFameManager _hallOfFameManager;
late final SpeedBoostManager _speedBoostManager;
late final ReturnRewardsManager _returnRewardsManager;
late final ResurrectionManager _resurrectionManager;
ProgressLoop? _loop;
StreamSubscription<GameState>? _subscription;
bool _cheatsEnabled = false;
GameSessionStatus _status = GameSessionStatus.idle;
GameState? _state;
String? _error;
// 복귀 보상 상태 (Phase 7)
MonetizationState _monetization = MonetizationState.initial();
/// 복귀 보상 콜백 (UI에서 다이얼로그 표시용)
void Function(ReturnChestReward reward)? onReturnRewardAvailable;
// ==========================================================================
// Getters
// ==========================================================================
GameSessionStatus get status => _status;
GameState? get state => _state;
String? get error => _error;
bool get isRunning => _status == GameSessionStatus.running;
bool get cheatsEnabled => _cheatsEnabled;
/// 자동 부활 활성화 여부
bool get autoResurrect => _resurrectionManager.autoResurrect;
/// 자동 부활 설정
void setAutoResurrect(bool value) {
_resurrectionManager.autoResurrect = value;
notifyListeners();
}
/// 현재 세션 통계
SessionStatistics get sessionStats => _statisticsManager.sessionStats;
/// 누적 통계
CumulativeStatistics get cumulativeStats =>
_statisticsManager.cumulativeStats;
/// 현재 ProgressLoop 인스턴스 (치트 기능용)
ProgressLoop? get loop => _loop;
/// 광고 배속 배율 (릴리즈: 5x, 디버그빌드+디버그모드: 20x)
int get adSpeedMultiplier => _speedBoostManager.speedBoostMultiplier;
/// 2x 배속 해금 여부 (명예의 전당에 캐릭터가 있으면 true)
bool get has2xUnlocked => _loop?.availableSpeeds.contains(2) ?? false;
/// 사망 상태 여부
bool get isDead =>
_status == GameSessionStatus.dead || (_state?.isDead ?? false);
/// 게임 클리어 여부
bool get isComplete => _status == GameSessionStatus.complete;
// 속도 부스트 관련 getters (매니저 위임)
bool get isSpeedBoostActive => _speedBoostManager.isSpeedBoostActive;
bool get isShowingAd => _speedBoostManager.isShowingAd;
bool get isRecentlyShowedAd => _speedBoostManager.isRecentlyShowedAd;
int get speedBoostMultiplier => _speedBoostManager.speedBoostMultiplier;
int get speedBoostDuration => _speedBoostManager.speedBoostDuration;
int get speedBoostRemainingSeconds => _speedBoostManager.getRemainingSeconds(
_monetization,
_state?.skillSystem.elapsedMs ?? 0,
);
int get currentSpeedMultiplier =>
_speedBoostManager.getCurrentSpeedMultiplier(_loop);
// 복귀 보상 관련 getters (매니저 위임)
MonetizationState get monetization => _monetization;
ReturnChestReward? get pendingReturnReward =>
_returnRewardsManager.pendingReturnReward;
// ==========================================================================
// 게임 루프 관리
// ==========================================================================
Future<void> startNew(
GameState initialState, {
bool cheatsEnabled = false,
bool isNewGame = true,
}) async {
// 기존 배속 보존 (부활/재개 시 유지)
final previousSpeed =
_loop?.speedMultiplier ?? _speedBoostManager.savedSpeedMultiplier;
await _stopLoop(saveOnStop: false);
// 새 게임인 경우 초기화 (프롤로그 태스크 설정)
final state = isNewGame
? progressService.initializeNewGame(initialState)
: initialState;
_state = state;
_error = null;
_status = GameSessionStatus.running;
_cheatsEnabled = cheatsEnabled;
// 통계 초기화
if (isNewGame) {
await _statisticsManager.initializeForNewGame();
} else {
_statisticsManager.restoreFromLoadedGame(state);
}
_statisticsManager.initPreviousValues(state);
// 명예의 전당 체크 → 기본 가용 배속 결정
final baseAvailableSpeeds = await _hallOfFameManager.getAvailableSpeeds();
final hasHallOfFame = baseAvailableSpeeds.contains(2);
// 기본 배속 결정 (부스트 미적용 시)
final int baseSpeed;
if (isNewGame) {
baseSpeed = hasHallOfFame ? 2 : 1;
} else {
baseSpeed = (hasHallOfFame && previousSpeed < 2) ? 2 : previousSpeed;
}
// 배속 부스트 활성화 상태면 부스트 배속 적용, 아니면 기본 배속
final speedConfig = _speedBoostManager.calculateInitialSpeeds(
baseAvailableSpeeds: baseAvailableSpeeds,
baseSpeed: baseSpeed,
);
_loop = ProgressLoop(
initialState: state,
progressService: progressService,
saveManager: saveManager,
autoSaveConfig: autoSaveConfig,
tickInterval: _tickInterval,
now: _now,
cheatsEnabled: cheatsEnabled,
onPlayerDied: _onPlayerDied,
onGameComplete: _onGameComplete,
availableSpeeds: speedConfig.speeds,
initialSpeedMultiplier: speedConfig.initialSpeed,
);
_subscription = _loop!.stream.listen((next) {
final elapsedMs = next.skillSystem.elapsedMs;
// 버프 만료 체크 (게임 시간 기준)
_checkBuffExpiries(elapsedMs);
_statisticsManager.updateStatistics(next);
_state = next;
notifyListeners();
});
_loop!.start();
notifyListeners();
}
/// 매 틱마다 버프 만료 체크
void _checkBuffExpiries(int elapsedMs) {
// 속도 부스트 만료 체크
final boostEnded = _speedBoostManager.checkExpiry(
elapsedMs: elapsedMs,
monetization: _monetization,
loop: _loop,
);
if (boostEnded) {
_monetization = _monetization.copyWith(speedBoostEndMs: null);
}
// 자동부활 버프 만료 체크
final endMs = _monetization.autoReviveEndMs;
if (endMs != null && elapsedMs >= endMs) {
_monetization = _monetization.copyWith(autoReviveEndMs: null);
debugPrint('[GameSession] Auto-revive buff expired');
notifyListeners();
}
}
/// 누적 통계 로드
Future<void> loadCumulativeStats() async {
await _statisticsManager.loadCumulativeStats();
notifyListeners();
}
/// 세션 통계를 누적 통계에 병합
Future<void> mergeSessionStats() async {
await _statisticsManager.mergeSessionStats();
notifyListeners();
}
Future<void> loadAndStart({String? fileName}) async {
_status = GameSessionStatus.loading;
_error = null;
notifyListeners();
final (outcome, loaded, savedCheatsEnabled, savedMonetization) =
await saveManager.loadState(fileName: fileName);
if (!outcome.success || loaded == null) {
_status = GameSessionStatus.error;
_error = outcome.error ?? 'Unknown error';
notifyListeners();
return;
}
// 저장된 수익화 상태 복원
_monetization = savedMonetization ?? MonetizationState.initial();
// 복귀 보상 체크 (Phase 7)
_returnRewardsManager.checkReturnRewards(
monetization: _monetization,
loaded: loaded,
);
// 저장된 치트 모드 상태 복원
await startNew(loaded, cheatsEnabled: savedCheatsEnabled, isNewGame: false);
}
Future<void> pause({bool saveOnStop = false}) async {
await _stopLoop(saveOnStop: saveOnStop);
_status = GameSessionStatus.idle;
notifyListeners();
}
/// 일시 정지 상태에서 재개
Future<void> resume() async {
if (_state == null || _status != GameSessionStatus.idle) return;
await startNew(_state!, cheatsEnabled: _cheatsEnabled, isNewGame: false);
}
/// 일시 정지/재개 토글
Future<void> togglePause() async {
if (isRunning) {
await pause(saveOnStop: true);
} else if (_state != null && _status == GameSessionStatus.idle) {
await resume();
}
}
@override
void dispose() {
final stop = _stopLoop(saveOnStop: false);
if (stop != null) {
unawaited(stop);
}
super.dispose();
}
Future<void>? _stopLoop({required bool saveOnStop}) {
final loop = _loop;
final sub = _subscription;
// 배속 저장 (resume 시 복원용)
if (loop != null) {
_speedBoostManager.savedSpeedMultiplier = loop.speedMultiplier;
}
_loop = null;
_subscription = null;
sub?.cancel();
if (loop == null) return null;
return loop.stop(saveOnStop: saveOnStop);
}
// ==========================================================================
// 사망/부활 처리
// ==========================================================================
/// 플레이어 사망 콜백 (ProgressLoop에서 호출)
void _onPlayerDied() {
_statisticsManager.recordDeath();
_status = GameSessionStatus.dead;
notifyListeners();
// 자동 부활 조건 확인
final elapsedMs = _state?.skillSystem.elapsedMs ?? 0;
final shouldAutoResurrect = _resurrectionManager.shouldAutoResurrect(
monetization: _monetization,
elapsedMs: elapsedMs,
);
if (shouldAutoResurrect) {
_scheduleAutoResurrect();
}
}
/// 자동 부활 예약 (Auto-Resurrection Scheduler)
void _scheduleAutoResurrect() {
Future.delayed(const Duration(milliseconds: 800), () async {
if (_status != GameSessionStatus.dead) return;
// 자동 부활 조건 재확인
final elapsedMs = _state?.skillSystem.elapsedMs ?? 0;
final shouldAutoResurrect = _resurrectionManager.shouldAutoResurrect(
monetization: _monetization,
elapsedMs: elapsedMs,
);
if (shouldAutoResurrect) {
await resurrect();
await resumeAfterResurrection();
}
});
}
/// 플레이어 부활 처리 (상태만 업데이트, 게임 재개는 별도로)
Future<void> resurrect() async {
if (_state == null || !_state!.isDead) return;
final resurrectedState = await _resurrectionManager.processResurrection(
state: _state!,
saveManager: saveManager,
cheatsEnabled: _cheatsEnabled,
monetization: _monetization,
);
if (resurrectedState != null) {
_state = resurrectedState;
_status = GameSessionStatus.idle;
notifyListeners();
}
}
/// 부활 후 게임 재개
Future<void> resumeAfterResurrection() async {
if (_state == null) return;
await startNew(_state!, cheatsEnabled: _cheatsEnabled, isNewGame: false);
}
/// 광고 부활 (HP 100% + 아이템 복구 + 10분 자동부활 버프)
Future<void> adRevive() async {
if (_state == null || !_state!.isDead) return;
final result = await _resurrectionManager.processAdRevive(
state: _state!,
saveManager: saveManager,
cheatsEnabled: _cheatsEnabled,
monetization: _monetization,
);
if (result.success) {
_state = result.state;
_monetization = result.monetization;
_status = GameSessionStatus.idle;
notifyListeners();
}
}
// ==========================================================================
// 명예의 전당
// ==========================================================================
/// 게임 클리어 콜백 (ProgressLoop에서 호출, Act V 완료 시)
void _onGameComplete() {
_status = GameSessionStatus.complete;
notifyListeners();
unawaited(_registerToHallOfFame());
}
/// 명예의 전당 등록
Future<void> _registerToHallOfFame() async {
if (_state == null) {
debugPrint('[HallOfFame] _state is null, skipping registration');
return;
}
await _hallOfFameManager.registerCharacter(
state: _state!,
saveManager: saveManager,
statisticsManager: _statisticsManager,
);
}
/// 테스트 캐릭터 생성 (디버그 모드 전용)
Future<bool> createTestCharacter() async {
if (_state == null) {
debugPrint('[TestCharacter] _state is null');
return false;
}
await _stopLoop(saveOnStop: false);
final success = await _hallOfFameManager.createTestCharacter(
state: _state!,
saveManager: saveManager,
);
if (success) {
_state = null;
_status = GameSessionStatus.idle;
notifyListeners();
}
return success;
}
// ==========================================================================
// 속도 부스트 (Phase 6)
// ==========================================================================
/// 속도 부스트 활성화 (광고 시청 후)
Future<bool> activateSpeedBoost() async {
final (success, updatedMonetization) =
await _speedBoostManager.activateSpeedBoost(
loop: _loop,
monetization: _monetization,
currentElapsedMs: _state?.skillSystem.elapsedMs ?? 0,
);
if (success) {
_monetization = updatedMonetization;
notifyListeners();
}
return success;
}
/// 속도 부스트 수동 취소
void cancelSpeedBoost() {
_monetization = _speedBoostManager.cancelSpeedBoost(
loop: _loop,
monetization: _monetization,
);
notifyListeners();
}
// ==========================================================================
// 복귀 보상 (Phase 7)
// ==========================================================================
/// 복귀 보상 수령 완료 (상자 보상 적용)
Future<void> applyReturnReward(List<ChestReward> rewards) async {
if (_state == null) return;
final updatedState = await _returnRewardsManager.applyReturnReward(
rewards: rewards,
state: _state!,
loop: _loop,
saveManager: saveManager,
cheatsEnabled: _cheatsEnabled,
monetization: _monetization,
);
if (updatedState != null) {
_state = updatedState;
notifyListeners();
}
}
/// 복귀 보상 건너뛰기
void skipReturnReward() {
_returnRewardsManager.skipReturnReward();
}
}