From eee32c94b86767e5448f96dda04c0e8d051c7354 Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Mon, 30 Mar 2026 22:55:24 +0900 Subject: [PATCH] =?UTF-8?q?fix(buff):=20=EB=B0=B0=EC=86=8D=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EC=97=90=EC=84=9C=20=EB=B2=84=ED=94=84=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=EC=9D=B4=20=EB=B0=B0=EC=86=8D=EC=9D=84=20=EB=94=B0?= =?UTF-8?q?=EB=9D=BC=EA=B0=80=EB=8A=94=20=EB=B2=84=EA=B7=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - speedBoostEndMs: elapsedMs(게임 시간) → DateTime.now()(실제 시간) - autoReviveEndMs: elapsedMs → DateTime.now() - MonetizationState: isAutoReviveActive/isSpeedBoostActive 실제 시간 기준 - 5배속에서 5분 버프가 1분에 만료되던 문제 해결 - bossLevelingEndTime은 이미 DateTime.now() 기준 (변경 불필요) --- lib/src/core/model/monetization_state.dart | 12 ++--- .../game/game_session_controller.dart | 7 +-- .../game/managers/resurrection_manager.dart | 4 +- .../game/managers/speed_boost_manager.dart | 18 ++++---- test/core/model/monetization_state_test.dart | 46 +++++++++++-------- 5 files changed, 45 insertions(+), 42 deletions(-) diff --git a/lib/src/core/model/monetization_state.dart b/lib/src/core/model/monetization_state.dart index a273f79..6274d0a 100644 --- a/lib/src/core/model/monetization_state.dart +++ b/lib/src/core/model/monetization_state.dart @@ -69,18 +69,18 @@ class MonetizationState with _$MonetizationState { /// 무료 사용자 여부 bool get isFreeUser => !adRemovalPurchased; - /// 자동부활 버프 활성 여부 (elapsedMs 기준) - bool isAutoReviveActive(int elapsedMs) { + /// 자동부활 버프 활성 여부 (실제 시간 기준) + bool isAutoReviveActive([int? _]) { if (autoReviveEndMs == null) return false; - return elapsedMs < autoReviveEndMs!; + return DateTime.now().millisecondsSinceEpoch < autoReviveEndMs!; } - /// 5배속 버프 활성 여부 (elapsedMs 기준) + /// 5배속 버프 활성 여부 (실제 시간 기준) /// 유료 사용자는 항상 활성 - bool isSpeedBoostActive(int elapsedMs) { + bool isSpeedBoostActive([int? _]) { if (isPaidUser) return true; if (speedBoostEndMs == null) return false; - return elapsedMs < speedBoostEndMs!; + return DateTime.now().millisecondsSinceEpoch < speedBoostEndMs!; } /// 행운의 부적 버프 활성 여부 (elapsedMs 기준) diff --git a/lib/src/features/game/game_session_controller.dart b/lib/src/features/game/game_session_controller.dart index 3665452..7586c18 100644 --- a/lib/src/features/game/game_session_controller.dart +++ b/lib/src/features/game/game_session_controller.dart @@ -136,10 +136,8 @@ class GameSessionController extends ChangeNotifier { int get speedBoostMultiplier => _speedBoostManager.speedBoostMultiplier; int get speedBoostDuration => _speedBoostManager.speedBoostDuration; - int get speedBoostRemainingSeconds => _speedBoostManager.getRemainingSeconds( - _monetization, - _state?.skillSystem.elapsedMs ?? 0, - ); + int get speedBoostRemainingSeconds => + _speedBoostManager.getRemainingSeconds(_monetization); int get currentSpeedMultiplier => _speedBoostManager.getCurrentSpeedMultiplier(_loop); @@ -242,7 +240,6 @@ class GameSessionController extends ChangeNotifier { void _checkBuffExpiries(int elapsedMs) { // 속도 부스트 만료 체크 final boostEnded = _speedBoostManager.checkExpiry( - elapsedMs: elapsedMs, monetization: _monetization, loop: _loop, ); diff --git a/lib/src/features/game/managers/resurrection_manager.dart b/lib/src/features/game/managers/resurrection_manager.dart index c38ad83..d1a4210 100644 --- a/lib/src/features/game/managers/resurrection_manager.dart +++ b/lib/src/features/game/managers/resurrection_manager.dart @@ -101,9 +101,9 @@ class ResurrectionManager { void processRevive() { revivedState = resurrectionService.processAdRevive(state); - // 10분 자동부활 버프 활성화 (elapsedMs 기준) + // 10분 자동부활 버프 활성화 (실제 시간 기준, 배속 영향 받지 않음) final buffEndMs = - revivedState!.skillSystem.elapsedMs + 600000; // 10분 = 600,000ms + DateTime.now().millisecondsSinceEpoch + 600000; // 10분 = 600,000ms updatedMonetization = monetization.copyWith(autoReviveEndMs: buffEndMs); debugPrint( diff --git a/lib/src/features/game/managers/speed_boost_manager.dart b/lib/src/features/game/managers/speed_boost_manager.dart index bbfa701..bcd865c 100644 --- a/lib/src/features/game/managers/speed_boost_manager.dart +++ b/lib/src/features/game/managers/speed_boost_manager.dart @@ -51,15 +51,12 @@ class SpeedBoostManager { /// 속도 부스트 지속 시간 (초) int get speedBoostDuration => _speedBoostDuration; - /// 속도 부스트 남은 시간 (초) - 게임 시간(elapsedMs) 기준 계산 - int getRemainingSeconds( - MonetizationState monetization, - int currentElapsedMs, - ) { + /// 속도 부스트 남은 시간 (초) - 실제 시간(wall clock) 기준 계산 + int getRemainingSeconds(MonetizationState monetization) { if (!_isSpeedBoostActive) return 0; final endMs = monetization.speedBoostEndMs; if (endMs == null) return 0; - final remainingMs = endMs - currentElapsedMs; + final remainingMs = endMs - DateTime.now().millisecondsSinceEpoch; return remainingMs > 0 ? (remainingMs / 1000).ceil() : 0; } @@ -136,8 +133,9 @@ class SpeedBoostManager { loop.updateAvailableSpeeds([speedBoostMultiplier]); } - // 종료 시점 저장 (게임 시간 기준) - final endMs = currentElapsedMs + (_speedBoostDuration * 1000); + // 종료 시점 저장 (실제 시간 기준, 배속 영향 받지 않음) + final endMs = + DateTime.now().millisecondsSinceEpoch + (_speedBoostDuration * 1000); final updatedMonetization = monetization.copyWith(speedBoostEndMs: endMs); debugPrint('[SpeedBoost] Started, ends at $endMs ms'); @@ -150,14 +148,14 @@ class SpeedBoostManager { /// /// Returns: 부스트가 종료되었으면 true bool checkExpiry({ - required int elapsedMs, required MonetizationState monetization, required ProgressLoop? loop, }) { if (!_isSpeedBoostActive) return false; final endMs = monetization.speedBoostEndMs; - if (endMs != null && elapsedMs >= endMs) { + final now = DateTime.now().millisecondsSinceEpoch; + if (endMs != null && now >= endMs) { endSpeedBoost(loop: loop); return true; } diff --git a/test/core/model/monetization_state_test.dart b/test/core/model/monetization_state_test.dart index 37a368e..855974c 100644 --- a/test/core/model/monetization_state_test.dart +++ b/test/core/model/monetization_state_test.dart @@ -56,31 +56,36 @@ void main() { test('autoReviveEndMs가 null이면 false 반환', () { final state = MonetizationState.initial(); - expect(state.isAutoReviveActive(1000), isFalse); + expect(state.isAutoReviveActive(), isFalse); }); - test('elapsedMs가 종료 시점 이내면 true 반환', () { + test('종료 시점이 미래면 true 반환', () { + final futureMs = + DateTime.now().millisecondsSinceEpoch + 60000; // 1분 후 final state = MonetizationState.initial().copyWith( - autoReviveEndMs: 5000, + autoReviveEndMs: futureMs, ); - expect(state.isAutoReviveActive(3000), isTrue); + expect(state.isAutoReviveActive(), isTrue); }); - test('elapsedMs가 종료 시점을 초과하면 false 반환', () { + test('종료 시점이 과거면 false 반환', () { + final pastMs = + DateTime.now().millisecondsSinceEpoch - 1000; // 1초 전 final state = MonetizationState.initial().copyWith( - autoReviveEndMs: 5000, + autoReviveEndMs: pastMs, ); - expect(state.isAutoReviveActive(6000), isFalse); + expect(state.isAutoReviveActive(), isFalse); }); - test('elapsedMs가 종료 시점과 정확히 같으면 false 반환 (경계값)', () { + test('종료 시점이 현재와 거의 같으면 false 반환 (경계값)', () { + final nowMs = DateTime.now().millisecondsSinceEpoch - 1; final state = MonetizationState.initial().copyWith( - autoReviveEndMs: 5000, + autoReviveEndMs: nowMs, ); - expect(state.isAutoReviveActive(5000), isFalse); + expect(state.isAutoReviveActive(), isFalse); }); }); @@ -93,30 +98,33 @@ void main() { final state = MonetizationState.initial(isPaidUser: true); // speedBoostEndMs가 null이어도 유료 사용자는 항상 활성 - expect(state.isSpeedBoostActive(0), isTrue); - expect(state.isSpeedBoostActive(999999), isTrue); + expect(state.isSpeedBoostActive(), isTrue); }); - test('무료 사용자 - 종료 시점 이내면 true 반환', () { + test('무료 사용자 - 종료 시점이 미래면 true 반환', () { + final futureMs = + DateTime.now().millisecondsSinceEpoch + 60000; final state = MonetizationState.initial().copyWith( - speedBoostEndMs: 10000, + speedBoostEndMs: futureMs, ); - expect(state.isSpeedBoostActive(5000), isTrue); + expect(state.isSpeedBoostActive(), isTrue); }); - test('무료 사용자 - 종료 시점 초과 시 false 반환', () { + test('무료 사용자 - 종료 시점이 과거면 false 반환', () { + final pastMs = + DateTime.now().millisecondsSinceEpoch - 1000; final state = MonetizationState.initial().copyWith( - speedBoostEndMs: 10000, + speedBoostEndMs: pastMs, ); - expect(state.isSpeedBoostActive(15000), isFalse); + expect(state.isSpeedBoostActive(), isFalse); }); test('무료 사용자 - speedBoostEndMs가 null이면 false 반환', () { final state = MonetizationState.initial(); - expect(state.isSpeedBoostActive(1000), isFalse); + expect(state.isSpeedBoostActive(), isFalse); }); });