- GamePlayScreen 개선 - GameSessionController 확장 - MobileCarouselLayout 기능 추가 - SettingsScreen 테스트 기능 추가
800 lines
26 KiB
Dart
800 lines
26 KiB
Dart
import 'package:flutter/foundation.dart' show kDebugMode;
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:asciineverdie/src/core/notification/notification_service.dart';
|
|
import 'package:asciineverdie/data/game_text_l10n.dart' as l10n;
|
|
import 'package:asciineverdie/l10n/app_localizations.dart';
|
|
import 'package:asciineverdie/src/core/animation/ascii_animation_type.dart';
|
|
import 'package:asciineverdie/src/core/model/game_state.dart';
|
|
import 'package:asciineverdie/src/features/game/pages/character_sheet_page.dart';
|
|
import 'package:asciineverdie/src/features/game/pages/combat_log_page.dart';
|
|
import 'package:asciineverdie/src/features/game/pages/equipment_page.dart';
|
|
import 'package:asciineverdie/src/features/game/pages/inventory_page.dart';
|
|
import 'package:asciineverdie/src/features/game/pages/quest_page.dart';
|
|
import 'package:asciineverdie/src/features/game/pages/skills_page.dart';
|
|
import 'package:asciineverdie/src/features/game/pages/story_page.dart';
|
|
import 'package:asciineverdie/src/features/game/widgets/carousel_nav_bar.dart';
|
|
import 'package:asciineverdie/src/features/game/widgets/combat_log.dart';
|
|
import 'package:asciineverdie/src/features/game/widgets/enhanced_animation_panel.dart';
|
|
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
|
|
|
/// 모바일 캐로셀 레이아웃
|
|
///
|
|
/// 모바일 앱용 레이아웃:
|
|
/// - 상단: 확장 애니메이션 패널 (ASCII 애니메이션, HP/MP, 버프, 몬스터 HP)
|
|
/// - 중앙: 캐로셀 (7개 페이지: 스킬, 인벤토리, 장비, 캐릭터시트, 전투로그, 스토리, 퀘스트)
|
|
/// - 하단: 네비게이션 바 (7개 버튼)
|
|
class MobileCarouselLayout extends StatefulWidget {
|
|
const MobileCarouselLayout({
|
|
super.key,
|
|
required this.state,
|
|
required this.combatLogEntries,
|
|
required this.speedMultiplier,
|
|
required this.onSpeedCycle,
|
|
required this.isPaused,
|
|
required this.onPauseToggle,
|
|
required this.onSave,
|
|
required this.onExit,
|
|
required this.notificationService,
|
|
required this.onLanguageChange,
|
|
required this.onDeleteSaveAndNewGame,
|
|
this.specialAnimation,
|
|
this.currentThemeMode = ThemeMode.system,
|
|
this.onThemeModeChange,
|
|
this.bgmVolume = 0.7,
|
|
this.sfxVolume = 0.8,
|
|
this.onBgmVolumeChange,
|
|
this.onSfxVolumeChange,
|
|
this.onShowStatistics,
|
|
this.onShowHelp,
|
|
this.cheatsEnabled = false,
|
|
this.onCheatTask,
|
|
this.onCheatQuest,
|
|
this.onCheatPlot,
|
|
this.onCreateTestCharacter,
|
|
});
|
|
|
|
final GameState state;
|
|
final List<CombatLogEntry> combatLogEntries;
|
|
final int speedMultiplier;
|
|
final VoidCallback onSpeedCycle;
|
|
final bool isPaused;
|
|
final VoidCallback onPauseToggle;
|
|
final VoidCallback onSave;
|
|
final VoidCallback onExit;
|
|
final NotificationService notificationService;
|
|
final void Function(String locale) onLanguageChange;
|
|
final VoidCallback onDeleteSaveAndNewGame;
|
|
final AsciiAnimationType? specialAnimation;
|
|
final ThemeMode currentThemeMode;
|
|
final void Function(ThemeMode mode)? onThemeModeChange;
|
|
|
|
/// BGM 볼륨 (0.0 ~ 1.0)
|
|
final double bgmVolume;
|
|
|
|
/// SFX 볼륨 (0.0 ~ 1.0)
|
|
final double sfxVolume;
|
|
|
|
/// BGM 볼륨 변경 콜백
|
|
final void Function(double volume)? onBgmVolumeChange;
|
|
|
|
/// SFX 볼륨 변경 콜백
|
|
final void Function(double volume)? onSfxVolumeChange;
|
|
|
|
/// 통계 표시 콜백
|
|
final VoidCallback? onShowStatistics;
|
|
|
|
/// 도움말 표시 콜백
|
|
final VoidCallback? onShowHelp;
|
|
|
|
/// 치트 모드 활성화 여부
|
|
final bool cheatsEnabled;
|
|
|
|
/// 치트: 태스크 완료
|
|
final VoidCallback? onCheatTask;
|
|
|
|
/// 치트: 퀘스트 완료
|
|
final VoidCallback? onCheatQuest;
|
|
|
|
/// 치트: 액트(플롯) 완료
|
|
final VoidCallback? onCheatPlot;
|
|
|
|
/// 테스트 캐릭터 생성 콜백 (디버그 모드 전용)
|
|
final Future<void> Function()? onCreateTestCharacter;
|
|
|
|
@override
|
|
State<MobileCarouselLayout> createState() => _MobileCarouselLayoutState();
|
|
}
|
|
|
|
class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
|
late PageController _pageController;
|
|
int _currentPage = CarouselPage.character.index; // 기본: 캐릭터시트
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_pageController = PageController(initialPage: _currentPage);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_pageController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _onPageChanged(int page) {
|
|
setState(() {
|
|
_currentPage = page;
|
|
});
|
|
}
|
|
|
|
void _onNavPageSelected(int page) {
|
|
_pageController.animateToPage(
|
|
page,
|
|
duration: const Duration(milliseconds: 300),
|
|
curve: Curves.easeInOut,
|
|
);
|
|
}
|
|
|
|
/// 현재 언어명 가져오기
|
|
String _getCurrentLanguageName() {
|
|
final locale = l10n.currentGameLocale;
|
|
if (locale == 'ko') return l10n.languageKorean;
|
|
if (locale == 'ja') return l10n.languageJapanese;
|
|
return l10n.languageEnglish;
|
|
}
|
|
|
|
/// 현재 테마명 가져오기
|
|
String _getCurrentThemeName() {
|
|
return switch (widget.currentThemeMode) {
|
|
ThemeMode.light => l10n.themeLight,
|
|
ThemeMode.dark => l10n.themeDark,
|
|
ThemeMode.system => l10n.themeSystem,
|
|
};
|
|
}
|
|
|
|
/// 테마 아이콘 가져오기
|
|
IconData _getThemeIcon() {
|
|
return switch (widget.currentThemeMode) {
|
|
ThemeMode.light => Icons.light_mode,
|
|
ThemeMode.dark => Icons.dark_mode,
|
|
ThemeMode.system => Icons.brightness_auto,
|
|
};
|
|
}
|
|
|
|
/// 테마 선택 다이얼로그 표시
|
|
void _showThemeDialog(BuildContext context) {
|
|
showDialog<void>(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: Text(l10n.menuTheme),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
_buildThemeOption(context, ThemeMode.system, l10n.themeSystem),
|
|
_buildThemeOption(context, ThemeMode.light, l10n.themeLight),
|
|
_buildThemeOption(context, ThemeMode.dark, l10n.themeDark),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildThemeOption(BuildContext context, ThemeMode mode, String label) {
|
|
final isSelected = widget.currentThemeMode == mode;
|
|
return ListTile(
|
|
leading: Icon(
|
|
isSelected ? Icons.radio_button_checked : Icons.radio_button_unchecked,
|
|
color: isSelected ? Theme.of(context).colorScheme.primary : null,
|
|
),
|
|
title: Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
|
),
|
|
),
|
|
onTap: () {
|
|
Navigator.pop(context); // 다이얼로그 닫기
|
|
widget.onThemeModeChange?.call(mode);
|
|
},
|
|
);
|
|
}
|
|
|
|
/// 언어 선택 다이얼로그 표시
|
|
void _showLanguageDialog(BuildContext context) {
|
|
showDialog<void>(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: Text(l10n.menuLanguage),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
_buildLanguageOption(context, 'en', l10n.languageEnglish),
|
|
_buildLanguageOption(context, 'ko', l10n.languageKorean),
|
|
_buildLanguageOption(context, 'ja', l10n.languageJapanese),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildLanguageOption(
|
|
BuildContext context,
|
|
String locale,
|
|
String label,
|
|
) {
|
|
final isSelected = l10n.currentGameLocale == locale;
|
|
return ListTile(
|
|
leading: Icon(
|
|
isSelected ? Icons.radio_button_checked : Icons.radio_button_unchecked,
|
|
color: isSelected ? Theme.of(context).colorScheme.primary : null,
|
|
),
|
|
title: Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
|
),
|
|
),
|
|
onTap: () {
|
|
Navigator.pop(context); // 다이얼로그 닫기
|
|
widget.onLanguageChange(locale);
|
|
},
|
|
);
|
|
}
|
|
|
|
/// 사운드 상태 텍스트 가져오기
|
|
String _getSoundStatus() {
|
|
final bgmPercent = (widget.bgmVolume * 100).round();
|
|
final sfxPercent = (widget.sfxVolume * 100).round();
|
|
if (bgmPercent == 0 && sfxPercent == 0) {
|
|
return l10n.uiSoundOff;
|
|
}
|
|
return 'BGM $bgmPercent% / SFX $sfxPercent%';
|
|
}
|
|
|
|
/// 사운드 설정 다이얼로그 표시
|
|
void _showSoundDialog(BuildContext context) {
|
|
// StatefulBuilder를 사용하여 다이얼로그 내 상태 관리
|
|
var bgmVolume = widget.bgmVolume;
|
|
var sfxVolume = widget.sfxVolume;
|
|
|
|
showDialog<void>(
|
|
context: context,
|
|
builder: (context) => StatefulBuilder(
|
|
builder: (context, setDialogState) => AlertDialog(
|
|
title: Text(l10n.uiSound),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// BGM 볼륨
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
bgmVolume == 0 ? Icons.music_off : Icons.music_note,
|
|
color: Theme.of(context).colorScheme.primary,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(l10n.uiBgmVolume),
|
|
Text('${(bgmVolume * 100).round()}%'),
|
|
],
|
|
),
|
|
Slider(
|
|
value: bgmVolume,
|
|
onChanged: (value) {
|
|
setDialogState(() => bgmVolume = value);
|
|
widget.onBgmVolumeChange?.call(value);
|
|
},
|
|
divisions: 10,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
// SFX 볼륨
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
sfxVolume == 0 ? Icons.volume_off : Icons.volume_up,
|
|
color: Theme.of(context).colorScheme.primary,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(l10n.uiSfxVolume),
|
|
Text('${(sfxVolume * 100).round()}%'),
|
|
],
|
|
),
|
|
Slider(
|
|
value: sfxVolume,
|
|
onChanged: (value) {
|
|
setDialogState(() => sfxVolume = value);
|
|
widget.onSfxVolumeChange?.call(value);
|
|
},
|
|
divisions: 10,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: Text(l10n.buttonConfirm),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 세이브 삭제 확인 다이얼로그 표시
|
|
void _showDeleteConfirmDialog(BuildContext context) {
|
|
showDialog<void>(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: Text(l10n.confirmDeleteTitle),
|
|
content: Text(l10n.confirmDeleteMessage),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: Text(l10n.buttonCancel),
|
|
),
|
|
TextButton(
|
|
onPressed: () {
|
|
Navigator.pop(context); // 다이얼로그 닫기
|
|
widget.onDeleteSaveAndNewGame();
|
|
},
|
|
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
|
child: Text(l10n.buttonConfirm),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 테스트 캐릭터 생성 확인 다이얼로그
|
|
Future<void> _showTestCharacterDialog(BuildContext context) async {
|
|
final confirmed = await showDialog<bool>(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Create Test Character?'),
|
|
content: const Text(
|
|
'현재 캐릭터가 레벨 100으로 변환되어 명예의 전당에 등록됩니다.\n\n'
|
|
'⚠️ 현재 세이브 파일이 삭제됩니다.\n'
|
|
'이 작업은 되돌릴 수 없습니다.',
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(false),
|
|
child: const Text('Cancel'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () => Navigator.of(context).pop(true),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Theme.of(context).colorScheme.error,
|
|
foregroundColor: Theme.of(context).colorScheme.onError,
|
|
),
|
|
child: const Text('Create'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (confirmed == true && mounted) {
|
|
await widget.onCreateTestCharacter?.call();
|
|
}
|
|
}
|
|
|
|
/// 옵션 메뉴 표시
|
|
void _showOptionsMenu(BuildContext context) {
|
|
final localizations = L10n.of(context);
|
|
final panelBg = RetroColors.panelBgOf(context);
|
|
final gold = RetroColors.goldOf(context);
|
|
final surface = RetroColors.surfaceOf(context);
|
|
|
|
showModalBottomSheet<void>(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
backgroundColor: panelBg,
|
|
constraints: BoxConstraints(
|
|
maxHeight: MediaQuery.of(context).size.height * 0.7,
|
|
),
|
|
builder: (context) => SafeArea(
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// 헤더 (레트로 스타일)
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
width: double.infinity,
|
|
decoration: BoxDecoration(
|
|
color: surface,
|
|
border: Border(bottom: BorderSide(color: gold, width: 2)),
|
|
),
|
|
child: Text(
|
|
'OPTIONS',
|
|
style: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 12,
|
|
color: gold,
|
|
),
|
|
),
|
|
),
|
|
|
|
// 일시정지/재개
|
|
ListTile(
|
|
leading: Icon(
|
|
widget.isPaused ? Icons.play_arrow : Icons.pause,
|
|
color: widget.isPaused ? Colors.green : Colors.orange,
|
|
),
|
|
title: Text(widget.isPaused ? l10n.menuResume : l10n.menuPause),
|
|
onTap: () {
|
|
Navigator.pop(context);
|
|
widget.onPauseToggle();
|
|
},
|
|
),
|
|
|
|
// 속도 조절
|
|
ListTile(
|
|
leading: const Icon(Icons.speed),
|
|
title: Text(l10n.menuSpeed),
|
|
trailing: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 12,
|
|
vertical: 4,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).colorScheme.primaryContainer,
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Text(
|
|
'${widget.speedMultiplier}x',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
color: Theme.of(context).colorScheme.primary,
|
|
),
|
|
),
|
|
),
|
|
onTap: () {
|
|
widget.onSpeedCycle();
|
|
Navigator.pop(context);
|
|
},
|
|
),
|
|
|
|
const Divider(),
|
|
|
|
// 통계
|
|
if (widget.onShowStatistics != null)
|
|
ListTile(
|
|
leading: const Icon(Icons.bar_chart, color: Colors.blue),
|
|
title: Text(l10n.uiStatistics),
|
|
onTap: () {
|
|
Navigator.pop(context);
|
|
widget.onShowStatistics?.call();
|
|
},
|
|
),
|
|
|
|
// 도움말
|
|
if (widget.onShowHelp != null)
|
|
ListTile(
|
|
leading: const Icon(Icons.help_outline, color: Colors.green),
|
|
title: Text(l10n.uiHelp),
|
|
onTap: () {
|
|
Navigator.pop(context);
|
|
widget.onShowHelp?.call();
|
|
},
|
|
),
|
|
|
|
const Divider(),
|
|
|
|
// 언어 변경
|
|
ListTile(
|
|
leading: const Icon(Icons.language, color: Colors.teal),
|
|
title: Text(l10n.menuLanguage),
|
|
trailing: Text(
|
|
_getCurrentLanguageName(),
|
|
style: TextStyle(
|
|
color: Theme.of(context).colorScheme.primary,
|
|
),
|
|
),
|
|
onTap: () {
|
|
Navigator.pop(context);
|
|
_showLanguageDialog(context);
|
|
},
|
|
),
|
|
|
|
// 테마 변경
|
|
if (widget.onThemeModeChange != null)
|
|
ListTile(
|
|
leading: Icon(_getThemeIcon(), color: Colors.purple),
|
|
title: Text(l10n.menuTheme),
|
|
trailing: Text(
|
|
_getCurrentThemeName(),
|
|
style: TextStyle(
|
|
color: Theme.of(context).colorScheme.primary,
|
|
),
|
|
),
|
|
onTap: () {
|
|
Navigator.pop(context);
|
|
_showThemeDialog(context);
|
|
},
|
|
),
|
|
|
|
// 사운드 설정
|
|
if (widget.onBgmVolumeChange != null ||
|
|
widget.onSfxVolumeChange != null)
|
|
ListTile(
|
|
leading: Icon(
|
|
widget.bgmVolume == 0 && widget.sfxVolume == 0
|
|
? Icons.volume_off
|
|
: Icons.volume_up,
|
|
color: Colors.indigo,
|
|
),
|
|
title: Text(l10n.uiSound),
|
|
trailing: Text(
|
|
_getSoundStatus(),
|
|
style: TextStyle(
|
|
color: Theme.of(context).colorScheme.primary,
|
|
),
|
|
),
|
|
onTap: () {
|
|
Navigator.pop(context);
|
|
_showSoundDialog(context);
|
|
},
|
|
),
|
|
|
|
const Divider(),
|
|
|
|
// 저장
|
|
ListTile(
|
|
leading: const Icon(Icons.save, color: Colors.blue),
|
|
title: Text(l10n.menuSave),
|
|
onTap: () {
|
|
Navigator.pop(context);
|
|
widget.onSave();
|
|
widget.notificationService.showGameSaved(l10n.menuSaved);
|
|
},
|
|
),
|
|
|
|
// 새로하기 (세이브 삭제)
|
|
ListTile(
|
|
leading: const Icon(Icons.refresh, color: Colors.orange),
|
|
title: Text(l10n.menuNewGame),
|
|
subtitle: Text(
|
|
l10n.menuDeleteSave,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Theme.of(context).colorScheme.outline,
|
|
),
|
|
),
|
|
onTap: () {
|
|
Navigator.pop(context);
|
|
_showDeleteConfirmDialog(context);
|
|
},
|
|
),
|
|
|
|
// 종료
|
|
ListTile(
|
|
leading: const Icon(Icons.exit_to_app, color: Colors.red),
|
|
title: Text(localizations.exitGame),
|
|
onTap: () {
|
|
Navigator.pop(context);
|
|
widget.onExit();
|
|
},
|
|
),
|
|
|
|
// 치트 섹션 (디버그 모드에서만 표시)
|
|
if (widget.cheatsEnabled) ...[
|
|
const Divider(),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 8,
|
|
),
|
|
child: Text(
|
|
'DEBUG CHEATS',
|
|
style: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 8,
|
|
color: Colors.red.shade300,
|
|
),
|
|
),
|
|
),
|
|
ListTile(
|
|
leading: const Icon(Icons.fast_forward, color: Colors.red),
|
|
title: const Text('Skip Task (L+1)'),
|
|
subtitle: const Text('태스크 즉시 완료'),
|
|
onTap: () {
|
|
Navigator.pop(context);
|
|
widget.onCheatTask?.call();
|
|
},
|
|
),
|
|
ListTile(
|
|
leading: const Icon(Icons.skip_next, color: Colors.red),
|
|
title: const Text('Skip Quest (Q!)'),
|
|
subtitle: const Text('퀘스트 즉시 완료'),
|
|
onTap: () {
|
|
Navigator.pop(context);
|
|
widget.onCheatQuest?.call();
|
|
},
|
|
),
|
|
ListTile(
|
|
leading: const Icon(Icons.double_arrow, color: Colors.red),
|
|
title: const Text('Skip Act (P!)'),
|
|
subtitle: const Text('액트 즉시 완료 (명예의 전당 테스트용)'),
|
|
onTap: () {
|
|
Navigator.pop(context);
|
|
widget.onCheatPlot?.call();
|
|
},
|
|
),
|
|
],
|
|
|
|
// 디버그 도구 섹션 (kDebugMode에서만 표시)
|
|
if (kDebugMode && widget.onCreateTestCharacter != null) ...[
|
|
const Divider(),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 8,
|
|
),
|
|
child: Text(
|
|
'DEBUG TOOLS',
|
|
style: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 8,
|
|
color: Colors.orange.shade300,
|
|
),
|
|
),
|
|
),
|
|
ListTile(
|
|
leading: const Icon(Icons.science, color: Colors.orange),
|
|
title: const Text('Create Test Character'),
|
|
subtitle: const Text('레벨 100 캐릭터를 명예의 전당에 등록'),
|
|
onTap: () {
|
|
Navigator.pop(context);
|
|
_showTestCharacterDialog(context);
|
|
},
|
|
),
|
|
],
|
|
|
|
const SizedBox(height: 8),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final state = widget.state;
|
|
final background = RetroColors.backgroundOf(context);
|
|
final panelBg = RetroColors.panelBgOf(context);
|
|
final gold = RetroColors.goldOf(context);
|
|
|
|
return Scaffold(
|
|
backgroundColor: background,
|
|
appBar: AppBar(
|
|
backgroundColor: panelBg,
|
|
title: Text(
|
|
L10n.of(context).progressQuestTitle(state.traits.name),
|
|
style: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 10,
|
|
color: gold,
|
|
),
|
|
),
|
|
actions: [
|
|
// 옵션 버튼
|
|
IconButton(
|
|
icon: Icon(Icons.settings, color: gold),
|
|
onPressed: () => _showOptionsMenu(context),
|
|
tooltip: l10n.menuOptions,
|
|
),
|
|
],
|
|
),
|
|
body: SafeArea(
|
|
top: false, // AppBar가 상단 처리
|
|
child: Column(
|
|
children: [
|
|
// 상단: 확장 애니메이션 패널
|
|
EnhancedAnimationPanel(
|
|
progress: state.progress,
|
|
stats: state.stats,
|
|
skillSystem: state.skillSystem,
|
|
speedMultiplier: widget.speedMultiplier,
|
|
onSpeedCycle: widget.onSpeedCycle,
|
|
isPaused: widget.isPaused,
|
|
onPauseToggle: widget.onPauseToggle,
|
|
specialAnimation: widget.specialAnimation,
|
|
weaponName: state.equipment.weapon,
|
|
shieldName: state.equipment.shield,
|
|
characterLevel: state.traits.level,
|
|
monsterLevel: state.progress.currentTask.monsterLevel,
|
|
monsterGrade: state.progress.currentTask.monsterGrade,
|
|
latestCombatEvent:
|
|
state.progress.currentCombat?.recentEvents.lastOrNull,
|
|
raceId: state.traits.raceId,
|
|
weaponRarity: state.equipment.weaponItem.rarity,
|
|
),
|
|
|
|
// 중앙: 캐로셀 (PageView)
|
|
Expanded(
|
|
child: PageView(
|
|
controller: _pageController,
|
|
onPageChanged: _onPageChanged,
|
|
children: [
|
|
// 0: 스킬
|
|
SkillsPage(
|
|
skillBook: state.skillBook,
|
|
skillSystem: state.skillSystem,
|
|
),
|
|
|
|
// 1: 인벤토리
|
|
InventoryPage(
|
|
inventory: state.inventory,
|
|
potionInventory: state.potionInventory,
|
|
encumbrance: state.progress.encumbrance,
|
|
usedPotionTypes:
|
|
state.progress.currentCombat?.usedPotionTypes ??
|
|
const {},
|
|
),
|
|
|
|
// 2: 장비
|
|
EquipmentPage(equipment: state.equipment),
|
|
|
|
// 3: 캐릭터시트 (기본)
|
|
CharacterSheetPage(
|
|
traits: state.traits,
|
|
stats: state.stats,
|
|
exp: state.progress.exp,
|
|
),
|
|
|
|
// 4: 전투로그
|
|
CombatLogPage(entries: widget.combatLogEntries),
|
|
|
|
// 5: 퀘스트
|
|
QuestPage(
|
|
questHistory: state.progress.questHistory,
|
|
quest: state.progress.quest,
|
|
),
|
|
|
|
// 6: 스토리
|
|
StoryPage(
|
|
plotStageCount: state.progress.plotStageCount,
|
|
plot: state.progress.plot,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// 하단: 네비게이션 바
|
|
CarouselNavBar(
|
|
currentPage: _currentPage,
|
|
onPageSelected: _onNavPageSelected,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|