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:
JiWoong Sul
2025-12-23 17:52:43 +09:00
parent 1da6fa7a2b
commit e6af7dd91a
28 changed files with 2734 additions and 73 deletions

View File

@@ -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...'),
],
),
),
);
}
}