refactor(game): VictoryOverlay에서 크레딧 콘텐츠 분리
- victory_overlay.dart 734줄 → 265줄 - victory_credit_content.dart 475줄 신규 생성
This commit is contained in:
475
lib/src/features/game/widgets/victory_credit_content.dart
Normal file
475
lib/src/features/game/widgets/victory_credit_content.dart
Normal file
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:asciineverdie/l10n/app_localizations.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/core/model/game_state.dart';
|
||||||
|
import 'package:asciineverdie/src/features/game/widgets/victory_credit_content.dart';
|
||||||
import 'package:asciineverdie/src/shared/retro_colors.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 ProgressState progress;
|
||||||
final int elapsedMs;
|
final int elapsedMs;
|
||||||
|
|
||||||
/// 엔딩 완료 콜백 (명예의 전당으로 이동)
|
/// 엔딩 완료 콜백(callback) - 명예의 전당으로 이동
|
||||||
final VoidCallback onComplete;
|
final VoidCallback onComplete;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -97,7 +97,7 @@ class _VictoryOverlayState extends State<VictoryOverlay>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 터치 시작 - 스크롤 속도업
|
/// 터치 시작 - 스크롤 속도업(speed up)
|
||||||
void _onTouchStart() {
|
void _onTouchStart() {
|
||||||
if (_isScrollComplete) return;
|
if (_isScrollComplete) return;
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ class _VictoryOverlayState extends State<VictoryOverlay>
|
|||||||
_isTouching = true;
|
_isTouching = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 현재 진행도 저장
|
// 현재 진행도(progress) 저장
|
||||||
final currentProgress = _animationController.value;
|
final currentProgress = _animationController.value;
|
||||||
final remainingProgress = 1.0 - currentProgress;
|
final remainingProgress = 1.0 - currentProgress;
|
||||||
|
|
||||||
@@ -225,7 +225,7 @@ class _VictoryOverlayState extends State<VictoryOverlay>
|
|||||||
final screenHeight = MediaQuery.of(context).size.height;
|
final screenHeight = MediaQuery.of(context).size.height;
|
||||||
final contentHeight = _estimateContentHeight();
|
final contentHeight = _estimateContentHeight();
|
||||||
|
|
||||||
// 스크롤 오프셋: 화면 아래에서 시작 → 화면 위로 사라짐
|
// 스크롤 오프셋(offset): 화면 아래에서 시작 → 화면 위로 사라짐
|
||||||
final totalScrollDistance = screenHeight + contentHeight;
|
final totalScrollDistance = screenHeight + contentHeight;
|
||||||
final currentOffset =
|
final currentOffset =
|
||||||
screenHeight - (_scrollAnimation.value * totalScrollDistance);
|
screenHeight - (_scrollAnimation.value * totalScrollDistance);
|
||||||
@@ -234,7 +234,7 @@ class _VictoryOverlayState extends State<VictoryOverlay>
|
|||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
child: Transform.translate(
|
child: Transform.translate(
|
||||||
offset: Offset(0, currentOffset),
|
offset: Offset(0, currentOffset),
|
||||||
child: _buildCreditContent(context),
|
child: _buildCreditContent(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -244,491 +244,22 @@ class _VictoryOverlayState extends State<VictoryOverlay>
|
|||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
controller: _manualScrollController,
|
controller: _manualScrollController,
|
||||||
physics: const BouncingScrollPhysics(),
|
physics: const BouncingScrollPhysics(),
|
||||||
child: _buildCreditContent(context),
|
child: _buildCreditContent(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
double _estimateContentHeight() {
|
double _estimateContentHeight() {
|
||||||
// 대략적인 콘텐츠 높이 추정 (스크롤 계산용)
|
// 대략적인 콘텐츠 높이 추정 (스크롤 계산용)
|
||||||
// 명예의 전당 버튼 추가로 인해 높이 증가
|
|
||||||
return 1600.0;
|
return 1600.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCreditContent(BuildContext context) {
|
Widget _buildCreditContent() {
|
||||||
final l10n = L10n.of(context);
|
return VictoryCreditContent(
|
||||||
final gold = RetroColors.goldOf(context);
|
traits: widget.traits,
|
||||||
final textPrimary = RetroColors.textPrimaryOf(context);
|
stats: widget.stats,
|
||||||
|
progress: widget.progress,
|
||||||
return Center(
|
elapsedMs: widget.elapsedMs,
|
||||||
child: Container(
|
onComplete: widget.onComplete,
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user