feat(ui): 화면 및 컨트롤러 수익화 연동

- 앱 초기화에 광고/IAP 서비스 추가
- 게임 세션 컨트롤러 수익화 상태 관리
- 캐릭터 생성 화면 굴리기 제한 UI
- 설정 화면 광고 제거 구매 UI
- 애니메이션 패널 개선
This commit is contained in:
JiWoong Sul
2026-01-16 20:10:43 +09:00
parent c95e4de5a4
commit 748160d543
8 changed files with 1288 additions and 373 deletions

View File

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