import 'package:flutter/material.dart'; import 'package:asciineverdie/l10n/app_localizations.dart'; import 'package:asciineverdie/src/core/l10n/game_data_l10n.dart'; import 'package:asciineverdie/src/core/model/game_state.dart'; import 'package:asciineverdie/src/shared/retro_colors.dart'; /// 게임 클리어 엔딩 오버레이 (Act V 완료 시) /// /// 영화 엔딩 크레딧 스타일로 텍스트가 아래에서 위로 스크롤됨 /// - 탭/클릭 시 스크롤 최하단으로 즉시 이동 /// - 최하단에 명예의 전당 버튼 표시 class VictoryOverlay extends StatefulWidget { const VictoryOverlay({ super.key, required this.traits, required this.stats, required this.progress, required this.elapsedMs, required this.onComplete, }); final Traits traits; final Stats stats; final ProgressState progress; final int elapsedMs; /// 엔딩 완료 콜백 (명예의 전당으로 이동) final VoidCallback onComplete; @override State createState() => _VictoryOverlayState(); } class _VictoryOverlayState extends State with SingleTickerProviderStateMixin { late AnimationController _scrollController; late Animation _scrollAnimation; // 스크롤이 완료(최하단 도달) 되었는지 여부 bool _isScrollComplete = false; // 스크롤 지속 시간 (밀리초) static const _scrollDurationMs = 25000; // 25초 @override void initState() { super.initState(); _scrollController = AnimationController( duration: const Duration(milliseconds: _scrollDurationMs), vsync: this, ); _scrollAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _scrollController, curve: Curves.linear), ); // 스크롤 완료 시 버튼 표시 (자동 종료하지 않음) _scrollController.addStatusListener((status) { if (status == AnimationStatus.completed) { setState(() { _isScrollComplete = true; }); } }); _scrollController.forward(); } @override void dispose() { _scrollController.dispose(); super.dispose(); } /// 탭 시 스크롤 최하단으로 즉시 이동 void _skipToEnd() { _scrollController.stop(); _scrollController.value = 1.0; setState(() { _isScrollComplete = true; }); } @override Widget build(BuildContext context) { final gold = RetroColors.goldOf(context); return GestureDetector( onTap: _isScrollComplete ? null : _skipToEnd, // 스크롤 중에만 탭으로 스킵 child: Material( color: Colors.black, child: SafeArea( child: Stack( children: [ // 스크롤되는 크레딧 AnimatedBuilder( animation: _scrollAnimation, builder: (context, child) { return _buildScrollingCredits(context); }, ), // 스킵 버튼 (스크롤 중에만 표시) if (!_isScrollComplete) Positioned( top: 16, right: 16, child: TextButton( onPressed: _skipToEnd, child: Text( L10n.of(context).endingSkip, style: TextStyle( fontFamily: 'PressStart2P', fontSize: 10, color: gold.withValues(alpha: 0.5), ), ), ), ), // 하단 탭 힌트 (스크롤 중에만 표시) if (!_isScrollComplete) Positioned( bottom: 16, left: 0, right: 0, child: Text( L10n.of(context).endingTapToSkip, style: TextStyle( fontFamily: 'PressStart2P', fontSize: 8, color: Colors.white.withValues(alpha: 0.2), ), textAlign: TextAlign.center, ), ), ], ), ), ), ); } Widget _buildScrollingCredits(BuildContext context) { final screenHeight = MediaQuery.of(context).size.height; final contentHeight = _estimateContentHeight(); // 스크롤 오프셋: 화면 아래에서 시작 → 화면 위로 사라짐 final totalScrollDistance = screenHeight + contentHeight; final currentOffset = screenHeight - (_scrollAnimation.value * totalScrollDistance); return SingleChildScrollView( physics: const NeverScrollableScrollPhysics(), child: Transform.translate( offset: Offset(0, currentOffset), child: _buildCreditContent(context), ), ); } double _estimateContentHeight() { // 대략적인 콘텐츠 높이 추정 (스크롤 계산용) // 명예의 전당 버튼 추가로 인해 높이 증가 return 1600.0; } Widget _buildCreditContent(BuildContext context) { final l10n = L10n.of(context); final gold = RetroColors.goldOf(context); final textPrimary = RetroColors.textPrimaryOf(context); return Center( child: Container( constraints: const BoxConstraints(maxWidth: 500), padding: const EdgeInsets.symmetric(horizontal: 24), child: Column( mainAxisSize: MainAxisSize.min, children: [ // ═══════════════════════════════════ // VICTORY ASCII ART // ═══════════════════════════════════ _buildVictoryAsciiArt(gold), const SizedBox(height: 60), // ═══════════════════════════════════ // CONGRATULATIONS // ═══════════════════════════════════ Text( l10n.endingCongratulations, style: TextStyle( fontFamily: 'PressStart2P', fontSize: 14, color: gold, letterSpacing: 2, ), ), const SizedBox(height: 16), Text( l10n.endingGameComplete, style: TextStyle( fontFamily: 'JetBrainsMono', fontSize: 14, color: textPrimary, ), ), const SizedBox(height: 80), // ═══════════════════════════════════ // THE HERO // ═══════════════════════════════════ _buildSectionTitle(l10n.endingTheHero, gold), const SizedBox(height: 20), _buildHeroInfo(context), const SizedBox(height: 80), // ═══════════════════════════════════ // JOURNEY STATISTICS // ═══════════════════════════════════ _buildSectionTitle(l10n.endingJourneyStats, gold), const SizedBox(height: 20), _buildStatistics(context), const SizedBox(height: 80), // ═══════════════════════════════════ // FINAL STATS // ═══════════════════════════════════ _buildSectionTitle(l10n.endingFinalStats, gold), const SizedBox(height: 20), _buildFinalStats(context), const SizedBox(height: 100), // ═══════════════════════════════════ // ASCII TROPHY // ═══════════════════════════════════ _buildTrophyAsciiArt(gold), const SizedBox(height: 60), // ═══════════════════════════════════ // CREDITS // ═══════════════════════════════════ _buildSectionTitle(l10n.endingCredits, gold), const SizedBox(height: 20), _buildCredits(context), const SizedBox(height: 100), // ═══════════════════════════════════ // THE END // ═══════════════════════════════════ _buildTheEnd(context, gold), const SizedBox(height: 60), // ═══════════════════════════════════ // HALL OF FAME BUTTON // ═══════════════════════════════════ _buildHallOfFameButton(context, gold), const SizedBox(height: 100), // 여백 (스크롤 끝) ], ), ), ); } Widget _buildVictoryAsciiArt(Color gold) { const asciiArt = ''' ╔═══════════════════════════════════════════╗ ║ ║ ║ ██╗ ██╗██╗ ██████╗████████╗ ██████╗ ║ ║ ██║ ██║██║██╔════╝╚══██╔══╝██╔═══██╗ ║ ║ ██║ ██║██║██║ ██║ ██║ ██║ ║ ║ ╚██╗ ██╔╝██║██║ ██║ ██║ ██║ ║ ║ ╚████╔╝ ██║╚██████╗ ██║ ╚██████╔╝ ║ ║ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ║ ║ ║ ║ ██████╗ ██╗ ██╗ ║ ║ ██╔══██╗╚██╗ ██╔╝ ║ ║ ██████╔╝ ╚████╔╝ ║ ║ ██╔══██╗ ╚██╔╝ ║ ║ ██║ ██║ ██║ ║ ║ ╚═╝ ╚═╝ ╚═╝ ║ ║ ║ ╚═══════════════════════════════════════════╝'''; return Text( asciiArt, style: TextStyle( fontFamily: 'JetBrainsMono', fontSize: 8, color: gold, height: 1.0, ), textAlign: TextAlign.center, ); } Widget _buildSectionTitle(String title, Color gold) { return Column( children: [ Text( '═══════════════════', style: TextStyle( fontFamily: 'JetBrainsMono', fontSize: 12, color: gold.withValues(alpha: 0.5), ), ), const SizedBox(height: 8), Text( title, style: TextStyle( fontFamily: 'PressStart2P', fontSize: 12, color: gold, letterSpacing: 2, ), ), const SizedBox(height: 8), Text( '═══════════════════', style: TextStyle( fontFamily: 'JetBrainsMono', fontSize: 12, color: gold.withValues(alpha: 0.5), ), ), ], ); } Widget _buildHeroInfo(BuildContext context) { final l10n = L10n.of(context); final gold = RetroColors.goldOf(context); final textPrimary = RetroColors.textPrimaryOf(context); final textMuted = RetroColors.textMutedOf(context); return Column( children: [ // 캐릭터 이름 Text( widget.traits.name, style: TextStyle( fontFamily: 'PressStart2P', fontSize: 16, color: gold, ), ), const SizedBox(height: 12), // 레벨, 종족, 직업 Text( l10n.endingLevelFormat(widget.traits.level), style: TextStyle( fontFamily: 'PressStart2P', fontSize: 10, color: textPrimary, ), ), const SizedBox(height: 8), Text( '${GameDataL10n.getRaceName(context, widget.traits.race)} ' '${GameDataL10n.getKlassName(context, widget.traits.klass)}', style: TextStyle( fontFamily: 'JetBrainsMono', fontSize: 12, color: textMuted, ), ), ], ); } Widget _buildStatistics(BuildContext context) { final l10n = L10n.of(context); final textPrimary = RetroColors.textPrimaryOf(context); final exp = RetroColors.expOf(context); // 플레이 시간 포맷 final playTime = Duration(milliseconds: widget.elapsedMs); final hours = playTime.inHours; final minutes = playTime.inMinutes % 60; final seconds = playTime.inSeconds % 60; final playTimeStr = '${hours.toString().padLeft(2, '0')}:' '${minutes.toString().padLeft(2, '0')}:' '${seconds.toString().padLeft(2, '0')}'; return Column( children: [ _buildStatLine(l10n.endingMonstersSlain, '${widget.progress.monstersKilled}', textPrimary, exp), const SizedBox(height: 8), _buildStatLine(l10n.endingQuestsCompleted, '${widget.progress.questCount}', textPrimary, exp), const SizedBox(height: 8), _buildStatLine( l10n.endingPlayTime, playTimeStr, textPrimary, textPrimary), ], ); } Widget _buildStatLine( String label, String value, Color labelColor, Color valueColor) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( '$label: ', style: TextStyle( fontFamily: 'JetBrainsMono', fontSize: 12, color: labelColor.withValues(alpha: 0.7), ), ), Text( value, style: TextStyle( fontFamily: 'PressStart2P', fontSize: 10, color: valueColor, ), ), ], ); } Widget _buildFinalStats(BuildContext context) { final textPrimary = RetroColors.textPrimaryOf(context); final stats = widget.stats; return Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildStatBox('STR', '${stats.str}', textPrimary), const SizedBox(width: 16), _buildStatBox('CON', '${stats.con}', textPrimary), const SizedBox(width: 16), _buildStatBox('DEX', '${stats.dex}', textPrimary), ], ), const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildStatBox('INT', '${stats.intelligence}', textPrimary), const SizedBox(width: 16), _buildStatBox('WIS', '${stats.wis}', textPrimary), const SizedBox(width: 16), _buildStatBox('CHA', '${stats.cha}', textPrimary), ], ), ], ); } Widget _buildStatBox(String label, String value, Color color) { return Column( children: [ Text( label, style: TextStyle( fontFamily: 'JetBrainsMono', fontSize: 10, color: color.withValues(alpha: 0.5), ), ), const SizedBox(height: 4), Text( value, style: TextStyle( fontFamily: 'PressStart2P', fontSize: 12, color: color, ), ), ], ); } Widget _buildTrophyAsciiArt(Color gold) { const trophy = ''' ___________ '._==_==_=_.' .-\\: /-. | (|:. |) | '-|:. |-' \\::. / '::. .' ) ( _.' '._ '-------' '''; return Text( trophy, style: TextStyle( fontFamily: 'JetBrainsMono', fontSize: 12, color: gold, height: 1.0, ), textAlign: TextAlign.center, ); } Widget _buildCredits(BuildContext context) { final l10n = L10n.of(context); final textPrimary = RetroColors.textPrimaryOf(context); final textMuted = RetroColors.textMutedOf(context); final gold = RetroColors.goldOf(context); return Column( children: [ Text( l10n.appTitle, style: TextStyle( fontFamily: 'PressStart2P', fontSize: 12, color: gold, ), ), const SizedBox(height: 16), Text( l10n.endingThankYou, style: TextStyle( fontFamily: 'JetBrainsMono', fontSize: 12, color: textPrimary, ), ), const SizedBox(height: 8), Text( l10n.endingLegendLivesOn, style: TextStyle( fontFamily: 'JetBrainsMono', fontSize: 10, color: textMuted, fontStyle: FontStyle.italic, ), ), ], ); } Widget _buildTheEnd(BuildContext context, Color gold) { const theEnd = ''' ████████╗██╗ ██╗███████╗ ███████╗███╗ ██╗██████╗ ╚══██╔══╝██║ ██║██╔════╝ ██╔════╝████╗ ██║██╔══██╗ ██║ ███████║█████╗ █████╗ ██╔██╗ ██║██║ ██║ ██║ ██╔══██║██╔══╝ ██╔══╝ ██║╚██╗██║██║ ██║ ██║ ██║ ██║███████╗ ███████╗██║ ╚████║██████╔╝ ╚═╝ ╚═╝ ╚═╝╚══════╝ ╚══════╝╚═╝ ╚═══╝╚═════╝ '''; return Column( children: [ Text( theEnd, style: TextStyle( fontFamily: 'JetBrainsMono', fontSize: 8, color: gold, height: 1.0, ), textAlign: TextAlign.center, ), ], ); } /// 명예의 전당 버튼 (최하단) Widget _buildHallOfFameButton(BuildContext context, Color gold) { final l10n = L10n.of(context); return Column( children: [ // 안내 텍스트 Text( l10n.endingHallOfFameLine1, style: TextStyle( fontFamily: 'JetBrainsMono', fontSize: 12, color: gold.withValues(alpha: 0.7), ), ), const SizedBox(height: 4), Text( l10n.endingHallOfFameLine2, style: TextStyle( fontFamily: 'JetBrainsMono', fontSize: 12, 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, ), ), ], ), ), ), ], ); } }