785 lines
24 KiB
Dart
785 lines
24 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import 'package:asciineverdie/data/game_text_l10n.dart' as game_l10n;
|
|
import 'package:asciineverdie/l10n/app_localizations.dart';
|
|
import 'package:asciineverdie/src/core/audio/audio_service.dart';
|
|
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
|
import 'package:asciineverdie/src/core/engine/game_mutations.dart';
|
|
import 'package:asciineverdie/src/core/engine/progress_service.dart';
|
|
import 'package:asciineverdie/src/core/engine/reward_service.dart';
|
|
import 'package:asciineverdie/src/core/model/game_state.dart';
|
|
import 'package:asciineverdie/src/core/model/pq_config.dart';
|
|
import 'package:asciineverdie/src/core/notification/notification_service.dart';
|
|
import 'package:asciineverdie/src/core/storage/save_manager.dart';
|
|
import 'package:asciineverdie/src/core/storage/save_repository.dart';
|
|
import 'package:asciineverdie/src/core/storage/settings_repository.dart';
|
|
import 'package:asciineverdie/src/features/front/front_screen.dart';
|
|
import 'package:asciineverdie/src/features/front/save_picker_dialog.dart';
|
|
import 'package:asciineverdie/src/features/game/game_play_screen.dart';
|
|
import 'package:asciineverdie/src/features/game/game_session_controller.dart';
|
|
import 'package:asciineverdie/src/features/game/widgets/notification_overlay.dart';
|
|
import 'package:asciineverdie/src/features/hall_of_fame/hall_of_fame_screen.dart';
|
|
import 'package:asciineverdie/src/features/new_character/new_character_screen.dart';
|
|
|
|
class AskiiNeverDieApp extends StatefulWidget {
|
|
const AskiiNeverDieApp({super.key});
|
|
|
|
@override
|
|
State<AskiiNeverDieApp> createState() => _AskiiNeverDieAppState();
|
|
}
|
|
|
|
class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
|
|
late final GameSessionController _controller;
|
|
late final NotificationService _notificationService;
|
|
late final SettingsRepository _settingsRepository;
|
|
late final AudioService _audioService;
|
|
bool _isCheckingSave = true;
|
|
bool _hasSave = false;
|
|
ThemeMode _themeMode = ThemeMode.system;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
const config = PqConfig();
|
|
final mutations = GameMutations(config);
|
|
final rewards = RewardService(mutations);
|
|
|
|
_controller = GameSessionController(
|
|
progressService: ProgressService(
|
|
config: config,
|
|
mutations: mutations,
|
|
rewards: rewards,
|
|
),
|
|
saveManager: SaveManager(SaveRepository()),
|
|
);
|
|
_notificationService = NotificationService();
|
|
_settingsRepository = SettingsRepository();
|
|
_audioService = AudioService(settingsRepository: _settingsRepository);
|
|
|
|
// 초기 설정 및 오디오 서비스 로드
|
|
_loadSettings();
|
|
_audioService.init();
|
|
// 세이브 파일 존재 여부 확인
|
|
_checkForExistingSave();
|
|
}
|
|
|
|
/// 저장된 설정 불러오기
|
|
Future<void> _loadSettings() async {
|
|
final themeMode = await _settingsRepository.loadThemeMode();
|
|
if (mounted) {
|
|
setState(() => _themeMode = themeMode);
|
|
}
|
|
}
|
|
|
|
/// 테마 모드 변경
|
|
void _changeThemeMode(ThemeMode mode) {
|
|
setState(() => _themeMode = mode);
|
|
_settingsRepository.saveThemeMode(mode);
|
|
}
|
|
|
|
/// 세이브 파일 존재 여부 확인 후 자동 로드
|
|
Future<void> _checkForExistingSave() async {
|
|
final exists = await _controller.saveManager.saveExists();
|
|
if (mounted) {
|
|
setState(() {
|
|
_hasSave = exists;
|
|
_isCheckingSave = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
_notificationService.dispose();
|
|
_audioService.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
/// 라이트 테마 (Classic Parchment 스타일)
|
|
ThemeData get _lightTheme => ThemeData(
|
|
colorScheme: RetroColors.lightColorScheme,
|
|
scaffoldBackgroundColor: const Color(0xFFFAF4ED),
|
|
useMaterial3: true,
|
|
// 카드/다이얼로그 레트로 배경
|
|
cardColor: const Color(0xFFF2E8DC),
|
|
dialogTheme: const DialogThemeData(
|
|
backgroundColor: Color(0xFFF2E8DC),
|
|
titleTextStyle: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 12,
|
|
color: Color(0xFFB8860B),
|
|
),
|
|
),
|
|
// 앱바 레트로 스타일
|
|
appBarTheme: const AppBarTheme(
|
|
backgroundColor: Color(0xFFF2E8DC),
|
|
foregroundColor: Color(0xFF1F1F28),
|
|
titleTextStyle: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 12,
|
|
color: Color(0xFFB8860B),
|
|
),
|
|
),
|
|
// 버튼 테마
|
|
filledButtonTheme: FilledButtonThemeData(
|
|
style: FilledButton.styleFrom(
|
|
backgroundColor: const Color(0xFFE8DDD0),
|
|
foregroundColor: const Color(0xFF1F1F28),
|
|
textStyle: const TextStyle(
|
|
inherit: false,
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 10,
|
|
color: Color(0xFF1F1F28),
|
|
),
|
|
),
|
|
),
|
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: const Color(0xFFB8860B),
|
|
side: const BorderSide(color: Color(0xFFB8860B), width: 2),
|
|
textStyle: const TextStyle(
|
|
inherit: false,
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 10,
|
|
color: Color(0xFFB8860B),
|
|
),
|
|
),
|
|
),
|
|
textButtonTheme: TextButtonThemeData(
|
|
style: TextButton.styleFrom(
|
|
foregroundColor: const Color(0xFF4A4458),
|
|
textStyle: const TextStyle(
|
|
inherit: false,
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 10,
|
|
color: Color(0xFF4A4458),
|
|
),
|
|
),
|
|
),
|
|
// 텍스트 테마
|
|
textTheme: const TextTheme(
|
|
headlineLarge: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 18,
|
|
color: Color(0xFFB8860B),
|
|
),
|
|
headlineMedium: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 14,
|
|
color: Color(0xFFB8860B),
|
|
),
|
|
headlineSmall: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 12,
|
|
color: Color(0xFFB8860B),
|
|
),
|
|
titleLarge: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 12,
|
|
color: Color(0xFF1F1F28),
|
|
),
|
|
titleMedium: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 10,
|
|
color: Color(0xFF1F1F28),
|
|
),
|
|
titleSmall: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 8,
|
|
color: Color(0xFF1F1F28),
|
|
),
|
|
bodyLarge: TextStyle(fontSize: 14, color: Color(0xFF1F1F28)),
|
|
bodyMedium: TextStyle(fontSize: 12, color: Color(0xFF1F1F28)),
|
|
bodySmall: TextStyle(fontSize: 10, color: Color(0xFF1F1F28)),
|
|
labelLarge: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 10,
|
|
color: Color(0xFF1F1F28),
|
|
),
|
|
labelMedium: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 8,
|
|
color: Color(0xFF1F1F28),
|
|
),
|
|
labelSmall: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 6,
|
|
color: Color(0xFF1F1F28),
|
|
),
|
|
),
|
|
// 칩 테마
|
|
chipTheme: const ChipThemeData(
|
|
backgroundColor: Color(0xFFE8DDD0),
|
|
labelStyle: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 8,
|
|
color: Color(0xFF1F1F28),
|
|
),
|
|
side: BorderSide(color: Color(0xFF8B7355)),
|
|
),
|
|
// 리스트 타일 테마
|
|
listTileTheme: const ListTileThemeData(
|
|
textColor: Color(0xFF1F1F28),
|
|
iconColor: Color(0xFFB8860B),
|
|
),
|
|
// 프로그레스 인디케이터
|
|
progressIndicatorTheme: const ProgressIndicatorThemeData(
|
|
color: Color(0xFFB8860B),
|
|
linearTrackColor: Color(0xFFD4C4B0),
|
|
),
|
|
);
|
|
|
|
/// 다크 테마 (Dark Fantasy 스타일)
|
|
ThemeData get _darkTheme => ThemeData(
|
|
colorScheme: RetroColors.darkColorScheme,
|
|
scaffoldBackgroundColor: RetroColors.deepBrown,
|
|
useMaterial3: true,
|
|
// 카드/다이얼로그 레트로 배경
|
|
cardColor: RetroColors.darkBrown,
|
|
dialogTheme: const DialogThemeData(
|
|
backgroundColor: Color(0xFF24283B),
|
|
titleTextStyle: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 12,
|
|
color: Color(0xFFE0AF68),
|
|
),
|
|
),
|
|
// 앱바 레트로 스타일
|
|
appBarTheme: const AppBarTheme(
|
|
backgroundColor: Color(0xFF24283B),
|
|
foregroundColor: Color(0xFFC0CAF5),
|
|
titleTextStyle: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 12,
|
|
color: Color(0xFFE0AF68),
|
|
),
|
|
),
|
|
// 버튼 테마 (inherit: false로 애니메이션 lerp 오류 방지)
|
|
filledButtonTheme: FilledButtonThemeData(
|
|
style: FilledButton.styleFrom(
|
|
backgroundColor: const Color(0xFF3D4260),
|
|
foregroundColor: const Color(0xFFC0CAF5),
|
|
textStyle: const TextStyle(
|
|
inherit: false,
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 10,
|
|
color: Color(0xFFC0CAF5),
|
|
),
|
|
),
|
|
),
|
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: const Color(0xFFE0AF68),
|
|
side: const BorderSide(color: Color(0xFFE0AF68), width: 2),
|
|
textStyle: const TextStyle(
|
|
inherit: false,
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 10,
|
|
color: Color(0xFFE0AF68),
|
|
),
|
|
),
|
|
),
|
|
textButtonTheme: TextButtonThemeData(
|
|
style: TextButton.styleFrom(
|
|
foregroundColor: const Color(0xFFC0CAF5),
|
|
textStyle: const TextStyle(
|
|
inherit: false,
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 10,
|
|
color: Color(0xFFC0CAF5),
|
|
),
|
|
),
|
|
),
|
|
// 텍스트 테마
|
|
textTheme: const TextTheme(
|
|
headlineLarge: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 18,
|
|
color: Color(0xFFE0AF68),
|
|
),
|
|
headlineMedium: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 14,
|
|
color: Color(0xFFE0AF68),
|
|
),
|
|
headlineSmall: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 12,
|
|
color: Color(0xFFE0AF68),
|
|
),
|
|
titleLarge: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 12,
|
|
color: Color(0xFFC0CAF5),
|
|
),
|
|
titleMedium: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 10,
|
|
color: Color(0xFFC0CAF5),
|
|
),
|
|
titleSmall: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 8,
|
|
color: Color(0xFFC0CAF5),
|
|
),
|
|
bodyLarge: TextStyle(fontSize: 14, color: Color(0xFFC0CAF5)),
|
|
bodyMedium: TextStyle(fontSize: 12, color: Color(0xFFC0CAF5)),
|
|
bodySmall: TextStyle(fontSize: 10, color: Color(0xFFC0CAF5)),
|
|
labelLarge: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 10,
|
|
color: Color(0xFFC0CAF5),
|
|
),
|
|
labelMedium: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 8,
|
|
color: Color(0xFFC0CAF5),
|
|
),
|
|
labelSmall: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 6,
|
|
color: Color(0xFFC0CAF5),
|
|
),
|
|
),
|
|
// 칩 테마
|
|
chipTheme: const ChipThemeData(
|
|
backgroundColor: Color(0xFF2A2E3F),
|
|
labelStyle: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 8,
|
|
color: Color(0xFFC0CAF5),
|
|
),
|
|
side: BorderSide(color: Color(0xFF545C7E)),
|
|
),
|
|
// 리스트 타일 테마
|
|
listTileTheme: const ListTileThemeData(
|
|
textColor: Color(0xFFC0CAF5),
|
|
iconColor: Color(0xFFE0AF68),
|
|
),
|
|
// 프로그레스 인디케이터
|
|
progressIndicatorTheme: const ProgressIndicatorThemeData(
|
|
color: Color(0xFFE0AF68),
|
|
linearTrackColor: Color(0xFF3B4261),
|
|
),
|
|
);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MaterialApp(
|
|
title: 'ASCII NEVER DIE',
|
|
debugShowCheckedModeBanner: false,
|
|
localizationsDelegates: L10n.localizationsDelegates,
|
|
supportedLocales: L10n.supportedLocales,
|
|
theme: _lightTheme,
|
|
darkTheme: _darkTheme,
|
|
themeMode: _themeMode,
|
|
builder: (context, child) {
|
|
// 현재 로케일을 게임 텍스트 l10n 시스템에 동기화
|
|
final locale = Localizations.localeOf(context);
|
|
game_l10n.setGameLocale(locale.languageCode);
|
|
return child ?? const SizedBox.shrink();
|
|
},
|
|
home: NotificationOverlay(
|
|
notificationService: _notificationService,
|
|
child: _buildHomeScreen(),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 홈 화면 결정: 세이브 확인 중 → 스플래시, 세이브 있음 → 자동 로드, 없음 → 프론트
|
|
Widget _buildHomeScreen() {
|
|
// 세이브 확인 중이면 로딩 스플래시 표시
|
|
if (_isCheckingSave) {
|
|
return const _SplashScreen();
|
|
}
|
|
|
|
// 세이브 파일이 있으면 자동 로드 화면
|
|
if (_hasSave) {
|
|
return _AutoLoadScreen(
|
|
controller: _controller,
|
|
onLoadFailed: () {
|
|
// 로드 실패 시 프론트 화면으로
|
|
setState(() => _hasSave = false);
|
|
},
|
|
currentThemeMode: _themeMode,
|
|
onThemeModeChange: _changeThemeMode,
|
|
audioService: _audioService,
|
|
);
|
|
}
|
|
|
|
// 세이브 파일이 없으면 기존 프론트 화면 (타이틀 BGM 재생)
|
|
_audioService.playBgm('title');
|
|
return FrontScreen(
|
|
onNewCharacter: _navigateToNewCharacter,
|
|
onLoadSave: _loadSave,
|
|
onHallOfFame: _navigateToHallOfFame,
|
|
hasSaveFile: _hasSave,
|
|
);
|
|
}
|
|
|
|
void _navigateToNewCharacter(BuildContext context) {
|
|
Navigator.of(context).push(
|
|
MaterialPageRoute<void>(
|
|
builder: (context) => NewCharacterScreen(
|
|
onCharacterCreated: (initialState, {bool testMode = false}) {
|
|
_startGame(context, initialState, testMode: testMode);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _loadSave(BuildContext context) async {
|
|
// 저장 파일 목록 조회
|
|
final saves = await _controller.saveManager.listSaves();
|
|
|
|
if (!context.mounted) return;
|
|
|
|
String? selectedFileName;
|
|
|
|
if (saves.isEmpty) {
|
|
// 저장 파일이 없으면 안내 메시지
|
|
_notificationService.showInfo(L10n.of(context).noSavedGames);
|
|
return;
|
|
} else if (saves.length == 1) {
|
|
// 파일이 하나면 바로 선택
|
|
selectedFileName = saves.first.fileName;
|
|
} else {
|
|
// 여러 개면 다이얼로그 표시
|
|
selectedFileName = await SavePickerDialog.show(context, saves);
|
|
}
|
|
|
|
if (selectedFileName == null || !context.mounted) return;
|
|
|
|
// 선택된 파일 로드
|
|
await _controller.loadAndStart(
|
|
fileName: selectedFileName,
|
|
cheatsEnabled: false,
|
|
);
|
|
|
|
if (_controller.status == GameSessionStatus.running) {
|
|
if (context.mounted) {
|
|
_navigateToGame(context);
|
|
}
|
|
} else if (_controller.status == GameSessionStatus.error &&
|
|
context.mounted) {
|
|
_notificationService.showWarning(
|
|
L10n.of(context).loadError(_controller.error ?? 'Unknown error'),
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> _startGame(
|
|
BuildContext context,
|
|
GameState initialState, {
|
|
bool testMode = false,
|
|
}) async {
|
|
await _controller.startNew(initialState, cheatsEnabled: testMode);
|
|
|
|
if (context.mounted) {
|
|
// NewCharacterScreen을 pop하고 GamePlayScreen으로 이동
|
|
Navigator.of(context).pushReplacement(
|
|
MaterialPageRoute<void>(
|
|
builder: (context) => GamePlayScreen(
|
|
controller: _controller,
|
|
audioService: _audioService,
|
|
forceCarouselLayout: testMode,
|
|
currentThemeMode: _themeMode,
|
|
onThemeModeChange: _changeThemeMode,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
void _navigateToGame(BuildContext context) {
|
|
Navigator.of(context).push(
|
|
MaterialPageRoute<void>(
|
|
builder: (context) => GamePlayScreen(
|
|
controller: _controller,
|
|
audioService: _audioService,
|
|
currentThemeMode: _themeMode,
|
|
onThemeModeChange: _changeThemeMode,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Phase 10: 명예의 전당 화면으로 이동
|
|
void _navigateToHallOfFame(BuildContext context) {
|
|
Navigator.of(context).push(
|
|
MaterialPageRoute<void>(builder: (context) => const HallOfFameScreen()),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 스플래시 화면 (세이브 파일 확인 중) - 레트로 스타일
|
|
class _SplashScreen extends StatelessWidget {
|
|
const _SplashScreen();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: RetroColors.deepBrown,
|
|
body: Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
// 타이틀 로고
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
|
decoration: BoxDecoration(
|
|
color: RetroColors.panelBg,
|
|
border: Border.all(color: RetroColors.gold, width: 3),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
// 아이콘
|
|
const Icon(
|
|
Icons.auto_awesome,
|
|
size: 32,
|
|
color: RetroColors.gold,
|
|
),
|
|
const SizedBox(height: 12),
|
|
// 타이틀
|
|
const Text(
|
|
'ASCII',
|
|
style: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 20,
|
|
color: RetroColors.gold,
|
|
shadows: [
|
|
Shadow(
|
|
color: RetroColors.goldDark,
|
|
offset: Offset(2, 2),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
const Text(
|
|
'NEVER DIE',
|
|
style: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 14,
|
|
color: RetroColors.cream,
|
|
shadows: [
|
|
Shadow(
|
|
color: RetroColors.brown,
|
|
offset: Offset(1, 1),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 32),
|
|
// 레트로 로딩 바
|
|
SizedBox(
|
|
width: 160,
|
|
child: _RetroLoadingBar(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 자동 로드 화면 (세이브 파일 자동 로드) - 레트로 스타일
|
|
class _AutoLoadScreen extends StatefulWidget {
|
|
const _AutoLoadScreen({
|
|
required this.controller,
|
|
required this.onLoadFailed,
|
|
required this.currentThemeMode,
|
|
required this.onThemeModeChange,
|
|
this.audioService,
|
|
});
|
|
|
|
final GameSessionController controller;
|
|
final VoidCallback onLoadFailed;
|
|
final ThemeMode currentThemeMode;
|
|
final void Function(ThemeMode mode) onThemeModeChange;
|
|
final AudioService? audioService;
|
|
|
|
@override
|
|
State<_AutoLoadScreen> createState() => _AutoLoadScreenState();
|
|
}
|
|
|
|
class _AutoLoadScreenState extends State<_AutoLoadScreen> {
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
// 로딩 중에도 타이틀 BGM 재생
|
|
widget.audioService?.playBgm('title');
|
|
_autoLoad();
|
|
}
|
|
|
|
Future<void> _autoLoad() async {
|
|
await widget.controller.loadAndStart(cheatsEnabled: false);
|
|
|
|
if (!mounted) return;
|
|
|
|
if (widget.controller.status == GameSessionStatus.running) {
|
|
// 로드 성공 → 게임 화면으로 교체
|
|
Navigator.of(context).pushReplacement(
|
|
MaterialPageRoute<void>(
|
|
builder: (context) => GamePlayScreen(
|
|
controller: widget.controller,
|
|
audioService: widget.audioService,
|
|
currentThemeMode: widget.currentThemeMode,
|
|
onThemeModeChange: widget.onThemeModeChange,
|
|
),
|
|
),
|
|
);
|
|
} else {
|
|
// 로드 실패 → 프론트 화면으로 돌아가기
|
|
widget.onLoadFailed();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: RetroColors.deepBrown,
|
|
body: Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
// 타이틀 로고
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
|
decoration: BoxDecoration(
|
|
color: RetroColors.panelBg,
|
|
border: Border.all(color: RetroColors.gold, width: 3),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
// 아이콘
|
|
const Icon(
|
|
Icons.auto_awesome,
|
|
size: 32,
|
|
color: RetroColors.gold,
|
|
),
|
|
const SizedBox(height: 12),
|
|
// 타이틀
|
|
const Text(
|
|
'ASCII',
|
|
style: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 20,
|
|
color: RetroColors.gold,
|
|
shadows: [
|
|
Shadow(
|
|
color: RetroColors.goldDark,
|
|
offset: Offset(2, 2),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
const Text(
|
|
'NEVER DIE',
|
|
style: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 14,
|
|
color: RetroColors.cream,
|
|
shadows: [
|
|
Shadow(
|
|
color: RetroColors.brown,
|
|
offset: Offset(1, 1),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 32),
|
|
// 레트로 로딩 바
|
|
SizedBox(
|
|
width: 160,
|
|
child: _RetroLoadingBar(),
|
|
),
|
|
const SizedBox(height: 16),
|
|
// 로딩 텍스트
|
|
Text(
|
|
game_l10n.uiLoading,
|
|
style: const TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 8,
|
|
color: RetroColors.textDisabled,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 레트로 스타일 로딩 바 (애니메이션)
|
|
class _RetroLoadingBar extends StatefulWidget {
|
|
@override
|
|
State<_RetroLoadingBar> createState() => _RetroLoadingBarState();
|
|
}
|
|
|
|
class _RetroLoadingBarState extends State<_RetroLoadingBar>
|
|
with SingleTickerProviderStateMixin {
|
|
late AnimationController _controller;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = AnimationController(
|
|
duration: const Duration(milliseconds: 1500),
|
|
vsync: this,
|
|
)..repeat();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
const segmentCount = 10;
|
|
|
|
return AnimatedBuilder(
|
|
animation: _controller,
|
|
builder: (context, child) {
|
|
// 웨이브 효과: 각 세그먼트가 순차적으로 켜지고 꺼짐
|
|
return Container(
|
|
height: 16,
|
|
decoration: BoxDecoration(
|
|
color: RetroColors.panelBg,
|
|
border: Border.all(color: RetroColors.panelBorderOuter, width: 2),
|
|
),
|
|
child: Row(
|
|
children: List.generate(segmentCount, (index) {
|
|
// 웨이브 패턴 계산
|
|
final progress = _controller.value * segmentCount;
|
|
final distance = (index - progress).abs();
|
|
final isLit = distance < 2 || (segmentCount - distance) < 2;
|
|
final opacity = isLit ? 1.0 : 0.2;
|
|
|
|
return Expanded(
|
|
child: Container(
|
|
margin: const EdgeInsets.all(1),
|
|
decoration: BoxDecoration(
|
|
color: RetroColors.gold.withValues(alpha: opacity),
|
|
),
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|