- GameStatisticsManager: 세션/누적 통계 추적 - SpeedBoostManager: 광고 배속 부스트 기능 - ReturnRewardsManager: 복귀 보상 기능 - ResurrectionManager: 사망/부활 처리 - HallOfFameManager: 명예의 전당 관리
527 lines
16 KiB
Dart
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();
|
|
}
|
|
}
|