Files
asciinevrdie/lib/src/features/game/game_session_controller.dart
JiWoong Sul d90543dd86 fix(speed): 배속 관련 버그 수정
- 광고 후 배속 적용 안됨: isShowingAd 플래그로 lifecycle reload 방지
- 배속 종료 후 복귀 안됨: setSpeed(_savedSpeedMultiplier) 추가
- 복귀 상자 장비 장착 안됨: _loop?.replaceState() 추가
- 세이브 로드 시 1배속 고정: 명예의 전당 해금 시 최소 2배속 보장
2026-01-19 19:39:32 +09:00

868 lines
29 KiB
Dart

import 'dart:async';
import 'package:asciineverdie/src/core/engine/ad_service.dart';
import 'package:asciineverdie/src/core/engine/debug_settings_service.dart';
import 'package:asciineverdie/src/core/engine/iap_service.dart';
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/return_rewards_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/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: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;
// 배속 저장 (pause/resume 시 유지)
int _savedSpeedMultiplier = 1;
// 자동 부활 (Auto-Resurrection) 상태
bool _autoResurrect = false;
// 속도 부스트 상태 (Phase 6)
bool _isSpeedBoostActive = false;
Timer? _speedBoostTimer;
int _speedBoostRemainingSeconds = 0;
static const int _speedBoostDuration = 300; // 5분
// 광고 표시 중 플래그 (lifecycle reload 방지용)
bool _isShowingAd = false;
/// 광고 배속 배율 (릴리즈: 5x, 디버그빌드+디버그모드: 20x)
int get _speedBoostMultiplier => (kDebugMode && _cheatsEnabled) ? 20 : 5;
// 복귀 보상 상태 (Phase 7)
MonetizationState _monetization = MonetizationState.initial();
ReturnChestReward? _pendingReturnReward;
/// 복귀 보상 콜백 (UI에서 다이얼로그 표시용)
void Function(ReturnChestReward reward)? onReturnRewardAvailable;
// 통계 관련 필드
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;
/// 광고 배속 배율 (릴리즈: 5x, 디버그빌드+디버그모드: 20x)
int get adSpeedMultiplier => _speedBoostMultiplier;
/// 2x 배속 해금 여부 (명예의 전당에 캐릭터가 있으면 true)
bool get has2xUnlocked => _loop?.availableSpeeds.contains(2) ?? false;
Future<void> startNew(
GameState initialState, {
bool cheatsEnabled = false,
bool isNewGame = true,
}) async {
// 기존 배속 보존 (부활/재개 시 유지)
// _loop가 있으면 현재 배속 사용, 없으면 저장된 배속 사용
final previousSpeed = _loop?.speedMultiplier ?? _savedSpeedMultiplier;
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();
} else {
// 게임 로드 시 저장된 사망 횟수 복원
_sessionStats = _sessionStats.copyWith(
deathCount: state.progress.deathCount,
questsCompleted: state.progress.questCount,
monstersKilled: state.progress.monstersKilled,
playTimeMs: state.skillSystem.elapsedMs,
);
}
_initPreviousValues(state);
// 명예의 전당 체크 → 가용 배속 결정
final availableSpeeds = await _getAvailableSpeeds();
// 명예의 전당 해금 시 기본 2배속, 아니면 1배속
final hasHallOfFame = availableSpeeds.contains(2);
// 새 게임이면 기본 배속, 세이브 로드 시 명예의 전당 해금 시 최소 2배속 보장
final int initialSpeed;
if (isNewGame) {
initialSpeed = hasHallOfFame ? 2 : 1;
} else {
// 세이브 로드: 명예의 전당 해금 시 최소 2배속
initialSpeed = (hasHallOfFame && previousSpeed < 2) ? 2 : 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] (1x만)
/// - 명예의 전당에 캐릭터 있으면: [1, 2] (2x 해금)
/// - 광고 배속(5x/20x)은 별도 버프로만 활성화
Future<List<int>> _getAvailableSpeeds() async {
final hallOfFame = await _hallOfFameStorage.load();
if (hallOfFame.entries.isNotEmpty) {
return [1, 2]; // 명예의 전당 캐릭터 있으면 2x 해금
}
return [1]; // 기본: 1x만
}
/// 이전 값 초기화 (통계 변화 추적용)
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, 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)
_checkReturnRewards(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) {
_savedSpeedMultiplier = loop.speedMultiplier;
}
_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();
// 자동 부활 조건 확인:
// 1. 수동 토글 자동부활 (_autoResurrect)
// 2. 유료 유저 (항상 자동부활)
// 3. 광고 부활 버프 활성 (10분간)
final elapsedMs = _state?.skillSystem.elapsedMs ?? 0;
final shouldAutoResurrect = _autoResurrect ||
IAPService.instance.isAdRemovalPurchased ||
_monetization.isAutoReviveActive(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 = _autoResurrect ||
IAPService.instance.isAdRemovalPurchased ||
_monetization.isAutoReviveActive(elapsedMs);
if (shouldAutoResurrect) {
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: _state!.progress.deathCount, // GameState에 저장된 값 사용
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(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,
monetization: _monetization,
);
notifyListeners();
}
/// 부활 후 게임 재개
///
/// resurrect() 호출 후 애니메이션이 끝난 뒤 호출
Future<void> resumeAfterResurrection() async {
if (_state == null) return;
// 게임 재개
await startNew(_state!, cheatsEnabled: _cheatsEnabled, isNewGame: false);
}
// ===========================================================================
// 광고 부활 (HP 100% + 아이템 복구 + 10분 자동부활)
// ===========================================================================
/// 광고 부활 (HP 100% + 아이템 복구 + 10분 자동부활 버프)
///
/// 유료 유저: 광고 없이 부활
/// 무료 유저: 리워드 광고 시청 후 부활
Future<void> adRevive() async {
if (_state == null || !_state!.isDead) return;
final shopService = ShopService(rng: _state!.rng);
final resurrectionService = ResurrectionService(shopService: shopService);
// 부활 처리 함수
void processRevive() {
_state = resurrectionService.processAdRevive(_state!);
_status = GameSessionStatus.idle;
// 10분 자동부활 버프 활성화 (elapsedMs 기준)
final buffEndMs = _state!.skillSystem.elapsedMs + 600000; // 10분 = 600,000ms
_monetization = _monetization.copyWith(
autoReviveEndMs: buffEndMs,
);
debugPrint('[GameSession] Ad revive complete, auto-revive buff until $buffEndMs ms');
}
// 유료 유저는 광고 없이 부활
if (IAPService.instance.isAdRemovalPurchased) {
processRevive();
await saveManager.saveState(
_state!,
cheatsEnabled: _cheatsEnabled,
monetization: _monetization,
);
notifyListeners();
debugPrint('[GameSession] Ad revive (paid user)');
return;
}
// 무료 유저는 리워드 광고 필요
final adResult = await AdService.instance.showRewardedAd(
adType: AdType.rewardRevive,
onRewarded: processRevive,
);
if (adResult == AdResult.completed || adResult == AdResult.debugSkipped) {
await saveManager.saveState(
_state!,
cheatsEnabled: _cheatsEnabled,
monetization: _monetization,
);
notifyListeners();
debugPrint('[GameSession] Ad revive (free user with ad)');
} else {
debugPrint('[GameSession] Ad revive failed: $adResult');
}
}
/// 사망 상태 여부
bool get isDead =>
_status == GameSessionStatus.dead || (_state?.isDead ?? false);
/// 게임 클리어 여부
bool get isComplete => _status == GameSessionStatus.complete;
// ===========================================================================
// 속도 부스트 (Phase 6)
// ===========================================================================
/// 속도 부스트 활성화 여부
bool get isSpeedBoostActive => _isSpeedBoostActive;
/// 광고 표시 중 여부 (lifecycle reload 방지용)
bool get isShowingAd => _isShowingAd;
/// 속도 부스트 남은 시간 (초)
int get speedBoostRemainingSeconds => _speedBoostRemainingSeconds;
/// 속도 부스트 배율
int get speedBoostMultiplier => _speedBoostMultiplier;
/// 속도 부스트 지속 시간 (초)
int get speedBoostDuration => _speedBoostDuration;
/// 현재 실제 배속 (부스트 적용 포함)
int get currentSpeedMultiplier {
if (_isSpeedBoostActive) return _speedBoostMultiplier;
return _loop?.speedMultiplier ?? _savedSpeedMultiplier;
}
/// 속도 부스트 활성화 (광고 시청 후)
///
/// 유료 유저: 무료 활성화
/// 무료 유저: 인터스티셜 광고 시청 후 활성화
/// Returns: 활성화 성공 여부
Future<bool> activateSpeedBoost() async {
if (_isSpeedBoostActive) return false; // 이미 활성화됨
if (_loop == null) return false;
// 유료 유저는 무료 활성화
if (IAPService.instance.isAdRemovalPurchased) {
_startSpeedBoost();
debugPrint('[GameSession] Speed boost activated (paid user)');
return true;
}
// 무료 유저는 인터스티셜 광고 필요
bool activated = false;
_isShowingAd = true; // 광고 표시 시작 (lifecycle reload 방지)
final adResult = await AdService.instance.showInterstitialAd(
adType: AdType.interstitialSpeed,
onComplete: () {
_startSpeedBoost();
activated = true;
},
);
_isShowingAd = false; // 광고 표시 종료
if (adResult == AdResult.completed || adResult == AdResult.debugSkipped) {
debugPrint('[GameSession] Speed boost activated (free user with ad)');
return activated;
}
debugPrint('[GameSession] Speed boost activation failed: $adResult');
return false;
}
/// 속도 부스트 시작 (내부)
void _startSpeedBoost() {
if (_loop == null) return;
// 현재 배속 저장
_savedSpeedMultiplier = _loop!.speedMultiplier;
// 부스트 배속 적용
_isSpeedBoostActive = true;
_speedBoostRemainingSeconds = _speedBoostDuration;
// monetization 상태에 종료 시점 저장 (UI 표시용)
final currentElapsedMs = _state?.skillSystem.elapsedMs ?? 0;
final endMs = currentElapsedMs + (_speedBoostDuration * 1000);
_monetization = _monetization.copyWith(speedBoostEndMs: endMs);
// ProgressLoop에 직접 배속 설정
_loop!.updateAvailableSpeeds([_speedBoostMultiplier]);
// 1초마다 남은 시간 감소
_speedBoostTimer?.cancel();
_speedBoostTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
_speedBoostRemainingSeconds--;
notifyListeners();
if (_speedBoostRemainingSeconds <= 0) {
_endSpeedBoost();
}
});
notifyListeners();
}
/// 속도 부스트 종료 (내부)
void _endSpeedBoost() {
_speedBoostTimer?.cancel();
_speedBoostTimer = null;
_isSpeedBoostActive = false;
_speedBoostRemainingSeconds = 0;
// monetization 상태 초기화 (UI 표시 제거)
_monetization = _monetization.copyWith(speedBoostEndMs: null);
// 원래 배속 복원
if (_loop != null) {
_getAvailableSpeeds().then((speeds) {
_loop!.updateAvailableSpeeds(speeds);
_loop!.setSpeed(_savedSpeedMultiplier);
});
}
notifyListeners();
debugPrint('[GameSession] Speed boost ended');
}
/// 속도 부스트 수동 취소
void cancelSpeedBoost() {
if (_isSpeedBoostActive) {
_endSpeedBoost();
}
}
// ===========================================================================
// 복귀 보상 (Phase 7)
// ===========================================================================
/// 현재 수익화 상태
MonetizationState get monetization => _monetization;
/// 대기 중인 복귀 보상
ReturnChestReward? get pendingReturnReward => _pendingReturnReward;
/// 복귀 보상 체크 (로드 시 호출)
void _checkReturnRewards(GameState loaded) {
final rewardsService = ReturnRewardsService.instance;
final debugSettings = DebugSettingsService.instance;
// 디버그 모드: 오프라인 시간 시뮬레이션 적용
final lastPlayTime = debugSettings.getSimulatedLastPlayTime(
_monetization.lastPlayTime,
);
final reward = rewardsService.calculateReward(
lastPlayTime: lastPlayTime,
currentTime: DateTime.now(),
isPaidUser: _monetization.isPaidUser,
);
if (reward.hasReward) {
_pendingReturnReward = reward;
debugPrint('[ReturnRewards] Reward available: ${reward.chestCount} chests, '
'${reward.hoursAway} hours away');
// UI에서 다이얼로그 표시를 위해 콜백 호출
// startNew 후에 호출하도록 딜레이
Future<void>.delayed(const Duration(milliseconds: 500), () {
if (_pendingReturnReward != null) {
onReturnRewardAvailable?.call(_pendingReturnReward!);
}
});
}
}
/// 복귀 보상 수령 완료 (상자 보상 적용)
///
/// [rewards] 오픈된 상자 보상 목록
void applyReturnReward(List<ChestReward> rewards) {
if (_state == null) return;
if (rewards.isEmpty) {
// 보상 없이 건너뛴 경우
_pendingReturnReward = null;
debugPrint('[ReturnRewards] Reward skipped');
return;
}
var updatedState = _state!;
// 보상 적용
for (final reward in rewards) {
switch (reward.type) {
case ChestRewardType.equipment:
if (reward.equipment != null) {
// 현재 장비와 비교하여 더 좋으면 자동 장착
final slotIndex = reward.equipment!.slot.index;
final currentItem = updatedState.equipment.getItemByIndex(slotIndex);
if (currentItem.isEmpty ||
reward.equipment!.itemWeight > currentItem.itemWeight) {
updatedState = updatedState.copyWith(
equipment: updatedState.equipment.setItemByIndex(
slotIndex,
reward.equipment!,
),
);
debugPrint('[ReturnRewards] Equipped: ${reward.equipment!.name}');
} else {
// 더 좋지 않으면 판매 (골드로 변환)
final sellPrice =
(reward.equipment!.level * 50 * 0.3).round().clamp(1, 99999);
updatedState = updatedState.copyWith(
inventory: updatedState.inventory.copyWith(
gold: updatedState.inventory.gold + sellPrice,
),
);
debugPrint('[ReturnRewards] Sold: ${reward.equipment!.name} '
'for $sellPrice gold');
}
}
case ChestRewardType.potion:
if (reward.potionId != null) {
updatedState = updatedState.copyWith(
potionInventory: updatedState.potionInventory.addPotion(
reward.potionId!,
reward.potionCount ?? 1,
),
);
debugPrint('[ReturnRewards] Added potion: ${reward.potionId} '
'x${reward.potionCount}');
}
case ChestRewardType.gold:
if (reward.gold != null && reward.gold! > 0) {
updatedState = updatedState.copyWith(
inventory: updatedState.inventory.copyWith(
gold: updatedState.inventory.gold + reward.gold!,
),
);
debugPrint('[ReturnRewards] Added gold: ${reward.gold}');
}
case ChestRewardType.experience:
if (reward.experience != null && reward.experience! > 0) {
updatedState = updatedState.copyWith(
progress: updatedState.progress.copyWith(
exp: updatedState.progress.exp.copyWith(
position:
updatedState.progress.exp.position + reward.experience!,
),
),
);
debugPrint('[ReturnRewards] Added experience: ${reward.experience}');
}
}
}
_state = updatedState;
_loop?.replaceState(updatedState); // ProgressLoop 상태도 업데이트
// 저장
unawaited(saveManager.saveState(
_state!,
cheatsEnabled: _cheatsEnabled,
monetization: _monetization,
));
_pendingReturnReward = null;
notifyListeners();
debugPrint('[ReturnRewards] Rewards applied: ${rewards.length} items');
}
/// 복귀 보상 건너뛰기
void skipReturnReward() {
_pendingReturnReward = null;
debugPrint('[ReturnRewards] Reward skipped by user');
}
}