feat(ui): 게임 화면 및 UI 컴포넌트 개선

- front_screen: 프론트 화면 UI 업데이트
- game_play_screen: 게임 플레이 화면 수정
- game_session_controller: 세션 관리 로직 개선
- mobile_carousel_layout: 모바일 캐러셀 레이아웃 개선
- enhanced_animation_panel: 애니메이션 패널 업데이트
- help_dialog: 도움말 다이얼로그 수정
- return_rewards_dialog: 복귀 보상 다이얼로그 개선
- new_character_screen: 새 캐릭터 화면 수정
- settings_screen: 설정 화면 업데이트
This commit is contained in:
JiWoong Sul
2026-01-19 15:50:35 +09:00
parent ffc19c7ca6
commit 19faa9ea39
9 changed files with 2495 additions and 1355 deletions

View File

@@ -2,6 +2,7 @@ import 'dart:io' show Platform;
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:asciineverdie/data/game_text_l10n.dart' as game_l10n;
import 'package:asciineverdie/l10n/app_localizations.dart';
@@ -18,9 +19,13 @@ class FrontScreen extends StatefulWidget {
this.onHallOfFame,
this.onLocalArena,
this.onSettings,
this.onPurchaseRemoveAds,
this.onRestorePurchase,
this.hasSaveFile = false,
this.savedGamePreview,
this.hallOfFameCount = 0,
this.isAdRemovalPurchased = false,
this.removeAdsPrice,
this.routeObserver,
this.onRefresh,
});
@@ -40,6 +45,12 @@ class FrontScreen extends StatefulWidget {
/// "Settings" 버튼 클릭 시 호출 (언어, 테마, 사운드)
final void Function(BuildContext context)? onSettings;
/// "광고 제거" 구매 버튼 클릭 시 호출
final Future<void> Function(BuildContext context)? onPurchaseRemoveAds;
/// "구매 복원" 버튼 클릭 시 호출
final Future<void> Function(BuildContext context)? onRestorePurchase;
/// 세이브 파일 존재 여부 (새 캐릭터 시 경고용)
final bool hasSaveFile;
@@ -49,6 +60,12 @@ class FrontScreen extends StatefulWidget {
/// 명예의 전당 캐릭터 수 (아레나 활성화 조건: 2명 이상)
final int hallOfFameCount;
/// 광고 제거 구매 여부
final bool isAdRemovalPurchased;
/// 광고 제거 상품 가격 (null이면 스토어 비활성)
final String? removeAdsPrice;
/// RouteObserver (화면 복귀 시 갱신용)
final RouteObserver<ModalRoute<void>>? routeObserver;
@@ -132,8 +149,6 @@ class _FrontScreenState extends State<FrontScreen> with RouteAware {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const _RetroHeader(),
const SizedBox(height: 16),
const _AnimationPanel(),
const SizedBox(height: 16),
_ActionButtons(
@@ -154,8 +169,17 @@ class _FrontScreenState extends State<FrontScreen> with RouteAware {
onSettings: widget.onSettings != null
? () => widget.onSettings!(context)
: null,
onPurchaseRemoveAds:
widget.onPurchaseRemoveAds != null
? () => widget.onPurchaseRemoveAds!(context)
: null,
onRestorePurchase: widget.onRestorePurchase != null
? () => widget.onRestorePurchase!(context)
: null,
savedGamePreview: widget.savedGamePreview,
hallOfFameCount: widget.hallOfFameCount,
isAdRemovalPurchased: widget.isAdRemovalPurchased,
removeAdsPrice: widget.removeAdsPrice,
),
],
),
@@ -172,58 +196,7 @@ class _FrontScreenState extends State<FrontScreen> with RouteAware {
}
}
/// 레트로 스타일 헤더 (타이틀 + 태그)
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();
@@ -238,8 +211,25 @@ class _AnimationPanel extends StatelessWidget {
@override
Widget build(BuildContext context) {
return RetroPanel(
title: 'BATTLE',
return RetroGoldPanel(
titleWidget: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.auto_awesome, color: RetroColors.gold, size: 18),
const SizedBox(width: 10),
const Text(
'ASCII NEVER DIE',
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 14,
color: RetroColors.gold,
shadows: [
Shadow(color: RetroColors.goldDark, offset: Offset(2, 2)),
],
),
),
],
),
padding: const EdgeInsets.all(8),
child: AspectRatio(
aspectRatio: _getAspectRatio(),
@@ -257,8 +247,12 @@ class _ActionButtons extends StatelessWidget {
this.onHallOfFame,
this.onLocalArena,
this.onSettings,
this.onPurchaseRemoveAds,
this.onRestorePurchase,
this.savedGamePreview,
this.hallOfFameCount = 0,
this.isAdRemovalPurchased = false,
this.removeAdsPrice,
});
final VoidCallback? onNewCharacter;
@@ -266,8 +260,12 @@ class _ActionButtons extends StatelessWidget {
final VoidCallback? onHallOfFame;
final VoidCallback? onLocalArena;
final VoidCallback? onSettings;
final VoidCallback? onPurchaseRemoveAds;
final VoidCallback? onRestorePurchase;
final SavedGamePreview? savedGamePreview;
final int hallOfFameCount;
final bool isAdRemovalPurchased;
final String? removeAdsPrice;
@override
Widget build(BuildContext context) {
@@ -323,6 +321,24 @@ class _ActionButtons extends StatelessWidget {
onPressed: onSettings,
isPrimary: false,
),
// IAP 구매 (광고 제거) - 스토어 사용 가능하고 미구매 상태일 때만 표시
if (removeAdsPrice != null && !isAdRemovalPurchased) ...[
const SizedBox(height: 20),
const Divider(color: RetroColors.panelBorderInner, height: 1),
const SizedBox(height: 12),
_IapPurchaseButton(
price: removeAdsPrice!,
onPurchase: onPurchaseRemoveAds,
onRestore: onRestorePurchase,
),
],
// 이미 구매된 경우 표시
if (isAdRemovalPurchased) ...[
const SizedBox(height: 20),
const Divider(color: RetroColors.panelBorderInner, height: 1),
const SizedBox(height: 12),
_PurchasedBadge(),
],
],
),
);
@@ -370,50 +386,322 @@ class _CopyrightFooter extends StatelessWidget {
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,
child: FutureBuilder<PackageInfo>(
future: PackageInfo.fromPlatform(),
builder: (context, snapshot) {
final version = snapshot.data?.version ?? '';
final versionSuffix = version.isNotEmpty ? ' v$version' : '';
return Text(
'${game_l10n.copyrightText}$versionSuffix',
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 7,
color: RetroColors.textDisabled,
),
);
},
),
);
}
}
/// IAP 구매 버튼 (광고 제거)
class _IapPurchaseButton extends StatelessWidget {
const _IapPurchaseButton({
required this.price,
this.onPurchase,
this.onRestore,
});
final String price;
final VoidCallback? onPurchase;
final VoidCallback? onRestore;
void _showPurchaseDialog(BuildContext context) {
showDialog<void>(
context: context,
builder: (dialogContext) => _IapPurchaseDialog(
price: price,
onPurchase: () {
Navigator.pop(dialogContext);
onPurchase?.call();
},
),
);
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 구매 버튼 (클릭 시 팝업)
Container(
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF4A3B2A), Color(0xFF3D2E1F)],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
border: Border.all(color: RetroColors.gold, width: 2),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _showPurchaseDialog(context),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
child: Row(
children: [
const Icon(Icons.block, color: RetroColors.gold, size: 24),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
game_l10n.iapRemoveAds,
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 12,
color: RetroColors.gold,
),
),
const SizedBox(height: 4),
Text(
game_l10n.iapRemoveAdsDesc,
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 9,
color: RetroColors.textDisabled,
),
),
],
),
),
// 화살표 아이콘 (상세 보기)
const Icon(
Icons.arrow_forward_ios,
color: RetroColors.gold,
size: 16,
),
],
),
),
),
),
),
const SizedBox(height: 8),
// 복원 버튼
Center(
child: TextButton(
onPressed: onRestore,
child: Text(
game_l10n.iapRestorePurchase,
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 9,
color: RetroColors.textDisabled,
decoration: TextDecoration.underline,
),
),
),
),
],
);
}
}
/// IAP 구매 팝업 다이얼로그
class _IapPurchaseDialog extends StatelessWidget {
const _IapPurchaseDialog({required this.price, this.onPurchase});
final String price;
final VoidCallback? onPurchase;
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: RetroColors.deepBrown,
shape: RoundedRectangleBorder(
side: const BorderSide(color: RetroColors.gold, width: 2),
borderRadius: BorderRadius.circular(4),
),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 360),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 타이틀
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.star, color: RetroColors.gold, size: 20),
const SizedBox(width: 8),
Text(
game_l10n.iapBenefitTitle,
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 12,
color: RetroColors.gold,
),
),
const SizedBox(width: 8),
const Icon(Icons.star, color: RetroColors.gold, size: 20),
],
),
const SizedBox(height: 20),
// 혜택 목록
_BenefitItem(icon: Icons.block, text: game_l10n.iapBenefit1),
const SizedBox(height: 8),
_BenefitItem(icon: Icons.flash_on, text: game_l10n.iapBenefit2),
const SizedBox(height: 8),
_BenefitItem(icon: Icons.undo, text: game_l10n.iapBenefit3),
const SizedBox(height: 8),
_BenefitItem(icon: Icons.casino, text: game_l10n.iapBenefit4),
const SizedBox(height: 8),
_BenefitItem(icon: Icons.speed, text: game_l10n.iapBenefit5),
const SizedBox(height: 8),
_BenefitItem(icon: Icons.inventory_2, text: game_l10n.iapBenefit6),
const SizedBox(height: 20),
// 가격 + 구매 버튼
Container(
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF5A4B3A), Color(0xFF4A3B2A)],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
border: Border.all(color: RetroColors.gold, width: 2),
borderRadius: BorderRadius.circular(4),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onPurchase,
borderRadius: BorderRadius.circular(2),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 14),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
game_l10n.iapPurchaseButton,
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 12,
color: RetroColors.gold,
),
),
const SizedBox(width: 12),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration(
color: RetroColors.gold,
borderRadius: BorderRadius.circular(4),
),
child: Text(
price,
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 11,
color: RetroColors.deepBrown,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
),
),
),
const SizedBox(height: 12),
// 취소 버튼
Center(
child: TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
game_l10n.buttonCancel,
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 10,
color: RetroColors.textDisabled,
),
),
),
),
],
),
),
),
);
}
}
/// 레트로 태그 칩
class _RetroTag extends StatelessWidget {
const _RetroTag({required this.icon, required this.label});
/// 혜택 항목 위젯
class _BenefitItem extends StatelessWidget {
const _BenefitItem({required this.icon, required this.text});
final IconData icon;
final String label;
final String text;
@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,
return Row(
children: [
Icon(icon, color: RetroColors.expGreen, size: 18),
const SizedBox(width: 12),
Expanded(
child: Text(
text,
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 11,
fontSize: 10,
color: RetroColors.textLight,
),
),
),
],
);
}
}
/// 이미 구매됨 뱃지
class _PurchasedBadge extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: RetroColors.panelBgLight,
border: Border.all(color: RetroColors.expGreen, width: 2),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.check_circle, color: RetroColors.expGreen, size: 20),
const SizedBox(width: 8),
Text(
game_l10n.iapAlreadyPurchased,
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 11,
color: RetroColors.expGreen,
),
),
],
),
);
}
}