- audio_service: 오디오 처리 로직 수정 - ad_service: 광고 서비스 개선 - character_roll_service: 캐릭터 롤 로직 수정 - iap_service: 인앱 결제 로직 개선 - progress_loop: 진행 루프 업데이트 - return_rewards_service: 복귀 보상 로직 개선 - settings_repository: 설정 저장소 수정
198 lines
5.8 KiB
Dart
198 lines
5.8 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:asciineverdie/src/core/model/game_state.dart';
|
|
import 'package:asciineverdie/src/core/storage/save_manager.dart';
|
|
import 'package:asciineverdie/src/core/engine/progress_service.dart';
|
|
|
|
class AutoSaveConfig {
|
|
const AutoSaveConfig({
|
|
this.onLevelUp = true,
|
|
this.onQuestComplete = true,
|
|
this.onActComplete = true,
|
|
this.onStop = true,
|
|
this.onDeath = true,
|
|
});
|
|
|
|
final bool onLevelUp;
|
|
final bool onQuestComplete;
|
|
final bool onActComplete;
|
|
final bool onStop;
|
|
|
|
/// 사망 시 자동 저장 (Phase 4)
|
|
final bool onDeath;
|
|
|
|
bool shouldSave(ProgressTickResult result) {
|
|
return (onLevelUp && result.leveledUp) ||
|
|
(onQuestComplete && result.completedQuest) ||
|
|
(onActComplete && result.completedAct) ||
|
|
(onDeath && result.playerDied);
|
|
}
|
|
}
|
|
|
|
/// Runs the periodic timer loop that advances tasks/quests/plots.
|
|
class ProgressLoop {
|
|
ProgressLoop({
|
|
required GameState initialState,
|
|
required this.progressService,
|
|
this.saveManager,
|
|
Duration tickInterval = const Duration(milliseconds: 50),
|
|
AutoSaveConfig autoSaveConfig = const AutoSaveConfig(),
|
|
DateTime Function()? now,
|
|
this.cheatsEnabled = false,
|
|
this.onPlayerDied,
|
|
this.onGameComplete,
|
|
List<int> availableSpeeds = const [1, 5],
|
|
int initialSpeedMultiplier = 1,
|
|
}) : _state = initialState,
|
|
_tickInterval = tickInterval,
|
|
_autoSaveConfig = autoSaveConfig,
|
|
_now = now ?? DateTime.now,
|
|
_stateController = StreamController<GameState>.broadcast(),
|
|
_availableSpeeds = availableSpeeds.isNotEmpty ? availableSpeeds : [1],
|
|
_speedMultiplier = initialSpeedMultiplier;
|
|
|
|
final ProgressService progressService;
|
|
final SaveManager? saveManager;
|
|
final Duration _tickInterval;
|
|
|
|
/// 플레이어 사망 시 콜백 (Phase 4)
|
|
final void Function()? onPlayerDied;
|
|
|
|
/// 게임 클리어 시 콜백 (Act V 완료)
|
|
final void Function()? onGameComplete;
|
|
final AutoSaveConfig _autoSaveConfig;
|
|
final DateTime Function() _now;
|
|
final StreamController<GameState> _stateController;
|
|
bool cheatsEnabled;
|
|
|
|
Timer? _timer;
|
|
int? _lastTickMs;
|
|
int _speedMultiplier;
|
|
List<int> _availableSpeeds;
|
|
|
|
GameState get current => _state;
|
|
Stream<GameState> get stream => _stateController.stream;
|
|
GameState _state;
|
|
|
|
/// 가용 배속 목록
|
|
List<int> get availableSpeeds => List.unmodifiable(_availableSpeeds);
|
|
|
|
/// 현재 배속 (1x, 2x, 5x)
|
|
int get speedMultiplier => _speedMultiplier;
|
|
|
|
/// 배속 순환: 가용 배속 목록 순환
|
|
/// 명예의 전당에 캐릭터 없으면: 1 -> 5 -> 1
|
|
/// 명예의 전당에 캐릭터 있으면: 1 -> 2 -> 5 -> 1
|
|
void cycleSpeed() {
|
|
final currentIndex = _availableSpeeds.indexOf(_speedMultiplier);
|
|
final nextIndex = (currentIndex + 1) % _availableSpeeds.length;
|
|
_speedMultiplier = _availableSpeeds[nextIndex];
|
|
}
|
|
|
|
/// 특정 배속으로 직접 설정
|
|
/// 가용 배속 목록에 있는 경우에만 설정
|
|
void setSpeed(int speed) {
|
|
if (_availableSpeeds.contains(speed)) {
|
|
_speedMultiplier = speed;
|
|
}
|
|
}
|
|
|
|
/// 가용 배속 목록 업데이트 (명예의 전당 상태 변경 시)
|
|
void updateAvailableSpeeds(List<int> speeds) {
|
|
if (speeds.isEmpty) return;
|
|
_availableSpeeds = speeds;
|
|
// 현재 배속이 새 목록에 없으면 첫 번째로 리셋
|
|
if (!_availableSpeeds.contains(_speedMultiplier)) {
|
|
_speedMultiplier = _availableSpeeds.first;
|
|
}
|
|
}
|
|
|
|
void start() {
|
|
_lastTickMs = _now().millisecondsSinceEpoch;
|
|
_timer ??= Timer.periodic(_tickInterval, (_) => tickOnce());
|
|
}
|
|
|
|
Future<void> stop({bool saveOnStop = false}) async {
|
|
_timer?.cancel();
|
|
_timer = null;
|
|
if (saveOnStop && _autoSaveConfig.onStop && saveManager != null) {
|
|
await saveManager!.saveState(_state, cheatsEnabled: cheatsEnabled);
|
|
}
|
|
}
|
|
|
|
void dispose() {
|
|
_timer?.cancel();
|
|
_stateController.close();
|
|
}
|
|
|
|
/// Run one iteration of the loop (used by Timer or manual stepping).
|
|
GameState tickOnce({int? deltaMillis}) {
|
|
// 사망 상태면 틱 진행 안 함 (Phase 4)
|
|
if (_state.isDead) {
|
|
return _state;
|
|
}
|
|
|
|
final baseDelta = deltaMillis ?? _computeDelta();
|
|
final delta = baseDelta * _speedMultiplier;
|
|
final result = progressService.tick(_state, delta);
|
|
_state = result.state;
|
|
_stateController.add(_state);
|
|
|
|
if (saveManager != null && _autoSaveConfig.shouldSave(result)) {
|
|
saveManager!.saveState(_state, cheatsEnabled: cheatsEnabled);
|
|
}
|
|
|
|
// 사망 시 루프 정지 및 콜백 호출 (Phase 4)
|
|
if (result.playerDied) {
|
|
_timer?.cancel();
|
|
_timer = null;
|
|
onPlayerDied?.call();
|
|
}
|
|
|
|
// 게임 클리어 시 루프 정지 및 콜백 호출 (Act V 완료)
|
|
if (result.gameComplete) {
|
|
_timer?.cancel();
|
|
_timer = null;
|
|
onGameComplete?.call();
|
|
}
|
|
|
|
return _state;
|
|
}
|
|
|
|
/// Replace state (e.g., after loading) and reset timing.
|
|
void replaceState(GameState newState) {
|
|
_state = newState;
|
|
_stateController.add(newState);
|
|
_lastTickMs = _now().millisecondsSinceEpoch;
|
|
}
|
|
|
|
// Developer-only helpers mirroring original cheat panel actions.
|
|
void cheatCompleteTask() {
|
|
if (!cheatsEnabled) return;
|
|
_state = progressService.forceTaskComplete(_state);
|
|
_stateController.add(_state);
|
|
}
|
|
|
|
void cheatCompleteQuest() {
|
|
if (!cheatsEnabled) return;
|
|
_state = progressService.forceQuestComplete(_state);
|
|
_stateController.add(_state);
|
|
}
|
|
|
|
void cheatCompletePlot() {
|
|
if (!cheatsEnabled) return;
|
|
_state = progressService.forcePlotComplete(_state);
|
|
_stateController.add(_state);
|
|
}
|
|
|
|
int _computeDelta() {
|
|
final nowMs = _now().millisecondsSinceEpoch;
|
|
final last = _lastTickMs;
|
|
_lastTickMs = nowMs;
|
|
if (last == null) return 0;
|
|
final delta = nowMs - last;
|
|
if (delta < 0) return 0;
|
|
return delta;
|
|
}
|
|
}
|