289 lines
9.2 KiB
Dart
289 lines
9.2 KiB
Dart
import 'dart:math';
|
|
|
|
import 'package:asciineverdie/data/class_data.dart';
|
|
import 'package:asciineverdie/data/game_text_l10n.dart' as l10n;
|
|
import 'package:asciineverdie/data/race_data.dart';
|
|
import 'package:asciineverdie/src/core/engine/shop_service.dart';
|
|
import 'package:asciineverdie/src/core/model/class_traits.dart';
|
|
import 'package:asciineverdie/src/core/model/equipment_item.dart';
|
|
import 'package:asciineverdie/src/core/model/equipment_slot.dart';
|
|
import 'package:asciineverdie/src/core/model/game_state.dart';
|
|
import 'package:asciineverdie/src/core/model/race_traits.dart';
|
|
|
|
/// 부활 시스템 서비스 (Phase 4)
|
|
///
|
|
/// 사망 처리, 부활 처리, 장비 상실 등을 담당
|
|
class ResurrectionService {
|
|
const ResurrectionService({required this.shopService});
|
|
|
|
final ShopService shopService;
|
|
|
|
// ============================================================================
|
|
// 사망 처리
|
|
// ============================================================================
|
|
|
|
/// 플레이어 사망 처리
|
|
///
|
|
/// 1. 1개의 랜덤 장비 제거 (제물로 바침)
|
|
/// 2. 전투 상태 초기화
|
|
/// 3. 사망 정보 기록
|
|
GameState processDeath({
|
|
required GameState state,
|
|
required String killerName,
|
|
required DeathCause cause,
|
|
}) {
|
|
// 제물로 바칠 아이템 선택 (무기 제외, 장착된 아이템 중 랜덤 1개)
|
|
final equippedItems = <int>[]; // 장착된 아이템의 슬롯 인덱스
|
|
for (var i = 0; i < Equipment.slotCount; i++) {
|
|
final slot = EquipmentSlot.values[i];
|
|
// 무기 슬롯은 제외
|
|
if (slot == EquipmentSlot.weapon) continue;
|
|
|
|
final item = state.equipment.getItemByIndex(i);
|
|
// 빈 슬롯 제외
|
|
if (item.isNotEmpty) {
|
|
equippedItems.add(i);
|
|
}
|
|
}
|
|
|
|
String? lostItemName;
|
|
var newEquipment = state.equipment;
|
|
|
|
if (equippedItems.isNotEmpty) {
|
|
// 랜덤하게 1개 슬롯 선택
|
|
final random = Random();
|
|
final slotIndex = equippedItems[random.nextInt(equippedItems.length)];
|
|
final lostItem = state.equipment.getItemByIndex(slotIndex);
|
|
lostItemName = lostItem.name;
|
|
|
|
// 해당 슬롯만 빈 아이템으로 교체
|
|
final slot = EquipmentSlot.values[slotIndex];
|
|
newEquipment = state.equipment.setItemByIndex(
|
|
slotIndex,
|
|
EquipmentItem.empty(slot),
|
|
);
|
|
}
|
|
|
|
// 사망 정보 생성
|
|
final deathInfo = DeathInfo(
|
|
cause: cause,
|
|
killerName: killerName,
|
|
lostEquipmentCount: lostItemName != null ? 1 : 0,
|
|
lostItemName: lostItemName,
|
|
goldAtDeath: state.inventory.gold,
|
|
levelAtDeath: state.traits.level,
|
|
timestamp: state.skillSystem.elapsedMs,
|
|
);
|
|
|
|
// 전투 상태 초기화
|
|
final progress = state.progress.copyWith(currentCombat: null);
|
|
|
|
return state.copyWith(
|
|
equipment: newEquipment,
|
|
progress: progress,
|
|
deathInfo: deathInfo,
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// 부활 처리
|
|
// ============================================================================
|
|
|
|
/// 플레이어 부활 처리
|
|
///
|
|
/// 1. 골드로 구매 가능한 장비 자동 구매
|
|
/// 2. HP/MP 전체 회복 (장비/종족/클래스 보너스 포함)
|
|
/// 3. 사망 상태 해제
|
|
/// 4. 안전 지역으로 이동 태스크 설정
|
|
GameState processResurrection(GameState state) {
|
|
if (!state.isDead) return state;
|
|
|
|
// 1. 먼저 장비 구매 (HP 계산에 필요)
|
|
final autoBuyResult = shopService.autoBuyForEmptySlots(
|
|
playerLevel: state.traits.level,
|
|
currentGold: state.inventory.gold,
|
|
currentEquipment: state.equipment,
|
|
);
|
|
|
|
// 장비 적용
|
|
var nextState = state.copyWith(
|
|
equipment: autoBuyResult.updatedEquipment,
|
|
inventory: state.inventory.copyWith(gold: autoBuyResult.remainingGold),
|
|
);
|
|
|
|
// 2. 전체 HP/MP 계산 (장비 + 종족 + 클래스 보너스 포함)
|
|
final totalHpMax = _calculateTotalHpMax(nextState);
|
|
final totalMpMax = _calculateTotalMpMax(nextState);
|
|
|
|
// HP/MP 전체 회복
|
|
nextState = nextState.copyWith(
|
|
stats: nextState.stats.copyWith(
|
|
hpCurrent: totalHpMax,
|
|
mpCurrent: totalMpMax,
|
|
),
|
|
clearDeathInfo: true, // 사망 상태 해제
|
|
);
|
|
|
|
// 스킬 쿨타임 초기화
|
|
nextState = nextState.copyWith(
|
|
skillSystem: SkillSystemState.empty().copyWith(
|
|
elapsedMs: nextState.skillSystem.elapsedMs,
|
|
),
|
|
);
|
|
|
|
// 4. 부활 후 태스크 시퀀스 설정 (큐에 추가)
|
|
// 순서: 마을 귀환 → 샵 정비 → 사냥터 이동 → 전투
|
|
final resurrectionQueue = <QueueEntry>[
|
|
QueueEntry(
|
|
kind: QueueKind.task,
|
|
durationMillis: 3000, // 3초
|
|
caption: l10n.taskReturningToTown,
|
|
taskType: TaskType.neutral, // 걷기 애니메이션
|
|
),
|
|
QueueEntry(
|
|
kind: QueueKind.task,
|
|
durationMillis: 3000, // 3초
|
|
caption: l10n.taskRestockingAtShop,
|
|
taskType: TaskType.market, // town 애니메이션
|
|
),
|
|
QueueEntry(
|
|
kind: QueueKind.task,
|
|
durationMillis: 2000, // 2초
|
|
caption: l10n.taskHeadingToHuntingGrounds,
|
|
taskType: TaskType.neutral, // 걷기 애니메이션
|
|
),
|
|
];
|
|
|
|
// 기존 큐 초기화 후 부활 시퀀스만 설정
|
|
// 첫 번째 태스크를 현재 태스크로 설정하여 즉시 표시
|
|
final firstTask = resurrectionQueue.removeAt(0);
|
|
nextState = nextState.copyWith(
|
|
queue: QueueState(
|
|
entries: resurrectionQueue, // 나머지 큐
|
|
),
|
|
// 첫 번째 태스크를 현재 태스크로 직접 설정
|
|
progress: nextState.progress.copyWith(
|
|
currentTask: TaskInfo(
|
|
caption: firstTask.caption,
|
|
type: firstTask.taskType,
|
|
),
|
|
task: ProgressBarState(
|
|
position: 0,
|
|
max: firstTask.durationMillis,
|
|
),
|
|
currentCombat: null, // 전투 상태 명시적 초기화
|
|
),
|
|
);
|
|
|
|
return nextState;
|
|
}
|
|
|
|
/// 장비/종족/클래스 보너스를 포함한 전체 HP 계산
|
|
int _calculateTotalHpMax(GameState state) {
|
|
// 기본 HP + 장비 보너스
|
|
var totalHp = state.stats.hpMax + state.equipment.totalStats.hpBonus;
|
|
|
|
// 종족 HP 보너스 (예: Heap Troll +20%)
|
|
final race = RaceData.findById(state.traits.raceId);
|
|
if (race != null) {
|
|
final raceHpBonus = race.getPassiveValue(PassiveType.hpBonus);
|
|
if (raceHpBonus > 0) {
|
|
totalHp = (totalHp * (1 + raceHpBonus)).round();
|
|
}
|
|
}
|
|
|
|
// 클래스 HP 보너스 (예: Garbage Collector +30%)
|
|
final klass = ClassData.findById(state.traits.classId);
|
|
if (klass != null) {
|
|
final classHpBonus = klass.getPassiveValue(ClassPassiveType.hpBonus);
|
|
if (classHpBonus > 0) {
|
|
totalHp = (totalHp * (1 + classHpBonus)).round();
|
|
}
|
|
}
|
|
|
|
return totalHp;
|
|
}
|
|
|
|
/// 장비/종족/클래스 보너스를 포함한 전체 MP 계산
|
|
int _calculateTotalMpMax(GameState state) {
|
|
// 기본 MP + 장비 보너스
|
|
var totalMp = state.stats.mpMax + state.equipment.totalStats.mpBonus;
|
|
|
|
// 종족 MP 보너스 (예: Pointer Fairy +20%)
|
|
final race = RaceData.findById(state.traits.raceId);
|
|
if (race != null) {
|
|
final raceMpBonus = race.getPassiveValue(PassiveType.mpBonus);
|
|
if (raceMpBonus > 0) {
|
|
totalMp = (totalMp * (1 + raceMpBonus)).round();
|
|
}
|
|
}
|
|
|
|
return totalMp;
|
|
}
|
|
|
|
// ============================================================================
|
|
// 유틸리티
|
|
// ============================================================================
|
|
|
|
/// 부활 비용 계산 (향후 확장용)
|
|
///
|
|
/// 현재는 무료 부활, 향후 비용 도입 가능
|
|
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,
|
|
),
|
|
);
|
|
}
|
|
}
|