feat(ui): 승리 오버레이 개선

This commit is contained in:
JiWoong Sul
2026-01-08 20:11:03 +09:00
parent 1eaff23001
commit 5f9a063ae4

View File

@@ -8,6 +8,8 @@ import 'package:asciineverdie/src/shared/retro_colors.dart';
/// 게임 클리어 엔딩 오버레이 (Act V 완료 시) /// 게임 클리어 엔딩 오버레이 (Act V 완료 시)
/// ///
/// 영화 엔딩 크레딧 스타일로 텍스트가 아래에서 위로 스크롤됨 /// 영화 엔딩 크레딧 스타일로 텍스트가 아래에서 위로 스크롤됨
/// - 탭/클릭 시 스크롤 최하단으로 즉시 이동
/// - 최하단에 명예의 전당 버튼 표시
class VictoryOverlay extends StatefulWidget { class VictoryOverlay extends StatefulWidget {
const VictoryOverlay({ const VictoryOverlay({
super.key, super.key,
@@ -35,6 +37,9 @@ class _VictoryOverlayState extends State<VictoryOverlay>
late AnimationController _scrollController; late AnimationController _scrollController;
late Animation<double> _scrollAnimation; late Animation<double> _scrollAnimation;
// 스크롤이 완료(최하단 도달) 되었는지 여부
bool _isScrollComplete = false;
// 스크롤 지속 시간 (밀리초) // 스크롤 지속 시간 (밀리초)
static const _scrollDurationMs = 25000; // 25초 static const _scrollDurationMs = 25000; // 25초
@@ -51,10 +56,12 @@ class _VictoryOverlayState extends State<VictoryOverlay>
CurvedAnimation(parent: _scrollController, curve: Curves.linear), CurvedAnimation(parent: _scrollController, curve: Curves.linear),
); );
// 스크롤 완료 시 자동 종료 // 스크롤 완료 시 버튼 표시 (자동 종료하지 않음)
_scrollController.addStatusListener((status) { _scrollController.addStatusListener((status) {
if (status == AnimationStatus.completed) { if (status == AnimationStatus.completed) {
widget.onComplete(); setState(() {
_isScrollComplete = true;
});
} }
}); });
@@ -67,12 +74,21 @@ class _VictoryOverlayState extends State<VictoryOverlay>
super.dispose(); super.dispose();
} }
/// 탭 시 스크롤 최하단으로 즉시 이동
void _skipToEnd() {
_scrollController.stop();
_scrollController.value = 1.0;
setState(() {
_isScrollComplete = true;
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final gold = RetroColors.goldOf(context); final gold = RetroColors.goldOf(context);
return GestureDetector( return GestureDetector(
onTap: widget.onComplete, // 탭으로 스킵 onTap: _isScrollComplete ? null : _skipToEnd, // 스크롤 중에만 탭으로 스킵
child: Material( child: Material(
color: Colors.black, color: Colors.black,
child: SafeArea( child: SafeArea(
@@ -86,38 +102,40 @@ class _VictoryOverlayState extends State<VictoryOverlay>
}, },
), ),
// 스킵 버튼 // 스킵 버튼 (스크롤 중에만 표시)
Positioned( if (!_isScrollComplete)
top: 16, Positioned(
right: 16, top: 16,
child: TextButton( right: 16,
onPressed: widget.onComplete, child: TextButton(
child: Text( onPressed: _skipToEnd,
L10n.of(context).endingSkip, child: Text(
style: TextStyle( L10n.of(context).endingSkip,
fontFamily: 'PressStart2P', style: TextStyle(
fontSize: 10, fontFamily: 'PressStart2P',
color: gold.withValues(alpha: 0.5), fontSize: 10,
color: gold.withValues(alpha: 0.5),
),
), ),
), ),
), ),
),
// 하단 탭 힌트 // 하단 탭 힌트 (스크롤 중에만 표시)
Positioned( if (!_isScrollComplete)
bottom: 16, Positioned(
left: 0, bottom: 16,
right: 0, left: 0,
child: Text( right: 0,
L10n.of(context).endingTapToSkip, child: Text(
style: TextStyle( L10n.of(context).endingTapToSkip,
fontFamily: 'PressStart2P', style: TextStyle(
fontSize: 8, fontFamily: 'PressStart2P',
color: Colors.white.withValues(alpha: 0.2), fontSize: 8,
color: Colors.white.withValues(alpha: 0.2),
),
textAlign: TextAlign.center,
), ),
textAlign: TextAlign.center,
), ),
),
], ],
), ),
), ),
@@ -145,7 +163,8 @@ class _VictoryOverlayState extends State<VictoryOverlay>
double _estimateContentHeight() { double _estimateContentHeight() {
// 대략적인 콘텐츠 높이 추정 (스크롤 계산용) // 대략적인 콘텐츠 높이 추정 (스크롤 계산용)
return 1500.0; // 명예의 전당 버튼 추가로 인해 높이 증가
return 1600.0;
} }
Widget _buildCreditContent(BuildContext context) { Widget _buildCreditContent(BuildContext context) {
@@ -231,7 +250,13 @@ class _VictoryOverlayState extends State<VictoryOverlay>
// THE END // THE END
// ═══════════════════════════════════ // ═══════════════════════════════════
_buildTheEnd(context, gold), _buildTheEnd(context, gold),
const SizedBox(height: 200), // 여백 (스크롤 끝) const SizedBox(height: 60),
// ═══════════════════════════════════
// HALL OF FAME BUTTON
// ═══════════════════════════════════
_buildHallOfFameButton(context, gold),
const SizedBox(height: 100), // 여백 (스크롤 끝)
], ],
), ),
), ),
@@ -518,7 +543,6 @@ class _VictoryOverlayState extends State<VictoryOverlay>
} }
Widget _buildTheEnd(BuildContext context, Color gold) { Widget _buildTheEnd(BuildContext context, Color gold) {
final l10n = L10n.of(context);
const theEnd = ''' const theEnd = '''
████████╗██╗ ██╗███████╗ ███████╗███╗ ██╗██████╗ ████████╗██╗ ██╗███████╗ ███████╗███╗ ██╗██████╗
╚══██╔══╝██║ ██║██╔════╝ ██╔════╝████╗ ██║██╔══██╗ ╚══██╔══╝██║ ██║██╔════╝ ██╔════╝████╗ ██║██╔══██╗
@@ -539,7 +563,17 @@ class _VictoryOverlayState extends State<VictoryOverlay>
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 24), ],
);
}
/// 명예의 전당 버튼 (최하단)
Widget _buildHallOfFameButton(BuildContext context, Color gold) {
final l10n = L10n.of(context);
return Column(
children: [
// 안내 텍스트
Text( Text(
l10n.endingHallOfFameLine1, l10n.endingHallOfFameLine1,
style: TextStyle( style: TextStyle(
@@ -557,6 +591,38 @@ class _VictoryOverlayState extends State<VictoryOverlay>
color: gold.withValues(alpha: 0.7), color: gold.withValues(alpha: 0.7),
), ),
), ),
const SizedBox(height: 24),
// 명예의 전당 버튼
SizedBox(
width: 280,
height: 56,
child: ElevatedButton(
onPressed: widget.onComplete,
style: ElevatedButton.styleFrom(
backgroundColor: gold,
foregroundColor: Colors.black,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
elevation: 8,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.emoji_events, size: 24),
const SizedBox(width: 12),
Text(
l10n.endingHallOfFameButton,
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 10,
),
),
],
),
),
),
], ],
); );
} }