import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_status_res.dart'; import 'package:yumi/ui_kit/components/custom_cached_image.dart'; class RoomRocketDialog extends StatelessWidget { const RoomRocketDialog({ super.key, this.status, this.progressStage, this.bottomStage, this.levels, this.selectedLevel = 0, this.onLevelSelected, this.rewards, this.crew, this.onClose, this.onOpenRank, this.onRewardSelected, this.onOpenRecord, this.onOpenNote, }); final SCRoomRocketStatusRes? status; final RoomRocketProgressStage? progressStage; final RoomRocketBottomStage? bottomStage; final List? levels; final int selectedLevel; final ValueChanged? onLevelSelected; final List? rewards; final List? crew; final VoidCallback? onClose; final VoidCallback? onOpenRank; final ValueChanged? onRewardSelected; final VoidCallback? onOpenRecord; final VoidCallback? onOpenNote; @override Widget build(BuildContext context) { final resolvedLevels = _normalizeLevels(levels); final resolvedRewards = _normalizeRewards(rewards); final resolvedCrew = _normalizeCrew(crew, status); final percent = _normalizePercent(status?.energyPercent); final resolvedProgressStage = progressStage ?? RoomRocketProgressStage.fromPercent(percent); final resolvedBottomStage = bottomStage ?? (resolvedCrew.isEmpty ? RoomRocketBottomStage.empty : RoomRocketBottomStage.active); final round = (status?.roundNo ?? status?.level ?? 1).clamp(1, 99).toInt(); return Directionality( textDirection: TextDirection.ltr, child: Material( color: Colors.transparent, child: Stack( children: [ Positioned.fill( child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: onClose, child: Container(color: Colors.black.withValues(alpha: 0.60)), ), ), Positioned.fill( child: LayoutBuilder( builder: (context, constraints) { final scale = math.min( constraints.maxWidth / _designWidth, constraints.maxHeight / _designHeight, ); return Align( alignment: Alignment.topCenter, child: SizedBox( width: _designWidth * scale, height: _designHeight * scale, child: _RocketCanvas( scale: scale, percent: percent, progressStage: resolvedProgressStage, bottomStage: resolvedBottomStage, round: round, levels: resolvedLevels, selectedLevel: selectedLevel, onLevelSelected: onLevelSelected, rewards: resolvedRewards, crew: resolvedCrew, onClose: onClose, onOpenRank: onOpenRank, onRewardSelected: onRewardSelected, onOpenRecord: onOpenRecord, onOpenNote: onOpenNote, ), ), ); }, ), ), ], ), ), ); } static List _normalizeLevels( List? levels, ) { final source = levels ?.where((item) => item.label.trim().isNotEmpty) .toList(growable: false) ?? const []; if (source.isNotEmpty) { return source; } return List.generate( 6, (index) => RoomRocketLevelItem(label: 'Lv.${index + 1}'), growable: false, ); } static List _normalizeRewards( List? rewards, ) { final source = rewards?.where((item) => item.amount.trim().isNotEmpty).toList() ?? const []; if (source.isNotEmpty) { return source; } return const [ RoomRocketRewardItem(asset: _RocketAssets.rewardThumb, amount: '10000'), RoomRocketRewardItem(asset: _RocketAssets.rewardThumb, amount: '10000'), RoomRocketRewardItem(asset: _RocketAssets.rewardThumb, amount: '10000'), RoomRocketRewardItem(asset: _RocketAssets.rewardThumb, amount: '10000'), ]; } static List _normalizeCrew( List? crew, SCRoomRocketStatusRes? status, ) { final hasNoContributors = status != null && (status.totalContributors ?? 0) <= 0; if (hasNoContributors) { return const []; } final source = crew?.where((item) => item.asset != null || item.imageUrl != null) ?? const []; if (source.isNotEmpty) { return source.take(3).toList(); } return const []; } static double _normalizePercent(num? value) { if (value == null) { return 100; } final raw = value.toDouble(); final percent = raw > 0 && raw <= 1 ? raw * 100 : raw; return percent.clamp(0, 100).toDouble(); } } class RoomRocketRewardItem { const RoomRocketRewardItem({this.asset, this.imageUrl, required this.amount}); final String? asset; final String? imageUrl; final String amount; } class RoomRocketLevelItem { const RoomRocketLevelItem({ required this.label, this.asset, this.imageUrl, this.enabled = true, }); final String label; final String? asset; final String? imageUrl; final bool enabled; } class RoomRocketCrewMember { const RoomRocketCrewMember({this.asset, this.imageUrl}); final String? asset; final String? imageUrl; } enum RoomRocketProgressStage { empty, charging, full; static RoomRocketProgressStage fromPercent(double percent) { if (percent <= 0) { return RoomRocketProgressStage.empty; } if (percent >= 99.5) { return RoomRocketProgressStage.full; } return RoomRocketProgressStage.charging; } } enum RoomRocketBottomStage { empty, active } class _RocketCanvas extends StatelessWidget { const _RocketCanvas({ required this.scale, required this.percent, required this.progressStage, required this.bottomStage, required this.round, required this.levels, required this.selectedLevel, this.onLevelSelected, required this.rewards, required this.crew, this.onClose, this.onOpenRank, this.onRewardSelected, this.onOpenRecord, this.onOpenNote, }); final double scale; final double percent; final RoomRocketProgressStage progressStage; final RoomRocketBottomStage bottomStage; final int round; final List levels; final int selectedLevel; final ValueChanged? onLevelSelected; final List rewards; final List crew; final VoidCallback? onClose; final VoidCallback? onOpenRank; final ValueChanged? onRewardSelected; final VoidCallback? onOpenRecord; final VoidCallback? onOpenNote; double s(double value) => value * scale; @override Widget build(BuildContext context) { return Stack( clipBehavior: Clip.hardEdge, children: [ _image(_RocketAssets.rocketStage, 15, 361, 344, 97), _image(_RocketAssets.rocketBody, 88, 111, 200, 432), _image(_RocketAssets.sidePanel, 309, 202, 66, 207), _image(_RocketAssets.sidePanel, 0, 202, 66, 207), _buildLevelRail(), _buildProgress(), _buildSideActions(), _buildRoundTitle(), _buildResetTime(), _buildRewardPanel(), _buildBottomStrip(), ], ); } Widget _image( String asset, double left, double top, double width, double height, { double opacity = 1, BoxFit fit = BoxFit.fill, }) { return Positioned( left: s(left), top: s(top), width: s(width), height: s(height), child: Opacity( opacity: opacity, child: Image.asset(asset, fit: fit, gaplessPlayback: true), ), ); } Widget _buildRoundTitle() { return Positioned( left: 0, top: s(120), width: s(_designWidth), child: _GradientText( 'Round $round Today', fontSize: s(16), fontWeight: FontWeight.w600, textAlign: TextAlign.center, ), ); } Widget _buildLevelRail() { return Positioned( left: s(13), top: s(212), width: s(40), height: s(190), child: ScrollConfiguration( behavior: const _NoGlowScrollBehavior(), child: ListView.separated( padding: EdgeInsets.zero, primary: false, physics: const BouncingScrollPhysics(), itemCount: levels.length, separatorBuilder: (_, __) => SizedBox(height: s(4)), itemBuilder: (context, index) { final item = levels[index]; return GestureDetector( behavior: HitTestBehavior.opaque, onTap: item.enabled ? () => onLevelSelected?.call(index) : null, child: SizedBox( width: s(40), height: s(52), child: _LevelBadge( scale: scale, item: item, selected: index == selectedLevel, ), ), ); }, ), ), ); } Widget _buildProgress() { final asset = switch (progressStage) { RoomRocketProgressStage.empty => _RocketAssets.progressEmpty, RoomRocketProgressStage.charging => _RocketAssets.progressPartial, RoomRocketProgressStage.full => _RocketAssets.progressFull, }; return Stack( children: [ _image(asset, 320, 227, 44, 134), Positioned( left: s(316), top: s(373), width: s(56), child: _GradientText( '${percent.round()}%', fontSize: s(16), fontWeight: FontWeight.w600, textAlign: TextAlign.center, colors: const [Color(0xFFA4FFE3), Color(0xFF21C593)], ), ), ], ); } Widget _buildSideActions() { return Stack( children: [ _image(_RocketAssets.pillBg, 306, 122, 220, 33), Positioned( left: s(335), top: s(127), width: s(22), height: s(22), child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: onOpenNote, child: DecoratedBox( decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: const Color(0xFFA4FFE3).withValues(alpha: 0.95), width: s(1), ), ), child: Center( child: Text( '?', textScaler: TextScaler.noScaling, style: TextStyle( color: Colors.white, fontSize: s(15), height: 1, fontWeight: FontWeight.w700, decoration: TextDecoration.none, ), ), ), ), ), ), _image(_RocketAssets.pillBg, 306, 159, 220, 33), Positioned( left: s(306), top: s(159), width: s(69), height: s(33), child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: onOpenRecord, child: Center( child: _GradientText( 'Record', fontSize: s(14), fontWeight: FontWeight.w600, textAlign: TextAlign.center, ), ), ), ), ], ); } Widget _buildResetTime() { return Stack( children: [ _image(_RocketAssets.pillBg, 78, 458, 220, 33), Positioned( left: s(109), top: s(468), child: _RocketText( 'Reset Time', fontSize: s(12), color: Colors.white, ), ), for (final item in const [ _TimerToken('23', 193), _TimerToken('23', 223), _TimerToken('23', 253), ]) Positioned( left: s(item.left), top: s(468), child: _RocketText( item.value, fontSize: s(12), color: Color(0xFF2DFFC1), ), ), for (final left in const [209.0, 239.0]) Positioned( left: s(left), top: s(468), child: _RocketText( ':', fontSize: s(12), color: const Color(0xFF2DFFC1), ), ), ], ); } Widget _buildRewardPanel() { return Stack( children: [ _image(_RocketAssets.rewardTop, 8, 499, 360, 63), _image(_RocketAssets.rewardMiddle, 8, 559, 360, 59), _image(_RocketAssets.rewardMiddle, 8, 594, 360, 59), _image(_RocketAssets.rewardBottom, 8, 653, 360, 76), Positioned( left: 0, top: s(519), width: s(_designWidth), child: const _GradientText( 'Reward', fontSize: 16, fontWeight: FontWeight.w600, textAlign: TextAlign.center, ).scaled(scale), ), Positioned( left: s(28), top: s(543), width: s(320), height: math.max(1, s(1)), child: DecoratedBox( decoration: BoxDecoration( color: const Color(0xFFB2FFE7).withValues(alpha: 0.16), ), ), ), _RewardTab( scale: scale, left: 31, top: 552, label: 'Top1', active: true, ), _RewardTab(scale: scale, left: 147, top: 552, label: 'Set off'), _RewardTab(scale: scale, left: 252, top: 552, label: 'In the room'), Positioned( left: s(28), top: s(578), width: s(336), height: s(135), child: ClipRect( child: ScrollConfiguration( behavior: const _NoGlowScrollBehavior(), child: SingleChildScrollView( scrollDirection: Axis.horizontal, physics: const BouncingScrollPhysics(), child: Row( children: [ for (var index = 0; index < rewards.length; index++) ...[ SizedBox( width: s(97), height: s(135), child: _RewardCard( scale: scale, item: rewards[index], onTap: () => onRewardSelected?.call(rewards[index]), ), ), if (index != rewards.length - 1) SizedBox(width: s(12)), ], ], ), ), ), ), ), ], ); } Widget _buildBottomStrip() { final activeBottom = bottomStage == RoomRocketBottomStage.active; final showCrew = activeBottom && crew.isNotEmpty; return Stack( children: [ Positioned( left: s(0), top: s(729), width: s(375), height: s(83), child: Transform.rotate( angle: math.pi, child: Image.asset( activeBottom ? _RocketAssets.bottomStripActive : _RocketAssets.bottomStripEmpty, fit: BoxFit.fill, gaplessPlayback: true, ), ), ), _image(_RocketAssets.bottomTrophy, 31, 740, 52, 69), if (!activeBottom) Positioned( left: s(108), top: s(750), width: s(169), height: s(36), child: FittedBox( fit: BoxFit.scaleDown, alignment: Alignment.center, child: Text( 'Not up to Rocket rank; no\n“Rocket King” title', textScaler: TextScaler.noScaling, maxLines: 2, softWrap: false, overflow: TextOverflow.visible, textAlign: TextAlign.center, style: TextStyle( color: Colors.white, fontSize: s(14), height: 16 / 14, fontWeight: FontWeight.w400, decoration: TextDecoration.none, ), ), ), ), if (showCrew) for (var index = 0; index < crew.length.clamp(0, 3); index++) Positioned( left: s(97 + 71.0 * index), top: s(753), width: s(50), height: s(50), child: _CrewAvatar(scale: scale, member: crew[index]), ), Positioned( left: s(315), top: s(753), width: s(52), height: s(52), child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: onOpenRank ?? onClose, child: Center( child: Image.asset( _RocketAssets.backArrow, width: s(32), height: s(32), fit: BoxFit.fill, gaplessPlayback: true, ), ), ), ), Positioned( left: s(0), top: s(729), width: s(375), height: s(83), child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: onOpenRank ?? onClose, child: const SizedBox.expand(), ), ), ], ); } } class _RewardCard extends StatelessWidget { const _RewardCard({required this.scale, required this.item, this.onTap}); final double scale; final RoomRocketRewardItem item; final VoidCallback? onTap; @override Widget build(BuildContext context) { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: onTap, child: Stack( children: [ Positioned.fill(child: CustomPaint(painter: _RewardCardPainter())), Positioned( left: 23 * scale, top: 24 * scale, width: 50 * scale, height: 50 * scale, child: _RocketImage( asset: item.asset, imageUrl: item.imageUrl, fit: BoxFit.cover, ), ), Positioned( left: 21 * scale, top: 106 * scale, width: 14 * scale, height: 14 * scale, child: Image.asset(_RocketAssets.coin, fit: BoxFit.contain), ), Positioned( left: 37 * scale, top: 105 * scale, width: 46 * scale, child: _RocketText( item.amount, fontSize: 14 * scale, color: Colors.white, textAlign: TextAlign.left, ), ), ], ), ); } } class _LevelBadge extends StatelessWidget { const _LevelBadge({ required this.scale, required this.item, required this.selected, }); final double scale; final RoomRocketLevelItem item; final bool selected; @override Widget build(BuildContext context) { return SizedBox( width: 40 * scale, height: 52 * scale, child: Stack( children: [ Positioned.fill( child: CustomPaint( painter: _LevelBadgePainter( selected: selected, enabled: item.enabled, ), ), ), Positioned( left: 10 * scale, top: 6 * scale, width: 20 * scale, height: 29 * scale, child: ClipRRect( borderRadius: BorderRadius.circular(4 * scale), child: item.imageUrl != null || item.asset != null ? _RocketImage( asset: item.asset, imageUrl: item.imageUrl, fit: BoxFit.cover, ) : Stack( clipBehavior: Clip.hardEdge, children: [ Positioned( left: -5.45 * scale, top: -10.92 * scale, width: 30.86 * scale, height: 66.82 * scale, child: Image.asset( _RocketAssets.rocketBody, fit: BoxFit.cover, gaplessPlayback: true, ), ), ], ), ), ), Positioned( left: 0, right: 0, top: 38 * scale, child: _RocketText( item.label, fontSize: 10 * scale, color: item.enabled ? Colors.white : Colors.white.withValues(alpha: 0.52), ), ), ], ), ); } } class _CrewAvatar extends StatelessWidget { const _CrewAvatar({required this.scale, required this.member}); final double scale; final RoomRocketCrewMember member; @override Widget build(BuildContext context) { return Stack( alignment: Alignment.center, children: [ Image.asset( _RocketAssets.bottomAvatarRing, width: 40 * scale, height: 40 * scale, fit: BoxFit.fill, ), ClipOval( child: SizedBox( width: 42 * scale, height: 42 * scale, child: _RocketImage( asset: member.asset, imageUrl: member.imageUrl, fit: BoxFit.cover, ), ), ), ], ); } } class _RocketImage extends StatelessWidget { const _RocketImage({this.asset, this.imageUrl, this.fit = BoxFit.cover}); final String? asset; final String? imageUrl; final BoxFit fit; @override Widget build(BuildContext context) { if (imageUrl != null && imageUrl!.trim().isNotEmpty) { return CustomCachedImage(imageUrl: imageUrl!, fit: fit); } if (asset != null && asset!.trim().isNotEmpty) { return Image.asset(asset!, fit: fit, gaplessPlayback: true); } return const SizedBox.shrink(); } } class _RewardTab extends StatelessWidget { const _RewardTab({ required this.scale, required this.left, required this.top, required this.label, this.active = false, }); final double scale; final double left; final double top; final String label; final bool active; @override Widget build(BuildContext context) { final color = active ? const Color(0xFF75F3CB) : const Color(0xFF75F3CB).withValues(alpha: 0.52); final width = label == 'In the room' ? 93.0 : label == 'Set off' ? 76.0 : 68.0; return Positioned( left: left * scale, top: top * scale, width: width * scale, height: 14 * scale, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _TabSlash(color: color, scale: scale), SizedBox(width: 3 * scale), Flexible( child: Text( label, textScaler: TextScaler.noScaling, maxLines: 1, overflow: TextOverflow.visible, textAlign: TextAlign.center, style: TextStyle( color: color, fontSize: 12 * scale, height: 1, decoration: TextDecoration.none, ), ), ), SizedBox(width: 3 * scale), Transform.rotate( angle: math.pi, child: _TabSlash(color: color, scale: scale), ), ], ), ); } } class _TabSlash extends StatelessWidget { const _TabSlash({required this.color, required this.scale}); final Color color; final double scale; @override Widget build(BuildContext context) { return Text( '///', textScaler: TextScaler.noScaling, style: TextStyle( color: color, fontSize: 10 * scale, height: 1, letterSpacing: 0, decoration: TextDecoration.none, ), ); } } class _NoGlowScrollBehavior extends ScrollBehavior { const _NoGlowScrollBehavior(); @override Widget buildOverscrollIndicator( BuildContext context, Widget child, ScrollableDetails details, ) { return child; } } class _LevelBadgePainter extends CustomPainter { const _LevelBadgePainter({required this.selected, required this.enabled}); final bool selected; final bool enabled; @override void paint(Canvas canvas, Size size) { final path = _scaledPath( const [ Offset(35.625, 0.5), Offset(39.5, 4.39941), Offset(39.5, 47.5996), Offset(35.625, 51.5), Offset(5.18164, 51.5), Offset(0.5, 47.5732), Offset(0.5, 4.39941), Offset(4.375, 0.5), ], size, const Size(40, 52), ); canvas.drawPath( path, Paint() ..shader = const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Color(0xFF077C60), Color(0xFF08251E)], ).createShader(Offset.zero & size), ); canvas.drawPath( path, Paint() ..style = PaintingStyle.stroke ..strokeWidth = selected ? size.width / 22 : size.width / 40 ..shader = LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ enabled ? const Color(0xFFB2FBCC) : const Color(0xFFB2FBCC).withValues(alpha: 0.42), const Color(0xFFB2FBCC).withValues(alpha: 0), ], ).createShader(Offset.zero & size), ); } @override bool shouldRepaint(covariant _LevelBadgePainter oldDelegate) { return oldDelegate.selected != selected || oldDelegate.enabled != enabled; } } class _RewardCardPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final panelPath = _scaledPath( const [ Offset(88.293, 0.5), Offset(96.5, 8.70703), Offset(96.5, 126.293), Offset(88.293, 134.5), Offset(8.70703, 134.5), Offset(0.5, 126.293), Offset(0.5, 8.70703), Offset(8.70703, 0.5), ], size, const Size(97, 135), ); canvas.drawPath( panelPath, Paint() ..shader = const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Color(0xFF077C60), Color(0xFF08251E)], ).createShader(Offset.zero & size), ); canvas.drawPath( panelPath, Paint() ..style = PaintingStyle.stroke ..strokeWidth = size.width / 97 ..shader = LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ const Color(0xFFB2FBCC), const Color(0xFFB2FBCC).withValues(alpha: 0), ], ).createShader(Offset.zero & size), ); final pillPath = _scaledPath( const [ Offset(8, 107.367), Offset(14.5, 101), Offset(82.5, 101), Offset(89, 107.367), Offset(89, 118.633), Offset(82.5, 125), Offset(14.5, 125), Offset(8, 118.633), ], size, const Size(97, 135), ); canvas.drawPath( pillPath, Paint() ..shader = const LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, colors: [Color(0xFF136648), Color(0xFF06412B), Color(0xFF136648)], ).createShader(Offset.zero & size), ); canvas.drawPath( pillPath, Paint() ..style = PaintingStyle.stroke ..strokeWidth = size.width / 97 ..color = const Color(0xFF8BF9FE).withValues(alpha: 0.85), ); final circleCenter = Offset( size.width * 48.5 / 97, size.height * 48.5 / 135, ); final circleRadius = math.min(size.width / 97, size.height / 135) * 37; canvas.drawCircle( circleCenter, circleRadius, Paint() ..style = PaintingStyle.stroke ..strokeWidth = size.width / 97 ..maskFilter = MaskFilter.blur(BlurStyle.normal, size.width / 24) ..color = const Color(0xFFEFFBFF).withValues(alpha: 0.6), ); canvas.drawCircle( circleCenter, circleRadius, Paint() ..style = PaintingStyle.stroke ..strokeWidth = size.width / 97 ..color = const Color(0xFF75F3CB), ); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } Path _scaledPath(List points, Size size, Size viewBox) { final path = Path(); for (var index = 0; index < points.length; index++) { final point = Offset( points[index].dx / viewBox.width * size.width, points[index].dy / viewBox.height * size.height, ); if (index == 0) { path.moveTo(point.dx, point.dy); } else { path.lineTo(point.dx, point.dy); } } return path..close(); } class _RocketText extends StatelessWidget { const _RocketText( this.text, { required this.fontSize, required this.color, this.textAlign = TextAlign.center, }); final String text; final double fontSize; final Color color; final TextAlign textAlign; @override Widget build(BuildContext context) { return Text( text, textScaler: TextScaler.noScaling, maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: textAlign, style: TextStyle( color: color, fontSize: fontSize, height: 1, decoration: TextDecoration.none, ), ); } } class _GradientText extends StatelessWidget { const _GradientText( this.text, { required this.fontSize, this.fontWeight = FontWeight.w400, this.textAlign = TextAlign.left, this.colors = const [Colors.white, Color(0xFFA4FFE3)], }); final String text; final double fontSize; final FontWeight fontWeight; final TextAlign textAlign; final List colors; _GradientText scaled(double scale) { return _GradientText( text, fontSize: fontSize * scale, fontWeight: fontWeight, textAlign: textAlign, colors: colors, ); } @override Widget build(BuildContext context) { return ShaderMask( shaderCallback: (bounds) => LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: colors, ).createShader(bounds), blendMode: BlendMode.srcIn, child: Text( text, textScaler: TextScaler.noScaling, maxLines: 1, textAlign: textAlign, overflow: TextOverflow.ellipsis, style: TextStyle( color: Colors.white, fontSize: fontSize, height: 1, fontWeight: fontWeight, decoration: TextDecoration.none, ), ), ); } } class _TimerToken { const _TimerToken(this.value, this.left); final String value; final double left; } class _RocketAssets { const _RocketAssets._(); static const String rocketStage = 'sc_images/room/rocket/rocket_stage.png'; static const String rocketBody = 'sc_images/room/rocket/rocket_body.png'; static const String sidePanel = 'sc_images/room/rocket/side_panel.png'; static const String bottomStripActive = 'sc_images/room/rocket/bottom_strip_active.png'; static const String bottomStripEmpty = 'sc_images/room/rocket/bottom_strip_active.png'; static const String bottomTrophy = 'sc_images/room/rocket/bottom_trophy.png'; static const String backArrow = 'sc_images/room/rocket/back_arrow.png'; static const String bottomAvatarRing = 'sc_images/room/rocket/bottom_avatar_ring.png'; static const String pillBg = 'sc_images/room/rocket/pill_bg.png'; static const String progressFull = 'sc_images/room/rocket/progress_full.png'; static const String progressPartial = 'sc_images/room/rocket/progress_partial.png'; static const String progressEmpty = 'sc_images/room/rocket/progress_empty.png'; static const String rewardTop = 'sc_images/room/rocket/reward_dialog_top.png'; static const String rewardMiddle = 'sc_images/room/rocket/reward_dialog_middle.png'; static const String rewardBottom = 'sc_images/room/rocket/reward_dialog_bottom.png'; static const String rewardThumb = 'sc_images/room/rocket/reward_thumb.png'; static const String coin = 'sc_images/register_reward/yumi_coin.png'; } const double _designWidth = 375; const double _designHeight = 812;