Files
asciinevrdie/lib/src/features/front/front_screen.dart
JiWoong Sul 748160d543 feat(ui): 화면 및 컨트롤러 수익화 연동
- 앱 초기화에 광고/IAP 서비스 추가
- 게임 세션 컨트롤러 수익화 상태 관리
- 캐릭터 생성 화면 굴리기 제한 UI
- 설정 화면 광고 제거 구매 UI
- 애니메이션 패널 개선
2026-01-16 20:10:43 +09:00

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