diff --git a/lib/services/audio/rtc_manager.dart b/lib/services/audio/rtc_manager.dart index bd7939d..423d7f3 100644 --- a/lib/services/audio/rtc_manager.dart +++ b/lib/services/audio/rtc_manager.dart @@ -1,4 +1,6 @@ import 'dart:async'; +import 'dart:math' as math; + import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; @@ -4188,7 +4190,18 @@ class RealTimeCommunicationManager extends ChangeNotifier { if (currentStatus == null || !currentStatus.isEnabled) { return; } - final nextLevel = _nextRoomRocketLevelAfterLaunch(launch.safeLevel); + final currentLevel = + (currentStatus.currentLevel ?? currentStatus.level?.toInt() ?? 1) + .clamp(1, 99) + .toInt(); + final currentPercent = _roomRocketStatusPercent(currentStatus); + if (currentStatus.currentEnergy == 0 && + currentPercent <= 0 && + currentLevel >= launch.safeLevel) { + return; + } + final launchedLevel = math.max(currentLevel, launch.safeLevel); + final nextLevel = _nextRoomRocketLevelAfterLaunch(launchedLevel); final nextLevelInfo = _roomRocketLevelFor(nextLevel); final update = SCRoomRocketStatusRes( roomId: launch.roomId, @@ -4226,6 +4239,21 @@ class RealTimeCommunicationManager extends ChangeNotifier { return normalizedLevel; } + double _roomRocketStatusPercent(SCRoomRocketStatusRes status) { + final value = status.displayPercent ?? status.energyPercent; + if (value != null) { + final raw = value.toDouble(); + final percent = raw > 0 && raw <= 1 ? raw * 100 : raw; + return percent.clamp(0, 100).toDouble(); + } + final currentEnergy = status.currentEnergy; + final maxEnergy = status.maxEnergy ?? status.needEnergy; + if (currentEnergy == null || maxEnergy == null || maxEnergy <= 0) { + return 0; + } + return ((currentEnergy / maxEnergy) * 100).clamp(0, 100).toDouble(); + } + SCRoomRocketLevelRes? _roomRocketLevelFor(int level) { final normalizedLevel = level.clamp(1, 99).toInt(); for (final item diff --git a/lib/services/audio/rtm_manager.dart b/lib/services/audio/rtm_manager.dart index 0acda9d..27bbacb 100644 --- a/lib/services/audio/rtm_manager.dart +++ b/lib/services/audio/rtm_manager.dart @@ -4912,6 +4912,9 @@ class RealTimeMessagingManager extends ChangeNotifier { _markRoomRocketLaunchEffectConsumed(launch); _holdRoomRocketRewardPopupUntilLaunchEnds(launch); rtcProvider.advanceRoomRocketStatusAfterLaunch(launch); + rtcProvider.scheduleRoomRocketPostLaunchStatusRefresh( + roomId: launch.roomId, + ); rtcProvider.handleRoomRocketLaunchBroadcast(launch); if (shouldFetchReward) { _scheduleRoomRocketRewardPopupAfterHold( @@ -5571,14 +5574,10 @@ class RealTimeMessagingManager extends ChangeNotifier { required Map payload, required RoomRocketLaunchBroadcastMessage launch, }) { - final currentPercent = _roomRocketPayloadPercent(payload); - if (currentPercent < 99.5) { + if (!_roomRocketPayloadIndicatesFullOrLaunch(payload)) { return false; } - final previousPercent = _roomRocketStatusPercent( - rtcProvider.roomRocketStatus, - ); - if (previousPercent < 99.5) { + if (!(rtcProvider.roomRocketStatus?.shouldDisplayFullProgress ?? false)) { return true; } final launchKey = _roomRocketLaunchNoticeKey(launch); @@ -5592,6 +5591,29 @@ class RealTimeMessagingManager extends ChangeNotifier { !_recentRoomRocketLaunchLooseTimes.containsKey(looseLaunchKey)); } + bool _roomRocketPayloadIndicatesFullOrLaunch(Map payload) { + final status = _payloadText(payload['status']).toUpperCase(); + if (status.contains('LAUNCH')) { + return true; + } + final currentEnergy = _payloadNum( + payload['currentEnergy'] ?? payload['current_energy'], + ); + final maxEnergy = _payloadNum( + payload['maxEnergy'] ?? + payload['max_energy'] ?? + payload['needEnergy'] ?? + payload['need_energy'], + ); + if (currentEnergy != null && + maxEnergy != null && + maxEnergy > 0 && + currentEnergy >= maxEnergy) { + return true; + } + return _roomRocketPayloadPercent(payload) >= 100; + } + RoomRocketLaunchBroadcastMessage _buildRoomRocketLaunchFromStatusPayload({ required RealTimeCommunicationManager rtcProvider, required Map payload, @@ -5655,30 +5677,6 @@ class RealTimeMessagingManager extends ChangeNotifier { ); } - double _roomRocketStatusPercent(dynamic status) { - if (status == null) { - return 0; - } - final displayPercent = _payloadNum(status.displayPercent); - if (displayPercent != null) { - final raw = displayPercent.toDouble(); - final percent = raw > 0 && raw <= 1 ? raw * 100 : raw; - return percent.clamp(0, 100).toDouble(); - } - final energyPercent = _payloadNum(status.energyPercent); - if (energyPercent != null) { - final raw = energyPercent.toDouble(); - final percent = raw > 0 && raw <= 1 ? raw * 100 : raw; - return percent.clamp(0, 100).toDouble(); - } - final currentEnergy = _payloadNum(status.currentEnergy); - final maxEnergy = _payloadNum(status.maxEnergy ?? status.needEnergy); - if (currentEnergy == null || maxEnergy == null || maxEnergy <= 0) { - return 0; - } - return ((currentEnergy / maxEnergy) * 100).clamp(0, 100).toDouble(); - } - double _roomRocketPayloadPercent(Map payload) { final value = _payloadNum( payload['displayPercent'] ?? @@ -6021,6 +6019,28 @@ class RealTimeMessagingManager extends ChangeNotifier { priority: 1000, ); OverlayManager().addMessage(msg); + final currentContext = context; + if (currentContext != null && + currentContext.mounted && + _isCurrentVoiceRoom(launchRoomId)) { + final launch = RoomRocketLaunchBroadcastMessage.fromJson({ + ...payload, + 'roomId': launchRoomId, + 'level': rocketLevel, + 'rocketLevel': rocketLevel, + 'rocketIconUrl': rocketIconUrl, + }); + if (launch.isValid) { + final rtcProvider = Provider.of( + currentContext, + listen: false, + ); + rtcProvider.advanceRoomRocketStatusAfterLaunch(launch); + rtcProvider.scheduleRoomRocketPostLaunchStatusRefresh( + roomId: launchRoomId, + ); + } + } _scheduleRoomRocketStatusRefreshForCurrentRoom(launchRoomId); } else if (type == SCRoomMsgType.voiceRoomRocketLaunchBroadcast) { _handleVoiceRoomRocketLaunchBroadcast( diff --git a/lib/shared/business_logic/models/res/sc_room_rocket_status_res.dart b/lib/shared/business_logic/models/res/sc_room_rocket_status_res.dart index 821da50..eee99da 100644 --- a/lib/shared/business_logic/models/res/sc_room_rocket_status_res.dart +++ b/lib/shared/business_logic/models/res/sc_room_rocket_status_res.dart @@ -319,6 +319,26 @@ class SCRoomRocketStatusRes { normalizedStatus != 'DISABLED'; } + bool get isLaunchState { + final normalizedStatus = (_status ?? '').trim().toUpperCase(); + return normalizedStatus.contains('LAUNCH'); + } + + bool get hasFullEnergy { + final current = _currentEnergy; + final max = _maxEnergy ?? _needEnergy; + return current != null && max != null && max > 0 && current >= max; + } + + bool get hasFullPercent { + final percent = _normalizedPercent(_displayPercent ?? _energyPercent); + return percent != null && percent >= 100; + } + + bool get shouldDisplayFullProgress { + return hasFullEnergy || hasFullPercent || isLaunchState; + } + int? get roundNo => _roundNo; bool? get displayRound => _displayRound; @@ -529,6 +549,15 @@ class SCRoomRocketStatusRes { return num.tryParse(value?.toString().trim() ?? ''); } + static double? _normalizedPercent(num? value) { + if (value == null) { + return null; + } + final raw = value.toDouble(); + final percent = raw > 0 && raw <= 1 ? raw * 100 : raw; + return percent.clamp(0, 100).toDouble(); + } + static bool? _boolValue(dynamic value) { if (value is bool) { return value; diff --git a/lib/ui_kit/widgets/room/rocket/room_rocket_dialog.dart b/lib/ui_kit/widgets/room/rocket/room_rocket_dialog.dart index 429961c..4645323 100644 --- a/lib/ui_kit/widgets/room/rocket/room_rocket_dialog.dart +++ b/lib/ui_kit/widgets/room/rocket/room_rocket_dialog.dart @@ -60,8 +60,18 @@ class RoomRocketDialog extends StatelessWidget { selectedLevel.clamp(0, math.max(0, resolvedLevels.length - 1)).toInt(); final selectedLevelItem = resolvedLevels[resolvedSelectedLevel]; final percent = _percentForSelectedLevel(status, selectedLevelItem.level); + final displayPercent = _displayPercentForSelectedLevel( + status, + selectedLevelItem.level, + percent, + ); + final progressPercent = _progressPercentForSelectedLevel( + status, + selectedLevelItem.level, + percent, + ); final resolvedProgressStage = - progressStage ?? RoomRocketProgressStage.fromPercent(percent); + progressStage ?? RoomRocketProgressStage.fromPercent(progressPercent); final resolvedBottomStage = bottomStage ?? (resolvedCrew.isEmpty @@ -97,6 +107,7 @@ class RoomRocketDialog extends StatelessWidget { child: _RocketCanvas( scale: scale, percent: percent, + displayPercent: displayPercent, progressStage: resolvedProgressStage, bottomStage: resolvedBottomStage, status: status, @@ -207,6 +218,48 @@ class RoomRocketDialog extends StatelessWidget { } return _normalizePercent(status?.displayPercent ?? status?.energyPercent); } + + static int _displayPercentForSelectedLevel( + SCRoomRocketStatusRes? status, + int selectedLevel, + double percent, + ) { + final currentLevel = + (status?.currentLevel ?? status?.level?.toInt() ?? selectedLevel) + .clamp(1, 99) + .toInt(); + if (selectedLevel < currentLevel) { + return 100; + } + if (selectedLevel > currentLevel) { + return 0; + } + if (status?.shouldDisplayFullProgress ?? percent >= 100) { + return 100; + } + return percent.round().clamp(0, 99).toInt(); + } + + static double _progressPercentForSelectedLevel( + SCRoomRocketStatusRes? status, + int selectedLevel, + double percent, + ) { + final currentLevel = + (status?.currentLevel ?? status?.level?.toInt() ?? selectedLevel) + .clamp(1, 99) + .toInt(); + if (selectedLevel < currentLevel) { + return 100; + } + if (selectedLevel > currentLevel) { + return 0; + } + if (status?.shouldDisplayFullProgress ?? percent >= 100) { + return 100; + } + return percent; + } } @visibleForTesting @@ -214,6 +267,22 @@ String debugRoomRocketTitleText({required int level, required int round}) { return _roomRocketTitleText(level: level, round: round); } +@visibleForTesting +int debugRoomRocketDisplayPercentText({ + required SCRoomRocketStatusRes? status, + required int selectedLevel, +}) { + final percent = RoomRocketDialog._percentForSelectedLevel( + status, + selectedLevel, + ); + return RoomRocketDialog._displayPercentForSelectedLevel( + status, + selectedLevel, + percent, + ); +} + String _roomRocketTitleText({required int level, required int round}) { final normalizedLevel = level.clamp(1, 99).toInt(); final normalizedRound = round.clamp(1, 99).toInt(); @@ -352,6 +421,7 @@ class _RocketCanvas extends StatelessWidget { const _RocketCanvas({ required this.scale, required this.percent, + required this.displayPercent, required this.progressStage, required this.bottomStage, required this.status, @@ -373,6 +443,7 @@ class _RocketCanvas extends StatelessWidget { final double scale; final double percent; + final int displayPercent; final RoomRocketProgressStage progressStage; final RoomRocketBottomStage bottomStage; final SCRoomRocketStatusRes? status; @@ -506,7 +577,7 @@ class _RocketCanvas extends StatelessWidget { top: s(373), width: s(56), child: _GradientText( - '${percent.round()}%', + '$displayPercent%', fontSize: s(16), fontWeight: FontWeight.w600, textAlign: TextAlign.center, diff --git a/test/room_rocket_dialog_test.dart b/test/room_rocket_dialog_test.dart index ee89c0e..5bd2fee 100644 --- a/test/room_rocket_dialog_test.dart +++ b/test/room_rocket_dialog_test.dart @@ -42,4 +42,49 @@ void main() { ); }, ); + + test('rocket progress text caps rounded non-full progress at 99 percent', () { + final notFull = SCRoomRocketStatusRes( + currentLevel: 1, + currentEnergy: 995, + maxEnergy: 1000, + displayPercent: 99.5, + ); + final fullEnergy = SCRoomRocketStatusRes( + currentLevel: 1, + currentEnergy: 1000, + maxEnergy: 1000, + displayPercent: 99.5, + ); + final fullPercent = SCRoomRocketStatusRes( + currentLevel: 1, + currentEnergy: 995, + maxEnergy: 1000, + displayPercent: 100, + ); + final launchStatus = SCRoomRocketStatusRes( + currentLevel: 1, + currentEnergy: 995, + maxEnergy: 1000, + displayPercent: 99.5, + status: 'LAUNCHING', + ); + + expect( + debugRoomRocketDisplayPercentText(status: notFull, selectedLevel: 1), + 99, + ); + expect( + debugRoomRocketDisplayPercentText(status: fullEnergy, selectedLevel: 1), + 100, + ); + expect( + debugRoomRocketDisplayPercentText(status: fullPercent, selectedLevel: 1), + 100, + ); + expect( + debugRoomRocketDisplayPercentText(status: launchStatus, selectedLevel: 1), + 100, + ); + }); } diff --git a/test/room_rocket_status_test.dart b/test/room_rocket_status_test.dart index 77258c1..253478a 100644 --- a/test/room_rocket_status_test.dart +++ b/test/room_rocket_status_test.dart @@ -339,6 +339,38 @@ void main() { expect(disabledStatus.isEnabled, isFalse); }); + test( + 'identifies full rocket progress from energy percent or launch status', + () { + final roundedButNotFull = SCRoomRocketStatusRes.fromJson({ + 'currentEnergy': 995, + 'maxEnergy': 1000, + 'displayPercent': 99.5, + }); + final fullEnergy = SCRoomRocketStatusRes.fromJson({ + 'currentEnergy': 1000, + 'maxEnergy': 1000, + 'displayPercent': 99.5, + }); + final fullPercent = SCRoomRocketStatusRes.fromJson({ + 'currentEnergy': 995, + 'maxEnergy': 1000, + 'displayPercent': 100, + }); + final launchStatus = SCRoomRocketStatusRes.fromJson({ + 'currentEnergy': 995, + 'maxEnergy': 1000, + 'displayPercent': 99.5, + 'status': 'LAUNCHING', + }); + + expect(roundedButNotFull.shouldDisplayFullProgress, isFalse); + expect(fullEnergy.shouldDisplayFullProgress, isTrue); + expect(fullPercent.shouldDisplayFullProgress, isTrue); + expect(launchStatus.shouldDisplayFullProgress, isTrue); + }, + ); + test( 'uses top-level reward preview for the response level instead of current level', () {