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:
JiWoong Sul
2025-12-17 17:15:22 +09:00
parent 517bf54a56
commit 21bf057cfc
7 changed files with 974 additions and 3 deletions

View File

@@ -10,6 +10,7 @@ class AutoSaveConfig {
this.onQuestComplete = true,
this.onActComplete = true,
this.onStop = true,
this.onDeath = true,
});
final bool onLevelUp;
@@ -17,10 +18,14 @@ class AutoSaveConfig {
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);
(onActComplete && result.completedAct) ||
(onDeath && result.playerDied);
}
}
@@ -34,6 +39,7 @@ class ProgressLoop {
AutoSaveConfig autoSaveConfig = const AutoSaveConfig(),
DateTime Function()? now,
this.cheatsEnabled = false,
this.onPlayerDied,
}) : _state = initialState,
_tickInterval = tickInterval,
_autoSaveConfig = autoSaveConfig,
@@ -43,6 +49,9 @@ class ProgressLoop {
final ProgressService progressService;
final SaveManager? saveManager;
final Duration _tickInterval;
/// 플레이어 사망 시 콜백 (Phase 4)
final void Function()? onPlayerDied;
final AutoSaveConfig _autoSaveConfig;
final DateTime Function() _now;
final StreamController<GameState> _stateController;
@@ -88,6 +97,11 @@ class ProgressLoop {
/// 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);
@@ -97,6 +111,14 @@ class ProgressLoop {
if (saveManager != null && _autoSaveConfig.shouldSave(result)) {
saveManager!.saveState(_state);
}
// 사망 시 루프 정지 및 콜백 호출 (Phase 4)
if (result.playerDied) {
_timer?.cancel();
_timer = null;
onPlayerDied?.call();
}
return _state;
}

View File

@@ -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/model/combat_state.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/monster_combat_stats.dart';
import 'package:askiineverdie/src/core/model/pq_config.dart';
@@ -19,6 +21,7 @@ class ProgressTickResult {
this.leveledUp = false,
this.completedQuest = false,
this.completedAct = false,
this.playerDied = false,
});
final GameState state;
@@ -26,7 +29,11 @@ class ProgressTickResult {
final bool completedQuest;
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.
@@ -190,6 +197,17 @@ class ProgressService {
);
updatedCombat = combatResult.combat;
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(
@@ -947,4 +965,55 @@ class ProgressService {
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,
);
}
}

View 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,
),
);
}
}

View 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;
}

View File

@@ -22,6 +22,7 @@ class GameState {
ProgressState? progress,
QueueState? queue,
SkillSystemState? skillSystem,
this.deathInfo,
}) : rng = DeterministicRandom.clone(rng),
traits = traits ?? Traits.empty(),
stats = stats ?? Stats.empty(),
@@ -42,6 +43,7 @@ class GameState {
ProgressState? progress,
QueueState? queue,
SkillSystemState? skillSystem,
DeathInfo? deathInfo,
}) {
return GameState(
rng: DeterministicRandom(seed),
@@ -53,6 +55,7 @@ class GameState {
progress: progress,
queue: queue,
skillSystem: skillSystem,
deathInfo: deathInfo,
);
}
@@ -68,6 +71,12 @@ class GameState {
/// 스킬 시스템 상태 (Phase 3)
final SkillSystemState skillSystem;
/// 사망 정보 (Phase 4, null이면 생존 중)
final DeathInfo? deathInfo;
/// 사망 여부
bool get isDead => deathInfo != null;
GameState copyWith({
DeterministicRandom? rng,
Traits? traits,
@@ -78,6 +87,8 @@ class GameState {
ProgressState? progress,
QueueState? queue,
SkillSystemState? skillSystem,
DeathInfo? deathInfo,
bool clearDeathInfo = false,
}) {
return GameState(
rng: rng ?? DeterministicRandom.clone(this.rng),
@@ -89,10 +100,73 @@ class GameState {
progress: progress ?? this.progress,
queue: queue ?? this.queue,
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)
///
/// 스킬 쿨타임, 활성 버프, 게임 경과 시간 등을 관리