feat(game): 게임 세션 및 사망 오버레이 개선

- GameSessionController 기능 확장
- DeathOverlay 상세 사망 정보 표시
- GamePlayScreen 연동 업데이트
This commit is contained in:
JiWoong Sul
2026-01-07 22:13:25 +09:00
parent c02978c960
commit 590c79cc23
3 changed files with 132 additions and 5 deletions

View File

@@ -256,9 +256,6 @@ class _GamePlayScreenState extends State<GamePlayScreen>
final taskType = state.progress.currentTask.type; final taskType = state.progress.currentTask.type;
final isInBattleTask = taskType == TaskType.kill; final isInBattleTask = taskType == TaskType.kill;
debugPrint(
'[BGM] TaskType: $taskType, isInBattle: $isInBattleTask, currentBgm: ${audio.currentBgm}');
// 전투 태스크 상태 결정 // 전투 태스크 상태 결정
if (isInBattleTask) { if (isInBattleTask) {
// 전투 태스크: 보스 여부에 따라 BGM 선택 // 전투 태스크: 보스 여부에 따라 BGM 선택
@@ -269,13 +266,11 @@ class _GamePlayScreenState extends State<GamePlayScreen>
// 전환 시점이거나 현재 BGM이 일치하지 않으면 재생 // 전환 시점이거나 현재 BGM이 일치하지 않으면 재생
if (!_wasInBattleTask || audio.currentBgm != expectedBgm) { if (!_wasInBattleTask || audio.currentBgm != expectedBgm) {
debugPrint('[BGM] Playing battle BGM: $expectedBgm');
audio.playBgm(expectedBgm); audio.playBgm(expectedBgm);
} }
} else { } else {
// 비전투 태스크: 항상 마을 BGM 유지 (이미 town이면 스킵) // 비전투 태스크: 항상 마을 BGM 유지 (이미 town이면 스킵)
if (audio.currentBgm != 'town') { if (audio.currentBgm != 'town') {
debugPrint('[BGM] Playing town BGM (was: ${audio.currentBgm})');
audio.playBgm('town'); audio.playBgm('town');
} }
} }
@@ -825,6 +820,12 @@ class _GamePlayScreenState extends State<GamePlayScreen>
deathInfo: state.deathInfo!, deathInfo: state.deathInfo!,
traits: state.traits, traits: state.traits,
onResurrect: _handleResurrect, onResurrect: _handleResurrect,
isAutoResurrectEnabled: widget.controller.autoResurrect,
onToggleAutoResurrect: () {
widget.controller.setAutoResurrect(
!widget.controller.autoResurrect,
);
},
), ),
// 승리 오버레이 (게임 클리어) // 승리 오버레이 (게임 클리어)
if (widget.controller.isComplete) if (widget.controller.isComplete)
@@ -968,6 +969,12 @@ class _GamePlayScreenState extends State<GamePlayScreen>
deathInfo: state.deathInfo!, deathInfo: state.deathInfo!,
traits: state.traits, traits: state.traits,
onResurrect: _handleResurrect, onResurrect: _handleResurrect,
isAutoResurrectEnabled: widget.controller.autoResurrect,
onToggleAutoResurrect: () {
widget.controller.setAutoResurrect(
!widget.controller.autoResurrect,
);
},
), ),
// 승리 오버레이 (게임 클리어) // 승리 오버레이 (게임 클리어)
if (widget.controller.isComplete) if (widget.controller.isComplete)

View File

@@ -47,6 +47,9 @@ class GameSessionController extends ChangeNotifier {
GameState? _state; GameState? _state;
String? _error; String? _error;
// 자동 부활 (Auto-Resurrection) 상태
bool _autoResurrect = false;
// 통계 관련 필드 // 통계 관련 필드
SessionStatistics _sessionStats = SessionStatistics.empty(); SessionStatistics _sessionStats = SessionStatistics.empty();
CumulativeStatistics _cumulativeStats = CumulativeStatistics.empty(); CumulativeStatistics _cumulativeStats = CumulativeStatistics.empty();
@@ -61,6 +64,15 @@ class GameSessionController extends ChangeNotifier {
bool get isRunning => _status == GameSessionStatus.running; bool get isRunning => _status == GameSessionStatus.running;
bool get cheatsEnabled => _cheatsEnabled; bool get cheatsEnabled => _cheatsEnabled;
/// 자동 부활 활성화 여부
bool get autoResurrect => _autoResurrect;
/// 자동 부활 설정
void setAutoResurrect(bool value) {
_autoResurrect = value;
notifyListeners();
}
/// 현재 세션 통계 /// 현재 세션 통계
SessionStatistics get sessionStats => _sessionStats; SessionStatistics get sessionStats => _sessionStats;
@@ -75,6 +87,9 @@ class GameSessionController extends ChangeNotifier {
bool cheatsEnabled = false, bool cheatsEnabled = false,
bool isNewGame = true, bool isNewGame = true,
}) async { }) async {
// 기존 배속 보존 (부활/재개 시 유지)
final previousSpeed = _loop?.speedMultiplier ?? 1;
await _stopLoop(saveOnStop: false); await _stopLoop(saveOnStop: false);
// 새 게임인 경우 초기화 (프롤로그 태스크 설정) // 새 게임인 경우 초기화 (프롤로그 태스크 설정)
@@ -97,6 +112,9 @@ class GameSessionController extends ChangeNotifier {
// 명예의 전당 체크 → 가용 배속 결정 // 명예의 전당 체크 → 가용 배속 결정
final availableSpeeds = await _getAvailableSpeeds(); final availableSpeeds = await _getAvailableSpeeds();
// 새 게임이면 1배속, 재개/부활이면 기존 배속 유지
final initialSpeed = isNewGame ? 1 : previousSpeed;
_loop = ProgressLoop( _loop = ProgressLoop(
initialState: state, initialState: state,
progressService: progressService, progressService: progressService,
@@ -108,6 +126,7 @@ class GameSessionController extends ChangeNotifier {
onPlayerDied: _onPlayerDied, onPlayerDied: _onPlayerDied,
onGameComplete: _onGameComplete, onGameComplete: _onGameComplete,
availableSpeeds: availableSpeeds, availableSpeeds: availableSpeeds,
initialSpeedMultiplier: initialSpeed,
); );
_subscription = _loop!.stream.listen((next) { _subscription = _loop!.stream.listen((next) {
@@ -273,6 +292,24 @@ class GameSessionController extends ChangeNotifier {
_sessionStats = _sessionStats.recordDeath(); _sessionStats = _sessionStats.recordDeath();
_status = GameSessionStatus.dead; _status = GameSessionStatus.dead;
notifyListeners(); notifyListeners();
// 자동 부활이 활성화된 경우 잠시 후 자동으로 부활
if (_autoResurrect) {
_scheduleAutoResurrect();
}
}
/// 자동 부활 예약 (Auto-Resurrection Scheduler)
///
/// 사망 오버레이를 잠시 표시한 후 자동으로 부활 처리
void _scheduleAutoResurrect() {
Future.delayed(const Duration(milliseconds: 800), () async {
// 상태가 여전히 dead이고, 자동 부활이 활성화된 경우에만 부활
if (_status == GameSessionStatus.dead && _autoResurrect) {
await resurrect();
await resumeAfterResurrection();
}
});
} }
/// 게임 클리어 콜백 (ProgressLoop에서 호출, Act V 완료 시) /// 게임 클리어 콜백 (ProgressLoop에서 호출, Act V 완료 시)

View File

@@ -15,6 +15,8 @@ class DeathOverlay extends StatelessWidget {
required this.deathInfo, required this.deathInfo,
required this.traits, required this.traits,
required this.onResurrect, required this.onResurrect,
this.isAutoResurrectEnabled = false,
this.onToggleAutoResurrect,
}); });
/// 사망 정보 /// 사망 정보
@@ -26,6 +28,12 @@ class DeathOverlay extends StatelessWidget {
/// 부활 버튼 콜백 /// 부활 버튼 콜백
final VoidCallback onResurrect; final VoidCallback onResurrect;
/// 자동 부활 활성화 여부
final bool isAutoResurrectEnabled;
/// 자동 부활 토글 콜백
final VoidCallback? onToggleAutoResurrect;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// 테마 인식 색상 (Theme-aware colors) // 테마 인식 색상 (Theme-aware colors)
@@ -132,6 +140,12 @@ class DeathOverlay extends StatelessWidget {
// 부활 버튼 // 부활 버튼
_buildResurrectButton(context), _buildResurrectButton(context),
// 자동 부활 버튼
if (onToggleAutoResurrect != null) ...[
const SizedBox(height: 12),
_buildAutoResurrectButton(context),
],
], ],
), ),
), ),
@@ -456,6 +470,75 @@ class DeathOverlay extends StatelessWidget {
); );
} }
/// 자동 부활 토글 버튼
Widget _buildAutoResurrectButton(BuildContext context) {
final mpColor = RetroColors.mpOf(context);
final mpDark = RetroColors.mpDarkOf(context);
final muted = RetroColors.textMutedOf(context);
// 활성화 상태에 따른 색상
final buttonColor = isAutoResurrectEnabled ? mpColor : muted;
final buttonDark = isAutoResurrectEnabled ? mpDark : muted.withValues(alpha: 0.5);
return GestureDetector(
onTap: onToggleAutoResurrect,
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: buttonColor.withValues(alpha: 0.15),
border: Border(
top: BorderSide(color: buttonColor, width: 2),
left: BorderSide(color: buttonColor, width: 2),
bottom: BorderSide(
color: buttonDark.withValues(alpha: 0.8),
width: 2,
),
right: BorderSide(
color: buttonDark.withValues(alpha: 0.8),
width: 2,
),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
isAutoResurrectEnabled ? '' : '',
style: TextStyle(
fontSize: 14,
color: buttonColor,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 8),
Text(
l10n.deathAutoResurrect.toUpperCase(),
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 8,
color: buttonColor,
letterSpacing: 1,
),
),
if (isAutoResurrectEnabled) ...[
const SizedBox(width: 6),
Text(
'ON',
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 8,
color: mpColor,
fontWeight: FontWeight.bold,
),
),
],
],
),
),
);
}
/// 사망 직전 전투 로그 표시 /// 사망 직전 전투 로그 표시
Widget _buildCombatLog(BuildContext context) { Widget _buildCombatLog(BuildContext context) {
final events = deathInfo.lastCombatEvents; final events = deathInfo.lastCombatEvents;