Files
asciinevrdie/lib/src/core/engine/progress_loop.dart
JiWoong Sul 606d052e2c refactor(core): 진행 루프, 저장 데이터, 저장 관리자 개선
- ProgressLoop 로직 정리
- SaveData 모델 확장
- SaveManager 개선
2026-01-08 16:05:08 +09:00

190 lines
5.6 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 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;
}
}