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? _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 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> _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 loadCumulativeStats() async { _cumulativeStats = await _statisticsStorage.loadCumulative(); notifyListeners(); } /// 세션 통계를 누적 통계에 병합 Future mergeSessionStats() async { await _statisticsStorage.mergeSession(_sessionStats); _cumulativeStats = await _statisticsStorage.loadCumulative(); notifyListeners(); } Future 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 pause({bool saveOnStop = false}) async { await _stopLoop(saveOnStop: saveOnStop); _status = GameSessionStatus.idle; notifyListeners(); } /// 일시 정지 상태에서 재개 Future resume() async { if (_state == null || _status != GameSessionStatus.idle) return; await startNew(_state!, cheatsEnabled: _cheatsEnabled, isNewGame: false); } /// 일시 정지/재개 토글 Future 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? _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 _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 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 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 resumeAfterResurrection() async { if (_state == null) return; // 게임 재개 await startNew(_state!, cheatsEnabled: _cheatsEnabled, isNewGame: false); } // =========================================================================== // 광고 부활 (HP 100% + 아이템 복구 + 10분 자동부활) // =========================================================================== /// 광고 부활 (HP 100% + 아이템 복구 + 10분 자동부활 버프) /// /// 유료 유저: 광고 없이 부활 /// 무료 유저: 리워드 광고 시청 후 부활 Future 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 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.delayed(const Duration(milliseconds: 500), () { if (_pendingReturnReward != null) { onReturnRewardAvailable?.call(_pendingReturnReward!); } }); } } /// 복귀 보상 수령 완료 (상자 보상 적용) /// /// [rewards] 오픈된 상자 보상 목록 void applyReturnReward(List 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'); } }