feat(ui): 화면 및 컨트롤러 수익화 연동
- 앱 초기화에 광고/IAP 서비스 추가 - 게임 세션 컨트롤러 수익화 상태 관리 - 캐릭터 생성 화면 굴리기 제한 UI - 설정 화면 광고 제거 구매 UI - 애니메이션 패널 개선
This commit is contained in:
@@ -52,6 +52,10 @@ class MobileCarouselLayout extends StatefulWidget {
|
||||
this.onCheatQuest,
|
||||
this.onCheatPlot,
|
||||
this.onCreateTestCharacter,
|
||||
this.autoReviveEndMs,
|
||||
this.speedBoostEndMs,
|
||||
this.isPaidUser = false,
|
||||
this.onSpeedBoostActivate,
|
||||
});
|
||||
|
||||
final GameState state;
|
||||
@@ -102,6 +106,18 @@ class MobileCarouselLayout extends StatefulWidget {
|
||||
/// 테스트 캐릭터 생성 콜백 (디버그 모드 전용)
|
||||
final Future<void> Function()? onCreateTestCharacter;
|
||||
|
||||
/// 자동부활 버프 종료 시점 (elapsedMs 기준)
|
||||
final int? autoReviveEndMs;
|
||||
|
||||
/// 5배속 버프 종료 시점 (elapsedMs 기준)
|
||||
final int? speedBoostEndMs;
|
||||
|
||||
/// 유료 유저 여부
|
||||
final bool isPaidUser;
|
||||
|
||||
/// 5배속 버프 활성화 콜백 (광고 시청)
|
||||
final VoidCallback? onSpeedBoostActivate;
|
||||
|
||||
@override
|
||||
State<MobileCarouselLayout> createState() => _MobileCarouselLayoutState();
|
||||
}
|
||||
@@ -456,27 +472,7 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
||||
ListTile(
|
||||
leading: const Icon(Icons.speed),
|
||||
title: Text(l10n.menuSpeed),
|
||||
trailing: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
'${widget.speedMultiplier}x',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
widget.onSpeedCycle();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
trailing: _buildSpeedSelector(context),
|
||||
),
|
||||
|
||||
const Divider(),
|
||||
@@ -735,6 +731,10 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
||||
state.progress.currentCombat?.recentEvents.lastOrNull,
|
||||
raceId: state.traits.raceId,
|
||||
weaponRarity: state.equipment.weaponItem.rarity,
|
||||
autoReviveEndMs: widget.autoReviveEndMs,
|
||||
speedBoostEndMs: widget.speedBoostEndMs,
|
||||
isPaidUser: widget.isPaidUser,
|
||||
onSpeedBoostActivate: widget.onSpeedBoostActivate,
|
||||
),
|
||||
|
||||
// 중앙: 캐로셀 (PageView)
|
||||
@@ -794,4 +794,135 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 속도 선택기 빌드 (옵션 메뉴용)
|
||||
///
|
||||
/// - 1x, 2x: 무료 사이클
|
||||
/// - ▶5x: 광고 시청 후 버프 (또는 버프 활성 시)
|
||||
/// - 20x: 디버그 모드 전용
|
||||
Widget _buildSpeedSelector(BuildContext context) {
|
||||
final currentElapsedMs = widget.state.skillSystem.elapsedMs;
|
||||
final speedBoostEndMs = widget.speedBoostEndMs ?? 0;
|
||||
final isSpeedBoostActive =
|
||||
speedBoostEndMs > currentElapsedMs || widget.isPaidUser;
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 1x 버튼
|
||||
_buildSpeedChip(
|
||||
context,
|
||||
speed: 1,
|
||||
isSelected: widget.speedMultiplier == 1 && !isSpeedBoostActive,
|
||||
onTap: () {
|
||||
widget.onSpeedCycle();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
// 2x 버튼
|
||||
_buildSpeedChip(
|
||||
context,
|
||||
speed: 2,
|
||||
isSelected: widget.speedMultiplier == 2 && !isSpeedBoostActive,
|
||||
onTap: () {
|
||||
widget.onSpeedCycle();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
// 5x 버튼 (광고 또는 버프 활성)
|
||||
_buildSpeedChip(
|
||||
context,
|
||||
speed: 5,
|
||||
isSelected: isSpeedBoostActive,
|
||||
isAdBased: !isSpeedBoostActive && !widget.isPaidUser,
|
||||
onTap: () {
|
||||
if (!isSpeedBoostActive) {
|
||||
widget.onSpeedBoostActivate?.call();
|
||||
}
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
// 20x 버튼 (디버그 전용)
|
||||
if (widget.cheatsEnabled) ...[
|
||||
const SizedBox(width: 4),
|
||||
_buildSpeedChip(
|
||||
context,
|
||||
speed: 20,
|
||||
isSelected: widget.speedMultiplier == 20,
|
||||
isDebug: true,
|
||||
onTap: () {
|
||||
widget.onSpeedCycle();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 속도 칩 빌드
|
||||
Widget _buildSpeedChip(
|
||||
BuildContext context, {
|
||||
required int speed,
|
||||
required bool isSelected,
|
||||
required VoidCallback onTap,
|
||||
bool isAdBased = false,
|
||||
bool isDebug = false,
|
||||
}) {
|
||||
final Color bgColor;
|
||||
final Color textColor;
|
||||
|
||||
if (isSelected) {
|
||||
bgColor = isDebug
|
||||
? Colors.red
|
||||
: speed == 5
|
||||
? Colors.orange
|
||||
: Theme.of(context).colorScheme.primary;
|
||||
textColor = Colors.white;
|
||||
} else {
|
||||
bgColor = Theme.of(context).colorScheme.surfaceContainerHighest;
|
||||
textColor = isAdBased
|
||||
? Colors.orange
|
||||
: isDebug
|
||||
? Colors.red.withValues(alpha: 0.7)
|
||||
: Theme.of(context).colorScheme.onSurface;
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: isAdBased && !isSelected
|
||||
? Border.all(color: Colors.orange)
|
||||
: null,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (isAdBased && !isSelected)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(right: 2),
|
||||
child: Text(
|
||||
'▶',
|
||||
style: TextStyle(fontSize: 8, color: Colors.orange),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${speed}x',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user