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