feat(game): 게임 클리어 시 VictoryOverlay 추가
- VictoryOverlay 위젯 신규 생성 - GameSessionController에 isComplete 상태 추가 - 레벨 100 도달 시 승리 오버레이 표시 - 승리 후 명예의 전당 화면으로 이동
This commit is contained in:
@@ -627,9 +627,9 @@ class ProgressService {
|
|||||||
/// Advances plot to next act and applies any act-level rewards.
|
/// Advances plot to next act and applies any act-level rewards.
|
||||||
/// Returns gameComplete=true if Act V was completed (game ends).
|
/// Returns gameComplete=true if Act V was completed (game ends).
|
||||||
({GameState state, bool gameComplete}) completeAct(GameState state) {
|
({GameState state, bool gameComplete}) completeAct(GameState state) {
|
||||||
// Act V 완료 시 (plotStageCount == 5) 게임 클리어
|
// Act V 완료 시 (plotStageCount == 6) 게임 클리어
|
||||||
// plotStageCount: 0=Prologue, 1=Act I, 2=Act II, 3=Act III, 4=Act IV, 5=Act V
|
// plotStageCount: 1=Prologue, 2=Act I, 3=Act II, 4=Act III, 5=Act IV, 6=Act V
|
||||||
if (state.progress.plotStageCount >= 5) {
|
if (state.progress.plotStageCount >= 6) {
|
||||||
// Act V 완료 - 게임 클리어!
|
// Act V 완료 - 게임 클리어!
|
||||||
// 히스토리만 업데이트하고 새 Act는 생성하지 않음
|
// 히스토리만 업데이트하고 새 Act는 생성하지 않음
|
||||||
final updatedPlotHistory = [
|
final updatedPlotHistory = [
|
||||||
|
|||||||
@@ -12,18 +12,16 @@ import 'package:asciineverdie/src/core/animation/ascii_animation_type.dart';
|
|||||||
import 'package:asciineverdie/src/core/engine/story_service.dart';
|
import 'package:asciineverdie/src/core/engine/story_service.dart';
|
||||||
import 'package:asciineverdie/src/core/model/combat_event.dart';
|
import 'package:asciineverdie/src/core/model/combat_event.dart';
|
||||||
import 'package:asciineverdie/src/core/l10n/game_data_l10n.dart';
|
import 'package:asciineverdie/src/core/l10n/game_data_l10n.dart';
|
||||||
import 'package:asciineverdie/src/core/model/combat_stats.dart';
|
|
||||||
import 'package:asciineverdie/src/core/model/game_state.dart';
|
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||||
import 'package:asciineverdie/src/core/model/hall_of_fame.dart';
|
|
||||||
import 'package:asciineverdie/src/core/model/skill.dart';
|
import 'package:asciineverdie/src/core/model/skill.dart';
|
||||||
import 'package:asciineverdie/src/core/notification/notification_service.dart';
|
import 'package:asciineverdie/src/core/notification/notification_service.dart';
|
||||||
import 'package:asciineverdie/src/core/storage/hall_of_fame_storage.dart';
|
|
||||||
import 'package:asciineverdie/src/core/util/pq_logic.dart' as pq_logic;
|
import 'package:asciineverdie/src/core/util/pq_logic.dart' as pq_logic;
|
||||||
import 'package:asciineverdie/src/features/game/game_session_controller.dart';
|
import 'package:asciineverdie/src/features/game/game_session_controller.dart';
|
||||||
import 'package:asciineverdie/src/features/hall_of_fame/hall_of_fame_screen.dart';
|
import 'package:asciineverdie/src/features/hall_of_fame/hall_of_fame_screen.dart';
|
||||||
import 'package:asciineverdie/src/features/game/widgets/cinematic_view.dart';
|
import 'package:asciineverdie/src/features/game/widgets/cinematic_view.dart';
|
||||||
import 'package:asciineverdie/src/features/game/widgets/combat_log.dart';
|
import 'package:asciineverdie/src/features/game/widgets/combat_log.dart';
|
||||||
import 'package:asciineverdie/src/features/game/widgets/death_overlay.dart';
|
import 'package:asciineverdie/src/features/game/widgets/death_overlay.dart';
|
||||||
|
import 'package:asciineverdie/src/features/game/widgets/victory_overlay.dart';
|
||||||
import 'package:asciineverdie/src/features/game/widgets/hp_mp_bar.dart';
|
import 'package:asciineverdie/src/features/game/widgets/hp_mp_bar.dart';
|
||||||
import 'package:asciineverdie/src/features/game/widgets/notification_overlay.dart';
|
import 'package:asciineverdie/src/features/game/widgets/notification_overlay.dart';
|
||||||
import 'package:asciineverdie/src/features/game/widgets/stats_panel.dart';
|
import 'package:asciineverdie/src/features/game/widgets/stats_panel.dart';
|
||||||
@@ -138,14 +136,9 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
if (newAct != _lastAct && !_showingCinematic) {
|
if (newAct != _lastAct && !_showingCinematic) {
|
||||||
_lastAct = newAct;
|
_lastAct = newAct;
|
||||||
|
|
||||||
// Phase 10: 엔딩 도달 시 클리어 처리 (시네마틱 대신 클리어 다이얼로그)
|
// 엔딩은 controller.isComplete 상태에서 VictoryOverlay로 처리
|
||||||
// 다음 프레임에서 실행 (리스너 콜백 중 showDialog 문제 방지)
|
// 일반 Act 전환 시에만 시네마틱 표시
|
||||||
if (newAct == StoryAct.ending && state.traits.level >= 100) {
|
if (newAct != StoryAct.ending) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
if (mounted) _handleGameClear(state);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 일반 Act 전환 시 시네마틱 표시
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (mounted) _showCinematicForAct(newAct);
|
if (mounted) _showCinematicForAct(newAct);
|
||||||
});
|
});
|
||||||
@@ -419,49 +412,13 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
_showingCinematic = false;
|
_showingCinematic = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Phase 10: 게임 클리어 처리 (Handle Game Clear)
|
/// VictoryOverlay 완료 후 명예의 전당 화면으로 이동
|
||||||
Future<void> _handleGameClear(GameState state) async {
|
void _handleVictoryComplete() {
|
||||||
// 게임 일시 정지
|
Navigator.of(context).pushReplacement(
|
||||||
await widget.controller.pause(saveOnStop: true);
|
MaterialPageRoute<void>(
|
||||||
|
builder: (context) => const HallOfFameScreen(),
|
||||||
// 최종 전투 스탯 계산
|
),
|
||||||
final combatStats = CombatStats.fromStats(
|
|
||||||
stats: state.stats,
|
|
||||||
equipment: state.equipment,
|
|
||||||
level: state.traits.level,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 명예의 전당 엔트리 생성
|
|
||||||
final entry = HallOfFameEntry.fromGameState(
|
|
||||||
state: state,
|
|
||||||
totalDeaths: state.progress.deathCount,
|
|
||||||
monstersKilled: state.progress.monstersKilled,
|
|
||||||
combatStats: combatStats,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 명예의 전당에 저장
|
|
||||||
final storage = HallOfFameStorage();
|
|
||||||
await storage.addEntry(entry);
|
|
||||||
|
|
||||||
// 클리어 다이얼로그 표시
|
|
||||||
if (mounted) {
|
|
||||||
await showGameClearDialog(
|
|
||||||
context,
|
|
||||||
entry: entry,
|
|
||||||
onNewGame: () {
|
|
||||||
// 프론트 화면으로 돌아가기
|
|
||||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
|
||||||
},
|
|
||||||
onViewHallOfFame: () {
|
|
||||||
// 명예의 전당 화면으로 이동
|
|
||||||
Navigator.of(context).pushReplacement(
|
|
||||||
MaterialPageRoute<void>(
|
|
||||||
builder: (context) => const HallOfFameScreen(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _resetSpecialAnimationAfterFrame() {
|
void _resetSpecialAnimationAfterFrame() {
|
||||||
@@ -857,6 +814,15 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
traits: state.traits,
|
traits: state.traits,
|
||||||
onResurrect: _handleResurrect,
|
onResurrect: _handleResurrect,
|
||||||
),
|
),
|
||||||
|
// 승리 오버레이 (게임 클리어)
|
||||||
|
if (widget.controller.isComplete)
|
||||||
|
VictoryOverlay(
|
||||||
|
traits: state.traits,
|
||||||
|
stats: state.stats,
|
||||||
|
progress: state.progress,
|
||||||
|
elapsedMs: state.skillSystem.elapsedMs,
|
||||||
|
onComplete: _handleVictoryComplete,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -990,6 +956,15 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
traits: state.traits,
|
traits: state.traits,
|
||||||
onResurrect: _handleResurrect,
|
onResurrect: _handleResurrect,
|
||||||
),
|
),
|
||||||
|
// 승리 오버레이 (게임 클리어)
|
||||||
|
if (widget.controller.isComplete)
|
||||||
|
VictoryOverlay(
|
||||||
|
traits: state.traits,
|
||||||
|
stats: state.stats,
|
||||||
|
progress: state.progress,
|
||||||
|
elapsedMs: state.skillSystem.elapsedMs,
|
||||||
|
onComplete: _handleVictoryComplete,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:asciineverdie/src/core/engine/progress_loop.dart';
|
|||||||
import 'package:asciineverdie/src/core/engine/progress_service.dart';
|
import 'package:asciineverdie/src/core/engine/progress_service.dart';
|
||||||
import 'package:asciineverdie/src/core/engine/resurrection_service.dart';
|
import 'package:asciineverdie/src/core/engine/resurrection_service.dart';
|
||||||
import 'package:asciineverdie/src/core/engine/shop_service.dart';
|
import 'package:asciineverdie/src/core/engine/shop_service.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/combat_stats.dart';
|
||||||
import 'package:asciineverdie/src/core/model/game_state.dart';
|
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||||
import 'package:asciineverdie/src/core/model/game_statistics.dart';
|
import 'package:asciineverdie/src/core/model/game_statistics.dart';
|
||||||
import 'package:asciineverdie/src/core/model/hall_of_fame.dart';
|
import 'package:asciineverdie/src/core/model/hall_of_fame.dart';
|
||||||
@@ -285,18 +286,40 @@ class GameSessionController extends ChangeNotifier {
|
|||||||
|
|
||||||
/// 명예의 전당 등록
|
/// 명예의 전당 등록
|
||||||
Future<void> _registerToHallOfFame() async {
|
Future<void> _registerToHallOfFame() async {
|
||||||
if (_state == null) return;
|
if (_state == null) {
|
||||||
|
debugPrint('[HallOfFame] _state is null, skipping registration');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final entry = HallOfFameEntry.fromGameState(
|
try {
|
||||||
state: _state!,
|
debugPrint('[HallOfFame] Starting registration...');
|
||||||
totalDeaths: _sessionStats.deathCount,
|
|
||||||
monstersKilled: _state!.progress.monstersKilled,
|
|
||||||
);
|
|
||||||
|
|
||||||
await _hallOfFameStorage.addEntry(entry);
|
// 최종 전투 스탯 계산 (CombatStats)
|
||||||
|
final combatStats = CombatStats.fromStats(
|
||||||
|
stats: _state!.stats,
|
||||||
|
equipment: _state!.equipment,
|
||||||
|
level: _state!.traits.level,
|
||||||
|
);
|
||||||
|
|
||||||
// 통계 기록
|
final entry = HallOfFameEntry.fromGameState(
|
||||||
await _statisticsStorage.recordGameComplete();
|
state: _state!,
|
||||||
|
totalDeaths: _sessionStats.deathCount,
|
||||||
|
monstersKilled: _state!.progress.monstersKilled,
|
||||||
|
combatStats: combatStats,
|
||||||
|
);
|
||||||
|
|
||||||
|
debugPrint('[HallOfFame] Entry created: ${entry.characterName} Lv.${entry.level}');
|
||||||
|
|
||||||
|
final success = await _hallOfFameStorage.addEntry(entry);
|
||||||
|
debugPrint('[HallOfFame] Storage save result: $success');
|
||||||
|
|
||||||
|
// 통계 기록
|
||||||
|
await _statisticsStorage.recordGameComplete();
|
||||||
|
debugPrint('[HallOfFame] Registration complete');
|
||||||
|
} catch (e, st) {
|
||||||
|
debugPrint('[HallOfFame] ERROR: $e');
|
||||||
|
debugPrint('[HallOfFame] StackTrace: $st');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 플레이어 부활 처리 (상태만 업데이트, 게임 재개는 별도로)
|
/// 플레이어 부활 처리 (상태만 업데이트, 게임 재개는 별도로)
|
||||||
|
|||||||
572
lib/src/features/game/widgets/victory_overlay.dart
Normal file
572
lib/src/features/game/widgets/victory_overlay.dart
Normal file
@@ -0,0 +1,572 @@
|
|||||||
|
import 'package:flutter/material.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<VictoryOverlay> createState() => _VictoryOverlayState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VictoryOverlayState extends State<VictoryOverlay>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _scrollController;
|
||||||
|
late Animation<double> _scrollAnimation;
|
||||||
|
|
||||||
|
// 스크롤 지속 시간 (밀리초)
|
||||||
|
static const _scrollDurationMs = 25000; // 25초
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
_scrollController = AnimationController(
|
||||||
|
duration: const Duration(milliseconds: _scrollDurationMs),
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
|
||||||
|
_scrollAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||||
|
CurvedAnimation(parent: _scrollController, curve: Curves.linear),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 스크롤 완료 시 자동 종료
|
||||||
|
_scrollController.addStatusListener((status) {
|
||||||
|
if (status == AnimationStatus.completed) {
|
||||||
|
widget.onComplete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_scrollController.forward();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final gold = RetroColors.goldOf(context);
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: widget.onComplete, // 탭으로 스킵
|
||||||
|
child: Material(
|
||||||
|
color: Colors.black,
|
||||||
|
child: SafeArea(
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
// 스크롤되는 크레딧
|
||||||
|
AnimatedBuilder(
|
||||||
|
animation: _scrollAnimation,
|
||||||
|
builder: (context, child) {
|
||||||
|
return _buildScrollingCredits(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
// 스킵 버튼
|
||||||
|
Positioned(
|
||||||
|
top: 16,
|
||||||
|
right: 16,
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: widget.onComplete,
|
||||||
|
child: Text(
|
||||||
|
'SKIP',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 10,
|
||||||
|
color: gold.withValues(alpha: 0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 하단 탭 힌트
|
||||||
|
Positioned(
|
||||||
|
bottom: 16,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Text(
|
||||||
|
'TAP TO SKIP',
|
||||||
|
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 1500.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCreditContent(BuildContext 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(
|
||||||
|
'★ CONGRATULATIONS ★',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 14,
|
||||||
|
color: gold,
|
||||||
|
letterSpacing: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'You have completed the game!',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'JetBrainsMono',
|
||||||
|
fontSize: 14,
|
||||||
|
color: textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 80),
|
||||||
|
|
||||||
|
// ═══════════════════════════════════
|
||||||
|
// THE HERO
|
||||||
|
// ═══════════════════════════════════
|
||||||
|
_buildSectionTitle('THE HERO', gold),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
_buildHeroInfo(context),
|
||||||
|
const SizedBox(height: 80),
|
||||||
|
|
||||||
|
// ═══════════════════════════════════
|
||||||
|
// JOURNEY STATISTICS
|
||||||
|
// ═══════════════════════════════════
|
||||||
|
_buildSectionTitle('JOURNEY STATISTICS', gold),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
_buildStatistics(context),
|
||||||
|
const SizedBox(height: 80),
|
||||||
|
|
||||||
|
// ═══════════════════════════════════
|
||||||
|
// FINAL STATS
|
||||||
|
// ═══════════════════════════════════
|
||||||
|
_buildSectionTitle('FINAL STATS', gold),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
_buildFinalStats(context),
|
||||||
|
const SizedBox(height: 100),
|
||||||
|
|
||||||
|
// ═══════════════════════════════════
|
||||||
|
// ASCII TROPHY
|
||||||
|
// ═══════════════════════════════════
|
||||||
|
_buildTrophyAsciiArt(gold),
|
||||||
|
const SizedBox(height: 60),
|
||||||
|
|
||||||
|
// ═══════════════════════════════════
|
||||||
|
// CREDITS
|
||||||
|
// ═══════════════════════════════════
|
||||||
|
_buildSectionTitle('CREDITS', gold),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
_buildCredits(context),
|
||||||
|
const SizedBox(height: 100),
|
||||||
|
|
||||||
|
// ═══════════════════════════════════
|
||||||
|
// THE END
|
||||||
|
// ═══════════════════════════════════
|
||||||
|
_buildTheEnd(gold),
|
||||||
|
const SizedBox(height: 200), // 여백 (스크롤 끝)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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(
|
||||||
|
'Level ${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 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('Monsters Slain', '${widget.progress.monstersKilled}',
|
||||||
|
textPrimary, exp),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildStatLine('Quests Completed', '${widget.progress.questCount}',
|
||||||
|
textPrimary, exp),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildStatLine('Play Time', 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 textPrimary = RetroColors.textPrimaryOf(context);
|
||||||
|
final textMuted = RetroColors.textMutedOf(context);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Based on',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'JetBrainsMono',
|
||||||
|
fontSize: 10,
|
||||||
|
color: textMuted,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'Progress Quest',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 10,
|
||||||
|
color: textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'by Eric Fredricksen',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'JetBrainsMono',
|
||||||
|
fontSize: 10,
|
||||||
|
color: textMuted,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text(
|
||||||
|
'Flutter Port',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'JetBrainsMono',
|
||||||
|
fontSize: 10,
|
||||||
|
color: textMuted,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'ASCII Never Die',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 10,
|
||||||
|
color: textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTheEnd(Color gold) {
|
||||||
|
const theEnd = '''
|
||||||
|
████████╗██╗ ██╗███████╗ ███████╗███╗ ██╗██████╗
|
||||||
|
╚══██╔══╝██║ ██║██╔════╝ ██╔════╝████╗ ██║██╔══██╗
|
||||||
|
██║ ███████║█████╗ █████╗ ██╔██╗ ██║██║ ██║
|
||||||
|
██║ ██╔══██║██╔══╝ ██╔══╝ ██║╚██╗██║██║ ██║
|
||||||
|
██║ ██║ ██║███████╗ ███████╗██║ ╚████║██████╔╝
|
||||||
|
╚═╝ ╚═╝ ╚═╝╚══════╝ ╚══════╝╚═╝ ╚═══╝╚═════╝ ''';
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
theEnd,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'JetBrainsMono',
|
||||||
|
fontSize: 6,
|
||||||
|
color: gold,
|
||||||
|
height: 1.0,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text(
|
||||||
|
'Your heroic deeds will be',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'JetBrainsMono',
|
||||||
|
fontSize: 12,
|
||||||
|
color: gold.withValues(alpha: 0.7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'remembered in the Hall of Fame',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'JetBrainsMono',
|
||||||
|
fontSize: 12,
|
||||||
|
color: gold.withValues(alpha: 0.7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user