feat(ui): 반응형 레이아웃 및 저장 시스템 개선
## 반응형 레이아웃 - app.dart: 화면 크기별 레이아웃 분기 로직 추가 (+173 라인) - game_play_screen.dart: 반응형 UI 구조 개선 - layouts/, pages/ 디렉토리 추가 (새 레이아웃 시스템) - carousel_nav_bar.dart: 캐러셀 네비게이션 바 추가 - enhanced_animation_panel.dart: 향상된 애니메이션 패널 ## 저장 시스템 - save_manager.dart: 저장 관리 기능 확장 - save_repository.dart: 저장소 인터페이스 개선 - save_service.dart: 저장 서비스 로직 추가 ## UI 개선 - notification_service.dart: 알림 시스템 기능 확장 - notification_overlay.dart: 오버레이 UI 개선 - equipment_stats_panel.dart: 장비 스탯 패널 개선 - cinematic_view.dart: 시네마틱 뷰 개선 - new_character_screen.dart: 캐릭터 생성 화면 개선 ## 다국어 - game_text_l10n.dart: 텍스트 추가 (+182 라인) ## 테스트 - 관련 테스트 파일 업데이트
This commit is contained in:
173
lib/src/app.dart
173
lib/src/app.dart
@@ -7,12 +7,14 @@ import 'package:askiineverdie/src/core/engine/progress_service.dart';
|
||||
import 'package:askiineverdie/src/core/engine/reward_service.dart';
|
||||
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||
import 'package:askiineverdie/src/core/model/pq_config.dart';
|
||||
import 'package:askiineverdie/src/core/notification/notification_service.dart';
|
||||
import 'package:askiineverdie/src/core/storage/save_manager.dart';
|
||||
import 'package:askiineverdie/src/core/storage/save_repository.dart';
|
||||
import 'package:askiineverdie/src/features/front/front_screen.dart';
|
||||
import 'package:askiineverdie/src/features/front/save_picker_dialog.dart';
|
||||
import 'package:askiineverdie/src/features/game/game_play_screen.dart';
|
||||
import 'package:askiineverdie/src/features/game/game_session_controller.dart';
|
||||
import 'package:askiineverdie/src/features/game/widgets/notification_overlay.dart';
|
||||
import 'package:askiineverdie/src/features/hall_of_fame/hall_of_fame_screen.dart';
|
||||
import 'package:askiineverdie/src/features/new_character/new_character_screen.dart';
|
||||
|
||||
@@ -25,6 +27,9 @@ class AskiiNeverDieApp extends StatefulWidget {
|
||||
|
||||
class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
|
||||
late final GameSessionController _controller;
|
||||
late final NotificationService _notificationService;
|
||||
bool _isCheckingSave = true;
|
||||
bool _hasSave = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -41,11 +46,27 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
|
||||
),
|
||||
saveManager: SaveManager(SaveRepository()),
|
||||
);
|
||||
_notificationService = NotificationService();
|
||||
|
||||
// 세이브 파일 존재 여부 확인
|
||||
_checkForExistingSave();
|
||||
}
|
||||
|
||||
/// 세이브 파일 존재 여부 확인 후 자동 로드
|
||||
Future<void> _checkForExistingSave() async {
|
||||
final exists = await _controller.saveManager.saveExists();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_hasSave = exists;
|
||||
_isCheckingSave = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_notificationService.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -67,20 +88,45 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
|
||||
game_l10n.setGameLocale(locale.languageCode);
|
||||
return child ?? const SizedBox.shrink();
|
||||
},
|
||||
home: FrontScreen(
|
||||
onNewCharacter: _navigateToNewCharacter,
|
||||
onLoadSave: _loadSave,
|
||||
onHallOfFame: _navigateToHallOfFame,
|
||||
home: NotificationOverlay(
|
||||
notificationService: _notificationService,
|
||||
child: _buildHomeScreen(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 홈 화면 결정: 세이브 확인 중 → 스플래시, 세이브 있음 → 자동 로드, 없음 → 프론트
|
||||
Widget _buildHomeScreen() {
|
||||
// 세이브 확인 중이면 로딩 스플래시 표시
|
||||
if (_isCheckingSave) {
|
||||
return const _SplashScreen();
|
||||
}
|
||||
|
||||
// 세이브 파일이 있으면 자동 로드 화면
|
||||
if (_hasSave) {
|
||||
return _AutoLoadScreen(
|
||||
controller: _controller,
|
||||
onLoadFailed: () {
|
||||
// 로드 실패 시 프론트 화면으로
|
||||
setState(() => _hasSave = false);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 세이브 파일이 없으면 기존 프론트 화면
|
||||
return FrontScreen(
|
||||
onNewCharacter: _navigateToNewCharacter,
|
||||
onLoadSave: _loadSave,
|
||||
onHallOfFame: _navigateToHallOfFame,
|
||||
);
|
||||
}
|
||||
|
||||
void _navigateToNewCharacter(BuildContext context) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => NewCharacterScreen(
|
||||
onCharacterCreated: (initialState) {
|
||||
_startGame(context, initialState);
|
||||
onCharacterCreated: (initialState, {bool testMode = false}) {
|
||||
_startGame(context, initialState, testMode: testMode);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -97,9 +143,7 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
|
||||
|
||||
if (saves.isEmpty) {
|
||||
// 저장 파일이 없으면 안내 메시지
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(L10n.of(context).noSavedGames)));
|
||||
_notificationService.showInfo(L10n.of(context).noSavedGames);
|
||||
return;
|
||||
} else if (saves.length == 1) {
|
||||
// 파일이 하나면 바로 선택
|
||||
@@ -121,27 +165,29 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
|
||||
if (context.mounted) {
|
||||
_navigateToGame(context);
|
||||
}
|
||||
} else if (_controller.status == GameSessionStatus.error) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
L10n.of(context).loadError(_controller.error ?? 'Unknown error'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
} 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) async {
|
||||
Future<void> _startGame(
|
||||
BuildContext context,
|
||||
GameState initialState, {
|
||||
bool testMode = false,
|
||||
}) async {
|
||||
await _controller.startNew(initialState, cheatsEnabled: false);
|
||||
|
||||
if (context.mounted) {
|
||||
// NewCharacterScreen을 pop하고 GamePlayScreen으로 이동
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => GamePlayScreen(controller: _controller),
|
||||
builder: (context) => GamePlayScreen(
|
||||
controller: _controller,
|
||||
forceCarouselLayout: testMode,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -162,3 +208,88 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 스플래시 화면 (세이브 파일 확인 중)
|
||||
class _SplashScreen extends StatelessWidget {
|
||||
const _SplashScreen();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold(
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'ASCII NEVER DIE',
|
||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
CircularProgressIndicator(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 자동 로드 화면 (세이브 파일 자동 로드)
|
||||
class _AutoLoadScreen extends StatefulWidget {
|
||||
const _AutoLoadScreen({required this.controller, required this.onLoadFailed});
|
||||
|
||||
final GameSessionController controller;
|
||||
final VoidCallback onLoadFailed;
|
||||
|
||||
@override
|
||||
State<_AutoLoadScreen> createState() => _AutoLoadScreenState();
|
||||
}
|
||||
|
||||
class _AutoLoadScreenState extends State<_AutoLoadScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_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,
|
||||
// 자동 로드 시에는 플랫폼 기본값 사용 (모바일만 캐로셀)
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// 로드 실패 → 프론트 화면으로 돌아가기
|
||||
widget.onLoadFailed();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold(
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'ASCII NEVER DIE',
|
||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 16),
|
||||
Text('Loading...'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user