- 앱 초기화에 광고/IAP 서비스 추가 - 게임 세션 컨트롤러 수익화 상태 관리 - 캐릭터 생성 화면 굴리기 제한 UI - 설정 화면 광고 제거 구매 UI - 애니메이션 패널 개선
420 lines
12 KiB
Dart
420 lines
12 KiB
Dart
import 'dart:io' show Platform;
|
|
|
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
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/app.dart' show SavedGamePreview;
|
|
import 'package:asciineverdie/src/features/front/widgets/hero_vs_boss_animation.dart';
|
|
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
|
import 'package:asciineverdie/src/shared/widgets/retro_widgets.dart';
|
|
|
|
class FrontScreen extends StatefulWidget {
|
|
const FrontScreen({
|
|
super.key,
|
|
this.onNewCharacter,
|
|
this.onLoadSave,
|
|
this.onHallOfFame,
|
|
this.onLocalArena,
|
|
this.onSettings,
|
|
this.hasSaveFile = false,
|
|
this.savedGamePreview,
|
|
this.hallOfFameCount = 0,
|
|
this.routeObserver,
|
|
this.onRefresh,
|
|
});
|
|
|
|
/// "New character" 버튼 클릭 시 호출
|
|
final void Function(BuildContext context)? onNewCharacter;
|
|
|
|
/// "Load save" 버튼 클릭 시 호출
|
|
final Future<void> Function(BuildContext context)? onLoadSave;
|
|
|
|
/// "Hall of Fame" 버튼 클릭 시 호출
|
|
final void Function(BuildContext context)? onHallOfFame;
|
|
|
|
/// "Local Arena" 버튼 클릭 시 호출
|
|
final void Function(BuildContext context)? onLocalArena;
|
|
|
|
/// "Settings" 버튼 클릭 시 호출 (언어, 테마, 사운드)
|
|
final void Function(BuildContext context)? onSettings;
|
|
|
|
/// 세이브 파일 존재 여부 (새 캐릭터 시 경고용)
|
|
final bool hasSaveFile;
|
|
|
|
/// 저장된 게임 미리보기 정보
|
|
final SavedGamePreview? savedGamePreview;
|
|
|
|
/// 명예의 전당 캐릭터 수 (아레나 활성화 조건: 2명 이상)
|
|
final int hallOfFameCount;
|
|
|
|
/// RouteObserver (화면 복귀 시 갱신용)
|
|
final RouteObserver<ModalRoute<void>>? routeObserver;
|
|
|
|
/// 화면 복귀 시 호출할 콜백
|
|
final VoidCallback? onRefresh;
|
|
|
|
@override
|
|
State<FrontScreen> createState() => _FrontScreenState();
|
|
}
|
|
|
|
class _FrontScreenState extends State<FrontScreen> with RouteAware {
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
// RouteObserver 구독
|
|
final route = ModalRoute.of(context);
|
|
if (route != null) {
|
|
widget.routeObserver?.subscribe(this, route);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
widget.routeObserver?.unsubscribe(this);
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
void didPopNext() {
|
|
// 다른 화면에서 돌아왔을 때 갱신
|
|
widget.onRefresh?.call();
|
|
}
|
|
|
|
/// 새 캐릭터 생성 시 세이브 파일 존재하면 경고 표시
|
|
void _handleNewCharacter(BuildContext context) {
|
|
if (widget.hasSaveFile) {
|
|
_showDeleteWarningDialog(context);
|
|
} else {
|
|
widget.onNewCharacter?.call(context);
|
|
}
|
|
}
|
|
|
|
/// 세이브 삭제 경고 다이얼로그
|
|
void _showDeleteWarningDialog(BuildContext context) {
|
|
showDialog<void>(
|
|
context: context,
|
|
builder: (dialogContext) => AlertDialog(
|
|
title: Text(game_l10n.uiWarning),
|
|
content: Text(game_l10n.warningDeleteSave),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(dialogContext),
|
|
child: Text(game_l10n.buttonCancel),
|
|
),
|
|
FilledButton(
|
|
onPressed: () {
|
|
Navigator.pop(dialogContext);
|
|
widget.onNewCharacter?.call(context);
|
|
},
|
|
child: Text(game_l10n.buttonConfirm),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: RetroColors.deepBrown,
|
|
body: SafeArea(
|
|
child: Column(
|
|
children: [
|
|
// 스크롤 영역 (헤더, 애니메이션, 버튼)
|
|
Expanded(
|
|
child: Center(
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 800),
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
const _RetroHeader(),
|
|
const SizedBox(height: 16),
|
|
const _AnimationPanel(),
|
|
const SizedBox(height: 16),
|
|
_ActionButtons(
|
|
onNewCharacter: widget.onNewCharacter != null
|
|
? () => _handleNewCharacter(context)
|
|
: null,
|
|
onLoadSave: widget.onLoadSave != null
|
|
? () => widget.onLoadSave!(context)
|
|
: null,
|
|
onHallOfFame: widget.onHallOfFame != null
|
|
? () => widget.onHallOfFame!(context)
|
|
: null,
|
|
onLocalArena:
|
|
widget.onLocalArena != null &&
|
|
widget.hallOfFameCount >= 2
|
|
? () => widget.onLocalArena!(context)
|
|
: null,
|
|
onSettings: widget.onSettings != null
|
|
? () => widget.onSettings!(context)
|
|
: null,
|
|
savedGamePreview: widget.savedGamePreview,
|
|
hallOfFameCount: widget.hallOfFameCount,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// 카피라이트 푸터 (하단 고정)
|
|
const _CopyrightFooter(),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 레트로 스타일 헤더 (타이틀 + 태그)
|
|
class _RetroHeader extends StatelessWidget {
|
|
const _RetroHeader();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l10n = L10n.of(context);
|
|
return RetroGoldPanel(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
|
|
child: Column(
|
|
children: [
|
|
// 타이틀 (픽셀 폰트)
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(Icons.auto_awesome, color: RetroColors.gold, size: 20),
|
|
const SizedBox(width: 12),
|
|
Text(
|
|
l10n.appTitle,
|
|
style: const TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 13,
|
|
color: RetroColors.gold,
|
|
shadows: [
|
|
Shadow(color: RetroColors.goldDark, offset: Offset(2, 2)),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
// 태그 (레트로 스타일)
|
|
Wrap(
|
|
alignment: WrapAlignment.center,
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: [
|
|
_RetroTag(
|
|
icon: Icons.cloud_off_outlined,
|
|
label: l10n.tagNoNetwork,
|
|
),
|
|
_RetroTag(icon: Icons.timer_outlined, label: l10n.tagIdleRpg),
|
|
_RetroTag(icon: Icons.storage_rounded, label: l10n.tagLocalSaves),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 애니메이션 패널
|
|
class _AnimationPanel extends StatelessWidget {
|
|
const _AnimationPanel();
|
|
|
|
/// 플랫폼별 비율 반환 (데스크톱: 넓게, 모바일: 높게)
|
|
double _getAspectRatio() {
|
|
if (kIsWeb) return 2.5; // 웹: 넓은 비율
|
|
if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {
|
|
return 2.5; // 데스크톱: 넓은 비율
|
|
}
|
|
return 16 / 9; // 모바일(iOS/Android): 16:9
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return RetroPanel(
|
|
title: 'BATTLE',
|
|
padding: const EdgeInsets.all(8),
|
|
child: AspectRatio(
|
|
aspectRatio: _getAspectRatio(),
|
|
child: const HeroVsBossAnimation(),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 액션 버튼 (레트로 스타일)
|
|
class _ActionButtons extends StatelessWidget {
|
|
const _ActionButtons({
|
|
this.onNewCharacter,
|
|
this.onLoadSave,
|
|
this.onHallOfFame,
|
|
this.onLocalArena,
|
|
this.onSettings,
|
|
this.savedGamePreview,
|
|
this.hallOfFameCount = 0,
|
|
});
|
|
|
|
final VoidCallback? onNewCharacter;
|
|
final VoidCallback? onLoadSave;
|
|
final VoidCallback? onHallOfFame;
|
|
final VoidCallback? onLocalArena;
|
|
final VoidCallback? onSettings;
|
|
final SavedGamePreview? savedGamePreview;
|
|
final int hallOfFameCount;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l10n = L10n.of(context);
|
|
|
|
return RetroPanel(
|
|
title: 'MENU',
|
|
padding: const EdgeInsets.all(12),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
// 새 캐릭터 (Primary)
|
|
RetroTextButton(
|
|
text: l10n.newCharacter,
|
|
icon: Icons.casino_outlined,
|
|
onPressed: onNewCharacter,
|
|
),
|
|
const SizedBox(height: 12),
|
|
// 불러오기 (Secondary)
|
|
RetroTextButton(
|
|
text: l10n.loadSave,
|
|
icon: Icons.folder_open,
|
|
onPressed: onLoadSave,
|
|
isPrimary: false,
|
|
),
|
|
// 저장된 게임 정보 표시
|
|
if (savedGamePreview != null) ...[
|
|
const SizedBox(height: 6),
|
|
_SavedGameInfo(preview: savedGamePreview!),
|
|
],
|
|
const SizedBox(height: 12),
|
|
// 명예의 전당
|
|
if (onHallOfFame != null)
|
|
RetroTextButton(
|
|
text: game_l10n.uiHallOfFame,
|
|
icon: Icons.emoji_events_outlined,
|
|
onPressed: onHallOfFame,
|
|
isPrimary: false,
|
|
),
|
|
// 로컬 아레나 (항상 표시, 2명 이상일 때만 활성화)
|
|
const SizedBox(height: 12),
|
|
RetroTextButton(
|
|
text: game_l10n.uiLocalArena,
|
|
icon: Icons.sports_kabaddi,
|
|
onPressed: hallOfFameCount >= 2 ? onLocalArena : null,
|
|
isPrimary: false,
|
|
),
|
|
// 설정
|
|
const SizedBox(height: 12),
|
|
RetroTextButton(
|
|
text: game_l10n.uiSettings,
|
|
icon: Icons.settings,
|
|
onPressed: onSettings,
|
|
isPrimary: false,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 저장된 게임 미리보기 정보 위젯
|
|
class _SavedGameInfo extends StatelessWidget {
|
|
const _SavedGameInfo({required this.preview});
|
|
|
|
final SavedGamePreview preview;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(
|
|
Icons.person_outline,
|
|
size: 10,
|
|
color: RetroColors.textDisabled,
|
|
),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
'${preview.characterName} Lv.${preview.level} ${preview.actName}',
|
|
style: const TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 11,
|
|
color: RetroColors.textDisabled,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 카피라이트 푸터
|
|
class _CopyrightFooter extends StatelessWidget {
|
|
const _CopyrightFooter();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
child: Text(
|
|
game_l10n.copyrightText,
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 7,
|
|
color: RetroColors.textDisabled,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 레트로 태그 칩
|
|
class _RetroTag extends StatelessWidget {
|
|
const _RetroTag({required this.icon, required this.label});
|
|
|
|
final IconData icon;
|
|
final String label;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: RetroColors.panelBgLight,
|
|
border: Border.all(color: RetroColors.panelBorderInner, width: 1),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(icon, color: RetroColors.gold, size: 12),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
label,
|
|
style: const TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 11,
|
|
color: RetroColors.textLight,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|