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:
@@ -1,3 +1,5 @@
|
||||
import 'package:flutter/foundation.dart'
|
||||
show kIsWeb, defaultTargetPlatform, TargetPlatform;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:askiineverdie/data/game_text_l10n.dart' as game_l10n;
|
||||
@@ -26,15 +28,27 @@ import 'package:askiineverdie/src/features/game/widgets/equipment_stats_panel.da
|
||||
import 'package:askiineverdie/src/features/game/widgets/potion_inventory_panel.dart';
|
||||
import 'package:askiineverdie/src/features/game/widgets/task_progress_panel.dart';
|
||||
import 'package:askiineverdie/src/features/game/widgets/active_buff_panel.dart';
|
||||
import 'package:askiineverdie/src/features/game/layouts/mobile_carousel_layout.dart';
|
||||
|
||||
/// 게임 진행 화면 (Main.dfm 기반 3패널 레이아웃)
|
||||
///
|
||||
/// Phase 7: colorTheme 제거됨, 고정 4색 팔레트 사용
|
||||
class GamePlayScreen extends StatefulWidget {
|
||||
const GamePlayScreen({super.key, required this.controller});
|
||||
const GamePlayScreen({
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.forceCarouselLayout = false,
|
||||
this.forceDesktopLayout = false,
|
||||
});
|
||||
|
||||
final GameSessionController controller;
|
||||
|
||||
/// 테스트 모드: 웹에서도 모바일 캐로셀 레이아웃 강제 사용
|
||||
final bool forceCarouselLayout;
|
||||
|
||||
/// 테스트 모드: 모바일에서도 데스크톱 3패널 레이아웃 강제 사용
|
||||
final bool forceDesktopLayout;
|
||||
|
||||
@override
|
||||
State<GamePlayScreen> createState() => _GamePlayScreenState();
|
||||
}
|
||||
@@ -400,6 +414,102 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
/// 캐로셀 레이아웃 사용 여부 판단
|
||||
///
|
||||
/// - forceDesktopLayout (테스트 모드) 활성화 시 데스크톱 레이아웃 사용
|
||||
/// - forceCarouselLayout (테스트 모드) 활성화 시 캐로셀 레이아웃 사용
|
||||
/// - 실제 모바일 플랫폼 (iOS/Android) 시 캐로셀 사용
|
||||
bool _shouldUseCarouselLayout(BuildContext context) {
|
||||
// 테스트 모드: 데스크톱 레이아웃 강제
|
||||
if (widget.forceDesktopLayout) return false;
|
||||
|
||||
// 테스트 모드: 캐로셀 레이아웃 강제
|
||||
if (widget.forceCarouselLayout) return true;
|
||||
|
||||
// 웹에서는 3패널 레이아웃 사용 (테스트 모드가 아닌 경우)
|
||||
if (kIsWeb) return false;
|
||||
|
||||
// 모바일 플랫폼(iOS/Android)에서는 캐로셀 사용
|
||||
final platform = defaultTargetPlatform;
|
||||
return platform == TargetPlatform.iOS || platform == TargetPlatform.android;
|
||||
}
|
||||
|
||||
/// 현재 언어명 가져오기
|
||||
String _getCurrentLanguageName() {
|
||||
final locale = game_l10n.currentGameLocale;
|
||||
if (locale == 'ko') return game_l10n.languageKorean;
|
||||
if (locale == 'ja') return game_l10n.languageJapanese;
|
||||
return game_l10n.languageEnglish;
|
||||
}
|
||||
|
||||
/// 언어 선택 다이얼로그 표시
|
||||
void _showLanguageDialog(BuildContext context) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(game_l10n.menuLanguage),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildLanguageOption(context, 'en', game_l10n.languageEnglish),
|
||||
_buildLanguageOption(context, 'ko', game_l10n.languageKorean),
|
||||
_buildLanguageOption(context, 'ja', game_l10n.languageJapanese),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLanguageOption(
|
||||
BuildContext context,
|
||||
String locale,
|
||||
String label,
|
||||
) {
|
||||
final isSelected = game_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); // 다이얼로그 닫기
|
||||
game_l10n.setGameLocale(locale);
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 부활 처리 핸들러
|
||||
Future<void> _handleResurrect() async {
|
||||
// 1. 부활 처리 (HP/MP 회복, 장비 구매 - 게임 재개 없음)
|
||||
await widget.controller.resurrect();
|
||||
|
||||
// 2. 부활 애니메이션 재생
|
||||
setState(() {
|
||||
_specialAnimation = AsciiAnimationType.resurrection;
|
||||
});
|
||||
|
||||
// 3. 애니메이션 종료 후 게임 재개
|
||||
final duration = getSpecialAnimationDuration(
|
||||
AsciiAnimationType.resurrection,
|
||||
);
|
||||
Future.delayed(Duration(milliseconds: duration), () async {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_specialAnimation = null;
|
||||
});
|
||||
// 부활 후 게임 재개 (새 루프 시작)
|
||||
await widget.controller.resumeAfterResurrection();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = widget.controller.state;
|
||||
@@ -407,7 +517,84 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
return const Scaffold(body: Center(child: CircularProgressIndicator()));
|
||||
}
|
||||
|
||||
// 로케일 변경 시 전체 위젯 트리 강제 리빌드를 위한 Key
|
||||
final localeKey = ValueKey(game_l10n.currentGameLocale);
|
||||
|
||||
// 캐로셀 레이아웃 사용 여부 확인
|
||||
if (_shouldUseCarouselLayout(context)) {
|
||||
return NotificationOverlay(
|
||||
key: localeKey,
|
||||
notificationService: _notificationService,
|
||||
child: PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, result) async {
|
||||
if (didPop) return;
|
||||
final shouldPop = await _onPopInvoked();
|
||||
if (shouldPop && context.mounted) {
|
||||
await widget.controller.pause(saveOnStop: false);
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
MobileCarouselLayout(
|
||||
state: state,
|
||||
combatLogEntries: _combatLogEntries,
|
||||
speedMultiplier: widget.controller.loop?.speedMultiplier ?? 1,
|
||||
onSpeedCycle: () {
|
||||
widget.controller.loop?.cycleSpeed();
|
||||
setState(() {});
|
||||
},
|
||||
isPaused: !widget.controller.isRunning,
|
||||
onPauseToggle: () async {
|
||||
await widget.controller.togglePause();
|
||||
setState(() {});
|
||||
},
|
||||
onSave: _saveGameState,
|
||||
onExit: () async {
|
||||
final shouldExit = await _onPopInvoked();
|
||||
if (shouldExit && context.mounted) {
|
||||
await widget.controller.pause(saveOnStop: false);
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
},
|
||||
notificationService: _notificationService,
|
||||
specialAnimation: _specialAnimation,
|
||||
onLanguageChange: (locale) {
|
||||
game_l10n.setGameLocale(locale);
|
||||
setState(() {});
|
||||
},
|
||||
onDeleteSaveAndNewGame: () async {
|
||||
// 게임 루프 중지
|
||||
await widget.controller.pause(saveOnStop: false);
|
||||
// 세이브 파일 삭제
|
||||
await widget.controller.saveManager.deleteSave();
|
||||
// 캐릭터 생성 화면으로 돌아가기
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
),
|
||||
// 사망 오버레이
|
||||
if (state.isDead && state.deathInfo != null)
|
||||
DeathOverlay(
|
||||
deathInfo: state.deathInfo!,
|
||||
traits: state.traits,
|
||||
onResurrect: _handleResurrect,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 기존 데스크톱 레이아웃
|
||||
return NotificationOverlay(
|
||||
key: localeKey,
|
||||
notificationService: _notificationService,
|
||||
child: PopScope(
|
||||
canPop: false,
|
||||
@@ -443,6 +630,12 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
onPressed: () => widget.controller.loop?.cheatCompletePlot(),
|
||||
),
|
||||
],
|
||||
// 언어 변경 버튼
|
||||
TextButton.icon(
|
||||
onPressed: () => _showLanguageDialog(context),
|
||||
icon: const Icon(Icons.language, size: 18),
|
||||
label: Text(_getCurrentLanguageName()),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Stack(
|
||||
@@ -497,29 +690,7 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
DeathOverlay(
|
||||
deathInfo: state.deathInfo!,
|
||||
traits: state.traits,
|
||||
onResurrect: () async {
|
||||
// 1. 부활 처리 (HP/MP 회복, 장비 구매 - 게임 재개 없음)
|
||||
await widget.controller.resurrect();
|
||||
|
||||
// 2. 부활 애니메이션 재생
|
||||
setState(() {
|
||||
_specialAnimation = AsciiAnimationType.resurrection;
|
||||
});
|
||||
|
||||
// 3. 애니메이션 종료 후 게임 재개
|
||||
final duration = getSpecialAnimationDuration(
|
||||
AsciiAnimationType.resurrection,
|
||||
);
|
||||
Future.delayed(Duration(milliseconds: duration), () async {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_specialAnimation = null;
|
||||
});
|
||||
// 부활 후 게임 재개 (새 루프 시작)
|
||||
await widget.controller.resumeAfterResurrection();
|
||||
}
|
||||
});
|
||||
},
|
||||
onResurrect: _handleResurrect,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user