feat(death): Phase 4 사망/부활 시스템 구현
- DeathInfo, DeathCause 클래스 정의 (game_state.dart) - 사망 원인, 상실 장비 수, 사망 시점 정보 기록 - ShopService 구현 (shop_service.dart) - 장비 가격 계산 (레벨 * 50 * 희귀도 배율) - 슬롯별 장비 생성 (프로그래밍 테마) - 자동 구매 (빈 슬롯에 Common 장비) - ResurrectionService 구현 (resurrection_service.dart) - 사망 처리: 모든 장비 상실, 기본 무기만 유지 - 부활 처리: HP/MP 회복, 자동 장비 구매 - progress_service.dart 사망 판정 로직 추가 - 전투 중 HP <= 0 시 사망 처리 - ProgressTickResult에 playerDied 플래그 추가 - progress_loop.dart 사망 시 루프 정지 - onPlayerDied 콜백 추가 - 사망 상태에서 틱 진행 방지 - DeathOverlay 위젯 구현 (death_overlay.dart) - ASCII 스컬 아트, 사망 원인, 상실 정보 표시 - 부활 버튼 - GameSessionController 사망/부활 상태 관리 - GameSessionStatus.dead 상태 추가 - resurrect() 메서드로 부활 처리
This commit is contained in:
@@ -2,11 +2,13 @@ import 'dart:async';
|
||||
|
||||
import 'package:askiineverdie/src/core/engine/progress_loop.dart';
|
||||
import 'package:askiineverdie/src/core/engine/progress_service.dart';
|
||||
import 'package:askiineverdie/src/core/engine/resurrection_service.dart';
|
||||
import 'package:askiineverdie/src/core/engine/shop_service.dart';
|
||||
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||
import 'package:askiineverdie/src/core/storage/save_manager.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
enum GameSessionStatus { idle, loading, running, error }
|
||||
enum GameSessionStatus { idle, loading, running, error, dead }
|
||||
|
||||
/// Presentation-friendly wrapper that owns ProgressLoop and SaveManager.
|
||||
class GameSessionController extends ChangeNotifier {
|
||||
@@ -68,6 +70,7 @@ class GameSessionController extends ChangeNotifier {
|
||||
tickInterval: _tickInterval,
|
||||
now: _now,
|
||||
cheatsEnabled: cheatsEnabled,
|
||||
onPlayerDied: _onPlayerDied,
|
||||
);
|
||||
|
||||
_subscription = _loop!.stream.listen((next) {
|
||||
@@ -138,4 +141,36 @@ class GameSessionController extends ChangeNotifier {
|
||||
if (loop == null) return null;
|
||||
return loop.stop(saveOnStop: saveOnStop);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Phase 4: 사망/부활 처리
|
||||
// ============================================================================
|
||||
|
||||
/// 플레이어 사망 콜백 (ProgressLoop에서 호출)
|
||||
void _onPlayerDied() {
|
||||
_status = GameSessionStatus.dead;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 플레이어 부활 처리
|
||||
///
|
||||
/// HP/MP 회복, 빈 슬롯에 장비 자동 구매, 게임 재개
|
||||
Future<void> resurrect() async {
|
||||
if (_state == null || !_state!.isDead) return;
|
||||
|
||||
// ResurrectionService를 사용하여 부활 처리
|
||||
final shopService = ShopService(rng: _state!.rng);
|
||||
final resurrectionService = ResurrectionService(shopService: shopService);
|
||||
|
||||
final resurrectedState = resurrectionService.processResurrection(_state!);
|
||||
|
||||
// 저장
|
||||
await saveManager.saveState(resurrectedState);
|
||||
|
||||
// 게임 재개
|
||||
await startNew(resurrectedState, cheatsEnabled: _cheatsEnabled, isNewGame: false);
|
||||
}
|
||||
|
||||
/// 사망 상태 여부
|
||||
bool get isDead => _status == GameSessionStatus.dead || (_state?.isDead ?? false);
|
||||
}
|
||||
|
||||
256
lib/src/features/game/widgets/death_overlay.dart
Normal file
256
lib/src/features/game/widgets/death_overlay.dart
Normal file
@@ -0,0 +1,256 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||
|
||||
/// 사망 오버레이 위젯 (Phase 4)
|
||||
///
|
||||
/// 플레이어 사망 시 표시되는 전체 화면 오버레이
|
||||
class DeathOverlay extends StatelessWidget {
|
||||
const DeathOverlay({
|
||||
super.key,
|
||||
required this.deathInfo,
|
||||
required this.traits,
|
||||
required this.onResurrect,
|
||||
});
|
||||
|
||||
/// 사망 정보
|
||||
final DeathInfo deathInfo;
|
||||
|
||||
/// 캐릭터 특성 (이름, 직업 등)
|
||||
final Traits traits;
|
||||
|
||||
/// 부활 버튼 콜백
|
||||
final VoidCallback onResurrect;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return Material(
|
||||
color: Colors.black87,
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
margin: const EdgeInsets.all(24),
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: colorScheme.error.withValues(alpha: 0.5),
|
||||
width: 2,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colorScheme.error.withValues(alpha: 0.3),
|
||||
blurRadius: 20,
|
||||
spreadRadius: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 사망 타이틀
|
||||
_buildDeathTitle(context),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 캐릭터 정보
|
||||
_buildCharacterInfo(context),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 사망 원인
|
||||
_buildDeathCause(context),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 구분선
|
||||
Divider(color: colorScheme.outlineVariant),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 상실 정보
|
||||
_buildLossInfo(context),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 부활 버튼
|
||||
_buildResurrectButton(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDeathTitle(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
// ASCII 스컬
|
||||
Text(
|
||||
' _____\n / \\\n| () () |\n \\ ^ /\n |||||',
|
||||
style: TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
height: 1.0,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'YOU DIED',
|
||||
style: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
letterSpacing: 4,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCharacterInfo(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
traits.name,
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Level ${deathInfo.levelAtDeath} ${traits.klass}',
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDeathCause(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final causeText = _getDeathCauseText();
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.errorContainer.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.dangerous_outlined,
|
||||
size: 20,
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
causeText,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getDeathCauseText() {
|
||||
return switch (deathInfo.cause) {
|
||||
DeathCause.monster => 'Killed by ${deathInfo.killerName}',
|
||||
DeathCause.selfDamage => 'Self-inflicted damage',
|
||||
DeathCause.environment => 'Environmental hazard',
|
||||
};
|
||||
}
|
||||
|
||||
Widget _buildLossInfo(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
_buildInfoRow(
|
||||
context,
|
||||
icon: Icons.shield_outlined,
|
||||
label: 'Equipment Lost',
|
||||
value: '${deathInfo.lostEquipmentCount} items',
|
||||
isNegative: true,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildInfoRow(
|
||||
context,
|
||||
icon: Icons.monetization_on_outlined,
|
||||
label: 'Gold Remaining',
|
||||
value: _formatGold(deathInfo.goldAtDeath),
|
||||
isNegative: false,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(
|
||||
BuildContext context, {
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required String value,
|
||||
required bool isNegative,
|
||||
}) {
|
||||
final theme = Theme.of(context);
|
||||
final valueColor = isNegative
|
||||
? theme.colorScheme.error
|
||||
: theme.colorScheme.primary;
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, size: 18, color: theme.colorScheme.onSurfaceVariant),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
label,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: valueColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _formatGold(int gold) {
|
||||
if (gold >= 1000000) {
|
||||
return '${(gold / 1000000).toStringAsFixed(1)}M';
|
||||
} else if (gold >= 1000) {
|
||||
return '${(gold / 1000).toStringAsFixed(1)}K';
|
||||
}
|
||||
return gold.toString();
|
||||
}
|
||||
|
||||
Widget _buildResurrectButton(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: onResurrect,
|
||||
icon: const Icon(Icons.replay),
|
||||
label: const Text('Resurrect'),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.primary,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user