fix(monetization): 버프 종료 버그 수정 (게임 시간 기준 통일)

- 배속 부스트: 실시간 타이머 → 게임 시간(elapsedMs) 기준 종료
- 자동부활 버프: 만료 시 autoReviveEndMs null 초기화 추가
- 매 틱마다 _checkSpeedBoostExpiry(), _checkAutoReviveExpiry() 호출
- 광고 직후 앱 resume 시 reload 방지 (isRecentlyShowedAd)
- 앱 pause/reload와 무관하게 버프 정상 종료
This commit is contained in:
JiWoong Sul
2026-01-20 18:13:40 +09:00
parent 2b4ea44623
commit 7b9f1f87a6
2 changed files with 103 additions and 47 deletions

View File

@@ -289,11 +289,15 @@ class _GamePlayScreenState extends State<GamePlayScreen>
} }
// 모바일: 앱이 포그라운드로 돌아올 때 전체 재로드 // 모바일: 앱이 포그라운드로 돌아올 때 전체 재로드
// (광고 표시 중에는 reload 건너뛰기 - 배속 부스트 등 상태 유지) // (광고 표시 중 또는 최근 광고 시청 후에는 reload 건너뛰기)
if (appState == AppLifecycleState.resumed && isMobile) { if (appState == AppLifecycleState.resumed && isMobile) {
_audioController.resumeAll(); _audioController.resumeAll();
if (!widget.controller.isShowingAd) { if (!widget.controller.isShowingAd &&
!widget.controller.isRecentlyShowedAd) {
_reloadGameScreen(); _reloadGameScreen();
} else {
// 광고 직후: 게임 재개만 (reload 없이 상태 유지)
widget.controller.resume();
} }
} }
} }

View File

@@ -61,13 +61,13 @@ class GameSessionController extends ChangeNotifier {
bool _autoResurrect = false; bool _autoResurrect = false;
// 속도 부스트 상태 (Phase 6) // 속도 부스트 상태 (Phase 6)
// 실시간 타이머 대신 게임 시간(elapsedMs) 기준으로 종료 판정
bool _isSpeedBoostActive = false; bool _isSpeedBoostActive = false;
Timer? _speedBoostTimer; static const int _speedBoostDuration = 300; // 5분 (게임 시간 기준)
int _speedBoostRemainingSeconds = 0;
static const int _speedBoostDuration = 300; // 5분
// 광고 표시 중 플래그 (lifecycle reload 방지용) // 광고 표시 중 플래그 (lifecycle reload 방지용)
bool _isShowingAd = false; bool _isShowingAd = false;
int _adEndTimeMs = 0; // 광고 종료 시점 (밀리초)
/// 광고 배속 배율 (릴리즈: 5x, 디버그빌드+디버그모드: 20x) /// 광고 배속 배율 (릴리즈: 5x, 디버그빌드+디버그모드: 20x)
int get _speedBoostMultiplier => (kDebugMode && _cheatsEnabled) ? 20 : 5; int get _speedBoostMultiplier => (kDebugMode && _cheatsEnabled) ? 20 : 5;
@@ -153,18 +153,31 @@ class GameSessionController extends ChangeNotifier {
} }
_initPreviousValues(state); _initPreviousValues(state);
// 명예의 전당 체크 → 가용 배속 결정 // 명예의 전당 체크 → 기본 가용 배속 결정
final availableSpeeds = await _getAvailableSpeeds(); final baseAvailableSpeeds = await _getAvailableSpeeds();
final hasHallOfFame = baseAvailableSpeeds.contains(2);
// 명예의 전당 해금 시 기본 2배속, 아니면 1배속 // 기본 배속 결정 (부스트 미적용 시)
final hasHallOfFame = availableSpeeds.contains(2); final int baseSpeed;
// 새 게임이면 기본 배속, 세이브 로드 시 명예의 전당 해금 시 최소 2배속 보장
final int initialSpeed;
if (isNewGame) { if (isNewGame) {
initialSpeed = hasHallOfFame ? 2 : 1; baseSpeed = hasHallOfFame ? 2 : 1;
} else { } else {
// 세이브 로드: 명예의 전당 해금 시 최소 2배속 baseSpeed = (hasHallOfFame && previousSpeed < 2) ? 2 : previousSpeed;
initialSpeed = (hasHallOfFame && previousSpeed < 2) ? 2 : previousSpeed; }
// 배속 부스트 활성화 상태면 부스트 배속 적용, 아니면 기본 배속
final List<int> finalAvailableSpeeds;
final int finalInitialSpeed;
if (_isSpeedBoostActive) {
// 부스트 상태: 부스트 배속만 사용, 기본 배속 저장
finalAvailableSpeeds = [_speedBoostMultiplier];
finalInitialSpeed = _speedBoostMultiplier;
_savedSpeedMultiplier = baseSpeed; // 종료 시 복원할 배속 저장
} else {
// 일반 상태: 기본 배속 사용
finalAvailableSpeeds = baseAvailableSpeeds;
finalInitialSpeed = baseSpeed;
} }
_loop = ProgressLoop( _loop = ProgressLoop(
@@ -177,11 +190,15 @@ class GameSessionController extends ChangeNotifier {
cheatsEnabled: cheatsEnabled, cheatsEnabled: cheatsEnabled,
onPlayerDied: _onPlayerDied, onPlayerDied: _onPlayerDied,
onGameComplete: _onGameComplete, onGameComplete: _onGameComplete,
availableSpeeds: availableSpeeds, availableSpeeds: finalAvailableSpeeds,
initialSpeedMultiplier: initialSpeed, initialSpeedMultiplier: finalInitialSpeed,
); );
_subscription = _loop!.stream.listen((next) { _subscription = _loop!.stream.listen((next) {
final elapsedMs = next.skillSystem.elapsedMs;
// 버프 만료 체크 (게임 시간 기준)
_checkSpeedBoostExpiry(elapsedMs);
_checkAutoReviveExpiry(elapsedMs);
_updateStatistics(next); _updateStatistics(next);
_state = next; _state = next;
notifyListeners(); notifyListeners();
@@ -307,6 +324,7 @@ class GameSessionController extends ChangeNotifier {
/// 일시 정지 상태에서 재개 /// 일시 정지 상태에서 재개
Future<void> resume() async { Future<void> resume() async {
if (_state == null || _status != GameSessionStatus.idle) return; if (_state == null || _status != GameSessionStatus.idle) return;
// 배속 부스트 상태는 startNew() 내에서 자동 처리됨
await startNew(_state!, cheatsEnabled: _cheatsEnabled, isNewGame: false); await startNew(_state!, cheatsEnabled: _cheatsEnabled, isNewGame: false);
} }
@@ -607,8 +625,22 @@ class GameSessionController extends ChangeNotifier {
/// 광고 표시 중 여부 (lifecycle reload 방지용) /// 광고 표시 중 여부 (lifecycle reload 방지용)
bool get isShowingAd => _isShowingAd; bool get isShowingAd => _isShowingAd;
/// 속도 부스트 남은 시간 (초) /// 최근 광고를 시청했는지 여부 (1초 이내)
int get speedBoostRemainingSeconds => _speedBoostRemainingSeconds; /// 광고 종료 후 resumed 이벤트가 늦게 발생하는 경우를 처리
bool get isRecentlyShowedAd {
if (_adEndTimeMs == 0) return false;
return DateTime.now().millisecondsSinceEpoch - _adEndTimeMs < 1000;
}
/// 속도 부스트 남은 시간 (초) - 게임 시간(elapsedMs) 기준 계산
int get speedBoostRemainingSeconds {
if (!_isSpeedBoostActive) return 0;
final endMs = _monetization.speedBoostEndMs;
if (endMs == null) return 0;
final currentMs = _state?.skillSystem.elapsedMs ?? 0;
final remainingMs = endMs - currentMs;
return remainingMs > 0 ? (remainingMs / 1000).ceil() : 0;
}
/// 속도 부스트 배율 /// 속도 부스트 배율
int get speedBoostMultiplier => _speedBoostMultiplier; int get speedBoostMultiplier => _speedBoostMultiplier;
@@ -651,6 +683,7 @@ class GameSessionController extends ChangeNotifier {
); );
_isShowingAd = false; // 광고 표시 종료 _isShowingAd = false; // 광고 표시 종료
_adEndTimeMs = DateTime.now().millisecondsSinceEpoch; // 종료 시점 기록
if (adResult == AdResult.completed || adResult == AdResult.debugSkipped) { if (adResult == AdResult.completed || adResult == AdResult.debugSkipped) {
debugPrint('[GameSession] Speed boost activated (free user with ad)'); debugPrint('[GameSession] Speed boost activated (free user with ad)');
@@ -662,53 +695,72 @@ class GameSessionController extends ChangeNotifier {
} }
/// 속도 부스트 시작 (내부) /// 속도 부스트 시작 (내부)
///
/// 게임 시간(elapsedMs) 기준으로 종료 시점 설정.
/// 실시간 타이머 대신 매 틱에서 종료 여부 체크.
void _startSpeedBoost() { void _startSpeedBoost() {
if (_loop == null) return;
// 현재 배속 저장
_savedSpeedMultiplier = _loop!.speedMultiplier;
// 부스트 배속 적용
_isSpeedBoostActive = true; _isSpeedBoostActive = true;
_speedBoostRemainingSeconds = _speedBoostDuration;
// monetization 상태에 종료 시점 저장 (UI 표시용) // loop가 있으면 현재 배속 저장 및 즉시 적용
if (_loop != null) {
_savedSpeedMultiplier = _loop!.speedMultiplier;
_loop!.updateAvailableSpeeds([_speedBoostMultiplier]);
}
// 종료 시점 저장 (게임 시간 기준)
final currentElapsedMs = _state?.skillSystem.elapsedMs ?? 0; final currentElapsedMs = _state?.skillSystem.elapsedMs ?? 0;
final endMs = currentElapsedMs + (_speedBoostDuration * 1000); final endMs = currentElapsedMs + (_speedBoostDuration * 1000);
_monetization = _monetization.copyWith(speedBoostEndMs: endMs); _monetization = _monetization.copyWith(speedBoostEndMs: endMs);
// ProgressLoop에 직접 배속 설정 debugPrint('[GameSession] Speed boost started, ends at $endMs ms');
_loop!.updateAvailableSpeeds([_speedBoostMultiplier]);
// 1초마다 남은 시간 감소
_speedBoostTimer?.cancel();
_speedBoostTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
_speedBoostRemainingSeconds--;
notifyListeners();
if (_speedBoostRemainingSeconds <= 0) {
_endSpeedBoost();
}
});
notifyListeners(); notifyListeners();
} }
/// 매 틱마다 부스트 만료 체크
///
/// 게임 시간(elapsedMs)이 종료 시점을 넘으면 자동 종료.
void _checkSpeedBoostExpiry(int elapsedMs) {
if (!_isSpeedBoostActive) return;
final endMs = _monetization.speedBoostEndMs;
if (endMs != null && elapsedMs >= endMs) {
_endSpeedBoost();
}
}
/// 매 틱마다 자동부활 버프 만료 체크
///
/// 게임 시간(elapsedMs)이 종료 시점을 넘으면 버프 상태 초기화.
void _checkAutoReviveExpiry(int elapsedMs) {
final endMs = _monetization.autoReviveEndMs;
if (endMs == null) return;
if (elapsedMs >= endMs) {
_monetization = _monetization.copyWith(autoReviveEndMs: null);
debugPrint('[GameSession] Auto-revive buff expired');
notifyListeners();
}
}
/// 속도 부스트 종료 (내부) /// 속도 부스트 종료 (내부)
void _endSpeedBoost() { void _endSpeedBoost() {
_speedBoostTimer?.cancel();
_speedBoostTimer = null;
_isSpeedBoostActive = false; _isSpeedBoostActive = false;
_speedBoostRemainingSeconds = 0;
// monetization 상태 초기화 (UI 표시 제거) // monetization 상태 초기화
_monetization = _monetization.copyWith(speedBoostEndMs: null); _monetization = _monetization.copyWith(speedBoostEndMs: null);
// 원래 배속 복원 // 원래 배속 복원
if (_loop != null) { final currentLoop = _loop;
final savedSpeed = _savedSpeedMultiplier;
if (currentLoop != null) {
_getAvailableSpeeds().then((speeds) { _getAvailableSpeeds().then((speeds) {
_loop!.updateAvailableSpeeds(speeds); // 콜백 실행 시점에 loop가 변경되지 않았는지 확인
_loop!.setSpeed(_savedSpeedMultiplier); if (_loop == currentLoop) {
currentLoop.updateAvailableSpeeds(speeds);
currentLoop.setSpeed(savedSpeed);
debugPrint('[GameSession] Speed restored to ${savedSpeed}x');
}
}); });
} }