fix(monetization): 버프 종료 버그 수정 (게임 시간 기준 통일)
- 배속 부스트: 실시간 타이머 → 게임 시간(elapsedMs) 기준 종료 - 자동부활 버프: 만료 시 autoReviveEndMs null 초기화 추가 - 매 틱마다 _checkSpeedBoostExpiry(), _checkAutoReviveExpiry() 호출 - 광고 직후 앱 resume 시 reload 방지 (isRecentlyShowedAd) - 앱 pause/reload와 무관하게 버프 정상 종료
This commit is contained in:
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user