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:
@@ -10,6 +10,7 @@ class AutoSaveConfig {
|
|||||||
this.onQuestComplete = true,
|
this.onQuestComplete = true,
|
||||||
this.onActComplete = true,
|
this.onActComplete = true,
|
||||||
this.onStop = true,
|
this.onStop = true,
|
||||||
|
this.onDeath = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
final bool onLevelUp;
|
final bool onLevelUp;
|
||||||
@@ -17,10 +18,14 @@ class AutoSaveConfig {
|
|||||||
final bool onActComplete;
|
final bool onActComplete;
|
||||||
final bool onStop;
|
final bool onStop;
|
||||||
|
|
||||||
|
/// 사망 시 자동 저장 (Phase 4)
|
||||||
|
final bool onDeath;
|
||||||
|
|
||||||
bool shouldSave(ProgressTickResult result) {
|
bool shouldSave(ProgressTickResult result) {
|
||||||
return (onLevelUp && result.leveledUp) ||
|
return (onLevelUp && result.leveledUp) ||
|
||||||
(onQuestComplete && result.completedQuest) ||
|
(onQuestComplete && result.completedQuest) ||
|
||||||
(onActComplete && result.completedAct);
|
(onActComplete && result.completedAct) ||
|
||||||
|
(onDeath && result.playerDied);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,6 +39,7 @@ class ProgressLoop {
|
|||||||
AutoSaveConfig autoSaveConfig = const AutoSaveConfig(),
|
AutoSaveConfig autoSaveConfig = const AutoSaveConfig(),
|
||||||
DateTime Function()? now,
|
DateTime Function()? now,
|
||||||
this.cheatsEnabled = false,
|
this.cheatsEnabled = false,
|
||||||
|
this.onPlayerDied,
|
||||||
}) : _state = initialState,
|
}) : _state = initialState,
|
||||||
_tickInterval = tickInterval,
|
_tickInterval = tickInterval,
|
||||||
_autoSaveConfig = autoSaveConfig,
|
_autoSaveConfig = autoSaveConfig,
|
||||||
@@ -43,6 +49,9 @@ class ProgressLoop {
|
|||||||
final ProgressService progressService;
|
final ProgressService progressService;
|
||||||
final SaveManager? saveManager;
|
final SaveManager? saveManager;
|
||||||
final Duration _tickInterval;
|
final Duration _tickInterval;
|
||||||
|
|
||||||
|
/// 플레이어 사망 시 콜백 (Phase 4)
|
||||||
|
final void Function()? onPlayerDied;
|
||||||
final AutoSaveConfig _autoSaveConfig;
|
final AutoSaveConfig _autoSaveConfig;
|
||||||
final DateTime Function() _now;
|
final DateTime Function() _now;
|
||||||
final StreamController<GameState> _stateController;
|
final StreamController<GameState> _stateController;
|
||||||
@@ -88,6 +97,11 @@ class ProgressLoop {
|
|||||||
|
|
||||||
/// Run one iteration of the loop (used by Timer or manual stepping).
|
/// Run one iteration of the loop (used by Timer or manual stepping).
|
||||||
GameState tickOnce({int? deltaMillis}) {
|
GameState tickOnce({int? deltaMillis}) {
|
||||||
|
// 사망 상태면 틱 진행 안 함 (Phase 4)
|
||||||
|
if (_state.isDead) {
|
||||||
|
return _state;
|
||||||
|
}
|
||||||
|
|
||||||
final baseDelta = deltaMillis ?? _computeDelta();
|
final baseDelta = deltaMillis ?? _computeDelta();
|
||||||
final delta = baseDelta * _speedMultiplier;
|
final delta = baseDelta * _speedMultiplier;
|
||||||
final result = progressService.tick(_state, delta);
|
final result = progressService.tick(_state, delta);
|
||||||
@@ -97,6 +111,14 @@ class ProgressLoop {
|
|||||||
if (saveManager != null && _autoSaveConfig.shouldSave(result)) {
|
if (saveManager != null && _autoSaveConfig.shouldSave(result)) {
|
||||||
saveManager!.saveState(_state);
|
saveManager!.saveState(_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 사망 시 루프 정지 및 콜백 호출 (Phase 4)
|
||||||
|
if (result.playerDied) {
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = null;
|
||||||
|
onPlayerDied?.call();
|
||||||
|
}
|
||||||
|
|
||||||
return _state;
|
return _state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import 'package:askiineverdie/src/core/engine/reward_service.dart';
|
|||||||
import 'package:askiineverdie/src/core/engine/skill_service.dart';
|
import 'package:askiineverdie/src/core/engine/skill_service.dart';
|
||||||
import 'package:askiineverdie/src/core/model/combat_state.dart';
|
import 'package:askiineverdie/src/core/model/combat_state.dart';
|
||||||
import 'package:askiineverdie/src/core/model/combat_stats.dart';
|
import 'package:askiineverdie/src/core/model/combat_stats.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/equipment_item.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/equipment_slot.dart';
|
||||||
import 'package:askiineverdie/src/core/model/game_state.dart';
|
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||||
import 'package:askiineverdie/src/core/model/monster_combat_stats.dart';
|
import 'package:askiineverdie/src/core/model/monster_combat_stats.dart';
|
||||||
import 'package:askiineverdie/src/core/model/pq_config.dart';
|
import 'package:askiineverdie/src/core/model/pq_config.dart';
|
||||||
@@ -19,6 +21,7 @@ class ProgressTickResult {
|
|||||||
this.leveledUp = false,
|
this.leveledUp = false,
|
||||||
this.completedQuest = false,
|
this.completedQuest = false,
|
||||||
this.completedAct = false,
|
this.completedAct = false,
|
||||||
|
this.playerDied = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
final GameState state;
|
final GameState state;
|
||||||
@@ -26,7 +29,11 @@ class ProgressTickResult {
|
|||||||
final bool completedQuest;
|
final bool completedQuest;
|
||||||
final bool completedAct;
|
final bool completedAct;
|
||||||
|
|
||||||
bool get shouldAutosave => leveledUp || completedQuest || completedAct;
|
/// 플레이어 사망 여부 (Phase 4)
|
||||||
|
final bool playerDied;
|
||||||
|
|
||||||
|
bool get shouldAutosave =>
|
||||||
|
leveledUp || completedQuest || completedAct || playerDied;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drives quest/plot/task progression by applying queued actions and rewards.
|
/// Drives quest/plot/task progression by applying queued actions and rewards.
|
||||||
@@ -190,6 +197,17 @@ class ProgressService {
|
|||||||
);
|
);
|
||||||
updatedCombat = combatResult.combat;
|
updatedCombat = combatResult.combat;
|
||||||
updatedSkillSystem = combatResult.skillSystem;
|
updatedSkillSystem = combatResult.skillSystem;
|
||||||
|
|
||||||
|
// Phase 4: 플레이어 사망 체크
|
||||||
|
if (!updatedCombat.playerStats.isAlive) {
|
||||||
|
final monsterName = updatedCombat.monsterStats.name;
|
||||||
|
nextState = _processPlayerDeath(
|
||||||
|
nextState,
|
||||||
|
killerName: monsterName,
|
||||||
|
cause: DeathCause.monster,
|
||||||
|
);
|
||||||
|
return ProgressTickResult(state: nextState, playerDied: true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
progress = progress.copyWith(
|
progress = progress.copyWith(
|
||||||
@@ -947,4 +965,55 @@ class ProgressService {
|
|||||||
skillSystem: updatedSkillSystem,
|
skillSystem: updatedSkillSystem,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 플레이어 사망 처리 (Phase 4)
|
||||||
|
///
|
||||||
|
/// 모든 장비 상실 및 사망 정보 기록
|
||||||
|
GameState _processPlayerDeath(
|
||||||
|
GameState state, {
|
||||||
|
required String killerName,
|
||||||
|
required DeathCause cause,
|
||||||
|
}) {
|
||||||
|
// 상실할 장비 개수 계산
|
||||||
|
final lostCount = state.equipment.equippedItems.length;
|
||||||
|
|
||||||
|
// 빈 장비 생성 (기본 무기만 유지)
|
||||||
|
final emptyEquipment = Equipment(
|
||||||
|
items: [
|
||||||
|
EquipmentItem.defaultWeapon(),
|
||||||
|
EquipmentItem.empty(EquipmentSlot.shield),
|
||||||
|
EquipmentItem.empty(EquipmentSlot.helm),
|
||||||
|
EquipmentItem.empty(EquipmentSlot.hauberk),
|
||||||
|
EquipmentItem.empty(EquipmentSlot.brassairts),
|
||||||
|
EquipmentItem.empty(EquipmentSlot.vambraces),
|
||||||
|
EquipmentItem.empty(EquipmentSlot.gauntlets),
|
||||||
|
EquipmentItem.empty(EquipmentSlot.gambeson),
|
||||||
|
EquipmentItem.empty(EquipmentSlot.cuisses),
|
||||||
|
EquipmentItem.empty(EquipmentSlot.greaves),
|
||||||
|
EquipmentItem.empty(EquipmentSlot.sollerets),
|
||||||
|
],
|
||||||
|
bestIndex: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 사망 정보 생성
|
||||||
|
final deathInfo = DeathInfo(
|
||||||
|
cause: cause,
|
||||||
|
killerName: killerName,
|
||||||
|
lostEquipmentCount: lostCount,
|
||||||
|
goldAtDeath: state.inventory.gold,
|
||||||
|
levelAtDeath: state.traits.level,
|
||||||
|
timestamp: state.skillSystem.elapsedMs,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 전투 상태 초기화
|
||||||
|
final progress = state.progress.copyWith(
|
||||||
|
currentCombat: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
return state.copyWith(
|
||||||
|
equipment: emptyEquipment,
|
||||||
|
progress: progress,
|
||||||
|
deathInfo: deathInfo,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
182
lib/src/core/engine/resurrection_service.dart
Normal file
182
lib/src/core/engine/resurrection_service.dart
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
import 'package:askiineverdie/src/core/engine/shop_service.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/equipment_item.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/equipment_slot.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||||
|
|
||||||
|
/// 부활 시스템 서비스 (Phase 4)
|
||||||
|
///
|
||||||
|
/// 사망 처리, 부활 처리, 장비 상실 등을 담당
|
||||||
|
class ResurrectionService {
|
||||||
|
const ResurrectionService({required this.shopService});
|
||||||
|
|
||||||
|
final ShopService shopService;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 사망 처리
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// 플레이어 사망 처리
|
||||||
|
///
|
||||||
|
/// 1. 모든 장비 제거 (인벤토리로 이동하지 않음 - 상실)
|
||||||
|
/// 2. 전투 상태 초기화
|
||||||
|
/// 3. 사망 정보 기록
|
||||||
|
GameState processDeath({
|
||||||
|
required GameState state,
|
||||||
|
required String killerName,
|
||||||
|
required DeathCause cause,
|
||||||
|
}) {
|
||||||
|
// 상실할 장비 개수 계산
|
||||||
|
final lostCount = state.equipment.equippedItems.length;
|
||||||
|
|
||||||
|
// 빈 장비 생성 (기본 무기만 유지)
|
||||||
|
final emptyEquipment = Equipment(
|
||||||
|
items: [
|
||||||
|
EquipmentItem.defaultWeapon(), // 무기 슬롯에 기본 Keyboard
|
||||||
|
EquipmentItem.empty(EquipmentSlot.shield),
|
||||||
|
EquipmentItem.empty(EquipmentSlot.helm),
|
||||||
|
EquipmentItem.empty(EquipmentSlot.hauberk),
|
||||||
|
EquipmentItem.empty(EquipmentSlot.brassairts),
|
||||||
|
EquipmentItem.empty(EquipmentSlot.vambraces),
|
||||||
|
EquipmentItem.empty(EquipmentSlot.gauntlets),
|
||||||
|
EquipmentItem.empty(EquipmentSlot.gambeson),
|
||||||
|
EquipmentItem.empty(EquipmentSlot.cuisses),
|
||||||
|
EquipmentItem.empty(EquipmentSlot.greaves),
|
||||||
|
EquipmentItem.empty(EquipmentSlot.sollerets),
|
||||||
|
],
|
||||||
|
bestIndex: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 사망 정보 생성
|
||||||
|
final deathInfo = DeathInfo(
|
||||||
|
cause: cause,
|
||||||
|
killerName: killerName,
|
||||||
|
lostEquipmentCount: lostCount,
|
||||||
|
goldAtDeath: state.inventory.gold,
|
||||||
|
levelAtDeath: state.traits.level,
|
||||||
|
timestamp: state.skillSystem.elapsedMs,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 전투 상태 초기화
|
||||||
|
final progress = state.progress.copyWith(
|
||||||
|
currentCombat: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
return state.copyWith(
|
||||||
|
equipment: emptyEquipment,
|
||||||
|
progress: progress,
|
||||||
|
deathInfo: deathInfo,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 부활 처리
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// 플레이어 부활 처리
|
||||||
|
///
|
||||||
|
/// 1. HP/MP 전체 회복
|
||||||
|
/// 2. 골드로 구매 가능한 장비 자동 구매
|
||||||
|
/// 3. 사망 상태 해제
|
||||||
|
/// 4. 안전 지역으로 이동 태스크 설정
|
||||||
|
GameState processResurrection(GameState state) {
|
||||||
|
if (!state.isDead) return state;
|
||||||
|
|
||||||
|
// HP/MP 전체 회복
|
||||||
|
var nextState = state.copyWith(
|
||||||
|
stats: state.stats.copyWith(
|
||||||
|
hpCurrent: state.stats.hpMax,
|
||||||
|
mpCurrent: state.stats.mpMax,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 빈 슬롯에 자동 장비 구매
|
||||||
|
final autoBuyResult = shopService.autoBuyForEmptySlots(
|
||||||
|
playerLevel: nextState.traits.level,
|
||||||
|
currentGold: nextState.inventory.gold,
|
||||||
|
currentEquipment: nextState.equipment,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 결과 적용
|
||||||
|
nextState = nextState.copyWith(
|
||||||
|
equipment: autoBuyResult.updatedEquipment,
|
||||||
|
inventory: nextState.inventory.copyWith(
|
||||||
|
gold: autoBuyResult.remainingGold,
|
||||||
|
),
|
||||||
|
clearDeathInfo: true, // 사망 상태 해제
|
||||||
|
);
|
||||||
|
|
||||||
|
// 스킬 쿨타임 초기화
|
||||||
|
nextState = nextState.copyWith(
|
||||||
|
skillSystem: SkillSystemState.empty().copyWith(
|
||||||
|
elapsedMs: nextState.skillSystem.elapsedMs,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return nextState;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 유틸리티
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// 부활 비용 계산 (향후 확장용)
|
||||||
|
///
|
||||||
|
/// 현재는 무료 부활, 향후 비용 도입 가능
|
||||||
|
int calculateResurrectionCost(int playerLevel) {
|
||||||
|
// 기본: 무료
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 부활 가능 여부 확인
|
||||||
|
bool canResurrect(GameState state) {
|
||||||
|
if (!state.isDead) return false;
|
||||||
|
|
||||||
|
final cost = calculateResurrectionCost(state.traits.level);
|
||||||
|
return state.inventory.gold >= cost;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 장비 보존 아이템 적용 (향후 확장용)
|
||||||
|
///
|
||||||
|
/// [protectedSlots] 보존할 슬롯 인덱스 목록
|
||||||
|
GameState processDeathWithProtection({
|
||||||
|
required GameState state,
|
||||||
|
required String killerName,
|
||||||
|
required DeathCause cause,
|
||||||
|
required List<int> protectedSlots,
|
||||||
|
}) {
|
||||||
|
// 보존할 아이템 추출
|
||||||
|
final protectedItems = <int, EquipmentItem>{};
|
||||||
|
for (final slotIndex in protectedSlots) {
|
||||||
|
if (slotIndex >= 0 && slotIndex < Equipment.slotCount) {
|
||||||
|
final item = state.equipment.getItemByIndex(slotIndex);
|
||||||
|
if (item.isNotEmpty) {
|
||||||
|
protectedItems[slotIndex] = item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기본 사망 처리
|
||||||
|
var nextState = processDeath(
|
||||||
|
state: state,
|
||||||
|
killerName: killerName,
|
||||||
|
cause: cause,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 보존된 아이템 복원
|
||||||
|
var equipment = nextState.equipment;
|
||||||
|
for (final entry in protectedItems.entries) {
|
||||||
|
equipment = equipment.setItemByIndex(entry.key, entry.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 상실 개수 재계산
|
||||||
|
final actualLostCount =
|
||||||
|
state.equipment.equippedItems.length - protectedItems.length;
|
||||||
|
|
||||||
|
return nextState.copyWith(
|
||||||
|
equipment: equipment,
|
||||||
|
deathInfo: nextState.deathInfo?.copyWith(
|
||||||
|
lostEquipmentCount: actualLostCount,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
333
lib/src/core/engine/shop_service.dart
Normal file
333
lib/src/core/engine/shop_service.dart
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
import 'package:askiineverdie/src/core/model/equipment_item.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/equipment_slot.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/item_stats.dart';
|
||||||
|
import 'package:askiineverdie/src/core/util/deterministic_random.dart';
|
||||||
|
|
||||||
|
/// 샵 시스템 서비스 (Phase 4)
|
||||||
|
///
|
||||||
|
/// 장비 구매, 판매, 자동 장착 등을 담당
|
||||||
|
class ShopService {
|
||||||
|
const ShopService({required this.rng});
|
||||||
|
|
||||||
|
final DeterministicRandom rng;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 가격 계산
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// 장비 구매 가격 계산
|
||||||
|
///
|
||||||
|
/// 가격 = 아이템 레벨 * 50 * 희귀도 배율
|
||||||
|
int calculateBuyPrice(EquipmentItem item) {
|
||||||
|
if (item.isEmpty) return 0;
|
||||||
|
return (item.level * 50 * _getRarityPriceMultiplier(item.rarity)).round();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 장비 판매 가격 계산
|
||||||
|
///
|
||||||
|
/// 판매 가격 = 구매 가격 * 0.3
|
||||||
|
int calculateSellPrice(EquipmentItem item) {
|
||||||
|
return (calculateBuyPrice(item) * 0.3).round();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 희귀도별 가격 배율 (Phase 4 문서 기준)
|
||||||
|
double _getRarityPriceMultiplier(ItemRarity rarity) {
|
||||||
|
return switch (rarity) {
|
||||||
|
ItemRarity.common => 1.0,
|
||||||
|
ItemRarity.uncommon => 2.0,
|
||||||
|
ItemRarity.rare => 5.0,
|
||||||
|
ItemRarity.epic => 15.0,
|
||||||
|
ItemRarity.legendary => 50.0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 장비 생성
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// 특정 슬롯의 샵 장비 생성
|
||||||
|
///
|
||||||
|
/// [playerLevel] 플레이어 레벨 (장비 레벨 범위 결정)
|
||||||
|
/// [slot] 장비 슬롯
|
||||||
|
/// [targetRarity] 목표 희귀도 (null이면 랜덤)
|
||||||
|
EquipmentItem generateShopItem({
|
||||||
|
required int playerLevel,
|
||||||
|
required EquipmentSlot slot,
|
||||||
|
ItemRarity? targetRarity,
|
||||||
|
}) {
|
||||||
|
// 아이템 레벨: 플레이어 레벨 기준 ±2
|
||||||
|
final minLevel = (playerLevel - 2).clamp(1, 999);
|
||||||
|
final maxLevel = playerLevel + 2;
|
||||||
|
final itemLevel = minLevel + rng.nextInt(maxLevel - minLevel + 1);
|
||||||
|
|
||||||
|
// 희귀도 결정 (샵 아이템은 Common~Rare만)
|
||||||
|
final rarity = targetRarity ?? _rollShopRarity();
|
||||||
|
|
||||||
|
// 아이템 이름 생성
|
||||||
|
final name = _generateItemName(slot, rarity, itemLevel);
|
||||||
|
|
||||||
|
// 스탯 생성
|
||||||
|
final stats = _generateItemStats(slot, itemLevel, rarity);
|
||||||
|
|
||||||
|
// 무게 계산
|
||||||
|
final weight = _calculateWeight(slot, itemLevel);
|
||||||
|
|
||||||
|
return EquipmentItem(
|
||||||
|
name: name,
|
||||||
|
slot: slot,
|
||||||
|
level: itemLevel,
|
||||||
|
weight: weight,
|
||||||
|
stats: stats,
|
||||||
|
rarity: rarity,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 샵 희귀도 롤 (Common 70%, Uncommon 25%, Rare 5%)
|
||||||
|
ItemRarity _rollShopRarity() {
|
||||||
|
final roll = rng.nextInt(100);
|
||||||
|
if (roll < 70) return ItemRarity.common;
|
||||||
|
if (roll < 95) return ItemRarity.uncommon;
|
||||||
|
return ItemRarity.rare;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 슬롯별 아이템 이름 생성
|
||||||
|
String _generateItemName(EquipmentSlot slot, ItemRarity rarity, int level) {
|
||||||
|
final prefix = _getRarityPrefix(rarity);
|
||||||
|
final baseName = _getSlotBaseName(slot);
|
||||||
|
final suffix = level > 10 ? ' +${level ~/ 10}' : '';
|
||||||
|
return '$prefix$baseName$suffix'.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getRarityPrefix(ItemRarity rarity) {
|
||||||
|
return switch (rarity) {
|
||||||
|
ItemRarity.common => '',
|
||||||
|
ItemRarity.uncommon => 'Fine ',
|
||||||
|
ItemRarity.rare => 'Superior ',
|
||||||
|
ItemRarity.epic => 'Epic ',
|
||||||
|
ItemRarity.legendary => 'Legendary ',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getSlotBaseName(EquipmentSlot slot) {
|
||||||
|
return switch (slot) {
|
||||||
|
EquipmentSlot.weapon => 'Keyboard',
|
||||||
|
EquipmentSlot.shield => 'Firewall Shield',
|
||||||
|
EquipmentSlot.helm => 'Neural Headset',
|
||||||
|
EquipmentSlot.hauberk => 'Server Rack Armor',
|
||||||
|
EquipmentSlot.brassairts => 'Cable Brassairts',
|
||||||
|
EquipmentSlot.vambraces => 'USB Vambraces',
|
||||||
|
EquipmentSlot.gauntlets => 'Typing Gauntlets',
|
||||||
|
EquipmentSlot.gambeson => 'Padded Gambeson',
|
||||||
|
EquipmentSlot.cuisses => 'Circuit Cuisses',
|
||||||
|
EquipmentSlot.greaves => 'Copper Greaves',
|
||||||
|
EquipmentSlot.sollerets => 'Static Boots',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 슬롯과 레벨에 따른 스탯 생성
|
||||||
|
ItemStats _generateItemStats(EquipmentSlot slot, int level, ItemRarity rarity) {
|
||||||
|
final multiplier = rarity.multiplier;
|
||||||
|
final baseValue = (level * multiplier).round();
|
||||||
|
|
||||||
|
return switch (slot) {
|
||||||
|
EquipmentSlot.weapon => ItemStats(
|
||||||
|
atk: baseValue * 2,
|
||||||
|
criRate: 0.01 * (level ~/ 5),
|
||||||
|
parryRate: 0.005 * level,
|
||||||
|
),
|
||||||
|
EquipmentSlot.shield => ItemStats(
|
||||||
|
def: baseValue,
|
||||||
|
blockRate: 0.02 * (level ~/ 3).clamp(1, 10),
|
||||||
|
),
|
||||||
|
EquipmentSlot.helm => ItemStats(
|
||||||
|
def: baseValue ~/ 2,
|
||||||
|
magDef: baseValue ~/ 2,
|
||||||
|
intBonus: level ~/ 10,
|
||||||
|
),
|
||||||
|
EquipmentSlot.hauberk => ItemStats(
|
||||||
|
def: baseValue,
|
||||||
|
hpBonus: level * 2,
|
||||||
|
),
|
||||||
|
EquipmentSlot.brassairts => ItemStats(
|
||||||
|
def: baseValue ~/ 2,
|
||||||
|
strBonus: level ~/ 15,
|
||||||
|
),
|
||||||
|
EquipmentSlot.vambraces => ItemStats(
|
||||||
|
def: baseValue ~/ 2,
|
||||||
|
dexBonus: level ~/ 15,
|
||||||
|
),
|
||||||
|
EquipmentSlot.gauntlets => ItemStats(
|
||||||
|
atk: baseValue ~/ 2,
|
||||||
|
def: baseValue ~/ 4,
|
||||||
|
),
|
||||||
|
EquipmentSlot.gambeson => ItemStats(
|
||||||
|
def: baseValue ~/ 2,
|
||||||
|
conBonus: level ~/ 15,
|
||||||
|
),
|
||||||
|
EquipmentSlot.cuisses => ItemStats(
|
||||||
|
def: baseValue ~/ 2,
|
||||||
|
evasion: 0.005 * level,
|
||||||
|
),
|
||||||
|
EquipmentSlot.greaves => ItemStats(
|
||||||
|
def: baseValue ~/ 2,
|
||||||
|
evasion: 0.003 * level,
|
||||||
|
),
|
||||||
|
EquipmentSlot.sollerets => ItemStats(
|
||||||
|
def: baseValue ~/ 3,
|
||||||
|
evasion: 0.002 * level,
|
||||||
|
dexBonus: level ~/ 20,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 무게 계산
|
||||||
|
int _calculateWeight(EquipmentSlot slot, int level) {
|
||||||
|
final baseWeight = switch (slot) {
|
||||||
|
EquipmentSlot.weapon => 5,
|
||||||
|
EquipmentSlot.shield => 8,
|
||||||
|
EquipmentSlot.helm => 4,
|
||||||
|
EquipmentSlot.hauberk => 15,
|
||||||
|
EquipmentSlot.brassairts => 3,
|
||||||
|
EquipmentSlot.vambraces => 3,
|
||||||
|
EquipmentSlot.gauntlets => 2,
|
||||||
|
EquipmentSlot.gambeson => 6,
|
||||||
|
EquipmentSlot.cuisses => 5,
|
||||||
|
EquipmentSlot.greaves => 4,
|
||||||
|
EquipmentSlot.sollerets => 3,
|
||||||
|
};
|
||||||
|
return baseWeight + (level ~/ 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 자동 구매
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// 빈 슬롯에 대해 자동으로 장비 구매
|
||||||
|
///
|
||||||
|
/// Returns: 구매 결과
|
||||||
|
AutoBuyResult autoBuyForEmptySlots({
|
||||||
|
required int playerLevel,
|
||||||
|
required int currentGold,
|
||||||
|
required Equipment currentEquipment,
|
||||||
|
}) {
|
||||||
|
var remainingGold = currentGold;
|
||||||
|
final purchasedItems = <EquipmentItem>[];
|
||||||
|
var updatedEquipment = currentEquipment;
|
||||||
|
|
||||||
|
// 각 슬롯 순회하며 빈 슬롯에 구매
|
||||||
|
for (var i = 0; i < Equipment.slotCount; i++) {
|
||||||
|
final currentItem = currentEquipment.getItemByIndex(i);
|
||||||
|
if (currentItem.isNotEmpty) continue;
|
||||||
|
|
||||||
|
final slot = EquipmentSlot.values[i];
|
||||||
|
|
||||||
|
// 구매 가능한 아이템 생성 시도
|
||||||
|
final shopItem = generateShopItem(
|
||||||
|
playerLevel: playerLevel,
|
||||||
|
slot: slot,
|
||||||
|
targetRarity: ItemRarity.common, // 부활 시 Common만 구매
|
||||||
|
);
|
||||||
|
|
||||||
|
final price = calculateBuyPrice(shopItem);
|
||||||
|
if (price <= remainingGold) {
|
||||||
|
remainingGold -= price;
|
||||||
|
purchasedItems.add(shopItem);
|
||||||
|
updatedEquipment = updatedEquipment.setItemByIndex(i, shopItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AutoBuyResult(
|
||||||
|
purchasedItems: purchasedItems,
|
||||||
|
totalCost: currentGold - remainingGold,
|
||||||
|
remainingGold: remainingGold,
|
||||||
|
updatedEquipment: updatedEquipment,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 특정 슬롯에 장비 구매
|
||||||
|
///
|
||||||
|
/// Returns: 구매 성공 시 결과, 실패 시 null
|
||||||
|
PurchaseResult? buyItem({
|
||||||
|
required int playerLevel,
|
||||||
|
required int currentGold,
|
||||||
|
required EquipmentSlot slot,
|
||||||
|
ItemRarity? preferredRarity,
|
||||||
|
}) {
|
||||||
|
final item = generateShopItem(
|
||||||
|
playerLevel: playerLevel,
|
||||||
|
slot: slot,
|
||||||
|
targetRarity: preferredRarity,
|
||||||
|
);
|
||||||
|
|
||||||
|
final price = calculateBuyPrice(item);
|
||||||
|
if (price > currentGold) return null;
|
||||||
|
|
||||||
|
return PurchaseResult(
|
||||||
|
item: item,
|
||||||
|
price: price,
|
||||||
|
remainingGold: currentGold - price,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 장비 판매
|
||||||
|
SellResult sellItem(EquipmentItem item, int currentGold) {
|
||||||
|
final price = calculateSellPrice(item);
|
||||||
|
return SellResult(
|
||||||
|
item: item,
|
||||||
|
price: price,
|
||||||
|
newGold: currentGold + price,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 자동 구매 결과
|
||||||
|
class AutoBuyResult {
|
||||||
|
const AutoBuyResult({
|
||||||
|
required this.purchasedItems,
|
||||||
|
required this.totalCost,
|
||||||
|
required this.remainingGold,
|
||||||
|
required this.updatedEquipment,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 구매한 아이템 목록
|
||||||
|
final List<EquipmentItem> purchasedItems;
|
||||||
|
|
||||||
|
/// 총 비용
|
||||||
|
final int totalCost;
|
||||||
|
|
||||||
|
/// 남은 골드
|
||||||
|
final int remainingGold;
|
||||||
|
|
||||||
|
/// 업데이트된 장비
|
||||||
|
final Equipment updatedEquipment;
|
||||||
|
|
||||||
|
/// 구매한 아이템 개수
|
||||||
|
int get purchasedCount => purchasedItems.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 단일 구매 결과
|
||||||
|
class PurchaseResult {
|
||||||
|
const PurchaseResult({
|
||||||
|
required this.item,
|
||||||
|
required this.price,
|
||||||
|
required this.remainingGold,
|
||||||
|
});
|
||||||
|
|
||||||
|
final EquipmentItem item;
|
||||||
|
final int price;
|
||||||
|
final int remainingGold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 판매 결과
|
||||||
|
class SellResult {
|
||||||
|
const SellResult({
|
||||||
|
required this.item,
|
||||||
|
required this.price,
|
||||||
|
required this.newGold,
|
||||||
|
});
|
||||||
|
|
||||||
|
final EquipmentItem item;
|
||||||
|
final int price;
|
||||||
|
final int newGold;
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ class GameState {
|
|||||||
ProgressState? progress,
|
ProgressState? progress,
|
||||||
QueueState? queue,
|
QueueState? queue,
|
||||||
SkillSystemState? skillSystem,
|
SkillSystemState? skillSystem,
|
||||||
|
this.deathInfo,
|
||||||
}) : rng = DeterministicRandom.clone(rng),
|
}) : rng = DeterministicRandom.clone(rng),
|
||||||
traits = traits ?? Traits.empty(),
|
traits = traits ?? Traits.empty(),
|
||||||
stats = stats ?? Stats.empty(),
|
stats = stats ?? Stats.empty(),
|
||||||
@@ -42,6 +43,7 @@ class GameState {
|
|||||||
ProgressState? progress,
|
ProgressState? progress,
|
||||||
QueueState? queue,
|
QueueState? queue,
|
||||||
SkillSystemState? skillSystem,
|
SkillSystemState? skillSystem,
|
||||||
|
DeathInfo? deathInfo,
|
||||||
}) {
|
}) {
|
||||||
return GameState(
|
return GameState(
|
||||||
rng: DeterministicRandom(seed),
|
rng: DeterministicRandom(seed),
|
||||||
@@ -53,6 +55,7 @@ class GameState {
|
|||||||
progress: progress,
|
progress: progress,
|
||||||
queue: queue,
|
queue: queue,
|
||||||
skillSystem: skillSystem,
|
skillSystem: skillSystem,
|
||||||
|
deathInfo: deathInfo,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +71,12 @@ class GameState {
|
|||||||
/// 스킬 시스템 상태 (Phase 3)
|
/// 스킬 시스템 상태 (Phase 3)
|
||||||
final SkillSystemState skillSystem;
|
final SkillSystemState skillSystem;
|
||||||
|
|
||||||
|
/// 사망 정보 (Phase 4, null이면 생존 중)
|
||||||
|
final DeathInfo? deathInfo;
|
||||||
|
|
||||||
|
/// 사망 여부
|
||||||
|
bool get isDead => deathInfo != null;
|
||||||
|
|
||||||
GameState copyWith({
|
GameState copyWith({
|
||||||
DeterministicRandom? rng,
|
DeterministicRandom? rng,
|
||||||
Traits? traits,
|
Traits? traits,
|
||||||
@@ -78,6 +87,8 @@ class GameState {
|
|||||||
ProgressState? progress,
|
ProgressState? progress,
|
||||||
QueueState? queue,
|
QueueState? queue,
|
||||||
SkillSystemState? skillSystem,
|
SkillSystemState? skillSystem,
|
||||||
|
DeathInfo? deathInfo,
|
||||||
|
bool clearDeathInfo = false,
|
||||||
}) {
|
}) {
|
||||||
return GameState(
|
return GameState(
|
||||||
rng: rng ?? DeterministicRandom.clone(this.rng),
|
rng: rng ?? DeterministicRandom.clone(this.rng),
|
||||||
@@ -89,10 +100,73 @@ class GameState {
|
|||||||
progress: progress ?? this.progress,
|
progress: progress ?? this.progress,
|
||||||
queue: queue ?? this.queue,
|
queue: queue ?? this.queue,
|
||||||
skillSystem: skillSystem ?? this.skillSystem,
|
skillSystem: skillSystem ?? this.skillSystem,
|
||||||
|
deathInfo: clearDeathInfo ? null : (deathInfo ?? this.deathInfo),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 사망 정보 (Phase 4)
|
||||||
|
///
|
||||||
|
/// 사망 시점의 정보와 상실한 장비 목록을 기록
|
||||||
|
class DeathInfo {
|
||||||
|
const DeathInfo({
|
||||||
|
required this.cause,
|
||||||
|
required this.killerName,
|
||||||
|
required this.lostEquipmentCount,
|
||||||
|
required this.goldAtDeath,
|
||||||
|
required this.levelAtDeath,
|
||||||
|
required this.timestamp,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 사망 원인
|
||||||
|
final DeathCause cause;
|
||||||
|
|
||||||
|
/// 사망시킨 몬스터/원인 이름
|
||||||
|
final String killerName;
|
||||||
|
|
||||||
|
/// 상실한 장비 개수
|
||||||
|
final int lostEquipmentCount;
|
||||||
|
|
||||||
|
/// 사망 시점 골드
|
||||||
|
final int goldAtDeath;
|
||||||
|
|
||||||
|
/// 사망 시점 레벨
|
||||||
|
final int levelAtDeath;
|
||||||
|
|
||||||
|
/// 사망 시각 (밀리초)
|
||||||
|
final int timestamp;
|
||||||
|
|
||||||
|
DeathInfo copyWith({
|
||||||
|
DeathCause? cause,
|
||||||
|
String? killerName,
|
||||||
|
int? lostEquipmentCount,
|
||||||
|
int? goldAtDeath,
|
||||||
|
int? levelAtDeath,
|
||||||
|
int? timestamp,
|
||||||
|
}) {
|
||||||
|
return DeathInfo(
|
||||||
|
cause: cause ?? this.cause,
|
||||||
|
killerName: killerName ?? this.killerName,
|
||||||
|
lostEquipmentCount: lostEquipmentCount ?? this.lostEquipmentCount,
|
||||||
|
goldAtDeath: goldAtDeath ?? this.goldAtDeath,
|
||||||
|
levelAtDeath: levelAtDeath ?? this.levelAtDeath,
|
||||||
|
timestamp: timestamp ?? this.timestamp,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 사망 원인
|
||||||
|
enum DeathCause {
|
||||||
|
/// 몬스터에 의한 사망
|
||||||
|
monster,
|
||||||
|
|
||||||
|
/// 자해 스킬에 의한 사망
|
||||||
|
selfDamage,
|
||||||
|
|
||||||
|
/// 환경 피해에 의한 사망
|
||||||
|
environment,
|
||||||
|
}
|
||||||
|
|
||||||
/// 스킬 시스템 상태 (Phase 3)
|
/// 스킬 시스템 상태 (Phase 3)
|
||||||
///
|
///
|
||||||
/// 스킬 쿨타임, 활성 버프, 게임 경과 시간 등을 관리
|
/// 스킬 쿨타임, 활성 버프, 게임 경과 시간 등을 관리
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:askiineverdie/src/core/engine/progress_loop.dart';
|
import 'package:askiineverdie/src/core/engine/progress_loop.dart';
|
||||||
import 'package:askiineverdie/src/core/engine/progress_service.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/model/game_state.dart';
|
||||||
import 'package:askiineverdie/src/core/storage/save_manager.dart';
|
import 'package:askiineverdie/src/core/storage/save_manager.dart';
|
||||||
import 'package:flutter/foundation.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.
|
/// Presentation-friendly wrapper that owns ProgressLoop and SaveManager.
|
||||||
class GameSessionController extends ChangeNotifier {
|
class GameSessionController extends ChangeNotifier {
|
||||||
@@ -68,6 +70,7 @@ class GameSessionController extends ChangeNotifier {
|
|||||||
tickInterval: _tickInterval,
|
tickInterval: _tickInterval,
|
||||||
now: _now,
|
now: _now,
|
||||||
cheatsEnabled: cheatsEnabled,
|
cheatsEnabled: cheatsEnabled,
|
||||||
|
onPlayerDied: _onPlayerDied,
|
||||||
);
|
);
|
||||||
|
|
||||||
_subscription = _loop!.stream.listen((next) {
|
_subscription = _loop!.stream.listen((next) {
|
||||||
@@ -138,4 +141,36 @@ class GameSessionController extends ChangeNotifier {
|
|||||||
if (loop == null) return null;
|
if (loop == null) return null;
|
||||||
return loop.stop(saveOnStop: saveOnStop);
|
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