Files
asciinevrdie/lib/src/features/game/game_session_controller.dart
JiWoong Sul c9f0e35914 fix(i18n): VictoryOverlay 번역 및 개선
- 일본어/중국어 번역 수정
- game_text_l10n 번역 데이터 정리
- VictoryOverlay 레이아웃 개선
- GameSessionController 상태 관리 개선
2026-01-01 03:29:48 +09:00

371 lines
12 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/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;
// 통계 관련 필드
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;
/// 현재 세션 통계
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 {
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();
_loop = ProgressLoop(
initialState: state,
progressService: progressService,
saveManager: saveManager,
autoSaveConfig: autoSaveConfig,
tickInterval: _tickInterval,
now: _now,
cheatsEnabled: cheatsEnabled,
onPlayerDied: _onPlayerDied,
onGameComplete: _onGameComplete,
availableSpeeds: availableSpeeds,
);
_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,
bool cheatsEnabled = false,
}) async {
_status = GameSessionStatus.loading;
_error = null;
notifyListeners();
final (outcome, loaded) = await saveManager.loadState(fileName: fileName);
if (!outcome.success || loaded == null) {
_status = GameSessionStatus.error;
_error = outcome.error ?? 'Unknown error';
notifyListeners();
return;
}
await startNew(loaded, cheatsEnabled: cheatsEnabled, 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();
}
/// 게임 클리어 콜백 (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');
}
}
/// 플레이어 부활 처리 (상태만 업데이트, 게임 재개는 별도로)
///
/// 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);
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;
}