- GamePlayScreen 개선 - GameSessionController 확장 - MobileCarouselLayout 기능 추가 - SettingsScreen 테스트 기능 추가
466 lines
15 KiB
Dart
466 lines
15 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/engine/resurrection_service.dart';
|
|
import 'package:asciineverdie/src/core/engine/shop_service.dart';
|
|
import 'package:asciineverdie/src/core/engine/test_character_service.dart';
|
|
import 'package:asciineverdie/src/core/model/combat_stats.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, complete }
|
|
|
|
/// Presentation-friendly wrapper that owns ProgressLoop and SaveManager.
|
|
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,
|
|
_statisticsStorage = statisticsStorage ?? StatisticsStorage(),
|
|
_hallOfFameStorage = hallOfFameStorage ?? HallOfFameStorage();
|
|
|
|
final ProgressService progressService;
|
|
final SaveManager saveManager;
|
|
final AutoSaveConfig autoSaveConfig;
|
|
final StatisticsStorage _statisticsStorage;
|
|
final HallOfFameStorage _hallOfFameStorage;
|
|
|
|
final Duration _tickInterval;
|
|
final DateTime Function() _now;
|
|
|
|
ProgressLoop? _loop;
|
|
StreamSubscription<GameState>? _subscription;
|
|
bool _cheatsEnabled = false;
|
|
|
|
GameSessionStatus _status = GameSessionStatus.idle;
|
|
GameState? _state;
|
|
String? _error;
|
|
|
|
// 자동 부활 (Auto-Resurrection) 상태
|
|
bool _autoResurrect = false;
|
|
|
|
// 통계 관련 필드
|
|
SessionStatistics _sessionStats = SessionStatistics.empty();
|
|
CumulativeStatistics _cumulativeStats = CumulativeStatistics.empty();
|
|
int _previousLevel = 0;
|
|
int _previousGold = 0;
|
|
int _previousMonstersKilled = 0;
|
|
int _previousQuestsCompleted = 0;
|
|
|
|
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 => _autoResurrect;
|
|
|
|
/// 자동 부활 설정
|
|
void setAutoResurrect(bool value) {
|
|
_autoResurrect = value;
|
|
notifyListeners();
|
|
}
|
|
|
|
/// 현재 세션 통계
|
|
SessionStatistics get sessionStats => _sessionStats;
|
|
|
|
/// 누적 통계
|
|
CumulativeStatistics get cumulativeStats => _cumulativeStats;
|
|
|
|
/// 현재 ProgressLoop 인스턴스 (치트 기능용)
|
|
ProgressLoop? get loop => _loop;
|
|
|
|
Future<void> startNew(
|
|
GameState initialState, {
|
|
bool cheatsEnabled = false,
|
|
bool isNewGame = true,
|
|
}) async {
|
|
// 기존 배속 보존 (부활/재개 시 유지)
|
|
final previousSpeed = _loop?.speedMultiplier ?? 1;
|
|
|
|
await _stopLoop(saveOnStop: false);
|
|
|
|
// 새 게임인 경우 초기화 (프롤로그 태스크 설정)
|
|
final state = isNewGame
|
|
? progressService.initializeNewGame(initialState)
|
|
: initialState;
|
|
|
|
_state = state;
|
|
_error = null;
|
|
_status = GameSessionStatus.running;
|
|
_cheatsEnabled = cheatsEnabled;
|
|
|
|
// 통계 초기화
|
|
if (isNewGame) {
|
|
_sessionStats = SessionStatistics.empty();
|
|
await _statisticsStorage.recordGameStart();
|
|
}
|
|
_initPreviousValues(state);
|
|
|
|
// 명예의 전당 체크 → 가용 배속 결정
|
|
final availableSpeeds = await _getAvailableSpeeds();
|
|
|
|
// 새 게임이면 1배속, 재개/부활이면 기존 배속 유지
|
|
final initialSpeed = isNewGame ? 1 : previousSpeed;
|
|
|
|
_loop = ProgressLoop(
|
|
initialState: state,
|
|
progressService: progressService,
|
|
saveManager: saveManager,
|
|
autoSaveConfig: autoSaveConfig,
|
|
tickInterval: _tickInterval,
|
|
now: _now,
|
|
cheatsEnabled: cheatsEnabled,
|
|
onPlayerDied: _onPlayerDied,
|
|
onGameComplete: _onGameComplete,
|
|
availableSpeeds: availableSpeeds,
|
|
initialSpeedMultiplier: initialSpeed,
|
|
);
|
|
|
|
_subscription = _loop!.stream.listen((next) {
|
|
_updateStatistics(next);
|
|
_state = next;
|
|
notifyListeners();
|
|
});
|
|
|
|
_loop!.start();
|
|
notifyListeners();
|
|
}
|
|
|
|
/// 명예의 전당 상태에 따른 가용 배속 목록 반환
|
|
/// - 디버그 모드(치트 활성화): [1, 5, 20] (터보 모드)
|
|
/// - 명예의 전당에 캐릭터 없음: [1, 5]
|
|
/// - 명예의 전당에 캐릭터 있음: [1, 2, 5]
|
|
Future<List<int>> _getAvailableSpeeds() async {
|
|
// 디버그 모드면 터보(20x) 추가
|
|
if (_cheatsEnabled) {
|
|
return [1, 5, 20];
|
|
}
|
|
|
|
final hallOfFame = await _hallOfFameStorage.load();
|
|
return hallOfFame.isEmpty ? [1, 5] : [1, 2, 5];
|
|
}
|
|
|
|
/// 이전 값 초기화 (통계 변화 추적용)
|
|
void _initPreviousValues(GameState state) {
|
|
_previousLevel = state.traits.level;
|
|
_previousGold = state.inventory.gold;
|
|
_previousMonstersKilled = state.progress.monstersKilled;
|
|
_previousQuestsCompleted = state.progress.questCount;
|
|
}
|
|
|
|
/// 상태 변화에 따른 통계 업데이트
|
|
void _updateStatistics(GameState next) {
|
|
// 플레이 시간 업데이트
|
|
_sessionStats = _sessionStats.updatePlayTime(next.skillSystem.elapsedMs);
|
|
|
|
// 레벨업 감지
|
|
if (next.traits.level > _previousLevel) {
|
|
final levelUps = next.traits.level - _previousLevel;
|
|
for (var i = 0; i < levelUps; i++) {
|
|
_sessionStats = _sessionStats.recordLevelUp();
|
|
}
|
|
_previousLevel = next.traits.level;
|
|
|
|
// 최고 레벨 업데이트
|
|
unawaited(_statisticsStorage.updateHighestLevel(next.traits.level));
|
|
}
|
|
|
|
// 골드 변화 감지
|
|
if (next.inventory.gold > _previousGold) {
|
|
final earned = next.inventory.gold - _previousGold;
|
|
_sessionStats = _sessionStats.recordGoldEarned(earned);
|
|
|
|
// 최대 골드 업데이트
|
|
unawaited(_statisticsStorage.updateHighestGold(next.inventory.gold));
|
|
} else if (next.inventory.gold < _previousGold) {
|
|
final spent = _previousGold - next.inventory.gold;
|
|
_sessionStats = _sessionStats.recordGoldSpent(spent);
|
|
}
|
|
_previousGold = next.inventory.gold;
|
|
|
|
// 몬스터 처치 감지
|
|
if (next.progress.monstersKilled > _previousMonstersKilled) {
|
|
final kills = next.progress.monstersKilled - _previousMonstersKilled;
|
|
for (var i = 0; i < kills; i++) {
|
|
_sessionStats = _sessionStats.recordKill();
|
|
}
|
|
_previousMonstersKilled = next.progress.monstersKilled;
|
|
}
|
|
|
|
// 퀘스트 완료 감지
|
|
if (next.progress.questCount > _previousQuestsCompleted) {
|
|
final quests = next.progress.questCount - _previousQuestsCompleted;
|
|
for (var i = 0; i < quests; i++) {
|
|
_sessionStats = _sessionStats.recordQuestComplete();
|
|
}
|
|
_previousQuestsCompleted = next.progress.questCount;
|
|
}
|
|
}
|
|
|
|
/// 누적 통계 로드
|
|
Future<void> loadCumulativeStats() async {
|
|
_cumulativeStats = await _statisticsStorage.loadCumulative();
|
|
notifyListeners();
|
|
}
|
|
|
|
/// 세션 통계를 누적 통계에 병합
|
|
Future<void> mergeSessionStats() async {
|
|
await _statisticsStorage.mergeSession(_sessionStats);
|
|
_cumulativeStats = await _statisticsStorage.loadCumulative();
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> loadAndStart({String? fileName}) async {
|
|
_status = GameSessionStatus.loading;
|
|
_error = null;
|
|
notifyListeners();
|
|
|
|
final (outcome, loaded, savedCheatsEnabled) = await saveManager.loadState(
|
|
fileName: fileName,
|
|
);
|
|
if (!outcome.success || loaded == null) {
|
|
_status = GameSessionStatus.error;
|
|
_error = outcome.error ?? 'Unknown error';
|
|
notifyListeners();
|
|
return;
|
|
}
|
|
|
|
// 저장된 치트 모드 상태 복원
|
|
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;
|
|
_loop = null;
|
|
_subscription = null;
|
|
|
|
sub?.cancel();
|
|
if (loop == null) return null;
|
|
return loop.stop(saveOnStop: saveOnStop);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Phase 4: 사망/부활 처리
|
|
// ============================================================================
|
|
|
|
/// 플레이어 사망 콜백 (ProgressLoop에서 호출)
|
|
void _onPlayerDied() {
|
|
_sessionStats = _sessionStats.recordDeath();
|
|
_status = GameSessionStatus.dead;
|
|
notifyListeners();
|
|
|
|
// 자동 부활이 활성화된 경우 잠시 후 자동으로 부활
|
|
if (_autoResurrect) {
|
|
_scheduleAutoResurrect();
|
|
}
|
|
}
|
|
|
|
/// 자동 부활 예약 (Auto-Resurrection Scheduler)
|
|
///
|
|
/// 사망 오버레이를 잠시 표시한 후 자동으로 부활 처리
|
|
void _scheduleAutoResurrect() {
|
|
Future.delayed(const Duration(milliseconds: 800), () async {
|
|
// 상태가 여전히 dead이고, 자동 부활이 활성화된 경우에만 부활
|
|
if (_status == GameSessionStatus.dead && _autoResurrect) {
|
|
await resurrect();
|
|
await resumeAfterResurrection();
|
|
}
|
|
});
|
|
}
|
|
|
|
/// 게임 클리어 콜백 (ProgressLoop에서 호출, Act V 완료 시)
|
|
void _onGameComplete() {
|
|
_status = GameSessionStatus.complete;
|
|
notifyListeners();
|
|
|
|
// Hall of Fame 등록 (비동기)
|
|
unawaited(_registerToHallOfFame());
|
|
}
|
|
|
|
/// 명예의 전당 등록
|
|
Future<void> _registerToHallOfFame() async {
|
|
if (_state == null) {
|
|
debugPrint('[HallOfFame] _state is null, skipping registration');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
debugPrint('[HallOfFame] Starting registration...');
|
|
|
|
// 최종 전투 스탯 계산 (CombatStats)
|
|
final combatStats = CombatStats.fromStats(
|
|
stats: _state!.stats,
|
|
equipment: _state!.equipment,
|
|
level: _state!.traits.level,
|
|
);
|
|
|
|
final entry = HallOfFameEntry.fromGameState(
|
|
state: _state!,
|
|
totalDeaths: _sessionStats.deathCount,
|
|
monstersKilled: _state!.progress.monstersKilled,
|
|
combatStats: combatStats,
|
|
);
|
|
|
|
debugPrint(
|
|
'[HallOfFame] Entry created: ${entry.characterName} Lv.${entry.level}',
|
|
);
|
|
|
|
final success = await _hallOfFameStorage.addEntry(entry);
|
|
debugPrint('[HallOfFame] Storage save result: $success');
|
|
|
|
// 통계 기록
|
|
await _statisticsStorage.recordGameComplete();
|
|
debugPrint('[HallOfFame] Registration complete');
|
|
|
|
// 클리어된 세이브 파일 삭제 (중복 등록 방지)
|
|
if (success) {
|
|
final deleteResult = await saveManager.deleteSave();
|
|
debugPrint('[HallOfFame] Save file deleted: ${deleteResult.success}');
|
|
}
|
|
} catch (e, st) {
|
|
debugPrint('[HallOfFame] ERROR: $e');
|
|
debugPrint('[HallOfFame] StackTrace: $st');
|
|
}
|
|
}
|
|
|
|
/// 테스트 캐릭터 생성 (디버그 모드 전용)
|
|
///
|
|
/// 현재 캐릭터를 레벨 100, 고급 장비, 다수의 스킬을 가진
|
|
/// 캐릭터로 변환하여 명예의 전당에 등록하고 세이브를 삭제함.
|
|
Future<bool> createTestCharacter() async {
|
|
if (_state == null) {
|
|
debugPrint('[TestCharacter] _state is null');
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
debugPrint('[TestCharacter] Creating test character...');
|
|
|
|
// 게임 일시정지
|
|
await _stopLoop(saveOnStop: false);
|
|
|
|
// TestCharacterService로 테스트 캐릭터 생성
|
|
final testService = TestCharacterService(
|
|
config: progressService.config,
|
|
rng: _state!.rng,
|
|
);
|
|
|
|
final entry = testService.createTestCharacter(_state!);
|
|
|
|
debugPrint(
|
|
'[TestCharacter] Entry created: ${entry.characterName} Lv.${entry.level}',
|
|
);
|
|
|
|
// 명예의 전당에 등록
|
|
final success = await _hallOfFameStorage.addEntry(entry);
|
|
debugPrint('[TestCharacter] HallOfFame save result: $success');
|
|
|
|
if (success) {
|
|
// 세이브 파일 삭제
|
|
final deleteResult = await saveManager.deleteSave();
|
|
debugPrint('[TestCharacter] Save deleted: ${deleteResult.success}');
|
|
}
|
|
|
|
// 상태 초기화
|
|
_state = null;
|
|
_status = GameSessionStatus.idle;
|
|
notifyListeners();
|
|
|
|
debugPrint('[TestCharacter] Complete');
|
|
return success;
|
|
} catch (e, st) {
|
|
debugPrint('[TestCharacter] ERROR: $e');
|
|
debugPrint('[TestCharacter] StackTrace: $st');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// 플레이어 부활 처리 (상태만 업데이트, 게임 재개는 별도로)
|
|
///
|
|
/// HP/MP 회복, 빈 슬롯에 장비 자동 구매
|
|
/// 게임 재개는 resumeAfterResurrection()으로 별도 호출 필요
|
|
Future<void> resurrect() async {
|
|
if (_state == null || !_state!.isDead) return;
|
|
|
|
// ResurrectionService를 사용하여 부활 처리
|
|
final shopService = ShopService(rng: _state!.rng);
|
|
final resurrectionService = ResurrectionService(shopService: shopService);
|
|
|
|
final resurrectedState = resurrectionService.processResurrection(_state!);
|
|
|
|
// 상태 업데이트 (게임 재개 없이)
|
|
_state = resurrectedState;
|
|
_status = GameSessionStatus.idle; // 사망 상태 해제
|
|
|
|
// 저장 (치트 모드 상태 유지)
|
|
await saveManager.saveState(
|
|
resurrectedState,
|
|
cheatsEnabled: _cheatsEnabled,
|
|
);
|
|
|
|
notifyListeners();
|
|
}
|
|
|
|
/// 부활 후 게임 재개
|
|
///
|
|
/// resurrect() 호출 후 애니메이션이 끝난 뒤 호출
|
|
Future<void> resumeAfterResurrection() async {
|
|
if (_state == null) return;
|
|
|
|
// 게임 재개
|
|
await startNew(_state!, cheatsEnabled: _cheatsEnabled, isNewGame: false);
|
|
}
|
|
|
|
/// 사망 상태 여부
|
|
bool get isDead =>
|
|
_status == GameSessionStatus.dead || (_state?.isDead ?? false);
|
|
|
|
/// 게임 클리어 여부
|
|
bool get isComplete => _status == GameSessionStatus.complete;
|
|
}
|