From d9132a72eaa4210629baffb0e03782958932e635 Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Thu, 19 Mar 2026 16:56:12 +0900 Subject: [PATCH] =?UTF-8?q?refactor(game):=20VictoryOverlay=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=81=AC=EB=A0=88=EB=94=A7=20=EC=BD=98=ED=85=90?= =?UTF-8?q?=EC=B8=A0=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - victory_overlay.dart 734줄 → 265줄 - victory_credit_content.dart 475줄 신규 생성 --- .../game/widgets/victory_credit_content.dart | 475 +++++++++++++++++ .../game/widgets/victory_overlay.dart | 499 +----------------- 2 files changed, 490 insertions(+), 484 deletions(-) create mode 100644 lib/src/features/game/widgets/victory_credit_content.dart diff --git a/lib/src/features/game/widgets/victory_credit_content.dart b/lib/src/features/game/widgets/victory_credit_content.dart new file mode 100644 index 0000000..9cefa90 --- /dev/null +++ b/lib/src/features/game/widgets/victory_credit_content.dart @@ -0,0 +1,475 @@ +import 'package:flutter/material.dart'; + +import 'package:asciineverdie/l10n/app_localizations.dart'; +import 'package:asciineverdie/src/shared/l10n/game_data_l10n.dart'; +import 'package:asciineverdie/src/core/model/game_state.dart'; +import 'package:asciineverdie/src/shared/retro_colors.dart'; + +/// 승리 크레딧 콘텐츠(victory credit content) 위젯 +/// +/// 영웅 정보, 통계, ASCII 아트, 크레딧 텍스트, +/// 명예의 전당 버튼 등 스크롤되는 콘텐츠를 구성한다. +class VictoryCreditContent extends StatelessWidget { + const VictoryCreditContent({ + 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; + + /// 엔딩 완료 콜백(callback) - 명예의 전당으로 이동 + final VoidCallback onComplete; + + @override + Widget build(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: [ + _buildVictoryAsciiArt(gold), + const SizedBox(height: 60), + + // 축하 메시지(congratulations) + Text( + l10n.endingCongratulations, + style: TextStyle( + fontFamily: 'PressStart2P', + fontSize: 16, + color: gold, + letterSpacing: 2, + ), + ), + const SizedBox(height: 16), + Text( + l10n.endingGameComplete, + style: TextStyle( + fontFamily: 'JetBrainsMono', + fontSize: 18, + color: textPrimary, + ), + ), + const SizedBox(height: 80), + + _buildSectionTitle(l10n.endingTheHero, gold), + const SizedBox(height: 20), + _buildHeroInfo(context), + const SizedBox(height: 80), + + _buildSectionTitle(l10n.endingJourneyStats, gold), + const SizedBox(height: 20), + _buildStatistics(context), + const SizedBox(height: 80), + + _buildSectionTitle(l10n.endingFinalStats, gold), + const SizedBox(height: 20), + _buildFinalStats(context), + const SizedBox(height: 100), + + _buildTrophyAsciiArt(gold), + const SizedBox(height: 60), + + _buildSectionTitle(l10n.endingCredits, gold), + const SizedBox(height: 20), + _buildCredits(context), + const SizedBox(height: 100), + + _buildTheEnd(context, gold), + const SizedBox(height: 60), + + _buildHallOfFameButton(context, gold), + const SizedBox(height: 100), + ], + ), + ), + ); + } + + Widget _buildVictoryAsciiArt(Color gold) { + const asciiArt = ''' +╔═══════════════════════════════════════════╗ +║ ║ +║ ██╗ ██╗██╗ ██████╗████████╗ ██████╗ ║ +║ ██║ ██║██║██╔════╝╚══██╔══╝██╔═══██╗ ║ +║ ██║ ██║██║██║ ██║ ██║ ██║ ║ +║ ╚██╗ ██╔╝██║██║ ██║ ██║ ██║ ║ +║ ╚████╔╝ ██║╚██████╗ ██║ ╚██████╔╝ ║ +║ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ║ +║ ║ +║ ██████╗ ██╗ ██╗ ║ +║ ██╔══██╗╚██╗ ██╔╝ ║ +║ ██████╔╝ ╚████╔╝ ║ +║ ██╔══██╗ ╚██╔╝ ║ +║ ██║ ██║ ██║ ║ +║ ╚═╝ ╚═╝ ╚═╝ ║ +║ ║ +╚═══════════════════════════════════════════╝'''; + + return Text( + asciiArt, + style: TextStyle( + fontFamily: 'JetBrainsMono', + fontSize: 14, + color: gold, + height: 1.0, + ), + textAlign: TextAlign.center, + ); + } + + Widget _buildSectionTitle(String title, Color gold) { + return Column( + children: [ + Text( + '═══════════════════', + style: TextStyle( + fontFamily: 'JetBrainsMono', + fontSize: 17, + color: gold.withValues(alpha: 0.5), + ), + ), + const SizedBox(height: 8), + Text( + title, + style: TextStyle( + fontFamily: 'PressStart2P', + fontSize: 15, + color: gold, + letterSpacing: 2, + ), + ), + const SizedBox(height: 8), + Text( + '═══════════════════', + style: TextStyle( + fontFamily: 'JetBrainsMono', + fontSize: 17, + 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( + traits.name, + style: TextStyle( + fontFamily: 'PressStart2P', + fontSize: 18, + color: gold, + ), + ), + const SizedBox(height: 12), + Text( + l10n.endingLevelFormat(traits.level), + style: TextStyle( + fontFamily: 'PressStart2P', + fontSize: 14, + color: textPrimary, + ), + ), + const SizedBox(height: 8), + Text( + '${GameDataL10n.getRaceName(context, traits.race)} ' + '${GameDataL10n.getKlassName(context, traits.klass)}', + style: TextStyle( + fontFamily: 'JetBrainsMono', + fontSize: 17, + color: textMuted, + ), + ), + ], + ); + } + + Widget _buildStatistics(BuildContext context) { + final l10n = L10n.of(context); + final textPrimary = RetroColors.textPrimaryOf(context); + final exp = RetroColors.expOf(context); + + // 플레이 시간(play time) 포맷 + final playTime = Duration(milliseconds: 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, + '${progress.monstersKilled}', + textPrimary, + exp, + ), + const SizedBox(height: 8), + _buildStatLine( + l10n.endingQuestsCompleted, + '${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: 17, + color: labelColor.withValues(alpha: 0.7), + ), + ), + Text( + value, + style: TextStyle( + fontFamily: 'PressStart2P', + fontSize: 14, + color: valueColor, + ), + ), + ], + ); + } + + Widget _buildFinalStats(BuildContext context) { + final textPrimary = RetroColors.textPrimaryOf(context); + + 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: 15, + color: color.withValues(alpha: 0.5), + ), + ), + const SizedBox(height: 4), + Text( + value, + style: TextStyle( + fontFamily: 'PressStart2P', + fontSize: 15, + color: color, + ), + ), + ], + ); + } + + Widget _buildTrophyAsciiArt(Color gold) { + const trophy = ''' + ____________ + '._==_==_=_.' + .-\\: /-. + | (|:. |) | + '-|:. |-' + \\::. / + '::. .' + ) ( + _.' '._ + '-------' '''; + + return Text( + trophy, + style: TextStyle( + fontFamily: 'JetBrainsMono', + fontSize: 17, + 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: 15, + color: gold, + ), + ), + const SizedBox(height: 16), + Text( + l10n.endingThankYou, + style: TextStyle( + fontFamily: 'JetBrainsMono', + fontSize: 17, + color: textPrimary, + ), + ), + const SizedBox(height: 8), + Text( + l10n.endingLegendLivesOn, + style: TextStyle( + fontFamily: 'JetBrainsMono', + fontSize: 15, + color: textMuted, + fontStyle: FontStyle.italic, + ), + ), + ], + ); + } + + Widget _buildTheEnd(BuildContext context, Color gold) { + const theEnd = ''' +████████╗██╗ ██╗███████╗ ███████╗███╗ ██╗██████╗ + ╚══██╔══╝██║ ██║██╔════╝ ██╔════╝████╗ ██║██╔══██╗ + ██║ ███████║█████╗ █████╗ ██╔██╗ ██║██║ ██║ + ██║ ██╔══██║██╔══╝ ██╔══╝ ██║╚██╗██║██║ ██║ + ██║ ██║ ██║███████╗ ███████╗██║ ╚████║██████╔╝ + ╚═╝ ╚═╝ ╚═╝╚══════╝ ╚══════╝╚═╝ ╚═══╝╚═════╝ '''; + + // FittedBox로 감싸서 화면 너비에 맞게 자동 스케일링(scaling) + return FittedBox( + fit: BoxFit.scaleDown, + child: Text( + theEnd, + style: TextStyle( + fontFamily: 'JetBrainsMono', + fontSize: 12, + color: gold, + height: 1.0, + ), + textAlign: TextAlign.center, + ), + ); + } + + /// 명예의 전당(Hall of Fame) 버튼 + Widget _buildHallOfFameButton(BuildContext context, Color gold) { + final l10n = L10n.of(context); + + return Column( + children: [ + Text( + l10n.endingHallOfFameLine1, + style: TextStyle( + fontFamily: 'JetBrainsMono', + fontSize: 17, + color: gold.withValues(alpha: 0.7), + ), + ), + const SizedBox(height: 4), + Text( + l10n.endingHallOfFameLine2, + style: TextStyle( + fontFamily: 'JetBrainsMono', + fontSize: 17, + color: gold.withValues(alpha: 0.7), + ), + ), + const SizedBox(height: 24), + SizedBox( + width: 280, + height: 56, + child: ElevatedButton( + onPressed: 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: 14, + ), + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/src/features/game/widgets/victory_overlay.dart b/lib/src/features/game/widgets/victory_overlay.dart index 2d0e9de..a26a5a0 100644 --- a/lib/src/features/game/widgets/victory_overlay.dart +++ b/lib/src/features/game/widgets/victory_overlay.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:asciineverdie/l10n/app_localizations.dart'; -import 'package:asciineverdie/src/shared/l10n/game_data_l10n.dart'; import 'package:asciineverdie/src/core/model/game_state.dart'; +import 'package:asciineverdie/src/features/game/widgets/victory_credit_content.dart'; import 'package:asciineverdie/src/shared/retro_colors.dart'; -/// 게임 클리어 엔딩 오버레이 (Act V 완료 시) +/// 게임 클리어 엔딩 오버레이(overlay) - Act V 완료 시 /// /// 영화 엔딩 크레딧 스타일로 텍스트가 아래에서 위로 스크롤됨 /// - 탭/클릭 시 스크롤 최하단으로 즉시 이동 @@ -25,7 +25,7 @@ class VictoryOverlay extends StatefulWidget { final ProgressState progress; final int elapsedMs; - /// 엔딩 완료 콜백 (명예의 전당으로 이동) + /// 엔딩 완료 콜백(callback) - 명예의 전당으로 이동 final VoidCallback onComplete; @override @@ -97,7 +97,7 @@ class _VictoryOverlayState extends State }); } - /// 터치 시작 - 스크롤 속도업 + /// 터치 시작 - 스크롤 속도업(speed up) void _onTouchStart() { if (_isScrollComplete) return; @@ -105,7 +105,7 @@ class _VictoryOverlayState extends State _isTouching = true; }); - // 현재 진행도 저장 + // 현재 진행도(progress) 저장 final currentProgress = _animationController.value; final remainingProgress = 1.0 - currentProgress; @@ -225,7 +225,7 @@ class _VictoryOverlayState extends State final screenHeight = MediaQuery.of(context).size.height; final contentHeight = _estimateContentHeight(); - // 스크롤 오프셋: 화면 아래에서 시작 → 화면 위로 사라짐 + // 스크롤 오프셋(offset): 화면 아래에서 시작 → 화면 위로 사라짐 final totalScrollDistance = screenHeight + contentHeight; final currentOffset = screenHeight - (_scrollAnimation.value * totalScrollDistance); @@ -234,7 +234,7 @@ class _VictoryOverlayState extends State physics: const NeverScrollableScrollPhysics(), child: Transform.translate( offset: Offset(0, currentOffset), - child: _buildCreditContent(context), + child: _buildCreditContent(), ), ); } @@ -244,491 +244,22 @@ class _VictoryOverlayState extends State return SingleChildScrollView( controller: _manualScrollController, physics: const BouncingScrollPhysics(), - child: _buildCreditContent(context), + child: _buildCreditContent(), ); } 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: 16, - color: gold, - letterSpacing: 2, - ), - ), - const SizedBox(height: 16), - Text( - l10n.endingGameComplete, - style: TextStyle( - fontFamily: 'JetBrainsMono', - fontSize: 18, - 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: 14, - color: gold, - height: 1.0, - ), - textAlign: TextAlign.center, - ); - } - - Widget _buildSectionTitle(String title, Color gold) { - return Column( - children: [ - Text( - '═══════════════════', - style: TextStyle( - fontFamily: 'JetBrainsMono', - fontSize: 17, - color: gold.withValues(alpha: 0.5), - ), - ), - const SizedBox(height: 8), - Text( - title, - style: TextStyle( - fontFamily: 'PressStart2P', - fontSize: 15, - color: gold, - letterSpacing: 2, - ), - ), - const SizedBox(height: 8), - Text( - '═══════════════════', - style: TextStyle( - fontFamily: 'JetBrainsMono', - fontSize: 17, - 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: 18, - color: gold, - ), - ), - const SizedBox(height: 12), - // 레벨, 종족, 직업 - Text( - l10n.endingLevelFormat(widget.traits.level), - style: TextStyle( - fontFamily: 'PressStart2P', - fontSize: 14, - color: textPrimary, - ), - ), - const SizedBox(height: 8), - Text( - '${GameDataL10n.getRaceName(context, widget.traits.race)} ' - '${GameDataL10n.getKlassName(context, widget.traits.klass)}', - style: TextStyle( - fontFamily: 'JetBrainsMono', - fontSize: 17, - 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: 17, - color: labelColor.withValues(alpha: 0.7), - ), - ), - Text( - value, - style: TextStyle( - fontFamily: 'PressStart2P', - fontSize: 14, - 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: 15, - color: color.withValues(alpha: 0.5), - ), - ), - const SizedBox(height: 4), - Text( - value, - style: TextStyle( - fontFamily: 'PressStart2P', - fontSize: 15, - color: color, - ), - ), - ], - ); - } - - Widget _buildTrophyAsciiArt(Color gold) { - // 중앙 정렬을 위해 각 줄 좌우 공백 균형 맞춤 - const trophy = ''' - ____________ - '._==_==_=_.' - .-\\: /-. - | (|:. |) | - '-|:. |-' - \\::. / - '::. .' - ) ( - _.' '._ - '-------' '''; - - return Text( - trophy, - style: TextStyle( - fontFamily: 'JetBrainsMono', - fontSize: 17, - 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: 15, - color: gold, - ), - ), - const SizedBox(height: 16), - Text( - l10n.endingThankYou, - style: TextStyle( - fontFamily: 'JetBrainsMono', - fontSize: 17, - color: textPrimary, - ), - ), - const SizedBox(height: 8), - Text( - l10n.endingLegendLivesOn, - style: TextStyle( - fontFamily: 'JetBrainsMono', - fontSize: 15, - color: textMuted, - fontStyle: FontStyle.italic, - ), - ), - ], - ); - } - - Widget _buildTheEnd(BuildContext context, Color gold) { - const theEnd = ''' -████████╗██╗ ██╗███████╗ ███████╗███╗ ██╗██████╗ - ╚══██╔══╝██║ ██║██╔════╝ ██╔════╝████╗ ██║██╔══██╗ - ██║ ███████║█████╗ █████╗ ██╔██╗ ██║██║ ██║ - ██║ ██╔══██║██╔══╝ ██╔══╝ ██║╚██╗██║██║ ██║ - ██║ ██║ ██║███████╗ ███████╗██║ ╚████║██████╔╝ - ╚═╝ ╚═╝ ╚═╝╚══════╝ ╚══════╝╚═╝ ╚═══╝╚═════╝ '''; - - // FittedBox로 감싸서 화면 너비에 맞게 자동 스케일링 - return FittedBox( - fit: BoxFit.scaleDown, - child: Text( - theEnd, - style: TextStyle( - fontFamily: 'JetBrainsMono', - fontSize: 12, - 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: 17, - color: gold.withValues(alpha: 0.7), - ), - ), - const SizedBox(height: 4), - Text( - l10n.endingHallOfFameLine2, - style: TextStyle( - fontFamily: 'JetBrainsMono', - fontSize: 17, - 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: 14, - ), - ), - ], - ), - ), - ), - ], + Widget _buildCreditContent() { + return VictoryCreditContent( + traits: widget.traits, + stats: widget.stats, + progress: widget.progress, + elapsedMs: widget.elapsedMs, + onComplete: widget.onComplete, ); } }