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:
@@ -2,6 +2,7 @@ import 'dart:io' show Platform;
|
|||||||
|
|
||||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
import 'package:flutter/material.dart';
|
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/data/game_text_l10n.dart' as game_l10n;
|
||||||
import 'package:asciineverdie/l10n/app_localizations.dart';
|
import 'package:asciineverdie/l10n/app_localizations.dart';
|
||||||
@@ -18,9 +19,13 @@ class FrontScreen extends StatefulWidget {
|
|||||||
this.onHallOfFame,
|
this.onHallOfFame,
|
||||||
this.onLocalArena,
|
this.onLocalArena,
|
||||||
this.onSettings,
|
this.onSettings,
|
||||||
|
this.onPurchaseRemoveAds,
|
||||||
|
this.onRestorePurchase,
|
||||||
this.hasSaveFile = false,
|
this.hasSaveFile = false,
|
||||||
this.savedGamePreview,
|
this.savedGamePreview,
|
||||||
this.hallOfFameCount = 0,
|
this.hallOfFameCount = 0,
|
||||||
|
this.isAdRemovalPurchased = false,
|
||||||
|
this.removeAdsPrice,
|
||||||
this.routeObserver,
|
this.routeObserver,
|
||||||
this.onRefresh,
|
this.onRefresh,
|
||||||
});
|
});
|
||||||
@@ -40,6 +45,12 @@ class FrontScreen extends StatefulWidget {
|
|||||||
/// "Settings" 버튼 클릭 시 호출 (언어, 테마, 사운드)
|
/// "Settings" 버튼 클릭 시 호출 (언어, 테마, 사운드)
|
||||||
final void Function(BuildContext context)? onSettings;
|
final void Function(BuildContext context)? onSettings;
|
||||||
|
|
||||||
|
/// "광고 제거" 구매 버튼 클릭 시 호출
|
||||||
|
final Future<void> Function(BuildContext context)? onPurchaseRemoveAds;
|
||||||
|
|
||||||
|
/// "구매 복원" 버튼 클릭 시 호출
|
||||||
|
final Future<void> Function(BuildContext context)? onRestorePurchase;
|
||||||
|
|
||||||
/// 세이브 파일 존재 여부 (새 캐릭터 시 경고용)
|
/// 세이브 파일 존재 여부 (새 캐릭터 시 경고용)
|
||||||
final bool hasSaveFile;
|
final bool hasSaveFile;
|
||||||
|
|
||||||
@@ -49,6 +60,12 @@ class FrontScreen extends StatefulWidget {
|
|||||||
/// 명예의 전당 캐릭터 수 (아레나 활성화 조건: 2명 이상)
|
/// 명예의 전당 캐릭터 수 (아레나 활성화 조건: 2명 이상)
|
||||||
final int hallOfFameCount;
|
final int hallOfFameCount;
|
||||||
|
|
||||||
|
/// 광고 제거 구매 여부
|
||||||
|
final bool isAdRemovalPurchased;
|
||||||
|
|
||||||
|
/// 광고 제거 상품 가격 (null이면 스토어 비활성)
|
||||||
|
final String? removeAdsPrice;
|
||||||
|
|
||||||
/// RouteObserver (화면 복귀 시 갱신용)
|
/// RouteObserver (화면 복귀 시 갱신용)
|
||||||
final RouteObserver<ModalRoute<void>>? routeObserver;
|
final RouteObserver<ModalRoute<void>>? routeObserver;
|
||||||
|
|
||||||
@@ -132,8 +149,6 @@ class _FrontScreenState extends State<FrontScreen> with RouteAware {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
const _RetroHeader(),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
const _AnimationPanel(),
|
const _AnimationPanel(),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_ActionButtons(
|
_ActionButtons(
|
||||||
@@ -154,8 +169,17 @@ class _FrontScreenState extends State<FrontScreen> with RouteAware {
|
|||||||
onSettings: widget.onSettings != null
|
onSettings: widget.onSettings != null
|
||||||
? () => widget.onSettings!(context)
|
? () => widget.onSettings!(context)
|
||||||
: null,
|
: null,
|
||||||
|
onPurchaseRemoveAds:
|
||||||
|
widget.onPurchaseRemoveAds != null
|
||||||
|
? () => widget.onPurchaseRemoveAds!(context)
|
||||||
|
: null,
|
||||||
|
onRestorePurchase: widget.onRestorePurchase != null
|
||||||
|
? () => widget.onRestorePurchase!(context)
|
||||||
|
: null,
|
||||||
savedGamePreview: widget.savedGamePreview,
|
savedGamePreview: widget.savedGamePreview,
|
||||||
hallOfFameCount: widget.hallOfFameCount,
|
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 {
|
class _AnimationPanel extends StatelessWidget {
|
||||||
const _AnimationPanel();
|
const _AnimationPanel();
|
||||||
|
|
||||||
@@ -238,8 +211,25 @@ class _AnimationPanel extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return RetroPanel(
|
return RetroGoldPanel(
|
||||||
title: 'BATTLE',
|
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),
|
padding: const EdgeInsets.all(8),
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: _getAspectRatio(),
|
aspectRatio: _getAspectRatio(),
|
||||||
@@ -257,8 +247,12 @@ class _ActionButtons extends StatelessWidget {
|
|||||||
this.onHallOfFame,
|
this.onHallOfFame,
|
||||||
this.onLocalArena,
|
this.onLocalArena,
|
||||||
this.onSettings,
|
this.onSettings,
|
||||||
|
this.onPurchaseRemoveAds,
|
||||||
|
this.onRestorePurchase,
|
||||||
this.savedGamePreview,
|
this.savedGamePreview,
|
||||||
this.hallOfFameCount = 0,
|
this.hallOfFameCount = 0,
|
||||||
|
this.isAdRemovalPurchased = false,
|
||||||
|
this.removeAdsPrice,
|
||||||
});
|
});
|
||||||
|
|
||||||
final VoidCallback? onNewCharacter;
|
final VoidCallback? onNewCharacter;
|
||||||
@@ -266,8 +260,12 @@ class _ActionButtons extends StatelessWidget {
|
|||||||
final VoidCallback? onHallOfFame;
|
final VoidCallback? onHallOfFame;
|
||||||
final VoidCallback? onLocalArena;
|
final VoidCallback? onLocalArena;
|
||||||
final VoidCallback? onSettings;
|
final VoidCallback? onSettings;
|
||||||
|
final VoidCallback? onPurchaseRemoveAds;
|
||||||
|
final VoidCallback? onRestorePurchase;
|
||||||
final SavedGamePreview? savedGamePreview;
|
final SavedGamePreview? savedGamePreview;
|
||||||
final int hallOfFameCount;
|
final int hallOfFameCount;
|
||||||
|
final bool isAdRemovalPurchased;
|
||||||
|
final String? removeAdsPrice;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -323,6 +321,24 @@ class _ActionButtons extends StatelessWidget {
|
|||||||
onPressed: onSettings,
|
onPressed: onSettings,
|
||||||
isPrimary: false,
|
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) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
child: Text(
|
child: FutureBuilder<PackageInfo>(
|
||||||
game_l10n.copyrightText,
|
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,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontFamily: 'PressStart2P',
|
fontFamily: 'PressStart2P',
|
||||||
fontSize: 7,
|
fontSize: 7,
|
||||||
color: RetroColors.textDisabled,
|
color: RetroColors.textDisabled,
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 레트로 태그 칩
|
/// IAP 구매 버튼 (광고 제거)
|
||||||
class _RetroTag extends StatelessWidget {
|
class _IapPurchaseButton extends StatelessWidget {
|
||||||
const _RetroTag({required this.icon, required this.label});
|
const _IapPurchaseButton({
|
||||||
|
required this.price,
|
||||||
|
this.onPurchase,
|
||||||
|
this.onRestore,
|
||||||
|
});
|
||||||
|
|
||||||
final IconData icon;
|
final String price;
|
||||||
final String label;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Column(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
// 구매 버튼 (클릭 시 팝업)
|
||||||
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: RetroColors.panelBgLight,
|
gradient: const LinearGradient(
|
||||||
border: Border.all(color: RetroColors.panelBorderInner, width: 1),
|
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(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, color: RetroColors.gold, size: 12),
|
const Icon(Icons.block, color: RetroColors.gold, size: 24),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
Text(
|
Text(
|
||||||
label,
|
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(
|
style: const TextStyle(
|
||||||
fontFamily: 'PressStart2P',
|
fontFamily: 'PressStart2P',
|
||||||
fontSize: 11,
|
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 _BenefitItem extends StatelessWidget {
|
||||||
|
const _BenefitItem({required this.icon, required this.text});
|
||||||
|
|
||||||
|
final IconData icon;
|
||||||
|
final String text;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, color: RetroColors.expGreen, size: 18),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 10,
|
||||||
color: RetroColors.textLight,
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import 'package:flutter/services.dart' show KeyDownEvent, LogicalKeyboardKey;
|
|||||||
import 'package:asciineverdie/data/game_text_l10n.dart' as game_l10n;
|
import 'package:asciineverdie/data/game_text_l10n.dart' as game_l10n;
|
||||||
import 'package:asciineverdie/data/skill_data.dart';
|
import 'package:asciineverdie/data/skill_data.dart';
|
||||||
import 'package:asciineverdie/src/core/engine/iap_service.dart';
|
import 'package:asciineverdie/src/core/engine/iap_service.dart';
|
||||||
import 'package:asciineverdie/src/core/engine/return_rewards_service.dart';
|
import 'package:asciineverdie/src/core/model/treasure_chest.dart';
|
||||||
import 'package:asciineverdie/data/story_data.dart';
|
import 'package:asciineverdie/data/story_data.dart';
|
||||||
import 'package:asciineverdie/l10n/app_localizations.dart';
|
import 'package:asciineverdie/l10n/app_localizations.dart';
|
||||||
import 'package:asciineverdie/src/core/animation/ascii_animation_type.dart';
|
import 'package:asciineverdie/src/core/animation/ascii_animation_type.dart';
|
||||||
@@ -51,8 +51,6 @@ class GamePlayScreen extends StatefulWidget {
|
|||||||
this.audioService,
|
this.audioService,
|
||||||
this.forceCarouselLayout = false,
|
this.forceCarouselLayout = false,
|
||||||
this.forceDesktopLayout = false,
|
this.forceDesktopLayout = false,
|
||||||
this.onThemeModeChange,
|
|
||||||
this.currentThemeMode = ThemeMode.system,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final GameSessionController controller;
|
final GameSessionController controller;
|
||||||
@@ -66,12 +64,6 @@ class GamePlayScreen extends StatefulWidget {
|
|||||||
/// 테스트 모드: 모바일에서도 데스크톱 3패널 레이아웃 강제 사용
|
/// 테스트 모드: 모바일에서도 데스크톱 3패널 레이아웃 강제 사용
|
||||||
final bool forceDesktopLayout;
|
final bool forceDesktopLayout;
|
||||||
|
|
||||||
/// 테마 모드 변경 콜백
|
|
||||||
final void Function(ThemeMode mode)? onThemeModeChange;
|
|
||||||
|
|
||||||
/// 현재 테마 모드
|
|
||||||
final ThemeMode currentThemeMode;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<GamePlayScreen> createState() => _GamePlayScreenState();
|
State<GamePlayScreen> createState() => _GamePlayScreenState();
|
||||||
}
|
}
|
||||||
@@ -316,8 +308,6 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
builder: (_) => GamePlayScreen(
|
builder: (_) => GamePlayScreen(
|
||||||
controller: widget.controller,
|
controller: widget.controller,
|
||||||
audioService: widget.audioService,
|
audioService: widget.audioService,
|
||||||
currentThemeMode: widget.currentThemeMode,
|
|
||||||
onThemeModeChange: widget.onThemeModeChange,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -407,15 +397,19 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 복귀 보상 다이얼로그 표시 (Phase 7)
|
/// 복귀 보상 다이얼로그 표시 (Phase 7)
|
||||||
void _showReturnRewardsDialog(ReturnReward reward) {
|
void _showReturnRewardsDialog(ReturnChestReward reward) {
|
||||||
// 잠시 후 다이얼로그 표시 (게임 시작 후)
|
// 잠시 후 다이얼로그 표시 (게임 시작 후)
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
final state = widget.controller.state;
|
||||||
|
if (state == null) return;
|
||||||
|
|
||||||
ReturnRewardsDialog.show(
|
ReturnRewardsDialog.show(
|
||||||
context,
|
context,
|
||||||
reward: reward,
|
reward: reward,
|
||||||
onClaim: (totalGold) {
|
playerLevel: state.traits.level,
|
||||||
widget.controller.applyReturnReward(totalGold);
|
onClaim: (rewards) {
|
||||||
|
widget.controller.applyReturnReward(rewards);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -436,10 +430,6 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
SettingsScreen.show(
|
SettingsScreen.show(
|
||||||
context,
|
context,
|
||||||
settingsRepository: settingsRepo,
|
settingsRepository: settingsRepo,
|
||||||
currentThemeMode: widget.currentThemeMode,
|
|
||||||
onThemeModeChange: (mode) {
|
|
||||||
widget.onThemeModeChange?.call(mode);
|
|
||||||
},
|
|
||||||
onLocaleChange: (locale) async {
|
onLocaleChange: (locale) async {
|
||||||
// 안전한 언어 변경: 전체 화면 재생성
|
// 안전한 언어 변경: 전체 화면 재생성
|
||||||
final navigator = Navigator.of(this.context);
|
final navigator = Navigator.of(this.context);
|
||||||
@@ -452,8 +442,6 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
builder: (_) => GamePlayScreen(
|
builder: (_) => GamePlayScreen(
|
||||||
controller: widget.controller,
|
controller: widget.controller,
|
||||||
audioService: widget.audioService,
|
audioService: widget.audioService,
|
||||||
currentThemeMode: widget.currentThemeMode,
|
|
||||||
onThemeModeChange: widget.onThemeModeChange,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -586,6 +574,10 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
widget.controller.loop?.cycleSpeed();
|
widget.controller.loop?.cycleSpeed();
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
|
onSetSpeed: (speed) {
|
||||||
|
widget.controller.loop?.setSpeed(speed);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
// 특수 애니메이션 중에는 일시정지 상태로 표시하지 않음
|
// 특수 애니메이션 중에는 일시정지 상태로 표시하지 않음
|
||||||
isPaused:
|
isPaused:
|
||||||
!widget.controller.isRunning && _specialAnimation == null,
|
!widget.controller.isRunning && _specialAnimation == null,
|
||||||
@@ -620,8 +612,6 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
builder: (_) => GamePlayScreen(
|
builder: (_) => GamePlayScreen(
|
||||||
controller: widget.controller,
|
controller: widget.controller,
|
||||||
audioService: widget.audioService,
|
audioService: widget.audioService,
|
||||||
currentThemeMode: widget.currentThemeMode,
|
|
||||||
onThemeModeChange: widget.onThemeModeChange,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -637,8 +627,6 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
currentThemeMode: widget.currentThemeMode,
|
|
||||||
onThemeModeChange: widget.onThemeModeChange,
|
|
||||||
// 사운드 설정
|
// 사운드 설정
|
||||||
bgmVolume: _audioController.bgmVolume,
|
bgmVolume: _audioController.bgmVolume,
|
||||||
sfxVolume: _audioController.sfxVolume,
|
sfxVolume: _audioController.sfxVolume,
|
||||||
@@ -666,11 +654,13 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
navigator.popUntil((route) => route.isFirst);
|
navigator.popUntil((route) => route.isFirst);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 수익화 버프 (자동부활, 5배속)
|
// 수익화 버프 (자동부활, 광고배속)
|
||||||
autoReviveEndMs: widget.controller.monetization.autoReviveEndMs,
|
autoReviveEndMs: widget.controller.monetization.autoReviveEndMs,
|
||||||
speedBoostEndMs: widget.controller.monetization.speedBoostEndMs,
|
speedBoostEndMs: widget.controller.monetization.speedBoostEndMs,
|
||||||
isPaidUser: widget.controller.monetization.isPaidUser,
|
isPaidUser: widget.controller.monetization.isPaidUser,
|
||||||
onSpeedBoostActivate: _handleSpeedBoost,
|
onSpeedBoostActivate: _handleSpeedBoost,
|
||||||
|
adSpeedMultiplier: widget.controller.adSpeedMultiplier,
|
||||||
|
has2xUnlocked: widget.controller.has2xUnlocked,
|
||||||
),
|
),
|
||||||
// 사망 오버레이
|
// 사망 오버레이
|
||||||
if (state.isDead && state.deathInfo != null)
|
if (state.isDead && state.deathInfo != null)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import 'package:asciineverdie/src/core/model/game_state.dart';
|
|||||||
import 'package:asciineverdie/src/core/model/game_statistics.dart';
|
import 'package:asciineverdie/src/core/model/game_statistics.dart';
|
||||||
import 'package:asciineverdie/src/core/model/hall_of_fame.dart';
|
import 'package:asciineverdie/src/core/model/hall_of_fame.dart';
|
||||||
import 'package:asciineverdie/src/core/model/monetization_state.dart';
|
import 'package:asciineverdie/src/core/model/monetization_state.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/treasure_chest.dart';
|
||||||
import 'package:asciineverdie/src/core/storage/hall_of_fame_storage.dart';
|
import 'package:asciineverdie/src/core/storage/hall_of_fame_storage.dart';
|
||||||
import 'package:asciineverdie/src/core/storage/save_manager.dart';
|
import 'package:asciineverdie/src/core/storage/save_manager.dart';
|
||||||
import 'package:asciineverdie/src/core/storage/statistics_storage.dart';
|
import 'package:asciineverdie/src/core/storage/statistics_storage.dart';
|
||||||
@@ -64,14 +65,16 @@ class GameSessionController extends ChangeNotifier {
|
|||||||
Timer? _speedBoostTimer;
|
Timer? _speedBoostTimer;
|
||||||
int _speedBoostRemainingSeconds = 0;
|
int _speedBoostRemainingSeconds = 0;
|
||||||
static const int _speedBoostDuration = 300; // 5분
|
static const int _speedBoostDuration = 300; // 5분
|
||||||
static const int _speedBoostMultiplier = 5; // 5x 속도
|
|
||||||
|
/// 광고 배속 배율 (릴리즈: 5x, 디버그빌드+디버그모드: 20x)
|
||||||
|
int get _speedBoostMultiplier => (kDebugMode && _cheatsEnabled) ? 20 : 5;
|
||||||
|
|
||||||
// 복귀 보상 상태 (Phase 7)
|
// 복귀 보상 상태 (Phase 7)
|
||||||
MonetizationState _monetization = MonetizationState.initial();
|
MonetizationState _monetization = MonetizationState.initial();
|
||||||
ReturnReward? _pendingReturnReward;
|
ReturnChestReward? _pendingReturnReward;
|
||||||
|
|
||||||
/// 복귀 보상 콜백 (UI에서 다이얼로그 표시용)
|
/// 복귀 보상 콜백 (UI에서 다이얼로그 표시용)
|
||||||
void Function(ReturnReward reward)? onReturnRewardAvailable;
|
void Function(ReturnChestReward reward)? onReturnRewardAvailable;
|
||||||
|
|
||||||
// 통계 관련 필드
|
// 통계 관련 필드
|
||||||
SessionStatistics _sessionStats = SessionStatistics.empty();
|
SessionStatistics _sessionStats = SessionStatistics.empty();
|
||||||
@@ -105,6 +108,12 @@ class GameSessionController extends ChangeNotifier {
|
|||||||
/// 현재 ProgressLoop 인스턴스 (치트 기능용)
|
/// 현재 ProgressLoop 인스턴스 (치트 기능용)
|
||||||
ProgressLoop? get loop => _loop;
|
ProgressLoop? get loop => _loop;
|
||||||
|
|
||||||
|
/// 광고 배속 배율 (릴리즈: 5x, 디버그빌드+디버그모드: 20x)
|
||||||
|
int get adSpeedMultiplier => _speedBoostMultiplier;
|
||||||
|
|
||||||
|
/// 2x 배속 해금 여부 (명예의 전당에 캐릭터가 있으면 true)
|
||||||
|
bool get has2xUnlocked => _loop?.availableSpeeds.contains(2) ?? false;
|
||||||
|
|
||||||
Future<void> startNew(
|
Future<void> startNew(
|
||||||
GameState initialState, {
|
GameState initialState, {
|
||||||
bool cheatsEnabled = false,
|
bool cheatsEnabled = false,
|
||||||
@@ -172,13 +181,16 @@ class GameSessionController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 가용 배속 목록 반환
|
/// 가용 배속 목록 반환
|
||||||
/// - 디버그 모드(치트 활성화): [1, 2, 20] (터보 모드 포함)
|
///
|
||||||
/// - 일반 모드: [1, 2] (5x는 광고 버프로만 활성화)
|
/// - 기본: [1] (1x만)
|
||||||
|
/// - 명예의 전당에 캐릭터 있으면: [1, 2] (2x 해금)
|
||||||
|
/// - 광고 배속(5x/20x)은 별도 버프로만 활성화
|
||||||
Future<List<int>> _getAvailableSpeeds() async {
|
Future<List<int>> _getAvailableSpeeds() async {
|
||||||
if (_cheatsEnabled) {
|
final hallOfFame = await _hallOfFameStorage.load();
|
||||||
return [1, 2, 20];
|
if (hallOfFame.entries.isNotEmpty) {
|
||||||
|
return [1, 2]; // 명예의 전당 캐릭터 있으면 2x 해금
|
||||||
}
|
}
|
||||||
return [1, 2];
|
return [1]; // 기본: 1x만
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 이전 값 초기화 (통계 변화 추적용)
|
/// 이전 값 초기화 (통계 변화 추적용)
|
||||||
@@ -700,7 +712,7 @@ class GameSessionController extends ChangeNotifier {
|
|||||||
MonetizationState get monetization => _monetization;
|
MonetizationState get monetization => _monetization;
|
||||||
|
|
||||||
/// 대기 중인 복귀 보상
|
/// 대기 중인 복귀 보상
|
||||||
ReturnReward? get pendingReturnReward => _pendingReturnReward;
|
ReturnChestReward? get pendingReturnReward => _pendingReturnReward;
|
||||||
|
|
||||||
/// 복귀 보상 체크 (로드 시 호출)
|
/// 복귀 보상 체크 (로드 시 호출)
|
||||||
void _checkReturnRewards(GameState loaded) {
|
void _checkReturnRewards(GameState loaded) {
|
||||||
@@ -715,17 +727,17 @@ class GameSessionController extends ChangeNotifier {
|
|||||||
final reward = rewardsService.calculateReward(
|
final reward = rewardsService.calculateReward(
|
||||||
lastPlayTime: lastPlayTime,
|
lastPlayTime: lastPlayTime,
|
||||||
currentTime: DateTime.now(),
|
currentTime: DateTime.now(),
|
||||||
playerLevel: loaded.traits.level,
|
isPaidUser: _monetization.isPaidUser,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (reward.hasReward) {
|
if (reward.hasReward) {
|
||||||
_pendingReturnReward = reward;
|
_pendingReturnReward = reward;
|
||||||
debugPrint('[ReturnRewards] Reward available: ${reward.goldReward} gold, '
|
debugPrint('[ReturnRewards] Reward available: ${reward.chestCount} chests, '
|
||||||
'${reward.hoursAway} hours away');
|
'${reward.hoursAway} hours away');
|
||||||
|
|
||||||
// UI에서 다이얼로그 표시를 위해 콜백 호출
|
// UI에서 다이얼로그 표시를 위해 콜백 호출
|
||||||
// startNew 후에 호출하도록 딜레이
|
// startNew 후에 호출하도록 딜레이
|
||||||
Future.delayed(const Duration(milliseconds: 500), () {
|
Future<void>.delayed(const Duration(milliseconds: 500), () {
|
||||||
if (_pendingReturnReward != null) {
|
if (_pendingReturnReward != null) {
|
||||||
onReturnRewardAvailable?.call(_pendingReturnReward!);
|
onReturnRewardAvailable?.call(_pendingReturnReward!);
|
||||||
}
|
}
|
||||||
@@ -733,23 +745,86 @@ class GameSessionController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 복귀 보상 수령 완료 (골드 적용)
|
/// 복귀 보상 수령 완료 (상자 보상 적용)
|
||||||
///
|
///
|
||||||
/// [totalGold] 수령한 총 골드 (기본 + 보너스)
|
/// [rewards] 오픈된 상자 보상 목록
|
||||||
void applyReturnReward(int totalGold) {
|
void applyReturnReward(List<ChestReward> rewards) {
|
||||||
if (_state == null) return;
|
if (_state == null) return;
|
||||||
if (totalGold <= 0) {
|
if (rewards.isEmpty) {
|
||||||
// 보상 없이 건너뛴 경우
|
// 보상 없이 건너뛴 경우
|
||||||
_pendingReturnReward = null;
|
_pendingReturnReward = null;
|
||||||
debugPrint('[ReturnRewards] Reward skipped');
|
debugPrint('[ReturnRewards] Reward skipped');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 골드 추가
|
var updatedState = _state!;
|
||||||
final updatedInventory = _state!.inventory.copyWith(
|
|
||||||
gold: _state!.inventory.gold + totalGold,
|
// 보상 적용
|
||||||
|
for (final reward in rewards) {
|
||||||
|
switch (reward.type) {
|
||||||
|
case ChestRewardType.equipment:
|
||||||
|
if (reward.equipment != null) {
|
||||||
|
// 현재 장비와 비교하여 더 좋으면 자동 장착
|
||||||
|
final slotIndex = reward.equipment!.slot.index;
|
||||||
|
final currentItem = updatedState.equipment.getItemByIndex(slotIndex);
|
||||||
|
if (currentItem.isEmpty ||
|
||||||
|
reward.equipment!.itemWeight > currentItem.itemWeight) {
|
||||||
|
updatedState = updatedState.copyWith(
|
||||||
|
equipment: updatedState.equipment.setItemByIndex(
|
||||||
|
slotIndex,
|
||||||
|
reward.equipment!,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
_state = _state!.copyWith(inventory: updatedInventory);
|
debugPrint('[ReturnRewards] Equipped: ${reward.equipment!.name}');
|
||||||
|
} else {
|
||||||
|
// 더 좋지 않으면 판매 (골드로 변환)
|
||||||
|
final sellPrice =
|
||||||
|
(reward.equipment!.level * 50 * 0.3).round().clamp(1, 99999);
|
||||||
|
updatedState = updatedState.copyWith(
|
||||||
|
inventory: updatedState.inventory.copyWith(
|
||||||
|
gold: updatedState.inventory.gold + sellPrice,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
debugPrint('[ReturnRewards] Sold: ${reward.equipment!.name} '
|
||||||
|
'for $sellPrice gold');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case ChestRewardType.potion:
|
||||||
|
if (reward.potionId != null) {
|
||||||
|
updatedState = updatedState.copyWith(
|
||||||
|
potionInventory: updatedState.potionInventory.addPotion(
|
||||||
|
reward.potionId!,
|
||||||
|
reward.potionCount ?? 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
debugPrint('[ReturnRewards] Added potion: ${reward.potionId} '
|
||||||
|
'x${reward.potionCount}');
|
||||||
|
}
|
||||||
|
case ChestRewardType.gold:
|
||||||
|
if (reward.gold != null && reward.gold! > 0) {
|
||||||
|
updatedState = updatedState.copyWith(
|
||||||
|
inventory: updatedState.inventory.copyWith(
|
||||||
|
gold: updatedState.inventory.gold + reward.gold!,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
debugPrint('[ReturnRewards] Added gold: ${reward.gold}');
|
||||||
|
}
|
||||||
|
case ChestRewardType.experience:
|
||||||
|
if (reward.experience != null && reward.experience! > 0) {
|
||||||
|
updatedState = updatedState.copyWith(
|
||||||
|
progress: updatedState.progress.copyWith(
|
||||||
|
exp: updatedState.progress.exp.copyWith(
|
||||||
|
position:
|
||||||
|
updatedState.progress.exp.position + reward.experience!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
debugPrint('[ReturnRewards] Added experience: ${reward.experience}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_state = updatedState;
|
||||||
|
|
||||||
// 저장
|
// 저장
|
||||||
unawaited(saveManager.saveState(
|
unawaited(saveManager.saveState(
|
||||||
@@ -761,7 +836,7 @@ class GameSessionController extends ChangeNotifier {
|
|||||||
_pendingReturnReward = null;
|
_pendingReturnReward = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
debugPrint('[ReturnRewards] Reward applied: $totalGold gold');
|
debugPrint('[ReturnRewards] Rewards applied: ${rewards.length} items');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 복귀 보상 건너뛰기
|
/// 복귀 보상 건너뛰기
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -41,6 +41,7 @@ class EnhancedAnimationPanel extends StatefulWidget {
|
|||||||
this.speedBoostEndMs,
|
this.speedBoostEndMs,
|
||||||
this.isPaidUser = false,
|
this.isPaidUser = false,
|
||||||
this.onSpeedBoostActivate,
|
this.onSpeedBoostActivate,
|
||||||
|
this.adSpeedMultiplier = 5,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ProgressState progress;
|
final ProgressState progress;
|
||||||
@@ -75,12 +76,15 @@ class EnhancedAnimationPanel extends StatefulWidget {
|
|||||||
/// 5배속 버프 종료 시점 (elapsedMs 기준, null이면 비활성)
|
/// 5배속 버프 종료 시점 (elapsedMs 기준, null이면 비활성)
|
||||||
final int? speedBoostEndMs;
|
final int? speedBoostEndMs;
|
||||||
|
|
||||||
/// 유료 유저 여부 (5배속 항상 활성)
|
/// 유료 유저 여부 (광고배속 항상 활성)
|
||||||
final bool isPaidUser;
|
final bool isPaidUser;
|
||||||
|
|
||||||
/// 5배속 버프 활성화 콜백 (광고 시청)
|
/// 광고 배속 활성화 콜백 (광고 시청)
|
||||||
final VoidCallback? onSpeedBoostActivate;
|
final VoidCallback? onSpeedBoostActivate;
|
||||||
|
|
||||||
|
/// 광고 배속 배율 (릴리즈: 5x, 디버그빌드+디버그모드: 20x)
|
||||||
|
final int adSpeedMultiplier;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<EnhancedAnimationPanel> createState() => _EnhancedAnimationPanelState();
|
State<EnhancedAnimationPanel> createState() => _EnhancedAnimationPanelState();
|
||||||
}
|
}
|
||||||
@@ -284,14 +288,14 @@ class _EnhancedAnimationPanelState extends State<EnhancedAnimationPanel>
|
|||||||
color: Colors.green,
|
color: Colors.green,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// 우상단: 5배속 버프
|
// 우상단: 광고배속 버프 (버프 활성 시에만)
|
||||||
if (_speedBoostRemainingMs > 0 || widget.isPaidUser)
|
if (_speedBoostRemainingMs > 0 || widget.isPaidUser)
|
||||||
Positioned(
|
Positioned(
|
||||||
right: 4,
|
right: 4,
|
||||||
top: 4,
|
top: 4,
|
||||||
child: _buildBuffChip(
|
child: _buildBuffChip(
|
||||||
icon: '⚡',
|
icon: '⚡',
|
||||||
label: '5x',
|
label: '${widget.adSpeedMultiplier}x',
|
||||||
remainingMs: widget.isPaidUser ? -1 : _speedBoostRemainingMs,
|
remainingMs: widget.isPaidUser ? -1 : _speedBoostRemainingMs,
|
||||||
color: Colors.orange,
|
color: Colors.orange,
|
||||||
isPermanent: widget.isPaidUser,
|
isPermanent: widget.isPaidUser,
|
||||||
@@ -303,7 +307,7 @@ class _EnhancedAnimationPanelState extends State<EnhancedAnimationPanel>
|
|||||||
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
// 상태 바 영역: HP/MP (40%) + 컨트롤 (20%) + 몬스터 HP (40%)
|
// 상태 바 영역: HP/MP (40%) + 빈공간 (20%) + 몬스터 HP (40%)
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 48,
|
height: 48,
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -322,11 +326,8 @@ class _EnhancedAnimationPanelState extends State<EnhancedAnimationPanel>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 중앙: 컨트롤 버튼 (20%)
|
// 중앙: 빈 공간 (20%)
|
||||||
Expanded(
|
const Spacer(flex: 1),
|
||||||
flex: 1,
|
|
||||||
child: _buildControlButtons(),
|
|
||||||
),
|
|
||||||
|
|
||||||
// 우측: 몬스터 HP (전투 중) 또는 빈 공간 (40%)
|
// 우측: 몬스터 HP (전투 중) 또는 빈 공간 (40%)
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -673,44 +674,41 @@ class _EnhancedAnimationPanelState extends State<EnhancedAnimationPanel>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 컨트롤 버튼 (중앙 영역)
|
/// 속도 컨트롤 버튼 (태스크 프로그레스 바 우측)
|
||||||
Widget _buildControlButtons() {
|
///
|
||||||
return Column(
|
/// - 일반배속: 1x (기본) ↔ 2x (명예의 전당 해금)
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
/// - 광고배속: 릴리즈 5x, 디버그빌드+디버그모드 20x
|
||||||
children: [
|
Widget _buildSpeedControls() {
|
||||||
// 상단: 속도 버튼 (1x ↔ 2x)
|
|
||||||
_buildCompactSpeedButton(),
|
|
||||||
const SizedBox(height: 2),
|
|
||||||
// 하단: 5x 광고 버튼 (2x일 때만 표시)
|
|
||||||
_buildAdSpeedButton(),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 컴팩트 속도 버튼 (1x ↔ 2x 사이클)
|
|
||||||
Widget _buildCompactSpeedButton() {
|
|
||||||
final isSpeedBoostActive = _speedBoostRemainingMs > 0 || widget.isPaidUser;
|
final isSpeedBoostActive = _speedBoostRemainingMs > 0 || widget.isPaidUser;
|
||||||
|
final adSpeed = widget.adSpeedMultiplier;
|
||||||
|
// 2x일 때 광고 버튼 표시 (버프 비활성이고 무료유저)
|
||||||
|
final showAdButton =
|
||||||
|
widget.speedMultiplier == 2 && !isSpeedBoostActive && !widget.isPaidUser;
|
||||||
|
|
||||||
return SizedBox(
|
return Row(
|
||||||
width: 32,
|
mainAxisSize: MainAxisSize.min,
|
||||||
height: 22,
|
children: [
|
||||||
|
// 속도 사이클 버튼 (1x ↔ 2x, 버프 활성시 광고배속)
|
||||||
|
SizedBox(
|
||||||
|
width: 44,
|
||||||
|
height: 32,
|
||||||
child: OutlinedButton(
|
child: OutlinedButton(
|
||||||
onPressed: widget.onSpeedCycle,
|
onPressed: widget.onSpeedCycle,
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
side: BorderSide(
|
side: BorderSide(
|
||||||
color: isSpeedBoostActive
|
color: isSpeedBoostActive
|
||||||
? Colors.orange
|
? Colors.orange
|
||||||
: widget.speedMultiplier > 1
|
: widget.speedMultiplier > 1
|
||||||
? Theme.of(context).colorScheme.primary
|
? Theme.of(context).colorScheme.primary
|
||||||
: Theme.of(context).colorScheme.outline.withValues(alpha: 0.5),
|
: Theme.of(context).colorScheme.outline,
|
||||||
|
width: isSpeedBoostActive ? 2 : 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
isSpeedBoostActive ? '5x' : '${widget.speedMultiplier}x',
|
isSpeedBoostActive ? '${adSpeed}x' : '${widget.speedMultiplier}x',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: isSpeedBoostActive
|
color: isSpeedBoostActive
|
||||||
? Colors.orange
|
? Colors.orange
|
||||||
@@ -720,38 +718,31 @@ class _EnhancedAnimationPanelState extends State<EnhancedAnimationPanel>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
}
|
// 광고 배속 버튼 (2x일 때만 표시)
|
||||||
|
if (showAdButton) ...[
|
||||||
/// 5x 광고 버튼 (2x일 때만 표시)
|
const SizedBox(width: 4),
|
||||||
Widget _buildAdSpeedButton() {
|
SizedBox(
|
||||||
final isSpeedBoostActive = _speedBoostRemainingMs > 0 || widget.isPaidUser;
|
width: 52,
|
||||||
// 2x이고 5배속 버프 비활성이고 무료유저일 때만 표시
|
height: 32,
|
||||||
final showAdButton =
|
|
||||||
widget.speedMultiplier == 2 && !isSpeedBoostActive && !widget.isPaidUser;
|
|
||||||
|
|
||||||
if (!showAdButton) {
|
|
||||||
return const SizedBox(height: 22);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SizedBox(
|
|
||||||
height: 22,
|
|
||||||
child: OutlinedButton(
|
child: OutlinedButton(
|
||||||
onPressed: widget.onSpeedBoostActivate,
|
onPressed: widget.onSpeedBoostActivate,
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
padding: EdgeInsets.zero,
|
||||||
visualDensity: VisualDensity.compact,
|
side: const BorderSide(color: Colors.orange, width: 1.5),
|
||||||
side: const BorderSide(color: Colors.orange),
|
|
||||||
),
|
),
|
||||||
child: const Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text('▶', style: TextStyle(fontSize: 8, color: Colors.orange)),
|
const Text(
|
||||||
SizedBox(width: 2),
|
'▶',
|
||||||
|
style: TextStyle(fontSize: 9, color: Colors.orange),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 2),
|
||||||
Text(
|
Text(
|
||||||
'5x',
|
'${adSpeed}x',
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.orange,
|
color: Colors.orange,
|
||||||
),
|
),
|
||||||
@@ -759,6 +750,9 @@ class _EnhancedAnimationPanelState extends State<EnhancedAnimationPanel>
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -775,7 +769,7 @@ class _EnhancedAnimationPanelState extends State<EnhancedAnimationPanel>
|
|||||||
return widget.progress.currentTask.caption;
|
return widget.progress.currentTask.caption;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 태스크 프로그레스 바
|
/// 태스크 프로그레스 바 + 속도 컨트롤
|
||||||
Widget _buildTaskProgress() {
|
Widget _buildTaskProgress() {
|
||||||
final task = widget.progress.task;
|
final task = widget.progress.task;
|
||||||
final progressValue = task.max > 0
|
final progressValue = task.max > 0
|
||||||
@@ -792,7 +786,13 @@ class _EnhancedAnimationPanelState extends State<EnhancedAnimationPanel>
|
|||||||
? grade.displayColor
|
? grade.displayColor
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return Column(
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// 좌측: 캡션 + 프로그레스 바
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// 캡션 (등급에 따른 접두사 및 색상)
|
// 캡션 (등급에 따른 접두사 및 색상)
|
||||||
Text.rich(
|
Text.rich(
|
||||||
@@ -808,7 +808,8 @@ class _EnhancedAnimationPanelState extends State<EnhancedAnimationPanel>
|
|||||||
),
|
),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: _getStatusMessage(),
|
text: _getStatusMessage(),
|
||||||
style: gradeColor != null ? TextStyle(color: gradeColor) : null,
|
style:
|
||||||
|
gradeColor != null ? TextStyle(color: gradeColor) : null,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -819,9 +820,7 @@ class _EnhancedAnimationPanelState extends State<EnhancedAnimationPanel>
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
// 프로그레스 바
|
// 프로그레스 바
|
||||||
Padding(
|
LinearProgressIndicator(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
||||||
child: LinearProgressIndicator(
|
|
||||||
value: progressValue,
|
value: progressValue,
|
||||||
backgroundColor: Theme.of(
|
backgroundColor: Theme.of(
|
||||||
context,
|
context,
|
||||||
@@ -831,7 +830,12 @@ class _EnhancedAnimationPanelState extends State<EnhancedAnimationPanel>
|
|||||||
),
|
),
|
||||||
minHeight: 10,
|
minHeight: 10,
|
||||||
),
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
// 우측: 속도 컨트롤
|
||||||
|
_buildSpeedControls(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,13 +108,13 @@ class _BasicsHelpView extends StatelessWidget {
|
|||||||
? 'ゲーム紹介'
|
? 'ゲーム紹介'
|
||||||
: 'About the Game',
|
: 'About the Game',
|
||||||
content: isKorean
|
content: isKorean
|
||||||
? 'Askii Never Die는 자동 진행 RPG입니다. 캐릭터가 자동으로 몬스터와 싸우고, '
|
? 'Askii Never Die는 완전 자동 진행 RPG입니다. 캐릭터가 자동으로 몬스터와 싸우고, '
|
||||||
'퀘스트를 완료하며, 레벨업합니다. 여러분은 장비와 스킬을 관리하면 됩니다.'
|
'퀘스트를 완료하며, 레벨업합니다. 장비와 스킬도 자동으로 획득/장착됩니다.'
|
||||||
: isJapanese
|
: isJapanese
|
||||||
? 'Askii Never Dieは自動進行RPGです。キャラクターが自動でモンスターと戦い、'
|
? 'Askii Never Dieは完全自動進行RPGです。キャラクターが自動でモンスターと戦い、'
|
||||||
'クエストを完了し、レベルアップします。装備とスキルの管理だけで大丈夫です。'
|
'クエストを完了し、レベルアップします。装備とスキルも自動で獲得・装着されます。'
|
||||||
: 'Askii Never Die is an idle RPG. Your character automatically fights monsters, '
|
: 'Askii Never Die is a fully automatic idle RPG. Your character automatically fights monsters, '
|
||||||
'completes quests, and levels up. You manage equipment and skills.',
|
'completes quests, and levels up. Equipment and skills are auto-acquired and equipped.',
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_HelpSection(
|
_HelpSection(
|
||||||
@@ -214,20 +214,26 @@ class _CombatHelpView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_HelpSection(
|
_HelpSection(
|
||||||
icon: '♥',
|
icon: '♻',
|
||||||
title: isKorean
|
title: isKorean
|
||||||
? '사망과 부활'
|
? '부활 시스템'
|
||||||
: isJapanese
|
: isJapanese
|
||||||
? '死亡と復活'
|
? '復活システム'
|
||||||
: 'Death & Revival',
|
: 'Revival System',
|
||||||
content: isKorean
|
content: isKorean
|
||||||
? 'HP가 0이 되면 사망합니다. 사망 시 장비 하나를 제물로 바쳐 부활할 수 있습니다. '
|
? '사망 시 두 가지 부활 방법이 있습니다:\n'
|
||||||
'부활 후 HP/MP가 완전 회복되고 빈 장비 슬롯에 기본 장비가 지급됩니다.'
|
'• 기본 부활: 장비 1개 제물, HP/MP 회복\n'
|
||||||
|
'• 광고 부활: 아이템 보존, HP 100%, 10분 자동부활\n'
|
||||||
|
'유료 유저는 항상 광고 없이 부활 가능합니다.'
|
||||||
: isJapanese
|
: isJapanese
|
||||||
? 'HPが0になると死亡します。死亡時に装備1つを捧げて復活できます。'
|
? '死亡時に2つの復活方法があります:\n'
|
||||||
'復活後HP/MPが完全回復し、空の装備スロットに基本装備が支給されます。'
|
'• 基本復活: 装備1つ消費、HP/MP回復\n'
|
||||||
: 'You die when HP reaches 0. Sacrifice one equipment piece to revive. '
|
'• 広告復活: アイテム保存、HP100%、10分自動復活\n'
|
||||||
'After revival, HP/MP fully restore and empty slots get basic equipment.',
|
'課金ユーザーは常に広告なしで復活可能です。'
|
||||||
|
: 'Two revival methods on death:\n'
|
||||||
|
'• Basic: Sacrifice 1 equipment, restore HP/MP\n'
|
||||||
|
'• Ad Revival: Keep items, 100% HP, 10-min auto-revive\n'
|
||||||
|
'Paid users can always revive without ads.',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -306,21 +312,21 @@ class _SkillsHelpView extends StatelessWidget {
|
|||||||
? 'スキルランク'
|
? 'スキルランク'
|
||||||
: 'Skill Ranks',
|
: 'Skill Ranks',
|
||||||
content: isKorean
|
content: isKorean
|
||||||
? '스킬은 I ~ IX 랭크가 있습니다. 랭크가 높을수록:\n'
|
? '스킬 랭크는 I, II, III... 형태로 표시됩니다. 랭크가 높을수록:\n'
|
||||||
'• 데미지/회복량 증가\n'
|
'• 데미지/회복량 증가\n'
|
||||||
'• MP 소모량 증가\n'
|
'• MP 소모량 감소\n'
|
||||||
'• 쿨타임 증가\n'
|
'• 쿨타임 감소\n'
|
||||||
'레벨업 시 랜덤하게 스킬을 배웁니다.'
|
'레벨업 시 랜덤하게 스킬을 배웁니다.'
|
||||||
: isJapanese
|
: isJapanese
|
||||||
? 'スキルにはI~IXランクがあります。ランクが高いほど:\n'
|
? 'スキルランクはI、II、III...の形式で表示されます。ランクが高いほど:\n'
|
||||||
'• ダメージ/回復量増加\n'
|
'• ダメージ/回復量増加\n'
|
||||||
'• MP消費量増加\n'
|
'• MP消費量減少\n'
|
||||||
'• クールタイム増加\n'
|
'• クールタイム減少\n'
|
||||||
'レベルアップ時にランダムでスキルを習得します。'
|
'レベルアップ時にランダムでスキルを習得します。'
|
||||||
: 'Skills have ranks I~IX. Higher rank means:\n'
|
: 'Skill ranks are displayed as I, II, III... Higher rank means:\n'
|
||||||
'• More damage/healing\n'
|
'• More damage/healing\n'
|
||||||
'• More MP cost\n'
|
'• Less MP cost\n'
|
||||||
'• Longer cooldown\n'
|
'• Shorter cooldown\n'
|
||||||
'Learn random skills on level up.',
|
'Learn random skills on level up.',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -348,19 +354,31 @@ class _UIHelpView extends StatelessWidget {
|
|||||||
? '画面構成'
|
? '画面構成'
|
||||||
: 'Screen Layout',
|
: 'Screen Layout',
|
||||||
content: isKorean
|
content: isKorean
|
||||||
? '• 상단: 전투 애니메이션, 태스크 진행바\n'
|
? '모바일에서는 좌우 스와이프로 7개 페이지 탐색:\n'
|
||||||
'• 좌측: 캐릭터 정보, HP/MP, 스탯\n'
|
'• 캐릭터: 이름, 레벨, 종족, 직업\n'
|
||||||
'• 중앙: 장비, 인벤토리\n'
|
'• 스탯: STR, DEX, CON, INT 등\n'
|
||||||
'• 우측: 플롯/퀘스트 진행, 스펠북'
|
'• 장비: 무기, 방어구, 액세서리\n'
|
||||||
|
'• 인벤토리: 보유 아이템, 골드\n'
|
||||||
|
'• 스킬북: 습득한 스킬 목록\n'
|
||||||
|
'• 퀘스트: 진행 중인 퀘스트\n'
|
||||||
|
'• 플롯: 스토리 진행 상황'
|
||||||
: isJapanese
|
: isJapanese
|
||||||
? '• 上部: 戦闘アニメーション、タスク進行バー\n'
|
? 'モバイルでは左右スワイプで7ページ切替:\n'
|
||||||
'• 左側: キャラクター情報、HP/MP、ステータス\n'
|
'• キャラクター: 名前、レベル、種族、職業\n'
|
||||||
'• 中央: 装備、インベントリ\n'
|
'• ステータス: STR、DEX、CON、INT等\n'
|
||||||
'• 右側: プロット/クエスト進行、スペルブック'
|
'• 装備: 武器、防具、アクセサリー\n'
|
||||||
: '• Top: Combat animation, task progress bar\n'
|
'• インベントリ: 所持アイテム、ゴールド\n'
|
||||||
'• Left: Character info, HP/MP, stats\n'
|
'• スキルブック: 習得したスキル一覧\n'
|
||||||
'• Center: Equipment, inventory\n'
|
'• クエスト: 進行中のクエスト\n'
|
||||||
'• Right: Plot/quest progress, spellbook',
|
'• プロット: ストーリー進行状況'
|
||||||
|
: 'On mobile, swipe left/right to browse 7 pages:\n'
|
||||||
|
'• Character: Name, level, race, class\n'
|
||||||
|
'• Stats: STR, DEX, CON, INT, etc.\n'
|
||||||
|
'• Equipment: Weapons, armor, accessories\n'
|
||||||
|
'• Inventory: Items, gold\n'
|
||||||
|
'• Skillbook: Learned skills\n'
|
||||||
|
'• Quests: Active quests\n'
|
||||||
|
'• Plot: Story progress',
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_HelpSection(
|
_HelpSection(
|
||||||
@@ -371,22 +389,42 @@ class _UIHelpView extends StatelessWidget {
|
|||||||
? '速度調整'
|
? '速度調整'
|
||||||
: 'Speed Control',
|
: 'Speed Control',
|
||||||
content: isKorean
|
content: isKorean
|
||||||
? '태스크 진행바 옆 속도 버튼으로 게임 속도를 조절할 수 있습니다:\n'
|
? '게임 속도를 조절할 수 있습니다:\n'
|
||||||
'• 1x: 기본 속도\n'
|
'• 1x: 기본 속도\n'
|
||||||
'• 2x: 2배 속도\n'
|
'• 2x: 명예의 전당 캐릭터 1명 이상 시 해금\n'
|
||||||
'• 5x: 5배 속도\n'
|
'• 5x: 광고 시청으로 5분간 부스트 (유료 유저 무료)'
|
||||||
'• 10x: 10배 속도'
|
|
||||||
: isJapanese
|
: isJapanese
|
||||||
? 'タスク進行バー横の速度ボタンでゲーム速度を調整できます:\n'
|
? 'ゲーム速度を調整できます:\n'
|
||||||
'• 1x: 基本速度\n'
|
'• 1x: 基本速度\n'
|
||||||
'• 2x: 2倍速\n'
|
'• 2x: 殿堂入り1人以上で解放\n'
|
||||||
'• 5x: 5倍速\n'
|
'• 5x: 広告視聴で5分間ブースト(課金ユーザー無料)'
|
||||||
'• 10x: 10倍速'
|
: 'Adjust game speed:\n'
|
||||||
: 'Use the speed button next to task bar to adjust game speed:\n'
|
|
||||||
'• 1x: Normal speed\n'
|
'• 1x: Normal speed\n'
|
||||||
'• 2x: 2x speed\n'
|
'• 2x: Unlocked with 1+ Hall of Fame character\n'
|
||||||
'• 5x: 5x speed\n'
|
'• 5x: 5-min boost via ad (free for paid users)',
|
||||||
'• 10x: 10x speed',
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_HelpSection(
|
||||||
|
icon: '🏆',
|
||||||
|
title: isKorean
|
||||||
|
? '명예의 전당'
|
||||||
|
: isJapanese
|
||||||
|
? '殿堂入り'
|
||||||
|
: 'Hall of Fame',
|
||||||
|
content: isKorean
|
||||||
|
? 'Act V를 클리어하면 캐릭터가 명예의 전당에 등록됩니다.\n'
|
||||||
|
'• 캐릭터 이름, 레벨, 스탯이 영구 기록됨\n'
|
||||||
|
'• 첫 등록 시 2x 속도 영구 해금\n'
|
||||||
|
'• 2명 이상 등록 시 로컬 아레나 기능 해금'
|
||||||
|
: isJapanese
|
||||||
|
? 'Act Vクリアでキャラクターが殿堂入りします。\n'
|
||||||
|
'• キャラクター名、レベル、ステータスが永久記録\n'
|
||||||
|
'• 初登録で2倍速が永久解放\n'
|
||||||
|
'• 2人以上でローカルアリーナ機能解放'
|
||||||
|
: 'Characters enter Hall of Fame upon completing Act V.\n'
|
||||||
|
'• Name, level, stats are permanently recorded\n'
|
||||||
|
'• First entry permanently unlocks 2x speed\n'
|
||||||
|
'• 2+ entries unlock Local Arena feature',
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_HelpSection(
|
_HelpSection(
|
||||||
|
|||||||
@@ -1,37 +1,45 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:asciineverdie/data/game_text_l10n.dart' as l10n;
|
import 'package:asciineverdie/data/game_text_l10n.dart' as l10n;
|
||||||
|
import 'package:asciineverdie/data/potion_data.dart';
|
||||||
import 'package:asciineverdie/src/core/engine/iap_service.dart';
|
import 'package:asciineverdie/src/core/engine/iap_service.dart';
|
||||||
import 'package:asciineverdie/src/core/engine/return_rewards_service.dart';
|
import 'package:asciineverdie/src/core/engine/return_rewards_service.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/treasure_chest.dart';
|
||||||
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
||||||
|
|
||||||
/// 복귀 보상 다이얼로그 (Phase 7)
|
/// 복귀 보상 다이얼로그 (Phase 7)
|
||||||
///
|
///
|
||||||
/// 게임 복귀 시 보상을 표시하는 다이얼로그
|
/// 게임 복귀 시 보물 상자 보상을 표시하는 다이얼로그
|
||||||
class ReturnRewardsDialog extends StatefulWidget {
|
class ReturnRewardsDialog extends StatefulWidget {
|
||||||
const ReturnRewardsDialog({
|
const ReturnRewardsDialog({
|
||||||
super.key,
|
super.key,
|
||||||
required this.reward,
|
required this.reward,
|
||||||
|
required this.playerLevel,
|
||||||
required this.onClaim,
|
required this.onClaim,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// 복귀 보상 데이터
|
/// 복귀 보상 데이터
|
||||||
final ReturnReward reward;
|
final ReturnChestReward reward;
|
||||||
|
|
||||||
/// 보상 수령 콜백 (totalGold)
|
/// 플레이어 레벨 (상자 보상 스케일링용)
|
||||||
final void Function(int totalGold) onClaim;
|
final int playerLevel;
|
||||||
|
|
||||||
|
/// 보상 수령 콜백 (상자 보상 목록)
|
||||||
|
final void Function(List<ChestReward> rewards) onClaim;
|
||||||
|
|
||||||
/// 다이얼로그 표시
|
/// 다이얼로그 표시
|
||||||
static Future<void> show(
|
static Future<void> show(
|
||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
required ReturnReward reward,
|
required ReturnChestReward reward,
|
||||||
required void Function(int totalGold) onClaim,
|
required int playerLevel,
|
||||||
|
required void Function(List<ChestReward> rewards) onClaim,
|
||||||
}) async {
|
}) async {
|
||||||
return showDialog<void>(
|
return showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
builder: (context) => ReturnRewardsDialog(
|
builder: (context) => ReturnRewardsDialog(
|
||||||
reward: reward,
|
reward: reward,
|
||||||
|
playerLevel: playerLevel,
|
||||||
onClaim: onClaim,
|
onClaim: onClaim,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -41,27 +49,50 @@ class ReturnRewardsDialog extends StatefulWidget {
|
|||||||
State<ReturnRewardsDialog> createState() => _ReturnRewardsDialogState();
|
State<ReturnRewardsDialog> createState() => _ReturnRewardsDialogState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ReturnRewardsDialogState extends State<ReturnRewardsDialog> {
|
class _ReturnRewardsDialogState extends State<ReturnRewardsDialog>
|
||||||
bool _basicClaimed = false;
|
with SingleTickerProviderStateMixin {
|
||||||
bool _bonusClaimed = false;
|
|
||||||
bool _isClaimingBonus = false;
|
|
||||||
int _totalClaimed = 0;
|
|
||||||
|
|
||||||
final _rewardsService = ReturnRewardsService.instance;
|
final _rewardsService = ReturnRewardsService.instance;
|
||||||
|
|
||||||
|
// 상태
|
||||||
|
bool _basicOpened = false;
|
||||||
|
bool _bonusOpened = false;
|
||||||
|
bool _isOpeningBasic = false;
|
||||||
|
bool _isOpeningBonus = false;
|
||||||
|
List<ChestReward> _basicRewards = [];
|
||||||
|
List<ChestReward> _bonusRewards = [];
|
||||||
|
|
||||||
|
// 애니메이션
|
||||||
|
late AnimationController _animController;
|
||||||
|
late Animation<double> _shakeAnimation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_animController = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 500),
|
||||||
|
);
|
||||||
|
_shakeAnimation = Tween<double>(begin: 0, end: 1).animate(
|
||||||
|
CurvedAnimation(parent: _animController, curve: Curves.elasticOut),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_animController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final gold = RetroColors.goldOf(context);
|
final gold = RetroColors.goldOf(context);
|
||||||
final goldDark = RetroColors.goldDarkOf(context);
|
|
||||||
final panelBg = RetroColors.panelBgOf(context);
|
final panelBg = RetroColors.panelBgOf(context);
|
||||||
final borderColor = RetroColors.borderOf(context);
|
final borderColor = RetroColors.borderOf(context);
|
||||||
final expColor = RetroColors.expOf(context);
|
|
||||||
final isPaidUser = IAPService.instance.isAdRemovalPurchased;
|
|
||||||
|
|
||||||
return Dialog(
|
return Dialog(
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: const BoxConstraints(maxWidth: 360),
|
constraints: const BoxConstraints(maxWidth: 400),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: panelBg,
|
color: panelBg,
|
||||||
border: Border(
|
border: Border(
|
||||||
@@ -96,40 +127,41 @@ class _ReturnRewardsDialogState extends State<ReturnRewardsDialog> {
|
|||||||
),
|
),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'PressStart2P',
|
fontFamily: 'PressStart2P',
|
||||||
fontSize: 11,
|
fontSize: 10,
|
||||||
color: gold.withValues(alpha: 0.8),
|
color: gold.withValues(alpha: 0.8),
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// 기본 보상
|
// 기본 상자 섹션
|
||||||
_buildRewardSection(
|
_buildChestSection(
|
||||||
context,
|
context,
|
||||||
title: l10n.returnRewardBasic,
|
title: l10n.returnRewardChests(widget.reward.chestCount),
|
||||||
gold: widget.reward.goldReward,
|
chestCount: widget.reward.chestCount,
|
||||||
color: gold,
|
rewards: _basicRewards,
|
||||||
colorDark: goldDark,
|
isOpened: _basicOpened,
|
||||||
claimed: _basicClaimed,
|
isOpening: _isOpeningBasic,
|
||||||
onClaim: _claimBasic,
|
onOpen: _openBasicChests,
|
||||||
buttonText: l10n.returnRewardClaim,
|
isGold: true,
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// 보너스 보상
|
// 보너스 상자 섹션
|
||||||
_buildRewardSection(
|
_buildChestSection(
|
||||||
context,
|
context,
|
||||||
title: l10n.returnRewardBonus,
|
title: l10n.returnRewardBonusChests,
|
||||||
gold: widget.reward.bonusGold,
|
chestCount: widget.reward.bonusChestCount,
|
||||||
color: expColor,
|
rewards: _bonusRewards,
|
||||||
colorDark: expColor.withValues(alpha: 0.6),
|
isOpened: _bonusOpened,
|
||||||
claimed: _bonusClaimed,
|
isOpening: _isOpeningBonus,
|
||||||
onClaim: _claimBonus,
|
onOpen: _openBonusChests,
|
||||||
buttonText: l10n.returnRewardClaimBonus,
|
isGold: false,
|
||||||
showAdIcon: !isPaidUser,
|
enabled: _basicOpened && !_bonusOpened,
|
||||||
isLoading: _isClaimingBonus,
|
showAdIcon: !IAPService.instance.isAdRemovalPurchased,
|
||||||
enabled: _basicClaimed && !_bonusClaimed,
|
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// 완료/건너뛰기 버튼
|
// 완료/건너뛰기 버튼
|
||||||
@@ -154,7 +186,7 @@ class _ReturnRewardsDialogState extends State<ReturnRewardsDialog> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text('🎁', style: TextStyle(fontSize: 20, color: gold)),
|
const Text('📦', style: TextStyle(fontSize: 20)),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
l10n.returnRewardTitle,
|
l10n.returnRewardTitle,
|
||||||
@@ -166,32 +198,40 @@ class _ReturnRewardsDialogState extends State<ReturnRewardsDialog> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text('🎁', style: TextStyle(fontSize: 20, color: gold)),
|
const Text('📦', style: TextStyle(fontSize: 20)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildRewardSection(
|
Widget _buildChestSection(
|
||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
required String title,
|
required String title,
|
||||||
required int gold,
|
required int chestCount,
|
||||||
required Color color,
|
required List<ChestReward> rewards,
|
||||||
required Color colorDark,
|
required bool isOpened,
|
||||||
required bool claimed,
|
required bool isOpening,
|
||||||
required VoidCallback onClaim,
|
required VoidCallback onOpen,
|
||||||
required String buttonText,
|
required bool isGold,
|
||||||
bool showAdIcon = false,
|
|
||||||
bool isLoading = false,
|
|
||||||
bool enabled = true,
|
bool enabled = true,
|
||||||
|
bool showAdIcon = false,
|
||||||
}) {
|
}) {
|
||||||
|
final gold = RetroColors.goldOf(context);
|
||||||
|
final expColor = RetroColors.expOf(context);
|
||||||
final muted = RetroColors.textMutedOf(context);
|
final muted = RetroColors.textMutedOf(context);
|
||||||
|
final color = isGold ? gold : expColor;
|
||||||
|
final isPaidUser = IAPService.instance.isAdRemovalPurchased;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: color.withValues(alpha: 0.1),
|
color: color.withValues(alpha: 0.1),
|
||||||
border: Border.all(color: color.withValues(alpha: 0.5), width: 2),
|
border: Border.all(
|
||||||
|
color: (enabled || isOpened)
|
||||||
|
? color.withValues(alpha: 0.5)
|
||||||
|
: muted.withValues(alpha: 0.3),
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -201,46 +241,39 @@ class _ReturnRewardsDialogState extends State<ReturnRewardsDialog> {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'PressStart2P',
|
fontFamily: 'PressStart2P',
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: color,
|
color: (enabled || isOpened) ? color : muted,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
|
||||||
|
|
||||||
// 골드 표시
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Text('💰', style: TextStyle(fontSize: 20)),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
l10n.returnRewardGold(gold),
|
|
||||||
style: TextStyle(
|
|
||||||
fontFamily: 'PressStart2P',
|
|
||||||
fontSize: 14,
|
|
||||||
color: claimed ? muted : color,
|
|
||||||
decoration: claimed ? TextDecoration.lineThrough : null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (claimed) ...[
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
'✓',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
color: RetroColors.expOf(context),
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
if (!claimed) ...[
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
// 수령 버튼
|
// 상자 아이콘들 또는 보상 목록
|
||||||
|
if (isOpened)
|
||||||
|
_buildRewardsList(context, rewards)
|
||||||
|
else
|
||||||
|
_buildChestIcons(chestCount, color, enabled),
|
||||||
|
|
||||||
|
if (!isOpened) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// 오픈 버튼
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: enabled && !isLoading ? onClaim : null,
|
onTap: enabled && !isOpening ? onOpen : null,
|
||||||
|
child: AnimatedBuilder(
|
||||||
|
animation: _shakeAnimation,
|
||||||
|
builder: (context, child) {
|
||||||
|
return Transform.translate(
|
||||||
|
offset: isOpening
|
||||||
|
? Offset(
|
||||||
|
_shakeAnimation.value * 2 *
|
||||||
|
((_animController.value * 10).round() % 2 == 0
|
||||||
|
? 1
|
||||||
|
: -1),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
: Offset.zero,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 16,
|
horizontal: 16,
|
||||||
@@ -258,26 +291,38 @@ class _ReturnRewardsDialogState extends State<ReturnRewardsDialog> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (isLoading) ...[
|
if (isOpening) ...[
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 16,
|
width: 14,
|
||||||
height: 16,
|
height: 14,
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
color: color,
|
color: color,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
],
|
|
||||||
Text(
|
Text(
|
||||||
buttonText,
|
l10n.returnRewardOpening,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'PressStart2P',
|
fontFamily: 'PressStart2P',
|
||||||
fontSize: 11,
|
fontSize: 10,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
] else ...[
|
||||||
|
Text(
|
||||||
|
isGold
|
||||||
|
? l10n.returnRewardOpenChests
|
||||||
|
: (isPaidUser
|
||||||
|
? l10n.returnRewardClaimBonusFree
|
||||||
|
: l10n.returnRewardClaimBonus),
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 10,
|
||||||
color: enabled ? color : muted,
|
color: enabled ? color : muted,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (showAdIcon && !isLoading) ...[
|
if (showAdIcon && !isPaidUser) ...[
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
@@ -299,6 +344,8 @@ class _ReturnRewardsDialogState extends State<ReturnRewardsDialog> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -308,12 +355,118 @@ class _ReturnRewardsDialogState extends State<ReturnRewardsDialog> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildChestIcons(int count, Color color, bool enabled) {
|
||||||
|
final muted = RetroColors.textMutedOf(context);
|
||||||
|
return Wrap(
|
||||||
|
alignment: WrapAlignment.center,
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: List.generate(
|
||||||
|
count,
|
||||||
|
(index) => Text(
|
||||||
|
'📦',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
color: enabled ? null : muted,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRewardsList(BuildContext context, List<ChestReward> rewards) {
|
||||||
|
if (rewards.isEmpty) {
|
||||||
|
return const Text(
|
||||||
|
'(empty)',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 10,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: rewards.map((reward) => _buildRewardItem(context, reward)).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRewardItem(BuildContext context, ChestReward reward) {
|
||||||
|
final gold = RetroColors.goldOf(context);
|
||||||
|
final expColor = RetroColors.expOf(context);
|
||||||
|
|
||||||
|
String icon;
|
||||||
|
String text;
|
||||||
|
Color color;
|
||||||
|
|
||||||
|
switch (reward.type) {
|
||||||
|
case ChestRewardType.equipment:
|
||||||
|
icon = '⚔️';
|
||||||
|
text = reward.equipment?.name ?? 'Unknown';
|
||||||
|
color = _getRarityColor(reward.equipment?.rarity);
|
||||||
|
break;
|
||||||
|
case ChestRewardType.potion:
|
||||||
|
final potion = PotionData.getById(reward.potionId ?? '');
|
||||||
|
icon = potion?.type.name == 'hp' ? '❤️' : '💙';
|
||||||
|
text = l10n.chestRewardPotionAmount(
|
||||||
|
potion?.name ?? 'Potion',
|
||||||
|
reward.potionCount ?? 1,
|
||||||
|
);
|
||||||
|
color = Colors.white;
|
||||||
|
break;
|
||||||
|
case ChestRewardType.gold:
|
||||||
|
icon = '💰';
|
||||||
|
text = l10n.chestRewardGoldAmount(reward.gold ?? 0);
|
||||||
|
color = gold;
|
||||||
|
break;
|
||||||
|
case ChestRewardType.experience:
|
||||||
|
icon = '⭐';
|
||||||
|
text = l10n.chestRewardExpAmount(reward.experience ?? 0);
|
||||||
|
color = expColor;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(icon, style: const TextStyle(fontSize: 16)),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 9,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getRarityColor(dynamic rarity) {
|
||||||
|
if (rarity == null) return Colors.white;
|
||||||
|
return switch (rarity.toString()) {
|
||||||
|
'ItemRarity.common' => Colors.grey,
|
||||||
|
'ItemRarity.uncommon' => Colors.green,
|
||||||
|
'ItemRarity.rare' => Colors.blue,
|
||||||
|
'ItemRarity.epic' => Colors.purple,
|
||||||
|
'ItemRarity.legendary' => Colors.orange,
|
||||||
|
_ => Colors.white,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildBottomButton(BuildContext context) {
|
Widget _buildBottomButton(BuildContext context) {
|
||||||
final gold = RetroColors.goldOf(context);
|
final gold = RetroColors.goldOf(context);
|
||||||
final goldDark = RetroColors.goldDarkOf(context);
|
final goldDark = RetroColors.goldDarkOf(context);
|
||||||
final muted = RetroColors.textMutedOf(context);
|
final muted = RetroColors.textMutedOf(context);
|
||||||
|
|
||||||
final canComplete = _basicClaimed;
|
final canComplete = _basicOpened;
|
||||||
final buttonColor = canComplete ? gold : muted;
|
final buttonColor = canComplete ? gold : muted;
|
||||||
final buttonDark = canComplete ? goldDark : muted.withValues(alpha: 0.5);
|
final buttonDark = canComplete ? goldDark : muted.withValues(alpha: 0.5);
|
||||||
|
|
||||||
@@ -344,43 +497,70 @@ class _ReturnRewardsDialogState extends State<ReturnRewardsDialog> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _claimBasic() {
|
Future<void> _openBasicChests() async {
|
||||||
if (_basicClaimed) return;
|
if (_basicOpened || _isOpeningBasic) return;
|
||||||
|
|
||||||
final claimed = _rewardsService.claimBasicReward(widget.reward);
|
|
||||||
setState(() {
|
|
||||||
_basicClaimed = true;
|
|
||||||
_totalClaimed += claimed;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _claimBonus() async {
|
|
||||||
if (_bonusClaimed || _isClaimingBonus) return;
|
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_isClaimingBonus = true;
|
_isOpeningBasic = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
final bonus = await _rewardsService.claimBonusReward(widget.reward);
|
// 애니메이션 시작
|
||||||
|
_animController.repeat();
|
||||||
|
|
||||||
|
// 약간의 딜레이 후 상자 오픈
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 800));
|
||||||
|
|
||||||
|
final rewards = _rewardsService.claimBasicReward(
|
||||||
|
widget.reward,
|
||||||
|
widget.playerLevel,
|
||||||
|
);
|
||||||
|
|
||||||
|
_animController.stop();
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isClaimingBonus = false;
|
_isOpeningBasic = false;
|
||||||
if (bonus > 0) {
|
_basicOpened = true;
|
||||||
_bonusClaimed = true;
|
_basicRewards = rewards;
|
||||||
_totalClaimed += bonus;
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _openBonusChests() async {
|
||||||
|
if (_bonusOpened || _isOpeningBonus) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isOpeningBonus = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
_animController.repeat();
|
||||||
|
|
||||||
|
final rewards = await _rewardsService.claimBonusReward(
|
||||||
|
widget.reward,
|
||||||
|
widget.playerLevel,
|
||||||
|
);
|
||||||
|
|
||||||
|
_animController.stop();
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isOpeningBonus = false;
|
||||||
|
if (rewards.isNotEmpty) {
|
||||||
|
_bonusOpened = true;
|
||||||
|
_bonusRewards = rewards;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _complete() {
|
void _complete() {
|
||||||
widget.onClaim(_totalClaimed);
|
final allRewards = [..._basicRewards, ..._bonusRewards];
|
||||||
|
widget.onClaim(allRewards);
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _skip() {
|
void _skip() {
|
||||||
widget.onClaim(0);
|
widget.onClaim([]);
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -196,14 +196,20 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
|||||||
final random = math.Random();
|
final random = math.Random();
|
||||||
_currentSeed = random.nextInt(0x7FFFFFFF);
|
_currentSeed = random.nextInt(0x7FFFFFFF);
|
||||||
|
|
||||||
// 종족/클래스도 랜덤 선택
|
// 종족/클래스 랜덤 선택 및 스탯 굴림
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedRaceIndex = random.nextInt(_races.length);
|
_selectedRaceIndex = random.nextInt(_races.length);
|
||||||
_selectedKlassIndex = random.nextInt(_klasses.length);
|
_selectedKlassIndex = random.nextInt(_klasses.length);
|
||||||
|
// 스탯 굴림 (setState 내에서 실행하여 UI 갱신 보장)
|
||||||
|
final rng = DeterministicRandom(_currentSeed);
|
||||||
|
_str = rollStat(rng);
|
||||||
|
_con = rollStat(rng);
|
||||||
|
_dex = rollStat(rng);
|
||||||
|
_int = rollStat(rng);
|
||||||
|
_wis = rollStat(rng);
|
||||||
|
_cha = rollStat(rng);
|
||||||
});
|
});
|
||||||
|
|
||||||
_rollStats();
|
|
||||||
|
|
||||||
// 선택된 종족/직업으로 스크롤
|
// 선택된 종족/직업으로 스크롤
|
||||||
_scrollToSelectedItems();
|
_scrollToSelectedItems();
|
||||||
|
|
||||||
@@ -296,7 +302,10 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
|||||||
snapshot = await _rollService.undoFreeUser();
|
snapshot = await _rollService.undoFreeUser();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (snapshot != null && mounted) {
|
// UI 상태 갱신 (성공/실패 여부와 관계없이 버튼 상태 업데이트)
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
if (snapshot != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_str = snapshot!.stats.str;
|
_str = snapshot!.stats.str;
|
||||||
_con = snapshot.stats.con;
|
_con = snapshot.stats.con;
|
||||||
@@ -309,6 +318,9 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
|||||||
_currentSeed = snapshot.seed;
|
_currentSeed = snapshot.seed;
|
||||||
});
|
});
|
||||||
_scrollToSelectedItems();
|
_scrollToSelectedItems();
|
||||||
|
} else {
|
||||||
|
// 광고 취소/실패 시에도 버튼 상태 갱신
|
||||||
|
setState(() {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,16 +507,19 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
|||||||
: RetroColors.textDisabled,
|
: RetroColors.textDisabled,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Flexible(
|
||||||
'DEBUG: TURBO MODE (20x)',
|
child: Text(
|
||||||
|
'DEBUG: TURBO (20x)',
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'PressStart2P',
|
fontFamily: 'PressStart2P',
|
||||||
fontSize: 13,
|
fontSize: 11,
|
||||||
color: _cheatsEnabled
|
color: _cheatsEnabled
|
||||||
? RetroColors.hpRed
|
? RetroColors.hpRed
|
||||||
: RetroColors.textDisabled,
|
: RetroColors.textDisabled,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user