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

@@ -8,6 +8,8 @@ import 'package:asciineverdie/data/class_data.dart';
import 'package:asciineverdie/data/game_text_l10n.dart' as game_l10n;
import 'package:asciineverdie/data/race_data.dart';
import 'package:asciineverdie/l10n/app_localizations.dart';
import 'package:asciineverdie/src/core/engine/character_roll_service.dart';
import 'package:asciineverdie/src/core/engine/iap_service.dart';
import 'package:asciineverdie/src/core/model/class_traits.dart';
import 'package:asciineverdie/src/core/model/game_state.dart';
import 'package:asciineverdie/src/core/model/race_traits.dart';
@@ -52,10 +54,6 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
int _wis = 0;
int _cha = 0;
// 롤 이력 (Unroll 기능용) - 원본 OldRolls TListBox
static const int _maxRollHistory = 20; // 최대 저장 개수
final List<int> _rollHistory = [];
// 현재 RNG 시드 (Re-Roll 전 저장)
int _currentSeed = 0;
@@ -68,10 +66,19 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
// 굴리기 버튼 연속 클릭 방지
bool _isRolling = false;
// 굴리기/되돌리기 서비스
final CharacterRollService _rollService = CharacterRollService.instance;
// 서비스 초기화 완료 여부
bool _isServiceInitialized = false;
@override
void initState() {
super.initState();
// 서비스 초기화
_initializeService();
// 초기 랜덤화
final random = math.Random();
_selectedRaceIndex = random.nextInt(_races.length);
@@ -89,6 +96,16 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
_scrollToSelectedItems();
}
/// 서비스 초기화
Future<void> _initializeService() async {
await _rollService.initialize();
if (mounted) {
setState(() {
_isServiceInitialized = true;
});
}
}
@override
void dispose() {
_nameController.dispose();
@@ -144,12 +161,35 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
if (_isRolling) return;
_isRolling = true;
// 현재 시드를 이력에 저장
_rollHistory.insert(0, _currentSeed);
// 굴리기 가능 여부 확인
if (!_rollService.canRoll) {
_isRolling = false;
_showRechargeDialog();
return;
}
// 최대 개수 초과 시 가장 오래된 항목 제거
if (_rollHistory.length > _maxRollHistory) {
_rollHistory.removeLast();
// 현재 상태를 서비스에 저장
final currentStats = Stats(
str: _str,
con: _con,
dex: _dex,
intelligence: _int,
wis: _wis,
cha: _cha,
hpMax: 0,
mpMax: 0,
);
final success = _rollService.roll(
currentStats: currentStats,
currentRaceIndex: _selectedRaceIndex,
currentKlassIndex: _selectedKlassIndex,
currentSeed: _currentSeed,
);
if (!success) {
_isRolling = false;
return;
}
// 새 시드로 굴림
@@ -173,14 +213,103 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
});
}
/// Unroll 버튼 클릭 (이전 롤로 복원)
void _onUnroll() {
if (_rollHistory.isEmpty) return;
/// 굴리기 충전 다이얼로그
Future<void> _showRechargeDialog() async {
final isPaidUser = IAPService.instance.isAdRemovalPurchased;
setState(() {
_currentSeed = _rollHistory.removeAt(0);
});
_rollStats();
final result = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
backgroundColor: RetroColors.panelBg,
title: const Text(
'RECHARGE ROLLS',
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 14,
color: RetroColors.gold,
),
),
content: Text(
isPaidUser
? 'Recharge 5 rolls for free?'
: 'Watch an ad to recharge 5 rolls?',
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 12,
color: RetroColors.textLight,
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text(
'CANCEL',
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 11,
color: RetroColors.textDisabled,
),
),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (!isPaidUser) ...[
const Icon(Icons.play_circle, size: 14, color: RetroColors.gold),
const SizedBox(width: 4),
],
const Text(
'RECHARGE',
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 11,
color: RetroColors.gold,
),
),
],
),
),
],
),
);
if (result == true && mounted) {
final success = await _rollService.rechargeRollsWithAd();
if (success && mounted) {
setState(() {});
}
}
}
/// Unroll 버튼 클릭 (이전 롤로 복원)
Future<void> _onUnroll() async {
if (!_rollService.canUndo) return;
final isPaidUser = IAPService.instance.isAdRemovalPurchased;
RollSnapshot? snapshot;
if (isPaidUser) {
snapshot = _rollService.undoPaidUser();
} else {
snapshot = await _rollService.undoFreeUser();
}
if (snapshot != null && mounted) {
setState(() {
_str = snapshot!.stats.str;
_con = snapshot.stats.con;
_dex = snapshot.stats.dex;
_int = snapshot.stats.intelligence;
_wis = snapshot.stats.wis;
_cha = snapshot.stats.cha;
_selectedRaceIndex = snapshot.raceIndex;
_selectedKlassIndex = snapshot.klassIndex;
_currentSeed = snapshot.seed;
});
_scrollToSelectedItems();
}
}
/// 이름 생성 버튼 클릭
@@ -266,6 +395,9 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
queue: QueueState.empty(),
);
// 캐릭터 생성 완료 알림 (되돌리기 상태 초기화)
_rollService.onCharacterCreated();
widget.onCharacterCreated?.call(initialState, testMode: _cheatsEnabled);
}
@@ -493,34 +625,27 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
RetroTextButton(
text: l10n.unroll,
icon: Icons.undo,
onPressed: _rollHistory.isEmpty ? null : _onUnroll,
isPrimary: false,
),
_buildUndoButton(l10n),
const SizedBox(width: 16),
RetroTextButton(
text: l10n.roll,
icon: Icons.casino,
onPressed: _onReroll,
),
_buildRollButton(l10n),
],
),
if (_rollHistory.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Center(
child: Text(
game_l10n.uiRollHistory(_rollHistory.length),
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 13,
color: RetroColors.textDisabled,
),
// 남은 횟수 표시
Padding(
padding: const EdgeInsets.only(top: 8),
child: Center(
child: Text(
_rollService.canUndo
? 'Undo: ${_rollService.availableUndos} | Rolls: ${_rollService.rollsRemaining}/5'
: 'Rolls: ${_rollService.rollsRemaining}/5',
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 11,
color: RetroColors.textDisabled,
),
),
),
),
],
),
);
@@ -790,4 +915,96 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
ClassPassiveType.firstStrikeBonus => passive.description,
};
}
// ===========================================================================
// 굴리기/되돌리기 버튼 위젯
// ===========================================================================
/// 되돌리기 버튼
Widget _buildUndoButton(L10n l10n) {
final canUndo = _rollService.canUndo;
final isPaidUser = IAPService.instance.isAdRemovalPurchased;
return GestureDetector(
onTap: canUndo ? _onUnroll : null,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: canUndo
? RetroColors.panelBgLight
: RetroColors.panelBg.withValues(alpha: 0.5),
border: Border.all(
color: canUndo ? RetroColors.panelBorderInner : RetroColors.panelBg,
width: 2,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
// 무료 유저는 광고 아이콘 표시
if (!isPaidUser && canUndo) ...[
const Icon(
Icons.play_circle,
size: 14,
color: RetroColors.gold,
),
const SizedBox(width: 4),
],
Icon(
Icons.undo,
size: 14,
color: canUndo ? RetroColors.textLight : RetroColors.textDisabled,
),
const SizedBox(width: 4),
Text(
l10n.unroll.toUpperCase(),
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 11,
color: canUndo ? RetroColors.textLight : RetroColors.textDisabled,
),
),
],
),
),
);
}
/// 굴리기 버튼
Widget _buildRollButton(L10n l10n) {
final canRoll = _rollService.canRoll;
return GestureDetector(
onTap: _onReroll,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: RetroColors.panelBgLight,
border: Border.all(color: RetroColors.gold, width: 2),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
// 0회일 때 광고 아이콘 표시
if (!canRoll) ...[
const Icon(Icons.play_circle, size: 14, color: RetroColors.gold),
const SizedBox(width: 4),
],
const Icon(Icons.casino, size: 14, color: RetroColors.gold),
const SizedBox(width: 4),
Text(
canRoll
? '${l10n.roll.toUpperCase()} (${_rollService.rollsRemaining})'
: l10n.roll.toUpperCase(),
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 11,
color: RetroColors.gold,
),
),
],
),
),
);
}
}