feat(story): Phase 9 스토리/엔딩 시스템 구현
- story_data.dart: 5개 Act 스토리 텍스트 및 ASCII 아트 - story_service.dart: Act 전환/보스 조우/엔딩 이벤트 관리 - cinematic_view.dart: 풀스크린 시네마틱 UI (페이드, 스킵) - game_play_screen.dart: 레벨 기반 Act 전환 시 시네마틱 재생
This commit is contained in:
186
lib/src/core/engine/story_service.dart
Normal file
186
lib/src/core/engine/story_service.dart
Normal file
@@ -0,0 +1,186 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:askiineverdie/data/story_data.dart';
|
||||
|
||||
/// 스토리 이벤트 타입 (Story Event Type)
|
||||
enum StoryEventType {
|
||||
actStart, // Act 시작
|
||||
actComplete, // Act 완료
|
||||
bossEncounter, // 보스 조우
|
||||
bossDefeat, // 보스 처치
|
||||
ending, // 엔딩
|
||||
}
|
||||
|
||||
/// 스토리 이벤트 (Story Event)
|
||||
class StoryEvent {
|
||||
const StoryEvent({
|
||||
required this.type,
|
||||
required this.act,
|
||||
this.data,
|
||||
});
|
||||
|
||||
final StoryEventType type;
|
||||
final StoryAct act;
|
||||
final Map<String, dynamic>? data;
|
||||
}
|
||||
|
||||
/// 스토리 서비스 (Phase 9: Story Progression Management)
|
||||
///
|
||||
/// Act 전환, 시네마틱 트리거, 보스 조우 관리
|
||||
class StoryService {
|
||||
StoryService();
|
||||
|
||||
final _eventController = StreamController<StoryEvent>.broadcast();
|
||||
|
||||
/// 스토리 이벤트 스트림 (Story Event Stream)
|
||||
Stream<StoryEvent> get events => _eventController.stream;
|
||||
|
||||
// 현재 Act 추적
|
||||
StoryAct _currentAct = StoryAct.prologue;
|
||||
bool _hasSeenPrologue = false;
|
||||
final Set<StoryAct> _completedActs = {};
|
||||
|
||||
/// 현재 Act (Current Act)
|
||||
StoryAct get currentAct => _currentAct;
|
||||
|
||||
/// 프롤로그 시청 여부
|
||||
bool get hasSeenPrologue => _hasSeenPrologue;
|
||||
|
||||
/// 완료된 Act 목록
|
||||
Set<StoryAct> get completedActs => Set.unmodifiable(_completedActs);
|
||||
|
||||
/// 레벨 변화 감지 및 Act 전환 처리 (Process Level Change)
|
||||
///
|
||||
/// 레벨업 시 호출하여 Act 전환 이벤트 트리거
|
||||
StoryEvent? processLevelChange(int oldLevel, int newLevel) {
|
||||
final oldAct = getActForLevel(oldLevel);
|
||||
final newAct = getActForLevel(newLevel);
|
||||
|
||||
// 새 게임 시작 (프롤로그)
|
||||
if (oldLevel == 0 && newLevel == 1 && !_hasSeenPrologue) {
|
||||
_hasSeenPrologue = true;
|
||||
_currentAct = StoryAct.prologue;
|
||||
final event = StoryEvent(
|
||||
type: StoryEventType.actStart,
|
||||
act: StoryAct.prologue,
|
||||
);
|
||||
_eventController.add(event);
|
||||
return event;
|
||||
}
|
||||
|
||||
// Act 전환 감지
|
||||
if (newAct != oldAct && newAct != _currentAct) {
|
||||
// 이전 Act 완료 처리
|
||||
if (_currentAct != StoryAct.prologue) {
|
||||
_completedActs.add(_currentAct);
|
||||
_eventController.add(StoryEvent(
|
||||
type: StoryEventType.actComplete,
|
||||
act: _currentAct,
|
||||
));
|
||||
}
|
||||
|
||||
// 새 Act 시작
|
||||
_currentAct = newAct;
|
||||
final event = StoryEvent(
|
||||
type: StoryEventType.actStart,
|
||||
act: newAct,
|
||||
);
|
||||
_eventController.add(event);
|
||||
return event;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 보스 조우 처리 (Process Boss Encounter)
|
||||
void processBossEncounter(String monsterName) {
|
||||
// BOSS: 접두사가 있는 몬스터인지 확인
|
||||
if (!monsterName.startsWith('BOSS:')) return;
|
||||
|
||||
final event = StoryEvent(
|
||||
type: StoryEventType.bossEncounter,
|
||||
act: _currentAct,
|
||||
data: {'bossName': monsterName},
|
||||
);
|
||||
_eventController.add(event);
|
||||
}
|
||||
|
||||
/// 보스 처치 처리 (Process Boss Defeat)
|
||||
void processBossDefeat(String monsterName) {
|
||||
if (!monsterName.startsWith('BOSS:')) return;
|
||||
|
||||
final event = StoryEvent(
|
||||
type: StoryEventType.bossDefeat,
|
||||
act: _currentAct,
|
||||
data: {'bossName': monsterName},
|
||||
);
|
||||
_eventController.add(event);
|
||||
|
||||
// 최종 보스 처치 시 엔딩
|
||||
if (monsterName.contains('Primordial Glitch')) {
|
||||
_triggerEnding();
|
||||
}
|
||||
}
|
||||
|
||||
/// 엔딩 트리거 (Trigger Ending)
|
||||
void _triggerEnding() {
|
||||
_completedActs.add(StoryAct.act5);
|
||||
_currentAct = StoryAct.ending;
|
||||
_eventController.add(StoryEvent(
|
||||
type: StoryEventType.ending,
|
||||
act: StoryAct.ending,
|
||||
));
|
||||
}
|
||||
|
||||
/// 시네마틱 데이터 가져오기 (Get Cinematic Data)
|
||||
List<CinematicStep> getCinematicSteps(StoryAct act) {
|
||||
return cinematicData[act] ?? [];
|
||||
}
|
||||
|
||||
/// Act 제목 가져오기 (Get Act Title)
|
||||
String getActTitle(StoryAct act) {
|
||||
return actTitles[act] ?? 'Unknown';
|
||||
}
|
||||
|
||||
/// Act 보스 이름 가져오기 (Get Act Boss Name)
|
||||
String? getActBossName(StoryAct act) {
|
||||
return actBossNames[act];
|
||||
}
|
||||
|
||||
/// 저장 데이터로 상태 복원 (Restore State from Save)
|
||||
void restoreState({
|
||||
required StoryAct currentAct,
|
||||
required bool hasSeenPrologue,
|
||||
required Set<StoryAct> completedActs,
|
||||
}) {
|
||||
_currentAct = currentAct;
|
||||
_hasSeenPrologue = hasSeenPrologue;
|
||||
_completedActs
|
||||
..clear()
|
||||
..addAll(completedActs);
|
||||
}
|
||||
|
||||
/// 현재 상태를 저장 데이터로 변환 (Convert to Save Data)
|
||||
Map<String, dynamic> toSaveData() {
|
||||
return {
|
||||
'currentAct': _currentAct.index,
|
||||
'hasSeenPrologue': _hasSeenPrologue,
|
||||
'completedActs': _completedActs.map((a) => a.index).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
/// 저장 데이터에서 복원 (Restore from Save Data)
|
||||
void fromSaveData(Map<String, dynamic> data) {
|
||||
_currentAct = StoryAct.values[data['currentAct'] as int? ?? 0];
|
||||
_hasSeenPrologue = data['hasSeenPrologue'] as bool? ?? false;
|
||||
final completedIndices = data['completedActs'] as List<dynamic>? ?? [];
|
||||
_completedActs
|
||||
..clear()
|
||||
..addAll(completedIndices.map((i) => StoryAct.values[i as int]));
|
||||
}
|
||||
|
||||
/// 서비스 정리 (Dispose)
|
||||
void dispose() {
|
||||
_eventController.close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user