火箭功能修复

This commit is contained in:
roxy 2026-05-28 11:08:37 +08:00
parent 0c4acb28e5
commit cedfc912cf
6 changed files with 258 additions and 33 deletions

View File

@ -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

View File

@ -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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<RealTimeCommunicationManager>(
currentContext,
listen: false,
);
rtcProvider.advanceRoomRocketStatusAfterLaunch(launch);
rtcProvider.scheduleRoomRocketPostLaunchStatusRefresh(
roomId: launchRoomId,
);
}
}
_scheduleRoomRocketStatusRefreshForCurrentRoom(launchRoomId);
} else if (type == SCRoomMsgType.voiceRoomRocketLaunchBroadcast) {
_handleVoiceRoomRocketLaunchBroadcast(

View File

@ -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;

View File

@ -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,

View File

@ -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,
);
});
}

View File

@ -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',
() {