feat(character): 캐릭터 롤 서비스 추가
- 굴리기 횟수 제한 및 충전 - 스탯 히스토리 기반 되돌리기 - 광고 시청으로 굴리기 충전
This commit is contained in:
282
lib/src/core/engine/character_roll_service.dart
Normal file
282
lib/src/core/engine/character_roll_service.dart
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import 'package:asciineverdie/src/core/engine/ad_service.dart';
|
||||||
|
import 'package:asciineverdie/src/core/engine/iap_service.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||||
|
|
||||||
|
/// 캐릭터 생성 굴리기/되돌리기 서비스
|
||||||
|
///
|
||||||
|
/// 굴리기 횟수 제한과 되돌리기 기능을 관리합니다.
|
||||||
|
/// - 굴리기: 5회 (0회 시 광고 시청으로 충전)
|
||||||
|
/// - 되돌리기: 무료 유저 1회(광고), 유료 유저 3회(무료)
|
||||||
|
class CharacterRollService {
|
||||||
|
CharacterRollService._();
|
||||||
|
|
||||||
|
static CharacterRollService? _instance;
|
||||||
|
|
||||||
|
/// 싱글톤 인스턴스
|
||||||
|
static CharacterRollService get instance {
|
||||||
|
_instance ??= CharacterRollService._();
|
||||||
|
return _instance!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// 상수
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
/// 저장 키
|
||||||
|
static const String _rollsRemainingKey = 'char_rolls_remaining';
|
||||||
|
|
||||||
|
/// 최대 굴리기 횟수
|
||||||
|
static const int maxRolls = 5;
|
||||||
|
|
||||||
|
/// 최대 되돌리기 횟수 (무료 유저)
|
||||||
|
static const int maxUndoFreeUser = 1;
|
||||||
|
|
||||||
|
/// 최대 되돌리기 횟수 (유료 유저)
|
||||||
|
static const int maxUndoPaidUser = 3;
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// 상태
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
bool _isInitialized = false;
|
||||||
|
|
||||||
|
/// 남은 굴리기 횟수
|
||||||
|
int _rollsRemaining = maxRolls;
|
||||||
|
|
||||||
|
/// 남은 되돌리기 횟수
|
||||||
|
int _undoRemaining = maxUndoFreeUser;
|
||||||
|
|
||||||
|
/// 되돌리기용 스탯 히스토리
|
||||||
|
final List<RollSnapshot> _rollHistory = [];
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// 초기화
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
/// 서비스 초기화
|
||||||
|
Future<void> initialize() async {
|
||||||
|
if (_isInitialized) return;
|
||||||
|
|
||||||
|
await _loadState();
|
||||||
|
_resetUndoForNewSession();
|
||||||
|
|
||||||
|
_isInitialized = true;
|
||||||
|
debugPrint('[CharacterRollService] Initialized: '
|
||||||
|
'rolls=$_rollsRemaining, undo=$_undoRemaining');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 저장된 상태 로드
|
||||||
|
Future<void> _loadState() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
_rollsRemaining = prefs.getInt(_rollsRemainingKey) ?? maxRolls;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 굴리기 횟수 저장
|
||||||
|
Future<void> _saveRollsRemaining() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setInt(_rollsRemainingKey, _rollsRemaining);
|
||||||
|
debugPrint('[CharacterRollService] Saved rolls: $_rollsRemaining');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 새 세션 시작 시 되돌리기 초기화
|
||||||
|
void _resetUndoForNewSession() {
|
||||||
|
_undoRemaining = _isPaidUser ? maxUndoPaidUser : maxUndoFreeUser;
|
||||||
|
_rollHistory.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// 유료 사용자 확인
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
/// 유료 사용자 여부
|
||||||
|
bool get _isPaidUser => IAPService.instance.isAdRemovalPurchased;
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// 굴리기
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
/// 남은 굴리기 횟수
|
||||||
|
int get rollsRemaining => _rollsRemaining;
|
||||||
|
|
||||||
|
/// 굴리기 가능 여부
|
||||||
|
bool get canRoll => _rollsRemaining > 0;
|
||||||
|
|
||||||
|
/// 굴리기 실행
|
||||||
|
///
|
||||||
|
/// [currentStats] 현재 스탯 (되돌리기용 저장)
|
||||||
|
/// [currentRaceIndex] 현재 종족 인덱스
|
||||||
|
/// [currentKlassIndex] 현재 직업 인덱스
|
||||||
|
/// [currentSeed] 현재 RNG 시드
|
||||||
|
/// Returns: 굴리기 성공 여부
|
||||||
|
bool roll({
|
||||||
|
required Stats currentStats,
|
||||||
|
required int currentRaceIndex,
|
||||||
|
required int currentKlassIndex,
|
||||||
|
required int currentSeed,
|
||||||
|
}) {
|
||||||
|
if (!canRoll) {
|
||||||
|
debugPrint('[CharacterRollService] Cannot roll: no rolls remaining');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 현재 상태를 히스토리에 저장
|
||||||
|
_rollHistory.insert(
|
||||||
|
0,
|
||||||
|
RollSnapshot(
|
||||||
|
stats: currentStats,
|
||||||
|
raceIndex: currentRaceIndex,
|
||||||
|
klassIndex: currentKlassIndex,
|
||||||
|
seed: currentSeed,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 최대 히스토리 개수 제한 (되돌리기 가능 횟수만큼)
|
||||||
|
final maxHistory = _isPaidUser ? maxUndoPaidUser : maxUndoFreeUser;
|
||||||
|
while (_rollHistory.length > maxHistory) {
|
||||||
|
_rollHistory.removeLast();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 굴리기 횟수 감소
|
||||||
|
_rollsRemaining--;
|
||||||
|
_saveRollsRemaining();
|
||||||
|
|
||||||
|
debugPrint('[CharacterRollService] Rolled: remaining=$_rollsRemaining, '
|
||||||
|
'history=${_rollHistory.length}');
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 광고 시청 후 굴리기 충전
|
||||||
|
Future<bool> rechargeRollsWithAd() async {
|
||||||
|
// 유료 사용자는 광고 없이 충전
|
||||||
|
if (_isPaidUser) {
|
||||||
|
_rollsRemaining = maxRolls;
|
||||||
|
await _saveRollsRemaining();
|
||||||
|
debugPrint('[CharacterRollService] Recharged (paid user): $maxRolls');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 인터스티셜 광고 표시
|
||||||
|
final result = await AdService.instance.showInterstitialAd(
|
||||||
|
adType: AdType.interstitialRoll,
|
||||||
|
onComplete: () {
|
||||||
|
_rollsRemaining = maxRolls;
|
||||||
|
_saveRollsRemaining();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result == AdResult.completed || result == AdResult.debugSkipped) {
|
||||||
|
debugPrint('[CharacterRollService] Recharged with ad: $maxRolls');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('[CharacterRollService] Recharge failed: $result');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// 되돌리기
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
/// 남은 되돌리기 횟수
|
||||||
|
int get undoRemaining => _undoRemaining;
|
||||||
|
|
||||||
|
/// 되돌리기 히스토리 길이
|
||||||
|
int get historyLength => _rollHistory.length;
|
||||||
|
|
||||||
|
/// 실제 사용 가능한 되돌리기 횟수
|
||||||
|
/// min(undoRemaining, historyLength)
|
||||||
|
int get availableUndos {
|
||||||
|
final available = _undoRemaining < _rollHistory.length
|
||||||
|
? _undoRemaining
|
||||||
|
: _rollHistory.length;
|
||||||
|
return available;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 되돌리기 가능 여부
|
||||||
|
bool get canUndo => availableUndos > 0;
|
||||||
|
|
||||||
|
/// 되돌리기 실행 (유료 사용자)
|
||||||
|
///
|
||||||
|
/// Returns: 복원된 스냅샷 (null이면 실패)
|
||||||
|
RollSnapshot? undoPaidUser() {
|
||||||
|
if (!_isPaidUser) return null;
|
||||||
|
if (!canUndo) return null;
|
||||||
|
|
||||||
|
final snapshot = _rollHistory.removeAt(0);
|
||||||
|
_undoRemaining--;
|
||||||
|
|
||||||
|
debugPrint('[CharacterRollService] Undo (paid): '
|
||||||
|
'remaining=$_undoRemaining, history=${_rollHistory.length}');
|
||||||
|
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 되돌리기 실행 (무료 사용자 - 광고 필요)
|
||||||
|
///
|
||||||
|
/// [onSuccess] 광고 시청 완료 후 콜백
|
||||||
|
Future<RollSnapshot?> undoFreeUser() async {
|
||||||
|
if (_isPaidUser) return undoPaidUser();
|
||||||
|
if (!canUndo) return null;
|
||||||
|
|
||||||
|
// 리워드 광고 표시
|
||||||
|
RollSnapshot? result;
|
||||||
|
|
||||||
|
final adResult = await AdService.instance.showRewardedAd(
|
||||||
|
adType: AdType.rewardUndo,
|
||||||
|
onRewarded: () {
|
||||||
|
if (_rollHistory.isNotEmpty) {
|
||||||
|
result = _rollHistory.removeAt(0);
|
||||||
|
_undoRemaining--;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (adResult == AdResult.completed || adResult == AdResult.debugSkipped) {
|
||||||
|
debugPrint('[CharacterRollService] Undo (free with ad): '
|
||||||
|
'remaining=$_undoRemaining, history=${_rollHistory.length}');
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('[CharacterRollService] Undo failed: $adResult');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// 캐릭터 생성 완료
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
/// 캐릭터 생성 완료 시 호출
|
||||||
|
///
|
||||||
|
/// 되돌리기 상태만 초기화 (굴리기 횟수는 유지)
|
||||||
|
void onCharacterCreated() {
|
||||||
|
_resetUndoForNewSession();
|
||||||
|
debugPrint('[CharacterRollService] Character created, undo reset');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 굴리기 횟수 완전 초기화 (디버그용)
|
||||||
|
Future<void> resetRolls() async {
|
||||||
|
_rollsRemaining = maxRolls;
|
||||||
|
await _saveRollsRemaining();
|
||||||
|
_resetUndoForNewSession();
|
||||||
|
debugPrint('[CharacterRollService] Reset all');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 굴리기 스냅샷 (되돌리기용)
|
||||||
|
class RollSnapshot {
|
||||||
|
const RollSnapshot({
|
||||||
|
required this.stats,
|
||||||
|
required this.raceIndex,
|
||||||
|
required this.klassIndex,
|
||||||
|
required this.seed,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Stats stats;
|
||||||
|
final int raceIndex;
|
||||||
|
final int klassIndex;
|
||||||
|
final int seed;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user